From 345e1e513f3887f8817599a16f7aa586de156d87 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 9 Jul 2024 22:43:18 +0200 Subject: [PATCH 001/136] Add spar integration test about empty scim search results. (#4132) --- .../test-integration/Test/Spar/Scim/UserSpec.hs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index c5d6ae7f436..5ead460ad3d 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -1152,11 +1152,13 @@ specListUsers = describe "GET /Users" $ do it "lists all SCIM users in a team" $ testListProvisionedUsers context "1 SAML IdP" $ do it "finds a SCIM-provisioned user by userName or externalId" $ testFindProvisionedUser + it "returns an empty list of SCIM-provisioned users if user not found (userName, externalId)" $ testDoNotFindProvisionedUser True it "finds a user autoprovisioned via saml by externalId via email" $ testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO it "finds a user invited via team settings by externalId via email" $ testFindTeamSettingsInvitedUserMigratedWithEmailInTeamWithSSO it "finds a user invited via team settings by UserId" $ testFindTeamSettingsInvitedUserMigratedWithEmailInTeamWithSSOViaUserId context "0 SAML IdP" $ do it "finds a SCIM-provisioned user by userName or externalId" $ testFindProvisionedUserNoIdP + it "returns an empty list of SCIM-provisioned users if user not found (userName, externalId)" $ testDoNotFindProvisionedUser False it "finds a non-SCIM-provisioned user by userName" $ testFindNonProvisionedUserNoIdP FindByHandle it "finds a non-SCIM-provisioned user by externalId" $ testFindNonProvisionedUserNoIdP FindByExternalId it "finds a non-SCIM-provisioned user by UserId" $ testFindNonProvisionedUserNoIdP GetByUserId @@ -1188,6 +1190,21 @@ testFindProvisionedUser = do users' <- listUsers tok (Just (filterBy "externalId" externalId)) liftIO $ users' `shouldBe` [storedUser] +testDoNotFindProvisionedUser :: Bool -> TestSpar () +testDoNotFindProvisionedUser hasSaml = do + tok <- + if hasSaml + then registerIdPAndScimToken <&> fst + else do + env <- ask + (_owner, teamid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + registerScimToken teamid Nothing + byName <- listUsers tok (Just (filterBy "userName" "6861f068-3dc7-11ef-9bc2-73f612ae094d")) + byEmail <- listUsers tok (Just (filterBy "externalId" "6861f068-3dc7-11ef-9bc2-73f612ae094d")) + liftIO $ do + byName `shouldBe` [] + byEmail `shouldBe` [] + -- The user is migrated by using the email as the externalId testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO :: TestSpar () testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO = do From 71a30aba2ed84efe0f6b54a7db1b7e93e294a9bc Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Fri, 12 Jul 2024 09:51:56 +0200 Subject: [PATCH 002/136] allow subconversations for MLS 1-1 conversation (#4133) * allow subconversations for MLS 1-1 conversation * add changelog entry --- changelog.d/2-features/WPB-9773 | 1 + integration/test/Test/MLS/SubConversation.hs | 22 +++++++++++++++++++ .../src/Galley/API/MLS/SubConversation.hs | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 changelog.d/2-features/WPB-9773 diff --git a/changelog.d/2-features/WPB-9773 b/changelog.d/2-features/WPB-9773 new file mode 100644 index 00000000000..e7f45204eb3 --- /dev/null +++ b/changelog.d/2-features/WPB-9773 @@ -0,0 +1 @@ +allow subconversations for MLS 1-1 conversations diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 11dfdc4e7da..7cacebea70f 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -30,6 +30,28 @@ testJoinSubConv = do $ createExternalCommit 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] + conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetGroup alice1 conv + + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + createSubConv bob1 "conference" + + -- bob adds his first client to the subconversation + sub' <- getSubConversation bob conv "conference" >>= getJSON 200 + 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 + >>= sendAndConsumeCommitBundle + testDeleteParentOfSubConv :: (HasCallStack) => Domain -> App () testDeleteParentOfSubConv secondDomain = do (alice, tid, _) <- createTeam OwnDomain 1 diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index c5c57889e1a..781cacc7769 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -115,7 +115,7 @@ getLocalSubConversation :: getLocalSubConversation qusr lconv sconv = do c <- getConversationAndCheckMembership qusr lconv - unless (Data.convType c == RegularConv) $ + unless (Data.convType c == RegularConv || Data.convType c == One2OneConv) $ throwS @'MLSSubConvUnsupportedConvType msub <- Eff.getSubConversation (tUnqualified lconv) sconv From dab374bd4d4bb9e96fdba37b4d2084a43a3f7678 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 15 Jul 2024 09:12:31 +0200 Subject: [PATCH 003/136] Concurrently fetch user profiles from the DB (#4140) --- changelog.d/5-internal/optimize-list-users | 1 + .../src/Wire/UserSubsystem/Interpreter.hs | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 changelog.d/5-internal/optimize-list-users diff --git a/changelog.d/5-internal/optimize-list-users b/changelog.d/5-internal/optimize-list-users new file mode 100644 index 00000000000..b2880c8542d --- /dev/null +++ b/changelog.d/5-internal/optimize-list-users @@ -0,0 +1 @@ +Optimize getting a lot of users by concurrently getting target users \ No newline at end of file diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 945f128e700..97cecc82093 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -138,7 +138,8 @@ getLocalUserProfilesImpl :: Member (Input UserSubsystemConfig) r, Member DeleteQueue r, Member Now r, - Member GalleyAPIAccess r + Member GalleyAPIAccess r, + Member (Concurrency Unsafe) r ) => Local [UserId] -> Sem r [UserProfile] @@ -154,7 +155,8 @@ getUserProfilesFromDomain :: Member UserStore r, RunClient (fedM 'Brig), FederationMonad fedM, - Typeable fedM + Typeable fedM, + Member (Concurrency Unsafe) r ) => Local UserId -> Qualified [UserId] -> @@ -183,7 +185,8 @@ getUserProfilesLocalPart :: Member (Input UserSubsystemConfig) r, Member DeleteQueue r, Member Now r, - Member GalleyAPIAccess r + Member GalleyAPIAccess r, + Member (Concurrency Unsafe) r ) => Maybe (Local UserId) -> Local [UserId] -> @@ -199,7 +202,7 @@ getUserProfilesLocalPart requestingUser luids = do <$> traverse getRequestingUserInfo requestingUser -- FUTUREWORK: (in the interpreters where it makes sense) pull paginated lists from the DB, -- not just single rows. - catMaybes <$> traverse (getLocalUserProfileImpl emailVisibilityConfigWithViewer) (sequence luids) + catMaybes <$> unsafePooledForConcurrentlyN 8 (sequence luids) (getLocalUserProfileImpl emailVisibilityConfigWithViewer) where getRequestingUserInfo :: Local UserId -> Sem r (Maybe (TeamId, TeamMember)) getRequestingUserInfo self = do From feda42b562e2a80dd275a47e3e31014a286ac23c Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 15 Jul 2024 13:54:49 +0200 Subject: [PATCH 004/136] WPB-10058 delete phone column in brig's user table (#4130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * delete phone keys * hide ctor of phone for safety * log while searching for phone keys * db migration for dropping phone column * changelog * moved to tools/db, renamed to remove-phone-keys * removed phone from inconsistencies tool * remove phone from move-team tool * remove phone from queries * Better split and organize the changelog * Update the README of remove-phone-keys tool * remove db tool * updated changelog --------- Co-authored-by: Marko Dimjašević --- cassandra-schema.cql | 1 - changelog.d/0-release-notes/WPB-10058 | 1 + changelog.d/0-release-notes/WPB-10058-5xx | 4 +++ changelog.d/2-features/WPB-10058 | 1 + libs/wire-subsystems/src/Wire/StoredUser.hs | 1 - .../src/Wire/UserStore/Cassandra.hs | 4 +-- nix/wire-server.nix | 2 +- services/brig/brig.cabal | 1 + services/brig/src/Brig/Schema/Run.hs | 4 ++- .../src/Brig/Schema/V82_DropPhoneColumn.hs | 35 +++++++++++++++++++ .../inconsistencies/src/DanglingUserKeys.hs | 9 ++--- tools/db/move-team/src/Schema.hs | 8 ++--- tools/db/move-team/src/Types.hs | 14 ++++---- 13 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 changelog.d/0-release-notes/WPB-10058 create mode 100644 changelog.d/0-release-notes/WPB-10058-5xx create mode 100644 changelog.d/2-features/WPB-10058 create mode 100644 services/brig/src/Brig/Schema/V82_DropPhoneColumn.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index bbeefe5b6e3..0e143beeacf 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -678,7 +678,6 @@ CREATE TABLE brig_test.user ( managed_by int, name text, password blob, - phone text, picture list, provider uuid, searchable boolean, diff --git a/changelog.d/0-release-notes/WPB-10058 b/changelog.d/0-release-notes/WPB-10058 new file mode 100644 index 00000000000..8f9c066875b --- /dev/null +++ b/changelog.d/0-release-notes/WPB-10058 @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000000..e884d0434be --- /dev/null +++ b/changelog.d/0-release-notes/WPB-10058-5xx @@ -0,0 +1,4 @@ +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/2-features/WPB-10058 b/changelog.d/2-features/WPB-10058 new file mode 100644 index 00000000000..02fab832d8c --- /dev/null +++ b/changelog.d/2-features/WPB-10058 @@ -0,0 +1 @@ +DB migration for dropping `phone` column from `user` table diff --git a/libs/wire-subsystems/src/Wire/StoredUser.hs b/libs/wire-subsystems/src/Wire/StoredUser.hs index b2ace0784cb..6a716ba04d6 100644 --- a/libs/wire-subsystems/src/Wire/StoredUser.hs +++ b/libs/wire-subsystems/src/Wire/StoredUser.hs @@ -21,7 +21,6 @@ data StoredUser = StoredUser name :: Name, pict :: Maybe Pict, email :: Maybe Email, - phone :: Maybe Phone, ssoId :: Maybe UserSSOId, accentId :: ColourId, assets :: Maybe [Asset], diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index cba7356f22e..b6662fbac63 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -127,7 +127,7 @@ lookupLocaleImpl u = do selectUser :: PrepQuery R (Identity UserId) (TupleType StoredUser) selectUser = - "SELECT id, name, picture, email, phone, sso_id, accent_id, assets, \ + "SELECT id, name, picture, email, sso_id, accent_id, assets, \ \activated, status, expires, language, country, provider, service, \ \handle, team, managed_by, supported_protocols \ \FROM user where id = ?" @@ -166,7 +166,7 @@ updateUserToTombstone :: PrepQuery W (AccountStatus, Name, ColourId, Pict, [Asse updateUserToTombstone = "UPDATE user SET status = ?, name = ?,\ \ accent_id = ?, picture = ?, assets = ?, handle = null, country = null,\ - \ language = null, email = null, phone = null, sso_id = null WHERE id = ?" + \ language = null, email = null, sso_id = null WHERE id = ?" statusSelect :: PrepQuery R (Identity UserId) (Identity (Maybe AccountStatus)) statusSelect = "SELECT status FROM user WHERE id = ?" diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 6fafadb5efa..d052b10c7d6 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -477,7 +477,7 @@ let out = import ./all-toplevel-derivations.nix { inherit (pkgs) lib; fn = mk; - # more than two takes more than 32GB of RAM, so this is what + # more than two takes more than 32GB of RAM, so this is what # we're limiting ourselves to recursionDepth = 2; keyFilter = k: k != "passthru"; diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 49d45527d52..fd3434fe99d 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -188,6 +188,7 @@ library Brig.Schema.V79_ConnectionRemoteIndex Brig.Schema.V80_KeyPackageCiphersuite Brig.Schema.V81_AddFederationRemoteTeams + Brig.Schema.V82_DropPhoneColumn Brig.Schema.V_FUTUREWORK Brig.Team.API Brig.Team.DB diff --git a/services/brig/src/Brig/Schema/Run.hs b/services/brig/src/Brig/Schema/Run.hs index 049a51e5f5f..be608d28578 100644 --- a/services/brig/src/Brig/Schema/Run.hs +++ b/services/brig/src/Brig/Schema/Run.hs @@ -56,6 +56,7 @@ import Brig.Schema.V78_ClientLastActive qualified as V78_ClientLastActive import Brig.Schema.V79_ConnectionRemoteIndex qualified as V79_ConnectionRemoteIndex import Brig.Schema.V80_KeyPackageCiphersuite qualified as V80_KeyPackageCiphersuite import Brig.Schema.V81_AddFederationRemoteTeams qualified as V81_AddFederationRemoteTeams +import Brig.Schema.V82_DropPhoneColumn qualified as V82_DropPhoneColumn import Cassandra.MigrateSchema (migrateSchema) import Cassandra.Schema import Control.Exception (finally) @@ -118,7 +119,8 @@ migrations = V78_ClientLastActive.migration, V79_ConnectionRemoteIndex.migration, V80_KeyPackageCiphersuite.migration, - V81_AddFederationRemoteTeams.migration + V81_AddFederationRemoteTeams.migration, + V82_DropPhoneColumn.migration -- FUTUREWORK: undo V41 (searchable flag); we stopped using it in -- https://github.com/wireapp/wire-server/pull/964 -- diff --git a/services/brig/src/Brig/Schema/V82_DropPhoneColumn.hs b/services/brig/src/Brig/Schema/V82_DropPhoneColumn.hs new file mode 100644 index 00000000000..dc086e04a60 --- /dev/null +++ b/services/brig/src/Brig/Schema/V82_DropPhoneColumn.hs @@ -0,0 +1,35 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 Brig.Schema.V82_DropPhoneColumn + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 82 "Drop phone column from user table" $ do + schema' + [r| + ALTER TABLE user DROP phone + |] diff --git a/tools/db/inconsistencies/src/DanglingUserKeys.hs b/tools/db/inconsistencies/src/DanglingUserKeys.hs index 3d27d4c208c..1242a1a2972 100644 --- a/tools/db/inconsistencies/src/DanglingUserKeys.hs +++ b/tools/db/inconsistencies/src/DanglingUserKeys.hs @@ -82,7 +82,6 @@ data Inconsistency = Inconsistency time :: Writetime UserId, status :: Maybe (WithWritetime AccountStatus), userEmail :: Maybe (WithWritetime Email), - userPhone :: Maybe (WithWritetime Phone), inconsistencyCase :: Text } deriving (Generic) @@ -130,13 +129,13 @@ instance Cql EmailKey where instance Aeson.ToJSON EmailKey where toJSON = Aeson.toJSON . emailKeyUniq -type UserDetailsRow = (Maybe AccountStatus, Maybe (Writetime AccountStatus), Maybe Email, Maybe (Writetime Email), Maybe Phone, Maybe (Writetime Phone)) +type UserDetailsRow = (Maybe AccountStatus, Maybe (Writetime AccountStatus), Maybe Email, Maybe (Writetime Email)) getUserDetails :: UserId -> Client (Maybe UserDetailsRow) getUserDetails uid = retry x5 $ query1 cql (params LocalQuorum (Identity uid)) where cql :: PrepQuery R (Identity UserId) UserDetailsRow - cql = "SELECT status, writetime(status), email, writetime(email), phone, writetime(phone) from user where id = ?" + cql = "SELECT status, writetime(status), email, writetime(email) from user where id = ?" checkKey :: Logger -> ClientState -> EmailKey -> Bool -> IO (Maybe Inconsistency) checkKey l brig key repairData = do @@ -179,16 +178,14 @@ checkUser l brig key uid time repairData = do Nothing -> do let status = Nothing userEmail = Nothing - userPhone = Nothing inconsistencyCase = "2." when repairData $ -- case 2. runClient brig $ freeEmailKey l key pure . Just $ Inconsistency {userId = uid, ..} - Just (mStatus, mStatusWriteTime, mEmail, mEmailWriteTime, mPhone, mPhoneWriteTime) -> do + Just (mStatus, mStatusWriteTime, mEmail, mEmailWriteTime) -> do let status = WithWritetime <$> mStatus <*> mStatusWriteTime userEmail = WithWritetime <$> mEmail <*> mEmailWriteTime - userPhone = WithWritetime <$> mPhone <*> mPhoneWriteTime statusError = case mStatus of Nothing -> True Just Deleted -> True diff --git a/tools/db/move-team/src/Schema.hs b/tools/db/move-team/src/Schema.hs index fdf17e81cae..2249bb2f1a5 100644 --- a/tools/db/move-team/src/Schema.hs +++ b/tools/db/move-team/src/Schema.hs @@ -360,10 +360,10 @@ importBrigRichInfo Env {..} path = do -- brig.user -type RowBrigUser = (Maybe UUID, Maybe [Float], Maybe Int32, Maybe Bool, Maybe [AssetIgnoreData], Maybe Ascii, Maybe Text, Maybe UTCTime, Maybe Text, Maybe Ascii, Maybe Int32, Maybe Text, Maybe Blob, Maybe Text, Maybe [Blob], Maybe UUID, Maybe Bool, Maybe UUID, Maybe Text, Maybe Int32, Maybe UUID) +type RowBrigUser = (Maybe UUID, Maybe [Float], Maybe Int32, Maybe Bool, Maybe [AssetIgnoreData], Maybe Ascii, Maybe Text, Maybe UTCTime, Maybe Text, Maybe Ascii, Maybe Int32, Maybe Text, Maybe Blob, Maybe [Blob], Maybe UUID, Maybe Bool, Maybe UUID, Maybe Text, Maybe Int32, Maybe UUID) selectBrigUser :: PrepQuery R (Identity [UserId]) RowBrigUser -selectBrigUser = "SELECT id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, phone, picture, provider, searchable, service, sso_id, status, team FROM user WHERE id in ?" +selectBrigUser = "SELECT id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, picture, provider, searchable, service, sso_id, status, team FROM user WHERE id in ?" readBrigUser :: Env -> [UserId] -> ConduitM () [RowBrigUser] IO () readBrigUser Env {..} uids = @@ -371,7 +371,7 @@ readBrigUser Env {..} uids = paginateC selectBrigUser (paramsP LocalQuorum (pure uids) envPageSize) x5 selectBrigUserAll :: PrepQuery R () RowBrigUser -selectBrigUserAll = "SELECT id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, phone, picture, provider, searchable, service, sso_id, status, team FROM user" +selectBrigUserAll = "SELECT id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, picture, provider, searchable, service, sso_id, status, team FROM user" readBrigUserAll :: Env -> ConduitM () [RowBrigUser] IO () readBrigUserAll Env {..} = @@ -388,7 +388,7 @@ exportBrigUserFull env path = do insertBrigUser :: PrepQuery W RowBrigUser () insertBrigUser = - "INSERT INTO user (id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, phone, picture, provider, searchable, service, sso_id, status, team) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + "INSERT INTO user (id, accent, accent_id, activated, assets, country, email, expires, handle, language, managed_by, name, password, picture, provider, searchable, service, sso_id, status, team) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" importBrigUser :: Env -> FilePath -> IO () importBrigUser Env {..} path = do diff --git a/tools/db/move-team/src/Types.hs b/tools/db/move-team/src/Types.hs index 3f7ef7ebe36..2839331ccb1 100644 --- a/tools/db/move-team/src/Types.hs +++ b/tools/db/move-team/src/Types.hs @@ -104,8 +104,8 @@ deriving instance ToJSON TimeUuid deriving instance FromJSON TimeUuid -instance (ToJSON a, ToJSON b, ToJSON c, ToJSON d, ToJSON e, ToJSON f, ToJSON g, ToJSON h, ToJSON i, ToJSON j, ToJSON k, ToJSON l, ToJSON m, ToJSON n, ToJSON o, ToJSON p, ToJSON q, ToJSON r, ToJSON s, ToJSON t, ToJSON u) => ToJSON ((,,,,,,,,,,,,,,,,,,,,) a b c d e f g h i j k l m n o p q r s t u) where - toJSON (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) = +instance (ToJSON a, ToJSON b, ToJSON c, ToJSON d, ToJSON e, ToJSON f, ToJSON g, ToJSON h, ToJSON i, ToJSON j, ToJSON k, ToJSON l, ToJSON m, ToJSON n, ToJSON o, ToJSON p, ToJSON q, ToJSON r, ToJSON s, ToJSON t) => ToJSON ((,,,,,,,,,,,,,,,,,,,) a b c d e f g h i j k l m n o p q r s t) where + toJSON (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) = Array $ V.fromList [ toJSON a, @@ -127,14 +127,13 @@ instance (ToJSON a, ToJSON b, ToJSON c, ToJSON d, ToJSON e, ToJSON f, ToJSON g, toJSON q, toJSON r, toJSON s, - toJSON t, - toJSON u + toJSON t ] -instance (FromJSON a, FromJSON b, FromJSON c, FromJSON d, FromJSON e, FromJSON f, FromJSON g, FromJSON h, FromJSON i, FromJSON j, FromJSON k, FromJSON l, FromJSON m, FromJSON n, FromJSON o, FromJSON p, FromJSON q, FromJSON r, FromJSON s, FromJSON t, FromJSON u) => FromJSON ((,,,,,,,,,,,,,,,,,,,,) a b c d e f g h i j k l m n o p q r s t u) where +instance (FromJSON a, FromJSON b, FromJSON c, FromJSON d, FromJSON e, FromJSON f, FromJSON g, FromJSON h, FromJSON i, FromJSON j, FromJSON k, FromJSON l, FromJSON m, FromJSON n, FromJSON o, FromJSON p, FromJSON q, FromJSON r, FromJSON s, FromJSON t) => FromJSON ((,,,,,,,,,,,,,,,,,,,) a b c d e f g h i j k l m n o p q r s t) where parseJSON = withArray "Tuple" $ \case - (toList -> [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u]) -> - (,,,,,,,,,,,,,,,,,,,,) + (toList -> [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t]) -> + (,,,,,,,,,,,,,,,,,,,) <$> parseJSON a <*> parseJSON b <*> parseJSON c @@ -155,5 +154,4 @@ instance (FromJSON a, FromJSON b, FromJSON c, FromJSON d, FromJSON e, FromJSON f <*> parseJSON r <*> parseJSON s <*> parseJSON t - <*> parseJSON u _ -> fail "Expected array of length 21" From 006785a3cf51478b81936e8430a5b02da502869c Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 15 Jul 2024 14:14:47 +0200 Subject: [PATCH 005/136] Federation V1 test setup (#4125) * Initial v1 docker-compose setup * Update service configuration in federation-v1 * Improve run script * Rename federation-v0 and federation-v1 services * Make federation-v* optional * Allow PTest to use IO * Introduce VersionedFed * Setup federation-v1 in integration tests * Fix coredns network * Use legacy backends in some of the tests * Fix background worker config in fed-v1 * federation-v0 and -v1 configuration for integration tests * fix linter * fix Helm chart for integration tests * Add CHANGELOG entry * Enable debug log level for federator * Disable one of the tests on legacy backends --------- Co-authored-by: Stefan Berthold --- changelog.d/5-internal/federation-v1 | 1 + charts/integration/templates/configmap.yaml | 40 ++ .../templates/integration-integration.yaml | 4 + .../coredns-config/db.example.com | 1 + deploy/dockerephemeral/docker/elasticmq.conf | 7 +- deploy/dockerephemeral/federation-v0.yaml | 66 ++- .../coredns-config/db.example.com | 3 +- .../federation-v0/nginz/upstreams | 16 +- deploy/dockerephemeral/federation-v1.yaml | 274 +++++++++ .../federation-v1/background-worker.yaml | 28 + .../dockerephemeral/federation-v1/brig.yaml | 218 ++++++++ .../dockerephemeral/federation-v1/cannon.yaml | 26 + .../federation-v1/cargohold.yaml | 33 ++ .../federation-v1/coredns-config/Corefile | 4 + .../coredns-config/db.example.com | 21 + .../federation-v1/federator.yaml | 29 + .../dockerephemeral/federation-v1/galley.yaml | 107 ++++ .../federation-v1/gundeck.yaml | 46 ++ .../federation-v1/integration-ca.pem | 19 + .../federation-v1/integration-leaf-key.pem | 28 + .../federation-v1/integration-leaf.pem | 20 + .../federation-v1/jwt-ed25519-bundle.pem | 6 + .../federation-v1/mls-private-key-ed25519.pem | 3 + .../federation-v1/nexmo-credentials.yaml | 2 + .../federation-v1/nginz/conf/README.md | 7 + .../nginz/conf/common_response.conf | 38 ++ .../nginz/conf/common_response_no_zauth.conf | 2 + .../conf/common_response_with_zauth.conf | 3 + .../federation-v1/nginz/conf/integration.conf | 19 + .../federation-v1/nginz/conf/nginx.conf | 518 ++++++++++++++++++ .../federation-v1/nginz/conf/pid.conf | 1 + .../federation-v1/nginz/conf/zauth_acl.txt | 15 + .../federation-v1/nginz/upstreams | 38 ++ .../federation-v1/oauth-ed25519.jwk | 1 + .../federation-v1/proxy.config | 8 + .../dockerephemeral/federation-v1/proxy.yaml | 20 + .../dockerephemeral/federation-v1/spar.yaml | 44 ++ .../federation-v1/turn-secret.txt | 1 + .../federation-v1/twilio-credentials.yaml | 2 + .../federation-v1/zauth-privkeys.txt | 4 + .../federation-v1/zauth-pubkeys.txt | 4 + deploy/dockerephemeral/init_vhosts.sh | 1 + deploy/dockerephemeral/run.sh | 31 +- hack/helm_vars/wire-server/values.yaml.gotmpl | 1 + integration/Setup.hs | 7 +- integration/integration.cabal | 1 + integration/test/SetupHelpers.hs | 13 +- integration/test/Test/Connection.hs | 76 +-- integration/test/Test/Conversation.hs | 53 +- integration/test/Test/Demo.hs | 15 +- integration/test/Test/MLS/One2One.hs | 11 +- integration/test/Test/User.hs | 3 +- integration/test/Test/Version.hs | 24 +- integration/test/Testlib/App.hs | 5 - integration/test/Testlib/Env.hs | 11 +- integration/test/Testlib/HTTP.hs | 10 +- integration/test/Testlib/PTest.hs | 88 +-- integration/test/Testlib/Run.hs | 1 + integration/test/Testlib/Types.hs | 5 + integration/test/Testlib/VersionedFed.hs | 65 +++ services/integration.yaml | 39 ++ 61 files changed, 2005 insertions(+), 182 deletions(-) create mode 100644 changelog.d/5-internal/federation-v1 create mode 100644 deploy/dockerephemeral/federation-v1.yaml create mode 100644 deploy/dockerephemeral/federation-v1/background-worker.yaml create mode 100644 deploy/dockerephemeral/federation-v1/brig.yaml create mode 100644 deploy/dockerephemeral/federation-v1/cannon.yaml create mode 100644 deploy/dockerephemeral/federation-v1/cargohold.yaml create mode 100644 deploy/dockerephemeral/federation-v1/coredns-config/Corefile create mode 100644 deploy/dockerephemeral/federation-v1/coredns-config/db.example.com create mode 100644 deploy/dockerephemeral/federation-v1/federator.yaml create mode 100644 deploy/dockerephemeral/federation-v1/galley.yaml create mode 100644 deploy/dockerephemeral/federation-v1/gundeck.yaml create mode 100644 deploy/dockerephemeral/federation-v1/integration-ca.pem create mode 100644 deploy/dockerephemeral/federation-v1/integration-leaf-key.pem create mode 100644 deploy/dockerephemeral/federation-v1/integration-leaf.pem create mode 100644 deploy/dockerephemeral/federation-v1/jwt-ed25519-bundle.pem create mode 100644 deploy/dockerephemeral/federation-v1/mls-private-key-ed25519.pem create mode 100644 deploy/dockerephemeral/federation-v1/nexmo-credentials.yaml create mode 100644 deploy/dockerephemeral/federation-v1/nginz/conf/README.md create mode 100644 deploy/dockerephemeral/federation-v1/nginz/conf/common_response.conf create mode 100644 deploy/dockerephemeral/federation-v1/nginz/conf/common_response_no_zauth.conf create mode 100644 deploy/dockerephemeral/federation-v1/nginz/conf/common_response_with_zauth.conf create mode 100644 deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf create mode 100644 deploy/dockerephemeral/federation-v1/nginz/conf/nginx.conf create mode 100644 deploy/dockerephemeral/federation-v1/nginz/conf/pid.conf create mode 100644 deploy/dockerephemeral/federation-v1/nginz/conf/zauth_acl.txt create mode 100644 deploy/dockerephemeral/federation-v1/nginz/upstreams create mode 100644 deploy/dockerephemeral/federation-v1/oauth-ed25519.jwk create mode 100644 deploy/dockerephemeral/federation-v1/proxy.config create mode 100644 deploy/dockerephemeral/federation-v1/proxy.yaml create mode 100644 deploy/dockerephemeral/federation-v1/spar.yaml create mode 100644 deploy/dockerephemeral/federation-v1/turn-secret.txt create mode 100644 deploy/dockerephemeral/federation-v1/twilio-credentials.yaml create mode 100644 deploy/dockerephemeral/federation-v1/zauth-privkeys.txt create mode 100644 deploy/dockerephemeral/federation-v1/zauth-pubkeys.txt create mode 100644 integration/test/Testlib/VersionedFed.hs diff --git a/changelog.d/5-internal/federation-v1 b/changelog.d/5-internal/federation-v1 new file mode 100644 index 00000000000..01960024d5d --- /dev/null +++ b/changelog.d/5-internal/federation-v1 @@ -0,0 +1 @@ +Add federation-v1 environment for testing compatibility of the federation API with version 1 diff --git a/charts/integration/templates/configmap.yaml b/charts/integration/templates/configmap.yaml index ca2d49f9bec..42297c017c6 100644 --- a/charts/integration/templates/configmap.yaml +++ b/charts/integration/templates/configmap.yaml @@ -164,4 +164,44 @@ data: stern: host: stern.wire-federation-v0.svc.cluster.local port: 8080 + + federation-v1: + originDomain: federation-test-helper.wire-federation-v1.svc.cluster.local + brig: + host: brig.wire-federation-v1.svc.cluster.local + port: 8080 + cannon: + host: cannon.wire-federation-v1.svc.cluster.local + port: 8080 + cargohold: + host: cargohold.wire-federation-v1.svc.cluster.local + port: 8080 + federatorInternal: + host: federator.wire-federation-v1.svc.cluster.local + port: 8080 + federatorExternal: + host: federator.wire-federation-v1.svc.cluster.local + port: 8081 + galley: + host: galley.wire-federation-v1.svc.cluster.local + port: 8080 + gundeck: + host: gundeck.wire-federation-v1.svc.cluster.local + port: 8080 + nginz: + host: nginz-integration-http.wire-federation-v1.svc.cluster.local + port: 8080 + spar: + host: spar.wire-federation-v1.svc.cluster.local + port: 8080 + proxy: + host: proxy.wire-federation-v1.svc.cluster.local + port: 8080 + backgroundWorker: + host: backgroundWorker.wire-federation-v1.svc.cluster.local + port: 8080 + stern: + host: stern.wire-federation-v1.svc.cluster.local + port: 8080 + integrationTestHostName: integration-headless.{{ .Release.Namespace }}.svc.cluster.local diff --git a/charts/integration/templates/integration-integration.yaml b/charts/integration/templates/integration-integration.yaml index 56dbf2bf8e7..3fe4284dc5b 100644 --- a/charts/integration/templates/integration-integration.yaml +++ b/charts/integration/templates/integration-integration.yaml @@ -328,3 +328,7 @@ spec: key: uploadXmlAwsSecretAccessKey {{- end }} {{- end }} + - name: ENABLE_FEDERATION_V0 + value: "1" + - name: ENABLE_FEDERATION_V1 + value: "1" diff --git a/deploy/dockerephemeral/coredns-config/db.example.com b/deploy/dockerephemeral/coredns-config/db.example.com index a458686bca7..46df3527882 100644 --- a/deploy/dockerephemeral/coredns-config/db.example.com +++ b/deploy/dockerephemeral/coredns-config/db.example.com @@ -18,3 +18,4 @@ _wire-server-federator._tcp.d1 IN SRV 0 0 10443 localhost. _wire-server-federator._tcp.d2 IN SRV 0 0 11443 localhost. _wire-server-federator._tcp.d3 IN SRV 0 0 12443 localhost. _wire-server-federator._tcp.federation-v0 IN SRV 0 0 21443 localhost. +_wire-server-federator._tcp.federation-v1 IN SRV 0 0 22443 localhost. diff --git a/deploy/dockerephemeral/docker/elasticmq.conf b/deploy/dockerephemeral/docker/elasticmq.conf index 7cd41d7317e..77cdd11e4b4 100644 --- a/deploy/dockerephemeral/docker/elasticmq.conf +++ b/deploy/dockerephemeral/docker/elasticmq.conf @@ -43,6 +43,7 @@ queues { integration-brig-events4 = ${queues.default-queue-template} integration-brig-events5 = ${queues.default-queue-template} integration-brig-events-federation-v0 = ${queues.default-queue-template} + integration-brig-events-federation-v1 = ${queues.default-queue-template} integration-brig-events-internal = ${queues.default-queue-template} integration-brig-events-internal2 = ${queues.default-queue-template} @@ -50,6 +51,7 @@ queues { integration-brig-events-internal4 = ${queues.default-queue-template} integration-brig-events-internal5 = ${queues.default-queue-template} integration-brig-events-internal-federation-v0 = ${queues.default-queue-template} + integration-brig-events-internal-federation-v1 = ${queues.default-queue-template} "integration-user-events.fifo" = ${queues.fifo-queue-template} "integration-user-events2.fifo" = ${queues.fifo-queue-template} @@ -57,6 +59,7 @@ queues { "integration-user-events4.fifo" = ${queues.fifo-queue-template} "integration-user-events5.fifo" = ${queues.fifo-queue-template} "integration-user-events-federation-v0.fifo" = ${queues.fifo-queue-template} + "integration-user-events-federation-v1.fifo" = ${queues.fifo-queue-template} integration-gundeck-events = ${queues.default-queue-template} integration-gundeck-events2 = ${queues.default-queue-template} @@ -64,6 +67,7 @@ queues { integration-gundeck-events4 = ${queues.default-queue-template} integration-gundeck-events5 = ${queues.default-queue-template} integration-gundeck-events-federation-v0 = ${queues.default-queue-template} + integration-gundeck-events-federation-v1 = ${queues.default-queue-template} "integration-team-events.fifo" = ${queues.fifo-queue-template} "integration-team-events2.fifo" = ${queues.fifo-queue-template} @@ -71,10 +75,11 @@ queues { "integration-team-events4.fifo" = ${queues.fifo-queue-template} "integration-team-events5.fifo" = ${queues.fifo-queue-template} "integration-team-events-federation-v0.fifo" = ${queues.fifo-queue-template} + "integration-team-events-federation-v1.fifo" = ${queues.fifo-queue-template} } # Region and accountId which will be included in resource ids aws { region = eu-west-1 accountId = 000000000000 -} \ No newline at end of file +} diff --git a/deploy/dockerephemeral/federation-v0.yaml b/deploy/dockerephemeral/federation-v0.yaml index 28e50750273..d34be166d50 100644 --- a/deploy/dockerephemeral/federation-v0.yaml +++ b/deploy/dockerephemeral/federation-v0.yaml @@ -2,8 +2,11 @@ networks: demo_wire: external: false + coredns: + external: false + services: - brig_schema: + brig_schema-v0: container_name: brig-schema-federation-v0 image: quay.io/wire/brig-schema:4.38.51 command: --host cassandra --keyspace brig_test_federation_v0 --replication-factor 1 @@ -15,7 +18,7 @@ services: condition: on-failure networks: - demo_wire - brig: + brig-v0: container_name: brig-federation-v0 image: quay.io/wire/brig:4.38.0-mandarin.14 volumes: @@ -27,7 +30,7 @@ services: healthcheck: &haskell_health_check test: "curl --fail localhost:8080/i/status" depends_on: - brig_schema: + brig_schema-v0: condition: service_completed_successfully aws_cli: condition: service_completed_successfully @@ -41,7 +44,7 @@ services: - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} - galley_schema: + galley_schema-v0: container_name: galley-schema-federation-v0 image: quay.io/wire/galley-schema:4.38.51 command: --host cassandra --keyspace galley_test_federation_v0 --replication-factor 1 @@ -54,7 +57,7 @@ services: networks: - demo_wire - galley: + galley-v0: container_name: galley-federation-v0 image: quay.io/wire/galley:4.38.0-mandarin.14 volumes: @@ -65,7 +68,7 @@ services: - '127.0.0.1:21085:8080' healthcheck: *haskell_health_check depends_on: - galley_schema: + galley_schema-v0: condition: service_completed_successfully aws_cli: condition: service_completed_successfully @@ -79,7 +82,7 @@ services: - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} - cargohold: + cargohold-v0: container_name: cargohold-federation-v0 image: quay.io/wire/cargohold:4.38.0-mandarin.14 volumes: @@ -98,7 +101,7 @@ services: - AWS_ACCESS_KEY_ID=dummykey - AWS_SECRET_ACCESS_KEY=dummysecret - gundeck_schema: + gundeck_schema-v0: container_name: gundeck-schema-federation-v0 image: quay.io/wire/gundeck-schema:4.38.51 command: --host cassandra --keyspace gundeck_test_federation_v0 --replication-factor 1 @@ -111,7 +114,7 @@ services: networks: - demo_wire - gundeck: + gundeck-v0: container_name: gundeck-federation-v0 image: quay.io/wire/gundeck:4.38.0-mandarin.14 volumes: @@ -122,11 +125,11 @@ services: - '127.0.0.1:21086:8080' healthcheck: *haskell_health_check depends_on: - gundeck_schema: + gundeck_schema-v0: condition: service_completed_successfully aws_cli: condition: service_completed_successfully - redis: + redis-v0: condition: service_started environment: @@ -134,7 +137,7 @@ services: - AWS_ACCESS_KEY_ID=dummykey - AWS_SECRET_ACCESS_KEY=dummysecret - spar_schema: + spar_schema-v0: container_name: spar-schema-federation-v0 image: quay.io/wire/spar-schema:4.38.51 command: --host cassandra --keyspace spar_test_federation_v0 --replication-factor 1 @@ -147,7 +150,7 @@ services: networks: - demo_wire - spar: + spar-v0: container_name: spar-federation-v0 image: quay.io/wire/spar:4.38.0-mandarin.14 volumes: @@ -158,10 +161,10 @@ services: - '127.0.0.1:21088:8080' healthcheck: *haskell_health_check depends_on: - spar_schema: + spar_schema-v0: condition: service_completed_successfully - cannon: + cannon-v0: container_name: cannon-federation-v0 image: quay.io/wire/cannon:4.38.0-mandarin.14 volumes: @@ -172,7 +175,7 @@ services: - '127.0.0.1:21083:8080' healthcheck: *haskell_health_check - federator: + federator-v0: container_name: federator-federation-v0 image: quay.io/wire/federator:4.38.0-mandarin.14 volumes: @@ -189,10 +192,10 @@ services: healthcheck: test: "true" depends_on: - coredns-federation: + coredns-federation-v0: condition: service_started - background_worker: + background_worker-v0: container_name: background-worker-federation-v0 image: quay.io/wire/background-worker:4.38.0-mandarin.14 volumes: @@ -201,6 +204,7 @@ services: - demo_wire ports: - '127.0.0.1:21089:8080' + healthcheck: *haskell_health_check depends_on: init_vhosts: condition: service_completed_successfully @@ -208,7 +212,7 @@ services: - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} - proxy: + proxy-v0: container_name: proxy-federation-v0 image: quay.io/wire/proxy:4.38.0-mandarin.14 volumes: @@ -219,7 +223,7 @@ services: - '127.0.0.1:21087:8080' healthcheck: *haskell_health_check - nginz: + nginz-v0: container_name: nginz-federation-v0 image: quay.io/wire/nginz:4.38.0-mandarin.14 volumes: @@ -230,26 +234,28 @@ services: - '127.0.0.1:21080:8080' - '127.0.0.1:21443:8443' depends_on: - brig: + brig-v0: + condition: service_healthy + galley-v0: condition: service_healthy - galley: + gundeck-v0: condition: service_healthy - gundeck: + cargohold-v0: condition: service_healthy - cargohold: + cannon-v0: condition: service_healthy - cannon: + spar-v0: condition: service_healthy - spar: + federator-v0: condition: service_healthy - federator: + proxy-v0: condition: service_healthy - proxy: + background_worker-v0: condition: service_healthy # We have to run a separate redis instance for each version of wire-server we # want. This is because gundeck just assumes the whole redis is for itself - redis: + redis-v0: container_name: redis-federation-v0 image: redis:6.0-alpine networks: @@ -257,7 +263,7 @@ services: # This coredns serves slightly different SRV records, so federator running in # a docker container can talk to federator running on the host. - coredns-federation: + coredns-federation-v0: image: docker.io/coredns/coredns:1.8.4 volumes: - ./federation-v0/coredns-config:/coredns-config diff --git a/deploy/dockerephemeral/federation-v0/coredns-config/db.example.com b/deploy/dockerephemeral/federation-v0/coredns-config/db.example.com index 448d8b5f594..407f4770916 100644 --- a/deploy/dockerephemeral/federation-v0/coredns-config/db.example.com +++ b/deploy/dockerephemeral/federation-v0/coredns-config/db.example.com @@ -17,4 +17,5 @@ _wire-server-federator._tcp.b IN SRV 0 0 9443 host.docker.internal. _wire-server-federator._tcp.d1 IN SRV 0 0 10443 host.docker.internal. _wire-server-federator._tcp.d2 IN SRV 0 0 11443 host.docker.internal. _wire-server-federator._tcp.d3 IN SRV 0 0 12443 host.docker.internal. -_wire-server-federator._tcp.v0 IN SRV 0 0 21443 host.docker.internal. +_wire-server-federator._tcp.federation-v0 IN SRV 0 0 21443 host.docker.internal. +_wire-server-federator._tcp.federation-v1 IN SRV 0 0 22443 host.docker.internal. diff --git a/deploy/dockerephemeral/federation-v0/nginz/upstreams b/deploy/dockerephemeral/federation-v0/nginz/upstreams index a3e6afada32..adfdafa6eec 100644 --- a/deploy/dockerephemeral/federation-v0/nginz/upstreams +++ b/deploy/dockerephemeral/federation-v0/nginz/upstreams @@ -1,38 +1,38 @@ upstream cargohold { least_conn; keepalive 32; - server cargohold:8080 max_fails=3 weight=1; + server cargohold-v0:8080 max_fails=3 weight=1; } upstream gundeck { least_conn; keepalive 32; - server gundeck:8080 max_fails=3 weight=1; + server gundeck-v0:8080 max_fails=3 weight=1; } upstream cannon { least_conn; keepalive 32; - server cannon:8080 max_fails=3 weight=1; + server cannon-v0:8080 max_fails=3 weight=1; } upstream galley { least_conn; keepalive 32; - server galley:8080 max_fails=3 weight=1; + server galley-v0:8080 max_fails=3 weight=1; } upstream proxy { least_conn; keepalive 32; - server proxy:8080 max_fails=3 weight=1; + server proxy-v0:8080 max_fails=3 weight=1; } upstream brig { least_conn; keepalive 32; - server brig:8080 max_fails=3 weight=1; + server brig-v0:8080 max_fails=3 weight=1; } upstream spar { least_conn; keepalive 32; - server spar:8080 max_fails=3 weight=1; + server spar-v0:8080 max_fails=3 weight=1; } upstream federator_external { - server federator:8081 max_fails=3 weight=1; + server federator-v0:8081 max_fails=3 weight=1; } diff --git a/deploy/dockerephemeral/federation-v1.yaml b/deploy/dockerephemeral/federation-v1.yaml new file mode 100644 index 00000000000..fc151a37dd0 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1.yaml @@ -0,0 +1,274 @@ +networks: + demo_wire: + external: false + + coredns: + external: false + +services: + brig_schema-v1: + container_name: brig-schema-federation-v1 + image: quay.io/wire/brig-schema:4.42.0-pre.27 + command: --host cassandra --keyspace brig_test_federation_v1 --replication-factor 1 + depends_on: + cassandra: + condition: service_healthy + deploy: + restart_policy: + condition: on-failure + networks: + - demo_wire + + brig-v1: + container_name: brig-federation-v1 + image: quay.io/wire/brig:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/brig/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22082:8080' + healthcheck: &haskell_health_check + test: "curl --fail localhost:8080/i/status" + depends_on: + brig_schema-v1: + condition: service_completed_successfully + aws_cli: + condition: service_completed_successfully + init_vhosts: + condition: service_completed_successfully + environment: + - AWS_REGION=eu-west-1 + - AWS_ACCESS_KEY_ID=dummykey + - AWS_SECRET_ACCESS_KEY=dummysecret + - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} + - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} + + galley_schema-v1: + container_name: galley-schema-federation-v1 + image: quay.io/wire/galley-schema:4.42.0-pre.27 + command: --host cassandra --keyspace galley_test_federation_v1 --replication-factor 1 + depends_on: + cassandra: + condition: service_healthy + deploy: + restart_policy: + condition: on-failure + networks: + - demo_wire + + galley-v1: + container_name: galley-federation-v1 + image: quay.io/wire/galley:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/galley/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22085:8080' + healthcheck: *haskell_health_check + depends_on: + galley_schema-v1: + condition: service_completed_successfully + aws_cli: + condition: service_completed_successfully + init_vhosts: + condition: service_completed_successfully + environment: + - AWS_REGION=eu-west-1 + - AWS_ACCESS_KEY_ID=dummykey + - AWS_SECRET_ACCESS_KEY=dummysecret + - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} + - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} + + cargohold-v1: + container_name: cargohold-federation-v1 + image: quay.io/wire/cargohold:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/cargohold/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22084:8080' + healthcheck: *haskell_health_check + depends_on: + aws_cli: + condition: service_completed_successfully + environment: + - AWS_REGION=eu-west-1 + - AWS_ACCESS_KEY_ID=dummykey + - AWS_SECRET_ACCESS_KEY=dummysecret + + gundeck_schema-v1: + container_name: gundeck-schema-federation-v1 + image: quay.io/wire/gundeck-schema:4.42.0-pre.27 + command: --host cassandra --keyspace gundeck_test_federation_v1 --replication-factor 1 + depends_on: + cassandra: + condition: service_healthy + deploy: + restart_policy: + condition: on-failure + networks: + - demo_wire + + gundeck-v1: + container_name: gundeck-federation-v1 + image: quay.io/wire/gundeck:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/gundeck/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22086:8080' + healthcheck: *haskell_health_check + depends_on: + gundeck_schema-v1: + condition: service_completed_successfully + aws_cli: + condition: service_completed_successfully + redis-v1: + condition: service_started + environment: + - AWS_REGION=eu-west-1 + - AWS_ACCESS_KEY_ID=dummykey + - AWS_SECRET_ACCESS_KEY=dummysecret + + spar_schema-v1: + container_name: spar-schema-federation-v1 + image: quay.io/wire/spar-schema:4.42.0-pre.27 + command: --host cassandra --keyspace spar_test_federation_v1 --replication-factor 1 + depends_on: + cassandra: + condition: service_healthy + deploy: + restart_policy: + condition: on-failure + networks: + - demo_wire + + spar-v1: + container_name: spar-federation-v1 + image: quay.io/wire/spar:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/spar/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22088:8080' + healthcheck: *haskell_health_check + depends_on: + spar_schema-v1: + condition: service_completed_successfully + + cannon-v1: + container_name: cannon-federation-v1 + image: quay.io/wire/cannon:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/cannon/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22083:8080' + healthcheck: *haskell_health_check + + federator-v1: + container_name: federator-federation-v1 + image: quay.io/wire/federator:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/federator/conf + networks: + - demo_wire + - coredns + extra_hosts: + - "host.docker.internal.:host-gateway" + ports: + - '127.0.0.1:22097:8080' + - '127.0.0.1:22098:8081' + # healthcheck: *haskell_health_check + healthcheck: + test: "true" + depends_on: + coredns-federation-v1: + condition: service_started + + background_worker-v1: + container_name: background-worker-federation-v1 + image: quay.io/wire/background-worker:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/background-worker/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22089:8080' + healthcheck: *haskell_health_check + depends_on: + init_vhosts: + condition: service_completed_successfully + environment: + - RABBITMQ_USERNAME=${RABBITMQ_USERNAME} + - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} + + proxy-v1: + container_name: proxy-federation-v1 + image: quay.io/wire/proxy:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/proxy/conf + networks: + - demo_wire + ports: + - '127.0.0.1:22087:8080' + healthcheck: *haskell_health_check + + nginz-v1: + container_name: nginz-federation-v1 + image: quay.io/wire/nginz:4.42.0-pre.27 + volumes: + - ./federation-v1:/etc/wire/ + networks: + - demo_wire + ports: + - '127.0.0.1:22080:8080' + - '127.0.0.1:22443:8443' + depends_on: + brig-v1: + condition: service_healthy + galley-v1: + condition: service_healthy + gundeck-v1: + condition: service_healthy + cargohold-v1: + condition: service_healthy + cannon-v1: + condition: service_healthy + spar-v1: + condition: service_healthy + federator-v1: + condition: service_healthy + proxy-v1: + condition: service_healthy + background_worker-v1: + condition: service_healthy + + # We have to run a separate redis instance for each version of wire-server we + # want. This is because gundeck just assumes the whole redis is for itself + redis-v1: + container_name: redis-federation-v1 + image: redis:6.0-alpine + networks: + - demo_wire + + + # This coredns serves slightly different SRV records, so federator running in + # a docker container can talk to federator running on the host. + coredns-federation-v1: + image: docker.io/coredns/coredns:1.8.4 + volumes: + - ./federation-v1/coredns-config:/coredns-config + entrypoint: + - /coredns + - -conf + - /coredns-config/Corefile + networks: + coredns: + ipv4_address: 172.20.1.4 diff --git a/deploy/dockerephemeral/federation-v1/background-worker.yaml b/deploy/dockerephemeral/federation-v1/background-worker.yaml new file mode 100644 index 00000000000..d70699f7f4e --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/background-worker.yaml @@ -0,0 +1,28 @@ +logLevel: Debug + +backgroundWorker: + host: 0.0.0.0 + port: 8080 + +federatorInternal: + host: federator-federation-v1 + port: 8080 + +galley: + host: galley-federation-v1 + port: 8080 + +brig: + host: brig-federation-v1 + port: 8080 + +rabbitmq: + host: rabbitmq + port: 5672 + vHost: federation-v1 + adminPort: 15672 + +backendNotificationPusher: + pushBackoffMinWait: 1000 + pushBackoffMaxWait: 1000000 + remotesRefreshInterval: 5000000 diff --git a/deploy/dockerephemeral/federation-v1/brig.yaml b/deploy/dockerephemeral/federation-v1/brig.yaml new file mode 100644 index 00000000000..62a6c8c2a8f --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/brig.yaml @@ -0,0 +1,218 @@ +brig: + host: 0.0.0.0 + port: 8080 + +cassandra: + endpoint: + host: demo_wire_cassandra + port: 9042 + keyspace: brig_test_federation_v1 + # filterNodesByDatacentre: datacenter1 + +elasticsearch: + url: http://nginz-federation-v1:9201 + index: directory_test + +rabbitmq: + host: rabbitmq + port: 5672 + vHost: federation-v1 + +cargohold: + host: cargohold-federation-v1 + port: 8080 + +galley: + host: galley-federation-v1 + port: 8080 + +gundeck: + host: gundeck-federation-v1 + port: 8080 + +federatorInternal: + host: federator-federation-v1 + port: 8080 + +multiSFT: false + +# You can set up local SQS/Dynamo running e.g. `../../deploy/dockerephemeral/run.sh` +aws: + userJournalQueue: integration-user-events-federation-v1.fifo + # ^ Comment this out if you don't want to journal user events + prekeyTable: integration-brig-prekeys-federation-v1 + sqsEndpoint: http://fake_sqs:4568 # https://sqs.eu-west-1.amazonaws.com + # dynamoDBEndpoint: http://localhost:4567 # https://dynamodb.eu-west-1.amazonaws.com + +# Uncomment to use the randomPrekey allocation strategy instead of dynamoDB +randomPrekeys: true + +# Uncomment this if you want STOMP. +# +# stomp: +# stompHost: localhost +# stompPort: 61613 +# stompTls: false + +# TODO: possibly move 'userJournalQueue' to the top level as well +internalEvents: + queueType: sqs + queueName: integration-brig-events-internal-federation-v1 + # queueType: stomp + # queueName: /queue/integration-brig-events-internal + +emailSMS: + # You can either use SES directly (in which case, ensure a feedback queue is configured) + # or you can use SMTP directly (blacklisting of email/phone must be otherwise handled by + # the operator). + email: + sesQueue: integration-brig-events-federation-v1 + sesEndpoint: http://ses:4569 # https://email.eu-west-1.amazonaws.com + # If you prefer to use SMTP directly, uncomment the following lines + # and set the correct credentials. + # NOTE: In case a user tries to supply config values for both SES and SMTP, + # SES takes precedence and gets used instead + # smtpEndpoint: + # host: localhost + # port: 2500 + # smtpCredentials: + # username: + # password: test/resources/smtp-secret.txt + # smtpConnType: plain + # ^ NOTE: blacklisting of emails (processing of bounces and complaints) is only done + # automatically IF sesQueue/sesEndpoint are used. If SMTP is used directly, the + # operator must handle these notifications "manually" (there are internal endpoints) + # that may be used for this + + general: + templateDir: /usr/share/wire/templates + emailSender: backend-integration@wire.com + smsSender: "+123456789" # or MG123456789... (twilio alphanumeric sender id) + templateBranding: + brand: Wire + brandUrl: https://wire.com + brandLabelUrl: wire.com # This is the text in the label for the above URL + brandLogoUrl: https://wire.com/p/img/email/logo-email-black.png + brandService: Wire Service Provider + copyright: © WIRE SWISS GmbH + misuse: misuse@wire.com + legal: https://wire.com/legal/ + forgot: https://wire.com/forgot/ + support: https://support.wire.com/ + user: + activationUrl: http://127.0.0.1:8080/activate?key=${key}&code=${code} + smsActivationUrl: http://127.0.0.1:8080/v/${code} + passwordResetUrl: http://127.0.0.1:8080/password-reset/${key}?code=${code} + invitationUrl: http://127.0.0.1:8080/register?invitation_code=${code} + deletionUrl: http://127.0.0.1:8080/users/delete?key=${key}&code=${code} + + provider: + homeUrl: https://provider.localhost/ + providerActivationUrl: http://127.0.0.1:8080/provider/activate?key=${key}&code=${code} + approvalUrl: http://127.0.0.1:8080/provider/approve?key=${key}&code=${code} + approvalTo: success@simulator.amazonses.com + providerPwResetUrl: http://127.0.0.1:8080/provider/password-reset?key=${key}&code=${code} + + team: + tInvitationUrl: http://127.0.0.1:8080/register?team=${team}&team_code=${code} + tActivationUrl: http://127.0.0.1:8080/register?team=${team}&team_code=${code} + tCreatorWelcomeUrl: http://127.0.0.1:8080/creator-welcome-website + tMemberWelcomeUrl: http://127.0.0.1:8080/member-welcome-website + +zauth: + privateKeys: /etc/wire/brig/conf/zauth-privkeys.txt + publicKeys: /etc/wire/brig/conf/zauth-pubkeys.txt + authSettings: + keyIndex: 1 + userTokenTimeout: 120 + sessionTokenTimeout: 20 + accessTokenTimeout: 30 + providerTokenTimeout: 60 + legalHoldUserTokenTimeout: 120 + legalHoldAccessTokenTimeout: 30 + +turn: + serversSource: dns # files | dns + baseDomain: example.com + discoveryIntervalSeconds: 100 + + # This should be the same secret as used by the TURN servers + secret: /etc/wire/brig/conf/turn-secret.txt + configTTL: 3600 + tokenTTL: 21600 + +optSettings: + setActivationTimeout: 10 + setVerificationTimeout: 10 + setTeamInvitationTimeout: 10 + setExpiredUserCleanupTimeout: 1 + setTwilio: /etc/wire/brig/conf/twilio-credentials.yaml + setNexmo: /etc/wire/brig/conf/nexmo-credentials.yaml + # setStomp: test/resources/stomp-credentials.yaml + setUserMaxConnections: 16 + setCookieInsecure: true + setUserCookieRenewAge: 2 + setUserCookieLimit: 5 + setUserCookieThrottle: + stdDev: 5 + retryAfter: 3 + setLimitFailedLogins: + timeout: 5 # seconds. if you reach the limit, how long do you have to wait to try again. + retryLimit: 5 # how many times can you have a failed login in that timeframe. + setSuspendInactiveUsers: # if this is omitted: never suspend inactive users. + suspendTimeout: 10 + setRichInfoLimit: 5000 # should be in sync with Spar + setDefaultUserLocale: en + setMaxTeamSize: 32 + setMaxConvSize: 16 + setEmailVisibility: visible_to_self + setPropertyMaxKeyLen: 1024 + setPropertyMaxValueLen: 4096 + setDeleteThrottleMillis: 0 + setSqsThrottleMillis: 1000 + setRestrictUserCreation: false + # setSearchSameTeamOnly: false + # ^ NOTE: this filters out search results for team users, + # i.e., if you are a team user the search endpoints will + # return only users part of the same team. For name search, + # this is slightly more inefficient as it requires 2 extra DB lookups + # setUserMaxPermClients: 7 + # ^ You can limit the max number of permanent clients that a user is allowed + # to register, per account. The default value is '7' if the option is unset. + + # Federation domain is used to qualify local IDs and handles, + # e.g. 0c4d8944-70fa-480e-a8b7-9d929862d18c@wire.com and somehandle@wire.com. + # It should also match the SRV DNS records under which other wire-server installations can find this backend: + # _wire-server-federator._tcp. + # 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 Galley. + setFederationDomain: federation-v1.example.com + setFeatureFlags: # see #RefConfigOptions in `/docs/reference` + setFederationDomainConfigsUpdateFreq: 1 + setFederationStrategy: allowAll + setFederationDomainConfigs: + - domain: example.com + search_policy: full_search + set2FACodeGenerationDelaySecs: 5 + setDisabledAPIVersions: [] + setNonceTtlSecs: 5 + setDpopMaxSkewSecs: 1 + setDpopTokenExpirationTimeSecs: 300 # 5 minutes + setPublicKeyBundle: /etc/wire/brig/conf/jwt-ed25519-bundle.pem + setEnableMLS: true + # To only allow specific email address domains to register, uncomment and update the setting below + # setAllowlistEmailDomains: + # - wire.com + # To only allow specific phone number prefixes to register uncomment and update the settings below + # setAllowlistPhonePrefixes: + # - "+1555555" + # needs to be kept in sync with services/nginz/integration-test/resources/oauth/ed25519_public.jwk + setOAuthJwkKeyPair: /etc/wire/brig/conf/oauth-ed25519.jwk + setOAuthAuthCodeExpirationTimeSecs: 3 # 3 secs + setOAuthAccessTokenExpirationTimeSecs: 3 # 3 secs + setOAuthEnabled: true + setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks + setOAuthMaxActiveRefreshTokens: 10 + +logLevel: Warn +logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/cannon.yaml b/deploy/dockerephemeral/federation-v1/cannon.yaml new file mode 100644 index 00000000000..c091ea67ed2 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/cannon.yaml @@ -0,0 +1,26 @@ +# Example yaml-formatted configuration for cannon used in integration tests + +# cannon can be started with a config file (e.g. ./dist/cannon -c cannon.yaml.example) + +cannon: + host: 0.0.0.0 + port: 8080 + + # Each cannon instance advertises its own location (ip or dns name) to gundeck. + # Either externalHost or externalHostFile must be set (externalHost takes precedence if both are defined) + # externalHostFile expects a file with a single line containing the IP or dns name of this instance of cannon + externalHost: cannon-federation-v1 + #externalHostFile: /etc/wire/cannon/cannon-host.txt + +gundeck: + host: gundeck-federation-v1 + port: 8080 + +drainOpts: + gracePeriodSeconds: 1 + millisecondsBetweenBatches: 500 + minBatchSize: 5 + +disabledAPIVersions: [] +logLevel: Warn +logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/cargohold.yaml b/deploy/dockerephemeral/federation-v1/cargohold.yaml new file mode 100644 index 00000000000..4913082900e --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/cargohold.yaml @@ -0,0 +1,33 @@ +brig: + host: brig-federation-v1 + port: 8080 + +cargohold: + host: 0.0.0.0 + port: 8080 + +federator: + host: federator-federation-v1 + port: 8080 + +aws: + s3Bucket: dummy-bucket-federation-v1 # <-- insert-bucket-name-here + s3Endpoint: http://fake_s3:4570 # https://s3-eu-west-1.amazonaws.com:443 + # s3DownloadEndpoint: http://fake-s3:4570 + # ^ When not using a real S3 service, we may need to use a different, + # publicly accessible endpoint for downloading assets. + # + # If you want to use cloudfront for asset downloads + # cloudFront: + # domain: + # keyPairId: + # privateKey: cf-pk.pem + +settings: + maxTotalBytes: 27262976 + downloadLinkTTL: 300 # Seconds + federationDomain: example.com + disabledAPIVersions: [] + +logLevel: Warn +logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/coredns-config/Corefile b/deploy/dockerephemeral/federation-v1/coredns-config/Corefile new file mode 100644 index 00000000000..7bf495f2e89 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/coredns-config/Corefile @@ -0,0 +1,4 @@ +example.com { + file /coredns-config/db.example.com + log +} \ No newline at end of file diff --git a/deploy/dockerephemeral/federation-v1/coredns-config/db.example.com b/deploy/dockerephemeral/federation-v1/coredns-config/db.example.com new file mode 100644 index 00000000000..407f4770916 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/coredns-config/db.example.com @@ -0,0 +1,21 @@ +$ORIGIN example.com. +@ 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. ( + 2017042745 ; serial + 7200 ; refresh (2 hours) + 3600 ; retry (1 hour) + 1209600 ; expire (2 weeks) + 3600 ; minimum (1 hour) + ) + + 3600 IN NS a.iana-servers.net. + 3600 IN NS b.iana-servers.net. + +www IN A 127.0.0.1 + IN AAAA ::1 +_wire-server-federator._tcp IN SRV 0 0 8443 host.docker.internal. +_wire-server-federator._tcp.b IN SRV 0 0 9443 host.docker.internal. +_wire-server-federator._tcp.d1 IN SRV 0 0 10443 host.docker.internal. +_wire-server-federator._tcp.d2 IN SRV 0 0 11443 host.docker.internal. +_wire-server-federator._tcp.d3 IN SRV 0 0 12443 host.docker.internal. +_wire-server-federator._tcp.federation-v0 IN SRV 0 0 21443 host.docker.internal. +_wire-server-federator._tcp.federation-v1 IN SRV 0 0 22443 host.docker.internal. diff --git a/deploy/dockerephemeral/federation-v1/federator.yaml b/deploy/dockerephemeral/federation-v1/federator.yaml new file mode 100644 index 00000000000..1973a0540be --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/federator.yaml @@ -0,0 +1,29 @@ +federatorInternal: + host: 0.0.0.0 + port: 8080 +federatorExternal: + host: 0.0.0.0 + port: 8081 +brig: + host: brig-federation-v1 + port: 8080 +cargohold: + host: cargohold-federation-v1 + port: 8080 +galley: + host: galley-federation-v1 + port: 8080 + +logLevel: Warn +logNetStrings: false + +optSettings: + # Filepath to one or more PEM-encoded server certificates to use as a trust + # store when making requests to remote backends + remoteCAStore: "/etc/wire/federator/conf/integration-ca.pem" + useSystemCAStore: false + clientCertificate: "/etc/wire/federator/conf/integration-leaf.pem" + clientPrivateKey: "/etc/wire/federator/conf/integration-leaf-key.pem" + tcpConnectionTimeout: 5000000 + dnsHost: 172.20.1.3 + dnsPort: 53 diff --git a/deploy/dockerephemeral/federation-v1/galley.yaml b/deploy/dockerephemeral/federation-v1/galley.yaml new file mode 100644 index 00000000000..f272536260c --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/galley.yaml @@ -0,0 +1,107 @@ +galley: + host: 0.0.0.0 + port: 8080 + +cassandra: + endpoint: + host: demo_wire_cassandra + port: 9042 + keyspace: galley_test_federation_v1 + # filterNodesByDatacentre: datacenter1 + +brig: + host: brig-federation-v1 + port: 8080 + +gundeck: + host: gundeck-federation-v1 + port: 8080 + +spar: + host: spar-federation-v1 + port: 8080 + +federator: + host: federator-federation-v1 + port: 8080 + +rabbitmq: + host: rabbitmq + port: 5672 + vHost: federation-v1 + +settings: + httpPoolSize: 128 + maxTeamSize: 32 + maxFanoutSize: 18 + exposeInvitationURLsTeamAllowlist: [] + maxConvSize: 16 + intraListing: false + conversationCodeURI: https://account.wire.com/conversation-join/ + concurrentDeletionEvents: 1024 + deleteConvThrottleMillis: 0 + # Federation domain is used to qualify local IDs and handles, + # e.g. 0c4d8944-70fa-480e-a8b7-9d929862d18c@wire.com and somehandle@wire.com. + # It should also match the SRV DNS records under which other wire-server installations can find this backend: + # _wire-server-federator._tcp. + # 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: federation-v1.example.com + mlsPrivateKeyPaths: + removal: + ed25519: /etc/wire/galley/conf/mls-private-key-ed25519.pem + guestLinkTTLSeconds: 604800 + disabledAPIVersions: [] + + featureFlags: # see #RefConfigOptions in `/docs/reference` + sso: disabled-by-default + legalhold: whitelist-teams-and-implicit-consent + teamSearchVisibility: disabled-by-default + appLock: + defaults: + status: enabled + config: + enforceAppLock: false + inactivityTimeoutSecs: 60 + classifiedDomains: + status: enabled + config: + domains: ["example.com"] + fileSharing: + defaults: + status: enabled + lockStatus: unlocked + conferenceCalling: + defaults: + status: enabled + outlookCalIntegration: + defaults: + status: disabled + lockStatus: locked + mlsE2EId: + defaults: + status: disabled + config: + verificationExpiration: 86400 + acmeDiscoveryUrl: null + lockStatus: unlocked + mlsMigration: + defaults: + status: enabled + config: + startTime: "2029-05-16T10:11:12.123Z" + finaliseRegardlessAfter: "2029-10-17T00:00:00.000Z" + usersThreshold: 100 + clientsThreshold: 50 + lockStatus: locked + limitedEventFanout: + defaults: + status: disabled + +logLevel: Warn +logNetStrings: false + +journal: # if set, journals; if not set, disables journaling + queueName: integration-team-events-federation-v1.fifo + endpoint: http://demo_wire_sqs:4568 # https://sqs.eu-west-1.amazonaws.com + region: eu-west-1 diff --git a/deploy/dockerephemeral/federation-v1/gundeck.yaml b/deploy/dockerephemeral/federation-v1/gundeck.yaml new file mode 100644 index 00000000000..6722f2e0b90 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/gundeck.yaml @@ -0,0 +1,46 @@ +gundeck: + host: 0.0.0.0 + port: 8080 + +brig: + host: brig-federation-v1 + port: 8080 + +cassandra: + endpoint: + host: demo_wire_cassandra + port: 9042 + keyspace: gundeck_test_federation_v1 + # filterNodesByDatacentre: datacenter1 + +redis: + host: redis-federation-v1 + port: 6379 + connectionMode: master + +# redisAdditionalWrite: +# host: 127.0.0.1 +# port: 6379 +# connectionMode: master + +aws: + queueName: integration-gundeck-events-federation-v1 + region: eu-west-1 + account: "123456789012" # Default account nr used by localstack + arnEnv: integration + sqsEndpoint: http://demo_wire_sqs:4568 # https://sqs.eu-west-1.amazonaws.com + snsEndpoint: http://demo_wire_sns:4575 # https://sns.eu-west-1.amazonaws.com + +settings: + httpPoolSize: 1024 + notificationTTL: 24192200 + bulkPush: true + perNativePushConcurrency: 32 + sqsThrottleMillis: 1000 + maxConcurrentNativePushes: + hard: 30 # more than this number of threads will not be allowed + soft: 10 # more than this number of threads will be warned about + disabledAPIVersions: [] + +logLevel: Warn +logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/integration-ca.pem b/deploy/dockerephemeral/federation-v1/integration-ca.pem new file mode 100644 index 00000000000..304fc892245 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/integration-ca.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDEzCCAfugAwIBAgIUQ35aUV70pJjvDTbfgFUj5YmchHQwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwNjE3MTMxNTMxWhcN +MzQwNjE1MTMxNTMxWjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJQlUOLNmd7Ll7iskcSnsv9xcx/+TnMw +qtqkK17w54/Kto+NJJAkD1L+X5EkSPZ7FDKqt2bGfoETWGnlpH/zsUTUpchlf6Jf +w6TJOejQer5FQNLCtQSnOIchlAFKzFxhGSvcOrRWiBAPjTVIkv9eiCNXcJ5PE9Sk +8+bmn2ztz7LVHcv46PmT/+ihRxKJ01T5CsXWPUHOZQRfGvKZmyGf+iTBuhcxMPYC +nXb7/M3rYCQXL8FQZiaqbIVMqNRpMBVkAqU3l2JnSrlNIjIh6Nqowjog8QYGuIz6 +fxwWkw6EU5ZBwHIr2rOakCnQoKeXVqBJdWZNRMX1Vtqeh7O9zDoW4/0CAwEAAaNT +MFEwHQYDVR0OBBYEFHNgZ4nZQoNKnb0AnDkefTXxxYDqMB8GA1UdIwQYMBaAFHNg +Z4nZQoNKnb0AnDkefTXxxYDqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAIuLuyF7m1SP6PBu29jXnfGtaGi7j0jlqfcAysn7VmAU3StgWvSatlAl +AO6MIasjSQ+ygAbfIQW6W2Wc/U+NLQq5fRVi1cnmlxH5OULOFeQZCVyux8Maq0fT +jj4mmsz62b/iiA4tyS5r+foY4v1u2siSViBJSbfYbMp/VggIimt26RNV2u/ZV6Kf +UrOxazMx1yyuqARiqoA3VOMV8Byv8SEIiteWUSYni6u7xOT4gucPORhbM1HOSQ/S +CVq95x4FeKQnbEMykHI+bpBdkoadMVtrjCbskU49mOrvl/pli9V44R8KK6C1Nv3E +VLLcoOctdw90aT3sIjaXBcZtDTE6p6g= +-----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/federation-v1/integration-leaf-key.pem b/deploy/dockerephemeral/federation-v1/integration-leaf-key.pem new file mode 100644 index 00000000000..1e7a83068de --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/integration-leaf-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCZjOHeUnlauuxD +WgrRnh3hj5Fs+uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47I +RgA5VLvGxI+T1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjG +QBmFF7NxrvjGgerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9K +zNQ7ZTlBQvJG8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzog +D+jgoAD5/9sk3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P +2jMpJ1xxAgMBAAECggEAS3NBjWgDP4T4EUROaqACWNKeB+nmkdt68T0gGtoNVD+D +EN9UPnpFQPdHFngAgWnzF858UIKzq1Pzdg+HjqRHPK1bS67tvua3xP1GHuR/CGPk +28T1hefqPHRen7GqHDAfdwarYBWCGv4Sjz/yCkcSIrtyfMBb5fAya5GO02pckUSK +19sl7XhkPtHJVirRkjQL29R2TCpkNNpQMjkuYLk7mox+6pNTbxgbk0cnT3eGj1pV +mlPqpwzC5GevRziE/VE/WXFLChY+8KB4fDLRqWnyvabDvQ4coaXgzwbdScJyM5hX ++Dxdfni/P2m7xAZXUyfBsr0VUzqUkJfK3WWvvAGTDQKBgQDNi3RUEjVnU/MN4aDz +iZB2VYGfo/K69xTPNEbLQWs1F4ZMpHVtUVXzTfx/xG9ug989ijEm6ncL9OsnhThn +UldSz2ojSJUxLmhgCHZGYHT72v/9rEqfT9JisWpIj44KXufUHCcl3Cozj1ae3EUp +NVhN1HphB2LsCIJvLYfLIGdBNwKBgQC/PhHQMm/MQe4pOHAbdzDrRZWdG2KSRVxp +9mmJ/aT8LOp7BDjq+Dkct6a56JGqlOTeJirMTTmCKiOiTInuB9S+K7kWJJiYg9g4 +UCiuMU+40Px/1Z4/uxRj3DSdGLXG7S6kPeADx9f9BUNpAytGqOnSnfbDiDVvQVbp +0N0+nIXDlwKBgQC2uZOXrXxGOE4pd/ySpCeF2yvZ1HDTnxWjwlBxHt4Em74rYkR2 +A0mKezjOCL4bHCaYWcKqWuOsAHYQcxEaYQv6NSOg7ESdLSlivgMPO26j+yN5yvGn +wNlCHYBjsyLNu2MSoFh5AsmNfo69uQnOwXqX7h1BJsTdGg+CcJJ4lHzWbwKBgQCD +/CRzGbwKrh3eGPNWIUaDuTxudy3qYTBMeSGReJpa5+zUBa/6imFwLldEyvttTOE/ +Z/v1j/52lPqO0mAHBSSQMsDERXGDIMsi4j+RKLsqhCEfYKCcv1JtMNam7RzXM24T +MBjgwxWPrAg/+03ssDrffuGFRQYLyH5hVCK9SW0P9QKBgQDJ1ZSto+RWxv/uOKNr +7FYeQoKpMb2IvNvnGlnYHC8KS9qRq6wUE+FtuKcdLBQP4M9Cgq71VD/dsawrhEw7 +1rAYk3OqmHxBOU5Dcb152NxYHEf53pfEfWc0x4AEVe+Jzynj2EYixRKNWwODNTEx +LKJOYd0CuWywxg6d9G7A7XbgWQ== +-----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/federation-v1/integration-leaf.pem b/deploy/dockerephemeral/federation-v1/integration-leaf.pem new file mode 100644 index 00000000000..635d332de70 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/integration-leaf.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgIBADANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5jYS5l +eGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzFaFw0yNDA3MTcxMzE1MzFaMAAwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCZjOHeUnlauuxDWgrRnh3hj5Fs ++uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47IRgA5VLvGxI+T +1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjGQBmFF7NxrvjG +gerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9KzNQ7ZTlBQvJG +8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzogD+jgoAD5/9sk +3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P2jMpJ1xxAgMB +AAGjgawwgakwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEgGA1UdEQEB +/wQ+MDyCGSouaW50ZWdyYXRpb24uZXhhbXBsZS5jb22CFGhvc3QuZG9ja2VyLmlu +dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFPowAfmLPCmdCMdSxQjsR6UQSoyH +MB8GA1UdIwQYMBaAFHNgZ4nZQoNKnb0AnDkefTXxxYDqMA0GCSqGSIb3DQEBCwUA +A4IBAQCMJwbLzUsrkQkgdGKVi/Mb5XAAV0sfkwZch1Fx0vhJI072cZSow5A2ZUHa +LScFNTPmilPKEr6MS4xIKtRQaMHInbfxSsyNViKhpzkSOKoAiJjIJ2xPKFPnbTDI +uV74nxxyf9q/p3SLQfJFk7fxbvNeLqg5bYSrMeklHj4bpMJ9fybS8/mZVc8AkTFK +fsXSu9CW1B3GF+jP3E2GrFF3Zh9MgvWjMlSYg4ljPf5FoMCUq6GmQ17hQeJFvb5h +Jqk6TcgUrp082bcVlPW17XzFwVe3n6uzvWMtwI62EztVUj98+YkBiFL3i4+OQwAU +/noc22fq20OyJtCPJY4FIK7xUcgD +-----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/federation-v1/jwt-ed25519-bundle.pem b/deploy/dockerephemeral/federation-v1/jwt-ed25519-bundle.pem new file mode 100644 index 00000000000..afbd4dfb0ec --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/jwt-ed25519-bundle.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIFANnxZLNE4p+GDzWzR3wm/v8x/0bxZYkCyke1aTRucX +-----END PRIVATE KEY----- +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEACPvhIdimF20tOPjbb+fXJrwS2RKDp7686T90AZ0+Th8= +-----END PUBLIC KEY----- diff --git a/deploy/dockerephemeral/federation-v1/mls-private-key-ed25519.pem b/deploy/dockerephemeral/federation-v1/mls-private-key-ed25519.pem new file mode 100644 index 00000000000..182df6f5a7d --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/mls-private-key-ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIKqoSUVW579Aw8Nz47CRwArSigl/25jg0suQmg6mOwdy +-----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/federation-v1/nexmo-credentials.yaml b/deploy/dockerephemeral/federation-v1/nexmo-credentials.yaml new file mode 100644 index 00000000000..1f83517f2ee --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nexmo-credentials.yaml @@ -0,0 +1,2 @@ +key: "dummy" +secret: "dummy" diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/README.md b/deploy/dockerephemeral/federation-v1/nginz/conf/README.md new file mode 100644 index 00000000000..8e614e99d1b --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/README.md @@ -0,0 +1,7 @@ +# How to regenerate certificates in this directory + +Run from this directory: + +```bash +../../../../../hack/bin/gen-certs.sh +``` diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/common_response.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response.conf new file mode 100644 index 00000000000..1b8a947f437 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response.conf @@ -0,0 +1,38 @@ + # remove access_token from logs, see 'Note sanitized_request'. + set $sanitized_request $request; + if ($sanitized_request ~ (.*)access_token=[^&]*(.*)) { + set $sanitized_request $1access_token=****$2; + } + + # Should be overriden when using websockets + proxy_set_header Connection ""; + proxy_set_header Z-Type $zauth_type; + proxy_set_header Z-User $zauth_user; + proxy_set_header Z-Client $zauth_client; + proxy_set_header Z-Connection $zauth_connection; + proxy_set_header Z-Provider $zauth_provider; + proxy_set_header Z-Bot $zauth_bot; + proxy_set_header Z-Conversation $zauth_conversation; + proxy_set_header Request-Id $request_id; + + # NOTE: This should only be used on endpoints where credentials are needed + more_set_headers 'Access-Control-Allow-Credentials: true'; + # NOTE: This allows all origins, you may want to tune this value + more_set_headers 'Access-Control-Allow-Origin: $http_origin'; + more_set_headers 'Access-Control-Expose-Headers: Request-Id, Location'; + more_set_headers 'Request-Id: $request_id'; + more_set_headers 'Strict-Transport-Security: max-age=31536000; preload'; + + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Methods' "GET, POST, PUT, DELETE, OPTIONS"; + add_header 'Access-Control-Allow-Headers' "$http_access_control_request_headers, DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type"; + add_header 'Content-Type' 'text/plain; charset=UTF-8'; + add_header 'Content-Length' 0; + return 204; + } + + + proxy_http_version 1.1; + + # NOTE: You may want to tune this + client_max_body_size 64M; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_no_zauth.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_no_zauth.conf new file mode 100644 index 00000000000..4277ede8c0f --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_no_zauth.conf @@ -0,0 +1,2 @@ + zauth off; + include common_response.conf; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_with_zauth.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_with_zauth.conf new file mode 100644 index 00000000000..699dd263b31 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/common_response_with_zauth.conf @@ -0,0 +1,3 @@ + include common_response.conf; + proxy_set_header Authorization ""; + proxy_set_header Z-Host $host; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf new file mode 100644 index 00000000000..12c49ccfe88 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf @@ -0,0 +1,19 @@ +# plain TCP/http listening for integration tests only. +listen 8080; +listen 8081; + +# for nginx-without-tls, we need to use a separate port for http2 traffic, +# as nginx cannot handle unencrypted http1 and http2 traffic on the same +# port. +# This port is only used for trying out nginx http2 forwarding without TLS locally and should not +# be ported to any production nginz config. +listen 8090 http2; + +######## TLS/SSL block start ############## +# +# Most integration tests simply use the http ports 8080 and 8081 +# 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 http2; +listen [::]:8443 ssl http2; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/nginx.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/nginx.conf new file mode 100644 index 00000000000..43f8c68b306 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/nginx.conf @@ -0,0 +1,518 @@ +worker_processes 4; +worker_rlimit_nofile 1024; +include pid.conf; # for easy overriding + +# nb. start up errors (eg. misconfiguration) may still end up in /$(LOG_PATH)/error.log +error_log stderr warn; + +events { + worker_connections 1024; + multi_accept off; +} + +http { + # + # Some temporary paths (by default, will use the `prefix` path given when starting nginx) + # + + client_body_temp_path /tmp; + fastcgi_temp_path /tmp; + proxy_temp_path /tmp; + scgi_temp_path /tmp; + uwsgi_temp_path /tmp; + + # + # Sockets + # + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + # + # Timeouts + # + + client_body_timeout 60; + client_header_timeout 60; + keepalive_timeout 75; + send_timeout 60; + + ignore_invalid_headers off; + + types_hash_max_size 2048; + + server_names_hash_bucket_size 64; + server_name_in_redirect off; + + large_client_header_buffers 4 8k; + + # + # Security + # + + server_tokens off; + + # + # Logging + # + # Note sanitized_request: + # We allow passing access_token as query parameter for e.g. websockets + # However we do not want to log access tokens. + # + + log_format custom_zeta '$remote_addr - $remote_user [$time_local] "$sanitized_request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for" - $connection $request_time $upstream_response_time $upstream_cache_status $zauth_user $zauth_connection $request_id $proxy_protocol_addr'; + access_log /dev/stdout custom_zeta; + + # + # Monitoring + # + vhost_traffic_status_zone; + + # + # Gzip + # + + gzip on; + gzip_disable msie6; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_min_length 1024; + gzip_types 'text/plain text/css application/json text/xml'; + + # + # Proxied Upstream Services + # + + include ../upstreams; + + # + # Mapping for websocket connections + # + + map $http_upgrade $connection_upgrade { + websocket upgrade; + default ''; + } + + # + # Locations + # + + server { + # elastic search does not support running http and https listeners + # at the same time. so our instance only runs https, but + # federation-v1 only supports http. this proxy rule helps with + # that. + # + # see also: git grep -Hn 'elasticsearch:' ../../brig.yaml + listen 9201; + + zauth_keystore /etc/wire/zauth-pubkeys.txt; + zauth_acl /etc/wire/nginz/conf/zauth_acl.txt; + + location "" { + zauth off; + + proxy_pass https://demo_wire_elasticsearch:9200; + proxy_set_header Authorization "Basic ZWxhc3RpYzpjaGFuZ2VtZQ=="; + } + } + + server { + include integration.conf; + + # self-signed certificates generated using wire-server/hack/bin/gen-certs.sh + ssl_certificate /etc/wire/integration-leaf.pem; + ssl_certificate_key /etc/wire/integration-leaf-key.pem; + + ssl_verify_client on; + ssl_client_certificate /etc/wire/integration-ca.pem; + ######## TLS/SSL block end ############## + + zauth_keystore /etc/wire/zauth-pubkeys.txt; + zauth_acl /etc/wire/nginz/conf/zauth_acl.txt; + # needs to be kept in sync with services/brig/test/resources/oauth/ed25519.jwk + oauth_pub_key /etc/wire/oauth-ed25519_public.jwk; + + location /status { + set $sanitized_request $request; + zauth off; + return 200; + } + + location /i/status { + set $sanitized_request $request; + zauth off; + return 200; + } + + location /vts { + set $sanitized_request $request; + zauth off; + vhost_traffic_status_display; + vhost_traffic_status_display_format html; + } + + # + # Service Routing + # + + # Federator endpoints: expose the federatorExternal port (Inward service) + location /federation { + set $sanitized_request $request; + zauth off; + + proxy_set_header "X-SSL-Certificate" $ssl_client_escaped_cert; + proxy_pass http://federator_external; + + # FUTUREWORK(federation): are any other settings + # (e.g. timeouts, body size, buffers, headers,...) + # useful/recommended/important-for-security?) + } + + # Brig Endpoints + # + ## brig unauthenticated endpoints + + location ~* ^(/v[0-9]+)?/api/swagger-ui { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/api/swagger.json { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/api-internal/swagger-ui { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/api-internal/swagger.json { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /register { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /access { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /activate { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /login { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/teams/invitations/([^/]*)$ { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /verification-code/send { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + ## brig authenticated endpoints + + location ~* ^(/v[0-9]+)?/self$ { + include common_response_with_zauth.conf; + oauth_scope self; + proxy_pass http://brig; + } + + location /users { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /list-users { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /search { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /list-connections { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^/teams/([^/]+)/search$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /connections { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/clients { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/mls/key-packages { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /properties { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /calls/config { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^/teams/([^/]*)/size$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/system/settings/unauthorized$ { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^(/v[0-9]+)?/system/settings$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^/oauth/clients/([^/]*)$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location ~* ^/oauth/authorization/codes$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + location /oauth/token { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /oauth/revoke { + include common_response_no_zauth.conf; + proxy_pass http://brig; + } + + location /oauth/applications { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + + # Cargohold Endpoints + + location ~* ^(/v[0-9]+)?/assets { + include common_response_with_zauth.conf; + proxy_pass http://cargohold; + } + + location /assets { + include common_response_with_zauth.conf; + proxy_pass http://cargohold; + } + + location /bot/assets { + include common_response_with_zauth.conf; + proxy_pass http://cargohold; + } + + location /provider/assets { + include common_response_with_zauth.conf; + proxy_pass http://cargohold; + } + + # Galley Endpoints + + location ~* ^(/v[0-9]+)?/legalhold/conversations/(.*)$ { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/conversations$ { + include common_response_with_zauth.conf; + oauth_scope conversations; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/conversations/([^/]*)/code { + include common_response_with_zauth.conf; + oauth_scope conversations_code; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/conversations.* { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/conversations/([^/]*)/otr/messages { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/conversations/([^/]*)/([^/]*)/proteus/messages { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location /broadcast { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location /bot/conversation { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location /bot/messages { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams$ { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)$ { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/members(.*) { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/conversations(.*) { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/features { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/features/([^/]*) { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/feature-configs$ { + include common_response_with_zauth.conf; + oauth_scope feature_configs; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/feature-configs(.*) { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/legalhold(.*) { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^/teams/([^/]*)/members/csv$ { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location /mls/welcome { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location /mls/messages { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/mls/commit-bundles { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + location ~* ^(/v[0-9]+)?/mls/public-keys { + include common_response_with_zauth.conf; + proxy_pass http://galley; + } + + # Gundeck Endpoints + + location /push { + include common_response_with_zauth.conf; + proxy_pass http://gundeck; + } + + location /presences { + include common_response_with_zauth.conf; + proxy_pass http://gundeck; + } + + location ~* ^(/v[0-9]+)?/notifications$ { + include common_response_with_zauth.conf; + proxy_pass http://gundeck; + } + + # Proxy Endpoints + + location /proxy { + include common_response_with_zauth.conf; + proxy_pass http://proxy; + } + + # Cannon Endpoints + + location /await { + include common_response_with_zauth.conf; + proxy_pass http://cannon; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 1h; + } + + # Spar Endpoints + + location /sso { + include common_response_no_zauth.conf; + proxy_pass http://spar; + } + + location /identity-providers { + include common_response_with_zauth.conf; + proxy_pass http://spar; + } + } +} diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/pid.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/pid.conf new file mode 100644 index 00000000000..e722aa5ae23 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/pid.conf @@ -0,0 +1 @@ +pid /tmp/nginz.pid; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/zauth_acl.txt b/deploy/dockerephemeral/federation-v1/nginz/conf/zauth_acl.txt new file mode 100644 index 00000000000..3b644bf3d98 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/zauth_acl.txt @@ -0,0 +1,15 @@ +a (blacklist (regex "(/v[0-9]+)?/provider(/.*)?") + (regex "(/v[0-9]+)?/bot(/.*)?") + (regex "(/v[0-9]+)?/i/.*")) + +b (whitelist (regex "(/v[0-9]+)?/bot(/.*)?")) + +p (whitelist (regex "(/v[0-9]+)?/provider(/.*)?")) + +# LegalHold Access Tokens +# FUTUREWORK: remove /legalhold/conversations/ when support for v1 dropped +la (whitelist (regex "(/v[0-9]+)?/notifications") + (regex "(/v[0-9]+)?/assets/v3/.*") + (regex "(/v[0-9]+)?/users(/.*)?") + (regex "(/v[0-9]+)?/legalhold/conversations/[^/]+") + (regex "(/v[0-9]+)?/conversations/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")) diff --git a/deploy/dockerephemeral/federation-v1/nginz/upstreams b/deploy/dockerephemeral/federation-v1/nginz/upstreams new file mode 100644 index 00000000000..cbf7b353fe1 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/nginz/upstreams @@ -0,0 +1,38 @@ +upstream cargohold { + least_conn; + keepalive 32; + server cargohold-v1:8080 max_fails=3 weight=1; +} +upstream gundeck { + least_conn; + keepalive 32; + server gundeck-v1:8080 max_fails=3 weight=1; +} +upstream cannon { + least_conn; + keepalive 32; + server cannon-v1:8080 max_fails=3 weight=1; +} +upstream galley { + least_conn; + keepalive 32; + server galley-v1:8080 max_fails=3 weight=1; +} +upstream proxy { + least_conn; + keepalive 32; + server proxy-v1:8080 max_fails=3 weight=1; +} +upstream brig { + least_conn; + keepalive 32; + server brig-v1:8080 max_fails=3 weight=1; +} +upstream spar { + least_conn; + keepalive 32; + server spar-v1:8080 max_fails=3 weight=1; +} +upstream federator_external { + server federator-v1:8081 max_fails=3 weight=1; +} diff --git a/deploy/dockerephemeral/federation-v1/oauth-ed25519.jwk b/deploy/dockerephemeral/federation-v1/oauth-ed25519.jwk new file mode 100644 index 00000000000..c00a8270aa4 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/oauth-ed25519.jwk @@ -0,0 +1 @@ +{"kty":"OKP","crv":"Ed25519","x":"mhP-NgFw3ifIXGZqJVB0kemt9L3BtD5P8q4Gah4Iklc","d":"R8-pV2-sPN7dykV8HFJ73S64F3kMHTNnJiSN8UdWk_o"} diff --git a/deploy/dockerephemeral/federation-v1/proxy.config b/deploy/dockerephemeral/federation-v1/proxy.config new file mode 100644 index 00000000000..d2225ca26c9 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/proxy.config @@ -0,0 +1,8 @@ +secrets { + youtube = "my-youtube-secret" + googlemaps = "my-googlemaps-secret" + soundcloud = "my-soundcloud-secret" + giphy = "my-giphy-secret" + # Base64 encoded client ID and secret: `Bearer id:secret`: + spotify = "my-spotify-secret" +} diff --git a/deploy/dockerephemeral/federation-v1/proxy.yaml b/deploy/dockerephemeral/federation-v1/proxy.yaml new file mode 100644 index 00000000000..43a117e6a20 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/proxy.yaml @@ -0,0 +1,20 @@ +# Example yaml-formatted configuration for proxy +# proxy can be started with a config file (e.g. ./dist/proxy -c proxy.yaml.example) + +host: 0.0.0.0 +port: 8080 + +# number of connections for the http pool +httpPoolSize: 1000 + +# maximum number of incoming connections +maxConns: 5000 + +# File containing upstream secrets. +secretsConfig: /etc/wire/proxy/conf/proxy.config + +disabledAPIVersions: [] + +# Logging settings +logLevel: Info +logNetStrings: false diff --git a/deploy/dockerephemeral/federation-v1/spar.yaml b/deploy/dockerephemeral/federation-v1/spar.yaml new file mode 100644 index 00000000000..e111292bc03 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/spar.yaml @@ -0,0 +1,44 @@ +saml: + version: SAML2.0 + logLevel: Warn + + spHost: 0.0.0.0 + spPort: 8080 + # TODO: change these + spAppUri: http://localhost:8080/ + spSsoUri: http://localhost:8080/sso + + contacts: + - type: ContactBilling + company: evil corp. + givenName: Dr. + surname: Girlfriend + email: email:president@evil.corp + +brig: + host: brig-federation-v1 + port: 8080 + +galley: + host: galley-federation-v1 + port: 8080 + +cassandra: + endpoint: + host: demo_wire_cassandra + port: 9042 + keyspace: spar_test_federation_v1 + filterNodesByDatacentre: datacenter1 + +# Wire/AWS specific, optional +# discoUrl: "https://" + +disabledAPIVersions: [] + +maxttlAuthreq: 5 # seconds. don't set this too large, it is also the run time of one TTL test. +maxttlAuthresp: 7200 # seconds. do not set this to 1h or less, as that is what the mock idp wants. + +maxScimTokens: 2 # Token limit {#RefScimToken} +richInfoLimit: 5000 # should be in sync with Brig + +logNetStrings: False # log using netstrings encoding (see http://cr.yp.to/proto/netstrings.txt) diff --git a/deploy/dockerephemeral/federation-v1/turn-secret.txt b/deploy/dockerephemeral/federation-v1/turn-secret.txt new file mode 100644 index 00000000000..5e558cab2cc --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/turn-secret.txt @@ -0,0 +1 @@ +xMtZyTpu=Leb?YKCoq#BXQR:gG^UrE83dNWzFJ2VcD \ No newline at end of file diff --git a/deploy/dockerephemeral/federation-v1/twilio-credentials.yaml b/deploy/dockerephemeral/federation-v1/twilio-credentials.yaml new file mode 100644 index 00000000000..d64e0ec4f23 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/twilio-credentials.yaml @@ -0,0 +1,2 @@ +sid: "dummy" +token: "dummy" diff --git a/deploy/dockerephemeral/federation-v1/zauth-privkeys.txt b/deploy/dockerephemeral/federation-v1/zauth-privkeys.txt new file mode 100644 index 00000000000..7b6d17ed984 --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/zauth-privkeys.txt @@ -0,0 +1,4 @@ +qjIAZtKrpXInwyqgM7JCZ3QeK9B4JGBYAv1_63YjTtgDylLfTTpdwvDYSy32is13biThD03QZAUOhBO042Odrw== +dNLsH_oIA6hJCyw-AwokLz3AukHNghlP3H-pW5Ao1Wy06OI2MGgBwRnvjgfI2l1mgCLPJQflUR-7DsYO0p6zoQ== +drShe2GnggBy-VAW1gdE6myf4UAFcN1ZdixCO8NRuYLv_TO-xNQzRj-8RfemJ4R6Oz-R5KTfP6Oj_Tj0qezDTw== +tZWlAKOCe5-vlQl0TbECvxeIptEBGRrnGSiej-olAFe-46gXpFkWTas2Ci84VUWyhWzRJj4rtBmyJkAm-TMvwQ== diff --git a/deploy/dockerephemeral/federation-v1/zauth-pubkeys.txt b/deploy/dockerephemeral/federation-v1/zauth-pubkeys.txt new file mode 100644 index 00000000000..661fcfc71ba --- /dev/null +++ b/deploy/dockerephemeral/federation-v1/zauth-pubkeys.txt @@ -0,0 +1,4 @@ +A8pS3006XcLw2Est9orNd24k4Q9N0GQFDoQTtONjna8= +tOjiNjBoAcEZ744HyNpdZoAizyUH5VEfuw7GDtKes6E= +7_0zvsTUM0Y_vEX3pieEejs_keSk3z-jo_049Knsw08= +vuOoF6RZFk2rNgovOFVFsoVs0SY-K7QZsiZAJvkzL8E= diff --git a/deploy/dockerephemeral/init_vhosts.sh b/deploy/dockerephemeral/init_vhosts.sh index 688d635e0a5..93746557fa8 100755 --- a/deploy/dockerephemeral/init_vhosts.sh +++ b/deploy/dockerephemeral/init_vhosts.sh @@ -16,5 +16,6 @@ create_vhost d1.example.com create_vhost d2.example.com create_vhost d3.example.com create_vhost federation-v0 +create_vhost federation-v1 echo 'RabbitMQ resources created successfully!' diff --git a/deploy/dockerephemeral/run.sh b/deploy/dockerephemeral/run.sh index 8d9a98cc8be..f6455dacf5c 100755 --- a/deploy/dockerephemeral/run.sh +++ b/deploy/dockerephemeral/run.sh @@ -1,17 +1,34 @@ #!/usr/bin/env bash -set -xe +set -e # run.sh should work no matter what is the current directory SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" DOCKER_FILE="$SCRIPT_DIR/docker-compose.yaml" +FED_VERSIONS=(0 1) + +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" ) + fi +done + +dc() { + docker-compose "${opts[@]}" "$@" +} cleanup () { - docker-compose --file "$DOCKER_FILE" --file "$SCRIPT_DIR/federation-v0.yaml" down + dc down } -docker-compose --file "$DOCKER_FILE" --file "$SCRIPT_DIR/federation-v0.yaml" up -d -trap cleanup EXIT -echo "All Services started successfully, press Ctrl+C to stop them" -# Wait for something to kill this -while true; do sleep 100000000; done +if [ -z "$1" ]; then + dc up -d + trap cleanup EXIT + echo "All Services started successfully, press Ctrl+C to stop them" + # Wait for something to kill this + sleep infinity +else + dc "$@" +fi diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 452b3864685..5ca735fe38e 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -459,6 +459,7 @@ federator: config: optSettings: useSystemCAStore: false + logLevel: Debug tests: {{- if .Values.uploadXml }} config: diff --git a/integration/Setup.hs b/integration/Setup.hs index a9c6110e00b..71b172a7e29 100644 --- a/integration/Setup.hs +++ b/integration/Setup.hs @@ -191,10 +191,11 @@ testHooks hooks = [ "module RunAllTests where", "import Testlib.PTest", "import Prelude", + "import Control.Monad.Trans.Writer", unlines (map ("import qualified " <>) modules), - "allTests :: [Test]", - "allTests =", - " " <> intercalate " <>\n " (map (\(m, n, s, f) -> "mkTests " <> unwords [show m, show n, show s, show f, m <> "." <> n]) tests) + "mkAllTests :: IO [Test]", + "mkAllTests = execWriterT $ do", + unlines (map (\(m, n, s, f) -> " yieldTests " <> unwords [show m, show n, show s, show f, m <> "." <> n]) tests) ] ) pure () diff --git a/integration/integration.cabal b/integration/integration.cabal index 0c4c2f93b6c..75e68530583 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -170,6 +170,7 @@ library Testlib.Run Testlib.RunServices Testlib.Types + Testlib.VersionedFed Testlib.XML build-depends: diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 63e4c61b786..73a7bfe692f 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -227,14 +227,15 @@ withFederatingBackendsAllowDynamic k = do -- | Create two users on different domains such that the one-to-one -- conversation, once finalised, will be hosted on the backend given by the --- input domain. -createOne2OneConversation :: (HasCallStack) => Domain -> App (Value, Value, Value) -createOne2OneConversation owningDomain = do +-- first domain. +createOne2OneConversation :: + (HasCallStack, MakesValue domain1, MakesValue domain2) => + domain1 -> + domain2 -> + App (Value, Value, Value) +createOne2OneConversation owningDomain otherDomain = do owningUser <- randomUser owningDomain def domainName <- owningUser %. "qualified_id.domain" - let otherDomain = case owningDomain of - OwnDomain -> OtherDomain - OtherDomain -> OwnDomain let go = do otherUser <- randomUser otherDomain def otherUserId <- otherUser %. "qualified_id" diff --git a/integration/test/Test/Connection.hs b/integration/test/Test/Connection.hs index d12feb41f01..c0468db6a39 100644 --- a/integration/test/Test/Connection.hs +++ b/integration/test/Test/Connection.hs @@ -22,11 +22,15 @@ import API.Galley import Notifications import SetupHelpers import Testlib.Prelude +import Testlib.VersionedFed import UnliftIO.Async (forConcurrently_) -testConnectWithRemoteUser :: (HasCallStack) => Domain -> App () +testConnectWithRemoteUser :: (HasCallStack) => OneOf Domain AnyFedDomain -> App () testConnectWithRemoteUser owningDomain = do - (alice, bob, one2oneId) <- createOne2OneConversation owningDomain + let otherDomain = case owningDomain of + OneOfA OwnDomain -> OtherDomain + _ -> OwnDomain + (alice, bob, one2oneId) <- createOne2OneConversation owningDomain otherDomain aliceId <- alice %. "qualified_id" getConversation alice one2oneId `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 @@ -79,20 +83,21 @@ testRemoteUserGetsDeleted = do pure charlie - charlieUnconnected <- do - randomUser OtherDomain def + charlieUnconnected <- randomUser OtherDomain def - forConcurrently_ [charliePending, charlieConnected, charlieBlocked, charlieUnconnected] \charlie -> do - deleteUser charlie + forConcurrently_ + [charliePending, charlieConnected, charlieBlocked, charlieUnconnected] + \charlie -> do + deleteUser charlie - -- charlie is on their local backend, so asking should be instant - getConnection charlie alice `bindResponse` \resp -> - resp.status `shouldMatchInt` 404 + -- charlie is on their local backend, so asking should be instant + getConnection charlie alice `bindResponse` \resp -> + resp.status `shouldMatchInt` 404 - -- for alice, charlie is on the remote backend, so the status change - -- may not be instant - getConnection alice charlie `waitForResponse` \resp -> - resp.status `shouldMatchInt` 404 + -- for alice, charlie is on the remote backend, so the status change + -- may not be instant + getConnection alice charlie `waitForResponse` \resp -> + resp.status `shouldMatchInt` 404 testInternalGetConStatusesAll :: (HasCallStack) => App () testInternalGetConStatusesAll = @@ -149,9 +154,11 @@ assertConnectionStatus userFrom userTo connStatus = resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` connStatus -testConnectFromIgnored :: (HasCallStack) => App () -testConnectFromIgnored = do - [alice, bob] <- forM [OwnDomain, OtherDomain] $ flip randomUser def +testConnectFromIgnored :: (HasCallStack) => StaticDomain -> App () +testConnectFromIgnored domain = do + alice <- randomUser OwnDomain def + bob <- randomUser domain def + void $ postConnection bob alice >>= getBody 201 -- set up an initial "ignored" state on Alice's side assertConnectionStatus alice bob "pending" @@ -168,9 +175,11 @@ testConnectFromIgnored = do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "accepted" -testSentFromIgnored :: (HasCallStack) => App () -testSentFromIgnored = do - [alice, bob] <- forM [OwnDomain, OtherDomain] $ flip randomUser def +testSentFromIgnored :: (HasCallStack) => StaticDomain -> App () +testSentFromIgnored domain = do + alice <- randomUser OwnDomain def + bob <- randomUser domain def + -- set up an initial "ignored" state void $ postConnection bob alice >>= getBody 201 void $ putConnection alice bob "ignored" >>= getBody 200 @@ -185,9 +194,9 @@ testSentFromIgnored = do void $ putConnection alice bob "accepted" >>= getBody 200 assertConnectionStatus alice bob "sent" -testConnectFromBlocked :: (HasCallStack) => App () -testConnectFromBlocked = do - (alice, bob, one2oneId) <- createOne2OneConversation OwnDomain +testConnectFromBlocked :: (HasCallStack) => StaticDomain -> App () +testConnectFromBlocked domain = do + (alice, bob, one2oneId) <- createOne2OneConversation OwnDomain domain bobId <- bob %. "qualified_id" -- set up an initial "blocked" state @@ -211,9 +220,11 @@ testConnectFromBlocked = do qIds <- for others (%. "qualified_id") qIds `shouldMatchSet` [bobId] -testSentFromBlocked :: (HasCallStack) => App () -testSentFromBlocked = do - [alice, bob] <- forM [OwnDomain, OtherDomain] $ flip randomUser def +testSentFromBlocked :: (HasCallStack) => StaticDomain -> App () +testSentFromBlocked domain = do + alice <- randomUser OwnDomain def + bob <- randomUser domain def + -- set up an initial "blocked" state void $ postConnection bob alice >>= getBody 201 void $ putConnection alice bob "blocked" >>= getBody 200 @@ -228,9 +239,10 @@ testSentFromBlocked = do void $ putConnection alice bob "accepted" >>= getBody 200 assertConnectionStatus alice bob "sent" -testCancel :: (HasCallStack) => App () -testCancel = do - [alice, bob] <- forM [OwnDomain, OtherDomain] $ flip randomUser def +testCancel :: (HasCallStack) => StaticDomain -> App () +testCancel domain = do + alice <- randomUser OwnDomain def + bob <- randomUser domain def void $ postConnection alice bob >>= getBody 201 assertConnectionStatus alice bob "sent" @@ -238,16 +250,16 @@ testCancel = do void $ putConnection alice bob "cancelled" >>= getBody 200 assertConnectionStatus alice bob "cancelled" -testConnectionLimits :: (HasCallStack) => App () -testConnectionLimits = do +testConnectionLimits :: (HasCallStack) => StaticDomain -> App () +testConnectionLimits domain = do let connectionLimit = 16 alice <- randomUser OwnDomain def [charlie1, charlie2, charlie3, charlie4] <- replicateM 4 do - randomUser OtherDomain def + randomUser domain def -- connect to connectionLimit - 1 many users (charlie5 : _) <- replicateM (connectionLimit - 1) do - charlie <- randomUser OtherDomain def + charlie <- randomUser domain def postConnection alice charlie `bindResponse` \resp -> resp.status `shouldMatchInt` 201 pure charlie diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index f44eb9eea2f..844fd6e295e 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -35,6 +35,7 @@ import SetupHelpers hiding (deleteUser) import Testlib.One2One (generateRemoteAndConvIdWithDomain) import Testlib.Prelude import Testlib.ResourcePool +import Testlib.VersionedFed testDynamicBackendsFullyConnectedWhenAllowAll :: (HasCallStack) => App () testDynamicBackendsFullyConnectedWhenAllowAll = do @@ -113,10 +114,10 @@ testDynamicBackendsNotFullyConnected = do resp.json %. "status" `shouldMatch` "non-fully-connected" resp.json %. "not_connected" `shouldMatchSet` [domainB, domainC] -testFederationStatus :: (HasCallStack) => App () -testFederationStatus = do +testFederationStatus :: (HasCallStack) => StaticDomain -> App () +testFederationStatus domain = do uid <- randomUser OwnDomain def {BrigI.team = True} - federatingRemoteDomain <- asString OtherDomain + federatingRemoteDomain <- asString domain let invalidDomain = "c.example.com" -- Does not have any srv records bindResponse (getFederationStatus uid []) @@ -261,12 +262,12 @@ testAddMemberV1 domain = do users <- resp.json %. "data.users" >>= asList traverse (%. "qualified_id") users `shouldMatchSet` [bobId] -testConvWithUnreachableRemoteUsers :: (HasCallStack) => App () -testConvWithUnreachableRemoteUsers = do +testConvWithUnreachableRemoteUsers :: (HasCallStack) => StaticDomain -> App () +testConvWithUnreachableRemoteUsers domain = do ([alice, alex, bob, charlie, dylan], domains) <- startDynamicBackends [def, def] $ \domains -> do own <- make OwnDomain & asString - other <- make OtherDomain & asString + other <- make domain & asString users@(alice : others) <- createUsers $ [own, own, other] <> domains forM_ others $ connectTwoUsers alice pure (users, domains) @@ -280,11 +281,11 @@ testConvWithUnreachableRemoteUsers = do regConvs <- filterM (\c -> (==) <$> (c %. "type" & asInt) <*> pure 0) convs regConvs `shouldMatch` ([] :: [Value]) -testAddUserWithUnreachableRemoteUsers :: (HasCallStack) => App () -testAddUserWithUnreachableRemoteUsers = do +testAddUserWithUnreachableRemoteUsers :: (HasCallStack) => StaticDomain -> App () +testAddUserWithUnreachableRemoteUsers domain = do resourcePool <- asks resourcePool own <- make OwnDomain & asString - other <- make OtherDomain & asString + other <- make domain & asString runCodensity (acquireResources 1 resourcePool) $ \[cDom] -> do ([alex, bobId, bradId, chrisId], conv) <- runCodensity (startDynamicBackend cDom mempty) $ \_ -> do [alice, alex, bob, brad, charlie, chris] <- @@ -302,7 +303,7 @@ testAddUserWithUnreachableRemoteUsers = do runCodensity (startDynamicBackend cDom mempty) $ \_ -> void $ addMembers alex conv def {users = [bobId]} >>= getBody 200 - -- even though backend C is unreachable, we know B/OtherDomain and C + -- even though backend C is unreachable, we know B/domain and C -- federate because Bob joined when C was reachable, hence it is OK to add -- brad from B to the conversation. void $ addMembers alex conv def {users = [bradId]} >>= getBody 200 @@ -312,13 +313,13 @@ testAddUserWithUnreachableRemoteUsers = do resp.status `shouldMatchInt` 533 resp.jsonBody %. "unreachable_backends" `shouldMatchSet` [cDom.berDomain] -testAddUnreachableUserFromFederatingBackend :: (HasCallStack) => App () -testAddUnreachableUserFromFederatingBackend = do +testAddUnreachableUserFromFederatingBackend :: (HasCallStack) => StaticDomain -> App () +testAddUnreachableUserFromFederatingBackend domain = do resourcePool <- asks resourcePool runCodensity (acquireResources 1 resourcePool) $ \[cDom] -> do (alice, chadId, conv) <- runCodensity (startDynamicBackend cDom mempty) $ \_ -> do ownDomain <- make OwnDomain & asString - otherDomain <- make OtherDomain & asString + otherDomain <- make domain & asString [alice, bob, charlie, chad] <- createAndConnectUsers [ownDomain, otherDomain, cDom.berDomain, cDom.berDomain] @@ -355,11 +356,11 @@ testAddUnreachable = do -- need to be reachable so we can check that the graph for those domains is fully connected. resp.json %. "unreachable_backends" `shouldMatchSet` [charlieDomain, dylanDomain] -testGetOneOnOneConvInStatusSentFromRemote :: App () -testGetOneOnOneConvInStatusSentFromRemote = do +testGetOneOnOneConvInStatusSentFromRemote :: (HasCallStack) => StaticDomain -> App () +testGetOneOnOneConvInStatusSentFromRemote domain = do d1User <- randomUser OwnDomain def let shouldBeLocal = True - (d2Usr, d2ConvId) <- generateRemoteAndConvIdWithDomain OtherDomain (not shouldBeLocal) d1User + (d2Usr, d2ConvId) <- generateRemoteAndConvIdWithDomain domain (not shouldBeLocal) d1User bindResponse (postConnection d1User d2Usr) $ \r -> do r.status `shouldMatchInt` 201 r.json %. "status" `shouldMatch` "sent" @@ -373,8 +374,8 @@ testGetOneOnOneConvInStatusSentFromRemote = do resp <- getConversation d1User d2ConvId resp.status `shouldMatchInt` 200 -testAddingUserNonFullyConnectedFederation :: (HasCallStack) => App () -testAddingUserNonFullyConnectedFederation = do +testAddingUserNonFullyConnectedFederation :: (HasCallStack) => StaticDomain -> App () +testAddingUserNonFullyConnectedFederation domain = do let overrides = def { brigCfg = @@ -382,7 +383,7 @@ testAddingUserNonFullyConnectedFederation = do } startDynamicBackends [overrides] $ \[dynBackend] -> do own <- asString OwnDomain - other <- asString OtherDomain + other <- asString domain -- Ensure that dynamic backend only federates with own domain, but not other -- domain. @@ -484,10 +485,12 @@ testAddUserWhenOtherBackendOffline = do bindResponse (addMembers alice conv def {users = [alex]}) $ \resp -> do resp.status `shouldMatchInt` 200 -testSynchroniseUserRemovalNotification :: (HasCallStack) => App () -testSynchroniseUserRemovalNotification = do +testSynchroniseUserRemovalNotification :: (HasCallStack) => StaticDomain -> App () +testSynchroniseUserRemovalNotification domain = do resourcePool <- asks resourcePool - [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + ownDomain <- make OwnDomain + otherDomain <- make domain + [alice, bob] <- createAndConnectUsers [ownDomain, otherDomain] runCodensity (acquireResources 1 resourcePool) $ \[dynBackend] -> do (conv, charlie, client) <- runCodensity (startDynamicBackend dynBackend mempty) $ \_ -> do @@ -851,10 +854,10 @@ testGuestLinksExpired = do bindResponse (getJoinCodeConv tm k v) $ \resp -> do resp.status `shouldMatchInt` 404 -testConversationWithFedV0 :: (HasCallStack) => App () -testConversationWithFedV0 = do +testConversationWithLegacyFed :: (HasCallStack) => AnyFedDomain -> App () +testConversationWithLegacyFed domain = do alice <- randomUser OwnDomain def - bob <- randomUser FedV0Domain def + bob <- randomUser domain def withAPIVersion 4 $ connectTwoUsers alice bob conv <- diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 85f67354f3c..376e5fd3258 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -10,6 +10,7 @@ import qualified API.Nginz as Nginz import GHC.Stack import SetupHelpers import Testlib.Prelude +import Testlib.VersionedFed -- | Deleting unknown clients should fail with 404. testDeleteUnknownClient :: (HasCallStack) => App () @@ -194,15 +195,15 @@ testUnrace = do -} retryT $ True `shouldMatch` True -testFedV0Instance :: (HasCallStack) => App () -testFedV0Instance = do - res <- BrigP.getAPIVersion FedV0Domain >>= getJSON 200 - res %. "domain" `shouldMatch` FedV0Domain +testLegacyFedInstance :: (HasCallStack) => AnyFedDomain -> App () +testLegacyFedInstance domain = do + res <- BrigP.getAPIVersion domain >>= getJSON 200 + res %. "domain" `shouldMatch` domain -testFedV0Federation :: (HasCallStack) => App () -testFedV0Federation = do +testLegacyFedFederation :: (HasCallStack) => AnyFedDomain -> App () +testLegacyFedFederation domain = do alice <- randomUser OwnDomain def - bob <- randomUser FedV0Domain def + bob <- randomUser domain def bob' <- BrigP.getUser alice bob >>= getJSON 200 bob' %. "qualified_id" `shouldMatch` (bob %. "qualified_id") diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index 338cae3a7e4..515a82683d0 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -215,11 +215,12 @@ data One2OneScenario One2OneScenarioRemoteConv instance TestCases One2OneScenario where - testCases = - [ MkTestCase "[domain=own]" One2OneScenarioLocal, - MkTestCase "[domain=other;conv=own]" One2OneScenarioLocalConv, - MkTestCase "[domain=other;conv=other]" One2OneScenarioRemoteConv - ] + mkTestCases = + pure + [ MkTestCase "[domain=own]" One2OneScenarioLocal, + MkTestCase "[domain=other;conv=own]" One2OneScenarioLocalConv, + MkTestCase "[domain=other;conv=other]" One2OneScenarioRemoteConv + ] one2OneScenarioUserDomain :: One2OneScenario -> Domain one2OneScenarioUserDomain One2OneScenarioLocal = OwnDomain diff --git a/integration/test/Test/User.hs b/integration/test/Test/User.hs index 183a391d779..7002de72e49 100644 --- a/integration/test/Test/User.hs +++ b/integration/test/Test/User.hs @@ -11,8 +11,9 @@ import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID import SetupHelpers import Testlib.Prelude +import Testlib.VersionedFed -testSupportedProtocols :: (HasCallStack) => Domain -> App () +testSupportedProtocols :: (HasCallStack) => OneOf Domain AnyFedDomain -> App () testSupportedProtocols bobDomain = do alice <- randomUser OwnDomain def alice %. "supported_protocols" `shouldMatchSet` ["proteus"] diff --git a/integration/test/Test/Version.hs b/integration/test/Test/Version.hs index abd59a49958..df0a7ab731c 100644 --- a/integration/test/Test/Version.hs +++ b/integration/test/Test/Version.hs @@ -9,22 +9,24 @@ newtype Versioned' = Versioned' Versioned -- | This instance is used to generate tests for some of the versions. (Not checking all of them for time efficiency reasons) instance TestCases Versioned' where - testCases = - [ MkTestCase "[version=unversioned]" (Versioned' Unversioned), - MkTestCase "[version=versioned]" (Versioned' Versioned), - MkTestCase "[version=v1]" (Versioned' (ExplicitVersion 1)), - MkTestCase "[version=v3]" (Versioned' (ExplicitVersion 3)), - MkTestCase "[version=v6]" (Versioned' (ExplicitVersion 6)) - ] + mkTestCases = + pure + [ MkTestCase "[version=unversioned]" (Versioned' Unversioned), + MkTestCase "[version=versioned]" (Versioned' Versioned), + MkTestCase "[version=v1]" (Versioned' (ExplicitVersion 1)), + MkTestCase "[version=v3]" (Versioned' (ExplicitVersion 3)), + MkTestCase "[version=v6]" (Versioned' (ExplicitVersion 6)) + ] -- | Used to test endpoints that have changed after version 5 data Version5 = Version5 | NoVersion5 instance TestCases Version5 where - testCases = - [ MkTestCase "[version=versioned]" NoVersion5, - MkTestCase "[version=v5]" Version5 - ] + mkTestCases = + pure + [ MkTestCase "[version=versioned]" NoVersion5, + MkTestCase "[version=v5]" Version5 + ] withVersion5 :: Version5 -> App a -> App a withVersion5 Version5 = withAPIVersion 5 diff --git a/integration/test/Testlib/App.hs b/integration/test/Testlib/App.hs index 38188f9a67e..2eecee9d686 100644 --- a/integration/test/Testlib/App.hs +++ b/integration/test/Testlib/App.hs @@ -63,11 +63,6 @@ instance MakesValue Domain where make OwnDomain = asks (String . T.pack . (.domain1)) make OtherDomain = asks (String . T.pack . (.domain2)) -data FedDomain = FedV0Domain - -instance MakesValue FedDomain where - make FedV0Domain = asks (String . T.pack . (.federationV0Domain)) - -- | Run an action, `recoverAll`ing with exponential backoff (min step 8ms, total timeout -- ~15s). Search this package for examples how to use it. -- diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 336383deb72..42ae9ea25f3 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -88,7 +88,8 @@ mkGlobalEnv cfgFile = do Map.fromList $ [ (intConfig.backendOne.originDomain, intConfig.backendOne.beServiceMap), (intConfig.backendTwo.originDomain, intConfig.backendTwo.beServiceMap), - (intConfig.federationV0.originDomain, intConfig.federationV0.beServiceMap) + (intConfig.federationV0.originDomain, intConfig.federationV0.beServiceMap), + (intConfig.federationV1.originDomain, intConfig.federationV1.beServiceMap) ] <> [(berDomain resource, resourceServiceMap resource) | resource <- resources] tempDir <- Codensity $ withSystemTempDirectory "test" @@ -102,6 +103,7 @@ mkGlobalEnv cfgFile = do gDomain2 = intConfig.backendTwo.originDomain, gIntegrationTestHostName = intConfig.integrationTestHostName, gFederationV0Domain = intConfig.federationV0.originDomain, + gFederationV1Domain = intConfig.federationV1.originDomain, gDynamicDomains = (.domain) <$> Map.elems intConfig.dynamicBackends, gDefaultAPIVersion = 6, gManager = manager, @@ -141,8 +143,15 @@ mkEnv ge = do domain2 = gDomain2 ge, integrationTestHostName = gIntegrationTestHostName ge, federationV0Domain = gFederationV0Domain ge, + federationV1Domain = gFederationV1Domain ge, dynamicDomains = gDynamicDomains ge, defaultAPIVersion = gDefaultAPIVersion ge, + -- hardcode version 5 for fed 0 backend + apiVersionByDomain = + Map.fromList + [ (gFederationV0Domain ge, 5), + (gFederationV1Domain ge, 6) + ], manager = gManager ge, servicesCwdBase = gServicesCwdBase ge, removalKeyPaths = gRemovalKeyPaths ge, diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index d155a45c46f..153a8008c79 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -11,6 +11,7 @@ import qualified Data.CaseInsensitive as CI import Data.Function import Data.List import Data.List.Split (splitOn) +import qualified Data.Map as Map import Data.Maybe import Data.String import Data.String.Conversions (cs) @@ -130,15 +131,20 @@ data Versioned = Versioned | Unversioned | ExplicitVersion Int -- OwnDomain ...`. rawBaseRequest :: (HasCallStack, MakesValue domain) => domain -> Service -> Versioned -> String -> App HTTP.Request rawBaseRequest domain service versioned path = do + domainV <- objDomain domain + pathSegsPrefix <- case versioned of Versioned -> do - v <- asks (.defaultAPIVersion) + d <- asString domainV + versionMap <- asks (.apiVersionByDomain) + v <- case Map.lookup d versionMap of + Nothing -> asks (.defaultAPIVersion) + Just v -> pure v pure ["v" <> show v] Unversioned -> pure [] ExplicitVersion v -> do pure ["v" <> show v] - domainV <- objDomain domain serviceMap <- getServiceMap domainV liftIO . HTTP.parseRequest $ diff --git a/integration/test/Testlib/PTest.hs b/integration/test/Testlib/PTest.hs index 037cb276260..850960b2d2c 100644 --- a/integration/test/Testlib/PTest.hs +++ b/integration/test/Testlib/PTest.hs @@ -1,44 +1,45 @@ module Testlib.PTest where +import Control.Monad.Trans.Class +import Control.Monad.Trans.Writer import Data.Bifunctor (bimap) import Data.Char (toLower) import Data.Functor ((<&>)) import Data.Kind import Data.Proxy +import Data.Traversable import GHC.Generics import GHC.TypeLits import Testlib.Env +import Testlib.JSON import Testlib.Types import Prelude type Test = (String, String, String, String, App ()) +yieldTests :: (HasTests x) => String -> String -> String -> String -> x -> WriterT [Test] IO () +yieldTests m n s f x = do + t <- lift (mkTests m n s f x) + tell t + class HasTests x where - mkTests :: String -> String -> String -> String -> x -> [Test] + mkTests :: String -> String -> String -> String -> x -> IO [Test] instance HasTests (App ()) where - mkTests m n s f x = [(m, n, s, f, x)] + mkTests m n s f x = pure [(m, n, s, f, x)] instance (HasTests x, TestCases a) => HasTests (a -> x) where - mkTests m n s f x = - flip foldMap (testCases @a) \tc -> + mkTests m n s f x = do + tcs <- mkTestCases @a + fmap concat $ for tcs $ \tc -> mkTests m (n <> tc.testCaseName) s f (x tc.testCase) data TestCase a = MkTestCase {testCaseName :: String, testCase :: a} - deriving stock (Eq, Ord, Show, Generic) + deriving stock (Eq, Ord, Show, Generic, Functor, Foldable, Traversable) -- | enumerate all members of a bounded enum type --- --- >>> testCases @Bool --- [MkTestCase {testCaseName = "[bool=false]", testCase = False},MkTestCase {testCaseName = "[bool=true]", testCase = True}] --- >>> testCases @Domain --- [MkTestCase {testCaseName = "[domain=owndomain]", testCase = OwnDomain},MkTestCase {testCaseName = "[domain=otherdomain]", testCase = OtherDomain}] --- >>> testCases @Ciphersuite --- [MkTestCase {testCaseName = "[suite=0x0001]", testCase = Ciphersuite {code = "0x0001"}},MkTestCase {testCaseName = "[suite=0xf031]", testCase = Ciphersuite {code = "0xf031"}}] --- >>> testCases @(Tagged "foo" Bool) --- [MkTestCase {testCaseName = "[foo=false]", testCase = MkTagged {unTagged = False}},MkTestCase {testCaseName = "[foo=true]", testCase = MkTagged {unTagged = True}}] class TestCases a where - testCases :: [TestCase a] + mkTestCases :: IO [TestCase a] type Tagged :: Symbol -> Type -> Type newtype Tagged s a = MkTagged {unTagged :: a} @@ -52,21 +53,20 @@ pattern TaggedBool a = MkTagged a {-# COMPLETE TaggedBool #-} -- | only works for outer-most use of `Tagged` (not: `Maybe (Tagged "bla" Bool)`) --- --- >>> testCases @(Tagged "bla" Bool) instance (GEnum (Rep a), KnownSymbol s, Generic a) => TestCases (Tagged s a) where - testCases = - uni @(Rep a) <&> \case - -- replace the toplevel - (Left _ : ls, tc) -> - MkTestCase - { testCaseName = foldr mkName "" (Left (symbolVal @s Proxy) : ls), - testCase = MkTagged $ to tc - } - _ -> error "tagged test cases: impossible" + mkTestCases = + pure $ + uni @(Rep a) <&> \case + -- replace the toplevel + (Left _ : ls, tc) -> + MkTestCase + { testCaseName = foldr mkName "" (Left (symbolVal @s Proxy) : ls), + testCase = MkTagged $ to tc + } + _ -> error "tagged test cases: impossible" instance TestCases Ciphersuite where - testCases = do + mkTestCases = pure $ do suite <- allCiphersuites pure $ MkTestCase @@ -75,20 +75,22 @@ instance TestCases Ciphersuite where } instance TestCases CredentialType where - testCases = - [ MkTestCase "[ctype=basic]" BasicCredentialType, - MkTestCase "[ctype=x509]" X509CredentialType - ] + mkTestCases = + pure + [ MkTestCase "[ctype=basic]" BasicCredentialType, + MkTestCase "[ctype=x509]" X509CredentialType + ] -- | a default instance, normally we don't do such things but this is more convenient in -- the test suite as you don't have to derive anything instance {-# OVERLAPPABLE #-} (Generic a, GEnum (Rep a)) => TestCases a where - testCases = - uni @(Rep a) <&> \(tcn, tc) -> - MkTestCase - { testCaseName = foldr mkName "" tcn, - testCase = to tc - } + mkTestCases = + pure $ + uni @(Rep a) <&> \(tcn, tc) -> + MkTestCase + { testCaseName = foldr mkName "" tcn, + testCase = to tc + } {-# INLINE [1] mkName #-} mkName :: Either String String -> String -> String @@ -118,3 +120,15 @@ instance GEnum U1 where instance (GEnum (Rep k), Generic k) => GEnum (K1 r k) where uni = fmap (K1 . to) <$> uni @(Rep k) + +data OneOf a b = OneOfA a | OneOfB b + +instance (MakesValue a, MakesValue b) => MakesValue (OneOf a b) where + make (OneOfA a) = make a + make (OneOfB b) = make b + +instance (TestCases a, TestCases b) => TestCases (OneOf a b) where + mkTestCases = do + as <- fmap (map (fmap OneOfA)) mkTestCases + bs <- fmap (map (fmap OneOfB)) mkTestCases + pure $ as <> bs diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 431530c91e0..6500b6f71e6 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -99,6 +99,7 @@ main = do let f = testFilter opts cfg = opts.configFile + allTests <- mkAllTests let tests = filter (\(qname, _, _, _) -> f qname) . sortOn (\(qname, _, _, _) -> qname) diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index e77e8b0a457..2ebec043a86 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -105,6 +105,7 @@ data GlobalEnv = GlobalEnv gDomain2 :: String, gIntegrationTestHostName :: String, gFederationV0Domain :: String, + gFederationV1Domain :: String, gDynamicDomains :: [String], gDefaultAPIVersion :: Int, gManager :: HTTP.Manager, @@ -120,6 +121,7 @@ data IntegrationConfig = IntegrationConfig { backendOne :: BackendConfig, backendTwo :: BackendConfig, federationV0 :: BackendConfig, + federationV1 :: BackendConfig, integrationTestHostName :: String, dynamicBackends :: Map String DynamicBackendConfig, rabbitmq :: RabbitMQConfig, @@ -134,6 +136,7 @@ instance FromJSON IntegrationConfig where <$> parseJSON (Object o) <*> o .: fromString "backendTwo" <*> o .: fromString "federation-v0" + <*> o .: fromString "federation-v1" <*> o .: fromString "integrationTestHostName" <*> o .: fromString "dynamicBackends" <*> o .: fromString "rabbitmq" @@ -201,8 +204,10 @@ data Env = Env domain2 :: String, integrationTestHostName :: String, federationV0Domain :: String, + federationV1Domain :: String, dynamicDomains :: [String], defaultAPIVersion :: Int, + apiVersionByDomain :: Map String Int, manager :: HTTP.Manager, servicesCwdBase :: Maybe FilePath, -- | paths to removal keys by signature scheme diff --git a/integration/test/Testlib/VersionedFed.hs b/integration/test/Testlib/VersionedFed.hs new file mode 100644 index 00000000000..1dcadc2bffb --- /dev/null +++ b/integration/test/Testlib/VersionedFed.hs @@ -0,0 +1,65 @@ +module Testlib.VersionedFed where + +import Control.Monad.Reader +import Data.Proxy +import qualified Data.Text as T +import GHC.TypeLits +import System.Environment +import Testlib.PTest +import Testlib.Prelude + +data FedDomain n = FedDomain + +instance MakesValue (FedDomain 0) where + make FedDomain = asks (String . T.pack . (.federationV0Domain)) + +instance MakesValue (FedDomain 1) where + make FedDomain = asks (String . T.pack . (.federationV1Domain)) + +instance (KnownNat n) => TestCases (FedDomain n) where + mkTestCases = + map (fmap (const FedDomain)) + <$> mkFedTestCase "" (natVal (Proxy @n)) + +mkFedTestCase :: String -> Integer -> IO [TestCase Integer] +mkFedTestCase name n = do + v <- lookupEnv $ "ENABLE_FEDERATION_V" <> show n + if v == Just "1" + then pure [MkTestCase name n] + else pure [] + +data AnyFedDomain = AnyFedDomain Integer + +instance MakesValue AnyFedDomain where + make (AnyFedDomain 0) = asks (String . T.pack . (.federationV0Domain)) + make (AnyFedDomain 1) = asks (String . T.pack . (.federationV1Domain)) + make (AnyFedDomain _) = error "invalid federation version" + +instance TestCases AnyFedDomain where + mkTestCases = + map (fmap AnyFedDomain) + . concat + <$> traverse + (uncurry mkFedTestCase) + [("[domain=fed-v" <> show v <> "]", v) | v <- [0, 1]] + +-- | This can be used as an argument for parametrised tests. It will be bound +-- to at least 'OtherDomain', and optionally to legacy federated domains, +-- according to the values of the corresponding environment variables +-- (@ENABLE_FEDERATION_V0@ and similar). +data StaticDomain = StaticDomain | StaticFedDomain Integer + deriving (Eq) + +instance MakesValue StaticDomain where + make StaticDomain = make OtherDomain + make (StaticFedDomain n) = make (AnyFedDomain n) + +instance TestCases StaticDomain where + mkTestCases = do + feds <- + map (fmap StaticFedDomain) + . concat + <$> traverse + (uncurry mkFedTestCase) + [("[domain=fed-v" <> show v <> "]", v) | v <- [0, 1]] + pure $ [MkTestCase "[domain=other]" StaticDomain] <> feds diff --git a/services/integration.yaml b/services/integration.yaml index b33259f873a..201b99ce025 100644 --- a/services/integration.yaml +++ b/services/integration.yaml @@ -183,4 +183,43 @@ federation-v0: host: 127.0.0.1 port: 21091 +federation-v1: + originDomain: federation-v1.example.com + brig: + host: 127.0.0.1 + port: 22082 + cannon: + host: 127.0.0.1 + port: 22083 + cargohold: + host: 127.0.0.1 + port: 22084 + federatorInternal: + host: 127.0.0.1 + port: 22097 + federatorExternal: + host: 127.0.0.1 + port: 22098 + galley: + host: 127.0.0.1 + port: 22085 + gundeck: + host: 127.0.0.1 + port: 22086 + nginz: + host: 127.0.0.1 + port: 22080 + spar: + host: 127.0.0.1 + port: 22088 + proxy: + host: 127.0.0.1 + port: 22087 + backgroundWorker: + host: 127.0.0.1 + port: 22089 + stern: + host: 127.0.0.1 + port: 22091 + integrationTestHostName: "localhost" From d4811cb5bb20925edee3bae91af7d93274da7ab3 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:29:35 +0200 Subject: [PATCH 006/136] Pin http2 to suprress log. (#4147) --- nix/haskell-pins.nix | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 3cc68c3effc..3145ad60cb2 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -270,6 +270,17 @@ let }; }; + # this contains an important fix to the initialization of the window size + # and should be switched to upstream as soon as we can + # version = "5.2.5"; + # This patch also includes suppressing ConnectionIsClosed + http2 = { + src = fetchgit { + url = "https://github.com/wireapp/http2"; + rev = "45653e3caab0642e539fab2681cb09402aae29ca"; + hash = "sha256-L90PQtDw/JFwyltSVFvmfjTAb0ZLhFt9Hl0jbzn+cFQ="; + }; + }; }; hackagePins = { @@ -281,14 +292,6 @@ let }; # start pinned dependencies for http2 - - # this contains an important fix to the initialization of the window size - # and should be switched to upstream as soon as we can - http2 = { - version = "5.2.5"; - sha256 = "sha256-FCd4lPydwWqm2lrhgYtPW+BuXGqmmA8KFrB87SYEowY="; - }; - http-semantics = { version = "0.1.2"; sha256 = "sha256-S4rGBCIKVPpLPumLcVzrPONrbWm8VBizqxI3dXNIfr0="; From 006852e4cfb94db7a65c912f3284aefe7bb0edb3 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Mon, 15 Jul 2024 19:01:55 +0200 Subject: [PATCH 007/136] move ciphersuite updates into the commit lock (#4151) --- changelog.d/3-bug-fixes/ciphersuite-update | 1 + integration/test/Test/MLS/One2One.hs | 36 +++++++++++++++++++ .../Galley/API/MLS/Commit/ExternalCommit.hs | 10 +++++- .../Galley/API/MLS/Commit/InternalCommit.hs | 9 ++++- services/galley/src/Galley/API/MLS/Message.hs | 10 +++--- 5 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 changelog.d/3-bug-fixes/ciphersuite-update diff --git a/changelog.d/3-bug-fixes/ciphersuite-update b/changelog.d/3-bug-fixes/ciphersuite-update new file mode 100644 index 00000000000..81ece68cf71 --- /dev/null +++ b/changelog.d/3-bug-fixes/ciphersuite-update @@ -0,0 +1 @@ +move cipher suite updates into the commit lock diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index 515a82683d0..cc15d1f0d8f 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -19,6 +19,8 @@ module Test.MLS.One2One where import API.Brig import API.Galley +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.Set as Set @@ -268,3 +270,37 @@ testMLSOne2One suite scenario = do conv' <- getMLSOne2OneConversation alice bob >>= getJSON 200 (suiteCode, _) <- assertOne $ T.hexadecimal (T.pack suite.code) conv' %. "cipher_suite" `shouldMatchInt` suiteCode + +-- | This test verifies that one-to-one conversations are created inside the +-- commit lock. There used to be an issue where a conversation could be +-- partially created at the time of setting its ciphersuite, resulting in an +-- incomplete database entry that would prevent further uses of the +-- conversation. +testMLSGhostOne2OneConv :: App () +testMLSGhostOne2OneConv = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] + traverse_ uploadNewKeyPackage [bob1, bob2] + conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetGroup alice1 conv + + doneVar <- liftIO $ newEmptyMVar + let checkConversation = + liftIO (tryReadMVar doneVar) >>= \case + Nothing -> do + bindResponse (getConversation alice conv) $ \resp -> + resp.status `shouldMatchOneOf` [404 :: Int, 403, 200] + checkConversation + Just _ -> pure () + checkConversationIO <- appToIO checkConversation + + createCommit <- + appToIO + $ void + $ createAddCommit alice1 [bob] + >>= sendAndConsumeCommitBundle + + liftIO $ withAsync checkConversationIO $ \a -> do + createCommit + liftIO $ putMVar doneVar () + wait a diff --git a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs index 7a3a815b950..8e67a0d9e4d 100644 --- a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs @@ -32,7 +32,9 @@ import Galley.API.MLS.Removal import Galley.API.MLS.Types import Galley.API.MLS.Util import Galley.Effects +import Galley.Effects.ConversationStore import Galley.Effects.MemberStore +import Galley.Effects.SubConversationStore import Imports import Polysemy import Polysemy.Error @@ -135,11 +137,12 @@ processExternalCommit :: ClientIdentity -> Local ConvOrSubConv -> CipherSuiteTag -> + Bool -> Epoch -> ExternalCommitAction -> Maybe UpdatePath -> Sem r () -processExternalCommit senderIdentity lConvOrSub ciphersuite epoch action updatePath = do +processExternalCommit senderIdentity lConvOrSub ciphersuite ciphersuiteUpdate epoch action updatePath = do let convOrSub = tUnqualified lConvOrSub -- only members can join a subconversation @@ -173,6 +176,11 @@ processExternalCommit senderIdentity lConvOrSub ciphersuite epoch action updateP -- skip proposals for clients already removed by the external commit let indices = maybe id Set.delete action.remove indices0 + -- set cipher suite + when ciphersuiteUpdate $ case convOrSub.id of + Conv cid -> setConversationCipherSuite cid ciphersuite + SubConv cid sub -> setSubConversationCipherSuite cid sub ciphersuite + -- requeue backend remove proposals for the current epoch createAndSendRemoveProposals lConvOrSub' diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs index f0b71cb216f..6b466db99ff 100644 --- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs @@ -39,6 +39,7 @@ import Galley.API.Util import Galley.Data.Conversation.Types hiding (Conversation) import Galley.Data.Conversation.Types qualified as Data import Galley.Effects +import Galley.Effects.ConversationStore import Galley.Effects.MemberStore import Galley.Effects.ProposalStore import Galley.Effects.SubConversationStore @@ -78,11 +79,12 @@ processInternalCommit :: Maybe ConnId -> Local ConvOrSubConv -> CipherSuiteTag -> + Bool -> Epoch -> ProposalAction -> Commit -> Sem r [LocalConversationUpdate] -processInternalCommit senderIdentity con lConvOrSub ciphersuite epoch action commit = do +processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdate epoch action commit = do let convOrSub = tUnqualified lConvOrSub qusr = cidQualifiedUser senderIdentity cm = convOrSub.members @@ -261,6 +263,11 @@ processInternalCommit senderIdentity con lConvOrSub ciphersuite epoch action com for_ newUserClients $ \(qtarget, newClients) -> do addMLSClients (cnvmlsGroupId convOrSub.mlsMeta) qtarget (Set.fromList (Map.assocs newClients)) + -- set cipher suite + when ciphersuiteUpdate $ case convOrSub.id of + Conv cid -> setConversationCipherSuite cid ciphersuite + SubConv cid sub -> setSubConversationCipherSuite cid sub ciphersuite + -- increment epoch number for_ lConvOrSub incrementEpoch diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 8451e05019c..b5eef0766c4 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -211,18 +211,16 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do note (mlsProtocolError "Unsupported ciphersuite") $ cipherSuiteTag bundle.groupInfo.value.groupContext.cipherSuite - case convOrSub.mlsMeta.cnvmlsActiveData of + ciphersuiteUpdate <- case convOrSub.mlsMeta.cnvmlsActiveData of -- if this is the first commit of the conversation, update ciphersuite - Nothing -> do - case convOrSub.id of - Conv cid -> setConversationCipherSuite cid ciphersuite - SubConv cid sub -> setSubConversationCipherSuite cid sub ciphersuite + Nothing -> pure True -- otherwise, make sure the ciphersuite matches Just activeData -> do unless (ciphersuite == activeData.ciphersuite) $ throw $ mlsProtocolError "GroupInfo ciphersuite does not match conversation" unless (bundle.epoch == activeData.epoch) $ throwS @'MLSStaleMessage + pure False senderIdentity <- getSenderIdentity qusr c bundle.sender lConvOrSub @@ -238,6 +236,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do conn lConvOrSub ciphersuite + ciphersuiteUpdate bundle.epoch action bundle.commit.value @@ -253,6 +252,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do senderIdentity lConvOrSub ciphersuite + ciphersuiteUpdate bundle.epoch action bundle.commit.value.path From 6afbea8cbb525abf1a679e94d58898c061550013 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 16 Jul 2024 11:16:10 +0200 Subject: [PATCH 008/136] [fix] API version check comes before method check (#4152) --- changelog.d/3-bug-fixes/PR-4152 | 1 + libs/wire-api/src/Wire/API/VersionInfo.hs | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3-bug-fixes/PR-4152 diff --git a/changelog.d/3-bug-fixes/PR-4152 b/changelog.d/3-bug-fixes/PR-4152 new file mode 100644 index 00000000000..76f53d73ce4 --- /dev/null +++ b/changelog.d/3-bug-fixes/PR-4152 @@ -0,0 +1 @@ +Fixed API version check. It has now precedence over other checks like e.g. method check. diff --git a/libs/wire-api/src/Wire/API/VersionInfo.hs b/libs/wire-api/src/Wire/API/VersionInfo.hs index b7267028b60..590dd7b380d 100644 --- a/libs/wire-api/src/Wire/API/VersionInfo.hs +++ b/libs/wire-api/src/Wire/API/VersionInfo.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE RecordWildCards #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -72,7 +74,7 @@ instance route _ ctx action = route (Proxy @api) ctx $ - fmap const action `addHeaderCheck` withRequest headerCheck + action `addVersionCheck` withRequest headerCheck where headerCheck :: Wai.Request -> DelayedIO () headerCheck req = do @@ -85,6 +87,14 @@ instance when (v >= demote @n) $ delayedFail err404 + -- this hack makes sure that the version check is executed before the method check + addVersionCheck :: Delayed env b -> DelayedIO () -> Delayed env b + addVersionCheck Delayed {..} new = + Delayed + { capturesD = \env -> capturesD env <* new, + .. + } + hoistServerWithContext _ ctx f = hoistServerWithContext (Proxy @api) ctx f From 2744d2b8be0ecce723c0b8ebed67670f25412079 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Tue, 16 Jul 2024 22:48:57 +0200 Subject: [PATCH 009/136] WPB-10204 Add text status field to user (profile) data (#4155) * add textStatus to user record * changelog * hide ctor of text status * update some golden tests * gen nix packages * clean up --------- Co-authored-by: Leif Battermann --- changelog.d/2-features/WPB-10204 | 1 + libs/wire-api/src/Wire/API/User.hs | 11 ++++ libs/wire-api/src/Wire/API/User/Profile.hs | 22 +++++++ libs/wire-api/src/Wire/API/UserEvent.hs | 4 ++ .../API/Golden/Generated/SelfProfile_user.hs | 20 +++--- .../API/Golden/Generated/UserProfile_user.hs | 19 +++--- .../API/Golden/Generated/UserUpdate_user.hs | 16 ++--- .../Wire/API/Golden/Generated/User_user.hs | 6 ++ .../Wire/API/Golden/Manual/ListUsersById.hs | 3 + .../Test/Wire/API/Golden/Manual/UserEvent.hs | 4 ++ .../testObject_ListUsersById_user_2.json | 61 +++++++++++-------- .../golden/testObject_SelfProfile_user_1.json | 3 +- .../test/golden/testObject_UserEvent_1.json | 3 +- .../test/golden/testObject_UserEvent_2.json | 3 +- .../test/golden/testObject_UserEvent_6.json | 3 +- .../golden/testObject_UserProfile_user_1.json | 3 +- .../golden/testObject_UserUpdate_user_2.json | 3 +- .../test/golden/testObject_User_user_2.json | 3 +- libs/wire-api/test/unit/Test/Wire/API/User.hs | 2 +- libs/wire-subsystems/src/Wire/StoredUser.hs | 2 + libs/wire-subsystems/src/Wire/UserStore.hs | 3 +- .../src/Wire/UserStore/Cassandra.hs | 6 +- .../wire-subsystems/src/Wire/UserSubsystem.hs | 2 + .../src/Wire/UserSubsystem/Interpreter.hs | 2 + .../Wire/UserSubsystem/InterpreterSpec.hs | 1 + services/brig/brig.cabal | 2 + services/brig/default.nix | 2 + services/brig/src/Brig/API/Public.hs | 1 + services/brig/src/Brig/Data/User.hs | 19 ++++-- services/brig/src/Brig/Provider/API.hs | 2 +- services/brig/src/Brig/Schema/Run.hs | 4 +- .../brig/src/Brig/Schema/V83_AddTextStatus.hs | 35 +++++++++++ services/brig/test/integration/API/Team.hs | 2 +- .../brig/test/integration/API/User/Account.hs | 10 ++- services/brig/test/integration/Util.hs | 1 + services/galley/test/integration/API/Util.hs | 1 + .../Test/Spar/Scim/UserSpec.hs | 2 +- 37 files changed, 212 insertions(+), 75 deletions(-) create mode 100644 changelog.d/2-features/WPB-10204 create mode 100644 services/brig/src/Brig/Schema/V83_AddTextStatus.hs diff --git a/changelog.d/2-features/WPB-10204 b/changelog.d/2-features/WPB-10204 new file mode 100644 index 00000000000..40f979f1e62 --- /dev/null +++ b/changelog.d/2-features/WPB-10204 @@ -0,0 +1 @@ +A text status field was added to user and user profile diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index e24f63536f1..8465f2f1e6f 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -479,6 +479,7 @@ instance (1 <= max) => ToJSON (LimitedQualifiedUserIdList max) where data UserProfile = UserProfile { profileQualifiedId :: Qualified UserId, profileName :: Name, + profileTextStatus :: Maybe TextStatus, -- | DEPRECATED profilePict :: Pict, profileAssets :: [Asset], @@ -508,6 +509,8 @@ instance ToSchema UserProfile where .= optional (field "id" (deprecatedSchema "qualified_id" schema)) <*> profileName .= field "name" schema + <*> profileTextStatus + .= maybe_ (optField "text_status" schema) <*> profilePict .= (field "picture" schema <|> pure noPict) <*> profileAssets @@ -562,6 +565,8 @@ data User = User userIdentity :: Maybe UserIdentity, -- | required; non-unique userDisplayName :: Name, + -- | text status + userTextStatus :: Maybe TextStatus, -- | DEPRECATED userPict :: Pict, userAssets :: [Asset], @@ -605,6 +610,8 @@ userObjectSchema = .= maybeUserIdentityObjectSchema <*> userDisplayName .= field "name" schema + <*> userTextStatus + .= maybe_ (optField "text_status" schema) <*> userPict .= (fromMaybe noPict <$> optField "picture" schema) <*> userAssets @@ -692,6 +699,7 @@ mkUserProfileWithEmail memail u legalHoldStatus = { profileQualifiedId = userQualifiedId u, profileHandle = userHandle u, profileName = userDisplayName u, + profileTextStatus = userTextStatus u, profilePict = userPict u, profileAssets = userAssets u, profileAccentId = userAccentId u, @@ -1368,6 +1376,7 @@ instance ToSchema UserSet where data UserUpdate = UserUpdate { uupName :: Maybe Name, + uupTextStatus :: Maybe TextStatus, -- | DEPRECATED uupPict :: Maybe Pict, uupAssets :: Maybe [Asset], @@ -1383,6 +1392,8 @@ instance ToSchema UserUpdate where UserUpdate <$> uupName .= maybe_ (optField "name" schema) + <*> uupTextStatus + .= maybe_ (optField "text_status" schema) <*> uupPict .= maybe_ (optField "picture" schema) <*> uupAssets diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index 022c0cc50cf..cb6e3025a78 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -21,6 +21,9 @@ module Wire.API.User.Profile ( Name (..), mkName, + TextStatus, + mkTextStatus, + fromTextStatus, ColourId (..), defaultAccentId, @@ -72,6 +75,25 @@ instance ToSchema Name where deriving instance C.Cql Name +-------------------------------------------------------------------------------- +-- TextStatus + +-- Length is between 1 and 256 characters. +newtype TextStatus = TextStatus + {fromTextStatus :: Text} + deriving stock (Eq, Ord, Show, Generic) + deriving newtype (FromByteString, ToByteString) + deriving (Arbitrary) via (Ranged 1 256 Text) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema TextStatus + +mkTextStatus :: Text -> Either String TextStatus +mkTextStatus txt = TextStatus . fromRange <$> checkedEitherMsg @_ @1 @256 "TextStatus" txt + +instance ToSchema TextStatus where + schema = TextStatus <$> fromTextStatus .= untypedRangedSchema 1 256 schema + +deriving instance C.Cql TextStatus + -------------------------------------------------------------------------------- -- Colour diff --git a/libs/wire-api/src/Wire/API/UserEvent.hs b/libs/wire-api/src/Wire/API/UserEvent.hs index 41cbc98fe75..5f54e7ea54d 100644 --- a/libs/wire-api/src/Wire/API/UserEvent.hs +++ b/libs/wire-api/src/Wire/API/UserEvent.hs @@ -146,6 +146,7 @@ data ClientEvent data UserUpdatedData = UserUpdatedData { eupId :: !UserId, eupName :: !(Maybe Name), + eupTextStatus :: !(Maybe TextStatus), -- | DEPRECATED eupPict :: !(Maybe Pict), eupAccentId :: !(Maybe ColourId), @@ -220,6 +221,7 @@ profileUpdated u UserUpdate {..} = UserUpdated $ (emptyUserUpdatedData u) { eupName = uupName, + eupTextStatus = uupTextStatus, eupPict = uupPict, eupAccentId = uupAccentId, eupAssets = uupAssets @@ -233,6 +235,7 @@ emptyUserUpdatedData u = UserUpdatedData { eupId = u, eupName = Nothing, + eupTextStatus = Nothing, eupPict = Nothing, eupAccentId = Nothing, eupAssets = Nothing, @@ -273,6 +276,7 @@ eventObjectSchema = ( UserUpdatedData <$> eupId .= field "id" schema <*> eupName .= maybe_ (optField "name" schema) + <*> eupTextStatus .= maybe_ (optField "text_status" schema) <*> eupPict .= maybe_ (optField "picture" schema) -- DEPRECATED <*> eupAccentId .= maybe_ (optField "accent_id" schema) <*> eupAssets .= maybe_ (optField "assets" (array schema)) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs index d2ad435f18c..bdefe03af5b 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs @@ -19,16 +19,17 @@ module Test.Wire.API.Golden.Generated.SelfProfile_user where -import Data.Domain (Domain (Domain, _domainText)) +import Data.Domain +import Data.Either.Combinators (rightToMaybe) import Data.Handle -import Data.ISO3166_CountryCodes (CountryCode (PA)) -import Data.Id (Id (Id)) -import Data.Json.Util (readUTCTimeMillis) -import Data.LanguageCodes qualified (ISO639_1 (GL)) -import Data.Qualified (Qualified (Qualified, qDomain, qUnqualified)) -import Data.UUID qualified as UUID (fromString) -import Imports (Bool (False), Maybe (Just), fromJust) -import Wire.API.Provider.Service (ServiceRef (ServiceRef, _serviceRefId, _serviceRefProvider)) +import Data.ISO3166_CountryCodes +import Data.Id +import Data.Json.Util +import Data.LanguageCodes qualified +import Data.Qualified +import Data.UUID qualified as UUID +import Imports +import Wire.API.Provider.Service import Wire.API.User testObject_SelfProfile_user_1 :: SelfProfile @@ -44,6 +45,7 @@ testObject_SelfProfile_user_1 = userIdentity = Just (EmailIdentity (Email {emailLocal = "\a", emailDomain = ""})), userDisplayName = Name {fromName = "@\1457\2598\66242\US\1104967l+\137302\&6\996495^\162211Mu\t"}, + userTextStatus = rightToMaybe $ mkTextStatus "text status", userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 1}, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserProfile_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserProfile_user.hs index 44f9d311e39..10856803c8d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserProfile_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserProfile_user.hs @@ -19,15 +19,16 @@ module Test.Wire.API.Golden.Generated.UserProfile_user where -import Data.Domain (Domain (Domain, _domainText)) +import Data.Domain +import Data.Either.Combinators import Data.Handle -import Data.Id (Id (Id)) -import Data.Json.Util (readUTCTimeMillis) -import Data.LegalHold (UserLegalHoldStatus (..)) -import Data.Qualified (Qualified (Qualified, qDomain, qUnqualified)) -import Data.UUID qualified as UUID (fromString) -import Imports (Bool (False, True), Maybe (Just, Nothing), fromJust) -import Wire.API.Provider.Service (ServiceRef (ServiceRef, _serviceRefId, _serviceRefProvider)) +import Data.Id +import Data.Json.Util +import Data.LegalHold +import Data.Qualified +import Data.UUID qualified as UUID +import Imports +import Wire.API.Provider.Service import Wire.API.User testObject_UserProfile_user_1 :: UserProfile @@ -39,6 +40,7 @@ testObject_UserProfile_user_1 = qDomain = Domain {_domainText = "v.ay64d"} }, profileName = Name {fromName = "\50534\3354]$\169938\183604UV`\nF\f\23427ys'd\bXy\ENQ:\ESC\139288\RSD[<\132982E"}, + profileTextStatus = rightToMaybe $ mkTextStatus "text status", profilePict = Pict {fromPict = []}, profileAssets = [], profileAccentId = ColourId {fromColourId = 2}, @@ -61,6 +63,7 @@ testObject_UserProfile_user_2 = qDomain = Domain {_domainText = "go.7.w-3r8iy2.a"} }, profileName = Name {fromName = "si4v\999679\ESC^'\12447k\21889\NAK?\1082547\NULBw;\b3*R/\164149lrI"}, + profileTextStatus = Nothing, profilePict = Pict {fromPict = []}, profileAssets = [], profileAccentId = ColourId {fromColourId = -1}, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserUpdate_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserUpdate_user.hs index 48829b1f4c7..f5c9ba5d0f3 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserUpdate_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UserUpdate_user.hs @@ -19,23 +19,18 @@ module Test.Wire.API.Golden.Generated.UserUpdate_user where -import Data.Id (Id (Id)) -import Data.UUID qualified as UUID (fromString) -import Imports (Maybe (Just, Nothing), fromJust) +import Data.Either.Combinators +import Data.Id +import Data.UUID qualified as UUID +import Imports import Wire.API.Asset import Wire.API.User - ( Asset (ImageAsset), - AssetSize (AssetComplete), - ColourId (ColourId, fromColourId), - Name (Name, fromName), - Pict (Pict, fromPict), - UserUpdate (..), - ) testObject_UserUpdate_user_1 :: UserUpdate testObject_UserUpdate_user_1 = UserUpdate { uupName = Nothing, + uupTextStatus = Nothing, uupPict = Nothing, uupAssets = Nothing, uupAccentId = Nothing @@ -45,6 +40,7 @@ testObject_UserUpdate_user_2 :: UserUpdate testObject_UserUpdate_user_2 = UserUpdate { uupName = Just (Name {fromName = "~\RSK\1033973w\EMd\156648\59199g"}), + uupTextStatus = rightToMaybe $ mkTextStatus "text status", uupPict = Just (Pict {fromPict = []}), uupAssets = Just [ImageAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "5cd81cc4-c643-4e9c-849c-c596a88c27fd"))) AssetExpiring) (Just AssetComplete)], uupAccentId = Just (ColourId {fromColourId = 3}) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs index 42e501c6f2b..ef2774f918d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs @@ -20,6 +20,7 @@ module Test.Wire.API.Golden.Generated.User_user where import Data.Domain (Domain (Domain, _domainText)) +import Data.Either.Combinators (rightToMaybe) import Data.Handle import Data.ISO3166_CountryCodes ( CountryCode @@ -56,6 +57,7 @@ testObject_User_user_1 = }, userIdentity = Nothing, userDisplayName = Name {fromName = "\NULuv\996028su\28209lRi"}, + userTextStatus = Nothing, userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 1}, @@ -83,6 +85,7 @@ testObject_User_user_2 = { fromName = "4\1067195\&7\ACK\DC2\DC2\ETBbp\SOH\40601\&0Yr\\\984611vKRg\1048403)\1040186S\983500\1057766:3B\ACK\DC3\ETXT" }, + userTextStatus = rightToMaybe $ mkTextStatus "text status", userPict = Pict {fromPict = []}, userAssets = [ ImageAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "5cd81cc4-c643-4e9c-849c-c596a88c27fd"))) AssetExpiring) Nothing, @@ -117,6 +120,7 @@ testObject_User_user_3 = userIdentity = Just (EmailIdentity (Email {emailLocal = "f", emailDomain = "\83115"})), userDisplayName = Name {fromName = ",r\EMXEg0$\98187\RS\SI'uS\ETX/\1009222`\228V.J{\fgE(\rK!\SOp8s9gXO\21810Xj\STX\RS\DC2"}, + userTextStatus = Nothing, userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = -2}, @@ -151,6 +155,7 @@ testObject_User_user_4 = { fromName = "^\1025896F\1083260=&o>f<7\SOq|6\DC1\EM\997351\1054148\ESCf\1014774\170183\DC3bnVAj`^L\f\1047425\USLI\ENQ!\1061384\ETB`\1041537\ETXe\26313\SUBK|" }, + userTextStatus = Nothing, userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 0}, @@ -186,6 +191,7 @@ testObject_User_user_5 = { fromName = "^\1025896F\1083260=&o>f<7\SOq|6\DC1\EM\997351\1054148\ESCf\1014774\170183\DC3bnVAj`^L\f\1047425\USLI\ENQ!\1061384\ETB`\1041537\ETXe\26313\SUBK|" }, + userTextStatus = Nothing, userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 0}, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ListUsersById.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ListUsersById.hs index 0a9db679bd7..ac8515ecdad 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ListUsersById.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ListUsersById.hs @@ -20,6 +20,7 @@ module Test.Wire.API.Golden.Manual.ListUsersById where import Data.Domain +import Data.Either.Combinators (rightToMaybe) import Data.Id import Data.LegalHold import Data.Qualified @@ -41,6 +42,7 @@ profile1 = UserProfile { profileQualifiedId = Qualified user1 domain1, profileName = Name "user1", + profileTextStatus = Nothing, profilePict = Pict [], profileAssets = [], profileAccentId = ColourId 0, @@ -57,6 +59,7 @@ profile2 = UserProfile { profileQualifiedId = Qualified user2 domain2, profileName = Name "user2", + profileTextStatus = rightToMaybe $ mkTextStatus "text status", profilePict = Pict [], profileAssets = [], profileAccentId = ColourId 0, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs index 7ae86628304..b593b1b17b2 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs @@ -38,6 +38,7 @@ where import Data.Aeson (toJSON) import Data.Domain +import Data.Either.Combinators (rightToMaybe) import Data.ISO3166_CountryCodes import Data.Id import Data.Json.Util @@ -89,6 +90,7 @@ testObject_UserEvent_6 = ( UserUpdatedData (userId alice) (Just alice.userDisplayName) + alice.userTextStatus (Just alice.userPict) (Just alice.userAccentId) (Just alice.userAssets) @@ -202,6 +204,7 @@ alice = }, userIdentity = Nothing, userDisplayName = Name "alice", + userTextStatus = rightToMaybe $ mkTextStatus "text status", userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 1}, @@ -229,6 +232,7 @@ bob = }, userIdentity = Nothing, userDisplayName = Name "bob", + userTextStatus = rightToMaybe $ mkTextStatus "text status", userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 2}, diff --git a/libs/wire-api/test/golden/testObject_ListUsersById_user_2.json b/libs/wire-api/test/golden/testObject_ListUsersById_user_2.json index cdc89915f2d..aebbe2c3d03 100644 --- a/libs/wire-api/test/golden/testObject_ListUsersById_user_2.json +++ b/libs/wire-api/test/golden/testObject_ListUsersById_user_2.json @@ -1,27 +1,36 @@ -{ "found" : - [ { "qualified_id" : - { "domain" : "example.com" - , "id" : "4f201a43-935e-4e19-8fe0-0a878d3d6e74" - } - , "id" : "4f201a43-935e-4e19-8fe0-0a878d3d6e74" - , "name" : "user1" - , "picture" : [] - , "assets" : [] - , "accent_id" : 0 - , "legalhold_status" : "disabled" - , "supported_protocols" : ["proteus"] - } - , { "qualified_id" : - { "domain" : "test.net" - , "id" : "eb48b095-d96f-4a94-b4ec-2a1d61447e13" - } - , "id" : "eb48b095-d96f-4a94-b4ec-2a1d61447e13" - , "name" : "user2" - , "picture" : [] - , "assets" : [] - , "accent_id" : 0 - , "legalhold_status" : "disabled" - , "supported_protocols" : ["proteus", "mls"] - } - ] +{ + "found": [ + { + "accent_id": 0, + "assets": [], + "id": "4f201a43-935e-4e19-8fe0-0a878d3d6e74", + "legalhold_status": "disabled", + "name": "user1", + "picture": [], + "qualified_id": { + "domain": "example.com", + "id": "4f201a43-935e-4e19-8fe0-0a878d3d6e74" + }, + "supported_protocols": [ + "proteus" + ] + }, + { + "accent_id": 0, + "assets": [], + "id": "eb48b095-d96f-4a94-b4ec-2a1d61447e13", + "legalhold_status": "disabled", + "name": "user2", + "picture": [], + "qualified_id": { + "domain": "test.net", + "id": "eb48b095-d96f-4a94-b4ec-2a1d61447e13" + }, + "supported_protocols": [ + "proteus", + "mls" + ], + "text_status": "text status" + } + ] } diff --git a/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json b/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json index 953ed882d45..01fa58df1a6 100644 --- a/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json +++ b/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json @@ -20,5 +20,6 @@ "supported_protocols": [ "proteus" ], - "team": "00000001-0000-0002-0000-000000000002" + "team": "00000001-0000-0002-0000-000000000002", + "text_status": "text status" } diff --git a/libs/wire-api/test/golden/testObject_UserEvent_1.json b/libs/wire-api/test/golden/testObject_UserEvent_1.json index 09940cd4e9e..6938bd328fe 100644 --- a/libs/wire-api/test/golden/testObject_UserEvent_1.json +++ b/libs/wire-api/test/golden/testObject_UserEvent_1.json @@ -15,6 +15,7 @@ }, "supported_protocols": [ "proteus" - ] + ], + "text_status": "text status" } } diff --git a/libs/wire-api/test/golden/testObject_UserEvent_2.json b/libs/wire-api/test/golden/testObject_UserEvent_2.json index 36ec06060ef..2b051ddd45a 100644 --- a/libs/wire-api/test/golden/testObject_UserEvent_2.json +++ b/libs/wire-api/test/golden/testObject_UserEvent_2.json @@ -15,6 +15,7 @@ }, "supported_protocols": [ "proteus" - ] + ], + "text_status": "text status" } } diff --git a/libs/wire-api/test/golden/testObject_UserEvent_6.json b/libs/wire-api/test/golden/testObject_UserEvent_6.json index 328b2cb2193..33966562feb 100644 --- a/libs/wire-api/test/golden/testObject_UserEvent_6.json +++ b/libs/wire-api/test/golden/testObject_UserEvent_6.json @@ -9,6 +9,7 @@ "name": "alice", "picture": [], "sso_id_deleted": false, - "supported_protocols": [] + "supported_protocols": [], + "text_status": "text status" } } diff --git a/libs/wire-api/test/golden/testObject_UserProfile_user_1.json b/libs/wire-api/test/golden/testObject_UserProfile_user_1.json index 6bc1529086f..2c81d681cc0 100644 --- a/libs/wire-api/test/golden/testObject_UserProfile_user_1.json +++ b/libs/wire-api/test/golden/testObject_UserProfile_user_1.json @@ -11,5 +11,6 @@ }, "supported_protocols": [ "proteus" - ] + ], + "text_status": "text status" } diff --git a/libs/wire-api/test/golden/testObject_UserUpdate_user_2.json b/libs/wire-api/test/golden/testObject_UserUpdate_user_2.json index 50ab64fbde0..3785f90aad5 100644 --- a/libs/wire-api/test/golden/testObject_UserUpdate_user_2.json +++ b/libs/wire-api/test/golden/testObject_UserUpdate_user_2.json @@ -8,5 +8,6 @@ } ], "name": "~\u001eK󼛵w\u0019d𦏨g", - "picture": [] + "picture": [], + "text_status": "text status" } diff --git a/libs/wire-api/test/golden/testObject_User_user_2.json b/libs/wire-api/test/golden/testObject_User_user_2.json index 368503cf207..d11f5369f5d 100644 --- a/libs/wire-api/test/golden/testObject_User_user_2.json +++ b/libs/wire-api/test/golden/testObject_User_user_2.json @@ -32,5 +32,6 @@ "id": "00000000-0000-0000-0000-000000000001", "provider": "00000000-0000-0000-0000-000100000000" }, - "supported_protocols": [] + "supported_protocols": [], + "text_status": "text status" } diff --git a/libs/wire-api/test/unit/Test/Wire/API/User.hs b/libs/wire-api/test/unit/Test/Wire/API/User.hs index 2a5fa7d31e1..ca603cd4a95 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/User.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/User.hs @@ -117,7 +117,7 @@ testUserProfile = do uid <- Id <$> UUID.nextRandom let domain = Domain "example.com" let colour = ColourId 0 - let userProfile = UserProfile (Qualified uid domain) (Name "name") (Pict []) [] colour False Nothing Nothing Nothing Nothing Nothing UserLegalHoldNoConsent defSupportedProtocols + let userProfile = UserProfile (Qualified uid domain) (Name "name") Nothing (Pict []) [] colour False Nothing Nothing Nothing Nothing Nothing UserLegalHoldNoConsent defSupportedProtocols let profileJSONAsText = show $ Aeson.encode userProfile let msg = "toJSON encoding must not convert Nothing to null, but instead omit those json fields for backwards compatibility. UserProfileJSON:" <> profileJSONAsText assertBool msg (not $ "null" `isInfixOf` profileJSONAsText) diff --git a/libs/wire-subsystems/src/Wire/StoredUser.hs b/libs/wire-subsystems/src/Wire/StoredUser.hs index 6a716ba04d6..2e02a0355f3 100644 --- a/libs/wire-subsystems/src/Wire/StoredUser.hs +++ b/libs/wire-subsystems/src/Wire/StoredUser.hs @@ -19,6 +19,7 @@ import Wire.Arbitrary data StoredUser = StoredUser { id :: UserId, name :: Name, + textStatus :: Maybe TextStatus, pict :: Maybe Pict, email :: Maybe Email, ssoId :: Maybe UserSSOId, @@ -79,6 +80,7 @@ mkUserFromStored domain defaultLocale storedUser = { userQualifiedId = (Qualified storedUser.id domain), userIdentity = storedUser.identity, userDisplayName = storedUser.name, + userTextStatus = storedUser.textStatus, userPict = (fromMaybe noPict storedUser.pict), userAssets = (fromMaybe [] storedUser.assets), userAccentId = storedUser.accentId, diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index fc4260a5a3d..3544ec5b35b 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -19,6 +19,7 @@ import Wire.StoredUser -- | see 'UserProfileUpdate'. data StoredUserUpdate = MkStoredUserUpdate { name :: Maybe Name, + textStatus :: Maybe TextStatus, pict :: Maybe Pict, assets :: Maybe [Asset], accentId :: Maybe ColourId, @@ -29,7 +30,7 @@ data StoredUserUpdate = MkStoredUserUpdate deriving (Arbitrary) via GenericUniform StoredUserUpdate instance Default StoredUserUpdate where - def = MkStoredUserUpdate Nothing Nothing Nothing Nothing Nothing Nothing + def = MkStoredUserUpdate Nothing Nothing Nothing Nothing Nothing Nothing Nothing -- | Update user handle (this involves several http requests for locking the required handle). -- The old/previous handle (for deciding idempotency). diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index b6662fbac63..b62e615220e 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -42,6 +42,7 @@ updateUserImpl uid update = setType BatchLogged setConsistency LocalQuorum for_ update.name \n -> addPrepQuery userDisplayNameUpdate (n, uid) + for_ update.textStatus \s -> addPrepQuery userTextStatusUpdate (s, uid) for_ update.pict \p -> addPrepQuery userPictUpdate (p, uid) for_ update.assets \a -> addPrepQuery userAssetsUpdate (a, uid) for_ update.locale \a -> addPrepQuery userLocaleUpdate (a.lLanguage, a.lCountry, uid) @@ -127,7 +128,7 @@ lookupLocaleImpl u = do selectUser :: PrepQuery R (Identity UserId) (TupleType StoredUser) selectUser = - "SELECT id, name, picture, email, sso_id, accent_id, assets, \ + "SELECT id, name, text_status, picture, email, sso_id, accent_id, assets, \ \activated, status, expires, language, country, provider, service, \ \handle, team, managed_by, supported_protocols \ \FROM user where id = ?" @@ -135,6 +136,9 @@ selectUser = userDisplayNameUpdate :: PrepQuery W (Name, UserId) () userDisplayNameUpdate = "UPDATE user SET name = ? WHERE id = ?" +userTextStatusUpdate :: PrepQuery W (TextStatus, UserId) () +userTextStatusUpdate = "UPDATE user SET text_status = ? WHERE id = ?" + userPictUpdate :: PrepQuery W (Pict, UserId) () userPictUpdate = "UPDATE user SET picture = ? WHERE id = ?" diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 16f53f23f1d..e7209dd1ecf 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -31,6 +31,7 @@ data UpdateOriginType -- operations). data UserProfileUpdate = MkUserProfileUpdate { name :: Maybe Name, + textStatus :: Maybe TextStatus, pict :: Maybe Pict, -- DEPRECATED assets :: Maybe [Asset], accentId :: Maybe ColourId, @@ -44,6 +45,7 @@ instance Default UserProfileUpdate where def = MkUserProfileUpdate { name = Nothing, + textStatus = Nothing, pict = Nothing, -- DEPRECATED assets = Nothing, accentId = Nothing, diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 97cecc82093..ea11d3d16d0 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -379,6 +379,7 @@ storedUserUpdate :: UserProfileUpdate -> StoredUserUpdate storedUserUpdate update = MkStoredUserUpdate { name = update.name, + textStatus = update.textStatus, pict = update.pict, assets = update.assets, accentId = update.accentId, @@ -391,6 +392,7 @@ mkProfileUpdateEvent uid update = UserUpdated $ (emptyUserUpdatedData uid) { eupName = update.name, + eupTextStatus = update.textStatus, eupPict = update.pict, eupAccentId = update.accentId, eupAssets = update.assets, diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 4abd27efd0f..2aed0f71b41 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -263,6 +263,7 @@ spec = describe "UserSubsystem.Interpreter" do ( UserUpdated $ (emptyUserUpdatedData alice.id) { eupName = update.name, + eupTextStatus = update.textStatus, eupPict = update.pict, eupAccentId = update.accentId, eupAssets = update.assets, diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index fd3434fe99d..f2e34896e46 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -189,6 +189,7 @@ library Brig.Schema.V80_KeyPackageCiphersuite Brig.Schema.V81_AddFederationRemoteTeams Brig.Schema.V82_DropPhoneColumn + Brig.Schema.V83_AddTextStatus Brig.Schema.V_FUTUREWORK Brig.Team.API Brig.Team.DB @@ -427,6 +428,7 @@ executable brig-integration , cookie , data-default , data-timeout + , either , email-validate , exceptions , extra diff --git a/services/brig/default.nix b/services/brig/default.nix index 4a2369d7813..0d545251678 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -36,6 +36,7 @@ , data-timeout , dns , dns-util +, either , email-validate , enclosed-exceptions , errors @@ -301,6 +302,7 @@ mkDerivation { cookie data-default data-timeout + either email-validate exceptions extended diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index e468afda8af..2c849ed62c1 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -948,6 +948,7 @@ updateUser uid conn uu = do def { name = uu.uupName, pict = uu.uupPict, + textStatus = uu.uupTextStatus, assets = uu.uupAssets, accentId = uu.uupAccentId } diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 7dcd1ed89d5..bd4da155d8b 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -148,7 +148,7 @@ newAccount u inv tid mbHandle = do locale defLoc = fromMaybe defLoc (newUserLocale u) managedBy = fromMaybe defaultManagedBy (newUserManagedBy u) prots = fromMaybe defSupportedProtocols (newUserSupportedProtocols u) - user uid domain l e = User (Qualified uid domain) ident name pict assets colour False l Nothing mbHandle e tid managedBy prots + user uid domain l e = User (Qualified uid domain) ident name Nothing pict assets colour False l Nothing mbHandle e tid managedBy prots newAccountInviteViaScim :: (MonadReader Env m) => UserId -> TeamId -> Maybe Locale -> Name -> Email -> m UserAccount newAccountInviteViaScim uid tid locale name email = do @@ -162,6 +162,7 @@ newAccountInviteViaScim uid tid locale name email = do (Qualified uid domain) (Just $ EmailIdentity email) name + Nothing (Pict []) [] defaultAccentId @@ -243,6 +244,7 @@ insertAccount (UserAccount u status) mbConv password activated = retry x5 . batc userInsert ( userId u, userDisplayName u, + userTextStatus u, userPict u, userAssets u, userEmail u, @@ -463,6 +465,7 @@ type Activated = Bool type UserRow = ( UserId, Name, + Maybe TextStatus, Maybe Pict, Maybe Email, Maybe UserSSOId, @@ -484,6 +487,7 @@ type UserRow = type UserRowInsert = ( UserId, Name, + Maybe TextStatus, Pict, [Asset], Maybe Email, @@ -510,7 +514,7 @@ type AccountRow = UserRow usersSelect :: PrepQuery R (Identity [UserId]) UserRow usersSelect = - "SELECT id, name, picture, email, sso_id, accent_id, assets, \ + "SELECT id, name, text_status, picture, email, sso_id, accent_id, assets, \ \activated, status, expires, language, country, provider, service, \ \handle, team, managed_by, supported_protocols \ \FROM user where id IN ?" @@ -538,17 +542,17 @@ teamSelect = "SELECT team FROM user WHERE id = ?" accountsSelect :: PrepQuery R (Identity [UserId]) AccountRow accountsSelect = - "SELECT id, name, picture, email, sso_id, accent_id, assets, \ + "SELECT id, name, text_status, picture, email, sso_id, accent_id, assets, \ \activated, status, expires, language, country, provider, \ \service, handle, team, managed_by, supported_protocols \ \FROM user WHERE id IN ?" userInsert :: PrepQuery W UserRowInsert () userInsert = - "INSERT INTO user (id, name, picture, assets, email, sso_id, \ + "INSERT INTO user (id, name, text_status, picture, assets, email, sso_id, \ \accent_id, password, activated, status, expires, language, \ \country, provider, service, handle, team, managed_by, supported_protocols) \ - \VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + \VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" userEmailUpdate :: PrepQuery W (Email, UserId) () userEmailUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email = ? WHERE id = ?" @@ -590,6 +594,7 @@ toUserAccount defaultLocale ( uid, name, + textStatus, pict, email, ssoid, @@ -617,6 +622,7 @@ toUserAccount (Qualified uid domain) ident name + textStatus (fromMaybe noPict pict) (fromMaybe [] assets) accent @@ -641,6 +647,7 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp NoPendingInvitations -> ( \( _uid, _name, + _textStatus, _pict, _email, _ssoid, @@ -664,6 +671,7 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp mk ( uid, name, + textStatus, pict, email, ssoid, @@ -690,6 +698,7 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp (Qualified uid domain) ident name + textStatus (fromMaybe noPict pict) (fromMaybe [] assets) accent diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index c9e13f86cdf..e9c715246f3 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -675,7 +675,7 @@ addBot zuid zcon cid add = do let colour = fromMaybe defaultAccentId (Ext.rsNewBotColour rs) let pict = Pict [] -- Legacy let sref = newServiceRef sid pid - let usr = User (Qualified (botUserId bid) domain) Nothing name pict assets colour False locale (Just sref) Nothing Nothing Nothing ManagedByWire defSupportedProtocols + let usr = User (Qualified (botUserId bid) domain) Nothing name Nothing pict assets colour False locale (Just sref) Nothing Nothing Nothing ManagedByWire defSupportedProtocols let newClt = (newClient PermanentClientType (Ext.rsNewBotLastPrekey rs)) { newClientPrekeys = Ext.rsNewBotPrekeys rs diff --git a/services/brig/src/Brig/Schema/Run.hs b/services/brig/src/Brig/Schema/Run.hs index be608d28578..e991a4ebe54 100644 --- a/services/brig/src/Brig/Schema/Run.hs +++ b/services/brig/src/Brig/Schema/Run.hs @@ -57,6 +57,7 @@ import Brig.Schema.V79_ConnectionRemoteIndex qualified as V79_ConnectionRemoteIn import Brig.Schema.V80_KeyPackageCiphersuite qualified as V80_KeyPackageCiphersuite import Brig.Schema.V81_AddFederationRemoteTeams qualified as V81_AddFederationRemoteTeams import Brig.Schema.V82_DropPhoneColumn qualified as V82_DropPhoneColumn +import Brig.Schema.V83_AddTextStatus qualified as V83_AddTextStatus import Cassandra.MigrateSchema (migrateSchema) import Cassandra.Schema import Control.Exception (finally) @@ -120,7 +121,8 @@ migrations = V79_ConnectionRemoteIndex.migration, V80_KeyPackageCiphersuite.migration, V81_AddFederationRemoteTeams.migration, - V82_DropPhoneColumn.migration + V82_DropPhoneColumn.migration, + V83_AddTextStatus.migration -- FUTUREWORK: undo V41 (searchable flag); we stopped using it in -- https://github.com/wireapp/wire-server/pull/964 -- diff --git a/services/brig/src/Brig/Schema/V83_AddTextStatus.hs b/services/brig/src/Brig/Schema/V83_AddTextStatus.hs new file mode 100644 index 00000000000..3b92c2a127e --- /dev/null +++ b/services/brig/src/Brig/Schema/V83_AddTextStatus.hs @@ -0,0 +1,35 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 Brig.Schema.V83_AddTextStatus + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 83 "Add field for textStatus to user table" $ + schema' + [r| ALTER TABLE user ADD ( + text_status text + ) |] diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 6eafdb1ed9c..772ad7f0d1a 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -187,7 +187,7 @@ testUpdateEvents brig cannon = do newAssets = Just [ImageAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "5cd81cc4-c643-4e9c-849c-c596a88c27fd"))) AssetExpiring) (Just AssetComplete)] newName = Just $ Name "Alice in Wonderland" newPic = Nothing -- Legacy - userUpdate = UserUpdate newName newPic newAssets newColId + userUpdate = UserUpdate newName Nothing newPic newAssets newColId update = RequestBodyLBS . encode $ userUpdate -- Update profile & receive notification WS.bracketRN cannon [alice, bob] $ \[aliceWS, bobWS] -> do diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 8e46b4437dd..e36436bf1d0 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -44,6 +44,7 @@ import Data.ByteString qualified as C8 import Data.ByteString.Char8 (pack) import Data.ByteString.Conversion import Data.Domain +import Data.Either.Combinators import Data.Handle import Data.Id import Data.Json.Util (fromUTCTimeMillis) @@ -759,6 +760,7 @@ testMultipleUsers opts brig = do UserProfile { profileQualifiedId = u5, profileName = Name "u5", + profileTextStatus = Nothing, profilePict = Pict [], profileAssets = [], profileAccentId = ColourId 0, @@ -882,8 +884,9 @@ testUserUpdate brig cannon userJournalWatcher = do (Just AssetComplete) ] mNewName = Just $ aliceNewName + mNewTextStatus = rightToMaybe $ mkTextStatus "fun status" newPic = Nothing -- Legacy - userUpdate = UserUpdate mNewName newPic newAssets newColId + userUpdate = UserUpdate mNewName mNewTextStatus newPic newAssets newColId update = RequestBodyLBS . encode $ userUpdate -- Update profile & receive notification WS.bracketRN cannon [alice, bob] $ \[aliceWS, bobWS] -> do @@ -895,9 +898,10 @@ testUserUpdate brig cannon userJournalWatcher = do -- get the updated profile get (brig . path "/self" . zUser alice) !!! do const 200 === statusCode - const (mNewName, newColId, newAssets) + const (mNewName, mNewTextStatus, newColId, newAssets) === ( \u -> ( fmap userDisplayName u, + userTextStatus =<< u, fmap userAccentId u, fmap userAssets u ) @@ -1275,7 +1279,7 @@ testDeleteWithProfilePic brig cargohold = do (qUnqualified $ ast ^. Asset.assetKey) (Just AssetComplete) ] - userUpdate = UserUpdate Nothing Nothing newAssets Nothing + userUpdate = UserUpdate Nothing Nothing Nothing newAssets Nothing update = RequestBodyLBS . encode $ userUpdate -- Update profile with the uploaded asset put (brig . path "/self" . contentJson . zUser uid . zConn "c" . body update) diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index f2aae1e00de..e1b01f8fe36 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -366,6 +366,7 @@ assertUpdateNotification ws uid upd = WS.assertMatch (5 # Second) ws $ \n -> do let u = j ^?! key "user" u ^? key "id" . _String @?= Just (UUID.toText (toUUID uid)) u ^? key "name" . _String @?= fromName <$> uupName upd + u ^? key "text_status" . _String @?= fromTextStatus <$> uupTextStatus upd u ^? key "accent_id" . _Integral @?= fromColourId <$> uupAccentId upd u ^? key "assets" @?= Just (toJSON (uupAssets upd)) diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index c7b157051ae..09241ea7534 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -2654,6 +2654,7 @@ mkProfile quid name = { profileQualifiedId = quid, profileName = name, profilePict = noPict, + profileTextStatus = Nothing, profileAssets = mempty, profileAccentId = defaultAccentId, profileDeleted = False, diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 5ead460ad3d..4ac175f5afb 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -2367,7 +2367,7 @@ specSCIMManaged = do do displayName <- Name <$> randomAlphaNum - let uupd = UserUpdate (Just displayName) Nothing Nothing Nothing + let uupd = UserUpdate (Just displayName) Nothing Nothing Nothing Nothing call $ updateProfileBrig brig uid uupd !!! do (fmap Wai.label . responseJsonEither @Wai.Error) === const (Right "managed-by-scim") From 3751d5597351a52f93125370048f7588f59eea4d Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 17 Jul 2024 10:17:09 +0200 Subject: [PATCH 010/136] Use user ID hashes as SFT usernames (#4156) * Replace random data with user ID hash in SFT auth * Use base26 to encode sft username * Regenerate nix packages * Add CHANGELOG entry --- changelog.d/2-features/sft-username | 1 + services/brig/brig.cabal | 4 +- services/brig/default.nix | 7 ++- services/brig/src/Brig/Calling.hs | 8 +-- services/brig/src/Brig/Calling/API.hs | 56 +++++++++++++------ services/brig/src/Brig/Calling/Internal.hs | 11 ++++ services/brig/test/unit/Test/Brig/Calling.hs | 15 +++-- .../test/unit/Test/Brig/Calling/Internal.hs | 25 ++++++++- 8 files changed, 94 insertions(+), 33 deletions(-) create mode 100644 changelog.d/2-features/sft-username diff --git a/changelog.d/2-features/sft-username b/changelog.d/2-features/sft-username new file mode 100644 index 00000000000..33c2b5cfa35 --- /dev/null +++ b/changelog.d/2-features/sft-username @@ -0,0 +1 @@ +The SFT and turn usernames returned by `/calls/config/v2` are now deterministically computed from the user ID diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index f2e34896e46..4ab0deeacf9 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -245,6 +245,7 @@ library , cookie >=0.4 , cql , cryptobox-haskell >=0.1.1 + , crypton , currency-codes >=2.0 , data-default , dns @@ -275,6 +276,7 @@ library , jwt-tools , lens >=3.8 , lens-aeson >=1.0 + , memory , metrics-core >=0.3 , metrics-wai >=0.3 , mime @@ -282,7 +284,6 @@ library , mmorph , MonadRandom >=0.5 , mtl >=2.1 - , mwc-random , network >=2.4 , network-conduit-tls , openapi3 @@ -556,6 +557,7 @@ test-suite brig-tests , tasty , tasty-hunit , tasty-quickcheck + , text , time , tinylog , types-common diff --git a/services/brig/default.nix b/services/brig/default.nix index 0d545251678..79e87adb1b0 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -31,6 +31,7 @@ , cookie , cql , cryptobox-haskell +, crypton , currency-codes , data-default , data-timeout @@ -71,6 +72,7 @@ , lens , lens-aeson , lib +, memory , metrics-core , metrics-wai , mime @@ -78,7 +80,6 @@ , mmorph , MonadRandom , mtl -, mwc-random , network , network-conduit-tls , network-uri @@ -190,6 +191,7 @@ mkDerivation { cookie cql cryptobox-haskell + crypton currency-codes data-default dns @@ -220,6 +222,7 @@ mkDerivation { jwt-tools lens lens-aeson + memory metrics-core metrics-wai mime @@ -227,7 +230,6 @@ mkDerivation { mmorph MonadRandom mtl - mwc-random network network-conduit-tls openapi3 @@ -399,6 +401,7 @@ mkDerivation { tasty tasty-hunit tasty-quickcheck + text time tinylog types-common diff --git a/services/brig/src/Brig/Calling.hs b/services/brig/src/Brig/Calling.hs index c9501b3fcad..8c1c2749d32 100644 --- a/services/brig/src/Brig/Calling.hs +++ b/services/brig/src/Brig/Calling.hs @@ -45,7 +45,6 @@ module Brig.Calling turnConfigTTL, turnSecret, turnSHA512, - turnPrng, ) where @@ -74,7 +73,6 @@ import Polysemy.TinyLog import System.FSNotify qualified as FS import System.FilePath qualified as Path import System.Logger qualified as Log -import System.Random.MWC (GenIO, createSystemRandom) import System.Random.Shuffle import UnliftIO (Async) import UnliftIO.Async qualified as Async @@ -189,7 +187,6 @@ srvDiscoveryLoop domain discoveryInterval saveAction = forever $ do data SFTTokenEnv = SFTTokenEnv { sftTokenTTL :: Word32, sftTokenSecret :: ByteString, - sftTokenPRNG :: GenIO, sftTokenSHA :: Digest } @@ -214,7 +211,6 @@ mkSFTTokenEnv :: Digest -> Opts.SFTTokenOptions -> IO SFTTokenEnv mkSFTTokenEnv digest opts = SFTTokenEnv (Opts.sttTTL opts) <$> BS.readFile (Opts.sttSecret opts) - <*> createSystemRandom <*> pure digest -- | Start SFT service discovery synchronously @@ -240,8 +236,7 @@ data TurnEnv = TurnEnv _turnTokenTTL :: Word32, _turnConfigTTL :: Word32, _turnSecret :: ByteString, - _turnSHA512 :: Digest, - _turnPrng :: GenIO + _turnSHA512 :: Digest } makeLenses ''TurnEnv @@ -260,7 +255,6 @@ mkTurnEnv serversSource _turnTokenTTL _turnConfigTTL _turnSecret _turnSHA512 = d TurnServersFromFiles files <$> newIORef NotDiscoveredYet <*> newIORef NotDiscoveredYet - _turnPrng <- createSystemRandom pure $ TurnEnv {..} turnServersV1 :: (MonadIO m) => TurnServers -> m (Discovery (NonEmpty TurnURI)) diff --git a/services/brig/src/Brig/Calling/API.hs b/services/brig/src/Brig/Calling/API.hs index e97c51e19c2..ec4f823a4a0 100644 --- a/services/brig/src/Brig/Calling/API.hs +++ b/services/brig/src/Brig/Calling/API.hs @@ -21,6 +21,7 @@ module Brig.Calling.API ( getCallsConfig, getCallsConfigV2, + base26, -- * Exposed for testing purposes newConfig, @@ -40,22 +41,24 @@ import Brig.Options (ListAllSFTServers (..)) import Brig.Options qualified as Opt import Control.Error (hush, throwE) import Control.Lens +import Crypto.Hash qualified as Crypto +import Data.ByteArray (convert) +import Data.ByteString qualified as B import Data.ByteString.Conversion -import Data.ByteString.Lens +import Data.ByteString.Lazy qualified as BL import Data.Id import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty import Data.Misc (HttpsUrl) import Data.Range import Data.Text.Ascii (AsciiBase64, encodeBase64) -import Data.Text.Strict.Lens import Data.Time.Clock.POSIX +import Data.UUID qualified as UUID import Imports hiding (head) import OpenSSL.EVP.Digest (Digest, hmacBS) import Polysemy import Polysemy.Error qualified as Polysemy import System.Logger.Class qualified as Log -import System.Random.MWC qualified as MWC import Wire.API.Call.Config qualified as Public import Wire.API.Team.Feature (AllFeatureConfigs (afcConferenceCalling), FeatureStatus (FeatureStatusDisabled, FeatureStatusEnabled), wsStatus) import Wire.Error @@ -88,7 +91,7 @@ getCallsConfigV2 uid _ limit = do lift . liftSem . Polysemy.runError - $ newConfig env discoveredServers staticUrl sftEnv' limit sftListAllServers (CallsConfigV2 sftFederation) shared + $ newConfig uid env discoveredServers staticUrl sftEnv' limit sftListAllServers (CallsConfigV2 sftFederation) shared handleNoTurnServers eitherConfig -- | Throws '500 Internal Server Error' when no turn servers are found. This is @@ -124,7 +127,7 @@ getCallsConfig uid _ = do . lift . liftSem . Polysemy.runError - $ newConfig env discoveredServers Nothing Nothing Nothing HideAllSFTServers CallsConfigDeprecated shared + $ newConfig uid env discoveredServers Nothing Nothing Nothing HideAllSFTServers CallsConfigDeprecated shared handleNoTurnServers eitherConfig where -- In order to avoid being backwards incompatible, remove the `transport` query param from the URIs @@ -153,6 +156,7 @@ newConfig :: Member SFT r, Member (Polysemy.Error NoTurnServers) r ) => + UserId -> Calling.TurnEnv -> Discovery (NonEmpty Public.TurnURI) -> Maybe HttpsUrl -> @@ -162,7 +166,7 @@ newConfig :: CallsConfigVersion -> Bool -> Sem r Public.RTCConfiguration -newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers version shared = do +newConfig uid env discoveredServers sftStaticUrl mSftEnv limit listAllServers version shared = do -- randomize list of servers (before limiting the list, to ensure not always the same servers are chosen if limit is set) randomizedUris <- liftIO . randomize @@ -173,7 +177,7 @@ newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers versio -- randomize again (as limitedList partially re-orders uris) finalUris <- liftIO $ randomize limitedUris srvs <- for finalUris $ \uri -> do - u <- liftIO $ genTurnUsername (env ^. turnTokenTTL) (env ^. turnPrng) + u <- liftIO $ genTurnUsername (env ^. turnTokenTTL) pure . Public.rtcIceServer (pure uri) u $ computeCred (env ^. turnSHA512) (env ^. turnSecret) u let staticSft = pure . Public.sftServer <$> sftStaticUrl @@ -211,20 +215,38 @@ newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers versio NonEmpty.nonEmpty (Public.limitServers (NonEmpty.toList uris) (fromRange lim)) & fromMaybe (error "newConfig:limitedList: empty list of servers") - genUsername :: Word32 -> MWC.GenIO -> IO (POSIXTime, Text) - genUsername ttl prng = do - rnd <- view (packedBytes . utf8) <$> replicateM 16 (MWC.uniformR (97, 122) prng) - t <- fromIntegral . (+ ttl) . round <$> getPOSIXTime - pure $ (t, rnd) + hash :: ByteString -> ByteString + hash = convert . Crypto.hash @ByteString @Crypto.SHA256 + + genUsername :: UserId -> Text + genUsername = + base26 + . foldr (\x r -> fromIntegral x + r * 256) 0 + . take 16 + . B.unpack + . hash + . BL.toStrict + . UUID.toByteString + . toUUID - genTurnUsername :: Word32 -> MWC.GenIO -> IO Public.TurnUsername - genTurnUsername = (fmap (uncurry Public.turnUsername) .) . genUsername + getTime :: Word32 -> IO POSIXTime + getTime ttl = fromIntegral . (+ ttl) . round <$> getPOSIXTime - genSFTUsername :: Word32 -> MWC.GenIO -> IO Public.SFTUsername - genSFTUsername = (fmap (uncurry (Public.mkSFTUsername shared)) .) . genUsername + genTurnUsername :: Word32 -> IO Public.TurnUsername + genTurnUsername ttl = + Public.turnUsername + <$> getTime ttl + <*> pure (genUsername uid) + + genSFTUsername :: Word32 -> IO Public.SFTUsername + genSFTUsername ttl = + Public.mkSFTUsername shared + <$> getTime ttl + <*> pure (genUsername uid) computeCred :: (ToByteString a) => Digest -> ByteString -> a -> AsciiBase64 computeCred dig secret = encodeBase64 . hmacBS dig secret . toByteString' + authenticate :: (Member (Embed IO) r) => Public.SFTServer -> @@ -233,7 +255,7 @@ newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers versio maybe (pure . Public.nauthSFTServer) ( \SFTTokenEnv {..} sftsvr -> do - username <- liftIO $ genSFTUsername sftTokenTTL sftTokenPRNG + username <- liftIO $ genSFTUsername sftTokenTTL let credential = computeCred sftTokenSHA sftTokenSecret username pure $ Public.authSFTServer sftsvr username credential ) diff --git a/services/brig/src/Brig/Calling/Internal.hs b/services/brig/src/Brig/Calling/Internal.hs index cc891c77590..d06a25431e4 100644 --- a/services/brig/src/Brig/Calling/Internal.hs +++ b/services/brig/src/Brig/Calling/Internal.hs @@ -20,6 +20,7 @@ module Brig.Calling.Internal where import Control.Lens ((?~)) import Data.ByteString.Char8 qualified as BS import Data.Misc (ensureHttpsUrl) +import Data.Text qualified as T import Imports import URI.ByteString qualified as URI import URI.ByteString.QQ qualified as URI @@ -40,3 +41,13 @@ sftServerFromSrvTarget (SrvTarget host port) = if BS.last bs == '.' then BS.init bs else bs + +base26 :: Integer -> Text +base26 0 = "a" +base26 num = T.pack $ go [] num + where + go :: String -> Integer -> String + go acc 0 = acc + go acc n = + let (q, r) = divMod n 26 + in go (chr (fromIntegral r + ord 'a') : acc) q diff --git a/services/brig/test/unit/Test/Brig/Calling.hs b/services/brig/test/unit/Test/Brig/Calling.hs index 3b22294d16c..0dcf489a12a 100644 --- a/services/brig/test/unit/Test/Brig/Calling.hs +++ b/services/brig/test/unit/Test/Brig/Calling.hs @@ -291,12 +291,13 @@ testSFTStaticDeprecatedEndpoint :: IO () testSFTStaticDeprecatedEndpoint = do env <- fst <$> sftStaticEnv turnUri <- generate arbitrary + uid <- generate arbitrary cfg <- runM @IO . ignoreLogs . interpretSFTInMemory mempty . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) Nothing Nothing Nothing HideAllSFTServers CallsConfigDeprecated True + $ newConfig uid env (Discovered turnUri) Nothing Nothing Nothing HideAllSFTServers CallsConfigDeprecated True assertEqual "when SFT static URL is disabled, sft_servers should be empty." Set.empty @@ -305,6 +306,7 @@ testSFTStaticDeprecatedEndpoint = do -- The v2 endpoint `GET /calls/config/v2` without an SFT static URL testSFTStaticV2NoStaticUrl :: IO () testSFTStaticV2NoStaticUrl = do + uid <- generate arbitrary env <- fst <$> sftStaticEnv let entry1 = SrvEntry 0 0 (SrvTarget "sft1.foo.example.com." 443) entry2 = SrvEntry 0 0 (SrvTarget "sft2.foo.example.com." 443) @@ -323,7 +325,7 @@ testSFTStaticV2NoStaticUrl = do . ignoreLogs . interpretSFTInMemory mempty . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) Nothing (Just sftEnv) (Just . unsafeRange $ 2) ListAllSFTServers (CallsConfigV2 Nothing) True + $ newConfig uid env (Discovered turnUri) Nothing (Just sftEnv) (Just . unsafeRange $ 2) ListAllSFTServers (CallsConfigV2 Nothing) True assertEqual "when SFT static URL is disabled, sft_servers_all should be from SFT environment" (Just . fmap ((^. sftURL) . sftServerFromSrvTarget . srvTarget) . toList $ servers) @@ -334,12 +336,13 @@ testSFTStaticV2StaticUrlError :: IO () testSFTStaticV2StaticUrlError = do (env, staticUrl) <- sftStaticEnv turnUri <- generate arbitrary + uid <- generate arbitrary cfg <- runM @IO . ignoreLogs . interpretSFTInMemory mempty -- an empty lookup map, meaning there was an error . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 2) ListAllSFTServers (CallsConfigV2 Nothing) True + $ newConfig uid env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 2) ListAllSFTServers (CallsConfigV2 Nothing) True assertEqual "when SFT static URL is enabled (and setSftListAllServers is enabled), but returns error, sft_servers_all should be omitted" Nothing @@ -353,12 +356,13 @@ testSFTStaticV2StaticUrlList = do -- for sft_servers_all servers <- generate $ replicateM 10 arbitrary turnUri <- generate arbitrary + uid <- generate arbitrary cfg <- runM @IO . ignoreLogs . interpretSFTInMemory (Map.singleton staticUrl (SFTGetResponse $ Right servers)) . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) ListAllSFTServers (CallsConfigV2 Nothing) True + $ newConfig uid env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) ListAllSFTServers (CallsConfigV2 Nothing) True assertEqual "when SFT static URL and setSftListAllServers are enabled, sft_servers_all should be from /sft_servers_all.json" ((^. sftURL) <$$> Just servers) @@ -371,12 +375,13 @@ testSFTStaticV2ListAllServersDisabled = do -- for sft_servers_all servers <- generate $ replicateM 10 arbitrary turnUri <- generate arbitrary + uid <- generate arbitrary cfg <- runM @IO . ignoreLogs . interpretSFTInMemory (Map.singleton staticUrl (SFTGetResponse . Right $ servers)) . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) HideAllSFTServers (CallsConfigV2 Nothing) True + $ newConfig uid env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) HideAllSFTServers (CallsConfigV2 Nothing) True assertEqual "when SFT static URL is enabled and setSftListAllServers is \"disabled\" then sft_servers_all is missing" Nothing diff --git a/services/brig/test/unit/Test/Brig/Calling/Internal.hs b/services/brig/test/unit/Test/Brig/Calling/Internal.hs index 1967f708268..406108db991 100644 --- a/services/brig/test/unit/Test/Brig/Calling/Internal.hs +++ b/services/brig/test/unit/Test/Brig/Calling/Internal.hs @@ -21,9 +21,11 @@ module Test.Brig.Calling.Internal where import Brig.Calling.Internal import Data.Misc (mkHttpsUrl) +import Data.Text qualified as T import Imports import Test.Tasty import Test.Tasty.HUnit +import Test.Tasty.QuickCheck import URI.ByteString.QQ as URI import Wire.API.Call.Config (sftServer) import Wire.Network.DNS.SRV (SrvTarget (SrvTarget)) @@ -44,5 +46,26 @@ tests = "the dot should be stripped from sft server" expectedServer (sftServerFromSrvTarget $ SrvTarget "sft2.env.example.com" 443) - ] + ], + testCase "base26" $ do + "a" @=? base26 0 + "ba" @=? base26 26 + "cfox" @=? base26 38919, + testProperty "base26 . unbase26 === id" $ \(Base26 s) -> base26 (unbase26 s) === s, + testProperty "unbase26 . base26 === id" $ \(NonNegative n) -> unbase26 (base26 n) === n ] + +newtype Base26 = Base26 Text + deriving (Eq, Show) + +mkBase26 :: String -> Base26 +mkBase26 s = Base26 $ case dropWhile (== 'a') s of + "" -> "a" + str -> T.pack str + +instance Arbitrary Base26 where + arbitrary = + mkBase26 <$> listOf1 (fmap chr (chooseInt (ord 'a', ord 'z'))) + +unbase26 :: Text -> Integer +unbase26 = foldl' (\v c -> fromIntegral (ord c - ord 'a') + v * 26) 0 . T.unpack From 1506ebf4c1593c94eee464aa5f3b076493b643bd Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Wed, 17 Jul 2024 11:26:23 +0200 Subject: [PATCH 011/136] WPB-10207 Match cipher suite tag in query parameters against key packages on replacing key packages (#4158) --- changelog.d/3-bug-fixes/WPB-10207 | 1 + integration/test/API/Brig.hs | 6 +-- integration/test/Test/MLS/KeyPackage.hs | 47 ++++++++++++++++++- services/brig/src/Brig/API/MLS/CipherSuite.hs | 19 ++++++-- services/brig/src/Brig/API/MLS/KeyPackages.hs | 2 +- services/brig/src/Brig/Data/MLS/KeyPackage.hs | 4 +- 6 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-10207 diff --git a/changelog.d/3-bug-fixes/WPB-10207 b/changelog.d/3-bug-fixes/WPB-10207 new file mode 100644 index 00000000000..a02d5d4d3b6 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-10207 @@ -0,0 +1 @@ +Match cipher suite tag in query parameters against key packages on replacing key packages diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index c41865273e9..b0d3bbbc4c4 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -349,14 +349,14 @@ deleteKeyPackages cid kps = do req <- baseRequest cid Brig Versioned ("/mls/key-packages/self/" <> cid.client) submit "DELETE" $ req & addJSONObject ["key_packages" .= kps] -replaceKeyPackages :: ClientIdentity -> [Ciphersuite] -> [ByteString] -> App Response -replaceKeyPackages cid suites kps = do +replaceKeyPackages :: ClientIdentity -> Maybe [Ciphersuite] -> [ByteString] -> App Response +replaceKeyPackages cid mSuites kps = do req <- baseRequest cid Brig Versioned $ "/mls/key-packages/self/" <> cid.client submit "PUT" $ req - & addQueryParams [("ciphersuites", intercalate "," (map (.code) suites))] + & maybe id (\suites -> addQueryParams [("ciphersuites", intercalate "," (map (.code) suites))]) mSuites & addJSONObject ["key_packages" .= map (T.decodeUtf8 . Base64.encode) kps] -- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_self diff --git a/integration/test/Test/MLS/KeyPackage.hs b/integration/test/Test/MLS/KeyPackage.hs index 507d7ff7eb3..5f95025a1da 100644 --- a/integration/test/Test/MLS/KeyPackage.hs +++ b/integration/test/Test/MLS/KeyPackage.hs @@ -238,7 +238,7 @@ testReplaceKeyPackages = do (kps, refs) <- unzip <$> replicateM 3 (generateKeyPackage alice1) -- replace old key packages with new - void $ replaceKeyPackages alice1 [suite] kps >>= getBody 201 + void $ replaceKeyPackages alice1 (Just [suite]) kps >>= getBody 201 checkCount def 4 checkCount suite 3 @@ -274,7 +274,50 @@ testReplaceKeyPackages = do setMLSCiphersuite suite kps2 <- replicateM 2 (fmap fst (generateKeyPackage alice1)) - void $ replaceKeyPackages alice1 [def, suite] (kps1 <> kps2) >>= getBody 201 + void $ replaceKeyPackages alice1 (Just [def, suite]) (kps1 <> kps2) >>= getBody 201 checkCount def 2 checkCount suite 2 + + do + setMLSCiphersuite def + defKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1)) + setMLSCiphersuite suite + suiteKeyPackages <- replicateM 3 (fmap fst (generateKeyPackage alice1)) + + void + $ replaceKeyPackages alice1 (Just []) [] + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + + void + $ replaceKeyPackages alice1 Nothing defKeyPackages + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + + checkCount def 3 + checkCount suite 2 + + let testErrorCases :: (HasCallStack) => Maybe [Ciphersuite] -> [ByteString] -> App () + testErrorCases ciphersuites keyPackages = do + void + $ replaceKeyPackages alice1 ciphersuites keyPackages + `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-protocol-error" + checkCount def 3 + checkCount suite 2 + + testErrorCases (Just []) defKeyPackages + testErrorCases (Just []) suiteKeyPackages + testErrorCases Nothing [] + testErrorCases Nothing suiteKeyPackages + testErrorCases Nothing (suiteKeyPackages <> defKeyPackages) + + testErrorCases (Just [suite]) defKeyPackages + testErrorCases (Just [suite]) (suiteKeyPackages <> defKeyPackages) + testErrorCases (Just [suite]) [] + + testErrorCases (Just [def]) suiteKeyPackages + testErrorCases (Just [def]) (suiteKeyPackages <> defKeyPackages) + testErrorCases (Just [def]) [] diff --git a/services/brig/src/Brig/API/MLS/CipherSuite.hs b/services/brig/src/Brig/API/MLS/CipherSuite.hs index da8182c0a41..c47bd0fedaa 100644 --- a/services/brig/src/Brig/API/MLS/CipherSuite.hs +++ b/services/brig/src/Brig/API/MLS/CipherSuite.hs @@ -15,12 +15,15 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.API.MLS.CipherSuite (getCipherSuite, getCipherSuites) where +module Brig.API.MLS.CipherSuite (getCipherSuite, validateCipherSuites) where import Brig.API.Handler import Brig.API.MLS.KeyPackages.Validation +import Data.Set qualified as Set import Imports import Wire.API.MLS.CipherSuite +import Wire.API.MLS.KeyPackage +import Wire.API.MLS.Serialisation getOneCipherSuite :: CipherSuite -> Handler r CipherSuiteTag getOneCipherSuite s = @@ -32,5 +35,15 @@ getOneCipherSuite s = getCipherSuite :: Maybe CipherSuite -> Handler r CipherSuiteTag getCipherSuite = maybe (pure defCipherSuite) getOneCipherSuite -getCipherSuites :: Maybe [CipherSuite] -> Handler r [CipherSuiteTag] -getCipherSuites = maybe (pure [defCipherSuite]) (traverse getOneCipherSuite) +validateCipherSuites :: + Maybe [CipherSuite] -> + KeyPackageUpload -> + Handler r (Set CipherSuiteTag) +validateCipherSuites suites upload = do + suitesQuery <- Set.fromList <$> maybe (pure [defCipherSuite]) (traverse getOneCipherSuite) suites + when (any isNothing suitesKPM) . void $ mlsProtocolError "uploaded key packages contains unsupported cipher suite" + unless (suitesQuery == suitesKP) . void $ mlsProtocolError "uploaded key packages for unannounced cipher suites" + pure suitesQuery + where + suitesKPM = map (cipherSuiteTag . (.cipherSuite) . value) upload.keyPackages + suitesKP = Set.fromList $ catMaybes suitesKPM diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index 9226a4db7be..d27fc78db78 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -168,6 +168,6 @@ replaceKeyPackages :: Handler r () replaceKeyPackages lusr c (fmap toList -> mSuites) upload = do assertMLSEnabled - suites <- getCipherSuites mSuites + suites <- validateCipherSuites mSuites upload lift $ wrapClient (Data.deleteAllKeyPackages (tUnqualified lusr) c suites) uploadKeyPackages lusr c upload diff --git a/services/brig/src/Brig/Data/MLS/KeyPackage.hs b/services/brig/src/Brig/Data/MLS/KeyPackage.hs index de816cbbbf0..b5242afd6fc 100644 --- a/services/brig/src/Brig/Data/MLS/KeyPackage.hs +++ b/services/brig/src/Brig/Data/MLS/KeyPackage.hs @@ -145,10 +145,10 @@ deleteKeyPackages u c suite refs = deleteQuery = "DELETE FROM mls_key_packages WHERE user = ? AND client = ? AND cipher_suite = ? AND ref in ?" deleteAllKeyPackages :: - (MonadClient m, MonadUnliftIO m) => + (MonadClient m, MonadUnliftIO m, Foldable f) => UserId -> ClientId -> - [CipherSuiteTag] -> + f CipherSuiteTag -> m () deleteAllKeyPackages u c suites = pooledForConcurrentlyN_ 16 suites $ \suite -> From 9a1a176a57642a9cc055659a67f64fc91e88a455 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 18 Jul 2024 11:01:24 +0200 Subject: [PATCH 012/136] Set test certificate lifetime to 10 years (#4162) --- .../docker/elasticsearch-ca.pem | 32 ++++++------ .../docker/elasticsearch-cert.pem | 32 ++++++------ .../docker/elasticsearch-key.pem | 52 +++++++++---------- deploy/dockerephemeral/docker/redis-ca.pem | 34 ++++++------ .../docker/redis-node-1-cert.pem | 32 ++++++------ .../docker/redis-node-1-key.pem | 52 +++++++++---------- .../docker/redis-node-2-cert.pem | 32 ++++++------ .../docker/redis-node-2-key.pem | 52 +++++++++---------- .../docker/redis-node-3-cert.pem | 32 ++++++------ .../docker/redis-node-3-key.pem | 52 +++++++++---------- .../docker/redis-node-4-cert.pem | 32 ++++++------ .../docker/redis-node-4-key.pem | 52 +++++++++---------- .../docker/redis-node-5-cert.pem | 32 ++++++------ .../docker/redis-node-5-key.pem | 52 +++++++++---------- .../docker/redis-node-6-cert.pem | 32 ++++++------ .../docker/redis-node-6-key.pem | 52 +++++++++---------- .../federation-v0/integration-ca.pem | 34 ++++++------ .../federation-v0/integration-leaf-key.pem | 52 +++++++++---------- .../federation-v0/integration-leaf.pem | 30 +++++------ .../rabbitmq-config/certificates/ca-key.pem | 52 +++++++++---------- .../rabbitmq-config/certificates/ca.pem | 34 ++++++------ .../rabbitmq-config/certificates/cert.pem | 32 ++++++------ .../rabbitmq-config/certificates/key.pem | 52 +++++++++---------- hack/bin/gen-certs.sh | 2 +- hack/helm_vars/certs/elasticsearch-ca-key.pem | 52 +++++++++---------- hack/helm_vars/certs/elasticsearch-ca.pem | 32 ++++++------ .../conf/nginz/integration-ca-key.pem | 52 +++++++++---------- .../conf/nginz/integration-ca.pem | 34 ++++++------ .../conf/nginz/integration-leaf-key.pem | 52 +++++++++---------- .../conf/nginz/integration-leaf.pem | 30 +++++------ 30 files changed, 597 insertions(+), 597 deletions(-) diff --git a/deploy/dockerephemeral/docker/elasticsearch-ca.pem b/deploy/dockerephemeral/docker/elasticsearch-ca.pem index f17e9cb41ac..1c6f3128257 100644 --- a/deploy/dockerephemeral/docker/elasticsearch-ca.pem +++ b/deploy/dockerephemeral/docker/elasticsearch-ca.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDLzCCAhegAwIBAgIUMGKU64YSPkGrWyHiXiLsuoKC/9owDQYJKoZIhvcNAQEL +MIIDLzCCAhegAwIBAgIUXMOPFnGTAQ30+xOQ2od/HYZiSwQwDQYJKoZIhvcNAQEL BQAwJzElMCMGA1UEAwwcZWxhc3RpY3NlYXJjaC5jYS5leGFtcGxlLmNvbTAeFw0y -NDA2MTcxMzE1MzFaFw0zNDA2MTUxMzE1MzFaMCcxJTAjBgNVBAMMHGVsYXN0aWNz +NDA3MTgwNjQ4MjBaFw0zNDA3MTYwNjQ4MjBaMCcxJTAjBgNVBAMMHGVsYXN0aWNz ZWFyY2guY2EuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQC/oFJpJMdyG9FTpNw4K9f1pdkNikwbQsx4dokiQBMTu89IMTnNfsHz2IDr -xCKTCKC3oPupniaEPNprYpV6RMz1UPvUYu/IpvOXGeIGlVd9ixcoYN6763R2nZhM -lFS8Tma9mV+e/B0jr9DbV1pUWIPufuPrYXcOotxDO/W5I+GpKVTz/ZGD//O5odX1 -mJzkwqjeqGa1WNdg+/ALiDtVZ/YAKGdfjx81uqc16fYuYRDw3BYImBp5MyNu/jxd -gNxFB1edcVowvcKXVs5pSlay2ad0eQSa0Ux8n3RjfisjTLAHks/4dkPa3hQyBYzm -xwBhMcMDc06yxiCkXsVnlXRn9nf/AgMBAAGjUzBRMB0GA1UdDgQWBBSGMhy1Uvrs -lmdHKAGQ9avMSWhz2jAfBgNVHSMEGDAWgBSGMhy1UvrslmdHKAGQ9avMSWhz2jAP -BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA4vndI6NRcMgzba1y3 -lUPxy40bs/jQajR3A5fmCCX4c0ZeRc4YqE9cdYgeGffCZvPogyYjWDlavOma2uAQ -+3lZ35k0wG9GsU2g3fDIXpUuoSUjfYRLBQ3oqD7VRKYs1rDD87c+91DrsfIVZKF1 -W1RzOOvcW9QX2RHghFS4IluX6LEboo48cKtycA/nfmYDT/L9I4oYjaxc9l+HMUSH -gkQUU1xZnQ9GCqdhL3+2dmn0jvdgJLiFuefMGkE0oP/kFD/bhuOmDhpIDb10Cuck -Nw/nOSbBLINx2qDOa1f3Kox/PesQO4tp0dMp6XqZCOPTQ95vHsIOxuX1d+pxhX2V -ToWP +AoIBAQC5RatgsLSQdpLdJyW+kmpOeyIoJgJVm+76D3eSx5tPnSMtsGolXNJHynR4 +AzJ+h5tiSOhK+x/RPiv7ofjD5P+cl6q0cWckhQ089sp+deRjZrhvTskn/0xkg9W1 +Gk2awYl9Oq9ij2hLSQgtw+QRkv0LkdnQLKyGWQ8N7BLksAcvr9N5gTaLE3PVQQop +SaD72IE+gGk4HDQzCque/eeqNjp5qq9umFlXJk8GX3HfB14VRslGmh5OIs8y/HJG +FRMcbVXJ11T+d6xpmzCFfwXYSN5OJghs1AC6SyirqYEgOigJsMnioYbbyKHh+HLA +YKnY+Bi/KQJQ/ipwzibW9G//058FAgMBAAGjUzBRMB0GA1UdDgQWBBRRMD3Fk8ui +R5eRsezFZrT0oMKlpzAfBgNVHSMEGDAWgBRRMD3Fk8uiR5eRsezFZrT0oMKlpzAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAQSW/I5EbTbsBY4rIA +RX+0S7gbx2+d1fhMFEloLagXipn8vd8PGtX9riYSb4k2KmP4BjeQo3TMpJVTUmh8 +RkppbOLabIcyuCI732PJfwlkRHBThguv905uuzil8IC1mDR7qy6Rkg2ByDRlqAgB +icX/3uG7A6XDNsNrwP8Pj0X/YRSbqLIlFtyQ6RCCOYn1CCUDkciHgDMHlgK90r4s ++hrgtB6zKdMI89hnn8MLbQ+eaZ9UDpYbovBbDvZfEI5AaTlSQTL503+gM5bUgZRl +YT8z44Piip8VhkKwb/31C+tht6gNvqEQBFudusrHrg/KphpFTpAHQgW2vA/z9LJt +vzAR -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/elasticsearch-cert.pem b/deploy/dockerephemeral/docker/elasticsearch-cert.pem index 3a9d4ad013f..302931a0971 100755 --- a/deploy/dockerephemeral/docker/elasticsearch-cert.pem +++ b/deploy/dockerephemeral/docker/elasticsearch-cert.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDMDCCAhigAwIBAgIBADANBgkqhkiG9w0BAQsFADAnMSUwIwYDVQQDDBxlbGFz -dGljc2VhcmNoLmNhLmV4YW1wbGUuY29tMB4XDTI0MDYxNzEzMTUzMVoXDTI0MDcx -NzEzMTUzMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAvWOmaFQEjlt8yqmMtpKFyoFoaGYfsGX5YhNoZOMtEtKX -F6ct1nIcJA9h5awgAlivRKeAkySUZSsWKCibaeNGneN9XTcrhedVpEtcz3js2CbB -1MDyfS9mrgt78uv4zQ5ZHY3wh6LC8k5Aj0aK2PJMNjJogIksO7zKBBGU/L+IMglU -j0kPIn8qiIxgNYRhqxQ0iQpiD065PrjU+jfwz7/Q1Oslq+Xxa9fY2+yYG1XMVdC8 -s2waBl953qv3gNtWZ3O+O3cS5egH/HiNKSWRmaoFebuI3RPAORbRVgHe1k/xTI7V -VE9A2IvHkETmd0Kx4qh66tAc2qayX4c979I7oA382QIDAQABo3oweDAdBgNVHSUE +dGljc2VhcmNoLmNhLmV4YW1wbGUuY29tMB4XDTI0MDcxODA2NDgyMVoXDTM0MDcx +NjA2NDgyMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAo03bK4ePl/aJdjMVBegNVn/usLAGstuSYx6fjrkCpDoa +xlsWhKTIzIS1Kct2az8PS18a54G2UEC1LG2RsonD/TqEqOzm91B9IvQk+4/KEWFk +bI60yaLkoS5aErqCsKVirjrMY7iUHH2theB2VCzhKQbe/NWq6/9gs7kWis8ORKVw +MZS7Mu2mP1isxkpDL/faFEOO8xICg3vT4tPe0w+WCkcuIG8iP+/EuAvJfS7mzlr9 +Dyfxq2rktZPcofgxzlpKo23iPwxwtoTq2FKYLWaF3WPb6jci3YOrWeAbo45Mv7zx +iZc76ihuah7FfuTu6zcBedhuuqPKY0xyQ93opWKswQIDAQABo3oweDAdBgNVHSUE FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwFwYDVR0RAQH/BA0wC4IJbG9jYWxob3N0 -MB0GA1UdDgQWBBQDv9kWb35hEik7oNPxU62c6mt6UzAfBgNVHSMEGDAWgBSGMhy1 -UvrslmdHKAGQ9avMSWhz2jANBgkqhkiG9w0BAQsFAAOCAQEASMywZx+iTfpXH4Tu -C9261lD5Q2HZ3NtNRjGiImRjLhPQUt+5gLwwca0oiHBFa+xIt5MVwhnatJ2x8IZ1 -8ttQiqJUhXC8k62DVq1oMsgIusf+FaVxRQO5uCp5erroeUqJWvumC8013lNDjXHW -/X9PiouUTSndGI/pv6RokK+8VCT8mv7DvwhsTRyely51o7tCqHp6VjtD2wpm9ApW -qpySHKwEdwRSMvOIH2+x/Qa0ykFPKV1T+oqF4xM1x6ob06z3rS74uSK825g7Kyqd -zcjImK2DCVIkA3bSGxONQ/APTNd0TwAw9khhncjLJWjk1as6tuQGtpKWRA/01z+M -KHyT2Q== +MB0GA1UdDgQWBBQYML2G5YnBl34yhh0UEPYr+rGdQjAfBgNVHSMEGDAWgBRRMD3F +k8uiR5eRsezFZrT0oMKlpzANBgkqhkiG9w0BAQsFAAOCAQEALHy5QQLAcThsdt9Q +cL3VJzkPfDSOwFJ4L+hRMZBrPh0flbQn2gziVlbqdpsbh7ou1OTGJRkSnfWQbhqo +3Jeh+i6LhW1YjvX5n/zMq/A+gcE8DgK1m7m0IYaytESmyNbsbYcb8lv3pis+cZWb +ISFH8U/TYvpUgS4PM9+KtdfsPatakSRFlbcy8/3JLe5khnwdmTb0TGKiIlEfIuKh +UxVzJDUB0Fsa7ZRXEBkbQFPlWFIws3oATwMSLzA7AKEHEY3vf67ZzcPxD1gZU1Vn +2W8ABV25cT8EtwRldvZ9E8IwezAeLEi92x9LUaBy2YKBSgmm+wH/0Do8nGG0Zh5/ +hFDF8w== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/elasticsearch-key.pem b/deploy/dockerephemeral/docker/elasticsearch-key.pem index 0f15c75e114..0a1e2ce6c90 100755 --- a/deploy/dockerephemeral/docker/elasticsearch-key.pem +++ b/deploy/dockerephemeral/docker/elasticsearch-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9Y6ZoVASOW3zK -qYy2koXKgWhoZh+wZfliE2hk4y0S0pcXpy3WchwkD2HlrCACWK9Ep4CTJJRlKxYo -KJtp40ad431dNyuF51WkS1zPeOzYJsHUwPJ9L2auC3vy6/jNDlkdjfCHosLyTkCP -RorY8kw2MmiAiSw7vMoEEZT8v4gyCVSPSQ8ifyqIjGA1hGGrFDSJCmIPTrk+uNT6 -N/DPv9DU6yWr5fFr19jb7JgbVcxV0LyzbBoGX3neq/eA21Znc747dxLl6Af8eI0p -JZGZqgV5u4jdE8A5FtFWAd7WT/FMjtVUT0DYi8eQROZ3QrHiqHrq0BzaprJfhz3v -0jugDfzZAgMBAAECggEAKZ8z3CvS0IJ0u4llnl43PxkPnBoNjtPqac6AG+P9bOyR -PiaEoWN0ocwrpLEeW8WnxzvUuwHIBy/f77V06mGDjIGJdKoCS6xamv/hBsu5qYti -/+HjqPV46HknpWyMwmwL0731BaoUk/H0qEhFjYY6j5KmetEqwnosH5bJmn5xbSVU -yrXSoWYX5yX9e2gL2QD3IyVdlIzbRnWwxaHhSUSo4jIlw7t/oaLL2gurzYQVpI6R -a+0HsQ81IulEIMH6iWZCyKn3NCcB/5TifA3e3DwjiYxYxGC2JmxRBb84F0pV8DhX -vETgIhG8vrkz8h2coCzYe7XIwiklMpbijMREpC6QnQKBgQDrfD0JjHWhQ9u1qCAb -E1vN+xVAZ9LUJVFoex+BeOjU0JkcM7i1tQy4mEcq8LGjpPCX7k5XqtMo2UUPDhLf -bppuNWCmFeDJjetj3b/zxEe0UMz4+Z8anW9AZpIJYkeRN6R4/ptiErbygxqr0Wus -inT+qRvjZuSz6ajj8qdeZun9NwKBgQDN42I84JYtViVptJeIG2TcJVSVRq24ADNy -w9V/y53Nc4zRCfR5Yz9pw0pRuFaSgaDZvKFGwU51j/8/t/nDyO7+Y4fFziiDpsFP -LBKc9fI4UTpPP8QEPBxQ7gK4vTN0ouziqQ1bA7kXF3mPh/g1rRBesEEFtnu+lcoR -nnz5HtlebwKBgHH30PqcFhoUY3NJiTBRcC8Cg8iF9w1hekLcw+S/hb/prRBvH8gh -daSpXlgz4WVX4HFHjnbzX/r3HGsq3otwViFciAgZso8ZtoDAw7PQnPtx16Hv/ca9 -xygd/DO6cvSfP2SnpMAUWqKIPRJG6pu47uKJKcwm8iz4uxqHR+VyXXCFAoGBAMPv -jlEDJshUgFxdigv0jgLX3+wEDFTclBm29xqcizu3qJ5TS/6tje639KVaucDJbmto -kU8FrgZBmJdqHV7OfWtJCzAa5wGLE9KlzbzkbrRb0RMUSxYAoq3+JEbtf+eTGb8H -RPeFzoKES6JlsrhaUAbc07R9GrygTmKAIszuJ80vAoGBAIudK5mEcqD8VNMnMAo/ -atWoImkCKLNDkAxr1E34BCorQ3ZvJZ5k+vjxTtiaOIzo6/qj1MAzfHBx22sCyJ36 -4RhCfk56XiAzZiwTRALDcd0l40Z6OoitwsXdXazeG6PMPleZmV+t7lejYfGokeI6 -6jRKZxwsF6kSk7XNnHmeB9qX +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCjTdsrh4+X9ol2 +MxUF6A1Wf+6wsAay25JjHp+OuQKkOhrGWxaEpMjMhLUpy3ZrPw9LXxrngbZQQLUs +bZGyicP9OoSo7Ob3UH0i9CT7j8oRYWRsjrTJouShLloSuoKwpWKuOsxjuJQcfa2F +4HZULOEpBt781arr/2CzuRaKzw5EpXAxlLsy7aY/WKzGSkMv99oUQ47zEgKDe9Pi +097TD5YKRy4gbyI/78S4C8l9LubOWv0PJ/GrauS1k9yh+DHOWkqjbeI/DHC2hOrY +UpgtZoXdY9vqNyLdg6tZ4Bujjky/vPGJlzvqKG5qHsV+5O7rNwF52G66o8pjTHJD +3eilYqzBAgMBAAECggEADKBUmFaDEeRUIjP2kFODUyimwLmIzbkau0LfDQQdV1ZO +GKpqoMyJi//yGUOr1G9L6ZohCeemZtPB+P1AcnX4Fd25lm1kDu4wZrdKyWUyN0rO +SXMf46aObS9Dc/GWG5NbVbj3xvllVkNEsIu3inBCjnoDXA18iYJgLIqHMCoB4pO/ +tDsuH+2GcfnkMsaaMsWkqxAPhCJvzx0Qwnxgm60xE7FGuqkoECoDHT5vFeVREiEt +iLYUJgAOb/uIQGU96EHBVhfMSRNUmFRc/oOA8kHKO6zjyH1uZquUNkxsiYsphNmP +Qqan+Q+ZaJ5wtgyNzdJL8O0dwzBksLGqQOYeQBH4qwKBgQDZBOJUqeldZ2V7r+IS +n1riDnFD8MTr5UvvJhxypxb3xHM68LcqCidmo+MmksLgRZ1mPvsh2t2yLijKrbWU +GcRshSC1vqLUBUr+ptaNtR4WbEWwlFcS+ky/tJbgEnA5ykxlyvfGPDaNHQnf1i5u +kJBuP8BbjjzxVUhj7d2JD+h0mwKBgQDAowPoOffyaPAGfVZh2tQdquWSQmxqF+yw +kBlnZzewk2TdrQi96uOxovJIt+5L5OvIWLYvtWT3/VhQnNeBAmWbTZufbVwyzu9W +N/FlQqaqoR2F5IDCUuJwHD+d3b1l+R2EVSWhMVBBZgNboqMO32YYvDEmMjZWLumc +WSHAhDJD0wKBgHvnDvV5gNQkGUux0mgBdVkFF+PLThLEekMSxkErZrCVB5kKH/kv +jOlL/n9iYUK8XC4pHSZqGBMHyaBV5wqkX4H5zAAX1E0qrHORe4OyeXgh3vP+7WvX +XgRBrbZGsK498rpXVHlonViZ0K5sUVwsy1k2qrNbFS5QG6F6B/aeD4CrAoGAbfPe +BAwqYRvSfPHHf30m+3QTKyNsvDXBrJnjVikNGWVX+kuMpNJQepD2V9lcU8draWRx +QNP5uK7LXN/ZBdL3aeinPh1utbV12LF/wHuFo/joYAcoE0K56qHFEfeB5pWFaoYr +P1FlbJ6spf4zsgaDQPUR3KpmZ1TJlKsvX7JU+m8CgYAz3H23AnF1MEgKvi4xVIwV +ir6yD/Ezb70BB40NdNIib9XAYy7t8JrTmZMhHRLrC88i7vwy5KFd2m75XE/PGy5n +Bc4/O9zua/0wxAgpKKgsiv92H10XVP6yR5xJ//ATO/mGGFllFrZQae01yZZ+qwlA +MilofPD7tfCLoQl/SQ0KyQ== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-ca.pem b/deploy/dockerephemeral/docker/redis-ca.pem index 85d169823c6..0562d9cdc82 100644 --- a/deploy/dockerephemeral/docker/redis-ca.pem +++ b/deploy/dockerephemeral/docker/redis-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-1-cert.pem b/deploy/dockerephemeral/docker/redis-node-1-cert.pem index 37bd4bc75dd..a313b0e38f6 100644 --- a/deploy/dockerephemeral/docker/redis-node-1-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-1-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzJaFw0yNDA3MTcxMzE1MzJa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCkyI311U2ZCCKvnPqv -K0y5A8CI8A3W146s2490ReOZp1xA+l1bPcVJan2N0Na/kLNYo9Lm1xbuNWxadllq -0DnhYTMzP48Rlh69lPL1GjWWI0vZjC4qcv0r6k4DrKVn6yvzs8jQDiykvsIXHcPi -OovQ+ol30xd01KV8k7CsAgFpDON9PgaLKTV5S9I2R+zfTGWHWZCfJlJea2fbf6Ui -O4VwNCO62C46aRLUh0qgdkqvjts1BV9/rzeLQ6UQBU3o4h+9obTOI56ZaUk5fU5v -mb9P0Fj+NLlEqIb2Zl7IopwiIBQSzA+3USFYMQl/fppyCm5X7OrQ1tjJNZ3Tpm+K -7CflAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0xhwSsFAAfMB0GA1UdDgQWBBT7rOsZpR0sBNmrAIIj -raJniMK0FTAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEAcYPqHdms1aYR5aWdqJYPRgydaAdTp14J6jXNQh8NU9jMIV0S -CTVuZwuSMoiMzQXezicHJjMc5YZTvB6SHNi0bidvx48Xuw/JUvlDHVysZgPZYR11 -diAsp+iD0+EB2hR5vHseehwTmyfyVIbFvWXNDNvRrU628gzWUlC4adsUVue8xsfp -dzQQUJKizO4sBM9hpxjF2iWnRDsE/QZPmPpuRD3ys8ym08zUH+R3dLFbNuDkWb9t -mr8IQJI6eALdbcn9vVHlGIluRni4Oe9d/lZ+adbLvbwsZsyeUldn/VzPUIAFE1P9 -HqWg9/JFnc3EQeuLGEqea+nk6WCHJyU5w7GETQ== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjFaFw0zNDA3MTYwNjQ4MjFa +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPy96cVsBu5V6JjKwB +zt+S7QuIXuvwVGAkzNF2R+sOSzBReh4fKP2bhkHp2bKsr9ZxFw7ECndQ8npm7Q46 +P9/Xryj7lSVe9FypB1OAhnpP+cblep+I5y/2hKsvC19y0/ynDRzlBz+2nczjr++A +uQxOHghDuEew3561DLr35K1XFkj7Yg4vHU776/DcmwnWkdcetwxZ2V89q+CHtsMd +Qr/ZZrnod4hwVv9JzV14Z99aJv2aMubchNKZUqRwnv6BHSktzFXR1DPErAGErIVj +IHkTXhhIHSHD9e2VK5Qlk169EzrGNMSP0CT0i+yh4/RQgIJIJfLQPP4uyWppmj/Z +TZYFAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0xhwSsFAAfMB0GA1UdDgQWBBSnv9zqno1wP7lB0bx2 +RSml5I2yEzAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG +9w0BAQsFAAOCAQEAIU2iEbP0fDoDejqMnv38VdumRegI46/HEpWNSmySGyzoCjnH +70FRs8B7qLJd0DZ6ofRUH1p5kmJ3z3SHSngEiCCU7sCwGIl2ahxf3B8tci1wBeRg +qbvcPgCSFqHxR40N7+Uc8Wp3E1UpidWPlQqVHffHAF3KX6M0ooikRb7qNPx4jGPI +gDy0gCFxau1u5V1JiXJ75hjSS3/OFysXweZymTc5t0e8D0/2CQCdvVjEZq4//bvy +xIRHWzOMgoNhW//io4QEUhe2c48QUbTAuXIzzuV23h8BZzIwXiCVlAtQPhJWJsC1 +pHOVb8t/4hdvTAyjmMjwOICSxBqSh5qh80+sgw== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-1-key.pem b/deploy/dockerephemeral/docker/redis-node-1-key.pem index 024e676a48f..eb3937a20ce 100644 --- a/deploy/dockerephemeral/docker/redis-node-1-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-1-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCkyI311U2ZCCKv -nPqvK0y5A8CI8A3W146s2490ReOZp1xA+l1bPcVJan2N0Na/kLNYo9Lm1xbuNWxa -dllq0DnhYTMzP48Rlh69lPL1GjWWI0vZjC4qcv0r6k4DrKVn6yvzs8jQDiykvsIX -HcPiOovQ+ol30xd01KV8k7CsAgFpDON9PgaLKTV5S9I2R+zfTGWHWZCfJlJea2fb -f6UiO4VwNCO62C46aRLUh0qgdkqvjts1BV9/rzeLQ6UQBU3o4h+9obTOI56ZaUk5 -fU5vmb9P0Fj+NLlEqIb2Zl7IopwiIBQSzA+3USFYMQl/fppyCm5X7OrQ1tjJNZ3T -pm+K7CflAgMBAAECggEAKKucSBbVoGXe47+nqrjR5p8rs9Cl5ccNnpHRQgYm4uto -7Fuu04B3M0POicRH4H+XGFNU0Cc5sGDspZYswx1yD7O1FprDFbjazPlYjtChdbUv -+RltYoo/fMmHaEZCC9hCIJPYxisdbyrqzhhJWsqO7C0N5U5rLWl3j7wHAKk9Dl9b -lIdn+AlEiA2cpAk/5rqSZysOv0+v56jh1ay6Hqsp7jm9NAdEoSpJMDwZ/FJxu4PZ -vvxxpACJhyVZkJuuJPc68HbwXIPhEImPzc2TA2Zok+PdgEpnNWpiSGainsaDY6l3 -9XQabptbwhHke0eikQQ+27PXFl5XuvU6qHBEXnFXQQKBgQDdDRILeIUyZHFRJCaJ -uaK9/0IXgTD2p5/ef6a5RcoSEDda+fvKUbd7MlLnLkSOWko27uheVwnpVC3RtgQf -XmMenbAcsh+3a6bDHBK1VwJZAHTb11aLOVzBiof16FTr89OO5gu4WEM2XedOW+Lz -o6QJQsoJfR/6Q7jfwSwNQR54pQKBgQC+1hRuRJsJiaDRIq/T/9dmNhMdPMw1ojM2 -i4EeS1wYUnDr2D/KfpDFKZ/uVobFekH1eqKOQhaUkND9/kHyx/4XBZYcabxxyC2w -SoY26G6ha/gKNlgKhioqAtMc8f7caNwZADrggYIilfPc2uQAaAtKbWb8TuJlGDmF -WcxPRvSOQQKBgQCEZo3GXRu6wTq2VSbYG16E2t1lYrZHJsO061SbaFfOVfQyA8Vy -u1tg6RWK7sWVVjNZj+OSjiObpBYFpDX36/sGnYCcz3v7yvkJqEj0YPdBA+r6upJV -tbf/HNCu08f5xAOVdejTM9qeN8SRxKu9LujTuzN0V4PNzL5xFy0hiz2LGQKBgEO1 -CMKmrKsRnXEV8XQyDWZCQT3aWEmfJrRvgnwRGLe4aEAFFXzussaBIjEZme9ulQBX -Zl06rXBAgSXck+Fje48HeF7UVPu5nhwyFLReewHioLpe1ZXGTCdjoStf4KCqw4xL -PJhy2o0SztbJAqPyRi896ZATHNfpZF8foRFvh00BAoGAIrvHzt0EBLUaquJEKotX -b56F7s7uhgoc/ugcHgAK/b7K82B4/3K8lg3naynmabkU0/rdmcnyVfyXnjVFevpe -szUiOX+zG20LJnN9G589fRxFJM9Vny2WLV+7y7VoDoLl5BkjR9VBUo11aYVW97QH -Vr72lA4ZaymQK2MtMWlTXsQ= +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCPy96cVsBu5V6J +jKwBzt+S7QuIXuvwVGAkzNF2R+sOSzBReh4fKP2bhkHp2bKsr9ZxFw7ECndQ8npm +7Q46P9/Xryj7lSVe9FypB1OAhnpP+cblep+I5y/2hKsvC19y0/ynDRzlBz+2nczj +r++AuQxOHghDuEew3561DLr35K1XFkj7Yg4vHU776/DcmwnWkdcetwxZ2V89q+CH +tsMdQr/ZZrnod4hwVv9JzV14Z99aJv2aMubchNKZUqRwnv6BHSktzFXR1DPErAGE +rIVjIHkTXhhIHSHD9e2VK5Qlk169EzrGNMSP0CT0i+yh4/RQgIJIJfLQPP4uyWpp +mj/ZTZYFAgMBAAECggEAIshtDPK0Ii8T9uBC9D4DGUKDNWXGmzABwK0Vps+fNWot +IixQsHdlHzNy6rr47CotjFYIQZYJhhhdUNvbQu5T+lN5rZ+GdmlUJ6PoyDBfUkyo +VraadA5+LNqrINpWqItMNGlo2aKvAAC8QMA8Ri4c4qGDnMPs/YUeGgveBxw27NdP +ggLyy9Qjg/x7B2evmOVjo74fOHDma4z4LitwcY3rHHeuvlyDxB1rCbv6VbSqiYX6 +UNEwwmQRW7nOp9EGQTJeErAR+cUV17luFlC8KYD0i8IXoeswmfmd09MLyneSLc61 +C0HzKzk8Vkpk+A3aNmX0lvSfY3f7SzN6ejHc9n1OeQKBgQDH9CDKmZKNG6vdYeRo +nje7chDBnDPCilrfyYqcl6s8H538CIhRErFbX7nJA+dAXz1lwmAgl0gfWqRhkHZn +GVRxNp/uG6Q648hhUtVLrIVtiIRWKUjIQdle3rBekw++yvNHW9/Q6lI+F4OERHpN +VeVMJl+Gt1Umxww82xfNR3QceQKBgQC4Gh8LTT7m1pCypFhYlRLS0FJaHxDH1pc/ +7A2DxKJeJ10qDbpNov6KPVi5HyDy2l1cc6Y6MB4VGMDE+6ggxiquCpLel6ESPSl4 +uTerXQkSqRUnmBirimJa4G/77wQDE3gxfcAq6pTHhcYqd9OfjfnShlAdr6rMqM85 +pjGvjaaK7QKBgEeH4Tc5S0EptgkDnSeD+mIXQ0FP9QBSaIIIYor0gzCGCwl/r+x4 +6HPMwfTUbaUMrTU7HRJrrERzM70nZgQp/phltz8CKnVayXNvo5hnxm/R163PJRdm +3zFeLvAWYhqaFf/gMShWu0c1ODpYGPyTjuz4CVJzQYYWzRz0MAai2jnZAoGAVSoY +RFUmjQijBVDLYacMfyNJhVErpRZa/4IGOneDGQUiruqMzY9iKrb4TSLeThm/6J3D +PtW1hNLfkgBMpWSmp75SdNA1/cb3YVZlL0upf81h8OAGQYyRtTJv+151P6sJBfQD +Kpc73hS/ODQYXI4EDGR/uUvjOiu5ORTtlSV07n0CgYB3QkdSdH84v1OVM1C57kqZ +4cbuWqPUrnb2Mevqtcjy4F6uDZxFO29ifXrOwF7LF1ewBJWGg+4KfWg+7Ja80S6u +u/WTtp1qmt9zOM72AnYTbLA8OlfzSL8fJ7QKv9RnDRiIj1j/MFXSogbxKkBSrufX +LbSwLKtIhQ1nfwZVxGSqrg== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-2-cert.pem b/deploy/dockerephemeral/docker/redis-node-2-cert.pem index 4681392ce71..fb3643482cd 100644 --- a/deploy/dockerephemeral/docker/redis-node-2-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-2-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzJaFw0yNDA3MTcxMzE1MzJa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDusjRUdb9cOIGhRXVG -WqsawsdIqljT2r2G7wTtSPfTPpLqMY1rcw1VJdHKG4Kx8SYEZZ02IOjHkV8Ik7/m -0kiKUNo+nRURvNkWsedoQt8/5NvL6O0d15BoHMSjJMkQYKDew2pEbcR3YyLrndjl -qRv4QSaESA1c854IejG1V91Tvk7KC4jqmisZEz6hrHg5XBPGk0cTb3rAQhFgpZo1 -tpjhc9CHQzNv+FM6lgy/n53kpTDYpGJgYN1I+lqU1qN29WHKaMNHXBj3GT9uZ64e -4IPmbCIn+U3+KWZACaD2HbDq8QLcTFxT3kyETH27UPkETDa56pPHgqRWziaNbwgS -cZx5AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0yhwSsFAAgMB0GA1UdDgQWBBRd+gHa2Eis8uVk//hq -jqoBINup4DAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEAV8hQotbxJAdXEyZQjQPmG8AmUZSi4U8LnMDe9od58sD59J7o -m26WbNvq7tDRVpBsrUCk/rfVT8I26h1ImS2tlZtyW5LiKOi9t3I3W1s2fnWk6GBg -tH3SKf0aZRw96RSoYa7DNp4MilRtF3pQF8rg78b3BYaAUezCe2KO9Ddlym2YhAth -rzSY4cek3Gsd62SFyq8ufFq54Q3pImcVF6shGidmqfeAgRRXumekdDSr7rkvEJ+5 -6fAVMhLRu4YGGH5SPF2dauFgpMCgMFHp5uUmAcq4sLbnLZhSckuzaagoaQRZAALT -NdJ9nFHupmUGV/dagYSx+cFccEVJ0nl29YF4qg== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjFaFw0zNDA3MTYwNjQ4MjFa +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQtr75a8WIjdBEN74y +ZfHUnah2x2Poru8Gia0JhsPjhq7oB4HxmIxujq8XQJrnO0kchiuuCZCzICRucpou +LzKLyAQYJ+NmlUZQvp7/vR3gNp/AwjGQVKZ/uBecYoPrYaufXAZhtkabmUCufxqf +d2KHjo6tIEU2JVL3K76Whov0sreUfo6u4+SIBnbxUa0rePsg/El0DQNmgog+Bu5H +XKuKrm7G8GJ2qIMxH+MgfcLxur+8BX9uyAJkC/eo/pDhk9f6Zsb3h0dCDwZY4yQO +pKaN3YnTwHBAsaQlWQS8xazIidUBJ8mMWaTOM1bL8U4sSvB7QwR9gk2AmSlCV4i+ +vSzBAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0yhwSsFAAgMB0GA1UdDgQWBBTPy51DTA9zTKFPrx2/ +VePzSvmlnDAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG +9w0BAQsFAAOCAQEAAChPwq5/b0mALMwMGMWlL+JVtiw0eq9zmsR47AWBnbZH9Hao +sDAJiNUJxSA34xY7toL6Z2rnN/HmsKIqfLuPWk5mWf1fWEgj0E3gLlO623XziYID +YDBl9plvrrCR6omjC+9jaXWHk+HGCllLx4lHCyI58IF+hmCNydhSCvHWp1inouMM +FEq/l9YElna3SZ238G4YppBHpP7fsKlM59/8zPhxXCE4xD2GKfvvWbUThQcbC5Gb +mbnWb0D8EFl0oCW3I9JBMPsjeC1yiFsAkDG5cvT2V4QbEd1zXqawSJgP93/tgOUo +gxmeu3GfOc77PRDTJvd3eLTSQFvWRrK1Jlv8lQ== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-2-key.pem b/deploy/dockerephemeral/docker/redis-node-2-key.pem index ab7fffcf64c..53307028d70 100644 --- a/deploy/dockerephemeral/docker/redis-node-2-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-2-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDusjRUdb9cOIGh -RXVGWqsawsdIqljT2r2G7wTtSPfTPpLqMY1rcw1VJdHKG4Kx8SYEZZ02IOjHkV8I -k7/m0kiKUNo+nRURvNkWsedoQt8/5NvL6O0d15BoHMSjJMkQYKDew2pEbcR3YyLr -ndjlqRv4QSaESA1c854IejG1V91Tvk7KC4jqmisZEz6hrHg5XBPGk0cTb3rAQhFg -pZo1tpjhc9CHQzNv+FM6lgy/n53kpTDYpGJgYN1I+lqU1qN29WHKaMNHXBj3GT9u -Z64e4IPmbCIn+U3+KWZACaD2HbDq8QLcTFxT3kyETH27UPkETDa56pPHgqRWziaN -bwgScZx5AgMBAAECggEAGaBQNfEeRkxavnGykYcSb6ERvB9twfDuABqRMNhwouFI -7JO9VxfXCpkw2L3zXh9BsZ8nLbSCyUo2JbmXFLTmzNK5W5eJt4nK1MDs0yi6xyVO -46lyK44FFuhfxBQi8fstyjy4n/gY66hdC2a67o0lT5XPCMyjgqM1CDv2Mj3oqSDE -QXxQtT3sSVLWGT+ztQhcLBdUpIG6Q3qaXr/JTLDDNn3kIAB9XOw21tDibRPGdw/P -54b4fx7x9K/0bYZg0STh6xWUBTM3geUEW3tRUMkaqtbjXltu12j6rf6FfBOip397 -pdER/YCFn23nIHAn44jp60S4eT2p9QYPxPcAeFnIcQKBgQD/VYO+G3peH7fKI9FO -kY8bLIr9aiF4F8AzPBeUgJjK3MrhkV2wZULT/VD0JMJubAICfSwYgBvQXiDNwD82 -CeMasapzlolnMTa7zYyjIYZSeBXvJpOcSyuPNy8DCPp5mwfEXeoGcpbuUjPBIBsD -rHO45gFS75kf6YBO8/h3AuJNOwKBgQDvUZSqTDDF3sF/Z2C7Kn2cBgoB6tsUqTQV -ZdKRjSoIjSI3XoPzyCQdLrnq1bn7aUXt6IlQySZNjJ4hXr/yduf141l8j4XTNMPe -kisPNNwIPvsDOVO/27+emn44Py4IMIr3kdwoO7YnVHXu4IM6DEhbVC3Pi0glSxol -ydODQh/x2wKBgQCsog6+vClR9jP3MZxUeMm+37DhgZ47aiODAIAY4ZFspzdspzIn -D2/NkJnpV+k1a0U4lZT4w7UKfnnDYtXaHXk1FSZfnEouQPH2rBUIPqRoodSCqxxm -MdSzseXRMYLYMV9g/vY5gcRWQbHIQ4LASxq6ypfekSyAjQk5WG6HWKXU/QKBgQDl -ejqtmWVjNxggDIbKshHEHF5YPFVa2Gyi4AIro0rc7EgVA8JPbmiCux13Ov2dP/LY -EBQrrNXXorC2mt4/pxkBxME4GX9faMcwksRLTop2Nb4H916BKDvz33yMfrirDbET -d3+97JPb3rc/GXV7oe93854B1zKU4BDwjzkMMcnj5QKBgHsko7YzcZKjmaEV9ecr -/9wrBA5OkckoxeJo5qlqxg7p63V7gEZ6/QjfJcuCvDMEYMKjhgb8bbq+JgCyjCHB -0dll5cH6Foe7RTePT07zhAEutLxUU32XiwKtN9dyBQlXoMmJl8o+G+pfcWb59jJx -Rv22/ufIlLl6Z4JZ9RWM3/Ka +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDQtr75a8WIjdBE +N74yZfHUnah2x2Poru8Gia0JhsPjhq7oB4HxmIxujq8XQJrnO0kchiuuCZCzICRu +cpouLzKLyAQYJ+NmlUZQvp7/vR3gNp/AwjGQVKZ/uBecYoPrYaufXAZhtkabmUCu +fxqfd2KHjo6tIEU2JVL3K76Whov0sreUfo6u4+SIBnbxUa0rePsg/El0DQNmgog+ +Bu5HXKuKrm7G8GJ2qIMxH+MgfcLxur+8BX9uyAJkC/eo/pDhk9f6Zsb3h0dCDwZY +4yQOpKaN3YnTwHBAsaQlWQS8xazIidUBJ8mMWaTOM1bL8U4sSvB7QwR9gk2AmSlC +V4i+vSzBAgMBAAECgf9fJIjqKoOU1d2a1QGSKxn46+XGP7gyDSZPh9mmZHvnvGQy +YaAnm6+b6aKBEKWMj/oQ7RjJI8ZSrm3tHoQqVlmaxUZLvLAGk2wtlesYdmpBsdgh +U5hEf+vM5p6pkdjEWqgufRGe38WDXAxtGOpWx2I8nHMHeMgUM4keih895X4ajVee +34OVAU6/4udDr89sak4dA/BIQIkUfyWRPApYDOcN5C5Y6reyrK/j5S96TCSNrMdo +fJtup4plI7rhXskJKR6/SXAjBfvJw7Mk6IGDFjZEoiE8/6e5hPlp/b5tZyig5KVt +gTGHPEVxe68GQP4eVGgaxbbHQmHszjJwhsq+r/8CgYEA8JruM9ygGUkB1zY09dbV +DCv/2Sf3GlAtPjJM6bx92TyjLJC7ccmq5IhEnEml3yK2se94KTUysNlOuG5idg/3 +5oqOD6iUi77OmvyGlh3BVnAvx8QUekENvuUgfD0TGrI1Nme1jDN0RGxYAkFSvz1d +4bDt0goqljpKoMdRbZ53x/sCgYEA3hFw6Nl2RVAkUN4DU0b71hscWiV/YvvwUnXw +156iLKxifSLoPTOy2U68ZkSQVOPRFwyupZf+rHhbIqrsr/lu2bpqaEjj/YQ+QJDF +vX+2EiJnyOyCvAjox1f89UN9E3GxP1lHotdoHp6AdB0SyOgTnGzt1cRY5giw+uQM +Zf4YVXMCgYEArBd0hr2n+U3htie8a5YUXhdecNkIAdcU9SaPIqNCNE4NvANtPq7q +v3jD8jEvJdEzcUOB4598OUfE6V9yp1U2j7vMbmC6ltWL+wjhzp9LuOKXGkAiEWtU +RJSnzpT0hCSwsNAu5y+qWoJP1JUadVSUQKgHAjNpUHgzBpppoIk2zV8CgYEAqGmF +vbGeNnbO891LnE6LExdAa0Vg1IrI+WCkpIGT8FlT4B8nDbM1ggRqcQyygQ69NcPS +d5dL9zTXuPTzx4ldfhYYOLp+3Xb7Vy/0JwDB7gLVvtVPWJdRIk0idEcYhjSE/cwR +vfeq6P2/4U9jPaZzqQAbZzEfUmVpAv0MQhVwEu0CgYBRJkaBDjot0zSwtfvZ38q7 +iiCN5WpmmXpbsf3bg4fHLITdEimhTgA86SGjCO5kP7TYdxbdd2FquusooWuwOUkh +a+86fKsY8SmDMyBeUezRci4va3jEndl7+Qkk6re6M3GheKf7c837la1Eiyhk49sw +B0q/0CB5YyVsQZeEnEU4oA== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-3-cert.pem b/deploy/dockerephemeral/docker/redis-node-3-cert.pem index f676a744e30..bed5a68b5f5 100644 --- a/deploy/dockerephemeral/docker/redis-node-3-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-3-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzNaFw0yNDA3MTcxMzE1MzNa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDYU44MmK1l+2zjBrHY -/ORzWNWNsM9cMuh31KuNBDf1yD8Wg4YxfRgrqI2la88qRVNz3bUr5P8P/ubk1UH2 -agK5Drta7fYkvDPhveeTIOKz9l/ojxb4mXQ5aNmRZThtftokSbnPj8rCLRTvwpxW -wtZGBPAOTHcIRAZs3w8XPIFY/2FnOILHMpGUD6MrG0texcV05GLi10ZEevVb4tPl -1QF4dvyQdGjpOZ9qVn27xl2GAxX7yOlxC5AgLS7HuzLyCP5eyB4i7hRK06XjrVu+ -VUi1nzrOneDJzBFZLhcY1ktEKnmqvZ9Wh6eZGepXo8lV6QCH77OJ1TNSL2ke8qUb -IMJHAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0zhwSsFAAhMB0GA1UdDgQWBBSq82oTIR7xwMx2Cfim -DbOoj8FPajAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEATue+pKPQpi+RUUNsxi7REOmKjVwvEOUePqsovmXzE8aC3P8v -akpVDDggA7JeAgWcFfFng8SimNTq+TqfNRx06E7MYc0Fcekqa1wfTe7eEdaHrekd -vR/HvKONaenxQ0jDD7PLQi+8dZAvValb9avw8howkrQlt0lLt3KVUHepO9ErDJ3P -ymhAl1Dc/8PiwH0wicXmJSnxSpIttv1WHW1wj31G8f6D8W8k6i8fQYPeMI/OyGu6 -tpSe8SXZ/P/trEVFCWISYaq861jufkaTdHGKVMv+rL0E+Ow+zmLmeBRLq+rrmTT7 -gWCV/wl41D9nrzWYtSmBEnwcDHK7eRqb5NiGmA== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjFaFw0zNDA3MTYwNjQ4MjFa +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGgE9XH/XpZa8t3sxP +LXFdXr34kgvvNcQKgooBYJsb+KusvI3k1ILfpewbApd3jOh4JNc/lC6UvSGGuMGg +pBikz99krcIKL2Ls1TrZF6bcd2khr9c23WQiolvqcFwsuxsL14o39/wVbNKx7A69 +/qK79bcuWq8Fpnq/z5ysqZN1m7gsJTaVL5Md4NIFJcj43WLjOhMAR4M+NWq0iyVf +0RuiMltvuCfylywn7pCA8XFfdXEuGPaKzQethG0JE+PjUdTp6bwGpFNpf6PNUBcl +c1U0pjcLiYkgzMHv+tkr4Qpsha2imhi8Jt8ie8rKYtn6KFKqJozQUK0Xl6Vv3BTM +dx8ZAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0zhwSsFAAhMB0GA1UdDgQWBBSbSo/v05wDR2uVWtEi +Usu54mOETDAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG +9w0BAQsFAAOCAQEADRE5aBznKA2AEft4YeXsTpNleV6YzjQzilaAtAd2QdtxN7KB ++Xi+J+H4v1WHasZmw9rIpGyzvBRPl/cZoThMJJvabFJsm+waY9j8y5H29w2kJED9 +mywAmy5N9xvTFoAyxB8ivZf1Lo5GiGAEg9ZtERJsdY5mU4VhENwKJB4mWTYbQw+z +u5z0iyRolEJyesrcQxtPVXaRkCmV/m+DBmfVlI/3AHad3927iq2w2ZW7kKcQ2Sp5 +vC48qdUJOsPbqc1V9XIzfUKVhClMtvRZ6T6b9XCHWr9lfUaEY3MXtwdiYa3zhMJF +VG/ouuFToXd0zTR+fJRcL4Cpe0pWTYnLzr2YgQ== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-3-key.pem b/deploy/dockerephemeral/docker/redis-node-3-key.pem index 0264fce611d..8673303ece5 100644 --- a/deploy/dockerephemeral/docker/redis-node-3-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-3-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDYU44MmK1l+2zj -BrHY/ORzWNWNsM9cMuh31KuNBDf1yD8Wg4YxfRgrqI2la88qRVNz3bUr5P8P/ubk -1UH2agK5Drta7fYkvDPhveeTIOKz9l/ojxb4mXQ5aNmRZThtftokSbnPj8rCLRTv -wpxWwtZGBPAOTHcIRAZs3w8XPIFY/2FnOILHMpGUD6MrG0texcV05GLi10ZEevVb -4tPl1QF4dvyQdGjpOZ9qVn27xl2GAxX7yOlxC5AgLS7HuzLyCP5eyB4i7hRK06Xj -rVu+VUi1nzrOneDJzBFZLhcY1ktEKnmqvZ9Wh6eZGepXo8lV6QCH77OJ1TNSL2ke -8qUbIMJHAgMBAAECggEAMnVG2GRSacu8CbZZjHHsfYU2hq67p1dOhwjpnOJjhSZY -pNE331o85Y4SwAeGEmeKQCfyJtNqtRnxVGXz1VzD1tN7WwnPVKE7fsezeMt+ZZit -pUqfAoyUogF1YicYgt3IVxeFSkdRdXpbfFNJ8SjQHxPuxH8McrafQwzCcdqQlyfI -fnhNYxHt9lL262lywRLkuAwXB69cUdLXaemfvNVcTW9+QUnz+Emx3KnlhRyRlMNn -hgkwsp4NB4nElHKHntYaoVlEqrDRJyz7mCXNviHb/WC7kNznLEArzPJJa4YjAedy -P9kXTlYEkUcmrv/furc7wrAYeJ/+qQ27ToGk+JI9UQKBgQD0LsPKfD/ep8Y7Qcvl -VOSYqUrjQ9azW2IXkK8L5U5IOZbXScsB3gdxhLhSk2MDz2R8n0BUlYEihdzRBA4K -aH+4qW0OipwjQD+qUU5oW9SJ6SaHRp8Mpq7d/mSR7HnNfXCz60YjYPID+71nK+6q -FcBvFvLxopQt7ZDFaONHMWNodwKBgQDiy6wP5b3ncfG3LzapKM73X765scTUXuZa -ow0aHMJ9nRi0aiLnlGzIPwh3QU5L5mpG+gAIUlaguI29x08BtkeorKhOJ0M/taT1 -nbf0FLIQBm2uzpm4ICTlGEi/drUwssw5kFou4AA3pscdY7dam7BRb7eMcaABECz9 -WlJldDC4sQKBgA4vNUJq607k0hgZH14IC2tu0iHXi/5JPa5+whxfyqdZaRDCgZ9v -JWGLwyVQ2HydLIosuhDvyluWCRi/Mo2aOmkgtmwU0zMdBVXAeVyIkRUdzRYonQ6g -FCJjJ7ZuVTkBo21gKmfdttFSa1M18xxAPTh2zdAJkLAGT9WX3TQCg3LLAoGAXQKE -LPzeNdXP+H0/YH5g6qh0cnlKLIJC3Db0P5o91QAhSpQgfnKrbjATi7zXnF8BhNww -OTlzV3R4hLUBXMVhe/ZbC7okZTNcVHJ7J3l5UQMh5kfKWO2t09pyszq+shsRkCX4 -JjMtQ6V9ETt8zYb991fmoY1TvjvhB4IMOpk9BfECgYBZ6TaBR81zK0bX2dJpvWBu -ECwG9vg80NuM8UIjeQ7iCzfSD/6MsYDedU8+u7seV7LZx01DISUFu6q4rWpncJ/1 -W3+LU/apgy7+jeJmUjN0lFCEbf7f0h4x4GGXYbDCiPdzcrL7g5gcwaaf0+0PO4bZ -t7SfNCX7wVFT8ipYG2vTNQ== +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDGgE9XH/XpZa8t +3sxPLXFdXr34kgvvNcQKgooBYJsb+KusvI3k1ILfpewbApd3jOh4JNc/lC6UvSGG +uMGgpBikz99krcIKL2Ls1TrZF6bcd2khr9c23WQiolvqcFwsuxsL14o39/wVbNKx +7A69/qK79bcuWq8Fpnq/z5ysqZN1m7gsJTaVL5Md4NIFJcj43WLjOhMAR4M+NWq0 +iyVf0RuiMltvuCfylywn7pCA8XFfdXEuGPaKzQethG0JE+PjUdTp6bwGpFNpf6PN +UBclc1U0pjcLiYkgzMHv+tkr4Qpsha2imhi8Jt8ie8rKYtn6KFKqJozQUK0Xl6Vv +3BTMdx8ZAgMBAAECggEAXI6OsDrUYPCLhvF4xcCUOCvJm+KJkxA4aYglzm+b06aX +chN3fEhFAACvf4atVs7KxN60yU4QjEVGITn7+yoY3ZyZ9yl4LWScFX91ka2QHgPF +7zG9QbVokCexgTbEHA1glpx5tBA7KEhWVCUUWK4ndkokEIazToiqes7VKMNnYTIY +nEZAxjJYXou5k3lsRApoemJgotT/p1NfEq//ZjBWIpvtfch17GX/PkdPsry6hMY6 +F/LOQM9UUoJ8vEaeyKzkvu1o94gPN1G6Y6twijRj0ySPmOxH7yB9jH5uCDO+MHUF +9ssPVxKwqlkj/VnLLsQX7dYSHd58MP3m2nzJwfUOMwKBgQDyiXxsM+0sVe1xKnJw +iB8//ZOl1vhdAmNGR3Ofak+Eb2CRTZ1IlKQ+epvY+TgkTyydadwODQFE0tQubRSJ +s2s4j/2vYSRa+V8jaLAiwGNYxsnjNTjyvUSv1m/mn369frkPLdkvvYv9/DwS4zzR +7l5wolejXxp+UjmxP1khv9vc/wKBgQDRhRAvBq+3oLkMwrMLyrskKNvaWsBWD8OY +iCiZjmqc6qGRjgstAFUbGvDLQivZ4hB/tN3E0c7IxjLxLveXFGvql98XKi42rcBb +WnXXAGV0ulwPOxVDp5Th5pMy7nwkNG5sp3QGkduo5A1xYu4mXhGhGz95kSy3PAGw +4eWlOKxL5wKBgAybxyMc4/SNFwXuDfr5qJ48AYP6k/jJ2f1aU5FzBmU9IQkMvuN6 +DrvMxfNWqWuBzjD0wuLcHDfGug8bzpiGAknzel22sBwmoKKHm7iCxedkljRAnRBJ +dJuriy+zFPSm9NnsKUFJGlD+3uSgeZX0TWaPmfy9QfRVM/iZ8XlGrxhjAoGAY2GI +caXsR1+XJvRbVSaOafJvhj0xqiDEGF/NUjj5XQD2LkKADpJvy/GVcfQrNKhERy8V +Wjxip11L4Jb0ndbz8UykZyp8zTbRXQOljZwEg7+51wehaHve5OAnxirU+59bGXK8 +WDlrRcsWjUftyokoN5DjJNi1qxxteOdNtHcTUtUCgYBEU3Q5YwsKTBuemoncQQJ+ +Zl4r7QpBpD21d2dyqvfr+rIDYzl9eFAvWiIOymtOheN6wvYO3UX0WtAgsW53lnua +iHqvGocvPhEGlpWr1spd7EdPdmY7zCgPH/8w/mKlllMtqFPuUavHxYa8h1ThTO7K +CXpmRTh8KFcS7YsWeKQYvQ== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-4-cert.pem b/deploy/dockerephemeral/docker/redis-node-4-cert.pem index e8ebd88bce0..47c57e52a56 100644 --- a/deploy/dockerephemeral/docker/redis-node-4-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-4-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzNaFw0yNDA3MTcxMzE1MzNa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvitE2tBIZ5zco7Acg -zZXive6YAUJTrsyfLAFWWJo7rOZJ3vlVvKlGgRLcDRTqS7Rjs5ZiKKjO+vbCo9GH -akHz1hzDQpXUoSeyRlu/AausQbBB1ZhDOzlcg2bmhhG3CYswrSSXM6GdV7C1J4wS -0pmXoZJO9QKvgdskkgHn4I8RGGIshtKRA040yoRQPNJeC7QUOZj94YC+pyR0Yb0e -pawKDllkhpCIosg9TWqEeOFTdm4ibN+g4M5xihqsHTkBlQjvb4U45ybZyJdT3bw2 -inN2f/FZsyOp8as4JoMbXnp/Bwd01Ze/cQ5pVVz4pnvW9jaUzJ5yRi4du3v70Ft0 -mP4TAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy00hwSsFAAiMB0GA1UdDgQWBBR2P9NFa+sluP6Ic0su -lqWSIP2i4zAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEAjInARY3/TdhAT0RJDdDLyxfx/NF3VD+L0GlA5YGAqj9lLyr9 -rE96N7y6/imhc8r+zecHKcJVNZ+NkA9YHhK0NqkC8UXcV/te6KWxe8KbvrFfuhep -BlWQ0RhQYUDDoFuyZ9FoH+gdynz3OU1J1LyGZG280O5/QQL+ON5t9rD1wYCcTRM6 -zlUyWtbUWxrGVvGClRn6lrTNphOTxBtKM0cqXD6jnnGUqLhCY1y801HHzfJ07jIY -b5iLW+kRiwdnDIvuiJ8gRmqHgr+rHwpv15HfumedQBUuZsVTPpcFiAqs1wrT8BFn -EWPtolHtCOwd57X4UP/LpPnUAHwQJPmloDaL8g== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjFaFw0zNDA3MTYwNjQ4MjFa +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9uasXMmYQzllPUAy1 +/2+G7fnq75TSxTOKlJB/al70Pi7sCP+Pwo+4KEaZsLlxBhDa6vTVcKW6T7XRNyqf +bqcAgnKwiVFXs6rVOGCWArYOrP2OV9lmixCll86TYWaCPNjkoar7vlLWRaMxxe6h +LwxJ20xw/L/I9BLJmQ8+0VyuCL25meH/wRILuxsLKiJB5IcwN3Ku6WUAXLcw1+bk +tXXyKhGcr/SwHlPJaH+z94Cqo8yAXecWayCQrI2A01SWkPSb69j1aofO//EoccTf +jsKf+gCsNVYJvdsSj4SeRAk3SgYu94rSPkuEQQrIwAoO9Y8ErqSFCw0ir/0kggbv +NQkbAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy00hwSsFAAiMB0GA1UdDgQWBBRlLmkyRJrl+CRRT+US +42/LoE+lszAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG +9w0BAQsFAAOCAQEAe3dZVFLY27yyNBiaiyzXf6hBG5syraU5EtgzOiU9/+XEExo4 +DYL99BjznimG3B1fNHt6rafum90wtSoajkel0R6yfqh2Lu6mGa5U/5KYeu1IPU2i +PCi24ztGDwBUOr1ZmFoIz0ihp+hMjAheGY/gVqjIXbwLmj/Wuy9cH8U16LK0wkCm +QUHvwM2d3QQtKgrCqrZKospOroEETzbOkpOTPd7sFPZuhj3uecCsf54G7FGH+w/G +ISuKbBsaIkCs3lSl7CSRMyrU1BV5uEaw26pTN9zk/4vZ7lFupPH6rkJdrTjz2tQU +dBMaCG3GLDeEeIJRwIkLP8FZuF+y/j2btQAxlQ== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-4-key.pem b/deploy/dockerephemeral/docker/redis-node-4-key.pem index 5da453ec076..924d849b73c 100644 --- a/deploy/dockerephemeral/docker/redis-node-4-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-4-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvitE2tBIZ5zco -7AcgzZXive6YAUJTrsyfLAFWWJo7rOZJ3vlVvKlGgRLcDRTqS7Rjs5ZiKKjO+vbC -o9GHakHz1hzDQpXUoSeyRlu/AausQbBB1ZhDOzlcg2bmhhG3CYswrSSXM6GdV7C1 -J4wS0pmXoZJO9QKvgdskkgHn4I8RGGIshtKRA040yoRQPNJeC7QUOZj94YC+pyR0 -Yb0epawKDllkhpCIosg9TWqEeOFTdm4ibN+g4M5xihqsHTkBlQjvb4U45ybZyJdT -3bw2inN2f/FZsyOp8as4JoMbXnp/Bwd01Ze/cQ5pVVz4pnvW9jaUzJ5yRi4du3v7 -0Ft0mP4TAgMBAAECggEACYE2L8STQFTNH0mcXzHSfkrzasaSrU5HJQ0wa1jzzOxh -MbnBfVtwPPGLMGAC9Gax9z4Hk/wIm+Bp0QMmurLNrGK4/veRfkhVimkV2aNBBNwv -q3jhvC4uPmyc+zliJyt8nl+Znhg9FXRkjIJ+Kpy9lUC5182bXh5lW7cOJFx70pyM -5+UZ3+ogNnGqEtTSRSEKR0TzCLC6hORmlWCbnyYCaVG/H3tKAjg8Mwb7vFpreXSQ -QXqeRT8i/wVcWGvnuYyhkIfuLWOFXMswazLPyf1H8pF6xAxqeaX5QKhv19mE6xf3 -A7ZdBgsCigWcKuNDb9Fx2s/5xZM5SygzbStOvH27xQKBgQDbcZr3mZx2NaYLbDWX -Rk9b/plm7sYBCbenUXHZVE4px2DJ+r4V5XjVvzHM8cLRcNmSPhZJjamhVNsLNUOS -L39ouxuSqarLd7bxB5i2vCUEqnqVs2JhgVAjucIuOhtEGhk1yR9gGIgadXYhO93Q -bdHbop2qgxut7XWPWhuqe/u5zwKBgQDMyP0dOpElgmpzJ9mzgNNvwtxzA4uqllEH -Mst1I+8mQRRVEaqmB+TpfeoR/OUKKKk3XcziDTLCCkwyplahDW7AHbYeizevg++6 -G/09Z0XyZZ51L7LxjVi1ORWyCDrTFMASjPzUGedcIANKkCHZ5q3971iSyjFmXrKa -G/i6pyF8fQKBgQDIuDQf7/CuK0oyvoqSUOx73/ger56LCoFi2NtDB5rrGgRNGz3N -N3T8RgLeS/B/tDI+Uu3930beW4hzyweAalOmzyZcUzb3HwxFkUY9NwDBMNIppcgC -Gc7crqePsvSHqTuP9+Pr+ORdFz2zDlhIsnq25BpFAeFKiJ30Pl555SgN/wKBgGV/ -UISGHJ58rwn4PFxNg34nFGAk57pa2jo5IMIkV0mcg9lN8khsLTbU44ia0WJhmM0K -Ppvjcr7dn7qS2ujj4Xpyv2sQET96ovyZFsCySObFGu51/7jdF5RqgKhGj/FCnZgU -LNNrK1Jrw3XXTg/T13S+hiXq9OUKFndvWa4ZW+15AoGABGPWL6H0hQWyzYq1CZl6 -kdCW0b1cZeuJyl8C3MCFq5f20myvTehua0DdROT+MaLJQ+20etJ9TRHpKiUKhcgq -gJ9y/8tcMG0hMPjQkFzFsHuDiIE3UOtJd9kUwLbl97WUZX1EZ5jmIVdCpcsx6WcU -PFoetX5NdFoY2jhfWym5WcY= +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC9uasXMmYQzllP +UAy1/2+G7fnq75TSxTOKlJB/al70Pi7sCP+Pwo+4KEaZsLlxBhDa6vTVcKW6T7XR +NyqfbqcAgnKwiVFXs6rVOGCWArYOrP2OV9lmixCll86TYWaCPNjkoar7vlLWRaMx +xe6hLwxJ20xw/L/I9BLJmQ8+0VyuCL25meH/wRILuxsLKiJB5IcwN3Ku6WUAXLcw +1+bktXXyKhGcr/SwHlPJaH+z94Cqo8yAXecWayCQrI2A01SWkPSb69j1aofO//Eo +ccTfjsKf+gCsNVYJvdsSj4SeRAk3SgYu94rSPkuEQQrIwAoO9Y8ErqSFCw0ir/0k +ggbvNQkbAgMBAAECggEAHndePg9dzH0WYmIcaG1oX2Z/p3Zpk58PM8W/nnZaYSZL +KqQXReKcaZouHCgA32F1+3GXd17rfgumyr3tHkUKlE5eVHL4mPjFChBPkkdFLP4i +iWUaCBl0xuKlzYzqhSd4PN6pMlvRuY7dMfTy6PdBJesNT2eG9KIdEjp99DxygY+m +0yTnmsf5buDxlKHxYIUrEpLNo4giTHZdIVnf5zF+a7FGZJOOVK3VOVklPiWt60QQ +IEVVfHCpzUls14R1GAbJz+BRjWxE2xYYcrZZu3APerpZlx39fVx5nQ1Hyg2xQrct +gj8l+PysM8gNmO8Xmmzo0/yXY+gRQyq/IXVNCJKMwQKBgQDsc6krsC9q23GcCsD5 +LzSuX0usnoJnDA5/u8f/jFbc0OPepsMvAYCRQtM+/7CJh8JsuvAzXKGIp06rzv2o +cIHRfHY+SbQlNJUZWT9j+/bQuEnxgzpURasZFluHNbNJbmkFnzIOjAMCAYEDMBF6 +ap/xD3usguGdmNympznnQjbUowKBgQDNaRKafYFmBkrY3qimPXAG8XipNL/6b9y5 +dXm8s4HIMeuPi+qPk6jnOm+IuXXc8G4WNAZTQcM22spSJvQ10YHPks6Zp8atJtlK +mvmkFK/oTTnUjIewod9oVVurG5Bga1yRzEbkF3Zq38OYphe7GUc8DQDfq8V6jVQC +//JwGTPJKQKBgFhnmg2Kjv/90glMf//qpWC4onuEvC649EbPt6QVHXjr5PafFQTj +I+WrvX2lbaTODGRItHwPmxmTrDdSacZrYi4nwbHiLqdmdISIuMmyMAKzlHnm3Y0a +izETCd+QtVq0HDIM5lNIB+vdEhZWB4LkkK45Yr0KJj6dI4pvpZeQSx3PAoGAMn1q +SjkhTl+rlCUe1UXyvHIsU4MY0UkfuyJqGv0QoJHMsgsVS9diw/t0IOpdU0Jx/Nkq +2NooTtp2srzKeFQYEVqnl9NKnZMYBCOVy0QefP5Ggb1NORiA3pdkoelzko+xQFEy +96vguqJn5KSm3qF3Bga4OUJylw4YIWiiQfWf6gkCgYB81MbSxLsJ47068bS7oLMj +OrUmYoxzxTpY7SntxpDxYuH4WqfphHYum5xhWsJOsLPxJrEBAOqN70nQqzFtCPP6 +7dV5luVafyFtQZ9zD9bbk7GNxYCu7+HLpXYDqhcqmHP/juZEIe3AZBi9POYFklxt +LVAjPTs2DRn0NgkyaUkYjw== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-5-cert.pem b/deploy/dockerephemeral/docker/redis-node-5-cert.pem index d771a21b461..465d5ae2f1a 100644 --- a/deploy/dockerephemeral/docker/redis-node-5-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-5-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzNaFw0yNDA3MTcxMzE1MzNa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCiYYwsg3+qF8hXrp0A -ni/h4oySwTN7JBsElWzipzoY5k5VLZWdYITaYc+mypys1OSHiVsDff12FGWAsKMD -ItoFC2jnMGx9FQcXMokNRmEdmvcOMEx6Y314U/63HzAKYC7XCrV6TdK12zmVxiCc -pZ7Iz8Ch7bzeFTQQY4cdvA9sJJeJ5oJ5Tm/JJGgSzNPBOHbdeDuprQayihA3Hfac -19oM7tZGEqvjk/otzxmi0X7qMKFO43cxD4URqHWa6So2T2g//HvwMoq0AUajmXUI -9DYgownjfZNJ0ISEouuPLHe4C3jdG8ku24r25cugpkY57zN7BlDU0trOFk4TeeyM -HXD1AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy01hwSsFAAjMB0GA1UdDgQWBBTM1a20hY1IgokeC3tT -zg6shjIA7TAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEABtQ69VGkEHPkIotuB6kqtz7LtDAf4D7N1lIE3pib02n+5wHi -ITzzv0uuNpdzAPfvaR8OU3/8uNzA6GvrspNLaDbhRnXdTI4eDpFro+vRGvsqaLPa -FWpooa+zNgoIqPzQ/3exN6nA0APYqvxRUcAdsaP3C4clecBvHWOpZya3Q1sdvCH9 -b0Fidfb24D0B6arHrx3hEwufmamkMxOnvUFh7mqyEwuyb2lF2x0VKT5/u8+rfOSj -+xmv0A5gsc/Q1jIkzdfGco7i+BWbINS8dj77ajDykpxvbdP1mGzXCqJdBkxaFhO+ -iLUAqbXLDw47wCi3Pe7wDaWFqQfgs97j585lUA== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjJaFw0zNDA3MTYwNjQ4MjJa +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgkGG3QkBQRGOack0q +58F8YD4e03XG17KJwEFH7vCAuAnkeu2RpdN68DSor7yDJr8fpoIekmeYalJSLgR/ +tDknCpUmowWnO51klwK57Q7ovdmzscS/ICOs2zjqV2j9CqNXwCgD9ttjCCU0Sgcd +JBomQUK5dMLAxx8GzlM6AYdI5MKb9S5k9vRUtceZG6bOIBtmdLsFBd+B4iIFElya +J4aEVj3G5oWtSsU+afVzoFXHTTLUG4c22iOBPyF2zFKTHbkxefjY8M/9iShZxELu +SjSRckAA9pAlnwM0cKiP2afxBfw7meqAlZL+d7brCetO6rfTbORa0vOYvoGVo1YE +NWZjAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy01hwSsFAAjMB0GA1UdDgQWBBRhj4NDNIr0kuEwRK9M +uLRma8jGaTAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG +9w0BAQsFAAOCAQEAkV0sRJkSEixB3Dc2xrtm38vItBxfY8gXM+faqlscQsYveqhB +vdQv9Haq6sIp047ZqKppRDxr8CNwAJO/4af0T2bDE4+CkqfLmEtAlPMc0g6+9YbK +W1+U7FGnM43k1deLiDJtMKNAEemdVHox7xCdEL/kkOv51e+3wPs2dU0uIWsa/CWN +WLRvxnahih7dTghDO8J87gHS+hB3LSEJUFPZ3BZsDdJSUDfxFJK5cwV7hvT57/6h +o1zbd1+XN9d0cWFWP1ab2HWx4fDMNvMXaZRI8eYIT5xDWZhVBzxprnASjgUeUvHq +9MP1Rl2iGZEcjFpgtIXh8raa9Cl9R37wlwtbyw== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-5-key.pem b/deploy/dockerephemeral/docker/redis-node-5-key.pem index 6167a8e8275..b6b093ab2c4 100644 --- a/deploy/dockerephemeral/docker/redis-node-5-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-5-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCiYYwsg3+qF8hX -rp0Ani/h4oySwTN7JBsElWzipzoY5k5VLZWdYITaYc+mypys1OSHiVsDff12FGWA -sKMDItoFC2jnMGx9FQcXMokNRmEdmvcOMEx6Y314U/63HzAKYC7XCrV6TdK12zmV -xiCcpZ7Iz8Ch7bzeFTQQY4cdvA9sJJeJ5oJ5Tm/JJGgSzNPBOHbdeDuprQayihA3 -Hfac19oM7tZGEqvjk/otzxmi0X7qMKFO43cxD4URqHWa6So2T2g//HvwMoq0AUaj -mXUI9DYgownjfZNJ0ISEouuPLHe4C3jdG8ku24r25cugpkY57zN7BlDU0trOFk4T -eeyMHXD1AgMBAAECggEAHJX6IIr8wkOiDgeMFaQDb2tbzmkLKFI8nGO20bbZRDFl -GFsoQ9aORMijzuvLxaRL3+1nE5gOMweXr95IsEBmK62sx8hPTPzS7PtFQ8xAQ/84 -H2wSxpf1qmV1CaVIpob0sAAvXwrMvZ2Mh2ij7Je+eoESWx9YWKtYaUswKeSlvWZy -2OQVQ72eX3MTMTjwXBTqo7cVC+j0/yMxAha6lRRd4BlWDuUi9VZIRS2LC934sPbx -dDDm3qkP9zXF4xg/8MrWe5BTA9pxVcAT+RMIhnk5g+mjHxKL3B7Klbhzn+SLWc9A -1TWRntwbUC+8RYWqCNesAdoLIlEJrNUncOhY+CrPgQKBgQDVFO9yUbp0uMeAJFD2 -qQEwnybIWyB0SWUEq91kFQ3cSFoVSiIvaLYZM6n9lSRwxdXS/KtLnhdyUbkOR0rD -VOJmPf136MwlTHMud3YPxku7YWEDH40BIbFvBNUYj9y4GpPGANen7NgF4Vnn8T6G -7SInfx79y+JhQ+Oxd2DFikQXgQKBgQDDFljiqPQeGUz6B/y4CHBfc+e8eagdt3km -NXsgFuGmu3ksy5uBcgCebXthh3coKPeP2cIwob4sfnjq8vuOWSsIqquLZmidyyb9 -ARBQ/CtSnBXAyakoOnI4usPQmrbFq58xIh6I0MRk1N1L1D2MTz4i6QZzRekxAFdc -nBpaclAzdQKBgFfvadm9zLr6vqotUpRYrrsIExNAOCaFW4EQBC+XWL79xN9gVrdF -+VBxN8gE0qMPoeyOhYqRVY/CFiLEXSA7WatkDcR8eDM0V5xnhHuCFCLiTwzg6mn7 -I6RzVBXs2OPJZA6krlsIrSXQGDBWKL26AwxVs859Y5FMWR0V7QPYyb0BAoGAJZ4A -g6wqbkdYpXm2zFGsQWubCqe2uAwxyyFS3Ywr9Ld/lRipops17VaVDOhPHKpRmiZW -IIR/pBq6/CrgQMGG38PxEg8sKwkKOozi9Yq6W9KHC0aXXI9wiOnSaj368kC2kIXQ -t3bx97Nn/IAvYgfBpn+iY8XeQjmbntrm5fvW5SUCgYBmqOIT2yiFGjJybTvwcDr8 -Lz9QpCnP2mHYiTmzXnPZGCaLIeSmXxCr4YDTgKajKcx0NVZmX/iGm9VDrfZn593w -EVy/oxg+vpME2RnnBLDstO8dVMOuSs8/ao0PZWylkuC+5bMvYO8iaGk3EZSX2fmY -AH0a1dtdGMveeGsFqnQyjg== +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCgkGG3QkBQRGOa +ck0q58F8YD4e03XG17KJwEFH7vCAuAnkeu2RpdN68DSor7yDJr8fpoIekmeYalJS +LgR/tDknCpUmowWnO51klwK57Q7ovdmzscS/ICOs2zjqV2j9CqNXwCgD9ttjCCU0 +SgcdJBomQUK5dMLAxx8GzlM6AYdI5MKb9S5k9vRUtceZG6bOIBtmdLsFBd+B4iIF +ElyaJ4aEVj3G5oWtSsU+afVzoFXHTTLUG4c22iOBPyF2zFKTHbkxefjY8M/9iShZ +xELuSjSRckAA9pAlnwM0cKiP2afxBfw7meqAlZL+d7brCetO6rfTbORa0vOYvoGV +o1YENWZjAgMBAAECggEAMIK+yyf8l2O6NikPkIV5y1KmohigbmWv3veToaCa0EEK +WBod2dHgnbWiK08BJRzZRL5BdOwl2YJSAds+Z7jzRYzoeEZryFV2HbSUUclCJmZp +tmVgvKAAt1J6lS64nS8QH8yCKoR0TyzgVLaDBLZqIiG4f6C70JO4l41RzuY0Ufy1 +E0zGSbm0hjT9lsrmbR+fCy3pu5UsTfZ3BVfwuQVFoZSBe4kxILN2vZLFVfQmFSuX +I8GTav5ENSGqhhAifUOizjIJVOmfQG3jtVftok3NDD2EVSrNJCjwrsv1QbEbbm34 +2G+PL6FjhqQ4WhOf+051HNVb/wkEtrS09orlg3zEyQKBgQDQW82bMBSVPYCyAGT1 +wcw9bmJDqshFmCECw7BVGsmC1C6vxAuwN+EXJxConb2Kibf0FkS4Qke5AKG8InF/ +3h4bds4T2bZCVogUACaUwXOrzXJBR1LAyAdchLkRsXeM8U28SdtjTF3R5sUJyX6y +zZ7pg8oKzbDIikUEWNg8A926KQKBgQDFRvAR4M0CcwQI1P0WRMtUHCudPAOQhmD9 +LVfxn0n3USaT6aXFb0A/+y7UZI6ofw2T0pZZS15AYzqahC1OVCIQgHX5Um1eVpCY +gB+DaoKRqQQyLHDfU7AQI3FH6n/aefbfx6Rt8DXgpttyp+vdLqRbgSrYaJBOjNST ++5aDJCVFqwKBgC5QuduNTIYALeNjgw2+DpB5QQ6Zn/sYXf4nUcMZOUIDuH0Jry90 +vGxRGrrglYl+I432hUAQO7E8GrefUGuEDF0+g4CWHJWSdp07i1f1yKif+o3YNOT1 +ke1W82yjble+K/F22XWxPAm0qogKakeEvZZa3UaZgnqRgdX9idONaHRBAoGAHgLm +rrGWPpMkv/s27VZV4FvQvsDMggYPZzSotldXN0qfJc1brKd6DMG3pBQQJ838UMqu +mLMAiacO2UbWZZ4i+IOybtV9Uea1ZJ3JLYLcjjA6NS/RlAf1Nt9NcnVYMfJv/icu ++pKaf6yiodSt6x4XXtxNmlJ98ZU3GbQid5zeFrUCgYAFcjIwBwY3JBXrk7Ilxpb2 +Gm7VLQfQPMqsIg+3quMaLNRkHziIlA+srs85NvCdD7hpxmX3rTRpK/KpmXG5a0VS +LcnRWtHrY8fXvsAKEc46ZTy9+e8JyTtrsNZbe3FHl/za22YB9/mX/ScpNytl1X74 +/nHN3Slfzn9kpWDYDHmx8Q== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-6-cert.pem b/deploy/dockerephemeral/docker/redis-node-6-cert.pem index 9e323b2b5cd..94c1b673e6b 100644 --- a/deploy/dockerephemeral/docker/redis-node-6-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-6-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzNaFw0yNDA3MTcxMzE1MzNa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6YjcRhT/CWKdXQXIZ -+qee0UPul0gw+QHPcc8MHsO06bDN8y40//sa2/5fhp80SkFCZSlT99tCBBO+M8Nx -ALADOGvl01aOL9LY9O2nXkya6/6DkIsV+GssBtC1OIBOiSrHfHy+C7ICbV1Ax5Nk -HWEXpkKc8kAZo3ETDqXzCoYq+01qgb12RBBwQxz0yxHDOZcfXFaffIM3+Wv7XnHp -RT22tWJuw7h5TTxx9u1dhZKBWERa2kVUhA6/Ihk/zCpASWbRwOf355jTNAuO+pQT -mXFDwr+/JcenBiwCQzxaTFkUDPwy0UvhrKvK6WsXuySNO6QNFZXPyuus2IOlnXd0 -8+vBAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy02hwSsFAAkMB0GA1UdDgQWBBR6J2P3AJJ/LRcvVhcE -nrAvvFDyqTAfBgNVHSMEGDAWgBSHldnboqMeE+DY2VnBYEYkuOMuODANBgkqhkiG -9w0BAQsFAAOCAQEAtt28vPDd4DnT9/vIfIAR0xdLVrw6EGBqrsDtGHbF+0SJ0GJA -+2DxQazJGGgBYXqfjZz1+yBImHP3Rio8+gdU84C+K3CKsa6k4N76f2Ym85FrOOjY -nMNVhdPSdlFptq67euCbSJc9fzXE6Aq73Zm9dRtsLQVmYOAMOkw6EPXNLrwVRaCP -FUswJctD0RcRareRsgiDVgRXfPBzfuxMYMYNwWNcQ6R9dL1r0db4O3Py4L2GkB3o -ukPcoemA2FA1ExA+shzzXBIBr2aK79VkaWPzoUuY/TeRmqdxKeDiaFT9F2eUCdez -FbId94n/8E69dSSCtmbEwwQMsxgMxALZEFustA== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjJaFw0zNDA3MTYwNjQ4MjJa +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQqFm0DBSO4J1i7QJf +WbCtHyiPe5znhh581B2macsLkAnZVp+tV98scNbyHx4p0lO3+QxogshEg79PnyrX +R9LuSQOGwAyUsy4GLrjKitpGy1ZUkRj2Lg9yK/a+HNdr/UPE9UFAgtyPqljGs/Tg +99NVEZv6W91sB7SH4Mp6gIQcudgCNrZg87RzuDc48F2IFjjDtnVxRHeEQc5iRWYS +6o0mSr8r87RuYNiDCBff4KahHYNAqvF6qKxIQC9Z/VxBSDnyMz76+ber9IhaJRlX +wB45ugdmeUMEnJKmwbIOl275gRNMIj3OVlobZyMLeY2aWEq2Ql51PO2HySjXcy6k +9ywJAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy02hwSsFAAkMB0GA1UdDgQWBBQ/9AV4TxrTpejQNv4b +HO1ULpu8vTAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG +9w0BAQsFAAOCAQEAV6eD35KsmU0g8jmtnDB6YgnolGyGbJkWHNrr9q0XRU6qGmb6 +vnlb2yWDcnfDagaFQCTLWOR4zwoSKXt11uhMAWYYGFEIQETZz5CTlPwjy3qf9s20 +n/BEkxiFxRV75aF70rt5SMdA6hPIW+XdRPTlNSpBv8Bu/mMiwZziwpTiTpkkV5pL +J5+lD4Z8teao9aPB3G9z+g6Skt8ieTkTxdH7CJeDiU/TSFJsYDVQqGmD5KVsMClZ +Stz19647dM9XIo50PqyHMZSXe9R1KPnm/mCd8U9XrfymvX75PYTTs8DUIf6U1yVd +R5wIrvAqkh6X84hPv3sOn84fT3BCuz9/JrLVfg== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-6-key.pem b/deploy/dockerephemeral/docker/redis-node-6-key.pem index a214d2d810f..699c15d93ca 100644 --- a/deploy/dockerephemeral/docker/redis-node-6-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-6-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC6YjcRhT/CWKdX -QXIZ+qee0UPul0gw+QHPcc8MHsO06bDN8y40//sa2/5fhp80SkFCZSlT99tCBBO+ -M8NxALADOGvl01aOL9LY9O2nXkya6/6DkIsV+GssBtC1OIBOiSrHfHy+C7ICbV1A -x5NkHWEXpkKc8kAZo3ETDqXzCoYq+01qgb12RBBwQxz0yxHDOZcfXFaffIM3+Wv7 -XnHpRT22tWJuw7h5TTxx9u1dhZKBWERa2kVUhA6/Ihk/zCpASWbRwOf355jTNAuO -+pQTmXFDwr+/JcenBiwCQzxaTFkUDPwy0UvhrKvK6WsXuySNO6QNFZXPyuus2IOl -nXd08+vBAgMBAAECggEAEori36NaDIO1YkDokR0Wv/4hvALg875SJ8kyyAnnfoAh -Ttv6pNsyqCFq1SYXgKRCidB2pBvsfEzbifisYPmoiSl70omL+ulXGK6FVjlTdbY0 -w/IFZFIql162NNFCMo4C64W/A0k2lHc858zzJOqnVir8RZD0P5i7DyJN8DgD0RKz -uDpulugDgHgWuyfhkve3rmN8RAJkFiSlyJJCKPA1YoSdKrUmqwjZ9WJfV8UrUPPC -PoEksVEiLB0NrE2X0CtcSRuSZV0JDDciCLiCHkwDSWRgLzWnE7ECa8BNgdz+MyUH -WQjAoG5ZNP/9pPtVb2yyHqrC7ekc0wZyzahgiKb+gQKBgQDp8cMdHxf4kBwcJIXq -69OB1/0BwnXs0nXVaWQycy2pEFRC6CGfXkUEG9nT/nnHiQyD2ENcjgR9ATFe4Tx0 -CRfB6LQBmpsBK3fidYyBIUqDypMmrzFfc0Kj00o2v8TlO6bj2cFxYwWiu7VQ4E6i -ACmWfi5Aww+yCVPxzKgbAdkULQKBgQDL9JOjvkFbtFmbOn5yvoiIAOMnVlM6BvAd -vzUxg9Hp+xxYaBLPSEhqqc+kcCUdVzGOKeCZheB+D6OzZ9tAMqv46qBk6gsBdhk0 -uieaD5gNnVJm+l2ziTLNGdIF1StqaqXC/GU58BYqiajZABoePdS9pGK3BprDr1NW -8pcy8laOZQKBgGa3fuq/Zz/8zkrRAnemOcSt9+mY3zwvIAum7ZZ1Gdw8TjLeRzz5 -ICZwsBCzj/a7RuJwxwrRVEkqh+nXzTpJb8P1D2wQ3PQDiOzGnf1oh5YcEMYQcAYv -zleuAszNIH9h1KIATz4gsy3DaxXqlrvshFYOavKGctLB47isGjdZdV21AoGANh2d -8utvUhLHV82scWumtFdv7icUjCf9HBd42Lt+PhQX0ElE/GTUeiC2bI4o+uEA0BTC -eFmyWCB0Mg0TerQ3NyOiDUSgSPH5/CiMi28pzCr7C0HRDOsRZKQ+Orf1/hVwCA2K -GlZeu0itWW6Sf4WuZecxHhkNhXCGr2JMxgLQ/pUCgYAYw3Zvvs1C88geKyugdjtA -RIHPrkU5iPk7N+lr3Fb7HQft063f+ejuUIR6RJUUQsAf1OCsYK2AT9xd6JqAfpZA -AhDHPd8lMy1mepqG7MscICH31pFdLjfyBP9z/aktVgzDgQ5c/VbTxrW/+Zm6vQUC -JAeAbzN4IggEBDj6higxWg== +MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQDQqFm0DBSO4J1i +7QJfWbCtHyiPe5znhh581B2macsLkAnZVp+tV98scNbyHx4p0lO3+QxogshEg79P +nyrXR9LuSQOGwAyUsy4GLrjKitpGy1ZUkRj2Lg9yK/a+HNdr/UPE9UFAgtyPqljG +s/Tg99NVEZv6W91sB7SH4Mp6gIQcudgCNrZg87RzuDc48F2IFjjDtnVxRHeEQc5i +RWYS6o0mSr8r87RuYNiDCBff4KahHYNAqvF6qKxIQC9Z/VxBSDnyMz76+ber9Iha +JRlXwB45ugdmeUMEnJKmwbIOl275gRNMIj3OVlobZyMLeY2aWEq2Ql51PO2HySjX +cy6k9ywJAgMBAAECgf8ZaYwVjz6EA6BMB39LnZem67/7uZt94DxWUxFt8xkZ2Ix/ +GYSP2AhxoSf6Qr2ptbP8hk9O2OfmlNv38fPO6OdQwqX9lZ0XNgSAYNTp1hGtMija +o9FLBayAezWtTTFdEQSCOFx6GlFnCU79dpHliyS7CeDZ3Prs7Uxz9uONh+KPLyPO +83EFF3NbVMNIDsFzuadpHC7uvdUP+uMpUrz1USAAxZ6zwVvohUZYpjId4BCIk5ns +jWjHSzg7w5sNxXvUOO+eWZPQKnx/T+tAqh1BTVEwVPWa7/eRjQ2rQVoPFuWDMrcK +HxJlGsX6CdiS2D069iEB+x0cRC37uj7m3OAFkkECgYEA+0suBBs2Y5f1ZJMQIKsv +IMp1JZNaD5Aoidu6Z110jGi//LIaBrwmJL7bKbRGbCPP/wpPofvcdtEjrxK0lXnq +plEJ2cJou2ESQtXJzwXQy2oeDstvaIgavgaQr4VMUOhXqa4gVVk7aK035P8sRgYF +9RiSEBkUQnj1fS0gHmyhX6kCgYEA1JDA2mSNLFV9OPHVYEP8zYxY4UHxB1PFOfr9 +mP0ljq6Nj/UoDuHX18FH20W0TgGRnW0asx60w8G6abpdSem97hSyfWTmf8pWyh3q +mwkEuTpMdXTijVCqeDVDUE30Y+a+kG005a8vQt7eTBVLASwKLfz4U3mWmSxTsjpL +gvIapWECgYAflXJiJ71tRRMdofI7+OgCeg/BOkTugdLmiMxj43Ybk6rVqtjkkc9F +fQt0sWjMfK/OwVAC7vHlqSGQBozV4K3iW3seeHXLX0b5SX+E2plEh8DhYSZOgBTE +X3Td6qYN4TXraKw9repunJ7S1FOPNYCYLo9lIJHQTP2lzv8jc8nQiQKBgE2l4wzk +Fj3PrMKUdKGJtFtRnVYLxIQssasQaHruXj3UvZmMsGlfTn1d+WW7/LVSFWMwa8Rq +vxWTOwlMLq/FVsAVh24O4bRksXd7niusC7Gt/igZ3nhIszzeGAzJrTChJZOUkPIm +IFmJGCMq1A9FiyJpejzj+YNSkfBVIyheUCWBAoGALz8kUbVgdz+tO+Zr2g2b6xI6 +SmvB1wfZJjsua92vZnF0vxxod/Gs+YID7K7HMtobSRptw5RwoE9pdCRyB9WzyQHV +LKa6WZLmK+5Vf3cOGt6B3F04Z7Et4ElNwUlOVgAH9V5rHL3zhiZAahqnhINQLdQy +qWfo1SNim8yk7PD5psM= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/federation-v0/integration-ca.pem b/deploy/dockerephemeral/federation-v0/integration-ca.pem index 304fc892245..a38ae3c9efa 100644 --- a/deploy/dockerephemeral/federation-v0/integration-ca.pem +++ b/deploy/dockerephemeral/federation-v0/integration-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIUQ35aUV70pJjvDTbfgFUj5YmchHQwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwNjE3MTMxNTMxWhcN -MzQwNjE1MTMxNTMxWjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJQlUOLNmd7Ll7iskcSnsv9xcx/+TnMw -qtqkK17w54/Kto+NJJAkD1L+X5EkSPZ7FDKqt2bGfoETWGnlpH/zsUTUpchlf6Jf -w6TJOejQer5FQNLCtQSnOIchlAFKzFxhGSvcOrRWiBAPjTVIkv9eiCNXcJ5PE9Sk -8+bmn2ztz7LVHcv46PmT/+ihRxKJ01T5CsXWPUHOZQRfGvKZmyGf+iTBuhcxMPYC -nXb7/M3rYCQXL8FQZiaqbIVMqNRpMBVkAqU3l2JnSrlNIjIh6Nqowjog8QYGuIz6 -fxwWkw6EU5ZBwHIr2rOakCnQoKeXVqBJdWZNRMX1Vtqeh7O9zDoW4/0CAwEAAaNT -MFEwHQYDVR0OBBYEFHNgZ4nZQoNKnb0AnDkefTXxxYDqMB8GA1UdIwQYMBaAFHNg -Z4nZQoNKnb0AnDkefTXxxYDqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBAIuLuyF7m1SP6PBu29jXnfGtaGi7j0jlqfcAysn7VmAU3StgWvSatlAl -AO6MIasjSQ+ygAbfIQW6W2Wc/U+NLQq5fRVi1cnmlxH5OULOFeQZCVyux8Maq0fT -jj4mmsz62b/iiA4tyS5r+foY4v1u2siSViBJSbfYbMp/VggIimt26RNV2u/ZV6Kf -UrOxazMx1yyuqARiqoA3VOMV8Byv8SEIiteWUSYni6u7xOT4gucPORhbM1HOSQ/S -CVq95x4FeKQnbEMykHI+bpBdkoadMVtrjCbskU49mOrvl/pli9V44R8KK6C1Nv3E -VLLcoOctdw90aT3sIjaXBcZtDTE6p6g= +MIIDEzCCAfugAwIBAgIUMtt4ZsS3KWgdBSet+D9ifnmmYiQwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwNzE4MDY0ODIwWhcN +MzQwNzE2MDY0ODIwWjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN0AJjIqUVqnbZbiBEm6b7migSGIA6js +0KHOtg41Ai4PHPId2CHuQ6ZKcSvDE8AegeykkxLCRNagvBMo6ua8u/MRdp/xQ3AG +hTDTGly9hppcZo0nkXQtvXSLcNK8L/supSzrxnt0qD2dcaq8KGzPBYk/RERLpEVq +Z7sARId/QI1S7DMvSSTGgHrqxy0VXBd4E+ziElIdZG6mEMwx4agyDK/Fslzw1Cp4 +Ufv1NXhIzSofrKJ9nI6wuKSAymmJh1G8l72H4POa+YCLXaPdDUVv1j/1HqrsWs9f +M9/z8GfjeqJ94WJ/Z1P65si69eo4zX18TzME7EwZyVKBiK1eA8phTPUCAwEAAaNT +MFEwHQYDVR0OBBYEFL8s7KTIH5P0tLQ9TdQ3bKAatD2DMB8GA1UdIwQYMBaAFL8s +7KTIH5P0tLQ9TdQ3bKAatD2DMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBANIYy7aAUp+UFfcxsPIcNCjAeYdQNi09wJ1cvg6ctU1GzRGinvr9PiCR +f13ZNX0SsK4farNR2UK9TTF1vv++sYiC6EgzJFWBqBW3XqjQMqF12rApf9faHvg9 +bcyWJNgQxuXc6ugxXrUI+Sj4U2LRnnEkh427/Hs1WbD4Bd0zfTHMCVmk6gvi2kcU +e3agZInTAIAwS59afC/6bGIaXb6QVyWlnEhWB2LJRIM0H7aXB2Ot63upin+yDZGU +Esz3c82RNPBzRaUGJezlaQq5ZGZyJjkkBWjYaSYO2RaR3h/PqhLIfshqC223SCCV +Uo2ofMIfAvXNA8hHK/u3f1WVTEJ4ERE= -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem b/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem index 1e7a83068de..28266b3e5b3 100644 --- a/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem +++ b/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCZjOHeUnlauuxD -WgrRnh3hj5Fs+uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47I -RgA5VLvGxI+T1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjG -QBmFF7NxrvjGgerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9K -zNQ7ZTlBQvJG8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzog -D+jgoAD5/9sk3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P -2jMpJ1xxAgMBAAECggEAS3NBjWgDP4T4EUROaqACWNKeB+nmkdt68T0gGtoNVD+D -EN9UPnpFQPdHFngAgWnzF858UIKzq1Pzdg+HjqRHPK1bS67tvua3xP1GHuR/CGPk -28T1hefqPHRen7GqHDAfdwarYBWCGv4Sjz/yCkcSIrtyfMBb5fAya5GO02pckUSK -19sl7XhkPtHJVirRkjQL29R2TCpkNNpQMjkuYLk7mox+6pNTbxgbk0cnT3eGj1pV -mlPqpwzC5GevRziE/VE/WXFLChY+8KB4fDLRqWnyvabDvQ4coaXgzwbdScJyM5hX -+Dxdfni/P2m7xAZXUyfBsr0VUzqUkJfK3WWvvAGTDQKBgQDNi3RUEjVnU/MN4aDz -iZB2VYGfo/K69xTPNEbLQWs1F4ZMpHVtUVXzTfx/xG9ug989ijEm6ncL9OsnhThn -UldSz2ojSJUxLmhgCHZGYHT72v/9rEqfT9JisWpIj44KXufUHCcl3Cozj1ae3EUp -NVhN1HphB2LsCIJvLYfLIGdBNwKBgQC/PhHQMm/MQe4pOHAbdzDrRZWdG2KSRVxp -9mmJ/aT8LOp7BDjq+Dkct6a56JGqlOTeJirMTTmCKiOiTInuB9S+K7kWJJiYg9g4 -UCiuMU+40Px/1Z4/uxRj3DSdGLXG7S6kPeADx9f9BUNpAytGqOnSnfbDiDVvQVbp -0N0+nIXDlwKBgQC2uZOXrXxGOE4pd/ySpCeF2yvZ1HDTnxWjwlBxHt4Em74rYkR2 -A0mKezjOCL4bHCaYWcKqWuOsAHYQcxEaYQv6NSOg7ESdLSlivgMPO26j+yN5yvGn -wNlCHYBjsyLNu2MSoFh5AsmNfo69uQnOwXqX7h1BJsTdGg+CcJJ4lHzWbwKBgQCD -/CRzGbwKrh3eGPNWIUaDuTxudy3qYTBMeSGReJpa5+zUBa/6imFwLldEyvttTOE/ -Z/v1j/52lPqO0mAHBSSQMsDERXGDIMsi4j+RKLsqhCEfYKCcv1JtMNam7RzXM24T -MBjgwxWPrAg/+03ssDrffuGFRQYLyH5hVCK9SW0P9QKBgQDJ1ZSto+RWxv/uOKNr -7FYeQoKpMb2IvNvnGlnYHC8KS9qRq6wUE+FtuKcdLBQP4M9Cgq71VD/dsawrhEw7 -1rAYk3OqmHxBOU5Dcb152NxYHEf53pfEfWc0x4AEVe+Jzynj2EYixRKNWwODNTEx -LKJOYd0CuWywxg6d9G7A7XbgWQ== +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQClrSse09wlL7P6 +pcsCNQ16CcrYS7+IF5yq9Nul0gnJsHl3XC1SKqNU1shMfcZYa4pEXF1LU414EWkL +/0WWVTs23U0fcUaybZJAG57jXD+571vEkoFCESJxBPLCtEOBUYbLx1IE2bZ9ybHZ +yD+W9XulQ7FgIS04tPgkttunl11CXdRAcMiL796/t8j0eVU049Pta420/hWYiVzv +fz74AyUCQwOlXhl8Io8HgO0NUNgWwu+3kI/2oswPZFcCXmSBTyht6CjSYNDtViVy +btdd+U7InM2XTKczXaweP1oBniWLxem1vRAltkYU0S89jnlcBtfoJHTUDKLBeNal +mys0ebrLAgMBAAECggEACxs0t3hUWwR7ouMtB2ovC8NO9Hj/efa7QJVG3t2EXR+W +Gkj08NEoPy5hdwnncKik4uLzjh0nxVNwIKcYLx+/kcn8EDjebbpSrOGCdpNfN5kI +JIFTEO69Hv50l6t7QFa1cUEHX96IZp5NbIq0A0FUQfFBGW9Kl3E/lpZ7hcdBL+BR +VrL96mTFqKtvYza/8wOOT05XCvVZH2Q/cN75Ih11t84UHtUCZWjAOoRhddcblo/2 +jCj91N2W7/Zut94o/2KcRY8Glf8Dps7gZUE1cB3PaZvHU5eEsCupwyNYwx785Pn1 +zzyCGklxeSu7t84WJh/B8dx6uYGiVRcd+ujc3BLKgQKBgQDerZ+ReVEF6cWYOKQD +6Hv2bv4PdME5LQkq1XoT4AQNRb2pXw2BrA/Crr7xNlOz2u6hN+LnmEFLfzMR/5UE +2UAS4tENTb4eibqgeBPVx3RksfpHWni2y8Dvdax3GgDwx9BVVMycOe3td3IlpnAN +rRU+jYRZuAZr5OQdQESu5bNUUQKBgQC+d+qB43WWtla47nFMEWM0RB6nHqbAo+dw +0BIaoxJyuC6SyE7P7APdBAlOw8P0Hy/Dzwe+ZzGnjXiXYyctTHss/zSLfhf+43/A +Z+Mzi5u0vzL60ING5UzBY9P/0XxrJym0eVr4YYPBvLBojwLhiDgQIXUHkgUO7IBY +UPjM+ggiWwKBgQC+zq3FvNulonxThG1ef+8A6ni/C7+qW6HYV1alAzbVnKX5JN7w +91wF6TDqhi/RFM+Xy8idxMRmiddcG9I4dmRGCp8xtCUuC7ykVmBAtglRY4RfcfGw +SQXI6t9eqySVLdKh2+j8EVOEQO7JvkWUInTqxd7b9ilieJ7TRcfUyjUREQKBgQCR +QWp6fDllItGoX0/QL0J0za6CzQFm0JjklAn6fnrHOmdqUZCpSNj5aOagRvPd7RrE +PdMuBgz8NwvMiDWMelNF0asE5rjuDhmTZqcC3Gl2wonidbpoCt8qbTN0WRKFtWw8 +0n/qBJQy3++5Dbeov/Xhd2KEz3tEEmEe+UGFMPmbGQKBgHcpn8SGrNuxjL/6vB1j +a5+LNOQAtr7akKlJbrl9B7poMVu8483fN4LG2HclWPPILnE3iPU/G559ZMo3QL6G +0+dImewlUSGtQ7GtLssai5wmmgLfXufiewHq/WoLDkd0xdu5RHc7GGgixx8XqpBG +ZTrVeb2nVeiwvIiZkuT13bGf -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/federation-v0/integration-leaf.pem b/deploy/dockerephemeral/federation-v0/integration-leaf.pem index 635d332de70..9eca6a70899 100644 --- a/deploy/dockerephemeral/federation-v0/integration-leaf.pem +++ b/deploy/dockerephemeral/federation-v0/integration-leaf.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgIBADANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5jYS5l -eGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzFaFw0yNDA3MTcxMzE1MzFaMAAwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCZjOHeUnlauuxDWgrRnh3hj5Fs -+uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47IRgA5VLvGxI+T -1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjGQBmFF7NxrvjG -gerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9KzNQ7ZTlBQvJG -8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzogD+jgoAD5/9sk -3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P2jMpJ1xxAgMB +eGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjBaFw0zNDA3MTYwNjQ4MjBaMAAwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQClrSse09wlL7P6pcsCNQ16CcrY +S7+IF5yq9Nul0gnJsHl3XC1SKqNU1shMfcZYa4pEXF1LU414EWkL/0WWVTs23U0f +cUaybZJAG57jXD+571vEkoFCESJxBPLCtEOBUYbLx1IE2bZ9ybHZyD+W9XulQ7Fg +IS04tPgkttunl11CXdRAcMiL796/t8j0eVU049Pta420/hWYiVzvfz74AyUCQwOl +Xhl8Io8HgO0NUNgWwu+3kI/2oswPZFcCXmSBTyht6CjSYNDtViVybtdd+U7InM2X +TKczXaweP1oBniWLxem1vRAltkYU0S89jnlcBtfoJHTUDKLBeNalmys0ebrLAgMB AAGjgawwgakwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEgGA1UdEQEB /wQ+MDyCGSouaW50ZWdyYXRpb24uZXhhbXBsZS5jb22CFGhvc3QuZG9ja2VyLmlu -dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFPowAfmLPCmdCMdSxQjsR6UQSoyH -MB8GA1UdIwQYMBaAFHNgZ4nZQoNKnb0AnDkefTXxxYDqMA0GCSqGSIb3DQEBCwUA -A4IBAQCMJwbLzUsrkQkgdGKVi/Mb5XAAV0sfkwZch1Fx0vhJI072cZSow5A2ZUHa -LScFNTPmilPKEr6MS4xIKtRQaMHInbfxSsyNViKhpzkSOKoAiJjIJ2xPKFPnbTDI -uV74nxxyf9q/p3SLQfJFk7fxbvNeLqg5bYSrMeklHj4bpMJ9fybS8/mZVc8AkTFK -fsXSu9CW1B3GF+jP3E2GrFF3Zh9MgvWjMlSYg4ljPf5FoMCUq6GmQ17hQeJFvb5h -Jqk6TcgUrp082bcVlPW17XzFwVe3n6uzvWMtwI62EztVUj98+YkBiFL3i4+OQwAU -/noc22fq20OyJtCPJY4FIK7xUcgD +dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFIro61Yvf3swiRDOD/qOkwKJ0+Li +MB8GA1UdIwQYMBaAFL8s7KTIH5P0tLQ9TdQ3bKAatD2DMA0GCSqGSIb3DQEBCwUA +A4IBAQDNWgHWibMJvGI5YzkTlgXEvxjTTdYM6SpyLQFkju/PUuLP4KoiOvl2SY// +OWJH9v1XmZJ1DlnNRdgAHW+Uj8SpXJXRPkm1/5B9d0Eh8kfc+oiapZT7qfrKH5Ln +Wod5A/gzBv8rpaqHP8HP00b5SFdStTnqbQeBPXYMl+cbVwHBtZF3U6NVVJc0VOEe +MWx8bhJ6Vn8KmcLLoPPJVf4/u/toFAm7q61yZWISOMpZmLMbis0M+vJ6t57ImVTv +utffv1HhuCfEWQSc4XHI9JOMc3iexJYfgZQCIZdC7VxEJaOVB1DSkc3bYq/UaEyf +Wo6HKS47x295j7b1rbSO5ZCbhPYb -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem index 406f6d9ed97..c24f0a00333 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC/vE2Cea18UZ1J -J0a3IkIoXl2JPSJp7y/bPXsN6sk44F5Dv9mt5hxVERyCQSMiuM6dXfzkRcMAZ7dx -5nQ7GpSEJksqe4h+WFHWDQjaoxrOYVg9UAa6q0rq5h+uHZEpBWwJWNlwRgzyf5zf -IZnjttVD2mu4Gp2xRqtNkEbAOgMJp7ijb76foKsGLFrxJNA3khNjsnDlwRuoffVS -LafF0CA7cW2FYxjwKM/IymCaRVUS18IftCtm3KCl5ou+1aD0/rMsLMKEY1HYCyGo -ZSOnvd5xhRPj6upk3MpWUUyULSkpkQtVPy+RZKUNXb3CGVNJz3UgvMwNXKpW9FdG -Suze9HxdAgMBAAECggEAEU8SKZA10tOaAQue/P4GaOyJQdAXYObV3tNAXkjux3Ks -hS3hnIBPLc1wpxWdnWR/n9c8nZg/+rO3l3xiy8nM1IKR0JD8Xnjh/RKKKmqvtdKL -NmXDZcCm775nPRRa5rrK6QEbXWEFiYgZr6Rckcu57vkzNkM42dMeYyR+Lpujazs6 -Um3Z7rPXevX/gVr9XHjxJ5bX9WYB7sJfZTHLqkO7VGwrXf7HGrtT1ES+iXqjGLpH -5Sg55V5XJfxsqhq+TQgEnorzp8+LEXms2HYTP3G47wP51IWbHa54BUBwkwhiNYV7 -os71j5mrZbUnJ/2KvQPMjiF7uHKlKYjxXiAoj9wRZQKBgQD4e4RuFVaLtF1+khNI -uEgmY4AfakeCB9D2Do1/fhLDTT6EdAxFeSx62VyY3wTG5Pi8DyrFIUNbIYbO8vRx -u8XpzCPxn9TnPnLZ9BRf1+GrCuyQWaFZOnnfAovk3KK4D3vWD9Yn38aTYpTd+3Hg -AEIzd7Bd4dozKtKW7+wI9uOm0wKBgQDFiUih6D0TYrS4T+cM5KhI+ErqTTiFpZ/L -BvA2hyRZTbP+erII9A+IqRNlwidGc1UF4xGu9Ei5QBVfFFbch6C1IRwIoog0hqsH -7s47VIcDuoASq52DHoUABbw9SrfsLjAZz5bLNPmvrEorwIImHNwDG/yOgpT8z7PV -z4/MhoWyDwKBgB+8FrPAgechx/cMTO4yqvRMLObWOf+/Y86pGSU5Qsgyq1NbRt3w -ld+ytwLHKOMGB0ZtYXb/wox3AbKYkOOdqa8sZULMuPI3pY90fs2m0ql3obLl35d3 -wmza9GbsTtPXFmfGagF5sPDN3FllbavAHLRaCupSl/2E8JRaW/jhHz4FAoGAfL4H -Ggd4mkdY7JO4ytGS3BG/7Vo6eVtwH1wQUb7h22tQYUHGMBU/wgNTdo03FCw84uzT -+/HUAvhPBq3ndHhJqlhwRZut+82XL/lETv9AC8C4pBGv9F9PigYVK3eF0iYQxhvr -lAOuMZvRcvOsvLi4z1XbFXus7kGTxU+/9V52C00CgYBY5SgRETt5kgbH/rm36SsE -4x58yK8uYF8MgtBCLxn7E0vnZ2cAMmmDC9wWCHtuq2QhqL/pB+fPI8ri4XNPMXJC -faAxJ0VNmz8fYTzliAWy3Sqp/kgeXdrX9KJkN24LP345LocDBcaML+thDFevmXBW -mahBgoa1ZWxnLJe5XweVkg== +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC8A9zUO9zbB/bH +Tat97i3Ypppv3u+FVk6QisaSJLECLcdnyxe9YqIPXA1HsEI+lbnxsNiaXXvD0f+p +aJfJBgN8ff4GCu2x39YKvwaMSVqH6rWljbtA/gVSy+SDrNPZTgP/I0hP/DKA4s9K +xgOQOIFOMsy/epitKTt1Ou1eOTNOo673eHcS0J9V6uwy2+j9VFANmIvRW3w3KwNu +h8vUvKYzsZQx7upX+YyfbhN7YXVnCEBG+SJOR7KuMd9gUxoWqrQVP3lpEQ7b8hXx +0IB2JPgzaTQ9IaDYaGjFiEQHI6mipwHuwTS6dIkXbg2iBLAhA97T+SZkj6wLltxI +cztwaQWlAgMBAAECgf8czfMo+jAM1vMKrFfSJC8lsvcq2aY6Ya+RE5OrUChz3fF9 +6gytvqcsBhAv9sW25303OvshffxHNNnFaZdfC3n9QhP/dx4ZD2pk2TIcU4oO5swx +QdwfSdqdhabL3+6F2IhTRsz+RuUMeA1+xUhkl47dhnnucpJTXpCbhxyS1rCYbj+E +cqkS+oUOne1F/YlQANFKZYnf7nGdjSw9u7fdgZ+gANCvJAnliUfClV89kO9Y+F3E +aVC+yJu8MaBP2mci0M/qbt8YETVZ9c1vC2JKPdHVJBZNKuYfGdtp3fEC/TsvTzfB +wEjqD3IkBq6xK8UQ1bur7+CUjempFV7cZ+cCn3kCgYEA6OELeu1epC2rms4nekqH +nNTvOmKCONHYnT8h/EIYSor7vKKGU4KBTOVtz9Kr36UTpaLYWEsZ+i/eYu2v5X5F +iakNo3yrQ7A6nkoCD2RGXinTDd5MxpKZ5gpPwEhq8lEbDMYv8LcM01H+Hm5cajP6 +XzbO58onSCik7lGL0mICOvkCgYEAzq6JAS+Tijv+JqB4PeEYcxp3jzSvD6Pv25af +MFuE2ePHmzV3pRV8Fdx8YNYhlARsJz/tkWm4IZLpLCoDbFbZQ/sOROaPA6m0Wetr +zdGaT3OGvXKTCn3dPZ76TQ5dSTyMTi/HCPT2NIHBuAZAkSvNSHlnKm29YpSMK/OQ +W69+/w0CgYEAoVME+OtnHKTmtB8MChOHToXUE8YaH/J+9K+/g1jmKv2M1mhgVYma +uQJWyBlRJ2Tb72qYJNIh9Mckb7PonjqTQYHzCMZcfk+ey/jI5JC6jpC6vGi7FvSH +2GxcQv/n1mWJL5g7ra2hHOM3/yzEqG3JjBwTyU6pV7uQRegHzH5IvUECgYEAsLjS +Er6AdDFJ5fNN/PMMOddGpZ9RlJkDTYpjwTBvzvMRyKeWDwTo3bRycUaG3Y5Of90M +oEp6E9MPJyEhXjCAg70V/Vn6rRIdUMmYmxr+y7KnYjOmgNEQLFFUCjEfGLD58xyt +Hf5+ynSslFJcQQTn+XE9Ai1lQvZrSGVxaMQNXb0CgYAqYI15K5qo0tnhIBXesCdY +uPRttUEzk0Ew1ijTHkKiYC5KoN1YGjL4Zvag+1zsW10kr4u4mYNvtumQ2jp/kcOG +P0RN1FHaXO2W2MdEyChjf0xfbuk0Y6AP9BOXOxXLrL4Vh6+EXgu6VZefDKBsYDau +5GdldZlC4GnuCnTUnehJBw== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem index cb18742fab2..993f3cdc48c 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem index 6d5744d1f7d..617f4165cac 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDPTCCAiWgAwIBAgIBADANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBdyYWJi -aXRtcS5jYS5leGFtcGxlLmNvbTAeFw0yNDA2MTcxNDAyMTRaFw0yNDA3MTcxNDAy -MTRaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAJZ3b8mfnf8XuUJmFQ8xN9V8N1PiMe5X+WMqOKduZXqPeW9rECmC -B3opcDVMQ3iyRtc+fXYSJiCllMeCCwzIWQw+k1PcFZ6zXWsvtEFQRCN91vcShZm0 -v8YlNcYl3wxsnIcZ5/IAZTiyX2U/hTBkgOszJcfe8cBOZsI9QzRuLRzE3kkpA+U7 -/3ekPsIxk/g0NtbRA4BgSrcKl3iAI4CMJTJlsezQbF6LZqW7yIOyvaQzT0kyJ564 -0X7YCT5QozL09ZdbQY5b6pphNNfXqY1KEP/aje+UrzQm2R3e9BUGMM4o14pQOU7Q -cxWRjPSPL3nDKUxI3kI9etrluFLH9lQ1uT8CAwEAAaOBizCBiDAdBgNVHSUEFjAU +aXRtcS5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjJaFw0zNDA3MTYwNjQ4 +MjJaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBALmQ5cHMo7xPrsCnBX67HULG/QLle0hAp6fQr1qTzHJ6LG4wEsU3 +KDyoQopwxXe8DaloKxKpYBDSQM46FKUbk57el6nHZfQi3IBPe1uEpRgKaDh6dZiB +FJ9JjoTMcacr5bchdtUgLiTggacy5CRfSAw+RCwSefrtVah6ciFm1nVVxtvceE7w +T7SKQrjhWKPUUraiLSKGVBmpNn8pPJQglIVoY4lHvMzH5OMj5nLEIqeNoQeazlL4 +2yRyNqviQYaCOOc1ihqYvT8BXU4nakN5M/tdjTJqKeza2lZ/lz9wHdYrRAIjZjQ/ +7ChjHUWokBjzETkTQhlLSY0uDs/aSqiHslMCAwEAAaOBizCBiDAdBgNVHSUEFjAU BggrBgEFBQcDAQYIKwYBBQUHAwIwJwYDVR0RAQH/BB0wG4IJbG9jYWxob3N0gghy -YWJiaXRtcYcEfwAAATAdBgNVHQ4EFgQUf53Mqv9QZmcO5uwUUNZcMQA05cAwHwYD -VR0jBBgwFoAU3yBZkYpHT8r976rIZw37wB4iiS4wDQYJKoZIhvcNAQELBQADggEB -ABXBCl+jy+EeDPLwFlHX/DTJrce3VQMAG+x5WxbuKr68zS8uwJFfqmb4dK01RiSe -QAaISp/vr4KRbbNc5f/TA5dOhc2qXf8dZ0rILWE0u1I+1y9DFuNnymIywbodo6ho -ln7bj2wNl1vZ1A6Tm9fH6MJhavCCM18AHZuz+ml9b8SSVnL3XfPUWuZjYnElSXWj -qTJUF+o/1QC3E+ILj5iiwaAgp8kJJezr5m90RC/DTchYS/CRtz79jYMY8IMdOpN6 -JC92KzpO0jKZ4qWkDi4ZgszPTNcUdnjUc4botJrfZhioA26skUiuacyqfpvnspno -y5DFD+Od2XpBCCwgeYk6IPM= +YWJiaXRtcYcEfwAAATAdBgNVHQ4EFgQU2jdkp2M6Nljo6K/8GJCM51I+99swHwYD +VR0jBBgwFoAUpyKhnMzMAmNc6EzYZ/14gkiD4HEwDQYJKoZIhvcNAQELBQADggEB +AJV2gv9PZak/1uNaV4C6ev3PgcLui4eBZwBeWM4mTrVNSMoIHsgj4J3lSWBW5KB6 +Ly4Ey9qdHdtx7OdHeRgErDtwkgblUc9jEIvTuxNNmGrgxzX7TlksfXg1dONoYhPI +VzB2qE5entd8xyG1JtzCIOHcDcqUFphqajeAz0mWElaaz0VI4YGE1NDF5fTROOrK +4V227FOSXCHoZeymRAp+ZfDNCjzYiO7euWxtmI2g4utq8VjxuLfLRq/KBxZwxrGK +mtRJV9xNgcgJQxW83Q2nITnOK2DI7Kd/4rbOUGOEgzRRaJIQtEH82dM/cjNxusyO +4ZS62G2D28rd8MDtIc36Bn0= -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem index 6471c8d1781..a555f7a5054 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWd2/Jn53/F7lC -ZhUPMTfVfDdT4jHuV/ljKjinbmV6j3lvaxApggd6KXA1TEN4skbXPn12EiYgpZTH -ggsMyFkMPpNT3BWes11rL7RBUEQjfdb3EoWZtL/GJTXGJd8MbJyHGefyAGU4sl9l -P4UwZIDrMyXH3vHATmbCPUM0bi0cxN5JKQPlO/93pD7CMZP4NDbW0QOAYEq3Cpd4 -gCOAjCUyZbHs0Gxei2alu8iDsr2kM09JMieeuNF+2Ak+UKMy9PWXW0GOW+qaYTTX -16mNShD/2o3vlK80Jtkd3vQVBjDOKNeKUDlO0HMVkYz0jy95wylMSN5CPXra5bhS -x/ZUNbk/AgMBAAECggEAFSsQawktrSmlQpYh+FUwSbSEBCUaaTGvQCg8eDGrzSZK -K0agq3ZDnwgdZSIpi91o4fdEp0u+WXFyEO9WpqG5BWP4Th/0WrNZPS8k6Ntl+qhF -idTtPsaTBElP22SQkKrnCoq2evFbTDKsAQ6CqmA5Ut2LPyc6U5e0FTeRMNsfNaC1 -e+60J5yjxYWfZQdU5F+uiycWWiqabOafJfbN0gdLeuIICG+Z8AuWoUjLg2v55itw -X9T3AWZ2+/kdUY8j5FXFoK2MfuzW7Ys+Y1JeLMHrquy2hicSMbJE7vnxNsv1VMPc -IZzlgS+N/Lqre0S0NQAKqTGxe4PcUw+Mp5ZqXHtBwQKBgQDEViEeOAAtfvpK4pFv -drXmv2KacieEtUeEVfgbzMY4tL2q7RfFGxC4iiLklvwhQSGyfRamtut+t+eR4eFx -XKHaZxobwwfW5sMi6Ye/iyuL3YXvtWiaOz6XNImFTeWUPLnrX5qtMuVbx4UGiKa7 -kjg/214A8Zf/qoVJxzAJwp1E6QKBgQDEMOM+dnUlUc8FrllXmlsGYMxwWdQ+vvvw -BdKrm6Q61z3+C5189VwQQ1+ruIcmfVqCm1BKa0J76evgdqHo/pgiAaGEhItVt8cN -3IVnpQu9Fhphgd/iFYxyTOCW2d1Nze30H1oqwpgmZsw2vE/6WrU8e1j279+SUevS -2+rx7i1T5wKBgE6rhFGrdsbEHl5rMoNLOc/f2A6ytwsB6EoqeGQLRVHreiRHJEMi -eSy4jQqzRQu+IVZ3sN/UY8A+yFc3/zGBQIlWzqtZFocRqBcRJAeoKCa++K/4LJXA -L3A+6Ou1LsybGJQrlrrXrfd8ltzrXIPELy3HJH+UTqdvGEFbwu/mP0YhAoGBAINX -Pyp33yDmzbM97y3Idhuk/fhRCtgev0cGfuzHu4BwzF2gpQQctk9k601osYHA9bDu -DShk+hM+nNyeTvJOTsalVN4EZcsyxx2ufdjPEza471xLt/gA+Q8kDE6w94i4zg5a -VuC9eWJr+1bBZsFxrFcbNInMOF4aXcfB1l20V8ANAoGAXZcAv5zU5Cj4ktoe0uqi -7p9zR8mgW2oXU0orgdQ3Ce2Z2qy4yFU5AfHPmn1RuRFsQCxX8RpUqLDHOvpn6gyt -/u9GBqlCqYG4KAbGKGVjodEIXilbIVNEbCIi4kGcRO038fzZJawwhrXg3FuMd6EV -G92A1vtGnTZYkatPK4LRnBk= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5kOXBzKO8T67A +pwV+ux1Cxv0C5XtIQKen0K9ak8xyeixuMBLFNyg8qEKKcMV3vA2paCsSqWAQ0kDO +OhSlG5Oe3pepx2X0ItyAT3tbhKUYCmg4enWYgRSfSY6EzHGnK+W3IXbVIC4k4IGn +MuQkX0gMPkQsEnn67VWoenIhZtZ1Vcbb3HhO8E+0ikK44Vij1FK2oi0ihlQZqTZ/ +KTyUIJSFaGOJR7zMx+TjI+ZyxCKnjaEHms5S+Nskcjar4kGGgjjnNYoamL0/AV1O +J2pDeTP7XY0yains2tpWf5c/cB3WK0QCI2Y0P+woYx1FqJAY8xE5E0IZS0mNLg7P +2kqoh7JTAgMBAAECggEADpXzlHsAobqEd2ZWQCLDOlT7kGDRgsysmojq+sxXceYY +tb6EmZWfM+/CO/4aOP7O9uWWGrzo4EABuDpur5dBaeezW+gTvPYihwyR5/ZYp1Ey +5cGTNB4G4D0lY43R6++lrXI6OqbxYy4PmUDrfgP/qaqy30ePTyhpjVK2kHNak9VV ++wL89csanvb2uKwhrgiFx+8BksFX/Y4CNInF1LIbNIMbehMCxHyO6TqhmErgwqPU +E2R6l2EsiHaihRurmMGmW2UitamUzFUAP3pnFcxCYWB9faiE4YRm2bemSaS5SufC +3NSzvfSJN7lmvx2q4+LulWiuLJ7EuqtzjmFAxFKgrQKBgQDlgdo5EtpSDbfbP5ME +oj8YBwdA0ZxKfv0yW7y34k7RtagrfC2ddNT7haZlCGQGyJ1leeQHSzhnZYvcWlmU +PH94YjzfoGQHF1a7qAIVldyi9sBkxI4jU0qbxKcSkfoGqEqAdfZo8nUzUoOTA90j +4eQK1ut7qT7Ss6yLE99HjSmpRwKBgQDO/IqvMPNlhzdDaFO+EIgLTD+KmbGFTtsA +PWf6RwFo3tVBgmAv4KqyutZlWxrqC2g5Ksfyw9azUz1U+BOF2pWohNZo/OHrbbkp +4dkrEANHlxCokDqXOVKaVTEKtqoZzT22VW/oFB9GxlPJL7q27PctXjiiB709uNJa +ph5jdsx0lQKBgQCJBjYbzT27r6UNqa9FHPk+hzO1Z3BAqgDRiCPsRZl5a1O0Yrd5 +Qr/GS81ElPXjdvNCGrwh/q72TJJsRSUmc9hHL5/YhBI0iaKm93AHIypPwbKsdw3F +2Xy583cshysXvnJ8r/EmR1viAGm95JirS7qzHg4KDsoLUmq5vmuYdJdjEQKBgELp +fOO5jVVq6sCNv1SX/4K3eWsS2EJiBYYEU9KilaATORleTj3sAQKaR6ioVQEIAv9I +By9Bg+ygohkPwS/qQ6sgljeGWHpFFDCn5A55tLW17hqv1WEBlORzWdE+z6pboPGK +mQyLRLkacAd/uHpeDGHMLb6jhdeoIchQH07EHsApAoGAbLLN6YsiBaP6vSj1gzZZ +ro1eKxyBSjzBixWRaTKFSbe6maUe3VNZ6z7m4013YyOuptanqSb/lqGwIotI0BNG +b4NPpAh7yoWJbcsuaFqa1DFTb20yVL2j3osLMsCOMtuKI/zqCzQgHgyqBnnYmLjp +JuXF8vUCg7hxjhk3pYjhCvg= -----END PRIVATE KEY----- diff --git a/hack/bin/gen-certs.sh b/hack/bin/gen-certs.sh index a2a33a26253..cea23db3ff5 100755 --- a/hack/bin/gen-certs.sh +++ b/hack/bin/gen-certs.sh @@ -34,7 +34,7 @@ gen_cert() { if [ -n "$3" ]; then subj=(-subj "/CN=$3") fi - openssl x509 -req -in <(openssl req -nodes -newkey rsa:2048 -keyout "$1/key.pem" -out /dev/stdout -subj "/" 2>/dev/null) -CA "$1/ca.pem" -CAkey "$1/ca-key.pem" "${subj[@]}" -out "$1/cert.pem" -set_serial 0 -extfile <( echo "extendedKeyUsage = serverAuth, clientAuth"; echo "subjectAltName = critical, $2" ) 2>/dev/null + openssl x509 -req -in <(openssl req -nodes -newkey rsa:2048 -keyout "$1/key.pem" -out /dev/stdout -subj "/" 2>/dev/null) -CA "$1/ca.pem" -CAkey "$1/ca-key.pem" "${subj[@]}" -out "$1/cert.pem" -set_serial 0 -days 3650 -extfile <( echo "extendedKeyUsage = serverAuth, clientAuth"; echo "subjectAltName = critical, $2" ) 2>/dev/null } # usage: install_certs source_dir target_dir ca ca-key cert key diff --git a/hack/helm_vars/certs/elasticsearch-ca-key.pem b/hack/helm_vars/certs/elasticsearch-ca-key.pem index 53785fe3292..f5d62ee5d49 100644 --- a/hack/helm_vars/certs/elasticsearch-ca-key.pem +++ b/hack/helm_vars/certs/elasticsearch-ca-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/oFJpJMdyG9FT -pNw4K9f1pdkNikwbQsx4dokiQBMTu89IMTnNfsHz2IDrxCKTCKC3oPupniaEPNpr -YpV6RMz1UPvUYu/IpvOXGeIGlVd9ixcoYN6763R2nZhMlFS8Tma9mV+e/B0jr9Db -V1pUWIPufuPrYXcOotxDO/W5I+GpKVTz/ZGD//O5odX1mJzkwqjeqGa1WNdg+/AL -iDtVZ/YAKGdfjx81uqc16fYuYRDw3BYImBp5MyNu/jxdgNxFB1edcVowvcKXVs5p -Slay2ad0eQSa0Ux8n3RjfisjTLAHks/4dkPa3hQyBYzmxwBhMcMDc06yxiCkXsVn -lXRn9nf/AgMBAAECggEABQZr3GzMSImPaRvqPnrZdFkMb30QVw94YMxS9xf3dOc4 -hB8hi4PNPqf1yx9e/Lx9yNleE1BqmCf0XltWdvKPVJUlrw5TiJwZyGOZ+F9tAB81 -CA6j29YZcFoPoJDfOMghjGVIpNjdqfSC8jP0BXQ3LK22xZLOIw8eqypLKYPvkTA3 -OfuJ/1doiHl+geZkXaKcLSpCCddLKCaWSbLyqYMJxbQ5SSZ9bPUeQ7aQppb5M/wO -1B4+oMmRLcmG81QnL0kU9JiAtYaGsrP22qGuEGVjEZE8RXJz3iQ1KvSlj0xerqi7 -/LY0HLixkx4n3Qtpm9FFaT3rzeDlJIE54qmI73sdYQKBgQDw6KJJIxmQScLZb4ml -yjd+pBvPuUe9cM9KMRNk2C7Z1QMxORXsIbgCPqpkJ96XUquta2ii7rxt/sXAkrh1 -c8IYU3Qp03+585J+6lZF6yaH9TrwYfDCRqKoSgAEwhJlvtoWHSMI6YguWsaczdgH -czd+0OzJl1w4vQqQBuXFwz4eEQKBgQDLoVzk+z/1//CJUfAe/Z6WYFHmTh+M9RGP -vC7GCQVCjIFUNsXqrWLl6DL5UeipYVhqu5eB7vOo/gNnb6J1vMOO0j9e1cY1Q2lG -BdSIHUD7P1Lly1/K+pn2QqIIHp+72H5qsX+8R5Tkln00jwQ0t5DrMVgJvWBW7/GC -lach4BZlDwKBgQDjfdraE8ItJepRJ+mk3GtBNLlqk/0x4FhvKB63SQoc+/Dyx4Rz -Ing/7ms6/wdMgG3L6rS5v5XCjSayrhpwFyr/i7cTVDy2HVOGc8Waau9Mzf+lRedz -nf41ywNvetCisfIBlewim1zU4TXSlvNcPan3IFWqHDui/Kj/zvOlp7R98QKBgQCn -fdi89/TKUXT2XpFVzGLvadazyrqk5MdHJRCMD8tly9BtBoiQ2YEpfm6/KKJpAAsL -77VVSMjezeDa6bYFhfiMt18skEXydbpXwF/qfbV/c7yqCziF6s9NAc3pQ9c7WX3S -IKHiqjZMN4RRAPoCqqLm8bVqfXyKxd4u/Q12Da4d/QKBgByVkmAoFVF1iYkb5b/0 -cPRXMnn5Xw5C6CRWpEwl3dSlt/uVACcFyxKsUP7QDTbqN7DSl/RX6DxkDIR6GBEz -vt0yXsdFx8y9Lzw1TNj2zhPVrtyslX/GFmi0R7/oyTEuVOMNy1rl/wftRqRX90md -JLSFJ4QslRlPwYOWGPDjJZVe +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5RatgsLSQdpLd +JyW+kmpOeyIoJgJVm+76D3eSx5tPnSMtsGolXNJHynR4AzJ+h5tiSOhK+x/RPiv7 +ofjD5P+cl6q0cWckhQ089sp+deRjZrhvTskn/0xkg9W1Gk2awYl9Oq9ij2hLSQgt +w+QRkv0LkdnQLKyGWQ8N7BLksAcvr9N5gTaLE3PVQQopSaD72IE+gGk4HDQzCque +/eeqNjp5qq9umFlXJk8GX3HfB14VRslGmh5OIs8y/HJGFRMcbVXJ11T+d6xpmzCF +fwXYSN5OJghs1AC6SyirqYEgOigJsMnioYbbyKHh+HLAYKnY+Bi/KQJQ/ipwzibW +9G//058FAgMBAAECggEAIL62hnVUxH+gf2PO4PrBvTM4Gz50hSr1Ns8LBC8xPQX5 +1LZsXEQmije3FAsEnqZbCSj3nWD7A6FoZqX+8KiFoOiRbCjq4OJ/L3oy2dz+S685 +A7s6BE6z8sP2Pnbyplp0cWSw4MuV1FCJGIWZxp1jCetyQr/SkkAlUAGcaTzPWFb/ +BjbPM9VIZOgYXWMmvzdG5kQepn+lgHNXXQuZpnLiFAJsZ9esKyfjw4DTaAiZvrXY +gUX16eJlcOq9b+TZWBQKwgKHPCVc4GzPdoY4uUCXV/UuY7XVw8X/VXBzxq0jkqki +VvBR16Xl3hd8OcInUWj7jlf23ryR6yt7gsrCPhLFAQKBgQDk6azL3FCbqqLy5Lju +h/HEIFajl8HiQg1tL5/X/IkBeKw5bO0M7CzQ5WvhUXQViDu2xnFW0q+QT7l2sl4G +2CSTnMpBGCT1FigMBM5i9eEua6B3KXaNtOBX6dlvO2D4EBEToy5Ik6eydtroAS0J +ODdA/pGcjj7XjiMjskb6yV+iwQKBgQDPMgNfdovwikLqs2oJ8aKp/PdtS7K0EbmN +MhpwFV1K4uF/v1GzCcsrhvr4egmec7lhgqV+ODus97k3lxzAys5/VyAwo39cQEAo +QKWwzDr8JP2z5ftaXV0h4aph4DsoPnnf10tES0XVRdKW2mDBuSssU7VoCzU9+7ja +0wd51eoBRQKBgE4g8zkhGOIIe1Ure3LuMzYdU3TCdwoiQTLi7ktphdlatm1jIAUp +FqK1qvxcMKKovLjFQim//uviSgqZFj5/xvwap21QMEz2IvT3LvnXseOGGF6TaEM1 +WNyok+3C9nW0BiANsd5ThwkCR/SncheTeEhWmpw0cH5hpNyqHE+8K0gBAoGANCiQ +9M0w+UK1CcRUo2Ay5LwLxXXS7MWxgjvkr+aQ77MhtTkCZiHHBZQbRcXi+gKD3mo3 +Iwkg7LAH7liaImZriV7zeYsPGrgJ7pgnndQr3SGqxEjW966dLVRTwgPioITpxVG7 +XtvcHo5PLy6WQO5OUgBYoHKB2rKtnFiXfzI8kEkCgYEAtE21IgrJDzCennOfXPn5 +x3MZaO3t6fTKKrKXXUFYzh5JsQiUJQOzf7bEcYBi3VLqLNVZOg4H6FeydzEMbd0U +t+7+tj7vkw+B6q8npXIGZO592dNtKkoUA+DMGrjOOIV3d2GRzYZph7X1qvSZ4Twl +Q2QQlP+7XHJRVYE0oECGYPQ= -----END PRIVATE KEY----- diff --git a/hack/helm_vars/certs/elasticsearch-ca.pem b/hack/helm_vars/certs/elasticsearch-ca.pem index f17e9cb41ac..1c6f3128257 100644 --- a/hack/helm_vars/certs/elasticsearch-ca.pem +++ b/hack/helm_vars/certs/elasticsearch-ca.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDLzCCAhegAwIBAgIUMGKU64YSPkGrWyHiXiLsuoKC/9owDQYJKoZIhvcNAQEL +MIIDLzCCAhegAwIBAgIUXMOPFnGTAQ30+xOQ2od/HYZiSwQwDQYJKoZIhvcNAQEL BQAwJzElMCMGA1UEAwwcZWxhc3RpY3NlYXJjaC5jYS5leGFtcGxlLmNvbTAeFw0y -NDA2MTcxMzE1MzFaFw0zNDA2MTUxMzE1MzFaMCcxJTAjBgNVBAMMHGVsYXN0aWNz +NDA3MTgwNjQ4MjBaFw0zNDA3MTYwNjQ4MjBaMCcxJTAjBgNVBAMMHGVsYXN0aWNz ZWFyY2guY2EuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQC/oFJpJMdyG9FTpNw4K9f1pdkNikwbQsx4dokiQBMTu89IMTnNfsHz2IDr -xCKTCKC3oPupniaEPNprYpV6RMz1UPvUYu/IpvOXGeIGlVd9ixcoYN6763R2nZhM -lFS8Tma9mV+e/B0jr9DbV1pUWIPufuPrYXcOotxDO/W5I+GpKVTz/ZGD//O5odX1 -mJzkwqjeqGa1WNdg+/ALiDtVZ/YAKGdfjx81uqc16fYuYRDw3BYImBp5MyNu/jxd -gNxFB1edcVowvcKXVs5pSlay2ad0eQSa0Ux8n3RjfisjTLAHks/4dkPa3hQyBYzm -xwBhMcMDc06yxiCkXsVnlXRn9nf/AgMBAAGjUzBRMB0GA1UdDgQWBBSGMhy1Uvrs -lmdHKAGQ9avMSWhz2jAfBgNVHSMEGDAWgBSGMhy1UvrslmdHKAGQ9avMSWhz2jAP -BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA4vndI6NRcMgzba1y3 -lUPxy40bs/jQajR3A5fmCCX4c0ZeRc4YqE9cdYgeGffCZvPogyYjWDlavOma2uAQ -+3lZ35k0wG9GsU2g3fDIXpUuoSUjfYRLBQ3oqD7VRKYs1rDD87c+91DrsfIVZKF1 -W1RzOOvcW9QX2RHghFS4IluX6LEboo48cKtycA/nfmYDT/L9I4oYjaxc9l+HMUSH -gkQUU1xZnQ9GCqdhL3+2dmn0jvdgJLiFuefMGkE0oP/kFD/bhuOmDhpIDb10Cuck -Nw/nOSbBLINx2qDOa1f3Kox/PesQO4tp0dMp6XqZCOPTQ95vHsIOxuX1d+pxhX2V -ToWP +AoIBAQC5RatgsLSQdpLdJyW+kmpOeyIoJgJVm+76D3eSx5tPnSMtsGolXNJHynR4 +AzJ+h5tiSOhK+x/RPiv7ofjD5P+cl6q0cWckhQ089sp+deRjZrhvTskn/0xkg9W1 +Gk2awYl9Oq9ij2hLSQgtw+QRkv0LkdnQLKyGWQ8N7BLksAcvr9N5gTaLE3PVQQop +SaD72IE+gGk4HDQzCque/eeqNjp5qq9umFlXJk8GX3HfB14VRslGmh5OIs8y/HJG +FRMcbVXJ11T+d6xpmzCFfwXYSN5OJghs1AC6SyirqYEgOigJsMnioYbbyKHh+HLA +YKnY+Bi/KQJQ/ipwzibW9G//058FAgMBAAGjUzBRMB0GA1UdDgQWBBRRMD3Fk8ui +R5eRsezFZrT0oMKlpzAfBgNVHSMEGDAWgBRRMD3Fk8uiR5eRsezFZrT0oMKlpzAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAQSW/I5EbTbsBY4rIA +RX+0S7gbx2+d1fhMFEloLagXipn8vd8PGtX9riYSb4k2KmP4BjeQo3TMpJVTUmh8 +RkppbOLabIcyuCI732PJfwlkRHBThguv905uuzil8IC1mDR7qy6Rkg2ByDRlqAgB +icX/3uG7A6XDNsNrwP8Pj0X/YRSbqLIlFtyQ6RCCOYn1CCUDkciHgDMHlgK90r4s ++hrgtB6zKdMI89hnn8MLbQ+eaZ9UDpYbovBbDvZfEI5AaTlSQTL503+gM5bUgZRl +YT8z44Piip8VhkKwb/31C+tht6gNvqEQBFudusrHrg/KphpFTpAHQgW2vA/z9LJt +vzAR -----END CERTIFICATE----- diff --git a/services/nginz/integration-test/conf/nginz/integration-ca-key.pem b/services/nginz/integration-test/conf/nginz/integration-ca-key.pem index 812d4ddc4a1..56193dd04ae 100644 --- a/services/nginz/integration-test/conf/nginz/integration-ca-key.pem +++ b/services/nginz/integration-test/conf/nginz/integration-ca-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCUJVDizZney5e4 -rJHEp7L/cXMf/k5zMKrapCte8OePyraPjSSQJA9S/l+RJEj2exQyqrdmxn6BE1hp -5aR/87FE1KXIZX+iX8OkyTno0Hq+RUDSwrUEpziHIZQBSsxcYRkr3Dq0VogQD401 -SJL/XogjV3CeTxPUpPPm5p9s7c+y1R3L+Oj5k//ooUcSidNU+QrF1j1BzmUEXxry -mZshn/okwboXMTD2Ap12+/zN62AkFy/BUGYmqmyFTKjUaTAVZAKlN5diZ0q5TSIy -IejaqMI6IPEGBriM+n8cFpMOhFOWQcByK9qzmpAp0KCnl1agSXVmTUTF9Vbanoez -vcw6FuP9AgMBAAECggEACB7IgXoMEFiAAz0gS1N23gYRraQCmFFHWC8t+mkBhFHz -8kfmBGmZlm6/fcTro+kIqSNO5LkGF5ygGMPf4ayRn6h5QtP/bD7MCkUGYdLFm5bP -sA3AntXspQmL44s+SuT+nHcYl6hzkk/L6WsGNa2wkCFbmK3UdDArd1FWVUHuw8pR -2s2V1KpVR6/3Wdw86l3khcDbY3CHimenmGSxxjFPixHMpcni3cTPdnULo+vZT3fh -MMmsRMwQvcZXNFtUjzwelx+/e0MB+AyoEYPaKa+afKKQBxlVmldrn9q/m3++fkiT -PWLg4yNcG+M+78vldoJb3kHANYCNxn438LDUrgNvAQKBgQDGNoSjC6Zmt7OwmO0H -kszLTbzbtNBmV5aFNRtopSL5H/DcMpq1MUXxsCpEK8cRHlbDLaEV/lrADjFN7KNg -Hvy0B77iiHGLm2rB6psZpSafapFjFC24q0VKS95Z6UyTIUiajIj2aYEPz0HOrgFC -lw2Ba7VTV2OxWUegVLoxbaV2/QKBgQC/VhGUf53klmi2XEfh5X+CtvH6v5P48VyZ -8P8e4PcZVBvgAbuMPMT+EW6+46J73GMJ2ISs0kDZEge0k+RRzUVqWvUlBWV9nt04 -BUGZT//w8bqD8Dfo1TeRwiLYuYMUNWaAdYvs0nt49dFpX5hyd+KUB+A5v1QbjTSY -PQT3yscxAQKBgQCE4DteigrNRU0ikAImV5UOnViD+NzUHtd7CTUMm9esJmtzUkFA -Qn3fHffXp3lV0n7bbRVWByOTKHCJCqAjaeKCVcbzWgC0VEXnJX1AXeRcbjZ0syxL -ZhWXTvEKWUnKQD/Jy3htqCCrFofJJAEYQOb+4dO2wRjF5VIM+3+ubxDDiQKBgFIn -tqy4jydTneqPfR312OZbf1NXZ0YA/O3smN69YdwyTTXGCK2SelNNUOwN+fqNCslz -eqRqMwYBw+U5i1PEfAXKwHAA/S8PQ5WGTEB0JUVjxd5ZCuiihJXFcgj0vt+yfiyy -TD6HshSiGCTSszaTW2qMZy7khEzAONEVgkiTfSwBAoGAb48KvxQtxW+2RXkNWzMv -D7DyHm9jTTcTARTf7WtY0KMWQa//MPWofieD6KdzRd65lea2Z8wX5vcPVIEUp803 -zQrZMeLTcAQjsTsSP3qBWBi8F/Vd3JKc++F9+7dNfMEhN/fElxDqFrMbXeWtn/Xr -meIImb/2qCWt45/YjQGL8Do= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDdACYyKlFap22W +4gRJum+5ooEhiAOo7NChzrYONQIuDxzyHdgh7kOmSnErwxPAHoHspJMSwkTWoLwT +KOrmvLvzEXaf8UNwBoUw0xpcvYaaXGaNJ5F0Lb10i3DSvC/7LqUs68Z7dKg9nXGq +vChszwWJP0RES6RFame7AESHf0CNUuwzL0kkxoB66sctFVwXeBPs4hJSHWRuphDM +MeGoMgyvxbJc8NQqeFH79TV4SM0qH6yifZyOsLikgMppiYdRvJe9h+DzmvmAi12j +3Q1Fb9Y/9R6q7FrPXzPf8/Bn43qifeFif2dT+ubIuvXqOM19fE8zBOxMGclSgYit +XgPKYUz1AgMBAAECggEADVeHdsrYO31VH+FIOf0/5niZjCEue2HEnKgilIv9tDMk +X7eOh0nfmqfu2iH+TMBqvpMW7/B7gGuWvx9ewwxM2nlI7JH/rMEwBEkSU9v7RtFi +PY5QeS+Tuvf6GTbUPLlNrn8TbfuWdpXNOW3/kMYUwvrnT6ozYh9w4Li86mMzzfRB +YkzXq9Ex5Stmb7jPw1XiHyC+cM51V9dtZ8/TJWVi5XlUQjWgHT9keuoJp8470fru +J544z5Su2xWAeLzC0i208C0vZLN86ZtLWJkYhD83XUiHHRq+YrCvlSaaeG/1wj+u +mPnu3P7m0aSpCaVyuAmjPm4bNnUrqtRYkLR/NU/DgQKBgQD2iZXXx5uiHYAa63s3 +rvfNJkc6ehmSQTGPKSeCqCzjLamvJ7eEphzcFdDL53JkRoohYh4diUPOXvmPdobz +mnZ/3FIal64aGiwagor0V4ER6IXU12emH51/RdfaxI+8lfNUUMfYxVmSPlYDR4pg +F2u1gskOedWMN3g85TrE0vSqwQKBgQDle6Un8oMcU9JJ9qjEJuoaISqT7x7jyjbs +xUlECWZs12gWxzwzinio1VFxDJcnXNrdfJZ3bmhTRLJBLXyzjdRN6jIlnDdOjf1e +YJrI1iOXHRyBuL2EKcAzG7L3MToK1rYr13tUAUSW7HQ9DqPr9b+jSuKhg27F9C5Q +Ne+NqJWzNQKBgCY9Ht2yGySg+L60KY9wdwT92+xpBdBWhk5TLsqoNRYjff8p5OAR +N8a3J4SI6Ig/HKui4VLpeHfo6UJkOvhLy/d2/9EaF6n6xz5xYwYVEHLrot5pbq0o +mDAmcB2BgV3Z0D0Srnyj14nEW2j0zrSqzU0A9Rhms0WlUOP5Fg1zPvnBAoGAILjl +zvFssqBdLwDGBdpKrVknWhrRu8d813w2O0Zf3YtFo2Hbern3BJQOXeFeuFUsPELk +rbkHlUAJbvPOgUfrCwUnC2fgFwp2I3wA9jxarNSQ2Qp/s5XEe0Uq2sahMSR2q3+5 +bTwVDLRAyugIhb/wCJfIAyHbrMxpwjQ+qWNtnTUCgYEAuM0uyQX5MLi6X/DwzEFv +2ROJDaLekh1NJfj70J5RMsdRBltG0t12vsBDt2k/vHPoFQLZ0nNm8KBcNWnuRyGB +4yE5Dr8CJofXgmzbFdR9osIT9QsIAiG5GCwP2btn6HPBqcK2fv2IMkQNe1tJfPdN +6SGL/tnl/QRLRXbOwyrz5wM= -----END PRIVATE KEY----- diff --git a/services/nginz/integration-test/conf/nginz/integration-ca.pem b/services/nginz/integration-test/conf/nginz/integration-ca.pem index 304fc892245..a38ae3c9efa 100644 --- a/services/nginz/integration-test/conf/nginz/integration-ca.pem +++ b/services/nginz/integration-test/conf/nginz/integration-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIUQ35aUV70pJjvDTbfgFUj5YmchHQwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwNjE3MTMxNTMxWhcN -MzQwNjE1MTMxNTMxWjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJQlUOLNmd7Ll7iskcSnsv9xcx/+TnMw -qtqkK17w54/Kto+NJJAkD1L+X5EkSPZ7FDKqt2bGfoETWGnlpH/zsUTUpchlf6Jf -w6TJOejQer5FQNLCtQSnOIchlAFKzFxhGSvcOrRWiBAPjTVIkv9eiCNXcJ5PE9Sk -8+bmn2ztz7LVHcv46PmT/+ihRxKJ01T5CsXWPUHOZQRfGvKZmyGf+iTBuhcxMPYC -nXb7/M3rYCQXL8FQZiaqbIVMqNRpMBVkAqU3l2JnSrlNIjIh6Nqowjog8QYGuIz6 -fxwWkw6EU5ZBwHIr2rOakCnQoKeXVqBJdWZNRMX1Vtqeh7O9zDoW4/0CAwEAAaNT -MFEwHQYDVR0OBBYEFHNgZ4nZQoNKnb0AnDkefTXxxYDqMB8GA1UdIwQYMBaAFHNg -Z4nZQoNKnb0AnDkefTXxxYDqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBAIuLuyF7m1SP6PBu29jXnfGtaGi7j0jlqfcAysn7VmAU3StgWvSatlAl -AO6MIasjSQ+ygAbfIQW6W2Wc/U+NLQq5fRVi1cnmlxH5OULOFeQZCVyux8Maq0fT -jj4mmsz62b/iiA4tyS5r+foY4v1u2siSViBJSbfYbMp/VggIimt26RNV2u/ZV6Kf -UrOxazMx1yyuqARiqoA3VOMV8Byv8SEIiteWUSYni6u7xOT4gucPORhbM1HOSQ/S -CVq95x4FeKQnbEMykHI+bpBdkoadMVtrjCbskU49mOrvl/pli9V44R8KK6C1Nv3E -VLLcoOctdw90aT3sIjaXBcZtDTE6p6g= +MIIDEzCCAfugAwIBAgIUMtt4ZsS3KWgdBSet+D9ifnmmYiQwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwNzE4MDY0ODIwWhcN +MzQwNzE2MDY0ODIwWjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN0AJjIqUVqnbZbiBEm6b7migSGIA6js +0KHOtg41Ai4PHPId2CHuQ6ZKcSvDE8AegeykkxLCRNagvBMo6ua8u/MRdp/xQ3AG +hTDTGly9hppcZo0nkXQtvXSLcNK8L/supSzrxnt0qD2dcaq8KGzPBYk/RERLpEVq +Z7sARId/QI1S7DMvSSTGgHrqxy0VXBd4E+ziElIdZG6mEMwx4agyDK/Fslzw1Cp4 +Ufv1NXhIzSofrKJ9nI6wuKSAymmJh1G8l72H4POa+YCLXaPdDUVv1j/1HqrsWs9f +M9/z8GfjeqJ94WJ/Z1P65si69eo4zX18TzME7EwZyVKBiK1eA8phTPUCAwEAAaNT +MFEwHQYDVR0OBBYEFL8s7KTIH5P0tLQ9TdQ3bKAatD2DMB8GA1UdIwQYMBaAFL8s +7KTIH5P0tLQ9TdQ3bKAatD2DMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBANIYy7aAUp+UFfcxsPIcNCjAeYdQNi09wJ1cvg6ctU1GzRGinvr9PiCR +f13ZNX0SsK4farNR2UK9TTF1vv++sYiC6EgzJFWBqBW3XqjQMqF12rApf9faHvg9 +bcyWJNgQxuXc6ugxXrUI+Sj4U2LRnnEkh427/Hs1WbD4Bd0zfTHMCVmk6gvi2kcU +e3agZInTAIAwS59afC/6bGIaXb6QVyWlnEhWB2LJRIM0H7aXB2Ot63upin+yDZGU +Esz3c82RNPBzRaUGJezlaQq5ZGZyJjkkBWjYaSYO2RaR3h/PqhLIfshqC223SCCV +Uo2ofMIfAvXNA8hHK/u3f1WVTEJ4ERE= -----END CERTIFICATE----- diff --git a/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem b/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem index 1e7a83068de..28266b3e5b3 100644 --- a/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem +++ b/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCZjOHeUnlauuxD -WgrRnh3hj5Fs+uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47I -RgA5VLvGxI+T1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjG -QBmFF7NxrvjGgerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9K -zNQ7ZTlBQvJG8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzog -D+jgoAD5/9sk3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P -2jMpJ1xxAgMBAAECggEAS3NBjWgDP4T4EUROaqACWNKeB+nmkdt68T0gGtoNVD+D -EN9UPnpFQPdHFngAgWnzF858UIKzq1Pzdg+HjqRHPK1bS67tvua3xP1GHuR/CGPk -28T1hefqPHRen7GqHDAfdwarYBWCGv4Sjz/yCkcSIrtyfMBb5fAya5GO02pckUSK -19sl7XhkPtHJVirRkjQL29R2TCpkNNpQMjkuYLk7mox+6pNTbxgbk0cnT3eGj1pV -mlPqpwzC5GevRziE/VE/WXFLChY+8KB4fDLRqWnyvabDvQ4coaXgzwbdScJyM5hX -+Dxdfni/P2m7xAZXUyfBsr0VUzqUkJfK3WWvvAGTDQKBgQDNi3RUEjVnU/MN4aDz -iZB2VYGfo/K69xTPNEbLQWs1F4ZMpHVtUVXzTfx/xG9ug989ijEm6ncL9OsnhThn -UldSz2ojSJUxLmhgCHZGYHT72v/9rEqfT9JisWpIj44KXufUHCcl3Cozj1ae3EUp -NVhN1HphB2LsCIJvLYfLIGdBNwKBgQC/PhHQMm/MQe4pOHAbdzDrRZWdG2KSRVxp -9mmJ/aT8LOp7BDjq+Dkct6a56JGqlOTeJirMTTmCKiOiTInuB9S+K7kWJJiYg9g4 -UCiuMU+40Px/1Z4/uxRj3DSdGLXG7S6kPeADx9f9BUNpAytGqOnSnfbDiDVvQVbp -0N0+nIXDlwKBgQC2uZOXrXxGOE4pd/ySpCeF2yvZ1HDTnxWjwlBxHt4Em74rYkR2 -A0mKezjOCL4bHCaYWcKqWuOsAHYQcxEaYQv6NSOg7ESdLSlivgMPO26j+yN5yvGn -wNlCHYBjsyLNu2MSoFh5AsmNfo69uQnOwXqX7h1BJsTdGg+CcJJ4lHzWbwKBgQCD -/CRzGbwKrh3eGPNWIUaDuTxudy3qYTBMeSGReJpa5+zUBa/6imFwLldEyvttTOE/ -Z/v1j/52lPqO0mAHBSSQMsDERXGDIMsi4j+RKLsqhCEfYKCcv1JtMNam7RzXM24T -MBjgwxWPrAg/+03ssDrffuGFRQYLyH5hVCK9SW0P9QKBgQDJ1ZSto+RWxv/uOKNr -7FYeQoKpMb2IvNvnGlnYHC8KS9qRq6wUE+FtuKcdLBQP4M9Cgq71VD/dsawrhEw7 -1rAYk3OqmHxBOU5Dcb152NxYHEf53pfEfWc0x4AEVe+Jzynj2EYixRKNWwODNTEx -LKJOYd0CuWywxg6d9G7A7XbgWQ== +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQClrSse09wlL7P6 +pcsCNQ16CcrYS7+IF5yq9Nul0gnJsHl3XC1SKqNU1shMfcZYa4pEXF1LU414EWkL +/0WWVTs23U0fcUaybZJAG57jXD+571vEkoFCESJxBPLCtEOBUYbLx1IE2bZ9ybHZ +yD+W9XulQ7FgIS04tPgkttunl11CXdRAcMiL796/t8j0eVU049Pta420/hWYiVzv +fz74AyUCQwOlXhl8Io8HgO0NUNgWwu+3kI/2oswPZFcCXmSBTyht6CjSYNDtViVy +btdd+U7InM2XTKczXaweP1oBniWLxem1vRAltkYU0S89jnlcBtfoJHTUDKLBeNal +mys0ebrLAgMBAAECggEACxs0t3hUWwR7ouMtB2ovC8NO9Hj/efa7QJVG3t2EXR+W +Gkj08NEoPy5hdwnncKik4uLzjh0nxVNwIKcYLx+/kcn8EDjebbpSrOGCdpNfN5kI +JIFTEO69Hv50l6t7QFa1cUEHX96IZp5NbIq0A0FUQfFBGW9Kl3E/lpZ7hcdBL+BR +VrL96mTFqKtvYza/8wOOT05XCvVZH2Q/cN75Ih11t84UHtUCZWjAOoRhddcblo/2 +jCj91N2W7/Zut94o/2KcRY8Glf8Dps7gZUE1cB3PaZvHU5eEsCupwyNYwx785Pn1 +zzyCGklxeSu7t84WJh/B8dx6uYGiVRcd+ujc3BLKgQKBgQDerZ+ReVEF6cWYOKQD +6Hv2bv4PdME5LQkq1XoT4AQNRb2pXw2BrA/Crr7xNlOz2u6hN+LnmEFLfzMR/5UE +2UAS4tENTb4eibqgeBPVx3RksfpHWni2y8Dvdax3GgDwx9BVVMycOe3td3IlpnAN +rRU+jYRZuAZr5OQdQESu5bNUUQKBgQC+d+qB43WWtla47nFMEWM0RB6nHqbAo+dw +0BIaoxJyuC6SyE7P7APdBAlOw8P0Hy/Dzwe+ZzGnjXiXYyctTHss/zSLfhf+43/A +Z+Mzi5u0vzL60ING5UzBY9P/0XxrJym0eVr4YYPBvLBojwLhiDgQIXUHkgUO7IBY +UPjM+ggiWwKBgQC+zq3FvNulonxThG1ef+8A6ni/C7+qW6HYV1alAzbVnKX5JN7w +91wF6TDqhi/RFM+Xy8idxMRmiddcG9I4dmRGCp8xtCUuC7ykVmBAtglRY4RfcfGw +SQXI6t9eqySVLdKh2+j8EVOEQO7JvkWUInTqxd7b9ilieJ7TRcfUyjUREQKBgQCR +QWp6fDllItGoX0/QL0J0za6CzQFm0JjklAn6fnrHOmdqUZCpSNj5aOagRvPd7RrE +PdMuBgz8NwvMiDWMelNF0asE5rjuDhmTZqcC3Gl2wonidbpoCt8qbTN0WRKFtWw8 +0n/qBJQy3++5Dbeov/Xhd2KEz3tEEmEe+UGFMPmbGQKBgHcpn8SGrNuxjL/6vB1j +a5+LNOQAtr7akKlJbrl9B7poMVu8483fN4LG2HclWPPILnE3iPU/G559ZMo3QL6G +0+dImewlUSGtQ7GtLssai5wmmgLfXufiewHq/WoLDkd0xdu5RHc7GGgixx8XqpBG +ZTrVeb2nVeiwvIiZkuT13bGf -----END PRIVATE KEY----- diff --git a/services/nginz/integration-test/conf/nginz/integration-leaf.pem b/services/nginz/integration-test/conf/nginz/integration-leaf.pem index 635d332de70..9eca6a70899 100644 --- a/services/nginz/integration-test/conf/nginz/integration-leaf.pem +++ b/services/nginz/integration-test/conf/nginz/integration-leaf.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgIBADANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5jYS5l -eGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzFaFw0yNDA3MTcxMzE1MzFaMAAwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCZjOHeUnlauuxDWgrRnh3hj5Fs -+uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47IRgA5VLvGxI+T -1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjGQBmFF7NxrvjG -gerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9KzNQ7ZTlBQvJG -8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzogD+jgoAD5/9sk -3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P2jMpJ1xxAgMB +eGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjBaFw0zNDA3MTYwNjQ4MjBaMAAwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQClrSse09wlL7P6pcsCNQ16CcrY +S7+IF5yq9Nul0gnJsHl3XC1SKqNU1shMfcZYa4pEXF1LU414EWkL/0WWVTs23U0f +cUaybZJAG57jXD+571vEkoFCESJxBPLCtEOBUYbLx1IE2bZ9ybHZyD+W9XulQ7Fg +IS04tPgkttunl11CXdRAcMiL796/t8j0eVU049Pta420/hWYiVzvfz74AyUCQwOl +Xhl8Io8HgO0NUNgWwu+3kI/2oswPZFcCXmSBTyht6CjSYNDtViVybtdd+U7InM2X +TKczXaweP1oBniWLxem1vRAltkYU0S89jnlcBtfoJHTUDKLBeNalmys0ebrLAgMB AAGjgawwgakwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEgGA1UdEQEB /wQ+MDyCGSouaW50ZWdyYXRpb24uZXhhbXBsZS5jb22CFGhvc3QuZG9ja2VyLmlu -dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFPowAfmLPCmdCMdSxQjsR6UQSoyH -MB8GA1UdIwQYMBaAFHNgZ4nZQoNKnb0AnDkefTXxxYDqMA0GCSqGSIb3DQEBCwUA -A4IBAQCMJwbLzUsrkQkgdGKVi/Mb5XAAV0sfkwZch1Fx0vhJI072cZSow5A2ZUHa -LScFNTPmilPKEr6MS4xIKtRQaMHInbfxSsyNViKhpzkSOKoAiJjIJ2xPKFPnbTDI -uV74nxxyf9q/p3SLQfJFk7fxbvNeLqg5bYSrMeklHj4bpMJ9fybS8/mZVc8AkTFK -fsXSu9CW1B3GF+jP3E2GrFF3Zh9MgvWjMlSYg4ljPf5FoMCUq6GmQ17hQeJFvb5h -Jqk6TcgUrp082bcVlPW17XzFwVe3n6uzvWMtwI62EztVUj98+YkBiFL3i4+OQwAU -/noc22fq20OyJtCPJY4FIK7xUcgD +dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFIro61Yvf3swiRDOD/qOkwKJ0+Li +MB8GA1UdIwQYMBaAFL8s7KTIH5P0tLQ9TdQ3bKAatD2DMA0GCSqGSIb3DQEBCwUA +A4IBAQDNWgHWibMJvGI5YzkTlgXEvxjTTdYM6SpyLQFkju/PUuLP4KoiOvl2SY// +OWJH9v1XmZJ1DlnNRdgAHW+Uj8SpXJXRPkm1/5B9d0Eh8kfc+oiapZT7qfrKH5Ln +Wod5A/gzBv8rpaqHP8HP00b5SFdStTnqbQeBPXYMl+cbVwHBtZF3U6NVVJc0VOEe +MWx8bhJ6Vn8KmcLLoPPJVf4/u/toFAm7q61yZWISOMpZmLMbis0M+vJ6t57ImVTv +utffv1HhuCfEWQSc4XHI9JOMc3iexJYfgZQCIZdC7VxEJaOVB1DSkc3bYq/UaEyf +Wo6HKS47x295j7b1rbSO5ZCbhPYb -----END CERTIFICATE----- From 4787839503970c0260495efdeb9b3c09e8fece32 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Fri, 19 Jul 2024 09:33:38 +0200 Subject: [PATCH 013/136] Stop leaking ES error info. (#4153) --- changelog.d/3-bug-fixes/WPB-6865 | 1 + libs/wai-utilities/src/Network/Wai/Utilities/Server.hs | 7 +++---- services/brig/test/integration/API/TeamUserSearch.hs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-6865 diff --git a/changelog.d/3-bug-fixes/WPB-6865 b/changelog.d/3-bug-fixes/WPB-6865 new file mode 100644 index 00000000000..31b77de070c --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-6865 @@ -0,0 +1 @@ +Exclude exception message from error response diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs index 55cc81dfe49..9186ed78333 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs @@ -65,7 +65,6 @@ import Data.Metrics.GC (spawnGCMetricsCollector) import Data.Streaming.Zlib (ZlibException (..)) import Data.Text.Encoding qualified as Text import Data.Text.Encoding.Error (lenientDecode) -import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Encoding qualified as LT import Data.UUID qualified as UUID import Data.UUID.V4 qualified as UUID @@ -253,7 +252,7 @@ errorHandlers = ThreadKilled -> throwIO x _ -> pure . Left $ - Wai.mkError status500 "server-error" ("Server Error. " <> LT.pack (displayException x)), + Wai.mkError status500 "server-error" "Server Error", Handler $ \(_ :: InvalidRequest) -> pure . Left $ Wai.mkError status400 "client-error" "Invalid Request", @@ -267,9 +266,9 @@ errorHandlers = ZlibException _ -> pure . Left $ Wai.mkError status500 "server-error" "Server Error", - Handler $ \(e :: SomeException) -> + Handler $ \(_ :: SomeException) -> pure . Left $ - Wai.mkError status500 "server-error" ("Server Error. " <> LT.pack (displayException e)) + Wai.mkError status500 "server-error" "Server Error" ] {-# INLINE errorHandlers #-} diff --git a/services/brig/test/integration/API/TeamUserSearch.hs b/services/brig/test/integration/API/TeamUserSearch.hs index b70f59a4b17..f873b2f65d9 100644 --- a/services/brig/test/integration/API/TeamUserSearch.hs +++ b/services/brig/test/integration/API/TeamUserSearch.hs @@ -43,7 +43,7 @@ type TestConstraints m = (MonadFail m, MonadCatch m, MonadIO m, MonadHttp m) tests :: Opt.Opts -> Manager -> Galley -> Brig -> IO TestTree tests opts mgr _galley brig = do pure $ - testGroup "/teams/:tid/search" $ + testGroup "teams user search" $ [ testWithNewIndex "can find user by email" (testSearchByEmailSameTeam brig), testWithNewIndex "empty query returns the whole team sorted" (testEmptyQuerySorted brig), testWithNewIndex "sorting by some properties works" (testSort brig), From 8151faea233e5dd71494f5275be097df12befb11 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 22 Jul 2024 11:49:43 +0200 Subject: [PATCH 014/136] Test leaving one2one subconversations (#4160) * Test leaving one2one subconversations * Simplify websocket assertions in removal test * Take removal key from conversation backend Instead of using a hardcoded removal key, get the removal key from the correct backend using its public API. * Remove loading of public keys from configuration * Remove dead code --- integration/test/MLS/Util.hs | 16 ++-- integration/test/Test/MLS/SubConversation.hs | 40 +++++++++- integration/test/Testlib/Env.hs | 2 - integration/test/Testlib/Run.hs | 77 +------------------- integration/test/Testlib/RunServices.hs | 3 +- integration/test/Testlib/Types.hs | 3 - 6 files changed, 46 insertions(+), 95 deletions(-) diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index e8417123bad..2c8638d578a 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -233,29 +233,27 @@ resetGroup cid conv = do epoch = 0, newMembers = mempty } - resetClientGroup cid groupId + resetClientGroup cid groupId convId -resetClientGroup :: ClientIdentity -> String -> App () -resetClientGroup cid gid = do +resetClientGroup :: (MakesValue conv) => ClientIdentity -> String -> conv -> App () +resetClientGroup cid gid conv = do mls <- getMLSState - removalKeyPaths <- asks (.removalKeyPaths) - removalKeyPath <- - assertOne $ - Map.lookup (csSignatureScheme mls.ciphersuite) removalKeyPaths + keys <- withAPIVersion 5 $ getMLSPublicKeys conv >>= getJSON 200 + removalKey <- asByteString $ keys %. ("removal." <> csSignatureScheme mls.ciphersuite) void $ mlscli cid [ "group", "create", "--removal-key", - removalKeyPath, + "-", "--group-out", "", "--ciphersuite", mls.ciphersuite.code, gid ] - Nothing + (Just removalKey) keyPackageFile :: (HasCallStack) => ClientIdentity -> String -> App FilePath keyPackageFile cid ref = do diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 7cacebea70f..db13018017b 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -7,6 +7,7 @@ import qualified Data.Set as Set import MLS.Util import Notifications import SetupHelpers +import Test.MLS.One2One import Testlib.Prelude testJoinSubConv :: App () @@ -52,6 +53,38 @@ testJoinOne2OneSubConv = do $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle +testLeaveOne2OneSubConv :: One2OneScenario -> Leaver -> App () +testLeaveOne2OneSubConv scenario leaver = do + -- set up 1-1 conversation + 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] + conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetGroup alice1 conv + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + -- create and join subconversation + createSubConv alice1 "conference" + void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle + + -- one of the two clients leaves + let (leaverClient, leaverIndex, otherClient) = case leaver of + Alice -> (alice1, 0, bob1) + Bob -> (bob1, 1, alice1) + + withWebSocket otherClient $ \ws -> do + leaveCurrentConv leaverClient + + msg <- consumeMessage otherClient 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 otherClient >>= sendAndConsumeCommitBundle + testDeleteParentOfSubConv :: (HasCallStack) => Domain -> App () testDeleteParentOfSubConv secondDomain = do (alice, tid, _) <- createTeam OwnDomain 1 @@ -227,7 +260,7 @@ testCreatorRemovesUserFromParent = do setMLSState childState let idxBob1 :: Int = 1 idxBob2 :: Int = 2 - for_ ((,) <$> [idxBob1, idxBob2] <*> [alice1, charlie1, charlie2] `zip` wss) \(idx, (consumer, ws)) -> do + for_ ((,) <$> [idxBob1, idxBob2] <*> wss) \(idx, ws) -> do msg <- awaitMatch do @@ -244,9 +277,8 @@ testCreatorRemovesUserFromParent = do lift do (== idx) <$> (prop %. "Remove.removed" & asInt) ws - msg %. "payload.0.data" - & asByteString - >>= mlsCliConsume consumer + 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]} diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 42ae9ea25f3..08ddcb5d965 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -108,7 +108,6 @@ mkGlobalEnv cfgFile = do gDefaultAPIVersion = 6, gManager = manager, gServicesCwdBase = devEnvProjectRoot <&> ( "services"), - gRemovalKeyPaths = mempty, gBackendResourcePool = resourcePool, gRabbitMQConfig = intConfig.rabbitmq, gTempDir = tempDir, @@ -154,7 +153,6 @@ mkEnv ge = do ], manager = gManager ge, servicesCwdBase = gServicesCwdBase ge, - removalKeyPaths = gRemovalKeyPaths ge, prekeys = pks, lastPrekeys = lpks, mls = mls, diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 6500b6f71e6..d5385a16376 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -1,34 +1,22 @@ -module Testlib.Run (main, mainI, createGlobalEnv) where +module Testlib.Run (main, mainI) where import Control.Concurrent import Control.Exception as E import Control.Monad import Control.Monad.Codensity import Control.Monad.IO.Class -import Control.Monad.Reader -import Crypto.Error -import qualified Crypto.PubKey.Ed25519 as Ed25519 -import Data.Aeson (Value) -import Data.ByteArray (convert) -import Data.ByteString (ByteString) -import qualified Data.ByteString as B import Data.Foldable import Data.Function import Data.Functor import Data.List -import qualified Data.Map as Map -import Data.PEM import Data.Time.Clock -import Data.Traversable (for) import RunAllTests import System.Directory import System.Environment import System.Exit import System.FilePath -import Testlib.App import Testlib.Assertions import Testlib.Env -import Testlib.JSON import Testlib.Options import Testlib.Printing import Testlib.Types @@ -112,67 +100,6 @@ main = do if opts.listTests then doListTests tests else runTests tests opts.xmlReport cfg -createGlobalEnv :: FilePath -> Codensity IO GlobalEnv -createGlobalEnv cfg = do - genv0 <- mkGlobalEnv cfg - -- Run codensity locally here, because we only need the environment to get at - -- Galley's configuration. Accessing the environment has the side effect of - -- creating a temporary mls directory, which we don't need here. - - let removalKeysDir = gTempDir genv0 "removal-keys" - keys <- liftIO . lowerCodensity $ do - env <- mkEnv genv0 - liftIO $ createDirectoryIfMissing True removalKeysDir - liftIO . runAppWithEnv env $ do - config <- readServiceConfig Galley - for - [ ("ed25519", loadEd25519Key), - ("ecdsa_secp256r1_sha256", loadEcKey "ecdsa_secp256r1_sha256" 73), - ("ecdsa_secp384r1_sha384", loadEcKey "ecdsa_secp384r1_sha384" 88), - ("ecdsa_secp521r1_sha512", loadEcKey "ecdsa_secp521r1_sha512" 108) - ] - $ \(sigScheme, load) -> do - key <- load config - let path = removalKeysDir (sigScheme <> ".key") - liftIO $ B.writeFile path key - pure (sigScheme, path) - - -- save removal key to a temporary file - pure genv0 {gRemovalKeyPaths = Map.fromList keys} - -getPrivateKeyPath :: Value -> String -> App FilePath -getPrivateKeyPath config signatureScheme = do - relPath <- config %. "settings.mlsPrivateKeyPaths.removal" %. signatureScheme & asString - asks \env' -> case env'.servicesCwdBase of - Nothing -> relPath - Just dir -> dir "galley" relPath - -loadEcKey :: String -> Int -> Value -> App ByteString -loadEcKey sigScheme offset config = do - path <- getPrivateKeyPath config sigScheme - bs <- liftIO $ B.readFile path - pems <- case pemParseBS bs of - Left err -> assertFailure $ "Could not parse removal key PEM: " <> err - Right x -> pure x - asn1 <- pemContent <$> assertOne pems - -- quick and dirty ASN.1 decoding: assume the key is of the correct - -- format, and simply skip the header - pure $ B.drop offset asn1 - -loadEd25519Key :: Value -> App ByteString -loadEd25519Key config = do - path <- getPrivateKeyPath config "ed25519" - bs <- liftIO $ B.readFile path - pems <- case pemParseBS bs of - Left err -> assertFailure $ "Could not parse removal key PEM: " <> err - Right x -> pure x - asn1 <- pemContent <$> assertOne pems - -- quick and dirty ASN.1 decoding: assume the key is of the correct - -- format, and simply skip the 16 byte header - let bytes = B.drop 16 asn1 - priv <- liftIO . throwCryptoErrorIO $ Ed25519.secretKey bytes - pure (convert (Ed25519.toPublic priv)) - runTests :: [(String, x, y, App ())] -> Maybe FilePath -> FilePath -> IO () runTests tests mXMLOutput cfg = do output <- newChan @@ -182,7 +109,7 @@ runTests tests mXMLOutput cfg = do Nothing -> pure () let writeOutput = writeChan output . Just - runCodensity (createGlobalEnv cfg) $ \genv -> + runCodensity (mkGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do -- Currently 4 seems to be stable, more seems to create more timeouts. report <- fmap mconcat $ pooledForConcurrentlyN 4 tests $ \(qname, _, _, action) -> do diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index e5c5c7611ce..e4641a21983 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -10,7 +10,6 @@ import System.Posix (getWorkingDirectory) import System.Process import Testlib.Prelude import Testlib.ResourcePool -import Testlib.Run (createGlobalEnv) parentDir :: FilePath -> Maybe FilePath parentDir path = @@ -52,7 +51,7 @@ main = do (_, _, _, ph) <- createProcess cp exitWith =<< waitForProcess ph - runCodensity (createGlobalEnv cfg >>= mkEnv) $ \env -> + runCodensity (mkGlobalEnv cfg >>= mkEnv) $ \env -> runAppWithEnv env $ lowerCodensity $ do diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 2ebec043a86..5ba37b377b3 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -110,7 +110,6 @@ data GlobalEnv = GlobalEnv gDefaultAPIVersion :: Int, gManager :: HTTP.Manager, gServicesCwdBase :: Maybe FilePath, - gRemovalKeyPaths :: Map String FilePath, gBackendResourcePool :: ResourcePool BackendResource, gRabbitMQConfig :: RabbitMQConfig, gTempDir :: FilePath, @@ -210,8 +209,6 @@ data Env = Env apiVersionByDomain :: Map String Int, manager :: HTTP.Manager, servicesCwdBase :: Maybe FilePath, - -- | paths to removal keys by signature scheme - removalKeyPaths :: Map String FilePath, prekeys :: IORef [(Int, String)], lastPrekeys :: IORef [String], mls :: IORef MLSState, From a9e8c5fda5966d034545cee10780bbab7e6a9112 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 24 Jul 2024 10:51:18 +0200 Subject: [PATCH 015/136] Introduce PropertySubsystem (#4148) * Migrate integration tests for user properties to the new suite * AsciiText: Write correct instance for FromHttpApiData * AsciiText: Write correct instance for FromJSONKey * Allow setting existing properties even if we have max properties * Rename UserEvents -> Events, also support PropertyEvent * Introduce PropertiesSubsystem --- changelog.d/3-bug-fixes/ascii-text-parsing | 1 + changelog.d/3-bug-fixes/max-properties | 1 + changelog.d/5-internal/property-subsystem | 1 + integration/integration.cabal | 1 + integration/test/API/Brig.hs | 30 ++ integration/test/API/Common.hs | 25 ++ integration/test/Test/Property.hs | 143 ++++++++++ libs/types-common/src/Data/Text/Ascii.hs | 10 +- libs/wire-api/src/Wire/API/Error/Brig.hs | 9 + libs/wire-api/src/Wire/API/Properties.hs | 19 +- libs/wire-subsystems/default.nix | 2 + libs/wire-subsystems/src/Wire/Events.hs | 14 + .../wire-subsystems/src/Wire/PropertyStore.hs | 19 ++ .../src/Wire/PropertyStore/Cassandra.hs | 78 ++++++ .../src/Wire/PropertySubsystem.hs | 42 +++ .../src/Wire/PropertySubsystem/Interpreter.hs | 151 ++++++++++ libs/wire-subsystems/src/Wire/UserEvents.hs | 13 - .../src/Wire/UserSubsystem/Interpreter.hs | 10 +- .../test/unit/Wire/MiniBackend.hs | 4 +- .../test/unit/Wire/MockInterpreters.hs | 3 +- .../test/unit/Wire/MockInterpreters/Events.hs | 22 ++ .../Wire/MockInterpreters/PropertyStore.hs | 19 ++ .../unit/Wire/MockInterpreters/UserEvents.hs | 20 -- .../Wire/PropertySubsystem/InterpreterSpec.hs | 265 ++++++++++++++++++ .../Wire/UserSubsystem/InterpreterSpec.hs | 7 +- libs/wire-subsystems/wire-subsystems.cabal | 11 +- services/brig/brig.cabal | 3 - services/brig/src/Brig/API/Error.hs | 12 - services/brig/src/Brig/API/Internal.hs | 10 +- services/brig/src/Brig/API/Properties.hs | 54 ---- services/brig/src/Brig/API/Public.hs | 100 ++----- services/brig/src/Brig/API/Types.hs | 2 - services/brig/src/Brig/API/User.hs | 18 +- .../brig/src/Brig/CanonicalInterpreter.hs | 24 +- services/brig/src/Brig/Data/Properties.hs | 95 ------- services/brig/src/Brig/IO/Intra.hs | 11 +- .../brig/src/Brig/InternalEvent/Process.hs | 4 +- services/brig/test/integration/API/User.hs | 2 - .../test/integration/API/User/Property.hs | 170 ----------- 39 files changed, 933 insertions(+), 492 deletions(-) create mode 100644 changelog.d/3-bug-fixes/ascii-text-parsing create mode 100644 changelog.d/3-bug-fixes/max-properties create mode 100644 changelog.d/5-internal/property-subsystem create mode 100644 integration/test/Test/Property.hs create mode 100644 libs/wire-subsystems/src/Wire/Events.hs create mode 100644 libs/wire-subsystems/src/Wire/PropertyStore.hs create mode 100644 libs/wire-subsystems/src/Wire/PropertyStore/Cassandra.hs create mode 100644 libs/wire-subsystems/src/Wire/PropertySubsystem.hs create mode 100644 libs/wire-subsystems/src/Wire/PropertySubsystem/Interpreter.hs delete mode 100644 libs/wire-subsystems/src/Wire/UserEvents.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/Events.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/PropertyStore.hs delete mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserEvents.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/PropertySubsystem/InterpreterSpec.hs delete mode 100644 services/brig/src/Brig/API/Properties.hs delete mode 100644 services/brig/src/Brig/Data/Properties.hs delete mode 100644 services/brig/test/integration/API/User/Property.hs diff --git a/changelog.d/3-bug-fixes/ascii-text-parsing b/changelog.d/3-bug-fixes/ascii-text-parsing new file mode 100644 index 00000000000..6472aa949f2 --- /dev/null +++ b/changelog.d/3-bug-fixes/ascii-text-parsing @@ -0,0 +1 @@ +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/max-properties b/changelog.d/3-bug-fixes/max-properties new file mode 100644 index 00000000000..4273020c7e2 --- /dev/null +++ b/changelog.d/3-bug-fixes/max-properties @@ -0,0 +1 @@ +Allow setting existing properties even if we have max properties \ No newline at end of file diff --git a/changelog.d/5-internal/property-subsystem b/changelog.d/5-internal/property-subsystem new file mode 100644 index 00000000000..6ef618ff81e --- /dev/null +++ b/changelog.d/5-internal/property-subsystem @@ -0,0 +1 @@ +Introduce proeprty subsytem \ No newline at end of file diff --git a/integration/integration.cabal b/integration/integration.cabal index 75e68530583..9a212d87b2a 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -139,6 +139,7 @@ library Test.MLS.Unreachable Test.Notifications Test.Presence + Test.Property Test.Provider Test.PushToken Test.Roles diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index b0d3bbbc4c4..36d6527ae9d 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -675,3 +675,33 @@ addBot user providerId serviceId convId = do req & zType "access" & addJSONObject ["provider" .= providerId, "service" .= serviceId] + +setProperty :: (MakesValue user, ToJSON val) => user -> String -> val -> App Response +setProperty user propName val = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties", propName] + submit "PUT" $ req & addJSON val + +getProperty :: (MakesValue user) => user -> String -> App Response +getProperty user propName = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties", propName] + submit "GET" req + +deleteProperty :: (MakesValue user) => user -> String -> App Response +deleteProperty user propName = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties", propName] + submit "DELETE" req + +getAllPropertyNames :: (MakesValue user) => user -> App Response +getAllPropertyNames user = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties"] + submit "GET" req + +getAllPropertyValues :: (MakesValue user) => user -> App Response +getAllPropertyValues user = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties-values"] + submit "GET" req + +clearProperties :: (MakesValue user) => user -> App Response +clearProperties user = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["properties"] + submit "DELETE" req diff --git a/integration/test/API/Common.hs b/integration/test/API/Common.hs index c07816cc5b4..12f4a3866ab 100644 --- a/integration/test/API/Common.hs +++ b/integration/test/API/Common.hs @@ -5,6 +5,8 @@ import Control.Monad.IO.Class import Data.Array ((!)) import qualified Data.Array as Array import qualified Data.ByteString as BS +import Data.Scientific (scientific) +import qualified Data.Vector as Vector import System.Random (randomIO, randomRIO) import Testlib.Prelude @@ -47,6 +49,29 @@ randomHandleWithRange min' max' = liftIO $ do randomBytes :: Int -> App ByteString randomBytes n = liftIO $ BS.pack <$> replicateM n randomIO +randomString :: Int -> App String +randomString n = liftIO $ replicateM n randomIO + +randomJSON :: App Value +randomJSON = do + let maxThings = 5 + liftIO (randomRIO (0 :: Int, 5)) >>= \case + 0 -> String . fromString <$> (randomString =<< randomRIO (0, maxThings)) + 1 -> Number <$> liftIO (scientific <$> randomIO <*> randomIO) + 2 -> Bool <$> liftIO randomIO + 3 -> pure Null + 4 -> do + n <- liftIO $ randomRIO (0, maxThings) + Array . Vector.fromList <$> replicateM n randomJSON + 5 -> do + n <- liftIO $ randomRIO (0, maxThings) + keys <- do + keyLength <- randomRIO (0, maxThings) + replicateM n (randomString keyLength) + vals <- replicateM n randomJSON + pure . object $ zipWith (.=) keys vals + _ -> error $ "impopssible: randomJSON" + randomHex :: Int -> App String randomHex n = liftIO $ replicateM n pick where diff --git a/integration/test/Test/Property.hs b/integration/test/Test/Property.hs new file mode 100644 index 00000000000..4440c6f8983 --- /dev/null +++ b/integration/test/Test/Property.hs @@ -0,0 +1,143 @@ +module Test.Property where + +import API.Brig +import API.Common +import qualified Data.Map as Map +import SetupHelpers +import Testlib.Prelude + +testSetGetDeleteProperty :: App () +testSetGetDeleteProperty = do + user <- randomUser OwnDomain def + setProperty user "foo" "bar" `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + getProperty user "foo" `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` toJSON "bar" + + deleteProperty user "foo" `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + + getProperty user "foo" `bindResponse` \resp -> do + resp.status `shouldMatchInt` 404 + +testGetProperties :: App () +testGetProperties = do + user <- randomUser OwnDomain def + -- Property names can only be printable ascii, using the handle function here + -- as a little shortcut. + propertyNames <- replicateM 16 $ randomHandleWithRange 8 20 + propertyVals <- replicateM 16 $ randomJSON + let properties = zip propertyNames propertyVals + forM_ properties $ \(prop, val) -> + setProperty user prop val `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + getAllPropertyNames user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatchSet` propertyNames + + getAllPropertyValues user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` Map.fromList properties + +testClearProperties :: App () +testClearProperties = do + user <- randomUser OwnDomain def + + propertyNames <- replicateM 16 $ randomHandleWithRange 8 20 + propertyVals <- replicateM 16 $ randomJSON + let properties = zip propertyNames propertyVals + forM_ properties $ \(prop, val) -> + setProperty user prop val `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + clearProperties user `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + getAllPropertyNames user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatchSet` mempty @[String] + + getAllPropertyValues user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` Map.empty @String @Value + +testMaxProperties :: App () +testMaxProperties = do + user <- randomUser OwnDomain def + + -- This is hardcoded in the prod code. + let maxProperties = 16 + + propertyNames <- replicateM maxProperties $ randomHandleWithRange 8 20 + propertyVals <- replicateM maxProperties $ randomJSON + let properties = zip propertyNames propertyVals + forM_ properties $ \(prop, val) -> + setProperty user prop val `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + seventeenthPropName <- randomHandleWithRange 8 20 + seventeenthPropVal <- randomJSON + + -- cannot set seventeenth property + setProperty user seventeenthPropName seventeenthPropVal `bindResponse` \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "too-many-properties" + + -- Old properties are maintained + getAllPropertyValues user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` Map.fromList properties + + -- Can still update the old properties + newPropertyVals <- replicateM 16 $ randomJSON + let newProperties = zip propertyNames newPropertyVals + forM_ newProperties $ \(prop, val) -> + setProperty user prop val `bindResponse` \resp -> + resp.status `shouldMatchInt` 200 + + getAllPropertyValues user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` Map.fromList newProperties + +testPropertyNameNotAscii :: App () +testPropertyNameNotAscii = do + user <- randomUser OwnDomain def + setProperty user "döner" "yes" `bindResponse` \resp -> + resp.status `shouldMatchInt` 400 + +testMaxLength :: App () +testMaxLength = do + user <- randomUser OwnDomain def + + maxKeyLength <- asInt $ readServiceConfig Brig %. "optSettings.setPropertyMaxKeyLen" + maxValLength <- asInt $ readServiceConfig Brig %. "optSettings.setPropertyMaxValueLen" + + tooLongProperty <- randomHandleWithRange (maxKeyLength + 1) (maxKeyLength + 1) + acceptableProperty <- randomHandleWithRange maxKeyLength maxKeyLength + + -- Two chars are taken by the quotes for string values. + -- + -- We use the `randomHandleWithRange` function because having non-ascii + -- characters or unprintable characters will increase the length of the JSON. + tooLongValue <- randomHandleWithRange (maxValLength - 1) (maxValLength - 1) + acceptableValue <- randomHandleWithRange (maxValLength - 2) (maxValLength - 2) + + putStrLn $ "acceptableValue= " <> acceptableValue + + setProperty user tooLongProperty acceptableValue `bindResponse` \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "property-key-too-large" + + setProperty user acceptableProperty tooLongValue `bindResponse` \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "property-value-too-large" + + setProperty user acceptableProperty acceptableValue `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + + getProperty user acceptableProperty `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` toJSON acceptableValue diff --git a/libs/types-common/src/Data/Text/Ascii.hs b/libs/types-common/src/Data/Text/Ascii.hs index aed072030c0..7f167d611cd 100644 --- a/libs/types-common/src/Data/Text/Ascii.hs +++ b/libs/types-common/src/Data/Text/Ascii.hs @@ -80,6 +80,7 @@ where import Cassandra hiding (Ascii) import Data.Aeson (FromJSON (..), FromJSONKey, ToJSON (..), ToJSONKey) import Data.Attoparsec.ByteString (Parser) +import Data.Bifunctor (first) import Data.ByteString.Base16 qualified as B16 import Data.ByteString.Base64 qualified as B64 import Data.ByteString.Base64.URL qualified as B64Url @@ -104,11 +105,9 @@ newtype AsciiText c = AsciiText {toText :: Text} Monoid, NFData, ToByteString, - FromJSONKey, ToJSONKey, Hashable, - ToHttpApiData, - FromHttpApiData + ToHttpApiData ) newtype AsciiChar c = AsciiChar {toChar :: Char} @@ -141,6 +140,9 @@ class AsciiChars c where instance (AsciiChars c) => FromByteString (AsciiText c) where parser = parseBytes validate +instance (AsciiChars c) => FromHttpApiData (AsciiText c) where + parseUrlPiece = first Text.pack . validate + -- | Note: 'fromString' is a partial function that will 'error' when given -- a string containing characters not in the set @c@. It is only intended to be used -- via the @OverloadedStrings@ extension, i.e. for known ASCII string literals. @@ -156,6 +158,8 @@ instance (AsciiChars c) => ToJSON (AsciiText c) where instance (AsciiChars c) => FromJSON (AsciiText c) where parseJSON = schemaParseJSON +instance (FromJSON (AsciiText c)) => FromJSONKey (AsciiText c) + instance (Typeable c, AsciiChars c) => S.ToSchema (AsciiText c) where declareNamedSchema = schemaToSwagger diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index e84846c1620..8399afaf1a5 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -94,6 +94,9 @@ data BrigError | ProviderNotFound | TeamsNotFederating | PasswordIsStale + | TooManyProperties + | PropertyKeyTooLarge + | PropertyValueTooLarge instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: BrigError) where addToOpenApi = addStaticErrorToSwagger @(MapError e) @@ -282,3 +285,9 @@ type instance MapError 'ConflictingInvitations = 'StaticError 409 "conflicting-i type instance MapError 'TeamsNotFederating = 'StaticError 403 "team-not-federating" "The target user is owned by a federated backend, but is not in an allow-listed team" type instance MapError 'PasswordIsStale = 'StaticError 403 "password-is-stale" "The password is too old, please update your password." + +type instance MapError 'TooManyProperties = 'StaticError 403 "too-many-properties" "Too many properties" + +type instance MapError 'PropertyKeyTooLarge = 'StaticError 403 "property-key-too-large" "The property key is too large." + +type instance MapError 'PropertyValueTooLarge = 'StaticError 403 "property-value-too-large" "The property value is too large" diff --git a/libs/wire-api/src/Wire/API/Properties.hs b/libs/wire-api/src/Wire/API/Properties.hs index 83c8ee1aa50..67a3b5b554c 100644 --- a/libs/wire-api/src/Wire/API/Properties.hs +++ b/libs/wire-api/src/Wire/API/Properties.hs @@ -21,7 +21,6 @@ module Wire.API.Properties ( PropertyKeysAndValues (..), PropertyKey (..), RawPropertyValue (..), - PropertyValue (..), ) where @@ -35,9 +34,10 @@ import Data.OpenApi qualified as S import Data.Text.Ascii import Imports import Servant -import Wire.Arbitrary (Arbitrary) +import Test.QuickCheck -newtype PropertyKeysAndValues = PropertyKeysAndValues (Map PropertyKey PropertyValue) +newtype PropertyKeysAndValues = PropertyKeysAndValues (Map PropertyKey Value) + deriving stock (Eq, Show) deriving newtype (ToJSON) instance S.ToSchema PropertyKeysAndValues where @@ -72,6 +72,7 @@ deriving instance C.Cql PropertyKey -- | A raw, unparsed property value. newtype RawPropertyValue = RawPropertyValue {rawPropertyBytes :: LByteString} + deriving (Eq, Show) instance C.Cql RawPropertyValue where ctype = C.Tagged C.BlobColumn @@ -89,15 +90,3 @@ instance S.ToSchema RawPropertyValue where declareNamedSchema _ = pure . S.NamedSchema (Just "PropertyValue") $ mempty & S.description ?~ "An arbitrary JSON value for a property" - --- | A property value together with its original serialisation. -data PropertyValue = PropertyValue - { propertyRaw :: RawPropertyValue, - propertyValue :: Value - } - -instance ToJSON PropertyValue where - toJSON = propertyValue - -instance Show PropertyValue where - show = show . propertyValue diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index ab5d6d19a48..890275b857d 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -56,6 +56,7 @@ , resource-pool , resourcet , retry +, scientific , servant , servant-client-core , stomp-queue @@ -174,6 +175,7 @@ mkDerivation { QuickCheck quickcheck-instances random + scientific servant-client-core streaming-commons string-conversions diff --git a/libs/wire-subsystems/src/Wire/Events.hs b/libs/wire-subsystems/src/Wire/Events.hs new file mode 100644 index 00000000000..57a48e4ac72 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/Events.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.Events where + +import Data.Id +import Imports +import Polysemy +import Wire.API.UserEvent + +data Events m a where + GenerateUserEvent :: UserId -> Maybe ConnId -> UserEvent -> Events m () + GeneratePropertyEvent :: UserId -> ConnId -> PropertyEvent -> Events m () + +makeSem ''Events diff --git a/libs/wire-subsystems/src/Wire/PropertyStore.hs b/libs/wire-subsystems/src/Wire/PropertyStore.hs new file mode 100644 index 00000000000..77e255581ca --- /dev/null +++ b/libs/wire-subsystems/src/Wire/PropertyStore.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.PropertyStore where + +import Data.Id +import Imports +import Polysemy +import Wire.API.Properties + +data PropertyStore m a where + InsertProperty :: UserId -> PropertyKey -> RawPropertyValue -> PropertyStore m () + LookupProperty :: UserId -> PropertyKey -> PropertyStore m (Maybe RawPropertyValue) + CountProperties :: UserId -> PropertyStore m Int + DeleteProperty :: UserId -> PropertyKey -> PropertyStore m () + ClearProperties :: UserId -> PropertyStore m () + GetPropertyKeys :: UserId -> PropertyStore m [PropertyKey] + GetAllProperties :: UserId -> PropertyStore m [(PropertyKey, RawPropertyValue)] + +makeSem ''PropertyStore diff --git a/libs/wire-subsystems/src/Wire/PropertyStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/PropertyStore/Cassandra.hs new file mode 100644 index 00000000000..f5a7189466e --- /dev/null +++ b/libs/wire-subsystems/src/Wire/PropertyStore/Cassandra.hs @@ -0,0 +1,78 @@ +module Wire.PropertyStore.Cassandra where + +import Cassandra +import Data.Id +import Imports +import Polysemy +import Polysemy.Embed +import Wire.API.Properties +import Wire.PropertyStore + +interpretPropertyStoreCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor PropertyStore r +interpretPropertyStoreCassandra casClient = + interpret $ + runEmbedded (runClient @IO casClient) . embed . \case + InsertProperty u k v -> insertPropertyImpl u k v + LookupProperty u k -> lookupPropertyImpl u k + CountProperties u -> countPropertiesImpl u + DeleteProperty u k -> deletePropertyImpl u k + ClearProperties u -> clearPropertieImpl u + GetPropertyKeys u -> lookupPropertyKeyImpl u + GetAllProperties u -> getAllPropertiesImpl u + +insertPropertyImpl :: + (MonadClient m) => + UserId -> + PropertyKey -> + RawPropertyValue -> + m () +insertPropertyImpl u k v = + retry x5 $ write propertyInsert (params LocalQuorum (u, k, v)) + +deletePropertyImpl :: (MonadClient m) => UserId -> PropertyKey -> m () +deletePropertyImpl u k = retry x5 $ write propertyDelete (params LocalQuorum (u, k)) + +clearPropertieImpl :: (MonadClient m) => UserId -> m () +clearPropertieImpl u = retry x5 $ write propertyReset (params LocalQuorum (Identity u)) + +lookupPropertyImpl :: (MonadClient m) => UserId -> PropertyKey -> m (Maybe RawPropertyValue) +lookupPropertyImpl u k = + fmap runIdentity + <$> retry x1 (query1 propertySelect (params LocalQuorum (u, k))) + +lookupPropertyKeyImpl :: (MonadClient m) => UserId -> m [PropertyKey] +lookupPropertyKeyImpl u = + map runIdentity + <$> retry x1 (query propertyKeysSelect (params LocalQuorum (Identity u))) + +countPropertiesImpl :: (MonadClient m) => UserId -> m Int +countPropertiesImpl u = do + maybe 0 fromIntegral <$> retry x1 (query1 propertyCount (params LocalQuorum (Identity u))) + +getAllPropertiesImpl :: (MonadClient m) => UserId -> m [(PropertyKey, RawPropertyValue)] +getAllPropertiesImpl u = + retry x1 (query propertyKeysValuesSelect (params LocalQuorum (Identity u))) + +------------------------------------------------------------------------------- +-- Queries + +propertyInsert :: PrepQuery W (UserId, PropertyKey, RawPropertyValue) () +propertyInsert = "INSERT INTO properties (user, key, value) VALUES (?, ?, ?)" + +propertyDelete :: PrepQuery W (UserId, PropertyKey) () +propertyDelete = "DELETE FROM properties where user = ? and key = ?" + +propertyReset :: PrepQuery W (Identity UserId) () +propertyReset = "DELETE FROM properties where user = ?" + +propertySelect :: PrepQuery R (UserId, PropertyKey) (Identity RawPropertyValue) +propertySelect = "SELECT value FROM properties where user = ? and key = ?" + +propertyKeysSelect :: PrepQuery R (Identity UserId) (Identity PropertyKey) +propertyKeysSelect = "SELECT key FROM properties where user = ?" + +propertyKeysValuesSelect :: PrepQuery R (Identity UserId) (PropertyKey, RawPropertyValue) +propertyKeysValuesSelect = "SELECT key, value FROM properties where user = ?" + +propertyCount :: PrepQuery R (Identity UserId) (Identity Int64) +propertyCount = "SELECT COUNT(*) FROM properties where user = ?" diff --git a/libs/wire-subsystems/src/Wire/PropertySubsystem.hs b/libs/wire-subsystems/src/Wire/PropertySubsystem.hs new file mode 100644 index 00000000000..2a8bef98df2 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/PropertySubsystem.hs @@ -0,0 +1,42 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.PropertySubsystem where + +import Data.Id +import Data.Text.Lazy qualified as LText +import Imports +import Network.HTTP.Types +import Network.Wai.Utilities qualified as Wai +import Polysemy +import Wire.API.Error +import Wire.API.Error.Brig qualified as E +import Wire.API.Properties +import Wire.Error + +data PropertySubsystemError + = TooManyProperties + | PropertyKeyTooLarge + | PropertyValueTooLarge + | PropertyValueInvalid String + | StoredPropertyValueInvalid + deriving (Show, Eq) + +propertySubsystemErrorToHttpError :: PropertySubsystemError -> HttpError +propertySubsystemErrorToHttpError = + StdError . \case + TooManyProperties -> errorToWai @E.TooManyProperties + PropertyKeyTooLarge -> errorToWai @E.PropertyKeyTooLarge + PropertyValueTooLarge -> errorToWai @E.PropertyValueTooLarge + PropertyValueInvalid err -> Wai.mkError status400 "bad-request" (LText.pack err) + StoredPropertyValueInvalid -> Wai.mkError status500 "internal-server-error" "Internal Server Error" + +data PropertySubsystem m a where + SetProperty :: UserId -> ConnId -> PropertyKey -> RawPropertyValue -> PropertySubsystem m () + DeleteProperty :: UserId -> ConnId -> PropertyKey -> PropertySubsystem m () + ClearProperties :: UserId -> ConnId -> PropertySubsystem m () + OnUserDeleted :: UserId -> PropertySubsystem m () + LookupProperty :: UserId -> PropertyKey -> PropertySubsystem m (Maybe RawPropertyValue) + GetPropertyKeys :: UserId -> PropertySubsystem m [PropertyKey] + GetAllProperties :: UserId -> PropertySubsystem m PropertyKeysAndValues + +makeSem ''PropertySubsystem diff --git a/libs/wire-subsystems/src/Wire/PropertySubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/PropertySubsystem/Interpreter.hs new file mode 100644 index 00000000000..5c347928dfb --- /dev/null +++ b/libs/wire-subsystems/src/Wire/PropertySubsystem/Interpreter.hs @@ -0,0 +1,151 @@ +module Wire.PropertySubsystem.Interpreter where + +import Data.Aeson (Value) +import Data.Aeson qualified as Aeson +import Data.ByteString.Lazy qualified as LBS +import Data.Id +import Data.Map qualified as Map +import Data.Text qualified as Text +import Data.Text.Ascii qualified as Ascii +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.Input +import Polysemy.TinyLog (TinyLog) +import Polysemy.TinyLog qualified as Log +import System.Logger.Message qualified as Log +import Wire.API.Properties +import Wire.API.UserEvent +import Wire.Events +import Wire.PropertyStore (PropertyStore) +import Wire.PropertyStore qualified as PropertyStore +import Wire.PropertySubsystem + +data PropertySubsystemConfig = PropertySubsystemConfig + { maxKeyLength :: Int64, + maxValueLength :: Int64, + maxProperties :: Int + } + +interpretPropertySubsystem :: + ( Member PropertyStore r, + Member (Error PropertySubsystemError) r, + Member Events r, + Member TinyLog r + ) => + PropertySubsystemConfig -> + InterpreterFor PropertySubsystem r +interpretPropertySubsystem cfg = + interpret $ + runInputConst cfg . \case + SetProperty uid connId key val -> setPropertyImpl uid connId key val + DeleteProperty uid connId key -> deletePropertyImpl uid connId key + ClearProperties uid connId -> clearPropertiesImpl uid connId + OnUserDeleted uid -> onUserDeletdImpl uid + LookupProperty uid key -> lookupPropertyImpl uid key + GetPropertyKeys uid -> getPropertyKeysImpl uid + GetAllProperties uid -> getAllPropertiesImpl uid + +setPropertyImpl :: + ( Member PropertyStore r, + Member (Input PropertySubsystemConfig) r, + Member (Error PropertySubsystemError) r, + Member Events r + ) => + UserId -> + ConnId -> + PropertyKey -> + RawPropertyValue -> + Sem r () +setPropertyImpl uid connId key val = do + validatePropertyKey key + checkMaxProperties uid key + parsedVal <- validatePropertyValue val + PropertyStore.insertProperty uid key val + generatePropertyEvent uid connId $ PropertySet key parsedVal + +checkMaxProperties :: + ( Member PropertyStore r, + Member (Input PropertySubsystemConfig) r, + Member (Error PropertySubsystemError) r + ) => + UserId -> + PropertyKey -> + Sem r () +checkMaxProperties uid key = do + propExists <- isJust <$> PropertyStore.lookupProperty uid key + unless propExists $ do + cfg <- input + count <- PropertyStore.countProperties uid + when (count >= cfg.maxProperties) $ + throw TooManyProperties + +validatePropertyKey :: + ( Member (Input PropertySubsystemConfig) r, + Member (Error PropertySubsystemError) r + ) => + PropertyKey -> + Sem r () +validatePropertyKey key = do + cfg <- input + let keyText = Ascii.toText $ propertyKeyName key + when (Text.compareLength keyText (fromIntegral cfg.maxKeyLength) == GT) $ + throw PropertyKeyTooLarge + +validatePropertyValue :: + ( Member (Input PropertySubsystemConfig) r, + Member (Error PropertySubsystemError) r + ) => + RawPropertyValue -> + Sem r Value +validatePropertyValue (RawPropertyValue bs) = do + cfg <- input + when (LBS.compareLength bs cfg.maxValueLength == GT) $ + throw PropertyValueTooLarge + + case Aeson.eitherDecode @Value bs of + Left e -> throw $ PropertyValueInvalid e + Right val -> pure val + +deletePropertyImpl :: (Member PropertyStore r, Member Events r) => UserId -> ConnId -> PropertyKey -> Sem r () +deletePropertyImpl uid connId key = do + PropertyStore.deleteProperty uid key + generatePropertyEvent uid connId $ PropertyDeleted key + +onUserDeletdImpl :: (Member PropertyStore r) => UserId -> Sem r () +onUserDeletdImpl uid = do + PropertyStore.clearProperties uid + +clearPropertiesImpl :: (Member PropertyStore r, Member Events r) => UserId -> ConnId -> Sem r () +clearPropertiesImpl uid connId = do + PropertyStore.clearProperties uid + generatePropertyEvent uid connId PropertiesCleared + +lookupPropertyImpl :: (Member PropertyStore r) => UserId -> PropertyKey -> Sem r (Maybe RawPropertyValue) +lookupPropertyImpl uid key = + PropertyStore.lookupProperty uid key + +getPropertyKeysImpl :: (Member PropertyStore r) => UserId -> Sem r [PropertyKey] +getPropertyKeysImpl uid = + PropertyStore.getPropertyKeys uid + +getAllPropertiesImpl :: + ( Member PropertyStore r, + Member TinyLog r, + Member (Error PropertySubsystemError) r + ) => + UserId -> + Sem r PropertyKeysAndValues +getAllPropertiesImpl uid = do + rawProps <- Map.fromList <$> PropertyStore.getAllProperties uid + PropertyKeysAndValues <$> traverse parseStoredPropertyValue rawProps + +parseStoredPropertyValue :: (Member TinyLog r, Member (Error PropertySubsystemError) r) => RawPropertyValue -> Sem r Value +parseStoredPropertyValue raw = case Aeson.eitherDecode raw.rawPropertyBytes of + Right value -> pure value + Left e -> do + Log.err $ + Log.msg (Log.val "Failed to parse a stored property value") + . Log.field "raw_value" raw.rawPropertyBytes + . Log.field "parse_error" e + throw StoredPropertyValueInvalid diff --git a/libs/wire-subsystems/src/Wire/UserEvents.hs b/libs/wire-subsystems/src/Wire/UserEvents.hs deleted file mode 100644 index 0288dee8d92..00000000000 --- a/libs/wire-subsystems/src/Wire/UserEvents.hs +++ /dev/null @@ -1,13 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - -module Wire.UserEvents where - -import Data.Id -import Imports -import Polysemy -import Wire.API.UserEvent - -data UserEvents m a where - GenerateUserEvent :: UserId -> Maybe ConnId -> UserEvent -> UserEvents m () - -makeSem ''UserEvents diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index ea11d3d16d0..505f763e7dc 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -29,13 +29,13 @@ import Wire.API.User import Wire.API.UserEvent import Wire.Arbitrary import Wire.DeleteQueue +import Wire.Events import Wire.FederationAPIAccess import Wire.GalleyAPIAccess import Wire.Sem.Concurrency import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredUser -import Wire.UserEvents import Wire.UserKeyStore import Wire.UserStore as UserStore import Wire.UserSubsystem @@ -60,7 +60,7 @@ runUserSubsystem :: Member (Error UserSubsystemError) r, Member (FederationAPIAccess fedM) r, Member DeleteQueue r, - Member UserEvents r, + Member Events r, Member Now r, RunClient (fedM 'Brig), FederationMonad fedM, @@ -80,7 +80,7 @@ interpretUserSubsystem :: Member (FederationAPIAccess fedM) r, Member (Input UserSubsystemConfig) r, Member DeleteQueue r, - Member UserEvents r, + Member Events r, Member Now r, RunClient (fedM 'Brig), FederationMonad fedM, @@ -360,7 +360,7 @@ guardLockedHandleField user updateOrigin handle = do updateUserProfileImpl :: ( Member UserStore r, Member (Error UserSubsystemError) r, - Member UserEvents r, + Member Events r, Member GalleyAPIAccess r ) => Local UserId -> @@ -423,7 +423,7 @@ getLocalUserAccountByUserKeyImpl target = runMaybeT $ do updateHandleImpl :: ( Member (Error UserSubsystemError) r, Member GalleyAPIAccess r, - Member UserEvents r, + Member Events r, Member UserStore r ) => Local UserId -> diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index d1fea2a4012..72bc9a465b9 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -53,6 +53,7 @@ import Wire.API.User as User hiding (DeleteUser) import Wire.API.User.Password import Wire.DeleteQueue import Wire.DeleteQueue.InMemory +import Wire.Events import Wire.FederationAPIAccess import Wire.FederationAPIAccess.Interpreter as FI import Wire.GalleyAPIAccess @@ -63,7 +64,6 @@ import Wire.Sem.Concurrency import Wire.Sem.Concurrency.Sequential import Wire.Sem.Now hiding (get) import Wire.StoredUser -import Wire.UserEvents import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem @@ -100,7 +100,7 @@ type MiniBackendEffects = UserKeyStore, State (Map EmailKey UserId), DeleteQueue, - UserEvents, + Events, State [InternalNotification], State MiniBackend, State [MiniEvent], diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs index 9145369b703..5dc96a34f9a 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs @@ -5,14 +5,15 @@ module Wire.MockInterpreters (module MockInterpreters) where import Wire.MockInterpreters.EmailSubsystem as MockInterpreters import Wire.MockInterpreters.Error as MockInterpreters +import Wire.MockInterpreters.Events as MockInterpreters import Wire.MockInterpreters.GalleyAPIAccess as MockInterpreters import Wire.MockInterpreters.HashPassword as MockInterpreters import Wire.MockInterpreters.Now as MockInterpreters import Wire.MockInterpreters.PasswordResetCodeStore as MockInterpreters import Wire.MockInterpreters.PasswordStore as MockInterpreters +import Wire.MockInterpreters.PropertyStore as MockInterpreters import Wire.MockInterpreters.Random as MockInterpreters import Wire.MockInterpreters.SessionStore as MockInterpreters -import Wire.MockInterpreters.UserEvents as MockInterpreters import Wire.MockInterpreters.UserKeyStore as MockInterpreters import Wire.MockInterpreters.UserStore as MockInterpreters import Wire.MockInterpreters.UserSubsystem as MockInterpreters diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/Events.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/Events.hs new file mode 100644 index 00000000000..a80ec590088 --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/Events.hs @@ -0,0 +1,22 @@ +module Wire.MockInterpreters.Events where + +import Data.Id +import Imports +import Polysemy +import Polysemy.State +import Wire.API.UserEvent +import Wire.Events + +data MiniEvent = MkMiniEvent + { userId :: UserId, + mConnId :: Maybe ConnId, + event :: Event + } + deriving stock (Eq, Show) + +miniEventInterpreter :: + (Member (State [MiniEvent]) r) => + InterpreterFor Events r +miniEventInterpreter = interpret \case + GenerateUserEvent uid mconn e -> modify (MkMiniEvent uid mconn (UserEvent e) :) + GeneratePropertyEvent uid mconn e -> modify (MkMiniEvent uid (Just mconn) (PropertyEvent e) :) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PropertyStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PropertyStore.hs new file mode 100644 index 00000000000..6cbc980f1ff --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PropertyStore.hs @@ -0,0 +1,19 @@ +module Wire.MockInterpreters.PropertyStore where + +import Data.Id +import Data.Map qualified as Map +import Imports +import Polysemy +import Polysemy.State +import Wire.API.Properties +import Wire.PropertyStore + +inMemoryPropertyStoreInterpreter :: (Member (State (Map UserId (Map PropertyKey RawPropertyValue))) r) => InterpreterFor PropertyStore r +inMemoryPropertyStoreInterpreter = interpret $ \case + InsertProperty u k v -> modify $ Map.insertWith (Map.union) u (Map.singleton k v) + LookupProperty u k -> gets $ Map.lookup k <=< Map.lookup u + CountProperties u -> gets $ Map.size . Map.findWithDefault mempty u + DeleteProperty u k -> modify $ Map.adjust (Map.delete k) u + ClearProperties u -> modify $ Map.delete u + GetPropertyKeys u -> gets $ Map.keys . Map.findWithDefault mempty u + GetAllProperties u -> gets $ Map.toAscList . Map.findWithDefault mempty u diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserEvents.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserEvents.hs deleted file mode 100644 index 4bcd7319418..00000000000 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserEvents.hs +++ /dev/null @@ -1,20 +0,0 @@ -module Wire.MockInterpreters.UserEvents where - -import Data.Id -import Imports -import Polysemy -import Polysemy.State -import Wire.API.UserEvent -import Wire.UserEvents - -data MiniEvent = MkMiniEvent - { userId :: UserId, - event :: UserEvent - } - deriving stock (Eq, Show) - -miniEventInterpreter :: - (Member (State [MiniEvent]) r) => - InterpreterFor UserEvents r -miniEventInterpreter = interpret \case - GenerateUserEvent uid _mconn e -> modify (MkMiniEvent uid e :) diff --git a/libs/wire-subsystems/test/unit/Wire/PropertySubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/PropertySubsystem/InterpreterSpec.hs new file mode 100644 index 00000000000..7065e5b8b1d --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/PropertySubsystem/InterpreterSpec.hs @@ -0,0 +1,265 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +module Wire.PropertySubsystem.InterpreterSpec where + +import Data.Aeson (FromJSON, ToJSON, Value (..), (.=)) +import Data.Aeson qualified as Aeson +import Data.Aeson.Key qualified as Key +import Data.Aeson.Text qualified as Aeson +import Data.Bifunctor (second) +import Data.ByteString.Lazy qualified as LBS +import Data.Map qualified as Map +import Data.Range +import Data.Scientific (scientific) +import Data.Set qualified as Set +import Data.Text qualified as Text +import Data.Text.Ascii (AsciiPrintable, AsciiText (..), validatePrintable) +import Data.Text.Lazy qualified as LText +import Data.Text.Lazy.Encoding qualified as LText +import GHC.IsList (fromList) +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.State +import Polysemy.TinyLog +import Test.Hspec +import Test.Hspec.QuickCheck +import Test.QuickCheck +import Wire.API.Properties +import Wire.API.UserEvent +import Wire.Events +import Wire.MockInterpreters +import Wire.PropertyStore (PropertyStore) +import Wire.PropertySubsystem +import Wire.PropertySubsystem.Interpreter +import Wire.Sem.Logger.TinyLog (discardTinyLogs) + +defaultConfig :: PropertySubsystemConfig +defaultConfig = + PropertySubsystemConfig + { maxKeyLength = 1024, + maxValueLength = 1024, + maxProperties = 16 + } + +interpretDependencies :: Sem '[PropertyStore, Events, State [MiniEvent], TinyLog, Error e] a -> Either e a +interpretDependencies = + run + . runError + . discardTinyLogs + . evalState mempty + . miniEventInterpreter + . evalState mempty + . inMemoryPropertyStoreInterpreter + . raiseUnder + +spec :: Spec +spec = do + describe "Wire.PropertySubsystem.Interpreter" $ do + prop "set/lookup property" $ + \uid connId key (SmallJSON val) -> + let valBS = Aeson.encode val + rawVal = RawPropertyValue valBS + retrievedVal = + interpretDependencies + . interpretPropertySubsystem defaultConfig + $ do + setProperty uid connId key rawVal + lookupProperty uid key + in retrievedVal === Right (Just rawVal) + + prop "events" $ do + \uid connId key (SmallJSON val) -> + let valBS = Aeson.encode val + rawVal = RawPropertyValue valBS + assertion = + interpretDependencies + . interpretPropertySubsystem defaultConfig + $ do + setProperty uid connId key rawVal + eventsAfterSet <- get + + -- clear events + put [] + deleteProperty uid connId key + eventsAfterDelete <- get + + put [] + clearProperties uid connId + eventsAfterClear <- get + + -- assertions + pure $ + eventsAfterSet === [MkMiniEvent uid (Just connId) $ PropertyEvent $ PropertySet key val] + .&&. eventsAfterDelete === [MkMiniEvent uid (Just connId) $ PropertyEvent $ PropertyDeleted key] + .&&. eventsAfterClear === [MkMiniEvent uid (Just connId) $ PropertyEvent PropertiesCleared] + in either + (\e -> counterexample ("UnexpectedError: " <> show e) False) + id + assertion + + prop "set/delete/lookup property" $ + \uid connId key (SmallJSON val) -> + let valBS = Aeson.encode val + rawVal = RawPropertyValue valBS + retrievedVal = interpretDependencies . interpretPropertySubsystem defaultConfig $ do + setProperty uid connId key rawVal + deleteProperty uid connId key + lookupProperty uid key + in retrievedVal === Right Nothing + + prop "getAllProperties" $ + -- 16 is the default maxProperties + \uid connId (fromRange @0 @16 @[(PropertyKey, SmallJSON)] -> keySmallVal) -> + let keyVal = unwrapSmallJSON <$> Map.fromList keySmallVal + keyValRaw = RawPropertyValue . Aeson.encode <$> keyVal + retrievedVal = + interpretDependencies + . interpretPropertySubsystem defaultConfig + $ do + forM_ (Map.toAscList keyValRaw) (uncurry (setProperty uid connId)) + getAllProperties uid + in retrievedVal === Right (PropertyKeysAndValues keyVal) + + prop "getPropertyKeys" $ + -- 16 is the default maxProperties + \uid connId (fromRange @0 @16 @[(PropertyKey, SmallJSON)] -> keyVals) -> + let keyValRaw = Map.fromList $ map (second (RawPropertyValue . Aeson.encode)) keyVals + retrievedVal = + interpretDependencies + . interpretPropertySubsystem defaultConfig + $ do + forM_ (Map.toAscList keyValRaw) (uncurry (setProperty uid connId)) + getPropertyKeys uid + in second Set.fromList retrievedVal === Right (Map.keysSet keyValRaw) + + prop "clearProperties" $ + -- 16 is the default maxProperties + \uid connId (fromRange @0 @16 @[(PropertyKey, SmallJSON)] -> keyVals) -> + let keyValRaw = Map.fromList $ map (second (RawPropertyValue . Aeson.encode)) keyVals + retrievedVal = + interpretDependencies + . interpretPropertySubsystem defaultConfig + $ do + forM_ (Map.toAscList keyValRaw) (uncurry (setProperty uid connId)) + clearProperties uid connId + getAllProperties uid + in retrievedVal === Right (PropertyKeysAndValues mempty) + + prop "setting non JSON values should result in an error" $ + -- 1024 is the default max value length + \uid connId key (fromRange @0 @1024 @[Word8] -> nonJSONBytes) -> + let nonJSONBS = LBS.pack nonJSONBytes + setPropertyResult = interpretDependencies . interpretPropertySubsystem defaultConfig $ do + setProperty uid connId key (RawPropertyValue nonJSONBS) + in isNothing (Aeson.decode @Value nonJSONBS) ==> + case setPropertyResult of + Left (PropertyValueInvalid _) -> property True + Left x -> counterexample ("Expected PropertyValueInvalid, got: " <> show x) False + Right () -> counterexample ("Expected PropertyValueInvalid, but there was no error") False + + prop "setting very big JSON values should result in an error" $ + -- Capping default max value length to 1024 to make tests faster, bigger + -- number => slower tests. + \uid connId key (val :: Value) (fromIntegral . fromRange @0 @1024 @Int32 -> maxValueLength) -> + let cfg = defaultConfig {maxValueLength = maxValueLength} + -- Adding spaces to the end shouldn't change the meaning of a JSON, + -- maybe there are better ways of generating a big JSON + valBS = + LText.encodeUtf8 + . LText.justifyLeft (fromIntegral $ maxValueLength + 1) ' ' + $ Aeson.encodeToLazyText val + rawVal = RawPropertyValue valBS + setPropertyResult = interpretDependencies . interpretPropertySubsystem cfg $ do + setProperty uid connId key rawVal + in setPropertyResult === Left PropertyValueTooLarge + + prop "setting very big key names should result in an error" $ + \uid connId (fromRange @1 @1024 @AsciiPrintable -> unwrappedKey) (val :: SmallJSON) -> + let cfg = defaultConfig {maxKeyLength = (fromIntegral . Text.length $ toText unwrappedKey) - 1} + valBS = Aeson.encode val + rawVal = RawPropertyValue valBS + setPropertyResult = interpretDependencies . interpretPropertySubsystem cfg $ do + setProperty uid connId (PropertyKey unwrappedKey) rawVal + in setPropertyResult === Left PropertyKeyTooLarge + + prop "setProperty should respect maxProperties config" $ + \uid connId keyPrefix (SmallJSON val) (fromIntegral . fromRange @1 @20 @Int32 -> maxProperties) -> + let cfg = defaultConfig {maxProperties = maxProperties} + mkKey n = + let Right suffix = validatePrintable $ Text.pack $ show n + in PropertyKey $ keyPrefix <> suffix + keys = map mkKey [1 .. maxProperties] + extraKey = mkKey (maxProperties + 1) + valBS = Aeson.encode val + rawVal = RawPropertyValue valBS + assertion = + interpretDependencies + . interpretPropertySubsystem cfg + $ do + forM_ keys $ \key -> setProperty uid connId key rawVal + setPropErr <- catchExpectedError $ setProperty uid connId extraKey rawVal + allProps <- getAllProperties uid + pure $ + LBS.length valBS <= defaultConfig.maxValueLength ==> + setPropErr === Just TooManyProperties + .&&. allProps === PropertyKeysAndValues (Map.fromList (map (,val) keys)) + in either + (\e -> counterexample ("UnexpectedError: " <> show e) False) + id + assertion + + prop "setProperty should work for pre-existing properties even when maxProperties is reached" $ + \uid connId keyPrefix (SmallJSON val) (SmallJSON newVal) (fromIntegral . fromRange @1 @20 @Int32 -> maxProperties) -> + let cfg = defaultConfig {maxProperties = maxProperties} + mkKey n = + let Right suffix = validatePrintable $ Text.pack $ show n + in PropertyKey $ keyPrefix <> suffix + keys = map mkKey [1 .. maxProperties] + rawVal = RawPropertyValue (Aeson.encode val) + newRawVal = RawPropertyValue (Aeson.encode newVal) + retrievedVal = + interpretDependencies + . interpretPropertySubsystem cfg + $ do + forM_ keys $ \key -> setProperty uid connId key rawVal + setProperty uid connId (head keys) newRawVal + lookupProperty uid (head keys) + in retrievedVal === Right (Just newRawVal) + + describe "arbitrary @SmallJSON" $ + -- Please run this at least a million times when something about it changes + prop "Always produces JSON <= 1024 bytes" $ + \(smallJSON :: SmallJSON) -> + let jsonStr = LText.unpack $ Aeson.encodeToLazyText smallJSON + jsonBS = Aeson.encode smallJSON + in counterexample ("length = " <> show (LBS.length jsonBS) <> "\n" <> jsonStr) $ LBS.length jsonBS <= 1024 + +newtype SmallJSON = SmallJSON {unwrapSmallJSON :: Value} + deriving stock (Show, Eq) + deriving newtype (FromJSON, ToJSON) + +-- | generates small-ish JSON values +instance Arbitrary SmallJSON where + arbitrary = SmallJSON <$> go 0 + where + maxThings = 5 + -- ASCII chars take less space in the JSON + genText = toText . fromRange <$> arbitrary @(Range 0 5 AsciiPrintable) + go depth + | depth >= maxThings = pure Null + | otherwise = do + chooseInt (0, 5) >>= \case + 0 -> String <$> genText + 1 -> Number <$> (scientific <$> chooseInteger (0, 1000) <*> chooseInt (-1, 2)) + 2 -> Bool <$> arbitrary + 3 -> pure $ Null + 4 -> do + n <- chooseInt (0, maxThings) + Array . fromList <$> replicateM n (go (depth + 1)) + _ -> do + n <- chooseInt (0, maxThings) + keys <- Key.fromText <$$> replicateM n genText + vals <- replicateM n $ go (depth + 1) + pure . Aeson.object $ zipWith (.=) keys vals diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 2aed0f71b41..03fb0a2cda5 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -251,16 +251,17 @@ spec = describe "UserSubsystem.Interpreter" do .&&. userAfterUpdate.userLocale === fromMaybe userBeforeUpdate.userLocale update.locale prop "Update user events" $ - \(NotPendingStoredUser alice) localDomain update config -> do + \(NotPendingStoredUser alice) connId localDomain update config -> do let lusr = toLocalUnsafe localDomain alice.id localBackend = def {users = [alice {managedBy = Just ManagedByWire}]} events = runNoFederationStack localBackend Nothing config do - updateUserProfile lusr Nothing UpdateOriginScim update + updateUserProfile lusr connId UpdateOriginScim update get @[MiniEvent] in events === [ MkMiniEvent alice.id - ( UserUpdated $ + connId + ( UserEvent . UserUpdated $ (emptyUserUpdatedData alice.id) { eupName = update.name, eupTextStatus = update.textStatus, diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index a603df5d43f..2169aa80aca 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -82,6 +82,7 @@ library Wire.EmailSubsystem.Interpreter Wire.EmailSubsystem.Template Wire.Error + Wire.Events Wire.FederationAPIAccess Wire.FederationAPIAccess.Interpreter Wire.GalleyAPIAccess @@ -96,11 +97,14 @@ library Wire.PasswordResetCodeStore.Cassandra Wire.PasswordStore Wire.PasswordStore.Cassandra + Wire.PropertyStore + Wire.PropertyStore.Cassandra + Wire.PropertySubsystem + Wire.PropertySubsystem.Interpreter Wire.Rpc Wire.SessionStore Wire.SessionStore.Cassandra Wire.StoredUser - Wire.UserEvents Wire.UserKeyStore Wire.UserKeyStore.Cassandra Wire.UserStore @@ -202,19 +206,21 @@ test-suite wire-subsystems-tests Wire.MockInterpreters Wire.MockInterpreters.EmailSubsystem Wire.MockInterpreters.Error + Wire.MockInterpreters.Events Wire.MockInterpreters.GalleyAPIAccess Wire.MockInterpreters.HashPassword Wire.MockInterpreters.Now Wire.MockInterpreters.PasswordResetCodeStore Wire.MockInterpreters.PasswordStore + Wire.MockInterpreters.PropertyStore Wire.MockInterpreters.Random Wire.MockInterpreters.SessionStore - Wire.MockInterpreters.UserEvents Wire.MockInterpreters.UserKeyStore Wire.MockInterpreters.UserStore Wire.MockInterpreters.UserSubsystem Wire.MockInterpreters.VerificationCodeStore Wire.NotificationSubsystem.InterpreterSpec + Wire.PropertySubsystem.InterpreterSpec Wire.UserStoreSpec Wire.UserSubsystem.InterpreterSpec Wire.VerificationCodeSubsystem.InterpreterSpec @@ -247,6 +253,7 @@ test-suite wire-subsystems-tests , QuickCheck , quickcheck-instances , random + , scientific , servant-client-core , streaming-commons , string-conversions diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 4ab0deeacf9..428264a318f 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -90,7 +90,6 @@ library Brig.API.MLS.KeyPackages.Validation Brig.API.MLS.Util Brig.API.OAuth - Brig.API.Properties Brig.API.Public Brig.API.Public.Swagger Brig.API.Types @@ -111,7 +110,6 @@ library Brig.Data.LoginCode Brig.Data.MLS.KeyPackage Brig.Data.Nonce - Brig.Data.Properties Brig.Data.Types Brig.Data.User Brig.DeleteQueue.Interpreter @@ -397,7 +395,6 @@ executable brig-integration API.User.Connection API.User.Handles API.User.PasswordReset - API.User.Property API.User.RichInfo API.User.Util API.UserPendingActivation diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index 9c120e6d5b5..bba2cd54e2a 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -210,9 +210,6 @@ certEnrollmentError MissingName = StdError $ Wai.mkError status400 "missing-name fedError :: FederationError -> HttpError fedError = StdError . federationErrorToWai -propDataError :: PropertiesDataError -> HttpError -propDataError TooManyProperties = StdError tooManyProperties - clientDataError :: ClientDataError -> HttpError clientDataError TooManyClients = StdError (errorToWai @'E.TooManyClients) clientDataError (ClientReAuthError e) = reauthError e @@ -250,15 +247,6 @@ verificationCodeThrottledError (VerificationCodeThrottled t) = -- WAI Errors ----------------------------------------------------------------- -tooManyProperties :: Wai.Error -tooManyProperties = Wai.mkError status403 "too-many-properties" "Too many properties" - -propertyKeyTooLarge :: Wai.Error -propertyKeyTooLarge = Wai.mkError status403 "property-key-too-large" "The property key is too large." - -propertyValueTooLarge :: Wai.Error -propertyValueTooLarge = Wai.mkError status403 "property-value-too-large" "The property value is too large" - clientCapabilitiesCannotBeRemoved :: Wai.Error clientCapabilitiesCannotBeRemoved = Wai.mkError status409 "client-capabilities-cannot-be-removed" "You can only add capabilities to a client, not remove them." diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 1e05d13e6cf..1f764b4b126 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -105,6 +105,7 @@ import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem (EmailSubsystem) import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..)) import Wire.NotificationSubsystem +import Wire.PropertySubsystem import Wire.Rpc import Wire.Sem.Concurrency import Wire.Sem.Paging.Cassandra (InternalPaging) @@ -137,7 +138,8 @@ servantSitemap :: Member (UserPendingActivationStore p) r, Member EmailSending r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member PropertySubsystem r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -187,7 +189,8 @@ accountAPI :: Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member PropertySubsystem r ) => ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = @@ -513,7 +516,8 @@ deleteUserNoAuthH :: Member UserKeyStore r, Member (Input (Local ())) r, Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member (ConnectionStore InternalPaging) r, + Member PropertySubsystem r ) => UserId -> (Handler r) DeleteUserResponse diff --git a/services/brig/src/Brig/API/Properties.hs b/services/brig/src/Brig/API/Properties.hs deleted file mode 100644 index 814b899962a..00000000000 --- a/services/brig/src/Brig/API/Properties.hs +++ /dev/null @@ -1,54 +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 Brig.API.Properties - ( PropertiesDataError (..), - setProperty, - deleteProperty, - clearProperties, - Data.lookupProperty, - Data.lookupPropertyKeys, - Data.lookupPropertyKeysAndValues, - ) -where - -import Brig.App -import Brig.Data.Properties (PropertiesDataError) -import Brig.Data.Properties qualified as Data -import Brig.IO.Intra qualified as Intra -import Control.Error -import Data.Id -import Imports -import Polysemy -import Wire.API.Properties -import Wire.API.UserEvent -import Wire.NotificationSubsystem - -setProperty :: (Member NotificationSubsystem r) => UserId -> ConnId -> PropertyKey -> PropertyValue -> ExceptT PropertiesDataError (AppT r) () -setProperty u c k v = do - wrapClientE $ Data.insertProperty u k (propertyRaw v) - lift $ liftSem $ Intra.onPropertyEvent u c (PropertySet k (propertyValue v)) - -deleteProperty :: (Member NotificationSubsystem r) => UserId -> ConnId -> PropertyKey -> AppT r () -deleteProperty u c k = do - wrapClient $ Data.deleteProperty u k - liftSem $ Intra.onPropertyEvent u c (PropertyDeleted k) - -clearProperties :: (Member NotificationSubsystem r) => UserId -> ConnId -> AppT r () -clearProperties u c = do - wrapClient $ Data.clearProperties u - liftSem $ Intra.onPropertyEvent u c PropertiesCleared diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 2c849ed62c1..fb2b03b23f6 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -32,7 +32,6 @@ import Brig.API.Error import Brig.API.Handler import Brig.API.MLS.KeyPackages import Brig.API.OAuth (oauthAPI) -import Brig.API.Properties qualified as API import Brig.API.Public.Swagger import Brig.API.Types import Brig.API.User qualified as API @@ -67,9 +66,7 @@ import Control.Lens (view, (.~), (?~), (^.)) import Control.Monad.Catch (throwM) import Control.Monad.Except import Data.Aeson hiding (json) -import Data.Bifunctor import Data.ByteString (fromStrict, toStrict) -import Data.ByteString.Lazy qualified as Lazy import Data.ByteString.Lazy.Char8 qualified as LBS import Data.ByteString.UTF8 qualified as UTF8 import Data.Code qualified as Code @@ -88,10 +85,7 @@ import Data.OpenApi qualified as S import Data.Qualified import Data.Range import Data.Schema () -import Data.Text qualified as Text -import Data.Text.Ascii qualified as Ascii import Data.Text.Encoding qualified as Text -import Data.Text.Lazy (pack) import Data.Time.Clock (UTCTime) import Data.ZAuth.Token qualified as ZAuth import FileEmbedLzma @@ -160,6 +154,7 @@ import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.NotificationSubsystem import Wire.PasswordStore (PasswordStore, lookupHashedPassword) +import Wire.PropertySubsystem import Wire.Sem.Concurrency import Wire.Sem.Jwk (Jwk) import Wire.Sem.Now (Now) @@ -307,7 +302,8 @@ servantSitemap :: Member (UserPendingActivationStore p) r, Member EmailSubsystem r, Member EmailSending r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member PropertySubsystem r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -421,13 +417,13 @@ servantSitemap = propertiesAPI :: ServerT PropertiesAPI (Handler r) propertiesAPI = - ( Named @"set-property" setProperty - :<|> Named @"delete-property" deleteProperty - :<|> Named @"clear-properties" clearProperties - :<|> Named @"get-property" getProperty - :<|> Named @"list-property-keys" listPropertyKeys + ( Named @"set-property" setPropertyH + :<|> Named @"delete-property" deletePropertyH + :<|> Named @"clear-properties" clearPropertiesH + :<|> Named @"get-property" getPropertyH + :<|> Named @"list-property-keys" listPropertyKeysH ) - :<|> Named @"list-properties" listPropertyKeysAndValues + :<|> Named @"list-properties" listPropertyKeysAndValuesH mlsAPI :: ServerT MLSAPI (Handler r) mlsAPI = @@ -476,61 +472,23 @@ servantSitemap = --------------------------------------------------------------------------- -- Handlers -setProperty :: (Member NotificationSubsystem r) => UserId -> ConnId -> Public.PropertyKey -> Public.RawPropertyValue -> Handler r () -setProperty u c key raw = do - checkPropertyKey key - val <- safeParsePropertyValue raw - API.setProperty u c key val !>> propDataError - -checkPropertyKey :: Public.PropertyKey -> Handler r () -checkPropertyKey k = do - maxKeyLen <- fromMaybe defMaxKeyLen <$> view (settings . propertyMaxKeyLen) - let keyText = Ascii.toText (Public.propertyKeyName k) - when (Text.compareLength keyText (fromIntegral maxKeyLen) == GT) $ - throwStd propertyKeyTooLarge - --- | Parse a 'PropertyValue' from a bytestring. This is different from 'FromJSON' in that --- checks the byte size of the input, and fails *without consuming all of it* if that size --- exceeds the settings. -safeParsePropertyValue :: Public.RawPropertyValue -> Handler r Public.PropertyValue -safeParsePropertyValue raw = do - maxValueLen <- fromMaybe defMaxValueLen <$> view (settings . propertyMaxValueLen) - let lbs = Lazy.take (maxValueLen + 1) (Public.rawPropertyBytes raw) - unless (Lazy.length lbs <= maxValueLen) $ - throwStd propertyValueTooLarge - hoistEither $ first (StdError . badRequest . pack) (propertyValueFromRaw raw) - -propertyValueFromRaw :: Public.RawPropertyValue -> Either String Public.PropertyValue -propertyValueFromRaw raw = - Public.PropertyValue raw - <$> eitherDecode (Public.rawPropertyBytes raw) - -parseStoredPropertyValue :: Public.RawPropertyValue -> Handler r Public.PropertyValue -parseStoredPropertyValue raw = case propertyValueFromRaw raw of - Right value -> pure value - Left e -> do - Log.err $ - Log.msg (Log.val "Failed to parse a stored property value") - . Log.field "raw_value" (Public.rawPropertyBytes raw) - . Log.field "parse_error" e - throwStd internalServerError - -deleteProperty :: (Member NotificationSubsystem r) => UserId -> ConnId -> Public.PropertyKey -> Handler r () -deleteProperty u c k = lift (API.deleteProperty u c k) - -clearProperties :: (Member NotificationSubsystem r) => UserId -> ConnId -> Handler r () -clearProperties u c = lift (API.clearProperties u c) - -getProperty :: UserId -> Public.PropertyKey -> Handler r (Maybe Public.RawPropertyValue) -getProperty u k = lift . wrapClient $ API.lookupProperty u k - -listPropertyKeys :: UserId -> Handler r [Public.PropertyKey] -listPropertyKeys u = lift $ wrapClient (API.lookupPropertyKeys u) - -listPropertyKeysAndValues :: UserId -> Handler r Public.PropertyKeysAndValues -listPropertyKeysAndValues u = do - keysAndVals <- fmap Map.fromList . lift $ wrapClient (API.lookupPropertyKeysAndValues u) - Public.PropertyKeysAndValues <$> traverse parseStoredPropertyValue keysAndVals +setPropertyH :: (Member PropertySubsystem r) => UserId -> ConnId -> Public.PropertyKey -> Public.RawPropertyValue -> Handler r () +setPropertyH u c key raw = lift . liftSem $ setProperty u c key raw + +deletePropertyH :: (Member PropertySubsystem r) => UserId -> ConnId -> Public.PropertyKey -> Handler r () +deletePropertyH u c k = lift . liftSem $ deleteProperty u c k + +clearPropertiesH :: (Member PropertySubsystem r) => UserId -> ConnId -> Handler r () +clearPropertiesH u c = lift . liftSem $ clearProperties u c + +getPropertyH :: (Member PropertySubsystem r) => UserId -> Public.PropertyKey -> Handler r (Maybe Public.RawPropertyValue) +getPropertyH u k = lift . liftSem $ lookupProperty u k + +listPropertyKeysH :: (Member PropertySubsystem r) => UserId -> Handler r [Public.PropertyKey] +listPropertyKeysH u = lift . liftSem $ getPropertyKeys u + +listPropertyKeysAndValuesH :: (Member PropertySubsystem r) => UserId -> Handler r Public.PropertyKeysAndValues +listPropertyKeysAndValuesH u = lift . liftSem $ getAllProperties u getPrekeyUnqualifiedH :: (Member DeleteQueue r) => @@ -1238,7 +1196,8 @@ deleteSelfUser :: Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member PropertySubsystem r ) => UserId -> Public.DeleteUser -> @@ -1255,7 +1214,8 @@ verifyDeleteUser :: Member UserKeyStore r, Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member PropertySubsystem r ) => Public.VerifyDeleteUser -> Handler r () diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 2152214961c..6e6259f3202 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -22,7 +22,6 @@ module Brig.API.Types Activation (..), ActivationError (..), ClientDataError (..), - PropertiesDataError (..), AuthError (..), ReAuthError (..), LegalHoldLoginError (..), @@ -33,7 +32,6 @@ where import Brig.Data.Activation (Activation (..), ActivationError (..)) import Brig.Data.Client (ClientDataError (..)) -import Brig.Data.Properties (PropertiesDataError (..)) import Brig.Data.User (AuthError (..), ReAuthError (..)) import Brig.Types.Intra import Data.Code diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index da5333d7088..543ec6d839e 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -84,7 +84,6 @@ import Brig.Data.Activation qualified as Data import Brig.Data.Client qualified as Data import Brig.Data.Connection (countConnections) import Brig.Data.Connection qualified as Data -import Brig.Data.Properties qualified as Data import Brig.Data.User import Brig.Data.User qualified as Data import Brig.Effects.BlacklistStore (BlacklistStore) @@ -152,6 +151,7 @@ import Wire.Error import Wire.GalleyAPIAccess as GalleyAPIAccess import Wire.NotificationSubsystem import Wire.PasswordStore (PasswordStore, lookupHashedPassword, upsertHashedPassword) +import Wire.PropertySubsystem as PropertySubsystem import Wire.Sem.Concurrency import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore @@ -927,7 +927,8 @@ deleteSelfUser :: Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member PropertySubsystem r ) => UserId -> Maybe PlainTextPassword6 -> @@ -998,7 +999,8 @@ verifyDeleteUser :: Member UserStore r, Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member PropertySubsystem r ) => VerifyDeleteUser -> ExceptT DeleteUserError (AppT r) () @@ -1024,7 +1026,8 @@ ensureAccountDeleted :: Member (Input (Local ())) r, Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, - Member UserStore r + Member UserStore r, + Member PropertySubsystem r ) => UserId -> AppT r DeleteUserResult @@ -1033,7 +1036,7 @@ ensureAccountDeleted uid = do case mbAcc of Nothing -> pure NoUser Just acc -> do - probs <- wrapClient $ Data.lookupPropertyKeysAndValues uid + probs <- liftSem $ getPropertyKeys uid let accIsDeleted = accountStatus acc == Deleted clients <- wrapClient $ Data.lookupClients uid @@ -1073,7 +1076,8 @@ deleteAccount :: Member (Input (Local ())) r, Member UserStore r, Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member (ConnectionStore InternalPaging) r, + Member PropertySubsystem r ) => UserAccount -> Sem r () @@ -1084,7 +1088,7 @@ deleteAccount (accountUser -> user) = do -- Free unique keys for_ (userEmail user) $ deleteKeyForUser uid . mkEmailKey - embed $ Data.clearProperties uid + PropertySubsystem.onUserDeleted uid deleteUser user diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 13158e6f03d..3e8a92f48d5 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -14,7 +14,7 @@ import Brig.Effects.PublicKeyBundle import Brig.Effects.SFT (SFT, interpretSFT) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore.Cassandra (userPendingActivationStoreToCassandra) -import Brig.IO.Intra (runUserEvents) +import Brig.IO.Intra (runEvents) import Brig.Options (ImplicitNoFederationRestriction (federationDomainConfig), federationDomainConfigs, federationStrategy) import Brig.Options qualified as Opt import Cassandra qualified as Cas @@ -43,6 +43,7 @@ import Wire.EmailSending.SMTP import Wire.EmailSubsystem import Wire.EmailSubsystem.Interpreter import Wire.Error +import Wire.Events import Wire.FederationAPIAccess qualified import Wire.FederationAPIAccess.Interpreter (FederationAPIAccessConfig (..), interpretFederationAPIAccess) import Wire.GalleyAPIAccess (GalleyAPIAccess) @@ -56,6 +57,10 @@ import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PasswordResetCodeStore.Cassandra (interpretClientToIO, passwordResetCodeStoreToCassandra) import Wire.PasswordStore (PasswordStore) import Wire.PasswordStore.Cassandra (interpretPasswordStore) +import Wire.PropertyStore +import Wire.PropertyStore.Cassandra +import Wire.PropertySubsystem +import Wire.PropertySubsystem.Interpreter import Wire.Rpc import Wire.Sem.Concurrency import Wire.Sem.Concurrency.IO @@ -69,7 +74,6 @@ import Wire.Sem.Random import Wire.Sem.Random.IO import Wire.SessionStore import Wire.SessionStore.Cassandra (interpretSessionStoreCassandra) -import Wire.UserEvents import Wire.UserKeyStore import Wire.UserKeyStore.Cassandra import Wire.UserStore @@ -87,12 +91,14 @@ type BrigCanonicalEffects = UserSubsystem, EmailSubsystem, VerificationCodeSubsystem, + PropertySubsystem, DeleteQueue, - UserEvents, + Wire.Events.Events, Error UserSubsystemError, Error AuthenticationSubsystemError, Error Wire.API.Federation.Error.FederationError, Error VerificationCodeSubsystemError, + Error PropertySubsystemError, Error HttpError, Wire.FederationAPIAccess.FederationAPIAccess Wire.API.Federation.Client.FederatorClient, HashPassword, @@ -101,6 +107,7 @@ type BrigCanonicalEffects = SessionStore, PasswordStore, VerificationCodeStore, + PropertyStore, SFT, ConnectionStore InternalPaging, Input VerificationCodeThrottleTTL, @@ -149,6 +156,12 @@ runBrigToIO e (AppT ma) = do http2Manager = e ^. App.http2Manager, requestId = e ^. App.requestId } + propertySubsystemConfig = + PropertySubsystemConfig + { maxKeyLength = fromMaybe Opt.defMaxKeyLen $ e ^. settings . Opt.propertyMaxKeyLen, + maxValueLength = fromMaybe Opt.defMaxValueLen $ e ^. settings . Opt.propertyMaxValueLen, + maxProperties = 16 + } ( either throwM pure <=< ( runFinal . unsafelyPerformConcurrency @@ -182,6 +195,7 @@ runBrigToIO e (AppT ma) = do . runInputConst (e ^. settings . to Opt.set2FACodeGenerationDelaySecs . to fromIntegral) . connectionStoreToCassandra . interpretSFT (e ^. httpManager) + . interpretPropertyStoreCassandra (e ^. casClient) . interpretVerificationCodeStoreCassandra (e ^. casClient) . interpretPasswordStore (e ^. casClient) . interpretSessionStoreCassandra (e ^. casClient) @@ -190,12 +204,14 @@ runBrigToIO e (AppT ma) = do . runHashPassword . interpretFederationAPIAccess federationApiAccessConfig . rethrowHttpErrorIO + . mapError propertySubsystemErrorToHttpError . mapError verificationCodeSubsystemErrorToHttpError . mapError (StdError . federationErrorToWai) . mapError authenticationSubsystemErrorToHttpError . mapError userSubsystemErrorToHttpError - . runUserEvents + . runEvents . runDeleteQueue (e ^. internalEvents) + . interpretPropertySubsystem propertySubsystemConfig . interpretVerificationCodeSubsystem . emailSubsystemInterpreter (e ^. usrTemplates) (e ^. templateBranding) . runUserSubsystem userSubsystemConfig diff --git a/services/brig/src/Brig/Data/Properties.hs b/services/brig/src/Brig/Data/Properties.hs deleted file mode 100644 index 6fd099d8620..00000000000 --- a/services/brig/src/Brig/Data/Properties.hs +++ /dev/null @@ -1,95 +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 Brig.Data.Properties - ( PropertiesDataError (..), - insertProperty, - deleteProperty, - clearProperties, - lookupProperty, - lookupPropertyKeys, - lookupPropertyKeysAndValues, - ) -where - -import Cassandra -import Control.Error -import Data.Id -import Imports -import Wire.API.Properties - -maxProperties :: Int64 -maxProperties = 16 - -data PropertiesDataError - = TooManyProperties - -insertProperty :: - (MonadClient m) => - UserId -> - PropertyKey -> - RawPropertyValue -> - ExceptT PropertiesDataError m () -insertProperty u k v = do - n <- lift . fmap (maybe 0 runIdentity) . retry x1 $ query1 propertyCount (params LocalQuorum (Identity u)) - unless (n < maxProperties) $ - throwE TooManyProperties - lift . retry x5 $ write propertyInsert (params LocalQuorum (u, k, v)) - -deleteProperty :: (MonadClient m) => UserId -> PropertyKey -> m () -deleteProperty u k = retry x5 $ write propertyDelete (params LocalQuorum (u, k)) - -clearProperties :: (MonadClient m) => UserId -> m () -clearProperties u = retry x5 $ write propertyReset (params LocalQuorum (Identity u)) - -lookupProperty :: (MonadClient m) => UserId -> PropertyKey -> m (Maybe RawPropertyValue) -lookupProperty u k = - fmap runIdentity - <$> retry x1 (query1 propertySelect (params LocalQuorum (u, k))) - -lookupPropertyKeys :: (MonadClient m) => UserId -> m [PropertyKey] -lookupPropertyKeys u = - map runIdentity - <$> retry x1 (query propertyKeysSelect (params LocalQuorum (Identity u))) - -lookupPropertyKeysAndValues :: (MonadClient m) => UserId -> m [(PropertyKey, RawPropertyValue)] -lookupPropertyKeysAndValues u = - retry x1 (query propertyKeysValuesSelect (params LocalQuorum (Identity u))) - -------------------------------------------------------------------------------- --- Queries - -propertyInsert :: PrepQuery W (UserId, PropertyKey, RawPropertyValue) () -propertyInsert = "INSERT INTO properties (user, key, value) VALUES (?, ?, ?)" - -propertyDelete :: PrepQuery W (UserId, PropertyKey) () -propertyDelete = "DELETE FROM properties where user = ? and key = ?" - -propertyReset :: PrepQuery W (Identity UserId) () -propertyReset = "DELETE FROM properties where user = ?" - -propertySelect :: PrepQuery R (UserId, PropertyKey) (Identity RawPropertyValue) -propertySelect = "SELECT value FROM properties where user = ? and key = ?" - -propertyKeysSelect :: PrepQuery R (Identity UserId) (Identity PropertyKey) -propertyKeysSelect = "SELECT key FROM properties where user = ?" - -propertyKeysValuesSelect :: PrepQuery R (Identity UserId) (PropertyKey, RawPropertyValue) -propertyKeysValuesSelect = "SELECT key, value FROM properties where user = ?" - -propertyCount :: PrepQuery R (Identity UserId) (Identity Int64) -propertyCount = "SELECT COUNT(*) FROM properties where user = ?" diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 650940bac87..716272ccf62 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -26,7 +26,7 @@ module Brig.IO.Intra onClientEvent, -- * user subsystem interpretation for user events - runUserEvents, + runEvents, -- * Conversations createConnectConv, @@ -99,12 +99,12 @@ import Wire.API.Team.Member qualified as Team import Wire.API.User import Wire.API.User.Client import Wire.API.UserEvent +import Wire.Events import Wire.NotificationSubsystem import Wire.Rpc import Wire.Sem.Logger qualified as Log import Wire.Sem.Paging qualified as P import Wire.Sem.Paging.Cassandra (InternalPaging) -import Wire.UserEvents ----------------------------------------------------------------------------- -- Event Handlers @@ -126,7 +126,7 @@ onUserEvent orig conn e = *> dispatchNotifications orig conn e *> embed (journalEvent orig e) -runUserEvents :: +runEvents :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member TinyLog r, @@ -134,10 +134,11 @@ runUserEvents :: Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r ) => - InterpreterFor UserEvents r -runUserEvents = interpret \case + InterpreterFor Events r +runEvents = interpret \case -- FUTUREWORK(mangoiv): should this be in another module? GenerateUserEvent uid mconnid event -> onUserEvent uid mconnid event + GeneratePropertyEvent uid connid event -> onPropertyEvent uid connid event onConnectionEvent :: (Member NotificationSubsystem r) => diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 912d5241c01..899381faa23 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -39,6 +39,7 @@ import Polysemy.TinyLog as Log import System.Logger.Class (field, msg, val, (~~)) import Wire.API.UserEvent import Wire.NotificationSubsystem +import Wire.PropertySubsystem import Wire.Sem.Delay import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore @@ -57,7 +58,8 @@ onEvent :: Member UserKeyStore r, Member (Input UTCTime) r, Member UserStore r, - Member (ConnectionStore InternalPaging) r + Member (ConnectionStore InternalPaging) r, + Member PropertySubsystem r ) => InternalNotification -> Sem r () diff --git a/services/brig/test/integration/API/User.hs b/services/brig/test/integration/API/User.hs index 59e79905156..d791df93082 100644 --- a/services/brig/test/integration/API/User.hs +++ b/services/brig/test/integration/API/User.hs @@ -27,7 +27,6 @@ import API.User.Client qualified import API.User.Connection qualified import API.User.Handles qualified import API.User.PasswordReset qualified -import API.User.Property qualified import API.User.RichInfo qualified import API.User.Util import Bilge hiding (accept, timeout) @@ -69,7 +68,6 @@ tests conf fbc p b c ch g n aws db userJournalWatcher = do API.User.Connection.tests cl at p b c g fbc db, API.User.Handles.tests cl at conf p b c g, API.User.PasswordReset.tests db cl at conf p b c g, - API.User.Property.tests cl at conf p b c g, API.User.RichInfo.tests cl at conf p b c g ] diff --git a/services/brig/test/integration/API/User/Property.hs b/services/brig/test/integration/API/User/Property.hs deleted file mode 100644 index 071ea2d356d..00000000000 --- a/services/brig/test/integration/API/User/Property.hs +++ /dev/null @@ -1,170 +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 API.User.Property - ( tests, - ) -where - -import API.User.Util -import Bilge hiding (accept, timeout) -import Bilge.Assert -import Brig.Options -import Brig.Options qualified as Opt -import Data.Aeson -import Data.ByteString.Char8 qualified as C -import Data.String.Conversions -import Data.Text qualified as T -import Imports -import Network.Wai.Utilities.Error qualified as Error -import Test.Tasty hiding (Timeout) -import Util -import Wire.API.User - -tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> Galley -> TestTree -tests _cl _at opts p b _c _g = - testGroup - "property" - [ test p "put/get /properties/:key - 200" $ testSetGetProperty b, - test p "delete /properties/:key - 200" $ testDeleteProperty b, - test p "get /properties - 200" $ testListPropertyKeys b, - test p "get /properties-values - 200" $ testListPropertyKeysAndValues b, - test p "delete /properties - 200" $ testClearProperties b, - test p "put /properties/:key - 403" $ testPropertyLimits opts b, - test p "size limits" $ testSizeLimits opts b - ] - -testSetGetProperty :: Brig -> Http () -testSetGetProperty brig = do - u <- randomUser brig - setProperty brig (userId u) "foo" objectProp - !!! const 200 === statusCode - getProperty brig (userId u) "foo" !!! do - const 200 === statusCode - const (Just objectProp) === responseJsonMaybe - -- String Literals - setProperty brig (userId u) "foo" (String "foo") - !!! const 200 === statusCode - getProperty brig (userId u) "foo" !!! do - const 200 === statusCode - const (Just "\"foo\"") === responseBody - -- Boolean Literals - setProperty brig (userId u) "foo" (Bool True) - !!! const 200 === statusCode - getProperty brig (userId u) "foo" !!! do - const 200 === statusCode - const (Just "true") === responseBody - -- Numeric Literals - setProperty brig (userId u) "foo" (Number 42) - !!! const 200 === statusCode - getProperty brig (userId u) "foo" !!! do - const 200 === statusCode - const (Just "42") === responseBody - where - objectProp = - object - [ "key.1" .= ("val1" :: Text), - "key.2" .= ("val2" :: Text) - ] - -testDeleteProperty :: Brig -> Http () -testDeleteProperty brig = do - u <- randomUser brig - setProperty brig (userId u) "foo" (Bool True) - !!! const 200 === statusCode - deleteProperty brig (userId u) "foo" - !!! const 200 === statusCode - getProperty brig (userId u) "foo" - !!! const 404 === statusCode - -testListPropertyKeys :: Brig -> Http () -testListPropertyKeys = - testListProperties' - "/properties" - (toJSON ["bar" :: Text, "foo"]) - -testListPropertyKeysAndValues :: Brig -> Http () -testListPropertyKeysAndValues = - testListProperties' - "/properties-values" - (object ["bar" .= String "hello", "foo" .= True]) - -testListProperties' :: ByteString -> Value -> Brig -> Http () -testListProperties' endpoint rval brig = do - u <- randomUser brig - setProperty brig (userId u) "foo" (Bool True) - !!! const 200 === statusCode - setProperty brig (userId u) "bar" (String "hello") - !!! const 200 === statusCode - get (brig . path endpoint . zUser (userId u)) !!! do - const 200 === statusCode - const (Just rval) === responseJsonMaybe - -testClearProperties :: Brig -> Http () -testClearProperties brig = do - u <- randomUser brig - setProperty brig (userId u) "foo" (Bool True) - !!! const 200 === statusCode - setProperty brig (userId u) "bar" (String "hello") - !!! const 200 === statusCode - delete (brig . path "/properties" . zUser (userId u) . zConn "conn") - !!! const 200 === statusCode - getProperty brig (userId u) "foo" - !!! const 404 === statusCode - getProperty brig (userId u) "bar" - !!! const 404 === statusCode - -testPropertyLimits :: Opt.Opts -> Brig -> Http () -testPropertyLimits opts brig = do - u <- randomUser brig - let maxKeyLen = fromIntegral $ fromMaybe defMaxKeyLen . setPropertyMaxKeyLen $ optSettings opts - maxValueLen = fromIntegral $ fromMaybe defMaxValueLen . setPropertyMaxValueLen $ optSettings opts - -- Maximum key length - setProperty brig (userId u) (C.replicate (maxKeyLen + 1) 'x') (String "y") !!! do - const 403 === statusCode - const (Just "property-key-too-large") === fmap Error.label . responseJsonMaybe - -- Maximum value length - setProperty brig (userId u) "foo" (String (T.replicate (maxValueLen + 1) "x")) !!! do - const 403 === statusCode - const (Just "property-value-too-large") === fmap Error.label . responseJsonMaybe - -- Maximum count - forM_ [1 .. 16 :: Int] $ \i -> - setProperty brig (userId u) ("foo" <> C.pack (show i)) (Number (fromIntegral i)) - !!! const 200 === statusCode - setProperty brig (userId u) "bar" (String "hello") !!! do - const 403 === statusCode - const (Just "too-many-properties") === fmap Error.label . responseJsonMaybe - -testSizeLimits :: (HasCallStack) => Opt.Opts -> Brig -> Http () -testSizeLimits opts brig = do - let maxKeyLen = fromIntegral $ fromMaybe defMaxKeyLen . setPropertyMaxKeyLen $ optSettings opts - maxValueLen = fromIntegral $ fromMaybe defMaxValueLen . setPropertyMaxValueLen $ optSettings opts - badKey = cs $ replicate (maxKeyLen + 2) '_' - okKey = cs $ replicate (maxKeyLen - 2) '_' - -- we use String Values here that have an encoding that is 2 characters longer than - -- the decoded string value (because of the quotes). - badValue = String . cs $ replicate maxValueLen '_' - okValue = String . cs $ replicate (maxValueLen - 3) '_' - u <- randomUser brig - setProperty brig (userId u) okKey okValue - !!! const 200 === statusCode - setProperty brig (userId u) badKey okValue - !!! const 403 === statusCode - setProperty brig (userId u) okKey badValue - !!! const 403 === statusCode - setProperty brig (userId u) badKey badValue - !!! const 403 === statusCode From c3afa445430f9fdc9504d65d542344fe43a6a2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 24 Jul 2024 13:52:31 +0200 Subject: [PATCH 016/136] Add a forgotten schema change for PR #4155 (#4169) --- cassandra-schema.cql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 0e143beeacf..6a19d682539 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -685,7 +685,8 @@ CREATE TABLE brig_test.user ( sso_id text, status int, supported_protocols int, - team uuid + team uuid, + text_status text ) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' From 2259719041e513eed81873177603e77be5e0af20 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 24 Jul 2024 14:35:57 +0200 Subject: [PATCH 017/136] [WPB-8892] Add block list operations to the user subsystem (#4167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * moved blocklist to subsystems * Drop old BlacklistStore effect in Brig * Add a changelog --------- Co-authored-by: Marko Dimjašević --- changelog.d/5-internal/WPB-8892 | 1 + .../src/Wire/BlockListStore.hs | 14 +++++++ .../src/Wire/BlockListStore}/Cassandra.hs | 14 +++---- .../wire-subsystems/src/Wire/UserSubsystem.hs | 6 +++ .../src/Wire/UserSubsystem/Interpreter.hs | 15 ++++++++ .../test/unit/Wire/MiniBackend.hs | 16 +++++++- .../test/unit/Wire/MockInterpreters.hs | 1 + .../Wire/MockInterpreters/BlockListStore.hs | 13 +++++++ libs/wire-subsystems/wire-subsystems.cabal | 3 ++ services/brig/brig.cabal | 2 - services/brig/src/Brig/API/Auth.hs | 4 +- services/brig/src/Brig/API/Internal.hs | 23 ++++++------ services/brig/src/Brig/API/Public.hs | 10 ++--- services/brig/src/Brig/API/User.hs | 37 +++++++++---------- services/brig/src/Brig/AWS/SesNotification.hs | 14 +++---- .../brig/src/Brig/CanonicalInterpreter.hs | 8 ++-- .../brig/src/Brig/Effects/BlacklistStore.hs | 14 ------- services/brig/src/Brig/Team/API.hs | 21 +++++------ 18 files changed, 130 insertions(+), 86 deletions(-) create mode 100644 changelog.d/5-internal/WPB-8892 create mode 100644 libs/wire-subsystems/src/Wire/BlockListStore.hs rename {services/brig/src/Brig/Effects/BlacklistStore => libs/wire-subsystems/src/Wire/BlockListStore}/Cassandra.hs (80%) create mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/BlockListStore.hs delete mode 100644 services/brig/src/Brig/Effects/BlacklistStore.hs diff --git a/changelog.d/5-internal/WPB-8892 b/changelog.d/5-internal/WPB-8892 new file mode 100644 index 00000000000..e808269195c --- /dev/null +++ b/changelog.d/5-internal/WPB-8892 @@ -0,0 +1 @@ +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/libs/wire-subsystems/src/Wire/BlockListStore.hs b/libs/wire-subsystems/src/Wire/BlockListStore.hs new file mode 100644 index 00000000000..55ce155d46f --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BlockListStore.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.BlockListStore where + +import Imports +import Polysemy +import Wire.UserKeyStore + +data BlockListStore m a where + Insert :: EmailKey -> BlockListStore m () + Exists :: EmailKey -> BlockListStore m Bool + Delete :: EmailKey -> BlockListStore m () + +makeSem ''BlockListStore diff --git a/services/brig/src/Brig/Effects/BlacklistStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs similarity index 80% rename from services/brig/src/Brig/Effects/BlacklistStore/Cassandra.hs rename to libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs index 45ada1cebc9..d8e0e0f077a 100644 --- a/services/brig/src/Brig/Effects/BlacklistStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs @@ -1,20 +1,20 @@ -module Brig.Effects.BlacklistStore.Cassandra - ( interpretBlacklistStoreToCassandra, +module Wire.BlockListStore.Cassandra + ( interpretBlockListStoreToCassandra, ) where -import Brig.Effects.BlacklistStore (BlacklistStore (..)) import Cassandra import Imports import Polysemy +import Wire.BlockListStore (BlockListStore (..)) import Wire.UserKeyStore -interpretBlacklistStoreToCassandra :: +interpretBlockListStoreToCassandra :: forall m r a. (MonadClient m, Member (Embed m) r) => - Sem (BlacklistStore ': r) a -> + Sem (BlockListStore ': r) a -> Sem r a -interpretBlacklistStoreToCassandra = +interpretBlockListStoreToCassandra = interpret $ embed @m . \case Insert uk -> insert uk @@ -22,7 +22,7 @@ interpretBlacklistStoreToCassandra = Delete uk -> delete uk -------------------------------------------------------------------------------- --- UserKey blacklisting +-- UserKey block listing insert :: (MonadClient m) => EmailKey -> m () insert uk = retry x5 $ write keyInsert (params LocalQuorum (Identity $ emailKeyUniq uk)) diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index e7209dd1ecf..0838da2bb18 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -74,6 +74,12 @@ data UserSubsystem m a where GetLocalUserAccountByUserKey :: Local EmailKey -> UserSubsystem m (Maybe UserAccount) -- | returns the user's locale or the default locale if the users exists LookupLocaleWithDefault :: Local UserId -> UserSubsystem m (Maybe Locale) + -- | checks if an email is blocked + IsBlocked :: Email -> UserSubsystem m Bool + -- | removes an email from the block list + BlockListDelete :: Email -> UserSubsystem m () + -- | adds an email to the block list + BlockListInsert :: Email -> UserSubsystem m () -- | the return type of 'CheckHandle' data CheckHandleResp diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 505f763e7dc..7daba81f6d8 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -28,6 +28,7 @@ import Wire.API.Team.Member hiding (userId) import Wire.API.User import Wire.API.UserEvent import Wire.Arbitrary +import Wire.BlockListStore as BlockList import Wire.DeleteQueue import Wire.Events import Wire.FederationAPIAccess @@ -55,6 +56,7 @@ runUserSubsystem :: ( Member GalleyAPIAccess r, Member UserStore r, Member UserKeyStore r, + Member BlockListStore r, Member (Concurrency 'Unsafe) r, -- FUTUREWORK: subsystems should implement concurrency inside interpreters, not depend on this dangerous effect. Member (Error FederationError) r, Member (Error UserSubsystemError) r, @@ -74,6 +76,7 @@ interpretUserSubsystem :: ( Member GalleyAPIAccess r, Member UserStore r, Member UserKeyStore r, + Member BlockListStore r, Member (Concurrency 'Unsafe) r, Member (Error FederationError) r, Member (Error UserSubsystemError) r, @@ -98,6 +101,18 @@ interpretUserSubsystem = interpret \case UpdateHandle uid mconn mb uhandle -> updateHandleImpl uid mconn mb uhandle GetLocalUserAccountByUserKey userKey -> getLocalUserAccountByUserKeyImpl userKey LookupLocaleWithDefault luid -> lookupLocaleOrDefaultImpl luid + IsBlocked email -> isBlockedImpl email + BlockListDelete email -> blockListDeleteImpl email + BlockListInsert email -> blockListInsertImpl email + +isBlockedImpl :: (Member BlockListStore r) => Email -> Sem r Bool +isBlockedImpl = BlockList.exists . mkEmailKey + +blockListDeleteImpl :: (Member BlockListStore r) => Email -> Sem r () +blockListDeleteImpl = BlockList.delete . mkEmailKey + +blockListInsertImpl :: (Member BlockListStore r) => Email -> Sem r () +blockListInsertImpl = BlockList.insert . mkEmailKey lookupLocaleOrDefaultImpl :: (Member UserStore r, Member (Input UserSubsystemConfig) r) => Local UserId -> Sem r (Maybe Locale) lookupLocaleOrDefaultImpl luid = do diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index 72bc9a465b9..8d31d806a9b 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -51,6 +51,7 @@ import Wire.API.Team.Feature import Wire.API.Team.Member hiding (userId) import Wire.API.User as User hiding (DeleteUser) import Wire.API.User.Password +import Wire.BlockListStore import Wire.DeleteQueue import Wire.DeleteQueue.InMemory import Wire.Events @@ -95,6 +96,8 @@ type AllErrors = type MiniBackendEffects = [ UserSubsystem, GalleyAPIAccess, + BlockListStore, + State [EmailKey], UserStore, State [StoredUser], UserKeyStore, @@ -118,7 +121,8 @@ data MiniBackend = MkMiniBackend -- invariant: for each key, the user.id and the key are the same users :: [StoredUser], userKeys :: Map EmailKey UserId, - passwordResetCodes :: Map PasswordResetKey (PRQueryData Identity) + passwordResetCodes :: Map PasswordResetKey (PRQueryData Identity), + blockList :: [EmailKey] } instance Default MiniBackend where @@ -126,7 +130,8 @@ instance Default MiniBackend where MkMiniBackend { users = mempty, userKeys = mempty, - passwordResetCodes = mempty + passwordResetCodes = mempty, + blockList = mempty } -- | represents an entire federated, stateful world of backends @@ -354,9 +359,16 @@ interpretMaybeFederationStackState maybeFederationAPIAccess localBackend teamMem . inMemoryUserKeyStoreInterpreter . liftUserStoreState . inMemoryUserStoreInterpreter + . liftBlockListStoreState + . inMemoryBlockListStoreInterpreter . miniGalleyAPIAccess teamMember galleyConfigs . runUserSubsystem cfg +liftBlockListStoreState :: (Member (State MiniBackend) r) => Sem (State [EmailKey] : r) a -> Sem r a +liftBlockListStoreState = interpret $ \case + Polysemy.State.Get -> gets (.blockList) + Put newBlockList -> modify $ \b -> b {blockList = newBlockList} + liftUserKeyStoreState :: (Member (State MiniBackend) r) => Sem (State (Map EmailKey UserId) : r) a -> Sem r a liftUserKeyStoreState = interpret $ \case Polysemy.State.Get -> gets (.userKeys) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs index 5dc96a34f9a..ebd8d4d1ee5 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs @@ -3,6 +3,7 @@ module Wire.MockInterpreters (module MockInterpreters) where -- Run this from project root to generate the imports: -- ls libs/wire-subsystems/test/unit/Wire/MockInterpreters | sed 's|\(.*\)\.hs|import Wire.MockInterpreters.\1 as MockInterpreters|' +import Wire.MockInterpreters.BlockListStore as MockInterpreters import Wire.MockInterpreters.EmailSubsystem as MockInterpreters import Wire.MockInterpreters.Error as MockInterpreters import Wire.MockInterpreters.Events as MockInterpreters diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BlockListStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BlockListStore.hs new file mode 100644 index 00000000000..2ed63f4e081 --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BlockListStore.hs @@ -0,0 +1,13 @@ +module Wire.MockInterpreters.BlockListStore where + +import Imports +import Polysemy +import Polysemy.State +import Wire.BlockListStore +import Wire.UserKeyStore + +inMemoryBlockListStoreInterpreter :: (Member (State [EmailKey]) r) => InterpreterFor BlockListStore r +inMemoryBlockListStoreInterpreter = interpret $ \case + Insert uk -> modify (uk :) + Exists uk -> gets (elem uk) + Delete uk -> modify (filter (/= uk)) diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 2169aa80aca..e2763335c9f 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -73,6 +73,8 @@ library Wire.AuthenticationSubsystem.Error Wire.AuthenticationSubsystem.Interpreter Wire.AWS + Wire.BlockListStore + Wire.BlockListStore.Cassandra Wire.DeleteQueue Wire.DeleteQueue.InMemory Wire.EmailSending @@ -204,6 +206,7 @@ test-suite wire-subsystems-tests Wire.AuthenticationSubsystem.InterpreterSpec Wire.MiniBackend Wire.MockInterpreters + Wire.MockInterpreters.BlockListStore Wire.MockInterpreters.EmailSubsystem Wire.MockInterpreters.Error Wire.MockInterpreters.Events diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 428264a318f..e3237b22240 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -113,8 +113,6 @@ library Brig.Data.Types Brig.Data.User Brig.DeleteQueue.Interpreter - Brig.Effects.BlacklistStore - Brig.Effects.BlacklistStore.Cassandra Brig.Effects.ConnectionStore Brig.Effects.ConnectionStore.Cassandra Brig.Effects.FederationConfigStore diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index 581876fe0ce..cb167140ffb 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -23,7 +23,6 @@ import Brig.API.Types import Brig.API.User import Brig.App import Brig.Data.User qualified as User -import Brig.Effects.BlacklistStore import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Options import Brig.User.Auth qualified as Auth @@ -53,6 +52,7 @@ import Wire.API.User.Auth hiding (access) import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso +import Wire.BlockListStore import Wire.EmailSubsystem (EmailSubsystem) import Wire.GalleyAPIAccess import Wire.NotificationSubsystem @@ -139,7 +139,7 @@ logout _ Nothing = throwStd authMissingToken logout uts (Just at) = Auth.logout (List1 uts) at !>> zauthError changeSelfEmailH :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r ) => diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 1f764b4b126..85d5342bc0b 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -37,7 +37,6 @@ import Brig.Data.Client qualified as Data import Brig.Data.Connection qualified as Data import Brig.Data.MLS.KeyPackage qualified as Data import Brig.Data.User qualified as Data -import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.FederationConfigStore ( AddFederationRemoteResult (..), @@ -100,6 +99,7 @@ import Wire.API.User.Client import Wire.API.User.RichInfo import Wire.API.UserEvent import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.BlockListStore (BlockListStore) import Wire.DeleteQueue import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem (EmailSubsystem) @@ -119,7 +119,7 @@ import Wire.VerificationCodeSubsystem servantSitemap :: forall r p. - ( Member BlacklistStore r, + ( Member BlockListStore r, Member DeleteQueue r, Member (Concurrency 'Unsafe) r, Member (ConnectionStore InternalPaging) r, @@ -174,7 +174,7 @@ mlsAPI :: ServerT BrigIRoutes.MLSAPI (Handler r) mlsAPI = getMLSClients accountAPI :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member GalleyAPIAccess r, Member AuthenticationSubsystem r, Member DeleteQueue r, @@ -232,7 +232,7 @@ accountAPI = teamsAPI :: ( Member GalleyAPIAccess r, Member (UserPendingActivationStore p) r, - Member BlacklistStore r, + Member BlockListStore r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member UserKeyStore r, @@ -241,7 +241,8 @@ teamsAPI :: Member (Input (Local ())) r, Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, - Member EmailSending r + Member EmailSending r, + Member UserSubsystem r ) => ServerT BrigIRoutes.TeamsAPI (Handler r) teamsAPI = @@ -458,7 +459,7 @@ internalListFullClientsH (UserSet usrs) = lift $ do UserClientsFull <$> wrapClient (Data.lookupClientsBulk (Set.toList usrs)) createUserNoVerify :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member GalleyAPIAccess r, Member (UserPendingActivationStore p) r, Member TinyLog r, @@ -528,14 +529,14 @@ deleteUserNoAuthH uid = do AccountAlreadyDeleted -> pure UserResponseAccountAlreadyDeleted AccountDeleted -> pure UserResponseAccountDeleted -changeSelfEmailMaybeSendH :: (Member BlacklistStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> EmailUpdate -> Maybe Bool -> (Handler r) ChangeEmailResponse +changeSelfEmailMaybeSendH :: (Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> EmailUpdate -> Maybe Bool -> (Handler r) ChangeEmailResponse changeSelfEmailMaybeSendH u body (fromMaybe False -> validate) = do let email = euEmail body changeSelfEmailMaybeSend u (if validate then ActuallySendEmail else DoNotSendEmail) email UpdateOriginScim data MaybeSendEmail = ActuallySendEmail | DoNotSendEmail -changeSelfEmailMaybeSend :: (Member BlacklistStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> MaybeSendEmail -> Email -> UpdateOriginType -> (Handler r) ChangeEmailResponse +changeSelfEmailMaybeSend :: (Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> MaybeSendEmail -> Email -> UpdateOriginType -> (Handler r) ChangeEmailResponse changeSelfEmailMaybeSend u ActuallySendEmail email allowScim = do API.changeSelfEmail u email allowScim changeSelfEmailMaybeSend u DoNotSendEmail email allowScim = do @@ -695,13 +696,13 @@ updateConnectionInternalH updateConn = do API.updateConnectionInternal updateConn !>> connError pure NoContent -checkBlacklist :: (Member BlacklistStore r) => Email -> Handler r CheckBlacklistResponse +checkBlacklist :: (Member BlockListStore r) => Email -> Handler r CheckBlacklistResponse checkBlacklist email = lift $ bool NotBlacklisted YesBlacklisted <$> API.isBlacklisted email -deleteFromBlacklist :: (Member BlacklistStore r) => Email -> Handler r NoContent +deleteFromBlacklist :: (Member BlockListStore r) => Email -> Handler r NoContent deleteFromBlacklist email = lift $ NoContent <$ API.blacklistDelete email -addBlacklist :: (Member BlacklistStore r) => Email -> Handler r NoContent +addBlacklist :: (Member BlockListStore r) => Email -> Handler r NoContent addBlacklist email = lift $ NoContent <$ API.blacklistInsert email updateSSOIdH :: diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index fb2b03b23f6..e7881044116 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -41,7 +41,6 @@ import Brig.Calling.API qualified as Calling import Brig.Data.Connection qualified as Data import Brig.Data.Nonce as Nonce import Brig.Data.User qualified as Data -import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.FederationConfigStore (FederationConfigStore) import Brig.Effects.JwtTools (JwtTools) @@ -146,6 +145,7 @@ import Wire.API.User.RichInfo qualified as Public import Wire.API.UserMap qualified as Public import Wire.API.Wrapped qualified as Public import Wire.AuthenticationSubsystem (AuthenticationSubsystem, createPasswordResetCode, resetPassword) +import Wire.BlockListStore (BlockListStore) import Wire.DeleteQueue import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem @@ -277,7 +277,7 @@ internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = servantSitemap :: forall r p. - ( Member BlacklistStore r, + ( Member BlockListStore r, Member DeleteQueue r, Member (Concurrency 'Unsafe) r, Member (ConnectionStore InternalPaging) r, @@ -697,7 +697,7 @@ createAccessToken method luid cid proof = do -- | docs/reference/user/registration.md {#RefRegistration} createUser :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member GalleyAPIAccess r, Member (UserPendingActivationStore p) r, Member TinyLog r, @@ -1029,7 +1029,7 @@ completePasswordReset req = do -- docs/reference/user/activation.md {#RefActivationRequest} -- docs/reference/user/registration.md {#RefRegistration} sendActivationCode :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member EmailSubsystem r, Member GalleyAPIAccess r, Member UserKeyStore r @@ -1223,7 +1223,7 @@ verifyDeleteUser body = API.verifyDeleteUser body !>> deleteUserError updateUserEmail :: forall r. - ( Member BlacklistStore r, + ( Member BlockListStore r, Member UserKeyStore r, Member GalleyAPIAccess r, Member EmailSubsystem r diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 543ec6d839e..3fa288a39fb 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -86,8 +86,6 @@ import Brig.Data.Connection (countConnections) import Brig.Data.Connection qualified as Data import Brig.Data.User import Brig.Data.User qualified as Data -import Brig.Effects.BlacklistStore (BlacklistStore) -import Brig.Effects.BlacklistStore qualified as BlacklistStore import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore qualified as UserPendingActivationStore @@ -145,6 +143,7 @@ import Wire.API.User.Client import Wire.API.User.RichInfo import Wire.API.UserEvent import Wire.AuthenticationSubsystem (AuthenticationSubsystem, internalLookupPasswordResetCode) +import Wire.BlockListStore as BlockListStore import Wire.DeleteQueue import Wire.EmailSubsystem import Wire.Error @@ -180,14 +179,14 @@ identityErrorToBrigError = \case IdentityErrorUserKeyExists -> StdError $ errorToWai @'E.UserKeyExists verifyUniquenessAndCheckBlacklist :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member UserKeyStore r ) => EmailKey -> ExceptT IdentityError (AppT r) () verifyUniquenessAndCheckBlacklist uk = do checkKey Nothing uk - blacklisted <- lift $ liftSem $ BlacklistStore.exists uk + blacklisted <- lift $ liftSem $ BlockListStore.exists uk when blacklisted $ throwE IdentityErrorBlacklistedEmail where checkKey u k = do @@ -267,7 +266,7 @@ createUserSpar new = do -- docs/reference/user/registration.md {#RefRegistration} createUser :: forall r p. - ( Member BlacklistStore r, + ( Member BlockListStore r, Member GalleyAPIAccess r, Member (UserPendingActivationStore p) r, Member UserKeyStore r, @@ -497,7 +496,7 @@ initAccountFeatureConfig uid = do -- all over the place there, we add a new function that handles just the one new flow where -- users are invited to the team via scim. createUserInviteViaScim :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member UserKeyStore r, Member (UserPendingActivationStore p) r, Member TinyLog r @@ -562,7 +561,7 @@ changeManagedBy uid conn (ManagedByUpdate mb) = do -- | Call 'changeEmail' and process result: if email changes to itself, succeed, if not, send -- validation email. -changeSelfEmail :: (Member BlacklistStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> Email -> UpdateOriginType -> ExceptT HttpError (AppT r) ChangeEmailResponse +changeSelfEmail :: (Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> Email -> UpdateOriginType -> ExceptT HttpError (AppT r) ChangeEmailResponse changeSelfEmail u email allowScim = do changeEmail u email allowScim !>> Error.changeEmailError >>= \case ChangeEmailIdempotent -> @@ -582,7 +581,7 @@ changeSelfEmail u email allowScim = do (Just (userLocale usr)) -- | Prepare changing the email (checking a number of invariants). -changeEmail :: (Member BlacklistStore r, Member UserKeyStore r) => UserId -> Email -> UpdateOriginType -> ExceptT ChangeEmailError (AppT r) ChangeEmailResult +changeEmail :: (Member BlockListStore r, Member UserKeyStore r) => UserId -> Email -> UpdateOriginType -> ExceptT ChangeEmailError (AppT r) ChangeEmailResult changeEmail u email updateOrigin = do em <- either @@ -590,7 +589,7 @@ changeEmail u email updateOrigin = do pure (validateEmail email) let ek = mkEmailKey em - blacklisted <- lift . liftSem $ BlacklistStore.exists ek + blacklisted <- lift . liftSem $ BlockListStore.exists ek when blacklisted $ throwE (ChangeBlacklistedEmail email) available <- lift $ liftSem $ keyAvailable ek (Just u) @@ -804,7 +803,7 @@ onActivated (EmailActivated uid email) = do -- docs/reference/user/activation.md {#RefActivationRequest} sendActivationCode :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member EmailSubsystem r, Member GalleyAPIAccess r, Member UserKeyStore r @@ -819,11 +818,11 @@ sendActivationCode email loc _call = do (const . throwE . InvalidRecipient $ mkEmailKey email) (pure . mkEmailKey) (validateEmail email) - exists <- lift $ liftSem $ isJust <$> lookupKey ek - when exists $ + doesExist <- lift $ liftSem $ isJust <$> lookupKey ek + when doesExist $ throwE $ UserKeyInUse ek - blacklisted <- lift . liftSem $ BlacklistStore.exists ek + blacklisted <- lift . liftSem $ BlockListStore.exists ek when blacklisted $ throwE (ActivationBlacklistedUserKey ek) uc <- lift . wrapClient $ Data.lookupActivationCode ek @@ -1202,17 +1201,17 @@ lookupAccountsByIdentity email includePendingInvitations = do then pure result else pure $ filter ((/= PendingInvitation) . accountStatus) result -isBlacklisted :: (Member BlacklistStore r) => Email -> AppT r Bool +isBlacklisted :: (Member BlockListStore r) => Email -> AppT r Bool isBlacklisted email = do let uk = mkEmailKey email - liftSem $ BlacklistStore.exists uk + liftSem $ BlockListStore.exists uk -blacklistInsert :: (Member BlacklistStore r) => Email -> AppT r () +blacklistInsert :: (Member BlockListStore r) => Email -> AppT r () blacklistInsert email = do let uk = mkEmailKey email - liftSem $ BlacklistStore.insert uk + liftSem $ BlockListStore.insert uk -blacklistDelete :: (Member BlacklistStore r) => Email -> AppT r () +blacklistDelete :: (Member BlockListStore r) => Email -> AppT r () blacklistDelete email = do let uk = mkEmailKey email - liftSem $ BlacklistStore.delete uk + liftSem $ BlockListStore.delete uk diff --git a/services/brig/src/Brig/AWS/SesNotification.hs b/services/brig/src/Brig/AWS/SesNotification.hs index 9902d260830..261a9fd2ddb 100644 --- a/services/brig/src/Brig/AWS/SesNotification.hs +++ b/services/brig/src/Brig/AWS/SesNotification.hs @@ -22,25 +22,23 @@ where import Brig.AWS.Types import Brig.App -import Brig.Effects.BlacklistStore (BlacklistStore) -import Brig.Effects.BlacklistStore qualified as BlacklistStore import Imports import Polysemy (Member) import System.Logger.Class (field, msg, (~~)) import System.Logger.Class qualified as Log import Wire.API.User.Identity -import Wire.UserKeyStore +import Wire.UserSubsystem -onEvent :: (Member BlacklistStore r) => SESNotification -> AppT r () +onEvent :: (Member UserSubsystem r) => SESNotification -> AppT r () onEvent (MailBounce BouncePermanent es) = onPermanentBounce es onEvent (MailBounce BounceTransient es) = onTransientBounce es onEvent (MailBounce BounceUndetermined es) = onUndeterminedBounce es onEvent (MailComplaint es) = onComplaint es -onPermanentBounce :: (Member BlacklistStore r) => [Email] -> AppT r () +onPermanentBounce :: (Member UserSubsystem r) => [Email] -> AppT r () onPermanentBounce = mapM_ $ \e -> do logEmailEvent "Permanent bounce" e - liftSem $ BlacklistStore.insert (mkEmailKey e) + liftSem $ blockListInsert e onTransientBounce :: [Email] -> AppT r () onTransientBounce = mapM_ (logEmailEvent "Transient bounce") @@ -48,10 +46,10 @@ onTransientBounce = mapM_ (logEmailEvent "Transient bounce") onUndeterminedBounce :: [Email] -> AppT r () onUndeterminedBounce = mapM_ (logEmailEvent "Undetermined bounce") -onComplaint :: (Member BlacklistStore r) => [Email] -> AppT r () +onComplaint :: (Member UserSubsystem r) => [Email] -> AppT r () onComplaint = mapM_ $ \e -> do logEmailEvent "Complaint" e - liftSem $ BlacklistStore.insert (mkEmailKey e) + liftSem $ blockListInsert e logEmailEvent :: Text -> Email -> AppT r () logEmailEvent t e = Log.info $ field "email" (fromEmail e) ~~ msg t diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 3e8a92f48d5..62aca48a5f8 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -3,8 +3,6 @@ module Brig.CanonicalInterpreter where import Brig.AWS (amazonkaEnv) import Brig.App as App import Brig.DeleteQueue.Interpreter as DQ -import Brig.Effects.BlacklistStore (BlacklistStore) -import Brig.Effects.BlacklistStore.Cassandra (interpretBlacklistStoreToCassandra) import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.ConnectionStore.Cassandra (connectionStoreToCassandra) import Brig.Effects.FederationConfigStore (FederationConfigStore) @@ -36,6 +34,8 @@ import Wire.API.Federation.Client qualified import Wire.API.Federation.Error import Wire.AuthenticationSubsystem import Wire.AuthenticationSubsystem.Interpreter +import Wire.BlockListStore +import Wire.BlockListStore.Cassandra import Wire.DeleteQueue import Wire.EmailSending import Wire.EmailSending.SES @@ -120,7 +120,7 @@ type BrigCanonicalEffects = Jwk, PublicKeyBundle, JwtTools, - BlacklistStore, + BlockListStore, UserPendingActivationStore InternalPaging, Now, Delay, @@ -182,7 +182,7 @@ runBrigToIO e (AppT ma) = do . runDelay . nowToIOAction (e ^. currentTime) . userPendingActivationStoreToCassandra - . interpretBlacklistStoreToCassandra @Cas.Client + . interpretBlockListStoreToCassandra @Cas.Client . interpretJwtTools . interpretPublicKeyBundle . interpretJwk diff --git a/services/brig/src/Brig/Effects/BlacklistStore.hs b/services/brig/src/Brig/Effects/BlacklistStore.hs deleted file mode 100644 index e888194d7a3..00000000000 --- a/services/brig/src/Brig/Effects/BlacklistStore.hs +++ /dev/null @@ -1,14 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - -module Brig.Effects.BlacklistStore where - -import Imports -import Polysemy -import Wire.UserKeyStore - -data BlacklistStore m a where - Insert :: EmailKey -> BlacklistStore m () - Exists :: EmailKey -> BlacklistStore m Bool - Delete :: EmailKey -> BlacklistStore m () - -makeSem ''BlacklistStore diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 900506d6bd7..0bda80a9f68 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -32,8 +32,6 @@ import Brig.API.User (createUserInviteViaScim, fetchUserIdentity) import Brig.API.User qualified as API import Brig.API.Util (logEmail, logInvitationCode) import Brig.App -import Brig.Effects.BlacklistStore (BlacklistStore) -import Brig.Effects.BlacklistStore qualified as BlacklistStore import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Options (setMaxTeamSize, setTeamInvitationTimeout) @@ -77,6 +75,7 @@ import Wire.API.Team.Role qualified as Public import Wire.API.User hiding (fromEmail) import Wire.API.User qualified as Public import Wire.API.User.Identity qualified as Email +import Wire.BlockListStore import Wire.EmailSending (EmailSending) import Wire.Error import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..)) @@ -88,8 +87,7 @@ import Wire.UserKeyStore import Wire.UserSubsystem servantAPI :: - ( Member BlacklistStore r, - Member GalleyAPIAccess r, + ( Member GalleyAPIAccess r, Member UserKeyStore r, Member UserSubsystem r, Member EmailSending r @@ -118,8 +116,7 @@ getInvitationCode t r = do maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) (pure . FoundInvitationCode) code createInvitationPublicH :: - ( Member BlacklistStore r, - Member GalleyAPIAccess r, + ( Member GalleyAPIAccess r, Member UserKeyStore r, Member UserSubsystem r, Member EmailSending r @@ -143,8 +140,7 @@ data CreateInvitationInviter = CreateInvitationInviter deriving (Eq, Show) createInvitationPublic :: - ( Member BlacklistStore r, - Member GalleyAPIAccess r, + ( Member GalleyAPIAccess r, Member UserKeyStore r, Member UserSubsystem r, Member EmailSending r @@ -173,12 +169,13 @@ createInvitationPublic uid tid body = do (createInvitation' tid Nothing inviteeRole (Just (inviterUid inviter)) (inviterEmail inviter) body) createInvitationViaScim :: - ( Member BlacklistStore r, + ( Member BlockListStore r, Member GalleyAPIAccess r, Member UserKeyStore r, Member (UserPendingActivationStore p) r, Member TinyLog r, - Member EmailSending r + Member EmailSending r, + Member UserSubsystem r ) => TeamId -> NewUserScimInvitation -> @@ -225,7 +222,7 @@ logInvitationRequest context action = pure (Right result) createInvitation' :: - ( Member BlacklistStore r, + ( Member UserSubsystem r, Member GalleyAPIAccess r, Member UserKeyStore r, Member EmailSending r @@ -244,7 +241,7 @@ createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do -- Validate e-mail inviteeEmail <- either (const $ throwStd (errorToWai @'E.InvalidEmail)) pure (Email.validateEmail (irInviteeEmail body)) let uke = mkEmailKey inviteeEmail - blacklistedEm <- lift $ liftSem $ BlacklistStore.exists uke + blacklistedEm <- lift $ liftSem $ isBlocked inviteeEmail when blacklistedEm $ throwStd blacklistedEmail emailTaken <- lift $ liftSem $ isJust <$> lookupKey uke From e60ad7b8f6195a0bcf9f5c2f66c8ce21b83efa82 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 25 Jul 2024 15:47:22 +0200 Subject: [PATCH 018/136] Remove FUTUREWORK about openapi3/swagger. (#4171) some parts of it were outdated or wrong, and what was valid is now tracked in WPB-1031{5,6}. --- services/brig/src/Brig/API/Public/Swagger.hs | 25 -------------------- 1 file changed, 25 deletions(-) diff --git a/services/brig/src/Brig/API/Public/Swagger.hs b/services/brig/src/Brig/API/Public/Swagger.hs index 6db030d193f..92304d5dfa0 100644 --- a/services/brig/src/Brig/API/Public/Swagger.hs +++ b/services/brig/src/Brig/API/Public/Swagger.hs @@ -129,31 +129,6 @@ emptySwagger = & S.info . S.description ?~ "There is no Swagger documentation for this version. Please refer to v5 or later." -{- FUTUREWORK(fisx): there are a few things that need to be fixed before this schema collection - is of any practical use! - -- `ToSchema` instances of team notifications are wrong. To do this right, search - schema-profunctor tutorial for bind/dispatch, and consult conversation events for - examples. - -- swagger2 doesn't handle types with the same name from different models well; it silently - drops the second definition, which is what you want only if there are no name clashes as - in our case with three types called `Event` and three types called `EventType`. We have - solved this by rendering the three event types seperately and returning each - declarations list in a super-list. For a better work-around, check - https://github.com/GetShopTV/swagger2/issues/14. - -- The different `EventData` constructors all have different json schema that partially - overlap. Our schemas only represent the union of all those constructors, rather than a - list of cases. There may be a better way even in swagger v2: - https://swagger.io/specification/v2/ (look for "polymorphism") - -- (wire cloud) expose end-point via nginz (only on staging). - -- Document how this works in - https://docs.wire.com/understand/api-client-perspective/swagger.html - -tracked in https://wearezeta.atlassian.net/browse/FS-1008 -} eventNotificationSchemas :: [S.Definitions S.Schema] eventNotificationSchemas = fst . (`S.runDeclare` mempty) <$> renderAll where From d157ffbc39765a818efba5b926c23e221c06c46e Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Thu, 25 Jul 2024 23:00:40 +0200 Subject: [PATCH 019/136] [chore] Weed out dead code. (#4170) Co-authored-by: Matthias Fischmann --- changelog.d/5-internal/weed | 1 + integration/test/API/Brig.hs | 11 - integration/test/API/BrigInternal.hs | 11 - integration/test/API/Cargohold.hs | 3 - integration/test/API/Common.hs | 7 - integration/test/API/Galley.hs | 30 +-- integration/test/MLS/Util.hs | 11 - integration/test/Test/Cargohold/API/Util.hs | 10 - integration/test/Test/LegalHold.hs | 18 -- integration/test/Testlib/Certs.hs | 19 -- integration/test/Testlib/Env.hs | 14 - integration/test/Testlib/HTTP.hs | 3 - integration/test/Testlib/Prelude.hs | 36 --- libs/bilge/src/Bilge/RPC.hs | 13 - libs/bilge/src/Bilge/TestSession.hs | 3 - libs/brig-types/brig-types.cabal | 7 +- libs/brig-types/default.nix | 1 - .../test/unit/Test/Brig/Roundtrip.hs | 12 - libs/cassandra-util/src/Cassandra/Exec.hs | 10 - libs/cassandra-util/src/Cassandra/Helpers.hs | 2 +- libs/extended/default.nix | 4 - libs/extended/extended.cabal | 3 - libs/extended/src/Data/Time/Clock/DiffTime.hs | 25 +- .../src/Options/Applicative/Extended.hs | 41 --- libs/galley-types/src/Galley/Types.hs | 18 +- .../src/Gundeck/Types/Push/V2.hs | 5 - libs/metrics-wai/src/Data/Metrics/Servant.hs | 15 -- .../src/Wire/Sem/Concurrency.hs | 65 ----- libs/tasty-cannon/src/Test/Tasty/Cannon.hs | 32 --- libs/types-common-aws/src/Util/Test/SQS.hs | 6 - .../types-common-journal/src/Data/Proto/Id.hs | 5 +- libs/types-common/src/Data/Handle.hs | 4 - libs/types-common/src/Data/Json/Util.hs | 4 - libs/types-common/src/Data/Qualified.hs | 6 +- libs/types-common/src/Data/Text/Ascii.hs | 21 -- libs/types-common/src/Data/UUID/Tagged.hs | 13 - libs/types-common/src/Util/Options.hs | 8 - libs/types-common/src/Util/Options/Common.hs | 9 - .../src/Network/Wai/Utilities/Error.hs | 6 - .../src/Network/Wai/Utilities/Request.hs | 35 --- .../src/Network/Wai/Utilities/Response.hs | 10 +- .../src/Network/Wai/Utilities/Server.hs | 19 +- .../src/Network/Wai/Utilities/ZAuth.hs | 56 ---- .../src/Wire/API/Federation/API.hs | 9 - .../API/Federation/BackendNotifications.hs | 3 - .../src/Wire/API/Federation/Component.hs | 6 - .../src/Wire/API/Federation/Version.hs | 13 +- .../Golden/MLSMessageSendingStatus.hs | 17 -- libs/wire-api/src/Wire/API/Call/Config.hs | 11 - libs/wire-api/src/Wire/API/ConverProtoLens.hs | 33 --- libs/wire-api/src/Wire/API/Conversation.hs | 4 - .../src/Wire/API/Conversation/Member.hs | 9 - .../src/Wire/API/Conversation/Protocol.hs | 8 +- .../src/Wire/API/Conversation/Role.hs | 4 - libs/wire-api/src/Wire/API/MLS/CipherSuite.hs | 16 -- libs/wire-api/src/Wire/API/MLS/Message.hs | 30 --- .../src/Wire/API/MLS/Serialisation.hs | 4 - .../src/Wire/API/MakesFederatedCall.hs | 9 - libs/wire-api/src/Wire/API/Message/Proto.hs | 4 - .../src/Wire/API/Provider/Service/Tag.hs | 8 - .../Wire/API/Routes/FederationDomainConfig.hs | 2 - .../wire-api/src/Wire/API/Routes/MultiVerb.hs | 3 - .../src/Wire/API/Routes/Version/Wai.hs | 4 +- libs/wire-api/src/Wire/API/User.hs | 4 - libs/wire-api/wire-api.cabal | 1 - .../test/unit/Wire/MiniBackend.hs | 11 - services/brig/src/Brig/API/Error.hs | 50 ---- services/brig/src/Brig/API/Handler.hs | 10 - services/brig/src/Brig/API/User.hs | 20 -- .../brig/test/integration/API/Search/Util.hs | 7 - .../brig/test/integration/API/Team/Util.hs | 16 -- .../brig/test/integration/API/User/Util.hs | 49 ---- services/brig/test/integration/Util.hs | 96 +------ services/cargohold/cargohold.cabal | 1 - services/cargohold/default.nix | 1 - .../cargohold/test/integration/API/Util.hs | 77 +----- .../cargohold/test/integration/TestSetup.hs | 46 +--- services/galley/default.nix | 3 - services/galley/galley.cabal | 3 - .../galley/test/integration/API/MLS/Mocks.hs | 7 - services/galley/test/integration/API/SQS.hs | 3 - .../integration/API/Teams/LegalHold/Util.hs | 12 - services/galley/test/integration/API/Util.hs | 242 +----------------- .../test/integration/API/Util/TeamFeature.hs | 221 +++------------- services/gundeck/default.nix | 1 - services/gundeck/gundeck.cabal | 1 - services/gundeck/test/unit/ThreadBudget.hs | 21 +- services/proxy/src/Proxy/API/Public.hs | 2 +- weeder.toml | 54 +++- 89 files changed, 137 insertions(+), 1651 deletions(-) create mode 100644 changelog.d/5-internal/weed delete mode 100644 libs/extended/src/Options/Applicative/Extended.hs delete mode 100644 libs/wire-api/src/Wire/API/ConverProtoLens.hs diff --git a/changelog.d/5-internal/weed b/changelog.d/5-internal/weed new file mode 100644 index 00000000000..03b7ed904d9 --- /dev/null +++ b/changelog.d/5-internal/weed @@ -0,0 +1 @@ +Started weeding out dead code. diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 36d6527ae9d..d88cafb9187 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -123,14 +123,6 @@ getUser user target = do joinHttpPath ["users", domain, uid] submit "GET" req -getUserByHandle :: (HasCallStack, MakesValue user, MakesValue domain) => user -> domain -> String -> App Response -getUserByHandle user domain handle = do - domainStr <- asString domain - req <- - baseRequest user Brig Versioned $ - joinHttpPath ["users", "by-handle", domainStr, handle] - submit "GET" req - -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_clients__client_ getClient :: (HasCallStack, MakesValue user, MakesValue client) => @@ -476,9 +468,6 @@ getSwaggerPublicTOC = do joinHttpPath ["api", "swagger-ui"] submit "GET" req -getSwaggerInternalTOC :: (HasCallStack) => App Response -getSwaggerInternalTOC = error "FUTUREWORK: this API end-point does not exist." - getSwaggerPublicAllUI :: (HasCallStack) => Int -> App Response getSwaggerPublicAllUI version = do req <- diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 5fbfd5cf2e5..cb5be7d48c0 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -160,17 +160,6 @@ refreshIndex domain = do res <- submit "POST" req res.status `shouldMatchInt` 200 -connectWithRemoteUser :: (MakesValue userFrom, MakesValue userTo) => userFrom -> userTo -> App () -connectWithRemoteUser userFrom userTo = do - userFromId <- objId userFrom - qUserTo <- make userTo - let body = ["tag" .= "CreateConnectionForTest", "user" .= userFromId, "other" .= qUserTo] - req <- - baseRequest userFrom Brig Unversioned $ - joinHttpPath ["i", "connections", "connection-update"] - res <- submit "PUT" (req & addJSONObject body) - res.status `shouldMatchInt` 200 - addFederationRemoteTeam :: (HasCallStack, MakesValue domain, MakesValue remoteDomain, MakesValue team) => domain -> remoteDomain -> team -> App () addFederationRemoteTeam domain remoteDomain team = do void $ addFederationRemoteTeam' domain remoteDomain team >>= getBody 200 diff --git a/integration/test/API/Cargohold.hs b/integration/test/API/Cargohold.hs index e21e26fed81..df8af34d71c 100644 --- a/integration/test/API/Cargohold.hs +++ b/integration/test/API/Cargohold.hs @@ -72,9 +72,6 @@ textPlainMime = MIME.Text $ T.pack "plain" multipartMixedMime :: String multipartMixedMime = "multipart/mixed; boundary=" <> multipartBoundary -mimeTypeToString :: MIME.MIMEType -> String -mimeTypeToString = T.unpack . MIME.showMIMEType - buildUploadAssetRequestBody :: (HasCallStack, MakesValue assetRetention) => Bool -> diff --git a/integration/test/API/Common.hs b/integration/test/API/Common.hs index 12f4a3866ab..6b80f9e5305 100644 --- a/integration/test/API/Common.hs +++ b/integration/test/API/Common.hs @@ -10,13 +10,6 @@ import qualified Data.Vector as Vector import System.Random (randomIO, randomRIO) import Testlib.Prelude -teamRole :: String -> Int -teamRole "partner" = 1025 -teamRole "member" = 1587 -teamRole "admin" = 5951 -teamRole "owner" = 8191 -teamRole bad = error $ "unknown team role: " <> bad - -- | please don't use special shell characters like '!' here. it makes writing shell lines -- that use test data a lot less straight-forward. defPassword :: String diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index d4c4b6e366e..a0fe93d2993 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -614,6 +614,21 @@ disableLegalHold tid ownerid uid pw = do req <- baseRequest ownerid Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr]) submit "DELETE" (addJSONObject ["password" .= pw] req) +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_teams__tid__legalhold_consent +consentToLegalHold :: (HasCallStack, MakesValue tid, MakesValue zusr) => tid -> zusr -> String -> App Response +consentToLegalHold tid zusr pwd = do + tidStr <- asString tid + req <- baseRequest zusr Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", "consent"]) + submit "POST" (addJSONObject ["password" .= pwd] req) + +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_teams__tid__legalhold__uid_ +getLegalHoldStatus :: (HasCallStack, MakesValue tid, MakesValue zusr) => tid -> zusr -> App Response +getLegalHoldStatus tid zusr = do + tidStr <- asString tid + uidStr <- asString $ zusr %. "id" + req <- baseRequest zusr Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr]) + submit "GET" req + -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_teams__tid__legalhold_settings postLegalHoldSettings :: (HasCallStack, MakesValue ownerid, MakesValue tid, MakesValue newService) => tid -> ownerid -> newService -> App Response postLegalHoldSettings tid owner newSettings = @@ -653,21 +668,6 @@ approveLegalHoldDevice' tid uid forUid pwd = do req <- baseRequest uid Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr, "approve"]) submit "PUT" (addJSONObject ["password" .= pwd] req) --- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_teams__tid__legalhold_consent -consentToLegalHold :: (HasCallStack, MakesValue tid, MakesValue zusr) => tid -> zusr -> String -> App Response -consentToLegalHold tid zusr pwd = do - tidStr <- asString tid - req <- baseRequest zusr Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", "consent"]) - submit "POST" (addJSONObject ["password" .= pwd] req) - --- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_teams__tid__legalhold__uid_ -getLegalHoldStatus :: (HasCallStack, MakesValue tid, MakesValue zusr) => tid -> zusr -> App Response -getLegalHoldStatus tid zusr = do - tidStr <- asString tid - uidStr <- asString $ zusr %. "id" - req <- baseRequest zusr Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr]) - submit "GET" req - -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/put_teams__tid__features_legalhold putLegalholdStatus :: (HasCallStack, MakesValue tid, MakesValue usr) => diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 2c8638d578a..684da6542f3 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -755,17 +755,6 @@ createApplicationMessage cid messageContent = do setMLSCiphersuite :: Ciphersuite -> App () setMLSCiphersuite suite = modifyMLSState $ \mls -> mls {ciphersuite = suite} -withCiphersuite :: (HasCallStack) => Ciphersuite -> App a -> App a -withCiphersuite suite action = do - suite0 <- (.ciphersuite) <$> getMLSState - setMLSCiphersuiteIO <- appToIOKleisli setMLSCiphersuite - actionIO <- appToIO action - liftIO $ - bracket - (setMLSCiphersuiteIO suite) - (const (setMLSCiphersuiteIO suite0)) - (const actionIO) - leaveCurrentConv :: (HasCallStack) => ClientIdentity -> diff --git a/integration/test/Test/Cargohold/API/Util.hs b/integration/test/Test/Cargohold/API/Util.hs index 8ffb512da7b..dffc4168bbe 100644 --- a/integration/test/Test/Cargohold/API/Util.hs +++ b/integration/test/Test/Cargohold/API/Util.hs @@ -98,16 +98,6 @@ header :: String -> String -> Request -> Request header name value req = req {requestHeaders = (mk $ cs name, cs value) : requestHeaders req} -downloadAssetWithAssetKey :: - (HasCallStack, MakesValue user) => - (HTTP.Request -> HTTP.Request) -> - user -> - String -> - App Response -downloadAssetWithAssetKey r user tok = do - req <- baseRequest user Cargohold (ExplicitVersion 1) $ "assets/v3/" <> tok - submit "GET" $ r $ req & tokenParam tok - class IsAssetToken tok where tokenParam :: tok -> Request -> Request diff --git a/integration/test/Test/LegalHold.hs b/integration/test/Test/LegalHold.hs index 22195f3afdb..c948bccb649 100644 --- a/integration/test/Test/LegalHold.hs +++ b/integration/test/Test/LegalHold.hs @@ -593,24 +593,6 @@ testLHGetMembersIncludesStatus = do -- bob has accepted the legalhold device statusShouldBe "enabled" -type TB s = TaggedBool s - -enableLH :: (MakesValue tid, MakesValue teamAdmin, MakesValue targetUser, HasCallStack) => tid -> teamAdmin -> targetUser -> Bool -> App (Maybe String) -enableLH tid teamAdmin targetUser approveLH = do - -- alice requests a legalhold device for herself - requestLegalHoldDevice tid teamAdmin targetUser - >>= assertStatus 201 - - when approveLH do - approveLegalHoldDevice tid targetUser defPassword - >>= assertStatus 200 - legalholdUserStatus tid targetUser targetUser `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "status" `shouldMatch` if approveLH then "enabled" else "pending" - if approveLH - then Just <$> lhDeviceIdOf targetUser - else pure Nothing - testLHConnectionsWithNonConsentingUsers :: App () testLHConnectionsWithNonConsentingUsers = do (alice, tid, []) <- createTeam OwnDomain 1 diff --git a/integration/test/Testlib/Certs.hs b/integration/test/Testlib/Certs.hs index b6fda9b5204..64df6e4c152 100644 --- a/integration/test/Testlib/Certs.hs +++ b/integration/test/Testlib/Certs.hs @@ -32,10 +32,6 @@ privateKeyToString = toPem . keyToPEM PKCS8Format . PrivKeyRSA publicKeyToString :: RSA.PublicKey -> String publicKeyToString = toPem . pubKeyToPEM . PubKeyRSA --- | order: publickey, private key -keyPairToString :: RSAKeyPair -> (String, String) -keyPairToString = bimap publicKeyToString privateKeyToString - -- | the minimum key size is hard coded to be 256 bytes (= 2048 bits) mkKeyPair :: (HasCallStack) => (Integer, Integer) -> App RSAKeyPair mkKeyPair primes = @@ -57,21 +53,6 @@ primesB = 1030843359898456423663521323846594342599509001361505950190458094255790543792826808869649005832755187592625111972154015489882697017782849415061917844274039201990123282710414810809677284498651901967728601289390435426055251344683598043635553930587608961202440578033000424009931449958127951542294372025522185552538021557179009278446615246891375299863655746951224012338422185000952023195927317706092311999889180603374149659663869483313116251085191329801800565556652256960650364631610748235925879940728370511827034946814052737660926604082837303885143652256413187183052924192977324527952882600246973965189570970469037044568259408811931440525775822585332497163319841870179534838043708793539688804501356153704884928847627798172061867373042270416202913078776299057112318300845218218100606684092792088779583532324019862407866255929320869554565576301069075336647916168479092314004711778618335406757602974282533765740790546167166172626995630463716394043281720388344899550856555259477489548509996409954619324524195894460510128676025203769176155038527250084664954695197534485529595784255553806751541708069739004260117122700058054443774458724994738753921481706985581116480802534320353367271370286704034867136678539759260831996400891886615914808935283451835347282009482924185619896114631919985205238905153951336432886954324618000593140640843908517786951586431386674557882396487935889471856924185568502767114186884930347618747984770073080480895996031031971187681573023398782756925726725786964170460286504569090697402674905089317540771910375616350312239688178277204391962835159620450731320465816254229575392846112372636483958055913716148919092913102176828552932292829256960875180097808893909460952573027221089128208000054670526724565994184754244760290009957352237133054978847493874379201323517903544742831961755055100216728931496213920467911320372016970509300894067675803619448926461034580033818298648457643287641768005986812455071220244863874301028965665847375769473444088940776224643189987541019987285740411119351744972645543429351630677554481991322726604779330104110295967482897278840078926508970545806499140537364387530291523697762079684955475417383069988065253583073257131193644210418873929829417895241230927769637328283865111435730810586338426336027745629520975220163350734423915441885289661065494424704587153904031874537230782548938379423349488654701140981815973723582107593419642780372301171156324514852331126462907486017679770773972513376077318418003532168673261819818236071249 ) --- | create a root certificate authority CertificateBundle -createRootCA :: - (HasCallStack) => - -- | the root CA's name - String -> - -- | the root CA's keymaterial - RSAKeyPair -> - SignedCert -createRootCA caName (pubKey, privKey) = - mkSignedCert - pubKey - privKey - caName - caName - -- | sign an intermediate/ leaf certificate by signing with an intermediate/ root CA's key intermediateCert :: (HasCallStack) => diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 08ddcb5d965..6f1e7d4a2a9 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -11,8 +11,6 @@ import Data.Functor import Data.IORef import qualified Data.Map as Map import Data.Maybe (fromMaybe) -import Data.Set (Set) -import qualified Data.Set as Set import Data.Traversable (for) import qualified Data.Yaml as Yaml import qualified Database.CQL.IO as Cassandra @@ -161,18 +159,6 @@ mkEnv ge = do timeOutSeconds = ge.gTimeOutSeconds } -destroy :: IORef (Set BackendResource) -> BackendResource -> IO () -destroy ioRef = modifyIORef' ioRef . Set.insert - -create :: IORef (Set.Set BackendResource) -> IO BackendResource -create ioRef = - atomicModifyIORef - ioRef - $ \s -> - case Set.minView s of - Nothing -> error "No resources available" - Just (r, s') -> (s', r) - allCiphersuites :: [Ciphersuite] -- FUTUREWORK: add 0x0005 to this list once openmls supports it allCiphersuites = map Ciphersuite ["0x0001", "0xf031", "0x0002", "0x0007"] diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index 153a8008c79..14c285f964a 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -78,9 +78,6 @@ addQueryParams :: [(String, String)] -> HTTP.Request -> HTTP.Request addQueryParams params req = HTTP.setQueryString (map (\(k, v) -> (cs k, Just (cs v))) params) req -contentTypeJSON :: HTTP.Request -> HTTP.Request -contentTypeJSON = addHeader "Content-Type" "application/json" - contentTypeMixed :: HTTP.Request -> HTTP.Request contentTypeMixed = addHeader "Content-Type" "multipart/mixed" diff --git a/integration/test/Testlib/Prelude.hs b/integration/test/Testlib/Prelude.hs index 69c3797f54d..3bacfc4dd82 100644 --- a/integration/test/Testlib/Prelude.hs +++ b/integration/test/Testlib/Prelude.hs @@ -52,15 +52,6 @@ module Testlib.Prelude putStr, putStrLn, print, - getChar, - getLine, - getContents, - interact, - readFile, - writeFile, - appendFile, - readIO, - readLn, liftIO, -- * Functor @@ -186,33 +177,6 @@ putStrLn = liftIO . P.putStrLn print :: (Show a, MonadIO m) => a -> m () print = liftIO . P.print -getChar :: (MonadIO m) => m Char -getChar = liftIO P.getChar - -getLine :: (MonadIO m) => m String -getLine = liftIO P.getLine - -getContents :: (MonadIO m) => m String -getContents = liftIO P.getContents - -interact :: (MonadIO m) => (String -> String) -> m () -interact = liftIO . P.interact - -readFile :: (MonadIO m) => FilePath -> m String -readFile = liftIO . P.readFile - -writeFile :: (MonadIO m) => FilePath -> String -> m () -writeFile = fmap liftIO . P.writeFile - -appendFile :: (MonadIO m) => FilePath -> String -> m () -appendFile = fmap liftIO . P.appendFile - -readIO :: (Read a, MonadIO m) => String -> m a -readIO = liftIO . P.readIO - -readLn :: (Read a, MonadIO m) => m a -readLn = liftIO P.readLn - ---------------------------------------------------------------------- -- Functor diff --git a/libs/bilge/src/Bilge/RPC.hs b/libs/bilge/src/Bilge/RPC.hs index 77edab5326f..e07324e172a 100644 --- a/libs/bilge/src/Bilge/RPC.hs +++ b/libs/bilge/src/Bilge/RPC.hs @@ -23,7 +23,6 @@ module Bilge.RPC RPCException (..), rpc, rpc', - statusCheck, parseResponse, rpcExceptionMsg, ) @@ -34,7 +33,6 @@ import Bilge.Request import Bilge.Response import Control.Error hiding (err) import Control.Monad.Catch (MonadCatch, MonadThrow (..), try) -import Control.Monad.Except import Data.Aeson (FromJSON, eitherDecode') import Data.CaseInsensitive (original) import Data.Text.Lazy (pack) @@ -104,17 +102,6 @@ rpcExceptionMsg (RPCException sys req ex) = headers = foldr hdr id (HTTP.requestHeaders req) hdr (k, v) x = x ~~ original k .= v -statusCheck :: - (MonadError e m) => - Int -> - (LText -> e) -> - Response (Maybe LByteString) -> - m () -statusCheck c f r = - unless (statusCode r == c) $ - throwError $ - f ("unexpected status code: " <> pack (show $ statusCode r)) - parseResponse :: (Exception e, MonadThrow m, FromJSON a) => (LText -> e) -> diff --git a/libs/bilge/src/Bilge/TestSession.hs b/libs/bilge/src/Bilge/TestSession.hs index 246b7a17bcb..4f49c2d23e6 100644 --- a/libs/bilge/src/Bilge/TestSession.hs +++ b/libs/bilge/src/Bilge/TestSession.hs @@ -40,6 +40,3 @@ liftSession session = SessionT $ do let resultInState = runReaderT session app let resultInIO = ST.evalStateT resultInState clientState liftIO resultInIO - -runSessionT :: (Monad m) => SessionT m a -> Wai.Application -> m a -runSessionT session app = ST.evalStateT (runReaderT (unSessionT session) app) WaiTest.initState diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index 7f294c52fac..2f67b800eb5 100644 --- a/libs/brig-types/brig-types.cabal +++ b/libs/brig-types/brig-types.cabal @@ -148,13 +148,12 @@ test-suite brig-types-tests -Wunused-packages build-depends: - aeson >=2.0.1.0 - , base >=4 && <5 + aeson >=2.0.1.0 + , base >=4 && <5 , brig-types - , bytestring-conversion >=0.3.1 , imports , openapi3 - , QuickCheck >=2.9 + , QuickCheck >=2.9 , tasty , tasty-hunit , tasty-quickcheck diff --git a/libs/brig-types/default.nix b/libs/brig-types/default.nix index 78932b5d379..50587cd4eeb 100644 --- a/libs/brig-types/default.nix +++ b/libs/brig-types/default.nix @@ -43,7 +43,6 @@ mkDerivation { testHaskellDepends = [ aeson base - bytestring-conversion imports openapi3 QuickCheck diff --git a/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs b/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs index 13cfc3570e6..d7f91ce70c7 100644 --- a/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs +++ b/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs @@ -19,7 +19,6 @@ module Test.Brig.Roundtrip where import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) import Data.Aeson.Types (parseEither) -import Data.ByteString.Conversion import Data.OpenApi (ToSchema, validatePrettyToJSON) import Imports import Test.Tasty (TestTree) @@ -56,14 +55,3 @@ testRoundTripWithSwagger = testProperty msg (trip .&&. scm) validatePrettyToJSON v ) $ isNothing (validatePrettyToJSON v) - -testRoundTripByteString :: - forall a. - (Arbitrary a, Typeable a, ToByteString a, FromByteString a, Eq a, Show a) => - TestTree -testRoundTripByteString = testProperty msg trip - where - msg = show (typeRep @a) - trip (v :: a) = - counterexample (show $ toByteString' v) $ - Just v === (fromByteString . toByteString') v diff --git a/libs/cassandra-util/src/Cassandra/Exec.hs b/libs/cassandra-util/src/Cassandra/Exec.hs index 795083fe39d..c7d4c352a99 100644 --- a/libs/cassandra-util/src/Cassandra/Exec.hs +++ b/libs/cassandra-util/src/Cassandra/Exec.hs @@ -24,7 +24,6 @@ module Cassandra.Exec paramsP, x5, x1, - syncCassandra, paginateC, PageWithState (..), paginateWithState, @@ -80,15 +79,6 @@ data CassandraError | Other !SomeException deriving (Show) -syncCassandra :: (MonadIO m, MonadCatch m) => m a -> m (Either CassandraError a) -syncCassandra m = - catches - (Right <$> m) - [ Handler $ \(e :: Error) -> pure . Left . Cassandra $ e, - Handler $ \(e :: IOException) -> pure . Left . Comm $ e, - Handler $ \(e :: SomeException) -> pure . Left . Other $ e - ] - -- | Stream results of a query. -- -- You can execute this conduit by doing @transPipe (runClient ...)@. diff --git a/libs/cassandra-util/src/Cassandra/Helpers.hs b/libs/cassandra-util/src/Cassandra/Helpers.hs index 8a260d530b5..4c2834f7ffc 100644 --- a/libs/cassandra-util/src/Cassandra/Helpers.hs +++ b/libs/cassandra-util/src/Cassandra/Helpers.hs @@ -1,4 +1,4 @@ -module Cassandra.Helpers where +module Cassandra.Helpers (toOptionFieldName) where import Data.Aeson.TH import Imports diff --git a/libs/extended/default.nix b/libs/extended/default.nix index b47de8057a2..61f4643c17e 100644 --- a/libs/extended/default.nix +++ b/libs/extended/default.nix @@ -14,7 +14,6 @@ , data-default , errors , exceptions -, extra , gitignoreSource , hspec , hspec-discover @@ -25,7 +24,6 @@ , lib , metrics-wai , monad-control -, optparse-applicative , resourcet , retry , servant @@ -59,14 +57,12 @@ mkDerivation { data-default errors exceptions - extra http-client http-client-tls http-types imports metrics-wai monad-control - optparse-applicative resourcet retry servant diff --git a/libs/extended/extended.cabal b/libs/extended/extended.cabal index 03d180a004a..65ad7864014 100644 --- a/libs/extended/extended.cabal +++ b/libs/extended/extended.cabal @@ -22,7 +22,6 @@ library Data.Time.Clock.DiffTime Network.AMQP.Extended Network.RabbitMqAdmin - Options.Applicative.Extended Servant.API.Extended Servant.API.Extended.Endpath Servant.API.Extended.RawM @@ -90,14 +89,12 @@ library , data-default , errors , exceptions - , extra , http-client , http-client-tls , http-types , imports , metrics-wai , monad-control - , optparse-applicative , resourcet , retry , servant diff --git a/libs/extended/src/Data/Time/Clock/DiffTime.hs b/libs/extended/src/Data/Time/Clock/DiffTime.hs index 5541fd43d38..b84c9f9a95e 100644 --- a/libs/extended/src/Data/Time/Clock/DiffTime.hs +++ b/libs/extended/src/Data/Time/Clock/DiffTime.hs @@ -1,13 +1,7 @@ module Data.Time.Clock.DiffTime ( DiffTime, - weeksToDiffTime, - daysToDiffTime, - hoursToDiffTime, - minutesToDiffTime, secondsToDiffTime, millisecondsToDiffTime, - microsecondsToDiffTime, - nanosecondsToDiffTime, picosecondsToDiffTime, diffTimeToFullMicroseconds, diffTimeToPicoseconds, @@ -17,27 +11,14 @@ where import Data.Time import Imports -weeksToDiffTime, - daysToDiffTime, - hoursToDiffTime, - minutesToDiffTime, - millisecondsToDiffTime, - microsecondsToDiffTime, - nanosecondsToDiffTime :: - Integer -> DiffTime -weeksToDiffTime = daysToDiffTime . (7 *) -daysToDiffTime = hoursToDiffTime . (24 *) -hoursToDiffTime = minutesToDiffTime . (60 *) -minutesToDiffTime = secondsToDiffTime . (60 *) +-- we really should be doing all this with https://hackage.haskell.org/package/units... +millisecondsToDiffTime :: Integer -> DiffTime millisecondsToDiffTime = picosecondsToDiffTime . (e9 *) -microsecondsToDiffTime = picosecondsToDiffTime . (e6 *) -nanosecondsToDiffTime = picosecondsToDiffTime . (e3 *) -- | Rounds down. Useful for 'threadDelay', 'timeout', etc. diffTimeToFullMicroseconds :: DiffTime -> Int diffTimeToFullMicroseconds = fromInteger . (`div` e6) . diffTimeToPicoseconds -e3, e6, e9 :: Integer -e3 = 1_000 +e6, e9 :: Integer e6 = 1_000_000 e9 = 1_000_000_000 diff --git a/libs/extended/src/Options/Applicative/Extended.hs b/libs/extended/src/Options/Applicative/Extended.hs deleted file mode 100644 index 3a44fecb188..00000000000 --- a/libs/extended/src/Options/Applicative/Extended.hs +++ /dev/null @@ -1,41 +0,0 @@ -{-# LANGUAGE NoImplicitPrelude #-} - --- 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 . - --- | A version of "Options.Applicative" with extra utilities. -module Options.Applicative.Extended - ( module Options.Applicative, - - -- * Extra option readers - autoRange, - ) -where - -import Data.List.Extra (stripInfix) -import Imports -import Options.Applicative - --- | A reader that accepts either @N@ or @N..M@ (not necessarily just --- numbers). -autoRange :: (Read a) => ReadM (a, a) -autoRange = eitherReader $ \arg -> case stripInfix ".." arg of - Nothing -> (\a -> (a, a)) <$> readEither arg - Just (l, r) -> case (readEither l, readEither r) of - (Right lv, Right rv) -> Right (lv, rv) - (Left e, _) -> Left ("can't parse lower end: " <> e) - (_, Left e) -> Left ("can't parse upper end: " <> e) diff --git a/libs/galley-types/src/Galley/Types.hs b/libs/galley-types/src/Galley/Types.hs index b08103a22cd..1674d3bae0f 100644 --- a/libs/galley-types/src/Galley/Types.hs +++ b/libs/galley-types/src/Galley/Types.hs @@ -18,16 +18,13 @@ -- with this program. If not, see . module Galley.Types - ( foldrOtrRecipients, - Accept (..), + ( Accept (..), ) where import Data.Aeson -import Data.Id (ClientId, UserId) -import Data.Map.Strict qualified as Map +import Data.Id (UserId) import Imports -import Wire.API.Message -------------------------------------------------------------------------------- -- Accept @@ -47,14 +44,3 @@ instance ToJSON Accept where instance FromJSON Accept where parseJSON = withObject "accept" $ \o -> Accept <$> o .: "user" - --------------------------------------------------------------------------------- --- utility functions - -foldrOtrRecipients :: (UserId -> ClientId -> Text -> a -> a) -> a -> OtrRecipients -> a -foldrOtrRecipients f a = - Map.foldrWithKey go a - . userClientMap - . otrRecipientsMap - where - go u cs acc = Map.foldrWithKey (f u) acc cs diff --git a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs index 6c0df12d8a5..b9539ae81ac 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs +++ b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs @@ -35,7 +35,6 @@ module Gundeck.Types.Push.V2 pushNativeAps, pushNativePriority, pushPayload, - singletonRecipient, singletonPayload, Recipient (..), RecipientClients (..), @@ -79,7 +78,6 @@ import Data.Json.Util import Data.List1 import Data.List1 qualified as List1 import Data.Range -import Data.Range qualified as Range import Data.Set qualified as Set import Imports import Wire.API.Message (Priority (..)) @@ -273,9 +271,6 @@ newPush from to pload = _pushPayload = pload } -singletonRecipient :: Recipient -> Range 1 1024 (Set Recipient) -singletonRecipient = Range.unsafeRange . Set.singleton - singletonPayload :: (ToJSONObject a) => a -> List1 Object singletonPayload = List1.singleton . toJSONObject diff --git a/libs/metrics-wai/src/Data/Metrics/Servant.hs b/libs/metrics-wai/src/Data/Metrics/Servant.hs index 6d1df7d26ff..a66da6837a2 100644 --- a/libs/metrics-wai/src/Data/Metrics/Servant.hs +++ b/libs/metrics-wai/src/Data/Metrics/Servant.hs @@ -27,10 +27,8 @@ module Data.Metrics.Servant where import Data.ByteString.UTF8 qualified as UTF8 -import Data.Metrics.Middleware.Prometheus (normalizeWaiRequestRoute) import Data.Metrics.Types import Data.Metrics.Types qualified as Metrics -import Data.Metrics.WaiRoute (treeToPaths) import Data.Proxy import Data.Text.Encoding import Data.Text.Encoding.Error @@ -40,7 +38,6 @@ import Imports import Network.Wai qualified as Wai import Network.Wai.Middleware.Prometheus import Network.Wai.Middleware.Prometheus qualified as Promth -import Network.Wai.Routing (Routes, prepare) import Servant.API import Servant.Multipart @@ -57,18 +54,6 @@ servantPrometheusMiddleware _ = Promth.prometheus conf . instrument promthNormal -- See Note [Raw Response] instrument = Promth.instrumentHandlerValueWithFilter Promth.ignoreRawResponses -servantPlusWAIPrometheusMiddleware :: forall proxy api a m b. (RoutesToPaths api, Monad m) => Routes a m b -> proxy api -> Wai.Middleware -servantPlusWAIPrometheusMiddleware routes _ = do - Promth.prometheus conf . instrument (normalizeWaiRequestRoute paths) - where - -- See Note [Raw Response] - instrument = Promth.instrumentHandlerValueWithFilter Promth.ignoreRawResponses - - paths = - let Paths servantPaths = routesToPaths @api - Paths waiPaths = treeToPaths (prepare routes) - in Paths (meltTree (servantPaths <> waiPaths)) - conf :: PrometheusSettings conf = Promth.def diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Concurrency.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Concurrency.hs index 8cdf9f6600a..8d2b82bc758 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Concurrency.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Concurrency.hs @@ -5,7 +5,6 @@ module Wire.Sem.Concurrency where import Data.Kind (Type) import Imports import Polysemy -import Polysemy.Internal data ConcurrencySafety = Safe | Unsafe @@ -105,67 +104,3 @@ unsafePooledForConcurrentlyN_ n as f = send (UnsafePooledMapConcurrentlyN_ n f as :: Concurrency 'Unsafe (Sem r) ()) {-# INLINEABLE unsafePooledForConcurrentlyN_ #-} - -pooledMapConcurrentlyN :: - forall r' r t a b. - (r' ~ '[Final IO]) => - (Member (Concurrency 'Safe) r, Subsume r' r, Foldable t) => - -- | Max. number of threads. Should not be less than 1. - Int -> - (a -> Sem r' b) -> - t a -> - Sem r [b] -pooledMapConcurrentlyN n f as = - send - ( UnsafePooledMapConcurrentlyN n (subsume_ @r' @r . f) as :: - Concurrency 'Safe (Sem r) [b] - ) -{-# INLINEABLE pooledMapConcurrentlyN #-} - -pooledMapConcurrentlyN_ :: - forall r' r t a b. - (r' ~ '[Final IO]) => - (Member (Concurrency 'Safe) r, Subsume r' r, Foldable t) => - -- | Max. number of threads. Should not be less than 1. - Int -> - (a -> Sem r' b) -> - t a -> - Sem r () -pooledMapConcurrentlyN_ n f as = - send - ( UnsafePooledMapConcurrentlyN_ n (subsume_ @r' @r . f) as :: - Concurrency 'Safe (Sem r) () - ) -{-# INLINEABLE pooledMapConcurrentlyN_ #-} - -pooledForConcurrentlyN :: - forall r' r t a b. - (r' ~ '[Final IO]) => - (Member (Concurrency 'Safe) r, Subsume r' r, Foldable t) => - -- | Max. number of threads. Should not be less than 1. - Int -> - t a -> - (a -> Sem r' b) -> - Sem r [b] -pooledForConcurrentlyN n as f = - send - ( UnsafePooledMapConcurrentlyN n (subsume_ @r' @r . f) as :: - Concurrency 'Safe (Sem r) [b] - ) -{-# INLINEABLE pooledForConcurrentlyN #-} - -pooledForConcurrentlyN_ :: - forall r' r t a b. - (r' ~ '[Final IO]) => - (Member (Concurrency 'Safe) r, Subsume r' r, Foldable t) => - -- | Max. number of threads. Should not be less than 1. - Int -> - t a -> - (a -> Sem r' b) -> - Sem r () -pooledForConcurrentlyN_ n as f = - send - ( UnsafePooledMapConcurrentlyN_ n (subsume_ @r' @r . f) as :: - Concurrency 'Safe (Sem r) () - ) -{-# INLINEABLE pooledForConcurrentlyN_ #-} diff --git a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs index 7fb72b5ca33..44503d63123 100644 --- a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs +++ b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs @@ -31,12 +31,8 @@ module Test.Tasty.Cannon close, bracket, bracketAsClient, - bracketN, - bracketAsClientN, -- ** Random Connection IDs - connectR, - connectAsClientR, bracketR, bracketAsClientR, bracketR2, @@ -142,36 +138,8 @@ bracketAsClient :: bracketAsClient can uid client conn = Catch.bracket (connectAsClient can uid client conn) close -bracketN :: - (MonadIO m, MonadMask m) => - Cannon -> - [(UserId, ConnId)] -> - ([WebSocket] -> m a) -> - m a -bracketN c us f = go [] us - where - go wss [] = f (reverse wss) - go wss ((x, y) : xs) = bracket c x y (\ws -> go (ws : wss) xs) - -bracketAsClientN :: - (MonadMask m, MonadIO m) => - Cannon -> - [(UserId, ClientId, ConnId)] -> - ([WebSocket] -> m a) -> - m a -bracketAsClientN c us f = go [] us - where - go wss [] = f (reverse wss) - go wss ((x, y, z) : xs) = bracketAsClient c x y z (\ws -> go (ws : wss) xs) - -- Random Connection IDs -connectR :: (MonadIO m) => Cannon -> UserId -> m WebSocket -connectR can uid = randomConnId >>= connect can uid - -connectAsClientR :: (MonadIO m) => Cannon -> UserId -> ClientId -> m WebSocket -connectAsClientR can uid clientId = randomConnId >>= connectAsClient can uid clientId - bracketR :: (MonadIO m, MonadMask m) => Cannon -> UserId -> (WebSocket -> m a) -> m a bracketR can usr f = do cid <- randomConnId diff --git a/libs/types-common-aws/src/Util/Test/SQS.hs b/libs/types-common-aws/src/Util/Test/SQS.hs index 6a527482150..f9d015a1631 100644 --- a/libs/types-common-aws/src/Util/Test/SQS.hs +++ b/libs/types-common-aws/src/Util/Test/SQS.hs @@ -135,12 +135,6 @@ execute env = AWS.runResourceT . flip runReaderT env ----------------------------------------------------------------------------- -- Internal. Most of these functions _can_ be used outside of this function -- but probably do not need to -receive :: Int -> Text -> SQS.ReceiveMessage -receive n url = - SQS.newReceiveMessage url - & set SQS.receiveMessage_waitTimeSeconds (Just 1) - . set SQS.receiveMessage_maxNumberOfMessages (Just n) - . set SQS.receiveMessage_visibilityTimeout (Just 1) deleteMessage :: (MonadReader AWS.Env m, MonadResource m) => Text -> SQS.Message -> m () deleteMessage url m = do diff --git a/libs/types-common-journal/src/Data/Proto/Id.hs b/libs/types-common-journal/src/Data/Proto/Id.hs index 6210c8e78ac..8a0c3ed6a25 100644 --- a/libs/types-common-journal/src/Data/Proto/Id.hs +++ b/libs/types-common-journal/src/Data/Proto/Id.hs @@ -17,13 +17,10 @@ module Data.Proto.Id where -import Data.ByteString.Lazy (fromStrict, toStrict) +import Data.ByteString.Lazy (toStrict) import Data.Id import Data.UUID qualified as UUID import Imports toBytes :: Id a -> ByteString toBytes = toStrict . UUID.toByteString . toUUID - -fromBytes :: ByteString -> Maybe (Id a) -fromBytes = fmap Id . UUID.fromByteString . fromStrict diff --git a/libs/types-common/src/Data/Handle.hs b/libs/types-common/src/Data/Handle.hs index 64842b51e70..ce8c8f1a4ac 100644 --- a/libs/types-common/src/Data/Handle.hs +++ b/libs/types-common/src/Data/Handle.hs @@ -21,7 +21,6 @@ module Data.Handle ( Handle (fromHandle), parseHandle, parseHandleEither, - isValidHandle, BadHandle (..), ) where @@ -73,9 +72,6 @@ instance FromByteString Handle where parseHandle :: Text -> Maybe Handle parseHandle = either (const Nothing) Just . parseHandleEither -isValidHandle :: Text -> Bool -isValidHandle = isRight . parseHandleEither - parseHandleEither :: Text -> Either String Handle parseHandleEither = Atto.parseOnly (handleParser <* Atto.endOfInput) . Text.E.encodeUtf8 diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index 87ac386a7c4..c152fc6539c 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -43,7 +43,6 @@ module Data.Json.Util base64Schema, base64URLSchema, Base64ByteStringL (..), - base64SchemaL, fromBase64TextLenient, fromBase64Text, toBase64Text, @@ -269,9 +268,6 @@ instance S.ToParamSchema Base64ByteStringL where base64SchemaLN :: ValueSchema NamedSwaggerDoc LByteString base64SchemaLN = L.toStrict .= fmap L.fromStrict base64SchemaN -base64SchemaL :: ValueSchema SwaggerDoc LByteString -base64SchemaL = unnamed base64SchemaLN - -------------------------------------------------------------------------------- -- Utilities diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index 0d1632ad4b1..d6367a1f851 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -26,7 +26,6 @@ module Data.Qualified qToPair, QualifiedWithTag, tUnqualified, - tUnqualifiedL, tDomain, tUntagged, qTagUnsafe, @@ -48,7 +47,7 @@ module Data.Qualified ) where -import Control.Lens (Lens, lens, over, (?~)) +import Control.Lens (over, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bifunctor (first) import Data.Domain (Domain) @@ -93,9 +92,6 @@ tUnqualified = qUnqualified . tUntagged tDomain :: QualifiedWithTag t a -> Domain tDomain = qDomain . tUntagged -tUnqualifiedL :: Lens (QualifiedWithTag t a) (QualifiedWithTag t b) a b -tUnqualifiedL = lens tUnqualified qualifyAs - -- | A type representing a 'Qualified' value where the domain is guaranteed to -- be remote. type Remote = QualifiedWithTag 'QRemote diff --git a/libs/types-common/src/Data/Text/Ascii.hs b/libs/types-common/src/Data/Text/Ascii.hs index 7f167d611cd..2299facd5c7 100644 --- a/libs/types-common/src/Data/Text/Ascii.hs +++ b/libs/types-common/src/Data/Text/Ascii.hs @@ -38,7 +38,6 @@ module Data.Text.Ascii -- * Standard Characters Standard (..), Ascii, - validateStandard, -- * Printable Characters Printable (..), @@ -67,10 +66,6 @@ module Data.Text.Ascii encodeBase16, decodeBase16, - -- * Safe Widening - widen, - widenChar, - -- * Unsafe Construction unsafeFromText, unsafeFromByteString, @@ -198,9 +193,6 @@ instance AsciiChars Standard where contains Standard = isAscii {-# INLINE contains #-} -validateStandard :: Text -> Either String Ascii -validateStandard = validate - -------------------------------------------------------------------------------- -- Printable @@ -364,19 +356,6 @@ encodeBase16 = unsafeFromByteString . B16.encode decodeBase16 :: AsciiBase16 -> Maybe ByteString decodeBase16 t = either (const Nothing) Just (B16.decode (toByteString' t)) --------------------------------------------------------------------------------- --- Safe Widening - --- | Safely widen an ASCII text into another ASCII text with a larger --- character set. -widen :: (Subset c c' ~ 'True) => AsciiText c -> AsciiText c' -widen (AsciiText t) = AsciiText t - --- | Safely widen an ASCII character into another ASCII character with a larger --- character set. -widenChar :: (Subset c c' ~ 'True) => AsciiChar c -> AsciiChar c' -widenChar (AsciiChar t) = AsciiChar t - -------------------------------------------------------------------------------- -- Unsafe Construction diff --git a/libs/types-common/src/Data/UUID/Tagged.hs b/libs/types-common/src/Data/UUID/Tagged.hs index fa6eb11ce5f..14aceb1f5d2 100644 --- a/libs/types-common/src/Data/UUID/Tagged.hs +++ b/libs/types-common/src/Data/UUID/Tagged.hs @@ -22,17 +22,14 @@ module Data.UUID.Tagged V5, Version (..), version, - variant, addv4, unpack, - create, mk, ) where import Data.Bits import Data.UUID qualified as D -import Data.UUID.V4 qualified as D4 import Imports -- | Versioned UUID. @@ -68,10 +65,6 @@ mk u = UUID $ (retainVariant 2 x2) x3 --- | Create a fresh UUIDv4. -create :: IO (UUID V4) -create = UUID <$> D4.nextRandom - -- | Extract the 'D.UUID' from a versioned UUID. unpack :: UUID v -> D.UUID unpack (UUID x) = x @@ -100,12 +93,6 @@ version u = let (_, x, _, _) = D.toWords u in (x .&. 0x0000F000) `shiftR` 12 --- | Tell the variant of a 'D.UUID' value. -variant :: D.UUID -> Word32 -variant u = - let (_, _, x, _) = D.toWords u - in (x .&. 0xC0000000) `shiftR` 30 - -- Internal: retainVersion :: Word32 -> Word32 -> Word32 diff --git a/libs/types-common/src/Util/Options.hs b/libs/types-common/src/Util/Options.hs index 2d46e74097a..f82600dc00b 100644 --- a/libs/types-common/src/Util/Options.hs +++ b/libs/types-common/src/Util/Options.hs @@ -37,7 +37,6 @@ import Imports import Options.Applicative import Options.Applicative.Types import URI.ByteString -import Util.Options.Common data AWSEndpoint = AWSEndpoint { _awsHost :: !ByteString, @@ -147,10 +146,3 @@ getOptions desc mp defaultPath = do parseAWSEndpoint :: ReadM AWSEndpoint parseAWSEndpoint = readerAsk >>= maybe (error "Could not parse AWS endpoint") pure . fromByteString . fromString - -discoUrlParser :: Parser Text -discoUrlParser = - textOption $ - long "disco-url" - <> metavar "URL" - <> help "klabautermann url" diff --git a/libs/types-common/src/Util/Options/Common.hs b/libs/types-common/src/Util/Options/Common.hs index 14b997bee7e..e7b3eaf3dac 100644 --- a/libs/types-common/src/Util/Options/Common.hs +++ b/libs/types-common/src/Util/Options/Common.hs @@ -22,10 +22,7 @@ module Util.Options.Common where import Cassandra.Helpers (toOptionFieldName) -import Data.ByteString.Char8 qualified as C -import Data.Text qualified as T import Imports hiding (reader) -import Options.Applicative import System.Posix.Env qualified as Posix optOrEnv :: (a -> b) -> Maybe a -> (String -> b) -> String -> IO b @@ -37,9 +34,3 @@ optOrEnvSafe :: (a -> b) -> Maybe a -> (String -> b) -> String -> IO (Maybe b) optOrEnvSafe getter conf reader var = case conf of Nothing -> fmap reader <$> Posix.getEnv var Just c -> pure $ Just (getter c) - -bytesOption :: Mod OptionFields String -> Parser ByteString -bytesOption = fmap C.pack . strOption - -textOption :: Mod OptionFields String -> Parser Text -textOption = fmap T.pack . strOption diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs index aea3d8b41f3..09abc767e1a 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Error.hs @@ -23,7 +23,6 @@ module Network.Wai.Utilities.Error ErrorData (..), mkError, (!>>), - byteStringError, ) where @@ -31,7 +30,6 @@ import Control.Error import Data.Aeson hiding (Error) import Data.Aeson.Types (Pair) import Data.Domain -import Data.Text.Lazy.Encoding (decodeUtf8) import Imports import Network.HTTP.Types @@ -69,10 +67,6 @@ instance FromJSON ErrorData where <$> o .: "domain" <*> o .: "path" --- | Assumes UTF-8 encoding. -byteStringError :: Status -> LByteString -> LByteString -> Error -byteStringError s l m = mkError s (decodeUtf8 l) (decodeUtf8 m) - instance ToJSON Error where toJSON (Error c l m md inner) = object $ diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs index 7da25d4449b..2450bfd7b47 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs @@ -23,7 +23,6 @@ module Network.Wai.Utilities.Request where import Control.Error -import Control.Monad.Catch (MonadThrow, throwM) import Data.Aeson import Data.ByteString qualified as B import Data.ByteString.Lazy qualified as Lazy @@ -32,10 +31,7 @@ import Data.Text.Lazy qualified as Text import Imports import Network.HTTP.Types import Network.Wai -import Network.Wai.Predicate import Network.Wai.Predicate.Request -import Network.Wai.Utilities.Error qualified as Wai -import Network.Wai.Utilities.ZAuth ((.&>)) import Pipes import Pipes.Prelude qualified as P @@ -54,21 +50,6 @@ parseBody :: ExceptT LText m a parseBody r = readBody r >>= hoistEither . fmapL Text.pack . eitherDecode' -parseBody' :: (FromJSON a, MonadIO m, MonadThrow m) => JsonRequest a -> m a -parseBody' r = either thrw pure =<< runExceptT (parseBody r) - where - thrw msg = throwM $ Wai.mkError status400 "bad-request" msg - -parseOptionalBody :: - (MonadIO m, FromJSON a) => - OptionalJsonRequest a -> - ExceptT LText m (Maybe a) -parseOptionalBody r = - hoistEither . fmapL Text.pack . traverse eitherDecode' . nonEmptyBody =<< readBody r - where - nonEmptyBody "" = Nothing - nonEmptyBody ne = Just ne - lookupRequestId :: HeaderName -> Request -> Maybe ByteString lookupRequestId reqIdHeaderName = lookup reqIdHeaderName . requestHeaders @@ -82,24 +63,8 @@ getRequestId reqIdHeaderName req = newtype JsonRequest body = JsonRequest {fromJsonRequest :: Request} -jsonRequest :: - forall body r. - (HasRequest r, HasHeaders r) => - Predicate r Error (JsonRequest body) -jsonRequest = - contentType "application" "json" - .&> (pure . JsonRequest . getRequest) - newtype OptionalJsonRequest body = OptionalJsonRequest {fromOptionalJsonRequest :: Request} -optionalJsonRequest :: - forall body r. - (HasRequest r, HasHeaders r) => - Predicate r Error (OptionalJsonRequest body) -optionalJsonRequest = - opt (contentType "application" "json") - .&> (pure . OptionalJsonRequest . getRequest) - ---------------------------------------------------------------------------- -- Instances diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Response.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Response.hs index ce838ff5463..bb27d08fdee 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Response.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Response.hs @@ -30,9 +30,6 @@ import Network.Wai.Utilities.Error empty :: Response empty = plain "" -noContent :: Response -noContent = empty & setStatus status204 - plain :: Lazy.ByteString -> Response plain = responseLBS status200 [plainContent] @@ -45,11 +42,8 @@ json = responseLBS status200 [jsonContent] . encode jsonContent :: Header jsonContent = (hContentType, "application/json") -errorRs :: Status -> LText -> LText -> Response -errorRs s l m = errorRs' (mkError s l m) - -errorRs' :: Error -> Response -errorRs' e = setStatus (code e) (json e) +errorRs :: Error -> Response +errorRs e = setStatus (code e) (json e) addHeader :: HeaderName -> ByteString -> Response -> Response addHeader k v (ResponseFile s h f ff) = ResponseFile s ((k, v) : h) f ff diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs index 9186ed78333..dd3306f4a65 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs @@ -42,7 +42,6 @@ module Network.Wai.Utilities.Server logError, logError', logErrorMsg, - restrict, flushRequestBody, -- * Constants @@ -185,7 +184,7 @@ compile routes = Route.prepare (Route.renderer predicateError >> routes) messageStr Nothing = mempty route :: (MonadIO m) => Tree (App m) -> Request -> Continue IO -> m ResponseReceived -route rt rq k = Route.routeWith (Route.Config $ errorRs' noEndpoint) rt rq (liftIO . k) +route rt rq k = Route.routeWith (Route.Config $ errorRs noEndpoint) rt rq (liftIO . k) where noEndpoint = Wai.mkError status404 "no-endpoint" "The requested endpoint does not exist" {-# INLINEABLE route #-} @@ -469,22 +468,6 @@ runHandlers :: SomeException -> [Handler IO a] -> IO a runHandlers e [] = throwIO e runHandlers e (Handler h : hs) = maybe (runHandlers e hs) h (fromException e) -restrict :: Int -> Int -> Predicate r P.Error Int -> Predicate r P.Error Int -restrict l u = fmap $ \x -> - x >>= \v -> - if v >= l && v <= u - then x - else Fail (setMessage (emsg v) . setReason TypeError $ e400) - where - emsg v = - LBS.toStrict . toLazyByteString $ - byteString "outside range [" - <> intDec l - <> byteString ", " - <> intDec u - <> byteString "]: " - <> intDec v - flushRequestBody :: Request -> IO () flushRequestBody req = do bs <- getRequestBodyChunk req diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/ZAuth.hs b/libs/wai-utilities/src/Network/Wai/Utilities/ZAuth.hs index 5733203a0bd..a96a16f2032 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/ZAuth.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/ZAuth.hs @@ -19,25 +19,14 @@ module Network.Wai.Utilities.ZAuth ( ZAuthType (..), - zauthType, - zauth, - zauthUserId, - zauthConnId, - zauthBotId, - zauthConvId, - zauthProviderId, (<&.), (.&>), ) where import Data.ByteString.Conversion -import Data.Id import Imports -import Network.HTTP.Types.Header -import Network.HTTP.Types.Status import Network.Wai.Predicate -import Network.Wai.Predicate.Request -- ZAuth headers -------------------------------------------------------------- @@ -65,40 +54,6 @@ instance FromByteString ZAuthType where "provider" -> pure ZAuthProvider _ -> fail $ "Invalid ZAuth type: " ++ show t --- | A token type is present if the request was authenticated. -zauthType :: (HasHeaders r) => Predicate r Error ZAuthType -zauthType = zheader "Z-Type" - --- | Require a specific token type to be used. -zauth :: (HasHeaders r) => ZAuthType -> Predicate r Error () -zauth t = do - r <- zauthType - pure $ case r of - Okay _ z | z == t -> Okay 0 () - _ -> Fail accessDenied - --- | A zauth user ID is present if 'zauthType' is either 'ZAuthAccess' --- or 'ZAuthUser'. -zauthUserId :: (HasHeaders r) => Predicate r Error UserId -zauthUserId = zheader "Z-User" - --- | A zauth connection ID is present if 'zauthType' is 'ZAuthAccess'. -zauthConnId :: (HasHeaders r) => Predicate r Error ConnId -zauthConnId = zheader "Z-Connection" - --- | A zauth bot ID is present if 'zauthType' is 'ZAuthBot'. -zauthBotId :: (HasHeaders r) => Predicate r Error BotId -zauthBotId = zheader "Z-Bot" - --- | A zauth conversation ID is present if 'zauthType' is 'ZAuthBot'. -zauthConvId :: (HasHeaders r) => Predicate r Error ConvId -zauthConvId = zheader "Z-Conversation" - --- | A provider ID is present if 'zauthType' is either 'ZAuthBot' --- or 'ZAuthProvider'. -zauthProviderId :: (HasHeaders r) => Predicate r Error ProviderId -zauthProviderId = zheader "Z-Provider" - -- Extra Predicate Combinators ------------------------------------------------ -- Variations of '.&.' that keep only the result of the left or right @@ -114,14 +69,3 @@ infixr 3 .&> (.&>) :: Predicate a f t -> Predicate a f t' -> Predicate a f t' (.&>) a b = fmap (fmap tl) (a .&. b) - --- Internal ------------------------------------------------------------------- - --- | Missing or invalid zauth-related headers due to a misconfiguration --- between the zauth ACL and / or API handlers should yield an opaque 403 --- error, in order not to leak such details to clients on public API endpoints. -zheader :: (HasHeaders r, FromByteString a) => HeaderName -> Predicate r Error a -zheader = fmap (result (Fail . const accessDenied) Okay) . header - -accessDenied :: Error -accessDenied = setMessage "Access denied" (err status403) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API.hs b/libs/wire-api-federation/src/Wire/API/Federation/API.hs index bf33723b172..1c45da47edf 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -29,7 +29,6 @@ module Wire.API.Federation.API fedQueueClient, sendBundle, fedClientIn, - unsafeFedClientIn, module Wire.API.MakesFederatedCall, -- * Re-exports @@ -165,11 +164,3 @@ fedQueueClient :: Payload tag -> FedQueueClient c () fedQueueClient payload = sendBundle =<< makeBundle @tag payload - --- | Like 'fedClientIn', but doesn't propagate a 'CallsFed' constraint. Intended --- to be used in test situations only. -unsafeFedClientIn :: - forall (comp :: Component) (name :: Symbol) m api. - (HasUnsafeFedEndpoint comp api name, HasClient m api) => - Client m api -unsafeFedClientIn = clientIn (Proxy @api) (Proxy @m) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs index 2e3f4b8d488..7cde4f2b733 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs @@ -160,9 +160,6 @@ routingKey t = "backend-notifications." <> t -- they are stored in Rabbit. type DefederationDomain = Domain -defederationQueue :: Text -defederationQueue = "delete-federation" - -- | If you ever change this function and modify -- queue parameters, know that it will start failing in the -- next release! So be prepared to write migrations. diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Component.hs b/libs/wire-api-federation/src/Wire/API/Federation/Component.hs index aef5cc95980..dcf029f1d8a 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Component.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Component.hs @@ -25,12 +25,6 @@ import Data.Proxy import Imports import Wire.API.MakesFederatedCall (Component (..)) -parseComponent :: Text -> Maybe Component -parseComponent "brig" = Just Brig -parseComponent "galley" = Just Galley -parseComponent "cargohold" = Just Cargohold -parseComponent _ = Nothing - componentName :: Component -> Text componentName Brig = "brig" componentName Galley = "galley" 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 c6f14413058..d10d00e6c4b 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs @@ -32,17 +32,14 @@ module Wire.API.Federation.Version -- * VersionRange VersionUpperBound (..), VersionRange (..), - fromVersion, - toVersionExcl, allVersions, latestCommonVersion, rangeFromVersion, rangeUntilVersion, - enumVersionRange, ) where -import Control.Lens (makeLenses, (?~)) +import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.OpenApi qualified as S import Data.Schema @@ -131,8 +128,6 @@ deriving instance Show VersionRange deriving instance Ord VersionRange -makeLenses ''VersionRange - instance ToSchema VersionRange where schema = object "VersionRange" $ @@ -165,12 +160,6 @@ rangeFromVersion v = VersionRange v Unbounded rangeUntilVersion :: Version -> VersionRange rangeUntilVersion v = VersionRange minBound (VersionUpperBound v) -enumVersionRange :: VersionRange -> Set Version -enumVersionRange = - Set.fromList . \case - VersionRange l Unbounded -> [l ..] - VersionRange l (VersionUpperBound u) -> init [l .. u] - -- | For a version range of a local backend and for a set of versions that a -- remote backend supports, compute the newest version supported by both. The -- remote versions are given as integers as the range of versions supported by diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs index 27fba120068..17842d6df14 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/MLSMessageSendingStatus.hs @@ -17,11 +17,7 @@ module Test.Wire.API.Federation.Golden.MLSMessageSendingStatus where -import Data.Domain -import Data.Id import Data.Json.Util -import Data.Qualified -import Data.UUID qualified as UUID import Imports import Wire.API.MLS.Message @@ -45,16 +41,3 @@ testObject_MLSMessageSendingStatus3 = { mmssEvents = [], mmssTime = toUTCTimeMillis (read "1999-04-12 12:22:43.673 UTC") } - -failed1 :: [Qualified UserId] -failed1 = - let domain = Domain "offline.example.com" - in [Qualified (Id . fromJust . UUID.fromString $ "00000000-0000-0000-0000-000200000008") domain] - -failed2 :: [Qualified UserId] -failed2 = - let domain = Domain "golden.example.com" - in flip Qualified domain . Id . fromJust . UUID.fromString - <$> [ "00000000-0000-0000-0000-000200000008", - "00000000-0000-0000-0000-000100000007" - ] diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index b48d771e20d..889b8ffd1bf 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -46,16 +46,10 @@ module Wire.API.Call.Config turiTransport, Transport (..), TurnHost (..), - isHostName, -- * SFTUsername SFTUsername, mkSFTUsername, - suExpiresAt, - suVersion, - suKeyindex, - suShared, - suRandom, -- * TurnUsername TurnUsername, @@ -409,10 +403,6 @@ instance Arbitrary TurnHost where "xn--mgbh0fb.xn--kgbechtv" ] -isHostName :: TurnHost -> Bool -isHostName (TurnHostIp _) = False -isHostName (TurnHostName _) = True - parseTurnHost :: Text -> Maybe TurnHost parseTurnHost h = case BC.fromByteString host of Just ip@(IpAddr _) -> Just $ TurnHostIp ip @@ -645,7 +635,6 @@ isTls uri = makeLenses ''RTCConfiguration makeLenses ''RTCIceServer makeLenses ''TurnURI -makeLenses ''SFTUsername makeLenses ''TurnUsername makeLenses ''SFTServer makeLenses ''AuthSFTServer diff --git a/libs/wire-api/src/Wire/API/ConverProtoLens.hs b/libs/wire-api/src/Wire/API/ConverProtoLens.hs deleted file mode 100644 index 6e4398c47f7..00000000000 --- a/libs/wire-api/src/Wire/API/ConverProtoLens.hs +++ /dev/null @@ -1,33 +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.ConverProtoLens where - -import Data.Bifunctor (Bifunctor (first)) -import Imports - --- | This typeclass exists to provide overloaded function names for convertion --- between data types generated by proto-lens and data types used in wire --- We added fundeps here for better type inference, but we can't be as explicit as we wanted --- with @a -> b, b -> a@, since our instances would be orphaned on the left hand side argument. -class ConvertProtoLens a b | b -> a where - fromProtolens :: a -> Either Text b - toProtolens :: b -> a - --- | Add labels to error messages -protoLabel :: Text -> Either Text a -> Either Text a -protoLabel lbl = first ((lbl <> ": ") <>) diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 0aa78bd25c6..e4184cb1d2d 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -68,7 +68,6 @@ module Wire.API.Conversation -- * invite Invite (..), InviteQualified (..), - newInvite, -- * update ConversationRename (..), @@ -805,9 +804,6 @@ instance ToSchema InviteQualified where <*> invQRoleName .= (fromMaybe roleNameWireAdmin <$> optField "conversation_role" schema) -newInvite :: List1 UserId -> Invite -newInvite us = Invite us roleNameWireAdmin - -------------------------------------------------------------------------------- -- update diff --git a/libs/wire-api/src/Wire/API/Conversation/Member.hs b/libs/wire-api/src/Wire/API/Conversation/Member.hs index 1443e158af8..7d3636f3f40 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Member.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Member.hs @@ -26,7 +26,6 @@ module Wire.API.Conversation.Member defMember, MutedStatus (..), OtherMember (..), - defOtherMember, -- * Member Update MemberUpdate (..), @@ -150,14 +149,6 @@ data OtherMember = OtherMember deriving (Arbitrary) via (GenericUniform OtherMember) deriving (FromJSON, ToJSON, S.ToSchema) via Schema OtherMember -defOtherMember :: Qualified UserId -> OtherMember -defOtherMember uid = - OtherMember - { omQualifiedId = uid, - omService = Nothing, - omConvRoleName = roleNameWireMember - } - instance ToSchema OtherMember where schema = object "OtherMember" $ diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index c0060347b7b..9e213a26fdd 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -28,7 +28,6 @@ module Wire.API.Conversation.Protocol _ProtocolMLS, _ProtocolMixed, _ProtocolProteus, - conversationMLSData, protocolSchema, ConversationMLSData (..), ActiveMLSConversationData (..), @@ -40,7 +39,7 @@ where import Control.Applicative import Control.Arrow -import Control.Lens (Traversal', makePrisms, (?~)) +import Control.Lens (makePrisms, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Json.Util import Data.OpenApi qualified as S @@ -201,11 +200,6 @@ data Protocol $(makePrisms ''Protocol) -conversationMLSData :: Traversal' Protocol ConversationMLSData -conversationMLSData _ ProtocolProteus = pure ProtocolProteus -conversationMLSData f (ProtocolMLS mls) = ProtocolMLS <$> f mls -conversationMLSData f (ProtocolMixed mls) = ProtocolMixed <$> f mls - protocolTag :: Protocol -> ProtocolTag protocolTag ProtocolProteus = ProtocolProteusTag protocolTag (ProtocolMLS _) = ProtocolMLSTag diff --git a/libs/wire-api/src/Wire/API/Conversation/Role.hs b/libs/wire-api/src/Wire/API/Conversation/Role.hs index edb97c23f42..c22cccf72c1 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Role.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Role.hs @@ -33,7 +33,6 @@ module Wire.API.Conversation.Role RoleName, fromRoleName, parseRoleName, - wireConvRoleNames, roleNameWireAdmin, roleNameWireMember, @@ -246,9 +245,6 @@ instance Arbitrary RoleName where where genChar = QC.elements $ ['a' .. 'z'] <> ['0' .. '9'] <> ['_'] -wireConvRoleNames :: [RoleName] -wireConvRoleNames = [roleNameWireAdmin, roleNameWireMember] - roleNameWireAdmin :: RoleName roleNameWireAdmin = RoleName "wire_admin" diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index a286a02d0a1..5f93d01f8c3 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -30,9 +30,7 @@ module Wire.API.MLS.CipherSuite IsSignatureScheme, SignatureSchemeTag (..), SignatureSchemeCurve, - signatureScheme, signatureSchemeName, - signatureSchemeTag, csSignatureScheme, -- * Key pairs @@ -282,9 +280,6 @@ newtype SignatureScheme = SignatureScheme {unSignatureScheme :: Word16} deriving stock (Eq, Show) deriving newtype (ParseMLS, Arbitrary) -signatureScheme :: SignatureSchemeTag -> SignatureScheme -signatureScheme = SignatureScheme . signatureSchemeNumber - data SignatureSchemeTag = Ed25519 | Ecdsa_secp256r1_sha256 @@ -330,23 +325,12 @@ instance Cql SignatureSchemeTag where signatureSchemeFromName name fromCql _ = Left "SignatureScheme: Text expected" -signatureSchemeNumber :: SignatureSchemeTag -> Word16 -signatureSchemeNumber Ed25519 = 0x807 -signatureSchemeNumber Ecdsa_secp256r1_sha256 = 0x403 -signatureSchemeNumber Ecdsa_secp384r1_sha384 = 0x503 -signatureSchemeNumber Ecdsa_secp521r1_sha512 = 0x603 - signatureSchemeName :: SignatureSchemeTag -> Text signatureSchemeName Ed25519 = "ed25519" signatureSchemeName Ecdsa_secp256r1_sha256 = "ecdsa_secp256r1_sha256" signatureSchemeName Ecdsa_secp384r1_sha384 = "ecdsa_secp384r1_sha384" signatureSchemeName Ecdsa_secp521r1_sha512 = "ecdsa_secp521r1_sha512" -signatureSchemeTag :: SignatureScheme -> Maybe SignatureSchemeTag -signatureSchemeTag (SignatureScheme n) = getAlt $ - flip foldMap [minBound .. maxBound] $ \s -> - guard (signatureSchemeNumber s == n) $> s - signatureSchemeFromName :: Text -> Maybe SignatureSchemeTag signatureSchemeFromName name = getAlt $ flip foldMap [minBound .. maxBound] $ \s -> diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs index 342bb739e23..cb1003ab8fe 100644 --- a/libs/wire-api/src/Wire/API/MLS/Message.hs +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -30,9 +30,6 @@ module Wire.API.MLS.Message FramedContentAuthData (..), Sender (..), - -- * Utilities - verifyMessageSignature, - -- * Servant types MLSMessageSendingStatus (..), ) @@ -48,7 +45,6 @@ import GHC.Records import Imports import Test.QuickCheck hiding (label) import Wire.API.Event.Conversation -import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.Epoch import Wire.API.MLS.Group @@ -238,11 +234,6 @@ instance SerialiseMLS Sender where serialiseMLS SenderNewMemberCommit = serialiseMLS SenderNewMemberCommitTag -needsGroupContext :: Sender -> Bool -needsGroupContext (SenderMember _) = True -needsGroupContext (SenderExternal _) = True -needsGroupContext _ = False - -- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6-4 data FramedContent = FramedContent { groupId :: GroupId, @@ -329,15 +320,6 @@ instance SerialiseMLS FramedContentTBS where serialiseMLS tbs.content traverse_ serialiseMLS tbs.groupContext -framedContentTBS :: RawMLS GroupContext -> RawMLS FramedContent -> FramedContentTBS -framedContentTBS ctx msgContent = - FramedContentTBS - { protocolVersion = defaultProtocolVersion, - wireFormat = WireFormatPublicTag, - content = msgContent, - groupContext = guard (needsGroupContext msgContent.value.sender) $> ctx - } - -- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6.1-2 data FramedContentAuthData = FramedContentAuthData { signature_ :: ByteString, @@ -359,18 +341,6 @@ instance SerialiseMLS FramedContentAuthData where serialiseMLSBytes @VarInt ad.signature_ traverse_ (serialiseMLSBytes @VarInt) ad.confirmationTag -verifyMessageSignature :: - RawMLS GroupContext -> - RawMLS FramedContent -> - RawMLS FramedContentAuthData -> - ByteString -> - Bool -verifyMessageSignature ctx msgContent authData pubkey = isJust $ do - let tbs = mkRawMLS (framedContentTBS ctx msgContent) - sig = authData.value.signature_ - cs <- cipherSuiteTag ctx.value.cipherSuite - guard $ csVerifySignature cs pubkey tbs sig - -------------------------------------------------------------------------------- -- Servant diff --git a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs index 618d26201bf..ca6783cd192 100644 --- a/libs/wire-api/src/Wire/API/MLS/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Serialisation.hs @@ -23,7 +23,6 @@ module Wire.API.MLS.Serialisation SerialiseMLS (..), VarInt (..), parseMLSStream, - serialiseMLSStream, parseMLSVector, serialiseMLSVector, parseMLSBytes, @@ -129,9 +128,6 @@ parseMLSStream p = do then pure [] else (:) <$> p <*> parseMLSStream p -serialiseMLSStream :: (a -> Put) -> [a] -> Put -serialiseMLSStream = traverse_ - parseMLSVector :: forall w a. (Binary w, Integral w) => Get a -> Get [a] parseMLSVector getItem = do len <- get @w diff --git a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs index 2a59e5648fb..a076e39ea85 100644 --- a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs +++ b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs @@ -22,7 +22,6 @@ module Wire.API.MakesFederatedCall MakesFederatedCall, Component (..), callsFed, - unsafeCallsFed, AddAnnotation, Location (..), ShowComponent, @@ -218,14 +217,6 @@ instance (c ~ ((k, d) :: Constraint), SolveCallsFed d r a) => SolveCallsFed c r instance {-# OVERLAPPABLE #-} (c ~ (() :: Constraint), r ~ a) => SolveCallsFed c r a where callsFed f = f --- | Unsafely discharge a 'CallsFed' constraint. Necessary for interacting with --- wai-routes. --- --- This is unsafe in the sense that it will drop the 'CallsFed' constraint, and --- thus might mean a federated call gets forgotten in the documentation. -unsafeCallsFed :: forall (comp :: Component) (name :: Symbol) r. ((CallsFed comp name) => r) -> r -unsafeCallsFed f = withDict (synthesizeCallsFed @comp @name) f - data FedCallFrom' f = FedCallFrom { name :: f String, method :: f String, diff --git a/libs/wire-api/src/Wire/API/Message/Proto.hs b/libs/wire-api/src/Wire/API/Message/Proto.hs index d20ecd75ab6..21e698b0abd 100644 --- a/libs/wire-api/src/Wire/API/Message/Proto.hs +++ b/libs/wire-api/src/Wire/API/Message/Proto.hs @@ -24,7 +24,6 @@ module Wire.API.Message.Proto userId, fromUserId, ClientId, - clientId, newClientId, fromClientId, toClientId, @@ -86,9 +85,6 @@ instance Decode ClientId newClientId :: Word64 -> ClientId newClientId c = ClientId {_client = putField c} -clientId :: (Functor f) => (Word64 -> f Word64) -> ClientId -> f ClientId -clientId f c = (\x -> c {_client = x}) <$> field f (_client c) - toClientId :: ClientId -> Id.ClientId toClientId c = Id.ClientId $ getField (_client c) diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index ec44311dece..4b0d8e1c848 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -31,8 +31,6 @@ module Wire.API.Provider.Service.Tag -- * ServiceTag Matchers MatchAny (..), MatchAll (..), - (.||.), - (.&&.), matchAll, match1, match, @@ -305,12 +303,6 @@ newtype MatchAll = MatchAll {matchAllSet :: Set ServiceTag} deriving stock (Eq, Show, Ord) -(.||.) :: MatchAny -> MatchAny -> MatchAny -(.||.) (MatchAny a) (MatchAny b) = MatchAny (Set.union a b) - -(.&&.) :: MatchAll -> MatchAll -> MatchAll -(.&&.) (MatchAll a) (MatchAll b) = MatchAll (Set.union a b) - matchAll :: MatchAll -> MatchAny matchAll = MatchAny . Set.singleton diff --git a/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs b/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs index 74257a99e66..6b5463bfdb9 100644 --- a/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs +++ b/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs @@ -48,8 +48,6 @@ makePrisms ''FederationRestriction data FederationRestrictionTag = FederationRestrictionAllowAllTag | FederationRestrictionByTeamTag deriving (Eq, Enum, Bounded) -makePrisms ''FederationRestrictionTag - deriving via Schema FederationRestriction instance (S.ToSchema FederationRestriction) deriving via Schema FederationRestriction instance (FromJSON FederationRestriction) diff --git a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs index 0ee626b9d98..bb972553319 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs @@ -544,9 +544,6 @@ instance (ResponseType r ~ a) => AsUnion '[r] a where toUnion = Z . I fromUnion = unI . unZ -_foo :: Union '[Int] -_foo = toUnion @'[Respond 200 "test" Int] @Int 3 - class InjectAfter as bs where injectAfter :: Union bs -> Union (as .++ bs) 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 cd797101f11..0b48d00ad53 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs @@ -44,12 +44,12 @@ versionMiddleware disabledAPIVersions app req k = case parseVersion (removeVersi where err :: Text -> IO ResponseReceived err v = - k . errorRs' . mkError HTTP.status404 "unsupported-version" $ + k . errorRs . mkError HTTP.status404 "unsupported-version" $ "Version " <> fromStrict v <> " is not supported" errint :: IO ResponseReceived errint = - k . errorRs' . mkError HTTP.status404 "unsupported-version" $ + k . errorRs . mkError HTTP.status404 "unsupported-version" $ "Internal APIs (`/i/...`) are not under version control" data ParseVersionError = NoVersion | BadVersion Text | InternalApisAreUnversioned diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 8465f2f1e6f..91ab3f61fd4 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -99,7 +99,6 @@ module Wire.API.User DeleteUser (..), mkDeleteUser, VerifyDeleteUser (..), - mkVerifyDeleteUser, DeletionCodeTimeout (..), DeleteUserResponse (..), DeleteUserResult (..), @@ -1650,9 +1649,6 @@ data VerifyDeleteUser = VerifyDeleteUser deriving (Arbitrary) via (GenericUniform VerifyDeleteUser) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema VerifyDeleteUser) -mkVerifyDeleteUser :: Code.Key -> Code.Value -> VerifyDeleteUser -mkVerifyDeleteUser = VerifyDeleteUser - instance ToSchema VerifyDeleteUser where schema = objectWithDocModifier "VerifyDeleteUser" (description ?~ "Data for verifying an account deletion.") $ diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 5c37e1dbca2..172e2cf043b 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -75,7 +75,6 @@ library Wire.API.Bot.Service Wire.API.Call.Config Wire.API.Connection - Wire.API.ConverProtoLens Wire.API.Conversation Wire.API.Conversation.Action Wire.API.Conversation.Action.Tag diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index 8d31d806a9b..1c0e673fa88 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -6,7 +6,6 @@ module Wire.MiniBackend interpretFederationStack, runFederationStack, interpretNoFederationStack, - runNoFederationStackState, interpretNoFederationStackState, runNoFederationStack, runAllErrorsUnsafe, @@ -303,16 +302,6 @@ runNoFederationStack localBackend teamMember cfg = -- want to do errors?) runAllErrorsUnsafe . interpretNoFederationStack localBackend teamMember def cfg -runNoFederationStackState :: - (HasCallStack) => - MiniBackend -> - Maybe TeamMember -> - UserSubsystemConfig -> - Sem (MiniBackendEffects `Append` AllErrors) a -> - (MiniBackend, a) -runNoFederationStackState localBackend teamMember cfg = - runAllErrorsUnsafe . interpretNoFederationStackState localBackend teamMember def cfg - interpretNoFederationStack :: (Members AllErrors r) => MiniBackend -> diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index bba2cd54e2a..98618e5dbd0 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -19,14 +19,12 @@ module Brig.API.Error where import Brig.API.Types import Control.Monad.Error.Class -import Data.Aeson import Data.ByteString.Conversion import Data.Domain (Domain) import Data.Jwt.Tools (DPoPTokenGenerationError (..)) import Data.Text.Lazy as LT import Data.ZAuth.Validation qualified as ZAuth import Imports -import Network.HTTP.Types.Header import Network.HTTP.Types.Status import Network.Wai.Utilities.Error qualified as Wai import Wire.API.Error @@ -38,9 +36,6 @@ import Wire.Error throwStd :: (MonadError HttpError m) => Wai.Error -> m a throwStd = throwError . StdError -throwRich :: (MonadError HttpError m, ToJSON x) => Wai.Error -> x -> [Header] -> m a -throwRich e x h = throwError (RichError e x h) - -- Error Mapping ---------------------------------------------------------- connError :: ConnectionError -> HttpError @@ -87,12 +82,6 @@ changeEmailError (EmailExists _) = StdError (errorToWai @'E.UserKeyExists) changeEmailError (ChangeBlacklistedEmail _) = StdError blacklistedEmail changeEmailError EmailManagedByScim = StdError $ propertyManagedByScim "email" -changeHandleError :: ChangeHandleError -> HttpError -changeHandleError ChangeHandleNoIdentity = StdError (errorToWai @'E.NoIdentity) -changeHandleError ChangeHandleExists = StdError (errorToWai @'E.HandleExists) -changeHandleError ChangeHandleInvalid = StdError (errorToWai @'E.InvalidHandle) -changeHandleError ChangeHandleManagedByScim = StdError (errorToWai @'E.HandleManagedByScim) - legalHoldLoginError :: LegalHoldLoginError -> HttpError legalHoldLoginError LegalHoldLoginNoBindingTeam = StdError noBindingTeam legalHoldLoginError LegalHoldLoginLegalHoldNotEnabled = StdError legalHoldNotEnabled @@ -234,10 +223,6 @@ accountStatusError :: AccountStatusError -> HttpError accountStatusError InvalidAccountStatus = StdError invalidAccountStatus accountStatusError AccountNotFound = StdError (notFound "Account not found") -updateProfileError :: UpdateProfileError -> HttpError -updateProfileError DisplayNameManagedByScim = StdError (propertyManagedByScim "name") -updateProfileError ProfileNotFound = StdError (errorToWai @'E.UserNotFound) - verificationCodeThrottledError :: VerificationCodeThrottledError -> HttpError verificationCodeThrottledError (VerificationCodeThrottled t) = RichError @@ -253,15 +238,9 @@ clientCapabilitiesCannotBeRemoved = Wai.mkError status409 "client-capabilities-c emailExists :: Wai.Error emailExists = Wai.mkError status409 "email-exists" "The given e-mail address is in use." -phoneExists :: Wai.Error -phoneExists = Wai.mkError status409 "phone-exists" "The given phone number is in use." - badRequest :: LText -> Wai.Error badRequest = Wai.mkError status400 "bad-request" -loginCodePending :: Wai.Error -loginCodePending = Wai.mkError status403 "pending-login" "A login code is still pending." - loginCodeNotFound :: Wai.Error loginCodeNotFound = Wai.mkError status404 "no-pending-login" "No login code was found." @@ -274,12 +253,6 @@ invalidAccountStatus = Wai.mkError status400 "invalid-status" "The specified acc activationKeyNotFound :: Wai.Error activationKeyNotFound = notFound "Activation key not found." -invalidActivationCode :: LText -> Wai.Error -invalidActivationCode = Wai.mkError status404 "invalid-code" - -activationCodeNotFound :: Wai.Error -activationCodeNotFound = invalidActivationCode "Activation key/code not found or invalid." - deletionCodePending :: Wai.Error deletionCodePending = Wai.mkError status403 "pending-delete" "A verification code for account deletion is still pending." @@ -294,38 +267,15 @@ blacklistedEmail = "The given e-mail address has been blacklisted due to a permanent bounce \ \or a complaint." -passwordExists :: Wai.Error -passwordExists = - Wai.mkError - status403 - "password-exists" - "The operation is not permitted because the user has a password set." - -phoneBudgetExhausted :: Wai.Error -phoneBudgetExhausted = - Wai.mkError - status403 - "phone-budget-exhausted" - "The SMS or voice call budget for the given phone number has been \ - \exhausted. Please try again later. Repeated exhaustion of the SMS or \ - \voice call budget is considered abuse of the API and may result in \ - \permanent blacklisting of the phone number." - authMissingCookie :: Wai.Error authMissingCookie = Wai.mkError status403 "invalid-credentials" "Missing cookie" -authInvalidCookie :: Wai.Error -authInvalidCookie = Wai.mkError status403 "invalid-credentials" "Invalid cookie" - authMissingToken :: Wai.Error authMissingToken = Wai.mkError status403 "invalid-credentials" "Missing token" authMissingCookieAndToken :: Wai.Error authMissingCookieAndToken = Wai.mkError status403 "invalid-credentials" "Missing cookie and token" -invalidAccessToken :: Wai.Error -invalidAccessToken = Wai.mkError status403 "invalid-credentials" "Invalid access token" - missingAccessToken :: Wai.Error missingAccessToken = Wai.mkError status403 "invalid-credentials" "Missing access token" diff --git a/services/brig/src/Brig/API/Handler.hs b/services/brig/src/Brig/API/Handler.hs index 2971f28e4e9..dcd6eba66a1 100644 --- a/services/brig/src/Brig/API/Handler.hs +++ b/services/brig/src/Brig/API/Handler.hs @@ -21,7 +21,6 @@ module Brig.API.Handler toServantHandler, -- * Utilities - parseJsonBody, checkAllowlist, checkAllowlistWithError, isAllowlisted, @@ -41,16 +40,13 @@ import Control.Lens (view) import Control.Monad.Catch (catches, throwM) import Control.Monad.Catch qualified as Catch import Control.Monad.Except (MonadError, throwError) -import Data.Aeson (FromJSON) import Data.Aeson qualified as Aeson import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Data.ZAuth.Validation qualified as ZV import Imports import Network.HTTP.Types (Status (statusCode, statusMessage)) -import Network.Wai.Utilities.Error ((!>>)) import Network.Wai.Utilities.Error qualified as WaiError -import Network.Wai.Utilities.Request (JsonRequest, parseBody) import Network.Wai.Utilities.Server qualified as Server import Servant qualified import System.Logger qualified as Log @@ -122,12 +118,6 @@ brigErrorHandlers logger reqId = ------------------------------------------------------------------------------- -- Utilities --- This could go to libs/wai-utilities. There is a `parseJson'` in --- "Network.Wai.Utilities.Request", but adding `parseJsonBody` there would require to move --- more code out of brig. -parseJsonBody :: (FromJSON a, MonadIO m) => JsonRequest a -> ExceptT HttpError m a -parseJsonBody req = parseBody req !>> StdError . badRequest - -- | If an Allowlist is configured, consult it, otherwise a no-op. {#RefActivationAllowlist} checkAllowlist :: Email -> Handler r () checkAllowlist = wrapHttpClientE . checkAllowlistWithError (StdError allowlistError) diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 3fa288a39fb..45b1d03fb37 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -29,7 +29,6 @@ module Brig.API.User CheckHandleResp (..), checkHandle, lookupHandle, - changeManagedBy, changeAccountStatus, changeSingleAccountStatus, Data.lookupAccounts, @@ -537,25 +536,6 @@ checkRestrictedUserCreation new = do ) $ throwE RegisterErrorUserCreationRestricted -------------------------------------------------------------------------------- --- Update ManagedBy - -changeManagedBy :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r - ) => - UserId -> - ConnId -> - ManagedByUpdate -> - (AppT r) () -changeManagedBy uid conn (ManagedByUpdate mb) = do - wrapClient $ Data.updateManagedBy uid mb - liftSem $ Intra.onUserEvent uid (Just conn) (managedByUpdate uid mb) - ------------------------------------------------------------------------------- -- Change Email diff --git a/services/brig/test/integration/API/Search/Util.hs b/services/brig/test/integration/API/Search/Util.hs index 9f8c83b34e0..3dc5598a871 100644 --- a/services/brig/test/integration/API/Search/Util.hs +++ b/services/brig/test/integration/API/Search/Util.hs @@ -102,13 +102,6 @@ assertCan'tFind brig self expected q = do assertBool ("User shouldn't be present in results for query: " <> show q) $ expected `notElem` map contactQualifiedId r -assertCan'tFindWithDomain :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> UserId -> Qualified UserId -> Text -> Domain -> m () -assertCan'tFindWithDomain brig self expected q domain = do - r <- searchResults <$> executeSearchWithDomain brig self q domain - liftIO $ do - assertBool ("User shouldn't be present in results for query: " <> show q) $ - expected `notElem` map contactQualifiedId r - executeTeamUserSearch :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 61ab960962f..6f78f951fe8 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -110,22 +110,6 @@ createPopulatedBindingTeamWithNames brig names = do pure invitee pure (tid, inviter, invitees) -createTeam :: UserId -> Galley -> Http TeamId -createTeam u galley = do - tid <- randomId - r <- - put - ( galley - . paths ["i", "teams", toByteString' tid] - . contentJson - . zAuthAccess u "conn" - . expect2xx - . lbytes (encode newTeam) - ) - maybe (error "invalid team id") pure $ - fromByteString $ - getHeader' "Location" r - -- | Create user and binding team. -- -- NB: the created user is the team owner. diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index d862c73ddd1..5e6c4856d96 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -24,7 +24,6 @@ module API.User.Util where import Bilge hiding (accept, timeout) import Bilge.Assert import Brig.Options (Opts) -import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.ZAuth (Token) import Cassandra qualified as DB import Codec.MIME.Type qualified as MIME @@ -50,7 +49,6 @@ import Data.Text.Ascii qualified as Ascii import Data.Vector qualified as Vec import Data.ZAuth.Token qualified as ZAuth import Federation.Util (withTempMockFederator) -import Federator.MockServer (FederatedRequest (..)) import GHC.TypeLits (KnownSymbol) import Imports import Test.Tasty.Cannon qualified as WS @@ -73,7 +71,6 @@ import Wire.API.User.Activation import Wire.API.User.Auth import Wire.API.User.Client import Wire.API.User.Client.DPoPAccessToken (Proof) -import Wire.API.User.Client.Prekey import Wire.API.User.Handle import Wire.API.User.Password import Wire.VerificationCode qualified as Code @@ -375,33 +372,6 @@ receiveConnectionAction brig fedBrigClient uid1 quid2 action expectedReaction ex res @?= F.NewConnectionResponseOk expectedReaction assertConnectionQualified brig uid1 quid2 expectedRel -sendConnectionAction :: - (HasCallStack) => - Brig -> - Opts -> - UserId -> - Qualified UserId -> - Maybe F.RemoteConnectionAction -> - Relation -> - Http () -sendConnectionAction brig opts uid1 quid2 reaction expectedRel = do - let mockConnectionResponse = F.NewConnectionResponseOk reaction - mockResponse = encode mockConnectionResponse - (res, reqs) <- - liftIO . withTempMockFederator opts mockResponse $ - postConnectionQualified brig uid1 quid2 - - liftIO $ do - req <- assertOne reqs - frTargetDomain req @?= qDomain quid2 - frComponent req @?= Brig - frRPC req @?= "send-connection-action" - eitherDecode (frBody req) - @?= Right (F.NewConnectionRequest uid1 Nothing (qUnqualified quid2) F.RemoteConnect) - - liftIO $ assertBool "postConnectionQualified failed" $ statusCode res `elem` [200, 201] - assertConnectionQualified brig uid1 quid2 expectedRel - sendConnectionUpdateAction :: (HasCallStack) => Brig -> @@ -462,25 +432,6 @@ downloadAsset c usr ast = . zConn "conn" ) -requestLegalHoldDevice :: Brig -> UserId -> UserId -> LastPrekey -> (MonadHttp m) => m ResponseLBS -requestLegalHoldDevice brig requesterId targetUserId lastPrekey' = - post $ - brig - . paths ["i", "clients", "legalhold", toByteString' targetUserId, "request"] - . contentJson - . body payload - where - payload = - RequestBodyLBS . encode $ - LegalHoldClientRequest requesterId lastPrekey' - -deleteLegalHoldDevice :: Brig -> UserId -> (MonadHttp m) => m ResponseLBS -deleteLegalHoldDevice brig uid = - delete $ - brig - . paths ["i", "clients", "legalhold", toByteString' uid] - . contentJson - matchDeleteUserNotification :: Qualified UserId -> Notification -> Assertion matchDeleteUserNotification quid n = do let j = Object $ List1.head (ntfPayload n) diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index e1b01f8fe36..6ce6f9ece74 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -58,7 +58,7 @@ import Data.List1 (List1) import Data.List1 qualified as List1 import Data.Misc import Data.Proxy -import Data.Qualified hiding (isLocal) +import Data.Qualified import Data.Range import Data.Sequence qualified as Seq import Data.String.Conversions @@ -104,7 +104,6 @@ import Test.Tasty.Pending (flakyTestCase) import Text.Printf (printf) import UnliftIO.Async qualified as Async import Util.Options -import Web.Internal.HttpApiData import Wire.API.Connection import Wire.API.Conversation import Wire.API.Conversation.Role (roleNameWireAdmin) @@ -112,7 +111,6 @@ import Wire.API.Federation.API import Wire.API.Federation.Domain import Wire.API.Federation.Version import Wire.API.Internal.Notification -import Wire.API.MLS.SubConversation import Wire.API.Routes.MultiTablePaging import Wire.API.Team.Member hiding (userId) import Wire.API.User hiding (AccountStatus (..)) @@ -265,8 +263,8 @@ localAndRemoteUserWithConvId brig shouldBeLocal = do let go = do other <- Qualified <$> randomId <*> pure (Domain "far-away.example.com") let convId = one2OneConvId BaseProtocolProteusTag quid other - isLocal = qDomain quid == qDomain convId - if shouldBeLocal == isLocal + isLocalUntagged = qDomain quid == qDomain convId + if shouldBeLocal == isLocalUntagged then pure (qUnqualified quid, other, convId) else go go @@ -353,12 +351,6 @@ getActivationCode brig ep = do let acode = ActivationCode . Ascii.unsafeFromText <$> (lbs ^? key "code" . _String) pure $ (,) <$> akey <*> acode -getPhoneLoginCode :: Brig -> Phone -> Http (Maybe LoginCode) -getPhoneLoginCode brig p = do - r <- get $ brig . path "/i/users/login-code" . queryItem "phone" (toByteString' p) - let lbs = fromMaybe "" $ responseBody r - pure (LoginCode <$> (lbs ^? key "code" . _String)) - assertUpdateNotification :: (HasCallStack) => WS.WebSocket -> UserId -> UserUpdate -> IO () assertUpdateNotification ws uid upd = WS.assertMatch (5 # Second) ws $ \n -> do let j = Object $ List1.head (ntfPayload n) @@ -537,23 +529,6 @@ decodeToken' r = fromMaybe (error "invalid access_token") $ do data LoginCodeType = LoginCodeSMS | LoginCodeVoice deriving (Eq) -sendLoginCode :: Brig -> Phone -> LoginCodeType -> Bool -> Http ResponseLBS -sendLoginCode b p typ force = - post $ - b - . path "/login/send" - . contentJson - . body js - where - js = - RequestBodyLBS - . encode - $ object - [ "phone" .= fromPhone p, - "voice_call" .= (typ == LoginCodeVoice), - "force" .= force - ] - postConnection :: Brig -> UserId -> UserId -> (MonadHttp m) => m ResponseLBS postConnection brig from to = post $ @@ -642,23 +617,6 @@ createUserWithHandle brig = do -- when using this function. pure (handle, userWithHandle) -getUserInfoFromHandle :: - (MonadIO m, MonadCatch m, MonadHttp m, HasCallStack) => - Brig -> - Domain -> - Handle -> - m UserProfile -getUserInfoFromHandle brig domain handle = do - u <- randomId - responseJsonError - =<< get - ( apiVersion "v1" - . brig - . paths ["users", "by-handle", toByteString' (domainText domain), toByteString' handle] - . zUser u - . expect2xx - ) - addClient :: (MonadHttp m, HasCallStack) => Brig -> @@ -733,47 +691,6 @@ getConversationQualified galley usr cnv = . paths ["conversations", toByteString' (qDomain cnv), toByteString' (qUnqualified cnv)] . zAuthAccess usr "conn" -createMLSConversation :: (MonadHttp m) => Galley -> UserId -> ClientId -> m ResponseLBS -createMLSConversation galley zusr c = do - let conv = - NewConv - [] - mempty - (checked "gossip") - mempty - Nothing - Nothing - Nothing - Nothing - roleNameWireAdmin - BaseProtocolMLSTag - post $ - galley - . path "/conversations" - . zUser zusr - . zConn "conn" - . zClient c - . json conv - -createMLSSubConversation :: - (MonadIO m, MonadHttp m) => - Galley -> - UserId -> - Qualified ConvId -> - SubConvId -> - m ResponseLBS -createMLSSubConversation galley zusr qcnv sconv = - get $ - galley - . paths - [ "conversations", - toByteString' (qDomain qcnv), - toByteString' (qUnqualified qcnv), - "subconversations", - toHeader sconv - ] - . zUser zusr - createConversation :: (MonadHttp m) => Galley -> UserId -> [Qualified UserId] -> m ResponseLBS createConversation galley zusr usersToAdd = do let conv = @@ -966,10 +883,6 @@ somePrekeys = Prekey (PrekeyId 26) "pQABARgaAqEAWCBMSQoQ6B35plB80i1O3AWlJSftCEbCbju97Iykg5+NWQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2" ] --- | The client ID of the first of 'someLastPrekeys' -someClientId :: ClientId -someClientId = ClientId 0x1dbfbe22c8a35cb2 - someLastPrekeys :: [LastPrekey] someLastPrekeys = [ lastPrekey "pQABARn//wKhAFggnCcZIK1pbtlJf4wRQ44h4w7/sfSgj5oWXMQaUGYAJ/sDoQChAFgglacihnqg/YQJHkuHNFU7QD6Pb3KN4FnubaCF2EVOgRkE9g==", @@ -1118,9 +1031,6 @@ aFewTimes (\_ -> pure . not . good) (const action) -retryT :: (MonadIO m, MonadMask m) => m a -> m a -retryT = recoverAll (exponentialBackoff 8000 <> limitRetries 3) . const - assertOne :: (HasCallStack, MonadIO m, Show a) => [a] -> m a assertOne [a] = pure a assertOne xs = liftIO . assertFailure $ "Expected exactly one element, found " <> show xs diff --git a/services/cargohold/cargohold.cabal b/services/cargohold/cargohold.cabal index 2a8a5b2ba93..39a6dce3593 100644 --- a/services/cargohold/cargohold.cabal +++ b/services/cargohold/cargohold.cabal @@ -280,7 +280,6 @@ executable cargohold-integration , imports , kan-extensions , lens >=3.8 - , mime >=0.4 , mmorph , mtl , optparse-applicative diff --git a/services/cargohold/default.nix b/services/cargohold/default.nix index 32c9e73b371..2116b776445 100644 --- a/services/cargohold/default.nix +++ b/services/cargohold/default.nix @@ -152,7 +152,6 @@ mkDerivation { imports kan-extensions lens - mime mmorph mtl optparse-applicative diff --git a/services/cargohold/test/integration/API/Util.hs b/services/cargohold/test/integration/API/Util.hs index 2c51dc9b29f..a1feeb7739a 100644 --- a/services/cargohold/test/integration/API/Util.hs +++ b/services/cargohold/test/integration/API/Util.hs @@ -17,13 +17,6 @@ module API.Util ( randomUser, - uploadSimple, - decodeHeaderOrFail, - getContentType, - applicationText, - applicationOctetStream, - deleteAssetV3, - deleteAsset, downloadAsset, withMockFederator, ) @@ -33,26 +26,20 @@ import Bilge hiding (body, host, port) import qualified Bilge import CargoHold.Options import CargoHold.Run -import qualified Codec.MIME.Parse as MIME -import qualified Codec.MIME.Type as MIME import Control.Lens hiding ((.=)) import Control.Monad.Codensity import Data.Aeson (object, (.=)) -import Data.ByteString.Builder import qualified Data.ByteString.Char8 as C import Data.ByteString.Conversion -import qualified Data.ByteString.Lazy as Lazy import Data.Default import Data.Id import Data.Qualified -import Data.Text.Encoding (decodeLatin1, encodeUtf8) +import Data.Text.Encoding (encodeUtf8) import qualified Data.UUID as UUID import Data.UUID.V4 (nextRandom) import Federator.MockServer import Imports hiding (head) import qualified Network.HTTP.Media as HTTP -import Network.HTTP.Types.Header -import Network.HTTP.Types.Method import Network.Wai.Utilities.MockServer import Safe (readNote) import TestSetup @@ -86,71 +73,9 @@ randomUser = do uid <- nextRandom pure $ loc <> "+" <> UUID.toText uid <> "@" <> dom -uploadSimple :: - (Request -> Request) -> - UserId -> - AssetSettings -> - (MIME.Type, ByteString) -> - TestM (Response (Maybe Lazy.ByteString)) -uploadSimple c usr sts (ct, bs) = - let mp = buildMultipartBody sts ct (Lazy.fromStrict bs) - in uploadRaw c usr (toLazyByteString mp) - -decodeHeaderOrFail :: (HasCallStack, FromByteString a) => HeaderName -> Response b -> a -decodeHeaderOrFail h = - fromMaybe (error $ "decodeHeaderOrFail: missing or invalid header: " ++ show h) - . fromByteString - . getHeader' h - -uploadRaw :: - (Request -> Request) -> - UserId -> - Lazy.ByteString -> - TestM (Response (Maybe Lazy.ByteString)) -uploadRaw c usr bs = do - cargohold' <- viewUnversionedCargohold - post $ - apiVersion "v1" - . c - . cargohold' - . method POST - . zUser usr - . zConn "conn" - . content "multipart/mixed" - . lbytes bs - -getContentType :: Response a -> Maybe MIME.Type -getContentType = MIME.parseContentType . decodeLatin1 . getHeader' "Content-Type" - -applicationText :: MIME.Type -applicationText = MIME.Type (MIME.Application "text") [] - -applicationOctetStream :: MIME.Type -applicationOctetStream = MIME.Type (MIME.Application "octet-stream") [] - zUser :: UserId -> Request -> Request zUser = header "Z-User" . UUID.toASCIIBytes . toUUID -zConn :: ByteString -> Request -> Request -zConn = header "Z-Connection" - -deleteAssetV3 :: UserId -> Qualified AssetKey -> TestM (Response (Maybe Lazy.ByteString)) -deleteAssetV3 u k = do - c <- viewUnversionedCargohold - delete $ apiVersion "v1" . c . zUser u . paths ["assets", "v3", toByteString' (qUnqualified k)] - -deleteAsset :: UserId -> Qualified AssetKey -> TestM (Response (Maybe Lazy.ByteString)) -deleteAsset u k = do - c <- viewCargohold - delete $ - c - . zUser u - . paths - [ "assets", - toByteString' (qDomain k), - toByteString' (qUnqualified k) - ] - class IsAssetLocation key where locationPath :: key -> Request -> Request diff --git a/services/cargohold/test/integration/TestSetup.hs b/services/cargohold/test/integration/TestSetup.hs index 93f361e34c3..f5b69fe2fd5 100644 --- a/services/cargohold/test/integration/TestSetup.hs +++ b/services/cargohold/test/integration/TestSetup.hs @@ -31,10 +31,7 @@ module TestSetup viewCargohold, createTestSetup, runFederationClient, - withFederationClient, - withFederationError, apiVersion, - unversioned, ) where @@ -45,7 +42,6 @@ import Control.Lens import Control.Monad.Codensity import Control.Monad.Except import Control.Monad.Morph -import qualified Data.Aeson as Aeson import qualified Data.ByteString.Char8 as B8 import Data.ByteString.Conversion import qualified Data.Text as T @@ -55,7 +51,6 @@ import Imports import Network.HTTP.Client hiding (responseBody) import qualified Network.HTTP.Client as HTTP import Network.HTTP.Client.TLS -import qualified Network.Wai.Utilities.Error as Wai import Servant.Client.Streaming import Test.Tasty import Test.Tasty.HUnit @@ -103,16 +98,8 @@ removeVersionPrefix bs = do (_, s') <- B8.readInteger s pure (B8.tail s') --- | 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) - } +viewUnversionedCargohold :: TestM Cargohold +viewUnversionedCargohold = mkRequest <$> view tsEndpoint viewCargohold :: TestM Cargohold viewCargohold = @@ -123,9 +110,6 @@ viewCargohold = latestVersion :: Version latestVersion = maxBound -viewUnversionedCargohold :: TestM Cargohold -viewUnversionedCargohold = mkRequest <$> view tsEndpoint - runTestM :: TestSetup -> TestM a -> IO a runTestM ts action = runHttpT (view tsManager ts) (runReaderT action ts) @@ -188,29 +172,3 @@ runFederationClient action = do catch (withClientM action env k) (k . Left) either throwError pure r - -hoistFederation :: ReaderT TestSetup (ExceptT ClientError (Codensity IO)) a -> ExceptT ClientError TestM a -hoistFederation action = do - env <- ask - hoist (liftIO . lowerCodensity) $ runReaderT action env - -withFederationClient :: ReaderT TestSetup (ExceptT ClientError (Codensity IO)) a -> TestM a -withFederationClient action = - runExceptT (hoistFederation action) >>= \case - Left err -> - liftIO - . assertFailure - $ "Unexpected federation client error: " - <> displayException err - Right x -> pure x - -withFederationError :: ReaderT TestSetup (ExceptT ClientError (Codensity IO)) a -> TestM Wai.Error -withFederationError action = - runExceptT (hoistFederation action) - >>= liftIO - . \case - Left (FailureResponse _ resp) -> case Aeson.eitherDecode (responseBody resp) of - Left err -> assertFailure $ "Error while parsing error response: " <> err - Right e -> (Wai.code e @?= responseStatusCode resp) $> e - Left err -> assertFailure $ "Unexpected federation client error: " <> displayException err - Right _ -> assertFailure "Unexpected success" diff --git a/services/galley/default.nix b/services/galley/default.nix index b414e5b0551..362e174d34a 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -230,7 +230,6 @@ mkDerivation { bytestring bytestring-conversion call-stack - case-insensitive cassandra-util cassava cereal @@ -272,7 +271,6 @@ mkDerivation { random retry saml2-web-sso - schema-profunctor servant-client servant-client-core servant-server @@ -300,7 +298,6 @@ mkDerivation { uuid vector wai - wai-extra wai-utilities warp warp-tls diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 47474894165..64bb0d69b4f 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -472,7 +472,6 @@ executable galley-integration , bytestring , bytestring-conversion , call-stack - , case-insensitive , cassandra-util , cassava , cereal @@ -513,7 +512,6 @@ executable galley-integration , random , retry , saml2-web-sso >=0.20 - , schema-profunctor , servant-client , servant-client-core , servant-server @@ -541,7 +539,6 @@ executable galley-integration , uuid , vector , wai - , wai-extra , wai-utilities , warp , warp-tls >=3.2 diff --git a/services/galley/test/integration/API/MLS/Mocks.hs b/services/galley/test/integration/API/MLS/Mocks.hs index 49165e64bc7..3e82b298246 100644 --- a/services/galley/test/integration/API/MLS/Mocks.hs +++ b/services/galley/test/integration/API/MLS/Mocks.hs @@ -19,7 +19,6 @@ module API.MLS.Mocks ( receiveCommitMock, receiveCommitMockByDomain, messageSentMock, - messageSentMockByDomain, welcomeMock, welcomeMockByDomain, sendMessageMock, @@ -65,12 +64,6 @@ receiveCommitMockByDomain clients = do messageSentMock :: Mock LByteString messageSentMock = "on-mls-message-sent" ~> RemoteMLSMessageOk -messageSentMockByDomain :: [Domain] -> Mock LByteString -messageSentMockByDomain reachables = do - domain <- frTargetDomain <$> getRequest - guard (domain `elem` reachables) - messageSentMock - welcomeMock :: Mock LByteString welcomeMock = "mls-welcome" ~> MLSWelcomeSent diff --git a/services/galley/test/integration/API/SQS.hs b/services/galley/test/integration/API/SQS.hs index 2057433b150..ccf45732c90 100644 --- a/services/galley/test/integration/API/SQS.hs +++ b/services/galley/test/integration/API/SQS.hs @@ -111,9 +111,6 @@ tUpdate expectedCount uids l (Just e) = liftIO $ do (Set.fromList $ billingUserIds) tUpdate _ _ l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 TeamUpdate, got nothing" -updateMatcher :: TeamId -> TeamEvent -> Bool -updateMatcher tid e = e ^. eventType == E.TeamEvent'TEAM_UPDATE && decodeIdFromBS (e ^. teamId) == tid - assertTeamUpdate :: (HasCallStack) => String -> TeamId -> Int32 -> [UserId] -> TestM () assertTeamUpdate l tid c uids = assertIfWatcher l (\e -> e ^. eventType == E.TeamEvent'TEAM_UPDATE && decodeIdFromBS (e ^. teamId) == tid) $ tUpdate c uids diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs index 6fd3eee176b..b0918506e84 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/Util.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -529,18 +529,6 @@ putLHWhitelistTeam' g tid = do . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] ) -_deleteLHWhitelistTeam :: (HasCallStack) => TeamId -> TestM ResponseLBS -_deleteLHWhitelistTeam tid = do - galleyCall <- viewGalley - deleteLHWhitelistTeam' galleyCall tid - -deleteLHWhitelistTeam' :: (HasCallStack, MonadHttp m) => GalleyR -> TeamId -> m ResponseLBS -deleteLHWhitelistTeam' g tid = do - delete - ( g - . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] - ) - errWith :: (HasCallStack, Typeable a, FromJSON a) => Int -> (a -> Bool) -> ResponseLBS -> TestM () errWith wantStatus wantBody rsp = liftIO $ do assertEqual "" wantStatus (statusCode rsp) diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 09241ea7534..6d7df5a1e23 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -27,22 +27,18 @@ import Bilge.Assert import Bilge.TestSession import Control.Applicative import Control.Concurrent.Async -import Control.Exception (throw) import Control.Lens hiding (from, to, uncons, (#), (.=)) import Control.Monad.Catch (MonadCatch, MonadMask) import Control.Monad.Codensity (lowerCodensity) -import Control.Monad.Except (ExceptT, runExceptT) import Control.Retry (constantDelay, exponentialBackoff, limitRetries, retrying) import Data.Aeson hiding (json) import Data.Aeson qualified as A import Data.Aeson.Lens (key, _String) import Data.ByteString qualified as BS -import Data.ByteString.Base64.URL qualified as B64U import Data.ByteString.Char8 qualified as B8 import Data.ByteString.Char8 qualified as C import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as Lazy -import Data.CaseInsensitive qualified as CI import Data.Code qualified as Code import Data.Currency qualified as Currency import Data.Default @@ -51,7 +47,6 @@ import Data.Handle qualified as Handle import Data.HashMap.Strict qualified as HashMap import Data.Id import Data.Json.Util hiding ((#)) -import Data.Kind import Data.LegalHold (defUserLegalHoldStatus) import Data.List.NonEmpty (NonEmpty) import Data.List1 as List1 @@ -60,7 +55,7 @@ import Data.Map.Strict qualified as Map import Data.Misc import Data.ProtoLens qualified as Protolens import Data.ProtocolBuffers (encodeMessage) -import Data.Qualified hiding (isLocal) +import Data.Qualified import Data.Range import Data.Serialize (runPut) import Data.Set qualified as Set @@ -69,14 +64,12 @@ import Data.String.Conversions import Data.Text qualified as Text import Data.Text.Encoding qualified as T import Data.Text.Encoding qualified as Text -import Data.Text.Lazy.Encoding qualified as LT import Data.Time (getCurrentTime) import Data.Tuple.Extra import Data.UUID qualified as UUID import Data.UUID.V4 import Federator.MockServer import Federator.MockServer qualified as Mock -import GHC.TypeLits (KnownSymbol) import GHC.TypeNats import Galley.Intra.User (chunkify) import Galley.Options qualified as Opts @@ -86,17 +79,12 @@ import Galley.Types.UserList import Imports import Network.HTTP.Client qualified as HTTP import Network.HTTP.Media.MediaType -import Network.HTTP.Types qualified as HTTP import Network.URI (pathSegments) -import Network.Wai (defaultRequest) -import Network.Wai qualified as Wai -import Network.Wai.Test qualified as Wai import Network.Wai.Utilities.MockServer (withMockServer) import Servant import System.Exit import System.Process import System.Random -import Test.QuickCheck qualified as Q import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit @@ -123,7 +111,6 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley -import Wire.API.Federation.Domain (originDomainHeaderName) import Wire.API.Internal.Notification hiding (target) import Wire.API.MLS.LeafNode import Wire.API.MLS.Message @@ -134,12 +121,10 @@ import Wire.API.Message import Wire.API.Message.Proto qualified as Proto import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Galley.ConversationsIntra -import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.Version import Wire.API.Team -import Wire.API.Team.Feature import Wire.API.Team.Invitation import Wire.API.Team.Member hiding (userId) import Wire.API.Team.Member qualified as Team @@ -198,7 +183,7 @@ createBindingTeam = do createBindingTeam' :: (HasCallStack) => TestM (User, TeamId) createBindingTeam' = do - owner <- randomTeamCreator' + owner <- randomTeamCreator teams <- getTeams (User.userId owner) [] team <- assertOne $ view teamListTeams teams let tid = view teamId team @@ -311,12 +296,6 @@ createBindingTeamInternalWithCurrency name owner cur = do === statusCode pure tid -getTeamInternal :: (HasCallStack) => TeamId -> TestM TeamData -getTeamInternal tid = do - g <- viewGalley - r <- get (g . paths ["i/teams", toByteString' tid]) UserId -> TeamId -> TestM Team getTeam usr tid = do g <- viewGalley @@ -428,14 +407,6 @@ getTeamMemberInternal tid mid = do r <- get (g . paths ["i", "teams", toByteString' tid, "members", toByteString' mid]) UserId -> TeamId -> UserId -> Permissions -> Maybe (UserId, UTCTimeMillis) -> TestM () -addTeamMember usr tid muid mperms mmbinv = do - g <- viewGalley - let payload = json (mkNewTeamMember muid mperms mmbinv) - post (g . paths ["teams", toByteString' tid, "members"] . zUser usr . zConn "conn" . payload) - !!! const 200 - === statusCode - -- | FUTUREWORK: do not use this, it's broken!! use 'addUserToTeam' instead! https://wearezeta.atlassian.net/browse/SQSERVICES-471 addTeamMemberInternal :: (HasCallStack) => TeamId -> UserId -> Permissions -> Maybe (UserId, UTCTimeMillis) -> TestM () addTeamMemberInternal tid muid mperms mmbinv = addTeamMemberInternal' tid muid mperms mmbinv !!! const 200 === statusCode @@ -731,26 +702,6 @@ postConvQualified u c n = do . zType "access" . json n -postConvWithRemoteUsersGeneric :: - (HasCallStack) => - Mock LByteString -> - UserId -> - Maybe ClientId -> - NewConv -> - TestM (Response (Maybe LByteString)) -postConvWithRemoteUsersGeneric m u c n = do - let mock = - ("get-not-fully-connected-backends" ~> NonConnectedBackends mempty) - <|> m - fmap fst $ - withTempMockFederator' mock $ - postConvQualified u c n {newConvName = setName (newConvName n)} - Maybe (Range n m Text) -> Maybe (Range n m Text) - setName Nothing = checked "federated gossip" - setName x = x - postConvWithRemoteUsers :: (HasCallStack) => UserId -> @@ -1033,15 +984,6 @@ getConvs u cids = do . zConn "conn" . json (ListConversations (unsafeRange cids)) -getConvClients :: (HasCallStack) => GroupId -> TestM ClientList -getConvClients gid = do - g <- viewGalley - responseJsonError - =<< get - ( g - . paths ["i", "group", B64U.encode $ unGroupId gid] - ) - getAllConvs :: (HasCallStack) => UserId -> TestM [Conversation] getAllConvs u = do g <- viewGalley @@ -1409,15 +1351,6 @@ postJoinCodeConv' mPw u j = do -- `json (JoinConversationByCode j Nothing)` and `json j` are equivalent, using the latter to test backwards compatibility . (if isJust mPw then json (JoinConversationByCode j mPw) else json j) -deleteFederation :: - (MonadHttp m, HasGalley m, MonadIO m) => - Domain -> - m ResponseLBS -deleteFederation dom = do - g <- viewGalley - delete $ - g . paths ["/i/federation", toByteString' dom] - putQualifiedAccessUpdate :: (MonadHttp m, HasGalley m, MonadIO m) => UserId -> @@ -1581,15 +1514,6 @@ registerRemoteConv convId originUser name othMembers = do protocol = ProtocolProteus } -getFeatureStatusMulti :: forall cfg. (KnownSymbol (FeatureSymbol cfg)) => Multi.TeamFeatureNoConfigMultiRequest -> TestM ResponseLBS -getFeatureStatusMulti req = do - g <- viewGalley - post - ( g - . paths ["i", "features-multi-teams", featureNameBS @cfg] - . json req - ) - ------------------------------------------------------------------------------- -- Common Assertions @@ -1961,21 +1885,12 @@ decodeQualifiedConvIdList = fmap mtpResults . responseJsonEither @ConvIdsPage zUser :: UserId -> Request -> Request zUser = header "Z-User" . toByteString' -zBot :: UserId -> Request -> Request -zBot = header "Z-Bot" . toByteString' - zClient :: ClientId -> Request -> Request zClient = header "Z-Client" . toByteString' zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" -zProvider :: ProviderId -> Request -> Request -zProvider = header "Z-Provider" . toByteString' - -zConv :: ConvId -> Request -> Request -zConv = header "Z-Conversation" . toByteString' - zType :: ByteString -> Request -> Request zType = header "Z-Type" @@ -2068,16 +1983,6 @@ postConnection from to = do RequestBodyLBS . encode $ ConnectionRequest to (unsafeRange "some conv name") -postConnectionQualified :: UserId -> Qualified UserId -> TestM ResponseLBS -postConnectionQualified from (Qualified toUser toDomain) = do - brig <- viewBrig - post $ - brig - . paths ["connections", toByteString' toDomain, toByteString' toUser] - . contentJson - . zUser from - . zConn "conn" - -- | A copy of 'putConnection' from Brig integration tests. putConnection :: UserId -> UserId -> Relation -> TestM ResponseLBS putConnection from to r = do @@ -2144,11 +2049,8 @@ randomQualifiedUser = randomUser' False True True randomQualifiedId :: (MonadIO m) => Domain -> m (Qualified (Id a)) randomQualifiedId domain = Qualified <$> randomId <*> pure domain -randomTeamCreator :: (HasCallStack) => TestM UserId -randomTeamCreator = qUnqualified <$> randomUser' True True True - -randomTeamCreator' :: (HasCallStack) => TestM User -randomTeamCreator' = randomUser'' True True True +randomTeamCreator :: (HasCallStack) => TestM User +randomTeamCreator = randomUser'' True True True randomUser' :: (HasCallStack) => Bool -> Bool -> Bool -> TestM (Qualified UserId) randomUser' isCreator hasPassword hasEmail = userQualifiedId <$> randomUser'' isCreator hasPassword hasEmail @@ -2263,36 +2165,6 @@ deleteClient u c pw = do [ "password" .= pw ] --- TODO: Refactor, as used also in brig -isUserDeleted :: (HasCallStack) => UserId -> TestM Bool -isUserDeleted u = do - b <- viewBrig - r <- - get (b . paths ["i", "users", toByteString' u, "status"]) - error $ "getStatus: failed to parse response: " ++ show r - Just j -> do - let st = maybeFromJSON =<< j ^? key "status" - let decoded = fromMaybe (error $ "getStatus: failed to decode status" ++ show j) st - pure $ decoded == Deleted - where - maybeFromJSON :: (FromJSON a) => Value -> Maybe a - maybeFromJSON v = case fromJSON v of - Success a -> Just a - _ -> Nothing - -isMember :: UserId -> ConvId -> TestM Bool -isMember usr cnv = do - g <- viewGalley - res <- - get $ - g - . paths ["i", "conversations", toByteString' cnv, "members", toByteString' usr] - . expect2xx - pure $ isJust (responseJsonMaybe @Member res) - randomUserWithClient :: LastPrekey -> TestM (UserId, ClientId) randomUserWithClient lk = do (u, c) <- randomUserWithClientQualified lk @@ -2304,9 +2176,6 @@ randomUserWithClientQualified lk = do c <- randomClient (qUnqualified u) lk pure (u, c) -newNonce :: TestM (Id ()) -newNonce = randomId - fromBS :: (HasCallStack, FromByteString a, MonadIO m) => ByteString -> m a fromBS bs = case fromByteString bs of @@ -2399,9 +2268,6 @@ otrRecipients = . fmap Map.fromList . foldr ((uncurry Map.insert . fmap pure) . (\(a, b, c) -> (a, (b, c)))) mempty -genRandom :: (Q.Arbitrary a, MonadIO m) => m a -genRandom = liftIO . Q.generate $ Q.arbitrary - defPassword :: PlainTextPassword6 defPassword = plainTextPassword6Unsafe "topsecretdefaultpassword" @@ -2577,13 +2443,6 @@ deleteTeam owner tid = do !!! do const 202 === statusCode --- (Duplicate of 'Galley.Intra.User.getUsers'.) -getUsersByUid :: [UserId] -> TestM [User] -getUsersByUid = getUsersBy "ids" - -getUsersByHandle :: [Handle.Handle] -> TestM [User] -getUsersByHandle = getUsersBy "handles" - getUsersBy :: forall uidsOrHandles. (ToByteString uidsOrHandles) => ByteString -> [uidsOrHandles] -> TestM [User] getUsersBy keyName = chunkify $ \keys -> do brig <- viewBrig @@ -2598,11 +2457,8 @@ getUsersBy keyName = chunkify $ \keys -> do let accounts = fromJust $ responseJsonMaybe @[UserAccount] res pure $ fmap accountUser accounts -getUserProfile :: UserId -> UserId -> TestM UserProfile -getUserProfile zusr uid = do - brig <- view tsUnversionedBrig - res <- get (brig . zUser zusr . paths ["v1", "users", toByteString' uid]) - responseJsonError res +getUsersByHandle :: [Handle.Handle] -> TestM [User] +getUsersByHandle = getUsersBy "handles" upgradeClientToLH :: (HasCallStack) => UserId -> ClientId -> TestM () upgradeClientToLH zusr cid = @@ -2688,51 +2544,11 @@ withTempMockFederator' resp action = do $ \mockPort -> do withSettingsOverrides (\opts -> opts & Opts.federator ?~ Endpoint "127.0.0.1" (fromIntegral mockPort)) action --- Starts a servant Application in Network.Wai.Test session and runs the --- FederatedRequest against it. -makeFedRequestToServant :: - forall (api :: Type). - (HasServer api '[]) => - Domain -> - Server api -> - FederatedRequest -> - IO LByteString -makeFedRequestToServant originDomain server fedRequest = do - sresp <- Wai.runSession session app - let status = Wai.simpleStatus sresp - bdy = Wai.simpleBody sresp - if HTTP.statusIsSuccessful status - then pure bdy - else throw (Mock.MockErrorResponse status (LT.decodeUtf8 bdy)) - where - app :: Application - app = serve (Proxy @api) server - - session :: Wai.Session Wai.SResponse - session = do - Wai.srequest - ( Wai.SRequest - ( defaultRequest - { Wai.requestMethod = HTTP.methodPost, - Wai.pathInfo = [frRPC fedRequest], - Wai.requestHeaders = - [ (CI.mk "Content-Type", "application/json"), - (CI.mk "Accept", "application/json"), - (originDomainHeaderName, cs . domainText $ originDomain) - ] - } - ) - (frBody fedRequest) - ) - assertRight :: (MonadIO m, Show a, HasCallStack) => Either a b -> m b assertRight = \case Left e -> liftIO $ assertFailure $ "Expected Right, got Left: " <> show e Right x -> pure x -assertRightT :: (MonadIO m, Show a, HasCallStack) => ExceptT a m b -> m b -assertRightT = assertRight <=< runExceptT - -- | Run a probe several times, until a "good" value materializes or until patience runs out -- (after ~2secs). -- If all retries were unsuccessful, 'aFewTimes' will return the last obtained value, even @@ -2751,14 +2567,6 @@ aFewTimesAssertBool msg good action = do result <- aFewTimes action good liftIO $ assertBool msg (good result) -checkUserUpdateEvent :: (HasCallStack) => UserId -> WS.WebSocket -> TestM () -checkUserUpdateEvent uid w = WS.assertMatch_ checkTimeout w $ \notif -> do - let j = Object $ List1.head (ntfPayload notif) - let etype = j ^? key "type" . _String - let euser = j ^?! key "user" ^? key "id" . _String - etype @?= Just "user.update" - euser @?= Just (UUID.toText (toUUID uid)) - checkUserDeleteEvent :: (HasCallStack) => UserId -> WS.Timeout -> WS.WebSocket -> TestM () checkUserDeleteEvent uid timeout_ w = WS.assertMatch_ timeout_ w $ \notif -> do let j = Object $ List1.head (ntfPayload notif) @@ -2864,10 +2672,6 @@ mockedFederatedBrigResponse users = do guardComponent Brig mockReply [mkProfile mem (Name name) | (mem, name) <- users] -fedRequestsForDomain :: (HasCallStack) => Domain -> Component -> [FederatedRequest] -> [FederatedRequest] -fedRequestsForDomain domain component = - filter $ \req -> frTargetDomain req == domain && frComponent req == component - parseFedRequest :: (FromJSON a) => FederatedRequest -> Either String a parseFedRequest fr = eitherDecode (frBody fr) @@ -2879,14 +2683,6 @@ assertTwo :: (HasCallStack, Show a) => [a] -> (a, a) assertTwo [a, b] = (a, b) assertTwo xs = error $ "Expected two elements, found " <> show xs -assertThree :: (HasCallStack, Show a) => [a] -> (a, a, a) -assertThree [a1, a2, a3] = (a1, a2, a3) -assertThree xs = error $ "Expected three elements, found " <> show xs - -assertNone :: (HasCallStack, MonadIO m, Show a) => [a] -> m () -assertNone [] = pure () -assertNone xs = liftIO . error $ "Expected exactly no elements, found " <> show xs - assertJust :: (HasCallStack, MonadIO m) => Maybe a -> m a assertJust (Just a) = pure a assertJust Nothing = liftIO $ error "Expected Just, got Nothing" @@ -2918,16 +2714,10 @@ generateRemoteAndConvIdWithDomain :: Domain -> Bool -> Local UserId -> TestM (Re generateRemoteAndConvIdWithDomain remoteDomain shouldBeLocal lUserId = do other <- Qualified <$> randomId <*> pure remoteDomain let convId = one2OneConvId BaseProtocolProteusTag (tUntagged lUserId) other - isLocal = tDomain lUserId == qDomain convId - if shouldBeLocal == isLocal + if shouldBeLocal == isLocal lUserId convId then pure (qTagUnsafe other, convId) else generateRemoteAndConvIdWithDomain remoteDomain shouldBeLocal lUserId -matchFedRequest :: Domain -> Text -> FederatedRequest -> Bool -matchFedRequest domain reqpath req = - frTargetDomain req == domain - && frRPC req == reqpath - spawn :: (HasCallStack) => CreateProcess -> Maybe ByteString -> IO ByteString spawn cp minput = do (mout, ex) <- withCreateProcess @@ -3038,24 +2828,6 @@ createAndConnectUsers domains = do (False, False) -> pure () pure users -putConversationProtocol :: (MonadIO m, MonadHttp m, HasGalley m, HasCallStack) => UserId -> ClientId -> Qualified ConvId -> ProtocolTag -> m ResponseLBS -putConversationProtocol uid client (Qualified conv domain) protocol = do - galley <- viewGalley - put - ( galley - . paths ["conversations", toByteString' domain, toByteString' conv, "protocol"] - . zUser uid - . zConn "conn" - . zClient client - . Bilge.json (object ["protocol" .= protocol]) - ) - -assertMixedProtocol :: (MonadIO m, HasCallStack) => Conversation -> m ConversationMLSData -assertMixedProtocol conv = do - case cnvProtocol conv of - ProtocolMixed mlsData -> pure mlsData - _ -> liftIO $ assertFailure "Unexpected protocol" - connectBackend :: UserId -> Remote Backend -> TestM [Qualified UserId] connectBackend usr (tDomain &&& bUsers . tUnqualified -> (d, c)) = do users <- replicateM (fromIntegral c) (randomQualifiedId d) diff --git a/services/galley/test/integration/API/Util/TeamFeature.hs b/services/galley/test/integration/API/Util/TeamFeature.hs index 749ea934531..630f030e3f2 100644 --- a/services/galley/test/integration/API/Util/TeamFeature.hs +++ b/services/galley/test/integration/API/Util/TeamFeature.hs @@ -24,21 +24,15 @@ module API.Util.TeamFeature where import API.Util (HasGalley (viewGalley), zUser) import API.Util qualified as Util import Bilge -import Control.Lens ((.~), (^?)) -import Control.Monad.Catch (MonadThrow) -import Data.Aeson (FromJSON, Result (Success), ToJSON, Value, fromJSON) -import Data.Aeson.Key qualified as Key -import Data.Aeson.Lens +import Control.Lens ((.~)) +import Data.Aeson (ToJSON) import Data.ByteString.Conversion (toByteString') import Data.Id (ConvId, TeamId, UserId) -import Data.Schema import GHC.TypeLits (KnownSymbol) import Galley.Options (featureFlags, settings) import Galley.Types.Teams import Imports -import Test.Tasty.HUnit (assertBool, assertFailure, (@?=)) import TestSetup -import Wire.API.Team.Feature import Wire.API.Team.Feature qualified as Public withCustomSearchFeature :: FeatureTeamSearchVisibilityAvailability -> TestM () -> TestM () @@ -58,113 +52,6 @@ putTeamSearchVisibilityAvailableInternal tid statusValue = tid (Public.WithStatusNoLock statusValue Public.SearchVisibilityAvailableConfig Public.FeatureTTLUnlimited) -getTeamFeatureInternal :: - forall cfg m. - (HasGalley m, MonadIO m, MonadHttp m, KnownSymbol (Public.FeatureSymbol cfg)) => - TeamId -> - m ResponseLBS -getTeamFeatureInternal tid = do - g <- viewGalley - get $ - g - . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] - -getTeamFeature :: - forall cfg m. - (HasGalley m, MonadIO m, MonadHttp m, HasCallStack, KnownSymbol (Public.FeatureSymbol cfg)) => - UserId -> - TeamId -> - m ResponseLBS -getTeamFeature uid tid = do - galley <- viewGalley - get $ - galley - . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] - . zUser uid - -getAllTeamFeatures :: - (HasCallStack, HasGalley m, MonadIO m, MonadHttp m) => - UserId -> - TeamId -> - m ResponseLBS -getAllTeamFeatures uid tid = do - g <- viewGalley - get $ - g - . paths ["teams", toByteString' tid, "features"] - . zUser uid - -getTeamFeatureFromAll :: - forall cfg m. - ( HasCallStack, - MonadThrow m, - HasGalley m, - MonadIO m, - MonadHttp m, - KnownSymbol (Public.FeatureSymbol cfg), - FromJSON (Public.WithStatus cfg) - ) => - UserId -> - TeamId -> - m (Public.WithStatus cfg) -getTeamFeatureFromAll uid tid = do - response :: Value <- responseJsonError =<< getAllTeamFeatures uid tid - let status = response ^? key (Key.fromText (Public.featureName @cfg)) - maybe (error "getting all features failed") pure (status >>= fromResult . fromJSON) - where - fromResult :: Result a -> Maybe a - fromResult (Success b) = Just b - fromResult _ = Nothing - -getAllFeatureConfigs :: - (HasCallStack, HasGalley m, Monad m, MonadHttp m) => - UserId -> - m ResponseLBS -getAllFeatureConfigs uid = do - g <- viewGalley - get $ - g - . paths ["feature-configs"] - . zUser uid - -getFeatureConfig :: - forall cfg m. - ( HasCallStack, - MonadThrow m, - HasGalley m, - MonadHttp m, - KnownSymbol (Public.FeatureSymbol cfg), - FromJSON (Public.WithStatus cfg) - ) => - UserId -> - m (Public.WithStatus cfg) -getFeatureConfig uid = do - response :: Value <- responseJsonError =<< getAllFeatureConfigs uid - let status = response ^? key (Key.fromText (Public.featureName @cfg)) - maybe (error "getting all feature configs failed") pure (status >>= fromResult . fromJSON) - where - fromResult :: Result a -> Maybe a - fromResult (Success b) = Just b - fromResult _ = Nothing - -putTeamFeature :: - forall cfg. - ( HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg), - ToJSON (Public.WithStatusNoLock cfg) - ) => - UserId -> - TeamId -> - Public.WithStatusNoLock cfg -> - TestM ResponseLBS -putTeamFeature uid tid status = do - galley <- viewGalley - put $ - galley - . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] - . json status - . zUser uid - putTeamFeatureInternal :: forall cfg m. ( Monad m, @@ -186,50 +73,23 @@ putTeamFeatureInternal reqmod tid status = do . json status . reqmod -setLockStatusInternal :: - forall cfg. - ( HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg) - ) => - (Request -> Request) -> - TeamId -> - Public.LockStatus -> - TestM ResponseLBS -setLockStatusInternal reqmod tid lockStatus = do - galley <- viewGalley - put $ - galley - . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg, toByteString' lockStatus] - . reqmod - -patchTeamFeatureInternal :: - forall cfg. - ( HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg), - ToSchema cfg - ) => - TeamId -> - Public.WithStatusPatch cfg -> - TestM ResponseLBS -patchTeamFeatureInternal = patchTeamFeatureInternalWithMod id - -patchTeamFeatureInternalWithMod :: +putTeamFeature :: forall cfg. ( HasCallStack, KnownSymbol (Public.FeatureSymbol cfg), - ToSchema cfg + ToJSON (Public.WithStatusNoLock cfg) ) => - (Request -> Request) -> + UserId -> TeamId -> - Public.WithStatusPatch cfg -> + Public.WithStatusNoLock cfg -> TestM ResponseLBS -patchTeamFeatureInternalWithMod reqmod tid reqBody = do +putTeamFeature uid tid status = do galley <- viewGalley - patch $ + put $ galley - . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] - . json reqBody - . reqmod + . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . json status + . zUser uid getGuestLinkStatus :: (HasCallStack) => @@ -243,43 +103,26 @@ getGuestLinkStatus galley u cid = . paths ["conversations", toByteString' cid, "features", Public.featureNameBS @Public.GuestLinksConfig] . zUser u -checkTeamFeatureAllEndpoints :: - forall cfg. - ( HasCallStack, - IsFeatureConfig cfg, - ToSchema cfg, - Typeable cfg, - Eq cfg, - Show cfg, - KnownSymbol (FeatureSymbol cfg) - ) => - UserId -> +getTeamFeatureInternal :: + forall cfg m. + (HasGalley m, MonadIO m, MonadHttp m, KnownSymbol (Public.FeatureSymbol cfg)) => TeamId -> - WithStatus cfg -> - TestM () -checkTeamFeatureAllEndpoints uid tid expected = do - compareLeniently $ responseJsonUnsafe <$> getTeamFeatureInternal @cfg tid - compareLeniently $ responseJsonUnsafe <$> getTeamFeature @cfg uid tid - compareLeniently $ getTeamFeatureFromAll @cfg uid tid - compareLeniently $ getFeatureConfig uid - where - compareLeniently :: TestM (WithStatus cfg) -> TestM () - compareLeniently receive = do - received <- receive - liftIO $ do - wsStatus received @?= wsStatus expected - wsLockStatus received @?= wsLockStatus expected - wsConfig received @?= wsConfig expected - checkTtl (wsTTL received) (wsTTL expected) + m ResponseLBS +getTeamFeatureInternal tid = do + g <- viewGalley + get $ + g + . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] - checkTtl :: FeatureTTL -> FeatureTTL -> IO () - checkTtl (FeatureTTLSeconds actualTtl) (FeatureTTLSeconds expectedTtl) = - assertBool - ("expected the actual TTL to be greater than 0 and equal to or no more than 2 seconds less than " <> show expectedTtl <> ", but it was " <> show actualTtl) - ( actualTtl > 0 - && actualTtl <= expectedTtl - && abs (fromIntegral @Word @Int actualTtl - fromIntegral @Word @Int expectedTtl) <= 2 - ) - checkTtl FeatureTTLUnlimited FeatureTTLUnlimited = pure () - checkTtl FeatureTTLUnlimited _ = assertFailure "expected the actual TTL to be unlimited, but it was limited" - checkTtl _ FeatureTTLUnlimited = assertFailure "expected the actual TTL to be limited, but it was unlimited" +getTeamFeature :: + forall cfg m. + (HasGalley m, MonadIO m, MonadHttp m, HasCallStack, KnownSymbol (Public.FeatureSymbol cfg)) => + UserId -> + TeamId -> + m ResponseLBS +getTeamFeature uid tid = do + galley <- viewGalley + get $ + galley + . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . zUser uid diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index b925700365e..dedd9cc1dab 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -218,7 +218,6 @@ mkDerivation { tasty-hunit tasty-quickcheck text - time tinylog types-common wai-utilities diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index e2150a6251c..2c4777a19b2 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -556,7 +556,6 @@ test-suite gundeck-tests , tasty-hunit , tasty-quickcheck , text - , time , tinylog , types-common , wai-utilities diff --git a/services/gundeck/test/unit/ThreadBudget.hs b/services/gundeck/test/unit/ThreadBudget.hs index 7715d8c8a7c..b5953867211 100644 --- a/services/gundeck/test/unit/ThreadBudget.hs +++ b/services/gundeck/test/unit/ThreadBudget.hs @@ -30,7 +30,6 @@ import Control.Concurrent.Async import Control.Lens import Control.Monad.Catch (MonadCatch, catch) import Data.String.Conversions -import Data.Time import GHC.Generics import Gundeck.Options import Gundeck.ThreadBudget.Internal @@ -56,16 +55,6 @@ newtype NumberOfThreads = NumberOfThreads {fromNumberOfThreads :: Int} newtype MilliSeconds = MilliSeconds {fromMilliSeconds :: Int} deriving (Eq, Ord, Show, Generic, ToExpr) --- toMillisecondsCeiling 0.03 == MilliSeconds 30 --- toMillisecondsCeiling 0.003 == MilliSeconds 3 --- toMillisecondsCeiling 0.0003 == MilliSeconds 1 --- toMillisecondsCeiling 0.0000003 == MilliSeconds 1 -toMillisecondsCeiling :: NominalDiffTime -> MilliSeconds -toMillisecondsCeiling = MilliSeconds . ceiling . (* 1000) . toRational - -milliSecondsToNominalDiffTime :: MilliSeconds -> NominalDiffTime -milliSecondsToNominalDiffTime = fromRational . (/ 1000) . toRational . fromMilliSeconds - instance Arbitrary NumberOfThreads where arbitrary = NumberOfThreads <$> choose (1, 30) shrink (NumberOfThreads n) = NumberOfThreads <$> filter (> 0) (shrink n) @@ -112,9 +101,6 @@ instance LC.MonadLogger (ReaderT LogHistory IO) where delayms :: (MonadCatch m, MonadIO m) => MilliSeconds -> m () delayms = delay' . (* 1000) . fromMilliSeconds -delayndt :: (MonadCatch m, MonadIO m) => NominalDiffTime -> m () -delayndt = delay' . round . (* 1000) . (* 1000) . toRational - delay' :: (MonadCatch m, MonadIO m) => Int -> m () delay' microsecs = threadDelay microsecs `catch` \AsyncCancelled -> pure () @@ -146,14 +132,15 @@ tests = "thread budgets" [ -- flaky test case as described in https://wearezeta.atlassian.net/browse/BE-527 -- testCase "unit test" testThreadBudgets, - testProperty "qc stm (sequential)" propSequential + testProperty "qc stm (sequential)" propSequential, + testCase "thread buckets" testThreadBudgets ] ---------------------------------------------------------------------- -- deterministic unit test -_testThreadBudgets :: Assertion -_testThreadBudgets = do +testThreadBudgets :: Assertion +testThreadBudgets = do let timeUnits n = MilliSeconds $ lengthOfTimeUnit * n lengthOfTimeUnit = 5 -- if you make this larger, the test will run more slowly, and be -- less likely to have timing issues. if you make it too small, some of the calls to diff --git a/services/proxy/src/Proxy/API/Public.hs b/services/proxy/src/Proxy/API/Public.hs index 03fd4b65bd1..a33bafb1d9c 100644 --- a/services/proxy/src/Proxy/API/Public.hs +++ b/services/proxy/src/Proxy/API/Public.hs @@ -110,7 +110,7 @@ proxy e qparam keyname reroute path phost rq k = do else runProxy e waiReq (k res) onUpstreamError runInIO x _ next = do void . runInIO $ Logger.warn (msg (val "gateway error") ~~ field "error" (show x)) - next (errorRs' error502) + next (errorRs error502) spotifyToken :: Request -> Proxy Response spotifyToken rq = do diff --git a/weeder.toml b/weeder.toml index 5e2042081e4..e0ddc4c24e5 100644 --- a/weeder.toml +++ b/weeder.toml @@ -1,5 +1,57 @@ # weeder intro and further reading: https://github.com/ocharles/weeder?tab=readme-ov-file#weeder -roots = ["^Main.main$", "^Paths_.*", "^Testlib.RunServices.main$", "^Testlib.Run.main$", "^Test.Wire.API.Golden.Run.main$"] + +roots = [ # may of the entries here are about general-purpose module + # interfaces that make sense as a whole, but are *currently* + # only used in part. it's possible that we should remove + # those entries here and extend the tests to cover them. + + "^API.Cargohold.getFederationAsset", # FUTUREWORK: write tests that need this! + "^API.Cargohold.uploadAssetV3", # FUTUREWORK: write tests that need this! + "^API.Galley.consentToLegalHold", # FUTUREWORK: write tests that need this! + "^API.Galley.enableLegalHold", # FUTUREWORK: write tests that need this! + "^API.Galley.getLegalHoldStatus", # FUTUREWORK: write tests that need this! + "^Data.ETag.opaqueDigest", + "^Data.ETag._OpaqueDigest", + "^Data.ETag.opaqueMD5", + "^Data.ETag.opaqueSHA1", + "^Data.ETag.strictETag", + "^Data.ETag._StrictETag", + "^Data.ETag.weakETag", + "^Data.ETag._WeakETag", + "^Data.Qualified.isLocal", + "^Data.Range.(<|)", + "^Data.Range.rappend", + "^Data.Range.rcons", + "^Data.Range.rinc", + "^Data.Range.rsingleton", + "^Imports.getChar", + "^Imports.getContents", + "^Imports.interact", + "^Imports.putChar", + "^Imports.readIO", + "^Imports.readLn", + "^Main.main$", + "^Paths_.*", + "^Test.Cargohold.API.Util.shouldMatchALittle", + "^Test.Cargohold.API.Util.shouldMatchLeniently", + "^Test.Cargohold.API.Util.shouldMatchSloppily", + "^Testlib.JSON.(<$$$>)", + "^Testlib.JSON.member", + "^Testlib.Prelude.appendFile", + "^Testlib.Prelude.getChar", + "^Testlib.Prelude.getContents", + "^Testlib.Prelude.getLine", + "^Testlib.Prelude.interact", + "^Testlib.Prelude.readFile", + "^Testlib.Prelude.readIO", + "^Testlib.Prelude.readLn", + "^Testlib.Prelude.writeFile", + "^Testlib.Printing.gray", + "^Testlib.Printing.hline", + "^Testlib.Run.main$", + "^Testlib.RunServices.main$", + "^Test.Wire.API.Golden.Run.main$" + ] type-class-roots = true # `root-instances` is more precise, but requires more config maintenance. # FUTUREWORK: unused-types = true From 96300a72a6f15d791c422f1cebbf48a1a77d3b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Mon, 29 Jul 2024 12:09:55 +0200 Subject: [PATCH 020/136] [WPB-8707] Remove phone functionality in the development client API version (#4149) * Drop endpoints for updating and removing phone num * Remove a few phone endpoints * Drop phone from desc's of /activate endpoints * remove obsolete tests * Drop 'phone' from request in `POST /activate/send` * Drop phone from team invitations * Drop 'phone' from Brig's 'team_invitation' DB table * Add a changelog * Drop phone from the invitation request body in V6 * Give a versioned 'Invitation' response For client API versions up to and including V5, the response is the same and the 'phone' field is always null. The field does not exist in versions V6 and above. * Ignore phone when registering a user The response to `POST /register` no longer throws an error when the request contains a phone number. Instead, the "phone" and "phone_code" fields are not parsed and are therefore ignored. * Drop meaningless phone login tests The tests should have been dropped when removing phone number support in client API versions v0..v5 as they lost meaning at least then, if not even before that. * Delete a misguiding test in brig-integration The test's name is suggesting one, but testing a different thing. The test does not make much sense in the current situation. * Delete another meaningless test * Drop phone from `POST /login` in client API v6 * Drop phone from request for `POST /activate` in v6 * Add a changelog on the API change * remove everything V5 related from wire-api * updated brig after removing V5 types * Adjust expectation in phone v5 test --------- Co-authored-by: Leif Battermann --- cassandra-schema.cql | 1 - changelog.d/0-release-notes/WPB-8707 | 1 + changelog.d/1-api-changes/WPB-8707 | 1 + integration/test/Test/User.hs | 2 +- .../src/Wire/API/Routes/Public/Brig.hs | 8 +- libs/wire-api/src/Wire/API/Team/Invitation.hs | 30 +-- libs/wire-api/src/Wire/API/User.hs | 20 -- libs/wire-api/src/Wire/API/User/Activation.hs | 82 ++----- libs/wire-api/src/Wire/API/User/Auth.hs | 106 +++----- .../golden/Test/Wire/API/Golden/Generated.hs | 15 -- .../API/Golden/Generated/Activate_user.hs | 192 --------------- .../Golden/Generated/InvitationList_team.hs | 66 +---- .../Generated/InvitationRequest_team.hs | 182 +++++++------- .../API/Golden/Generated/Invitation_team.hs | 22 +- .../Wire/API/Golden/Generated/LoginId_user.hs | 119 --------- .../Wire/API/Golden/Generated/Login_user.hs | 227 ------------------ .../Golden/Generated/NewUserPublic_user.hs | 2 - .../Wire/API/Golden/Generated/NewUser_user.hs | 14 -- .../Generated/SendActivationCode_user.hs | 201 ---------------- .../golden/Test/Wire/API/Golden/Manual.hs | 33 +++ .../Wire/API/Golden/Manual/Activate_user.hs | 56 +++++ .../Wire/API/Golden/Manual/LoginId_user.hs | 51 ++++ .../Test/Wire/API/Golden/Manual/Login_user.hs | 80 ++++++ .../Golden/Manual/SendActivationCode_user.hs | 39 +++ .../golden/testObject_Activate_user_1.json | 6 +- .../golden/testObject_Activate_user_10.json | 5 - .../golden/testObject_Activate_user_11.json | 5 - .../golden/testObject_Activate_user_12.json | 5 - .../golden/testObject_Activate_user_13.json | 5 - .../golden/testObject_Activate_user_14.json | 5 - .../golden/testObject_Activate_user_15.json | 5 - .../golden/testObject_Activate_user_16.json | 5 - .../golden/testObject_Activate_user_17.json | 5 - .../golden/testObject_Activate_user_18.json | 5 - .../golden/testObject_Activate_user_19.json | 5 - .../golden/testObject_Activate_user_2.json | 4 +- .../golden/testObject_Activate_user_20.json | 5 - .../golden/testObject_Activate_user_3.json | 4 +- .../golden/testObject_Activate_user_4.json | 6 +- .../golden/testObject_Activate_user_5.json | 5 - .../golden/testObject_Activate_user_6.json | 5 - .../golden/testObject_Activate_user_7.json | 5 - .../golden/testObject_Activate_user_8.json | 5 - .../golden/testObject_Activate_user_9.json | 5 - .../testObject_InvitationList_team_10.json | 1 - .../testObject_InvitationList_team_11.json | 1 - .../testObject_InvitationList_team_13.json | 7 - .../testObject_InvitationList_team_15.json | 5 - .../testObject_InvitationList_team_16.json | 1 - .../testObject_InvitationList_team_17.json | 1 - .../testObject_InvitationList_team_2.json | 1 - .../testObject_InvitationList_team_20.json | 2 - .../testObject_InvitationList_team_4.json | 8 - .../testObject_InvitationList_team_6.json | 15 -- .../testObject_InvitationList_team_7.json | 3 - .../testObject_InvitationList_team_8.json | 2 - .../testObject_InvitationList_team_9.json | 3 - .../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 - .../golden/testObject_Invitation_team_1.json | 1 - .../golden/testObject_Invitation_team_10.json | 1 - .../golden/testObject_Invitation_team_11.json | 1 - .../golden/testObject_Invitation_team_12.json | 1 - .../golden/testObject_Invitation_team_13.json | 1 - .../golden/testObject_Invitation_team_14.json | 1 - .../golden/testObject_Invitation_team_15.json | 1 - .../golden/testObject_Invitation_team_16.json | 1 - .../golden/testObject_Invitation_team_17.json | 1 - .../golden/testObject_Invitation_team_18.json | 1 - .../golden/testObject_Invitation_team_19.json | 1 - .../golden/testObject_Invitation_team_2.json | 1 - .../golden/testObject_Invitation_team_20.json | 1 - .../golden/testObject_Invitation_team_3.json | 1 - .../golden/testObject_Invitation_team_4.json | 1 - .../golden/testObject_Invitation_team_5.json | 1 - .../golden/testObject_Invitation_team_6.json | 1 - .../golden/testObject_Invitation_team_7.json | 1 - .../golden/testObject_Invitation_team_8.json | 1 - .../golden/testObject_Invitation_team_9.json | 1 - .../golden/testObject_LoginId_user_10.json | 3 - .../golden/testObject_LoginId_user_11.json | 3 - .../golden/testObject_LoginId_user_12.json | 3 - .../golden/testObject_LoginId_user_13.json | 3 - .../golden/testObject_LoginId_user_14.json | 3 - .../golden/testObject_LoginId_user_15.json | 3 - .../golden/testObject_LoginId_user_16.json | 3 - .../golden/testObject_LoginId_user_17.json | 3 - .../golden/testObject_LoginId_user_18.json | 3 - .../golden/testObject_LoginId_user_19.json | 3 - .../golden/testObject_LoginId_user_2.json | 2 +- .../golden/testObject_LoginId_user_20.json | 3 - .../golden/testObject_LoginId_user_3.json | 2 +- .../golden/testObject_LoginId_user_4.json | 2 +- .../golden/testObject_LoginId_user_5.json | 2 +- .../golden/testObject_LoginId_user_6.json | 2 +- .../golden/testObject_LoginId_user_7.json | 3 - .../golden/testObject_LoginId_user_8.json | 3 - .../golden/testObject_LoginId_user_9.json | 3 - .../test/golden/testObject_Login_user_10.json | 5 - .../test/golden/testObject_Login_user_11.json | 5 - .../test/golden/testObject_Login_user_12.json | 6 - .../test/golden/testObject_Login_user_13.json | 5 - .../test/golden/testObject_Login_user_14.json | 5 - .../test/golden/testObject_Login_user_15.json | 5 - .../test/golden/testObject_Login_user_16.json | 5 - .../test/golden/testObject_Login_user_17.json | 5 - .../test/golden/testObject_Login_user_18.json | 5 - .../test/golden/testObject_Login_user_19.json | 5 - .../test/golden/testObject_Login_user_2.json | 7 +- .../test/golden/testObject_Login_user_20.json | 6 - .../test/golden/testObject_Login_user_3.json | 6 +- .../test/golden/testObject_Login_user_4.json | 7 +- .../test/golden/testObject_Login_user_5.json | 6 +- .../test/golden/testObject_Login_user_6.json | 6 - .../test/golden/testObject_Login_user_7.json | 6 - .../test/golden/testObject_Login_user_8.json | 6 - .../test/golden/testObject_Login_user_9.json | 6 - .../golden/testObject_NewUser_user_9.json | 30 --- .../testObject_SendActivationCode_1.json | 3 + ...n => testObject_SendActivationCode_2.json} | 2 +- .../testObject_SendActivationCode_user_1.json | 5 - ...testObject_SendActivationCode_user_10.json | 4 - ...testObject_SendActivationCode_user_11.json | 5 - ...testObject_SendActivationCode_user_12.json | 5 - ...testObject_SendActivationCode_user_13.json | 5 - ...testObject_SendActivationCode_user_14.json | 5 - ...testObject_SendActivationCode_user_15.json | 5 - ...testObject_SendActivationCode_user_16.json | 5 - ...testObject_SendActivationCode_user_17.json | 5 - ...testObject_SendActivationCode_user_18.json | 5 - ...testObject_SendActivationCode_user_19.json | 5 - .../testObject_SendActivationCode_user_2.json | 5 - ...testObject_SendActivationCode_user_20.json | 4 - .../testObject_SendActivationCode_user_3.json | 5 - .../testObject_SendActivationCode_user_5.json | 4 - .../testObject_SendActivationCode_user_6.json | 5 - .../testObject_SendActivationCode_user_7.json | 4 - .../testObject_SendActivationCode_user_8.json | 5 - .../testObject_SendActivationCode_user_9.json | 4 - .../test/unit/Test/Wire/API/User/Auth.hs | 8 +- libs/wire-api/wire-api.cabal | 8 +- services/brig/brig.cabal | 1 + services/brig/src/Brig/API/Public.hs | 12 +- services/brig/src/Brig/API/User.hs | 8 +- services/brig/src/Brig/Schema/Run.hs | 6 +- .../Schema/V84_DropTeamInvitationPhone.hs | 33 +++ services/brig/src/Brig/Team/API.hs | 63 ++--- services/brig/src/Brig/Team/DB.hs | 28 +-- services/brig/src/Brig/User/Auth.hs | 9 +- services/brig/test/integration/API/Team.hs | 19 +- .../brig/test/integration/API/Team/Util.hs | 2 +- .../brig/test/integration/API/User/Account.hs | 102 +------- .../brig/test/integration/API/User/Auth.hs | 41 +--- .../brig/test/integration/API/User/Client.hs | 6 +- .../integration/API/User/PasswordReset.hs | 2 +- services/brig/test/integration/Util.hs | 11 +- services/galley/test/integration/API/Util.hs | 2 +- services/spar/test-integration/Util/Core.hs | 2 +- services/spar/test-integration/Util/Email.hs | 3 +- tools/stern/test/integration/Util.hs | 6 +- 178 files changed, 570 insertions(+), 1826 deletions(-) create mode 100644 changelog.d/0-release-notes/WPB-8707 create mode 100644 changelog.d/1-api-changes/WPB-8707 delete mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Activate_user.hs delete mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LoginId_user.hs delete mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Login_user.hs delete mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SendActivationCode_user.hs create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Activate_user.hs create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Login_user.hs create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/SendActivationCode_user.hs delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_10.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_11.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_12.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_13.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_14.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_15.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_16.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_17.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_18.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_19.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_20.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_5.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_6.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_7.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_8.json delete mode 100644 libs/wire-api/test/golden/testObject_Activate_user_9.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_10.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_11.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_12.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_13.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_14.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_15.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_16.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_17.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_18.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_19.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_20.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_7.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_8.json delete mode 100644 libs/wire-api/test/golden/testObject_LoginId_user_9.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_10.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_11.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_12.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_13.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_14.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_15.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_16.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_17.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_18.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_19.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_20.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_6.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_7.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_8.json delete mode 100644 libs/wire-api/test/golden/testObject_Login_user_9.json delete mode 100644 libs/wire-api/test/golden/testObject_NewUser_user_9.json create mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_1.json rename libs/wire-api/test/golden/{testObject_SendActivationCode_user_4.json => testObject_SendActivationCode_2.json} (64%) delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_1.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_10.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_11.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_12.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_13.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_14.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_15.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_16.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_17.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_18.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_19.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_2.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_20.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_3.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_5.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_6.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_7.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_8.json delete mode 100644 libs/wire-api/test/golden/testObject_SendActivationCode_user_9.json create mode 100644 services/brig/src/Brig/Schema/V84_DropTeamInvitationPhone.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 6a19d682539..ccb834c1c8e 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -643,7 +643,6 @@ CREATE TABLE brig_test.team_invitation ( created_by uuid, email text, name text, - phone text, role int, PRIMARY KEY (team, id) ) WITH CLUSTERING ORDER BY (id ASC) diff --git a/changelog.d/0-release-notes/WPB-8707 b/changelog.d/0-release-notes/WPB-8707 new file mode 100644 index 00000000000..5e4ad202600 --- /dev/null +++ b/changelog.d/0-release-notes/WPB-8707 @@ -0,0 +1 @@ +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/1-api-changes/WPB-8707 b/changelog.d/1-api-changes/WPB-8707 new file mode 100644 index 00000000000..47f0ca8d6ef --- /dev/null +++ b/changelog.d/1-api-changes/WPB-8707 @@ -0,0 +1 @@ +All the phone number-based functionality is removed from the client API v6 diff --git a/integration/test/Test/User.hs b/integration/test/Test/User.hs index 7002de72e49..3c6cfdb9694 100644 --- a/integration/test/Test/User.hs +++ b/integration/test/Test/User.hs @@ -173,4 +173,4 @@ testActivateAccountWithPhoneV5 = do let reqBody = Aeson.object ["phone" .= phone] activateUserV5 dom reqBody `bindResponse` \resp -> do resp.status `shouldMatchInt` 400 - resp.json %. "label" `shouldMatch` "invalid-phone" + resp.json %. "label" `shouldMatch` "bad-request" 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 8a9cbfc0842..d85673496e0 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -343,6 +343,7 @@ type SelfAPI = :<|> Named "change-phone" ( Summary "Change your phone number." + :> Until 'V6 :> ZUser :> ZConn :> "self" @@ -356,6 +357,7 @@ type SelfAPI = Named "remove-phone" ( Summary "Remove your phone number." + :> Until 'V6 :> Description "Your phone number can only be removed if you also have an \ \email address and a password." @@ -503,7 +505,7 @@ type AccountAPI = -- - UserIdentityUpdated event to the user, if email or phone get activated :<|> Named "get-activate" - ( Summary "Activate (i.e. confirm) an email address or phone number." + ( Summary "Activate (i.e. confirm) an email address." :> MakesFederatedCall 'Brig "send-connection-action" :> Description "See also 'POST /activate' which has a larger feature set." :> CanThrow 'UserKeyExists @@ -527,7 +529,7 @@ type AccountAPI = -- - UserIdentityUpdated event to the user, if email or phone get activated :<|> Named "post-activate" - ( Summary "Activate (i.e. confirm) an email address or phone number." + ( Summary "Activate (i.e. confirm) an email address." :> Description "Activation only succeeds once and the number of \ \failed attempts for a valid key is limited." @@ -551,7 +553,6 @@ type AccountAPI = ( Summary "Send (or resend) an email activation code." :> CanThrow 'UserKeyExists :> CanThrow 'InvalidEmail - :> CanThrow 'InvalidPhone :> CanThrow 'BlacklistedEmail :> CanThrow 'CustomerExtensionBlockedDomain :> "activate" @@ -1417,6 +1418,7 @@ type AuthAPI = "send-login-code" ( "login" :> "send" + :> Until 'V6 :> Summary "Send a login code to a verified phone number" :> Description "This operation generates and sends a login code via sms for phone login.\ diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index c51492dc19c..e712dc520e1 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -44,7 +44,7 @@ import Wire.API.Error.Brig import Wire.API.Locale (Locale) import Wire.API.Routes.MultiVerb import Wire.API.Team.Role (Role, defaultRole) -import Wire.API.User.Identity (Email, Phone) +import Wire.API.User.Identity (Email) import Wire.API.User.Profile (Name) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -52,11 +52,10 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- InvitationRequest data InvitationRequest = InvitationRequest - { irLocale :: Maybe Locale, - irRole :: Maybe Role, - irInviteeName :: Maybe Name, - irInviteeEmail :: Email, - irInviteePhone :: Maybe Phone + { locale :: Maybe Locale, + role :: Maybe Role, + inviteeName :: Maybe Name, + inviteeEmail :: Email } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform InvitationRequest) @@ -66,16 +65,14 @@ instance ToSchema InvitationRequest where schema = objectWithDocModifier "InvitationRequest" (description ?~ "A request to join a team on Wire.") $ InvitationRequest - <$> irLocale + <$> locale .= optFieldWithDocModifier "locale" (description ?~ "Locale to use for the invitation.") (maybeWithDefault A.Null schema) - <*> irRole + <*> role .= optFieldWithDocModifier "role" (description ?~ "Role of the invitee (invited user).") (maybeWithDefault A.Null schema) - <*> irInviteeName + <*> inviteeName .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters).") (maybeWithDefault A.Null schema) - <*> irInviteeEmail + <*> inviteeEmail .= fieldWithDocModifier "email" (description ?~ "Email of the invitee.") schema - <*> irInviteePhone - .= optFieldWithDocModifier "phone" (description ?~ "Phone number of the invitee, in the E.164 format.") (maybeWithDefault A.Null schema) -------------------------------------------------------------------------------- -- Invitation @@ -90,7 +87,6 @@ data Invitation = Invitation inCreatedBy :: Maybe UserId, inInviteeEmail :: Email, inInviteeName :: Maybe Name, - inInviteePhone :: Maybe Phone, inInviteeUrl :: Maybe (URIRef Absolute) } deriving stock (Eq, Show, Generic) @@ -99,8 +95,10 @@ data Invitation = Invitation instance ToSchema Invitation where schema = - objectWithDocModifier "Invitation" (description ?~ "An invitation to join a team on Wire") $ - Invitation + objectWithDocModifier + "Invitation" + (description ?~ "An invitation to join a team on Wire") + $ Invitation <$> inTeam .= fieldWithDocModifier "team" (description ?~ "Team ID of the inviting team") schema <*> inRole @@ -116,8 +114,6 @@ instance ToSchema Invitation where .= fieldWithDocModifier "email" (description ?~ "Email of the invitee") schema <*> inInviteeName .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters)") (maybeWithDefault A.Null schema) - <*> inInviteePhone - .= optFieldWithDocModifier "phone" (description ?~ "Phone number of the invitee, in the E.164 format") (maybeWithDefault A.Null schema) <*> (fmap (TE.decodeUtf8 . serializeURIRef') . inInviteeUrl) .= optFieldWithDocModifier "url" (description ?~ "URL of the invitation link to be sent to the invitee") (maybeWithDefault A.Null urlSchema) where diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 91ab3f61fd4..9477ace1dc0 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -948,12 +948,10 @@ newUserFromSpar new = { newUserDisplayName = newUserSparDisplayName new, newUserUUID = Just $ newUserSparUUID new, newUserIdentity = Just $ SSOIdentity (newUserSparSSOId new) Nothing, - newUserPhone = Nothing, newUserPict = Nothing, newUserAssets = [], newUserAccentId = Nothing, newUserEmailCode = Nothing, - newUserPhoneCode = Nothing, newUserOrigin = Just . NewUserOriginTeamUser . NewTeamMemberSSO $ newUserSparTeamId new, newUserLabel = Nothing, newUserPassword = Nothing, @@ -968,13 +966,11 @@ data NewUser = NewUser -- | use this as 'UserId' (if 'Nothing', call 'Data.UUID.nextRandom'). newUserUUID :: Maybe UUID, newUserIdentity :: Maybe UserIdentity, - newUserPhone :: Maybe Phone, -- | DEPRECATED newUserPict :: Maybe Pict, newUserAssets :: [Asset], newUserAccentId :: Maybe ColourId, newUserEmailCode :: Maybe ActivationCode, - newUserPhoneCode :: Maybe ActivationCode, newUserOrigin :: Maybe NewUserOrigin, newUserLabel :: Maybe CookieLabel, newUserLocale :: Maybe Locale, @@ -992,12 +988,10 @@ emptyNewUser name = { newUserDisplayName = name, newUserUUID = Nothing, newUserIdentity = Nothing, - newUserPhone = Nothing, newUserPict = Nothing, newUserAssets = [], newUserAccentId = Nothing, newUserEmailCode = Nothing, - newUserPhoneCode = Nothing, newUserOrigin = Nothing, newUserLabel = Nothing, newUserLocale = Nothing, @@ -1015,16 +1009,12 @@ data NewUserRaw = NewUserRaw { newUserRawDisplayName :: Name, newUserRawUUID :: Maybe UUID, newUserRawEmail :: Maybe Email, - -- | This is deprecated and it should always be 'Nothing'. - newUserRawPhone :: Maybe Phone, newUserRawSSOId :: Maybe UserSSOId, -- | DEPRECATED newUserRawPict :: Maybe Pict, newUserRawAssets :: [Asset], newUserRawAccentId :: Maybe ColourId, newUserRawEmailCode :: Maybe ActivationCode, - -- | This is deprecated and it should always be 'Nothing'. - newUserRawPhoneCode :: Maybe ActivationCode, newUserRawInvitationCode :: Maybe InvitationCode, newUserRawTeamCode :: Maybe InvitationCode, newUserRawTeam :: Maybe BindingNewTeamUser, @@ -1046,8 +1036,6 @@ newUserRawObjectSchema = .= maybe_ (optField "uuid" genericToSchema) <*> newUserRawEmail .= maybe_ (optField "email" schema) - <*> newUserRawPhone - .= maybe_ (optField "phone" schema) <*> newUserRawSSOId .= maybe_ (optField "sso_id" genericToSchema) <*> newUserRawPict @@ -1058,8 +1046,6 @@ newUserRawObjectSchema = .= maybe_ (optField "accent_id" schema) <*> newUserRawEmailCode .= maybe_ (optField "email_code" schema) - <*> newUserRawPhoneCode - .= maybe_ (optField "phone_code" schema) <*> newUserRawInvitationCode .= maybe_ (optField "invitation_code" schema) <*> newUserRawTeamCode @@ -1092,13 +1078,11 @@ newUserToRaw NewUser {..} = { newUserRawDisplayName = newUserDisplayName, newUserRawUUID = newUserUUID, newUserRawEmail = emailIdentity =<< newUserIdentity, - newUserRawPhone = newUserPhone, newUserRawSSOId = ssoIdentity =<< newUserIdentity, newUserRawPict = newUserPict, newUserRawAssets = newUserAssets, newUserRawAccentId = newUserAccentId, newUserRawEmailCode = newUserEmailCode, - newUserRawPhoneCode = newUserPhoneCode, newUserRawInvitationCode = newUserOriginInvitationCode =<< newUserOrigin, newUserRawTeamCode = newTeamUserCode =<< maybeOriginNTU, newUserRawTeam = newTeamUserCreator =<< maybeOriginNTU, @@ -1131,12 +1115,10 @@ newUserFromRaw NewUserRaw {..} = do { newUserDisplayName = newUserRawDisplayName, newUserUUID = newUserRawUUID, newUserIdentity = identity, - newUserPhone = newUserRawPhone, newUserPict = newUserRawPict, newUserAssets = newUserRawAssets, newUserAccentId = newUserRawAccentId, newUserEmailCode = newUserRawEmailCode, - newUserPhoneCode = newUserRawPhoneCode, newUserOrigin = origin, newUserLabel = newUserRawLabel, newUserLocale = newUserRawLocale, @@ -1150,7 +1132,6 @@ newUserFromRaw NewUserRaw {..} = do instance Arbitrary NewUser where arbitrary = do newUserIdentity <- arbitrary - newUserPhone <- arbitrary newUserOrigin <- genUserOrigin newUserIdentity newUserDisplayName <- arbitrary newUserUUID <- QC.elements [Just nil, Nothing] @@ -1158,7 +1139,6 @@ instance Arbitrary NewUser where newUserAssets <- arbitrary newUserAccentId <- arbitrary newUserEmailCode <- arbitrary - newUserPhoneCode <- arbitrary newUserLabel <- arbitrary newUserLocale <- arbitrary newUserPassword <- genUserPassword newUserIdentity newUserOrigin diff --git a/libs/wire-api/src/Wire/API/User/Activation.hs b/libs/wire-api/src/Wire/API/User/Activation.hs index ff21fc57ac7..6a294b407c6 100644 --- a/libs/wire-api/src/Wire/API/User/Activation.hs +++ b/libs/wire-api/src/Wire/API/User/Activation.hs @@ -45,7 +45,6 @@ import Data.OpenApi (ToParamSchema) import Data.OpenApi qualified as S import Data.Schema import Data.Text.Ascii -import Data.Tuple.Extra (fst3, snd3, thd3) import Imports import Servant (FromHttpApiData (..)) import Wire.API.Locale @@ -59,8 +58,6 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) data ActivationTarget = -- | An opaque key for some email awaiting activation. ActivateKey ActivationKey - | -- | A known phone number awaiting activation. - ActivatePhone Phone | -- | A known email address awaiting activation. ActivateEmail Email deriving stock (Eq, Show, Generic) @@ -69,7 +66,6 @@ data ActivationTarget instance ToByteString ActivationTarget where builder (ActivateKey k) = builder k builder (ActivateEmail e) = builder e - builder (ActivatePhone p) = builder p -- | An opaque identifier of a 'UserKey' awaiting activation. newtype ActivationKey = ActivationKey @@ -142,33 +138,29 @@ instance ToSchema Activate where \cookies or tokens on success but failures still count \ \towards the maximum failure count." - maybeActivationTargetObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe ActivationKey, Maybe Phone, Maybe Email) ActivationTarget + maybeActivationTargetObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe ActivationKey, Maybe Email) ActivationTarget maybeActivationTargetObjectSchema = withParser activationTargetTupleObjectSchema maybeActivationTargetTargetFromTuple where - activationTargetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe ActivationKey, Maybe Phone, Maybe Email) + activationTargetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe ActivationKey, Maybe Email) activationTargetTupleObjectSchema = - (,,) - <$> fst3 .= maybe_ (optFieldWithDocModifier "key" keyDocs schema) - <*> snd3 .= maybe_ (optFieldWithDocModifier "phone" phoneDocs schema) - <*> thd3 .= maybe_ (optFieldWithDocModifier "email" emailDocs schema) + (,) + <$> fst .= maybe_ (optFieldWithDocModifier "key" keyDocs schema) + <*> snd .= maybe_ (optFieldWithDocModifier "email" emailDocs schema) where keyDocs = description ?~ "An opaque key to activate, as it was sent by the API." - phoneDocs = description ?~ "A known phone number to activate." emailDocs = description ?~ "A known email address to activate." - maybeActivationTargetTargetFromTuple :: (Maybe ActivationKey, Maybe Phone, Maybe Email) -> Parser ActivationTarget + maybeActivationTargetTargetFromTuple :: (Maybe ActivationKey, Maybe Email) -> Parser ActivationTarget maybeActivationTargetTargetFromTuple = \case - (Just key, _, _) -> pure $ ActivateKey key - (_, _, Just email) -> pure $ ActivateEmail email - (_, Just phone, _) -> pure $ ActivatePhone phone - _ -> fail "key, email or phone must be present" + (Just key, _) -> pure $ ActivateKey key + (_, Just email) -> pure $ ActivateEmail email + _ -> fail "key or email must be present" - maybeActivationTargetToTuple :: ActivationTarget -> (Maybe ActivationKey, Maybe Phone, Maybe Email) + maybeActivationTargetToTuple :: ActivationTarget -> (Maybe ActivationKey, Maybe Email) maybeActivationTargetToTuple = \case - ActivateKey key -> (Just key, Nothing, Nothing) - ActivatePhone phone -> (Nothing, Just phone, Nothing) - ActivateEmail email -> (Nothing, Nothing, Just email) + ActivateKey key -> (Just key, Nothing) + ActivateEmail email -> (Nothing, Just email) -- | Information returned as part of a successful activation. data ActivationResponse = ActivationResponse @@ -191,13 +183,11 @@ instance ToSchema ActivationResponse where -------------------------------------------------------------------------------- -- SendActivationCode --- | Payload for a request to (re-)send an activation code --- for a phone number or e-mail address. If a phone is used, --- one can also request a call instead of SMS. +-- | Payload for a request to (re-)send an activation code for an e-mail +-- address. data SendActivationCode = SendActivationCode - { saUserKey :: Either Email Phone, - saLocale :: Maybe Locale, - saCall :: Bool + { emailKey :: Email, + locale :: Maybe Locale } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SendActivationCode) @@ -207,37 +197,17 @@ instance ToSchema SendActivationCode where schema = objectWithDocModifier "SendActivationCode" objectDesc $ SendActivationCode - <$> (maybeUserKeyToTuple . saUserKey) .= userKeyObjectSchema - <*> saLocale .= maybe_ (optFieldWithDocModifier "locale" (description ?~ "Locale to use for the activation code template.") schema) - <*> saCall .= (fromMaybe False <$> optFieldWithDocModifier "voice_call" (description ?~ "Request the code with a call instead (default is SMS).") schema) + <$> emailKey .= field "email" schema + <*> locale + .= maybe_ + ( optFieldWithDocModifier + "locale" + ( description ?~ "Locale to use for the activation code template." + ) + schema + ) where - maybeUserKeyToTuple :: Either Email Phone -> (Maybe Email, Maybe Phone) - maybeUserKeyToTuple = \case - Left email -> (Just email, Nothing) - Right phone -> (Nothing, Just phone) - objectDesc :: NamedSwaggerDoc -> NamedSwaggerDoc objectDesc = description - ?~ "Data for requesting an email or phone activation code to be sent. \ - \One of 'email' or 'phone' must be present." - - userKeyObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe Email, Maybe Phone) (Either Email Phone) - userKeyObjectSchema = - withParser userKeyTupleObjectSchema maybeUserKeyFromTuple - where - userKeyTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe Email, Maybe Phone) - userKeyTupleObjectSchema = - (,) - <$> fst .= maybe_ (optFieldWithDocModifier "email" phoneDocs schema) - <*> snd .= maybe_ (optFieldWithDocModifier "phone" emailDocs schema) - where - emailDocs = description ?~ "Email address to send the code to." - phoneDocs = description ?~ "E.164 phone number to send the code to." - - maybeUserKeyFromTuple :: (Maybe Email, Maybe Phone) -> Parser (Either Email Phone) - maybeUserKeyFromTuple = \case - (Just _, Just _) -> fail "Only one of 'email' or 'phone' allowed." - (Just email, Nothing) -> pure $ Left email - (Nothing, Just phone) -> pure $ Right phone - (Nothing, Nothing) -> fail "One of 'email' or 'phone' required." + ?~ "Data for requesting an email code to be sent. 'email' must be present." diff --git a/libs/wire-api/src/Wire/API/User/Auth.hs b/libs/wire-api/src/Wire/API/User/Auth.hs index ad49c8be0b8..eef98189def 100644 --- a/libs/wire-api/src/Wire/API/User/Auth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth.hs @@ -1,6 +1,5 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. -- @@ -22,8 +21,6 @@ module Wire.API.User.Auth ( -- * Login Login (..), - PasswordLoginData (..), - SmsLoginData (..), loginLabel, LoginCode (..), LoginId (..), @@ -63,7 +60,6 @@ where import Cassandra import Control.Applicative import Control.Lens ((?~), (^.)) -import Control.Lens.TH import Data.Aeson (FromJSON, ToJSON) import Data.Aeson.Types qualified as A import Data.Bifunctor @@ -97,40 +93,36 @@ import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -------------------------------------------------------------------------------- -- LoginId +-- | The login ID for client API versions v0..v5 data LoginId = LoginByEmail Email - | LoginByPhone Phone | LoginByHandle Handle deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform LoginId) deriving (FromJSON, ToJSON, S.ToSchema) via Schema LoginId --- NB. this should fail if (e.g.) the email is present but unparseable even if the JSON contains a valid phone number or handle. --- See tests in `Test.Wire.API.User.Auth`. +-- NB. this should fail if (e.g.) the email is present but unparseable even if +-- the JSON contains a valid handle. instance ToSchema LoginId where - schema = object "LoginId" $ loginObjectSchema + schema = object "LoginId" loginObjectSchema loginObjectSchema :: ObjectSchema SwaggerDoc LoginId loginObjectSchema = fromLoginId .= tupleSchema `withParser` validate where - fromLoginId :: LoginId -> (Maybe Email, Maybe Phone, Maybe Handle) + fromLoginId :: LoginId -> (Maybe Email, Maybe Handle) fromLoginId = \case - LoginByEmail e -> (Just e, Nothing, Nothing) - LoginByPhone p -> (Nothing, Just p, Nothing) - LoginByHandle h -> (Nothing, Nothing, Just h) - tupleSchema :: ObjectSchema SwaggerDoc (Maybe Email, Maybe Phone, Maybe Handle) + LoginByEmail e -> (Just e, Nothing) + LoginByHandle h -> (Nothing, Just h) + tupleSchema :: ObjectSchema SwaggerDoc (Maybe Email, Maybe Handle) tupleSchema = - (,,) - <$> fst3 .= maybe_ (optField "email" schema) - <*> snd3 .= maybe_ (optField "phone" schema) - <*> thd3 .= maybe_ (optField "handle" schema) - validate :: (Maybe Email, Maybe Phone, Maybe Handle) -> A.Parser LoginId - validate (mEmail, mPhone, mHandle) = - maybe (fail "'email', 'phone' or 'handle' required") pure $ - (LoginByEmail <$> mEmail) - <|> (LoginByPhone <$> mPhone) - <|> (LoginByHandle <$> mHandle) + (,) + <$> fst .= maybe_ (optField "email" schema) + <*> snd .= maybe_ (optField "handle" schema) + validate :: (Maybe Email, Maybe Handle) -> A.Parser LoginId + validate (mEmail, mHandle) = + maybe (fail "'email' or 'handle' required") pure $ + (LoginByEmail <$> mEmail) <|> (LoginByHandle <$> mHandle) -------------------------------------------------------------------------------- -- LoginCode @@ -336,69 +328,27 @@ toUnitCookie c = c {cookieValue = ()} -------------------------------------------------------------------------------- -- Login --- | Different kinds of logins. -data Login - = PasswordLogin PasswordLoginData - | SmsLogin SmsLoginData - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform Login) - -data PasswordLoginData = PasswordLoginData - { plId :: LoginId, - plPassword :: PlainTextPassword6, - plLabel :: Maybe CookieLabel, - plCode :: Maybe Code.Value - } - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform PasswordLoginData) - -passwordLoginSchema :: ObjectSchema SwaggerDoc PasswordLoginData -passwordLoginSchema = - PasswordLoginData - <$> plId .= loginObjectSchema - <*> plPassword .= field "password" schema - <*> plLabel .= optField "label" (maybeWithDefault A.Null schema) - <*> plCode .= optField "verification_code" (maybeWithDefault A.Null schema) - -data SmsLoginData = SmsLoginData - { slPhone :: Phone, - slCode :: LoginCode, - slLabel :: Maybe CookieLabel +data Login = MkLogin + { lId :: LoginId, + lPassword :: PlainTextPassword6, + lLabel :: Maybe CookieLabel, + lCode :: Maybe Code.Value } deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform SmsLoginData) - -smsLoginSchema :: ObjectSchema SwaggerDoc SmsLoginData -smsLoginSchema = - SmsLoginData - <$> slPhone .= field "phone" schema - <*> slCode .= field "code" schema - <*> slLabel - .= optFieldWithDocModifier - "label" - ( description - ?~ "This label can be used to delete all cookies matching it\ - \ (cf. /cookies/remove)" - ) - (maybeWithDefault A.Null schema) - -$(makePrisms ''Login) + deriving (Arbitrary) via (GenericUniform Login) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema Login) instance ToSchema Login where schema = object "Login" $ - tag _PasswordLogin passwordLoginSchema - <> tag _SmsLogin smsLoginSchema - -deriving via Schema Login instance FromJSON Login - -deriving via Schema Login instance ToJSON Login - -deriving via Schema Login instance S.ToSchema Login + MkLogin + <$> lId .= loginObjectSchema + <*> lPassword .= field "password" schema + <*> lLabel .= optField "label" (maybeWithDefault A.Null schema) + <*> lCode .= optField "verification_code" (maybeWithDefault A.Null schema) loginLabel :: Login -> Maybe CookieLabel -loginLabel (PasswordLogin pl) = plLabel pl -loginLabel (SmsLogin sl) = slLabel sl +loginLabel = lLabel -------------------------------------------------------------------------------- -- RemoveCookies 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 38c2fa673ea..d2c152497d3 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 @@ -23,7 +23,6 @@ import Test.Wire.API.Golden.Generated.AccessRoleLegacy_user qualified import Test.Wire.API.Golden.Generated.AccessToken_user qualified import Test.Wire.API.Golden.Generated.Access_user qualified import Test.Wire.API.Golden.Generated.Action_user qualified -import Test.Wire.API.Golden.Generated.Activate_user qualified import Test.Wire.API.Golden.Generated.ActivationCode_user qualified import Test.Wire.API.Golden.Generated.ActivationKey_user qualified import Test.Wire.API.Golden.Generated.ActivationResponse_user qualified @@ -102,8 +101,6 @@ import Test.Wire.API.Golden.Generated.LocaleUpdate_user qualified import Test.Wire.API.Golden.Generated.Locale_user qualified import Test.Wire.API.Golden.Generated.LoginCodeTimeout_user qualified import Test.Wire.API.Golden.Generated.LoginCode_user qualified -import Test.Wire.API.Golden.Generated.LoginId_user qualified -import Test.Wire.API.Golden.Generated.Login_user qualified import Test.Wire.API.Golden.Generated.ManagedBy_user qualified import Test.Wire.API.Golden.Generated.MemberUpdateData_user qualified import Test.Wire.API.Golden.Generated.MemberUpdate_user qualified @@ -176,7 +173,6 @@ import Test.Wire.API.Golden.Generated.Scheme_user qualified import Test.Wire.API.Golden.Generated.SearchResult_20Contact_user qualified import Test.Wire.API.Golden.Generated.SearchResult_20TeamContact_user qualified import Test.Wire.API.Golden.Generated.SelfProfile_user qualified -import Test.Wire.API.Golden.Generated.SendActivationCode_user qualified import Test.Wire.API.Golden.Generated.SendLoginCode_user qualified import Test.Wire.API.Golden.Generated.ServiceKeyPEM_provider qualified import Test.Wire.API.Golden.Generated.ServiceKeyType_provider qualified @@ -866,9 +862,6 @@ tests = ), ( Test.Wire.API.Golden.Generated.NewUser_user.testObject_NewUser_user_8, "testObject_NewUser_user_8.json" - ), - ( Test.Wire.API.Golden.Generated.NewUser_user.testObject_NewUser_user_9, - "testObject_NewUser_user_9.json" ) ], testGroup "Golden: NewUserPublic_user" $ @@ -984,14 +977,8 @@ tests = testObjects [(Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_1, "testObject_ActivationKey_user_1.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_2, "testObject_ActivationKey_user_2.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_3, "testObject_ActivationKey_user_3.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_4, "testObject_ActivationKey_user_4.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_5, "testObject_ActivationKey_user_5.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_6, "testObject_ActivationKey_user_6.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_7, "testObject_ActivationKey_user_7.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_8, "testObject_ActivationKey_user_8.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_9, "testObject_ActivationKey_user_9.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_10, "testObject_ActivationKey_user_10.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_11, "testObject_ActivationKey_user_11.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_12, "testObject_ActivationKey_user_12.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_13, "testObject_ActivationKey_user_13.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_14, "testObject_ActivationKey_user_14.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_15, "testObject_ActivationKey_user_15.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_16, "testObject_ActivationKey_user_16.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_17, "testObject_ActivationKey_user_17.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_18, "testObject_ActivationKey_user_18.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_19, "testObject_ActivationKey_user_19.json"), (Test.Wire.API.Golden.Generated.ActivationKey_user.testObject_ActivationKey_user_20, "testObject_ActivationKey_user_20.json")], testGroup "Golden: ActivationCode_user" $ testObjects [(Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_1, "testObject_ActivationCode_user_1.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_2, "testObject_ActivationCode_user_2.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_3, "testObject_ActivationCode_user_3.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_4, "testObject_ActivationCode_user_4.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_5, "testObject_ActivationCode_user_5.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_6, "testObject_ActivationCode_user_6.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_7, "testObject_ActivationCode_user_7.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_8, "testObject_ActivationCode_user_8.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_9, "testObject_ActivationCode_user_9.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_10, "testObject_ActivationCode_user_10.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_11, "testObject_ActivationCode_user_11.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_12, "testObject_ActivationCode_user_12.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_13, "testObject_ActivationCode_user_13.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_14, "testObject_ActivationCode_user_14.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_15, "testObject_ActivationCode_user_15.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_16, "testObject_ActivationCode_user_16.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_17, "testObject_ActivationCode_user_17.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_18, "testObject_ActivationCode_user_18.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_19, "testObject_ActivationCode_user_19.json"), (Test.Wire.API.Golden.Generated.ActivationCode_user.testObject_ActivationCode_user_20, "testObject_ActivationCode_user_20.json")], - testGroup "Golden: Activate_user" $ - testObjects [(Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_1, "testObject_Activate_user_1.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_2, "testObject_Activate_user_2.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_3, "testObject_Activate_user_3.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_4, "testObject_Activate_user_4.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_5, "testObject_Activate_user_5.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_6, "testObject_Activate_user_6.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_7, "testObject_Activate_user_7.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_8, "testObject_Activate_user_8.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_9, "testObject_Activate_user_9.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_10, "testObject_Activate_user_10.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_11, "testObject_Activate_user_11.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_12, "testObject_Activate_user_12.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_13, "testObject_Activate_user_13.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_14, "testObject_Activate_user_14.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_15, "testObject_Activate_user_15.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_16, "testObject_Activate_user_16.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_17, "testObject_Activate_user_17.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_18, "testObject_Activate_user_18.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_19, "testObject_Activate_user_19.json"), (Test.Wire.API.Golden.Generated.Activate_user.testObject_Activate_user_20, "testObject_Activate_user_20.json")], testGroup "Golden: ActivationResponse_user" $ testObjects [(Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_1, "testObject_ActivationResponse_user_1.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_2, "testObject_ActivationResponse_user_2.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_3, "testObject_ActivationResponse_user_3.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_4, "testObject_ActivationResponse_user_4.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_5, "testObject_ActivationResponse_user_5.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_6, "testObject_ActivationResponse_user_6.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_7, "testObject_ActivationResponse_user_7.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_8, "testObject_ActivationResponse_user_8.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_9, "testObject_ActivationResponse_user_9.json"), (Test.Wire.API.Golden.Generated.ActivationResponse_user.testObject_ActivationResponse_user_10, "testObject_ActivationResponse_user_10.json")], - testGroup "Golden: SendActivationCode_user" $ - testObjects [(Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_1, "testObject_SendActivationCode_user_1.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_2, "testObject_SendActivationCode_user_2.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_3, "testObject_SendActivationCode_user_3.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_4, "testObject_SendActivationCode_user_4.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_5, "testObject_SendActivationCode_user_5.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_6, "testObject_SendActivationCode_user_6.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_7, "testObject_SendActivationCode_user_7.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_8, "testObject_SendActivationCode_user_8.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_9, "testObject_SendActivationCode_user_9.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_10, "testObject_SendActivationCode_user_10.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_11, "testObject_SendActivationCode_user_11.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_12, "testObject_SendActivationCode_user_12.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_13, "testObject_SendActivationCode_user_13.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_14, "testObject_SendActivationCode_user_14.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_15, "testObject_SendActivationCode_user_15.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_16, "testObject_SendActivationCode_user_16.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_17, "testObject_SendActivationCode_user_17.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_18, "testObject_SendActivationCode_user_18.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_19, "testObject_SendActivationCode_user_19.json"), (Test.Wire.API.Golden.Generated.SendActivationCode_user.testObject_SendActivationCode_user_20, "testObject_SendActivationCode_user_20.json")], - testGroup "Golden: LoginId_user" $ - testObjects [(Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_1, "testObject_LoginId_user_1.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_2, "testObject_LoginId_user_2.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_3, "testObject_LoginId_user_3.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_4, "testObject_LoginId_user_4.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_5, "testObject_LoginId_user_5.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_6, "testObject_LoginId_user_6.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_7, "testObject_LoginId_user_7.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_8, "testObject_LoginId_user_8.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_9, "testObject_LoginId_user_9.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_10, "testObject_LoginId_user_10.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_11, "testObject_LoginId_user_11.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_12, "testObject_LoginId_user_12.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_13, "testObject_LoginId_user_13.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_14, "testObject_LoginId_user_14.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_15, "testObject_LoginId_user_15.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_16, "testObject_LoginId_user_16.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_17, "testObject_LoginId_user_17.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_18, "testObject_LoginId_user_18.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_19, "testObject_LoginId_user_19.json"), (Test.Wire.API.Golden.Generated.LoginId_user.testObject_LoginId_user_20, "testObject_LoginId_user_20.json")], testGroup "Golden: LoginCode_user" $ testObjects [(Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_1, "testObject_LoginCode_user_1.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_2, "testObject_LoginCode_user_2.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_3, "testObject_LoginCode_user_3.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_4, "testObject_LoginCode_user_4.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_5, "testObject_LoginCode_user_5.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_6, "testObject_LoginCode_user_6.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_7, "testObject_LoginCode_user_7.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_8, "testObject_LoginCode_user_8.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_9, "testObject_LoginCode_user_9.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_10, "testObject_LoginCode_user_10.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_11, "testObject_LoginCode_user_11.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_12, "testObject_LoginCode_user_12.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_13, "testObject_LoginCode_user_13.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_14, "testObject_LoginCode_user_14.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_15, "testObject_LoginCode_user_15.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_16, "testObject_LoginCode_user_16.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_17, "testObject_LoginCode_user_17.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_18, "testObject_LoginCode_user_18.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_19, "testObject_LoginCode_user_19.json"), (Test.Wire.API.Golden.Generated.LoginCode_user.testObject_LoginCode_user_20, "testObject_LoginCode_user_20.json")], testGroup "Golden: PendingLoginCode_user" $ @@ -1002,8 +989,6 @@ tests = testObjects [(Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_1, "testObject_LoginCodeTimeout_user_1.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_2, "testObject_LoginCodeTimeout_user_2.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_3, "testObject_LoginCodeTimeout_user_3.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_4, "testObject_LoginCodeTimeout_user_4.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_5, "testObject_LoginCodeTimeout_user_5.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_6, "testObject_LoginCodeTimeout_user_6.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_7, "testObject_LoginCodeTimeout_user_7.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_8, "testObject_LoginCodeTimeout_user_8.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_9, "testObject_LoginCodeTimeout_user_9.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_10, "testObject_LoginCodeTimeout_user_10.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_11, "testObject_LoginCodeTimeout_user_11.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_12, "testObject_LoginCodeTimeout_user_12.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_13, "testObject_LoginCodeTimeout_user_13.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_14, "testObject_LoginCodeTimeout_user_14.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_15, "testObject_LoginCodeTimeout_user_15.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_16, "testObject_LoginCodeTimeout_user_16.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_17, "testObject_LoginCodeTimeout_user_17.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_18, "testObject_LoginCodeTimeout_user_18.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_19, "testObject_LoginCodeTimeout_user_19.json"), (Test.Wire.API.Golden.Generated.LoginCodeTimeout_user.testObject_LoginCodeTimeout_user_20, "testObject_LoginCodeTimeout_user_20.json")], testGroup "Golden: CookieLabel_user" $ testObjects [(Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_1, "testObject_CookieLabel_user_1.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_2, "testObject_CookieLabel_user_2.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_3, "testObject_CookieLabel_user_3.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_4, "testObject_CookieLabel_user_4.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_5, "testObject_CookieLabel_user_5.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_6, "testObject_CookieLabel_user_6.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_7, "testObject_CookieLabel_user_7.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_8, "testObject_CookieLabel_user_8.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_9, "testObject_CookieLabel_user_9.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_10, "testObject_CookieLabel_user_10.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_11, "testObject_CookieLabel_user_11.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_12, "testObject_CookieLabel_user_12.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_13, "testObject_CookieLabel_user_13.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_14, "testObject_CookieLabel_user_14.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_15, "testObject_CookieLabel_user_15.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_16, "testObject_CookieLabel_user_16.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_17, "testObject_CookieLabel_user_17.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_18, "testObject_CookieLabel_user_18.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_19, "testObject_CookieLabel_user_19.json"), (Test.Wire.API.Golden.Generated.CookieLabel_user.testObject_CookieLabel_user_20, "testObject_CookieLabel_user_20.json")], - testGroup "Golden: Login_user" $ - testObjects [(Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_1, "testObject_Login_user_1.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_2, "testObject_Login_user_2.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_3, "testObject_Login_user_3.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_4, "testObject_Login_user_4.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_5, "testObject_Login_user_5.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_6, "testObject_Login_user_6.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_7, "testObject_Login_user_7.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_8, "testObject_Login_user_8.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_9, "testObject_Login_user_9.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_10, "testObject_Login_user_10.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_11, "testObject_Login_user_11.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_12, "testObject_Login_user_12.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_13, "testObject_Login_user_13.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_14, "testObject_Login_user_14.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_15, "testObject_Login_user_15.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_16, "testObject_Login_user_16.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_17, "testObject_Login_user_17.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_18, "testObject_Login_user_18.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_19, "testObject_Login_user_19.json"), (Test.Wire.API.Golden.Generated.Login_user.testObject_Login_user_20, "testObject_Login_user_20.json")], testGroup "Golden: CookieId_user" $ testObjects [(Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_1, "testObject_CookieId_user_1.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_2, "testObject_CookieId_user_2.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_3, "testObject_CookieId_user_3.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_4, "testObject_CookieId_user_4.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_5, "testObject_CookieId_user_5.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_6, "testObject_CookieId_user_6.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_7, "testObject_CookieId_user_7.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_8, "testObject_CookieId_user_8.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_9, "testObject_CookieId_user_9.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_10, "testObject_CookieId_user_10.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_11, "testObject_CookieId_user_11.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_12, "testObject_CookieId_user_12.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_13, "testObject_CookieId_user_13.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_14, "testObject_CookieId_user_14.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_15, "testObject_CookieId_user_15.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_16, "testObject_CookieId_user_16.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_17, "testObject_CookieId_user_17.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_18, "testObject_CookieId_user_18.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_19, "testObject_CookieId_user_19.json"), (Test.Wire.API.Golden.Generated.CookieId_user.testObject_CookieId_user_20, "testObject_CookieId_user_20.json")], testGroup "Golden: CookieType_user" $ diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Activate_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Activate_user.hs deleted file mode 100644 index b6200b6fb76..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Activate_user.hs +++ /dev/null @@ -1,192 +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 Test.Wire.API.Golden.Generated.Activate_user where - -import Data.Text.Ascii (AsciiChars (validate)) -import Imports (Bool (False, True), fromRight, undefined) -import Wire.API.User (Email (Email, emailDomain, emailLocal), Phone (Phone, fromPhone)) -import Wire.API.User.Activation - ( Activate (..), - ActivationCode (ActivationCode, fromActivationCode), - ActivationKey (ActivationKey, fromActivationKey), - ActivationTarget (ActivateEmail, ActivateKey, ActivatePhone), - ) - -testObject_Activate_user_1 :: Activate -testObject_Activate_user_1 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+45520903"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "HUUpJQ==")}, - activateDryrun = True - } - -testObject_Activate_user_2 :: Activate -testObject_Activate_user_2 = - Activate - { activateTarget = - ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "e3sm9EjNmzA=")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "fg==")}, - activateDryrun = False - } - -testObject_Activate_user_3 :: Activate -testObject_Activate_user_3 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+44508058"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "OAbwDkw=")}, - activateDryrun = True - } - -testObject_Activate_user_4 :: Activate -testObject_Activate_user_4 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+97751884"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "811p-743Gvpi")}, - activateDryrun = False - } - -testObject_Activate_user_5 :: Activate -testObject_Activate_user_5 = - Activate - { activateTarget = ActivateEmail (Email {emailLocal = "\1002810\NUL\1075125", emailDomain = "k\\\SOHa\SYN*\176499"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "")}, - activateDryrun = False - } - -testObject_Activate_user_6 :: Activate -testObject_Activate_user_6 = - Activate - { activateTarget = ActivateEmail (Email {emailLocal = "\1104323i>\1007870Ha!", emailDomain = ""}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "FXrNll0Kqg==")}, - activateDryrun = False - } - -testObject_Activate_user_7 :: Activate -testObject_Activate_user_7 = - Activate - { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "jQ==")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "8yl3qERc")}, - activateDryrun = False - } - -testObject_Activate_user_8 :: Activate -testObject_Activate_user_8 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+3276478697350"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "NF20Avw=")}, - activateDryrun = True - } - -testObject_Activate_user_9 :: Activate -testObject_Activate_user_9 = - Activate - { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "DkV9xQ==")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "61wG")}, - activateDryrun = True - } - -testObject_Activate_user_10 :: Activate -testObject_Activate_user_10 = - Activate - { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "1szizA==")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "kcvCq2A=")}, - activateDryrun = False - } - -testObject_Activate_user_11 :: Activate -testObject_Activate_user_11 = - Activate - { activateTarget = ActivateEmail (Email {emailLocal = "\ETX4\SUB", emailDomain = ""}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "MZpmmg==")}, - activateDryrun = False - } - -testObject_Activate_user_12 :: Activate -testObject_Activate_user_12 = - Activate - { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "V3mr5D4=")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "sScBopoNTb0=")}, - activateDryrun = True - } - -testObject_Activate_user_13 :: Activate -testObject_Activate_user_13 = - Activate - { activateTarget = - ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "haH9_sUNFw==")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "ysvb")}, - activateDryrun = False - } - -testObject_Activate_user_14 :: Activate -testObject_Activate_user_14 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+13340815619"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "hQ==")}, - activateDryrun = True - } - -testObject_Activate_user_15 :: Activate -testObject_Activate_user_15 = - Activate - { activateTarget = - ActivateEmail (Email {emailLocal = "\22308W[\1041599G\996204]{\n", emailDomain = " V8\992253\NAK*"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "biTZ")}, - activateDryrun = False - } - -testObject_Activate_user_16 :: Activate -testObject_Activate_user_16 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+77635104433"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "5W4=")}, - activateDryrun = True - } - -testObject_Activate_user_17 :: Activate -testObject_Activate_user_17 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+556856857856"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "ShjEcgx6P0Hs")}, - activateDryrun = False - } - -testObject_Activate_user_18 :: Activate -testObject_Activate_user_18 = - Activate - { activateTarget = - ActivateEmail (Email {emailLocal = "2\1107376B\1099134\ETX2\US\1080331", emailDomain = "v\SOH\SO\1007855/e"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "xRvktQ==")}, - activateDryrun = False - } - -testObject_Activate_user_19 :: Activate -testObject_Activate_user_19 = - Activate - { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "1fCrdg==")}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "")}, - activateDryrun = False - } - -testObject_Activate_user_20 :: Activate -testObject_Activate_user_20 = - Activate - { activateTarget = ActivatePhone (Phone {fromPhone = "+893051142276"}), - activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "7PtclAevMzA=")}, - activateDryrun = False - } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs index d95f54bd302..26671442c2e 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs @@ -26,22 +26,8 @@ import Data.UUID qualified as UUID (fromString) import Imports (Bool (False, True), Maybe (Just, Nothing), fromJust) import URI.ByteString (parseURI, strictURIParserOptions) import Wire.API.Team.Invitation - ( Invitation - ( Invitation, - inCreatedAt, - inCreatedBy, - inInvitation, - inInviteeEmail, - inInviteeName, - inInviteePhone, - inInviteeUrl, - inRole, - inTeam - ), - InvitationList (..), - ) import Wire.API.Team.Role (Role (RoleAdmin, RoleExternalPartner, RoleMember, RoleOwner)) -import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal), Phone (Phone, fromPhone)) +import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) import Wire.API.User.Profile (Name (Name, fromName)) testObject_InvitationList_team_1 :: InvitationList @@ -65,7 +51,6 @@ testObject_InvitationList_team_2 = "fuC9p\1098501A\163554\f\ENQ\SO\21027N\47326_?oCX.U\r\163744W\33096\58996\1038685\DC3\t[\37667\SYN/\8408A\145025\173325\DC4H\135001\STX\166880\EOT\165028o\DC3" } ), - inInviteePhone = Just (Phone {fromPhone = "+851333011"}), inInviteeUrl = Just (fromRight' (parseURI strictURIParserOptions "https://example.com/inv14")) } ], @@ -93,7 +78,6 @@ testObject_InvitationList_team_4 = "R6\133444\134053VQ\187682\SUB\SOH\180538\&0C\1088909\ESCR\185800\125002@\38857Z?\STX\169387\1067878e}\SOH\ETB\EOTm\184898\US]\986782\189015\1059374\986508\b\DC1zfw-5\120662\CAN\1064450 \EMe\DC4|\14426Vo{\1076439\DC3#\USS\45051&zz\160719\&9\142411,\SI\f\SOHp\1025840\DLE\163178\1060369.&\997544kZ\50431u\b\50764\1109279n:\1103691D$.Q" } ), - inInviteePhone = Just (Phone {fromPhone = "+60506387292"}), inInviteeUrl = Nothing }, Invitation @@ -110,7 +94,6 @@ testObject_InvitationList_team_4 = "\DC2}q\CAN=SA\ETXx\t\ETX\\\v[\b)(\ESC]\135875Y\v@p\41515l\45065\157388\NUL\t\1100066\SOH1\DC1\ENQ\1021763\"i\29460\EM\b\ACK\SI\DC2v\ACK" } ), - inInviteePhone = Just (Phone {fromPhone = "+913945015"}), inInviteeUrl = Nothing }, Invitation @@ -127,7 +110,6 @@ testObject_InvitationList_team_4 = "\58076&\1059325Ec\NUL\16147}k\1036184l\172911\USJ\EM0^.+F\DEL\NUL\f$'`!\ETB[p\1041609}>E0y\96440#4I\a\66593jc\ESCgt\22473\1093208P\DC4!\1095909E93'Y$YL\46886b\r:,\181790\SO\153247y\ETX;\1064633\1099478z4z-D\1096755a\139100\&6\164829r\1033640\987906J\DLE\48134" } ), - inInviteePhone = Just (Phone {fromPhone = "+17046334"}), inInviteeUrl = Nothing }, Invitation @@ -144,7 +126,6 @@ testObject_InvitationList_team_4 = "Ft*O1\b&\SO\CAN<\72219\1092619m\n\DC4\DC2; \ETX\988837\DC1\1059627\"k.T\1023249[[\FS\EOT{j`\GS\997342c\1066411{\SUB\GSQY\182805\t\NAKy\t\132339j\1036225W " } ), - inInviteePhone = Nothing, inInviteeUrl = Nothing }, Invitation @@ -155,7 +136,6 @@ testObject_InvitationList_team_4 = inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, inInviteeName = Nothing, - inInviteePhone = Just (Phone {fromPhone = "+918848647685283"}), inInviteeUrl = Nothing }, Invitation @@ -172,7 +152,6 @@ testObject_InvitationList_team_4 = "Lo\r\1107113\1111565\1042998\1027480g\"\1055088\SUB\SUB\180703\43419\EOTv\188258,\171408(\GSQT\150160;\1063450\ENQ\ETBB\1106414H\170195\\\1040638,Y" } ), - inInviteePhone = Just (Phone {fromPhone = "+45207005641274"}), inInviteeUrl = Nothing } @@ -118,7 +113,6 @@ testObject_Invitation_team_6 = "O~\DC4U\RS?V3_\191280Slh\1072236Q1\1011443j|~M7\1092762\1097596\94632\DC1K\1078140Afs\178951lGV\1113159]`o\EMf\34020InvfDDy\\DI\163761\1091945\ETBB\159212F*X\SOH\SUB\50580\ETX\DLE<\ETX\SYNc\DEL\DLE,p\v*\1005720Vn\fI\70201xS\STXV\ESC$\EMu\1002390xl>\aZ\DC44e\DC4aZ" } ), - inInviteePhone = Just (Phone {fromPhone = "+75547625285"}), inInviteeUrl = Nothing } @@ -138,7 +132,6 @@ testObject_Invitation_team_7 = "\CAN.\110967\1085214\DLE\f\DLE\CAN\150564o;Yay:yY $\ETX<\879%@\USre>5L'R\DC3\178035oy#]c4!\99741U\54858\26279\1042232\1062242p_>f\SO\DEL\175240\1077738\995735_Vm\US}\STXPz\r\ENQK\SO+>\991648\NUL\153467?pu?r\ESC\SUB!?\168405;\6533S\18757\a\1071148\b\1023581\996567\17385\120022\b\SUB\FS\SIF%<\125113\SIh\ESC\ETX\SI\994739\USO\NULg_\151272\47274\1026399\EOT\1058084\1089771z~%IA'R\b\1011572Hv^\1043633wrjb\t\166747\ETX" } ), - inInviteePhone = Just (Phone {fromPhone = "+518729615781"}), inInviteeUrl = Nothing } @@ -220,7 +209,6 @@ testObject_Invitation_team_12 = "\DLEZ+wd^\67082\1073384\&1\STXYdXt>\1081020LSB7F9\\\135148\ENQ\n\987295\"\127009|\a\61724\157754\DEL'\ESCTygU\1106772R\52822\1071584O4\1035713E9\"\1016016\DC2Re\ENQD}\1051112\161959\1104733\bV\176894%98'\RS9\ACK4yP\83405\14400\345\aw\t\1098022\v\1078003xv/Yl\1005740\158703" } ), - inInviteePhone = Just (Phone {fromPhone = "+68945103783764"}), inInviteeUrl = Nothing } @@ -234,7 +222,6 @@ testObject_Invitation_team_13 = inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000002"))), inInviteeEmail = Email {emailLocal = "", emailDomain = "\DELr"}, inInviteeName = Just (Name {fromName = "U"}), - inInviteePhone = Just (Phone {fromPhone = "+549940856897515"}), inInviteeUrl = Nothing } @@ -248,7 +235,6 @@ testObject_Invitation_team_14 = inCreatedBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000200000000"))), inInviteeEmail = Email {emailLocal = "EI", emailDomain = "{"}, inInviteeName = Nothing, - inInviteePhone = Just (Phone {fromPhone = "+89058877371"}), inInviteeUrl = Nothing } @@ -268,7 +254,6 @@ testObject_Invitation_team_15 = "\71448\US&KIL\DC3\1086159![\n6\1111661HEj4E\12136UL\US>2\1070931_\nJ\53410Pv\SO\SIR\30897\&8\bmS\45510mE\ag\SYN\ENQ%\14545\f!\v\US\119306\ENQ\184817\1044744\SO83!j\73854\GS\1071331,\RS\CANF\1062795\1110535U\EMJb\DC1j\EMY\92304O\1007855" } ), - inInviteePhone = Just (Phone {fromPhone = "+57741900390998"}), inInviteeUrl = Nothing } @@ -282,7 +267,6 @@ testObject_Invitation_team_16 = inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))), inInviteeEmail = Email {emailLocal = "\\", emailDomain = "\"\DEL{"}, inInviteeName = Just (Name {fromName = "\GS\DC4Q;6/_f*7\1093966\SI+\1092810\41698\&9"}), - inInviteePhone = Nothing, inInviteeUrl = Nothing } @@ -302,7 +286,6 @@ testObject_Invitation_team_17 = "Z\ESC9E\DEL\NAK\37708\83413}(3m\97177\97764'\1072786.WY;\RS8?v-\1100720\DC2\1015859" } ), - inInviteePhone = Nothing, inInviteeUrl = Nothing } @@ -342,7 +324,6 @@ testObject_Invitation_team_19 = "\38776r\111317\ETXQi\1000087\1097943\EM\170747\74323+\1067948Q?H=G-\RS;\1103719\SOq^K;a\1052250W\EM X\83384\1073320>M\980\26387jjbU-&\1040136v\NULy\181884\a|\SYNUfJCHjP\SO\1111555\27981DNA:~s" } ), - inInviteePhone = Just (Phone {fromPhone = "+05787228893"}), inInviteeUrl = Nothing } @@ -356,6 +337,5 @@ testObject_Invitation_team_20 = inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))), inInviteeEmail = Email {emailLocal = "b", emailDomain = "u9T"}, inInviteeName = Nothing, - inInviteePhone = Just (Phone {fromPhone = "+27259486019"}), inInviteeUrl = Nothing } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LoginId_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LoginId_user.hs deleted file mode 100644 index 117789dfdf7..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LoginId_user.hs +++ /dev/null @@ -1,119 +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 Test.Wire.API.Golden.Generated.LoginId_user where - -import Data.Handle (parseHandle) -import Data.Maybe -import Wire.API.User (Email (Email, emailDomain, emailLocal), Phone (Phone, fromPhone)) -import Wire.API.User.Auth (LoginId (..)) - -testObject_LoginId_user_1 :: LoginId -testObject_LoginId_user_1 = - LoginByEmail - (Email {emailLocal = "~]z^?j\NAK\1088399\1112814X{)\1087092t\f", emailDomain = "\1113045\n\vL$\ENQY\NUL\DELUj?H%"}) - -testObject_LoginId_user_2 :: LoginId -testObject_LoginId_user_2 = LoginByPhone (Phone {fromPhone = "+178807168"}) - -testObject_LoginId_user_3 :: LoginId -testObject_LoginId_user_3 = - LoginByEmail - ( Email - { emailLocal = "0\1088863^\1000125\144267\NUL)|\183379:", - emailDomain = "q6e/$\1033221Zb\1050001)\991223\&05i\20077~q\1071660\128584y" - } - ) - -testObject_LoginId_user_4 :: LoginId -testObject_LoginId_user_4 = LoginByHandle (fromJust (parseHandle "7a8gg3v98")) - -testObject_LoginId_user_5 :: LoginId -testObject_LoginId_user_5 = LoginByPhone (Phone {fromPhone = "+041157889572"}) - -testObject_LoginId_user_6 :: LoginId -testObject_LoginId_user_6 = LoginByPhone (Phone {fromPhone = "+2351341820189"}) - -testObject_LoginId_user_7 :: LoginId -testObject_LoginId_user_7 = LoginByHandle (fromJust (parseHandle "lb")) - -testObject_LoginId_user_8 :: LoginId -testObject_LoginId_user_8 = LoginByPhone (Phone {fromPhone = "+2831673805093"}) - -testObject_LoginId_user_9 :: LoginId -testObject_LoginId_user_9 = LoginByPhone (Phone {fromPhone = "+1091378734554"}) - -testObject_LoginId_user_10 :: LoginId -testObject_LoginId_user_10 = - LoginByHandle (fromJust (parseHandle "z58-6fbjhtx11d8t6oplyijpkc2.fp_lf3kpk3_.qle4iecjun2xd0tpcordlg2bwv636v3cthpgwah3undqmuofgzp8ry6gc6g-n-kxnj7sl6771hxou7-t_ps_lu_t3.4ukz6dh6fkjq2i3aggtkbpzbd1162.qv.rbtb6e.90-xpayg65z9t9lk2aur452zcs9a")) - -testObject_LoginId_user_11 :: LoginId -testObject_LoginId_user_11 = - LoginByEmail - ( Email - { emailLocal = "\154036\140469A\1031528ovP Ig\92578t';\6199\SOHC\29188\157632{\n%\1090626\v2\GS\180557\1112803&", - emailDomain = "m\180009U{f&.3\3846\&1?Ew\30701G-" - } - ) - -testObject_LoginId_user_12 :: LoginId -testObject_LoginId_user_12 = - LoginByEmail (Email {emailLocal = "", emailDomain = "\18232\EM+h\ENQ(D\SO\28757\993545 \a\r1"}) - -testObject_LoginId_user_13 :: LoginId -testObject_LoginId_user_13 = - LoginByEmail - ( Email - { emailLocal = "5-h\1094050\1011032&$og\1084464\26226\989383<%\2855\fGF-yJ\f*cK", - emailDomain = "*g\EM\120758\&7$L\CAN\59033\57589\tV\1102330D\a\\yK\1090380T" - } - ) - -testObject_LoginId_user_14 :: LoginId -testObject_LoginId_user_14 = LoginByPhone (Phone {fromPhone = "+8668821360611"}) - -testObject_LoginId_user_15 :: LoginId -testObject_LoginId_user_15 = - LoginByEmail - ( Email - { emailLocal = "\ACK\ENQX\ACK&\94893\&8\1044677\&7E`Y'\DC1TV\ACK\DLE", - emailDomain = "\GS\ESCj\999191,j\994949\1043277#a1)}\DC3Vk\SOHQ7&;" - } - ) - -testObject_LoginId_user_16 :: LoginId -testObject_LoginId_user_16 = - LoginByEmail - ( Email - { emailLocal = "\1013039\&1", - emailDomain = - "\v`\EM\49692v\1082687;F\18618\&0\4155Sgu%>\1076869y\v\1018080\NAK\133308\US\1025555\ACKs\SI\a\US" - } - ) - -testObject_LoginId_user_17 :: LoginId -testObject_LoginId_user_17 = LoginByHandle (fromJust (parseHandle "e3iusdy")) - -testObject_LoginId_user_18 :: LoginId -testObject_LoginId_user_18 = - LoginByHandle (fromJust (parseHandle "8vpices3usz1dfs4u2lf_e3jendod_szl1z111_eoj4b7k7ajj-xo.qzbw4espf3smnz_")) - -testObject_LoginId_user_19 :: LoginId -testObject_LoginId_user_19 = LoginByHandle (fromJust (parseHandle "3jzpp2bo8")) - -testObject_LoginId_user_20 :: LoginId -testObject_LoginId_user_20 = LoginByEmail (Email {emailLocal = "", emailDomain = "\155899"}) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Login_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Login_user.hs deleted file mode 100644 index e0b6a4cf88a..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Login_user.hs +++ /dev/null @@ -1,227 +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 Test.Wire.API.Golden.Generated.Login_user where - -import Data.Code -import Data.Handle (parseHandle) -import Data.Maybe -import Data.Misc (plainTextPassword6Unsafe) -import Data.Range (unsafeRange) -import Data.Text.Ascii (AsciiChars (validate)) -import Imports -import Wire.API.User (Email (Email, emailDomain, emailLocal), Phone (Phone, fromPhone)) -import Wire.API.User.Auth - -testObject_Login_user_1 :: Login -testObject_Login_user_1 = - PasswordLogin - ( PasswordLoginData - (LoginByEmail (Email {emailLocal = "4\1069339\vEaP", emailDomain = "\ENQ\n\FS\ESC\997356i03!"})) - ( plainTextPassword6Unsafe - "\b5Ta\61971\150647\186716fa&\1047748o!ov\SI\1100133i\DC4\ETXY\SOR\991323\1086159Ta^s\ETB\SI[\189068\988899\26508\CAN6\STXp\1069462-9\983823&\NAK\1052068]^\13044;>-Z$Z\NAK\r\1101550a\RS%\NUL:\188721\47674\157548?e]\ETX \142608 C\SOH\SIS%8m\1091987V\147131[\1006262\&6\171610\1011219\164656SX\n%\1061259*>\t+\132427Y\989558\993346\GSU\1067541\&6TU!*\40114\&90\1055516\RSV\162483N\t*\EOT{I<\1084278\SOH\183116!c\\\n\1107501\183146\DC1,-xX\EMV?\t\168648\1054239\DC2\DEL1\SOHu\SOH\63459\53061\SO+h\ACK::\RS\21356_g,\SO*\v\DC4\1093710HFF\188918\1081075fF\ESC2\SOHT\DC1)\fc\35905l\1061547\f#~\STX]\1035086/Or)kY\1031423\SOHNCk\1067954\&5\1083470x=H\NUL\23760\1058646\1099097E/$\DELpbi\137522\FSKi\15676\1018134\t7\"OL\54208\7516\&5\43466\NUL(\1030852\166514\SOH\149343\994835\25513C==\GSTV3\DELl6\999006.Z)$\16723|\172732\1090303J;O\GSbw\vI\1101024I\SYN\DC2^\149630\STX3%i\EMW\138614\DC4\1113619tsL5\147087W\96700(_,\1091179*\1041287rckx\SOH\SIs\SOHJd\140574\SYNev.\DC4\DLE\99082.\1106785\996992\143448\US_\ETBf\STX\SO\DC3\1043748\&6O\DC1Q\SOH'\GS,|]W\SIa\62568\151062.\v\aH&-L\DC2+\147179\1095524\EOTm)\19925\181147\183368!\185223\142946m\DC4\DC3\1034282m\GS\185509>>\"NDw\1076877hY\1033831sFKz^ \1108187\&5Qec\NAK}|\1108194.Q\173114imb\1027220 p;\1089082\SYN\1065748kF\1102854r8o\DC1" - ) - (Just (CookieLabel {cookieLabelText = "r"})) - Nothing - ) - -testObject_Login_user_2 :: Login -testObject_Login_user_2 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+956057641851"}) - (LoginCode {fromLoginCode = "\nG\1076650\&8\b"}) - (Just (CookieLabel {cookieLabelText = "G"})) - ) - -testObject_Login_user_3 :: Login -testObject_Login_user_3 = - PasswordLogin - ( PasswordLoginData - (LoginByHandle (fromJust (parseHandle "c2wp.7s5."))) - ( plainTextPassword6Unsafe - "&\RS\DC4\1104052Z\11418n\SO\158691\1010906/\127253'\1063038m\1010345\"\9772\138717\RS(&\996590\SOf1Wf'I\SI\100286\1047270\1033961\DC1Jq\1050673Y\\Bedu@\1014647c\1003986D\53211\1050614S\144414\ETX\ETXW>\1005358\DC4\rSO8FXy\166833a\EM\170017\SUBNF\158145L\RS$5\NULk\RSz*s\148780\157980\v\175417\"SY\DEL\STX\994691\1103514ub5q\ENQ\1014299\vN.\t\183536:l\1105396\RS\1027721\a\168001\SO\vt\1098704W\SYN\1042396\1109979\a'v\ETB\64211\NAK\59538\STX \NAK\STX\49684,\1111630x\1047668^\1067127\27366I;\NAKb\1092049o\162763_\190546MME\1022528\SI\1096252H;\SO\ETBs\SO\1065937{Knlrd;\35750\DC4\SI\1075008TO\1090529\999639U\48787\1099927t\1068680^y\17268u$\DC1Jp\1054308\164905\164446\STX\"\1095399*\SO\1004302\32166\990924X\1098844\ETXsK}\b\143918\NUL0\988724\&12\171116\tM052\189551\EOT0\RS\986138\1084688{ji\ESC\1020800\27259&t \SI\ESCy\aL\136111\131558\994027\r\1054821ga,\DC4do,tx[I&\DC4h\DLE\ETX\DLEBpm\1002292-\a]/ZI\1033117q]w3n\46911e\23692kYo5\1090844'K\1089820}v\146759;\1018792\\=\41264\&8g\DLEg*has\44159\1006118\DC3\USYg?I\19462\NAKaW2\150415m\t}h\155161RbU\STX\ETBlz2!\DC3JW5\ESC\1026156U\SOg,rpO\5857]0\ESC\479\1005443F\SI\1045994\RS\SO\11908rl\1104306~\ACK+Mn{5\993784a\EM2\v{jM\ETBT\1058105$\DC1\1099974\GSj_~Z\1007141P\SOH\EOTo@TJhk\EOT\ETBk:-\96583[p\DLE\DC1\RS'\r\STXQ,,\1016866?H\rh\30225\rj\147982\DC2\\(u\ESCu\154705\1002696o\DC4\988492\1103465\1052034\DC1q\GS-\b\40807\DC1qW>\fys\8130,'\159954<" - ) - (Just (CookieLabel {cookieLabelText = "\1082362\66362>XC"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_4 :: Login -testObject_Login_user_4 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+04332691687649"}) - (LoginCode {fromLoginCode = "\94770m"}) - (Just (CookieLabel {cookieLabelText = ":"})) - ) - -testObject_Login_user_5 :: Login -testObject_Login_user_5 = - PasswordLogin - ( PasswordLoginData - ( LoginByHandle - (fromJust (parseHandle "c372iaa_v5onjcck67rlzq4dn5_oxhtx7dpx7v82lp1rhx0e97i26--8r3c6k773bxtlzmkjc20-11_047ydua_o9_5u4sll_fl3ng_0sa.")) - ) - ( plainTextPassword6Unsafe - "\120347\184756DU\1035832hp\1006715t~\DC2\SOH\STX*\1053210y1\1078382H\173223{e\\S\SO?c_7\t\DC4X\135187\&6\172722E\100168j\SUB\t\SYN\1088511>HO]60\990035\ETX\"+w,t\1066040\ak(b%u\151197`>b\1028272e\ACKc\151393\1107996)\12375\&7\1082464`\186313yO+v%\1033664\rc<\65764\&2>8u\1094258\1080669\1113623\75033a\179193\NAK=\EOT\1077021\&8R&j\1042630\ESC\t4sj-\991835\40404n\136765\1064089N\GS\\\1026123\72288\&5\r\97004(P!\DEL\29235\26855\b\1067772Mr~\65123\EMjt>Z\GS~\140732A\1031358\SO\\>\DC16\">%\45860\1084751I@u5\187891\vrY\r;7\1071052#\1078407\1016286\CAN'\63315\1041397\EM_I_zY\987300\149441\EMd\1039844cd\DEL\1061999\136326Cp3\26325\GSXj\n\46305jy\44050\58825\t-\19065\43336d\1046547L\SUBYF\ACKPOL\54766\DC2\DC1\DC1\DC2*\rH\DLE(?\DC3F\25820\DLE\r]\1069451j\170177 @\ENQT\1100685s\FSF2\NAK]8\a\DC3!\NAKW\176469\1110834K\1025058\1112222_%\1001818\1113069'\1098149\70360(#\SOHky\t\ETB!\17570\NAK\DC4\ESC{\119317U2LS'" - ) - (Just (CookieLabel {cookieLabelText = "LGz%\119949j\f\RS/\SOH"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_6 :: Login -testObject_Login_user_6 = - PasswordLogin - ( PasswordLoginData - (LoginByPhone (Phone {fromPhone = "+930266260693371"})) - ( plainTextPassword6Unsafe - "K?)V\148106}_\185335\1060952\fJ3!\986581\1062221\51615\166583\1071064\a\1015675\SOH7\\#z9\133503\1081163\985690\1041362\EM\DC3\156174'\r)~Ke9+\175606\175778\994126M\1099049\"h\SOHTh\EOT`;\ACK\1093024\ENQ\1026474'e{\FSv\40757\US\143355*\16236\1076902\52767:E]:R\1093823K}l\1111648Y\51665\1049318S~\EOT#T\1029316\&1hIWn\v`\45455Kb~\ESC\DLEdT\FS\SI\1092141f\ETBY7\DEL\RS\131804\t\998971\13414\48242\GSG\DC3BH#\DEL\\RAd\166099g\1072356\1054332\SIk&\STXE\22217\FS\FS\FS$t\1001957:O\1098769q}_\1039296.\SOH\DC4\STX\157262c`L>\1050744l\1086722m'BtB5\1003280,t\"\1066340\&9(#\ENQ4\SIIy>\1031158\1100542\GSbf\"i\ETB\14367a\1086113C@\1078844\1092137\32415\NAK\999161\23344*N\SYN\ESC:iXibA\136851\169508q\1048663]:9r\63027\73801\NUL\1050763\USCN\US\147710\1048697\1016861eR\RSZbD5!8N\ESCV\7344\ACK\173064\SUBuz\1053950\188308~\ESC\SI%{3I/F\25232/DMS\US>o\187199\63000Z\1108766\GS[K\184801\94661\1088369\995346\ESCO-4\CAN\US\FSZp" - ) - (Just (CookieLabel {cookieLabelText = "\1014596'\998013KW\\\NUL\DC4"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_7 :: Login -testObject_Login_user_7 = - PasswordLogin - ( PasswordLoginData - (LoginByEmail (Email {emailLocal = "BG", emailDomain = "\12137c\v}\SIL$_"})) - ( plainTextPassword6Unsafe - "&\991818\1023244\83352\STXJ<-~\STX>\v\74228\151871\&5QN\53968\166184ql\NAK\74290\&3}{\DC3\173242S\22739;\t7\183958_F~D*f\1049940)\1067330-9\20699\&7GK= %\RS@kOF#\179945\1094401\124994\&8_\42309\GSL\37698\ETX\1047946\&0Wl1A`LYz\USy\20728\SUBo\ESC[\DC4\bt\66640a\ETXs~\USF\175140G`$\vG\DC1\1044421\128611/\1014458C>\SI" - ) - (Just (CookieLabel {cookieLabelText = "\SO\NAKeC/"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_8 :: Login -testObject_Login_user_8 = - PasswordLogin - ( PasswordLoginData - (LoginByEmail (Email {emailLocal = "", emailDomain = "~^G\1075856\\"})) - ( plainTextPassword6Unsafe - "z>\1088515\1024903/\137135\1092812\b%$\1037736\143620:}\t\CAN\1058585\1044157)\12957\1005180s\1006270\CAN}\40034\EM[\41342\vX#VG,df4\141493\&8m5\46365OTK\144460\37582\DEL\44719\9670Z\"ZS\ESCms|[Q%\1088673\ENQW\\\1000857C\185096+\1070458\4114\17825v\180321\41886){\1028513\DEL\143570f\187156}:X-\b2N\EM\USl\127906\49608Y\1071393\1012763r2.1\49912\EOT+\137561\DC3\145480]'\1028275s\997684\42805.}\185059o\992118X\132901\11013\r\SUBNq6\1019605'\fd\RS\14503\1097628,:%\t\151916\73955QD\1086880\ESC(q4KDQ2zcI\DLE>\EM5\993596\&1\fBkd\DC3\ACK:F:\EOT\100901\11650O N\FS,N\1054390\1000247[h\DEL9\5932:xZ=\f\1085312\DC3u\RS\fe#\SUB^$lkx\32804 \rr\SUBJ\1013606\1017057\FSR][_5\NAK\58351\11748\35779\&5\24821\1055669\996852\37445K!\1052768eRR%\32108+h~1\993198\35871lTzS$\DLE\1060275\"*\1086839pmRE\DC3(\US^\8047Jc\10129\1071815i\n+G$|\993993\156283g\FS\fgU3Y\119068\ACKf)\1093562\SYN\78340\1100638/\NULPi\43622{\1048095j\1083269\FS9\132797\1024684\32713w$\45599\126246)Si\167172\29311FX\1057490j{`\44452`\999383\159809\&4u%\1070378P*\1057403\25422\DELC\RSR\SYN-\51098\1011541g\68666:S>c\15266\132940\DLEY\1066831~a)YW_J\1063076P\a+ U\1084883j\EMk\SOH\1096984\DC1\18679e\172760\175328,\5135g@\DC2\GSHXl.\ETB\153793\&2\DC3mY\1054891\tv?L8L\1074044N\133565\nb1j\1044024\148213xfQ=\\\ENQe\995818\1023862U\DC2p{\SO\1099404jd^@U\994269tP.\DC2Y%R`a\r\160622\&7}HnUf\132856m^7:\NAK=\52348>l\95313hwp27\149950jE\fx=!.\DC3]Ar\tw\DC4&\SUBk\194572s\1042820\4498I\146071\61461\1060645dsY\DLE\181922dX.\146295i]\151113\1028288\rWS\USU\1098732\SUB\49884\1083906\DLE\STXN~-\SO6\190031\1110322\\O\185165Jc\1052359\1071278\NULHSo\DLE-W\DC36\170321I\1068712)\99800={\99796h\27961\61707M\1022570FwJQ\1111976ck\SUB\CAN|UV-\NAK\SOH|\DC4;\f\156907\145795\ENQS\NAK.B\"D\163007#o*\126577\32988m\RS\1049834B3Gg;\DC1\\\180659\1098926\ENQ B^\SI\152630$e\39220\170037>fMgC\187276,o\128488\\?\1033955~/s\SOH?MMc;D18Ne\EOT\CAN)*\STX\GS\162681/\t\NAK \1010386\1013311z\33488Bv\1109131(=<\SOq\1104556?L\6845\1066491\2972c\997644<&!\1103500\999823j~O3USw\DC2\ETX\a\ETB+\1024033Ny\31920(/Sco\STX{3\SIEh\SYN\1032591\1022672\27668-\FS.'\ENQX\98936\150419Ti3\1051250\"%\SYN\b\188444+\EOT\STX^\1108463)2bR\ACK\SIJB[\1045179&O9{w{aV\ENQgZ?3z\1065517\&8\4979\156950\990517`\1063252\"PE)uKq|w\SYN0\ESC. \ETX\73440sxW\160357\1001111m\ENQ7e)\77912\1008764:s\CANYj\9870\16356\ACK\USlTu\1110309I.\1087068O#kQ\RS!g\1062167\CANQ\US\172867\SYN\ACK|\"M\"P\US\ETX@ZPq\1016598gY\148621=\a\1057645l8\1041152\&3\995012\1022626CN<\147876gJ\1038434]\94932mX~\ACKw3\DLE\179764\&8\a6\EOT}\DLEi\DC3L5\1032336PY^|!Vz\ESC4\36208!iLa\12091\DC4\1059706\167964\GS:\1042431\149640h\\dLx\1087701\EM\194900\SUB\134635R%ps7\95168s\1074387fg\nIf\1067199\DC1l\SUB\1022871-n_\6065UY?4d]|c\\[T\ajS\18838\55046\37136aK\1025430\1112672\ETX\FSx+" - ) - (Just (CookieLabel {cookieLabelText = ""})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_10 :: Login -testObject_Login_user_10 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+4211134144507"}) - (LoginCode {fromLoginCode = "\13379\61834\135400!\ETBi\1050047"}) - (Just (CookieLabel {cookieLabelText = ""})) - ) - -testObject_Login_user_11 :: Login -testObject_Login_user_11 = - SmsLogin (SmsLoginData (Phone {fromPhone = "+338932197597737"}) (LoginCode {fromLoginCode = "\1069411+W\EM3"}) Nothing) - -testObject_Login_user_12 :: Login -testObject_Login_user_12 = - PasswordLogin - ( PasswordLoginData - (LoginByPhone (Phone {fromPhone = "+153353668"})) - ( plainTextPassword6Unsafe - "n\1095465Q\169408\ESC\1003840&Q/\rd\43034\US\EOTw2C\ACK\1056364\178004\EOT\EOTv\1010012\bf,b\DEL\STX\1013552'\175696C]G\46305\1017071\190782\&4\NULY.\173618\SO3sI\194978F\1084606\&5\21073rG/:\"\1013990X\46943\&6\FS:\CAN\aeYwWT\1083802\136913Msbm\NAK@\984540\1013513\EOT^\FS\147032\NAK@\ENQ>\f\RSUc\EOTV9&c\3517\a\986228a'PPG\100445\179638>[\3453\&2\64964Xc\131306[0\1002646\b\99652B\DC1[\1029237\GS\19515\US\EMs-u\ETBs\1067133\1005008\161663n\1072320?\1045643ck\DC48XC\174289\RSI2\2862\STX\DLEM\ESC\n?<\\\DC3E\72219\GS\n$cyS\136198!,\v9\ETB/\DC1\62324?P\ETB\41758\DC2\999537~\1058761W-W4K8.\DC27\EML\1078049h\SI}t+H\SUB\ESCX\120523s\EOTt\177703taa\GS\f\152365(v\1024552M\ESCvg3P1\1032835\57603]g\3933\&4T\NAK$\38212);\\8\1109165\nK\NAK}D'^fJ'\143205e\174052\39597!\EM.\DC2{\\CEp\1045384\ETBk_\1083904\18397\164138\1063468]MG$\187650[E\1112126\b\1073487{b\50650\ESC^b@W\NAK$\FS<\1023895&\155992R\ACKJ\SI\1093108\1101041\41438n\1007134\&8]\148288\ENQ}|k\STX\CANQ\USI\a\CANDZ\1062877\NUL\50197rb\18947\&3G%\FS\162081\EOT\NAK4YB0-i\1018065IM\1073908[\1111554:Cr$\99636)L\136837W\40897.x;\41461\1030711\995525\USkb\CANY9)\SYN4\SI\1103461Av.\r\f\1061861\&9{\SO\ETBP\f\33538u\r-9cB4\1016091G\RS\22817\1014740r\128247HcsPm\59419s\120987!|J<\DLE8\FS[\NAKWYAK\75011^\987050c3\1042176\aC\ETX\ETB\1053739Y\DC4f\ACK\1060945!\1032209:RlQ!BX\f=\1070694f\151362\DEL\113727O\ETX\\\"\53275B<\RSLV4g%3\1098063\ACK`\NAK>\n\44626kp\986102\171479\DEL\60526H\20888lyJ\DC2)\1055149(\1027099A\FSh\EOTj\35251\DC4M\ESCP-q\bn\CAN\143310~\GS\EM\"o\21512%*e2\165597L\1023807sy\152913\&2m\GS\1049046{EG]\DC16B+{\983622IYa\1008153\&5,<\ESCX\f\SI\186613\153744E\134407\1011088L<\EMdUO\ETB\SUBZYm\ACK\1086320R\SUB\991954\DC3^\60967s\fu_g\EM?i~}\DELV2\148681R\FS\EOT3j\45841m\1542\1100884\n7S\SIT5j\170914\SI\1015133\141587h\182480Q\146618\59914\DEL\NAKZM\1110574\&02f\129340l!*\SOH\1027033\SOH\1070384\1094775\t\72805\ESCa:q UKEN\RS-\n\ETXH\22365a\1074707\b\37494\"\1035508\149695\1033139R4\ETX\DLE\FS\STX\1004750%\"@\1009369\&6=/x\NULP\EOT\174871/\190041\f\f\1005146?*\fIcKW\DELQ\"\1001726P*\1095849\&6=d\n\157680\RS\1087962\EOT\DC2I\47501U\b=Pc\DLE" - ) - (Just (CookieLabel {cookieLabelText = "\SI\128787-\125004:\136001\39864\ACK\SO"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) - -testObject_Login_user_13 :: Login -testObject_Login_user_13 = - SmsLogin (SmsLoginData (Phone {fromPhone = "+626804710"}) (LoginCode {fromLoginCode = "&\1040514y"}) Nothing) - -testObject_Login_user_14 :: Login -testObject_Login_user_14 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+5693913858477"}) - (LoginCode {fromLoginCode = ""}) - (Just (CookieLabel {cookieLabelText = "\95804\25610"})) - ) - -testObject_Login_user_15 :: Login -testObject_Login_user_15 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+56208262"}) - (LoginCode {fromLoginCode = ""}) - (Just (CookieLabel {cookieLabelText = "q\ETB(\1086676\187384>8\141442\n6"})) - ) - -testObject_Login_user_16 :: Login -testObject_Login_user_16 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+588058222975"}) - (LoginCode {fromLoginCode = "_\1110666\1003968\1108501-_\ETB"}) - (Just (CookieLabel {cookieLabelText = "\SOL\1079080\1008939\1059848@\FS\DLE$"})) - ) - -testObject_Login_user_17 :: Login -testObject_Login_user_17 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+3649176551364"}) - (LoginCode {fromLoginCode = "\ETB1\1002982n\DLEdV\1030538d\SOH"}) - (Just (CookieLabel {cookieLabelText = "\1112281{/p\100214"})) - ) - -testObject_Login_user_18 :: Login -testObject_Login_user_18 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+478931600"}) - (LoginCode {fromLoginCode = ",\139681\13742,"}) - (Just (CookieLabel {cookieLabelText = "5"})) - ) - -testObject_Login_user_19 :: Login -testObject_Login_user_19 = - SmsLogin - ( SmsLoginData - (Phone {fromPhone = "+92676996582869"}) - (LoginCode {fromLoginCode = "x\27255<"}) - (Just (CookieLabel {cookieLabelText = "w;U\ESCx:"})) - ) - -testObject_Login_user_20 :: Login -testObject_Login_user_20 = - PasswordLogin - ( PasswordLoginData - (LoginByEmail (Email {emailLocal = "[%", emailDomain = ","})) - ( plainTextPassword6Unsafe - "ryzP\DC39\11027-1A)\b,u\8457j~0\1090580\1033743\fI\170254er\DC4V|}'kzG%A;3H\amD\STXU1\NUL^\1043764\DLEO&5u\EOT\SUB\167046\&0A\996223X\DC2\FS7fEt\97366rPvytT\136915!\100713$Q|BI+EM5\NAK\t\DELRKrE\DLE\US\r?.\STX|@1v^\vycpu\n$\DC2\186675\131718-Q\151081\n\r\1033981\68381O\ENQ*\68660Z\USo\EOTn\188565%&\DC3Me*\STX;\DLE034\nv\NAK\140398(\1075494\990138n@\1108345|\48421d\n*\SI\NUL}\NAKA!\1045882\1036527Hx\ETB3\STX{#T|5|GC\1089070z.\USN\1080851\22324\vu\SYN~LP\147583CV\SO q\151952\DC2e8h\USg\1019358;\f\996107\1108688At\1022346)\USG\DC3\166541\39337|\1042043\SI\134073\EOTc~6\DLE:u\165393##^\nn{d\CAN\ng\16237\ESC\US\US~A8};T\RS\NAK)&\b\ACK\1106044\GS(\DC3u;\1094683;=e\1051162\"\40669vCt)o\987006m\43912\78088l1+\1036284[\STXFLx\1080932:\1031973\992752\&71/kE\93787p\DC4Ij\ETB\194985&\SUB^\FSl1\ACK\1019548\ETXW,+3\128058\95671\DLE7\59727\&7rG'\1078914JC9M\1053804\SYN\DC2\44350>~\1016308Y\1062059=i-\fS\172440\156520K2-@\ENQ\f\1108851_1D-&\128386lR\187248/\993988$:\31415:\52267Dg\1015243O\1010173\170117\SO\179807\&2z\NAKq\141547c\FSliJ{\1055925\1060070'BL\168670;\STX\1046844\18443B\NUL\7839b\1072569:w\1108016Ad\SUB6\NAKo\55279\nsPWM{\ETXfW\1018373JT\1021361$\989069\54608\190318\173259u4\1103286\t\34021\1039458\"\153264UM\1084148\1095406\34105\1105325\t\nIn'\1070532\21097\16091\EM\DC1<\v\bW\SI}\141807\b\1072339\1035283\GS`\1094467x\NUL\986937K\FSj\1079287\DC1\SI\168992d\991620k4\SUB\1009876\49943^\58464\1052547\1016875i2=$:[f\1064579\DC2n\NAKJ<=\2028\SI!z\1105364\SON\NAK\EM\180748V\1024876CQ_G\nY#ky\132779k\DC3\ENQ}OC\96566}~M\EMp\ETX\RSx\b\183962\1073008\b8/\DC4?\1081654B\1025870\EOT\SO\DELU\1020905\ESC=%\51062J\168855\ETB\992593\990312\985186\to\1101036X_@@\45111\43952$" - ) - (Just (CookieLabel {cookieLabelText = "\1055424\r9\998420`\NAKx"})) - (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) - ) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUserPublic_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUserPublic_user.hs index e51c5ce8aff..502167b678d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUserPublic_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUserPublic_user.hs @@ -39,7 +39,6 @@ testObject_NewUserPublic_user_1 = Name {fromName = "\\sY4]u\1033976\DLE\1027259\FS\ETX \US\ETB\1066640dw;}\1073386@\184511\r8"}, newUserUUID = Nothing, newUserIdentity = Just (EmailIdentity (Email {emailLocal = "test", emailDomain = "example.com"})), - newUserPhone = Nothing, newUserPict = Nothing, newUserAssets = [ ImageAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "5cd81cc4-c643-4e9c-849c-c596a88c27fd"))) AssetExpiring) (Just AssetComplete), @@ -53,7 +52,6 @@ testObject_NewUserPublic_user_1 = { fromActivationCode = fromRight undefined (validate "cfTQLlhl6H6sYloQXsghILggxWoGhM2WGbxjzm0=") } ), - newUserPhoneCode = Nothing, newUserOrigin = Just ( NewUserOriginTeamUser diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs index 5d0a458757c..9f47b858fff 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs @@ -52,7 +52,6 @@ testObject_NewUser_user_1 = }, newUserUUID = (Just . toUUID) (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))), newUserIdentity = Just (EmailIdentity (Email {emailLocal = "S\ENQX\1076723$\STX\"\1110507e\1015716\24831\1031964L\ETB", emailDomain = "P.b"})), - newUserPhone = Nothing, newUserPict = Just (Pict {fromPict = []}), newUserAssets = [ ImageAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "5cd81cc4-c643-4e9c-849c-c596a88c27fd"))) AssetExpiring) (Just AssetPreview), @@ -61,7 +60,6 @@ testObject_NewUser_user_1 = ], newUserAccentId = Just (ColourId {fromColourId = -7404}), newUserEmailCode = Just (ActivationCode {fromActivationCode = fromRight undefined (validate "1YgaHo0=")}), - newUserPhoneCode = Nothing, newUserOrigin = Just ( NewUserOriginInvitationCode @@ -173,15 +171,3 @@ testObject_NewUser_user_8 = ), newUserPassword = Just (plainTextPassword8Unsafe "12345678") } - -testObject_NewUser_user_9 :: NewUser -testObject_NewUser_user_9 = - testObject_NewUser_user_1 - { newUserPhoneCode = - Just - ( ActivationCode - { fromActivationCode = - fromRight undefined (validate "z1OeJQ==") - } - ) - } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SendActivationCode_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SendActivationCode_user.hs deleted file mode 100644 index 9ef7d361f43..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SendActivationCode_user.hs +++ /dev/null @@ -1,201 +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 Test.Wire.API.Golden.Generated.SendActivationCode_user where - -import Data.ISO3166_CountryCodes (CountryCode (AO, BB, FI, FR, IN, MU, PM, VI, VU)) -import Data.LanguageCodes qualified (ISO639_1 (CU, DE, DV, FI, GD, GN, HO, HY, IU, KK, KW, PA, TG, VE)) -import Imports (Bool (False, True), Either (Left, Right), Maybe (Just, Nothing)) -import Wire.API.User - ( Country (Country, fromCountry), - Email (Email, emailDomain, emailLocal), - Language (Language), - Locale (Locale, lCountry, lLanguage), - Phone (Phone, fromPhone), - ) -import Wire.API.User.Activation (SendActivationCode (..)) - -testObject_SendActivationCode_user_1 :: SendActivationCode -testObject_SendActivationCode_user_1 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+77566129334842"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.CU, lCountry = Just (Country {fromCountry = VI})}), - saCall = False - } - -testObject_SendActivationCode_user_2 :: SendActivationCode -testObject_SendActivationCode_user_2 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "\1021635", emailDomain = "nK"}), - saLocale = Just (Locale {lLanguage = Language Data.LanguageCodes.DE, lCountry = Nothing}), - saCall = False - } - -testObject_SendActivationCode_user_3 :: SendActivationCode -testObject_SendActivationCode_user_3 = - SendActivationCode - { saUserKey = - Left - ( Email - { emailLocal = "#\ACK\1103236l\1069771F\147486", - emailDomain = "-\DC32\1101045\&1\DC2\1014718\167922\SO\68149" - } - ), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.GN, lCountry = Just (Country {fromCountry = VU})}), - saCall = True - } - -testObject_SendActivationCode_user_4 :: SendActivationCode -testObject_SendActivationCode_user_4 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "b", emailDomain = "4M\1076452P\149723$[\DC2j"}), - saLocale = Nothing, - saCall = False - } - -testObject_SendActivationCode_user_5 :: SendActivationCode -testObject_SendActivationCode_user_5 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "test", emailDomain = "example.com"}), - saLocale = Nothing, - saCall = False - } - -testObject_SendActivationCode_user_6 :: SendActivationCode -testObject_SendActivationCode_user_6 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+38093636958"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.DV, lCountry = Just (Country {fromCountry = IN})}), - saCall = False - } - -testObject_SendActivationCode_user_7 :: SendActivationCode -testObject_SendActivationCode_user_7 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "B+l\1054055\1082148", emailDomain = "\a%"}), - saLocale = Nothing, - saCall = True - } - -testObject_SendActivationCode_user_8 :: SendActivationCode -testObject_SendActivationCode_user_8 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "\NUL3", emailDomain = "\59252g\155998\11926Ea?\DC2\\\DC4"}), - saLocale = Just (Locale {lLanguage = Language Data.LanguageCodes.HO, lCountry = Nothing}), - saCall = True - } - -testObject_SendActivationCode_user_9 :: SendActivationCode -testObject_SendActivationCode_user_9 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "Rn\STXv", emailDomain = "(\NULN"}), - saLocale = Nothing, - saCall = False - } - -testObject_SendActivationCode_user_10 :: SendActivationCode -testObject_SendActivationCode_user_10 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "\t\1040376\NUL2\160662t\152821", emailDomain = "^s"}), - saLocale = Nothing, - saCall = True - } - -testObject_SendActivationCode_user_11 :: SendActivationCode -testObject_SendActivationCode_user_11 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "rT", emailDomain = "a\tL\DC4"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.HY, lCountry = Just (Country {fromCountry = BB})}), - saCall = False - } - -testObject_SendActivationCode_user_12 :: SendActivationCode -testObject_SendActivationCode_user_12 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+6599921229041"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.VE, lCountry = Just (Country {fromCountry = MU})}), - saCall = True - } - -testObject_SendActivationCode_user_13 :: SendActivationCode -testObject_SendActivationCode_user_13 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+260369295110"}), - saLocale = Just (Locale {lLanguage = Language Data.LanguageCodes.KK, lCountry = Nothing}), - saCall = False - } - -testObject_SendActivationCode_user_14 :: SendActivationCode -testObject_SendActivationCode_user_14 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "B;b\164357\DC1\SIHm\DC3{", emailDomain = "?\64159Jd\f"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.KW, lCountry = Just (Country {fromCountry = PM})}), - saCall = False - } - -testObject_SendActivationCode_user_15 :: SendActivationCode -testObject_SendActivationCode_user_15 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "\1024828\DC1", emailDomain = "t=\69734\42178\1032441,AG2"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.IU, lCountry = Just (Country {fromCountry = FR})}), - saCall = False - } - -testObject_SendActivationCode_user_16 :: SendActivationCode -testObject_SendActivationCode_user_16 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "O_\37211\1022996^t", emailDomain = ""}), - saLocale = Just (Locale {lLanguage = Language Data.LanguageCodes.FI, lCountry = Nothing}), - saCall = True - } - -testObject_SendActivationCode_user_17 :: SendActivationCode -testObject_SendActivationCode_user_17 = - SendActivationCode - { saUserKey = Left (Email {emailLocal = "T\vI9H}C\STX\SO\1017900", emailDomain = "\151457\35555=N"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.PA, lCountry = Just (Country {fromCountry = AO})}), - saCall = True - } - -testObject_SendActivationCode_user_18 :: SendActivationCode -testObject_SendActivationCode_user_18 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+715068856505655"}), - saLocale = Just (Locale {lLanguage = Language Data.LanguageCodes.TG, lCountry = Nothing}), - saCall = True - } - -testObject_SendActivationCode_user_19 :: SendActivationCode -testObject_SendActivationCode_user_19 = - SendActivationCode - { saUserKey = Right (Phone {fromPhone = "+22888251856"}), - saLocale = - Just (Locale {lLanguage = Language Data.LanguageCodes.GD, lCountry = Just (Country {fromCountry = FI})}), - saCall = True - } - -testObject_SendActivationCode_user_20 :: SendActivationCode -testObject_SendActivationCode_user_20 = - SendActivationCode {saUserKey = Right (Phone {fromPhone = "+8943652812"}), saLocale = Nothing, saCall = True} 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 57daad1dd22..f2e7e5adf51 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 @@ -19,6 +19,7 @@ module Test.Wire.API.Golden.Manual where import Imports import Test.Tasty +import Test.Wire.API.Golden.Manual.Activate_user import Test.Wire.API.Golden.Manual.ClientCapability import Test.Wire.API.Golden.Manual.ClientCapabilityList import Test.Wire.API.Golden.Manual.Contact @@ -38,9 +39,12 @@ import Test.Wire.API.Golden.Manual.GetPaginatedConversationIds import Test.Wire.API.Golden.Manual.GroupId import Test.Wire.API.Golden.Manual.ListConversations import Test.Wire.API.Golden.Manual.ListUsersById +import Test.Wire.API.Golden.Manual.LoginId_user +import Test.Wire.API.Golden.Manual.Login_user import Test.Wire.API.Golden.Manual.MLSKeys import Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap import Test.Wire.API.Golden.Manual.SearchResultContact +import Test.Wire.API.Golden.Manual.SendActivationCode_user import Test.Wire.API.Golden.Manual.SubConversation import Test.Wire.API.Golden.Manual.TeamSize import Test.Wire.API.Golden.Manual.Token @@ -238,5 +242,34 @@ tests = testGroup "MLSKeysByPurpose" $ testObjects [ (testObject_MLSKeysByPurpose1, "testObject_MLSKeysByPurpose_1.json") + ], + testGroup "SendActivationCode" $ + testObjects + [ (testObject_SendActivationCode_1, "testObject_SendActivationCode_1.json"), + (testObject_SendActivationCode_2, "testObject_SendActivationCode_2.json") + ], + testGroup "LoginId" $ + testObjects + [ (testObject_LoginId_user_1, "testObject_LoginId_user_1.json"), + (testObject_LoginId_user_2, "testObject_LoginId_user_2.json"), + (testObject_LoginId_user_3, "testObject_LoginId_user_3.json"), + (testObject_LoginId_user_4, "testObject_LoginId_user_4.json"), + (testObject_LoginId_user_5, "testObject_LoginId_user_5.json"), + (testObject_LoginId_user_6, "testObject_LoginId_user_6.json") + ], + testGroup "Login" $ + testObjects + [ (testObject_Login_user_1, "testObject_Login_user_1.json"), + (testObject_Login_user_2, "testObject_Login_user_2.json"), + (testObject_Login_user_3, "testObject_Login_user_3.json"), + (testObject_Login_user_4, "testObject_Login_user_4.json"), + (testObject_Login_user_5, "testObject_Login_user_5.json") + ], + testGroup "Activate" $ + testObjects + [ (testObject_Activate_user_1, "testObject_Activate_user_1.json"), + (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") ] ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Activate_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Activate_user.hs new file mode 100644 index 00000000000..e5a7c0c3ec9 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Activate_user.hs @@ -0,0 +1,56 @@ +-- 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.Activate_user where + +import Data.Text.Ascii (AsciiChars (validate)) +import Imports (Bool (False, True), fromRight, undefined) +import Wire.API.User +import Wire.API.User.Activation + +testObject_Activate_user_1 :: Activate +testObject_Activate_user_1 = + Activate + { activateTarget = + ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "e3sm9EjNmzA=")}), + activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "fg==")}, + activateDryrun = False + } + +testObject_Activate_user_2 :: Activate +testObject_Activate_user_2 = + Activate + { activateTarget = ActivateEmail (Email {emailLocal = "\1002810\NUL\1075125", emailDomain = "k\\\SOHa\SYN*\176499"}), + activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "")}, + activateDryrun = False + } + +testObject_Activate_user_3 :: Activate +testObject_Activate_user_3 = + Activate + { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "DkV9xQ==")}), + activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "61wG")}, + activateDryrun = True + } + +testObject_Activate_user_4 :: Activate +testObject_Activate_user_4 = + Activate + { activateTarget = ActivateKey (ActivationKey {fromActivationKey = fromRight undefined (validate "V3mr5D4=")}), + activateCode = ActivationCode {fromActivationCode = fromRight undefined (validate "sScBopoNTb0=")}, + activateDryrun = True + } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs new file mode 100644 index 00000000000..3df352a0216 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/LoginId_user.hs @@ -0,0 +1,51 @@ +-- 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.LoginId_user where + +import Data.Handle (parseHandle) +import Data.Maybe +import Wire.API.User +import Wire.API.User.Auth (LoginId (..)) + +testObject_LoginId_user_1 :: LoginId +testObject_LoginId_user_1 = + LoginByEmail + (Email {emailLocal = "~]z^?j\NAK\1088399\1112814X{)\1087092t\f", emailDomain = "\1113045\n\vL$\ENQY\NUL\DELUj?H%"}) + +testObject_LoginId_user_2 :: LoginId +testObject_LoginId_user_2 = + LoginByEmail + ( Email + { emailLocal = "0\1088863^\1000125\144267\NUL)|\183379:", + emailDomain = "q6e/$\1033221Zb\1050001)\991223\&05i\20077~q\1071660\128584y" + } + ) + +testObject_LoginId_user_3 :: LoginId +testObject_LoginId_user_3 = LoginByHandle (fromJust (parseHandle "7a8gg3v98")) + +testObject_LoginId_user_4 :: LoginId +testObject_LoginId_user_4 = LoginByHandle (fromJust (parseHandle "lb")) + +testObject_LoginId_user_5 :: LoginId +testObject_LoginId_user_5 = + LoginByHandle (fromJust (parseHandle "z58-6fbjhtx11d8t6oplyijpkc2.fp_lf3kpk3_.qle4iecjun2xd0tpcordlg2bwv636v3cthpgwah3undqmuofgzp8ry6gc6g-n-kxnj7sl6771hxou7-t_ps_lu_t3.4ukz6dh6fkjq2i3aggtkbpzbd1162.qv.rbtb6e.90-xpayg65z9t9lk2aur452zcs9a")) + +testObject_LoginId_user_6 :: LoginId +testObject_LoginId_user_6 = + LoginByEmail (Email {emailLocal = "", emailDomain = "\18232\EM+h\ENQ(D\SO\28757\993545 \a\r1"}) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Login_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Login_user.hs new file mode 100644 index 00000000000..97c96601359 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Login_user.hs @@ -0,0 +1,80 @@ +-- 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.Login_user where + +import Data.Code +import Data.Handle (parseHandle) +import Data.Maybe +import Data.Misc (plainTextPassword6Unsafe) +import Data.Range (unsafeRange) +import Data.Text.Ascii (AsciiChars (validate)) +import Imports +import Wire.API.User (Email (Email, emailDomain, emailLocal)) +import Wire.API.User.Auth + +testObject_Login_user_1 :: Login +testObject_Login_user_1 = + MkLogin + (LoginByEmail (Email {emailLocal = "4\1069339\vEaP", emailDomain = "\ENQ\n\FS\ESC\997356i03!"})) + ( plainTextPassword6Unsafe + "\b5Ta\61971\150647\186716fa&\1047748o!ov\SI\1100133i\DC4\ETXY\SOR\991323\1086159Ta^s\ETB\SI[\189068\988899\26508\CAN6\STXp\1069462-9\983823&\NAK\1052068]^\13044;>-Z$Z\NAK\r\1101550a\RS%\NUL:\188721\47674\157548?e]\ETX \142608 C\SOH\SIS%8m\1091987V\147131[\1006262\&6\171610\1011219\164656SX\n%\1061259*>\t+\132427Y\989558\993346\GSU\1067541\&6TU!*\40114\&90\1055516\RSV\162483N\t*\EOT{I<\1084278\SOH\183116!c\\\n\1107501\183146\DC1,-xX\EMV?\t\168648\1054239\DC2\DEL1\SOHu\SOH\63459\53061\SO+h\ACK::\RS\21356_g,\SO*\v\DC4\1093710HFF\188918\1081075fF\ESC2\SOHT\DC1)\fc\35905l\1061547\f#~\STX]\1035086/Or)kY\1031423\SOHNCk\1067954\&5\1083470x=H\NUL\23760\1058646\1099097E/$\DELpbi\137522\FSKi\15676\1018134\t7\"OL\54208\7516\&5\43466\NUL(\1030852\166514\SOH\149343\994835\25513C==\GSTV3\DELl6\999006.Z)$\16723|\172732\1090303J;O\GSbw\vI\1101024I\SYN\DC2^\149630\STX3%i\EMW\138614\DC4\1113619tsL5\147087W\96700(_,\1091179*\1041287rckx\SOH\SIs\SOHJd\140574\SYNev.\DC4\DLE\99082.\1106785\996992\143448\US_\ETBf\STX\SO\DC3\1043748\&6O\DC1Q\SOH'\GS,|]W\SIa\62568\151062.\v\aH&-L\DC2+\147179\1095524\EOTm)\19925\181147\183368!\185223\142946m\DC4\DC3\1034282m\GS\185509>>\"NDw\1076877hY\1033831sFKz^ \1108187\&5Qec\NAK}|\1108194.Q\173114imb\1027220 p;\1089082\SYN\1065748kF\1102854r8o\DC1" + ) + (Just (CookieLabel {cookieLabelText = "r"})) + Nothing + +testObject_Login_user_2 :: Login +testObject_Login_user_2 = + MkLogin + (LoginByHandle (fromJust (parseHandle "c2wp.7s5."))) + ( plainTextPassword6Unsafe + "&\RS\DC4\1104052Z\11418n\SO\158691\1010906/\127253'\1063038m\1010345\"\9772\138717\RS(&\996590\SOf1Wf'I\SI\100286\1047270\1033961\DC1Jq\1050673Y\\Bedu@\1014647c\1003986D\53211\1050614S\144414\ETX\ETXW>\1005358\DC4\rSO8FXy\166833a\EM\170017\SUBNF\158145L\RS$5\NULk\RSz*s\148780\157980\v\175417\"SY\DEL\STX\994691\1103514ub5q\ENQ\1014299\vN.\t\183536:l\1105396\RS\1027721\a\168001\SO\vt\1098704W\SYN\1042396\1109979\a'v\ETB\64211\NAK\59538\STX \NAK\STX\49684,\1111630x\1047668^\1067127\27366I;\NAKb\1092049o\162763_\190546MME\1022528\SI\1096252H;\SO\ETBs\SO\1065937{Knlrd;\35750\DC4\SI\1075008TO\1090529\999639U\48787\1099927t\1068680^y\17268u$\DC1Jp\1054308\164905\164446\STX\"\1095399*\SO\1004302\32166\990924X\1098844\ETXsK}\b\143918\NUL0\988724\&12\171116\tM052\189551\EOT0\RS\986138\1084688{ji\ESC\1020800\27259&t \SI\ESCy\aL\136111\131558\994027\r\1054821ga,\DC4do,tx[I&\DC4h\DLE\ETX\DLEBpm\1002292-\a]/ZI\1033117q]w3n\46911e\23692kYo5\1090844'K\1089820}v\146759;\1018792\\=\41264\&8g\DLEg*has\44159\1006118\DC3\USYg?I\19462\NAKaW2\150415m\t}h\155161RbU\STX\ETBlz2!\DC3JW5\ESC\1026156U\SOg,rpO\5857]0\ESC\479\1005443F\SI\1045994\RS\SO\11908rl\1104306~\ACK+Mn{5\993784a\EM2\v{jM\ETBT\1058105$\DC1\1099974\GSj_~Z\1007141P\SOH\EOTo@TJhk\EOT\ETBk:-\96583[p\DLE\DC1\RS'\r\STXQ,,\1016866?H\rh\30225\rj\147982\DC2\\(u\ESCu\154705\1002696o\DC4\988492\1103465\1052034\DC1q\GS-\b\40807\DC1qW>\fys\8130,'\159954<" + ) + (Just (CookieLabel {cookieLabelText = "\1082362\66362>XC"})) + (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) + +testObject_Login_user_3 :: Login +testObject_Login_user_3 = + MkLogin + ( LoginByHandle + (fromJust (parseHandle "c372iaa_v5onjcck67rlzq4dn5_oxhtx7dpx7v82lp1rhx0e97i26--8r3c6k773bxtlzmkjc20-11_047ydua_o9_5u4sll_fl3ng_0sa.")) + ) + ( plainTextPassword6Unsafe + "\120347\184756DU\1035832hp\1006715t~\DC2\SOH\STX*\1053210y1\1078382H\173223{e\\S\SO?c_7\t\DC4X\135187\&6\172722E\100168j\SUB\t\SYN\1088511>HO]60\990035\ETX\"+w,t\1066040\ak(b%u\151197`>b\1028272e\ACKc\151393\1107996)\12375\&7\1082464`\186313yO+v%\1033664\rc<\65764\&2>8u\1094258\1080669\1113623\75033a\179193\NAK=\EOT\1077021\&8R&j\1042630\ESC\t4sj-\991835\40404n\136765\1064089N\GS\\\1026123\72288\&5\r\97004(P!\DEL\29235\26855\b\1067772Mr~\65123\EMjt>Z\GS~\140732A\1031358\SO\\>\DC16\">%\45860\1084751I@u5\187891\vrY\r;7\1071052#\1078407\1016286\CAN'\63315\1041397\EM_I_zY\987300\149441\EMd\1039844cd\DEL\1061999\136326Cp3\26325\GSXj\n\46305jy\44050\58825\t-\19065\43336d\1046547L\SUBYF\ACKPOL\54766\DC2\DC1\DC1\DC2*\rH\DLE(?\DC3F\25820\DLE\r]\1069451j\170177 @\ENQT\1100685s\FSF2\NAK]8\a\DC3!\NAKW\176469\1110834K\1025058\1112222_%\1001818\1113069'\1098149\70360(#\SOHky\t\ETB!\17570\NAK\DC4\ESC{\119317U2LS'" + ) + (Just (CookieLabel {cookieLabelText = "LGz%\119949j\f\RS/\SOH"})) + (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) + +testObject_Login_user_4 :: Login +testObject_Login_user_4 = + MkLogin + (LoginByEmail (Email {emailLocal = "BG", emailDomain = "\12137c\v}\SIL$_"})) + ( plainTextPassword6Unsafe + "&\991818\1023244\83352\STXJ<-~\STX>\v\74228\151871\&5QN\53968\166184ql\NAK\74290\&3}{\DC3\173242S\22739;\t7\183958_F~D*f\1049940)\1067330-9\20699\&7GK= %\RS@kOF#\179945\1094401\124994\&8_\42309\GSL\37698\ETX\1047946\&0Wl1A`LYz\USy\20728\SUBo\ESC[\DC4\bt\66640a\ETXs~\USF\175140G`$\vG\DC1\1044421\128611/\1014458C>\SI" + ) + (Just (CookieLabel {cookieLabelText = "\SO\NAKeC/"})) + (Just (Value {asciiValue = unsafeRange (fromRight undefined (validate "RcplMOQiGa-JY"))})) + +testObject_Login_user_5 :: Login +testObject_Login_user_5 = + MkLogin + (LoginByEmail (Email {emailLocal = "", emailDomain = "~^G\1075856\\"})) + ( plainTextPassword6Unsafe + "z>\1088515\1024903/\137135\1092812\b%$\1037736\143620:}\t\CAN\1058585\1044157)\12957\1005180s\1006270\CAN}\40034\EM[\41342\vX#VG,df4\141493\&8m5\46365OTK\144460\37582\DEL\44719\9670Z\"ZS\ESCms|[Q%\1088673\ENQW\\\1000857C\185096+\1070458\4114\17825v\180321\41886){\1028513\DEL\143570f\187156}:X-\b2N\EM\USl\127906\49608Y\1071393\1012763r2.1\49912\EOT+\137561\DC3\145480]'\1028275s\997684\42805.}\185059o\992118X\132901\11013\r\SUBNq6\1019605'\fd\RS\14503\1097628,:%\t\151916\73955QD\1086880\ESC(q4KDQ2zcI\DLE>\EM5\993596\&1\fBkd\DC3\ACK:F:\EOT\100901\11650O N\FS,N\1054390\1000247[h\DEL9\5932:xZ=\f\1085312\DC3u\RS\fe#\SUB^$lkx\32804 \rr\SUBJ\1013606\1017057\FSR][_5\NAK\58351\11748\35779\&5\24821\1055669\996852\37445K!\1052768eRR%\32108+h~1\993198\35871lTzS$\DLE\1060275\"*\1086839pmRE\DC3(\US^\8047Jc\10129\1071815i\n+G$|\993993\156283g\FS\fgU3Y\119068\ACKf)\1093562\SYN\78340\1100638/\NULPi\43622{\1048095j\1083269\FS9\132797\1024684\32713w$\45599\126246)Si\167172\29311FX\1057490j{`\44452`\999383\159809\&4u%\1070378P*\1057403\25422\DELC\RSR\SYN-\51098\1011541g\68666:S>c\15266\132940\DLEY\1066831~a)YW_J\1063076P\a+ U\1084883j\EMk\SOH\1096984\DC1\18679e\172760\175328,\5135g@\DC2\GSHXl.\ETB\153793\&2\DC3mY\1054891\tv?L8L\1074044N\133565\nb1j\1044024\148213xfQ=\\\ENQe\995818\1023862U\DC2p{\SO\1099404jd^@U\994269tP.\DC2Y%R`a\r\160622\&7}HnUf\132856m^7:\NAK=\52348>l\95313hwp27\149950jE\fx=!.\DC3]Ar\tw\DC4&\SUBk\194572s\1042820\4498I\146071\61461\1060645dsY\DLE\181922dX.\146295i]\151113\1028288\rWS\USU\1098732\SUB\49884\1083906\DLE\STXN~-\SO6\190031\1110322\\O\185165Jc\1052359\1071278\NULHSo\DLE-W\DC36\170321I\1068712)\99800={\99796h\27961\61707M\1022570FwJQ\1111976ck\SUB\CAN|UV-\NAK\SOH|\DC4;\f\156907\145795\ENQS\NAK.B\"D\163007#o*\126577\32988m\RS\1049834B3Gg;\DC1\\\180659\1098926\ENQ B^\SI\152630$e\39220\170037>fMgC\187276,o\128488\\?\1033955~/s\SOH?MMc;D18Ne\EOT\CAN)*\STX\GS\16268 +-- +-- 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.SendActivationCode_user where + +import Data.ISO3166_CountryCodes +import Data.LanguageCodes qualified +import Imports +import Wire.API.User +import Wire.API.User.Activation + +testObject_SendActivationCode_1 :: SendActivationCode +testObject_SendActivationCode_1 = + SendActivationCode + { emailKey = Email {emailLocal = "\1021635", emailDomain = "nK"}, + locale = Nothing + } + +testObject_SendActivationCode_2 :: SendActivationCode +testObject_SendActivationCode_2 = + SendActivationCode + { emailKey = Email {emailLocal = "b", emailDomain = "4M\1076452P\149723$[\DC2j"}, + locale = + Just (Locale {lLanguage = Language Data.LanguageCodes.CU, lCountry = Just (Country {fromCountry = VI})}) + } diff --git a/libs/wire-api/test/golden/testObject_Activate_user_1.json b/libs/wire-api/test/golden/testObject_Activate_user_1.json index 1578b22ac2f..d4cc0474516 100644 --- a/libs/wire-api/test/golden/testObject_Activate_user_1.json +++ b/libs/wire-api/test/golden/testObject_Activate_user_1.json @@ -1,5 +1,5 @@ { - "code": "HUUpJQ==", - "dryrun": true, - "phone": "+45520903" + "code": "fg==", + "dryrun": false, + "key": "e3sm9EjNmzA=" } diff --git a/libs/wire-api/test/golden/testObject_Activate_user_10.json b/libs/wire-api/test/golden/testObject_Activate_user_10.json deleted file mode 100644 index 93b403e17a8..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_10.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "kcvCq2A=", - "dryrun": false, - "key": "1szizA==" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_11.json b/libs/wire-api/test/golden/testObject_Activate_user_11.json deleted file mode 100644 index 87251a5b113..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_11.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "MZpmmg==", - "dryrun": false, - "email": "\u00034\u001a@" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_12.json b/libs/wire-api/test/golden/testObject_Activate_user_12.json deleted file mode 100644 index 601ac6c7255..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_12.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "sScBopoNTb0=", - "dryrun": true, - "key": "V3mr5D4=" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_13.json b/libs/wire-api/test/golden/testObject_Activate_user_13.json deleted file mode 100644 index 638148bec02..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_13.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "ysvb", - "dryrun": false, - "key": "haH9_sUNFw==" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_14.json b/libs/wire-api/test/golden/testObject_Activate_user_14.json deleted file mode 100644 index fd2763b59c3..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_14.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "hQ==", - "dryrun": true, - "phone": "+13340815619" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_15.json b/libs/wire-api/test/golden/testObject_Activate_user_15.json deleted file mode 100644 index ce73a7296db..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_15.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "biTZ", - "dryrun": false, - "email": "圤W[󾒿G󳍬]{\n@ V8󲏽\u0015*" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_16.json b/libs/wire-api/test/golden/testObject_Activate_user_16.json deleted file mode 100644 index fcbcd59dbf7..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_16.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "5W4=", - "dryrun": true, - "phone": "+77635104433" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_17.json b/libs/wire-api/test/golden/testObject_Activate_user_17.json deleted file mode 100644 index 029ba0efa82..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_17.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "ShjEcgx6P0Hs", - "dryrun": false, - "phone": "+556856857856" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_18.json b/libs/wire-api/test/golden/testObject_Activate_user_18.json deleted file mode 100644 index f1fdb6c2dae..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_18.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "xRvktQ==", - "dryrun": false, - "email": "2􎖰B􌕾\u00032\u001f􇰋@v\u0001\u000e󶃯/e" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_19.json b/libs/wire-api/test/golden/testObject_Activate_user_19.json deleted file mode 100644 index 78325351be1..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_19.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "", - "dryrun": false, - "key": "1fCrdg==" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_2.json b/libs/wire-api/test/golden/testObject_Activate_user_2.json index d4cc0474516..8975d735dc0 100644 --- a/libs/wire-api/test/golden/testObject_Activate_user_2.json +++ b/libs/wire-api/test/golden/testObject_Activate_user_2.json @@ -1,5 +1,5 @@ { - "code": "fg==", + "code": "", "dryrun": false, - "key": "e3sm9EjNmzA=" + "email": "󴴺\u0000􆞵@k\\\u0001a\u0016*𫅳" } diff --git a/libs/wire-api/test/golden/testObject_Activate_user_20.json b/libs/wire-api/test/golden/testObject_Activate_user_20.json deleted file mode 100644 index 6e3a3bb7641..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_20.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "7PtclAevMzA=", - "dryrun": false, - "phone": "+893051142276" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_3.json b/libs/wire-api/test/golden/testObject_Activate_user_3.json index bc9eb55c56f..a54047d9a78 100644 --- a/libs/wire-api/test/golden/testObject_Activate_user_3.json +++ b/libs/wire-api/test/golden/testObject_Activate_user_3.json @@ -1,5 +1,5 @@ { - "code": "OAbwDkw=", + "code": "61wG", "dryrun": true, - "phone": "+44508058" + "key": "DkV9xQ==" } diff --git a/libs/wire-api/test/golden/testObject_Activate_user_4.json b/libs/wire-api/test/golden/testObject_Activate_user_4.json index fd7147e34c9..601ac6c7255 100644 --- a/libs/wire-api/test/golden/testObject_Activate_user_4.json +++ b/libs/wire-api/test/golden/testObject_Activate_user_4.json @@ -1,5 +1,5 @@ { - "code": "811p-743Gvpi", - "dryrun": false, - "phone": "+97751884" + "code": "sScBopoNTb0=", + "dryrun": true, + "key": "V3mr5D4=" } diff --git a/libs/wire-api/test/golden/testObject_Activate_user_5.json b/libs/wire-api/test/golden/testObject_Activate_user_5.json deleted file mode 100644 index 8975d735dc0..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_5.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "", - "dryrun": false, - "email": "󴴺\u0000􆞵@k\\\u0001a\u0016*𫅳" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_6.json b/libs/wire-api/test/golden/testObject_Activate_user_6.json deleted file mode 100644 index 844c5c091d7..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_6.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "FXrNll0Kqg==", - "dryrun": false, - "email": "􍧃i>󶃾Ha!@" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_7.json b/libs/wire-api/test/golden/testObject_Activate_user_7.json deleted file mode 100644 index 8622155b543..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_7.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "8yl3qERc", - "dryrun": false, - "key": "jQ==" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_8.json b/libs/wire-api/test/golden/testObject_Activate_user_8.json deleted file mode 100644 index 681812203b2..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_8.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "NF20Avw=", - "dryrun": true, - "phone": "+3276478697350" -} diff --git a/libs/wire-api/test/golden/testObject_Activate_user_9.json b/libs/wire-api/test/golden/testObject_Activate_user_9.json deleted file mode 100644 index a54047d9a78..00000000000 --- a/libs/wire-api/test/golden/testObject_Activate_user_9.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "61wG", - "dryrun": true, - "key": "DkV9xQ==" -} diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_10.json b/libs/wire-api/test/golden/testObject_InvitationList_team_10.json index c06f56c3cf3..9fe5bac056d 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_10.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_10.json @@ -7,7 +7,6 @@ "email": "}@", "id": "00000000-0000-0000-0000-000000000000", "name": "P𥖧\u0006'e\u0010\u001d\"\u0011K󽗨Fcvm[\"Sc}U𑊒􂌨󿔟~!E􀖇\u000bV", - "phone": null, "role": "member", "team": "00000000-0000-0001-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_11.json b/libs/wire-api/test/golden/testObject_InvitationList_team_11.json index 3b5e6bce7fb..78ed3f5c569 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_11.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_11.json @@ -7,7 +7,6 @@ "email": "@Z", "id": "00000001-0000-0000-0000-000000000001", "name": "G\\,\u0000=ෝI-w󠀹}𠉭抳-92\u0013@\u0006\u001f\\F\u001a\"-r꒫6\u000fඬ\u001f*}c󼘹\u001f\u0007T8m@旅M\u0012#MIq\r4nW􍦐y\u0005Ud룫#𫶒5\n\u0002V]𨡀\"󶂃𩫘0:ﲼ𮭩+\u0001\u000bP󹎷X镟􅔧.\u0019N\"𬋻", - "phone": "+872574694", "role": "admin", "team": "00000000-0000-0001-0000-000100000000", "url": null @@ -18,7 +17,6 @@ "email": "@", "id": "00000000-0000-0000-0000-000100000000", "name": "叕5q}B\u0001𦌜`イw\\X@󼶝𢼈7Mw,*z{𠚷&~", - "phone": "+143031479742", "role": "partner", "team": "00000000-0000-0001-0000-000000000001", "url": null @@ -29,7 +27,6 @@ "email": "@", "id": "00000000-0000-0001-0000-000000000000", "name": "V􈫮\u0010qYヒCU\u000e􄕀fQJ\u0005ਓq+\u0007\u0016󱊸\u0011@𤠼`坟qh+𬾬A7𦄡Y \u0011Tㅎ1_􈩇#B<􂡁;a6o=", - "phone": "+236346166386230", "role": "partner", "team": "00000001-0000-0000-0000-000000000000", "url": null @@ -40,7 +37,6 @@ "email": "@", "id": "00000001-0000-0001-0000-000100000000", "name": ",􃠾{ս\u000c𬕻Uh죙\t\u001b\u0004\u0001O@\u001a_\u0002D􎰥𦀛\u0016g}", - "phone": "+80162248", "role": "admin", "team": "00000001-0000-0001-0000-000100000001", "url": null @@ -51,7 +47,6 @@ "email": "@", "id": "00000000-0000-0001-0000-000100000000", "name": null, - "phone": null, "role": "owner", "team": "00000000-0000-0001-0000-000100000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_16.json b/libs/wire-api/test/golden/testObject_InvitationList_team_16.json index 535fe0678e2..944313e9490 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_16.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_16.json @@ -7,7 +7,6 @@ "email": "\u000f@", "id": "00000001-0000-0000-0000-000100000001", "name": "E𝘆YM<󾪤j􆢆\r􇳗O󴟴MCU\u001eI󳊃m𔒷hG\u0012|:P􅛽Vj\u001c\u0000ffgG)K{􁇏7x5󱟰𪔘\n\u000clT􆊞", - "phone": "+36515555", "role": "owner", "team": "00000001-0000-0001-0000-000100000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_17.json b/libs/wire-api/test/golden/testObject_InvitationList_team_17.json index eba7991502c..ae671cd4808 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_17.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_17.json @@ -7,7 +7,6 @@ "email": "&@𫳦", "id": "00000001-0000-0001-0000-000000000001", "name": null, - "phone": null, "role": "partner", "team": "00000001-0000-0000-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_2.json b/libs/wire-api/test/golden/testObject_InvitationList_team_2.json index 076b78a0d43..66ae47e2f38 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_2.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_2.json @@ -7,7 +7,6 @@ "email": "𥝢@w", "id": "00000001-0000-0001-0000-000000000000", "name": "fuC9p􌌅A𧻢\u000c\u0005\u000e刣N룞_?oCX.U\r𧾠W腈󽥝\u0013\t[錣\u0016/⃘A𣚁𪔍\u0014H𠽙\u0002𨯠\u0004𨒤o\u0013", - "phone": "+851333011", "role": "owner", "team": "00000000-0000-0000-0000-000000000001", "url": "https://example.com/inv14" diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_20.json b/libs/wire-api/test/golden/testObject_InvitationList_team_20.json index 26a5ab01344..0ffd1042d30 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_20.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_20.json @@ -7,7 +7,6 @@ "email": "@", "id": "00000001-0000-0000-0000-000100000000", "name": null, - "phone": "+745177056001783", "role": "partner", "team": "00000001-0000-0001-0000-000000000000", "url": null @@ -18,7 +17,6 @@ "email": "@", "id": "00000001-0000-0001-0000-000000000000", "name": "YPf╞:\u0005Ỉ&\u0018\u0011󽧛%ꦡk𪯋􅥏:Q\u0005F+\u0008b8Jh􌎓K\u0007\u001dY\u0004􃏡\u000f󽝰\u0016 􁗠6>I󾉩B$z?𤢾wECB\u001e𥼬덄\"W𗤞󲴂@\u001eg)\u0001m!-U􇧦󵜰o\u0006a\u0004𭂢;R􂪧kgT􍆈f\u0004\u001e\rp𓎎󿉊X/􄂲)\u00025.Ym󵳬n싟N\u0013𫅄]?'𠴺a4\"󳟾!i5\u001e\u001dC14", - "phone": null, "role": "owner", "team": "00000001-0000-0000-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_4.json b/libs/wire-api/test/golden/testObject_InvitationList_team_4.json index 3063b4fdeb3..d0cbd90f1e7 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_4.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_4.json @@ -7,7 +7,6 @@ "email": "@", "id": "00000001-0000-0001-0000-000100000000", "name": "R6𠥄𠮥VQ𭴢\u001a\u0001𬄺0C􉶍\u001bR𭗈𞡊@韉Z?\u0002𩖫􄭦e}\u0001\u0017\u0004m𭉂\u001f]󰺞𮉗􂨮󰶌\u0008\u0011zfw-5𝝖\u0018􃸂 \u0019e\u0014|㡚Vo{􆳗\u0013#\u001fS꿻&zz𧏏9𢱋,\u000f\u000c\u0001p󺜰\u0010𧵪􂸑.&󳢨kZ쓿u\u0008왌􎴟n:􍝋D$.Q", - "phone": "+60506387292", "role": "admin", "team": "00000000-0000-0001-0000-000000000000", "url": null @@ -18,7 +17,6 @@ "email": "@", "id": "00000001-0000-0000-0000-000100000000", "name": "\u0012}q\u0018=SA\u0003x\t\u0003\\\u000b[\u0008)(\u001b]𡋃Y\u000b@pꈫl뀉𦛌\u0000\t􌤢\u00011\u0011\u0005󹝃\"i猔\u0019\u0008\u0006\u000f\u0012v\u0006", - "phone": "+913945015", "role": "admin", "team": "00000000-0000-0001-0000-000100000000", "url": null @@ -29,7 +27,6 @@ "email": "@", "id": "00000001-0000-0001-0000-000100000001", "name": "&􂧽Ec\u0000㼓}k󼾘l𪍯\u001fJ\u00190^.+F\u0000\u000c$'`!\u0017[p󾓉}>E0y𗢸#4I\u0007𐐡jc\u001bgt埉􊹘P\u0014!􋣥E93'Y$YL뜦b\r:,𬘞\u000e𥚟y\u0003;􃺹􌛖z4z-D􋰳a𡽜6𨏝r󼖨󱌂J\u0010밆", - "phone": "+17046334", "role": "member", "team": "00000001-0000-0000-0000-000000000001", "url": null @@ -40,7 +37,6 @@ "email": "@", "id": "00000001-0000-0000-0000-000000000000", "name": "Ft*O1\u0008&\u000e\u0018<𑨛􊰋m\n\u0014\u0012; \u0003󱚥\u0011􂬫\"k.T󹴑[[\u001c\u0004{j`\u001d󳟞c􄖫{\u001a\u001dQY𬨕\t\u0015y\t𠓳j󼿁W ", - "phone": null, "role": "owner", "team": "00000000-0000-0000-0000-000000000000", "url": null @@ -51,7 +47,6 @@ "email": "@", "id": "00000001-0000-0000-0000-000000000000", "name": null, - "phone": "+918848647685283", "role": "admin", "team": "00000001-0000-0000-0000-000100000000", "url": null @@ -62,7 +57,6 @@ "email": "@", "id": "00000000-0000-0000-0000-000100000001", "name": "Lo\r􎒩B𗚰_v󰔢􆍶󻀬􊽦9\u0002vyQ🖰&W󻟑𠸘􇹬'􁔫:𤟗𡶘􏹠}-o󿜊le8Zp󺩐􋾙)nK\u00140⛟0DE\u0015K$io\u001e|Ip2ClnU𬖍", - "phone": "+2239859474784", "role": "owner", "team": "00000001-0000-0001-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_6.json b/libs/wire-api/test/golden/testObject_InvitationList_team_6.json index 03aa3d04857..689afff6db0 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_6.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_6.json @@ -7,7 +7,6 @@ "email": "@", "id": "00000001-0000-0000-0000-000000000000", "name": null, - "phone": null, "role": "admin", "team": "00000001-0000-0001-0000-000100000000", "url": null @@ -18,7 +17,6 @@ "email": "@", "id": "00000000-0000-0001-0000-000000000000", "name": null, - "phone": "+85999765", "role": "admin", "team": "00000000-0000-0000-0000-000100000000", "url": null @@ -29,7 +27,6 @@ "email": "@", "id": "00000000-0000-0001-0000-000000000001", "name": null, - "phone": "+150835819626453", "role": "owner", "team": "00000001-0000-0000-0000-000100000000", "url": null @@ -40,7 +37,6 @@ "email": "@", "id": "00000001-0000-0000-0000-000100000000", "name": "YBc\r웶8{\\\n􋸓+\u0008\u0016'<\u0004􈄿Z\u0007nOb􋨴􌸖𩮤}2o@v/", - "phone": "+787465997389", "role": "member", "team": "00000000-0000-0001-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json index ffd2be10c76..4d2324b3889 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json @@ -2,6 +2,5 @@ "email": "/Y𨎂\u000b}?@󲚚󾋉𫟰\u000e󽈝", "locale": "nn", "name": null, - "phone": null, "role": "owner" } diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json index ec3bbd9af53..ee7c2e71517 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json @@ -2,6 +2,5 @@ "email": "󶩭\u000c\u0006\u0010^s@d", "locale": "ny-OM", "name": "H󶌔\u001e댥𖢯uv󿊧\u0012󿕜\u001a 𧆤=a\u001b4H,B\u0018󽲴GpV0󿇇;_\u0000𪔺Z\u0011滘\u00156耐'W9z⻒\tr𤭦􂃸\u0016_ge豍\u0004D𗈌o\u0007n>󲤯", - "phone": "+3547398978719", "role": "member" } 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 165dc4525b6..976d7bf73bc 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json @@ -2,6 +2,5 @@ "email": "\u0001\u0000󸊱nJ@t\u0002.", "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", - "phone": "+861174152363", "role": "owner" } 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 e4109225571..c89a4fc9558 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json @@ -2,6 +2,5 @@ "email": "􎪠􇿸@", "locale": "ar-PA", "name": "_\u0019@\u001d0춲󾌹󷱿\u001c\u0010􌩶!􈇮\u000ec\u001f\u0000\u0001>􆖳𩈈\u0019𪶲1}!h0\u0010􁈑w\u0004􆈑1aJ6c\u001d󰼊b𠍕{󳔞𠅳\u0007􋊉", - "phone": "+498796466910243", "role": null } 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 70e6af591fc..c19fcc929aa 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json @@ -2,6 +2,5 @@ "email": "\u0000r@c,", "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", - "phone": "+82438666720661", "role": null } 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 a53561f331f..3788c8d60ef 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json @@ -2,6 +2,5 @@ "email": "@\u000b", "locale": "dv-LB", "name": "\u0015wGn󳔃𤠘1}\u0004gY.>=}", - "phone": "+08345603", "role": "admin" } 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 76b0fcf6745..d3f6725f143 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json @@ -2,6 +2,5 @@ "email": "U@􈘸", "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", - "phone": "+19939600", "role": "owner" } 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 80a2ed29456..aba9a6724be 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json @@ -2,6 +2,5 @@ "email": "壧@\u0001", "locale": "om-BJ", "name": null, - "phone": "+3394446441", "role": "admin" } 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 10db3ce14b1..b5cff60b2b8 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json @@ -2,6 +2,5 @@ "email": "3\u000cC\u0017\"@\u00010x𝗢", "locale": "kj-TC", "name": null, - "phone": "+403706662", "role": "partner" } 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 bebfad27b85..b7214299685 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json @@ -2,6 +2,5 @@ "email": "\u0008\u0006b\n0@UJj&鞱", "locale": "ku", "name": "8VPAp𡧑2L}𫙕", "locale": null, "name": "kl\u0003\u0004\u0016%s7󻼗fX󲹙A\u00087\u0011D\u0004\u0011𨔣sg)dD𦙚Rx[󺭌Tw𐨕\u001e\u001a􀑔z\\\u000f\u0005䊞l􉾾l|oKc\\(𭬥􌵬=脜2VI*􋖛2oTh&#+;o᎙dXA⽇=*􆗾Q󼂨{󲺕󠁑5}\u001d9D𭟸􃿙r􇸖P:󳓗䏩𝓖\u0008\u001a\u001c\u000fF%<𞢹\u000fh\u001b\u0003\u000f󲶳\u001fO\u0000g_𤻨뢪󺥟\u0004􂔤􊃫z~%IA'R\u0008󶽴Hv^󾲱wrjb\t𨭛\u0003", - "phone": "+518729615781", "role": "admin", "team": "00000001-0000-0001-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_12.json b/libs/wire-api/test/golden/testObject_Invitation_team_12.json index ece82b4d173..582e9a37f65 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_12.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_12.json @@ -4,7 +4,6 @@ "email": "󸐞𢜑\u001e@", "id": "00000000-0000-0000-0000-000100000002", "name": "\u0010Z+wd^𐘊􆃨1\u0002YdXt>􇺼LSB7F9\\𠿬\u0005\n󱂟\"🀡|\u0007𦠺'\u001bTygU􎍔R칖􅧠O4󼷁E9\"󸃐\u0012Re\u0005D}􀧨𧢧􍭝\u0008V𫋾%98'\u001e9\u00064yP𔗍㡀ř\u0007w\t􌄦\u000b􇋳xv/Yl󵢬𦯯", - "phone": "+68945103783764", "role": "admin", "team": "00000000-0000-0000-0000-000000000002", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_13.json b/libs/wire-api/test/golden/testObject_Invitation_team_13.json index f12163f667d..75f28fbd493 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_13.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_13.json @@ -4,7 +4,6 @@ "email": "@r", "id": "00000002-0000-0000-0000-000200000002", "name": "U", - "phone": "+549940856897515", "role": "member", "team": "00000002-0000-0001-0000-000000000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_14.json b/libs/wire-api/test/golden/testObject_Invitation_team_14.json index 7b5764a6871..de52080fdea 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_14.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_14.json @@ -4,7 +4,6 @@ "email": "EI@{", "id": "00000001-0000-0000-0000-000200000002", "name": null, - "phone": "+89058877371", "role": "owner", "team": "00000002-0000-0002-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_15.json b/libs/wire-api/test/golden/testObject_Invitation_team_15.json index 7d5215c7822..c43a423ccf6 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_15.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_15.json @@ -4,7 +4,6 @@ "email": ".@", "id": "00000001-0000-0001-0000-000200000001", "name": "𑜘\u001f&KIL\u0013􉋏![\n6􏙭HEj4E⽨UL\u001f>2􅝓_\nJ킢Pv\u000e\u000fR碱8\u0008mS뇆mE\u0007g\u0016\u0005%㣑\u000c!\u000b\u001f𝈊\u0005𭇱󿄈\u000e83!j𒁾\u001d􅣣,\u001e\u0018F􃞋􏈇U\u0019Jb\u0011j\u0019Y𖢐O󶃯", - "phone": "+57741900390998", "role": "owner", "team": "00000000-0000-0002-0000-000100000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_16.json b/libs/wire-api/test/golden/testObject_Invitation_team_16.json index 853aab3be71..4fd4c8f9bfc 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_16.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_16.json @@ -4,7 +4,6 @@ "email": "\\@\"{", "id": "00000001-0000-0002-0000-000200000001", "name": "\u001d\u0014Q;6/_f*7􋅎\u000f+􊳊ꋢ9", - "phone": null, "role": "partner", "team": "00000001-0000-0001-0000-000100000002", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_17.json b/libs/wire-api/test/golden/testObject_Invitation_team_17.json index d7ae310a544..9ceba395190 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_17.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_17.json @@ -4,7 +4,6 @@ "email": "@\u0001[𗭟", "id": "00000001-0000-0001-0000-000100000001", "name": "Z\u001b9E\u0015鍌𔗕}(3m𗮙𗷤'􅺒.WY;\u001e8?v-􌮰\u0012󸀳", - "phone": null, "role": "admin", "team": "00000000-0000-0001-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_19.json b/libs/wire-api/test/golden/testObject_Invitation_team_19.json index aaa9b35ce06..33980c38c72 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_19.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_19.json @@ -4,7 +4,6 @@ "email": "󸽎𗜲@(S\u0017", "id": "00000001-0000-0002-0000-000000000001", "name": "靸r𛋕\u0003Qi󴊗􌃗\u0019𩫻𒉓+􄮬Q?H=G-\u001e;􍝧\u000eq^K;a􀹚W\u0019 X𔖸􆂨>Mϔ朓jjbU-&󽼈v\u0000y𬙼\u0007|\u0016UfJCHjP\u000e􏘃浍DNA:~s", - "phone": "+05787228893", "role": "member", "team": "00000000-0000-0000-0000-000200000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_2.json b/libs/wire-api/test/golden/testObject_Invitation_team_2.json index 393eaccd4f2..fd2dea38a1e 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_2.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_2.json @@ -4,7 +4,6 @@ "email": "i@m_:", "id": "00000002-0000-0001-0000-000100000002", "name": "􄭇} 2pGEW+\rT𩹙p𪨳𦘢&𣫡v0\u0008", - "phone": null, "role": "partner", "team": "00000000-0000-0001-0000-000000000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_20.json b/libs/wire-api/test/golden/testObject_Invitation_team_20.json index 653fafc89ea..fb578312b1c 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_20.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_20.json @@ -4,7 +4,6 @@ "email": "b@u9T", "id": "00000002-0000-0001-0000-000000000001", "name": null, - "phone": "+27259486019", "role": "partner", "team": "00000001-0000-0000-0000-000000000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_3.json b/libs/wire-api/test/golden/testObject_Invitation_team_3.json index 6222659d12a..c9d2554f3a6 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_3.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_3.json @@ -4,7 +4,6 @@ "email": "@秕L", "id": "00000002-0000-0001-0000-000100000002", "name": null, - "phone": null, "role": "partner", "team": "00000002-0000-0001-0000-000100000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_4.json b/libs/wire-api/test/golden/testObject_Invitation_team_4.json index 8e8dedc4a4d..96a0ef0b999 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_4.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_4.json @@ -4,7 +4,6 @@ "email": "^@e", "id": "00000001-0000-0001-0000-000000000001", "name": null, - "phone": null, "role": "admin", "team": "00000000-0000-0000-0000-000100000000", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_5.json b/libs/wire-api/test/golden/testObject_Invitation_team_5.json index ce4196efbb0..46aeeb0d060 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_5.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_5.json @@ -4,7 +4,6 @@ "email": "\u0001V@f􉌩꧆", "id": "00000000-0000-0002-0000-000000000002", "name": "}G_𤃊`X󻋗𠆝󷲞L\"󿶗e6:E쨕󲟇f-$𠬒Z!s2p?#\tF 8𭿰𨕿󹵇\u0004􉢘*󸚄\u0016\u0010%Y𩀄>􏘍󾨶󺶘g\"􁥰\u001a\u001a𬇟ꦛ\u0004v𭽢,𩶐(\u001dQT𤪐;􃨚\u0005\u0017B􎇮H𩣓\\󾃾,Y", - "phone": "+45207005641274", "role": "owner", "team": "00000002-0000-0000-0000-000000000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_6.json b/libs/wire-api/test/golden/testObject_Invitation_team_6.json index 37e3f45bdcd..6cd8e6fb8b7 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_6.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_6.json @@ -4,7 +4,6 @@ "email": "@OC", "id": "00000001-0000-0002-0000-000100000000", "name": "O~\u0014U\u001e?V3_𮬰Slh􅱬Q1󶻳j|~M7􊲚􋽼𗆨\u0011K􇍼Afs𫬇lGV􏱇]`o\u0019f蓤InvfDDy\\DI𧾱􊥩\u0017B𦷬F*X\u0001\u001a얔\u0003\u0010<\u0003\u0016c\u0010,p\u000b*󵢘Vn\u000cI𑈹xS\u0002V\u001b$\u0019u󴮖xl>\u0007Z\u00144e\u0014aZ", - "phone": "+75547625285", "role": "admin", "team": "00000001-0000-0000-0000-000000000001", "url": null diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_7.json b/libs/wire-api/test/golden/testObject_Invitation_team_7.json index 844522e7165..d961ec2d508 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_7.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_7.json @@ -4,7 +4,6 @@ "email": "oj@", "id": "00000000-0000-0000-0000-000000000002", "name": "\u0018.𛅷􈼞\u0010\u000c\u0010\u0018𤰤o;Yay:yY $\u0003<ͯ%@\u001fre>5L'R\u0013𫝳oy#]c4!𘖝U홊暧󾜸􃕢p_>f\u000e𪲈􇇪󳆗_Vm\u001f}\u0002Pz\r\u0005K\u000e+>󲆠\u0000𥝻?pu?r\u001b\u001a!?𩇕;ᦅS䥅\u0007􅠬\u0008󹹝􆺅y\u000b󸣠\u0015𠢼\u001f󺘓\u0006s\u000f\u0007\u001f" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_17.json b/libs/wire-api/test/golden/testObject_LoginId_user_17.json deleted file mode 100644 index 4c307e4295a..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_17.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "handle": "e3iusdy" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_18.json b/libs/wire-api/test/golden/testObject_LoginId_user_18.json deleted file mode 100644 index 5d067be0f3e..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_18.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "handle": "8vpices3usz1dfs4u2lf_e3jendod_szl1z111_eoj4b7k7ajj-xo.qzbw4espf3smnz_" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_19.json b/libs/wire-api/test/golden/testObject_LoginId_user_19.json deleted file mode 100644 index adb91ee9260..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_19.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "handle": "3jzpp2bo8" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_2.json b/libs/wire-api/test/golden/testObject_LoginId_user_2.json index 104b2810dc6..7f8f368af3c 100644 --- a/libs/wire-api/test/golden/testObject_LoginId_user_2.json +++ b/libs/wire-api/test/golden/testObject_LoginId_user_2.json @@ -1,3 +1,3 @@ { - "phone": "+178807168" + "email": "0􉵟^󴊽𣎋\u0000)|𬱓:@q6e/$󼐅Zb􀖑)󱿷05i乭~q􅨬🙈y" } diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_20.json b/libs/wire-api/test/golden/testObject_LoginId_user_20.json deleted file mode 100644 index a361b16adfa..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_20.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "@𦃻" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_3.json b/libs/wire-api/test/golden/testObject_LoginId_user_3.json index 7f8f368af3c..809acaf04c8 100644 --- a/libs/wire-api/test/golden/testObject_LoginId_user_3.json +++ b/libs/wire-api/test/golden/testObject_LoginId_user_3.json @@ -1,3 +1,3 @@ { - "email": "0􉵟^󴊽𣎋\u0000)|𬱓:@q6e/$󼐅Zb􀖑)󱿷05i乭~q􅨬🙈y" + "handle": "7a8gg3v98" } diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_4.json b/libs/wire-api/test/golden/testObject_LoginId_user_4.json index 809acaf04c8..a0990b9f33e 100644 --- a/libs/wire-api/test/golden/testObject_LoginId_user_4.json +++ b/libs/wire-api/test/golden/testObject_LoginId_user_4.json @@ -1,3 +1,3 @@ { - "handle": "7a8gg3v98" + "handle": "lb" } diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_5.json b/libs/wire-api/test/golden/testObject_LoginId_user_5.json index ee5aa511ee7..59286829b46 100644 --- a/libs/wire-api/test/golden/testObject_LoginId_user_5.json +++ b/libs/wire-api/test/golden/testObject_LoginId_user_5.json @@ -1,3 +1,3 @@ { - "phone": "+041157889572" + "handle": "z58-6fbjhtx11d8t6oplyijpkc2.fp_lf3kpk3_.qle4iecjun2xd0tpcordlg2bwv636v3cthpgwah3undqmuofgzp8ry6gc6g-n-kxnj7sl6771hxou7-t_ps_lu_t3.4ukz6dh6fkjq2i3aggtkbpzbd1162.qv.rbtb6e.90-xpayg65z9t9lk2aur452zcs9a" } diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_6.json b/libs/wire-api/test/golden/testObject_LoginId_user_6.json index 0af8c1681cc..ee43cc24642 100644 --- a/libs/wire-api/test/golden/testObject_LoginId_user_6.json +++ b/libs/wire-api/test/golden/testObject_LoginId_user_6.json @@ -1,3 +1,3 @@ { - "phone": "+2351341820189" + "email": "@䜸\u0019+h\u0005(D\u000e灕󲤉 \u0007\r1" } diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_7.json b/libs/wire-api/test/golden/testObject_LoginId_user_7.json deleted file mode 100644 index a0990b9f33e..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_7.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "handle": "lb" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_8.json b/libs/wire-api/test/golden/testObject_LoginId_user_8.json deleted file mode 100644 index 984511ad78b..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_8.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "phone": "+2831673805093" -} diff --git a/libs/wire-api/test/golden/testObject_LoginId_user_9.json b/libs/wire-api/test/golden/testObject_LoginId_user_9.json deleted file mode 100644 index 9d3fe372cee..00000000000 --- a/libs/wire-api/test/golden/testObject_LoginId_user_9.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "phone": "+1091378734554" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_10.json b/libs/wire-api/test/golden/testObject_Login_user_10.json deleted file mode 100644 index 4c70905e87c..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_10.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "㑃𡃨!\u0017i􀖿", - "label": "", - "phone": "+4211134144507" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_11.json b/libs/wire-api/test/golden/testObject_Login_user_11.json deleted file mode 100644 index 00eae0e5a33..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_11.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "􅅣+W\u00193", - "label": null, - "phone": "+338932197597737" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_12.json b/libs/wire-api/test/golden/testObject_Login_user_12.json deleted file mode 100644 index e3414dd15b4..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_12.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "label": "\u000f🜓-𞡌:𡍁鮸\u0006\u000e", - "password": "n􋜩Q𩗀\u001b󵅀&Q/\rdꠚ\u001f\u0004w2C\u0006􁹬𫝔\u0004\u0004v󶥜\u0008f,b\u0002󷜰'𪹐C]G듡󸓯𮤾4\u0000Y.𪘲\u000e3sI菌F􈲾5剑rG/:\"󷣦X띟6\u001c:\u0018\u0007eYwWT􈦚𡛑Msbm\u0015@󰗜󷜉\u0004^\u001c𣹘\u0015@\u0005>\u000c\u001eUc\u0004V9&cල\u0007󰱴a'PPG𘡝𫶶>[ൽ2ﷄXc𠃪[0󴲖\u0008𘕄B\u0011[󻑵\u001d䰻\u001f\u0019s-u\u0017s􄡽󵗐𧝿n􅳀?󿒋ck\u00148XC𪣑\u001eI2ମ\u0002\u0010M\u001b\n?<\\\u0013E𑨛\u001d\n$cyS𡐆!,\u000b9\u0017/\u0011?P\u0017ꌞ\u0012󴁱~􂟉W-W4K8.\u00127\u0019L􇌡h\u000f}t+H\u001a\u001bX𝛋s\u0004t𫘧taa\u001d\u000c𥌭(v󺈨M\u001bvg3P1󼊃]gཝ4T\u0015$镄);\\8􎲭\nK\u0015}D'^fJ'𢽥e𪟤骭!\u0019.\u0012{\\CEp󿎈\u0017k_􈨀䟝𨄪􃨬]MG$𭴂[E􏠾\u0008􆅏{b엚\u001b^b@W\u0015$\u001c<󹾗&𦅘R\u0006J\u000f􊷴􌳱ꇞn󵸞8]𤍀\u0005}|k\u0002\u0018Q\u001fI\u0007\u0018DZ􃟝\u0000쐕rb䨃3G%\u001c𧤡\u0004\u00154YB0-i󸣑IM􆋴[􏘂:Cr$𘔴)L𡚅W鿁.x;ꇵ󻨷󳃅\u001fkb\u0018Y9)\u00164\u000f􍙥Av.\r\u000c􃏥9{\u000e\u0017P\u000c茂u\r-9cB4󸄛G\u001e夡󷯔r📷HcsPms𝢛!|J<\u00108\u001c[\u0015WYAK𒔃^󰾪c3󾜀\u0007C\u0003\u0017􁐫Y\u0014f\u0006􃁑!󼀑:RlQ!BX\u000c=􅙦f𤽂𛰿O\u0003\\\"퀛B<\u001eLV4g%3􌅏\u0006`\u0015>\n깒kp󰯶𩷗H冘lyJ\u0012)􁦭(󺰛A\u001ch\u0004j観\u0014M\u001bP-q\u0008n\u0018𢿎~\u001d\u0019\"o合%*e2𨛝L󹼿sy𥕑2m\u001d􀇖{EG]\u00116B+{󰉆IYa󶈙5,<\u001bX\u000c\u000f𭣵𥢐E𠴇󶶐L<\u0019dUO\u0017\u001aZYm\u0006􉍰R\u001a󲋒\u0013^s\u000cu_g\u0019?i~}V2𤓉R\u001c\u00043j댑m؆􌱔\n7S\u000fT5j𩮢\u000f󷵝𢤓h𬣐Q𣲺\u0015ZM􏈮02f🤼l!*\u0001󺯙\u0001􅔰􋑷\t𑱥\u001ba:q UKEN\u001e-\n\u0003H坝a􆘓\u0008鉶\"󼳴𤢿󼎳R4\u0003\u0010\u001c\u0002󵓎%\"@󶛙6=/x\u0000P\u0004𪬗/𮙙\u000c\u000c󵙚?*\u000cIcKWQ\"󴣾P*􋢩6=d\n𦟰\u001e􉧚\u0004\u0012I릍U\u0008=Pc\u0010", - "phone": "+153353668", - "verification_code": "RcplMOQiGa-JY" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_13.json b/libs/wire-api/test/golden/testObject_Login_user_13.json deleted file mode 100644 index 5b2504ef693..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_13.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "&󾂂y", - "label": null, - "phone": "+626804710" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_14.json b/libs/wire-api/test/golden/testObject_Login_user_14.json deleted file mode 100644 index 779e05afdc9..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_14.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "", - "label": "𗘼搊", - "phone": "+5693913858477" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_15.json b/libs/wire-api/test/golden/testObject_Login_user_15.json deleted file mode 100644 index b0b8cce642e..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_15.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "", - "label": "q\u0017(􉓔𭯸>8𢢂\n6", - "phone": "+56208262" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_16.json b/libs/wire-api/test/golden/testObject_Login_user_16.json deleted file mode 100644 index 778e9add165..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_16.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "_􏊊󵇀􎨕-_\u0017", - "label": "\u000eL􇜨󶔫􂰈@\u001c\u0010$", - "phone": "+588058222975" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_17.json b/libs/wire-api/test/golden/testObject_Login_user_17.json deleted file mode 100644 index a211d02d3a9..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_17.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "\u00171󴷦n\u0010dV󻦊d\u0001", - "label": "􏣙{/p𘝶", - "phone": "+3649176551364" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_18.json b/libs/wire-api/test/golden/testObject_Login_user_18.json deleted file mode 100644 index 25726fe2df5..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_18.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": ",𢆡㖮,", - "label": "5", - "phone": "+478931600" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_19.json b/libs/wire-api/test/golden/testObject_Login_user_19.json deleted file mode 100644 index 4fb0e32cbc2..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_19.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "code": "x橷<", - "label": "w;U\u001bx:", - "phone": "+92676996582869" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_2.json b/libs/wire-api/test/golden/testObject_Login_user_2.json index ccab54a54bc..b61e6008106 100644 --- a/libs/wire-api/test/golden/testObject_Login_user_2.json +++ b/libs/wire-api/test/golden/testObject_Login_user_2.json @@ -1,5 +1,6 @@ { - "code": "\nG􆶪8\u0008", - "label": "G", - "phone": "+956057641851" + "handle": "c2wp.7s5.", + "label": "􈏺𐌺>XC", + "password": "&\u001e\u0014􍢴ZⲚn\u000e𦯣󶳚/🄕'􃡾m󶪩\"☬𡷝\u001e(&󳓮\u000ef1Wf'I\u000f𘞾󿫦󼛩\u0011Jq􀠱Y\\Bedu@󷭷c󵇒D쿛􀟶S𣐞\u0003\u0003W>󵜮\u0014\rSO8FXy𨮱a\u0019𩠡\u001aNF𦧁L\u001e$5\u0000k\u001ez*s𤔬𦤜\u000b𪴹\"SY\u0002󲶃􍚚ub5q\u0005󷨛\u000bN.\t𬳰:l􍷴\u001e󺺉\u0007𩁁\u000e\u000bt􌏐W\u0016󾟜􎿛\u0007'v\u0017䀘\u0015\u0002 \u0015\u0002숔,􏙎x󿱴^􄡷櫦I;\u0015b􊧑o𧯋_𮡒MME󹩀\u000f􋨼H;\u000e\u0017s\u000e􄏑{Knlrd;讦\u0014\u000f􆝀TO􊏡󴃗U뺓􌢗t􄺈^y䍴u$\u0011Jp􁙤𨐩𨉞\u0002\"􋛧*\u000e󵌎綦󱻌X􌑜\u0003sK}\u0008𣈮\u00000󱘴12𩱬\tM052𮑯\u00040\u001e󰰚􈴐{ji\u001b󹎀橻&t \u000f\u001by\u0007L𡎯𠇦󲫫\r􁡥ga,\u0014do,tx[I&\u0014h\u0010\u0003\u0010Bpm󴬴-\u0007]/ZI󼎝q]w3n뜿e岌kYo5􊔜'K􊄜}v𣵇;󸮨\\=ꄰ8g\u0010g*has걿󵨦\u0013\u001fYg?I䰆\u0015aW2𤮏m\t}h𥸙RbU\u0002\u0017lz2!\u0013JW5\u001b󺡬U\u000eg,rpOᛡ]0\u001bǟ󵞃F\u000f󿗪\u001e\u000e⺄rl􍦲~\u0006+Mn{5󲧸a\u00192\u000b{jM\u0017T􂔹$\u0011􌣆\u001dj_~Z󵸥P\u0001\u0004o@TJhk\u0004\u0017k:-𗥇[p\u0010\u0011\u001e'\r\u0002Q,,󸐢?H\rh瘑\rj𤈎\u0012\\(u\u001bu𥱑󴳈o\u0014󱕌􍙩􀶂\u0011q\u001d-\u0008齧\u0011qW>\u000cysῂ,'𧃒<", + "verification_code": "RcplMOQiGa-JY" } diff --git a/libs/wire-api/test/golden/testObject_Login_user_20.json b/libs/wire-api/test/golden/testObject_Login_user_20.json deleted file mode 100644 index bfe793f70bb..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_20.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "email": "[%@,", - "label": "􁫀\r9󳰔`\u0015x", - "password": "ryzP\u00139⬓-1A)\u0008,u℉j~0􊐔󼘏\u000cI𩤎er\u0014V|}'kzG%A;3H\u0007mD\u0002U1\u0000^󾴴\u0010O&5u\u0004\u001a𨲆0A󳍿X\u0012\u001c7fEt𗱖rPvytT𡛓!𘥩$Q|BI+EM5\u0015\tRKrE\u0010\u001f\r?.\u0002|@1v^\u000bycpu\n$\u0012𭤳𠊆-Q𤸩\n\r󼛽𐬝O\u0005*𐰴Z\u001fo\u0004n𮂕%&\u0013Me*\u0002;\u0010034\nv\u0015𢑮(􆤦󱮺n@􎥹|봥d\n*\u000f\u0000}\u0015A!󿕺󽃯Hx\u00173\u0002{#T|5|GC􉸮z.\u001fN􇸓圴\u000bu\u0016~LP𤁿CV\u000e q𥆐\u0012e8h\u001fg󸷞;\u000c󳌋􎫐At󹦊)\u001fG\u0013𨪍馩|󾙻\u000f𠮹\u0004c~6\u0010:u𨘑##^\nn{d\u0018\ng㽭\u001b\u001f\u001f~A8};T\u001e\u0015)&\u0008\u0006􎁼\u001d(\u0013u;􋐛;=e􀨚\"黝vCt)o󰽾mꮈ𓄈l1+󼿼[\u0002FLx􇹤:󻼥󲗰71/kE𖹛p\u0014Ij\u0017蓳&\u001a^\u001cl1\u0006󸺜\u0003W,+3🐺𗖷\u001077rG'􇚂JC9M􁑬\u0016\u0012괾>~󸇴Y􃒫=i-\u000cS𪆘𦍨K2-@\u0005\u000c􎭳_1D-&🖂lR𭭰/󲫄$:窷:찫Dg󷷋O󶧽𩢅\u000e𫹟2z\u0015q𢣫c\u001cliJ{􁲵􂳦'BL𩋞;\u0002󿤼䠋B\u0000ẟb􅶹:w􎠰Ad\u001a6\u0015oퟯ\nsPWM{\u0003fW󸨅JT󹖱$󱞍핐𮝮𪓋u4􍖶\t蓥󽱢\"𥚰UM􈫴􋛮蔹􍶭\t\nIn'􅗄剩㻛\u0019\u0011<\u000b\u0008W\u000f}𢧯\u0008􅳓󼰓\u001d`􋍃x\u0000󰼹K\u001cj􇟷\u0011\u000f𩐠d󲆄k4\u001a󶣔쌗^􀾃󸐫i2=$:[f􃺃\u0012n\u0015J<=߬\u000f!z􍷔\u000eN\u0015\u0019𬈌V󺍬CQ_G\nY#ky𠚫k\u0013\u0005}OC𗤶}~M\u0019p\u0003\u001ex\u0008𬺚􅽰\u00088/\u0014?􈄶B󺝎\u0004\u000eU󹏩\u001b=%읶J𩎗\u0017󲕑󱱨󰡢\to􌳬X_@@뀷ꮰ$", - "verification_code": "RcplMOQiGa-JY" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_3.json b/libs/wire-api/test/golden/testObject_Login_user_3.json index b61e6008106..7042d152a24 100644 --- a/libs/wire-api/test/golden/testObject_Login_user_3.json +++ b/libs/wire-api/test/golden/testObject_Login_user_3.json @@ -1,6 +1,6 @@ { - "handle": "c2wp.7s5.", - "label": "􈏺𐌺>XC", - "password": "&\u001e\u0014􍢴ZⲚn\u000e𦯣󶳚/🄕'􃡾m󶪩\"☬𡷝\u001e(&󳓮\u000ef1Wf'I\u000f𘞾󿫦󼛩\u0011Jq􀠱Y\\Bedu@󷭷c󵇒D쿛􀟶S𣐞\u0003\u0003W>󵜮\u0014\rSO8FXy𨮱a\u0019𩠡\u001aNF𦧁L\u001e$5\u0000k\u001ez*s𤔬𦤜\u000b𪴹\"SY\u0002󲶃􍚚ub5q\u0005󷨛\u000bN.\t𬳰:l􍷴\u001e󺺉\u0007𩁁\u000e\u000bt􌏐W\u0016󾟜􎿛\u0007'v\u0017䀘\u0015\u0002 \u0015\u0002숔,􏙎x󿱴^􄡷櫦I;\u0015b􊧑o𧯋_𮡒MME󹩀\u000f􋨼H;\u000e\u0017s\u000e􄏑{Knlrd;讦\u0014\u000f􆝀TO􊏡󴃗U뺓􌢗t􄺈^y䍴u$\u0011Jp􁙤𨐩𨉞\u0002\"􋛧*\u000e󵌎綦󱻌X􌑜\u0003sK}\u0008𣈮\u00000󱘴12𩱬\tM052𮑯\u00040\u001e󰰚􈴐{ji\u001b󹎀橻&t \u000f\u001by\u0007L𡎯𠇦󲫫\r􁡥ga,\u0014do,tx[I&\u0014h\u0010\u0003\u0010Bpm󴬴-\u0007]/ZI󼎝q]w3n뜿e岌kYo5􊔜'K􊄜}v𣵇;󸮨\\=ꄰ8g\u0010g*has걿󵨦\u0013\u001fYg?I䰆\u0015aW2𤮏m\t}h𥸙RbU\u0002\u0017lz2!\u0013JW5\u001b󺡬U\u000eg,rpOᛡ]0\u001bǟ󵞃F\u000f󿗪\u001e\u000e⺄rl􍦲~\u0006+Mn{5󲧸a\u00192\u000b{jM\u0017T􂔹$\u0011􌣆\u001dj_~Z󵸥P\u0001\u0004o@TJhk\u0004\u0017k:-𗥇[p\u0010\u0011\u001e'\r\u0002Q,,󸐢?H\rh瘑\rj𤈎\u0012\\(u\u001bu𥱑󴳈o\u0014󱕌􍙩􀶂\u0011q\u001d-\u0008齧\u0011qW>\u000cysῂ,'𧃒<", + "handle": "c372iaa_v5onjcck67rlzq4dn5_oxhtx7dpx7v82lp1rhx0e97i26--8r3c6k773bxtlzmkjc20-11_047ydua_o9_5u4sll_fl3ng_0sa.", + "label": "LGz%𝒍j\u000c\u001e/\u0001", + "password": "𝘛𭆴DU󼸸hp󵱻t~\u0012\u0001\u0002*􁈚y1􇑮H𪒧{e\\S\u000e?c_7\t\u0014X𡀓6𪊲E𘝈j\u001a\t\u0016􉯿>HO]60󱭓\u0003\"+w,t􄐸\u0007k(b%u𤺝`>b󻂰e\u0006c𤽡􎠜)し7􈑠`𭟉yO+v%󼗀\rc<𐃤2>8u􋉲􇵝􏸗𒔙a𫯹\u0015=\u0004􆼝8R&j󾣆\u001b\t4sj-󲉛鷔n𡘽􃲙N\u001d\\󺡋𑩠5\r𗫬(P!爳棧\u0008􄫼Mr~﹣\u0019jt>Z\u001d~𢖼A󻲾\u000e\\>\u00116\">%댤􈵏I@u5𭷳\u000brY\r;7􅟌#􇒇󸇞\u0018'󾏵\u0019_I_zY󱂤𤟁\u0019d󽷤cd􃑯𡒆Cp3曕\u001dXj\n듡jy값\t-䩹ꥈd󿠓L\u001aYF\u0006POL헮\u0012\u0011\u0011\u0012*\rH\u0010(?\u0013F擜\u0010\r]􅆋j𩣁 @\u0005T􌮍s\u001cF2\u0015]8\u0007\u0013!\u0015W𫅕􏌲K󺐢􏢞_%󴥚􏯭'􌆥𑋘(#\u0001ky\t\u0017!䒢\u0015\u0014\u001b{𝈕U2LS'", "verification_code": "RcplMOQiGa-JY" } diff --git a/libs/wire-api/test/golden/testObject_Login_user_4.json b/libs/wire-api/test/golden/testObject_Login_user_4.json index 472dbd71aa8..8ba7a5ba2aa 100644 --- a/libs/wire-api/test/golden/testObject_Login_user_4.json +++ b/libs/wire-api/test/golden/testObject_Login_user_4.json @@ -1,5 +1,6 @@ { - "code": "𗈲m", - "label": ":", - "phone": "+04332691687649" + "email": "BG@⽩c\u000b}\u000fL$_", + "label": "\u000e\u0015eC/", + "password": "&󲉊󹴌𔖘\u0002J<-~\u0002>\u000b𒇴𥄿5QN틐𨤨ql\u0015𒈲3}{\u0013𪒺S壓;\t7𬺖_F~D*f􀕔)􄥂-9僛7GK= %\u001e@kOF#𫻩􋌁𞡂8_ꕅ\u001dL鍂\u0003󿶊0Wl1A`LYz\u001fy僸\u001ao\u001b[\u0014\u0008t𐑐a\u0003s~\u001fF𪰤G`$\u000bG\u0011󾿅🙣/󷪺C>\u000f", + "verification_code": "RcplMOQiGa-JY" } diff --git a/libs/wire-api/test/golden/testObject_Login_user_5.json b/libs/wire-api/test/golden/testObject_Login_user_5.json index 7042d152a24..20658e70ab3 100644 --- a/libs/wire-api/test/golden/testObject_Login_user_5.json +++ b/libs/wire-api/test/golden/testObject_Login_user_5.json @@ -1,6 +1,6 @@ { - "handle": "c372iaa_v5onjcck67rlzq4dn5_oxhtx7dpx7v82lp1rhx0e97i26--8r3c6k773bxtlzmkjc20-11_047ydua_o9_5u4sll_fl3ng_0sa.", - "label": "LGz%𝒍j\u000c\u001e/\u0001", - "password": "𝘛𭆴DU󼸸hp󵱻t~\u0012\u0001\u0002*􁈚y1􇑮H𪒧{e\\S\u000e?c_7\t\u0014X𡀓6𪊲E𘝈j\u001a\t\u0016􉯿>HO]60󱭓\u0003\"+w,t􄐸\u0007k(b%u𤺝`>b󻂰e\u0006c𤽡􎠜)し7􈑠`𭟉yO+v%󼗀\rc<𐃤2>8u􋉲􇵝􏸗𒔙a𫯹\u0015=\u0004􆼝8R&j󾣆\u001b\t4sj-󲉛鷔n𡘽􃲙N\u001d\\󺡋𑩠5\r𗫬(P!爳棧\u0008􄫼Mr~﹣\u0019jt>Z\u001d~𢖼A󻲾\u000e\\>\u00116\">%댤􈵏I@u5𭷳\u000brY\r;7􅟌#􇒇󸇞\u0018'󾏵\u0019_I_zY󱂤𤟁\u0019d󽷤cd􃑯𡒆Cp3曕\u001dXj\n듡jy값\t-䩹ꥈd󿠓L\u001aYF\u0006POL헮\u0012\u0011\u0011\u0012*\rH\u0010(?\u0013F擜\u0010\r]􅆋j𩣁 @\u0005T􌮍s\u001cF2\u0015]8\u0007\u0013!\u0015W𫅕􏌲K󺐢􏢞_%󴥚􏯭'􌆥𑋘(#\u0001ky\t\u0017!䒢\u0015\u0014\u001b{𝈕U2LS'", + "email": "@~^G􆪐\\", + "label": null, + "password": "z>􉰃󺎇/𡞯􊳌\u0008%$󽖨𣄄:}\t\u0018􂜙󾺽)㊝󵙼s󵪾\u0018}鱢\u0019[ꅾ\u000bX#VG,df4𢢵8m5딝OTK𣑌鋎꺯◆Z\"ZS\u001bms|[Q%􉲡\u0005W\\󴖙C𭌈+􅕺ဒ䖡v𬁡ꎞ){󻆡𣃒f𭬔}:X-\u00082N\u0019\u001fl🎢쇈Y􅤡󷐛r2.1싸\u0004+𡥙\u0013𣡈]'󻂳s󳤴ꜵ.}𭋣o󲍶X𠜥⬅\r\u001aNq6󸻕'\u000cd\u001e㢧􋾜,:%\t𥅬𒃣QD􉖠\u001b(q4KDQ2zcI\u0010>\u00195󲤼1\u000cBkd\u0013\u0006:F:\u0004𘨥ⶂO N\u001c,N􁚶󴌷[h9ᜬ:xZ=\u000c􈾀\u0013u\u001e\u000ce#\u001a^$lkx耤 \rr\u001aJ󷝦󸓡\u001cR][_5\u0015ⷤ诃5惵􁮵󳗴鉅K!􁁠eRR%絬+h~1󲞮谟lTzS$\u0010􂶳\"*􉕷pmRE\u0013(\u001f^ὯJc➑􅫇i\n+G$|󲫉𦉻g\u001c\u000cgU3Y𝄜\u0006f)􊾺\u0016𓈄􌭞/\u0000Piꩦ{󿸟j􈞅\u001c9𠚽󺊬翉w$눟𞴦)Si𨴄牿FX􂋒j{`궤`󳿗𧁁4u%􅔪P*􂉻捎C\u001eR\u0016-잚󶽕g𐰺:S>c㮢𠝌\u0010Y􄝏~a)YW_J􃢤P\u0007+ U􈷓j\u0019k\u0001􋴘\u0011䣷e𪋘𪳠,ᐏg@\u0012\u001dHXl.\u0017𥣁2\u0013mY􁢫\tv?L8L􆍼N𠦽\nb1j󾸸𤋵xfQ=\\\u0005e󳇪󹽶U\u0012p{\u000e􌚌jd^@U󲯝tP.\u0012Y%R`a\r𧍮7}HnUf𠛸m^7:\u0015=챼>l𗑑hwp27𤦾jE\u000cx=!.\u0013]Ar\tw\u0014&\u001ak㒞s󾦄ᆒI𣪗􂼥dsY\u0010𬚢dX.𣭷i]𤹉󻃀\rWS\u001fU􌏬\u001a시􈨂\u0010\u0002N~-\u000e6𮙏􏄲\\O𭍍Jc􀻇􅢮\u0000HSo\u0010-W\u00136𩥑I􄺨)𘗘={𘗔h洹M󹩪FwJQ􏞨ck\u001a\u0018|UV-\u0015\u0001|\u0014;\u000c𦓫𣦃\u0005S\u0015.B\"D𧲿#o*𞹱胜m\u001e􀓪B3Gg;\u0011\\𬆳􌒮\u0005 B^\u000f𥐶$e餴𩠵>fMgC𭮌,o🗨\\?󼛣~/s\u0001?MMc;D18Ne\u0004\u0018)*\u0002\u001d㾌􀡸l􉔂m'BtB5󴼐,t\"􄕤9(#\u00054\u000fIy>󻯶􌫾\u001dbf\"i\u0017㠟a􉊡C@􇘼􊨩纟\u0015󳻹嬰*N\u0016\u001b:iXibA𡚓𩘤q􀁗]:9r𒁉\u0000􀢋\u001fCN\u001f𤃾􀁹󸐝eR\u001eZbD5!8N\u001bVᲰ\u0006𪐈\u001auz􁓾𭾔~\u001b\u000f%{3I/F抐/DMS\u001f>o𭬿Z􎬞\u001d[K𭇡𗇅􉭱󳀒\u001bO-4\u0018\u001f\u001cZp", - "phone": "+930266260693371", - "verification_code": "RcplMOQiGa-JY" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_7.json b/libs/wire-api/test/golden/testObject_Login_user_7.json deleted file mode 100644 index 8ba7a5ba2aa..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_7.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "email": "BG@⽩c\u000b}\u000fL$_", - "label": "\u000e\u0015eC/", - "password": "&󲉊󹴌𔖘\u0002J<-~\u0002>\u000b𒇴𥄿5QN틐𨤨ql\u0015𒈲3}{\u0013𪒺S壓;\t7𬺖_F~D*f􀕔)􄥂-9僛7GK= %\u001e@kOF#𫻩􋌁𞡂8_ꕅ\u001dL鍂\u0003󿶊0Wl1A`LYz\u001fy僸\u001ao\u001b[\u0014\u0008t𐑐a\u0003s~\u001fF𪰤G`$\u000bG\u0011󾿅🙣/󷪺C>\u000f", - "verification_code": "RcplMOQiGa-JY" -} diff --git a/libs/wire-api/test/golden/testObject_Login_user_8.json b/libs/wire-api/test/golden/testObject_Login_user_8.json deleted file mode 100644 index 20658e70ab3..00000000000 --- a/libs/wire-api/test/golden/testObject_Login_user_8.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "email": "@~^G􆪐\\", - "label": null, - "password": "z>􉰃󺎇/𡞯􊳌\u0008%$󽖨𣄄:}\t\u0018􂜙󾺽)㊝󵙼s󵪾\u0018}鱢\u0019[ꅾ\u000bX#VG,df4𢢵8m5딝OTK𣑌鋎꺯◆Z\"ZS\u001bms|[Q%􉲡\u0005W\\󴖙C𭌈+􅕺ဒ䖡v𬁡ꎞ){󻆡𣃒f𭬔}:X-\u00082N\u0019\u001fl🎢쇈Y􅤡󷐛r2.1싸\u0004+𡥙\u0013𣡈]'󻂳s󳤴ꜵ.}𭋣o󲍶X𠜥⬅\r\u001aNq6󸻕'\u000cd\u001e㢧􋾜,:%\t𥅬𒃣QD􉖠\u001b(q4KDQ2zcI\u0010>\u00195󲤼1\u000cBkd\u0013\u0006:F:\u0004𘨥ⶂO N\u001c,N􁚶󴌷[h9ᜬ:xZ=\u000c􈾀\u0013u\u001e\u000ce#\u001a^$lkx耤 \rr\u001aJ󷝦󸓡\u001cR][_5\u0015ⷤ诃5惵􁮵󳗴鉅K!􁁠eRR%絬+h~1󲞮谟lTzS$\u0010􂶳\"*􉕷pmRE\u0013(\u001f^ὯJc➑􅫇i\n+G$|󲫉𦉻g\u001c\u000cgU3Y𝄜\u0006f)􊾺\u0016𓈄􌭞/\u0000Piꩦ{󿸟j􈞅\u001c9𠚽󺊬翉w$눟𞴦)Si𨴄牿FX􂋒j{`궤`󳿗𧁁4u%􅔪P*􂉻捎C\u001eR\u0016-잚󶽕g𐰺:S>c㮢𠝌\u0010Y􄝏~a)YW_J􃢤P\u0007+ U􈷓j\u0019k\u0001􋴘\u0011䣷e𪋘𪳠,ᐏg@\u0012\u001dHXl.\u0017𥣁2\u0013mY􁢫\tv?L8L􆍼N𠦽\nb1j󾸸𤋵xfQ=\\\u0005e󳇪󹽶U\u0012p{\u000e􌚌jd^@U󲯝tP.\u0012Y%R`a\r𧍮7}HnUf𠛸m^7:\u0015=챼>l𗑑hwp27𤦾jE\u000cx=!.\u0013]Ar\tw\u0014&\u001ak㒞s󾦄ᆒI𣪗􂼥dsY\u0010𬚢dX.𣭷i]𤹉󻃀\rWS\u001fU􌏬\u001a시􈨂\u0010\u0002N~-\u000e6𮙏􏄲\\O𭍍Jc􀻇􅢮\u0000HSo\u0010-W\u00136𩥑I􄺨)𘗘={𘗔h洹M󹩪FwJQ􏞨ck\u001a\u0018|UV-\u0015\u0001|\u0014;\u000c𦓫𣦃\u0005S\u0015.B\"D𧲿#o*𞹱胜m\u001e􀓪B3Gg;\u0011\\𬆳􌒮\u0005 B^\u000f𥐶$e餴𩠵>fMgC𭮌,o🗨\\?󼛣~/s\u0001?MMc;D18Ne\u0004\u0018)*\u0002\u001d㾌1/\t\u0015 󶫒󷘿z苐Bv􎲋(=<\u000eq􍪬?L᪽􄗻ஜc󳤌<&!􍚌󴆏j~O3USw\u0012\u0003\u0007\u0017+󺀡Ny粰(/Sco\u0002{3\u000fEh\u0016󼆏󹫐气-\u001c.'\u0005X𘉸𤮓Ti3􀩲\"%\u0016\u0008𮀜+\u0004\u0002^􎧯)2bR\u0006\u000fJB[󿊻&O9{w{aV\u0005gZ?3z􄈭8፳𦔖󱴵`􃥔\"PE)uKq|w\u00160\u001b. \u0003𑻠sxW𧉥󴚗m\u00057e)𓁘󶑼:s\u0018Yj⚎㿤\u0006\u001flTu􏄥I.􉙜O#kQ\u001e!g􃔗\u0018Q\u001f𪍃\u0016\u0006|\"M\"P\u001f\u0003@ZPq󸌖gY𤒍=\u0007􂍭l8󾌀3󲻄󹪢CN<𤆤gJ󽡢]𗋔mX~\u0006w3\u0010𫸴8\u00076\u0004}\u0010i\u0013L5󼂐PY^|!Vz\u001b4走!iLa⼻\u0014􂭺𩀜\u001d:󾟿𤢈h\\dLx􉣕\u0019𥚚\u001a𠷫R%ps7𗏀s􆓓fg\nIf􄢿\u0011l\u001a󹮗-n_ឱUY?4d]|c\\[T\u0007jS䦖휆鄐aK󺖖􏩠\u0003\u001cx+", - "verification_code": "RcplMOQiGa-JY" -} diff --git a/libs/wire-api/test/golden/testObject_NewUser_user_9.json b/libs/wire-api/test/golden/testObject_NewUser_user_9.json deleted file mode 100644 index 975b72224b9..00000000000 --- a/libs/wire-api/test/golden/testObject_NewUser_user_9.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "accent_id": -7404, - "assets": [ - { - "key": "3-5-5cd81cc4-c643-4e9c-849c-c596a88c27fd", - "size": "preview", - "type": "image" - }, - { - "key": "3-5-5cd81cc4-c643-4e9c-849c-c596a88c27fd", - "size": "complete", - "type": "image" - }, - { - "key": "3-5-5cd81cc4-c643-4e9c-849c-c596a88c27fd", - "type": "image" - } - ], - "email": "S\u0005X􆷳$\u0002\"􏇫e󷾤惿󻼜L\u0017@P.b", - "email_code": "1YgaHo0=", - "invitation_code": "DhBvokHtVbWSKbWi0_IATMGH3P8DLEOw5YIcYg==", - "label": "𭤐15XwT󲆬: \u0011Z+\ty𗌉\u0001", - "locale": "sn-LY", - "managed_by": "wire", - "name": "V~㛘钟\u0000w􍻃􇅡1𑵼󹄧%㥫]y*𝧑jqM\u0016𒈔/􎨑-*\u001f \u001eA\u000e}ﭛcv [󹦺t󷇵R𬌻Y󽃈6tg\u0016󿅷+\u0010𘚈;\u0006Oj\u0013뷑&aD:\nf󴯋!*", - "password": "𣉙%5T5'䎆᳦\u0005-􃭧𘨛7W@)!$%v{\u000c\n_I6􉱮츜]r􍶔\u0002Gi_L\u0005@tr<讃2Dr䂇\\\u000b8쁽\u0014􅈿e\u0008𮞲𑚜srN蜨旗Qk+赥󳼩O\\c6󼉭X󺩽􆓖VV\\󴀯^􍺔\u0014(P~y\u000f(\nrO󽖎U=$󽩻k󷀘7.\u0015[dn􃊾粷_\u0000󳞑\u000bNVd햲z󻓕pV6\u001e𨭗#/m􄊮w\u0015沐u𣎯\u000fs\u0011𡔱^A𗔌>\u001a#\u0019sC!3#`𧂅q𐅄\\VrnT\u0010\u0016􂹙\u0014\u0002𦍺󵅅\u0012d 󻆃#\u0018𫺦/k㤣X\"I\u000fO,`GU+\u0011\"\n럲n)\u001b􂰕x󸨾􋽯%\u0012\u000fVr\u000c󾾡H`🚇W\u001c\u0015􀛞vii\u001c\u0007\u0005󵙼&d\u001d𣶇󲅊.􊈄j󶈟$=a_s\u0010Q󹇪\u000e\u000c\u0003󸽌B\u0005\u0018L\u0002_ZX\u0015 h_sGj)󿬂|\u0000\u000f\rlUN)\u0006\u0011`8\u000c󸫲󳼍\u0008,A\u0011\tt/0lT􅪡\u0007}\u0016j\u000f\u0007z|\u0005𥕰J,26󹰅\u00039⮫0\u0019w'\u0000O&g\u001fF0󴞭kg\u0002\u0011|Q􀁨\u001aM𠌸󽣾vuPgVp𬆇)/ Public.SendActivationCode -> Handler r () -sendActivationCode Public.SendActivationCode {..} = do - email <- case saUserKey of - Left email -> pure email - Right _ -> throwStd (errorToWai @'E.InvalidPhone) +sendActivationCode ac = do + let email = ac.emailKey customerExtensionCheckBlockedDomains email checkAllowlist email - API.sendActivationCode email saLocale saCall !>> sendActCodeError + API.sendActivationCode email (ac.locale) !>> sendActCodeError -- | If the user presents an email address from a blocked domain, throw an error. -- diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 45b1d03fb37..dad9ecdefe6 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -385,10 +385,6 @@ createUser new = do pure (validateEmail e) - -- Disallow registering a user with a phone number - when (isJust (newUserPhone newUser)) $ - throwE RegisterErrorInvalidPhone - for_ (mkEmailKey <$> email) $ \k -> verifyUniquenessAndCheckBlacklist k !>> identityErrorToRegisterError @@ -790,9 +786,8 @@ sendActivationCode :: ) => Email -> Maybe Locale -> - Bool -> ExceptT SendActivationCodeError (AppT r) () -sendActivationCode email loc _call = do +sendActivationCode email loc = do ek <- either (const . throwE . InvalidRecipient $ mkEmailKey email) @@ -855,7 +850,6 @@ mkActivationKey (ActivateEmail e) = do (pure . mkEmailKey) (validateEmail e) liftIO $ Data.mkActivationKey ek -mkActivationKey (ActivatePhone p) = throwE $ InvalidActivationPhone p ------------------------------------------------------------------------------- -- Password Management diff --git a/services/brig/src/Brig/Schema/Run.hs b/services/brig/src/Brig/Schema/Run.hs index e991a4ebe54..cf9b27a2eb9 100644 --- a/services/brig/src/Brig/Schema/Run.hs +++ b/services/brig/src/Brig/Schema/Run.hs @@ -1,6 +1,6 @@ -- This file is part of the Wire Server implementation. -- --- Copyright (C) 2022 Wire Swiss GmbH +-- 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 @@ -58,6 +58,7 @@ import Brig.Schema.V80_KeyPackageCiphersuite qualified as V80_KeyPackageCiphersu import Brig.Schema.V81_AddFederationRemoteTeams qualified as V81_AddFederationRemoteTeams import Brig.Schema.V82_DropPhoneColumn qualified as V82_DropPhoneColumn import Brig.Schema.V83_AddTextStatus qualified as V83_AddTextStatus +import Brig.Schema.V84_DropTeamInvitationPhone qualified as V84_DropTeamInvitationPhone import Cassandra.MigrateSchema (migrateSchema) import Cassandra.Schema import Control.Exception (finally) @@ -122,7 +123,8 @@ migrations = V80_KeyPackageCiphersuite.migration, V81_AddFederationRemoteTeams.migration, V82_DropPhoneColumn.migration, - V83_AddTextStatus.migration + V83_AddTextStatus.migration, + V84_DropTeamInvitationPhone.migration -- FUTUREWORK: undo V41 (searchable flag); we stopped using it in -- https://github.com/wireapp/wire-server/pull/964 -- diff --git a/services/brig/src/Brig/Schema/V84_DropTeamInvitationPhone.hs b/services/brig/src/Brig/Schema/V84_DropTeamInvitationPhone.hs new file mode 100644 index 00000000000..02158bbb447 --- /dev/null +++ b/services/brig/src/Brig/Schema/V84_DropTeamInvitationPhone.hs @@ -0,0 +1,33 @@ +-- 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 Brig.Schema.V84_DropTeamInvitationPhone + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 84 "Drop phone column from team_invitation table" $ do + schema' + [r| + ALTER TABLE team_invitation DROP phone + |] diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 0bda80a9f68..9151b8ebf80 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -49,6 +49,7 @@ import Data.Qualified (Local) import Data.Range import Data.Text.Lazy qualified as LT import Data.Time.Clock (UTCTime) +import Data.Tuple.Extra import Imports hiding (head) import Network.Wai.Utilities hiding (code, message) import Polysemy @@ -94,7 +95,7 @@ servantAPI :: ) => ServerT TeamsAPI (Handler r) servantAPI = - Named @"send-team-invitation" createInvitationPublicH + Named @"send-team-invitation" createInvitation :<|> Named @"get-team-invitations" listInvitations :<|> Named @"get-team-invitation" getInvitation :<|> Named @"delete-team-invitation" deleteInvitation @@ -115,31 +116,13 @@ getInvitationCode t r = do code <- lift . wrapClient $ DB.lookupInvitationCode t r maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) (pure . FoundInvitationCode) code -createInvitationPublicH :: - ( Member GalleyAPIAccess r, - Member UserKeyStore r, - Member UserSubsystem r, - Member EmailSending r - ) => - UserId -> - TeamId -> - Public.InvitationRequest -> - Handler r (Public.Invitation, Public.InvitationLocation) -createInvitationPublicH uid tid body = do - inv <- createInvitationPublic uid tid body - pure (inv, loc inv) - where - loc :: Invitation -> InvitationLocation - loc inv = - InvitationLocation $ "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' (inInvitation inv) - data CreateInvitationInviter = CreateInvitationInviter { inviterUid :: UserId, inviterEmail :: Email } deriving (Eq, Show) -createInvitationPublic :: +createInvitation :: ( Member GalleyAPIAccess r, Member UserKeyStore r, Member UserSubsystem r, @@ -148,9 +131,9 @@ createInvitationPublic :: UserId -> TeamId -> Public.InvitationRequest -> - Handler r Public.Invitation -createInvitationPublic uid tid body = do - let inviteeRole = fromMaybe defaultRole . irRole $ body + Handler r (Public.Invitation, Public.InvitationLocation) +createInvitation uid tid body = do + let inviteeRole = fromMaybe defaultRole body.role inviter <- do let inviteePerms = Teams.rolePermissions inviteeRole idt <- maybe (throwStd (errorToWai @'E.NoIdentity)) pure =<< lift (fetchUserIdentity uid) @@ -159,14 +142,18 @@ createInvitationPublic uid tid body = do pure $ CreateInvitationInviter uid from let context = - logFunction "Brig.Team.API.createInvitationPublic" + logFunction "Brig.Team.API.createInvitation" . logTeam tid - . logEmail (irInviteeEmail body) + . logEmail body.inviteeEmail - fst + (id &&& loc) . fst <$> logInvitationRequest context (createInvitation' tid Nothing inviteeRole (Just (inviterUid inviter)) (inviterEmail inviter) body) + where + loc :: Invitation -> InvitationLocation + loc inv = + InvitationLocation $ "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' (inInvitation inv) createInvitationViaScim :: ( Member BlockListStore r, @@ -186,11 +173,10 @@ createInvitationViaScim tid newUser@(NewUserScimInvitation _tid uid loc name ema fromEmail = env ^. emailSender invreq = InvitationRequest - { irLocale = loc, - irRole = Nothing, -- (unused, it's in the type for 'createInvitationPublicH') - irInviteeName = Just name, - irInviteeEmail = email, - irInviteePhone = Nothing + { locale = loc, + role = Nothing, -- (unused, it's in the type for 'createInvitationV5') + inviteeName = Just name, + inviteeEmail = email } context = @@ -239,9 +225,9 @@ createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do -- sendActivationCode. Refactor this to a single place -- Validate e-mail - inviteeEmail <- either (const $ throwStd (errorToWai @'E.InvalidEmail)) pure (Email.validateEmail (irInviteeEmail body)) - let uke = mkEmailKey inviteeEmail - blacklistedEm <- lift $ liftSem $ isBlocked inviteeEmail + validatedEmail <- either (const $ throwStd (errorToWai @'E.InvalidEmail)) pure (Email.validateEmail (inviteeEmail body)) + let uke = mkEmailKey validatedEmail + blacklistedEm <- lift $ liftSem $ isBlocked validatedEmail when blacklistedEm $ throwStd blacklistedEmail emailTaken <- lift $ liftSem $ isJust <$> lookupKey uke @@ -253,8 +239,6 @@ createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do when (fromIntegral pending >= maxSize) $ throwStd (errorToWai @'E.TooManyTeamInvitations) - let locale = irLocale body - let inviteeName = irInviteeName body showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid lift $ do @@ -270,11 +254,10 @@ createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do inviteeRole now mbInviterUid - inviteeEmail - inviteeName - Nothing -- ignore phone + validatedEmail + body.inviteeName timeout - (newInv, code) <$ sendInvitationMail inviteeEmail tid fromEmail code locale + (newInv, code) <$ sendInvitationMail validatedEmail tid fromEmail code body.locale deleteInvitation :: (Member GalleyAPIAccess r) => UserId -> TeamId -> InvitationId -> (Handler r) () deleteInvitation uid tid iid = do diff --git a/services/brig/src/Brig/Team/DB.hs b/services/brig/src/Brig/Team/DB.hs index a31875142c1..ed5898c59a5 100644 --- a/services/brig/src/Brig/Team/DB.hs +++ b/services/brig/src/Brig/Team/DB.hs @@ -95,26 +95,25 @@ insertInvitation :: Maybe UserId -> Email -> Maybe Name -> - Maybe Phone -> -- | The timeout for the invitation code. Timeout -> m (Invitation, InvitationCode) -insertInvitation showUrl iid t role (toUTCTimeMillis -> now) minviter email inviteeName phone timeout = do +insertInvitation showUrl iid t role (toUTCTimeMillis -> now) minviter email inviteeName timeout = do code <- liftIO mkInvitationCode url <- mkInviteUrl showUrl t code - let inv = Invitation t role iid now minviter email inviteeName phone url + let inv = Invitation t role iid now minviter email inviteeName url retry x5 . batch $ do setType BatchLogged setConsistency LocalQuorum - addPrepQuery cqlInvitation (t, role, iid, code, email, now, minviter, inviteeName, phone, round timeout) + addPrepQuery cqlInvitation (t, role, iid, code, email, now, minviter, inviteeName, round timeout) addPrepQuery cqlInvitationInfo (code, t, iid, round timeout) addPrepQuery cqlInvitationByEmail (email, t, iid, code, round timeout) pure (inv, code) where cqlInvitationInfo :: PrepQuery W (InvitationCode, TeamId, InvitationId, Int32) () cqlInvitationInfo = "INSERT INTO team_invitation_info (code, team, id) VALUES (?, ?, ?) USING TTL ?" - cqlInvitation :: PrepQuery W (TeamId, Role, InvitationId, InvitationCode, Email, UTCTimeMillis, Maybe UserId, Maybe Name, Maybe Phone, Int32) () - cqlInvitation = "INSERT INTO team_invitation (team, role, id, code, email, created_at, created_by, name, phone) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) USING TTL ?" + cqlInvitation :: PrepQuery W (TeamId, Role, InvitationId, InvitationCode, Email, UTCTimeMillis, Maybe UserId, Maybe Name, Int32) () + cqlInvitation = "INSERT INTO team_invitation (team, role, id, code, email, created_at, created_by, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?) USING TTL ?" -- Note: the edge case of multiple invites to the same team by different admins from the same team results in last-invite-wins in the team_invitation_email table. cqlInvitationByEmail :: PrepQuery W (Email, TeamId, InvitationId, InvitationCode, Int32) () cqlInvitationByEmail = "INSERT INTO team_invitation_email (email, team, invitation, code) VALUES (?, ?, ?, ?) USING TTL ?" @@ -132,8 +131,8 @@ lookupInvitation showUrl t r = do inv <- retry x1 (query1 cqlInvitation (params LocalQuorum (t, r))) traverse (toInvitation showUrl) inv where - cqlInvitation :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone, InvitationCode) - cqlInvitation = "SELECT team, role, id, created_at, created_by, email, name, phone, code FROM team_invitation WHERE team = ? AND id = ?" + cqlInvitation :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, InvitationCode) + cqlInvitation = "SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id = ?" lookupInvitationByCode :: ( Log.MonadLogger m, @@ -185,10 +184,10 @@ lookupInvitations showUrl team start (fromRange -> size) = do { result = invs, hasMore = more } - cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone, InvitationCode) - cqlSelect = "SELECT team, role, id, created_at, created_by, email, name, phone, code FROM team_invitation WHERE team = ? ORDER BY id ASC" - cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone, InvitationCode) - cqlSelectFrom = "SELECT team, role, id, created_at, created_by, email, name, phone, code FROM team_invitation WHERE team = ? AND id > ? ORDER BY id ASC" + cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, InvitationCode) + cqlSelect = "SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? ORDER BY id ASC" + cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, InvitationCode) + cqlSelectFrom = "SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id > ? ORDER BY id ASC" deleteInvitation :: (MonadClient m) => TeamId -> InvitationId -> m () deleteInvitation t i = do @@ -284,13 +283,12 @@ toInvitation :: Maybe UserId, Email, Maybe Name, - Maybe Phone, InvitationCode ) -> m Invitation -toInvitation showUrl (t, r, i, tm, minviter, e, inviteeName, p, code) = do +toInvitation showUrl (t, r, i, tm, minviter, e, inviteeName, code) = do url <- mkInviteUrl showUrl t code - pure $ Invitation t (fromMaybe defaultRole r) i tm minviter e inviteeName p url + pure $ Invitation t (fromMaybe defaultRole r) i tm minviter e inviteeName url mkInviteUrl :: ( MonadReader Env m, diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index ba4f765436a..8f5fbe0392e 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -102,7 +102,7 @@ login :: Login -> CookieType -> ExceptT LoginError (AppT r) (Access ZAuth.User) -login (PasswordLogin (PasswordLoginData li pw label code)) typ = do +login (MkLogin li pw label code) typ = do uid <- resolveLoginId li lift . liftSem . Log.debug $ field "user" (toByteString uid) . field "action" (val "User.login") wrapHttpClientE $ checkRetryLimit uid @@ -122,9 +122,6 @@ login (PasswordLogin (PasswordLoginData li pw label code)) typ = do VerificationCodeNoPendingCode -> wrapHttpClientE $ loginFailedWith LoginCodeInvalid uid VerificationCodeRequired -> wrapHttpClientE $ loginFailedWith LoginCodeRequired uid VerificationCodeNoEmail -> wrapHttpClientE $ loginFailed uid -login (SmsLogin _) _ = do - -- sms login not supported - throwE LoginFailed verifyCode :: forall r. @@ -302,9 +299,6 @@ validateLoginId (LoginByEmail email) = (const $ throwE LoginFailed) (pure . Left . mkEmailKey) (validateEmail email) -validateLoginId (LoginByPhone _) = do - -- phone logins are not supported - throwE LoginFailed validateLoginId (LoginByHandle h) = pure (Right h) @@ -312,7 +306,6 @@ isPendingActivation :: (MonadClient m, MonadReader Env m) => LoginId -> m Bool isPendingActivation ident = case ident of (LoginByHandle _) -> pure False (LoginByEmail e) -> checkKey (mkEmailKey e) - (LoginByPhone _) -> pure False where checkKey k = do usr <- (>>= fst) <$> Data.lookupActivationCode k diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 772ad7f0d1a..3e829d00d25 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -214,9 +214,8 @@ testInvitationEmail brig = do assertInvitationResponseInvariants :: InvitationRequest -> Invitation -> Assertion assertInvitationResponseInvariants invReq inv = do - irInviteeName invReq @=? inInviteeName inv - irInviteePhone invReq @=? inInviteePhone inv - irInviteeEmail invReq @=? inInviteeEmail inv + inviteeName invReq @=? inInviteeName inv + inviteeEmail invReq @=? inInviteeEmail inv testGetInvitation :: Brig -> Http () testGetInvitation brig = do @@ -430,19 +429,19 @@ testInvitationRoles brig galley = do testInvitationEmailAccepted :: Brig -> Galley -> Http () testInvitationEmailAccepted brig galley = do - inviteeEmail <- randomEmail - let invite = stdInvitationRequest inviteeEmail - void $ createAndVerifyInvitation (accept (irInviteeEmail invite)) invite brig galley + email <- randomEmail + let invite = stdInvitationRequest email + void $ createAndVerifyInvitation (accept invite.inviteeEmail) invite brig galley -- | Related: 'testDomainsBlockedForRegistration'. When we remove the customer-specific -- extension of domain blocking, this test will fail to compile (so you will know it's time to -- remove it). testInvitationEmailAcceptedInBlockedDomain :: Opt.Opts -> Brig -> Galley -> Http () testInvitationEmailAcceptedInBlockedDomain opts brig galley = do - inviteeEmail :: Email <- randomEmail - let invite = stdInvitationRequest inviteeEmail - replacementBrigApp = withDomainsBlockedForRegistration opts [emailDomain inviteeEmail] - void $ createAndVerifyInvitation' (Just replacementBrigApp) (accept (irInviteeEmail invite)) invite brig galley + email :: Email <- randomEmail + let invite = stdInvitationRequest email + replacementBrigApp = withDomainsBlockedForRegistration opts [emailDomain email] + void $ createAndVerifyInvitation' (Just replacementBrigApp) (accept invite.inviteeEmail) invite brig galley -- | FUTUREWORK: this is an alternative helper to 'createPopulatedBindingTeam'. it has been -- added concurrently, and the two should probably be consolidated. diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 6f78f951fe8..331309043cf 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 -> Email -> InvitationRequest stdInvitationRequest' loc role email = - InvitationRequest loc role Nothing email Nothing + InvitationRequest loc role Nothing email setTeamTeamSearchVisibilityAvailable :: (HasCallStack, MonadHttp m, MonadIO m, MonadCatch m) => Galley -> TeamId -> FeatureStatus -> m () setTeamTeamSearchVisibilityAvailable galley tid status = diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index e36436bf1d0..5cd1538a79d 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -100,17 +100,14 @@ tests _ at opts p b c ch g aws userJournalWatcher = testGroup "account" [ test p "post /register - 201 (with preverified)" $ testCreateUserWithPreverified opts b userJournalWatcher, - test p "testCreateUserWithInvalidVerificationCode - post /register - 400 (with preverified)" $ testCreateUserWithInvalidVerificationCode b, test p "post /register - 201" $ testCreateUser b g, - test p "post /register - 400 + no email" $ testCreateUserNoEmailNoPassword b, test p "post /register - 201 anonymous" $ testCreateUserAnon b g, test p "testCreateUserEmptyName - post /register - 400 empty name" $ testCreateUserEmptyName b, test p "testCreateUserLongName - post /register - 400 name too long" $ testCreateUserLongName b, test p "post /register - 201 anonymous expiry" $ testCreateUserAnonExpiry b, test p "post /register - 201 pending" $ testCreateUserPending opts b, - test p "post /register - 201 existing activation" $ testCreateAccountPendingActivationKey opts b, test p "testCreateUserConflict - post /register - 409 conflict" $ testCreateUserConflict opts b, - test p "testCreateUserInvalidEmailOrPhone - post /register - 400 invalid input" $ testCreateUserInvalidEmailOrPhone opts b, + test p "testCreateUserInvalidEmail - post /register - 400 invalid input" $ testCreateUserInvalidEmail opts b, test p "post /register - 403 blacklist" $ testCreateUserBlacklist opts b aws, test p "post /register - 400 external-SSO" $ testCreateUserExternalSSO b, test p "post /register - 403 restricted user creation" $ testRestrictedUserCreation opts b, @@ -130,7 +127,6 @@ tests _ at opts p b c ch g aws userJournalWatcher = test p "post /list-users - 200" $ testMultipleUsers opts b, test p "put /self - 200" $ testUserUpdate b c userJournalWatcher, test p "put /access/self/email - 2xx" $ testEmailUpdate b userJournalWatcher, - test p "put /self/phone - 400" $ testPhoneUpdate b, test p "head /self/password - 200/404" $ testPasswordSet b, test p "put /self/password - 400" $ testPasswordSetInvalidPasswordLength b, test p "put /self/password - 200" $ testPasswordChange b, @@ -164,34 +160,6 @@ tests _ at opts p b c ch g aws userJournalWatcher = ] ] --- The testCreateUserWithInvalidVerificationCode test conforms to the following testing standards: --- --- Registering with an invalid verification code and valid account details should fail. -testCreateUserWithInvalidVerificationCode :: Brig -> Http () -testCreateUserWithInvalidVerificationCode brig = do - -- Attempt to register (pre verified) user with phone - p <- randomPhone - code <- randomActivationCode -- incorrect but syntactically valid activation code - let Object regPhone = - object - [ "name" .= Name "Alice", - "phone" .= fromPhone p, - "phone_code" .= code - ] - postUserRegister' regPhone brig !!! do - const 400 === statusCode - const (Just "invalid-phone") === fmap Wai.label . responseJsonMaybe - - -- Attempt to register (pre verified) user with email - e <- randomEmail - let Object regEmail = - object - [ "name" .= Name "Alice", - "email" .= fromEmail e, - "email_code" .= code - ] - postUserRegister' regEmail brig !!! const 404 === statusCode - testUpdateUserEmailByTeamOwner :: Opt.Opts -> Brig -> Http () testUpdateUserEmailByTeamOwner opts brig = do (_, teamOwner, emailOwner : otherTeamMember : _) <- createPopulatedBindingTeamWithNamesAndHandles brig 2 @@ -239,14 +207,6 @@ testUpdateUserEmailByTeamOwner opts brig = do testCreateUserWithPreverified :: Opt.Opts -> Brig -> UserJournalWatcher -> Http () testCreateUserWithPreverified opts brig userJournalWatcher = do - -- Register (pre verified) user with phone - p <- randomPhone - let phoneReq = RequestBodyLBS . encode $ object ["phone" .= fromPhone p] - post (brig . path "/activate/send" . contentJson . body phoneReq) - !!! do - const 400 === statusCode - const (Just "invalid-phone") === fmap Wai.label . responseJsonMaybe - -- Register (pre verified) user with email e <- randomEmail let emailReq = RequestBodyLBS . encode $ object ["email" .= fromEmail e] @@ -390,21 +350,6 @@ testCreateUserPending _ brig = do Search.refreshIndex brig Search.assertCan'tFind brig suid quid "Mr. Pink" -testCreateUserNoEmailNoPassword :: Brig -> Http () -testCreateUserNoEmailNoPassword brig = do - p <- randomPhone - let newUser = - RequestBodyLBS . encode $ - object - [ "name" .= ("Alice" :: Text), - "phone" .= fromPhone p - ] - post - (brig . path "/i/users" . contentJson . body newUser) - !!! do - const 400 === statusCode - (const (Just "invalid-phone") === fmap Error.label . responseJsonMaybe) - -- The testCreateUserConflict test conforms to the following testing standards: -- -- email address must not be taken on @/register@. @@ -437,12 +382,12 @@ testCreateUserConflict _ brig = do const 409 === statusCode const (Just "key-exists") === fmap Error.label . responseJsonMaybe --- The testCreateUserInvalidEmailOrPhone test conforms to the following testing standards: +-- The testCreateUserInvalidEmail test conforms to the following testing standards: -- -- Test to make sure a new user cannot be created with an invalid email address or invalid phone number. -testCreateUserInvalidEmailOrPhone :: Opt.Opts -> Brig -> Http () -testCreateUserInvalidEmailOrPhone (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () -testCreateUserInvalidEmailOrPhone _ brig = do +testCreateUserInvalidEmail :: Opt.Opts -> Brig -> Http () +testCreateUserInvalidEmail (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () +testCreateUserInvalidEmail _ brig = do email <- randomEmail let reqEmail = RequestBodyLBS . encode $ @@ -450,10 +395,10 @@ testCreateUserInvalidEmailOrPhone _ brig = do [ "name" .= ("foo" :: Text), "email" .= fromEmail email, "password" .= defPassword, - "phone" .= ("123456" :: Text) -- invalid phone nr + "phone" .= ("123456" :: Text) -- invalid phone number, but ignored ] post (brig . path "/register" . contentJson . body reqEmail) - !!! const 400 === statusCode + !!! const 201 === statusCode phone <- randomPhone let reqPhone = @@ -962,27 +907,6 @@ testEmailUpdate brig userJournalWatcher = do login brig (defEmailLogin eml) SessionCookie !!! const 200 === statusCode login brig (defEmailLogin (Email "test" "example.com")) SessionCookie !!! const 200 === statusCode -testPhoneUpdate :: Brig -> Http () -testPhoneUpdate brig = do - uid <- userId <$> randomUser brig - phn <- randomPhone - updatePhone brig uid phn - -- check new phone - get (brig . path "/self" . zUser uid) !!! do - const 200 === statusCode - -testCreateAccountPendingActivationKey :: Opt.Opts -> Brig -> Http () -testCreateAccountPendingActivationKey (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () -testCreateAccountPendingActivationKey _ brig = do - uid <- userId <$> randomUser brig - phn <- randomPhone - -- update phone - let phoneUpdate = RequestBodyLBS . encode $ PhoneUpdate phn - put (brig . path "/self/phone" . contentJson . zUser uid . zConn "c" . body phoneUpdate) - !!! do - const 400 === statusCode - const (Just "invalid-phone") === fmap Error.label . responseJsonMaybe - testUserLocaleUpdate :: Brig -> UserJournalWatcher -> Http () testUserLocaleUpdate brig userJournalWatcher = do usr <- randomUser brig @@ -1112,7 +1036,7 @@ testPasswordChange brig = do -- login with new password login brig - (PasswordLogin (PasswordLoginData (LoginByEmail email) newPass Nothing Nothing)) + (MkLogin (LoginByEmail email) newPass Nothing Nothing) PersistentCookie !!! const 200 === statusCode -- try to change the password to itself should fail @@ -1312,11 +1236,9 @@ testUpdateSSOId brig galley = do assertEqual "updateSSOId/ssoid" ssoid ssoid' assertEqual "updateSSOId/email" (userEmail user) mEmail (owner, teamid) <- createUserWithTeam brig - let mkMember :: Bool -> Bool -> Http User - mkMember hasEmail hasPhone = do + let mkMember :: Bool -> Http User + mkMember hasEmail = do member <- createTeamMember brig galley owner teamid noPermissions - when hasPhone $ do - updatePhone brig (userId member) =<< randomPhone unless hasEmail $ do error "not implemented" selfUser <$> (responseJsonError =<< get (brig . path "/self" . zUser (userId member))) @@ -1324,7 +1246,7 @@ testUpdateSSOId brig galley = do ssoids2 = [UserSSOId (mkSampleUref "2" "1"), UserSSOId (mkSampleUref "2" "2")] users <- sequence - [ mkMember True False + [ mkMember True -- the following two could be implemented by creating the user implicitly via SSO login. -- , mkMember False False ] @@ -1345,7 +1267,7 @@ testDomainsBlockedForRegistration opts brig = withDomainsBlockedForRegistration post (brig . path "/activate/send" . contentJson . body (p goodEmail)) !!! do const 200 === statusCode where - p email = RequestBodyLBS . encode $ SendActivationCode (Left email) Nothing False + p email = RequestBodyLBS . encode $ SendActivationCode email Nothing -- | FUTUREWORK: @setRestrictUserCreation@ perhaps needs to be tested in one place only, since it's the -- first thing that we check on the /register endpoint. Other tests that make use of @setRestrictUserCreation@ diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 966481ef84d..a2e34731214 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -94,10 +94,8 @@ tests conf m z db b g n = [ testGroup "login" [ test m "email" (testEmailLogin b), - test m "phone" (testPhoneLogin b), test m "handle" (testHandleLogin b), test m "email-untrusted-domain" (testLoginUntrustedDomain b), - test m "send-phone-code" (testSendLoginCode b), test m "testLoginFailure - failure" (testLoginFailure b), test m "throttle" (testThrottleLogins conf b), test m "testLimitRetries - limit-retry" (testLimitRetries conf b), @@ -181,7 +179,7 @@ testLoginWith6CharPassword brig db = do checkLogin email pw expectedStatusCode = login brig - (PasswordLogin (PasswordLoginData (LoginByEmail email) pw Nothing Nothing)) + (MkLogin (LoginByEmail email) pw Nothing Nothing) PersistentCookie !!! const expectedStatusCode === statusCode @@ -357,21 +355,6 @@ testEmailLogin brig = do login brig (defEmailLogin email') PersistentCookie !!! const 200 === statusCode -testPhoneLogin :: Brig -> Http () -testPhoneLogin brig = do - p <- randomPhone - let newUser = - RequestBodyLBS . encode $ - object - [ "name" .= ("Alice" :: Text), - "phone" .= fromPhone p - ] - -- phone logins are not supported anymore - post (brig . path "/i/users" . contentJson . Http.body newUser) - !!! do - const 400 === statusCode - const (Just "invalid-phone") === errorLabel - testHandleLogin :: Brig -> Http () testHandleLogin brig = do usr <- Public.userId <$> randomUser brig @@ -379,7 +362,7 @@ testHandleLogin brig = do let update = RequestBodyLBS . encode $ HandleUpdate hdl put (brig . path "/self/handle" . contentJson . zUser usr . zConn "c" . Http.body update) !!! const 200 === statusCode - let l = PasswordLogin (PasswordLoginData (LoginByHandle (fromJust $ parseHandle hdl)) defPassword Nothing Nothing) + let l = MkLogin (LoginByHandle (fromJust $ parseHandle hdl)) defPassword Nothing Nothing login brig l PersistentCookie !!! const 200 === statusCode -- | Check that local part after @+@ is ignored by equality on email addresses if the domain is @@ -392,21 +375,6 @@ testLoginUntrustedDomain brig = do login brig (defEmailLogin email') PersistentCookie !!! const 200 === statusCode -testSendLoginCode :: Brig -> Http () -testSendLoginCode brig = do - p <- randomPhone - let newUser = - RequestBodyLBS . encode $ - object - [ "name" .= ("Alice" :: Text), - "phone" .= fromPhone p, - "password" .= ("topsecretdefaultpassword" :: Text) - ] - post (brig . path "/i/users" . contentJson . Http.body newUser) - !!! do - const 400 === statusCode - const (Just "invalid-phone") === errorLabel - -- The testLoginFailure test conforms to the following testing standards: -- -- Test that trying to log in with a wrong password or non-existent email fails. @@ -417,15 +385,14 @@ testLoginFailure brig = do let badpw = plainTextPassword6Unsafe "wrongpassword" login brig - (PasswordLogin (PasswordLoginData (LoginByEmail email) badpw Nothing Nothing)) + (MkLogin (LoginByEmail email) badpw Nothing Nothing) PersistentCookie !!! const 403 === statusCode -- login with wrong / non-existent email let badmail = Email "wrong" "wire.com" login brig - ( PasswordLogin - (PasswordLoginData (LoginByEmail badmail) defPassword Nothing Nothing) + ( MkLogin (LoginByEmail badmail) defPassword Nothing Nothing ) PersistentCookie !!! const 403 === statusCode diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index fef7075b728..40d1569f660 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -156,8 +156,7 @@ testAddGetClientVerificationCode db brig galley = do let k = mkKey email codeValue <- Code.codeValue <$$> lookupCode db k Code.AccountLogin checkLoginSucceeds $ - PasswordLogin $ - PasswordLoginData (LoginByEmail email) defPassword (Just defCookieLabel) codeValue + MkLogin (LoginByEmail email) defPassword (Just defCookieLabel) codeValue c <- addClient' codeValue getClient brig uid (clientId c) !!! do const 200 === statusCode @@ -212,8 +211,7 @@ testAddGetClientCodeExpired db opts brig galley = do let k = mkKey email codeValue <- (.codeValue) <$$> lookupCode db k Code.AccountLogin checkLoginSucceeds $ - PasswordLogin $ - PasswordLoginData (LoginByEmail email) defPassword (Just defCookieLabel) codeValue + MkLogin (LoginByEmail email) defPassword (Just defCookieLabel) codeValue let verificationTimeout = round (Opt.setVerificationTimeout (Opt.optSettings opts)) threadDelay $ ((verificationTimeout + 1) * 1000_000) addClient' codeValue !!! do diff --git a/services/brig/test/integration/API/User/PasswordReset.hs b/services/brig/test/integration/API/User/PasswordReset.hs index b478af41749..857bb6c48a2 100644 --- a/services/brig/test/integration/API/User/PasswordReset.hs +++ b/services/brig/test/integration/API/User/PasswordReset.hs @@ -75,7 +75,7 @@ testPasswordReset brig = do !!! const 403 === statusCode login brig - (PasswordLogin (PasswordLoginData (LoginByEmail email) (plainTextPassword8To6 newpw) Nothing Nothing)) + (MkLogin (LoginByEmail email) (plainTextPassword8To6 newpw) Nothing Nothing) PersistentCookie !!! const 200 === statusCode -- reset password again to the same new password, get 400 "must be different" diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 6ce6f9ece74..5445c497c7e 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -84,7 +84,6 @@ import Network.Wai qualified as Wai import Network.Wai.Handler.Warp qualified as Warp import Network.Wai.Test (Session) import Network.Wai.Test qualified as WaiTest -import Network.Wai.Utilities.Error qualified as Wai import OpenSSL.BN (randIntegerZeroToNMinusOne) import Servant.Client (ClientError (FailureResponse)) import Servant.Client qualified as Servant @@ -839,19 +838,11 @@ randomActivationCode = . printf "%06d" <$> randIntegerZeroToNMinusOne 1000000 -updatePhone :: (HasCallStack) => Brig -> UserId -> Phone -> Http () -updatePhone brig uid phn = do - -- update phone - let phoneUpdate = RequestBodyLBS . encode $ PhoneUpdate phn - put (brig . path "/self/phone" . contentJson . zUser uid . zConn "c" . body phoneUpdate) !!! do - const 400 === statusCode - const (Just "invalid-phone") === fmap Wai.label . responseJsonMaybe - defEmailLogin :: Email -> Login defEmailLogin e = emailLogin e defPassword (Just defCookieLabel) emailLogin :: Email -> PlainTextPassword6 -> Maybe CookieLabel -> Login -emailLogin e pw cl = PasswordLogin (PasswordLoginData (LoginByEmail e) pw cl Nothing) +emailLogin e pw cl = MkLogin (LoginByEmail e) pw cl Nothing somePrekeys :: [Prekey] somePrekeys = diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 6d7df5a1e23..8e6d49e3d21 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -440,7 +440,7 @@ addUserToTeamWithRole' :: (HasCallStack) => Maybe Role -> UserId -> TeamId -> Te addUserToTeamWithRole' role inviter tid = do brig <- viewBrig inviteeEmail <- randomEmail - let invite = InvitationRequest Nothing role Nothing inviteeEmail Nothing + let invite = InvitationRequest Nothing role Nothing inviteeEmail invResponse <- postInvitation tid inviter invite inv <- responseJsonError invResponse inviteeCode <- getInvitationCode tid (inInvitation inv) diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index a9a29c3445f..2fa022b1b4f 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -1252,7 +1252,7 @@ stdInvitationRequest = stdInvitationRequest' Nothing Nothing -- | copied from brig integration tests stdInvitationRequest' :: Maybe User.Locale -> Maybe Role -> User.Email -> TeamInvitation.InvitationRequest stdInvitationRequest' loc role email = - TeamInvitation.InvitationRequest loc role Nothing email Nothing + TeamInvitation.InvitationRequest loc role Nothing email setRandomHandleBrig :: (HasCallStack) => UserId -> TestSpar () setRandomHandleBrig uid = do diff --git a/services/spar/test-integration/Util/Email.hs b/services/spar/test-integration/Util/Email.hs index 74c564ad2bc..8fe7c002872 100644 --- a/services/spar/test-integration/Util/Email.hs +++ b/services/spar/test-integration/Util/Email.hs @@ -60,8 +60,7 @@ changeEmailBrig brig usr newEmail = do where emailLogin :: Email -> Misc.PlainTextPassword6 -> Maybe Auth.CookieLabel -> Auth.Login emailLogin e pw cl = - Auth.PasswordLogin $ - Auth.PasswordLoginData (Auth.LoginByEmail e) pw cl Nothing + Auth.MkLogin (Auth.LoginByEmail e) pw cl Nothing login :: Auth.Login -> Auth.CookieType -> (MonadHttp m) => m ResponseLBS login l t = diff --git a/tools/stern/test/integration/Util.hs b/tools/stern/test/integration/Util.hs index 2c26c513d13..4fe29bb75a5 100644 --- a/tools/stern/test/integration/Util.hs +++ b/tools/stern/test/integration/Util.hs @@ -149,8 +149,8 @@ addUserToTeamWithRole role inviter tid = do addUserToTeamWithRole' :: (HasCallStack) => Maybe Role -> UserId -> TeamId -> TestM (Invitation, ResponseLBS) addUserToTeamWithRole' role inviter tid = do brig <- view tsBrig - inviteeEmail <- randomEmail - let invite = InvitationRequest Nothing role Nothing inviteeEmail Nothing + email <- randomEmail + let invite = InvitationRequest Nothing role Nothing email invResponse <- postInvitation tid inviter invite inv <- responseJsonError invResponse inviteeCode <- getInvitationCode tid (inInvitation inv) @@ -159,7 +159,7 @@ addUserToTeamWithRole' role inviter tid = do ( brig . path "/register" . contentJson - . body (acceptInviteBody inviteeEmail inviteeCode) + . body (acceptInviteBody email inviteeCode) ) pure (inv, r) From 34bfff602a58eb661c268eeaec72ddf42a95a436 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:03:05 +0200 Subject: [PATCH 021/136] [chore] Weed out dead code (part 2) (#4173) * WIP: trying to weed out some dead code. * Weeding out more. * More weeding. * Lint. * Weed+. * Updated cassandra schema cql. * More weeding * Weed the Second. * Weed+ * Restoring. * More cleaning. * Ignored more test and util code. * Fixed golden test. * Restore more. * Fix cql --- cassandra-schema.cql | 19 ---- integration/test/API/Galley.hs | 7 -- integration/test/Testlib/Assertions.hs | 15 --- libs/extended/src/Data/Time/Clock/DiffTime.hs | 2 +- libs/galley-types/src/Galley/Types/Teams.hs | 1 - libs/hscim/src/Web/Scim/Client.hs | 10 -- libs/imports/src/Imports.hs | 24 ----- .../src/Wire/API/Team/Conversation.hs | 7 +- libs/wire-api/src/Wire/API/Team/Feature.hs | 9 -- libs/wire-api/src/Wire/API/Team/Member.hs | 4 - libs/wire-api/src/Wire/API/User.hs | 6 -- libs/wire-api/src/Wire/API/User/Auth.hs | 4 - libs/wire-api/src/Wire/API/User/Client.hs | 4 - libs/wire-api/src/Wire/API/User/Identity.hs | 4 - .../src/Wire/API/User/IdentityProvider.hs | 6 +- libs/wire-api/src/Wire/API/User/Saml.hs | 3 - libs/wire-api/src/Wire/API/User/Scim.hs | 7 -- libs/wire-api/src/Wire/API/User/Search.hs | 5 +- libs/wire-api/src/Wire/API/UserEvent.hs | 30 ------ .../golden/Test/Wire/API/Golden/Generated.hs | 25 +++++ .../testObject_FeatureStatus_team_1.json | 1 + .../testObject_FeatureStatus_team_10.json | 1 + .../testObject_FeatureStatus_team_11.json | 1 + .../testObject_FeatureStatus_team_12.json | 1 + .../testObject_FeatureStatus_team_13.json | 1 + .../testObject_FeatureStatus_team_14.json | 1 + .../testObject_FeatureStatus_team_15.json | 1 + .../testObject_FeatureStatus_team_16.json | 1 + .../testObject_FeatureStatus_team_17.json | 1 + .../testObject_FeatureStatus_team_18.json | 1 + .../testObject_FeatureStatus_team_19.json | 1 + .../testObject_FeatureStatus_team_2.json | 1 + .../testObject_FeatureStatus_team_20.json | 1 + .../testObject_FeatureStatus_team_3.json | 1 + .../testObject_FeatureStatus_team_4.json | 1 + .../testObject_FeatureStatus_team_5.json | 1 + .../testObject_FeatureStatus_team_6.json | 1 + .../testObject_FeatureStatus_team_7.json | 1 + .../testObject_FeatureStatus_team_8.json | 1 + .../testObject_FeatureStatus_team_9.json | 1 + .../test/unit/Test/Wire/API/Password.hs | 2 + .../src/Wire/NotificationSubsystem.hs | 6 +- .../Wire/NotificationSubsystem/Interpreter.hs | 4 +- .../wire-subsystems/src/Wire/UserSubsystem.hs | 3 - libs/zauth/src/Data/ZAuth/Creation.hs | 10 +- .../background-worker/test/Test/Wire/Util.hs | 16 ---- services/brig/brig.cabal | 3 +- services/brig/src/Brig/API/User.hs | 12 --- services/brig/src/Brig/API/Util.hs | 68 -------------- services/brig/src/Brig/App.hs | 8 +- .../brig/src/Brig/CanonicalInterpreter.hs | 2 +- services/brig/src/Brig/Data/Client.hs | 7 -- services/brig/src/Brig/Data/Connection.hs | 28 +----- services/brig/src/Brig/Data/LoginCode.hs | 93 ------------------- services/brig/src/Brig/Options.hs | 5 - services/brig/src/Brig/Provider/Email.hs | 55 ----------- services/brig/src/Brig/Queue.hs | 49 ---------- services/brig/src/Brig/Schema/Run.hs | 6 +- ...UTUREWORK.hs => V85_DropUserKeysHashed.hs} | 8 +- services/brig/src/Brig/Team/Email.hs | 30 ------ services/brig/src/Brig/User/Auth/Cookie.hs | 12 --- services/brig/src/Brig/ZAuth.hs | 4 - .../brig/test/integration/API/User/Util.hs | 19 ---- .../brig/test/integration/Federation/Util.hs | 29 ------ services/cannon/src/Cannon/Run.hs | 4 +- services/cannon/src/Cannon/Types.hs | 24 +---- services/cargohold/src/CargoHold/API/Error.hs | 28 ------ services/cargohold/src/CargoHold/AWS.hs | 11 --- services/cargohold/src/CargoHold/App.hs | 5 - services/federator/src/Federator/Error.hs | 7 -- .../federator/src/Federator/Validation.hs | 8 -- .../test/integration/Test/Federator/Util.hs | 11 +-- services/galley/src/Galley/API/Action.hs | 23 ----- services/galley/src/Galley/API/Error.hs | 6 -- services/galley/src/Galley/API/Federation.hs | 2 +- services/galley/src/Galley/API/MLS/Types.hs | 6 -- services/galley/src/Galley/API/Push.hs | 2 +- services/galley/src/Galley/API/Util.hs | 19 ---- .../galley/src/Galley/Cassandra/Access.hs | 3 - .../galley/src/Galley/Cassandra/Queries.hs | 9 -- .../galley/src/Galley/Data/Conversation.hs | 4 - .../src/Galley/Effects/FederatorAccess.hs | 8 -- services/galley/src/Galley/Intra/Util.hs | 14 --- services/galley/src/Galley/Types/Clients.hs | 53 ----------- .../galley/test/integration/API/MLS/Util.hs | 37 -------- services/galley/test/integration/API/Util.hs | 53 ----------- .../galley/test/integration/Federation.hs | 23 ----- services/gundeck/src/Gundeck/Aws.hs | 21 ----- services/gundeck/src/Gundeck/Env.hs | 3 - .../gundeck/src/Gundeck/Push/Native/Types.hs | 8 +- services/gundeck/src/Gundeck/Run.hs | 3 +- services/gundeck/src/Gundeck/Util.hs | 5 - services/gundeck/src/Gundeck/Util/Redis.hs | 3 - services/proxy/src/Proxy/Options.hs | 18 +--- services/spar/default.nix | 1 - .../src/Spar/DataMigration/RIO.hs | 38 -------- .../src/Spar/DataMigration/V2_UserV2.hs | 4 - services/spar/spar.cabal | 2 - services/spar/src/Spar/API.hs | 2 +- services/spar/src/Spar/Data.hs | 4 - services/spar/src/Spar/Intra/BrigApp.hs | 1 - .../test-integration/Test/Spar/APISpec.hs | 6 +- services/spar/test-integration/Util/Core.hs | 44 +-------- services/spar/test-integration/Util/Email.hs | 43 +-------- services/spar/test-integration/Util/Scim.hs | 3 - tools/db/inconsistencies/src/Options.hs | 10 -- tools/db/move-team/src/Schema.hs | 34 ------- tools/stern/default.nix | 1 - tools/stern/src/Stern/App.hs | 30 +----- tools/stern/src/Stern/Intra.hs | 19 ---- tools/stern/stern.cabal | 1 - weeder.toml | 52 ++++++++++- 112 files changed, 136 insertions(+), 1271 deletions(-) create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_1.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_10.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_11.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_12.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_13.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_14.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_15.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_16.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_17.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_18.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_19.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_2.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_20.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_3.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_4.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_5.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_6.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_7.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_8.json create mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_9.json delete mode 100644 services/brig/src/Brig/Data/LoginCode.hs rename services/brig/src/Brig/Schema/{V_FUTUREWORK.hs => V85_DropUserKeysHashed.hs} (82%) delete mode 100644 services/spar/migrate-data/src/Spar/DataMigration/RIO.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index ccb834c1c8e..f34be3f2041 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -112,25 +112,6 @@ CREATE TABLE brig_test.rich_info ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; -CREATE TABLE brig_test.user_keys_hash ( - key blob PRIMARY KEY, - key_type int, - user uuid -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - CREATE TABLE brig_test.service_tag ( bucket int, tag bigint, diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index a0fe93d2993..14861a26f04 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -592,13 +592,6 @@ legalholdUserStatus tid ownerid user = do req <- baseRequest ownerid Galley Versioned (joinHttpPath ["teams", tidS, "legalhold", uid]) submit "GET" req --- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_teams__tid__legalhold_settings -enableLegalHold :: (HasCallStack, MakesValue tid, MakesValue ownerid) => tid -> ownerid -> App Response -enableLegalHold tid ownerid = do - tidStr <- asString tid - req <- baseRequest ownerid Galley Versioned (joinHttpPath ["teams", tidStr, "features", "legalhold"]) - submit "PUT" (addJSONObject ["status" .= "enabled", "ttl" .= "unlimited"] req) - -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/delete_teams__tid__legalhold__uid_ disableLegalHold :: (HasCallStack, MakesValue tid, MakesValue ownerid, MakesValue uid) => diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index f426336b6c7..af7d18900e2 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -84,21 +84,6 @@ shouldMatchWithMsg msg a b = do else pure "" assertFailure $ (maybe "" (<> "\n") msg) <> "Actual:\n" <> pa <> "\nExpected:\n" <> pb <> diff --- | apply some canonicalization transformations that *usually* do not change semantics before --- comparing. -shouldMatchLeniently :: (MakesValue a, MakesValue b, HasCallStack) => a -> b -> App () -shouldMatchLeniently = shouldMatchWithRules [EmptyArrayIsNull, RemoveNullFieldsFromObjects] (const $ pure Nothing) - --- | apply *all* canonicalization transformations before comparing. some of these may not be --- valid on your input, see 'LenientMatchRule' for details. -shouldMatchSloppily :: (MakesValue a, MakesValue b, HasCallStack) => a -> b -> App () -shouldMatchSloppily = shouldMatchWithRules [minBound ..] (const $ pure Nothing) - --- | apply *all* canonicalization transformations before comparing. some of these may not be --- valid on your input, see 'LenientMatchRule' for details. -shouldMatchALittle :: (MakesValue a, MakesValue b, HasCallStack) => (Aeson.Value -> App (Maybe Aeson.Value)) -> a -> b -> App () -shouldMatchALittle = shouldMatchWithRules [minBound ..] - data LenientMatchRule = EmptyArrayIsNull | ArraysAreSets diff --git a/libs/extended/src/Data/Time/Clock/DiffTime.hs b/libs/extended/src/Data/Time/Clock/DiffTime.hs index b84c9f9a95e..2480cf4e59d 100644 --- a/libs/extended/src/Data/Time/Clock/DiffTime.hs +++ b/libs/extended/src/Data/Time/Clock/DiffTime.hs @@ -11,7 +11,7 @@ where import Data.Time import Imports --- we really should be doing all this with https://hackage.haskell.org/package/units... +-- FUTUREWORK: we really should be doing all this with https://hackage.haskell.org/package/units... millisecondsToDiffTime :: Integer -> DiffTime millisecondsToDiffTime = picosecondsToDiffTime . (e9 *) diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 75d70c0fb14..6dbba074972 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -245,7 +245,6 @@ findTeamMember u = find ((u ==) . view userId) isTeamOwner :: TeamMemberOptPerms -> Bool isTeamOwner tm = optionalPermissions tm == Just fullPermissions --- | Use this to construct the condition expected by 'teamMemberJson', 'teamMemberListJson' canSeePermsOf :: TeamMember -> TeamMember -> Bool canSeePermsOf seeer seeee = seeer `hasPermission` GetMemberPermissions || seeer == seeee diff --git a/libs/hscim/src/Web/Scim/Client.hs b/libs/hscim/src/Web/Scim/Client.hs index c80070fb038..e736534e56e 100644 --- a/libs/hscim/src/Web/Scim/Client.hs +++ b/libs/hscim/src/Web/Scim/Client.hs @@ -31,7 +31,6 @@ module Web.Scim.Client getUsers, getUser, postUser, - putUser, patchUser, deleteUser, @@ -134,15 +133,6 @@ postUser :: IO (StoredUser tag) postUser env tok = case users (scimClients env) tok of ((_ :<|> (_ :<|> r)) :<|> (_ :<|> (_ :<|> _))) -> r -putUser :: - (HasScimClient tag) => - ClientEnv -> - Maybe (AuthData tag) -> - UserId tag -> - User tag -> - IO (StoredUser tag) -putUser env tok = case users (scimClients env) tok of ((_ :<|> (_ :<|> _)) :<|> (r :<|> (_ :<|> _))) -> r - patchUser :: (HasScimClient tag) => ClientEnv -> diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index ef162e09846..94ad9d1dc11 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -60,19 +60,13 @@ module Imports module UnliftIO.Directory, -- ** Prelude - putChar, putStr, putStrLn, print, - getChar, getLine, - getContents, - interact, readFile, writeFile, appendFile, - readIO, - readLn, -- ** Environment getArgs, @@ -241,9 +235,6 @@ type LByteString = Data.ByteString.Lazy.ByteString ---------------------------------------------------------------------------- -- Lifted functions from Prelude -putChar :: (MonadIO m) => Char -> m () -putChar = liftIO . P.putChar - putStr :: (MonadIO m) => String -> m () putStr = liftIO . P.putStr @@ -253,18 +244,9 @@ putStrLn = liftIO . P.putStrLn print :: (Show a, MonadIO m) => a -> m () print = liftIO . P.print -getChar :: (MonadIO m) => m Char -getChar = liftIO P.getChar - getLine :: (MonadIO m) => m String getLine = liftIO P.getLine -getContents :: (MonadIO m) => m String -getContents = liftIO P.getContents - -interact :: (MonadIO m) => (String -> String) -> m () -interact = liftIO . P.interact - readFile :: (MonadIO m) => FilePath -> m String readFile = liftIO . P.readFile @@ -274,12 +256,6 @@ writeFile = fmap liftIO . P.writeFile appendFile :: (MonadIO m) => FilePath -> String -> m () appendFile = fmap liftIO . P.appendFile -readIO :: (Read a, MonadIO m) => String -> m a -readIO = liftIO . P.readIO - -readLn :: (Read a, MonadIO m) => m a -readLn = liftIO P.readLn - ---------------------------------------------------------------------- -- Functor diff --git a/libs/wire-api/src/Wire/API/Team/Conversation.hs b/libs/wire-api/src/Wire/API/Team/Conversation.hs index 877ca425df3..d3b240d9f54 100644 --- a/libs/wire-api/src/Wire/API/Team/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Team/Conversation.hs @@ -77,9 +77,7 @@ newTeamConversation = TeamConversation -------------------------------------------------------------------------------- -- TeamConversationList -newtype TeamConversationList = TeamConversationList - { _teamConversations :: [TeamConversation] - } +newtype TeamConversationList = TeamConversationList {teamConversations :: [TeamConversation]} deriving (Generic) deriving stock (Eq, Show) deriving newtype (Arbitrary) @@ -91,10 +89,9 @@ instance ToSchema TeamConversationList where "TeamConversationList" (description ?~ "Team conversation list") $ TeamConversationList - <$> _teamConversations .= field "conversations" (array schema) + <$> teamConversations .= field "conversations" (array schema) newTeamConversationList :: [TeamConversation] -> TeamConversationList newTeamConversationList = TeamConversationList makeLenses ''TeamConversation -makeLenses ''TeamConversationList diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index f2fec9ce3d6..c9f24b7b158 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -39,7 +39,6 @@ module Wire.API.Team.Feature setTTL, setWsTTL, WithStatusPatch, - wsPatch, wspStatus, wspLockStatus, wspConfig, @@ -53,7 +52,6 @@ module Wire.API.Team.Feature FeatureTTL' (..), FeatureTTLUnit (..), convertFeatureTTLDaysToSeconds, - convertFeatureTTLSecondsToDays, EnforceAppLock (..), defFeatureStatusNoLock, computeFeatureConfigForTeamUser, @@ -319,9 +317,6 @@ deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg), Typeable cfg) => S.ToSchema (WithStatusPatch cfg) -wsPatch :: Maybe FeatureStatus -> Maybe LockStatus -> Maybe cfg -> Maybe FeatureTTL -> WithStatusPatch cfg -wsPatch = WithStatusBase - wspStatus :: WithStatusPatch cfg -> Maybe FeatureStatus wspStatus = wsbStatus @@ -421,10 +416,6 @@ convertFeatureTTLDaysToSeconds :: FeatureTTLDays -> FeatureTTL convertFeatureTTLDaysToSeconds FeatureTTLUnlimited = FeatureTTLUnlimited convertFeatureTTLDaysToSeconds (FeatureTTLSeconds d) = FeatureTTLSeconds (d * (60 * 60 * 24)) -convertFeatureTTLSecondsToDays :: FeatureTTL -> FeatureTTLDays -convertFeatureTTLSecondsToDays FeatureTTLUnlimited = FeatureTTLUnlimited -convertFeatureTTLSecondsToDays (FeatureTTLSeconds d) = FeatureTTLSeconds (d `div` (60 * 60 * 24)) - instance Arbitrary FeatureTTL where arbitrary = (nonZero <$> arbitrary) diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index 812c63c000d..d94cbfecc32 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -28,7 +28,6 @@ module Wire.API.Team.Member invitation, legalHoldStatus, ntmNewTeamMember, - teamMemberJson, setOptionalPerms, setOptionalPermsMany, teamMemberObjectSchema, @@ -426,9 +425,6 @@ permissions = newTeamMember . nPermissions invitation :: Lens' TeamMember (Maybe (UserId, UTCTimeMillis)) invitation = newTeamMember . nInvitation -teamMemberJson :: (TeamMember -> Bool) -> TeamMember -> Value -teamMemberJson withPerms = toJSON . setOptionalPerms withPerms - setOptionalPerms :: (TeamMember -> Bool) -> TeamMember -> TeamMember' 'Optional setOptionalPerms withPerms m = m & permissions %~ setPerm (withPerms m) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 9477ace1dc0..d3692bb5900 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -62,7 +62,6 @@ module Wire.API.User urefToExternalIdUnsafe, urefToEmail, ExpiresIn, - newUserInvitationCode, newUserTeam, newUserEmail, newUserSSOId, @@ -1169,11 +1168,6 @@ instance Arbitrary NewUser where genUserExpiresIn newUserIdentity = if isJust newUserIdentity then pure Nothing else arbitrary -newUserInvitationCode :: NewUser -> Maybe InvitationCode -newUserInvitationCode nu = case newUserOrigin nu of - Just (NewUserOriginInvitationCode ic) -> Just ic - _ -> Nothing - newUserTeam :: NewUser -> Maybe NewTeamUser newUserTeam nu = case newUserOrigin nu of Just (NewUserOriginTeamUser tu) -> Just tu diff --git a/libs/wire-api/src/Wire/API/User/Auth.hs b/libs/wire-api/src/Wire/API/User/Auth.hs index eef98189def..806f9745c14 100644 --- a/libs/wire-api/src/Wire/API/User/Auth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth.hs @@ -21,7 +21,6 @@ module Wire.API.User.Auth ( -- * Login Login (..), - loginLabel, LoginCode (..), LoginId (..), PendingLoginCode (..), @@ -347,9 +346,6 @@ instance ToSchema Login where <*> lLabel .= optField "label" (maybeWithDefault A.Null schema) <*> lCode .= optField "verification_code" (maybeWithDefault A.Null schema) -loginLabel :: Login -> Maybe CookieLabel -loginLabel = lLabel - -------------------------------------------------------------------------------- -- RemoveCookies diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index 35bbde4892d..fc80e19c4e7 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -36,7 +36,6 @@ module Wire.API.User.Client mkQualifiedUserClientPrekeyMap, qualifiedUserClientPrekeyMapFromList, UserClientsFull (..), - userClientsFullToUserClients, UserClients (..), mkUserClients, QualifiedUserClients (..), @@ -394,9 +393,6 @@ instance FromJSON UserClientsFull where instance Arbitrary UserClientsFull where arbitrary = UserClientsFull <$> mapOf' arbitrary (setOf' arbitrary) -userClientsFullToUserClients :: UserClientsFull -> UserClients -userClientsFullToUserClients (UserClientsFull mp) = UserClients $ Set.map clientId <$> mp - newtype UserClients = UserClients { userClients :: Map UserId (Set ClientId) } diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index b96ad4135fa..a06c5beb64b 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -44,7 +44,6 @@ module Wire.API.User.Identity -- * UserSSOId UserSSOId (..), emailFromSAML, - emailToSAML, emailToSAMLNameID, emailFromSAMLNameID, mkSampleUref, @@ -421,9 +420,6 @@ lenientlyParseSAMLNameID (Just txt) = do emailFromSAML :: (HasCallStack) => SAMLEmail.Email -> Email emailFromSAML = fromJust . parseEmail . SAMLEmail.render -emailToSAML :: (HasCallStack) => Email -> SAMLEmail.Email -emailToSAML = CI.original . fromRight (error "emailToSAML") . SAMLEmail.validate . toByteString - -- | FUTUREWORK(fisx): if saml2-web-sso exported the 'NameID' constructor, we could make this -- function total without all that praying and hoping. emailToSAMLNameID :: (HasCallStack) => Email -> SAML.NameID diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs index 9100be731e2..7283333f7a2 100644 --- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs +++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs @@ -132,13 +132,9 @@ instance Cql.Cql WireIdPAPIVersion where -- | A list of 'IdP's, returned by some endpoints. Wrapped into an object to -- allow extensibility later on. -data IdPList = IdPList - { _providers :: [IdP] - } +newtype IdPList = IdPList {providers :: [IdP]} deriving (Eq, Show, Generic) -makeLenses ''IdPList - -- Same as WireIdP, we want the lenses, so we have to drop a prefix deriveJSON (defaultOptsDropChar '_') ''IdPList diff --git a/libs/wire-api/src/Wire/API/User/Saml.hs b/libs/wire-api/src/Wire/API/User/Saml.hs index fa97f24fb07..4bebb7bf6d0 100644 --- a/libs/wire-api/src/Wire/API/User/Saml.hs +++ b/libs/wire-api/src/Wire/API/User/Saml.hs @@ -24,7 +24,6 @@ -- for them. module Wire.API.User.Saml where -import Control.Lens (makeLenses) import Control.Monad.Except import Data.Aeson hiding (fieldLabelModifier) import Data.Aeson.TH hiding (fieldLabelModifier) @@ -62,8 +61,6 @@ data VerdictFormat | VerdictFormatMobile {_formatGrantedURI :: URI, _formatDeniedURI :: URI} deriving (Eq, Show, Generic) -makeLenses ''VerdictFormat - deriveJSON deriveJSONOptions ''VerdictFormat mkVerdictGrantedFormatMobile :: (MonadError String m) => URI -> SetCookie -> UserId -> m URI diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index e27bfcb26d2..23360888ad5 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -382,13 +382,6 @@ veidUref = prism' UrefOnly $ UrefOnly uref -> Just uref EmailOnly _ -> Nothing -veidEmail :: Prism' ValidExternalId Email -veidEmail = prism' EmailOnly $ - \case - EmailAndUref em _ -> Just em - UrefOnly _ -> Nothing - EmailOnly em -> Just em - makeLenses ''ValidScimUser makeLenses ''ValidExternalId diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index dfeb601c4e0..455c0c0d2a4 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -1,7 +1,6 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. -- @@ -35,7 +34,7 @@ where import Cassandra qualified as C import Control.Error -import Control.Lens (makePrisms, (?~)) +import Control.Lens ((?~)) import Data.Aeson hiding (object, (.=)) import Data.Aeson qualified as Aeson import Data.Attoparsec.ByteString (sepBy) @@ -329,5 +328,3 @@ instance C.Cql FederatedUserSearchPolicy where fromCql (C.CqlInt 1) = pure ExactHandleSearch fromCql (C.CqlInt 2) = pure FullSearch fromCql n = Left $ "Unexpected SearchVisibilityInbound: " ++ show n - -makePrisms ''FederatedUserSearchPolicy diff --git a/libs/wire-api/src/Wire/API/UserEvent.hs b/libs/wire-api/src/Wire/API/UserEvent.hs index 5f54e7ea54d..acc39c3ebc2 100644 --- a/libs/wire-api/src/Wire/API/UserEvent.hs +++ b/libs/wire-api/src/Wire/API/UserEvent.hs @@ -200,36 +200,6 @@ phoneUpdated :: UserId -> Phone -> UserEvent phoneUpdated u p = UserIdentityUpdated $ UserIdentityUpdatedData u Nothing (Just p) -handleUpdated :: UserId -> Handle -> UserEvent -handleUpdated u h = - UserUpdated $ (emptyUserUpdatedData u) {eupHandle = Just h} - -localeUpdate :: UserId -> Locale -> UserEvent -localeUpdate u loc = - UserUpdated $ (emptyUserUpdatedData u) {eupLocale = Just loc} - -managedByUpdate :: UserId -> ManagedBy -> UserEvent -managedByUpdate u mb = - UserUpdated $ (emptyUserUpdatedData u) {eupManagedBy = Just mb} - -supportedProtocolUpdate :: UserId -> Set BaseProtocolTag -> UserEvent -supportedProtocolUpdate u prots = - UserUpdated $ (emptyUserUpdatedData u) {eupSupportedProtocols = Just prots} - -profileUpdated :: UserId -> UserUpdate -> UserEvent -profileUpdated u UserUpdate {..} = - UserUpdated $ - (emptyUserUpdatedData u) - { eupName = uupName, - eupTextStatus = uupTextStatus, - eupPict = uupPict, - eupAccentId = uupAccentId, - eupAssets = uupAssets - } - -emptyUpdate :: UserId -> UserEvent -emptyUpdate = UserUpdated . emptyUserUpdatedData - emptyUserUpdatedData :: UserId -> UserUpdatedData emptyUserUpdatedData u = UserUpdatedData 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 d2c152497d3..fe01eb3ec34 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 @@ -86,6 +86,7 @@ import Test.Wire.API.Golden.Generated.Event_conversation qualified import Test.Wire.API.Golden.Generated.Event_featureConfig qualified import Test.Wire.API.Golden.Generated.Event_team qualified import Test.Wire.API.Golden.Generated.Event_user qualified +import Test.Wire.API.Golden.Generated.FeatureStatus_team qualified import Test.Wire.API.Golden.Generated.HandleUpdate_user qualified import Test.Wire.API.Golden.Generated.InvitationCode_user qualified import Test.Wire.API.Golden.Generated.InvitationList_team qualified @@ -1428,6 +1429,30 @@ tests = (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_9, "testObject_Event_featureConfig_9.json"), (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_10, "testObject_Event_featureConfig_10.json") ], + testGroup + "Golden: FeatureStatus_team" + $ testObjects + [ (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_1, "testObject_FeatureStatus_team_1.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_2, "testObject_FeatureStatus_team_2.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_3, "testObject_FeatureStatus_team_3.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_4, "testObject_FeatureStatus_team_4.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_5, "testObject_FeatureStatus_team_5.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_6, "testObject_FeatureStatus_team_6.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_7, "testObject_FeatureStatus_team_7.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_8, "testObject_FeatureStatus_team_8.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_9, "testObject_FeatureStatus_team_9.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_10, "testObject_FeatureStatus_team_10.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_11, "testObject_FeatureStatus_team_11.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_12, "testObject_FeatureStatus_team_12.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_13, "testObject_FeatureStatus_team_13.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_14, "testObject_FeatureStatus_team_14.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_15, "testObject_FeatureStatus_team_15.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_16, "testObject_FeatureStatus_team_16.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_17, "testObject_FeatureStatus_team_17.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_18, "testObject_FeatureStatus_team_18.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_19, "testObject_FeatureStatus_team_19.json"), + (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_20, "testObject_FeatureStatus_team_20.json") + ], testGroup "Golden: Event_Conversation" $ testObjects diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_1.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_1.json new file mode 100644 index 00000000000..78bf971c5a4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_1.json @@ -0,0 +1 @@ +"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_10.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_10.json new file mode 100644 index 00000000000..a0760977f71 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_10.json @@ -0,0 +1 @@ +"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_11.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_11.json new file mode 100644 index 00000000000..78bf971c5a4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_11.json @@ -0,0 +1 @@ +"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_12.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_12.json new file mode 100644 index 00000000000..78bf971c5a4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_12.json @@ -0,0 +1 @@ +"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_13.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_13.json new file mode 100644 index 00000000000..78bf971c5a4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_13.json @@ -0,0 +1 @@ +"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_14.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_14.json new file mode 100644 index 00000000000..a0760977f71 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_14.json @@ -0,0 +1 @@ +"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_15.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_15.json new file mode 100644 index 00000000000..a0760977f71 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_15.json @@ -0,0 +1 @@ +"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_16.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_16.json new file mode 100644 index 00000000000..a0760977f71 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_16.json @@ -0,0 +1 @@ +"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_17.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_17.json new file mode 100644 index 00000000000..a0760977f71 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_17.json @@ -0,0 +1 @@ +"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_18.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_18.json new file mode 100644 index 00000000000..a0760977f71 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_18.json @@ -0,0 +1 @@ +"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_19.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_19.json new file mode 100644 index 00000000000..78bf971c5a4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_19.json @@ -0,0 +1 @@ +"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_2.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_2.json new file mode 100644 index 00000000000..a0760977f71 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_2.json @@ -0,0 +1 @@ +"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_20.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_20.json new file mode 100644 index 00000000000..a0760977f71 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_20.json @@ -0,0 +1 @@ +"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_3.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_3.json new file mode 100644 index 00000000000..78bf971c5a4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_3.json @@ -0,0 +1 @@ +"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_4.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_4.json new file mode 100644 index 00000000000..a0760977f71 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_4.json @@ -0,0 +1 @@ +"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_5.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_5.json new file mode 100644 index 00000000000..78bf971c5a4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_5.json @@ -0,0 +1 @@ +"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_6.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_6.json new file mode 100644 index 00000000000..a0760977f71 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_6.json @@ -0,0 +1 @@ +"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_7.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_7.json new file mode 100644 index 00000000000..78bf971c5a4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_7.json @@ -0,0 +1 @@ +"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_8.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_8.json new file mode 100644 index 00000000000..78bf971c5a4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_8.json @@ -0,0 +1 @@ +"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_9.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_9.json new file mode 100644 index 00000000000..a0760977f71 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_FeatureStatus_team_9.json @@ -0,0 +1 @@ +"disabled" diff --git a/libs/wire-api/test/unit/Test/Wire/API/Password.hs b/libs/wire-api/test/unit/Test/Wire/API/Password.hs index e55bf2ff6cf..8850a377c79 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Password.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Password.hs @@ -32,6 +32,8 @@ tests = testCase "verify old scrypt password still works" testHashingOldScrypt ] +-- TODO: Address password hashing being wrong +-- https://wearezeta.atlassian.net/browse/WPB-9746 testHashPasswordScrypt :: IO () testHashPasswordScrypt = do pwd <- genPassword diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs index 499b1eb12e4..96d9d4c221b 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs @@ -13,14 +13,12 @@ import Polysemy import Wire.Arbitrary data Recipient = Recipient - { _recipientUserId :: UserId, - _recipientClients :: RecipientClients + { recipientUserId :: UserId, + recipientClients :: RecipientClients } deriving stock (Show, Ord, Eq, Generic) deriving (Arbitrary) via GenericUniform Recipient -makeLenses ''Recipient - data Push = Push { _pushConn :: Maybe ConnId, _pushTransient :: Bool, diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs index f59c79d0c2d..7f14c802389 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs @@ -128,8 +128,8 @@ toV2Push p = recipients = map toRecipient $ toList p._pushRecipients toRecipient :: Recipient -> V2.Recipient toRecipient r = - (recipient r._recipientUserId p._pushRoute) - { V2._recipientClients = r._recipientClients + (recipient r.recipientUserId p._pushRoute) + { V2._recipientClients = r.recipientClients } {-# INLINE [1] chunkPushes #-} diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 0838da2bb18..398bb85145c 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -96,6 +96,3 @@ getUserProfile luid targetUser = getLocalUserProfile :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe UserProfile) getLocalUserProfile targetUser = listToMaybe <$> getLocalUserProfiles ((: []) <$> targetUser) - -updateSupportedProtocols :: (Member UserSubsystem r) => Local UserId -> UpdateOriginType -> Set BaseProtocolTag -> Sem r () -updateSupportedProtocols uid mb prots = updateUserProfile uid Nothing mb (def {supportedProtocols = Just prots}) diff --git a/libs/zauth/src/Data/ZAuth/Creation.hs b/libs/zauth/src/Data/ZAuth/Creation.hs index f7dfda93d17..54fec4f357d 100644 --- a/libs/zauth/src/Data/ZAuth/Creation.hs +++ b/libs/zauth/src/Data/ZAuth/Creation.hs @@ -39,13 +39,12 @@ module Data.ZAuth.Creation legalHoldUserToken, -- * Generic - withIndex, newToken, renewToken, ) where -import Control.Lens hiding (withIndex) +import Control.Lens import Control.Monad.Catch (MonadCatch, MonadThrow) import Data.ByteString qualified as Strict import Data.ByteString.Builder (toLazyByteString) @@ -90,13 +89,6 @@ runCreate z k m = do error "runCreate: Key index out of range." runReaderT (zauth m) (z {keyIdx = k}) -withIndex :: Int -> Create a -> Create a -withIndex k m = Create $ do - e <- ask - when (k < 1 || k > Vec.length (zSign e)) $ - error "withIndex: Key index out of range." - local (const (e {keyIdx = k})) (zauth m) - userToken :: Integer -> UUID -> Maybe Text -> Word32 -> Create (Token User) userToken dur usr cli rnd = do d <- expiry dur diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs index 7c6fbf48aab..57ccf6bf0e1 100644 --- a/services/background-worker/test/Test/Wire/Util.hs +++ b/services/background-worker/test/Test/Wire/Util.hs @@ -9,7 +9,6 @@ import Util.Options (Endpoint (..)) import Wire.BackgroundWorker.Env hiding (federatorInternal) import Wire.BackgroundWorker.Env qualified as E import Wire.BackgroundWorker.Options -import Wire.BackgroundWorker.Util testEnv :: IO Env testEnv = do @@ -34,18 +33,3 @@ runTestAppTWithEnv :: Env -> AppT IO a -> Int -> IO a runTestAppTWithEnv Env {..} app port = do let env = Env {federatorInternal = Endpoint "localhost" (fromIntegral port), ..} runAppT env app - -data FakeEnvelope = FakeEnvelope - { rejections :: IORef [Bool], - acks :: IORef Int - } - -newFakeEnvelope :: IO FakeEnvelope -newFakeEnvelope = - FakeEnvelope - <$> newIORef [] - <*> newIORef 0 - -instance RabbitMQEnvelope FakeEnvelope where - ack e = atomicModifyIORef' e.acks $ \a -> (a + 1, ()) - reject e requeueFlag = atomicModifyIORef' e.rejections $ \r -> (r <> [requeueFlag], ()) diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 91dd6a130ac..377b7cdcf86 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -107,7 +107,6 @@ library Brig.Data.Activation Brig.Data.Client Brig.Data.Connection - Brig.Data.LoginCode Brig.Data.MLS.KeyPackage Brig.Data.Nonce Brig.Data.Types @@ -187,7 +186,7 @@ library Brig.Schema.V82_DropPhoneColumn Brig.Schema.V83_AddTextStatus Brig.Schema.V84_DropTeamInvitationPhone - Brig.Schema.V_FUTUREWORK + Brig.Schema.V85_DropUserKeysHashed Brig.Team.API Brig.Team.DB Brig.Team.Email diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index dad9ecdefe6..2d865be62c6 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -34,7 +34,6 @@ module Brig.API.User Data.lookupAccounts, Data.lookupAccount, lookupAccountsByIdentity, - lookupProfilesV3, getLegalHoldStatus, Data.lookupName, Data.lookupUser, @@ -126,7 +125,6 @@ import UnliftIO.Async (mapConcurrently_) import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig qualified as E -import Wire.API.Federation.Error import Wire.API.Password import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Team hiding (newTeam) @@ -1132,16 +1130,6 @@ enqueueMultiDeleteCallsCounter = Prom.metricHelp = "Number of users enqueued to be deleted" } --- | Similar to lookupProfiles except it returns all results and all errors --- allowing for partial success. -lookupProfilesV3 :: - (Member UserSubsystem r) => - Local UserId -> - -- | The users ('others') for which to obtain the profiles. - [Qualified UserId] -> - Sem r ([(Qualified UserId, FederationError)], [UserProfile]) -lookupProfilesV3 self others = getUserProfilesWithErrors self others - getLegalHoldStatus :: (Member GalleyAPIAccess r) => UserId -> diff --git a/services/brig/src/Brig/API/Util.hs b/services/brig/src/Brig/API/Util.hs index 6a1d1d532d7..77c08763f89 100644 --- a/services/brig/src/Brig/API/Util.hs +++ b/services/brig/src/Brig/API/Util.hs @@ -17,54 +17,34 @@ module Brig.API.Util ( fetchUserIdentity, - lookupProfilesMaybeFilterSameTeamOnly, logInvitationCode, - validateHandle, logEmail, - traverseConcurrentlyAppT, traverseConcurrentlySem, traverseConcurrentlyWithErrors, - traverseConcurrentlyWithErrorsSem, - traverseConcurrentlyWithErrorsAppT, exceptTToMaybe, ensureLocal, ) where -import Brig.API.Error -import Brig.API.Handler import Brig.API.Types import Brig.App -import Brig.Data.User qualified as Data import Control.Monad.Catch (throwM) import Control.Monad.Trans.Except import Data.Bifunctor -import Data.Handle (Handle, parseHandle) import Data.Id import Data.Maybe import Data.Text qualified as T import Data.Text.Ascii (AsciiText (toText)) import Imports import Polysemy -import Polysemy.Error qualified as E import System.Logger (Msg) import System.Logger qualified as Log import UnliftIO.Async import UnliftIO.Exception (throwIO, try) import Util.Logging (sha256String) -import Wire.API.Error -import Wire.API.Error.Brig import Wire.API.User -import Wire.Sem.Concurrency qualified as C import Wire.UserSubsystem -lookupProfilesMaybeFilterSameTeamOnly :: UserId -> [UserProfile] -> (Handler r) [UserProfile] -lookupProfilesMaybeFilterSameTeamOnly self us = do - selfTeam <- lift $ wrapClient $ Data.lookupUserTeam self - pure $ case selfTeam of - Just team -> filter (\x -> profileTeam x == Just team) us - Nothing -> us - fetchUserIdentity :: (Member UserSubsystem r) => UserId -> AppT r (Maybe UserIdentity) fetchUserIdentity uid = do luid <- qualifyLocal uid @@ -73,9 +53,6 @@ fetchUserIdentity uid = do (throwM $ UserProfileNotFound uid) (pure . userIdentity . selfUser) -validateHandle :: Text -> (Handler r) Handle -validateHandle = maybe (throwStd (errorToWai @'InvalidHandle)) pure . parseHandle - logEmail :: Email -> (Msg -> Msg) logEmail email = Log.field "email_sha256" (sha256String . T.pack . show $ email) @@ -83,21 +60,6 @@ logEmail email = logInvitationCode :: InvitationCode -> (Msg -> Msg) logInvitationCode code = Log.field "invitation_code" (toText $ fromInvitationCode code) --- | Traverse concurrently and collect errors. -traverseConcurrentlyAppT :: - (Traversable t, Member (C.Concurrency 'C.Unsafe) r) => - (a -> ExceptT e (AppT r) b) -> - t a -> - AppT r [Either (a, e) b] -traverseConcurrentlyAppT f t = do - env <- temporaryGetEnv - AppT $ - lift $ - C.unsafePooledMapConcurrentlyN - 8 - (\a -> first (a,) <$> lowerAppT env (runExceptT $ f a)) - t - -- | Traverse concurrently and fail on first error. traverseConcurrentlyWithErrors :: (Traversable t, Exception e, MonadUnliftIO m) => @@ -119,35 +81,5 @@ traverseConcurrentlySem :: traverseConcurrentlySem f = pooledMapConcurrentlyN 8 $ \a -> first (a,) <$> runExceptT (f a) --- | Traverse concurrently and fail on first error. -traverseConcurrentlyWithErrorsSem :: - forall t e a r b. - (Traversable t, Member (C.Concurrency 'C.Unsafe) r) => - (a -> ExceptT e (Sem r) b) -> - t a -> - ExceptT e (Sem r) [b] -traverseConcurrentlyWithErrorsSem f = - ExceptT - . E.runError - . ( traverse (either E.throw pure) - <=< C.unsafePooledMapConcurrentlyN 8 (raise . runExceptT . f) - ) - -traverseConcurrentlyWithErrorsAppT :: - forall t e a r b. - (Traversable t, Member (C.Concurrency 'C.Unsafe) r) => - (a -> ExceptT e (AppT r) b) -> - t a -> - ExceptT e (AppT r) [b] -traverseConcurrentlyWithErrorsAppT f t = do - env <- lift temporaryGetEnv - ExceptT $ - AppT $ - lift $ - runExceptT $ - traverseConcurrentlyWithErrorsSem - (mapExceptT (lowerAppT env) . f) - t - exceptTToMaybe :: (Monad m) => ExceptT e m () -> m (Maybe e) exceptTToMaybe = (pure . either Just (const Nothing)) <=< runExceptT diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 9d475b37262..4c9e0e75b74 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -40,7 +40,6 @@ module Brig.App federator, casClient, userTemplates, - usrTemplates, providerTemplates, teamTemplates, templateBranding, @@ -181,7 +180,7 @@ data Env = Env _applog :: Logger, _internalEvents :: QueueEnv, _requestId :: RequestId, - _usrTemplates :: Localised UserTemplates, + _userTemplates :: Localised UserTemplates, _provTemplates :: Localised ProviderTemplates, _tmTemplates :: Localised TeamTemplates, _templateBranding :: TemplateBranding, @@ -277,7 +276,7 @@ newEnv o = do _applog = lgr, _internalEvents = (eventsQueue :: QueueEnv), _requestId = RequestId "N/A", - _usrTemplates = utp, + _userTemplates = utp, _provTemplates = ptp, _tmTemplates = ttp, _templateBranding = branding, @@ -437,9 +436,6 @@ initCassandra o g = (Just schemaVersion) g -userTemplates :: (MonadReader Env m) => Maybe Locale -> m (Locale, UserTemplates) -userTemplates l = forLocale l <$> view usrTemplates - providerTemplates :: (MonadReader Env m) => Maybe Locale -> m (Locale, ProviderTemplates) providerTemplates l = forLocale l <$> view provTemplates diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 62aca48a5f8..ca597c1063a 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -213,7 +213,7 @@ runBrigToIO e (AppT ma) = do . runDeleteQueue (e ^. internalEvents) . interpretPropertySubsystem propertySubsystemConfig . interpretVerificationCodeSubsystem - . emailSubsystemInterpreter (e ^. usrTemplates) (e ^. templateBranding) + . emailSubsystemInterpreter (e ^. userTemplates) (e ^. templateBranding) . runUserSubsystem userSubsystemConfig . interpretAuthenticationSubsystem ) diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index ef2909b35d9..4c0c2b3415c 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -27,7 +27,6 @@ module Brig.Data.Client addClientWithReAuthPolicy, addClient, rmClient, - hasClient, lookupClient, lookupClients, lookupPubClientsBulk, @@ -238,9 +237,6 @@ lookupPrekeyIds u c = map runIdentity <$> retry x1 (query selectPrekeyIds (params LocalQuorum (u, c))) -hasClient :: (MonadClient m) => UserId -> ClientId -> m Bool -hasClient u d = isJust <$> retry x1 (query1 checkClient (params LocalQuorum (u, d))) - rmClient :: ( MonadClient m, MonadReader Brig.App.Env m, @@ -416,9 +412,6 @@ selectPrekeyIds = "SELECT key FROM prekeys where user = ? and client = ?" removePrekey :: PrepQuery W (UserId, ClientId, PrekeyId) () removePrekey = "DELETE FROM prekeys where user = ? and client = ? and key = ?" -checkClient :: PrepQuery R (UserId, ClientId) (Identity ClientId) -checkClient = "SELECT client from clients where user = ? and client = ?" - selectMLSPublicKey :: PrepQuery R (UserId, ClientId, SignatureSchemeTag) (Identity Blob) selectMLSPublicKey = "SELECT key from mls_public_keys where user = ? and client = ? and sig_scheme = ?" diff --git a/services/brig/src/Brig/Data/Connection.hs b/services/brig/src/Brig/Data/Connection.hs index ff843f215f4..fbe8221018e 100644 --- a/services/brig/src/Brig/Data/Connection.hs +++ b/services/brig/src/Brig/Data/Connection.hs @@ -21,7 +21,6 @@ module Brig.Data.Connection updateConnection, updateConnectionStatus, lookupConnection, - lookupRelation, lookupLocalConnectionsPage, lookupRemoteConnectionsPage, lookupRelationWithHistory, @@ -33,17 +32,14 @@ module Brig.Data.Connection lookupLocalConnectionStatuses, lookupRemoteConnectionStatuses, lookupAllStatuses, - lookupRemoteConnectedUsersC, lookupRemoteConnectedUsersPaginated, countConnections, deleteConnections, deleteRemoteConnections, - deleteRemoteConnectionsDomain, remoteConnectionInsert, remoteConnectionSelect, remoteConnectionSelectFrom, remoteConnectionDelete, - remoteConnectionSelectFromDomain, remoteConnectionClear, -- * Re-exports @@ -56,7 +52,7 @@ import Brig.Data.Types as T import Cassandra import Control.Monad.Morph import Control.Monad.Trans.Maybe -import Data.Conduit (ConduitT, runConduit, (.|)) +import Data.Conduit (runConduit, (.|)) import Data.Conduit.List qualified as C import Data.Domain (Domain) import Data.Id @@ -154,12 +150,6 @@ lookupRelationWithHistory self target = do query1 remoteRelationSelect (params LocalQuorum (tUnqualified self, domain, rtarget)) runIdentity <$$> retry x1 (foldQualified self local remote target) -lookupRelation :: (MonadClient m) => Local UserId -> Qualified UserId -> m Relation -lookupRelation self target = - lookupRelationWithHistory self target <&> \case - Nothing -> Cancelled - Just relh -> (relationDropHistory relh) - -- | For a given user 'A', lookup their outgoing connections (A -> X) to other users. lookupLocalConnections :: (MonadClient m) => @@ -267,11 +257,6 @@ lookupAllStatuses lfroms = do map (\(d, u, r) -> toConnectionStatusV2 from d u r) <$> retry x1 (query remoteRelationsSelectAll (params LocalQuorum (Identity from))) -lookupRemoteConnectedUsersC :: forall m. (MonadClient m) => Local UserId -> Int32 -> ConduitT () [Remote UserConnection] m () -lookupRemoteConnectedUsersC u maxResults = - paginateC remoteConnectionSelect (paramsP LocalQuorum (Identity (tUnqualified u)) maxResults) x1 - .| C.map (\xs -> map (\x@(d, _, _, _, _, _) -> toRemoteUnsafe d (toRemoteUserConnection u x)) xs) - lookupRemoteConnectedUsersPaginated :: (MonadClient m) => Local UserId -> Int32 -> m (Page (Remote UserConnection)) lookupRemoteConnectedUsersPaginated u maxResults = do (\x@(d, _, _, _, _, _) -> toRemoteUnsafe d (toRemoteUserConnection u x)) <$$> retry x1 (paginate remoteConnectionSelect (paramsP LocalQuorum (Identity (tUnqualified u)) maxResults)) @@ -329,14 +314,6 @@ deleteRemoteConnections (tUntagged -> Qualified remoteUser remoteDomain) (fromRa pooledForConcurrentlyN_ 16 locals $ \u -> write remoteConnectionDelete $ params LocalQuorum (u, remoteDomain, remoteUser) -deleteRemoteConnectionsDomain :: (MonadClient m, MonadUnliftIO m) => Domain -> m () -deleteRemoteConnectionsDomain dom = do - -- Select all triples for the given domain, and then delete them - runConduit $ - paginateC remoteConnectionSelectFromDomain (paramsP LocalQuorum (pure dom) 100) x1 - .| C.mapM_ - (pooledMapConcurrentlyN_ 16 $ write remoteConnectionDelete . params LocalQuorum) - -- Queries connectionInsert :: PrepQuery W (UserId, UserId, RelationWithHistory, UTCTimeMillis, ConvId) () @@ -399,9 +376,6 @@ remoteConnectionUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDA remoteConnectionDelete :: PrepQuery W (UserId, Domain, UserId) () remoteConnectionDelete = "DELETE FROM connection_remote where left = ? AND right_domain = ? AND right_user = ?" -remoteConnectionSelectFromDomain :: PrepQuery R (Identity Domain) (UserId, Domain, UserId) -remoteConnectionSelectFromDomain = "SELECT left, right_domain, right_user FROM connection_remote where right_domain = ?" - remoteConnectionClear :: PrepQuery W (Identity UserId) () remoteConnectionClear = "DELETE FROM connection_remote where left = ?" diff --git a/services/brig/src/Brig/Data/LoginCode.hs b/services/brig/src/Brig/Data/LoginCode.hs deleted file mode 100644 index 3103e939747..00000000000 --- a/services/brig/src/Brig/Data/LoginCode.hs +++ /dev/null @@ -1,93 +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 . - --- | Persistent storage for login codes. --- TODO: Use Brig.Data.Codes --- TODO: Move to Brig.User.Auth.DB.LoginCode -module Brig.Data.LoginCode - ( LoginCode, - createLoginCode, - verifyLoginCode, - lookupLoginCode, - ) -where - -import Brig.App (Env, currentTime) -import Cassandra -import Control.Lens (view) -import Data.Code -import Data.Id -import Data.Text qualified as T -import Data.Time.Clock -import Imports -import OpenSSL.BN (randIntegerZeroToNMinusOne) -import Text.Printf (printf) -import Wire.API.User.Auth - --- | Max. number of verification attempts per code. -maxAttempts :: Int32 -maxAttempts = 3 - --- | Timeout of individual codes. -ttl :: NominalDiffTime -ttl = 600 - -createLoginCode :: (MonadClient m, MonadReader Env m) => UserId -> m PendingLoginCode -createLoginCode u = do - now <- liftIO =<< view currentTime - code <- liftIO genCode - insertLoginCode u code maxAttempts (ttl `addUTCTime` now) - pure $! PendingLoginCode code (Timeout ttl) - where - genCode = LoginCode . T.pack . printf "%06d" <$> randIntegerZeroToNMinusOne 1000000 - -verifyLoginCode :: (MonadClient m, MonadReader Env m) => UserId -> LoginCode -> m Bool -verifyLoginCode u c = do - code <- retry x1 (query1 codeSelect (params LocalQuorum (Identity u))) - now <- liftIO =<< view currentTime - case code of - Just (c', _, t) | c == c' && t >= now -> deleteLoginCode u >> pure True - Just (c', n, t) | n > 1 && t > now -> insertLoginCode u c' (n - 1) t >> pure False - Just (_, _, _) -> deleteLoginCode u >> pure False - Nothing -> pure False - -lookupLoginCode :: (MonadReader Env m, MonadClient m) => UserId -> m (Maybe PendingLoginCode) -lookupLoginCode u = do - now <- liftIO =<< view currentTime - validate now =<< retry x1 (query1 codeSelect (params LocalQuorum (Identity u))) - where - validate now (Just (c, _, t)) | now < t = pure (Just (pending c now t)) - validate _ _ = pure Nothing - pending c now t = PendingLoginCode c (timeout now t) - timeout now t = Timeout (t `diffUTCTime` now) - -deleteLoginCode :: (MonadClient m) => UserId -> m () -deleteLoginCode u = retry x5 . write codeDelete $ params LocalQuorum (Identity u) - -insertLoginCode :: (MonadClient m) => UserId -> LoginCode -> Int32 -> UTCTime -> m () -insertLoginCode u c n t = retry x5 . write codeInsert $ params LocalQuorum (u, c, n, t, round ttl) - --- Queries - -codeInsert :: PrepQuery W (UserId, LoginCode, Int32, UTCTime, Int32) () -codeInsert = "INSERT INTO login_codes (user, code, retries, timeout) VALUES (?, ?, ?, ?) USING TTL ?" - -codeSelect :: PrepQuery R (Identity UserId) (LoginCode, Int32, UTCTime) -codeSelect = "SELECT code, retries, timeout FROM login_codes WHERE user = ?" - -codeDelete :: PrepQuery W (Identity UserId) () -codeDelete = "DELETE FROM login_codes WHERE user = ?" diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 66e4ea9d69e..96a1c81341b 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -743,9 +743,6 @@ instance ToJSON AccountFeatureConfigs where getAfcConferenceCallingDefNewMaybe :: Lens.Getter Settings (Maybe (Public.WithStatus Public.ConferenceCallingConfig)) getAfcConferenceCallingDefNewMaybe = Lens.to (Lens.^? (Lens.to setFeatureFlags . Lens._Just . Lens.to afcConferenceCallingDefNew . unImplicitLockStatus)) -getAfcConferenceCallingDefNullMaybe :: Lens.Getter Settings (Maybe (Public.WithStatus Public.ConferenceCallingConfig)) -getAfcConferenceCallingDefNullMaybe = Lens.to (Lens.^? (Lens.to setFeatureFlags . Lens._Just . Lens.to afcConferenceCallingDefNull . unImplicitLockStatus)) - getAfcConferenceCallingDefNew :: Lens.Getter Settings (Public.WithStatus Public.ConferenceCallingConfig) getAfcConferenceCallingDefNew = Lens.to (Public._unImplicitLockStatus . afcConferenceCallingDefNew . fromMaybe defAccountFeatureConfigs . setFeatureFlags) @@ -944,6 +941,4 @@ Lens.makeLensesFor ] ''ElasticSearchOpts -Lens.makeLensesFor [("sftBaseDomain", "sftBaseDomainL")] ''SFTOptions - Lens.makeLensesFor [("serversSource", "serversSourceL")] ''TurnOpts diff --git a/services/brig/src/Brig/Provider/Email.hs b/services/brig/src/Brig/Provider/Email.hs index 1b8f329c240..2e95cbb0ded 100644 --- a/services/brig/src/Brig/Provider/Email.hs +++ b/services/brig/src/Brig/Provider/Email.hs @@ -19,7 +19,6 @@ module Brig.Provider.Email ( sendActivationMail, - sendApprovalRequestMail, sendApprovalConfirmMail, sendPasswordResetMail, ) @@ -28,17 +27,14 @@ where import Brig.App import Brig.Provider.Template import Control.Lens (view) -import Data.ByteString.Conversion import Data.Code qualified as Code import Data.Range import Data.Text (pack) import Data.Text.Ascii qualified as Ascii -import Data.Text.Encoding qualified as Text import Data.Text.Lazy qualified as LT import Imports import Network.Mail.Mime import Polysemy -import Wire.API.Provider import Wire.API.User import Wire.EmailSending import Wire.EmailSubsystem.Interpreter (mkMimeAddress) @@ -96,57 +92,6 @@ renderActivationUrl t (Code.Key k) (Code.Value v) branding = replace "code" = Ascii.toText (fromRange v) replace x = x --------------------------------------------------------------------------------- --- Approval Request Email - -sendApprovalRequestMail :: (Member EmailSending r) => Name -> Email -> HttpsUrl -> Text -> Code.Key -> Code.Value -> (AppT r) () -sendApprovalRequestMail name email url descr key val = do - tpl <- approvalRequestEmail . snd <$> providerTemplates Nothing - branding <- view templateBranding - let mail = ApprovalRequestEmail email name url descr key val - liftSem $ sendMail $ renderApprovalRequestMail mail tpl branding - -data ApprovalRequestEmail = ApprovalRequestEmail - { aprTo :: !Email, - aprName :: !Name, - aprUrl :: !HttpsUrl, - aprDescr :: !Text, - aprKey :: !Code.Key, - aprCode :: !Code.Value - } - -renderApprovalRequestMail :: ApprovalRequestEmail -> ApprovalRequestEmailTemplate -> TemplateBranding -> Mail -renderApprovalRequestMail ApprovalRequestEmail {..} ApprovalRequestEmailTemplate {..} branding = - (emptyMail from) - { mailTo = [to], - mailHeaders = - [ ("Subject", LT.toStrict subj), - ("X-Zeta-Purpose", "ProviderApprovalRequest") - ], - mailParts = [[plainPart txt, htmlPart html]] - } - where - from = Address (Just approvalRequestEmailSenderName) (fromEmail approvalRequestEmailSender) - to = Address (Just "Provider Approval Staff") (fromEmail approvalRequestEmailTo) - txt = renderTextWithBranding approvalRequestEmailBodyText replace branding - html = renderHtmlWithBranding approvalRequestEmailBodyHtml replace branding - subj = renderTextWithBranding approvalRequestEmailSubject replace branding - replace "email" = fromEmail aprTo - replace "name" = fromName aprName - replace "url" = Text.decodeUtf8 (toByteString' aprUrl) - replace "description" = aprDescr - replace "approvalUrl" = renderApprovalUrl approvalRequestEmailUrl aprKey aprCode branding - replace x = x - --- TODO: Unify with renderActivationUrl -renderApprovalUrl :: Template -> Code.Key -> Code.Value -> TemplateBranding -> Text -renderApprovalUrl t (Code.Key k) (Code.Value v) branding = - LT.toStrict $ renderTextWithBranding t replace branding - where - replace "key" = Ascii.toText (fromRange k) - replace "code" = Ascii.toText (fromRange v) - replace x = x - -------------------------------------------------------------------------------- -- Approval Confirmation Email diff --git a/services/brig/src/Brig/Queue.hs b/services/brig/src/Brig/Queue.hs index a5e7d4d275b..3772b57fc08 100644 --- a/services/brig/src/Brig/Queue.hs +++ b/services/brig/src/Brig/Queue.hs @@ -18,68 +18,19 @@ -- | Working with remote queues (like Amazon SQS). module Brig.Queue ( module Brig.Queue.Types, - enqueue, listen, ) where -import Amazonka.SQS.Lens (sendMessageResponse_mD5OfMessageBody) import Brig.AWS qualified as AWS -import Brig.App import Brig.DeleteQueue.Interpreter (QueueEnv (..)) import Brig.Queue.Stomp qualified as Stomp import Brig.Queue.Types -import Control.Exception (ErrorCall (..)) -import Control.Lens (view, (^.)) import Control.Monad.Catch import Data.Aeson -import Data.ByteString.Base16 qualified as B16 -import Data.ByteString.Lazy qualified as BL -import Data.Text.Encoding qualified as T import Imports -import OpenSSL.EVP.Digest (Digest, digestLBS) import System.Logger.Class as Log hiding (settings) --- Note [queue refactoring] --- ~~~~~~~~~~~~~~~~ --- --- The way we deal with queues is not the best. There is at least one piece of --- technical debt here: --- --- 1. 'Queue' is currently used only for the internal events queue, even --- though we have queues in other places (and not only in Brig). We --- should move 'Brig.Queue' out of Brig and use it elsewhere too. - --- | Enqueue a message. --- --- Throws an error in case of failure. -enqueue :: - ( MonadReader Env m, - ToJSON a, - MonadIO m, - MonadLogger m, - MonadThrow m - ) => - QueueEnv -> - a -> - m () -enqueue (StompQueueEnv env queue) message = - Stomp.enqueue env queue message -enqueue (SqsQueueEnv env _ queue) message = do - let body = encode message - bodyMD5 <- digest <$> view digestMD5 <*> pure body - resp <- AWS.execute env (AWS.enqueueStandard queue body) - unless (resp ^. sendMessageResponse_mD5OfMessageBody == Just bodyMD5) $ do - Log.err $ - msg (val "Returned hash (MD5) doesn't match message hash") - . field "SqsQueue" (show queue) - . field "returned_hash" (show (resp ^. sendMessageResponse_mD5OfMessageBody)) - . field "message_hash" (show (Just bodyMD5)) - throwM (ErrorCall "The server couldn't access a queue") - where - digest :: Digest -> BL.ByteString -> Text - digest d = T.decodeLatin1 . B16.encode . digestLBS d - -- | Forever listen to messages coming from a queue and execute a callback -- for each incoming message. -- diff --git a/services/brig/src/Brig/Schema/Run.hs b/services/brig/src/Brig/Schema/Run.hs index cf9b27a2eb9..f833b8d97b1 100644 --- a/services/brig/src/Brig/Schema/Run.hs +++ b/services/brig/src/Brig/Schema/Run.hs @@ -59,6 +59,7 @@ import Brig.Schema.V81_AddFederationRemoteTeams qualified as V81_AddFederationRe import Brig.Schema.V82_DropPhoneColumn qualified as V82_DropPhoneColumn import Brig.Schema.V83_AddTextStatus qualified as V83_AddTextStatus import Brig.Schema.V84_DropTeamInvitationPhone qualified as V84_DropTeamInvitationPhone +import Brig.Schema.V85_DropUserKeysHashed qualified as V85_DropUserKeysHashed import Cassandra.MigrateSchema (migrateSchema) import Cassandra.Schema import Control.Exception (finally) @@ -124,9 +125,8 @@ migrations = V81_AddFederationRemoteTeams.migration, V82_DropPhoneColumn.migration, V83_AddTextStatus.migration, - V84_DropTeamInvitationPhone.migration + V84_DropTeamInvitationPhone.migration, + V85_DropUserKeysHashed.migration -- FUTUREWORK: undo V41 (searchable flag); we stopped using it in -- https://github.com/wireapp/wire-server/pull/964 - -- - -- FUTUREWORK after July 2023: integrate V_FUTUREWORK here. ] diff --git a/services/brig/src/Brig/Schema/V_FUTUREWORK.hs b/services/brig/src/Brig/Schema/V85_DropUserKeysHashed.hs similarity index 82% rename from services/brig/src/Brig/Schema/V_FUTUREWORK.hs rename to services/brig/src/Brig/Schema/V85_DropUserKeysHashed.hs index d4e00c4ec19..22e4879a247 100644 --- a/services/brig/src/Brig/Schema/V_FUTUREWORK.hs +++ b/services/brig/src/Brig/Schema/V85_DropUserKeysHashed.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.Schema.V_FUTUREWORK +module Brig.Schema.V85_DropUserKeysHashed ( migration, ) where @@ -36,13 +36,9 @@ import Text.RawString.QQ -- backwards-incompatbile schema migration docs in -- https://docs.wire.com/developer/developer/cassandra-interaction.html?highlight=backwards+incompatbile#backwards-incompatible-schema-changes -- --- FUTUREWORK: remove futurework_number and replace its usage by the next matching number after July 2023, rename this module with a version number, and --- integrate it inside Main.hs and App.hs -futureworkNumber :: Int32 -futureworkNumber = undefined migration :: Migration -migration = Migration futureworkNumber "Drop deprecated user_keys_hashed table" $ do +migration = Migration 85 "Drop deprecated user_keys_hashed table" $ do schema' [r| DROP TABLE IF EXISTS user_keys_hash diff --git a/services/brig/src/Brig/Team/Email.hs b/services/brig/src/Brig/Team/Email.hs index 07b38e1a57b..f13582fd6f4 100644 --- a/services/brig/src/Brig/Team/Email.hs +++ b/services/brig/src/Brig/Team/Email.hs @@ -22,7 +22,6 @@ module Brig.Team.Email CreatorWelcomeEmail (..), MemberWelcomeEmail (..), sendInvitationMail, - sendCreatorWelcomeMail, sendMemberWelcomeMail, ) where @@ -50,13 +49,6 @@ sendInvitationMail to tid from code loc = do let mail = InvitationEmail to tid code from liftSem $ sendMail $ renderInvitationEmail mail tpl branding -sendCreatorWelcomeMail :: (Member EmailSending r) => Email -> TeamId -> Text -> Maybe Locale -> (AppT r) () -sendCreatorWelcomeMail to tid teamName loc = do - tpl <- creatorWelcomeEmail . snd <$> teamTemplates loc - branding <- view templateBranding - let mail = CreatorWelcomeEmail to tid teamName - liftSem $ sendMail $ renderCreatorWelcomeMail mail tpl branding - sendMemberWelcomeMail :: (Member EmailSending r) => Email -> TeamId -> Text -> Maybe Locale -> (AppT r) () sendMemberWelcomeMail to tid teamName loc = do tpl <- memberWelcomeEmail . snd <$> teamTemplates loc @@ -113,28 +105,6 @@ data CreatorWelcomeEmail = CreatorWelcomeEmail cwTeamName :: !Text } -renderCreatorWelcomeMail :: CreatorWelcomeEmail -> CreatorWelcomeEmailTemplate -> TemplateBranding -> Mail -renderCreatorWelcomeMail CreatorWelcomeEmail {..} CreatorWelcomeEmailTemplate {..} branding = - (emptyMail from) - { mailTo = [to], - mailHeaders = - [ ("Subject", toStrict subj), - ("X-Zeta-Purpose", "Welcome") - ], - mailParts = [[plainPart txt, htmlPart html]] - } - where - from = Address (Just creatorWelcomeEmailSenderName) (fromEmail creatorWelcomeEmailSender) - to = Address Nothing (fromEmail cwTo) - txt = renderTextWithBranding creatorWelcomeEmailBodyText replace branding - html = renderHtmlWithBranding creatorWelcomeEmailBodyHtml replace branding - subj = renderTextWithBranding creatorWelcomeEmailSubject replace branding - replace "url" = creatorWelcomeEmailUrl - replace "email" = fromEmail cwTo - replace "team_id" = idToText cwTid - replace "team_name" = cwTeamName - replace x = x - ------------------------------------------------------------------------------- -- Member Welcome Email diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index e1204c43bfd..23ed4c461bf 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -31,7 +31,6 @@ module Brig.User.Auth.Cookie newCookieLimited, -- * HTTP - setResponseCookie, toWebCookie, -- * Re-exports @@ -55,8 +54,6 @@ import Data.Proxy import Data.RetryAfter import Data.Time.Clock import Imports -import Network.Wai (Response) -import Network.Wai.Utilities.Response (addHeader) import Prometheus qualified as Prom import System.Logger.Class (field, msg, val, (~~)) import System.Logger.Class qualified as Log @@ -264,15 +261,6 @@ newCookieLimited u c typ label = do -------------------------------------------------------------------------------- -- HTTP -setResponseCookie :: - (MonadReader Env m, ZAuth.UserTokenLike u) => - Cookie (ZAuth.Token u) -> - Response -> - m Response -setResponseCookie c r = do - hdr <- toByteString' . WebCookie.renderSetCookie <$> toWebCookie c - pure (addHeader "Set-Cookie" hdr r) - toWebCookie :: (MonadReader Env m, ZAuth.UserTokenLike u) => Cookie (ZAuth.Token u) -> m WebCookie.SetCookie toWebCookie c = do s <- view settings diff --git a/services/brig/src/Brig/ZAuth.hs b/services/brig/src/Brig/ZAuth.hs index 9eaf2cba30a..512e1251b98 100644 --- a/services/brig/src/Brig/ZAuth.hs +++ b/services/brig/src/Brig/ZAuth.hs @@ -77,7 +77,6 @@ module Brig.ZAuth userTokenRand, tokenExpires, tokenExpiresUTC, - tokenKeyIndex, zauthType, -- * Re-exports @@ -444,9 +443,6 @@ userTokenRand' t = t ^. body . rand legalHoldUserTokenRand :: Token LegalHoldUser -> Word32 legalHoldUserTokenRand t = t ^. body . legalHoldUser . rand -tokenKeyIndex :: Token a -> Int -tokenKeyIndex t = t ^. header . key - tokenExpires :: Token a -> POSIXTime tokenExpires t = fromIntegral (t ^. header . time) diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 5e6c4856d96..0a3789a9d47 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -23,7 +23,6 @@ module API.User.Util where import Bilge hiding (accept, timeout) import Bilge.Assert -import Brig.Options (Opts) import Brig.ZAuth (Token) import Cassandra qualified as DB import Codec.MIME.Type qualified as MIME @@ -48,7 +47,6 @@ import Data.String.Conversions import Data.Text.Ascii qualified as Ascii import Data.Vector qualified as Vec import Data.ZAuth.Token qualified as ZAuth -import Federation.Util (withTempMockFederator) import GHC.TypeLits (KnownSymbol) import Imports import Test.Tasty.Cannon qualified as WS @@ -372,23 +370,6 @@ receiveConnectionAction brig fedBrigClient uid1 quid2 action expectedReaction ex res @?= F.NewConnectionResponseOk expectedReaction assertConnectionQualified brig uid1 quid2 expectedRel -sendConnectionUpdateAction :: - (HasCallStack) => - Brig -> - Opts -> - UserId -> - Qualified UserId -> - Maybe F.RemoteConnectionAction -> - Relation -> - Http () -sendConnectionUpdateAction brig opts uid1 quid2 reaction expectedRel = do - let mockConnectionResponse = F.NewConnectionResponseOk reaction - mockResponse = encode mockConnectionResponse - void $ - liftIO . withTempMockFederator opts mockResponse $ - putConnectionQualified brig uid1 quid2 expectedRel !!! const 200 === statusCode - assertConnectionQualified brig uid1 quid2 expectedRel - assertEmailVisibility :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> User -> User -> Bool -> m () assertEmailVisibility brig a b visible = get (apiVersion "v1" . brig . paths ["users", pack . show $ userId b] . zUser (userId a)) !!! do diff --git a/services/brig/test/integration/Federation/Util.hs b/services/brig/test/integration/Federation/Util.hs index ace4d04fbbe..a1cc7edb483 100644 --- a/services/brig/test/integration/Federation/Util.hs +++ b/services/brig/test/integration/Federation/Util.hs @@ -116,32 +116,3 @@ connectUsersEnd2End brig1 brig2 quid1 quid2 = do !!! const 201 === statusCode putConnectionQualified brig2 (qUnqualified quid2) quid1 Accepted !!! const 200 === statusCode - -sendCommitBundle :: (HasCallStack) => FilePath -> FilePath -> Maybe FilePath -> Galley -> UserId -> ClientId -> ByteString -> Http () -sendCommitBundle tmp subGroupStateFn welcomeFn galley uid cid commit = do - subGroupStateRaw <- liftIO $ BS.readFile $ tmp subGroupStateFn - subGroupState <- either (liftIO . assertFailure . T.unpack) pure . decodeMLS' $ subGroupStateRaw - subCommit <- either (liftIO . assertFailure . T.unpack) pure . decodeMLS' $ commit - mbWelcome <- - for - welcomeFn - $ \fn -> do - bs <- liftIO $ BS.readFile $ tmp fn - msg :: Message <- either (liftIO . assertFailure . T.unpack) pure . decodeMLS' $ bs - case msg.content of - MessageWelcome welcome -> pure welcome - _ -> liftIO . assertFailure $ "Expected a welcome" - - let subGroupBundle = CommitBundle subCommit mbWelcome subGroupState - post - ( galley - . paths - ["mls", "commit-bundles"] - . zUser uid - . zClient cid - . zConn "conn" - . header "Z-Type" "access" - . Bilge.content "message/mls" - . lbytes (encodeMLS subGroupBundle) - ) - !!! const 201 === statusCode diff --git a/services/cannon/src/Cannon/Run.hs b/services/cannon/src/Cannon/Run.hs index 05984cedcb1..fe86d2c2d8b 100644 --- a/services/cannon/src/Cannon/Run.hs +++ b/services/cannon/src/Cannon/Run.hs @@ -27,7 +27,7 @@ import Cannon.API.Public import Cannon.App (maxPingInterval) import Cannon.Dict qualified as D import Cannon.Options -import Cannon.Types (Cannon, applog, clients, env, mkEnv, runCannon', runCannonToServant) +import Cannon.Types (Cannon, applog, clients, env, mkEnv, runCannon, runCannonToServant) import Cannon.WS hiding (env) import Control.Concurrent import Control.Concurrent.Async qualified as Async @@ -74,7 +74,7 @@ run o = do <*> newManager defaultManagerSettings {managerConnCount = 128} <*> createSystemRandom <*> mkClock - refreshMetricsThread <- Async.async $ runCannon' e refreshMetrics + refreshMetricsThread <- Async.async $ runCannon e refreshMetrics s <- newSettings $ Server (o ^. cannon . host) (o ^. cannon . port) (applog e) (Just idleTimeout) let middleware :: Wai.Middleware diff --git a/services/cannon/src/Cannon/Types.hs b/services/cannon/src/Cannon/Types.hs index e085a0d9f20..eec8d20ac4b 100644 --- a/services/cannon/src/Cannon/Types.hs +++ b/services/cannon/src/Cannon/Types.hs @@ -23,13 +23,10 @@ module Cannon.Types applog, dict, env, - logger, Cannon, mapConcurrentlyCannon, mkEnv, runCannon, - runCannon', - options, clients, wsenv, runCannonToServant, @@ -47,9 +44,6 @@ import Control.Lens ((^.)) import Control.Monad.Catch import Data.Text.Encoding import Imports -import Network.Wai -import Network.Wai.Utilities.Request qualified as Wai -import Network.Wai.Utilities.Server import Prometheus import Servant qualified import System.Logger qualified as Logger @@ -109,17 +103,8 @@ mkEnv external o l d p g t = Env o l d (RequestId "N/A") $ WS.env external (o ^. cannon . port) (encodeUtf8 $ o ^. gundeck . host) (o ^. gundeck . port) l p d g t (o ^. drainOpts) -runCannon :: Env -> Cannon a -> Request -> IO a -runCannon e c r = do - let rid = Wai.getRequestId defaultRequestIdHeaderName r - e' = e {reqId = rid} - runCannon' e' c - -runCannon' :: Env -> Cannon a -> IO a -runCannon' e c = runReaderT (unCannon c) e - -options :: Cannon Opts -options = Cannon $ asks opts +runCannon :: Env -> Cannon a -> IO a +runCannon e c = runReaderT (unCannon c) e clients :: Cannon (Dict Key Websocket) clients = Cannon $ asks dict @@ -130,10 +115,7 @@ wsenv = Cannon $ do r <- asks reqId pure $ WS.setRequestId r e -logger :: Cannon Logger -logger = Cannon $ asks applog - -- | Natural transformation from 'Cannon' to 'Handler' monad. -- Used to call 'Cannon' from servant. runCannonToServant :: Cannon.Types.Env -> Cannon x -> Servant.Handler x -runCannonToServant env c = liftIO $ runCannon' env c +runCannonToServant env c = liftIO $ runCannon env c diff --git a/services/cargohold/src/CargoHold/API/Error.hs b/services/cargohold/src/CargoHold/API/Error.hs index 4fed14f95bd..ef38babfe79 100644 --- a/services/cargohold/src/CargoHold/API/Error.hs +++ b/services/cargohold/src/CargoHold/API/Error.hs @@ -41,34 +41,6 @@ unverifiedUser = errorToWai @'UnverifiedUser userNotFound :: Error userNotFound = errorToWai @'UserNotFound -invalidMD5 :: Error -invalidMD5 = mkError status400 "client-error" "Invalid MD5." - -requestTimeout :: Error -requestTimeout = - mkError - status408 - "request-timeout" - "The request timed out. The server was still expecting more data \ - \but none was sent over an extended period of time. Idle connections \ - \will be closed." - -uploadTooSmall :: Error -uploadTooSmall = - mkError - status403 - "client-error" - "The current chunk size is \ - \smaller than the minimum allowed." - -uploadTooLarge :: Error -uploadTooLarge = - mkError - status413 - "client-error" - "The current chunk size + offset \ - \is larger than the full upload size." - noMatchingAssetEndpoint :: Error noMatchingAssetEndpoint = errorToWai @'NoMatchingAssetEndpoint diff --git a/services/cargohold/src/CargoHold/AWS.hs b/services/cargohold/src/CargoHold/AWS.hs index 587937d7aa2..29aa2750f44 100644 --- a/services/cargohold/src/CargoHold/AWS.hs +++ b/services/cargohold/src/CargoHold/AWS.hs @@ -32,7 +32,6 @@ module CargoHold.AWS amazonkaDownloadEndpoint, -- * AWS - send, sendCatch, exec, execStream, @@ -164,16 +163,6 @@ sendCatch :: m (Either AWS.Error (AWSResponse r)) sendCatch env = AWS.trying AWS._Error . AWS.send env -send :: - (AWSRequest r, Typeable r, Typeable (AWSResponse r)) => - AWS.Env -> - r -> - Amazon (AWSResponse r) -send env r = throwA =<< sendCatch env r - -throwA :: Either AWS.Error a -> Amazon a -throwA = either (throwM . GeneralError) pure - exec :: ( AWSRequest r, Typeable r, diff --git a/services/cargohold/src/CargoHold/App.hs b/services/cargohold/src/CargoHold/App.hs index 85c31799667..e40d499915b 100644 --- a/services/cargohold/src/CargoHold/App.hs +++ b/services/cargohold/src/CargoHold/App.hs @@ -39,7 +39,6 @@ module CargoHold.App AppT, App, runAppT, - runAppResourceT, executeBrigInteral, -- * Handler Monad @@ -59,7 +58,6 @@ import Control.Error (ExceptT, exceptT) import Control.Exception (throw) import Control.Lens (Lens', makeLenses, non, view, (?~), (^.)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) -import Control.Monad.Trans.Resource (ResourceT, runResourceT, transResourceT) import qualified Data.Map as Map import Data.Qualified import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) @@ -233,9 +231,6 @@ instance HasRequestId (ExceptT e App) where runAppT :: Env -> AppT m a -> m a runAppT e (AppT a) = runReaderT a e -runAppResourceT :: (MonadIO m) => Env -> ResourceT App a -> m a -runAppResourceT e rma = liftIO . runResourceT $ transResourceT (runAppT e) rma - executeBrigInteral :: BrigInternalClient a -> App (Either Servant.ClientError a) executeBrigInteral action = do httpMgr <- view httpManager diff --git a/services/federator/src/Federator/Error.hs b/services/federator/src/Federator/Error.hs index 7b6f06342d9..28d52c18a9e 100644 --- a/services/federator/src/Federator/Error.hs +++ b/services/federator/src/Federator/Error.hs @@ -17,17 +17,10 @@ module Federator.Error ( AsWai (..), - errorResponse, ) where -import Data.Aeson qualified as A -import Network.HTTP.Types.Header -import Network.Wai qualified as Wai import Network.Wai.Utilities.Error qualified as Wai class AsWai e where toWai :: e -> Wai.Error - -errorResponse :: [Header] -> Wai.Error -> Wai.Response -errorResponse hdrs e = Wai.responseLBS (Wai.code e) hdrs (A.encode e) diff --git a/services/federator/src/Federator/Validation.hs b/services/federator/src/Federator/Validation.hs index 38c315a6498..362c081d4ed 100644 --- a/services/federator/src/Federator/Validation.hs +++ b/services/federator/src/Federator/Validation.hs @@ -18,7 +18,6 @@ module Federator.Validation ( ensureCanFederateWith, parseDomain, - parseDomainText, decodeCertificate, validateDomain, validateDomainName, @@ -127,13 +126,6 @@ parseDomain domain = note (DomainParseError (Text.decodeUtf8With Text.lenientDecode domain)) $ fromByteString domain -parseDomainText :: (Member (Error ValidationError) r) => Text -> Sem r Domain -parseDomainText domain = - mapError @String (const (DomainParseError domain)) - . fromEither - . mkDomain - $ domain - -- | Validates an unknown domain string against the allow list using the -- federator startup configuration and checks that it matches the names reported -- by the client certificate diff --git a/services/federator/test/integration/Test/Federator/Util.hs b/services/federator/test/integration/Test/Federator/Util.hs index 92d80d7d752..76f8e31dca4 100644 --- a/services/federator/test/integration/Test/Federator/Util.hs +++ b/services/federator/test/integration/Test/Federator/Util.hs @@ -182,22 +182,15 @@ randomUser' :: m User randomUser' hasPwd brig = do n <- fromName <$> randomName - createUser' hasPwd n brig + createUser hasPwd n brig createUser :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => - Text -> - BrigReq -> - m User -createUser = createUser' True - -createUser' :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Bool -> Text -> BrigReq -> m User -createUser' hasPwd name brig = do +createUser hasPwd name brig = do r <- postUser' hasPwd True name True False Nothing Nothing brig extraTargets) action' --- | Similar to 'updateLocalConversationUnchecked', but skips performing --- user authorisation checks. This is written for use in de-federation code --- where conversations for many users will be torn down at once and must work. --- --- Additionally, this function doesn't make notification calls to clients. -updateLocalConversationUserUnchecked :: - forall tag r. - ( SingI tag, - HasConversationActionEffects tag r, - Member BackendNotificationQueueAccess r, - Member (Error FederationError) r - ) => - Local Conversation -> - Qualified UserId -> - ConversationAction tag -> - Sem r () -updateLocalConversationUserUnchecked lconv qusr action = do - let tag = sing @tag - - -- perform action - void $ performAction tag qusr lconv action - -- -------------------------------------------------------------------------------- -- -- Utilities diff --git a/services/galley/src/Galley/API/Error.hs b/services/galley/src/Galley/API/Error.hs index 04423558a2f..5ece86d0a4f 100644 --- a/services/galley/src/Galley/API/Error.hs +++ b/services/galley/src/Galley/API/Error.hs @@ -25,9 +25,6 @@ module Galley.API.Error internalErrorWithDescription, internalErrorDescription, legalHoldServiceUnavailable, - - -- * Errors thrown by wai-routing handlers - invalidTeamNotificationId, ) where @@ -101,6 +98,3 @@ badConvState cid = legalHoldServiceUnavailable :: (Show a) => a -> Wai.Error legalHoldServiceUnavailable e = Wai.mkError status412 "legalhold-unavailable" ("legal hold service unavailable with underlying error: " <> (LT.pack . show $ e)) - -invalidTeamNotificationId :: Wai.Error -invalidTeamNotificationId = Wai.mkError status400 "invalid-notification-id" "Could not parse notification id (must be UUIDv1)." diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index aab4029df73..ee913e41c13 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -824,7 +824,7 @@ onMLSMessageSent domain rmm = ByteString ) let recipients = - filter (\r -> Set.member (_recipientUserId r) members) + filter (\r -> Set.member (recipientUserId r) members) . map (\(u, clts) -> Recipient u (RecipientClientsSome (List1 clts))) . Map.assocs $ rmm.recipients diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index 3274956f1bd..76745094c1f 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -207,9 +207,3 @@ instance HasField "id" ConvOrSubConv ConvOrSubConvId where instance HasField "migrationState" ConvOrSubConv MLSMigrationState where getField (Conv c) = c.mcMigrationState getField (SubConv _ _) = MLSMigrationMLS - -convOrSubConvActivate :: ActiveMLSConversationData -> ConvOrSubConv -> ConvOrSubConv -convOrSubConvActivate activeData (Conv c) = - Conv $ c {mcMLSData = (mcMLSData c) {cnvmlsActiveData = Just activeData}} -convOrSubConvActivate activeData (SubConv c s) = - SubConv c $ s {scMLSData = (scMLSData s) {cnvmlsActiveData = Just activeData}} diff --git a/services/galley/src/Galley/API/Push.hs b/services/galley/src/Galley/API/Push.hs index c66a8ae73a4..5d379999bf0 100644 --- a/services/galley/src/Galley/API/Push.hs +++ b/services/galley/src/Galley/API/Push.hs @@ -69,7 +69,7 @@ newMessagePush :: Event -> MessagePush newMessagePush botMap mconn mm userOrBots event = - let toPair r = case Map.lookup (_recipientUserId r) botMap of + let toPair r = case Map.lookup (recipientUserId r) botMap of Just botMember -> ([], [botMember]) Nothing -> ([r], []) (recipients, botMembers) = foldMap (toPair . toRecipient) userOrBots diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index b87dcf5e051..cc0332b22a1 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -22,7 +22,6 @@ module Galley.API.Util where import Control.Lens (set, view, (.~), (^.)) import Control.Monad.Extra (allM, anyM) import Data.Bifunctor -import Data.ByteString.Conversion import Data.Code qualified as Code import Data.Domain (Domain) import Data.Id as Id @@ -38,7 +37,6 @@ import Data.Set qualified as Set import Data.Singletons import Data.Text qualified as T import Data.Time -import GHC.TypeLits import Galley.API.Error import Galley.API.Mapping import Galley.Data.Conversation qualified as Data @@ -64,14 +62,10 @@ import Galley.Types.UserList import Gundeck.Types.Push.V2 qualified as PushV2 import Imports hiding (forkIO) import Network.AMQP qualified as Q -import Network.HTTP.Types -import Network.Wai -import Network.Wai.Utilities qualified as Wai import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P -import System.Logger qualified as Log import Wire.API.Connection import Wire.API.Conversation hiding (Member, cnvAccess, cnvAccessRoles, cnvName, cnvType) import Wire.API.Conversation qualified as Public @@ -520,9 +514,6 @@ localBotsAndUsers = foldMap botOrUser Just _ -> (toList (newBotMember m), []) Nothing -> ([], [m]) -location :: (ToByteString a) => a -> Response -> Response -location = Wai.addHeader hLocation . toByteString' - nonTeamMembers :: [LocalMember] -> [TeamMember] -> [LocalMember] nonTeamMembers cm tm = filter (not . isMemberOfTeam . lmId) cm where @@ -1089,13 +1080,3 @@ instance if err' == demote @e then throwS @e else rethrowErrors @effs @r err' - -logRemoteNotificationError :: - forall rpc r. - (Member P.TinyLog r, KnownSymbol rpc) => - FederationError -> - Sem r () -logRemoteNotificationError e = - P.warn $ - Log.field "federation call" (symbolVal (Proxy @rpc)) - . Log.msg (displayException e) diff --git a/services/galley/src/Galley/Cassandra/Access.hs b/services/galley/src/Galley/Cassandra/Access.hs index 05c566bfd11..3d196cd54c2 100644 --- a/services/galley/src/Galley/Cassandra/Access.hs +++ b/services/galley/src/Galley/Cassandra/Access.hs @@ -32,6 +32,3 @@ defAccess ConnectConv (Just (Set [])) = [PrivateAccess] defAccess One2OneConv (Just (Set [])) = [PrivateAccess] defAccess RegularConv (Just (Set [])) = defRegularConvAccess defAccess _ (Just (Set (x : xs))) = x : xs - -privateOnly :: Set Access -privateOnly = Set [PrivateAccess] diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index fa8c5c89042..309996486b6 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -234,9 +234,6 @@ selectConv :: ) selectConv = "select type, creator, access, access_role, access_roles_v2, name, team, deleted, message_timer, receipt_mode, protocol, group_id, epoch, WRITETIME(epoch), cipher_suite from conversation where conv = ?" -selectReceiptMode :: PrepQuery R (Identity ConvId) (Identity (Maybe ReceiptMode)) -selectReceiptMode = "select receipt_mode from conversation where conv = ?" - isConvDeleted :: PrepQuery R (Identity ConvId) (Identity (Maybe Bool)) isConvDeleted = "select deleted from conversation where conv = ?" @@ -364,9 +361,6 @@ insertCipherSuiteForSubConversation = "UPDATE subconversation set cipher_suite = listSubConversations :: PrepQuery R (Identity ConvId) (SubConvId, CipherSuiteTag, Epoch, Writetime Epoch, GroupId) listSubConversations = "SELECT subconv_id, cipher_suite, epoch, WRITETIME(epoch), group_id FROM subconversation WHERE conv_id = ?" -selectSubConversations :: PrepQuery R (Identity ConvId) (Identity SubConvId) -selectSubConversations = "SELECT subconv_id FROM subconversation WHERE conv_id = ?" - deleteSubConversation :: PrepQuery W (ConvId, SubConvId) () deleteSubConversation = "DELETE FROM subconversation where conv_id = ? and subconv_id = ?" @@ -460,9 +454,6 @@ updateRemoteOtrMemberArchived = {- `IF EXISTS`, but that requires benchmarking - updateRemoteMemberHidden :: PrepQuery W (Bool, Maybe Text, Domain, ConvId, UserId) () updateRemoteMemberHidden = {- `IF EXISTS`, but that requires benchmarking -} "update user_remote_conv set hidden = ?, hidden_ref = ? where conv_remote_domain = ? and conv_remote_id = ? and user = ?" -selectRemoteMemberStatus :: PrepQuery R (Domain, ConvId, UserId) (Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text) -selectRemoteMemberStatus = "select otr_muted_status, otr_muted_ref, otr_archived, otr_archived_ref, hidden, hidden_ref from user_remote_conv where conv_remote_domain = ? and conv_remote_id = ? and user = ?" - -- Clients ------------------------------------------------------------------ selectClients :: PrepQuery R (Identity [UserId]) (UserId, C.Set ClientId) diff --git a/services/galley/src/Galley/Data/Conversation.hs b/services/galley/src/Galley/Data/Conversation.hs index fe18548b2c0..aeec69ae609 100644 --- a/services/galley/src/Galley/Data/Conversation.hs +++ b/services/galley/src/Galley/Data/Conversation.hs @@ -27,7 +27,6 @@ module Galley.Data.Conversation convAccess, convAccessData, convAccessRoles, - convCreator, convMessageTimer, convName, convReceiptMode, @@ -86,9 +85,6 @@ convAccessData c = (Set.fromList (convAccess c)) (convAccessRoles c) -convCreator :: Conversation -> Maybe UserId -convCreator = cnvmCreator . convMetadata - convName :: Conversation -> Maybe Text convName = cnvmName . convMetadata diff --git a/services/galley/src/Galley/Effects/FederatorAccess.hs b/services/galley/src/Galley/Effects/FederatorAccess.hs index eaa5e70ba01..73ab4ca9844 100644 --- a/services/galley/src/Galley/Effects/FederatorAccess.hs +++ b/services/galley/src/Galley/Effects/FederatorAccess.hs @@ -25,7 +25,6 @@ module Galley.Effects.FederatorAccess runFederatedConcurrently, runFederatedConcurrentlyEither, runFederatedConcurrentlyBucketsEither, - runFederatedConcurrently_, isFederationConfigured, ) where @@ -71,10 +70,3 @@ data FederatorAccess m a where IsFederationConfigured :: FederatorAccess m Bool makeSem ''FederatorAccess - -runFederatedConcurrently_ :: - (KnownComponent c, Foldable f, Functor f, Member FederatorAccess r) => - f (Remote a) -> - (Remote [a] -> FederatorClient c x) -> - Sem r () -runFederatedConcurrently_ xs = void . runFederatedConcurrently xs diff --git a/services/galley/src/Galley/Intra/Util.hs b/services/galley/src/Galley/Intra/Util.hs index 0ebff1f349f..8947d4a7a4a 100644 --- a/services/galley/src/Galley/Intra/Util.hs +++ b/services/galley/src/Galley/Intra/Util.hs @@ -18,7 +18,6 @@ module Galley.Intra.Util ( IntraComponent (..), call, - asyncCall, ) where @@ -27,7 +26,6 @@ import Bilge qualified as B import Bilge.RPC import Bilge.Retry import Control.Lens (view, (^.)) -import Control.Monad.Catch import Control.Retry import Data.ByteString.Lazy qualified as LB import Data.Misc (portNumber) @@ -38,8 +36,6 @@ import Galley.Monad import Galley.Options import Imports hiding (log) import Network.HTTP.Types -import System.Logger -import System.Logger.Class qualified as LC import Util.Options data IntraComponent = Brig | Spar | Gundeck @@ -79,15 +75,5 @@ call comp r = do let n = LT.pack (componentName comp) recovering (componentRetryPolicy comp) rpcHandlers (const (rpc n (r . r0))) -asyncCall :: IntraComponent -> (Request -> Request) -> App () -asyncCall comp req = void $ do - let n = LT.pack (componentName comp) - forkIO $ catches (void (call comp req)) (handlers n) - where - handlers n = - [ Handler $ \(x :: RPCException) -> LC.err (rpcExceptionMsg x), - Handler $ \(x :: SomeException) -> LC.err $ "remote" .= n ~~ msg (show x) - ] - x1 :: RetryPolicy x1 = limitRetries 1 diff --git a/services/galley/src/Galley/Types/Clients.hs b/services/galley/src/Galley/Types/Clients.hs index 4992b1aaaf3..8c4d925f3cd 100644 --- a/services/galley/src/Galley/Types/Clients.hs +++ b/services/galley/src/Galley/Types/Clients.hs @@ -19,21 +19,12 @@ module Galley.Types.Clients ( Clients, - userIds, clientIds, toList, fromList, fromUserClients, toMap, - fromMap, - singleton, - insert, - diff, - filter, contains, - Galley.Types.Clients.null, - Galley.Types.Clients.nil, - rmClient, ) where @@ -54,15 +45,6 @@ instance Bounds Clients where let n = Map.size ((userClients . clients) c) in n >= fromIntegral x && n <= fromIntegral y -null :: Clients -> Bool -null = Map.null . (userClients . clients) - -nil :: Clients -nil = Clients $ UserClients Map.empty - -userIds :: Clients -> [UserId] -userIds = Map.keys . (userClients . clients) - clientIds :: UserId -> Clients -> [ClientId] clientIds u c = Set.toList $ fromMaybe Set.empty (Map.lookup u ((userClients . clients) c)) @@ -79,44 +61,9 @@ fromList = Clients . UserClients . foldr fn Map.empty fromUserClients :: UserClients -> Clients fromUserClients = Clients -fromMap :: Map UserId (Set ClientId) -> Clients -fromMap = Clients . UserClients - toMap :: Clients -> Map UserId (Set ClientId) toMap = userClients . clients -singleton :: UserId -> [ClientId] -> Clients -singleton u c = - Clients . UserClients $ Map.singleton u (Set.fromList c) - -filter :: (UserId -> Bool) -> Clients -> Clients -filter p = - Clients - . UserClients - . Map.filterWithKey (\u _ -> p u) - . (userClients . clients) - contains :: UserId -> ClientId -> Clients -> Bool contains u c = maybe False (Set.member c) . Map.lookup u . (userClients . clients) - -insert :: UserId -> ClientId -> Clients -> Clients -insert u c = - Clients - . UserClients - . Map.insertWith Set.union u (Set.singleton c) - . (userClients . clients) - -diff :: Clients -> Clients -> Clients -diff (Clients (UserClients ca)) (Clients (UserClients cb)) = - Clients . UserClients $ Map.differenceWith fn ca cb - where - fn a b = - let d = a `Set.difference` b - in if Set.null d then Nothing else Just d - -rmClient :: UserId -> ClientId -> Clients -> Clients -rmClient u c (Clients (UserClients m)) = - Clients . UserClients $ Map.update f u m - where - f x = let s = Set.delete c x in if Set.null s then Nothing else Just s diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 978f7ab4d14..fd0f371a2d2 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -757,43 +757,6 @@ readWelcome fp = runMaybeT $ do guard $ fileSize stat > 0 liftIO $ BS.readFile fp -createRemoveCommit :: (HasCallStack) => ClientIdentity -> [ClientIdentity] -> MLSTest MessagePackage -createRemoveCommit cid targets = do - bd <- State.gets mlsBaseDir - welcomeFile <- liftIO $ emptyTempFile bd "welcome" - pgsFile <- liftIO $ emptyTempFile bd "pgs" - - g <- getClientGroupState cid - - let groupStateMap = Map.fromList (readGroupState g) - let indices = map (fromMaybe (error "could not find target") . flip Map.lookup groupStateMap) targets - commit <- - mlscli - cid - ( [ "member", - "remove", - "--group", - "", - "--group-out", - "", - "--welcome-out", - welcomeFile, - "--group-info-out", - pgsFile - ] - <> map show indices - ) - Nothing - welcome <- liftIO $ readWelcome welcomeFile - pgs <- liftIO $ BS.readFile pgsFile - pure - MessagePackage - { mpSender = cid, - mpMessage = commit, - mpWelcome = welcome, - mpGroupInfo = Just pgs - } - createExternalAddProposal :: (HasCallStack) => ClientIdentity -> MLSTest MessagePackage createExternalAddProposal joiner = do groupId <- diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 8e6d49e3d21..ea775d4b40e 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -59,7 +59,6 @@ import Data.Qualified import Data.Range import Data.Serialize (runPut) import Data.Set qualified as Set -import Data.Singletons import Data.String.Conversions import Data.Text qualified as Text import Data.Text.Encoding qualified as T @@ -96,14 +95,12 @@ import Web.Cookie import Wire.API.Connection import Wire.API.Conversation import Wire.API.Conversation qualified as Conv -import Wire.API.Conversation.Action import Wire.API.Conversation.Code hiding (Value) import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Event.Conversation import Wire.API.Event.Conversation qualified as Conv -import Wire.API.Event.Federation qualified as Fed import Wire.API.Event.LeaveReason import Wire.API.Event.Team import Wire.API.Event.Team qualified as TE @@ -1695,10 +1692,6 @@ assertMLSMessageEvent qcs u message e = do evtFrom e @?= u evtData e @?= EdMLSMessage message --- | This assumes the default role name -wsAssertMemberJoin :: (HasCallStack) => Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> Notification -> IO () -wsAssertMemberJoin conv usr new = wsAssertMemberJoinWithRole conv usr new roleNameWireAdmin - wsAssertMemberJoinWithRole :: (HasCallStack) => Qualified ConvId -> Qualified UserId -> [Qualified UserId] -> RoleName -> Notification -> IO () wsAssertMemberJoinWithRole conv usr new role n = do let e = List1.head (WS.unpackPayload n) @@ -1712,23 +1705,6 @@ assertJoinEvent conv usr new role e = do evtFrom e @?= usr fmap (sort . mMembers) (evtData e ^? _EdMembersJoin) @?= Just (sort (fmap (`SimpleMember` role) new)) -wsAssertFederationDeleted :: - (HasCallStack) => - Domain -> - Notification -> - IO () -wsAssertFederationDeleted dom n = do - ntfTransient n @?= False - assertFederationDeletedEvent dom $ List1.head (WS.unpackPayload n) - -assertFederationDeletedEvent :: - Domain -> - Fed.Event -> - IO () -assertFederationDeletedEvent dom e = do - Fed._eventType e @?= Fed.FederationDelete - Fed._eventDomain e @?= dom - -- FUTUREWORK: See if this one can be implemented in terms of: -- -- checkConvMemberLeaveEvent :: HasCallStack => Qualified ConvId -> Qualified UserId -> WS.WebSocket -> TestM () @@ -1817,29 +1793,6 @@ assertNoMsg ws f = do Left _ -> pure () -- expected Right _ -> assertFailure "Unexpected message" -assertRemoveUpdate :: (MonadIO m, HasCallStack) => FederatedRequest -> Qualified ConvId -> Qualified UserId -> [UserId] -> Qualified UserId -> m () -assertRemoveUpdate req qconvId remover alreadyPresentUsers victim = liftIO $ do - frRPC req @?= "on-conversation-updated" - frOriginDomain req @?= qDomain qconvId - cu <- assertJust $ decode (frBody req) - cuOrigUserId cu @?= remover - cuConvId cu @?= qUnqualified qconvId - sort (cuAlreadyPresentUsers cu) @?= sort alreadyPresentUsers - cuAction cu - @?= SomeConversationAction - (sing @'ConversationRemoveMembersTag) - (ConversationRemoveMembers (pure victim) EdReasonRemoved) - -assertLeaveUpdate :: (MonadIO m, HasCallStack) => FederatedRequest -> Qualified ConvId -> Qualified UserId -> [UserId] -> m () -assertLeaveUpdate req qconvId remover alreadyPresentUsers = liftIO $ do - frRPC req @?= "on-conversation-updated" - frOriginDomain req @?= qDomain qconvId - cu <- assertJust $ decode (frBody req) - cuOrigUserId cu @?= remover - cuConvId cu @?= qUnqualified qconvId - sort (cuAlreadyPresentUsers cu) @?= sort alreadyPresentUsers - cuAction cu @?= SomeConversationAction (sing @'ConversationLeaveTag) () - ------------------------------------------------------------------------------- -- Helpers @@ -1873,15 +1826,9 @@ decodeConvId = qUnqualified . decodeQualifiedConvId decodeQualifiedConvId :: (HasCallStack) => Response (Maybe Lazy.ByteString) -> Qualified ConvId decodeQualifiedConvId = cnvQualifiedId . responseJsonUnsafe -decodeConvList :: (HasCallStack) => Response (Maybe Lazy.ByteString) -> [Conversation] -decodeConvList = convList . responseJsonUnsafeWithMsg "conversations" - decodeConvIdList :: (HasCallStack) => Response (Maybe Lazy.ByteString) -> [ConvId] decodeConvIdList = convList . responseJsonUnsafeWithMsg "conversation-ids" -decodeQualifiedConvIdList :: Response (Maybe Lazy.ByteString) -> Either String [Qualified ConvId] -decodeQualifiedConvIdList = fmap mtpResults . responseJsonEither @ConvIdsPage - zUser :: UserId -> Request -> Request zUser = header "Z-User" . toByteString' diff --git a/services/galley/test/integration/Federation.hs b/services/galley/test/integration/Federation.hs index 666c557ad7d..c2fd087ce73 100644 --- a/services/galley/test/integration/Federation.hs +++ b/services/galley/test/integration/Federation.hs @@ -1,15 +1,10 @@ -{-# LANGUAGE RecordWildCards #-} - module Federation where -import Cassandra qualified as C import Control.Lens ((^.)) import Control.Monad.Catch -import Data.ByteString qualified as LBS import Data.Domain import Data.Id import Data.Qualified -import Data.Set qualified as Set import Data.UUID qualified as UUID import Galley.API.Util import Galley.App @@ -21,15 +16,8 @@ import Test.Tasty.HUnit import TestSetup import UnliftIO.Retry import Wire.API.Conversation -import Wire.API.Conversation qualified as Public import Wire.API.Conversation.Protocol (Protocol (..)) import Wire.API.Conversation.Role (roleNameWireMember) -import Wire.API.Routes.FederationDomainConfig -import Wire.API.Routes.MultiTablePaging -import Wire.API.Routes.MultiTablePaging qualified as Public - -x3 :: RetryPolicy -x3 = limitRetries 3 <> exponentialBackoff 100000 isConvMemberLTests :: TestM () isConvMemberLTests = do @@ -60,16 +48,5 @@ isConvMemberLTests = do liftIO $ assertBool "Qualified UserId (local)" $ isConvMemberL lconv $ tUntagged lUserId liftIO $ assertBool "Qualified UserId (remote)" $ isConvMemberL lconv $ tUntagged rUserId -fromFedList :: FederationDomainConfigs -> Set Domain -fromFedList = Set.fromList . fmap domain . remotes - constHandlers :: (MonadIO m) => [RetryStatus -> Handler m Bool] constHandlers = [const $ Handler $ (\(_ :: SomeException) -> pure True)] - -pageToConvIdPage :: Public.LocalOrRemoteTable -> C.PageWithState (Qualified ConvId) -> Public.ConvIdsPage -pageToConvIdPage table page@C.PageWithState {..} = - Public.MultiTablePage - { mtpResults = pwsResults, - mtpHasMore = C.pwsHasMore page, - mtpPagingState = Public.ConversationPagingState table (LBS.toStrict . C.unPagingState <$> pwsState) - } diff --git a/services/gundeck/src/Gundeck/Aws.hs b/services/gundeck/src/Gundeck/Aws.hs index 944a9d213bf..21d4ada077b 100644 --- a/services/gundeck/src/Gundeck/Aws.hs +++ b/services/gundeck/src/Gundeck/Aws.hs @@ -46,7 +46,6 @@ module Gundeck.Aws Attributes, AWS.Seconds (..), publish, - timeToLive, -- * Feedback listen, @@ -383,26 +382,6 @@ newtype Attributes = Attributes -- -- cf. http://docs.aws.amazon.com/sns/latest/dg/sns-ttl.html -timeToLive :: Transport -> AWS.Seconds -> Attributes -timeToLive t s = Attributes (Endo (ttlAttr s)) - where - ttlAttr n - | n == 0 = setTTL (ttlNow t) - | otherwise = setTTL (toText n) - setTTL v = - let ty = SNS.newMessageAttributeValue "String" - in Map.insert (ttlKey t) (ty & SNS.messageAttributeValue_stringValue ?~ v) - ttlNow GCM = "0" - ttlNow APNS = "0" - ttlNow APNSSandbox = "0" - ttlNow APNSVoIP = "15" -- See note [VoIP TTLs] - ttlNow APNSVoIPSandbox = "15" -- See note [VoIP TTLs] - ttlKey GCM = "AWS.SNS.MOBILE.GCM.TTL" - ttlKey APNS = "AWS.SNS.MOBILE.APNS.TTL" - ttlKey APNSSandbox = "AWS.SNS.MOBILE.APNS_SANDBOX.TTL" - ttlKey APNSVoIP = "AWS.SNS.MOBILE.APNS_VOIP.TTL" - ttlKey APNSVoIPSandbox = "AWS.SNS.MOBILE.APNS_VOIP_SANDBOX.TTL" - publish :: EndpointArn -> LT.Text -> Attributes -> Amazon (Either PublishError ()) publish arn txt attrs = do -- TODO: Make amazonka accept a lazy text or bytestring. diff --git a/services/gundeck/src/Gundeck/Env.hs b/services/gundeck/src/Gundeck/Env.hs index 8fc8b78abaf..e3d1fcbe148 100644 --- a/services/gundeck/src/Gundeck/Env.hs +++ b/services/gundeck/src/Gundeck/Env.hs @@ -62,9 +62,6 @@ data Env = Env makeLenses ''Env -schemaVersion :: Int32 -schemaVersion = 7 - createEnv :: Opts -> IO ([Async ()], Env) createEnv o = do l <- Logger.mkLogger (o ^. logLevel) (o ^. logNetStrings) (o ^. logFormat) diff --git a/services/gundeck/src/Gundeck/Push/Native/Types.hs b/services/gundeck/src/Gundeck/Push/Native/Types.hs index d191bfb0459..b58726da4a6 100644 --- a/services/gundeck/src/Gundeck/Push/Native/Types.hs +++ b/services/gundeck/src/Gundeck/Push/Native/Types.hs @@ -29,7 +29,6 @@ module Gundeck.Push.Native.Types addrEndpoint, addrConn, addrClient, - addrEqualClient, addrPushToken, -- * Re-Exports @@ -42,7 +41,7 @@ module Gundeck.Push.Native.Types ) where -import Control.Lens (Lens', makeLenses, view, (^.)) +import Control.Lens (Lens', makeLenses, (^.)) import Data.Id (ClientId, ConnId, UserId) import Gundeck.Aws.Arn import Gundeck.Types @@ -72,11 +71,6 @@ addrToken = addrPushToken . token addrClient :: Lens' Address ClientId addrClient = addrPushToken . tokenClient -addrEqualClient :: Address -> Address -> Bool -addrEqualClient a a' = - view addrConn a == view addrConn a' - || view addrClient a == view addrClient a' - instance Show Address where show a = showString "Address" diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index 4780f1142a9..c63daf3cf68 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -39,6 +39,7 @@ import Gundeck.Env qualified as Env import Gundeck.Monad import Gundeck.Options hiding (host, port) import Gundeck.React +import Gundeck.Schema.Run (lastSchemaVersion) import Gundeck.ThreadBudget import Imports hiding (head) import Network.Wai as Wai @@ -59,7 +60,7 @@ run :: Opts -> IO () run o = do (rThreads, e) <- createEnv o runClient (e ^. cstate) $ - versionCheck schemaVersion + versionCheck lastSchemaVersion let l = e ^. applog s <- newSettings $ defaultServer (unpack $ o ^. gundeck . host) (o ^. gundeck . port) l let throttleMillis = fromMaybe defSqsThrottleMillis $ o ^. (settings . sqsThrottleMillis) diff --git a/services/gundeck/src/Gundeck/Util.hs b/services/gundeck/src/Gundeck/Util.hs index 9b210881463..5bc0e77f724 100644 --- a/services/gundeck/src/Gundeck/Util.hs +++ b/services/gundeck/src/Gundeck/Util.hs @@ -47,8 +47,3 @@ mapAsync :: m (t (Either SomeException b)) mapAsync f = mapM waitCatch <=< mapM (async . f) {-# INLINE mapAsync #-} - -maybeEqual :: (Eq a) => Maybe a -> Maybe a -> Bool -maybeEqual (Just x) (Just y) = x == y -maybeEqual _ _ = False -{-# INLINE maybeEqual #-} diff --git a/services/gundeck/src/Gundeck/Util/Redis.hs b/services/gundeck/src/Gundeck/Util/Redis.hs index 891505c39ae..d125d04baca 100644 --- a/services/gundeck/src/Gundeck/Util/Redis.hs +++ b/services/gundeck/src/Gundeck/Util/Redis.hs @@ -29,9 +29,6 @@ import System.Logger.Message retry :: (MonadIO m, MonadMask m, MonadLogger m) => RetryPolicyM m -> m a -> m a retry x = recovering x handlers . const -x1 :: RetryPolicy -x1 = limitRetries 1 <> exponentialBackoff 100000 - x3 :: RetryPolicy x3 = limitRetries 3 <> exponentialBackoff 100000 diff --git a/services/proxy/src/Proxy/Options.hs b/services/proxy/src/Proxy/Options.hs index e484dccf6d3..66d8d09c36b 100644 --- a/services/proxy/src/Proxy/Options.hs +++ b/services/proxy/src/Proxy/Options.hs @@ -27,7 +27,6 @@ module Proxy.Options logLevel, logNetStrings, logFormat, - mockOpts, disabledAPIVersions, ) where @@ -36,7 +35,7 @@ import Control.Lens hiding (Level) import Data.Aeson import Data.Aeson.TH import Imports -import System.Logger.Extended (Level (Debug), LogFormat) +import System.Logger.Extended (Level, LogFormat) import Wire.API.Routes.Version data Opts = Opts @@ -64,18 +63,3 @@ data Opts = Opts makeLenses ''Opts deriveJSON defaultOptions {fieldLabelModifier = drop 1} ''Opts - --- | for testing. -mockOpts :: FilePath -> Opts -mockOpts secrets = - Opts - { _host = mempty, - _port = 0, - _secretsConfig = secrets, - _httpPoolSize = 0, - _maxConns = 0, - _logLevel = Debug, - _logNetStrings = pure $ pure $ True, - _logFormat = mempty, - _disabledAPIVersions = mempty - } diff --git a/services/spar/default.nix b/services/spar/default.nix index fe5d88485e7..6256df4df2a 100644 --- a/services/spar/default.nix +++ b/services/spar/default.nix @@ -199,7 +199,6 @@ mkDerivation { vector wai-extra wai-utilities - warp wire-api xml-conduit yaml diff --git a/services/spar/migrate-data/src/Spar/DataMigration/RIO.hs b/services/spar/migrate-data/src/Spar/DataMigration/RIO.hs deleted file mode 100644 index 3db5aa9aa7c..00000000000 --- a/services/spar/migrate-data/src/Spar/DataMigration/RIO.hs +++ /dev/null @@ -1,38 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - --- 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.DataMigration.RIO where - -import Imports - -newtype RIO env a = RIO {unRIO :: ReaderT env IO a} - deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader env) - -runRIO :: env -> RIO env a -> IO a -runRIO e f = runReaderT (unRIO f) e - -modifyRef :: (env -> IORef a) -> (a -> a) -> RIO env () -modifyRef get_ mod' = do - ref <- asks get_ - liftIO (modifyIORef ref mod') - -readRef :: (env -> IORef b) -> RIO env b -readRef g = do - ref <- asks g - liftIO $ readIORef ref diff --git a/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs b/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs index 59c30b74b1f..ea329e08f8a 100644 --- a/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs +++ b/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs @@ -188,10 +188,6 @@ filterResolved resolver migMapInv = yieldOld old go --- for debugging only -resolveNothing :: CollisionResolver -resolveNothing = const (pure . Left) - combineResolver :: CollisionResolver -> CollisionResolver -> CollisionResolver combineResolver resolver1 resolver2 pair olds = resolver1 pair olds >>= \case diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 1e161ee0560..1015d5d2650 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -392,7 +392,6 @@ executable spar-integration , vector , wai-extra , wai-utilities - , warp , wire-api , xml-conduit , yaml @@ -405,7 +404,6 @@ executable spar-migrate-data other-modules: Paths_spar Spar.DataMigration.Options - Spar.DataMigration.RIO Spar.DataMigration.Run Spar.DataMigration.Types Spar.DataMigration.V2_UserV2 diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 12a53e10b96..67d1bd5ace5 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -382,7 +382,7 @@ idpGetAll :: Sem r IdPList idpGetAll zusr = withDebugLog "idpGetAll" (const Nothing) $ do teamid <- Brig.getZUsrCheckPerm zusr ReadIdp - _providers <- IdPConfigStore.getConfigsByTeam teamid + providers <- IdPConfigStore.getConfigsByTeam teamid pure IdPList {..} -- | Delete empty IdPs, or if @"purge=true"@ in the HTTP query, delete all users diff --git a/services/spar/src/Spar/Data.hs b/services/spar/src/Spar/Data.hs index fc79c7dfb7b..919d1ddce4b 100644 --- a/services/spar/src/Spar/Data.hs +++ b/services/spar/src/Spar/Data.hs @@ -23,7 +23,6 @@ module Spar.Data mkTTLAssertions, nominalDiffToSeconds, mkTTLAuthnRequests, - mkTTLAuthnRequestsNDT, -- * SAML Users NormalizedUNameID (..), @@ -75,9 +74,6 @@ mkEnv opts now = mkTTLAuthnRequests :: (MonadError TTLError m) => Env -> UTCTime -> m (TTL "authreq") mkTTLAuthnRequests (Env now maxttl _) = mkTTL now maxttl -mkTTLAuthnRequestsNDT :: (MonadError TTLError m) => Env -> NominalDiffTime -> m (TTL "authreq") -mkTTLAuthnRequestsNDT (Env _ maxttl _) = mkTTLNDT maxttl - mkTTLAssertions :: (MonadError TTLError m) => Env -> UTCTime -> m (TTL "authresp") mkTTLAssertions (Env now _ maxttl) = mkTTL now maxttl diff --git a/services/spar/src/Spar/Intra/BrigApp.hs b/services/spar/src/Spar/Intra/BrigApp.hs index acca8893826..9704658039d 100644 --- a/services/spar/src/Spar/Intra/BrigApp.hs +++ b/services/spar/src/Spar/Intra/BrigApp.hs @@ -38,7 +38,6 @@ module Spar.Intra.BrigApp -- * re-exports, mostly for historical reasons and lazyness emailFromSAML, - emailToSAML, emailToSAMLNameID, emailFromSAMLNameID, ) diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 90895f2164c..d953b064fa8 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -516,7 +516,7 @@ specCRUDIdentityProvider = do (owner :: UserId, _teamid :: TeamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) callIdpGetAll (env ^. teSpar) (Just owner) - `shouldRespondWith` (null . _providers) + `shouldRespondWith` (null . providers) context "some idps are registered" $ do context "client is team owner with email" $ do it "returns a non-empty empty list" $ do @@ -525,7 +525,7 @@ specCRUDIdentityProvider = do (owner, _tid) <- callCreateUserWithTeam _ <- registerTestIdPFrom metadata (env ^. teMgr) owner (env ^. teSpar) callIdpGetAll (env ^. teSpar) (Just owner) - `shouldRespondWith` (not . null . _providers) + `shouldRespondWith` (not . null . providers) context "client is team owner without email" $ do it "returns a non-empty empty list" $ do env <- ask @@ -534,7 +534,7 @@ specCRUDIdentityProvider = do idp <- registerTestIdPFrom metadata (env ^. teMgr) firstOwner (env ^. teSpar) ssoOwner <- mkSsoOwner firstOwner tid idp privcreds callIdpGetAll (env ^. teSpar) (Just ssoOwner) - `shouldRespondWith` (not . null . _providers) + `shouldRespondWith` (not . null . providers) describe "DELETE /identity-providers/:idp" $ do testGetPutDelete (\o t i _ -> callIdpDelete' o t i) context "zuser has wrong team" $ do diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 2fa022b1b4f..e0b1233bf5d 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -38,7 +38,6 @@ module Util.Core it, pending, pendingWith, - xit, shouldRespondWith, module Test.Hspec, aFewTimes, @@ -48,8 +47,6 @@ module Util.Core -- * HTTP call, endpointToReq, - endpointToSettings, - endpointToURL, -- * Other randomEmail, @@ -60,7 +57,6 @@ module Util.Core updateProfileBrig, createUserWithTeam, createUserWithTeamDisableSSO, - getSSOEnabledInternal, putSSOEnabledInternal, inviteAndRegisterUser, createTeamMember, @@ -95,7 +91,6 @@ module Util.Core loginSsoUserFirstTime, loginSsoUserFirstTime', loginCreatedSsoUser, - callAuthnReqPrecheck', callAuthnReq, callAuthnReq', callIdpGet, @@ -170,8 +165,6 @@ import qualified Data.Yaml as Yaml import GHC.TypeLits import Imports hiding (head) import Network.HTTP.Client.MultipartFormData -import qualified Network.Wai.Handler.Warp as Warp -import qualified Network.Wai.Handler.Warp.Internal as Warp import qualified Options.Applicative as OPA import Polysemy (Sem) import SAML2.WebSSO as SAML hiding ((<$$>)) @@ -299,15 +292,6 @@ it :: SpecWith TestEnv it msg bdy = Test.Hspec.it msg $ runReaderT bdy -xit :: - (HasCallStack) => - -- or, more generally: - -- MonadIO m, Example (TestEnv -> m ()), Arg (TestEnv -> m ()) ~ TestEnv - String -> - TestSpar () -> - SpecWith TestEnv -xit msg bdy = Test.Hspec.xit msg $ runReaderT bdy - pending :: (HasCallStack, MonadIO m) => m () pending = liftIO Test.Hspec.pending @@ -396,12 +380,6 @@ createUserWithTeamDisableSSO brg gly = do pure () pure (uid, tid) -getSSOEnabledInternal :: (HasCallStack, MonadHttp m) => GalleyReq -> TeamId -> m ResponseLBS -getSSOEnabledInternal gly tid = do - get $ - gly - . paths ["i", "teams", toByteString' tid, "features", "sso"] - putSSOEnabledInternal :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyReq -> TeamId -> FeatureStatus -> m () putSSOEnabledInternal gly tid enabled = do void . put $ @@ -688,21 +666,6 @@ zConn = header "Z-Connection" endpointToReq :: Endpoint -> (Bilge.Request -> Bilge.Request) endpointToReq ep = Bilge.host (ep ^. host . to cs) . Bilge.port (ep ^. port) -endpointToSettings :: Endpoint -> Warp.Settings -endpointToSettings ep = - Warp.defaultSettings - { Warp.settingsHost = Imports.fromString . cs $ ep ^. host, - Warp.settingsPort = fromIntegral $ ep ^. port - } - -endpointToURL :: (MonadIO m) => Endpoint -> Text -> m URI -endpointToURL ep urlpath = either err pure url - where - url = parseURI' ("http://" <> urlhost <> ":" <> urlport) <&> (=/ urlpath) - urlhost = cs $ ep ^. host - urlport = cs . show $ ep ^. port - err = liftIO . throwIO . ErrorCall . show . (,(ep, url)) - -- spar specifics shouldRespondWith :: @@ -793,8 +756,7 @@ getCookie proxy rsp = do then Right $ SimpleSetCookie web else Left $ "bad cookie name. (found, expected) == " <> show (Web.setCookieName web, SAML.cookieName proxy) --- | In 'setResponseCookie' we set an expiration date iff cookie is persistent. So here we test for --- expiration date. Easier than parsing and inspecting the cookie value. +-- | we test for expiration date as it's asier than parsing and inspecting the cookie value. hasPersistentCookieHeader :: ResponseLBS -> Either String () hasPersistentCookieHeader rsp = do cky <- getCookie (Proxy @"zuid") rsp @@ -982,10 +944,6 @@ callAuthnReq' :: (MonadHttp m) => SparReq -> SAML.IdPId -> m ResponseLBS callAuthnReq' sparreq_ idpid = do get $ sparreq_ . path (cs $ "/sso/initiate-login/" -/ SAML.idPIdToST idpid) -callAuthnReqPrecheck' :: (MonadHttp m) => SparReq -> SAML.IdPId -> m ResponseLBS -callAuthnReqPrecheck' sparreq_ idpid = do - head $ sparreq_ . path (cs $ "/sso/initiate-login/" -/ SAML.idPIdToST idpid) - callIdpGet :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdPId -> m IdP callIdpGet sparreq_ muid idpid = do resp <- callIdpGet' (sparreq_ . expect2xx) muid idpid diff --git a/services/spar/test-integration/Util/Email.hs b/services/spar/test-integration/Util/Email.hs index 8fe7c002872..49a6ca62690 100644 --- a/services/spar/test-integration/Util/Email.hs +++ b/services/spar/test-integration/Util/Email.hs @@ -24,13 +24,10 @@ module Util.Email where import Bilge hiding (accept, timeout) import Bilge.Assert import Brig.Types.Activation -import Control.Lens (view, (^?)) +import Control.Lens (view) import Control.Monad.Catch (MonadCatch) -import Data.Aeson.Lens import Data.ByteString.Conversion import Data.Id -import qualified Data.Misc as Misc -import Data.Text.Encoding (encodeUtf8) import qualified Data.ZAuth.Token as ZAuth import Imports import Test.Tasty.HUnit @@ -40,44 +37,6 @@ import Util.Types import qualified Wire.API.Team.Feature as Feature import Wire.API.User import Wire.API.User.Activation -import qualified Wire.API.User.Auth as Auth - -changeEmailBrig :: - (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => - BrigReq -> - User -> - Email -> - m ResponseLBS -changeEmailBrig brig usr newEmail = do - -- most of this code is stolen from brig integration tests - let oldEmail = fromJust (userEmail usr) - (cky, tok) <- do - rsp <- - login (emailLogin oldEmail defPassword Nothing) Auth.PersistentCookie - Misc.PlainTextPassword6 -> Maybe Auth.CookieLabel -> Auth.Login - emailLogin e pw cl = - Auth.MkLogin (Auth.LoginByEmail e) pw cl Nothing - - login :: Auth.Login -> Auth.CookieType -> (MonadHttp m) => m ResponseLBS - login l t = - post $ - brig - . path "/login" - . (if t == Auth.PersistentCookie then queryItem "persist" "true" else id) - . json l - - decodeCookie :: (HasCallStack) => Response a -> Bilge.Cookie - decodeCookie = fromMaybe (error "missing zuid cookie") . Bilge.getCookie "zuid" - - decodeToken :: (HasCallStack) => Response (Maybe LByteString) -> ZAuth.Token ZAuth.Access - decodeToken r = fromMaybe (error "invalid access_token") $ do - x <- responseBody r - t <- x ^? key "access_token" . _String - fromByteString (encodeUtf8 t) changeEmailBrigCreds :: (MonadHttp m, HasCallStack) => diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index 40ef9884d0a..0dac2a24a75 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -603,9 +603,6 @@ acceptScim = accept "application/scim+json" scimUserId :: Scim.StoredUser SparTag -> UserId scimUserId = Scim.id . Scim.thing -scimPreferredLanguage :: Scim.StoredUser SparTag -> Maybe Text -scimPreferredLanguage = Scim.preferredLanguage . Scim.value . Scim.thing - -- | There are a number of user types that all partially map on each other. This class -- provides a uniform interface to data stored in those types. -- diff --git a/tools/db/inconsistencies/src/Options.hs b/tools/db/inconsistencies/src/Options.hs index 95f7e27e95f..ad81539b070 100644 --- a/tools/db/inconsistencies/src/Options.hs +++ b/tools/db/inconsistencies/src/Options.hs @@ -20,7 +20,6 @@ module Options where import Cassandra qualified as C -import Data.Id import Data.Text qualified as Text import Imports import Options.Applicative @@ -97,15 +96,6 @@ inconsistenciesFileParser = <> metavar "FILEPATH" ) -teamIdParser :: Parser TeamId -teamIdParser = - option - (eitherReader (parseIdFromText . Text.pack)) - ( long "team-id" - <> help "Team id to search into" - <> metavar "TEAM_ID" - ) - cassandraSettingsParser :: String -> Parser CassandraSettings cassandraSettingsParser ks = CassandraSettings diff --git a/tools/db/move-team/src/Schema.hs b/tools/db/move-team/src/Schema.hs index 2249bb2f1a5..00c08360bf6 100644 --- a/tools/db/move-team/src/Schema.hs +++ b/tools/db/move-team/src/Schema.hs @@ -22,7 +22,6 @@ module Schema where import Cassandra import Common import Data.Conduit -import Data.Handle (Handle) import Data.IP (IP) import Data.Id import Data.Time @@ -32,7 +31,6 @@ import Imports import System.FilePath.Posix (()) import Types import Wire.API.Team.Permission -import Wire.API.User.Password (PasswordResetKey) -- This file was autogenerated by move-team-generate @@ -178,14 +176,6 @@ importBrigLoginCodes Env {..} path = do type RowBrigPasswordReset = (Maybe Ascii, Maybe Ascii, Maybe Int32, Maybe UTCTime, Maybe UUID) -selectBrigPasswordReset :: PrepQuery R (Identity [PasswordResetKey]) RowBrigPasswordReset -selectBrigPasswordReset = "SELECT key, code, retries, timeout, user FROM password_reset WHERE key in ?" - -readBrigPasswordReset :: Env -> [PasswordResetKey] -> ConduitM () [RowBrigPasswordReset] IO () -readBrigPasswordReset Env {..} reset_keys = - transPipe (runClient envBrig) $ - paginateC selectBrigPasswordReset (paramsP LocalQuorum (pure reset_keys) envPageSize) x5 - selectBrigPasswordResetAll :: PrepQuery R () RowBrigPasswordReset selectBrigPasswordResetAll = "SELECT key, code, retries, timeout, user FROM password_reset" @@ -408,14 +398,6 @@ importBrigUser Env {..} path = do type RowBrigUserHandle = (Maybe Text, Maybe UUID) -selectBrigUserHandle :: PrepQuery R (Identity [Handle]) RowBrigUserHandle -selectBrigUserHandle = "SELECT handle, user FROM user_handle WHERE handle in ?" - -readBrigUserHandle :: Env -> [Handle] -> ConduitM () [RowBrigUserHandle] IO () -readBrigUserHandle Env {..} handles = - transPipe (runClient envBrig) $ - paginateC selectBrigUserHandle (paramsP LocalQuorum (pure handles) envPageSize) x5 - selectBrigUserHandleAll :: PrepQuery R () RowBrigUserHandle selectBrigUserHandleAll = "SELECT handle, user FROM user_handle" @@ -454,14 +436,6 @@ importBrigUserHandle Env {..} path = do type RowBrigUserKeys = (Maybe Text, Maybe UUID) -selectBrigUserKeys :: PrepQuery R (Identity [Int32]) RowBrigUserKeys -selectBrigUserKeys = "SELECT key, user FROM user_keys WHERE key in ?" - -readBrigUserKeys :: Env -> [Int32] -> ConduitM () [RowBrigUserKeys] IO () -readBrigUserKeys Env {..} keys = - transPipe (runClient envBrig) $ - paginateC selectBrigUserKeys (paramsP LocalQuorum (pure keys) envPageSize) x5 - selectBrigUserKeysAll :: PrepQuery R () RowBrigUserKeys selectBrigUserKeysAll = "SELECT key, user FROM user_keys" @@ -1144,14 +1118,6 @@ importSparScimUserTimes Env {..} path = do type RowSparUser = (Maybe Text, Maybe Text, Maybe UUID) -selectSparUser :: PrepQuery R (Identity [Text]) RowSparUser -selectSparUser = "SELECT issuer, sso_id, uid FROM user WHERE issuer in ?" - -readSparUser :: Env -> [Text] -> ConduitM () [RowSparUser] IO () -readSparUser Env {..} issuer = - transPipe (runClient envSpar) $ - paginateC selectSparUser (paramsP LocalQuorum (pure issuer) envPageSize) x5 - selectSparUserAll :: PrepQuery R () RowSparUser selectSparUserAll = "SELECT issuer, sso_id, uid FROM user" diff --git a/tools/stern/default.nix b/tools/stern/default.nix index 8ccf0f63f20..cde1f4ba46a 100644 --- a/tools/stern/default.nix +++ b/tools/stern/default.nix @@ -87,7 +87,6 @@ mkDerivation { types-common unliftio utf8-string - uuid wai wai-utilities wire-api diff --git a/tools/stern/src/Stern/App.hs b/tools/stern/src/Stern/App.hs index 3a75f308748..3c044890795 100644 --- a/tools/stern/src/Stern/App.hs +++ b/tools/stern/src/Stern/App.hs @@ -26,7 +26,7 @@ module Stern.App where import Bilge qualified import Bilge.RPC (HasRequestId (..)) import Control.Error -import Control.Lens (makeLenses, set, view, (^.)) +import Control.Lens (makeLenses, view, (^.)) import Control.Monad.Catch (MonadCatch, MonadThrow) import Control.Monad.IO.Class import Control.Monad.Reader.Class @@ -34,16 +34,10 @@ import Control.Monad.Trans.Class import Data.ByteString.Conversion (toByteString') import Data.Id import Data.Text.Encoding (encodeUtf8) -import Data.UUID (toString) -import Data.UUID.V4 qualified as UUID import Imports import Network.HTTP.Client (responseTimeoutMicro) -import Network.Wai (Request, Response, ResponseReceived) -import Network.Wai.Utilities (Error (..), lookupRequestId) -import Network.Wai.Utilities.Error qualified as WaiError -import Network.Wai.Utilities.Response (json, setStatus) -import Network.Wai.Utilities.Server (defaultRequestIdHeaderName) -import Network.Wai.Utilities.Server qualified as Server +import Network.Wai (Response, ResponseReceived) +import Network.Wai.Utilities (Error (..)) import Stern.Options as O import System.Logger qualified as Log import System.Logger.Class hiding (Error, info) @@ -124,23 +118,5 @@ type Handler = ExceptT Error App type Continue m = Response -> m ResponseReceived -runHandler :: Env -> Request -> Handler ResponseReceived -> Continue IO -> IO ResponseReceived -runHandler e r h k = do - i <- reqId (lookupRequestId defaultRequestIdHeaderName r) - let e' = set requestId (Bilge.RequestId i) e - a <- runAppT e' (runExceptT h) - either (onError (view applog e) r k) pure a - where - reqId (Just i) = pure i - reqId Nothing = do - uuid <- UUID.nextRandom - pure $ toByteString' $ "stern-" ++ toString uuid - -onError :: Logger -> Request -> Continue IO -> Error -> IO ResponseReceived -onError g r k e = do - Server.logError g (Just r) e - Server.flushRequestBody r - k (setStatus (WaiError.code e) (json e)) - userMsg :: UserId -> Msg -> Msg userMsg = field "user" . toByteString' diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 59636d7e5ba..326f5ebf821 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -23,7 +23,6 @@ module Stern.Intra ( backendApiVersion, - putUser, putUserStatus, getContacts, getUserConnections, @@ -141,24 +140,6 @@ versionedPaths = Bilge.paths . (encodeUtf8 (toUrlPiece backendApiVersion) :) ------------------------------------------------------------------------------- -putUser :: UserId -> UserUpdate -> Handler () -putUser uid upd = do - info $ userMsg uid . msg "Changing user state" - b <- view brig - void $ - catchRpcErrors $ - rpc' - "brig" - b - ( method PUT - . versionedPath "self" - . header "Z-User" (toByteString' uid) - . header "Z-Connection" (toByteString' "") - . lbytes (encode upd) - . contentJson - . expect2xx - ) - putUserStatus :: AccountStatus -> UserId -> Handler () putUserStatus status uid = do info $ userMsg uid . msg "Changing user status" diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index e7572f7c330..01b6639617a 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -101,7 +101,6 @@ library , types-common >=0.4.13 , unliftio , utf8-string - , uuid >=1.3 , wai >=3.0 , wai-utilities >=0.9 , wire-api >=0.1 diff --git a/weeder.toml b/weeder.toml index e0ddc4c24e5..3b2d1056098 100644 --- a/weeder.toml +++ b/weeder.toml @@ -10,16 +10,18 @@ roots = [ # may of the entries here are about general-purpose module "^API.Galley.consentToLegalHold", # FUTUREWORK: write tests that need this! "^API.Galley.enableLegalHold", # FUTUREWORK: write tests that need this! "^API.Galley.getLegalHoldStatus", # FUTUREWORK: write tests that need this! - "^Data.ETag.opaqueDigest", + "^API.MLS.Util.getKeyPackageCount", + "^API.MLS.Util.getKeyPair", + "^API.MLS.Util.getCurrentGroupId", "^Data.ETag._OpaqueDigest", + "^Data.ETag._StrictETag", + "^Data.ETag._WeakETag", + "^Data.ETag.opaqueDigest", "^Data.ETag.opaqueMD5", "^Data.ETag.opaqueSHA1", "^Data.ETag.strictETag", - "^Data.ETag._StrictETag", "^Data.ETag.weakETag", - "^Data.ETag._WeakETag", "^Data.Qualified.isLocal", - "^Data.Range.(<|)", "^Data.Range.rappend", "^Data.Range.rcons", "^Data.Range.rinc", @@ -32,16 +34,31 @@ roots = [ # may of the entries here are about general-purpose module "^Imports.readLn", "^Main.main$", "^Paths_.*", + "^Spec.main$", "^Test.Cargohold.API.Util.shouldMatchALittle", "^Test.Cargohold.API.Util.shouldMatchLeniently", "^Test.Cargohold.API.Util.shouldMatchSloppily", - "^Testlib.JSON.(<$$$>)", + "^Test.Data.Schema.detailSchema", + "^Test.Data.Schema.userSchemaWithDefaultName", + "^Test.Data.Schema.userSchemaWithDefaultName'", + "^TestSetup.runFederationClient", + "^TestSetup.viewCargohold", + "^Testlib.Cannon.awaitAtLeastNMatches", + "^Testlib.Cannon.awaitAtLeastNMatchesResult", + "^Testlib.Cannon.awaitNToMMatches", + "^Testlib.Cannon.awaitNToMMatchesResult", + "^Testlib.Cannon.prettyAwaitAtLeastResult", + "^Testlib.Cannon.printAwaitAtLeastResult", + "^Testlib.Cannon.printAwaitResult", "^Testlib.JSON.member", "^Testlib.Prelude.appendFile", "^Testlib.Prelude.getChar", "^Testlib.Prelude.getContents", "^Testlib.Prelude.getLine", "^Testlib.Prelude.interact", + "^Testlib.Prelude.print", + "^Testlib.Prelude.putChar", + "^Testlib.Prelude.putStr", "^Testlib.Prelude.readFile", "^Testlib.Prelude.readIO", "^Testlib.Prelude.readLn", @@ -50,6 +67,31 @@ roots = [ # may of the entries here are about general-purpose module "^Testlib.Printing.hline", "^Testlib.Run.main$", "^Testlib.RunServices.main$", + "^ThreadBudget.extractLogHistory", + "^Util.assertOne", + "^Util.randomActivationCode", + "^Util.zClient", + "^Web.Scim.Client.deleteGroup", + "^Web.Scim.Client.deleteUser", + "^Web.Scim.Client.getGroup", + "^Web.Scim.Client.getGroups", + "^Web.Scim.Client.getSchemas", + "^Web.Scim.Client.getUser", + "^Web.Scim.Client.getUsers", + "^Web.Scim.Client.patchGroup", + "^Web.Scim.Client.patchUser", + "^Web.Scim.Client.postGroup", + "^Web.Scim.Client.postUser", + "^Web.Scim.Client.putGroup", + "^Web.Scim.Client.resourceTypes", + "^Web.Scim.Client.schema", + "^Web.Scim.Client.scimClients", + "^Web.Scim.Client.scimClients", + "^Web.Scim.Client.spConfig", + "^Web.Scim.Test.Util.getField", + "^Web.Scim.Test.Util.put'", + "^Web.Scim.Test.Util.scim", + "^Web.Scim.Test.Util.shouldEventuallyRespondWith", "^Test.Wire.API.Golden.Run.main$" ] type-class-roots = true # `root-instances` is more precise, but requires more config maintenance. From 49c21db7d303300c66a9e5cf959841d486cc4a1b Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:29:57 +0200 Subject: [PATCH 022/136] [chore] Remove dead code, update weeder.toml to ignore more deps. (#4177) * Deleted obsolete golden tests. * Added more libraries to weeder ignore list. * Removed dead code from brig and a golden test. * Restore unused golden test * Ignore more deps. * More weeding. * More ignored deps, dangling phone functions. * Another golden test, more comments. --- .../Test/Wire/API/Federation/Golden/Runner.hs | 21 ---- .../src/Wire/API/Routes/Internal/LegalHold.hs | 1 + libs/wire-api/src/Wire/API/UserEvent.hs | 8 -- .../golden/Test/Wire/API/Golden/Generated.hs | 25 ----- .../Golden/Generated/FeatureStatus_team.hs | 80 ---------------- .../Wire/API/Golden/Generated/NewConv_user.hs | 4 - .../golden/Test/Wire/API/Golden/Manual.hs | 3 +- .../golden/Test/Wire/API/Golden/Protobuf.hs | 3 + .../testObject_FeatureStatus_team_1.json | 1 - .../testObject_FeatureStatus_team_10.json | 1 - .../testObject_FeatureStatus_team_11.json | 1 - .../testObject_FeatureStatus_team_12.json | 1 - .../testObject_FeatureStatus_team_13.json | 1 - .../testObject_FeatureStatus_team_14.json | 1 - .../testObject_FeatureStatus_team_15.json | 1 - .../testObject_FeatureStatus_team_16.json | 1 - .../testObject_FeatureStatus_team_17.json | 1 - .../testObject_FeatureStatus_team_18.json | 1 - .../testObject_FeatureStatus_team_19.json | 1 - .../testObject_FeatureStatus_team_2.json | 1 - .../testObject_FeatureStatus_team_20.json | 1 - .../testObject_FeatureStatus_team_3.json | 1 - .../testObject_FeatureStatus_team_4.json | 1 - .../testObject_FeatureStatus_team_5.json | 1 - .../testObject_FeatureStatus_team_6.json | 1 - .../testObject_FeatureStatus_team_7.json | 1 - .../testObject_FeatureStatus_team_8.json | 1 - .../testObject_FeatureStatus_team_9.json | 1 - ...ct_QualifiedNewOtrMessage_user_10.protobuf | 11 +++ .../testObject_UserClientPrekeyMap_9.json | 1 + libs/wire-api/wire-api.cabal | 1 - services/brig/src/Brig/App.hs | 4 - services/brig/src/Brig/Data/User.hs | 13 --- services/brig/src/Brig/Options.hs | 3 - services/gundeck/src/Gundeck/Monad.hs | 5 - services/spar/test-integration/Util/Types.hs | 15 ++- tools/stern/src/Stern/API/Routes.hs | 9 -- tools/stern/src/Stern/Intra.hs | 19 ---- weeder.toml | 96 ++++++++++++++++--- 39 files changed, 107 insertions(+), 235 deletions(-) delete mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/FeatureStatus_team.hs delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_1.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_10.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_11.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_12.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_13.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_14.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_15.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_16.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_17.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_18.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_19.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_2.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_20.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_3.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_4.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_5.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_6.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_7.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_8.json delete mode 100644 libs/wire-api/test/golden/testObject_FeatureStatus_team_9.json create mode 100644 libs/wire-api/test/golden/testObject_QualifiedNewOtrMessage_user_10.protobuf create mode 100644 libs/wire-api/test/golden/testObject_UserClientPrekeyMap_9.json diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs index ba2c5e02e54..030d84247cd 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/Runner.hs @@ -17,8 +17,6 @@ module Test.Wire.API.Federation.Golden.Runner ( testObjects, - testFromJSONFailure, - testFromJSONObjects, ) where @@ -60,25 +58,6 @@ testObject obj path = do pure exists -testFromJSONObjects :: forall a. (Typeable a, FromJSON a, Eq a, Show a) => [(a, FilePath)] -> IO () -testFromJSONObjects = traverse_ (uncurry testFromJSONObject) - -testFromJSONObject :: forall a. (Typeable a, FromJSON a, Eq a, Show a) => a -> FilePath -> IO () -testFromJSONObject expected path = do - let dir = "test/golden/fromJSON" - fullPath = dir <> "/" <> path - parsed <- eitherDecodeFileStrict fullPath - assertEqual (show (typeRep @a) <> ": FromJSON of " <> path <> " should match object") (Right expected) parsed - -testFromJSONFailure :: forall a. (Typeable a, FromJSON a, Show a) => FilePath -> IO () -testFromJSONFailure path = do - let dir = "test/golden/fromJSON" - fullPath = dir <> "/" <> path - parsed <- eitherDecodeFileStrict @a fullPath - case parsed of - Right x -> assertFailure $ show (typeRep @a) <> ": FromJSON of " <> path <> ": expected failure, got " <> show x - Left _ -> pure () - assertRight :: (Show a) => Either a b -> IO b assertRight = \case diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs index ffde2e561c3..f77137eab0a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs @@ -39,6 +39,7 @@ type InternalLegalHoldAPI = :> Put '[] NoContent ) +-- TODO: should be called, is currently dead code. swaggerDoc :: OpenApi swaggerDoc = toOpenApi (Proxy @InternalLegalHoldAPI) diff --git a/libs/wire-api/src/Wire/API/UserEvent.hs b/libs/wire-api/src/Wire/API/UserEvent.hs index acc39c3ebc2..23f6de751e9 100644 --- a/libs/wire-api/src/Wire/API/UserEvent.hs +++ b/libs/wire-api/src/Wire/API/UserEvent.hs @@ -188,18 +188,10 @@ emailRemoved :: UserId -> Email -> UserEvent emailRemoved u e = UserIdentityRemoved $ UserIdentityRemovedData u (Just e) Nothing -phoneRemoved :: UserId -> Phone -> UserEvent -phoneRemoved u p = - UserIdentityRemoved $ UserIdentityRemovedData u Nothing (Just p) - emailUpdated :: UserId -> Email -> UserEvent emailUpdated u e = UserIdentityUpdated $ UserIdentityUpdatedData u (Just e) Nothing -phoneUpdated :: UserId -> Phone -> UserEvent -phoneUpdated u p = - UserIdentityUpdated $ UserIdentityUpdatedData u Nothing (Just p) - emptyUserUpdatedData :: UserId -> UserUpdatedData emptyUserUpdatedData u = UserUpdatedData 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 fe01eb3ec34..d2c152497d3 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 @@ -86,7 +86,6 @@ import Test.Wire.API.Golden.Generated.Event_conversation qualified import Test.Wire.API.Golden.Generated.Event_featureConfig qualified import Test.Wire.API.Golden.Generated.Event_team qualified import Test.Wire.API.Golden.Generated.Event_user qualified -import Test.Wire.API.Golden.Generated.FeatureStatus_team qualified import Test.Wire.API.Golden.Generated.HandleUpdate_user qualified import Test.Wire.API.Golden.Generated.InvitationCode_user qualified import Test.Wire.API.Golden.Generated.InvitationList_team qualified @@ -1429,30 +1428,6 @@ tests = (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_9, "testObject_Event_featureConfig_9.json"), (Test.Wire.API.Golden.Generated.Event_featureConfig.testObject_Event_featureConfig_10, "testObject_Event_featureConfig_10.json") ], - testGroup - "Golden: FeatureStatus_team" - $ testObjects - [ (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_1, "testObject_FeatureStatus_team_1.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_2, "testObject_FeatureStatus_team_2.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_3, "testObject_FeatureStatus_team_3.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_4, "testObject_FeatureStatus_team_4.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_5, "testObject_FeatureStatus_team_5.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_6, "testObject_FeatureStatus_team_6.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_7, "testObject_FeatureStatus_team_7.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_8, "testObject_FeatureStatus_team_8.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_9, "testObject_FeatureStatus_team_9.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_10, "testObject_FeatureStatus_team_10.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_11, "testObject_FeatureStatus_team_11.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_12, "testObject_FeatureStatus_team_12.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_13, "testObject_FeatureStatus_team_13.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_14, "testObject_FeatureStatus_team_14.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_15, "testObject_FeatureStatus_team_15.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_16, "testObject_FeatureStatus_team_16.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_17, "testObject_FeatureStatus_team_17.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_18, "testObject_FeatureStatus_team_18.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_19, "testObject_FeatureStatus_team_19.json"), - (Test.Wire.API.Golden.Generated.FeatureStatus_team.testObject_FeatureStatus_team_20, "testObject_FeatureStatus_team_20.json") - ], testGroup "Golden: Event_Conversation" $ testObjects diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/FeatureStatus_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/FeatureStatus_team.hs deleted file mode 100644 index 6c1d40ec4ed..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/FeatureStatus_team.hs +++ /dev/null @@ -1,80 +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 Test.Wire.API.Golden.Generated.FeatureStatus_team where - -import Wire.API.Team.Feature (FeatureStatus (..)) - -testObject_FeatureStatus_team_1 :: FeatureStatus -testObject_FeatureStatus_team_1 = FeatureStatusEnabled - -testObject_FeatureStatus_team_2 :: FeatureStatus -testObject_FeatureStatus_team_2 = FeatureStatusDisabled - -testObject_FeatureStatus_team_3 :: FeatureStatus -testObject_FeatureStatus_team_3 = FeatureStatusEnabled - -testObject_FeatureStatus_team_4 :: FeatureStatus -testObject_FeatureStatus_team_4 = FeatureStatusDisabled - -testObject_FeatureStatus_team_5 :: FeatureStatus -testObject_FeatureStatus_team_5 = FeatureStatusEnabled - -testObject_FeatureStatus_team_6 :: FeatureStatus -testObject_FeatureStatus_team_6 = FeatureStatusDisabled - -testObject_FeatureStatus_team_7 :: FeatureStatus -testObject_FeatureStatus_team_7 = FeatureStatusEnabled - -testObject_FeatureStatus_team_8 :: FeatureStatus -testObject_FeatureStatus_team_8 = FeatureStatusEnabled - -testObject_FeatureStatus_team_9 :: FeatureStatus -testObject_FeatureStatus_team_9 = FeatureStatusDisabled - -testObject_FeatureStatus_team_10 :: FeatureStatus -testObject_FeatureStatus_team_10 = FeatureStatusDisabled - -testObject_FeatureStatus_team_11 :: FeatureStatus -testObject_FeatureStatus_team_11 = FeatureStatusEnabled - -testObject_FeatureStatus_team_12 :: FeatureStatus -testObject_FeatureStatus_team_12 = FeatureStatusEnabled - -testObject_FeatureStatus_team_13 :: FeatureStatus -testObject_FeatureStatus_team_13 = FeatureStatusEnabled - -testObject_FeatureStatus_team_14 :: FeatureStatus -testObject_FeatureStatus_team_14 = FeatureStatusDisabled - -testObject_FeatureStatus_team_15 :: FeatureStatus -testObject_FeatureStatus_team_15 = FeatureStatusDisabled - -testObject_FeatureStatus_team_16 :: FeatureStatus -testObject_FeatureStatus_team_16 = FeatureStatusDisabled - -testObject_FeatureStatus_team_17 :: FeatureStatus -testObject_FeatureStatus_team_17 = FeatureStatusDisabled - -testObject_FeatureStatus_team_18 :: FeatureStatus -testObject_FeatureStatus_team_18 = FeatureStatusDisabled - -testObject_FeatureStatus_team_19 :: FeatureStatus -testObject_FeatureStatus_team_19 = FeatureStatusEnabled - -testObject_FeatureStatus_team_20 :: FeatureStatus -testObject_FeatureStatus_team_20 = FeatureStatusDisabled diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs index 654575f9943..65d223dcd11 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewConv_user.hs @@ -19,7 +19,6 @@ module Test.Wire.API.Golden.Generated.NewConv_user where -import Data.Domain (Domain (Domain)) import Data.Id import Data.Misc (Milliseconds (Ms, ms)) import Data.Set qualified as Set (fromList) @@ -29,9 +28,6 @@ import Wire.API.Conversation import Wire.API.Conversation.Role import Wire.API.User -testDomain :: Domain -testDomain = Domain "testdomain.example.com" - testObject_NewConv_user_1 :: NewConv testObject_NewConv_user_1 = NewConv 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 f2e7e5adf51..feae9694e32 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 @@ -68,7 +68,8 @@ tests = (testObject_UserClientPrekeyMap_5, "testObject_UserClientPrekeyMap_5.json"), (testObject_UserClientPrekeyMap_6, "testObject_UserClientPrekeyMap_6.json"), (testObject_UserClientPrekeyMap_7, "testObject_UserClientPrekeyMap_7.json"), - (testObject_UserClientPrekeyMap_8, "testObject_UserClientPrekeyMap_8.json") + (testObject_UserClientPrekeyMap_8, "testObject_UserClientPrekeyMap_8.json"), + (testObject_UserClientPrekeyMap_8, "testObject_UserClientPrekeyMap_9.json") ], testGroup "QualifiedUserClientPrekeyMap" $ testObjects diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Protobuf.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Protobuf.hs index 81b85c39424..37f444be782 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Protobuf.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Protobuf.hs @@ -57,6 +57,9 @@ tests = ( testObject_QualifiedNewOtrMessage_user_9, "testObject_QualifiedNewOtrMessage_user_9.protobuf" ), + ( testObject_QualifiedNewOtrMessage_user_10, + "testObject_QualifiedNewOtrMessage_user_10.protobuf" + ), ( testObject_QualifiedNewOtrMessage_user_11, "testObject_QualifiedNewOtrMessage_user_11.protobuf" ), diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_1.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_1.json deleted file mode 100644 index 78bf971c5a4..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_1.json +++ /dev/null @@ -1 +0,0 @@ -"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_10.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_10.json deleted file mode 100644 index a0760977f71..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_10.json +++ /dev/null @@ -1 +0,0 @@ -"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_11.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_11.json deleted file mode 100644 index 78bf971c5a4..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_11.json +++ /dev/null @@ -1 +0,0 @@ -"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_12.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_12.json deleted file mode 100644 index 78bf971c5a4..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_12.json +++ /dev/null @@ -1 +0,0 @@ -"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_13.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_13.json deleted file mode 100644 index 78bf971c5a4..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_13.json +++ /dev/null @@ -1 +0,0 @@ -"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_14.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_14.json deleted file mode 100644 index a0760977f71..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_14.json +++ /dev/null @@ -1 +0,0 @@ -"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_15.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_15.json deleted file mode 100644 index a0760977f71..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_15.json +++ /dev/null @@ -1 +0,0 @@ -"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_16.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_16.json deleted file mode 100644 index a0760977f71..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_16.json +++ /dev/null @@ -1 +0,0 @@ -"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_17.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_17.json deleted file mode 100644 index a0760977f71..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_17.json +++ /dev/null @@ -1 +0,0 @@ -"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_18.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_18.json deleted file mode 100644 index a0760977f71..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_18.json +++ /dev/null @@ -1 +0,0 @@ -"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_19.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_19.json deleted file mode 100644 index 78bf971c5a4..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_19.json +++ /dev/null @@ -1 +0,0 @@ -"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_2.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_2.json deleted file mode 100644 index a0760977f71..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_2.json +++ /dev/null @@ -1 +0,0 @@ -"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_20.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_20.json deleted file mode 100644 index a0760977f71..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_20.json +++ /dev/null @@ -1 +0,0 @@ -"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_3.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_3.json deleted file mode 100644 index 78bf971c5a4..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_3.json +++ /dev/null @@ -1 +0,0 @@ -"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_4.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_4.json deleted file mode 100644 index a0760977f71..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_4.json +++ /dev/null @@ -1 +0,0 @@ -"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_5.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_5.json deleted file mode 100644 index 78bf971c5a4..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_5.json +++ /dev/null @@ -1 +0,0 @@ -"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_6.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_6.json deleted file mode 100644 index a0760977f71..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_6.json +++ /dev/null @@ -1 +0,0 @@ -"disabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_7.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_7.json deleted file mode 100644 index 78bf971c5a4..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_7.json +++ /dev/null @@ -1 +0,0 @@ -"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_8.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_8.json deleted file mode 100644 index 78bf971c5a4..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_8.json +++ /dev/null @@ -1 +0,0 @@ -"enabled" diff --git a/libs/wire-api/test/golden/testObject_FeatureStatus_team_9.json b/libs/wire-api/test/golden/testObject_FeatureStatus_team_9.json deleted file mode 100644 index a0760977f71..00000000000 --- a/libs/wire-api/test/golden/testObject_FeatureStatus_team_9.json +++ /dev/null @@ -1 +0,0 @@ -"disabled" diff --git a/libs/wire-api/test/golden/testObject_QualifiedNewOtrMessage_user_10.protobuf b/libs/wire-api/test/golden/testObject_QualifiedNewOtrMessage_user_10.protobuf new file mode 100644 index 00000000000..2d80c883253 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_QualifiedNewOtrMessage_user_10.protobuf @@ -0,0 +1,11 @@ +sender { client: 8 } +recipients { domain: "79-y-r4-9.d" } +recipients { domain: "7f3.ra.9.r37.xavdz88-9vw-z" } +recipients { domain: "7g.hw9aq-1" } +recipients { domain: "8w5.g5l-7.tys" } +recipients { domain: "n.659-s.nfd" } +recipients { domain: "pc5s-p9-48-x.r8cq.ss89h" } +native_push: false +blob: "GL\006" +transient: false +ignore_all { } \ No newline at end of file diff --git a/libs/wire-api/test/golden/testObject_UserClientPrekeyMap_9.json b/libs/wire-api/test/golden/testObject_UserClientPrekeyMap_9.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserClientPrekeyMap_9.json @@ -0,0 +1 @@ +{} diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 191643405d7..5f1a6b8bdc0 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -420,7 +420,6 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.Event_user Test.Wire.API.Golden.Generated.EventType_team Test.Wire.API.Golden.Generated.EventType_user - Test.Wire.API.Golden.Generated.FeatureStatus_team Test.Wire.API.Golden.Generated.HandleUpdate_user Test.Wire.API.Golden.Generated.Invitation_team Test.Wire.API.Golden.Generated.InvitationCode_user diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 4c9e0e75b74..d5b051a3508 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -83,7 +83,6 @@ module Brig.App runHttpClientIO, liftSem, lowerAppT, - temporaryGetEnv, initHttpManagerWithTLSConfig, adhocUserKeyStoreInterpreter, adhocSessionStoreInterpreter, @@ -464,9 +463,6 @@ newtype AppT r a = AppT lowerAppT :: (Member (Final IO) r) => Env -> AppT r a -> Sem r a lowerAppT env (AppT r) = runReaderT r env -temporaryGetEnv :: AppT r Env -temporaryGetEnv = AppT ask - instance Functor (AppT r) where fmap fab (AppT x0) = AppT $ fmap fab x0 diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index bd4da155d8b..147fd666c7a 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -28,7 +28,6 @@ module Brig.Data.User insertAccount, authenticate, reauthenticate, - filterActive, isSamlUser, -- * Lookups @@ -342,15 +341,6 @@ updateStatus u s = userExists :: (MonadClient m) => UserId -> m Bool userExists uid = isJust <$> retry x1 (query1 idSelect (params LocalQuorum (Identity uid))) -filterActive :: (MonadClient m) => [UserId] -> m [UserId] -filterActive us = - map (view _1) . filter isActiveUser - <$> retry x1 (query accountStateSelectAll (params LocalQuorum (Identity us))) - where - isActiveUser :: (UserId, Bool, Maybe AccountStatus) -> Bool - isActiveUser (_, True, Just Active) = True - isActiveUser _ = False - lookupUser :: (MonadClient m, MonadReader Env m) => HavePendingInvitations -> UserId -> m (Maybe User) lookupUser hpi u = listToMaybe <$> lookupUsers hpi [u] @@ -528,9 +518,6 @@ nameSelect = "SELECT name FROM user WHERE id = ?" authSelect :: PrepQuery R (Identity UserId) (Maybe Password, Maybe AccountStatus) authSelect = "SELECT password, status FROM user WHERE id = ?" -accountStateSelectAll :: PrepQuery R (Identity [UserId]) (UserId, Bool, Maybe AccountStatus) -accountStateSelectAll = "SELECT id, activated, status FROM user WHERE id IN ?" - richInfoSelect :: PrepQuery R (Identity UserId) (Identity RichInfoAssocList) richInfoSelect = "SELECT json FROM rich_info WHERE user = ?" diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 96a1c81341b..9be924b3866 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -743,9 +743,6 @@ instance ToJSON AccountFeatureConfigs where getAfcConferenceCallingDefNewMaybe :: Lens.Getter Settings (Maybe (Public.WithStatus Public.ConferenceCallingConfig)) getAfcConferenceCallingDefNewMaybe = Lens.to (Lens.^? (Lens.to setFeatureFlags . Lens._Just . Lens.to afcConferenceCallingDefNew . unImplicitLockStatus)) -getAfcConferenceCallingDefNew :: Lens.Getter Settings (Public.WithStatus Public.ConferenceCallingConfig) -getAfcConferenceCallingDefNew = Lens.to (Public._unImplicitLockStatus . afcConferenceCallingDefNew . fromMaybe defAccountFeatureConfigs . setFeatureFlags) - getAfcConferenceCallingDefNull :: Lens.Getter Settings (Public.WithStatus Public.ConferenceCallingConfig) getAfcConferenceCallingDefNull = Lens.to (Public._unImplicitLockStatus . afcConferenceCallingDefNull . fromMaybe defAccountFeatureConfigs . setFeatureFlags) diff --git a/services/gundeck/src/Gundeck/Monad.hs b/services/gundeck/src/Gundeck/Monad.hs index 5320f725501..1ccce16a55b 100644 --- a/services/gundeck/src/Gundeck/Monad.hs +++ b/services/gundeck/src/Gundeck/Monad.hs @@ -32,7 +32,6 @@ module Gundeck.Monad runDirect, runGundeck, fromJsonBody, - ifNothing, posixTime, -- * Select which redis to target @@ -206,10 +205,6 @@ fromJsonBody :: (FromJSON a) => JsonRequest a -> Gundeck a fromJsonBody r = exceptT (throwM . mkError status400 "bad-request") pure (parseBody r) {-# INLINE fromJsonBody #-} -ifNothing :: Error -> Maybe a -> Gundeck a -ifNothing e = maybe (throwM e) pure -{-# INLINE ifNothing #-} - posixTime :: Gundeck Milliseconds posixTime = view time >>= liftIO {-# INLINE posixTime #-} diff --git a/services/spar/test-integration/Util/Types.hs b/services/spar/test-integration/Util/Types.hs index 777470f2bb2..553ed09674e 100644 --- a/services/spar/test-integration/Util/Types.hs +++ b/services/spar/test-integration/Util/Types.hs @@ -43,7 +43,6 @@ where import Bilge import Cassandra as Cas -import Control.Exception import Control.Lens (makeLenses, view) import Crypto.Random.Types (MonadRandom (..)) import Data.Aeson @@ -111,13 +110,13 @@ instance FromJSON TestErrorLabel where -- A quick unit test that serves two purposes: (1) shows that it works (and helped with debugging); -- (2) demonstrates how to use it. -_unitTestTestErrorLabel :: IO () -_unitTestTestErrorLabel = do - let val :: Either String TestErrorLabel - val = Aeson.eitherDecode "{\"code\":404,\"message\":\"Not found.\",\"label\":\"not-found\"}" - unless (val == Right "not-found") $ - throwIO . ErrorCall . show $ - val +-- _unitTestTestErrorLabel :: IO () +-- _unitTestTestErrorLabel = do +-- let val :: Either String TestErrorLabel +-- val = Aeson.eitherDecode "{\"code\":404,\"message\":\"Not found.\",\"label\":\"not-found\"}" +-- unless (val == Right "not-found") $ +-- throwIO . ErrorCall . show $ +-- val -- | FUTUREWORK(fisx): we're running all tests for all constructors of `WireIdPAPIVersion`, -- which sometimes makes little sense. 'skipIdPAPIVersions' can be used to pend individual diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index b52d262f142..fab41b2971a 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -21,13 +21,11 @@ module Stern.API.Routes SwaggerDocsAPI, swaggerDocs, UserConnectionGroups (..), - doubleMaybeToEither, RedirectToSwaggerDocsAPI, ) where import Control.Lens -import Control.Monad.Trans.Except import Data.Aeson qualified as A import Data.Handle import Data.Id @@ -35,8 +33,6 @@ import Data.Kind import Data.OpenApi qualified as S import Data.Schema qualified as Schema import Imports hiding (head) -import Network.HTTP.Types.Status -import Network.Wai.Utilities import Servant hiding (Handler, WithStatus (..), addHeader, respond) import Servant.OpenApi (HasOpenApi (toOpenApi)) import Servant.OpenApi.Internal.Orphans () @@ -482,11 +478,6 @@ instance Schema.ToSchema UserConnectionGroups where <*> ucgMissingLegalholdConsent Schema..= Schema.field "ucgMissingLegalholdConsent" Schema.schema <*> ucgTotal Schema..= Schema.field "ucgTotal" Schema.schema -doubleMaybeToEither :: (Monad m) => LText -> Maybe a -> Maybe b -> ExceptT Error m (Either a b) -doubleMaybeToEither _ (Just a) Nothing = pure $ Left a -doubleMaybeToEither _ Nothing (Just b) = pure $ Right b -doubleMaybeToEither msg _ _ = throwE $ mkError status400 "either-params" ("Must use exactly one of two query params: " <> msg) - type MkFeatureGetRoute (feature :: Type) = Summary "Shows whether a feature flag is enabled or not for a given team." :> "teams" diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 326f5ebf821..8baf5875930 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -34,7 +34,6 @@ module Stern.Intra getInvoiceUrl, revokeIdentity, changeEmail, - changePhone, deleteAccount, setStatusBindingTeam, deleteBindingTeam, @@ -388,24 +387,6 @@ changeEmail u upd = do . expect2xx ) -changePhone :: UserId -> PhoneUpdate -> Handler () -changePhone u upd = do - info $ msg "Updating phone number" - b <- view brig - void - . catchRpcErrors - $ rpc' - "brig" - b - ( method PUT - . versionedPath "self/phone" - . header "Z-User" (toByteString' u) - . header "Z-Connection" (toByteString' "") - . lbytes (encode upd) - . contentJson - . expect2xx - ) - getTeamInfo :: TeamId -> Handler TeamInfo getTeamInfo tid = do d <- getTeamData tid diff --git a/weeder.toml b/weeder.toml index 3b2d1056098..9521cfeee9c 100644 --- a/weeder.toml +++ b/weeder.toml @@ -10,9 +10,12 @@ roots = [ # may of the entries here are about general-purpose module "^API.Galley.consentToLegalHold", # FUTUREWORK: write tests that need this! "^API.Galley.enableLegalHold", # FUTUREWORK: write tests that need this! "^API.Galley.getLegalHoldStatus", # FUTUREWORK: write tests that need this! + "^API.MLS.Util.getCurrentGroupId", "^API.MLS.Util.getKeyPackageCount", "^API.MLS.Util.getKeyPair", - "^API.MLS.Util.getCurrentGroupId", + "^API.Nginz.*$", # FUTUREWORK: consider using everything or cleaning up. + "^Bilge.*$", + "^Cassandra.Helpers.toOptionFieldName", "^Data.ETag._OpaqueDigest", "^Data.ETag._StrictETag", "^Data.ETag._WeakETag", @@ -22,18 +25,88 @@ roots = [ # may of the entries here are about general-purpose module "^Data.ETag.strictETag", "^Data.ETag.weakETag", "^Data.Qualified.isLocal", + "^Data.Range.*$", "^Data.Range.rappend", "^Data.Range.rcons", "^Data.Range.rinc", "^Data.Range.rsingleton", + "^Data.ZAuth.Validation.*$", + "^Galley.Types.UserList.ulDiff", + "^HTTP2.Client.Manager.*$", "^Imports.getChar", "^Imports.getContents", "^Imports.interact", "^Imports.putChar", "^Imports.readIO", "^Imports.readLn", + "^Main.debugMainDebugExportFull", # move-team + "^Main.debugMainExport", # move-team + "^Main.debugMainImport", # move-team "^Main.main$", + "^Network.Wai.Utilities.ZAuth.*$", + "^Notifications.*$", # new integration tests + "^ParseSchema._printAllTables", + "^ParseSchema.debug", + "^ParseSchema.debugwrite", + "^ParseSchema.projectFile", "^Paths_.*", + "^Proto.Mls_Fields.commit", + "^Proto.Mls_Fields.groupInfo", + "^Proto.Mls_Fields.groupInfoBundle", + "^Proto.Mls_Fields.groupInfoType", + "^Proto.Mls_Fields.ignoreAll", + "^Proto.Mls_Fields.ignoreOnly", + "^Proto.Mls_Fields.isInline", + "^Proto.Mls_Fields.maybe'blob", + "^Proto.Mls_Fields.maybe'ignoreAll", + "^Proto.Mls_Fields.maybe'ignoreOnly", + "^Proto.Mls_Fields.maybe'isInline", + "^Proto.Mls_Fields.maybe'nativePush", + "^Proto.Mls_Fields.maybe'reportAll", + "^Proto.Mls_Fields.maybe'reportOnly", + "^Proto.Mls_Fields.maybe'transient", + "^Proto.Mls_Fields.maybe'welcome", + "^Proto.Mls_Fields.nativePriority", + "^Proto.Mls_Fields.ratchetTreeType", + "^Proto.Mls_Fields.reportAll", + "^Proto.Mls_Fields.reportMissing", + "^Proto.Mls_Fields.reportOnly", + "^Proto.Mls_Fields.vec'clients", + "^Proto.Mls_Fields.vec'entries", + "^Proto.Mls_Fields.vec'recipients", + "^Proto.Mls_Fields.vec'reportMissing", + "^Proto.Mls_Fields.vec'userIds", + "^Proto.Mls_Fields.welcome", + "^Proto.Otr._QualifiedNewOtrMessage'IgnoreAll", + "^Proto.Otr._QualifiedNewOtrMessage'IgnoreOnly", + "^Proto.Otr._QualifiedNewOtrMessage'ReportAll", + "^Proto.Otr._QualifiedNewOtrMessage'ReportOnly", + "^Proto.Otr_Fields.ignoreAll", + "^Proto.Otr_Fields.ignoreOnly", + "^Proto.Otr_Fields.isInline", + "^Proto.Otr_Fields.maybe'blob", + "^Proto.Otr_Fields.maybe'ignoreAll", + "^Proto.Otr_Fields.maybe'ignoreOnly", + "^Proto.Otr_Fields.maybe'isInline", + "^Proto.Otr_Fields.maybe'nativePush", + "^Proto.Otr_Fields.maybe'reportAll", + "^Proto.Otr_Fields.maybe'reportOnly", + "^Proto.Otr_Fields.maybe'transient", + "^Proto.Otr_Fields.nativePriority", + "^Proto.Otr_Fields.reportAll", + "^Proto.Otr_Fields.reportMissing", + "^Proto.Otr_Fields.reportOnly", + "^Proto.Otr_Fields.vec'clients", + "^Proto.Otr_Fields.vec'entries", + "^Proto.Otr_Fields.vec'recipients", + "^Proto.Otr_Fields.vec'reportMissing", + "^Proto.Otr_Fields.vec'userIds", + "^Proto.TeamEvents_Fields.currency", + "^Proto.TeamEvents_Fields.vec'billingUser", + "^Spar.Sem.AReqIDStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? + "^Spar.Sem.AssIDStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? + "^Spar.Sem.ScimTokenStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? + "^Spar.Sem.VerdictFormatStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? "^Spec.main$", "^Test.Cargohold.API.Util.shouldMatchALittle", "^Test.Cargohold.API.Util.shouldMatchLeniently", @@ -41,6 +114,7 @@ roots = [ # may of the entries here are about general-purpose module "^Test.Data.Schema.detailSchema", "^Test.Data.Schema.userSchemaWithDefaultName", "^Test.Data.Schema.userSchemaWithDefaultName'", + "^Test.Wire.API.Golden.Run.main$", "^TestSetup.runFederationClient", "^TestSetup.viewCargohold", "^Testlib.Cannon.awaitAtLeastNMatches", @@ -51,18 +125,9 @@ roots = [ # may of the entries here are about general-purpose module "^Testlib.Cannon.printAwaitAtLeastResult", "^Testlib.Cannon.printAwaitResult", "^Testlib.JSON.member", - "^Testlib.Prelude.appendFile", - "^Testlib.Prelude.getChar", - "^Testlib.Prelude.getContents", - "^Testlib.Prelude.getLine", - "^Testlib.Prelude.interact", - "^Testlib.Prelude.print", - "^Testlib.Prelude.putChar", - "^Testlib.Prelude.putStr", - "^Testlib.Prelude.readFile", - "^Testlib.Prelude.readIO", - "^Testlib.Prelude.readLn", - "^Testlib.Prelude.writeFile", + "^Testlib.JSON.printJSON", + "^Testlib.JSON.traceJSON", + "^Testlib.Prelude.*$", # FUTUREWORK: consider making them individually "^Testlib.Printing.gray", "^Testlib.Printing.hline", "^Testlib.Run.main$", @@ -71,6 +136,7 @@ roots = [ # may of the entries here are about general-purpose module "^Util.assertOne", "^Util.randomActivationCode", "^Util.zClient", + "^Web.Scim.*$", "^Web.Scim.Client.deleteGroup", "^Web.Scim.Client.deleteUser", "^Web.Scim.Client.getGroup", @@ -92,7 +158,9 @@ roots = [ # may of the entries here are about general-purpose module "^Web.Scim.Test.Util.put'", "^Web.Scim.Test.Util.scim", "^Web.Scim.Test.Util.shouldEventuallyRespondWith", - "^Test.Wire.API.Golden.Run.main$" + "^Wire.API.MLS.Serialisation.traceMLS", # Debug + "^Wire.Sem.Concurrency.IO.performConcurrency", + "^Wire.Sem.Logger.fatal" ] type-class-roots = true # `root-instances` is more precise, but requires more config maintenance. From 53ed5918d8ef9ece2dbc1310e14c894ced03b3ff Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 2 Aug 2024 08:56:51 +0200 Subject: [PATCH 023/136] Remove debug statement (#4180) --- integration/test/Test/Property.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/integration/test/Test/Property.hs b/integration/test/Test/Property.hs index 4440c6f8983..40356c305bd 100644 --- a/integration/test/Test/Property.hs +++ b/integration/test/Test/Property.hs @@ -125,8 +125,6 @@ testMaxLength = do tooLongValue <- randomHandleWithRange (maxValLength - 1) (maxValLength - 1) acceptableValue <- randomHandleWithRange (maxValLength - 2) (maxValLength - 2) - putStrLn $ "acceptableValue= " <> acceptableValue - setProperty user tooLongProperty acceptableValue `bindResponse` \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "property-key-too-large" From 1e97149f1d6487214708e1c420f6863d2c057d80 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 5 Aug 2024 13:48:33 +0200 Subject: [PATCH 024/136] One to one SFT feature flag config (#4164) * Restore weeded-out wsPatch * Add field to ConferenceCallingConfig * Use patch endpoint to set feature status in stern * Remove FeatureTrivialConfig class * Add migration to add conference calling sft flag * Implement get/set conference calling feature flag * Fix golden tests * Add endpoint to put ConferenceCallingConfig. * Added lock status to conf. calling. WIP: Tests need fixing sinnce it's no longer a SimpleFlag. * Fix golden test cases * Update conference call ttl test * Fix conference calling patch test * Update CHANGELOG entry * Fix assertions in conferenceCalling TTL test * Move user feature test to integration Also remove its dependency on Cassandra, simplify logic and expand its scope to include team users. * Remove dead code * Update cassandra schema file * Change how conferenceCalling is stored and loaded * Add general AllFeatures for any type constructor * Return Maybe values from FeatureStore * Compute feature values in a uniform manner * Unify logic for fetching features from db * Use adhoc DbFeature type instead of WithStatusBase * Fix default logic of MlsE2EI flag * Implement getAllFeatureConfigs * Lint * Change conference option from boolean to int * Repurpose conference_calling column for lock status Also add a new column to hold the feature status * Ignore TTL for conferenceCalling Now the TTL field is completely ignored when writing for all feature flags. We will get rid of the TTL code in a future refactoring. * Add default logic for conference calling * Remove feature ttl tests * Add CHANGELOG entry about TTL * Add note about unsettable features * Lint * Conference calling flag should be locked by default * Make lockStatus field optional in galley's conf It used to be implicit before, now it is a normal flag, so we need to make this field optional to preserve compatibility with older configuration files. * Remove unused ToJSON instance * Lint * Align conferenceCalling setting in CI * Fix stern integration tests * Update cassandra schema * Use bindResponse in tests * Revert default lock status when parsing feature Instead of hardcoding an unlocked status when parsing the conferenceCalling default configuration, set the default in the helm chart. --------- Co-authored-by: Igor Ranieri --- cassandra-schema.cql | 2 + changelog.d/1-api-changes/ttl | 1 + changelog.d/1-api-changes/wpb-10235 | 1 + charts/galley/values.yaml | 1 + hack/helm_vars/wire-server/values.yaml.gotmpl | 9 + integration/integration.cabal | 1 + integration/test/API/Brig.hs | 2 +- integration/test/API/BrigInternal.hs | 29 +- integration/test/API/Galley.hs | 4 + integration/test/API/GalleyInternal.hs | 8 + integration/test/Test/FeatureFlags.hs | 183 ++++---- integration/test/Test/FeatureFlags/User.hs | 78 ++++ integration/test/Test/FeatureFlags/Util.hs | 45 +- libs/galley-types/default.nix | 11 - libs/galley-types/galley-types.cabal | 71 --- libs/galley-types/src/Galley/Types/Teams.hs | 47 +- libs/galley-types/test/unit/Main.hs | 28 -- .../test/unit/Test/Galley/Roundtrip.hs | 36 -- .../test/unit/Test/Galley/Types.hs | 59 --- .../src/Wire/API/Routes/Internal/Galley.hs | 1 + .../Wire/API/Routes/Public/Galley/Feature.hs | 1 + libs/wire-api/src/Wire/API/Team/Feature.hs | 246 ++++++---- .../golden/Test/Wire/API/Golden/FromJSON.hs | 5 +- .../Golden/Generated/WithStatusNoLock_team.hs | 2 +- .../Golden/Generated/WithStatusPatch_team.hs | 2 +- .../API/Golden/Generated/WithStatus_team.hs | 2 +- .../testObject_WithStatus_team_14.json | 5 + .../testObject_WithStatusNoLock_team_14.json | 5 +- .../testObject_WithStatusPatch_team_14.json | 4 +- .../golden/testObject_WithStatus_team_14.json | 5 +- services/brig/brig.integration.yaml | 5 + services/brig/src/Brig/Calling/API.hs | 2 +- services/brig/src/Brig/Options.hs | 6 +- .../brig/test/integration/API/Internal.hs | 92 +--- services/galley/galley.cabal | 1 + services/galley/galley.integration.yaml | 3 +- services/galley/src/Galley/API/Internal.hs | 1 + .../galley/src/Galley/API/LegalHold/Team.hs | 37 +- .../galley/src/Galley/API/Public/Feature.hs | 1 + services/galley/src/Galley/API/Query.hs | 13 +- .../galley/src/Galley/API/Teams/Features.hs | 51 ++- .../src/Galley/API/Teams/Features/Get.hs | 272 +++++++---- .../Cassandra/GetAllTeamFeatureConfigs.hs | 426 +++++++++--------- .../src/Galley/Cassandra/TeamFeatures.hs | 244 +++------- .../src/Galley/Effects/TeamFeatureStore.hs | 7 +- services/galley/src/Galley/Schema/Run.hs | 4 +- .../V93_ConferenceCallingSftForOneToOne.hs | 16 + .../Test/Spar/Scim/AuthSpec.hs | 2 +- services/spar/test-integration/Util/Core.hs | 4 +- services/spar/test-integration/Util/Email.hs | 2 +- tools/stern/src/Stern/API.hs | 13 +- tools/stern/src/Stern/API/Routes.hs | 26 +- tools/stern/src/Stern/Intra.hs | 73 +-- tools/stern/test/integration/API.hs | 12 +- 54 files changed, 1091 insertions(+), 1116 deletions(-) create mode 100644 changelog.d/1-api-changes/ttl create mode 100644 changelog.d/1-api-changes/wpb-10235 create mode 100644 integration/test/Test/FeatureFlags/User.hs delete mode 100644 libs/galley-types/test/unit/Main.hs delete mode 100644 libs/galley-types/test/unit/Test/Galley/Roundtrip.hs delete mode 100644 libs/galley-types/test/unit/Test/Galley/Types.hs create mode 100644 libs/wire-api/test/golden/fromJSON/testObject_WithStatus_team_14.json create mode 100644 services/galley/src/Galley/Schema/V93_ConferenceCallingSftForOneToOne.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index f34be3f2041..fbc45dc57bb 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -1170,6 +1170,8 @@ CREATE TABLE galley_test.team_features ( app_lock_inactivity_timeout_secs int, app_lock_status int, conference_calling int, + conference_calling_one_to_one int, + conference_calling_status int, digital_signatures int, enforce_file_download_location text, enforce_file_download_location_lock_status int, diff --git a/changelog.d/1-api-changes/ttl b/changelog.d/1-api-changes/ttl new file mode 100644 index 00000000000..5a9d4711e68 --- /dev/null +++ b/changelog.d/1-api-changes/ttl @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000000..0dce921d998 --- /dev/null +++ b/changelog.d/1-api-changes/wpb-10235 @@ -0,0 +1 @@ +Add useSFTForOneToOneCalls as a config option for the Conference Calling feature flag and make its lock status explicit. diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 1d170d39883..f6bda0eb643 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -80,6 +80,7 @@ config: conferenceCalling: defaults: status: enabled + lockStatus: locked conversationGuestLinks: defaults: lockStatus: unlocked diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 5ca735fe38e..2ed14739f79 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -115,6 +115,12 @@ brig: setMaxConvSize: 16 # See helmfile for the real value setFederationDomain: integration.example.com + setFeatureFlags: + conferenceCalling: + defaultForNew: + status: disabled + defaultForNull: + status: disabled setFederationStrategy: allowAll setFederationDomainConfigsUpdateFreq: 10 setDisabledAPIVersions: [] @@ -256,6 +262,9 @@ galley: sso: disabled-by-default # this needs to be the default; tests can enable it when needed. legalhold: whitelist-teams-and-implicit-consent teamSearchVisibility: disabled-by-default + conferenceCalling: + defaults: + status: disabled classifiedDomains: status: enabled config: diff --git a/integration/integration.cabal b/integration/integration.cabal index 9a212d87b2a..d87c87a1f7e 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -123,6 +123,7 @@ library Test.Errors Test.ExternalPartner Test.FeatureFlags + Test.FeatureFlags.User Test.FeatureFlags.Util Test.Federation Test.Federator diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index d88cafb9187..898afda288e 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -657,7 +657,7 @@ getCallsConfigV2 user = do req <- baseRequest user Brig Versioned $ joinHttpPath ["calls", "config", "v2"] submit "GET" req -addBot :: (MakesValue user) => user -> String -> String -> String -> App Response +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"] submit "POST" $ diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index cb5be7d48c0..ccdeb10224c 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -244,10 +244,37 @@ getEJPDInfo dom handles mode = do bad -> error $ show bad submit "POST" $ req & addJSONObject ["EJPDRequest" .= handles] & addQueryParams query --- https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/get_i_users__uid__verification_code__action_ +-- | https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/get_i_users__uid__verification_code__action_ getVerificationCode :: (HasCallStack, MakesValue user) => user -> String -> App Response getVerificationCode user action = do uid <- objId user domain <- objDomain user req <- baseRequest domain Brig Unversioned $ joinHttpPath ["i", "users", uid, "verification-code", action] submit "GET" req + +-- | http://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/get_i_users__uid__features_conferenceCalling +getFeatureForUser :: (HasCallStack, MakesValue user) => user -> String -> App Response +getFeatureForUser user featureName = do + uid <- objId user + req <- baseRequest user Brig Unversioned $ joinHttpPath ["i", "users", uid, "features", featureName] + submit "GET" req + +-- | http://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/put_i_users__uid__features_conferenceCalling +putFeatureForUser :: + (HasCallStack, MakesValue user, MakesValue config) => + user -> + String -> + config -> + App Response +putFeatureForUser user featureName config = do + uid <- objId user + req <- baseRequest user Brig Unversioned $ joinHttpPath ["i", "users", uid, "features", featureName] + configValue <- make config + submit "PUT" $ req & addJSON configValue + +-- | http://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/delete_i_users__uid__features_conferenceCalling +deleteFeatureForUser :: (HasCallStack, MakesValue user) => user -> String -> App Response +deleteFeatureForUser user featureName = do + uid <- objId user + req <- baseRequest user Brig Unversioned $ joinHttpPath ["i", "users", uid, "features", featureName] + submit "DELETE" req diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 14861a26f04..4b04bb65bbf 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -705,3 +705,7 @@ setTeamFeatureConfigVersioned versioned user team featureName payload = do p <- make payload req <- baseRequest user Galley versioned $ joinHttpPath ["teams", tid, "features", fn] submit "PUT" $ req & addJSON p + +-- | http://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_feature_configs +getFeaturesForUser :: (HasCallStack, MakesValue user) => user -> App Response +getFeaturesForUser user = baseRequest user Galley Versioned "feature-configs" >>= submit "GET" diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index ef0f773d426..de6f5c21c47 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -106,6 +106,14 @@ setTeamFeatureConfig domain team featureName payload = do req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", fn] submit "PUT" $ req & addJSON p +patchTeamFeatureConfig :: (HasCallStack, MakesValue domain, MakesValue team, MakesValue featureName, MakesValue payload) => domain -> team -> featureName -> payload -> App Response +patchTeamFeatureConfig domain team featureName payload = do + tid <- asString team + fn <- asString featureName + p <- make payload + req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", fn] + submit "PATCH" $ req & addJSON p + -- https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/galley/#/galley/post_i_features_multi_teams_searchVisibilityInbound getFeatureStatusMulti :: (HasCallStack, MakesValue domain, MakesValue featureName) => domain -> featureName -> [String] -> App Response getFeatureStatusMulti domain featureName tids = do diff --git a/integration/test/Test/FeatureFlags.hs b/integration/test/Test/FeatureFlags.hs index e0943931f9e..a0d274b9a2e 100644 --- a/integration/test/Test/FeatureFlags.hs +++ b/integration/test/Test/FeatureFlags.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2023 Wire Swiss GmbH @@ -19,14 +21,12 @@ module Test.FeatureFlags where import qualified API.Galley as Public import qualified API.GalleyInternal as Internal -import Control.Concurrent (threadDelay) import Control.Monad.Codensity (Codensity (runCodensity)) import Control.Monad.Reader import qualified Data.Aeson as A import qualified Data.Aeson.Key as A import qualified Data.Aeson.KeyMap as KM import qualified Data.Set as Set -import Data.String.Conversions (cs) import Notifications import SetupHelpers import Test.FeatureFlags.Util @@ -295,9 +295,6 @@ testDigitalSignaturesInternal = _testSimpleFlag "digitalSignatures" Internal.set testValidateSAMLEmailsInternal :: (HasCallStack) => App () testValidateSAMLEmailsInternal = _testSimpleFlag "validateSAMLemails" Internal.setTeamFeatureConfig True -testConferenceCallingInternal :: (HasCallStack) => App () -testConferenceCallingInternal = _testSimpleFlag "conferenceCalling" Internal.setTeamFeatureConfig True - testSearchVisibilityInboundInternal :: (HasCallStack) => App () testSearchVisibilityInboundInternal = _testSimpleFlag "searchVisibilityInbound" Internal.setTeamFeatureConfig False @@ -313,16 +310,16 @@ _testSimpleFlag featureName setFeatureConfig featureEnabledByDefault = do assertForbidden =<< Public.getTeamFeature nonTeamMember tid featureName checkFeature featureName m tid defaultValue -- should receive an event - void $ withWebSockets [m] $ \wss -> do + void $ withWebSocket m $ \ws -> do assertSuccess =<< setFeatureConfig owner tid featureName (object ["status" .= otherStatus]) - for_ wss $ \ws -> do + do notif <- awaitMatch isFeatureConfigUpdateNotif ws notif %. "payload.0.name" `shouldMatch` featureName notif %. "payload.0.data" `shouldMatch` otherValue checkFeature featureName m tid otherValue assertSuccess =<< setFeatureConfig owner tid featureName (object ["status" .= defaultStatus]) - for_ wss $ \ws -> do + do notif <- awaitMatch isFeatureConfigUpdateNotif ws notif %. "payload.0.name" `shouldMatch` featureName notif %. "payload.0.data" `shouldMatch` defaultValue @@ -376,12 +373,11 @@ _testSimpleFlagWithLockStatus featureName setFeatureConfig featureEnabledByDefau -- change the status let otherValue = if featureEnabledByDefault then disabled else enabled - void $ withWebSockets [m] $ \wss -> do + void $ withWebSocket m $ \ws -> do assertSuccess =<< setFeatureConfig owner tid featureName (object ["status" .= otherStatus]) - for_ wss $ \ws -> do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` otherValue + notif <- awaitMatch isFeatureConfigUpdateNotif ws + notif %. "payload.0.name" `shouldMatch` featureName + notif %. "payload.0.data" `shouldMatch` otherValue checkFeature featureName m tid otherValue @@ -424,18 +420,21 @@ testClassifiedDomainsDisabled = do testAllFeatures :: (HasCallStack) => App () testAllFeatures = do (_, tid, m : _) <- createTeam OwnDomain 2 - let expected = + let defEnabledObj :: Value -> Value + defEnabledObj conf = object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= conf] + expected = object $ [ "legalhold" .= disabled, "sso" .= disabled, "searchVisibility" .= disabled, "validateSAMLemails" .= enabled, "digitalSignatures" .= disabled, - "appLock" .= object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= object ["enforceAppLock" .= False, "inactivityTimeoutSecs" .= A.Number 60]], + "appLock" .= defEnabledObj (object ["enforceAppLock" .= False, "inactivityTimeoutSecs" .= A.Number 60]), "fileSharing" .= enabled, - "classifiedDomains" .= object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= object ["domains" .= ["example.com"]]], - "conferenceCalling" .= enabled, - "selfDeletingMessages" .= object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0]], + "classifiedDomains" .= defEnabledObj (object ["domains" .= ["example.com"]]), + "conferenceCalling" .= confCalling def {lockStatus = Just "locked"}, + "selfDeletingMessages" + .= defEnabledObj (object ["enforcedTimeoutSeconds" .= A.Number 0]), "conversationGuestLinks" .= enabled, "sndFactorPasswordChallenge" .= disabledLocked, "mls" @@ -787,6 +786,59 @@ testMLSE2EIdInternal = do cfg2 invalidCfg' +testConferenceCalling :: (HasCallStack) => App () +testConferenceCalling = do + _testLockStatusWithConfig + "conferenceCalling" + Public.setTeamFeatureConfig + (confCalling def {lockStatus = Just "locked"}) + (confCalling def {sft = toJSON True}) + (confCalling def) + (confCalling def {sft = toJSON (0 :: Int)}) + +testConferenceCallingInternal :: (HasCallStack) => App () +testConferenceCallingInternal = do + let defaultArgs = def {lockStatus = Just "locked"} + + (owner, tid, m : _) <- createTeam OwnDomain 2 + nonTeamMember <- randomUser OwnDomain def + assertForbidden =<< Public.getTeamFeature nonTeamMember tid "conferenceCalling" + checkFeature "conferenceCalling" m tid (confCalling defaultArgs) + + void $ withWebSocket m $ \ws -> do + -- unlock and enable + assertSuccess =<< Internal.patchTeamFeatureConfig owner tid "conferenceCalling" (object ["status" .= "enabled", "lockStatus" .= "unlocked"]) + do + notif <- awaitMatch isFeatureConfigUpdateNotif ws + notif %. "payload.0.name" `shouldMatch` "conferenceCalling" + -- TODO: the patch event is currently wrong, and does not reflect the update + notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs {status = "disabled", lockStatus = Just "locked"}) + checkFeature "conferenceCalling" m tid (confCalling defaultArgs {status = "enabled", lockStatus = Just "unlocked"}) + + -- just disable + assertSuccess =<< Internal.setTeamFeatureConfig owner tid "conferenceCalling" (confCalling def {status = "disabled"}) + do + notif <- awaitMatch isFeatureConfigUpdateNotif ws + notif %. "payload.0.name" `shouldMatch` "conferenceCalling" + notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs {status = "disabled", lockStatus = Just "unlocked"}) + checkFeature "conferenceCalling" m tid (confCalling defaultArgs {lockStatus = Just "unlocked"}) + + -- re-enable + assertSuccess =<< Internal.setTeamFeatureConfig owner tid "conferenceCalling" (confCalling def {status = "enabled"}) + do + notif <- awaitMatch isFeatureConfigUpdateNotif ws + notif %. "payload.0.name" `shouldMatch` "conferenceCalling" + notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs {status = "enabled", lockStatus = Just "unlocked"}) + checkFeature "conferenceCalling" m tid (confCalling defaultArgs {status = "enabled", lockStatus = Just "unlocked"}) + + -- restore initial state + assertSuccess =<< Internal.patchTeamFeatureConfig owner tid "conferenceCalling" (object ["status" .= "disabled", "lockStatus" .= "locked"]) + do + notif <- awaitMatch isFeatureConfigUpdateNotif ws + notif %. "payload.0.name" `shouldMatch` "conferenceCalling" + notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs {lockStatus = Just "unlocked"}) + checkFeature "conferenceCalling" m tid (confCalling defaultArgs) + _testLockStatusWithConfig :: (HasCallStack) => String -> @@ -835,15 +887,18 @@ _testLockStatusWithConfigWithTeam (owner, tid, m) featureName setTeamFeatureConf -- lock the feature Internal.setTeamFeatureLockStatus OwnDomain tid featureName "locked" + bindResponse (Public.getTeamFeature owner tid featureName) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "lockStatus" `shouldMatch` "locked" + assertStatus 409 =<< setTeamFeatureConfig owner tid featureName config1 Internal.setTeamFeatureLockStatus OwnDomain tid featureName "unlocked" - void $ withWebSockets [m] $ \wss -> do + void $ withWebSocket m $ \ws -> do assertSuccess =<< setTeamFeatureConfig owner tid featureName config1 - for_ wss $ \ws -> do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` (config1 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") + notif <- awaitMatch isFeatureConfigUpdateNotif ws + notif %. "payload.0.name" `shouldMatch` featureName + notif %. "payload.0.data" `shouldMatch` (config1 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") checkFeature featureName m tid =<< (config1 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") @@ -851,18 +906,17 @@ _testLockStatusWithConfigWithTeam (owner, tid, m) featureName setTeamFeatureConf checkFeature featureName m tid =<< setField "lockStatus" "locked" defaultFeatureConfig Internal.setTeamFeatureLockStatus OwnDomain tid featureName "unlocked" - void $ withWebSockets [m] $ \wss -> do + void $ withWebSocket m $ \ws -> do assertStatus 400 =<< setTeamFeatureConfig owner tid featureName invalidConfig - for_ wss $ assertNoEvent 2 + assertNoEvent 2 ws checkFeature featureName m tid =<< (config1 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") - void $ withWebSockets [m] $ \wss -> do + void $ withWebSocket m $ \ws -> do assertSuccess =<< setTeamFeatureConfig owner tid featureName config2 - for_ wss $ \ws -> do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` (config2 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") + notif <- awaitMatch isFeatureConfigUpdateNotif ws + notif %. "payload.0.name" `shouldMatch` featureName + notif %. "payload.0.data" `shouldMatch` (config2 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") checkFeature featureName m tid =<< (config2 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") @@ -879,64 +933,6 @@ testFeatureNoConfigMultiSearchVisibilityInbound = do length statuses `shouldMatchInt` 2 statuses `shouldMatchSet` [object ["team" .= team1, "status" .= "disabled"], object ["team" .= team2, "status" .= "enabled"]] -testConferenceCallingTTLIncreaseToUnlimited :: (HasCallStack) => App () -testConferenceCallingTTLIncreaseToUnlimited = _testSimpleFlagTTLOverride "conferenceCalling" True (Just 2) Nothing - -testConferenceCallingTTLIncrease :: (HasCallStack) => App () -testConferenceCallingTTLIncrease = _testSimpleFlagTTLOverride "conferenceCalling" True (Just 2) (Just 4) - -testConferenceCallingTTLReduceFromUnlimited :: (HasCallStack) => App () -testConferenceCallingTTLReduceFromUnlimited = _testSimpleFlagTTLOverride "conferenceCalling" True Nothing (Just 2) - -testConferenceCallingTTLReduce :: (HasCallStack) => App () -testConferenceCallingTTLReduce = _testSimpleFlagTTLOverride "conferenceCalling" True (Just 5) (Just 2) - -testConferenceCallingTTLUnlimitedToUnlimited :: (HasCallStack) => App () -testConferenceCallingTTLUnlimitedToUnlimited = _testSimpleFlagTTLOverride "conferenceCalling" True Nothing Nothing - -_testSimpleFlagTTLOverride :: (HasCallStack) => String -> Bool -> Maybe Int -> Maybe Int -> App () -_testSimpleFlagTTLOverride featureName enabledByDefault mTtl mTtlAfter = do - let ttl = maybe (A.String . cs $ "unlimited") (A.Number . fromIntegral) mTtl - let ttlAfter = maybe (A.String . cs $ "unlimited") (A.Number . fromIntegral) mTtlAfter - (owner, tid, _) <- createTeam OwnDomain 0 - let (defaultValue, otherValue) = if enabledByDefault then ("enabled", "disabled") else ("disabled", "enabled") - - -- Initial value should be the default value - let defFeatureStatus = object ["status" .= defaultValue, "ttl" .= "unlimited", "lockStatus" .= "unlocked"] - checkFeature featureName owner tid defFeatureStatus - - -- Setting should work - assertSuccess =<< Internal.setTeamFeatureConfig OwnDomain tid featureName (object ["status" .= otherValue, "ttl" .= ttl]) - checkFeatureLenientTtl featureName owner tid (object ["status" .= otherValue, "ttl" .= ttl, "lockStatus" .= "unlocked"]) - - case (mTtl, mTtlAfter) of - (Just d, Just d') -> do - -- wait less than expiration, override and recheck. - liftIO $ threadDelay (d * 1000000 `div` 2) -- waiting half of TTL - -- setFlagInternal otherValue ttlAfter - assertSuccess =<< Internal.setTeamFeatureConfig OwnDomain tid featureName (object ["status" .= otherValue, "ttl" .= ttlAfter]) - -- value is still correct - checkFeatureLenientTtl featureName owner tid (object ["status" .= otherValue, "ttl" .= ttlAfter, "lockStatus" .= "unlocked"]) - - liftIO $ threadDelay (d' * 1000000) -- waiting for new TTL - checkFeatureLenientTtl featureName owner tid defFeatureStatus - (Just d, Nothing) -> do - -- wait less than expiration, override and recheck. - liftIO $ threadDelay (d * 1000000 `div` 2) -- waiting half of TTL - assertSuccess =<< Internal.setTeamFeatureConfig OwnDomain tid featureName (object ["status" .= otherValue, "ttl" .= ttlAfter]) - -- value is still correct - checkFeatureLenientTtl featureName owner tid (object ["status" .= otherValue, "ttl" .= ttlAfter, "lockStatus" .= "unlocked"]) - (Nothing, Nothing) -> do - -- overriding in this case should have no effect. - assertSuccess =<< Internal.setTeamFeatureConfig OwnDomain tid featureName (object ["status" .= otherValue, "ttl" .= ttl]) - checkFeatureLenientTtl featureName owner tid (object ["status" .= otherValue, "ttl" .= ttl, "lockStatus" .= "unlocked"]) - (Nothing, Just d) -> do - assertSuccess =<< Internal.setTeamFeatureConfig OwnDomain tid featureName (object ["status" .= otherValue, "ttl" .= ttlAfter]) - checkFeatureLenientTtl featureName owner tid (object ["status" .= otherValue, "ttl" .= ttlAfter, "lockStatus" .= "unlocked"]) - liftIO $ threadDelay (d * 1000000) -- waiting it out - -- value reverts back - checkFeatureLenientTtl featureName owner tid defFeatureStatus - -------------------------------------------------------------------------------- -- Simple flags with implicit lock status @@ -949,9 +945,6 @@ testPatchValidateSAMLEmails = _testPatch "validateSAMLemails" False enabled disa testPatchDigitalSignatures :: (HasCallStack) => App () testPatchDigitalSignatures = _testPatch "digitalSignatures" False disabled enabled -testPatchConferenceCalling :: (HasCallStack) => App () -testPatchConferenceCalling = _testPatch "conferenceCalling" False enabled disabled - -------------------------------------------------------------------------------- -- Simple flags with explicit lock status @@ -988,6 +981,14 @@ testPatchAppLock = do -------------------------------------------------------------------------------- -- Flags with config & explicit lock status +testPatchConferenceCalling :: (HasCallStack) => App () +testPatchConferenceCalling = do + let defCfg = confCalling def {lockStatus = Just "locked"} + _testPatch "conferenceCalling" True defCfg (object ["lockStatus" .= "locked"]) + _testPatch "conferenceCalling" True defCfg (object ["status" .= "disabled"]) + _testPatch "conferenceCalling" True defCfg (object ["lockStatus" .= "locked", "status" .= "disabled"]) + _testPatch "conferenceCalling" True defCfg (object ["lockStatus" .= "unlocked", "config" .= object ["useSFTForOneToOneCalls" .= toJSON True]]) + testPatchSelfDeletingMessages :: (HasCallStack) => App () testPatchSelfDeletingMessages = do let defCfg = diff --git a/integration/test/Test/FeatureFlags/User.hs b/integration/test/Test/FeatureFlags/User.hs new file mode 100644 index 00000000000..a6ebffbb25c --- /dev/null +++ b/integration/test/Test/FeatureFlags/User.hs @@ -0,0 +1,78 @@ +module Test.FeatureFlags.User where + +import qualified API.BrigInternal as I +import API.Galley +import qualified API.GalleyInternal as I +import SetupHelpers +import Testlib.Prelude + +testFeatureConferenceCallingForUser :: App () +testFeatureConferenceCallingForUser = do + (alice, tid, _) <- createTeam OwnDomain 0 -- team user + bob <- randomUser OwnDomain def -- non-team user + let featureName = "conferenceCalling" + + -- set initial value at the team level + let patch = + object + [ "lockStatus" .= "unlocked", + "status" .= "enabled", + "config" .= object ["useSFTForOneToOneCalls" .= True] + ] + + assertSuccess =<< I.patchTeamFeatureConfig OwnDomain tid featureName patch + + -- set user value for both users + for_ [alice, bob] $ \u -> do + void + $ I.putFeatureForUser + u + featureName + ( object + [ "status" .= "disabled" + ] + ) + >>= getBody 200 + I.getFeatureForUser u featureName `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + config <- resp.json + config %. "status" `shouldMatch` "disabled" + + -- this config is just made up by brig, it does not reflect the actual value + -- that will be returned to the user + config %. "config.useSFTForOneToOneCalls" `shouldMatch` False + + -- alice + do + getFeaturesForUser alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + config <- resp.json %. featureName + + -- alice is a team user, so her config reflects that of the team + config %. "status" `shouldMatch` "enabled" + config %. "config.useSFTForOneToOneCalls" `shouldMatch` True + + do + void $ I.deleteFeatureForUser alice featureName >>= getBody 200 + getFeaturesForUser alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + config <- resp.json %. featureName + config %. "status" `shouldMatch` "enabled" + config %. "config.useSFTForOneToOneCalls" `shouldMatch` True + + -- bob + do + getFeaturesForUser bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + config <- resp.json %. featureName + -- bob is not in a team, so we get his own personal settings here + config %. "status" `shouldMatch` "disabled" + -- but only for status, config is the server defaults + config %. "config.useSFTForOneToOneCalls" `shouldMatch` False + do + void $ I.deleteFeatureForUser bob featureName >>= getBody 200 + getFeaturesForUser bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + config <- resp.json %. featureName + config %. "status" `shouldMatch` "disabled" + config %. "config.useSFTForOneToOneCalls" `shouldMatch` False diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index 92426fd5f4f..ddcfbdac758 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -34,7 +34,7 @@ enabled = object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "u checkFeature :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App () checkFeature = checkFeatureWith shouldMatch -checkFeatureWith :: (HasCallStack, MakesValue user, MakesValue tid, MakesValue expected) => (App Value -> expected -> App ()) -> String -> user -> tid -> expected -> App () +checkFeatureWith :: (HasCallStack, MakesValue user, MakesValue tid, MakesValue expected) => ((HasCallStack) => App Value -> expected -> App ()) -> String -> user -> tid -> expected -> App () checkFeatureWith shouldMatch' feature user tid expected = do tidStr <- asString tid domain <- objDomain user @@ -54,7 +54,7 @@ checkFeatureWith shouldMatch' feature user tid expected = do checkFeatureLenientTtl :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App () checkFeatureLenientTtl = checkFeatureWith shouldMatchLenientTtl where - shouldMatchLenientTtl :: App Value -> Value -> App () + shouldMatchLenientTtl :: (HasCallStack) => App Value -> Value -> App () shouldMatchLenientTtl actual expected = do expectedLockStatus <- expected %. "lockStatus" actual %. "lockStatus" `shouldMatch` expectedLockStatus @@ -67,13 +67,18 @@ checkFeatureLenientTtl = checkFeatureWith shouldMatchLenientTtl actualTtl <- actual %. "ttl" checkTtl actualTtl expectedTtl - checkTtl :: Value -> Value -> App () - checkTtl (A.String a) (A.String b) = do +checkTtl :: (MakesValue a, MakesValue b) => a -> b -> App () +checkTtl x y = do + vx <- make x + vy <- make y + check vx vy + where + check (A.String a) (A.String b) = do a `shouldMatch` "unlimited" b `shouldMatch` "unlimited" - checkTtl _ (A.String _) = assertFailure "expected the actual ttl to be unlimited, but it was limited" - checkTtl (A.String _) _ = assertFailure "expected the actual ttl to be limited, but it was unlimited" - checkTtl (A.Number actualTtl) (A.Number expectedTtl) = do + check _ (A.String _) = assertFailure "expected the actual ttl to be unlimited, but it was limited" + check (A.String _) _ = assertFailure "expected the actual ttl to be limited, but it was unlimited" + check (A.Number actualTtl) (A.Number expectedTtl) = do assertBool ("expected the actual TTL to be greater than 0 and equal to or no more than 2 seconds less than " <> show expectedTtl <> ", but it was " <> show actualTtl) ( actualTtl @@ -83,7 +88,31 @@ checkFeatureLenientTtl = checkFeatureWith shouldMatchLenientTtl && abs (actualTtl - expectedTtl) <= 2 ) - checkTtl _ _ = assertFailure "unexpected ttl value(s)" + check _ _ = assertFailure "unexpected ttl value(s)" assertForbidden :: (HasCallStack) => Response -> App () assertForbidden = assertLabel 403 "no-team-member" + +data ConfCalling = ConfCalling + { lockStatus :: Maybe String, + status :: String, + sft :: Value + } + +instance Default ConfCalling where + def = + ConfCalling + { lockStatus = Nothing, + status = "disabled", + sft = toJSON False + } + +confCalling :: ConfCalling -> Value +confCalling args = + object + $ ["lockStatus" .= s | s <- toList args.lockStatus] + <> ["ttl" .= "unlimited"] + <> [ "status" .= args.status, + "config" + .= object ["useSFTForOneToOneCalls" .= args.sft] + ] diff --git a/libs/galley-types/default.nix b/libs/galley-types/default.nix index 5a6070c01a4..c67ae7c7cb1 100644 --- a/libs/galley-types/default.nix +++ b/libs/galley-types/default.nix @@ -17,8 +17,6 @@ , memory , QuickCheck , schema-profunctor -, tasty -, tasty-quickcheck , text , types-common , utf8-string @@ -48,14 +46,5 @@ mkDerivation { uuid wire-api ]; - testHaskellDepends = [ - aeson - base - imports - QuickCheck - tasty - tasty-quickcheck - wire-api - ]; license = lib.licenses.agpl3Only; } diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index 04201486276..f1fae8db830 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -88,74 +88,3 @@ library , wire-api default-language: GHC2021 - -test-suite galley-types-tests - type: exitcode-stdio-1.0 - main-is: Main.hs - - -- cabal-fmt: expand test - other-modules: - Paths_galley_types - Test.Galley.Roundtrip - Test.Galley.Types - - hs-source-dirs: test/unit - default-extensions: - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - NoImplicitPrelude - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -threaded -with-rtsopts=-N -Wredundant-constraints - -Wunused-packages - - build-depends: - aeson - , base - , galley-types - , imports - , QuickCheck - , tasty - , tasty-quickcheck - , wire-api - - default-language: GHC2021 diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 6dbba074972..47ae6d8a516 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -85,7 +85,7 @@ data FeatureFlags = FeatureFlags _flagAppLockDefaults :: !(Defaults (ImplicitLockStatus AppLockConfig)), _flagClassifiedDomains :: !(ImplicitLockStatus ClassifiedDomainsConfig), _flagFileSharing :: !(Defaults (WithStatus FileSharingConfig)), - _flagConferenceCalling :: !(Defaults (ImplicitLockStatus ConferenceCallingConfig)), + _flagConferenceCalling :: !(Defaults (WithStatus ConferenceCallingConfig)), _flagSelfDeletingMessages :: !(Defaults (WithStatus SelfDeletingMessagesConfig)), _flagConversationGuestLinks :: !(Defaults (WithStatus GuestLinksConfig)), _flagsTeamFeatureValidateSAMLEmailsStatus :: !(Defaults (ImplicitLockStatus ValidateSAMLEmailsConfig)), @@ -139,7 +139,7 @@ instance FromJSON FeatureFlags where <*> withImplicitLockStatusOrDefault obj "appLock" <*> (fromMaybe (ImplicitLockStatus (defFeatureStatus @ClassifiedDomainsConfig)) <$> (obj .:? "classifiedDomains")) <*> (fromMaybe (Defaults (defFeatureStatus @FileSharingConfig)) <$> (obj .:? "fileSharing")) - <*> withImplicitLockStatusOrDefault obj "conferenceCalling" + <*> (fromMaybe (Defaults (defFeatureStatus @ConferenceCallingConfig)) <$> (obj .:? "conferenceCalling")) <*> (fromMaybe (Defaults (defFeatureStatus @SelfDeletingMessagesConfig)) <$> (obj .:? "selfDeletingMessages")) <*> (fromMaybe (Defaults (defFeatureStatus @GuestLinksConfig)) <$> (obj .:? "conversationGuestLinks")) <*> withImplicitLockStatusOrDefault obj "validateSAMLEmails" @@ -155,49 +155,6 @@ instance FromJSON FeatureFlags where withImplicitLockStatusOrDefault :: forall cfg. (IsFeatureConfig cfg, Schema.ToSchema cfg) => Object -> Key -> A.Parser (Defaults (ImplicitLockStatus cfg)) withImplicitLockStatusOrDefault obj fieldName = fromMaybe (Defaults (ImplicitLockStatus (defFeatureStatus @cfg))) <$> obj .:? fieldName -instance ToJSON FeatureFlags where - toJSON - ( FeatureFlags - sso - legalhold - searchVisibility - appLock - classifiedDomains - fileSharing - conferenceCalling - selfDeletingMessages - guestLinks - validateSAMLEmails - sndFactorPasswordChallenge - searchVisibilityInbound - mls - outlookCalIntegration - mlsE2EId - mlsMigration - enforceFileDownloadLocation - teamMemberDeletedLimitedEventFanout - ) = - object - [ "sso" .= sso, - "legalhold" .= legalhold, - "teamSearchVisibility" .= searchVisibility, - "appLock" .= appLock, - "classifiedDomains" .= classifiedDomains, - "fileSharing" .= fileSharing, - "conferenceCalling" .= conferenceCalling, - "selfDeletingMessages" .= selfDeletingMessages, - "conversationGuestLinks" .= guestLinks, - "validateSAMLEmails" .= validateSAMLEmails, - "sndFactorPasswordChallenge" .= sndFactorPasswordChallenge, - "searchVisibilityInbound" .= searchVisibilityInbound, - "mls" .= mls, - "outlookCalIntegration" .= outlookCalIntegration, - "mlsE2EId" .= mlsE2EId, - "mlsMigration" .= mlsMigration, - "enforceFileDownloadLocation" .= enforceFileDownloadLocation, - "limitedEventFanout" .= teamMemberDeletedLimitedEventFanout - ] - instance FromJSON FeatureSSO where parseJSON (String "enabled-by-default") = pure FeatureSSOEnabledByDefault parseJSON (String "disabled-by-default") = pure FeatureSSODisabledByDefault diff --git a/libs/galley-types/test/unit/Main.hs b/libs/galley-types/test/unit/Main.hs deleted file mode 100644 index 90b692813d3..00000000000 --- a/libs/galley-types/test/unit/Main.hs +++ /dev/null @@ -1,28 +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 Main - ( main, - ) -where - -import Imports -import Test.Galley.Types qualified -import Test.Tasty - -main :: IO () -main = defaultMain $ testGroup "Tests" [Test.Galley.Types.tests] diff --git a/libs/galley-types/test/unit/Test/Galley/Roundtrip.hs b/libs/galley-types/test/unit/Test/Galley/Roundtrip.hs deleted file mode 100644 index b9d1fcc8568..00000000000 --- a/libs/galley-types/test/unit/Test/Galley/Roundtrip.hs +++ /dev/null @@ -1,36 +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 Test.Galley.Roundtrip where - -import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) -import Data.Aeson.Types (parseEither) -import Imports -import Test.Tasty (TestTree) -import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===)) -import Type.Reflection (typeRep) - -testRoundTrip :: - forall a. - (Arbitrary a, Typeable a, ToJSON a, FromJSON a, Eq a, Show a) => - TestTree -testRoundTrip = testProperty msg trip - where - msg = show (typeRep @a) - trip (v :: a) = - counterexample (show $ toJSON v) $ - Right v === (parseEither parseJSON . toJSON) v diff --git a/libs/galley-types/test/unit/Test/Galley/Types.hs b/libs/galley-types/test/unit/Test/Galley/Types.hs deleted file mode 100644 index aa2c03a1411..00000000000 --- a/libs/galley-types/test/unit/Test/Galley/Types.hs +++ /dev/null @@ -1,59 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# OPTIONS_GHC -Wno-orphans -Wno-incomplete-uni-patterns #-} - --- 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.Galley.Types where - -import Galley.Types.Teams -import Imports -import Test.Galley.Roundtrip (testRoundTrip) -import Test.QuickCheck qualified as QC -import Test.Tasty -import Test.Tasty.QuickCheck -import Wire.API.Team.Feature as Public - -tests :: TestTree -tests = testGroup "Tests" [testRoundTrip @FeatureFlags] - -instance Arbitrary FeatureFlags where - arbitrary = - FeatureFlags - <$> QC.elements [minBound ..] - <*> QC.elements [minBound ..] - <*> QC.elements [minBound ..] - -- the default lock status is implicitly added on deserialization and ignored on serialization, therefore we need to fix it to the default here - -- we will be able to remove this once the lock status is explicitly included in the config - <*> fmap (fmap unlocked) arbitrary - <*> fmap unlocked arbitrary - <*> arbitrary - <*> fmap (fmap unlocked) arbitrary - <*> arbitrary - <*> arbitrary - <*> fmap (fmap unlocked) arbitrary - <*> arbitrary - <*> fmap (fmap unlocked) arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> fmap (fmap unlocked) arbitrary - where - unlocked :: ImplicitLockStatus a -> ImplicitLockStatus a - unlocked = ImplicitLockStatus . Public.setLockStatus Public.LockStatusUnlocked . _unImplicitLockStatus 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 8e7a8991d31..44afc4e627a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -117,6 +117,7 @@ type IFeatureAPI = -- ConferenceCallingConfig :<|> IFeatureStatusGet ConferenceCallingConfig :<|> IFeatureStatusPut '[] '() ConferenceCallingConfig + :<|> IFeatureStatusLockStatusPut ConferenceCallingConfig :<|> IFeatureStatusPatch '[] '() ConferenceCallingConfig -- SelfDeletingMessagesConfig :<|> IFeatureStatusGet SelfDeletingMessagesConfig diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index 654f79657a2..4aba788fcf5 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -73,6 +73,7 @@ type FeatureAPI = :<|> FeatureStatusPut '[] '() FileSharingConfig :<|> FeatureStatusGet ClassifiedDomainsConfig :<|> FeatureStatusGet ConferenceCallingConfig + :<|> FeatureStatusPut '[] '() ConferenceCallingConfig :<|> FeatureStatusGet SelfDeletingMessagesConfig :<|> FeatureStatusPut '[] '() SelfDeletingMessagesConfig :<|> FeatureStatusGet GuestLinksConfig diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index c9f24b7b158..f5083f5c87f 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-} @@ -25,6 +26,13 @@ module Wire.API.Team.Feature featureName, featureNameBS, LockStatus (..), + WithStatusBase (..), + DbFeature (..), + DbFeatureWithLock (..), + dbFeatureStatus, + dbFeatureTTL, + dbFeatureConfig, + dbFeatureModConfig, WithStatus, withStatus, withStatus', @@ -39,6 +47,7 @@ module Wire.API.Team.Feature setTTL, setWsTTL, WithStatusPatch, + wsPatch, wspStatus, wspLockStatus, wspConfig, @@ -54,12 +63,13 @@ module Wire.API.Team.Feature convertFeatureTTLDaysToSeconds, EnforceAppLock (..), defFeatureStatusNoLock, + genericComputeFeature, computeFeatureConfigForTeamUser, IsFeatureConfig (..), FeatureSingleton (..), - FeatureTrivialConfig (..), HasDeprecatedFeatureName (..), LockStatusResponse (..), + One2OneCalls (..), -- Features LegalholdConfig (..), SSOConfig (..), @@ -81,7 +91,8 @@ module Wire.API.Team.Feature MlsMigrationConfig (..), EnforceFileDownloadLocationConfig (..), LimitedEventFanoutConfig (..), - AllFeatureConfigs (..), + AllFeatures (..), + AllFeatureConfigs, unImplicitLockStatus, ImplicitLockStatus (..), ) @@ -133,8 +144,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- being enabled/disabled, locked/unlocked, then the config should be a unit -- type, e.g. **data MyFeatureConfig = MyFeatureConfig**. Add a singleton for -- the new data type. Implement type classes 'RenderableSymbol', 'ToSchema', --- 'IsFeatureConfig' and 'Arbitrary'. If your feature doesn't have a config --- implement 'FeatureTrivialConfig'. +-- 'IsFeatureConfig' and 'Arbitrary'. -- -- 2. Add the config to 'AllFeatureConfigs'. -- @@ -215,9 +225,6 @@ data FeatureSingleton cfg where FeatureSingletonEnforceFileDownloadLocationConfig :: FeatureSingleton EnforceFileDownloadLocationConfig FeatureSingletonLimitedEventFanoutConfig :: FeatureSingleton LimitedEventFanoutConfig -class FeatureTrivialConfig cfg where - trivialConfig :: cfg - class HasDeprecatedFeatureName cfg where type DeprecatedFeatureName cfg :: Symbol @@ -238,9 +245,57 @@ data WithStatusBase (m :: Type -> Type) (cfg :: Type) = WithStatusBase } deriving stock (Generic, Typeable, Functor) +-------------------------------------------------------------------------------- +-- DbFeature + +-- | Feature data stored in the database, as a function of its default values. +newtype DbFeature cfg = DbFeature + {unDbFeature :: WithStatusNoLock cfg -> WithStatusNoLock cfg} + +instance Semigroup (DbFeature cfg) where + DbFeature f <> DbFeature g = DbFeature (f . g) + +instance Monoid (DbFeature cfg) where + mempty = DbFeature id + +dbFeatureStatus :: FeatureStatus -> DbFeature cfg +dbFeatureStatus s = DbFeature $ \w -> w {wssStatus = s} + +dbFeatureTTL :: FeatureTTL -> DbFeature cfg +dbFeatureTTL ttl = DbFeature $ \w -> w {wssTTL = ttl} + +dbFeatureConfig :: cfg -> DbFeature cfg +dbFeatureConfig c = DbFeature $ \w -> w {wssConfig = c} + +dbFeatureModConfig :: (cfg -> cfg) -> DbFeature cfg +dbFeatureModConfig f = DbFeature $ \w -> w {wssConfig = f (wssConfig w)} + +data DbFeatureWithLock cfg = DbFeatureWithLock + { lockStatus :: Maybe LockStatus, + feature :: DbFeature cfg + } + ---------------------------------------------------------------------- -- WithStatus +-- [Note: unsettable features] +-- +-- Some feature flags (e.g. sso) don't have a lock status stored in the +-- database. Instead, they are considered unlocked by default, but behave as if +-- they were locked, since they lack a public PUT endpoint. +-- +-- This trick has caused a lot of confusion in the past, and cannot be extended +-- to flags that have non-trivial configuration. For this reason, we are in the +-- process of changing this mechanism to make it work like every other feature. +-- +-- That means that such features will afterwards be toggled by setting their +-- lock status instead. And we'll have some logic in place to make the default +-- status when unlocked be enabled. This achieves a similar behaviour but with +-- fewer exceptional code paths. +-- +-- See the implementation of 'computeFeature' for 'ConferenceCallingConfig' for +-- an example of this mechanism in practice. + -- FUTUREWORK: use lenses, maybe? wsStatus :: WithStatus cfg -> FeatureStatus wsStatus = runIdentity . wsbStatus @@ -275,7 +330,7 @@ setTTL ttl (WithStatusBase s ls c _) = WithStatusBase s ls c (pure ttl) setWsTTL :: FeatureTTL -> WithStatus cfg -> WithStatus cfg setWsTTL = setTTL -type WithStatus (cfg :: Type) = WithStatusBase Identity cfg +type WithStatus = WithStatusBase Identity deriving instance (Eq cfg) => Eq (WithStatus cfg) @@ -317,6 +372,9 @@ deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg), Typeable cfg) => S.ToSchema (WithStatusPatch cfg) +wsPatch :: Maybe FeatureStatus -> Maybe LockStatus -> Maybe cfg -> Maybe FeatureTTL -> WithStatusPatch cfg +wsPatch = WithStatusBase + wspStatus :: WithStatusPatch cfg -> Maybe FeatureStatus wspStatus = wsbStatus @@ -352,14 +410,6 @@ instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (WithStatusPatch cfg) ---------------------------------------------------------------------- -- WithStatusNoLock --- FUTUREWORK(fisx): remove this type. we want all features to have fields `lockStatus` and --- `status`, and we want them to have the same semantics everywhere. currently we have --- eg. conf calling, which was introduced before `lockStatus`, and where `status` means --- `lockStatus`. TTL always refers to `lockStatus`, not `status`. In order to keep current --- (desired) behavior, consider eg. conf calling: let's only allow setting `lockStatus`, but --- if we switch to `unlocked`, we auto-enable the feature, and if we switch to locked, we --- auto-disable it. But we need to change the API to force clients to use `lockStatus` --- instead of `status`, current behavior is just wrong. data WithStatusNoLock (cfg :: Type) = WithStatusNoLock { wssStatus :: FeatureStatus, wssConfig :: cfg, @@ -561,6 +611,19 @@ instance (IsFeatureConfig a, ToSchema a) => ToJSON (ImplicitLockStatus a) where instance (IsFeatureConfig a, ToSchema a) => FromJSON (ImplicitLockStatus a) where parseJSON v = ImplicitLockStatus . withLockStatus (wsLockStatus $ defFeatureStatus @a) <$> A.parseJSON v +-- | Convert a feature coming from the database to its public form. This can be +-- overridden on a feature basis by implementing the `computeFeature` method of +-- the `GetFeatureConfig` class. +genericComputeFeature :: + WithStatus cfg -> + Maybe LockStatus -> + DbFeature cfg -> + WithStatus cfg +genericComputeFeature defFeature lockStatus dbFeature = + case fromMaybe (wsLockStatus defFeature) lockStatus of + LockStatusLocked -> setLockStatus LockStatusLocked defFeature + LockStatusUnlocked -> withUnlocked $ unDbFeature dbFeature (forgetLock defFeature) + -- | This contains the pure business logic for users from teams computeFeatureConfigForTeamUser :: Maybe (WithStatusNoLock cfg) -> Maybe LockStatus -> WithStatus cfg -> WithStatus cfg computeFeatureConfigForTeamUser mStatusDb mLockStatusDb defStatus = @@ -594,9 +657,6 @@ instance IsFeatureConfig GuestLinksConfig where objectSchema = pure GuestLinksConfig -instance FeatureTrivialConfig GuestLinksConfig where - trivialConfig = GuestLinksConfig - -------------------------------------------------------------------------------- -- Legalhold feature @@ -616,12 +676,10 @@ instance IsFeatureConfig LegalholdConfig where instance ToSchema LegalholdConfig where schema = object "LegalholdConfig" objectSchema -instance FeatureTrivialConfig LegalholdConfig where - trivialConfig = LegalholdConfig - -------------------------------------------------------------------------------- -- SSO feature +-- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data SSOConfig = SSOConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SSOConfig) @@ -638,9 +696,6 @@ instance IsFeatureConfig SSOConfig where instance ToSchema SSOConfig where schema = object "SSOConfig" objectSchema -instance FeatureTrivialConfig SSOConfig where - trivialConfig = SSOConfig - -------------------------------------------------------------------------------- -- SearchVisibility available feature @@ -662,15 +717,13 @@ instance IsFeatureConfig SearchVisibilityAvailableConfig where instance ToSchema SearchVisibilityAvailableConfig where schema = object "SearchVisibilityAvailableConfig" objectSchema -instance FeatureTrivialConfig SearchVisibilityAvailableConfig where - trivialConfig = SearchVisibilityAvailableConfig - instance HasDeprecatedFeatureName SearchVisibilityAvailableConfig where type DeprecatedFeatureName SearchVisibilityAvailableConfig = "search-visibility" -------------------------------------------------------------------------------- -- ValidateSAMLEmails feature +-- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data ValidateSAMLEmailsConfig = ValidateSAMLEmailsConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ValidateSAMLEmailsConfig) @@ -690,12 +743,10 @@ instance IsFeatureConfig ValidateSAMLEmailsConfig where instance HasDeprecatedFeatureName ValidateSAMLEmailsConfig where type DeprecatedFeatureName ValidateSAMLEmailsConfig = "validate-saml-emails" -instance FeatureTrivialConfig ValidateSAMLEmailsConfig where - trivialConfig = ValidateSAMLEmailsConfig - -------------------------------------------------------------------------------- -- DigitalSignatures feature +-- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data DigitalSignaturesConfig = DigitalSignaturesConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform DigitalSignaturesConfig) @@ -715,30 +766,58 @@ instance HasDeprecatedFeatureName DigitalSignaturesConfig where instance ToSchema DigitalSignaturesConfig where schema = object "DigitalSignaturesConfig" objectSchema -instance FeatureTrivialConfig DigitalSignaturesConfig where - trivialConfig = DigitalSignaturesConfig - -------------------------------------------------------------------------------- -- ConferenceCalling feature +data One2OneCalls = One2OneCallsTurn | One2OneCallsSft + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform One2OneCalls) + +one2OneCallsFromUseSftFlag :: Bool -> One2OneCalls +one2OneCallsFromUseSftFlag False = One2OneCallsTurn +one2OneCallsFromUseSftFlag True = One2OneCallsSft + +instance Default One2OneCalls where + def = One2OneCallsTurn + +instance Cass.Cql One2OneCalls where + ctype = Cass.Tagged Cass.IntColumn + + fromCql (Cass.CqlInt n) = case n of + 0 -> pure One2OneCallsTurn + 1 -> pure One2OneCallsSft + _ -> Left "fromCql: Invalid One2OneCalls" + fromCql _ = Left "fromCql: One2OneCalls: CqlInt expected" + + toCql One2OneCallsTurn = Cass.CqlInt 0 + toCql One2OneCallsSft = Cass.CqlInt 1 + data ConferenceCallingConfig = ConferenceCallingConfig + { one2OneCalls :: One2OneCalls + } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ConferenceCallingConfig) +instance Default ConferenceCallingConfig where + def = ConferenceCallingConfig {one2OneCalls = def} + instance RenderableSymbol ConferenceCallingConfig where renderSymbol = "ConferenceCallingConfig" instance IsFeatureConfig ConferenceCallingConfig where type FeatureSymbol ConferenceCallingConfig = "conferenceCalling" - defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked ConferenceCallingConfig FeatureTTLUnlimited + defFeatureStatus = withStatus FeatureStatusEnabled LockStatusLocked def FeatureTTLUnlimited featureSingleton = FeatureSingletonConferenceCallingConfig - objectSchema = pure ConferenceCallingConfig + objectSchema = fromMaybe def <$> optField "config" schema instance ToSchema ConferenceCallingConfig where - schema = object "ConferenceCallingConfig" objectSchema - -instance FeatureTrivialConfig ConferenceCallingConfig where - trivialConfig = ConferenceCallingConfig + schema = + object "ConferenceCallingConfig" $ + ConferenceCallingConfig + <$> ((== One2OneCallsSft) . one2OneCalls) + .= ( maybe def one2OneCallsFromUseSftFlag + <$> optField "useSFTForOneToOneCalls" schema + ) -------------------------------------------------------------------------------- -- SndFactorPasswordChallenge feature @@ -759,9 +838,6 @@ instance IsFeatureConfig SndFactorPasswordChallengeConfig where featureSingleton = FeatureSingletonSndFactorPasswordChallengeConfig objectSchema = pure SndFactorPasswordChallengeConfig -instance FeatureTrivialConfig SndFactorPasswordChallengeConfig where - trivialConfig = SndFactorPasswordChallengeConfig - -------------------------------------------------------------------------------- -- SearchVisibilityInbound feature @@ -782,12 +858,12 @@ instance IsFeatureConfig SearchVisibilityInboundConfig where instance ToSchema SearchVisibilityInboundConfig where schema = object "SearchVisibilityInboundConfig" objectSchema -instance FeatureTrivialConfig SearchVisibilityInboundConfig where - trivialConfig = SearchVisibilityInboundConfig - ---------------------------------------------------------------------- -- ClassifiedDomains feature +-- | This feature is quite special, in that it does not have any database +-- state. Its value cannot be updated dynamically, and is always set to the +-- server default taken from the backend configuration. data ClassifiedDomainsConfig = ClassifiedDomainsConfig { classifiedDomainsDomains :: [Domain] } @@ -877,9 +953,6 @@ instance IsFeatureConfig FileSharingConfig where instance ToSchema FileSharingConfig where schema = object "FileSharingConfig" objectSchema -instance FeatureTrivialConfig FileSharingConfig where - trivialConfig = FileSharingConfig - ---------------------------------------------------------------------- -- SelfDeletingMessagesConfig @@ -969,9 +1042,6 @@ instance IsFeatureConfig ExposeInvitationURLsToTeamAdminConfig where instance ToSchema ExposeInvitationURLsToTeamAdminConfig where schema = object "ExposeInvitationURLsToTeamAdminConfig" objectSchema -instance FeatureTrivialConfig ExposeInvitationURLsToTeamAdminConfig where - trivialConfig = ExposeInvitationURLsToTeamAdminConfig - ---------------------------------------------------------------------- -- OutlookCalIntegrationConfig @@ -993,9 +1063,6 @@ instance IsFeatureConfig OutlookCalIntegrationConfig where instance ToSchema OutlookCalIntegrationConfig where schema = object "OutlookCalIntegrationConfig" objectSchema -instance FeatureTrivialConfig OutlookCalIntegrationConfig where - trivialConfig = OutlookCalIntegrationConfig - ---------------------------------------------------------------------- -- MlsE2EId @@ -1039,7 +1106,7 @@ instance ToSchema MlsE2EIdConfig where description ?~ "When a client first tries to fetch or renew a certificate, \ \they may need to login to an identity provider (IdP) depending on their IdP domain authentication policy. \ - \The user may have a grace period during which they can “snooze” this login. \ + \The user may have a grace period during which they can \"snooze\" this login. \ \The duration of this grace period (in seconds) is set in the `verificationDuration` parameter, \ \which is enforced separately by each client. \ \After the grace period has expired, the client will not allow the user to use the application \ @@ -1127,6 +1194,7 @@ instance IsFeatureConfig EnforceFileDownloadLocationConfig where -- months of its introduction, namely once all clients get a chance to adapt to -- a limited event fanout. +-- | This feature does not have a PUT endpoint. See [Note: unsettable features]. data LimitedEventFanoutConfig = LimitedEventFanoutConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform LimitedEventFanoutConfig) @@ -1143,9 +1211,6 @@ instance IsFeatureConfig LimitedEventFanoutConfig where instance ToSchema LimitedEventFanoutConfig where schema = object "LimitedEventFanoutConfig" objectSchema -instance FeatureTrivialConfig LimitedEventFanoutConfig where - trivialConfig = LimitedEventFanoutConfig - ---------------------------------------------------------------------- -- FeatureStatus @@ -1209,34 +1274,35 @@ instance Cass.Cql FeatureStatus where defFeatureStatusNoLock :: (IsFeatureConfig cfg) => WithStatusNoLock cfg defFeatureStatusNoLock = forgetLock defFeatureStatus -data AllFeatureConfigs = AllFeatureConfigs - { afcLegalholdStatus :: WithStatus LegalholdConfig, - afcSSOStatus :: WithStatus SSOConfig, - afcTeamSearchVisibilityAvailable :: WithStatus SearchVisibilityAvailableConfig, - afcSearchVisibilityInboundConfig :: WithStatus SearchVisibilityInboundConfig, - afcValidateSAMLEmails :: WithStatus ValidateSAMLEmailsConfig, - afcDigitalSignatures :: WithStatus DigitalSignaturesConfig, - afcAppLock :: WithStatus AppLockConfig, - afcFileSharing :: WithStatus FileSharingConfig, - afcClassifiedDomains :: WithStatus ClassifiedDomainsConfig, - afcConferenceCalling :: WithStatus ConferenceCallingConfig, - afcSelfDeletingMessages :: WithStatus SelfDeletingMessagesConfig, - afcGuestLink :: WithStatus GuestLinksConfig, - afcSndFactorPasswordChallenge :: WithStatus SndFactorPasswordChallengeConfig, - afcMLS :: WithStatus MLSConfig, - afcExposeInvitationURLsToTeamAdmin :: WithStatus ExposeInvitationURLsToTeamAdminConfig, - afcOutlookCalIntegration :: WithStatus OutlookCalIntegrationConfig, - afcMlsE2EId :: WithStatus MlsE2EIdConfig, - afcMlsMigration :: WithStatus MlsMigrationConfig, - afcEnforceFileDownloadLocation :: WithStatus EnforceFileDownloadLocationConfig, - afcLimitedEventFanout :: WithStatus LimitedEventFanoutConfig +-- FUTUREWORK: rewrite using SOP +data AllFeatures f = AllFeatures + { afcLegalholdStatus :: f LegalholdConfig, + afcSSOStatus :: f SSOConfig, + afcTeamSearchVisibilityAvailable :: f SearchVisibilityAvailableConfig, + afcSearchVisibilityInboundConfig :: f SearchVisibilityInboundConfig, + afcValidateSAMLEmails :: f ValidateSAMLEmailsConfig, + afcDigitalSignatures :: f DigitalSignaturesConfig, + afcAppLock :: f AppLockConfig, + afcFileSharing :: f FileSharingConfig, + afcClassifiedDomains :: f ClassifiedDomainsConfig, + afcConferenceCalling :: f ConferenceCallingConfig, + afcSelfDeletingMessages :: f SelfDeletingMessagesConfig, + afcGuestLink :: f GuestLinksConfig, + afcSndFactorPasswordChallenge :: f SndFactorPasswordChallengeConfig, + afcMLS :: f MLSConfig, + afcExposeInvitationURLsToTeamAdmin :: f ExposeInvitationURLsToTeamAdminConfig, + afcOutlookCalIntegration :: f OutlookCalIntegrationConfig, + afcMlsE2EId :: f MlsE2EIdConfig, + afcMlsMigration :: f MlsMigrationConfig, + afcEnforceFileDownloadLocation :: f EnforceFileDownloadLocationConfig, + afcLimitedEventFanout :: f LimitedEventFanoutConfig } - deriving stock (Eq, Show) - deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AllFeatureConfigs) + +type AllFeatureConfigs = AllFeatures WithStatus instance Default AllFeatureConfigs where def = - AllFeatureConfigs + AllFeatures { afcLegalholdStatus = defFeatureStatus, afcSSOStatus = defFeatureStatus, afcTeamSearchVisibilityAvailable = defFeatureStatus, @@ -1262,7 +1328,7 @@ instance Default AllFeatureConfigs where instance ToSchema AllFeatureConfigs where schema = object "AllFeatureConfigs" $ - AllFeatureConfigs + AllFeatures <$> afcLegalholdStatus .= featureField <*> afcSSOStatus .= featureField <*> afcTeamSearchVisibilityAvailable .= featureField @@ -1292,7 +1358,7 @@ instance ToSchema AllFeatureConfigs where instance Arbitrary AllFeatureConfigs where arbitrary = - AllFeatureConfigs + AllFeatures <$> arbitrary <*> arbitrary <*> arbitrary @@ -1315,3 +1381,13 @@ instance Arbitrary AllFeatureConfigs where <*> arbitrary makeLenses ''ImplicitLockStatus + +deriving instance Show AllFeatureConfigs + +deriving instance Eq AllFeatureConfigs + +deriving via (Schema AllFeatureConfigs) instance (FromJSON AllFeatureConfigs) + +deriving via (Schema AllFeatureConfigs) instance (ToJSON AllFeatureConfigs) + +deriving via (Schema AllFeatureConfigs) instance (S.ToSchema AllFeatureConfigs) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs index 1294ca9bd06..e999ab389a2 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs @@ -25,6 +25,7 @@ import Test.Wire.API.Golden.Generated.MemberUpdateData_user import Test.Wire.API.Golden.Generated.NewOtrMessage_user import Test.Wire.API.Golden.Generated.RmClient_user import Test.Wire.API.Golden.Generated.SimpleMember_user +import Test.Wire.API.Golden.Generated.WithStatus_team import Test.Wire.API.Golden.Runner import Wire.API.Conversation (Conversation, MemberUpdate, OtherMemberUpdate) import Wire.API.User (NewUser, NewUserPublic) @@ -88,5 +89,7 @@ tests = testFromJSONFailureWithMsg @NewUserPublic (Just "only managed-by-Wire users can be created here.") "testObject_NewUserPublic_user_1-3.json" - ] + ], + testCase "WithStatus_ConferenceCallingConfig" $ + testFromJSONObject testObject_WithStatus_team_14 "testObject_WithStatus_team_14.json" ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusNoLock_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusNoLock_team.hs index e2babe794aa..efc0c52b7d5 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusNoLock_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusNoLock_team.hs @@ -63,7 +63,7 @@ testObject_WithStatusNoLock_team_13 :: WithStatusNoLock DigitalSignaturesConfig testObject_WithStatusNoLock_team_13 = WithStatusNoLock FeatureStatusEnabled DigitalSignaturesConfig FeatureTTLUnlimited testObject_WithStatusNoLock_team_14 :: WithStatusNoLock ConferenceCallingConfig -testObject_WithStatusNoLock_team_14 = WithStatusNoLock FeatureStatusDisabled ConferenceCallingConfig FeatureTTLUnlimited +testObject_WithStatusNoLock_team_14 = WithStatusNoLock FeatureStatusDisabled (ConferenceCallingConfig One2OneCallsSft) FeatureTTLUnlimited testObject_WithStatusNoLock_team_15 :: WithStatusNoLock GuestLinksConfig testObject_WithStatusNoLock_team_15 = WithStatusNoLock FeatureStatusEnabled GuestLinksConfig FeatureTTLUnlimited diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs index d4bb69801a7..a5dd2c94955 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs @@ -63,7 +63,7 @@ testObject_WithStatusPatch_team_13 :: WithStatusPatch DigitalSignaturesConfig testObject_WithStatusPatch_team_13 = withStatus (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just DigitalSignaturesConfig) testObject_WithStatusPatch_team_14 :: WithStatusPatch ConferenceCallingConfig -testObject_WithStatusPatch_team_14 = withStatus Nothing (Just LockStatusUnlocked) (Just ConferenceCallingConfig) +testObject_WithStatusPatch_team_14 = withStatus Nothing (Just LockStatusUnlocked) (Just (ConferenceCallingConfig One2OneCallsSft)) testObject_WithStatusPatch_team_15 :: WithStatusPatch GuestLinksConfig testObject_WithStatusPatch_team_15 = withStatus (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just GuestLinksConfig) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs index 78523389109..6acd1c8f634 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs @@ -66,7 +66,7 @@ testObject_WithStatus_team_13 :: WithStatus DigitalSignaturesConfig testObject_WithStatus_team_13 = withStatus FeatureStatusEnabled LockStatusLocked DigitalSignaturesConfig testObject_WithStatus_team_14 :: WithStatus ConferenceCallingConfig -testObject_WithStatus_team_14 = withStatus FeatureStatusDisabled LockStatusUnlocked ConferenceCallingConfig +testObject_WithStatus_team_14 = withStatus FeatureStatusDisabled LockStatusUnlocked (ConferenceCallingConfig One2OneCallsTurn) testObject_WithStatus_team_15 :: WithStatus GuestLinksConfig testObject_WithStatus_team_15 = withStatus FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig diff --git a/libs/wire-api/test/golden/fromJSON/testObject_WithStatus_team_14.json b/libs/wire-api/test/golden/fromJSON/testObject_WithStatus_team_14.json new file mode 100644 index 00000000000..e304622641b --- /dev/null +++ b/libs/wire-api/test/golden/fromJSON/testObject_WithStatus_team_14.json @@ -0,0 +1,5 @@ +{ + "lockStatus": "unlocked", + "status": "disabled", + "ttl": "unlimited" +} diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_14.json b/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_14.json index e5482fe80cd..9148fb4871f 100644 --- a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_14.json +++ b/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_14.json @@ -1,4 +1,7 @@ { "status": "disabled", - "ttl": "unlimited" + "ttl": "unlimited", + "config": { + "useSFTForOneToOneCalls": true + } } diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_14.json b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_14.json index d9c772e591c..da8c2395e05 100644 --- a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_14.json +++ b/libs/wire-api/test/golden/testObject_WithStatusPatch_team_14.json @@ -1,5 +1,7 @@ { - "config": {}, + "config": { + "useSFTForOneToOneCalls": true + }, "lockStatus": "unlocked", "ttl": "unlimited" } diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_14.json b/libs/wire-api/test/golden/testObject_WithStatus_team_14.json index e304622641b..648c75ec1cd 100644 --- a/libs/wire-api/test/golden/testObject_WithStatus_team_14.json +++ b/libs/wire-api/test/golden/testObject_WithStatus_team_14.json @@ -1,5 +1,8 @@ { "lockStatus": "unlocked", "status": "disabled", - "ttl": "unlimited" + "ttl": "unlimited", + "config": { + "useSFTForOneToOneCalls": false + } } diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index e0c76b082ca..265eca6cfc9 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -198,6 +198,11 @@ optSettings: # Remember to keep it the same in Galley. setFederationDomain: example.com setFeatureFlags: # see #RefConfigOptions in `/docs/reference` + conferenceCalling: + defaultForNew: + status: disabled + defaultForNull: + status: disabled setFederationDomainConfigsUpdateFreq: 1 setFederationStrategy: allowAll setFederationDomainConfigs: diff --git a/services/brig/src/Brig/Calling/API.hs b/services/brig/src/Brig/Calling/API.hs index ec4f823a4a0..e1e71eb74d0 100644 --- a/services/brig/src/Brig/Calling/API.hs +++ b/services/brig/src/Brig/Calling/API.hs @@ -60,7 +60,7 @@ import Polysemy import Polysemy.Error qualified as Polysemy import System.Logger.Class qualified as Log import Wire.API.Call.Config qualified as Public -import Wire.API.Team.Feature (AllFeatureConfigs (afcConferenceCalling), FeatureStatus (FeatureStatusDisabled, FeatureStatusEnabled), wsStatus) +import Wire.API.Team.Feature import Wire.Error import Wire.GalleyAPIAccess (GalleyAPIAccess, getAllFeatureConfigsForUser) import Wire.Network.DNS.SRV (srvTarget) diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 9be924b3866..36ddf319be2 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -705,10 +705,10 @@ data AccountFeatureConfigs = AccountFeatureConfigs deriving (Show, Eq, Generic) instance Arbitrary AccountFeatureConfigs where - arbitrary = AccountFeatureConfigs <$> fmap unlocked arbitrary <*> fmap unlocked arbitrary + arbitrary = AccountFeatureConfigs <$> fmap locked arbitrary <*> fmap locked arbitrary where - unlocked :: Public.ImplicitLockStatus a -> Public.ImplicitLockStatus a - unlocked = Public.ImplicitLockStatus . Public.setLockStatus Public.LockStatusUnlocked . Public._unImplicitLockStatus + locked :: Public.ImplicitLockStatus a -> Public.ImplicitLockStatus a + locked = Public.ImplicitLockStatus . Public.setLockStatus Public.LockStatusLocked . Public._unImplicitLockStatus instance FromJSON AccountFeatureConfigs where parseJSON = diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index f3b65f6b37c..0f304d3ada5 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -27,29 +27,21 @@ import API.Internal.Util import API.MLS.Util import Bilge import Bilge.Assert -import Brig.Data.User (lookupFeatureConferenceCalling, userExists) +import Brig.Data.User import Brig.Options qualified as Opt import Cassandra qualified as C import Cassandra qualified as Cass import Cassandra.Util -import Control.Exception (ErrorCall (ErrorCall), throwIO) -import Control.Lens ((^.), (^?!)) -import Data.Aeson qualified as Aeson -import Data.Aeson.Lens qualified as Aeson -import Data.Aeson.Types qualified as Aeson import Data.ByteString.Conversion (toByteString') import Data.Default import Data.Id import Data.Qualified -import GHC.TypeLits (KnownSymbol) import Imports import System.IO.Temp import Test.Tasty import Test.Tasty.HUnit import Util import Util.Options (Endpoint) -import Wire.API.Team.Feature -import Wire.API.Team.Feature qualified as ApiFt import Wire.API.User import Wire.API.User.Client @@ -57,9 +49,7 @@ tests :: Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Gundeck tests opts mgr db brig brigep _gundeck galley = do pure $ testGroup "api/internal" $ - [ test mgr "account features: conferenceCalling" $ - testFeatureConferenceCallingByAccount opts mgr db brig brigep galley, - test mgr "suspend and unsuspend user" $ testSuspendUser db brig, + [ test mgr "suspend and unsuspend user" $ testSuspendUser db brig, test mgr "suspend non existing user and verify no db entry" $ testSuspendNonExistingUser db brig, test mgr "mls/clients" $ testGetMlsClients brig, @@ -94,76 +84,6 @@ setAccountStatus brig u s = . json (AccountStatusUpdate s) ) -testFeatureConferenceCallingByAccount :: forall m. (TestConstraints m) => Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Galley -> m () -testFeatureConferenceCallingByAccount (Opt.optSettings -> settings) mgr db brig brigep galley = do - let check :: (HasCallStack) => ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig -> m () - check status = do - uid <- userId <$> createUser "joe" brig - _ <- - aFewTimes 12 (putAccountConferenceCallingConfigClient brigep mgr uid status) isRight - >>= either (liftIO . throwIO . ErrorCall . ("putAccountConferenceCallingConfigClient: " <>) . show) pure - - mbStatus' <- getAccountConferenceCallingConfigClient brigep mgr uid - liftIO $ assertEqual "GET /i/users/:uid/features/conferenceCalling" (Right status) mbStatus' - - featureConfigs <- getAllFeatureConfigs galley uid - liftIO $ assertEqual "GET /feature-configs" status (ApiFt.forgetLock $ readFeatureConfigs featureConfigs) - - featureConfigsConfCalling <- getFeatureConfig @ApiFt.ConferenceCallingConfig galley uid - liftIO $ assertEqual "GET /feature-configs/conferenceCalling" status (responseJsonUnsafe featureConfigsConfCalling) - - check' :: m () - check' = do - uid <- userId <$> createUser "joe" brig - let defaultIfNull :: ApiFt.WithStatus ApiFt.ConferenceCallingConfig - defaultIfNull = settings ^. Opt.getAfcConferenceCallingDefNull - - defaultIfNewRaw :: Maybe (ApiFt.WithStatus ApiFt.ConferenceCallingConfig) - defaultIfNewRaw = - -- tested manually: whether we remove `defaultForNew` from `brig.yaml` or set it - -- to `enabled` or `disabled`, this test always passes. - settings ^. Opt.getAfcConferenceCallingDefNewMaybe - - do - cassandraResp :: Maybe (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig) <- - aFewTimes - 12 - (Cass.runClient db (lookupFeatureConferenceCalling uid)) - isJust - liftIO $ assertEqual mempty (ApiFt.forgetLock <$> defaultIfNewRaw) cassandraResp - - _ <- - aFewTimes 12 (deleteAccountConferenceCallingConfigClient brigep mgr uid) isRight - >>= either (liftIO . throwIO . ErrorCall . ("deleteAccountConferenceCallingConfigClient: " <>) . show) pure - - do - cassandraResp :: Maybe (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig) <- - aFewTimes - 12 - (Cass.runClient db (lookupFeatureConferenceCalling uid)) - isJust - liftIO $ assertEqual mempty Nothing cassandraResp - - mbStatus' <- getAccountConferenceCallingConfigClient brigep mgr uid - liftIO $ assertEqual "GET /i/users/:uid/features/conferenceCalling" (Right (ApiFt.forgetLock defaultIfNull)) mbStatus' - - featureConfigs <- getAllFeatureConfigs galley uid - liftIO $ assertEqual "GET /feature-configs" defaultIfNull (readFeatureConfigs featureConfigs) - - featureConfigsConfCalling <- getFeatureConfig @ApiFt.ConferenceCallingConfig galley uid - liftIO $ assertEqual "GET /feature-configs/conferenceCalling" defaultIfNull (responseJsonUnsafe featureConfigsConfCalling) - - readFeatureConfigs :: (HasCallStack) => ResponseLBS -> ApiFt.WithStatus ApiFt.ConferenceCallingConfig - readFeatureConfigs = - either (error . show) id - . Aeson.parseEither Aeson.parseJSON - . (^?! Aeson.key "conferenceCalling") - . responseJsonUnsafe @Aeson.Value - - check $ ApiFt.WithStatusNoLock ApiFt.FeatureStatusEnabled ApiFt.ConferenceCallingConfig ApiFt.FeatureTTLUnlimited - check $ ApiFt.WithStatusNoLock ApiFt.FeatureStatusDisabled ApiFt.ConferenceCallingConfig ApiFt.FeatureTTLUnlimited - check' - testGetMlsClients :: Brig -> Http () testGetMlsClients brig = do qusr <- userQualifiedId <$> randomUser brig @@ -198,14 +118,6 @@ createClient brig u i = (defNewClient PermanentClientType [somePrekeys !! i] (someLastPrekeys !! i)) (Request -> Request) -> UserId -> m ResponseLBS -getFeatureConfig galley uid = do - get $ apiVersion "v1" . galley . paths ["feature-configs", featureNameBS @cfg] . zUser uid - -getAllFeatureConfigs :: (MonadHttp m, HasCallStack) => (Request -> Request) -> UserId -> m ResponseLBS -getAllFeatureConfigs galley uid = do - get $ galley . paths ["feature-configs"] . zUser uid - testWritetimeRepresentation :: forall m. (TestConstraints m) => Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Galley -> m () testWritetimeRepresentation _ _mgr db brig _brigep _galley = do quid <- userQualifiedId <$> randomUser brig diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 64bb0d69b4f..58f9f6c9fab 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -272,6 +272,7 @@ library Galley.Schema.V90_EnforceFileDownloadLocationConfig Galley.Schema.V91_TeamMemberDeletedLimitedEventFanout Galley.Schema.V92_MlsE2EIdConfig + Galley.Schema.V93_ConferenceCallingSftForOneToOne Galley.Types.Clients Galley.Types.ToUserRole Galley.Types.UserList diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 465d807cec3..5c025935607 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -81,7 +81,8 @@ settings: lockStatus: unlocked conferenceCalling: defaults: - status: enabled + status: disabled + lockStatus: locked outlookCalIntegration: defaults: status: disabled diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 3cf4708fa8b..92a176a4dea 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -257,6 +257,7 @@ featureAPI = <@> mkNamedAPI @'("ipatch", FileSharingConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("iget", ConferenceCallingConfig) (getFeatureStatus DontDoAuth) <@> mkNamedAPI @'("iput", ConferenceCallingConfig) setFeatureStatusInternal + <@> mkNamedAPI @'("ilock", ConferenceCallingConfig) (updateLockStatus @ConferenceCallingConfig) <@> mkNamedAPI @'("ipatch", ConferenceCallingConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("iget", SelfDeletingMessagesConfig) (getFeatureStatus DontDoAuth) <@> mkNamedAPI @'("iput", SelfDeletingMessagesConfig) setFeatureStatusInternal diff --git a/services/galley/src/Galley/API/LegalHold/Team.hs b/services/galley/src/Galley/API/LegalHold/Team.hs index 6fbc8f3bfd6..c7052c2d8bc 100644 --- a/services/galley/src/Galley/API/LegalHold/Team.hs +++ b/services/galley/src/Galley/API/LegalHold/Team.hs @@ -17,6 +17,7 @@ module Galley.API.LegalHold.Team ( isLegalHoldEnabledForTeam, + computeLegalHoldFeatureStatus, assertLegalHoldEnabledForTeam, ensureNotTooLargeToActivateLegalHold, teamSizeBelowLimit, @@ -28,14 +29,14 @@ import Data.Range import Galley.Effects import Galley.Effects.BrigAccess import Galley.Effects.LegalHoldStore qualified as LegalHoldData -import Galley.Effects.TeamFeatureStore qualified as TeamFeatures +import Galley.Effects.TeamFeatureStore import Galley.Effects.TeamStore import Galley.Types.Teams as Team import Imports import Polysemy import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.Team.Feature qualified as Public +import Wire.API.Team.Feature import Wire.API.Team.Size assertLegalHoldEnabledForTeam :: @@ -51,6 +52,23 @@ assertLegalHoldEnabledForTeam tid = unlessM (isLegalHoldEnabledForTeam tid) $ throwS @'LegalHoldNotEnabled +computeLegalHoldFeatureStatus :: + ( Member TeamStore r, + Member LegalHoldStore r + ) => + TeamId -> + DbFeature LegalholdConfig -> + Sem r FeatureStatus +computeLegalHoldFeatureStatus tid dbFeature = + getLegalHoldFlag >>= \case + FeatureLegalHoldDisabledPermanently -> pure FeatureStatusDisabled + FeatureLegalHoldDisabledByDefault -> + pure . wssStatus $ + unDbFeature dbFeature defFeatureStatusNoLock + FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> do + wl <- LegalHoldData.isTeamLegalholdWhitelisted tid + pure $ if wl then FeatureStatusEnabled else FeatureStatusDisabled + isLegalHoldEnabledForTeam :: forall r. ( Member LegalHoldStore r, @@ -60,18 +78,9 @@ isLegalHoldEnabledForTeam :: TeamId -> Sem r Bool isLegalHoldEnabledForTeam tid = do - getLegalHoldFlag >>= \case - FeatureLegalHoldDisabledPermanently -> do - pure False - FeatureLegalHoldDisabledByDefault -> do - statusValue <- - Public.wssStatus <$$> TeamFeatures.getFeatureConfig Public.FeatureSingletonLegalholdConfig tid - pure $ case statusValue of - Just Public.FeatureStatusEnabled -> True - Just Public.FeatureStatusDisabled -> False - Nothing -> False - FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> - LegalHoldData.isTeamLegalholdWhitelisted tid + dbFeature <- getFeatureConfig FeatureSingletonLegalholdConfig tid + status <- computeLegalHoldFeatureStatus tid dbFeature + pure $ status == FeatureStatusEnabled ensureNotTooLargeToActivateLegalHold :: ( Member BrigAccess r, diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 3e9d3f68a54..02b880f1bbe 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -47,6 +47,7 @@ featureAPI = <@> mkNamedAPI @'("put", FileSharingConfig) (setFeatureStatus . DoAuth) <@> mkNamedAPI @'("get", ClassifiedDomainsConfig) (getFeatureStatus . DoAuth) <@> mkNamedAPI @'("get", ConferenceCallingConfig) (getFeatureStatus . DoAuth) + <@> mkNamedAPI @'("put", ConferenceCallingConfig) (setFeatureStatus . DoAuth) <@> mkNamedAPI @'("get", SelfDeletingMessagesConfig) (getFeatureStatus . DoAuth) <@> mkNamedAPI @'("put", SelfDeletingMessagesConfig) (setFeatureStatus . DoAuth) <@> mkNamedAPI @'("get", GuestLinksConfig) (getFeatureStatus . DoAuth) diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 8facb7f7b76..29073e71bbc 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -63,6 +63,7 @@ import Galley.API.MLS.Types import Galley.API.Mapping import Galley.API.Mapping qualified as Mapping import Galley.API.One2One +import Galley.API.Teams.Features.Get import Galley.API.Util import Galley.Data.Conversation qualified as Data import Galley.Data.Conversation.Types qualified as Data @@ -73,11 +74,9 @@ import Galley.Effects.ConversationStore qualified as E import Galley.Effects.FederatorAccess qualified as E import Galley.Effects.ListItems qualified as E import Galley.Effects.MemberStore qualified as E -import Galley.Effects.TeamFeatureStore qualified as TeamFeatures import Galley.Env import Galley.Options import Galley.Types.Conversations.Members -import Galley.Types.Teams import Imports import Polysemy import Polysemy.Error @@ -681,14 +680,8 @@ getConversationGuestLinksFeatureStatus :: ) => Maybe TeamId -> Sem r (WithStatus GuestLinksConfig) -getConversationGuestLinksFeatureStatus mbTid = do - defaultStatus :: WithStatus GuestLinksConfig <- input <&> view (settings . featureFlags . flagConversationGuestLinks . unDefaults) - case mbTid of - Nothing -> pure defaultStatus - Just tid -> do - mbConfigNoLock <- TeamFeatures.getFeatureConfig FeatureSingletonGuestLinksConfig tid - mbLockStatus <- TeamFeatures.getFeatureLockStatus FeatureSingletonGuestLinksConfig tid - pure $ computeFeatureConfigForTeamUser mbConfigNoLock mbLockStatus defaultStatus +getConversationGuestLinksFeatureStatus Nothing = getConfigForServer @GuestLinksConfig +getConversationGuestLinksFeatureStatus (Just tid) = getConfigForTeam @GuestLinksConfig tid -- | The same as 'getMLSSelfConversation', but it throws an error in case the -- backend is not configured for MLS (the proxy for it being the existance of diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index aa65a594ed4..bf5d9b6bf53 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -58,6 +58,7 @@ import Galley.Effects.SearchVisibilityStore qualified as SearchVisibilityData import Galley.Effects.TeamFeatureStore import Galley.Effects.TeamFeatureStore qualified as TeamFeatures import Galley.Effects.TeamStore (getLegalHoldFlag, getTeamMember) +import Galley.Options import Galley.Types.Teams import Imports import Polysemy @@ -79,10 +80,11 @@ import Wire.Sem.Paging.Cassandra patchFeatureStatusInternal :: forall cfg r. ( SetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, + ComputeFeatureConstraints cfg r, SetConfigForTeamConstraints cfg r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r, + Member (Input Opts) r, Member TeamStore r, Member TeamFeatureStore r, Member (P.Logger (Log.Msg -> Log.Msg)) r, @@ -111,12 +113,13 @@ patchFeatureStatusInternal tid patch = do setFeatureStatus :: forall cfg r. ( SetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, + ComputeFeatureConstraints cfg r, SetConfigForTeamConstraints cfg r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, Member (ErrorS 'TeamNotFound) r, Member (Error TeamFeatureError) r, + Member (Input Opts) r, Member TeamStore r, Member TeamFeatureStore r, Member (P.Logger (Log.Msg -> Log.Msg)) r, @@ -139,12 +142,13 @@ setFeatureStatus doauth tid wsnl = do setFeatureStatusInternal :: forall cfg r. ( SetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, + ComputeFeatureConstraints cfg r, SetConfigForTeamConstraints cfg r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, Member (ErrorS 'TeamNotFound) r, Member (Error TeamFeatureError) r, + Member (Input Opts) r, Member TeamStore r, Member TeamFeatureStore r, Member (P.Logger (Log.Msg -> Log.Msg)) r, @@ -175,7 +179,8 @@ persistAndPushEvent :: ( KnownSymbol (FeatureSymbol cfg), ToSchema cfg, GetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, Member TeamFeatureStore r, Member (P.Logger (Log.Msg -> Log.Msg)) r, Member NotificationSubsystem r, @@ -235,27 +240,25 @@ class (GetFeatureConfig cfg) => SetFeatureConfig cfg where -- push a event to clients (see 'persistAndPushEvent'). setConfigForTeam :: ( SetConfigForTeamConstraints cfg r, - GetConfigForTeamConstraints cfg r, - ( Member TeamFeatureStore r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, - Member NotificationSubsystem r, - Member TeamStore r - ) + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, + Member TeamFeatureStore r, + Member (P.Logger (Log.Msg -> Log.Msg)) r, + Member NotificationSubsystem r, + Member TeamStore r ) => TeamId -> WithStatusNoLock cfg -> Sem r (WithStatus cfg) default setConfigForTeam :: - ( GetConfigForTeamConstraints cfg r, + ( ComputeFeatureConstraints cfg r, KnownSymbol (FeatureSymbol cfg), ToSchema cfg, - Members - '[ TeamFeatureStore, - P.Logger (Log.Msg -> Log.Msg), - NotificationSubsystem, - TeamStore - ] - r + Member (Input Opts) r, + Member TeamFeatureStore r, + Member (P.Logger (Log.Msg -> Log.Msg)) r, + Member NotificationSubsystem r, + Member TeamStore r ) => TeamId -> WithStatusNoLock cfg -> @@ -263,7 +266,11 @@ class (GetFeatureConfig cfg) => SetFeatureConfig cfg where setConfigForTeam tid wsnl = persistAndPushEvent tid wsnl instance SetFeatureConfig SSOConfig where - type SetConfigForTeamConstraints SSOConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) + type + SetConfigForTeamConstraints SSOConfig (r :: EffectRow) = + ( Member (Input Opts) r, + Member (Error TeamFeatureError) r + ) setConfigForTeam tid wsnl = do case wssStatus wsnl of @@ -272,7 +279,11 @@ instance SetFeatureConfig SSOConfig where persistAndPushEvent tid wsnl instance SetFeatureConfig SearchVisibilityAvailableConfig where - type SetConfigForTeamConstraints SearchVisibilityAvailableConfig (r :: EffectRow) = (Member SearchVisibilityStore r) + type + SetConfigForTeamConstraints SearchVisibilityAvailableConfig (r :: EffectRow) = + ( Member SearchVisibilityStore r, + Member (Input Opts) r + ) setConfigForTeam tid wsnl = do case wssStatus wsnl of diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index 685266b3ea1..4cec1f5f047 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE RecordWildCards #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -23,6 +25,7 @@ module Galley.API.Teams.Features.Get getAllFeatureConfigsForTeam, getAllFeatureConfigsForUser, GetFeatureConfig (..), + getConfigForTeam, guardSecondFactorDisabled, DoAuth (..), featureEnabledForTeam, @@ -35,7 +38,7 @@ import Data.Bifunctor (second) import Data.Id import Data.Kind import Data.Qualified (Local, tUnqualified) -import Galley.API.LegalHold.Team (isLegalHoldEnabledForTeam) +import Galley.API.LegalHold.Team import Galley.API.Util import Galley.Effects import Galley.Effects.BrigAccess (getAccountConferenceCallingConfigClient) @@ -57,13 +60,6 @@ data DoAuth = DoAuth UserId | DontDoAuth -- | Don't export methods of this typeclass class (IsFeatureConfig cfg) => GetFeatureConfig cfg where - type GetConfigForTeamConstraints cfg (r :: EffectRow) :: Constraint - type - GetConfigForTeamConstraints cfg (r :: EffectRow) = - ( Member (Input Opts) r, - Member TeamFeatureStore r - ) - type GetConfigForUserConstraints cfg (r :: EffectRow) :: Constraint type GetConfigForUserConstraints cfg (r :: EffectRow) = @@ -75,6 +71,9 @@ class (IsFeatureConfig cfg) => GetFeatureConfig cfg where Member TeamFeatureStore r ) + type ComputeFeatureConstraints cfg (r :: EffectRow) :: Constraint + type ComputeFeatureConstraints cfg r = () + getConfigForServer :: (Member (Input Opts) r) => Sem r (WithStatus cfg) @@ -84,18 +83,6 @@ class (IsFeatureConfig cfg) => GetFeatureConfig cfg where default getConfigForServer :: Sem r (WithStatus cfg) getConfigForServer = pure defFeatureStatus - getConfigForTeam :: - (GetConfigForTeamConstraints cfg r) => - TeamId -> - Sem r (WithStatus cfg) - default getConfigForTeam :: - ( Member (Input Opts) r, - Member TeamFeatureStore r - ) => - TeamId -> - Sem r (WithStatus cfg) - getConfigForTeam = genericGetConfigForTeam - getConfigForUser :: (GetConfigForUserConstraints cfg r) => UserId -> @@ -105,20 +92,39 @@ class (IsFeatureConfig cfg) => GetFeatureConfig cfg where Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r, Member TeamStore r, - Member TeamFeatureStore r + Member TeamFeatureStore r, + ComputeFeatureConstraints cfg r ) => UserId -> Sem r (WithStatus cfg) getConfigForUser = genericGetConfigForUser + computeFeature :: + (ComputeFeatureConstraints cfg r) => + TeamId -> + WithStatus cfg -> + Maybe LockStatus -> + DbFeature cfg -> + Sem r (WithStatus cfg) + default computeFeature :: + TeamId -> + WithStatus cfg -> + Maybe LockStatus -> + DbFeature cfg -> + Sem r (WithStatus cfg) + computeFeature _tid defFeature lockStatus dbFeature = + pure $ + genericComputeFeature @cfg defFeature lockStatus dbFeature + getFeatureStatus :: forall cfg r. ( GetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, - ( Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamStore r - ) + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, + Member TeamFeatureStore r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r ) => DoAuth -> TeamId -> @@ -134,13 +140,14 @@ getFeatureStatus doauth tid = do getFeatureStatusMulti :: forall cfg r. ( GetFeatureConfig cfg, + ComputeFeatureConstraints cfg r, Member (Input Opts) r, Member TeamFeatureStore r ) => Multi.TeamFeatureNoConfigMultiRequest -> Sem r (Multi.TeamFeatureNoConfigMultiResponse cfg) getFeatureStatusMulti (Multi.TeamFeatureNoConfigMultiRequest tids) = do - cfgs <- genericGetConfigForMultiTeam @cfg tids + cfgs <- getConfigForMultiTeam @cfg tids let xs = uncurry toTeamStatus . second forgetLock <$> cfgs pure $ Multi.TeamFeatureNoConfigMultiResponse xs @@ -153,11 +160,13 @@ toTeamStatus tid ws = Multi.TeamStatus tid (wssStatus ws) -- In `getConfigForUser` this is mostly also the case. But there are exceptions, e.g. `ConferenceCallingConfig` getFeatureStatusForUser :: forall cfg r. - ( Member (ErrorS 'NotATeamMember) r, + ( Member (Input Opts) r, + Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r, + Member TeamFeatureStore r, Member TeamStore r, - GetConfigForTeamConstraints cfg r, GetConfigForUserConstraints cfg r, + ComputeFeatureConstraints cfg r, GetFeatureConfig cfg ) => UserId -> @@ -193,13 +202,15 @@ getAllFeatureConfigsForUser zusr = do maybe (throwS @'NotATeamMember) (const $ pure ()) zusrMembership case mbTeam of Just tid -> - TeamFeatures.getAllFeatureConfigs tid + getAllFeatureConfigs tid Nothing -> getAllFeatureConfigsUser zusr getAllFeatureConfigsForTeam :: forall r. - ( Member (ErrorS 'NotATeamMember) r, + ( Member (Input Opts) r, + Member (ErrorS 'NotATeamMember) r, + Member LegalHoldStore r, Member TeamFeatureStore r, Member TeamStore r ) => @@ -209,14 +220,73 @@ getAllFeatureConfigsForTeam :: getAllFeatureConfigsForTeam luid tid = do zusrMembership <- getTeamMember tid (tUnqualified luid) maybe (throwS @'NotATeamMember) (const $ pure ()) zusrMembership - TeamFeatures.getAllFeatureConfigs tid + getAllFeatureConfigs tid + +getAllFeatureConfigs :: + ( Member (Input Opts) r, + Member LegalHoldStore r, + Member TeamFeatureStore r, + Member TeamStore r + ) => + TeamId -> + Sem r AllFeatureConfigs +getAllFeatureConfigs tid = do + features <- TeamFeatures.getAllFeatureConfigs tid + defFeatures <- getAllFeatureConfigsForServer + biTraverseAllFeatures (computeFeatureWithLock tid) defFeatures features + +computeFeatureWithLock :: + forall cfg r. + (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => + TeamId -> + WithStatus cfg -> + DbFeatureWithLock cfg -> + Sem r (WithStatus cfg) +computeFeatureWithLock tid defFeature feat = + computeFeature @cfg tid defFeature feat.lockStatus feat.feature + +-- | One of a number of possible combinators. This is the only one we happen to need. +biTraverseAllFeatures :: + ( Member (Input Opts) r, + Member TeamStore r, + Member LegalHoldStore r + ) => + ( forall cfg. + (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => + f cfg -> + g cfg -> + Sem r (h cfg) + ) -> + (AllFeatures f -> AllFeatures g -> Sem r (AllFeatures h)) +biTraverseAllFeatures phi features1 features2 = do + afcLegalholdStatus <- phi (afcLegalholdStatus features1) (afcLegalholdStatus features2) + afcSSOStatus <- phi (afcSSOStatus features1) (afcSSOStatus features2) + afcTeamSearchVisibilityAvailable <- phi (afcTeamSearchVisibilityAvailable features1) (afcTeamSearchVisibilityAvailable features2) + afcSearchVisibilityInboundConfig <- phi (afcSearchVisibilityInboundConfig features1) (afcSearchVisibilityInboundConfig features2) + afcValidateSAMLEmails <- phi (afcValidateSAMLEmails features1) (afcValidateSAMLEmails features2) + afcDigitalSignatures <- phi (afcDigitalSignatures features1) (afcDigitalSignatures features2) + afcAppLock <- phi (afcAppLock features1) (afcAppLock features2) + afcFileSharing <- phi (afcFileSharing features1) (afcFileSharing features2) + afcClassifiedDomains <- phi (afcClassifiedDomains features1) (afcClassifiedDomains features2) + afcConferenceCalling <- phi (afcConferenceCalling features1) (afcConferenceCalling features2) + afcSelfDeletingMessages <- phi (afcSelfDeletingMessages features1) (afcSelfDeletingMessages features2) + afcGuestLink <- phi (afcGuestLink features1) (afcGuestLink features2) + afcSndFactorPasswordChallenge <- phi (afcSndFactorPasswordChallenge features1) (afcSndFactorPasswordChallenge features2) + afcMLS <- phi (afcMLS features1) (afcMLS features2) + afcExposeInvitationURLsToTeamAdmin <- phi (afcExposeInvitationURLsToTeamAdmin features1) (afcExposeInvitationURLsToTeamAdmin features2) + afcOutlookCalIntegration <- phi (afcOutlookCalIntegration features1) (afcOutlookCalIntegration features2) + afcMlsE2EId <- phi (afcMlsE2EId features1) (afcMlsE2EId features2) + afcMlsMigration <- phi (afcMlsMigration features1) (afcMlsMigration features2) + afcEnforceFileDownloadLocation <- phi (afcEnforceFileDownloadLocation features1) (afcEnforceFileDownloadLocation features2) + afcLimitedEventFanout <- phi (afcLimitedEventFanout features1) (afcLimitedEventFanout features2) + pure AllFeatures {..} getAllFeatureConfigsForServer :: forall r. (Member (Input Opts) r) => Sem r AllFeatureConfigs getAllFeatureConfigsForServer = - AllFeatureConfigs + AllFeatures <$> getConfigForServer @LegalholdConfig <*> getConfigForServer @SSOConfig <*> getConfigForServer @SearchVisibilityAvailableConfig @@ -252,7 +322,7 @@ getAllFeatureConfigsUser :: UserId -> Sem r AllFeatureConfigs getAllFeatureConfigsUser uid = - AllFeatureConfigs + AllFeatures <$> getConfigForUser @LegalholdConfig uid <*> getConfigForUser @SSOConfig uid <*> getConfigForUser @SearchVisibilityAvailableConfig uid @@ -274,32 +344,41 @@ getAllFeatureConfigsUser uid = <*> getConfigForUser @EnforceFileDownloadLocationConfig uid <*> getConfigForUser @LimitedEventFanoutConfig uid --- | Note: this is an internal function which doesn't cover all features, e.g. LegalholdConfig -genericGetConfigForTeam :: +getConfigForTeam :: forall cfg r. - (GetFeatureConfig cfg) => - (Member TeamFeatureStore r) => - (Member (Input Opts) r) => + ( GetFeatureConfig cfg, + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, + Member TeamFeatureStore r + ) => TeamId -> Sem r (WithStatus cfg) -genericGetConfigForTeam tid = do - computeFeatureConfigForTeamUser - <$> TeamFeatures.getFeatureConfig (featureSingleton @cfg) tid - <*> TeamFeatures.getFeatureLockStatus (featureSingleton @cfg) tid - <*> getConfigForServer +getConfigForTeam tid = do + dbFeature <- TeamFeatures.getFeatureConfig (featureSingleton @cfg) tid + lockStatus <- TeamFeatures.getFeatureLockStatus (featureSingleton @cfg) tid + defFeature <- getConfigForServer + computeFeature @cfg + tid + defFeature + lockStatus + dbFeature -- Note: this function assumes the feature cannot be locked -genericGetConfigForMultiTeam :: +getConfigForMultiTeam :: forall cfg r. - (GetFeatureConfig cfg) => - (Member TeamFeatureStore r) => - (Member (Input Opts) r) => + ( GetFeatureConfig cfg, + ComputeFeatureConstraints cfg r, + Member TeamFeatureStore r, + Member (Input Opts) r + ) => [TeamId] -> Sem r [(TeamId, WithStatus cfg)] -genericGetConfigForMultiTeam tids = do - def <- getConfigForServer - (\(tid, mwsnl) -> (tid, computeFeatureConfigForTeamUser mwsnl (Just LockStatusUnlocked) def)) - <$$> TeamFeatures.getFeatureConfigMulti (featureSingleton @cfg) tids +getConfigForMultiTeam tids = do + defFeature <- getConfigForServer + features <- TeamFeatures.getFeatureConfigMulti (featureSingleton @cfg) tids + for features $ \(tid, dbFeature) -> do + feat <- computeFeature @cfg tid defFeature (Just LockStatusUnlocked) dbFeature + pure (tid, feat) -- | Note: this is an internal function which doesn't cover all features, e.g. conference calling genericGetConfigForUser :: @@ -309,7 +388,8 @@ genericGetConfigForUser :: Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r, Member TeamStore r, - GetFeatureConfig cfg + GetFeatureConfig cfg, + ComputeFeatureConstraints cfg r ) => UserId -> Sem r (WithStatus cfg) @@ -322,7 +402,7 @@ genericGetConfigForUser uid = do zusrMembership <- getTeamMember tid uid maybe (throwS @'NotATeamMember) (const $ pure ()) zusrMembership assertTeamExists tid - genericGetConfigForTeam tid + getConfigForTeam tid ------------------------------------------------------------------------------- -- GetFeatureConfig instances @@ -352,13 +432,6 @@ instance GetFeatureConfig ValidateSAMLEmailsConfig where instance GetFeatureConfig DigitalSignaturesConfig instance GetFeatureConfig LegalholdConfig where - type - GetConfigForTeamConstraints LegalholdConfig (r :: EffectRow) = - ( Member (Input Opts) r, - Member TeamFeatureStore r, - Member LegalHoldStore r, - Member TeamStore r - ) type GetConfigForUserConstraints LegalholdConfig (r :: EffectRow) = ( Member (Input Opts) r, @@ -369,13 +442,13 @@ instance GetFeatureConfig LegalholdConfig where Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r ) + type + ComputeFeatureConstraints LegalholdConfig r = + (Member TeamStore r, Member LegalHoldStore r) - getConfigForTeam tid = do - status <- - isLegalHoldEnabledForTeam tid <&> \case - True -> FeatureStatusEnabled - False -> FeatureStatusDisabled - pure $ setStatus status defFeatureStatus + computeFeature tid defFeature _lockStatus dbFeature = do + status <- computeLegalHoldFeatureStatus tid dbFeature + pure $ setStatus status defFeature instance GetFeatureConfig FileSharingConfig where getConfigForServer = @@ -389,6 +462,16 @@ instance GetFeatureConfig ClassifiedDomainsConfig where getConfigForServer = input <&> view (settings . featureFlags . flagClassifiedDomains . unImplicitLockStatus) +-- | Conference calling gets enabled automatically once unlocked. To achieve +-- that, the default feature status in the unlocked case is forced to be +-- "enabled" before the database data is applied. +-- +-- Previously, we were assuming that this feature would be left as "unlocked", +-- and the clients were simply setting the status field. Now, the pre-existing +-- status field is reinterpreted as the lock status, which means that the +-- status will be NULL in many cases. The defaulting logic in 'computeFeature' +-- here makes sure that the status is aligned with the lock status in those +-- situations. instance GetFeatureConfig ConferenceCallingConfig where type GetConfigForUserConstraints ConferenceCallingConfig r = @@ -402,12 +485,22 @@ instance GetFeatureConfig ConferenceCallingConfig where ) getConfigForServer = - input <&> view (settings . featureFlags . flagConferenceCalling . unDefaults . unImplicitLockStatus) + input <&> view (settings . featureFlags . flagConferenceCalling . unDefaults) getConfigForUser uid = do wsnl <- getAccountConferenceCallingConfigClient uid pure $ withLockStatus (wsLockStatus (defFeatureStatus @ConferenceCallingConfig)) wsnl + computeFeature _tid defFeature lockStatus dbFeature = + pure $ case fromMaybe (wsLockStatus defFeature) lockStatus of + LockStatusLocked -> setLockStatus LockStatusLocked defFeature + LockStatusUnlocked -> + withUnlocked $ + (unDbFeature dbFeature) + (forgetLock defFeature) + { wssStatus = FeatureStatusEnabled + } + instance GetFeatureConfig SelfDeletingMessagesConfig where getConfigForServer = input <&> view (settings . featureFlags . flagSelfDeletingMessages . unDefaults) @@ -429,26 +522,16 @@ instance GetFeatureConfig MLSConfig where input <&> view (settings . featureFlags . flagMLS . unDefaults) instance GetFeatureConfig ExposeInvitationURLsToTeamAdminConfig where - getConfigForTeam tid = do + type + ComputeFeatureConstraints ExposeInvitationURLsToTeamAdminConfig r = + (Member (Input Opts) r) + + -- the lock status of this feature is calculated from the allow list, not the database + computeFeature tid defFeature _lockStatus dbFeature = do allowList <- input <&> view (settings . exposeInvitationURLsTeamAllowlist . to (fromMaybe [])) - mbOldStatus <- TeamFeatures.getFeatureConfig FeatureSingletonExposeInvitationURLsToTeamAdminConfig tid <&> fmap wssStatus let teamAllowed = tid `elem` allowList - pure $ computeConfigForTeam teamAllowed (fromMaybe FeatureStatusDisabled mbOldStatus) - where - computeConfigForTeam :: Bool -> FeatureStatus -> WithStatus ExposeInvitationURLsToTeamAdminConfig - computeConfigForTeam teamAllowed teamDbStatus = - if teamAllowed - then makeConfig LockStatusUnlocked teamDbStatus - else -- FUTUREWORK: use default feature status instead - makeConfig LockStatusLocked FeatureStatusDisabled - - makeConfig :: LockStatus -> FeatureStatus -> WithStatus ExposeInvitationURLsToTeamAdminConfig - makeConfig lockStatus status = - withStatus - status - lockStatus - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited + lockStatus = if teamAllowed then LockStatusUnlocked else LockStatusLocked + pure $ genericComputeFeature defFeature (Just lockStatus) dbFeature instance GetFeatureConfig OutlookCalIntegrationConfig where getConfigForServer = @@ -506,13 +589,16 @@ guardSecondFactorDisabled uid cid action = do featureEnabledForTeam :: forall cfg r. ( GetFeatureConfig cfg, - GetConfigForTeamConstraints cfg r, - ( Member (ErrorS OperationDenied) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamStore r - ) + Member (Input Opts) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r, + Member TeamFeatureStore r, + ComputeFeatureConstraints cfg r ) => TeamId -> Sem r Bool -featureEnabledForTeam tid = (==) FeatureStatusEnabled . wsStatus <$> getFeatureStatus @cfg DontDoAuth tid +featureEnabledForTeam tid = + (==) FeatureStatusEnabled + . wsStatus + <$> getFeatureStatus @cfg DontDoAuth tid diff --git a/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs b/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs index 282e9d916c2..c55808c7823 100644 --- a/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs +++ b/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs @@ -9,7 +9,6 @@ import Data.Misc (HttpsUrl) import Data.Time import Database.CQL.Protocol import Galley.Cassandra.Instances () -import Galley.Types.Teams (FeatureLegalHold (..)) import Imports import Wire.API.Conversation.Protocol (ProtocolTag) import Wire.API.MLS.CipherSuite @@ -40,6 +39,8 @@ data AllTeamFeatureConfigsRow = AllTeamFeatureConfigsRow -- conference calling conferenceCalling :: Maybe FeatureStatus, conferenceCallingTtl :: Maybe FeatureTTL, + conferenceCallingOne2One :: Maybe One2OneCalls, + conferenceCallingLock :: Maybe LockStatus, -- guest links guestLinks :: Maybe FeatureStatus, guestLinksLock :: Maybe LockStatus, @@ -100,6 +101,8 @@ emptyRow = selfDeletingMessagesLock = Nothing, conferenceCalling = Nothing, conferenceCallingTtl = Nothing, + conferenceCallingOne2One = Nothing, + conferenceCallingLock = Nothing, guestLinks = Nothing, guestLinksLock = Nothing, sndFactor = Nothing, @@ -130,227 +133,80 @@ emptyRow = limitEventFanout = Nothing } -allFeatureConfigsFromRow :: - -- id of team of which we want to see the feature - TeamId -> - -- team id list is from "settings.exposeInvitationURLsTeamAllowlist" - Maybe [TeamId] -> - FeatureLegalHold -> - Bool -> - AllFeatureConfigs -> - AllTeamFeatureConfigsRow -> - AllFeatureConfigs -allFeatureConfigsFromRow ourteam allowListForExposeInvitationURLs featureLH hasTeamImplicitLegalhold serverConfigs row = - AllFeatureConfigs - { afcLegalholdStatus = legalholdComputeFeatureStatus row.legalhold, - afcSSOStatus = - computeConfig - row.sso - Nothing - FeatureTTLUnlimited - (Just SSOConfig) - serverConfigs.afcSSOStatus, - afcTeamSearchVisibilityAvailable = - computeConfig - row.searchVisibility - Nothing - FeatureTTLUnlimited - (Just SearchVisibilityAvailableConfig) - serverConfigs.afcTeamSearchVisibilityAvailable, - afcSearchVisibilityInboundConfig = - computeConfig - row.searchVisibility - Nothing - FeatureTTLUnlimited - (Just SearchVisibilityInboundConfig) - serverConfigs.afcSearchVisibilityInboundConfig, - afcValidateSAMLEmails = - computeConfig - row.validateSamlEmails - Nothing - FeatureTTLUnlimited - (Just ValidateSAMLEmailsConfig) - serverConfigs.afcValidateSAMLEmails, - afcDigitalSignatures = - computeConfig - row.digitalSignatures - Nothing - FeatureTTLUnlimited - (Just DigitalSignaturesConfig) - serverConfigs.afcDigitalSignatures, +allFeatureConfigsFromRow :: AllTeamFeatureConfigsRow -> AllFeatures DbFeatureWithLock +allFeatureConfigsFromRow row = + AllFeatures + { afcLegalholdStatus = mkFeatureWithLock Nothing row.legalhold, + afcSSOStatus = mkFeatureWithLock Nothing row.sso, + afcTeamSearchVisibilityAvailable = mkFeatureWithLock Nothing row.searchVisibility, + afcSearchVisibilityInboundConfig = mkFeatureWithLock Nothing row.searchVisibility, + afcValidateSAMLEmails = mkFeatureWithLock Nothing row.validateSamlEmails, + afcDigitalSignatures = mkFeatureWithLock Nothing row.digitalSignatures, afcAppLock = - computeConfig - row.appLock + mkFeatureWithLock Nothing - FeatureTTLUnlimited - appLockConfig - serverConfigs.afcAppLock, - afcFileSharing = - computeConfig - row.fileSharing - row.fileSharingLock - FeatureTTLUnlimited - (Just FileSharingConfig) - serverConfigs.afcFileSharing, - afcClassifiedDomains = - computeConfig Nothing Nothing FeatureTTLUnlimited Nothing serverConfigs.afcClassifiedDomains, + (row.appLock, row.appLockEnforce, row.appLockInactivityTimeoutSecs), + afcFileSharing = mkFeatureWithLock row.fileSharingLock row.fileSharing, + afcClassifiedDomains = mkFeatureWithLock Nothing Nothing, afcConferenceCalling = - computeConfig - row.conferenceCalling - Nothing - (fromMaybe FeatureTTLUnlimited row.conferenceCallingTtl) - (Just ConferenceCallingConfig) - serverConfigs.afcConferenceCalling, + mkFeatureWithLock + row.conferenceCallingLock + ( row.conferenceCalling, + row.conferenceCallingTtl, + row.conferenceCallingOne2One + ), afcSelfDeletingMessages = - computeConfig - row.selfDeletingMessages + mkFeatureWithLock row.selfDeletingMessagesLock - FeatureTTLUnlimited - selfDeletingMessagesConfig - serverConfigs.afcSelfDeletingMessages, - afcGuestLink = - computeConfig - row.guestLinks - row.guestLinksLock - FeatureTTLUnlimited - (Just GuestLinksConfig) - serverConfigs.afcGuestLink, - afcSndFactorPasswordChallenge = - computeConfig - row.sndFactor - row.sndFactorLock - FeatureTTLUnlimited - (Just SndFactorPasswordChallengeConfig) - serverConfigs.afcSndFactorPasswordChallenge, + ( row.selfDeletingMessages, + row.selfDeletingMessagesTtl + ), + afcGuestLink = mkFeatureWithLock row.guestLinksLock row.guestLinks, + afcSndFactorPasswordChallenge = mkFeatureWithLock row.sndFactorLock row.sndFactor, afcMLS = - computeConfig - row.mls + mkFeatureWithLock row.mlsLock - FeatureTTLUnlimited - mlsConfig - serverConfigs.afcMLS, - afcExposeInvitationURLsToTeamAdmin = exposeInvitationURLsComputeFeatureStatus row.exposeInvitationUrls, + ( row.mls, + row.mlsDefaultProtocol, + row.mlsToggleUsers, + row.mlsAllowedCipherSuites, + row.mlsDefaultCipherSuite, + row.mlsSupportedProtocols + ), + afcExposeInvitationURLsToTeamAdmin = mkFeatureWithLock Nothing row.exposeInvitationUrls, afcOutlookCalIntegration = - computeConfig - row.outlookCalIntegration + mkFeatureWithLock row.outlookCalIntegrationLock - FeatureTTLUnlimited - (Just OutlookCalIntegrationConfig) - serverConfigs.afcOutlookCalIntegration, + row.outlookCalIntegration, afcMlsE2EId = - computeConfig - row.mlsE2eid + mkFeatureWithLock row.mlsE2eidLock - FeatureTTLUnlimited - mlsE2eidConfig - serverConfigs.afcMlsE2EId, + ( row.mlsE2eid, + row.mlsE2eidGracePeriod, + row.mlsE2eidAcmeDiscoverUrl, + row.mlsE2eidMaybeCrlProxy, + row.mlsE2eidMaybeUseProxyOnMobile + ), afcMlsMigration = - computeConfig - row.mlsMigration + mkFeatureWithLock row.mlsMigrationLock - FeatureTTLUnlimited - mlsMigrationConfig - serverConfigs.afcMlsMigration, + ( row.mlsMigration, + row.mlsMigrationStartTime, + row.mlsMigrationFinalizeRegardlessAfter + ), afcEnforceFileDownloadLocation = - computeConfig - row.enforceDownloadLocation + mkFeatureWithLock row.enforceDownloadLocationLock - FeatureTTLUnlimited - downloadLocationConfig - serverConfigs.afcEnforceFileDownloadLocation, - afcLimitedEventFanout = - computeConfig - row.limitEventFanout - Nothing - FeatureTTLUnlimited - (Just LimitedEventFanoutConfig) - serverConfigs.afcLimitedEventFanout + ( row.enforceDownloadLocation, + row.enforceDownloadLocation_Location + ), + afcLimitedEventFanout = mkFeatureWithLock Nothing row.limitEventFanout } - where - computeConfig :: Maybe FeatureStatus -> Maybe LockStatus -> FeatureTTL -> Maybe cfg -> WithStatus cfg -> WithStatus cfg - computeConfig mDbStatus mDbLock dbTtl mDbCfg serverCfg = - let withStatusNoLock = case (mDbStatus, mDbCfg) of - (Just dbStatus, Just dbCfg) -> - Just $ - WithStatusNoLock - { wssTTL = dbTtl, - wssStatus = dbStatus, - wssConfig = dbCfg - } - _ -> Nothing - in computeFeatureConfigForTeamUser withStatusNoLock mDbLock serverCfg - - -- FUTUREWORK: the following lines are duplicated in - -- "Galley.Cassandra.TeamFeatures"; make sure the pairs don't diverge! - appLockConfig = AppLockConfig <$> row.appLockEnforce <*> row.appLockInactivityTimeoutSecs - - selfDeletingMessagesConfig = SelfDeletingMessagesConfig <$> row.selfDeletingMessagesTtl - - mlsConfig = - MLSConfig - <$> maybe (Just []) (Just . C.fromSet) row.mlsToggleUsers - <*> row.mlsDefaultProtocol - <*> maybe (Just []) (Just . C.fromSet) row.mlsAllowedCipherSuites - <*> row.mlsDefaultCipherSuite - <*> maybe (Just []) (Just . C.fromSet) row.mlsSupportedProtocols - - mlsE2eidConfig = - Just $ - MlsE2EIdConfig - (toGracePeriodOrDefault row.mlsE2eidGracePeriod) - row.mlsE2eidAcmeDiscoverUrl - row.mlsE2eidMaybeCrlProxy - (fromMaybe (useProxyOnMobile . wsConfig $ defFeatureStatus) row.mlsE2eidMaybeUseProxyOnMobile) - where - toGracePeriodOrDefault :: Maybe Int32 -> NominalDiffTime - toGracePeriodOrDefault = maybe (verificationExpiration $ wsConfig defFeatureStatus) fromIntegral - - mlsMigrationConfig = - Just $ - MlsMigrationConfig - { startTime = row.mlsMigrationStartTime, - finaliseRegardlessAfter = row.mlsMigrationFinalizeRegardlessAfter - } - - downloadLocationConfig = Just $ EnforceFileDownloadLocationConfig row.enforceDownloadLocation_Location - - -- FUTUREWORK: this duplicates logic hidden elsewhere for the other getters and setters. do not change lightly! - exposeInvitationURLsComputeFeatureStatus :: - Maybe FeatureStatus -> - WithStatus ExposeInvitationURLsToTeamAdminConfig - exposeInvitationURLsComputeFeatureStatus mFeatureStatus = - if ourteam `elem` fromMaybe [] allowListForExposeInvitationURLs - then - serverConfigs.afcExposeInvitationURLsToTeamAdmin - & maybe id setStatus mFeatureStatus - & setLockStatus LockStatusUnlocked - else serverConfigs.afcExposeInvitationURLsToTeamAdmin - - -- FUTUREWORK: this duplicates logic hidden elsewhere for the other getters and setters. do not change lightly! - legalholdComputeFeatureStatus :: Maybe FeatureStatus -> WithStatus LegalholdConfig - legalholdComputeFeatureStatus mStatusValue = setStatus status defFeatureStatus - where - status = - if isLegalHoldEnabledForTeam - then FeatureStatusEnabled - else FeatureStatusDisabled - isLegalHoldEnabledForTeam = - case featureLH of - FeatureLegalHoldDisabledPermanently -> False - FeatureLegalHoldDisabledByDefault -> maybe False ((==) FeatureStatusEnabled) mStatusValue - FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> hasTeamImplicitLegalhold - -getAllFeatureConfigs :: (MonadClient m) => Maybe [TeamId] -> FeatureLegalHold -> Bool -> AllFeatureConfigs -> TeamId -> m AllFeatureConfigs -getAllFeatureConfigs allowListForExposeInvitationURLs featureLH hasTeamImplicitLegalhold serverConfigs tid = do + +getAllFeatureConfigs :: (MonadClient m) => TeamId -> m (AllFeatures DbFeatureWithLock) +getAllFeatureConfigs tid = do mRow <- retry x1 $ query1 select (params LocalQuorum (Identity tid)) - pure - $ allFeatureConfigsFromRow - tid - allowListForExposeInvitationURLs - featureLH - hasTeamImplicitLegalhold - serverConfigs - $ maybe emptyRow asRecord mRow + pure $ allFeatureConfigsFromRow $ maybe emptyRow asRecord mRow where select :: PrepQuery @@ -367,7 +223,7 @@ getAllFeatureConfigs allowListForExposeInvitationURLs featureLH hasTeamImplicitL \app_lock_status, app_lock_enforce, app_lock_inactivity_timeout_secs, \ \file_sharing, file_sharing_lock_status, \ \self_deleting_messages_status, self_deleting_messages_ttl, self_deleting_messages_lock_status, \ - \conference_calling, ttl(conference_calling), \ + \conference_calling_status, ttl(conference_calling_status), conference_calling_one_to_one, conference_calling, \ \guest_links_status, guest_links_lock_status, \ \snd_factor_password_challenge_status, snd_factor_password_challenge_lock_status, \ \\ @@ -384,3 +240,161 @@ getAllFeatureConfigs allowListForExposeInvitationURLs featureLH hasTeamImplicitL \enforce_file_download_location_status, enforce_file_download_location, enforce_file_download_location_lock_status, \ \limited_event_fanout_status \ \from team_features where team_id = ?" + +class (Tuple (FeatureRow cfg), HasRowType (FeatureRow cfg)) => MakeFeature cfg where + type FeatureRow cfg + type FeatureRow cfg = Identity (Maybe FeatureStatus) + + mkFeature :: RowType (FeatureRow cfg) -> DbFeature cfg + default mkFeature :: + (FeatureRow cfg ~ Identity (Maybe FeatureStatus)) => + RowType (FeatureRow cfg) -> + DbFeature cfg + mkFeature = foldMap dbFeatureStatus + +mkFeatureWithLock :: + (MakeFeature cfg) => + Maybe LockStatus -> + RowType (FeatureRow cfg) -> + DbFeatureWithLock cfg +mkFeatureWithLock lockStatus row = DbFeatureWithLock lockStatus (mkFeature row) + +-- | Used to remove the annoying Identity wrapper around single-element rows. +type family RowType a where + RowType (Identity a) = a + RowType tuple = tuple + +class HasRowType a where + fromRowType :: RowType a -> a + default fromRowType :: (RowType a ~ a) => RowType a -> a + fromRowType = id + + toRowType :: a -> RowType a + default toRowType :: (RowType a ~ a) => a -> RowType a + toRowType = id + +instance HasRowType (a, b) + +instance HasRowType (a, b, c) + +instance HasRowType (a, b, c, d) + +instance HasRowType (a, b, c, d, e) + +instance HasRowType (a, b, c, d, e, f) + +instance HasRowType (Identity a) where + fromRowType = Identity + toRowType = runIdentity + +instance MakeFeature LegalholdConfig + +instance MakeFeature SSOConfig + +instance MakeFeature SearchVisibilityAvailableConfig + +instance MakeFeature SearchVisibilityInboundConfig + +instance MakeFeature ValidateSAMLEmailsConfig + +instance MakeFeature DigitalSignaturesConfig + +instance MakeFeature AppLockConfig where + type FeatureRow AppLockConfig = (Maybe FeatureStatus, Maybe EnforceAppLock, Maybe Int32) + + mkFeature (status, enforce, timeout) = + foldMap dbFeatureStatus status + <> foldMap dbFeatureConfig (AppLockConfig <$> enforce <*> timeout) + +instance MakeFeature FileSharingConfig + +instance MakeFeature ClassifiedDomainsConfig + +instance MakeFeature ConferenceCallingConfig where + type FeatureRow ConferenceCallingConfig = (Maybe FeatureStatus, Maybe FeatureTTL, Maybe One2OneCalls) + + mkFeature (status, ttl, sftForOneToOne) = + foldMap dbFeatureStatus status + <> foldMap dbFeatureTTL ttl + <> foldMap (dbFeatureConfig . ConferenceCallingConfig) sftForOneToOne + +instance MakeFeature SelfDeletingMessagesConfig where + type FeatureRow SelfDeletingMessagesConfig = (Maybe FeatureStatus, Maybe Int32) + + mkFeature (status, ttl) = + foldMap dbFeatureStatus status + <> foldMap (dbFeatureConfig . SelfDeletingMessagesConfig) ttl + +instance MakeFeature GuestLinksConfig + +instance MakeFeature SndFactorPasswordChallengeConfig + +instance MakeFeature ExposeInvitationURLsToTeamAdminConfig + +instance MakeFeature OutlookCalIntegrationConfig + +instance MakeFeature MLSConfig where + type + FeatureRow MLSConfig = + ( Maybe FeatureStatus, + Maybe ProtocolTag, + Maybe (C.Set UserId), + Maybe (C.Set CipherSuiteTag), + Maybe CipherSuiteTag, + Maybe (C.Set ProtocolTag) + ) + + mkFeature (status, defProto, toggleUsers, ciphersuites, defCiphersuite, supportedProtos) = + foldMap dbFeatureStatus status + <> foldMap + dbFeatureConfig + ( MLSConfig (foldMap C.fromSet toggleUsers) + <$> defProto + <*> pure (foldMap C.fromSet ciphersuites) + <*> defCiphersuite + <*> pure (foldMap C.fromSet supportedProtos) + ) + +instance MakeFeature MlsE2EIdConfig where + type + FeatureRow MlsE2EIdConfig = + ( Maybe FeatureStatus, + Maybe Int32, + Maybe HttpsUrl, + Maybe HttpsUrl, + Maybe Bool + ) + + mkFeature (status, gracePeriod, acmeDiscoveryUrl, crlProxy, useProxyOnMobile) = + foldMap dbFeatureStatus status + <> dbFeatureModConfig + ( \defCfg -> + defCfg + { verificationExpiration = + maybe defCfg.verificationExpiration fromIntegral gracePeriod, + acmeDiscoveryUrl = acmeDiscoveryUrl, + crlProxy = crlProxy, + useProxyOnMobile = fromMaybe defCfg.useProxyOnMobile useProxyOnMobile + } + ) + +instance MakeFeature MlsMigrationConfig where + type + FeatureRow MlsMigrationConfig = + ( Maybe FeatureStatus, + Maybe UTCTime, + Maybe UTCTime + ) + + mkFeature (status, startTime, finalizeAfter) = + foldMap dbFeatureStatus status + <> dbFeatureConfig (MlsMigrationConfig startTime finalizeAfter) + +instance MakeFeature EnforceFileDownloadLocationConfig where + type FeatureRow EnforceFileDownloadLocationConfig = (Maybe FeatureStatus, Maybe Text) + + mkFeature (status, location) = + foldMap dbFeatureStatus status + <> dbFeatureConfig (EnforceFileDownloadLocationConfig location) + +instance MakeFeature LimitedEventFanoutConfig diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 618b242efaf..a751060e668 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -24,7 +24,6 @@ where import Cassandra import Cassandra qualified as C -import Control.Monad.Trans.Maybe import Data.Id import Data.Misc (HttpsUrl) import Data.Time @@ -33,10 +32,7 @@ import Galley.Cassandra.GetAllTeamFeatureConfigs import Galley.Cassandra.Instances () import Galley.Cassandra.Store import Galley.Cassandra.Util -import Galley.Effects (LegalHoldStore) -import Galley.Effects.LegalHoldStore qualified as LH import Galley.Effects.TeamFeatureStore qualified as TFS -import Galley.Types.Teams (FeatureLegalHold) import Imports import Polysemy import Polysemy.Input @@ -49,9 +45,6 @@ import Wire.API.Team.Feature interpretTeamFeatureStoreToCassandra :: ( Member (Embed IO) r, Member (Input ClientState) r, - Member (Input AllFeatureConfigs) r, - Member (Input (Maybe [TeamId], FeatureLegalHold)) r, - Member LegalHoldStore r, Member TinyLog r ) => Sem (TFS.TeamFeatureStore ': r) a -> @@ -74,153 +67,56 @@ interpretTeamFeatureStoreToCassandra = interpret $ \case embedClient $ setFeatureLockStatus sing tid ls TFS.GetAllFeatureConfigs tid -> do logEffect "TeamFeatureStore.GetAllFeatureConfigs" - serverConfigs <- input - (allowListForExposeInvitationURLs, featureLH) <- input - hasTeamImplicitLegalhold <- LH.isTeamLegalholdWhitelisted tid - embedClient $ - getAllFeatureConfigs - allowListForExposeInvitationURLs - featureLH - hasTeamImplicitLegalhold - serverConfigs - tid + embedClient $ getAllFeatureConfigs tid -getFeatureConfig :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (Maybe (WithStatusNoLock cfg)) -getFeatureConfig FeatureSingletonLegalholdConfig tid = getTrivialConfigC "legalhold_status" tid -getFeatureConfig FeatureSingletonSSOConfig tid = getTrivialConfigC "sso_status" tid -getFeatureConfig FeatureSingletonSearchVisibilityAvailableConfig tid = getTrivialConfigC "search_visibility_status" tid -getFeatureConfig FeatureSingletonValidateSAMLEmailsConfig tid = getTrivialConfigC "validate_saml_emails" tid -getFeatureConfig FeatureSingletonClassifiedDomainsConfig _tid = pure Nothing -- TODO(fisx): what's this about? -getFeatureConfig FeatureSingletonDigitalSignaturesConfig tid = getTrivialConfigC "digital_signatures" tid -getFeatureConfig FeatureSingletonAppLockConfig tid = runMaybeT $ do - (mStatus, mEnforce, mTimeout) <- - MaybeT . retry x1 $ - query1 select (params LocalQuorum (Identity tid)) - maybe mzero pure $ - WithStatusNoLock - <$> mStatus - <*> (AppLockConfig <$> mEnforce <*> mTimeout) - -- FUTUREWORK: the above line is duplicated in - -- "Galley.Cassandra.GetAllTeamFeatureConfigs"; make sure the two don't diverge! - <*> Just FeatureTTLUnlimited - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe EnforceAppLock, Maybe Int32) - select = - "select app_lock_status, app_lock_enforce, app_lock_inactivity_timeout_secs \ - \ from team_features where team_id = ?" -getFeatureConfig FeatureSingletonFileSharingConfig tid = getTrivialConfigC "file_sharing" tid -getFeatureConfig FeatureSingletonSelfDeletingMessagesConfig tid = runMaybeT $ do - (mEnabled, mTimeout) <- - MaybeT . retry x1 $ - query1 select (params LocalQuorum (Identity tid)) - maybe mzero pure $ - WithStatusNoLock - <$> mEnabled - <*> fmap SelfDeletingMessagesConfig mTimeout - -- FUTUREWORK: the above line is duplicated in - -- "Galley.Cassandra.GetAllTeamFeatureConfigs"; make sure the two don't diverge! - <*> Just FeatureTTLUnlimited - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe Int32) - select = - "select self_deleting_messages_status, self_deleting_messages_ttl\ - \ from team_features where team_id = ?" -getFeatureConfig FeatureSingletonConferenceCallingConfig tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - retry x1 q <&> \case - Nothing -> Nothing - Just (Nothing, _) -> Nothing - Just (Just status, mTtl) -> - Just - . forgetLock - . setStatus status - . setWsTTL (fromMaybe FeatureTTLUnlimited mTtl) - $ defFeatureStatus - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe FeatureTTL) - select = - fromString $ - "select conference_calling, ttl(conference_calling) from team_features where team_id = ?" -getFeatureConfig FeatureSingletonGuestLinksConfig tid = getTrivialConfigC "guest_links_status" tid -getFeatureConfig FeatureSingletonSndFactorPasswordChallengeConfig tid = getTrivialConfigC "snd_factor_password_challenge_status" tid -getFeatureConfig FeatureSingletonSearchVisibilityInboundConfig tid = getTrivialConfigC "search_visibility_status" tid -getFeatureConfig FeatureSingletonMLSConfig tid = do - m <- retry x1 $ query1 select (params LocalQuorum (Identity tid)) - pure $ case m of - Nothing -> Nothing - Just (status, defaultProtocol, protocolToggleUsers, allowedCipherSuites, defaultCipherSuite, supportedProtocols) -> - WithStatusNoLock - <$> status - <*> ( -- FUTUREWORK: this block is duplicated in - -- "Galley.Cassandra.GetAllTeamFeatureConfigs"; make sure the two don't diverge! - MLSConfig - <$> maybe (Just []) (Just . C.fromSet) protocolToggleUsers - <*> defaultProtocol - <*> maybe (Just []) (Just . C.fromSet) allowedCipherSuites - <*> defaultCipherSuite - <*> maybe (Just []) (Just . C.fromSet) supportedProtocols - ) - <*> Just FeatureTTLUnlimited - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe ProtocolTag, Maybe (C.Set UserId), Maybe (C.Set CipherSuiteTag), Maybe CipherSuiteTag, Maybe (C.Set ProtocolTag)) - select = - "select mls_status, mls_default_protocol, mls_protocol_toggle_users, mls_allowed_ciphersuites, \ - \mls_default_ciphersuite, mls_supported_protocols from team_features where team_id = ?" -getFeatureConfig FeatureSingletonMlsE2EIdConfig tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - retry x1 q <&> \case - Nothing -> Nothing - Just (Nothing, _, _, _, _) -> Nothing - Just (Just fs, mGracePeriod, mUrl, mCrlProxy, mUseProxyOnMobile) -> - Just $ - WithStatusNoLock - fs - ( -- FUTUREWORK: this block is duplicated in - -- "Galley.Cassandra.GetAllTeamFeatureConfigs"; make sure the two don't diverge! - MlsE2EIdConfig (toGracePeriodOrDefault mGracePeriod) mUrl mCrlProxy (fromMaybe (useProxyOnMobile . wsConfig $ defFeatureStatus @MlsE2EIdConfig) mUseProxyOnMobile) - ) - FeatureTTLUnlimited - where - toGracePeriodOrDefault :: Maybe Int32 -> NominalDiffTime - toGracePeriodOrDefault = maybe (verificationExpiration $ wsConfig defFeatureStatus) fromIntegral - - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe Int32, Maybe HttpsUrl, Maybe HttpsUrl, Maybe Bool) - select = - fromString $ - "select mls_e2eid_status, mls_e2eid_grace_period, mls_e2eid_acme_discovery_url, mls_e2eid_crl_proxy, mls_e2eid_use_proxy_on_mobile from team_features where team_id = ?" -getFeatureConfig FeatureSingletonMlsMigration tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - retry x1 q <&> \case - Nothing -> Nothing - Just (Nothing, _, _) -> Nothing - Just (Just fs, startTime, finaliseRegardlessAfter) -> - Just $ - WithStatusNoLock - fs - -- FUTUREWORK: the following expression is duplicated in - -- "Galley.Cassandra.GetAllTeamFeatureConfigs"; make sure the two don't diverge! - MlsMigrationConfig - { startTime = startTime, - finaliseRegardlessAfter = finaliseRegardlessAfter - } - FeatureTTLUnlimited - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe UTCTime, Maybe UTCTime) - select = "select mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after from team_features where team_id = ?" -getFeatureConfig FeatureSingletonExposeInvitationURLsToTeamAdminConfig tid = getTrivialConfigC "expose_invitation_urls_to_team_admin" tid -getFeatureConfig FeatureSingletonOutlookCalIntegrationConfig tid = getTrivialConfigC "outlook_cal_integration_status" tid -getFeatureConfig FeatureSingletonEnforceFileDownloadLocationConfig tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - retry x1 q <&> \case - Nothing -> Nothing - Just (Nothing, _) -> Nothing - Just (Just fs, mbLocation) -> - Just $ WithStatusNoLock fs (EnforceFileDownloadLocationConfig mbLocation) FeatureTTLUnlimited - where - select :: PrepQuery R (Identity TeamId) (Maybe FeatureStatus, Maybe Text) - select = "select enforce_file_download_location_status, enforce_file_download_location from team_features where team_id = ?" +getFeatureConfig :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (DbFeature cfg) +getFeatureConfig FeatureSingletonLegalholdConfig tid = getFeature "legalhold_status" tid +getFeatureConfig FeatureSingletonSSOConfig tid = getFeature "sso_status" tid +getFeatureConfig FeatureSingletonSearchVisibilityAvailableConfig tid = getFeature "search_visibility_status" tid +getFeatureConfig FeatureSingletonValidateSAMLEmailsConfig tid = getFeature "validate_saml_emails" tid +getFeatureConfig FeatureSingletonClassifiedDomainsConfig _tid = pure mempty +getFeatureConfig FeatureSingletonDigitalSignaturesConfig tid = getFeature "digital_signatures" tid +getFeatureConfig FeatureSingletonAppLockConfig tid = + getFeature + "app_lock_status, app_lock_enforce, app_lock_inactivity_timeout_secs" + tid +getFeatureConfig FeatureSingletonFileSharingConfig tid = getFeature "file_sharing" tid +getFeatureConfig FeatureSingletonSelfDeletingMessagesConfig tid = + getFeature + "self_deleting_messages_status, self_deleting_messages_ttl" + tid +getFeatureConfig FeatureSingletonConferenceCallingConfig tid = + getFeature + "conference_calling_status, ttl(conference_calling_status), conference_calling_one_to_one" + tid +getFeatureConfig FeatureSingletonGuestLinksConfig tid = getFeature "guest_links_status" tid +getFeatureConfig FeatureSingletonSndFactorPasswordChallengeConfig tid = getFeature "snd_factor_password_challenge_status" tid +getFeatureConfig FeatureSingletonSearchVisibilityInboundConfig tid = getFeature "search_visibility_status" tid +getFeatureConfig FeatureSingletonMLSConfig tid = + getFeature + "mls_status, mls_default_protocol, mls_protocol_toggle_users, \ + \mls_allowed_ciphersuites, mls_default_ciphersuite, mls_supported_protocols" + tid +getFeatureConfig FeatureSingletonMlsE2EIdConfig tid = + getFeature + "mls_e2eid_status, mls_e2eid_grace_period, mls_e2eid_acme_discovery_url, \ + \mls_e2eid_crl_proxy, mls_e2eid_use_proxy_on_mobile" + tid +getFeatureConfig FeatureSingletonMlsMigration tid = + getFeature + "mls_migration_status, mls_migration_start_time, \ + \mls_migration_finalise_regardless_after" + tid +getFeatureConfig FeatureSingletonExposeInvitationURLsToTeamAdminConfig tid = + getFeature "expose_invitation_urls_to_team_admin" tid +getFeatureConfig FeatureSingletonOutlookCalIntegrationConfig tid = + getFeature "outlook_cal_integration_status" tid +getFeatureConfig FeatureSingletonEnforceFileDownloadLocationConfig tid = + getFeature + "enforce_file_download_location_status, enforce_file_download_location" + tid getFeatureConfig FeatureSingletonLimitedEventFanoutConfig tid = - getTrivialConfigC "limited_event_fanout_status" tid + getFeature "limited_event_fanout_status" tid setFeatureConfig :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> WithStatusNoLock cfg -> m () setFeatureConfig FeatureSingletonLegalholdConfig tid statusNoLock = setFeatureStatusC "legalhold_status" tid (wssStatus statusNoLock) @@ -250,18 +146,17 @@ setFeatureConfig FeatureSingletonSelfDeletingMessagesConfig tid status = do insert = "insert into team_features (team_id, self_deleting_messages_status,\ \ self_deleting_messages_ttl) values (?, ?, ?)" -setFeatureConfig FeatureSingletonConferenceCallingConfig tid statusNoLock = - retry x5 $ write insert (params LocalQuorum (tid, wssStatus statusNoLock)) +setFeatureConfig FeatureSingletonConferenceCallingConfig tid statusNoLock = do + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery insertStatus (tid, statusNoLock.wssStatus) + addPrepQuery insertConfig (tid, statusNoLock.wssConfig.one2OneCalls) where - renderFeatureTtl :: FeatureTTL -> String - renderFeatureTtl = \case - FeatureTTLSeconds d | d > 0 -> " using ttl " <> show d - _ -> " using ttl 0" -- 0 or unlimited (delete a column's existing TTL by setting its value to zero) - insert :: PrepQuery W (TeamId, FeatureStatus) () - insert = - fromString $ - "insert into team_features (team_id,conference_calling) values (?, ?)" - <> renderFeatureTtl (wssTTL statusNoLock) + insertStatus :: PrepQuery W (TeamId, FeatureStatus) () + insertStatus = "insert into team_features (team_id, conference_calling_status) values (?, ?)" + insertConfig :: PrepQuery W (TeamId, One2OneCalls) () + insertConfig = "insert into team_features (team_id, conference_calling_one_to_one) values (?, ?)" setFeatureConfig FeatureSingletonGuestLinksConfig tid statusNoLock = setFeatureStatusC "guest_links_status" tid (wssStatus statusNoLock) setFeatureConfig FeatureSingletonSndFactorPasswordChallengeConfig tid statusNoLock = setFeatureStatusC "snd_factor_password_challenge_status" tid (wssStatus statusNoLock) @@ -332,6 +227,7 @@ getFeatureLockStatus FeatureSingletonMlsMigration tid = getLockStatusC "mls_migr getFeatureLockStatus FeatureSingletonOutlookCalIntegrationConfig tid = getLockStatusC "outlook_cal_integration_lock_status" tid getFeatureLockStatus FeatureSingletonMLSConfig tid = getLockStatusC "mls_lock_status" tid getFeatureLockStatus FeatureSingletonEnforceFileDownloadLocationConfig tid = getLockStatusC "enforce_file_download_location_lock_status" tid +getFeatureLockStatus FeatureSingletonConferenceCallingConfig tid = getLockStatusC "conference_calling" tid getFeatureLockStatus _ _ = pure Nothing setFeatureLockStatus :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> LockStatus -> m () @@ -344,26 +240,24 @@ setFeatureLockStatus FeatureSingletonMlsMigration tid status = setLockStatusC "m setFeatureLockStatus FeatureSingletonOutlookCalIntegrationConfig tid status = setLockStatusC "outlook_cal_integration_lock_status" tid status setFeatureLockStatus FeatureSingletonMLSConfig tid status = setLockStatusC "mls_lock_status" tid status setFeatureLockStatus FeatureSingletonEnforceFileDownloadLocationConfig tid status = setLockStatusC "enforce_file_download_location_lock_status" tid status +setFeatureLockStatus FeatureSingletonConferenceCallingConfig tid status = setLockStatusC "conference_calling" tid status setFeatureLockStatus _ _tid _status = pure () -getTrivialConfigC :: +getFeature :: forall m cfg. - (MonadClient m, IsFeatureConfig cfg) => + (MonadClient m, MakeFeature cfg) => String -> TeamId -> - m (Maybe (WithStatusNoLock cfg)) -getTrivialConfigC statusCol tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - mFeatureStatus <- (>>= runIdentity) <$> retry x1 q - pure $ case mFeatureStatus of - Nothing -> Nothing - Just status -> Just . forgetLock $ setStatus status defFeatureStatus + m (DbFeature cfg) +getFeature columns tid = do + row <- retry x1 $ query1 select (params LocalQuorum (Identity tid)) + pure $ foldMap (mkFeature . toRowType) row where - select :: PrepQuery R (Identity TeamId) (Identity (Maybe FeatureStatus)) + select :: PrepQuery R (Identity TeamId) (FeatureRow cfg) select = fromString $ "select " - <> statusCol + <> columns <> " from team_features where team_id = ?" setFeatureStatusC :: @@ -417,6 +311,6 @@ getFeatureConfigMulti :: (MonadClient m, MonadUnliftIO m) => FeatureSingleton cfg -> [TeamId] -> - m [(TeamId, Maybe (WithStatusNoLock cfg))] + m [(TeamId, DbFeature cfg)] getFeatureConfigMulti proxy = pooledMapConcurrentlyN 8 (\tid -> getFeatureConfig proxy tid <&> (tid,)) diff --git a/services/galley/src/Galley/Effects/TeamFeatureStore.hs b/services/galley/src/Galley/Effects/TeamFeatureStore.hs index 5011d72a3ce..0d24a1821af 100644 --- a/services/galley/src/Galley/Effects/TeamFeatureStore.hs +++ b/services/galley/src/Galley/Effects/TeamFeatureStore.hs @@ -25,14 +25,15 @@ import Polysemy import Wire.API.Team.Feature data TeamFeatureStore m a where + -- | Returns all stored feature values excluding lock status. GetFeatureConfig :: FeatureSingleton cfg -> TeamId -> - TeamFeatureStore m (Maybe (WithStatusNoLock cfg)) + TeamFeatureStore m (DbFeature cfg) GetFeatureConfigMulti :: FeatureSingleton cfg -> [TeamId] -> - TeamFeatureStore m [(TeamId, Maybe (WithStatusNoLock cfg))] + TeamFeatureStore m [(TeamId, DbFeature cfg)] SetFeatureConfig :: FeatureSingleton cfg -> TeamId -> @@ -49,6 +50,6 @@ data TeamFeatureStore m a where TeamFeatureStore m () GetAllFeatureConfigs :: TeamId -> - TeamFeatureStore m AllFeatureConfigs + TeamFeatureStore m (AllFeatures DbFeatureWithLock) makeSem ''TeamFeatureStore diff --git a/services/galley/src/Galley/Schema/Run.hs b/services/galley/src/Galley/Schema/Run.hs index 5039676a3fa..45417b188f8 100644 --- a/services/galley/src/Galley/Schema/Run.hs +++ b/services/galley/src/Galley/Schema/Run.hs @@ -93,6 +93,7 @@ import Galley.Schema.V89_MlsLockStatus qualified as V89_MlsLockStatus import Galley.Schema.V90_EnforceFileDownloadLocationConfig qualified as V90_EnforceFileDownloadLocationConfig import Galley.Schema.V91_TeamMemberDeletedLimitedEventFanout qualified as V91_TeamMemberDeletedLimitedEventFanout import Galley.Schema.V92_MlsE2EIdConfig qualified as V92_MlsE2EIdConfig +import Galley.Schema.V93_ConferenceCallingSftForOneToOne qualified as V93_ConferenceCallingSftForOneToOne import Imports import Options.Applicative import System.Logger.Extended qualified as Log @@ -186,7 +187,8 @@ migrations = V89_MlsLockStatus.migration, V90_EnforceFileDownloadLocationConfig.migration, V91_TeamMemberDeletedLimitedEventFanout.migration, - V92_MlsE2EIdConfig.migration + V92_MlsE2EIdConfig.migration, + V93_ConferenceCallingSftForOneToOne.migration -- FUTUREWORK: once #1726 has made its way to master/production, -- the 'message' field in connections table can be dropped. -- See also https://github.com/wireapp/wire-server/pull/1747/files diff --git a/services/galley/src/Galley/Schema/V93_ConferenceCallingSftForOneToOne.hs b/services/galley/src/Galley/Schema/V93_ConferenceCallingSftForOneToOne.hs new file mode 100644 index 00000000000..f93f63d70f9 --- /dev/null +++ b/services/galley/src/Galley/Schema/V93_ConferenceCallingSftForOneToOne.hs @@ -0,0 +1,16 @@ +module Galley.Schema.V93_ConferenceCallingSftForOneToOne where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 93 "Add conference_calling_one_to_one and status to team_features" $ + -- the existing field `conference_calling` is now repurposed to represent the lock status + schema' + [r| ALTER TABLE team_features ADD ( + conference_calling_one_to_one int, + conference_calling_status int + ) + |] diff --git a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs index 9247594fe87..0d9ace470ec 100644 --- a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs @@ -147,7 +147,7 @@ unlockFeature galley tid = setSndFactorPasswordChallengeStatus :: GalleyReq -> TeamId -> Public.FeatureStatus -> TestSpar () setSndFactorPasswordChallengeStatus galley tid status = do - let js = RequestBodyLBS $ encode $ Public.WithStatusNoLock @Public.SndFactorPasswordChallengeConfig status Public.trivialConfig Public.FeatureTTLUnlimited + let js = RequestBodyLBS $ encode $ Public.WithStatusNoLock @Public.SndFactorPasswordChallengeConfig status Public.SndFactorPasswordChallengeConfig Public.FeatureTTLUnlimited call $ put (galley . paths ["i", "teams", toByteString' tid, "features", featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index e0b1233bf5d..30e18ed8cfa 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -196,7 +196,7 @@ import Util.Types import qualified Web.Cookie as Web import Wire.API.Team (Icon (..)) import qualified Wire.API.Team as Galley -import Wire.API.Team.Feature (FeatureStatus (..), FeatureTTL' (..), FeatureTrivialConfig (trivialConfig), SSOConfig, WithStatusNoLock (WithStatusNoLock)) +import Wire.API.Team.Feature import qualified Wire.API.Team.Invitation as TeamInvitation import Wire.API.Team.Member (NewTeamMember, TeamMemberList, rolePermissions) import qualified Wire.API.Team.Member as Member @@ -385,7 +385,7 @@ putSSOEnabledInternal gly tid enabled = do void . put $ gly . paths ["i", "teams", toByteString' tid, "features", "sso"] - . json (WithStatusNoLock @SSOConfig enabled trivialConfig FeatureTTLUnlimited) + . json (WithStatusNoLock @SSOConfig enabled SSOConfig FeatureTTLUnlimited) . expect2xx -- | cloned from `/services/brig/test/integration/API/Team/Util.hs`. diff --git a/services/spar/test-integration/Util/Email.hs b/services/spar/test-integration/Util/Email.hs index 49a6ca62690..0a1910127fe 100644 --- a/services/spar/test-integration/Util/Email.hs +++ b/services/spar/test-integration/Util/Email.hs @@ -110,6 +110,6 @@ activate brig (k, c) = setSamlEmailValidation :: (HasCallStack) => TeamId -> Feature.FeatureStatus -> TestSpar () setSamlEmailValidation tid status = do galley <- view teGalley - let req = put $ galley . paths p . json (Feature.WithStatusNoLock @Feature.ValidateSAMLEmailsConfig status Feature.trivialConfig Feature.FeatureTTLUnlimited) + let req = put $ galley . paths p . json (Feature.WithStatusNoLock @Feature.ValidateSAMLEmailsConfig status Feature.ValidateSAMLEmailsConfig Feature.FeatureTTLUnlimited) p = ["/i/teams", toByteString' tid, "features", Feature.featureNameBS @Feature.ValidateSAMLEmailsConfig] call req !!! const 200 === statusCode diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 8b071b59b45..b95aa15c989 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -328,11 +328,6 @@ mkFeaturePutRoute :: mkFeaturePutRoute tid payload = NoContent <$ Intra.setTeamFeatureFlag @cfg tid payload type MkFeaturePutConstraints cfg = - ( MkFeaturePutLockConstraints cfg, - FeatureTrivialConfig cfg - ) - -type MkFeaturePutLockConstraints cfg = ( IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cfg), ToSchema cfg, @@ -346,7 +341,7 @@ mkFeaturePutRouteTrivialConfigNoTTL :: mkFeaturePutRouteTrivialConfigNoTTL tid status = mkFeaturePutRouteTrivialConfig @cfg tid status Nothing mkFeatureLockUnlockRouteTrivialConfigNoTTL :: - forall cfg. (MkFeaturePutLockConstraints cfg) => TeamId -> LockStatus -> Handler NoContent + forall cfg. (MkFeaturePutConstraints cfg) => TeamId -> LockStatus -> Handler NoContent mkFeatureLockUnlockRouteTrivialConfigNoTTL tid lstat = NoContent <$ Intra.setTeamFeatureLockStatus @cfg tid lstat mkFeaturePutRouteTrivialConfigWithTTL :: @@ -355,9 +350,9 @@ mkFeaturePutRouteTrivialConfigWithTTL tid status = mkFeaturePutRouteTrivialConfi mkFeaturePutRouteTrivialConfig :: forall cfg. (MkFeaturePutConstraints cfg) => TeamId -> FeatureStatus -> Maybe FeatureTTLDays -> Handler NoContent -mkFeaturePutRouteTrivialConfig tid status (maybe FeatureTTLUnlimited convertFeatureTTLDaysToSeconds -> ttl) = do - let fullStatus = WithStatusNoLock status trivialConfig ttl - NoContent <$ Intra.setTeamFeatureFlag @cfg tid fullStatus +mkFeaturePutRouteTrivialConfig tid status (fmap convertFeatureTTLDaysToSeconds -> ttl) = do + let patch = wsPatch (Just status) Nothing Nothing ttl + NoContent <$ Intra.patchTeamFeatureFlag @cfg tid patch getSearchVisibility :: TeamId -> Handler TeamSearchVisibilityView getSearchVisibility = Intra.getSearchVisibility diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index fab41b2971a..93b99fce9f1 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -249,20 +249,20 @@ type SternAPI = :> Get '[JSON] TeamAdminInfo ) :<|> Named "get-route-legalhold-config" (MkFeatureGetRoute LegalholdConfig) - :<|> Named "put-route-legalhold-config" (MkFeaturePutRouteTrivialConfigNoTTL LegalholdConfig) + :<|> Named "put-route-legalhold-config" (MkFeaturePutRouteNoTTL LegalholdConfig) :<|> Named "get-route-sso-config" (MkFeatureGetRoute SSOConfig) - :<|> Named "put-route-sso-config" (MkFeaturePutRouteTrivialConfigNoTTL SSOConfig) + :<|> Named "put-route-sso-config" (MkFeaturePutRouteNoTTL SSOConfig) :<|> Named "get-route-search-visibility-available-config" (MkFeatureGetRoute SearchVisibilityAvailableConfig) - :<|> Named "put-route-search-visibility-available-config" (MkFeaturePutRouteTrivialConfigNoTTL SearchVisibilityAvailableConfig) + :<|> Named "put-route-search-visibility-available-config" (MkFeaturePutRouteNoTTL SearchVisibilityAvailableConfig) :<|> Named "get-route-validate-saml-emails-config" (MkFeatureGetRoute ValidateSAMLEmailsConfig) - :<|> Named "put-route-validate-saml-emails-config" (MkFeaturePutRouteTrivialConfigNoTTL ValidateSAMLEmailsConfig) + :<|> Named "put-route-validate-saml-emails-config" (MkFeaturePutRouteNoTTL ValidateSAMLEmailsConfig) :<|> Named "get-route-digital-signatures-config" (MkFeatureGetRoute DigitalSignaturesConfig) - :<|> Named "put-route-digital-signatures-config" (MkFeaturePutRouteTrivialConfigNoTTL DigitalSignaturesConfig) + :<|> Named "put-route-digital-signatures-config" (MkFeaturePutRouteNoTTL DigitalSignaturesConfig) :<|> Named "get-route-file-sharing-config" (MkFeatureGetRoute FileSharingConfig) - :<|> Named "put-route-file-sharing-config" (MkFeaturePutRouteTrivialConfigNoTTL FileSharingConfig) + :<|> Named "put-route-file-sharing-config" (MkFeaturePutRouteNoTTL FileSharingConfig) :<|> Named "get-route-classified-domains-config" (MkFeatureGetRoute ClassifiedDomainsConfig) :<|> Named "get-route-conference-calling-config" (MkFeatureGetRoute ConferenceCallingConfig) - :<|> Named "put-route-conference-calling-config" (MkFeaturePutRouteTrivialConfigWithTTL ConferenceCallingConfig) + :<|> Named "put-route-conference-calling-config" (MkFeaturePutRouteWithTTL ConferenceCallingConfig) :<|> Named "get-route-applock-config" (MkFeatureGetRoute AppLockConfig) :<|> Named "put-route-applock-config" (MkFeaturePutRoute AppLockConfig) :<|> Named "get-route-mls-config" (MkFeatureGetRoute MLSConfig) @@ -293,8 +293,8 @@ type SternAPI = :> Put '[JSON] NoContent ) :<|> Named "get-route-outlook-cal-config" (MkFeatureGetRoute OutlookCalIntegrationConfig) - :<|> Named "lock-unlock-route-outlook-cal-config" (MkFeatureLockUnlockRouteTrivialConfigNoTTL OutlookCalIntegrationConfig) - :<|> Named "put-route-outlook-cal-config" (MkFeaturePutRouteTrivialConfigNoTTL OutlookCalIntegrationConfig) + :<|> Named "lock-unlock-route-outlook-cal-config" (MkFeatureLockUnlockRouteNoTTL OutlookCalIntegrationConfig) + :<|> Named "put-route-outlook-cal-config" (MkFeaturePutRouteNoTTL OutlookCalIntegrationConfig) :<|> Named "get-route-enforce-file-download-location" ( Description @@ -305,7 +305,7 @@ type SternAPI = "lock-unlock-route-enforce-file-download-location" ( Description "

Custom feature: only supported for some decidated on-prem systems.

" - :> MkFeatureLockUnlockRouteTrivialConfigNoTTL EnforceFileDownloadLocationConfig + :> MkFeatureLockUnlockRouteNoTTL EnforceFileDownloadLocationConfig ) :<|> Named "put-route-enforce-file-download-location" @@ -486,7 +486,7 @@ type MkFeatureGetRoute (feature :: Type) = :> FeatureSymbol feature :> Get '[JSON] (WithStatus feature) -type MkFeaturePutRouteTrivialConfigNoTTL (feature :: Type) = +type MkFeaturePutRouteNoTTL (feature :: Type) = Summary "Disable / enable status for a given feature / team" :> "teams" :> Capture "tid" TeamId @@ -495,7 +495,7 @@ type MkFeaturePutRouteTrivialConfigNoTTL (feature :: Type) = :> QueryParam' [Required, Strict] "status" FeatureStatus :> Put '[JSON] NoContent -type MkFeaturePutRouteTrivialConfigWithTTL (feature :: Type) = +type MkFeaturePutRouteWithTTL (feature :: Type) = Summary "Disable / enable status for a given feature / team" :> Description "team feature time to live, given in days, or 'unlimited' (default). only available on *some* features!" :> "teams" @@ -506,7 +506,7 @@ type MkFeaturePutRouteTrivialConfigWithTTL (feature :: Type) = :> QueryParam' [Required, Strict, Description "team feature time to live, given in days, or 'unlimited' (default)."] "ttl" FeatureTTLDays :> Put '[JSON] NoContent -type MkFeatureLockUnlockRouteTrivialConfigNoTTL (feature :: Type) = +type MkFeatureLockUnlockRouteNoTTL (feature :: Type) = Summary "Lock / unlock status for a given feature / team (en-/disable should happen in team settings)" :> "teams" :> Capture "tid" TeamId diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 8baf5875930..939c60be287 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -44,6 +44,7 @@ module Stern.Intra setBlacklistStatus, getTeamFeatureFlag, setTeamFeatureFlag, + patchTeamFeatureFlag, setTeamFeatureLockStatus, getTeamData, getSearchVisibility, @@ -68,7 +69,7 @@ module Stern.Intra ) where -import Bilge hiding (head, options, path, paths, requestId) +import Bilge hiding (head, options, patch, path, paths, requestId) import Bilge qualified import Bilge.RPC import Brig.Types.Intra @@ -532,38 +533,58 @@ setTeamFeatureFlag :: setTeamFeatureFlag tid status = do info $ msg "Setting team feature status" checkDaysLimit (wssTTL status) + galleyRpc $ + method PUT + . Bilge.paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . Bilge.json status + . contentJson + +patchTeamFeatureFlag :: + forall cfg. + ( ToJSON (Public.WithStatusPatch cfg), + KnownSymbol (Public.FeatureSymbol cfg) + ) => + TeamId -> + Public.WithStatusPatch cfg -> + Handler () +patchTeamFeatureFlag tid patch = do + info $ msg "Patching team feature status" + for_ (wspTTL patch) $ \ttl -> checkDaysLimit ttl + galleyRpc $ + method PATCH + . Bilge.paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . Bilge.json patch + . contentJson + +galleyRpc :: (Bilge.Request -> Bilge.Request) -> Handler () +galleyRpc req = do gly <- view galley - let req = - method PUT - . Bilge.paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] - . Bilge.json status - . contentJson resp <- catchRpcErrors $ rpc' "galley" gly req case statusCode resp of 200 -> pure () 404 -> throwE (mkError status404 "bad-upstream" "team does not exist") - 403 -> throwE (mkError status403 "bad-upstream" "legal hold config cannot be changed") + 403 -> throwE (mkError status403 "bad-upstream" "config cannot be changed") _ -> throwE (mkError status502 "bad-upstream" (errorMessage resp)) - where - checkDaysLimit :: FeatureTTL -> Handler () - checkDaysLimit = \case - FeatureTTLUnlimited -> pure () - FeatureTTLSeconds ((`div` (60 * 60 * 24)) -> days) -> do - unless (days <= daysLimit) $ do - throwE - ( mkError - status400 - "bad-data" - ( LT.pack $ - "ttl limit is " - <> show daysLimit - <> " days; I got " - <> show days - <> "." - ) + +checkDaysLimit :: FeatureTTL -> Handler () +checkDaysLimit = \case + FeatureTTLUnlimited -> pure () + FeatureTTLSeconds ((`div` (60 * 60 * 24)) -> days) -> do + unless (days <= daysLimit) $ do + throwE + ( mkError + status400 + "bad-data" + ( LT.pack $ + "ttl limit is " + <> show daysLimit + <> " days; I got " + <> show days + <> "." ) - where - daysLimit = 2000 + ) + where + daysLimit = 2000 setTeamFeatureLockStatus :: forall cfg. diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index 69b351cabdb..b35aadcf554 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -85,7 +85,7 @@ tests s = test s "/teams/:tid/features/validateSamlEmails" $ testFeatureStatus @ValidateSAMLEmailsConfig, test s "/teams/:tid/features/digitalSignatures" $ testFeatureStatus @DigitalSignaturesConfig, test s "/teams/:tid/features/fileSharing" $ testFeatureStatus @FileSharingConfig, - test s "/teams/:tid/features/conference-calling" $ testFeatureStatusOptTtl @ConferenceCallingConfig (Just FeatureTTLUnlimited), + test s "/teams/:tid/features/conference-calling" $ testFeatureStatusOptTtl defConfCalling (Just FeatureTTLUnlimited), test s "/teams/:tid/searchVisibility" $ testFeatureStatus @SearchVisibilityAvailableConfig, test s "/teams/:tid/features/appLock" $ testFeatureConfig @AppLockConfig, test s "/teams/:tid/features/mls" $ testFeatureConfig @MLSConfig, @@ -105,6 +105,9 @@ tests s = -- - `POST /teams/:tid/billing` ] +defConfCalling :: WithStatus ConferenceCallingConfig +defConfCalling = setStatus FeatureStatusDisabled defFeatureStatus + testRudSsoDomainRedirect :: TestM () testRudSsoDomainRedirect = do testGet 1 Nothing @@ -307,7 +310,7 @@ testFeatureStatus :: Show cfg ) => TestM () -testFeatureStatus = testFeatureStatusOptTtl @cfg Nothing +testFeatureStatus = testFeatureStatusOptTtl (defFeatureStatus @cfg) Nothing testFeatureStatusOptTtl :: forall cfg. @@ -318,12 +321,13 @@ testFeatureStatusOptTtl :: Eq cfg, Show cfg ) => + WithStatus cfg -> Maybe FeatureTTL -> TestM () -testFeatureStatusOptTtl mTtl = do +testFeatureStatusOptTtl defValue mTtl = do (_, tid, _) <- createTeamWithNMembers 10 cfg <- getFeatureConfig @cfg tid - liftIO $ cfg @?= defFeatureStatus @cfg + liftIO $ cfg @?= defValue when (wsLockStatus cfg == LockStatusLocked) $ unlockFeature @cfg tid let newStatus = if wsStatus cfg == FeatureStatusEnabled then FeatureStatusDisabled else FeatureStatusEnabled putFeatureStatus @cfg tid newStatus mTtl !!! const 200 === statusCode From 44823f0a7c21b9344b76ff11d7d251915a2591ae Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 6 Aug 2024 15:06:47 +0200 Subject: [PATCH 025/136] Finalise version 6 (#4179) * Fix swagger generation for Versioned responses * Introduce version 7 and finalise version 6 * Move changes to capabilities field to v7 * Add pregenerated v6 swagger * Add CHANGELOG entries * Fix swagger tests --- changelog.d/1-api-changes/capabilities-v7 | 1 + changelog.d/1-api-changes/finalise-v6 | 1 + integration/test/Test/Notifications.hs | 4 +- integration/test/Test/Swagger.hs | 4 +- integration/test/Testlib/Env.hs | 2 +- .../src/Wire/API/Routes/Public/Brig.hs | 22 +- .../src/Wire/API/Routes/Public/Brig/Bot.hs | 8 +- libs/wire-api/src/Wire/API/Routes/Version.hs | 4 +- .../wire-api/src/Wire/API/Routes/Versioned.hs | 6 +- libs/wire-api/src/Wire/API/User/Client.hs | 13 +- libs/wire-api/src/Wire/API/UserEvent.hs | 2 +- .../golden/Test/Wire/API/Golden/Generated.hs | 2 +- ...1.json => testObject_ClientV6_user_1.json} | 0 ....json => testObject_ClientV6_user_10.json} | 0 ....json => testObject_ClientV6_user_11.json} | 0 ....json => testObject_ClientV6_user_12.json} | 0 ....json => testObject_ClientV6_user_13.json} | 0 ....json => testObject_ClientV6_user_14.json} | 0 ....json => testObject_ClientV6_user_15.json} | 0 ....json => testObject_ClientV6_user_16.json} | 0 ....json => testObject_ClientV6_user_17.json} | 0 ....json => testObject_ClientV6_user_18.json} | 0 ....json => testObject_ClientV6_user_19.json} | 0 ...2.json => testObject_ClientV6_user_2.json} | 0 ....json => testObject_ClientV6_user_20.json} | 0 ...3.json => testObject_ClientV6_user_3.json} | 0 ...4.json => testObject_ClientV6_user_4.json} | 0 ...5.json => testObject_ClientV6_user_5.json} | 0 ...6.json => testObject_ClientV6_user_6.json} | 0 ...7.json => testObject_ClientV6_user_7.json} | 0 ...8.json => testObject_ClientV6_user_8.json} | 0 ...9.json => testObject_ClientV6_user_9.json} | 0 services/brig/docs/swagger-v6.json | 30770 ++++++++++++++++ services/brig/src/Brig/API/Public.hs | 54 +- services/brig/src/Brig/Provider/API.hs | 2 +- .../brig/test/integration/API/User/Client.hs | 2 +- tools/stern/src/Stern/Intra.hs | 2 +- 37 files changed, 30827 insertions(+), 72 deletions(-) create mode 100644 changelog.d/1-api-changes/capabilities-v7 create mode 100644 changelog.d/1-api-changes/finalise-v6 rename libs/wire-api/test/golden/{testObject_ClientV5_user_1.json => testObject_ClientV6_user_1.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_10.json => testObject_ClientV6_user_10.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_11.json => testObject_ClientV6_user_11.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_12.json => testObject_ClientV6_user_12.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_13.json => testObject_ClientV6_user_13.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_14.json => testObject_ClientV6_user_14.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_15.json => testObject_ClientV6_user_15.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_16.json => testObject_ClientV6_user_16.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_17.json => testObject_ClientV6_user_17.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_18.json => testObject_ClientV6_user_18.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_19.json => testObject_ClientV6_user_19.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_2.json => testObject_ClientV6_user_2.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_20.json => testObject_ClientV6_user_20.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_3.json => testObject_ClientV6_user_3.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_4.json => testObject_ClientV6_user_4.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_5.json => testObject_ClientV6_user_5.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_6.json => testObject_ClientV6_user_6.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_7.json => testObject_ClientV6_user_7.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_8.json => testObject_ClientV6_user_8.json} (100%) rename libs/wire-api/test/golden/{testObject_ClientV5_user_9.json => testObject_ClientV6_user_9.json} (100%) create mode 100644 services/brig/docs/swagger-v6.json diff --git a/changelog.d/1-api-changes/capabilities-v7 b/changelog.d/1-api-changes/capabilities-v7 new file mode 100644 index 00000000000..2516454b30f --- /dev/null +++ b/changelog.d/1-api-changes/capabilities-v7 @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000000..03633def115 --- /dev/null +++ b/changelog.d/1-api-changes/finalise-v6 @@ -0,0 +1 @@ +Finalise version 6 and introduce new development version 7 diff --git a/integration/test/Test/Notifications.hs b/integration/test/Test/Notifications.hs index b94060814ca..c9116d80ec9 100644 --- a/integration/test/Test/Notifications.hs +++ b/integration/test/Test/Notifications.hs @@ -93,12 +93,12 @@ testInvalidNotification = do $ getNotifications user def {since = Just notifId} >>= getJSON 404 --- | Check that client-add notifications use the V5 format: +-- | Check that client-add notifications use the V6 format: -- @ -- "capabilities": { "capabilities": [..] } -- @ -- --- Migration plan: clients must be able to parse both old and new schema starting from V6. Once V5 is deprecated, the backend can start sending notifications in the new form. +-- Migration plan: clients must be able to parse both old and new schema starting from V7. Once V6 is deprecated, the backend can start sending notifications in the new form. testAddClientNotification :: (HasCallStack) => App () testAddClientNotification = do alice <- randomUser OwnDomain def diff --git a/integration/test/Test/Swagger.hs b/integration/test/Test/Swagger.hs index 5836ead12e0..b7f7618092c 100644 --- a/integration/test/Test/Swagger.hs +++ b/integration/test/Test/Swagger.hs @@ -8,7 +8,7 @@ import Testlib.Assertions import Testlib.Prelude existingVersions :: Set Int -existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6] +existingVersions = Set.fromList [0, 1, 2, 3, 4, 5, 6, 7] internalApis :: Set String internalApis = Set.fromList ["brig", "cannon", "cargohold", "cannon", "spar"] @@ -79,4 +79,4 @@ 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/
" + 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/
" diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 6f1e7d4a2a9..7c9bd150e9b 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -103,7 +103,7 @@ mkGlobalEnv cfgFile = do gFederationV0Domain = intConfig.federationV0.originDomain, gFederationV1Domain = intConfig.federationV1.originDomain, gDynamicDomains = (.domain) <$> Map.elems intConfig.dynamicBackends, - gDefaultAPIVersion = 6, + gDefaultAPIVersion = 7, gManager = manager, gServicesCwdBase = devEnvProjectRoot <&> ( "services"), gBackendResourcePool = resourcePool, 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 d85673496e0..1b77cd8b1c1 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -734,9 +734,9 @@ type UserClientAPI = -- - ClientAdded event to self -- - ClientRemoved event to self, if removing old clients due to max number Named - "add-client-v5" + "add-client-v6" ( Summary "Register a new client" - :> Until 'V6 + :> Until 'V7 :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'TooManyClients :> CanThrow 'MissingAuth @@ -753,7 +753,7 @@ type UserClientAPI = ( WithHeaders ClientHeaders Client - (VersionedRespond 'V5 201 "Client registered" Client) + (VersionedRespond 'V6 201 "Client registered" Client) ) ) :<|> Named @@ -803,21 +803,21 @@ type UserClientAPI = :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 200 "Client deleted"] () ) :<|> Named - "list-clients-v5" + "list-clients-v6" ( Summary "List the registered clients" - :> Until 'V6 + :> Until 'V7 :> ZUser :> "clients" :> MultiVerb1 'GET '[JSON] - ( VersionedRespond 'V5 200 "List of clients" [Client] + ( VersionedRespond 'V6 200 "List of clients" [Client] ) ) :<|> Named "list-clients" ( Summary "List the registered clients" - :> From 'V6 + :> From 'V7 :> ZUser :> "clients" :> MultiVerb1 @@ -827,9 +827,9 @@ type UserClientAPI = ) ) :<|> Named - "get-client-v5" + "get-client-v6" ( Summary "Get a registered client by ID" - :> Until 'V6 + :> Until 'V7 :> ZUser :> "clients" :> CaptureClientId "client" @@ -837,14 +837,14 @@ type UserClientAPI = 'GET '[JSON] '[ EmptyErrorForLegacyReasons 404 "Client not found", - VersionedRespond 'V5 200 "Client found" Client + VersionedRespond 'V6 200 "Client found" Client ] (Maybe Client) ) :<|> Named "get-client" ( Summary "Get a registered client by ID" - :> From 'V6 + :> From 'V7 :> ZUser :> "clients" :> CaptureClientId "client" 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 635e6711c4a..8f5719ad2d4 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 @@ -114,9 +114,9 @@ type BotAPI = :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "") ) :<|> Named - "bot-get-client-v5" + "bot-get-client-v6" ( Summary "Get client for bot" - :> Until 'V6 + :> Until 'V7 :> CanThrow 'AccessDenied :> CanThrow 'ClientNotFound :> ZBot @@ -126,14 +126,14 @@ type BotAPI = 'GET '[JSON] '[ ErrorResponse 'ClientNotFound, - VersionedRespond 'V5 200 "Client found" Client + VersionedRespond 'V6 200 "Client found" Client ] (Maybe Client) ) :<|> Named "bot-get-client" ( Summary "Get client for bot" - :> From 'V6 + :> From 'V7 :> CanThrow 'AccessDenied :> CanThrow 'ClientNotFound :> ZBot diff --git a/libs/wire-api/src/Wire/API/Routes/Version.hs b/libs/wire-api/src/Wire/API/Routes/Version.hs index 2256e54ac69..ec1673aee49 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version.hs @@ -83,7 +83,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (GenericUniform)) -- and 'developmentVersions' stay in sync; everything else here should keep working without -- change. See also documentation in the *docs* directory. -- https://docs.wire.com/developer/developer/api-versioning.html#version-bump-checklist -data Version = V0 | V1 | V2 | V3 | V4 | V5 | V6 +data Version = V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (FromJSON, ToJSON) via (Schema Version) deriving (Arbitrary) via (GenericUniform Version) @@ -102,6 +102,7 @@ versionInt V3 = 3 versionInt V4 = 4 versionInt V5 = 5 versionInt V6 = 6 +versionInt V7 = 7 supportedVersions :: [Version] supportedVersions = [minBound .. maxBound] @@ -211,6 +212,7 @@ isDevelopmentVersion V2 = False isDevelopmentVersion V3 = False isDevelopmentVersion V4 = False isDevelopmentVersion V5 = False +isDevelopmentVersion V6 = False isDevelopmentVersion _ = True developmentVersions :: [Version] diff --git a/libs/wire-api/src/Wire/API/Routes/Versioned.hs b/libs/wire-api/src/Wire/API/Routes/Versioned.hs index 405ec783e00..685fcbb3291 100644 --- a/libs/wire-api/src/Wire/API/Routes/Versioned.hs +++ b/libs/wire-api/src/Wire/API/Routes/Versioned.hs @@ -89,10 +89,10 @@ instance responseUnrender c = fmap unVersioned . responseUnrender @cs @(Respond s desc (Versioned v a)) c instance - (KnownSymbol desc, S.ToSchema a) => - IsSwaggerResponse (VersionedRespond v s desc a) + (KnownSymbol desc, S.ToSchema a, SingI v, ToSchema (Versioned v a), Typeable v) => + IsSwaggerResponse (VersionedRespond (v :: Version) s desc a) where - responseSwagger = simpleResponseSwagger @a @'[JSON] @desc + responseSwagger = simpleResponseSwagger @(Versioned v a) @'[JSON] @desc ------------------------------------------------------------------------------- -- Versioned newtype wrapper diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index fc80e19c4e7..d9b7756419e 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -88,7 +88,6 @@ import Data.Qualified import Data.SOP hiding (fn) import Data.Schema import Data.Set qualified as Set -import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Time.Clock import Data.UUID (toASCIIBytes) @@ -502,7 +501,7 @@ mlsPublicKeysSchema = clientSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc Client clientSchema mv = - object ("Client" <> T.pack (foldMap show mv)) $ + object "Client" $ Client <$> clientId .= field "id" schema <*> clientType .= field "type" schema @@ -518,20 +517,20 @@ clientSchema mv = caps :: ObjectSchemaP SwaggerDoc ClientCapabilityList (Maybe ClientCapabilityList) caps = case mv of -- broken capability serialisation for backwards compatibility - Just v | v <= V5 -> optField "capabilities" schema + Just v | v <= V6 -> optField "capabilities" schema _ -> fmap ClientCapabilityList <$> fromClientCapabilityList .= capabilitiesFieldSchema instance ToSchema Client where schema = clientSchema Nothing -instance ToSchema (Versioned 'V5 Client) where - schema = Versioned <$> unVersioned .= clientSchema (Just V5) +instance ToSchema (Versioned 'V6 Client) where + schema = Versioned <$> unVersioned .= clientSchema (Just V6) -instance {-# OVERLAPPING #-} ToSchema (Versioned 'V5 [Client]) where +instance {-# OVERLAPPING #-} ToSchema (Versioned 'V6 [Client]) where schema = Versioned <$> unVersioned - .= named "ClientList" (array (clientSchema (Just V5))) + .= named "ClientList" (array (clientSchema (Just V6))) mlsPublicKeysFieldSchema :: ObjectSchema SwaggerDoc MLSPublicKeys mlsPublicKeysFieldSchema = fromMaybe mempty <$> optField "mls_public_keys" mlsPublicKeysSchema diff --git a/libs/wire-api/src/Wire/API/UserEvent.hs b/libs/wire-api/src/Wire/API/UserEvent.hs index 23f6de751e9..5c3e9386af5 100644 --- a/libs/wire-api/src/Wire/API/UserEvent.hs +++ b/libs/wire-api/src/Wire/API/UserEvent.hs @@ -348,7 +348,7 @@ eventObjectSchema = _ClientEvent ( tag _ClientAdded - (field "client" (clientSchema (Just V5))) + (field "client" (clientSchema (Just V6))) ) EventTypeClientRemoved -> tag 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 d2c152497d3..cf0a3e20eaf 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 @@ -1014,7 +1014,7 @@ tests = testGroup "Golden: PubClient_user" $ testObjects [(Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_1, "testObject_PubClient_user_1.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_2, "testObject_PubClient_user_2.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_3, "testObject_PubClient_user_3.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_4, "testObject_PubClient_user_4.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_5, "testObject_PubClient_user_5.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_6, "testObject_PubClient_user_6.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_7, "testObject_PubClient_user_7.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_8, "testObject_PubClient_user_8.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_9, "testObject_PubClient_user_9.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_10, "testObject_PubClient_user_10.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_11, "testObject_PubClient_user_11.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_12, "testObject_PubClient_user_12.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_13, "testObject_PubClient_user_13.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_14, "testObject_PubClient_user_14.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_15, "testObject_PubClient_user_15.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_16, "testObject_PubClient_user_16.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_17, "testObject_PubClient_user_17.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_18, "testObject_PubClient_user_18.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_19, "testObject_PubClient_user_19.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_20, "testObject_PubClient_user_20.json")], testGroup "Golden: ClientV5_user" $ - testObjects [(Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_ClientV5_user_1.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_ClientV5_user_2.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_ClientV5_user_3.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_ClientV5_user_4.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_ClientV5_user_5.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_ClientV5_user_6.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_ClientV5_user_7.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_ClientV5_user_8.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_ClientV5_user_9.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_ClientV5_user_10.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_ClientV5_user_11.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_ClientV5_user_12.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_ClientV5_user_13.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_ClientV5_user_14.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_ClientV5_user_15.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_ClientV5_user_16.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_ClientV5_user_17.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_ClientV5_user_18.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_ClientV5_user_19.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_ClientV5_user_20.json")], + testObjects [(Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_ClientV6_user_1.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_ClientV6_user_2.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_ClientV6_user_3.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_ClientV6_user_4.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_ClientV6_user_5.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_ClientV6_user_6.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_ClientV6_user_7.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_ClientV6_user_8.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_ClientV6_user_9.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_ClientV6_user_10.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_ClientV6_user_11.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_ClientV6_user_12.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_ClientV6_user_13.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_ClientV6_user_14.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_ClientV6_user_15.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_ClientV6_user_16.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_ClientV6_user_17.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_ClientV6_user_18.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_ClientV6_user_19.json"), (Versioned @'V6 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_ClientV6_user_20.json")], testGroup "Golden: Client_user" $ testObjects [(Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_Client_user_1.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_Client_user_2.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_Client_user_3.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_Client_user_4.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_Client_user_5.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_Client_user_6.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_Client_user_7.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_Client_user_8.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_Client_user_9.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_Client_user_10.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_Client_user_11.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_Client_user_12.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_Client_user_13.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_Client_user_14.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_Client_user_15.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_Client_user_16.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_Client_user_17.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_Client_user_18.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_Client_user_19.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_Client_user_20.json")], testGroup "Golden: NewClient_user" $ diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_1.json b/libs/wire-api/test/golden/testObject_ClientV6_user_1.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_1.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_1.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_10.json b/libs/wire-api/test/golden/testObject_ClientV6_user_10.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_10.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_10.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_11.json b/libs/wire-api/test/golden/testObject_ClientV6_user_11.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_11.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_11.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_12.json b/libs/wire-api/test/golden/testObject_ClientV6_user_12.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_12.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_12.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_13.json b/libs/wire-api/test/golden/testObject_ClientV6_user_13.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_13.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_13.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_14.json b/libs/wire-api/test/golden/testObject_ClientV6_user_14.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_14.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_14.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_15.json b/libs/wire-api/test/golden/testObject_ClientV6_user_15.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_15.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_15.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_16.json b/libs/wire-api/test/golden/testObject_ClientV6_user_16.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_16.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_16.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_17.json b/libs/wire-api/test/golden/testObject_ClientV6_user_17.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_17.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_17.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_18.json b/libs/wire-api/test/golden/testObject_ClientV6_user_18.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_18.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_18.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_19.json b/libs/wire-api/test/golden/testObject_ClientV6_user_19.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_19.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_19.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_2.json b/libs/wire-api/test/golden/testObject_ClientV6_user_2.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_2.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_2.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_20.json b/libs/wire-api/test/golden/testObject_ClientV6_user_20.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_20.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_20.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_3.json b/libs/wire-api/test/golden/testObject_ClientV6_user_3.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_3.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_3.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_4.json b/libs/wire-api/test/golden/testObject_ClientV6_user_4.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_4.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_4.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_5.json b/libs/wire-api/test/golden/testObject_ClientV6_user_5.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_5.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_5.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_6.json b/libs/wire-api/test/golden/testObject_ClientV6_user_6.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_6.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_6.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_7.json b/libs/wire-api/test/golden/testObject_ClientV6_user_7.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_7.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_7.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_8.json b/libs/wire-api/test/golden/testObject_ClientV6_user_8.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_8.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_8.json diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_9.json b/libs/wire-api/test/golden/testObject_ClientV6_user_9.json similarity index 100% rename from libs/wire-api/test/golden/testObject_ClientV5_user_9.json rename to libs/wire-api/test/golden/testObject_ClientV6_user_9.json diff --git a/services/brig/docs/swagger-v6.json b/services/brig/docs/swagger-v6.json new file mode 100644 index 00000000000..6d4d1b91149 --- /dev/null +++ b/services/brig/docs/swagger-v6.json @@ -0,0 +1,30770 @@ +{ + "components": { + "schemas": { + "": { + "enum": [ + "audio", + "books", + "business", + "design", + "education", + "entertainment", + "finance", + "fitness", + "food-drink", + "games", + "graphics", + "health", + "integration", + "lifestyle", + "media", + "medical", + "movies", + "music", + "news", + "photography", + "poll", + "productivity", + "quiz", + "rating", + "shopping", + "social", + "sports", + "travel", + "tutorial", + "video", + "weather" + ], + "type": "string" + }, + "ASCII": { + "example": "aGVsbG8", + "type": "string" + }, + "Access": { + "description": "How users can join conversations", + "enum": [ + "private", + "invite", + "link", + "code" + ], + "type": "string" + }, + "AccessRole": { + "description": "Which users/services can join conversations. This replaces legacy access roles and allows a more fine grained configuration of access roles, and in particular a separation of guest and services access.\n\nThis field is optional. If it is not present, the default will be `[team_member, non_team_member, service]`. Please note that an empty list is not allowed when creating a new conversation.", + "enum": [ + "team_member", + "non_team_member", + "guest", + "service" + ], + "type": "string" + }, + "AccessRoleLegacy": { + "deprecated": true, + "description": "Deprecated, please use access_role_v2", + "enum": [ + "private", + "team", + "activated", + "non_activated" + ], + "type": "string" + }, + "AccessToken": { + "properties": { + "access_token": { + "description": "The opaque access token string", + "type": "string" + }, + "expires_in": { + "description": "The number of seconds this token is valid", + "type": "integer" + }, + "token_type": { + "$ref": "#/components/schemas/TokenType" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "access_token", + "token_type", + "expires_in" + ], + "type": "object" + }, + "AccessTokenType": { + "enum": [ + "DPoP" + ], + "type": "string" + }, + "Action": { + "enum": [ + "add_conversation_member", + "remove_conversation_member", + "modify_conversation_name", + "modify_conversation_message_timer", + "modify_conversation_receipt_mode", + "modify_conversation_access", + "modify_other_conversation_member", + "leave_conversation", + "delete_conversation" + ], + "type": "string" + }, + "Activate": { + "description": "Data for an activation request.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "dryrun": { + "description": "At least one of key, email, or phone has to be present while key takes precedence over email, and email takes precedence over phone. Whether to perform a dryrun, i.e. to only check whether activation would succeed. Dry-runs never issue access cookies or tokens on success but failures still count towards the maximum failure count.", + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "code", + "dryrun" + ], + "type": "object" + }, + "ActivationResponse": { + "description": "Response body of a successful activation request", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "first": { + "description": "Whether this is the first successful activation (i.e. account activation).", + "type": "boolean" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + } + }, + "type": "object" + }, + "AddBot": { + "properties": { + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "service": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "provider", + "service" + ], + "type": "object" + }, + "AddBotResponse": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "event": { + "$ref": "#/components/schemas/Event" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "id", + "client", + "name", + "accent_id", + "assets", + "event" + ], + "type": "object" + }, + "AllFeatureConfigs": { + "properties": { + "appLock": { + "$ref": "#/components/schemas/AppLockConfig.WithStatus" + }, + "classifiedDomains": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig.WithStatus" + }, + "conferenceCalling": { + "$ref": "#/components/schemas/ConferenceCallingConfig.WithStatus" + }, + "conversationGuestLinks": { + "$ref": "#/components/schemas/GuestLinksConfig.WithStatus" + }, + "digitalSignatures": { + "$ref": "#/components/schemas/DigitalSignaturesConfig.WithStatus" + }, + "enforceFileDownloadLocation": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.WithStatus" + }, + "exposeInvitationURLsToTeamAdmin": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + }, + "fileSharing": { + "$ref": "#/components/schemas/FileSharingConfig.WithStatus" + }, + "legalhold": { + "$ref": "#/components/schemas/LegalholdConfig.WithStatus" + }, + "limitedEventFanout": { + "$ref": "#/components/schemas/LimitedEventFanoutConfig.WithStatus" + }, + "mls": { + "$ref": "#/components/schemas/MLSConfig.WithStatus" + }, + "mlsE2EId": { + "$ref": "#/components/schemas/MlsE2EIdConfig.WithStatus" + }, + "mlsMigration": { + "$ref": "#/components/schemas/MlsMigration.WithStatus" + }, + "outlookCalIntegration": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.WithStatus" + }, + "searchVisibility": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.WithStatus" + }, + "searchVisibilityInbound": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.WithStatus" + }, + "selfDeletingMessages": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.WithStatus" + }, + "sndFactorPasswordChallenge": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.WithStatus" + }, + "sso": { + "$ref": "#/components/schemas/SSOConfig.WithStatus" + }, + "validateSAMLemails": { + "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.WithStatus" + } + }, + "required": [ + "legalhold", + "sso", + "searchVisibility", + "searchVisibilityInbound", + "validateSAMLemails", + "digitalSignatures", + "appLock", + "fileSharing", + "classifiedDomains", + "conferenceCalling", + "selfDeletingMessages", + "conversationGuestLinks", + "sndFactorPasswordChallenge", + "mls", + "exposeInvitationURLsToTeamAdmin", + "outlookCalIntegration", + "mlsE2EId", + "mlsMigration", + "enforceFileDownloadLocation", + "limitedEventFanout" + ], + "type": "object" + }, + "Alpha": { + "enum": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRO", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLL", + "SOS", + "SRD", + "SSP", + "STD", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "UYI", + "UYU", + "UZS", + "VEF", + "VND", + "VUV", + "WST", + "XAF", + "XAG", + "XAU", + "XBA", + "XBB", + "XBC", + "XBD", + "XCD", + "XDR", + "XOF", + "XPD", + "XPF", + "XPT", + "XSU", + "XTS", + "XUA", + "XXX", + "YER", + "ZAR", + "ZMW", + "ZWL" + ], + "type": "string" + }, + "AppLockConfig": { + "properties": { + "enforceAppLock": { + "type": "boolean" + }, + "inactivityTimeoutSecs": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforceAppLock", + "inactivityTimeoutSecs" + ], + "type": "object" + }, + "AppLockConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/components/schemas/AppLockConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "AppLockConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/components/schemas/AppLockConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "ApproveLegalHoldForUserRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "Asset": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "expires": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "key": { + "$ref": "#/components/schemas/AssetKey" + }, + "token": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "domain" + ], + "type": "object" + }, + "AssetKey": { + "example": "3-1-47de4580-ae51-4650-acbb-d10c028cb0ac", + "type": "string" + }, + "AssetSize": { + "enum": [ + "preview", + "complete" + ], + "type": "string" + }, + "AssetSource": {}, + "AssetType": { + "enum": [ + "image" + ], + "type": "string" + }, + "AuthnRequest": { + "properties": { + "iD": { + "$ref": "#/components/schemas/ID_*_AuthnRequest" + }, + "issueInstant": { + "$ref": "#/components/schemas/Time" + }, + "issuer": { + "type": "string" + }, + "nameIDPolicy": { + "$ref": "#/components/schemas/NameIdPolicy" + } + }, + "required": [ + "iD", + "issueInstant", + "issuer" + ], + "type": "object" + }, + "Base64ByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "Base64URLByteString": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "BaseProtocol": { + "enum": [ + "proteus", + "mls" + ], + "type": "string" + }, + "BindingNewTeamUser": { + "properties": { + "currency": { + "$ref": "#/components/schemas/Alpha" + }, + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "description": "team icon asset key", + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "members": { + "description": "initial team member ids (between 1 and 127)" + }, + "name": { + "description": "team name", + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "name", + "icon" + ], + "type": "object" + }, + "Body": {}, + "BotConvView": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "members": { + "items": { + "$ref": "#/components/schemas/OtherMember" + }, + "type": "array" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "members" + ], + "type": "object" + }, + "BotUserView": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "name", + "accent_id" + ], + "type": "object" + }, + "CheckHandles": { + "properties": { + "handles": { + "items": { + "type": "string" + }, + "maxItems": 50, + "minItems": 1, + "type": "array" + }, + "return": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "handles", + "return" + ], + "type": "object" + }, + "CipherSuiteTag": { + "description": "The cipher suite of the corresponding MLS group", + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "ClassifiedDomainsConfig": { + "properties": { + "domains": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "domains" + ], + "type": "object" + }, + "ClassifiedDomainsConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "Client": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "cookie": { + "type": "string" + }, + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "label": { + "type": "string" + }, + "last_active": { + "$ref": "#/components/schemas/UTCTime" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "type": { + "$ref": "#/components/schemas/ClientType" + } + }, + "required": [ + "id", + "type", + "time" + ], + "type": "object" + }, + "ClientCapability": { + "enum": [ + "legalhold-implicit-consent" + ], + "type": "string" + }, + "ClientCapabilityList": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/components/schemas/ClientCapability" + }, + "type": "array" + } + }, + "required": [ + "capabilities" + ], + "type": "object" + }, + "ClientClass": { + "enum": [ + "phone", + "tablet", + "desktop", + "legalhold" + ], + "type": "string" + }, + "ClientIdentity": { + "properties": { + "client_id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "user_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "user_id", + "client_id" + ], + "type": "object" + }, + "ClientListv6": { + "items": { + "$ref": "#/components/schemas/Client" + }, + "type": "array" + }, + "ClientMismatch": { + "properties": { + "deleted": { + "$ref": "#/components/schemas/UserClients" + }, + "missing": { + "$ref": "#/components/schemas/UserClients" + }, + "redundant": { + "$ref": "#/components/schemas/UserClients" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted" + ], + "type": "object" + }, + "ClientPrekey": { + "properties": { + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "prekey": { + "$ref": "#/components/schemas/Prekey" + } + }, + "required": [ + "client", + "prekey" + ], + "type": "object" + }, + "ClientType": { + "enum": [ + "temporary", + "permanent", + "legalhold" + ], + "type": "string" + }, + "Clientv6": { + "properties": { + "capabilities": { + "$ref": "#/components/schemas/ClientCapabilityList" + }, + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "cookie": { + "type": "string" + }, + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "label": { + "type": "string" + }, + "last_active": { + "$ref": "#/components/schemas/UTCTime" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "type": { + "$ref": "#/components/schemas/ClientType" + } + }, + "required": [ + "id", + "type", + "time" + ], + "type": "object" + }, + "CodeChallengeMethod": { + "description": "The method used to encode the code challenge. Only `S256` is supported.", + "enum": [ + "S256" + ], + "type": "string" + }, + "CommitBundle": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "CompletePasswordReset": { + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "key", + "code", + "password" + ], + "type": "object" + }, + "ConferenceCallingConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "Connect": { + "properties": { + "email": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "qualified_recipient": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "recipient": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "qualified_recipient" + ], + "type": "object" + }, + "ConnectionUpdate": { + "properties": { + "status": { + "$ref": "#/components/schemas/Relation" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Connections_Page": { + "properties": { + "connections": { + "items": { + "$ref": "#/components/schemas/UserConnection" + }, + "type": "array" + }, + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/Connections_PagingState" + } + }, + "required": [ + "connections", + "has_more", + "paging_state" + ], + "type": "object" + }, + "Connections_PagingState": { + "type": "string" + }, + "Contact": { + "description": "Contact discovered through search", + "properties": { + "accent_id": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "qualified_id", + "name" + ], + "type": "object" + }, + "ConvMembers": { + "description": "Users of a conversation", + "properties": { + "others": { + "description": "All other current users of this conversation", + "items": { + "$ref": "#/components/schemas/OtherMember" + }, + "type": "array" + }, + "self": { + "$ref": "#/components/schemas/Member" + } + }, + "required": [ + "self", + "others" + ], + "type": "object" + }, + "ConvTeamInfo": { + "description": "Team information of this conversation", + "properties": { + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + }, + "teamid": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "teamid", + "managed" + ], + "type": "object" + }, + "ConvType": { + "enum": [ + 0, + 1, + 2, + 3 + ], + "type": "integer" + }, + "Conversation": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "ConversationAccessData": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access", + "access_role" + ], + "type": "object" + }, + "ConversationAccessDataV2": { + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + } + }, + "required": [ + "access" + ], + "type": "object" + }, + "ConversationCode": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "ConversationCodeInfo": { + "description": "Contains conversation properties to update", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "key", + "code", + "has_password" + ], + "type": "object" + }, + "ConversationCoverView": { + "description": "Limited view of Conversation.", + "properties": { + "has_password": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "has_password" + ], + "type": "object" + }, + "ConversationIds_Page": { + "properties": { + "has_more": { + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/ConversationIds_PagingState" + }, + "qualified_conversations": { + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "qualified_conversations", + "has_more", + "paging_state" + ], + "type": "object" + }, + "ConversationIds_PagingState": { + "type": "string" + }, + "ConversationMessageTimerUpdate": { + "description": "Contains conversation properties to update", + "properties": { + "message_timer": { + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "type": "object" + }, + "ConversationReceiptModeUpdate": { + "description": "Contains conversation receipt mode to update to. Receipt mode tells clients whether certain types of receipts should be sent in the given conversation or not. How this value is interpreted is up to clients.", + "properties": { + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "receipt_mode" + ], + "type": "object" + }, + "ConversationRename": { + "properties": { + "name": { + "description": "The new conversation name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "ConversationRole": { + "properties": { + "actions": { + "description": "The set of actions allowed for this role", + "items": { + "$ref": "#/components/schemas/Action" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + } + } + }, + "ConversationRolesList": { + "properties": { + "conversation_roles": { + "items": { + "$ref": "#/components/schemas/ConversationRole" + }, + "type": "array" + } + }, + "required": [ + "conversation_roles" + ], + "type": "object" + }, + "ConversationV2": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "members", + "group_id", + "epoch", + "cipher_suite" + ], + "type": "object" + }, + "ConversationV3v3": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch", + "cipher_suite" + ], + "type": "object" + }, + "ConversationV6v6": { + "description": "A conversation object as returned from the server", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch" + ], + "type": "object" + }, + "ConversationsResponse": { + "description": "Response object for getting metadata of a list of conversations", + "properties": { + "failed": { + "description": "The server failed to fetch these conversations, most likely due to network issues while contacting a remote server", + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/components/schemas/Conversation" + }, + "type": "array" + }, + "not_found": { + "description": "These conversations either don't exist or are deleted.", + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "type": "array" + } + }, + "required": [ + "found", + "not_found", + "failed" + ], + "type": "object" + }, + "Cookie": { + "properties": { + "created": { + "$ref": "#/components/schemas/UTCTime" + }, + "expires": { + "$ref": "#/components/schemas/UTCTime" + }, + "id": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "label": { + "type": "string" + }, + "successor": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": { + "$ref": "#/components/schemas/CookieType" + } + }, + "required": [ + "id", + "type", + "created", + "expires" + ], + "type": "object" + }, + "CookieList": { + "description": "List of cookie information", + "properties": { + "cookies": { + "items": { + "$ref": "#/components/schemas/Cookie" + }, + "type": "array" + } + }, + "required": [ + "cookies" + ], + "type": "object" + }, + "CookieType": { + "enum": [ + "session", + "persistent" + ], + "type": "string" + }, + "CreateConversationCodeRequest": { + "description": "Request body for creating a conversation code", + "properties": { + "password": { + "description": "Password for accessing the conversation via guest link. Set to null or omit for no password.", + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "type": "object" + }, + "CreateGroupConversationv6": { + "description": "A created group-conversation object extended with a list of failed-to-add users", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "failed_to_add": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + } + }, + "required": [ + "qualified_id", + "type", + "access", + "access_role", + "members", + "group_id", + "epoch", + "failed_to_add" + ], + "type": "object" + }, + "CreateOAuthAuthorizationCodeRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "code_challenge": { + "$ref": "#/components/schemas/OAuthCodeChallenge" + }, + "code_challenge_method": { + "$ref": "#/components/schemas/CodeChallengeMethod" + }, + "redirect_uri": { + "$ref": "#/components/schemas/RedirectUrl" + }, + "response_type": { + "$ref": "#/components/schemas/OAuthResponseType" + }, + "scope": { + "description": "The scopes which are requested to get authorization for, separated by a space", + "type": "string" + }, + "state": { + "description": "An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery", + "type": "string" + } + }, + "required": [ + "client_id", + "scope", + "response_type", + "redirect_uri", + "state", + "code_challenge_method", + "code_challenge" + ], + "type": "object" + }, + "CreateScimToken": { + "properties": { + "description": { + "type": "string" + }, + "password": { + "type": "string" + }, + "verification_code": { + "type": "string" + } + }, + "required": [ + "description" + ], + "type": "object" + }, + "CreateScimTokenResponse": { + "properties": { + "info": { + "$ref": "#/components/schemas/ScimTokenInfo" + }, + "token": { + "description": "Authentication token", + "type": "string" + } + }, + "required": [ + "token", + "info" + ], + "type": "object" + }, + "CustomBackend": { + "description": "Description of a custom backend", + "properties": { + "config_json_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "webapp_welcome_url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "config_json_url", + "webapp_welcome_url" + ], + "type": "object" + }, + "DPoPAccessToken": { + "type": "string" + }, + "DPoPAccessTokenResponse": { + "properties": { + "expires_in": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "token": { + "$ref": "#/components/schemas/DPoPAccessToken" + }, + "type": { + "$ref": "#/components/schemas/AccessTokenType" + } + }, + "required": [ + "token", + "type", + "expires_in" + ], + "type": "object" + }, + "DeleteClient": { + "properties": { + "password": { + "description": "The password of the authenticated user for verification. The password is not required for deleting temporary clients.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeleteKeyPackages": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackageRef" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "DeleteProvider": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "DeleteService": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "DeleteSubConversationRequest": { + "description": "Delete an MLS subconversation", + "properties": { + "epoch": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + } + }, + "required": [ + "group_id", + "epoch" + ], + "type": "object" + }, + "DeleteUser": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "DeletionCodeTimeout": { + "properties": { + "expires_in": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "expires_in" + ], + "type": "object" + }, + "DeprecatedMatchingResult": { + "deprecated": true, + "properties": { + "auto-connects": { + "items": {}, + "type": "array" + }, + "results": { + "items": {}, + "type": "array" + } + }, + "required": [ + "results", + "auto-connects" + ], + "type": "object" + }, + "DigitalSignaturesConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "DisableLegalHoldForUserRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "Domain": { + "example": "example.com", + "type": "string" + }, + "EdMemberLeftReason": { + "enum": [ + "left", + "user-deleted", + "removed" + ], + "type": "string" + }, + "Either_OAuthAccessTokenRequest_OAuthRefreshAccessTokenRequest": { + "oneOf": [ + { + "properties": { + "Left": { + "$ref": "#/components/schemas/OAuthAccessTokenRequest" + } + }, + "required": [ + "Left" + ], + "title": "Left", + "type": "object" + }, + { + "properties": { + "Right": { + "$ref": "#/components/schemas/OAuthRefreshAccessTokenRequest" + } + }, + "required": [ + "Right" + ], + "title": "Right", + "type": "object" + } + ] + }, + "Email": { + "type": "string" + }, + "EmailUpdate": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "EnforceFileDownloadLocation": { + "properties": { + "enforcedDownloadLocation": { + "type": "string" + } + }, + "type": "object" + }, + "EnforceFileDownloadLocation.WithStatus": { + "properties": { + "config": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "EnforceFileDownloadLocation.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "Event": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "data": { + "description": "Encrypted message of a conversation", + "example": "ZXhhbXBsZQo=", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "$ref": "#/components/schemas/AccessRoleLegacy" + }, + "access_role_v2": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "email": { + "type": "string" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "has_password": { + "description": "Whether the conversation has a password", + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "last_event": { + "type": "string" + }, + "last_event_time": { + "type": "string" + }, + "members": { + "$ref": "#/components/schemas/ConvMembers" + }, + "message": { + "type": "string" + }, + "message_timer": { + "description": "Per-conversation message timer (can be null)", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "protocol": { + "$ref": "#/components/schemas/Protocol" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "qualified_recipient": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "qualified_target": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "qualified_user_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "reason": { + "$ref": "#/components/schemas/EdMemberLeftReason" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "recipient": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/TypingStatus" + }, + "target": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/ConvType" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "user_ids": { + "deprecated": true, + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/components/schemas/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users", + "reason", + "qualified_user_ids", + "user_ids", + "qualified_target", + "name", + "access", + "key", + "code", + "has_password", + "qualified_id", + "type", + "members", + "group_id", + "epoch", + "cipher_suite", + "qualified_recipient", + "receipt_mode", + "sender", + "recipient", + "text", + "status" + ], + "type": "object" + }, + "from": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "qualified_from": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "subconv": { + "type": "string" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "type": { + "$ref": "#/components/schemas/EventType" + } + }, + "required": [ + "type", + "data", + "qualified_conversation", + "qualified_from", + "time" + ], + "type": "object" + }, + "EventType": { + "enum": [ + "conversation.member-join", + "conversation.member-leave", + "conversation.member-update", + "conversation.rename", + "conversation.access-update", + "conversation.receipt-mode-update", + "conversation.message-timer-update", + "conversation.code-update", + "conversation.code-delete", + "conversation.create", + "conversation.delete", + "conversation.connect-request", + "conversation.typing", + "conversation.otr-message-add", + "conversation.mls-message-add", + "conversation.mls-welcome", + "conversation.protocol-update" + ], + "type": "string" + }, + "ExposeInvitationURLsToTeamAdminConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "ExposeInvitationURLsToTeamAdminConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "FeatureStatus": { + "enum": [ + "enabled", + "disabled" + ], + "type": "string" + }, + "FederatedUserSearchPolicy": { + "description": "Search policy that was applied when searching for users", + "enum": [ + "no_search", + "exact_handle_search", + "full_search" + ], + "type": "string" + }, + "FileSharingConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "FileSharingConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Fingerprint": { + "example": "ioy3GeIjgQRsobf2EKGO3O8mq/FofFxHRqy0T4ERIZ8=", + "type": "string" + }, + "FormRedirect": { + "properties": { + "uri": { + "type": "string" + }, + "xml": { + "$ref": "#/components/schemas/AuthnRequest" + } + }, + "type": "object" + }, + "GetPaginated_Connections": { + "description": "A request to list some or all of a user's Connections, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/components/schemas/Connections_PagingState" + }, + "size": { + "description": "optional, must be <= 500, defaults to 100.", + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GetPaginated_ConversationIds": { + "description": "A request to list some or all of a user's ConversationIds, including remote ones", + "properties": { + "paging_state": { + "$ref": "#/components/schemas/ConversationIds_PagingState" + }, + "size": { + "description": "optional, must be <= 1000, defaults to 1000.", + "format": "int32", + "maximum": 1000, + "minimum": 1, + "type": "integer" + } + }, + "type": "object" + }, + "GroupId": { + "description": "A base64-encoded MLS group ID", + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "GroupInfoData": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "GuestLinksConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "GuestLinksConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Handle": { + "type": "string" + }, + "HandleUpdate": { + "properties": { + "handle": { + "type": "string" + } + }, + "required": [ + "handle" + ], + "type": "object" + }, + "HttpsUrl": { + "example": "https://example.com", + "type": "string" + }, + "ID_*_AuthnRequest": { + "properties": { + "iD": { + "$ref": "#/components/schemas/XmlText" + } + }, + "required": [ + "iD" + ], + "type": "object" + }, + "Icon": { + "type": "string" + }, + "Id": { + "properties": { + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "IdPConfig_WireIdP": { + "properties": { + "extraInfo": { + "$ref": "#/components/schemas/WireIdP" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "metadata": { + "$ref": "#/components/schemas/IdPMetadata" + } + }, + "required": [ + "id", + "metadata", + "extraInfo" + ], + "type": "object" + }, + "IdPList": { + "properties": { + "providers": { + "items": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + }, + "type": "array" + } + }, + "required": [ + "providers" + ], + "type": "object" + }, + "IdPMetadata": { + "properties": { + "certAuthnResponse": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array" + }, + "issuer": { + "type": "string" + }, + "requestURI": { + "type": "string" + } + }, + "required": [ + "issuer", + "requestURI", + "certAuthnResponse" + ], + "type": "object" + }, + "IdPMetadataInfo": { + "maxProperties": 1, + "minProperties": 1, + "properties": { + "value": { + "type": "string" + } + }, + "type": "object" + }, + "Invitation": { + "description": "An invitation to join a team on Wire", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters)", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "url": { + "$ref": "#/components/schemas/URIRef Absolute" + } + }, + "required": [ + "team", + "id", + "created_at", + "email" + ], + "type": "object" + }, + "InvitationList": { + "description": "A list of sent team invitations.", + "properties": { + "has_more": { + "description": "Indicator that the server has more invitations than returned.", + "type": "boolean" + }, + "invitations": { + "items": { + "$ref": "#/components/schemas/Invitation" + }, + "type": "array" + } + }, + "required": [ + "invitations", + "has_more" + ], + "type": "object" + }, + "InvitationRequest": { + "description": "A request to join a team on Wire.", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "name": { + "description": "Name of the invitee (1 - 128 characters).", + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "InviteQualified": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "qualified_users": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "JWK": { + "properties": { + "crv": { + "type": "string" + }, + "kty": { + "type": "string" + }, + "x": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "y": { + "example": "ZXhhbXBsZQo=", + "type": "string" + } + }, + "required": [ + "kty", + "crv", + "x" + ], + "type": "object" + }, + "JoinConversationByCode": { + "description": "Request body for joining a conversation by code", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "uri": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "KeyPackage": { + "example": "a2V5IHBhY2thZ2UgZGF0YQo=", + "type": "string" + }, + "KeyPackageBundle": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackageBundleEntry" + }, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "KeyPackageBundleEntry": { + "properties": { + "client": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "key_package": { + "$ref": "#/components/schemas/KeyPackage" + }, + "key_package_ref": { + "$ref": "#/components/schemas/KeyPackageRef" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "user", + "client", + "key_package_ref", + "key_package" + ], + "type": "object" + }, + "KeyPackageRef": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "KeyPackageUpload": { + "properties": { + "key_packages": { + "items": { + "$ref": "#/components/schemas/KeyPackage" + }, + "type": "array" + } + }, + "required": [ + "key_packages" + ], + "type": "object" + }, + "LHServiceStatus": { + "enum": [ + "configured", + "not_configured", + "disabled" + ], + "type": "string" + }, + "LegalholdConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LegalholdConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "LimitedEventFanoutConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "LimitedQualifiedUserIdList_500": { + "properties": { + "qualified_users": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + } + }, + "required": [ + "qualified_users" + ], + "type": "object" + }, + "List1": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "minItems": 1, + "type": "array" + }, + "ListConversations": { + "description": "A request to list some of a user's conversations, including remote ones. Maximum 1000 qualified conversation IDs", + "properties": { + "qualified_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "maxItems": 1000, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "qualified_ids" + ], + "type": "object" + }, + "ListType": { + "description": "true if 'members' doesn't contain all team members", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "ListUsersById": { + "properties": { + "failed": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "minItems": 1, + "type": "array" + }, + "found": { + "items": { + "$ref": "#/components/schemas/UserProfile" + }, + "type": "array" + } + }, + "required": [ + "found" + ], + "type": "object" + }, + "ListUsersQuery": { + "description": "exactly one of qualified_ids or qualified_handles must be provided.", + "example": { + "qualified_ids": [ + { + "domain": "example.com", + "id": "00000000-0000-0000-0000-000000000000" + } + ] + }, + "properties": { + "qualified_handles": { + "items": { + "$ref": "#/components/schemas/Qualified_Handle" + }, + "type": "array" + }, + "qualified_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + } + }, + "type": "object" + }, + "Locale": { + "type": "string" + }, + "LocaleUpdate": { + "properties": { + "locale": { + "$ref": "#/components/schemas/Locale" + } + }, + "required": [ + "locale" + ], + "type": "object" + }, + "LockStatus": { + "enum": [ + "locked", + "unlocked" + ], + "type": "string" + }, + "Login": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "label": { + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "MLSConfig": { + "properties": { + "allowedCipherSuites": { + "items": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "type": "array" + }, + "defaultCipherSuite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "defaultProtocol": { + "$ref": "#/components/schemas/Protocol" + }, + "protocolToggleUsers": { + "description": "allowlist of users that may change protocols", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "supportedProtocols": { + "items": { + "$ref": "#/components/schemas/Protocol" + }, + "type": "array" + } + }, + "required": [ + "protocolToggleUsers", + "defaultProtocol", + "allowedCipherSuites", + "defaultCipherSuite", + "supportedProtocols" + ], + "type": "object" + }, + "MLSConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/components/schemas/MLSConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MLSConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/components/schemas/MLSConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "MLSKeys": { + "properties": { + "ecdsa_secp256r1_sha256": { + "$ref": "#/components/schemas/JWK" + }, + "ecdsa_secp384r1_sha384": { + "$ref": "#/components/schemas/JWK" + }, + "ecdsa_secp521r1_sha512": { + "$ref": "#/components/schemas/JWK" + }, + "ed25519": { + "$ref": "#/components/schemas/JWK" + } + }, + "required": [ + "ed25519", + "ecdsa_secp256r1_sha256", + "ecdsa_secp384r1_sha384", + "ecdsa_secp521r1_sha512" + ], + "type": "object" + }, + "MLSKeysByPurpose": { + "properties": { + "removal": { + "$ref": "#/components/schemas/MLSKeys" + } + }, + "required": [ + "removal" + ], + "type": "object" + }, + "MLSMessage": { + "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." + }, + "MLSMessageSendingStatus": { + "properties": { + "events": { + "description": "A list of events caused by sending the message.", + "items": { + "$ref": "#/components/schemas/Event" + }, + "type": "array" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "events", + "time" + ], + "type": "object" + }, + "MLSPublicKeys": { + "additionalProperties": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, + "description": "Mapping from signature scheme (tags) to public key data", + "example": { + "ecdsa_secp256r1_sha256": "ZXhhbXBsZQo=", + "ecdsa_secp384r1_sha384": "ZXhhbXBsZQo=", + "ecdsa_secp521r1_sha512": "ZXhhbXBsZQo=", + "ed25519": "ZXhhbXBsZQo=" + }, + "type": "object" + }, + "ManagedBy": { + "enum": [ + "wire", + "scim" + ], + "type": "string" + }, + "Member": { + "description": "The user ID of the requestor", + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "status": {}, + "status_ref": {}, + "status_time": {} + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "MemberUpdate": { + "properties": { + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "type": "object" + }, + "MemberUpdateData": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "hidden": { + "type": "boolean" + }, + "hidden_ref": { + "type": "string" + }, + "otr_archived": { + "type": "boolean" + }, + "otr_archived_ref": { + "type": "string" + }, + "otr_muted_ref": { + "type": "string" + }, + "otr_muted_status": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "qualified_target": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "target": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "qualified_target" + ], + "type": "object" + }, + "MessageSendingStatus": { + "description": "The Proteus message sending status. It has these fields:\n- `time`: Time of sending message.\n- `missing`: Clients that the message /should/ have been encrypted for, but wasn't.\n- `redundant`: Clients that the message /should not/ have been encrypted for, but was.\n- `deleted`: Clients that were deleted.\n- `failed_to_send`: When message sending fails for some clients but succeeds for others, e.g., because a remote backend is unreachable, this field will contain the list of clients for which the message sending failed. This list should be empty when message sending is not even tried, like when some clients are missing.", + "properties": { + "deleted": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "failed_to_confirm_clients": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "failed_to_send": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "missing": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "redundant": { + "$ref": "#/components/schemas/QualifiedUserClients" + }, + "time": { + "$ref": "#/components/schemas/UTCTimeMillis" + } + }, + "required": [ + "time", + "missing", + "redundant", + "deleted", + "failed_to_send", + "failed_to_confirm_clients" + ], + "type": "object" + }, + "MlsE2EIdConfig": { + "properties": { + "acmeDiscoveryUrl": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "crlProxy": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "useProxyOnMobile": { + "type": "boolean" + }, + "verificationExpiration": { + "description": "When a client first tries to fetch or renew a certificate, they may need to login to an identity provider (IdP) depending on their IdP domain authentication policy. The user may have a grace period during which they can “snooze” this login. The duration of this grace period (in seconds) is set in the `verificationDuration` parameter, which is enforced separately by each client. After the grace period has expired, the client will not allow the user to use the application until they have logged to refresh the certificate. The default value is 1 day (86400s). The client enrolls using the Automatic Certificate Management Environment (ACME) protocol. The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: `https://acme.example.com/acme/provisioner1/discovery`.", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "verificationExpiration" + ], + "type": "object" + }, + "MlsE2EIdConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsE2EIdConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MlsE2EIdConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsE2EIdConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "MlsMigration": { + "properties": { + "finaliseRegardlessAfter": { + "$ref": "#/components/schemas/UTCTime" + }, + "startTime": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "type": "object" + }, + "MlsMigration.WithStatus": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsMigration" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "MlsMigration.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/components/schemas/MlsMigration" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "NameIDFormat": { + "enum": [ + "NameIDFUnspecified", + "NameIDFEmail", + "NameIDFX509", + "NameIDFWindows", + "NameIDFKerberos", + "NameIDFEntity", + "NameIDFPersistent", + "NameIDFTransient" + ], + "type": "string" + }, + "NameIdPolicy": { + "properties": { + "allowCreate": { + "type": "boolean" + }, + "format": { + "$ref": "#/components/schemas/NameIDFormat" + }, + "spNameQualifier": { + "$ref": "#/components/schemas/XmlText" + } + }, + "required": [ + "format", + "allowCreate" + ], + "type": "object" + }, + "NewAssetToken": { + "properties": { + "token": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "NewClient": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/components/schemas/ClientCapability" + }, + "type": "array" + }, + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "cookie": { + "description": "The cookie label, i.e. the label used when logging in.", + "type": "string" + }, + "label": { + "type": "string" + }, + "lastkey": { + "$ref": "#/components/schemas/Prekey" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "model": { + "type": "string" + }, + "password": { + "description": "The password of the authenticated user for verification. Note: Required for registration of the 2nd, 3rd, ... client.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "prekeys": { + "description": "Prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/components/schemas/Prekey" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/ClientType" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "prekeys", + "lastkey", + "type" + ], + "type": "object" + }, + "NewConv": { + "description": "JSON object to create a new conversation. When using 'qualified_users' (preferred), you can omit 'users'", + "properties": { + "access": { + "items": { + "$ref": "#/components/schemas/Access" + }, + "type": "array" + }, + "access_role": { + "items": { + "$ref": "#/components/schemas/AccessRole" + }, + "type": "array" + }, + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "message_timer": { + "description": "Per-conversation message timer", + "format": "int64", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "protocol": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "qualified_users": { + "description": "List of qualified user IDs (excluding the requestor) to be part of this conversation", + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "receipt_mode": { + "description": "Conversation receipt mode", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "team": { + "$ref": "#/components/schemas/ConvTeamInfo" + }, + "users": { + "deprecated": true, + "description": "List of user IDs (excluding the requestor) to be part of this conversation (deprecated)", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "type": "object" + }, + "NewLegalHoldService": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + } + }, + "required": [ + "base_url", + "public_key", + "auth_token" + ], + "type": "object" + }, + "NewPasswordReset": { + "description": "Data to initiate a password reset", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "phone": { + "description": "Email", + "type": "string" + } + }, + "type": "object" + }, + "NewProvider": { + "properties": { + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "name", + "email", + "url", + "description" + ], + "type": "object" + }, + "NewProviderResponse": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "NewService": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "summary": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/" + }, + "maxItems": 3, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "name", + "summary", + "description", + "base_url", + "public_key", + "assets", + "tags" + ], + "type": "object" + }, + "NewServiceResponse": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "NewTeamMember": { + "description": "Required data when creating new team members", + "properties": { + "member": { + "description": "the team member to add (the legalhold_status field must be null or missing!)", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + } + }, + "required": [ + "member" + ], + "type": "object" + }, + "NewUser": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_code": { + "$ref": "#/components/schemas/ASCII" + }, + "expires_in": { + "maximum": 604800, + "minimum": 1, + "type": "integer" + }, + "invitation_code": { + "$ref": "#/components/schemas/ASCII" + }, + "label": { + "type": "string" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "password": { + "maxLength": 1024, + "minLength": 8, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/BindingNewTeamUser" + }, + "team_code": { + "$ref": "#/components/schemas/ASCII" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + }, + "uuid": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "OAuthAccessTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "code": { + "$ref": "#/components/schemas/OAuthAuthorizationCode" + }, + "code_verifier": { + "description": "The code verifier to complete the code challenge", + "maxLength": 128, + "minLength": 43, + "type": "string" + }, + "grant_type": { + "$ref": "#/components/schemas/OAuthGrantType" + }, + "redirect_uri": { + "$ref": "#/components/schemas/RedirectUrl" + } + }, + "required": [ + "grant_type", + "client_id", + "code_verifier", + "code", + "redirect_uri" + ], + "type": "object" + }, + "OAuthAccessTokenResponse": { + "properties": { + "access_token": { + "description": "The access token, which has a relatively short lifetime", + "type": "string" + }, + "expires_in": { + "description": "The lifetime of the access token in seconds", + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "refresh_token": { + "description": "The refresh token, which has a relatively long lifetime, and can be used to obtain a new access token", + "type": "string" + }, + "token_type": { + "$ref": "#/components/schemas/OAuthAccessTokenType" + } + }, + "required": [ + "access_token", + "token_type", + "expires_in", + "refresh_token" + ], + "type": "object" + }, + "OAuthAccessTokenType": { + "description": "The type of the access token. Currently only `Bearer` is supported.", + "enum": [ + "Bearer" + ], + "type": "string" + }, + "OAuthApplication": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "description": "The OAuth client's name", + "maxLength": 256, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "OAuthAuthorizationCode": { + "description": "The authorization code", + "type": "string" + }, + "OAuthClient": { + "properties": { + "application_name": { + "maxLength": 256, + "minLength": 6, + "type": "string" + }, + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "redirect_url": { + "$ref": "#/components/schemas/RedirectUrl" + } + }, + "required": [ + "client_id", + "application_name", + "redirect_url" + ], + "type": "object" + }, + "OAuthCodeChallenge": { + "description": "Generated by the client from the code verifier (unpadded base64url-encoded SHA256 hash of the code verifier)", + "type": "string" + }, + "OAuthGrantType": { + "description": "Indicates which authorization flow to use. Use `authorization_code` for authorization code flow.", + "enum": [ + "authorization_code", + "refresh_token" + ], + "type": "string" + }, + "OAuthRefreshAccessTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "grant_type": { + "$ref": "#/components/schemas/OAuthGrantType" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "grant_type", + "client_id", + "refresh_token" + ], + "type": "object" + }, + "OAuthResponseType": { + "description": "Indicates which authorization flow to use. Use `code` for authorization code flow.", + "enum": [ + "code" + ], + "type": "string" + }, + "OAuthRevokeRefreshTokenRequest": { + "properties": { + "client_id": { + "$ref": "#/components/schemas/UUID" + }, + "refresh_token": { + "description": "The refresh token", + "type": "string" + } + }, + "required": [ + "client_id", + "refresh_token" + ], + "type": "object" + }, + "Object": { + "additionalProperties": true, + "description": "A single notification event", + "properties": { + "type": { + "description": "Event type", + "type": "string" + } + }, + "title": "Event", + "type": "object" + }, + "OtherMember": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "status": { + "deprecated": true, + "description": "deprecated", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "OtherMemberUpdate": { + "description": "Update user properties of other members relative to a conversation", + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + } + }, + "type": "object" + }, + "OtrMessage": { + "description": "Encrypted message of a conversation", + "properties": { + "data": { + "description": "Extra (symmetric) data (i.e. ciphertext, Base64 in JSON) that is common with all other recipients.", + "type": "string" + }, + "recipient": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "text": { + "description": "The ciphertext for the recipient (Base64 in JSON)", + "type": "string" + } + }, + "required": [ + "sender", + "recipient", + "text" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "OutlookCalIntegrationConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "OwnKeyPackages": { + "properties": { + "count": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "count" + ], + "type": "object" + }, + "PagingState": { + "description": "Paging state that should be supplied to retrieve the next page of results", + "type": "string" + }, + "PasswordChange": { + "properties": { + "new_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "old_password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "old_password", + "new_password" + ], + "type": "object" + }, + "PasswordReset": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "Permissions": { + "description": "This is just a complicated way of representing a team role. self and copy always have to contain the same integer, and only the following integers are allowed: 1025 (partner), 1587 (member), 5951 (admin), 8191 (owner). Unit tests of the galley-types package in wire-server contain an authoritative list.", + "properties": { + "copy": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "self": { + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "self", + "copy" + ], + "type": "object" + }, + "PhoneNumber": { + "description": "A known phone number with a pending password reset.", + "type": "string" + }, + "Pict": { + "items": {}, + "maxItems": 10, + "minItems": 0, + "type": "array" + }, + "Prekey": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "PrekeyBundle": { + "properties": { + "clients": { + "items": { + "$ref": "#/components/schemas/ClientPrekey" + }, + "type": "array" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "clients" + ], + "type": "object" + }, + "Priority": { + "enum": [ + "low", + "high" + ], + "type": "string" + }, + "PropertyKeysAndValues": { + "type": "object" + }, + "PropertyValue": { + "description": "An arbitrary JSON value for a property" + }, + "Protocol": { + "enum": [ + "proteus", + "mls", + "mixed" + ], + "type": "string" + }, + "ProtocolUpdate": { + "properties": { + "protocol": { + "$ref": "#/components/schemas/Protocol" + } + }, + "type": "object" + }, + "Provider": { + "properties": { + "description": { + "type": "string" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "required": [ + "id", + "name", + "email", + "url", + "description" + ], + "type": "object" + }, + "ProviderActivationResponse": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "ProviderLogin": { + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "email", + "password" + ], + "type": "object" + }, + "PubClient": { + "properties": { + "class": { + "$ref": "#/components/schemas/ClientClass" + }, + "id": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "PublicSubConversation": { + "description": "An MLS subconversation", + "properties": { + "cipher_suite": { + "$ref": "#/components/schemas/CipherSuiteTag" + }, + "epoch": { + "description": "The epoch number of the corresponding MLS group", + "format": "int64", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + }, + "epoch_timestamp": { + "$ref": "#/components/schemas/UTCTime" + }, + "group_id": { + "$ref": "#/components/schemas/GroupId" + }, + "members": { + "items": { + "$ref": "#/components/schemas/ClientIdentity" + }, + "type": "array" + }, + "parent_qualified_id": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "subconv_id": { + "type": "string" + } + }, + "required": [ + "parent_qualified_id", + "subconv_id", + "group_id", + "epoch", + "members" + ], + "type": "object" + }, + "PushToken": { + "description": "Native Push Token", + "properties": { + "app": { + "description": "Application", + "type": "string" + }, + "client": { + "description": "Client ID", + "type": "string" + }, + "token": { + "description": "Access Token", + "type": "string" + }, + "transport": { + "$ref": "#/components/schemas/Transport" + } + }, + "required": [ + "transport", + "app", + "token", + "client" + ], + "type": "object" + }, + "PushTokenList": { + "description": "List of Native Push Tokens", + "properties": { + "tokens": { + "description": "Push tokens", + "items": { + "$ref": "#/components/schemas/PushToken" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "QualifiedNewOtrMessage": { + "description": "This object can only be parsed from Protobuf.\nThe specification for the protobuf types is here: \nhttps://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto." + }, + "QualifiedUserClientPrekeyMapV4": { + "properties": { + "failed_to_list": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "qualified_user_client_prekeys": { + "additionalProperties": { + "$ref": "#/components/schemas/UserClientPrekeyMap" + }, + "type": "object" + } + }, + "required": [ + "qualified_user_client_prekeys" + ], + "type": "object" + }, + "QualifiedUserClients": { + "additionalProperties": { + "additionalProperties": { + "items": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "type": "array" + }, + "type": "object" + }, + "description": "Map of Domain to UserClients", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + } + }, + "type": "object" + }, + "QualifiedUserIdList with EdMemberLeftReason": { + "properties": { + "qualified_user_ids": { + "items": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "type": "array" + }, + "reason": { + "$ref": "#/components/schemas/EdMemberLeftReason" + }, + "user_ids": { + "deprecated": true, + "description": "Deprecated, use qualified_user_ids", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "reason", + "qualified_user_ids", + "user_ids" + ], + "type": "object" + }, + "QualifiedUserMap_Set_PubClient": { + "additionalProperties": { + "$ref": "#/components/schemas/UserMap_Set_PubClient" + }, + "description": "Map of Domain to (UserMap (Set_PubClient)).", + "example": { + "domain1.example.com": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + } + }, + "type": "object" + }, + "Qualified_ConvId": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "Qualified_Handle": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + } + }, + "required": [ + "domain", + "handle" + ], + "type": "object" + }, + "Qualified_UserId": { + "properties": { + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "domain", + "id" + ], + "type": "object" + }, + "QueuedNotification": { + "description": "A single notification", + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "payload": { + "description": "List of events", + "items": { + "$ref": "#/components/schemas/Object" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "id", + "payload" + ], + "type": "object" + }, + "QueuedNotificationList": { + "description": "Zero or more notifications", + "properties": { + "has_more": { + "description": "Whether there are still more notifications.", + "type": "boolean" + }, + "notifications": { + "description": "Notifications", + "items": { + "$ref": "#/components/schemas/QueuedNotification" + }, + "type": "array" + }, + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "notifications" + ], + "type": "object" + }, + "RTCConfiguration": { + "description": "A subset of the WebRTC 'RTCConfiguration' dictionary", + "properties": { + "ice_servers": { + "description": "Array of 'RTCIceServer' objects", + "items": { + "$ref": "#/components/schemas/RTCIceServer" + }, + "minItems": 1, + "type": "array" + }, + "is_federating": { + "description": "True if the client should connect to an SFT in the sft_servers_all and request it to federate", + "type": "boolean" + }, + "sft_servers": { + "description": "Array of 'SFTServer' objects (optional)", + "items": { + "$ref": "#/components/schemas/SftServer" + }, + "minItems": 1, + "type": "array" + }, + "sft_servers_all": { + "description": "Array of all SFT servers", + "items": { + "$ref": "#/components/schemas/SftServer" + }, + "type": "array" + }, + "ttl": { + "description": "Number of seconds after which the configuration should be refreshed (advisory)", + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "ice_servers", + "ttl" + ], + "type": "object" + }, + "RTCIceServer": { + "description": "A subset of the WebRTC 'RTCIceServer' object", + "properties": { + "credential": { + "$ref": "#/components/schemas/ASCII" + }, + "urls": { + "description": "Array of TURN server addresses of the form 'turn::'", + "items": { + "$ref": "#/components/schemas/TurnURI" + }, + "minItems": 1, + "type": "array" + }, + "username": { + "$ref": "#/components/schemas/" + } + }, + "required": [ + "urls", + "username", + "credential" + ], + "type": "object" + }, + "RedirectUrl": { + "description": "The URL must match the URL that was used to generate the authorization code.", + "type": "string" + }, + "Relation": { + "enum": [ + "accepted", + "blocked", + "pending", + "ignored", + "sent", + "cancelled", + "missing-legalhold-consent" + ], + "type": "string" + }, + "RemoveBotResponse": { + "properties": { + "event": { + "$ref": "#/components/schemas/Event" + } + }, + "required": [ + "event" + ], + "type": "object" + }, + "RemoveCookies": { + "description": "Data required to remove cookies", + "properties": { + "ids": { + "description": "A list of cookie IDs to revoke", + "items": { + "format": "int32", + "maximum": 4294967295, + "minimum": 0, + "type": "integer" + }, + "type": "array" + }, + "labels": { + "description": "A list of cookie labels for which to revoke the cookies", + "items": { + "type": "string" + }, + "type": "array" + }, + "password": { + "description": "The user's password", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "RemoveLegalHoldSettingsRequest": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "RichField": { + "properties": { + "type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "type", + "value" + ], + "type": "object" + }, + "RichInfoAssocList": { + "description": "json object with case-insensitive fields.", + "properties": { + "fields": { + "items": { + "$ref": "#/components/schemas/RichField" + }, + "type": "array" + }, + "version": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "version", + "fields" + ], + "type": "object" + }, + "Role": { + "description": "Role of the invited user", + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "RoleName": { + "description": "Role name, between 2 and 128 chars, 'wire_' prefix is reserved for roles designed by Wire (i.e., no custom roles can have the same prefix)", + "type": "string" + }, + "SSOConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "ScimTokenInfo": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTime" + }, + "description": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "idp": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "id", + "created_at", + "description" + ], + "type": "object" + }, + "ScimTokenList": { + "properties": { + "tokens": { + "items": { + "$ref": "#/components/schemas/ScimTokenInfo" + }, + "type": "array" + } + }, + "required": [ + "tokens" + ], + "type": "object" + }, + "SearchResult": { + "properties": { + "documents": { + "description": "List of contacts found", + "items": { + "$ref": "#/components/schemas/TeamContact" + }, + "type": "array" + }, + "found": { + "description": "Total number of hits", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "has_more": { + "description": "Indicates whether there are more results to be fetched", + "type": "boolean" + }, + "paging_state": { + "$ref": "#/components/schemas/PagingState" + }, + "returned": { + "description": "Total number of hits returned", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "search_policy": { + "$ref": "#/components/schemas/FederatedUserSearchPolicy" + }, + "took": { + "description": "Search time in ms", + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + } + }, + "required": [ + "found", + "returned", + "took", + "documents", + "search_policy" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SearchVisibilityAvailableConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SearchVisibilityInboundConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig": { + "properties": { + "enforcedTimeoutSeconds": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + } + }, + "required": [ + "enforcedTimeoutSeconds" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.WithStatus": { + "properties": { + "config": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig" + }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus", + "config" + ], + "type": "object" + }, + "SelfDeletingMessagesConfig.WithStatusNoLock": { + "properties": { + "config": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "config" + ], + "type": "object" + }, + "SendActivationCode": { + "description": "Data for requesting an email code to be sent. 'email' must be present.", + "properties": { + "email": { + "$ref": "#/components/schemas/Email" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + } + }, + "required": [ + "email" + ], + "type": "object" + }, + "SendVerificationCode": { + "properties": { + "action": { + "$ref": "#/components/schemas/VerificationAction" + }, + "email": { + "$ref": "#/components/schemas/Email" + } + }, + "required": [ + "action", + "email" + ], + "type": "object" + }, + "Service": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "auth_tokens": { + "$ref": "#/components/schemas/List1" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "public_keys": { + "$ref": "#/components/schemas/List1" + }, + "summary": { + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/" + }, + "type": "array" + } + }, + "required": [ + "id", + "name", + "summary", + "description", + "base_url", + "auth_tokens", + "public_keys", + "assets", + "tags", + "enabled" + ], + "type": "object" + }, + "ServiceKey": { + "properties": { + "pem": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "size": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "type": { + "$ref": "#/components/schemas/ServiceKeyType" + } + }, + "required": [ + "type", + "size", + "pem" + ], + "type": "object" + }, + "ServiceKeyPEM": { + "example": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+Kg/PHHU3atXrUbKnw0\nG06FliXcNt3lMwl2os5twEDcPPFw/feGiAKymxp+7JqZDrseS5D9THGrW+OQRIPH\nWvUBdiLfGrZqJO223DB6D8K2Su/odmnjZJ2z23rhXoEArTplu+Dg9K+c2LVeXTKV\nVPOaOzgtAB21XKRiQ4ermqgi3/njr03rXyq/qNkuNd6tNcg+HAfGxfGvvCSYBfiS\nbUKr/BeArYRcjzr/h5m1In6fG/if9GEI6m8dxHT9JbY53wiksowy6ajCuqskIFg8\n7X883H+LA/d6X5CTiPv1VMxXdBUiGPuC9IT/6CNQ1/LFt0P37ax58+LGYlaFo7la\nnQIDAQAB\n-----END PUBLIC KEY-----\n", + "type": "string" + }, + "ServiceKeyType": { + "enum": [ + "rsa" + ], + "type": "string" + }, + "ServiceProfile": { + "properties": { + "has_more": { + "type": "boolean" + }, + "services": { + "items": { + "$ref": "#/components/schemas/ServiceProfile" + }, + "type": "array" + } + }, + "required": [ + "has_more", + "services" + ], + "type": "object" + }, + "ServiceRef": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "provider" + ], + "type": "object" + }, + "ServiceTagList": { + "items": { + "$ref": "#/components/schemas/" + }, + "type": "array" + }, + "SftServer": { + "description": "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers", + "properties": { + "urls": { + "description": "Array containing exactly one SFT server address of the form 'https://:'", + "items": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "type": "array" + } + }, + "required": [ + "urls" + ], + "type": "object" + }, + "SimpleMember": { + "properties": { + "conversation_role": { + "$ref": "#/components/schemas/RoleName" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + } + }, + "required": [ + "qualified_id" + ], + "type": "object" + }, + "SimpleMembers": { + "properties": { + "user_ids": { + "deprecated": true, + "description": "deprecated", + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "users": { + "items": { + "$ref": "#/components/schemas/SimpleMember" + }, + "type": "array" + } + }, + "required": [ + "users" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "SndFactorPasswordChallengeConfig.WithStatusNoLock": { + "properties": { + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "Sso": { + "properties": { + "issuer": { + "type": "string" + }, + "nameid": { + "type": "string" + } + }, + "required": [ + "issuer", + "nameid" + ], + "type": "object" + }, + "SsoSettings": { + "properties": { + "default_sso_code": { + "$ref": "#/components/schemas/UUID" + } + }, + "type": "object" + }, + "SupportedProtocolUpdate": { + "properties": { + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + } + }, + "required": [ + "supported_protocols" + ], + "type": "object" + }, + "SystemSettings": { + "properties": { + "setEnableMls": { + "description": "Whether MLS is enabled or not", + "type": "boolean" + }, + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation", + "setEnableMls" + ], + "type": "object" + }, + "SystemSettingsPublic": { + "properties": { + "setRestrictUserCreation": { + "description": "Do not allow certain user creation flows", + "type": "boolean" + } + }, + "required": [ + "setRestrictUserCreation" + ], + "type": "object" + }, + "Team": { + "description": "`binding` is deprecated, and should be ignored. The non-binding teams API is not used (and will not be supported from API version V4 onwards), and `binding` will always be `true`.", + "properties": { + "binding": { + "$ref": "#/components/schemas/TeamBinding" + }, + "creator": { + "$ref": "#/components/schemas/UUID" + }, + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "name": { + "type": "string" + }, + "splash_screen": { + "$ref": "#/components/schemas/Icon" + } + }, + "required": [ + "id", + "creator", + "name", + "icon" + ], + "type": "object" + }, + "TeamBinding": { + "deprecated": true, + "description": "Deprecated, please ignore.", + "enum": [ + true, + false + ], + "type": "boolean" + }, + "TeamContact": { + "properties": { + "accent_id": { + "maximum": 9223372036854775807, + "minimum": -9223372036854775808, + "type": "integer" + }, + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "email_unvalidated": { + "$ref": "#/components/schemas/Email" + }, + "handle": { + "type": "string" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy" + }, + "name": { + "type": "string" + }, + "role": { + "$ref": "#/components/schemas/Role" + }, + "saml_idp": { + "type": "string" + }, + "scim_external_id": { + "type": "string" + }, + "sso": { + "$ref": "#/components/schemas/Sso" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "id", + "name" + ], + "type": "object" + }, + "TeamConversation": { + "description": "Team conversation data", + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "managed": { + "description": "This field MUST NOT be used by clients. It is here only for backwards compatibility of the interface." + } + }, + "required": [ + "conversation", + "managed" + ], + "type": "object" + }, + "TeamConversationList": { + "description": "Team conversation list", + "properties": { + "conversations": { + "items": { + "$ref": "#/components/schemas/TeamConversation" + }, + "type": "array" + } + }, + "required": [ + "conversations" + ], + "type": "object" + }, + "TeamDeleteData": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "verification_code": { + "$ref": "#/components/schemas/ASCII" + } + }, + "type": "object" + }, + "TeamMember": { + "description": "team member data", + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "legalhold_status": { + "$ref": "#/components/schemas/UserLegalHoldStatus" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user" + ], + "type": "object" + }, + "TeamMemberDeleteData": { + "description": "Data for a team member deletion request in case of binding teams.", + "properties": { + "password": { + "description": "The account password to authorise the deletion.", + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, + "TeamMemberList": { + "description": "list of team member", + "properties": { + "hasMore": { + "$ref": "#/components/schemas/ListType" + }, + "members": { + "description": "the array of team members", + "items": { + "$ref": "#/components/schemas/TeamMember" + }, + "type": "array" + } + }, + "required": [ + "members", + "hasMore" + ], + "type": "object" + }, + "TeamMembersPage": { + "properties": { + "hasMore": { + "type": "boolean" + }, + "members": { + "items": { + "$ref": "#/components/schemas/TeamMember" + }, + "type": "array" + }, + "pagingState": { + "$ref": "#/components/schemas/TeamMembers_PagingState" + } + }, + "required": [ + "members", + "hasMore", + "pagingState" + ], + "type": "object" + }, + "TeamMembers_PagingState": { + "type": "string" + }, + "TeamSearchVisibility": { + "description": "value of visibility", + "enum": [ + "standard", + "no-name-outside-team" + ], + "type": "string" + }, + "TeamSearchVisibilityView": { + "description": "Search visibility value for the team", + "properties": { + "search_visibility": { + "$ref": "#/components/schemas/TeamSearchVisibility" + } + }, + "required": [ + "search_visibility" + ], + "type": "object" + }, + "TeamSize": { + "description": "A simple object with a total number of team members.", + "properties": { + "teamSize": { + "description": "Team size.", + "exclusiveMinimum": false, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "teamSize" + ], + "type": "object" + }, + "TeamUpdateData": { + "properties": { + "icon": { + "$ref": "#/components/schemas/Icon" + }, + "icon_key": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "splash_screen": { + "$ref": "#/components/schemas/Icon" + } + }, + "type": "object" + }, + "Time": { + "properties": { + "time": { + "$ref": "#/components/schemas/UTCTime" + } + }, + "required": [ + "time" + ], + "type": "object" + }, + "TokenType": { + "enum": [ + "Bearer" + ], + "type": "string" + }, + "Transport": { + "description": "Transport", + "enum": [ + "GCM", + "APNS", + "APNS_SANDBOX", + "APNS_VOIP", + "APNS_VOIP_SANDBOX" + ], + "type": "string" + }, + "TurnURI": { + "type": "string" + }, + "TypingData": { + "properties": { + "status": { + "$ref": "#/components/schemas/TypingStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "TypingStatus": { + "enum": [ + "started", + "stopped" + ], + "type": "string" + }, + "URIRef Absolute": { + "description": "URL of the invitation link to be sent to the invitee", + "type": "string" + }, + "UTCTime": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, + "UTCTimeMillis": { + "example": "2021-05-12T10:52:02.671Z", + "format": "yyyy-mm-ddThh:MM:ss.qqqZ", + "type": "string" + }, + "UUID": { + "description": "The OAuth client's ID", + "example": "99db9768-04e3-4b5d-9268-831b6a25c4ab", + "format": "uuid", + "type": "string" + }, + "Unnamed": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "created_by": { + "$ref": "#/components/schemas/UUID" + }, + "permissions": { + "$ref": "#/components/schemas/Permissions" + }, + "user": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "user", + "permissions" + ], + "type": "object" + }, + "UpdateBotPrekeys": { + "properties": { + "prekeys": { + "items": { + "$ref": "#/components/schemas/Prekey" + }, + "type": "array" + } + }, + "required": [ + "prekeys" + ], + "type": "object" + }, + "UpdateClient": { + "properties": { + "capabilities": { + "description": "Hints provided by the client for the backend so it can behave in a backwards-compatible way.", + "items": { + "$ref": "#/components/schemas/ClientCapability" + }, + "type": "array" + }, + "label": { + "description": "A new name for this client.", + "type": "string" + }, + "lastkey": { + "$ref": "#/components/schemas/Prekey" + }, + "mls_public_keys": { + "$ref": "#/components/schemas/MLSPublicKeys" + }, + "prekeys": { + "description": "New prekeys for other clients to establish OTR sessions.", + "items": { + "$ref": "#/components/schemas/Prekey" + }, + "type": "array" + } + }, + "type": "object" + }, + "UpdateProvider": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "url": { + "$ref": "#/components/schemas/HttpsUrl" + } + }, + "type": "object" + }, + "UpdateService": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "description": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "summary": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/components/schemas/" + }, + "maxItems": 3, + "minItems": 1, + "type": "array" + } + }, + "type": "object" + }, + "UpdateServiceConn": { + "properties": { + "auth_tokens": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "enabled": { + "type": "boolean" + }, + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + }, + "public_keys": { + "items": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "maxItems": 2, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "password" + ], + "type": "object" + }, + "UpdateServiceWhitelist": { + "properties": { + "id": { + "$ref": "#/components/schemas/UUID" + }, + "provider": { + "$ref": "#/components/schemas/UUID" + }, + "whitelisted": { + "type": "boolean" + } + }, + "required": [ + "provider", + "id", + "whitelisted" + ], + "type": "object" + }, + "User": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "locale": { + "$ref": "#/components/schemas/Locale" + }, + "managed_by": { + "$ref": "#/components/schemas/ManagedBy" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "sso_id": { + "$ref": "#/components/schemas/UserSSOId" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "locale" + ], + "type": "object" + }, + "UserAsset": { + "properties": { + "key": { + "$ref": "#/components/schemas/AssetKey" + }, + "size": { + "$ref": "#/components/schemas/AssetSize" + }, + "type": { + "$ref": "#/components/schemas/AssetType" + } + }, + "required": [ + "key", + "type" + ], + "type": "object" + }, + "UserClientMap": { + "additionalProperties": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "object" + }, + "UserClientPrekeyMap": { + "additionalProperties": { + "additionalProperties": { + "properties": { + "id": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ], + "type": "object" + }, + "type": "object" + }, + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": { + "44901fb0712e588f": { + "id": 1, + "key": "pQABAQECoQBYIOjl7hw0D8YRNq..." + } + } + }, + "type": "object" + }, + "UserClients": { + "additionalProperties": { + "items": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "type": "array" + }, + "description": "Map of user id to list of client ids.", + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + "60f85e4b15ad3786", + "6e323ab31554353b" + ] + }, + "type": "object" + }, + "UserConnection": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/UUID" + }, + "from": { + "$ref": "#/components/schemas/UUID" + }, + "last_update": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "qualified_conversation": { + "$ref": "#/components/schemas/Qualified_ConvId" + }, + "qualified_to": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "status": { + "$ref": "#/components/schemas/Relation" + }, + "to": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "from", + "qualified_to", + "status", + "last_update" + ], + "type": "object" + }, + "UserIdList": { + "properties": { + "user_ids": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + } + }, + "required": [ + "user_ids" + ], + "type": "object" + }, + "UserLegalHoldStatus": { + "description": "The state of Legal Hold compliance for the member", + "enum": [ + "enabled", + "pending", + "disabled", + "no_consent" + ], + "type": "string" + }, + "UserLegalHoldStatusResponse": { + "properties": { + "client": { + "$ref": "#/components/schemas/Id" + }, + "last_prekey": { + "$ref": "#/components/schemas/Prekey" + }, + "status": { + "$ref": "#/components/schemas/UserLegalHoldStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "UserMap_Set_PubClient": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array", + "uniqueItems": true + }, + "description": "Map of UserId to (Set PubClient)", + "example": { + "1d51e2d6-9c70-605f-efc8-ff85c3dabdc7": [ + { + "class": "legalhold", + "id": "d0" + } + ] + }, + "type": "object" + }, + "UserProfile": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "deleted": { + "type": "boolean" + }, + "email": { + "$ref": "#/components/schemas/Email" + }, + "expires_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "handle": { + "$ref": "#/components/schemas/Handle" + }, + "id": { + "$ref": "#/components/schemas/UUID" + }, + "legalhold_status": { + "$ref": "#/components/schemas/UserLegalHoldStatus" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "qualified_id": { + "$ref": "#/components/schemas/Qualified_UserId" + }, + "service": { + "$ref": "#/components/schemas/ServiceRef" + }, + "supported_protocols": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array" + }, + "team": { + "$ref": "#/components/schemas/UUID" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "required": [ + "qualified_id", + "name", + "accent_id", + "legalhold_status" + ], + "type": "object" + }, + "UserSSOId": { + "properties": { + "scim_external_id": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "tenant": { + "type": "string" + } + }, + "type": "object" + }, + "UserUpdate": { + "properties": { + "accent_id": { + "format": "int32", + "maximum": 2147483647, + "minimum": -2147483648, + "type": "integer" + }, + "assets": { + "items": { + "$ref": "#/components/schemas/UserAsset" + }, + "type": "array" + }, + "name": { + "maxLength": 128, + "minLength": 1, + "type": "string" + }, + "picture": { + "$ref": "#/components/schemas/Pict" + }, + "text_status": { + "maxLength": 256, + "minLength": 1, + "type": "string" + } + }, + "type": "object" + }, + "ValidateSAMLEmailsConfig.WithStatus": { + "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status", + "lockStatus" + ], + "type": "object" + }, + "VerificationAction": { + "enum": [ + "create_scim_token", + "login", + "delete_team" + ], + "type": "string" + }, + "VerifyDeleteUser": { + "description": "Data for verifying an account deletion.", + "properties": { + "code": { + "$ref": "#/components/schemas/ASCII" + }, + "key": { + "$ref": "#/components/schemas/ASCII" + } + }, + "required": [ + "key", + "code" + ], + "type": "object" + }, + "VersionInfo": { + "example": { + "development": [ + 7 + ], + "domain": "example.com", + "federation": false, + "supported": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "properties": { + "development": { + "items": { + "$ref": "#/components/schemas/VersionNumber" + }, + "type": "array" + }, + "domain": { + "$ref": "#/components/schemas/Domain" + }, + "federation": { + "type": "boolean" + }, + "supported": { + "items": { + "$ref": "#/components/schemas/VersionNumber" + }, + "type": "array" + } + }, + "required": [ + "supported", + "development", + "federation", + "domain" + ], + "type": "object" + }, + "VersionNumber": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "type": "integer" + }, + "ViewLegalHoldService": { + "properties": { + "settings": { + "$ref": "#/components/schemas/ViewLegalHoldServiceInfo" + }, + "status": { + "$ref": "#/components/schemas/LHServiceStatus" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ViewLegalHoldServiceInfo": { + "properties": { + "auth_token": { + "$ref": "#/components/schemas/ASCII" + }, + "base_url": { + "$ref": "#/components/schemas/HttpsUrl" + }, + "fingerprint": { + "$ref": "#/components/schemas/Fingerprint" + }, + "public_key": { + "$ref": "#/components/schemas/ServiceKeyPEM" + }, + "team_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team_id", + "base_url", + "fingerprint", + "auth_token", + "public_key" + ], + "type": "object" + }, + "WireIdP": { + "properties": { + "apiVersion": { + "$ref": "#/components/schemas/WireIdPAPIVersion" + }, + "handle": { + "type": "string" + }, + "oldIssuers": { + "items": { + "type": "string" + }, + "type": "array" + }, + "replacedBy": { + "$ref": "#/components/schemas/UUID" + }, + "team": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "team", + "oldIssuers", + "handle" + ], + "type": "object" + }, + "WireIdPAPIVersion": { + "enum": [ + "WireIdPAPIV1", + "WireIdPAPIV2" + ], + "type": "string" + }, + "XmlText": { + "properties": { + "fromXmlText": { + "type": "string" + } + }, + "required": [ + "fromXmlText" + ], + "type": "object" + }, + "new-otr-message": { + "properties": { + "data": { + "type": "string" + }, + "native_priority": { + "$ref": "#/components/schemas/Priority" + }, + "native_push": { + "type": "boolean" + }, + "recipients": { + "$ref": "#/components/schemas/UserClientMap" + }, + "report_missing": { + "items": { + "$ref": "#/components/schemas/UUID" + }, + "type": "array" + }, + "sender": { + "description": "A 64-bit unsigned integer, represented as a hexadecimal numeral. Any valid hexadecimal numeral is accepted, but the backend will only produce representations with lowercase digits and no leading zeros", + "type": "string" + }, + "transient": { + "type": "boolean" + } + }, + "required": [ + "sender", + "recipients" + ], + "type": "object" + } + }, + "securitySchemes": { + "ZAuth": { + "description": "Must be a token retrieved by calling 'POST /login' or 'POST /access'. It must be presented in this format: 'Bearer \\'.", + "in": "header", + "name": "Authorization", + "type": "apiKey" + } + } + }, + "info": { + "description": "## Authentication / Authorization\n\nThe end-points in this API support differing authorization protocols:\nsome are unauthenticated (`/api-version`, `/login`), some require\n[zauth](), and some support both [zauth]() and [oauth]().\n\nThe end-points that require zauth are labelled so in the description\nbelow. The end-points that support oauth as an alternative to zauth\nhave the required oauth scopes listed in the same description.\n\nFuther reading:\n- https://docs.wire.com/developer/reference/oauth.html\n- https://github.com/wireapp/wire-server/blob/develop/libs/wire-api/src/Wire/API/Routes/Public.hs (search for HasSwagger instances)\n- `curl https://staging-nginz-https.zinfra.io/v4/api/swagger.json | jq '.security, .securityDefinitions`\n\n### SSO Endpoints\n\n#### Overview\n\n`/sso/metadata` will be requested by the IdPs to learn how to talk to wire.\n\n`/sso/initiate-login`, `/sso/finalize-login` are for the SAML authentication handshake performed by a user in order to log into wire. They are not exactly standard in their details: they may return HTML or XML; redirect to error URLs instead of throwing errors, etc.\n\n`/identity-providers` end-points are for use in the team settings page when IdPs are registered. They talk json.\n\n\n#### Configuring IdPs\n\nIdPs usually allow you to copy the metadata into your clipboard. That should contain all the details you need to post the idp in your team under `/identity-providers`. (Team id is derived from the authorization credentials of the request.)\n\n##### okta.com\n\nOkta will ask you to provide two URLs when you set it up for talking to wireapp:\n\n1. The `Single sign on URL`. This is the end-point that accepts the user's credentials after successful authentication against the IdP. Choose `/sso/finalize-login` with schema and hostname of the wire server you are configuring.\n\n2. The `Audience URI`. You can find this in the metadata returned by the `/sso/metadata` end-point. It is the contents of the `md:OrganizationURL` element.\n\n##### centrify.com\n\nCentrify allows you to upload the metadata xml document that you get from the `/sso/metadata` end-point. You can also enter the metadata url and have centrify retrieve the xml, but to guarantee integrity of the setup, the metadata should be copied from the team settings page and pasted into the centrify setup page without any URL indirections.\n\n## Federation errors\n\nEndpoints involving federated calls to other domains can return some extra failure responses, common to all endpoints. Instead of listing them as possible responses for each endpoint, we document them here.\n\nFor errors that are more likely to be transient, we suggest clients to retry whatever request resulted in the error. Transient errors are indicated explicitly below.\n\n**Note**: when a failure occurs as a result of making a federated RPC to another backend, the error response contains the following extra fields:\n\n - `type`: \"federation\" (just the literal string in quotes, which can be used as an error type identifier when parsing errors)\n - `domain`: the target backend of the RPC that failed;\n - `path`: the path of the RPC that failed.\n\n### Domain errors\n\nErrors in this category result from trying to communicate with a backend that is considered non-existent or invalid. They can result from invalid user input or client issues, but they can also be a symptom of misconfiguration in one or multiple backends. These errors have a 4xx status code.\n\n - **Remote backend not found** (status: 422, label: `invalid-domain`): This backend attempted to contact a backend which does not exist or is not properly configured. For the most part, clients can consider this error equivalent to a domain not existing, although it should be noted that certain mistakes in the DNS configuration on a remote backend can lead to the backend not being recognized, and hence to this error. It is therefore not advisable to take any destructive action upon encountering this error, such as deleting remote users from conversations.\n - **Federation denied locally** (status: 400, label: `federation-denied`): This backend attempted an RPC to a non-whitelisted backend. Similar considerations as for the previous error apply.\n - **Federation not enabled** (status: 400, label: `federation-not-enabled`): Federation has not been configured for this backend. This will happen if a federation-aware client tries to talk to a backend for which federation is disabled, or if federation was disabled on the backend after reaching a federation-specific state (e.g. conversations with remote users). There is no way to cleanly recover from these errors at this point.\n\n### Local federation errors\n\nAn error in this category likely indicates an issue with the configuration of federation on the local backend. Possibly transient errors are indicated explicitly below. All these errors have a 500 status code.\n\n - **Federation unavailable** (status: 500, label: `federation-not-available`): Federation is configured for this backend, but the local federator cannot be reached. This can be transient, so clients should retry the request.\n - **Federation not implemented** (status: 500, label: `federation-not-implemented`): Federated behaviour for a certain endpoint is not yet implemented.\n - **Federator discovery failed** (status: 400, label: `discovery-failure`): A DNS error occurred during discovery of a remote backend. This can be transient, so clients should retry the request.\n - **Local federation error** (status: 500, label: `federation-local-error`): An error occurred in the communication between this backend and its local federator. These errors are most likely caused by bugs in the backend, and should be reported as such.\n\n### Remote federation errors\n\nErrors in this category are returned in case of communication issues between the local backend and a remote one, or if the remote side encountered an error while processing an RPC. Some errors in this category might be caused by incorrect client behaviour, wrong user input, or incorrect certificate configuration. Possibly transient errors are indicated explicitly. We use non-standard 5xx status codes for these errors.\n\n - **HTTP2 error** (status: 533, label: `federation-http2-error`): The current federator encountered an error when making an HTTP2 request to a remote one. Check the error message for more details.\n - **Connection refused** (status: 521, label: `federation-connection-refused`): The local federator could not connect to a remote one. This could be transient, so clients should retry the request.\n - **TLS failure**: (status: 525, label: `federation-tls-error`): An error occurred during the TLS handshake between the local federator and a remote one. This is most likely due to an issue with the certificate on the remote end.\n - **Remote federation error** (status: 533, label: `federation-remote-error`): The remote backend could not process a request coming from this backend. Check the error message for more details.\n - **Version negotiation error** (status: 533, label: `federation-version-error`): The remote backend returned invalid version information.\n\n### Backend compatibility errors\n\nAn error in this category will be returned when this backend makes an invalid or unsupported RPC to another backend. This can indicate some incompatibility between backends or a backend bug. These errors are unlikely to be transient, so retrying requests is *not* advised.\n\n - **Version mismatch** (status: 531, label: `federation-version-mismatch`): A remote backend is running an unsupported version of the federator.\n - **Invalid content type** (status: 533, label: `federation-invalid-content-type`): An RPC to another backend returned with an invalid content type.\n - **Unsupported content type** (status: 533, label: `federation-unsupported-content-type`): An RPC to another backend returned with an unsupported content type.\n", + "title": "Wire-Server API", + "version": "" + }, + "openapi": "3.0.0", + "paths": { + "/": { + "get": { + "description": " [internal route ID: \"get-services-tags\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceTagList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get services tags" + } + }, + "/access": { + "post": { + "description": " [internal route ID: \"access\"]\n\nYou can provide only a cookie or a cookie and token. Every other combination is invalid. Access tokens can be given as query parameter or authorisation header, with the latter being preferred.Calls federation service brig on send-connection-action", + "parameters": [ + { + "in": "query", + "name": "client_id", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + } + }, + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Obtain an access tokens for a cookie" + } + }, + "/access/logout": { + "post": { + "description": " [internal route ID: \"logout\"]\n\nCalling this endpoint will effectively revoke the given cookie and subsequent calls to /access with the same cookie will result in a 403.", + "responses": { + "200": { + "description": "Logout" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Log out in order to remove a cookie from the server" + } + }, + "/access/self/email": { + "put": { + "description": " [internal route ID: \"change-self-email\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate" + } + } + }, + "required": true + }, + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Update accepted and pending activation of the new email" + }, + "204": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "No update, current and new email address are the same" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid e-mail address. (label: `invalid-email`) or `body`" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Change your email address" + } + }, + "/activate": { + "get": { + "description": " [internal route ID: \"get-activate\"]\n\nSee also 'POST /activate' which has a larger feature set.
Calls federation service brig on send-connection-action", + "parameters": [ + { + "description": "Activation key", + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Activation code", + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + } + }, + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `code` or `key`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Activate (i.e. confirm) an email address." + }, + "post": { + "description": " [internal route ID: \"post-activate\"]\n\nActivation only succeeds once and the number of failed attempts for a valid key is limited.Calls federation service brig on send-connection-action", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Activate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ActivationResponse" + } + } + }, + "description": "Activation successful.\n\nActivation successful. (Dry run)\n\nActivation successful." + }, + "204": { + "description": "A recent activation was already successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-phone", + "message": "Invalid mobile phone number" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-phone", + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid mobile phone number (label: `invalid-phone`)\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "Invalid activation code" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid activation code (label: `invalid-code`)\n\nUser does not exist (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Activate (i.e. confirm) an email address." + } + }, + "/activate/send": { + "post": { + "description": " [internal route ID: \"post-activate-send\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SendActivationCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Activation code sent." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "blacklisted-email", + "message": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "blacklisted-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + }, + "451": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 451, + "label": "domain-blocked-for-registration", + "message": "[Customer extension] the email domain example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department." + }, + "properties": { + "code": { + "enum": [ + 451 + ], + "type": "integer" + }, + "label": { + "enum": [ + "domain-blocked-for-registration" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "[Customer extension] the email domain example.com that you are attempting to register a user with has been blocked for creating wire users. Please contact your IT department. (label: `domain-blocked-for-registration`)" + } + }, + "summary": "Send (or resend) an email activation code." + } + }, + "/api-version": { + "get": { + "description": " [internal route ID: \"get-version\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/VersionInfo" + } + } + }, + "description": "" + } + } + } + }, + "/assets": { + "post": { + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-length", + "message": "Invalid content length" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/assets/{key_domain}/{key}": { + "delete": { + "description": "**Note**: only local assets can be deleted.", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key_domain` or `key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "description": "**Note**: local assets result in a redirect, while remote assets are streamed directly.Calls federation service cargohold on stream-asset
Calls federation service cargohold on get-asset", + "parameters": [ + { + "in": "path", + "name": "key_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset returned directly with content type `application/octet-stream`" + }, + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key_domain` or `key` or Asset not found (label: `not-found`)\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/assets/{key}/token": { + "delete": { + "description": "**Note**: deleting the token makes the asset public.", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset token deleted" + } + }, + "summary": "Delete an asset token" + }, + "post": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewAssetToken" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Renew an asset token" + } + }, + "/await": { + "get": { + "description": " [internal route ID: \"await-notifications\"]\n\n", + "externalDocs": { + "description": "RFC 6455", + "url": "https://datatracker.ietf.org/doc/html/rfc6455" + }, + "parameters": [ + { + "description": "Client ID", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "101": { + "description": "Connection upgraded." + }, + "426": { + "description": "Upgrade required." + } + }, + "summary": "Establish websocket connection" + } + }, + "/bot/assets": { + "post": { + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-length", + "message": "Invalid content length" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/bot/assets/{key}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` or Asset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/bot/client": { + "get": { + "description": " [internal route ID: \"bot-get-client-v6\"]\n\n", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Clientv6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Clientv6" + } + } + }, + "description": "Client found" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Client not found (label: `client-not-found`)\n\nClient not found (label: `client-not-found`)" + } + }, + "summary": "Get client for bot" + } + }, + "/bot/client/prekeys": { + "get": { + "description": " [internal route ID: \"bot-list-prekeys\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List prekeys for bot" + }, + "post": { + "description": " [internal route ID: \"bot-update-prekeys\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateBotPrekeys" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "client-not-found", + "message": "Client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Client not found (label: `client-not-found`)" + } + }, + "summary": "Update prekeys for bot" + } + }, + "/bot/conversation": { + "get": { + "description": " [internal route ID: \"get-bot-conversation\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/BotConvView" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + } + } + } + }, + "/bot/messages": { + "post": { + "description": " [internal route ID: \"post-bot-message-unqualified\"]\n\nCalls federation service brig on get-user-clients
Calls federation service galley on on-message-sent", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Missing clients" + } + } + } + }, + "/bot/self": { + "delete": { + "description": " [internal route ID: \"bot-delete-self\"]\n\n", + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-bot", + "message": "The targeted user is not a bot." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-bot", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The targeted user is not a bot. (label: `invalid-bot`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Delete self" + }, + "get": { + "description": " [internal route ID: \"bot-get-self\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserProfile" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "User not found (label: `not-found`)" + } + }, + "summary": "Get self" + } + }, + "/bot/users": { + "get": { + "description": " [internal route ID: \"bot-list-users\"]\n\n", + "parameters": [ + { + "in": "query", + "name": "ids", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/BotUserView" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List users" + } + }, + "/bot/users/prekeys": { + "post": { + "description": " [internal route ID: \"bot-claim-users-prekeys\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserClients" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserClientPrekeyMap" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients", + "too-many-clients", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nToo many clients (label: `too-many-clients`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Claim users prekeys" + } + }, + "/bot/users/{User ID}/clients": { + "get": { + "description": " [internal route ID: \"bot-get-user-clients\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "User ID", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get user clients" + } + }, + "/broadcast/otr/messages": { + "post": { + "description": " [internal route ID: \"post-otr-broadcast-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "parameters": [ + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + }, + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-users-to-broadcast", + "message": "Too many users to fan out the broadcast event to" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-users-to-broadcast" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `report_missing` or `ignore_missing`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Broadcast an encrypted message to all team members and all contacts (accepts JSON or Protobuf)" + } + }, + "/broadcast/proteus/messages": { + "post": { + "description": " [internal route ID: \"post-proteus-broadcast\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\nThe sending of messages in a federated conversation could theoretically fail partially. To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. So, if any backend is down, the message is not propagated to anyone. But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.", + "requestBody": { + "content": { + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/QualifiedNewOtrMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-users-to-broadcast", + "message": "Too many users to fan out the broadcast event to" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-users-to-broadcast" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nToo many users to fan out the broadcast event to (label: `too-many-users-to-broadcast`)" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "non-binding-team", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nNot a member of a binding team (label: `non-binding-team`)\n\nTeam not found (label: `no-team`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to all team members and all contacts (accepts only Protobuf)" + } + }, + "/calls/config": { + "get": { + "deprecated": true, + "description": " [internal route ID: \"get-calls-config\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RTCConfiguration" + } + } + }, + "description": "" + } + }, + "summary": "Retrieve TURN server addresses and credentials for IP addresses, scheme `turn` and transport `udp` only (deprecated)" + } + }, + "/calls/config/v2": { + "get": { + "description": " [internal route ID: \"get-calls-config-v2\"]\n\n", + "parameters": [ + { + "description": "Limit resulting list. Allowed values [1..10]", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "maximum": 10, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RTCConfiguration" + } + } + }, + "description": "" + } + }, + "summary": "Retrieve all TURN server addresses and credentials. Clients are expected to do a DNS lookup to resolve the IP addresses of the given hostnames " + } + }, + "/clients": { + "get": { + "description": " [internal route ID: \"list-clients-v6\"]\n\n", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientListv6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientListv6" + } + } + }, + "description": "List of clients" + } + }, + "summary": "List the registered clients" + }, + "post": { + "description": " [internal route ID: \"add-client\"]\n\nCalls federation service brig on send-connection-action", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewClient" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Client" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Client" + } + } + }, + "description": "Client registered", + "headers": { + "Location": { + "description": "Client ID", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "bad-request", + "message": "Malformed prekeys uploaded" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "missing-auth", + "too-many-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nToo many clients (label: `too-many-clients`)" + } + }, + "summary": "Register a new client" + } + }, + "/clients/{cid}/access-token": { + "post": { + "description": " [internal route ID: \"create-access-token\"]\n\nCreate an JWT DPoP access token for the client CSR, given a JWT DPoP proof, specified in the `DPoP` header. The access token will be returned as JWT DPoP token in the `DPoP` header.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "cid", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "DPoP", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DPoPAccessTokenResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DPoPAccessTokenResponse" + } + } + }, + "description": "Access token created", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Create a JWT DPoP access token" + } + }, + "/clients/{client}": { + "delete": { + "description": " [internal route ID: \"delete-client\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteClient" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Client deleted" + } + }, + "summary": "Delete an existing client" + }, + "get": { + "description": " [internal route ID: \"get-client-v6\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Clientv6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Clientv6" + } + } + }, + "description": "Client found" + }, + "404": { + "description": "`client` or Client not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a registered client by ID" + }, + "put": { + "description": " [internal route ID: \"update-client\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateClient" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Client updated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "bad-request", + "message": "Malformed prekeys uploaded" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "bad-request" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMalformed prekeys uploaded (label: `bad-request`)" + } + }, + "summary": "Update a registered client" + } + }, + "/clients/{client}/capabilities": { + "get": { + "description": " [internal route ID: \"get-client-capabilities\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientCapabilityList" + } + } + }, + "description": "" + } + }, + "summary": "Read back what the client has been posting about itself" + } + }, + "/clients/{client}/nonce": { + "get": { + "description": " [internal route ID: \"get-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + }, + "Replay-Nonce": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Get a new nonce for a client CSR" + }, + "head": { + "description": " [internal route ID: \"head-nonce\"]\n\nGet a new nonce for a client CSR, specified in the response header `Replay-Nonce` as a uuidv4 in base64url encoding.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "No Content", + "headers": { + "Cache-Control": { + "schema": { + "type": "string" + } + }, + "Replay-Nonce": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Get a new nonce for a client CSR" + } + }, + "/clients/{client}/prekeys": { + "get": { + "description": " [internal route ID: \"get-client-prekeys\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "List the remaining prekey IDs of a client" + } + }, + "/connections/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-connection\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection found" + }, + "404": { + "description": "`uid_domain` or `uid` or Connection not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get an existing connection to another user (local or remote)" + }, + "post": { + "description": " [internal route ID: \"create-connection\"]\n\nYou can have no more than 1000 connections in accepted or sent state
Calls federation service brig on send-connection-action
Calls federation service brig on get-users-by-ids", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection existed" + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection was created" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified email" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "connection-limit", + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The user has no verified email (label: `no-identity`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + } + }, + "summary": "Create a connection to another user" + }, + "put": { + "description": " [internal route ID: \"update-connection\"]\n\nCalls federation service brig on send-connection-action
Calls federation service brig on get-users-by-ids", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConnectionUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserConnection" + } + } + }, + "description": "Connection updated" + }, + "204": { + "description": "Connection unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-identity", + "message": "The user has no verified email" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-identity", + "bad-conn-update", + "not-connected", + "connection-limit", + "missing-legalhold-consent", + "missing-legalhold-consent-old-clients" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The user has no verified email (label: `no-identity`)\n\nInvalid status transition (label: `bad-conn-update`)\n\nUsers are not connected (label: `not-connected`)\n\nToo many sent/accepted connections (label: `connection-limit`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)" + } + }, + "summary": "Update a connection to another user" + } + }, + "/conversations": { + "post": { + "description": " [internal route ID: \"create-group-conversation\"]\n\nThis returns 201 when a new conversation is created, and 200 when the conversation already existed
Calls federation service galley on on-conversation-updated
Calls federation service galley on on-conversation-created
Calls federation service brig on get-not-fully-connected-backends
Calls federation service brig on api-version", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewConv" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversationv6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateGroupConversationv6" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled", + "non-empty-member-list" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nAttempting to add group members outside MLS (label: `non-empty-member-list`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "operation-denied", + "no-team-member", + "not-connected", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nUsers are not connected (label: `not-connected`)\n\nConversation access denied (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Create a new conversation" + } + }, + "/conversations/code-check": { + "post": { + "description": " [internal route ID: \"code-check\"]\n\nIf the guest links team feature is disabled, this will fail with 404 CodeNotFound.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/join` which responds with 409 GuestLinksDisabled if guest links are disabled.", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Valid" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation-password", + "message": "Invalid conversation password" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + } + }, + "summary": "Check validity of a conversation code." + } + }, + "/conversations/join": { + "get": { + "description": " [internal route ID: \"get-conversation-by-reusable-code\"]\n\n", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCoverView" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Get limited conversation information by key/code pair" + }, + "post": { + "description": " [internal route ID: \"join-conversation-by-code-unqualified\"]\n\nIf the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled.Calls federation service galley on on-conversation-updated", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/JoinConversationByCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation joined" + }, + "204": { + "description": "Conversation unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "too-many-members", + "message": "Maximum number of members per conversation reached" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-members", + "no-team-member", + "invalid-op", + "access-denied", + "invalid-conversation-password" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Maximum number of members per conversation reached (label: `too-many-members`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInvalid conversation password (label: `invalid-conversation-password`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Join a conversation using a reusable code" + } + }, + "/conversations/list": { + "post": { + "description": " [internal route ID: \"list-conversations\"]\n\nCalls federation service galley on get-conversations", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListConversations" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationsResponse" + } + } + }, + "description": "" + } + }, + "summary": "Get conversation metadata for a list of conversation ids" + } + }, + "/conversations/list-ids": { + "post": { + "description": " [internal route ID: \"list-conversation-ids\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetPaginated_ConversationIds" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationIds_Page" + } + } + }, + "description": "" + } + }, + "summary": "Get all conversation IDs." + } + }, + "/conversations/mls-self": { + "get": { + "description": " [internal route ID: \"get-mls-self-conversation\"]\n\n", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + } + }, + "description": "The MLS self-conversation" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + } + }, + "summary": "Get the user's MLS self-conversation" + } + }, + "/conversations/one2one": { + "post": { + "description": " [internal route ID: \"create-one-to-one-conversation\"]\n\nCalls federation service galley on on-conversation-created", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewConv" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationV3v3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationV3v3" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationV3v3" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationV3v3" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "operation-denied", + "not-connected", + "no-team-member", + "non-binding-team-members", + "invalid-op", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nInsufficient permissions (label: `operation-denied`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nBoth users must be members of the same binding team (label: `non-binding-team-members`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "non-binding-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)\n\nNot a member of a binding team (label: `non-binding-team`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Create a 1:1 conversation" + } + }, + "/conversations/one2one/{usr_domain}/{usr}": { + "get": { + "description": " [internal route ID: \"get-one-to-one-mls-conversation\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + } + }, + "description": "MLS 1-1 conversation" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "not-connected", + "message": "Users are not connected" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-connected" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Users are not connected (label: `not-connected`)" + } + }, + "summary": "Get an MLS 1:1 conversation" + } + }, + "/conversations/self": { + "post": { + "description": " [internal route ID: \"create-self-conversation\"]\n\n", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + } + }, + "description": "Conversation existed", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationV6v6" + } + } + }, + "description": "Conversation created", + "headers": { + "Location": { + "description": "Conversation ID", + "schema": { + "format": "uuid", + "type": "string" + } + } + } + } + }, + "summary": "Create a self-conversation" + } + }, + "/conversations/{Conversation ID}/bots": { + "post": { + "description": " [internal route ID: \"add-bot\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "Conversation ID", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddBot" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddBotResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AddBotResponse" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "service-disabled", + "message": "The desired service is currently disabled." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "service-disabled", + "too-many-members", + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The desired service is currently disabled. (label: `service-disabled`)\n\nMaximum number of members per conversation reached. (label: `too-many-members`)\n\nThe operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Add bot" + } + }, + "/conversations/{Conversation ID}/bots/{Bot ID}": { + "delete": { + "description": " [internal route ID: \"remove-bot\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "Conversation ID", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "Bot ID", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveBotResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveBotResponse" + } + } + }, + "description": "User found" + }, + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-conversation", + "message": "The operation is not allowed in this conversation." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-conversation", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The operation is not allowed in this conversation. (label: `invalid-conversation`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Remove bot" + } + }, + "/conversations/{cnv_domain}/{cnv}": { + "get": { + "description": " [internal route ID: \"get-conversation\"]\n\nCalls federation service galley on get-conversations", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Conversation" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get a conversation by ID" + } + }, + "/conversations/{cnv_domain}/{cnv}/access": { + "put": { + "description": " [internal route ID: \"update-conversation-access\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationAccessData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Access updated" + }, + "204": { + "description": "Access unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid target access" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid target access (label: `invalid-op`)\n\nInvalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nInsufficient authorization (missing modify_conversation_access) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update access modes for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/groupinfo": { + "get": { + "description": " [internal route ID: \"get-group-info\"]\n\nCalls federation service galley on query-group-info", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/GroupInfoData" + } + } + }, + "description": "The group information" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-missing-group-info", + "message": "The conversation has no group information" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-missing-group-info", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get MLS group information" + } + }, + "/conversations/{cnv_domain}/{cnv}/members": { + "post": { + "description": " [internal route ID: \"add-members-to-conversation\"]\n\nCalls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InviteQualified" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation updated" + }, + "204": { + "description": "Conversation unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "missing-legalhold-consent", + "message": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "missing-legalhold-consent", + "not-connected", + "no-team-member", + "access-denied", + "too-many-members", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Failed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nUsers are not connected (label: `not-connected`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nConversation access denied (label: `access-denied`)\n\nMaximum number of members per conversation reached (label: `too-many-members`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nInsufficient authorization (missing add_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Add qualified members to an existing conversation." + } + }, + "/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}": { + "delete": { + "description": " [internal route ID: \"remove-member\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated
Calls federation service galley on leave-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Target User ID", + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Member removed" + }, + "204": { + "description": "No change" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Remove a member from a conversation" + }, + "put": { + "description": " [internal route ID: \"update-other-member\"]\n\n**Note**: at least one field has to be provided.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "usr_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Target User ID", + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OtherMemberUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Membership updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation-member", + "message": "Conversation member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation-member", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `usr_domain` or `usr` not found\n\nConversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update membership of the specified user" + } + }, + "/conversations/{cnv_domain}/{cnv}/message-timer": { + "put": { + "description": " [internal route ID: \"update-conversation-message-timer\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationMessageTimerUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Message timer updated" + }, + "204": { + "description": "Message timer unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update the message timer for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/name": { + "put": { + "description": " [internal route ID: \"update-conversation-name\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRename" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Name unchanged" + }, + "204": { + "description": "Name updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update conversation name" + } + }, + "/conversations/{cnv_domain}/{cnv}/proteus/messages": { + "post": { + "description": " [internal route ID: \"post-proteus-message\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts `client_mismatch_strategy` in the body. It can have these values:\n- `report_all`: When set, the message is not sent if any clients are missing. The missing clients are reported in the response.\n- `ignore_all`: When set, no checks about missing clients are carried out.\n- `report_only`: Takes a list of qualified UserIDs. If any clients of the listed users are missing, the message is not sent. The missing clients are reported in the response.\n- `ignore_only`: Takes a list of qualified UserIDs. If any clients of the non-listed users are missing, the message is not sent. The missing clients are reported in the response.\n\nThe sending of messages in a federated conversation could theoretically fail partially. To make this case unlikely, the backend first gets a list of clients from all the involved backends and then tries to send a message. So, if any backend is down, the message is not propagated to anyone. But the actual message fan out to multiple backends could still fail partially. This type of failure is reported as a 201, the clients for which the message sending failed are part of the response body.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.Calls federation service galley on send-message
Calls federation service galley on on-message-sent
Calls federation service brig on get-user-clients", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/QualifiedNewOtrMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or Conversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MessageSendingStatus" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to a conversation (accepts only Protobuf)" + } + }, + "/conversations/{cnv_domain}/{cnv}/protocol": { + "put": { + "description": " [internal route ID: \"update-conversation-protocol\"]\n\n**Note**: Only proteus->mixed upgrade is supported.", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProtocolUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation updated" + }, + "204": { + "description": "Conversation unchanged" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-migration-criteria-not-satisfied", + "message": "The migration criteria for mixed to MLS protocol transition are not satisfied for this conversation" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-migration-criteria-not-satisfied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nThe migration criteria for mixed to MLS protocol transition are not satisfied for this conversation (label: `mls-migration-criteria-not-satisfied`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "invalid-op", + "action-denied", + "invalid-protocol-transition" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing leave_conversation) (label: `action-denied`)\n\nProtocol transition is invalid (label: `invalid-protocol-transition`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nTeam not found (label: `no-team`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update the protocol of the conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/receipt-mode": { + "put": { + "description": " [internal route ID: \"update-conversation-receipt-mode\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on update-conversation
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationReceiptModeUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Receipt mode updated" + }, + "204": { + "description": "Receipt mode unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update receipt mode for a conversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/self": { + "put": { + "description": " [internal route ID: \"update-conversation-self\"]\n\n**Note**: at least one field has to be provided.", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MemberUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Update successful" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update self membership properties" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}": { + "delete": { + "description": " [internal route ID: \"delete-subconversation\"]\n\nCalls federation service galley on delete-sub-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteSubConversationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Deletion successful" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Delete an MLS subconversation" + }, + "get": { + "description": " [internal route ID: \"get-subconversation\"]\n\nCalls federation service galley on get-sub-conversation", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicSubConversation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PublicSubConversation" + } + } + }, + "description": "Subconversation" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-subconv-unsupported-convtype", + "message": "MLS subconversations are only supported for regular conversations" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-subconv-unsupported-convtype", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS subconversations are only supported for regular conversations (label: `mls-subconv-unsupported-convtype`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get information about an MLS subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/groupinfo": { + "get": { + "description": " [internal route ID: \"get-subconversation-group-info\"]\n\nCalls federation service galley on query-group-info", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/GroupInfoData" + } + } + }, + "description": "The group information" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-missing-group-info", + "message": "The conversation has no group information" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-missing-group-info", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nThe conversation has no group information (label: `mls-missing-group-info`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get MLS group information of subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/subconversations/{subconv}/self": { + "delete": { + "description": " [internal route ID: \"leave-subconversation\"]\n\nCalls federation service galley on leave-sub-conversation
Calls federation service galley on on-mls-message-sent", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "subconv", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled", + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` or `subconv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "mls-stale-message", + "message": "The conversation epoch in a message is too old" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-stale-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The conversation epoch in a message is too old (label: `mls-stale-message`)" + } + }, + "summary": "Leave an MLS subconversation" + } + }, + "/conversations/{cnv_domain}/{cnv}/typing": { + "post": { + "description": " [internal route ID: \"member-typing-qualified\"]\n\nCalls federation service galley on on-typing-indicator-updated
Calls federation service galley on update-typing-indicator", + "parameters": [ + { + "in": "path", + "name": "cnv_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TypingData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Notification sent" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv_domain` or `cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Sending typing notifications" + } + }, + "/conversations/{cnv}": { + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-conversation-name-deprecated\"]\n\nUse `/conversations/:domain/:conv/name` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRename" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Name updated" + }, + "204": { + "description": "Name unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update conversation name (deprecated)" + } + }, + "/conversations/{cnv}/code": { + "delete": { + "description": " [internal route ID: \"remove-code-unqualified\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation code deleted." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Delete conversation code" + }, + "get": { + "description": " [internal route ID: \"get-code\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + } + }, + "description": "Conversation Code" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation", + "no-conversation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)\n\nConversation code not found (label: `no-conversation-code`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "guest-links-disabled", + "message": "The guest link feature is disabled and all guest links have been revoked" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Get existing conversation code" + }, + "post": { + "description": " [internal route ID: \"create-conversation-code-unqualified\"]\n\n\nOAuth scope: `write:conversations_code`", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateConversationCodeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationCodeInfo" + } + } + }, + "description": "Conversation code already exists." + }, + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Conversation code created." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "create-conv-code-conflict", + "message": "Conversation code already exists with a different password setting than the requested one." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "create-conv-code-conflict", + "guest-links-disabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation code already exists with a different password setting than the requested one. (label: `create-conv-code-conflict`)\n\nThe guest link feature is disabled and all guest links have been revoked (label: `guest-links-disabled`)" + } + }, + "summary": "Create or recreate a conversation code" + } + }, + "/conversations/{cnv}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: \"get-conversation-guest-links-status\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get the status of the guest links feature for a conversation that potentially has been created by someone from another team." + } + }, + "/conversations/{cnv}/members/{usr}": { + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-other-member-unqualified\"]\n\nUse `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` insteadCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Target User ID", + "in": "path", + "name": "usr", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OtherMemberUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Membership updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInvalid target (label: `invalid-op`)\n\nInsufficient authorization (missing modify_other_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation-member", + "message": "Conversation member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation-member", + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` or `usr` not found\n\nConversation member not found (label: `no-conversation-member`)\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update membership of the specified user (deprecated)" + } + }, + "/conversations/{cnv}/message-timer": { + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-conversation-message-timer-unqualified\"]\n\nUse `/conversations/:domain/:cnv/message-timer` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationMessageTimerUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Message timer updated" + }, + "204": { + "description": "Message timer unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_message_timer) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update the message timer for a conversation (deprecated)" + } + }, + "/conversations/{cnv}/name": { + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-conversation-name-unqualified\"]\n\nUse `/conversations/:domain/:conv/name` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRename" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Name updated" + }, + "204": { + "description": "Name unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing modify_conversation_name) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update conversation name (deprecated)" + } + }, + "/conversations/{cnv}/otr/messages": { + "post": { + "description": " [internal route ID: \"post-otr-message-unqualified\"]\n\nThis endpoint ensures that the list of clients is correct and only sends the message if the list is correct.\nTo override this, the endpoint accepts two query params:\n- `ignore_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are ignored.\n - When 'false' all missing clients are reported.\n - When comma separated list of user-ids, only clients for listed users are ignored.\n- `report_missing`: Can be 'true' 'false' or a comma separated list of user IDs.\n - When 'true' all missing clients are reported.\n - When 'false' all missing clients are ignored.\n - When comma separated list of user-ids, only clients for listed users are reported.\n\nApart from these, the request body also accepts `report_missing` which can only be a list of user ids and behaves the same way as the query parameter.\n\nAll three of these should be considered mutually exclusive. The server however does not error if more than one is specified, it reads them in this order of precedence:\n- `report_missing` in the request body has highest precedence.\n- `ignore_missing` in the query param is the next.\n- `report_missing` in the query param has the lowest precedence.\n\nThis endpoint can lead to OtrMessageAdd event being sent to the recipients.\n\n**NOTE:** The protobuf definitions of the request body can be found at https://github.com/wireapp/generic-message-proto/blob/master/proto/otr.proto.Calls federation service brig on get-user-clients
Calls federation service galley on on-message-sent", + "parameters": [ + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "ignore_missing", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "report_missing", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + }, + "application/x-protobuf": { + "schema": { + "$ref": "#/components/schemas/new-otr-message" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Message sent" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unknown-client", + "message": "Unknown Client" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unknown-client", + "missing-legalhold-consent-old-clients", + "missing-legalhold-consent" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unknown Client (label: `unknown-client`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has old clients that do not support legalhold's UI requirements (label: `missing-legalhold-consent-old-clients`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` or Conversation not found (label: `no-conversation`)" + }, + "412": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientMismatch" + } + } + }, + "description": "Missing clients" + } + }, + "summary": "Post an encrypted message to a conversation (accepts JSON or Protobuf)" + } + }, + "/conversations/{cnv}/receipt-mode": { + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-conversation-receipt-mode-unqualified\"]\n\nUse `PUT /conversations/:domain/:cnv/receipt-mode` instead.Calls federation service brig on get-users-by-ids
Calls federation service galley on update-conversation
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationReceiptModeUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + "description": "Receipt mode updated" + }, + "204": { + "description": "Receipt mode unchanged" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "access-denied", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nConversation access denied (label: `access-denied`)\n\nInsufficient authorization (missing modify_conversation_receipt_mode) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update receipt mode for a conversation (deprecated)" + } + }, + "/conversations/{cnv}/roles": { + "get": { + "description": " [internal route ID: \"get-conversation-roles\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRolesList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Conversation access denied" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Conversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get existing roles available for the given conversation" + } + }, + "/conversations/{cnv}/self": { + "get": { + "deprecated": true, + "description": " [internal route ID: \"get-conversation-self-unqualified\"]\n\n", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Member" + } + } + }, + "description": "" + } + }, + "summary": "Get self membership properties (deprecated)" + }, + "put": { + "deprecated": true, + "description": " [internal route ID: \"update-conversation-self-unqualified\"]\n\nUse `/conversations/:domain/:conv/self` instead.", + "parameters": [ + { + "description": "Conversation ID", + "in": "path", + "name": "cnv", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MemberUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Update successful" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`cnv` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Update self membership properties (deprecated)" + } + }, + "/cookies": { + "get": { + "description": " [internal route ID: \"list-cookies\"]\n\n", + "parameters": [ + { + "description": "Filter by label (comma-separated list)", + "in": "query", + "name": "labels", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CookieList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CookieList" + } + } + }, + "description": "List of cookies" + } + }, + "summary": "Retrieve the list of cookies currently stored for the user" + } + }, + "/cookies/remove": { + "post": { + "description": " [internal route ID: \"remove-cookies\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveCookies" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Cookies revoked" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Revoke stored cookies" + } + }, + "/custom-backend/by-domain/{domain}": { + "get": { + "description": " [internal route ID: \"get-custom-backend-by-domain\"]\n\n", + "parameters": [ + { + "description": "URL-encoded email domain", + "in": "path", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CustomBackend" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "custom-backend-not-found", + "message": "Custom backend not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "custom-backend-not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`domain` not found\n\nCustom backend not found (label: `custom-backend-not-found`)" + } + }, + "summary": "Shows information about custom backends related to a given email domain" + } + }, + "/delete": { + "post": { + "description": " [internal route ID: \"verify-delete\"]\n\nCalls federation service brig on send-connection-action", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/VerifyDeleteUser" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid verification code (label: `invalid-code`)" + } + }, + "summary": "Verify account deletion with a code." + } + }, + "/feature-configs": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-user\"]\n\nGets feature configs for a user. If the user is a member of a team and has the required permissions, this will return the team's feature configs.If the user is not a member of a team, this will return the personal feature configs (the server defaults).\nOAuth scope: `read:feature_configs`", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AllFeatureConfigs" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)" + } + }, + "summary": "Gets feature configs for a user" + } + }, + "/identity-providers": { + "get": { + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPList" + } + } + }, + "description": "" + } + } + }, + "post": { + "parameters": [ + { + "in": "query", + "name": "replaces", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "api_version", + "required": false, + "schema": { + "default": "v2", + "enum": [ + "v1", + "v2" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "handle", + "required": false, + "schema": { + "maxLength": 1, + "minLength": 32, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + } + }, + "/identity-providers/{id}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "purge", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "204": { + "description": "" + } + } + }, + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + }, + "put": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "handle", + "required": false, + "schema": { + "maxLength": 1, + "minLength": 32, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/IdPMetadataInfo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/IdPConfig_WireIdP" + } + } + }, + "description": "" + } + } + } + }, + "/identity-providers/{id}/raw": { + "get": { + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/list-connections": { + "post": { + "description": " [internal route ID: \"list-connections\"]\n\nThe IDs returned by this endpoint are paginated. To get the first page, make a call with the `paging_state` field set to `null` (or omitted). Whenever the `has_more` field of the response is set to `true`, more results are available, and they can be obtained by calling the endpoint again, but this time passing the value of `paging_state` returned by the previous call. One can continue in this fashion until all results are returned, which is indicated by `has_more` being `false`. Note that `paging_state` should be considered an opaque token. It should not be inspected, or stored, or reused across multiple unrelated invocations of the endpoint.", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GetPaginated_Connections" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Connections_Page" + } + } + }, + "description": "" + } + }, + "summary": "List the connections to other users, including remote users" + } + }, + "/list-users": { + "post": { + "description": " [internal route ID: \"list-users-by-ids-or-handles\"]\n\nThe 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive.Calls federation service brig on get-users-by-ids", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListUsersQuery" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ListUsersById" + } + } + }, + "description": "" + } + }, + "summary": "List users" + } + }, + "/login": { + "post": { + "description": " [internal route ID: \"login\"]\n\nLogins are throttled at the server's discretionCalls federation service brig on send-connection-action", + "parameters": [ + { + "description": "Request a persistent cookie instead of a session cookie", + "in": "query", + "name": "persist", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Login" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + } + }, + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "pending-activation", + "suspended", + "invalid-credentials" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nAccount pending activation (label: `pending-activation`)\n\nAccount suspended (label: `suspended`)\n\nAuthentication failed (label: `invalid-credentials`)" + } + }, + "summary": "Authenticate a user to obtain a cookie and first access token" + } + }, + "/mls/commit-bundles": { + "post": { + "description": " [internal route ID: \"mls-commit-bundle\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.
Calls federation service brig on api-version
Calls federation service brig on get-users-by-ids
Calls federation service brig on get-mls-clients
Calls federation service galley on on-conversation-updated
Calls federation service galley on send-mls-commit-bundle
Calls federation service galley on mls-welcome
Calls federation service galley on on-mls-message-sent", + "requestBody": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/CommitBundle" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + } + }, + "description": "Commit accepted and forwarded" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-welcome-mismatch", + "message": "The list of targets of a welcome message does not match the list of new clients in a group" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-welcome-mismatch", + "mls-self-removal-not-allowed", + "mls-protocol-error", + "mls-not-enabled", + "mls-invalid-leaf-node-index", + "mls-group-conversation-mismatch", + "mls-commit-missing-references", + "mls-client-sender-user-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nThe list of targets of a welcome message does not match the list of new clients in a group (label: `mls-welcome-mismatch`)\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-subconv-join-parent-missing", + "message": "MLS client cannot join the subconversation because it is not member of the parent conversation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-subconv-join-parent-missing", + "missing-legalhold-consent", + "legalhold-not-enabled", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-proposal-not-found", + "message": "A proposal referenced in a commit message could not be found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-proposal-not-found", + "no-conversation", + "no-conversation-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" + }, + "422": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 422, + "label": "mls-unsupported-proposal", + "message": "Unsupported proposal type" + }, + "properties": { + "code": { + "enum": [ + 422 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-unsupported-proposal", + "mls-unsupported-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Post a MLS CommitBundle" + } + }, + "/mls/key-packages/claim/{user_domain}/{user}": { + "post": { + "description": " [internal route ID: \"mls-key-packages-claim\"]\n\nOnly key packages for the specified ciphersuite are claimed. For backwards compatibility, the `ciphersuite` parameter is optional, defaulting to ciphersuite 0x0001 when omitted.", + "parameters": [ + { + "in": "path", + "name": "user_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "user", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0xf031) - default is 0x0001", + "in": "query", + "name": "ciphersuite", + "required": false, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyPackageBundle" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageBundle" + } + } + }, + "description": "Claimed key packages" + } + }, + "summary": "Claim one key package for each client of the given user" + } + }, + "/mls/key-packages/self/{client}": { + "delete": { + "description": " [internal route ID: \"mls-key-packages-delete\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0xf031) - default is 0x0001", + "in": "query", + "name": "ciphersuite", + "required": false, + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteKeyPackages" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "OK" + } + }, + "summary": "Delete all key packages for a given ciphersuite and client" + }, + "post": { + "description": " [internal route ID: \"mls-key-packages-upload\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageUpload" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Key packages uploaded" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-identity-mismatch", + "message": "Key package credential does not match qualified client ID" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-identity-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" + } + }, + "summary": "Upload a fresh batch of key packages" + }, + "put": { + "description": " [internal route ID: \"mls-key-packages-replace\"]\n\nThe request body should be a json object containing a list of base64-encoded key packages. Use this sparingly.", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Comma-separated list of ciphersuites in hex format (e.g. 0xf031) - default is 0x0001", + "in": "query", + "name": "ciphersuites", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/KeyPackageUpload" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Key packages replaced" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-protocol-error", + "message": "MLS protocol error" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-protocol-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `ciphersuites`\n\nMLS protocol error (label: `mls-protocol-error`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-identity-mismatch", + "message": "Key package credential does not match qualified client ID" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-identity-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Key package credential does not match qualified client ID (label: `mls-identity-mismatch`)" + } + }, + "summary": "Upload a fresh batch of key packages and replace the old ones" + } + }, + "/mls/key-packages/self/{client}/count": { + "get": { + "description": " [internal route ID: \"mls-key-packages-count\"]\n\n", + "parameters": [ + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Ciphersuite in hex format (e.g. 0xf031) - default is 0x0001", + "in": "query", + "name": "ciphersuite", + "required": false, + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OwnKeyPackages" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OwnKeyPackages" + } + } + }, + "description": "Number of key packages" + } + }, + "summary": "Return the number of unclaimed key packages for a given ciphersuite and client" + } + }, + "/mls/messages": { + "post": { + "description": " [internal route ID: \"mls-message\"]\n\n\n\n**Note**: this endpoint can execute proposals, and therefore return all possible errors associated with adding or removing members to a conversation, in addition to the ones listed below. See the documentation of [POST /conversations/{cnv}/members/v2](#/default/post_conversations__cnv__members_v2) and [POST /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}](#/default/delete_conversations__cnv_domain___cnv__members__usr_domain___usr_) for more details on the possible error responses of each type of proposal.
Calls federation service brig on get-mls-clients
Calls federation service galley on on-conversation-updated
Calls federation service galley on send-mls-message
Calls federation service galley on on-mls-message-sent", + "requestBody": { + "content": { + "message/mls": { + "schema": { + "$ref": "#/components/schemas/MLSMessage" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSMessageSendingStatus" + } + } + }, + "description": "Message sent" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-self-removal-not-allowed", + "message": "Self removal from group is not allowed" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-self-removal-not-allowed", + "mls-protocol-error", + "mls-not-enabled", + "mls-invalid-leaf-node-index", + "mls-group-conversation-mismatch", + "mls-commit-missing-references", + "mls-client-sender-user-mismatch" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nSelf removal from group is not allowed (label: `mls-self-removal-not-allowed`)\n\nMLS protocol error (label: `mls-protocol-error`)\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)\n\nA referenced leaf node index points to a blank or non-existing node (label: `mls-invalid-leaf-node-index`)\n\nConversation ID resolved from Group ID does not match submitted Conversation ID (label: `mls-group-conversation-mismatch`)\n\nThe commit is not referencing all pending proposals (label: `mls-commit-missing-references`)\n\nUser ID resolved from Client ID does not match message's sender user ID (label: `mls-client-sender-user-mismatch`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "mls-subconv-join-parent-missing", + "message": "MLS client cannot join the subconversation because it is not member of the parent conversation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-subconv-join-parent-missing", + "missing-legalhold-consent", + "legalhold-not-enabled", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS client cannot join the subconversation because it is not member of the parent conversation (label: `mls-subconv-join-parent-missing`)\n\nFailed to connect to a user or to invite a user to a group because somebody is under legalhold and somebody else has not granted consent (label: `missing-legalhold-consent`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nConversation access denied (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "mls-proposal-not-found", + "message": "A proposal referenced in a commit message could not be found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-proposal-not-found", + "no-conversation", + "no-conversation-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "A proposal referenced in a commit message could not be found (label: `mls-proposal-not-found`)\n\nConversation not found (label: `no-conversation`)\n\nConversation member not found (label: `no-conversation-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "non_federating_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "non_federating_backends" + ], + "type": "object" + } + } + }, + "description": "Adding members to the conversation is not possible because the backends involved do not form a fully connected graph\n\nThe conversation epoch in a message is too old (label: `mls-stale-message`)\n\nA proposal of type Add or Remove does not apply to the full list of clients for a user (label: `mls-client-mismatch`)" + }, + "422": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 422, + "label": "mls-unsupported-proposal", + "message": "Unsupported proposal type" + }, + "properties": { + "code": { + "enum": [ + 422 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-unsupported-proposal", + "mls-unsupported-message" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unsupported proposal type (label: `mls-unsupported-proposal`)\n\nAttempted to send a message with an unsupported combination of content type and wire format (label: `mls-unsupported-message`)" + }, + "533": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "unreachable_backends": { + "items": { + "$ref": "#/components/schemas/Domain" + }, + "type": "array" + } + }, + "required": [ + "unreachable_backends" + ], + "type": "object" + } + } + }, + "description": "Some domains are unreachable" + } + }, + "summary": "Post an MLS message" + } + }, + "/mls/public-keys": { + "get": { + "description": " [internal route ID: \"mls-public-keys\"]\n\n", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MLSKeysByPurpose" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSKeysByPurpose" + } + } + }, + "description": "Public keys" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "mls-not-enabled", + "message": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "mls-not-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + } + }, + "summary": "Get public keys used by the backend to sign external proposals" + } + }, + "/notifications": { + "get": { + "description": " [internal route ID: \"get-notifications\"]\n\n", + "parameters": [ + { + "description": "Only return notifications more recent than this", + "in": "query", + "name": "since", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Maximum number of notifications to return", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 10000, + "minimum": 100, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList" + } + } + }, + "description": "Notification list" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch notifications" + } + }, + "/notifications/last": { + "get": { + "description": " [internal route ID: \"get-last-notification\"]\n\n", + "parameters": [ + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + } + }, + "description": "Notification found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch the last notification" + } + }, + "/notifications/{id}": { + "get": { + "description": " [internal route ID: \"get-notification-by-id\"]\n\n", + "parameters": [ + { + "description": "Notification ID", + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Only return notifications targeted at the given client", + "in": "query", + "name": "client", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotification" + } + } + }, + "description": "Notification found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Some notifications not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`id` or Some notifications not found (label: `not-found`)" + } + }, + "summary": "Fetch a notification by ID" + } + }, + "/oauth/applications": { + "get": { + "description": " [internal route ID: \"get-oauth-applications\"]\n\nGet all OAuth applications with active account access for a user.", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/OAuthApplication" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/OAuthApplication" + }, + "type": "array" + } + } + }, + "description": "OAuth applications found" + } + }, + "summary": "Get OAuth applications with account access" + } + }, + "/oauth/applications/{OAuthClientId}": { + "delete": { + "description": " [internal route ID: \"revoke-oauth-account-access\"]\n\n", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "OAuth application access revoked" + } + }, + "summary": "Revoke account access from an OAuth application" + } + }, + "/oauth/authorization/codes": { + "post": { + "description": " [internal route ID: \"create-oauth-auth-code\"]\n\nCurrently only supports the 'code' response type, which corresponds to the authorization code flow.", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateOAuthAuthorizationCodeRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 400, + "label": "redirect-url-miss-match", + "message": "The redirect URL does not match the one registered with the client" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "redirect-url-miss-match" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "redirect-url-miss-match", + "message": "The redirect URL does not match the one registered with the client" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "redirect-url-miss-match" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Bad Request\n\nThe redirect URL does not match the one registered with the client (label: `redirect-url-miss-match`) or `body`", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "description": "Forbidden", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + } + }, + "summary": "Create an OAuth authorization code" + } + }, + "/oauth/clients/{OAuthClientId}": { + "get": { + "description": " [internal route ID: \"get-oauth-client\"]\n\n", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClient" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthClient" + } + } + }, + "description": "OAuth client found" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "OAuth is disabled" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth is disabled (label: `forbidden`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`OAuthClientId` or OAuth client not found (label: `not-found`)\n\nOAuth client not found (label: `not-found`)" + } + }, + "summary": "Get OAuth client information" + } + }, + "/oauth/revoke": { + "post": { + "description": " [internal route ID: \"revoke-oauth-refresh-token\"]\n\nRevoke an access token.", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthRevokeRefreshTokenRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "forbidden", + "message": "Invalid refresh token" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid refresh token (label: `forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth client not found (label: `not-found`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Internal error while handling JWT token (label: `jwt-error`)" + } + }, + "summary": "Revoke an OAuth refresh token" + } + }, + "/oauth/token": { + "post": { + "description": " [internal route ID: \"create-oauth-access-token\"]\n\nObtain a new access token from an authorization code or a refresh token.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Either_OAuthAccessTokenRequest_OAuthRefreshAccessTokenRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OAuthAccessTokenResponse" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid_grant", + "message": "Invalid grant" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid_grant", + "forbidden" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid grant (label: `invalid_grant`)\n\nInvalid client credentials (label: `forbidden`)\n\nInvalid grant type (label: `forbidden`)\n\nInvalid refresh token (label: `forbidden`)\n\nOAuth is disabled (label: `forbidden`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "OAuth client not found (label: `not-found`)\n\nOAuth authorization code not found (label: `not-found`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "jwt-error", + "message": "Internal error while handling JWT token" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "jwt-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Internal error while handling JWT token (label: `jwt-error`)" + } + }, + "summary": "Create an OAuth access token" + } + }, + "/onboarding/v3": { + "post": { + "deprecated": true, + "description": " [internal route ID: \"onboarding\"]\n\nDEPRECATED: the feature has been turned off, the end-point does nothing and always returns '{\"results\":[],\"auto-connects\":[]}'.", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Body" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeprecatedMatchingResult" + } + } + }, + "description": "" + } + }, + "summary": "Upload contacts and invoke matching." + } + }, + "/password-reset": { + "post": { + "description": " [internal route ID: \"post-password-reset\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewPasswordReset" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Password reset code created and sent by email." + } + }, + "summary": "Initiate a password reset." + } + }, + "/password-reset/complete": { + "post": { + "description": " [internal route ID: \"post-password-reset-complete\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CompletePasswordReset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + } + }, + "summary": "Complete a password reset." + } + }, + "/password-reset/{key}": { + "post": { + "deprecated": true, + "description": " [internal route ID: \"post-password-reset-key-deprecated\"]\n\nDEPRECATED: Use 'POST /password-reset/complete'.", + "parameters": [ + { + "description": "An opaque key for a pending password reset.", + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password reset successful." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "invalid-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ", + "code-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)\n\nA password reset is already in progress. (label: `code-exists`)" + } + }, + "summary": "Complete a password reset." + } + }, + "/properties": { + "delete": { + "description": " [internal route ID: \"clear-properties\"]\n\n", + "responses": { + "200": { + "description": "Properties cleared" + } + }, + "summary": "Clear all properties" + }, + "get": { + "description": " [internal route ID: \"list-property-keys\"]\n\n", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/ASCII" + }, + "type": "array" + } + } + }, + "description": "List of property keys" + } + }, + "summary": "List all property keys" + } + }, + "/properties-values": { + "get": { + "description": " [internal route ID: \"list-properties\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyKeysAndValues" + } + } + }, + "description": "" + } + }, + "summary": "List all properties with key and value" + } + }, + "/properties/{key}": { + "delete": { + "description": " [internal route ID: \"delete-property\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Property deleted" + } + }, + "summary": "Delete a property" + }, + "get": { + "description": " [internal route ID: \"get-property\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + } + }, + "description": "The property value" + }, + "404": { + "description": "`key` or Property not found(**Note**: This error has an empty body for legacy reasons)" + } + }, + "summary": "Get a property value" + }, + "put": { + "description": " [internal route ID: \"set-property\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "format": "printable", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PropertyValue" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Property set" + } + }, + "summary": "Set a user property" + } + }, + "/provider": { + "delete": { + "description": " [internal route ID: \"provider-delete\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteProvider" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Delete a provider" + }, + "get": { + "description": " [internal route ID: \"provider-get-account\"]\n\n", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Provider not found. (label: `not-found`)\n\nProvider not found. (label: `not-found`)" + } + }, + "summary": "Get account" + }, + "put": { + "description": " [internal route ID: \"provider-update\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateProvider" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-provider", + "message": "The provider does not exist." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Update a provider" + } + }, + "/provider/activate": { + "get": { + "description": " [internal route ID: \"provider-activate\"]\n\n", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderActivationResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProviderActivationResponse" + } + } + }, + "description": "" + }, + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-code", + "message": "Invalid verification code" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Activate a provider" + } + }, + "/provider/assets": { + "post": { + "requestBody": { + "content": { + "multipart/mixed": { + "schema": { + "$ref": "#/components/schemas/AssetSource" + } + } + }, + "description": "A body with content type `multipart/mixed body`. The first section's content type should be `application/json`. The second section's content type should be always be `application/octet-stream`. Other content types will be ignored by the server." + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + }, + "description": "Asset posted", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-length", + "message": "Invalid content length" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-length" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid content length (label: `invalid-length`)" + }, + "413": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "client-error", + "message": "Asset too large" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "client-error" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Asset too large (label: `client-error`)" + } + }, + "summary": "Upload an asset" + } + }, + "/provider/assets/{key}": { + "delete": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Asset deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorised", + "message": "Unauthorised operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorised" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorised operation (label: `unauthorised`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` not found\n\nAsset not found (label: `not-found`)" + } + }, + "summary": "Delete an asset" + }, + "get": { + "parameters": [ + { + "in": "path", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "Asset-Token", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "asset_token", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "302": { + "description": "Asset found", + "headers": { + "Location": { + "description": "Asset location", + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Asset not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`key` or Asset not found (label: `not-found`)" + } + }, + "summary": "Download an asset" + } + }, + "/provider/email": { + "put": { + "description": " [internal route ID: \"provider-update-email\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-provider", + "message": "The provider does not exist." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-provider", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The provider does not exist. (label: `invalid-provider`)\n\nAccess denied. (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Update a provider email" + } + }, + "/provider/login": { + "post": { + "description": " [internal route ID: \"provider-login\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ProviderLogin" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "headers": { + "Set-Cookie": { + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + } + }, + "summary": "Login as a provider" + } + }, + "/provider/password": { + "put": { + "description": " [internal route ID: \"provider-update-password\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordChange" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Update a provider password" + } + }, + "/provider/password-reset": { + "post": { + "description": " [internal route ID: \"provider-password-reset\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReset" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code", + "invalid-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)\n\nInvalid email or mobile number for password reset. (label: `invalid-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ", + "code-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)\n\nA password reset is already in progress. (label: `code-exists`)\n\nA password reset is already in progress. (label: `code-exists`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Begin a password reset" + } + }, + "/provider/password-reset/complete": { + "post": { + "description": " [internal route ID: \"provider-password-reset-complete\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CompletePasswordReset" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-code", + "message": "Invalid password reset code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid password reset code. (label: `invalid-code`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "invalid-code", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)\n\nAccess denied. (label: `access-denied`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password reset, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password reset, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Complete a password reset" + } + }, + "/provider/register": { + "post": { + "description": " [internal route ID: \"provider-register\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewProvider" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewProviderResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewProviderResponse" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "429": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 429, + "label": "too-many-requests", + "message": "Too many request to generate a verification code." + }, + "properties": { + "code": { + "enum": [ + 429 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-requests" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many request to generate a verification code. (label: `too-many-requests`)" + } + }, + "summary": "Register a new provider" + } + }, + "/provider/services": { + "get": { + "description": " [internal route ID: \"get-provider-services\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Service" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List provider services" + }, + "post": { + "description": " [internal route ID: \"post-provider-services\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewService" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewServiceResponse" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewServiceResponse" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-service-key", + "message": "Invalid service key." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-service-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Create a new service" + } + }, + "/provider/services/{service-id}": { + "delete": { + "description": " [internal route ID: \"delete-provider-services-by-service-id\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteService" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Delete service" + }, + "get": { + "description": " [internal route ID: \"get-provider-services-by-service-id\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Service" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Get provider service by service id" + }, + "put": { + "description": " [internal route ID: \"put-provider-services-by-service-id\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateService" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Provider service updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nProvider not found. (label: `not-found`)\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Update provider service" + } + }, + "/provider/services/{service-id}/connection": { + "put": { + "description": " [internal route ID: \"put-provider-services-connection-by-service-id\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateServiceConn" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Provider service connection updated" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-service-key", + "message": "Invalid service key." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-service-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid service key. (label: `invalid-service-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nAccess denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Update provider service connection" + } + }, + "/providers/{pid}": { + "get": { + "description": " [internal route ID: \"provider-get-profile\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "pid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Provider" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Provider not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`pid` or Provider not found. (label: `not-found`)" + } + }, + "summary": "Get profile" + } + }, + "/providers/{provider-id}/services": { + "get": { + "description": " [internal route ID: \"get-provider-services-by-provider-id\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "provider-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/ServiceProfile" + }, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "Get provider services by provider id" + } + }, + "/providers/{provider-id}/services/{service-id}": { + "get": { + "description": " [internal route ID: \"get-provider-services-by-provider-id-and-service-id\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "provider-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "service-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfile" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Service not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`provider-id` or `service-id` not found\n\nService not found. (label: `not-found`)" + } + }, + "summary": "Get provider service by provider id and service id" + } + }, + "/proxy/giphy/v1/gifs": {}, + "/proxy/googlemaps/api/staticmap": {}, + "/proxy/googlemaps/maps/api/geocode": {}, + "/proxy/youtube/v3": {}, + "/push/tokens": { + "get": { + "description": " [internal route ID: \"get-push-tokens\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushTokenList" + } + } + }, + "description": "" + } + }, + "summary": "List the user's registered push tokens" + }, + "post": { + "description": " [internal route ID: \"register-push-token\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushToken" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PushToken" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PushToken" + } + } + }, + "description": "Push token registered", + "headers": { + "Location": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 400, + "label": "apns-voip-not-supported", + "message": "Adding APNS_VOIP tokens is not supported" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "apns-voip-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "apns-voip-not-supported", + "message": "Adding APNS_VOIP tokens is not supported" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "apns-voip-not-supported" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Adding APNS_VOIP tokens is not supported (label: `apns-voip-not-supported`) or `body`" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "app-not-found", + "message": "App does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "app-not-found", + "invalid-token" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "app-not-found", + "message": "App does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "app-not-found", + "invalid-token" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "App does not exist (label: `app-not-found`)\n\nInvalid push token (label: `invalid-token`)" + }, + "413": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 413, + "label": "sns-thread-budget-reached", + "message": "Too many concurrent calls to SNS; is SNS down?" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "sns-thread-budget-reached", + "token-too-long", + "metadata-too-long" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 413, + "label": "sns-thread-budget-reached", + "message": "Too many concurrent calls to SNS; is SNS down?" + }, + "properties": { + "code": { + "enum": [ + 413 + ], + "type": "integer" + }, + "label": { + "enum": [ + "sns-thread-budget-reached", + "token-too-long", + "metadata-too-long" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Too many concurrent calls to SNS; is SNS down? (label: `sns-thread-budget-reached`)\n\nPush token length must be < 8192 for GCM or 400 for APNS (label: `token-too-long`)\n\nTried to add token to endpoint resulting in metadata length > 2048 (label: `metadata-too-long`)" + } + }, + "summary": "Register a native push token" + } + }, + "/push/tokens/{pid}": { + "delete": { + "description": " [internal route ID: \"delete-push-token\"]\n\n", + "parameters": [ + { + "description": "The push token to delete", + "in": "path", + "name": "pid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Push token unregistered" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Push token not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Push token not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`pid` or Push token not found (label: `not-found`)" + } + }, + "summary": "Unregister a native push token" + } + }, + "/register": { + "post": { + "description": " [internal route ID: \"register\"]\n\nIf the environment where the registration takes place is private and a registered email address is not whitelisted, a 403 error is returned.Calls federation service brig on send-connection-action", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewUser" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "description": "User created and pending activation", + "headers": { + "Location": { + "description": "UserId", + "schema": { + "format": "uuid", + "type": "string" + } + }, + "Set-Cookie": { + "description": "Cookie", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email", + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code", + "invalid-email", + "invalid-phone" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid invitation code. (label: `invalid-invitation-code`)\n\nInvalid e-mail address. (label: `invalid-email`)\n\nInvalid mobile phone number (label: `invalid-phone`) or `body`" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "unauthorized", + "message": "Unauthorized e-mail address" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "unauthorized", + "missing-identity", + "blacklisted-email", + "too-many-team-members", + "user-creation-restricted" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Unauthorized e-mail address (label: `unauthorized`)\n\nUsing an invitation code requires registering the given email. (label: `missing-identity`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nToo many members in this team. (label: `too-many-team-members`)\n\nThis instance does not allow creation of personal users or teams. (label: `user-creation-restricted`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "User does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "invalid-code", + "message": "User does not exist" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "User does not exist (label: `invalid-code`)\n\nInvalid activation code (label: `invalid-code`)" + }, + "409": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "key-exists", + "message": "The given e-mail address is in use." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "key-exists" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given e-mail address is in use. (label: `key-exists`)" + } + }, + "summary": "Register a new user." + } + }, + "/scim/auth-tokens": { + "delete": { + "parameters": [ + { + "in": "query", + "name": "id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)" + } + } + }, + "get": { + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ScimTokenList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)" + } + } + }, + "post": { + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateScimToken" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CreateScimTokenResponse" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Code authentication is required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "password-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Code authentication is required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nPassword authentication failed. (label: `password-authentication-failed`)" + } + } + } + }, + "/search/contacts": { + "get": { + "description": " [internal route ID: \"search-contacts\"]\n\nCalls federation service brig on search-users
Calls federation service brig on get-users-by-ids", + "parameters": [ + { + "description": "Search query", + "in": "query", + "name": "q", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Searched domain. Note: This is optional only for backwards compatibility, future versions will mandate this.", + "in": "query", + "name": "domain", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Number of results to return (min: 1, max: 500, default 15)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchResult" + } + } + }, + "description": "" + } + }, + "summary": "Search for users" + } + }, + "/self": { + "delete": { + "description": " [internal route ID: \"delete-self\"]\n\nif the account has a verified identity, a verification code is sent and needs to be confirmed to authorise the deletion. if the account has no verified identity but a password, it must be provided. if password is correct, or if neither a verified identity nor a password exists, account deletion is scheduled immediately.Calls federation service brig on send-connection-action", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeleteUser" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Deletion is initiated." + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletionCodeTimeout" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DeletionCodeTimeout" + } + } + }, + "description": "Deletion is pending verification with a code." + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-user", + "message": "Invalid user" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-user" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid user (label: `invalid-user`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-self-delete-for-team-owner", + "message": "Team owners are not allowed to delete themselves; ask a fellow owner" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-self-delete-for-team-owner", + "pending-delete", + "missing-auth", + "invalid-credentials", + "invalid-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team owners are not allowed to delete themselves; ask a fellow owner (label: `no-self-delete-for-team-owner`)\n\nA verification code for account deletion is still pending (label: `pending-delete`)\n\nRe-authentication via password required (label: `missing-auth`)\n\nAuthentication failed (label: `invalid-credentials`)\n\nInvalid verification code (label: `invalid-code`)" + } + }, + "summary": "Initiate account deletion." + }, + "get": { + "description": " [internal route ID: \"get-self\"]\n\n\nOAuth scope: `read:self`", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + }, + "description": "" + } + }, + "summary": "Get your own profile" + }, + "put": { + "description": " [internal route ID: \"put-self\"]\n\nCalls federation service brig on send-connection-action", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User updated" + } + }, + "summary": "Update your profile." + } + }, + "/self/email": { + "delete": { + "description": " [internal route ID: \"remove-email\"]\n\nYour email address can only be removed if you also have a phone number.Calls federation service brig on send-connection-action", + "responses": { + "200": { + "description": "Identity Removed" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-password", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "last-identity", + "message": "The last user identity cannot be removed." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "last-identity", + "no-password", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The last user identity cannot be removed. (label: `last-identity`)\n\nThe user has no password. (label: `no-password`)\n\nThe user has no verified email (label: `no-identity`)" + } + }, + "summary": "Remove your email address." + } + }, + "/self/handle": { + "put": { + "description": " [internal route ID: \"change-handle\"]\n\nCalls federation service brig on send-connection-action
Calls federation service brig on send-connection-action", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/HandleUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Handle Changed" + } + }, + "summary": "Change your handle." + } + }, + "/self/locale": { + "put": { + "description": " [internal route ID: \"change-locale\"]\n\nCalls federation service brig on send-connection-action", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LocaleUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Local Changed" + } + }, + "summary": "Change your locale." + } + }, + "/self/password": { + "head": { + "description": " [internal route ID: \"check-password-exists\"]\n\n", + "responses": { + "200": { + "description": "Password is set" + }, + "404": { + "description": "Password is not set" + } + }, + "summary": "Check that your password is set." + }, + "put": { + "description": " [internal route ID: \"change-password\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordChange" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Password Changed" + }, + "403": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-credentials", + "message": "Authentication failed" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-credentials", + "no-identity" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Authentication failed (label: `invalid-credentials`)\n\nThe user has no verified email (label: `no-identity`)" + }, + "409": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password change, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "password-must-differ", + "message": "For password change, new and old password must be different." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "password-must-differ" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "For password change, new and old password must be different. (label: `password-must-differ`)" + } + }, + "summary": "Change your password." + } + }, + "/self/supported-protocols": { + "put": { + "description": " [internal route ID: \"change-supported-protocols\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SupportedProtocolUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Supported protocols changed" + } + }, + "summary": "Change your supported protocols" + } + }, + "/services": { + "get": { + "description": " [internal route ID: \"get-services\"]\n\n", + "parameters": [ + { + "in": "query", + "name": "tags", + "required": false, + "schema": { + "enum": [ + "audio", + "books", + "business", + "design", + "education", + "entertainment", + "finance", + "fitness", + "food-drink", + "games", + "graphics", + "health", + "integration", + "lifestyle", + "media", + "medical", + "movies", + "music", + "news", + "photography", + "poll", + "productivity", + "quiz", + "rating", + "shopping", + "social", + "sports", + "travel", + "tutorial", + "video", + "weather" + ], + "type": "string" + } + }, + { + "in": "query", + "name": "start", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 100, + "minimum": 10, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfile" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + } + }, + "summary": "List services" + } + }, + "/sso/finalize-login": { + "post": { + "deprecated": true, + "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/finalize-login/{team}": { + "post": { + "parameters": [ + { + "in": "path", + "name": "team", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/initiate-login/{idp}": { + "get": { + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "idp", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/FormRedirect" + } + } + }, + "description": "" + } + } + }, + "head": { + "parameters": [ + { + "in": "query", + "name": "success_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "error_redirect", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "idp", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain;charset=utf-8": {} + }, + "description": "" + } + } + } + }, + "/sso/metadata": { + "get": { + "deprecated": true, + "description": "DEPRECATED! use /sso/metadata/:tid instead! Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams", + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/metadata/{team}": { + "get": { + "parameters": [ + { + "in": "path", + "name": "team", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/xml": { + "schema": { + "type": "string" + } + } + }, + "description": "" + } + } + } + }, + "/sso/settings": { + "get": { + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SsoSettings" + } + } + }, + "description": "" + } + } + } + }, + "/system/settings": { + "get": { + "description": " [internal route ID: \"get-system-settings\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SystemSettings" + } + } + }, + "description": "" + } + }, + "summary": "Returns a curated set of system configuration settings for authorized users." + } + }, + "/system/settings/unauthorized": { + "get": { + "description": " [internal route ID: \"get-system-settings-unauthorized\"]\n\n", + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SystemSettingsPublic" + } + } + }, + "description": "" + } + }, + "summary": "Returns a curated set of system configuration settings." + } + }, + "/teams/invitations/by-email": { + "head": { + "description": " [internal route ID: \"head-team-invitations\"]\n\n", + "parameters": [ + { + "description": "Email address", + "in": "query", + "name": "email", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Pending invitation exists." + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "No pending invitations exists." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "No pending invitations exists." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "No pending invitations exists. (label: `not-found`)" + }, + "409": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 409, + "label": "conflicting-invitations", + "message": "Multiple conflicting invitations to different teams exists." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "conflicting-invitations" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "conflicting-invitations", + "message": "Multiple conflicting invitations to different teams exists." + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "conflicting-invitations" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Multiple conflicting invitations to different teams exists. (label: `conflicting-invitations`)" + } + }, + "summary": "Check if there is an invitation pending given an email address." + } + }, + "/teams/invitations/info": { + "get": { + "description": " [internal route ID: \"get-team-invitation-info\"]\n\n", + "parameters": [ + { + "description": "Invitation code", + "in": "query", + "name": "code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + } + }, + "description": "Invitation info" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `code`\n\nInvalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get invitation info given a code." + } + }, + "/teams/notifications": { + "get": { + "description": " [internal route ID: \"get-team-notifications\"]\n\nThis is a work-around for scalability issues with gundeck user event fan-out. It does not track all team-wide events, but only `member-join`.\nNote that `/teams/notifications` behaves differently from `/notifications`:\n- If there is a gap between the notification id requested with `since` and the available data, team queues respond with 200 and the data that could be found. They do NOT respond with status 404, but valid data in the body.\n- The notification with the id given via `since` is included in the response if it exists. You should remove this and only use it to decide whether there was a gap between your last request and this one.\n- If the notification id does *not* exist, you get the more recent events from the queue (instead of all of them). This can be done because a notification id is a UUIDv1, which is essentially a time stamp.\n- There is no corresponding `/last` end-point to get only the most recent event. That end-point was only useful to avoid having to pull the entire queue. In team queues, if you have never requested the queue before and have no prior notification id, just pull with timestamp 'now'.", + "parameters": [ + { + "description": "Notification id to start with in the response (UUIDv1)", + "in": "query", + "name": "since", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum number of events to return (1..10000; default: 1000)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 10000, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QueuedNotificationList" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-notification-id", + "message": "Could not parse notification id (must be UUIDv1)." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-notification-id" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `size` or `since`\n\nCould not parse notification id (must be UUIDv1). (label: `invalid-notification-id`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Team not found (label: `no-team`)" + } + }, + "summary": "Read recently added team members from team queue" + } + }, + "/teams/{team-id}/services/whitelist": { + "post": { + "description": " [internal route ID: \"post-team-whitelist-by-team-id\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "team-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UpdateServiceWhitelist" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "UpdateServiceWhitelistRespChanged" + }, + "204": { + "description": "UpdateServiceWhitelistRespUnchanged" + } + }, + "summary": "Update service whitelist" + } + }, + "/teams/{team-id}/services/whitelisted": { + "get": { + "description": " [internal route ID: \"get-whitelisted-services-by-team-id\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "team-id", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "query", + "name": "prefix", + "required": false, + "schema": { + "maxLength": 1, + "minLength": 128, + "type": "string" + } + }, + { + "in": "query", + "name": "filter_disabled", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 100, + "minimum": 10, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ServiceProfile" + } + } + }, + "description": "" + } + }, + "summary": "Get whitelisted services by team id" + } + }, + "/teams/{tid}": { + "delete": { + "description": " [internal route ID: \"delete-team\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamDeleteData" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Team is scheduled for removal" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "code-authentication-required", + "message": "Verification code required" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "code-authentication-required", + "code-authentication-failed", + "access-denied", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Verification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (missing DeleteTeam) (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + }, + "503": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 503, + "label": "queue-full", + "message": "The delete queue is full; no further delete requests can be processed at the moment" + }, + "properties": { + "code": { + "enum": [ + 503 + ], + "type": "integer" + }, + "label": { + "enum": [ + "queue-full" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The delete queue is full; no further delete requests can be processed at the moment (label: `queue-full`)" + } + }, + "summary": "Delete a team" + }, + "get": { + "description": " [internal route ID: \"get-team\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Team" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get a team by ID" + }, + "put": { + "description": " [internal route ID: \"update-team\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamUpdateData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Team updated" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions (missing SetTeamData)" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (missing SetTeamData) (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Update team properties" + } + }, + "/teams/{tid}/conversations": { + "get": { + "description": " [internal route ID: \"get-team-conversations\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamConversationList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + } + }, + "summary": "Get team conversations" + } + }, + "/teams/{tid}/conversations/roles": { + "get": { + "description": " [internal route ID: \"get-team-conversation-roles\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConversationRolesList" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get existing roles available for the given team" + } + }, + "/teams/{tid}/conversations/{cid}": { + "delete": { + "description": " [internal route ID: \"delete-team-conversation\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Conversation deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing delete_conversation) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Remove a team conversation" + }, + "get": { + "description": " [internal route ID: \"get-team-conversation\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "cid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamConversation" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-conversation", + "message": "Conversation not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-conversation" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `cid` not found\n\nConversation not found (label: `no-conversation`)" + } + }, + "summary": "Get one team conversation" + } + }, + "/teams/{tid}/features": { + "get": { + "description": " [internal route ID: \"get-all-feature-configs-for-team\"]\n\nGets feature configs for a team. User must be a member of the team and have permission to view team features.", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AllFeatureConfigs" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Gets feature configs for a team" + } + }, + "/teams/{tid}/features/appLock": { + "get": { + "description": " [internal route ID: (\"get\", AppLockConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AppLockConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for appLock" + }, + "put": { + "description": " [internal route ID: (\"put\", AppLockConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AppLockConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/AppLockConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for appLock" + } + }, + "/teams/{tid}/features/classifiedDomains": { + "get": { + "description": " [internal route ID: (\"get\", ClassifiedDomainsConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClassifiedDomainsConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for classifiedDomains" + } + }, + "/teams/{tid}/features/conferenceCalling": { + "get": { + "description": " [internal route ID: (\"get\", ConferenceCallingConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConferenceCallingConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for conferenceCalling" + } + }, + "/teams/{tid}/features/conversationGuestLinks": { + "get": { + "description": " [internal route ID: (\"get\", GuestLinksConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for conversationGuestLinks" + }, + "put": { + "description": " [internal route ID: (\"put\", GuestLinksConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/GuestLinksConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for conversationGuestLinks" + } + }, + "/teams/{tid}/features/digitalSignatures": { + "get": { + "description": " [internal route ID: (\"get\", DigitalSignaturesConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DigitalSignaturesConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for digitalSignatures" + } + }, + "/teams/{tid}/features/enforceFileDownloadLocation": { + "get": { + "description": " [internal route ID: (\"get\", EnforceFileDownloadLocationConfig)]\n\n

Custom feature: only supported for some decidated on-prem systems.

", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for enforceFileDownloadLocation" + }, + "put": { + "description": " [internal route ID: (\"put\", EnforceFileDownloadLocationConfig)]\n\n

Custom feature: only supported for some decidated on-prem systems.

", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EnforceFileDownloadLocation.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for enforceFileDownloadLocation" + } + }, + "/teams/{tid}/features/exposeInvitationURLsToTeamAdmin": { + "get": { + "description": " [internal route ID: (\"get\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for exposeInvitationURLsToTeamAdmin" + }, + "put": { + "description": " [internal route ID: (\"put\", ExposeInvitationURLsToTeamAdminConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for exposeInvitationURLsToTeamAdmin" + } + }, + "/teams/{tid}/features/fileSharing": { + "get": { + "description": " [internal route ID: (\"get\", FileSharingConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/FileSharingConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for fileSharing" + }, + "put": { + "description": " [internal route ID: (\"put\", FileSharingConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/FileSharingConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/FileSharingConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for fileSharing" + } + }, + "/teams/{tid}/features/legalhold": { + "get": { + "description": " [internal route ID: (\"get\", LegalholdConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LegalholdConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for legalhold" + }, + "put": { + "description": " [internal route ID: (\"put\", LegalholdConfig)]\n\nCalls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LegalholdConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LegalholdConfig.WithStatus" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "too-large-team-for-legalhold", + "code-authentication-required", + "code-authentication-failed", + "access-denied", + "action-denied", + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nCannot enable legalhold on large teams (reason: for removing LH from team, we need to iterate over all members, which is only supported for teams with less than 2k members) (label: `too-large-team-for-legalhold`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Put config for legalhold" + } + }, + "/teams/{tid}/features/limitedEventFanout": { + "get": { + "description": " [internal route ID: (\"get\", LimitedEventFanoutConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LimitedEventFanoutConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for limitedEventFanout" + } + }, + "/teams/{tid}/features/mls": { + "get": { + "description": " [internal route ID: (\"get\", MLSConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mls" + }, + "put": { + "description": " [internal route ID: (\"put\", MLSConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MLSConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mls" + } + }, + "/teams/{tid}/features/mlsE2EId": { + "get": { + "description": " [internal route ID: (\"get\", MlsE2EIdConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsE2EIdConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mlsE2EId" + }, + "put": { + "description": " [internal route ID: (\"put\", MlsE2EIdConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsE2EIdConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsE2EIdConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mlsE2EId" + } + }, + "/teams/{tid}/features/mlsMigration": { + "get": { + "description": " [internal route ID: (\"get\", MlsMigrationConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsMigration.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for mlsMigration" + }, + "put": { + "description": " [internal route ID: (\"put\", MlsMigrationConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsMigration.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/MlsMigration.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for mlsMigration" + } + }, + "/teams/{tid}/features/outlookCalIntegration": { + "get": { + "description": " [internal route ID: (\"get\", OutlookCalIntegrationConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for outlookCalIntegration" + }, + "put": { + "description": " [internal route ID: (\"put\", OutlookCalIntegrationConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for outlookCalIntegration" + } + }, + "/teams/{tid}/features/searchVisibility": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityAvailableConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for searchVisibility" + }, + "put": { + "description": " [internal route ID: (\"put\", SearchVisibilityAvailableConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for searchVisibility" + } + }, + "/teams/{tid}/features/searchVisibilityInbound": { + "get": { + "description": " [internal route ID: (\"get\", SearchVisibilityInboundConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for searchVisibilityInbound" + }, + "put": { + "description": " [internal route ID: (\"put\", SearchVisibilityInboundConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for searchVisibilityInbound" + } + }, + "/teams/{tid}/features/selfDeletingMessages": { + "get": { + "description": " [internal route ID: (\"get\", SelfDeletingMessagesConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for selfDeletingMessages" + }, + "put": { + "description": " [internal route ID: (\"put\", SelfDeletingMessagesConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for selfDeletingMessages" + } + }, + "/teams/{tid}/features/sndFactorPasswordChallenge": { + "get": { + "description": " [internal route ID: (\"get\", SndFactorPasswordChallengeConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for sndFactorPasswordChallenge" + }, + "put": { + "description": " [internal route ID: (\"put\", SndFactorPasswordChallengeConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.WithStatusNoLock" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for sndFactorPasswordChallenge" + } + }, + "/teams/{tid}/features/sso": { + "get": { + "description": " [internal route ID: (\"get\", SSOConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SSOConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for sso" + } + }, + "/teams/{tid}/features/validateSAMLemails": { + "get": { + "description": " [internal route ID: (\"get\", ValidateSAMLEmailsConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.WithStatus" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Get config for validateSAMLemails" + } + }, + "/teams/{tid}/get-members-by-ids-using-post": { + "post": { + "description": " [internal route ID: \"get-team-members-by-ids\"]\n\nThe `has_more` field in the response body is always `false`.", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum results to be returned", + "in": "query", + "name": "maxResults", + "required": false, + "schema": { + "format": "int32", + "maximum": 2000, + "minimum": 1, + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserIdList" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMemberList" + } + } + }, + "description": "" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "too-many-uids", + "message": "Can only process 2000 user ids per request." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "too-many-uids" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body` or `maxResults`\n\nCan only process 2000 user ids per request. (label: `too-many-uids`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get team members by user id list" + } + }, + "/teams/{tid}/invitations": { + "get": { + "description": " [internal route ID: \"get-team-invitations\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Invitation id to start from (ascending).", + "in": "query", + "name": "start", + "required": false, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Number of results to return (default 100, max 500).", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvitationList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InvitationList" + } + } + }, + "description": "List of sent invitations" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "List the sent team invitations" + }, + "post": { + "description": " [internal route ID: \"send-team-invitation\"]\n\nInvitations are sent by email. The maximum allowed number of pending team invitations is equal to the team size.", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/InvitationRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + } + }, + "description": "Invitation was created and sent.", + "headers": { + "Location": { + "schema": { + "format": "url", + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-email", + "message": "Invalid e-mail address." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nInvalid e-mail address. (label: `invalid-email`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions", + "too-many-team-invitations", + "blacklisted-email", + "no-identity", + "no-email" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)\n\nToo many team invitations for this team (label: `too-many-team-invitations`)\n\nThe given e-mail address has been blacklisted due to a permanent bounce or a complaint. (label: `blacklisted-email`)\n\nThe user has no verified email (label: `no-identity`)\n\nThis operation requires the user to have a verified email address. (label: `no-email`)" + } + }, + "summary": "Create and send a new team invitation." + } + }, + "/teams/{tid}/invitations/{iid}": { + "delete": { + "description": " [internal route ID: \"delete-team-invitation\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "iid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Invitation deleted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Delete a pending team invitation by ID." + }, + "get": { + "description": " [internal route ID: \"get-team-invitation\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "iid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/Invitation" + } + } + }, + "description": "Invitation" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Notification not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Notification not found." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `iid` or Notification not found. (label: `not-found`)" + } + }, + "summary": "Get a pending team invitation by ID." + } + }, + "/teams/{tid}/legalhold/consent": { + "post": { + "description": " [internal route ID: \"consent-to-legal-hold\"]\n\nCalls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Grant consent successful" + }, + "204": { + "description": "Consent already granted" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "invalid-op", + "message": "Invalid operation" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-op", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Consent to legal hold" + } + }, + "/teams/{tid}/legalhold/settings": { + "delete": { + "description": " [internal route ID: \"delete-legal-hold-settings\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to members with a legalhold client (via brig)\n- UserLegalHoldDisabled event to contacts of members with a legalhold client (via brig)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RemoveLegalHoldSettingsRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Legal hold service settings deleted" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-disable-unimplemented", + "message": "legal hold cannot be disabled for whitelisted teams" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-disable-unimplemented", + "legalhold-not-enabled", + "invalid-op", + "action-denied", + "no-team-member", + "operation-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInvalid operation (label: `invalid-op`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Delete legal hold service settings" + }, + "get": { + "description": " [internal route ID: \"get-legal-hold-settings\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get legal hold service settings" + }, + "post": { + "description": " [internal route ID: \"create-legal-hold-settings\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewLegalHoldService" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ViewLegalHoldService" + } + } + }, + "description": "Legal hold service settings created" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-status-bad", + "message": "legal hold service: invalid response" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-status-bad", + "legalhold-invalid-key" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)\n\nlegal hold service pubkey is invalid (label: `legalhold-invalid-key`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Create legal hold service settings" + } + }, + "/teams/{tid}/legalhold/{uid}": { + "delete": { + "description": " [internal route ID: \"disable-legal-hold-for-user\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientRemoved event to the user owning the client (via brig)\n- UserLegalHoldDisabled event to contacts of the user owning the client (via brig)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/DisableLegalHoldForUserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Disable legal hold successful" + }, + "204": { + "description": "Legal hold was not enabled" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "action-denied", + "code-authentication-required", + "code-authentication-failed", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Disable legal hold for user" + }, + "get": { + "description": " [internal route ID: \"get-legal-hold\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserLegalHoldStatusResponse" + } + } + }, + "description": "" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + } + }, + "summary": "Get legal hold status" + }, + "post": { + "description": " [internal route ID: \"request-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- LegalHoldClientRequested event to contacts of the user the device is requested for, if they didn't already have a legalhold client (via brig)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Request device successful" + }, + "204": { + "description": "Request device already pending" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered", + "legalhold-status-bad" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service has not been registered for this team (label: `legalhold-not-registered`)\n\nlegal hold service: invalid response (label: `legalhold-status-bad`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "operation-denied", + "no-team-member", + "action-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "legalhold-no-consent", + "message": "user has not given consent to using legal hold" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-no-consent", + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "user has not given consent to using legal hold (label: `legalhold-no-consent`)\n\nlegal hold is already enabled for this user (label: `legalhold-already-enabled`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-illegal-op", + "message": "internal server error: inconsistent change of user's legalhold state" + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-illegal-op", + "legalhold-internal" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "internal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)\n\nlegal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)" + } + }, + "summary": "Request legal hold device" + } + }, + "/teams/{tid}/legalhold/{uid}/approve": { + "put": { + "description": " [internal route ID: \"approve-legal-hold-device\"]\n\nThis endpoint can lead to the following events being sent:\n- ClientAdded event to the user owning the client (via brig)\n- UserLegalHoldEnabled event to contacts of the user owning the client (via brig)\n- ClientRemoved event to the user, if removing old client due to max number (via brig)
Calls federation service brig on get-users-by-ids
Calls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ApproveLegalHoldForUserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Legal hold approved" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "legalhold-not-registered", + "message": "legal hold service has not been registered for this team" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-registered" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid `body`\n\nlegal hold service has not been registered for this team (label: `legalhold-not-registered`)" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "legalhold-not-enabled", + "message": "legal hold is not enabled for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-enabled", + "no-team-member", + "action-denied", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "legalhold-no-device-allocated", + "message": "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow." + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-no-device-allocated" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nno legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow. (label: `legalhold-no-device-allocated`)" + }, + "409": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 409, + "label": "legalhold-already-enabled", + "message": "legal hold is already enabled for this user" + }, + "properties": { + "code": { + "enum": [ + 409 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-already-enabled" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold is already enabled for this user (label: `legalhold-already-enabled`)" + }, + "412": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 412, + "label": "legalhold-not-pending", + "message": "legal hold cannot be approved without being in a pending state" + }, + "properties": { + "code": { + "enum": [ + 412 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-not-pending" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold cannot be approved without being in a pending state (label: `legalhold-not-pending`)" + }, + "500": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 500, + "label": "legalhold-internal", + "message": "legal hold service: could not block connections when resolving policy conflicts." + }, + "properties": { + "code": { + "enum": [ + 500 + ], + "type": "integer" + }, + "label": { + "enum": [ + "legalhold-internal", + "legalhold-illegal-op" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "legal hold service: could not block connections when resolving policy conflicts. (label: `legalhold-internal`)\n\ninternal server error: inconsistent change of user's legalhold state (label: `legalhold-illegal-op`)" + } + }, + "summary": "Approve legal hold device" + } + }, + "/teams/{tid}/members": { + "get": { + "description": " [internal route ID: \"get-team-members\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Maximum results to be returned", + "in": "query", + "name": "maxResults", + "required": false, + "schema": { + "format": "int32", + "maximum": 2000, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Optional, when not specified, the first page will be returned.Every returned page contains a `pagingState`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMembersPage" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Get team members" + }, + "put": { + "description": " [internal route ID: \"update-team-member\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/NewTeamMember" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "too-many-team-admins", + "invalid-permissions", + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nMaximum number of admins per team reached (label: `too-many-team-admins`)\n\nThe specified permissions are invalid (label: `invalid-permissions`)\n\nYou do not have permission to access this resource (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam member not found (label: `no-team-member`)\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Update an existing team member" + } + }, + "/teams/{tid}/members/csv": { + "get": { + "description": " [internal route ID: \"get-team-members-csv\"]\n\nThe endpoint returns data in chunked transfer encoding. Internal server errors might result in a failed transfer instead of a 500 response.", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/csv": {} + }, + "description": "CSV of team members" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "You do not have permission to access this resource" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "You do not have permission to access this resource (label: `access-denied`)" + } + }, + "summary": "Get all members of the team as a CSV file" + } + }, + "/teams/{tid}/members/{uid}": { + "delete": { + "description": " [internal route ID: \"delete-team-member\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMemberDeleteData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "" + }, + "202": { + "description": "Team member scheduled for deletion" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member", + "access-denied", + "code-authentication-required", + "code-authentication-failed" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nYou do not have permission to access this resource (label: `access-denied`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam not found (label: `no-team`)\n\nTeam member not found (label: `no-team-member`)" + } + }, + "summary": "Remove an existing team member" + }, + "get": { + "description": " [internal route ID: \"get-team-member\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamMember" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team-member", + "message": "Team member not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` or `uid` not found\n\nTeam member not found (label: `no-team-member`)" + } + }, + "summary": "Get single team member" + } + }, + "/teams/{tid}/search": { + "get": { + "description": " [internal route ID: \"browse-team\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "Search expression", + "in": "query", + "name": "q", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Role filter, eg. `member,partner`. Empty list means do not filter.", + "in": "query", + "name": "frole", + "required": false, + "schema": { + "items": { + "enum": [ + "owner", + "admin", + "member", + "partner" + ], + "type": "string" + }, + "type": "array" + } + }, + { + "description": "Can be one of name, handle, email, saml_idp, managed_by, role, created_at.", + "in": "query", + "name": "sortby", + "required": false, + "schema": { + "enum": [ + "name", + "handle", + "email", + "saml_idp", + "managed_by", + "role", + "created_at" + ], + "type": "string" + } + }, + { + "description": "Can be one of asc, desc.", + "in": "query", + "name": "sortorder", + "required": false, + "schema": { + "enum": [ + "asc", + "desc" + ], + "type": "string" + } + }, + { + "description": "Number of results to return (min: 1, max: 500, default: 15)", + "in": "query", + "name": "size", + "required": false, + "schema": { + "format": "int32", + "maximum": 500, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Optional, when not specified, the first page will be returned. Every returned page contains a `paging_state`, this should be supplied to retrieve the next page.", + "in": "query", + "name": "pagingState", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchResult" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SearchResult" + } + } + }, + "description": "Search results" + } + }, + "summary": "Browse team for members (requires add-user permission)" + } + }, + "/teams/{tid}/search-visibility": { + "get": { + "description": " [internal route ID: \"get-search-visibility\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSearchVisibilityView" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "operation-denied", + "message": "Insufficient permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + } + }, + "summary": "Shows the value for search visibility" + }, + "put": { + "description": " [internal route ID: \"set-search-visibility\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSearchVisibilityView" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Search visibility set" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "team-search-visibility-not-enabled", + "message": "Custom search is not available for this team" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "team-search-visibility-not-enabled", + "operation-denied", + "no-team-member" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Custom search is not available for this team (label: `team-search-visibility-not-enabled`)\n\nInsufficient permissions (label: `operation-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Sets the search visibility for the whole team" + } + }, + "/teams/{tid}/size": { + "get": { + "description": " [internal route ID: \"get-team-size\"]\n\nCan be out of sync by roughly the `refresh_interval` of the ES index.", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamSize" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/TeamSize" + } + } + }, + "description": "Number of team members" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-invitation-code", + "message": "Invalid invitation code." + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-invitation-code" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Invalid invitation code. (label: `invalid-invitation-code`)" + } + }, + "summary": "Get the number of team members as an integer" + } + }, + "/users/handles": { + "post": { + "description": " [internal route ID: \"check-user-handles\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/CheckHandles" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Handle" + }, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/Handle" + }, + "type": "array" + } + } + }, + "description": "List of free handles" + } + }, + "summary": "Check availability of user handles" + } + }, + "/users/handles/{handle}": { + "head": { + "description": " [internal route ID: \"check-user-handle\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "handle", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "Handle is taken" + }, + "400": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 400, + "label": "invalid-handle", + "message": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist)" + }, + "properties": { + "code": { + "enum": [ + 400 + ], + "type": "integer" + }, + "label": { + "enum": [ + "invalid-handle" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "The given handle is invalid (less than 2 or more than 256 characters; chars not in \"a-z0-9_.-\"; or on the blocklist) (label: `invalid-handle`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "Handle not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`handle` not found\n\nHandle not found (label: `not-found`)" + } + }, + "summary": "Check whether a user handle can be taken" + } + }, + "/users/list-clients": { + "post": { + "description": " [internal route ID: \"list-clients-bulk@v2\"]\n\nIf a backend is unreachable, the clients from that backend will be omitted from the responseCalls federation service brig on get-user-clients", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/LimitedQualifiedUserIdList_500" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "properties": { + "qualified_user_map": { + "$ref": "#/components/schemas/QualifiedUserMap_Set_PubClient" + } + }, + "type": "object" + } + } + }, + "description": "" + } + }, + "summary": "List all clients for a set of user ids" + } + }, + "/users/list-prekeys": { + "post": { + "description": " [internal route ID: \"get-multi-user-prekey-bundle-qualified\"]\n\nYou can't request information for more users than maximum conversation size.Calls federation service brig on claim-multi-prekey-bundle", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QualifiedUserClients" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/QualifiedUserClientPrekeyMapV4" + } + } + }, + "description": "" + } + }, + "summary": "(deprecated) Given a map of user IDs to client IDs return a prekey for each one." + } + }, + "/users/{uid_domain}/{uid}": { + "get": { + "description": " [internal route ID: \"get-user-qualified\"]\n\nCalls federation service brig on get-users-by-ids", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserProfile" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/UserProfile" + } + } + }, + "description": "User found" + }, + "404": { + "content": { + "application/json": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + }, + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "User not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`uid_domain` or `uid` or User not found (label: `not-found`)" + } + }, + "summary": "Get a user by Domain and UserId" + } + }, + "/users/{uid_domain}/{uid}/clients": { + "get": { + "description": " [internal route ID: \"get-user-clients-qualified\"]\n\nCalls federation service brig on get-user-clients", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/PubClient" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Get all of a user's clients" + } + }, + "/users/{uid_domain}/{uid}/clients/{client}": { + "get": { + "description": " [internal route ID: \"get-user-client-qualified\"]\n\nCalls federation service brig on get-user-clients", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PubClient" + } + } + }, + "description": "" + } + }, + "summary": "Get a specific client of a user" + } + }, + "/users/{uid_domain}/{uid}/prekeys": { + "get": { + "description": " [internal route ID: \"get-users-prekey-bundle-qualified\"]\n\nCalls federation service brig on claim-prekey-bundle", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PrekeyBundle" + } + } + }, + "description": "" + } + }, + "summary": "Get a prekey for each client of a user." + } + }, + "/users/{uid_domain}/{uid}/prekeys/{client}": { + "get": { + "description": " [internal route ID: \"get-users-prekeys-client-qualified\"]\n\nCalls federation service brig on claim-prekey", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "ClientId", + "in": "path", + "name": "client", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ClientPrekey" + } + } + }, + "description": "" + } + }, + "summary": "Get a prekey for a specific client of a user." + } + }, + "/users/{uid_domain}/{uid}/supported-protocols": { + "get": { + "description": " [internal route ID: \"get-supported-protocols\"]\n\n", + "parameters": [ + { + "in": "path", + "name": "uid_domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array", + "uniqueItems": true + } + }, + "application/json;charset=utf-8": { + "schema": { + "items": { + "$ref": "#/components/schemas/BaseProtocol" + }, + "type": "array", + "uniqueItems": true + } + } + }, + "description": "Protocols supported by the user" + } + }, + "summary": "Get a user's supported protocols" + } + }, + "/users/{uid}/email": { + "put": { + "description": " [internal route ID: \"update-user-email\"]\n\nIf the user has a pending email validation, the validation email will be resent.", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/EmailUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + } + }, + "summary": "Resend email address validation email." + } + }, + "/users/{uid}/rich-info": { + "get": { + "description": " [internal route ID: \"get-rich-info\"]\n\n", + "parameters": [ + { + "description": "User Id", + "in": "path", + "name": "uid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RichInfoAssocList" + } + }, + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/RichInfoAssocList" + } + } + }, + "description": "Rich info about the user" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "insufficient-permissions", + "message": "Insufficient team permissions" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "insufficient-permissions" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Insufficient team permissions (label: `insufficient-permissions`)" + } + }, + "summary": "Get a user's rich info" + } + }, + "/verification-code/send": { + "post": { + "description": " [internal route ID: \"send-verification-code\"]\n\n", + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/SendVerificationCode" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Verification code sent." + } + }, + "summary": "Send a verification code to a given email address." + } + } + }, + "security": [ + { + "ZAuth": [] + } + ], + "servers": [ + { + "url": "/v6" + } + ] +} diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index d748968c44b..5ba6a022565 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -194,22 +194,23 @@ internalEndpointsSwaggerDocsAPIs = -- -- Dual to `internalEndpointsSwaggerDocsAPI`. versionedSwaggerDocsAPI :: Servant.Server VersionedSwaggerDocsAPI -versionedSwaggerDocsAPI (Just (VersionNumber V6)) = +versionedSwaggerDocsAPI (Just (VersionNumber V7)) = swaggerSchemaUIServer $ - ( serviceSwagger @VersionAPITag @'V6 - <> serviceSwagger @BrigAPITag @'V6 - <> serviceSwagger @GalleyAPITag @'V6 - <> serviceSwagger @SparAPITag @'V6 - <> serviceSwagger @CargoholdAPITag @'V6 - <> serviceSwagger @CannonAPITag @'V6 - <> serviceSwagger @GundeckAPITag @'V6 - <> serviceSwagger @ProxyAPITag @'V6 - <> serviceSwagger @OAuthAPITag @'V6 + ( serviceSwagger @VersionAPITag @'V7 + <> serviceSwagger @BrigAPITag @'V7 + <> serviceSwagger @GalleyAPITag @'V7 + <> serviceSwagger @SparAPITag @'V7 + <> serviceSwagger @CargoholdAPITag @'V7 + <> serviceSwagger @CannonAPITag @'V7 + <> serviceSwagger @GundeckAPITag @'V7 + <> serviceSwagger @ProxyAPITag @'V7 + <> serviceSwagger @OAuthAPITag @'V7 ) & S.info . S.title .~ "Wire-Server API" & S.info . S.description ?~ $(embedText =<< makeRelativeToProject "docs/swagger.md") - & S.servers .~ [S.Server ("/" <> toUrlPiece V6) Nothing mempty] + & S.servers .~ [S.Server ("/" <> toUrlPiece V7) Nothing mempty] & cleanupSwagger +versionedSwaggerDocsAPI (Just (VersionNumber V6)) = swaggerPregenUIServer $(pregenSwagger V6) versionedSwaggerDocsAPI (Just (VersionNumber V5)) = swaggerPregenUIServer $(pregenSwagger V5) versionedSwaggerDocsAPI (Just (VersionNumber V4)) = swaggerPregenUIServer $(pregenSwagger V4) versionedSwaggerDocsAPI (Just (VersionNumber V3)) = swaggerPregenUIServer $(pregenSwagger V3) @@ -242,38 +243,19 @@ versionedSwaggerDocsAPI Nothing = allroutes (throwError listAllVersionsResp) ] <> "" --- | Serves Swagger docs for internal endpoints --- --- Dual to `versionedSwaggerDocsAPI`. Swagger docs for old versions are (almost) --- empty. It would have been too tedious to create them. Please add --- pre-generated docs on version increase as it's done in --- `versionedSwaggerDocsAPI`. --- --- If you're having issues with this function not typechecking when it should, --- be sure to supply the type argument explicitly +-- | Serves Swagger docs for internal endpoints. internalEndpointsSwaggerDocsAPI :: forall service. String -> PortNumber -> S.OpenApi -> Servant.Server (VersionedSwaggerDocsAPIBase service) -internalEndpointsSwaggerDocsAPI service examplePort swagger (Just (VersionNumber V6)) = - swaggerSchemaUIServer $ - swagger - & adjustSwaggerForInternalEndpoint service examplePort - & cleanupSwagger -internalEndpointsSwaggerDocsAPI service examplePort swagger (Just (VersionNumber V5)) = +internalEndpointsSwaggerDocsAPI _ _ _ (Just _) = emptySwagger +internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = swaggerSchemaUIServer $ swagger & adjustSwaggerForInternalEndpoint service examplePort & cleanupSwagger -internalEndpointsSwaggerDocsAPI _ _ _ (Just (VersionNumber V4)) = emptySwagger -internalEndpointsSwaggerDocsAPI _ _ _ (Just (VersionNumber V3)) = emptySwagger -internalEndpointsSwaggerDocsAPI _ _ _ (Just (VersionNumber V2)) = emptySwagger -internalEndpointsSwaggerDocsAPI _ _ _ (Just (VersionNumber V1)) = emptySwagger -internalEndpointsSwaggerDocsAPI _ _ _ (Just (VersionNumber V0)) = emptySwagger -internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = - internalEndpointsSwaggerDocsAPI service examplePort swagger (Just maxBound) servantSitemap :: forall r p. @@ -389,13 +371,13 @@ servantSitemap = userClientAPI :: ServerT UserClientAPI (Handler r) userClientAPI = - Named @"add-client-v5" (callsFed (exposeAnnotations addClient)) + Named @"add-client-v6" (callsFed (exposeAnnotations addClient)) :<|> Named @"add-client" (callsFed (exposeAnnotations addClient)) :<|> Named @"update-client" updateClient :<|> Named @"delete-client" deleteClient - :<|> Named @"list-clients-v5" listClients + :<|> Named @"list-clients-v6" listClients :<|> Named @"list-clients" listClients - :<|> Named @"get-client-v5" getClient + :<|> Named @"get-client-v6" getClient :<|> Named @"get-client" getClient :<|> Named @"get-client-capabilities" getClientCapabilities :<|> Named @"get-client-prekeys" getClientPrekeys diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index e9c715246f3..f87e181648d 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -141,7 +141,7 @@ botAPI = :<|> Named @"bot-delete-self" botDeleteSelf :<|> Named @"bot-list-prekeys" botListPrekeys :<|> Named @"bot-update-prekeys" botUpdatePrekeys - :<|> Named @"bot-get-client-v5" botGetClient + :<|> Named @"bot-get-client-v6" botGetClient :<|> Named @"bot-get-client" botGetClient :<|> Named @"bot-claim-users-prekeys" botClaimUsersPrekeys :<|> Named @"bot-list-users" botListUserProfiles diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index 40d1569f660..3fc750c12ce 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -251,7 +251,7 @@ testAddGetClient params brig cannon = do let etype = j ^? key "type" . _String let eclient = j ^? key "client" etype @?= Just "user.client-add" - fmap fromJSON eclient @?= Just (Success (Versioned @'V5 c)) + fmap fromJSON eclient @?= Just (Success (Versioned @'V6 c)) pure c liftIO $ clientMLSPublicKeys c @?= keys getClient brig uid (clientId c) !!! do diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 939c60be287..f3350ee7bcc 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -850,7 +850,7 @@ getUserClients uid = do . expect2xx ) info $ msg ("Response" ++ show r) - let resultOrError :: Either String [Versioned 'V5 Client] = responseJsonEither r + let resultOrError :: Either String [Versioned 'V6 Client] = responseJsonEither r case resultOrError of Left e -> do Log.err $ msg ("Error parsing client response: " ++ e) From 4f828c83bb4633d6533245bdacd6e3c8372c3a7d Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Wed, 7 Aug 2024 09:01:16 +0200 Subject: [PATCH 026/136] [feat] bump nixpkgs - allow cabal-install 3.12 (#4183) * [feat] bump nixpkgs - allow cabal-install 3.12 * [chore] don't allow Cabal <3.12 * [fix] fix federator * [fix] junit-formatter: restore old behaviour and leave futurework --- Makefile | 4 ++-- cabal.project | 1 + integration/integration.cabal | 2 +- .../types-common-journal.cabal | 2 +- .../wire-message-proto-lens.cabal | 2 +- nix/default.nix | 2 +- nix/haskell-pins.nix | 7 ------- nix/manual-overrides.nix | 13 ++++++++---- nix/overlay-docs.nix | 4 ++-- nix/sources.json | 6 +++--- nix/wire-server.nix | 6 +++--- services/federator/default.nix | 3 +-- services/federator/federator.cabal | 2 +- services/federator/test/integration/Main.hs | 20 ++++++++++--------- .../test/integration/Test/Federator/Util.hs | 3 ++- services/spar/default.nix | 2 -- services/spar/spar.cabal | 6 +++--- services/spar/test-integration/Main.hs | 16 +++++++++------ 18 files changed, 52 insertions(+), 49 deletions(-) diff --git a/Makefile b/Makefile index 7ec7a59287a..3dd6227d047 100644 --- a/Makefile +++ b/Makefile @@ -238,11 +238,11 @@ add-license: .PHONY: treefmt treefmt: - treefmt + treefmt -u debug .PHONY: treefmt-check treefmt-check: - treefmt --fail-on-change + treefmt --fail-on-change -u debug ################################# ## docker targets diff --git a/cabal.project b/cabal.project index fe2c42af262..d490134c847 100644 --- a/cabal.project +++ b/cabal.project @@ -74,3 +74,4 @@ program-options allow-newer: , proto-lens-protoc:base , proto-lens-protoc:ghc + , proto-lens-setup:Cabal diff --git a/integration/integration.cabal b/integration/integration.cabal index d87c87a1f7e..1b5069a2b4b 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -13,7 +13,7 @@ build-type: Custom custom-setup setup-depends: , base - , Cabal + , Cabal >=3.12 , containers , directory , filepath diff --git a/libs/types-common-journal/types-common-journal.cabal b/libs/types-common-journal/types-common-journal.cabal index d0dc0986382..958378b2367 100644 --- a/libs/types-common-journal/types-common-journal.cabal +++ b/libs/types-common-journal/types-common-journal.cabal @@ -17,7 +17,7 @@ extra-source-files: custom-setup setup-depends: base - , Cabal + , Cabal >=3.12 , proto-lens-setup library diff --git a/libs/wire-message-proto-lens/wire-message-proto-lens.cabal b/libs/wire-message-proto-lens/wire-message-proto-lens.cabal index 3caba005121..8087bfa00f8 100644 --- a/libs/wire-message-proto-lens/wire-message-proto-lens.cabal +++ b/libs/wire-message-proto-lens/wire-message-proto-lens.cabal @@ -16,7 +16,7 @@ extra-source-files: custom-setup setup-depends: base - , Cabal + , Cabal >=3.12 , proto-lens-setup library diff --git a/nix/default.nix b/nix/default.nix index 02c0d9e01a7..f0631506f6a 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -28,7 +28,7 @@ let docsPkgs = [ pkgs.plantuml pkgs.texlive.combined.scheme-full - (pkgs.python3.withPackages + (pkgs.python310.withPackages (ps: with ps; [ myst-parser rst2pdf diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 3145ad60cb2..044b1f62879 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -316,13 +316,6 @@ let }; # end pinned dependencies for http2 - # pinned for warp - warp-tls = { - version = "3.4.5"; - sha256 = "sha256-3cDi/+n7wHfcWT/iFWAsGdLYXtKYXmvzolDt+ACJnaM="; - }; - # end pinned for warp - # PR: https://github.com/wireapp/wire-server/pull/4027 HsOpenSSL = { version = "0.11.7.7"; diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index d2d6a1baef8..7545ed0032b 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -1,4 +1,4 @@ -{ libsodium, protobuf, hlib, mls-test-cli, fetchurl, curl, fetchpatch, ... }: +{ libsodium, protobuf, hlib, mls-test-cli, fetchurl, curl, ... }: # FUTUREWORK: Figure out a way to detect if some of these packages are not # actually marked broken, so we can cleanup this file on every nixpkgs bump. hself: hsuper: { @@ -55,7 +55,6 @@ hself: hsuper: { # depend on an old version of hedgehog polysemy-test = hlib.markUnbroken (hlib.doJailbreak hsuper.polysemy-test); - polysemy-conc = hlib.markUnbroken (hlib.doJailbreak hsuper.polysemy-conc); # ------------------------------------ # okay but marked broken (nixpkgs bug) @@ -67,12 +66,18 @@ hself: hsuper: { # version overrides # (these are fine but will probably need to be adjusted in a future nixpkgs update) # ----------------- - tls = hsuper.tls_2_0_5; - tls-session-manager = hsuper.tls-session-manager_0_0_5; + tls = hsuper.tls_2_1_0; + tls-session-manager = hsuper.tls-session-manager_0_0_6; + crypton-connection = hsuper.crypton-connection_0_4_1; # older version doesn't allow tls 2.1 + amqp = hlib.dontCheck hsuper.amqp_0_23_0; # older version doesn't allow cryton-connection 0.4.1, this one has broken tests # warp requires curl in its testsuite warp = hlib.addTestToolDepends hsuper.warp [ curl ]; + # cabal multirepl requires Cabal 3.12 + Cabal = hsuper.Cabal_3_12_1_0; + Cabal-syntax = hsuper.Cabal-syntax_3_12_1_0; + # ----------------- # flags and patches # (these are fine) diff --git a/nix/overlay-docs.nix b/nix/overlay-docs.nix index c97cbe66e6a..210822d70dc 100644 --- a/nix/overlay-docs.nix +++ b/nix/overlay-docs.nix @@ -1,5 +1,5 @@ self: super: rec { - python3 = super.python3.override { + python310 = super.python310.override { packageOverrides = pself: psuper: { rst2pdf = pself.callPackage ./pkgs/python-docs/rst2pdf.nix { }; sphinx-multiversion = pself.callPackage ./pkgs/python-docs/sphinx-multiversion.nix { }; @@ -9,5 +9,5 @@ self: super: rec { }; }; - python3Packages = python3.pkgs; + python310Packages = python310.pkgs; } diff --git a/nix/sources.json b/nix/sources.json index 0abe53ae006..b69da9490f3 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -5,10 +5,10 @@ "homepage": "https://github.com/NixOS/nixpkgs", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4a3fc4cf736b7d2d288d7a8bf775ac8d4c0920b4", - "sha256": "1ibmc6iijim53bpi1wc1b295l579wzxgs8ynmsi0ldgjrxhgli1a", + "rev": "f3834de3782b82bfc666abf664f946d0e7d1f116", + "sha256": "0kzp4d4hbsc968wavwyh31lzipd4cv7wvnca167y21l5rb1kx9az", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/4a3fc4cf736b7d2d288d7a8bf775ac8d4c0920b4.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/f3834de3782b82bfc666abf664f946d0e7d1f116.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/nix/wire-server.nix b/nix/wire-server.nix index d052b10c7d6..9f3eed3d4e4 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -517,8 +517,8 @@ in pkgs.kind pkgs.netcat pkgs.niv - (hlib.justStaticExecutables pkgs.haskellPackages.apply-refact) - (pkgs.python3.withPackages + pkgs.haskellPackages.apply-refact + (pkgs.python310.withPackages (ps: with ps; [ black bokeh @@ -547,7 +547,7 @@ in ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ # linux-only, not strictly required tools pkgs.docker-compose - pkgs.telepresence + (pkgs.telepresence.override { pythonPackages = pkgs.python310Packages; }) ]; }; diff --git a/services/federator/default.nix b/services/federator/default.nix index 9b687bbd39e..cb42cc59c93 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -25,7 +25,6 @@ , hinotify , HsOpenSSL , hspec -, hspec-core , hspec-junit-formatter , http-client , http-client-tls @@ -139,11 +138,11 @@ mkDerivation { bytestring-conversion crypton crypton-connection + data-default dns-util exceptions HsOpenSSL hspec - hspec-core hspec-junit-formatter http-client-tls http-types diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index 4c88186b527..65f9a0ef444 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -285,12 +285,12 @@ executable federator-integration , bytestring-conversion , crypton , crypton-connection + , data-default , dns-util , exceptions , federator , HsOpenSSL , hspec - , hspec-core , hspec-junit-formatter , http-client-tls , http-types diff --git a/services/federator/test/integration/Main.hs b/services/federator/test/integration/Main.hs index d63572adf78..a18f17708ed 100644 --- a/services/federator/test/integration/Main.hs +++ b/services/federator/test/integration/Main.hs @@ -14,13 +14,11 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# OPTIONS_GHC -Wno-deprecations #-} -module Main - ( main, - ) -where +module Main (main) where -import Control.Concurrent.Async +import Control.Concurrent.Async (concurrently_) import Imports import OpenSSL (withOpenSSL) import System.Environment (withArgs) @@ -28,7 +26,6 @@ import Test.Federator.IngressSpec qualified import Test.Federator.InwardSpec qualified import Test.Federator.Util (TestEnv, mkEnvFromOptions) import Test.Hspec -import Test.Hspec.Core.Format import Test.Hspec.JUnit import Test.Hspec.JUnit.Config.Env import Test.Hspec.Runner @@ -39,8 +36,14 @@ main = withOpenSSL $ do env <- withArgs wireArgs mkEnvFromOptions -- withArgs hspecArgs . hspec $ do -- beforeAll (pure env) . afterAll destroyEnv $ Hspec.mkspec - cfg <- hspecConfig - withArgs hspecArgs . hspecWith cfg $ mkspec env + + -- FUTUREWORK(mangoiv): we should remove the deprecated module and instaed move to this config, however, this + -- needs check of whether it modifies the behaviour + -- junitConfig <- envJUnitConfig + -- withArgs hspecArgs . hspec . add junitConfig $ do + + conf <- hspecConfig + withArgs hspecArgs . hspecWith conf $ mkspec env hspecConfig :: IO Config hspecConfig = do @@ -52,7 +55,6 @@ hspecConfig = do : configAvailableFormatters defaultConfig } where - checksAndJUnitFormatter :: JUnitConfig -> FormatConfig -> IO Format checksAndJUnitFormatter junitConfig config = do junit <- junitFormat junitConfig config let checksFormatter = fromJust (lookup "checks" $ configAvailableFormatters defaultConfig) diff --git a/services/federator/test/integration/Test/Federator/Util.hs b/services/federator/test/integration/Test/Federator/Util.hs index 76f8e31dca4..47ea855dd95 100644 --- a/services/federator/test/integration/Test/Federator/Util.hs +++ b/services/federator/test/integration/Test/Federator/Util.hs @@ -35,6 +35,7 @@ import Data.Aeson import Data.Aeson.TH import Data.Aeson.Types qualified as Aeson import Data.ByteString.Char8 qualified as C8 +import Data.Default (def) import Data.Id import Data.Misc import Data.String.Conversions @@ -151,7 +152,7 @@ cliOptsParser = -- | Create an environment for integration tests from integration and federator config files. mkEnv :: (HasCallStack) => IntegrationConfig -> Opts -> IO TestEnv mkEnv _teTstOpts _teOpts = do - let managerSettings = mkManagerSettings (Network.Connection.TLSSettingsSimple True False False) Nothing + let managerSettings = mkManagerSettings (Network.Connection.TLSSettingsSimple True False False def) Nothing _teMgr :: Manager <- newManager managerSettings let _teBrig = endpointToReq _teTstOpts.brig _teCargohold = endpointToReq _teTstOpts.cargohold diff --git a/services/spar/default.nix b/services/spar/default.nix index 6256df4df2a..1a3549b3b90 100644 --- a/services/spar/default.nix +++ b/services/spar/default.nix @@ -27,7 +27,6 @@ , hscim , HsOpenSSL , hspec -, hspec-core , hspec-discover , hspec-junit-formatter , hspec-wai @@ -162,7 +161,6 @@ mkDerivation { hscim HsOpenSSL hspec - hspec-core hspec-junit-formatter hspec-wai http-api-data diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 1015d5d2650..3ef0220f044 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -264,9 +264,10 @@ executable spar executable spar-integration main-is: Main.hs - -- cabal-fmt: expand test-integration + -- we should not use cabal-fmt expand here because `Main` should not be in `other-modules`, it's wrong + -- and cabal chokes on it + -- FUTUREWORK(mangoiv): move Main to a different directory such that this one can be expanded other-modules: - Main Test.LoggingSpec Test.MetricsSpec Test.Spar.APISpec @@ -355,7 +356,6 @@ executable spar-integration , hscim , HsOpenSSL , hspec - , hspec-core , hspec-junit-formatter , hspec-wai , http-api-data diff --git a/services/spar/test-integration/Main.hs b/services/spar/test-integration/Main.hs index 3eefa283be5..f1a42c99847 100644 --- a/services/spar/test-integration/Main.hs +++ b/services/spar/test-integration/Main.hs @@ -1,4 +1,5 @@ {-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-deprecations #-} -- This file is part of the Wire Server implementation. -- @@ -29,7 +30,7 @@ -- the solution: https://github.com/hspec/hspec/pull/397. module Main where -import Control.Concurrent.Async +import Control.Concurrent.Async (concurrently_) import Control.Lens ((.~), (^.)) import Data.Text (pack) import Imports @@ -38,10 +39,9 @@ import Spar.Run (mkApp) import System.Environment (withArgs) import System.Random (randomRIO) import Test.Hspec -import Test.Hspec.Core.Format -import Test.Hspec.Core.Runner import Test.Hspec.JUnit import Test.Hspec.JUnit.Config.Env +import Test.Hspec.Runner (Config (configAvailableFormatters), defaultConfig, hspecWith) import qualified Test.LoggingSpec import qualified Test.MetricsSpec import qualified Test.Spar.APISpec @@ -58,8 +58,13 @@ main :: IO () main = do (wireArgs, hspecArgs) <- partitionArgs <$> getArgs let env = withArgs wireArgs mkEnvFromOptions - cfg <- hspecConfig - withArgs hspecArgs . hspecWith cfg $ do + -- FUTUREWORK(mangoiv): we should remove the deprecated module and instaed move to this config, however, this + -- needs check of whether it modifies the behaviour + -- junitConfig <- envJUnitConfig + -- withArgs hspecArgs . hspec . add junitConfig $ do + + conf <- hspecConfig + withArgs hspecArgs . hspecWith conf $ do for_ [minBound ..] $ \idpApiVersion -> do describe (show idpApiVersion) . beforeAll (env <&> teWireIdPAPIVersion .~ idpApiVersion) . afterAll destroyEnv $ do mkspecMisc @@ -77,7 +82,6 @@ hspecConfig = do : configAvailableFormatters defaultConfig } where - checksAndJUnitFormatter :: JUnitConfig -> FormatConfig -> IO Format checksAndJUnitFormatter junitConfig config = do junit <- junitFormat junitConfig config let checksFormatter = fromJust (lookup "checks" $ configAvailableFormatters defaultConfig) From 20f888c339e705d170d3757178c4f105e2c69cd0 Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Wed, 7 Aug 2024 11:34:52 +0200 Subject: [PATCH 027/136] coturn: Allow setting of K8s annotations at the Service (#4189) This can e.g. be used to set external-dns annotations. Or, any other annotations (depending on the setup of the K8s cluster.) --- changelog.d/2-features/helm-coturn-service-annotations | 1 + charts/coturn/templates/service.yaml | 4 ++++ charts/coturn/values.yaml | 3 +++ 3 files changed, 8 insertions(+) create mode 100644 changelog.d/2-features/helm-coturn-service-annotations diff --git a/changelog.d/2-features/helm-coturn-service-annotations b/changelog.d/2-features/helm-coturn-service-annotations new file mode 100644 index 00000000000..220b536130a --- /dev/null +++ b/changelog.d/2-features/helm-coturn-service-annotations @@ -0,0 +1 @@ +Allow setting of Kubernetes annotations for the `coturn` Service. diff --git a/charts/coturn/templates/service.yaml b/charts/coturn/templates/service.yaml index f1420c44d62..54f2e75da2d 100644 --- a/charts/coturn/templates/service.yaml +++ b/charts/coturn/templates/service.yaml @@ -8,6 +8,10 @@ metadata: chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} spec: # Needs to be headless # See: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ diff --git a/charts/coturn/values.yaml b/charts/coturn/values.yaml index fc6fe3b2917..1035c40d734 100644 --- a/charts/coturn/values.yaml +++ b/charts/coturn/values.yaml @@ -115,3 +115,6 @@ readinessProbe: timeoutSeconds: 5 failureThreshold: 5 +service: + # Kubernetes annotations to be set at the Service + annotations: {} From b91f9f87be3b029dbec2c395d56dc909fa8651d7 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 7 Aug 2024 15:14:50 +0200 Subject: [PATCH 028/136] Simplify user feature logic (#4178) * Remove redundant team lookups for user features * Remove pointless CPS * Add CHANGELOG entry --- changelog.d/5-internal/user-features | 1 + services/galley/src/Galley/API/Public/Bot.hs | 10 +- .../galley/src/Galley/API/Public/Feature.hs | 27 +-- .../galley/src/Galley/API/Teams/Features.hs | 1 - .../src/Galley/API/Teams/Features/Get.hs | 218 +++++++----------- 5 files changed, 107 insertions(+), 150 deletions(-) create mode 100644 changelog.d/5-internal/user-features diff --git a/changelog.d/5-internal/user-features b/changelog.d/5-internal/user-features new file mode 100644 index 00000000000..785bf2dc38a --- /dev/null +++ b/changelog.d/5-internal/user-features @@ -0,0 +1 @@ +Refactor user feature logic diff --git a/services/galley/src/Galley/API/Public/Bot.hs b/services/galley/src/Galley/API/Public/Bot.hs index 6a7dc0bd138..0465c5903e8 100644 --- a/services/galley/src/Galley/API/Public/Bot.hs +++ b/services/galley/src/Galley/API/Public/Bot.hs @@ -26,7 +26,6 @@ import Galley.App import Galley.Effects import Galley.Effects qualified as E import Galley.Options -import Imports hiding (head) import Polysemy import Polysemy.Input import Wire.API.Error @@ -50,14 +49,11 @@ getBotConversation :: Member TeamFeatureStore r, Member (ErrorS 'AccessDenied) r, Member (ErrorS 'ConvNotFound) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, Member TeamStore r ) => BotId -> ConvId -> Sem r BotConvView -getBotConversation bid cnv = - Features.guardSecondFactorDisabled (botUserId bid) cnv $ - Query.getBotConversation bid cnv +getBotConversation bid cnv = do + Features.guardSecondFactorDisabled (botUserId bid) cnv + Query.getBotConversation bid cnv diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 02b880f1bbe..d19ddefe523 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -19,6 +19,7 @@ module Galley.API.Public.Feature where import Galley.API.Teams import Galley.API.Teams.Features +import Galley.API.Teams.Features.Get import Galley.App import Imports import Wire.API.Federation.API @@ -72,16 +73,16 @@ featureAPI = <@> mkNamedAPI @'("get", LimitedEventFanoutConfig) (getFeatureStatus . DoAuth) <@> mkNamedAPI @"get-all-feature-configs-for-user" getAllFeatureConfigsForUser <@> mkNamedAPI @"get-all-feature-configs-for-team" getAllFeatureConfigsForTeam - <@> mkNamedAPI @'("get-config", LegalholdConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", SSOConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", DigitalSignaturesConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", AppLockConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", FileSharingConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", ClassifiedDomainsConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", ConferenceCallingConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", SelfDeletingMessagesConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", GuestLinksConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", SndFactorPasswordChallengeConfig) getFeatureStatusForUser - <@> mkNamedAPI @'("get-config", MLSConfig) getFeatureStatusForUser + <@> mkNamedAPI @'("get-config", LegalholdConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", SSOConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", DigitalSignaturesConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", AppLockConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", FileSharingConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", ClassifiedDomainsConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", ConferenceCallingConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", SelfDeletingMessagesConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", GuestLinksConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", SndFactorPasswordChallengeConfig) getSingleFeatureConfigForUser + <@> mkNamedAPI @'("get-config", MLSConfig) getSingleFeatureConfigForUser diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index bf5d9b6bf53..7fb0e456069 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -21,7 +21,6 @@ module Galley.API.Teams.Features setFeatureStatus, setFeatureStatusInternal, patchFeatureStatusInternal, - getFeatureStatusForUser, getAllFeatureConfigsForTeam, getAllFeatureConfigsForUser, updateLockStatus, diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index 4cec1f5f047..a83d1bad4f7 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -20,10 +20,10 @@ module Galley.API.Teams.Features.Get ( getFeatureStatus, getFeatureStatusMulti, - getFeatureStatusForUser, getAllFeatureConfigsForServer, getAllFeatureConfigsForTeam, getAllFeatureConfigsForUser, + getSingleFeatureConfigForUser, GetFeatureConfig (..), getConfigForTeam, guardSecondFactorDisabled, @@ -33,43 +33,46 @@ module Galley.API.Teams.Features.Get ) where +import Control.Error (hush) import Control.Lens import Data.Bifunctor (second) import Data.Id import Data.Kind import Data.Qualified (Local, tUnqualified) +import Data.Tagged import Galley.API.LegalHold.Team import Galley.API.Util import Galley.Effects import Galley.Effects.BrigAccess (getAccountConferenceCallingConfigClient) import Galley.Effects.ConversationStore as ConversationStore import Galley.Effects.TeamFeatureStore qualified as TeamFeatures -import Galley.Effects.TeamStore (getOneUserTeam, getTeam, getTeamMember) +import Galley.Effects.TeamStore (getOneUserTeam, getTeamMember) import Galley.Options import Galley.Types.Teams import Imports import Polysemy +import Polysemy.Error import Polysemy.Input import Wire.API.Conversation (cnvmTeam) -import Wire.API.Error (ErrorS, throwS) +import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi import Wire.API.Team.Feature data DoAuth = DoAuth UserId | DontDoAuth +type DefaultGetConfigForUserConstraints cfg r = + ( Member (Input Opts) r, + Member TeamFeatureStore r, + ComputeFeatureConstraints cfg r + ) + -- | Don't export methods of this typeclass class (IsFeatureConfig cfg) => GetFeatureConfig cfg where type GetConfigForUserConstraints cfg (r :: EffectRow) :: Constraint type GetConfigForUserConstraints cfg (r :: EffectRow) = - ( Member (Input Opts) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamStore r, - Member TeamFeatureStore r - ) + DefaultGetConfigForUserConstraints cfg r type ComputeFeatureConstraints cfg (r :: EffectRow) :: Constraint type ComputeFeatureConstraints cfg r = () @@ -88,16 +91,10 @@ class (IsFeatureConfig cfg) => GetFeatureConfig cfg where UserId -> Sem r (WithStatus cfg) default getConfigForUser :: - ( Member (Input Opts) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamStore r, - Member TeamFeatureStore r, - ComputeFeatureConstraints cfg r - ) => + (DefaultGetConfigForUserConstraints cfg r) => UserId -> Sem r (WithStatus cfg) - getConfigForUser = genericGetConfigForUser + getConfigForUser _ = getConfigForServer computeFeature :: (ComputeFeatureConstraints cfg r) => @@ -154,57 +151,20 @@ getFeatureStatusMulti (Multi.TeamFeatureNoConfigMultiRequest tids) = do toTeamStatus :: TeamId -> WithStatusNoLock cfg -> Multi.TeamStatus cfg toTeamStatus tid ws = Multi.TeamStatus tid (wssStatus ws) --- | For individual users to get feature config for their account (personal or team). --- This looks supposedly redundant to the implementations of `getConfigForUser` but it's not. --- Here we explicitly return the team setting if the user is a team member. --- In `getConfigForUser` this is mostly also the case. But there are exceptions, e.g. `ConferenceCallingConfig` -getFeatureStatusForUser :: - forall cfg r. - ( Member (Input Opts) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamFeatureStore r, - Member TeamStore r, - GetConfigForUserConstraints cfg r, - ComputeFeatureConstraints cfg r, - GetFeatureConfig cfg - ) => - UserId -> - Sem r (WithStatus cfg) -getFeatureStatusForUser zusr = do - mbTeam <- getOneUserTeam zusr - case mbTeam of - Nothing -> - getConfigForUser @cfg zusr - Just tid -> do - zusrMembership <- getTeamMember tid zusr - void $ maybe (throwS @'NotATeamMember) pure zusrMembership - assertTeamExists tid - getConfigForTeam @cfg tid - -getAllFeatureConfigsForUser :: - forall r. - ( Member BrigAccess r, +getTeamAndCheckMembership :: + ( Member TeamStore r, Member (ErrorS 'NotATeamMember) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'TeamNotFound) r, - Member (Input Opts) r, - Member LegalHoldStore r, - Member TeamFeatureStore r, - Member TeamStore r + Member (ErrorS 'TeamNotFound) r ) => UserId -> - Sem r AllFeatureConfigs -getAllFeatureConfigsForUser zusr = do - mbTeam <- getOneUserTeam zusr - when (isJust mbTeam) $ do - zusrMembership <- maybe (pure Nothing) (`getTeamMember` zusr) mbTeam - maybe (throwS @'NotATeamMember) (const $ pure ()) zusrMembership - case mbTeam of - Just tid -> - getAllFeatureConfigs tid - Nothing -> - getAllFeatureConfigsUser zusr + Sem r (Maybe TeamId) +getTeamAndCheckMembership uid = do + mTid <- getOneUserTeam uid + for_ mTid $ \tid -> do + zusrMembership <- getTeamMember tid uid + void $ maybe (throwS @'NotATeamMember) pure zusrMembership + assertTeamExists tid + pure mTid getAllFeatureConfigsForTeam :: forall r. @@ -218,8 +178,7 @@ getAllFeatureConfigsForTeam :: TeamId -> Sem r AllFeatureConfigs getAllFeatureConfigsForTeam luid tid = do - zusrMembership <- getTeamMember tid (tUnqualified luid) - maybe (throwS @'NotATeamMember) (const $ pure ()) zusrMembership + void $ getTeamMember tid (tUnqualified luid) >>= noteS @'NotATeamMember getAllFeatureConfigs tid getAllFeatureConfigs :: @@ -308,7 +267,7 @@ getAllFeatureConfigsForServer = <*> getConfigForServer @EnforceFileDownloadLocationConfig <*> getConfigForServer @LimitedEventFanoutConfig -getAllFeatureConfigsUser :: +getAllFeatureConfigsForUser :: forall r. ( Member BrigAccess r, Member (ErrorS 'NotATeamMember) r, @@ -321,28 +280,46 @@ getAllFeatureConfigsUser :: ) => UserId -> Sem r AllFeatureConfigs -getAllFeatureConfigsUser uid = +getAllFeatureConfigsForUser uid = do + mTid <- getTeamAndCheckMembership uid AllFeatures - <$> getConfigForUser @LegalholdConfig uid - <*> getConfigForUser @SSOConfig uid - <*> getConfigForUser @SearchVisibilityAvailableConfig uid - <*> getConfigForUser @SearchVisibilityInboundConfig uid - <*> getConfigForUser @ValidateSAMLEmailsConfig uid - <*> getConfigForUser @DigitalSignaturesConfig uid - <*> getConfigForUser @AppLockConfig uid - <*> getConfigForUser @FileSharingConfig uid - <*> getConfigForUser @ClassifiedDomainsConfig uid - <*> getConfigForUser @ConferenceCallingConfig uid - <*> getConfigForUser @SelfDeletingMessagesConfig uid - <*> getConfigForUser @GuestLinksConfig uid - <*> getConfigForUser @SndFactorPasswordChallengeConfig uid - <*> getConfigForUser @MLSConfig uid - <*> getConfigForUser @ExposeInvitationURLsToTeamAdminConfig uid - <*> getConfigForUser @OutlookCalIntegrationConfig uid - <*> getConfigForUser @MlsE2EIdConfig uid - <*> getConfigForUser @MlsMigrationConfig uid - <*> getConfigForUser @EnforceFileDownloadLocationConfig uid - <*> getConfigForUser @LimitedEventFanoutConfig uid + <$> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + <*> getConfigForTeamUser uid mTid + +getSingleFeatureConfigForUser :: + forall cfg r. + ( GetFeatureConfig cfg, + Member (Input Opts) r, + Member (ErrorS 'NotATeamMember) r, + Member (ErrorS 'TeamNotFound) r, + Member TeamStore r, + Member TeamFeatureStore r, + GetConfigForUserConstraints cfg r, + ComputeFeatureConstraints cfg r + ) => + UserId -> + Sem r (WithStatus cfg) +getSingleFeatureConfigForUser uid = do + mTid <- getTeamAndCheckMembership uid + getConfigForTeamUser @cfg uid mTid getConfigForTeam :: forall cfg r. @@ -380,29 +357,19 @@ getConfigForMultiTeam tids = do feat <- computeFeature @cfg tid defFeature (Just LockStatusUnlocked) dbFeature pure (tid, feat) --- | Note: this is an internal function which doesn't cover all features, e.g. conference calling -genericGetConfigForUser :: +getConfigForTeamUser :: forall cfg r. - ( Member (Input Opts) r, - Member TeamFeatureStore r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, - Member TeamStore r, - GetFeatureConfig cfg, - ComputeFeatureConstraints cfg r + ( GetFeatureConfig cfg, + GetConfigForUserConstraints cfg r, + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, + Member TeamFeatureStore r ) => UserId -> + Maybe TeamId -> Sem r (WithStatus cfg) -genericGetConfigForUser uid = do - mbTeam <- getOneUserTeam uid - case mbTeam of - Nothing -> do - getConfigForServer - Just tid -> do - zusrMembership <- getTeamMember tid uid - maybe (throwS @'NotATeamMember) (const $ pure ()) zusrMembership - assertTeamExists tid - getConfigForTeam tid +getConfigForTeamUser uid Nothing = getConfigForUser uid +getConfigForTeamUser _ (Just tid) = getConfigForTeam @cfg tid ------------------------------------------------------------------------------- -- GetFeatureConfig instances @@ -415,8 +382,6 @@ instance GetFeatureConfig SSOConfig where FeatureSSODisabledByDefault -> FeatureStatusDisabled pure $ setStatus status defFeatureStatus - getConfigForUser = genericGetConfigForUser - instance GetFeatureConfig SearchVisibilityAvailableConfig where getConfigForServer = do status <- @@ -559,31 +524,26 @@ instance GetFeatureConfig LimitedEventFanoutConfig where -- -- This function exists to resolve a cyclic dependency. guardSecondFactorDisabled :: - forall r a. - ( Member (Input Opts) r, - Member TeamFeatureStore r, + forall r. + ( Member TeamFeatureStore r, + Member (Input Opts) r, Member (ErrorS 'AccessDenied) r, - Member (ErrorS OperationDenied) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, Member TeamStore r, Member ConversationStore r ) => UserId -> ConvId -> - Sem r a -> - Sem r a -guardSecondFactorDisabled uid cid action = do - mbCnvData <- ConversationStore.getConversationMetadata cid - tf <- case mbCnvData >>= cnvmTeam of - Nothing -> getConfigForUser @SndFactorPasswordChallengeConfig uid - Just tid -> do - teamExists <- isJust <$> getTeam tid - if teamExists - then getConfigForTeam @SndFactorPasswordChallengeConfig tid - else getConfigForUser @SndFactorPasswordChallengeConfig uid + Sem r () +guardSecondFactorDisabled uid cid = do + mTid <- fmap hush . runError @() $ do + convData <- ConversationStore.getConversationMetadata cid >>= note () + tid <- note () convData.cnvmTeam + mapError (unTagged @'TeamNotFound @()) $ assertTeamExists tid + pure tid + + tf <- getConfigForTeamUser @SndFactorPasswordChallengeConfig uid mTid case wsStatus tf of - FeatureStatusDisabled -> action + FeatureStatusDisabled -> pure () FeatureStatusEnabled -> throwS @'AccessDenied featureEnabledForTeam :: From c84072c6ddc9895fd7e0b7bf2417203635ee1ead Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Thu, 8 Aug 2024 09:35:39 +0200 Subject: [PATCH 029/136] [chore] Remove more dead code, restore LegalHold internal API swagger (#4191) * Weeding out and enabling dangling golden test. * Removed remaining weeds, restored legahold swagger. * Added changelog. --- changelog.d/4-docs/WPB-9742 | 1 + integration/test/Test/FeatureFlags/Util.hs | 40 ----------- .../src/Wire/API/Routes/Internal/Brig.hs | 11 ++- .../src/Wire/API/Routes/Internal/LegalHold.hs | 1 - .../golden/Test/Wire/API/Golden/Manual.hs | 2 +- .../Manual/QualifiedUserClientPrekeyMap.hs | 3 +- ...Object_QualifiedUserClientPrekeyMap_2.json | 1 + .../testObject_UserClientPrekeyMap_9.json | 15 ++++- libs/wire-subsystems/src/Wire/Error.hs | 4 -- services/brig/brig.cabal | 1 - .../brig/test/integration/API/Internal.hs | 4 +- .../test/integration/API/Internal/Util.hs | 67 ------------------- .../brig/test/integration/API/User/Util.hs | 8 --- .../brig/test/integration/Federation/Util.hs | 3 - services/brig/test/integration/SMTP.hs | 3 - .../test/integration/Test/Federator/Util.hs | 3 - .../galley/test/integration/API/Federation.hs | 24 ------- .../galley/test/integration/API/MLS/Mocks.hs | 7 -- .../galley/test/integration/Federation.hs | 5 -- weeder.toml | 9 +++ 20 files changed, 40 insertions(+), 172 deletions(-) create mode 100644 changelog.d/4-docs/WPB-9742 delete mode 100644 services/brig/test/integration/API/Internal/Util.hs diff --git a/changelog.d/4-docs/WPB-9742 b/changelog.d/4-docs/WPB-9742 new file mode 100644 index 00000000000..c6fbdf93714 --- /dev/null +++ b/changelog.d/4-docs/WPB-9742 @@ -0,0 +1 @@ +Restored LegalHold internal API swagger as part of Brig. diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index ddcfbdac758..c1e3bc9acc9 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -19,7 +19,6 @@ module Test.FeatureFlags.Util where import qualified API.Galley as Public import qualified API.GalleyInternal as Internal -import qualified Data.Aeson as A import Testlib.Prelude disabled :: Value @@ -51,45 +50,6 @@ checkFeatureWith shouldMatch' feature user tid expected = do resp.status `shouldMatchInt` 200 resp.json %. feature `shouldMatch'` expected -checkFeatureLenientTtl :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App () -checkFeatureLenientTtl = checkFeatureWith shouldMatchLenientTtl - where - shouldMatchLenientTtl :: (HasCallStack) => App Value -> Value -> App () - shouldMatchLenientTtl actual expected = do - expectedLockStatus <- expected %. "lockStatus" - actual %. "lockStatus" `shouldMatch` expectedLockStatus - expectedStatus <- expected %. "status" - actual %. "status" `shouldMatch` expectedStatus - mExpectedConfig <- lookupField expected "config" - mActualConfig <- lookupField actual "config" - mActualConfig `shouldMatch` mExpectedConfig - expectedTtl <- expected %. "ttl" - actualTtl <- actual %. "ttl" - checkTtl actualTtl expectedTtl - -checkTtl :: (MakesValue a, MakesValue b) => a -> b -> App () -checkTtl x y = do - vx <- make x - vy <- make y - check vx vy - where - check (A.String a) (A.String b) = do - a `shouldMatch` "unlimited" - b `shouldMatch` "unlimited" - check _ (A.String _) = assertFailure "expected the actual ttl to be unlimited, but it was limited" - check (A.String _) _ = assertFailure "expected the actual ttl to be limited, but it was unlimited" - check (A.Number actualTtl) (A.Number expectedTtl) = do - assertBool - ("expected the actual TTL to be greater than 0 and equal to or no more than 2 seconds less than " <> show expectedTtl <> ", but it was " <> show actualTtl) - ( actualTtl - > 0 - && actualTtl - <= expectedTtl - && abs (actualTtl - expectedTtl) - <= 2 - ) - check _ _ = assertFailure "unexpected ttl value(s)" - assertForbidden :: (HasCallStack) => Response -> App () assertForbidden = assertLabel 403 "no-team-member" 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 7d31bedba95..a143f9e3e33 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -72,6 +72,7 @@ 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) @@ -747,8 +748,14 @@ type FederationRemotesAPIDescription = swaggerDoc :: OpenApi swaggerDoc = - toOpenApi (Proxy @API) - & info . title .~ "Wire-Server internal brig API" + brigSwaggerDoc + <> LegalHoldInternalAPI.swaggerDoc + +brigSwaggerDoc :: OpenApi +brigSwaggerDoc = + ( toOpenApi (Proxy @API) + & info . title .~ "Wire-Server internal brig API" + ) newtype BrigInternalClient a = BrigInternalClient (Servant.ClientM a) deriving newtype (Functor, Applicative, Monad, Servant.RunClient) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs index f77137eab0a..ffde2e561c3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs @@ -39,7 +39,6 @@ type InternalLegalHoldAPI = :> Put '[] NoContent ) --- TODO: should be called, is currently dead code. swaggerDoc :: OpenApi swaggerDoc = toOpenApi (Proxy @InternalLegalHoldAPI) 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 feae9694e32..0ff493d9e85 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 @@ -69,7 +69,7 @@ tests = (testObject_UserClientPrekeyMap_6, "testObject_UserClientPrekeyMap_6.json"), (testObject_UserClientPrekeyMap_7, "testObject_UserClientPrekeyMap_7.json"), (testObject_UserClientPrekeyMap_8, "testObject_UserClientPrekeyMap_8.json"), - (testObject_UserClientPrekeyMap_8, "testObject_UserClientPrekeyMap_9.json") + (testObject_UserClientPrekeyMap_9, "testObject_UserClientPrekeyMap_9.json") ], testGroup "QualifiedUserClientPrekeyMap" $ testObjects diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/QualifiedUserClientPrekeyMap.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/QualifiedUserClientPrekeyMap.hs index 8f25987a882..0c3e30c2b7d 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/QualifiedUserClientPrekeyMap.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/QualifiedUserClientPrekeyMap.hs @@ -38,5 +38,6 @@ testObject_QualifiedUserClientPrekeyMap_2 = (Domain "epsilon.example.com", testObject_UserClientPrekeyMap_5), (Domain "zeta.example.com", testObject_UserClientPrekeyMap_6), (Domain "eta.example.com", testObject_UserClientPrekeyMap_7), - (Domain "theta.example.com", testObject_UserClientPrekeyMap_8) + (Domain "theta.example.com", testObject_UserClientPrekeyMap_8), + (Domain "meta.example.com", testObject_UserClientPrekeyMap_8) ] diff --git a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMap_2.json b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMap_2.json index 68dd80f79b4..3d60acebf01 100644 --- a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMap_2.json +++ b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMap_2.json @@ -161,6 +161,7 @@ } } }, + "meta.example.com": {}, "theta.example.com": {}, "zeta.example.com": { "0000004d-0000-001f-0000-006300000073": { diff --git a/libs/wire-api/test/golden/testObject_UserClientPrekeyMap_9.json b/libs/wire-api/test/golden/testObject_UserClientPrekeyMap_9.json index 0967ef424bc..83f2a51b546 100644 --- a/libs/wire-api/test/golden/testObject_UserClientPrekeyMap_9.json +++ b/libs/wire-api/test/golden/testObject_UserClientPrekeyMap_9.json @@ -1 +1,14 @@ -{} +{ + "00000054-0000-003b-0000-00210000005f": {}, + "00000065-0000-0040-0000-005f00000064": { + "0": { + "id": 128, + "key": "𧮘峰$ LText errorLabel (StdError e) = Wai.label e errorLabel (RichError e _ _) = Wai.label e -errorStatus :: HttpError -> Status -errorStatus (StdError e) = Wai.code e -errorStatus (RichError e _ _) = Wai.code e - instance ToJSON HttpError where toJSON (StdError e) = toJSON e toJSON (RichError e x _) = case (toJSON e, toJSON x) of diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 377b7cdcf86..7174d2407e8 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -373,7 +373,6 @@ executable brig-integration API.Calling API.Federation API.Internal - API.Internal.Util API.Metrics API.MLS.Util API.OAuth diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index 0f304d3ada5..55a713d321d 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -23,7 +23,6 @@ module API.Internal ) where -import API.Internal.Util import API.MLS.Util import Bilge import Bilge.Assert @@ -32,6 +31,7 @@ import Brig.Options qualified as Opt import Cassandra qualified as C import Cassandra qualified as Cass import Cassandra.Util +import Control.Monad.Catch import Data.ByteString.Conversion (toByteString') import Data.Default import Data.Id @@ -45,6 +45,8 @@ import Util.Options (Endpoint) import Wire.API.User import Wire.API.User.Client +type TestConstraints m = (MonadFail m, MonadCatch m, MonadIO m, MonadHttp m) + tests :: Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Gundeck -> Galley -> IO TestTree tests opts mgr db brig brigep _gundeck galley = do pure $ diff --git a/services/brig/test/integration/API/Internal/Util.hs b/services/brig/test/integration/API/Internal/Util.hs deleted file mode 100644 index b37bff338a2..00000000000 --- a/services/brig/test/integration/API/Internal/Util.hs +++ /dev/null @@ -1,67 +0,0 @@ --- Disabling to stop warnings on HasCallStack -{-# OPTIONS_GHC -Wno-redundant-constraints #-} - --- 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 API.Internal.Util - ( TestConstraints, - getAccountConferenceCallingConfigClient, - putAccountConferenceCallingConfigClient, - deleteAccountConferenceCallingConfigClient, - ) -where - -import Bilge hiding (host, port) -import Control.Lens ((^.)) -import Control.Monad.Catch (MonadCatch) -import Data.Id -import Data.Proxy (Proxy (Proxy)) -import Data.String.Conversions -import Imports -import Servant.API ((:>)) -import Servant.API.ContentTypes (NoContent) -import Servant.Client qualified as Client -import Util.Options (Endpoint, host, port) -import Wire.API.Routes.Internal.Brig as IAPI -import Wire.API.Team.Feature qualified as Public - -type TestConstraints m = (MonadFail m, MonadCatch m, MonadIO m, MonadHttp m) - -getAccountConferenceCallingConfigClientM :: UserId -> Client.ClientM (Public.WithStatusNoLock Public.ConferenceCallingConfig) -getAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.GetAccountConferenceCallingConfig)) - -putAccountConferenceCallingConfigClientM :: UserId -> Public.WithStatusNoLock Public.ConferenceCallingConfig -> Client.ClientM NoContent -putAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.PutAccountConferenceCallingConfig)) - -deleteAccountConferenceCallingConfigClientM :: UserId -> Client.ClientM NoContent -deleteAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.DeleteAccountConferenceCallingConfig)) - -getAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> UserId -> m (Either Client.ClientError (Public.WithStatusNoLock Public.ConferenceCallingConfig)) -getAccountConferenceCallingConfigClient brigep mgr uid = runHereClientM brigep mgr (getAccountConferenceCallingConfigClientM uid) - -putAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> UserId -> Public.WithStatusNoLock Public.ConferenceCallingConfig -> m (Either Client.ClientError NoContent) -putAccountConferenceCallingConfigClient brigep mgr uid cfg = runHereClientM brigep mgr (putAccountConferenceCallingConfigClientM uid cfg) - -deleteAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> UserId -> m (Either Client.ClientError NoContent) -deleteAccountConferenceCallingConfigClient brigep mgr uid = runHereClientM brigep mgr (deleteAccountConferenceCallingConfigClientM uid) - -runHereClientM :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> Client.ClientM a -> m (Either Client.ClientError a) -runHereClientM brigep mgr action = do - let env = Client.mkClientEnv mgr baseurl - baseurl = Client.BaseUrl Client.Http (cs $ brigep ^. host) (fromIntegral $ brigep ^. port) "" - liftIO $ Client.runClientM action env diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 0a3789a9d47..8627d78c989 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -316,14 +316,6 @@ getProperty brig u k = . paths ["/properties", k] . zUser u -deleteProperty :: Brig -> UserId -> ByteString -> (MonadHttp m) => m ResponseLBS -deleteProperty brig u k = - delete $ - brig - . paths ["/properties", k] - . zConn "conn" - . zUser u - countCookies :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> UserId -> CookieLabel -> m (Maybe Int) countCookies brig u label = do r <- diff --git a/services/brig/test/integration/Federation/Util.hs b/services/brig/test/integration/Federation/Util.hs index a1cc7edb483..c7041eb96e5 100644 --- a/services/brig/test/integration/Federation/Util.hs +++ b/services/brig/test/integration/Federation/Util.hs @@ -98,9 +98,6 @@ generateClientPrekeys brig prekeys = do clients <- traverse (responseJsonError <=< addClient brig (qUnqualified quser)) nclients pure (quser, zipWith mkClientPrekey prekeys clients) -assertRightT :: (MonadIO m, Show a, HasCallStack) => ExceptT a m b -> m b -assertRightT = assertRight <=< runExceptT - getConvQualified :: Galley -> UserId -> Qualified ConvId -> Http ResponseLBS getConvQualified g u (Qualified cnvId domain) = get $ diff --git a/services/brig/test/integration/SMTP.hs b/services/brig/test/integration/SMTP.hs index 4911ffbcebc..95b50ac5e4b 100644 --- a/services/brig/test/integration/SMTP.hs +++ b/services/brig/test/integration/SMTP.hs @@ -215,9 +215,6 @@ mailStoringApp receivedMailRef mail = do mailRejectingApp :: Postie.Application mailRejectingApp = const (pure Postie.Rejected) -mailAcceptingApp :: Postie.Application -mailAcceptingApp = const (pure Postie.Accepted) - delayingApp :: (TimeUnit t) => t -> Postie.Application delayingApp delay = const diff --git a/services/federator/test/integration/Test/Federator/Util.hs b/services/federator/test/integration/Test/Federator/Util.hs index 47ea855dd95..eb352855627 100644 --- a/services/federator/test/integration/Test/Federator/Util.hs +++ b/services/federator/test/integration/Test/Federator/Util.hs @@ -161,9 +161,6 @@ mkEnv _teTstOpts _teOpts = do let _teSettings = optSettings _teOpts pure TestEnv {..} -destroyEnv :: (HasCallStack) => TestEnv -> IO () -destroyEnv _ = pure () - endpointToReq :: Endpoint -> (Bilge.Request -> Bilge.Request) endpointToReq ep = Bilge.host (ep ^. O.host . to cs) . Bilge.port (ep ^. O.port) diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index abd5bfccac8..2f302fe6e17 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -916,27 +916,3 @@ sendMessage = do -- check that alice received the message WS.assertMatch_ (5 # Second) ws $ wsAssertOtr' "" conv bob bobClient aliceClient (toBase64Text "hi alice") - -getConvAction :: Sing tag -> SomeConversationAction -> Maybe (ConversationAction tag) -getConvAction tquery (SomeConversationAction tag action) = - case (tag, tquery) of - (SConversationJoinTag, SConversationJoinTag) -> Just action - (SConversationJoinTag, _) -> Nothing - (SConversationLeaveTag, SConversationLeaveTag) -> Just action - (SConversationLeaveTag, _) -> Nothing - (SConversationMemberUpdateTag, SConversationMemberUpdateTag) -> Just action - (SConversationMemberUpdateTag, _) -> Nothing - (SConversationDeleteTag, SConversationDeleteTag) -> Just action - (SConversationDeleteTag, _) -> Nothing - (SConversationRenameTag, SConversationRenameTag) -> Just action - (SConversationRenameTag, _) -> Nothing - (SConversationMessageTimerUpdateTag, SConversationMessageTimerUpdateTag) -> Just action - (SConversationMessageTimerUpdateTag, _) -> Nothing - (SConversationReceiptModeUpdateTag, SConversationReceiptModeUpdateTag) -> Just action - (SConversationReceiptModeUpdateTag, _) -> Nothing - (SConversationAccessDataTag, SConversationAccessDataTag) -> Just action - (SConversationAccessDataTag, _) -> Nothing - (SConversationRemoveMembersTag, SConversationRemoveMembersTag) -> Just action - (SConversationRemoveMembersTag, _) -> Nothing - (SConversationUpdateProtocolTag, SConversationUpdateProtocolTag) -> Just action - (SConversationUpdateProtocolTag, _) -> Nothing diff --git a/services/galley/test/integration/API/MLS/Mocks.hs b/services/galley/test/integration/API/MLS/Mocks.hs index 3e82b298246..58c988cb390 100644 --- a/services/galley/test/integration/API/MLS/Mocks.hs +++ b/services/galley/test/integration/API/MLS/Mocks.hs @@ -24,7 +24,6 @@ module API.MLS.Mocks sendMessageMock, claimKeyPackagesMock, queryGroupStateMock, - deleteMLSConvMock, ) where @@ -90,9 +89,3 @@ queryGroupStateMock gs qusr = do if uid == qUnqualified qusr then GetGroupInfoResponseState (Base64ByteString gs) else GetGroupInfoResponseError ConvNotFound - -deleteMLSConvMock :: Mock LByteString -deleteMLSConvMock = - asum - [ "on-conversation-updated" ~> EmptyResponse - ] diff --git a/services/galley/test/integration/Federation.hs b/services/galley/test/integration/Federation.hs index c2fd087ce73..bfa44d53dfc 100644 --- a/services/galley/test/integration/Federation.hs +++ b/services/galley/test/integration/Federation.hs @@ -1,7 +1,6 @@ module Federation where import Control.Lens ((^.)) -import Control.Monad.Catch import Data.Domain import Data.Id import Data.Qualified @@ -14,7 +13,6 @@ import Galley.Types.Conversations.Members (LocalMember (..), RemoteMember (..), import Imports import Test.Tasty.HUnit import TestSetup -import UnliftIO.Retry import Wire.API.Conversation import Wire.API.Conversation.Protocol (Protocol (..)) import Wire.API.Conversation.Role (roleNameWireMember) @@ -47,6 +45,3 @@ isConvMemberLTests = do liftIO $ assertBool "Remote UserId" $ isConvMemberL lconv rUserId liftIO $ assertBool "Qualified UserId (local)" $ isConvMemberL lconv $ tUntagged lUserId liftIO $ assertBool "Qualified UserId (remote)" $ isConvMemberL lconv $ tUntagged rUserId - -constHandlers :: (MonadIO m) => [RetryStatus -> Handler m Bool] -constHandlers = [const $ Handler $ (\(_ :: SomeException) -> pure True)] diff --git a/weeder.toml b/weeder.toml index 9521cfeee9c..7c19d1f538b 100644 --- a/weeder.toml +++ b/weeder.toml @@ -10,10 +10,14 @@ roots = [ # may of the entries here are about general-purpose module "^API.Galley.consentToLegalHold", # FUTUREWORK: write tests that need this! "^API.Galley.enableLegalHold", # FUTUREWORK: write tests that need this! "^API.Galley.getLegalHoldStatus", # FUTUREWORK: write tests that need this! + "^API.GalleyInternal.putTeamMember", + "^API.MLS.Util.clientKeyPair", "^API.MLS.Util.getCurrentGroupId", "^API.MLS.Util.getKeyPackageCount", "^API.MLS.Util.getKeyPair", "^API.Nginz.*$", # FUTUREWORK: consider using everything or cleaning up. + "^API.Search._testOrderName", + "^API.Team.Util.*$", # FUTUREWORK: Consider whether unused utility functions should be kept. "^Bilge.*$", "^Cassandra.Helpers.toOptionFieldName", "^Data.ETag._OpaqueDigest", @@ -32,6 +36,7 @@ roots = [ # may of the entries here are about general-purpose module "^Data.Range.rsingleton", "^Data.ZAuth.Validation.*$", "^Galley.Types.UserList.ulDiff", + "^Galley.Types.Teams.canSeePermsOf", # TODO: figure out why weeder is confused by let bindings with curried infix notation "^HTTP2.Client.Manager.*$", "^Imports.getChar", "^Imports.getContents", @@ -114,9 +119,12 @@ roots = [ # may of the entries here are about general-purpose module "^Test.Data.Schema.detailSchema", "^Test.Data.Schema.userSchemaWithDefaultName", "^Test.Data.Schema.userSchemaWithDefaultName'", + "^Test.Federator.JSON.deriveJSONOptions", # This is used inside an instance derivation via TH "^Test.Wire.API.Golden.Run.main$", + "^Test.Wire.API.Password.testHashPasswordScrypt", # FUTUREWORK: reworking scrypt/argon2id is planned for next sprint "^TestSetup.runFederationClient", "^TestSetup.viewCargohold", + "^Testlib.App.*$", # FUTUREWORK: See how we can have weeder parse operators in the config file. "^Testlib.Cannon.awaitAtLeastNMatches", "^Testlib.Cannon.awaitAtLeastNMatchesResult", "^Testlib.Cannon.awaitNToMMatches", @@ -131,6 +139,7 @@ roots = [ # may of the entries here are about general-purpose module "^Testlib.Printing.gray", "^Testlib.Printing.hline", "^Testlib.Run.main$", + "^Testlib.Run.mainI$", "^Testlib.RunServices.main$", "^ThreadBudget.extractLogHistory", "^Util.assertOne", From 3c0854c9228d3d03979564cedcbcf7571de9d392 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 8 Aug 2024 12:26:56 +0200 Subject: [PATCH 030/136] Small clarification in SAML/SCIM docs. (#4194) (The old phrasing can be interpreted as suggesting that `NameID` can not be an email, which is false.) --- docs/src/understand/single-sign-on/understand/main.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/understand/single-sign-on/understand/main.md b/docs/src/understand/single-sign-on/understand/main.md index f413538c138..adabf76b74a 100644 --- a/docs/src/understand/single-sign-on/understand/main.md +++ b/docs/src/understand/single-sign-on/understand/main.md @@ -269,7 +269,7 @@ You need to configure your SCIM client to use the following mandatory SCIM attri 3. The `externalId` attribute: - 1. If you are using Wire's SAML SSO feature then set `externalId` attribute to the same identifier used for `NameID` in your SAML configuration. + 1. If you are using Wire's SAML SSO feature then set `externalId` attribute to the same identifier used for `NameID` in your SAML configuration (suppored `NameID` types are `email` and `unspecified`). 2. If you are using email/password authentication then set the `externalId` attribute to the user's email address. The user will receive an invitation email during provisioning. Also note that the account will be set to `"active": false` until the user has accepted the invitation and activated the account. From f1342a5bf425d103532e791db217dff588c9a101 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 8 Aug 2024 17:20:32 +0200 Subject: [PATCH 031/136] Re-introduce test case tags for BSI audit (revert #4041). (#4192) --- changelog.d/4-docs/revert-wpb8628 | 1 + integration/test/Test/AccessUpdate.hs | 3 ++ integration/test/Test/Login.hs | 9 ++++++ libs/zauth/test/ZAuth.hs | 5 ++- .../brig/test/integration/API/User/Account.hs | 32 +++++++++++++++++++ .../brig/test/integration/API/User/Auth.hs | 12 +++++++ .../brig/test/integration/API/User/Client.hs | 24 ++++++++++++++ .../brig/test/integration/API/User/Handles.hs | 3 ++ .../integration/Test/Federator/IngressSpec.hs | 3 ++ .../integration/Test/Federator/InwardSpec.hs | 8 +++++ .../unit/Test/Federator/ExternalServer.hs | 4 +++ .../unit/Test/Federator/InternalServer.hs | 3 ++ .../test/unit/Test/Federator/Options.hs | 4 +++ .../test/unit/Test/Federator/Remote.hs | 3 ++ .../test/unit/Test/Federator/Validation.hs | 9 ++++++ services/galley/test/integration/API.hs | 30 +++++++++++++++++ .../galley/test/integration/API/Federation.hs | 3 ++ services/galley/test/integration/API/Teams.hs | 12 +++++++ .../test-integration/Test/Spar/APISpec.hs | 22 +++++++++++++ .../Test/Spar/Scim/AuthSpec.hs | 10 ++++++ .../Test/Spar/Scim/UserSpec.hs | 5 +++ 21 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 changelog.d/4-docs/revert-wpb8628 diff --git a/changelog.d/4-docs/revert-wpb8628 b/changelog.d/4-docs/revert-wpb8628 new file mode 100644 index 00000000000..4400ff6154a --- /dev/null +++ b/changelog.d/4-docs/revert-wpb8628 @@ -0,0 +1 @@ +Re-introduce test case tags for BSI audit (revert #4041) \ No newline at end of file diff --git a/integration/test/Test/AccessUpdate.hs b/integration/test/Test/AccessUpdate.hs index ad2f12a978b..c63c10cbd0b 100644 --- a/integration/test/Test/AccessUpdate.hs +++ b/integration/test/Test/AccessUpdate.hs @@ -38,6 +38,7 @@ testBaz :: HasCallStack => App () testBaz = pure () -} +-- | @SF.Federation @SF.Separation @TSFI.RESTfulAPI @S2 -- -- The test asserts that, among others, remote users are removed from a -- conversation when an access update occurs that disallows guests from @@ -73,6 +74,8 @@ testAccessUpdateGuestRemoved = do res.status `shouldMatchInt` 200 res.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject bob +-- @END + testAccessUpdateGuestRemovedUnreachableRemotes :: (HasCallStack) => App () testAccessUpdateGuestRemovedUnreachableRemotes = do resourcePool <- asks resourcePool diff --git a/integration/test/Test/Login.hs b/integration/test/Test/Login.hs index 096f441a50f..56ded5e6026 100644 --- a/integration/test/Test/Login.hs +++ b/integration/test/Test/Login.hs @@ -23,6 +23,7 @@ testLoginVerify6DigitEmailCodeSuccess = do bindResponse (loginWith2ndFactor owner email defPassword code) $ \resp -> do resp.status `shouldMatchInt` 200 +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that login fails with wrong second factor email verification code testLoginVerify6DigitWrongCodeFails :: (HasCallStack) => App () @@ -38,6 +39,9 @@ testLoginVerify6DigitWrongCodeFails = do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "code-authentication-failed" +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that login without verification code fails if SndFactorPasswordChallenge feature is enabled in team testLoginVerify6DigitMissingCodeFails :: (HasCallStack) => App () @@ -50,6 +54,9 @@ testLoginVerify6DigitMissingCodeFails = do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "code-authentication-required" +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that login fails with expired second factor email verification code testLoginVerify6DigitExpiredCodeFails :: (HasCallStack) => App () @@ -73,6 +80,8 @@ testLoginVerify6DigitExpiredCodeFails = do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "code-authentication-failed" +-- @END + testLoginVerify6DigitResendCodeSuccessAndRateLimiting :: (HasCallStack) => App () testLoginVerify6DigitResendCodeSuccessAndRateLimiting = do (owner, team, []) <- createTeam OwnDomain 0 diff --git a/libs/zauth/test/ZAuth.hs b/libs/zauth/test/ZAuth.hs index db94845d04c..80545f884ae 100644 --- a/libs/zauth/test/ZAuth.hs +++ b/libs/zauth/test/ZAuth.hs @@ -56,7 +56,7 @@ tests = do ], testGroup "Signing and Verifying" - [ testCase "testExpired - expired" (runCreate z 1 $ testExpired v), + [ testCase "expired" (runCreate z 1 $ testExpired v), testCase "not expired" (runCreate z 2 $ testNotExpired v), testCase "signed access-token is valid" (runCreate z 3 $ testSignAndVerify v) ], @@ -94,6 +94,7 @@ testNotExpired p = do liftIO $ assertBool "testNotExpired: validation failed" (isRight x) -- The testExpired test conforms to the following testing standards: +-- @SF.Channel @TSFI.RESTfulAPI @TSFI.NTP @S2 @S3 -- -- Using an expired access token should fail testExpired :: V.Env -> Create () @@ -104,6 +105,8 @@ testExpired p = do x <- liftIO $ runValidate p $ check t liftIO $ Left Expired @=? x +-- @END + testSignAndVerify :: V.Env -> Create () testSignAndVerify p = do u <- liftIO nextRandom diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 5cd1538a79d..b041300dca0 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -100,6 +100,7 @@ tests _ at opts p b c ch g aws userJournalWatcher = testGroup "account" [ test p "post /register - 201 (with preverified)" $ testCreateUserWithPreverified opts b userJournalWatcher, + test p "testCreateUserWithInvalidVerificationCode - post /register - 400 (with preverified)" $ testCreateUserWithInvalidVerificationCode b, test p "post /register - 201" $ testCreateUser b g, test p "post /register - 201 anonymous" $ testCreateUserAnon b g, test p "testCreateUserEmptyName - post /register - 400 empty name" $ testCreateUserEmptyName b, @@ -160,6 +161,25 @@ tests _ at opts p b c ch g aws userJournalWatcher = ] ] +-- The testCreateUserWithInvalidVerificationCode test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 +-- +-- Registering with an invalid verification code and valid account details should fail. +testCreateUserWithInvalidVerificationCode :: Brig -> Http () +testCreateUserWithInvalidVerificationCode brig = do + -- Attempt to register (pre verified) user with email + e <- randomEmail + code <- randomActivationCode -- incorrect but syntactically valid activation code + let Object regEmail = + object + [ "name" .= Name "Alice", + "email" .= fromEmail e, + "email_code" .= code + ] + postUserRegister' regEmail brig !!! const 404 === statusCode + +-- @END + testUpdateUserEmailByTeamOwner :: Opt.Opts -> Brig -> Http () testUpdateUserEmailByTeamOwner opts brig = do (_, teamOwner, emailOwner : otherTeamMember : _) <- createPopulatedBindingTeamWithNamesAndHandles brig 2 @@ -270,6 +290,7 @@ assertOnlySelfConversations galley uid = do liftIO $ cnvType conv @?= SelfConv -- The testCreateUserEmptyName test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- An empty name is not allowed on registration testCreateUserEmptyName :: Brig -> Http () @@ -281,7 +302,10 @@ testCreateUserEmptyName brig = do post (brig . path "/register" . contentJson . body p) !!! const 400 === statusCode +-- @END + -- The testCreateUserLongName test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- a name with > 128 characters is not allowed. testCreateUserLongName :: Brig -> Http () @@ -294,6 +318,8 @@ testCreateUserLongName brig = do post (brig . path "/register" . contentJson . body p) !!! const 400 === statusCode +-- @END + testCreateUserAnon :: Brig -> Galley -> Http () testCreateUserAnon brig galley = do let p = @@ -351,6 +377,7 @@ testCreateUserPending _ brig = do Search.assertCan'tFind brig suid quid "Mr. Pink" -- The testCreateUserConflict test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- email address must not be taken on @/register@. testCreateUserConflict :: Opt.Opts -> Brig -> Http () @@ -382,7 +409,10 @@ testCreateUserConflict _ brig = do const 409 === statusCode const (Just "key-exists") === fmap Error.label . responseJsonMaybe +-- @END + -- The testCreateUserInvalidEmail test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- Test to make sure a new user cannot be created with an invalid email address or invalid phone number. testCreateUserInvalidEmail :: Opt.Opts -> Brig -> Http () @@ -412,6 +442,8 @@ testCreateUserInvalidEmail _ brig = do post (brig . path "/register" . contentJson . body reqPhone) !!! const 400 === statusCode +-- @END + testCreateUserBlacklist :: Opt.Opts -> Brig -> AWS.Env -> Http () testCreateUserBlacklist (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ _ = pure () testCreateUserBlacklist _ brig aws = diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index a2e34731214..2dbf722c596 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -376,6 +376,7 @@ testLoginUntrustedDomain brig = do !!! const 200 === statusCode -- The testLoginFailure test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- Test that trying to log in with a wrong password or non-existent email fails. testLoginFailure :: Brig -> Http () @@ -397,6 +398,8 @@ testLoginFailure brig = do PersistentCookie !!! const 403 === statusCode +-- @END + testThrottleLogins :: Opts.Opts -> Brig -> Http () testThrottleLogins conf b = do -- Get the maximum amount of times we are allowed to login before @@ -422,6 +425,7 @@ testThrottleLogins conf b = do login b (defEmailLogin e) SessionCookie !!! const 200 === statusCode -- The testLimitRetries test conforms to the following testing standards: +-- @SF.Channel @TSFI.RESTfulAPI @TSFI.NTP @S2 -- -- The following test tests the login retries. It checks that a user can make -- only a prespecified number of attempts to log in with an invalid password, @@ -476,6 +480,8 @@ testLimitRetries conf brig = do liftIO $ threadDelay (1000000 * 2) login brig (defEmailLogin email) SessionCookie !!! const 200 === statusCode +-- @END + ------------------------------------------------------------------------------- -- LegalHold Login @@ -602,6 +608,7 @@ testNoUserSsoLogin brig = do -- Token Refresh -- The testInvalidCookie test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @TSFI.NTP @S2 -- -- Test that invalid and expired tokens do not work. testInvalidCookie :: forall u. (ZAuth.UserTokenLike u) => ZAuth.Env -> Brig -> Http () @@ -619,6 +626,8 @@ testInvalidCookie z b = do const 403 === statusCode const (Just "expired") =~= responseBody +-- @END + testInvalidToken :: ZAuth.Env -> Brig -> Http () testInvalidToken z b = do user <- Public.userId <$> randomUser b @@ -1131,6 +1140,7 @@ testRemoveCookiesByLabelAndId b = do listCookies b (userId u) >>= liftIO . ([lbl] @=?) . map cookieLabel -- The testTooManyCookies test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- The test asserts that there is an upper limit for the number of user cookies -- per cookie type. It does that by concurrently attempting to create more @@ -1180,6 +1190,8 @@ testTooManyCookies config b = do ) xxx -> error ("Unexpected status code when logging in: " ++ show xxx) +-- @END + testLogout :: Brig -> Http () testLogout b = do Just email <- userEmail <$> randomUser b diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index 3fc750c12ce..4cc3d7e9648 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -162,6 +162,7 @@ testAddGetClientVerificationCode db brig galley = do const 200 === statusCode const (Just c) === responseJsonMaybe +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that device cannot be added with missing second factor email verification code when this feature is enabled testAddGetClientMissingCode :: Brig -> Galley -> Http () @@ -178,6 +179,9 @@ testAddGetClientMissingCode brig galley = do const 403 === statusCode const (Just "code-authentication-required") === fmap Error.label . responseJsonMaybe +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that device cannot be added with wrong second factor email verification code when this feature is enabled testAddGetClientWrongCode :: Brig -> Galley -> Http () @@ -195,6 +199,9 @@ testAddGetClientWrongCode brig galley = do const 403 === statusCode const (Just "code-authentication-failed") === fmap Error.label . responseJsonMaybe +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that device cannot be added with expired second factor email verification code when this feature is enabled testAddGetClientCodeExpired :: DB.ClientState -> Opt.Opts -> Brig -> Galley -> Http () @@ -218,6 +225,8 @@ testAddGetClientCodeExpired db opts brig galley = do const 403 === statusCode const (Just "code-authentication-failed") === fmap Error.label . responseJsonMaybe +-- @END + data AddGetClient = AddGetClient { addWithPassword :: Bool, addWithMLSKeys :: Bool @@ -895,6 +904,7 @@ testMultiUserGetPrekeysQualifiedV4 brig opts = do const (Right $ expectedUserClientMap) === responseJsonEither -- The testTooManyClients test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- The test validates the upper bound on the number of permanent clients per -- user. It does so by trying to create one permanent client more than allowed. @@ -975,7 +985,10 @@ testRegularPrekeysCannotBeSentAsLastPrekeysDuringUpdate brig = do !!! const 400 === statusCode +-- @END + -- The testRemoveClient test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- This test validates creating and deleting a client. A client is created and -- consequently deleted. Deleting a second time yields response 404 not found. @@ -1021,7 +1034,10 @@ testRemoveClient hasPwd brig cannon = do newClientCookie = Just defCookieLabel } +-- @END + -- The testRemoveClientShortPwd test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- The test checks if a client can be deleted by providing a too short password. -- This is done by using a single-character password, whereas the minimum is 6 @@ -1054,7 +1070,10 @@ testRemoveClientShortPwd brig = do newClientCookie = Just defCookieLabel } +-- @END + -- The testRemoveClientIncorrectPwd test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- The test checks if a client can be deleted by providing a syntax-valid, but -- incorrect password. The client deletion attempt fails with a 403 error @@ -1087,6 +1106,8 @@ testRemoveClientIncorrectPwd brig = do newClientCookie = Just defCookieLabel } +-- @END + testUpdateClient :: Opt.Opts -> Brig -> Http () testUpdateClient opts brig = do uid <- userId <$> randomUser brig @@ -1279,6 +1300,7 @@ testMissingClient brig = do . responseHeaders -- The testAddMultipleTemporary test conforms to the following testing standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- Legacy (galley) -- -- Add temporary client, check that all services (both galley and @@ -1336,6 +1358,8 @@ testAddMultipleTemporary brig galley cannon = do . zUser u pure $ Vec.length <$> (preview _Array =<< responseJsonMaybe @Value r) +-- @END + testPreKeyRace :: Brig -> Http () testPreKeyRace brig = do uid <- userId <$> randomUser brig diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index db34f2f9277..8da3c774ef2 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -69,6 +69,7 @@ tests _cl _at conf p b c g = ] -- The next line contains a mapping from the testHandleUpdate test to the following test standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- -- The test validates various updates to the user's handle. First, it attempts -- to set invalid handles. This fails. Then it successfully sets a valid handle. @@ -139,6 +140,8 @@ testHandleUpdate brig cannon = do put (brig . path "/self/handle" . contentJson . zUser uid2 . zConn "c" . body update) !!! const 200 === statusCode +-- @END + testHandleRace :: Brig -> Http () testHandleRace brig = do us <- replicateM 10 (userId <$> randomUser brig) diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs index 2165edb0761..babbaca3a43 100644 --- a/services/federator/test/integration/Test/Federator/IngressSpec.hs +++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs @@ -75,6 +75,7 @@ spec env = do it "testRejectRequestsWithoutClientCertIngress" (testRejectRequestsWithoutClientCertIngress env) +-- @SF.Federation @TSFI.RESTfulAPI @S2 @S3 @S7 -- -- This test was primarily intended to test that federator is using the API right (header -- name etc.), but it is also effectively testing that federator rejects clients without @@ -110,6 +111,8 @@ testRejectRequestsWithoutClientCertIngress env = runTestFederator env $ do expectationFailure "Expected client certificate error, got remote error" Left (RemoteErrorResponse _ _ status _) -> status `shouldBe` HTTP.status400 +-- @END + liftToCodensity :: (Member (Embed (Codensity IO)) r) => Sem (Embed IO ': r) a -> Sem r a liftToCodensity = runEmbedded @IO @(Codensity IO) lift diff --git a/services/federator/test/integration/Test/Federator/InwardSpec.hs b/services/federator/test/integration/Test/Federator/InwardSpec.hs index 85980f8948c..1daee8e77e4 100644 --- a/services/federator/test/integration/Test/Federator/InwardSpec.hs +++ b/services/federator/test/integration/Test/Federator/InwardSpec.hs @@ -112,11 +112,17 @@ spec env = it "testRejectRequestsWithoutClientCertInward" (testRejectRequestsWithoutClientCertInward env) +-- @SF.Federation @TSFI.RESTfulAPI @S2 @S3 @S7 +-- -- This test is covered by the unit tests 'validateDomainCertWrongDomain' because -- the domain matching is checked on certificate validation. testShouldRejectMissmatchingOriginDomainInward :: TestEnv -> IO () testShouldRejectMissmatchingOriginDomainInward env = runTestFederator env $ pure () +-- @END + +-- @SF.Federation @TSFI.RESTfulAPI @S2 @S3 @S7 +-- -- See related tests in unit tests (for matching client certificates against domain names) -- and "IngressSpec". testRejectRequestsWithoutClientCertInward :: TestEnv -> IO () @@ -129,6 +135,8 @@ testRejectRequestsWithoutClientCertInward env = runTestFederator env $ do (encode hdl) !!! const 400 === statusCode +-- @END + inwardCallWithHeaders :: (MonadIO m, MonadHttp m, MonadReader TestEnv m, HasCallStack) => ByteString -> diff --git a/services/federator/test/unit/Test/Federator/ExternalServer.hs b/services/federator/test/unit/Test/Federator/ExternalServer.hs index ac45f5aae2d..93af2e8ba9c 100644 --- a/services/federator/test/unit/Test/Federator/ExternalServer.hs +++ b/services/federator/test/unit/Test/Federator/ExternalServer.hs @@ -256,6 +256,8 @@ requestNoCertificate = assertEqual "no calls to any service should be made" [] serviceCalls pure Wai.ResponseReceived +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S2 @S3 @S7 +-- Reject request if the client certificate for federator is invalid requestInvalidCertificate :: TestTree requestInvalidCertificate = testCase "testRequestInvalidCertificate - should fail with a 404 when an invalid certificate is given" $ do @@ -274,6 +276,8 @@ requestInvalidCertificate = assertEqual "no calls to any service should be made" [] serviceCalls pure Wai.ResponseReceived +-- @END + testInvalidPaths :: TestTree testInvalidPaths = do let invalidPaths = diff --git a/services/federator/test/unit/Test/Federator/InternalServer.hs b/services/federator/test/unit/Test/Federator/InternalServer.hs index 66706b74f68..db91bf3dfe9 100644 --- a/services/federator/test/unit/Test/Federator/InternalServer.hs +++ b/services/federator/test/unit/Test/Federator/InternalServer.hs @@ -112,6 +112,7 @@ federatedRequestSuccess = body <- Wai.lazyResponseBody res body @?= "\"bar\"" +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S2 @S3 @S7 -- -- Refuse to send outgoing request to non-included domain when AllowDynamic is configured. federatedRequestFailureAllowList :: TestTree @@ -154,3 +155,5 @@ federatedRequestFailureAllowList = . interpretMetricsEmpty $ callOutward targetDomain Brig (RPC "get-user-by-handle") request undefined eith @?= Left (FederationDenied targetDomain) + +-- @END diff --git a/services/federator/test/unit/Test/Federator/Options.hs b/services/federator/test/unit/Test/Federator/Options.hs index bece8365ab0..137b6d411e5 100644 --- a/services/federator/test/unit/Test/Federator/Options.hs +++ b/services/federator/test/unit/Test/Federator/Options.hs @@ -163,6 +163,8 @@ testSettings = assertFailure "expected failure for invalid private key, got success" ] +-- @SF.Federation @TSFI.Federate @S3 @S7 +-- failToStartWithInvalidServerCredentials :: IO () failToStartWithInvalidServerCredentials = do let settings = @@ -186,6 +188,8 @@ failToStartWithInvalidServerCredentials = do Right _ -> assertFailure "expected failure for invalid client certificate, got success" +-- @END + assertParsesAs :: (HasCallStack, Eq a, FromJSON a, Show a) => a -> ByteString -> Assertion assertParsesAs v bs = assertEqual "YAML parsing" (Right v) $ diff --git a/services/federator/test/unit/Test/Federator/Remote.hs b/services/federator/test/unit/Test/Federator/Remote.hs index 0a8b92e432a..9409a0c9f45 100644 --- a/services/federator/test/unit/Test/Federator/Remote.hs +++ b/services/federator/test/unit/Test/Federator/Remote.hs @@ -130,6 +130,7 @@ testValidatesCertificateSuccess = Right _ -> assertFailure "Congratulations, you fixed a known issue!" ] +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S2 -- -- This is a group of test cases where refusing to connect with the server is -- checked. The second test case refuses to connect with a server when the @@ -155,6 +156,8 @@ testValidatesCertificateWrongHostname = Right _ -> assertFailure "Expected connection with the server to fail" ] +-- @END + testConnectionError :: TestTree testConnectionError = testCase "connection failures are reported correctly" $ do tlsSettings <- mkTLSSettingsOrThrow settings diff --git a/services/federator/test/unit/Test/Federator/Validation.hs b/services/federator/test/unit/Test/Federator/Validation.hs index bd2c882c0e7..1a36f2f6644 100644 --- a/services/federator/test/unit/Test/Federator/Validation.hs +++ b/services/federator/test/unit/Test/Federator/Validation.hs @@ -115,6 +115,7 @@ federateWithAllowListFail = $ ensureCanFederateWith (Domain "hello.world") assertBool "federating should not be allowed" (isLeft eith) +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S2 @S3 @S7 -- -- Refuse to send outgoing request to non-included domain when AllowDynamic is configured. validateDomainAllowListFail :: TestTree @@ -132,6 +133,8 @@ validateDomainAllowListFail = $ validateDomain exampleCert (Domain "localhost.example.com") res @?= Left (FederationDenied (Domain "localhost.example.com")) +-- @END + validateDomainAllowListSuccess :: TestTree validateDomainAllowListSuccess = testCase "should give parsed domain if in the allow list" $ do @@ -148,6 +151,7 @@ validateDomainAllowListSuccess = $ validateDomain exampleCert domain assertEqual "validateDomain should give 'localhost.example.com' as domain" domain res +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S3 @S7 -- -- Reject request if the infrastructure domain in the client cert does not match the backend -- domain in the `Wire-origin-domain` header. @@ -165,6 +169,8 @@ validateDomainCertWrongDomain = $ validateDomain exampleCert (Domain "foo.example.com") res @?= Left (AuthenticationFailure (pure [X509.NameMismatch "foo.example.com"])) +-- @END + validateDomainCertCN :: TestTree validateDomainCertCN = testCase "should succeed if the certificate has subject CN but no SAN" $ do @@ -247,9 +253,12 @@ validateDomainNonIdentitySRV = $ validateDomain exampleCert domain res @?= domain +-- @SF.Federation @TSFI.Federate @TSFI.DNS @S2 @S3 @S7 -- Reject request if the client certificate for federator is invalid validateDomainCertInvalid :: TestTree validateDomainCertInvalid = testCase "validateDomainCertInvalid - should fail if the client certificate is invalid" $ do let res = decodeCertificate "not a certificate" res @?= Left "no certificate found" + +-- @END diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 2a6c1f3a8bf..f05541d3a76 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -408,6 +408,7 @@ postConvWithUnreachableRemoteUsers rbs = do groupConvs WS.assertNoEvent (3 # Second) [wsAlice, wsAlex] +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- This test verifies whether a message actually gets sent all the way to -- cannon. postCryptoMessageVerifyMsgSentAndRejectIfMissingClient :: TestM () @@ -496,6 +497,9 @@ postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do liftIO $ assertBool "unexpected equal clients" (bc /= bc2) assertNoMsg wsB2 (wsAssertOtr qconv qalice ac bc cipher) +-- @END + +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- This test verifies basic mismatch behavior of the the JSON endpoint. postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysJson :: TestM () postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysJson = do @@ -521,6 +525,9 @@ postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysJson = do Map.keys (userClientMap (getUserClientPrekeyMap p)) @=? [eve] Map.keys <$> Map.lookup eve (userClientMap (getUserClientPrekeyMap p)) @=? Just [ec] +-- @END + +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- This test verifies basic mismatch behaviour of the protobuf endpoint. postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysProto :: TestM () postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysProto = do @@ -548,6 +555,8 @@ postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysProto = do Map.keys (userClientMap (getUserClientPrekeyMap p)) @=? [eve] Map.keys <$> Map.lookup eve (userClientMap (getUserClientPrekeyMap p)) @=? Just [ec] +-- @END + -- | This test verifies behaviour when an unknown client posts the message. Only -- tests the Protobuf endpoint. postCryptoMessageNotAuthorizeUnknownClient :: TestM () @@ -563,6 +572,7 @@ postCryptoMessageNotAuthorizeUnknownClient = do postProtoOtrMessage alice (ClientId 0x172618352518396) conv m !!! const 403 === statusCode +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- This test verifies the following scenario. -- A client sends a message to all clients of a group and one more who is not part of the group. -- The server must not send this message to client ids not part of the group. @@ -588,6 +598,9 @@ postMessageClientNotInGroupDoesNotReceiveMsg = do checkEveGetsMsg checkChadDoesNotGetMsg +-- @END + +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- This test verifies that when a client sends a message not to all clients of a group then the server should reject the message and sent a notification to the sender (412 Missing clients). -- The test is somewhat redundant because this is already tested as part of other tests already. This is a stand alone test that solely tests the behavior described above. postMessageRejectIfMissingClients :: TestM () @@ -615,6 +628,9 @@ postMessageRejectIfMissingClients = do mkMsg :: ByteString -> (UserId, ClientId) -> (UserId, ClientId, Text) mkMsg text (uid, clientId) = (uid, clientId, toBase64Text text) +-- @END + +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- This test verifies behaviour under various values of ignore_missing and -- report_missing. Only tests the JSON endpoint. postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam :: TestM () @@ -672,6 +688,9 @@ postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam = do where listToByteString = BS.intercalate "," . map toByteString' +-- @END + +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- Sets up a conversation on Backend A known as "owning backend". One of the -- users from Backend A will send the message but have a missing client. It is -- expected that the message will not be sent. @@ -732,6 +751,8 @@ postMessageQualifiedLocalOwningBackendMissingClients = do assertMismatchQualified mempty expectedMissing mempty mempty mempty WS.assertNoEvent (1 # Second) [wsBob, wsChad] +-- @END + -- | Sets up a conversation on Backend A known as "owning backend". One of the -- users from Backend A will send the message, it is expected that message will -- be sent successfully. @@ -822,6 +843,7 @@ postMessageQualifiedLocalOwningBackendRedundantAndDeletedClients = do -- Wait less for no message WS.assertNoEvent (1 # Second) [wsNonMember] +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- Sets up a conversation on Backend A known as "owning backend". One of the -- users from Backend A will send the message but have a missing client. It is -- expected that the message will be sent except when it is specifically @@ -948,6 +970,8 @@ postMessageQualifiedLocalOwningBackendIgnoreMissingClients = do assertMismatchQualified mempty expectedMissing mempty mempty mempty WS.assertNoEvent (1 # Second) [wsBob, wsChad] +-- @END + postMessageQualifiedLocalOwningBackendFailedToSendClients :: TestM () postMessageQualifiedLocalOwningBackendFailedToSendClients = do -- WS receive timeout @@ -1177,6 +1201,7 @@ testPostCodeRejectedIfGuestLinksDisabled = do setStatus Public.FeatureStatusEnabled checkPostCode 200 +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- Check if guests cannot join anymore if guest invite feature was disabled on team level testJoinTeamConvGuestLinksDisabled :: TestM () testJoinTeamConvGuestLinksDisabled = do @@ -1234,6 +1259,8 @@ testJoinTeamConvGuestLinksDisabled = do postJoinCodeConv bob' cCode !!! const 200 === statusCode checkFeatureStatus Public.FeatureStatusEnabled +-- @END + testJoinNonTeamConvGuestLinksDisabled :: TestM () testJoinNonTeamConvGuestLinksDisabled = do let convName = "testConversation" @@ -1257,6 +1284,7 @@ testJoinNonTeamConvGuestLinksDisabled = do const (Right (ConversationCoverView convId (Just convName) False)) === responseJsonEither const 200 === statusCode +-- @SF.Separation @TSFI.RESTfulAPI @S2 -- This test case covers a negative check that if access code of a guest link is revoked no further -- people can join the group conversation. Additionally it covers: -- Random users can use invite link @@ -1311,6 +1339,8 @@ postJoinCodeConvOk = do putQualifiedAccessUpdate alice qconv noCodeAccess !!! const 200 === statusCode postJoinCodeConv dave payload !!! const 404 === statusCode +-- @END + postJoinCodeConvWithPassword :: TestM () postJoinCodeConvWithPassword = do alice <- randomUser diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 2f302fe6e17..5a3e0221fff 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -159,6 +159,7 @@ getConversationsAllFound = do (Just (sort [bob, qUnqualified carlQ])) (fmap (sort . map (qUnqualified . omQualifiedId) . (.members.others)) c2) +-- @SF.Federation @TSFI.RESTfulAPI @S2 -- -- The test asserts that via a federation client a user cannot fetch -- conversation details of a conversation they are not part of: they get an @@ -187,6 +188,8 @@ getConversationsNotPartOf = do GetConversationsRequest rando [qUnqualified . cnvQualifiedId $ cnv1] liftIO $ assertEqual "conversation list not empty" [] convs +-- @END + onConvCreated :: TestM () onConvCreated = do c <- view tsCannon diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index cad9536576d..8aca27809c2 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -1044,6 +1044,7 @@ testDeleteTeamVerificationCodeSuccess = do const 202 === statusCode assertTeamDelete 10 "team delete, should be there" tid +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that team cannot be deleted with missing second factor email verification code when this feature is enabled testDeleteTeamVerificationCodeMissingCode :: TestM () @@ -1065,6 +1066,9 @@ testDeleteTeamVerificationCodeMissingCode = do const 403 === statusCode const "code-authentication-required" === (Error.label . responseJsonUnsafeWithMsg "error label") +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that team cannot be deleted with expired second factor email verification code when this feature is enabled testDeleteTeamVerificationCodeExpiredCode :: TestM () @@ -1089,6 +1093,9 @@ testDeleteTeamVerificationCodeExpiredCode = do const 403 === statusCode const "code-authentication-failed" === (Error.label . responseJsonUnsafeWithMsg "error label") +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 -- -- Test that team cannot be deleted with wrong second factor email verification code when this feature is enabled testDeleteTeamVerificationCodeWrongCode :: TestM () @@ -1111,6 +1118,8 @@ testDeleteTeamVerificationCodeWrongCode = do const 403 === statusCode const "code-authentication-failed" === (Error.label . responseJsonUnsafeWithMsg "error label") +-- @END + setFeatureLockStatus :: forall cfg. (KnownSymbol (Public.FeatureSymbol cfg)) => TeamId -> Public.LockStatus -> TestM () setFeatureLockStatus tid status = do g <- viewGalley @@ -1388,6 +1397,7 @@ testBillingInLargeTeam = do assertTeamUpdate ("delete fanoutLimit + 3rd billing member: " <> show ownerFanoutPlusThree) team (fanoutLimit + 2) (allOwnersBeforeFanoutLimit <> [ownerFanoutPlusTwo]) refreshIndex +-- | @SF.Management @TSFI.RESTfulAPI @S2 -- This test covers: -- Promotion, demotion of team roles. -- Demotion by superior roles is allowed. @@ -1454,6 +1464,8 @@ testUpdateTeamMember = do e ^. eventTeam @?= tid e ^. eventData @?= EdMemberUpdate uid mPerm +-- @END + testUpdateTeamStatus :: TestM () testUpdateTeamStatus = do g <- viewGalley diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index d953b064fa8..07670e1d3f4 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -293,6 +293,7 @@ specFinalizeLogin = do authnresp <- runSimpleSP $ mkAuthnResponse privcreds idp3 spmeta authnreq True loginSuccess =<< submitAuthnResponse tid3 authnresp + -- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 -- Do not authenticate if SSO IdP response is for different team context "rejectsSAMLResponseInWrongTeam" $ do it "fails" $ do @@ -319,6 +320,8 @@ specFinalizeLogin = do authnresp <- runSimpleSP $ mkAuthnResponseWithSubj subj privcreds idp2 spmeta authnreq True loginFailure =<< submitAuthnResponse tid2 authnresp + -- @END + context "user is created once, then deleted in team settings, then can login again." $ do it "responds with 'allowed'" $ do (ownerid, teamid) <- callCreateUserWithTeam @@ -1662,6 +1665,7 @@ specReAuthSsoUserWithPassword = ---------------------------------------------------------------------- -- tests for bsi audit +-- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 testRejectsSAMLResponseSayingAccessNotGranted :: TestSpar () testRejectsSAMLResponseSayingAccessNotGranted = do (user, tid) <- callCreateUserWithTeam @@ -1683,6 +1687,10 @@ testRejectsSAMLResponseSayingAccessNotGranted = do bdy `shouldContain` "}, receiverOrigin)" hasPersistentCookieHeader sparresp `shouldBe` Left "no set-cookie header" +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 +-- -- Do not authenticate if SSO IdP response is for unknown issuer testRejectsSAMLResponseFromWrongIssuer :: TestSpar () testRejectsSAMLResponseFromWrongIssuer = do @@ -1707,6 +1715,10 @@ testRejectsSAMLResponseFromWrongIssuer = do submitaresp checkresp +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 +-- -- Do not authenticate if SSO IdP response is signed with wrong key testRejectsSAMLResponseSignedWithWrongKey :: TestSpar () testRejectsSAMLResponseSignedWithWrongKey = do @@ -1724,6 +1736,10 @@ testRejectsSAMLResponseSignedWithWrongKey = do checkresp sparresp = statusCode sparresp `shouldBe` 400 checkSamlFlow mkareq mkaresp submitaresp checkresp +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 +-- -- Do not authenticate if SSO IdP response has no corresponding request anymore testRejectsSAMLResponseIfRequestIsStale :: TestSpar () testRejectsSAMLResponseIfRequestIsStale = do @@ -1739,6 +1755,10 @@ testRejectsSAMLResponseIfRequestIsStale = do (cs . fromJust . responseBody $ sparresp) `shouldContain` "bad InResponseTo attribute(s)" checkSamlFlow mkareq mkaresp submitaresp checkresp +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 +-- -- Do not authenticate if SSO IdP response is gone missing testRejectsSAMLResponseIfResponseIsStale :: TestSpar () testRejectsSAMLResponseIfResponseIsStale = do @@ -1752,6 +1772,8 @@ testRejectsSAMLResponseIfResponseIsStale = do (cs . fromJust . responseBody $ sparresp) `shouldContain` "wire:sso:error:forbidden" checkSamlFlow mkareq mkaresp submitaresp checkresp +-- @END + ---------------------------------------------------------------------- -- Helpers diff --git a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs index 0d9ace470ec..69a064400cf 100644 --- a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs @@ -106,6 +106,8 @@ testCreateToken = do listUsers_ (Just token) (Just fltr) (env ^. teSpar) !!! const 200 === statusCode +-- @SF.Channel @TSFI.RESTfulAPI @S2 +-- -- Test positive case but also that a SCIM token cannot be created with wrong -- or missing second factor email verification code when this feature is enabled testCreateTokenWithVerificationCode :: TestSpar () @@ -141,6 +143,8 @@ testCreateTokenWithVerificationCode = do call $ post (brig . paths ["verification-code", "send"] . contentJson . json (Public.SendVerificationCode action email)) +-- @END + unlockFeature :: GalleyReq -> TeamId -> TestSpar () unlockFeature galley tid = call $ put (galley . paths ["i", "teams", toByteString' tid, "features", featureNameBS @Public.SndFactorPasswordChallengeConfig, toByteString' Public.LockStatusUnlocked]) !!! const 200 === statusCode @@ -219,6 +223,7 @@ testNumIdPs = do createToken_ owner (CreateScimToken "drei" (Just defPassword) Nothing) (env ^. teSpar) !!! checkErr 400 (Just "more-than-one-idp") +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- Test that a token can only be created as a team owner testCreateTokenAuthorizesOnlyAdmins :: TestSpar () testCreateTokenAuthorizesOnlyAdmins = do @@ -251,6 +256,8 @@ testCreateTokenAuthorizesOnlyAdmins = do (mkUser RoleAdmin >>= createToken') !!! const 200 === statusCode +-- @END + -- | Test that for a user with a password, token creation requires reauthentication (i.e. the -- field @"password"@ should be provided). -- @@ -449,6 +456,7 @@ testDeletedTokensAreUnlistable = do ---------------------------------------------------------------------------- -- Miscellaneous tests +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- This test verifies that the SCIM API responds with an authentication error -- and can't be used if it receives an invalid secret token -- or if no token is provided at all @@ -460,3 +468,5 @@ testAuthIsNeeded = do listUsers_ (Just invalidToken) Nothing (env ^. teSpar) !!! checkErr 401 Nothing -- Try to do @GET /Users@ without a token and check that it fails listUsers_ Nothing Nothing (env ^. teSpar) !!! checkErr 401 Nothing + +-- @END diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 4ac175f5afb..e138bc76db5 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -840,6 +840,9 @@ testExternalIdIsRequired = do createUser_ (Just tok) user' (env ^. teSpar) !!! const 400 === statusCode +-- The next line contains a mapping from this test to the following test standards: +-- @SF.Provisioning @TSFI.RESTfulAPI @S2 +-- -- Test that user creation fails if handle is invalid testCreateRejectsInvalidHandle :: TestSpar () testCreateRejectsInvalidHandle = do @@ -850,6 +853,8 @@ testCreateRejectsInvalidHandle = do createUser_ (Just tok) (user {Scim.User.userName = "#invalid name"}) (env ^. teSpar) !!! const 400 === statusCode +-- @END + -- | Test that user creation fails if handle is already in use (even on different team). testCreateRejectsTakenHandle :: TestSpar () testCreateRejectsTakenHandle = do From 3f9526104fa0e5f1c177868fdfa2a6c93ea02fda Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Mon, 12 Aug 2024 10:23:28 +0200 Subject: [PATCH 032/136] [chore] Deleted dangling phone references in Brig (#4197) * [chore] Deleted dangling phone references in Brig * Removed use site. --- services/brig/src/Brig/API/Public.hs | 2 +- services/brig/src/Brig/API/User.hs | 16 +++++----------- services/brig/src/Brig/Data/User.hs | 2 +- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 5ba6a022565..ea39dc9355a 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -901,7 +901,7 @@ changePhone :: changePhone _ _ _ = pure . Just $ Public.InvalidNewPhone removePhone :: UserId -> Handler r (Maybe Public.RemoveIdentityError) -removePhone self = lift . exceptTToMaybe $ API.removePhone self +removePhone _ = (lift . pure) Nothing removeEmail :: ( Member (Embed HttpClientIO) r, diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 2d865be62c6..b56a8f29834 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -40,7 +40,6 @@ module Brig.API.User Data.lookupRichInfo, Data.lookupRichInfoMultiUsers, removeEmail, - removePhone, revokeIdentity, deleteUserNoVerify, deleteUsersNoVerify, @@ -277,7 +276,7 @@ createUser :: NewUser -> ExceptT RegisterError (AppT r) CreateUserResult createUser new = do - email <- validateEmailAndPhone new + email <- fetchAndValidateEmail new -- get invitation and existing account (mNewTeamUser, teamInvitation, tid) <- @@ -374,8 +373,8 @@ createUser new = do where -- NOTE: all functions in the where block don't use any arguments of createUser - validateEmailAndPhone :: NewUser -> ExceptT RegisterError (AppT r) (Maybe Email) - validateEmailAndPhone newUser = do + fetchAndValidateEmail :: NewUser -> ExceptT RegisterError (AppT r) (Maybe Email) + fetchAndValidateEmail newUser = do -- Validate e-mail email <- for (newUserEmail newUser) $ \e -> either @@ -607,11 +606,6 @@ removeEmail uid = do Nothing -> throwE NoIdentity ------------------------------------------------------------------------------- --- Remove Phone - --- | Phones are not supported any longer. -removePhone :: UserId -> ExceptT RemoveIdentityError (AppT r) () -removePhone _uid = pure () ------------------------------------------------------------------------------- -- Forcefully revoke a verified identity @@ -875,7 +869,7 @@ changePassword uid cp = do -- User Deletion -- | Initiate validation of a user's delete request. Called via @delete /self@. Users with an --- 'UserSSOId' can still do this if they also have an 'Email', 'Phone', and/or password. Otherwise, +-- 'UserSSOId' can still do this if they also have an 'Email' and/or password. Otherwise, -- the team admin has to delete them via the team console on galley. -- -- Owners are not allowed to delete themselves. Instead, they must ask a fellow owner to @@ -924,7 +918,7 @@ deleteSelfUser uid pwd = do when isOwner $ throwE DeleteUserOwnerDeletingSelf go a = maybe (byIdentity a) (byPassword a) pwd byIdentity a = case emailIdentity =<< userIdentity (accountUser a) of - Just emailOrPhone -> sendCode a emailOrPhone + Just email -> sendCode a email Nothing -> case pwd of Just _ -> throwE DeleteUserMissingPassword Nothing -> lift . liftSem $ deleteAccount a >> pure Nothing diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 147fd666c7a..91f77d4c76a 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -709,7 +709,7 @@ toLocale l _ = l -- activated. -- -- The reason it's just a "precaution" is that we /also/ have an invariant that having an --- email or phone in the database means the user has to be activated. +-- email in the database means the user has to be activated. toIdentity :: -- | Whether the user is activated Bool -> From a594d1037cd4dc8fd78e7e19ddd67b7c680149aa Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 12 Aug 2024 10:27:21 +0200 Subject: [PATCH 033/136] tools/{hlint,ormolu}.sh: Allow having different base for PR than origin/develop (#4187) --- tools/hlint.sh | 4 +++- tools/ormolu.sh | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/hlint.sh b/tools/hlint.sh index e03ecb7161a..067a75e01ce 100755 --- a/tools/hlint.sh +++ b/tools/hlint.sh @@ -5,6 +5,8 @@ usage() { echo "Usage: $0 -f [all, changeset, pr] -m [check, inplace]" 1>&2; exi files='' +PR_BASE=${PR_BASE:-"origin/develop"} + while getopts ':f:m:k' opt do case $opt in @@ -12,7 +14,7 @@ while getopts ':f:m:k' opt if [ "$f" = "all" ]; then files=$(git ls-files | grep \.hs\$) elif [ "$f" = "pr" ]; then - files=$(git diff --name-only origin/develop... | grep \.hs\$) + files=$(git diff --name-only "$PR_BASE"... | grep \.hs\$) elif [ "$f" = "changeset" ]; then files=$(git diff --name-only HEAD | grep \.hs\$) else diff --git a/tools/ormolu.sh b/tools/ormolu.sh index 901baa11e0d..de3e4fc73b6 100755 --- a/tools/ormolu.sh +++ b/tools/ormolu.sh @@ -6,6 +6,7 @@ cd "$( dirname "${BASH_SOURCE[0]}" )/.." ALLOW_DIRTY_WC="0" ARG_ORMOLU_MODE="inplace" +PR_BASE=${PR_BASE:-"origin/develop"} USAGE=" This bash script can either @@ -73,7 +74,7 @@ fi if [ "$f" = "all" ] || [ "$f" = "" ]; then files=$(git ls-files | grep '\.hsc\?$') elif [ "$f" = "pr" ]; then - files=$(git diff --diff-filter=ACMR --name-only origin/develop... | { grep '\.hsc\?$' || true; }; git diff --diff-filter=ACMR --name-only HEAD | { grep \.hs\$ || true ; }) + files=$(git diff --diff-filter=ACMR --name-only "$PR_BASE"... | { grep '\.hsc\?$' || true; }; git diff --diff-filter=ACMR --name-only HEAD | { grep \.hs\$ || true ; }) fi count=$( echo "$files" | sed '/^\s*$/d' | wc -l ) From cdeeb0bc5024d66e49a3098a72fd72b4e3ba2a2d Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 12 Aug 2024 10:45:00 +0200 Subject: [PATCH 034/136] Feature flag refactoring (part 1) (#4181) --- .gitignore | 3 + .../5-internal/feature-flag-refactoring-1 | 7 + integration/test/Test/FeatureFlags.hs | 5 +- libs/galley-types/default.nix | 2 + libs/galley-types/galley-types.cabal | 1 + libs/galley-types/src/Galley/Types/Teams.hs | 45 +- .../src/Wire/API/Event/FeatureConfig.hs | 49 +- .../src/Wire/API/Routes/Internal/Brig.hs | 6 +- .../src/Wire/API/Routes/Internal/Galley.hs | 10 +- .../src/Wire/API/Routes/Internal/LegalHold.hs | 6 +- .../src/Wire/API/Routes/Public/Galley.hs | 2 +- .../src/Wire/API/Routes/Public/Galley/Bot.hs | 2 +- .../API/Routes/Public/Galley/Conversation.hs | 4 +- .../API/Routes/Public/Galley/CustomBackend.hs | 2 +- .../Wire/API/Routes/Public/Galley/Feature.hs | 16 +- .../API/Routes/Public/Galley/LegalHold.hs | 2 +- .../src/Wire/API/Routes/Public/Galley/MLS.hs | 2 +- .../API/Routes/Public/Galley/Messaging.hs | 2 +- .../src/Wire/API/Routes/Public/Galley/Team.hs | 2 +- .../Routes/Public/Galley/TeamConversation.hs | 2 +- .../API/Routes/Public/Galley/TeamMember.hs | 2 +- libs/wire-api/src/Wire/API/Team/Feature.hs | 682 +++++++++--------- .../golden/Test/Wire/API/Golden/FromJSON.hs | 6 +- .../golden/Test/Wire/API/Golden/Generated.hs | 200 ++--- .../Wire/API/Golden/Generated/Feature_team.hs | 75 ++ .../Generated/LockableFeaturePatch_team.hs | 81 +++ .../Golden/Generated/LockableFeature_team.hs | 104 +++ .../Golden/Generated/WithStatusNoLock_team.hs | 75 -- .../Golden/Generated/WithStatusPatch_team.hs | 84 --- .../API/Golden/Generated/WithStatus_team.hs | 108 --- ...> testObject_LockableFeature_team_14.json} | 0 ..._1.json => testObject_Feature_team_1.json} | 0 ...0.json => testObject_Feature_team_10.json} | 0 ...1.json => testObject_Feature_team_11.json} | 0 ...2.json => testObject_Feature_team_12.json} | 0 ...3.json => testObject_Feature_team_13.json} | 0 ...4.json => testObject_Feature_team_14.json} | 0 ...5.json => testObject_Feature_team_15.json} | 0 ...6.json => testObject_Feature_team_16.json} | 0 ...7.json => testObject_Feature_team_17.json} | 0 ..._2.json => testObject_Feature_team_2.json} | 0 ..._3.json => testObject_Feature_team_3.json} | 0 ..._4.json => testObject_Feature_team_4.json} | 0 ..._5.json => testObject_Feature_team_5.json} | 0 ..._6.json => testObject_Feature_team_6.json} | 0 ..._7.json => testObject_Feature_team_7.json} | 0 ..._8.json => testObject_Feature_team_8.json} | 0 ..._9.json => testObject_Feature_team_9.json} | 0 ...stObject_LockableFeaturePatch_team_1.json} | 0 ...tObject_LockableFeaturePatch_team_10.json} | 0 ...tObject_LockableFeaturePatch_team_11.json} | 0 ...tObject_LockableFeaturePatch_team_12.json} | 0 ...tObject_LockableFeaturePatch_team_13.json} | 0 ...tObject_LockableFeaturePatch_team_14.json} | 0 ...tObject_LockableFeaturePatch_team_15.json} | 0 ...tObject_LockableFeaturePatch_team_16.json} | 0 ...tObject_LockableFeaturePatch_team_17.json} | 0 ...tObject_LockableFeaturePatch_team_18.json} | 0 ...tObject_LockableFeaturePatch_team_19.json} | 0 ...stObject_LockableFeaturePatch_team_2.json} | 0 ...stObject_LockableFeaturePatch_team_3.json} | 0 ...stObject_LockableFeaturePatch_team_4.json} | 0 ...stObject_LockableFeaturePatch_team_5.json} | 0 ...stObject_LockableFeaturePatch_team_6.json} | 0 ...stObject_LockableFeaturePatch_team_7.json} | 0 ...stObject_LockableFeaturePatch_team_8.json} | 0 ...stObject_LockableFeaturePatch_team_9.json} | 0 ...=> testObject_LockableFeature_team_1.json} | 0 ...> testObject_LockableFeature_team_10.json} | 0 ...> testObject_LockableFeature_team_11.json} | 0 ...> testObject_LockableFeature_team_12.json} | 0 ...> testObject_LockableFeature_team_13.json} | 0 ...> testObject_LockableFeature_team_14.json} | 0 ...> testObject_LockableFeature_team_15.json} | 0 ...> testObject_LockableFeature_team_16.json} | 0 ...> testObject_LockableFeature_team_17.json} | 0 ...> testObject_LockableFeature_team_18.json} | 0 ...> testObject_LockableFeature_team_19.json} | 0 ...=> testObject_LockableFeature_team_2.json} | 0 ...=> testObject_LockableFeature_team_3.json} | 0 ...=> testObject_LockableFeature_team_4.json} | 0 ...=> testObject_LockableFeature_team_5.json} | 0 ...=> testObject_LockableFeature_team_6.json} | 0 ...=> testObject_LockableFeature_team_7.json} | 0 ...=> testObject_LockableFeature_team_8.json} | 0 ...=> testObject_LockableFeature_team_9.json} | 0 .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 8 +- libs/wire-api/test/unit/Test/Wire/API/Run.hs | 4 +- .../test/unit/Test/Wire/API/Team/Feature.hs | 92 --- libs/wire-api/wire-api.cabal | 7 +- .../src/Wire/GalleyAPIAccess.hs | 2 +- .../src/Wire/GalleyAPIAccess/Rpc.hs | 6 +- .../src/Wire/UserSubsystem/Interpreter.hs | 3 +- .../test/unit/Wire/MiniBackend.hs | 2 + .../Wire/UserSubsystem/InterpreterSpec.hs | 25 +- services/brig/src/Brig/API/Internal.hs | 4 +- services/brig/src/Brig/Calling/API.hs | 4 +- services/brig/src/Brig/Data/User.hs | 12 +- services/brig/src/Brig/Options.hs | 16 +- services/brig/src/Brig/Provider/API.hs | 8 +- services/brig/src/Brig/User/Auth.hs | 7 +- .../brig/test/integration/API/Provider.hs | 2 +- services/brig/test/integration/API/Team.hs | 3 +- .../brig/test/integration/API/Team/Util.hs | 6 +- .../brig/test/integration/API/User/Account.hs | 5 +- .../brig/test/integration/API/User/Util.hs | 2 +- services/galley/default.nix | 10 +- services/galley/galley.cabal | 9 +- services/galley/src/Galley/API/Internal.hs | 6 +- .../galley/src/Galley/API/LegalHold/Team.hs | 4 +- .../galley/src/Galley/API/MLS/Migration.hs | 7 +- services/galley/src/Galley/API/Query.hs | 8 +- services/galley/src/Galley/API/Teams.hs | 2 +- .../galley/src/Galley/API/Teams/Features.hs | 107 +-- .../src/Galley/API/Teams/Features/Get.hs | 203 ++---- services/galley/src/Galley/App.hs | 4 +- .../galley/src/Galley/Cassandra/FeatureTH.hs | 53 ++ .../Cassandra/GetAllTeamFeatureConfigs.hs | 444 ++---------- .../src/Galley/Cassandra/MakeFeature.hs | 463 ++++++++++++ .../galley/src/Galley/Cassandra/Orphans.hs | 8 + .../src/Galley/Cassandra/TeamFeatures.hs | 266 +------ .../galley/src/Galley/Effects/BrigAccess.hs | 2 +- .../src/Galley/Effects/TeamFeatureStore.hs | 9 +- services/galley/src/Galley/Intra/User.hs | 2 +- services/galley/test/integration/API.hs | 26 +- services/galley/test/integration/API/Teams.hs | 16 +- .../API/Teams/LegalHold/DisabledByDefault.hs | 25 +- .../integration/API/Teams/LegalHold/Util.hs | 2 +- .../test/integration/API/Util/TeamFeature.hs | 10 +- services/spar/src/Spar/Intra/Galley.hs | 6 +- .../Test/Spar/Scim/AuthSpec.hs | 2 +- services/spar/test-integration/Util/Core.hs | 2 +- services/spar/test-integration/Util/Email.hs | 2 +- tools/stern/default.nix | 2 + tools/stern/src/Stern/API.hs | 16 +- tools/stern/src/Stern/API/Routes.hs | 4 +- tools/stern/src/Stern/Intra.hs | 38 +- tools/stern/stern.cabal | 1 + tools/stern/test/integration/API.hs | 61 +- 139 files changed, 1719 insertions(+), 1906 deletions(-) create mode 100644 changelog.d/5-internal/feature-flag-refactoring-1 create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Feature_team.hs create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeaturePatch_team.hs create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs delete mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusNoLock_team.hs delete mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs delete mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs rename libs/wire-api/test/golden/fromJSON/{testObject_WithStatus_team_14.json => testObject_LockableFeature_team_14.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_1.json => testObject_Feature_team_1.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_10.json => testObject_Feature_team_10.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_11.json => testObject_Feature_team_11.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_12.json => testObject_Feature_team_12.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_13.json => testObject_Feature_team_13.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_14.json => testObject_Feature_team_14.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_15.json => testObject_Feature_team_15.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_16.json => testObject_Feature_team_16.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_17.json => testObject_Feature_team_17.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_2.json => testObject_Feature_team_2.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_3.json => testObject_Feature_team_3.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_4.json => testObject_Feature_team_4.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_5.json => testObject_Feature_team_5.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_6.json => testObject_Feature_team_6.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_7.json => testObject_Feature_team_7.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_8.json => testObject_Feature_team_8.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusNoLock_team_9.json => testObject_Feature_team_9.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_1.json => testObject_LockableFeaturePatch_team_1.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_10.json => testObject_LockableFeaturePatch_team_10.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_11.json => testObject_LockableFeaturePatch_team_11.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_12.json => testObject_LockableFeaturePatch_team_12.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_13.json => testObject_LockableFeaturePatch_team_13.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_14.json => testObject_LockableFeaturePatch_team_14.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_15.json => testObject_LockableFeaturePatch_team_15.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_16.json => testObject_LockableFeaturePatch_team_16.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_17.json => testObject_LockableFeaturePatch_team_17.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_18.json => testObject_LockableFeaturePatch_team_18.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_19.json => testObject_LockableFeaturePatch_team_19.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_2.json => testObject_LockableFeaturePatch_team_2.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_3.json => testObject_LockableFeaturePatch_team_3.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_4.json => testObject_LockableFeaturePatch_team_4.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_5.json => testObject_LockableFeaturePatch_team_5.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_6.json => testObject_LockableFeaturePatch_team_6.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_7.json => testObject_LockableFeaturePatch_team_7.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_8.json => testObject_LockableFeaturePatch_team_8.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatusPatch_team_9.json => testObject_LockableFeaturePatch_team_9.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_1.json => testObject_LockableFeature_team_1.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_10.json => testObject_LockableFeature_team_10.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_11.json => testObject_LockableFeature_team_11.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_12.json => testObject_LockableFeature_team_12.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_13.json => testObject_LockableFeature_team_13.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_14.json => testObject_LockableFeature_team_14.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_15.json => testObject_LockableFeature_team_15.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_16.json => testObject_LockableFeature_team_16.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_17.json => testObject_LockableFeature_team_17.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_18.json => testObject_LockableFeature_team_18.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_19.json => testObject_LockableFeature_team_19.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_2.json => testObject_LockableFeature_team_2.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_3.json => testObject_LockableFeature_team_3.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_4.json => testObject_LockableFeature_team_4.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_5.json => testObject_LockableFeature_team_5.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_6.json => testObject_LockableFeature_team_6.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_7.json => testObject_LockableFeature_team_7.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_8.json => testObject_LockableFeature_team_8.json} (100%) rename libs/wire-api/test/golden/{testObject_WithStatus_team_9.json => testObject_LockableFeature_team_9.json} (100%) delete mode 100644 libs/wire-api/test/unit/Test/Wire/API/Team/Feature.hs create mode 100644 services/galley/src/Galley/Cassandra/FeatureTH.hs create mode 100644 services/galley/src/Galley/Cassandra/MakeFeature.hs create mode 100644 services/galley/src/Galley/Cassandra/Orphans.hs diff --git a/.gitignore b/.gitignore index a6318e378e3..c5be8f38512 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,9 @@ stack-dev.yaml # HIE db files (e.g. generated for stan) *.hie +# dump timings +*.dump-timings + # generated files under .local .local diff --git a/changelog.d/5-internal/feature-flag-refactoring-1 b/changelog.d/5-internal/feature-flag-refactoring-1 new file mode 100644 index 00000000000..92f0a33d35a --- /dev/null +++ b/changelog.d/5-internal/feature-flag-refactoring-1 @@ -0,0 +1,7 @@ +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/integration/test/Test/FeatureFlags.hs b/integration/test/Test/FeatureFlags.hs index a0d274b9a2e..4b285e6bef2 100644 --- a/integration/test/Test/FeatureFlags.hs +++ b/integration/test/Test/FeatureFlags.hs @@ -811,8 +811,7 @@ testConferenceCallingInternal = do do notif <- awaitMatch isFeatureConfigUpdateNotif ws notif %. "payload.0.name" `shouldMatch` "conferenceCalling" - -- TODO: the patch event is currently wrong, and does not reflect the update - notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs {status = "disabled", lockStatus = Just "locked"}) + notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs {status = "enabled", lockStatus = Just "unlocked"}) checkFeature "conferenceCalling" m tid (confCalling defaultArgs {status = "enabled", lockStatus = Just "unlocked"}) -- just disable @@ -836,7 +835,7 @@ testConferenceCallingInternal = do do notif <- awaitMatch isFeatureConfigUpdateNotif ws notif %. "payload.0.name" `shouldMatch` "conferenceCalling" - notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs {lockStatus = Just "unlocked"}) + notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs) checkFeature "conferenceCalling" m tid (confCalling defaultArgs) _testLockStatusWithConfig :: diff --git a/libs/galley-types/default.nix b/libs/galley-types/default.nix index c67ae7c7cb1..f977e3444c9 100644 --- a/libs/galley-types/default.nix +++ b/libs/galley-types/default.nix @@ -9,6 +9,7 @@ , bytestring-conversion , containers , crypton +, data-default , errors , gitignoreSource , imports @@ -34,6 +35,7 @@ mkDerivation { bytestring-conversion containers crypton + data-default errors imports lens diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index f1fae8db830..7a07066d2e3 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -75,6 +75,7 @@ library , bytestring-conversion , containers >=0.5 , crypton + , data-default , errors , imports , lens >=4.12 diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 47ae6d8a516..23300b55c27 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -64,6 +64,7 @@ import Data.Aeson import Data.Aeson.Types qualified as A import Data.ByteString (toStrict) import Data.ByteString.UTF8 qualified as UTF8 +import Data.Default import Data.Id (UserId) import Data.Schema qualified as Schema import Data.Set qualified as Set @@ -84,18 +85,18 @@ data FeatureFlags = FeatureFlags _flagTeamSearchVisibility :: !FeatureTeamSearchVisibilityAvailability, _flagAppLockDefaults :: !(Defaults (ImplicitLockStatus AppLockConfig)), _flagClassifiedDomains :: !(ImplicitLockStatus ClassifiedDomainsConfig), - _flagFileSharing :: !(Defaults (WithStatus FileSharingConfig)), - _flagConferenceCalling :: !(Defaults (WithStatus ConferenceCallingConfig)), - _flagSelfDeletingMessages :: !(Defaults (WithStatus SelfDeletingMessagesConfig)), - _flagConversationGuestLinks :: !(Defaults (WithStatus GuestLinksConfig)), + _flagFileSharing :: !(Defaults (LockableFeature FileSharingConfig)), + _flagConferenceCalling :: !(Defaults (LockableFeature ConferenceCallingConfig)), + _flagSelfDeletingMessages :: !(Defaults (LockableFeature SelfDeletingMessagesConfig)), + _flagConversationGuestLinks :: !(Defaults (LockableFeature GuestLinksConfig)), _flagsTeamFeatureValidateSAMLEmailsStatus :: !(Defaults (ImplicitLockStatus ValidateSAMLEmailsConfig)), - _flagTeamFeatureSndFactorPasswordChallengeStatus :: !(Defaults (WithStatus SndFactorPasswordChallengeConfig)), + _flagTeamFeatureSndFactorPasswordChallengeStatus :: !(Defaults (LockableFeature SndFactorPasswordChallengeConfig)), _flagTeamFeatureSearchVisibilityInbound :: !(Defaults (ImplicitLockStatus SearchVisibilityInboundConfig)), - _flagMLS :: !(Defaults (WithStatus MLSConfig)), - _flagOutlookCalIntegration :: !(Defaults (WithStatus OutlookCalIntegrationConfig)), - _flagMlsE2EId :: !(Defaults (WithStatus MlsE2EIdConfig)), - _flagMlsMigration :: !(Defaults (WithStatus MlsMigrationConfig)), - _flagEnforceFileDownloadLocation :: !(Defaults (WithStatus EnforceFileDownloadLocationConfig)), + _flagMLS :: !(Defaults (LockableFeature MLSConfig)), + _flagOutlookCalIntegration :: !(Defaults (LockableFeature OutlookCalIntegrationConfig)), + _flagMlsE2EId :: !(Defaults (LockableFeature MlsE2EIdConfig)), + _flagMlsMigration :: !(Defaults (LockableFeature MlsMigrationConfig)), + _flagEnforceFileDownloadLocation :: !(Defaults (LockableFeature EnforceFileDownloadLocationConfig)), _flagLimitedEventFanout :: !(Defaults (ImplicitLockStatus LimitedEventFanoutConfig)) } deriving (Eq, Show, Generic) @@ -137,23 +138,23 @@ instance FromJSON FeatureFlags where <*> obj .: "legalhold" <*> obj .: "teamSearchVisibility" <*> withImplicitLockStatusOrDefault obj "appLock" - <*> (fromMaybe (ImplicitLockStatus (defFeatureStatus @ClassifiedDomainsConfig)) <$> (obj .:? "classifiedDomains")) - <*> (fromMaybe (Defaults (defFeatureStatus @FileSharingConfig)) <$> (obj .:? "fileSharing")) - <*> (fromMaybe (Defaults (defFeatureStatus @ConferenceCallingConfig)) <$> (obj .:? "conferenceCalling")) - <*> (fromMaybe (Defaults (defFeatureStatus @SelfDeletingMessagesConfig)) <$> (obj .:? "selfDeletingMessages")) - <*> (fromMaybe (Defaults (defFeatureStatus @GuestLinksConfig)) <$> (obj .:? "conversationGuestLinks")) + <*> (fromMaybe (ImplicitLockStatus def) <$> (obj .:? "classifiedDomains")) + <*> (fromMaybe (Defaults def) <$> (obj .:? "fileSharing")) + <*> (fromMaybe (Defaults def) <$> (obj .:? "conferenceCalling")) + <*> (fromMaybe (Defaults def) <$> (obj .:? "selfDeletingMessages")) + <*> (fromMaybe (Defaults def) <$> (obj .:? "conversationGuestLinks")) <*> withImplicitLockStatusOrDefault obj "validateSAMLEmails" - <*> (fromMaybe (Defaults (defFeatureStatus @SndFactorPasswordChallengeConfig)) <$> (obj .:? "sndFactorPasswordChallenge")) + <*> (fromMaybe (Defaults def) <$> (obj .:? "sndFactorPasswordChallenge")) <*> withImplicitLockStatusOrDefault obj "searchVisibilityInbound" - <*> (fromMaybe (Defaults (defFeatureStatus @MLSConfig)) <$> (obj .:? "mls")) - <*> (fromMaybe (Defaults (defFeatureStatus @OutlookCalIntegrationConfig)) <$> (obj .:? "outlookCalIntegration")) - <*> (fromMaybe (Defaults (defFeatureStatus @MlsE2EIdConfig)) <$> (obj .:? "mlsE2EId")) - <*> (fromMaybe (Defaults (defFeatureStatus @MlsMigrationConfig)) <$> (obj .:? "mlsMigration")) - <*> (fromMaybe (Defaults (defFeatureStatus @EnforceFileDownloadLocationConfig)) <$> (obj .:? "enforceFileDownloadLocation")) + <*> (fromMaybe (Defaults def) <$> (obj .:? "mls")) + <*> (fromMaybe (Defaults def) <$> (obj .:? "outlookCalIntegration")) + <*> (fromMaybe (Defaults def) <$> (obj .:? "mlsE2EId")) + <*> (fromMaybe (Defaults def) <$> (obj .:? "mlsMigration")) + <*> (fromMaybe (Defaults def) <$> (obj .:? "enforceFileDownloadLocation")) <*> withImplicitLockStatusOrDefault obj "limitedEventFanout" where withImplicitLockStatusOrDefault :: forall cfg. (IsFeatureConfig cfg, Schema.ToSchema cfg) => Object -> Key -> A.Parser (Defaults (ImplicitLockStatus cfg)) - withImplicitLockStatusOrDefault obj fieldName = fromMaybe (Defaults (ImplicitLockStatus (defFeatureStatus @cfg))) <$> obj .:? fieldName + withImplicitLockStatusOrDefault obj fieldName = fromMaybe (Defaults (ImplicitLockStatus def)) <$> obj .:? fieldName instance FromJSON FeatureSSO where parseJSON (String "enabled-by-default") = pure FeatureSSOEnabledByDefault diff --git a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs index 32e67dcfaf6..ca1e3fc7534 100644 --- a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs +++ b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs @@ -30,7 +30,7 @@ import Data.OpenApi qualified as S import Data.Schema import GHC.TypeLits (KnownSymbol) import Imports -import Test.QuickCheck.Gen (oneof) +import Test.QuickCheck.Gen import Wire.API.Team.Feature import Wire.Arbitrary (Arbitrary (..), GenericUniform (..)) @@ -42,30 +42,31 @@ data Event = Event deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON) via Schema Event +arbitraryFeature :: forall cfg. (IsFeatureConfig cfg, ToSchema cfg, Arbitrary cfg) => Gen A.Value +arbitraryFeature = toJSON <$> arbitrary @(LockableFeature cfg) + +class AllArbitraryFeatures cfgs where + allArbitraryFeatures :: [Gen A.Value] + +instance AllArbitraryFeatures '[] where + allArbitraryFeatures = [] + +instance + ( IsFeatureConfig cfg, + ToSchema cfg, + Arbitrary cfg, + AllArbitraryFeatures cfgs + ) => + AllArbitraryFeatures (cfg : cfgs) + where + allArbitraryFeatures = arbitraryFeature @cfg : allArbitraryFeatures @cfgs + instance Arbitrary Event where arbitrary = - do - let arbConfig = - oneof - [ arbitrary @(WithStatus SSOConfig) <&> toJSON, - arbitrary @(WithStatus SearchVisibilityAvailableConfig) <&> toJSON, - arbitrary @(WithStatus ValidateSAMLEmailsConfig) <&> toJSON, - arbitrary @(WithStatus DigitalSignaturesConfig) <&> toJSON, - arbitrary @(WithStatus AppLockConfig) <&> toJSON, - arbitrary @(WithStatus FileSharingConfig) <&> toJSON, - arbitrary @(WithStatus ClassifiedDomainsConfig) <&> toJSON, - arbitrary @(WithStatus ConferenceCallingConfig) <&> toJSON, - arbitrary @(WithStatus SelfDeletingMessagesConfig) <&> toJSON, - arbitrary @(WithStatus GuestLinksConfig) <&> toJSON, - arbitrary @(WithStatus SndFactorPasswordChallengeConfig) <&> toJSON, - arbitrary @(WithStatus SearchVisibilityInboundConfig) <&> toJSON, - arbitrary @(WithStatus MLSConfig) <&> toJSON, - arbitrary @(WithStatus ExposeInvitationURLsToTeamAdminConfig) <&> toJSON - ] - Event - <$> arbitrary - <*> arbitrary - <*> arbConfig + Event + <$> arbitrary + <*> arbitrary + <*> oneof (allArbitraryFeatures @Features) data EventType = Update deriving (Eq, Show, Generic) @@ -98,5 +99,5 @@ instance ToJSONObject Event where instance S.ToSchema Event where declareNamedSchema = schemaToSwagger -mkUpdateEvent :: forall cfg. (IsFeatureConfig cfg, ToSchema cfg, KnownSymbol (FeatureSymbol cfg)) => WithStatus cfg -> Event +mkUpdateEvent :: forall cfg. (IsFeatureConfig cfg, ToSchema cfg, KnownSymbol (FeatureSymbol cfg)) => LockableFeature cfg -> Event mkUpdateEvent ws = Event Update (featureName @cfg) (toJSON ws) 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 a143f9e3e33..6c14cbd6916 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -55,7 +55,7 @@ import Data.Text qualified as Text import GHC.TypeLits import Imports hiding (head) import Network.HTTP.Client qualified as HTTP -import Servant hiding (Handler, WithStatus, addHeader, respond) +import Servant hiding (Handler, addHeader, respond) import Servant.Client qualified as Servant import Servant.Client.Core qualified as Servant import Servant.OpenApi (HasOpenApi (toOpenApi)) @@ -114,7 +114,7 @@ type GetAccountConferenceCallingConfig = :> Capture "uid" UserId :> "features" :> "conferenceCalling" - :> Get '[Servant.JSON] (WithStatusNoLock ConferenceCallingConfig) + :> Get '[Servant.JSON] (Feature ConferenceCallingConfig) type PutAccountConferenceCallingConfig = Summary @@ -123,7 +123,7 @@ type PutAccountConferenceCallingConfig = :> Capture "uid" UserId :> "features" :> "conferenceCalling" - :> Servant.ReqBody '[Servant.JSON] (WithStatusNoLock ConferenceCallingConfig) + :> Servant.ReqBody '[Servant.JSON] (Feature ConferenceCallingConfig) :> Put '[Servant.JSON] NoContent type DeleteAccountConferenceCallingConfig = 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 44afc4e627a..b8f6b73b324 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -24,7 +24,7 @@ import Data.OpenApi (OpenApi, info, title) import Data.Range import GHC.TypeLits (AppendSymbol) import Imports hiding (head) -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi import Wire.API.ApplyMods import Wire.API.Bot @@ -410,8 +410,8 @@ type FeatureStatusBasePutInternal errs featureConfig = (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) errs featureConfig - ( ReqBody '[JSON] (WithStatusNoLock featureConfig) - :> Put '[JSON] (WithStatus featureConfig) + ( ReqBody '[JSON] (Feature featureConfig) + :> Put '[JSON] (LockableFeature featureConfig) ) type FeatureStatusBasePatchInternal errs featureConfig = @@ -419,8 +419,8 @@ type FeatureStatusBasePatchInternal errs featureConfig = (AppendSymbol "Patch config for " (FeatureSymbol featureConfig)) errs featureConfig - ( ReqBody '[JSON] (WithStatusPatch featureConfig) - :> Patch '[JSON] (WithStatus featureConfig) + ( ReqBody '[JSON] (LockableFeaturePatch featureConfig) + :> Patch '[JSON] (LockableFeature featureConfig) ) type FeatureStatusBaseInternal desc errs featureConfig a = diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs index ffde2e561c3..73087b78ea3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/LegalHold.hs @@ -23,7 +23,7 @@ import Data.OpenApi (OpenApi) import Data.OpenApi.Lens import Data.Proxy import Imports -import Servant.API hiding (Header, WithStatus) +import Servant.API import Servant.OpenApi import Wire.API.Team.Feature @@ -32,10 +32,10 @@ type InternalLegalHoldAPI = :> "teams" :> ( Capture "tid" TeamId :> "legalhold" - :> Get '[JSON] (WithStatus LegalholdConfig) + :> Get '[JSON] (LockableFeature LegalholdConfig) :<|> Capture "tid" TeamId :> "legalhold" - :> ReqBody '[JSON] (WithStatusNoLock LegalholdConfig) + :> ReqBody '[JSON] (Feature LegalholdConfig) :> Put '[] NoContent ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs index 52ec0ee5022..e7610068772 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley.hs @@ -20,7 +20,7 @@ module Wire.API.Routes.Public.Galley where -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Bot diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs index 6d4359b545c..06b1df74de1 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs @@ -17,7 +17,7 @@ module Wire.API.Routes.Public.Galley.Bot where -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley 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 064dd35f673..c228a3c2621 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 @@ -23,7 +23,7 @@ import Data.Id import Data.Range import Data.SOP (I (..), NS (..)) import Imports hiding (head) -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation import Wire.API.Conversation.Code @@ -884,7 +884,7 @@ type ConversationAPI = :> Capture' '[Description "Conversation ID"] "cnv" ConvId :> "features" :> FeatureSymbol GuestLinksConfig - :> Get '[Servant.JSON] (WithStatus GuestLinksConfig) + :> Get '[Servant.JSON] (LockableFeature GuestLinksConfig) ) -- This endpoint can lead to the following events being sent: -- - ConvCodeDelete event to members diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs index 607a6e62573..c91dd758fdd 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/CustomBackend.hs @@ -18,7 +18,7 @@ module Wire.API.Routes.Public.Galley.CustomBackend where import Data.Domain -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.CustomBackend import Wire.API.Error diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index 4aba788fcf5..5e69d130941 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -19,7 +19,7 @@ module Wire.API.Routes.Public.Galley.Feature where import Data.Id import GHC.TypeLits -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.ApplyMods import Wire.API.Conversation.Role @@ -157,7 +157,7 @@ type FeatureStatusBaseGet featureConfig = :> Capture "tid" TeamId :> "features" :> FeatureSymbol featureConfig - :> Get '[Servant.JSON] (WithStatus featureConfig) + :> Get '[Servant.JSON] (LockableFeature featureConfig) type FeatureStatusBasePutPublic errs featureConfig = Summary (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) @@ -170,8 +170,8 @@ type FeatureStatusBasePutPublic errs featureConfig = :> Capture "tid" TeamId :> "features" :> FeatureSymbol featureConfig - :> ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) - :> Put '[Servant.JSON] (WithStatus featureConfig) + :> ReqBody '[Servant.JSON] (Feature featureConfig) + :> Put '[Servant.JSON] (LockableFeature featureConfig) -- | A type for a GET endpoint for a feature with a deprecated path type FeatureStatusBaseDeprecatedGet desc featureConfig = @@ -191,7 +191,7 @@ type FeatureStatusBaseDeprecatedGet desc featureConfig = :> Capture "tid" TeamId :> "features" :> DeprecatedFeatureName featureConfig - :> Get '[Servant.JSON] (WithStatus featureConfig) + :> Get '[Servant.JSON] (LockableFeature featureConfig) ) -- | A type for a PUT endpoint for a feature with a deprecated path @@ -213,8 +213,8 @@ type FeatureStatusBaseDeprecatedPut desc featureConfig = :> Capture "tid" TeamId :> "features" :> DeprecatedFeatureName featureConfig - :> ReqBody '[Servant.JSON] (WithStatusNoLock featureConfig) - :> Put '[Servant.JSON] (WithStatus featureConfig) + :> ReqBody '[Servant.JSON] (Feature featureConfig) + :> Put '[Servant.JSON] (LockableFeature featureConfig) type FeatureConfigDeprecatedGet desc featureConfig = Named @@ -228,7 +228,7 @@ type FeatureConfigDeprecatedGet desc featureConfig = :> CanThrow 'TeamNotFound :> "feature-configs" :> FeatureSymbol featureConfig - :> Get '[Servant.JSON] (WithStatus featureConfig) + :> Get '[Servant.JSON] (LockableFeature featureConfig) ) type AllFeatureConfigsUserGet = diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs index f04ad6c3e70..a9d7ebe219d 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs @@ -20,7 +20,7 @@ module Wire.API.Routes.Public.Galley.LegalHold where import Data.Id import GHC.Generics import Generics.SOP qualified as GSOP -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Role import Wire.API.Error diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index 6c53e5e3398..41d5dbf27a6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -17,7 +17,7 @@ module Wire.API.Routes.Public.Galley.MLS where -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs index d4b81661b79..c862d5863d0 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs @@ -23,7 +23,7 @@ import Data.OpenApi qualified as S import Data.SOP import Generics.SOP qualified as GSOP import Imports -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Brig qualified as BrigError diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs index fd3fd392a4a..4c0c61751d4 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs @@ -19,7 +19,7 @@ module Wire.API.Routes.Public.Galley.Team where import Data.Id import Imports -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs index 0f45c2ac92c..98573abb02e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs @@ -18,7 +18,7 @@ module Wire.API.Routes.Public.Galley.TeamConversation where import Data.Id -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Role import Wire.API.Error diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs index 4c71df03e49..ef66057baa3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamMember.hs @@ -22,7 +22,7 @@ import Data.Int import Data.Range import GHC.Generics import Generics.SOP qualified as GSOP -import Servant hiding (WithStatus) +import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index f5083f5c87f..095c2fe5f9b 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -3,6 +3,7 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. -- @@ -26,33 +27,16 @@ module Wire.API.Team.Feature featureName, featureNameBS, LockStatus (..), - WithStatusBase (..), DbFeature (..), - DbFeatureWithLock (..), + dbFeatureLockStatus, dbFeatureStatus, - dbFeatureTTL, dbFeatureConfig, dbFeatureModConfig, - WithStatus, - withStatus, - withStatus', - wsStatus, - wsLockStatus, - wsConfig, - wsTTL, - setStatus, - setLockStatus, - setConfig, - setConfig', - setTTL, - setWsTTL, - WithStatusPatch, - wsPatch, - wspStatus, - wspLockStatus, - wspConfig, - wspTTL, - WithStatusNoLock (..), + LockableFeature (..), + defUnlockedFeature, + defLockedFeature, + LockableFeaturePatch (..), + Feature (..), forgetLock, withLockStatus, withUnlocked, @@ -62,9 +46,7 @@ module Wire.API.Team.Feature FeatureTTLUnit (..), convertFeatureTTLDaysToSeconds, EnforceAppLock (..), - defFeatureStatusNoLock, genericComputeFeature, - computeFeatureConfigForTeamUser, IsFeatureConfig (..), FeatureSingleton (..), HasDeprecatedFeatureName (..), @@ -91,7 +73,12 @@ module Wire.API.Team.Feature MlsMigrationConfig (..), EnforceFileDownloadLocationConfig (..), LimitedEventFanoutConfig (..), - AllFeatures (..), + Features, + AllFeatures, + NpProject (..), + npProject, + NpUpdate (..), + npUpdate, AllFeatureConfigs, unImplicitLockStatus, ImplicitLockStatus (..), @@ -113,8 +100,10 @@ import Data.Id import Data.Json.Util import Data.Kind import Data.Misc (HttpsUrl) +import Data.Monoid import Data.OpenApi qualified as S import Data.Proxy +import Data.SOP import Data.Schema import Data.Scientific (toBoundedInteger) import Data.Text qualified as T @@ -189,9 +178,8 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- 12. Add a section to the documentation at an appropriate place -- (e.g. 'docs/src/developer/reference/config-options.md' (if applicable) or -- 'docs/src/understand/team-feature-settings.md') -class IsFeatureConfig cfg where +class (Default cfg, Default (LockableFeature cfg)) => IsFeatureConfig cfg where type FeatureSymbol cfg :: Symbol - defFeatureStatus :: WithStatus cfg featureSingleton :: FeatureSingleton cfg objectSchema :: @@ -218,10 +206,7 @@ data FeatureSingleton cfg where FeatureSingletonExposeInvitationURLsToTeamAdminConfig :: FeatureSingleton ExposeInvitationURLsToTeamAdminConfig FeatureSingletonOutlookCalIntegrationConfig :: FeatureSingleton OutlookCalIntegrationConfig FeatureSingletonMlsE2EIdConfig :: FeatureSingleton MlsE2EIdConfig - FeatureSingletonMlsMigration :: - -- FUTUREWORK: rename to `FeatureSingletonMlsMigrationConfig` (or drop the `Config` from - -- all other constructors) - FeatureSingleton MlsMigrationConfig + FeatureSingletonMlsMigrationConfig :: FeatureSingleton MlsMigrationConfig FeatureSingletonEnforceFileDownloadLocationConfig :: FeatureSingleton EnforceFileDownloadLocationConfig FeatureSingletonLimitedEventFanoutConfig :: FeatureSingleton LimitedEventFanoutConfig @@ -234,49 +219,28 @@ featureName = T.pack $ symbolVal (Proxy @(FeatureSymbol cfg)) featureNameBS :: forall cfg. (KnownSymbol (FeatureSymbol cfg)) => ByteString featureNameBS = UTF8.fromString $ symbolVal (Proxy @(FeatureSymbol cfg)) ----------------------------------------------------------------------- --- WithStatusBase - -data WithStatusBase (m :: Type -> Type) (cfg :: Type) = WithStatusBase - { wsbStatus :: m FeatureStatus, - wsbLockStatus :: m LockStatus, - wsbConfig :: m cfg, - wsbTTL :: m FeatureTTL - } - deriving stock (Generic, Typeable, Functor) - -------------------------------------------------------------------------------- -- DbFeature -- | Feature data stored in the database, as a function of its default values. newtype DbFeature cfg = DbFeature - {unDbFeature :: WithStatusNoLock cfg -> WithStatusNoLock cfg} + {applyDbFeature :: LockableFeature cfg -> LockableFeature cfg} + deriving (Semigroup, Monoid) via Endo (LockableFeature cfg) -instance Semigroup (DbFeature cfg) where - DbFeature f <> DbFeature g = DbFeature (f . g) - -instance Monoid (DbFeature cfg) where - mempty = DbFeature id +dbFeatureLockStatus :: LockStatus -> DbFeature cfg +dbFeatureLockStatus s = DbFeature $ \w -> w {lockStatus = s} dbFeatureStatus :: FeatureStatus -> DbFeature cfg -dbFeatureStatus s = DbFeature $ \w -> w {wssStatus = s} - -dbFeatureTTL :: FeatureTTL -> DbFeature cfg -dbFeatureTTL ttl = DbFeature $ \w -> w {wssTTL = ttl} +dbFeatureStatus s = DbFeature $ \w -> w {status = s} dbFeatureConfig :: cfg -> DbFeature cfg -dbFeatureConfig c = DbFeature $ \w -> w {wssConfig = c} +dbFeatureConfig c = DbFeature $ \w -> w {config = c} dbFeatureModConfig :: (cfg -> cfg) -> DbFeature cfg -dbFeatureModConfig f = DbFeature $ \w -> w {wssConfig = f (wssConfig w)} - -data DbFeatureWithLock cfg = DbFeatureWithLock - { lockStatus :: Maybe LockStatus, - feature :: DbFeature cfg - } +dbFeatureModConfig f = DbFeature $ \w -> w {config = f w.config} ---------------------------------------------------------------------- --- WithStatus +-- LockableFeature -- [Note: unsettable features] -- @@ -296,153 +260,119 @@ data DbFeatureWithLock cfg = DbFeatureWithLock -- See the implementation of 'computeFeature' for 'ConferenceCallingConfig' for -- an example of this mechanism in practice. --- FUTUREWORK: use lenses, maybe? -wsStatus :: WithStatus cfg -> FeatureStatus -wsStatus = runIdentity . wsbStatus - -wsLockStatus :: WithStatus cfg -> LockStatus -wsLockStatus = runIdentity . wsbLockStatus - -wsConfig :: WithStatus cfg -> cfg -wsConfig = runIdentity . wsbConfig - -wsTTL :: WithStatus cfg -> FeatureTTL -wsTTL = runIdentity . wsbTTL - -withStatus :: FeatureStatus -> LockStatus -> cfg -> FeatureTTL -> WithStatus cfg -withStatus s ls c ttl = WithStatusBase (Identity s) (Identity ls) (Identity c) (Identity ttl) - -setStatus :: FeatureStatus -> WithStatus cfg -> WithStatus cfg -setStatus s (WithStatusBase _ ls c ttl) = WithStatusBase (Identity s) ls c ttl - -setLockStatus :: LockStatus -> WithStatus cfg -> WithStatus cfg -setLockStatus ls (WithStatusBase s _ c ttl) = WithStatusBase s (Identity ls) c ttl - -setConfig :: cfg -> WithStatus cfg -> WithStatus cfg -setConfig = setConfig' - -setConfig' :: forall (m :: Type -> Type) (cfg :: Type). (Applicative m) => cfg -> WithStatusBase m cfg -> WithStatusBase m cfg -setConfig' c (WithStatusBase s ls _ ttl) = WithStatusBase s ls (pure c) ttl - -setTTL :: forall (m :: Type -> Type) (cfg :: Type). (Applicative m) => FeatureTTL -> WithStatusBase m cfg -> WithStatusBase m cfg -setTTL ttl (WithStatusBase s ls c _) = WithStatusBase s ls c (pure ttl) - -setWsTTL :: FeatureTTL -> WithStatus cfg -> WithStatus cfg -setWsTTL = setTTL - -type WithStatus = WithStatusBase Identity - -deriving instance (Eq cfg) => Eq (WithStatus cfg) - -deriving instance (Show cfg) => Show (WithStatus cfg) - -deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => ToJSON (WithStatus cfg) - -deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg)) => FromJSON (WithStatus cfg) - -deriving via (Schema (WithStatus cfg)) instance (ToSchema (WithStatus cfg), Typeable cfg) => S.ToSchema (WithStatus cfg) - -instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (WithStatus cfg) where +data LockableFeature cfg = LockableFeature + { status :: FeatureStatus, + lockStatus :: LockStatus, + config :: cfg + } + deriving stock (Eq, Show) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema (LockableFeature cfg) + +instance (Default (LockableFeature cfg)) => Default (Feature cfg) where + def = forgetLock def + +-- | A feature that is disabled and locked. +defLockedFeature :: (Default cfg) => LockableFeature cfg +defLockedFeature = + LockableFeature + { status = FeatureStatusDisabled, + lockStatus = LockStatusLocked, + config = def + } + +-- | A feature that is enabled and unlocked. +defUnlockedFeature :: (Default cfg) => LockableFeature cfg +defUnlockedFeature = + LockableFeature + { status = FeatureStatusEnabled, + lockStatus = LockStatusUnlocked, + config = def + } + +instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (LockableFeature cfg) where schema = object name $ - WithStatusBase - <$> (runIdentity . wsbStatus) .= (Identity <$> field "status" schema) - <*> (runIdentity . wsbLockStatus) .= (Identity <$> field "lockStatus" schema) - <*> (runIdentity . wsbConfig) .= (Identity <$> objectSchema @cfg) - <*> (runIdentity . wsbTTL) .= (Identity . fromMaybe FeatureTTLUnlimited <$> optField "ttl" schema) + LockableFeature + <$> (.status) .= field "status" schema + <*> (.lockStatus) .= field "lockStatus" schema + <*> (.config) .= objectSchema @cfg + <* const FeatureTTLUnlimited + .= optField + "ttl" + (schema :: ValueSchema NamedSwaggerDoc FeatureTTL) where inner = schema @cfg - name = fromMaybe "" (getName (schemaDoc inner)) <> ".WithStatus" + name = fromMaybe "" (getName (schemaDoc inner)) <> ".LockableFeature" -instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (WithStatus cfg) where - arbitrary = WithStatusBase <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary +instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (LockableFeature cfg) where + arbitrary = LockableFeature <$> arbitrary <*> arbitrary <*> arbitrary ---------------------------------------------------------------------- --- WithStatusPatch - -type WithStatusPatch (cfg :: Type) = WithStatusBase Maybe cfg - -deriving instance (Eq cfg) => Eq (WithStatusPatch cfg) - -deriving instance (Show cfg) => Show (WithStatusPatch cfg) - -deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg)) => ToJSON (WithStatusPatch cfg) - -deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg)) => FromJSON (WithStatusPatch cfg) +-- LockableFeaturePatch -deriving via (Schema (WithStatusPatch cfg)) instance (ToSchema (WithStatusPatch cfg), Typeable cfg) => S.ToSchema (WithStatusPatch cfg) - -wsPatch :: Maybe FeatureStatus -> Maybe LockStatus -> Maybe cfg -> Maybe FeatureTTL -> WithStatusPatch cfg -wsPatch = WithStatusBase - -wspStatus :: WithStatusPatch cfg -> Maybe FeatureStatus -wspStatus = wsbStatus - -wspLockStatus :: WithStatusPatch cfg -> Maybe LockStatus -wspLockStatus = wsbLockStatus - -wspConfig :: WithStatusPatch cfg -> Maybe cfg -wspConfig = wsbConfig - -wspTTL :: WithStatusPatch cfg -> Maybe FeatureTTL -wspTTL = wsbTTL - -withStatus' :: Maybe FeatureStatus -> Maybe LockStatus -> Maybe cfg -> Maybe FeatureTTL -> WithStatusPatch cfg -withStatus' = WithStatusBase +data LockableFeaturePatch (cfg :: Type) = LockableFeaturePatch + { status :: Maybe FeatureStatus, + lockStatus :: Maybe LockStatus, + config :: Maybe cfg + } + deriving stock (Eq, Show) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema (LockableFeaturePatch cfg)) --- | The ToJSON implementation of `WithStatusPatch` will encode the trivial config as `"config": {}` +-- | The ToJSON implementation of `LockableFeaturePatch` will encode the trivial config as `"config": {}` -- when the value is a `Just`, if it's `Nothing` it will be omitted, which is the important part. -instance (ToSchema cfg) => ToSchema (WithStatusPatch cfg) where +instance (ToSchema cfg) => ToSchema (LockableFeaturePatch cfg) where schema = object name $ - WithStatusBase - <$> wsbStatus .= maybe_ (optField "status" schema) - <*> wsbLockStatus .= maybe_ (optField "lockStatus" schema) - <*> wsbConfig .= maybe_ (optField "config" schema) - <*> wsbTTL .= maybe_ (optField "ttl" schema) + LockableFeaturePatch + <$> (.status) .= maybe_ (optField "status" schema) + <*> (.lockStatus) .= maybe_ (optField "lockStatus" schema) + <*> (.config) .= maybe_ (optField "config" schema) + <* const FeatureTTLUnlimited + .= optField + "ttl" + (schema :: ValueSchema NamedSwaggerDoc FeatureTTL) where inner = schema @cfg - name = fromMaybe "" (getName (schemaDoc inner)) <> ".WithStatusPatch" + name = fromMaybe "" (getName (schemaDoc inner)) <> ".LockableFeaturePatch" -instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (WithStatusPatch cfg) where - arbitrary = WithStatusBase <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary +instance (Arbitrary cfg, IsFeatureConfig cfg) => Arbitrary (LockableFeaturePatch cfg) where + arbitrary = LockableFeaturePatch <$> arbitrary <*> arbitrary <*> arbitrary ---------------------------------------------------------------------- --- WithStatusNoLock +-- Feature -data WithStatusNoLock (cfg :: Type) = WithStatusNoLock - { wssStatus :: FeatureStatus, - wssConfig :: cfg, - wssTTL :: FeatureTTL +data Feature (cfg :: Type) = Feature + { status :: FeatureStatus, + config :: cfg } deriving stock (Eq, Show, Generic, Typeable, Functor) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema (WithStatusNoLock cfg)) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema (Feature cfg)) -instance (Arbitrary cfg) => Arbitrary (WithStatusNoLock cfg) where - arbitrary = WithStatusNoLock <$> arbitrary <*> arbitrary <*> arbitrary +instance (Arbitrary cfg) => Arbitrary (Feature cfg) where + arbitrary = Feature <$> arbitrary <*> arbitrary -forgetLock :: WithStatus a -> WithStatusNoLock a -forgetLock ws = WithStatusNoLock (wsStatus ws) (wsConfig ws) (wsTTL ws) +forgetLock :: LockableFeature a -> Feature a +forgetLock ws = Feature ws.status ws.config -withLockStatus :: LockStatus -> WithStatusNoLock a -> WithStatus a -withLockStatus ls (WithStatusNoLock s c ttl) = withStatus s ls c ttl +withLockStatus :: LockStatus -> Feature a -> LockableFeature a +withLockStatus ls (Feature s c) = LockableFeature s ls c -withUnlocked :: WithStatusNoLock a -> WithStatus a +withUnlocked :: Feature a -> LockableFeature a withUnlocked = withLockStatus LockStatusUnlocked -withLocked :: WithStatusNoLock a -> WithStatus a -withLocked = withLockStatus LockStatusLocked - -instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (WithStatusNoLock cfg) where +instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (Feature cfg) where schema = object name $ - WithStatusNoLock - <$> wssStatus .= field "status" schema - <*> wssConfig .= objectSchema @cfg - <*> wssTTL .= (fromMaybe FeatureTTLUnlimited <$> optField "ttl" schema) + Feature + <$> (.status) .= field "status" schema + <*> (.config) .= objectSchema @cfg + <* const FeatureTTLUnlimited + .= optField + "ttl" + (schema :: ValueSchema NamedSwaggerDoc FeatureTTL) where inner = schema @cfg - name = fromMaybe "" (getName (schemaDoc inner)) <> ".WithStatusNoLock" + name = fromMaybe "" (getName (schemaDoc inner)) <> ".Feature" ---------------------------------------------------------------------- -- FeatureTTL @@ -602,40 +532,24 @@ instance ToSchema LockStatusResponse where LockStatusResponse <$> _unlockStatus .= field "lockStatus" schema -newtype ImplicitLockStatus (cfg :: Type) = ImplicitLockStatus {_unImplicitLockStatus :: WithStatus cfg} +newtype ImplicitLockStatus (cfg :: Type) = ImplicitLockStatus {_unImplicitLockStatus :: LockableFeature cfg} deriving newtype (Eq, Show, Arbitrary) instance (IsFeatureConfig a, ToSchema a) => ToJSON (ImplicitLockStatus a) where toJSON (ImplicitLockStatus a) = A.toJSON $ forgetLock a instance (IsFeatureConfig a, ToSchema a) => FromJSON (ImplicitLockStatus a) where - parseJSON v = ImplicitLockStatus . withLockStatus (wsLockStatus $ defFeatureStatus @a) <$> A.parseJSON v + parseJSON v = ImplicitLockStatus . withLockStatus ((def @(LockableFeature a)).lockStatus) <$> A.parseJSON v -- | Convert a feature coming from the database to its public form. This can be -- overridden on a feature basis by implementing the `computeFeature` method of -- the `GetFeatureConfig` class. -genericComputeFeature :: - WithStatus cfg -> - Maybe LockStatus -> - DbFeature cfg -> - WithStatus cfg -genericComputeFeature defFeature lockStatus dbFeature = - case fromMaybe (wsLockStatus defFeature) lockStatus of - LockStatusLocked -> setLockStatus LockStatusLocked defFeature - LockStatusUnlocked -> withUnlocked $ unDbFeature dbFeature (forgetLock defFeature) - --- | This contains the pure business logic for users from teams -computeFeatureConfigForTeamUser :: Maybe (WithStatusNoLock cfg) -> Maybe LockStatus -> WithStatus cfg -> WithStatus cfg -computeFeatureConfigForTeamUser mStatusDb mLockStatusDb defStatus = - case lockStatus of - LockStatusLocked -> - withLocked (forgetLock defStatus) - LockStatusUnlocked -> - withUnlocked $ case mStatusDb of - Nothing -> forgetLock defStatus - Just fs -> fs - where - lockStatus = fromMaybe (wsLockStatus defStatus) mLockStatusDb +genericComputeFeature :: forall cfg. LockableFeature cfg -> DbFeature cfg -> LockableFeature cfg +genericComputeFeature defFeature dbFeature = + let feat = applyDbFeature dbFeature defFeature + in case feat.lockStatus of + LockStatusLocked -> defFeature {lockStatus = LockStatusLocked} + LockStatusUnlocked -> feat -------------------------------------------------------------------------------- -- GuestLinks feature @@ -644,15 +558,20 @@ data GuestLinksConfig = GuestLinksConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform GuestLinksConfig) +instance Default GuestLinksConfig where + def = GuestLinksConfig + instance RenderableSymbol GuestLinksConfig where renderSymbol = "GuestLinksConfig" instance ToSchema GuestLinksConfig where schema = object "GuestLinksConfig" objectSchema +instance Default (LockableFeature GuestLinksConfig) where + def = defUnlockedFeature + instance IsFeatureConfig GuestLinksConfig where type FeatureSymbol GuestLinksConfig = "conversationGuestLinks" - defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonGuestLinksConfig objectSchema = pure GuestLinksConfig @@ -664,12 +583,17 @@ data LegalholdConfig = LegalholdConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform LegalholdConfig) +instance Default LegalholdConfig where + def = LegalholdConfig + instance RenderableSymbol LegalholdConfig where renderSymbol = "LegalholdConfig" +instance Default (LockableFeature LegalholdConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} + instance IsFeatureConfig LegalholdConfig where type FeatureSymbol LegalholdConfig = "legalhold" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked LegalholdConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonLegalholdConfig objectSchema = pure LegalholdConfig @@ -684,12 +608,17 @@ data SSOConfig = SSOConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SSOConfig) +instance Default SSOConfig where + def = SSOConfig + instance RenderableSymbol SSOConfig where renderSymbol = "SSOConfig" +instance Default (LockableFeature SSOConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} + instance IsFeatureConfig SSOConfig where type FeatureSymbol SSOConfig = "sso" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked SSOConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonSSOConfig objectSchema = pure SSOConfig @@ -705,12 +634,17 @@ data SearchVisibilityAvailableConfig = SearchVisibilityAvailableConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SearchVisibilityAvailableConfig) +instance Default SearchVisibilityAvailableConfig where + def = SearchVisibilityAvailableConfig + instance RenderableSymbol SearchVisibilityAvailableConfig where renderSymbol = "SearchVisibilityAvailableConfig" +instance Default (LockableFeature SearchVisibilityAvailableConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} + instance IsFeatureConfig SearchVisibilityAvailableConfig where type FeatureSymbol SearchVisibilityAvailableConfig = "searchVisibility" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityAvailableConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonSearchVisibilityAvailableConfig objectSchema = pure SearchVisibilityAvailableConfig @@ -728,15 +662,20 @@ data ValidateSAMLEmailsConfig = ValidateSAMLEmailsConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ValidateSAMLEmailsConfig) +instance Default ValidateSAMLEmailsConfig where + def = ValidateSAMLEmailsConfig + instance RenderableSymbol ValidateSAMLEmailsConfig where renderSymbol = "ValidateSAMLEmailsConfig" instance ToSchema ValidateSAMLEmailsConfig where schema = object "ValidateSAMLEmailsConfig" objectSchema +instance Default (LockableFeature ValidateSAMLEmailsConfig) where + def = defUnlockedFeature + instance IsFeatureConfig ValidateSAMLEmailsConfig where type FeatureSymbol ValidateSAMLEmailsConfig = "validateSAMLemails" - defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked ValidateSAMLEmailsConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonValidateSAMLEmailsConfig objectSchema = pure ValidateSAMLEmailsConfig @@ -751,12 +690,17 @@ data DigitalSignaturesConfig = DigitalSignaturesConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform DigitalSignaturesConfig) +instance Default DigitalSignaturesConfig where + def = DigitalSignaturesConfig + instance RenderableSymbol DigitalSignaturesConfig where renderSymbol = "DigitalSignaturesConfig" +instance Default (LockableFeature DigitalSignaturesConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} + instance IsFeatureConfig DigitalSignaturesConfig where type FeatureSymbol DigitalSignaturesConfig = "digitalSignatures" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked DigitalSignaturesConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonDigitalSignaturesConfig objectSchema = pure DigitalSignaturesConfig @@ -804,9 +748,11 @@ instance Default ConferenceCallingConfig where instance RenderableSymbol ConferenceCallingConfig where renderSymbol = "ConferenceCallingConfig" +instance Default (LockableFeature ConferenceCallingConfig) where + def = defLockedFeature {status = FeatureStatusEnabled} + instance IsFeatureConfig ConferenceCallingConfig where type FeatureSymbol ConferenceCallingConfig = "conferenceCalling" - defFeatureStatus = withStatus FeatureStatusEnabled LockStatusLocked def FeatureTTLUnlimited featureSingleton = FeatureSingletonConferenceCallingConfig objectSchema = fromMaybe def <$> optField "config" schema @@ -826,15 +772,20 @@ data SndFactorPasswordChallengeConfig = SndFactorPasswordChallengeConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SndFactorPasswordChallengeConfig) +instance Default SndFactorPasswordChallengeConfig where + def = SndFactorPasswordChallengeConfig + instance RenderableSymbol SndFactorPasswordChallengeConfig where renderSymbol = "SndFactorPasswordChallengeConfig" instance ToSchema SndFactorPasswordChallengeConfig where schema = object "SndFactorPasswordChallengeConfig" objectSchema +instance Default (LockableFeature SndFactorPasswordChallengeConfig) where + def = defLockedFeature + instance IsFeatureConfig SndFactorPasswordChallengeConfig where type FeatureSymbol SndFactorPasswordChallengeConfig = "sndFactorPasswordChallenge" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked SndFactorPasswordChallengeConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonSndFactorPasswordChallengeConfig objectSchema = pure SndFactorPasswordChallengeConfig @@ -846,12 +797,17 @@ data SearchVisibilityInboundConfig = SearchVisibilityInboundConfig deriving (Arbitrary) via (GenericUniform SearchVisibilityInboundConfig) deriving (S.ToSchema) via Schema SearchVisibilityInboundConfig +instance Default SearchVisibilityInboundConfig where + def = SearchVisibilityInboundConfig + instance RenderableSymbol SearchVisibilityInboundConfig where renderSymbol = "SearchVisibilityInboundConfig" +instance Default (LockableFeature SearchVisibilityInboundConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} + instance IsFeatureConfig SearchVisibilityInboundConfig where type FeatureSymbol SearchVisibilityInboundConfig = "searchVisibilityInbound" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked SearchVisibilityInboundConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonSearchVisibilityInboundConfig objectSchema = pure SearchVisibilityInboundConfig @@ -870,6 +826,9 @@ data ClassifiedDomainsConfig = ClassifiedDomainsConfig deriving stock (Show, Eq, Generic) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema ClassifiedDomainsConfig) +instance Default ClassifiedDomainsConfig where + def = ClassifiedDomainsConfig [] + instance RenderableSymbol ClassifiedDomainsConfig where renderSymbol = "ClassifiedDomainsConfig" @@ -881,15 +840,12 @@ instance ToSchema ClassifiedDomainsConfig where ClassifiedDomainsConfig <$> classifiedDomainsDomains .= field "domains" (array schema) +instance Default (LockableFeature ClassifiedDomainsConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} + instance IsFeatureConfig ClassifiedDomainsConfig where type FeatureSymbol ClassifiedDomainsConfig = "classifiedDomains" - defFeatureStatus = - withStatus - FeatureStatusDisabled - LockStatusUnlocked - (ClassifiedDomainsConfig []) - FeatureTTLUnlimited featureSingleton = FeatureSingletonClassifiedDomainsConfig objectSchema = field "config" schema @@ -904,6 +860,9 @@ data AppLockConfig = AppLockConfig deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AppLockConfig) deriving (Arbitrary) via (GenericUniform AppLockConfig) +instance Default AppLockConfig where + def = AppLockConfig (EnforceAppLock False) 60 + instance RenderableSymbol AppLockConfig where renderSymbol = "AppLockConfig" @@ -914,15 +873,12 @@ instance ToSchema AppLockConfig where <$> applockEnforceAppLock .= field "enforceAppLock" schema <*> applockInactivityTimeoutSecs .= field "inactivityTimeoutSecs" schema +instance Default (LockableFeature AppLockConfig) where + def = defUnlockedFeature + instance IsFeatureConfig AppLockConfig where type FeatureSymbol AppLockConfig = "appLock" - defFeatureStatus = - withStatus - FeatureStatusEnabled - LockStatusUnlocked - (AppLockConfig (EnforceAppLock False) 60) - FeatureTTLUnlimited featureSingleton = FeatureSingletonAppLockConfig objectSchema = field "config" schema @@ -941,12 +897,17 @@ data FileSharingConfig = FileSharingConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform FileSharingConfig) +instance Default FileSharingConfig where + def = FileSharingConfig + instance RenderableSymbol FileSharingConfig where renderSymbol = "FileSharingConfig" +instance Default (LockableFeature FileSharingConfig) where + def = defUnlockedFeature + instance IsFeatureConfig FileSharingConfig where type FeatureSymbol FileSharingConfig = "fileSharing" - defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked FileSharingConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonFileSharingConfig objectSchema = pure FileSharingConfig @@ -963,6 +924,9 @@ newtype SelfDeletingMessagesConfig = SelfDeletingMessagesConfig deriving (FromJSON, ToJSON, S.ToSchema) via (Schema SelfDeletingMessagesConfig) deriving (Arbitrary) via (GenericUniform SelfDeletingMessagesConfig) +instance Default SelfDeletingMessagesConfig where + def = SelfDeletingMessagesConfig 0 + instance RenderableSymbol SelfDeletingMessagesConfig where renderSymbol = "SelfDeletingMessagesConfig" @@ -972,14 +936,11 @@ instance ToSchema SelfDeletingMessagesConfig where SelfDeletingMessagesConfig <$> sdmEnforcedTimeoutSeconds .= field "enforcedTimeoutSeconds" schema +instance Default (LockableFeature SelfDeletingMessagesConfig) where + def = defUnlockedFeature + instance IsFeatureConfig SelfDeletingMessagesConfig where type FeatureSymbol SelfDeletingMessagesConfig = "selfDeletingMessages" - defFeatureStatus = - withStatus - FeatureStatusEnabled - LockStatusUnlocked - (SelfDeletingMessagesConfig 0) - FeatureTTLUnlimited featureSingleton = FeatureSingletonSelfDeletingMessagesConfig objectSchema = field "config" schema @@ -996,6 +957,15 @@ data MLSConfig = MLSConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform MLSConfig) +instance Default MLSConfig where + def = + MLSConfig + [] + ProtocolProteusTag + [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] + MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + [ProtocolProteusTag, ProtocolMLSTag] + instance RenderableSymbol MLSConfig where renderSymbol = "MLSConfig" @@ -1009,17 +979,11 @@ instance ToSchema MLSConfig where <*> mlsDefaultCipherSuite .= field "defaultCipherSuite" schema <*> mlsSupportedProtocols .= field "supportedProtocols" (array schema) +instance Default (LockableFeature MLSConfig) where + def = defUnlockedFeature {status = FeatureStatusDisabled} + instance IsFeatureConfig MLSConfig where type FeatureSymbol MLSConfig = "mls" - defFeatureStatus = - let config = - MLSConfig - [] - ProtocolProteusTag - [MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519] - MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - [ProtocolProteusTag, ProtocolMLSTag] - in withStatus FeatureStatusDisabled LockStatusUnlocked config FeatureTTLUnlimited featureSingleton = FeatureSingletonMLSConfig objectSchema = field "config" schema @@ -1030,12 +994,17 @@ data ExposeInvitationURLsToTeamAdminConfig = ExposeInvitationURLsToTeamAdminConf deriving stock (Show, Eq, Generic) deriving (Arbitrary) via (GenericUniform ExposeInvitationURLsToTeamAdminConfig) +instance Default ExposeInvitationURLsToTeamAdminConfig where + def = ExposeInvitationURLsToTeamAdminConfig + instance RenderableSymbol ExposeInvitationURLsToTeamAdminConfig where renderSymbol = "ExposeInvitationURLsToTeamAdminConfig" +instance Default (LockableFeature ExposeInvitationURLsToTeamAdminConfig) where + def = defLockedFeature + instance IsFeatureConfig ExposeInvitationURLsToTeamAdminConfig where type FeatureSymbol ExposeInvitationURLsToTeamAdminConfig = "exposeInvitationURLsToTeamAdmin" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked ExposeInvitationURLsToTeamAdminConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonExposeInvitationURLsToTeamAdminConfig objectSchema = pure ExposeInvitationURLsToTeamAdminConfig @@ -1051,12 +1020,17 @@ data OutlookCalIntegrationConfig = OutlookCalIntegrationConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform OutlookCalIntegrationConfig) +instance Default OutlookCalIntegrationConfig where + def = OutlookCalIntegrationConfig + instance RenderableSymbol OutlookCalIntegrationConfig where renderSymbol = "OutlookCalIntegrationConfig" +instance Default (LockableFeature OutlookCalIntegrationConfig) where + def = defLockedFeature + instance IsFeatureConfig OutlookCalIntegrationConfig where type FeatureSymbol OutlookCalIntegrationConfig = "outlookCalIntegration" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked OutlookCalIntegrationConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonOutlookCalIntegrationConfig objectSchema = pure OutlookCalIntegrationConfig @@ -1074,6 +1048,9 @@ data MlsE2EIdConfig = MlsE2EIdConfig } deriving stock (Eq, Show, Generic) +instance Default MlsE2EIdConfig where + def = MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) Nothing Nothing False + instance RenderableSymbol MlsE2EIdConfig where renderSymbol = "MlsE2EIdConfig" @@ -1116,11 +1093,11 @@ instance ToSchema MlsE2EIdConfig where \this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: \ \`https://acme.example.com/acme/provisioner1/discovery`." +instance Default (LockableFeature MlsE2EIdConfig) where + def = defLockedFeature + instance IsFeatureConfig MlsE2EIdConfig where type FeatureSymbol MlsE2EIdConfig = "mlsE2EId" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusUnlocked defValue FeatureTTLUnlimited - where - defValue = MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) Nothing Nothing False featureSingleton = FeatureSingletonMlsE2EIdConfig objectSchema = field "config" schema @@ -1133,6 +1110,9 @@ data MlsMigrationConfig = MlsMigrationConfig } deriving stock (Eq, Show, Generic) +instance Default MlsMigrationConfig where + def = MlsMigrationConfig Nothing Nothing + instance RenderableSymbol MlsMigrationConfig where renderSymbol = "MlsMigrationConfig" @@ -1153,12 +1133,12 @@ instance ToSchema MlsMigrationConfig where <$> startTime .= maybe_ (optField "startTime" utcTimeSchema) <*> finaliseRegardlessAfter .= maybe_ (optField "finaliseRegardlessAfter" utcTimeSchema) +instance Default (LockableFeature MlsMigrationConfig) where + def = defLockedFeature + instance IsFeatureConfig MlsMigrationConfig where type FeatureSymbol MlsMigrationConfig = "mlsMigration" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked defValue FeatureTTLUnlimited - where - defValue = MlsMigrationConfig Nothing Nothing - featureSingleton = FeatureSingletonMlsMigration + featureSingleton = FeatureSingletonMlsMigrationConfig objectSchema = field "config" schema ---------------------------------------------------------------------- @@ -1169,6 +1149,9 @@ data EnforceFileDownloadLocationConfig = EnforceFileDownloadLocationConfig } deriving stock (Eq, Show, Generic) +instance Default EnforceFileDownloadLocationConfig where + def = EnforceFileDownloadLocationConfig Nothing + instance RenderableSymbol EnforceFileDownloadLocationConfig where renderSymbol = "EnforceFileDownloadLocationConfig" @@ -1181,9 +1164,11 @@ instance ToSchema EnforceFileDownloadLocationConfig where EnforceFileDownloadLocationConfig <$> enforcedDownloadLocation .= maybe_ (optField "enforcedDownloadLocation" schema) +instance Default (LockableFeature EnforceFileDownloadLocationConfig) where + def = defLockedFeature + instance IsFeatureConfig EnforceFileDownloadLocationConfig where type FeatureSymbol EnforceFileDownloadLocationConfig = "enforceFileDownloadLocation" - defFeatureStatus = withStatus FeatureStatusDisabled LockStatusLocked (EnforceFileDownloadLocationConfig Nothing) FeatureTTLUnlimited featureSingleton = FeatureSingletonEnforceFileDownloadLocationConfig objectSchema = field "config" schema @@ -1199,12 +1184,17 @@ data LimitedEventFanoutConfig = LimitedEventFanoutConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform LimitedEventFanoutConfig) +instance Default LimitedEventFanoutConfig where + def = LimitedEventFanoutConfig + instance RenderableSymbol LimitedEventFanoutConfig where renderSymbol = "LimitedEventFanoutConfig" +instance Default (LockableFeature LimitedEventFanoutConfig) where + def = defUnlockedFeature + instance IsFeatureConfig LimitedEventFanoutConfig where type FeatureSymbol LimitedEventFanoutConfig = "limitedEventFanout" - defFeatureStatus = withStatus FeatureStatusEnabled LockStatusUnlocked LimitedEventFanoutConfig FeatureTTLUnlimited featureSingleton = FeatureSingletonLimitedEventFanoutConfig objectSchema = pure LimitedEventFanoutConfig @@ -1271,120 +1261,108 @@ instance Cass.Cql FeatureStatus where toCql FeatureStatusDisabled = Cass.CqlInt 0 toCql FeatureStatusEnabled = Cass.CqlInt 1 -defFeatureStatusNoLock :: (IsFeatureConfig cfg) => WithStatusNoLock cfg -defFeatureStatusNoLock = forgetLock defFeatureStatus - --- FUTUREWORK: rewrite using SOP -data AllFeatures f = AllFeatures - { afcLegalholdStatus :: f LegalholdConfig, - afcSSOStatus :: f SSOConfig, - afcTeamSearchVisibilityAvailable :: f SearchVisibilityAvailableConfig, - afcSearchVisibilityInboundConfig :: f SearchVisibilityInboundConfig, - afcValidateSAMLEmails :: f ValidateSAMLEmailsConfig, - afcDigitalSignatures :: f DigitalSignaturesConfig, - afcAppLock :: f AppLockConfig, - afcFileSharing :: f FileSharingConfig, - afcClassifiedDomains :: f ClassifiedDomainsConfig, - afcConferenceCalling :: f ConferenceCallingConfig, - afcSelfDeletingMessages :: f SelfDeletingMessagesConfig, - afcGuestLink :: f GuestLinksConfig, - afcSndFactorPasswordChallenge :: f SndFactorPasswordChallengeConfig, - afcMLS :: f MLSConfig, - afcExposeInvitationURLsToTeamAdmin :: f ExposeInvitationURLsToTeamAdminConfig, - afcOutlookCalIntegration :: f OutlookCalIntegrationConfig, - afcMlsE2EId :: f MlsE2EIdConfig, - afcMlsMigration :: f MlsMigrationConfig, - afcEnforceFileDownloadLocation :: f EnforceFileDownloadLocationConfig, - afcLimitedEventFanout :: f LimitedEventFanoutConfig - } - -type AllFeatureConfigs = AllFeatures WithStatus +-- | list of available features config types +type Features :: [Type] +type Features = + [ LegalholdConfig, + SSOConfig, + SearchVisibilityAvailableConfig, + SearchVisibilityInboundConfig, + ValidateSAMLEmailsConfig, + DigitalSignaturesConfig, + AppLockConfig, + FileSharingConfig, + ClassifiedDomainsConfig, + ConferenceCallingConfig, + SelfDeletingMessagesConfig, + GuestLinksConfig, + SndFactorPasswordChallengeConfig, + MLSConfig, + ExposeInvitationURLsToTeamAdminConfig, + OutlookCalIntegrationConfig, + MlsE2EIdConfig, + MlsMigrationConfig, + EnforceFileDownloadLocationConfig, + LimitedEventFanoutConfig + ] + +-- | list of available features as a record +type AllFeatures f = NP f Features + +-- | 'AllFeatures' specialised to the 'LockableFeature' functor +type AllFeatureConfigs = AllFeatures LockableFeature + +class (Default (LockableFeature cfg)) => LockableFeatureDefault cfg + +instance (Default (LockableFeature cfg)) => LockableFeatureDefault cfg instance Default AllFeatureConfigs where - def = - AllFeatures - { afcLegalholdStatus = defFeatureStatus, - afcSSOStatus = defFeatureStatus, - afcTeamSearchVisibilityAvailable = defFeatureStatus, - afcSearchVisibilityInboundConfig = defFeatureStatus, - afcValidateSAMLEmails = defFeatureStatus, - afcDigitalSignatures = defFeatureStatus, - afcAppLock = defFeatureStatus, - afcFileSharing = defFeatureStatus, - afcClassifiedDomains = defFeatureStatus, - afcConferenceCalling = defFeatureStatus, - afcSelfDeletingMessages = defFeatureStatus, - afcGuestLink = defFeatureStatus, - afcSndFactorPasswordChallenge = defFeatureStatus, - afcMLS = defFeatureStatus, - afcExposeInvitationURLsToTeamAdmin = defFeatureStatus, - afcOutlookCalIntegration = defFeatureStatus, - afcMlsE2EId = defFeatureStatus, - afcMlsMigration = defFeatureStatus, - afcEnforceFileDownloadLocation = defFeatureStatus, - afcLimitedEventFanout = defFeatureStatus - } + def = hcpure (Proxy @LockableFeatureDefault) def + +-- | object schema for nary products +class HObjectSchema c xs where + hobjectSchema :: (forall cfg. (c cfg) => ObjectSchema SwaggerDoc (f cfg)) -> ObjectSchema SwaggerDoc (NP f xs) + +instance HObjectSchema c '[] where + hobjectSchema _ = pure Nil + +instance (HObjectSchema c xs, c x) => HObjectSchema c ((x :: Type) : xs) where + hobjectSchema f = (:*) <$> hd .= f <*> tl .= hobjectSchema @c @xs f + +-- | constraint synonym for 'ToSchema' 'AllFeatureConfigs' +class (IsFeatureConfig cfg, ToSchema cfg, KnownSymbol (FeatureSymbol cfg)) => FeatureFieldConstraints cfg + +instance (IsFeatureConfig cfg, ToSchema cfg, KnownSymbol (FeatureSymbol cfg)) => FeatureFieldConstraints cfg instance ToSchema AllFeatureConfigs where schema = - object "AllFeatureConfigs" $ - AllFeatures - <$> afcLegalholdStatus .= featureField - <*> afcSSOStatus .= featureField - <*> afcTeamSearchVisibilityAvailable .= featureField - <*> afcSearchVisibilityInboundConfig .= featureField - <*> afcValidateSAMLEmails .= featureField - <*> afcDigitalSignatures .= featureField - <*> afcAppLock .= featureField - <*> afcFileSharing .= featureField - <*> afcClassifiedDomains .= featureField - <*> afcConferenceCalling .= featureField - <*> afcSelfDeletingMessages .= featureField - <*> afcGuestLink .= featureField - <*> afcSndFactorPasswordChallenge .= featureField - <*> afcMLS .= featureField - <*> afcExposeInvitationURLsToTeamAdmin .= featureField - <*> afcOutlookCalIntegration .= featureField - <*> afcMlsE2EId .= featureField - <*> afcMlsMigration .= featureField - <*> afcEnforceFileDownloadLocation .= featureField - <*> afcLimitedEventFanout .= featureField + object "AllFeatureConfigs" $ hobjectSchema @FeatureFieldConstraints featureField where - featureField :: - forall cfg. - (IsFeatureConfig cfg, ToSchema cfg, KnownSymbol (FeatureSymbol cfg)) => - ObjectSchema SwaggerDoc (WithStatus cfg) + featureField :: forall cfg. (FeatureFieldConstraints cfg) => ObjectSchema SwaggerDoc (LockableFeature cfg) featureField = field (T.pack (symbolVal (Proxy @(FeatureSymbol cfg)))) schema +class (Arbitrary cfg, IsFeatureConfig cfg) => ArbitraryFeatureConfig cfg + +instance (Arbitrary cfg, IsFeatureConfig cfg) => ArbitraryFeatureConfig cfg + instance Arbitrary AllFeatureConfigs where - arbitrary = - AllFeatures - <$> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary - <*> arbitrary + arbitrary = hsequence' $ hcpure (Proxy @ArbitraryFeatureConfig) (Comp arbitrary) -makeLenses ''ImplicitLockStatus +-- | FUTUREWORK: 'NpProject' and 'NpUpdate' can be useful for more than +-- features. Maybe they should be moved somewhere else. +class NpProject x xs where + npProject' :: Proxy x -> NP f xs -> f x + +instance {-# OVERLAPPING #-} NpProject x (x : xs) where + npProject' _ (x :* _) = x + +instance (NpProject x xs) => NpProject x (y : xs) where + npProject' p (_ :* xs) = npProject' p xs -deriving instance Show AllFeatureConfigs +instance (TypeError ('ShowType x :<>: 'Text " not found")) => NpProject x '[] where + npProject' = error "npProject': someone naughty removed the type error constraint" -deriving instance Eq AllFeatureConfigs +-- | Get the first field of a given type out of an @'NP' f xs@. +npProject :: forall x f xs. (NpProject x xs) => NP f xs -> f x +npProject = npProject' (Proxy @x) + +class NpUpdate x xs where + npUpdate' :: Proxy x -> f x -> NP f xs -> NP f xs + +instance {-# OVERLAPPING #-} NpUpdate x (x : xs) where + npUpdate' _ x (_ :* xs) = x :* xs + +instance (NpUpdate x xs) => NpUpdate x (y : xs) where + npUpdate' p x (y :* xs) = y :* npUpdate' p x xs + +instance (TypeError ('ShowType x :<>: 'Text " not found")) => NpUpdate x '[] where + npUpdate' = error "npUpdate': someone naughty removed the type error constraint" + +-- | Update the first field of a given type in an @'NP' f xs@. +npUpdate :: forall x f xs. (NpUpdate x xs) => f x -> NP f xs -> NP f xs +npUpdate = npUpdate' (Proxy @x) + +makeLenses ''ImplicitLockStatus deriving via (Schema AllFeatureConfigs) instance (FromJSON AllFeatureConfigs) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs index e999ab389a2..9aece18a8cb 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs @@ -21,11 +21,11 @@ import Imports import Test.Tasty import Test.Tasty.HUnit import Test.Wire.API.Golden.Generated.Invite_user (testObject_Invite_user_2) +import Test.Wire.API.Golden.Generated.LockableFeature_team import Test.Wire.API.Golden.Generated.MemberUpdateData_user import Test.Wire.API.Golden.Generated.NewOtrMessage_user import Test.Wire.API.Golden.Generated.RmClient_user import Test.Wire.API.Golden.Generated.SimpleMember_user -import Test.Wire.API.Golden.Generated.WithStatus_team import Test.Wire.API.Golden.Runner import Wire.API.Conversation (Conversation, MemberUpdate, OtherMemberUpdate) import Wire.API.User (NewUser, NewUserPublic) @@ -90,6 +90,6 @@ tests = (Just "only managed-by-Wire users can be created here.") "testObject_NewUserPublic_user_1-3.json" ], - testCase "WithStatus_ConferenceCallingConfig" $ - testFromJSONObject testObject_WithStatus_team_14 "testObject_WithStatus_team_14.json" + testCase "LockableFeature_ConferenceCallingConfig" $ + testFromJSONObject testObject_LockableFeature_team_14 "testObject_LockableFeature_team_14.json" ] 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 cf0a3e20eaf..36af8c92ec1 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 @@ -86,6 +86,7 @@ import Test.Wire.API.Golden.Generated.Event_conversation qualified import Test.Wire.API.Golden.Generated.Event_featureConfig qualified import Test.Wire.API.Golden.Generated.Event_team qualified import Test.Wire.API.Golden.Generated.Event_user qualified +import Test.Wire.API.Golden.Generated.Feature_team qualified import Test.Wire.API.Golden.Generated.HandleUpdate_user qualified import Test.Wire.API.Golden.Generated.InvitationCode_user qualified import Test.Wire.API.Golden.Generated.InvitationList_team qualified @@ -99,6 +100,8 @@ import Test.Wire.API.Golden.Generated.LimitedQualifiedUserIdList_user qualified import Test.Wire.API.Golden.Generated.ListType_team qualified import Test.Wire.API.Golden.Generated.LocaleUpdate_user qualified import Test.Wire.API.Golden.Generated.Locale_user qualified +import Test.Wire.API.Golden.Generated.LockableFeaturePatch_team qualified +import Test.Wire.API.Golden.Generated.LockableFeature_team qualified import Test.Wire.API.Golden.Generated.LoginCodeTimeout_user qualified import Test.Wire.API.Golden.Generated.LoginCode_user qualified import Test.Wire.API.Golden.Generated.ManagedBy_user qualified @@ -227,9 +230,6 @@ import Test.Wire.API.Golden.Generated.VerificationAction_user qualified import Test.Wire.API.Golden.Generated.VerifyDeleteUser_user qualified import Test.Wire.API.Golden.Generated.ViewLegalHoldServiceInfo_team qualified import Test.Wire.API.Golden.Generated.ViewLegalHoldService_team qualified -import Test.Wire.API.Golden.Generated.WithStatusNoLock_team qualified -import Test.Wire.API.Golden.Generated.WithStatusPatch_team qualified -import Test.Wire.API.Golden.Generated.WithStatus_team qualified import Test.Wire.API.Golden.Generated.Wrapped_20_22some_5fint_22_20Int_user qualified import Test.Wire.API.Golden.Runner import Wire.API.Routes.Version @@ -1181,110 +1181,110 @@ tests = "testObject_TeamConversationList_team_2.json" ) ], - testGroup "Golden: WithStatusNoLock_team 1" $ + testGroup "Golden: Feature_team 1" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_1, "testObject_WithStatusNoLock_team_1.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_2, "testObject_WithStatusNoLock_team_2.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_3, "testObject_WithStatusNoLock_team_3.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_1, "testObject_Feature_team_1.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_2, "testObject_Feature_team_2.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_3, "testObject_Feature_team_3.json") ], - testGroup "Golden: WithStatusNoLock_team 2" $ + testGroup "Golden: Feature_team 2" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_4, "testObject_WithStatusNoLock_team_4.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_5, "testObject_WithStatusNoLock_team_5.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_6, "testObject_WithStatusNoLock_team_6.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_4, "testObject_Feature_team_4.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_5, "testObject_Feature_team_5.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_6, "testObject_Feature_team_6.json") ], - testGroup "Golden: WithStatusNoLock_team 3" $ + testGroup "Golden: Feature_team 3" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_7, "testObject_WithStatusNoLock_team_7.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_8, "testObject_WithStatusNoLock_team_8.json"), - (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_9, "testObject_WithStatusNoLock_team_9.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_7, "testObject_Feature_team_7.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_8, "testObject_Feature_team_8.json"), + (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_9, "testObject_Feature_team_9.json") ], - testGroup "Golden: WithStatusNoLock_team 4" $ + testGroup "Golden: Feature_team 4" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_10, "testObject_WithStatusNoLock_team_10.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_10, "testObject_Feature_team_10.json") ], - testGroup "Golden: WithStatusNoLock_team 5" $ + testGroup "Golden: Feature_team 5" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_11, "testObject_WithStatusNoLock_team_11.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_11, "testObject_Feature_team_11.json") ], - testGroup "Golden: WithStatusNoLock_team 6" $ + testGroup "Golden: Feature_team 6" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_12, "testObject_WithStatusNoLock_team_12.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_12, "testObject_Feature_team_12.json") ], - testGroup "Golden: WithStatusNoLock_team 7" $ + testGroup "Golden: Feature_team 7" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_13, "testObject_WithStatusNoLock_team_13.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_13, "testObject_Feature_team_13.json") ], - testGroup "Golden: WithStatusNoLock_team 8" $ + testGroup "Golden: Feature_team 8" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_14, "testObject_WithStatusNoLock_team_14.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_14, "testObject_Feature_team_14.json") ], - testGroup "Golden: WithStatusNoLock_team 9" $ + testGroup "Golden: Feature_team 9" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_15, "testObject_WithStatusNoLock_team_15.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_15, "testObject_Feature_team_15.json") ], - testGroup "Golden: WithStatusNoLock_team 10" $ + testGroup "Golden: Feature_team 10" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_16, "testObject_WithStatusNoLock_team_16.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_16, "testObject_Feature_team_16.json") ], - testGroup "Golden: WithStatusNoLock_team 11" $ + testGroup "Golden: Feature_team 11" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatusNoLock_team.testObject_WithStatusNoLock_team_17, "testObject_WithStatusNoLock_team_17.json") + [ (Test.Wire.API.Golden.Generated.Feature_team.testObject_Feature_team_17, "testObject_Feature_team_17.json") ], - testGroup "Golden: WithStatus_team 1" $ + testGroup "Golden: LockableFeature_team 1" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_1, "testObject_WithStatus_team_1.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_2, "testObject_WithStatus_team_2.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_3, "testObject_WithStatus_team_3.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_1, "testObject_LockableFeature_team_1.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_2, "testObject_LockableFeature_team_2.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_3, "testObject_LockableFeature_team_3.json") ], - testGroup "Golden: WithStatus_team 2" $ + testGroup "Golden: LockableFeature_team 2" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_4, "testObject_WithStatus_team_4.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_5, "testObject_WithStatus_team_5.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_6, "testObject_WithStatus_team_6.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_4, "testObject_LockableFeature_team_4.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_5, "testObject_LockableFeature_team_5.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_6, "testObject_LockableFeature_team_6.json") ], - testGroup "Golden: WithStatus_team 3" $ + testGroup "Golden: LockableFeature_team 3" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_7, "testObject_WithStatus_team_7.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_8, "testObject_WithStatus_team_8.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_9, "testObject_WithStatus_team_9.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_7, "testObject_LockableFeature_team_7.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_8, "testObject_LockableFeature_team_8.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_9, "testObject_LockableFeature_team_9.json") ], - testGroup "Golden: WithStatus_team 4" $ + testGroup "Golden: LockableFeature_team 4" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_10, "testObject_WithStatus_team_10.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_10, "testObject_LockableFeature_team_10.json") ], - testGroup "Golden: WithStatus_team 5" $ + testGroup "Golden: LockableFeature_team 5" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_11, "testObject_WithStatus_team_11.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_11, "testObject_LockableFeature_team_11.json") ], - testGroup "Golden: WithStatus_team 6" $ + testGroup "Golden: LockableFeature_team 6" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_12, "testObject_WithStatus_team_12.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_12, "testObject_LockableFeature_team_12.json") ], - testGroup "Golden: WithStatus_team 7" $ + testGroup "Golden: LockableFeature_team 7" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_13, "testObject_WithStatus_team_13.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_13, "testObject_LockableFeature_team_13.json") ], - testGroup "Golden: WithStatus_team 8" $ + testGroup "Golden: LockableFeature_team 8" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_14, "testObject_WithStatus_team_14.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_14, "testObject_LockableFeature_team_14.json") ], - testGroup "Golden: WithStatus_team 9" $ + testGroup "Golden: LockableFeature_team 9" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_15, "testObject_WithStatus_team_15.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_15, "testObject_LockableFeature_team_15.json") ], - testGroup "Golden: WithStatus_team 10" $ + testGroup "Golden: LockableFeature_team 10" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_16, "testObject_WithStatus_team_16.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_16, "testObject_LockableFeature_team_16.json") ], - testGroup "Golden: WithStatus_team 11" $ + testGroup "Golden: LockableFeature_team 11" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_17, "testObject_WithStatus_team_17.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_17, "testObject_LockableFeature_team_17.json") ], - testGroup "Golden: WithStatus_team 12" $ + testGroup "Golden: LockableFeature_team 12" $ testObjects - [ (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_18, "testObject_WithStatus_team_18.json"), - (Test.Wire.API.Golden.Generated.WithStatus_team.testObject_WithStatus_team_19, "testObject_WithStatus_team_19.json") + [ (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_18, "testObject_LockableFeature_team_18.json"), + (Test.Wire.API.Golden.Generated.LockableFeature_team.testObject_LockableFeature_team_19, "testObject_LockableFeature_team_19.json") ], 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")], @@ -1339,81 +1339,81 @@ tests = (Test.Wire.API.Golden.Generated.VerificationAction_user.testObject_VerificationAction_user_3, "testObject_VerificationAction_user_3") ], testGroup - "Golden: WithStatusPatch_team 1" + "Golden: LockableFeaturePatch_team 1" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_1, "testObject_WithStatusPatch_team_1.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_1, "testObject_LockableFeaturePatch_team_1.json")], testGroup - "Golden: WithStatusPatch_team 2" + "Golden: LockableFeaturePatch_team 2" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_2, "testObject_WithStatusPatch_team_2.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_2, "testObject_LockableFeaturePatch_team_2.json")], testGroup - "Golden: WithStatusPatch_team 3" + "Golden: LockableFeaturePatch_team 3" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_3, "testObject_WithStatusPatch_team_3.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_3, "testObject_LockableFeaturePatch_team_3.json")], testGroup - "Golden: WithStatusPatch_team 4" + "Golden: LockableFeaturePatch_team 4" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_4, "testObject_WithStatusPatch_team_4.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_4, "testObject_LockableFeaturePatch_team_4.json")], testGroup - "Golden: WithStatusPatch_team 5" + "Golden: LockableFeaturePatch_team 5" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_5, "testObject_WithStatusPatch_team_5.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_5, "testObject_LockableFeaturePatch_team_5.json")], testGroup - "Golden: WithStatusPatch_team 6" + "Golden: LockableFeaturePatch_team 6" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_6, "testObject_WithStatusPatch_team_6.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_6, "testObject_LockableFeaturePatch_team_6.json")], testGroup - "Golden: WithStatusPatch_team 7" + "Golden: LockableFeaturePatch_team 7" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_7, "testObject_WithStatusPatch_team_7.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_7, "testObject_LockableFeaturePatch_team_7.json")], testGroup - "Golden: WithStatusPatch_team 8" + "Golden: LockableFeaturePatch_team 8" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_8, "testObject_WithStatusPatch_team_8.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_8, "testObject_LockableFeaturePatch_team_8.json")], testGroup - "Golden: WithStatusPatch_team 9" + "Golden: LockableFeaturePatch_team 9" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_9, "testObject_WithStatusPatch_team_9.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_9, "testObject_LockableFeaturePatch_team_9.json")], testGroup - "Golden: WithStatusPatch_team 10" + "Golden: LockableFeaturePatch_team 10" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_10, "testObject_WithStatusPatch_team_10.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_10, "testObject_LockableFeaturePatch_team_10.json")], testGroup - "Golden: WithStatusPatch_team 11" + "Golden: LockableFeaturePatch_team 11" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_11, "testObject_WithStatusPatch_team_11.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_11, "testObject_LockableFeaturePatch_team_11.json")], testGroup - "Golden: WithStatusPatch_team 12" + "Golden: LockableFeaturePatch_team 12" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_12, "testObject_WithStatusPatch_team_12.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_12, "testObject_LockableFeaturePatch_team_12.json")], testGroup - "Golden: WithStatusPatch_team 13" + "Golden: LockableFeaturePatch_team 13" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_13, "testObject_WithStatusPatch_team_13.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_13, "testObject_LockableFeaturePatch_team_13.json")], testGroup - "Golden: WithStatusPatch_team 14" + "Golden: LockableFeaturePatch_team 14" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_14, "testObject_WithStatusPatch_team_14.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_14, "testObject_LockableFeaturePatch_team_14.json")], testGroup - "Golden: WithStatusPatch_team 15" + "Golden: LockableFeaturePatch_team 15" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_15, "testObject_WithStatusPatch_team_15.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_15, "testObject_LockableFeaturePatch_team_15.json")], testGroup - "Golden: WithStatusPatch_team 16" + "Golden: LockableFeaturePatch_team 16" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_16, "testObject_WithStatusPatch_team_16.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_16, "testObject_LockableFeaturePatch_team_16.json")], testGroup - "Golden: WithStatusPatch_team 17" + "Golden: LockableFeaturePatch_team 17" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_17, "testObject_WithStatusPatch_team_17.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_17, "testObject_LockableFeaturePatch_team_17.json")], testGroup - "Golden: WithStatusPatch_team 18" + "Golden: LockableFeaturePatch_team 18" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_18, "testObject_WithStatusPatch_team_18.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_18, "testObject_LockableFeaturePatch_team_18.json")], testGroup - "Golden: WithStatusPatch_team 19" + "Golden: LockableFeaturePatch_team 19" $ testObjects - [(Test.Wire.API.Golden.Generated.WithStatusPatch_team.testObject_WithStatusPatch_team_19, "testObject_WithStatusPatch_team_19.json")], + [(Test.Wire.API.Golden.Generated.LockableFeaturePatch_team.testObject_LockableFeaturePatch_team_19, "testObject_LockableFeaturePatch_team_19.json")], testGroup "Golden: Event_FeatureConfig" $ testObjects diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Feature_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Feature_team.hs new file mode 100644 index 00000000000..540fa355c3f --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Feature_team.hs @@ -0,0 +1,75 @@ +{-# LANGUAGE OverloadedLists #-} + +-- 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.Generated.Feature_team where + +import Data.Domain +import Imports +import Wire.API.Team.Feature + +testObject_Feature_team_1 :: Feature AppLockConfig +testObject_Feature_team_1 = Feature FeatureStatusEnabled (AppLockConfig (EnforceAppLock False) (-98)) + +testObject_Feature_team_2 :: Feature AppLockConfig +testObject_Feature_team_2 = Feature FeatureStatusEnabled (AppLockConfig (EnforceAppLock True) 0) + +testObject_Feature_team_3 :: Feature AppLockConfig +testObject_Feature_team_3 = Feature FeatureStatusEnabled (AppLockConfig (EnforceAppLock True) 111) + +testObject_Feature_team_4 :: Feature SelfDeletingMessagesConfig +testObject_Feature_team_4 = Feature FeatureStatusEnabled (SelfDeletingMessagesConfig (-97)) + +testObject_Feature_team_5 :: Feature SelfDeletingMessagesConfig +testObject_Feature_team_5 = Feature FeatureStatusEnabled (SelfDeletingMessagesConfig 0) + +testObject_Feature_team_6 :: Feature SelfDeletingMessagesConfig +testObject_Feature_team_6 = Feature FeatureStatusEnabled (SelfDeletingMessagesConfig 77) + +testObject_Feature_team_7 :: Feature ClassifiedDomainsConfig +testObject_Feature_team_7 = Feature FeatureStatusEnabled (ClassifiedDomainsConfig []) + +testObject_Feature_team_8 :: Feature ClassifiedDomainsConfig +testObject_Feature_team_8 = Feature FeatureStatusEnabled (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"]) + +testObject_Feature_team_9 :: Feature ClassifiedDomainsConfig +testObject_Feature_team_9 = Feature FeatureStatusEnabled (ClassifiedDomainsConfig [Domain "test.foobar"]) + +testObject_Feature_team_10 :: Feature SSOConfig +testObject_Feature_team_10 = Feature FeatureStatusDisabled SSOConfig + +testObject_Feature_team_11 :: Feature SearchVisibilityAvailableConfig +testObject_Feature_team_11 = Feature FeatureStatusEnabled SearchVisibilityAvailableConfig + +testObject_Feature_team_12 :: Feature ValidateSAMLEmailsConfig +testObject_Feature_team_12 = Feature FeatureStatusDisabled ValidateSAMLEmailsConfig + +testObject_Feature_team_13 :: Feature DigitalSignaturesConfig +testObject_Feature_team_13 = Feature FeatureStatusEnabled DigitalSignaturesConfig + +testObject_Feature_team_14 :: Feature ConferenceCallingConfig +testObject_Feature_team_14 = Feature FeatureStatusDisabled (ConferenceCallingConfig One2OneCallsSft) + +testObject_Feature_team_15 :: Feature GuestLinksConfig +testObject_Feature_team_15 = Feature FeatureStatusEnabled GuestLinksConfig + +testObject_Feature_team_16 :: Feature SndFactorPasswordChallengeConfig +testObject_Feature_team_16 = Feature FeatureStatusDisabled SndFactorPasswordChallengeConfig + +testObject_Feature_team_17 :: Feature SearchVisibilityInboundConfig +testObject_Feature_team_17 = Feature FeatureStatusEnabled SearchVisibilityInboundConfig diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeaturePatch_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeaturePatch_team.hs new file mode 100644 index 00000000000..478398eb383 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeaturePatch_team.hs @@ -0,0 +1,81 @@ +{-# LANGUAGE OverloadedLists #-} + +-- 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.Generated.LockableFeaturePatch_team where + +import Data.Domain +import Imports +import Wire.API.Team.Feature + +testObject_LockableFeaturePatch_team_1 :: LockableFeaturePatch AppLockConfig +testObject_LockableFeaturePatch_team_1 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (AppLockConfig (EnforceAppLock False) (-98))) + +testObject_LockableFeaturePatch_team_2 :: LockableFeaturePatch AppLockConfig +testObject_LockableFeaturePatch_team_2 = LockableFeaturePatch Nothing Nothing (Just (AppLockConfig (EnforceAppLock True) 0)) + +testObject_LockableFeaturePatch_team_3 :: LockableFeaturePatch AppLockConfig +testObject_LockableFeaturePatch_team_3 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just (AppLockConfig (EnforceAppLock True) 111)) + +testObject_LockableFeaturePatch_team_4 :: LockableFeaturePatch SelfDeletingMessagesConfig +testObject_LockableFeaturePatch_team_4 = LockableFeaturePatch (Just FeatureStatusEnabled) Nothing (Just (SelfDeletingMessagesConfig (-97))) + +testObject_LockableFeaturePatch_team_5 :: LockableFeaturePatch SelfDeletingMessagesConfig +testObject_LockableFeaturePatch_team_5 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (SelfDeletingMessagesConfig 0)) + +testObject_LockableFeaturePatch_team_6 :: LockableFeaturePatch SelfDeletingMessagesConfig +testObject_LockableFeaturePatch_team_6 = LockableFeaturePatch (Just FeatureStatusEnabled) Nothing (Just (SelfDeletingMessagesConfig 77)) + +testObject_LockableFeaturePatch_team_7 :: LockableFeaturePatch ClassifiedDomainsConfig +testObject_LockableFeaturePatch_team_7 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just (ClassifiedDomainsConfig [])) + +testObject_LockableFeaturePatch_team_8 :: LockableFeaturePatch ClassifiedDomainsConfig +testObject_LockableFeaturePatch_team_8 = LockableFeaturePatch Nothing (Just LockStatusLocked) (Just (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"])) + +testObject_LockableFeaturePatch_team_9 :: LockableFeaturePatch ClassifiedDomainsConfig +testObject_LockableFeaturePatch_team_9 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (ClassifiedDomainsConfig [Domain "test.foobar"])) + +testObject_LockableFeaturePatch_team_10 :: LockableFeaturePatch SSOConfig +testObject_LockableFeaturePatch_team_10 = LockableFeaturePatch (Just FeatureStatusDisabled) (Just LockStatusLocked) (Just SSOConfig) + +testObject_LockableFeaturePatch_team_11 :: LockableFeaturePatch SearchVisibilityAvailableConfig +testObject_LockableFeaturePatch_team_11 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just SearchVisibilityAvailableConfig) + +testObject_LockableFeaturePatch_team_12 :: LockableFeaturePatch ValidateSAMLEmailsConfig +testObject_LockableFeaturePatch_team_12 = LockableFeaturePatch (Just FeatureStatusDisabled) Nothing (Just ValidateSAMLEmailsConfig) + +testObject_LockableFeaturePatch_team_13 :: LockableFeaturePatch DigitalSignaturesConfig +testObject_LockableFeaturePatch_team_13 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just DigitalSignaturesConfig) + +testObject_LockableFeaturePatch_team_14 :: LockableFeaturePatch ConferenceCallingConfig +testObject_LockableFeaturePatch_team_14 = LockableFeaturePatch Nothing (Just LockStatusUnlocked) (Just (ConferenceCallingConfig One2OneCallsSft)) + +testObject_LockableFeaturePatch_team_15 :: LockableFeaturePatch GuestLinksConfig +testObject_LockableFeaturePatch_team_15 = LockableFeaturePatch (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just GuestLinksConfig) + +testObject_LockableFeaturePatch_team_16 :: LockableFeaturePatch SndFactorPasswordChallengeConfig +testObject_LockableFeaturePatch_team_16 = LockableFeaturePatch (Just FeatureStatusDisabled) (Just LockStatusUnlocked) (Just SndFactorPasswordChallengeConfig) + +testObject_LockableFeaturePatch_team_17 :: LockableFeaturePatch SearchVisibilityInboundConfig +testObject_LockableFeaturePatch_team_17 = LockableFeaturePatch (Just FeatureStatusEnabled) Nothing (Just SearchVisibilityInboundConfig) + +testObject_LockableFeaturePatch_team_18 :: LockableFeaturePatch GuestLinksConfig +testObject_LockableFeaturePatch_team_18 = LockableFeaturePatch (Just FeatureStatusEnabled) Nothing Nothing + +testObject_LockableFeaturePatch_team_19 :: LockableFeaturePatch SelfDeletingMessagesConfig +testObject_LockableFeaturePatch_team_19 = LockableFeaturePatch Nothing (Just LockStatusUnlocked) Nothing diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs new file mode 100644 index 00000000000..8c4f9562f39 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LockableFeature_team.hs @@ -0,0 +1,104 @@ +{-# LANGUAGE OverloadedLists #-} + +-- 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.Generated.LockableFeature_team where + +import Data.ByteString.Conversion (parser, runParser) +import Data.Domain +import Data.Misc +import Imports +import Wire.API.Team.Feature + +testObject_LockableFeature_team_1 :: LockableFeature AppLockConfig +testObject_LockableFeature_team_1 = LockableFeature FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock False) (-98)) + +testObject_LockableFeature_team_2 :: LockableFeature AppLockConfig +testObject_LockableFeature_team_2 = LockableFeature FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock True) 0) + +testObject_LockableFeature_team_3 :: LockableFeature AppLockConfig +testObject_LockableFeature_team_3 = LockableFeature FeatureStatusEnabled LockStatusLocked (AppLockConfig (EnforceAppLock True) 111) + +testObject_LockableFeature_team_4 :: LockableFeature SelfDeletingMessagesConfig +testObject_LockableFeature_team_4 = LockableFeature FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig (-97)) + +testObject_LockableFeature_team_5 :: LockableFeature SelfDeletingMessagesConfig +testObject_LockableFeature_team_5 = LockableFeature FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig 0) + +testObject_LockableFeature_team_6 :: LockableFeature SelfDeletingMessagesConfig +testObject_LockableFeature_team_6 = LockableFeature FeatureStatusEnabled LockStatusLocked (SelfDeletingMessagesConfig 77) + +testObject_LockableFeature_team_7 :: LockableFeature ClassifiedDomainsConfig +testObject_LockableFeature_team_7 = LockableFeature FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig []) + +testObject_LockableFeature_team_8 :: LockableFeature ClassifiedDomainsConfig +testObject_LockableFeature_team_8 = LockableFeature FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"]) + +testObject_LockableFeature_team_9 :: LockableFeature ClassifiedDomainsConfig +testObject_LockableFeature_team_9 = LockableFeature FeatureStatusEnabled LockStatusUnlocked (ClassifiedDomainsConfig [Domain "test.foobar"]) + +testObject_LockableFeature_team_10 :: LockableFeature SSOConfig +testObject_LockableFeature_team_10 = LockableFeature FeatureStatusDisabled LockStatusLocked SSOConfig + +testObject_LockableFeature_team_11 :: LockableFeature SearchVisibilityAvailableConfig +testObject_LockableFeature_team_11 = LockableFeature FeatureStatusEnabled LockStatusLocked SearchVisibilityAvailableConfig + +testObject_LockableFeature_team_12 :: LockableFeature ValidateSAMLEmailsConfig +testObject_LockableFeature_team_12 = LockableFeature FeatureStatusDisabled LockStatusLocked ValidateSAMLEmailsConfig + +testObject_LockableFeature_team_13 :: LockableFeature DigitalSignaturesConfig +testObject_LockableFeature_team_13 = LockableFeature FeatureStatusEnabled LockStatusLocked DigitalSignaturesConfig + +testObject_LockableFeature_team_14 :: LockableFeature ConferenceCallingConfig +testObject_LockableFeature_team_14 = LockableFeature FeatureStatusDisabled LockStatusUnlocked (ConferenceCallingConfig One2OneCallsTurn) + +testObject_LockableFeature_team_15 :: LockableFeature GuestLinksConfig +testObject_LockableFeature_team_15 = LockableFeature FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig + +testObject_LockableFeature_team_16 :: LockableFeature SndFactorPasswordChallengeConfig +testObject_LockableFeature_team_16 = LockableFeature FeatureStatusDisabled LockStatusUnlocked SndFactorPasswordChallengeConfig + +testObject_LockableFeature_team_17 :: LockableFeature SearchVisibilityInboundConfig +testObject_LockableFeature_team_17 = LockableFeature FeatureStatusEnabled LockStatusUnlocked SearchVisibilityInboundConfig + +testObject_LockableFeature_team_18 :: LockableFeature MlsE2EIdConfig +testObject_LockableFeature_team_18 = + LockableFeature + FeatureStatusEnabled + LockStatusLocked + ( MlsE2EIdConfig + (fromIntegral @Int (60 * 60 * 24)) + Nothing + (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com") + False + ) + +parseHttpsUrl :: ByteString -> Either String HttpsUrl +parseHttpsUrl url = runParser parser url + +testObject_LockableFeature_team_19 :: LockableFeature MlsE2EIdConfig +testObject_LockableFeature_team_19 = + LockableFeature + FeatureStatusEnabled + LockStatusLocked + ( MlsE2EIdConfig + (fromIntegral @Int (60 * 60 * 24)) + (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com") + Nothing + True + ) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusNoLock_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusNoLock_team.hs deleted file mode 100644 index efc0c52b7d5..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusNoLock_team.hs +++ /dev/null @@ -1,75 +0,0 @@ -{-# LANGUAGE OverloadedLists #-} - --- 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.Generated.WithStatusNoLock_team where - -import Data.Domain -import Imports -import Wire.API.Team.Feature - -testObject_WithStatusNoLock_team_1 :: WithStatusNoLock AppLockConfig -testObject_WithStatusNoLock_team_1 = WithStatusNoLock FeatureStatusEnabled (AppLockConfig (EnforceAppLock False) (-98)) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_2 :: WithStatusNoLock AppLockConfig -testObject_WithStatusNoLock_team_2 = WithStatusNoLock FeatureStatusEnabled (AppLockConfig (EnforceAppLock True) 0) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_3 :: WithStatusNoLock AppLockConfig -testObject_WithStatusNoLock_team_3 = WithStatusNoLock FeatureStatusEnabled (AppLockConfig (EnforceAppLock True) 111) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_4 :: WithStatusNoLock SelfDeletingMessagesConfig -testObject_WithStatusNoLock_team_4 = WithStatusNoLock FeatureStatusEnabled (SelfDeletingMessagesConfig (-97)) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_5 :: WithStatusNoLock SelfDeletingMessagesConfig -testObject_WithStatusNoLock_team_5 = WithStatusNoLock FeatureStatusEnabled (SelfDeletingMessagesConfig 0) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_6 :: WithStatusNoLock SelfDeletingMessagesConfig -testObject_WithStatusNoLock_team_6 = WithStatusNoLock FeatureStatusEnabled (SelfDeletingMessagesConfig 77) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_7 :: WithStatusNoLock ClassifiedDomainsConfig -testObject_WithStatusNoLock_team_7 = WithStatusNoLock FeatureStatusEnabled (ClassifiedDomainsConfig []) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_8 :: WithStatusNoLock ClassifiedDomainsConfig -testObject_WithStatusNoLock_team_8 = WithStatusNoLock FeatureStatusEnabled (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"]) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_9 :: WithStatusNoLock ClassifiedDomainsConfig -testObject_WithStatusNoLock_team_9 = WithStatusNoLock FeatureStatusEnabled (ClassifiedDomainsConfig [Domain "test.foobar"]) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_10 :: WithStatusNoLock SSOConfig -testObject_WithStatusNoLock_team_10 = WithStatusNoLock FeatureStatusDisabled SSOConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_11 :: WithStatusNoLock SearchVisibilityAvailableConfig -testObject_WithStatusNoLock_team_11 = WithStatusNoLock FeatureStatusEnabled SearchVisibilityAvailableConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_12 :: WithStatusNoLock ValidateSAMLEmailsConfig -testObject_WithStatusNoLock_team_12 = WithStatusNoLock FeatureStatusDisabled ValidateSAMLEmailsConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_13 :: WithStatusNoLock DigitalSignaturesConfig -testObject_WithStatusNoLock_team_13 = WithStatusNoLock FeatureStatusEnabled DigitalSignaturesConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_14 :: WithStatusNoLock ConferenceCallingConfig -testObject_WithStatusNoLock_team_14 = WithStatusNoLock FeatureStatusDisabled (ConferenceCallingConfig One2OneCallsSft) FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_15 :: WithStatusNoLock GuestLinksConfig -testObject_WithStatusNoLock_team_15 = WithStatusNoLock FeatureStatusEnabled GuestLinksConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_16 :: WithStatusNoLock SndFactorPasswordChallengeConfig -testObject_WithStatusNoLock_team_16 = WithStatusNoLock FeatureStatusDisabled SndFactorPasswordChallengeConfig FeatureTTLUnlimited - -testObject_WithStatusNoLock_team_17 :: WithStatusNoLock SearchVisibilityInboundConfig -testObject_WithStatusNoLock_team_17 = WithStatusNoLock FeatureStatusEnabled SearchVisibilityInboundConfig FeatureTTLUnlimited diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs deleted file mode 100644 index a5dd2c94955..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatusPatch_team.hs +++ /dev/null @@ -1,84 +0,0 @@ -{-# LANGUAGE OverloadedLists #-} - --- 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.Generated.WithStatusPatch_team where - -import Data.Domain -import Imports -import Wire.API.Team.Feature hiding (withStatus) - -testObject_WithStatusPatch_team_1 :: WithStatusPatch AppLockConfig -testObject_WithStatusPatch_team_1 = withStatus (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (AppLockConfig (EnforceAppLock False) (-98))) - -testObject_WithStatusPatch_team_2 :: WithStatusPatch AppLockConfig -testObject_WithStatusPatch_team_2 = withStatus Nothing Nothing (Just (AppLockConfig (EnforceAppLock True) 0)) - -testObject_WithStatusPatch_team_3 :: WithStatusPatch AppLockConfig -testObject_WithStatusPatch_team_3 = withStatus (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just (AppLockConfig (EnforceAppLock True) 111)) - -testObject_WithStatusPatch_team_4 :: WithStatusPatch SelfDeletingMessagesConfig -testObject_WithStatusPatch_team_4 = withStatus (Just FeatureStatusEnabled) Nothing (Just (SelfDeletingMessagesConfig (-97))) - -testObject_WithStatusPatch_team_5 :: WithStatusPatch SelfDeletingMessagesConfig -testObject_WithStatusPatch_team_5 = withStatus (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (SelfDeletingMessagesConfig 0)) - -testObject_WithStatusPatch_team_6 :: WithStatusPatch SelfDeletingMessagesConfig -testObject_WithStatusPatch_team_6 = withStatus (Just FeatureStatusEnabled) Nothing (Just (SelfDeletingMessagesConfig 77)) - -testObject_WithStatusPatch_team_7 :: WithStatusPatch ClassifiedDomainsConfig -testObject_WithStatusPatch_team_7 = withStatus (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just (ClassifiedDomainsConfig [])) - -testObject_WithStatusPatch_team_8 :: WithStatusPatch ClassifiedDomainsConfig -testObject_WithStatusPatch_team_8 = withStatus Nothing (Just LockStatusLocked) (Just (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"])) - -testObject_WithStatusPatch_team_9 :: WithStatusPatch ClassifiedDomainsConfig -testObject_WithStatusPatch_team_9 = withStatus (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just (ClassifiedDomainsConfig [Domain "test.foobar"])) - -testObject_WithStatusPatch_team_10 :: WithStatusPatch SSOConfig -testObject_WithStatusPatch_team_10 = withStatus (Just FeatureStatusDisabled) (Just LockStatusLocked) (Just SSOConfig) - -testObject_WithStatusPatch_team_11 :: WithStatusPatch SearchVisibilityAvailableConfig -testObject_WithStatusPatch_team_11 = withStatus (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just SearchVisibilityAvailableConfig) - -testObject_WithStatusPatch_team_12 :: WithStatusPatch ValidateSAMLEmailsConfig -testObject_WithStatusPatch_team_12 = withStatus (Just FeatureStatusDisabled) Nothing (Just ValidateSAMLEmailsConfig) - -testObject_WithStatusPatch_team_13 :: WithStatusPatch DigitalSignaturesConfig -testObject_WithStatusPatch_team_13 = withStatus (Just FeatureStatusEnabled) (Just LockStatusLocked) (Just DigitalSignaturesConfig) - -testObject_WithStatusPatch_team_14 :: WithStatusPatch ConferenceCallingConfig -testObject_WithStatusPatch_team_14 = withStatus Nothing (Just LockStatusUnlocked) (Just (ConferenceCallingConfig One2OneCallsSft)) - -testObject_WithStatusPatch_team_15 :: WithStatusPatch GuestLinksConfig -testObject_WithStatusPatch_team_15 = withStatus (Just FeatureStatusEnabled) (Just LockStatusUnlocked) (Just GuestLinksConfig) - -testObject_WithStatusPatch_team_16 :: WithStatusPatch SndFactorPasswordChallengeConfig -testObject_WithStatusPatch_team_16 = withStatus (Just FeatureStatusDisabled) (Just LockStatusUnlocked) (Just SndFactorPasswordChallengeConfig) - -testObject_WithStatusPatch_team_17 :: WithStatusPatch SearchVisibilityInboundConfig -testObject_WithStatusPatch_team_17 = withStatus (Just FeatureStatusEnabled) Nothing (Just SearchVisibilityInboundConfig) - -testObject_WithStatusPatch_team_18 :: WithStatusPatch GuestLinksConfig -testObject_WithStatusPatch_team_18 = withStatus (Just FeatureStatusEnabled) Nothing Nothing - -testObject_WithStatusPatch_team_19 :: WithStatusPatch SelfDeletingMessagesConfig -testObject_WithStatusPatch_team_19 = withStatus Nothing (Just LockStatusUnlocked) Nothing - -withStatus :: Maybe FeatureStatus -> Maybe LockStatus -> Maybe cfg -> WithStatusPatch cfg -withStatus fs ls cfg = withStatus' fs ls cfg (Just FeatureTTLUnlimited) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs deleted file mode 100644 index 6acd1c8f634..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/WithStatus_team.hs +++ /dev/null @@ -1,108 +0,0 @@ -{-# LANGUAGE OverloadedLists #-} - --- 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.Generated.WithStatus_team where - -import Data.ByteString.Conversion (parser, runParser) -import Data.Domain -import Data.Misc -import Imports -import Wire.API.Team.Feature hiding (withStatus) -import Wire.API.Team.Feature qualified as F - -testObject_WithStatus_team_1 :: WithStatus AppLockConfig -testObject_WithStatus_team_1 = withStatus FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock False) (-98)) - -testObject_WithStatus_team_2 :: WithStatus AppLockConfig -testObject_WithStatus_team_2 = withStatus FeatureStatusEnabled LockStatusUnlocked (AppLockConfig (EnforceAppLock True) 0) - -testObject_WithStatus_team_3 :: WithStatus AppLockConfig -testObject_WithStatus_team_3 = withStatus FeatureStatusEnabled LockStatusLocked (AppLockConfig (EnforceAppLock True) 111) - -testObject_WithStatus_team_4 :: WithStatus SelfDeletingMessagesConfig -testObject_WithStatus_team_4 = withStatus FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig (-97)) - -testObject_WithStatus_team_5 :: WithStatus SelfDeletingMessagesConfig -testObject_WithStatus_team_5 = withStatus FeatureStatusEnabled LockStatusUnlocked (SelfDeletingMessagesConfig 0) - -testObject_WithStatus_team_6 :: WithStatus SelfDeletingMessagesConfig -testObject_WithStatus_team_6 = withStatus FeatureStatusEnabled LockStatusLocked (SelfDeletingMessagesConfig 77) - -testObject_WithStatus_team_7 :: WithStatus ClassifiedDomainsConfig -testObject_WithStatus_team_7 = withStatus FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig []) - -testObject_WithStatus_team_8 :: WithStatus ClassifiedDomainsConfig -testObject_WithStatus_team_8 = withStatus FeatureStatusEnabled LockStatusLocked (ClassifiedDomainsConfig [Domain "example.com", Domain "test.foobar"]) - -testObject_WithStatus_team_9 :: WithStatus ClassifiedDomainsConfig -testObject_WithStatus_team_9 = withStatus FeatureStatusEnabled LockStatusUnlocked (ClassifiedDomainsConfig [Domain "test.foobar"]) - -testObject_WithStatus_team_10 :: WithStatus SSOConfig -testObject_WithStatus_team_10 = withStatus FeatureStatusDisabled LockStatusLocked SSOConfig - -testObject_WithStatus_team_11 :: WithStatus SearchVisibilityAvailableConfig -testObject_WithStatus_team_11 = withStatus FeatureStatusEnabled LockStatusLocked SearchVisibilityAvailableConfig - -testObject_WithStatus_team_12 :: WithStatus ValidateSAMLEmailsConfig -testObject_WithStatus_team_12 = withStatus FeatureStatusDisabled LockStatusLocked ValidateSAMLEmailsConfig - -testObject_WithStatus_team_13 :: WithStatus DigitalSignaturesConfig -testObject_WithStatus_team_13 = withStatus FeatureStatusEnabled LockStatusLocked DigitalSignaturesConfig - -testObject_WithStatus_team_14 :: WithStatus ConferenceCallingConfig -testObject_WithStatus_team_14 = withStatus FeatureStatusDisabled LockStatusUnlocked (ConferenceCallingConfig One2OneCallsTurn) - -testObject_WithStatus_team_15 :: WithStatus GuestLinksConfig -testObject_WithStatus_team_15 = withStatus FeatureStatusEnabled LockStatusUnlocked GuestLinksConfig - -testObject_WithStatus_team_16 :: WithStatus SndFactorPasswordChallengeConfig -testObject_WithStatus_team_16 = withStatus FeatureStatusDisabled LockStatusUnlocked SndFactorPasswordChallengeConfig - -testObject_WithStatus_team_17 :: WithStatus SearchVisibilityInboundConfig -testObject_WithStatus_team_17 = withStatus FeatureStatusEnabled LockStatusUnlocked SearchVisibilityInboundConfig - -testObject_WithStatus_team_18 :: WithStatus MlsE2EIdConfig -testObject_WithStatus_team_18 = - withStatus - FeatureStatusEnabled - LockStatusLocked - ( MlsE2EIdConfig - (fromIntegral @Int (60 * 60 * 24)) - Nothing - (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com") - False - ) - -parseHttpsUrl :: ByteString -> Either String HttpsUrl -parseHttpsUrl url = runParser parser url - -testObject_WithStatus_team_19 :: WithStatus MlsE2EIdConfig -testObject_WithStatus_team_19 = - withStatus - FeatureStatusEnabled - LockStatusLocked - ( MlsE2EIdConfig - (fromIntegral @Int (60 * 60 * 24)) - (either (\e -> error (show e)) Just $ parseHttpsUrl "https://example.com") - Nothing - True - ) - -withStatus :: FeatureStatus -> LockStatus -> cfg -> WithStatus cfg -withStatus fs ls cfg = F.withStatus fs ls cfg FeatureTTLUnlimited diff --git a/libs/wire-api/test/golden/fromJSON/testObject_WithStatus_team_14.json b/libs/wire-api/test/golden/fromJSON/testObject_LockableFeature_team_14.json similarity index 100% rename from libs/wire-api/test/golden/fromJSON/testObject_WithStatus_team_14.json rename to libs/wire-api/test/golden/fromJSON/testObject_LockableFeature_team_14.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_1.json b/libs/wire-api/test/golden/testObject_Feature_team_1.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_1.json rename to libs/wire-api/test/golden/testObject_Feature_team_1.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_10.json b/libs/wire-api/test/golden/testObject_Feature_team_10.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_10.json rename to libs/wire-api/test/golden/testObject_Feature_team_10.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_11.json b/libs/wire-api/test/golden/testObject_Feature_team_11.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_11.json rename to libs/wire-api/test/golden/testObject_Feature_team_11.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_12.json b/libs/wire-api/test/golden/testObject_Feature_team_12.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_12.json rename to libs/wire-api/test/golden/testObject_Feature_team_12.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_13.json b/libs/wire-api/test/golden/testObject_Feature_team_13.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_13.json rename to libs/wire-api/test/golden/testObject_Feature_team_13.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_14.json b/libs/wire-api/test/golden/testObject_Feature_team_14.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_14.json rename to libs/wire-api/test/golden/testObject_Feature_team_14.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_15.json b/libs/wire-api/test/golden/testObject_Feature_team_15.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_15.json rename to libs/wire-api/test/golden/testObject_Feature_team_15.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_16.json b/libs/wire-api/test/golden/testObject_Feature_team_16.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_16.json rename to libs/wire-api/test/golden/testObject_Feature_team_16.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_17.json b/libs/wire-api/test/golden/testObject_Feature_team_17.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_17.json rename to libs/wire-api/test/golden/testObject_Feature_team_17.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_2.json b/libs/wire-api/test/golden/testObject_Feature_team_2.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_2.json rename to libs/wire-api/test/golden/testObject_Feature_team_2.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_3.json b/libs/wire-api/test/golden/testObject_Feature_team_3.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_3.json rename to libs/wire-api/test/golden/testObject_Feature_team_3.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_4.json b/libs/wire-api/test/golden/testObject_Feature_team_4.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_4.json rename to libs/wire-api/test/golden/testObject_Feature_team_4.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_5.json b/libs/wire-api/test/golden/testObject_Feature_team_5.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_5.json rename to libs/wire-api/test/golden/testObject_Feature_team_5.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_6.json b/libs/wire-api/test/golden/testObject_Feature_team_6.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_6.json rename to libs/wire-api/test/golden/testObject_Feature_team_6.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_7.json b/libs/wire-api/test/golden/testObject_Feature_team_7.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_7.json rename to libs/wire-api/test/golden/testObject_Feature_team_7.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_8.json b/libs/wire-api/test/golden/testObject_Feature_team_8.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_8.json rename to libs/wire-api/test/golden/testObject_Feature_team_8.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusNoLock_team_9.json b/libs/wire-api/test/golden/testObject_Feature_team_9.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusNoLock_team_9.json rename to libs/wire-api/test/golden/testObject_Feature_team_9.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_1.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_1.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_1.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_1.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_10.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_10.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_10.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_10.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_11.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_11.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_11.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_11.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_12.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_12.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_12.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_12.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_13.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_13.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_13.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_13.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_14.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_14.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_14.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_14.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_15.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_15.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_15.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_15.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_16.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_16.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_16.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_16.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_17.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_17.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_17.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_17.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_18.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_18.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_18.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_18.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_19.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_19.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_19.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_19.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_2.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_2.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_2.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_2.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_3.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_3.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_3.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_3.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_4.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_4.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_4.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_4.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_5.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_5.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_5.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_5.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_6.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_6.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_6.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_6.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_7.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_7.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_7.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_7.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_8.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_8.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_8.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_8.json diff --git a/libs/wire-api/test/golden/testObject_WithStatusPatch_team_9.json b/libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_9.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatusPatch_team_9.json rename to libs/wire-api/test/golden/testObject_LockableFeaturePatch_team_9.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_1.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_1.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_1.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_1.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_10.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_10.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_10.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_10.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_11.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_11.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_11.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_11.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_12.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_12.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_12.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_12.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_13.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_13.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_13.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_13.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_14.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_14.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_14.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_14.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_15.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_15.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_15.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_15.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_16.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_16.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_16.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_16.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_17.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_17.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_17.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_17.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_18.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_18.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_18.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_18.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_19.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_19.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_19.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_19.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_2.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_2.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_2.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_2.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_3.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_3.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_3.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_3.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_4.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_4.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_4.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_4.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_5.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_5.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_5.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_5.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_6.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_6.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_6.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_6.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_7.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_7.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_7.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_7.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_8.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_8.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_8.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_8.json diff --git a/libs/wire-api/test/golden/testObject_WithStatus_team_9.json b/libs/wire-api/test/golden/testObject_LockableFeature_team_9.json similarity index 100% rename from libs/wire-api/test/golden/testObject_WithStatus_team_9.json rename to libs/wire-api/test/golden/testObject_LockableFeature_team_9.json 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 bec9d3c96f1..a4d3b841fa5 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 @@ -214,10 +214,10 @@ tests = testRoundTrip @Team.TeamDeleteData, testRoundTrip @Team.Conversation.TeamConversation, testRoundTrip @Team.Conversation.TeamConversationList, - testRoundTrip @(Team.Feature.WithStatus Team.Feature.LegalholdConfig), - testRoundTrip @(Team.Feature.WithStatusPatch Team.Feature.LegalholdConfig), - testRoundTrip @(Team.Feature.WithStatusPatch Team.Feature.SelfDeletingMessagesConfig), - testRoundTrip @(Team.Feature.WithStatusNoLock Team.Feature.LegalholdConfig), + testRoundTrip @(Team.Feature.LockableFeature Team.Feature.LegalholdConfig), + testRoundTrip @(Team.Feature.LockableFeaturePatch Team.Feature.LegalholdConfig), + testRoundTrip @(Team.Feature.LockableFeaturePatch Team.Feature.SelfDeletingMessagesConfig), + testRoundTrip @(Team.Feature.Feature Team.Feature.LegalholdConfig), testRoundTrip @Team.Feature.AllFeatureConfigs, testRoundTrip @Team.Feature.FeatureStatus, testRoundTrip @Team.Feature.LockStatus, diff --git a/libs/wire-api/test/unit/Test/Wire/API/Run.hs b/libs/wire-api/test/unit/Test/Wire/API/Run.hs index 417d543e0e4..5301f44cdc9 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Run.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Run.hs @@ -37,7 +37,6 @@ import Test.Wire.API.Routes.Version qualified as Routes.Version import Test.Wire.API.Routes.Version.Wai qualified as Routes.Version.Wai import Test.Wire.API.Swagger qualified as Swagger import Test.Wire.API.Team.Export qualified as Team.Export -import Test.Wire.API.Team.Feature qualified as Team.Feature import Test.Wire.API.Team.Member qualified as Team.Member import Test.Wire.API.User qualified as User import Test.Wire.API.User.Auth qualified as User.Auth @@ -70,6 +69,5 @@ main = unsafePerformIO Routes.Version.Wai.tests, RawJson.tests, OAuth.tests, - Password.tests, - Team.Feature.tests + Password.tests ] diff --git a/libs/wire-api/test/unit/Test/Wire/API/Team/Feature.hs b/libs/wire-api/test/unit/Test/Wire/API/Team/Feature.hs deleted file mode 100644 index 60b634c9d17..00000000000 --- a/libs/wire-api/test/unit/Test/Wire/API/Team/Feature.hs +++ /dev/null @@ -1,92 +0,0 @@ -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} - --- 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.Team.Feature (tests) where - -import Imports -import Test.Tasty -import Test.Tasty.HUnit -import Wire.API.Team.Feature - -tests :: TestTree -tests = - testGroup - "Wire.API.Team.Feature" - [ testCase "no lock status in DB" testComputeFeatureConfigForTeamUserLsIsNothing, - testCase "feature is locked in DB" testComputeFeatureConfigForTeamUserLocked, - testCase "feature is unlocked in DB but has no feature status" testComputeFeatureConfigForTeamUserUnlocked, - testCase "feature is unlocked in DB and has feature status" testComputeFeatureConfigForTeamWithDbStatus - ] - -testComputeFeatureConfigForTeamUserLsIsNothing :: Assertion -testComputeFeatureConfigForTeamUserLsIsNothing = do - let mStatusDb = undefined - let mLockStatusDb = Nothing - let defStatus = - withStatus - FeatureStatusEnabled - LockStatusLocked - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited - let expected = defStatus - let actual = computeFeatureConfigForTeamUser @ExposeInvitationURLsToTeamAdminConfig mStatusDb mLockStatusDb defStatus - actual @?= expected - -testComputeFeatureConfigForTeamUserLocked :: Assertion -testComputeFeatureConfigForTeamUserLocked = do - let mStatusDb = undefined - let mLockStatusDb = Just LockStatusLocked - let defStatus = - withStatus - FeatureStatusEnabled - LockStatusLocked - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited - let expected = defStatus - let actual = computeFeatureConfigForTeamUser @ExposeInvitationURLsToTeamAdminConfig mStatusDb mLockStatusDb defStatus - actual @?= expected - -testComputeFeatureConfigForTeamUserUnlocked :: Assertion -testComputeFeatureConfigForTeamUserUnlocked = do - let mStatusDb = Nothing - let mLockStatusDb = Just LockStatusUnlocked - let defStatus = - withStatus - FeatureStatusEnabled - LockStatusLocked - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited - let expected = defStatus & setLockStatus LockStatusUnlocked - let actual = computeFeatureConfigForTeamUser @ExposeInvitationURLsToTeamAdminConfig mStatusDb mLockStatusDb defStatus - actual @?= expected - -testComputeFeatureConfigForTeamWithDbStatus :: Assertion -testComputeFeatureConfigForTeamWithDbStatus = do - let mStatusDb = - Just . forgetLock $ - withStatus - FeatureStatusDisabled - LockStatusUnlocked - ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited - let mLockStatusDb = Just LockStatusUnlocked - let defStatus = undefined - let (Just expected) = withUnlocked <$> mStatusDb - let actual = computeFeatureConfigForTeamUser @ExposeInvitationURLsToTeamAdminConfig mStatusDb mLockStatusDb defStatus - actual @?= expected diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 5f1a6b8bdc0..e875f415f6c 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -420,6 +420,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.Event_user Test.Wire.API.Golden.Generated.EventType_team Test.Wire.API.Golden.Generated.EventType_user + Test.Wire.API.Golden.Generated.Feature_team Test.Wire.API.Golden.Generated.HandleUpdate_user Test.Wire.API.Golden.Generated.Invitation_team Test.Wire.API.Golden.Generated.InvitationCode_user @@ -433,6 +434,8 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.ListType_team Test.Wire.API.Golden.Generated.Locale_user Test.Wire.API.Golden.Generated.LocaleUpdate_user + Test.Wire.API.Golden.Generated.LockableFeature_team + Test.Wire.API.Golden.Generated.LockableFeaturePatch_team Test.Wire.API.Golden.Generated.LoginCode_user Test.Wire.API.Golden.Generated.LoginCodeTimeout_user Test.Wire.API.Golden.Generated.ManagedBy_user @@ -562,9 +565,6 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.VerifyDeleteUser_user Test.Wire.API.Golden.Generated.ViewLegalHoldService_team Test.Wire.API.Golden.Generated.ViewLegalHoldServiceInfo_team - Test.Wire.API.Golden.Generated.WithStatus_team - Test.Wire.API.Golden.Generated.WithStatusNoLock_team - Test.Wire.API.Golden.Generated.WithStatusPatch_team Test.Wire.API.Golden.Generated.Wrapped_20_22some_5fint_22_20Int_user Test.Wire.API.Golden.Manual Test.Wire.API.Golden.Manual.Activate_user @@ -660,7 +660,6 @@ test-suite wire-api-tests Test.Wire.API.Run Test.Wire.API.Swagger Test.Wire.API.Team.Export - Test.Wire.API.Team.Feature Test.Wire.API.Team.Member Test.Wire.API.User Test.Wire.API.User.Auth diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index b039bff1303..3ef3d01a3bb 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -92,7 +92,7 @@ data GalleyAPIAccess m a where GalleyAPIAccess m Team.TeamName GetTeamLegalHoldStatus :: TeamId -> - GalleyAPIAccess m (WithStatus LegalholdConfig) + GalleyAPIAccess m (LockableFeature LegalholdConfig) GetTeamSearchVisibility :: TeamId -> GalleyAPIAccess m TeamSearchVisibility diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index e05584e9a36..7f451bad632 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -403,7 +403,7 @@ getTeamLegalHoldStatus :: Member TinyLog r ) => TeamId -> - Sem r (WithStatus LegalholdConfig) + Sem r (LockableFeature LegalholdConfig) getTeamLegalHoldStatus tid = do debug $ remote "galley" . msg (val "Get legalhold settings") galleyRequest req >>= decodeBodyOrThrow "galley" @@ -443,7 +443,7 @@ getVerificationCodeEnabled :: getVerificationCodeEnabled tid = do debug $ remote "galley" . msg (val "Get snd factor password challenge settings") response <- galleyRequest req - status <- wsStatus <$> decodeBodyOrThrow @(WithStatus SndFactorPasswordChallengeConfig) "galley" response + status <- (.status) <$> decodeBodyOrThrow @(LockableFeature SndFactorPasswordChallengeConfig) "galley" response case status of FeatureStatusEnabled -> pure True FeatureStatusDisabled -> pure False @@ -500,7 +500,7 @@ getTeamExposeInvitationURLsToTeamAdmin :: getTeamExposeInvitationURLsToTeamAdmin tid = do debug $ remote "galley" . msg (val "Get expose invitation URLs to team admin settings") response <- galleyRequest req - status <- wsStatus <$> decodeBodyOrThrow @(WithStatus ExposeInvitationURLsToTeamAdminConfig) "galley" response + status <- (.status) <$> decodeBodyOrThrow @(LockableFeature ExposeInvitationURLsToTeamAdminConfig) "galley" response case status of FeatureStatusEnabled -> pure ShowInvitationUrl FeatureStatusDisabled -> pure HideInvitationUrl diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 7daba81f6d8..7b0edcee302 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -472,7 +472,8 @@ checkHandleImpl uhandle = do hasE2EId :: (Member GalleyAPIAccess r) => StoredUser -> Sem r Bool hasE2EId user = - wsStatus . afcMlsE2EId + -- FUTUREWORK(mangoiv): we should use a function 'getSingleFeatureConfigForUser' + (.status) . npProject @MlsE2EIdConfig <$> getAllFeatureConfigsForUser (Just user.id) <&> \case FeatureStatusEnabled -> True FeatureStatusDisabled -> False diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index 1c0e673fa88..84f870e01b2 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + module Wire.MiniBackend ( -- * Mini backends MiniBackend (..), diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 03fb0a2cda5..096e740c642 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -221,9 +221,12 @@ spec = describe "UserSubsystem.Interpreter" do getSelfProfile (toLocalUnsafe domain selfId) in retrievedProfile === Nothing - prop "should mark user as managed by scim if E2EId is enabled for the user and they have a handle" \storedSelf domain susbsystemConfig mlsE2EIdConfig -> + prop "should mark user as managed by scim if E2EId is enabled for the user and they have a handle" \storedSelf domain susbsystemConfig (mlsE2EIdConfig :: MlsE2EIdConfig) -> let localBackend = def {users = [storedSelf]} - allFeatureConfigs = def {afcMlsE2EId = withStatus FeatureStatusEnabled LockStatusUnlocked mlsE2EIdConfig FeatureTTLUnlimited} + allFeatureConfigs = + npUpdate + (LockableFeature FeatureStatusEnabled LockStatusUnlocked mlsE2EIdConfig) + def SelfProfile retrievedUser = fromJust . runAllErrorsUnsafe @@ -326,9 +329,21 @@ spec = describe "UserSubsystem.Interpreter" do run . runErrorUnsafe . runError - $ interpretNoFederationStack localBackend Nothing def {afcMlsE2EId = setStatus FeatureStatusEnabled defFeatureStatus} config do - updateUserProfile lusr Nothing UpdateOriginScim (def {name = Just newName}) - getUserProfile lusr (tUntagged lusr) + $ interpretNoFederationStack + localBackend + Nothing + ( npUpdate + ( def + { status = FeatureStatusEnabled + } :: + LockableFeature MlsE2EIdConfig + ) + def + ) + config + do + updateUserProfile lusr Nothing UpdateOriginScim (def {name = Just newName}) + getUserProfile lusr (tUntagged lusr) in profileErr === Left UserSubsystemDisplayNameManagedByScim prop diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 85d5342bc0b..ab43f085d49 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -350,12 +350,12 @@ updateFederationRemote dom fedcfg = do \do that, removing or updating items listed in the config file is not allowed." -- | Responds with 'Nothing' if field is NULL in existing user or user does not exist. -getAccountConferenceCallingConfig :: UserId -> (Handler r) (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig) +getAccountConferenceCallingConfig :: UserId -> (Handler r) (ApiFt.Feature ApiFt.ConferenceCallingConfig) getAccountConferenceCallingConfig uid = lift (wrapClient $ Data.lookupFeatureConferenceCalling uid) >>= maybe (ApiFt.forgetLock <$> view (settings . getAfcConferenceCallingDefNull)) pure -putAccountConferenceCallingConfig :: UserId -> ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig -> (Handler r) NoContent +putAccountConferenceCallingConfig :: UserId -> ApiFt.Feature ApiFt.ConferenceCallingConfig -> (Handler r) NoContent putAccountConferenceCallingConfig uid status = lift $ wrapClient $ Data.updateFeatureConferenceCalling uid (Just status) $> NoContent diff --git a/services/brig/src/Brig/Calling/API.hs b/services/brig/src/Brig/Calling/API.hs index e1e71eb74d0..9b58b382ff9 100644 --- a/services/brig/src/Brig/Calling/API.hs +++ b/services/brig/src/Brig/Calling/API.hs @@ -83,7 +83,7 @@ getCallsConfigV2 uid _ limit = do sftFederation <- view enableSFTFederation discoveredServers <- turnServersV2 (env ^. turnServers) shared <- do - ccStatus <- lift $ liftSem $ (wsStatus . afcConferenceCalling <$> getAllFeatureConfigsForUser (Just uid)) + ccStatus <- lift $ liftSem $ ((.status) . npProject @ConferenceCallingConfig <$> getAllFeatureConfigsForUser (Just uid)) pure $ case ccStatus of FeatureStatusEnabled -> True FeatureStatusDisabled -> False @@ -118,7 +118,7 @@ getCallsConfig uid _ = do env <- view turnEnv discoveredServers <- turnServersV1 (env ^. turnServers) shared <- do - ccStatus <- lift $ liftSem $ (wsStatus . afcConferenceCalling <$> getAllFeatureConfigsForUser (Just uid)) + ccStatus <- lift $ liftSem $ ((.status) . npProject @ConferenceCallingConfig <$> getAllFeatureConfigsForUser (Just uid)) pure $ case ccStatus of FeatureStatusEnabled -> True FeatureStatusDisabled -> False diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 91f77d4c76a..afe68155e3c 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -1,4 +1,5 @@ -{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} +{-# OPTIONS_GHC -Wno-orphans #-} -- This file is part of the Wire Server implementation. -- @@ -70,6 +71,7 @@ import Cassandra hiding (Set) import Control.Error import Control.Lens hiding (from) import Data.Conduit (ConduitM) +import Data.Default import Data.Domain import Data.Handle (Handle) import Data.Id @@ -299,9 +301,9 @@ updateManagedBy u h = retry x5 $ write userManagedByUpdate (params LocalQuorum ( updateRichInfo :: (MonadClient m) => UserId -> RichInfoAssocList -> m () updateRichInfo u ri = retry x5 $ write userRichInfoUpdate (params LocalQuorum (ri, u)) -updateFeatureConferenceCalling :: (MonadClient m) => UserId -> Maybe (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig) -> m (Maybe (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig)) +updateFeatureConferenceCalling :: (MonadClient m) => UserId -> Maybe (ApiFt.Feature ApiFt.ConferenceCallingConfig) -> m (Maybe (ApiFt.Feature ApiFt.ConferenceCallingConfig)) updateFeatureConferenceCalling uid mbStatus = do - let flag = ApiFt.wssStatus <$> mbStatus + let flag = (.status) <$> mbStatus retry x5 $ write update (params LocalQuorum (flag, uid)) pure mbStatus where @@ -436,13 +438,13 @@ lookupServiceUsersForTeam pid sid tid = "SELECT user, conv FROM service_team \ \WHERE provider = ? AND service = ? AND team = ?" -lookupFeatureConferenceCalling :: (MonadClient m) => UserId -> m (Maybe (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig)) +lookupFeatureConferenceCalling :: (MonadClient m) => UserId -> m (Maybe (ApiFt.Feature ApiFt.ConferenceCallingConfig)) lookupFeatureConferenceCalling uid = do let q = query1 select (params LocalQuorum (Identity uid)) mStatusValue <- (>>= runIdentity) <$> retry x1 q case mStatusValue of Nothing -> pure Nothing - Just status -> pure $ Just $ ApiFt.defFeatureStatusNoLock {ApiFt.wssStatus = status} + Just status -> pure $ Just $ def {ApiFt.status = status} where select :: PrepQuery R (Identity UserId) (Identity (Maybe ApiFt.FeatureStatus)) select = fromString "select feature_conference_calling from user where id = ?" diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 36ddf319be2..45c72d9b0f5 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- Disabling to stop errors on Getters {-# OPTIONS_GHC -Wno-redundant-constraints #-} @@ -34,6 +35,7 @@ import Data.Aeson qualified as Aeson import Data.Aeson.Types (typeMismatch) import Data.Char qualified as Char import Data.Code qualified as Code +import Data.Default import Data.Domain (Domain (..)) import Data.Id import Data.LanguageCodes (ISO639_1 (EN)) @@ -708,7 +710,11 @@ instance Arbitrary AccountFeatureConfigs where arbitrary = AccountFeatureConfigs <$> fmap locked arbitrary <*> fmap locked arbitrary where locked :: Public.ImplicitLockStatus a -> Public.ImplicitLockStatus a - locked = Public.ImplicitLockStatus . Public.setLockStatus Public.LockStatusLocked . Public._unImplicitLockStatus + locked impl = + Public.ImplicitLockStatus $ + (Public._unImplicitLockStatus impl) + { Public.lockStatus = Public.LockStatusLocked + } instance FromJSON AccountFeatureConfigs where parseJSON = @@ -740,17 +746,17 @@ instance ToJSON AccountFeatureConfigs where ] ] -getAfcConferenceCallingDefNewMaybe :: Lens.Getter Settings (Maybe (Public.WithStatus Public.ConferenceCallingConfig)) +getAfcConferenceCallingDefNewMaybe :: Lens.Getter Settings (Maybe (Public.LockableFeature Public.ConferenceCallingConfig)) getAfcConferenceCallingDefNewMaybe = Lens.to (Lens.^? (Lens.to setFeatureFlags . Lens._Just . Lens.to afcConferenceCallingDefNew . unImplicitLockStatus)) -getAfcConferenceCallingDefNull :: Lens.Getter Settings (Public.WithStatus Public.ConferenceCallingConfig) +getAfcConferenceCallingDefNull :: Lens.Getter Settings (Public.LockableFeature Public.ConferenceCallingConfig) getAfcConferenceCallingDefNull = Lens.to (Public._unImplicitLockStatus . afcConferenceCallingDefNull . fromMaybe defAccountFeatureConfigs . setFeatureFlags) defAccountFeatureConfigs :: AccountFeatureConfigs defAccountFeatureConfigs = AccountFeatureConfigs - { afcConferenceCallingDefNew = Public.ImplicitLockStatus Public.defFeatureStatus, - afcConferenceCallingDefNull = Public.ImplicitLockStatus Public.defFeatureStatus + { afcConferenceCallingDefNew = Public.ImplicitLockStatus def, + afcConferenceCallingDefNull = Public.ImplicitLockStatus def } -- | Customer extensions naturally are covered by the AGPL like everything else, but use them diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index f87e181648d..f55928f4fb1 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -804,8 +804,12 @@ guardSecondFactorDisabled :: Maybe UserId -> ExceptT HttpError (AppT r) () guardSecondFactorDisabled mbUserId = do - enabled <- lift $ liftSem $ (==) Feature.FeatureStatusEnabled . Feature.wsStatus . Feature.afcSndFactorPasswordChallenge <$> GalleyAPIAccess.getAllFeatureConfigsForUser mbUserId - when enabled $ (throwStd (errorToWai @'E.AccessDenied)) + feat <- lift $ liftSem $ GalleyAPIAccess.getAllFeatureConfigsForUser mbUserId + let enabled = + (Feature.npProject @Feature.SndFactorPasswordChallengeConfig feat).status + == Feature.FeatureStatusEnabled + when enabled do + throwStd $ errorToWai @'E.AccessDenied minRsaKeySize :: Int minRsaKeySize = 256 -- Bytes (= 2048 bits) diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 8f5fbe0392e..e79955c528f 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -51,6 +51,7 @@ import Control.Error hiding (bool) import Control.Lens (to, view) import Data.ByteString.Conversion (toByteString) import Data.Code qualified as Code +import Data.Default import Data.Handle (Handle) import Data.Id import Data.List.NonEmpty qualified as NE @@ -134,7 +135,7 @@ verifyCode mbCode action uid = do (mbEmail, mbTeamId) <- getEmailAndTeamId uid featureEnabled <- lift $ do mbFeatureEnabled <- liftSem $ GalleyAPIAccess.getVerificationCodeEnabled `traverse` mbTeamId - pure $ fromMaybe (Public.wsStatus (Public.defFeatureStatus @Public.SndFactorPasswordChallengeConfig) == Public.FeatureStatusEnabled) mbFeatureEnabled + pure $ fromMaybe ((def @(Feature Public.SndFactorPasswordChallengeConfig)).status == Public.FeatureStatusEnabled) mbFeatureEnabled isSsoUser <- wrapHttpClientE $ Data.isSamlUser uid when (featureEnabled && not isSsoUser) $ do case (mbCode, mbEmail) of @@ -421,8 +422,8 @@ assertLegalHoldEnabled :: TeamId -> ExceptT LegalHoldLoginError (AppT r) () assertLegalHoldEnabled tid = do - stat <- lift $ liftSem $ GalleyAPIAccess.getTeamLegalHoldStatus tid - case wsStatus stat of + feat <- lift $ liftSem $ GalleyAPIAccess.getTeamLegalHoldStatus tid + case feat.status of FeatureStatusDisabled -> throwE LegalHoldLoginLegalHoldNotEnabled FeatureStatusEnabled -> pure () diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 0d2aad8a5db..f803d6a988a 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -1580,7 +1580,7 @@ enabled2ndFaForTeamInternal galley tid = do ( galley . paths ["i", "teams", toByteString' tid, "features", featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson - . Bilge.json (Public.WithStatusNoLock Public.FeatureStatusEnabled Public.SndFactorPasswordChallengeConfig Public.FeatureTTLUnlimited) + . Bilge.json (Public.Feature Public.FeatureStatusEnabled Public.SndFactorPasswordChallengeConfig) ) !!! const 200 === statusCode diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 3e829d00d25..72ff959ab9f 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -274,11 +274,10 @@ invitationUrlGalleyMock featureStatus tid inviter (ReceivedRequest mth pth body_ && pth == ["i", "teams", Text.pack (show tid), "features", "exposeInvitationURLsToTeamAdmin"] = pure . Wai.responseLBS HTTP.status200 mempty $ encode - ( withStatus + ( LockableFeature featureStatus LockStatusUnlocked ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited ) | mth == "GET" && pth == ["i", "teams", Text.pack (show tid), "members", Text.pack (show inviter)] = diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 331309043cf..9a862fba44a 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -266,7 +266,7 @@ putLegalHoldEnabled tid enabled g = do g . paths ["i", "teams", toByteString' tid, "features", "legalhold"] . contentJson - . lbytes (encode (Public.WithStatusNoLock enabled Public.LegalholdConfig Public.FeatureTTLUnlimited)) + . lbytes (encode (Public.Feature enabled Public.LegalholdConfig)) . expect2xx putLHWhitelistTeam :: (HasCallStack) => Galley -> TeamId -> Http ResponseLBS @@ -436,7 +436,7 @@ setTeamTeamSearchVisibilityAvailable galley tid status = ( galley . paths ["i/teams", toByteString' tid, "features/searchVisibility"] . contentJson - . body (RequestBodyLBS . encode $ Public.WithStatusNoLock status Public.SearchVisibilityAvailableConfig Public.FeatureTTLUnlimited) + . body (RequestBodyLBS . encode $ Public.Feature status Public.SearchVisibilityAvailableConfig) ) !!! do const 200 === statusCode @@ -458,7 +458,7 @@ setTeamSearchVisibilityInboundAvailable galley tid status = ( galley . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @Public.SearchVisibilityInboundConfig] . contentJson - . body (RequestBodyLBS . encode $ Public.WithStatusNoLock status Public.SearchVisibilityInboundConfig Public.FeatureTTLUnlimited) + . body (RequestBodyLBS . encode $ Public.Feature status Public.SearchVisibilityInboundConfig) ) !!! do const 200 === statusCode diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index b041300dca0..3c378e4f4ae 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -86,7 +86,7 @@ import Wire.API.Asset qualified as Asset import Wire.API.Connection import Wire.API.Conversation import Wire.API.Routes.MultiTablePaging -import Wire.API.Team.Feature (ExposeInvitationURLsToTeamAdminConfig (..), FeatureStatus (..), FeatureTTL' (..), LockStatus (LockStatusLocked), withStatus) +import Wire.API.Team.Feature import Wire.API.Team.Invitation (Invitation (inInvitation)) import Wire.API.Team.Permission hiding (self) import Wire.API.User @@ -1408,11 +1408,10 @@ testTooManyMembersForLegalhold opts brig = do && pth == ["i", "teams", Text.pack (show tid), "features", "exposeInvitationURLsToTeamAdmin"] = pure . Wai.responseLBS HTTP.status200 mempty $ encode - ( withStatus + ( LockableFeature FeatureStatusDisabled LockStatusLocked ExposeInvitationURLsToTeamAdminConfig - FeatureTTLUnlimited ) | otherwise = pure $ Wai.responseLBS HTTP.status500 mempty "Unexpected request to mocked galley" diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 8627d78c989..8a1c1004ad9 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -441,7 +441,7 @@ generateVerificationCode' brig req = do setTeamSndFactorPasswordChallenge :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Galley -> TeamId -> Public.FeatureStatus -> m () setTeamSndFactorPasswordChallenge galley tid status = do - let js = RequestBodyLBS $ encode $ Public.WithStatusNoLock status Public.SndFactorPasswordChallengeConfig Public.FeatureTTLUnlimited + let js = RequestBodyLBS $ encode $ Public.Feature status Public.SndFactorPasswordChallengeConfig put (galley . paths ["i", "teams", toByteString' tid, "features", featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode setTeamFeatureLockStatus :: diff --git a/services/galley/default.nix b/services/galley/default.nix index 362e174d34a..c3ef5ddd9e3 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -27,7 +27,6 @@ , conduit , containers , cookie -, cql , crypton , crypton-x509 , currency-codes @@ -42,6 +41,7 @@ , federator , filepath , galley-types +, generics-sop , gitignoreSource , gundeck-types , hex @@ -86,6 +86,7 @@ , servant-client-core , servant-server , singletons +, singletons-base , sop-core , split , ssl-util @@ -98,6 +99,7 @@ , tasty-cannon , tasty-hunit , tasty-quickcheck +, template-haskell , temporary , text , time @@ -152,10 +154,10 @@ mkDerivation { cassava comonad containers - cql crypton crypton-x509 currency-codes + data-default data-timeout either enclosed-exceptions @@ -164,6 +166,7 @@ mkDerivation { extended extra galley-types + generics-sop gundeck-types hex HsOpenSSL @@ -193,10 +196,13 @@ mkDerivation { servant-client servant-server singletons + singletons-base + sop-core split ssl-util stm tagged + template-haskell text time tinylog diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 58f9f6c9fab..3437caf3795 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -136,9 +136,12 @@ library Galley.Cassandra.Conversation.MLS Galley.Cassandra.ConversationList Galley.Cassandra.CustomBackend + Galley.Cassandra.FeatureTH Galley.Cassandra.GetAllTeamFeatureConfigs Galley.Cassandra.Instances Galley.Cassandra.LegalHold + Galley.Cassandra.MakeFeature + Galley.Cassandra.Orphans Galley.Cassandra.Proposal Galley.Cassandra.Queries Galley.Cassandra.SearchVisibility @@ -300,10 +303,10 @@ library , cassava >=0.5.2 , comonad , containers >=0.5 - , cql , crypton , crypton-x509 , currency-codes >=2.0 + , data-default , data-timeout , either , enclosed-exceptions >=1.0 @@ -312,6 +315,7 @@ library , extended , extra >=1.3 , galley-types >=0.65.0 + , generics-sop , gundeck-types >=1.35.2 , hex , HsOpenSSL >=0.11 @@ -341,10 +345,13 @@ library , servant-client , servant-server , singletons + , singletons-base + , sop-core , split >=0.2 , ssl-util >=0.1 , stm >=2.4 , tagged + , template-haskell , text >=0.11 , time >=1.4 , tinylog >=0.10 diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 92a176a4dea..7dc40d9c289 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -72,7 +72,7 @@ import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P -import Servant hiding (JSON, WithStatus) +import Servant import System.Logger.Class hiding (Path, name) import System.Logger.Class qualified as Log import Wire.API.Conversation hiding (Member) @@ -90,7 +90,7 @@ import Wire.API.Routes.Internal.Galley import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Routes.MultiTablePaging (mtpHasMore, mtpPagingState, mtpResults) import Wire.API.Routes.MultiTablePaging qualified as MTP -import Wire.API.Team.Feature hiding (setStatus) +import Wire.API.Team.Feature import Wire.API.User.Client import Wire.NotificationSubsystem import Wire.Sem.Paging @@ -364,7 +364,7 @@ rmUser lusr conn = do FeatureStatusEnabled -> Left <$> E.getTeamAdmins tid FeatureStatusDisabled -> Right <$> getTeamMembersForFanout tid ) - . wsStatus + . (.status) uncheckedDeleteTeamMember lusr conn tid (tUnqualified lusr) toNotify page' <- listTeams @p2 (tUnqualified lusr) (Just (pageState page)) maxBound leaveTeams page' diff --git a/services/galley/src/Galley/API/LegalHold/Team.hs b/services/galley/src/Galley/API/LegalHold/Team.hs index c7052c2d8bc..c62137f4e1a 100644 --- a/services/galley/src/Galley/API/LegalHold/Team.hs +++ b/services/galley/src/Galley/API/LegalHold/Team.hs @@ -24,6 +24,7 @@ module Galley.API.LegalHold.Team ) where +import Data.Default import Data.Id import Data.Range import Galley.Effects @@ -63,8 +64,7 @@ computeLegalHoldFeatureStatus tid dbFeature = getLegalHoldFlag >>= \case FeatureLegalHoldDisabledPermanently -> pure FeatureStatusDisabled FeatureLegalHoldDisabledByDefault -> - pure . wssStatus $ - unDbFeature dbFeature defFeatureStatusNoLock + pure (applyDbFeature dbFeature def).status FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> do wl <- LegalHoldData.isTeamLegalholdWhitelisted tid pure $ if wl then FeatureStatusEnabled else FeatureStatusDisabled diff --git a/services/galley/src/Galley/API/MLS/Migration.hs b/services/galley/src/Galley/API/MLS/Migration.hs index 747de458cd4..4cb5c35d8a6 100644 --- a/services/galley/src/Galley/API/MLS/Migration.hs +++ b/services/galley/src/Galley/API/MLS/Migration.hs @@ -52,15 +52,14 @@ checkMigrationCriteria :: ) => UTCTime -> MLSConversation -> - WithStatus MlsMigrationConfig -> + LockableFeature MlsMigrationConfig -> Sem r Bool checkMigrationCriteria now conv ws - | wsStatus ws == FeatureStatusDisabled = pure False + | ws.status == FeatureStatusDisabled = pure False | afterDeadline = pure True | otherwise = unApAll $ mconcat [localUsersMigrated, remoteUsersMigrated] where - mig = wsConfig ws - afterDeadline = maybe False (now >=) mig.finaliseRegardlessAfter + afterDeadline = maybe False (now >=) ws.config.finaliseRegardlessAfter containsMLS = Set.member BaseProtocolMLSTag diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 29073e71bbc..87457e2ccab 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -97,7 +97,7 @@ import Wire.API.Federation.Client (FederatorClient) import Wire.API.Federation.Error import Wire.API.Provider.Bot qualified as Public import Wire.API.Routes.MultiTablePaging qualified as Public -import Wire.API.Team.Feature as Public hiding (setStatus) +import Wire.API.Team.Feature as Public import Wire.API.User import Wire.Sem.Paging.Cassandra @@ -653,7 +653,7 @@ ensureGuestLinksEnabled :: Maybe TeamId -> Sem r () ensureGuestLinksEnabled mbTid = - getConversationGuestLinksFeatureStatus mbTid >>= \ws -> case wsStatus ws of + getConversationGuestLinksFeatureStatus mbTid >>= \ws -> case ws.status of FeatureStatusEnabled -> pure () FeatureStatusDisabled -> throwS @'GuestLinksDisabled @@ -667,7 +667,7 @@ getConversationGuestLinksStatus :: ) => UserId -> ConvId -> - Sem r (WithStatus GuestLinksConfig) + Sem r (LockableFeature GuestLinksConfig) getConversationGuestLinksStatus uid convId = do conv <- E.getConversation convId >>= noteS @'ConvNotFound ensureConvAdmin (Data.convLocalMembers conv) uid @@ -679,7 +679,7 @@ getConversationGuestLinksFeatureStatus :: Member (Input Opts) r ) => Maybe TeamId -> - Sem r (WithStatus GuestLinksConfig) + Sem r (LockableFeature GuestLinksConfig) getConversationGuestLinksFeatureStatus Nothing = getConfigForServer @GuestLinksConfig getConversationGuestLinksFeatureStatus (Just tid) = getConfigForTeam @GuestLinksConfig tid diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index e3aed8fbd4c..c489516322b 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -1000,7 +1000,7 @@ deleteTeamMember' lusr zcon tid remove mBody = do mems <- getTeamMembersForFanout tid uncheckedDeleteTeamMember lusr (Just zcon) tid remove (Right mems) ) - . wsStatus + . (.status) pure TeamMemberDeleteCompleted -- This function is "unchecked" because it does not validate that the user has the `RemoveTeamMember` permission. diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 7fb0e456069..38d16da2f45 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -90,24 +92,22 @@ patchFeatureStatusInternal :: Member NotificationSubsystem r ) => TeamId -> - WithStatusPatch cfg -> - Sem r (WithStatus cfg) + LockableFeaturePatch cfg -> + Sem r (LockableFeature cfg) patchFeatureStatusInternal tid patch = do assertTeamExists tid currentFeatureStatus <- getFeatureStatus @cfg DontDoAuth tid let newFeatureStatus = applyPatch currentFeatureStatus - -- setting the config can fail, so we need to do it first - void $ setConfigForTeam @cfg tid (forgetLock newFeatureStatus) - when (isJust $ wspLockStatus patch) $ void $ updateLockStatus @cfg tid (wsLockStatus newFeatureStatus) + void $ setConfigForTeam @cfg tid newFeatureStatus getFeatureStatus @cfg DontDoAuth tid where - applyPatch :: WithStatus cfg -> WithStatus cfg + applyPatch :: LockableFeature cfg -> LockableFeature cfg applyPatch current = current - & setStatus (fromMaybe (wsStatus current) (wspStatus patch)) - & setLockStatus (fromMaybe (wsLockStatus current) (wspLockStatus patch)) - & setConfig (fromMaybe (wsConfig current) (wspConfig patch)) - & setWsTTL (fromMaybe (wsTTL current) (wspTTL patch)) + { status = fromMaybe current.status patch.status, + lockStatus = fromMaybe current.lockStatus patch.lockStatus, + config = fromMaybe current.config patch.config + } setFeatureStatus :: forall cfg r. @@ -126,17 +126,18 @@ setFeatureStatus :: ) => DoAuth -> TeamId -> - WithStatusNoLock cfg -> - Sem r (WithStatus cfg) -setFeatureStatus doauth tid wsnl = do + Feature cfg -> + Sem r (LockableFeature cfg) +setFeatureStatus doauth tid feat = do case doauth of DoAuth uid -> do zusrMembership <- getTeamMember tid uid void $ permissionCheck ChangeTeamFeature zusrMembership DontDoAuth -> assertTeamExists tid - guardLockStatus . wsLockStatus =<< getConfigForTeam @cfg tid - setConfigForTeam @cfg tid wsnl + feat0 <- getConfigForTeam @cfg tid + guardLockStatus feat0.lockStatus + setConfigForTeam @cfg tid (withLockStatus feat0.lockStatus feat) setFeatureStatusInternal :: forall cfg r. @@ -154,8 +155,8 @@ setFeatureStatusInternal :: Member NotificationSubsystem r ) => TeamId -> - WithStatusNoLock cfg -> - Sem r (WithStatus cfg) + Feature cfg -> + Sem r (LockableFeature cfg) setFeatureStatusInternal = setFeatureStatus @cfg DontDoAuth updateLockStatus :: @@ -186,10 +187,10 @@ persistAndPushEvent :: Member TeamStore r ) => TeamId -> - WithStatusNoLock cfg -> - Sem r (WithStatus cfg) -persistAndPushEvent tid wsnl = do - setFeatureConfig (featureSingleton @cfg) tid wsnl + LockableFeature cfg -> + Sem r (LockableFeature cfg) +persistAndPushEvent tid feat = do + setFeatureConfig (featureSingleton @cfg) tid feat fs <- getConfigForTeam @cfg tid pushFeatureConfigEvent tid (Event.mkUpdateEvent fs) pure fs @@ -247,8 +248,8 @@ class (GetFeatureConfig cfg) => SetFeatureConfig cfg where Member TeamStore r ) => TeamId -> - WithStatusNoLock cfg -> - Sem r (WithStatus cfg) + LockableFeature cfg -> + Sem r (LockableFeature cfg) default setConfigForTeam :: ( ComputeFeatureConstraints cfg r, KnownSymbol (FeatureSymbol cfg), @@ -260,9 +261,9 @@ class (GetFeatureConfig cfg) => SetFeatureConfig cfg where Member TeamStore r ) => TeamId -> - WithStatusNoLock cfg -> - Sem r (WithStatus cfg) - setConfigForTeam tid wsnl = persistAndPushEvent tid wsnl + LockableFeature cfg -> + Sem r (LockableFeature cfg) + setConfigForTeam tid feat = persistAndPushEvent tid feat instance SetFeatureConfig SSOConfig where type @@ -271,11 +272,11 @@ instance SetFeatureConfig SSOConfig where Member (Error TeamFeatureError) r ) - setConfigForTeam tid wsnl = do - case wssStatus wsnl of + setConfigForTeam tid feat = do + case feat.status of FeatureStatusEnabled -> pure () FeatureStatusDisabled -> throw DisableSsoNotImplemented - persistAndPushEvent tid wsnl + persistAndPushEvent tid feat instance SetFeatureConfig SearchVisibilityAvailableConfig where type @@ -284,11 +285,11 @@ instance SetFeatureConfig SearchVisibilityAvailableConfig where Member (Input Opts) r ) - setConfigForTeam tid wsnl = do - case wssStatus wsnl of + setConfigForTeam tid feat = do + case feat.status of FeatureStatusEnabled -> pure () FeatureStatusDisabled -> SearchVisibilityData.resetSearchVisibility tid - persistAndPushEvent tid wsnl + persistAndPushEvent tid feat instance SetFeatureConfig ValidateSAMLEmailsConfig @@ -335,7 +336,7 @@ instance SetFeatureConfig LegalholdConfig where ) -- we're good to update the status now. - setConfigForTeam tid wsnl = do + setConfigForTeam tid feat = do -- this extra do is to encapsulate the assertions running before the actual operation. -- enabling LH for teams is only allowed in normal operation; disabled-permanently and -- whitelist-teams have no or their own way to do that, resp. @@ -348,20 +349,20 @@ instance SetFeatureConfig LegalholdConfig where FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> do throw LegalHoldWhitelistedOnly - case wssStatus wsnl of + case feat.status of FeatureStatusDisabled -> LegalHold.removeSettings' @InternalPaging tid FeatureStatusEnabled -> ensureNotTooLargeToActivateLegalHold tid - persistAndPushEvent tid wsnl + persistAndPushEvent tid feat instance SetFeatureConfig FileSharingConfig instance SetFeatureConfig AppLockConfig where type SetConfigForTeamConstraints AppLockConfig r = Member (Error TeamFeatureError) r - setConfigForTeam tid wsnl = do - when ((applockInactivityTimeoutSecs . wssConfig $ wsnl) < 30) $ + setConfigForTeam tid feat = do + when ((applockInactivityTimeoutSecs feat.config) < 30) $ throw AppLockInactivityTimeoutTooLow - persistAndPushEvent tid wsnl + persistAndPushEvent tid feat instance SetFeatureConfig ConferenceCallingConfig @@ -373,22 +374,22 @@ instance SetFeatureConfig SndFactorPasswordChallengeConfig instance SetFeatureConfig SearchVisibilityInboundConfig where type SetConfigForTeamConstraints SearchVisibilityInboundConfig (r :: EffectRow) = (Member BrigAccess r) - setConfigForTeam tid wsnl = do - updateSearchVisibilityInbound $ toTeamStatus tid wsnl - persistAndPushEvent tid wsnl + setConfigForTeam tid feat = do + updateSearchVisibilityInbound $ toTeamStatus tid feat + persistAndPushEvent tid feat instance SetFeatureConfig MLSConfig where type SetConfigForTeamConstraints MLSConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) - setConfigForTeam tid wsnl = do + setConfigForTeam tid feat = do mlsMigrationConfig <- getConfigForTeam @MlsMigrationConfig tid unless ( -- default protocol needs to be included in supported protocols - mlsDefaultProtocol (wssConfig wsnl) `elem` mlsSupportedProtocols (wssConfig wsnl) + feat.config.mlsDefaultProtocol `elem` feat.config.mlsSupportedProtocols -- when MLS migration is enabled, MLS needs to be enabled as well - && (wsStatus mlsMigrationConfig == FeatureStatusDisabled || wssStatus wsnl == FeatureStatusEnabled) + && (mlsMigrationConfig.status == FeatureStatusDisabled || feat.status == FeatureStatusEnabled) ) $ throw MLSProtocolMismatch - persistAndPushEvent tid wsnl + persistAndPushEvent tid feat instance SetFeatureConfig ExposeInvitationURLsToTeamAdminConfig @@ -399,25 +400,25 @@ instance SetFeatureConfig MlsE2EIdConfig guardMlsE2EIdConfig :: forall r a. (Member (Error TeamFeatureError) r) => - (UserId -> TeamId -> WithStatusNoLock MlsE2EIdConfig -> Sem r a) -> + (UserId -> TeamId -> Feature MlsE2EIdConfig -> Sem r a) -> UserId -> TeamId -> - WithStatusNoLock MlsE2EIdConfig -> + Feature MlsE2EIdConfig -> Sem r a -guardMlsE2EIdConfig handler uid tid conf = do - when (isNothing . crlProxy . wssConfig $ conf) $ throw MLSE2EIDMissingCrlProxy - handler uid tid conf +guardMlsE2EIdConfig handler uid tid feat = do + when (isNothing feat.config.crlProxy) $ throw MLSE2EIDMissingCrlProxy + handler uid tid feat instance SetFeatureConfig MlsMigrationConfig where type SetConfigForTeamConstraints MlsMigrationConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) - setConfigForTeam tid wsnl = do + setConfigForTeam tid feat = do mlsConfig <- getConfigForTeam @MLSConfig tid unless ( -- when MLS migration is enabled, MLS needs to be enabled as well - wssStatus wsnl == FeatureStatusDisabled || wsStatus mlsConfig == FeatureStatusEnabled + feat.status == FeatureStatusDisabled || mlsConfig.status == FeatureStatusEnabled ) $ throw MLSProtocolMismatch - persistAndPushEvent tid wsnl + persistAndPushEvent tid feat instance SetFeatureConfig EnforceFileDownloadLocationConfig diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index a83d1bad4f7..842403e7960 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -1,4 +1,5 @@ -{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE UndecidableSuperClasses #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. -- @@ -35,10 +36,11 @@ where import Control.Error (hush) import Control.Lens -import Data.Bifunctor (second) +import Data.Default import Data.Id import Data.Kind import Data.Qualified (Local, tUnqualified) +import Data.SOP import Data.Tagged import Galley.API.LegalHold.Team import Galley.API.Util @@ -79,39 +81,37 @@ class (IsFeatureConfig cfg) => GetFeatureConfig cfg where getConfigForServer :: (Member (Input Opts) r) => - Sem r (WithStatus cfg) + Sem r (LockableFeature cfg) -- only override if there is additional business logic for getting the feature config -- and/or if the feature flag is configured for the backend in 'FeatureFlags' for galley in 'Galley.Types.Teams' -- otherwise this will return the default config from wire-api - default getConfigForServer :: Sem r (WithStatus cfg) - getConfigForServer = pure defFeatureStatus + default getConfigForServer :: Sem r (LockableFeature cfg) + getConfigForServer = pure def getConfigForUser :: (GetConfigForUserConstraints cfg r) => UserId -> - Sem r (WithStatus cfg) + Sem r (LockableFeature cfg) default getConfigForUser :: (DefaultGetConfigForUserConstraints cfg r) => UserId -> - Sem r (WithStatus cfg) + Sem r (LockableFeature cfg) getConfigForUser _ = getConfigForServer computeFeature :: (ComputeFeatureConstraints cfg r) => TeamId -> - WithStatus cfg -> - Maybe LockStatus -> + LockableFeature cfg -> DbFeature cfg -> - Sem r (WithStatus cfg) + Sem r (LockableFeature cfg) default computeFeature :: TeamId -> - WithStatus cfg -> - Maybe LockStatus -> + LockableFeature cfg -> DbFeature cfg -> - Sem r (WithStatus cfg) - computeFeature _tid defFeature lockStatus dbFeature = + Sem r (LockableFeature cfg) + computeFeature _tid defFeature dbFeature = pure $ - genericComputeFeature @cfg defFeature lockStatus dbFeature + genericComputeFeature @cfg defFeature dbFeature getFeatureStatus :: forall cfg r. @@ -125,7 +125,7 @@ getFeatureStatus :: ) => DoAuth -> TeamId -> - Sem r (WithStatus cfg) + Sem r (LockableFeature cfg) getFeatureStatus doauth tid = do case doauth of DoAuth uid -> @@ -145,11 +145,11 @@ getFeatureStatusMulti :: Sem r (Multi.TeamFeatureNoConfigMultiResponse cfg) getFeatureStatusMulti (Multi.TeamFeatureNoConfigMultiRequest tids) = do cfgs <- getConfigForMultiTeam @cfg tids - let xs = uncurry toTeamStatus . second forgetLock <$> cfgs + let xs = uncurry toTeamStatus <$> cfgs pure $ Multi.TeamFeatureNoConfigMultiResponse xs -toTeamStatus :: TeamId -> WithStatusNoLock cfg -> Multi.TeamStatus cfg -toTeamStatus tid ws = Multi.TeamStatus tid (wssStatus ws) +toTeamStatus :: TeamId -> LockableFeature cfg -> Multi.TeamStatus cfg +toTeamStatus tid feat = Multi.TeamStatus tid feat.status getTeamAndCheckMembership :: ( Member TeamStore r, @@ -181,7 +181,18 @@ getAllFeatureConfigsForTeam luid tid = do void $ getTeamMember tid (tUnqualified luid) >>= noteS @'NotATeamMember getAllFeatureConfigs tid +class (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllFeatureConfigsForServerConstraints r cfg + +instance (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllFeatureConfigsForServerConstraints r cfg + +getAllFeatureConfigsForServer :: + forall r. + (Member (Input Opts) r) => + Sem r AllFeatureConfigs +getAllFeatureConfigsForServer = hsequence' $ hcpure (Proxy @GetFeatureConfig) $ Comp getConfigForServer + getAllFeatureConfigs :: + forall r. ( Member (Input Opts) r, Member LegalHoldStore r, Member TeamFeatureStore r, @@ -192,80 +203,18 @@ getAllFeatureConfigs :: getAllFeatureConfigs tid = do features <- TeamFeatures.getAllFeatureConfigs tid defFeatures <- getAllFeatureConfigsForServer - biTraverseAllFeatures (computeFeatureWithLock tid) defFeatures features + hsequence' $ hcliftA2 (Proxy @(GetAllFeatureConfigsForServerConstraints r)) compute defFeatures features + where + compute :: + (ComputeFeatureConstraints p r, GetFeatureConfig p) => + LockableFeature p -> + DbFeature p -> + (Sem r :.: LockableFeature) p + compute defFeature feat = Comp $ computeFeature tid defFeature feat -computeFeatureWithLock :: - forall cfg r. - (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => - TeamId -> - WithStatus cfg -> - DbFeatureWithLock cfg -> - Sem r (WithStatus cfg) -computeFeatureWithLock tid defFeature feat = - computeFeature @cfg tid defFeature feat.lockStatus feat.feature - --- | One of a number of possible combinators. This is the only one we happen to need. -biTraverseAllFeatures :: - ( Member (Input Opts) r, - Member TeamStore r, - Member LegalHoldStore r - ) => - ( forall cfg. - (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => - f cfg -> - g cfg -> - Sem r (h cfg) - ) -> - (AllFeatures f -> AllFeatures g -> Sem r (AllFeatures h)) -biTraverseAllFeatures phi features1 features2 = do - afcLegalholdStatus <- phi (afcLegalholdStatus features1) (afcLegalholdStatus features2) - afcSSOStatus <- phi (afcSSOStatus features1) (afcSSOStatus features2) - afcTeamSearchVisibilityAvailable <- phi (afcTeamSearchVisibilityAvailable features1) (afcTeamSearchVisibilityAvailable features2) - afcSearchVisibilityInboundConfig <- phi (afcSearchVisibilityInboundConfig features1) (afcSearchVisibilityInboundConfig features2) - afcValidateSAMLEmails <- phi (afcValidateSAMLEmails features1) (afcValidateSAMLEmails features2) - afcDigitalSignatures <- phi (afcDigitalSignatures features1) (afcDigitalSignatures features2) - afcAppLock <- phi (afcAppLock features1) (afcAppLock features2) - afcFileSharing <- phi (afcFileSharing features1) (afcFileSharing features2) - afcClassifiedDomains <- phi (afcClassifiedDomains features1) (afcClassifiedDomains features2) - afcConferenceCalling <- phi (afcConferenceCalling features1) (afcConferenceCalling features2) - afcSelfDeletingMessages <- phi (afcSelfDeletingMessages features1) (afcSelfDeletingMessages features2) - afcGuestLink <- phi (afcGuestLink features1) (afcGuestLink features2) - afcSndFactorPasswordChallenge <- phi (afcSndFactorPasswordChallenge features1) (afcSndFactorPasswordChallenge features2) - afcMLS <- phi (afcMLS features1) (afcMLS features2) - afcExposeInvitationURLsToTeamAdmin <- phi (afcExposeInvitationURLsToTeamAdmin features1) (afcExposeInvitationURLsToTeamAdmin features2) - afcOutlookCalIntegration <- phi (afcOutlookCalIntegration features1) (afcOutlookCalIntegration features2) - afcMlsE2EId <- phi (afcMlsE2EId features1) (afcMlsE2EId features2) - afcMlsMigration <- phi (afcMlsMigration features1) (afcMlsMigration features2) - afcEnforceFileDownloadLocation <- phi (afcEnforceFileDownloadLocation features1) (afcEnforceFileDownloadLocation features2) - afcLimitedEventFanout <- phi (afcLimitedEventFanout features1) (afcLimitedEventFanout features2) - pure AllFeatures {..} +class (GetConfigForUserConstraints cfg r, GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllFeatureConfigsForUserConstraints r cfg -getAllFeatureConfigsForServer :: - forall r. - (Member (Input Opts) r) => - Sem r AllFeatureConfigs -getAllFeatureConfigsForServer = - AllFeatures - <$> getConfigForServer @LegalholdConfig - <*> getConfigForServer @SSOConfig - <*> getConfigForServer @SearchVisibilityAvailableConfig - <*> getConfigForServer @SearchVisibilityInboundConfig - <*> getConfigForServer @ValidateSAMLEmailsConfig - <*> getConfigForServer @DigitalSignaturesConfig - <*> getConfigForServer @AppLockConfig - <*> getConfigForServer @FileSharingConfig - <*> getConfigForServer @ClassifiedDomainsConfig - <*> getConfigForServer @ConferenceCallingConfig - <*> getConfigForServer @SelfDeletingMessagesConfig - <*> getConfigForServer @GuestLinksConfig - <*> getConfigForServer @SndFactorPasswordChallengeConfig - <*> getConfigForServer @MLSConfig - <*> getConfigForServer @ExposeInvitationURLsToTeamAdminConfig - <*> getConfigForServer @OutlookCalIntegrationConfig - <*> getConfigForServer @MlsE2EIdConfig - <*> getConfigForServer @MlsMigrationConfig - <*> getConfigForServer @EnforceFileDownloadLocationConfig - <*> getConfigForServer @LimitedEventFanoutConfig +instance (GetConfigForUserConstraints cfg r, GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllFeatureConfigsForUserConstraints r cfg getAllFeatureConfigsForUser :: forall r. @@ -282,27 +231,7 @@ getAllFeatureConfigsForUser :: Sem r AllFeatureConfigs getAllFeatureConfigsForUser uid = do mTid <- getTeamAndCheckMembership uid - AllFeatures - <$> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid - <*> getConfigForTeamUser uid mTid + hsequence' $ hcpure (Proxy @(GetAllFeatureConfigsForUserConstraints r)) $ Comp $ getConfigForTeamUser uid mTid getSingleFeatureConfigForUser :: forall cfg r. @@ -316,7 +245,7 @@ getSingleFeatureConfigForUser :: ComputeFeatureConstraints cfg r ) => UserId -> - Sem r (WithStatus cfg) + Sem r (LockableFeature cfg) getSingleFeatureConfigForUser uid = do mTid <- getTeamAndCheckMembership uid getConfigForTeamUser @cfg uid mTid @@ -329,18 +258,15 @@ getConfigForTeam :: Member TeamFeatureStore r ) => TeamId -> - Sem r (WithStatus cfg) + Sem r (LockableFeature cfg) getConfigForTeam tid = do dbFeature <- TeamFeatures.getFeatureConfig (featureSingleton @cfg) tid - lockStatus <- TeamFeatures.getFeatureLockStatus (featureSingleton @cfg) tid defFeature <- getConfigForServer computeFeature @cfg tid defFeature - lockStatus dbFeature --- Note: this function assumes the feature cannot be locked getConfigForMultiTeam :: forall cfg r. ( GetFeatureConfig cfg, @@ -349,12 +275,12 @@ getConfigForMultiTeam :: Member (Input Opts) r ) => [TeamId] -> - Sem r [(TeamId, WithStatus cfg)] + Sem r [(TeamId, LockableFeature cfg)] getConfigForMultiTeam tids = do defFeature <- getConfigForServer features <- TeamFeatures.getFeatureConfigMulti (featureSingleton @cfg) tids for features $ \(tid, dbFeature) -> do - feat <- computeFeature @cfg tid defFeature (Just LockStatusUnlocked) dbFeature + feat <- computeFeature @cfg tid defFeature dbFeature pure (tid, feat) getConfigForTeamUser :: @@ -367,7 +293,7 @@ getConfigForTeamUser :: ) => UserId -> Maybe TeamId -> - Sem r (WithStatus cfg) + Sem r (LockableFeature cfg) getConfigForTeamUser uid Nothing = getConfigForUser uid getConfigForTeamUser _ (Just tid) = getConfigForTeam @cfg tid @@ -380,7 +306,7 @@ instance GetFeatureConfig SSOConfig where inputs (view (settings . featureFlags . flagSSO)) <&> \case FeatureSSOEnabledByDefault -> FeatureStatusEnabled FeatureSSODisabledByDefault -> FeatureStatusDisabled - pure $ setStatus status defFeatureStatus + pure $ def {status = status} instance GetFeatureConfig SearchVisibilityAvailableConfig where getConfigForServer = do @@ -388,7 +314,7 @@ instance GetFeatureConfig SearchVisibilityAvailableConfig where inputs (view (settings . featureFlags . flagTeamSearchVisibility)) <&> \case FeatureTeamSearchVisibilityAvailableByDefault -> FeatureStatusEnabled FeatureTeamSearchVisibilityUnavailableByDefault -> FeatureStatusDisabled - pure $ setStatus status defFeatureStatus + pure $ def {status = status} instance GetFeatureConfig ValidateSAMLEmailsConfig where getConfigForServer = @@ -411,9 +337,9 @@ instance GetFeatureConfig LegalholdConfig where ComputeFeatureConstraints LegalholdConfig r = (Member TeamStore r, Member LegalHoldStore r) - computeFeature tid defFeature _lockStatus dbFeature = do + computeFeature tid defFeature dbFeature = do status <- computeLegalHoldFeatureStatus tid dbFeature - pure $ setStatus status defFeature + pure $ defFeature {status = status} instance GetFeatureConfig FileSharingConfig where getConfigForServer = @@ -453,18 +379,15 @@ instance GetFeatureConfig ConferenceCallingConfig where input <&> view (settings . featureFlags . flagConferenceCalling . unDefaults) getConfigForUser uid = do - wsnl <- getAccountConferenceCallingConfigClient uid - pure $ withLockStatus (wsLockStatus (defFeatureStatus @ConferenceCallingConfig)) wsnl - - computeFeature _tid defFeature lockStatus dbFeature = - pure $ case fromMaybe (wsLockStatus defFeature) lockStatus of - LockStatusLocked -> setLockStatus LockStatusLocked defFeature - LockStatusUnlocked -> - withUnlocked $ - (unDbFeature dbFeature) - (forgetLock defFeature) - { wssStatus = FeatureStatusEnabled - } + feat <- getAccountConferenceCallingConfigClient uid + pure $ withLockStatus (def @(LockableFeature ConferenceCallingConfig)).lockStatus feat + + computeFeature _tid defFeature dbFeature = + pure $ + let feat = applyDbFeature dbFeature defFeature {status = FeatureStatusEnabled} + in case feat.lockStatus of + LockStatusLocked -> defFeature {lockStatus = LockStatusLocked} + LockStatusUnlocked -> feat instance GetFeatureConfig SelfDeletingMessagesConfig where getConfigForServer = @@ -492,11 +415,11 @@ instance GetFeatureConfig ExposeInvitationURLsToTeamAdminConfig where (Member (Input Opts) r) -- the lock status of this feature is calculated from the allow list, not the database - computeFeature tid defFeature _lockStatus dbFeature = do + computeFeature tid defFeature dbFeature = do allowList <- input <&> view (settings . exposeInvitationURLsTeamAllowlist . to (fromMaybe [])) let teamAllowed = tid `elem` allowList lockStatus = if teamAllowed then LockStatusUnlocked else LockStatusLocked - pure $ genericComputeFeature defFeature (Just lockStatus) dbFeature + pure $ genericComputeFeature defFeature (dbFeatureLockStatus lockStatus <> dbFeature) instance GetFeatureConfig OutlookCalIntegrationConfig where getConfigForServer = @@ -542,7 +465,7 @@ guardSecondFactorDisabled uid cid = do pure tid tf <- getConfigForTeamUser @SndFactorPasswordChallengeConfig uid mTid - case wsStatus tf of + case tf.status of FeatureStatusDisabled -> pure () FeatureStatusEnabled -> throwS @'AccessDenied @@ -560,5 +483,5 @@ featureEnabledForTeam :: Sem r Bool featureEnabledForTeam tid = (==) FeatureStatusEnabled - . wsStatus + . (.status) <$> getFeatureStatus @cfg DontDoAuth tid diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 0eab89da5c7..1e87befbc8d 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -147,8 +147,8 @@ validateOptions o = do (Just _, Nothing) -> error "Federator is specified and RabbitMQ config is not, please specify both or none" _ -> pure () let mlsFlag = settings' ^. featureFlags . Teams.flagMLS . Teams.unDefaults - mlsConfig = wsConfig mlsFlag - migrationStatus = wsStatus $ settings' ^. featureFlags . Teams.flagMlsMigration . Teams.unDefaults + mlsConfig = mlsFlag.config + migrationStatus = (.status) $ settings' ^. featureFlags . Teams.flagMlsMigration . Teams.unDefaults when (migrationStatus == FeatureStatusEnabled && ProtocolMLSTag `notElem` mlsSupportedProtocols mlsConfig) $ error "For starting MLS migration, MLS must be included in the supportedProtocol list" unless (mlsDefaultProtocol mlsConfig `elem` mlsSupportedProtocols mlsConfig) $ diff --git a/services/galley/src/Galley/Cassandra/FeatureTH.hs b/services/galley/src/Galley/Cassandra/FeatureTH.hs new file mode 100644 index 00000000000..cf52cdc6caf --- /dev/null +++ b/services/galley/src/Galley/Cassandra/FeatureTH.hs @@ -0,0 +1,53 @@ +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskellQuotes #-} + +module Galley.Cassandra.FeatureTH where + +import Data.Kind +import Generics.SOP.TH +import Imports +import Language.Haskell.TH hiding (Type) +import Wire.API.Team.Feature + +featureCases :: ExpQ -> Q Exp +featureCases rhsQ = do + rhs <- rhsQ + TyConI (DataD _ _ _ _ constructors _) <- reify ''FeatureSingleton + pure $ + LamCaseE + [ Match (ConP c [] []) (NormalB rhs) [] + | GadtC [c] _ _ <- constructors + ] + +generateTupleP :: Q [Dec] +generateTupleP = do + let maxSize = 64 :: Int + tylist <- [t|[Type]|] + let vars = [VarT (mkName ("a" <> show i)) | i <- [0 .. maxSize - 1]] + pure + [ ClosedTypeFamilyD + (TypeFamilyHead (mkName "TupleP") [KindedTV (mkName "xs") () tylist] NoSig Nothing) + [ TySynEqn + Nothing + ( ConT (mkName "TupleP") + `AppT` mkPattern (take n vars) + ) + (mkTuple (take n vars)) + | n <- [0 .. maxSize] + ] + ] + where + mkPattern = foldr (\x y -> PromotedConsT `AppT` x `AppT` y) PromotedNilT + + mkTuple [] = ConT ''() + mkTuple [v] = ConT ''Identity `AppT` v + mkTuple vs = + let n = length vs + in foldl' AppT (TupleT n) vs + +-- | generates some of the remaining @SOP.Generic@ instances as orphans +-- it is cut off at 50 on purpose to reduce compilation times +-- you may increase up to 64 which is the number at which you +-- you should probably start fixing cql instead. +generateSOPInstances :: Q [Dec] +generateSOPInstances = concat <$> traverse (deriveGeneric . tupleTypeName) [31 .. 50] diff --git a/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs b/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs index c55808c7823..6fd27b9e107 100644 --- a/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs +++ b/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs @@ -1,400 +1,86 @@ -{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-} -module Galley.Cassandra.GetAllTeamFeatureConfigs where +module Galley.Cassandra.GetAllTeamFeatureConfigs (getAllFeatureConfigs) where import Cassandra -import Cassandra qualified as C import Data.Id -import Data.Misc (HttpsUrl) -import Data.Time -import Database.CQL.Protocol import Galley.Cassandra.Instances () -import Imports -import Wire.API.Conversation.Protocol (ProtocolTag) -import Wire.API.MLS.CipherSuite +import Galley.Cassandra.MakeFeature +import Galley.Cassandra.Orphans () +import Generics.SOP +import Imports hiding (Map) +import Polysemy.Internal import Wire.API.Team.Feature -data AllTeamFeatureConfigsRow = AllTeamFeatureConfigsRow - { -- legalhold - legalhold :: Maybe FeatureStatus, - -- sso - sso :: Maybe FeatureStatus, - -- search visibility - searchVisibility :: Maybe FeatureStatus, - -- validate saml emails - validateSamlEmails :: Maybe FeatureStatus, - -- digital signatures - digitalSignatures :: Maybe FeatureStatus, - -- app lock - appLock :: Maybe FeatureStatus, - appLockEnforce :: Maybe EnforceAppLock, - appLockInactivityTimeoutSecs :: Maybe Int32, - -- file sharing - fileSharing :: Maybe FeatureStatus, - fileSharingLock :: Maybe LockStatus, - -- self deleting messages - selfDeletingMessages :: Maybe FeatureStatus, - selfDeletingMessagesTtl :: Maybe Int32, - selfDeletingMessagesLock :: Maybe LockStatus, - -- conference calling - conferenceCalling :: Maybe FeatureStatus, - conferenceCallingTtl :: Maybe FeatureTTL, - conferenceCallingOne2One :: Maybe One2OneCalls, - conferenceCallingLock :: Maybe LockStatus, - -- guest links - guestLinks :: Maybe FeatureStatus, - guestLinksLock :: Maybe LockStatus, - -- snd factor - sndFactor :: Maybe FeatureStatus, - sndFactorLock :: Maybe LockStatus, - -- mls - mls :: Maybe FeatureStatus, - mlsDefaultProtocol :: Maybe ProtocolTag, - mlsToggleUsers :: Maybe (C.Set UserId), - mlsAllowedCipherSuites :: Maybe (C.Set CipherSuiteTag), - mlsDefaultCipherSuite :: Maybe CipherSuiteTag, - mlsSupportedProtocols :: Maybe (C.Set ProtocolTag), - mlsLock :: Maybe LockStatus, - -- mls e2eid - mlsE2eid :: Maybe FeatureStatus, - mlsE2eidGracePeriod :: Maybe Int32, - mlsE2eidAcmeDiscoverUrl :: Maybe HttpsUrl, - mlsE2eidMaybeCrlProxy :: Maybe HttpsUrl, - mlsE2eidMaybeUseProxyOnMobile :: Maybe Bool, - mlsE2eidLock :: Maybe LockStatus, - -- mls migration - mlsMigration :: Maybe FeatureStatus, - mlsMigrationStartTime :: Maybe UTCTime, - mlsMigrationFinalizeRegardlessAfter :: Maybe UTCTime, - mlsMigrationLock :: Maybe LockStatus, - -- expose invitation urls - exposeInvitationUrls :: Maybe FeatureStatus, - -- outlook calendar integration - outlookCalIntegration :: Maybe FeatureStatus, - outlookCalIntegrationLock :: Maybe LockStatus, - -- enforce download location - enforceDownloadLocation :: Maybe FeatureStatus, - enforceDownloadLocation_Location :: Maybe Text, - enforceDownloadLocationLock :: Maybe LockStatus, - -- limit event fanout - limitEventFanout :: Maybe FeatureStatus - } - deriving (Generic, Show) +type family ConcatFeatureRow xs where + ConcatFeatureRow '[] = '[] + ConcatFeatureRow (x : xs) = Append (FeatureRow x) (ConcatFeatureRow xs) -recordInstance ''AllTeamFeatureConfigsRow +type AllFeatureRow = ConcatFeatureRow Features -emptyRow :: AllTeamFeatureConfigsRow -emptyRow = - AllTeamFeatureConfigsRow - { legalhold = Nothing, - sso = Nothing, - searchVisibility = Nothing, - validateSamlEmails = Nothing, - digitalSignatures = Nothing, - appLock = Nothing, - appLockEnforce = Nothing, - appLockInactivityTimeoutSecs = Nothing, - fileSharing = Nothing, - fileSharingLock = Nothing, - selfDeletingMessages = Nothing, - selfDeletingMessagesTtl = Nothing, - selfDeletingMessagesLock = Nothing, - conferenceCalling = Nothing, - conferenceCallingTtl = Nothing, - conferenceCallingOne2One = Nothing, - conferenceCallingLock = Nothing, - guestLinks = Nothing, - guestLinksLock = Nothing, - sndFactor = Nothing, - sndFactorLock = Nothing, - mls = Nothing, - mlsDefaultProtocol = Nothing, - mlsToggleUsers = Nothing, - mlsAllowedCipherSuites = Nothing, - mlsDefaultCipherSuite = Nothing, - mlsSupportedProtocols = Nothing, - mlsLock = Nothing, - mlsE2eid = Nothing, - mlsE2eidGracePeriod = Nothing, - mlsE2eidAcmeDiscoverUrl = Nothing, - mlsE2eidMaybeCrlProxy = Nothing, - mlsE2eidMaybeUseProxyOnMobile = Nothing, - mlsE2eidLock = Nothing, - mlsMigration = Nothing, - mlsMigrationStartTime = Nothing, - mlsMigrationFinalizeRegardlessAfter = Nothing, - mlsMigrationLock = Nothing, - exposeInvitationUrls = Nothing, - outlookCalIntegration = Nothing, - outlookCalIntegrationLock = Nothing, - enforceDownloadLocation = Nothing, - enforceDownloadLocation_Location = Nothing, - enforceDownloadLocationLock = Nothing, - limitEventFanout = Nothing - } +emptyRow :: NP Maybe AllFeatureRow +emptyRow = hpure Nothing -allFeatureConfigsFromRow :: AllTeamFeatureConfigsRow -> AllFeatures DbFeatureWithLock -allFeatureConfigsFromRow row = - AllFeatures - { afcLegalholdStatus = mkFeatureWithLock Nothing row.legalhold, - afcSSOStatus = mkFeatureWithLock Nothing row.sso, - afcTeamSearchVisibilityAvailable = mkFeatureWithLock Nothing row.searchVisibility, - afcSearchVisibilityInboundConfig = mkFeatureWithLock Nothing row.searchVisibility, - afcValidateSAMLEmails = mkFeatureWithLock Nothing row.validateSamlEmails, - afcDigitalSignatures = mkFeatureWithLock Nothing row.digitalSignatures, - afcAppLock = - mkFeatureWithLock - Nothing - (row.appLock, row.appLockEnforce, row.appLockInactivityTimeoutSecs), - afcFileSharing = mkFeatureWithLock row.fileSharingLock row.fileSharing, - afcClassifiedDomains = mkFeatureWithLock Nothing Nothing, - afcConferenceCalling = - mkFeatureWithLock - row.conferenceCallingLock - ( row.conferenceCalling, - row.conferenceCallingTtl, - row.conferenceCallingOne2One - ), - afcSelfDeletingMessages = - mkFeatureWithLock - row.selfDeletingMessagesLock - ( row.selfDeletingMessages, - row.selfDeletingMessagesTtl - ), - afcGuestLink = mkFeatureWithLock row.guestLinksLock row.guestLinks, - afcSndFactorPasswordChallenge = mkFeatureWithLock row.sndFactorLock row.sndFactor, - afcMLS = - mkFeatureWithLock - row.mlsLock - ( row.mls, - row.mlsDefaultProtocol, - row.mlsToggleUsers, - row.mlsAllowedCipherSuites, - row.mlsDefaultCipherSuite, - row.mlsSupportedProtocols - ), - afcExposeInvitationURLsToTeamAdmin = mkFeatureWithLock Nothing row.exposeInvitationUrls, - afcOutlookCalIntegration = - mkFeatureWithLock - row.outlookCalIntegrationLock - row.outlookCalIntegration, - afcMlsE2EId = - mkFeatureWithLock - row.mlsE2eidLock - ( row.mlsE2eid, - row.mlsE2eidGracePeriod, - row.mlsE2eidAcmeDiscoverUrl, - row.mlsE2eidMaybeCrlProxy, - row.mlsE2eidMaybeUseProxyOnMobile - ), - afcMlsMigration = - mkFeatureWithLock - row.mlsMigrationLock - ( row.mlsMigration, - row.mlsMigrationStartTime, - row.mlsMigrationFinalizeRegardlessAfter - ), - afcEnforceFileDownloadLocation = - mkFeatureWithLock - row.enforceDownloadLocationLock - ( row.enforceDownloadLocation, - row.enforceDownloadLocation_Location - ), - afcLimitedEventFanout = mkFeatureWithLock Nothing row.limitEventFanout - } +class ConcatFeatures cfgs where + rowToAllFeatures :: NP Maybe (ConcatFeatureRow cfgs) -> NP DbFeature cfgs -getAllFeatureConfigs :: (MonadClient m) => TeamId -> m (AllFeatures DbFeatureWithLock) -getAllFeatureConfigs tid = do - mRow <- retry x1 $ query1 select (params LocalQuorum (Identity tid)) - pure $ allFeatureConfigsFromRow $ maybe emptyRow asRecord mRow - where - select :: - PrepQuery - R - (Identity TeamId) - (TupleType AllTeamFeatureConfigsRow) - select = - "select \ - \legalhold_status, \ - \sso_status, \ - \search_visibility_status, \ - \validate_saml_emails, \ - \digital_signatures, \ - \app_lock_status, app_lock_enforce, app_lock_inactivity_timeout_secs, \ - \file_sharing, file_sharing_lock_status, \ - \self_deleting_messages_status, self_deleting_messages_ttl, self_deleting_messages_lock_status, \ - \conference_calling_status, ttl(conference_calling_status), conference_calling_one_to_one, conference_calling, \ - \guest_links_status, guest_links_lock_status, \ - \snd_factor_password_challenge_status, snd_factor_password_challenge_lock_status, \ - \\ - \mls_status, mls_default_protocol, mls_protocol_toggle_users, mls_allowed_ciphersuites, \ - \mls_default_ciphersuite, mls_supported_protocols, mls_lock_status, \ - \\ - \mls_e2eid_status, mls_e2eid_grace_period, mls_e2eid_acme_discovery_url, mls_e2eid_crl_proxy, mls_e2eid_use_proxy_on_mobile, mls_e2eid_lock_status, \ - \\ - \mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after, \ - \mls_migration_lock_status, \ - \\ - \expose_invitation_urls_to_team_admin, \ - \outlook_cal_integration_status, outlook_cal_integration_lock_status, \ - \enforce_file_download_location_status, enforce_file_download_location, enforce_file_download_location_lock_status, \ - \limited_event_fanout_status \ - \from team_features where team_id = ?" - -class (Tuple (FeatureRow cfg), HasRowType (FeatureRow cfg)) => MakeFeature cfg where - type FeatureRow cfg - type FeatureRow cfg = Identity (Maybe FeatureStatus) - - mkFeature :: RowType (FeatureRow cfg) -> DbFeature cfg - default mkFeature :: - (FeatureRow cfg ~ Identity (Maybe FeatureStatus)) => - RowType (FeatureRow cfg) -> - DbFeature cfg - mkFeature = foldMap dbFeatureStatus - -mkFeatureWithLock :: - (MakeFeature cfg) => - Maybe LockStatus -> - RowType (FeatureRow cfg) -> - DbFeatureWithLock cfg -mkFeatureWithLock lockStatus row = DbFeatureWithLock lockStatus (mkFeature row) - --- | Used to remove the annoying Identity wrapper around single-element rows. -type family RowType a where - RowType (Identity a) = a - RowType tuple = tuple - -class HasRowType a where - fromRowType :: RowType a -> a - default fromRowType :: (RowType a ~ a) => RowType a -> a - fromRowType = id - - toRowType :: a -> RowType a - default toRowType :: (RowType a ~ a) => a -> RowType a - toRowType = id - -instance HasRowType (a, b) - -instance HasRowType (a, b, c) - -instance HasRowType (a, b, c, d) - -instance HasRowType (a, b, c, d, e) - -instance HasRowType (a, b, c, d, e, f) - -instance HasRowType (Identity a) where - fromRowType = Identity - toRowType = runIdentity - -instance MakeFeature LegalholdConfig - -instance MakeFeature SSOConfig - -instance MakeFeature SearchVisibilityAvailableConfig +instance ConcatFeatures '[] where + rowToAllFeatures Nil = Nil -instance MakeFeature SearchVisibilityInboundConfig - -instance MakeFeature ValidateSAMLEmailsConfig - -instance MakeFeature DigitalSignaturesConfig - -instance MakeFeature AppLockConfig where - type FeatureRow AppLockConfig = (Maybe FeatureStatus, Maybe EnforceAppLock, Maybe Int32) - - mkFeature (status, enforce, timeout) = - foldMap dbFeatureStatus status - <> foldMap dbFeatureConfig (AppLockConfig <$> enforce <*> timeout) - -instance MakeFeature FileSharingConfig - -instance MakeFeature ClassifiedDomainsConfig - -instance MakeFeature ConferenceCallingConfig where - type FeatureRow ConferenceCallingConfig = (Maybe FeatureStatus, Maybe FeatureTTL, Maybe One2OneCalls) - - mkFeature (status, ttl, sftForOneToOne) = - foldMap dbFeatureStatus status - <> foldMap dbFeatureTTL ttl - <> foldMap (dbFeatureConfig . ConferenceCallingConfig) sftForOneToOne - -instance MakeFeature SelfDeletingMessagesConfig where - type FeatureRow SelfDeletingMessagesConfig = (Maybe FeatureStatus, Maybe Int32) - - mkFeature (status, ttl) = - foldMap dbFeatureStatus status - <> foldMap (dbFeatureConfig . SelfDeletingMessagesConfig) ttl - -instance MakeFeature GuestLinksConfig - -instance MakeFeature SndFactorPasswordChallengeConfig - -instance MakeFeature ExposeInvitationURLsToTeamAdminConfig - -instance MakeFeature OutlookCalIntegrationConfig +instance + ( SplitNP (FeatureRow cfg) (ConcatFeatureRow cfgs), + ConcatFeatures cfgs, + MakeFeature cfg + ) => + ConcatFeatures (cfg : cfgs) + where + rowToAllFeatures row = case splitNP @(FeatureRow cfg) @(ConcatFeatureRow cfgs) row of + (row0, row1) -> rowToFeature row0 :* rowToAllFeatures row1 -instance MakeFeature MLSConfig where - type - FeatureRow MLSConfig = - ( Maybe FeatureStatus, - Maybe ProtocolTag, - Maybe (C.Set UserId), - Maybe (C.Set CipherSuiteTag), - Maybe CipherSuiteTag, - Maybe (C.Set ProtocolTag) - ) +class SplitNP xs ys where + splitNP :: NP f (Append xs ys) -> (NP f xs, NP f ys) - mkFeature (status, defProto, toggleUsers, ciphersuites, defCiphersuite, supportedProtos) = - foldMap dbFeatureStatus status - <> foldMap - dbFeatureConfig - ( MLSConfig (foldMap C.fromSet toggleUsers) - <$> defProto - <*> pure (foldMap C.fromSet ciphersuites) - <*> defCiphersuite - <*> pure (foldMap C.fromSet supportedProtos) - ) +instance SplitNP '[] ys where + splitNP ys = (Nil, ys) -instance MakeFeature MlsE2EIdConfig where - type - FeatureRow MlsE2EIdConfig = - ( Maybe FeatureStatus, - Maybe Int32, - Maybe HttpsUrl, - Maybe HttpsUrl, - Maybe Bool - ) +instance (SplitNP xs ys) => SplitNP (x ': xs) ys where + splitNP (z :* zs) = case splitNP zs of + (xs, ys) -> (z :* xs, ys) - mkFeature (status, gracePeriod, acmeDiscoveryUrl, crlProxy, useProxyOnMobile) = - foldMap dbFeatureStatus status - <> dbFeatureModConfig - ( \defCfg -> - defCfg - { verificationExpiration = - maybe defCfg.verificationExpiration fromIntegral gracePeriod, - acmeDiscoveryUrl = acmeDiscoveryUrl, - crlProxy = crlProxy, - useProxyOnMobile = fromMaybe defCfg.useProxyOnMobile useProxyOnMobile - } - ) +class AppendNP xs ys where + appendNP :: NP f xs -> NP f ys -> NP f (Append xs ys) -instance MakeFeature MlsMigrationConfig where - type - FeatureRow MlsMigrationConfig = - ( Maybe FeatureStatus, - Maybe UTCTime, - Maybe UTCTime - ) +instance AppendNP '[] ys where + appendNP Nil ys = ys - mkFeature (status, startTime, finalizeAfter) = - foldMap dbFeatureStatus status - <> dbFeatureConfig (MlsMigrationConfig startTime finalizeAfter) +instance (AppendNP xs ys) => AppendNP (x : xs) ys where + appendNP (x :* xs) ys = x :* appendNP xs ys -instance MakeFeature EnforceFileDownloadLocationConfig where - type FeatureRow EnforceFileDownloadLocationConfig = (Maybe FeatureStatus, Maybe Text) +class ConcatColumns cfgs where + concatColumns :: NP (K String) (ConcatFeatureRow cfgs) - mkFeature (status, location) = - foldMap dbFeatureStatus status - <> dbFeatureConfig (EnforceFileDownloadLocationConfig location) +instance ConcatColumns '[] where + concatColumns = Nil -instance MakeFeature LimitedEventFanoutConfig +instance + ( AppendNP (FeatureRow cfg) (ConcatFeatureRow cfgs), + MakeFeature cfg, + ConcatColumns cfgs + ) => + ConcatColumns (cfg : cfgs) + where + concatColumns = featureColumns @cfg `appendNP` concatColumns @cfgs + +getAllFeatureConfigs :: + forall row mrow m. + ( MonadClient m, + row ~ AllFeatureRow, + Tuple (TupleP mrow), + IsProductType (TupleP mrow) mrow, + AllZip (IsF Maybe) row mrow + ) => + TeamId -> + m (AllFeatures DbFeature) +getAllFeatureConfigs tid = do + mRow <- fetchFeatureRow @row @mrow tid (concatColumns @Features) + pure . rowToAllFeatures $ fromMaybe emptyRow mRow diff --git a/services/galley/src/Galley/Cassandra/MakeFeature.hs b/services/galley/src/Galley/Cassandra/MakeFeature.hs new file mode 100644 index 00000000000..2db777f2521 --- /dev/null +++ b/services/galley/src/Galley/Cassandra/MakeFeature.hs @@ -0,0 +1,463 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE TemplateHaskell #-} + +-- | Abstraction to fetch and store feature values from and to the database. +module Galley.Cassandra.MakeFeature where + +import Cassandra +import Cassandra qualified as C +import Data.Functor +import Data.Functor.Identity +import Data.Id +import Data.Kind +import Data.List.Singletons (Length) +import Data.Misc (HttpsUrl) +import Data.Singletons (demote) +import Data.Time +import GHC.TypeNats +import Galley.Cassandra.FeatureTH +import Galley.Cassandra.Instances () +import Generics.SOP +import Imports hiding (Generic, Map) +import Wire.API.Conversation.Protocol (ProtocolTag) +import Wire.API.MLS.CipherSuite +import Wire.API.Team.Feature + +-- | This is necessary in order to convert an @NP f xs@ type to something that +-- CQL can understand. +-- +-- The generated code looks like: +-- @@ +-- instance TupleP xs where +-- TupleP '[] = () +-- TupleP '[a] = Identity a +-- TupleP '[a, b] = (a, b) +-- ... +-- @@ +$generateTupleP + +class MakeFeature cfg where + type FeatureRow cfg :: [Type] + type FeatureRow cfg = '[FeatureStatus] + + featureColumns :: NP (K String) (FeatureRow cfg) + + rowToFeature :: NP Maybe (FeatureRow cfg) -> DbFeature cfg + default rowToFeature :: + (FeatureRow cfg ~ '[FeatureStatus]) => + NP Maybe (FeatureRow cfg) -> + DbFeature cfg + rowToFeature = foldMap dbFeatureStatus . hd + + featureToRow :: LockableFeature cfg -> NP Maybe (FeatureRow cfg) + default featureToRow :: + (FeatureRow cfg ~ '[FeatureStatus]) => + LockableFeature cfg -> + NP Maybe (FeatureRow cfg) + featureToRow feat = Just feat.status :* Nil + +instance MakeFeature LegalholdConfig where + featureColumns = K "legalhold_status" :* Nil + +instance MakeFeature SSOConfig where + featureColumns = K "sso_status" :* Nil + +instance MakeFeature SearchVisibilityAvailableConfig where + featureColumns = K "search_visibility_status" :* Nil + +-- | This feature shares its status column with +-- 'SearchVisibilityAvailableConfig'. This means that when fetching all +-- features, this column is repeated in the query, i.e. the query looks like: +-- @@ +-- select ..., search_visibility_status, search_visibility_status, ... from team_features ... +-- @@ +instance MakeFeature SearchVisibilityInboundConfig where + featureColumns = K "search_visibility_status" :* Nil + +instance MakeFeature ValidateSAMLEmailsConfig where + featureColumns = K "validate_saml_emails" :* Nil + +instance MakeFeature DigitalSignaturesConfig where + featureColumns = K "digital_signatures" :* Nil + +instance MakeFeature AppLockConfig where + type FeatureRow AppLockConfig = '[FeatureStatus, EnforceAppLock, Int32] + featureColumns = + K "app_lock_status" + :* K "app_lock_enforce" + :* K "app_lock_inactivity_timeout_secs" + :* Nil + + rowToFeature (status :* enforce :* timeout :* Nil) = + foldMap dbFeatureStatus status + <> foldMap dbFeatureConfig (AppLockConfig <$> enforce <*> timeout) + + featureToRow feat = + Just feat.status + :* Just feat.config.applockEnforceAppLock + :* Just feat.config.applockInactivityTimeoutSecs + :* Nil + +instance MakeFeature ClassifiedDomainsConfig where + type FeatureRow ClassifiedDomainsConfig = '[] + featureColumns = Nil + + rowToFeature Nil = mempty + featureToRow _ = Nil + +instance MakeFeature FileSharingConfig where + type FeatureRow FileSharingConfig = '[LockStatus, FeatureStatus] + featureColumns = K "file_sharing_lock_status" :* K "file_sharing" :* Nil + + rowToFeature (lockStatus :* status :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + + featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil + +instance MakeFeature ConferenceCallingConfig where + type FeatureRow ConferenceCallingConfig = '[LockStatus, FeatureStatus, One2OneCalls] + featureColumns = + K "conference_calling" + :* K "conference_calling_status" + :* K "conference_calling_one_to_one" + :* Nil + + rowToFeature (lockStatus :* status :* calls :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> foldMap (dbFeatureConfig . ConferenceCallingConfig) calls + + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + :* Just feat.config.one2OneCalls + :* Nil + +instance MakeFeature SelfDeletingMessagesConfig where + type FeatureRow SelfDeletingMessagesConfig = '[LockStatus, FeatureStatus, Int32] + featureColumns = + K "self_deleting_messages_lock_status" + :* K "self_deleting_messages_status" + :* K "self_deleting_messages_ttl" + :* Nil + + rowToFeature (lockStatus :* status :* ttl :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> foldMap (dbFeatureConfig . SelfDeletingMessagesConfig) ttl + + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + :* Just feat.config.sdmEnforcedTimeoutSeconds + :* Nil + +instance MakeFeature GuestLinksConfig where + type FeatureRow GuestLinksConfig = '[LockStatus, FeatureStatus] + featureColumns = K "guest_links_lock_status" :* K "guest_links_status" :* Nil + + rowToFeature (lockStatus :* status :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + + featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil + +instance MakeFeature SndFactorPasswordChallengeConfig where + type FeatureRow SndFactorPasswordChallengeConfig = '[LockStatus, FeatureStatus] + featureColumns = + K "snd_factor_password_challenge_lock_status" + :* K "snd_factor_password_challenge_status" + :* Nil + + rowToFeature (lockStatus :* status :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + + featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil + +instance MakeFeature ExposeInvitationURLsToTeamAdminConfig where + featureColumns = K "expose_invitation_urls_to_team_admin" :* Nil + +instance MakeFeature OutlookCalIntegrationConfig where + type FeatureRow OutlookCalIntegrationConfig = '[LockStatus, FeatureStatus] + + featureColumns = + K "outlook_cal_integration_lock_status" + :* K "outlook_cal_integration_status" + :* Nil + + rowToFeature (lockStatus :* status :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + + featureToRow feat = Just feat.lockStatus :* Just feat.status :* Nil + +instance MakeFeature MLSConfig where + type + FeatureRow MLSConfig = + '[ LockStatus, + FeatureStatus, + ProtocolTag, + (C.Set UserId), + (C.Set CipherSuiteTag), + CipherSuiteTag, + (C.Set ProtocolTag) + ] + featureColumns = + K "mls_lock_status" + :* K "mls_status" + :* K "mls_default_protocol" + :* K "mls_protocol_toggle_users" + :* K "mls_allowed_ciphersuites" + :* K "mls_default_ciphersuite" + :* K "mls_supported_protocols" + :* Nil + + rowToFeature + ( lockStatus + :* status + :* defProto + :* toggleUsers + :* ciphersuites + :* defCiphersuite + :* supportedProtos + :* Nil + ) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> foldMap + dbFeatureConfig + ( MLSConfig (foldMap C.fromSet toggleUsers) + <$> defProto + <*> pure (foldMap C.fromSet ciphersuites) + <*> defCiphersuite + <*> pure (foldMap C.fromSet supportedProtos) + ) + + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + :* Just feat.config.mlsDefaultProtocol + :* Just (C.Set feat.config.mlsProtocolToggleUsers) + :* Just (C.Set feat.config.mlsAllowedCipherSuites) + :* Just feat.config.mlsDefaultCipherSuite + :* Just (C.Set feat.config.mlsSupportedProtocols) + :* Nil + +instance MakeFeature MlsE2EIdConfig where + type + FeatureRow MlsE2EIdConfig = + '[ LockStatus, + FeatureStatus, + Int32, + HttpsUrl, + HttpsUrl, + Bool + ] + featureColumns = + K "mls_e2eid_lock_status" + :* K "mls_e2eid_status" + :* K "mls_e2eid_grace_period" + :* K "mls_e2eid_acme_discovery_url" + :* K "mls_e2eid_crl_proxy" + :* K "mls_e2eid_use_proxy_on_mobile" + :* Nil + + rowToFeature + ( lockStatus + :* status + :* gracePeriod + :* acmeDiscoveryUrl + :* crlProxy + :* useProxyOnMobile + :* Nil + ) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> dbFeatureModConfig + ( \defCfg -> + defCfg + { verificationExpiration = + maybe defCfg.verificationExpiration fromIntegral gracePeriod, + acmeDiscoveryUrl = acmeDiscoveryUrl, + crlProxy = crlProxy, + useProxyOnMobile = fromMaybe defCfg.useProxyOnMobile useProxyOnMobile + } + ) + + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + :* Just (truncate feat.config.verificationExpiration) + :* feat.config.acmeDiscoveryUrl + :* feat.config.crlProxy + :* Just feat.config.useProxyOnMobile + :* Nil + +instance MakeFeature MlsMigrationConfig where + type + FeatureRow MlsMigrationConfig = + '[LockStatus, FeatureStatus, UTCTime, UTCTime] + + featureColumns = + K "mls_migration_lock_status" + :* K "mls_migration_status" + :* K "mls_migration_start_time" + :* K "mls_migration_finalise_regardless_after" + :* Nil + + rowToFeature (lockStatus :* status :* startTime :* finalizeAfter :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> dbFeatureConfig (MlsMigrationConfig startTime finalizeAfter) + + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + :* feat.config.startTime + :* feat.config.finaliseRegardlessAfter + :* Nil + +instance MakeFeature EnforceFileDownloadLocationConfig where + type FeatureRow EnforceFileDownloadLocationConfig = '[LockStatus, FeatureStatus, Text] + + featureColumns = + K "enforce_file_download_location_lock_status" + :* K "enforce_file_download_location_status" + :* K "enforce_file_download_location" + :* Nil + + rowToFeature (lockStatus :* status :* location :* Nil) = + foldMap dbFeatureLockStatus lockStatus + <> foldMap dbFeatureStatus status + <> dbFeatureConfig (EnforceFileDownloadLocationConfig location) + featureToRow feat = + Just feat.lockStatus + :* Just feat.status + :* feat.config.enforcedDownloadLocation + :* Nil + +instance MakeFeature LimitedEventFanoutConfig where + featureColumns = K "limited_event_fanout_status" :* Nil + +fetchFeature :: + forall cfg m row mrow. + ( MonadClient m, + row ~ FeatureRow cfg, + MakeFeature cfg, + IsProductType (TupleP mrow) mrow, + AllZip (IsF Maybe) row mrow, + Tuple (TupleP mrow) + ) => + TeamId -> + m (DbFeature cfg) +fetchFeature tid = do + case featureColumns @cfg of + Nil -> pure (rowToFeature Nil) + cols -> do + mRow <- fetchFeatureRow @row @mrow tid cols + pure $ foldMap rowToFeature mRow + +fetchFeatureRow :: + forall row mrow m. + ( MonadClient m, + IsProductType (TupleP mrow) mrow, + AllZip (IsF Maybe) row mrow, + Tuple (TupleP mrow) + ) => + TeamId -> + NP (K String) row -> + m (Maybe (NP Maybe row)) +fetchFeatureRow tid cols = do + let select :: PrepQuery R (Identity TeamId) (TupleP mrow) + select = + fromString $ + "select " + <> intercalate ", " (hcollapse cols) + <> " from team_features where team_id = ?" + row <- retry x1 $ query1 select (params LocalQuorum (Identity tid)) + pure $ fmap (unfactorI . productTypeFrom) row + +storeFeature :: + forall cfg m row mrow. + ( MonadClient m, + row ~ FeatureRow cfg, + MakeFeature cfg, + IsProductType (TupleP (TeamId : mrow)) (TeamId : mrow), + AllZip (IsF Maybe) row mrow, + Tuple (TupleP (TeamId : mrow)), + KnownNat (Length row) + ) => + TeamId -> + LockableFeature cfg -> + m () +storeFeature tid feat = do + if n == 0 + then pure () + else + retry x5 $ + write + insert + ( params LocalQuorum (productTypeTo (I tid :* factorI (featureToRow feat))) + ) + where + n :: Int + n = fromIntegral (demote @(Length row)) + + insert :: PrepQuery W (TupleP (TeamId ': mrow)) () + insert = + fromString $ + "insert into team_features (team_id, " + <> intercalate ", " (hcollapse (featureColumns @cfg)) + <> ") values (" + <> intercalate "," (replicate (succ n) "?") + <> ")" + +class (FeatureRow cfg ~ row) => StoreFeatureLockStatus (row :: [Type]) cfg where + storeFeatureLockStatus' :: (MonadClient m) => TeamId -> Tagged cfg LockStatus -> m () + +instance + {-# OVERLAPPING #-} + ( FeatureRow cfg ~ (LockStatus ': row), + MakeFeature cfg + ) => + StoreFeatureLockStatus (LockStatus ': row) cfg + where + storeFeatureLockStatus' tid lock = do + let col = unK (hd (featureColumns @cfg)) + insert :: PrepQuery W (TeamId, LockStatus) () + insert = + fromString $ + "insert into team_features (team_id, " <> col <> ") values (?, ?)" + retry x5 $ write insert (params LocalQuorum (tid, (untag lock))) + +instance (FeatureRow cfg ~ row) => StoreFeatureLockStatus row cfg where + storeFeatureLockStatus' _ _ = pure () + +storeFeatureLockStatus :: + forall cfg m. + (MonadClient m, StoreFeatureLockStatus (FeatureRow cfg) cfg) => + TeamId -> + Tagged cfg LockStatus -> + m () +storeFeatureLockStatus = storeFeatureLockStatus' @(FeatureRow cfg) + +-- | Convert @NP f [x1, ..., xn]@ to @NP I [f x1, ..., f xn]@. +-- +-- This works because @I . f = f@. +factorI :: forall f xs ys. (AllZip (IsF f) xs ys) => NP f xs -> NP I ys +factorI Nil = Nil +factorI (x :* xs) = I x :* factorI xs + +-- | Convert @NP I [f x1, ..., f xn]@ to @NP f [x1, ..., xn]@. +-- +-- See 'factorI'. +unfactorI :: forall f xs ys. (AllZip (IsF f) xs ys) => NP I ys -> NP f xs +unfactorI Nil = Nil +unfactorI (I x :* xs) = x :* unfactorI xs + +-- | This is to emulate a constraint-level lambda. +class (f x ~ y) => IsF f x y | y -> x + +instance (f x ~ y) => IsF f x y diff --git a/services/galley/src/Galley/Cassandra/Orphans.hs b/services/galley/src/Galley/Cassandra/Orphans.hs new file mode 100644 index 00000000000..d939cdafdb0 --- /dev/null +++ b/services/galley/src/Galley/Cassandra/Orphans.hs @@ -0,0 +1,8 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wno-orphans #-} + +module Galley.Cassandra.Orphans where + +import Galley.Cassandra.FeatureTH + +$generateSOPInstances diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index a751060e668..f1db0f1a5c6 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE TemplateHaskell #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -23,13 +25,12 @@ module Galley.Cassandra.TeamFeatures where import Cassandra -import Cassandra qualified as C import Data.Id -import Data.Misc (HttpsUrl) -import Data.Time import Galley.API.Teams.Features.Get +import Galley.Cassandra.FeatureTH import Galley.Cassandra.GetAllTeamFeatureConfigs import Galley.Cassandra.Instances () +import Galley.Cassandra.MakeFeature import Galley.Cassandra.Store import Galley.Cassandra.Util import Galley.Effects.TeamFeatureStore qualified as TFS @@ -38,8 +39,6 @@ import Polysemy import Polysemy.Input import Polysemy.TinyLog import UnliftIO.Async (pooledMapConcurrentlyN) -import Wire.API.Conversation.Protocol (ProtocolTag) -import Wire.API.MLS.CipherSuite import Wire.API.Team.Feature interpretTeamFeatureStoreToCassandra :: @@ -56,256 +55,16 @@ interpretTeamFeatureStoreToCassandra = interpret $ \case TFS.GetFeatureConfigMulti sing tids -> do logEffect "TeamFeatureStore.GetFeatureConfigMulti" embedClient $ getFeatureConfigMulti sing tids - TFS.SetFeatureConfig sing tid wsnl -> do + TFS.SetFeatureConfig sing tid feat -> do logEffect "TeamFeatureStore.SetFeatureConfig" - embedClient $ setFeatureConfig sing tid wsnl - TFS.GetFeatureLockStatus sing tid -> do - logEffect "TeamFeatureStore.GetFeatureLockStatus" - embedClient $ getFeatureLockStatus sing tid - TFS.SetFeatureLockStatus sing tid ls -> do + embedClient $ setFeatureConfig sing tid feat + TFS.SetFeatureLockStatus sing tid lock -> do logEffect "TeamFeatureStore.SetFeatureLockStatus" - embedClient $ setFeatureLockStatus sing tid ls + embedClient $ setFeatureLockStatus sing tid (Tagged lock) TFS.GetAllFeatureConfigs tid -> do logEffect "TeamFeatureStore.GetAllFeatureConfigs" embedClient $ getAllFeatureConfigs tid -getFeatureConfig :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (DbFeature cfg) -getFeatureConfig FeatureSingletonLegalholdConfig tid = getFeature "legalhold_status" tid -getFeatureConfig FeatureSingletonSSOConfig tid = getFeature "sso_status" tid -getFeatureConfig FeatureSingletonSearchVisibilityAvailableConfig tid = getFeature "search_visibility_status" tid -getFeatureConfig FeatureSingletonValidateSAMLEmailsConfig tid = getFeature "validate_saml_emails" tid -getFeatureConfig FeatureSingletonClassifiedDomainsConfig _tid = pure mempty -getFeatureConfig FeatureSingletonDigitalSignaturesConfig tid = getFeature "digital_signatures" tid -getFeatureConfig FeatureSingletonAppLockConfig tid = - getFeature - "app_lock_status, app_lock_enforce, app_lock_inactivity_timeout_secs" - tid -getFeatureConfig FeatureSingletonFileSharingConfig tid = getFeature "file_sharing" tid -getFeatureConfig FeatureSingletonSelfDeletingMessagesConfig tid = - getFeature - "self_deleting_messages_status, self_deleting_messages_ttl" - tid -getFeatureConfig FeatureSingletonConferenceCallingConfig tid = - getFeature - "conference_calling_status, ttl(conference_calling_status), conference_calling_one_to_one" - tid -getFeatureConfig FeatureSingletonGuestLinksConfig tid = getFeature "guest_links_status" tid -getFeatureConfig FeatureSingletonSndFactorPasswordChallengeConfig tid = getFeature "snd_factor_password_challenge_status" tid -getFeatureConfig FeatureSingletonSearchVisibilityInboundConfig tid = getFeature "search_visibility_status" tid -getFeatureConfig FeatureSingletonMLSConfig tid = - getFeature - "mls_status, mls_default_protocol, mls_protocol_toggle_users, \ - \mls_allowed_ciphersuites, mls_default_ciphersuite, mls_supported_protocols" - tid -getFeatureConfig FeatureSingletonMlsE2EIdConfig tid = - getFeature - "mls_e2eid_status, mls_e2eid_grace_period, mls_e2eid_acme_discovery_url, \ - \mls_e2eid_crl_proxy, mls_e2eid_use_proxy_on_mobile" - tid -getFeatureConfig FeatureSingletonMlsMigration tid = - getFeature - "mls_migration_status, mls_migration_start_time, \ - \mls_migration_finalise_regardless_after" - tid -getFeatureConfig FeatureSingletonExposeInvitationURLsToTeamAdminConfig tid = - getFeature "expose_invitation_urls_to_team_admin" tid -getFeatureConfig FeatureSingletonOutlookCalIntegrationConfig tid = - getFeature "outlook_cal_integration_status" tid -getFeatureConfig FeatureSingletonEnforceFileDownloadLocationConfig tid = - getFeature - "enforce_file_download_location_status, enforce_file_download_location" - tid -getFeatureConfig FeatureSingletonLimitedEventFanoutConfig tid = - getFeature "limited_event_fanout_status" tid - -setFeatureConfig :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> WithStatusNoLock cfg -> m () -setFeatureConfig FeatureSingletonLegalholdConfig tid statusNoLock = setFeatureStatusC "legalhold_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonSSOConfig tid statusNoLock = setFeatureStatusC "sso_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonSearchVisibilityAvailableConfig tid statusNoLock = setFeatureStatusC "search_visibility_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonValidateSAMLEmailsConfig tid statusNoLock = setFeatureStatusC "validate_saml_emails" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonClassifiedDomainsConfig _tid _statusNoLock = pure () -setFeatureConfig FeatureSingletonDigitalSignaturesConfig tid statusNoLock = setFeatureStatusC "digital_signatures" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonAppLockConfig tid status = do - let enforce = applockEnforceAppLock (wssConfig status) - timeout = applockInactivityTimeoutSecs (wssConfig status) - - retry x5 $ write insert (params LocalQuorum (tid, wssStatus status, enforce, timeout)) - where - insert :: PrepQuery W (TeamId, FeatureStatus, EnforceAppLock, Int32) () - insert = - fromString $ - "insert into team_features (team_id, app_lock_status, app_lock_enforce,\ - \ app_lock_inactivity_timeout_secs) values (?, ?, ?, ?)" -setFeatureConfig FeatureSingletonFileSharingConfig tid statusNoLock = setFeatureStatusC "file_sharing" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonSelfDeletingMessagesConfig tid status = do - let statusValue = wssStatus status - timeout = sdmEnforcedTimeoutSeconds . wssConfig $ status - retry x5 $ write insert (params LocalQuorum (tid, statusValue, timeout)) - where - insert :: PrepQuery W (TeamId, FeatureStatus, Int32) () - insert = - "insert into team_features (team_id, self_deleting_messages_status,\ - \ self_deleting_messages_ttl) values (?, ?, ?)" -setFeatureConfig FeatureSingletonConferenceCallingConfig tid statusNoLock = do - retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery insertStatus (tid, statusNoLock.wssStatus) - addPrepQuery insertConfig (tid, statusNoLock.wssConfig.one2OneCalls) - where - insertStatus :: PrepQuery W (TeamId, FeatureStatus) () - insertStatus = "insert into team_features (team_id, conference_calling_status) values (?, ?)" - insertConfig :: PrepQuery W (TeamId, One2OneCalls) () - insertConfig = "insert into team_features (team_id, conference_calling_one_to_one) values (?, ?)" -setFeatureConfig FeatureSingletonGuestLinksConfig tid statusNoLock = setFeatureStatusC "guest_links_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonSndFactorPasswordChallengeConfig tid statusNoLock = - setFeatureStatusC "snd_factor_password_challenge_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonSearchVisibilityInboundConfig tid statusNoLock = setFeatureStatusC "search_visibility_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonMLSConfig tid statusNoLock = do - let status = wssStatus statusNoLock - let MLSConfig protocolToggleUsers defaultProtocol allowedCipherSuites defaultCipherSuite supportedProtocols = wssConfig statusNoLock - retry x5 $ - write - insert - ( params - LocalQuorum - ( tid, - status, - defaultProtocol, - C.Set protocolToggleUsers, - C.Set allowedCipherSuites, - defaultCipherSuite, - C.Set supportedProtocols - ) - ) - where - insert :: PrepQuery W (TeamId, FeatureStatus, ProtocolTag, C.Set UserId, C.Set CipherSuiteTag, CipherSuiteTag, C.Set ProtocolTag) () - insert = - "insert into team_features (team_id, mls_status, mls_default_protocol, \ - \mls_protocol_toggle_users, mls_allowed_ciphersuites, mls_default_ciphersuite, mls_supported_protocols) values (?, ?, ?, ?, ?, ?, ?)" -setFeatureConfig FeatureSingletonMlsE2EIdConfig tid status = do - let statusValue = wssStatus status - vex = verificationExpiration . wssConfig $ status - mUrl = acmeDiscoveryUrl . wssConfig $ status - mCrlProxy = crlProxy . wssConfig $ status - useProxy = useProxyOnMobile . wssConfig $ status - retry x5 $ write insert (params LocalQuorum (tid, statusValue, truncate vex, mUrl, mCrlProxy, useProxy)) - where - insert :: PrepQuery W (TeamId, FeatureStatus, Int32, Maybe HttpsUrl, Maybe HttpsUrl, Bool) () - insert = - "insert into team_features (team_id, mls_e2eid_status, mls_e2eid_grace_period, mls_e2eid_acme_discovery_url, mls_e2eid_crl_proxy, mls_e2eid_use_proxy_on_mobile) values (?, ?, ?, ?, ?, ?)" -setFeatureConfig FeatureSingletonMlsMigration tid status = do - let statusValue = wssStatus status - config = wssConfig status - - retry x5 $ write insert (params LocalQuorum (tid, statusValue, config.startTime, config.finaliseRegardlessAfter)) - where - insert :: PrepQuery W (TeamId, FeatureStatus, Maybe UTCTime, Maybe UTCTime) () - insert = - "insert into team_features (team_id, mls_migration_status, mls_migration_start_time, mls_migration_finalise_regardless_after) values (?, ?, ?, ?)" -setFeatureConfig FeatureSingletonExposeInvitationURLsToTeamAdminConfig tid statusNoLock = setFeatureStatusC "expose_invitation_urls_to_team_admin" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonOutlookCalIntegrationConfig tid statusNoLock = setFeatureStatusC "outlook_cal_integration_status" tid (wssStatus statusNoLock) -setFeatureConfig FeatureSingletonEnforceFileDownloadLocationConfig tid status = do - let statusValue = wssStatus status - config = wssConfig status - - retry x5 $ write insert (params LocalQuorum (tid, statusValue, config.enforcedDownloadLocation)) - where - insert :: PrepQuery W (TeamId, FeatureStatus, Maybe Text) () - insert = - "insert into team_features (team_id, enforce_file_download_location_status, enforce_file_download_location) values (?, ?, ?)" -setFeatureConfig FeatureSingletonLimitedEventFanoutConfig tid statusNoLock = - setFeatureStatusC "limited_event_fanout_status" tid (wssStatus statusNoLock) - -getFeatureLockStatus :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (Maybe LockStatus) -getFeatureLockStatus FeatureSingletonFileSharingConfig tid = getLockStatusC "file_sharing_lock_status" tid -getFeatureLockStatus FeatureSingletonSelfDeletingMessagesConfig tid = getLockStatusC "self_deleting_messages_lock_status" tid -getFeatureLockStatus FeatureSingletonGuestLinksConfig tid = getLockStatusC "guest_links_lock_status" tid -getFeatureLockStatus FeatureSingletonSndFactorPasswordChallengeConfig tid = getLockStatusC "snd_factor_password_challenge_lock_status" tid -getFeatureLockStatus FeatureSingletonMlsE2EIdConfig tid = getLockStatusC "mls_e2eid_lock_status" tid -getFeatureLockStatus FeatureSingletonMlsMigration tid = getLockStatusC "mls_migration_lock_status" tid -getFeatureLockStatus FeatureSingletonOutlookCalIntegrationConfig tid = getLockStatusC "outlook_cal_integration_lock_status" tid -getFeatureLockStatus FeatureSingletonMLSConfig tid = getLockStatusC "mls_lock_status" tid -getFeatureLockStatus FeatureSingletonEnforceFileDownloadLocationConfig tid = getLockStatusC "enforce_file_download_location_lock_status" tid -getFeatureLockStatus FeatureSingletonConferenceCallingConfig tid = getLockStatusC "conference_calling" tid -getFeatureLockStatus _ _ = pure Nothing - -setFeatureLockStatus :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> LockStatus -> m () -setFeatureLockStatus FeatureSingletonFileSharingConfig tid status = setLockStatusC "file_sharing_lock_status" tid status -setFeatureLockStatus FeatureSingletonSelfDeletingMessagesConfig tid status = setLockStatusC "self_deleting_messages_lock_status" tid status -setFeatureLockStatus FeatureSingletonGuestLinksConfig tid status = setLockStatusC "guest_links_lock_status" tid status -setFeatureLockStatus FeatureSingletonSndFactorPasswordChallengeConfig tid status = setLockStatusC "snd_factor_password_challenge_lock_status" tid status -setFeatureLockStatus FeatureSingletonMlsE2EIdConfig tid status = setLockStatusC "mls_e2eid_lock_status" tid status -setFeatureLockStatus FeatureSingletonMlsMigration tid status = setLockStatusC "mls_migration_lock_status" tid status -setFeatureLockStatus FeatureSingletonOutlookCalIntegrationConfig tid status = setLockStatusC "outlook_cal_integration_lock_status" tid status -setFeatureLockStatus FeatureSingletonMLSConfig tid status = setLockStatusC "mls_lock_status" tid status -setFeatureLockStatus FeatureSingletonEnforceFileDownloadLocationConfig tid status = setLockStatusC "enforce_file_download_location_lock_status" tid status -setFeatureLockStatus FeatureSingletonConferenceCallingConfig tid status = setLockStatusC "conference_calling" tid status -setFeatureLockStatus _ _tid _status = pure () - -getFeature :: - forall m cfg. - (MonadClient m, MakeFeature cfg) => - String -> - TeamId -> - m (DbFeature cfg) -getFeature columns tid = do - row <- retry x1 $ query1 select (params LocalQuorum (Identity tid)) - pure $ foldMap (mkFeature . toRowType) row - where - select :: PrepQuery R (Identity TeamId) (FeatureRow cfg) - select = - fromString $ - "select " - <> columns - <> " from team_features where team_id = ?" - -setFeatureStatusC :: - forall m. - (MonadClient m) => - String -> - TeamId -> - FeatureStatus -> - m () -setFeatureStatusC statusCol tid status = do - retry x5 $ write insert (params LocalQuorum (tid, status)) - where - insert :: PrepQuery W (TeamId, FeatureStatus) () - insert = - fromString $ - "insert into team_features (team_id, " <> statusCol <> ") values (?, ?)" - -getLockStatusC :: - forall m. - (MonadClient m) => - String -> - TeamId -> - m (Maybe LockStatus) -getLockStatusC lockStatusCol tid = do - let q = query1 select (params LocalQuorum (Identity tid)) - (>>= runIdentity) <$> retry x1 q - where - select :: PrepQuery R (Identity TeamId) (Identity (Maybe LockStatus)) - select = - fromString $ - "select " - <> lockStatusCol - <> " from team_features where team_id = ?" - -setLockStatusC :: - (MonadClient m) => - String -> - TeamId -> - LockStatus -> - m () -setLockStatusC col tid status = do - retry x5 $ write insert (params LocalQuorum (tid, status)) - where - insert :: PrepQuery W (TeamId, LockStatus) () - insert = - fromString $ - "insert into team_features (team_id, " <> col <> ") values (?, ?)" - getFeatureConfigMulti :: forall cfg m. (MonadClient m, MonadUnliftIO m) => @@ -314,3 +73,12 @@ getFeatureConfigMulti :: m [(TeamId, DbFeature cfg)] getFeatureConfigMulti proxy = pooledMapConcurrentlyN 8 (\tid -> getFeatureConfig proxy tid <&> (tid,)) + +getFeatureConfig :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (DbFeature cfg) +getFeatureConfig = $(featureCases [|fetchFeature|]) + +setFeatureConfig :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> LockableFeature cfg -> m () +setFeatureConfig = $(featureCases [|storeFeature|]) + +setFeatureLockStatus :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> Tagged cfg LockStatus -> m () +setFeatureLockStatus = $(featureCases [|storeFeatureLockStatus|]) diff --git a/services/galley/src/Galley/Effects/BrigAccess.hs b/services/galley/src/Galley/Effects/BrigAccess.hs index c825a3e7129..2e14a25c104 100644 --- a/services/galley/src/Galley/Effects/BrigAccess.hs +++ b/services/galley/src/Galley/Effects/BrigAccess.hs @@ -122,7 +122,7 @@ data BrigAccess m a where LastPrekey -> BrigAccess m (Either AuthenticationError ClientId) RemoveLegalHoldClientFromUser :: UserId -> BrigAccess m () - GetAccountConferenceCallingConfigClient :: UserId -> BrigAccess m (WithStatusNoLock ConferenceCallingConfig) + GetAccountConferenceCallingConfigClient :: UserId -> BrigAccess m (Feature ConferenceCallingConfig) GetLocalMLSClients :: Local UserId -> CipherSuiteTag -> BrigAccess m (Set ClientInfo) UpdateSearchVisibilityInbound :: Multi.TeamStatus SearchVisibilityInboundConfig -> diff --git a/services/galley/src/Galley/Effects/TeamFeatureStore.hs b/services/galley/src/Galley/Effects/TeamFeatureStore.hs index 0d24a1821af..d319d3515da 100644 --- a/services/galley/src/Galley/Effects/TeamFeatureStore.hs +++ b/services/galley/src/Galley/Effects/TeamFeatureStore.hs @@ -20,7 +20,6 @@ module Galley.Effects.TeamFeatureStore where import Data.Id -import Imports import Polysemy import Wire.API.Team.Feature @@ -37,12 +36,8 @@ data TeamFeatureStore m a where SetFeatureConfig :: FeatureSingleton cfg -> TeamId -> - WithStatusNoLock cfg -> + LockableFeature cfg -> TeamFeatureStore m () - GetFeatureLockStatus :: - FeatureSingleton cfg -> - TeamId -> - TeamFeatureStore m (Maybe LockStatus) SetFeatureLockStatus :: FeatureSingleton cfg -> TeamId -> @@ -50,6 +45,6 @@ data TeamFeatureStore m a where TeamFeatureStore m () GetAllFeatureConfigs :: TeamId -> - TeamFeatureStore m (AllFeatures DbFeatureWithLock) + TeamFeatureStore m (AllFeatures DbFeature) makeSem ''TeamFeatureStore diff --git a/services/galley/src/Galley/Intra/User.hs b/services/galley/src/Galley/Intra/User.hs index 5419b68ecea..6e6ef60859e 100644 --- a/services/galley/src/Galley/Intra/User.hs +++ b/services/galley/src/Galley/Intra/User.hs @@ -238,7 +238,7 @@ getRichInfoMultiUser = chunkify $ \uids -> do . expect2xx parseResponse (mkError status502 "server-error: could not parse response to `GET brig:/i/users/rich-info`") resp -getAccountConferenceCallingConfigClient :: (HasCallStack) => UserId -> App (WithStatusNoLock ConferenceCallingConfig) +getAccountConferenceCallingConfigClient :: (HasCallStack) => UserId -> App (Feature ConferenceCallingConfig) getAccountConferenceCallingConfigClient uid = runHereClientM (namedClient @IAPI.API @"get-account-conference-calling-config" uid) >>= handleServantResp diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index f05541d3a76..16fa23d97be 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedRecordDot #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-unused-local-binds #-} @@ -1176,7 +1177,7 @@ testGetCodeRejectedIfGuestLinksDisabled = do convId <- createConvWithGuestLink let checkGetCode expectedStatus = getConvCode owner convId !!! const expectedStatus === statusCode let setStatus tfStatus = - TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId (Public.WithStatusNoLock tfStatus Public.GuestLinksConfig Public.FeatureTTLUnlimited) !!! do + TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId (Public.Feature tfStatus Public.GuestLinksConfig) !!! do const 200 === statusCode checkGetCode 200 @@ -1192,7 +1193,7 @@ testPostCodeRejectedIfGuestLinksDisabled = do convId <- decodeConvId <$> postTeamConv teamId owner [] (Just "testConversation") [CodeAccess] (Just noGuestsAccess) Nothing let checkPostCode expectedStatus = postConvCode owner convId !!! statusCode === const expectedStatus let setStatus tfStatus = - TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId (Public.WithStatusNoLock tfStatus Public.GuestLinksConfig Public.FeatureTTLUnlimited) !!! do + TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId (Public.Feature tfStatus Public.GuestLinksConfig) !!! do const 200 === statusCode checkPostCode 201 @@ -1216,7 +1217,16 @@ testJoinTeamConvGuestLinksDisabled = do let checkFeatureStatus fstatus = Util.getTeamFeature @Public.GuestLinksConfig owner teamId !!! do const 200 === statusCode - const (Right (Public.withStatus fstatus Public.LockStatusUnlocked Public.GuestLinksConfig Public.FeatureTTLUnlimited)) === responseJsonEither + const + ( Right + ( Public.LockableFeature + { Public.status = fstatus, + Public.lockStatus = Public.LockStatusUnlocked, + Public.config = Public.GuestLinksConfig + } + ) + ) + === responseJsonEither -- guest can join if guest link feature is enabled checkFeatureStatus Public.FeatureStatusEnabled @@ -1229,7 +1239,7 @@ testJoinTeamConvGuestLinksDisabled = do postJoinCodeConv bob cCode !!! const 200 === statusCode -- disabled guest links feature - let disabled = Public.WithStatusNoLock Public.FeatureStatusDisabled Public.GuestLinksConfig Public.FeatureTTLUnlimited + let disabled = Public.Feature Public.FeatureStatusDisabled Public.GuestLinksConfig TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId disabled !!! do const 200 === statusCode @@ -1248,7 +1258,7 @@ testJoinTeamConvGuestLinksDisabled = do checkFeatureStatus Public.FeatureStatusDisabled -- after re-enabling, the old link is still valid - let enabled = Public.WithStatusNoLock Public.FeatureStatusEnabled Public.GuestLinksConfig Public.FeatureTTLUnlimited + let enabled = Public.Feature Public.FeatureStatusEnabled Public.GuestLinksConfig TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId enabled !!! do const 200 === statusCode getJoinCodeConv eve' (conversationKey cCode) (conversationCode cCode) !!! do @@ -1276,7 +1286,7 @@ testJoinNonTeamConvGuestLinksDisabled = do const 200 === statusCode -- for non-team conversations it still works if status is disabled for the team but not server wide - let tfStatus = Public.WithStatusNoLock Public.FeatureStatusDisabled Public.GuestLinksConfig Public.FeatureTTLUnlimited + let tfStatus = Public.Feature Public.FeatureStatusDisabled Public.GuestLinksConfig TeamFeatures.putTeamFeature @Public.GuestLinksConfig owner teamId tfStatus !!! do const 200 === statusCode @@ -1516,12 +1526,12 @@ getGuestLinksStatusFromForeignTeamConv = do localDomain <- viewFederationDomain galley <- viewGalley let setTeamStatus u tid tfStatus = - TeamFeatures.putTeamFeature @Public.GuestLinksConfig u tid (Public.WithStatusNoLock tfStatus Public.GuestLinksConfig Public.FeatureTTLUnlimited) !!! do + TeamFeatures.putTeamFeature @Public.GuestLinksConfig u tid (Public.Feature tfStatus Public.GuestLinksConfig) !!! do const 200 === statusCode let checkGuestLinksStatus u c s = getGuestLinkStatus galley u c !!! do const 200 === statusCode - const s === (Public.wsStatus . (responseJsonUnsafe @(Public.WithStatus Public.GuestLinksConfig))) + const s === ((.status) . (responseJsonUnsafe @(Public.LockableFeature Public.GuestLinksConfig))) let checkGetGuestLinksStatus s u c = getGuestLinkStatus galley u c !!! do const s === statusCode diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index 8aca27809c2..72bd68fa8e5 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -398,9 +398,8 @@ testEnableSSOPerTeam = do assertTeamActivate "create team" tid let check :: (HasCallStack) => String -> Public.FeatureStatus -> TestM () check msg enabledness = do - status :: Public.WithStatusNoLock Public.SSOConfig <- responseJsonUnsafe <$> (getSSOEnabledInternal tid (getSSOEnabledInternal tid TestM () putSSOEnabledInternalCheckNotImplemented = do g <- viewGalley @@ -409,7 +408,7 @@ testEnableSSOPerTeam = do <$> put ( g . paths ["i", "teams", toByteString' tid, "features", "sso"] - . json (Public.WithStatusNoLock Public.FeatureStatusDisabled Public.SSOConfig Public.FeatureTTLUnlimited) + . json (Public.Feature Public.FeatureStatusDisabled Public.SSOConfig) ) liftIO $ do assertEqual "bad status" status403 (Wai.code waierr) @@ -427,10 +426,9 @@ testEnableTeamSearchVisibilityPerTeam = do (tid, owner, member : _) <- Util.createBindingTeamWithMembers 2 let check :: String -> Public.FeatureStatus -> TestM () check msg enabledness = do - status :: Public.WithStatusNoLock Public.SearchVisibilityAvailableConfig <- responseJsonUnsafe <$> (Util.getTeamFeatureInternal @Public.SearchVisibilityAvailableConfig tid (Util.getTeamFeatureInternal @Public.SearchVisibilityAvailableConfig tid Public.FeatureStatus -> TestM () setTeamSndFactorPasswordChallenge tid status = do g <- viewGalley - let js = RequestBodyLBS $ encode $ Public.WithStatusNoLock status Public.SndFactorPasswordChallengeConfig Public.FeatureTTLUnlimited + let js = RequestBodyLBS $ encode $ Public.Feature status Public.SndFactorPasswordChallengeConfig put (g . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode getVerificationCode :: UserId -> Public.VerificationAction -> TestM Code.Value @@ -1745,7 +1743,7 @@ getSSOEnabledInternal = Util.getTeamFeatureInternal @Public.SSOConfig putSSOEnabledInternal :: (HasCallStack) => TeamId -> Public.FeatureStatus -> TestM () putSSOEnabledInternal tid statusValue = - void $ Util.putTeamFeatureInternal @Public.SSOConfig expect2xx tid (Public.WithStatusNoLock statusValue Public.SSOConfig Public.FeatureTTLUnlimited) + void $ Util.putTeamFeatureInternal @Public.SSOConfig expect2xx tid (Public.Feature statusValue Public.SSOConfig) getSearchVisibility :: (HasCallStack) => (Request -> Request) -> UserId -> TeamId -> (MonadHttp m) => m ResponseLBS getSearchVisibility g uid tid = do diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index 0ed8319d99e..923a25b92e7 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -502,14 +502,12 @@ testEnablePerTeam = do member <- randomUser addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing do - status :: Public.WithStatusNoLock Public.LegalholdConfig <- responseJsonUnsafe <$> (getEnabled tid (getEnabled tid (getEnabled tid (getEnabled tid do grantConsent tid member requestLegalHoldDevice owner member tid !!! const 201 === statusCode @@ -519,9 +517,8 @@ testEnablePerTeam = do liftIO $ assertEqual "User legal hold status should be enabled" UserLegalHoldEnabled status do putEnabled tid Public.FeatureStatusDisabled -- disable again - status :: Public.WithStatusNoLock Public.LegalholdConfig <- responseJsonUnsafe <$> (getEnabled tid (getEnabled tid (getEnabled tid (getEnabled tid (getEnabled tid (getEnabled tid UserId -> TeamId -> NewLegalHoldService -> TestM ResponseLBS diff --git a/services/galley/test/integration/API/Util/TeamFeature.hs b/services/galley/test/integration/API/Util/TeamFeature.hs index 630f030e3f2..873eb4e51ea 100644 --- a/services/galley/test/integration/API/Util/TeamFeature.hs +++ b/services/galley/test/integration/API/Util/TeamFeature.hs @@ -50,7 +50,7 @@ putTeamSearchVisibilityAvailableInternal tid statusValue = @Public.SearchVisibilityAvailableConfig expect2xx tid - (Public.WithStatusNoLock statusValue Public.SearchVisibilityAvailableConfig Public.FeatureTTLUnlimited) + (Public.Feature statusValue Public.SearchVisibilityAvailableConfig) putTeamFeatureInternal :: forall cfg m. @@ -59,11 +59,11 @@ putTeamFeatureInternal :: MonadHttp m, HasCallStack, KnownSymbol (Public.FeatureSymbol cfg), - ToJSON (Public.WithStatusNoLock cfg) + ToJSON (Public.Feature cfg) ) => (Request -> Request) -> TeamId -> - Public.WithStatusNoLock cfg -> + Public.Feature cfg -> m ResponseLBS putTeamFeatureInternal reqmod tid status = do galley <- viewGalley @@ -77,11 +77,11 @@ putTeamFeature :: forall cfg. ( HasCallStack, KnownSymbol (Public.FeatureSymbol cfg), - ToJSON (Public.WithStatusNoLock cfg) + ToJSON (Public.Feature cfg) ) => UserId -> TeamId -> - Public.WithStatusNoLock cfg -> + Public.Feature cfg -> TestM ResponseLBS putTeamFeature uid tid status = do galley <- viewGalley diff --git a/services/spar/src/Spar/Intra/Galley.hs b/services/spar/src/Spar/Intra/Galley.hs index 03dc835df67..8e9b508c947 100644 --- a/services/spar/src/Spar/Intra/Galley.hs +++ b/services/spar/src/Spar/Intra/Galley.hs @@ -99,8 +99,8 @@ assertSSOEnabled tid = do . paths ["i", "teams", toByteString' tid, "features", "sso"] unless (statusCode resp == 200) $ rethrow "galley" resp - ws :: WithStatus SSOConfig <- parseResponse "galley" resp - unless (wsStatus ws == FeatureStatusEnabled) $ + ws :: LockableFeature SSOConfig <- parseResponse "galley" resp + unless (ws.status == FeatureStatusEnabled) $ throwSpar SparSSODisabled isEmailValidationEnabledTeam :: (HasCallStack, MonadSparToGalley m) => TeamId -> m Bool @@ -108,7 +108,7 @@ isEmailValidationEnabledTeam tid = do resp <- call $ method GET . paths ["i", "teams", toByteString' tid, "features", "validateSAMLemails"] pure ( statusCode resp == 200 - && ( (wsStatus <$> responseJsonMaybe @(WithStatus ValidateSAMLEmailsConfig) resp) + && ( ((.status) <$> responseJsonMaybe @(LockableFeature ValidateSAMLEmailsConfig) resp) == Just FeatureStatusEnabled ) ) diff --git a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs index 69a064400cf..6f7983a10b9 100644 --- a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs @@ -151,7 +151,7 @@ unlockFeature galley tid = setSndFactorPasswordChallengeStatus :: GalleyReq -> TeamId -> Public.FeatureStatus -> TestSpar () setSndFactorPasswordChallengeStatus galley tid status = do - let js = RequestBodyLBS $ encode $ Public.WithStatusNoLock @Public.SndFactorPasswordChallengeConfig status Public.SndFactorPasswordChallengeConfig Public.FeatureTTLUnlimited + let js = RequestBodyLBS $ encode $ Public.Feature @Public.SndFactorPasswordChallengeConfig status Public.SndFactorPasswordChallengeConfig call $ put (galley . paths ["i", "teams", toByteString' tid, "features", featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 30e18ed8cfa..250ef14efd4 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -385,7 +385,7 @@ putSSOEnabledInternal gly tid enabled = do void . put $ gly . paths ["i", "teams", toByteString' tid, "features", "sso"] - . json (WithStatusNoLock @SSOConfig enabled SSOConfig FeatureTTLUnlimited) + . json (Feature enabled SSOConfig) . expect2xx -- | cloned from `/services/brig/test/integration/API/Team/Util.hs`. diff --git a/services/spar/test-integration/Util/Email.hs b/services/spar/test-integration/Util/Email.hs index 0a1910127fe..3d639e0c3b9 100644 --- a/services/spar/test-integration/Util/Email.hs +++ b/services/spar/test-integration/Util/Email.hs @@ -110,6 +110,6 @@ activate brig (k, c) = setSamlEmailValidation :: (HasCallStack) => TeamId -> Feature.FeatureStatus -> TestSpar () setSamlEmailValidation tid status = do galley <- view teGalley - let req = put $ galley . paths p . json (Feature.WithStatusNoLock @Feature.ValidateSAMLEmailsConfig status Feature.ValidateSAMLEmailsConfig Feature.FeatureTTLUnlimited) + let req = put $ galley . paths p . json (Feature.Feature @Feature.ValidateSAMLEmailsConfig status Feature.ValidateSAMLEmailsConfig) p = ["/i/teams", toByteString' tid, "features", Feature.featureNameBS @Feature.ValidateSAMLEmailsConfig] call req !!! const 200 === statusCode diff --git a/tools/stern/default.nix b/tools/stern/default.nix index cde1f4ba46a..18246b4fc52 100644 --- a/tools/stern/default.nix +++ b/tools/stern/default.nix @@ -11,6 +11,7 @@ , bytestring-conversion , containers , cookie +, data-default , errors , exceptions , extended @@ -100,6 +101,7 @@ mkDerivation { bytestring-conversion containers cookie + data-default exceptions extra HsOpenSSL diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index b95aa15c989..611b366ca08 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -70,7 +70,7 @@ import Wire.API.Routes.Internal.Brig.Connection (ConnectionStatus) import Wire.API.Routes.Internal.Brig.EJPD qualified as EJPD import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Named (Named (Named)) -import Wire.API.Team.Feature hiding (setStatus) +import Wire.API.Team.Feature import Wire.API.Team.SearchVisibility import Wire.API.User import Wire.API.User.Search @@ -314,16 +314,16 @@ mkFeatureGetRoute :: Typeable cfg ) => TeamId -> - Handler (WithStatus cfg) + Handler (LockableFeature cfg) mkFeatureGetRoute = Intra.getTeamFeatureFlag @cfg mkFeaturePutRoute :: forall cfg. ( KnownSymbol (FeatureSymbol cfg), - ToJSON (WithStatusNoLock cfg) + ToJSON (Feature cfg) ) => TeamId -> - WithStatusNoLock cfg -> + Feature cfg -> Handler NoContent mkFeaturePutRoute tid payload = NoContent <$ Intra.setTeamFeatureFlag @cfg tid payload @@ -331,8 +331,8 @@ type MkFeaturePutConstraints cfg = ( IsFeatureConfig cfg, KnownSymbol (FeatureSymbol cfg), ToSchema cfg, - FromJSON (WithStatusNoLock cfg), - ToJSON (WithStatusNoLock cfg), + FromJSON (Feature cfg), + ToJSON (Feature cfg), Typeable cfg ) @@ -350,8 +350,8 @@ mkFeaturePutRouteTrivialConfigWithTTL tid status = mkFeaturePutRouteTrivialConfi mkFeaturePutRouteTrivialConfig :: forall cfg. (MkFeaturePutConstraints cfg) => TeamId -> FeatureStatus -> Maybe FeatureTTLDays -> Handler NoContent -mkFeaturePutRouteTrivialConfig tid status (fmap convertFeatureTTLDaysToSeconds -> ttl) = do - let patch = wsPatch (Just status) Nothing Nothing ttl +mkFeaturePutRouteTrivialConfig tid status _ = do + let patch = LockableFeaturePatch (Just status) Nothing Nothing NoContent <$ Intra.patchTeamFeatureFlag @cfg tid patch getSearchVisibility :: TeamId -> Handler TeamSearchVisibilityView diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index 93b99fce9f1..f0472676e47 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -484,7 +484,7 @@ type MkFeatureGetRoute (feature :: Type) = :> Capture "tid" TeamId :> "features" :> FeatureSymbol feature - :> Get '[JSON] (WithStatus feature) + :> Get '[JSON] (LockableFeature feature) type MkFeaturePutRouteNoTTL (feature :: Type) = Summary "Disable / enable status for a given feature / team" @@ -522,5 +522,5 @@ type MkFeaturePutRoute (feature :: Type) = :> Capture "tid" TeamId :> "features" :> FeatureSymbol feature - :> ReqBody '[JSON] (WithStatusNoLock feature) + :> ReqBody '[JSON] (Feature feature) :> Put '[JSON] NoContent diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index f3350ee7bcc..14e2c62e1fc 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -504,12 +504,12 @@ setBlacklistStatus status email = do getTeamFeatureFlag :: forall cfg. - ( Typeable (Public.WithStatus cfg), - FromJSON (Public.WithStatus cfg), + ( Typeable (Public.LockableFeature cfg), + FromJSON (Public.LockableFeature cfg), KnownSymbol (Public.FeatureSymbol cfg) ) => TeamId -> - Handler (Public.WithStatus cfg) + Handler (Public.LockableFeature cfg) getTeamFeatureFlag tid = do info $ msg "Getting team feature status" gly <- view galley @@ -518,21 +518,20 @@ getTeamFeatureFlag tid = do . Bilge.paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] resp <- catchRpcErrors $ rpc' "galley" gly req case Bilge.statusCode resp of - 200 -> pure $ responseJsonUnsafe @(Public.WithStatus cfg) resp + 200 -> pure $ responseJsonUnsafe @(Public.LockableFeature cfg) resp 404 -> throwE (mkError status404 "bad-upstream" "team doesnt exist") _ -> throwE (mkError status502 "bad-upstream" (errorMessage resp)) setTeamFeatureFlag :: forall cfg. - ( ToJSON (Public.WithStatusNoLock cfg), + ( ToJSON (Public.Feature cfg), KnownSymbol (Public.FeatureSymbol cfg) ) => TeamId -> - Public.WithStatusNoLock cfg -> + Public.Feature cfg -> Handler () setTeamFeatureFlag tid status = do info $ msg "Setting team feature status" - checkDaysLimit (wssTTL status) galleyRpc $ method PUT . Bilge.paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] @@ -541,15 +540,14 @@ setTeamFeatureFlag tid status = do patchTeamFeatureFlag :: forall cfg. - ( ToJSON (Public.WithStatusPatch cfg), + ( ToJSON (Public.LockableFeaturePatch cfg), KnownSymbol (Public.FeatureSymbol cfg) ) => TeamId -> - Public.WithStatusPatch cfg -> + Public.LockableFeaturePatch cfg -> Handler () patchTeamFeatureFlag tid patch = do info $ msg "Patching team feature status" - for_ (wspTTL patch) $ \ttl -> checkDaysLimit ttl galleyRpc $ method PATCH . Bilge.paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] @@ -566,26 +564,6 @@ galleyRpc req = do 403 -> throwE (mkError status403 "bad-upstream" "config cannot be changed") _ -> throwE (mkError status502 "bad-upstream" (errorMessage resp)) -checkDaysLimit :: FeatureTTL -> Handler () -checkDaysLimit = \case - FeatureTTLUnlimited -> pure () - FeatureTTLSeconds ((`div` (60 * 60 * 24)) -> days) -> do - unless (days <= daysLimit) $ do - throwE - ( mkError - status400 - "bad-data" - ( LT.pack $ - "ttl limit is " - <> show daysLimit - <> " days; I got " - <> show days - <> "." - ) - ) - where - daysLimit = 2000 - setTeamFeatureLockStatus :: forall cfg. ( KnownSymbol (Public.FeatureSymbol cfg) diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index 01b6639617a..ba50b7edb6b 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -251,6 +251,7 @@ executable stern-integration , bytestring-conversion , containers , cookie + , data-default , exceptions , extra >=1.3 , HsOpenSSL diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index b35aadcf554..de4d3917d9c 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedRecordDot #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} +{-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-redundant-constraints #-} -{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} -- This file is part of the Wire Server implementation. -- @@ -30,6 +30,7 @@ import Control.Lens hiding ((.=)) import Data.Aeson (ToJSON, Value) import Data.Aeson qualified as A import Data.ByteString.Conversion +import Data.Default import Data.Handle import Data.Id import Data.Range (unsafeRange) @@ -105,8 +106,8 @@ tests s = -- - `POST /teams/:tid/billing` ] -defConfCalling :: WithStatus ConferenceCallingConfig -defConfCalling = setStatus FeatureStatusDisabled defFeatureStatus +defConfCalling :: LockableFeature ConferenceCallingConfig +defConfCalling = def {status = FeatureStatusDisabled} testRudSsoDomainRedirect :: TestM () testRudSsoDomainRedirect = do @@ -261,7 +262,7 @@ testLegalholdConfig :: TestM () testLegalholdConfig = do (_, tid, _) <- createTeamWithNMembers 10 cfg <- getFeatureConfig @LegalholdConfig tid - liftIO $ cfg @?= defFeatureStatus @LegalholdConfig + liftIO $ cfg @?= def -- Legal hold is enabled for teams via server config and cannot be changed here putFeatureStatus @LegalholdConfig tid FeatureStatusEnabled Nothing !!! const 403 === statusCode @@ -278,11 +279,11 @@ testFeatureConfig :: testFeatureConfig = do (_, tid, _) <- createTeamWithNMembers 10 cfg <- getFeatureConfig @cfg tid - liftIO $ cfg @?= defFeatureStatus @cfg - let newStatus = if wsStatus cfg == FeatureStatusEnabled then FeatureStatusDisabled else FeatureStatusEnabled - putFeatureConfig @cfg tid (setStatus newStatus cfg) !!! const 200 === statusCode + liftIO $ cfg @?= def + let newStatus = if cfg.status == FeatureStatusEnabled then FeatureStatusDisabled else FeatureStatusEnabled + putFeatureConfig @cfg tid cfg {status = newStatus} !!! const 200 === statusCode cfg' <- getFeatureConfig @cfg tid - liftIO $ wsStatus cfg' @?= newStatus + liftIO $ cfg'.status @?= newStatus testGetFeatureConfig :: forall cfg. @@ -298,7 +299,7 @@ testGetFeatureConfig :: testGetFeatureConfig mDef = do (_, tid, _) <- createTeamWithNMembers 10 cfg <- getFeatureConfig @cfg tid - liftIO $ wsStatus cfg @?= fromMaybe (wsStatus $ defFeatureStatus @cfg) mDef + liftIO $ cfg.status @?= fromMaybe (def @(Feature cfg)).status mDef testFeatureStatus :: forall cfg. @@ -310,7 +311,7 @@ testFeatureStatus :: Show cfg ) => TestM () -testFeatureStatus = testFeatureStatusOptTtl (defFeatureStatus @cfg) Nothing +testFeatureStatus = testFeatureStatusOptTtl @cfg def Nothing testFeatureStatusOptTtl :: forall cfg. @@ -321,18 +322,18 @@ testFeatureStatusOptTtl :: Eq cfg, Show cfg ) => - WithStatus cfg -> + LockableFeature cfg -> Maybe FeatureTTL -> TestM () testFeatureStatusOptTtl defValue mTtl = do (_, tid, _) <- createTeamWithNMembers 10 cfg <- getFeatureConfig @cfg tid liftIO $ cfg @?= defValue - when (wsLockStatus cfg == LockStatusLocked) $ unlockFeature @cfg tid - let newStatus = if wsStatus cfg == FeatureStatusEnabled then FeatureStatusDisabled else FeatureStatusEnabled + when (cfg.lockStatus == LockStatusLocked) $ unlockFeature @cfg tid + let newStatus = if cfg.status == FeatureStatusEnabled then FeatureStatusDisabled else FeatureStatusEnabled putFeatureStatus @cfg tid newStatus mTtl !!! const 200 === statusCode cfg' <- getFeatureConfig @cfg tid - liftIO $ wsStatus cfg' @?= newStatus + liftIO $ cfg'.status @?= newStatus testFeatureStatusWithLock :: forall cfg. @@ -348,31 +349,31 @@ testFeatureStatusWithLock = do let mTtl = Nothing -- this function can become a variant of `testFeatureStatusOptTtl` if we need one. (_, tid, _) <- createTeamWithNMembers 10 getFeatureConfig @cfg tid >>= \cfg -> liftIO $ do - cfg @?= defFeatureStatus @cfg + cfg @?= def -- if either of these two lines fails, it's probably because the default is surprising. -- in that case, make the text more flexible. - wsLockStatus cfg @?= LockStatusLocked - wsStatus cfg @?= FeatureStatusDisabled + cfg.lockStatus @?= LockStatusLocked + cfg.status @?= FeatureStatusDisabled void $ putFeatureStatusLock @cfg tid LockStatusUnlocked mTtl getFeatureConfig @cfg tid >>= \cfg -> liftIO $ do - wsLockStatus cfg @?= LockStatusUnlocked - wsStatus cfg @?= FeatureStatusDisabled + cfg.lockStatus @?= LockStatusUnlocked + cfg.status @?= FeatureStatusDisabled void $ putFeatureStatus @cfg tid FeatureStatusEnabled Nothing getFeatureConfig @cfg tid >>= \cfg -> liftIO $ do - wsLockStatus cfg @?= LockStatusUnlocked - wsStatus cfg @?= FeatureStatusEnabled + cfg.lockStatus @?= LockStatusUnlocked + cfg.status @?= FeatureStatusEnabled void $ putFeatureStatusLock @cfg tid LockStatusLocked mTtl getFeatureConfig @cfg tid >>= \cfg -> liftIO $ do - wsLockStatus cfg @?= LockStatusLocked - wsStatus cfg @?= FeatureStatusDisabled + cfg.lockStatus @?= LockStatusLocked + cfg.status @?= FeatureStatusDisabled void $ putFeatureStatusLock @cfg tid LockStatusUnlocked mTtl getFeatureConfig @cfg tid >>= \cfg -> liftIO $ do - wsLockStatus cfg @?= LockStatusUnlocked - wsStatus cfg @?= FeatureStatusEnabled + cfg.lockStatus @?= LockStatusUnlocked + cfg.status @?= FeatureStatusEnabled testGetConsentLog :: TestM () testGetConsentLog = do @@ -614,7 +615,7 @@ getFeatureConfig :: IsFeatureConfig cfg ) => TeamId -> - TestM (WithStatus cfg) + TestM (LockableFeature cfg) getFeatureConfig tid = do s <- view tsStern r <- get (s . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] . expect2xx) @@ -669,10 +670,10 @@ putFeatureConfig :: ToSchema cfg, Typeable cfg, IsFeatureConfig cfg, - ToJSON (WithStatus cfg) + ToJSON (LockableFeature cfg) ) => TeamId -> - WithStatus cfg -> + LockableFeature cfg -> TestM ResponseLBS putFeatureConfig tid cfg = do s <- view tsStern @@ -706,7 +707,7 @@ unlockFeature :: ToSchema cfg, Typeable cfg, IsFeatureConfig cfg, - ToJSON (WithStatus cfg) + ToJSON (LockableFeature cfg) ) => TeamId -> TestM () From 04f0047f9e389ed6e907b01ef8dd186e126faa1e Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:00:28 +0200 Subject: [PATCH 035/136] add the `todo` function and the `TODO` pattern (#4198) * [feat] add the 'todo' function and the 'TODO' pattern --- changelog.d/5-internal/todo | 1 + libs/imports/src/Imports.hs | 130 +++++++++++++++++++++++++++++++++--- 2 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 changelog.d/5-internal/todo diff --git a/changelog.d/5-internal/todo b/changelog.d/5-internal/todo new file mode 100644 index 00000000000..6326d872e23 --- /dev/null +++ b/changelog.d/5-internal/todo @@ -0,0 +1 @@ +add the TODO pattern and the todo function to Imports diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index 94ad9d1dc11..9f283614362 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -112,12 +114,18 @@ module Imports -- * Functor (<$$>), (<$$$>), + + -- * development + todo, + pattern TODO, + TodoException (..), ) where -- common in some libs import Control.Applicative hiding (empty, many, optional, some) import Control.DeepSeq (NFData (..), deepseq) +import Control.Exception import Control.Monad hiding (forM, forM_, mapM, mapM_, msum, sequence, sequence_) import Control.Monad.Extra (unlessM, whenM) import Control.Monad.IO.Unlift @@ -146,15 +154,12 @@ import Data.Functor.Identity import Data.HashMap.Strict (HashMap) import Data.HashSet (HashSet) import Data.Int --- 'insert' and 'delete' are common in database modules +import Data.Kind (Type) import Data.List hiding (delete, insert, singleton) --- Lazy and strict versions are the same import Data.Map (Map) import Data.Maybe --- First and Last are going to be deprecated. Use Semigroup instead import Data.Monoid hiding (First (..), Last (..)) import Data.Ord --- conflicts with Options.Applicative.Option (should we care?) import Data.Semigroup hiding (diff) import Data.Set (Set) import Data.String @@ -164,21 +169,18 @@ import Data.Traversable import Data.Tuple import Data.Void import Data.Word +import GHC.Exts import GHC.Generics (Generic) import GHC.Stack (HasCallStack) import Text.Read (readEither, readMaybe) import UnliftIO.Concurrent --- Permissions is common in Galley import UnliftIO.Directory hiding (Permissions) import UnliftIO.Environment import UnliftIO.Exception --- Handle is hidden because it's common in Brig import UnliftIO.IO hiding (Handle, getMonotonicTime) import UnliftIO.IORef import UnliftIO.MVar import UnliftIO.STM --- Explicitly saying what to import because some things from Prelude clash --- with e.g. UnliftIO modules import Prelude ( Bounded (..), Double, @@ -256,6 +258,118 @@ writeFile = fmap liftIO . P.writeFile appendFile :: (MonadIO m) => FilePath -> String -> m () appendFile = fmap liftIO . P.appendFile +---------------------------------------------------------------------- +-- placeholders + +-- | 'todo' indicates unfinished code. +-- +-- It is to be used whenever you want to indicate that you are missing a part of +-- the implementation and want to fill that in later. +-- +-- This takes a middle ground between other alternatives - unlike typed holes it doesn't cause +-- a /compile time error/, but in contrast to 'GHC.Err.error' and 'GHC.Err.undefined', it does emit +-- a /warning at compilation time/. +-- +-- Similarly to all of 'GHC.Err.undefined', 'GHC.Err.error' and typed holes, this /will throw an error/ +-- if it is /evaluated at runtime/. This error can only be caught in 'System.IO.IO'. +-- +-- This is intended to /never/ stay in code but exists purely for signifying + +-- "work in progress" code. +-- +-- To make the emitted warning a compile error instead (e.g. for use in CI), add +-- the @-Werror=x-todo@ flag to your @OPTIONS_GHC@. +-- +-- ==== __Examples__ +-- +-- @ +-- superComplexFunction :: 'Data.Maybe.Maybe' a -> 'System.IO.IO' 'Data.Int.Int' +-- -- we already know how to implement this in the 'Data.Maybe.Nothing' case +-- superComplexFunction 'Data.Maybe.Nothing' = 'Control.Applicative.pure' 42 +-- -- but the 'Data.Maybe.Just' case is super complicated, so we leave it as 'todo' for now +-- superComplexFunction ('Data.Maybe.Just' a) = 'todo' +-- @ +-- +-- ==== __Representation Polymorphism__ +-- +-- 'todo', in contrast to 'TODO', is fully representation polymorphic +-- +-- @since base-4.21.0.0 +todo :: forall {r :: RuntimeRep} (a :: TYPE r). (HasCallStack) => a +todo = throw TodoException +{-# WARNING todo "'todo' left in code" #-} + +-- FUTUREWORK(mangoiv): should be: WARNING in "x-todo" from ghc 9.8 on + +-- | 'TODO' indicates unfinished code or an unimplemented pattern match +-- +-- You can use this in most positions where you could pass 'todo', but it /also/ can be used in +-- the position of a pattern to indicate that there are cases you have not yet considered. +-- +-- This pattern synonym is marked @COMPLETE@, implying that every match after matching on 'TODO' +-- will /emit a redundant pattern match warning/. Adding new options to your datatype, similarly +-- to how wildcard patterns (patterns starting with an underscore) work, will /not cause any warnings or errors/. +-- +-- ==== __Examples__ +-- +-- Since the pattern match is strict, even if the branch itself does not evaluate to bottom, matching on +-- 'TODO' will. +-- +-- @ +-- >>> x = [] +-- >>> case x of +-- ... (x : _) -> x +-- ... 'TODO' -> 42 +-- *** Exception: Develop.Placeholder.todo: not yet implemented +-- @ +-- +-- As usual, this behaviour can be reversed by using a @~@ in front of 'TODO' in pattern position. +-- +-- @ +-- >>> x = [] +-- >>> case x of +-- ... (x : _) -> x +-- ... ~'TODO' -> 42 +-- 42 +-- @ +-- +-- In most situations, 'TODO' can be used just like 'todo', where the above is equivalent to the below +-- +-- @ +-- >>> y :: 'Data.Int.Int' = 'todo' +-- >>> x :: 'Data.Int.Int' = 'TODO' +-- @ +-- +-- +-- ==== __Representation Polymorphism__ +-- +-- Mind that pattern synonyms may not be representation polymorphic, hence, if you need something +-- that can be used with some kind other than 'Data.Kind.Type', you have to use 'todo'. For example, +-- 'TODO' cannot stand instead of a pattern match on an @'GHC.Exts.Int#' :: 'TYPE' 'GHC.Exts.IntRep'@ +-- or as a placeholder for a @'GHC.Exts.ByteArray#' :: 'GHC.Exts.UnliftedType'@ +-- +-- @since base-4.21.0.0 +pattern TODO :: forall (a :: Type). (HasCallStack) => forall. a +pattern TODO <- (throw TodoException -> !_unused) + where + TODO = throw TodoException +{-# WARNING TODO "'TODO' left in code" #-} + +-- FUTUREWORK(mangoiv): should be WARNING in "x-todo" from ghc 9.8 on + +{-# COMPLETE TODO #-} + +-- | This is the 'Exception' thrown by 'todo'. +-- +-- This exception occurring indicates a bug as 'todo' should /never/ remain in code. +-- +-- @since base-4.21.0.0 +data TodoException = TodoException + deriving stock (Eq, Show) + +instance Exception TodoException where + displayException _ = "Develop.Placeholder.todo: not yet implemented" + ---------------------------------------------------------------------- -- Functor From 91755c5bffa6a7c188128faf2dc93c0c06ac249f Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:18:01 +0200 Subject: [PATCH 036/136] [fix] update treefmt s.t. it doesn't segfault anymore (#4199) --- nix/sources.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nix/sources.json b/nix/sources.json index b69da9490f3..05e854e4f6a 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -5,10 +5,10 @@ "homepage": "https://github.com/NixOS/nixpkgs", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f3834de3782b82bfc666abf664f946d0e7d1f116", - "sha256": "0kzp4d4hbsc968wavwyh31lzipd4cv7wvnca167y21l5rb1kx9az", + "rev": "154bcb95ad51bc257c2ce4043a725de6ca700ef6", + "sha256": "0gv8wgjqldh9nr3lvpjas7sk0ffyahmvfrz5g4wd8l2r15wyk67f", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/f3834de3782b82bfc666abf664f946d0e7d1f116.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/154bcb95ad51bc257c2ce4043a725de6ca700ef6.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } From 035a17d7c6e57288c2548207e80d4f01eea50ae0 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 12 Aug 2024 14:58:06 +0200 Subject: [PATCH 037/136] Remove helm charts k8ssandra-test-cluster and smallstep-accomp (#4202) --- charts/k8ssandra-test-cluster/.helmignore | 23 -- charts/k8ssandra-test-cluster/Chart.yaml | 9 - charts/k8ssandra-test-cluster/README.md | 89 -------- .../templates/check-cluster-job.yaml | 33 --- .../templates/jks-store-pass.yaml | 9 - .../templates/k8ssandra-cluster.yaml | 62 ----- .../templates/tls-certificate-bundle.yaml | 24 -- .../templates/tls-certificate.yaml | 44 ---- .../templates/tls-issuer.yaml | 9 - charts/k8ssandra-test-cluster/values.yaml | 40 ---- charts/smallstep-accomp/Chart.yaml | 4 - charts/smallstep-accomp/README.md | 113 ---------- charts/smallstep-accomp/requirements.yaml | 4 - .../smallstep-accomp/templates/_helpers.tpl | 3 - .../templates/ca-password.yaml | 12 - charts/smallstep-accomp/templates/certs.yaml | 13 -- .../smallstep-accomp/templates/secrets.yaml | 14 -- .../templates/server-block-configmap.yaml | 39 ---- .../templates/step-config.yaml | 9 - charts/smallstep-accomp/values.yaml | 212 ------------------ 20 files changed, 765 deletions(-) delete mode 100644 charts/k8ssandra-test-cluster/.helmignore delete mode 100644 charts/k8ssandra-test-cluster/Chart.yaml delete mode 100644 charts/k8ssandra-test-cluster/README.md delete mode 100644 charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml delete mode 100644 charts/k8ssandra-test-cluster/templates/jks-store-pass.yaml delete mode 100644 charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml delete mode 100644 charts/k8ssandra-test-cluster/templates/tls-certificate-bundle.yaml delete mode 100644 charts/k8ssandra-test-cluster/templates/tls-certificate.yaml delete mode 100644 charts/k8ssandra-test-cluster/templates/tls-issuer.yaml delete mode 100644 charts/k8ssandra-test-cluster/values.yaml delete mode 100644 charts/smallstep-accomp/Chart.yaml delete mode 100644 charts/smallstep-accomp/README.md delete mode 100644 charts/smallstep-accomp/requirements.yaml delete mode 100644 charts/smallstep-accomp/templates/_helpers.tpl delete mode 100644 charts/smallstep-accomp/templates/ca-password.yaml delete mode 100644 charts/smallstep-accomp/templates/certs.yaml delete mode 100644 charts/smallstep-accomp/templates/secrets.yaml delete mode 100644 charts/smallstep-accomp/templates/server-block-configmap.yaml delete mode 100644 charts/smallstep-accomp/templates/step-config.yaml delete mode 100644 charts/smallstep-accomp/values.yaml diff --git a/charts/k8ssandra-test-cluster/.helmignore b/charts/k8ssandra-test-cluster/.helmignore deleted file mode 100644 index 0e8a0eb36f4..00000000000 --- a/charts/k8ssandra-test-cluster/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/charts/k8ssandra-test-cluster/Chart.yaml b/charts/k8ssandra-test-cluster/Chart.yaml deleted file mode 100644 index b67746d3075..00000000000 --- a/charts/k8ssandra-test-cluster/Chart.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v2 -name: k8ssandra-test-cluster -description: K8ssandra (Cassandra cluster) K8ssandraCluster object for wire test servers. (This does not install K8ssandra itself!) - -type: application - -version: 0.1.0 - -appVersion: "0.39.2" diff --git a/charts/k8ssandra-test-cluster/README.md b/charts/k8ssandra-test-cluster/README.md deleted file mode 100644 index 865183c1566..00000000000 --- a/charts/k8ssandra-test-cluster/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# k8ssandra-test-cluster Helm chart - -`k8ssandra-test-cluster` provides a `K8ssandraCluster` object to create a -*Cassandra* database with -[`k8ssandra-operator`](https://artifacthub.io/packages/helm/k8ssandra/k8ssandra-operator). -**It does not install `k8ssandra-operator` itself!** This configuration is meant -to be used in test environments: **It lacks crucial parts like backups -(`medusa`)!** - -## Usage in Helmfile - -### Prerequisites - -You need a *storage class* that can automatically request storage volumes. For -Hetzner's cloud see: [Container Storage Interface driver for Hetzner -Cloud](https://github.com/hetznercloud/csi-driver) - -### Usage - -These entries are used in the `helfile` file: - -``` yaml -... - -repositories: - - name: wire - url: 'https://s3-eu-west-1.amazonaws.com/public.wire.com/charts' - - name: k8ssandra-stable - url: https://helm.k8ssandra.io/stable - -... - -releases: - - name: k8ssandra-operator - chart: 'k8ssandra-stable/k8ssandra-operator' - namespace: databases - version: 0.39.2 - values: - # Use a cass-operator image that is compatible to the K8s cluster version - - cass-operator: - image: - tag: v1.10.5 - - # Installs CDRs automatically - - name: k8ssandra-test-cluster - chart: "wire/k8ssandra-test-cluster" - namespace: "databases" - version: {{ .Values.wireChartVersion | quote }} - needs: - - 'databases/k8ssandra-operator' - wait: true - waitForJobs: true - - - name: 'wire-server' - namespace: 'wire' - chart: 'wire/wire-server' - version: {{ .Values.wireChartVersion | quote }} - values: - - './helm_vars/wire-server/values.yaml.gotmpl' - secrets: - - './helm_vars/wire-server/secrets.yaml' - needs: - - 'databases/k8ssandra-test-cluster' - -... -``` - -Please note the `needs` relations of the releases: `wire-server` *needs* -`k8ssandra-test-cluster` which *needs* `k8ssandra-operator`. - -`wait` and `waitForJobs` are mandatory for `k8ssandra-test-cluster` in this -setup: These settings ensure that the database really exists before resuming -with the deployment. - -## Implementation details - -### k8ssandra-cluster.yaml - -Contains the `K8ssandraCluster` object. Its schema is described in the [CRD -reference](https://docs-v2.k8ssandra.io/reference/crd/k8ssandra-operator-crds-latest/#k8ssandracluster) - -The specified *Cassandra* cluster runs on a single Node with reasonable -resources for test environments. - -### check-cluster-job.yaml - -Defines a job that tries to connect to the final *Cassandra* database. Other -deployments can wait on this. This is useful because `wire-server` needs a -working database right from the beginning of it's deployment. diff --git a/charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml b/charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml deleted file mode 100644 index 6fd8de25b93..00000000000 --- a/charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# This job fails until the Cassandra created database is reachable. The Helmfile -# deployment can wait for it. This is used to start wire-server deployments only -# with a reachable database. -apiVersion: batch/v1 -kind: Job -metadata: - name: check-cluster-job - namespace: {{ .Release.Namespace }} -spec: - template: - spec: - containers: - - name: cassandra - image: cassandra:3.11 - {{- if not .Values.client_encryption_options.enabled }} - command: ["cqlsh", "k8ssandra-cluster-datacenter-1-service"] - {{- else }} - command: ["cqlsh", "--ssl", "k8ssandra-cluster-datacenter-1-service"] - env: - - name: SSL_CERTFILE - value: "/certs/ca.crt" - volumeMounts: - - name: cassandra-jks-keystore - mountPath: "/certs" - volumes: - - name: cassandra-jks-keystore - secret: - secretName: cassandra-jks-keystore - {{- end }} - restartPolicy: OnFailure - # Default is 6 retries. 8 is a bit arbitrary, but should be sufficient for - # low resource environments (e.g. Wire-in-a-box.) - backoffLimit: 8 diff --git a/charts/k8ssandra-test-cluster/templates/jks-store-pass.yaml b/charts/k8ssandra-test-cluster/templates/jks-store-pass.yaml deleted file mode 100644 index 52e6f2d0ebb..00000000000 --- a/charts/k8ssandra-test-cluster/templates/jks-store-pass.yaml +++ /dev/null @@ -1,9 +0,0 @@ -{{- if .Values.client_encryption_options.enabled }} -apiVersion: v1 -kind: Secret -metadata: - name: jks-password - namespace: {{ .Release.Namespace }} -data: - keystore-pass: {{ .Values.client_encryption_options.keystorePassword | b64enc }} -{{- end }} diff --git a/charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml b/charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml deleted file mode 100644 index 33c39c50d90..00000000000 --- a/charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml +++ /dev/null @@ -1,62 +0,0 @@ -apiVersion: k8ssandra.io/v1alpha1 -kind: K8ssandraCluster -metadata: - name: k8ssandra-cluster - namespace: {{ .Release.Namespace }} -spec: - auth: false - cassandra: - serverVersion: "3.11.11" - telemetry: - prometheus: - enabled: {{ .Values.prometheus.enabled }} - resources: - requests: - cpu: 1 - memory: "4.0Gi" - limits: - memory: "4.0Gi" - config: - jvmOptions: - # Intentionally, half of the available memory - heap_max_size: "2G" - heap_initial_size: "2G" - gc_g1_rset_updating_pause_time_percent: 5 - gc: "G1GC" - gc_g1_max_gc_pause_ms: 300 - gc_g1_initiating_heap_occupancy_percent: 55 - gc_g1_parallel_threads: 16 - cassandraYaml: - client_encryption_options: - enabled: {{ .Values.client_encryption_options.enabled }} - optional: {{ .Values.client_encryption_options.optional }} - datacenters: - - metadata: - name: datacenter-1 - size: {{ .Values.datacenter.size }} - storageConfig: - cassandraDataVolumeClaimSpec: - storageClassName: {{ .Values.storageClassName }} - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.storageSize }} - {{- if .Values.client_encryption_options.enabled }} - clientEncryptionStores: - keystoreSecretRef: - name: cassandra-jks-keystore - key: keystore.jks - keystorePasswordSecretRef: - key: keystore-pass - name: jks-password - truststoreSecretRef: - name: cassandra-jks-keystore - key: truststore.jks - truststorePasswordSecretRef: - key: keystore-pass - name: jks-password - {{- end }} - reaper: - autoScheduling: - enabled: true diff --git a/charts/k8ssandra-test-cluster/templates/tls-certificate-bundle.yaml b/charts/k8ssandra-test-cluster/templates/tls-certificate-bundle.yaml deleted file mode 100644 index 4b06b31110c..00000000000 --- a/charts/k8ssandra-test-cluster/templates/tls-certificate-bundle.yaml +++ /dev/null @@ -1,24 +0,0 @@ -{{- if and .Values.client_encryption_options.enabled .Values.syncCACertToSecret }} -# Let trust-manager sync the CA PEM (and only that!) into secrets named -# `k8ssandra-tls-ca-certificate-` in all configured namespaces or only -# one if syncCACertNamespace is defined. This way we can hide the private key -# from public. -apiVersion: trust.cert-manager.io/v1alpha1 -kind: Bundle -metadata: - name: k8ssandra-tls-ca-certificate - namespace: {{ .Release.Namespace }} -spec: - sources: - - secret: - name: "cassandra-jks-keystore" - key: "ca.crt" - target: - secret: - key: "ca.crt" - {{- if hasKey .Values "syncCACertNamespace" }} - namespaceSelector: - matchLabels: - kubernetes.io/metadata.name: {{ .Values.syncCACertNamespace }} - {{- end }} -{{- end }} diff --git a/charts/k8ssandra-test-cluster/templates/tls-certificate.yaml b/charts/k8ssandra-test-cluster/templates/tls-certificate.yaml deleted file mode 100644 index c7efd99c8a6..00000000000 --- a/charts/k8ssandra-test-cluster/templates/tls-certificate.yaml +++ /dev/null @@ -1,44 +0,0 @@ -{{- if .Values.client_encryption_options.enabled }} -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: cassandra-certificate - namespace: {{ .Release.Namespace }} -spec: - # Secret names are always required. - secretName: cassandra-jks-keystore - duration: 2160h # 90d - renewBefore: 360h # 15d - subject: - organizations: - - PIT squad - # The use of the common name field has been deprecated since 2000 and is - # discouraged from being used. - # commonName: example.com - isCA: false - privateKey: - algorithm: RSA - encoding: PKCS1 - size: 2048 - usages: - - server auth - - client auth - # At least one of a DNS Name, URI, or IP address is required. - dnsNames: - - k8ssandra-cluster-datacenter-1-service.{{ .Release.Namespace }}.svc.cluster.local - - k8ssandra-cluster-datacenter-1-service - issuerRef: - name: ca-issuer - # We can reference ClusterIssuers by changing the kind here. - # The default value is Issuer (i.e. a locally namespaced Issuer) - kind: Issuer - # This is optional since cert-manager will default to this value however - # if you are using an external issuer, change this to that issuer group. - group: cert-manager.io - keystores: - jks: - create: true - passwordSecretRef: # Password used to encrypt the keystore - key: keystore-pass - name: jks-password -{{- end }} diff --git a/charts/k8ssandra-test-cluster/templates/tls-issuer.yaml b/charts/k8ssandra-test-cluster/templates/tls-issuer.yaml deleted file mode 100644 index 65bc3dbad38..00000000000 --- a/charts/k8ssandra-test-cluster/templates/tls-issuer.yaml +++ /dev/null @@ -1,9 +0,0 @@ -{{- if .Values.client_encryption_options.enabled }} -apiVersion: cert-manager.io/v1 -kind: Issuer -metadata: - name: ca-issuer - namespace: {{ .Release.Namespace }} -spec: - selfSigned: {} -{{- end }} diff --git a/charts/k8ssandra-test-cluster/values.yaml b/charts/k8ssandra-test-cluster/values.yaml deleted file mode 100644 index 239dba3c21d..00000000000 --- a/charts/k8ssandra-test-cluster/values.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# The values in k8ssandra-cluster.yaml are well choosen. Please only export and -# override them if you are confident the change is needed. - -# storageClassName: the name storageClass to use. This defines where the data is -# stored. Storage is automatically requested if the storage class is correctly -# setup. -storageClassName: hcloud-volumes-encrypted - -# storageSize: Size of the storage (persistent volume claim) to request. At -# Hetzner's cloud the smallest volume is 10GB. So, even if you need much less -# storage, it's fine to request 10GB. The memory units are described here: -# https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory -storageSize: 10G - -# These options relate to the client_encryption_options described in: -# https://cassandra.apache.org/doc/stable/cassandra/configuration/cass_yaml_file.html#client_encryption_options -client_encryption_options: - enabled: false - optional: true - # The password could be secured better. However, this chart is meant to be - # used as test setup. And, protecting a self-signed certificate isn't very - # useful. - keystorePassword: password - -# Guard the private key by syncing only the CA certificate to -# `k8ssandra-test-cluster-tls-ca-certificate` secrets. Requires `trust-manager` -# Helm chart to be installed (including CRDs.) -syncCACertToSecret: false - -# Limit syncing to this namespace. Otherwise, the secret is synced to all -# namespaces. -# syncCACertNamespace: - -# For telemetry data -prometheus: - enabled: true - -# Size of the datacenter -datacenter: - size: 1 diff --git a/charts/smallstep-accomp/Chart.yaml b/charts/smallstep-accomp/Chart.yaml deleted file mode 100644 index 6dad899102f..00000000000 --- a/charts/smallstep-accomp/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Accompanying chart for Smallstep E2EI support -name: smallstep-accomp -version: 1.0.4 diff --git a/charts/smallstep-accomp/README.md b/charts/smallstep-accomp/README.md deleted file mode 100644 index ad57924296c..00000000000 --- a/charts/smallstep-accomp/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# smallstep-acomp - Helm chart accompanying smallstep - -This Helm chart is meant to be installed alongside the [step-certificates Helm -chart](https://smallstep.github.io/helm-charts) in the same namespace. It has been tested with Helm -chart version `1.25.0` and image - -``` -image: - repository: cr.step.sm/smallstep/step-ca - tag: "0.25.3-rc7" -``` - -This Helm chart provides: - -- A reverse-proxy for Certificate Revocation List (CR) distribution endpoints to federating smallstep - servers -- Smallstep server configuration for the End-to-End Identity setup - - -## Reverse-proxy for CRL distribution points - -This Helm chart installs a reverse proxy that proxies the Certificate Revocation List (CRL) -Distribution Point of the Smallstep servers CRL Certificate Authority (CA) from federating domains -and the own domain. This reverse proxy is required for a working End-to-End Identity setup. - -The Helm chart deploys a nginx server that reverse-proxies -`https:///proxyCrl/` to `https://{other_acme_domain}/crl` -as well as an ingress for the `/proxyCrl` endpoint. For example if `upstreams.proxiedHosts` is set -to `[acme.alpha.example.com, acme.beta.example.com]` and the host for the Smallstep server on the -own domain is `acme.alpha.example.com` this helm chart will forward requests - -- `https://acme.alpha.example.com/proxyCrl/acme.alpha.example.com` to `https://acme.alpha.example.com/crl` -- `https://acme.alpha.example.com/proxyCrl/acme.beta.example.com` to `https://acme.beta.example.com/crl` - -| Name | Description | -| -------------------------- | --------------------------------------------------------------------------------------------------------- | -| `upstreams.enable` | Set to `false` in case you want to write custom nginx server block for the upstream rules | -| `upstreams.dnsResolver` | DNS server that nginx uses to resolve the proxied hostnames | -| `upstreams.proxiedHosts` | List of remote (federated) step-ca hostnames to proxy. Also include the own step-ca host here. | -| `nginx.ingress.enable` | Set to `false` if you need to define a custom ingress for the /proxyCrl endpoint. Make sure CORS is set. | -| `nginx.ingress.hostname` | Hostname of the step-ca server | -| `nginx.ingress.extraTls` | The TLS configuration | -| `nginx.ingress.annotations`| CORS config for the ingress, set the hostname of the step-ca server here | - -For more details on `nginx.*` parameters see README.md documentation in the `nginx` dependency chart. - -## Smallstep server configuration for the End-to-End Identity setup - -This Helm chart helps to create configuration file for step-ca. If `stepConfig.enabled` is `true` a -configmap that contains a `ca.json` will be created. The name of that configmap is compatible with the -step-certificates Helm chart, so that it can be directly used. However since step-ca is deployed -from a seperate Helm release updating and deploying a configuration won't cause an automatic reload -of the step-ca server. It is therefore recommended to manually restart step-ca after configuartion -changes if this Helm chart is used for these purposes. - -For references see: - -- [[1] Configuring `step-ca`](https://smallstep.com/docs/step-ca/configuration/) -- [[2] Configuring `step-ca` Provisioners - ACME for Wire messenger clients ](https://smallstep.com/docs/step-ca/provisioners/#acme-for-wire-messenger-clients) - -| Parameter | Description | -|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| -| `stepConfig.enabled` | Create a configmap with configuration file for `step-certificates` Helm chart. | -| | If `true` then almost all `stepConfig.*` parameters are required. | -| `stepConfig.configTemplate` | Template for the configuration file. Overwrite this if the default value is not generic enough for your use case. | -| `stepConfig.address` | See [1] | -| `stepConfig.dnsName` | Used in `dnsNames` config entry (See [1]) and to define the CRL URL. | -| `stepConfig.additionalDNSNames` | Optional. Additional entries to `dnsNames` configuration field | -| `stepConfig.root` | See [1]. Public key of the Root CA | -| `stepConfig.crt` | See [1]. Public key of the Intermediate CA | -| `stepConfig.key` | See [1]. Private key of the Intermediate CA | -| `stepConfig.federatedRoots` | See [1]. Add all cross-signed Intermediate CA certs from federating domains here. | -| `stepConfig.db` | See [1] | -| `stepConfig.tls` | See [1] | -| `stepConfig.logger` | See [1] | -| `stepConfig.authority.claims` | See [1] | -| `stepConfig.authority.jwk` | JSON string of the JWK provisioner to use. A JWK provisioner can be created | -| | by running `step ca init` then copying it out of the generated `ca.json`, discarding the `ca.json`. | -| `stepConfig.authority.acme.name` | Name of the ACME provisioner. Default: `"keycloakteams"` | -| `stepConfig.authority.acme.claims` | See [1] | -| `stepConfig.authority.acme.dpop.key` | See [2]. Public half of the DPoP signature key bundle configured of the Wire deployment. | -| | Use the same value as `brig.secrets.dpopSigKeyBundle` value of the `wire-server` Helm chart. | -| | Base64 encoded string of the PEM encoded public key. | -| `stepConfig.authority.acme.dpop.wireDomain` | Set this to the federation domain of the backend | -| `stepConfig.authority.acme.oidc.clientId` | Name of the OIDC client. Default: "wireapp". | -| `stepConfig.authority.acme.oidc.discoveryBaseUrl` | OpenID Connect Discovery endpoint. The OIDC provider must respond with its configuration when `/.well-known/openid-configuration` | -| | is appended to the URL. For Keycloak this URL is of format `https:///auth/realms/`. | -| `stepConfig.authority.acme.oidc.issuerUrl` | For Keycloak this must be of the format `https:///auth/realms/?client_id=wireapp` | -| `stepConfig.authority.acme.oidc.signatureAlgorithms` | See [2] | -| `stepConfig.authority.acme.oidc.transform` | See [2]. Has sensible default. There shouldn't be any need to customize this setting. | -| `stepConfig.authority.acme.x509.organization` | Set this to the federation domain of the backend | -| `stepConfig.authority.acme.x509.template` | See [2]. Go template for a client certificate which is evaluated by step-ca. | -| | This string is evaluated as template of the Helm chart first. | -| | Has a sensible default. There shouldn't be a need to customize this setting. | - -| Parameter | Description | -|-----------------------|-------------------------------------------------------------------------------------------------------| -| `caPassword.enabled` | If `true` generate Secret with a name that the `step-certificates` Helm chart will automatically use. | -| | The Helm chart will mount this at `/home/step/secrets/passwords/password`. | -| `caPassword.password` | Password that decrypts the intermediate CA private key | - -| Parameter | Description | -|---------------------------|-------------------------------------------------------------------------------------------------------| -| `existingSecrets.enabled` | If `true` generate Secret with a name that the `step-certificates` Helm chart will automatically use. | -| `existingSecrets.data` | Map from filename to content. Each entry will be mounted as file `/home/step/secrets/` | -| | Add the private key of the Intermediate CA here. | - -| Parameter | Description | -|-------------------------|-----------------------------------------------------------------------------------------------------| -| `existingCerts.enabled` | If `true` generate ConfigMap with a name that the Helm chart will automatically use. | -| `existingCerts.data` | Map from filename to content. Each entry will be mounted as file `/home/step/certs/` | -| `existingCerts.data` | Use it to make public keys of the Root, intermediate CA as well as the cross-signed certs available | -| | to step-ca. Each entry will be mounted as file `/home/step/certs/` | diff --git a/charts/smallstep-accomp/requirements.yaml b/charts/smallstep-accomp/requirements.yaml deleted file mode 100644 index e9d0780c6e9..00000000000 --- a/charts/smallstep-accomp/requirements.yaml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: -- name: nginx - version: 15.10.4 - repository: https://charts.bitnami.com/bitnami diff --git a/charts/smallstep-accomp/templates/_helpers.tpl b/charts/smallstep-accomp/templates/_helpers.tpl deleted file mode 100644 index fb5cb93c9ce..00000000000 --- a/charts/smallstep-accomp/templates/_helpers.tpl +++ /dev/null @@ -1,3 +0,0 @@ -{{- define "fullname" -}} -smallstep-step-certificates -{{- end -}} diff --git a/charts/smallstep-accomp/templates/ca-password.yaml b/charts/smallstep-accomp/templates/ca-password.yaml deleted file mode 100644 index cd1bdc962a9..00000000000 --- a/charts/smallstep-accomp/templates/ca-password.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.caPassword.enabled }} -apiVersion: v1 -kind: Secret -metadata: - name: smallstep-step-certificates-ca-password - labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: "{{ .Release.Name }}" -type: Opaque -data: - password: {{ .Values.caPassword.password | b64enc | quote }} -{{- end }} diff --git a/charts/smallstep-accomp/templates/certs.yaml b/charts/smallstep-accomp/templates/certs.yaml deleted file mode 100644 index c9ef0ce45a9..00000000000 --- a/charts/smallstep-accomp/templates/certs.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.existingCerts.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: smallstep-step-certificates-certs - labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: "{{ .Release.Name }}" -data: - {{- range $key, $value := .Values.existingCerts.data }} - {{ $key }}: {{ $value | quote }} - {{- end }} -{{- end }} diff --git a/charts/smallstep-accomp/templates/secrets.yaml b/charts/smallstep-accomp/templates/secrets.yaml deleted file mode 100644 index 8448fbc7f8f..00000000000 --- a/charts/smallstep-accomp/templates/secrets.yaml +++ /dev/null @@ -1,14 +0,0 @@ -{{- if .Values.existingSecrets.enabled }} -apiVersion: v1 -kind: Secret -metadata: - name: smallstep-step-certificates-secrets - labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: "{{ .Release.Name }}" -type: Opaque -data: - {{- range $key, $value := .Values.existingSecrets.data }} - {{ $key }}: {{ $value | b64enc | quote }} - {{- end }} -{{- end }} diff --git a/charts/smallstep-accomp/templates/server-block-configmap.yaml b/charts/smallstep-accomp/templates/server-block-configmap.yaml deleted file mode 100644 index 59c423d3345..00000000000 --- a/charts/smallstep-accomp/templates/server-block-configmap.yaml +++ /dev/null @@ -1,39 +0,0 @@ -{{- if and .Values.upstreams.enabled .Values.nginx.existingServerBlockConfigmap }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Values.nginx.existingServerBlockConfigmap }} - labels: - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -data: - server.conf: | - resolver {{ .Values.upstreams.dnsResolver }}; - - server { - listen 0.0.0.0:8080; - - {{- range .Values.upstreams.proxiedHosts }} - - location /proxyCrl/{{ . }} { - # This indirection is required to make the resolver check the domain. - # Otherwise, broken upstreams lead to broken deployments. - set $backend "{{ . }}"; - - proxy_redirect off; - proxy_set_header X-Forwarded-Host $http_host; - proxy_set_header Host $backend; - proxy_hide_header Content-Type; - add_header Content-Type application/pkix-crl; - # Prevent caching on client side - add_header Cache-Control 'no-cache, no-store, must-revalidate'; - add_header Pragma 'no-cache'; - add_header Expires '0'; - - proxy_pass "https://$backend/crl"; - } - - {{- end }} - } -{{- end }} diff --git a/charts/smallstep-accomp/templates/step-config.yaml b/charts/smallstep-accomp/templates/step-config.yaml deleted file mode 100644 index 0cb957fa88c..00000000000 --- a/charts/smallstep-accomp/templates/step-config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -{{- if .Values.stepConfig.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: smallstep-step-certificates-config -data: - ca.json: |- - {{(tpl .Values.stepConfig.configTemplate .) | fromYaml | toJson }} -{{- end }} diff --git a/charts/smallstep-accomp/values.yaml b/charts/smallstep-accomp/values.yaml deleted file mode 100644 index e4e3ad18437..00000000000 --- a/charts/smallstep-accomp/values.yaml +++ /dev/null @@ -1,212 +0,0 @@ -nginx: - existingServerBlockConfigmap: "smallstep-accomp-server-block" - - service: - type: ClusterIP - - ingress: - enabled: true - # ingressClassName: "nginx" - - # hostname: "acme.alpha.example.com" - path: "/proxyCrl" - pathType: "Prefix" - - # extraTls: - # - - # hosts: [ "acme.alpha.example.com" ] - # secretName: "smallstep-step-certificates-ingress-cert" - - # annotations: - # nginx.ingress.kubernetes.io/cors-allow-origin: https://webapp.acme.alpha.example.com - # nginx.ingress.kubernetes.io/cors-expose-headers: Replay-Nonce, Location - # nginx.ingress.kubernetes.io/enable-cors: 'true' - -upstreams: - enabled: true - # dnsResolver: 9.9.9.9 - - # Note: include the smallstep host of the own domain here as well - proxiedHosts: [] - # proxiedHosts: - # - acme.alpha.example.com - # - acme.beta.example.com - # - acme.gamma.example.com - - -caPassword: - enabled: true - password: "...." - -existingSecrets: - enabled: false - # data: - # ca.key: foobar - -existingCerts: - enabled: false - # data: - # ca.crt: "-----BEGIN CERTIFICATE-----...." - # root_ca.crt: "-----BEGIN CERTIFICATE-----...." - # ca-other2-cross-signed.crt: "-----BEGIN CERTIFICATE-----...." - # ca-other3-cross-signed.crt: "-----BEGIN CERTIFICATE-----...." - -stepConfig: - enabled: true - - address: "0.0.0.0:9000" - - # dnsName: acme.alpha.example.com - - # additionalDNSNames: - # - localhost - - root: /home/step/certs/root_ca.crt - crt: /home/step/certs/ca.crt - key: /home/step/secrets/ca.key - - federatedRoots: - - /home/step/certs/ca.crt - - # federatedRoots: - # - /home/step/certs/ca.crt - # - /home/step/certs/acme.beta.example.com-xsigned-by-acme.alpha.example.com - - db: - badgerFileLoadingMode: "" - dataSource: /home/step/db - type: badgerv2 - - tls: - cipherSuites: - - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 - maxVersion: 1.3 - minVersion: 1.2 - renegotiation: false - - logger: - format: text - - authority: - claims: - maxTLSCertDuration: 87701h - - # jwk: |- - # { - # "type": "JWK", - # "name": "..example.com", - # "key": { ... }, - # "encryptedKey": "e..." - # } - - acme: - name: keycloakteams - - claims: - allowRenewalAfterExpiry: false - defaultTLSCertDuration: 2160h - disableRenewal: false - maxTLSCertDuration: 2160h - minTLSCertDuration: 60s - - dpop: - # key: - wireDomain: alpha.example.com - - oidc: - clientId: wireapp - # discoveryBaseUrl: https://keycloak.example.com/auth/realms/master - # issuerUrl: https://keycloak.example.com/auth/realms/master?client_id=wireapp - signatureAlgorithms: - - RS256 - - ES256 - - ES384 - - EdDSA - transform: | - { - "name": "{{ .name }}", - "preferred_username": "wireapp://%40{{ .preferred_username }}" - } - - x509: - # organization: alpha.example.com - template: | - { - "subject": { - "organization": {{ required "stepConfig.authority.acme.x509.organization is missing" .Values.stepConfig.authority.acme.x509.organization | toJson }}, - "commonName": {{ "{{" }} toJson .Oidc.name {{ "}}" }} - }, - "uris": [{{ "{{" }} toJson .Oidc.preferred_username {{ "}}" }}, {{ "{{" }} toJson .Dpop.sub {{ "}}" }}], - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["clientAuth"], - "crlDistributionPoints": {{ tpl "[ https://{{ required \"stepConfig.dnsName is missing\" .Values.stepConfig.dnsName }}/crl ]" . | fromYamlArray | toJson }} - } - - configTemplate: |- - address: {{ required "stepConfig.address is missing" .Values.stepConfig.address }} - - dnsNames: - - {{ required "stepConfig.dnsName is missing" .Values.stepConfig.dnsName }} - {{- if .Values.stepConfig.additionalDNSNames }} - {{- .Values.stepConfig.additionalDNSNames | toYaml | nindent 2 }} - {{- end }} - - crt: {{ required "stepConfig.crt is missing" .Values.stepConfig.crt }} - key: {{ required "stepConfig.key is missing" .Values.stepConfig.key }} - root: {{ required "stepConfig.root is missing" .Values.stepConfig.root }} - - federatedRoots: - {{- required "stepConfig.federatedRoots is missing" .Values.stepConfig.federatedRoots | toYaml | nindent 2 }} - - crl: - enabled: true - generateOnRevoke: true - idpURL: https://{{ required "stepConfig.dnsName is missing" .Values.stepConfig.dnsName }}/crl - - db: - {{ required "stepConfig.db is missing" .Values.stepConfig.db | toYaml | nindent 2 }} - - tls: - {{ required "stepConfig.tls is missing" .Values.stepConfig.tls | toYaml | nindent 2 }} - - logger: - {{ required "stepConfig.logger is missing" .Values.stepConfig.logger | toYaml | nindent 2 }} - - authority: - claims: - {{ required "stepConfig.authority.claims is missing" .Values.stepConfig.authority.claims | toYaml | nindent 4 }} - provisioners: - - {{ required "stepConfig.authority.jwk is missing" .Values.stepConfig.authority.jwk | fromJson | toYaml | nindent 6 }} - - name: {{ required "stepConfig.authority.acme.name is missing" .Values.stepConfig.authority.acme.name }} - type: ACME - forceCN: true - challenges: - - wire-oidc-01 - - wire-dpop-01 - claims: - {{ required "stepConfig.authority.acme.claims is missing" .Values.stepConfig.authority.acme.claims | toYaml | nindent 8 }} - options: - wire: - dpop: - key: {{ required "stepConfig.authority.acme.dpop.key is missing" .Values.stepConfig.authority.acme.dpop.key }} - target: https://{{ required "stepConfig.authority.acme.dpop.wireDomain" .Values.stepConfig.authority.acme.dpop.wireDomain }}/clients/{{ "{{" }}.DeviceID{{ "}}" }}/access-token - oidc: - config: - clientId: {{ required "stepConfig.authority.acme.oidc.clientId is missing" .Values.stepConfig.authority.acme.oidc.clientId }} - signatureAlgorithms: - {{ required "stepConfig.authority.acme.oidc.signatureAlgorithms is missing" .Values.stepConfig.authority.acme.oidc.signatureAlgorithms | toYaml | nindent 14 }} - provider: - discoveryBaseUrl: {{ required "stepConfig.authority.acme.oidc.discoveryBaseUrl is missing" .Values.stepConfig.authority.acme.oidc.discoveryBaseUrl }} - id_token_signing_alg_values_supported: - {{ required "stepConfig.authority.acme.oidc.signatureAlgorithms is missing" .Values.stepConfig.authority.acme.oidc.signatureAlgorithms | toYaml | nindent 14 }} - issuerUrl: {{ required "stepConfig.authority.acme.oidc.issuerUrl is missing" .Values.stepConfig.authority.acme.oidc.issuerUrl }} - transform: {{ required "stepConfig.authority.acme.oidc.transform is missing" .Values.stepConfig.authority.acme.oidc.transform | toJson }} - x509: - template: {{ (tpl .Values.stepConfig.authority.acme.x509.template .) | toJson }} - - {{- if .Values.stepConfig.extraConfig }} - {{ .Values.stepConfig.extraconfig | toYaml }} - {{- end }} - - - From 47eef9ca02e24d1bec8aeacc043249ecd9a43b9a Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 12 Aug 2024 14:59:40 +0200 Subject: [PATCH 038/136] Revert "Remove helm charts k8ssandra-test-cluster and smallstep-accomp (#4202)" (#4203) This reverts commit 035a17d7c6e57288c2548207e80d4f01eea50ae0. --- charts/k8ssandra-test-cluster/.helmignore | 23 ++ charts/k8ssandra-test-cluster/Chart.yaml | 9 + charts/k8ssandra-test-cluster/README.md | 89 ++++++++ .../templates/check-cluster-job.yaml | 33 +++ .../templates/jks-store-pass.yaml | 9 + .../templates/k8ssandra-cluster.yaml | 62 +++++ .../templates/tls-certificate-bundle.yaml | 24 ++ .../templates/tls-certificate.yaml | 44 ++++ .../templates/tls-issuer.yaml | 9 + charts/k8ssandra-test-cluster/values.yaml | 40 ++++ charts/smallstep-accomp/Chart.yaml | 4 + charts/smallstep-accomp/README.md | 113 ++++++++++ charts/smallstep-accomp/requirements.yaml | 4 + .../smallstep-accomp/templates/_helpers.tpl | 3 + .../templates/ca-password.yaml | 12 + charts/smallstep-accomp/templates/certs.yaml | 13 ++ .../smallstep-accomp/templates/secrets.yaml | 14 ++ .../templates/server-block-configmap.yaml | 39 ++++ .../templates/step-config.yaml | 9 + charts/smallstep-accomp/values.yaml | 212 ++++++++++++++++++ 20 files changed, 765 insertions(+) create mode 100644 charts/k8ssandra-test-cluster/.helmignore create mode 100644 charts/k8ssandra-test-cluster/Chart.yaml create mode 100644 charts/k8ssandra-test-cluster/README.md create mode 100644 charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml create mode 100644 charts/k8ssandra-test-cluster/templates/jks-store-pass.yaml create mode 100644 charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml create mode 100644 charts/k8ssandra-test-cluster/templates/tls-certificate-bundle.yaml create mode 100644 charts/k8ssandra-test-cluster/templates/tls-certificate.yaml create mode 100644 charts/k8ssandra-test-cluster/templates/tls-issuer.yaml create mode 100644 charts/k8ssandra-test-cluster/values.yaml create mode 100644 charts/smallstep-accomp/Chart.yaml create mode 100644 charts/smallstep-accomp/README.md create mode 100644 charts/smallstep-accomp/requirements.yaml create mode 100644 charts/smallstep-accomp/templates/_helpers.tpl create mode 100644 charts/smallstep-accomp/templates/ca-password.yaml create mode 100644 charts/smallstep-accomp/templates/certs.yaml create mode 100644 charts/smallstep-accomp/templates/secrets.yaml create mode 100644 charts/smallstep-accomp/templates/server-block-configmap.yaml create mode 100644 charts/smallstep-accomp/templates/step-config.yaml create mode 100644 charts/smallstep-accomp/values.yaml diff --git a/charts/k8ssandra-test-cluster/.helmignore b/charts/k8ssandra-test-cluster/.helmignore new file mode 100644 index 00000000000..0e8a0eb36f4 --- /dev/null +++ b/charts/k8ssandra-test-cluster/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/k8ssandra-test-cluster/Chart.yaml b/charts/k8ssandra-test-cluster/Chart.yaml new file mode 100644 index 00000000000..b67746d3075 --- /dev/null +++ b/charts/k8ssandra-test-cluster/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: k8ssandra-test-cluster +description: K8ssandra (Cassandra cluster) K8ssandraCluster object for wire test servers. (This does not install K8ssandra itself!) + +type: application + +version: 0.1.0 + +appVersion: "0.39.2" diff --git a/charts/k8ssandra-test-cluster/README.md b/charts/k8ssandra-test-cluster/README.md new file mode 100644 index 00000000000..865183c1566 --- /dev/null +++ b/charts/k8ssandra-test-cluster/README.md @@ -0,0 +1,89 @@ +# k8ssandra-test-cluster Helm chart + +`k8ssandra-test-cluster` provides a `K8ssandraCluster` object to create a +*Cassandra* database with +[`k8ssandra-operator`](https://artifacthub.io/packages/helm/k8ssandra/k8ssandra-operator). +**It does not install `k8ssandra-operator` itself!** This configuration is meant +to be used in test environments: **It lacks crucial parts like backups +(`medusa`)!** + +## Usage in Helmfile + +### Prerequisites + +You need a *storage class* that can automatically request storage volumes. For +Hetzner's cloud see: [Container Storage Interface driver for Hetzner +Cloud](https://github.com/hetznercloud/csi-driver) + +### Usage + +These entries are used in the `helfile` file: + +``` yaml +... + +repositories: + - name: wire + url: 'https://s3-eu-west-1.amazonaws.com/public.wire.com/charts' + - name: k8ssandra-stable + url: https://helm.k8ssandra.io/stable + +... + +releases: + - name: k8ssandra-operator + chart: 'k8ssandra-stable/k8ssandra-operator' + namespace: databases + version: 0.39.2 + values: + # Use a cass-operator image that is compatible to the K8s cluster version + - cass-operator: + image: + tag: v1.10.5 + + # Installs CDRs automatically + - name: k8ssandra-test-cluster + chart: "wire/k8ssandra-test-cluster" + namespace: "databases" + version: {{ .Values.wireChartVersion | quote }} + needs: + - 'databases/k8ssandra-operator' + wait: true + waitForJobs: true + + - name: 'wire-server' + namespace: 'wire' + chart: 'wire/wire-server' + version: {{ .Values.wireChartVersion | quote }} + values: + - './helm_vars/wire-server/values.yaml.gotmpl' + secrets: + - './helm_vars/wire-server/secrets.yaml' + needs: + - 'databases/k8ssandra-test-cluster' + +... +``` + +Please note the `needs` relations of the releases: `wire-server` *needs* +`k8ssandra-test-cluster` which *needs* `k8ssandra-operator`. + +`wait` and `waitForJobs` are mandatory for `k8ssandra-test-cluster` in this +setup: These settings ensure that the database really exists before resuming +with the deployment. + +## Implementation details + +### k8ssandra-cluster.yaml + +Contains the `K8ssandraCluster` object. Its schema is described in the [CRD +reference](https://docs-v2.k8ssandra.io/reference/crd/k8ssandra-operator-crds-latest/#k8ssandracluster) + +The specified *Cassandra* cluster runs on a single Node with reasonable +resources for test environments. + +### check-cluster-job.yaml + +Defines a job that tries to connect to the final *Cassandra* database. Other +deployments can wait on this. This is useful because `wire-server` needs a +working database right from the beginning of it's deployment. diff --git a/charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml b/charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml new file mode 100644 index 00000000000..6fd8de25b93 --- /dev/null +++ b/charts/k8ssandra-test-cluster/templates/check-cluster-job.yaml @@ -0,0 +1,33 @@ +# This job fails until the Cassandra created database is reachable. The Helmfile +# deployment can wait for it. This is used to start wire-server deployments only +# with a reachable database. +apiVersion: batch/v1 +kind: Job +metadata: + name: check-cluster-job + namespace: {{ .Release.Namespace }} +spec: + template: + spec: + containers: + - name: cassandra + image: cassandra:3.11 + {{- if not .Values.client_encryption_options.enabled }} + command: ["cqlsh", "k8ssandra-cluster-datacenter-1-service"] + {{- else }} + command: ["cqlsh", "--ssl", "k8ssandra-cluster-datacenter-1-service"] + env: + - name: SSL_CERTFILE + value: "/certs/ca.crt" + volumeMounts: + - name: cassandra-jks-keystore + mountPath: "/certs" + volumes: + - name: cassandra-jks-keystore + secret: + secretName: cassandra-jks-keystore + {{- end }} + restartPolicy: OnFailure + # Default is 6 retries. 8 is a bit arbitrary, but should be sufficient for + # low resource environments (e.g. Wire-in-a-box.) + backoffLimit: 8 diff --git a/charts/k8ssandra-test-cluster/templates/jks-store-pass.yaml b/charts/k8ssandra-test-cluster/templates/jks-store-pass.yaml new file mode 100644 index 00000000000..52e6f2d0ebb --- /dev/null +++ b/charts/k8ssandra-test-cluster/templates/jks-store-pass.yaml @@ -0,0 +1,9 @@ +{{- if .Values.client_encryption_options.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: jks-password + namespace: {{ .Release.Namespace }} +data: + keystore-pass: {{ .Values.client_encryption_options.keystorePassword | b64enc }} +{{- end }} diff --git a/charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml b/charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml new file mode 100644 index 00000000000..33c39c50d90 --- /dev/null +++ b/charts/k8ssandra-test-cluster/templates/k8ssandra-cluster.yaml @@ -0,0 +1,62 @@ +apiVersion: k8ssandra.io/v1alpha1 +kind: K8ssandraCluster +metadata: + name: k8ssandra-cluster + namespace: {{ .Release.Namespace }} +spec: + auth: false + cassandra: + serverVersion: "3.11.11" + telemetry: + prometheus: + enabled: {{ .Values.prometheus.enabled }} + resources: + requests: + cpu: 1 + memory: "4.0Gi" + limits: + memory: "4.0Gi" + config: + jvmOptions: + # Intentionally, half of the available memory + heap_max_size: "2G" + heap_initial_size: "2G" + gc_g1_rset_updating_pause_time_percent: 5 + gc: "G1GC" + gc_g1_max_gc_pause_ms: 300 + gc_g1_initiating_heap_occupancy_percent: 55 + gc_g1_parallel_threads: 16 + cassandraYaml: + client_encryption_options: + enabled: {{ .Values.client_encryption_options.enabled }} + optional: {{ .Values.client_encryption_options.optional }} + datacenters: + - metadata: + name: datacenter-1 + size: {{ .Values.datacenter.size }} + storageConfig: + cassandraDataVolumeClaimSpec: + storageClassName: {{ .Values.storageClassName }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.storageSize }} + {{- if .Values.client_encryption_options.enabled }} + clientEncryptionStores: + keystoreSecretRef: + name: cassandra-jks-keystore + key: keystore.jks + keystorePasswordSecretRef: + key: keystore-pass + name: jks-password + truststoreSecretRef: + name: cassandra-jks-keystore + key: truststore.jks + truststorePasswordSecretRef: + key: keystore-pass + name: jks-password + {{- end }} + reaper: + autoScheduling: + enabled: true diff --git a/charts/k8ssandra-test-cluster/templates/tls-certificate-bundle.yaml b/charts/k8ssandra-test-cluster/templates/tls-certificate-bundle.yaml new file mode 100644 index 00000000000..4b06b31110c --- /dev/null +++ b/charts/k8ssandra-test-cluster/templates/tls-certificate-bundle.yaml @@ -0,0 +1,24 @@ +{{- if and .Values.client_encryption_options.enabled .Values.syncCACertToSecret }} +# Let trust-manager sync the CA PEM (and only that!) into secrets named +# `k8ssandra-tls-ca-certificate-` in all configured namespaces or only +# one if syncCACertNamespace is defined. This way we can hide the private key +# from public. +apiVersion: trust.cert-manager.io/v1alpha1 +kind: Bundle +metadata: + name: k8ssandra-tls-ca-certificate + namespace: {{ .Release.Namespace }} +spec: + sources: + - secret: + name: "cassandra-jks-keystore" + key: "ca.crt" + target: + secret: + key: "ca.crt" + {{- if hasKey .Values "syncCACertNamespace" }} + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Values.syncCACertNamespace }} + {{- end }} +{{- end }} diff --git a/charts/k8ssandra-test-cluster/templates/tls-certificate.yaml b/charts/k8ssandra-test-cluster/templates/tls-certificate.yaml new file mode 100644 index 00000000000..c7efd99c8a6 --- /dev/null +++ b/charts/k8ssandra-test-cluster/templates/tls-certificate.yaml @@ -0,0 +1,44 @@ +{{- if .Values.client_encryption_options.enabled }} +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: cassandra-certificate + namespace: {{ .Release.Namespace }} +spec: + # Secret names are always required. + secretName: cassandra-jks-keystore + duration: 2160h # 90d + renewBefore: 360h # 15d + subject: + organizations: + - PIT squad + # The use of the common name field has been deprecated since 2000 and is + # discouraged from being used. + # commonName: example.com + isCA: false + privateKey: + algorithm: RSA + encoding: PKCS1 + size: 2048 + usages: + - server auth + - client auth + # At least one of a DNS Name, URI, or IP address is required. + dnsNames: + - k8ssandra-cluster-datacenter-1-service.{{ .Release.Namespace }}.svc.cluster.local + - k8ssandra-cluster-datacenter-1-service + issuerRef: + name: ca-issuer + # We can reference ClusterIssuers by changing the kind here. + # The default value is Issuer (i.e. a locally namespaced Issuer) + kind: Issuer + # This is optional since cert-manager will default to this value however + # if you are using an external issuer, change this to that issuer group. + group: cert-manager.io + keystores: + jks: + create: true + passwordSecretRef: # Password used to encrypt the keystore + key: keystore-pass + name: jks-password +{{- end }} diff --git a/charts/k8ssandra-test-cluster/templates/tls-issuer.yaml b/charts/k8ssandra-test-cluster/templates/tls-issuer.yaml new file mode 100644 index 00000000000..65bc3dbad38 --- /dev/null +++ b/charts/k8ssandra-test-cluster/templates/tls-issuer.yaml @@ -0,0 +1,9 @@ +{{- if .Values.client_encryption_options.enabled }} +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: ca-issuer + namespace: {{ .Release.Namespace }} +spec: + selfSigned: {} +{{- end }} diff --git a/charts/k8ssandra-test-cluster/values.yaml b/charts/k8ssandra-test-cluster/values.yaml new file mode 100644 index 00000000000..239dba3c21d --- /dev/null +++ b/charts/k8ssandra-test-cluster/values.yaml @@ -0,0 +1,40 @@ +# The values in k8ssandra-cluster.yaml are well choosen. Please only export and +# override them if you are confident the change is needed. + +# storageClassName: the name storageClass to use. This defines where the data is +# stored. Storage is automatically requested if the storage class is correctly +# setup. +storageClassName: hcloud-volumes-encrypted + +# storageSize: Size of the storage (persistent volume claim) to request. At +# Hetzner's cloud the smallest volume is 10GB. So, even if you need much less +# storage, it's fine to request 10GB. The memory units are described here: +# https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#meaning-of-memory +storageSize: 10G + +# These options relate to the client_encryption_options described in: +# https://cassandra.apache.org/doc/stable/cassandra/configuration/cass_yaml_file.html#client_encryption_options +client_encryption_options: + enabled: false + optional: true + # The password could be secured better. However, this chart is meant to be + # used as test setup. And, protecting a self-signed certificate isn't very + # useful. + keystorePassword: password + +# Guard the private key by syncing only the CA certificate to +# `k8ssandra-test-cluster-tls-ca-certificate` secrets. Requires `trust-manager` +# Helm chart to be installed (including CRDs.) +syncCACertToSecret: false + +# Limit syncing to this namespace. Otherwise, the secret is synced to all +# namespaces. +# syncCACertNamespace: + +# For telemetry data +prometheus: + enabled: true + +# Size of the datacenter +datacenter: + size: 1 diff --git a/charts/smallstep-accomp/Chart.yaml b/charts/smallstep-accomp/Chart.yaml new file mode 100644 index 00000000000..6dad899102f --- /dev/null +++ b/charts/smallstep-accomp/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: Accompanying chart for Smallstep E2EI support +name: smallstep-accomp +version: 1.0.4 diff --git a/charts/smallstep-accomp/README.md b/charts/smallstep-accomp/README.md new file mode 100644 index 00000000000..ad57924296c --- /dev/null +++ b/charts/smallstep-accomp/README.md @@ -0,0 +1,113 @@ +# smallstep-acomp - Helm chart accompanying smallstep + +This Helm chart is meant to be installed alongside the [step-certificates Helm +chart](https://smallstep.github.io/helm-charts) in the same namespace. It has been tested with Helm +chart version `1.25.0` and image + +``` +image: + repository: cr.step.sm/smallstep/step-ca + tag: "0.25.3-rc7" +``` + +This Helm chart provides: + +- A reverse-proxy for Certificate Revocation List (CR) distribution endpoints to federating smallstep + servers +- Smallstep server configuration for the End-to-End Identity setup + + +## Reverse-proxy for CRL distribution points + +This Helm chart installs a reverse proxy that proxies the Certificate Revocation List (CRL) +Distribution Point of the Smallstep servers CRL Certificate Authority (CA) from federating domains +and the own domain. This reverse proxy is required for a working End-to-End Identity setup. + +The Helm chart deploys a nginx server that reverse-proxies +`https:///proxyCrl/` to `https://{other_acme_domain}/crl` +as well as an ingress for the `/proxyCrl` endpoint. For example if `upstreams.proxiedHosts` is set +to `[acme.alpha.example.com, acme.beta.example.com]` and the host for the Smallstep server on the +own domain is `acme.alpha.example.com` this helm chart will forward requests + +- `https://acme.alpha.example.com/proxyCrl/acme.alpha.example.com` to `https://acme.alpha.example.com/crl` +- `https://acme.alpha.example.com/proxyCrl/acme.beta.example.com` to `https://acme.beta.example.com/crl` + +| Name | Description | +| -------------------------- | --------------------------------------------------------------------------------------------------------- | +| `upstreams.enable` | Set to `false` in case you want to write custom nginx server block for the upstream rules | +| `upstreams.dnsResolver` | DNS server that nginx uses to resolve the proxied hostnames | +| `upstreams.proxiedHosts` | List of remote (federated) step-ca hostnames to proxy. Also include the own step-ca host here. | +| `nginx.ingress.enable` | Set to `false` if you need to define a custom ingress for the /proxyCrl endpoint. Make sure CORS is set. | +| `nginx.ingress.hostname` | Hostname of the step-ca server | +| `nginx.ingress.extraTls` | The TLS configuration | +| `nginx.ingress.annotations`| CORS config for the ingress, set the hostname of the step-ca server here | + +For more details on `nginx.*` parameters see README.md documentation in the `nginx` dependency chart. + +## Smallstep server configuration for the End-to-End Identity setup + +This Helm chart helps to create configuration file for step-ca. If `stepConfig.enabled` is `true` a +configmap that contains a `ca.json` will be created. The name of that configmap is compatible with the +step-certificates Helm chart, so that it can be directly used. However since step-ca is deployed +from a seperate Helm release updating and deploying a configuration won't cause an automatic reload +of the step-ca server. It is therefore recommended to manually restart step-ca after configuartion +changes if this Helm chart is used for these purposes. + +For references see: + +- [[1] Configuring `step-ca`](https://smallstep.com/docs/step-ca/configuration/) +- [[2] Configuring `step-ca` Provisioners - ACME for Wire messenger clients ](https://smallstep.com/docs/step-ca/provisioners/#acme-for-wire-messenger-clients) + +| Parameter | Description | +|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| `stepConfig.enabled` | Create a configmap with configuration file for `step-certificates` Helm chart. | +| | If `true` then almost all `stepConfig.*` parameters are required. | +| `stepConfig.configTemplate` | Template for the configuration file. Overwrite this if the default value is not generic enough for your use case. | +| `stepConfig.address` | See [1] | +| `stepConfig.dnsName` | Used in `dnsNames` config entry (See [1]) and to define the CRL URL. | +| `stepConfig.additionalDNSNames` | Optional. Additional entries to `dnsNames` configuration field | +| `stepConfig.root` | See [1]. Public key of the Root CA | +| `stepConfig.crt` | See [1]. Public key of the Intermediate CA | +| `stepConfig.key` | See [1]. Private key of the Intermediate CA | +| `stepConfig.federatedRoots` | See [1]. Add all cross-signed Intermediate CA certs from federating domains here. | +| `stepConfig.db` | See [1] | +| `stepConfig.tls` | See [1] | +| `stepConfig.logger` | See [1] | +| `stepConfig.authority.claims` | See [1] | +| `stepConfig.authority.jwk` | JSON string of the JWK provisioner to use. A JWK provisioner can be created | +| | by running `step ca init` then copying it out of the generated `ca.json`, discarding the `ca.json`. | +| `stepConfig.authority.acme.name` | Name of the ACME provisioner. Default: `"keycloakteams"` | +| `stepConfig.authority.acme.claims` | See [1] | +| `stepConfig.authority.acme.dpop.key` | See [2]. Public half of the DPoP signature key bundle configured of the Wire deployment. | +| | Use the same value as `brig.secrets.dpopSigKeyBundle` value of the `wire-server` Helm chart. | +| | Base64 encoded string of the PEM encoded public key. | +| `stepConfig.authority.acme.dpop.wireDomain` | Set this to the federation domain of the backend | +| `stepConfig.authority.acme.oidc.clientId` | Name of the OIDC client. Default: "wireapp". | +| `stepConfig.authority.acme.oidc.discoveryBaseUrl` | OpenID Connect Discovery endpoint. The OIDC provider must respond with its configuration when `/.well-known/openid-configuration` | +| | is appended to the URL. For Keycloak this URL is of format `https:///auth/realms/`. | +| `stepConfig.authority.acme.oidc.issuerUrl` | For Keycloak this must be of the format `https:///auth/realms/?client_id=wireapp` | +| `stepConfig.authority.acme.oidc.signatureAlgorithms` | See [2] | +| `stepConfig.authority.acme.oidc.transform` | See [2]. Has sensible default. There shouldn't be any need to customize this setting. | +| `stepConfig.authority.acme.x509.organization` | Set this to the federation domain of the backend | +| `stepConfig.authority.acme.x509.template` | See [2]. Go template for a client certificate which is evaluated by step-ca. | +| | This string is evaluated as template of the Helm chart first. | +| | Has a sensible default. There shouldn't be a need to customize this setting. | + +| Parameter | Description | +|-----------------------|-------------------------------------------------------------------------------------------------------| +| `caPassword.enabled` | If `true` generate Secret with a name that the `step-certificates` Helm chart will automatically use. | +| | The Helm chart will mount this at `/home/step/secrets/passwords/password`. | +| `caPassword.password` | Password that decrypts the intermediate CA private key | + +| Parameter | Description | +|---------------------------|-------------------------------------------------------------------------------------------------------| +| `existingSecrets.enabled` | If `true` generate Secret with a name that the `step-certificates` Helm chart will automatically use. | +| `existingSecrets.data` | Map from filename to content. Each entry will be mounted as file `/home/step/secrets/` | +| | Add the private key of the Intermediate CA here. | + +| Parameter | Description | +|-------------------------|-----------------------------------------------------------------------------------------------------| +| `existingCerts.enabled` | If `true` generate ConfigMap with a name that the Helm chart will automatically use. | +| `existingCerts.data` | Map from filename to content. Each entry will be mounted as file `/home/step/certs/` | +| `existingCerts.data` | Use it to make public keys of the Root, intermediate CA as well as the cross-signed certs available | +| | to step-ca. Each entry will be mounted as file `/home/step/certs/` | diff --git a/charts/smallstep-accomp/requirements.yaml b/charts/smallstep-accomp/requirements.yaml new file mode 100644 index 00000000000..e9d0780c6e9 --- /dev/null +++ b/charts/smallstep-accomp/requirements.yaml @@ -0,0 +1,4 @@ +dependencies: +- name: nginx + version: 15.10.4 + repository: https://charts.bitnami.com/bitnami diff --git a/charts/smallstep-accomp/templates/_helpers.tpl b/charts/smallstep-accomp/templates/_helpers.tpl new file mode 100644 index 00000000000..fb5cb93c9ce --- /dev/null +++ b/charts/smallstep-accomp/templates/_helpers.tpl @@ -0,0 +1,3 @@ +{{- define "fullname" -}} +smallstep-step-certificates +{{- end -}} diff --git a/charts/smallstep-accomp/templates/ca-password.yaml b/charts/smallstep-accomp/templates/ca-password.yaml new file mode 100644 index 00000000000..cd1bdc962a9 --- /dev/null +++ b/charts/smallstep-accomp/templates/ca-password.yaml @@ -0,0 +1,12 @@ +{{- if .Values.caPassword.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: smallstep-step-certificates-ca-password + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" +type: Opaque +data: + password: {{ .Values.caPassword.password | b64enc | quote }} +{{- end }} diff --git a/charts/smallstep-accomp/templates/certs.yaml b/charts/smallstep-accomp/templates/certs.yaml new file mode 100644 index 00000000000..c9ef0ce45a9 --- /dev/null +++ b/charts/smallstep-accomp/templates/certs.yaml @@ -0,0 +1,13 @@ +{{- if .Values.existingCerts.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: smallstep-step-certificates-certs + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" +data: + {{- range $key, $value := .Values.existingCerts.data }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/charts/smallstep-accomp/templates/secrets.yaml b/charts/smallstep-accomp/templates/secrets.yaml new file mode 100644 index 00000000000..8448fbc7f8f --- /dev/null +++ b/charts/smallstep-accomp/templates/secrets.yaml @@ -0,0 +1,14 @@ +{{- if .Values.existingSecrets.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: smallstep-step-certificates-secrets + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" +type: Opaque +data: + {{- range $key, $value := .Values.existingSecrets.data }} + {{ $key }}: {{ $value | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/charts/smallstep-accomp/templates/server-block-configmap.yaml b/charts/smallstep-accomp/templates/server-block-configmap.yaml new file mode 100644 index 00000000000..59c423d3345 --- /dev/null +++ b/charts/smallstep-accomp/templates/server-block-configmap.yaml @@ -0,0 +1,39 @@ +{{- if and .Values.upstreams.enabled .Values.nginx.existingServerBlockConfigmap }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.nginx.existingServerBlockConfigmap }} + labels: + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +data: + server.conf: | + resolver {{ .Values.upstreams.dnsResolver }}; + + server { + listen 0.0.0.0:8080; + + {{- range .Values.upstreams.proxiedHosts }} + + location /proxyCrl/{{ . }} { + # This indirection is required to make the resolver check the domain. + # Otherwise, broken upstreams lead to broken deployments. + set $backend "{{ . }}"; + + proxy_redirect off; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header Host $backend; + proxy_hide_header Content-Type; + add_header Content-Type application/pkix-crl; + # Prevent caching on client side + add_header Cache-Control 'no-cache, no-store, must-revalidate'; + add_header Pragma 'no-cache'; + add_header Expires '0'; + + proxy_pass "https://$backend/crl"; + } + + {{- end }} + } +{{- end }} diff --git a/charts/smallstep-accomp/templates/step-config.yaml b/charts/smallstep-accomp/templates/step-config.yaml new file mode 100644 index 00000000000..0cb957fa88c --- /dev/null +++ b/charts/smallstep-accomp/templates/step-config.yaml @@ -0,0 +1,9 @@ +{{- if .Values.stepConfig.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: smallstep-step-certificates-config +data: + ca.json: |- + {{(tpl .Values.stepConfig.configTemplate .) | fromYaml | toJson }} +{{- end }} diff --git a/charts/smallstep-accomp/values.yaml b/charts/smallstep-accomp/values.yaml new file mode 100644 index 00000000000..e4e3ad18437 --- /dev/null +++ b/charts/smallstep-accomp/values.yaml @@ -0,0 +1,212 @@ +nginx: + existingServerBlockConfigmap: "smallstep-accomp-server-block" + + service: + type: ClusterIP + + ingress: + enabled: true + # ingressClassName: "nginx" + + # hostname: "acme.alpha.example.com" + path: "/proxyCrl" + pathType: "Prefix" + + # extraTls: + # - + # hosts: [ "acme.alpha.example.com" ] + # secretName: "smallstep-step-certificates-ingress-cert" + + # annotations: + # nginx.ingress.kubernetes.io/cors-allow-origin: https://webapp.acme.alpha.example.com + # nginx.ingress.kubernetes.io/cors-expose-headers: Replay-Nonce, Location + # nginx.ingress.kubernetes.io/enable-cors: 'true' + +upstreams: + enabled: true + # dnsResolver: 9.9.9.9 + + # Note: include the smallstep host of the own domain here as well + proxiedHosts: [] + # proxiedHosts: + # - acme.alpha.example.com + # - acme.beta.example.com + # - acme.gamma.example.com + + +caPassword: + enabled: true + password: "...." + +existingSecrets: + enabled: false + # data: + # ca.key: foobar + +existingCerts: + enabled: false + # data: + # ca.crt: "-----BEGIN CERTIFICATE-----...." + # root_ca.crt: "-----BEGIN CERTIFICATE-----...." + # ca-other2-cross-signed.crt: "-----BEGIN CERTIFICATE-----...." + # ca-other3-cross-signed.crt: "-----BEGIN CERTIFICATE-----...." + +stepConfig: + enabled: true + + address: "0.0.0.0:9000" + + # dnsName: acme.alpha.example.com + + # additionalDNSNames: + # - localhost + + root: /home/step/certs/root_ca.crt + crt: /home/step/certs/ca.crt + key: /home/step/secrets/ca.key + + federatedRoots: + - /home/step/certs/ca.crt + + # federatedRoots: + # - /home/step/certs/ca.crt + # - /home/step/certs/acme.beta.example.com-xsigned-by-acme.alpha.example.com + + db: + badgerFileLoadingMode: "" + dataSource: /home/step/db + type: badgerv2 + + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + maxVersion: 1.3 + minVersion: 1.2 + renegotiation: false + + logger: + format: text + + authority: + claims: + maxTLSCertDuration: 87701h + + # jwk: |- + # { + # "type": "JWK", + # "name": "..example.com", + # "key": { ... }, + # "encryptedKey": "e..." + # } + + acme: + name: keycloakteams + + claims: + allowRenewalAfterExpiry: false + defaultTLSCertDuration: 2160h + disableRenewal: false + maxTLSCertDuration: 2160h + minTLSCertDuration: 60s + + dpop: + # key: + wireDomain: alpha.example.com + + oidc: + clientId: wireapp + # discoveryBaseUrl: https://keycloak.example.com/auth/realms/master + # issuerUrl: https://keycloak.example.com/auth/realms/master?client_id=wireapp + signatureAlgorithms: + - RS256 + - ES256 + - ES384 + - EdDSA + transform: | + { + "name": "{{ .name }}", + "preferred_username": "wireapp://%40{{ .preferred_username }}" + } + + x509: + # organization: alpha.example.com + template: | + { + "subject": { + "organization": {{ required "stepConfig.authority.acme.x509.organization is missing" .Values.stepConfig.authority.acme.x509.organization | toJson }}, + "commonName": {{ "{{" }} toJson .Oidc.name {{ "}}" }} + }, + "uris": [{{ "{{" }} toJson .Oidc.preferred_username {{ "}}" }}, {{ "{{" }} toJson .Dpop.sub {{ "}}" }}], + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["clientAuth"], + "crlDistributionPoints": {{ tpl "[ https://{{ required \"stepConfig.dnsName is missing\" .Values.stepConfig.dnsName }}/crl ]" . | fromYamlArray | toJson }} + } + + configTemplate: |- + address: {{ required "stepConfig.address is missing" .Values.stepConfig.address }} + + dnsNames: + - {{ required "stepConfig.dnsName is missing" .Values.stepConfig.dnsName }} + {{- if .Values.stepConfig.additionalDNSNames }} + {{- .Values.stepConfig.additionalDNSNames | toYaml | nindent 2 }} + {{- end }} + + crt: {{ required "stepConfig.crt is missing" .Values.stepConfig.crt }} + key: {{ required "stepConfig.key is missing" .Values.stepConfig.key }} + root: {{ required "stepConfig.root is missing" .Values.stepConfig.root }} + + federatedRoots: + {{- required "stepConfig.federatedRoots is missing" .Values.stepConfig.federatedRoots | toYaml | nindent 2 }} + + crl: + enabled: true + generateOnRevoke: true + idpURL: https://{{ required "stepConfig.dnsName is missing" .Values.stepConfig.dnsName }}/crl + + db: + {{ required "stepConfig.db is missing" .Values.stepConfig.db | toYaml | nindent 2 }} + + tls: + {{ required "stepConfig.tls is missing" .Values.stepConfig.tls | toYaml | nindent 2 }} + + logger: + {{ required "stepConfig.logger is missing" .Values.stepConfig.logger | toYaml | nindent 2 }} + + authority: + claims: + {{ required "stepConfig.authority.claims is missing" .Values.stepConfig.authority.claims | toYaml | nindent 4 }} + provisioners: + - {{ required "stepConfig.authority.jwk is missing" .Values.stepConfig.authority.jwk | fromJson | toYaml | nindent 6 }} + - name: {{ required "stepConfig.authority.acme.name is missing" .Values.stepConfig.authority.acme.name }} + type: ACME + forceCN: true + challenges: + - wire-oidc-01 + - wire-dpop-01 + claims: + {{ required "stepConfig.authority.acme.claims is missing" .Values.stepConfig.authority.acme.claims | toYaml | nindent 8 }} + options: + wire: + dpop: + key: {{ required "stepConfig.authority.acme.dpop.key is missing" .Values.stepConfig.authority.acme.dpop.key }} + target: https://{{ required "stepConfig.authority.acme.dpop.wireDomain" .Values.stepConfig.authority.acme.dpop.wireDomain }}/clients/{{ "{{" }}.DeviceID{{ "}}" }}/access-token + oidc: + config: + clientId: {{ required "stepConfig.authority.acme.oidc.clientId is missing" .Values.stepConfig.authority.acme.oidc.clientId }} + signatureAlgorithms: + {{ required "stepConfig.authority.acme.oidc.signatureAlgorithms is missing" .Values.stepConfig.authority.acme.oidc.signatureAlgorithms | toYaml | nindent 14 }} + provider: + discoveryBaseUrl: {{ required "stepConfig.authority.acme.oidc.discoveryBaseUrl is missing" .Values.stepConfig.authority.acme.oidc.discoveryBaseUrl }} + id_token_signing_alg_values_supported: + {{ required "stepConfig.authority.acme.oidc.signatureAlgorithms is missing" .Values.stepConfig.authority.acme.oidc.signatureAlgorithms | toYaml | nindent 14 }} + issuerUrl: {{ required "stepConfig.authority.acme.oidc.issuerUrl is missing" .Values.stepConfig.authority.acme.oidc.issuerUrl }} + transform: {{ required "stepConfig.authority.acme.oidc.transform is missing" .Values.stepConfig.authority.acme.oidc.transform | toJson }} + x509: + template: {{ (tpl .Values.stepConfig.authority.acme.x509.template .) | toJson }} + + {{- if .Values.stepConfig.extraConfig }} + {{ .Values.stepConfig.extraconfig | toYaml }} + {{- end }} + + + From 0edaac0cad4c86726754f7d39a29d154ef3b361e Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 12 Aug 2024 15:03:42 +0200 Subject: [PATCH 039/136] Remove smallstep-accomp helm chart (#4204) * Remove smallstep accomp * Add changelog entry * Remove smallstep-accompt from CHARTS_RELEASE --- Makefile | 2 +- changelog.d/5-internal/WPB-10424 | 1 + charts/smallstep-accomp/Chart.yaml | 4 - charts/smallstep-accomp/README.md | 113 ---------- charts/smallstep-accomp/requirements.yaml | 4 - .../smallstep-accomp/templates/_helpers.tpl | 3 - .../templates/ca-password.yaml | 12 - charts/smallstep-accomp/templates/certs.yaml | 13 -- .../smallstep-accomp/templates/secrets.yaml | 14 -- .../templates/server-block-configmap.yaml | 39 ---- .../templates/step-config.yaml | 9 - charts/smallstep-accomp/values.yaml | 212 ------------------ 12 files changed, 2 insertions(+), 424 deletions(-) create mode 100644 changelog.d/5-internal/WPB-10424 delete mode 100644 charts/smallstep-accomp/Chart.yaml delete mode 100644 charts/smallstep-accomp/README.md delete mode 100644 charts/smallstep-accomp/requirements.yaml delete mode 100644 charts/smallstep-accomp/templates/_helpers.tpl delete mode 100644 charts/smallstep-accomp/templates/ca-password.yaml delete mode 100644 charts/smallstep-accomp/templates/certs.yaml delete mode 100644 charts/smallstep-accomp/templates/secrets.yaml delete mode 100644 charts/smallstep-accomp/templates/server-block-configmap.yaml delete mode 100644 charts/smallstep-accomp/templates/step-config.yaml delete mode 100644 charts/smallstep-accomp/values.yaml diff --git a/Makefile b/Makefile index 3dd6227d047..46b6a25dc5e 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice \ calling-test demo-smtp elasticsearch-curator elasticsearch-external \ elasticsearch-ephemeral minio-external cassandra-external \ nginx-ingress-controller ingress-nginx-controller nginx-ingress-services reaper restund coturn \ -k8ssandra-test-cluster postgresql ldap-scim-bridge smallstep-accomp +k8ssandra-test-cluster postgresql ldap-scim-bridge KIND_CLUSTER_NAME := wire-server HELM_PARALLELISM ?= 1 # 1 for sequential tests; 6 for all-parallel tests diff --git a/changelog.d/5-internal/WPB-10424 b/changelog.d/5-internal/WPB-10424 new file mode 100644 index 00000000000..b635cc8d10e --- /dev/null +++ b/changelog.d/5-internal/WPB-10424 @@ -0,0 +1 @@ +Move smallstep-accomp` helm charts to `wireapp/helm-charts` diff --git a/charts/smallstep-accomp/Chart.yaml b/charts/smallstep-accomp/Chart.yaml deleted file mode 100644 index 6dad899102f..00000000000 --- a/charts/smallstep-accomp/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Accompanying chart for Smallstep E2EI support -name: smallstep-accomp -version: 1.0.4 diff --git a/charts/smallstep-accomp/README.md b/charts/smallstep-accomp/README.md deleted file mode 100644 index ad57924296c..00000000000 --- a/charts/smallstep-accomp/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# smallstep-acomp - Helm chart accompanying smallstep - -This Helm chart is meant to be installed alongside the [step-certificates Helm -chart](https://smallstep.github.io/helm-charts) in the same namespace. It has been tested with Helm -chart version `1.25.0` and image - -``` -image: - repository: cr.step.sm/smallstep/step-ca - tag: "0.25.3-rc7" -``` - -This Helm chart provides: - -- A reverse-proxy for Certificate Revocation List (CR) distribution endpoints to federating smallstep - servers -- Smallstep server configuration for the End-to-End Identity setup - - -## Reverse-proxy for CRL distribution points - -This Helm chart installs a reverse proxy that proxies the Certificate Revocation List (CRL) -Distribution Point of the Smallstep servers CRL Certificate Authority (CA) from federating domains -and the own domain. This reverse proxy is required for a working End-to-End Identity setup. - -The Helm chart deploys a nginx server that reverse-proxies -`https:///proxyCrl/` to `https://{other_acme_domain}/crl` -as well as an ingress for the `/proxyCrl` endpoint. For example if `upstreams.proxiedHosts` is set -to `[acme.alpha.example.com, acme.beta.example.com]` and the host for the Smallstep server on the -own domain is `acme.alpha.example.com` this helm chart will forward requests - -- `https://acme.alpha.example.com/proxyCrl/acme.alpha.example.com` to `https://acme.alpha.example.com/crl` -- `https://acme.alpha.example.com/proxyCrl/acme.beta.example.com` to `https://acme.beta.example.com/crl` - -| Name | Description | -| -------------------------- | --------------------------------------------------------------------------------------------------------- | -| `upstreams.enable` | Set to `false` in case you want to write custom nginx server block for the upstream rules | -| `upstreams.dnsResolver` | DNS server that nginx uses to resolve the proxied hostnames | -| `upstreams.proxiedHosts` | List of remote (federated) step-ca hostnames to proxy. Also include the own step-ca host here. | -| `nginx.ingress.enable` | Set to `false` if you need to define a custom ingress for the /proxyCrl endpoint. Make sure CORS is set. | -| `nginx.ingress.hostname` | Hostname of the step-ca server | -| `nginx.ingress.extraTls` | The TLS configuration | -| `nginx.ingress.annotations`| CORS config for the ingress, set the hostname of the step-ca server here | - -For more details on `nginx.*` parameters see README.md documentation in the `nginx` dependency chart. - -## Smallstep server configuration for the End-to-End Identity setup - -This Helm chart helps to create configuration file for step-ca. If `stepConfig.enabled` is `true` a -configmap that contains a `ca.json` will be created. The name of that configmap is compatible with the -step-certificates Helm chart, so that it can be directly used. However since step-ca is deployed -from a seperate Helm release updating and deploying a configuration won't cause an automatic reload -of the step-ca server. It is therefore recommended to manually restart step-ca after configuartion -changes if this Helm chart is used for these purposes. - -For references see: - -- [[1] Configuring `step-ca`](https://smallstep.com/docs/step-ca/configuration/) -- [[2] Configuring `step-ca` Provisioners - ACME for Wire messenger clients ](https://smallstep.com/docs/step-ca/provisioners/#acme-for-wire-messenger-clients) - -| Parameter | Description | -|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| -| `stepConfig.enabled` | Create a configmap with configuration file for `step-certificates` Helm chart. | -| | If `true` then almost all `stepConfig.*` parameters are required. | -| `stepConfig.configTemplate` | Template for the configuration file. Overwrite this if the default value is not generic enough for your use case. | -| `stepConfig.address` | See [1] | -| `stepConfig.dnsName` | Used in `dnsNames` config entry (See [1]) and to define the CRL URL. | -| `stepConfig.additionalDNSNames` | Optional. Additional entries to `dnsNames` configuration field | -| `stepConfig.root` | See [1]. Public key of the Root CA | -| `stepConfig.crt` | See [1]. Public key of the Intermediate CA | -| `stepConfig.key` | See [1]. Private key of the Intermediate CA | -| `stepConfig.federatedRoots` | See [1]. Add all cross-signed Intermediate CA certs from federating domains here. | -| `stepConfig.db` | See [1] | -| `stepConfig.tls` | See [1] | -| `stepConfig.logger` | See [1] | -| `stepConfig.authority.claims` | See [1] | -| `stepConfig.authority.jwk` | JSON string of the JWK provisioner to use. A JWK provisioner can be created | -| | by running `step ca init` then copying it out of the generated `ca.json`, discarding the `ca.json`. | -| `stepConfig.authority.acme.name` | Name of the ACME provisioner. Default: `"keycloakteams"` | -| `stepConfig.authority.acme.claims` | See [1] | -| `stepConfig.authority.acme.dpop.key` | See [2]. Public half of the DPoP signature key bundle configured of the Wire deployment. | -| | Use the same value as `brig.secrets.dpopSigKeyBundle` value of the `wire-server` Helm chart. | -| | Base64 encoded string of the PEM encoded public key. | -| `stepConfig.authority.acme.dpop.wireDomain` | Set this to the federation domain of the backend | -| `stepConfig.authority.acme.oidc.clientId` | Name of the OIDC client. Default: "wireapp". | -| `stepConfig.authority.acme.oidc.discoveryBaseUrl` | OpenID Connect Discovery endpoint. The OIDC provider must respond with its configuration when `/.well-known/openid-configuration` | -| | is appended to the URL. For Keycloak this URL is of format `https:///auth/realms/`. | -| `stepConfig.authority.acme.oidc.issuerUrl` | For Keycloak this must be of the format `https:///auth/realms/?client_id=wireapp` | -| `stepConfig.authority.acme.oidc.signatureAlgorithms` | See [2] | -| `stepConfig.authority.acme.oidc.transform` | See [2]. Has sensible default. There shouldn't be any need to customize this setting. | -| `stepConfig.authority.acme.x509.organization` | Set this to the federation domain of the backend | -| `stepConfig.authority.acme.x509.template` | See [2]. Go template for a client certificate which is evaluated by step-ca. | -| | This string is evaluated as template of the Helm chart first. | -| | Has a sensible default. There shouldn't be a need to customize this setting. | - -| Parameter | Description | -|-----------------------|-------------------------------------------------------------------------------------------------------| -| `caPassword.enabled` | If `true` generate Secret with a name that the `step-certificates` Helm chart will automatically use. | -| | The Helm chart will mount this at `/home/step/secrets/passwords/password`. | -| `caPassword.password` | Password that decrypts the intermediate CA private key | - -| Parameter | Description | -|---------------------------|-------------------------------------------------------------------------------------------------------| -| `existingSecrets.enabled` | If `true` generate Secret with a name that the `step-certificates` Helm chart will automatically use. | -| `existingSecrets.data` | Map from filename to content. Each entry will be mounted as file `/home/step/secrets/` | -| | Add the private key of the Intermediate CA here. | - -| Parameter | Description | -|-------------------------|-----------------------------------------------------------------------------------------------------| -| `existingCerts.enabled` | If `true` generate ConfigMap with a name that the Helm chart will automatically use. | -| `existingCerts.data` | Map from filename to content. Each entry will be mounted as file `/home/step/certs/` | -| `existingCerts.data` | Use it to make public keys of the Root, intermediate CA as well as the cross-signed certs available | -| | to step-ca. Each entry will be mounted as file `/home/step/certs/` | diff --git a/charts/smallstep-accomp/requirements.yaml b/charts/smallstep-accomp/requirements.yaml deleted file mode 100644 index e9d0780c6e9..00000000000 --- a/charts/smallstep-accomp/requirements.yaml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: -- name: nginx - version: 15.10.4 - repository: https://charts.bitnami.com/bitnami diff --git a/charts/smallstep-accomp/templates/_helpers.tpl b/charts/smallstep-accomp/templates/_helpers.tpl deleted file mode 100644 index fb5cb93c9ce..00000000000 --- a/charts/smallstep-accomp/templates/_helpers.tpl +++ /dev/null @@ -1,3 +0,0 @@ -{{- define "fullname" -}} -smallstep-step-certificates -{{- end -}} diff --git a/charts/smallstep-accomp/templates/ca-password.yaml b/charts/smallstep-accomp/templates/ca-password.yaml deleted file mode 100644 index cd1bdc962a9..00000000000 --- a/charts/smallstep-accomp/templates/ca-password.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.caPassword.enabled }} -apiVersion: v1 -kind: Secret -metadata: - name: smallstep-step-certificates-ca-password - labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: "{{ .Release.Name }}" -type: Opaque -data: - password: {{ .Values.caPassword.password | b64enc | quote }} -{{- end }} diff --git a/charts/smallstep-accomp/templates/certs.yaml b/charts/smallstep-accomp/templates/certs.yaml deleted file mode 100644 index c9ef0ce45a9..00000000000 --- a/charts/smallstep-accomp/templates/certs.yaml +++ /dev/null @@ -1,13 +0,0 @@ -{{- if .Values.existingCerts.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: smallstep-step-certificates-certs - labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: "{{ .Release.Name }}" -data: - {{- range $key, $value := .Values.existingCerts.data }} - {{ $key }}: {{ $value | quote }} - {{- end }} -{{- end }} diff --git a/charts/smallstep-accomp/templates/secrets.yaml b/charts/smallstep-accomp/templates/secrets.yaml deleted file mode 100644 index 8448fbc7f8f..00000000000 --- a/charts/smallstep-accomp/templates/secrets.yaml +++ /dev/null @@ -1,14 +0,0 @@ -{{- if .Values.existingSecrets.enabled }} -apiVersion: v1 -kind: Secret -metadata: - name: smallstep-step-certificates-secrets - labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: "{{ .Release.Name }}" -type: Opaque -data: - {{- range $key, $value := .Values.existingSecrets.data }} - {{ $key }}: {{ $value | b64enc | quote }} - {{- end }} -{{- end }} diff --git a/charts/smallstep-accomp/templates/server-block-configmap.yaml b/charts/smallstep-accomp/templates/server-block-configmap.yaml deleted file mode 100644 index 59c423d3345..00000000000 --- a/charts/smallstep-accomp/templates/server-block-configmap.yaml +++ /dev/null @@ -1,39 +0,0 @@ -{{- if and .Values.upstreams.enabled .Values.nginx.existingServerBlockConfigmap }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Values.nginx.existingServerBlockConfigmap }} - labels: - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -data: - server.conf: | - resolver {{ .Values.upstreams.dnsResolver }}; - - server { - listen 0.0.0.0:8080; - - {{- range .Values.upstreams.proxiedHosts }} - - location /proxyCrl/{{ . }} { - # This indirection is required to make the resolver check the domain. - # Otherwise, broken upstreams lead to broken deployments. - set $backend "{{ . }}"; - - proxy_redirect off; - proxy_set_header X-Forwarded-Host $http_host; - proxy_set_header Host $backend; - proxy_hide_header Content-Type; - add_header Content-Type application/pkix-crl; - # Prevent caching on client side - add_header Cache-Control 'no-cache, no-store, must-revalidate'; - add_header Pragma 'no-cache'; - add_header Expires '0'; - - proxy_pass "https://$backend/crl"; - } - - {{- end }} - } -{{- end }} diff --git a/charts/smallstep-accomp/templates/step-config.yaml b/charts/smallstep-accomp/templates/step-config.yaml deleted file mode 100644 index 0cb957fa88c..00000000000 --- a/charts/smallstep-accomp/templates/step-config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -{{- if .Values.stepConfig.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: smallstep-step-certificates-config -data: - ca.json: |- - {{(tpl .Values.stepConfig.configTemplate .) | fromYaml | toJson }} -{{- end }} diff --git a/charts/smallstep-accomp/values.yaml b/charts/smallstep-accomp/values.yaml deleted file mode 100644 index e4e3ad18437..00000000000 --- a/charts/smallstep-accomp/values.yaml +++ /dev/null @@ -1,212 +0,0 @@ -nginx: - existingServerBlockConfigmap: "smallstep-accomp-server-block" - - service: - type: ClusterIP - - ingress: - enabled: true - # ingressClassName: "nginx" - - # hostname: "acme.alpha.example.com" - path: "/proxyCrl" - pathType: "Prefix" - - # extraTls: - # - - # hosts: [ "acme.alpha.example.com" ] - # secretName: "smallstep-step-certificates-ingress-cert" - - # annotations: - # nginx.ingress.kubernetes.io/cors-allow-origin: https://webapp.acme.alpha.example.com - # nginx.ingress.kubernetes.io/cors-expose-headers: Replay-Nonce, Location - # nginx.ingress.kubernetes.io/enable-cors: 'true' - -upstreams: - enabled: true - # dnsResolver: 9.9.9.9 - - # Note: include the smallstep host of the own domain here as well - proxiedHosts: [] - # proxiedHosts: - # - acme.alpha.example.com - # - acme.beta.example.com - # - acme.gamma.example.com - - -caPassword: - enabled: true - password: "...." - -existingSecrets: - enabled: false - # data: - # ca.key: foobar - -existingCerts: - enabled: false - # data: - # ca.crt: "-----BEGIN CERTIFICATE-----...." - # root_ca.crt: "-----BEGIN CERTIFICATE-----...." - # ca-other2-cross-signed.crt: "-----BEGIN CERTIFICATE-----...." - # ca-other3-cross-signed.crt: "-----BEGIN CERTIFICATE-----...." - -stepConfig: - enabled: true - - address: "0.0.0.0:9000" - - # dnsName: acme.alpha.example.com - - # additionalDNSNames: - # - localhost - - root: /home/step/certs/root_ca.crt - crt: /home/step/certs/ca.crt - key: /home/step/secrets/ca.key - - federatedRoots: - - /home/step/certs/ca.crt - - # federatedRoots: - # - /home/step/certs/ca.crt - # - /home/step/certs/acme.beta.example.com-xsigned-by-acme.alpha.example.com - - db: - badgerFileLoadingMode: "" - dataSource: /home/step/db - type: badgerv2 - - tls: - cipherSuites: - - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 - maxVersion: 1.3 - minVersion: 1.2 - renegotiation: false - - logger: - format: text - - authority: - claims: - maxTLSCertDuration: 87701h - - # jwk: |- - # { - # "type": "JWK", - # "name": "..example.com", - # "key": { ... }, - # "encryptedKey": "e..." - # } - - acme: - name: keycloakteams - - claims: - allowRenewalAfterExpiry: false - defaultTLSCertDuration: 2160h - disableRenewal: false - maxTLSCertDuration: 2160h - minTLSCertDuration: 60s - - dpop: - # key: - wireDomain: alpha.example.com - - oidc: - clientId: wireapp - # discoveryBaseUrl: https://keycloak.example.com/auth/realms/master - # issuerUrl: https://keycloak.example.com/auth/realms/master?client_id=wireapp - signatureAlgorithms: - - RS256 - - ES256 - - ES384 - - EdDSA - transform: | - { - "name": "{{ .name }}", - "preferred_username": "wireapp://%40{{ .preferred_username }}" - } - - x509: - # organization: alpha.example.com - template: | - { - "subject": { - "organization": {{ required "stepConfig.authority.acme.x509.organization is missing" .Values.stepConfig.authority.acme.x509.organization | toJson }}, - "commonName": {{ "{{" }} toJson .Oidc.name {{ "}}" }} - }, - "uris": [{{ "{{" }} toJson .Oidc.preferred_username {{ "}}" }}, {{ "{{" }} toJson .Dpop.sub {{ "}}" }}], - "keyUsage": ["digitalSignature"], - "extKeyUsage": ["clientAuth"], - "crlDistributionPoints": {{ tpl "[ https://{{ required \"stepConfig.dnsName is missing\" .Values.stepConfig.dnsName }}/crl ]" . | fromYamlArray | toJson }} - } - - configTemplate: |- - address: {{ required "stepConfig.address is missing" .Values.stepConfig.address }} - - dnsNames: - - {{ required "stepConfig.dnsName is missing" .Values.stepConfig.dnsName }} - {{- if .Values.stepConfig.additionalDNSNames }} - {{- .Values.stepConfig.additionalDNSNames | toYaml | nindent 2 }} - {{- end }} - - crt: {{ required "stepConfig.crt is missing" .Values.stepConfig.crt }} - key: {{ required "stepConfig.key is missing" .Values.stepConfig.key }} - root: {{ required "stepConfig.root is missing" .Values.stepConfig.root }} - - federatedRoots: - {{- required "stepConfig.federatedRoots is missing" .Values.stepConfig.federatedRoots | toYaml | nindent 2 }} - - crl: - enabled: true - generateOnRevoke: true - idpURL: https://{{ required "stepConfig.dnsName is missing" .Values.stepConfig.dnsName }}/crl - - db: - {{ required "stepConfig.db is missing" .Values.stepConfig.db | toYaml | nindent 2 }} - - tls: - {{ required "stepConfig.tls is missing" .Values.stepConfig.tls | toYaml | nindent 2 }} - - logger: - {{ required "stepConfig.logger is missing" .Values.stepConfig.logger | toYaml | nindent 2 }} - - authority: - claims: - {{ required "stepConfig.authority.claims is missing" .Values.stepConfig.authority.claims | toYaml | nindent 4 }} - provisioners: - - {{ required "stepConfig.authority.jwk is missing" .Values.stepConfig.authority.jwk | fromJson | toYaml | nindent 6 }} - - name: {{ required "stepConfig.authority.acme.name is missing" .Values.stepConfig.authority.acme.name }} - type: ACME - forceCN: true - challenges: - - wire-oidc-01 - - wire-dpop-01 - claims: - {{ required "stepConfig.authority.acme.claims is missing" .Values.stepConfig.authority.acme.claims | toYaml | nindent 8 }} - options: - wire: - dpop: - key: {{ required "stepConfig.authority.acme.dpop.key is missing" .Values.stepConfig.authority.acme.dpop.key }} - target: https://{{ required "stepConfig.authority.acme.dpop.wireDomain" .Values.stepConfig.authority.acme.dpop.wireDomain }}/clients/{{ "{{" }}.DeviceID{{ "}}" }}/access-token - oidc: - config: - clientId: {{ required "stepConfig.authority.acme.oidc.clientId is missing" .Values.stepConfig.authority.acme.oidc.clientId }} - signatureAlgorithms: - {{ required "stepConfig.authority.acme.oidc.signatureAlgorithms is missing" .Values.stepConfig.authority.acme.oidc.signatureAlgorithms | toYaml | nindent 14 }} - provider: - discoveryBaseUrl: {{ required "stepConfig.authority.acme.oidc.discoveryBaseUrl is missing" .Values.stepConfig.authority.acme.oidc.discoveryBaseUrl }} - id_token_signing_alg_values_supported: - {{ required "stepConfig.authority.acme.oidc.signatureAlgorithms is missing" .Values.stepConfig.authority.acme.oidc.signatureAlgorithms | toYaml | nindent 14 }} - issuerUrl: {{ required "stepConfig.authority.acme.oidc.issuerUrl is missing" .Values.stepConfig.authority.acme.oidc.issuerUrl }} - transform: {{ required "stepConfig.authority.acme.oidc.transform is missing" .Values.stepConfig.authority.acme.oidc.transform | toJson }} - x509: - template: {{ (tpl .Values.stepConfig.authority.acme.x509.template .) | toJson }} - - {{- if .Values.stepConfig.extraConfig }} - {{ .Values.stepConfig.extraconfig | toYaml }} - {{- end }} - - - From 54f30dc489ee2725d7014bf5d5b8d7724868ca70 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 13 Aug 2024 08:49:59 +0200 Subject: [PATCH 040/136] Feature flag refactoring (part 2) (#4193) * More superclass constraints for IsFeatureConfig * Remove unnecessary error constraint * Abstract single feature internal API * Reorganise deprecated feature flag endpoints * Join Get and Put feature endpoints when possible --- .../5-internal/feature-flag-refactoring-2 | 1 + libs/galley-types/default.nix | 2 - libs/galley-types/galley-types.cabal | 1 - libs/galley-types/src/Galley/Types/Teams.hs | 3 +- libs/wire-api/src/Wire/API/Error.hs | 8 +- .../src/Wire/API/Event/FeatureConfig.hs | 6 +- libs/wire-api/src/Wire/API/Routes/Features.hs | 22 ++ .../src/Wire/API/Routes/Internal/Galley.hs | 195 +++++------------- libs/wire-api/src/Wire/API/Routes/Named.hs | 15 +- .../Wire/API/Routes/Public/Galley/Feature.hs | 166 +++++++-------- libs/wire-api/src/Wire/API/Team/Feature.hs | 33 +-- libs/wire-api/wire-api.cabal | 1 + .../brig/test/integration/API/User/Util.hs | 5 +- services/galley/default.nix | 2 - services/galley/galley.cabal | 1 - services/galley/src/Galley/API/Internal.hs | 106 ++++------ .../galley/src/Galley/API/Public/Feature.hs | 69 ++++--- .../galley/src/Galley/API/Teams/Features.hs | 15 +- services/galley/test/integration/API/Teams.hs | 61 +++--- .../test/integration/API/Util/TeamFeature.hs | 36 ++-- tools/stern/src/Stern/API.hs | 10 +- tools/stern/src/Stern/Intra.hs | 20 +- 22 files changed, 321 insertions(+), 457 deletions(-) create mode 100644 changelog.d/5-internal/feature-flag-refactoring-2 create mode 100644 libs/wire-api/src/Wire/API/Routes/Features.hs diff --git a/changelog.d/5-internal/feature-flag-refactoring-2 b/changelog.d/5-internal/feature-flag-refactoring-2 new file mode 100644 index 00000000000..8c985d1f6b3 --- /dev/null +++ b/changelog.d/5-internal/feature-flag-refactoring-2 @@ -0,0 +1 @@ +Clean up and reorganise feature flag endpoints diff --git a/libs/galley-types/default.nix b/libs/galley-types/default.nix index f977e3444c9..c7b207c15d7 100644 --- a/libs/galley-types/default.nix +++ b/libs/galley-types/default.nix @@ -17,7 +17,6 @@ , lib , memory , QuickCheck -, schema-profunctor , text , types-common , utf8-string @@ -41,7 +40,6 @@ mkDerivation { lens memory QuickCheck - schema-profunctor text types-common utf8-string diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index 7a07066d2e3..2a13877a49c 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -81,7 +81,6 @@ library , lens >=4.12 , memory , QuickCheck - , schema-profunctor , text >=0.11 , types-common >=0.16 , utf8-string diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 23300b55c27..13c2c063653 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -66,7 +66,6 @@ import Data.ByteString (toStrict) import Data.ByteString.UTF8 qualified as UTF8 import Data.Default import Data.Id (UserId) -import Data.Schema qualified as Schema import Data.Set qualified as Set import Imports import Test.QuickCheck (Arbitrary) @@ -153,7 +152,7 @@ instance FromJSON FeatureFlags where <*> (fromMaybe (Defaults def) <$> (obj .:? "enforceFileDownloadLocation")) <*> withImplicitLockStatusOrDefault obj "limitedEventFanout" where - withImplicitLockStatusOrDefault :: forall cfg. (IsFeatureConfig cfg, Schema.ToSchema cfg) => Object -> Key -> A.Parser (Defaults (ImplicitLockStatus cfg)) + withImplicitLockStatusOrDefault :: forall cfg. (IsFeatureConfig cfg) => Object -> Key -> A.Parser (Defaults (ImplicitLockStatus cfg)) withImplicitLockStatusOrDefault obj fieldName = fromMaybe (Defaults (ImplicitLockStatus def)) <$> obj .:? fieldName instance FromJSON FeatureSSO where diff --git a/libs/wire-api/src/Wire/API/Error.hs b/libs/wire-api/src/Wire/API/Error.hs index 275d554a139..f37711ac06f 100644 --- a/libs/wire-api/src/Wire/API/Error.hs +++ b/libs/wire-api/src/Wire/API/Error.hs @@ -164,7 +164,7 @@ instance (KnownError e) => ToSchema (SStaticError e) where data CanThrow e -data CanThrowMany e +data CanThrowMany (es :: [k]) instance (RoutesToPaths api) => RoutesToPaths (CanThrow err :> api) where getRoutes = getRoutes @api @@ -203,18 +203,18 @@ type instance SpecialiseToVersion v (CanThrowMany es :> api) = CanThrowMany es :> SpecialiseToVersion v api -instance (HasOpenApi api) => HasOpenApi (CanThrowMany '() :> api) where +instance (HasOpenApi api) => HasOpenApi (CanThrowMany '[] :> api) where toOpenApi _ = toOpenApi (Proxy @api) instance (HasOpenApi (CanThrowMany es :> api), IsSwaggerError e) => - HasOpenApi (CanThrowMany '(e, es) :> api) + HasOpenApi (CanThrowMany (e : es) :> api) where toOpenApi _ = addToOpenApi @e (toOpenApi (Proxy @(CanThrowMany es :> api))) type family DeclaredErrorEffects api :: EffectRow where DeclaredErrorEffects (CanThrow e :> api) = (ErrorEffect e ': DeclaredErrorEffects api) - DeclaredErrorEffects (CanThrowMany '(e, es) :> api) = + DeclaredErrorEffects (CanThrowMany (e : es) :> api) = DeclaredErrorEffects (CanThrow e :> CanThrowMany es :> api) DeclaredErrorEffects (x :> api) = DeclaredErrorEffects api DeclaredErrorEffects (Named n api) = DeclaredErrorEffects api diff --git a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs index ca1e3fc7534..30242dccf7f 100644 --- a/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs +++ b/libs/wire-api/src/Wire/API/Event/FeatureConfig.hs @@ -28,7 +28,6 @@ import Data.Aeson.KeyMap qualified as KeyMap import Data.Json.Util (ToJSONObject (toJSONObject)) import Data.OpenApi qualified as S import Data.Schema -import GHC.TypeLits (KnownSymbol) import Imports import Test.QuickCheck.Gen import Wire.API.Team.Feature @@ -42,7 +41,7 @@ data Event = Event deriving (Eq, Show, Generic) deriving (A.ToJSON, A.FromJSON) via Schema Event -arbitraryFeature :: forall cfg. (IsFeatureConfig cfg, ToSchema cfg, Arbitrary cfg) => Gen A.Value +arbitraryFeature :: forall cfg. (IsFeatureConfig cfg, Arbitrary cfg) => Gen A.Value arbitraryFeature = toJSON <$> arbitrary @(LockableFeature cfg) class AllArbitraryFeatures cfgs where @@ -53,7 +52,6 @@ instance AllArbitraryFeatures '[] where instance ( IsFeatureConfig cfg, - ToSchema cfg, Arbitrary cfg, AllArbitraryFeatures cfgs ) => @@ -99,5 +97,5 @@ instance ToJSONObject Event where instance S.ToSchema Event where declareNamedSchema = schemaToSwagger -mkUpdateEvent :: forall cfg. (IsFeatureConfig cfg, ToSchema cfg, KnownSymbol (FeatureSymbol cfg)) => LockableFeature cfg -> Event +mkUpdateEvent :: forall cfg. (IsFeatureConfig cfg) => LockableFeature cfg -> Event mkUpdateEvent ws = Event Update (featureName @cfg) (toJSON ws) diff --git a/libs/wire-api/src/Wire/API/Routes/Features.hs b/libs/wire-api/src/Wire/API/Routes/Features.hs new file mode 100644 index 00000000000..d61ab5546aa --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Features.hs @@ -0,0 +1,22 @@ +module Wire.API.Routes.Features where + +import Wire.API.Conversation.Role +import Wire.API.Error.Galley +import Wire.API.Team.Feature + +type family FeatureErrors cfg where + FeatureErrors LegalholdConfig = + '[ 'ActionDenied 'RemoveConversationMember, + 'CannotEnableLegalHoldServiceLargeTeam, + 'LegalHoldNotEnabled, + 'LegalHoldDisableUnimplemented, + 'LegalHoldServiceNotRegistered, + 'UserLegalHoldIllegalOperation, + 'LegalHoldCouldNotBlockConnections + ] + FeatureErrors _ = '[] + +type family FeatureAPIDesc cfg where + FeatureAPIDesc EnforceFileDownloadLocationConfig = + "

Custom feature: only supported on some dedicated on-prem systems.

" + FeatureAPIDesc _ = "" 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 b8f6b73b324..f05386dff80 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -26,7 +26,6 @@ import GHC.TypeLits (AppendSymbol) import Imports hiding (head) import Servant import Servant.OpenApi -import Wire.API.ApplyMods import Wire.API.Bot import Wire.API.Bot.Service import Wire.API.Conversation @@ -38,6 +37,7 @@ import Wire.API.Event.Conversation import Wire.API.FederationStatus import Wire.API.MakesFederatedCall 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 @@ -56,128 +56,40 @@ import Wire.API.Team.Member import Wire.API.Team.SearchVisibility import Wire.API.User.Client -type LegalHoldFeatureStatusChangeErrors = - '( 'ActionDenied 'RemoveConversationMember, - '( AuthenticationError, - '( 'CannotEnableLegalHoldServiceLargeTeam, - '( 'LegalHoldNotEnabled, - '( 'LegalHoldDisableUnimplemented, - '( 'LegalHoldServiceNotRegistered, - '( 'UserLegalHoldIllegalOperation, - '( 'LegalHoldCouldNotBlockConnections, '()) - ) - ) - ) - ) - ) - ) - ) - type LegalHoldFeaturesStatusChangeFederatedCalls = '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent" ] +type family IFeatureAPI1 cfg where + -- special case for classified domains, since it cannot be set + IFeatureAPI1 ClassifiedDomainsConfig = IFeatureStatusGet ClassifiedDomainsConfig + IFeatureAPI1 cfg = IFeatureAPI1Full cfg + +type IFeatureAPI1Full cfg = + IFeatureStatusGet cfg + :<|> IFeatureStatusPut cfg + :<|> IFeatureStatusPatch cfg + +type family IAllFeaturesAPI cfgs where + IAllFeaturesAPI '[cfg] = IFeatureAPI1 cfg + IAllFeaturesAPI (cfg : cfgs) = IFeatureAPI1 cfg :<|> IAllFeaturesAPI cfgs + type IFeatureAPI = - -- SSOConfig - IFeatureStatusGet SSOConfig - :<|> IFeatureStatusPut '[] '() SSOConfig - :<|> IFeatureStatusPatch '[] '() SSOConfig - -- LegalholdConfig - :<|> IFeatureStatusGet LegalholdConfig - :<|> IFeatureStatusPut - LegalHoldFeaturesStatusChangeFederatedCalls - LegalHoldFeatureStatusChangeErrors - LegalholdConfig - :<|> IFeatureStatusPatch - LegalHoldFeaturesStatusChangeFederatedCalls - LegalHoldFeatureStatusChangeErrors - LegalholdConfig - -- SearchVisibilityAvailableConfig - :<|> IFeatureStatusGet SearchVisibilityAvailableConfig - :<|> IFeatureStatusPut '[] '() SearchVisibilityAvailableConfig - :<|> IFeatureStatusPatch '[] '() SearchVisibilityAvailableConfig - -- ValidateSAMLEmailsConfig - :<|> IFeatureStatusGet ValidateSAMLEmailsConfig - :<|> IFeatureStatusPut '[] '() ValidateSAMLEmailsConfig - :<|> IFeatureStatusPatch '[] '() ValidateSAMLEmailsConfig - -- DigitalSignaturesConfig - :<|> IFeatureStatusGet DigitalSignaturesConfig - :<|> IFeatureStatusPut '[] '() DigitalSignaturesConfig - :<|> IFeatureStatusPatch '[] '() DigitalSignaturesConfig - -- AppLockConfig - :<|> IFeatureStatusGet AppLockConfig - :<|> IFeatureStatusPut '[] '() AppLockConfig - :<|> IFeatureStatusPatch '[] '() AppLockConfig - -- FileSharingConfig - :<|> IFeatureStatusGet FileSharingConfig - :<|> IFeatureStatusPut '[] '() FileSharingConfig + IAllFeaturesAPI Features + -- legacy lock status put endpoints :<|> IFeatureStatusLockStatusPut FileSharingConfig - :<|> IFeatureStatusPatch '[] '() FileSharingConfig - -- ConferenceCallingConfig - :<|> IFeatureStatusGet ConferenceCallingConfig - :<|> IFeatureStatusPut '[] '() ConferenceCallingConfig :<|> IFeatureStatusLockStatusPut ConferenceCallingConfig - :<|> IFeatureStatusPatch '[] '() ConferenceCallingConfig - -- SelfDeletingMessagesConfig - :<|> IFeatureStatusGet SelfDeletingMessagesConfig - :<|> IFeatureStatusPut '[] '() SelfDeletingMessagesConfig :<|> IFeatureStatusLockStatusPut SelfDeletingMessagesConfig - :<|> IFeatureStatusPatch '[] '() SelfDeletingMessagesConfig - -- GuestLinksConfig - :<|> IFeatureStatusGet GuestLinksConfig - :<|> IFeatureStatusPut '[] '() GuestLinksConfig :<|> IFeatureStatusLockStatusPut GuestLinksConfig - :<|> IFeatureStatusPatch '[] '() GuestLinksConfig - -- SndFactorPasswordChallengeConfig - :<|> IFeatureStatusGet SndFactorPasswordChallengeConfig - :<|> IFeatureStatusPut '[] '() SndFactorPasswordChallengeConfig :<|> IFeatureStatusLockStatusPut SndFactorPasswordChallengeConfig - :<|> IFeatureStatusPatch '[] '() SndFactorPasswordChallengeConfig - -- SearchVisibilityInboundConfig - :<|> IFeatureStatusGet SearchVisibilityInboundConfig - :<|> IFeatureStatusPut '[] '() SearchVisibilityInboundConfig - :<|> IFeatureStatusPatch '[] '() SearchVisibilityInboundConfig - :<|> IFeatureNoConfigMultiGet SearchVisibilityInboundConfig - -- ClassifiedDomainsConfig - :<|> IFeatureStatusGet ClassifiedDomainsConfig - -- MLSConfig - :<|> IFeatureStatusGet MLSConfig - :<|> IFeatureStatusPut '[] '() MLSConfig - :<|> IFeatureStatusPatch '[] '() MLSConfig :<|> IFeatureStatusLockStatusPut MLSConfig - -- ExposeInvitationURLsToTeamAdminConfig - :<|> IFeatureStatusGet ExposeInvitationURLsToTeamAdminConfig - :<|> IFeatureStatusPut '[] '() ExposeInvitationURLsToTeamAdminConfig - :<|> IFeatureStatusPatch '[] '() ExposeInvitationURLsToTeamAdminConfig - -- SearchVisibilityInboundConfig - :<|> IFeatureStatusGet SearchVisibilityInboundConfig - :<|> IFeatureStatusPut '[] '() SearchVisibilityInboundConfig - :<|> IFeatureStatusPatch '[] '() SearchVisibilityInboundConfig - -- OutlookCalIntegrationConfig - :<|> IFeatureStatusGet OutlookCalIntegrationConfig - :<|> IFeatureStatusPut '[] '() OutlookCalIntegrationConfig - :<|> IFeatureStatusPatch '[] '() OutlookCalIntegrationConfig :<|> IFeatureStatusLockStatusPut OutlookCalIntegrationConfig - -- MlsE2EIdConfig - :<|> IFeatureStatusGet MlsE2EIdConfig - :<|> IFeatureStatusPut '[] '() MlsE2EIdConfig - :<|> IFeatureStatusPatch '[] '() MlsE2EIdConfig :<|> IFeatureStatusLockStatusPut MlsE2EIdConfig - -- MlsMigrationConfig - :<|> IFeatureStatusGet MlsMigrationConfig - :<|> IFeatureStatusPut '[] '() MlsMigrationConfig - :<|> IFeatureStatusPatch '[] '() MlsMigrationConfig :<|> IFeatureStatusLockStatusPut MlsMigrationConfig - -- EnforceFileDownloadLocationConfig - :<|> IFeatureStatusGetWithDesc EnforceFileDownloadLocationConfig "

Custom feature: only supported for some decidated on-prem systems.

" - :<|> IFeatureStatusPutWithDesc '[] '() EnforceFileDownloadLocationConfig "

Custom feature: only supported for some decidated on-prem systems.

" - :<|> IFeatureStatusPatchWithDesc '[] '() EnforceFileDownloadLocationConfig "

Custom feature: only supported for some decidated on-prem systems.

" - :<|> IFeatureStatusLockStatusPutWithDesc EnforceFileDownloadLocationConfig "

Custom feature: only supported for some decidated on-prem systems.

" - -- LimitedEventFanoutConfig - :<|> IFeatureStatusGet LimitedEventFanoutConfig - :<|> IFeatureStatusPut '[] '() LimitedEventFanoutConfig - :<|> IFeatureStatusPatch '[] '() LimitedEventFanoutConfig + :<|> IFeatureStatusLockStatusPut EnforceFileDownloadLocationConfig + -- special endpoints + :<|> IFeatureNoConfigMultiGet SearchVisibilityInboundConfig -- all feature configs :<|> Named "feature-configs-internal" @@ -393,62 +305,67 @@ type ITeamsAPIBase = ) ) -type IFeatureStatusGet f = IFeatureStatusGetWithDesc f "" - -type IFeatureStatusGetWithDesc f desc = Named '("iget", f) (Description desc :> FeatureStatusBaseGet f) - -type IFeatureStatusPut calls errs f = IFeatureStatusPutWithDesc calls errs f "" - -type IFeatureStatusPutWithDesc calls errs f desc = Named '("iput", f) (ApplyMods calls (Description desc :> FeatureStatusBasePutInternal errs f)) +type IFeatureStatusGet cfg = + Named + '("iget", cfg) + ( Description (FeatureAPIDesc cfg) + :> FeatureStatusBaseGet cfg + ) -type IFeatureStatusPatch calls errs f = IFeatureStatusPatchWithDesc calls errs f "" +type IFeatureStatusPut cfg = + Named + '("iput", cfg) + ( Description (FeatureAPIDesc cfg) + :> FeatureStatusBasePutInternal cfg + ) -type IFeatureStatusPatchWithDesc calls errs f desc = Named '("ipatch", f) (ApplyMods calls (Description desc :> FeatureStatusBasePatchInternal errs f)) +type IFeatureStatusPatch cfg = + Named + '("ipatch", cfg) + ( Description (FeatureAPIDesc cfg) + :> FeatureStatusBasePatchInternal cfg + ) -type FeatureStatusBasePutInternal errs featureConfig = +type FeatureStatusBasePutInternal cfg = FeatureStatusBaseInternal - (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) - errs - featureConfig - ( ReqBody '[JSON] (Feature featureConfig) - :> Put '[JSON] (LockableFeature featureConfig) + (AppendSymbol "Put config for " (FeatureSymbol cfg)) + cfg + ( ReqBody '[JSON] (Feature cfg) + :> Put '[JSON] (LockableFeature cfg) ) -type FeatureStatusBasePatchInternal errs featureConfig = +type FeatureStatusBasePatchInternal cfg = FeatureStatusBaseInternal - (AppendSymbol "Patch config for " (FeatureSymbol featureConfig)) - errs - featureConfig - ( ReqBody '[JSON] (LockableFeaturePatch featureConfig) - :> Patch '[JSON] (LockableFeature featureConfig) + (AppendSymbol "Patch config for " (FeatureSymbol cfg)) + cfg + ( ReqBody '[JSON] (LockableFeaturePatch cfg) + :> Patch '[JSON] (LockableFeature cfg) ) -type FeatureStatusBaseInternal desc errs featureConfig a = +type FeatureStatusBaseInternal desc cfg a = Summary desc :> CanThrow OperationDenied :> CanThrow 'NotATeamMember :> CanThrow 'TeamNotFound :> CanThrow TeamFeatureError - :> CanThrowMany errs + :> CanThrowMany (FeatureErrors cfg) :> "teams" :> Capture "tid" TeamId :> "features" - :> FeatureSymbol featureConfig + :> FeatureSymbol cfg :> a -type IFeatureStatusLockStatusPut featureName = IFeatureStatusLockStatusPutWithDesc featureName "" - -type IFeatureStatusLockStatusPutWithDesc featureName desc = +type IFeatureStatusLockStatusPut cfg = Named - '("ilock", featureName) - ( Summary (AppendSymbol "(Un-)lock " (FeatureSymbol featureName)) - :> Description desc + '("ilock", cfg) + ( Summary (AppendSymbol "(Un-)lock " (FeatureSymbol cfg)) + :> Description (FeatureAPIDesc cfg) :> CanThrow 'NotATeamMember :> CanThrow 'TeamNotFound :> "teams" :> Capture "tid" TeamId :> "features" - :> FeatureSymbol featureName + :> FeatureSymbol cfg :> Capture "lockStatus" LockStatus :> Put '[JSON] LockStatusResponse ) diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index d7aad87521a..acfd4e79fae 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -137,9 +137,14 @@ namedClient :: Client m endpoint namedClient = clientIn (Proxy @endpoint) (Proxy @m) ---------------------------------------------- --- Utility to add a combinator to a Named API - +-- | Utility to push a Servant combinator inside Named APIs. +-- +-- For example: +-- @@ +-- From 'V5 ::> (Named "foo" (Get '[JSON] Foo) :<|> Named "bar" (Post '[JSON] Bar)) +-- == +-- Named "foo" (From 'V5 :> Get '[JSON] Foo) :<|> Named "bar" (From 'V5 :> Post '[JSON] Bar) +-- @@ type family x ::> api infixr 4 ::> @@ -147,3 +152,7 @@ infixr 4 ::> type instance x ::> (Named name api) = Named name (x :> api) + +type instance + x ::> (api1 :<|> api2) = + (x ::> api1) :<|> (x ::> api2) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index 5e69d130941..92b015e4062 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -21,12 +21,10 @@ import Data.Id import GHC.TypeLits import Servant import Servant.OpenApi.Internal.Orphans () -import Wire.API.ApplyMods -import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.MakesFederatedCall import Wire.API.OAuth +import Wire.API.Routes.Features import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -34,108 +32,84 @@ import Wire.API.Routes.Version import Wire.API.Team.Feature import Wire.API.Team.SearchVisibility (TeamSearchVisibilityView) +type FeatureAPIGetPut cfg = + FeatureAPIGet cfg :<|> FeatureAPIPut cfg + type FeatureAPI = - FeatureStatusGet SSOConfig - :<|> FeatureStatusGet LegalholdConfig - :<|> FeatureStatusPut - '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent" - ] - '( 'ActionDenied 'RemoveConversationMember, - '( AuthenticationError, - '( 'CannotEnableLegalHoldServiceLargeTeam, - '( 'LegalHoldNotEnabled, - '( 'LegalHoldDisableUnimplemented, - '( 'LegalHoldServiceNotRegistered, - '( 'UserLegalHoldIllegalOperation, - '( 'LegalHoldCouldNotBlockConnections, '()) - ) - ) - ) - ) - ) - ) - ) - LegalholdConfig - :<|> FeatureStatusGet SearchVisibilityAvailableConfig - :<|> FeatureStatusPut '[] '() SearchVisibilityAvailableConfig - :<|> FeatureStatusDeprecatedGet "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" SearchVisibilityAvailableConfig - :<|> FeatureStatusDeprecatedPut "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" SearchVisibilityAvailableConfig + FeatureAPIGet SSOConfig + :<|> FeatureAPIGetPut LegalholdConfig + :<|> FeatureAPIGetPut SearchVisibilityAvailableConfig :<|> SearchVisibilityGet :<|> SearchVisibilitySet - :<|> FeatureStatusGet ValidateSAMLEmailsConfig - :<|> FeatureStatusDeprecatedGet "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" ValidateSAMLEmailsConfig - :<|> FeatureStatusGet DigitalSignaturesConfig - :<|> FeatureStatusDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is potentially used by the old Android client. It is not used by team management, or webapp as of June 2022" DigitalSignaturesConfig - :<|> FeatureStatusGet AppLockConfig - :<|> FeatureStatusPut '[] '() AppLockConfig - :<|> FeatureStatusGet FileSharingConfig - :<|> FeatureStatusPut '[] '() FileSharingConfig - :<|> FeatureStatusGet ClassifiedDomainsConfig - :<|> FeatureStatusGet ConferenceCallingConfig - :<|> FeatureStatusPut '[] '() ConferenceCallingConfig - :<|> FeatureStatusGet SelfDeletingMessagesConfig - :<|> FeatureStatusPut '[] '() SelfDeletingMessagesConfig - :<|> FeatureStatusGet GuestLinksConfig - :<|> FeatureStatusPut '[] '() GuestLinksConfig - :<|> FeatureStatusGet SndFactorPasswordChallengeConfig - :<|> FeatureStatusPut '[] '() SndFactorPasswordChallengeConfig - :<|> From 'V5 ::> FeatureStatusGet MLSConfig - :<|> From 'V5 ::> FeatureStatusPut '[] '() MLSConfig - :<|> FeatureStatusGet ExposeInvitationURLsToTeamAdminConfig - :<|> FeatureStatusPut '[] '() ExposeInvitationURLsToTeamAdminConfig - :<|> FeatureStatusGet SearchVisibilityInboundConfig - :<|> FeatureStatusPut '[] '() SearchVisibilityInboundConfig - :<|> FeatureStatusGet OutlookCalIntegrationConfig - :<|> FeatureStatusPut '[] '() OutlookCalIntegrationConfig - :<|> From 'V5 ::> FeatureStatusGet MlsE2EIdConfig - :<|> From 'V5 ::> Until 'V6 ::> Named "put-MlsE2EIdConfig@v5" (ZUser :> FeatureStatusBasePutPublic '() MlsE2EIdConfig) - :<|> From 'V6 ::> FeatureStatusPut '[] '() MlsE2EIdConfig - :<|> From 'V5 ::> FeatureStatusGet MlsMigrationConfig - :<|> From 'V5 ::> FeatureStatusPut '[] '() MlsMigrationConfig - :<|> From 'V5 - ::> FeatureStatusGetWithDesc - EnforceFileDownloadLocationConfig - "

Custom feature: only supported for some decidated on-prem systems.

" - :<|> From 'V5 - ::> FeatureStatusPutWithDesc - '[] - '() - EnforceFileDownloadLocationConfig - "

Custom feature: only supported for some decidated on-prem systems.

" - :<|> From 'V5 ::> FeatureStatusGet LimitedEventFanoutConfig + :<|> FeatureAPIGet ValidateSAMLEmailsConfig + :<|> FeatureAPIGet DigitalSignaturesConfig + :<|> FeatureAPIGetPut AppLockConfig + :<|> FeatureAPIGetPut FileSharingConfig + :<|> FeatureAPIGet ClassifiedDomainsConfig + :<|> FeatureAPIGetPut ConferenceCallingConfig + :<|> FeatureAPIGetPut SelfDeletingMessagesConfig + :<|> FeatureAPIGetPut GuestLinksConfig + :<|> FeatureAPIGetPut SndFactorPasswordChallengeConfig + :<|> From 'V5 ::> FeatureAPIGetPut MLSConfig + :<|> FeatureAPIGetPut ExposeInvitationURLsToTeamAdminConfig + :<|> FeatureAPIGetPut SearchVisibilityInboundConfig + :<|> FeatureAPIGetPut OutlookCalIntegrationConfig + :<|> From 'V5 ::> FeatureAPIGet MlsE2EIdConfig + :<|> From 'V5 ::> Until 'V6 ::> Named "put-MlsE2EIdConfig@v5" (ZUser :> FeatureStatusBasePutPublic MlsE2EIdConfig) + :<|> From 'V6 ::> FeatureAPIPut MlsE2EIdConfig + :<|> From 'V5 ::> FeatureAPIGetPut MlsMigrationConfig + :<|> From 'V5 ::> FeatureAPIGetPut EnforceFileDownloadLocationConfig + :<|> From 'V5 ::> FeatureAPIGet LimitedEventFanoutConfig :<|> AllFeatureConfigsUserGet :<|> AllFeatureConfigsTeamGet - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" LegalholdConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" SSOConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" SearchVisibilityAvailableConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ValidateSAMLEmailsConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" DigitalSignaturesConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" AppLockConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" FileSharingConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ClassifiedDomainsConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" ConferenceCallingConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" SelfDeletingMessagesConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" GuestLinksConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" SndFactorPasswordChallengeConfig - :<|> FeatureConfigDeprecatedGet "The usage of this endpoint was removed in iOS in version 3.101. It is used by team management, webapp, and potentially the old Android client as of June 2022" MLSConfig + :<|> DeprecatedFeatureAPI + :<|> AllDeprecatedFeatureConfigAPI DeprecatedFeatureConfigs + +type DeprecationNotice1 = "This endpoint is potentially used by the old Android client. It is not used by iOS, team management, or webapp as of June 2022" + +type DeprecationNotice2 = "The usage of this endpoint was removed in iOS in version 3.101. It is not used by team management, or webapp, and is potentially used by the old Android client as of June 2022" + +type DeprecatedFeatureConfigs = + [ LegalholdConfig, + SSOConfig, + SearchVisibilityAvailableConfig, + ValidateSAMLEmailsConfig, + DigitalSignaturesConfig, + AppLockConfig, + FileSharingConfig, + ClassifiedDomainsConfig, + ConferenceCallingConfig, + SelfDeletingMessagesConfig, + GuestLinksConfig, + SndFactorPasswordChallengeConfig, + MLSConfig + ] -type FeatureStatusGet f = FeatureStatusGetWithDesc f "" +type family AllDeprecatedFeatureConfigAPI cfgs where + AllDeprecatedFeatureConfigAPI '[cfg] = FeatureConfigDeprecatedGet DeprecationNotice2 cfg + AllDeprecatedFeatureConfigAPI (cfg : cfgs) = + FeatureConfigDeprecatedGet DeprecationNotice2 cfg + :<|> AllDeprecatedFeatureConfigAPI cfgs -type FeatureStatusGetWithDesc f desc = +type DeprecatedFeatureAPI = + FeatureStatusDeprecatedGet DeprecationNotice1 SearchVisibilityAvailableConfig + :<|> FeatureStatusDeprecatedPut DeprecationNotice1 SearchVisibilityAvailableConfig + :<|> FeatureStatusDeprecatedGet DeprecationNotice1 ValidateSAMLEmailsConfig + :<|> FeatureStatusDeprecatedGet DeprecationNotice2 DigitalSignaturesConfig + +type FeatureAPIGet cfg = Named - '("get", f) - ( Description desc - :> (ZUser :> FeatureStatusBaseGet f) + '("get", cfg) + ( Description (FeatureAPIDesc cfg) + :> (ZUser :> FeatureStatusBaseGet cfg) ) -type FeatureStatusPut segs errs f = FeatureStatusPutWithDesc segs errs f "" - -type FeatureStatusPutWithDesc segs errs f desc = +type FeatureAPIPut cfg = Named - '("put", f) - ( Description desc - :> (ApplyMods segs (ZUser :> FeatureStatusBasePutPublic errs f)) + '("put", cfg) + ( Description (FeatureAPIDesc cfg) + :> ZUser + :> FeatureStatusBasePutPublic cfg ) type FeatureStatusDeprecatedGet d f = @@ -159,13 +133,13 @@ type FeatureStatusBaseGet featureConfig = :> FeatureSymbol featureConfig :> Get '[Servant.JSON] (LockableFeature featureConfig) -type FeatureStatusBasePutPublic errs featureConfig = +type FeatureStatusBasePutPublic featureConfig = Summary (AppendSymbol "Put config for " (FeatureSymbol featureConfig)) :> CanThrow OperationDenied :> CanThrow 'NotATeamMember :> CanThrow 'TeamNotFound :> CanThrow TeamFeatureError - :> CanThrowMany errs + :> CanThrowMany (FeatureErrors featureConfig) :> "teams" :> Capture "tid" TeamId :> "features" diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 095c2fe5f9b..525a8be49dc 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -49,7 +49,7 @@ module Wire.API.Team.Feature genericComputeFeature, IsFeatureConfig (..), FeatureSingleton (..), - HasDeprecatedFeatureName (..), + DeprecatedFeatureName, LockStatusResponse (..), One2OneCalls (..), -- Features @@ -178,7 +178,14 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- 12. Add a section to the documentation at an appropriate place -- (e.g. 'docs/src/developer/reference/config-options.md' (if applicable) or -- 'docs/src/understand/team-feature-settings.md') -class (Default cfg, Default (LockableFeature cfg)) => IsFeatureConfig cfg where +class + ( Default cfg, + ToSchema cfg, + Default (LockableFeature cfg), + KnownSymbol (FeatureSymbol cfg) + ) => + IsFeatureConfig cfg + where type FeatureSymbol cfg :: Symbol featureSingleton :: FeatureSingleton cfg @@ -210,13 +217,12 @@ data FeatureSingleton cfg where FeatureSingletonEnforceFileDownloadLocationConfig :: FeatureSingleton EnforceFileDownloadLocationConfig FeatureSingletonLimitedEventFanoutConfig :: FeatureSingleton LimitedEventFanoutConfig -class HasDeprecatedFeatureName cfg where - type DeprecatedFeatureName cfg :: Symbol +type family DeprecatedFeatureName cfg :: Symbol -featureName :: forall cfg. (KnownSymbol (FeatureSymbol cfg)) => Text +featureName :: forall cfg. (IsFeatureConfig cfg) => Text featureName = T.pack $ symbolVal (Proxy @(FeatureSymbol cfg)) -featureNameBS :: forall cfg. (KnownSymbol (FeatureSymbol cfg)) => ByteString +featureNameBS :: forall cfg. (IsFeatureConfig cfg) => ByteString featureNameBS = UTF8.fromString $ symbolVal (Proxy @(FeatureSymbol cfg)) -------------------------------------------------------------------------------- @@ -289,7 +295,7 @@ defUnlockedFeature = config = def } -instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (LockableFeature cfg) where +instance (IsFeatureConfig cfg) => ToSchema (LockableFeature cfg) where schema = object name $ LockableFeature @@ -651,8 +657,7 @@ instance IsFeatureConfig SearchVisibilityAvailableConfig where instance ToSchema SearchVisibilityAvailableConfig where schema = object "SearchVisibilityAvailableConfig" objectSchema -instance HasDeprecatedFeatureName SearchVisibilityAvailableConfig where - type DeprecatedFeatureName SearchVisibilityAvailableConfig = "search-visibility" +type instance DeprecatedFeatureName SearchVisibilityAvailableConfig = "search-visibility" -------------------------------------------------------------------------------- -- ValidateSAMLEmails feature @@ -679,8 +684,7 @@ instance IsFeatureConfig ValidateSAMLEmailsConfig where featureSingleton = FeatureSingletonValidateSAMLEmailsConfig objectSchema = pure ValidateSAMLEmailsConfig -instance HasDeprecatedFeatureName ValidateSAMLEmailsConfig where - type DeprecatedFeatureName ValidateSAMLEmailsConfig = "validate-saml-emails" +type instance DeprecatedFeatureName ValidateSAMLEmailsConfig = "validate-saml-emails" -------------------------------------------------------------------------------- -- DigitalSignatures feature @@ -704,8 +708,7 @@ instance IsFeatureConfig DigitalSignaturesConfig where featureSingleton = FeatureSingletonDigitalSignaturesConfig objectSchema = pure DigitalSignaturesConfig -instance HasDeprecatedFeatureName DigitalSignaturesConfig where - type DeprecatedFeatureName DigitalSignaturesConfig = "digital-signatures" +type instance DeprecatedFeatureName DigitalSignaturesConfig = "digital-signatures" instance ToSchema DigitalSignaturesConfig where schema = object "DigitalSignaturesConfig" objectSchema @@ -1310,9 +1313,9 @@ instance (HObjectSchema c xs, c x) => HObjectSchema c ((x :: Type) : xs) where hobjectSchema f = (:*) <$> hd .= f <*> tl .= hobjectSchema @c @xs f -- | constraint synonym for 'ToSchema' 'AllFeatureConfigs' -class (IsFeatureConfig cfg, ToSchema cfg, KnownSymbol (FeatureSymbol cfg)) => FeatureFieldConstraints cfg +class (IsFeatureConfig cfg, ToSchema cfg) => FeatureFieldConstraints cfg -instance (IsFeatureConfig cfg, ToSchema cfg, KnownSymbol (FeatureSymbol cfg)) => FeatureFieldConstraints cfg +instance (IsFeatureConfig cfg, ToSchema cfg) => FeatureFieldConstraints cfg instance ToSchema AllFeatureConfigs where schema = diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index e875f415f6c..3d84b9414dc 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -151,6 +151,7 @@ library Wire.API.Routes.ClientAlgebra Wire.API.Routes.Cookies Wire.API.Routes.CSV + Wire.API.Routes.Features Wire.API.Routes.FederationDomainConfig Wire.API.Routes.Internal.Brig Wire.API.Routes.Internal.Brig.Connection diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 8a1c1004ad9..c5a77e3de7b 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -47,7 +47,6 @@ import Data.String.Conversions import Data.Text.Ascii qualified as Ascii import Data.Vector qualified as Vec import Data.ZAuth.Token qualified as ZAuth -import GHC.TypeLits (KnownSymbol) import Imports import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit @@ -61,7 +60,7 @@ import Wire.API.Federation.Component import Wire.API.Internal.Notification (Notification (..)) import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.MultiTablePaging (LocalOrRemoteTable, MultiTablePagingState) -import Wire.API.Team.Feature (featureNameBS) +import Wire.API.Team.Feature (IsFeatureConfig, featureNameBS) import Wire.API.Team.Feature qualified as Public import Wire.API.User import Wire.API.User qualified as Public @@ -450,7 +449,7 @@ setTeamFeatureLockStatus :: MonadIO m, MonadHttp m, HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg) + IsFeatureConfig cfg ) => Galley -> TeamId -> diff --git a/services/galley/default.nix b/services/galley/default.nix index c3ef5ddd9e3..6e1d3c62f7a 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -80,7 +80,6 @@ , retry , safe-exceptions , saml2-web-sso -, schema-profunctor , servant , servant-client , servant-client-core @@ -191,7 +190,6 @@ mkDerivation { retry safe-exceptions saml2-web-sso - schema-profunctor servant servant-client servant-server diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 3437caf3795..5bd145d2eb3 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -340,7 +340,6 @@ library , retry >=0.5 , safe-exceptions >=0.1 , saml2-web-sso >=0.20 - , schema-profunctor , servant , servant-client , servant-server diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 7dc40d9c289..ebb62143d63 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE PartialTypeSignatures #-} +{-# OPTIONS_GHC -Wno-partial-type-signatures #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -42,13 +45,13 @@ import Galley.API.MLS.Removal import Galley.API.One2One import Galley.API.Public.Servant import Galley.API.Query qualified as Query -import Galley.API.Teams (uncheckedDeleteTeamMember) +import Galley.API.Teams import Galley.API.Teams qualified as Teams import Galley.API.Teams.Features +import Galley.API.Teams.Features.Get import Galley.API.Update qualified as Update import Galley.API.Util import Galley.App -import Galley.Cassandra.TeamFeatures (getAllFeatureConfigsForServer) import Galley.Data.Conversation qualified as Data import Galley.Effects import Galley.Effects.BackendNotificationQueueAccess @@ -231,80 +234,55 @@ miscAPI = <@> mkNamedAPI @"put-custom-backend" setCustomBackend <@> mkNamedAPI @"delete-custom-backend" deleteCustomBackend +featureAPI1Full :: + forall cfg r. + (_) => + API (IFeatureAPI1Full cfg) r +featureAPI1Full = + mkNamedAPI @'("iget", cfg) (getFeatureStatus DontDoAuth) + <@> mkNamedAPI @'("iput", cfg) setFeatureStatusInternal + <@> mkNamedAPI @'("ipatch", cfg) patchFeatureStatusInternal + +allFeaturesAPI :: API (IAllFeaturesAPI Features) GalleyEffects +allFeaturesAPI = + featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> mkNamedAPI @'("iget", ClassifiedDomainsConfig) (getFeatureStatus DontDoAuth) + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + <@> featureAPI1Full + featureAPI :: API IFeatureAPI GalleyEffects featureAPI = - mkNamedAPI @'("iget", SSOConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SSOConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", SSOConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", LegalholdConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", LegalholdConfig) (callsFed (exposeAnnotations setFeatureStatusInternal)) - <@> mkNamedAPI @'("ipatch", LegalholdConfig) (callsFed (exposeAnnotations patchFeatureStatusInternal)) - <@> mkNamedAPI @'("iget", SearchVisibilityAvailableConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SearchVisibilityAvailableConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", SearchVisibilityAvailableConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", ValidateSAMLEmailsConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", ValidateSAMLEmailsConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", ValidateSAMLEmailsConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", DigitalSignaturesConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", DigitalSignaturesConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", DigitalSignaturesConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", AppLockConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", AppLockConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", AppLockConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", FileSharingConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", FileSharingConfig) setFeatureStatusInternal + allFeaturesAPI + -- legacy endpoints <@> mkNamedAPI @'("ilock", FileSharingConfig) (updateLockStatus @FileSharingConfig) - <@> mkNamedAPI @'("ipatch", FileSharingConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", ConferenceCallingConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", ConferenceCallingConfig) setFeatureStatusInternal <@> mkNamedAPI @'("ilock", ConferenceCallingConfig) (updateLockStatus @ConferenceCallingConfig) - <@> mkNamedAPI @'("ipatch", ConferenceCallingConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", SelfDeletingMessagesConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SelfDeletingMessagesConfig) setFeatureStatusInternal <@> mkNamedAPI @'("ilock", SelfDeletingMessagesConfig) (updateLockStatus @SelfDeletingMessagesConfig) - <@> mkNamedAPI @'("ipatch", SelfDeletingMessagesConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", GuestLinksConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", GuestLinksConfig) setFeatureStatusInternal <@> mkNamedAPI @'("ilock", GuestLinksConfig) (updateLockStatus @GuestLinksConfig) - <@> mkNamedAPI @'("ipatch", GuestLinksConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", SndFactorPasswordChallengeConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SndFactorPasswordChallengeConfig) setFeatureStatusInternal <@> mkNamedAPI @'("ilock", SndFactorPasswordChallengeConfig) (updateLockStatus @SndFactorPasswordChallengeConfig) - <@> mkNamedAPI @'("ipatch", SndFactorPasswordChallengeConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", SearchVisibilityInboundConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SearchVisibilityInboundConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", SearchVisibilityInboundConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("igetmulti", SearchVisibilityInboundConfig) getFeatureStatusMulti - <@> mkNamedAPI @'("iget", ClassifiedDomainsConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iget", MLSConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", MLSConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", MLSConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("ilock", MLSConfig) (updateLockStatus @MLSConfig) - <@> mkNamedAPI @'("iget", ExposeInvitationURLsToTeamAdminConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", ExposeInvitationURLsToTeamAdminConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", ExposeInvitationURLsToTeamAdminConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", SearchVisibilityInboundConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", SearchVisibilityInboundConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", SearchVisibilityInboundConfig) patchFeatureStatusInternal - <@> mkNamedAPI @'("iget", OutlookCalIntegrationConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", OutlookCalIntegrationConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", OutlookCalIntegrationConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("ilock", OutlookCalIntegrationConfig) (updateLockStatus @OutlookCalIntegrationConfig) - <@> mkNamedAPI @'("iget", MlsE2EIdConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", MlsE2EIdConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", MlsE2EIdConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("ilock", MlsE2EIdConfig) (updateLockStatus @MlsE2EIdConfig) - <@> mkNamedAPI @'("iget", MlsMigrationConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", MlsMigrationConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", MlsMigrationConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("ilock", MlsMigrationConfig) (updateLockStatus @MlsMigrationConfig) - <@> mkNamedAPI @'("iget", EnforceFileDownloadLocationConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", EnforceFileDownloadLocationConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", EnforceFileDownloadLocationConfig) patchFeatureStatusInternal <@> mkNamedAPI @'("ilock", EnforceFileDownloadLocationConfig) (updateLockStatus @EnforceFileDownloadLocationConfig) - <@> mkNamedAPI @'("iget", LimitedEventFanoutConfig) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", LimitedEventFanoutConfig) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", LimitedEventFanoutConfig) patchFeatureStatusInternal + -- special endpoints + <@> mkNamedAPI @'("igetmulti", SearchVisibilityInboundConfig) getFeatureStatusMulti + -- all features <@> mkNamedAPI @"feature-configs-internal" (maybe getAllFeatureConfigsForServer getAllFeatureConfigsForUser) rmUser :: diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index d19ddefe523..45703385cc4 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE PartialTypeSignatures #-} +{-# OPTIONS_GHC -Wno-partial-type-signatures #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -22,58 +25,56 @@ import Galley.API.Teams.Features import Galley.API.Teams.Features.Get import Galley.App import Imports -import Wire.API.Federation.API import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Feature import Wire.API.Team.Feature +featureAPIGetPut :: forall cfg r. (_) => API (FeatureAPIGetPut cfg) r +featureAPIGetPut = + mkNamedAPI @'("get", cfg) (getFeatureStatus . DoAuth) + <@> mkNamedAPI @'("put", cfg) (setFeatureStatus . DoAuth) + featureAPI :: API FeatureAPI GalleyEffects featureAPI = mkNamedAPI @'("get", SSOConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", LegalholdConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", LegalholdConfig) (callsFed (exposeAnnotations (setFeatureStatus . DoAuth))) - <@> mkNamedAPI @'("get", SearchVisibilityAvailableConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", SearchVisibilityAvailableConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get-deprecated", SearchVisibilityAvailableConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put-deprecated", SearchVisibilityAvailableConfig) (setFeatureStatus . DoAuth) + <@> featureAPIGetPut + <@> featureAPIGetPut <@> mkNamedAPI @"get-search-visibility" getSearchVisibility <@> mkNamedAPI @"set-search-visibility" (setSearchVisibility (featureEnabledForTeam @SearchVisibilityAvailableConfig)) <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus . DoAuth) <@> mkNamedAPI @'("get", DigitalSignaturesConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get-deprecated", DigitalSignaturesConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", AppLockConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", AppLockConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", FileSharingConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", FileSharingConfig) (setFeatureStatus . DoAuth) + <@> featureAPIGetPut + <@> featureAPIGetPut <@> mkNamedAPI @'("get", ClassifiedDomainsConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", ConferenceCallingConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", ConferenceCallingConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", SelfDeletingMessagesConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", SelfDeletingMessagesConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", GuestLinksConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", GuestLinksConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", SndFactorPasswordChallengeConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", SndFactorPasswordChallengeConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", MLSConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", MLSConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", ExposeInvitationURLsToTeamAdminConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", ExposeInvitationURLsToTeamAdminConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", SearchVisibilityInboundConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", SearchVisibilityInboundConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", OutlookCalIntegrationConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", OutlookCalIntegrationConfig) (setFeatureStatus . DoAuth) + <@> featureAPIGetPut + <@> featureAPIGetPut + <@> featureAPIGetPut + <@> featureAPIGetPut + <@> hoistAPI id featureAPIGetPut + <@> featureAPIGetPut + <@> featureAPIGetPut + <@> featureAPIGetPut <@> mkNamedAPI @'("get", MlsE2EIdConfig) (getFeatureStatus . DoAuth) <@> mkNamedAPI @"put-MlsE2EIdConfig@v5" (setFeatureStatus . DoAuth) <@> mkNamedAPI @'("put", MlsE2EIdConfig) (guardMlsE2EIdConfig (setFeatureStatus . DoAuth)) - <@> mkNamedAPI @'("get", MlsMigrationConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", MlsMigrationConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", EnforceFileDownloadLocationConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", EnforceFileDownloadLocationConfig) (setFeatureStatus . DoAuth) + <@> hoistAPI id featureAPIGetPut + <@> hoistAPI id featureAPIGetPut <@> mkNamedAPI @'("get", LimitedEventFanoutConfig) (getFeatureStatus . DoAuth) <@> mkNamedAPI @"get-all-feature-configs-for-user" getAllFeatureConfigsForUser <@> mkNamedAPI @"get-all-feature-configs-for-team" getAllFeatureConfigsForTeam - <@> mkNamedAPI @'("get-config", LegalholdConfig) getSingleFeatureConfigForUser + <@> deprecatedFeatureConfigAPI + <@> deprecatedFeatureAPI + +deprecatedFeatureConfigAPI :: API DeprecatedFeatureAPI GalleyEffects +deprecatedFeatureConfigAPI = + mkNamedAPI @'("get-deprecated", SearchVisibilityAvailableConfig) (getFeatureStatus . DoAuth) + <@> mkNamedAPI @'("put-deprecated", SearchVisibilityAvailableConfig) (setFeatureStatus . DoAuth) + <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus . DoAuth) + <@> mkNamedAPI @'("get-deprecated", DigitalSignaturesConfig) (getFeatureStatus . DoAuth) + +deprecatedFeatureAPI :: API (AllDeprecatedFeatureConfigAPI DeprecatedFeatureConfigs) GalleyEffects +deprecatedFeatureAPI = + mkNamedAPI @'("get-config", LegalholdConfig) getSingleFeatureConfigForUser <@> mkNamedAPI @'("get-config", SSOConfig) getSingleFeatureConfigForUser <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) getSingleFeatureConfigForUser <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) getSingleFeatureConfigForUser diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 38d16da2f45..5f31df7e247 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -26,10 +26,8 @@ module Galley.API.Teams.Features getAllFeatureConfigsForTeam, getAllFeatureConfigsForUser, updateLockStatus, - -- Don't export methods of this typeclass - GetFeatureConfig, - -- Don't export methods of this typeclass - SetFeatureConfig, + GetFeatureConfig (..), + SetFeatureConfig (..), guardSecondFactorDisabled, DoAuth (..), featureEnabledForTeam, @@ -44,9 +42,7 @@ import Data.Id import Data.Json.Util import Data.Kind import Data.Qualified (Local) -import Data.Schema import Data.Time (UTCTime) -import GHC.TypeLits (KnownSymbol) import Galley.API.Error (InternalError) import Galley.API.LegalHold qualified as LegalHold import Galley.API.Teams (ensureNotTooLargeToActivateLegalHold) @@ -176,9 +172,7 @@ updateLockStatus tid lockStatus = do persistAndPushEvent :: forall cfg r. - ( KnownSymbol (FeatureSymbol cfg), - ToSchema cfg, - GetFeatureConfig cfg, + ( GetFeatureConfig cfg, ComputeFeatureConstraints cfg r, Member (Input Opts) r, Member TeamFeatureStore r, @@ -252,8 +246,6 @@ class (GetFeatureConfig cfg) => SetFeatureConfig cfg where Sem r (LockableFeature cfg) default setConfigForTeam :: ( ComputeFeatureConstraints cfg r, - KnownSymbol (FeatureSymbol cfg), - ToSchema cfg, Member (Input Opts) r, Member TeamFeatureStore r, Member (P.Logger (Log.Msg -> Log.Msg)) r, @@ -304,7 +296,6 @@ instance SetFeatureConfig LegalholdConfig where Member BrigAccess r, Member CodeStore r, Member ConversationStore r, - Member (Error AuthenticationError) r, Member (Error FederationError) r, Member (Error InternalError) r, Member (ErrorS ('ActionDenied 'RemoveConversationMember)) r, diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index 72bd68fa8e5..b318c0358bc 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -57,7 +57,6 @@ import Data.UUID qualified as UUID import Data.UUID.Util qualified as UUID import Data.UUID.V1 qualified as UUID import Data.Vector qualified as V -import GHC.TypeLits (KnownSymbol) import Galley.Env qualified as Galley import Galley.Options (featureFlags, maxConvSize, maxFanoutSize, settings) import Galley.Types.Conversations.Roles @@ -84,7 +83,7 @@ import Wire.API.Routes.Internal.Galley.TeamsIntra as TeamsIntra import Wire.API.Routes.Version import Wire.API.Team import Wire.API.Team.Export (TeamExportUser (..)) -import Wire.API.Team.Feature qualified as Public +import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Member qualified as Member import Wire.API.Team.Member qualified as TM @@ -396,9 +395,9 @@ testEnableSSOPerTeam = do owner <- Util.randomUser tid <- Util.createBindingTeamInternal "foo" owner assertTeamActivate "create team" tid - let check :: (HasCallStack) => String -> Public.FeatureStatus -> TestM () + let check :: (HasCallStack) => String -> FeatureStatus -> TestM () check msg enabledness = do - feat :: Public.Feature Public.SSOConfig <- responseJsonUnsafe <$> (getSSOEnabledInternal tid (getSSOEnabledInternal tid TestM () putSSOEnabledInternalCheckNotImplemented = do @@ -408,25 +407,25 @@ testEnableSSOPerTeam = do <$> put ( g . paths ["i", "teams", toByteString' tid, "features", "sso"] - . json (Public.Feature Public.FeatureStatusDisabled Public.SSOConfig) + . json (Feature FeatureStatusDisabled SSOConfig) ) liftIO $ do assertEqual "bad status" status403 (Wai.code waierr) assertEqual "bad label" "not-implemented" (Wai.label waierr) featureSSO <- view (tsGConf . settings . featureFlags . flagSSO) case featureSSO of - FeatureSSOEnabledByDefault -> check "Teams should start with SSO enabled" Public.FeatureStatusEnabled - FeatureSSODisabledByDefault -> check "Teams should start with SSO disabled" Public.FeatureStatusDisabled - putSSOEnabledInternal tid Public.FeatureStatusEnabled - check "Calling 'putEnabled True' should enable SSO" Public.FeatureStatusEnabled + FeatureSSOEnabledByDefault -> check "Teams should start with SSO enabled" FeatureStatusEnabled + FeatureSSODisabledByDefault -> check "Teams should start with SSO disabled" FeatureStatusDisabled + putSSOEnabledInternal tid FeatureStatusEnabled + check "Calling 'putEnabled True' should enable SSO" FeatureStatusEnabled putSSOEnabledInternalCheckNotImplemented testEnableTeamSearchVisibilityPerTeam :: TestM () testEnableTeamSearchVisibilityPerTeam = do (tid, owner, member : _) <- Util.createBindingTeamWithMembers 2 - let check :: String -> Public.FeatureStatus -> TestM () + let check :: String -> FeatureStatus -> TestM () check msg enabledness = do - feat :: Public.Feature Public.SearchVisibilityAvailableConfig <- responseJsonUnsafe <$> (Util.getTeamFeatureInternal @Public.SearchVisibilityAvailableConfig tid (Util.getTeamFeatureInternal @SearchVisibilityAvailableConfig tid TeamId -> Public.LockStatus -> TestM () +setFeatureLockStatus :: forall cfg. (IsFeatureConfig cfg) => TeamId -> LockStatus -> TestM () setFeatureLockStatus tid status = do g <- viewGalley - put (g . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg, toByteString' status]) !!! const 200 === statusCode + put (g . paths ["i", "teams", toByteString' tid, "features", featureNameBS @cfg, toByteString' status]) !!! const 200 === statusCode generateVerificationCode :: Public.SendVerificationCode -> TestM () generateVerificationCode req = do @@ -1129,11 +1128,11 @@ generateVerificationCode req = do let js = RequestBodyLBS $ encode req post (brig . paths ["verification-code", "send"] . contentJson . body js) !!! const 200 === statusCode -setTeamSndFactorPasswordChallenge :: TeamId -> Public.FeatureStatus -> TestM () +setTeamSndFactorPasswordChallenge :: TeamId -> FeatureStatus -> TestM () setTeamSndFactorPasswordChallenge tid status = do g <- viewGalley - let js = RequestBodyLBS $ encode $ Public.Feature status Public.SndFactorPasswordChallengeConfig - put (g . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @Public.SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode + let js = RequestBodyLBS $ encode $ Feature status SndFactorPasswordChallengeConfig + put (g . paths ["i", "teams", toByteString' tid, "features", featureNameBS @SndFactorPasswordChallengeConfig] . contentJson . body js) !!! const 200 === statusCode getVerificationCode :: UserId -> Public.VerificationAction -> TestM Code.Value getVerificationCode uid action = do @@ -1739,11 +1738,11 @@ newTeamMember' perms uid = Member.mkTeamMember uid perms Nothing LH.defUserLegal -- and with different kinds of internal checks, it's quite tedious to do so. getSSOEnabledInternal :: (HasCallStack) => TeamId -> TestM ResponseLBS -getSSOEnabledInternal = Util.getTeamFeatureInternal @Public.SSOConfig +getSSOEnabledInternal = Util.getTeamFeatureInternal @SSOConfig -putSSOEnabledInternal :: (HasCallStack) => TeamId -> Public.FeatureStatus -> TestM () +putSSOEnabledInternal :: (HasCallStack) => TeamId -> FeatureStatus -> TestM () putSSOEnabledInternal tid statusValue = - void $ Util.putTeamFeatureInternal @Public.SSOConfig expect2xx tid (Public.Feature statusValue Public.SSOConfig) + void $ Util.putTeamFeatureInternal @SSOConfig expect2xx tid (Feature statusValue SSOConfig) getSearchVisibility :: (HasCallStack) => (Request -> Request) -> UserId -> TeamId -> (MonadHttp m) => m ResponseLBS getSearchVisibility g uid tid = do diff --git a/services/galley/test/integration/API/Util/TeamFeature.hs b/services/galley/test/integration/API/Util/TeamFeature.hs index 873eb4e51ea..6d60c2bdb7a 100644 --- a/services/galley/test/integration/API/Util/TeamFeature.hs +++ b/services/galley/test/integration/API/Util/TeamFeature.hs @@ -25,15 +25,13 @@ import API.Util (HasGalley (viewGalley), zUser) import API.Util qualified as Util import Bilge import Control.Lens ((.~)) -import Data.Aeson (ToJSON) import Data.ByteString.Conversion (toByteString') import Data.Id (ConvId, TeamId, UserId) -import GHC.TypeLits (KnownSymbol) import Galley.Options (featureFlags, settings) import Galley.Types.Teams import Imports import TestSetup -import Wire.API.Team.Feature qualified as Public +import Wire.API.Team.Feature withCustomSearchFeature :: FeatureTeamSearchVisibilityAvailability -> TestM () -> TestM () withCustomSearchFeature flag action = do @@ -42,15 +40,15 @@ withCustomSearchFeature flag action = do putTeamSearchVisibilityAvailableInternal :: (HasCallStack) => TeamId -> - Public.FeatureStatus -> + FeatureStatus -> (MonadIO m, MonadHttp m, HasGalley m) => m () putTeamSearchVisibilityAvailableInternal tid statusValue = void $ putTeamFeatureInternal - @Public.SearchVisibilityAvailableConfig + @SearchVisibilityAvailableConfig expect2xx tid - (Public.Feature statusValue Public.SearchVisibilityAvailableConfig) + (Feature statusValue SearchVisibilityAvailableConfig) putTeamFeatureInternal :: forall cfg m. @@ -58,36 +56,32 @@ putTeamFeatureInternal :: HasGalley m, MonadHttp m, HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg), - ToJSON (Public.Feature cfg) + IsFeatureConfig cfg ) => (Request -> Request) -> TeamId -> - Public.Feature cfg -> + Feature cfg -> m ResponseLBS putTeamFeatureInternal reqmod tid status = do galley <- viewGalley put $ galley - . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . paths ["i", "teams", toByteString' tid, "features", featureNameBS @cfg] . json status . reqmod putTeamFeature :: forall cfg. - ( HasCallStack, - KnownSymbol (Public.FeatureSymbol cfg), - ToJSON (Public.Feature cfg) - ) => + (HasCallStack, IsFeatureConfig cfg) => UserId -> TeamId -> - Public.Feature cfg -> + Feature cfg -> TestM ResponseLBS putTeamFeature uid tid status = do galley <- viewGalley put $ galley - . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . paths ["teams", toByteString' tid, "features", featureNameBS @cfg] . json status . zUser uid @@ -100,23 +94,23 @@ getGuestLinkStatus :: getGuestLinkStatus galley u cid = get $ galley - . paths ["conversations", toByteString' cid, "features", Public.featureNameBS @Public.GuestLinksConfig] + . paths ["conversations", toByteString' cid, "features", featureNameBS @GuestLinksConfig] . zUser u getTeamFeatureInternal :: forall cfg m. - (HasGalley m, MonadIO m, MonadHttp m, KnownSymbol (Public.FeatureSymbol cfg)) => + (HasGalley m, MonadIO m, MonadHttp m, IsFeatureConfig cfg) => TeamId -> m ResponseLBS getTeamFeatureInternal tid = do g <- viewGalley get $ g - . paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . paths ["i", "teams", toByteString' tid, "features", featureNameBS @cfg] getTeamFeature :: forall cfg m. - (HasGalley m, MonadIO m, MonadHttp m, HasCallStack, KnownSymbol (Public.FeatureSymbol cfg)) => + (HasGalley m, MonadIO m, MonadHttp m, HasCallStack, IsFeatureConfig cfg) => UserId -> TeamId -> m ResponseLBS @@ -124,5 +118,5 @@ getTeamFeature uid tid = do galley <- viewGalley get $ galley - . paths ["teams", toByteString' tid, "features", Public.featureNameBS @cfg] + . paths ["teams", toByteString' tid, "features", featureNameBS @cfg] . zUser uid diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 611b366ca08..5011becb775 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -308,20 +308,14 @@ getTeamAdminInfo = fmap toAdminInfo . Intra.getTeamInfo mkFeatureGetRoute :: forall cfg. - ( IsFeatureConfig cfg, - ToSchema cfg, - KnownSymbol (FeatureSymbol cfg), - Typeable cfg - ) => + (IsFeatureConfig cfg, Typeable cfg) => TeamId -> Handler (LockableFeature cfg) mkFeatureGetRoute = Intra.getTeamFeatureFlag @cfg mkFeaturePutRoute :: forall cfg. - ( KnownSymbol (FeatureSymbol cfg), - ToJSON (Feature cfg) - ) => + (IsFeatureConfig cfg) => TeamId -> Feature cfg -> Handler NoContent diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 14e2c62e1fc..8051169cfc3 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -86,14 +86,12 @@ import Data.Id import Data.Int import Data.List.Split (chunksOf) import Data.Map qualified as Map -import Data.Proxy (Proxy (Proxy)) import Data.Qualified (qUnqualified) import Data.Text (strip) import Data.Text.Encoding import Data.Text.Encoding.Error import Data.Text.Lazy as LT (pack) import Data.Text.Lazy.Encoding qualified as TL -import GHC.TypeLits (KnownSymbol, symbolVal) import Imports import Network.HTTP.Types (urlEncode) import Network.HTTP.Types.Method @@ -504,10 +502,7 @@ setBlacklistStatus status email = do getTeamFeatureFlag :: forall cfg. - ( Typeable (Public.LockableFeature cfg), - FromJSON (Public.LockableFeature cfg), - KnownSymbol (Public.FeatureSymbol cfg) - ) => + (IsFeatureConfig cfg, Typeable cfg) => TeamId -> Handler (Public.LockableFeature cfg) getTeamFeatureFlag tid = do @@ -524,9 +519,7 @@ getTeamFeatureFlag tid = do setTeamFeatureFlag :: forall cfg. - ( ToJSON (Public.Feature cfg), - KnownSymbol (Public.FeatureSymbol cfg) - ) => + (IsFeatureConfig cfg) => TeamId -> Public.Feature cfg -> Handler () @@ -540,9 +533,7 @@ setTeamFeatureFlag tid status = do patchTeamFeatureFlag :: forall cfg. - ( ToJSON (Public.LockableFeaturePatch cfg), - KnownSymbol (Public.FeatureSymbol cfg) - ) => + (IsFeatureConfig cfg) => TeamId -> Public.LockableFeaturePatch cfg -> Handler () @@ -566,13 +557,12 @@ galleyRpc req = do setTeamFeatureLockStatus :: forall cfg. - ( KnownSymbol (Public.FeatureSymbol cfg) - ) => + (IsFeatureConfig cfg) => TeamId -> LockStatus -> Handler () setTeamFeatureLockStatus tid lstat = do - info $ msg ("Setting lock status: " <> show (symbolVal (Proxy @(Public.FeatureSymbol cfg)), lstat)) + info $ msg ("Setting lock status: " <> featureName @cfg) gly <- view galley fromResponseBody <=< catchRpcErrors From 19e5f5570af4b3e546cee50c042633244d16e990 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 13 Aug 2024 09:00:14 +0200 Subject: [PATCH 041/136] charts/{brig,galley}: Allow setting a preStop hook for the deployments (#4200) * charts/{brig,galley}: Allow setting a preStop hook for the deployments Co-authored-by: Leonhardt Wille --- changelog.d/5-internal/pre-stop | 1 + charts/brig/templates/deployment.yaml | 5 +++++ charts/brig/values.yaml | 4 ++++ charts/galley/templates/deployment.yaml | 5 +++++ charts/galley/values.yaml | 4 ++++ 5 files changed, 19 insertions(+) create mode 100644 changelog.d/5-internal/pre-stop diff --git a/changelog.d/5-internal/pre-stop b/changelog.d/5-internal/pre-stop new file mode 100644 index 00000000000..f7d0c0cf0fe --- /dev/null +++ b/changelog.d/5-internal/pre-stop @@ -0,0 +1 @@ +charts/{brig,galley}: Allow setting a preStop hook for the deployments diff --git a/charts/brig/templates/deployment.yaml b/charts/brig/templates/deployment.yaml index cff8bffd9bb..fa59c13ed36 100644 --- a/charts/brig/templates/deployment.yaml +++ b/charts/brig/templates/deployment.yaml @@ -165,5 +165,10 @@ spec: scheme: HTTP path: /i/status port: {{ .Values.service.internalPort }} + {{- if .Values.preStop }} + lifecycle: + preStop: +{{ toYaml .Values.preStop | indent 14 }} + {{- end }} resources: {{ toYaml .Values.resources | indent 12 }} diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index 7dcedbce2dc..c5f981d63bd 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -14,6 +14,10 @@ resources: metrics: serviceMonitor: enabled: false +# This is not supported for production use, only here for testing: +# preStop: +# exec: +# command: ["sh", "-c", "curl http://acme.example"] config: logLevel: Info logFormat: StructuredJSON diff --git a/charts/galley/templates/deployment.yaml b/charts/galley/templates/deployment.yaml index ebfb5582abd..06ad8d1cd21 100644 --- a/charts/galley/templates/deployment.yaml +++ b/charts/galley/templates/deployment.yaml @@ -126,5 +126,10 @@ spec: scheme: HTTP path: /i/status port: {{ .Values.service.internalPort }} + {{- if .Values.preStop }} + lifecycle: + preStop: +{{ toYaml .Values.preStop | indent 14 }} + {{- end }} resources: {{ toYaml .Values.resources | indent 12 }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index f6bda0eb643..947bb42c028 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -15,6 +15,10 @@ resources: cpu: "100m" limits: memory: "500Mi" +# This is not supported for production use, only here for testing: +# preStop: +# exec: +# command: ["sh", "-c", "curl http://acme.example"] config: logLevel: Info logFormat: StructuredJSON From 6fab2ff2947209e57909c71dab8c20128de27fc5 Mon Sep 17 00:00:00 2001 From: Amit Sagtani Date: Tue, 13 Aug 2024 15:56:04 +0200 Subject: [PATCH 042/136] remove postgres wrapper chart from wire-server (#4208) --- Makefile | 2 +- changelog.d/5-internal/migrate-postgres-chart | 1 + charts/postgresql/Chart.yaml | 4 ---- charts/postgresql/README.md | 4 ---- charts/postgresql/requirements.yaml | 4 ---- charts/postgresql/values.yaml | 5 ----- 6 files changed, 2 insertions(+), 18 deletions(-) create mode 100644 changelog.d/5-internal/migrate-postgres-chart delete mode 100644 charts/postgresql/Chart.yaml delete mode 100644 charts/postgresql/README.md delete mode 100644 charts/postgresql/requirements.yaml delete mode 100644 charts/postgresql/values.yaml diff --git a/Makefile b/Makefile index 46b6a25dc5e..e6a437a91a6 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice \ calling-test demo-smtp elasticsearch-curator elasticsearch-external \ elasticsearch-ephemeral minio-external cassandra-external \ nginx-ingress-controller ingress-nginx-controller nginx-ingress-services reaper restund coturn \ -k8ssandra-test-cluster postgresql ldap-scim-bridge +k8ssandra-test-cluster ldap-scim-bridge KIND_CLUSTER_NAME := wire-server HELM_PARALLELISM ?= 1 # 1 for sequential tests; 6 for all-parallel tests diff --git a/changelog.d/5-internal/migrate-postgres-chart b/changelog.d/5-internal/migrate-postgres-chart new file mode 100644 index 00000000000..bdd556d76b1 --- /dev/null +++ b/changelog.d/5-internal/migrate-postgres-chart @@ -0,0 +1 @@ +Postgresql helm chart is removed from charts/ directory and migrated to wireapp/helm-charts repo diff --git a/charts/postgresql/Chart.yaml b/charts/postgresql/Chart.yaml deleted file mode 100644 index bff81e0d6fb..00000000000 --- a/charts/postgresql/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -description: Wrapper chart for bitnami/postgresql -name: postgresql -version: 0.0.42 diff --git a/charts/postgresql/README.md b/charts/postgresql/README.md deleted file mode 100644 index 84f4ee02a57..00000000000 --- a/charts/postgresql/README.md +++ /dev/null @@ -1,4 +0,0 @@ -This is the wrapper for PostgreSQL Bitnami chart. - -Configure the values.yaml file to create the database, username, password and other configuration. -List of parameters available - https://artifacthub.io/packages/helm/bitnami/postgresql#parameters diff --git a/charts/postgresql/requirements.yaml b/charts/postgresql/requirements.yaml deleted file mode 100644 index c1d2a1f639b..00000000000 --- a/charts/postgresql/requirements.yaml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: -- name: postgresql - version: 11.9.8 - repository: https://charts.bitnami.com/bitnami diff --git a/charts/postgresql/values.yaml b/charts/postgresql/values.yaml deleted file mode 100644 index fa2230183f7..00000000000 --- a/charts/postgresql/values.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# Configure the parent postgresql chart -postgresql: - fullnameOverride: postgresql - volumePermissions: - enabled: true From f624ffab7ae65b73e96587d9c6a86dcf1a702385 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Tue, 13 Aug 2024 17:09:31 +0200 Subject: [PATCH 043/136] move JWK encoding of removal keys to API v7 (#4207) https://wearezeta.atlassian.net/browse/WPB-10263 --- libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs | 6 +++--- services/galley/src/Galley/API/Public/MLS.hs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index 41d5dbf27a6..4237ee9cecc 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -110,10 +110,10 @@ type MLSMessagingAPI = :> MultiVerb1 'POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus) ) :<|> Named - "mls-public-keys-v5" + "mls-public-keys-v6" ( Summary "Get public keys used by the backend to sign external proposals" :> From 'V5 - :> Until 'V6 + :> Until 'V7 :> CanThrow 'MLSNotEnabled :> "public-keys" :> ZLocalUser @@ -122,7 +122,7 @@ type MLSMessagingAPI = :<|> Named "mls-public-keys" ( Summary "Get public keys used by the backend to sign external proposals" - :> From 'V6 + :> From 'V7 :> CanThrow 'MLSNotEnabled :> "public-keys" :> ZLocalUser diff --git a/services/galley/src/Galley/API/Public/MLS.hs b/services/galley/src/Galley/API/Public/MLS.hs index 2391e44c081..ecebb6db990 100644 --- a/services/galley/src/Galley/API/Public/MLS.hs +++ b/services/galley/src/Galley/API/Public/MLS.hs @@ -27,5 +27,5 @@ mlsAPI :: API MLSAPI GalleyEffects mlsAPI = mkNamedAPI @"mls-message" (callsFed (exposeAnnotations postMLSMessageFromLocalUser)) <@> mkNamedAPI @"mls-commit-bundle" (callsFed (exposeAnnotations postMLSCommitBundleFromLocalUser)) - <@> mkNamedAPI @"mls-public-keys-v5" getMLSPublicKeys + <@> mkNamedAPI @"mls-public-keys-v6" getMLSPublicKeys <@> mkNamedAPI @"mls-public-keys" getMLSPublicKeysJWK From fba266f8a1eb429e1fbc7ba2d13875f2440dabc5 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Wed, 14 Aug 2024 17:26:13 +0200 Subject: [PATCH 044/136] WPB-10581: Remove coturn helm chat (#4209) --- Makefile | 4 +- .../WPB-10581-remove-coturn-helm-chart | 1 + charts/coturn/Chart.yaml | 14 -- charts/coturn/README.md | 25 -- charts/coturn/templates/_helpers.tpl | 54 ----- .../configmap-coturn-conf-template.yaml | 112 --------- .../templates/secret-or-certificate.yaml | 41 ---- charts/coturn/templates/secret.yaml | 20 -- charts/coturn/templates/service-account.yaml | 42 ---- charts/coturn/templates/service.yaml | 37 --- charts/coturn/templates/servicemonitor.yaml | 19 -- charts/coturn/templates/statefulset.yaml | 218 ------------------ charts/coturn/values.yaml | 120 ---------- 13 files changed, 3 insertions(+), 704 deletions(-) create mode 100644 changelog.d/5-internal/WPB-10581-remove-coturn-helm-chart delete mode 100644 charts/coturn/Chart.yaml delete mode 100644 charts/coturn/README.md delete mode 100644 charts/coturn/templates/_helpers.tpl delete mode 100644 charts/coturn/templates/configmap-coturn-conf-template.yaml delete mode 100644 charts/coturn/templates/secret-or-certificate.yaml delete mode 100644 charts/coturn/templates/secret.yaml delete mode 100644 charts/coturn/templates/service-account.yaml delete mode 100644 charts/coturn/templates/service.yaml delete mode 100644 charts/coturn/templates/servicemonitor.yaml delete mode 100644 charts/coturn/templates/statefulset.yaml delete mode 100644 charts/coturn/values.yaml diff --git a/Makefile b/Makefile index e6a437a91a6..a3aac683f65 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ DOCKER_TAG ?= $(USER) # default helm chart version must be 0.0.42 for local development (because 42 is the answer to the universe and everything) HELM_SEMVER ?= 0.0.42 # The list of helm charts needed on internal kubernetes testing environments -CHARTS_INTEGRATION := wire-server databases-ephemeral redis-cluster rabbitmq fake-aws ingress-nginx-controller nginx-ingress-controller nginx-ingress-services fluent-bit kibana restund coturn k8ssandra-test-cluster +CHARTS_INTEGRATION := wire-server databases-ephemeral redis-cluster rabbitmq fake-aws ingress-nginx-controller nginx-ingress-controller nginx-ingress-services fluent-bit kibana restund k8ssandra-test-cluster # The list of helm charts to publish on S3 # FUTUREWORK: after we "inline local subcharts", # (e.g. move charts/brig to charts/wire-server/brig) @@ -17,7 +17,7 @@ CHARTS_RELEASE := wire-server redis-ephemeral redis-cluster rabbitmq rabbitmq-ex fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice \ calling-test demo-smtp elasticsearch-curator elasticsearch-external \ elasticsearch-ephemeral minio-external cassandra-external \ -nginx-ingress-controller ingress-nginx-controller nginx-ingress-services reaper restund coturn \ +nginx-ingress-controller ingress-nginx-controller nginx-ingress-services reaper restund \ k8ssandra-test-cluster ldap-scim-bridge KIND_CLUSTER_NAME := wire-server HELM_PARALLELISM ?= 1 # 1 for sequential tests; 6 for all-parallel tests diff --git a/changelog.d/5-internal/WPB-10581-remove-coturn-helm-chart b/changelog.d/5-internal/WPB-10581-remove-coturn-helm-chart new file mode 100644 index 00000000000..a9a37a85fdc --- /dev/null +++ b/changelog.d/5-internal/WPB-10581-remove-coturn-helm-chart @@ -0,0 +1 @@ +Remove coturn helm chart. It is moved to `wireapp/coturn`. diff --git a/charts/coturn/Chart.yaml b/charts/coturn/Chart.yaml deleted file mode 100644 index 6a8abef6c9d..00000000000 --- a/charts/coturn/Chart.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v2 -name: coturn -description: coturn - a STUN and TURN server -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.0.42 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 4.6.2-federation-wireapp.16 diff --git a/charts/coturn/README.md b/charts/coturn/README.md deleted file mode 100644 index db46c3bf45f..00000000000 --- a/charts/coturn/README.md +++ /dev/null @@ -1,25 +0,0 @@ -**Warning**: this chart is currently considered beta. Use at your own risk! - -This chart deploys Wire's fork of [coturn](https://github.com/coturn/coturn), -a STUN and TURN server, with some additional features developed by Wire (see -[here](https://github.com/wireapp/coturn/tree/wireapp)) to support our calling -services. - -You need to supply a list of one or more zrest secrets at the key -`secrets.zrestSecrets`. The secret provided to the brig chart in -`secrets.turn.secret` must be included in this list. - -Note that coturn pods are deployed with `hostNetwork: true`, as they need to -listen on a wide range of UDP ports. Additionally, some TCP ports need to be -exposed on the hosting node, which are listed in `values.yaml`. - -Due to the nature of TURN, this service might also expose the -internal network to which the hosting node is connected. It is -therefore recommended to run coturn on a separate Kubernetes cluster -from the rest of the Wire services. Further details may be found in -Wire's documentation for Restund, another TURN implementation, on -[this](https://docs.wire.com/understand/restund.html#network) page. - -coturn can optionally be configured to expose a TLS control port. The TLS -private key and certificates should be provided in a `Secret` whose name is -given in `tls.secretRef`. diff --git a/charts/coturn/templates/_helpers.tpl b/charts/coturn/templates/_helpers.tpl deleted file mode 100644 index 70a70f30cd5..00000000000 --- a/charts/coturn/templates/_helpers.tpl +++ /dev/null @@ -1,54 +0,0 @@ -{{- define "coturn.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "coturn.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "coturn.labels" -}} -helm.sh/chart: {{ include "coturn.chart" . }} -{{ include "coturn.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Values.image.tag | default .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "coturn.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{- define "coturn.selectorLabels" -}} -app.kubernetes.io/name: {{ include "coturn.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* Allow KubeVersion to be overridden. */}} -{{- define "kubeVersion" -}} - {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}} -{{- end -}} - -{{- define "includeSecurityContext" -}} - {{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}} -{{- end -}} diff --git a/charts/coturn/templates/configmap-coturn-conf-template.yaml b/charts/coturn/templates/configmap-coturn-conf-template.yaml deleted file mode 100644 index f829900ad1c..00000000000 --- a/charts/coturn/templates/configmap-coturn-conf-template.yaml +++ /dev/null @@ -1,112 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: coturn - -data: - coturn.conf.template: | - ## disable dtls control plane; don't permit relaying tcp connections. - no-dtls - no-tcp-relay - - ## tls handling - {{- if .Values.tls.enabled }} - cert=/secrets-tls/tls.crt - pkey=/secrets-tls/tls.key - {{- if .Values.tls.ciphers }} - cipher-list={{ .Values.tls.ciphers }} - {{- end }} - {{- else }} - no-tls - {{- end }} - - # This is mandatory for federated DTLS - CA-file=/etc/ssl/certs/ca-certificates.crt - - ## don't turn on coturn's cli. - no-cli - - pidfile="/var/tmp/turnserver.pid" - - ## turn, stun. - listening-ip={{ default "__COTURN_EXT_IP__" .Values.coturnTurnListenIP }} - listening-port={{ .Values.coturnTurnListenPort }} - relay-ip={{ default "__COTURN_EXT_IP__" .Values.coturnTurnRelayIP }} - {{- if .Values.coturnTurnExternalIP }} - external-ip={{ default "__COTURN_EXT_IP__" .Values.coturnTurnExternalIP }} - {{- end }} - realm=dummy.io - no-stun-backward-compatibility - secure-stun - no-rfc5780 - - ## prometheus metrics - prometheus-ip={{ default "__COTURN_POD_IP__" .Values.coturnPrometheusIP }} - prometheus-port={{ .Values.coturnMetricsListenPort }} - - ## logs - log-file=stdout - {{- if .Values.config.verboseLogging }} - verbose - {{- end }} - - ## access control settings. - # the address ranges listed here are reserved for special use according - # to the iana registries for special-purposes ipv4 and ipv6 addresses. note - # however that these ranges do *not* include rfc1918 ipv4 space, or ula - # ipv6 space, as these may be valid peer addresses in some private network - # environments. - # - # ref: - # - https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml - # - https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml - # - https://www.rtcsec.com/article/cve-2020-26262-bypass-of-coturns-access-control-protection/#further-concerns-what-else - no-multicast-peers - denied-peer-ip=0.0.0.0-0.255.255.255 - denied-peer-ip=100.64.0.0-100.127.255.255 - denied-peer-ip=127.0.0.0-127.255.255.255 - denied-peer-ip=169.254.0.0-169.254.255.255 - denied-peer-ip=192.0.0.0-192.0.0.255 - denied-peer-ip=192.0.2.0-192.0.2.255 - denied-peer-ip=192.88.99.0-192.88.99.255 - denied-peer-ip=198.18.0.0-198.19.255.255 - denied-peer-ip=198.51.100.0-198.51.100.255 - denied-peer-ip=203.0.113.0-203.0.113.255 - denied-peer-ip=240.0.0.0-255.255.255.255 - denied-peer-ip=::1 - denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff - denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255 - denied-peer-ip=100::-100::ffff:ffff:ffff:ffff - denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff - denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff - # fc00::/7 is reserved for ipv6 ula, but fc00::/8 is not assigned at present. - denied-peer-ip=fc00::-fcff:ffff:ffff:ffff:ffff:ffff:ffff:ffff - denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff - - # FUTUREWORK: expose customisable access control settings. - - ## authentication setup - zrest - ## static authentication secrets will be added below this line when the - ## runtime configuration is generated. - - {{- if .Values.federate.enabled }} - ### federation setup - federation-listening-ip={{ default "__COTURN_EXT_IP__" .Values.coturnFederationListeningIP }} - federation-listening-port={{ .Values.federate.port }} - federation-no-dtls={{ not .Values.federate.dtls.enabled }} - {{- if .Values.federate.dtls.enabled }} - federation-cert=/coturn-dtls-certificate/tls.crt - federation-pkey=/coturn-dtls-certificate/tls.key - {{ if hasKey .Values.federate.dtls.tls "privateKeyPassword" }} - federation-pkey-pwd={{ .Values.federate.dtls.tls.privateKeyPassword }} - {{ end }} - # list of host/ip/cert common names / subject alt names, and optional issuer - # names to accept DTLS connections from. There can be multiple entries, each - # entry is formated as: - # [,] - {{ range $entry := .Values.federate.dtls.remoteWhitelist }} - federation-remote-whitelist={{ $entry.host }}{{ if hasKey $entry "issuer" }},{{ $entry.issuer }}{{end}} - {{ end }} - {{ end }} - {{ end }} diff --git a/charts/coturn/templates/secret-or-certificate.yaml b/charts/coturn/templates/secret-or-certificate.yaml deleted file mode 100644 index a48eba9b499..00000000000 --- a/charts/coturn/templates/secret-or-certificate.yaml +++ /dev/null @@ -1,41 +0,0 @@ -{{- if .Values.federate.dtls.enabled -}} - -{{- if .Values.federate.dtls.tls.issuerRef -}} -{{- if or .Values.federate.dtls.tls.key .Values.federate.dtls.tls.crt }} -{{- fail "issuerRef and {crt,key} are mutually exclusive" -}} -{{- end -}} -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: "{{ include "coturn.fullname" . }}" - labels: - {{- include "coturn.labels" . | nindent 4 }} - {{- if .Values.federate.dtls.tls.certificate.labels }} - {{- toYaml .Values.federate.dtls.tls.certificate.labels | nindent 4}} - {{- end }} -spec: - dnsNames: - {{- toYaml .Values.federate.dtls.tls.certificate.dnsNames | nindent 4 }} - secretName: coturn-dtls-certificate - issuerRef: - {{- toYaml .Values.federate.dtls.tls.issuerRef | nindent 4 }} - privateKey: - rotationPolicy: Always - algorithm: ECDSA - size: 384 -{{- else if and .Values.federate.dtls.tls.key .Values.federate.dtls.tls.crt }} -apiVersion: v1 -kind: Secret -metadata: - name: coturn-dtls-certificate - labels: - {{- include "coturn.labels" . | nindent 4 }} -type: Opaque -data: - tls.key: {{ .Values.federate.dtls.tls.key | b64enc }} - tls.crt: {{ .Values.federate.dtls.tls.crt | b64enc }} -{{- else -}} -{{- fail "must specify tls.key and tls.crt , or tls.issuerRef" -}} -{{- end -}} - -{{- end -}} diff --git a/charts/coturn/templates/secret.yaml b/charts/coturn/templates/secret.yaml deleted file mode 100644 index 6dd55212066..00000000000 --- a/charts/coturn/templates/secret.yaml +++ /dev/null @@ -1,20 +0,0 @@ -{{- if or (not .Values.secrets) (not .Values.secrets.zrestSecrets) }} -{{- fail "TURN authentication secrets are not defined in .Values.secrets.zrestSecrets" }} -{{- else if eq (len .Values.secrets.zrestSecrets) 0 }} -{{- fail "At least one authentication secret must be defined" }} -{{- else }} -apiVersion: v1 -kind: Secret -metadata: - name: coturn - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -type: Opaque -stringData: - zrest_secret.txt: | - {{- range .Values.secrets.zrestSecrets }}{{ . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/charts/coturn/templates/service-account.yaml b/charts/coturn/templates/service-account.yaml deleted file mode 100644 index ec932539fef..00000000000 --- a/charts/coturn/templates/service-account.yaml +++ /dev/null @@ -1,42 +0,0 @@ ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: coturn - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: coturn-{{ .Release.Namespace }} - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -rules: - - apiGroups: [""] - resources: [nodes] - verbs: [get] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: coturn-{{ .Release.Namespace }} - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -roleRef: - kind: ClusterRole - apiGroup: rbac.authorization.k8s.io - name: coturn-{{ .Release.Namespace }} -subjects: - - kind: ServiceAccount - name: coturn - namespace: {{ .Release.Namespace }} diff --git a/charts/coturn/templates/service.yaml b/charts/coturn/templates/service.yaml deleted file mode 100644 index 54f2e75da2d..00000000000 --- a/charts/coturn/templates/service.yaml +++ /dev/null @@ -1,37 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: coturn - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} - {{- with .Values.service.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - # Needs to be headless - # See: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/ - clusterIP: None - ports: - - name: coturn-tcp - port: {{ .Values.coturnTurnListenPort }} - targetPort: coturn-tcp - - name: coturn-udp - port: {{ .Values.coturnTurnListenPort }} - targetPort: coturn-udp - protocol: UDP - {{- if .Values.tls.enabled }} - - name: coturn-tls - port: {{ .Values.coturnTurnTlsListenPort }} - targetPort: coturn-tls - {{- end }} - - name: status-http - port: {{ .Values.coturnMetricsListenPort }} - targetPort: status-http - selector: - app: coturn - release: {{ .Release.Name }} diff --git a/charts/coturn/templates/servicemonitor.yaml b/charts/coturn/templates/servicemonitor.yaml deleted file mode 100644 index a21f0faea4e..00000000000 --- a/charts/coturn/templates/servicemonitor.yaml +++ /dev/null @@ -1,19 +0,0 @@ -{{- if .Values.metrics.serviceMonitor.enabled }} -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: coturn - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -spec: - endpoints: - - port: status-http - path: /metrics - selector: - matchLabels: - app: coturn - release: {{ .Release.Name }} -{{- end }} diff --git a/charts/coturn/templates/statefulset.yaml b/charts/coturn/templates/statefulset.yaml deleted file mode 100644 index e33c8be7ae2..00000000000 --- a/charts/coturn/templates/statefulset.yaml +++ /dev/null @@ -1,218 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: coturn - labels: - app: coturn - chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} - -spec: - replicas: {{ .Values.replicaCount }} - - # Allow starting and stopping coturn in parallel when scaling. This does not - # affect upgrades. - podManagementPolicy: Parallel - - serviceName: coturn - selector: - matchLabels: - app: coturn - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - - labels: - app: coturn - release: {{ .Release.Name }} - spec: - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - {{- if .Values.tls.enabled }} - # Needed for automatic certificate reload handling - shareProcessNamespace: true - {{- end }} - hostNetwork: true - serviceAccountName: coturn - volumes: - - name: external-ip - emptyDir: {} - - name: coturn-config - emptyDir: {} - - name: coturn-config-template - configMap: - name: coturn - - name: secrets - secret: - secretName: coturn - - name: coturndb - emptyDir: - medium: Memory - sizeLimit: 128Mi # observed size: 80 kilobytes - {{- if .Values.tls.enabled }} - - name: secrets-tls - secret: - secretName: {{ .Values.tls.secretRef }} - {{- end }} - {{- if .Values.federate.dtls.enabled }} - - name: coturn-dtls-certificate - secret: - secretName: coturn-dtls-certificate - {{- end }} - initContainers: - - name: get-external-ip - image: bitnami/kubectl:1.24.12 - volumeMounts: - - name: external-ip - mountPath: /external-ip - env: - - name: NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - command: - - /bin/sh - - -c - - | - set -e - - # In the cloud, this setting is available to indicate the true IP address - addr=$(kubectl get node $NODE_NAME -ojsonpath='{.status.addresses[?(@.type=="ExternalIP")].address}') - # On on-prem we allow people to set "wire.com/external-ip" to override this - if [ -z "$addr" ]; then - addr=$(kubectl get node $NODE_NAME -ojsonpath='{.metadata.annotations.wire\.com/external-ip}') - fi - echo -n "$addr" | tee /dev/stderr > /external-ip/ip - containers: - - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository}}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - env: - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: HOST_IP - valueFrom: - fieldRef: - fieldPath: status.hostIP - volumeMounts: - - name: external-ip - mountPath: /external-ip - - name: coturn-config - mountPath: /coturn-config - - name: coturn-config-template - mountPath: /coturn-template/coturn.conf.template - subPath: coturn.conf.template - - name: secrets - mountPath: /secrets/ - readOnly: true - # > By default, Coturn Docker image persists its data in /var/lib/coturn/ directory. - # > You can speedup Coturn simply by using tmpfs for that. - # We use a memory-backed emptyDir here instead. - - name: coturndb - mountPath: /var/lib/coturn - {{- if .Values.tls.enabled }} - - name: secrets-tls - mountPath: /secrets-tls/ - readOnly: true - {{- end }} - {{- if .Values.federate.dtls.enabled }} - - name: coturn-dtls-certificate - mountPath: /coturn-dtls-certificate/ - readOnly: true - {{- end }} - command: - - /usr/bin/dumb-init - - -- - - /bin/sh - - -c - - | - set -e - EXTERNAL_IP=$(cat /external-ip/ip) - sed -Ee "s;__COTURN_EXT_IP__;$EXTERNAL_IP;g" -e "s;__COTURN_POD_IP__;$POD_IP;g" -e "s;__COTURN_HOST_IP__;$HOST_IP;g" /coturn-template/coturn.conf.template > /coturn-config/turnserver.conf - sed -Ee 's/^/static-auth-secret=/' /secrets/zrest_secret.txt >> /coturn-config/turnserver.conf - exec /usr/bin/turnserver -c /coturn-config/turnserver.conf - {{- if .Values.coturnGracefulTermination }} - lifecycle: - preStop: - exec: - command: - - /bin/sh - - -c - - "exec /usr/local/bin/pre-stop-hook \"$POD_IP\" {{ .Values.coturnMetricsListenPort }}" - {{- end }} - - ports: - - name: coturn-tcp - containerPort: {{ .Values.coturnTurnListenPort }} - protocol: TCP - - name: coturn-udp - containerPort: {{ .Values.coturnTurnListenPort }} - protocol: UDP - {{- if .Values.tls.enabled }} - - name: coturn-tls - containerPort: {{ .Values.coturnTurnTlsListenPort }} - protocol: TCP - {{- end }} - - name: status-http - containerPort: {{ .Values.coturnMetricsListenPort }} - protocol: TCP - - livenessProbe: - timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} - failureThreshold: {{ .Values.livenessProbe.failureThreshold }} - httpGet: - path: / - port: status-http - - readinessProbe: - timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} - failureThreshold: {{ .Values.readinessProbe.failureThreshold }} - httpGet: - path: / - port: status-http - - resources: - {{- toYaml .Values.resources | nindent 12 }} - - {{- if .Values.tls.enabled }} - - name: {{ .Chart.Name }}-cert-reloader - image: "{{ .Values.tls.reloaderImage.repository }}:{{ .Values.tls.reloaderImage.tag }}" - imagePullPolicy: {{ .Values.tls.reloaderImage.pullPolicy }} - env: - - name: CONFIG_DIR - value: /secrets-tls - - name: PROCESS_NAME - value: turnserver - - name: RELOAD_SIGNAL - value: SIGUSR2 - volumeMounts: - - name: secrets-tls - mountPath: /secrets-tls/ - readOnly: true - {{- end }} - - {{- if .Values.coturnGracefulTermination }} - terminationGracePeriodSeconds: {{ .Values.coturnGracePeriodSeconds }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/charts/coturn/values.yaml b/charts/coturn/values.yaml deleted file mode 100644 index 1035c40d734..00000000000 --- a/charts/coturn/values.yaml +++ /dev/null @@ -1,120 +0,0 @@ -# The amount of coturn instances to run. NOTE: Only one coturn can run per node due -# to `hostNetwork`. If this number is higher than the amount of nodes that can -# be used for scheduling (Also see `nodeSelector`) pods will remain in a -# pending state untill you add more capacity. -replicaCount: 1 - -image: - repository: quay.io/wire/coturn - pullPolicy: IfNotPresent - # overwrite the tag here, otherwise `appVersion` of the chart will be used - tag: "" - -# If you have multiple deployments of coturn running in one cluster, it is -# important that they run on disjoint sets of nodes, you can use nodeSelector to enforce this -nodeSelector: {} - -podSecurityContext: - fsGroup: 31338 - -securityContext: - # Pick a high number that is unlikely to conflict with the host - # https://kubesec.io/basics/containers-securitycontext-runasuser/ - runAsUser: 31338 - -coturnTurnListenPort: 3478 -coturnMetricsListenPort: 9641 -coturnTurnTlsListenPort: 5349 - -# coturnTurnListenIP: "1.2.3.4" # can also be __COTURN_EXT_IP__, __COTURN_POD_IP__,__COTURN_HOST_IP__ -coturnTurnExternalIP: null -# coturnTurnRelayIP: -# coturnPrometheusIP: -# coturnFederationListeningIP: - -tls: - enabled: false - # compliant with BSI TR-02102-2 - ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384' - secretRef: - reloaderImage: - # container image containing https://github.com/Pluies/config-reloader-sidecar - # for handling runtime certificate reloads. - repository: quay.io/wire/config-reloader-sidecar - pullPolicy: IfNotPresent - tag: 72c3c8434660bd157d42012b0bcd67f338fc5c7a - -config: - verboseLogging: false - -federate: - enabled: false - port: 9191 - - dtls: - enabled: false - - tls: - # Example: - # - # tls: - # key: "-----BEGIN EC PRIVATE KEY----- ..." # (ascii blob) private key - # crt: "-----BEGIN CERTIFICATE----- ..." # (ascii blob) certificate - # privateKeyPassword: "XXX" # optional, used when the key is password protected - # - # OR (mutually exclusive) - # - # tls: - # issuerRef: - # name: letsencrypt-http01 - # - # # We can reference ClusterIssuers by changing the kind here. - # # The default value is Issuer (i.e. a locally namespaced Issuer) - # # kind: Issuer - # kind: Issuer - # - # # This is optional since cert-manager will default to this value however - # # if you are using an external issuer, change this to that issuer group. - # group: cert-manager.io - # - # # optional labels to attach to the cert-manager Certificate - # certificate: - # labels: .. - - # # list of host/ip/cert common names / subject alt names, and optional issuer - # # names to accept DTLS connections from. There can be multiple entries. - # remoteWhitelist: - # - host: wire.example - # issuer: Issuer Common Name - # - host: another.wire.example - # issuer: "DigiCert SHA2 Extended Validation Server CA" - # - host: another-host-without-issuer.wire.example - remoteWhitelist: [] - -metrics: - serviceMonitor: - enabled: false - -# This chart supports waiting for traffic to drain from coturn -# before pods are actually terminated. Once in 'drain' mode, no new connections -# are accepted, but old ones are kept alive. -# If you have 2 or more replicas, it's recommended to set this to true, -# and if you only have one coturn replica you may want this to be false, as -# otherwise while the pod restarts, no new calls can be established. -coturnGracefulTermination: false -# Grace period for terminating coturn pods, after which they will be forcibly -# terminated. This setting is only effective when coturnGracefulTermination is -# set to true. -coturnGracePeriodSeconds: 43200 # 12 hours - -livenessProbe: - timeoutSeconds: 5 - failureThreshold: 5 - -readinessProbe: - timeoutSeconds: 5 - failureThreshold: 5 - -service: - # Kubernetes annotations to be set at the Service - annotations: {} From 9481a886d40ae4ff199353bb9eed593a24597d7f Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 16 Aug 2024 12:42:39 +0200 Subject: [PATCH 045/136] WPB-1334 extend list of OAuth apps with active refresh token ids (#4211) --- changelog.d/2-features/WPB-1334 | 1 + integration/integration.cabal | 1 + integration/test/API/Brig.hs | 38 +++++++++++++++++++++ integration/test/API/BrigInternal.hs | 6 ++++ integration/test/Test/OAuth.hs | 25 ++++++++++++++ integration/test/Testlib/HTTP.hs | 10 ++++++ libs/wire-api/src/Wire/API/OAuth.hs | 30 +++++++++++++--- services/brig/src/Brig/API/OAuth.hs | 15 +++++--- services/brig/test/integration/API/OAuth.hs | 14 ++++---- 9 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 changelog.d/2-features/WPB-1334 create mode 100644 integration/test/Test/OAuth.hs diff --git a/changelog.d/2-features/WPB-1334 b/changelog.d/2-features/WPB-1334 new file mode 100644 index 00000000000..a9741efd7ee --- /dev/null +++ b/changelog.d/2-features/WPB-1334 @@ -0,0 +1 @@ +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/integration/integration.cabal b/integration/integration.cabal index 1b5069a2b4b..30bc3c641c3 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -139,6 +139,7 @@ library Test.MLS.SubConversation Test.MLS.Unreachable Test.Notifications + Test.OAuth Test.Presence Test.Property Test.Provider diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 898afda288e..e6233bcda94 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -694,3 +694,41 @@ clearProperties :: (MakesValue user) => user -> App Response clearProperties user = do req <- baseRequest user Brig Versioned $ joinHttpPath ["properties"] submit "DELETE" req + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/post_oauth_authorization_codes +generateOAuthAuthorizationCode :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> [String] -> String -> App Response +generateOAuthAuthorizationCode user cid scopes redirectUrl = do + cidStr <- asString cid + req <- baseRequest user Brig Versioned "/oauth/authorization/codes" + submit "POST" $ + req + & addJSONObject + [ "client_id" .= cidStr, + "scope" .= unwords scopes, + "redirect_uri" .= redirectUrl, + "code_challenge" .= "G7CWLBqYDT8doT_oEIN3un_QwZWYKHmOqG91nwNzITc", + "code_challenge_method" .= "S256", + "response_type" .= "code", + "state" .= "abc" + ] + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/post_oauth_token +createOAuthAccessToken :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> String -> String -> App Response +createOAuthAccessToken user cid code redirectUrl = do + cidStr <- asString cid + req <- baseRequest user Brig Versioned "/oauth/token" + submit "POST" $ + req + & addUrlEncodedForm + [ ("grant_type", "authorization_code"), + ("client_id", cidStr), + ("code_verifier", "nE3k3zykOmYki~kriKzAmeFiGT7cWugcuToFwo1YPgrZ1cFvaQqLa.dXY9MnDj3umAmG-8lSNIYIl31Cs_.fV5r2psa4WWZcB.Nlc3A-t3p67NDZaOJjIiH~8PvUH_hR"), + ("code", code), + ("redirect_uri", redirectUrl) + ] + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_oauth_applications +getOAuthApplications :: (HasCallStack, MakesValue user) => user -> App Response +getOAuthApplications user = do + req <- baseRequest user Brig Versioned "/oauth/applications" + submit "GET" req diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index ccdeb10224c..0e840713bc3 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -278,3 +278,9 @@ deleteFeatureForUser user featureName = do uid <- objId user req <- baseRequest user Brig Unversioned $ joinHttpPath ["i", "users", uid, "features", featureName] submit "DELETE" req + +-- | https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/post_i_oauth_clients +createOAuthClient :: (HasCallStack, MakesValue user) => user -> String -> String -> App Response +createOAuthClient user name url = do + req <- baseRequest user Brig Unversioned "i/oauth/clients" + submit "POST" $ req & addJSONObject ["application_name" .= name, "redirect_url" .= url] diff --git a/integration/test/Test/OAuth.hs b/integration/test/Test/OAuth.hs new file mode 100644 index 00000000000..4a98a235872 --- /dev/null +++ b/integration/test/Test/OAuth.hs @@ -0,0 +1,25 @@ +module Test.OAuth where + +import API.Brig +import API.BrigInternal +import Data.String.Conversions +import Network.HTTP.Types +import Network.URI +import SetupHelpers +import Testlib.Prelude + +testListApplicationsWithActiveSessions :: (HasCallStack) => App () +testListApplicationsWithActiveSessions = do + user <- randomUser OwnDomain def + oauthClient <- createOAuthClient user "foobar" "https://example.com" >>= getJSON 200 + cid <- oauthClient %. "client_id" + let scopes = ["write:conversations"] + let generateAccessToken = do + authCodeResponse <- generateOAuthAuthorizationCode user cid scopes "https://example.com" + let location = fromMaybe (error "no location header") $ parseURI . cs . snd =<< locationHeader authCodeResponse + let code = maybe "no code query param" cs $ join $ lookup (cs "code") $ parseQuery $ cs location.uriQuery + void $ createOAuthAccessToken user cid code "https://example.com" >>= getJSON 200 + replicateM_ 2 generateAccessToken + [app] <- getOAuthApplications user >>= getJSON 200 >>= asList + sessions <- app %. "sessions" >>= asList + length sessions `shouldMatchInt` 2 diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index 14c285f964a..6f1a9677eec 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -17,6 +17,7 @@ import Data.String import Data.String.Conversions (cs) import qualified Data.Text as T import qualified Data.Text.Encoding as T +import Data.Tuple.Extra import GHC.Generics import GHC.Stack import qualified Network.HTTP.Client as HTTP @@ -41,6 +42,15 @@ addJSONObject = addJSON . Aeson.object addJSON :: (Aeson.ToJSON a) => a -> HTTP.Request -> HTTP.Request addJSON obj = addBody (HTTP.RequestBodyLBS (Aeson.encode obj)) "application/json" +addUrlEncodedForm :: [(String, String)] -> HTTP.Request -> HTTP.Request +addUrlEncodedForm form req = + req + { HTTP.requestBody = HTTP.RequestBodyLBS (L.fromStrict (HTTP.renderSimpleQuery False (both C8.pack <$> form))), + HTTP.requestHeaders = + (fromString "Content-Type", fromString "application/x-www-form-urlencoded") + : HTTP.requestHeaders req + } + addBody :: HTTP.RequestBody -> String -> HTTP.Request -> HTTP.Request addBody body contentType req = req diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 89c28f98370..4861631b00a 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -29,6 +29,7 @@ import Data.ByteString.Lazy (fromStrict, toStrict) import Data.Either.Combinators (mapLeft) import Data.HashMap.Strict qualified as HM import Data.Id as Id +import Data.Json.Util import Data.OpenApi (ToParamSchema (..)) import Data.OpenApi qualified as S import Data.Range @@ -650,9 +651,28 @@ instance ToSchema OAuthRevokeRefreshTokenRequest where clientIdDescription = description ?~ "The OAuth client's ID" refreshTokenDescription = description ?~ "The refresh token" +data OAuthSession = OAuthSession + { refreshTokenId :: OAuthRefreshTokenId, + createdAt :: UTCTimeMillis + } + deriving (Eq, Show, Ord, Generic) + deriving (Arbitrary) via (GenericUniform OAuthSession) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema OAuthSession) + +instance ToSchema OAuthSession where + schema = + object "OAuthSession" $ + OAuthSession + <$> (.refreshTokenId) .= fieldWithDocModifier "refresh_token_id" refreshTokenIdDescription schema + <*> (.createdAt) .= fieldWithDocModifier "created_at" createdAtDescription schema + where + refreshTokenIdDescription = description ?~ "The ID of the refresh token" + createdAtDescription = description ?~ "The time when the session was created" + data OAuthApplication = OAuthApplication { applicationId :: OAuthClientId, - name :: OAuthApplicationName + name :: OAuthApplicationName, + sessions :: [OAuthSession] } deriving (Eq, Show, Ord, Generic) deriving (Arbitrary) via (GenericUniform OAuthApplication) @@ -662,13 +682,13 @@ instance ToSchema OAuthApplication where schema = object "OAuthApplication" $ OAuthApplication - <$> applicationId - .= fieldWithDocModifier "id" idDescription schema - <*> (.name) - .= fieldWithDocModifier "name" nameDescription schema + <$> applicationId .= fieldWithDocModifier "id" idDescription schema + <*> (.name) .= fieldWithDocModifier "name" nameDescription schema + <*> sessions .= fieldWithDocModifier "sessions" sessionsDescription (array schema) where idDescription = description ?~ "The OAuth client's ID" nameDescription = description ?~ "The OAuth client's name" + sessionsDescription = description ?~ "The OAuth client's sessions" -------------------------------------------------------------------------------- -- Errors diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 5ff461bf652..b9e69c4c613 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -36,6 +36,8 @@ import Crypto.JWT hiding (params, uri) import Data.ByteString.Conversion import Data.Domain import Data.Id +import Data.Json.Util (toUTCTimeMillis) +import Data.Map qualified as Map import Data.Misc import Data.Set qualified as Set import Data.Text.Ascii @@ -320,10 +322,15 @@ lookupAndVerifyToken key = getOAuthApplications :: UserId -> (Handler r) [OAuthApplication] getOAuthApplications uid = do activeRefreshTokens <- lift $ wrapClient $ lookupOAuthRefreshTokens uid - nub . catMaybes <$> for activeRefreshTokens oauthApp + toApplications activeRefreshTokens where - oauthApp :: OAuthRefreshTokenInfo -> (Handler r) (Maybe OAuthApplication) - oauthApp info = (OAuthApplication info.clientId . (.name)) <$$> getOAuthClient info.userId info.clientId + toApplications :: [OAuthRefreshTokenInfo] -> (Handler r) [OAuthApplication] + toApplications infos = do + let grouped = Map.fromListWith (<>) $ (\info -> (info.clientId, [info])) <$> infos + mApps <- for (Map.toList grouped) $ \(cid, tokens) -> do + mClient <- getOAuthClient uid cid + pure $ (\client -> OAuthApplication cid client.name ((\i -> OAuthSession i.refreshTokenId (toUTCTimeMillis i.createdAt)) <$> tokens)) <$> mClient + pure $ catMaybes mApps -------------------------------------------------------------------------------- @@ -404,7 +411,7 @@ insertOAuthRefreshToken maxActiveTokens ttl info = do determineOldestTokensToBeDeleted tokens = take (length sorted - fromIntegral maxActiveTokens + 1) sorted where - sorted = sortOn createdAt tokens + sorted = sortOn (.createdAt) tokens lookupOAuthRefreshTokens :: (MonadClient m) => UserId -> m [OAuthRefreshTokenInfo] lookupOAuthRefreshTokens uid = do diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 3b3eba50b38..9c6a0a92abf 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -16,7 +16,7 @@ -- with this program. If not, see . {-# OPTIONS_GHC -fno-warn-orphans #-} -module API.OAuth where +module API.OAuth (tests) where import API.Team.Util qualified as Team import Bilge @@ -439,7 +439,7 @@ testRefreshTokenMaxActiveTokens opts db brig = resp <- createOAuthAccessToken brig accessTokenRequest rid <- extractRefreshTokenId jwk resp.refreshToken tokens <- C.runClient db (lookupOAuthRefreshTokens uid) - liftIO $ assertBool testMsg $ [rid] `hasSameElems` (refreshTokenId <$> tokens) + liftIO $ assertBool testMsg $ [rid] `hasSameElems` ((.refreshTokenId) <$> tokens) pure (rid, cid, secret) delayOneSec rid2 <- do @@ -449,7 +449,7 @@ testRefreshTokenMaxActiveTokens opts db brig = resp <- createOAuthAccessToken brig accessTokenRequest rid <- extractRefreshTokenId jwk resp.refreshToken tokens <- C.runClient db (lookupOAuthRefreshTokens uid) - liftIO $ assertBool testMsg $ [rid1, rid] `hasSameElems` (refreshTokenId <$> tokens) + liftIO $ assertBool testMsg $ [rid1, rid] `hasSameElems` ((.refreshTokenId) <$> tokens) pure rid delayOneSec rid3 <- do @@ -460,7 +460,7 @@ testRefreshTokenMaxActiveTokens opts db brig = rid <- extractRefreshTokenId jwk resp.refreshToken recoverN 3 $ do tokens <- C.runClient db (lookupOAuthRefreshTokens uid) - liftIO $ assertBool testMsg $ [rid2, rid] `hasSameElems` (refreshTokenId <$> tokens) + liftIO $ assertBool testMsg $ [rid2, rid] `hasSameElems` ((.refreshTokenId) <$> tokens) pure rid delayOneSec do @@ -470,7 +470,7 @@ testRefreshTokenMaxActiveTokens opts db brig = resp <- createOAuthAccessToken brig accessTokenRequest rid <- extractRefreshTokenId jwk resp.refreshToken tokens <- C.runClient db (lookupOAuthRefreshTokens uid) - liftIO $ assertBool testMsg $ [rid3, rid] `hasSameElems` (refreshTokenId <$> tokens) + liftIO $ assertBool testMsg $ [rid3, rid] `hasSameElems` ((.refreshTokenId) <$> tokens) where extractRefreshTokenId :: (MonadIO m) => JWK -> OAuthRefreshToken -> m OAuthRefreshTokenId extractRefreshTokenId jwk rt = do @@ -609,14 +609,14 @@ testListApplicationsWithAccountAccess brig = do bob <- createUser "bob" brig do apps <- listOAuthApplications brig (User.userId alice) - liftIO $ assertEqual "apps" 0 (length apps) + liftIO $ apps @?= [] void $ createOAuthApplicationWithAccountAccess brig (User.userId alice) void $ createOAuthApplicationWithAccountAccess brig (User.userId alice) do aliceApps <- listOAuthApplications brig (User.userId alice) liftIO $ assertEqual "apps" 2 (length aliceApps) bobsApps <- listOAuthApplications brig (User.userId bob) - liftIO $ assertEqual "apps" 0 (length bobsApps) + liftIO $ bobsApps @?= [] void $ createOAuthApplicationWithAccountAccess brig (User.userId alice) void $ createOAuthApplicationWithAccountAccess brig (User.userId bob) do From 080160f8ae9db0450b5f2a9b1e26245d3ae270c1 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Fri, 16 Aug 2024 16:53:26 +0200 Subject: [PATCH 046/136] correct swagger for APIv6 (#4215) This fix was necessarry after f624ffab7ae65b73e96587d9c6a86dcf1a702385. --- services/brig/docs/swagger-v6.json | 548 +++++++++++++++++------------ 1 file changed, 332 insertions(+), 216 deletions(-) diff --git a/services/brig/docs/swagger-v6.json b/services/brig/docs/swagger-v6.json index 6d4d1b91149..3f0ab04e358 100644 --- a/services/brig/docs/swagger-v6.json +++ b/services/brig/docs/swagger-v6.json @@ -217,64 +217,64 @@ "AllFeatureConfigs": { "properties": { "appLock": { - "$ref": "#/components/schemas/AppLockConfig.WithStatus" + "$ref": "#/components/schemas/AppLockConfig.LockableFeature" }, "classifiedDomains": { - "$ref": "#/components/schemas/ClassifiedDomainsConfig.WithStatus" + "$ref": "#/components/schemas/ClassifiedDomainsConfig.LockableFeature" }, "conferenceCalling": { - "$ref": "#/components/schemas/ConferenceCallingConfig.WithStatus" + "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" }, "conversationGuestLinks": { - "$ref": "#/components/schemas/GuestLinksConfig.WithStatus" + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" }, "digitalSignatures": { - "$ref": "#/components/schemas/DigitalSignaturesConfig.WithStatus" + "$ref": "#/components/schemas/DigitalSignaturesConfig.LockableFeature" }, "enforceFileDownloadLocation": { - "$ref": "#/components/schemas/EnforceFileDownloadLocation.WithStatus" + "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" }, "exposeInvitationURLsToTeamAdmin": { - "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" }, "fileSharing": { - "$ref": "#/components/schemas/FileSharingConfig.WithStatus" + "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" }, "legalhold": { - "$ref": "#/components/schemas/LegalholdConfig.WithStatus" + "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" }, "limitedEventFanout": { - "$ref": "#/components/schemas/LimitedEventFanoutConfig.WithStatus" + "$ref": "#/components/schemas/LimitedEventFanoutConfig.LockableFeature" }, "mls": { - "$ref": "#/components/schemas/MLSConfig.WithStatus" + "$ref": "#/components/schemas/MLSConfig.LockableFeature" }, "mlsE2EId": { - "$ref": "#/components/schemas/MlsE2EIdConfig.WithStatus" + "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" }, "mlsMigration": { - "$ref": "#/components/schemas/MlsMigration.WithStatus" + "$ref": "#/components/schemas/MlsMigration.LockableFeature" }, "outlookCalIntegration": { - "$ref": "#/components/schemas/OutlookCalIntegrationConfig.WithStatus" + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" }, "searchVisibility": { - "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.WithStatus" + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" }, "searchVisibilityInbound": { - "$ref": "#/components/schemas/SearchVisibilityInboundConfig.WithStatus" + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" }, "selfDeletingMessages": { - "$ref": "#/components/schemas/SelfDeletingMessagesConfig.WithStatus" + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" }, "sndFactorPasswordChallenge": { - "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.WithStatus" + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" }, "sso": { - "$ref": "#/components/schemas/SSOConfig.WithStatus" + "$ref": "#/components/schemas/SSOConfig.LockableFeature" }, "validateSAMLemails": { - "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.WithStatus" + "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.LockableFeature" } }, "required": [ @@ -502,14 +502,11 @@ ], "type": "object" }, - "AppLockConfig.WithStatus": { + "AppLockConfig.Feature": { "properties": { "config": { "$ref": "#/components/schemas/AppLockConfig" }, - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -522,16 +519,18 @@ }, "required": [ "status", - "lockStatus", "config" ], "type": "object" }, - "AppLockConfig.WithStatusNoLock": { + "AppLockConfig.LockableFeature": { "properties": { "config": { "$ref": "#/components/schemas/AppLockConfig" }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -544,6 +543,7 @@ }, "required": [ "status", + "lockStatus", "config" ], "type": "object" @@ -623,10 +623,6 @@ "example": "ZXhhbXBsZQo=", "type": "string" }, - "Base64URLByteString": { - "example": "ZXhhbXBsZQo=", - "type": "string" - }, "BaseProtocol": { "enum": [ "proteus", @@ -758,7 +754,7 @@ ], "type": "object" }, - "ClassifiedDomainsConfig.WithStatus": { + "ClassifiedDomainsConfig.LockableFeature": { "properties": { "config": { "$ref": "#/components/schemas/ClassifiedDomainsConfig" @@ -999,8 +995,39 @@ ], "type": "object" }, - "ConferenceCallingConfig.WithStatus": { + "ConferenceCallingConfig": { + "properties": { + "useSFTForOneToOneCalls": { + "type": "boolean" + } + }, + "type": "object" + }, + "ConferenceCallingConfig.Feature": { + "properties": { + "config": { + "$ref": "#/components/schemas/ConferenceCallingConfig" + }, + "status": { + "$ref": "#/components/schemas/FeatureStatus" + }, + "ttl": { + "example": "unlimited", + "maximum": 18446744073709551615, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "ConferenceCallingConfig.LockableFeature": { "properties": { + "config": { + "$ref": "#/components/schemas/ConferenceCallingConfig" + }, "lockStatus": { "$ref": "#/components/schemas/LockStatus" }, @@ -2124,7 +2151,7 @@ ], "type": "object" }, - "DigitalSignaturesConfig.WithStatus": { + "DigitalSignaturesConfig.LockableFeature": { "properties": { "lockStatus": { "$ref": "#/components/schemas/LockStatus" @@ -2217,14 +2244,11 @@ }, "type": "object" }, - "EnforceFileDownloadLocation.WithStatus": { + "EnforceFileDownloadLocation.Feature": { "properties": { "config": { "$ref": "#/components/schemas/EnforceFileDownloadLocation" }, - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -2237,16 +2261,18 @@ }, "required": [ "status", - "lockStatus", "config" ], "type": "object" }, - "EnforceFileDownloadLocation.WithStatusNoLock": { + "EnforceFileDownloadLocation.LockableFeature": { "properties": { "config": { "$ref": "#/components/schemas/EnforceFileDownloadLocation" }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -2259,6 +2285,7 @@ }, "required": [ "status", + "lockStatus", "config" ], "type": "object" @@ -2518,11 +2545,8 @@ ], "type": "string" }, - "ExposeInvitationURLsToTeamAdminConfig.WithStatus": { + "ExposeInvitationURLsToTeamAdminConfig.Feature": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -2534,13 +2558,15 @@ } }, "required": [ - "status", - "lockStatus" + "status" ], "type": "object" }, - "ExposeInvitationURLsToTeamAdminConfig.WithStatusNoLock": { + "ExposeInvitationURLsToTeamAdminConfig.LockableFeature": { "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -2552,7 +2578,8 @@ } }, "required": [ - "status" + "status", + "lockStatus" ], "type": "object" }, @@ -2572,11 +2599,8 @@ ], "type": "string" }, - "FileSharingConfig.WithStatus": { + "FileSharingConfig.Feature": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -2588,13 +2612,15 @@ } }, "required": [ - "status", - "lockStatus" + "status" ], "type": "object" }, - "FileSharingConfig.WithStatusNoLock": { + "FileSharingConfig.LockableFeature": { "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -2606,7 +2632,8 @@ } }, "required": [ - "status" + "status", + "lockStatus" ], "type": "object" }, @@ -2665,11 +2692,8 @@ "GroupInfoData": { "description": "This object can only be parsed in TLS format. Please refer to the MLS specification for details." }, - "GuestLinksConfig.WithStatus": { + "GuestLinksConfig.Feature": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -2681,13 +2705,15 @@ } }, "required": [ - "status", - "lockStatus" + "status" ], "type": "object" }, - "GuestLinksConfig.WithStatusNoLock": { + "GuestLinksConfig.LockableFeature": { "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -2699,7 +2725,8 @@ } }, "required": [ - "status" + "status", + "lockStatus" ], "type": "object" }, @@ -2914,30 +2941,6 @@ ], "type": "object" }, - "JWK": { - "properties": { - "crv": { - "type": "string" - }, - "kty": { - "type": "string" - }, - "x": { - "example": "ZXhhbXBsZQo=", - "type": "string" - }, - "y": { - "example": "ZXhhbXBsZQo=", - "type": "string" - } - }, - "required": [ - "kty", - "crv", - "x" - ], - "type": "object" - }, "JoinConversationByCode": { "description": "Request body for joining a conversation by code", "properties": { @@ -3034,11 +3037,8 @@ ], "type": "string" }, - "LegalholdConfig.WithStatus": { + "LegalholdConfig.Feature": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -3050,13 +3050,15 @@ } }, "required": [ - "status", - "lockStatus" + "status" ], "type": "object" }, - "LegalholdConfig.WithStatusNoLock": { + "LegalholdConfig.LockableFeature": { "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -3068,11 +3070,12 @@ } }, "required": [ - "status" + "status", + "lockStatus" ], "type": "object" }, - "LimitedEventFanoutConfig.WithStatus": { + "LimitedEventFanoutConfig.LockableFeature": { "properties": { "lockStatus": { "$ref": "#/components/schemas/LockStatus" @@ -3269,14 +3272,11 @@ ], "type": "object" }, - "MLSConfig.WithStatus": { + "MLSConfig.Feature": { "properties": { "config": { "$ref": "#/components/schemas/MLSConfig" }, - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -3289,16 +3289,18 @@ }, "required": [ "status", - "lockStatus", "config" ], "type": "object" }, - "MLSConfig.WithStatusNoLock": { + "MLSConfig.LockableFeature": { "properties": { "config": { "$ref": "#/components/schemas/MLSConfig" }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -3311,6 +3313,7 @@ }, "required": [ "status", + "lockStatus", "config" ], "type": "object" @@ -3318,16 +3321,16 @@ "MLSKeys": { "properties": { "ecdsa_secp256r1_sha256": { - "$ref": "#/components/schemas/JWK" + "$ref": "#/components/schemas/MLSPublicKey" }, "ecdsa_secp384r1_sha384": { - "$ref": "#/components/schemas/JWK" + "$ref": "#/components/schemas/MLSPublicKey" }, "ecdsa_secp521r1_sha512": { - "$ref": "#/components/schemas/JWK" + "$ref": "#/components/schemas/MLSPublicKey" }, "ed25519": { - "$ref": "#/components/schemas/JWK" + "$ref": "#/components/schemas/MLSPublicKey" } }, "required": [ @@ -3371,6 +3374,10 @@ ], "type": "object" }, + "MLSPublicKey": { + "example": "ZXhhbXBsZQo=", + "type": "string" + }, "MLSPublicKeys": { "additionalProperties": { "example": "ZXhhbXBsZQo=", @@ -3545,7 +3552,7 @@ "type": "boolean" }, "verificationExpiration": { - "description": "When a client first tries to fetch or renew a certificate, they may need to login to an identity provider (IdP) depending on their IdP domain authentication policy. The user may have a grace period during which they can “snooze” this login. The duration of this grace period (in seconds) is set in the `verificationDuration` parameter, which is enforced separately by each client. After the grace period has expired, the client will not allow the user to use the application until they have logged to refresh the certificate. The default value is 1 day (86400s). The client enrolls using the Automatic Certificate Management Environment (ACME) protocol. The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: `https://acme.example.com/acme/provisioner1/discovery`.", + "description": "When a client first tries to fetch or renew a certificate, they may need to login to an identity provider (IdP) depending on their IdP domain authentication policy. The user may have a grace period during which they can \"snooze\" this login. The duration of this grace period (in seconds) is set in the `verificationDuration` parameter, which is enforced separately by each client. After the grace period has expired, the client will not allow the user to use the application until they have logged to refresh the certificate. The default value is 1 day (86400s). The client enrolls using the Automatic Certificate Management Environment (ACME) protocol. The `acmeDiscoveryUrl` parameter must be set to the HTTPS URL of the ACME server discovery endpoint for this team. It is of the form \"https://acme.{backendDomain}/acme/{provisionerName}/discovery\". For example: `https://acme.example.com/acme/provisioner1/discovery`.", "maximum": 9223372036854775807, "minimum": -9223372036854775808, "type": "integer" @@ -3556,14 +3563,11 @@ ], "type": "object" }, - "MlsE2EIdConfig.WithStatus": { + "MlsE2EIdConfig.Feature": { "properties": { "config": { "$ref": "#/components/schemas/MlsE2EIdConfig" }, - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -3576,16 +3580,18 @@ }, "required": [ "status", - "lockStatus", "config" ], "type": "object" }, - "MlsE2EIdConfig.WithStatusNoLock": { + "MlsE2EIdConfig.LockableFeature": { "properties": { "config": { "$ref": "#/components/schemas/MlsE2EIdConfig" }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -3598,6 +3604,7 @@ }, "required": [ "status", + "lockStatus", "config" ], "type": "object" @@ -3613,14 +3620,11 @@ }, "type": "object" }, - "MlsMigration.WithStatus": { + "MlsMigration.Feature": { "properties": { "config": { "$ref": "#/components/schemas/MlsMigration" }, - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -3633,16 +3637,18 @@ }, "required": [ "status", - "lockStatus", "config" ], "type": "object" }, - "MlsMigration.WithStatusNoLock": { + "MlsMigration.LockableFeature": { "properties": { "config": { "$ref": "#/components/schemas/MlsMigration" }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -3655,6 +3661,7 @@ }, "required": [ "status", + "lockStatus", "config" ], "type": "object" @@ -4310,11 +4317,8 @@ ], "type": "object" }, - "OutlookCalIntegrationConfig.WithStatus": { + "OutlookCalIntegrationConfig.Feature": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -4326,13 +4330,15 @@ } }, "required": [ - "status", - "lockStatus" + "status" ], "type": "object" }, - "OutlookCalIntegrationConfig.WithStatusNoLock": { + "OutlookCalIntegrationConfig.LockableFeature": { "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -4344,7 +4350,8 @@ } }, "required": [ - "status" + "status", + "lockStatus" ], "type": "object" }, @@ -5013,7 +5020,7 @@ "description": "Role name, between 2 and 128 chars, 'wire_' prefix is reserved for roles designed by Wire (i.e., no custom roles can have the same prefix)", "type": "string" }, - "SSOConfig.WithStatus": { + "SSOConfig.LockableFeature": { "properties": { "lockStatus": { "$ref": "#/components/schemas/LockStatus" @@ -5121,11 +5128,8 @@ ], "type": "object" }, - "SearchVisibilityAvailableConfig.WithStatus": { + "SearchVisibilityAvailableConfig.Feature": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -5137,13 +5141,15 @@ } }, "required": [ - "status", - "lockStatus" + "status" ], "type": "object" }, - "SearchVisibilityAvailableConfig.WithStatusNoLock": { + "SearchVisibilityAvailableConfig.LockableFeature": { "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -5155,15 +5161,13 @@ } }, "required": [ - "status" + "status", + "lockStatus" ], "type": "object" }, - "SearchVisibilityInboundConfig.WithStatus": { + "SearchVisibilityInboundConfig.Feature": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -5175,13 +5179,15 @@ } }, "required": [ - "status", - "lockStatus" + "status" ], "type": "object" }, - "SearchVisibilityInboundConfig.WithStatusNoLock": { + "SearchVisibilityInboundConfig.LockableFeature": { "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -5193,7 +5199,8 @@ } }, "required": [ - "status" + "status", + "lockStatus" ], "type": "object" }, @@ -5211,14 +5218,11 @@ ], "type": "object" }, - "SelfDeletingMessagesConfig.WithStatus": { + "SelfDeletingMessagesConfig.Feature": { "properties": { "config": { "$ref": "#/components/schemas/SelfDeletingMessagesConfig" }, - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -5231,16 +5235,18 @@ }, "required": [ "status", - "lockStatus", "config" ], "type": "object" }, - "SelfDeletingMessagesConfig.WithStatusNoLock": { + "SelfDeletingMessagesConfig.LockableFeature": { "properties": { "config": { "$ref": "#/components/schemas/SelfDeletingMessagesConfig" }, + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -5253,6 +5259,7 @@ }, "required": [ "status", + "lockStatus", "config" ], "type": "object" @@ -5468,11 +5475,8 @@ ], "type": "object" }, - "SndFactorPasswordChallengeConfig.WithStatus": { + "SndFactorPasswordChallengeConfig.Feature": { "properties": { - "lockStatus": { - "$ref": "#/components/schemas/LockStatus" - }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -5484,13 +5488,15 @@ } }, "required": [ - "status", - "lockStatus" + "status" ], "type": "object" }, - "SndFactorPasswordChallengeConfig.WithStatusNoLock": { + "SndFactorPasswordChallengeConfig.LockableFeature": { "properties": { + "lockStatus": { + "$ref": "#/components/schemas/LockStatus" + }, "status": { "$ref": "#/components/schemas/FeatureStatus" }, @@ -5502,7 +5508,8 @@ } }, "required": [ - "status" + "status", + "lockStatus" ], "type": "object" }, @@ -6437,7 +6444,7 @@ }, "type": "object" }, - "ValidateSAMLEmailsConfig.WithStatus": { + "ValidateSAMLEmailsConfig.LockableFeature": { "properties": { "lockStatus": { "$ref": "#/components/schemas/LockStatus" @@ -14677,7 +14684,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/GuestLinksConfig.WithStatus" + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" } } }, @@ -17172,7 +17179,7 @@ }, "/mls/public-keys": { "get": { - "description": " [internal route ID: \"mls-public-keys\"]\n\n", + "description": " [internal route ID: \"mls-public-keys-v6\"]\n\n", "responses": { "200": { "content": { @@ -23751,7 +23758,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AppLockConfig.WithStatus" + "$ref": "#/components/schemas/AppLockConfig.LockableFeature" } } }, @@ -23852,7 +23859,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AppLockConfig.WithStatusNoLock" + "$ref": "#/components/schemas/AppLockConfig.Feature" } } }, @@ -23863,7 +23870,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AppLockConfig.WithStatus" + "$ref": "#/components/schemas/AppLockConfig.LockableFeature" } } }, @@ -23967,7 +23974,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ClassifiedDomainsConfig.WithStatus" + "$ref": "#/components/schemas/ClassifiedDomainsConfig.LockableFeature" } } }, @@ -24071,7 +24078,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ConferenceCallingConfig.WithStatus" + "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" } } }, @@ -24154,6 +24161,118 @@ } }, "summary": "Get config for conferenceCalling" + }, + "put": { + "description": " [internal route ID: (\"put\", ConferenceCallingConfig)]\n\n", + "parameters": [ + { + "in": "path", + "name": "tid", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConferenceCallingConfig.Feature" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/ConferenceCallingConfig.LockableFeature" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "no-team-member", + "message": "Requesting user is not a team member" + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team-member", + "operation-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Requesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "no-team", + "message": "Team not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "no-team" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`tid` not found\n\nTeam not found (label: `no-team`)" + } + }, + "summary": "Put config for conferenceCalling" } }, "/teams/{tid}/features/conversationGuestLinks": { @@ -24175,7 +24294,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/GuestLinksConfig.WithStatus" + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" } } }, @@ -24276,7 +24395,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/GuestLinksConfig.WithStatusNoLock" + "$ref": "#/components/schemas/GuestLinksConfig.Feature" } } }, @@ -24287,7 +24406,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/GuestLinksConfig.WithStatus" + "$ref": "#/components/schemas/GuestLinksConfig.LockableFeature" } } }, @@ -24391,7 +24510,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/DigitalSignaturesConfig.WithStatus" + "$ref": "#/components/schemas/DigitalSignaturesConfig.LockableFeature" } } }, @@ -24478,7 +24597,7 @@ }, "/teams/{tid}/features/enforceFileDownloadLocation": { "get": { - "description": " [internal route ID: (\"get\", EnforceFileDownloadLocationConfig)]\n\n

Custom feature: only supported for some decidated on-prem systems.

", + "description": " [internal route ID: (\"get\", EnforceFileDownloadLocationConfig)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", "parameters": [ { "in": "path", @@ -24495,7 +24614,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/EnforceFileDownloadLocation.WithStatus" + "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" } } }, @@ -24580,7 +24699,7 @@ "summary": "Get config for enforceFileDownloadLocation" }, "put": { - "description": " [internal route ID: (\"put\", EnforceFileDownloadLocationConfig)]\n\n

Custom feature: only supported for some decidated on-prem systems.

", + "description": " [internal route ID: (\"put\", EnforceFileDownloadLocationConfig)]\n\n

Custom feature: only supported on some dedicated on-prem systems.

", "parameters": [ { "in": "path", @@ -24596,7 +24715,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/EnforceFileDownloadLocation.WithStatusNoLock" + "$ref": "#/components/schemas/EnforceFileDownloadLocation.Feature" } } }, @@ -24607,7 +24726,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/EnforceFileDownloadLocation.WithStatus" + "$ref": "#/components/schemas/EnforceFileDownloadLocation.LockableFeature" } } }, @@ -24711,7 +24830,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" } } }, @@ -24812,7 +24931,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.WithStatusNoLock" + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.Feature" } } }, @@ -24823,7 +24942,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.WithStatus" + "$ref": "#/components/schemas/ExposeInvitationURLsToTeamAdminConfig.LockableFeature" } } }, @@ -24927,7 +25046,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/FileSharingConfig.WithStatus" + "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" } } }, @@ -25028,7 +25147,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/FileSharingConfig.WithStatusNoLock" + "$ref": "#/components/schemas/FileSharingConfig.Feature" } } }, @@ -25039,7 +25158,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/FileSharingConfig.WithStatus" + "$ref": "#/components/schemas/FileSharingConfig.LockableFeature" } } }, @@ -25143,7 +25262,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/LegalholdConfig.WithStatus" + "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" } } }, @@ -25228,7 +25347,7 @@ "summary": "Get config for legalhold" }, "put": { - "description": " [internal route ID: (\"put\", LegalholdConfig)]\n\nCalls federation service galley on on-mls-message-sent
Calls federation service galley on on-conversation-updated", + "description": " [internal route ID: (\"put\", LegalholdConfig)]\n\n", "parameters": [ { "in": "path", @@ -25244,7 +25363,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/LegalholdConfig.WithStatusNoLock" + "$ref": "#/components/schemas/LegalholdConfig.Feature" } } }, @@ -25255,7 +25374,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/LegalholdConfig.WithStatus" + "$ref": "#/components/schemas/LegalholdConfig.LockableFeature" } } }, @@ -25319,9 +25438,6 @@ "legalhold-disable-unimplemented", "legalhold-not-enabled", "too-large-team-for-legalhold", - "code-authentication-required", - "code-authentication-failed", - "access-denied", "action-denied", "no-team-member", "operation-denied" @@ -25341,7 +25457,7 @@ } } }, - "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nCannot enable legalhold on large teams (reason: for removing LH from team, we need to iterate over all members, which is only supported for teams with less than 2k members) (label: `too-large-team-for-legalhold`)\n\nVerification code required (label: `code-authentication-required`)\n\nCode authentication failed (label: `code-authentication-failed`)\n\nThis operation requires reauthentication (label: `access-denied`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" + "description": "legal hold cannot be disabled for whitelisted teams (label: `legalhold-disable-unimplemented`)\n\nlegal hold is not enabled for this team (label: `legalhold-not-enabled`)\n\nCannot enable legalhold on large teams (reason: for removing LH from team, we need to iterate over all members, which is only supported for teams with less than 2k members) (label: `too-large-team-for-legalhold`)\n\nInsufficient authorization (missing remove_conversation_member) (label: `action-denied`)\n\nRequesting user is not a team member (label: `no-team-member`)\n\nInsufficient permissions (label: `operation-denied`)" }, "404": { "content": { @@ -25441,7 +25557,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/LimitedEventFanoutConfig.WithStatus" + "$ref": "#/components/schemas/LimitedEventFanoutConfig.LockableFeature" } } }, @@ -25545,7 +25661,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MLSConfig.WithStatus" + "$ref": "#/components/schemas/MLSConfig.LockableFeature" } } }, @@ -25646,7 +25762,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MLSConfig.WithStatusNoLock" + "$ref": "#/components/schemas/MLSConfig.Feature" } } }, @@ -25657,7 +25773,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MLSConfig.WithStatus" + "$ref": "#/components/schemas/MLSConfig.LockableFeature" } } }, @@ -25761,7 +25877,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsE2EIdConfig.WithStatus" + "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" } } }, @@ -25862,7 +25978,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsE2EIdConfig.WithStatusNoLock" + "$ref": "#/components/schemas/MlsE2EIdConfig.Feature" } } }, @@ -25873,7 +25989,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsE2EIdConfig.WithStatus" + "$ref": "#/components/schemas/MlsE2EIdConfig.LockableFeature" } } }, @@ -25977,7 +26093,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsMigration.WithStatus" + "$ref": "#/components/schemas/MlsMigration.LockableFeature" } } }, @@ -26078,7 +26194,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsMigration.WithStatusNoLock" + "$ref": "#/components/schemas/MlsMigration.Feature" } } }, @@ -26089,7 +26205,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/MlsMigration.WithStatus" + "$ref": "#/components/schemas/MlsMigration.LockableFeature" } } }, @@ -26193,7 +26309,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/OutlookCalIntegrationConfig.WithStatus" + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" } } }, @@ -26294,7 +26410,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/OutlookCalIntegrationConfig.WithStatusNoLock" + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.Feature" } } }, @@ -26305,7 +26421,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/OutlookCalIntegrationConfig.WithStatus" + "$ref": "#/components/schemas/OutlookCalIntegrationConfig.LockableFeature" } } }, @@ -26409,7 +26525,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.WithStatus" + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" } } }, @@ -26510,7 +26626,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.WithStatusNoLock" + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.Feature" } } }, @@ -26521,7 +26637,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.WithStatus" + "$ref": "#/components/schemas/SearchVisibilityAvailableConfig.LockableFeature" } } }, @@ -26625,7 +26741,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SearchVisibilityInboundConfig.WithStatus" + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" } } }, @@ -26726,7 +26842,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SearchVisibilityInboundConfig.WithStatusNoLock" + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.Feature" } } }, @@ -26737,7 +26853,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SearchVisibilityInboundConfig.WithStatus" + "$ref": "#/components/schemas/SearchVisibilityInboundConfig.LockableFeature" } } }, @@ -26841,7 +26957,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SelfDeletingMessagesConfig.WithStatus" + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" } } }, @@ -26942,7 +27058,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SelfDeletingMessagesConfig.WithStatusNoLock" + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.Feature" } } }, @@ -26953,7 +27069,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SelfDeletingMessagesConfig.WithStatus" + "$ref": "#/components/schemas/SelfDeletingMessagesConfig.LockableFeature" } } }, @@ -27057,7 +27173,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.WithStatus" + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" } } }, @@ -27158,7 +27274,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.WithStatusNoLock" + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.Feature" } } }, @@ -27169,7 +27285,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.WithStatus" + "$ref": "#/components/schemas/SndFactorPasswordChallengeConfig.LockableFeature" } } }, @@ -27273,7 +27389,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/SSOConfig.WithStatus" + "$ref": "#/components/schemas/SSOConfig.LockableFeature" } } }, @@ -27377,7 +27493,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.WithStatus" + "$ref": "#/components/schemas/ValidateSAMLEmailsConfig.LockableFeature" } } }, From d8f5a9e05cee4db9d0df4e0330a35325d2e8021a Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 19 Aug 2024 10:02:26 +0200 Subject: [PATCH 047/136] Feature flag refactoring (part 3) (#4196) --- .../5-internal/feature-flag-refactoring-3 | 1 + libs/galley-types/default.nix | 4 +- libs/galley-types/galley-types.cabal | 2 +- libs/galley-types/src/Galley/Types/Teams.hs | 370 ++++++++++++------ .../src/Wire/API/Routes/Internal/Galley.hs | 2 +- libs/wire-api/src/Wire/API/Routes/Named.hs | 16 +- .../Wire/API/Routes/Public/Galley/Feature.hs | 12 +- libs/wire-api/src/Wire/API/Team/Feature.hs | 197 ++++------ .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 2 +- .../src/Wire/GalleyAPIAccess.hs | 4 +- .../src/Wire/GalleyAPIAccess/Rpc.hs | 8 +- .../src/Wire/UserSubsystem/Interpreter.hs | 4 +- .../test/unit/Wire/MiniBackend.hs | 6 +- .../Wire/MockInterpreters/GalleyAPIAccess.hs | 6 +- services/brig/brig.cabal | 2 - services/brig/default.nix | 1 - services/brig/src/Brig/API/Internal.hs | 22 +- services/brig/src/Brig/API/User.hs | 7 +- services/brig/src/Brig/Calling/API.hs | 6 +- services/brig/src/Brig/Data/User.hs | 22 +- services/brig/src/Brig/Options.hs | 132 +++---- services/brig/src/Brig/Provider/API.hs | 2 +- services/brig/test/integration/API/Team.hs | 2 +- services/brig/test/unit/Run.hs | 2 - .../brig/test/unit/Test/Brig/Roundtrip.hs | 43 -- services/galley/galley.cabal | 2 +- services/galley/src/Galley/API/Action.hs | 6 +- services/galley/src/Galley/API/Internal.hs | 20 +- .../src/Galley/API/LegalHold/Conflicts.hs | 5 +- .../galley/src/Galley/API/LegalHold/Team.hs | 2 +- .../galley/src/Galley/API/Public/Feature.hs | 58 +-- services/galley/src/Galley/API/Query.hs | 4 +- services/galley/src/Galley/API/Teams.hs | 2 +- .../galley/src/Galley/API/Teams/Features.hs | 225 ++++++----- .../src/Galley/API/Teams/Features/Get.hs | 268 ++++++------- services/galley/src/Galley/API/Update.hs | 3 +- services/galley/src/Galley/API/Util.hs | 7 +- services/galley/src/Galley/App.hs | 15 +- ...eatureConfigs.hs => GetAllTeamFeatures.hs} | 6 +- .../galley/src/Galley/Cassandra/LegalHold.hs | 5 +- services/galley/src/Galley/Cassandra/Team.hs | 23 +- .../src/Galley/Cassandra/TeamFeatures.hs | 38 +- services/galley/src/Galley/Effects.hs | 8 +- .../src/Galley/Effects/TeamFeatureStore.hs | 41 +- .../galley/src/Galley/Effects/TeamStore.hs | 3 +- services/galley/test/integration/API/Teams.hs | 2 +- .../integration/API/Teams/LegalHold/Util.hs | 5 +- .../test/integration/API/Util/TeamFeature.hs | 10 +- 48 files changed, 840 insertions(+), 793 deletions(-) create mode 100644 changelog.d/5-internal/feature-flag-refactoring-3 delete mode 100644 services/brig/test/unit/Test/Brig/Roundtrip.hs rename services/galley/src/Galley/Cassandra/{GetAllTeamFeatureConfigs.hs => GetAllTeamFeatures.hs} (94%) diff --git a/changelog.d/5-internal/feature-flag-refactoring-3 b/changelog.d/5-internal/feature-flag-refactoring-3 new file mode 100644 index 00000000000..62a75a4b38d --- /dev/null +++ b/changelog.d/5-internal/feature-flag-refactoring-3 @@ -0,0 +1 @@ +Clean up feature default configuration code diff --git a/libs/galley-types/default.nix b/libs/galley-types/default.nix index c7b207c15d7..4edd7e398d8 100644 --- a/libs/galley-types/default.nix +++ b/libs/galley-types/default.nix @@ -16,7 +16,7 @@ , lens , lib , memory -, QuickCheck +, sop-core , text , types-common , utf8-string @@ -39,7 +39,7 @@ mkDerivation { imports lens memory - QuickCheck + sop-core text types-common utf8-string diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index 2a13877a49c..eb99c1afbb8 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -80,7 +80,7 @@ library , imports , lens >=4.12 , memory - , QuickCheck + , sop-core , text >=0.11 , types-common >=0.16 , utf8-string diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 13c2c063653..5e50181ecd7 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -4,6 +4,7 @@ {-# LANGUAGE StandaloneKindSignatures #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. -- @@ -25,32 +26,10 @@ module Galley.Types.Teams ( TeamCreationTime (..), tcTime, - FeatureFlags (..), - flagSSO, - flagLegalHold, - flagTeamSearchVisibility, - flagFileSharing, - flagAppLockDefaults, - flagClassifiedDomains, - flagConferenceCalling, - flagSelfDeletingMessages, - flagConversationGuestLinks, - flagsTeamFeatureValidateSAMLEmailsStatus, - flagTeamFeatureSndFactorPasswordChallengeStatus, - flagTeamFeatureSearchVisibilityInbound, - flagOutlookCalIntegration, - flagMLS, - flagMlsE2EId, - flagMlsMigration, - flagEnforceFileDownloadLocation, - flagLimitedEventFanout, - Defaults (..), - ImplicitLockStatus (..), - unImplicitLockStatus, - unDefaults, - FeatureSSO (..), - FeatureLegalHold (..), - FeatureTeamSearchVisibilityAvailability (..), + GetFeatureDefaults (..), + FeatureDefaults (..), + FeatureFlags, + featureDefaults, notTeamMember, findTeamMember, isTeamMember, @@ -61,14 +40,15 @@ where import Control.Lens (makeLenses, view) import Data.Aeson +import Data.Aeson.Key qualified as Key import Data.Aeson.Types qualified as A import Data.ByteString (toStrict) import Data.ByteString.UTF8 qualified as UTF8 import Data.Default import Data.Id (UserId) +import Data.SOP import Data.Set qualified as Set import Imports -import Test.QuickCheck (Arbitrary) import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Permission @@ -78,115 +58,277 @@ newtype TeamCreationTime = TeamCreationTime { _tcTime :: Int64 } -data FeatureFlags = FeatureFlags - { _flagSSO :: !FeatureSSO, - _flagLegalHold :: !FeatureLegalHold, - _flagTeamSearchVisibility :: !FeatureTeamSearchVisibilityAvailability, - _flagAppLockDefaults :: !(Defaults (ImplicitLockStatus AppLockConfig)), - _flagClassifiedDomains :: !(ImplicitLockStatus ClassifiedDomainsConfig), - _flagFileSharing :: !(Defaults (LockableFeature FileSharingConfig)), - _flagConferenceCalling :: !(Defaults (LockableFeature ConferenceCallingConfig)), - _flagSelfDeletingMessages :: !(Defaults (LockableFeature SelfDeletingMessagesConfig)), - _flagConversationGuestLinks :: !(Defaults (LockableFeature GuestLinksConfig)), - _flagsTeamFeatureValidateSAMLEmailsStatus :: !(Defaults (ImplicitLockStatus ValidateSAMLEmailsConfig)), - _flagTeamFeatureSndFactorPasswordChallengeStatus :: !(Defaults (LockableFeature SndFactorPasswordChallengeConfig)), - _flagTeamFeatureSearchVisibilityInbound :: !(Defaults (ImplicitLockStatus SearchVisibilityInboundConfig)), - _flagMLS :: !(Defaults (LockableFeature MLSConfig)), - _flagOutlookCalIntegration :: !(Defaults (LockableFeature OutlookCalIntegrationConfig)), - _flagMlsE2EId :: !(Defaults (LockableFeature MlsE2EIdConfig)), - _flagMlsMigration :: !(Defaults (LockableFeature MlsMigrationConfig)), - _flagEnforceFileDownloadLocation :: !(Defaults (LockableFeature EnforceFileDownloadLocationConfig)), - _flagLimitedEventFanout :: !(Defaults (ImplicitLockStatus LimitedEventFanoutConfig)) - } - deriving (Eq, Show, Generic) +-- | Used to extract the feature config type out of 'FeatureDefaults' or +-- related types. +type family ConfigOf a -newtype Defaults a = Defaults {_unDefaults :: a} - deriving (Eq, Ord, Show, Enum, Bounded, Generic, Functor) - deriving newtype (Arbitrary) +type instance ConfigOf (FeatureDefaults cfg) = cfg -instance (FromJSON a) => FromJSON (Defaults a) where - parseJSON = withObject "default object" $ \ob -> - Defaults <$> (ob .: "defaults") +-- | Convert a feature default value to an actual 'LockableFeature'. +class GetFeatureDefaults a where + featureDefaults1 :: a -> LockableFeature (ConfigOf a) -instance (ToJSON a) => ToJSON (Defaults a) where - toJSON (Defaults x) = - object ["defaults" .= toJSON x] +type instance ConfigOf (Feature cfg) = cfg -data FeatureSSO - = FeatureSSOEnabledByDefault - | FeatureSSODisabledByDefault - deriving (Eq, Ord, Show, Enum, Bounded, Generic) +instance (IsFeatureConfig cfg) => GetFeatureDefaults (Feature cfg) where + featureDefaults1 = withLockStatus (def @(LockableFeature cfg)).lockStatus + +-- | Some features do not have a configured default value, so this takes it +-- wholly from the 'Default' instance. +newtype FixedDefaults cfg = FixedDefaults (FeatureDefaults cfg) + +type instance ConfigOf (FixedDefaults cfg) = cfg -data FeatureLegalHold +instance (IsFeatureConfig cfg) => GetFeatureDefaults (FixedDefaults cfg) where + featureDefaults1 _ = def + +type instance ConfigOf (LockableFeature cfg) = cfg + +instance GetFeatureDefaults (LockableFeature cfg) where + featureDefaults1 = id + +data family FeatureDefaults cfg + +data instance FeatureDefaults LegalholdConfig = FeatureLegalHoldDisabledPermanently | FeatureLegalHoldDisabledByDefault | FeatureLegalHoldWhitelistTeamsAndImplicitConsent - deriving (Eq, Ord, Show, Enum, Bounded, Generic) + deriving stock (Eq, Ord, Show) + deriving (ParseFeatureDefaults) via RequiredField LegalholdConfig + deriving (GetFeatureDefaults) via FixedDefaults LegalholdConfig --- | Default value for all teams that have not enabled or disabled this feature explicitly. -data FeatureTeamSearchVisibilityAvailability - = FeatureTeamSearchVisibilityAvailableByDefault - | FeatureTeamSearchVisibilityUnavailableByDefault - deriving (Eq, Ord, Show, Enum, Bounded, Generic) - --- NOTE: This is used only in the config and thus YAML... camelcase -instance FromJSON FeatureFlags where - parseJSON = withObject "FeatureFlags" $ \obj -> - FeatureFlags - <$> obj .: "sso" - <*> obj .: "legalhold" - <*> obj .: "teamSearchVisibility" - <*> withImplicitLockStatusOrDefault obj "appLock" - <*> (fromMaybe (ImplicitLockStatus def) <$> (obj .:? "classifiedDomains")) - <*> (fromMaybe (Defaults def) <$> (obj .:? "fileSharing")) - <*> (fromMaybe (Defaults def) <$> (obj .:? "conferenceCalling")) - <*> (fromMaybe (Defaults def) <$> (obj .:? "selfDeletingMessages")) - <*> (fromMaybe (Defaults def) <$> (obj .:? "conversationGuestLinks")) - <*> withImplicitLockStatusOrDefault obj "validateSAMLEmails" - <*> (fromMaybe (Defaults def) <$> (obj .:? "sndFactorPasswordChallenge")) - <*> withImplicitLockStatusOrDefault obj "searchVisibilityInbound" - <*> (fromMaybe (Defaults def) <$> (obj .:? "mls")) - <*> (fromMaybe (Defaults def) <$> (obj .:? "outlookCalIntegration")) - <*> (fromMaybe (Defaults def) <$> (obj .:? "mlsE2EId")) - <*> (fromMaybe (Defaults def) <$> (obj .:? "mlsMigration")) - <*> (fromMaybe (Defaults def) <$> (obj .:? "enforceFileDownloadLocation")) - <*> withImplicitLockStatusOrDefault obj "limitedEventFanout" - where - withImplicitLockStatusOrDefault :: forall cfg. (IsFeatureConfig cfg) => Object -> Key -> A.Parser (Defaults (ImplicitLockStatus cfg)) - withImplicitLockStatusOrDefault obj fieldName = fromMaybe (Defaults (ImplicitLockStatus def)) <$> obj .:? fieldName - -instance FromJSON FeatureSSO where +instance FromJSON (FeatureDefaults LegalholdConfig) where + parseJSON (String "disabled-permanently") = pure $ FeatureLegalHoldDisabledPermanently + parseJSON (String "disabled-by-default") = pure $ FeatureLegalHoldDisabledByDefault + parseJSON (String "whitelist-teams-and-implicit-consent") = pure FeatureLegalHoldWhitelistTeamsAndImplicitConsent + parseJSON bad = fail $ "FeatureLegalHold: " <> (UTF8.toString . toStrict . encode $ bad) + +data instance FeatureDefaults SSOConfig + = FeatureSSOEnabledByDefault + | FeatureSSODisabledByDefault + deriving stock (Eq, Ord, Show) + deriving (ParseFeatureDefaults) via RequiredField SSOConfig + +instance FromJSON (FeatureDefaults SSOConfig) where parseJSON (String "enabled-by-default") = pure FeatureSSOEnabledByDefault parseJSON (String "disabled-by-default") = pure FeatureSSODisabledByDefault parseJSON bad = fail $ "FeatureSSO: " <> (UTF8.toString . toStrict . encode $ bad) -instance ToJSON FeatureSSO where - toJSON FeatureSSOEnabledByDefault = String "enabled-by-default" - toJSON FeatureSSODisabledByDefault = String "disabled-by-default" +instance GetFeatureDefaults (FeatureDefaults SSOConfig) where + featureDefaults1 flag = + def + { status = case flag of + FeatureSSOEnabledByDefault -> FeatureStatusEnabled + FeatureSSODisabledByDefault -> FeatureStatusDisabled + } -instance FromJSON FeatureLegalHold where - parseJSON (String "disabled-permanently") = pure $ FeatureLegalHoldDisabledPermanently - parseJSON (String "disabled-by-default") = pure $ FeatureLegalHoldDisabledByDefault - parseJSON (String "whitelist-teams-and-implicit-consent") = pure FeatureLegalHoldWhitelistTeamsAndImplicitConsent - parseJSON bad = fail $ "FeatureLegalHold: " <> (UTF8.toString . toStrict . encode $ bad) +-- | Default value for all teams that have not enabled or disabled this feature explicitly. +data instance FeatureDefaults SearchVisibilityAvailableConfig + = FeatureTeamSearchVisibilityAvailableByDefault + | FeatureTeamSearchVisibilityUnavailableByDefault + deriving stock (Eq, Ord, Show) -instance ToJSON FeatureLegalHold where - toJSON FeatureLegalHoldDisabledPermanently = String "disabled-permanently" - toJSON FeatureLegalHoldDisabledByDefault = String "disabled-by-default" - toJSON FeatureLegalHoldWhitelistTeamsAndImplicitConsent = String "whitelist-teams-and-implicit-consent" +instance ParseFeatureDefaults (FeatureDefaults SearchVisibilityAvailableConfig) where + parseFeatureDefaults obj = obj .: "teamSearchVisibility" -instance FromJSON FeatureTeamSearchVisibilityAvailability where +instance FromJSON (FeatureDefaults SearchVisibilityAvailableConfig) where parseJSON (String "enabled-by-default") = pure FeatureTeamSearchVisibilityAvailableByDefault parseJSON (String "disabled-by-default") = pure FeatureTeamSearchVisibilityUnavailableByDefault parseJSON bad = fail $ "FeatureSearchVisibility: " <> (UTF8.toString . toStrict . encode $ bad) -instance ToJSON FeatureTeamSearchVisibilityAvailability where - toJSON FeatureTeamSearchVisibilityAvailableByDefault = String "enabled-by-default" - toJSON FeatureTeamSearchVisibilityUnavailableByDefault = String "disabled-by-default" +instance GetFeatureDefaults (FeatureDefaults SearchVisibilityAvailableConfig) where + featureDefaults1 flag = + def + { status = case flag of + FeatureTeamSearchVisibilityAvailableByDefault -> FeatureStatusEnabled + FeatureTeamSearchVisibilityUnavailableByDefault -> FeatureStatusDisabled + } + +newtype instance FeatureDefaults SearchVisibilityInboundConfig + = SearchVisibilityInboundDefaults (Feature SearchVisibilityInboundConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (Feature SearchVisibilityInboundConfig) + deriving (ParseFeatureDefaults) via OptionalField SearchVisibilityInboundConfig + +newtype instance FeatureDefaults ValidateSAMLEmailsConfig + = ValidateSAMLEmailsDefaults (Feature ValidateSAMLEmailsConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (Feature ValidateSAMLEmailsConfig) + deriving (ParseFeatureDefaults) via OptionalField ValidateSAMLEmailsConfig + +data instance FeatureDefaults DigitalSignaturesConfig = DigitalSignaturesDefaults + deriving stock (Eq, Show) + deriving (GetFeatureDefaults) via FixedDefaults DigitalSignaturesConfig + +instance ParseFeatureDefaults (FeatureDefaults DigitalSignaturesConfig) where + parseFeatureDefaults _ = pure DigitalSignaturesDefaults + +newtype instance FeatureDefaults AppLockConfig + = AppLockDefaults (Feature AppLockConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (Feature AppLockConfig) + deriving (ParseFeatureDefaults) via OptionalField AppLockConfig + +newtype instance FeatureDefaults FileSharingConfig + = FileSharingDefaults (LockableFeature FileSharingConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature FileSharingConfig) + deriving (ParseFeatureDefaults) via OptionalField FileSharingConfig + +newtype instance FeatureDefaults ClassifiedDomainsConfig + = ClassifiedDomainsDefaults (Feature ClassifiedDomainsConfig) + deriving stock (Eq, Show) + deriving newtype (Default, FromJSON) + deriving (ParseFeatureDefaults) via OptionalField ClassifiedDomainsConfig + deriving (GetFeatureDefaults) via Feature ClassifiedDomainsConfig + +newtype instance FeatureDefaults ConferenceCallingConfig + = ConferenceCallingDefaults (LockableFeature ConferenceCallingConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature ConferenceCallingConfig) + deriving (ParseFeatureDefaults) via OptionalField ConferenceCallingConfig + +newtype instance FeatureDefaults SelfDeletingMessagesConfig + = SelfDeletingMessagesDefaults (LockableFeature SelfDeletingMessagesConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature SelfDeletingMessagesConfig) + deriving (ParseFeatureDefaults) via OptionalField SelfDeletingMessagesConfig + +newtype instance FeatureDefaults GuestLinksConfig + = GuestLinksDefaults (LockableFeature GuestLinksConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature GuestLinksConfig) + deriving (ParseFeatureDefaults) via OptionalField GuestLinksConfig + +newtype instance FeatureDefaults SndFactorPasswordChallengeConfig + = SndFactorPasswordChallengeDefaults (LockableFeature SndFactorPasswordChallengeConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature SndFactorPasswordChallengeConfig) + deriving (ParseFeatureDefaults) via OptionalField SndFactorPasswordChallengeConfig + +newtype instance FeatureDefaults MLSConfig = MLSDefaults (LockableFeature MLSConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature MLSConfig) + deriving (ParseFeatureDefaults) via OptionalField MLSConfig + +data instance FeatureDefaults ExposeInvitationURLsToTeamAdminConfig + = ExposeInvitationURLsToTeamAdminDefaults + deriving stock (Eq, Show) + deriving (GetFeatureDefaults) via FixedDefaults ExposeInvitationURLsToTeamAdminConfig + +instance ParseFeatureDefaults (FeatureDefaults ExposeInvitationURLsToTeamAdminConfig) where + parseFeatureDefaults _ = pure ExposeInvitationURLsToTeamAdminDefaults + +newtype instance FeatureDefaults OutlookCalIntegrationConfig + = OutlookCalIntegrationDefaults (LockableFeature OutlookCalIntegrationConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature OutlookCalIntegrationConfig) + deriving (ParseFeatureDefaults) via OptionalField OutlookCalIntegrationConfig + +newtype instance FeatureDefaults MlsE2EIdConfig + = MlsE2EIdDefaults (LockableFeature MlsE2EIdConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature MlsE2EIdConfig) + deriving (ParseFeatureDefaults) via OptionalField MlsE2EIdConfig + +newtype instance FeatureDefaults MlsMigrationConfig + = MlsMigrationDefaults (LockableFeature MlsMigrationConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature MlsMigrationConfig) + deriving (ParseFeatureDefaults) via OptionalField MlsMigrationConfig + +newtype instance FeatureDefaults EnforceFileDownloadLocationConfig + = EnforceFileDownloadLocationDefaults (LockableFeature EnforceFileDownloadLocationConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (LockableFeature EnforceFileDownloadLocationConfig) + deriving (ParseFeatureDefaults) via OptionalField EnforceFileDownloadLocationConfig + +newtype instance FeatureDefaults LimitedEventFanoutConfig + = LimitedEventFanoutDefaults (Feature LimitedEventFanoutConfig) + deriving stock (Eq, Show) + deriving newtype (Default, GetFeatureDefaults) + deriving (FromJSON) via Defaults (Feature LimitedEventFanoutConfig) + deriving (ParseFeatureDefaults) via OptionalField LimitedEventFanoutConfig + +featureKey :: forall cfg. (IsFeatureConfig cfg) => Key.Key +featureKey = Key.fromText $ featureName @cfg + +class ParseFeatureDefaults a where + parseFeatureDefaults :: A.Object -> A.Parser a + +newtype RequiredField cfg = RequiredField (FeatureDefaults cfg) + +instance + (IsFeatureConfig cfg, FromJSON (FeatureDefaults cfg)) => + ParseFeatureDefaults (RequiredField cfg) + where + parseFeatureDefaults obj = RequiredField <$> obj .: featureKey @cfg + +newtype OptionalField cfg = OptionalField (FeatureDefaults cfg) + +instance + ( IsFeatureConfig cfg, + Default (FeatureDefaults cfg), + FromJSON (FeatureDefaults cfg) + ) => + ParseFeatureDefaults (OptionalField cfg) + where + parseFeatureDefaults obj = OptionalField <$> obj .:? featureKey @cfg .!= def + +type FeatureFlags = AllFeatures FeatureDefaults + +featureDefaults :: + forall cfg. + ( GetFeatureDefaults (FeatureDefaults cfg), + NpProject cfg Features + ) => + FeatureFlags -> + LockableFeature cfg +featureDefaults = featureDefaults1 . npProject + +class FeatureFlagsFromObject f cfgs where + featureFlagsFromObject :: A.Object -> A.Parser (NP f cfgs) + +instance FeatureFlagsFromObject f '[] where + featureFlagsFromObject _ = pure Nil + +instance + ( ParseFeatureDefaults (f cfg), + FeatureFlagsFromObject f cfgs + ) => + FeatureFlagsFromObject f (cfg : cfgs) + where + featureFlagsFromObject obj = + (:*) + <$> parseFeatureDefaults obj + <*> featureFlagsFromObject obj + +instance + (FeatureFlagsFromObject FeatureDefaults Features) => + FromJSON FeatureFlags + where + parseJSON = withObject "FeatureFlags" featureFlagsFromObject + +newtype Defaults a = Defaults {_unDefaults :: a} + +instance (FromJSON a) => FromJSON (Defaults a) where + parseJSON = withObject "default object" $ \ob -> + Defaults <$> (ob .: "defaults") makeLenses ''TeamCreationTime -makeLenses ''FeatureFlags -makeLenses ''Defaults notTeamMember :: [UserId] -> [TeamMember] -> [UserId] notTeamMember uids tmms = 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 f05386dff80..fb4ab2cc9e5 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -105,7 +105,7 @@ type IFeatureAPI = ] "user_id" UserId - :> Get '[JSON] AllFeatureConfigs + :> Get '[JSON] AllTeamFeatures ) type InternalAPI = "i" :> InternalAPIBase diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index acfd4e79fae..91f702dd412 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -26,6 +26,7 @@ import Data.OpenApi.Lens hiding (HasServer) import Data.OpenApi.Operation import Data.Proxy import Data.Text qualified as T +import GHC.Generics import GHC.TypeLits import Imports import Servant @@ -42,12 +43,23 @@ newtype Named name x = Named {unnamed :: x} class RenderableSymbol a where renderSymbol :: Text -instance {-# OVERLAPPABLE #-} (KnownSymbol a) => RenderableSymbol a where +instance (KnownSymbol a) => RenderableSymbol a where renderSymbol = T.pack . show $ symbolVal (Proxy @a) -instance {-# OVERLAPPING #-} (RenderableSymbol a, RenderableSymbol b) => RenderableSymbol '(a, b) where +instance (RenderableSymbol a, RenderableSymbol b) => RenderableSymbol '(a, b) where renderSymbol = "(" <> (renderSymbol @a) <> ", " <> (renderSymbol @b) <> ")" +newtype RenderableTypeName a = RenderableTypeName a + +instance (GRenderableSymbol (Rep a)) => RenderableSymbol (RenderableTypeName a) where + renderSymbol = grenderSymbol @(Rep a) + +class GRenderableSymbol f where + grenderSymbol :: Text + +instance (KnownSymbol tyName) => GRenderableSymbol (D1 (MetaData tyName modName pkg b) k) where + grenderSymbol = T.pack $ symbolVal (Proxy @tyName) + instance (HasOpenApi api, RenderableSymbol name) => HasOpenApi (Named name api) where toOpenApi _ = toOpenApi (Proxy @api) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs index 92b015e4062..55c6bf0c810 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs @@ -60,8 +60,8 @@ type FeatureAPI = :<|> From 'V5 ::> FeatureAPIGetPut MlsMigrationConfig :<|> From 'V5 ::> FeatureAPIGetPut EnforceFileDownloadLocationConfig :<|> From 'V5 ::> FeatureAPIGet LimitedEventFanoutConfig - :<|> AllFeatureConfigsUserGet - :<|> AllFeatureConfigsTeamGet + :<|> AllTeamFeaturesUserGet + :<|> AllTeamFeaturesTeamGet :<|> DeprecatedFeatureAPI :<|> AllDeprecatedFeatureConfigAPI DeprecatedFeatureConfigs @@ -205,7 +205,7 @@ type FeatureConfigDeprecatedGet desc featureConfig = :> Get '[Servant.JSON] (LockableFeature featureConfig) ) -type AllFeatureConfigsUserGet = +type AllTeamFeaturesUserGet = Named "get-all-feature-configs-for-user" ( Summary @@ -219,10 +219,10 @@ type AllFeatureConfigsUserGet = :> CanThrow OperationDenied :> CanThrow 'TeamNotFound :> "feature-configs" - :> Get '[Servant.JSON] AllFeatureConfigs + :> Get '[Servant.JSON] AllTeamFeatures ) -type AllFeatureConfigsTeamGet = +type AllTeamFeaturesTeamGet = Named "get-all-feature-configs-for-team" ( Summary "Gets feature configs for a team" @@ -234,7 +234,7 @@ type AllFeatureConfigsTeamGet = :> "teams" :> Capture "tid" TeamId :> "features" - :> Get '[JSON] AllFeatureConfigs + :> Get '[JSON] AllTeamFeatures ) type SearchVisibilityGet = diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 525a8be49dc..7c1497edeb4 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -1,8 +1,6 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. @@ -79,14 +77,12 @@ module Wire.API.Team.Feature npProject, NpUpdate (..), npUpdate, - AllFeatureConfigs, - unImplicitLockStatus, - ImplicitLockStatus (..), + AllTeamFeatures, ) where import Cassandra.CQL qualified as Cass -import Control.Lens (makeLenses, (?~)) +import Control.Lens ((?~)) import Data.Aeson qualified as A import Data.Aeson.Types qualified as A import Data.Attoparsec.ByteString qualified as Parser @@ -120,7 +116,7 @@ import Test.QuickCheck.Arbitrary (arbitrary) import Test.QuickCheck.Gen (suchThat) import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite (CipherSuiteTag (MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519)) -import Wire.API.Routes.Named (RenderableSymbol (renderSymbol)) +import Wire.API.Routes.Named import Wire.Arbitrary (Arbitrary, GenericUniform (..)) ---------------------------------------------------------------------- @@ -128,54 +124,60 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- | Checklist for adding a new feature -- --- 1. Add a data type for your feature's "config" part, naming convention: --- **Config**. If your feature doesn't have a config besides --- being enabled/disabled, locked/unlocked, then the config should be a unit --- type, e.g. **data MyFeatureConfig = MyFeatureConfig**. Add a singleton for --- the new data type. Implement type classes 'RenderableSymbol', 'ToSchema', --- 'IsFeatureConfig' and 'Arbitrary'. +-- Assume we want to add a new feature called @dummy@. Every appearance of +-- @dummy@ or @Dummy@ in the following has to be replaced with the actual name +-- of the feature being added. -- --- 2. Add the config to 'AllFeatureConfigs'. +-- 1. Create a new type in this module for the feature configuration, called +-- @DummyConfig@. If your feature doesn't have a config besides being 'status' +-- and 'lockStatus', then the config should be a unit type, e.g. @data +-- DummyConfig = DummyConfig@. Derive 'Eq', 'Show', 'Generic', 'Arbitrary', +-- 'RenderableSymbol', 'FromJSON', 'ToJSON' and 'S.ToSchema'. Implement a +-- 'ToSchema' instance. Add a singleton. Add the config type to 'Features'. -- --- 3. If your feature is configurable on a per-team basis, add a schema --- migration in galley and extend 'getFeatureStatus' and similar functions in --- Galley.Cassandra.TeamFeatures +-- 2. Create a schema migration in galley, adding a column for each +-- configurable value of the feature. The new columns must contain all the +-- information needed to reconstruct a value of type 'LockableFeature +-- DummyConfig'. -- --- 4. Add the feature to the config schema of galley in Galley.Types.Teams. --- and extend the Arbitrary instance of FeatureConfigs in the unit tests --- Test.Galley.Types +-- 3. In 'Galley.Cassandra.MakeFeature', implement the 'MakeFeature' type +-- class: set 'FeatureRow' to the list of types of the rows added by the +-- migration. If the lock status is configurable (it should be in most cases), +-- it must be the first in the list. Set 'featureColumns' to the names of the +-- columns, in the same order. Implement `rowToFeature` and `featureToRow`. -- --- 5. Implement 'GetFeatureConfig' and 'SetFeatureConfig' in --- Galley.API.Teams.Features which defines the main business logic for getting --- and setting (with side-effects). Note that we don't have to check the --- lockstatus inside 'setConfigForTeam' because the lockstatus is checked in --- 'setFeatureStatus' before which is the public API for setting the feature --- status. +-- 4. Implement 'GetFeatureConfig' and 'SetFeatureConfig' in +-- 'Galley.API.Teams.Features'. Empty instances will work fine unless this +-- feature requires custom logic. -- --- 6. Add public routes to Wire.API.Routes.Public.Galley.Feature: --- 'FeatureStatusGet', 'FeatureStatusPut' (optional). Then implement them in --- Galley.API.Public.Feature. +-- 5. Add a public route to 'Wire.API.Routes.Public.Galley.Feature' and the +-- corresponding implementation in 'Galley.API.Public.Feature'. -- --- 7. Add internal routes in Wire.API.Routes.Internal.Galley and implement them --- in Galley.API.Internal. +-- 6. Add an internal route in 'Wire.API.Routes.Internal.Galley' and the +-- corresponding implementation in 'Galley.API.Internal'. -- --- 8. If the feature should be configurable via Stern add routes to Stern.API. +-- 7. If the feature should be configurable via Stern add routes to Stern.API. -- Manually check that the swagger looks okay and works. -- --- 9. If the feature is configured on a per-user level, see the --- 'ConferenceCallingConfig' as an example. --- (https://github.com/wireapp/wire-server/pull/1811, --- https://github.com/wireapp/wire-server/pull/1818) +-- 8. In 'Galley.Types.Team', add a new data instance @DummyDefaults@ to +-- represent the server-wide feature defaults read from the configuration file. +-- In most cases, this should be a newtype over 'LockableFeature DummyConfig'. +-- Then derive all the instances like for the other features in that module. +-- Note that 'ParseFeatureDefaults' can be derived either via 'OptionalField' +-- or 'RequiredField', depending on whether the feature configuration should be +-- optional or required. -- --- 10. Extend the integration tests with cases. +-- 9. If necessary, add configuration for the feature in +-- 'galley.integration.yaml', update the config map in +-- 'charts/galley/templates/configmap.yaml' and set defaults in +-- 'charts/galley/values.yaml'. Make sure that the configuration for CI matches +-- the local one, or adjust 'hack/helm_vars/wire-server/values.yaml' +-- accordingly. -- --- 11. If applicable, edit/update the configurations: --- - optionally add the config for local integration tests to 'galley.integration.yaml' --- - add a config mapping to 'charts/galley/templates/configmap.yaml' --- - add the defaults to 'charts/galley/values.yaml' --- - optionally add config for CI to 'hack/helm_vars/wire-server/values.yaml' +-- 10. Add the default values of this feature in 'testAllFeatures' +-- ('Test.FeatureFlags'). Add feature-specific integration tests. -- --- 12. Add a section to the documentation at an appropriate place +-- 11. Add a section to the documentation at an appropriate place -- (e.g. 'docs/src/developer/reference/config-options.md' (if applicable) or -- 'docs/src/understand/team-feature-settings.md') class @@ -538,15 +540,6 @@ instance ToSchema LockStatusResponse where LockStatusResponse <$> _unlockStatus .= field "lockStatus" schema -newtype ImplicitLockStatus (cfg :: Type) = ImplicitLockStatus {_unImplicitLockStatus :: LockableFeature cfg} - deriving newtype (Eq, Show, Arbitrary) - -instance (IsFeatureConfig a, ToSchema a) => ToJSON (ImplicitLockStatus a) where - toJSON (ImplicitLockStatus a) = A.toJSON $ forgetLock a - -instance (IsFeatureConfig a, ToSchema a) => FromJSON (ImplicitLockStatus a) where - parseJSON v = ImplicitLockStatus . withLockStatus ((def @(LockableFeature a)).lockStatus) <$> A.parseJSON v - -- | Convert a feature coming from the database to its public form. This can be -- overridden on a feature basis by implementing the `computeFeature` method of -- the `GetFeatureConfig` class. @@ -563,13 +556,11 @@ genericComputeFeature defFeature dbFeature = data GuestLinksConfig = GuestLinksConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform GuestLinksConfig) + deriving (RenderableSymbol) via (RenderableTypeName GuestLinksConfig) instance Default GuestLinksConfig where def = GuestLinksConfig -instance RenderableSymbol GuestLinksConfig where - renderSymbol = "GuestLinksConfig" - instance ToSchema GuestLinksConfig where schema = object "GuestLinksConfig" objectSchema @@ -588,13 +579,11 @@ instance IsFeatureConfig GuestLinksConfig where data LegalholdConfig = LegalholdConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform LegalholdConfig) + deriving (RenderableSymbol) via (RenderableTypeName LegalholdConfig) instance Default LegalholdConfig where def = LegalholdConfig -instance RenderableSymbol LegalholdConfig where - renderSymbol = "LegalholdConfig" - instance Default (LockableFeature LegalholdConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -613,13 +602,11 @@ instance ToSchema LegalholdConfig where data SSOConfig = SSOConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SSOConfig) + deriving (RenderableSymbol) via (RenderableTypeName SSOConfig) instance Default SSOConfig where def = SSOConfig -instance RenderableSymbol SSOConfig where - renderSymbol = "SSOConfig" - instance Default (LockableFeature SSOConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -639,13 +626,11 @@ instance ToSchema SSOConfig where data SearchVisibilityAvailableConfig = SearchVisibilityAvailableConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SearchVisibilityAvailableConfig) + deriving (RenderableSymbol) via (RenderableTypeName SearchVisibilityAvailableConfig) instance Default SearchVisibilityAvailableConfig where def = SearchVisibilityAvailableConfig -instance RenderableSymbol SearchVisibilityAvailableConfig where - renderSymbol = "SearchVisibilityAvailableConfig" - instance Default (LockableFeature SearchVisibilityAvailableConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -666,13 +651,11 @@ type instance DeprecatedFeatureName SearchVisibilityAvailableConfig = "search-vi data ValidateSAMLEmailsConfig = ValidateSAMLEmailsConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ValidateSAMLEmailsConfig) + deriving (RenderableSymbol) via (RenderableTypeName ValidateSAMLEmailsConfig) instance Default ValidateSAMLEmailsConfig where def = ValidateSAMLEmailsConfig -instance RenderableSymbol ValidateSAMLEmailsConfig where - renderSymbol = "ValidateSAMLEmailsConfig" - instance ToSchema ValidateSAMLEmailsConfig where schema = object "ValidateSAMLEmailsConfig" objectSchema @@ -693,13 +676,11 @@ type instance DeprecatedFeatureName ValidateSAMLEmailsConfig = "validate-saml-em data DigitalSignaturesConfig = DigitalSignaturesConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform DigitalSignaturesConfig) + deriving (RenderableSymbol) via (RenderableTypeName DigitalSignaturesConfig) instance Default DigitalSignaturesConfig where def = DigitalSignaturesConfig -instance RenderableSymbol DigitalSignaturesConfig where - renderSymbol = "DigitalSignaturesConfig" - instance Default (LockableFeature DigitalSignaturesConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -744,13 +725,11 @@ data ConferenceCallingConfig = ConferenceCallingConfig } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ConferenceCallingConfig) + deriving (RenderableSymbol) via (RenderableTypeName ConferenceCallingConfig) instance Default ConferenceCallingConfig where def = ConferenceCallingConfig {one2OneCalls = def} -instance RenderableSymbol ConferenceCallingConfig where - renderSymbol = "ConferenceCallingConfig" - instance Default (LockableFeature ConferenceCallingConfig) where def = defLockedFeature {status = FeatureStatusEnabled} @@ -774,13 +753,11 @@ instance ToSchema ConferenceCallingConfig where data SndFactorPasswordChallengeConfig = SndFactorPasswordChallengeConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SndFactorPasswordChallengeConfig) + deriving (RenderableSymbol) via (RenderableTypeName SndFactorPasswordChallengeConfig) instance Default SndFactorPasswordChallengeConfig where def = SndFactorPasswordChallengeConfig -instance RenderableSymbol SndFactorPasswordChallengeConfig where - renderSymbol = "SndFactorPasswordChallengeConfig" - instance ToSchema SndFactorPasswordChallengeConfig where schema = object "SndFactorPasswordChallengeConfig" objectSchema @@ -799,13 +776,11 @@ data SearchVisibilityInboundConfig = SearchVisibilityInboundConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SearchVisibilityInboundConfig) deriving (S.ToSchema) via Schema SearchVisibilityInboundConfig + deriving (RenderableSymbol) via (RenderableTypeName SearchVisibilityInboundConfig) instance Default SearchVisibilityInboundConfig where def = SearchVisibilityInboundConfig -instance RenderableSymbol SearchVisibilityInboundConfig where - renderSymbol = "SearchVisibilityInboundConfig" - instance Default (LockableFeature SearchVisibilityInboundConfig) where def = defUnlockedFeature {status = FeatureStatusDisabled} @@ -828,13 +803,11 @@ data ClassifiedDomainsConfig = ClassifiedDomainsConfig } deriving stock (Show, Eq, Generic) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema ClassifiedDomainsConfig) + deriving (RenderableSymbol) via (RenderableTypeName ClassifiedDomainsConfig) instance Default ClassifiedDomainsConfig where def = ClassifiedDomainsConfig [] -instance RenderableSymbol ClassifiedDomainsConfig where - renderSymbol = "ClassifiedDomainsConfig" - deriving via (GenericUniform ClassifiedDomainsConfig) instance Arbitrary ClassifiedDomainsConfig instance ToSchema ClassifiedDomainsConfig where @@ -862,13 +835,11 @@ data AppLockConfig = AppLockConfig deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema AppLockConfig) deriving (Arbitrary) via (GenericUniform AppLockConfig) + deriving (RenderableSymbol) via (RenderableTypeName AppLockConfig) instance Default AppLockConfig where def = AppLockConfig (EnforceAppLock False) 60 -instance RenderableSymbol AppLockConfig where - renderSymbol = "AppLockConfig" - instance ToSchema AppLockConfig where schema = object "AppLockConfig" $ @@ -899,13 +870,11 @@ instance ToSchema EnforceAppLock where data FileSharingConfig = FileSharingConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform FileSharingConfig) + deriving (RenderableSymbol) via (RenderableTypeName FileSharingConfig) instance Default FileSharingConfig where def = FileSharingConfig -instance RenderableSymbol FileSharingConfig where - renderSymbol = "FileSharingConfig" - instance Default (LockableFeature FileSharingConfig) where def = defUnlockedFeature @@ -926,13 +895,11 @@ newtype SelfDeletingMessagesConfig = SelfDeletingMessagesConfig deriving stock (Eq, Show, Generic) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema SelfDeletingMessagesConfig) deriving (Arbitrary) via (GenericUniform SelfDeletingMessagesConfig) + deriving (RenderableSymbol) via (RenderableTypeName SelfDeletingMessagesConfig) instance Default SelfDeletingMessagesConfig where def = SelfDeletingMessagesConfig 0 -instance RenderableSymbol SelfDeletingMessagesConfig where - renderSymbol = "SelfDeletingMessagesConfig" - instance ToSchema SelfDeletingMessagesConfig where schema = object "SelfDeletingMessagesConfig" $ @@ -959,6 +926,7 @@ data MLSConfig = MLSConfig } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform MLSConfig) + deriving (RenderableSymbol) via (RenderableTypeName MLSConfig) instance Default MLSConfig where def = @@ -969,9 +937,6 @@ instance Default MLSConfig where MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 [ProtocolProteusTag, ProtocolMLSTag] -instance RenderableSymbol MLSConfig where - renderSymbol = "MLSConfig" - instance ToSchema MLSConfig where schema = object "MLSConfig" $ @@ -996,13 +961,11 @@ instance IsFeatureConfig MLSConfig where data ExposeInvitationURLsToTeamAdminConfig = ExposeInvitationURLsToTeamAdminConfig deriving stock (Show, Eq, Generic) deriving (Arbitrary) via (GenericUniform ExposeInvitationURLsToTeamAdminConfig) + deriving (RenderableSymbol) via (RenderableTypeName ExposeInvitationURLsToTeamAdminConfig) instance Default ExposeInvitationURLsToTeamAdminConfig where def = ExposeInvitationURLsToTeamAdminConfig -instance RenderableSymbol ExposeInvitationURLsToTeamAdminConfig where - renderSymbol = "ExposeInvitationURLsToTeamAdminConfig" - instance Default (LockableFeature ExposeInvitationURLsToTeamAdminConfig) where def = defLockedFeature @@ -1022,13 +985,11 @@ instance ToSchema ExposeInvitationURLsToTeamAdminConfig where data OutlookCalIntegrationConfig = OutlookCalIntegrationConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform OutlookCalIntegrationConfig) + deriving (RenderableSymbol) via (RenderableTypeName OutlookCalIntegrationConfig) instance Default OutlookCalIntegrationConfig where def = OutlookCalIntegrationConfig -instance RenderableSymbol OutlookCalIntegrationConfig where - renderSymbol = "OutlookCalIntegrationConfig" - instance Default (LockableFeature OutlookCalIntegrationConfig) where def = defLockedFeature @@ -1050,13 +1011,11 @@ data MlsE2EIdConfig = MlsE2EIdConfig useProxyOnMobile :: Bool } deriving stock (Eq, Show, Generic) + deriving (RenderableSymbol) via (RenderableTypeName MlsE2EIdConfig) instance Default MlsE2EIdConfig where def = MlsE2EIdConfig (fromIntegral @Int (60 * 60 * 24)) Nothing Nothing False -instance RenderableSymbol MlsE2EIdConfig where - renderSymbol = "MlsE2EIdConfig" - instance Arbitrary MlsE2EIdConfig where arbitrary = MlsE2EIdConfig @@ -1112,13 +1071,11 @@ data MlsMigrationConfig = MlsMigrationConfig finaliseRegardlessAfter :: Maybe UTCTime } deriving stock (Eq, Show, Generic) + deriving (RenderableSymbol) via (RenderableTypeName MlsMigrationConfig) instance Default MlsMigrationConfig where def = MlsMigrationConfig Nothing Nothing -instance RenderableSymbol MlsMigrationConfig where - renderSymbol = "MlsMigrationConfig" - instance Arbitrary MlsMigrationConfig where arbitrary = do startTime <- fmap fromUTCTimeMillis <$> arbitrary @@ -1151,13 +1108,11 @@ data EnforceFileDownloadLocationConfig = EnforceFileDownloadLocationConfig { enforcedDownloadLocation :: Maybe Text } deriving stock (Eq, Show, Generic) + deriving (RenderableSymbol) via (RenderableTypeName EnforceFileDownloadLocationConfig) instance Default EnforceFileDownloadLocationConfig where def = EnforceFileDownloadLocationConfig Nothing -instance RenderableSymbol EnforceFileDownloadLocationConfig where - renderSymbol = "EnforceFileDownloadLocationConfig" - instance Arbitrary EnforceFileDownloadLocationConfig where arbitrary = EnforceFileDownloadLocationConfig . fmap (T.pack . getPrintableString) <$> arbitrary @@ -1186,13 +1141,11 @@ instance IsFeatureConfig EnforceFileDownloadLocationConfig where data LimitedEventFanoutConfig = LimitedEventFanoutConfig deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform LimitedEventFanoutConfig) + deriving (RenderableSymbol) via (RenderableTypeName LimitedEventFanoutConfig) instance Default LimitedEventFanoutConfig where def = LimitedEventFanoutConfig -instance RenderableSymbol LimitedEventFanoutConfig where - renderSymbol = "LimitedEventFanoutConfig" - instance Default (LockableFeature LimitedEventFanoutConfig) where def = defUnlockedFeature @@ -1293,13 +1246,13 @@ type Features = type AllFeatures f = NP f Features -- | 'AllFeatures' specialised to the 'LockableFeature' functor -type AllFeatureConfigs = AllFeatures LockableFeature +type AllTeamFeatures = AllFeatures LockableFeature class (Default (LockableFeature cfg)) => LockableFeatureDefault cfg instance (Default (LockableFeature cfg)) => LockableFeatureDefault cfg -instance Default AllFeatureConfigs where +instance Default AllTeamFeatures where def = hcpure (Proxy @LockableFeatureDefault) def -- | object schema for nary products @@ -1312,14 +1265,14 @@ instance HObjectSchema c '[] where instance (HObjectSchema c xs, c x) => HObjectSchema c ((x :: Type) : xs) where hobjectSchema f = (:*) <$> hd .= f <*> tl .= hobjectSchema @c @xs f --- | constraint synonym for 'ToSchema' 'AllFeatureConfigs' +-- | constraint synonym for 'ToSchema' 'AllTeamFeatures' class (IsFeatureConfig cfg, ToSchema cfg) => FeatureFieldConstraints cfg instance (IsFeatureConfig cfg, ToSchema cfg) => FeatureFieldConstraints cfg -instance ToSchema AllFeatureConfigs where +instance ToSchema AllTeamFeatures where schema = - object "AllFeatureConfigs" $ hobjectSchema @FeatureFieldConstraints featureField + object "AllTeamFeatures" $ hobjectSchema @FeatureFieldConstraints featureField where featureField :: forall cfg. (FeatureFieldConstraints cfg) => ObjectSchema SwaggerDoc (LockableFeature cfg) featureField = field (T.pack (symbolVal (Proxy @(FeatureSymbol cfg)))) schema @@ -1328,7 +1281,7 @@ class (Arbitrary cfg, IsFeatureConfig cfg) => ArbitraryFeatureConfig cfg instance (Arbitrary cfg, IsFeatureConfig cfg) => ArbitraryFeatureConfig cfg -instance Arbitrary AllFeatureConfigs where +instance Arbitrary AllTeamFeatures where arbitrary = hsequence' $ hcpure (Proxy @ArbitraryFeatureConfig) (Comp arbitrary) -- | FUTUREWORK: 'NpProject' and 'NpUpdate' can be useful for more than @@ -1365,10 +1318,8 @@ instance (TypeError ('ShowType x :<>: 'Text " not found")) => NpUpdate x '[] whe npUpdate :: forall x f xs. (NpUpdate x xs) => f x -> NP f xs -> NP f xs npUpdate = npUpdate' (Proxy @x) -makeLenses ''ImplicitLockStatus - -deriving via (Schema AllFeatureConfigs) instance (FromJSON AllFeatureConfigs) +deriving via (Schema AllTeamFeatures) instance (FromJSON AllTeamFeatures) -deriving via (Schema AllFeatureConfigs) instance (ToJSON AllFeatureConfigs) +deriving via (Schema AllTeamFeatures) instance (ToJSON AllTeamFeatures) -deriving via (Schema AllFeatureConfigs) instance (S.ToSchema AllFeatureConfigs) +deriving via (Schema AllTeamFeatures) instance (S.ToSchema AllTeamFeatures) 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 a4d3b841fa5..f425939ba1d 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 @@ -218,7 +218,7 @@ tests = testRoundTrip @(Team.Feature.LockableFeaturePatch Team.Feature.LegalholdConfig), testRoundTrip @(Team.Feature.LockableFeaturePatch Team.Feature.SelfDeletingMessagesConfig), testRoundTrip @(Team.Feature.Feature Team.Feature.LegalholdConfig), - testRoundTrip @Team.Feature.AllFeatureConfigs, + testRoundTrip @Team.Feature.AllTeamFeatures, testRoundTrip @Team.Feature.FeatureStatus, testRoundTrip @Team.Feature.LockStatus, testRoundTrip @Team.Invitation.InvitationRequest, diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index 3ef3d01a3bb..63075543d4a 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -105,9 +105,9 @@ data GalleyAPIAccess m a where TeamId -> UserId -> GalleyAPIAccess m Bool - GetAllFeatureConfigsForUser :: + GetAllTeamFeaturesForUser :: Maybe UserId -> - GalleyAPIAccess m AllFeatureConfigs + GalleyAPIAccess m AllTeamFeatures GetVerificationCodeEnabled :: TeamId -> GalleyAPIAccess m Bool diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index 7f451bad632..e226d09bcdd 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -82,7 +82,7 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = GetTeamSearchVisibility id' -> getTeamSearchVisibility id' ChangeTeamStatus id' ts m_al -> changeTeamStatus id' ts m_al MemberIsTeamOwner id' id'' -> memberIsTeamOwner id' id'' - GetAllFeatureConfigsForUser m_id' -> getAllFeatureConfigsForUser m_id' + GetAllTeamFeaturesForUser m_id' -> getAllTeamFeaturesForUser m_id' GetVerificationCodeEnabled id' -> getVerificationCodeEnabled id' GetExposeInvitationURLsToTeamAdmin id' -> getTeamExposeInvitationURLsToTeamAdmin id' IsMLSOne2OneEstablished lusr qother -> checkMLSOne2OneEstablished lusr qother @@ -456,11 +456,11 @@ getVerificationCodeEnabled tid = do decodeBodyOrThrow :: forall a r. (Typeable a, FromJSON a, Member (Error ParseException) r) => Text -> Response (Maybe BL.ByteString) -> Sem r a decodeBodyOrThrow ctx r = either (throw . ParseException ctx) pure (responseJsonEither r) -getAllFeatureConfigsForUser :: +getAllTeamFeaturesForUser :: (Member Rpc r, Member (Input Endpoint) r) => Maybe UserId -> - Sem r AllFeatureConfigs -getAllFeatureConfigsForUser mbUserId = + Sem r AllTeamFeatures +getAllTeamFeaturesForUser mbUserId = responseJsonUnsafe <$> galleyRequest ( method GET diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 7b0edcee302..2f0f37a87df 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -472,9 +472,9 @@ checkHandleImpl uhandle = do hasE2EId :: (Member GalleyAPIAccess r) => StoredUser -> Sem r Bool hasE2EId user = - -- FUTUREWORK(mangoiv): we should use a function 'getSingleFeatureConfigForUser' + -- FUTUREWORK(mangoiv): we should use a function 'getSingleFeatureForUser' (.status) . npProject @MlsE2EIdConfig - <$> getAllFeatureConfigsForUser (Just user.id) <&> \case + <$> getAllTeamFeaturesForUser (Just user.id) <&> \case FeatureStatusEnabled -> True FeatureStatusDisabled -> False diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index 84f870e01b2..bd712dfe489 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -308,7 +308,7 @@ interpretNoFederationStack :: (Members AllErrors r) => MiniBackend -> Maybe TeamMember -> - AllFeatureConfigs -> + AllTeamFeatures -> UserSubsystemConfig -> Sem (MiniBackendEffects `Append` r) a -> Sem r a @@ -319,7 +319,7 @@ interpretNoFederationStackState :: (Members AllErrors r) => MiniBackend -> Maybe TeamMember -> - AllFeatureConfigs -> + AllTeamFeatures -> UserSubsystemConfig -> Sem (MiniBackendEffects `Append` r) a -> Sem r (MiniBackend, a) @@ -330,7 +330,7 @@ interpretMaybeFederationStackState :: InterpreterFor (FederationAPIAccess MiniFederationMonad) (Logger (Log.Msg -> Log.Msg) : Concurrency 'Unsafe : r) -> MiniBackend -> Maybe TeamMember -> - AllFeatureConfigs -> + AllTeamFeatures -> UserSubsystemConfig -> Sem (MiniBackendEffects `Append` r) a -> Sem r (MiniBackend, a) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs index 1e8a81e9f51..1cfe41aeaf6 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs @@ -10,10 +10,10 @@ import Wire.GalleyAPIAccess miniGalleyAPIAccess :: -- | what to return when calling GetTeamMember Maybe TeamMember -> - -- | what to return when calling GetAllFeatureConfigsForUser - AllFeatureConfigs -> + -- | what to return when calling GetAllTeamFeaturesForUser + AllTeamFeatures -> InterpreterFor GalleyAPIAccess r miniGalleyAPIAccess member configs = interpret $ \case GetTeamMember _ _ -> pure member - GetAllFeatureConfigsForUser _ -> pure configs + GetAllTeamFeaturesForUser _ -> pure configs _ -> error "uninterpreted effect: GalleyAPIAccess" diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 7174d2407e8..4ce93388c87 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -334,7 +334,6 @@ library , wire-api , wire-api-federation , wire-subsystems - , yaml >=0.8.22 , zauth >=0.10.3 executable brig @@ -526,7 +525,6 @@ test-suite brig-tests Test.Brig.Effects.Delay Test.Brig.InternalNotification Test.Brig.MLS - Test.Brig.Roundtrip Test.Brig.User.Search.Index.Types hs-source-dirs: test/unit diff --git a/services/brig/default.nix b/services/brig/default.nix index 79e87adb1b0..b320133901a 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -284,7 +284,6 @@ mkDerivation { wire-api wire-api-federation wire-subsystems - yaml zauth ]; executableHaskellDepends = [ diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index ab43f085d49..11060b0b395 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -59,7 +59,7 @@ import Brig.User.API.Search qualified as Search import Brig.User.EJPD qualified import Brig.User.Search.Index qualified as Index import Control.Error hiding (bool) -import Control.Lens (view) +import Control.Lens (preview, to, view, _Just) import Data.ByteString.Conversion (toByteString) import Data.Code qualified as Code import Data.CommaSeparatedList @@ -92,7 +92,7 @@ import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Internal.Brig qualified as BrigIRoutes import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Named -import Wire.API.Team.Feature qualified as ApiFt +import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.Activation import Wire.API.User.Client @@ -349,17 +349,17 @@ updateFederationRemote dom fedcfg = do "keeping track of remote domains in the brig config file is deprecated, but as long as we \ \do that, removing or updating items listed in the config file is not allowed." --- | Responds with 'Nothing' if field is NULL in existing user or user does not exist. -getAccountConferenceCallingConfig :: UserId -> (Handler r) (ApiFt.Feature ApiFt.ConferenceCallingConfig) -getAccountConferenceCallingConfig uid = - lift (wrapClient $ Data.lookupFeatureConferenceCalling uid) - >>= maybe (ApiFt.forgetLock <$> view (settings . getAfcConferenceCallingDefNull)) pure +getAccountConferenceCallingConfig :: UserId -> Handler r (Feature ConferenceCallingConfig) +getAccountConferenceCallingConfig uid = do + mStatus <- lift $ wrapClient $ Data.lookupFeatureConferenceCalling uid + mDefStatus <- preview (settings . featureFlags . _Just . to conferenceCalling . to forNull) + pure $ def {status = mStatus <|> mDefStatus ?: (def :: LockableFeature ConferenceCallingConfig).status} -putAccountConferenceCallingConfig :: UserId -> ApiFt.Feature ApiFt.ConferenceCallingConfig -> (Handler r) NoContent -putAccountConferenceCallingConfig uid status = - lift $ wrapClient $ Data.updateFeatureConferenceCalling uid (Just status) $> NoContent +putAccountConferenceCallingConfig :: UserId -> Feature ConferenceCallingConfig -> Handler r NoContent +putAccountConferenceCallingConfig uid feat = do + lift $ wrapClient $ Data.updateFeatureConferenceCalling uid (Just feat.status) $> NoContent -deleteAccountConferenceCallingConfig :: UserId -> (Handler r) NoContent +deleteAccountConferenceCallingConfig :: UserId -> Handler r NoContent deleteAccountConferenceCallingConfig uid = lift $ wrapClient $ Data.updateFeatureConferenceCalling uid Nothing $> NoContent diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index b56a8f29834..d7a7c4159f6 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -96,7 +96,7 @@ import Brig.User.Search.Index (reindex) import Brig.User.Search.TeamSize qualified as TeamSize import Cassandra hiding (Set) import Control.Error -import Control.Lens (view, (^.)) +import Control.Lens (preview, to, view, (^.), _Just) import Control.Monad.Catch import Data.ByteString.Conversion import Data.Code @@ -127,7 +127,6 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Password import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Team hiding (newTeam) -import Wire.API.Team.Feature import Wire.API.Team.Invitation import Wire.API.Team.Invitation qualified as Team import Wire.API.Team.Member (legalHoldStatus) @@ -481,8 +480,8 @@ createUser new = do initAccountFeatureConfig :: UserId -> (AppT r) () initAccountFeatureConfig uid = do - mbCciDefNew <- view (settings . getAfcConferenceCallingDefNewMaybe) - forM_ (forgetLock <$> mbCciDefNew) $ wrapClient . Data.updateFeatureConferenceCalling uid . Just + mStatus <- preview (settings . featureFlags . _Just . to conferenceCalling . to forNew . _Just) + wrapClient $ traverse_ (Data.updateFeatureConferenceCalling uid . Just) mStatus -- | 'createUser' is becoming hard to maintain, and instead of adding more case distinctions -- all over the place there, we add a new function that handles just the one new flow where diff --git a/services/brig/src/Brig/Calling/API.hs b/services/brig/src/Brig/Calling/API.hs index 9b58b382ff9..3618f038093 100644 --- a/services/brig/src/Brig/Calling/API.hs +++ b/services/brig/src/Brig/Calling/API.hs @@ -62,7 +62,7 @@ import System.Logger.Class qualified as Log import Wire.API.Call.Config qualified as Public import Wire.API.Team.Feature import Wire.Error -import Wire.GalleyAPIAccess (GalleyAPIAccess, getAllFeatureConfigsForUser) +import Wire.GalleyAPIAccess (GalleyAPIAccess, getAllTeamFeaturesForUser) import Wire.Network.DNS.SRV (srvTarget) -- | ('UserId', 'ConnId' are required as args here to make sure this is an authenticated end-point.) @@ -83,7 +83,7 @@ getCallsConfigV2 uid _ limit = do sftFederation <- view enableSFTFederation discoveredServers <- turnServersV2 (env ^. turnServers) shared <- do - ccStatus <- lift $ liftSem $ ((.status) . npProject @ConferenceCallingConfig <$> getAllFeatureConfigsForUser (Just uid)) + ccStatus <- lift $ liftSem $ ((.status) . npProject @ConferenceCallingConfig <$> getAllTeamFeaturesForUser (Just uid)) pure $ case ccStatus of FeatureStatusEnabled -> True FeatureStatusDisabled -> False @@ -118,7 +118,7 @@ getCallsConfig uid _ = do env <- view turnEnv discoveredServers <- turnServersV1 (env ^. turnServers) shared <- do - ccStatus <- lift $ liftSem $ ((.status) . npProject @ConferenceCallingConfig <$> getAllFeatureConfigsForUser (Just uid)) + ccStatus <- lift $ liftSem $ ((.status) . npProject @ConferenceCallingConfig <$> getAllTeamFeaturesForUser (Just uid)) pure $ case ccStatus of FeatureStatusEnabled -> True FeatureStatusDisabled -> False diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index afe68155e3c..4bc2d82f506 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -71,7 +71,6 @@ import Cassandra hiding (Set) import Control.Error import Control.Lens hiding (from) import Data.Conduit (ConduitM) -import Data.Default import Data.Domain import Data.Handle (Handle) import Data.Id @@ -85,7 +84,7 @@ import Imports import Polysemy import Wire.API.Password import Wire.API.Provider.Service -import Wire.API.Team.Feature qualified as ApiFt +import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.RichInfo import Wire.PasswordStore @@ -301,13 +300,11 @@ updateManagedBy u h = retry x5 $ write userManagedByUpdate (params LocalQuorum ( updateRichInfo :: (MonadClient m) => UserId -> RichInfoAssocList -> m () updateRichInfo u ri = retry x5 $ write userRichInfoUpdate (params LocalQuorum (ri, u)) -updateFeatureConferenceCalling :: (MonadClient m) => UserId -> Maybe (ApiFt.Feature ApiFt.ConferenceCallingConfig) -> m (Maybe (ApiFt.Feature ApiFt.ConferenceCallingConfig)) -updateFeatureConferenceCalling uid mbStatus = do - let flag = (.status) <$> mbStatus - retry x5 $ write update (params LocalQuorum (flag, uid)) - pure mbStatus +updateFeatureConferenceCalling :: (MonadClient m) => UserId -> Maybe FeatureStatus -> m () +updateFeatureConferenceCalling uid mStatus = + retry x5 $ write update (params LocalQuorum (mStatus, uid)) where - update :: PrepQuery W (Maybe ApiFt.FeatureStatus, UserId) () + update :: PrepQuery W (Maybe FeatureStatus, UserId) () update = fromString "update user set feature_conference_calling = ? where id = ?" deleteEmail :: (MonadClient m) => UserId -> m () @@ -438,15 +435,12 @@ lookupServiceUsersForTeam pid sid tid = "SELECT user, conv FROM service_team \ \WHERE provider = ? AND service = ? AND team = ?" -lookupFeatureConferenceCalling :: (MonadClient m) => UserId -> m (Maybe (ApiFt.Feature ApiFt.ConferenceCallingConfig)) +lookupFeatureConferenceCalling :: (MonadClient m) => UserId -> m (Maybe FeatureStatus) lookupFeatureConferenceCalling uid = do let q = query1 select (params LocalQuorum (Identity uid)) - mStatusValue <- (>>= runIdentity) <$> retry x1 q - case mStatusValue of - Nothing -> pure Nothing - Just status -> pure $ Just $ def {ApiFt.status = status} + (>>= runIdentity) <$> retry x1 q where - select :: PrepQuery R (Identity UserId) (Identity (Maybe ApiFt.FeatureStatus)) + select :: PrepQuery R (Identity UserId) (Identity (Maybe FeatureStatus)) select = fromString "select feature_conference_calling from user where id = ?" ------------------------------------------------------------------------------- diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 45c72d9b0f5..42f57313d77 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -29,10 +29,8 @@ import Brig.User.Auth.Cookie.Limit import Brig.ZAuth qualified as ZAuth import Control.Applicative import Control.Lens qualified as Lens -import Data.Aeson (defaultOptions, fieldLabelModifier, genericParseJSON) -import Data.Aeson qualified as A -import Data.Aeson qualified as Aeson -import Data.Aeson.Types (typeMismatch) +import Data.Aeson +import Data.Aeson.Types qualified as A import Data.Char qualified as Char import Data.Code qualified as Code import Data.Default @@ -47,10 +45,7 @@ import Data.Scientific (toBoundedInteger) import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Data.Time.Clock (DiffTime, NominalDiffTime, secondsToDiffTime) -import Data.Yaml (FromJSON (..), ToJSON (..), (.:), (.:?)) -import Data.Yaml qualified as Y import Database.Bloodhound.Types qualified as ES -import Galley.Types.Teams (unImplicitLockStatus) import Imports import Network.AMQP.Extended import Network.DNS qualified as DNS @@ -59,9 +54,8 @@ import Util.Options import Wire.API.Allowlists (AllowlistEmailDomains (..)) import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Version -import Wire.API.Team.Feature qualified as Public +import Wire.API.Team.Feature import Wire.API.User -import Wire.Arbitrary (Arbitrary, arbitrary) import Wire.EmailSending.SMTP (SMTPConnType (..)) newtype Timeout = Timeout @@ -172,8 +166,8 @@ data InternalEventsOpts = InternalEventsOpts deriving (Show) instance FromJSON InternalEventsOpts where - parseJSON = Y.withObject "InternalEventsOpts" $ \o -> - InternalEventsOpts <$> parseJSON (Y.Object o) + parseJSON = withObject "InternalEventsOpts" $ \o -> + InternalEventsOpts <$> parseJSON (Object o) data EmailSMSGeneralOpts = EmailSMSGeneralOpts { -- | Email, SMS, ... template directory @@ -329,12 +323,12 @@ data TurnOpts = TurnOpts deriving (Show) instance FromJSON TurnOpts where - parseJSON = A.withObject "TurnOpts" $ \o -> do + parseJSON = withObject "TurnOpts" $ \o -> do sourceName <- o .: "serversSource" source <- case sourceName of - "files" -> TurnSourceFiles <$> A.parseJSON (A.Object o) - "dns" -> TurnSourceDNS <$> A.parseJSON (A.Object o) + "files" -> TurnSourceFiles <$> parseJSON (Object o) + "dns" -> TurnSourceDNS <$> parseJSON (Object o) _ -> fail $ "TurnOpts: Invalid sourceType, expected one of [files, dns] but got: " <> Text.unpack sourceName TurnOpts source <$> o .: "secret" @@ -353,7 +347,7 @@ data TurnServersFiles = TurnServersFiles deriving (Show) instance FromJSON TurnServersFiles where - parseJSON = A.withObject "TurnServersFiles" $ \o -> + parseJSON = withObject "TurnServersFiles" $ \o -> TurnServersFiles <$> o .: "servers" <*> o .: "serversV2" @@ -365,7 +359,7 @@ data TurnDnsOpts = TurnDnsOpts deriving (Show) instance FromJSON TurnDnsOpts where - parseJSON = A.withObject "TurnDnsOpts" $ \o -> + parseJSON = withObject "TurnDnsOpts" $ \o -> TurnDnsOpts <$> (asciiOnly =<< o .: "baseDomain") <*> o .:? "discoveryIntervalSeconds" @@ -553,7 +547,7 @@ data Settings = Settings -- docs/reference/user/registration.md {#RefRestrictRegistration}. setRestrictUserCreation :: !(Maybe Bool), -- | The analog to `Galley.Options.setFeatureFlags`. See 'AccountFeatureConfigs'. - setFeatureFlags :: !(Maybe AccountFeatureConfigs), + setFeatureFlags :: !(Maybe UserFeatureFlags), -- | Customer extensions. Read 'CustomerExtensions' docs carefully! setCustomerExtensions :: !(Maybe CustomerExtensions), -- | When set; instead of using SRV lookups to discover SFTs the calls @@ -611,11 +605,11 @@ newtype ImplicitNoFederationRestriction = ImplicitNoFederationRestriction instance FromJSON ImplicitNoFederationRestriction where parseJSON = - Aeson.withObject + withObject "ImplicitNoFederationRestriction" ( \obj -> do - domain <- obj Aeson..: "domain" - searchPolicy <- obj Aeson..: "search_policy" + domain <- obj .: "domain" + searchPolicy <- obj .: "search_policy" pure . ImplicitNoFederationRestriction $ FederationDomainConfig domain searchPolicy FederationRestrictionAllowAll ) @@ -692,72 +686,39 @@ defaultOAuthMaxActiveRefreshTokens = 10 setOAuthMaxActiveRefreshTokens :: Settings -> Word32 setOAuthMaxActiveRefreshTokens = fromMaybe defaultOAuthMaxActiveRefreshTokens . setOAuthMaxActiveRefreshTokensInternal --- | The analog to `GT.FeatureFlags`. This type tracks only the things that we need to --- express our current cloud business logic. --- --- FUTUREWORK: it would be nice to have a system of feature configs that allows to coherently --- express arbitrary logic across personal and team accounts, teams, and instances; including --- default values for new records, default for records that have a NULL value (eg., because --- they are grandfathered), and feature-specific extra data (eg., TLL for self-deleting --- messages). For now, we have something quick & simple. -data AccountFeatureConfigs = AccountFeatureConfigs - { afcConferenceCallingDefNew :: !(Public.ImplicitLockStatus Public.ConferenceCallingConfig), - afcConferenceCallingDefNull :: !(Public.ImplicitLockStatus Public.ConferenceCallingConfig) +-- | The analog to `FeatureFlags`. At the moment, only status flags for +-- conferenceCalling are stored. +data UserFeatureFlags = UserFeatureFlags + { conferenceCalling :: UserFeature ConferenceCallingConfig } - deriving (Show, Eq, Generic) + deriving (Eq, Ord, Show) -instance Arbitrary AccountFeatureConfigs where - arbitrary = AccountFeatureConfigs <$> fmap locked arbitrary <*> fmap locked arbitrary - where - locked :: Public.ImplicitLockStatus a -> Public.ImplicitLockStatus a - locked impl = - Public.ImplicitLockStatus $ - (Public._unImplicitLockStatus impl) - { Public.lockStatus = Public.LockStatusLocked - } - -instance FromJSON AccountFeatureConfigs where - parseJSON = - Aeson.withObject - "AccountFeatureConfigs" - ( \obj -> do - confCallInit <- obj Aeson..: "conferenceCalling" - Aeson.withObject - "conferenceCalling" - ( \obj' -> do - AccountFeatureConfigs - <$> obj' Aeson..: "defaultForNew" - <*> obj' Aeson..: "defaultForNull" - ) - confCallInit - ) +instance FromJSON UserFeatureFlags where + parseJSON = withObject "UserFeatureFlags" $ \obj -> do + UserFeatureFlags + <$> obj .:? "conferenceCalling" .!= def -instance ToJSON AccountFeatureConfigs where - toJSON - AccountFeatureConfigs - { afcConferenceCallingDefNew, - afcConferenceCallingDefNull - } = - Aeson.object - [ "conferenceCalling" - Aeson..= Aeson.object - [ "defaultForNew" Aeson..= afcConferenceCallingDefNew, - "defaultForNull" Aeson..= afcConferenceCallingDefNull - ] - ] +data family UserFeature cfg + +data instance UserFeature ConferenceCallingConfig = ConferenceCallingUserStatus + { -- | This will be set as the status of the feature for newly created users. + forNew :: Maybe FeatureStatus, + -- | How an unset status for this feature should be interpreted. + forNull :: FeatureStatus + } + deriving (Eq, Ord, Show) -getAfcConferenceCallingDefNewMaybe :: Lens.Getter Settings (Maybe (Public.LockableFeature Public.ConferenceCallingConfig)) -getAfcConferenceCallingDefNewMaybe = Lens.to (Lens.^? (Lens.to setFeatureFlags . Lens._Just . Lens.to afcConferenceCallingDefNew . unImplicitLockStatus)) +instance Default (UserFeature ConferenceCallingConfig) where + def = ConferenceCallingUserStatus Nothing FeatureStatusEnabled -getAfcConferenceCallingDefNull :: Lens.Getter Settings (Public.LockableFeature Public.ConferenceCallingConfig) -getAfcConferenceCallingDefNull = Lens.to (Public._unImplicitLockStatus . afcConferenceCallingDefNull . fromMaybe defAccountFeatureConfigs . setFeatureFlags) +instance FromJSON (UserFeature ConferenceCallingConfig) where + parseJSON = withObject "UserFeatureConferenceCalling" $ \obj -> do + ConferenceCallingUserStatus + <$> A.explicitParseFieldMaybe parseUserFeatureStatus obj "defaultForNew" + <*> A.explicitParseFieldMaybe parseUserFeatureStatus obj "defaultForNull" .!= forNull def -defAccountFeatureConfigs :: AccountFeatureConfigs -defAccountFeatureConfigs = - AccountFeatureConfigs - { afcConferenceCallingDefNew = Public.ImplicitLockStatus def, - afcConferenceCallingDefNull = Public.ImplicitLockStatus def - } +parseUserFeatureStatus :: A.Value -> A.Parser FeatureStatus +parseUserFeatureStatus = withObject "UserFeatureStatus" $ \obj -> obj .: "status" -- | Customer extensions naturally are covered by the AGPL like everything else, but use them -- at your own risk! If you use the default server config and do not set @@ -814,7 +775,7 @@ data SFTOptions = SFTOptions deriving (Show, Generic) instance FromJSON SFTOptions where - parseJSON = Y.withObject "SFTOptions" $ \o -> + parseJSON = withObject "SFTOptions" $ \o -> SFTOptions <$> (asciiOnly =<< o .: "sftBaseDomain") <*> (mapM asciiOnly =<< o .:? "sftSRVServiceName") @@ -829,12 +790,12 @@ data SFTTokenOptions = SFTTokenOptions deriving (Show, Generic) instance FromJSON SFTTokenOptions where - parseJSON = Y.withObject "SFTTokenOptions" $ \o -> + parseJSON = withObject "SFTTokenOptions" $ \o -> SFTTokenOptions <$> (o .: "ttl") <*> (o .: "secret") -asciiOnly :: Text -> Y.Parser ByteString +asciiOnly :: Text -> A.Parser ByteString asciiOnly t = if Text.all Char.isAscii t then pure $ Text.encodeUtf8 t @@ -865,14 +826,14 @@ defSftListLength :: Range 1 100 Int defSftListLength = unsafeRange 5 instance FromJSON Timeout where - parseJSON (Y.Number n) = + parseJSON (Number n) = let defaultV = 3600 bounded = toBoundedInteger n :: Maybe Int64 in pure $ Timeout $ fromIntegral @Int $ maybe defaultV fromIntegral bounded - parseJSON v = typeMismatch "activationTimeout" v + parseJSON v = A.typeMismatch "activationTimeout" v instance FromJSON Settings where parseJSON = genericParseJSON customOptions @@ -920,6 +881,7 @@ Lens.makeLensesFor ("setFederationDomainConfigs", "federationDomainConfigs"), ("setFederationStrategy", "federationStrategy"), ("setRestrictUserCreation", "restrictUserCreation"), + ("setFeatureFlags", "featureFlags"), ("setEnableMLS", "enableMLS"), ("setOAuthEnabledInternal", "oauthEnabledInternal"), ("setOAuthAuthorizationCodeExpirationTimeSecsInternal", "oauthAuthorizationCodeExpirationTimeSecsInternal"), diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index f55928f4fb1..aaaad388b6c 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -804,7 +804,7 @@ guardSecondFactorDisabled :: Maybe UserId -> ExceptT HttpError (AppT r) () guardSecondFactorDisabled mbUserId = do - feat <- lift $ liftSem $ GalleyAPIAccess.getAllFeatureConfigsForUser mbUserId + feat <- lift $ liftSem $ GalleyAPIAccess.getAllTeamFeaturesForUser mbUserId let enabled = (Feature.npProject @Feature.SndFactorPasswordChallengeConfig feat).status == Feature.FeatureStatusEnabled diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 72ff959ab9f..a52905354b6 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -285,7 +285,7 @@ invitationUrlGalleyMock featureStatus tid inviter (ReceivedRequest mth pth body_ encode (mkTeamMember inviter fullPermissions Nothing UserLegalHoldDisabled) | mth == "GET" && pth == ["i", "feature-configs"] = - pure $ Wai.responseLBS HTTP.status200 mempty (encode (def @AllFeatureConfigs)) + pure $ Wai.responseLBS HTTP.status200 mempty (encode (def @AllTeamFeatures)) | otherwise = let errBody = encode . object $ diff --git a/services/brig/test/unit/Run.hs b/services/brig/test/unit/Run.hs index 64092fef3b5..6d658acb536 100644 --- a/services/brig/test/unit/Run.hs +++ b/services/brig/test/unit/Run.hs @@ -25,7 +25,6 @@ import Test.Brig.Calling qualified import Test.Brig.Calling.Internal qualified import Test.Brig.InternalNotification qualified import Test.Brig.MLS qualified -import Test.Brig.Roundtrip qualified import Test.Brig.User.Search.Index.Types qualified import Test.Tasty @@ -37,7 +36,6 @@ main = [ Test.Brig.User.Search.Index.Types.tests, Test.Brig.Calling.tests, Test.Brig.Calling.Internal.tests, - Test.Brig.Roundtrip.tests, Test.Brig.MLS.tests, Test.Brig.InternalNotification.tests ] diff --git a/services/brig/test/unit/Test/Brig/Roundtrip.hs b/services/brig/test/unit/Test/Brig/Roundtrip.hs deleted file mode 100644 index d878178bec8..00000000000 --- a/services/brig/test/unit/Test/Brig/Roundtrip.hs +++ /dev/null @@ -1,43 +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 Test.Brig.Roundtrip (tests) where - -import Brig.Options qualified as Options -import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) -import Data.Aeson.Types (parseEither) -import Imports -import Test.Tasty qualified as T -import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===)) -import Type.Reflection (typeRep) - -tests :: T.TestTree -tests = - T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "JSON roundtrip tests" $ - [ testRoundTrip @Options.AccountFeatureConfigs - ] - -testRoundTrip :: - forall a. - (Arbitrary a, Typeable a, ToJSON a, FromJSON a, Eq a, Show a) => - T.TestTree -testRoundTrip = testProperty msg trip - where - msg = show (typeRep @a) - trip (v :: a) = - counterexample (show $ toJSON v) $ - Right v === (parseEither parseJSON . toJSON) v diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 5bd145d2eb3..0344978de3b 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -137,7 +137,7 @@ library Galley.Cassandra.ConversationList Galley.Cassandra.CustomBackend Galley.Cassandra.FeatureTH - Galley.Cassandra.GetAllTeamFeatureConfigs + Galley.Cassandra.GetAllTeamFeatures Galley.Cassandra.Instances Galley.Cassandra.LegalHold Galley.Cassandra.MakeFeature diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 2d7002678b6..62f1f5120f3 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -243,11 +243,8 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con HasConversationActionEffects 'ConversationUpdateProtocolTag r = ( Member ConversationStore r, Member (ErrorS 'ConvInvalidProtocolTransition) r, - Member (ErrorS OperationDenied) r, Member (ErrorS 'MLSMigrationCriteriaNotSatisfied) r, Member (Error NoChanges) r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS 'TeamNotFound) r, Member BrigAccess r, Member ExternalAccess r, Member FederatorAccess r, @@ -260,7 +257,6 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, - Member TeamStore r, Member TinyLog r ) @@ -493,7 +489,7 @@ performAction tag origUser lconv action = do E.updateToMixedProtocol lcnv (convType (tUnqualified lconv)) pure (mempty, action) (ProtocolMixedTag, ProtocolMLSTag, Just tid) -> do - mig <- getFeatureStatus @MlsMigrationConfig DontDoAuth tid + mig <- getFeatureForTeam @MlsMigrationConfig tid now <- input mlsConv <- mkMLSConversation conv >>= noteS @'ConvInvalidProtocolTransition ok <- checkMigrationCriteria now mlsConv mig diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index ebb62143d63..12a6ff05ac5 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -239,9 +239,15 @@ featureAPI1Full :: (_) => API (IFeatureAPI1Full cfg) r featureAPI1Full = - mkNamedAPI @'("iget", cfg) (getFeatureStatus DontDoAuth) - <@> mkNamedAPI @'("iput", cfg) setFeatureStatusInternal - <@> mkNamedAPI @'("ipatch", cfg) patchFeatureStatusInternal + mkNamedAPI @'("iget", cfg) getFeatureInternal + <@> mkNamedAPI @'("iput", cfg) setFeatureInternal + <@> mkNamedAPI @'("ipatch", cfg) patchFeatureInternal + +featureAPI1Get :: + forall cfg r. + (_) => + API (IFeatureStatusGet cfg) r +featureAPI1Get = mkNamedAPI @'("iget", cfg) getFeatureInternal allFeaturesAPI :: API (IAllFeaturesAPI Features) GalleyEffects allFeaturesAPI = @@ -253,7 +259,7 @@ allFeaturesAPI = <@> featureAPI1Full <@> featureAPI1Full <@> featureAPI1Full - <@> mkNamedAPI @'("iget", ClassifiedDomainsConfig) (getFeatureStatus DontDoAuth) + <@> featureAPI1Get <@> featureAPI1Full <@> featureAPI1Full <@> featureAPI1Full @@ -281,9 +287,9 @@ featureAPI = <@> mkNamedAPI @'("ilock", MlsMigrationConfig) (updateLockStatus @MlsMigrationConfig) <@> mkNamedAPI @'("ilock", EnforceFileDownloadLocationConfig) (updateLockStatus @EnforceFileDownloadLocationConfig) -- special endpoints - <@> mkNamedAPI @'("igetmulti", SearchVisibilityInboundConfig) getFeatureStatusMulti + <@> mkNamedAPI @'("igetmulti", SearchVisibilityInboundConfig) getFeatureMulti -- all features - <@> mkNamedAPI @"feature-configs-internal" (maybe getAllFeatureConfigsForServer getAllFeatureConfigsForUser) + <@> mkNamedAPI @"feature-configs-internal" (maybe getAllTeamFeaturesForServer getAllTeamFeaturesForUser) rmUser :: forall p1 p2 r. @@ -337,7 +343,7 @@ rmUser lusr conn = do leaveTeams page = for_ (pageItems page) $ \tid -> do toNotify <- handleImpossibleErrors $ - getFeatureStatus @LimitedEventFanoutConfig DontDoAuth tid + getFeatureForTeam @LimitedEventFanoutConfig tid >>= ( \case FeatureStatusEnabled -> Left <$> E.getTeamAdmins tid FeatureStatusDisabled -> Right <$> getTeamMembersForFanout tid diff --git a/services/galley/src/Galley/API/LegalHold/Conflicts.hs b/services/galley/src/Galley/API/LegalHold/Conflicts.hs index e70ffff0f3d..4a4f8e27968 100644 --- a/services/galley/src/Galley/API/LegalHold/Conflicts.hs +++ b/services/galley/src/Galley/API/LegalHold/Conflicts.hs @@ -24,7 +24,7 @@ module Galley.API.LegalHold.Conflicts ) where -import Control.Lens (view, (^.)) +import Control.Lens (to, view, (^.)) import Data.ByteString.Conversion (toByteString') import Data.Id import Data.LegalHold (UserLegalHoldStatus (..)) @@ -44,6 +44,7 @@ import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P import System.Logger.Class qualified as Log +import Wire.API.Team.Feature import Wire.API.Team.LegalHold import Wire.API.Team.Member import Wire.API.User @@ -92,7 +93,7 @@ guardLegalholdPolicyConflicts LegalholdPlusFederationNotImplemented _otherClient guardLegalholdPolicyConflicts UnprotectedBot _otherClients = pure () guardLegalholdPolicyConflicts (ProtectedUser self) otherClients = do opts <- input - case view (settings . featureFlags . flagLegalHold) opts of + case view (settings . featureFlags . to npProject) opts of FeatureLegalHoldDisabledPermanently -> case FutureWork @'LegalholdPlusFederationNotImplemented () of FutureWork () -> -- FUTUREWORK: if federation is enabled, we still need to run the guard! diff --git a/services/galley/src/Galley/API/LegalHold/Team.hs b/services/galley/src/Galley/API/LegalHold/Team.hs index c62137f4e1a..a8851f2f83f 100644 --- a/services/galley/src/Galley/API/LegalHold/Team.hs +++ b/services/galley/src/Galley/API/LegalHold/Team.hs @@ -78,7 +78,7 @@ isLegalHoldEnabledForTeam :: TeamId -> Sem r Bool isLegalHoldEnabledForTeam tid = do - dbFeature <- getFeatureConfig FeatureSingletonLegalholdConfig tid + dbFeature <- getDbFeature tid status <- computeLegalHoldFeatureStatus tid dbFeature pure $ status == FeatureStatusEnabled diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs index 45703385cc4..6f182884dde 100644 --- a/services/galley/src/Galley/API/Public/Feature.hs +++ b/services/galley/src/Galley/API/Public/Feature.hs @@ -31,21 +31,21 @@ import Wire.API.Team.Feature featureAPIGetPut :: forall cfg r. (_) => API (FeatureAPIGetPut cfg) r featureAPIGetPut = - mkNamedAPI @'("get", cfg) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", cfg) (setFeatureStatus . DoAuth) + mkNamedAPI @'("get", cfg) getFeature + <@> mkNamedAPI @'("put", cfg) setFeature featureAPI :: API FeatureAPI GalleyEffects featureAPI = - mkNamedAPI @'("get", SSOConfig) (getFeatureStatus . DoAuth) + mkNamedAPI @'("get", SSOConfig) getFeature <@> featureAPIGetPut <@> featureAPIGetPut <@> mkNamedAPI @"get-search-visibility" getSearchVisibility <@> mkNamedAPI @"set-search-visibility" (setSearchVisibility (featureEnabledForTeam @SearchVisibilityAvailableConfig)) - <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get", DigitalSignaturesConfig) (getFeatureStatus . DoAuth) + <@> mkNamedAPI @'("get", ValidateSAMLEmailsConfig) getFeature + <@> mkNamedAPI @'("get", DigitalSignaturesConfig) getFeature <@> featureAPIGetPut <@> featureAPIGetPut - <@> mkNamedAPI @'("get", ClassifiedDomainsConfig) (getFeatureStatus . DoAuth) + <@> mkNamedAPI @'("get", ClassifiedDomainsConfig) getFeature <@> featureAPIGetPut <@> featureAPIGetPut <@> featureAPIGetPut @@ -54,36 +54,36 @@ featureAPI = <@> featureAPIGetPut <@> featureAPIGetPut <@> featureAPIGetPut - <@> mkNamedAPI @'("get", MlsE2EIdConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @"put-MlsE2EIdConfig@v5" (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put", MlsE2EIdConfig) (guardMlsE2EIdConfig (setFeatureStatus . DoAuth)) + <@> mkNamedAPI @'("get", MlsE2EIdConfig) getFeature + <@> mkNamedAPI @"put-MlsE2EIdConfig@v5" setFeature + <@> mkNamedAPI @'("put", MlsE2EIdConfig) (guardMlsE2EIdConfig setFeature) <@> hoistAPI id featureAPIGetPut <@> hoistAPI id featureAPIGetPut - <@> mkNamedAPI @'("get", LimitedEventFanoutConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @"get-all-feature-configs-for-user" getAllFeatureConfigsForUser - <@> mkNamedAPI @"get-all-feature-configs-for-team" getAllFeatureConfigsForTeam + <@> mkNamedAPI @'("get", LimitedEventFanoutConfig) getFeature + <@> mkNamedAPI @"get-all-feature-configs-for-user" getAllTeamFeaturesForUser + <@> mkNamedAPI @"get-all-feature-configs-for-team" getAllTeamFeaturesForTeam <@> deprecatedFeatureConfigAPI <@> deprecatedFeatureAPI deprecatedFeatureConfigAPI :: API DeprecatedFeatureAPI GalleyEffects deprecatedFeatureConfigAPI = - mkNamedAPI @'("get-deprecated", SearchVisibilityAvailableConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("put-deprecated", SearchVisibilityAvailableConfig) (setFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) (getFeatureStatus . DoAuth) - <@> mkNamedAPI @'("get-deprecated", DigitalSignaturesConfig) (getFeatureStatus . DoAuth) + mkNamedAPI @'("get-deprecated", SearchVisibilityAvailableConfig) getFeature + <@> mkNamedAPI @'("put-deprecated", SearchVisibilityAvailableConfig) setFeature + <@> mkNamedAPI @'("get-deprecated", ValidateSAMLEmailsConfig) getFeature + <@> mkNamedAPI @'("get-deprecated", DigitalSignaturesConfig) getFeature deprecatedFeatureAPI :: API (AllDeprecatedFeatureConfigAPI DeprecatedFeatureConfigs) GalleyEffects deprecatedFeatureAPI = - mkNamedAPI @'("get-config", LegalholdConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", SSOConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", DigitalSignaturesConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", AppLockConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", FileSharingConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", ClassifiedDomainsConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", ConferenceCallingConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", SelfDeletingMessagesConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", GuestLinksConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", SndFactorPasswordChallengeConfig) getSingleFeatureConfigForUser - <@> mkNamedAPI @'("get-config", MLSConfig) getSingleFeatureConfigForUser + mkNamedAPI @'("get-config", LegalholdConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", SSOConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", SearchVisibilityAvailableConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", ValidateSAMLEmailsConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", DigitalSignaturesConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", AppLockConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", FileSharingConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", ClassifiedDomainsConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", ConferenceCallingConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", SelfDeletingMessagesConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", GuestLinksConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", SndFactorPasswordChallengeConfig) getSingleFeatureForUser + <@> mkNamedAPI @'("get-config", MLSConfig) getSingleFeatureForUser diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 87457e2ccab..f788e2d349f 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -680,8 +680,8 @@ getConversationGuestLinksFeatureStatus :: ) => Maybe TeamId -> Sem r (LockableFeature GuestLinksConfig) -getConversationGuestLinksFeatureStatus Nothing = getConfigForServer @GuestLinksConfig -getConversationGuestLinksFeatureStatus (Just tid) = getConfigForTeam @GuestLinksConfig tid +getConversationGuestLinksFeatureStatus Nothing = getFeatureForServer @GuestLinksConfig +getConversationGuestLinksFeatureStatus (Just tid) = getFeatureForTeam @GuestLinksConfig tid -- | The same as 'getMLSSelfConversation', but it throws an error in case the -- backend is not configured for MLS (the proxy for it being the existance of diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index c489516322b..02994a77a90 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -991,7 +991,7 @@ deleteTeamMember' lusr zcon tid remove mBody = do Journal.teamUpdate tid sizeAfterDelete $ filter (/= remove) owners pure TeamMemberDeleteAccepted else do - getFeatureStatus @LimitedEventFanoutConfig DontDoAuth tid + getFeatureForTeam @LimitedEventFanoutConfig tid >>= ( \case FeatureStatusEnabled -> do admins <- E.getTeamAdmins tid diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 5f31df7e247..2d260c6c223 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -18,18 +18,16 @@ -- with this program. If not, see . module Galley.API.Teams.Features - ( getFeatureStatus, - getFeatureStatusMulti, - setFeatureStatus, - setFeatureStatusInternal, - patchFeatureStatusInternal, - getAllFeatureConfigsForTeam, - getAllFeatureConfigsForUser, + ( getFeatureMulti, + setFeature, + setFeatureInternal, + patchFeatureInternal, + getAllTeamFeaturesForTeam, + getAllTeamFeaturesForUser, updateLockStatus, GetFeatureConfig (..), SetFeatureConfig (..), guardSecondFactorDisabled, - DoAuth (..), featureEnabledForTeam, guardMlsE2EIdConfig, ) @@ -53,7 +51,6 @@ import Galley.Effects import Galley.Effects.BrigAccess (updateSearchVisibilityInbound) import Galley.Effects.SearchVisibilityStore qualified as SearchVisibilityData import Galley.Effects.TeamFeatureStore -import Galley.Effects.TeamFeatureStore qualified as TeamFeatures import Galley.Effects.TeamStore (getLegalHoldFlag, getTeamMember) import Galley.Options import Galley.Types.Teams @@ -66,7 +63,7 @@ import System.Logger.Class qualified as Log import Wire.API.Conversation.Role (Action (RemoveConversationMember)) import Wire.API.Error (ErrorS) import Wire.API.Error.Galley -import Wire.API.Event.FeatureConfig qualified as Event +import Wire.API.Event.FeatureConfig import Wire.API.Federation.Error import Wire.API.Team.Feature import Wire.API.Team.Member @@ -74,28 +71,26 @@ import Wire.NotificationSubsystem import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra -patchFeatureStatusInternal :: +patchFeatureInternal :: forall cfg r. ( SetFeatureConfig cfg, ComputeFeatureConstraints cfg r, - SetConfigForTeamConstraints cfg r, - Member (ErrorS 'NotATeamMember) r, + SetFeatureForTeamConstraints cfg r, Member (ErrorS 'TeamNotFound) r, Member (Input Opts) r, Member TeamStore r, Member TeamFeatureStore r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, + Member P.TinyLog r, Member NotificationSubsystem r ) => TeamId -> LockableFeaturePatch cfg -> Sem r (LockableFeature cfg) -patchFeatureStatusInternal tid patch = do +patchFeatureInternal tid patch = do assertTeamExists tid - currentFeatureStatus <- getFeatureStatus @cfg DontDoAuth tid + currentFeatureStatus <- getFeatureForTeam @cfg tid let newFeatureStatus = applyPatch currentFeatureStatus - void $ setConfigForTeam @cfg tid newFeatureStatus - getFeatureStatus @cfg DontDoAuth tid + setFeatureForTeam @cfg tid newFeatureStatus where applyPatch :: LockableFeature cfg -> LockableFeature cfg applyPatch current = @@ -105,55 +100,68 @@ patchFeatureStatusInternal tid patch = do config = fromMaybe current.config patch.config } -setFeatureStatus :: +setFeature :: forall cfg r. ( SetFeatureConfig cfg, ComputeFeatureConstraints cfg r, - SetConfigForTeamConstraints cfg r, + SetFeatureForTeamConstraints cfg r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS OperationDenied) r, - Member (ErrorS 'TeamNotFound) r, Member (Error TeamFeatureError) r, Member (Input Opts) r, Member TeamStore r, Member TeamFeatureStore r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, + Member P.TinyLog r, Member NotificationSubsystem r ) => - DoAuth -> + UserId -> TeamId -> Feature cfg -> Sem r (LockableFeature cfg) -setFeatureStatus doauth tid feat = do - case doauth of - DoAuth uid -> do - zusrMembership <- getTeamMember tid uid - void $ permissionCheck ChangeTeamFeature zusrMembership - DontDoAuth -> - assertTeamExists tid - feat0 <- getConfigForTeam @cfg tid - guardLockStatus feat0.lockStatus - setConfigForTeam @cfg tid (withLockStatus feat0.lockStatus feat) +setFeature uid tid feat = do + zusrMembership <- getTeamMember tid uid + void $ permissionCheck ChangeTeamFeature zusrMembership + setFeatureUnchecked tid feat -setFeatureStatusInternal :: +setFeatureInternal :: forall cfg r. ( SetFeatureConfig cfg, ComputeFeatureConstraints cfg r, - SetConfigForTeamConstraints cfg r, - Member (ErrorS 'NotATeamMember) r, - Member (ErrorS OperationDenied) r, + SetFeatureForTeamConstraints cfg r, Member (ErrorS 'TeamNotFound) r, Member (Error TeamFeatureError) r, Member (Input Opts) r, Member TeamStore r, Member TeamFeatureStore r, + Member P.TinyLog r, + Member NotificationSubsystem r + ) => + TeamId -> + Feature cfg -> + Sem r (LockableFeature cfg) +setFeatureInternal tid feat = do + assertTeamExists tid + setFeatureUnchecked tid feat + +setFeatureUnchecked :: + forall cfg r. + ( SetFeatureConfig cfg, + ComputeFeatureConstraints cfg r, + SetFeatureForTeamConstraints cfg r, + Member (Error TeamFeatureError) r, + Member (Input Opts) r, + Member TeamStore r, + Member TeamFeatureStore r, Member (P.Logger (Log.Msg -> Log.Msg)) r, Member NotificationSubsystem r ) => TeamId -> Feature cfg -> Sem r (LockableFeature cfg) -setFeatureStatusInternal = setFeatureStatus @cfg DontDoAuth +setFeatureUnchecked tid feat = do + feat0 <- getFeatureForTeam @cfg tid + guardLockStatus feat0.lockStatus + setFeatureForTeam @cfg tid (withLockStatus feat0.lockStatus feat) updateLockStatus :: forall cfg r. @@ -167,43 +175,38 @@ updateLockStatus :: Sem r LockStatusResponse updateLockStatus tid lockStatus = do assertTeamExists tid - TeamFeatures.setFeatureLockStatus (featureSingleton @cfg) tid lockStatus + setFeatureLockStatus @cfg tid lockStatus pure $ LockStatusResponse lockStatus -persistAndPushEvent :: +persistFeature :: forall cfg r. ( GetFeatureConfig cfg, ComputeFeatureConstraints cfg r, Member (Input Opts) r, - Member TeamFeatureStore r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, - Member NotificationSubsystem r, - Member TeamStore r + Member TeamFeatureStore r ) => TeamId -> LockableFeature cfg -> Sem r (LockableFeature cfg) -persistAndPushEvent tid feat = do - setFeatureConfig (featureSingleton @cfg) tid feat - fs <- getConfigForTeam @cfg tid - pushFeatureConfigEvent tid (Event.mkUpdateEvent fs) - pure fs +persistFeature tid feat = do + setDbFeature tid feat + getFeatureForTeam @cfg tid -pushFeatureConfigEvent :: +pushFeatureEvent :: ( Member NotificationSubsystem r, Member TeamStore r, Member P.TinyLog r ) => TeamId -> - Event.Event -> + Event -> Sem r () -pushFeatureConfigEvent tid event = do +pushFeatureEvent tid event = do memList <- getTeamMembersForFanout tid if ((memList ^. teamMemberListType) == ListTruncated) then do P.warn $ Log.field "action" (Log.val "Features.pushFeatureConfigEvent") - . Log.field "feature" (Log.val (toByteString' . Event._eventFeatureName $ event)) + . Log.field "feature" (Log.val (toByteString' . _eventFeatureName $ event)) . Log.field "team" (Log.val (UTF8.fromString . show $ tid)) . Log.msg @Text "Fanout limit exceeded. Events will not be sent." else do @@ -221,67 +224,72 @@ guardLockStatus = \case LockStatusUnlocked -> pure () LockStatusLocked -> throw FeatureLocked +setFeatureForTeam :: + ( SetFeatureConfig cfg, + SetFeatureForTeamConstraints cfg r, + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, + Member P.TinyLog r, + Member NotificationSubsystem r, + Member TeamFeatureStore r, + Member TeamStore r + ) => + TeamId -> + LockableFeature cfg -> + Sem r (LockableFeature cfg) +setFeatureForTeam tid feat = do + preparedFeat <- prepareFeature tid feat + newFeat <- persistFeature tid preparedFeat + pushFeatureEvent tid (mkUpdateEvent newFeat) + pure newFeat + ------------------------------------------------------------------------------- -- SetFeatureConfig instances -- | Don't export methods of this typeclass class (GetFeatureConfig cfg) => SetFeatureConfig cfg where - type SetConfigForTeamConstraints cfg (r :: EffectRow) :: Constraint - type SetConfigForTeamConstraints cfg (r :: EffectRow) = () - - -- | This method should generate the side-effects of changing the feature and - -- also (depending on the feature) persist the new setting to the database and - -- push a event to clients (see 'persistAndPushEvent'). - setConfigForTeam :: - ( SetConfigForTeamConstraints cfg r, - ComputeFeatureConstraints cfg r, - Member (Input Opts) r, - Member TeamFeatureStore r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, - Member NotificationSubsystem r, - Member TeamStore r - ) => + type SetFeatureForTeamConstraints cfg (r :: EffectRow) :: Constraint + type SetFeatureForTeamConstraints cfg (r :: EffectRow) = () + + -- | This method takes a feature about to be set, performs the required + -- checks, makes any related updates via the internal API, then finally + -- returns the feature to be persisted and pushed to clients. + -- + -- The default simply returns the original feature unchanged, which should be + -- enough for most features. + prepareFeature :: + (SetFeatureForTeamConstraints cfg r) => TeamId -> LockableFeature cfg -> Sem r (LockableFeature cfg) - default setConfigForTeam :: - ( ComputeFeatureConstraints cfg r, - Member (Input Opts) r, - Member TeamFeatureStore r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, - Member NotificationSubsystem r, - Member TeamStore r - ) => - TeamId -> - LockableFeature cfg -> - Sem r (LockableFeature cfg) - setConfigForTeam tid feat = persistAndPushEvent tid feat + default prepareFeature :: TeamId -> LockableFeature cfg -> Sem r (LockableFeature cfg) + prepareFeature _tid feat = pure feat instance SetFeatureConfig SSOConfig where type - SetConfigForTeamConstraints SSOConfig (r :: EffectRow) = + SetFeatureForTeamConstraints SSOConfig (r :: EffectRow) = ( Member (Input Opts) r, Member (Error TeamFeatureError) r ) - setConfigForTeam tid feat = do + prepareFeature _tid feat = do case feat.status of FeatureStatusEnabled -> pure () FeatureStatusDisabled -> throw DisableSsoNotImplemented - persistAndPushEvent tid feat + pure feat instance SetFeatureConfig SearchVisibilityAvailableConfig where type - SetConfigForTeamConstraints SearchVisibilityAvailableConfig (r :: EffectRow) = + SetFeatureForTeamConstraints SearchVisibilityAvailableConfig (r :: EffectRow) = ( Member SearchVisibilityStore r, Member (Input Opts) r ) - setConfigForTeam tid feat = do + prepareFeature tid feat = do case feat.status of FeatureStatusEnabled -> pure () FeatureStatusDisabled -> SearchVisibilityData.resetSearchVisibility tid - persistAndPushEvent tid feat + pure feat instance SetFeatureConfig ValidateSAMLEmailsConfig @@ -289,7 +297,7 @@ instance SetFeatureConfig DigitalSignaturesConfig instance SetFeatureConfig LegalholdConfig where type - SetConfigForTeamConstraints LegalholdConfig (r :: EffectRow) = + SetFeatureForTeamConstraints LegalholdConfig (r :: EffectRow) = ( Bounded (PagingBounds InternalPaging TeamMember), Member BackendNotificationQueueAccess r, Member BotAccess r, @@ -326,8 +334,7 @@ instance SetFeatureConfig LegalholdConfig where Member Random r ) - -- we're good to update the status now. - setConfigForTeam tid feat = do + prepareFeature tid feat = do -- this extra do is to encapsulate the assertions running before the actual operation. -- enabling LH for teams is only allowed in normal operation; disabled-permanently and -- whitelist-teams have no or their own way to do that, resp. @@ -343,17 +350,17 @@ instance SetFeatureConfig LegalholdConfig where case feat.status of FeatureStatusDisabled -> LegalHold.removeSettings' @InternalPaging tid FeatureStatusEnabled -> ensureNotTooLargeToActivateLegalHold tid - persistAndPushEvent tid feat + pure feat instance SetFeatureConfig FileSharingConfig instance SetFeatureConfig AppLockConfig where - type SetConfigForTeamConstraints AppLockConfig r = Member (Error TeamFeatureError) r + type SetFeatureForTeamConstraints AppLockConfig r = Member (Error TeamFeatureError) r - setConfigForTeam tid feat = do + prepareFeature _tid feat = do when ((applockInactivityTimeoutSecs feat.config) < 30) $ throw AppLockInactivityTimeoutTooLow - persistAndPushEvent tid feat + pure feat instance SetFeatureConfig ConferenceCallingConfig @@ -364,15 +371,20 @@ instance SetFeatureConfig GuestLinksConfig instance SetFeatureConfig SndFactorPasswordChallengeConfig instance SetFeatureConfig SearchVisibilityInboundConfig where - type SetConfigForTeamConstraints SearchVisibilityInboundConfig (r :: EffectRow) = (Member BrigAccess r) - setConfigForTeam tid feat = do + type SetFeatureForTeamConstraints SearchVisibilityInboundConfig (r :: EffectRow) = (Member BrigAccess r) + prepareFeature tid feat = do updateSearchVisibilityInbound $ toTeamStatus tid feat - persistAndPushEvent tid feat + pure feat instance SetFeatureConfig MLSConfig where - type SetConfigForTeamConstraints MLSConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) - setConfigForTeam tid feat = do - mlsMigrationConfig <- getConfigForTeam @MlsMigrationConfig tid + type + SetFeatureForTeamConstraints MLSConfig (r :: EffectRow) = + ( Member (Input Opts) r, + Member TeamFeatureStore r, + Member (Error TeamFeatureError) r + ) + prepareFeature tid feat = do + mlsMigrationConfig <- getFeatureForTeam @MlsMigrationConfig tid unless ( -- default protocol needs to be included in supported protocols feat.config.mlsDefaultProtocol `elem` feat.config.mlsSupportedProtocols @@ -380,7 +392,7 @@ instance SetFeatureConfig MLSConfig where && (mlsMigrationConfig.status == FeatureStatusDisabled || feat.status == FeatureStatusEnabled) ) $ throw MLSProtocolMismatch - persistAndPushEvent tid feat + pure feat instance SetFeatureConfig ExposeInvitationURLsToTeamAdminConfig @@ -401,15 +413,20 @@ guardMlsE2EIdConfig handler uid tid feat = do handler uid tid feat instance SetFeatureConfig MlsMigrationConfig where - type SetConfigForTeamConstraints MlsMigrationConfig (r :: EffectRow) = (Member (Error TeamFeatureError) r) - setConfigForTeam tid feat = do - mlsConfig <- getConfigForTeam @MLSConfig tid + type + SetFeatureForTeamConstraints MlsMigrationConfig (r :: EffectRow) = + ( Member (Input Opts) r, + Member (Error TeamFeatureError) r, + Member TeamFeatureStore r + ) + prepareFeature tid feat = do + mlsConfig <- getFeatureForTeam @MLSConfig tid unless ( -- when MLS migration is enabled, MLS needs to be enabled as well feat.status == FeatureStatusDisabled || mlsConfig.status == FeatureStatusEnabled ) $ throw MLSProtocolMismatch - persistAndPushEvent tid feat + pure feat instance SetFeatureConfig EnforceFileDownloadLocationConfig diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index 842403e7960..d4d448fd700 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -19,14 +19,16 @@ -- with this program. If not, see . module Galley.API.Teams.Features.Get - ( getFeatureStatus, - getFeatureStatusMulti, - getAllFeatureConfigsForServer, - getAllFeatureConfigsForTeam, - getAllFeatureConfigsForUser, - getSingleFeatureConfigForUser, + ( getFeature, + getFeatureInternal, + getFeatureMulti, + getAllTeamFeaturesForServer, + getAllTeamFeaturesForTeam, + getAllTeamFeaturesForUser, + getSingleFeatureForUser, GetFeatureConfig (..), - getConfigForTeam, + getFeatureForTeam, + getFeatureForServer, guardSecondFactorDisabled, DoAuth (..), featureEnabledForTeam, @@ -47,7 +49,7 @@ import Galley.API.Util import Galley.Effects import Galley.Effects.BrigAccess (getAccountConferenceCallingConfigClient) import Galley.Effects.ConversationStore as ConversationStore -import Galley.Effects.TeamFeatureStore qualified as TeamFeatures +import Galley.Effects.TeamFeatureStore import Galley.Effects.TeamStore (getOneUserTeam, getTeamMember) import Galley.Options import Galley.Types.Teams @@ -63,40 +65,37 @@ import Wire.API.Team.Feature data DoAuth = DoAuth UserId | DontDoAuth -type DefaultGetConfigForUserConstraints cfg r = +type DefaultGetFeatureForUserConstraints cfg r = ( Member (Input Opts) r, Member TeamFeatureStore r, ComputeFeatureConstraints cfg r ) -- | Don't export methods of this typeclass -class (IsFeatureConfig cfg) => GetFeatureConfig cfg where - type GetConfigForUserConstraints cfg (r :: EffectRow) :: Constraint +class + ( IsFeatureConfig cfg, + GetFeatureDefaults (FeatureDefaults cfg), + NpProject cfg Features + ) => + GetFeatureConfig cfg + where + type GetFeatureForUserConstraints cfg (r :: EffectRow) :: Constraint type - GetConfigForUserConstraints cfg (r :: EffectRow) = - DefaultGetConfigForUserConstraints cfg r + GetFeatureForUserConstraints cfg (r :: EffectRow) = + DefaultGetFeatureForUserConstraints cfg r type ComputeFeatureConstraints cfg (r :: EffectRow) :: Constraint type ComputeFeatureConstraints cfg r = () - getConfigForServer :: - (Member (Input Opts) r) => - Sem r (LockableFeature cfg) - -- only override if there is additional business logic for getting the feature config - -- and/or if the feature flag is configured for the backend in 'FeatureFlags' for galley in 'Galley.Types.Teams' - -- otherwise this will return the default config from wire-api - default getConfigForServer :: Sem r (LockableFeature cfg) - getConfigForServer = pure def - - getConfigForUser :: - (GetConfigForUserConstraints cfg r) => + getFeatureForUser :: + (GetFeatureForUserConstraints cfg r) => UserId -> Sem r (LockableFeature cfg) - default getConfigForUser :: - (DefaultGetConfigForUserConstraints cfg r) => + default getFeatureForUser :: + (DefaultGetFeatureForUserConstraints cfg r) => UserId -> Sem r (LockableFeature cfg) - getConfigForUser _ = getConfigForServer + getFeatureForUser _ = getFeatureForServer computeFeature :: (ComputeFeatureConstraints cfg r) => @@ -113,28 +112,37 @@ class (IsFeatureConfig cfg) => GetFeatureConfig cfg where pure $ genericComputeFeature @cfg defFeature dbFeature -getFeatureStatus :: +getFeature :: forall cfg r. ( GetFeatureConfig cfg, ComputeFeatureConstraints cfg r, Member (Input Opts) r, Member TeamFeatureStore r, Member (ErrorS 'NotATeamMember) r, + Member TeamStore r + ) => + UserId -> + TeamId -> + Sem r (LockableFeature cfg) +getFeature uid tid = do + void $ getTeamMember tid uid >>= noteS @'NotATeamMember + getFeatureForTeam @cfg tid + +getFeatureInternal :: + ( GetFeatureConfig cfg, + ComputeFeatureConstraints cfg r, + Member (Input Opts) r, Member (ErrorS 'TeamNotFound) r, + Member TeamFeatureStore r, Member TeamStore r ) => - DoAuth -> TeamId -> Sem r (LockableFeature cfg) -getFeatureStatus doauth tid = do - case doauth of - DoAuth uid -> - getTeamMember tid uid >>= maybe (throwS @'NotATeamMember) (const $ pure ()) - DontDoAuth -> - assertTeamExists tid - getConfigForTeam @cfg tid - -getFeatureStatusMulti :: +getFeatureInternal tid = do + assertTeamExists tid + getFeatureForTeam tid + +getFeatureMulti :: forall cfg r. ( GetFeatureConfig cfg, ComputeFeatureConstraints cfg r, @@ -143,8 +151,8 @@ getFeatureStatusMulti :: ) => Multi.TeamFeatureNoConfigMultiRequest -> Sem r (Multi.TeamFeatureNoConfigMultiResponse cfg) -getFeatureStatusMulti (Multi.TeamFeatureNoConfigMultiRequest tids) = do - cfgs <- getConfigForMultiTeam @cfg tids +getFeatureMulti (Multi.TeamFeatureNoConfigMultiRequest tids) = do + cfgs <- getFeatureForMultiTeam @cfg tids let xs = uncurry toTeamStatus <$> cfgs pure $ Multi.TeamFeatureNoConfigMultiResponse xs @@ -166,7 +174,7 @@ getTeamAndCheckMembership uid = do assertTeamExists tid pure mTid -getAllFeatureConfigsForTeam :: +getAllTeamFeaturesForTeam :: forall r. ( Member (Input Opts) r, Member (ErrorS 'NotATeamMember) r, @@ -176,22 +184,29 @@ getAllFeatureConfigsForTeam :: ) => Local UserId -> TeamId -> - Sem r AllFeatureConfigs -getAllFeatureConfigsForTeam luid tid = do + Sem r AllTeamFeatures +getAllTeamFeaturesForTeam luid tid = do void $ getTeamMember tid (tUnqualified luid) >>= noteS @'NotATeamMember - getAllFeatureConfigs tid + getAllTeamFeatures tid -class (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllFeatureConfigsForServerConstraints r cfg +class + (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => + GetAllFeaturesForServerConstraints r cfg -instance (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllFeatureConfigsForServerConstraints r cfg +instance + (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => + GetAllFeaturesForServerConstraints r cfg -getAllFeatureConfigsForServer :: +getAllTeamFeaturesForServer :: forall r. (Member (Input Opts) r) => - Sem r AllFeatureConfigs -getAllFeatureConfigsForServer = hsequence' $ hcpure (Proxy @GetFeatureConfig) $ Comp getConfigForServer + Sem r AllTeamFeatures +getAllTeamFeaturesForServer = + hsequence' $ + hcpure (Proxy @GetFeatureConfig) $ + Comp getFeatureForServer -getAllFeatureConfigs :: +getAllTeamFeatures :: forall r. ( Member (Input Opts) r, Member LegalHoldStore r, @@ -199,11 +214,11 @@ getAllFeatureConfigs :: Member TeamStore r ) => TeamId -> - Sem r AllFeatureConfigs -getAllFeatureConfigs tid = do - features <- TeamFeatures.getAllFeatureConfigs tid - defFeatures <- getAllFeatureConfigsForServer - hsequence' $ hcliftA2 (Proxy @(GetAllFeatureConfigsForServerConstraints r)) compute defFeatures features + Sem r AllTeamFeatures +getAllTeamFeatures tid = do + features <- getAllDbFeatures tid + defFeatures <- getAllTeamFeaturesForServer + hsequence' $ hcliftA2 (Proxy @(GetAllFeaturesForServerConstraints r)) compute defFeatures features where compute :: (ComputeFeatureConstraints p r, GetFeatureConfig p) => @@ -212,11 +227,11 @@ getAllFeatureConfigs tid = do (Sem r :.: LockableFeature) p compute defFeature feat = Comp $ computeFeature tid defFeature feat -class (GetConfigForUserConstraints cfg r, GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllFeatureConfigsForUserConstraints r cfg +class (GetFeatureForUserConstraints cfg r, GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllTeamFeaturesForUserConstraints r cfg -instance (GetConfigForUserConstraints cfg r, GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllFeatureConfigsForUserConstraints r cfg +instance (GetFeatureForUserConstraints cfg r, GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllTeamFeaturesForUserConstraints r cfg -getAllFeatureConfigsForUser :: +getAllTeamFeaturesForUser :: forall r. ( Member BrigAccess r, Member (ErrorS 'NotATeamMember) r, @@ -228,12 +243,12 @@ getAllFeatureConfigsForUser :: Member TeamStore r ) => UserId -> - Sem r AllFeatureConfigs -getAllFeatureConfigsForUser uid = do + Sem r AllTeamFeatures +getAllTeamFeaturesForUser uid = do mTid <- getTeamAndCheckMembership uid - hsequence' $ hcpure (Proxy @(GetAllFeatureConfigsForUserConstraints r)) $ Comp $ getConfigForTeamUser uid mTid + hsequence' $ hcpure (Proxy @(GetAllTeamFeaturesForUserConstraints r)) $ Comp $ getFeatureForTeamUser uid mTid -getSingleFeatureConfigForUser :: +getSingleFeatureForUser :: forall cfg r. ( GetFeatureConfig cfg, Member (Input Opts) r, @@ -241,16 +256,16 @@ getSingleFeatureConfigForUser :: Member (ErrorS 'TeamNotFound) r, Member TeamStore r, Member TeamFeatureStore r, - GetConfigForUserConstraints cfg r, + GetFeatureForUserConstraints cfg r, ComputeFeatureConstraints cfg r ) => UserId -> Sem r (LockableFeature cfg) -getSingleFeatureConfigForUser uid = do +getSingleFeatureForUser uid = do mTid <- getTeamAndCheckMembership uid - getConfigForTeamUser @cfg uid mTid + getFeatureForTeamUser @cfg uid mTid -getConfigForTeam :: +getFeatureForTeam :: forall cfg r. ( GetFeatureConfig cfg, ComputeFeatureConstraints cfg r, @@ -259,15 +274,15 @@ getConfigForTeam :: ) => TeamId -> Sem r (LockableFeature cfg) -getConfigForTeam tid = do - dbFeature <- TeamFeatures.getFeatureConfig (featureSingleton @cfg) tid - defFeature <- getConfigForServer +getFeatureForTeam tid = do + dbFeature <- getDbFeature tid + defFeature <- getFeatureForServer computeFeature @cfg tid defFeature dbFeature -getConfigForMultiTeam :: +getFeatureForMultiTeam :: forall cfg r. ( GetFeatureConfig cfg, ComputeFeatureConstraints cfg r, @@ -276,17 +291,17 @@ getConfigForMultiTeam :: ) => [TeamId] -> Sem r [(TeamId, LockableFeature cfg)] -getConfigForMultiTeam tids = do - defFeature <- getConfigForServer - features <- TeamFeatures.getFeatureConfigMulti (featureSingleton @cfg) tids +getFeatureForMultiTeam tids = do + defFeature <- getFeatureForServer + features <- getDbFeatureMulti tids for features $ \(tid, dbFeature) -> do feat <- computeFeature @cfg tid defFeature dbFeature pure (tid, feat) -getConfigForTeamUser :: +getFeatureForTeamUser :: forall cfg r. ( GetFeatureConfig cfg, - GetConfigForUserConstraints cfg r, + GetFeatureForUserConstraints cfg r, ComputeFeatureConstraints cfg r, Member (Input Opts) r, Member TeamFeatureStore r @@ -294,37 +309,32 @@ getConfigForTeamUser :: UserId -> Maybe TeamId -> Sem r (LockableFeature cfg) -getConfigForTeamUser uid Nothing = getConfigForUser uid -getConfigForTeamUser _ (Just tid) = getConfigForTeam @cfg tid +getFeatureForTeamUser uid Nothing = getFeatureForUser uid +getFeatureForTeamUser _ (Just tid) = getFeatureForTeam @cfg tid + +getFeatureForServer :: + forall cfg r. + ( GetFeatureDefaults (FeatureDefaults cfg), + NpProject cfg Features, + Member (Input Opts) r + ) => + Sem r (LockableFeature cfg) +getFeatureForServer = inputs $ view (settings . featureFlags . to (featureDefaults @cfg)) ------------------------------------------------------------------------------- -- GetFeatureConfig instances -instance GetFeatureConfig SSOConfig where - getConfigForServer = do - status <- - inputs (view (settings . featureFlags . flagSSO)) <&> \case - FeatureSSOEnabledByDefault -> FeatureStatusEnabled - FeatureSSODisabledByDefault -> FeatureStatusDisabled - pure $ def {status = status} - -instance GetFeatureConfig SearchVisibilityAvailableConfig where - getConfigForServer = do - status <- - inputs (view (settings . featureFlags . flagTeamSearchVisibility)) <&> \case - FeatureTeamSearchVisibilityAvailableByDefault -> FeatureStatusEnabled - FeatureTeamSearchVisibilityUnavailableByDefault -> FeatureStatusDisabled - pure $ def {status = status} - -instance GetFeatureConfig ValidateSAMLEmailsConfig where - getConfigForServer = - inputs (view (settings . featureFlags . flagsTeamFeatureValidateSAMLEmailsStatus . unDefaults . unImplicitLockStatus)) +instance GetFeatureConfig SSOConfig + +instance GetFeatureConfig SearchVisibilityAvailableConfig + +instance GetFeatureConfig ValidateSAMLEmailsConfig instance GetFeatureConfig DigitalSignaturesConfig instance GetFeatureConfig LegalholdConfig where type - GetConfigForUserConstraints LegalholdConfig (r :: EffectRow) = + GetFeatureForUserConstraints LegalholdConfig (r :: EffectRow) = ( Member (Input Opts) r, Member TeamFeatureStore r, Member LegalHoldStore r, @@ -341,17 +351,11 @@ instance GetFeatureConfig LegalholdConfig where status <- computeLegalHoldFeatureStatus tid dbFeature pure $ defFeature {status = status} -instance GetFeatureConfig FileSharingConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagFileSharing . unDefaults) +instance GetFeatureConfig FileSharingConfig -instance GetFeatureConfig AppLockConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagAppLockDefaults . unDefaults . unImplicitLockStatus) +instance GetFeatureConfig AppLockConfig -instance GetFeatureConfig ClassifiedDomainsConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagClassifiedDomains . unImplicitLockStatus) +instance GetFeatureConfig ClassifiedDomainsConfig -- | Conference calling gets enabled automatically once unlocked. To achieve -- that, the default feature status in the unlocked case is forced to be @@ -365,7 +369,7 @@ instance GetFeatureConfig ClassifiedDomainsConfig where -- situations. instance GetFeatureConfig ConferenceCallingConfig where type - GetConfigForUserConstraints ConferenceCallingConfig r = + GetFeatureForUserConstraints ConferenceCallingConfig r = ( Member (Input Opts) r, Member (ErrorS OperationDenied) r, Member (ErrorS 'NotATeamMember) r, @@ -375,10 +379,7 @@ instance GetFeatureConfig ConferenceCallingConfig where Member BrigAccess r ) - getConfigForServer = - input <&> view (settings . featureFlags . flagConferenceCalling . unDefaults) - - getConfigForUser uid = do + getFeatureForUser uid = do feat <- getAccountConferenceCallingConfigClient uid pure $ withLockStatus (def @(LockableFeature ConferenceCallingConfig)).lockStatus feat @@ -389,25 +390,15 @@ instance GetFeatureConfig ConferenceCallingConfig where LockStatusLocked -> defFeature {lockStatus = LockStatusLocked} LockStatusUnlocked -> feat -instance GetFeatureConfig SelfDeletingMessagesConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagSelfDeletingMessages . unDefaults) +instance GetFeatureConfig SelfDeletingMessagesConfig -instance GetFeatureConfig GuestLinksConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagConversationGuestLinks . unDefaults) +instance GetFeatureConfig GuestLinksConfig -instance GetFeatureConfig SndFactorPasswordChallengeConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagTeamFeatureSndFactorPasswordChallengeStatus . unDefaults) +instance GetFeatureConfig SndFactorPasswordChallengeConfig -instance GetFeatureConfig SearchVisibilityInboundConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagTeamFeatureSearchVisibilityInbound . unDefaults . unImplicitLockStatus) +instance GetFeatureConfig SearchVisibilityInboundConfig -instance GetFeatureConfig MLSConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagMLS . unDefaults) +instance GetFeatureConfig MLSConfig instance GetFeatureConfig ExposeInvitationURLsToTeamAdminConfig where type @@ -421,25 +412,15 @@ instance GetFeatureConfig ExposeInvitationURLsToTeamAdminConfig where lockStatus = if teamAllowed then LockStatusUnlocked else LockStatusLocked pure $ genericComputeFeature defFeature (dbFeatureLockStatus lockStatus <> dbFeature) -instance GetFeatureConfig OutlookCalIntegrationConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagOutlookCalIntegration . unDefaults) +instance GetFeatureConfig OutlookCalIntegrationConfig -instance GetFeatureConfig MlsE2EIdConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagMlsE2EId . unDefaults) +instance GetFeatureConfig MlsE2EIdConfig -instance GetFeatureConfig MlsMigrationConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagMlsMigration . unDefaults) +instance GetFeatureConfig MlsMigrationConfig -instance GetFeatureConfig EnforceFileDownloadLocationConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagEnforceFileDownloadLocation . unDefaults) +instance GetFeatureConfig EnforceFileDownloadLocationConfig -instance GetFeatureConfig LimitedEventFanoutConfig where - getConfigForServer = - input <&> view (settings . featureFlags . flagLimitedEventFanout . unDefaults . unImplicitLockStatus) +instance GetFeatureConfig LimitedEventFanoutConfig -- | If second factor auth is enabled, make sure that end-points that don't support it, but -- should, are blocked completely. (This is a workaround until we have 2FA for those @@ -464,7 +445,7 @@ guardSecondFactorDisabled uid cid = do mapError (unTagged @'TeamNotFound @()) $ assertTeamExists tid pure tid - tf <- getConfigForTeamUser @SndFactorPasswordChallengeConfig uid mTid + tf <- getFeatureForTeamUser @SndFactorPasswordChallengeConfig uid mTid case tf.status of FeatureStatusDisabled -> pure () FeatureStatusEnabled -> throwS @'AccessDenied @@ -473,7 +454,6 @@ featureEnabledForTeam :: forall cfg r. ( GetFeatureConfig cfg, Member (Input Opts) r, - Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'TeamNotFound) r, Member TeamStore r, Member TeamFeatureStore r, @@ -484,4 +464,4 @@ featureEnabledForTeam :: featureEnabledForTeam tid = (==) FeatureStatusEnabled . (.status) - <$> getFeatureStatus @cfg DontDoAuth tid + <$> getFeatureInternal @cfg tid diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 6fe22e53beb..a05438a3e10 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -704,8 +704,7 @@ updateConversationProtocolWithLocalUser :: Member Random r, Member ProposalStore r, Member SubConversationStore r, - Member TeamFeatureStore r, - Member TeamStore r + Member TeamFeatureStore r ) => Local UserId -> ConnId -> diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index cc0332b22a1..fac4e08102f 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -19,7 +19,7 @@ module Galley.API.Util where -import Control.Lens (set, view, (.~), (^.)) +import Control.Lens (set, to, view, (.~), (^.)) import Control.Monad.Extra (allM, anyM) import Data.Bifunctor import Data.Code qualified as Code @@ -81,6 +81,7 @@ import Wire.API.Federation.Error import Wire.API.Password import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util +import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Member qualified as Mem import Wire.API.Team.Role @@ -961,7 +962,7 @@ anyLegalholdActivated :: Sem r Bool anyLegalholdActivated uids = do opts <- input - case view (settings . featureFlags . flagLegalHold) opts of + case view (settings . featureFlags . to npProject) opts of FeatureLegalHoldDisabledPermanently -> pure False FeatureLegalHoldDisabledByDefault -> check FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> check @@ -980,7 +981,7 @@ allLegalholdConsentGiven :: Sem r Bool allLegalholdConsentGiven uids = do opts <- input - case view (settings . featureFlags . flagLegalHold) opts of + case view (settings . featureFlags . to npProject) opts of FeatureLegalHoldDisabledPermanently -> pure False FeatureLegalHoldDisabledByDefault -> do flip allM (chunksOf 32 uids) $ \uidsPage -> do diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 1e87befbc8d..a7488032814 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -81,8 +81,7 @@ import Galley.Options hiding (brig, endpoint, federator) import Galley.Options qualified as O import Galley.Queue import Galley.Queue qualified as Q -import Galley.Types.Teams (FeatureLegalHold) -import Galley.Types.Teams qualified as Teams +import Galley.Types.Teams import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) import Imports hiding (forkIO) import Network.AMQP.Extended (mkRabbitMqChannelMVar) @@ -146,9 +145,9 @@ validateOptions o = do (Nothing, Just _) -> error "RabbitMQ config is specified and federator is not, please specify both or none" (Just _, Nothing) -> error "Federator is specified and RabbitMQ config is not, please specify both or none" _ -> pure () - let mlsFlag = settings' ^. featureFlags . Teams.flagMLS . Teams.unDefaults + let mlsFlag = settings' ^. featureFlags . to (featureDefaults @MLSConfig) mlsConfig = mlsFlag.config - migrationStatus = (.status) $ settings' ^. featureFlags . Teams.flagMlsMigration . Teams.unDefaults + migrationStatus = (.status) $ settings' ^. featureFlags . to (featureDefaults @MlsMigrationConfig) when (migrationStatus == FeatureStatusEnabled && ProtocolMLSTag `notElem` mlsSupportedProtocols mlsConfig) $ error "For starting MLS migration, MLS must be included in the supportedProtocol list" unless (mlsDefaultProtocol mlsConfig `elem` mlsSupportedProtocols mlsConfig) $ @@ -258,7 +257,7 @@ evalGalley e = . runInputConst (e ^. options) . runInputConst (toLocalUnsafe (e ^. options . settings . federationDomain) ()) . interpretTeamFeatureSpecialContext e - . runInputSem getAllFeatureConfigsForServer + . runInputSem getAllTeamFeaturesForServer . interpretInternalTeamListToCassandra . interpretTeamListToCassandra . interpretLegacyConversationListToCassandra @@ -291,11 +290,11 @@ evalGalley e = . interpretSparAccess . interpretBrigAccess where - lh = view (options . settings . featureFlags . Teams.flagLegalHold) e + lh = view (options . settings . featureFlags . to npProject) e -interpretTeamFeatureSpecialContext :: Env -> Sem (Input (Maybe [TeamId], FeatureLegalHold) ': r) a -> Sem r a +interpretTeamFeatureSpecialContext :: Env -> Sem (Input (Maybe [TeamId], FeatureDefaults LegalholdConfig) ': r) a -> Sem r a interpretTeamFeatureSpecialContext e = runInputConst ( e ^. options . settings . exposeInvitationURLsTeamAllowlist, - e ^. options . settings . featureFlags . Teams.flagLegalHold + e ^. options . settings . featureFlags . to npProject ) diff --git a/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs b/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs similarity index 94% rename from services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs rename to services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs index 6fd27b9e107..3019ce00275 100644 --- a/services/galley/src/Galley/Cassandra/GetAllTeamFeatureConfigs.hs +++ b/services/galley/src/Galley/Cassandra/GetAllTeamFeatures.hs @@ -1,6 +1,6 @@ {-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-} -module Galley.Cassandra.GetAllTeamFeatureConfigs (getAllFeatureConfigs) where +module Galley.Cassandra.GetAllTeamFeatures (getAllDbFeatures) where import Cassandra import Data.Id @@ -71,7 +71,7 @@ instance where concatColumns = featureColumns @cfg `appendNP` concatColumns @cfgs -getAllFeatureConfigs :: +getAllDbFeatures :: forall row mrow m. ( MonadClient m, row ~ AllFeatureRow, @@ -81,6 +81,6 @@ getAllFeatureConfigs :: ) => TeamId -> m (AllFeatures DbFeature) -getAllFeatureConfigs tid = do +getAllDbFeatures tid = do mRow <- fetchFeatureRow @row @mrow tid (concatColumns @Features) pure . rowToAllFeatures $ fromMaybe emptyRow mRow diff --git a/services/galley/src/Galley/Cassandra/LegalHold.hs b/services/galley/src/Galley/Cassandra/LegalHold.hs index 490e46dcaa9..d9270b29a38 100644 --- a/services/galley/src/Galley/Cassandra/LegalHold.hs +++ b/services/galley/src/Galley/Cassandra/LegalHold.hs @@ -54,6 +54,7 @@ import Polysemy.Input import Polysemy.TinyLog import Ssl.Util qualified as SSL import Wire.API.Provider.Service +import Wire.API.Team.Feature import Wire.API.User.Client.Prekey interpretLegalHoldStoreToCassandra :: @@ -62,7 +63,7 @@ interpretLegalHoldStoreToCassandra :: Member (Input Env) r, Member TinyLog r ) => - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> Sem (LegalHoldStore ': r) a -> Sem r a interpretLegalHoldStoreToCassandra lh = interpret $ \case @@ -159,7 +160,7 @@ unsetTeamLegalholdWhitelisted :: (MonadClient m) => TeamId -> m () unsetTeamLegalholdWhitelisted tid = retry x5 (write Q.removeLegalHoldWhitelistedTeam (params LocalQuorum (Identity tid))) -isTeamLegalholdWhitelisted :: FeatureLegalHold -> TeamId -> Client Bool +isTeamLegalholdWhitelisted :: FeatureDefaults LegalholdConfig -> TeamId -> Client Bool isTeamLegalholdWhitelisted FeatureLegalHoldDisabledPermanently _ = pure False isTeamLegalholdWhitelisted FeatureLegalHoldDisabledByDefault _ = pure False isTeamLegalholdWhitelisted FeatureLegalHoldWhitelistTeamsAndImplicitConsent tid = diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index 84b5458d115..ef5a1b96b5f 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -60,6 +60,7 @@ import UnliftIO qualified import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Team import Wire.API.Team.Conversation +import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Permission (Perm (SetBilling), Permissions, self) import Wire.Sem.Paging.Cassandra @@ -70,7 +71,7 @@ interpretTeamStoreToCassandra :: Member (Input ClientState) r, Member TinyLog r ) => - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> Sem (TeamStore ': r) a -> Sem r a interpretTeamStoreToCassandra lh = interpret $ \case @@ -154,7 +155,7 @@ interpretTeamStoreToCassandra lh = interpret $ \case embedApp (currentFanoutLimit <$> view options) GetLegalHoldFlag -> do logEffect "TeamStore.GetLegalHoldFlag" - view (options . settings . featureFlags . flagLegalHold) <$> input + view (options . settings . featureFlags . to npProject) <$> input EnqueueTeamEvent e -> do logEffect "TeamStore.EnqueueTeamEvent" menv <- inputs (view aEnv) @@ -197,7 +198,7 @@ interpretTeamMemberStoreToCassandra :: Member (Input ClientState) r, Member TinyLog r ) => - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> Sem (TeamMemberStore InternalPaging ': r) a -> Sem r a interpretTeamMemberStoreToCassandra lh = interpret $ \case @@ -214,7 +215,7 @@ interpretTeamMemberStoreToCassandraWithPaging :: Member (Input ClientState) r, Member TinyLog r ) => - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> Sem (TeamMemberStore CassandraPaging ': r) a -> Sem r a interpretTeamMemberStoreToCassandraWithPaging lh = interpret $ \case @@ -277,7 +278,7 @@ teamIdsForPagination usr range (fromRange -> max) = Just c -> paginate Cql.selectUserTeamsFrom (paramsP LocalQuorum (usr, c) max) Nothing -> paginate Cql.selectUserTeams (paramsP LocalQuorum (Identity usr) max) -teamMember :: FeatureLegalHold -> TeamId -> UserId -> Client (Maybe TeamMember) +teamMember :: FeatureDefaults LegalholdConfig -> TeamId -> UserId -> Client (Maybe TeamMember) teamMember lh t u = newTeamMember'' u =<< retry x1 (query1 Cql.selectTeamMember (params LocalQuorum (t, u))) where @@ -368,7 +369,7 @@ teamIdsOf usr tids = map runIdentity <$> retry x1 (query Cql.selectUserTeamsIn (params LocalQuorum (usr, toList tids))) teamMembersWithLimit :: - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> TeamId -> Range 1 HardTruncationLimit Int32 -> Client TeamMemberList @@ -383,7 +384,7 @@ teamMembersWithLimit lh t (fromRange -> limit) = do -- NOTE: Use this function with care... should only be required when deleting a team! -- Maybe should be left explicitly for the caller? -teamMembersCollectedWithPagination :: FeatureLegalHold -> TeamId -> Client [TeamMember] +teamMembersCollectedWithPagination :: FeatureDefaults LegalholdConfig -> TeamId -> Client [TeamMember] teamMembersCollectedWithPagination lh tid = do mems <- teamMembersForPagination tid Nothing (unsafeRange 2000) collectTeamMembersPaginated [] mems @@ -397,7 +398,7 @@ teamMembersCollectedWithPagination lh tid = do -- Lookup only specific team members: this is particularly useful for large teams when -- needed to look up only a small subset of members (typically 2, user to perform the action -- and the target user) -teamMembersLimited :: FeatureLegalHold -> TeamId -> [UserId] -> Client [TeamMember] +teamMembersLimited :: FeatureDefaults LegalholdConfig -> TeamId -> [UserId] -> Client [TeamMember] teamMembersLimited lh t u = mapM (newTeamMember' lh t) =<< retry x1 (query Cql.selectTeamMembers' (params LocalQuorum (t, u))) @@ -499,7 +500,7 @@ updateTeam tid u = retry x5 . batch $ do -- Throw an exception if one of invitation timestamp and inviter is 'Nothing' and the -- other is 'Just', which can only be caused by inconsistent database content. newTeamMember' :: - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> TeamId -> (UserId, Permissions, Maybe UserId, Maybe UTCTimeMillis, Maybe UserLegalHoldStatus) -> Client TeamMember @@ -550,7 +551,7 @@ teamMembersForPagination tid start (fromRange -> max) = Nothing -> paginate Cql.selectTeamMembers (paramsP LocalQuorum (Identity tid) max) teamMembersPageFrom :: - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> TeamId -> Maybe PagingState -> Range 1 HardTruncationLimit Int32 -> @@ -561,7 +562,7 @@ teamMembersPageFrom lh tid pagingState (fromRange -> max) = do pure $ PageWithState members (pwsState page) selectSomeTeamMembersPaginated :: - FeatureLegalHold -> + FeatureDefaults LegalholdConfig -> TeamId -> [UserId] -> Maybe PagingState -> diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index f1db0f1a5c6..55bb2a9b840 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -19,8 +19,8 @@ module Galley.Cassandra.TeamFeatures ( interpretTeamFeatureStoreToCassandra, - getFeatureConfigMulti, - getAllFeatureConfigsForServer, + getDbFeatureMulti, + getAllTeamFeaturesForServer, ) where @@ -28,7 +28,7 @@ import Cassandra import Data.Id import Galley.API.Teams.Features.Get import Galley.Cassandra.FeatureTH -import Galley.Cassandra.GetAllTeamFeatureConfigs +import Galley.Cassandra.GetAllTeamFeatures import Galley.Cassandra.Instances () import Galley.Cassandra.MakeFeature import Galley.Cassandra.Store @@ -49,36 +49,36 @@ interpretTeamFeatureStoreToCassandra :: Sem (TFS.TeamFeatureStore ': r) a -> Sem r a interpretTeamFeatureStoreToCassandra = interpret $ \case - TFS.GetFeatureConfig sing tid -> do + TFS.GetDbFeature sing tid -> do logEffect "TeamFeatureStore.GetFeatureConfig" - embedClient $ getFeatureConfig sing tid - TFS.GetFeatureConfigMulti sing tids -> do + embedClient $ getDbFeature sing tid + TFS.GetDbFeatureMulti sing tids -> do logEffect "TeamFeatureStore.GetFeatureConfigMulti" - embedClient $ getFeatureConfigMulti sing tids - TFS.SetFeatureConfig sing tid feat -> do + embedClient $ getDbFeatureMulti sing tids + TFS.SetDbFeature sing tid feat -> do logEffect "TeamFeatureStore.SetFeatureConfig" - embedClient $ setFeatureConfig sing tid feat + embedClient $ setDbFeature sing tid feat TFS.SetFeatureLockStatus sing tid lock -> do logEffect "TeamFeatureStore.SetFeatureLockStatus" embedClient $ setFeatureLockStatus sing tid (Tagged lock) - TFS.GetAllFeatureConfigs tid -> do - logEffect "TeamFeatureStore.GetAllFeatureConfigs" - embedClient $ getAllFeatureConfigs tid + TFS.GetAllDbFeatures tid -> do + logEffect "TeamFeatureStore.GetAllTeamFeatures" + embedClient $ getAllDbFeatures tid -getFeatureConfigMulti :: +getDbFeatureMulti :: forall cfg m. (MonadClient m, MonadUnliftIO m) => FeatureSingleton cfg -> [TeamId] -> m [(TeamId, DbFeature cfg)] -getFeatureConfigMulti proxy = - pooledMapConcurrentlyN 8 (\tid -> getFeatureConfig proxy tid <&> (tid,)) +getDbFeatureMulti proxy = + pooledMapConcurrentlyN 8 (\tid -> getDbFeature proxy tid <&> (tid,)) -getFeatureConfig :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (DbFeature cfg) -getFeatureConfig = $(featureCases [|fetchFeature|]) +getDbFeature :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> m (DbFeature cfg) +getDbFeature = $(featureCases [|fetchFeature|]) -setFeatureConfig :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> LockableFeature cfg -> m () -setFeatureConfig = $(featureCases [|storeFeature|]) +setDbFeature :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> LockableFeature cfg -> m () +setDbFeature = $(featureCases [|storeFeature|]) setFeatureLockStatus :: (MonadClient m) => FeatureSingleton cfg -> TeamId -> Tagged cfg LockStatus -> m () setFeatureLockStatus = $(featureCases [|storeFeatureLockStatus|]) diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index 35279a4882e..1a9be889d25 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -91,14 +91,14 @@ import Galley.Effects.TeamNotificationStore import Galley.Effects.TeamStore import Galley.Env import Galley.Options -import Galley.Types.Teams (FeatureLegalHold) +import Galley.Types.Teams import Imports import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog import Wire.API.Error -import Wire.API.Team.Feature (AllFeatureConfigs) +import Wire.API.Team.Feature import Wire.GundeckAPIAccess import Wire.NotificationSubsystem import Wire.Rpc @@ -138,8 +138,8 @@ type GalleyEffects1 = ListItems LegacyPaging ConvId, ListItems LegacyPaging TeamId, ListItems InternalPaging TeamId, - Input AllFeatureConfigs, - Input (Maybe [TeamId], FeatureLegalHold), + Input AllTeamFeatures, + Input (Maybe [TeamId], FeatureDefaults LegalholdConfig), Input (Local ()), Input Opts, Input UTCTime, diff --git a/services/galley/src/Galley/Effects/TeamFeatureStore.hs b/services/galley/src/Galley/Effects/TeamFeatureStore.hs index d319d3515da..a7773e1fe7f 100644 --- a/services/galley/src/Galley/Effects/TeamFeatureStore.hs +++ b/services/galley/src/Galley/Effects/TeamFeatureStore.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE TemplateHaskell #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -25,15 +23,15 @@ import Wire.API.Team.Feature data TeamFeatureStore m a where -- | Returns all stored feature values excluding lock status. - GetFeatureConfig :: + GetDbFeature :: FeatureSingleton cfg -> TeamId -> TeamFeatureStore m (DbFeature cfg) - GetFeatureConfigMulti :: + GetDbFeatureMulti :: FeatureSingleton cfg -> [TeamId] -> TeamFeatureStore m [(TeamId, DbFeature cfg)] - SetFeatureConfig :: + SetDbFeature :: FeatureSingleton cfg -> TeamId -> LockableFeature cfg -> @@ -43,8 +41,37 @@ data TeamFeatureStore m a where TeamId -> LockStatus -> TeamFeatureStore m () - GetAllFeatureConfigs :: + GetAllDbFeatures :: TeamId -> TeamFeatureStore m (AllFeatures DbFeature) -makeSem ''TeamFeatureStore +getDbFeature :: + (Member TeamFeatureStore r, IsFeatureConfig cfg) => + TeamId -> + 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 -> + LockableFeature cfg -> + Sem r () +setDbFeature tid feat = send (SetDbFeature featureSingleton tid feat) + +setFeatureLockStatus :: + forall cfg r. + (Member TeamFeatureStore r, IsFeatureConfig cfg) => + TeamId -> + LockStatus -> + Sem r () +setFeatureLockStatus tid lockStatus = + send (SetFeatureLockStatus (featureSingleton @cfg) tid lockStatus) + +getAllDbFeatures :: (Member TeamFeatureStore r) => TeamId -> Sem r (AllFeatures DbFeature) +getAllDbFeatures tid = send (GetAllDbFeatures tid) diff --git a/services/galley/src/Galley/Effects/TeamStore.hs b/services/galley/src/Galley/Effects/TeamStore.hs index bd403e17f55..6c6eca8de17 100644 --- a/services/galley/src/Galley/Effects/TeamStore.hs +++ b/services/galley/src/Galley/Effects/TeamStore.hs @@ -90,6 +90,7 @@ import Wire.API.Error.Galley import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Team import Wire.API.Team.Conversation +import Wire.API.Team.Feature import Wire.API.Team.Member (HardTruncationLimit, TeamMember, TeamMemberList) import Wire.API.Team.Permission import Wire.Sem.Paging @@ -135,7 +136,7 @@ data TeamStore m a where SetTeamData :: TeamId -> TeamUpdateData -> TeamStore m () SetTeamStatus :: TeamId -> TeamStatus -> TeamStore m () FanoutLimit :: TeamStore m (Range 1 HardTruncationLimit Int32) - GetLegalHoldFlag :: TeamStore m FeatureLegalHold + GetLegalHoldFlag :: TeamStore m (FeatureDefaults LegalholdConfig) EnqueueTeamEvent :: E.TeamEvent -> TeamStore m () makeSem ''TeamStore diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index b318c0358bc..ceb2dbb7f51 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -412,7 +412,7 @@ testEnableSSOPerTeam = do liftIO $ do assertEqual "bad status" status403 (Wai.code waierr) assertEqual "bad label" "not-implemented" (Wai.label waierr) - featureSSO <- view (tsGConf . settings . featureFlags . flagSSO) + featureSSO <- view (tsGConf . settings . featureFlags . to npProject) case featureSSO of FeatureSSOEnabledByDefault -> check "Teams should start with SSO enabled" FeatureStatusEnabled FeatureSSODisabledByDefault -> check "Teams should start with SSO disabled" FeatureStatusDisabled diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs index 5309b659c94..921ebad99cd 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/Util.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -54,6 +54,7 @@ import Test.Tasty.Runners import TestSetup import Wire.API.Internal.Notification (ntfPayload) import Wire.API.Provider.Service +import Wire.API.Team.Feature import Wire.API.Team.Feature qualified as Public import Wire.API.Team.LegalHold import Wire.API.Team.LegalHold.External @@ -547,13 +548,13 @@ testOnlyIfLhWhitelisted :: IO TestSetup -> TestName -> TestM () -> TestTree testOnlyIfLhWhitelisted setupAction name testAction = do singleTest name $ LHTest FeatureLegalHoldWhitelistTeamsAndImplicitConsent setupAction testAction -data LHTest = LHTest FeatureLegalHold (IO TestSetup) (TestM ()) +data LHTest = LHTest (FeatureDefaults LegalholdConfig) (IO TestSetup) (TestM ()) instance IsTest LHTest where run :: OptionSet -> LHTest -> (Progress -> IO ()) -> IO Result run _ (LHTest expectedFlag setupAction testAction) _ = do setup <- setupAction - let featureLegalHold = setup ^. tsGConf . settings . featureFlags . flagLegalHold + let featureLegalHold = setup ^. tsGConf . settings . featureFlags . to npProject if featureLegalHold == expectedFlag then do hunitResult <- try $ void . flip runReaderT setup . runTestM $ testAction diff --git a/services/galley/test/integration/API/Util/TeamFeature.hs b/services/galley/test/integration/API/Util/TeamFeature.hs index 6d60c2bdb7a..cb8b7422a35 100644 --- a/services/galley/test/integration/API/Util/TeamFeature.hs +++ b/services/galley/test/integration/API/Util/TeamFeature.hs @@ -24,7 +24,7 @@ module API.Util.TeamFeature where import API.Util (HasGalley (viewGalley), zUser) import API.Util qualified as Util import Bilge -import Control.Lens ((.~)) +import Control.Lens ((%~)) import Data.ByteString.Conversion (toByteString') import Data.Id (ConvId, TeamId, UserId) import Galley.Options (featureFlags, settings) @@ -33,9 +33,13 @@ import Imports import TestSetup import Wire.API.Team.Feature -withCustomSearchFeature :: FeatureTeamSearchVisibilityAvailability -> TestM () -> TestM () +withCustomSearchFeature :: FeatureDefaults SearchVisibilityAvailableConfig -> TestM () -> TestM () withCustomSearchFeature flag action = do - Util.withSettingsOverrides (\opts -> opts & settings . featureFlags . flagTeamSearchVisibility .~ flag) action + Util.withSettingsOverrides + ( \opts -> + opts & settings . featureFlags %~ npUpdate @SearchVisibilityAvailableConfig flag + ) + action putTeamSearchVisibilityAvailableInternal :: (HasCallStack) => From 78227272ce334d9dc37b58b2a00ba8a69bab0417 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 19 Aug 2024 10:29:21 +0200 Subject: [PATCH 048/136] Add format paramter to mls public key endpoint (#4216) --- changelog.d/1-api-changes/jwk | 1 + integration/test/API/Galley.hs | 5 +++ integration/test/Test/MLS/Keys.hs | 30 ++++++++++++- libs/wire-api/src/Wire/API/MLS/Keys.hs | 44 ++++++++++++++++++- .../src/Wire/API/Routes/Public/Galley/MLS.hs | 26 ++++++----- services/galley/src/Galley/API/MLS.hs | 30 +++++++------ services/galley/src/Galley/API/Public/MLS.hs | 3 +- 7 files changed, 109 insertions(+), 30 deletions(-) create mode 100644 changelog.d/1-api-changes/jwk diff --git a/changelog.d/1-api-changes/jwk b/changelog.d/1-api-changes/jwk new file mode 100644 index 00000000000..6c9fd0e647f --- /dev/null +++ b/changelog.d/1-api-changes/jwk @@ -0,0 +1 @@ +The `mls/public-key` endpoint now takes a `format` query parameter which can be either `raw` (default, for raw base64-encoded keys) or `jwk` (for JWK keys) diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 4b04bb65bbf..6bb55137d82 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -228,6 +228,11 @@ getMLSPublicKeys user = do req <- baseRequest user Galley Versioned "/mls/public-keys" submit "GET" req +getMLSPublicKeysJWK :: (HasCallStack, MakesValue user) => user -> App Response +getMLSPublicKeysJWK user = do + req <- baseRequest user Galley Versioned "/mls/public-keys" + submit "GET" $ addQueryParams [("format", "jwk")] req + postMLSMessage :: (HasCallStack) => ClientIdentity -> ByteString -> App Response postMLSMessage cid msg = do req <- baseRequest cid Galley Versioned "/mls/messages" diff --git a/integration/test/Test/MLS/Keys.hs b/integration/test/Test/MLS/Keys.hs index d5ac4867c60..299262cc0e5 100644 --- a/integration/test/Test/MLS/Keys.hs +++ b/integration/test/Test/MLS/Keys.hs @@ -1,16 +1,42 @@ module Test.MLS.Keys where import API.Galley +import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64.URL as B64U import qualified Data.ByteString.Char8 as B8 import SetupHelpers import Testlib.Prelude -testPublicKeys :: (HasCallStack) => App () -testPublicKeys = do +testRawPublicKeys :: (HasCallStack) => App () +testRawPublicKeys = do u <- randomUserId OwnDomain keys <- getMLSPublicKeys u >>= getJSON 200 + do + pubkeyS <- keys %. "removal.ed25519" & asString + pubkey <- assertOne . toList . B64.decode $ B8.pack pubkeyS + B8.length pubkey `shouldMatchInt` 32 + + do + pubkeyS <- keys %. "removal.ecdsa_secp256r1_sha256" & asString + pubkey <- assertOne . toList . B64.decode $ B8.pack pubkeyS + B8.length pubkey `shouldMatchInt` 65 + + do + pubkeyS <- keys %. "removal.ecdsa_secp384r1_sha384" & asString + pubkey <- assertOne . toList . B64.decode $ B8.pack pubkeyS + B8.length pubkey `shouldMatchInt` 97 + + do + pubkeyS <- keys %. "removal.ecdsa_secp521r1_sha512" & asString + pubkey <- assertOne . toList . B64.decode $ B8.pack pubkeyS + B8.length pubkey `shouldMatchInt` 133 + +testJWKPublicKeys :: (HasCallStack) => App () +testJWKPublicKeys = do + u <- randomUserId OwnDomain + keys <- getMLSPublicKeysJWK u >>= getJSON 200 + do keys %. "removal.ed25519.crv" `shouldMatch` "Ed25519" keys %. "removal.ed25519.kty" `shouldMatch` "OKP" diff --git a/libs/wire-api/src/Wire/API/MLS/Keys.hs b/libs/wire-api/src/Wire/API/MLS/Keys.hs index 545a9c1c5cf..f070e81f595 100644 --- a/libs/wire-api/src/Wire/API/MLS/Keys.hs +++ b/libs/wire-api/src/Wire/API/MLS/Keys.hs @@ -17,16 +17,20 @@ module Wire.API.MLS.Keys where +import Control.Lens ((?~)) import Crypto.ECC (Curve_P256R1, Curve_P384R1, Curve_P521R1) import Crypto.PubKey.ECDSA qualified as ECDSA import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.Aeson qualified as A import Data.Bifunctor import Data.ByteArray qualified as BA +import Data.Default import Data.Json.Util import Data.OpenApi qualified as S import Data.Proxy import Data.Schema hiding (HasField) import Imports hiding (First, getFirst) +import Web.HttpApiData import Wire.API.MLS.CipherSuite data MLSKeysByPurpose a = MLSKeysByPurpose @@ -47,7 +51,7 @@ data MLSKeys a = MLSKeys ecdsa_secp384r1_sha384 :: a, ecdsa_secp521r1_sha512 :: a } - deriving (Eq, Show) + deriving (Eq, Show, Functor, Foldable, Traversable) deriving (FromJSON, ToJSON, S.ToSchema) via Schema (MLSKeys a) instance (ToSchema a) => ToSchema (MLSKeys a) where @@ -70,6 +74,7 @@ type MLSPublicKeys = MLSKeys MLSPublicKey newtype MLSPublicKey = MLSPublicKey {unwrapMLSPublicKey :: ByteString} deriving (Eq, Show) + deriving (ToJSON) via Schema MLSPublicKey instance ToSchema MLSPublicKey where schema = named "MLSPublicKey" $ MLSPublicKey <$> unwrapMLSPublicKey .= base64Schema @@ -83,6 +88,30 @@ mlsKeysToPublic (MLSPrivateKeys (_, ed) (_, ec256) (_, ec384) (_, ec521)) = ecdsa_secp521r1_sha512 = MLSPublicKey $ ECDSA.encodePublic (Proxy @Curve_P521R1) ec521 } +data MLSPublicKeyFormat = MLSPublicKeyFormatRaw | MLSPublicKeyFormatJWK + deriving (Eq, Ord, Show) + +instance Default MLSPublicKeyFormat where + def = MLSPublicKeyFormatRaw + +instance FromHttpApiData MLSPublicKeyFormat where + parseQueryParam "raw" = pure MLSPublicKeyFormatRaw + parseQueryParam "jwk" = pure MLSPublicKeyFormatJWK + parseQueryParam _ = Left "invalid MLSPublicKeyFormat" + +instance ToHttpApiData MLSPublicKeyFormat where + toQueryParam MLSPublicKeyFormatRaw = "raw" + toQueryParam MLSPublicKeyFormatJWK = "jwk" + +instance S.ToParamSchema MLSPublicKeyFormat where + toParamSchema _ = + mempty + & S.type_ ?~ S.OpenApiString + & S.enum_ + ?~ map + (toJSON . toQueryParam) + [MLSPublicKeyFormatRaw, MLSPublicKeyFormatJWK] + data JWK = JWK { keyType :: String, curve :: String, @@ -90,6 +119,7 @@ data JWK = JWK pubY :: Maybe ByteString } deriving (Show, Ord, Eq) + deriving (ToJSON) via Schema JWK instance ToSchema JWK where schema = @@ -134,3 +164,15 @@ mlsKeysToPublicJWK (MLSPrivateKeys (_, ed) (_, ec256) (_, ec384) (_, ec521)) = -- to Y. let size = BA.length xy `div` 2 pure $ BA.splitAt size xy + +data SomeKey = SomeKey A.Value + +instance ToSchema SomeKey where + schema = mkSchema d r w + where + d = pure $ S.NamedSchema (Just "SomeKey") mempty + r = fmap SomeKey . parseJSON + w (SomeKey x) = Just (toJSON x) + +mkSomeKey :: (ToJSON a) => a -> SomeKey +mkSomeKey = SomeKey . toJSON diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index 4237ee9cecc..ccace964c21 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -109,24 +109,26 @@ type MLSMessagingAPI = :> ReqBody '[MLS] (RawMLS CommitBundle) :> MultiVerb1 'POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus) ) - :<|> Named - "mls-public-keys-v6" - ( Summary "Get public keys used by the backend to sign external proposals" - :> From 'V5 - :> Until 'V7 - :> CanThrow 'MLSNotEnabled - :> "public-keys" - :> ZLocalUser - :> MultiVerb1 'GET '[JSON] (Respond 200 "Public keys" (MLSKeysByPurpose MLSPublicKeys)) - ) :<|> Named "mls-public-keys" ( Summary "Get public keys used by the backend to sign external proposals" - :> From 'V7 + :> Description + "The format of the returned key is determined by the `format` query parameter:\n\ + \ - raw (default): base64-encoded raw public keys\n\ + \ - jwk: keys are nested objects in JWK format." + :> From 'V5 :> CanThrow 'MLSNotEnabled :> "public-keys" :> ZLocalUser - :> MultiVerb1 'GET '[JSON] (Respond 200 "Public keys" (MLSKeysByPurpose MLSPublicKeysJWK)) + :> QueryParam "format" MLSPublicKeyFormat + :> MultiVerb1 + 'GET + '[JSON] + ( Respond + 200 + "Public keys" + (MLSKeysByPurpose (MLSKeys SomeKey)) + ) ) type MLSAPI = LiftNamed ("mls" :> MLSMessagingAPI) diff --git a/services/galley/src/Galley/API/MLS.hs b/services/galley/src/Galley/API/MLS.hs index ed2b2afbd17..0e017c2d606 100644 --- a/services/galley/src/Galley/API/MLS.hs +++ b/services/galley/src/Galley/API/MLS.hs @@ -22,10 +22,10 @@ module Galley.API.MLS postMLSCommitBundleFromLocalUser, postMLSMessageFromLocalUser, getMLSPublicKeys, - getMLSPublicKeysJWK, ) where +import Data.Default import Data.Id import Data.Qualified import Galley.API.Error @@ -42,17 +42,21 @@ import Wire.API.MLS.Keys getMLSPublicKeys :: ( Member (Input Env) r, - Member (ErrorS 'MLSNotEnabled) r + Member (ErrorS 'MLSNotEnabled) r, + Member (Error InternalError) r ) => Local UserId -> - Sem r (MLSKeysByPurpose MLSPublicKeys) -getMLSPublicKeys _ = mlsKeysToPublic <$$> getMLSPrivateKeys - -getMLSPublicKeysJWK :: - ( Member (Input Env) r, - Member (Error InternalError) r, - Member (ErrorS 'MLSNotEnabled) r - ) => - Local UserId -> - Sem r (MLSKeysByPurpose MLSPublicKeysJWK) -getMLSPublicKeysJWK _ = mapM (note (InternalErrorWithDescription "malformed MLS removal keys") . mlsKeysToPublicJWK) =<< getMLSPrivateKeys + Maybe MLSPublicKeyFormat -> + Sem r (MLSKeysByPurpose (MLSKeys SomeKey)) +getMLSPublicKeys _ fmt = do + keys <- getMLSPrivateKeys + case fromMaybe def fmt of + MLSPublicKeyFormatRaw -> pure (fmap (fmap mkSomeKey . mlsKeysToPublic) keys) + MLSPublicKeyFormatJWK -> do + jwks <- + traverse + ( note (InternalErrorWithDescription "malformed MLS removal keys") + . mlsKeysToPublicJWK + ) + keys + pure $ fmap (fmap mkSomeKey) jwks diff --git a/services/galley/src/Galley/API/Public/MLS.hs b/services/galley/src/Galley/API/Public/MLS.hs index ecebb6db990..fa05f9bf5d6 100644 --- a/services/galley/src/Galley/API/Public/MLS.hs +++ b/services/galley/src/Galley/API/Public/MLS.hs @@ -27,5 +27,4 @@ mlsAPI :: API MLSAPI GalleyEffects mlsAPI = mkNamedAPI @"mls-message" (callsFed (exposeAnnotations postMLSMessageFromLocalUser)) <@> mkNamedAPI @"mls-commit-bundle" (callsFed (exposeAnnotations postMLSCommitBundleFromLocalUser)) - <@> mkNamedAPI @"mls-public-keys-v6" getMLSPublicKeys - <@> mkNamedAPI @"mls-public-keys" getMLSPublicKeysJWK + <@> mkNamedAPI @"mls-public-keys" getMLSPublicKeys From 103b8fb07cebc215e62f0fb39fe76ae7f4ab35b1 Mon Sep 17 00:00:00 2001 From: Amit Sagtani Date: Mon, 19 Aug 2024 11:54:22 +0200 Subject: [PATCH 049/136] add warning when team/user creation is enabled over internet (#4212) --- changelog.d/5-internal/wpb-9844 | 1 + charts/wire-server/templates/NOTES.txt | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog.d/5-internal/wpb-9844 diff --git a/changelog.d/5-internal/wpb-9844 b/changelog.d/5-internal/wpb-9844 new file mode 100644 index 00000000000..cbf16c484b3 --- /dev/null +++ b/changelog.d/5-internal/wpb-9844 @@ -0,0 +1 @@ +Added warning when deploying wire-server helm chart with User/Team creation over internet enabled. diff --git a/charts/wire-server/templates/NOTES.txt b/charts/wire-server/templates/NOTES.txt index 2f93387327c..f6f59e0eda2 100644 --- a/charts/wire-server/templates/NOTES.txt +++ b/charts/wire-server/templates/NOTES.txt @@ -1,2 +1,5 @@ -TODO: write nice NOTES.txt +{{- if not (index .Values "brig" "config" "optSettings" "setRestrictUserCreation") }} +⚠️ ⚠️ ⚠️ User/Team creation is possible from outside the cluster, via Internet ⚠️ ⚠️ ⚠️ +To disable, Set brig.optSettings.setRestrictUserCreation to true. +{{- end }} From c27366548d6168d263d1101670bb2bdb69ab48f0 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Mon, 19 Aug 2024 14:08:26 +0200 Subject: [PATCH 050/136] [fix] Export FEDERATION_DOMAIN_BASE vars for teardown. --- hack/bin/integration-teardown-federation.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hack/bin/integration-teardown-federation.sh b/hack/bin/integration-teardown-federation.sh index 01791d223cd..198cfa850d6 100755 --- a/hack/bin/integration-teardown-federation.sh +++ b/hack/bin/integration-teardown-federation.sh @@ -12,6 +12,8 @@ export NAMESPACE_2="$NAMESPACE-fed2" # these don't matter for destruction but have to be set. export FEDERATION_DOMAIN_1="." export FEDERATION_DOMAIN_2="." +export FEDERATION_DOMAIN_BASE_1="." +export FEDERATION_DOMAIN_BASE_2="." KUBERNETES_VERSION_MAJOR="$(kubectl version -o json | jq -r .serverVersion.major)" KUBERNETES_VERSION_MINOR="$(kubectl version -o json | jq -r .serverVersion.minor)" From 774f39a7ae27067c3d8bef1c84628baa495c9063 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:26:50 +0200 Subject: [PATCH 051/136] [chore] Simplify email types (#4206) --- changelog.d/5-internal/refactor-email | 1 + .../test/unit/Test/Brig/Types/User.hs | 2 +- libs/hscim/server/Main.hs | 2 +- libs/hscim/src/Web/Scim/Schema/User/Email.hs | 25 +- libs/hscim/test/Test/Schema/UserSpec.hs | 2 +- libs/imports/default.nix | 2 + libs/imports/imports.cabal | 1 + libs/imports/src/Imports.hs | 6 + libs/wire-api/default.nix | 3 - libs/wire-api/src/Wire/API/Allowlists.hs | 5 +- libs/wire-api/src/Wire/API/OAuth.hs | 1 - libs/wire-api/src/Wire/API/Provider.hs | 14 +- .../src/Wire/API/Routes/Internal/Brig.hs | 18 +- .../src/Wire/API/Routes/Internal/Brig/EJPD.hs | 6 +- .../Wire/API/Routes/MultiTablePaging/State.hs | 1 - .../wire-api/src/Wire/API/Routes/MultiVerb.hs | 1 - .../src/Wire/API/Routes/Public/Brig.hs | 2 +- libs/wire-api/src/Wire/API/Team/Export.hs | 8 +- libs/wire-api/src/Wire/API/Team/Invitation.hs | 6 +- libs/wire-api/src/Wire/API/User.hs | 22 +- libs/wire-api/src/Wire/API/User/Activation.hs | 12 +- libs/wire-api/src/Wire/API/User/Auth.hs | 10 +- .../src/Wire/API/User/EmailAddress.hs | 110 +++++++++ libs/wire-api/src/Wire/API/User/Identity.hs | 145 ++---------- libs/wire-api/src/Wire/API/User/Password.hs | 18 +- libs/wire-api/src/Wire/API/User/Scim.hs | 38 +-- libs/wire-api/src/Wire/API/User/Search.hs | 7 +- libs/wire-api/src/Wire/API/UserEvent.hs | 8 +- .../golden/Test/Wire/API/Golden/Generated.hs | 8 +- .../Generated/ActivationResponse_user.hs | 18 +- .../Generated/CompletePasswordReset_user.hs | 12 +- .../Golden/Generated/EmailUpdate_provider.hs | 145 +----------- .../API/Golden/Generated/EmailUpdate_user.hs | 90 ++------ .../Wire/API/Golden/Generated/Email_user.hs | 118 +--------- .../Golden/Generated/InvitationList_team.hs | 105 +++++---- .../Generated/InvitationRequest_team.hs | 42 ++-- .../API/Golden/Generated/Invitation_team.hs | 42 ++-- .../Golden/Generated/NewPasswordReset_user.hs | 7 +- .../Golden/Generated/NewProvider_provider.hs | 42 ++-- .../Golden/Generated/NewUserPublic_user.hs | 2 +- .../Wire/API/Golden/Generated/NewUser_user.hs | 9 +- .../Generated/PasswordReset_provider.hs | 91 ++------ .../ProviderActivationResponse_provider.hs | 78 ++----- .../Generated/ProviderLogin_provider.hs | 52 ++--- .../Generated/ProviderProfile_provider.hs | 42 ++-- .../API/Golden/Generated/Provider_provider.hs | 42 ++-- .../SearchResult_20TeamContact_user.hs | 90 ++++---- .../API/Golden/Generated/SelfProfile_user.hs | 3 +- .../API/Golden/Generated/TeamContact_user.hs | 46 ++-- .../API/Golden/Generated/UserProfile_user.hs | 3 +- .../API/Golden/Generated/UserUpdate_user.hs | 1 - .../Wire/API/Golden/Generated/User_user.hs | 9 +- .../Wire/API/Golden/Manual/Activate_user.hs | 2 +- .../Wire/API/Golden/Manual/ListUsersById.hs | 1 - .../Wire/API/Golden/Manual/LoginId_user.hs | 9 +- .../Test/Wire/API/Golden/Manual/Login_user.hs | 8 +- .../Golden/Manual/SendActivationCode_user.hs | 4 +- .../Test/Wire/API/Golden/Manual/UserEvent.hs | 5 +- .../fromJSON/testObject_NewUser_user_3-2.json | 2 +- .../golden/testObject_Activate_user_2.json | 2 +- .../testObject_ActivationResponse_user_1.json | 2 +- ...testObject_ActivationResponse_user_10.json | 2 +- .../testObject_ActivationResponse_user_2.json | 2 +- .../testObject_ActivationResponse_user_3.json | 2 +- .../testObject_ActivationResponse_user_4.json | 2 +- .../testObject_ActivationResponse_user_5.json | 2 +- .../testObject_ActivationResponse_user_7.json | 2 +- .../testObject_ActivationResponse_user_8.json | 2 +- .../testObject_ActivationResponse_user_9.json | 2 +- ...stObject_CompletePasswordReset_user_1.json | 2 +- ...tObject_CompletePasswordReset_user_12.json | 2 +- ...tObject_CompletePasswordReset_user_14.json | 2 +- ...tObject_CompletePasswordReset_user_15.json | 2 +- ...tObject_CompletePasswordReset_user_17.json | 2 +- ...stObject_CompletePasswordReset_user_9.json | 2 +- .../testObject_ConversationCoverView_1.json | 4 +- .../testObject_ConversationCoverView_2.json | 4 +- .../testObject_ConversationCoverView_3.json | 4 +- .../testObject_EmailUpdate_provider_1.json | 2 +- .../testObject_EmailUpdate_provider_10.json | 3 - .../testObject_EmailUpdate_provider_11.json | 3 - .../testObject_EmailUpdate_provider_12.json | 3 - .../testObject_EmailUpdate_provider_13.json | 3 - .../testObject_EmailUpdate_provider_14.json | 3 - .../testObject_EmailUpdate_provider_15.json | 3 - .../testObject_EmailUpdate_provider_16.json | 3 - .../testObject_EmailUpdate_provider_17.json | 3 - .../testObject_EmailUpdate_provider_18.json | 3 - .../testObject_EmailUpdate_provider_19.json | 3 - .../testObject_EmailUpdate_provider_2.json | 3 - .../testObject_EmailUpdate_provider_20.json | 3 - .../testObject_EmailUpdate_provider_3.json | 3 - .../testObject_EmailUpdate_provider_4.json | 3 - .../testObject_EmailUpdate_provider_5.json | 3 - .../testObject_EmailUpdate_provider_6.json | 3 - .../testObject_EmailUpdate_provider_7.json | 3 - .../testObject_EmailUpdate_provider_8.json | 3 - .../testObject_EmailUpdate_provider_9.json | 3 - .../golden/testObject_EmailUpdate_user_1.json | 2 +- .../testObject_EmailUpdate_user_10.json | 2 +- .../testObject_EmailUpdate_user_11.json | 2 +- .../testObject_EmailUpdate_user_12.json | 2 +- .../testObject_EmailUpdate_user_13.json | 2 +- .../testObject_EmailUpdate_user_14.json | 2 +- .../testObject_EmailUpdate_user_15.json | 2 +- .../testObject_EmailUpdate_user_16.json | 2 +- .../testObject_EmailUpdate_user_17.json | 2 +- .../testObject_EmailUpdate_user_18.json | 2 +- .../testObject_EmailUpdate_user_19.json | 2 +- .../golden/testObject_EmailUpdate_user_2.json | 2 +- .../testObject_EmailUpdate_user_20.json | 2 +- .../golden/testObject_EmailUpdate_user_3.json | 2 +- .../golden/testObject_EmailUpdate_user_4.json | 2 +- .../golden/testObject_EmailUpdate_user_5.json | 2 +- .../golden/testObject_EmailUpdate_user_6.json | 2 +- .../golden/testObject_EmailUpdate_user_7.json | 2 +- .../golden/testObject_EmailUpdate_user_8.json | 2 +- .../golden/testObject_EmailUpdate_user_9.json | 2 +- .../test/golden/testObject_Email_user_1.json | 2 +- .../test/golden/testObject_Email_user_10.json | 1 - .../test/golden/testObject_Email_user_11.json | 1 - .../test/golden/testObject_Email_user_12.json | 1 - .../test/golden/testObject_Email_user_13.json | 1 - .../test/golden/testObject_Email_user_14.json | 1 - .../test/golden/testObject_Email_user_15.json | 1 - .../test/golden/testObject_Email_user_16.json | 1 - .../test/golden/testObject_Email_user_17.json | 1 - .../test/golden/testObject_Email_user_18.json | 1 - .../test/golden/testObject_Email_user_19.json | 1 - .../test/golden/testObject_Email_user_2.json | 1 - .../test/golden/testObject_Email_user_20.json | 1 - .../test/golden/testObject_Email_user_3.json | 1 - .../test/golden/testObject_Email_user_4.json | 1 - .../test/golden/testObject_Email_user_5.json | 1 - .../test/golden/testObject_Email_user_6.json | 1 - .../test/golden/testObject_Email_user_7.json | 1 - .../test/golden/testObject_Email_user_8.json | 1 - .../test/golden/testObject_Email_user_9.json | 1 - .../testObject_Event_conversation_3.json | 4 +- .../test/golden/testObject_Event_user_14.json | 4 +- .../golden/testObject_Feature_team_14.json | 8 +- .../testObject_InvitationList_team_10.json | 2 +- .../testObject_InvitationList_team_11.json | 2 +- .../testObject_InvitationList_team_13.json | 14 +- .../testObject_InvitationList_team_15.json | 10 +- .../testObject_InvitationList_team_16.json | 2 +- .../testObject_InvitationList_team_17.json | 2 +- .../testObject_InvitationList_team_2.json | 2 +- .../testObject_InvitationList_team_20.json | 4 +- .../testObject_InvitationList_team_4.json | 16 +- .../testObject_InvitationList_team_6.json | 30 +-- .../testObject_InvitationList_team_7.json | 6 +- .../testObject_InvitationList_team_8.json | 4 +- .../testObject_InvitationList_team_9.json | 6 +- .../testObject_InvitationRequest_team_1.json | 2 +- .../testObject_InvitationRequest_team_10.json | 2 +- .../testObject_InvitationRequest_team_11.json | 2 +- .../testObject_InvitationRequest_team_12.json | 2 +- .../testObject_InvitationRequest_team_13.json | 2 +- .../testObject_InvitationRequest_team_14.json | 2 +- .../testObject_InvitationRequest_team_15.json | 2 +- .../testObject_InvitationRequest_team_16.json | 2 +- .../testObject_InvitationRequest_team_17.json | 2 +- .../testObject_InvitationRequest_team_18.json | 2 +- .../testObject_InvitationRequest_team_19.json | 2 +- .../testObject_InvitationRequest_team_2.json | 2 +- .../testObject_InvitationRequest_team_20.json | 2 +- .../testObject_InvitationRequest_team_3.json | 2 +- .../testObject_InvitationRequest_team_4.json | 2 +- .../testObject_InvitationRequest_team_5.json | 2 +- .../testObject_InvitationRequest_team_6.json | 2 +- .../testObject_InvitationRequest_team_7.json | 2 +- .../testObject_InvitationRequest_team_8.json | 2 +- .../testObject_InvitationRequest_team_9.json | 2 +- .../golden/testObject_Invitation_team_1.json | 2 +- .../golden/testObject_Invitation_team_10.json | 2 +- .../golden/testObject_Invitation_team_11.json | 2 +- .../golden/testObject_Invitation_team_12.json | 2 +- .../golden/testObject_Invitation_team_13.json | 2 +- .../golden/testObject_Invitation_team_14.json | 2 +- .../golden/testObject_Invitation_team_15.json | 2 +- .../golden/testObject_Invitation_team_16.json | 2 +- .../golden/testObject_Invitation_team_17.json | 2 +- .../golden/testObject_Invitation_team_18.json | 2 +- .../golden/testObject_Invitation_team_19.json | 2 +- .../golden/testObject_Invitation_team_2.json | 2 +- .../golden/testObject_Invitation_team_20.json | 2 +- .../golden/testObject_Invitation_team_3.json | 2 +- .../golden/testObject_Invitation_team_4.json | 2 +- .../golden/testObject_Invitation_team_5.json | 2 +- .../golden/testObject_Invitation_team_6.json | 2 +- .../golden/testObject_Invitation_team_7.json | 2 +- .../golden/testObject_Invitation_team_8.json | 2 +- .../golden/testObject_Invitation_team_9.json | 2 +- .../testObject_ListUsersById_user_1.json | 4 +- .../testObject_ListUsersById_user_3.json | 43 ++-- .../testObject_LockableFeature_team_14.json | 8 +- .../golden/testObject_LoginId_user_1.json | 2 +- .../golden/testObject_LoginId_user_2.json | 2 +- .../golden/testObject_LoginId_user_6.json | 2 +- .../test/golden/testObject_Login_user_1.json | 2 +- .../test/golden/testObject_Login_user_4.json | 2 +- .../test/golden/testObject_Login_user_5.json | 2 +- .../testObject_NewPasswordReset_user_1.json | 2 +- .../testObject_NewPasswordReset_user_20.json | 3 - .../testObject_NewProvider_provider_1.json | 2 +- .../testObject_NewProvider_provider_10.json | 2 +- .../testObject_NewProvider_provider_11.json | 2 +- .../testObject_NewProvider_provider_12.json | 2 +- .../testObject_NewProvider_provider_13.json | 2 +- .../testObject_NewProvider_provider_14.json | 2 +- .../testObject_NewProvider_provider_15.json | 2 +- .../testObject_NewProvider_provider_16.json | 2 +- .../testObject_NewProvider_provider_17.json | 2 +- .../testObject_NewProvider_provider_18.json | 2 +- .../testObject_NewProvider_provider_19.json | 2 +- .../testObject_NewProvider_provider_2.json | 2 +- .../testObject_NewProvider_provider_20.json | 2 +- .../testObject_NewProvider_provider_3.json | 2 +- .../testObject_NewProvider_provider_4.json | 2 +- .../testObject_NewProvider_provider_5.json | 2 +- .../testObject_NewProvider_provider_6.json | 2 +- .../testObject_NewProvider_provider_7.json | 2 +- .../testObject_NewProvider_provider_8.json | 2 +- .../testObject_NewProvider_provider_9.json | 2 +- .../testObject_NewUserPublic_user_1.json | 2 +- .../golden/testObject_NewUser_user_1.json | 2 +- .../golden/testObject_NewUser_user_7.json | 2 +- .../golden/testObject_NewUser_user_8.json | 2 +- .../testObject_PasswordReset_provider_1.json | 2 +- .../testObject_PasswordReset_provider_10.json | 2 +- .../testObject_PasswordReset_provider_11.json | 2 +- .../testObject_PasswordReset_provider_12.json | 2 +- .../testObject_PasswordReset_provider_13.json | 2 +- .../testObject_PasswordReset_provider_14.json | 2 +- .../testObject_PasswordReset_provider_15.json | 2 +- .../testObject_PasswordReset_provider_16.json | 2 +- .../testObject_PasswordReset_provider_17.json | 2 +- .../testObject_PasswordReset_provider_18.json | 2 +- .../testObject_PasswordReset_provider_19.json | 2 +- .../testObject_PasswordReset_provider_2.json | 2 +- .../testObject_PasswordReset_provider_20.json | 2 +- .../testObject_PasswordReset_provider_3.json | 2 +- .../testObject_PasswordReset_provider_4.json | 2 +- .../testObject_PasswordReset_provider_5.json | 2 +- .../testObject_PasswordReset_provider_6.json | 2 +- .../testObject_PasswordReset_provider_7.json | 2 +- .../testObject_PasswordReset_provider_8.json | 2 +- .../testObject_PasswordReset_provider_9.json | 2 +- ...ProviderActivationResponse_provider_1.json | 2 +- ...roviderActivationResponse_provider_10.json | 2 +- ...roviderActivationResponse_provider_11.json | 2 +- ...roviderActivationResponse_provider_12.json | 2 +- ...roviderActivationResponse_provider_13.json | 2 +- ...roviderActivationResponse_provider_14.json | 2 +- ...roviderActivationResponse_provider_15.json | 2 +- ...roviderActivationResponse_provider_16.json | 2 +- ...roviderActivationResponse_provider_17.json | 2 +- ...roviderActivationResponse_provider_18.json | 2 +- ...roviderActivationResponse_provider_19.json | 2 +- ...ProviderActivationResponse_provider_2.json | 2 +- ...roviderActivationResponse_provider_20.json | 2 +- ...ProviderActivationResponse_provider_3.json | 2 +- ...ProviderActivationResponse_provider_4.json | 2 +- ...ProviderActivationResponse_provider_5.json | 2 +- ...ProviderActivationResponse_provider_6.json | 2 +- ...ProviderActivationResponse_provider_7.json | 2 +- ...ProviderActivationResponse_provider_8.json | 2 +- ...ProviderActivationResponse_provider_9.json | 2 +- .../testObject_ProviderLogin_provider_1.json | 2 +- .../testObject_ProviderLogin_provider_10.json | 2 +- .../testObject_ProviderLogin_provider_11.json | 2 +- .../testObject_ProviderLogin_provider_12.json | 2 +- .../testObject_ProviderLogin_provider_13.json | 2 +- .../testObject_ProviderLogin_provider_14.json | 2 +- .../testObject_ProviderLogin_provider_15.json | 2 +- .../testObject_ProviderLogin_provider_16.json | 2 +- .../testObject_ProviderLogin_provider_17.json | 2 +- .../testObject_ProviderLogin_provider_18.json | 2 +- .../testObject_ProviderLogin_provider_19.json | 2 +- .../testObject_ProviderLogin_provider_2.json | 2 +- .../testObject_ProviderLogin_provider_20.json | 2 +- .../testObject_ProviderLogin_provider_3.json | 2 +- .../testObject_ProviderLogin_provider_4.json | 2 +- .../testObject_ProviderLogin_provider_5.json | 2 +- .../testObject_ProviderLogin_provider_6.json | 2 +- .../testObject_ProviderLogin_provider_7.json | 2 +- .../testObject_ProviderLogin_provider_8.json | 2 +- .../testObject_ProviderLogin_provider_9.json | 2 +- ...testObject_ProviderProfile_provider_1.json | 2 +- ...estObject_ProviderProfile_provider_10.json | 2 +- ...estObject_ProviderProfile_provider_11.json | 2 +- ...estObject_ProviderProfile_provider_12.json | 2 +- ...estObject_ProviderProfile_provider_13.json | 2 +- ...estObject_ProviderProfile_provider_14.json | 2 +- ...estObject_ProviderProfile_provider_15.json | 2 +- ...estObject_ProviderProfile_provider_16.json | 2 +- ...estObject_ProviderProfile_provider_17.json | 2 +- ...estObject_ProviderProfile_provider_18.json | 2 +- ...estObject_ProviderProfile_provider_19.json | 2 +- ...testObject_ProviderProfile_provider_2.json | 2 +- ...estObject_ProviderProfile_provider_20.json | 2 +- ...testObject_ProviderProfile_provider_3.json | 2 +- ...testObject_ProviderProfile_provider_4.json | 2 +- ...testObject_ProviderProfile_provider_5.json | 2 +- ...testObject_ProviderProfile_provider_6.json | 2 +- ...testObject_ProviderProfile_provider_7.json | 2 +- ...testObject_ProviderProfile_provider_8.json | 2 +- ...testObject_ProviderProfile_provider_9.json | 2 +- .../testObject_Provider_provider_1.json | 2 +- .../testObject_Provider_provider_10.json | 2 +- .../testObject_Provider_provider_11.json | 2 +- .../testObject_Provider_provider_12.json | 2 +- .../testObject_Provider_provider_13.json | 2 +- .../testObject_Provider_provider_14.json | 2 +- .../testObject_Provider_provider_15.json | 2 +- .../testObject_Provider_provider_16.json | 2 +- .../testObject_Provider_provider_17.json | 2 +- .../testObject_Provider_provider_18.json | 2 +- .../testObject_Provider_provider_19.json | 2 +- .../testObject_Provider_provider_2.json | 2 +- .../testObject_Provider_provider_20.json | 2 +- .../testObject_Provider_provider_3.json | 2 +- .../testObject_Provider_provider_4.json | 2 +- .../testObject_Provider_provider_5.json | 2 +- .../testObject_Provider_provider_6.json | 2 +- .../testObject_Provider_provider_7.json | 2 +- .../testObject_Provider_provider_8.json | 2 +- .../testObject_Provider_provider_9.json | 2 +- ...ject_QualifiedUserClientPrekeyMapV4_1.json | 5 +- ...ject_QualifiedUserClientPrekeyMapV4_2.json | 15 +- ...ject_QualifiedUserClientPrekeyMapV4_3.json | 23 +- ...ect_SearchResult_20TeamContact_user_1.json | 4 +- ...ct_SearchResult_20TeamContact_user_11.json | 12 +- ...ct_SearchResult_20TeamContact_user_12.json | 2 +- ...ct_SearchResult_20TeamContact_user_13.json | 6 +- ...ct_SearchResult_20TeamContact_user_15.json | 2 +- ...ct_SearchResult_20TeamContact_user_16.json | 6 +- ...ct_SearchResult_20TeamContact_user_18.json | 2 +- ...ect_SearchResult_20TeamContact_user_4.json | 6 +- ...ect_SearchResult_20TeamContact_user_5.json | 2 +- ...ect_SearchResult_20TeamContact_user_6.json | 26 +-- ...ect_SearchResult_20TeamContact_user_7.json | 10 +- ...ect_SearchResult_20TeamContact_user_8.json | 8 +- ...ect_SearchResult_20TeamContact_user_9.json | 2 +- .../golden/testObject_SelfProfile_user_1.json | 2 +- .../testObject_SendActivationCode_1.json | 2 +- .../testObject_SendActivationCode_2.json | 2 +- .../golden/testObject_TeamContact_user_1.json | 2 +- .../testObject_TeamContact_user_11.json | 2 +- .../testObject_TeamContact_user_12.json | 2 +- .../testObject_TeamContact_user_13.json | 2 +- .../testObject_TeamContact_user_14.json | 2 +- .../testObject_TeamContact_user_15.json | 4 +- .../testObject_TeamContact_user_16.json | 4 +- .../testObject_TeamContact_user_17.json | 2 +- .../testObject_TeamContact_user_18.json | 2 +- .../testObject_TeamContact_user_19.json | 2 +- .../golden/testObject_TeamContact_user_2.json | 2 +- .../testObject_TeamContact_user_20.json | 4 +- .../golden/testObject_TeamContact_user_3.json | 2 +- .../golden/testObject_TeamContact_user_4.json | 4 +- .../golden/testObject_TeamContact_user_7.json | 4 +- .../golden/testObject_TeamContact_user_8.json | 2 +- .../golden/testObject_TeamContact_user_9.json | 2 +- .../golden/testObject_UserProfile_user_2.json | 2 +- .../test/golden/testObject_User_user_2.json | 2 +- .../test/golden/testObject_User_user_3.json | 2 +- .../test/golden/testObject_User_user_4.json | 2 +- .../test/golden/testObject_User_user_5.json | 2 +- .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 2 +- .../Test/Wire/API/Roundtrip/ByteString.hs | 2 +- libs/wire-api/test/unit/Test/Wire/API/User.hs | 2 +- .../test/unit/Test/Wire/API/User/Auth.hs | 2 +- libs/wire-api/wire-api.cabal | 3 +- .../src/Wire/EmailSubsystem.hs | 20 +- .../src/Wire/EmailSubsystem/Interpreter.hs | 36 +-- .../src/Wire/EmailSubsystem/Template.hs | 14 +- libs/wire-subsystems/src/Wire/StoredUser.hs | 4 +- libs/wire-subsystems/src/Wire/UserKeyStore.hs | 15 +- .../wire-subsystems/src/Wire/UserSubsystem.hs | 6 +- .../src/Wire/UserSubsystem/Interpreter.hs | 7 +- .../src/Wire/VerificationCode.hs | 2 +- .../src/Wire/VerificationCodeGen.hs | 8 +- .../Wire/VerificationCodeStore/Cassandra.hs | 6 +- .../InterpreterSpec.hs | 11 +- .../Wire/MockInterpreters/EmailSubsystem.hs | 4 +- .../Wire/UserSubsystem/InterpreterSpec.hs | 4 +- services/brig/brig.cabal | 1 - services/brig/default.nix | 2 - services/brig/src/Brig/API/Client.hs | 1 - services/brig/src/Brig/API/Handler.hs | 8 +- services/brig/src/Brig/API/Internal.hs | 18 +- services/brig/src/Brig/API/Public.hs | 12 +- services/brig/src/Brig/API/Types.hs | 16 +- services/brig/src/Brig/API/User.hs | 69 ++---- services/brig/src/Brig/API/Util.hs | 2 +- services/brig/src/Brig/AWS/SesNotification.hs | 10 +- services/brig/src/Brig/AWS/Types.hs | 4 +- services/brig/src/Brig/App.hs | 4 +- services/brig/src/Brig/Data/Activation.hs | 8 +- services/brig/src/Brig/Data/User.hs | 18 +- services/brig/src/Brig/Effects/JwtTools.hs | 1 - services/brig/src/Brig/IO/Journal.hs | 6 +- services/brig/src/Brig/Options.hs | 4 +- services/brig/src/Brig/Provider/API.hs | 19 +- services/brig/src/Brig/Provider/DB.hs | 8 +- services/brig/src/Brig/Provider/Email.hs | 12 +- services/brig/src/Brig/Provider/Template.hs | 6 +- services/brig/src/Brig/Team/API.hs | 23 +- services/brig/src/Brig/Team/DB.hs | 26 +-- services/brig/src/Brig/Team/Email.hs | 12 +- services/brig/src/Brig/Team/Template.hs | 6 +- services/brig/src/Brig/User/Auth.hs | 11 +- services/brig/src/Brig/User/Search/Index.hs | 8 +- .../brig/src/Brig/User/Search/Index/Types.hs | 8 +- .../brig/test/integration/API/Provider.hs | 8 +- .../brig/test/integration/API/Settings.hs | 6 +- services/brig/test/integration/API/Team.hs | 12 +- .../brig/test/integration/API/Team/Util.hs | 16 +- .../test/integration/API/TeamUserSearch.hs | 2 +- .../brig/test/integration/API/User/Account.hs | 64 ++--- .../brig/test/integration/API/User/Auth.hs | 16 +- .../brig/test/integration/API/User/Util.hs | 16 +- .../integration/API/UserPendingActivation.hs | 26 +-- services/brig/test/integration/Util.hs | 30 +-- services/brig/test/integration/Util/AWS.hs | 8 +- .../unit/Test/Brig/User/Search/Index/Types.hs | 2 +- .../test/integration/Test/Federator/Util.hs | 12 +- .../test/resources/unit/localhost-dot-key.pem | 50 ++-- .../test/resources/unit/localhost-dot.pem | 34 +-- .../test/resources/unit/localhost-key.pem | 50 ++-- .../unit/localhost.example.com-key.pem | 50 ++-- .../resources/unit/localhost.example.com.pem | 34 +-- .../test/resources/unit/localhost.pem | 34 +-- .../multidomain-federator.example.com-key.pem | 50 ++-- .../multidomain-federator.example.com.pem | 34 +-- .../unit/second-federator.example.com-key.pem | 50 ++-- .../unit/second-federator.example.com.pem | 34 +-- .../test/resources/unit/unit-ca-key.pem | 50 ++-- .../federator/test/resources/unit/unit-ca.pem | 34 +-- services/galley/default.nix | 2 - services/galley/galley.cabal | 1 - .../galley/src/Galley/Cassandra/Instances.hs | 1 - services/galley/test/integration/API/Util.hs | 6 +- services/spar/src/Spar/API.hs | 2 +- services/spar/src/Spar/App.hs | 6 +- services/spar/src/Spar/Intra/Brig.hs | 6 +- services/spar/src/Spar/Intra/BrigApp.hs | 8 +- services/spar/src/Spar/Scim/User.hs | 218 ++++++++---------- services/spar/src/Spar/Sem/BrigAccess.hs | 6 +- .../spar/src/Spar/Sem/ScimExternalIdStore.hs | 8 +- .../Spar/Sem/ScimExternalIdStore/Cassandra.hs | 6 +- .../test-integration/Test/Spar/APISpec.hs | 4 +- .../Test/Spar/Scim/AuthSpec.hs | 2 +- .../Test/Spar/Scim/UserSpec.hs | 31 ++- .../spar/test-integration/Util/Activation.hs | 2 +- services/spar/test-integration/Util/Core.hs | 26 ++- services/spar/test-integration/Util/Email.hs | 8 +- .../spar/test-integration/Util/Invitation.hs | 8 +- services/spar/test-integration/Util/Scim.hs | 29 +-- services/spar/test/Test/Spar/Scim/UserSpec.hs | 2 +- services/spar/test/Test/Spar/ScimSpec.hs | 4 +- .../inconsistencies/src/DanglingUserKeys.hs | 6 +- .../db/inconsistencies/src/EmailLessUsers.hs | 10 +- tools/db/phone-users/src/PhoneUsers/Types.hs | 2 +- tools/stern/src/Stern/API.hs | 18 +- tools/stern/src/Stern/API/Routes.hs | 18 +- tools/stern/src/Stern/Intra.hs | 14 +- tools/stern/test/integration/API.hs | 20 +- tools/stern/test/integration/Util.hs | 10 +- 471 files changed, 1804 insertions(+), 2348 deletions(-) create mode 100644 changelog.d/5-internal/refactor-email create mode 100644 libs/wire-api/src/Wire/API/User/EmailAddress.hs delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_10.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_11.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_12.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_13.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_14.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_15.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_16.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_17.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_18.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_19.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_2.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_20.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_3.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_4.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_5.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_6.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_7.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_8.json delete mode 100644 libs/wire-api/test/golden/testObject_EmailUpdate_provider_9.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_10.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_11.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_12.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_13.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_14.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_15.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_16.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_17.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_18.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_19.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_2.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_20.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_3.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_4.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_5.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_6.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_7.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_8.json delete mode 100644 libs/wire-api/test/golden/testObject_Email_user_9.json delete mode 100644 libs/wire-api/test/golden/testObject_NewPasswordReset_user_20.json diff --git a/changelog.d/5-internal/refactor-email b/changelog.d/5-internal/refactor-email new file mode 100644 index 00000000000..9e2e91c7804 --- /dev/null +++ b/changelog.d/5-internal/refactor-email @@ -0,0 +1 @@ +Factored out our Email type in favour of EmailAddress from email-validate. diff --git a/libs/brig-types/test/unit/Test/Brig/Types/User.hs b/libs/brig-types/test/unit/Test/Brig/Types/User.hs index ee966465ad2..6ca50562cb4 100644 --- a/libs/brig-types/test/unit/Test/Brig/Types/User.hs +++ b/libs/brig-types/test/unit/Test/Brig/Types/User.hs @@ -68,4 +68,4 @@ testCaseUserAccount = testCase "UserAcccount" $ do json1 = "{\"accent_id\":1,\"assets\":[],\"deleted\":true,\"email\":\"foo@example.com\",\"expires_at\":\"1864-05-09T17:20:22.192Z\",\"handle\":\"-ve\",\"id\":\"00000000-0000-0001-0000-000100000000\",\"locale\":\"lu\",\"managed_by\":\"wire\",\"name\":\"bla\",\"picture\":[],\"qualified_id\":{\"domain\":\"4-o60.j7-i\",\"id\":\"00000000-0000-0001-0000-000100000000\"},\"service\":{\"id\":\"00000000-0000-0001-0000-000000000001\",\"provider\":\"00000001-0000-0001-0000-000000000001\"},\"status\":\"suspended\",\"supported_protocols\":[\"proteus\"],\"team\":\"00000000-0000-0001-0000-000100000001\"}" json2 :: LByteString - json2 = "{\"accent_id\":0,\"assets\":[{\"key\":\"3-4-00000000-0000-0001-0000-000000000000\",\"size\":\"preview\",\"type\":\"image\"}],\"email\":\"@\",\"expires_at\":\"1864-05-10T22:45:44.823Z\",\"handle\":\"b8m\",\"id\":\"00000000-0000-0000-0000-000000000001\",\"locale\":\"tk-KZ\",\"managed_by\":\"wire\",\"name\":\"name2\",\"picture\":[],\"qualified_id\":{\"domain\":\"1-8wq0.b22k1.w5\",\"id\":\"00000000-0000-0000-0000-000000000001\"},\"service\":{\"id\":\"00000000-0000-0001-0000-000000000001\",\"provider\":\"00000001-0000-0001-0000-000100000000\"},\"status\":\"pending-invitation\",\"supported_protocols\":[\"proteus\"],\"team\":\"00000000-0000-0001-0000-000000000001\"}" + json2 = "{\"accent_id\":0,\"assets\":[{\"key\":\"3-4-00000000-0000-0001-0000-000000000000\",\"size\":\"preview\",\"type\":\"image\"}],\"email\":\"a@b\",\"expires_at\":\"1864-05-10T22:45:44.823Z\",\"handle\":\"b8m\",\"id\":\"00000000-0000-0000-0000-000000000001\",\"locale\":\"tk-KZ\",\"managed_by\":\"wire\",\"name\":\"name2\",\"picture\":[],\"qualified_id\":{\"domain\":\"1-8wq0.b22k1.w5\",\"id\":\"00000000-0000-0000-0000-000000000001\"},\"service\":{\"id\":\"00000000-0000-0001-0000-000000000001\",\"provider\":\"00000001-0000-0001-0000-000100000000\"},\"status\":\"pending-invitation\",\"supported_protocols\":[\"proteus\"],\"team\":\"00000000-0000-0001-0000-000000000001\"}" diff --git a/libs/hscim/server/Main.hs b/libs/hscim/server/Main.hs index cfb31664ce0..3d21696c0e1 100644 --- a/libs/hscim/server/Main.hs +++ b/libs/hscim/server/Main.hs @@ -68,7 +68,7 @@ mkUserDB = do E.value = maybe (error "couldn't parse email") - EmailAddress2 + EmailAddress (emailAddress "elton@wire.com"), E.primary = Nothing } diff --git a/libs/hscim/src/Web/Scim/Schema/User/Email.hs b/libs/hscim/src/Web/Scim/Schema/User/Email.hs index 2b0364bcb7d..664a91550b7 100644 --- a/libs/hscim/src/Web/Scim/Schema/User/Email.hs +++ b/libs/hscim/src/Web/Scim/Schema/User/Email.hs @@ -21,24 +21,24 @@ import Data.Aeson import Data.Text hiding (dropWhile) import Data.Text.Encoding (decodeUtf8, encodeUtf8) import GHC.Generics (Generic) -import Text.Email.Validate -import Web.Scim.Schema.Common +import qualified Text.Email.Validate as Email +import Web.Scim.Schema.Common hiding (value) -newtype EmailAddress2 = EmailAddress2 - {unEmailAddress :: EmailAddress} +newtype EmailAddress = EmailAddress + {unEmailAddress :: Email.EmailAddress} deriving (Show, Eq) -instance FromJSON EmailAddress2 where - parseJSON = withText "Email" $ \e -> case emailAddress (encodeUtf8 e) of +instance FromJSON EmailAddress where + parseJSON = withText "Email" $ \e -> case Email.emailAddress (encodeUtf8 e) of Nothing -> fail "Invalid email" - Just some -> pure $ EmailAddress2 some + Just some -> pure $ EmailAddress some -instance ToJSON EmailAddress2 where - toJSON (EmailAddress2 e) = String $ decodeUtf8 . toByteString $ e +instance ToJSON EmailAddress where + toJSON (EmailAddress e) = String $ decodeUtf8 . Email.toByteString $ e data Email = Email - { typ :: Maybe Text, - value :: EmailAddress2, + { typ :: Maybe Text, -- Work, private, and so on + value :: EmailAddress, primary :: Maybe ScimBool } deriving (Show, Eq, Generic) @@ -48,3 +48,6 @@ instance FromJSON Email where instance ToJSON Email where toJSON = genericToJSON serializeOptions + +emailToEmailAddress :: Email -> Email.EmailAddress +emailToEmailAddress = unEmailAddress . value diff --git a/libs/hscim/test/Test/Schema/UserSpec.hs b/libs/hscim/test/Test/Schema/UserSpec.hs index 14b7b2ed8fb..8dcaa9d50ed 100644 --- a/libs/hscim/test/Test/Schema/UserSpec.hs +++ b/libs/hscim/test/Test/Schema/UserSpec.hs @@ -251,7 +251,7 @@ completeUser = Email.value = maybe (error "couldn't parse email") - EmailAddress2 + EmailAddress (emailAddress "user@example.com"), Email.primary = Nothing } diff --git a/libs/imports/default.nix b/libs/imports/default.nix index 728fca8f3b5..86157dd0c2a 100644 --- a/libs/imports/default.nix +++ b/libs/imports/default.nix @@ -7,6 +7,7 @@ , bytestring , containers , deepseq +, either , extra , gitignoreSource , lib @@ -26,6 +27,7 @@ mkDerivation { bytestring containers deepseq + either extra mtl text diff --git a/libs/imports/imports.cabal b/libs/imports/imports.cabal index a1ddc13d9bb..d2c9bb24cc3 100644 --- a/libs/imports/imports.cabal +++ b/libs/imports/imports.cabal @@ -73,6 +73,7 @@ library , bytestring , containers , deepseq + , either , extra , mtl , text diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index 9f283614362..e2ddf387e25 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -37,6 +37,7 @@ module Imports module Data.Monoid, module Data.Maybe, module Data.Either, + module Data.Either.Combinators, module Data.Foldable, module Data.Traversable, module Data.Tuple, @@ -110,6 +111,7 @@ module Imports -- * Extra Helpers whenM, unlessM, + catMaybesToList, -- * Functor (<$$>), @@ -147,6 +149,7 @@ import Data.ByteString (ByteString) import Data.ByteString.Lazy qualified import Data.Char import Data.Either +import Data.Either.Combinators hiding (fromLeft, fromRight, isLeft, isRight) import Data.Foldable import Data.Function import Data.Functor @@ -382,3 +385,6 @@ infix 4 <$$> (<$$$>) = fmap . fmap . fmap infix 4 <$$$> + +catMaybesToList :: Maybe (Maybe a) -> [a] +catMaybesToList = catMaybes . maybeToList diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index ce55894b212..8f29e1c12b5 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -32,7 +32,6 @@ , data-default , deriving-aeson , deriving-swagger2 -, either , email-validate , errors , extended @@ -143,7 +142,6 @@ mkDerivation { data-default deriving-aeson deriving-swagger2 - either email-validate errors extended @@ -228,7 +226,6 @@ mkDerivation { containers crypton currency-codes - either filepath hex hspec diff --git a/libs/wire-api/src/Wire/API/Allowlists.hs b/libs/wire-api/src/Wire/API/Allowlists.hs index 244a5e8cb85..ef986a2d5a0 100644 --- a/libs/wire-api/src/Wire/API/Allowlists.hs +++ b/libs/wire-api/src/Wire/API/Allowlists.hs @@ -25,6 +25,7 @@ module Wire.API.Allowlists where import Data.Aeson +import Data.Text.Encoding (decodeUtf8) import Imports import Wire.API.User.Identity @@ -36,6 +37,6 @@ instance FromJSON AllowlistEmailDomains -- | Consult the whitelist settings in brig's config file and verify that the provided -- email address is whitelisted. -verify :: Maybe AllowlistEmailDomains -> Email -> Bool -verify (Just (AllowlistEmailDomains allowed)) email = emailDomain email `elem` allowed +verify :: Maybe AllowlistEmailDomains -> EmailAddress -> Bool +verify (Just (AllowlistEmailDomains allowed)) email = (decodeUtf8 . domainPart $ email) `elem` allowed verify Nothing (_) = True diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 4861631b00a..97c8d0bc223 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -26,7 +26,6 @@ import Data.Aeson.Types qualified as A import Data.ByteArray (convert) import Data.ByteString.Conversion import Data.ByteString.Lazy (fromStrict, toStrict) -import Data.Either.Combinators (mapLeft) import Data.HashMap.Strict qualified as HM import Data.Id as Id import Data.Json.Util diff --git a/libs/wire-api/src/Wire/API/Provider.hs b/libs/wire-api/src/Wire/API/Provider.hs index 1fc5c34c114..8923fc6e5ed 100644 --- a/libs/wire-api/src/Wire/API/Provider.hs +++ b/libs/wire-api/src/Wire/API/Provider.hs @@ -61,7 +61,7 @@ import Imports import Wire.API.Conversation.Code as Code import Wire.API.Provider.Service (ServiceToken (..)) import Wire.API.Provider.Service.Tag (ServiceTag (..)) -import Wire.API.User.Identity (Email) +import Wire.API.User.EmailAddress import Wire.API.User.Profile (Name) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -72,7 +72,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) data Provider = Provider { providerId :: ProviderId, providerName :: Name, - providerEmail :: Email, + providerEmail :: EmailAddress, providerUrl :: HttpsUrl, providerDescr :: Text } @@ -103,7 +103,7 @@ newtype ProviderProfile = ProviderProfile Provider -- | Input data for registering a new provider. data NewProvider = NewProvider { newProviderName :: Name, - newProviderEmail :: Email, + newProviderEmail :: EmailAddress, newProviderUrl :: HttpsUrl, newProviderDescr :: Range 1 1024 Text, -- | If none provided, a password is generated. @@ -168,7 +168,7 @@ instance ToSchema UpdateProvider where -- | Successful response upon activating an email address (or possibly phone -- number in the future) of a provider. newtype ProviderActivationResponse = ProviderActivationResponse - {activatedProviderIdentity :: Email} + {activatedProviderIdentity :: EmailAddress} deriving stock (Eq, Show) deriving newtype (Arbitrary) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema ProviderActivationResponse @@ -184,7 +184,7 @@ instance ToSchema ProviderActivationResponse where -- | Input data for a provider login request. data ProviderLogin = ProviderLogin - { providerLoginEmail :: Email, + { providerLoginEmail :: EmailAddress, providerLoginPassword :: PlainTextPassword6 } deriving stock (Eq, Show, Generic) @@ -218,7 +218,7 @@ instance ToSchema DeleteProvider where -- Password Change/Reset -- | The payload for initiating a password reset. -newtype PasswordReset = PasswordReset {email :: Email} +newtype PasswordReset = PasswordReset {email :: EmailAddress} deriving stock (Eq, Show) deriving newtype (Arbitrary) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema PasswordReset @@ -264,7 +264,7 @@ instance ToSchema PasswordChange where <*> newPassword .= field "new_password" schema -- | The payload for updating an email address -newtype EmailUpdate = EmailUpdate {email :: Email} +newtype EmailUpdate = EmailUpdate {email :: EmailAddress} deriving stock (Eq, Show, Generic) deriving newtype (Arbitrary) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema EmailUpdate 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 6c14cbd6916..1c583784bbb 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -241,7 +241,7 @@ type AccountAPI = ( "users" :> QueryParam' [Optional, Strict] "ids" (CommaSeparatedList UserId) :> QueryParam' [Optional, Strict] "handles" (CommaSeparatedList Handle) - :> QueryParam' [Optional, Strict] "email" (CommaSeparatedList Email) -- don't rename to `emails`, for backwards compat! + :> QueryParam' [Optional, Strict] "email" (CommaSeparatedList EmailAddress) -- don't rename to `emails`, for backwards compat! :> QueryParam' [ Optional, Strict, @@ -262,14 +262,14 @@ type AccountAPI = "iGetUserActivationCode" ( "users" :> "activation-code" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Get '[Servant.JSON] GetActivationCodeResp ) :<|> Named "iGetUserPasswordResetCode" ( "users" :> "password-reset-code" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Get '[Servant.JSON] GetPasswordResetCodeResp ) :<|> Named @@ -277,14 +277,14 @@ type AccountAPI = ( Summary "This endpoint can lead to the following events being sent: UserIdentityRemoved event to target user" :> "users" :> "revoke-identity" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Post '[Servant.JSON] NoContent ) :<|> Named "iHeadBlacklist" ( "users" :> "blacklist" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> MultiVerb 'GET '[Servant.JSON] @@ -297,14 +297,14 @@ type AccountAPI = "iDeleteBlacklist" ( "users" :> "blacklist" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Delete '[Servant.JSON] NoContent ) :<|> Named "iPostBlacklist" ( "users" :> "blacklist" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Post '[Servant.JSON] NoContent ) :<|> Named @@ -534,7 +534,7 @@ type InvitationByEmail = ( "teams" :> "invitations" :> "by-email" - :> QueryParam' [Required, Strict] "email" Email + :> QueryParam' [Required, Strict] "email" EmailAddress :> Get '[Servant.JSON] Invitation ) @@ -738,7 +738,7 @@ type ProviderAPI = ( Summary "Retrieve activation code via api instead of email (for testing only)" :> "provider" :> "activation-code" - :> QueryParam' '[Required, Strict] "email" Email + :> QueryParam' '[Required, Strict] "email" EmailAddress :> MultiVerb1 'GET '[JSON] (Respond 200 "" Code.KeyValuePair) ) ) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs index 38bb517cb58..fca44780100 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs @@ -43,7 +43,7 @@ import Imports hiding (head) import Test.QuickCheck (Arbitrary) import Wire.API.Connection (Relation) import Wire.API.Team.Member (NewListType) -import Wire.API.User.Identity (Email, Phone) +import Wire.API.User.Identity (EmailAddress, Phone) import Wire.API.User.Profile (Name) import Wire.Arbitrary (GenericUniform (..)) @@ -62,7 +62,7 @@ data EJPDResponseItemRoot = EJPDResponseItemRoot ejpdResponseRootTeamId :: Maybe TeamId, ejpdResponseRootName :: Name, ejpdResponseRootHandle :: Maybe Handle, - ejpdResponseRootEmail :: Maybe Email, + ejpdResponseRootEmail :: Maybe EmailAddress, ejpdResponseRootPhone :: Maybe Phone, ejpdResponseRootPushTokens :: Set Text, -- 'Wire.API.Push.V2.Token.Token', but that would produce an orphan instance. ejpdResponseRootContacts :: Maybe (Set EJPDContact), @@ -78,7 +78,7 @@ data EJPDResponseItemLeaf = EJPDResponseItemLeaf ejpdResponseLeafTeamId :: Maybe TeamId, ejpdResponseLeafName :: Name, ejpdResponseLeafHandle :: Maybe Handle, - ejpdResponseLeafEmail :: Maybe Email, + ejpdResponseLeafEmail :: Maybe EmailAddress, ejpdResponseLeafPhone :: Maybe Phone, ejpdResponseLeafPushTokens :: Set Text, -- 'Wire.API.Push.V2.Token.Token', but that would produce an orphan instance. ejpdResponseLeafConversations :: Maybe (Set EJPDConvInfo), diff --git a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs index e14c63c1332..8188aa20a1f 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs @@ -25,7 +25,6 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Attoparsec.ByteString qualified as AB import Data.ByteString qualified as BS import Data.ByteString.Base64.URL qualified as Base64Url -import Data.Either.Combinators (mapLeft) import Data.OpenApi qualified as S import Data.Proxy import Data.Schema diff --git a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs index bb972553319..2dfeb16685a 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs @@ -55,7 +55,6 @@ import Control.Lens hiding (Context, (<|)) import Data.ByteString.Builder import Data.ByteString.Lazy qualified as LBS import Data.CaseInsensitive qualified as CI -import Data.Either.Combinators (leftToMaybe) import Data.HashMap.Strict.InsOrd (InsOrdHashMap, unionWith) import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Kind 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 1b77cd8b1c1..29f05a6b708 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1637,7 +1637,7 @@ type TeamsAPI = :> "teams" :> "invitations" :> "by-email" - :> QueryParam' '[Required, Strict, Description "Email address"] "email" Email + :> QueryParam' '[Required, Strict, Description "Email address"] "email" EmailAddress :> MultiVerb 'HEAD '[JSON] diff --git a/libs/wire-api/src/Wire/API/Team/Export.hs b/libs/wire-api/src/Wire/API/Team/Export.hs index 636950095bf..7a37047c307 100644 --- a/libs/wire-api/src/Wire/API/Team/Export.hs +++ b/libs/wire-api/src/Wire/API/Team/Export.hs @@ -28,18 +28,18 @@ import Data.Json.Util (UTCTimeMillis) import Data.Misc (HttpsUrl) import Data.Vector (fromList) import Imports -import Test.QuickCheck (Arbitrary) +import Test.QuickCheck import Wire.API.Team.Role (Role) import Wire.API.User (Name) -import Wire.API.User.Identity (Email) +import Wire.API.User.Identity (EmailAddress) import Wire.API.User.Profile (ManagedBy) import Wire.API.User.RichInfo (RichInfo) -import Wire.Arbitrary (GenericUniform (GenericUniform)) +import Wire.Arbitrary data TeamExportUser = TeamExportUser { tExportDisplayName :: Name, tExportHandle :: Maybe Handle, - tExportEmail :: Maybe Email, + tExportEmail :: Maybe EmailAddress, tExportRole :: Maybe Role, tExportCreatedOn :: Maybe UTCTimeMillis, tExportInvitedBy :: Maybe Handle, diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index e712dc520e1..b5c0d1a8096 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -44,7 +44,7 @@ import Wire.API.Error.Brig import Wire.API.Locale (Locale) import Wire.API.Routes.MultiVerb import Wire.API.Team.Role (Role, defaultRole) -import Wire.API.User.Identity (Email) +import Wire.API.User.Identity (EmailAddress) import Wire.API.User.Profile (Name) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -55,7 +55,7 @@ data InvitationRequest = InvitationRequest { locale :: Maybe Locale, role :: Maybe Role, inviteeName :: Maybe Name, - inviteeEmail :: Email + inviteeEmail :: EmailAddress } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform InvitationRequest) @@ -85,7 +85,7 @@ data Invitation = Invitation -- | this is always 'Just' for new invitations, but for -- migration it is allowed to be 'Nothing'. inCreatedBy :: Maybe UserId, - inInviteeEmail :: Email, + inInviteeEmail :: EmailAddress, inInviteeName :: Maybe Name, inInviteeUrl :: Maybe (URIRef Absolute) } diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index d3692bb5900..4875538165b 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -210,7 +210,7 @@ import Wire.API.Team.Member qualified as TeamMember import Wire.API.Team.Role import Wire.API.User.Activation (ActivationCode, ActivationKey) import Wire.API.User.Auth (CookieLabel) -import Wire.API.User.Identity +import Wire.API.User.Identity hiding (toByteString) import Wire.API.User.Password import Wire.API.User.Profile import Wire.API.User.RichInfo @@ -489,7 +489,7 @@ data UserProfile = UserProfile profileHandle :: Maybe Handle, profileExpire :: Maybe UTCTimeMillis, profileTeam :: Maybe TeamId, - profileEmail :: Maybe Email, + profileEmail :: Maybe EmailAddress, profileLegalholdStatus :: UserLegalHoldStatus, profileSupportedProtocols :: Set BaseProtocolTag } @@ -631,7 +631,7 @@ userObjectSchema = .= (fromMaybe ManagedByWire <$> optField "managed_by" schema) <*> userSupportedProtocols .= supportedProtocolsObjectSchema -userEmail :: User -> Maybe Email +userEmail :: User -> Maybe EmailAddress userEmail = emailIdentity <=< userIdentity userSSOId :: User -> Maybe UserSSOId @@ -689,7 +689,7 @@ instance FromJSON (EmailVisibility ()) where "visible_to_self" -> pure EmailVisibleToSelf _ -> fail "unexpected value for EmailVisibility settings" -mkUserProfileWithEmail :: Maybe Email -> User -> UserLegalHoldStatus -> UserProfile +mkUserProfileWithEmail :: Maybe EmailAddress -> User -> UserLegalHoldStatus -> UserProfile mkUserProfileWithEmail memail u legalHoldStatus = -- This profile would be visible to any other user. When a new field is -- added, please make sure it is OK for other users to have access to it. @@ -850,9 +850,9 @@ instance (res ~ RegisterInternalResponses) => AsUnion res (Either RegisterError urefToExternalId :: SAML.UserRef -> Maybe Text urefToExternalId = fmap CI.original . SAML.shortShowNameID . view SAML.uidSubject -urefToEmail :: SAML.UserRef -> Maybe Email +urefToEmail :: SAML.UserRef -> Maybe EmailAddress urefToEmail uref = case uref ^. SAML.uidSubject . SAML.nameID of - SAML.UNameIDEmail email -> parseEmail . SAMLEmail.render . CI.original $ email + SAML.UNameIDEmail email -> emailAddressText . SAMLEmail.render . CI.original $ email _ -> Nothing urefToExternalIdUnsafe :: SAML.UserRef -> Text @@ -1007,7 +1007,7 @@ type ExpiresIn = Range 1 604800 Integer data NewUserRaw = NewUserRaw { newUserRawDisplayName :: Name, newUserRawUUID :: Maybe UUID, - newUserRawEmail :: Maybe Email, + newUserRawEmail :: Maybe EmailAddress, newUserRawSSOId :: Maybe UserSSOId, -- | DEPRECATED newUserRawPict :: Maybe Pict, @@ -1173,7 +1173,7 @@ newUserTeam nu = case newUserOrigin nu of Just (NewUserOriginTeamUser tu) -> Just tu _ -> Nothing -newUserEmail :: NewUser -> Maybe Email +newUserEmail :: NewUser -> Maybe EmailAddress newUserEmail = emailIdentity <=< newUserIdentity newUserSSOId :: NewUser -> Maybe UserSSOId @@ -1448,7 +1448,7 @@ instance ToSchema LocaleUpdate where <$> luLocale .= field "locale" schema -newtype EmailUpdate = EmailUpdate {euEmail :: Email} +newtype EmailUpdate = EmailUpdate {euEmail :: EmailAddress} deriving stock (Eq, Show, Generic) deriving newtype (Arbitrary) deriving (S.ToSchema) via (Schema EmailUpdate) @@ -1810,7 +1810,7 @@ data NewUserScimInvitation = NewUserScimInvitation newUserScimInvUserId :: UserId, newUserScimInvLocale :: Maybe Locale, newUserScimInvName :: Name, - newUserScimInvEmail :: Email, + newUserScimInvEmail :: EmailAddress, newUserScimInvRole :: Role } deriving (Eq, Show, Generic) @@ -1881,7 +1881,7 @@ instance ToHttpApiData VerificationAction where data SendVerificationCode = SendVerificationCode { svcAction :: VerificationAction, - svcEmail :: Email + svcEmail :: EmailAddress } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SendVerificationCode) diff --git a/libs/wire-api/src/Wire/API/User/Activation.hs b/libs/wire-api/src/Wire/API/User/Activation.hs index 6a294b407c6..5e347a54afe 100644 --- a/libs/wire-api/src/Wire/API/User/Activation.hs +++ b/libs/wire-api/src/Wire/API/User/Activation.hs @@ -59,7 +59,7 @@ data ActivationTarget = -- | An opaque key for some email awaiting activation. ActivateKey ActivationKey | -- | A known email address awaiting activation. - ActivateEmail Email + ActivateEmail EmailAddress deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform ActivationTarget) @@ -138,11 +138,11 @@ instance ToSchema Activate where \cookies or tokens on success but failures still count \ \towards the maximum failure count." - maybeActivationTargetObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe ActivationKey, Maybe Email) ActivationTarget + maybeActivationTargetObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe ActivationKey, Maybe EmailAddress) ActivationTarget maybeActivationTargetObjectSchema = withParser activationTargetTupleObjectSchema maybeActivationTargetTargetFromTuple where - activationTargetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe ActivationKey, Maybe Email) + activationTargetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe ActivationKey, Maybe EmailAddress) activationTargetTupleObjectSchema = (,) <$> fst .= maybe_ (optFieldWithDocModifier "key" keyDocs schema) @@ -151,13 +151,13 @@ instance ToSchema Activate where keyDocs = description ?~ "An opaque key to activate, as it was sent by the API." emailDocs = description ?~ "A known email address to activate." - maybeActivationTargetTargetFromTuple :: (Maybe ActivationKey, Maybe Email) -> Parser ActivationTarget + maybeActivationTargetTargetFromTuple :: (Maybe ActivationKey, Maybe EmailAddress) -> Parser ActivationTarget maybeActivationTargetTargetFromTuple = \case (Just key, _) -> pure $ ActivateKey key (_, Just email) -> pure $ ActivateEmail email _ -> fail "key or email must be present" - maybeActivationTargetToTuple :: ActivationTarget -> (Maybe ActivationKey, Maybe Email) + maybeActivationTargetToTuple :: ActivationTarget -> (Maybe ActivationKey, Maybe EmailAddress) maybeActivationTargetToTuple = \case ActivateKey key -> (Just key, Nothing) ActivateEmail email -> (Nothing, Just email) @@ -186,7 +186,7 @@ instance ToSchema ActivationResponse where -- | Payload for a request to (re-)send an activation code for an e-mail -- address. data SendActivationCode = SendActivationCode - { emailKey :: Email, + { emailKey :: EmailAddress, locale :: Maybe Locale } deriving stock (Eq, Show, Generic) diff --git a/libs/wire-api/src/Wire/API/User/Auth.hs b/libs/wire-api/src/Wire/API/User/Auth.hs index 806f9745c14..e395fece0e6 100644 --- a/libs/wire-api/src/Wire/API/User/Auth.hs +++ b/libs/wire-api/src/Wire/API/User/Auth.hs @@ -86,7 +86,7 @@ import Imports import Servant import Web.Cookie import Wire.API.Routes.MultiVerb -import Wire.API.User.Identity (Email, Phone) +import Wire.API.User.Identity (EmailAddress, Phone) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -------------------------------------------------------------------------------- @@ -94,7 +94,7 @@ import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -- | The login ID for client API versions v0..v5 data LoginId - = LoginByEmail Email + = LoginByEmail EmailAddress | LoginByHandle Handle deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform LoginId) @@ -109,16 +109,16 @@ loginObjectSchema :: ObjectSchema SwaggerDoc LoginId loginObjectSchema = fromLoginId .= tupleSchema `withParser` validate where - fromLoginId :: LoginId -> (Maybe Email, Maybe Handle) + fromLoginId :: LoginId -> (Maybe EmailAddress, Maybe Handle) fromLoginId = \case LoginByEmail e -> (Just e, Nothing) LoginByHandle h -> (Nothing, Just h) - tupleSchema :: ObjectSchema SwaggerDoc (Maybe Email, Maybe Handle) + tupleSchema :: ObjectSchema SwaggerDoc (Maybe EmailAddress, Maybe Handle) tupleSchema = (,) <$> fst .= maybe_ (optField "email" schema) <*> snd .= maybe_ (optField "handle" schema) - validate :: (Maybe Email, Maybe Handle) -> A.Parser LoginId + validate :: (Maybe EmailAddress, Maybe Handle) -> A.Parser LoginId validate (mEmail, mHandle) = maybe (fail "'email' or 'handle' required") pure $ (LoginByEmail <$> mEmail) <|> (LoginByHandle <$> mHandle) diff --git a/libs/wire-api/src/Wire/API/User/EmailAddress.hs b/libs/wire-api/src/Wire/API/User/EmailAddress.hs new file mode 100644 index 00000000000..5048230e48c --- /dev/null +++ b/libs/wire-api/src/Wire/API/User/EmailAddress.hs @@ -0,0 +1,110 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + +module Wire.API.User.EmailAddress + ( fromEmail, + emailAddress, + emailAddressText, + module Text.Email.Parser, + ) +where + +----- +-- This is where we declare orphan instances +----- + +import Cassandra.CQL qualified as C +import Data.ByteString.Conversion hiding (toByteString) +import Data.Data (Proxy (..)) +import Data.OpenApi hiding (Schema, ToSchema) +import Data.Schema +import Data.Text hiding (null) +import Data.Text.Encoding +import Data.Text.Encoding.Error +import Deriving.Aeson +import Imports +import Servant.API qualified as S +import Test.QuickCheck +import Text.Email.Parser +import Text.Email.Validate + +-------------------------------------------------------------------------------- +-- Email + +instance ToByteString EmailAddress where + builder = builder . fromEmail + +instance FromByteString EmailAddress where + parser = parser >>= maybe (fail "Invalid email") pure . emailAddress + +deriving via (Schema EmailAddress) instance ToJSON EmailAddress + +deriving via (Schema EmailAddress) instance FromJSON EmailAddress + +instance ToParamSchema EmailAddress where + toParamSchema _ = toParamSchema (Proxy @Text) + +instance ToSchema EmailAddress where + schema = + fromEmail + .= parsedText + "Email" + ( maybe + (Left "Invalid email. Expected '@'.") + pure + . emailAddressText + ) + +instance S.FromHttpApiData EmailAddress where + parseUrlPiece = maybe (Left "Invalid email") Right . fromByteString . encodeUtf8 + +instance S.ToHttpApiData EmailAddress where + toUrlPiece = decodeUtf8With lenientDecode . toByteString' + +instance Arbitrary EmailAddress where + -- By generating arbitrary Text and then encoding as bytestrings + -- we avoid the risk of generating invalid UTF-8 bytes. + arbitrary = arbitraryValidMail + +-- loc <- fromString <$> listOf1 arbitraryMailString +-- dom <- fromString <$> listOf1 arbitraryMailString +-- pure $ unsafeEmailAddress loc dom + +instance C.Cql EmailAddress where + ctype = C.Tagged C.TextColumn + + fromCql (C.CqlText t) = case emailAddressText t of + Just e -> pure e + Nothing -> Left "fromCql: Invalid email" + fromCql _ = Left "fromCql: email: CqlText expected" + + toCql = C.toCql . fromEmail + +fromEmail :: EmailAddress -> Text +fromEmail = decodeUtf8 . toByteString + +emailAddressText :: Text -> Maybe EmailAddress +emailAddressText = emailAddress . encodeUtf8 + +-- | Generates any Unicode character (but not a surrogate) +arbitraryValidMail :: Gen EmailAddress +arbitraryValidMail = do + loc <- arbitrary `suchThat` isValidLoc + dom <- arbitrary `suchThat` isValidDom + pure . fromJust $ emailAddress (fromString $ loc <> "@" <> dom) + where + notAt :: String -> Bool + notAt = notElem '@' + + notNull = not . null + + isValidLoc :: String -> Bool + isValidLoc x = + notNull x + && notAt x + && isValid (fromString (x <> "@mail.com")) + + isValidDom :: String -> Bool + isValidDom x = + notNull x + && notAt x + && isValid (fromString ("me@" <> x)) diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index a06c5beb64b..2929efa269b 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -30,17 +30,14 @@ module Wire.API.User.Identity maybeUserIdentityObjectSchema, maybeUserIdentityFromComponents, - -- * Email - Email (..), - fromEmail, - parseEmail, - validateEmail, - -- * Phone Phone (..), parsePhone, isValidPhone, + -- * Email + module Wire.API.User.EmailAddress, + -- * UserSSOId UserSSOId (..), emailFromSAML, @@ -59,7 +56,6 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Aeson.Types qualified as A import Data.Attoparsec.Text -import Data.Bifunctor (first) import Data.ByteString (fromStrict, toStrict) import Data.ByteString.Conversion import Data.ByteString.UTF8 qualified as UTF8 @@ -81,9 +77,11 @@ import Servant import Servant.API qualified as S import System.FilePath (()) import Test.QuickCheck qualified as QC -import Text.Email.Validate qualified as Email.V +import Text.Email.Parser import URI.ByteString qualified as URI import URI.ByteString.QQ (uri) +import Web.Scim.Schema.User.Email () +import Wire.API.User.EmailAddress import Wire.API.User.Profile (fromName, mkName) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) @@ -93,8 +91,8 @@ import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -- | The private unique user identity that is used for login and -- account recovery. data UserIdentity - = EmailIdentity Email - | SSOIdentity UserSSOId (Maybe Email) + = EmailIdentity EmailAddress + | SSOIdentity UserSSOId (Maybe EmailAddress) deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserIdentity) @@ -110,7 +108,7 @@ maybeUserIdentityObjectSchema :: ObjectSchema SwaggerDoc (Maybe UserIdentity) maybeUserIdentityObjectSchema = dimap maybeUserIdentityToComponents maybeUserIdentityFromComponents userIdentityComponentsObjectSchema -type UserIdentityComponents = (Maybe Email, Maybe UserSSOId) +type UserIdentityComponents = (Maybe EmailAddress, Maybe UserSSOId) userIdentityComponentsObjectSchema :: ObjectSchema SwaggerDoc UserIdentityComponents userIdentityComponentsObjectSchema = @@ -129,12 +127,12 @@ maybeUserIdentityToComponents Nothing = (Nothing, Nothing) maybeUserIdentityToComponents (Just (EmailIdentity email)) = (Just email, Nothing) maybeUserIdentityToComponents (Just (SSOIdentity ssoid m_email)) = (m_email, Just ssoid) -newIdentity :: Maybe Email -> Maybe UserSSOId -> Maybe UserIdentity +newIdentity :: Maybe EmailAddress -> Maybe UserSSOId -> Maybe UserIdentity newIdentity email (Just sso) = Just $! SSOIdentity sso email newIdentity (Just e) Nothing = Just $! EmailIdentity e newIdentity Nothing Nothing = Nothing -emailIdentity :: UserIdentity -> Maybe Email +emailIdentity :: UserIdentity -> Maybe EmailAddress emailIdentity (EmailIdentity email) = Just email emailIdentity (SSOIdentity _ (Just email)) = Just email emailIdentity (SSOIdentity _ _) = Nothing @@ -143,113 +141,6 @@ ssoIdentity :: UserIdentity -> Maybe UserSSOId ssoIdentity (SSOIdentity ssoid _) = Just ssoid ssoIdentity _ = Nothing --------------------------------------------------------------------------------- --- Email - --- FUTUREWORK: replace this type with 'EmailAddress' -data Email = Email - { emailLocal :: Text, - emailDomain :: Text - } - deriving stock (Eq, Ord, Generic) - deriving (FromJSON, ToJSON, S.ToSchema) via Schema Email - -instance ToParamSchema Email where - toParamSchema _ = toParamSchema (Proxy @Text) - -instance ToSchema Email where - schema = - fromEmail - .= parsedText - "Email" - ( maybe - (Left "Invalid email. Expected '@'.") - pure - . parseEmail - ) - -instance Show Email where - show = Text.unpack . fromEmail - -instance ToByteString Email where - builder = builder . fromEmail - -instance FromByteString Email where - parser = parser >>= maybe (fail "Invalid email") pure . parseEmail - -instance S.FromHttpApiData Email where - parseUrlPiece = maybe (Left "Invalid email") Right . fromByteString . encodeUtf8 - -instance S.ToHttpApiData Email where - toUrlPiece = decodeUtf8With lenientDecode . toByteString' - -instance Arbitrary Email where - arbitrary = do - localPart <- Text.filter (/= '@') <$> arbitrary - domain <- Text.filter (/= '@') <$> arbitrary - pure $ Email localPart domain - -instance C.Cql Email where - ctype = C.Tagged C.TextColumn - - fromCql (C.CqlText t) = case parseEmail t of - Just e -> pure e - Nothing -> Left "fromCql: Invalid email" - fromCql _ = Left "fromCql: email: CqlText expected" - - toCql = C.toCql . fromEmail - -fromEmail :: Email -> Text -fromEmail (Email loc dom) = loc <> "@" <> dom - --- | Parses an email address of the form @. -parseEmail :: Text -> Maybe Email -parseEmail t = case Text.split (== '@') t of - [localPart, domain] -> Just $! Email localPart domain - _ -> Nothing - --- | --- FUTUREWORK: --- --- * Enforce these constrains during parsing already or use a separate type, see --- [Parse, don't validate](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate). --- --- * Check for differences to validation of `Data.Domain.Domain` and decide whether to --- align/de-duplicate the two. --- --- * Drop dependency on email-validate? We do our own email domain validation anyways, --- is the dependency worth it just for validating the local part? -validateEmail :: Email -> Either String Email -validateEmail = - pure - . uncurry Email - <=< validateDomain - <=< validateExternalLib - <=< validateLength - . fromEmail - where - validateLength e - | len <= 100 = Right e - | otherwise = Left $ "length " <> show len <> " exceeds 100" - where - len = Text.length e - validateExternalLib e = do - email <- Email.V.validate $ encodeUtf8 e - l <- first show . decodeUtf8' $ Email.V.localPart email - d <- first show . decodeUtf8' $ Email.V.domainPart email - pure (l, d) - -- cf. https://en.wikipedia.org/wiki/Email_address#Domain - -- n.b. We do not allow IP address literals, comments or non-ASCII - -- characters, mostly because SES (and probably many other mail - -- systems) don't support that (yet?) either. - validateDomain (l, d) = parseOnly domain d - where - domain = (label *> many1 (char '.' *> label) *> endOfInput) $> (l, d) - label = - satisfy (inClass "a-zA-Z0-9") - *> count 61 (optional (satisfy (inClass "-a-zA-Z0-9"))) - *> optional (satisfy (inClass "a-zA-Z0-9")) - -------------------------------------------------------------------------------- -- Phone @@ -401,8 +292,8 @@ lenientlyParseSAMLNameID (Just txt) = do asemail = maybe (Left "not an email") - (fmap emailToSAMLNameID . validateEmail) - (parseEmail . LT.toStrict $ txt) + emailToSAMLNameID + (emailAddressText . LT.toStrict $ txt) astxt :: Either String SAML.NameID astxt = do @@ -417,15 +308,15 @@ lenientlyParseSAMLNameID (Just txt) = do (pure . Just) (hush asxml <|> hush asemail <|> hush astxt) -emailFromSAML :: (HasCallStack) => SAMLEmail.Email -> Email -emailFromSAML = fromJust . parseEmail . SAMLEmail.render +emailFromSAML :: SAMLEmail.Email -> EmailAddress +emailFromSAML = fromJust . emailAddressText . SAMLEmail.render -- | FUTUREWORK(fisx): if saml2-web-sso exported the 'NameID' constructor, we could make this -- function total without all that praying and hoping. -emailToSAMLNameID :: (HasCallStack) => Email -> SAML.NameID -emailToSAMLNameID = fromRight (error "impossible") . SAML.emailNameID . fromEmail +emailToSAMLNameID :: EmailAddress -> Either String SAML.NameID +emailToSAMLNameID = SAML.emailNameID . fromEmail -emailFromSAMLNameID :: (HasCallStack) => SAML.NameID -> Maybe Email +emailFromSAMLNameID :: SAML.NameID -> Maybe EmailAddress emailFromSAMLNameID nid = case nid ^. SAML.nameID of SAML.UNameIDEmail email -> Just . emailFromSAML . CI.original $ email _ -> Nothing diff --git a/libs/wire-api/src/Wire/API/User/Password.hs b/libs/wire-api/src/Wire/API/User/Password.hs index f3955f3cd4f..e377939406e 100644 --- a/libs/wire-api/src/Wire/API/User/Password.hs +++ b/libs/wire-api/src/Wire/API/User/Password.hs @@ -59,7 +59,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- | The payload for initiating a password reset. data NewPasswordReset - = NewPasswordReset Email + = NewPasswordReset EmailAddress | -- | Resetting via phone is not really supported anymore, but this is still -- here to support older versions of the endpoint. NewPasswordResetUnsupportedPhone @@ -75,7 +75,7 @@ instance ToSchema NewPasswordReset where objectDesc :: NamedSwaggerDoc -> NamedSwaggerDoc objectDesc = description ?~ "Data to initiate a password reset" - newPasswordResetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe Email, Maybe Text) + newPasswordResetTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe EmailAddress, Maybe Text) newPasswordResetTupleObjectSchema = (,) <$> fst .= maybe_ (optFieldWithDocModifier "email" phoneDocs schema) @@ -87,14 +87,14 @@ instance ToSchema NewPasswordReset where phoneDocs :: NamedSwaggerDoc -> NamedSwaggerDoc phoneDocs = description ?~ "Phone" - fromTuple :: (Maybe Email, Maybe a) -> Parser NewPasswordReset + fromTuple :: (Maybe EmailAddress, Maybe a) -> Parser NewPasswordReset fromTuple = \case (Just _, Just _) -> fail "Only one of 'email' or 'phone' allowed." (Just email, Nothing) -> pure $ NewPasswordReset email (Nothing, Just _) -> pure NewPasswordResetUnsupportedPhone (Nothing, Nothing) -> fail "One of 'email' or 'phone' required." - toTuple :: NewPasswordReset -> (Maybe Email, Maybe Text) + toTuple :: NewPasswordReset -> (Maybe EmailAddress, Maybe Text) toTuple = \case NewPasswordReset e -> (Just e, Nothing) NewPasswordResetUnsupportedPhone -> (Nothing, Just "") @@ -129,11 +129,11 @@ instance ToSchema CompletePasswordReset where pwDocs :: NamedSwaggerDoc -> NamedSwaggerDoc pwDocs = description ?~ "New password (6 - 1024 characters)" - maybePasswordResetIdentityObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe PasswordResetKey, Maybe Email, Maybe Phone) PasswordResetIdentity + maybePasswordResetIdentityObjectSchema :: ObjectSchemaP SwaggerDoc (Maybe PasswordResetKey, Maybe EmailAddress, Maybe Phone) PasswordResetIdentity maybePasswordResetIdentityObjectSchema = withParser passwordResetIdentityTupleObjectSchema maybePasswordResetIdentityTargetFromTuple where - passwordResetIdentityTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe PasswordResetKey, Maybe Email, Maybe Phone) + passwordResetIdentityTupleObjectSchema :: ObjectSchema SwaggerDoc (Maybe PasswordResetKey, Maybe EmailAddress, Maybe Phone) passwordResetIdentityTupleObjectSchema = (,,) <$> fst3 .= maybe_ (optFieldWithDocModifier "key" keyDocs schema) @@ -144,14 +144,14 @@ instance ToSchema CompletePasswordReset where emailDocs = description ?~ "A known email with a pending password reset." phoneDocs = description ?~ "A known phone number with a pending password reset." - maybePasswordResetIdentityTargetFromTuple :: (Maybe PasswordResetKey, Maybe Email, Maybe Phone) -> Parser PasswordResetIdentity + maybePasswordResetIdentityTargetFromTuple :: (Maybe PasswordResetKey, Maybe EmailAddress, Maybe Phone) -> Parser PasswordResetIdentity maybePasswordResetIdentityTargetFromTuple = \case (Just key, _, _) -> pure $ PasswordResetIdentityKey key (_, Just email, _) -> pure $ PasswordResetEmailIdentity email (_, _, Just phone) -> pure $ PasswordResetPhoneIdentity phone _ -> fail "key, email or phone must be present" - maybePasswordResetIdentityToTuple :: PasswordResetIdentity -> (Maybe PasswordResetKey, Maybe Email, Maybe Phone) + maybePasswordResetIdentityToTuple :: PasswordResetIdentity -> (Maybe PasswordResetKey, Maybe EmailAddress, Maybe Phone) maybePasswordResetIdentityToTuple = \case PasswordResetIdentityKey key -> (Just key, Nothing, Nothing) PasswordResetEmailIdentity email -> (Nothing, Just email, Nothing) @@ -165,7 +165,7 @@ data PasswordResetIdentity = -- | An opaque identity key for a pending password reset. PasswordResetIdentityKey PasswordResetKey | -- | A known email address with a pending password reset. - PasswordResetEmailIdentity Email + PasswordResetEmailIdentity EmailAddress | -- | A known phone number with a pending password reset. PasswordResetPhoneIdentity Phone deriving stock (Eq, Show, Generic) diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index 23360888ad5..50f0c95b15d 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 (Prism', makeLenses, mapped, prism', (.~), (?~), (^.)) +import Control.Lens (makeLenses, mapped, (.~), (?~), (^.)) import Control.Monad.Except (throwError) import Crypto.Hash (hash) import Crypto.Hash.Algorithms (SHA512) @@ -86,7 +86,7 @@ import Web.Scim.Schema.User qualified as Scim.User import Wire.API.Locale import Wire.API.Team.Role (Role) import Wire.API.User (emailFromSAMLNameID, urefToExternalIdUnsafe) -import Wire.API.User.Identity (Email, fromEmail) +import Wire.API.User.Identity (EmailAddress, fromEmail) import Wire.API.User.Profile as BT import Wire.API.User.RichInfo qualified as RI import Wire.API.User.Saml () @@ -328,22 +328,23 @@ instance Scim.Patchable ScimUserExtra where -- and/or ignore POSTed content, returning the full representation can be useful to the -- client, enabling it to correlate the client's and server's views of the new resource." data ValidScimUser = ValidScimUser - { _vsuExternalId :: ValidExternalId, - _vsuHandle :: Handle, - _vsuName :: BT.Name, - _vsuRichInfo :: RI.RichInfo, - _vsuActive :: Bool, - _vsuLocale :: Maybe Locale, - _vsuRole :: Maybe Role + { externalId :: ValidExternalId, + handle :: Handle, + name :: BT.Name, + emails :: [EmailAddress], + richInfo :: RI.RichInfo, + active :: Bool, + locale :: Maybe Locale, + role :: Maybe Role } deriving (Eq, Show) -- | Note that a 'SAML.UserRef' may contain an email. Even though it is possible to construct a 'ValidExternalId' from such a 'UserRef' with 'UrefOnly', -- this does not represent a valid 'ValidExternalId'. So in case of a 'UrefOnly', we can assume that the 'UserRef' does not contain an email. data ValidExternalId - = EmailAndUref Email SAML.UserRef + = EmailAndUref EmailAddress SAML.UserRef | UrefOnly SAML.UserRef - | EmailOnly Email + | EmailOnly EmailAddress deriving (Eq, Show, Generic) instance Arbitrary ValidExternalId where @@ -356,7 +357,7 @@ instance Arbitrary ValidExternalId where Nothing -> EmailOnly <$> QC.arbitrary -- | Take apart a 'ValidExternalId', using 'SAML.UserRef' if available, otherwise 'Email'. -runValidExternalIdEither :: (SAML.UserRef -> a) -> (Email -> a) -> ValidExternalId -> a +runValidExternalIdEither :: (SAML.UserRef -> a) -> (EmailAddress -> a) -> ValidExternalId -> a runValidExternalIdEither doUref doEmail = \case EmailAndUref _ uref -> doUref uref UrefOnly uref -> doUref uref @@ -364,7 +365,7 @@ runValidExternalIdEither doUref doEmail = \case -- | Take apart a 'ValidExternalId', use both 'SAML.UserRef', 'Email' if applicable, and -- merge the result with a given function. -runValidExternalIdBoth :: (a -> a -> a) -> (SAML.UserRef -> a) -> (Email -> a) -> ValidExternalId -> a +runValidExternalIdBoth :: (a -> a -> a) -> (SAML.UserRef -> a) -> (EmailAddress -> a) -> ValidExternalId -> a runValidExternalIdBoth merge doUref doEmail = \case EmailAndUref eml uref -> doUref uref `merge` doEmail eml UrefOnly uref -> doUref uref @@ -375,12 +376,11 @@ runValidExternalIdBoth merge doUref doEmail = \case runValidExternalIdUnsafe :: ValidExternalId -> Text runValidExternalIdUnsafe = runValidExternalIdEither urefToExternalIdUnsafe fromEmail -veidUref :: Prism' ValidExternalId SAML.UserRef -veidUref = prism' UrefOnly $ - \case - EmailAndUref _ uref -> Just uref - UrefOnly uref -> Just uref - EmailOnly _ -> Nothing +veidUref :: ValidExternalId -> Maybe SAML.UserRef +veidUref = \case + EmailAndUref _ uref -> Just uref + UrefOnly uref -> Just uref + EmailOnly _ -> Nothing makeLenses ''ValidScimUser makeLenses ''ValidExternalId diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index 455c0c0d2a4..21f4181d248 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -40,7 +40,6 @@ import Data.Aeson qualified as Aeson import Data.Attoparsec.ByteString (sepBy) import Data.Attoparsec.ByteString.Char8 (char, string) import Data.ByteString.Conversion (FromByteString (..), ToByteString (..)) -import Data.Either.Combinators (mapLeft) import Data.Id (TeamId, UserId) import Data.Json.Util (UTCTimeMillis) import Data.OpenApi (ToParamSchema (..)) @@ -55,7 +54,7 @@ import Servant.API (FromHttpApiData, ToHttpApiData (..)) import Web.Internal.HttpApiData (parseQueryParam) import Wire.API.Team.Role (Role) import Wire.API.User (ManagedBy) -import Wire.API.User.Identity (Email (..)) +import Wire.API.User.Identity (EmailAddress) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) ------------------------------------------------------------------------------- @@ -183,14 +182,14 @@ data TeamContact = TeamContact teamContactColorId :: Maybe Int, teamContactHandle :: Maybe Text, teamContactTeam :: Maybe TeamId, - teamContactEmail :: Maybe Email, + teamContactEmail :: Maybe EmailAddress, teamContactCreatedAt :: Maybe UTCTimeMillis, teamContactManagedBy :: Maybe ManagedBy, teamContactSAMLIdp :: Maybe Text, teamContactRole :: Maybe Role, teamContactScimExternalId :: Maybe Text, teamContactSso :: Maybe Sso, - teamContactEmailUnvalidated :: Maybe Email + teamContactEmailUnvalidated :: Maybe EmailAddress } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform TeamContact) diff --git a/libs/wire-api/src/Wire/API/UserEvent.hs b/libs/wire-api/src/Wire/API/UserEvent.hs index 5c3e9386af5..db9794604c6 100644 --- a/libs/wire-api/src/Wire/API/UserEvent.hs +++ b/libs/wire-api/src/Wire/API/UserEvent.hs @@ -162,14 +162,14 @@ data UserUpdatedData = UserUpdatedData data UserIdentityUpdatedData = UserIdentityUpdatedData { eiuId :: !UserId, - eiuEmail :: !(Maybe Email), + eiuEmail :: !(Maybe EmailAddress), eiuPhone :: !(Maybe Phone) } deriving stock (Eq, Show) data UserIdentityRemovedData = UserIdentityRemovedData { eirId :: !UserId, - eirEmail :: !(Maybe Email), + eirEmail :: !(Maybe EmailAddress), eirPhone :: !(Maybe Phone) } deriving stock (Eq, Show) @@ -184,11 +184,11 @@ data LegalHoldClientRequestedData = LegalHoldClientRequestedData } deriving stock (Eq, Show) -emailRemoved :: UserId -> Email -> UserEvent +emailRemoved :: UserId -> EmailAddress -> UserEvent emailRemoved u e = UserIdentityRemoved $ UserIdentityRemovedData u (Just e) Nothing -emailUpdated :: UserId -> Email -> UserEvent +emailUpdated :: UserId -> EmailAddress -> UserEvent emailUpdated u e = UserIdentityUpdated $ UserIdentityUpdatedData u (Just e) Nothing 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 36af8c92ec1..ec9eb270c2f 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 @@ -1038,7 +1038,9 @@ tests = testGroup "Golden: CheckHandles_user" $ testObjects [(Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_1, "testObject_CheckHandles_user_1.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_2, "testObject_CheckHandles_user_2.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_3, "testObject_CheckHandles_user_3.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_4, "testObject_CheckHandles_user_4.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_5, "testObject_CheckHandles_user_5.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_6, "testObject_CheckHandles_user_6.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_7, "testObject_CheckHandles_user_7.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_8, "testObject_CheckHandles_user_8.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_9, "testObject_CheckHandles_user_9.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_10, "testObject_CheckHandles_user_10.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_11, "testObject_CheckHandles_user_11.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_12, "testObject_CheckHandles_user_12.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_13, "testObject_CheckHandles_user_13.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_14, "testObject_CheckHandles_user_14.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_15, "testObject_CheckHandles_user_15.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_16, "testObject_CheckHandles_user_16.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_17, "testObject_CheckHandles_user_17.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_18, "testObject_CheckHandles_user_18.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_19, "testObject_CheckHandles_user_19.json"), (Test.Wire.API.Golden.Generated.CheckHandles_user.testObject_CheckHandles_user_20, "testObject_CheckHandles_user_20.json")], testGroup "Golden: Email_user" $ - testObjects [(Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_1, "testObject_Email_user_1.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_2, "testObject_Email_user_2.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_3, "testObject_Email_user_3.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_4, "testObject_Email_user_4.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_5, "testObject_Email_user_5.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_6, "testObject_Email_user_6.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_7, "testObject_Email_user_7.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_8, "testObject_Email_user_8.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_9, "testObject_Email_user_9.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_10, "testObject_Email_user_10.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_11, "testObject_Email_user_11.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_12, "testObject_Email_user_12.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_13, "testObject_Email_user_13.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_14, "testObject_Email_user_14.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_15, "testObject_Email_user_15.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_16, "testObject_Email_user_16.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_17, "testObject_Email_user_17.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_18, "testObject_Email_user_18.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_19, "testObject_Email_user_19.json"), (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_20, "testObject_Email_user_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.Email_user.testObject_Email_user_1, "testObject_Email_user_1.json") + ], testGroup "Golden: Phone_user" $ testObjects [(Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_1, "testObject_Phone_user_1.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_2, "testObject_Phone_user_2.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_3, "testObject_Phone_user_3.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_4, "testObject_Phone_user_4.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_5, "testObject_Phone_user_5.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_6, "testObject_Phone_user_6.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_7, "testObject_Phone_user_7.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_8, "testObject_Phone_user_8.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_9, "testObject_Phone_user_9.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_10, "testObject_Phone_user_10.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_11, "testObject_Phone_user_11.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_12, "testObject_Phone_user_12.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_13, "testObject_Phone_user_13.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_14, "testObject_Phone_user_14.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_15, "testObject_Phone_user_15.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_16, "testObject_Phone_user_16.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_17, "testObject_Phone_user_17.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_18, "testObject_Phone_user_18.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_19, "testObject_Phone_user_19.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_20, "testObject_Phone_user_20.json")], testGroup "Golden: UserSSOId_user" $ @@ -1110,7 +1112,9 @@ tests = testGroup "Golden: PasswordChange_provider" $ testObjects [(Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_1, "testObject_PasswordChange_provider_1.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_2, "testObject_PasswordChange_provider_2.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_3, "testObject_PasswordChange_provider_3.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_4, "testObject_PasswordChange_provider_4.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_5, "testObject_PasswordChange_provider_5.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_6, "testObject_PasswordChange_provider_6.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_7, "testObject_PasswordChange_provider_7.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_8, "testObject_PasswordChange_provider_8.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_9, "testObject_PasswordChange_provider_9.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_10, "testObject_PasswordChange_provider_10.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_11, "testObject_PasswordChange_provider_11.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_12, "testObject_PasswordChange_provider_12.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_13, "testObject_PasswordChange_provider_13.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_14, "testObject_PasswordChange_provider_14.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_15, "testObject_PasswordChange_provider_15.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_16, "testObject_PasswordChange_provider_16.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_17, "testObject_PasswordChange_provider_17.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_18, "testObject_PasswordChange_provider_18.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_19, "testObject_PasswordChange_provider_19.json"), (Test.Wire.API.Golden.Generated.PasswordChange_provider.testObject_PasswordChange_provider_20, "testObject_PasswordChange_provider_20.json")], testGroup "Golden: EmailUpdate_provider" $ - testObjects [(Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_1, "testObject_EmailUpdate_provider_1.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_2, "testObject_EmailUpdate_provider_2.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_3, "testObject_EmailUpdate_provider_3.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_4, "testObject_EmailUpdate_provider_4.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_5, "testObject_EmailUpdate_provider_5.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_6, "testObject_EmailUpdate_provider_6.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_7, "testObject_EmailUpdate_provider_7.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_8, "testObject_EmailUpdate_provider_8.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_9, "testObject_EmailUpdate_provider_9.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_10, "testObject_EmailUpdate_provider_10.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_11, "testObject_EmailUpdate_provider_11.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_12, "testObject_EmailUpdate_provider_12.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_13, "testObject_EmailUpdate_provider_13.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_14, "testObject_EmailUpdate_provider_14.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_15, "testObject_EmailUpdate_provider_15.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_16, "testObject_EmailUpdate_provider_16.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_17, "testObject_EmailUpdate_provider_17.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_18, "testObject_EmailUpdate_provider_18.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_19, "testObject_EmailUpdate_provider_19.json"), (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_20, "testObject_EmailUpdate_provider_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.EmailUpdate_provider.testObject_EmailUpdate_provider_1, "testObject_EmailUpdate_provider_1.json") + ], testGroup "Golden: BotConvView_provider" $ testObjects [(Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_1, "testObject_BotConvView_provider_1.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_2, "testObject_BotConvView_provider_2.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_3, "testObject_BotConvView_provider_3.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_4, "testObject_BotConvView_provider_4.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_5, "testObject_BotConvView_provider_5.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_6, "testObject_BotConvView_provider_6.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_7, "testObject_BotConvView_provider_7.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_8, "testObject_BotConvView_provider_8.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_9, "testObject_BotConvView_provider_9.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_10, "testObject_BotConvView_provider_10.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_11, "testObject_BotConvView_provider_11.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_12, "testObject_BotConvView_provider_12.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_13, "testObject_BotConvView_provider_13.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_14, "testObject_BotConvView_provider_14.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_15, "testObject_BotConvView_provider_15.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_16, "testObject_BotConvView_provider_16.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_17, "testObject_BotConvView_provider_17.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_18, "testObject_BotConvView_provider_18.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_19, "testObject_BotConvView_provider_19.json"), (Test.Wire.API.Golden.Generated.BotConvView_provider.testObject_BotConvView_provider_20, "testObject_BotConvView_provider_20.json")], testGroup "Golden: BotUserView_provider" $ diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs index 020c4119ddd..05e0dc390d1 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs @@ -27,19 +27,19 @@ testObject_ActivationResponse_user_1 = { activatedIdentity = SSOIdentity (UserSSOId mkSimpleSampleUref) - (Just (Email {emailLocal = "\165918\rZ\a\ESC", emailDomain = "p\131777\62344"})), + (Just (unsafeEmailAddress "some" "example")), activatedFirst = False } testObject_ActivationResponse_user_2 :: ActivationResponse testObject_ActivationResponse_user_2 = - ActivationResponse {activatedIdentity = EmailIdentity (Email "foo" "example.com"), activatedFirst = False} + ActivationResponse {activatedIdentity = EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = False} testObject_ActivationResponse_user_3 :: ActivationResponse testObject_ActivationResponse_user_3 = ActivationResponse { activatedIdentity = - EmailIdentity (Email {emailLocal = "\10031*;'R\EM\SI\1032685\1041167", emailDomain = "Gw:[T8\34437"}), + EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = False } @@ -47,7 +47,7 @@ testObject_ActivationResponse_user_4 :: ActivationResponse testObject_ActivationResponse_user_4 = ActivationResponse { activatedIdentity = - EmailIdentity (Email {emailLocal = "h\nPr3", emailDomain = ""}), + EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = True } @@ -55,7 +55,7 @@ testObject_ActivationResponse_user_5 :: ActivationResponse testObject_ActivationResponse_user_5 = ActivationResponse { activatedIdentity = - EmailIdentity (Email {emailLocal = "7\1042098m\95296\b\1098765", emailDomain = "AJX*s&\173117\988870p"}), + EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = False } @@ -69,19 +69,19 @@ testObject_ActivationResponse_user_6 = testObject_ActivationResponse_user_7 :: ActivationResponse testObject_ActivationResponse_user_7 = ActivationResponse - { activatedIdentity = EmailIdentity (Email {emailLocal = "\98670", emailDomain = ""}), + { activatedIdentity = EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = True } testObject_ActivationResponse_user_8 :: ActivationResponse testObject_ActivationResponse_user_8 = - ActivationResponse {activatedIdentity = EmailIdentity (Email "bar" "example.com"), activatedFirst = True} + ActivationResponse {activatedIdentity = EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = True} testObject_ActivationResponse_user_9 :: ActivationResponse testObject_ActivationResponse_user_9 = ActivationResponse { activatedIdentity = - EmailIdentity (Email {emailLocal = "\ENQ?", emailDomain = ""}), + EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = False } @@ -89,6 +89,6 @@ testObject_ActivationResponse_user_10 :: ActivationResponse testObject_ActivationResponse_user_10 = ActivationResponse { activatedIdentity = - EmailIdentity (Email {emailLocal = "\ACK3", emailDomain = "\f\1040847\1071035\EOT\1003280P\DEL"}), + EmailIdentity (unsafeEmailAddress "some" "example"), activatedFirst = False } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_user.hs index 5881ee95222..2f6d25d9db2 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/CompletePasswordReset_user.hs @@ -26,7 +26,7 @@ import Wire.API.User.Password testObject_CompletePasswordReset_user_1 :: CompletePasswordReset testObject_CompletePasswordReset_user_1 = CompletePasswordReset - { cpwrIdent = PasswordResetEmailIdentity (Email {emailLocal = "\STXQ=\33841k", emailDomain = ""}), + { cpwrIdent = PasswordResetEmailIdentity (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = @@ -179,7 +179,7 @@ testObject_CompletePasswordReset_user_8 = testObject_CompletePasswordReset_user_9 :: CompletePasswordReset testObject_CompletePasswordReset_user_9 = CompletePasswordReset - { cpwrIdent = PasswordResetEmailIdentity (Email {emailLocal = "A", emailDomain = "9L\b\1021106\37856"}), + { cpwrIdent = PasswordResetEmailIdentity (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = @@ -237,7 +237,7 @@ testObject_CompletePasswordReset_user_12 = CompletePasswordReset { cpwrIdent = PasswordResetEmailIdentity - (Email {emailLocal = "(\142728\EM\DEL=]=\a", emailDomain = "\175673\SYN\b\n\64411\v&\1083262"}), + (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = @@ -277,7 +277,7 @@ testObject_CompletePasswordReset_user_14 = CompletePasswordReset { cpwrIdent = PasswordResetEmailIdentity - (Email {emailLocal = "\1046936Q?\1079889\1101745", emailDomain = "\178846\1002100\18704"}), + (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = @@ -297,7 +297,7 @@ testObject_CompletePasswordReset_user_15 = CompletePasswordReset { cpwrIdent = PasswordResetEmailIdentity - (Email {emailLocal = "6\vF\EOT]\ESC\1087604.'", emailDomain = "JEe\1090620\1085217\&2dK\996913"}), + (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = @@ -335,7 +335,7 @@ testObject_CompletePasswordReset_user_17 :: CompletePasswordReset testObject_CompletePasswordReset_user_17 = CompletePasswordReset { cpwrIdent = - PasswordResetEmailIdentity (Email {emailLocal = "\53825[\20709", emailDomain = "\38742wC\SUBE\17763\179609"}), + PasswordResetEmailIdentity (unsafeEmailAddress "some" "example"), cpwrCode = PasswordResetCode { fromPasswordResetCode = diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs index 8673f2ba821..59fd227553a 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_provider.hs @@ -18,148 +18,7 @@ module Test.Wire.API.Golden.Generated.EmailUpdate_provider where import Wire.API.Provider (EmailUpdate (..)) -import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) +import Wire.API.User.Identity testObject_EmailUpdate_provider_1 :: EmailUpdate -testObject_EmailUpdate_provider_1 = EmailUpdate {email = Email {emailLocal = "sL\98765", emailDomain = "%"}} - -testObject_EmailUpdate_provider_2 :: EmailUpdate -testObject_EmailUpdate_provider_2 = - EmailUpdate - { email = - Email - { emailLocal = "7\160957>t\21165\ACK\69619n9\b\USskT.\"\1106936\r\DC4`", - emailDomain = "^/>1Rp<\EM\1110261\1087553\STX#\a[E\ETX#\30865\162265\3392eJ " - } - } - -testObject_EmailUpdate_provider_3 :: EmailUpdate -testObject_EmailUpdate_provider_3 = - EmailUpdate - { email = - Email - { emailLocal = "1[Z\68778\r\35821\&3\1087344|u\996796\167850\GS \1071086" - } - } - -testObject_EmailUpdate_provider_20 :: EmailUpdate -testObject_EmailUpdate_provider_20 = - EmailUpdate - { email = - Email - { emailLocal = "o\SOH\1002138\aLL$\SO\65490\1099895l*p\984607\SUB", - emailDomain = "q\30683\DC3\12589\1001477\1015970q\1002402\145416\1056480&^\176848Z" - } - } +testObject_EmailUpdate_provider_1 = EmailUpdate {email = unsafeEmailAddress "some" "example"} diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_user.hs index c6507d1ebd3..71e78154c88 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/EmailUpdate_user.hs @@ -17,181 +17,133 @@ module Test.Wire.API.Golden.Generated.EmailUpdate_user where -import Wire.API.User (Email (Email, emailDomain, emailLocal), EmailUpdate (..)) +import Wire.API.User testObject_EmailUpdate_user_1 :: EmailUpdate testObject_EmailUpdate_user_1 = EmailUpdate { euEmail = - Email - { emailLocal = "<&\DELaW1q|0.n\EM", - emailDomain = "p\1107865\1021976l_R\141868l=;\1049523\&7u\"\DLE1}wm{\CAN}" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_2 :: EmailUpdate -testObject_EmailUpdate_user_2 = EmailUpdate {euEmail = Email {emailLocal = "C\78599|g\1035896(4", emailDomain = ""}} +testObject_EmailUpdate_user_2 = EmailUpdate {euEmail = unsafeEmailAddress "some" "example"} testObject_EmailUpdate_user_3 :: EmailUpdate testObject_EmailUpdate_user_3 = EmailUpdate { euEmail = - Email - { emailLocal = "uA76\1057701c\136605\DC3\148218\SOHU0]Ds$L", - emailDomain = "/\16026 u\1112080\DC3Pq\GSev\25066\1029859\16008" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_4 :: EmailUpdate testObject_EmailUpdate_user_4 = EmailUpdate { euEmail = - Email - { emailLocal = ":|\172071WYA\a`OS\DC3\NAK\1060128\1109387u\v-\DC3F2B\1009753'z\ENQ}4[", - emailDomain = "6\147383C\153603\1016221V\1091182\&8\"\SOHM\168763\58271l" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_5 :: EmailUpdate testObject_EmailUpdate_user_5 = EmailUpdate { euEmail = - Email - { emailLocal = "0a\10920\DC2n\FS!a;*l\55139Z\b\EM\NUL\NUL\1060546\RSj\\\95672_;\STX", - emailDomain = "I\65075j\1014141byd\155419K\129140\74591\1098637mwP" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_6 :: EmailUpdate testObject_EmailUpdate_user_6 = EmailUpdate { euEmail = - Email - { emailLocal = "\DELcom0$p\50570/\FS\1044616\1015174\SIN\1072010,", - emailDomain = - "\GS\1004969\155070,\41398/qeT&\152655\a\45871}\ETB\45684\1113465\1002232#\183342\&0\20887\&9),4F" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_7 :: EmailUpdate testObject_EmailUpdate_user_7 = EmailUpdate { euEmail = - Email {emailLocal = "-\53892\62061", emailDomain = "\a$\12768Be\1072209\fS7.\12322\NUL\2873\r+k>Z:E\ETXhX$?"} + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_8 :: EmailUpdate testObject_EmailUpdate_user_8 = EmailUpdate { euEmail = - Email - { emailLocal = "\EMf\\\SOdD9#XfnL!\995008\ACK\FSZ\53254U", - emailDomain = - ")\34765\1018468x9~t)Dd;P\ESC\1024361.M(p\1050395pCz\1103678\1001284\SI\ENQ\ACK{\1016539\1101104" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_9 :: EmailUpdate -testObject_EmailUpdate_user_9 = EmailUpdate {euEmail = Email {emailLocal = "\FS\SI,n}\13385", emailDomain = "x8\GS"}} +testObject_EmailUpdate_user_9 = EmailUpdate {euEmail = unsafeEmailAddress "some" "example"} testObject_EmailUpdate_user_10 :: EmailUpdate testObject_EmailUpdate_user_10 = EmailUpdate { euEmail = - Email - { emailLocal = "r)\158517\SI\DEL\ETB\STX\1072857\DC4*$", - emailDomain = "\27680\1111520h\1022893\27692\1014774|=Bb\177401X" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_11 :: EmailUpdate testObject_EmailUpdate_user_11 = - EmailUpdate {euEmail = Email {emailLocal = ".", emailDomain = "i4N\1006864f'\GSh\132316\189403\29546x54\48183h"}} + EmailUpdate {euEmail = unsafeEmailAddress "some" "example"} testObject_EmailUpdate_user_12 :: EmailUpdate testObject_EmailUpdate_user_12 = EmailUpdate { euEmail = - Email - { emailLocal = "\1066242\ENQo`\ENQebt*\119006!", - emailDomain = "\152953\169628Fk\DC3\DC1Dq\SYN|0 c\fY\1088003\988616" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_13 :: EmailUpdate testObject_EmailUpdate_user_13 = EmailUpdate { euEmail = - Email - { emailLocal = "5\1109085SV'\7023\169487fR\SOHa:L\184444\SOH`\CANY", - emailDomain = "Yt3l5\145133\1054884j\1087288\1103021&" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_14 :: EmailUpdate testObject_EmailUpdate_user_14 = EmailUpdate { euEmail = - Email {emailLocal = "\140912\993263r.", emailDomain = "\EM\54387\176848q\CANT:`]a$J\DC3'\179878\1010553"} + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_15 :: EmailUpdate testObject_EmailUpdate_user_15 = EmailUpdate { euEmail = - Email - { emailLocal = "8ao\5201Q", - emailDomain = "8T\110875\FS\1001671\1104097\NUL\ETX\5639\ENQ\1078168HZ\185913[rr27\1037003\5689" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_16 :: EmailUpdate testObject_EmailUpdate_user_16 = EmailUpdate { euEmail = - Email - { emailLocal = "\SI\20925\"\DC2\rn\GS\1082759J(?]\US\1002518$7\136749\&7J\1019807p\EMi", - emailDomain = "Q`6_\SYN\DC3\1055256\&5hv\23871\n\SI\171070)\64498\b\ENQ\nA\1450T\94210" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_17 :: EmailUpdate testObject_EmailUpdate_user_17 = EmailUpdate { euEmail = - Email - { emailLocal = "64A\999241\GS\DC1\DLE\7404\GSj", - emailDomain = - "\136875=\156122\f\ENQr\DC2Ga\25747\ETX/\55110G\NULk=\NAKq\1073443R}Ts\ETX\f\1027779\1088335\&2" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_18 :: EmailUpdate testObject_EmailUpdate_user_18 = EmailUpdate { euEmail = - Email - { emailLocal = "9L\ESC\DC3\32248/F\154604O\1061945>\bx;2\148788\&0\US", - emailDomain = " {\"_\v\1092033\1041960\1066771\1088769\EMJ%\1005251yy\SUB\1040487t>xoC" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_19 :: EmailUpdate testObject_EmailUpdate_user_19 = EmailUpdate { euEmail = - Email - { emailLocal = "\178973\1040124E\FS\SUB\126215\NAKC%\988145\ACK\US\DC1a8\r\64887E\990883%\178650\185749;|\GS", - emailDomain = "1+,\135308\&83" - } + unsafeEmailAddress "some" "example" } testObject_EmailUpdate_user_20 :: EmailUpdate testObject_EmailUpdate_user_20 = EmailUpdate { euEmail = - Email - { emailLocal = "e\NAKV\bD\SOH88Kh\FS\169565D4\1089993\36544zg\RS", - emailDomain = ":\174380D\ENQy+\DC4k>]\60696\ETB\FSr\1010033aWSw\a\6023\&6\RS\99409f" - } + unsafeEmailAddress "some" "example" } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Email_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Email_user.hs index a6205acf031..d73a4d3a218 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Email_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Email_user.hs @@ -17,120 +17,8 @@ module Test.Wire.API.Golden.Generated.Email_user where -import Wire.API.User (Email (..)) +import Wire.API.User -testObject_Email_user_1 :: Email +testObject_Email_user_1 :: EmailAddress testObject_Email_user_1 = - Email - { emailLocal = "\151983fa\49426\SOH2\v.\FS<\ESC\ETB#\t-\1105186`2", - emailDomain = "\EOT\"\27565\DLEn\GS]\ahDzP\CANp\15102T\133424\DC4d" - } - -testObject_Email_user_2 :: Email -testObject_Email_user_2 = Email {emailLocal = "\985610\DC3F\186317\1084807n:", emailDomain = "8\1072891\3215\SYNs"} - -testObject_Email_user_3 :: Email -testObject_Email_user_3 = - Email - { emailLocal = "\1107928o #\168176G\8169%/]\FSk\1056490t\1103662)\DC3\NAK*\ESCE}>\SUB\SOHEn==8", - emailDomain = "\26168\CANN\a;_l$*\16881P\STX\bK" - } - -testObject_Email_user_4 :: Email -testObject_Email_user_4 = - Email - { emailLocal = "!\141500\SI&\ao88i\989736(\SUBl1_\1078316-\f\t&\FS9\1090163", - emailDomain = "ef&\1030389\180990W(\999136cS\SYN\EOT\97583GLi" - } - -testObject_Email_user_5 :: Email -testObject_Email_user_5 = - Email {emailLocal = "", emailDomain = "\27960\DC1\1089303\1027305jh\1015732o\GSH0bW7^\17877\DELB"} - -testObject_Email_user_6 :: Email -testObject_Email_user_6 = - Email {emailLocal = "eM'5\br>\92509\996616\&4\133072\61444r\t", emailDomain = "\996839[n\ENQ )&D=\1020297\ACK\b"} - -testObject_Email_user_7 :: Email -testObject_Email_user_7 = - Email - { emailLocal = "\1108398\169243a\ETX'\94588L\SYN\37261\991394Q\1001290\998959Fc\1094805T\191410\SOTD", - emailDomain = "\176912?1\1100840DT" - } - -testObject_Email_user_8 :: Email -testObject_Email_user_8 = - Email - { emailLocal = " \ESC{\1106829EZ_\t+E\vE", - emailDomain = "h\92250%\54205g\14627Lu\DC2\178534J} Aq\"#f\ESC \EOTO\DC2" - } - -testObject_Email_user_9 :: Email -testObject_Email_user_9 = - Email {emailLocal = "\DEL\1009982\1032817\&5M6d*~-\DC3\ETB?\32582", emailDomain = "g\111007R\1093154|\986636\1030500"} - -testObject_Email_user_10 :: Email -testObject_Email_user_10 = - Email - { emailLocal = "\"K\1062412\1070216$s\988180\1078655V389V\a\ETB\FSH\1055625)\28401Dg\ETB", - emailDomain = "\1113745\1057450k\fi\n\1046406\139820{\GSl\14339YPbV\DEL-ZZ\1060246LK\36307\1053861Y" - } - -testObject_Email_user_11 :: Email -testObject_Email_user_11 = - Email {emailLocal = "", emailDomain = "+zGJ\b_t/N\NUL3S\1061013M\146321\1076256z\1099407\1106566SJD"} - -testObject_Email_user_12 :: Email -testObject_Email_user_12 = - Email - { emailLocal = "\EOT1[G\1014638\983349\&9\1086491:uJ\144560\FSMF\123165\985853\187923\US6|\996879\NAK\1075664", - emailDomain = "\96735_\1064048" - } - -testObject_Email_user_13 :: Email -testObject_Email_user_13 = - Email - { emailLocal = "\ENQ\rt\FSA#}\RSn\176776OA\SYN\SO\1040173\t2q\DC3n\161371\185193\f+", - emailDomain = "]\29293\159214na[\US'h\134423\DC2\1007180\147811\1110187" - } - -testObject_Email_user_14 :: Email -testObject_Email_user_14 = - Email - { emailLocal = "X\DLEE\DC4\1013278\1045648\1107074YMU[\n}\991766\r7\1010192\CAN\\", - emailDomain = "\165099\1036143M\GS!\142750T%F]" - } - -testObject_Email_user_15 :: Email -testObject_Email_user_15 = Email {emailLocal = "{C\174982\1042320eU\DC4w", emailDomain = "`\DC3>"} - -testObject_Email_user_16 :: Email -testObject_Email_user_16 = Email {emailLocal = "IO\EM2>\1053560+~", emailDomain = "\SO"} - -testObject_Email_user_17 :: Email -testObject_Email_user_17 = - Email - { emailLocal = "\RS\1097381\SYN\SOH>\51458V7C-asF\1055340IfrYTM;\1014918\1059325*l(d", - emailDomain = ".53Q\1097431\&26bfw\175553\73861~\165507\131884m\GS\NAK\SO}\152927~\1051259R" - } - -testObject_Email_user_18 :: Email -testObject_Email_user_18 = - Email - { emailLocal = "\181378|\NUL\STX3\DC2\1099608,:\ETBJuF\DLE*\15790\DC1\"\SYNkU!\989789\&8T\EM2", - emailDomain = "T\"q\DC1\71908}\DC3Z~\128415)" - } - -testObject_Email_user_19 :: Email -testObject_Email_user_19 = - Email - { emailLocal = "\t\ACKN~\RSWy5'\CANq:_K\1022684\"+WM\29811S.\DC2D\DEL`\CAN", - emailDomain = "DG\136157A\59646E=W\1075924" - } - -testObject_Email_user_20 :: Email -testObject_Email_user_20 = - Email - { emailLocal = "/\138192ZF\1003769\1027227", - emailDomain = "\187749I\1109889~FN\1016516\ENQ3PH\DC4k\1036543\131674{\1046142\ETBH\1020386\DC2IR" - } + unsafeEmailAddress "some" "example" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs index 26671442c2e..7ad5845c320 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs @@ -19,15 +19,14 @@ module Test.Wire.API.Golden.Generated.InvitationList_team where -import Data.Either.Combinators import Data.Id (Id (Id)) import Data.Json.Util (readUTCTimeMillis) import Data.UUID qualified as UUID (fromString) -import Imports (Bool (False, True), Maybe (Just, Nothing), fromJust) +import Imports (Bool (False, True), Maybe (Just, Nothing), fromJust, fromRight') import URI.ByteString (parseURI, strictURIParserOptions) import Wire.API.Team.Invitation import Wire.API.Team.Role (Role (RoleAdmin, RoleExternalPartner, RoleMember, RoleOwner)) -import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) +import Wire.API.User.Identity import Wire.API.User.Profile (Name (Name, fromName)) testObject_InvitationList_team_1 :: InvitationList @@ -43,7 +42,7 @@ testObject_InvitationList_team_2 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T09:28:36.729Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "\153442", emailDomain = "w"}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -70,7 +69,7 @@ testObject_InvitationList_team_4 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T19:46:50.121Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -86,7 +85,7 @@ testObject_InvitationList_team_4 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T09:00:02.901Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -102,7 +101,7 @@ testObject_InvitationList_team_4 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T11:10:31.203Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -118,7 +117,7 @@ testObject_InvitationList_team_4 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T23:41:34.529Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -134,7 +133,7 @@ testObject_InvitationList_team_4 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T00:29:17.658Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -144,7 +143,7 @@ testObject_InvitationList_team_4 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T13:34:37.117Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -160,7 +159,7 @@ testObject_InvitationList_team_4 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T18:05:30.889Z"), inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -176,7 +175,7 @@ testObject_InvitationList_team_4 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T15:21:05.519Z"), inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -203,7 +202,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T06:42:29.677Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -213,7 +212,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T11:26:36.672Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -223,7 +222,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T00:31:56.241Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -233,7 +232,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T21:10:47.237Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -249,7 +248,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T15:43:22.250Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -259,7 +258,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T20:44:34.056Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just (Name {fromName = "\1100765v\191022UcU+_\23043!?e Pr\40620=x-z5N\1059506"}), inInviteeUrl = Nothing }, @@ -269,7 +268,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T11:23:55.061Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -279,7 +278,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T10:06:43.943Z"), inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -289,7 +288,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T19:42:31.295Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -305,7 +304,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T06:58:18.517Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -315,7 +314,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T00:40:39.103Z"), inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just (Name {fromName = "')\28977mD\71122?\v\"Q&_8\DC4a"}), inInviteeUrl = Nothing }, @@ -325,7 +324,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T21:44:30.848Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -335,7 +334,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T14:27:46.655Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -351,7 +350,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T03:57:53.185Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just (Name {fromName = "\EM\1085994\5162\&29\93808\GS\n\RSzC`"}), inInviteeUrl = Nothing }, @@ -361,7 +360,7 @@ testObject_InvitationList_team_6 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T14:35:39.474Z"), inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing } @@ -379,7 +378,7 @@ testObject_InvitationList_team_7 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T14:44:40.049Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -395,7 +394,7 @@ testObject_InvitationList_team_7 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T19:09:35.565Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -411,7 +410,7 @@ testObject_InvitationList_team_7 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T11:05:26.660Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -435,7 +434,7 @@ testObject_InvitationList_team_8 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T13:24:44.890Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just (Name {fromName = "I/\SOH\RS\1084682\1069618U\ETB\178928\1078899`\1087404TD4KU\5388?["}), inInviteeUrl = Nothing @@ -446,7 +445,7 @@ testObject_InvitationList_team_8 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T15:37:31.278Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -470,7 +469,7 @@ testObject_InvitationList_team_9 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T12:45:57.694Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -480,7 +479,7 @@ testObject_InvitationList_team_9 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T08:06:09.682Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -496,7 +495,7 @@ testObject_InvitationList_team_9 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T01:04:27.531Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -520,7 +519,7 @@ testObject_InvitationList_team_10 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T17:28:36.896Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "}", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -543,7 +542,7 @@ testObject_InvitationList_team_11 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T01:33:08.374Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = "Z"}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -570,7 +569,7 @@ testObject_InvitationList_team_13 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T04:37:12.563Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -580,7 +579,7 @@ testObject_InvitationList_team_13 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T05:36:38.967Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -596,7 +595,7 @@ testObject_InvitationList_team_13 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T17:31:07.346Z"), inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -612,7 +611,7 @@ testObject_InvitationList_team_13 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T17:18:26.847Z"), inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -628,7 +627,7 @@ testObject_InvitationList_team_13 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T12:43:17.559Z"), inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -644,7 +643,7 @@ testObject_InvitationList_team_13 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T14:24:17.699Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just (Name {fromName = "\US\1039297@p(#\1103640\28521\&0\1083979\n[q~2\f\1057993P\CAN\ACK"}), inInviteeUrl = Nothing @@ -655,7 +654,7 @@ testObject_InvitationList_team_13 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T16:30:09.682Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -682,7 +681,7 @@ testObject_InvitationList_team_15 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T15:54:11.332Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -698,7 +697,7 @@ testObject_InvitationList_team_15 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T23:06:13.648Z"), inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just (Name {fromName = "\21461\&5q}B\SOH\156444`\65394w\\X@\1035677\143112\&7Mw,*z{\132791&~"}), inInviteeUrl = Nothing @@ -709,7 +708,7 @@ testObject_InvitationList_team_15 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T10:37:03.809Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -725,7 +724,7 @@ testObject_InvitationList_team_15 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T04:46:03.504Z"), inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -741,7 +740,7 @@ testObject_InvitationList_team_15 = inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T12:53:52.047Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing } @@ -759,7 +758,7 @@ testObject_InvitationList_team_16 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T15:25:30.297Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = Email {emailLocal = "\SI", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name @@ -783,7 +782,7 @@ testObject_InvitationList_team_17 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T10:54:19.942Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))), - inInviteeEmail = Email {emailLocal = "&", emailDomain = "\179430"}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing } @@ -807,7 +806,7 @@ testObject_InvitationList_team_20 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T07:22:02.426Z"), inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))), - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Nothing, inInviteeUrl = Nothing }, @@ -817,7 +816,7 @@ testObject_InvitationList_team_20 = inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T18:56:29.712Z"), inCreatedBy = Nothing, - inInviteeEmail = Email {emailLocal = "", emailDomain = ""}, + inInviteeEmail = unsafeEmailAddress "some" "example", inInviteeName = Just ( Name 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 2d004b3e305..f9f7c8ca50a 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 @@ -23,7 +23,7 @@ import Imports (Maybe (Just, Nothing)) import Wire.API.Locale import Wire.API.Team.Invitation (InvitationRequest (..)) import Wire.API.Team.Role (Role (RoleAdmin, RoleExternalPartner, RoleMember, RoleOwner)) -import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) +import Wire.API.User.Identity import Wire.API.User.Profile (Name (Name, fromName)) testObject_InvitationRequest_team_1 :: InvitationRequest @@ -32,7 +32,7 @@ testObject_InvitationRequest_team_1 = { locale = Just (Locale {lLanguage = Language Data.LanguageCodes.NN, lCountry = Nothing}), role = Just RoleOwner, inviteeName = Nothing, - inviteeEmail = Email {emailLocal = "/Y\164738\v}?", emailDomain = "\992922\1041097\178160\SO\1036829"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_2 :: InvitationRequest @@ -42,7 +42,7 @@ testObject_InvitationRequest_team_2 = Just (Locale {lLanguage = Language Data.LanguageCodes.AF, lCountry = Just (Country {fromCountry = GH})}), role = Nothing, inviteeName = Nothing, - inviteeEmail = Email {emailLocal = "E", emailDomain = "/"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_3 :: InvitationRequest @@ -58,7 +58,7 @@ 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 = Email {emailLocal = "\SYN", emailDomain = "\1107957Z\1034246we\1105089"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_4 :: InvitationRequest @@ -67,7 +67,7 @@ testObject_InvitationRequest_team_4 = { locale = Nothing, role = Just RoleMember, inviteeName = Nothing, - inviteeEmail = Email {emailLocal = "", emailDomain = ""} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_5 :: InvitationRequest @@ -76,7 +76,7 @@ testObject_InvitationRequest_team_5 = { locale = Nothing, role = Just RoleAdmin, inviteeName = Just (Name {fromName = "\171800\1076860\1103443\CAN8=\n;}\169054M\ao\v3+\n"}), - inviteeEmail = Email {emailLocal = "", emailDomain = "\DEL\15723"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_6 :: InvitationRequest @@ -92,7 +92,7 @@ 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 = Email {emailLocal = "\6559^\EOT\DC4", emailDomain = ".}\177921"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_7 :: InvitationRequest @@ -101,7 +101,7 @@ testObject_InvitationRequest_team_7 = { locale = Nothing, role = Just RoleAdmin, inviteeName = Nothing, - inviteeEmail = Email {emailLocal = "g\NUL-J\65751", emailDomain = "\ETXH\1033960eU"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_8 :: InvitationRequest @@ -112,7 +112,7 @@ testObject_InvitationRequest_team_8 = role = Nothing, inviteeName = Just (Name {fromName = "\1036838&f\1104978\1021739j5\CANv]k\1034960\993099c[\1019257\1047325\EOTw.uL~/"}), - inviteeEmail = Email {emailLocal = "\1031836\SUBh\ETBb\SI", emailDomain = ""} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_9 :: InvitationRequest @@ -123,7 +123,7 @@ 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 = Email {emailLocal = "\\\175244", emailDomain = ""} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_10 :: InvitationRequest @@ -139,7 +139,7 @@ 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 = Email {emailLocal = "\1010285\f\ACK\DLE^s", emailDomain = "d"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_11 :: InvitationRequest @@ -154,7 +154,7 @@ 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 = Email {emailLocal = "\SOH\NUL\1016497nJ", emailDomain = "t\STX."} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_12 :: InvitationRequest @@ -170,7 +170,7 @@ 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 = Email {emailLocal = "\1108640\1081336", emailDomain = ""} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_13 :: InvitationRequest @@ -185,7 +185,7 @@ 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 = Email {emailLocal = "\NULr", emailDomain = "c,"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_14 :: InvitationRequest @@ -195,7 +195,7 @@ 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 = Email {emailLocal = "", emailDomain = "\v"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_15 :: InvitationRequest @@ -210,7 +210,7 @@ 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 = Email {emailLocal = "U", emailDomain = "\1082936"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_16 :: InvitationRequest @@ -220,7 +220,7 @@ testObject_InvitationRequest_team_16 = Just (Locale {lLanguage = Language Data.LanguageCodes.OM, lCountry = Just (Country {fromCountry = BJ})}), role = Just RoleAdmin, inviteeName = Nothing, - inviteeEmail = Email {emailLocal = "\22759", emailDomain = "\SOH"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_17 :: InvitationRequest @@ -230,7 +230,7 @@ testObject_InvitationRequest_team_17 = Just (Locale {lLanguage = Language Data.LanguageCodes.KJ, lCountry = Just (Country {fromCountry = TC})}), role = Just RoleExternalPartner, inviteeName = Nothing, - inviteeEmail = Email {emailLocal = "3\fC\ETB\"", emailDomain = "\SOH0x\120290"} + inviteeEmail = unsafeEmailAddress "some" "example" } testObject_InvitationRequest_team_18 :: InvitationRequest @@ -245,7 +245,7 @@ testObject_InvitationRequest_team_18 = "8VPAp\137681\&2L\140974.\ACK$z,\127809s\1044091P\1053904\1033438a\47231\12226\182868n\ESC\"Xw$\aG" - } + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_13 :: PasswordReset testObject_PasswordReset_provider_13 = PasswordReset { email = - Email - { emailLocal = "\994700\&5\ACK\132331!\1085699\nVb\1027357nU&\1037025u\169968", - emailDomain = "+I\176471q\1064856\SYN\1069753#A\163779\DLE}.\SOHu\1015059" - } + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_14 :: PasswordReset -testObject_PasswordReset_provider_14 = PasswordReset {email = Email {emailLocal = "v", emailDomain = "\1090313"}} +testObject_PasswordReset_provider_14 = PasswordReset {email = unsafeEmailAddress "some" "example"} testObject_PasswordReset_provider_15 :: PasswordReset testObject_PasswordReset_provider_15 = PasswordReset { email = - Email - { emailLocal = "+\150753~\1073496VFc\RS\1102900R\a\ESC4J_\1087106I\f\1043823Dj\DC1\EOT\62142q", - emailDomain = "\1020153\138280n\1062475Gh?\vPXOO\v\1092723\DC2" - } + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_16 :: PasswordReset testObject_PasswordReset_provider_16 = PasswordReset { email = - Email - { emailLocal = "]\1111436Dn\b\NAK\n\17695\167052\ENQ\1024236\&2\r\1069249\1002489\1038720", - emailDomain = "%L(\EM\1109782\STXk\EOTo\170961B\18655O*/+", emailDomain = "\48353"} + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_18 :: PasswordReset testObject_PasswordReset_provider_18 = PasswordReset { email = - Email - { emailLocal = "\FS\1022850\1012117^3\68431*(\1037814\99655", - emailDomain = - "\1037557Y\ESC|=\137727E.A.\NUL\1002333K>\1067053cZZ~\CAN\1058810i\DLE.r\43079\1002153 \176978" - } + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_19 :: PasswordReset testObject_PasswordReset_provider_19 = PasswordReset { email = - Email - { emailLocal = "x|\58643\1101318J8\1007195|%\142798'9\1089195\172026\1085440F\1098543xyP\1054659 4,", - emailDomain = "!]w6:\SOHd4t(\1103884\1052833$\SOHrl9\9929\120677t8" - } + unsafeEmailAddress "some" "example" } testObject_PasswordReset_provider_20 :: PasswordReset testObject_PasswordReset_provider_20 = PasswordReset { email = - Email - { emailLocal = "\39795\&2\SYN)=Xd\155177}o", - emailDomain = "4\SUB\188588\1054317g\NUL\1092307\984568Q`\\\SOU\1017696" - } + unsafeEmailAddress "some" "example" } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderActivationResponse_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderActivationResponse_provider.hs index d3ba47e5892..ae26a5ba72f 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderActivationResponse_provider.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderActivationResponse_provider.hs @@ -18,172 +18,140 @@ module Test.Wire.API.Golden.Generated.ProviderActivationResponse_provider where import Wire.API.Provider (ProviderActivationResponse (..)) -import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) +import Wire.API.User.Identity testObject_ProviderActivationResponse_provider_1 :: ProviderActivationResponse testObject_ProviderActivationResponse_provider_1 = ProviderActivationResponse { activatedProviderIdentity = - Email - { emailLocal = "\16405\145084,kyz-\r\6937\1047584[\1099176#mh>6", - emailDomain = "c\EOT;QAjc\EOT2O\SO%\ENQ-\1003781\SUBn$\1009844\985973b" - } + unsafeEmailAddress "some" "example" } testObject_ProviderActivationResponse_provider_2 :: ProviderActivationResponse testObject_ProviderActivationResponse_provider_2 = ProviderActivationResponse { activatedProviderIdentity = - Email - { emailLocal = "\1089349\"K1\ETX;}\"n~X\134776\161302Fd\FS1^f\DELo}M\1053484q\v=\183432xU", - emailDomain = "C\\" - } + unsafeEmailAddress "some" "example" } testObject_ProviderActivationResponse_provider_3 :: ProviderActivationResponse testObject_ProviderActivationResponse_provider_3 = ProviderActivationResponse { activatedProviderIdentity = - Email - { emailLocal = "J\53673uv\EOT\SYN\NUL:XJ#\rA[\1063999\NAK6r\119313\DELA\DC3\DC3\65757\1003687p/\1081952\twP\1071823\RSCq[\DC4\62257(\1002708P]OL\191214\NUL\SI?\CAN\FS\DEL\62658\1067853O?*\133393\"~\95514\NAK5\DC23\993032\1062731GC \a:T\1086654|$r\1024226Q*US\119666\7973\990723\1092776\1012647\&2\SIXp\DC1l,{\53831$\1091822\SYNw\RS\1014066p\159782$6\1003029\17252\SYN\178493\&7\1094964]\141621\SIi\1073342%\n\SO9i\DC1\ETBI#\ETX\ACKz\"LJ+\f\EOTU\f!nSGq\1041642\1079338\b\n\SYN\58961\1100402\1107153vkoE\\>L\1071747\992957\&2\14662\61032V\USfCJJ\1041994\f\183187\DC1\141258\37968S=\1107082v\994620/jdg\1002901U\1025416s\tO\ESCD\DC2\"\1059656;\162790`C#\DC2\1073802^j^q\133762;`\1044114\1037819\DLE\986390\&0Q\1039253\188705\136022\CAN\1097897\&99\58156\v\132926\1080381\"\1015895\1101268\97449JW\DC3n\1048086\SUB'\ETB!\a;\CANF\1008408:\SI\CAND\61480Nhu\ETBvWC]i\1023609]>mM\96616\989899ISK+\97925\SOHm\"h8\30835\STX\DC43F/A\142221\1002286\98732M/\44462\1041696`\ENQ\1053777\22262k\FS+\t\1010757\DC1,6\a\68820D\1045784.:$'P\20749\1018853\1083057\166962w6V\"I~\f`\9746N2\DC3\SO\DC1S\1111933/\55133ZfjtU\GS\1022578u%\ETB[k\SYN\1038646s+G\EMh6\ETBk\1042066\STX\NAK\SOHi\1024430P\994456\DC1\999049O\a\r;;\72866\988084\DC1y\DC2\t`\ETX{n\CAN#fb\156098z\1089529@\148590-\134697\DLE_[)' r\SOh\ESC\1005694\&4\SIi\t\173183\1062912\SIEe\113729y\SOH\FS\26106U\DLEY\ETB[Wu\148140\1043600\a\1108631d\28497\b\1066901\\&F2F=L\rlk\NAK\1060365x\44894\n\1050464\1017030s\t\992979]T\\\1016800\1103758\177517z=^\ACK2)Y]E\NUL@\1024775_\1009598\"]\1027129\1018765\153761o0>Qd\ACK`GD\NUL\1013350p\51546JF\aw\FSCo\165594\DC2\ENQ\bq\\;\SUB\58807\986762to/Q/3\10426F\78530j\147563}yi_/\bj*y\1010612&\FS#KG\1040949\t{\USn\1009397\994198|\th;CJ|S\158628D\1100265rh\1043492\DC3\RS5VV\1017190ux9\SOu\141038?\1104936_\tx\DEL>4\994644\17535-d:y`*T7\166631_7\GS\SYN\36847bkb\EM\STX\1051731\DC4$\1022793i\NUL\178323\167679xUT\1003494a\ETX\134094V^\GSvz?;E\153708\67127\63995\n8X\183538\997398\CANot\r\ETB\NULV9>\126574`\58999c\SUB\66417W\161422\51315\120691rh\DC3&\139906\1102627RC\v&=2\b\1086617\ETX\994487)\8703\&7Uj\1029880A,ckL\CAN\155729o27\150661\ACKL\rucW\DC3\189366Bw_=\b" @@ -50,7 +48,7 @@ testObject_ProviderLogin_provider_2 = testObject_ProviderLogin_provider_3 :: ProviderLogin testObject_ProviderLogin_provider_3 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "h)\49363Z\1063824\1071314", emailDomain = "\186791\EM\19978\995909f\b"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "K\3841|\1085726HU\n\\Qik\EM\US\153712xG\165429\&1g\37919\&9\1104489v4\1082951\b\ENQ\1067666\1085466\&2\CAN\1081359|\1008692\NAK&\SI;2\DC1\b\1002383A\a=u\SI\40099\tz!pwH1\DC4\SO.lexJ\RS\1057638\&3\EM&s\177278\DEL\1004232\SON\1093350\v\1051086\49256\1091462#n\RSY\1087974\DLE#%ObL=s\59640(\f\EM\985401\1080301y@i\CAN/o\nof}Z\121432V\NULDqD<\EOT\987137\92240 ;\\\GSJ)\ESC\170276Sth\STX^'\993179\994691\171357/ct9\"\ENQ$s*\1112139\983635T\1000238\b\CAN\1053269\1032292\&9Drm\74016$3\78461j\37449y*R@\1045490\1075417mGM\1024517^~\ETX\SO*\"2#\1095216S\FS\CAN|Y]\SOWy&N\159632Dw\f\DC3Y\54016%^+\1070488s\1082203\SOe\1078681\SYN\1038724\71339\51097o\r\184885z\"jn2\f'(B\SYN?s#*d\ENQ\FS\24664\n\160475\ENQ\1088778$f)n\25546}&N\t\1074142\"1\1079112u_QCU\SYNeR`\1011732N\ty\1049057\15798\DC1\1106895k;\1088300\144423t~JX=vN6\a_~C\US\1001142\SO\1089507?$b\1031649\EMA\SUB=\57375vm\n\EOT\1031498'g@\\\162341AhVW\37558\143758\131257\1048128\4146\vO)U\1042082\1030755\\Ly\39677eA4\41869\ni\994151\43752\EOT_\97713n\DC4\140878\\S%\50171\1080044e\CANu\998027\1051199'V-D\1028947X,q\DC3\DC4\169513\ENQ$\46447\64290.e2l\1061537\NAK\ETX{TW\n\52800\1329H\1049309\1059378\994850\1094923\SI<\NAKM;[d\DC4\b}y3Jt\100213%l.\fU\1043697H\STX-.#\SOH\f\151738!\CANY{\ETBd\152209[(\1040856]j\181307am!Z[w\STX'*Tv\174621PM~\1033877\&0\RS!b\128530\\P}3v\EM\63181n\1064827\&9F2gW\DEL4\139178\1022339\t\176600\147459\175596hR\ENQ%\37966z\984421\1013392?*\13832(#\EMr\DC4\br\161885WN[ N{\1095601\EMv\53960\SUB\989224\1100619\1054425\DC1[S\DC38\RS?\1056015\aA\US\1047760A\GSb\98984\1057798-\CAN(j\7084t\ENQ;}\SIHX\ETB]_,\1110377xVY\92234\EOTH\1009657i\95997\GSuu'>S\SI\SOH\28747\19442\62495\1029286\GSb\985600\161392/\EM\1073931\&7\DC1/X\DEL\DC4oZ\1003485\171281\27236|FN'\1088003\DC1\1095084wB\DLE]\48797 V\1075141\1063573\an\1099423\t1\1049162 [}C}gG\1112557#o\990395kF\ETX\bK\1066860g<\SIn\CANZ\95543$}h\US\SYN\147130\&5m\78875\1022687<\1024861\38580h\DLE\37612\983382\r#_\1081233fD\SYN`\1048444_{n~I\EOT#7\"K\v{\28291L\1076561\DLE\1028456\991117\28670]l x\US\1060025\180458U\\\ACK\137215\37941\b>\RS$Y\1105355\EOT\DC4\135173\159718\66296gE[Z\129159\1030459\SYN@\ETX\1089314u\8040\46827(.\ENQ" @@ -59,7 +57,7 @@ testObject_ProviderLogin_provider_3 = testObject_ProviderLogin_provider_4 :: ProviderLogin testObject_ProviderLogin_provider_4 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "\188360%\135755\169860", emailDomain = "wY0nE\45983d,"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\997376`h\134851[/\DLE\SYN\1057356\190778c\DLE\144700\STX$\1049974V\DC2}\"\1056292\&1\34169K\USC\183035\ENQ\DEL'\998099\&70\RSe\1032261\DC2w_\42155\SYN" @@ -69,7 +67,7 @@ testObject_ProviderLogin_provider_5 :: ProviderLogin testObject_ProviderLogin_provider_5 = ProviderLogin { providerLoginEmail = - Email {emailLocal = "\1090846\1076550\ACKf?\1064024\DC1R`", emailDomain = "\31290c?\GS\1008740bOP"}, + unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\DELK\71213\EMr\52582s>qsz\1054321\1103761\14642\EM<*\1034130\37787\CAN^&\128177\&9N <\1092368YR.\163469A\ACK)u3\1077954\&6]\60462\133926\58248IwBZ\STX\991401\1006288mA}2\12958y\124971\168984\1071055\1032417/Q%E\bL<\STXI\ra/\1012873q-\50596 ]U:\DEL^YR0){\f{g\171878\ACKF\US<:\159079]IC\43618\99859\97648O($h\991036\DC1\SOHe\FS\SOHxQ)\1060127\1070870\177697\1050228OZ\1055218\NAK\"\989577/\DC3x[\153967\187729\DLEV\187019\99263\t\155326E)H\acIsa\CAN4\SUB\64144\&8\70723\ai\ETXDD4w\182368\tEjf\DC3h\1057977\1058648a\2001\ACK\30165\132313\118793\SYN<,\1000370+Q2'o\EM\60960\STX]\EM%\DC23PQ&H\1008877\1050884\SYN\US\1003442YR\1029695\18252\NULEx\v\SOH\110594\154395\132048O\US>FG\EM\6072\1035840(\185650B4,\161948\1082520\ENQ\1011783HUJ\SYN\1069998*\1100665w;j7\1041915\&1TJx\SI\1044958\1099495Pn\ETX*O\NULt>a_X|_MmL\6099\DC1\984250\985977:\1094973Cu[r>\1005272vp\DC4" @@ -78,14 +76,14 @@ testObject_ProviderLogin_provider_5 = testObject_ProviderLogin_provider_6 :: ProviderLogin testObject_ProviderLogin_provider_6 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "z_\1019380\DC2", emailDomain = "\180905VDG"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "R\NAK\19239m\20399|\168697|&\DC4\54144_/\1079716\60856Te\179713" } testObject_ProviderLogin_provider_7 :: ProviderLogin testObject_ProviderLogin_provider_7 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "R\1107663\1101373Tk\47808\&4\DEL\fm1r", emailDomain = "`O|Q%"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "AV$\r:\SO\1057925\twBe#aP\GSf\\D\1093131\f\"\1062474\DC4\1063531N\vW\1032165\151144; O\1095945\1039439\163822\1053248\996935\158292\180227*a\1032308w,9\11932\33469\1042359Q$\NAK\NAKpv\992954(Cl\ESC\"cg&2j\ETB\100514|\ENQv&e\48648g\58097fK'j{F\RS\174779zn~\26851a?\989074\ACK \26744Z,#\128914:/\1073971\999239/sGF\ETBi;{|\9210?np\24919_pCi;1j\1075816w\132426\1101926\RS&\1094263q$\DC2p\GSC\ENQV\r\179342g\SUBls,\166835d\SYNC \1003970<}\1090450)x>~\113696VV\1038818\ENQm\177584\RS\DC2\146064\STXykNo\1109305]\FS\n\nZ\ETXp\1093301\1040700\14783\70715oy!%m\1055994Pg\29043Mz\63458\151167\142629\ENQ\GSoiO\1079223R\FSHG(\155361\1043624<[nAlz'\EMN\aX^J-\33133\DEL\SUB`ubS2a\FS\1089953I\DLE\NAK\1066424u8rSJT\34653\983177\1103439|\b\997721V\SUB\CANK.(\126129\a\1111643\1099135y&t(\54546\139956Y@t\n]\rlJ-\65671H\SOEp_Nv<=^\37923\RS>A\RSt\NAKL\1083189\28040t\EOT\1021817|/\46641\NULs50\EOT\167880\1053339\SI1\1081864\a\1004866\RS\8114W\157166vsqz\CAN\32807m\986009\60083~j\1045359\1031943l\169109Zd\1030016\SOHlKy\NULITt\20709\184328\SOS(\34490K\45599*\NULn\28796^\188678\&0\1040248\DC3\1109095\149822\1084021\FS\\\22362f\1106493&N(*\151139\1032885\NUL~*_\NAK\1034617\1023597\&6s\1046400\41249z\NULs8!0m\USb\142489\\1lu2?>7x2^t3\54489L\1080612\30405|j[Hi\SO75&\EOT!\37099_pFu;'\7181(\169297Jk\SI;\n\93039@m\41290~\SYN\SI\38271\FS\1041438\ACK(qh%\"\SYN\ETBC\1089293C\63782\&8Ff,O\ESCks.q\38452J+W\994044\&3/\a\882Q/\127952&\EOTl\ENQ55]5\SUB<`\t\f[#\t7!YI\tei\66807\37932\&1yUS\1095848X2o\1030170\t\96997{\t\ETXOp\tf$B\STX?\NULxv\1029314P-u:CW?\1014394z\EOThO&\97914\179719\ESC\1045462z\DC2\178600\&7\SO\15990@|\171677\&5" @@ -94,7 +92,7 @@ testObject_ProviderLogin_provider_7 = testObject_ProviderLogin_provider_8 :: ProviderLogin testObject_ProviderLogin_provider_8 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "\a", emailDomain = "z\1065930\13842V:\178758\DC4\136826"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\1032807,+\26563:\1071780%G\NAK\997900/\DLE@\1027414\25655l7\DLEw\EOTe\ETX\USV\1028504i\1102233\tJ\1023546\188083k\143090Hz%|aG\1039292\r\52139Q\US&\GS\EM42\ACKC{t\16867]g\FSdO\48840`.\184346`m[\ETX\1077921-\ENQ\16213x\ESC\1101818N\142994\DLEK8\188217[\DEL\988988\1008523k|e\NUL\DEL}\SO\991947<\SOHg\1031754G\97218K|^\1095277\RS\167966M\168754/i\1093780!#\186388}\7777\EM0\1107848\&2 B^\DC1e@,I\SUB\1060988\93989v\1010096\NUL]\"c\138108\47542\RS\SOHB\DC4;+N\1108696UI:\\Zc\1066121zm|+{W\988550I\2530MU\EM\992874\n\NULH\ESC!n\1020509q.8{\1004748\162235T>\127905\1059100_ \ESC\NAKZ\EOT9\78187\990745r\ETB|\SOH\83522'#\3536\ETX{|=P\153911VTH\b\991886\8452'\95845\SOHvSk\1042204\52955\28694K#^\20633O!'_}\1093507\1043069\DLE\STXQp}\DLEfd\128876(\a;\1003531`_\a&\110809LI\GS\"\159092\\71\a\DC2F\998197\182925L\1070548a@\SYN\r\1076739\97129h\ACKP;_Yj\4138CH~V9\ETX\DC2M\SUB\1107806\1058529%5;\SOHd]" @@ -103,7 +101,7 @@ testObject_ProviderLogin_provider_8 = testObject_ProviderLogin_provider_9 :: ProviderLogin testObject_ProviderLogin_provider_9 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "?!", emailDomain = "\f\1047642v\30589\ACK\28844"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\191077R\tF3{7\986307Q\5004\ESC\189822h[\NULV_m8\ETX:K\r\1003166(\1088407\DEL\DC4\1066192\rKz\ETB\96897^o/&!6\SUBV\bU\8154-1n-\1022625r\ETX\35324F\a\1087954+\990349,\\Xm\ESC\789\1107982|\53584\21152\EM=\SUB\1049274fR\n\1028364cC>jhZ-\vBnq\ENQ\ACK\"\DC2L\60229\1089806\US;Q\1018560D\"Q@\1027316Qq\20765" @@ -113,7 +111,7 @@ testObject_ProviderLogin_provider_10 :: ProviderLogin testObject_ProviderLogin_provider_10 = ProviderLogin { providerLoginEmail = - Email {emailLocal = "N\1079983C\1019848\987758Q\1016550?\148085X", emailDomain = "\1069665\42373l"}, + unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "-=\32480w\ACK\990091YN\186686Mx\fz\20991\FSa\416e0ARM\167347\DLE{\8548A\r@3\16428\DC3\ETXgA\47834\&0g\147348\v\49080\1086233OQ@+\1007101kw=~Z=q\1075779\SOHq\179325\SOH\45786\1013252\1008755l\ne\1071386\1009919)Z]A\1012627_o\95076\146226\1045971Z;\18446M\132612\1112886\\\1088243b\r\50791\1020046%\39407vLCFZ]f\GS\t7\1096142uN? \DELY,hXW\16146\SO\8281%[t7R\48925S\n^\EOT\150600\191010J\1000079\tj;P\189612rw\DEL>@\SI\SO~ \48000k7\1102878{m@\1023548}D*i\t|\1007134\vF\1070537+\57561\SUB(qIOhI#\CAN\1009728\1008900\&7E\144838/\SIM_z\1028908\2400\11810\1691\NAK\n\US\SYN^)\SUBLVo\RS\50243\178287w\12126UeQi\ACK\12340\97806&\52880\DEL3S$$\179126\RS6\1077404\1067610*\131637Kkk&Ie\NUL{\CAN@}\66331L\92974\152099L-\r6U$3wC \993821\1009888yf3\1086000u\144879}}\1102943\&9\SOX\1103437t\1027564{\DC2m\1072289o\FSEzaDAZ\24534T\t1\DLE\37862\145559\NUL\75066\DC4D|t0'W\SI,l\aYI\SO\1011545\182577l,\SOH\1018137k\aB\1048822\SYNn\993483\aEjoDc\127837EhKM\DEL|\159141R\68861\t\ENQ\DC1q\SI\SYNC3\1011231\EMUy\vB\132997a\34076!8\ESC\GS\NULw\40648xEV\ENQy\DC36\1106820\18443_\184217\ETB\96220\&7f\1108082v0\38992\ENQ2*\1025120bW \1032288\DC4\140659,\18298zh\1100304rO\1027921\DC4N(\t\1041653\SO\48792,D|\1066254\998442)\GS;q8{\US3\FS)\1089425Z\STX\SOH\SUB,:htBg6Qz\1059787\1002112\1068685G\RS\DEL\41342\26517iS\172060\&8cdr\ESC;\DC2nf38\28682gl$9\ngdX\59459\NAK\1062568\1035354\1086375\144492q6~\DC1d\ESC\n\153700-5B\1038372/>M0\t\bVK \65825&\DLE<\161308\133305\&2Id\993601\&0\fKg\CANZF\164950rE\156086\1026024l$\119993\SOHf\1062528T(bH_e\"\7643w\45515\STX9o^\SYN\DEL\147213eM\ETX\188951-\181301\1111821E\999551\t\121125@(\46533m\148530\150405\986944Q\f~\1032957\&8Dv1\30512m/r\ESCYr\8210yq\SI \1084841x\96176\17805\149489\DC4\RS\157280zR\SI\1053172\&0\EOT\1034755\147571kF\43160@\1094412xqo/R\165394\DC3G\DLE\179117\10618\97627\SO@NL\1101566\DC30)uc\139710\&9\1000371\1002633\&1\DC1\f\DC4\1018005\1101074|\fN.\20166z\ENQr\SOH,;\EM\162767\136173\ETBa\145336\EMd\GSKvH\1003959\&3G(F\DC4)`\1092995O\1061374\CAN\SI\fqL\1094451\FS\tI@\SOH/k}`e~-\NAKo\120778S\1105769\3991h\92498^u>\992342\&1\143466%E\DC1o\182499us\1033582\RS\45417\1110073\SI\1051765\41403\fl\SOHt/\1027515U\190039\1045439\NUL\a\160207\RS\144945\ESCI\SOH\\\f\180467Ni\DC2=>\DC4#\121462*\DC3I\ETBO\SOh{\1085656\US\24409\FSw!MY\181626X\45991.\tf?a\17399\1051598=\SYN\1036417\1082173\&3><\1003370Iw\"\SI\CAN2'@29\DC2\50699\\\1077056\r\4318\\4w\983354%/6\1104193g\58587~.g\74148\50911\1086151 Au\ETBP\141262\170054\174447x\1094618\1080957\34978\1069200?\99187\ACKI :\STXZy$\EM#\1092580\146673jv\b\179001<\SI\EOT\159484\DC3!\1011657a\a\62678|\194723])\DC1\NUL]K\1038195\&2qo6,\100506z\37503\1108939Av\995584v\990741\v[e\CAN\DC24!)\a),@e?\32096DA\1356%%F+\1007987Oc\175553\a\r\1022239\NUL\EOTg4g*\8850AkKC\RS\SYNg>=\171781\&3\1039525\NUL\DC4\1086181p:2HH\137199\29594\DC3\148134\159816q\SIk%\188310\23312\141112\\a\120019E\30348\SI\183647Nr\SYN$&\1002603\1088350i\36041,\151865\FS7f\98027e\CANL\39708\DC2\999013>\EM\1032242N\48509\EM\DEL\1056374\&7p\1109882vP1?,c\1065156Xv\SUBN'\SOH\1000812lE\a\110963J\DLE\176674U\1017909kP\1014472`O\28714\NAKjjC\1046204 Me`06.\SOH\EMs\1108581\984997Z)\505Bw\NULzKZ\170904H\ETXF6yhu\111193\35302d+\120753y\1055023\1044971\1113599)\151175|\SO\EOT \1088176\986374,\SUB$.-7\145796a\EOT\1066588R5g\1058599rM\143676\1015936H\NULM\SYN\rT\174913Z\NAK\59354\993617\190813\US\1025870-\45910\ENQ\1013980~h\RSpV\NAKMTRyE9\ETBt\EOT\32355\184982\186747]$v\181164\182566ap\1039132; \1092510\t\"Ww\98795$\1068166y:`M\NUL/~Z\\eF6\1044984\NUL8\998267\83100\&1\NUL\12214\NULdaS\DC1\1057662f\74458E\vt\128426C\EM%Q\1066510\132734Xp*78\34762`\194697<\1078829dJ\1064424\1086026\SIb8u\147637\ESC3\166488F\1096009\b\13973\83061E\n>\52949q\vj\SUB\CAN\SO\1113037\984171Y\EOT\1078017X%\145648in\1014030w\f9\65731\1077451\1020021Ff\1059532c>\n$}\EM\25419Y\54667)\1095983\32915\1017920\SUBW+l\ab\62782\24910\DC4\145359FmR]|u\ETBkR\a7D\51919G\53611\164554@\SUB\36018\ETB/kt.\US{\1092794#^(zcVm\f\1094121'6j$[\ACK\ESC/VOQ\996687VLw\1067078#f\RSC\146914\97477@s\46339me2\ACKe\fs5g\US\144879\97542I$t\54653\ETB\SI\1073768&tPz\1010457\RS\NAK\150524\DEL&`x\1007249?:1p\61424\US\993411\RSC\157776\15121X\1051749\65224_ZZ\SOaTW\DC4\EOT\1110937$\CANp\44513|(\92589\&67\15667\&7\29934\1058652\98055zLc-Y^\1004000\f\34235\n\92226\b|\1044732\\\DC1\NUL\1068972\EM,W7`\183734\DLE\142014Jn\999320Efa\EM\NULJLwN" @@ -151,7 +149,7 @@ testObject_ProviderLogin_provider_13 = testObject_ProviderLogin_provider_14 :: ProviderLogin testObject_ProviderLogin_provider_14 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "q\100180\DC1 3%Y+RO\1022392\&1", emailDomain = "4`\1034127"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\SUB1\1011613\1053868\RS\DEL\GSG\1105681.\1071787T\STX\SI\128952:\165225\&8\ESC1aV\"\EMG|g|7\ETXP1\1113285A{_\44503\10934bufZ?\96371\53879\186363\DC3\1016903~g0\1011005B\186373\ESCR|\SO'4\t\ETB \1093146\67705\SYN\28861\40040\1058799\1095038I\1021393\&9\110714\DC3\EOTP+\SYNz@m\16478!-u}(\ESCG\\\187881\b|3\1032903\37872e\"\36203-\t\1095705\DC2" @@ -160,7 +158,7 @@ testObject_ProviderLogin_provider_14 = testObject_ProviderLogin_provider_15 :: ProviderLogin testObject_ProviderLogin_provider_15 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "\f9a\flw\NUL,", emailDomain = "i\DC2/d\158245I\ETX\\\150537t"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "':\1006988%\52805|\986668\1030808\135993\17938\1004363\CANo+!4T\b\ACK\99223W~\1028898\151854\"u(4l4nH2\1101674A\16088-\1018022\24141\111131K3@q#\161349A\ETXz}\190078 \ETBr\59344\9426\1078379C\45259\SI|\1067233\DEL\SYNx\ENQ<\1018777\GSrj\1070063\SUB\189644\EM\ACK[\RS\137992!VP\au\FS92f\\\ESC\172837 ^g\1015290\13337t\fvS\1043307\t\ENQF\r}\ESCP\144296<\ENQ)kD\99644\NAK\1067207@P\158876-J\11318\NUL\DC2R\52639\61640x\1000441\17876\ESC{Y\fk\n\37226\58197\ab\1032034u6\29086?5\ESC`\SI\ENQ`_\158422\&2d\RS~61Sk\178580\1068936K\50191\DLE\1019284\1080388\1107195\ETX\8366\134653v\DC1+\FS\1108302c8\42659\2331V\1104718\ETX\DC2h9\17336L\48192\&7\97754\154294)u#\NAK,\DEL)vl\1014830\&7Ogx\RSS\a\173846L~'g]\67981xG\995706j^\1102897\127370ZD('\DEL\n\DLE\DC35\1112268\185079L\1096532\1075622-z\f]la\1092147=]n#a\1038542\30579\1083984\vCPCRM\1042106\146305=uh2:Z\ETX\34750E\1105620Y[N\DC3\ESC\1110568\1066309_7\FS\1059513\&7\986868\&4x\nE\67808\29850\63016S\29795\be6\b1 F\bl\v\SUB\1045323\152687\ESC\DEL\128863C\ACKvX'/4\1091841\23510\151941+\996124$;+\DC3\174286/r'\1027484\v\983594H\1106500_M\EMUU^\1077134X=\EM0\ETX\ESC\1082555DQ\DC2\14716\133147=rm5Bv*\69735\1021551\171583" @@ -170,7 +168,7 @@ testObject_ProviderLogin_provider_16 :: ProviderLogin testObject_ProviderLogin_provider_16 = ProviderLogin { providerLoginEmail = - Email {emailLocal = "\1067205bp\STX.=d", emailDomain = "y\15356\DC3\161068\41681\21426\1020089Zq\128566\143938"}, + unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "\1036973\DLEv<\94969:C\1007217\&3\SO1_!\ESCP\t\180873JW?\97294\1017846\1045977l\1091653=\65478ct}sUS\1071996\EM\EOT\SYN\2051|/HR~.\1048900/N\f\\\141892\&4\32647t=CY\179433y\DC3\1054140p}_\r\rhEa\1075791c\f3\STX^@\GS\129362@\STX[=\ENQw\DC1\1029934\DC1\101063~\1092617\96064.\1089637\ESCn\36147.\1087267]6`A]\1050790pA\t\121086\NUL+\EOTMu\157233\ENQ\SI\1043849\ETX?&\FS\GSX\1100944dh6Fp\4861\DC4i\136377\1066303\&3Z7@8\ESC{\t\EOTQ4g\145660\65499)I\65609[t\EOT9\1026387w3\SOF<\NUL0\1071913~#\20452m6\61319I\CAN\1105033\988334-\ESC:76\DC4\US/?suL\1059445)x~\ETXn\nHI@b\1101299iT\988713%{a\52797!%\95084Ke\58514y\1087193>\1008548h*\74260Rs\ENQ[!R\EM\SI\129448a4\50351b\DEL=r<\SOHAQy_u\986104\NAK\73693\DLEj9,\RS\1008176\1060001[\71236)\EM\27772O\EOT\1029715\NAK\1067872\SUBm\ETBm;~!Nqx\r<%?\CAN\STXK[KxY5M?\DC3nS;\1039064\1056961@\143038\&1lb\40219-\f\USJZ\997750Ki\td\1091304i\132307[\100579~$#\SYN\SOHg\1921\1064317g\SYN9];|\a\189928bR\CAN\54684\143272\1035894\35517AlfL08\121168\DLE\US=.0\b%yk6n\169325\ENQ[\\\NAK\1032731\45474R\189972\179614[f\1038013\&5\NUL4f\13023\176583t\1100811\155909*\r/V$\1038174\44770\&8<\66811\132407-\b2\1066405\&6%f_-\1109010\&3a\ESC_\1110891\\u3XZ9 \187801Y5\1059719(\1006889R\b\1008505f\DEL\b\1031245z}\b\bY6UOz=\5767\DEL\1043399W,\1111327\DC4\1044326\66036\SYN>\1059628\92344\CAN\1114008Z\1076807\1019237g\SUB\1084387Z\t!Q46\DC2\68818jM\ETXc\1054316EZ\DEL\a\95416rcDK\SI\STX9\11372\1079523\bs\CAN}:\1101964\52216!gp\NAKz\NAK\119927\SO\62276L\1029468\no\97894;*E\a\15680gST\bj~\1071090d\1100387V\1015961Qf\1068607L\SOi\fY\190596\&6\GS<6N\"\a]\DEL,s9\1096598\1070844M\SI\990526k\US\32548E\1088460\RSTJkEc\FS\1012905\&4\1068530QE\1012911\24946\&8\92573\985406'\147964\SYN\1087141\a>?\v\a\r{rW,\1037280d\ENQ$2FA\1056946\USwL\30127!\993861\DLE\RScf\140888\NAK)uSz\1058795O#B3;\1035768rC\STXDA5\"\ACK \bj\DC1^\ACK\146554i\EOT\1108759BM]\CAN!\1111593\189909\rA&\r;GTE\CANz\EM6tQu\136534Rn\tT[\SOH\FSQn>\24878L\92573\1104026\FS#\180044&n\1033900\998864\ENQK\NULH#\DLEnpu\1057804Bt{X\FS2je@5\1047027\DEL\1035261\1071250;\"\1100173Q\n\99865\1085440\55133\DC1U~@:\US~\38191\nRLiD\1007697q\994233{9d\128484.\54287\NAK\b]HlZ\153287\t\26577\51191\CAN{3\1029087E;F8G0g\60158\1022972&'\1023598C\1054219\DC4i@v\CAN%[_g\1000893\&5\1087835+;.DtH\2792z|\1051376\1045157\DC4\1052468Z\fS\1029176\r\128163r~r^[\170410\17975o\SOH\25519_H+;\GS\127793\a&m\984047U8f%\FS\17902\37532Z\1029865\&8},P\1080560H\SUB\b\1054328H^\134222`gRf\133942\1092609+V\DC3\FS\SOH\128429#" @@ -188,7 +186,7 @@ testObject_ProviderLogin_provider_17 = testObject_ProviderLogin_provider_18 :: ProviderLogin testObject_ProviderLogin_provider_18 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "1+dHm*\71181b", emailDomain = ""}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "N\155960\EOTC9\DC4\DC1\n\170284\35034\22589\DEL/p\1091599\&8\vg\\\nYGz|/\aNd\1029498\57517\DC2%\53814o\SYN\SI\38380r\1084853\987326d\1109120o\1093352\DLEl5&}i\ESCg\1104181\6571'`8N\1097917@\136616\1008516Q[o\\\f\DEL\1104293\5068\ad~\42876\ACKK\42883snr]pLA\ACK\1069066\ACK\73854Xl,\DEL;\DELwz\EM\998652\1069068\SYNc\NAKA7q\DC3\GS\53763ft\183331f\182272\\\1016358\1107081\SOH\4988\&9F\1036590\156517D\1066242\NULz\1030228\151834\&4\r\1072124\1071359\1036618\SIO\CAN\EOTY9\48690C0|8\168539]n\140134\SI\1024618\60889ra\ENQ\1076182\n]J\GSB\62239\64626\ETXgT\95776P\EOT>t\64744\a\CANr\t\1009997;Yy\NUL\EM\1071471Qz\SUB-'\ETB*\SOH;J4]9\182652\ESC\137920[z\40314=cpG_;\EOTu\1078222ic\EOT\vkz\SUB|}\1011861\v]\20796G3\f\f0\DLEd\v,\1080412\17244[#\57647~8OG`n)\1105707\&5\143076\SYN\67623wK\SOH;\DC1+[\1090978'\1013253\3118\SO\27597-\tB\1061075h\vVl\n$\RS\ETB\ESCw^!\DLE\1104169\DC3\986109\&3j\FS\149241\&8oj\96002\DEL\b\1010145t4>:\35038_\EOT\CAN\135636F\1027492lup\ETX+n\SOHP'yV;:\170481V\DC1~ 1\1007357\1103037A-\"&8}\t\DEL=\EM\SOH\NAK\DC3F\986041\1097800\NAK\54547/BI\5055,#\EM,\1036876gp\RSw][\DC2>\v*b\53753,\SO\EM\STX\69989\325CuTd\178982\&27B\1017690oRm$\157877Y3\n'?\SI\v\nf\175460K\SOH6\ESCl]`U)m\18666" @@ -197,7 +195,7 @@ testObject_ProviderLogin_provider_18 = testObject_ProviderLogin_provider_19 :: ProviderLogin testObject_ProviderLogin_provider_19 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "W\1036612`)SR\STX,Zw\n", emailDomain = ""}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "yMl\64761\&0~\1050682\1021006a\1094043\fQ\94632~\\y\1074075\EM\ETXrqFsvG%So\ACKaTH,\f\STXP\100229\"\DC4o;UE\a\SYN\54275\DC2r`\vu1)2\DC3j*\"c9\988223\1066302)h#\164540m*j\DC3\44782~v+@\v\n5*ui\17305uS\1003317\1052173sqM\STX\1032754\FS\187251\156456fG\156207tv}\44129?V\r\1051429]\1014021\ETB\DLE*E+V}\155873\&9\EM*\1015779[o\17882\1106946\vr2\a\1067600\ENQ \47429@O\30164^\1059677R\42526!HLT\1018705.\1046981\1079471\GS=+D\160272\&4|\1023902T\NUL\111266\ETB\1028696\1054400w\1093841#\1038538b\EMF\a3k\CAN*#\989166\&3YKs\DEL\ACK\SUB\ACK\140776D\n\1089340(\EOT\1083765&/u\13397W@)pLY\987574)<{\GS[LkCU|\\\t\CAN\DC3Q\SI\GSS\DC2\1082373\1055041\STX\DLE\1005747\&3655H%\ETB\1002444\\k\150559xl\GSAf\CANy\NAK\1398\157796\1004189\auH\ESC\60829y0(\14035\986900q\99290\ENQ\1015592\SUB6l\GS\1101212H5\SIX&a1j#e\EOTgf6\1099735{\1100353\ACKa{\r\1075038\SUB\1021868Ih\v4\62793\13891\1065696\aSq\54460\135283B #\1022849\984792%1&\5746\1101604\NULW\NUL\1005888\v\12495,@p\996704_s\1005516\195083:\1053163\26817\&3m\1073977\v_\1030132\SYNL>\FSq_1\ETX\1014033I6L\ENQ\78160\995216\994139Y\64467zH\USu\1060134\1008128n\SYNvA\ETBG(\t~\GST\38349N\US\1055431bju\RS\164557Z\133252P \4977\rq\ACK\"K\SOH\DC4\aqcJ\SI{\171894\1057036K\NUL\175514F\1063815,\34846\SUB\ETXW\DC4\49752\ACKN\EOTLAE#\1052903hIWO\vj\125250:cP~q\1007541\ENQU4\32477\2386*z\EOT){\NUL.T\1065030=\176972\188734\1112137]\n\50074\&4s\f\1011957\140326+\52789\"Z\DC1oN2\174181\FSoee\DC1wD\FS\SOZ\165295\&4\b\NUL\r\917953\137757\DC4:7\1034978" @@ -206,7 +204,7 @@ testObject_ProviderLogin_provider_19 = testObject_ProviderLogin_provider_20 :: ProviderLogin testObject_ProviderLogin_provider_20 = ProviderLogin - { providerLoginEmail = Email {emailLocal = "#\4241|\18697\1075733", emailDomain = "\\\ETX\1091078A \ETXDT\r<"}, + { providerLoginEmail = unsafeEmailAddress "some" "example", providerLoginPassword = plainTextPassword6Unsafe "y\SUBWK\1113065\US:d\996680\&1\1043738d\n]\29018\SOH\1033033Q\SUB\DLE\1049295\CAN\984477#7$\1067481'i2/%u84\RS*\989691\1039072\&4^BB?Ox\41256sW\r\1111532\&2\NAKF.\1058002I\13200\&8\STX\ETX`\GS!\1051949<5Y\1053480d\1029498P\1085303P\1110913?1\1100218n\NAKa\191218-\169668\9581\SOHc\FSne_\1089495\1073819c7\9454Yoe\NUL_>\178568\NAK\NULN\120250\1017029J\SUB\9102OkZ~yV$\SOHR\NULQ\1084924[\1072369\1103823?\RS\23331\1006369t`\3663}t\58854^p\GS\20579\1083297qA\bR^\54231\1070382f\132642qZr\ETXK\ENQ1\SUB$\DEL\1048222\1041456\1057367\1015965\v_\63061C\SUBb[g\156231i\1063061j%jZ9L\SI6\1092470l~\t{}>N\1028038\1093894;\CAN\1012890#\DC3:\f\EOT.\1034408|\999878%iMr\132269:\ETX\r\1059952\ETBB\1030258;\SOT_KJ=b0\1002499/u\SOq\fcV[h\146171}`\NAKN\DLEP\129031H\CAN:F\49767\988419+\1037039%2}|s1\49683[Rp\SOHO\t_E\rK\SUB|\SOH\DC3x$\1020540\1083269.\64699\ESCZ\69926\SOHU`\t^\t0'8\"O\SUB\1048972\STXnbGo3_\1056648\1083755\995789\SYN.%\RS\30271\1091646Q\GSD:gJ=Q-0\1113951q\STXx\ETXr\ENQT?\ETX+$\29558>\1103438\16152\396\f\146489Qj\SI\1067862Fs.\SO\118908Y.\1023882J\33637\f\154768A\156772\5117P5\FS\ENQ\118931\SOH\SOH\1059107\1064213\17324\1061449/x?\SYNI\61424\185549\NAKU\1007536\14119Ig\145126L]^.\166857}&(\188383\188556vGg3\1026179\SOl\EOT\142432\RS\fs\164737p5LS\998282\SOH|\nvm6#\1103808\1103698\GS\DC1\RS\b\157935\1006286\SOH\ESC\184018\1053117bE\RS\ACK`wj#\1014169>Q\US\1051484ss\150964\1092969ck%>Q\3766B$\STX&\28586%\UST\US\ETB:B\ESC\1025229\bC\185625h;O:?\v\1035263j\136984j\1011335;t\1080482\t\SO@\4855\1035237p\190479\a\996213\15858@,\169117duBfR\DLE\72879!\ENQ<\DC2\SOj\1080800XFs\1037447'W\NUL@SDA\1088037" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderProfile_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderProfile_provider.hs index d94ab7f9b68..6f4d98b170e 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderProfile_provider.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ProviderProfile_provider.hs @@ -54,7 +54,7 @@ import Wire.API.Provider ), ProviderProfile (..), ) -import Wire.API.User.Identity (Email (Email, emailDomain, emailLocal)) +import Wire.API.User.Identity import Wire.API.User.Profile (Name (Name, fromName)) testObject_ProviderProfile_provider_1 :: ProviderProfile @@ -63,7 +63,7 @@ testObject_ProviderProfile_provider_1 = ( Provider { providerId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000700000006")), providerName = Name {fromName = "\46338\DC4"}, - providerEmail = Email {emailLocal = "OR\32966c", emailDomain = "\RS\ENQr"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -94,7 +94,7 @@ testObject_ProviderProfile_provider_2 = { fromName = "?\1050399\62357\12541$?\150548uTY\1101349fH\ETB\STX\ENQ\b\DLE%:!Y\ETB\92301\53905\1096036\1012090*]/Z\1050093>-\EOT\1041175\1025575!_*--7\SItEg\t\1028966\DC3\1079962\CANvE\DLE\134924?=\SO\1026118\40813\167977O\24641k\NUL\1019104\32399.\SI" }, - providerEmail = Email {emailLocal = "", emailDomain = "_ \1060474\990125"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -156,7 +156,7 @@ testObject_ProviderProfile_provider_4 = { fromName = "\SO\1046147n\1016911\&7f\1077840i\SI8|\STXe\nN~$[vAU\62541r1`\NAK\f/\b~\1084745PEhV={\1037388\160696\f\EM\1063647}}\3137x\994880\994942\1069553%\foA\50458\98884~t\182452\12080\t\1073906\rWA\186565\1104351t" }, - providerEmail = Email {emailLocal = "h\161768\t\1097554G", emailDomain = "\134955/\DC4"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -183,7 +183,7 @@ testObject_ProviderProfile_provider_5 = ( Provider { providerId = Id (fromJust (UUID.fromString "00000003-0000-0007-0000-000700000003")), providerName = Name {fromName = "\6923gr\n\35429-\37180f\fJ9\RSl)\f\20518_H^Xh\bA;O|"}, - providerEmail = Email {emailLocal = "%>", emailDomain = "\1075658\17096q"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -214,7 +214,7 @@ testObject_ProviderProfile_provider_6 = { fromName = "\DC3 &3\DLE\n\163723'\65487\&7\7618\ESCEwP\\\125089\DC2^\"\1023814\1002704\DC3\DEL-g\29654<\v\4324hAjOZ)\1045139W_\154260\135873s|+\1030412\"~D\1039156C\DLE\ETX\95249\ETBw\SOH\DC191\"\"6D\b\DLE\NUL.PC\RS\SO\1094846\1044317<\171750iuN\182436\1088261U{wgq\FSD\v\1034790\SUB\"\nw{Rl\ACKUa3\RSNx\SI" }, - providerEmail = Email {emailLocal = "\142265.\EMk\1035106", emailDomain = "n}\190773\n"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -338,7 +338,7 @@ testObject_ProviderProfile_provider_10 = { fromName = "\1027347|\US\187412-C:\v\CAN\1007173;|\DC3d\ACK>@\95987\165903\&2\DLE\138359\SUBM7/\1069218b\ACKO3m[{" }, - providerEmail = Email {emailLocal = "gd\1046608\1072562e", emailDomain = ""}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -369,7 +369,7 @@ testObject_ProviderProfile_provider_11 = { fromName = "T\35190nJwq\65943[\FSV\DC2I\179267\SOH\ENQJ:\"ay\1021260\998962\1026006L\SOH&%lT[l/?\1044443_\DLErW\1012807\1017169]\137723\1082379\83105wM\DC4#\39095" }, - providerEmail = Email {emailLocal = "", emailDomain = "W"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -400,7 +400,7 @@ testObject_ProviderProfile_provider_12 = { fromName = "h-\rE,\148173 \1088186t\DC3S\EM\14287&8Nf\EOTE$;;\163703\SO\SYN\191282D,pE\STX?'X*\STX\DC1>&\1103170WCGM=Ey\1088250,\44485$" }, - providerEmail = Email {emailLocal = "Z\148819", emailDomain = "!\1045867\69665\f\23358"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -431,7 +431,7 @@ testObject_ProviderProfile_provider_13 = { fromName = "r\1096559\DC2%_izF\33135e\46380\DC1/\r`u\1022998\a\SIf\60524\1098075f\1073391oP\EM&\131116\SUB\1059302\1108967jY\992453\1111715\ETXd\1063946e\1001823HK\129359\ETXy\1106634TE\SOH?\148357 \ENQ\ESC\1015779q#\SO\"(Q^\DEL\183337&\SYN\18804\DELPI]Q\"X\SUB\14938\145510x '" }, - providerEmail = Email {emailLocal = "\SOH#\134551", emailDomain = "u\SO\19353"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -458,7 +458,7 @@ testObject_ProviderProfile_provider_14 = ( Provider { providerId = Id (fromJust (UUID.fromString "00000007-0000-0006-0000-000700000007")), providerName = Name {fromName = "\SOH4\ENQ\ACK>\rx~J$k!~\t\DC14\985222\DLE\ETB\r\ETBy!9"}, - providerEmail = Email {emailLocal = "<", emailDomain = "M\SI"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -489,7 +489,7 @@ testObject_ProviderProfile_provider_15 = { fromName = "uU1;\34969b \ENQ%\1090011\51919\64324\&2f\1054192\b\1076489\DC40({\983593\1096114\997924}z\168790Lq\STX5.\STX\1092385s\1024579[\t\CANE\67601\95200W\1105521'\1036690_\1103544\&6g\a\160335\1033905X5j\1041586Q\100988\53621o/<\DC3wm\1069822'\135972F=\167089d\164390da\1010656\&6fbnN\ETB=\1062861\ETBx\v?{3\46096u\154857K\153847\26427" }, - providerEmail = Email {emailLocal = "\2355\CAN:c5", emailDomain = "G\1049614E`"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -520,7 +520,7 @@ testObject_ProviderProfile_provider_16 = { fromName = "sl\n\1104119\SUB\989977\a\143145\1017719>\DC4\SO\1016279\160098\&7\DELm7\NUL\35472\RS\54106uXwwA\1062534!\156472\EMN\1082758\164617\5214\ESCEZa\1030079\186679ZWY\189148|&\21785ikM\SUB\1061267\1056212\160249\988858\1020580K\SOHY:\ESC*Wzc\"\ACK\1038549\1092558\rWB8jSl\GS\EM\1003586\23627\STXf`\132324LN\nje" }, - providerEmail = Email {emailLocal = "\180899", emailDomain = "\34121z\16843'"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -547,7 +547,7 @@ testObject_ProviderProfile_provider_17 = ( Provider { providerId = Id (fromJust (UUID.fromString "00000008-0000-0000-0000-000400000006")), providerName = Name {fromName = "\37146|_;\1090300\48254\STX4/\13124yqDttZ\SUB\1065843y\17715\177370"}, - providerEmail = Email {emailLocal = "X\1050408J1\SYN", emailDomain = "\1024482%"}, + providerEmail = unsafeEmailAddress "some" "example", providerUrl = coerce URI @@ -578,7 +578,7 @@ testObject_ProviderProfile_provider_18 = { fromName = "~\US;@\1081284Oa\184911\DC2\SOHnw0\DC3Y\1044296.Qn\1111681\1078852\na>\ENQ<\1008904g\DC3\1017402x\1051129*8-T*\ACK'\NAK[/\1043140g\142008VT\EOT\12290,\179242\1069014kC\98612s\DLE+\GSz\78289\1040663`l" }, - providerEmail = Email {emailLocal = "", emailDomain = "\bU\SUB\62879\58674\148214G\FS\DLEK\44599tO\1109580i\GS_v\\\CAN\1018104\DC18-Z$Z\NAK\r\1101550a\RS%\NUL:\188721\47674\157548?e]\ETX \142608 C\SOH\SIS%8m\1091987V\147131[\1006262\&6\171610\1011219\164656SX\n%\1061259*>\t+\132427Y\989558\993346\GSU\1067541\&6TU!*\40114\&90\1055516\RSV\162483N\t*\EOT{I<\1084278\SOH\183116!c\\\n\1107501\183146\DC1,-xX\EMV?\t\168648\1054239\DC2\DEL1\SOHu\SOH\63459\53061\SO+h\ACK::\RS\21356_g,\SO*\v\DC4\1093710HFF\188918\1081075fF\ESC2\SOHT\DC1)\fc\35905l\1061547\f#~\STX]\1035086/Or)kY\1031423\SOHNCk\1067954\&5\1083470x=H\NUL\23760\1058646\1099097E/$\DELpbi\137522\FSKi\15676\1018134\t7\"OL\54208\7516\&5\43466\NUL(\1030852\166514\SOH\149343\994835\25513C==\GSTV3\DELl6\999006.Z)$\16723|\172732\1090303J;O\GSbw\vI\1101024I\SYN\DC2^\149630\STX3%i\EMW\138614\DC4\1113619tsL5\147087W\96700(_,\1091179*\1041287rckx\SOH\SIs\SOHJd\140574\SYNev.\DC4\DLE\99082.\1106785\996992\143448\US_\ETBf\STX\SO\DC3\1043748\&6O\DC1Q\SOH'\GS,|]W\SIa\62568\151062.\v\aH&-L\DC2+\147179\1095524\EOTm)\19925\181147\183368!\185223\142946m\DC4\DC3\1034282m\GS\185509>>\"NDw\1076877hY\1033831sFKz^ \1108187\&5Qec\NAK}|\1108194.Q\173114imb\1027220 p;\1089082\SYN\1065748kF\1102854r8o\DC1" ) @@ -62,7 +62,7 @@ testObject_Login_user_3 = testObject_Login_user_4 :: Login testObject_Login_user_4 = MkLogin - (LoginByEmail (Email {emailLocal = "BG", emailDomain = "\12137c\v}\SIL$_"})) + (LoginByEmail (unsafeEmailAddress "some" "example")) ( plainTextPassword6Unsafe "&\991818\1023244\83352\STXJ<-~\STX>\v\74228\151871\&5QN\53968\166184ql\NAK\74290\&3}{\DC3\173242S\22739;\t7\183958_F~D*f\1049940)\1067330-9\20699\&7GK= %\RS@kOF#\179945\1094401\124994\&8_\42309\GSL\37698\ETX\1047946\&0Wl1A`LYz\USy\20728\SUBo\ESC[\DC4\bt\66640a\ETXs~\USF\175140G`$\vG\DC1\1044421\128611/\1014458C>\SI" ) @@ -72,7 +72,7 @@ testObject_Login_user_4 = testObject_Login_user_5 :: Login testObject_Login_user_5 = MkLogin - (LoginByEmail (Email {emailLocal = "", emailDomain = "~^G\1075856\\"})) + (LoginByEmail (unsafeEmailAddress "some" "example")) ( plainTextPassword6Unsafe "z>\1088515\1024903/\137135\1092812\b%$\1037736\143620:}\t\CAN\1058585\1044157)\12957\1005180s\1006270\CAN}\40034\EM[\41342\vX#VG,df4\141493\&8m5\46365OTK\144460\37582\DEL\44719\9670Z\"ZS\ESCms|[Q%\1088673\ENQW\\\1000857C\185096+\1070458\4114\17825v\180321\41886){\1028513\DEL\143570f\187156}:X-\b2N\EM\USl\127906\49608Y\1071393\1012763r2.1\49912\EOT+\137561\DC3\145480]'\1028275s\997684\42805.}\185059o\992118X\132901\11013\r\SUBNq6\1019605'\fd\RS\14503\1097628,:%\t\151916\73955QD\1086880\ESC(q4KDQ2zcI\DLE>\EM5\993596\&1\fBkd\DC3\ACK:F:\EOT\100901\11650O N\FS,N\1054390\1000247[h\DEL9\5932:xZ=\f\1085312\DC3u\RS\fe#\SUB^$lkx\32804 \rr\SUBJ\1013606\1017057\FSR][_5\NAK\58351\11748\35779\&5\24821\1055669\996852\37445K!\1052768eRR%\32108+h~1\993198\35871lTzS$\DLE\1060275\"*\1086839pmRE\DC3(\US^\8047Jc\10129\1071815i\n+G$|\993993\156283g\FS\fgU3Y\119068\ACKf)\1093562\SYN\78340\1100638/\NULPi\43622{\1048095j\1083269\FS9\132797\1024684\32713w$\45599\126246)Si\167172\29311FX\1057490j{`\44452`\999383\159809\&4u%\1070378P*\1057403\25422\DELC\RSR\SYN-\51098\1011541g\68666:S>c\15266\132940\DLEY\1066831~a)YW_J\1063076P\a+ U\1084883j\EMk\SOH\1096984\DC1\18679e\172760\175328,\5135g@\DC2\GSHXl.\ETB\153793\&2\DC3mY\1054891\tv?L8L\1074044N\133565\nb1j\1044024\148213xfQ=\\\ENQe\995818\1023862U\DC2p{\SO\1099404jd^@U\994269tP.\DC2Y%R`a\r\160622\&7}HnUf\132856m^7:\NAK=\52348>l\95313hwp27\149950jE\fx=!.\DC3]Ar\tw\DC4&\SUBk\194572s\1042820\4498I\146071\61461\1060645dsY\DLE\181922dX.\146295i]\151113\1028288\rWS\USU\1098732\SUB\49884\1083906\DLE\STXN~-\SO6\190031\1110322\\O\185165Jc\1052359\1071278\NULHSo\DLE-W\DC36\170321I\1068712)\99800={\99796h\27961\61707M\1022570FwJQ\1111976ck\SUB\CAN|UV-\NAK\SOH|\DC4;\f\156907\145795\ENQS\NAK.B\"D\163007#o*\126577\32988m\RS\1049834B3Gg;\DC1\\\180659\1098926\ENQ B^\SI\152630$e\39220\170037>fMgC\187276,o\128488\\?\1033955~/s\SOH?MMc;D18Ne\EOT\CAN)*\STX\GS\16268}\u00195z𑈣\t>w\u000e󵾱" } diff --git a/libs/wire-api/test/golden/testObject_Activate_user_2.json b/libs/wire-api/test/golden/testObject_Activate_user_2.json index 8975d735dc0..849cc2f3b8a 100644 --- a/libs/wire-api/test/golden/testObject_Activate_user_2.json +++ b/libs/wire-api/test/golden/testObject_Activate_user_2.json @@ -1,5 +1,5 @@ { "code": "", "dryrun": false, - "email": "󴴺\u0000􆞵@k\\\u0001a\u0016*𫅳" + "email": "valid1j28hfna@iagh28nuwkas" } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_1.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_1.json index 1506d784408..5af83770fe9 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_1.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_1.json @@ -1,5 +1,5 @@ { - "email": "𨠞\rZ\u0007\u001b@p𠋁", + "email": "some@example", "first": false, "sso_id": { "subject": "me@example.com", diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_10.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_10.json index b98164f3fec..db708bf2a76 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_10.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_10.json @@ -1,4 +1,4 @@ { - "email": "\u00063@\u000c󾇏􅞻\u0004󴼐P", + "email": "some@example", "first": false } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_2.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_2.json index 7f4dc0a99de..db708bf2a76 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_2.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_2.json @@ -1,4 +1,4 @@ { - "email": "foo@example.com", + "email": "some@example", "first": false } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_3.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_3.json index 06c370721d9..db708bf2a76 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_3.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_3.json @@ -1,4 +1,4 @@ { - "email": "✯*;'R\u0019\u000f󼇭󾌏@Gw:[T8蚅", + "email": "some@example", "first": false } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_4.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_4.json index 2fc240718b3..aebb0287470 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_4.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_4.json @@ -1,4 +1,4 @@ { - "email": "h\nPr3@", + "email": "some@example", "first": true } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_5.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_5.json index f49cd246eb0..db708bf2a76 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_5.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_5.json @@ -1,4 +1,4 @@ { - "email": "7󾚲m𗑀\u0008􌐍@AJX*s&𪐽󱛆p", + "email": "some@example", "first": false } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_7.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_7.json index 1ffefc7f84b..aebb0287470 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_7.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_7.json @@ -1,4 +1,4 @@ { - "email": "𘅮@", + "email": "some@example", "first": true } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_8.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_8.json index 38b2903f340..aebb0287470 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_8.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_8.json @@ -1,4 +1,4 @@ { - "email": "bar@example.com", + "email": "some@example", "first": true } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_9.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_9.json index 83a3641e055..db708bf2a76 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_9.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_9.json @@ -1,4 +1,4 @@ { - "email": "\u0005?@", + "email": "some@example", "first": false } diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_1.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_1.json index 7328dbef7ee..9e0a77129bd 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_1.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_1.json @@ -1,5 +1,5 @@ { "code": "uLtYG9FEhpfNHht=ndxYbhEsOaZJJTPjEsBsnJt0UngpmW5OvqpW2F9E5VuFikdraC8s1xMQs9yOzKlgdV4rf371UTMjWzc59HdqfqFDx8=ARnQtIJ8VyAnd784fYJv2A=IiSQJhfc=tc7UKa4n_TN9Hq7BvAQ0YLBFuCRsH1cBsr35-I0aEKev_48AFCC2r2LceURv7tsXpODU=pjneuYFQSD2u8GmiQ3NxRqJEiIfvj3IE_S2gFTqb3Qod=rvVoT7yAejNg=F89T6bacNnzM-sdRhB7ZoQrQYQRc7j7d_1hDOzKsmkBVqpZ3466SwlHld09GyIAYBOo7TipyvgBENFlXnor2sPS2TwCtzmMdyMxhEt780DAdUgiasCsS08_rFrx3j8_wNCBzYsWRTYi7LSaY_IxpcH-mOkH86L=8SAMcCs_pJpKsoWa1EY4Ep0h8jTspHT-6tKd2s0gT_v5GvTPEg8BZyz04gt6I5JgdSrOJ1A0=w_zy4O-KSS-ba73v2v4p3x-N19X88brW3VCwbqgS_G3DAMDEr76Ekn7q0UMAd2MR13SgKWjM35lFtS6vN6b5a4QVqIxOqAvA2EPHV2UY4zGhJsgl7KpgtCzUMKIl-mTyjXP_a_c9y0uu9u6I", - "email": "\u0002Q=萱k@", + "email": "some@example", "password": "Tu􆢵8\u0013\"9\u000cB]𫧆\u0010<􈋋.C<6W sgS8_􈓌\n\nD$A\u0010鰞I\u0003󳋷跌\u001d>𒑖S𒅘EI_󴽘 a[𤊰󲼶𝄖\u00170﮽\t󺉨~죻$`\u0011𠘄y.Q)\u0016a󵚭\u000f^𩗉󰑹+\u0008E𢷞P\u000f\u001a\u000eg}>🤷>\u0015&HS𫭱󱙼M\u0010\"~?󺜵gd刍\u0012\\)􃄘F\u001fI􎟛V🌘\u0006𮨕[T5'wP\u000f\u0014!y+d\u001f𬒦󶌬󴼺\u0007h\u00122r\u0016~󽇘I\u0016YO\u0004􋯶sT𢍨6q\u0010Xjp(ᰛ\u000ec\u001c\u0007􆇮fc>8?Sy\u001c\u000f몋\nN\u000b&O俶􀟉\u0006/\u00178𮜹􋷬𩌣𗽺k笷𗖩b\u0017]\\\u0018\u0011;3 j3F+􉓛.0冀(p􅅽H\u0017j⾙d\u000e𢽵W] Hz@Eb􄂄zW}\u001f칛!\u0016;󶶏T\u0013![*BS􅌧w]쎡3\u0006􀴕𫐚G.A\u0019}%\u0014\u000bk𪅓\u0013Tf\u0011\u0003󶔜\u0004&󳖞o.ﻧE?jK|6z􇭾BC\u0011ዏ𮔟!>\u0001󱊸\t;Y廒\u0001#𝨄\u0016`w%\u0002\u0018𢌉zT􏲈2􏬙\u0018>Ü𗋢𧔨&7 8U\u001e4籢􉓺􀃫--\"\u0017" } diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_12.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_12.json index 797d2e40a6f..016789da3a9 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_12.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_12.json @@ -1,5 +1,5 @@ { "code": "GkNBvt5WkpZiqOtxpVKuBy8dXcbWuV8x4ejoV3EHdIAU=fZo3d_PjWQ36EzyO9eGbt5F8oQ=7vBzrTr9dpeETyJQWi9Vu38Efi7Dz-zsBvBp9p=AszTX69gzjPQ-xgcPvCw2Kvv6EStPojy", - "email": "(𢶈\u0019=]=\u0007@𪸹\u0016\u0008\nﮛ\u000b&􈝾", + "email": "some@example", "password": "\u00118莱\u0010\u0011꡶~KSy0􌍉\u001f𥌴lR􍴸+!\u0018\u0018[kv\u0016<\u0018􀘑kW󴚛𧽺+刻i[癭e2[u#\u0018 " } diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_14.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_14.json index 6d6080bb458..c64db5a3a88 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_14.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_14.json @@ -1,5 +1,5 @@ { "code": "FSTMhXuS1rYF_f_3aJfy8sn7CaY7BMCg6onJCAqtnt54fEvCkS40ml06ufrX9wvy192yCErw5Xei33_FoSQmC0RAjRN9eLFSBq15MclWbPrIsrwluYCiLmIB72IaR7ig8xGPv3-H8v=J_5xfvvpYRYSFZMZvTwTHKqaRL_uF8r=JULb6AQnLUG6__-nBrCq=91TRJ26VknMDuFrk-0Tfu72OJ73LrGfJqmWCR7gcFeyACyR17n3FI4GQquQ5Bb5qbfl5KZc7W_E3H=5sScZCa9r2Hj9ot5noSq-9nq2NlptoDc4mYTaWklhfbNCT8Wn2=3T8GfAx9nYW__2ZyAPlW9NPmbRSj5FYqqJAprLVa4GrT=PELXTFIba3inReJYtM4thgQ2LAgZYew4L0YGpIMOgr=uFKs3I3u4Bgd_77uNR-wayH3ENL0A97aV7p9DLLC6A2FeVugc2jMn1wViS06PkxJoM5ZtGZkibUTuycstG3VmGtC8ZMR3q2lAVNsfsiugBUZLg=MtzPz2Pqe=QaxCNq5N04ekL", - "email": "󿦘Q?􇩑􌾱@𫪞󴩴䤐", + "email": "some@example", "password": "A􈉖9YM4fO􊈾s􋍂\u0005\u0012\u00081𫸍\u0000#􉥳𬶼󸻰f󾏞ញ,z$h#\u000c\u0003࣫\u001f5󳑍识.𩹯 􄳩\tiW\u001a𭳌V\u001a5\u001b\u001a\u0001􇱒\u0013\u001df-J𡯠}0\u0012(&R*}𦗂6\u0019\u0012$\u0012e\u0017𪎏@ 󰺑M5N狭􆇳O㒅\u000f:]䝃\u0003󻻌됣*󸎅qGﴬ<&􏭝罺\u0006􄓃\u001b{\r󵼱l󲎿\\\u0007\u0019\t.𨩢󿸈\u0006{4g\u0005\u0017\u0005𝔎\u0018)a{=J#XSJ\t戍/\u000cu󷙞v2YZY-k$ #4𥵽^\\􎊶\u0013>b/5-Z}𢬢\r>󼇈\u0003\u0008\u001cRS9\u000bE&\u0018\u000fz#m5z\u0006\u0012pe𧱻0𩭠픝Q𣗚f,sH\u0015\u0013@󱯄󳴈&l󻅮\u0015\u0005.f$s\u000c\u0007\u0000趌F\"\u001d\u0011\u0005H\r𢾱j󰯜?\r𝀿𗎡lf􏗑􈩇s'󹭞􊪜𭬕t-\u0007sh\u001f󿼛w󶦖\u001bj𡘝L􅎿~H\u0017-🖎\u0019vml󷚍Eug\u000e\u000bDTR\u0005TO\u0016󷞟|\u0005􌺊\u0001\\\u00040Q*𨖃Ly4󵦨It恭w" } diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_15.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_15.json index c0b383904c7..99e2dadb4d5 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_15.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_15.json @@ -1,5 +1,5 @@ { "code": "hmgODqiry_V_t87ih4Ezo7GS8C38DYKENIE2t5nRiJMdagPBW-lTEhID3_8_ApDfxAfSNxAF03y2L8MCLqWWsX_wxkaLYtAI39FLtZZAwxHkSRRazNp7LAc_3QzGXR4O_iFiCqo0f3ZbmODskuoeNVUGBBPJhQ=uw1yVKyMVHojWD16khERjcHww2=hSmqUdh3W-46WPWaZe7IRN0_gk_UaBGwdMb4aDcTHJ6jIaTfQ58djcLVGrKpuO1xO=eQ2BjLJiK6Ik30JgICpvS5ZuumMjgkNFKtHwCu0C-E-oUDUmi3sWKkFQPCxpIy0Ol0SAyN2llCWAADjTR6SW-zRT4qDQNbtDe8nKWpJxZYjFj=IvyBHaK1q6NjPsrXQBEUfajtkh7OwbQwqOOBk5nt8RPP7xwUewzHEtkQUJUjbgGh80nuOdC7sMa2zOSEOy33oC1bjncA23BsaJoisQbFfju_UWiCSyDD-oUXsWkKR1cMGmwyVpf1IpZRnQq_8dwpgMKL4j4ehPxPrVBefQPmzdoK4nncLDB_zDKBBn4M5nbqDsLmO9OqSKeDH6tg=uKTaftrDK2w6Mhfo_fSZOsJAEouS02TJwr6vE_VlJbiOCPysMdVmCdn6Ai2n-p_WlwFoBIHLPkVnx7yYyskHuUMhYQfaq8=CHCwa8CDyOGu=cZVxOd6mTHRD=mXc2_cgkYJ94pdZOL0", - "email": "6\u000bF\u0004]\u001b􉡴.'@JEe􊐼􈼡2dK󳘱", + "email": "some@example", "password": "\u000e䆛@?󼡏\u0013\tMt\u001d\u001cD!Y𦷤􈭕9\u0013𓍟􌈯􉕺 _P󶐹/3𫑳bZ7鑴.u\u000ej㒛[𒎊\u0014𣺨%7Y󲝈󲷳)ﳀh𧁁\u000cYt𨥔󼽈#%y\u0003O\u0000옛Qo%E]𬫪6컅)\u0018\u0016,􂲁󸡜􈴸󻣝􎝀>砯\u0006dSfz\u001cX󶿪TU∃jM𧟥S🅞\u0014qE\u001cJ\u0016\u0000􁧥\u001aFg9󰘵x󽠏􏞞\u000f\u00019\u0014􌞒􀔶厡󽭶aSIN1s\t🜯\u001c y\u0008\u0002+\\G𬄫⪕9희 M#|=Bhd\u000f\u001et{a\u0002Az.𫮗㥬\"􏠤\r�I[\u0007T\u0017`ㄌ~3􉙟\u000fn󲸣}c;􀟺9\u000f󰡣/n󿾴5-\u0006Z=󻩎35𣮯]B\u0008\u001c7\u0005\u0010Gl'y\u0015b|4󱦦=\u0010g*y􂃡󰈗3Ej\u001e􏥯􉫯:𨵈􍥧p\u0005V\u0016𖧰\u001a\u0007DBA\u0018􁷡𨥬𬠨i_x𡰊#\u000bh5􈹓d􍺲𮜑`𮢊\u0011h\u0019K\nW\u0007ꊊ⢨Y\u000e#3`\u001f\u0018l􃁞\u001a[pr\u000c\u001fX)\u0018\u001a􍊩\u00183NrV3\r}\r𨉦\n\u0012h\\􀍂󼧸=쁐%*^|vp咝ix\u0011\u001bi𩂎78\u0007𓇉 g󹢷`\t?󸹮'W=􈮵\u001ej0\u0016k`Y\u0006\u0001\u000f4!𭮤𫕼90󸒿𭾭䙣FD!􆫐&𦩫)F\u0017&𣏩\u0007쟼\u0007K].^9~\u0012J6󷕕7𥴀}\n]W𫔟0e\u0005L𦕽a\"䗒ꈵ;\u00198ꃽ]J,m>\u0008\u000b^󹁲j60G󳟰1fMGY;[<2q󸡫􍗋󸌪O\u0006\u0014. +Q𣴻L\"\u0000QQ􈃎pfK𦅹.i􏅥\u0006\u0016𣊧g4Z\u001b􋄞yh\u0017 􍸱껷\u0012,\u000b}\u000f\u0015oSV𭉆lUZ?Fi^H𘖧\u001e􎞳oR\\\u000f'b\u0013a\u0002\u000eT\u001f𭀳Xm\u0004󹑸{Y\u000fQDO1OZ@􋯉􌋨)􂱾V\\F\u0010󵥀􃺚{0D\u0008j\u0012l-\u0003󿥵󻌷􊍨禲O\u0019a􍄩Jẻ^\u0018?:󾌫󳙷󻽜gz𣮸\u0014瘷\u0002🅈􁒊]N`􏼢𡷈\u0016t2nqJ!p\u000bI𖡛H\"5\u0011y7l􂯮EZ|kA\t㲺l\u001c7t\n\u0001?t􏥙\u0019.𮭖zJ󱦌󿀙WA{􁕵􊥫m\r\u000b4:㫡{􇆓\u0005권祱;i雐𤊔􈭌\u0019~󻨇𗑊\n,y7D\u0004⎭\u0010􀢷Zo􏵲MUD𣼄􆇆\u0011|'Q뎜\n2󴑥𢊗Nk󹽩@\u000eDia-0\tD\u0011󼻃􂇵\u0007*L􍿧\u001c\u0019󻨡\n\u00059󾒴\u001eF\u001dK𠯙\u0000\rA_􎜼{\\\"𔓷󱩯T@𗉉\u001bV戴󷝔l\u0012Ea🄵􉐫\u0017z7󵯓P`䙂\u000f𔘢S`u󿅋\n􉌯t\\wfㅐm\u0019 󶋪􅌲R􄄦\">[N\n\"ǧIQ\u001e2s\u0003J6\u001cU%錬HnDm\u0002@\u0004T 檻imm\u000ek\u0000J?*\u0002\u0012+hhIj$2𔓼𩜤􎍨\r.􎈨𣎱􌕭+o\u001ag􊕺o\\\u0007,\u0002\\𧣡\u001a󸮌$[􅒮Q\u0018󼥯#\u0010\u000f󳻺􊇻𤠏\\谠B\u0016s_ꗑ9\u000bZ𘣙avv:􅄱\u0019𐔄rv󰯚󾒜&𗼼,'/)􅊫'6􏩚5)\u0018꾜1XG􇃴>&󳋸0\t,N\u00014H􊦑\u001aR\tNT^;\u000fs;欐 󰹰#𨠢|uKo󽝘𡖮\u0016.􂏝af󼵒vu%󽁆adVJ󼸑r𦼩㩻#\u0018󼚿z􃍛\u001d_d\u0015\u0003Ql+&fe&2󶟢hbj􌗌[b\u0001kXF󺱱QZ\u0007*(\u000cOqN3f\u001b" } diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_17.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_17.json index 6c4c26edd0b..aba3cd9ee37 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_17.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_17.json @@ -1,5 +1,5 @@ { "code": "3gq6=cswHZ9ri64_HJPb0GHqnIvQsgakJ=HkufysG_pLk8piT7CmIFMoO0lif83sPks6mv-UWRbQCOyTECbFMlPIR57uJSHFmxolrFw", - "email": "퉁[僥@靖wC\u001aE䕣𫶙", + "email": "some@example", "password": "\u0015\u0003\u0015\u001eVB-^K[QI!h𥵶𑗍􈯞Z豂,\r𒂩>\u001cMI𝨝󾎴㌉44\u000fiw\u00157(\u000bu𮬯oy\u0012\u0015,GLefv}q2Zn@jp\u001aRucnj쏩1𬂡BC`\u0019󺆴\u0011dA\u0007w踇\\)yrkSp􀿗䪑􈤰d\u0006b􍥮V'\u0008{\u001b\u000b5𭤼\u0001Lp􃐝\u0002b𘆞69\u0007o\u000b󽠪䷙7㜹\u0011𭜣\u00179qT\u0000\u0015\u001fN{=w󱋏P􅄿>􈍆􂘾U\u0011L𓋧\u000fqV\u0005<\u0004\u001d?􅪺X􈂆rtX$F!R\u0010\u0002𢴜☸H𐁒*\u0012弥Z!Sq%3蠑\"r􎚼WCc⁆P]𩇱i󿧫e\u0016󿾜L센􆣌27V硤𗀍L􄙻P곻m󲑡𩀁S􃕱s!\u001d󷐍Sa𨉇(􎼄\u001f􂒦󴆵\u00061\u000b\u0006󰣟􌑚󻔤\r5N53\u0016k𭬵=<1\u0016H𗉻Mᚚ訰E𤗦QSy*fwg]Q]Y\"s󹷏\u0015􏦎!𣣭\u0000\u0017n􍞠\u001f\u0002!g랑[翍ഖ􎻤_􋝘\u0005\u001c\u000b$\nth𫍬\u001c\"y\u0001􅜂\u001b:6ZfF.Fs\u0018𫞐jnSm\tO9\u0017P@;\rO\u001eM\u0010󻒝\u0015󶷁𝙠쌃󽨴&5𗮣􂾎𬖢\u001b􀷉\u0014k0𧆡\u0014OcMAZy􄅶5\u001c}t8rUzI袷\u000fIL\n|q|\\W\u0014c􍌼=𢩃S(𝥈K󹗥Oꃡ\"7㒼Axp\u0007F\u0018$H6E\u0012{𫢶\u0013𣢋\u0015揂󻐾'࠶\u0012j𢙆W?&\u0014nF^s󱗳􅃽𢧢\u001a\u000esV㉺\u00157𭷿{\u001eꆙ\u001bꂵW𒋙\u000e\u0004g\u0000n\u0001}\u001e\u0003T􌷲\u0007\u000c𦗡ne𛈃NP" } diff --git a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_9.json b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_9.json index 79a5551e162..5997b8751b5 100644 --- a/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_9.json +++ b/libs/wire-api/test/golden/testObject_CompletePasswordReset_user_9.json @@ -1,5 +1,5 @@ { "code": "uyqP0_aQl3yI0f7i0fpyL6quXIf6WSJRbPrU6Z0j2gElHzfIXLenrK4ZwQl42i99XCnAjLGA2=sQczG10h7DBcYH4TmbO-li6YDpcduZ3XkbGQ=EalL5L2xZbwUpVFGp5J5e=yea3gDvfUwq0sdTrRFCFbTJBG5cU9K_5zQMB=DTFJoHAh=L_0uTZCRF_bj36cGxLegs42ji4GGO3kG4kcvpSCMpJV20a47V7GbqfEdQ3HV2gdN5CXpWXxRu71Y2XvAMijj8O-ciqslgJCveAgm6JlkZJf8-Cbj3tmBD1xYveBLOBVOW1=vaD23ST6FDLpzbRJslhJzwInpu5AaIxndPmLzeXH3I5mfrMBFyGO6e9Pro51aJPGV5COmIinyjxcM-vEmWYYkLy7owuVyswR89m--SRwgOWL5UtF-QbkS5bpltl6BmnrTEeaZNMQRPrcpPL4RT=0GFy=ka7Oq1Ixi5OR5EDYgIa_Rl3I9jq034w6wCQjW=33Z5wFRWcdX4lfqvA-66Huc--Xk3hAKScqNeL3Xre5eN1pwOrEFsMhncwuGoFZoXaHSMrQZEqVhVJcFA8afI_vpIk0Ft6NMcS3AtYLQgdqrvaBe42_s", - "email": "A@9L\u0008󹒲鏠", + "email": "some@example", "password": "t%􎈲'􈽛G\u0000G\u0011􄄕V𖨱H헣󿹑[3𤩜\u0005𢹖Y륓\nm\u000b󴧕d5􊓫O:\u0011􌛀\u001fb`🧎\nW\u00198\u0000\u0002𣯭W!mX𠎬z-􆚷ミ 󸘞M\u0007xT(\u000b-\"3Xx\u0007\u000c󱷮b7*P󹒝.\n\u0006fz\u0004N𝦺\u0019弴Z𔒷[7󾟖luXV\u0017Rh\u0019U5}y󳒹&􁍡UO𥼑L\u000ez󵓖\u0013G奣d邗5(iM\u001a\u0000\"󹧡QKY]_\u000b-󵰔𣛡1a\u0001}5]'L%s7􋼓5u\u000b9|F\u0005𫰔j\u001f󼧴L𪣳\u0014o뷃󼞩􌖦lqDs􀺛 ]\u0010\u0003f\u0001bO.\u001e\u001dj󴫟E\"\r󼷥𧮶Ig;~\u001f霙\u0017WQ󽢯𘢚?􌙰\\e:@ჼ󰵗Cq\u0010♞>\u0018zT]\u001c\u0011過\u0005)\u0003󻊛7𪄵\u0001\u0017\u0004𠽘r6;D%FM\":􁅞\u000e-S\u0015c`]쑔\u0018O\u000b$􍹁\u0005\\i󼴋\tk𠩘nME\u001d\u0018mi󺐛~K[K󶍼4\u0012jl\u00132𧹎w.\u0019O󽄜x󹮜榪\u0001􅴂蛂:JY\u0008榊\u0003DPH5􅫨Hh\u0002j!y󲵥􃹶\u001c\u001a0\u0015g\t􄇬q\u000b(c\u0008R=\"󶇑\u0014\u0012\u0015\u001ai󳴯M􃶪𫖿(幦l𫬿\u0012𡙎*聭Y󾊪DžW\t\u0000_pk#=r󼇕{HN􏙇󹓑W3 <%*0𢙮?\u0018|Q𤻀oE*\u000fx,\u0003㷥𒉀L{NC􈒵t𬘨I\u0010{\u0015M\u0011<󶮍efpSGC󱚵Cl\u0001-+\t\u000c􊓉\u0011'uWmR􉴝𠤛9󿌐>𢸀7\u0012s{󴊡my|􏹣\u0007\u0000+󼵙t'N\u0014\u000f礤7􈳣󺵧o􈊞􋯚Yx𠣿5{;𣷳0XM#󷩠Q瘗􈅞\tx􄷧M𧹔^-[a|󲆍聧\u0019\\=\tH􄈌2\u0010W􈲚#h'K󴎄\u000bpX\u0019\u000e\t𘇚_~𫁃󺕵G\u001fwX텣􈒼씚@/E\u000f󵩤\u0010Q𦙊p#pR󶃁\u0012󾫫$\n`l94.𩾱gun\u0004RG\u001bF󱨄Hbf㎕e^󵼝9R.\u0008\u0003;\u0000N$4\u000cVy𧉽,h贚𩍿󱾙\"^莱J\u0000#𭀤ᾣ𓏞z{|\u0006)g􉀃[DK\u0001:(jNn\r:D􏪫dM􁛄G,􉫘𤓀s9\u000eoy􎓰󱶄5Y&𨈘\u000b𥗖\u0016\"Ob50𡝮Q_W+'\"!𒍉\t5𥼍􃂽㿄>\u001a𤗗8𗪬y:U􀦵\u0010\u0001𮤑𩠛\"\u001f,q\u0001a􏯗\u001a_(j 𡕾B􎖩B!􊍔RL^𢶓\u0008𗵍쥻\u0007f\u0018󸸅\niK𠣾\u0004_㫪췝\u0016󲗔\u0007왑\u000c\u001c;\u001f|V󳚅\u0013\u0014󶒯Km\u001e\u0005𠷁􁟠\u001bnz\u0012wy𢡇􇠛𦋎5Q\u001c𑙑J\u0001􂡮/9Ew\u0000Sbe\u0007覿𤝤\u0017\rd1󵶒侜Rᮜ\u000f🟧\u0001v \\𬜗v`iF𬭓93\u0006dSl^I2.W洞󷎪V%*Nv􋉯󿯔]濺xi󶻔󠄝H殈\u001fJ>M􂕓𭜫_&𮚁󱺩󷌾딟.unqC\u0004y󿬔\u0011i#}􃃶𣿈`^󾲸Q\u001b.\u001e)f󱵙𣸍\u0000^\u0000i!\u001f쁏~\\󽮘t􋨚⑿\u0013\n\u0002;E\u000b\u0005z3󹮠2\u0002v\u00175k󹲋𫪢\u001bs\u0000\u000bꢱ9Y𖠺\u0016Q􉝖\u001b\\_7􎂅􊩧J\u00197󷦐v\u0003댡\u0001G󿿤{\u0018PE􆁌𤯦\u0014㕷:N/􌜊􈨫􂅔󻽶V\u0013}𪡿m>{𣼸4\u0000I\u0005y^$\n=)S; R`𢋐@xtE\u000f3" } diff --git a/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json b/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json index 917cfe4360a..28e2621bb1b 100644 --- a/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json +++ b/libs/wire-api/test/golden/testObject_ConversationCoverView_1.json @@ -1,5 +1,5 @@ { + "has_password": false, "id": "00000018-0000-0020-0000-000e00000002", - "name": null, - "has_password": false + "name": null } diff --git a/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json b/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json index c36128fa055..c213fe47c58 100644 --- a/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json +++ b/libs/wire-api/test/golden/testObject_ConversationCoverView_2.json @@ -1,5 +1,5 @@ { + "has_password": false, "id": "00000018-0000-0020-0000-000e00000002", - "name": "conversation name", - "has_password": false + "name": "conversation name" } diff --git a/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json b/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json index 453b2e9b2d4..234dc11a4cd 100644 --- a/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json +++ b/libs/wire-api/test/golden/testObject_ConversationCoverView_3.json @@ -1,5 +1,5 @@ { + "has_password": true, "id": "00000018-0000-0020-0000-000e00000002", - "name": "", - "has_password": true + "name": "" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_1.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_1.json index ced6ffcdc2c..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_1.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_1.json @@ -1,3 +1,3 @@ { - "email": "sL𘇍@%" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_10.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_10.json deleted file mode 100644 index a9f3b4b7b7f..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_10.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "󱬌g鯪Ns&N6r옂U^􊌕􅏔O=;\u0006~g@C3󱸇od𢹓󵏌𦷂P𬿣=𭴟\u0013\u001a󵯰" -} diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_11.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_11.json deleted file mode 100644 index 731d7437a1f..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_11.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "\u0019h𬨑V.JAq-󹣮𬆱46媅=􏛄󸏇@}=􈒽Y\u000c_O}:M\"󸻿" -} diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_12.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_12.json deleted file mode 100644 index 4d7e1d26c35..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_12.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "匣\u0005矹B{󼆎􁭱~@󳖼𨾪\u001d 􅟮" -} diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_2.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_2.json deleted file mode 100644 index fc2647a9e4f..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_2.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "7𧒽>t劭\u0006𐿳n9\u0008\u001fskT.\"􎏸\r\u0014`@^/>1Rp<\u0019􏃵􉡁\u0002#\u0007[E\u0003#碑𧧙ീeJ " -} diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_20.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_20.json deleted file mode 100644 index 9fa5fe447dd..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_20.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "o\u0001󴪚\u0007LL$\u000eᅭ􌡷l*p󰘟\u001a@q矛\u0013ㄭ󴠅󸂢q󴮢𣠈􁻠&^𫋐Z" -} diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_3.json b/libs/wire-api/test/golden/testObject_EmailUpdate_provider_3.json deleted file mode 100644 index c62559456ee..00000000000 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_provider_3.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "email": "1[Z𐲪\r语3􉝰|u\u0008x;2𤔴0\u001f@ {\"_\u000b􊧁󾘨􄜓􉴁\u0019J%󵛃yy\u001a󾁧t>xoC" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_19.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_19.json index 0a0c16440de..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_19.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_19.json @@ -1,3 +1,3 @@ { - "email": "𫬝󽻼E\u001c\u001a𞴇\u0015C%󱏱\u0006\u001f\u0011a8\rﵷE󱺣%𫧚𭖕;|\u001d@1+,𡂌83" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_2.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_2.json index 94a61ed1025..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_2.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_2.json @@ -1,3 +1,3 @@ { - "email": "C𓌇|g󼹸(4@" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_20.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_20.json index 4a3b7ef96db..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_20.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_20.json @@ -1,3 +1,3 @@ { - "email": "e\u0015V\u0008D\u000188Kh\u001c𩙝D4􊇉軀zg\u001e@:𪤬D\u0005y+\u0014k>]\u0017\u001cr󶥱aWSw\u0007ជ6\u001e𘑑f" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_3.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_3.json index 76ae2d73222..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_3.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_3.json @@ -1,3 +1,3 @@ { - "email": "uA76􂎥c𡖝\u0013𤋺\u0001U0]Ds$L@/㺚 u􏠐\u0013Pq\u001dev懪󻛣㺈" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_4.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_4.json index b22ad68e944..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_4.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_4.json @@ -1,3 +1,3 @@ { - "email": ":|𪀧WYA\u0007`OS\u0013\u0015􂴠􎶋u\u000b-\u0013F2B󶡙'z\u0005}4[@6𣾷C𥠃󸆝V􊙮8\"\u0001M𩌻l" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_5.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_5.json index 35da92953a9..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_5.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_5.json @@ -1,3 +1,3 @@ { - "email": "0a⪨\u0012n\u001c!a;*l흣Z\u0008\u0019\u0000\u0000􂻂\u001ej\\𗖸_;\u0002@I︳j󷥽byd𥼛K🡴𒍟􌎍mwP" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_6.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_6.json index ea0097da53d..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_6.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_6.json @@ -1,3 +1,3 @@ { - "email": "com0$p얊/\u001c󿂈󷶆\u000fN􅮊,@\u001d󵖩𥶾,ꆶ/qeT&𥑏\u0007댯}\u0017뉴􏵹󴫸#𬰮0冗9),4F" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_7.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_7.json index fbdd4873e81..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_7.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_7.json @@ -1,3 +1,3 @@ { - "email": "-튄@\u0007$㇠Be􅱑\u000cS7.〢\u0000ହ\r+k>Z:E\u0003hX$?" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_8.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_8.json index 74c30a84d4a..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_8.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_8.json @@ -1,3 +1,3 @@ { - "email": "\u0019f\\\u000edD9#XfnL!󲻀\u0006\u001cZ퀆U@)蟍󸩤x9~t)Dd;P\u001b󺅩.M(p􀜛pCz􍜾󴝄\u000f\u0005\u0006{󸋛􌴰" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_EmailUpdate_user_9.json b/libs/wire-api/test/golden/testObject_EmailUpdate_user_9.json index 43ab8e01e93..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_EmailUpdate_user_9.json +++ b/libs/wire-api/test/golden/testObject_EmailUpdate_user_9.json @@ -1,3 +1,3 @@ { - "email": "\u001c\u000f,n}㑉@x8\u001d" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_Email_user_1.json b/libs/wire-api/test/golden/testObject_Email_user_1.json index a4a7b273859..4a586896d22 100644 --- a/libs/wire-api/test/golden/testObject_Email_user_1.json +++ b/libs/wire-api/test/golden/testObject_Email_user_1.json @@ -1 +1 @@ -"𥆯fa섒\u00012\u000b.\u001c<\u001b\u0017#\t-􍴢`2@\u0004\"殭\u0010n\u001d]\u0007hDzP\u0018p㫾T𠤰\u0014d" +"some@example" diff --git a/libs/wire-api/test/golden/testObject_Email_user_10.json b/libs/wire-api/test/golden/testObject_Email_user_10.json deleted file mode 100644 index d30c881dd46..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_10.json +++ /dev/null @@ -1 +0,0 @@ -"\"K􃘌􅒈$s󱐔􇕿V389V\u0007\u0017\u001cH􁮉)滱Dg\u0017@􏺑􂊪k\u000ci\n󿞆𢈬{\u001dl㠃YPbV-ZZ􂶖LK跓􁒥Y" diff --git a/libs/wire-api/test/golden/testObject_Email_user_11.json b/libs/wire-api/test/golden/testObject_Email_user_11.json deleted file mode 100644 index b66ec316746..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_11.json +++ /dev/null @@ -1 +0,0 @@ -"@+zGJ\u0008_t/N\u00003S􃂕M𣮑􆰠z􌚏􎊆SJD" diff --git a/libs/wire-api/test/golden/testObject_Email_user_12.json b/libs/wire-api/test/golden/testObject_Email_user_12.json deleted file mode 100644 index 074eda1c716..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_12.json +++ /dev/null @@ -1 +0,0 @@ -"\u00041[G󷭮󰄵9􉐛:uJ𣒰\u001cMF𞄝󰫽𭸓\u001f6|󳘏\u0015􆧐@𗧟_􃱰" diff --git a/libs/wire-api/test/golden/testObject_Email_user_13.json b/libs/wire-api/test/golden/testObject_Email_user_13.json deleted file mode 100644 index 4c62fda0c06..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_13.json +++ /dev/null @@ -1 +0,0 @@ -"\u0005\rt\u001cA#}\u001en𫊈OA\u0016\u000e󽼭\t2q\u0013n𧙛𭍩\u000c+@]牭𦷮na[\u001f'h𠴗\u0012󵹌𤅣􏂫" diff --git a/libs/wire-api/test/golden/testObject_Email_user_14.json b/libs/wire-api/test/golden/testObject_Email_user_14.json deleted file mode 100644 index 00afac0771b..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_14.json +++ /dev/null @@ -1 +0,0 @@ -"X\u0010E\u0014󷘞󿒐􎒂YMU[\n}󲈖\r7󶨐\u0018\\@𨓫󼽯M\u001d!𢶞T%F]" diff --git a/libs/wire-api/test/golden/testObject_Email_user_15.json b/libs/wire-api/test/golden/testObject_Email_user_15.json deleted file mode 100644 index ac14ced80d4..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_15.json +++ /dev/null @@ -1 +0,0 @@ -"{C𪮆󾞐eU\u0014w@`\u0013>" diff --git a/libs/wire-api/test/golden/testObject_Email_user_16.json b/libs/wire-api/test/golden/testObject_Email_user_16.json deleted file mode 100644 index d47fad630cb..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_16.json +++ /dev/null @@ -1 +0,0 @@ -"IO\u00192>􁍸+~@\u000e" diff --git a/libs/wire-api/test/golden/testObject_Email_user_17.json b/libs/wire-api/test/golden/testObject_Email_user_17.json deleted file mode 100644 index 4dc0b271fc6..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_17.json +++ /dev/null @@ -1 +0,0 @@ -"\u001e􋺥\u0016\u0001>줂V7C-asF􁩬IfrYTM;󷲆􂧽*l(d@.53Q􋻗26bfw𪷁𒂅~𨚃𠌬m\u001d\u0015\u000e}𥕟~􀩻R" diff --git a/libs/wire-api/test/golden/testObject_Email_user_18.json b/libs/wire-api/test/golden/testObject_Email_user_18.json deleted file mode 100644 index ebc1ece9160..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_18.json +++ /dev/null @@ -1 +0,0 @@ -"𬒂|\u0000\u00023\u0012􌝘,:\u0017JuF\u0010*㶮\u0011\"\u0016kU!󱩝8T\u00192@T\"q\u0011𑣤}\u0013Z~🖟)" diff --git a/libs/wire-api/test/golden/testObject_Email_user_19.json b/libs/wire-api/test/golden/testObject_Email_user_19.json deleted file mode 100644 index 33346b6153c..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_19.json +++ /dev/null @@ -1 +0,0 @@ -"\t\u0006N~\u001eWy5'\u0018q:_K󹫜\"+WM瑳S.\u0012D`\u0018@DG𡏝AE=W􆫔" diff --git a/libs/wire-api/test/golden/testObject_Email_user_2.json b/libs/wire-api/test/golden/testObject_Email_user_2.json deleted file mode 100644 index 070584c3307..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_2.json +++ /dev/null @@ -1 +0,0 @@ -"󰨊\u0013F𭟍􈶇n:@8􅻻ಏ\u0016s" diff --git a/libs/wire-api/test/golden/testObject_Email_user_20.json b/libs/wire-api/test/golden/testObject_Email_user_20.json deleted file mode 100644 index 232b97cdc8e..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_20.json +++ /dev/null @@ -1 +0,0 @@ -"/𡯐ZF󵃹󺲛@𭵥I􎾁~FN󸋄\u00053PH\u0014k󽃿𠉚{󿙾\u0017H󹇢\u0012IR" diff --git a/libs/wire-api/test/golden/testObject_Email_user_3.json b/libs/wire-api/test/golden/testObject_Email_user_3.json deleted file mode 100644 index 1c831e16e3a..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_3.json +++ /dev/null @@ -1 +0,0 @@ -"􎟘o #𩃰GῩ%/]\u001ck􁻪t􍜮)\u0013\u0015*\u001bE}>\u001a\u0001En==8@昸\u0018N\u0007;_l$*䇱P\u0002\u0008K" diff --git a/libs/wire-api/test/golden/testObject_Email_user_4.json b/libs/wire-api/test/golden/testObject_Email_user_4.json deleted file mode 100644 index ceb8c7e0a35..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_4.json +++ /dev/null @@ -1 +0,0 @@ -"!𢢼\u000f&\u0007o88i󱨨(\u001al1_􇐬-\u000c\t&\u001c9􊉳@ef&󻣵𬋾W(󳻠cS\u0016\u0004𗴯GLi" diff --git a/libs/wire-api/test/golden/testObject_Email_user_5.json b/libs/wire-api/test/golden/testObject_Email_user_5.json deleted file mode 100644 index 2e5781ed517..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_5.json +++ /dev/null @@ -1 +0,0 @@ -"@洸\u0011􉼗󺳩jh󷾴o\u001dH0bW7^䗕B" diff --git a/libs/wire-api/test/golden/testObject_Email_user_6.json b/libs/wire-api/test/golden/testObject_Email_user_6.json deleted file mode 100644 index 918f37c1212..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_6.json +++ /dev/null @@ -1 +0,0 @@ -"eM'5\u0008r>𖥝󳔈4𠟐r\t@󳗧[n\u0005 )&D=󹆉\u0006\u0008" diff --git a/libs/wire-api/test/golden/testObject_Email_user_7.json b/libs/wire-api/test/golden/testObject_Email_user_7.json deleted file mode 100644 index 65b5478a87a..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_7.json +++ /dev/null @@ -1 +0,0 @@ -"􎦮𩔛a\u0003'𗅼L\u0016醍󲂢Q󴝊󳸯Fc􋒕T𮮲\u000eTD@𫌐?1􌰨DT" diff --git a/libs/wire-api/test/golden/testObject_Email_user_8.json b/libs/wire-api/test/golden/testObject_Email_user_8.json deleted file mode 100644 index 26f013774c1..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_8.json +++ /dev/null @@ -1 +0,0 @@ -" \u001b{􎎍EZ_\t+E\u000bE@h𖡚%펽g㤣Lu\u0012𫥦J} Aq\"#f\u001b \u0004O\u0012" diff --git a/libs/wire-api/test/golden/testObject_Email_user_9.json b/libs/wire-api/test/golden/testObject_Email_user_9.json deleted file mode 100644 index 327f67fd21c..00000000000 --- a/libs/wire-api/test/golden/testObject_Email_user_9.json +++ /dev/null @@ -1 +0,0 @@ -"󶤾󼉱5M6d*~-\u0013\u0017?罆@g𛆟R􊸢|󰸌󻥤" diff --git a/libs/wire-api/test/golden/testObject_Event_conversation_3.json b/libs/wire-api/test/golden/testObject_Event_conversation_3.json index 95ff02e8ca8..9e00bb1f2fc 100644 --- a/libs/wire-api/test/golden/testObject_Event_conversation_3.json +++ b/libs/wire-api/test/golden/testObject_Event_conversation_3.json @@ -2,9 +2,9 @@ "conversation": "2126ea99-ca79-43ea-ad99-a59616468e8e", "data": { "code": "7d6713", + "has_password": false, "key": "CRdONS7988O2QdyndJs1", - "uri": "https://example.com", - "has_password": false + "uri": "https://example.com" }, "from": "2126ea99-ca79-43ea-ad99-a59616468e8e", "qualified_conversation": { diff --git a/libs/wire-api/test/golden/testObject_Event_user_14.json b/libs/wire-api/test/golden/testObject_Event_user_14.json index 1657df9614d..20ebd595043 100644 --- a/libs/wire-api/test/golden/testObject_Event_user_14.json +++ b/libs/wire-api/test/golden/testObject_Event_user_14.json @@ -2,8 +2,8 @@ "conversation": "00000838-0000-1bc6-0000-686d00003565", "data": { "code": "lLz-9vR8ENum0kI-xWJs", - "key": "NEN=eLUWHXclTp=_2Nap", - "has_password": false + "has_password": false, + "key": "NEN=eLUWHXclTp=_2Nap" }, "from": "0000114a-0000-7da8-0000-40cb00007fcf", "qualified_conversation": { diff --git a/libs/wire-api/test/golden/testObject_Feature_team_14.json b/libs/wire-api/test/golden/testObject_Feature_team_14.json index 9148fb4871f..99c386e30af 100644 --- a/libs/wire-api/test/golden/testObject_Feature_team_14.json +++ b/libs/wire-api/test/golden/testObject_Feature_team_14.json @@ -1,7 +1,7 @@ { - "status": "disabled", - "ttl": "unlimited", "config": { - "useSFTForOneToOneCalls": true - } + "useSFTForOneToOneCalls": true + }, + "status": "disabled", + "ttl": "unlimited" } diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_10.json b/libs/wire-api/test/golden/testObject_InvitationList_team_10.json index 9fe5bac056d..03f8201ebf0 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_10.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_10.json @@ -4,7 +4,7 @@ { "created_at": "1864-05-08T17:28:36.896Z", "created_by": "00000001-0000-0001-0000-000000000000", - "email": "}@", + "email": "some@example", "id": "00000000-0000-0000-0000-000000000000", "name": "P𥖧\u0006'e\u0010\u001d\"\u0011K󽗨Fcvm[\"Sc}U𑊒􂌨󿔟~!E􀖇\u000bV", "role": "member", diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_11.json b/libs/wire-api/test/golden/testObject_InvitationList_team_11.json index 78ed3f5c569..35f94e7190f 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_11.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_11.json @@ -4,7 +4,7 @@ { "created_at": "1864-05-08T01:33:08.374Z", "created_by": "00000001-0000-0000-0000-000000000000", - "email": "@Z", + "email": "some@example", "id": "00000001-0000-0000-0000-000000000001", "name": "G\\,\u0000=ෝI-w󠀹}𠉭抳-92\u0013@\u0006\u001f\\F\u001a\"-r꒫6\u000fඬ\u001f*}c󼘹\u001f\u0007T8m@旅M\u0012#MIq\r4nW􍦐y\u0005Ud룫#𫶒5\n\u0002V]𨡀\"󶂃𩫘0:ﲼ𮭩+\u0001\u000bP󹎷X镟􅔧.\u0019N\"𬋻", "role": "admin", @@ -14,7 +14,7 @@ { "created_at": "1864-05-09T23:06:13.648Z", "created_by": null, - "email": "@", + "email": "some@example", "id": "00000000-0000-0000-0000-000100000000", "name": "叕5q}B\u0001𦌜`イw\\X@󼶝𢼈7Mw,*z{𠚷&~", "role": "partner", @@ -24,7 +24,7 @@ { "created_at": "1864-05-09T10:37:03.809Z", "created_by": "00000001-0000-0000-0000-000000000001", - "email": "@", + "email": "some@example", "id": "00000000-0000-0001-0000-000000000000", "name": "V􈫮\u0010qYヒCU\u000e􄕀fQJ\u0005ਓq+\u0007\u0016󱊸\u0011@𤠼`坟qh+𬾬A7𦄡Y \u0011Tㅎ1_􈩇#B<􂡁;a6o=", "role": "partner", @@ -34,7 +34,7 @@ { "created_at": "1864-05-09T04:46:03.504Z", "created_by": null, - "email": "@", + "email": "some@example", "id": "00000001-0000-0001-0000-000100000000", "name": ",􃠾{ս\u000c𬕻Uh죙\t\u001b\u0004\u0001O@\u001a_\u0002D􎰥𦀛\u0016g}", "role": "admin", @@ -44,7 +44,7 @@ { "created_at": "1864-05-09T12:53:52.047Z", "created_by": "00000000-0000-0000-0000-000000000001", - "email": "@", + "email": "some@example", "id": "00000000-0000-0001-0000-000100000000", "name": null, "role": "owner", diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_16.json b/libs/wire-api/test/golden/testObject_InvitationList_team_16.json index 944313e9490..ae37c5ce5e1 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_16.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_16.json @@ -4,7 +4,7 @@ { "created_at": "1864-05-09T15:25:30.297Z", "created_by": "00000001-0000-0001-0000-000000000000", - "email": "\u000f@", + "email": "some@example", "id": "00000001-0000-0000-0000-000100000001", "name": "E𝘆YM<󾪤j􆢆\r􇳗O󴟴MCU\u001eI󳊃m𔒷hG\u0012|:P􅛽Vj\u001c\u0000ffgG)K{􁇏7x5󱟰𪔘\n\u000clT􆊞", "role": "owner", diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_17.json b/libs/wire-api/test/golden/testObject_InvitationList_team_17.json index ae671cd4808..d36b7d8bdc6 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_17.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_17.json @@ -4,7 +4,7 @@ { "created_at": "1864-05-08T10:54:19.942Z", "created_by": "00000001-0000-0001-0000-000100000000", - "email": "&@𫳦", + "email": "some@example", "id": "00000001-0000-0001-0000-000000000001", "name": null, "role": "partner", diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_2.json b/libs/wire-api/test/golden/testObject_InvitationList_team_2.json index 66ae47e2f38..fb720150e22 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_2.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_2.json @@ -4,7 +4,7 @@ { "created_at": "1864-05-08T09:28:36.729Z", "created_by": "00000001-0000-0001-0000-000000000000", - "email": "𥝢@w", + "email": "some@example", "id": "00000001-0000-0001-0000-000000000000", "name": "fuC9p􌌅A𧻢\u000c\u0005\u000e刣N룞_?oCX.U\r𧾠W腈󽥝\u0013\t[錣\u0016/⃘A𣚁𪔍\u0014H𠽙\u0002𨯠\u0004𨒤o\u0013", "role": "owner", diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_20.json b/libs/wire-api/test/golden/testObject_InvitationList_team_20.json index 0ffd1042d30..f5b5cac7035 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_20.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_20.json @@ -4,7 +4,7 @@ { "created_at": "1864-05-09T07:22:02.426Z", "created_by": "00000001-0000-0001-0000-000100000001", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000100000000", "name": null, "role": "partner", @@ -14,7 +14,7 @@ { "created_at": "1864-05-09T18:56:29.712Z", "created_by": null, - "email": "@", + "email": "some@example", "id": "00000001-0000-0001-0000-000000000000", "name": "YPf╞:\u0005Ỉ&\u0018\u0011󽧛%ꦡk𪯋􅥏:Q\u0005F+\u0008b8Jh􌎓K\u0007\u001dY\u0004􃏡\u000f󽝰\u0016 􁗠6>I󾉩B$z?𤢾wECB\u001e𥼬덄\"W𗤞󲴂@\u001eg)\u0001m!-U􇧦󵜰o\u0006a\u0004𭂢;R􂪧kgT􍆈f\u0004\u001e\rp𓎎󿉊X/􄂲)\u00025.Ym󵳬n싟N\u0013𫅄]?'𠴺a4\"󳟾!i5\u001e\u001dC14", "role": "owner", diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_4.json b/libs/wire-api/test/golden/testObject_InvitationList_team_4.json index d0cbd90f1e7..f53ae2ac79a 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_4.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_4.json @@ -4,7 +4,7 @@ { "created_at": "1864-05-09T19:46:50.121Z", "created_by": "00000001-0000-0001-0000-000000000000", - "email": "@", + "email": "some@example", "id": "00000001-0000-0001-0000-000100000000", "name": "R6𠥄𠮥VQ𭴢\u001a\u0001𬄺0C􉶍\u001bR𭗈𞡊@韉Z?\u0002𩖫􄭦e}\u0001\u0017\u0004m𭉂\u001f]󰺞𮉗􂨮󰶌\u0008\u0011zfw-5𝝖\u0018􃸂 \u0019e\u0014|㡚Vo{􆳗\u0013#\u001fS꿻&zz𧏏9𢱋,\u000f\u000c\u0001p󺜰\u0010𧵪􂸑.&󳢨kZ쓿u\u0008왌􎴟n:􍝋D$.Q", "role": "admin", @@ -14,7 +14,7 @@ { "created_at": "1864-05-09T09:00:02.901Z", "created_by": "00000000-0000-0001-0000-000000000000", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000100000000", "name": "\u0012}q\u0018=SA\u0003x\t\u0003\\\u000b[\u0008)(\u001b]𡋃Y\u000b@pꈫl뀉𦛌\u0000\t􌤢\u00011\u0011\u0005󹝃\"i猔\u0019\u0008\u0006\u000f\u0012v\u0006", "role": "admin", @@ -24,7 +24,7 @@ { "created_at": "1864-05-09T11:10:31.203Z", "created_by": "00000001-0000-0001-0000-000000000000", - "email": "@", + "email": "some@example", "id": "00000001-0000-0001-0000-000100000001", "name": "&􂧽Ec\u0000㼓}k󼾘l𪍯\u001fJ\u00190^.+F\u0000\u000c$'`!\u0017[p󾓉}>E0y𗢸#4I\u0007𐐡jc\u001bgt埉􊹘P\u0014!􋣥E93'Y$YL뜦b\r:,𬘞\u000e𥚟y\u0003;􃺹􌛖z4z-D􋰳a𡽜6𨏝r󼖨󱌂J\u0010밆", "role": "member", @@ -34,7 +34,7 @@ { "created_at": "1864-05-09T23:41:34.529Z", "created_by": "00000001-0000-0001-0000-000100000000", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000000000000", "name": "Ft*O1\u0008&\u000e\u0018<𑨛􊰋m\n\u0014\u0012; \u0003󱚥\u0011􂬫\"k.T󹴑[[\u001c\u0004{j`\u001d󳟞c􄖫{\u001a\u001dQY𬨕\t\u0015y\t𠓳j󼿁W ", "role": "owner", @@ -44,7 +44,7 @@ { "created_at": "1864-05-09T00:29:17.658Z", "created_by": "00000000-0000-0000-0000-000000000001", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000000000000", "name": null, "role": "admin", @@ -54,7 +54,7 @@ { "created_at": "1864-05-09T13:34:37.117Z", "created_by": "00000000-0000-0000-0000-000100000001", - "email": "@", + "email": "some@example", "id": "00000000-0000-0000-0000-000100000001", "name": "Lo\r􎒩B𗚰_v󰔢􆍶󻀬􊽦9\u0002vyQ🖰&W󻟑𠸘􇹬'􁔫:𤟗𡶘􏹠}-o󿜊le8Zp󺩐􋾙)nK\u00140⛟0DE\u0015K$io\u001e|Ip2ClnU𬖍", "role": "owner", diff --git a/libs/wire-api/test/golden/testObject_InvitationList_team_6.json b/libs/wire-api/test/golden/testObject_InvitationList_team_6.json index 689afff6db0..481105a9d8d 100644 --- a/libs/wire-api/test/golden/testObject_InvitationList_team_6.json +++ b/libs/wire-api/test/golden/testObject_InvitationList_team_6.json @@ -4,7 +4,7 @@ { "created_at": "1864-05-09T06:42:29.677Z", "created_by": "00000000-0000-0000-0000-000100000001", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000000000000", "name": null, "role": "admin", @@ -14,7 +14,7 @@ { "created_at": "1864-05-09T11:26:36.672Z", "created_by": "00000001-0000-0000-0000-000000000000", - "email": "@", + "email": "some@example", "id": "00000000-0000-0001-0000-000000000000", "name": null, "role": "admin", @@ -24,7 +24,7 @@ { "created_at": "1864-05-09T00:31:56.241Z", "created_by": "00000001-0000-0001-0000-000100000001", - "email": "@", + "email": "some@example", "id": "00000000-0000-0001-0000-000000000001", "name": null, "role": "owner", @@ -34,7 +34,7 @@ { "created_at": "1864-05-09T21:10:47.237Z", "created_by": "00000001-0000-0000-0000-000100000001", - "email": "@", + "email": "some@example", "id": "00000001-0000-0000-0000-000100000000", "name": "YBc\r웶8{\\\n􋸓+\u0008\u0016'<\u0004􈄿Z\u0007nOb􋨴􌸖𩮤}2o@v/", "role": "member", diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json index 4d2324b3889..7948f9021bf 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_1.json @@ -1,5 +1,5 @@ { - "email": "/Y𨎂\u000b}?@󲚚󾋉𫟰\u000e󽈝", + "email": "some@example", "locale": "nn", "name": null, "role": "owner" diff --git a/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json b/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json index ee7c2e71517..bf9390b4067 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_10.json @@ -1,5 +1,5 @@ { - "email": "󶩭\u000c\u0006\u0010^s@d", + "email": "some@example", "locale": "ny-OM", "name": "H󶌔\u001e댥𖢯uv󿊧\u0012󿕜\u001a 𧆤=a\u001b4H,B\u0018󽲴GpV0󿇇;_\u0000𪔺Z\u0011滘\u00156耐'W9z⻒\tr𤭦􂃸\u0016_ge豍\u0004D𗈌o\u0007n>󲤯", "role": "member" 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 976d7bf73bc..4d0cd645404 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_11.json @@ -1,5 +1,5 @@ { - "email": "\u0001\u0000󸊱nJ@t\u0002.", + "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", "role": "owner" 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 c89a4fc9558..4ea54084e75 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_12.json @@ -1,5 +1,5 @@ { - "email": "􎪠􇿸@", + "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􋊉", "role": null 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 c19fcc929aa..af76a81bc0a 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_13.json @@ -1,5 +1,5 @@ { - "email": "\u0000r@c,", + "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", "role": null 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 3788c8d60ef..8aa099fd0d0 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_14.json @@ -1,5 +1,5 @@ { - "email": "@\u000b", + "email": "some@example", "locale": "dv-LB", "name": "\u0015wGn󳔃𤠘1}\u0004gY.>=}", "role": "admin" 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 d3f6725f143..d4ac032590e 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_15.json @@ -1,5 +1,5 @@ { - "email": "U@􈘸", + "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", "role": "owner" 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 aba9a6724be..89de798ef5c 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_16.json @@ -1,5 +1,5 @@ { - "email": "壧@\u0001", + "email": "some@example", "locale": "om-BJ", "name": null, "role": "admin" 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 b5cff60b2b8..8c154e43f07 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_17.json @@ -1,5 +1,5 @@ { - "email": "3\u000cC\u0017\"@\u00010x𝗢", + "email": "some@example", "locale": "kj-TC", "name": null, "role": "partner" 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 b7214299685..9021ab1aa62 100644 --- a/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json +++ b/libs/wire-api/test/golden/testObject_InvitationRequest_team_18.json @@ -1,5 +1,5 @@ { - "email": "\u0008\u0006b\n0@UJj&鞱", + "email": "some@example", "locale": "ku", "name": "8VPAp𡧑2L}𫙕", + "email": "some@example", "locale": null, "name": "kl\u0003\u0004\u0016%s7󻼗fX󲹙A\u00087\u0011D\u0004\u0011𨔣sg)dD𦙚Rx[󺭌Tw𐨕\u001e\u001a􀑔z\\\u000f\u0005䊞l􉾾l|oKc\\(𭬥􌵬=脜2VI*􋖛2oTh&#+;o᎙dXA⽇=*􆗾Q󼂨{󲺕󠁑5}\u001d9D𭟸􃿙r􇸖P:󳓗䏩𝓖\u0008\u001a\u001c\u000fF%<𞢹\u000fh\u001b\u0003\u000f󲶳\u001fO\u0000g_𤻨뢪󺥟\u0004􂔤􊃫z~%IA'R\u0008󶽴Hv^󾲱wrjb\t𨭛\u0003", "role": "admin", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_12.json b/libs/wire-api/test/golden/testObject_Invitation_team_12.json index 582e9a37f65..efd46107985 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_12.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_12.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-12T22:47:35.829Z", "created_by": "00000002-0000-0002-0000-000000000000", - "email": "󸐞𢜑\u001e@", + "email": "some@example", "id": "00000000-0000-0000-0000-000100000002", "name": "\u0010Z+wd^𐘊􆃨1\u0002YdXt>􇺼LSB7F9\\𠿬\u0005\n󱂟\"🀡|\u0007𦠺'\u001bTygU􎍔R칖􅧠O4󼷁E9\"󸃐\u0012Re\u0005D}􀧨𧢧􍭝\u0008V𫋾%98'\u001e9\u00064yP𔗍㡀ř\u0007w\t􌄦\u000b􇋳xv/Yl󵢬𦯯", "role": "admin", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_13.json b/libs/wire-api/test/golden/testObject_Invitation_team_13.json index 75f28fbd493..34387a9fbeb 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_13.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_13.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-08T01:18:31.982Z", "created_by": "00000001-0000-0002-0000-000100000002", - "email": "@r", + "email": "some@example", "id": "00000002-0000-0000-0000-000200000002", "name": "U", "role": "member", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_14.json b/libs/wire-api/test/golden/testObject_Invitation_team_14.json index de52080fdea..e7db3eaa41b 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_14.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_14.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-12T23:54:25.090Z", "created_by": "00000002-0000-0002-0000-000200000000", - "email": "EI@{", + "email": "some@example", "id": "00000001-0000-0000-0000-000200000002", "name": null, "role": "owner", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_15.json b/libs/wire-api/test/golden/testObject_Invitation_team_15.json index c43a423ccf6..3409278eda5 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_15.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_15.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-08T22:22:28.568Z", "created_by": null, - "email": ".@", + "email": "some@example", "id": "00000001-0000-0001-0000-000200000001", "name": "𑜘\u001f&KIL\u0013􉋏![\n6􏙭HEj4E⽨UL\u001f>2􅝓_\nJ킢Pv\u000e\u000fR碱8\u0008mS뇆mE\u0007g\u0016\u0005%㣑\u000c!\u000b\u001f𝈊\u0005𭇱󿄈\u000e83!j𒁾\u001d􅣣,\u001e\u0018F􃞋􏈇U\u0019Jb\u0011j\u0019Y𖢐O󶃯", "role": "owner", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_16.json b/libs/wire-api/test/golden/testObject_Invitation_team_16.json index 4fd4c8f9bfc..9b236a6526c 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_16.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_16.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-09T09:56:33.113Z", "created_by": "00000001-0000-0000-0000-000100000001", - "email": "\\@\"{", + "email": "some@example", "id": "00000001-0000-0002-0000-000200000001", "name": "\u001d\u0014Q;6/_f*7􋅎\u000f+􊳊ꋢ9", "role": "partner", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_17.json b/libs/wire-api/test/golden/testObject_Invitation_team_17.json index 9ceba395190..036e17f9aff 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_17.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_17.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-08T06:30:23.239Z", "created_by": "00000000-0000-0001-0000-000000000001", - "email": "@\u0001[𗭟", + "email": "some@example", "id": "00000001-0000-0001-0000-000100000001", "name": "Z\u001b9E\u0015鍌𔗕}(3m𗮙𗷤'􅺒.WY;\u001e8?v-􌮰\u0012󸀳", "role": "admin", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_19.json b/libs/wire-api/test/golden/testObject_Invitation_team_19.json index 33980c38c72..281f50e5b76 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_19.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_19.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-07T15:08:06.796Z", "created_by": null, - "email": "󸽎𗜲@(S\u0017", + "email": "some@example", "id": "00000001-0000-0002-0000-000000000001", "name": "靸r𛋕\u0003Qi󴊗􌃗\u0019𩫻𒉓+􄮬Q?H=G-\u001e;􍝧\u000eq^K;a􀹚W\u0019 X𔖸􆂨>Mϔ朓jjbU-&󽼈v\u0000y𬙼\u0007|\u0016UfJCHjP\u000e􏘃浍DNA:~s", "role": "member", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_2.json b/libs/wire-api/test/golden/testObject_Invitation_team_2.json index fd2dea38a1e..c03242304f4 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_2.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_2.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-12T14:47:35.551Z", "created_by": "00000002-0000-0001-0000-000200000001", - "email": "i@m_:", + "email": "some@example", "id": "00000002-0000-0001-0000-000100000002", "name": "􄭇} 2pGEW+\rT𩹙p𪨳𦘢&𣫡v0\u0008", "role": "partner", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_20.json b/libs/wire-api/test/golden/testObject_Invitation_team_20.json index fb578312b1c..e161b23730a 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_20.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_20.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-12T08:07:17.747Z", "created_by": "00000000-0000-0001-0000-000100000001", - "email": "b@u9T", + "email": "some@example", "id": "00000002-0000-0001-0000-000000000001", "name": null, "role": "partner", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_3.json b/libs/wire-api/test/golden/testObject_Invitation_team_3.json index c9d2554f3a6..e3d098d2bdb 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_3.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_3.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-08T22:07:35.846Z", "created_by": "00000001-0000-0002-0000-000200000001", - "email": "@秕L", + "email": "some@example", "id": "00000002-0000-0001-0000-000100000002", "name": null, "role": "partner", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_4.json b/libs/wire-api/test/golden/testObject_Invitation_team_4.json index 96a0ef0b999..68f5b40a53a 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_4.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_4.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-09T09:23:58.270Z", "created_by": "00000000-0000-0000-0000-000200000001", - "email": "^@e", + "email": "some@example", "id": "00000001-0000-0001-0000-000000000001", "name": null, "role": "admin", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_5.json b/libs/wire-api/test/golden/testObject_Invitation_team_5.json index 46aeeb0d060..dc84169a3af 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_5.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_5.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-09T03:42:15.266Z", "created_by": null, - "email": "\u0001V@f􉌩꧆", + "email": "some@example", "id": "00000000-0000-0002-0000-000000000002", "name": "}G_𤃊`X󻋗𠆝󷲞L\"󿶗e6:E쨕󲟇f-$𠬒Z!s2p?#\tF 8𭿰𨕿󹵇\u0004􉢘*󸚄\u0016\u0010%Y𩀄>􏘍󾨶󺶘g\"􁥰\u001a\u001a𬇟ꦛ\u0004v𭽢,𩶐(\u001dQT𤪐;􃨚\u0005\u0017B􎇮H𩣓\\󾃾,Y", "role": "owner", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_6.json b/libs/wire-api/test/golden/testObject_Invitation_team_6.json index 6cd8e6fb8b7..a9685bb1784 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_6.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_6.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-09T08:56:40.919Z", "created_by": "00000001-0000-0001-0000-000200000000", - "email": "@OC", + "email": "some@example", "id": "00000001-0000-0002-0000-000100000000", "name": "O~\u0014U\u001e?V3_𮬰Slh􅱬Q1󶻳j|~M7􊲚􋽼𗆨\u0011K􇍼Afs𫬇lGV􏱇]`o\u0019f蓤InvfDDy\\DI𧾱􊥩\u0017B𦷬F*X\u0001\u001a얔\u0003\u0010<\u0003\u0016c\u0010,p\u000b*󵢘Vn\u000cI𑈹xS\u0002V\u001b$\u0019u󴮖xl>\u0007Z\u00144e\u0014aZ", "role": "admin", diff --git a/libs/wire-api/test/golden/testObject_Invitation_team_7.json b/libs/wire-api/test/golden/testObject_Invitation_team_7.json index d961ec2d508..6ec71ddd5f3 100644 --- a/libs/wire-api/test/golden/testObject_Invitation_team_7.json +++ b/libs/wire-api/test/golden/testObject_Invitation_team_7.json @@ -1,7 +1,7 @@ { "created_at": "1864-05-07T18:46:22.786Z", "created_by": "00000000-0000-0002-0000-000100000000", - "email": "oj@", + "email": "some@example", "id": "00000000-0000-0000-0000-000000000002", "name": "\u0018.𛅷􈼞\u0010\u000c\u0010\u0018𤰤o;Yay:yY $\u0003<ͯ%@\u001fre>5L'R\u0013𫝳oy#]c4!𘖝U홊暧󾜸􃕢p_>f\u000e𪲈􇇪󳆗_Vm\u001f}\u0002Pz\r\u0005K\u000e+>󲆠\u0000𥝻?pu?r\u001b\u001a!?𩇕;ᦅS䥅\u0007􅠬\u0008󹹝-Z$Z\u0015\r􌻮a\u001e%\u0000:𮄱먺𦝬?e]\u0003 𢴐 C\u0001\u000fS%8m􊦓V𣺻[󵪶6𩹚󶸓𨌰SX\n%􃆋*>\t+𠕋Y󱥶󲡂\u001dU􄨕6TU!*鲲90􁬜\u001eV𧪳N\t*\u0004{I<􈭶\u0001𬭌!c\\\n􎘭𬭪\u0011,-xX\u0019V?\t𩋈􁘟\u00121\u0001u\u0001콅\u000e+h\u0006::\u001e卬_g,\u000e*\u000b\u0014􋁎HFF𮇶􇻳fF\u001b2\u0001T\u0011)\u000cc豁l􃊫\u000c#~\u0002]󼭎/Or)kY󻳿\u0001NCk􄮲5􈡎x=H\u0000峐􂝖􌕙E/$pbi𡤲\u001cKi㴼󸤖\t7\"OL폀ᵜ5꧊\u0000(󻫄𨩲\u0001𤝟󲸓掩C==\u001dTV3l6󳹞.Z)$䅓|𪊼􊋿J;O\u001dbw\u000bI􌳠I\u0016\u0012^𤡾\u00023%i\u0019W𡵶\u0014􏸓tsL5𣺏W𗦼(_,􊙫*󾎇rckx\u0001\u000fs\u0001Jd𢔞\u0016ev.\u0014\u0010𘌊.􎍡󳚀𣁘\u001f_\u0017f\u0002\u000e\u0013󾴤6O\u0011Q\u0001'\u001d,|]W\u000fa𤸖.\u000b\u0007H&-L\u0012+𣻫􋝤\u0004m)䷕𬎛𬱈!𭎇𢹢m\u0014\u0013󼠪m\u001d𭒥>>\"NDw􆺍hY󼙧sFKz^ 􎣛5Qec\u0015}|􎣢.Q𪐺imb󺲔 p;􉸺\u0016􄌔kF􍐆r8o\u0011", "verification_code": null diff --git a/libs/wire-api/test/golden/testObject_Login_user_4.json b/libs/wire-api/test/golden/testObject_Login_user_4.json index 8ba7a5ba2aa..a8f2a3074ac 100644 --- a/libs/wire-api/test/golden/testObject_Login_user_4.json +++ b/libs/wire-api/test/golden/testObject_Login_user_4.json @@ -1,5 +1,5 @@ { - "email": "BG@⽩c\u000b}\u000fL$_", + "email": "some@example", "label": "\u000e\u0015eC/", "password": "&󲉊󹴌𔖘\u0002J<-~\u0002>\u000b𒇴𥄿5QN틐𨤨ql\u0015𒈲3}{\u0013𪒺S壓;\t7𬺖_F~D*f􀕔)􄥂-9僛7GK= %\u001e@kOF#𫻩􋌁𞡂8_ꕅ\u001dL鍂\u0003󿶊0Wl1A`LYz\u001fy僸\u001ao\u001b[\u0014\u0008t𐑐a\u0003s~\u001fF𪰤G`$\u000bG\u0011󾿅🙣/󷪺C>\u000f", "verification_code": "RcplMOQiGa-JY" diff --git a/libs/wire-api/test/golden/testObject_Login_user_5.json b/libs/wire-api/test/golden/testObject_Login_user_5.json index 20658e70ab3..b882ff33bf4 100644 --- a/libs/wire-api/test/golden/testObject_Login_user_5.json +++ b/libs/wire-api/test/golden/testObject_Login_user_5.json @@ -1,5 +1,5 @@ { - "email": "@~^G􆪐\\", + "email": "some@example", "label": null, "password": "z>􉰃󺎇/𡞯􊳌\u0008%$󽖨𣄄:}\t\u0018􂜙󾺽)㊝󵙼s󵪾\u0018}鱢\u0019[ꅾ\u000bX#VG,df4𢢵8m5딝OTK𣑌鋎꺯◆Z\"ZS\u001bms|[Q%􉲡\u0005W\\󴖙C𭌈+􅕺ဒ䖡v𬁡ꎞ){󻆡𣃒f𭬔}:X-\u00082N\u0019\u001fl🎢쇈Y􅤡󷐛r2.1싸\u0004+𡥙\u0013𣡈]'󻂳s󳤴ꜵ.}𭋣o󲍶X𠜥⬅\r\u001aNq6󸻕'\u000cd\u001e㢧􋾜,:%\t𥅬𒃣QD􉖠\u001b(q4KDQ2zcI\u0010>\u00195󲤼1\u000cBkd\u0013\u0006:F:\u0004𘨥ⶂO N\u001c,N􁚶󴌷[h9ᜬ:xZ=\u000c􈾀\u0013u\u001e\u000ce#\u001a^$lkx耤 \rr\u001aJ󷝦󸓡\u001cR][_5\u0015ⷤ诃5惵􁮵󳗴鉅K!􁁠eRR%絬+h~1󲞮谟lTzS$\u0010􂶳\"*􉕷pmRE\u0013(\u001f^ὯJc➑􅫇i\n+G$|󲫉𦉻g\u001c\u000cgU3Y𝄜\u0006f)􊾺\u0016𓈄􌭞/\u0000Piꩦ{󿸟j􈞅\u001c9𠚽󺊬翉w$눟𞴦)Si𨴄牿FX􂋒j{`궤`󳿗𧁁4u%􅔪P*􂉻捎C\u001eR\u0016-잚󶽕g𐰺:S>c㮢𠝌\u0010Y􄝏~a)YW_J􃢤P\u0007+ U􈷓j\u0019k\u0001􋴘\u0011䣷e𪋘𪳠,ᐏg@\u0012\u001dHXl.\u0017𥣁2\u0013mY􁢫\tv?L8L􆍼N𠦽\nb1j󾸸𤋵xfQ=\\\u0005e󳇪󹽶U\u0012p{\u000e􌚌jd^@U󲯝tP.\u0012Y%R`a\r𧍮7}HnUf𠛸m^7:\u0015=챼>l𗑑hwp27𤦾jE\u000cx=!.\u0013]Ar\tw\u0014&\u001ak㒞s󾦄ᆒI𣪗􂼥dsY\u0010𬚢dX.𣭷i]𤹉󻃀\rWS\u001fU􌏬\u001a시􈨂\u0010\u0002N~-\u000e6𮙏􏄲\\O𭍍Jc􀻇􅢮\u0000HSo\u0010-W\u00136𩥑I􄺨)𘗘={𘗔h洹M󹩪FwJQ􏞨ck\u001a\u0018|UV-\u0015\u0001|\u0014;\u000c𦓫𣦃\u0005S\u0015.B\"D𧲿#o*𞹱胜m\u001e􀓪B3Gg;\u0011\\𬆳􌒮\u0005 B^\u000f𥐶$e餴𩠵>fMgC𭮌,o🗨\\?󼛣~/s\u0001?MMc;D18Ne\u0004\u0018)*\u0002\u001d㾌\u001c\n\u0002􇹨nI 􁞖V󺪋\u001e𐘏O'`@H띱 m3𤔌+^s\u001fm󵸥葨M𥇪脪\u0002\u001ccP󰸮\u0010𡞰>􀯷F-@V󰠆􊄪\u001b1\u0005w-\u001a,􌣺\u0006􉰡𓅀󵏲Jrl\u0002VX3-􄂗𬜃X4a􏷘V\u001a?=􁋀|𪙜loc\u0001V\u0002n?􂾊)U󴵸􈓴>탔v󴒝8v?⬜l)\u000f\u00073󷓀O𪣭𦪾𣴀U􄹍Dm\u0002.V𭑶𡥷LJ0裒\u001b󾯛\u0007F_􊗰􊾠󾴇0􁛍𦊃𠍞\u0018:OK !jPv\u0008_$s\u001fSC;\u0006\u0000i퀥\u001c􄖬Ze\u0003󰡗LI^1#𑀳\u0012s\u0016.|a\"􊳀{BD\u0015|󼽗l_􍞄󺄙󸾬\u0018\".󹏪D0\\\u0016.ZR\u0001竉\u0004􌥟+𠤥'B􄚜&7LM🢒𣳮\u001b󶎑\u000e6`>\u001c}C~UE𤄡~A8\u0004B􈜏\u0014b\u001c7=\n'𦿣m^z𐢃8[󽛌20\u0019 󱄺󸙁'\u001eu[\\DI\tz󲒆z\u0017Fz󹑩>)領\u0000O\u0001{\u000e󵬔hh􁬀爌b\u0011밲􄂋𨬶68􆚘&u瘫󽩝\u000c&W𣝕P𦳷Z^%{;\\$jh􃇈\r\rll\u0014jI)\u001c\u001a\\󳘻t꼔\u001d𒐲0\u001f0f\u00130Nmzs\u0018\u000c\u001b`\u0010𧯺𧔮^t\u0012r𬺵.f':􎾮l䑤w𩵒󹦯9㛚5􏁴\u001b󾍕@𗎈aU.􃗹\u000b]郻s𨖏Q\u0014􆬛ᑩ𠴟㫿\u001c6\u0018􃷤Q\n𡧂󷷛􎤪if\u0004\u001cT󷔬鵀k\u0006衦\u001a\u001ab5H4~󼴡V𩺒]-󿣢\u0014\u0012`zh􊠎\u0012~􌩰L\u0012h\u0015\u0018𧎽fJ~󴅹^\u001e𢜹{]ms𗟼de{\u0002祫N􇹮H𫶐OP]󹶁𤚷\u0017z󰫈\u0007𨂘4𠏍󰻹\u0017\u001d\"P\u0005𦒍뷕L~Oj\"R50G춂􆾡lhcU􊥔\u001d9𪪵\t{as𣉾\"𐛪v.\u0017\u0001SCD/\u001c\u0013𩐨l􉖹𓈔^𬶮􋅲\u001eo𥏝𡇗$銚T^\u0019\u001d&\u0005Tsl_un󷌗󸧱\u001dMy:pXᜄn\u000f_\u001a0", + "email": "some@example", "name": "󸟵8􌈸:(\u0010󴏏f󵛰\u0007㋗􇷰4KꟄL", "password": "u\u000b6\u0006\u0002\u000c\u001e􃲳\u0016\u0001\u0012|2q⤶󺟂f􃤂J\u0003Mo𬞡 D􉌕|-󲣺e|yfd\u0004\u0013\u0006󺮬t\u000e謱\u001a.SftU󰧸\u0000\u0011<􆍤\u0002'\u0008󳬭\u000eU卒\u001aᠨ|blDM0K8𧖸w\u001e=z\u0015[<\u0003\u0011\u0019𪊆􆋘􊼞\"1x*\u0019𑗔𝟃qo󲟢\u0000o􆍒\u0012On69@󰱛\u0015Zꜵ^y\u001en󻯚󼌤`U󳎎o\u000eE3f\u000eM􋉒C*Q=O𭇁\u0014𠫁/o|\r=z(􀱚\u0010\u0011^6셮?C778D􀷸<~+9𘟵hm𧝪f\\\u0001>B@`*J(\r\ry𡼬\u0011⟴i\u0018-S}􍍓\\𒒟Bh􍭍lY\u0005k>`)\u0019𪐖gZ4􃳌\u001eIAJ􁛰WgoA>텶󱅡󾢬Sw􏲏Ṗ8if\\X=\u001f?d(\u000ft꿁摷\u0014KR􉿽󰙊G\u0015C5\u0000􇆦c[7-kp'𦺝\u000e󽎜ꈗF󼍥e'=\u0004\u0007\u0013Z:q\u0008w7󾸡𠋋{]󻳴Ah|!$\u0007uzh\u0010o􅼹\u000e󹝤𥘼G󹵌\u0010剒1p6\u000eUX\u000eၶkY5(\u001cV𭺮\u0013\u0015􅠾󵷯.JJQg,%c'aq\t5f󷍿驹𥰄\u000c.\u001b{\r󿣰>'O롢\u0011\u0003$R\u0003)󳣀$𪕚Hj7}F\u001c%􏀁􈞩󳲘4𗎶\u000e\u0007𫼛Av󱗥\\\u0002Wq9\u0016W\u0003󲋝C%\u0008\u000b\u0016鍮AzG'4Dj𧙎|+S\u000b\u00029|0$tLe>ol\u0014\u0019􉨃\rmBK橺􆽓bX𮀨Pw+\u001a|e碢B󾉾6v\u001e􆏺\\", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_10.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_10.json index a0a13e470c4..cf3ee530ceb 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_10.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_10.json @@ -1,6 +1,6 @@ { "description": "-uD|3n󸴉+4+@􊦹Jw~𫤕𢾜Q󳯢𗮃r𦆴맹2,𤭁=i\u0008N#y𩡌/8QhH[𪌎􄢋yl\u000es\u0008\u000fW𛈵󻜹𘧄s?`rr\u0010􏱚\u0002j󳁬äq~d}*\u00117\u0011\u0008+^XL🖦0\u0008G􄉠/*\u0016𡦣-!󷊡w@3\u0018\u0002𥞲z8N \u00186`\nr$B\rgmp餫𗕴G\u000f􀃱䳎w􅶖\"𩄧&􆺓􁫠􋌷ﳐkZMV􋛶\u001e𡖽\u000f􊜶\u0011\u0001􍱟𥑇Q\u0012\u0000}󶒤љ𬟴𩇝􏕃\u0008z;􈢍[\u001a-<\u0006'󱮓󳨭􃈗𧳄\u001f{3\u000f~:X\u0008鍑!.V6rNo#^M\u0006a`W^@/􅲵\u0002KP\u0008AAT_\u0019\u001d\u0007md{Pjw𮤣k􃐣\u0003\u0019랑䏈eꡍz5tp𩅷\"#𥫘 \u0018🈠\u0018󼊘z[ 𠆾\u000eAz\u001c:;᮸\tc􂐨\u0018\u001ew⩗􁢱󵩦)K󷇢\u0013&󾱰r:eE~\u001c\u0019ᖽ;\u0018T􃒙𝄿TH\u0002󷷑b8t󽧞^?24Hb=;G󹽉\u0006\u001cж\u0018XGI:|hBb揔:kj\u0013𗔀:0\t`E3t㲚\u0004\u0016P􅸐-iw1z\u0005A\u0010u䁍󱇼𬉱\r3\u0001􍅝O􅛗F\u000c􃬯輎􈽶\u0011𣚗\u0008ybᏇ]t𭑣i󴌂-JD\u0002𦁝I𖤺\u0010\u000b\u001e3\u000e 􆲡􉿋\nh\u0005)n$1F&1\u0007\t\u0019`󱺆􍊛𗏊a#䦟T󻕮𩕘v*i6\u001e-q*VAcXM𥾉q鏛N󽀼<\u001br\u0005&+\u0012$\u001c\u0010hT쟛􏸞i3笧󶵂􈫵\u0017pl􁠀\u000f\u001aR\u001d", - "email": "_e_v\u001aL@\u000b4", + "email": "some@example", "name": "?󱶤웛Ykp*V~z8xa\u0003`6^\u000eT􍥅\u001f􍣫𪷋z󳃤P\u0007n/󹘚{/O麿f󲜇-OG{\u0013伱󺗐\u0010z\u0010\u001f󾭿c\u001e𤀶j𝘨A􎷃􂰭2Wc𝜟󸩰P왮m{\u001c'9Hi􃫊hMB􆦾􀣫Nl&됦8}𢠟⚎'\u0003g\u0005󼧊󵭁%Ps)C𫨺\u001f\"G.󼩀I ", "password": "po!l𩞋\"\u0004𪤚💮z\"mpt\u000ff/,\u001e^z𖢠􄇌Mb>󽡐s\u000f\u0006𢰆󽆪􏃏\u0002𬩷𣷳\u0012m𫪐\u0008>V?􊞯x\u0002􀬰F󳍧-ZT_5󾂿󱾢'uO\u001f뮬􅖁\u0005J􇢜\u000cM\u0018H􋧬YyF/d-)𗎣jS\u0007\u0018F\u0016*}󹁧f􎑳\u00137{X\u001daD훻\u0000n\u0012H\u0010\u0017uu/e", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_11.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_11.json index 5ab91c4ad00..590f6686255 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_11.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_11.json @@ -1,6 +1,6 @@ { "description": "VsX|n𬁵Ue;󶿲󻧙,\u0018]\u001aV%*\u001b6x􃯐\u0007D 󲚝􅌘)俻\u00053\u001d;I𐌌\u001d􅣂i Y𢌔c􅋸𦁦䝠r􎻤V𫼄*\u0010/|\u0011:􁕾𛋕B\tk\u000b\u0004󹓭𤌪.\u0001GA D^9􆷦󼣑󳆶𗤴젛}}6L󻌞FHWcu>({4I]\u0012\r\u0003&|5(KWw넦􍧤󸗠R~󿓘9+F𧮒qG@𧏷X𡜲gn^?$󰡗E嘎𩃄䵜\u0019\u0012 \u0011D󴎣􎰠\u00114O羕#􍴟jC|\u0003^n\r\u001br>Mw󵔂s\u001d祀:󺶿q\u0002e@INe\u0008󼺇􎻄W\u0001𤻑ZU󿐻+틱\u0017\u0016\\\u001a󸷣\u001c𡟜1\u0019䊴􇳚}/7\u000f)`꺵:)%k.u`BJ\u0012\u0001$𨞑\u0018󴼊'9󰆦naҷo六\u001fsC󵺒|󻛮A󹃨𢽣o􃨴#\u0007a􎾗2􈋱󴧁􌨺夹(􁲄|?[\u0001䎲WzQ杺I'𭍉'?𠀟*\"{𞋆\u0014^􆺗Gy㪁qs2z󼖔a<-r\u0013n.<𥐲\u0015F𨊤\u000c\u0014\u000b삎\u0011TR𤟵\r讘", - "email": "\u0007*\u001e_@6󻞻\u0007\u000c#9", + "email": "some@example", "name": "󰁝󴀗-\u0007%~䵖\u0014v4@yim⼁\u001f37䐗\u0002𠧤#@􍘠a}mE\u0013𪌻Es3CꀀTZaTUy𬝀󰀪u\u000e𦷠n\u0004[\u0003𨃁졉_\u0001d\u001a 𐦌\\#𧐀{@\u0011%8s䭟󲗇,!D<𖨤KM\u00115T\n\u0018@sl\u0000*\u0007\u0016xZ\u0006ᤖ ei:V+y\u0015\u000eA䖹dX󱿹󱼬\r􂏥\u0004􍑡", "url": "https://example.com" } diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_12.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_12.json index 11c467b239b..6c55c90dc11 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_12.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_12.json @@ -1,6 +1,6 @@ { "description": "Gt'`-h\u0004\\\u001ci󵣑kꫤEl󼄕?\u0019#󴈫\u000b\\𤜽\u0008/O奅w\u001a0𠽓􇦟h\u000cOs\nP􀴊U0d\u0002􇈶q9\nFᮞ􈑃+@\t\u000f\t(󷧟\u001f󿎢\u0005-3I\u0002V\u0004৲&~\nk kv󿝁9𪥾𦈁x􃪲sC(xNZT\u000b9+u\\⦥\u001eL\u0015L􉹉=\u000f𝣘q^\u0010\u00167%/\u0001jꢤKe𢅉󻣠􁓭5}\u0008\u0019󺴯tDB꤮\u0011􂉟#q1j(Cg硎RG\r\u0007~8\u000b8󸄳𨁸􋫔vOg\u0012;\u0016p굸Ip􍕈+j|􋷖TO&W🛩\u0019=\u0013T眯❳𡸗藺@Dg뗇鏈]m C!𠊡vF\u0001􎠈N&(𬗨苽x(j{\u000eES􉶣\u0003-\u0007P\t묘+ \u001b-\r㤓r􂘰􉲿\u001699\u0007ZG\u001f󺙪L$𡫓{駤𞲩j𨶓bY~i𝞼Ո󰾣􆼣Ev|䂬󾚺\u0016󸃿􏞂\u0001`󱑓Nv󵻱B'?􁓼\u000e\u000f󽁘󺶞\u0008'\u0006s\u0007𮮿ᇇ7'\u0019􄲈\u0003g𣩈󺐈5\u001cgDuX\u0019g}􋗰󻣼\u0008RN󽢹/D\u0011O0?\u0012RoG;=\"Q1m\u0005􍁗\u000f\u0007%<\u000bf艧􊪫\u000f\u0014쳋𧬨N%P􋸈g)=QA]t.􁓴F\u0016;\u00013.wfEL\u0012Ru!\u0005qN\u001f󺽭􆪫\u0012\u0015P@I(#\u0001<}*2𭴢😫r\u000b%E󽁑WlL2x)+󽅗=2;\u0012[\u0012H2)􇯅𔓣th@𧺋\u000b爫\nk\u0010eWF-", - "email": "@d*􀑚jA}", + "email": "some@example", "name": "E\u0013MNO膵\u0004Z󾢒\u0000I⎕5e3", "url": "https://example.com" } diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_13.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_13.json index af210cece22..e0c58e5ba57 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_13.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_13.json @@ -1,6 +1,6 @@ { "description": "W\u0015x;LV𠁿t+#࠷.~󼀷Y+ꊰ\\^(CK@\\\u0002|p\u0015{\\&j^󽫇$P^󷀳􋐗\u0006\u0010󹇸t󹠁=􁛇𧙿dx𠲌ᲢİO\u001bR츴\u0014\u00041GM\u0007S󵫹x$d\"\\jm;\u000fD\u0015h󺣚qd\u0016􂪙G\t𮆤w󺟃\u0002#`6󲹈􃱦$`B𑨎h?5?5!~9\u0008󽶻A􋛟6𪞦\\9(>\u000fq啓\u0018􍸩Kd\u0016𭊭𗱸𪜳)\u0000t0F\u001dF/\u0014*􌯧v\n𡄎%􌉸q󰩥Z㔠V'롤\u0017\u0004Fi\u001e􌀊茳󽏂~𠸵KR\u0010\u000b8\u0006A󿾍b\u001d􅍯E􊼼􆄍\u000e;󾓟I􂨟%~mH\u0001)\u0012,\u0018/9MZ𭤩h\u001e\u0003,{\u0005뇙4뻋Mi7T]B󷨔􎠌]\u000bU&<􃞵=k^𢎩\u0008a?)𗮸8\u001d|\u0007'㗆}@?hL겫QV/?p\u001cd􅻁iv􃕛z鳆77Qj \u000fnhW\tu\u0016%K𩳱]𪑥q\u0016ft􁰸\u0012jS\u00005q𪑚𩸛\u0005h􁹽⺃\u0006鼏1n{A#XQffⱌ\r󻹎3\u001b\u0016@z\u0000\u0003P𧔺ࡉd>|[U􇳄Iံ􎦠𝂍󰖇KyRA㣉+\u001ck妰𨍰\\nQ!􀅕𦊐r4󻹼?\u0008kO\u0010i?JR#V𦝗Zp5\u0015\"\t\t\u0004􈇶\u000bC@󴇞\u000cf􇺆\u0003\u000c㍹诺:\u0004f6\u0013▫푎󻩢󵾞7WnH*怿z𭮥S𗣯\u0019E\u0017\t\u0013TEf\u0013CP\u001f*𗐽`󼻮/\u001ca󻪈\"\u0012󽍐􍯧`𪗻\u001c\u000f\\􄦕$\r\u0010B𔒀󹋉𩏝\u000c\u0004𗇨6~\r􃈌䝅@%􀍬\u0003,I\u0012r@􋍜1뇅_뙧􁾨󴽲𔐉\u001b\u000c%54pb\u0010𐡪\tQ섿)􅂶~󾮰.􋢱`ꁥ􊧑G0󲿦􍂛uj\u0010w\u000e뽱^\u0010,;\\\u0014q󲇟m\u0015!j3t\t􄀽\r\u0017n\u001d)󵽺mT\u0004\u0006T\u0004\u001dh,帨\u0004ᕌ\"\u0013[椁lI𞢂󳤙5\u0002y7z", - "email": "B\u001evq􃫹7@굹", + "email": "some@example", "name": "𣵸敝7\u0012􆯧Y<􄿘+Babu\r𮐴𠘼\u0004Go𨁚:􀾑z􃄑𨻻᧟􊙟tIid𦔶CR+􆯗󼬰\u0013h+Wju\u000f'\u001bc|󾮆?y4L𤃫Z\u001c\nQ*\r慉\u0006an􋯁Sw톘󳘪\u000f\u0001Z䕱I晠Nm\u000f>\u000e\u0005\u0008\u0016\u0002􍪂𪝨\u001e,@+Fm,;(cl", "password": "󷆴󳚁󰁗L􊣬󰱺T𘟲綣F_􆐒\u0000V낓\tHSx)\u0004x󸮍i[㔲*󽆣\u0016t]g󻏭ꚳ;\u0012$U\u001dY)𡦱\u0012a\u0008ey*_垆\u0004󰷚\u0004_\u001c䭭(\u0013􌧽󸧇)`\u0018\tc{*𝜝\u0002󼸆\u0017(\u0016\u000c\u0010\"\u0006𫖽\u0013󱉵{A\u00123\u0008\"k^\u001702𩖚󷦬<\u001e\u001ad\u0016KhꍇL𢗧\u0001􃠽󸞛cG\u001d觚䕦2l􊖱𩠸>ᩗ7g􎙛󾭭HP8t\u0015A[r\u001d$mat-󾽡\u0012\u0003?𩾚M𮧝!󶈁󽹿7\u001b\u00189HB𑒔xL􎅜+\u0004N☟H=\u0016^䟄O𠄃\u0002`n,\u001a~+\u0016\u0018󻜳<\u0019\u0012\u000f>󸔐9\u000e()J[=\u000c?MBFQ^}􎋏,􈝜􊢝\u0007ᒽ\u0011𠰷\u001b 􅄐fL+;𫉭\u001fp\u0005s#፟i􃬕[,\u001b*\n\u0003d󱋁#v6HW *T4h\r\u0010\u001a\u00192􌳥󼿹㚶\u0005lJ~j\u001dd\u0004\n+n𢎍\u00064\u0011(\u0005󴠒󶩬7y+\u0012q\r0\u0018eF\u0016󽗧𡕬Y\u001d]v𨻲\u000bᶍ󼹶cY\u0016\u0002냑R𢅵b#-􄿇[P\u001a1\u001b逕<󴂆\u0006\r\u0018<\u0002􆇨\u000b1\u001akw}\n(􍎾x8N\u0016S\u001c!zw\u0019􊪦/\u0008㉲͐o𫍥󷯂󽌪􋍄\u000bwN\u001c?􈒒7Ml󲊇DSr􈛋w\u0005\u000cM󸰹󼦐$I𢼔𫸳:N\u0018\u001c􄦬{ \u0015\u0013QspHP4F\u0008\u0015ᴣTI\u0012\u000e\u000fp𧈋󰑼2&w𥏖6\u001d8㾀-\t\rA䎐c\re5\u001f\u0001D𥥰k𤶶&O􆤫\u0008L4J)NSkhAY,v[󿟁wPL􎨹a\u0004\u001a\u0003\u001e}\u0007O6\u001c\u0006𧝋TkjQ[2\u001eY#X4j\u0016+󿷞袑O𝅜S%\"g\u0017-􋑪𗻯􋕿䃓\u0001!&􇰣3$\u0001􂼮s0g\u0003\u00132\u0013\u0018kv5𧷄fI𮎜'\u001e5\u00008󸏮]埪\u0014􇓑~Qs}IGki8r\u0004𣓺奦\u001d6C𤪒ꧠ󾁵𦽑ὌDP\u001a+~𤂞|m+Wp𠈄\u001b;N􉅴@\u0019𑚋뛪애11󵪅q󲮿'l\u0013󴾸Q󲮝􉰽sK.\nx\u0003\u0008V􌢉\u0013𬗦q{\u0004W*l\u0006Ox@<𢫿._~\u001ayJ\u0010#Fw_󽢞(i0\u0004o秚 \nHy+\u0017pF&}\u0012O4\u000e@ZDt\r", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_14.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_14.json index 3cbad62f257..48298ed480f 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_14.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_14.json @@ -1,6 +1,6 @@ { "description": "<*n\u0001@磮-\u0002\r\u0014Wr\u000f󶃔\u0013\u001e􄩔|\u001f蠓\u0001E&𥚯󽃸𖣹!L\u0005J\u0006㞤󻮽/>\u000fF}5u\u001f\u001f9\u0014𪦾1\n󻢂􄓒I1V󸇄m\u0005𗬨g\u0005x󳫌\"\u001d􎭈c󻔔\r-/\u000e县Tf9𘗓\u0003a󽦒󷐱q\u001d󾧩\u000f1𭋛wL\u0000󵤪` o^𡾴D\r[!Z\u0019]𪷎\u00011\u000f`_xlwX~󰁍\u00109󻮑l\u0000^V6`𑵕􆣤\u0004D oo5\u0004u\u0010MA󹥡\u0005\u0019i㙌\u001buc󱃄z7t􇌓W𠺚e\u000b)9󿨭f𗥜Fn𨈒\u0011\u0004e𐲔\u0006\u0019(\t𐢀𥠠𑊏\u0017💋Eb􅶎1\u0018D\u0015>mqH\u000f󳶢𗆸:\u00101_l,\r@O?󻫸\u001d\u0017󵝽\u000c𒇌A􉙜)6Y筎􉓽N\n\u001a(^Z/~o\u0000qNdf5\u00158`D[t_s𪾭\nb=*\rQlb\u0019h󻑝O\u0019RKs\u001d\u0012\u000e梖q䚖=J@+\nz􄑙몎U,\u001aq􉘁7>M\u000bY􎺭hꋷT]􏈌[-+蕚Z'qW7%􌖓j6\\u\u0013\u0004`k𬅩!o􊜣\u001c\u000c\u001e𤡉1𬥥5MwmP;%Oԩ]Wp雰󳦸𗮑)\u0014앻󹳢MR󾗰P\u0005'\u0001\u001a-\u0013d􊂮{𭗺'<\u0001\u0018RGw𥵓䝘s,PwtQ~\u000b\u001cN\u0019\u0011@᠇R𦙊@\u001b(J\r<៕\u0007\u0004f󺤮겎~t\u000f􆰭|D()󱍉5j󳴚𬔖\u0018𑘳􆫦*􆴸#[0󶠞\u001c𠏴BCWP󳮺g=t𝇨\u0015󰍀󲒴𩪏V{q}#((𗹡6󸠝?:'7F\u0017㏥b􌊁D+j\u0005\u001eB'=󷅸\u0014􃚑􂖸k􌑞􀎄SB􀈸\u0013:b󳗤둜\u0000\u001b􆿁n?󶸶i𓏸8I趛[%[\u001ev /咖S𡢒𪿛?􏆦\u0018󴵽=w󴤬V󴑂톘\u0008]\u0018\u0015R%􌣀􋢘}􃤣$𭼥\u0015􃾁4\\\u0004|𢙬Ti󿃆\"󵡝󿣽\u0017EX g🎼 ❢X#q2􁚨<\u0010蘀􃬺H𪦠\u001a3W􂯌A𭋀Rv􅚳#𥰉B2\tF*𢤴wibh}+y4c\u0016𦀩`4󿱬o0}􂰵鲼EC$-\u0014b\u0012?CF\nK􆭦􁈶𘖤Y\u0002n7H𐮫p􋫁\u00117󻲿N%\u0013O\u0019{7􌷎h𗗭Pn𧯘\u0007頬_u𧯫洤\u000e𑚄;<", - "email": "%a󽕷@\u000f\"{", + "email": "some@example", "name": "\u00073󻤋􎮭𣚜W\u0010\u0003f\u001aW󾏐\u00126󻾞𪨱^5]b\u001aS󰵑\u000c􌲠|=􇣟\u000cYu\"\u000c꼘V⬼\u001a4S\u0017󰁒􅍞9DD\u0010􆴶􁧱.M0p)󼼈>𢚮.\u0006$z,🍁s󾹻P􁓐gO_p󺌧\u0001󻃦猎𦨕7I\u001ah𮉇qCk7󲷟𬣺𦧩N󺾿󳝭c롖t]|􃃏as􄯙ね􍩡5!w%𬫥*\u0018X\r󶾣R\u0011𡥒\u0007@#󾣛𬮳~\u000f\u001aP\u001e􇙭\\远9s>\u000e##\u001e\u0015󻼌􈲺U⦍g\u000c{\u000ei󰩴\u000bꌾ䒨󳀃\u0003\u001deo@E\u001en\u0010\u001a墠\u0013PZc(𗛧􇨮󲛥V2󶳧z􏡂\u000e愐.􅰤\u0006Y_#`&\u0007\u0016􏴥5^󲮸\u0019]Na\u000f􈃾\u0000%3$o=𠂠\u0006󻞷􂫎쿥%👡\u001a\u0018\u001a􌸿L𗁇Q/S\u001c󳪙<@a\u001c&!6𫚻~􆲇䑜\u000b𣊄𫜄\u0008\u0014\u0019b􋭢1𭹝#d\u001e&\r}𘘥🨆%t𥮙U|D\u0014m𬬾\u0015#yC\u0013amKV ?&pR8&4=󾑅\u000e3󽋒ꯏ*t2g\\0􋖁\t\u0013b\u0011𐬟{z\u0012%Hk􍰘sqUA=&A5Q+\\&𗣥􇵺A􅊛{^\u001f)^5\u001c\u0004W\u001a\u0010󵑴󱣬$T>*\u0010\u000c㌃c𔘙\u0001G\u000bV潄\"LB9\u0008🐳+󳨁7O+柀\n`q-8 iu𦻻/\u0008󹫪\u0014㨺E\u0000?c斝\rK🏫~\u000b𪻥w{𬐇\"M)GU󸕅Y_'\u0012A󸰙􏯣􆊶z}UukH:8J{\\M9}\u001a_O󶯓󴳋󴯊A\"\tKA𠁏hHA\u0003t𬷖\u0013􎷋\u0007󳰤􏙭\u0014󰛜\u000f,0\u0017Ps𧤾P􌯚曜<2Mf𬑂2𘥔MQB\n\u001c\u000f@\u0018\u0001\u0013/\u000e\u001a7z􂓴􋭉9(1Dh󰓭ꌉg\u0016\u001eb0-*U󶡈\u000b󱰨i􉈚𩥘D쑎Qӿ<)𡲵T1F\u0011􋙰$\u0004#Ꮣ8\u001a(Y􋯵\u0010>􈣟쑘𐬕𗣄AC󸇂2𬅱0\r\u0010󿪷{𬝗+\u0000y45󸏙~𑒕Poa倞T\njꇅ\tf󼺍O􌏅\u000c]􅂹\u0003뛓&\r𓇢yF_\u001f\u0010ASe~[=/\u001f \u0004\u0008$魔􏫽g\r6x\u0010P\u001e\u0013疹OO󽽓r-ARf", - "email": "𫘋-\\@", + "email": "some@example", "name": "\u0000A]𭔢Yw\"TOP[f\u0004w󲓊W뀥C=m瑜@s󲾇Z􌿧찯1 𧢓(\u0010树[\u0003V\u0008*𭎳\u0005󳤄", "password": "UL󻼽ཽ󾻖[𬫍ki􍵋ꦼ􃀃dMzz𢁁\u0011\"!Ut!󻇨𠀏󹯋u𤤔!𦎥󺀥K+'!x*e$^4\u0008􅭁􅊝&\"􆊬䠽P\u000fK􆬋𣼗\u0007$\n\u0014'%𥁋\u000f㜯󾸆\u001fq?𧸸N 󲵪NAsu_f䀻\u0011S\u0017J\u0007g(𢳳cee~􁔂Wᯘ\u0016`-B􎦦𗫘Zc\u0016\u0004𪧘.\u001bt䁧g\u0018𗊁\nt\u000bKD\u0001󼠟𘅣w\t(\u001a\u000cd\u001a󺑤J2\u00056;e󾺤c\u0016󴟪&+gU`hp\u0011W󲀝<\u000bq󵢔{􎱍vM󸱛⽃'c𤳐)\u0001뻹'\u0008Xw\u0012y-eil~󸊞s[𐑤􂱯󶁮ꕗ\u001d\u0011\u0011\u0015CWK킷tJu7􈚘\u000c[ꁂ\u001c󲀕&\u0014𫙀𩢐𭵼C\u0004db\u0011T\u0002t\u0008vs\u001dc1=`\u001aKmu:3\u0002􁸂餠RMW)􁡽c$Qk􀊩2\r+𗛝棿EC📭R\u001cm\u000fPG\u0005X\u001dN\u0015[Z3aVL?􋐜|ﭴۛ\u001a{\u000e𫕹󵬽桰p\u001d'sp\u0013𠷬󾗯Y􋒾𨏂KD+X䤪\u001c𣽐vT\u000bO𦒥\u00056KENY\u001aT󳑛5󻞦\u0016绔K`g 𥂆𨀢\u0011\u001dh]*(w0_𫶚=\u00178㊮b󻯕6쒻𢸼P󼨷􊚮ꆝcI\u0010㓅\u0013𬙯忂\u001f𦭮\"P\u00033󲨤c󷰔 \u0007\u0003उz\u0016.^e(Wiy\u0004\u000e$\n泹>􉍀᳕r󾷆)^AE;\u00149𭈂\u0001𝜴\r𢔰$\u0017􅩗\u0006蒪0𤗃6𔗱\u000e\u001f藴\n_\u0005󾊂\u0003\u0014􌫹u\u001d桏Q𥰈H\u0001\u0006\u0007􎈙jiyh=<\u0017)<\u001f$󶤙i𪒐? I\u001ap\u0019f4V𧎤xPﲨM7G2\u0010􃨨􆰊󴌷󱁞JF^󽰗􂦧\u00143\u0019𨍌\u0006\u0007/VMhe䤸Uw\u00026r󼰅𬏛P\u0016>]Z1\u001fe嶣Os*3􏈑􈘲𔘵𤓜􏨊𐂗=󾪯\u001dtDk?𥥇9t\u00108\u000b\u001c\u0010(⟏𣴾NCw\u0014<􍡯C+(|􊯽􃘣&).\u0001\u0007+b\u0008n'`Mk􁃚\u0010熟\t𘀉h?\u0005{/\u000c󸡜9\u0015\u0012@씪󰘪g𘜭4vKVT\u0003\u000f[O\u0004\u000fay\u0008󱂲\\s\u000c􋏿\u0017\u0008􇝎<|P𬑐\u0017=a󴿘\u0000D$󹠾b~$󸴲𥴠r乄\"R󼄐\u0003\u0011秩M*sC3𒈃k\u001eo\u0019,\u0015\n𪫋5\u0017g1!\rd5-󲮗𮓃C󶖙&%zz3🛌􀅳菊:`M5B󵅙ઙ\u0008g\u0017Qg􋓴Y𭁡kZ􁐗\u001b𛊆\"\u000c\n\u001aFJ\u0002\u0008\u0011b=\rE6Nെ𒍎Ns󷅥!J􄩫\u0001\u000bVT\u0002󴙧􎥶`2/啀\u001a󴝘za]\u001d)􅎣l󷿥𑴓q􂂸􂵢dtj󻑪:g镵₠𡵦*U\u0012\u0001u\u001eI􀁻i鞎'.=\u0005W", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_16.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_16.json index 061b6f49e32..1c51dd3be25 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_16.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_16.json @@ -1,6 +1,6 @@ { "description": "X\u001b&%\u000fr󷝶tGpJDG𗦖D\u0003𤠣p<'a\u0004kQus[=􃿘\u0019\u00144b\u0001fꊁxx\u0016􍷁c&Y`῾&𧠻\u001a@<0C\u0008&op\u0012).q\u000cR?L󽣈&􆽉\u0018Q\"k\t𧦰󷃓𪂧bg\u001b󵳠\u001dAK\u0012Y\u0008欯j倅.*WG󴥕K􀲡\u001c\u001fZ#\u0018\u001a𣀧𤠉_=𩰏I􏖍\u0008䌠r^\u001b\u001fA~\u0011\r뵖\u001a\"\u000e}𤉳\u000b\u000b𮤱\u000b󺔇@\n0\u000b\"{e\u0005f𦽈D𘀦􈃼G党󶪩󼦼猦RB䉂\u0014%upl\\0\npL V@󸕃 #ZI𑴤pv\u0012z.+@l;딴󱡷~QQa状\u001a𪍿\nk鶑\u0002󶽹Bw鰻\u0013J𗵗w$\u0018\u0018hJ\u0011􅝦\u0011\u0001⣺*\u0010j\u000e\u0006T󻼡o𖥦󸐍1$rQ?oKn󳑨t&󸻱~\u001d3 R8𪌯𗤋[$(󱐋Z4󽢚a Bp1𢩍t􋚕^/M𣟭H\u0011\u0011lW󳇈\u0004(󿡗)@4􋮕dM󼐁\u0014 0m\u0002IkmV\"\\𥮇M3_\u0001𘖆􆝐􀪈\u0004k^TU\u001b1D\u0011E$\u0013􄒠\u00042''Y􃓲g#$eꃄ<*YU\u001b]\u0012 ],󿨶\u0005~+@\rZ@Uw&IMz󳁬\u001f-\u001e󵹟\u0004ArWv-[?􈑞\u001f𤕵w\u001b\u001c\u0011𫀅󽿪􍛕𭈫\u00175􃲚􈐮lr\u0000ka&Y􇙦;r)U\u0002x\u001b?Uw𨕲\u0011\u001c􆸾􋙝'𑀜𦤣􂓫󴞀\u0016\n-꼇]𧄄\u0015s𭆛<\"[>g\"\u0001\u00029󳍟dY􈞋\u0018녷\u0018􊪨$L\u001aIO}\n}0(5?k𠔑󲢈󻫖\u00034ꠈn\n샼𣵺;􇡔\r\u000bP^u􏸙\u0011#㩦\u001d8󷒸\u001aU󿲾O`cT$o\u0013󺖣𤨻㊤psO.|[#\u0016X[\n\u001bM, 􌵶&y,\u0015,`d,\u0018gMNr\u0000\u001a/౯E{ⱉ9R-\u000e\u0012􇵌E.\n$", - "email": "\u001b\u000eFk2@;", + "email": "some@example", "name": "\u0016\t\u0017S,gN\u0006+mK󲯾>Yz\u0016^jl󱍧󼉪/@Up`\u0004䯎\u0005!󾿈Q\u001dH~?@=b\u0002_xీ\"𪵛cfF􀐩]㐍XP\u0008\u0010\"\u000e􁲕P'Z\rC]󼕬𐬐􄐢#󵌐Cq󷓭s\u0004􉴯=\u0016\u000e󹯹(%𦛜rU_~󴈣\u0016o|v<\r𭖡𫮕", "password": "ScV9\u0008湃\u0011/\u000e􈎅~\u0008@\u000fN\u0004p,\u000cﺵ󸥄O3鞠cu\u000bK𗐂𬁬󴅋Sfi􊿆&􍂑`\u001cG\nx𮊜0\u0018\r⓰>\u0002xC爩\u000f􊌉9󵲄\u0008H\"\u0018IA\u0012R𐆗&𠨻%\u001e󳇹Y㆚]*\u0019f!g\u0005;4􃱔M%􁒋󶗧𨛤h𠁳w󻔼􂵾絖\u001d\u0005~t&y𐇴i\u0008󽚨`ퟺl6􆮝􀽻+☼\tj𝡙;X\u0019)@m􅗾𒄑󻆣2`𥺇󺚑mw=wbEA\u000f&\u001b4wE􃦙SN䒓ZQ􀒨)V\u0015QX\u001ce\t-I\u001eeY{F`h\u0000x􆁑牛\u000b%\u0007p)tXiOV;+'6JKh\"\u0005\\h0\u00170󼺁􅧉K;󼡛3]H[󶱔O2f𫉳􇕍9iOa\u00030]!􆁧\u0017\u001cs>&󸤠[𫢾O𤁦/k\"h􁟿~#'G𐼾魑vmN𣁑\u0003[T&i$Vj㫫\u0015\u0016$􄛶𠿸^\u001chM\u0019\nr{4ੱd􏃲󽵨IG\u0002𗀲", - "email": "lW6/@fD􊻥", + "email": "some@example", "name": "\u0014*43􏰸Xg\u0015\u000e;\u0015J}󾲰wZJ󻍓.=c_\u000c\u0010I𔒨\u0008mU\u000fnT", "password": "\u0008\n6Pq=􀪁(y󹅻y\u0001\u0016􅜝PO4🦮LJ~𫸗\u001dXn Kc'Fn;;S\u0019]𪯎t󹝃Ib􏆢\u001d\u0015R􏳚FhbLz]\u0000\u0001🠼\u0015\u0005󴁧𦠐\u000fg^,\u000b\u001e󹏐𫕰#F\u0006p𐭣g\u0005i\u000e\u001a\u0013W\u0012\n\u000c\u0018\u0015赥wm;iKl\u0018$󺄓\\](*䎸M𦰚𘡲\u0000\u001aw𠃟t𪬤􆬳􂧈/넟$USI-\u0014\u000b%󺒟[궗󳙱*dh&'I{b󺯯)Y󲙂\u001b󿌅A\u0018Lั\u001e.𩅺i􎰇󴩥Tie=} \\(S'T\u0014m\t\u000e\u0010T\u000f\u0004􁱟m𛇖[1𩂛H𐴝痱}&\u001d晊^󺣵3H󶬱}G󱝳\u0016衃󱊻𫊪U󾯫6􊓸\u0017󴙑*b⋂(N􂽔x\u001eS󴌑􎫛]󳳅xL鷤}𨘩\u0008𭲗}𤉰𓌅\u000c\u0017\u00155K죿:𦯁䨰\u000f[􏫺\u001c>v\u001d&\u0016\u0017\"jK󷴃#\u0000㷋?󺤳emU\t3a~󸱑𭱸𥿻*u󳹑f_8􆐏ي󱜄e􇻽𥢷2ᦸ󹧀+c0𬢔R\u0004\u0002闢CW_3𭸚]󸔑&𪏥qU󶯙?7\u0001앯\u0012gc55u儉阛쨬Xt\u0012H𐹼;\u001b󾲠󵏐pP󽪅󰈁𠮤􇨽\u0005N-\u0016)06󿞳󱢾`賦YnY#(技t\u0013&\u0006iu^o5y1若#𑄹=𘏒g)ꚾ𦨔􈉂?􄎺𣬁\u0015浶墠 󶱩!K5􃚵fk\t;󾔲g\u0010l1\u0019\u0014Vgr$", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_18.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_18.json index cf6fe2fe510..ddaeab983a6 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_18.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_18.json @@ -1,6 +1,6 @@ { "description": "L\u0002\u000f\r{𬤮kn횤􏤙VXQ\u001b\u0012𩐆󻡨\u0003熀'􀪻햂𦸓\u001a\u0004󰚾\u0005\u000c\u0005C{@𮋅<[a􃗣gIW𦰆!R􉂊𑴺􋖝]󼀳z䜽𒌑\u000f𝃐N赂\u0014\u001a%1,T&􆉨*; +Lu\u000e󰙔nM󴐀\u0018\u001b\u0016X\u0002a$\u0010acT@P󽺹&𠑄t]k6욞;7Hළ𠑷𠔻MY.\u0007\u0000\u0007󿠾B\u0001Gdb=\u0000\u0010\u0007\nw\u000f\u0015𩸞𡑝Bci*\u0006A}y\u0014\u0008󲚁􁮓Z=󶪰S􍮢Rs(\u0019\u0010\u0014󵰔\"zA;jEcJ𗬦\\􉍮_0v\u000f#P\u0008􁚌x㰒sx󼦴𭷁𣪁𭝣$W􀖴S󵴼􎯳|6-d\u0017\u0007.\u0003􎱌􉫼)\u001cu􌼍.\u0006󶝙^l丫0􃄴q𗕿\u001eY󿌖ag陬Hᷮ󺎒􂷑4e懎aX[#*𤰐=:z냑󶇴􌜼7\u001b\u001bH󰣩dl병􉤍\u0019O𦌺'􈪏󷱆7:]夆x9JLR𫐂\u0001􏒕eGhco𩏾󳩜󳶠𮩭􃤗/]\u0001R\u000b=B𫋴K\u0001@Q%i\u000e\u0010\u0005\u001fzfh󹖑c<(x:_󺞝$\u001e|?_\u000c/𗐈bRvD\u0015󹒓66𫎍jr{ \u001c󾚀h?󰐐spq%&Rxm!i\u0011@󾤌𣥊2d􌒞xn󵖞oU 󱁳🄳f𣇇\u000bZg\u001c2􉟽㡭%T󰮚\u0011y豚\u0000B[\u0014`\u00110\u0002𠨗Q􉶡\u0011􎄿_!G*𑇄𑢶\u0006􍥠R\u0001\u000e1RV󾕌9<2&34b\u0013D2􌎯Ow.l\u001e\u000e\rAr󵹧𡖢\ra)🥊]!a\u0018\\9Vp6󼷽󼗋M\u0010\"}`8V􊽣펀ne\u0017&􃉃q𐌌\u000b咥\u0004Dcꑰ5a4U8󳐝fcm~উC􅯫󰚠4(􂗳Pk\t''M[茫𤗻1𧥶CBSt)k\u0000𠧡􄬻;s湚~8ml\u0014Ʉ󰓸󲑾ik&1}L;D]K𭁚\u0016􌻔m􉈾\r$󵴎𦺭$|𠻰M􉼑􆂫}+=\u0018&􃾖􎀭1𥼗𢚞w\u0017𫙶횳\u000b𨈤􏕓􍑂𭓡\u000bt􇋔7d𦳥e>38R􋟽􀿼\t%i]\u0018\u0006\u000c𑆻^\u0016\u0003t6´𣞸􏸋\u0015fgC7(-tv{mb􋦜=\u001a~}L]\u0000PpH\u001cqW]\nW𠕄\"󲒠\rgr)n'\u0016\u0019\u0004TNk鱟N󶍤Z󿉥􉐾\u0019󺕆𧣝㌉=F@!`\u0003;\u0014F\u00160k|8\u001envY ␏S\n=켻!l\u001bnUC:[HV븱\u000b𠒝\u0012N𧦒\u001czV󼟄W\u001d󶴷𫞍􄅞/G\u0015X𦀰\u0018 bA𬦮n\u0019tN𡂆%%a􈻝8`󹟛{_􁊟𘄾\u0015\u0001Bm\u000ck'Uz\u000bIi\u0019y􍋿x\u0018薱\u0012RmF}Ej\u0018S;s\u001f􍰕-\u0005Gi.p󹫰Y2𝕽\u0003^VUx䠮uA:,T\u000f[y⣳𭲤/\u0007󳔵\u000cz󻺅tq=,S㏕軗-0󷝴i3ld`*󲮭\"\u001c\u001c!\u0019Lt'W=-MW􇏏𦉗暈􌾿&x!Q𢦅\u000b􋥦0IY󹀬\u0001q􊷈0G𩥖h\u001bH \u0012𑋑􄽩62􇱽e󺬟󺷂/_@𥸾\u001a\u0010\u0013~(_\u001e4H\u0003K𑶘}\u000e􇧌Qb􏾉Js\u000c`\u001c􋵙𧣦(\u000cf\u001c", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_19.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_19.json index 3ddd6ec957c..05d93a39a24 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_19.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_19.json @@ -1,6 +1,6 @@ { "description": "0", - "email": "􍣯\u0018Oe\\\u0008@􈊍\u0004􍬇*\u0017(", + "email": "some@example", "name": "\u0013-\u0018\u0003ﳑ\t/㑋T􂙫\r\u0019􇲪XNiu\\LB.+􈬒𠔺\u0012󴬥", "password": "\rAc󾜌􊍗n𫴚N\u000c^&\u001eHl󵻶\u001d(V\u0016柛\u0014!\u0014P󠅶𠈐j𦎢L}D\u0016#u󾪬[\u00066y􎏝d󺍽Qr<󵢺l匙89􉐤.󰯑JDWC4\u0008𗍈\u001eW~t\u0010x\u001a􂓫H\u001aY𠫗ej󷰁\u000c=\\\"z\u0013NFooo󸡂󶬠\u001e;n:<=fq=𧞈\u0002󲕘ZV,<𢂛\\󼏟aL󵔫`\u0017.8􇀌𠔟0󲏯w\u0000*􆬬![𡌐", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_2.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_2.json index 773701d7452..49216e07811 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_2.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_2.json @@ -1,6 +1,6 @@ { "description": "􌠋Ygo%ΰI󵘼`F\u0017YT\u001f-3󲔬沖\u0011G'㧛<艔􁗩&ꤟ\u0015𗅁𒁆\u001b]\u0012\u0015)!\n2K3ႚ[+{\u000bCν󶠜\u0014L𛇬\t􀡜E\u001b]AF\u0018褯􈞃􃥇\u000c\u0000\u0019}w[F䡨\u0000𮈤\u000f2𪣰^z𝜭>\t\u000eY𠁿$4f\u0005e\r􄩇E{􎩉\n[>􎝂󲊋􏩵xa\u000eEG󼠍󷅍usz\u0018􇺊Im\u001c1[$󲍚9/\r즅S1U𥶧qK\u0005,\u0012攠󼆯2󲲦ElX&92𭕖T\t\u0017\tkM5󺹑,󳯚#􄢝~'vmCROC󵙙`q(\u0016q􈧉]<𡒠h\u0018;\u00179산𥢕猁X𠋪eH\u0005󵷟e􌒌\u0010XKDs\u001a\u001a \u001a.ྦྷGZ𞠎@󵱜QDOJ𬰪􄃨e𛃂􀉟\u001b7󲖒瓛L󻚂\u0013h=[F􃚺\u0007fH𡟕f𛉼*AQb]ᅙH𫞨𦸁o\u0013&𪨓4\t)yBn25pNa𬛓Ex>\u0006%x쭄B\u001f&MFo.S􅁅=m&us𠯹j\u0003{\u0012:𠍇\u001e􃣩𦹪󸛑𫂬!u_\u001b?毀1n1\u0017zg􎒦N\u000c𫆇?㹽p𠯰󻺩\u0019\u0014X7vS𦍁󿼙P\u000f'󼦴󿉗Nu=.4M𮯘Wu \u0014}N]a,🡹􈿇\u0015)|ぅ9k􀋳\u000b􄚨𭤐\u0000\u001d𩓜\u001e9\u0018Tl􀋌󳚭\u000c읭O%􂲝+D\u0015\u0014\u001bY󴎑𡺖M\u001fc\u0000Y\u001ex\u0004c):󶄷\u000eJ󿀗p\u000ebS?[KU𮝱Ap_𨂥\u0003󰵳\u0013C\u0007/铆1mEmb\\ʝ\u0018\u0003B󾤢J髴&\u0016Z焭\u001abniY\u0012B6\u0013\u0018vm𘧮lM*𘏂𣽸M\u0011趮펁󺻊\u0003𣇬Ee𦦒󲒂w핽𓅢\u00017#[ck𘟪u0P\nPpN\u0011`*\u0004:=-hn#\u00011u:|쬂/\u0015􂱬>\u0012\"珌i^1ER\u001e󳋮8ѥ𤴖🀶)N𑚂wm궊7\u001a\u000c𠓓\u0018W􏷪1jf\u0002\n󶾯𘤗輐D\u0017x\u001ec#􁽅B~󰎞T*\u000f𞤢󹡭0.r\u00049F{u\u0012", - "email": "S󴈑k\u001e뿋G@T,", + "email": "some@example", "name": "Tx뫫q􄅀@\u001a󺕹\u0007W\u001cG\u0014\u0010$\u0014$`\u000bO?\"Z󿢗􈟛5K", "password": "yx?@/􃻮I\u0010􋤗]L󻐣\u00126C\u0001醫\u001eF0A󶰭\u0011\u0007쩖v𦊍=ul<\u0001\u000e􏖾㝍=}\u000bZ\u00125\u000bZ酘yy]싻\u0017󱫒󹱓\u0017\u0007g!K\u0004~kg\u001c3<\u001d𥸡\u0015󾳍eT󳗖j􌾉j+*`𫚌␋wprI&󴇻H\u0001󱩖Sb\u0014.LaL󿧘􀒝\u0003!x#-􂙯\u001a 􋷢\t\u0001o􂶶xc􂁒y\u0003\u001b𘪤PH𘁨.K\u001f󺤔b\u0014%6\u0000汳\u001a}Q\u0002\r\u0008\u0001󺋶ໞ\u001f􆟛\u0002!xvB4-g)Ꞩ\u0006!GN<󱙶z􌝇􎀚\"𨚔n𣆠wr2lY𪙶9k🨀hL粤/\u001a\u001e􏩚}c󽞆10R\u001f\u0018isO\t㦝jP#ziᑉnaR\u0010\u0018a\u0016d῍􈙑OrW􁯆𔐴𞤟\"𡪠잲\u0011+'\u0008qS\u0006󳨆\u0002󰗈*:󷡃!𩊳J\u0018s{\u0010𓎛#􎸒󲫀u𨆘\rk\u0004ceB\u000f󽥡\"󰾉㗠q𪴆ed􅏤􄚜𗐵~䕝~]⣌'\"vi\u00069{[zG􋞠C^􌱾杞6\u001a𭽅-\u001em\u000b/\u001e𭗹Y:􎅺􌊹r\u0001x􏕬󹤢OR+􅴓㞁𗲬>𬎏:q0'\u0005a󴹠np{\"(􇠌畷뱓#a󵊔𥷃QN$T󱰦?Dcj韖:𦛞#nW}𗶖z\u001a𢼌n$􋮙UP`\u001e?1Z𘓚mx\\峱!*􏫗􌕂\u000f􎿫Qo󲳁󿨸\u0002\u0007|/\\6\u0011𧳩\u00147e􃩱y\u0006T\u0018f&_蹫^sy􍆮𑈝;^\u0014\u0006#l:i Y\u0007|f\u0017.$챶\t\u0011\u0006􉳓p\u0007􌟫\\l\u0010?di\u0015𬜬yr(􊩣O􃉁\u0003Ͳ\u001f𨡾h􁡤\"j\\\u001a}\u0017𬈊\t\u0014敏󾎣|']u󰽀󿵘=8\u0000\n𝇑=\u0013Bkuj]<迧춼\u000f-𣘸t㌘'󶖨D튙&\u001f\u0010T??􎑽\u0017\u0001𩔭\u001f㬨t𬸂\u0004|zUk\u0014󲡼\u0006J_4L\u000b\r2Egy{󵏖D󴐛\u001f졽B𐬂%󾭅\t\u000e􋏦𗕻X\u0015#\"ꨔ\"󻓰W;ar𣌆𧱌!B-[𑢮􊣵󼉸䖢#!|k-a?O􇽑𐑍Y\u0019*l𠥟c(\u0016]䏴\u0003c#󽍄m;􌅂𭤜2X9吻V\u0004b\u000c5&\u001e𧥒h􂳣O,gG鸱睪\u000cJ\u0019󽸟yex􄝣\r\u0003|\u0000>\u001d􃏘g\u000e\u0004Er\u0006i=O&肧𗎴𐔶\"\u0010V\u000f\u001b𗛾]\u000bA2\u0016l𝤻~pn􂽗H*eK󶁶%K4K~\u0017_O􆿿fV%󲫤\u0010󷜁b\u0003~7@'u􀻴𣞢㹡FfsljK~\u0014Igb𭐖𤤡󸣹8Nz􄟨\u0002Dy𝠤󻑝􉟸3pd3]^NBዥG\nG}7\u001b㙝􆫡b]􉘚WM!Hf𑲡e-𬳆\u0011I􇢰􁣥)P\n\u0008GK-Qh5\u000cS\"zA鍳\u0005\u0003\u00178m|qL`\u0003𡲱𨹷:\"*􆥂;hq\u00103b1𑰡\u0003{镓_􌣚9􈣓𑓅\u001ace𥪍\u0016\u001f櫫F詵\n\u0002dJ󲫳c\\4\u0005𝩎𑒪v\u0016\u001c\u0006\u0002𫬢iY􆧒緬4n\t􈞢/𔔭\u0011t(S[𡠸􁷬00`𡛙~󷣫^󰱤\u0002\u0000e􂺙\u001d[𫺴TsJꘑGܱ\n s \u001e\u0018 ^!b?t\rFe푥 82􏵡􋀛jf󶣥\rF

,驕UF􍩪𬤚4\u0018􁴆:R𨢪Jm􉺃𡬊G`7_\u001e\u001dX𦴂9𩳮T󾎺E>@\u000f\u0007@藹\u0018f2'Y􁐧V􆃵U󶿸R\u0000\u0013\u0006u\u0003\r𘩷g󰝮1d󲳴73)\u0002u\u000c\u001a69=\u0007\\!D3𦅍􌇶[U\u0011l#A𠐮@\u0014_\u0017b🖋𡐟ed\\󶁠h%\u001c=U(I#\u0015𗬠\u0015\u001a\u0005%J\u0007\u001b39}j_뾊\u0000눋|h&i{\u0014(\u001e\t󿩂/h옆𖼚}\r=eRH􎚔6\u0007\u0013󿣣T\u0008𢝂jD􎱋&\u0010k𧚛󿤼)𨜝􀼭E𮂷𭟾&K%]X𮝍^R|+\u000fUc\u0015@\u0002\u000cLI􀾋eH", - "email": "@\u0014:", + "email": "some@example", "name": "s𢣟\u001c4,v2\u0010E\u0002F\u0016f\u0016\u000b$🁧󽘝C􂜈􌏫𠶐*2K[􈩦2?\u001d^1` 𧥃6?!􁿝!\\\u0002ァW/䰄􄓓\u001e=\u001fM\u0003~.W", "password": "Wx\u000e\"\u0012\"5v\u000b𤷙\u0010}\u001e󽮔w􌤛:^ꨦ\u0015\\Z􀛝J&\u000c<\"𘏃􊾔&daGH􎀿\u001e󴶛􋿸~\u0010\u0006o󰻅속L􉔴㑈bέ/겠𫱟-#@_Ṏ\n🩬jdIs@r~k𦖺󹞎X\u0012>𭉲􄊚􅬢\"\nN5𬰸-d#67\u0006F^G;5𭖢f󼟦\u001e-g𢑎eg􋞧J(0<&\\𧧏z\u0008󲥄\u001e?Y(\u000f𬁸ORXUf=\u001fxW5=𬑖3󽀚N\u0002!YK󴣄,C󼔟#p󷙪욫r𫵨,<𥰏0󽄛󳖽E𠩝\rh󾤨\u000f(\u001214?pJMN\u0014\u000ed\u0011ᶫ\u0019󵥔📫\u0008𗇑/S𠯱꾲Pd \u0012fj\u001aLm'\u0013i7𢁝)2B󰤐鯿r7tLJyhj􁗄𭈏\u0011𫧎𣁤Z(\u0012􋑺\u000fL|t-󹝙=%\u001f}\u001c苙\u0019\u001e\u0005\r𦠯3𦉜\u0019B耢a;!𣜠'M󴱿k)%0+󹔛oM\u00043K\u0001$\u0002-tR>$`\u001bd\u001f\u0018Z\u0002𢊎RHG󿲣􈡄T-;<{忙K\u000b=7.mP#󰖇}G74\u0017^󳤹N\u000fF*$b\u001d\u000b'J󳾚5j귭𝛔􋈁s[\u0015𩥷\u0005᎙l󳸠\u0001xN𡭻6\u0006\u0005v𮗓>9aR􅺏󲄴T\u001b\u0008-QZ\u0003\u000e󷶶.\u00149]􎲇E𢜷\u001cl롟\u000cCww9Cys\u000f%揈􀍞WY补R3\u0012*Y\u000ba\u0016\u001b\u001d\u0015\r\u000e@ U\u001bG&,>]󾴩\u001e𑒞\u000c*j{𛊶\u0001G􀗐\u0007[m脦𖹅-󷬪󱌨)3qH]璝2 C5Ih$D􊥵[󵀩^@FfꟄ},{󴧁OemlSN􌽠\u000e|wF􂨥\u000eY\u0012𒃼󿑜Kt]\\\u0003BQ󰕬\u001c寁󽳝Db횕󻏾\u000c􅡮钼x󴞗󹰶!uj", - "email": "y\u000e𝟲@ e", + "email": "some@example", "name": "!\u0002OF󽠋􃥰􌞻󺝢𧯜𗲲+|\u001e\u0012NYwPo\\%𬓐vWF뷁\u0014\u001ck􂥴↝\u001eCQv􁣓󵹓b𧪂𥖧(𡽻_뮊\r􇫏􋵃r\u0005􀪔󳦌󺓃A,y%|⤕\u001fR\"󶉙w*􄥡B8󴛄k!{䶩M|e", "password": "~8㠃􃏴ڌ> \u0016q-gd\u001frD\u0003b\u0016Ux󷌬𣦉⊹msB\u001cqe\u000b*,\u0008􍣺\u000bs\u000c\u0004\u0010dh`j\u0011$V^\u0001\u000f𬈮\u001c\u000b\u000e󷨬􂫻\\\"\u0002󱄉hf\u0018s\u0005{\u001c e\u00188K􊳈L𤤵f\u0016sP󵊅oin􏣣\rG\"tXU7S𝦙\u00007C`x􇬼𥝇z\n\u0008\u00017󷌰\\\\\n𥫕\u001aG\u001a\u0015)d%󲥉\u0019 ]7坏H5t^+Sg \\\u0010𗽧\tbI3𩩬E5􈓁夆\\@vp#Y\u0005BA􅤑y9z\u00007🁯\u001ais^&\u001bh\u0017\u0018\u001etlM\n\u0008rT@W;qs󻚝n\u0004f\u0016#ᷠ󴃄))M\u0008`𪰦𥩭K􂢍(ftW\u0013􀕹r\u001b_󾶦􀿮*$2,\u0016(U,\u0019\u0018BFp\u000bXR\u0016\u0012\u0000=3)\u0013j9􂰗s􉵢-k𥔋콮UG󴝠N}\u0004􀜔", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_4.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_4.json index feb6897562c..0f97f5b591b 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_4.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_4.json @@ -1,6 +1,6 @@ { "description": "􁴳󸠝󰛍e\u0018\u0018'Y8}sJO(qR𑋪[\u001d\u0013W󼮜󾏗𢋴\u000eX$},u곀Kv𨣦<\u0012󵚷\u000c􀨌H&UJ𗯜$UJ}\u001b\u0007b!j🗲T󻙜\u0001eWj\u001fii\u000bLc 🦖􄣉7\\𪶹(%\u0001🐉T\u001a󰼳KC𫴝I𤒂#𥱴~ZS~+\t㎐H5\u0006u+𭘻Zꈸ󷰷Yd\"y=G󺪇!\u00048P$[\u00173𫠙𪽌͟|\u0006I􏔗O􌭅)\u0011\u0005뭋󱹱\u001f鱗G\u001eEG<%Q6􃊌T\u001ed\u000fFE)`\u0017􅞿O2\u0008󸄪𮁌I\u001bc?nw􈭹!󸷎T𫹨t𡫖拵\u0006𤙵p𓋞~lu\u0007\u001c礂}\u000eᔷaa􀇛T+\u00162𬭗XRZ\u000fb#\u0018ដXzW􍸢>󻰙􈂐\u000b䘁\u0000]󾼄\"%\u001e🪁ꬶ󸒧q\u0005􊴷𫓷𭨚]S%0 0T<6X\u001a\"i󴺃yBz𩎻\u001a\u001d󵶞󰢸$侂⸌*\\}\u0016\u0007SV𭚊\u0008\\4>GN_K􂽓\u001f󷷬Z~9N𐛋𢒩H#$𠥶&/di!𡑦,ZJ\n=O\ry/嚤\u0011𨦅7nN䆴l?𭫌xp𡿭q󸥻􏹡Ao\u0000N|G󿝕f󷊪󱶻𢌪z\r(;P銨鸢\u001eh?Yo#\u0017Udꯞ󹒴W􉉶a󾈼e^􌰏T𢒛􄛍x𠱘)M\u0008(꼇xJ𐌿􆋷}k􉭙𬧲x􄻘E󴆑3Q 捼\u0000^gd蜡\u0004}D𝒚j󶴦𨭢𩳭\u000c헐o|웋:RW.󸅀\u0000t𪚊󹩃I\u001by2mm災좝]\\R\u001e$\nV\t0&󸜿\u0014􌄎[X^𠳼$2􊨵s\u0015\u0004𝌘qQ遒􀞊𥷾\u0008.龣N􋎒.pSw\u0008WI\\\u0010\u0007%r\u0011\u000c󵳰?J\u000cJD\rLS@{吽6,6𑈗󸐹\u001f\u001d􍇪g󵧺輺b;𗚇U\u0002d\u0003􅢓\u000b<&E)ᄅ`ﲄk𞡯𪃋\u001bu#\u001e\u001b뷥8􊹻*@VJGm󿥮\u0019󳫁\u0014k\u0015I \u001f#_!􂅕􎃜\rc4@𝙽5 W\u001cGFS謂\r\tnGb\u0011󶙵繤\u000f>gCr?*#k8P\t\t\u0002􂵭\u0015w4u𐼺&`F\u001beNk< 0󾙳󲛄P.𡟑jᢨ|{􌑆k􍭯󼸱Ήn\u0008X􇍞\rB􆌫y\r𠒮U􁇕ꮛ@𞢕ki\u0017@𑁄\u001co꽇ZmB𡙲\u00160𒇇𩥌8󲍪8扫󠀮ᘲ\u0019媬\u0015\u0018\u000b\u0007|.nCX󹘍\u000e|4?\u0012t5h𖹨S\u001eu,&m3WL*푧\u001f3抪\u001d\u0007m/$+\u0005VM8$}󱐼\r\u0000􌑀𫖙\\}g󾸡􈷃G􅂼U󲟂P󲦸𭴩\r7\"\u0003讕7\u0002􇑻\u0016L\u000f\u001clf4;󲭋#\t𥷆\u00002:g@'K􊬷𝖢ſtꯣv\u00039𫢏D*Cjlr誵\u001d)𤪲{An3\u00178􀋱eu󻅍B0eO\u0012𣳀\"\u0014𮍚Q\u000cf󱜦󶆶b𫯫\u0019\u0010\u0003Xglf\u0004󶀟D%/dd\u0014𡲚l\\\u0015􋄞Q𧩤:𥌵x!3탵󻹴9㸩0?\u0008􋌕<@卾RQ\u0008\u001b∱W펻x!h𭟚󴊾6\u001a=~𮣝𩫽x𭳠\u0001\u001b𬟚\u0001𛱶碠墱\u001fA𭫃\n\to𢵁\u0012\u0007eX󳴟\t󵂮F~E⢊q8m햜\u0004󴤛(^𗏸H􀹦\rMlD>􉄣󹡈􂴼rC􋒤pfoc𝁫J\u001d𢗻woa-󼛩A/\u0003󻯊𬡯🨕R󵒦oGyj􁐥\u000b\u001f𡛼p\u001b?vw5]󲼕r𬧧􈊁#􌄽)DQ\u0014\u0015Uh`:){x𤓓󽓛/MHDZ􏧂O빆1;tR𮞃􊌧􃆚M􀪉\u0006QC󲕴􈞈", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_5.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_5.json index 7e72d476b51..f791e3d4e02 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_5.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_5.json @@ -1,6 +1,6 @@ { "description": "􏖙^\u0016A%BD󲩓ev}Ia󼏴p쵶+PI~%l􌧁6ZO󶩒EZ𣙰u𪘢X\t\u0014\u0013;걵􍊤c󿆯Nh`ᣚ{13\u000cjQ\u0019M']󿙡e𪎡^􃣿41𘋣𪓨1;\u0006\u001b;鲷\u0016I0*`t\u0008\u0006󶘖Art\u000c𥬶젽𤢖|g􍞅dd\u0001P􏦪\t𧸂x*\u001f\u0012=;𥢓N𧔙􀎢\u001bzH\u0013\\\u0006𓁝𧠚=M𥿭@|鑗\u001fi=\u001c\u000f􉝹􌾯", - "email": "􉐱@", + "email": "some@example", "name": "𩓢\u00124)􄒃\u0005v7\r\u0003p\u0010eI𬾐4SD^𐴃;9*", "password": "𡢗\u0001\u0003'\u0019Qz\u0001%􋴵𦳐p;뭙e\u0006iF􂁴\u001b\u0001gL#󽅕\u0017􎎁\u0006\u0006+\u000f󽔀T⠲r𬑥\u0014_0\u001d0󿷠])'􇢘\u0005'𥡆^n\u0016c;\u001e􋁖KwzB\u001e\u0008𘪃qdQ礈󺮏\u0007-郚h𬈮>~Hi,Ԃ􋍜𡌺R4'a𥣦\u001e}p􁓳󲣟𪻔P?\u0005naw\u001e\u001f󹏤@zr\u0010)𩳎&㈲\u0004\u0017!|T\u0000 '\u001f)cM\u0018\u00054`𨟢F殯=)U6.\u001b\u001c7\n__~􂖫Ruo􃽆󲞺", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_6.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_6.json index b27ff1aca4c..d0c196454fd 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_6.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_6.json @@ -1,6 +1,6 @@ { "description": "5ZK~v{􅋗EG\u0000>\\䧙\u001bb\u000cR󴥂E\u0014󸚡\u0017B6](WU󵈍t\u00003u%%\u001fy<<𬛎9\u001a\u0005RcFvשּׂ;nB?w􁜰\u000e󵹼􋕖|􂐢|l]\u001c󼔎\u0002<󽳽hBlj\u0019K\u000c\u0000*z𩼴葼󻸟󰉝*U󷧶~\u0011-󻒀ࣻ@𧪪􈙪q>𭡋hw\u001f󴃞 P􅥅G󺎠HS󺿮\u001fp󸙲􎄚w碧0􇨨󽜾ya-J3뢰𩮳\u0004J)\u001dki𘤱\u0000D|󲁥s⇴Ue󷥒u𗾇w\u000e\u0018|\u000b.𫀕􈧤\u0013𦱔雾𬰡r\u0013gH𡹡B)\u0000ᶙ,\"㝔n\u0013󼔾}䘡𫰴-V\u001el\u0000r\t\u0016􆓕^O*X\\6t󰅿.󶵥F􌀸b\u0015𨧭z\u0004r􄰼bG.l\u0016𣳂J󼗣𥴩 /4qOZP𡻔Q\u0019\u001d𧩸\u0001\\\u0004j#ZpBZ#󹗷\n\u0001#_XO\u00176?gg󰙲\tX􎭄g\u0010􀕤b󺃨G\u00178\u001b󺴛V u\u0012+h\u0003R􈺈d鳺󰔸Q\u001f\u0000v\t\u0004tj\u001fU\u000c+z𞲎\u001d󾱾F\\@􍘬=𐅪n\u0005\u0019Vr󻓲TW觸\u0012i󽀔\u0008{0XUg🚎\u0003t𓌚yz🁨󱄃\u0007OkJ珼􏠳1\u000bx\u0002~𦑇fQ\u001d􂘼\u0006H;o󷚉[{􁸕S\u0012KㆄA\t~ho9", - "email": "􏏤𤘹􂤒𡠆\u0015@\u0003􂟣\u0005-H", + "email": "some@example", "name": "\nLY-󾏄􁐨`\r\u0002贯`\rBa\u0012jv*m\n\u0012\u001app\u0010\u0004+pq=P󵪀𭌣c~ꜶQp5靅5􀭒X 󲣫*%􂕚&QNA\u000e6䏥e\"K供B\u0003=􋗧𫵘2a􂿜󽁃󿶰\u0006𦾩.\u0001\u000cP慌f\u0017𠭴2h􏝃t𑿡􎴵i󰚯x]cx\nC퉸\u00179;£${󴫱v􂄚#SNKZ@𗗠-\u0013E\u0006{\u001d", "url": "https://example.com" } diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_7.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_7.json index 584b3d68ff4..fa1fd1b7659 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_7.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_7.json @@ -1,6 +1,6 @@ { "description": "ia𩋕\u001a$\u0016忡Km\u0008Fc󱱜\u0010]m\\'\u0017d\n􆏥mm-𪊋L𨧭슗𘓊}\u0015󳨚\ne\n\u0000Wy뛤𩏯\u0016\u001a=P=寣\u0006sbD\u0019fY󵿟p\"R𘔨g󷣉H􋢟𔐥L\n􂡺>\u001bl>Q\u0014BlW𨥴\u0007\u000b\u0007T+'?rI)\u0015󸆜H\u001c\u001ad􂦿\u0001󻐓.A=>\u0007{@GBf𖣧$G2\u0017􉌴:󿳻󱋽v\r\u001eL\u000b^ujRN+b#ꚧ$CulYu7\u000f\u000f꣮R\u0014?󵪍XiDD\u0018\u0019᭼X\u0013\u001bi𥨗r3󹇌\\mZ􊢡󹔒􎶀|eJ;9쁛\u001cb\u0010􅣄\u0018p􄠰7𤏍[j\u0008\u0015)󺞃?X]c𧹺􆚿\u001d膌a\u0006ⶶ .yu󹱈󽦮𣩶􉀛Ê𩳟\u0014M𧱅?Q*pzVXTk/󻠈*\u0000D놂\u0001🎕⣖aD!6]\u000b5x蜬􇜆>n|#\u001e󳅃􍓗\u000eA\u0005㏞\t%4󺏡Ƶ\u001b+-_\u0016\u0016󶒇􎄺\u0005dH𢐴􊟲-:\u000fQx\\󵀈𤟐a\u001c󴀡𠡚eJ􌖀,󶵐\u0019|ட4檊󽹵k;\u0008󽩇6\u0017s~\u000e𐱆\u000ezJ𝀝Dm/ra\u0011S0 \u0011剐\u0019jw\u0013{𩓫\u0008􌥘C\u0002`J7#\u0001-\\]$\u0019𥯏`Z=|􁅴\u001f𨭂\u0007\u0012󵮍[\u0019kX?𐰺%0HOW@Ew𪜚'4𩊡[B𗭘󸌁^m􌸌󻧚tE􅄯Q\u0006𦐘\u0002Z\u0016󱾗M\u0015_R\u0017N\u0016\u00076\u0010:䬚S\\\u000b􈃻-Y7\u000c\n6􅃴hc󷊜&N􂔜􌭔E􏚠\u000bCqHrWV𧢏􃀳𠃋srO􀰺 󺥵/\u0002~𡜞蜞C愫𣝑A6WvF|󳢢MK\u000beS.+)4|\u000c󱉚m<_{\u0005\u000bW]􂠐m\u0003(&𖥺e㊱󱖠\"朇kC裚i󰱿\n/&,7󴭻v\u0019-s\u000e𫯷䁷󽴦몭󲟘_\u0017,_Rk,=Fx_𦯚\u000b􄚈#yℤ\u001e邤\u0000V\r󹑘\u0010\u0019WD󠀻BV󼛝6􆝋ࣘq\u0006圩𧪎欜󵽼\u0008􂕋>'\u0010󴤲", - "email": "e\u0011₋c[i@2CTG\u001f\r", + "email": "some@example", "name": "^􃊤\u0005MSO􃋏0\u0002", "password": "8\\%\u0002넖s\u0019F\u0019v5BC𩓷NL\u0004埚\nTASqz)\u0004kQ􄉽UD\u00060\u000e󴡀􀀫筸󻓽\u0016󾏸🔁\u0001􆖞qM_,𩭇+pa8\u0001J\u00002r\u000f#􎸯-\u0019\u0001^\t剽XuS𦯄\u0007\u001b\u001a_󱊎XPC𐝥\u0017&C73󼪬\u0016|/M2F\u000fS\u0008煱I3{/\u001c>\u0012H5rv5\"?!0𣢃^SC\u0008S5𓈊XP}0VT𩩍\u001cDꑺ`\u0006󸰟f􎣉𠂸Qt`P9~𪢶\u0006\u0017𑿪󼶵K=\u0015RC뇉􏙨􀉄\u001d\rY􏁑e&\u0013d\t率󴃶󻌿W\u0010\t!\u000bex@]xh\u0019o%𬜁􈍇􇑾k夁7/y:􋐦󷑊R𘘯A3㳣􁚔*tnwXQk𡋻􅭓􅢠𨰧𛆋\u001a𤝡\u000e\u001eㄾ\u000cꍭ'!%p\u0007􏠢x\u0001􂎬鵈SP󶈸T𦰓j\u0019$5vA;2Y3\u0010􇙿𐄔`{\u0019S6\u0017\u0016𡾾?󻾘m\u001c螮I𬱃E0䯖<.Gu\u0006\u0006\u0012\" -C_W䛵j\u0013\u0015\u0000C\u0015s欙\n\u001c&G -e㏣\u000bM+pB|𬤠d\r\\,2M󵰱𪧕qA泾\u0008.􀝔gP{5y0󶍁\u0001􃶭d|\\EUG비s𣏃\u0002F\u0016`󽭲OD\n';􆂬$$D􃓶: r\u00114𘣾\u001b\u000b\u0012j+\u0002P\u0016cZF,u􀰳𪽄\u0019\u001b|yAE'蠱\u001301I\\J\u0010\t\u0000\u0006^mn𪑷7/\u000fs撗\u000e4c!u󻹎P(\r-\t\u000f\"賵\u000b2Y;\"h􄿮`?;\u0004\u0003&tyLb(P􊻗2EC\u0006󰂁O+Gt𡝨\u001d􀽟;wwl8\u001czW󶩉\u001e󹶱c𭱤kSB%\u0004nC-\u0019%\u001c󷱍.\u001dy􆎑􄞈􂨨\u0004󿊴𢼠𝅜􋂢i󸖨&7𩊛\u0011󶹾\u0016󹌁.W𮕋9셖a(d􇭏,𢓰xOX􍓃􅵉󸖹𧮤)H󲈸7K𘋙@\u001fM䰮c휭K6xk\u0016\u0015𡋠CH,$⅜y\u001cK􇐰iZ􍚞\u0007\u0002\u0001k\u001ax𐌃􆒚h\u000f\u0007YH􋹘\"舙􁤁$u沞wk\u000f\u00113\u0013\u0004󻔅􋜵>\u0017􏞿D􅭨kHAt%\u0001-AN减\u00070|\nC\u001bLdigb\u0016O\u00154𦛉>\u0015\u0011哀𬑶쿉KE􊝤%𭄽huLv;𮘈𥐘栲ll(}]8#􌽗䂹0(󽳉#𨓕cd`-\u0005𓊑P󼲎[ 􊆺[$脼^􇍑,uKr𦬛!M웥#b𣯢]i𢹼\u0003u\u001d\u0013\u0018bU.pgb􂵂[N\u000f,q\u000bed\te\u001c`<7X'ot󰈺\u000b𦲑KK𥛾g𭄄w󺳵𣝻['!\u001fx/.\u0012ഠJV󿒇uC=C𤎛rا\"𥠛\u001djR*", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_8.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_8.json index a4194a2c1f8..032da5deefe 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_8.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_8.json @@ -1,6 +1,6 @@ { "description": "-\u0008k8Y|'줦F~?󵚡c􃘫@⽎\u0004=ੁ⽼j𠼎>s\u000fE\u001eZ􇐂VI󰖍xZ gz󠇧\u0019{둪Z;𗬰𤢥󿅍nX%h_,G𢱆逪+𢝯pgd󺶝𦹨V:/\u000cq~j(+Oa􌩬\u0011f\u000c\u001c𫒿/\r\u001e=v;􁨽9=7𢀃󳎲\u000cY-𠕓􇶩Z%􄞺𨖳PO꓆􀎭􌋉<𤪶\u000c`\u0017z䵪~𣇊𨌷\u0000\u001b\u000c󰵳K􏻢𦏸h\u0002􌭸\u000ft\u000e\u0019\u0013\u0012]TM\u001a#j俏_fzNi;󻺕e3🦔_b\n󽫏\\hn6Y󿐐+g\u0004\u0019FJ\u0014?󼦜𤧄ꤋ4l𑀬\u001fZ󴴜'Y􏪙\u00148𨾶󳎦=PZQ@􎒊\n󳭿옐󱩅𭁦y󺢏d\u0010e􀻢P𪕻L󴦃:􉮤I뷬<\u001a󿉽\u000c􇛟\u0013|E󽋗\u0002I􄞐3\n콴\u0000􉏵7\tn\u0014\nF󰷶@\u0012\u001d\u000eiB\u0010\u0011W6)𨳔8;\u000cIl\u0004)\u0013𫬃k􈟬_\u00047<=Wx향\"YG#󱫱#|=P􎯬\u000c\u0004\u0017\u0008ok𪜏V􄸡\u00042B]𥣍\u00010e\u0015𧨦鞅;@𫫾R\u001e,NUR􂈁7󾷬\u0010\u00013s,'󼼖𦑔􀧜\u0007\u000e󷜤􌄾58^󰥊\u0014da𨭘\u000e7\u001b(R𐳰x𫃬{㓍𛃬?l#\u0005q\u001dxB􊐗(%j󸳩𗥐\u000c^>q􄎄7鱸􅶓XL\u0004.􎛭\u00074𫮜􌁦\u0010cpr?釞4􃋷􌩤𣮮\u000bS𧁭gZ\u0000\u0001\u0000h(u􇕜`eRZT=Z\u000e𤤘\u0016Ha\u0003􈏘𬃁\u0014x𩑍abpmUb\u0001\u001e𪿍󵛧􇧪RA􍺋󽧷l),𠑩󿍟~0\u0005Na鬟󸪯𦠒護r?}􃡨\u0001\"㚎_9Ge󱌿\u0019BEԥͼ􀁵贻xmP𡱑pg\u001e\u001f\u001a\u000b𪌕虉J𩿱H\u0002ꊖ𐬤\"V傱\u000b`( JAC󶴧\u001f剞m[슅\u0014\u001be\u0004󺰡𑀩/\u0018SX:s>k⏓q-\u0006C $[M픵T^46\u0008󳍄{q􈈢x&G𫥅𘪹𞢎\u0011􎤶aM􁣊N􋿟\u0013\u0018󳆉G%󸴺^7\u0004󷠌(c\\\u0017󺯤𧞏gYb\u0015z+\u000f`\u001a\u0002hT?@旲􎥿\u0000!zX\u0017P􅑉my漯\u0000봹6𧝙Y𠵫\u001cE閤+:󿻏远%\u0010𭾀p\u001dO􌇼\u0010\u001d\u0018\u001d9\u0015E𣵋\u0010i𨑊T~i󻬠V\u0017𣆒H\n\u0010\u0015􃸦\u0015;􎁜\u0005𤋢𤠁\u000c\u001bv\u001eMq59{\u0017🏟2J\u0013.\u0013P^", - "email": "\u001a#\u0019dlb@\u0017󰦍\u0015^", + "email": "some@example", "name": "㩆\u001eh􅩏;Io2:\u0015^e3\u0015𥓍Vq=\u0019􊓵\u0003􆬸􏝇\u0011h5Zqc􈙩vz\u0008V\u0010\u0016Zh-\u000bZ4󶑯5/KI󸕫㦷\u0015\u00139徠:q5Oe􈮣9\u000b:xx'U􎎲\u001at|\u0011O-}\u0001\u0014Zz󹝟z[,F{􆗄\u000bਁ\u001a4\u00100-\r\u0008\u001fw\u000f\u0013a􆔜mv\u0011;鯝W&e|󶜇􇽰;o🔼𢑞\u0001ds眮\\󾽊\u000f.픴J.1", "password": "𬖩2𭽏qfb\u0002訅􋏕𫪬\u0002aC\u001b𒃙𬘐𐅥{\u0018𮘻w􏑆D􁠐q\u0011!N.]bQ\u0003*\u0017\u001f\u0007󿥧q𦥲w𥏽)p\u0006󳊩􆿴𬑒){N5\u000f'Eb㭴\u0016x\u0000􆆶󱘉YB7􋋤co\u0004,[󶶮kDB􇴒qU\tr좺s󽂘Jv*羇)TM1󵨘=䍩\u001f=g\u0002𨂂\u0014\u0004-5\u000cr@f뼘󸬋80\u001f𨳜뻍ns8WB\u000c􊹱#C\t겴_𗝪Fl𥇔\u0008K~\u0006󴍹\u001c𧑒9\u0016.\u001a4R\u001c#𨖵䄈2\u0000\u0006伸\u0004\r\u000f􈶘\\\u000c),퐣kJ𬆸􂗀2B27\to?Gލ+r􉓽o\u0013\u001d(\r\u0006J:`\tZ\u0000Kp\u001exK~,E􂖇\u001338󴖿\u0018󷉶𪳂ynza\u0010g􍳰fB\u000eX\n􁂊􏼫H􆯇󲆀b+󻷈IC$\u0008󶃰𘒍}jc󸥉B\u0005+𭡢\u001f箒\u001a\u001a\u001a\u0003|\u0015󾚃Q􈂢\u001a1󳭩C\u001dA󿾞X_𣜐\u0003T;󷯫\u0008(𣉜\u0001🏝𝛩9no 󷖾v媜b(\u001b48\u0007󺋦wEL𡾏􉸞\u001c6ePOm{\u0011𨆫,&zj\u00069f\u001cd%z􍿟󽖱6\u0010󽮑i,\u0010\u001dy,Tꆜ>z\u0006【\u0004{yUcu剕\u0016𠲦gB녯\u0007t\u0000Ka>𨒕󱵌 𬡽􌸿~\t󸭕􇶎yzRZW󶉐\u0001T󾑫,\u0004Y~F𮚉\u0011\u0002Z󰯾\\8ei$FViM[e2F\u000f􆶕RcZ󾺗\u0015N甔Kr\u0008o\u0008\u000e;\u0002s=𤵑v𮆬L&r꿑3\\𩚠6\u000f뾕惾\n􀏍𢷟𢱉4􄱺YX࠻\u001b󼩼廓𥃥cFE\u000c3􍚕QiPb\t𦅎\u001a\u00079&\u000c󹼳\u0005\u0000\u0002\u000f,M\u0014\u0013~tx0u𐹠c6\n􊮫􂪃WA-⣇S\u0011?󱞀𦲯^𠘬u\u0011|#0I𬔵􏩵\u001da𪤡𑈱r\t%􈛃RkwJ:􈓷^𧵔􅐞iI􏫩f\rx蒎eN𫆕uE𧅶𫉥鎁];毜\u0018\u001c𦃕蔌\rT\u0008𧮱𡚻\u001aS|V􃜉&􂦦\u0010:\rYxL[ረF,c쫇\u0014n>􅘏\u0004;/+􆡞\u001a𧞆\u0015t𤊇또#<\u000eV瑺\u0014$󾉁5󴍹􅦌𨴱eN\u0011\u001e\u0002􍔇\u0015u3~\\𨰛𖣘𪁿(󼐶[𥒈\u000fC<𨁹|Um􋩩e]J\\󼫊]\u0000Ac􎚍#銭󾲗󳒷o\u0004S󼜯餻9\u0007=T\n[WkRO\u0016/􁩥u$`(󶢦\u001bw\u0012\u0006;􉼱B􊶩I[gi󲙙", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_NewProvider_provider_9.json b/libs/wire-api/test/golden/testObject_NewProvider_provider_9.json index 1616f392f48..72ef2a3ddd3 100644 --- a/libs/wire-api/test/golden/testObject_NewProvider_provider_9.json +++ b/libs/wire-api/test/golden/testObject_NewProvider_provider_9.json @@ -1,6 +1,6 @@ { "description": "i󷕳gG\u001a[ᰩኬ𐓀WṶ\u0000/$󶖑PU+ā\t;/0􍬟夞U0\u0014ྐྵM2Q}$W􎛘-𨌕+𬅈\u0007M󷎌5\u0015鼉_𥒺󼢀%\u000f\u0011t\u0007𣩪R󺁃4o?V-:H瑢!P\u001d\u0017N,􀺧3\\㾪𣝘\u0001GqKb􋛽􃙕i=t|K󿧏G 9􌥢X󶁯o쵐􉍞)mR\u0013f\u0018\u0002\u001aNU\u0019Af󱲘Q9t𡣖𠹄\u000b􈊞@󶊶􌄲M\u00198S`[e𗘓m6F\u0016G}󹣮K>ᐉ󸯳^ec9\u000fYG\"6mcB4𭖨𢥕ᢷh$\u00055+|&L󸼼\u001aƆAv󼗧\u000bxS9􉂣c`D\"\"b𬫱\u0016ᢠ󽋤\u0013z𤇇Q`Dr􈉴R𗫛h^\u0015>\u0004\u001cM>J;󻒷=n𠥊z|E\u0015􏸦Vl􂓝P>󹳲V𑢸jz\u0003𦭨𪯶qP-h[冴R9󺝞*\u0012\u001a𦢶+\u0016$@𩋊<𫖛K_􉇩T𦀦?_〖Iz𣭛󴧪ꢞp8^>\u0006C욉ၷ?\r$𧽵f􄧧#\u0014I毪'\u0013\u001a𦍈l\u001f5󾉵\u00116o\u001flk󸿖𩅖𧈝y􎖰\u001b\n\u00025\u0011󱎓Ohq/\u001b􅳱S󵉭ewtk􆭍􊿡H眀tnSE5\u0019\u001f@a𫼽d𩿱\u0003@\u0005𖡙\u0010G`󳴃8𘟚𠳨􏪱𥃚?jN𠆝w)4,J\u0013𐧊(\u001c|*\u0017ZNdJ笱fcx\u0012rjH$㳩􎱈R󻿢󶒔\u0008A#=􏡶𦸵T󾰗ty!\u0000l樶H唆OK\u0017[R)𝐫1`𫱘\u001d\n\u001b婍rAnl𡗀3n󷇺\r+𪝠5􆅪%R5tIG/,E􀷔_C\u0012\u0003󱛹r룮\u0008𗺰\u0014𘕁Xy-󾲄h|𥳪\u0004􎖂s\u0016v4\u0003.Q\u000cb,􋛼\u0012C\u0014'讣JFS!I*p檴&\\E\u000eS8ꍇ9𒐤4t𨢫㢵󷭷9v\u001fV\u000f􅩠\u0019\u0018\t앒p\u0017V\u0008\u0006bY𨀔o\u0008𞄈j)Y\n􀈰LE[{\u0016'E: 𮡑󹯃D𬤌~𩒨\u001b\u0011p󺉡J\u0016𮀖[士􇐠\u0006Mg󱞉\u0001xK󻄊C\rK\u001f\u0016-鼅<􃌗P\u0003E<[4􄧔KIXm\u000eV\u0012]Q*r\u0016*\u0001􇉊\u0015\u0000~gA𗢺XB{\u0005N2􊝛뛊p\u0017\u001fn.}OB1\tU\u0019JHB􂽻\u000f𩂩DP\u0012󺭖$󳇴\u0016}윧\u001d𡬨e72𦸦)&ol\u00189I\u0007DOW\u0005i선4\u001ezbB\u0014统L\u0005X􌅶󾊋\u0013d󽭄2󷨞w8􀏙􍫮I\u00002𮥝\u001e𨨸\u0013x뇝(yLL󳢿\u001diFSE8\u0014#\u0001T󸸶\u0013\u001c𪯻\u0015􁬰kf𦖣ߊ\u0017\u0006mh效'Lqo,\u001d(󱾧\u00031@\u0015b\u0007\u001a:\u0018/\u000bA\t𪮋鼻gv􄽅𦳈i󵹩\u000e暑첱V4BEy󵲊\u0016\u0016&󵡱混ak󺄡𧬥$􇐻BV\u001d\u001bnV?$/F䰖閽7.?/K\u0006X\u0006IZ/\u001e𡔶𧪡𥡞x^𪼁;\u000f\u001e𬐳\u0018rwFL\u001b5T\u0010v𪴞\u001fG𨸇x\t ;\u0002h@󵸈~􎭇\u0000;􊗃*Mu\u0017Iy`FI`X汓6#,Hb\u0015~@𭐌\u0017m>눈`H)E𩏯p@~f󲭡\\\u0007\u0016H⣦𦴈\u000f󽥰oJ7󵃅)$\t8pxM􇷒\u0003\u000eﭐ]\u000cn􈴊-?^\u0018Mda/􄏸Ei(􏷀=󹜾𦑧&62.\u0000", - "email": "8-\u0016\\@\u0004옐~#", + "email": "some@example", "name": "h{kᮤ𧥕𘒭.t󶿥\u0011K\u0016>O\u000f\u0001뿐pJ\u0001\nh(􂜊E燙9䜃\u0015\u001cܛ\u000bD󳄵\u000eg6F􁙉qUD톢󻋲hn]f툏$kZg󰗯\t~;󸙈󵶞k󱟔5𧚍r\u001e\u0006[\u001e󸧪𬶝靟\u000733𤀰6􅉩$#{\u001d\u0010\u0004X\u001c=N\u0019𫾳6<0%\u0012cdm𪦑b㟼\u0005=9F2:𬡑~\u0019\u0017묬k!Q𥙾􏊀[\u0000eld", "url": "https://example.com" } diff --git a/libs/wire-api/test/golden/testObject_NewUserPublic_user_1.json b/libs/wire-api/test/golden/testObject_NewUserPublic_user_1.json index a22cdbd6852..8d5f51fcbba 100644 --- a/libs/wire-api/test/golden/testObject_NewUserPublic_user_1.json +++ b/libs/wire-api/test/golden/testObject_NewUserPublic_user_1.json @@ -16,7 +16,7 @@ "type": "image" } ], - "email": "test@example.com", + "email": "some@example", "email_code": "cfTQLlhl6H6sYloQXsghILggxWoGhM2WGbxjzm0=", "label": ">>Mp१𤘇9:󺰽􋼒\u0010D1j󾮢􂊠;􄆇󳸪f#]", "locale": "so", diff --git a/libs/wire-api/test/golden/testObject_NewUser_user_1.json b/libs/wire-api/test/golden/testObject_NewUser_user_1.json index 211528ba5f3..54619703ce6 100644 --- a/libs/wire-api/test/golden/testObject_NewUser_user_1.json +++ b/libs/wire-api/test/golden/testObject_NewUser_user_1.json @@ -16,7 +16,7 @@ "type": "image" } ], - "email": "S\u0005X􆷳$\u0002\"􏇫e󷾤惿󻼜L\u0017@P.b", + "email": "some@example", "email_code": "1YgaHo0=", "invitation_code": "DhBvokHtVbWSKbWi0_IATMGH3P8DLEOw5YIcYg==", "label": "𭤐15XwT󲆬: \u0011Z+\ty𗌉\u0001", diff --git a/libs/wire-api/test/golden/testObject_NewUser_user_7.json b/libs/wire-api/test/golden/testObject_NewUser_user_7.json index 4e1c9e40a64..6eca6b1916a 100644 --- a/libs/wire-api/test/golden/testObject_NewUser_user_7.json +++ b/libs/wire-api/test/golden/testObject_NewUser_user_7.json @@ -1,6 +1,6 @@ { "assets": [], - "email": "12345678@example.com", + "email": "some@example", "name": "test name", "password": "12345678", "team": { diff --git a/libs/wire-api/test/golden/testObject_NewUser_user_8.json b/libs/wire-api/test/golden/testObject_NewUser_user_8.json index ca19edb493a..017389cbaf6 100644 --- a/libs/wire-api/test/golden/testObject_NewUser_user_8.json +++ b/libs/wire-api/test/golden/testObject_NewUser_user_8.json @@ -1,6 +1,6 @@ { "assets": [], - "email": "S\u0005X􆷳$\u0002\"􏇫e󷾤惿󻼜L\u0017@P.b", + "email": "some@example", "name": "test name", "password": "12345678", "team_code": "RUne0vse27qsm5jxGmL0xQaeuEOqcqr65rU=" diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_1.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_1.json index 584b108feaf..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_1.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_1.json @@ -1,3 +1,3 @@ { - "email": "􉏬\r󷨎bW󾺓󱥥󷕕\r\u0003\u0001\u001bj𤯗@sC.\u0012PW" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_10.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_10.json index d003760ed45..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_10.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_10.json @@ -1,3 +1,3 @@ { - "email": "@$y0=|\u001d󾡌E􇩯!tN:" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_11.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_11.json index e66d0a5948f..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_11.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_11.json @@ -1,3 +1,3 @@ { - "email": "\u0010\u0001𗬳黄K!z|𠽽0▖,1,􈨅4\\钉Q@>Xi􇔬\u0001\u0011:󽌤𬀶𨥔\u001a[\u0018.+uOgWp" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_12.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_12.json index 6842effbc48..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_12.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_12.json @@ -1,3 +1,3 @@ { - "email": "􄵱𩆢🙖>@􄆤{]%\t\u0013n󱅶􆎎􃯵CD􊤽>󼓞a롿⿂𬩔n\u001b\"Xw$\u0007G" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_13.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_13.json index b28663d545d..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_13.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_13.json @@ -1,3 +1,3 @@ { - "email": "󲶌5\u0006𠓫!􉄃\nVb󺴝nU&󽋡u𩟰@+I𫅗q􃾘\u0016􅊹#A𧿃\u0010}.\u0001u󷴓" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_14.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_14.json index 47cf254a683..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_14.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_14.json @@ -1,3 +1,3 @@ { - "email": "v@􊌉" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_15.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_15.json index 1cea568c611..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_15.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_15.json @@ -1,3 +1,3 @@ { - "email": "+𤳡~􆅘VFc\u001e􍐴R\u0007\u001b4J_􉚂I\u000c󾵯Dj\u0011\u0004q@󹃹𡰨n􃙋Gh?\u000bPXOO\u000b􊱳\u0012" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_16.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_16.json index da903c0e028..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_16.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_16.json @@ -1,3 +1,3 @@ { - "email": "]􏖌Dn\u0008\u0015\n䔟𨲌\u0005󺃬2\r􅃁󴯹󽦀@%L(\u0019􎼖\u0002k\u0004o𩯑B䣟O*/+@볡" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_18.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_18.json index 54914a375df..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_18.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_18.json @@ -1,3 +1,3 @@ { - "email": "\u001c󹮂󷆕^3𐭏*(󽗶𘕇@󽓵Y\u001b|=𡧿E.A.\u0000󴭝K>􄠭cZZ~\u0018􂟺i\u0010.rꡇ󴪩 𫍒" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_19.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_19.json index d21c352e333..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_19.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_19.json @@ -1,3 +1,3 @@ { - "email": "x|􌸆J8󵹛|%𢷎'9􉺫𩿺􉀀F􌌯xyP􁟃 4,@!]w6:\u0001d4t(􍠌􁂡$\u0001rl9⛉𝝥t8" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_2.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_2.json index 4a7cac622ea..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_2.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_2.json @@ -1,3 +1,3 @@ { - "email": "mh\u0011􏧜\u0007􏝯#e󲴔m𞀌𮓹𧛢D]@\u001dJ-0𞅀DU~Ẕ􉺓\u0015F$" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_20.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_20.json index 7c18619be65..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_20.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_20.json @@ -1,3 +1,3 @@ { - "email": "魳2\u0016)=Xd𥸩}o@4\u001a𮂬􁙭g\u0000􊫓󰗸Q`\\\u000eU󸝠" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_3.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_3.json index 0904742ebb1..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_3.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_3.json @@ -1,3 +1,3 @@ { - "email": "-BP\u0018󵷒F䰯􌭱]W󽲘d𡹕􇈞\u0006\u001d𡳖Dy\u001bx\u0018𦯓uOU󱄌\u0018􆩶\u0006@󸷩e\u0011V-j" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_4.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_4.json index 44b7a3501a7..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_4.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_4.json @@ -1,3 +1,3 @@ { - "email": "\u0003!\u0014]$Zp@R\u0002\u0010Q" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_5.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_5.json index 7cca99e7318..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_5.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_5.json @@ -1,3 +1,3 @@ { - "email": "󶆁Mm𣢰\u001c9` 𣫱󲫷\u0010𤲜\u0003꾡]󿶏歲2\u000f\u0017뀙@B󱩠{\n􀎙O\u0004,P" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_6.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_6.json index f26d931ed4b..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_6.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_6.json @@ -1,3 +1,3 @@ { - "email": "ᒱ􂬛𥒗\u0003Lv 󽎁9@3J\"K'-Q𠂊P𗗖Q\rf􇇓6_kN􆉆\u0003$󳅍󹻌4𬐬k𠯣󹰶k" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_7.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_7.json index 00efb9b2268..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_7.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_7.json @@ -1,3 +1,3 @@ { - "email": "\u0001\u0003rra\u00014|]c&4%#Al\u0012*U\u0002𔐧m9\u0001󰧏UQꏘ󿤬@1G*𦂸f\u0018V󳒭㰒𗿫lR쥩" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_8.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_8.json index 67b2e07f780..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_8.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_8.json @@ -1,3 +1,3 @@ { - "email": "6􃨣C酵(|\u0000\u001e𠡓@襄\u0019饲" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_PasswordReset_provider_9.json b/libs/wire-api/test/golden/testObject_PasswordReset_provider_9.json index cc41c9ba8d6..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_PasswordReset_provider_9.json +++ b/libs/wire-api/test/golden/testObject_PasswordReset_provider_9.json @@ -1,3 +1,3 @@ { - "email": "ui0^p󸘴\u0003󲶬u<8\"YgWb\u0008x[\u001e},W\u000b󾮟耠\u0016@\u0008*󻥹0>*N`𠲧\u0013 t" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_1.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_1.json index 46df516a4a8..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_1.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_1.json @@ -1,3 +1,3 @@ { - "email": "䀕𣚼,kyz-\rᬙ󿰠[􌖨#mh>6@c\u0004;QAjc\u00042O\u000e%\u0005-󵄅\u001an$󶢴󰭵b" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_10.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_10.json index f37647d922e..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_10.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_10.json @@ -1,3 +1,3 @@ { - "email": "䂨㢃z-\n\"T𫉦󴡈𑊒f\u0016u`\u000f󰞐35𭛯𭫨/[3ᚫm@{~3J\u0005\u0005\u0010(\u0004Y\u000b:l" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_11.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_11.json index b0c9a2bb9e0..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_11.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_11.json @@ -1,3 +1,3 @@ { - "email": "h\u001a\u0006󼯑倀C\u000e\u0003!𗣦\"󵌵pWN𮬒E\u0011EGZ$T\u001a@󲚨^[\u0019" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_12.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_12.json index 58adaabfac7..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_12.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_12.json @@ -1,3 +1,3 @@ { - "email": "{TC\u001a\u0005?\u0007u􄶍E󻂧@ꉥyMb\u0019\u0006|-eH(\u001d\u0004|B~㳊\u001b󰰢j\u001b=\u0008cs" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_13.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_13.json index 8ebdc92778f..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_13.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_13.json @@ -1,3 +1,3 @@ { - "email": "iA􀍭󽂴Y\ni󴭬WCU𪪞I\u0001+:f@./" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_14.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_14.json index 4899a48978c..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_14.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_14.json @@ -1,3 +1,3 @@ { - "email": "z𨁰xGh\u000c\u0017󸻵D\u00034\u0015S@5-)鯡C\u001aO􄒙-\u001e\u0011}%\u0002󿠐𤍷𫍶𠟛\u001c𪖹Tﮍ㾏" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_15.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_15.json index ae1bddc4161..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_15.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_15.json @@ -1,3 +1,3 @@ { - "email": "𑂵켱ᐘ_(\u0015|􃓾@􍰗\u0007nt𪒫\u0000\u001cS𬪊󹅥-\u0003􌍴K}q􄸀O8" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_16.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_16.json index 727f4266a6b..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_16.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_16.json @@ -1,3 +1,3 @@ { - "email": "@" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_17.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_17.json index 21ed146bcaa..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_17.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_17.json @@ -1,3 +1,3 @@ { - "email": "0\u0015􄵿3@󲛼1\u000e𦂋Z穰𓍓|\u0012fA%:\u0011D􂵹" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_18.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_18.json index d6e1f452e1e..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_18.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_18.json @@ -1,3 +1,3 @@ { - "email": "x:鉩𢏘m\u0015󹔵gJ_\n{_.b\t\u0004<𘝙lB0\u001e촹(@7/k\u0001𬁟쌐\u0006u:󷝍b~\u0010^\u0001􈛪" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_19.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_19.json index 9363d0872cb..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_19.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_19.json @@ -1,3 +1,3 @@ { - "email": "󺁩\u0000𞸤}􃼣󸞢K8𭹾$驒@L" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_2.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_2.json index 8908a1ad5be..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_2.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_2.json @@ -1,3 +1,3 @@ { - "email": "􉽅\"K1\u0003;}\"n~X𠹸𧘖Fd\u001c1^fo}M􁌬q\u000b=𬲈xU@C\\" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_20.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_20.json index b079e812221..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_20.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_20.json @@ -1,3 +1,3 @@ { - "email": "2h/\u000f,\u001cl\u0012\u000f\u0000)@cu􋈜-\u0002iW\r" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_3.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_3.json index f18b3365a87..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_3.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_3.json @@ -1,3 +1,3 @@ { - "email": "J톩uv\u0004\u0016\u0000:nO𬄓YF\u001ao>H𩉘0&Q\u0000󷏯\u001cU􆳳犂\u000fᚄ􌍣Kf" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_4.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_4.json index 90eb35f710b..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_4.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_4.json @@ -1,3 +1,3 @@ { - "email": "b󺂈w\u0007f\u0017􀠥\u0015a􃔣\u00158@a" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_5.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_5.json index a444b3baa15..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_5.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_5.json @@ -1,3 +1,3 @@ { - "email": "C俵󰮩E\u0000'U󶖩@m􏑯\u001aGⱽ\r=P\u0015~􇜄%t㬸H󴥛􋤚Rn\u001a㓙亿󰛷[&" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_6.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_6.json index b3c4728a8b0..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_6.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_6.json @@ -1,3 +1,3 @@ { - "email": "{\u001b'໑DC\r󿣀|m4Z=|\u0001𥘡0\u0010\u001e􋍞󷼟OP\"𗼔𘀕@􈧎\u000b\\5" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_7.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_7.json index 2e474032e11..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_7.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_7.json @@ -1,3 +1,3 @@ { - "email": "bl𧧖u󼼂g,}𐢍63@7󷈃!I" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_8.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_8.json index 8039cca5390..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_8.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_8.json @@ -1,3 +1,3 @@ { - "email": "_𒀇:󱸧F$'Q3\\󳛐@MGx$\u0003w)8C+𫷨\u000e\u0000" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_9.json b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_9.json index 0f6bb5d78b7..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_9.json +++ b/libs/wire-api/test/golden/testObject_ProviderActivationResponse_provider_9.json @@ -1,3 +1,3 @@ { - "email": "~랹5QI3[$\u001c@Z\u0015\u001eওkv\u001c󹑣q𗸚\\𑐉􌃠" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_1.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_1.json index ac6d7124f22..ba470460278 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_1.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_1.json @@ -1,4 +1,4 @@ { - "email": "亴𬇛􃏗䷡Z\u0004𦙕􁤃𢟥@􍫠E􅰤Q|𤏶", + "email": "some@example", "password": "\u0011瓲􍀏𘡸\u001e>XJ#\rA[􃰿\u00156r𝈑A\u0013\u0013𐃝󵂧p/􈉠\twP􅫏\u001eCq[\u0014(󴳔P]OL𮫮\u0000\u000f?\u0018\u001c􄭍O?*𠤑\"~𗔚\u00155\u00123󲜈􃝋GC \u0007:T􉒾|$r󺃢Q*US𝍲ἥ󱸃􊲨󷎧2\u000fXp\u0011l,{퉇$􊣮\u0016w\u001e󷤲p𧀦$6󴸕䍤\u0016𫤽7􋔴]𢤵\u000fi􆂾%\n\u000e9i\u0011\u0017I#\u0003\u0006z\"LJ+\u000c\u0004U\u000c!nSGq󾓪􇠪\u0008\n\u0016􌩲􎓑vkoE\\>L􅪃󲚽2㥆V\u001ffCJJ󾙊\u000c𬮓\u0011𢟊鑐S=􎒊v󲴼/jdg󴶕U󺖈s\tO\u001bD\u0012\"􂭈;𧯦`C#\u0012􆊊^j^q𠪂;`󾺒󽗻\u0010󰴖0Q󽮕𮄡𡍖\u0018􌂩99\u000b𠜾􇰽\"󸁗􌷔𗲩JW\u0013n󿸖\u001a'\u0017!\u0007;\u0018F󶌘:\u000f\u0018DNhu\u0017vWC]i󹹹]>mM𗥨󱫋ISK+𗺅\u0001m\"h8硳\u0002\u00143F/A𢮍󴬮𘆬M/궮󾔠`\u0005􁑑囶k\u001c+\t󶱅\u0011,6\u0007𐳔D󿔘.:$'P儍󸯥􈚱𨰲w6V\"I~\u000c`☒N2\u0013\u000e\u0011S􏝽/흝ZfjtU\u001d󹩲u%\u0017[k\u0016󽤶s+G\u0019h6\u0017k󾚒\u0002\u0015\u0001i󺆮P󲲘\u0011󳺉O\u0007\r;;𑲢󱎴\u0011y\u0012\t`\u0003{n\u0018#fb𦇂z􉿹@𤑮-𠸩\u0010_[)' r\u000eh\u001b󵡾4\u000fi\t𪑿􃠀\u000fEe𛱁y\u0001\u001c旺U\u0010Y\u0017[Wu𤊬󾲐\u0007􎪗d潑\u0008􄞕\\&F2F=L\rlk\u0015􂸍x꽞\n􀝠󸓆s\t󲛓]T\\󸏠􍞎𫕭z=^\u00062)Y]E\u0000@\u000f\u000e~ 뮀k7􍐞{m@󹸼}D*i\t|󵸞\u000bF􅗉+\u001a(qIOhI#\u0018󶡀󶔄7E𣗆/\u000fM_z󻌬ॠ⸢ڛ\u0015\n\u001f\u0016^)\u001aLVo\u001e쑃𫡯w⽞UeQi\u0006〴𗸎&캐3S$$𫮶\u001e6􇂜􄩚*𠈵Kkk&Ie\u0000{\u0018@}𐌛L𖬮𥈣L-\r6U$3wC 󲨝󶣠yf3􉈰u𣗯}}􍑟9\u000eX􍙍t󺷬{\u0012m􅲡o\u001cEzaDAZ忖T\t1\u0010鏦𣢗\u0000𒔺\u0014D|t0'W\u000f,l\u0007YI\u000e󶽙𬤱l,\u0001󸤙k\u0007B􀃶\u0016n󲣋\u0007EjoDc🍝EhKM|𦶥R𐳽\t\u0005\u0011q\u000f\u0016C3󶸟\u0019Uy\u000bB𠞅a蔜!8\u001b\u001d\u0000w黈xEV\u0005y\u00136􎎄䠋_𬾙\u0017𗟜7f􎡲v0顐\u00052*󺑠bW 󼁠\u0014𢕳,䝺zh􌨐rO󺽑\u0014N(\t󾓵\u000e뺘,D|􄔎󳰪)\u001d;q8{\u001f3\u001c)􉾑Z\u0002\u0001\u001a,:htBg6Qz􂯋󴪀􄺍G\u001eꅾ枕iS𪀜8cdr\u001b;\u0012nf38瀊gl$9\ngdX\u0015􃚨󼱚􉎧𣑬q6~\u0011d\u001b\n𥡤-5B󽠤/>M0\t\u0008VK 𐄡&\u0010<𧘜𠢹2Id󲥁0\u000cKg\u0018ZF𨑖rE𦆶󺟨l$𝒹\u0001f􃚀T(bH_e\"ᷛw뇋\u00029o^\u0016𣼍eM\u0003𮈗-𬐵􏜍E󴁿\t𝤥@(뗅m𤐲𤮅󰽀Q\u000c~󼋽8Dv1眰m/r\u001bYr‒yq\u000f 􈶩x𗞰䖍𤟱\u0014\u001e𦙠zR\u000f􁇴0\u0004󼨃𤁳kFꢘ@􋌌xqo/R𨘒\u0013G\u0010𫮭⥺𗵛\u000e@NL􌻾\u00130)uc𢆾9󴎳󴲉1\u0011\u000c\u0014󸢕􌴒|\u000cN.仆z\u0005r\u0001,;\u0019𧯏𡏭\u0017a𣞸\u0019d\u001dKvH󵆷3G(F\u0014)`􊶃O􃇾\u0018\u000f\u000cqL􋌳\u001c\tI@\u0001/k}`e~-\u0015o𝟊S􍽩ྗh𖥒^u>󲑖1𣁪%E\u0011o𬣣us󼕮\u001e녩􏀹\u000f􀱵ꆻ\u000cl\u0001t/󺶻U𮙗󿎿\u0000\u0007𧇏\u001e𣘱\u001bI\u0001\\\u000c𬃳Ni\u0012=>\u0014#𝩶*\u0013I\u0017O\u000eh{􉃘\u001f彙\u001cw!MY𬕺X뎧.\tf?a䏷􀯎=\u0016󽂁􈌽3O\u0006􏭦􉅳r󽹸\u0000;}", + "email": "some@example", "password": "𭱝By$󳢞#JH*k𤘴\u0007S鴊`Yi\u00165𐰌󷔴s𤂜F彄\u000fQ􆝬+n\u001dj:`\u0002𠻢9V)t󽹺\u0005n􁐼8󻚣􍾴n||\u0010𔕱\u001c焞󾠵(󲮯徐j(􈩷3\u001cwS;\u0003]􊄾*􁎣j-󻲻-\u0007;`\u0004N\u000f􊁁" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_12.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_12.json index 7659f0df5a0..0117b0f581b 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_12.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_12.json @@ -1,4 +1,4 @@ { - "email": "ip𘑩􇦢\u0005h:󵎱G\u0015󸾲@󱮟6􎟖k", + "email": "some@example", "password": "h𩔟x^iBXW\u0007x}􈑶\u001b4J9􇝈󶀄􌙒.61x{c󵕔\nz韲8|󰠜\u000b\u0014k\u0014l'\u001b 𣚳},*Di鄡𫿏p𤵆%\u000fzoud\u000bU_y\r\"0󽛯<\u001c\u0001_;`w󰒯/x\u001d1r$Xnf\u0001l􇹌ana\u0005\u0005\u001ac;$\u00194􎺗l$L哅F􇖿󱟵Tq𤦓\u0015c1#SD󸕇v\t4kpS􍥮篏㤮G􂦴\u000f\u000ePT86H\u001b\u0007\u000f󴖭𠜮-\u0005OnJ䈶E\u001f.q\u0017𤑆`\u001a\u0013L蛌煒\u0015󹣾xY0\u001c\u001f\u0008sᆋg7\u00076𧲓 <=<􀵯\u0005􉑞󽁂aOrL󽲓0q􈗰g𭵟6\"#𡇋󼥅󱢓q\u0001V\u0006󾿱xO\u0005J\u0014\u001a\u0001𗏑\u0005:z#ﱇM\u0015􄽜4<2U!HWMMQ#𪍺h\u0012瑅}𝆝#\r\u000fL􈖨y#𗵲\u0001譫jS]!O􄝡脲󿍰o\u001b󼐕䍥$H0!DQ􄟨􌞧\"&J\u0011\u0018\u001e\u001d2󸏆S\u00020􁴸\u0001\u0017Q𡖚𣢉\u0002c9L􉘐s㑻4KFz󽩼\u0001䠋􃩨\u001d6ink>><󴽪Iw\"\u000f\u00182'@29\u0012옋\\􆽀\rპ\\4w󰄺%/6􍥁g~.g𒆤웟􉋇 Au\u0017P𢟎𩡆𪥯x􋏚􇹽袢􅂐?𘍳\u0006I :\u0002Zy$\u0019#􊯤𣳱jv\u0008𫬹<\u000f\u0004𦻼\u0013!󶿉a\u0007|悔])\u0011\u0000]K󽝳2qo6,𘢚z鉿􎯋Av󳄀v󱸕\u000b[e\u0018\u00124!)\u0007),@e?絠DAՌ%%F+󶅳Oc𪷁\u0007\r󹤟\u0000\u0004g4g*⊒AkKC\u001e\u0016g>=𩼅3󽲥\u0000\u0014􉋥p:2HH𡟯玚\u0013𤊦𧁈q\u000fk%𭾖嬐𢜸\\a𝓓E皌\u000f𬵟Nr\u0016$&󴱫􉭞i賉,𥄹\u001c7f𗻫e\u0018L鬜\u0012󳹥>\u0019󼀲N뵽\u0019􁹶7p􎽺vP1?,c􄃄Xv\u001aN'\u0001󴕬lE\u0007𛅳J\u0010𫈢U󸠵kP󷫈`O瀪\u0015jjC󿚼 Me`06.\u0001\u0019s􎩥󰞥Z)ǹBw\u0000zKZ𩮘H\u0003F6yhu𛉙触d+𝞱y􁤯󿇫􏷿)𤺇|\u000e\u0004 􉪰󰴆,\u001a$.-7𣦄a\u0004􄙜R5g􂜧rM𣄼󸂀H\u0000M\u0016\rT𪭁Z\u0015󲥑𮥝\u001f󺝎-덖\u0005󷣜~h\u001epV\u0015MTRyE9\u0017t\u0004繣𭊖𭥻]$v𬎬𬤦ap󽬜; 􊮞\t\"Ww𘇫$􄲆y:`M\u0000/~Z\\eF6󿇸\u00008󳭻𔒜1\u0000⾶\u0000daS\u0011􂍾f𒋚E\u000bt🖪C\u0019%Q􄘎𠙾Xp*78蟊`𢆃<􇘭dJ􃷨􉉊\u000fb8u𤂵\u001b3𨩘F􋥉\u0008㚕𔑵E\n>컕q\u000bj\u001a\u0018\u000e􏯍󰑫Y\u0004􇌁X%𣣰in󷤎w\u000c9𐃃􇃋󹁵Ff􂫌c>\n$}\u0019捋Y햋)􋤯肓󸡀\u001aW+l\u0007b慎\u0014𣟏FmR]|u\u0017kR\u00077D쫏G텫𨋊@\u001a貲\u0017/kt.\u001f{􊲺#^(zcVm\u000c􋇩'6j$[\u0006\u001b/VOQ󳕏VLw􄡆#f\u001eC𣷢𗳅@s딃me2\u0006e\u000cs5g\u001f𣗯𗴆I$t핽\u0017\u000f􆉨&tPz󶬙\u001e\u0015𤯼&`x󵺑?:1p\u001f󲢃\u001eC𦡐㬑X􀱥ﻈ_ZZ\u000eaTW\u0014\u0004􏎙$\u0018p귡|(𖦭67㴳7瓮􂝜𗼇zLc-Y^󵇠\u000c薻\n𖡂\u0008|󿃼\\\u0011\u0000􄾬\u0019,W7`𬶶\u0010𢪾Jn󳾘Efa\u0019\u0000JLwN" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_14.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_14.json index 0e55bc0b7b3..dbeb6287ed9 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_14.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_14.json @@ -1,4 +1,4 @@ { - "email": "q𘝔\u0011 3%Y+RO󹦸1@4`󼞏", + "email": "some@example", "password": "\u001a1󶾝􁒬\u001e\u001dG􍼑.􅪫T\u0002\u000f🞸:𨕩8\u001b1aV\"\u0019G|g|7\u0003P1􏳅A{_귗⪶bufZ?𗡳퉷𭟻\u0013󸑇~g0󶴽B𭠅\u001bR|\u000e'4\t\u0017 􊸚𐡹\u0016炽鱨􂟯􋕾I󹗑9𛁺\u0013\u0004P+\u0016z@m䁞!-u}(\u001bG\\𭷩\u0008|3󼋇鏰e\"赫-\t􋠙\u0012" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_15.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_15.json index 6f5ac6975c5..6aee4de4c76 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_15.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_15.json @@ -1,4 +1,4 @@ { - "email": "\u000c9a\u000clw\u0000,@i\u0012/d𦨥I\u0003\\𤰉t", + "email": "some@example", "password": "':󵶌%칅|󰸬󻪘𡌹䘒󵍋\u0018o+!4T\u0008\u0006𘎗W~󻌢𥄮\"u(4l4nH2􌽪A㻘-󸢦幍𛈛K3@q#𧙅A\u0003z}𮙾 \u0017rⓒ􇑫C냋\u000f|􄣡\u0016x\u0005<󸮙\u001drj􅏯\u001a𮓌\u0019\u0006[\u001e𡬈!VP\u0007u\u001c92f\\\u001b𪌥 ^g󷷺㐙t\u000cvS󾭫\t\u0005F\r}\u001bP𣎨<\u0005)kD𘔼\u0015􄣇@P𦲜-Jⰶ\u0000\u0012R춟x󴏹䗔\u001b{Y\u000ck\n酪\u0007b󻽢u6熞?5\u001b`\u000f\u0005`_𦫖2d\u001e~61Sk𫦔􄾈K쐏\u0010󸶔􇱄􎓻\u0003₮𠷽v\u0011+\u001c􎥎c8ꚣछV􍭎\u0003\u0012h9䎸L뱀7𗷚𥪶)u#\u0015,)vl󷰮7Ogx\u001eS\u0007𪜖L~'g]𐦍xG󳅺j^􍐱🆊ZD('\n\u0010\u00135􏣌𭋷L􋭔􆦦-z\u000c]la􊨳=]n#a󽣎睳􈩐\u000bCPCRM󾚺𣮁=uh2:Z\u0003螾E􍻔Y[N\u0013\u001b􏈨􄕅_7\u001c􂪹7󰻴4x\nE𐣠璚S瑣\u0008e6\u00081 F\u0008l\u000b\u001a󿍋𥑯\u001b🝟C\u0006vX'/4􊤁寖𥆅+󳌜$;+\u0013𪣎/r'󺶜\u000b󰈪H􎉄_M\u0019UU^􆾎X=\u00190\u0003\u001b􈒻DQ\u0012㥼𠠛=rm5Bv*𑁧󹙯𩸿" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_16.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_16.json index 872848275ba..337516617aa 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_16.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_16.json @@ -1,4 +1,4 @@ { - "email": "􄣅bp\u0002.=d@y㯼\u0013𧔬ꋑ厲󹂹Zq😶𣉂", + "email": "some@example", "password": "󽊭\u0010v<𗋹:C󵹱3\u000e1_!\u001bP\t𬊉JW?𗰎󸟶󿗙l􊡅=ᅥct}sUS􅭼\u0019\u0004\u0016ࠃ|/HR~.􀅄/N\u000c\\𢩄4羇t=CY𫳩y\u0013􁖼p}_\r\rhEa􆩏c\u000c3\u0002^@\u001d🥒@\u0002[=\u0005w\u0011󻜮\u0011𘫇~􊰉𗝀.􊁥\u001bn贳.􉜣]6`A]􀢦pA\t𝣾\u0000+\u0004Mu𦘱\u0005\u000f󾶉\u0003?&\u001c\u001dX􌲐dh6Fpዽ\u0014i𡒹􄔿3Z7@8\u001b{\t\u0004Q4g𣣼ᅴ)I𐁉[t\u00049󺥓w3\u000eF<\u00000􅬩~#俤m6I\u0018􍲉󱒮-\u001b:76\u0014\u001f/?suL􂩵)x~\u0003n\nHI@b􌷳iT󱘩%{a츽!%𗍬Key􉛙>󶎤h*𒈔Rs\u0005[!R\u0019\u000f🦨a4쒯b=r<\u0001AQy_u󰯸\u0015𑿝\u0010j9,\u001e󶈰􂲡[𑙄)\u0019汼O\u0004󻙓\u0015􄭠\u001am\u0017m;~!Nqx\r<%?\u0018\u0002K[KxY5M?\u0013nS;󽫘􂃁@𢺾1lb鴛-\u000c\u001fJZ󳥶Ki\td􊛨i𠓓[𘣣~$#\u0016\u0001gށ􃵽g\u00169];|\u0007𮗨bR\u0018햜𢾨󼹶誽AlfL08𝥐\u0010\u001f=.0\u0008%yk6n𩕭\u0005[\\\u0015󼈛놢R𮘔𫶞[f󽚽5\u00004f㋟𫇇t􌰋𦄅*\r/V$󽝞껢8<𐓻𠔷-\u00082􄖥6%f_-􎰒3a\u001b_􏍫\\u3XZ9 𭶙Y5􂮇(󵴩R\u0008󶍹f\u0008󻱍z}\u0008\u0008Y6UOz=ᚇ󾯇W,􏔟\u0014󾽦𐇴\u0016>􂬬𖢸\u0018􏾘Z􆹇󸵥g\u001a􈯣Z\t!Q46\u0012𐳒jM\u0003c􁙬EZ\u0007𗒸rcDK\u000f\u00029ⱬ􇣣\u0008s\u0018}:􍂌쯸!gp\u0015z\u0015𝑷\u000eL󻕜\no𗹦;*E\u0007㵀gST\u0008j~􅟲d􌩣V󸂙Qf􄸿L\u000ei\u000cY𮢄6\u001d<6N\"\u0007],s9􋮖􅛼M\u000f󱴾k\u001f缤E􉯌\u001eTJkEc\u001c󷒩4􄷲QE󷒯慲8𖦝󰤾'𤇼\u0016􉚥\u0007>?\u000b\u0007\r{rW,󽏠d\u0005$2FA􂂲\u001fwL疯!󲩅\u0010\u001ecf𢙘\u0015)uSz􂟫O#B3;󼷸rC\u0002DA5\"\u0006 \u0008j\u0011^\u0006𣱺i\u0004􎬗BM]\u0018!􏘩𮗕\rA&\r;GTE\u0018z\u00196tQu𡕖Rn\tT[\u0001\u001cQn>愮L𖦝􍢚\u001c#𫽌&n󼚬󳷐\u0005K\u0000H#\u0010npu􂐌Bt{X\u001c2je@5󿧳󼯽􅢒;\"􌦍Q\n𘘙􉀀흝\u0011U~@:\u001f~锯\nRLiD󶁑q󲮹{9d🗤.퐏\u0015\u0008]HlZ𥛇\t柑쟷\u0018{3󻏟E;F8G0g󹯼&'󹹮C􁘋\u0014i@v\u0018%[_g󴖽5􉥛+;.DtH૨z|􀫰󿊥\u0014􀼴Z\u000cS󻐸\r💣r~r^[𩦪䘷o\u0001掯_H+;\u001d🌱\u0007&m󰏯U8f%\u001c䗮銜Z󻛩8},P􇳰H\u001a\u0008􁙸H^𠱎`gRf𠬶􊰁+V\u0013\u001c\u0001🖭#" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_18.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_18.json index 799eebd86dd..af8b333a696 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_18.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_18.json @@ -1,4 +1,4 @@ { - "email": "1+dHm*𑘍b@", + "email": "some@example", "password": "N𦄸\u0004C9\u0014\u0011\n𩤬裚堽/p􊠏8\u000bg\\\nYGz|/\u0007Nd󻕺\u0012%툶o\u0016\u000f闬r􈶵󱂾d􎲀o􊻨\u0010l5&}i\u001bg􍤵ᦫ'`8N􌂽@𡖨󶎄Q[o\\\u000c􍦥Ꮜ\u0007d~ꝼ\u0006Kꞃsnr]pLA\u0006􅀊\u0006𒁾Xl,;wz\u0019󳳼􅀌\u0016c\u0015A7q\u0013\u001d툃ft𬰣f𬠀\\󸈦􎒉\u0001፼9F󽄮𦍥D􄔂\u0000z󻡔𥄚4\r􅯼􅣿󽅊\u000fO\u0018\u0004Y9븲C0|8𩉛]n𢍦\u000f󺉪ra\u0005􆯖\n]J\u001dBﱲ\u0003gT𗘠P\u0004>tﳨ\u0007\u0018r\t󶥍;Yy\u0000\u0019􅥯Qz\u001a-'\u0017*\u0001;J4]9𬥼\u001b𡫀[z鵺=cpG_;\u0004u􇏎ic\u0004\u000bkz\u001a|}󷂕\u000b]儼G3\u000c\u000c0\u0010d\u000b,􇱜䍜[#~8OG`n)􍼫5𢻤\u0016𐠧wK\u0001;\u0011+[􊖢'󷘅మ\u000e母-\tB􃃓h\u000bVl\n$\u001e\u0017\u001bw^!\u0010􍤩\u0013󰯽3j\u001c𤛹8oj𗜂\u0008󶧡t4>:裞_\u0004\u0018𡇔F󺶤lup\u0003+n\u0001P'yV;:𩧱V\u0011~ 1󵻽􍒽A-\"&8}\t=\u0019\u0001\u0015\u0013F󰮹􌁈\u0015픓/BIᎿ,#\u0019,󽉌gp\u001ew][\u0012>\u000b*b퇹,\u000e\u0019\u0002𑅥ŅCuTd𫬦27B󸝚oRm$𦢵Y3\n'?\u000f\u000b\nf𪵤K\u00016\u001bl]`U)m䣪" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_19.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_19.json index 8ac508be50f..141c7262b8f 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_19.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_19.json @@ -1,4 +1,4 @@ { - "email": "W󽅄`)SR\u0002,Zw\n@", + "email": "some@example", "password": "yMlﳹ0~􀠺󹑎a􋆛\u000cQ𗆨~\\y􆎛\u0019\u0003rqFsvG%So\u0006aTH,\u000c\u0002P𘞅\"\u0014o;UE\u0007\u0016퐃\u0012r`\u000bu1)2\u0013j*\"c9󱐿􄔾)h#𨊼m*j\u0013껮~v+@\u000b\n5*ui䎙uS󴼵􀸍sqM\u0002󼈲\u001c𭭳𦌨fG𦈯tv}걡?V\r􀬥]󷤅\u0017\u0010*E+V}𦃡9\u0019*󷿣[o䗚􎐂\u000br2\u0007􄩐\u0005 륅@O痔^􂭝Rꘞ!HLT󸭑.󿧅􇢯\u001d=+D𧈐4|󹾞T\u0000𛊢\u0017󻉘􁛀w􋃑#󽣊b\u0019F\u00073k\u0018*#󱟮3YKs\u0006\u001a\u0006𢗨D\n􉼼(\u0004􈥵&/u㑕W@)pLY󱆶)<{\u001d[LkCU|\\\t\u0018\u0013Q\u000f\u001dS\u0012􈐅􁥁\u0002\u0010󵢳3655H%\u0017󴯌\\k𤰟xl\u001dAf\u0018y\u0015ն𦡤󵊝\u0007uH\u001by0(㛓󰼔q𘏚\u0005󷼨\u001a6l\u001d􌶜H5\u000fX&a1j#e\u0004gf6􌟗{􌩁\u0006a{\r􆝞\u001a󹞬Ih\u000b4㙃􄋠\u0007Sq풼𡁳B #󹮁󰛘%1&ᙲ􌼤\u0000W\u0000󵥀\u000bハ,@p󳕠_s󵟌鱀:􁇫棁3m􆌹\u000b_󻟴\u0016L>\u001cq_1\u0003󷤑I6L\u0005𓅐󲾐󲭛YﯓzH\u001fu􂴦󶈀n\u0016vA\u0017G(\t~\u001dT闍N\u001f􁫇bju\u001e𨋍Z𠢄P ፱\rq\u0006\"K\u0001\u0014\u0007qcJ\u000f{𩽶􂄌K\u0000𪶚F􃮇,蠞\u001a\u0003W\u0014쉘\u0006N\u0004LAE#􁃧hIWO\u000bj𞥂:cP~q󵾵\u0005U4绝॒*z\u0004){\u0000.T􄁆=𫍌𮄾􏡉]\n쎚4s\u000c󷃵𢐦+층\"Z\u0011oN2𪡥\u001coee\u0011wD\u001c\u000eZ𨖯4\u0008\u0000\r󠇁𡨝\u0014:7󼫢" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_2.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_2.json index d0a40ab6719..17f66be22c5 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_2.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_2.json @@ -1,4 +1,4 @@ { - "email": "莟^Oz􏽿􀖮NG󸵚\u001djꦧ\u0000i𨙻@0&\u001bi^i\u0003G\r\u0016", + "email": "some@example", "password": "􋥵OY \u0000􆀲\u0001X昵}􁏀\u001a0@쁑N󿀾\u001c\u000c.+v1n{F'󷏺u𣮶.v=𭵷ᇎ\u0010af\r\u0015e\u0000\u000e\u001d􊞽𡫳\u0001)􈏘hw\u0001\r\u001c\u0013Z6僭X􋽭h]0%no\u0011󹦌 JEJk􆾒󺽣􌱹dTrtvuz􄥱𬁹f󱫿8\u0008$W0\"\u000e􄝑􌴇~3𔔀~f\u0005z\u0010f\u0003𡧶`1\u0006t􈯅@󺌇_󶞾\"]󺰹󸮍𥢡o0>Qd\u0006`GD\u0000󷙦p쥚JF\u0007w\u001cCo𨛚\u0012\u0005\u0008q\\;\u001a󰺊to/Q/3⢺F𓋂j𤁫}yi_/\u0008j*y󶮴&\u001c#KG󾈵\t{\u001fn󶛵󲮖|\th;CJ|S𦮤D􌧩rh󾰤\u0013\u001e5VV󸕦ux9\u000eu𢛮?􍰨_\tx>4󲵔䑿-d:y`*T7𨫧_7\u001d\u0016迯bkb\u0019\u0002􀱓\u0014$󹭉i\u0000𫢓𨻿xUT󴿦a\u0003𠯎V^\u001dvz?;E𥡬𐘷炙\n8X𬳲󳠖\u0018ot\r\u0017\u0000V9>𞹮`c\u001a𐍱W𧚎졳𝝳rh\u0013&𢊂􍌣RC\u000b&=2\u0008􉒙\u0003󲲷)⇿7Uj󻛸A,ckL\u0018𦁑o27𤲅\u0006L\rucW\u0013𮎶Bw_=\u0008" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_20.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_20.json index dd21aa39c15..b785dce2c83 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_20.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_20.json @@ -1,4 +1,4 @@ { - "email": "#႑|䤉􆨕@\\\u0003􊘆A \u0003DT\r<", + "email": "some@example", "password": "y\u001aWK􏯩\u001f:d󳕈1󾴚d\n]煚\u0001󼍉Q\u001a\u0010􀋏\u0018󰖝#7$􄧙'i2/%u84\u001e*󱧻󽫠4^BB?OxꄨsW\r􏗬2\u0015F.􂓒I㎐8\u0002\u0003`\u001d!􀴭<5Y􁌨d󻕺P􈽷P􏎁?1􌦺n\u0015a𮫲-𩛄╭\u0001c\u001cne_􉿗􆊛c7⓮Yoe\u0000_>𫦈\u0015\u0000N𝖺󸓅J\u001a⎎OkZ~yV$\u0001R\u0000Q􈷼[􅳱􍟏?\u001e嬣󵬡t`๏}t^p\u001d偣􈞡qA\u0008R^폗􅔮f𠘢qZr\u0003K\u00051\u001a$󿺞󾐰􂉗󸂝\u000b_C\u001ab[g𦉇i􃢕j%jZ9L\u000f6􊭶l~\t{}>N󺿆􋄆;\u0018󷒚#\u0013:\u000c\u0004.󼢨|󴇆%iMr𠒭:\u0003\r􂱰\u0017B󻡲;\u000eT_KJ=b0󴰃/u\u000eq\u000ccV[h𣫻}`\u0015N\u0010P🠇H\u0018:F쉧󱔃+󽋯%2}|s1숓[Rp\u0001O\t_E\rK\u001a|\u0001\u0013x$󹉼􈞅.ﲻ\u001bZ𑄦\u0001U`\t^\t0'8\"O\u001a􀆌\u0002nbGo3_􁾈􈥫󳇍\u0016.%\u001e瘿􊠾Q\u001dD:gJ=Q-0􏽟q\u0002x\u0003r\u0005T?\u0003+$獶>􍙎㼘ƌ\u000c𣰹Qj\u000f􄭖Fs.\u000e𝁼Y.󹾊J荥\u000c𥲐A𦑤ᏽP5\u001c\u0005𝂓\u0001\u0001􂤣􃴕䎬􃉉/x?\u0016I𭓍\u0015U󵾰㜧Ig𣛦L]^.𨯉}&(𭿟𮂌vGg3󺢃\u000el\u0004𢱠\u001e\u000cs𨎁p5LS󳮊\u0001|\nvm6#􍟀􍝒\u001d\u0011\u001e\u0008𦣯󵫎\u0001\u001b𬻒􁆽bE\u001e\u0006`wj#󷦙>Q\u001f􀭜ss𤶴􊵩ck%>QຶB$\u0002&澪%\u001fT\u001f\u0017:B\u001b󺓍\u0008C𭔙h;O:?\u000b󼯿j𡜘j󶺇;t􇲢\t\u000e@ዷ󼯥p𮠏\u0007󳍵㷲@,𩒝duBfR\u0010𑲯!\u0005<\u0012\u000ej􇷠XFs󽒇'W\u0000@SDA􉨥" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_3.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_3.json index decd3266f04..431930dea36 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_3.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_3.json @@ -1,4 +1,4 @@ { - "email": "h)샓Z􃮐􅣒@𭦧\u0019上󳉅f\u0008", + "email": "some@example", "password": "K༁|􉄞HU\n\\Qik\u0019\u001f𥡰xG𨘵1g鐟9􍩩v4􈙇\u0008\u0005􄪒􉀚2\u0018􈀏|󶐴\u0015&\u000f;2\u0011\u0008󴮏A\u0007=u\u000f鲣\tz!pwH1\u0014\u000e.lexJ\u001e􂍦3\u0019&s𫑾󵋈\u000eN􊻦\u000b􀧎쁨􊞆#n\u001eY􉧦\u0010#%ObL=s(\u000c\u0019󰤹􇯭y@i\u0018/o\nof}Z𝩘V\u0000DqD<\u0004󱀁𖡐 ;\\\u001dJ)\u001b𩤤Sth\u0002^'󲞛󲶃𩵝/ct9\"\u0005$s*􏡋󰉓T󴌮\u0008\u0018􁉕󼁤9Drm𒄠$3𓉽j鉉y*R@󿏲􆣙mGM󺈅^~\u0003\u000e*\"2#􋘰S\u001c\u0018|Y]\u000eWy&N𦾐Dw\u000c\u0013Y팀%^+􅖘s􈍛\u000ee􇖙\u0016󽦄𑚫잙o\r𭈵z\"jn2\u000c'(B\u0016?s#*d\u0005\u001c恘\n𧋛\u0005􉴊$f)n揊}&N\t􆏞\"1􇝈u_QCU\u0016eR`󷀔N\ty􀇡㶶\u0011􎏏k;􉬬𣐧t~JX=vN6\u0007_~C\u001f󴚶\u000e􉿣?$b󻷡\u0019A\u001a=vm\n\u0004󻵊'g@\\𧨥AhVW銶𣆎𠂹󿹀ဲ\u000bO)U󾚢󻩣\\Ly髽eA4ꎍ\ni󲭧ꫨ\u0004_𗶱n\u0014𢙎\\S%쏻􇫬e\u0018u󳪋􀨿'V-D󻍓X,q\u0013\u0014𩘩\u0005$땯ﬢ.e2l􃊡\u0015\u0003{TW\n칀ԱH􀋝􂨲󲸢􋔋\u000f<\u0015M;[d\u0014\u0008}y3Jt𘝵%l.\u000cU󾳱H\u0002-.#\u0001\u000c𥂺!\u0018Y{\u0017d𥊑[(󾇘]j𬐻am!Z[w\u0002'*Tv𪨝PM~󼚕0\u001e!b😒\\P}3v\u0019n􃽻9F2gW4𡾪󹦃\t𫇘𤀃𪷬hR\u0005%鑎z󰕥󷚐?*㘈(#\u0019r\u0014\u0008r𧡝WN[ N{􋞱\u0019v틈\u001a󱠨􌭋􁛙\u0011[S\u00138\u001e?􁴏\u0007A\u001f󿳐A\u001db𘊨􂐆-\u0018(jᮬt\u0005;}\u000fHX\u0017]_,􏅩xVY𖡊\u0004H󶟹i𗛽\u001duu'>S\u000f\u0001灋䯲󻒦\u001db󰨀𧙰/\u0019􆌋7\u0011/X\u0014oZ󴿝𩴑橤|FN'􉨃\u0011􋖬wB\u0010]뺝 V􆟅􃪕\u0007n􌚟\t1􀉊 [}C}gG􏧭#o󱲻kF\u0003\u0008K􄝬g<\u000fn\u0018Z𗔷$}h\u001f\u0016𣺺5m𓐛󹫟<󺍝隴h\u0010鋬󰅖\r#_􇾑fD\u0016`󿽼_{n~I\u0004#7\"K\u000b{溃L􆵑\u0010󻅨󱾍濾]l x\u001f􂲹𬃪U\\\u0006𡟿鐵\u0008>\u001e$Y􍷋\u0004\u0014𡀅𦿦𐋸gE[Z🢇󻤻\u0016@\u0003􉼢uὨ뛫(.\u0005" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_4.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_4.json index d8e2569373d..9714e72d3aa 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_4.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_4.json @@ -1,4 +1,4 @@ { - "email": "𭿈%𡉋𩞄@wY0nE뎟d,", + "email": "some@example", "password": "󳠀`h𠻃[/\u0010\u0016􂉌𮤺c\u0010𣔼\u0002$􀕶V\u0012}\"􁸤1蕹K\u001fC𬫻\u0005'󳫓70\u001ee󼁅\u0012w_꒫\u0016" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_5.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_5.json index 4e3d168c6b3..736dfafdff2 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_5.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_5.json @@ -1,4 +1,4 @@ { - "email": "􊔞􆵆\u0006f?􃱘\u0011R`@稺c?\u001d󶑤bOP", + "email": "some@example", "password": "K𑘭\u0019r쵦s>qsz􁙱􍞑㤲\u0019<*󼞒鎛\u0018^&💱9N <􊬐YR.𧺍A\u0006)u3􇋂6]𠬦IwBZ\u0002󲂩󵫐mA}2㊞y𞠫𩐘􅟏󼃡/Q%E\u0008L<\u0002I\ra/󷒉q-얤 ]U:^YR0){\u000c{g𩽦\u0006F\u001f<:𦵧]ICꩢ𘘓𗵰O($h󱼼\u0011\u0001e\u001c\u0001xQ)􂴟􅜖𫘡􀙴OZ􁧲\u0015\"󱦉/\u0013x[𥥯𭵑\u0010V𭪋𘎿\t𥺾E)H\u0007cIsa\u00184\u001a敖8𑑃\u0007i\u0003DD4w𬡠\tEjf\u0013h􂒹􂝘aߑ\u0006痕𠓙𝀉\u0016<,󴎲+Q2'o\u0019\u0002]\u0019%\u00123PQ&H󶓭􀤄\u0016\u001f󴾲YR󻘿䝌\u0000Ex\u000b\u0001𛀂𥬛𠏐O\u001f>FG\u0019ី󼹀(𭔲B4,𧢜􈒘\u0005󷁇HUJ\u0016􅎮*􌭹w;j7󾗻1TJx\u000f󿇞􌛧Pn\u0003*O\u0000t>a_X|_MmL៓\u0011󰒺󰭹:􋔽Cu[r>󵛘vp\u0014" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_6.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_6.json index c8274a44a88..e319feb7d9e 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_6.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_6.json @@ -1,4 +1,4 @@ { - "email": "z_󸷴\u0012@𬊩VDG", + "email": "some@example", "password": "R\u0015䬧m侯|𩋹|&\u0014펀_/􇦤Te𫸁" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_7.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_7.json index 77fce9a2b0b..83423c6a1bb 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_7.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_7.json @@ -1,4 +1,4 @@ { - "email": "R􎛏􌸽Tk뫀4\u000cm1r@`O|Q%", + "email": "some@example", "password": "AV$\r:\u000e􂒅\twBe#aP\u001df\\D􊸋\u000c\"􃙊\u0014􃩫N\u000bW󻿥𤹨; O􋤉󽱏𧿮􁉀󳙇𦩔𬀃*a󼁴w,9⺜芽󾞷Q$\u0015\u0015pv󲚺(Cl\u001b\"cg&2j\u0017𘢢|\u0005v&e븈gfK'j{F\u001e𪪻zn~棣a?󱞒\u0006 桸Z,#🞒:/􆌳󳽇/sGF\u0017i;{|⏺?np慗_pCi;1j􆩨w𠕊􍁦\u001e&􋉷q$\u0012p\u001dC\u0005V\r𫲎g\u001als,𨮳d\u0016C 󵇂<}􊎒)x>~𛰠VV󽧢\u0005m𫖰\u001e\u0012𣪐\u0002ykNo􎴹]\u001c\n\nZ\u0003p􊺵󾄼㦿𑐻oy!%m􁳺Pg煳Mz𤹿𢴥\u0005\u001doiO􇞷R\u001cHG(𥻡󾲨<[nAlz'\u0019N\u0007X^J-腭\u001a`ubS2a\u001c􊆡I\u0010\u0015􄖸u8rSJT蝝󰂉􍙏|\u0008󳥙V\u001a\u0018K.(𞲱\u0007􏙛􌕿y&t(픒𢊴Y@t\n]\rlJ-𐂇H\u000eEp_Nv<=^鐣\u001e>A\u001et\u0015L􈜵消t\u0004󹝹|/똱\u0000s50\u0004𨿈􁊛\u000f1􈈈\u0007󵕂\u001eᾲW𦗮vsqz\u0018耧m󰮙~j󿍯󻼇l𩒕Zd󻞀\u0001lKy\u0000ITt僥𭀈\u000eS(蚺K눟*\u0000n灼^𮄆0󽽸\u0013􎱧𤤾􈩵\u001c\\坚f􎈽&N(*𤹣󼊵\u0000~*_\u0015󼥹󹹭6s󿞀ꄡz\u0000s8!0m\u001fb𢲙\\1lu2?>7x2^t3퓙L􇴤盅|j[Hi\u000e75&\u0004!郫_pFu;'ᰍ(𩕑Jk\u000f;\n𖭯@mꅊ~\u0016\u000f长\u001c󾐞\u0006(qh%\"\u0016\u0017C􉼍C臘8Ff,O\u001bks.q阴J+W󲫼3/\u0007ͲQ/🏐&\u0004l\u000555]5\u001a<`\t\u000c[#\t7!YI\tei𐓷鐬1yUS􋢨X2o󻠚\t𗫥{\t\u0003Op\tf$B\u0002?\u0000xv󻓂P-u:CW?󷩺z\u0004hO&𗹺𫸇\u001b󿏖z\u0012𫦨7\u000e㹶@|𩺝5" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_8.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_8.json index 42dbd8365af..be7e48b51fb 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_8.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_8.json @@ -1,4 +1,4 @@ { - "email": "\u0007@z􄏊㘒V:𫩆\u0014𡙺", + "email": "some@example", "password": "󼉧,+柃:􅪤%G\u0015󳨌/\u0010@󺵖搷l7\u0010w\u0004e\u0003\u001fV󻆘i􍆙\tJ󹸺𭺳k𢻲Hz%|aG󽮼\r쮫Q\u001f&\u001d\u001942\u0006C{t䇣]g\u001cdO뻈`.𭀚`m[\u0003􇊡-\u0005㽕x\u001b􌿺N𢺒\u0010K8𭼹[󱜼󶎋k|e\u0000}\u000e󲋋<\u0001g󻹊G𗯂K|^􋙭\u001e𩀞M𩌲/i􋂔!#𭠔}ṡ\u00190􎞈2 B^\u0011e@,I\u001a􃁼𖼥v󶦰\u0000]\"c𡭼릶\u001e\u0001B\u0014;+N􎫘UI:\\Zc􄒉zm|+{W󱖆IৢMU\u0019󲙪\n\u0000H\u001b!n󹉝q.8{󵓌𧦻T>🎡􂤜_ \u001b\u0015Z\u00049𓅫󱸙r\u0017|\u0001𔙂'#ැ\u0003{|=P𥤷VTH\u0008󲊎℄'𗙥\u0001vSk󾜜컛瀖K#^備O!'_}􊾃󾩽\u0010\u0002Qp}\u0010fd🝬(\u0007;󵀋`_\u0007&𛃙LI\u001d\"𦵴\\71\u0007\u0012F󳬵𬪍L􅗔a@\u0016\r􆸃𗭩h\u0006P;_YjဪCH~V9\u0003\u0012M\u001a􎝞􂛡%5;\u0001d]" } diff --git a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_9.json b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_9.json index 2127624ac43..615f6e284d4 100644 --- a/libs/wire-api/test/golden/testObject_ProviderLogin_provider_9.json +++ b/libs/wire-api/test/golden/testObject_ProviderLogin_provider_9.json @@ -1,4 +1,4 @@ { - "email": "?!@\u000c󿱚v睽\u0006炬", + "email": "some@example", "password": "𮩥R\tF3{7󰳃Qᎌ\u001b𮕾h[\u0000V_m8\u0003:K\r󴺞(􉮗\u0014􄓐\rKz\u0017𗪁^o/&!6\u001aV\u0008UῚ-1n-󹪡r\u0003觼F\u0007􉧒+󱲍,\\Xm\u001b̕􎠎|텐加\u0019=\u001a􀊺fR\n󻄌cC>jhZ-\u000bBnq\u0005\u0006\"\u0012L􊄎\u001f;Q󸫀D\"Q@󺳴Qq儝" } diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_1.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_1.json index 39865211232..4c99e46ce63 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_1.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_1.json @@ -1,6 +1,6 @@ { "description": "3즽)S", - "email": "OR胆c@\u001e\u0005r", + "email": "some@example", "id": "00000002-0000-0001-0000-000700000006", "name": "딂\u0014", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_10.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_10.json index cf3dadd23ee..108ba95f964 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_10.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_10.json @@ -1,6 +1,6 @@ { "description": "󺔣隬E\u0001", - "email": "gd󿡐􅶲e@", + "email": "some@example", "id": "00000004-0000-0008-0000-000500000008", "name": "󺴓|\u001f𭰔-C:\u000b\u0018󵹅;|\u0013d\u0006>@𗛳𨠏2\u0010𡱷\u001aM7/􅂢b\u0006O3m[{", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_11.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_11.json index d28c998398a..d9352dc4b45 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_11.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_11.json @@ -1,6 +1,6 @@ { "description": "𓄃ME\u0011[", - "email": "@W", + "email": "some@example", "id": "00000001-0000-0003-0000-000700000004", "name": "T襶nJwq𐆗[\u001cV\u0012I𫱃\u0001\u0005J:\"ay󹕌󳸲󺟖L\u0001&%lT[l/?󾿛_\u0010rW󷑇󸕑]𡧻􈐋𔒡wM\u0014#颷", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_12.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_12.json index 1de307dbc97..5e1569b746f 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_12.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_12.json @@ -1,6 +1,6 @@ { "description": "\u000cE󽇵󿚪f\u0005", - "email": "Z𤕓@!󿕫𑀡\u000c嬾", + "email": "some@example", "id": "00000007-0000-0004-0000-000100000000", "name": "h-\rE,𤋍 􉪺t\u0013S\u0019㟏&8Nf\u0004E$;;𧽷\u000e\u0016𮬲D,pE\u0002?'X*\u0002\u0011>&􍕂WCGM=Ey􉫺,귅$", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_13.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_13.json index 4b7f555698d..52714a62ad2 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_13.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_13.json @@ -1,6 +1,6 @@ { "description": "\u000bY/\r", - "email": "\u0001#𠶗@u\u000e䮙", + "email": "some@example", "id": "00000006-0000-0005-0000-000800000004", "name": "r􋭯\u0012%_izF腯e딬\u0011/\r`u󹰖\u0007\u000ff􌅛f􆃯oP\u0019&𠀬\u001a􂧦􎯧jY󲓅􏚣\u0003d􃰊e󴥟HK🥏\u0003y􎋊TE\u0001?𤎅 \u0005\u001b󷿣q#\u000e\"(Q^𬰩&\u0016䥴PI]Q\"X\u001a㩚𣡦x '", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_14.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_14.json index 5688761f5ee..d5f80061f1f 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_14.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_14.json @@ -1,6 +1,6 @@ { "description": "-)\u0005/\r", - "email": "<@M\u000f", + "email": "some@example", "id": "00000007-0000-0006-0000-000700000007", "name": "\u00014\u0005\u0006>\rx~J$k!~\t\u00114󰢆\u0010\u0017\r\u0017y!9", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_15.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_15.json index df1f33f905d..03ce82319f2 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_15.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_15.json @@ -1,6 +1,6 @@ { "description": "$\u0013\u0003E", - "email": "ळ\u0018:c5@G􀐎E`", + "email": "some@example", "id": "00000001-0000-0006-0000-000300000005", "name": "uU1;袙b \u0005%􊇛쫏פּ2f􁗰\u0008􆴉\u00140({󰈩􋦲󳨤}z𩍖Lq\u00025.\u0002􊬡s󺉃[\t\u0018E𐠑𗏠W􍹱'󽆒_􍚸6g\u0007𧉏󼚱X5j󾒲Q𘩼텵o/<\u0013wm􅋾'𡌤F=𨲱d𨈦da󶯠6fbnN\u0017=􃟍\u0017x\u000b?{3됐u𥳩K𥣷朻", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_16.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_16.json index 6c3a2697049..172f35d92b8 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_16.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_16.json @@ -1,6 +1,6 @@ { "description": "\u000b", - "email": "𬊣@蕉z䇋'", + "email": "some@example", "id": "00000008-0000-0001-0000-000300000001", "name": "sl\n􍣷\u001a󱬙\u0007𢼩󸝷>\u0014\u000e󸇗𧅢7m7\u0000誐\u001e퍚uXwwA􃚆!𦌸\u0019N􈖆𨌉ᑞ\u001bEZa󻞿𭤷ZWY𮋜|&唙ikM\u001a􃆓􁷔𧇹󱚺󹊤K\u0001Y:\u001b*Wzc\"\u0006󽣕􊯎\rWB8jSl\u001d\u0019󵁂屋\u0002f`𠓤LN\nje", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_17.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_17.json index 8b71f3c17fd..b94aeda5e8e 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_17.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_17.json @@ -1,6 +1,6 @@ { "description": "𡪥5䁫", - "email": "X􀜨J1\u0016@󺇢%", + "email": "some@example", "id": "00000008-0000-0000-0000-000400000006", "name": "鄚|_;􊋼뱾\u00024/㍄yqDttZ\u001a􄍳y䔳𫓚", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_18.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_18.json index c6b3ce84320..105d1f609da 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_18.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_18.json @@ -1,6 +1,6 @@ { "description": "$󽋼3􄺁 L", - "email": "@\u0008\u0005<󶔈g\u0013󸘺x􀧹*8-T*\u0006'\u0015[/󾫄g𢪸VT\u0004。,𫰪􄿖kC𘄴s\u0010+\u001dz𓇑󾄗`l", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_19.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_19.json index 4e2c8c153df..b2c7466e28b 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_19.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_19.json @@ -1,6 +1,6 @@ { "description": "^ﭠo", - "email": "뒭`kmI@􍤻", + "email": "some@example", "id": "00000006-0000-0005-0000-000200000008", "name": "\u001e\u00161𭯇,%7\u000e􏭎\u000e\u0014󸩴k0*󴚍j\u0016㢎W󸽪]za粥𝢋\"9O-o", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_2.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_2.json index b8e14f08877..ac8e9c44717 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_2.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_2.json @@ -1,6 +1,6 @@ { "description": "}[N", - "email": "@\u00027𘐄", + "email": "some@example", "id": "00000000-0000-0008-0000-000000000004", "name": "?􀜟ヽ$?𤰔uTY􌸥fH\u0017\u0002\u0005\u0008\u0010%:!Y\u0017𖢍튑􋥤󷅺*]/Z􀗭>-\u0004󾌗󺘧!_*--7\u000ftEg\t󻍦\u0013􇪚\u0018vE\u0010𠼌?=\u000e󺡆齭𩀩O恁k\u0000󸳠纏.\u000f", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_4.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_4.json index b48c1863fd2..4da43c38fd2 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_4.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_4.json @@ -1,6 +1,6 @@ { "description": "𭃲j>W", - "email": "h𧟨\t􋽒G@𠼫/\u0014", + "email": "some@example", "id": "00000000-0000-0002-0000-000400000004", "name": "\u000e󿚃n󸑏7f􇉐i\u000f8|\u0002e\nN~$[vAUr1`\u0015\u000c/\u0008~􈵉PEhV={󽑌𧎸\u000c\u0019􃫟}}ుx󲹀󲹾􅇱%\u000coA씚𘉄~t𬢴⼰\t􆋲\rWA𭣅􍧟t", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_5.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_5.json index 0a523adb34e..0d7e9416b2d 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_5.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_5.json @@ -1,6 +1,6 @@ { "description": "T󻥮]\u0016/o", - "email": "%>@􆧊䋈q", + "email": "some@example", "id": "00000003-0000-0007-0000-000700000003", "name": "ᬋgr\n詥-鄼f\u000cJ9\u001el)\u000c倦_H^Xh\u0008A;O|", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_6.json b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_6.json index 6bf3095beba..8b82db2d6b1 100644 --- a/libs/wire-api/test/golden/testObject_ProviderProfile_provider_6.json +++ b/libs/wire-api/test/golden/testObject_ProviderProfile_provider_6.json @@ -1,6 +1,6 @@ { "description": "r", - "email": "󸳘@_q[w\u0000(", + "email": "some@example", "id": "00000008-0000-0000-0000-000600000008", "name": "\u0013 &3\u0010\n𧾋'ᅬ7᷂\u001bEwP\\𞢡\u0012^\"󹽆󴳐\u0013-g珖<\u000bფhAjOZ)󿊓W_𥪔𡋁s|+󻤌\"~D󽬴C\u0010\u0003𗐑\u0017w\u0001\u001191\"\"6D\u0008\u0010\u0000.PC\u001e\u000e􋒾󾽝<𩻦iuN𬢤􉬅U{wgq\u001cD\u000b󼨦\u001a\"\nw{Rl\u0006Ua3\u001eNx\u000f", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_1.json b/libs/wire-api/test/golden/testObject_Provider_provider_1.json index c646977db3e..07a5d2834ab 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_1.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_1.json @@ -1,6 +1,6 @@ { "description": "=󱶚", - "email": "@Mk\u0012󻎹c", + "email": "some@example", "id": "00000000-0000-0003-0000-000700000002", "name": "󰩉j\u00028'\u00145;QDq,z:4􂑎TQdrz齞r󲽝 o&\u000b쭂pVe􃡭x𧀷쑧'w#ﮜX􈌛􎞬𩇞󷒸)𒈜ꅓU[{bK\u0010\u0018狡󰻃𨋈\u001f,􉋟Inu茼E󵁰,󺴋\u0017\u0008\"\r󻫷\u001a@y􋥲𐊎m􃯹epM3Q{\u0015䧛8g2b\u0000􁏔\u00191\u0011\u001c1􆵸Ov𬺲u󱔩󵲿\u0001=\u0005$𭧄\u0015􍣷冯l\u0014/K)Y'⫨M\u001bX\u001e", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_10.json b/libs/wire-api/test/golden/testObject_Provider_provider_10.json index 8f42c362751..a6bfb96e1d4 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_10.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_10.json @@ -1,6 +1,6 @@ { "description": "E🍺:\u001cJ󺵘", - "email": "i􎈈V@\u0006w0\u0003&", + "email": "some@example", "id": "00000001-0000-0008-0000-000400000007", "name": "U𠑇2uXTV\u0012􋤕\"\u000c=K7}ws𛂑𒌥*1􂷗_\u0014⨫\u0005^4xt.􀅺毱m󺆜{\u001bt\u00192󼞀\u000cs\u001ai1⊹T􈼱=e輽횙1", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_11.json b/libs/wire-api/test/golden/testObject_Provider_provider_11.json index 1c12be8affb..b79eac431aa 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_11.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_11.json @@ -1,6 +1,6 @@ { "description": "\u000e\u001d􉵦\"'", - "email": "@\n\u000fX倉.&", + "email": "some@example", "id": "00000006-0000-0007-0000-000100000003", "name": "-\u0012\u001d\u0001q=~ 𨸳d𨙝HF9\u001fxT;x|2@a\u001e|\t,;Z\u0015󺌾嵑󿞌\u0018玨l\u00154\u001a\u00031b𢁡TSP謘\t\"𝃐;P𖡷\u0016\u0005 S\u0006EkM轳h𣌥󴣺𘑤w\u000c@\u0003O\u000c~P\"E6\u0007\u0013[7yu􏴴\u0012-\u0004󿔆2", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_12.json b/libs/wire-api/test/golden/testObject_Provider_provider_12.json index 839f5aefd30..bf691857870 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_12.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_12.json @@ -1,6 +1,6 @@ { "description": "x䍉@I%\r", - "email": "@m靐첇E\u0014󴆟", + "email": "some@example", "id": "00000006-0000-0007-0000-000300000006", "name": ",U5>pD󸉥O𩕽Rbk\u0019\u000f'V\u0011􂈜-]󴣏Q\\r-􅽥\u001a󺠄E󰵚\n!\u0006󷫭F|\u001bz󽯡s}h꣹O󷅢(v󻍓,C6쫃p\u0008󿋏5\n\t䠚􉼢㢿1\u0013}󷥋􌲒h󵿟𤬅>뻺7󼩀LI㊝Ck󳾚𧥋@\u0019\u001f󴊢`&zd7b􃈞Qܫ􏂧", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_13.json b/libs/wire-api/test/golden/testObject_Provider_provider_13.json index 09edc9f7510..fdbacbcba82 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_13.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_13.json @@ -1,6 +1,6 @@ { "description": "\u0012$=l\u00114", - "email": "y@\\", + "email": "some@example", "id": "00000004-0000-0002-0000-000400000008", "name": "뵀\u0010􃫻\r;\u0011T􉽸\u0005\r\u0004jtW󲔋S샎z𤣼(\u0011v󶀁𦬦QOl􉊯󽿤i#\u001a{􍆜(i\u0017J\u0003/s쥴?rre\u0004uf􅐷~\u0019𐀆􅾷E󿵙?-𨚮W6|A\u0019䩘@दp\u0010\r𨬆Y5\u0015p=,\u000e𨞰\rY\t𐩤\u0010𤣮r\u001b3XO􋧣!\u001d<\u001a2\u001e\t\u0006󻛌\u001b6\u000ca\u0013􇞲\"\u001dU|􄢗\u0001\u001b?5EO=4\u0010󽠋+e", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_14.json b/libs/wire-api/test/golden/testObject_Provider_provider_14.json index 3d1641cc6ae..d29399b6a8b 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_14.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_14.json @@ -1,6 +1,6 @@ { "description": "HY", - "email": "*\u001b\u001a󽅋@M/[O+", + "email": "some@example", "id": "00000006-0000-0002-0000-000400000007", "name": "uo;)𫘣/KN\u0006_#D{󼥙𬧩6X􅺉]<󿘴*%#􎁽uHJ󸢵q\u000exu󿶢􃢢󵗝􂈯􆃯M𝥑v<􋫀z覙M𗮚:t:I`Q3Vx䜽𤎉U~ᔒ\u0014`\u0003~󽇈󽮞\u0019`\"{VL툅@􋎇󿗌\u001a", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_15.json b/libs/wire-api/test/golden/testObject_Provider_provider_15.json index df8f252a809..e67b2210b5e 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_15.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_15.json @@ -1,6 +1,6 @@ { "description": "", - "email": "@𣕎\u0013&", + "email": "some@example", "id": "00000000-0000-0001-0000-000300000008", "name": "󼢁㽨\u0016;\t􀋐ln5핽z\r\"hdPTT㵨9S}oV?x>U\u001a𤋶G\u001c\u0010K긷tO􎹌i\u001d_v\\\u0018󸣸\u00118󸩚G", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_4.json b/libs/wire-api/test/golden/testObject_Provider_provider_4.json index b00cf53ab52..5070a3d537a 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_4.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_4.json @@ -1,6 +1,6 @@ { "description": "/㕐", - "email": "󱰹@'x􏑐#.U", + "email": "some@example", "id": "00000000-0000-0006-0000-000300000003", "name": "p󿾼q\u0004JO󳗥󲕦[=\u0018\u0017󳸫D\u000b|{𦯯n", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_5.json b/libs/wire-api/test/golden/testObject_Provider_provider_5.json index 341a08dcf52..7161def65d4 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_5.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_5.json @@ -1,6 +1,6 @@ { "description": "", - "email": "l\\@o:j\u0015", + "email": "some@example", "id": "00000007-0000-0003-0000-000800000005", "name": "𢅷粪#𫭶\u0015<􇕴\u001e㣕罉M􆒇~\u0006Z)x@_\u0017\u0001\u0014𣧠􁃑~b\u0008\u0016\\ꈉᡋj󽒌Cp>󼣳\u001a𞸟{=\u0010C𠵑𢛒\u001a]󴯌􏣷[釤;\u0016\u0006TpXQT|-獵󺯊0\u0018􀬉h]󷂀}􆓤DH𦠁\u000f~\u0013^", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_6.json b/libs/wire-api/test/golden/testObject_Provider_provider_6.json index dd922fd5645..6aeb112c072 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_6.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_6.json @@ -1,6 +1,6 @@ { "description": "j#G\r", - "email": "􀧊쭫4AC@5", + "email": "some@example", "id": "00000008-0000-0005-0000-000300000006", "name": "OT;/hR𔔼葙!~<󰐾𥟑FP\u0010pW.0짃f󶹪\u000f8\u0004ZIy𠿥𐁃|du#k\n2\u001b}W\u0014嶔TI5G\u0013􍞟\u0016)>&?􄰦~\n\u000c", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_7.json b/libs/wire-api/test/golden/testObject_Provider_provider_7.json index ef066b2d5c0..21272ca1dbb 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_7.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_7.json @@ -1,6 +1,6 @@ { "description": ")m", - "email": "@Z", + "email": "some@example", "id": "00000001-0000-0003-0000-000800000001", "name": "좺\u0018𬍥1\\\u001cV󲝕𐫙\u0012􌾇𥎌\u0016㰶Y\u0006}.b𧀛\t;𦋤u%0گ}f\u0002𡢔𭉲I]􇻲c", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_8.json b/libs/wire-api/test/golden/testObject_Provider_provider_8.json index c0744a404ca..650c354b334 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_8.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_8.json @@ -1,6 +1,6 @@ { "description": "𭂕2􇞫4", - "email": "\u001f?h@|^󸪛h𐚠", + "email": "some@example", "id": "00000008-0000-0007-0000-000100000004", "name": "x􍄃\u0013cQ\u0017w󸅙k}\u0017ᤎ8\u0008`폒􆗸JC곡,1\u0013^𧙓{\u0010:c\r+\u0005", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_Provider_provider_9.json b/libs/wire-api/test/golden/testObject_Provider_provider_9.json index 5db37c2a993..fb1e4cc4556 100644 --- a/libs/wire-api/test/golden/testObject_Provider_provider_9.json +++ b/libs/wire-api/test/golden/testObject_Provider_provider_9.json @@ -1,6 +1,6 @@ { "description": "", - "email": "@siw", + "email": "some@example", "id": "00000002-0000-0005-0000-000600000006", "name": "+QH􆄋2$DH\u001d𠳉oz&SQ󺪏Apl󾦣Dai𫖠`~ཝG\u001d@$i􏀹b\u001flBR\u000cIg󰢭𡵉4Pg[h\u0003\u0012􁚑4𣪢\u0003M*\\`(U&?yinFa(𪔗J,<\u00115R@󷼝\u0007AH􌩟\u0010\"𬊺𣖮\t", "url": "https://example.com" diff --git a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_1.json b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_1.json index 1c15f0a679d..37ecc03db09 100644 --- a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_1.json +++ b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_1.json @@ -1,2 +1,3 @@ -{ "qualified_user_client_prekeys" : {} -} \ No newline at end of file +{ + "qualified_user_client_prekeys": {} +} diff --git a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_2.json b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_2.json index 1da1f90e2a6..6114c42ac45 100644 --- a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_2.json +++ b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_2.json @@ -1,9 +1,10 @@ -{ "qualified_user_client_prekeys" : { - "example.com" : { - "44f9c51e-0dce-4e7f-85ba-b4e5a545ce68" : { - "123456789abcef" : null - } +{ + "failed_to_list": [], + "qualified_user_client_prekeys": { + "example.com": { + "44f9c51e-0dce-4e7f-85ba-b4e5a545ce68": { + "123456789abcef": null + } + } } - } -, "failed_to_list" : [] } diff --git a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_3.json b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_3.json index ffd0320a305..52d00bca511 100644 --- a/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_3.json +++ b/libs/wire-api/test/golden/testObject_QualifiedUserClientPrekeyMapV4_3.json @@ -1,10 +1,13 @@ -{ "qualified_user_client_prekeys" : {} -, "failed_to_list" : - [ { "domain" : "example.com" - , "id" : "44f9c51e-0dce-4e7f-85ba-b4e5a545ce68" - } - , { "domain" : "test.net" - , "id" : "284c4e8f-78ef-43f4-a77a-015c22e37960" - } - ] -} \ No newline at end of file +{ + "failed_to_list": [ + { + "domain": "example.com", + "id": "44f9c51e-0dce-4e7f-85ba-b4e5a545ce68" + }, + { + "domain": "test.net", + "id": "284c4e8f-78ef-43f4-a77a-015c22e37960" + } + ], + "qualified_user_client_prekeys": {} +} diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_1.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_1.json index 539ec4765c9..027adc7fe7d 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_1.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_1.json @@ -3,7 +3,7 @@ { "accent_id": 0, "created_at": "1864-05-09T20:48:17.263Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000001-0000-0001-0000-000100000000", @@ -18,7 +18,7 @@ { "accent_id": 0, "created_at": "1864-05-09T17:17:18.225Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000100000000", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_11.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_11.json index 62473fd9b9f..af0345204ac 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_11.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_11.json @@ -18,7 +18,7 @@ { "accent_id": 0, "created_at": "1864-05-09T09:36:08.567Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000001-0000-0000-0000-000000000001", @@ -33,7 +33,7 @@ { "accent_id": null, "created_at": "1864-05-09T11:56:16.082Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000000", @@ -63,7 +63,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000000000000", @@ -78,7 +78,7 @@ { "accent_id": null, "created_at": "1864-05-09T02:39:28.838Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000100000001", @@ -93,7 +93,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0001-0000-000100000001", @@ -108,7 +108,7 @@ { "accent_id": 0, "created_at": "1864-05-09T01:15:59.694Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000000000001", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_12.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_12.json index ffd2a8ea9db..3c1d41e23af 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_12.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_12.json @@ -3,7 +3,7 @@ { "accent_id": 0, "created_at": "1864-05-09T06:59:36.374Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000000000001", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_13.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_13.json index 230deefba01..5616b3ee0bb 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_13.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_13.json @@ -3,7 +3,7 @@ { "accent_id": 0, "created_at": "1864-05-09T17:55:15.951Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000100000001", @@ -18,7 +18,7 @@ { "accent_id": null, "created_at": "1864-05-09T05:08:55.558Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000001-0000-0000-0000-000000000001", @@ -33,7 +33,7 @@ { "accent_id": 0, "created_at": "1864-05-09T11:18:47.121Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000100000001", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_15.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_15.json index 9f98be5bfc3..c903b0f64dd 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_15.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_15.json @@ -3,7 +3,7 @@ { "accent_id": null, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000100000001", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_16.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_16.json index 926331afd80..5c4e87f3ca2 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_16.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_16.json @@ -3,7 +3,7 @@ { "accent_id": 0, "created_at": "1864-05-09T23:38:23.560Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000000000001", @@ -21,7 +21,7 @@ { "accent_id": null, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000000000000", @@ -36,7 +36,7 @@ { "accent_id": 0, "created_at": "1864-05-09T18:46:45.154Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000000000000", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_18.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_18.json index 76aba23d150..a38d13d0654 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_18.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_18.json @@ -3,7 +3,7 @@ { "accent_id": null, "created_at": "1864-05-09T12:35:16.437Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000000000000", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_4.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_4.json index 294ff7981f9..e08a9b2e2fe 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_4.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_4.json @@ -3,8 +3,8 @@ { "accent_id": null, "created_at": null, - "email": "@", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": null, "id": "00000000-0000-0000-0000-000000000001", "managed_by": "wire", @@ -57,7 +57,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000000", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_5.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_5.json index 531cb1926ec..aadc9a62d95 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_5.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_5.json @@ -3,7 +3,7 @@ { "accent_id": 0, "created_at": "1864-05-09T12:39:20.984Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000100000000", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_6.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_6.json index 9be9dd54244..dbfdd882e1e 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_6.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_6.json @@ -18,8 +18,8 @@ { "accent_id": 0, "created_at": null, - "email": "@", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": "", "id": "00000001-0000-0001-0000-000100000001", "managed_by": "wire", @@ -33,7 +33,7 @@ { "accent_id": null, "created_at": "1864-05-09T10:59:12.538Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000001", @@ -48,7 +48,7 @@ { "accent_id": 0, "created_at": "1864-05-09T23:24:12.000Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000000000000", @@ -63,7 +63,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0001-0000-000000000001", @@ -111,8 +111,8 @@ { "accent_id": 0, "created_at": "1864-05-09T01:45:42.970Z", - "email": "@", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": null, "id": "00000001-0000-0001-0000-000000000000", "managed_by": null, @@ -130,7 +130,7 @@ "accent_id": null, "created_at": null, "email": null, - "email_unvalidated": "foobar@example.com", + "email_unvalidated": "some@example", "handle": "", "id": "00000001-0000-0000-0000-000100000000", "managed_by": "wire", @@ -144,8 +144,8 @@ { "accent_id": 0, "created_at": "1864-05-09T23:36:06.671Z", - "email": "@", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": "", "id": "00000000-0000-0000-0000-000000000001", "managed_by": "scim", @@ -160,7 +160,7 @@ "accent_id": 0, "created_at": "1864-05-09T14:01:50.906Z", "email": null, - "email_unvalidated": "foobar@example.com", + "email_unvalidated": "some@example", "handle": "", "id": "00000000-0000-0000-0000-000100000001", "managed_by": "scim", @@ -174,7 +174,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000100000000", @@ -192,7 +192,7 @@ { "accent_id": 0, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000000000001", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_7.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_7.json index eadcd2b4997..8c36301dac5 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_7.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_7.json @@ -3,7 +3,7 @@ { "accent_id": null, "created_at": "1864-05-09T19:22:39.660Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000001", @@ -33,8 +33,8 @@ { "accent_id": 0, "created_at": null, - "email": "@", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": "", "id": "00000001-0000-0000-0000-000000000000", "managed_by": "wire", @@ -49,7 +49,7 @@ "accent_id": 0, "created_at": null, "email": null, - "email_unvalidated": "foobar@example.com", + "email_unvalidated": "some@example", "handle": "", "id": "00000001-0000-0001-0000-000000000001", "managed_by": "wire", @@ -64,7 +64,7 @@ "accent_id": 0, "created_at": "1864-05-09T00:45:08.016Z", "email": null, - "email_unvalidated": "foobar@example.com", + "email_unvalidated": "some@example", "handle": "", "id": "00000000-0000-0001-0000-000100000001", "managed_by": "scim", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_8.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_8.json index 4857e4d39f8..f1bc59bcaae 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_8.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_8.json @@ -3,7 +3,7 @@ { "accent_id": null, "created_at": null, - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000001", @@ -18,7 +18,7 @@ { "accent_id": 0, "created_at": "1864-05-09T13:46:22.701Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000000-0000-0000-0000-000000000000", @@ -33,7 +33,7 @@ { "accent_id": 0, "created_at": "1864-05-09T09:25:11.685Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0000-0000-000000000000", @@ -48,7 +48,7 @@ { "accent_id": 0, "created_at": "1864-05-09T11:37:20.763Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000000-0000-0001-0000-000000000000", diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_9.json b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_9.json index b3d92014d44..bb8d4a58d9e 100644 --- a/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_9.json +++ b/libs/wire-api/test/golden/testObject_SearchResult_20TeamContact_user_9.json @@ -18,7 +18,7 @@ { "accent_id": 0, "created_at": "1864-05-09T16:22:05.429Z", - "email": "@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0000-0000-000100000001", diff --git a/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json b/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json index 01fa58df1a6..fb9fd970c7f 100644 --- a/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json +++ b/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json @@ -1,7 +1,7 @@ { "accent_id": 1, "assets": [], - "email": "\u0007@", + "email": "some@example", "expires_at": "1864-05-07T21:09:29.342Z", "handle": "do9-5", "id": "00000001-0000-0000-0000-000000000002", diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_1.json b/libs/wire-api/test/golden/testObject_SendActivationCode_1.json index 25f7f5a2db4..1144f68d89c 100644 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_1.json +++ b/libs/wire-api/test/golden/testObject_SendActivationCode_1.json @@ -1,3 +1,3 @@ { - "email": "󹛃@nK" + "email": "some@example" } diff --git a/libs/wire-api/test/golden/testObject_SendActivationCode_2.json b/libs/wire-api/test/golden/testObject_SendActivationCode_2.json index 775259f31e5..f4224036325 100644 --- a/libs/wire-api/test/golden/testObject_SendActivationCode_2.json +++ b/libs/wire-api/test/golden/testObject_SendActivationCode_2.json @@ -1,4 +1,4 @@ { - "email": "b@4M􆳤P𤣛$[\u0012j", + "email": "some@example", "locale": "cu-VI" } diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_1.json b/libs/wire-api/test/golden/testObject_TeamContact_user_1.json index c7d636499aa..100e28b6e58 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_1.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_1.json @@ -1,7 +1,7 @@ { "accent_id": null, "created_at": "1864-05-11T12:52:22.086Z", - "email": "({@q", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000000-0000-0001-0000-000200000001", diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_11.json b/libs/wire-api/test/golden/testObject_TeamContact_user_11.json index 231eca48c3c..5ea5a511e1c 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_11.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_11.json @@ -1,7 +1,7 @@ { "accent_id": -3, "created_at": null, - "email": "m@𬯅", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000000-0000-0000-0000-000000000002", diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_12.json b/libs/wire-api/test/golden/testObject_TeamContact_user_12.json index bfa2dc5b6c7..5c355ec41d7 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_12.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_12.json @@ -1,7 +1,7 @@ { "accent_id": null, "created_at": "1864-05-06T13:09:44.601Z", - "email": "@(-", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000002-0000-0000-0000-000200000000", diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_13.json b/libs/wire-api/test/golden/testObject_TeamContact_user_13.json index a54789ad3f7..ea2a4b7b2d6 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_13.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_13.json @@ -1,7 +1,7 @@ { "accent_id": 0, "created_at": null, - "email": "\u0001㗅@_C", + "email": "some@example", "email_unvalidated": null, "handle": "S", "id": "00000002-0000-0002-0000-000100000001", diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_14.json b/libs/wire-api/test/golden/testObject_TeamContact_user_14.json index f7211944180..1522c9b809c 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_14.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_14.json @@ -1,7 +1,7 @@ { "accent_id": -3, "created_at": "1864-05-08T20:31:37.388Z", - "email": "4)=@I\u0010", + "email": "some@example", "email_unvalidated": null, "handle": "\"\u001f\u0014", "id": "00000001-0000-0001-0000-000100000000", diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_15.json b/libs/wire-api/test/golden/testObject_TeamContact_user_15.json index 0fada5ea3a4..8189e7c0912 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_15.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_15.json @@ -1,8 +1,8 @@ { "accent_id": null, "created_at": "1864-05-11T14:15:19.890Z", - "email": "9L@(", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": "J", "id": "00000002-0000-0002-0000-000100000002", "managed_by": null, diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_16.json b/libs/wire-api/test/golden/testObject_TeamContact_user_16.json index 08f6d920126..906af84cc8f 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_16.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_16.json @@ -1,8 +1,8 @@ { "accent_id": -1, "created_at": "1864-05-08T15:43:05.866Z", - "email": "@j", - "email_unvalidated": "foobar@example.com", + "email": "some@example", + "email_unvalidated": "some@example", "handle": null, "id": "00000001-0000-0000-0000-000000000002", "managed_by": "wire", diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_17.json b/libs/wire-api/test/golden/testObject_TeamContact_user_17.json index aa3c6db73e2..421e450a4a0 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_17.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_17.json @@ -1,7 +1,7 @@ { "accent_id": -3, "created_at": "1864-05-10T20:50:28.410Z", - "email": "X󵿆@D(0", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000000-0000-0001-0000-000200000001", diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_18.json b/libs/wire-api/test/golden/testObject_TeamContact_user_18.json index eeeb205a10f..8e8d71fc661 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_18.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_18.json @@ -1,7 +1,7 @@ { "accent_id": 3, "created_at": null, - "email": "􎲮]L@屰", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000002-0000-0001-0000-000000000002", diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_19.json b/libs/wire-api/test/golden/testObject_TeamContact_user_19.json index 14b5de5a6b8..e3320f0d3b6 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_19.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_19.json @@ -1,7 +1,7 @@ { "accent_id": -3, "created_at": "1864-05-10T11:20:36.673Z", - "email": "N@", + "email": "some@example", "email_unvalidated": null, "handle": null, "id": "00000001-0000-0002-0000-000200000002", diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_2.json b/libs/wire-api/test/golden/testObject_TeamContact_user_2.json index 06b1c2db6b4..ecfd6aad83e 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_2.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_2.json @@ -1,7 +1,7 @@ { "accent_id": 2, "created_at": "1864-05-08T03:35:20.125Z", - "email": "\u000f5g@", + "email": "some@example", "email_unvalidated": null, "handle": "", "id": "00000001-0000-0001-0000-000000000002", diff --git a/libs/wire-api/test/golden/testObject_TeamContact_user_20.json b/libs/wire-api/test/golden/testObject_TeamContact_user_20.json index 9985b7e57d2..6b57101d2a2 100644 --- a/libs/wire-api/test/golden/testObject_TeamContact_user_20.json +++ b/libs/wire-api/test/golden/testObject_TeamContact_user_20.json @@ -1,8 +1,8 @@ { "accent_id": -3, "created_at": "1864-05-06T18:23:32.240Z", - "email": "=0.2 , deriving-swagger2 - , either , email-validate >=2.0 , errors , extended @@ -615,7 +615,6 @@ test-suite wire-api-golden-tests , bytestring-conversion , containers >=0.5 , currency-codes - , either , imports , iso3166-country-codes , iso639 diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs index 13f0093ddd8..c604fb36ed8 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs @@ -11,15 +11,15 @@ import Wire.API.User.Activation (ActivationCode, ActivationKey) import Wire.API.User.Client (Client (..)) data EmailSubsystem m a where - SendPasswordResetMail :: Email -> PasswordResetPair -> Maybe Locale -> EmailSubsystem m () - SendVerificationMail :: Email -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () - SendCreateScimTokenVerificationMail :: Email -> Code.Value -> Maybe Locale -> EmailSubsystem m () - SendLoginVerificationMail :: Email -> Code.Value -> Maybe Locale -> EmailSubsystem m () - SendActivationMail :: Email -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () - SendEmailAddressUpdateMail :: Email -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () - SendNewClientEmail :: Email -> Name -> Client -> Locale -> EmailSubsystem m () - SendAccountDeletionEmail :: Email -> Name -> Code.Key -> Code.Value -> Locale -> EmailSubsystem m () - SendTeamActivationMail :: Email -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> Text -> EmailSubsystem m () - SendTeamDeletionVerificationMail :: Email -> Code.Value -> Maybe Locale -> EmailSubsystem m () + SendPasswordResetMail :: EmailAddress -> PasswordResetPair -> Maybe Locale -> EmailSubsystem m () + SendVerificationMail :: EmailAddress -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () + SendCreateScimTokenVerificationMail :: EmailAddress -> Code.Value -> Maybe Locale -> EmailSubsystem m () + SendLoginVerificationMail :: EmailAddress -> Code.Value -> Maybe Locale -> EmailSubsystem m () + SendActivationMail :: EmailAddress -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () + SendEmailAddressUpdateMail :: EmailAddress -> Name -> ActivationKey -> ActivationCode -> Maybe Locale -> EmailSubsystem m () + SendNewClientEmail :: EmailAddress -> Name -> Client -> Locale -> EmailSubsystem m () + 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 () makeSem ''EmailSubsystem diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs index 519c5101cb0..2fda920c11f 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs @@ -47,7 +47,7 @@ sendTeamDeletionVerificationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Code.Value -> Maybe Locale -> Sem r () @@ -59,7 +59,7 @@ sendCreateScimTokenVerificationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Code.Value -> Maybe Locale -> Sem r () @@ -71,7 +71,7 @@ sendLoginVerificationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Code.Value -> Maybe Locale -> Sem r () @@ -80,7 +80,7 @@ sendLoginVerificationMailImpl userTemplates branding email code mLocale = do sendMail $ renderSecondFactorVerificationEmail email code tpl branding renderSecondFactorVerificationEmail :: - Email -> + EmailAddress -> Code.Value -> SecondFactorVerificationEmailTemplate -> TemplateBranding -> @@ -114,7 +114,7 @@ sendActivationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Name -> ActivationKey -> ActivationCode -> @@ -128,7 +128,7 @@ sendEmailAddressUpdateMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Name -> ActivationKey -> ActivationCode -> @@ -138,7 +138,7 @@ sendEmailAddressUpdateMailImpl userTemplates branding email name akey acode mLoc let tpl = activationEmailUpdate . snd $ forLocale mLocale userTemplates sendMail $ renderActivationMail email name akey acode tpl branding -renderActivationMail :: Email -> Name -> ActivationKey -> ActivationCode -> ActivationEmailTemplate -> TemplateBranding -> Mail +renderActivationMail :: EmailAddress -> Name -> ActivationKey -> ActivationCode -> ActivationEmailTemplate -> TemplateBranding -> Mail renderActivationMail email name akey@(ActivationKey key) acode@(ActivationCode code) ActivationEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -184,7 +184,7 @@ sendTeamActivationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Name -> ActivationKey -> ActivationCode -> @@ -195,7 +195,7 @@ sendTeamActivationMailImpl userTemplates branding email name akey acode mLocale let tpl = teamActivationEmail . snd $ forLocale mLocale userTemplates sendMail $ renderTeamActivationMail email name teamName akey acode tpl branding -renderTeamActivationMail :: Email -> Name -> Text -> ActivationKey -> ActivationCode -> TeamActivationEmailTemplate -> TemplateBranding -> Mail +renderTeamActivationMail :: EmailAddress -> Name -> Text -> ActivationKey -> ActivationCode -> TeamActivationEmailTemplate -> TemplateBranding -> Mail renderTeamActivationMail email name teamName akey@(ActivationKey key) acode@(ActivationCode code) TeamActivationEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -229,7 +229,7 @@ sendVerificationMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> ActivationKey -> ActivationCode -> Maybe Locale -> @@ -238,7 +238,7 @@ sendVerificationMailImpl userTemplates branding email akey acode mLocale = do let tpl = verificationEmail . snd $ forLocale mLocale userTemplates sendMail $ renderVerificationMail email akey acode tpl branding -renderVerificationMail :: Email -> ActivationKey -> ActivationCode -> VerificationEmailTemplate -> TemplateBranding -> Mail +renderVerificationMail :: EmailAddress -> ActivationKey -> ActivationCode -> VerificationEmailTemplate -> TemplateBranding -> Mail renderVerificationMail email akey acode VerificationEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -269,7 +269,7 @@ sendPasswordResetMailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> PasswordResetKey -> PasswordResetCode -> Maybe Locale -> @@ -278,7 +278,7 @@ sendPasswordResetMailImpl userTemplates branding email pkey pcode mLocale = do let tpl = passwordResetEmail . snd $ forLocale mLocale userTemplates sendMail $ renderPwResetMail email pkey pcode tpl branding -renderPwResetMail :: Email -> PasswordResetKey -> PasswordResetCode -> PasswordResetEmailTemplate -> TemplateBranding -> Mail +renderPwResetMail :: EmailAddress -> PasswordResetKey -> PasswordResetCode -> PasswordResetEmailTemplate -> TemplateBranding -> Mail renderPwResetMail email pkey pcode PasswordResetEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -315,7 +315,7 @@ sendNewClientEmailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Name -> Client -> Locale -> @@ -324,7 +324,7 @@ sendNewClientEmailImpl userTemplates branding email name client locale = do let tpl = newClientEmail . snd $ forLocale (Just locale) userTemplates sendMail $ renderNewClientEmail email name locale client tpl branding -renderNewClientEmail :: Email -> Name -> Locale -> Client -> NewClientEmailTemplate -> TemplateBranding -> Mail +renderNewClientEmail :: EmailAddress -> Name -> Locale -> Client -> NewClientEmailTemplate -> TemplateBranding -> Mail renderNewClientEmail email name locale Client {..} NewClientEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -357,7 +357,7 @@ sendAccountDeletionEmailImpl :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> - Email -> + EmailAddress -> Name -> Code.Key -> Code.Value -> @@ -367,7 +367,7 @@ sendAccountDeletionEmailImpl userTemplates branding email name key code locale = let tpl = deletionEmail . snd $ forLocale (Just locale) userTemplates sendMail $ renderDeletionEmail email name key code tpl branding -renderDeletionEmail :: Email -> Name -> Code.Key -> Code.Value -> DeletionEmailTemplate -> TemplateBranding -> Mail +renderDeletionEmail :: EmailAddress -> Name -> Code.Key -> Code.Value -> DeletionEmailTemplate -> TemplateBranding -> Mail renderDeletionEmail email name cKey cValue DeletionEmailTemplate {..} branding = (emptyMail from) { mailTo = [to], @@ -403,7 +403,7 @@ renderDeletionEmail email name cKey cValue DeletionEmailTemplate {..} branding = -- in SMTP, which is a safe limit for most mail servers (including those of -- Amazon SES). The display name is only included if it fits within that -- limit, otherwise it is dropped. -mkMimeAddress :: Name -> Email -> Address +mkMimeAddress :: Name -> EmailAddress -> Address mkMimeAddress name email = let addr = Address (Just (fromName name)) (fromEmail email) in if Text.compareLength (renderAddress addr) 320 == GT diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs index 9c123e1c0e3..818cdca2e9e 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs @@ -126,7 +126,7 @@ data VerificationEmailTemplate = VerificationEmailTemplate verificationEmailSubject :: Template, verificationEmailBodyText :: Template, verificationEmailBodyHtml :: Template, - verificationEmailSender :: Email, + verificationEmailSender :: EmailAddress, verificationEmailSenderName :: Text } @@ -135,7 +135,7 @@ data ActivationEmailTemplate = ActivationEmailTemplate activationEmailSubject :: Template, activationEmailBodyText :: Template, activationEmailBodyHtml :: Template, - activationEmailSender :: Email, + activationEmailSender :: EmailAddress, activationEmailSenderName :: Text } @@ -144,7 +144,7 @@ data TeamActivationEmailTemplate = TeamActivationEmailTemplate teamActivationEmailSubject :: Template, teamActivationEmailBodyText :: Template, teamActivationEmailBodyHtml :: Template, - teamActivationEmailSender :: Email, + teamActivationEmailSender :: EmailAddress, teamActivationEmailSenderName :: Text } @@ -153,7 +153,7 @@ data DeletionEmailTemplate = DeletionEmailTemplate deletionEmailSubject :: Template, deletionEmailBodyText :: Template, deletionEmailBodyHtml :: Template, - deletionEmailSender :: Email, + deletionEmailSender :: EmailAddress, deletionEmailSenderName :: Text } @@ -162,7 +162,7 @@ data PasswordResetEmailTemplate = PasswordResetEmailTemplate passwordResetEmailSubject :: Template, passwordResetEmailBodyText :: Template, passwordResetEmailBodyHtml :: Template, - passwordResetEmailSender :: Email, + passwordResetEmailSender :: EmailAddress, passwordResetEmailSenderName :: Text } @@ -191,7 +191,7 @@ data NewClientEmailTemplate = NewClientEmailTemplate { newClientEmailSubject :: Template, newClientEmailBodyText :: Template, newClientEmailBodyHtml :: Template, - newClientEmailSender :: Email, + newClientEmailSender :: EmailAddress, newClientEmailSenderName :: Text } @@ -199,6 +199,6 @@ data SecondFactorVerificationEmailTemplate = SecondFactorVerificationEmailTempla { sndFactorVerificationEmailSubject :: Template, sndFactorVerificationEmailBodyText :: Template, sndFactorVerificationEmailBodyHtml :: Template, - sndFactorVerificationEmailSender :: Email, + sndFactorVerificationEmailSender :: EmailAddress, sndFactorVerificationEmailSenderName :: Text } diff --git a/libs/wire-subsystems/src/Wire/StoredUser.hs b/libs/wire-subsystems/src/Wire/StoredUser.hs index 2e02a0355f3..38bb072401d 100644 --- a/libs/wire-subsystems/src/Wire/StoredUser.hs +++ b/libs/wire-subsystems/src/Wire/StoredUser.hs @@ -21,7 +21,7 @@ data StoredUser = StoredUser name :: Name, textStatus :: Maybe TextStatus, pict :: Maybe Pict, - email :: Maybe Email, + email :: Maybe EmailAddress, ssoId :: Maybe UserSSOId, accentId :: ColourId, assets :: Maybe [Asset], @@ -115,7 +115,7 @@ toLocale l _ = l toIdentity :: -- | Whether the user is activated Bool -> - Maybe Email -> + Maybe EmailAddress -> Maybe UserSSOId -> Maybe UserIdentity toIdentity True (Just e) Nothing = Just $! EmailIdentity e diff --git a/libs/wire-subsystems/src/Wire/UserKeyStore.hs b/libs/wire-subsystems/src/Wire/UserKeyStore.hs index 5683c25b763..d372d150450 100644 --- a/libs/wire-subsystems/src/Wire/UserKeyStore.hs +++ b/libs/wire-subsystems/src/Wire/UserKeyStore.hs @@ -4,7 +4,8 @@ module Wire.UserKeyStore where import Data.Id import Data.Text qualified as Text -import Imports +import Data.Text.Encoding (decodeUtf8) +import Imports hiding (local) import Polysemy import Test.QuickCheck import Wire.API.User @@ -12,7 +13,7 @@ import Wire.API.User -- | An 'EmailKey' is an 'Email' in a form that serves as a unique lookup key. data EmailKey = EmailKey { emailKeyUniq :: !Text, - emailKeyOrig :: !Email + emailKeyOrig :: !EmailAddress } deriving (Ord) @@ -33,14 +34,16 @@ instance Arbitrary EmailKey where -- e-mail addresses fully case-insensitive. -- * "+" suffixes on the local part are stripped unless the domain -- part is contained in a trusted whitelist. -mkEmailKey :: Email -> EmailKey -mkEmailKey orig@(Email localPart domain) = +mkEmailKey :: EmailAddress -> EmailKey +mkEmailKey orig = let uniq = Text.toLower localPart' <> "@" <> Text.toLower domain in EmailKey uniq orig where + domain = decodeUtf8 . domainPart $ orig + local = decodeUtf8 . localPart $ orig localPart' - | domain `notElem` trusted = Text.takeWhile (/= '+') localPart - | otherwise = localPart + | (domainPart orig) `notElem` trusted = Text.takeWhile (/= '+') local + | otherwise = decodeUtf8 (localPart orig) trusted = ["wearezeta.com", "wire.com", "simulator.amazonses.com"] data UserKeyStore m a where diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 398bb85145c..3a0cab37a6a 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -75,11 +75,11 @@ data UserSubsystem m a where -- | returns the user's locale or the default locale if the users exists LookupLocaleWithDefault :: Local UserId -> UserSubsystem m (Maybe Locale) -- | checks if an email is blocked - IsBlocked :: Email -> UserSubsystem m Bool + IsBlocked :: EmailAddress -> UserSubsystem m Bool -- | removes an email from the block list - BlockListDelete :: Email -> UserSubsystem m () + BlockListDelete :: EmailAddress -> UserSubsystem m () -- | adds an email to the block list - BlockListInsert :: Email -> UserSubsystem m () + BlockListInsert :: EmailAddress -> UserSubsystem m () -- | the return type of 'CheckHandle' data CheckHandleResp diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 2f0f37a87df..39dd0e179af 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -8,7 +8,6 @@ where import Control.Lens (view) import Control.Monad.Trans.Maybe -import Data.Either.Extra import Data.Handle (Handle) import Data.Handle qualified as Handle import Data.Id @@ -105,13 +104,13 @@ interpretUserSubsystem = interpret \case BlockListDelete email -> blockListDeleteImpl email BlockListInsert email -> blockListInsertImpl email -isBlockedImpl :: (Member BlockListStore r) => Email -> Sem r Bool +isBlockedImpl :: (Member BlockListStore r) => EmailAddress -> Sem r Bool isBlockedImpl = BlockList.exists . mkEmailKey -blockListDeleteImpl :: (Member BlockListStore r) => Email -> Sem r () +blockListDeleteImpl :: (Member BlockListStore r) => EmailAddress -> Sem r () blockListDeleteImpl = BlockList.delete . mkEmailKey -blockListInsertImpl :: (Member BlockListStore r) => Email -> Sem r () +blockListInsertImpl :: (Member BlockListStore r) => EmailAddress -> Sem r () blockListInsertImpl = BlockList.insert . mkEmailKey lookupLocaleOrDefaultImpl :: (Member UserStore r, Member (Input UserSubsystemConfig) r) => Local UserId -> Sem r (Maybe Locale) diff --git a/libs/wire-subsystems/src/Wire/VerificationCode.hs b/libs/wire-subsystems/src/Wire/VerificationCode.hs index 1caea31049d..4dd32c5e799 100644 --- a/libs/wire-subsystems/src/Wire/VerificationCode.hs +++ b/libs/wire-subsystems/src/Wire/VerificationCode.hs @@ -59,7 +59,7 @@ data Code = Code -- once, and it cannot actually be "re"-tried after that. codeRetries :: !Retries, codeTTL :: !Timeout, - codeFor :: !Email, + codeFor :: !EmailAddress, codeAccount :: !(Maybe UUID) } deriving (Eq, Show) diff --git a/libs/wire-subsystems/src/Wire/VerificationCodeGen.hs b/libs/wire-subsystems/src/Wire/VerificationCodeGen.hs index 7290a0fbae4..8f9bef9985f 100644 --- a/libs/wire-subsystems/src/Wire/VerificationCodeGen.hs +++ b/libs/wire-subsystems/src/Wire/VerificationCodeGen.hs @@ -39,7 +39,7 @@ data RandomValueType -- different contexts for the same email address. -- TODO: newtype KeyContext = KeyContext ByteString data VerificationCodeGen = VerificationCodeGen - { genFor :: !Email, + { genFor :: !EmailAddress, genKey :: !Key, -- Note [Unique keys] genValueType :: !RandomValueType } @@ -49,17 +49,17 @@ data VerificationCodeGen = VerificationCodeGen -- | Initialise a 'Code' 'VerificationCodeGen'erator for a given natural key. -- This generates a link for emails and a 6-digit code for phone. See also: -- `mk6DigitVerificationCodeGen`. -mkVerificationCodeGen :: Email -> VerificationCodeGen +mkVerificationCodeGen :: EmailAddress -> VerificationCodeGen mkVerificationCodeGen email = VerificationCodeGen email (mkKey email) Random15Bytes -- | Initialise a 'Code' 'VerificationCodeGen'erator for a given natural key. -- This generates a 6-digit code, matter whether it is sent to a phone or to an -- email address. See also: `mkVerificationCodeGen`. -mk6DigitVerificationCodeGen :: Email -> VerificationCodeGen +mk6DigitVerificationCodeGen :: EmailAddress -> VerificationCodeGen mk6DigitVerificationCodeGen email = VerificationCodeGen email (mkKey email) Random6DigitNumber -mkKey :: Email -> Key +mkKey :: EmailAddress -> Key mkKey email = Key . unsafeRange diff --git a/libs/wire-subsystems/src/Wire/VerificationCodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/VerificationCodeStore/Cassandra.hs index e2e013ec62d..72671edec74 100644 --- a/libs/wire-subsystems/src/Wire/VerificationCodeStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/VerificationCodeStore/Cassandra.hs @@ -31,7 +31,7 @@ insertCodeImpl c = do let t = round (codeTTL c) retry x5 (write cql (params LocalQuorum (k, s, v, r, e, a, t))) where - cql :: PrepQuery W (Key, Scope, Value, Retries, Email, Maybe UUID, Int32) () + cql :: PrepQuery W (Key, Scope, Value, Retries, EmailAddress, Maybe UUID, Int32) () cql = "INSERT INTO vcodes (key, scope, value, retries, email, account) \ \VALUES (?, ?, ?, ?, ?, ?) USING TTL ?" @@ -40,12 +40,12 @@ insertCodeImpl c = do lookupCodeImpl :: (MonadClient m) => Key -> Scope -> m (Maybe Code) lookupCodeImpl k s = toCode <$$> retry x1 (query1 cql (params LocalQuorum (k, s))) where - cql :: PrepQuery R (Key, Scope) (Value, Int32, Retries, Email, Maybe UUID) + cql :: PrepQuery R (Key, Scope) (Value, Int32, Retries, EmailAddress, Maybe UUID) cql = "SELECT value, ttl(value), retries, email, account \ \FROM vcodes WHERE key = ? AND scope = ?" - toCode :: (Value, Int32, Retries, Email, Maybe UUID) -> Code + toCode :: (Value, Int32, Retries, EmailAddress, Maybe UUID) -> Code toCode (val, ttl, retries, email, account) = Code { codeKey = k, diff --git a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs index 39dda77c340..8e5924d2e76 100644 --- a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs @@ -6,6 +6,7 @@ import Data.Domain import Data.Id import Data.Misc (PlainTextPassword8) import Data.Qualified +import Data.Text.Encoding (decodeUtf8) import Data.Time import Imports import Polysemy @@ -50,7 +51,7 @@ type AllEffects = State (Map PasswordResetKey (PRQueryData Identity)), TinyLog, EmailSubsystem, - State (Map Email [SentMail]), + State (Map EmailAddress [SentMail]), UserSubsystem ] @@ -126,7 +127,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do . interpretAuthenticationSubsystem $ createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent - in emailDomain email /= "example.com" ==> + in domainPart email /= "example.com" ==> createPasswordResetCodeResult === Right () prop "reset code is generated when email is in allow list" $ @@ -134,7 +135,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do let user = userNoEmail {userIdentity = Just $ EmailIdentity email} localDomain = userNoEmail.userQualifiedId.qDomain createPasswordResetCodeResult = - interpretDependencies localDomain [UserAccount user Active] mempty (Just [emailDomain email]) + interpretDependencies localDomain [UserAccount user Active] mempty (Just [decodeUtf8 $ domainPart email]) . interpretAuthenticationSubsystem $ createPasswordResetCode (mkEmailKey email) in counterexample ("expected Right, got: " <> show createPasswordResetCodeResult) $ @@ -297,7 +298,7 @@ hashAndUpsertPassword :: (Member PasswordStore r, Member HashPassword r) => User hashAndUpsertPassword uid password = upsertHashedPassword uid =<< hashPassword password -expect1ResetPasswordEmail :: (Member (State (Map Email [SentMail])) r) => Email -> Sem r PasswordResetPair +expect1ResetPasswordEmail :: (Member (State (Map EmailAddress [SentMail])) r) => EmailAddress -> Sem r PasswordResetPair expect1ResetPasswordEmail email = getEmailsSentTo email <&> \case @@ -305,7 +306,7 @@ expect1ResetPasswordEmail email = [SentMail _ (PasswordResetMail resetPair)] -> resetPair wrongEmails -> error $ "Wrong emails sent: " <> show wrongEmails -expectNoEmailSent :: (Member (State (Map Email [SentMail])) r) => Sem r () +expectNoEmailSent :: (Member (State (Map EmailAddress [SentMail])) r) => Sem r () expectNoEmailSent = do emails <- get if null emails diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs index 57c9fac0c9e..48d347e3ff2 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs @@ -16,10 +16,10 @@ data SentMail = SentMail data SentMailContent = PasswordResetMail PasswordResetPair deriving (Show, Eq) -emailSubsystemInterpreter :: (Member (State (Map Email [SentMail])) r) => InterpreterFor EmailSubsystem r +emailSubsystemInterpreter :: (Member (State (Map EmailAddress [SentMail])) r) => InterpreterFor EmailSubsystem r emailSubsystemInterpreter = interpret \case SendPasswordResetMail email keyCodePair mLocale -> modify $ Map.insertWith (<>) email [SentMail mLocale $ PasswordResetMail keyCodePair] _ -> error "emailSubsystemInterpreter: implement on demand" -getEmailsSentTo :: (Member (State (Map Email [SentMail])) r) => Email -> Sem r [SentMail] +getEmailsSentTo :: (Member (State (Map EmailAddress [SentMail])) r) => EmailAddress -> Sem r [SentMail] getEmailsSentTo email = gets $ Map.findWithDefault [] email diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 096e740c642..9a98d7b1ae5 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -399,7 +399,7 @@ spec = describe "UserSubsystem.Interpreter" do prop "Updating handles succeeds when UpdateOriginScim" - \(alice, ssoId, email :: Maybe Email, fromHandle -> newHandle, domain, config) -> + \(alice, ssoId, email :: Maybe EmailAddress, fromHandle -> newHandle, domain, config) -> not (isBlacklistedHandle (fromJust (parseHandle newHandle))) ==> let res :: Either UserSubsystemError () = run . runErrorUnsafe @@ -486,7 +486,7 @@ spec = describe "UserSubsystem.Interpreter" do in retrievedUser === Just (mkAccountFromStored localDomain config.defaultLocale storedUser) prop "doesn't get users if they are not indexed by the UserKeyStore" $ - \(config :: UserSubsystemConfig) (localDomain :: Domain) (storedUserNoEmail :: StoredUser) (email :: Email) -> + \(config :: UserSubsystemConfig) (localDomain :: Domain) (storedUserNoEmail :: StoredUser) (email :: EmailAddress) -> let localBackend = def { users = [storedUser], diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 4ce93388c87..3f71d1eff87 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -422,7 +422,6 @@ executable brig-integration , cookie , data-default , data-timeout - , either , email-validate , exceptions , extra diff --git a/services/brig/default.nix b/services/brig/default.nix index b320133901a..20eb64d007b 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -37,7 +37,6 @@ , data-timeout , dns , dns-util -, either , email-validate , enclosed-exceptions , errors @@ -303,7 +302,6 @@ mkDerivation { cookie data-default data-timeout - either email-validate exceptions extended diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index e4511fae9a1..eecc682427b 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -75,7 +75,6 @@ import Data.ByteString (toStrict) import Data.ByteString.Conversion import Data.Code as Code import Data.Domain -import Data.Either.Extra (mapLeft) import Data.Id (ClientId, ConnId, UserId) import Data.List.Split (chunksOf) import Data.Map.Strict qualified as Map diff --git a/services/brig/src/Brig/API/Handler.hs b/services/brig/src/Brig/API/Handler.hs index dcd6eba66a1..078358b4613 100644 --- a/services/brig/src/Brig/API/Handler.hs +++ b/services/brig/src/Brig/API/Handler.hs @@ -54,7 +54,7 @@ import System.Logger.Class (Logger) import Wire.API.Allowlists qualified as Allowlists import Wire.API.Error import Wire.API.Error.Brig -import Wire.API.User (Email) +import Wire.API.User import Wire.Error ------------------------------------------------------------------------------- @@ -119,15 +119,15 @@ brigErrorHandlers logger reqId = -- Utilities -- | If an Allowlist is configured, consult it, otherwise a no-op. {#RefActivationAllowlist} -checkAllowlist :: Email -> Handler r () +checkAllowlist :: EmailAddress -> Handler r () checkAllowlist = wrapHttpClientE . checkAllowlistWithError (StdError allowlistError) -checkAllowlistWithError :: (MonadReader Env m, MonadError e m) => e -> Email -> m () +checkAllowlistWithError :: (MonadReader Env m, MonadError e m) => e -> EmailAddress -> m () checkAllowlistWithError e key = do ok <- isAllowlisted key unless ok (throwError e) -isAllowlisted :: (MonadReader Env m) => Email -> m Bool +isAllowlisted :: (MonadReader Env m) => EmailAddress -> m Bool isAllowlisted key = do env <- view settings pure $ Allowlists.verify (setAllowlistEmailDomains env) key diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 11060b0b395..500510dc9bf 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -536,7 +536,7 @@ changeSelfEmailMaybeSendH u body (fromMaybe False -> validate) = do data MaybeSendEmail = ActuallySendEmail | DoNotSendEmail -changeSelfEmailMaybeSend :: (Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> MaybeSendEmail -> Email -> UpdateOriginType -> (Handler r) ChangeEmailResponse +changeSelfEmailMaybeSend :: (Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> MaybeSendEmail -> EmailAddress -> UpdateOriginType -> (Handler r) ChangeEmailResponse changeSelfEmailMaybeSend u ActuallySendEmail email allowScim = do API.changeSelfEmail u email allowScim changeSelfEmailMaybeSend u DoNotSendEmail email allowScim = do @@ -555,7 +555,7 @@ listActivatedAccountsH :: ) => Maybe (CommaSeparatedList UserId) -> Maybe (CommaSeparatedList Handle) -> - Maybe (CommaSeparatedList Email) -> + Maybe (CommaSeparatedList EmailAddress) -> Maybe Bool -> Handler r [UserAccount] listActivatedAccountsH @@ -607,7 +607,7 @@ listActivatedAccounts elh includePendingInvitations = do (Deleted, _, _) -> pure True (Ephemeral, _, _) -> pure True -getActivationCode :: Email -> Handler r GetActivationCodeResp +getActivationCode :: EmailAddress -> Handler r GetActivationCodeResp getActivationCode email = do apair <- lift . wrapClient $ API.lookupActivationCode email maybe (throwStd activationKeyNotFound) (pure . GetActivationCodeResp) apair @@ -615,14 +615,14 @@ getActivationCode email = do getPasswordResetCodeH :: ( Member AuthenticationSubsystem r ) => - Email -> + EmailAddress -> Handler r GetPasswordResetCodeResp getPasswordResetCodeH email = getPasswordResetCode email getPasswordResetCode :: ( Member AuthenticationSubsystem r ) => - Email -> + EmailAddress -> Handler r GetPasswordResetCodeResp getPasswordResetCode email = (GetPasswordResetCodeResp <$$> lift (API.lookupPasswordResetCode email)) @@ -680,7 +680,7 @@ revokeIdentityH :: ( Member UserSubsystem r, Member UserKeyStore r ) => - Email -> + EmailAddress -> Handler r NoContent revokeIdentityH email = lift $ NoContent <$ API.revokeIdentity email @@ -696,13 +696,13 @@ updateConnectionInternalH updateConn = do API.updateConnectionInternal updateConn !>> connError pure NoContent -checkBlacklist :: (Member BlockListStore r) => Email -> Handler r CheckBlacklistResponse +checkBlacklist :: (Member BlockListStore r) => EmailAddress -> Handler r CheckBlacklistResponse checkBlacklist email = lift $ bool NotBlacklisted YesBlacklisted <$> API.isBlacklisted email -deleteFromBlacklist :: (Member BlockListStore r) => Email -> Handler r NoContent +deleteFromBlacklist :: (Member BlockListStore r) => EmailAddress -> Handler r NoContent deleteFromBlacklist email = lift $ NoContent <$ API.blacklistDelete email -addBlacklist :: (Member BlockListStore r) => Email -> Handler r NoContent +addBlacklist :: (Member BlockListStore r) => EmailAddress -> Handler r NoContent addBlacklist email = lift $ NoContent <$ API.blacklistInsert email updateSSOIdH :: diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index ea39dc9355a..dc58cb86e28 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -740,7 +740,7 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do -- pure $ CreateUserResponse cok userId (Public.SelfProfile usr) pure $ Public.RegisterSuccess cok (Public.SelfProfile usr) where - sendActivationEmail :: (Member EmailSubsystem r) => Public.Email -> Public.Name -> ActivationPair -> Maybe Public.Locale -> Maybe Public.NewTeamUser -> (AppT r) () + sendActivationEmail :: (Member EmailSubsystem r) => Public.EmailAddress -> Public.Name -> ActivationPair -> Maybe Public.Locale -> Maybe Public.NewTeamUser -> (AppT r) () sendActivationEmail email name (key, code) locale mTeamUser | Just teamUser <- mTeamUser, Public.NewTeamCreator creator <- teamUser, @@ -749,7 +749,7 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do | otherwise = liftSem $ sendActivationMail email name key code locale - sendWelcomeEmail :: (Member EmailSending r) => Public.Email -> CreateUserTeam -> Public.NewTeamUser -> Maybe Public.Locale -> (AppT r) () + sendWelcomeEmail :: (Member EmailSending r) => Public.EmailAddress -> CreateUserTeam -> Public.NewTeamUser -> Maybe Public.Locale -> (AppT r) () -- NOTE: Welcome e-mails for the team creator are not dealt by brig anymore sendWelcomeEmail e (CreateUserTeam t n) newUser l = case newUser of Public.NewTeamCreator _ -> @@ -1024,11 +1024,11 @@ sendActivationCode ac = do -- -- The tautological constraint in the type signature is added so that once we remove the -- feature, ghc will guide us here. -customerExtensionCheckBlockedDomains :: Public.Email -> (Handler r) () +customerExtensionCheckBlockedDomains :: Public.EmailAddress -> (Handler r) () customerExtensionCheckBlockedDomains email = do mBlockedDomains <- asks (fmap domainsBlockedForRegistration . setCustomerExtensions . view settings) for_ mBlockedDomains $ \(DomainsBlockedForRegistration blockedDomains) -> do - case mkDomain (Public.emailDomain email) of + case mkDomain (Text.decodeUtf8 $ Public.domainPart email) of Left _ -> pure () -- if it doesn't fit the syntax of blocked domains, it is not blocked Right domain -> @@ -1300,12 +1300,12 @@ sendVerificationCode req = do sendMail email code.codeValue (Just $ Public.userLocale $ accountUser account) action _ -> pure () where - getAccount :: Public.Email -> (Handler r) (Maybe UserAccount) + getAccount :: Public.EmailAddress -> (Handler r) (Maybe UserAccount) getAccount email = lift $ do mbUserId <- liftSem $ lookupKey $ mkEmailKey email join <$> wrapClient (Data.lookupAccount `traverse` mbUserId) - sendMail :: Public.Email -> Code.Value -> Maybe Public.Locale -> Public.VerificationAction -> (Handler r) () + sendMail :: Public.EmailAddress -> Code.Value -> Maybe Public.Locale -> Public.VerificationAction -> (Handler r) () sendMail email value mbLocale = lift . liftSem . \case Public.CreateScimToken -> sendCreateScimTokenVerificationMail email value mbLocale diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 6e6259f3202..028b87d541d 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -73,7 +73,7 @@ data ActivationResult -- | 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, Email) + ChangeEmailNeedsActivation !(User, Activation, EmailAddress) | -- | The user asked to change the email address to the one already owned ChangeEmailIdempotent @@ -85,7 +85,7 @@ data CreateUserError | MissingIdentity | EmailActivationError ActivationError | PhoneActivationError ActivationError - | InvalidEmail Email String + | InvalidEmail EmailAddress String | InvalidPhone Phone | DuplicateUserKey EmailKey | BlacklistedUserKey EmailKey @@ -96,8 +96,8 @@ data CreateUserError data InvitationError = InviteeEmailExists UserId - | InviteInvalidEmail Email - | InviteBlacklistedEmail Email + | InviteInvalidEmail EmailAddress + | InviteBlacklistedEmail EmailAddress data ConnectionError = -- | Max. # of 'Accepted' / 'Sent' connections reached @@ -115,7 +115,7 @@ data ConnectionError | -- | An attempt at creating an invitation to a blacklisted user key. ConnectBlacklistedUserKey EmailKey | -- | An attempt at creating an invitation to an invalid email address. - ConnectInvalidEmail Email String + ConnectInvalidEmail EmailAddress String | -- | An attempt at creating an invitation to an invalid phone nbumber. ConnectInvalidPhone Phone | -- | An attempt at creating a connection with another user from the same binding team. @@ -158,9 +158,9 @@ data VerificationCodeError | VerificationCodeNoEmail data ChangeEmailError - = InvalidNewEmail !Email !String - | EmailExists !Email - | ChangeBlacklistedEmail !Email + = InvalidNewEmail !EmailAddress !String + | EmailExists !EmailAddress + | ChangeBlacklistedEmail !EmailAddress | EmailManagedByScim data SendActivationCodeError diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index d7a7c4159f6..fd9787edcd3 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -302,19 +302,19 @@ createUser new = do Nothing ) Just existingAccount -> - let existingUser = accountUser existingAccount + let existingUser = existingAccount.accountUser mbSSOid = - case (teamInvitation, email, userManagedBy existingUser) of + case (teamInvitation, email, existingUser.userManagedBy) of -- isJust teamInvitation And ManagedByScim implies that the -- user invitation has been generated by SCIM and there is no IdP (Just _, Just em, ManagedByScim) -> Just $ UserScimExternalId (fromEmail em) _ -> newUserSSOId new in ( new - { newUserManagedBy = Just (userManagedBy existingUser), + { newUserManagedBy = Just existingUser.userManagedBy, newUserIdentity = newIdentity email mbSSOid }, - userHandle existingUser + existingUser.userHandle ) -- Create account @@ -372,15 +372,9 @@ createUser new = do where -- NOTE: all functions in the where block don't use any arguments of createUser - fetchAndValidateEmail :: NewUser -> ExceptT RegisterError (AppT r) (Maybe Email) + fetchAndValidateEmail :: NewUser -> ExceptT RegisterError (AppT r) (Maybe EmailAddress) fetchAndValidateEmail newUser = do - -- Validate e-mail - email <- for (newUserEmail newUser) $ \e -> - either - (const $ throwE RegisterErrorInvalidEmail) - pure - (validateEmail e) - + let email = newUserEmail newUser for_ (mkEmailKey <$> email) $ \k -> verifyUniquenessAndCheckBlacklist k !>> identityErrorToRegisterError @@ -460,7 +454,7 @@ createUser new = do pure $ CreateUserTeam tid nm -- Handle e-mail activation (deprecated, see #RefRegistrationNoPreverification in /docs/reference/user/registration.md) - handleEmailActivation :: Maybe Email -> 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 @@ -494,8 +488,7 @@ createUserInviteViaScim :: ) => NewUserScimInvitation -> ExceptT HttpError (AppT r) UserAccount -createUserInviteViaScim (NewUserScimInvitation tid uid loc name rawEmail _) = do - email <- either (const . throwE . StdError $ errorToWai @'E.InvalidEmail) pure (validateEmail rawEmail) +createUserInviteViaScim (NewUserScimInvitation tid uid loc name email _) = do let emKey = mkEmailKey email verifyUniquenessAndCheckBlacklist emKey !>> identityErrorToBrigError account <- lift . wrapClient $ newAccountInviteViaScim uid tid loc name email @@ -533,7 +526,7 @@ checkRestrictedUserCreation new = do -- | 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) => UserId -> Email -> UpdateOriginType -> ExceptT HttpError (AppT r) ChangeEmailResponse +changeSelfEmail :: (Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> EmailAddress -> UpdateOriginType -> ExceptT HttpError (AppT r) ChangeEmailResponse changeSelfEmail u email allowScim = do changeEmail u email allowScim !>> Error.changeEmailError >>= \case ChangeEmailIdempotent -> @@ -553,14 +546,9 @@ changeSelfEmail u email allowScim = do (Just (userLocale usr)) -- | Prepare changing the email (checking a number of invariants). -changeEmail :: (Member BlockListStore r, Member UserKeyStore r) => UserId -> Email -> UpdateOriginType -> ExceptT ChangeEmailError (AppT r) ChangeEmailResult +changeEmail :: (Member BlockListStore r, Member UserKeyStore r) => UserId -> EmailAddress -> UpdateOriginType -> ExceptT ChangeEmailError (AppT r) ChangeEmailResult changeEmail u email updateOrigin = do - em <- - either - (throwE . InvalidNewEmail email) - pure - (validateEmail email) - let ek = mkEmailKey em + let ek = mkEmailKey email blacklisted <- lift . liftSem $ BlockListStore.exists ek when blacklisted $ throwE (ChangeBlacklistedEmail email) @@ -571,13 +559,13 @@ changeEmail u email updateOrigin = do 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 == em -> pure ChangeEmailIdempotent + Just current | current == email -> pure ChangeEmailIdempotent _ -> do unless (userManagedBy usr /= ManagedByScim || updateOrigin == UpdateOriginScim) $ throwE EmailManagedByScim timeout <- setActivationTimeout <$> view settings act <- lift . wrapClient $ Data.newActivation ek timeout (Just u) - pure $ ChangeEmailNeedsActivation (usr, act, em) + pure $ ChangeEmailNeedsActivation (usr, act, email) ------------------------------------------------------------------------------- -- Remove Email @@ -615,7 +603,7 @@ revokeIdentity :: ( Member UserSubsystem r, Member UserKeyStore r ) => - Email -> + EmailAddress -> AppT r () revokeIdentity key = do mu <- liftSem . lookupKey . mkEmailKey $ key @@ -775,15 +763,11 @@ sendActivationCode :: Member GalleyAPIAccess r, Member UserKeyStore r ) => - Email -> + EmailAddress -> Maybe Locale -> ExceptT SendActivationCodeError (AppT r) () sendActivationCode email loc = do - ek <- - either - (const . throwE . InvalidRecipient $ mkEmailKey email) - (pure . mkEmailKey) - (validateEmail email) + let ek = mkEmailKey email doesExist <- lift $ liftSem $ isJust <$> lookupKey ek when doesExist $ throwE $ @@ -834,13 +818,8 @@ sendActivationCode email loc = do mkActivationKey :: (MonadClient m, MonadReader Env m) => ActivationTarget -> ExceptT ActivationError m ActivationKey mkActivationKey (ActivateKey k) = pure k -mkActivationKey (ActivateEmail e) = do - ek <- - either - (throwE . InvalidActivationEmail e) - (pure . mkEmailKey) - (validateEmail e) - liftIO $ Data.mkActivationKey ek +mkActivationKey (ActivateEmail e) = + liftIO $ Data.mkActivationKey (mkEmailKey e) ------------------------------------------------------------------------------- -- Password Management @@ -1071,7 +1050,7 @@ deleteAccount (accountUser -> user) = do lookupActivationCode :: (MonadClient m) => - Email -> + EmailAddress -> m (Maybe ActivationPair) lookupActivationCode email = do let uk = mkEmailKey email @@ -1082,7 +1061,7 @@ lookupActivationCode email = do lookupPasswordResetCode :: ( Member AuthenticationSubsystem r ) => - Email -> + EmailAddress -> (AppT r) (Maybe PasswordResetPair) lookupPasswordResetCode = liftSem . internalLookupPasswordResetCode . mkEmailKey @@ -1144,7 +1123,7 @@ getLegalHoldStatus' user = -- currently pending activation. lookupAccountsByIdentity :: (Member UserKeyStore r) => - Email -> + EmailAddress -> Bool -> AppT r [UserAccount] lookupAccountsByIdentity email includePendingInvitations = do @@ -1156,17 +1135,17 @@ lookupAccountsByIdentity email includePendingInvitations = do then pure result else pure $ filter ((/= PendingInvitation) . accountStatus) result -isBlacklisted :: (Member BlockListStore r) => Email -> AppT r Bool +isBlacklisted :: (Member BlockListStore r) => EmailAddress -> AppT r Bool isBlacklisted email = do let uk = mkEmailKey email liftSem $ BlockListStore.exists uk -blacklistInsert :: (Member BlockListStore r) => Email -> AppT r () +blacklistInsert :: (Member BlockListStore r) => EmailAddress -> AppT r () blacklistInsert email = do let uk = mkEmailKey email liftSem $ BlockListStore.insert uk -blacklistDelete :: (Member BlockListStore r) => Email -> AppT r () +blacklistDelete :: (Member BlockListStore r) => EmailAddress -> AppT r () blacklistDelete email = do let uk = mkEmailKey email liftSem $ BlockListStore.delete uk diff --git a/services/brig/src/Brig/API/Util.hs b/services/brig/src/Brig/API/Util.hs index 77c08763f89..81b1e79d754 100644 --- a/services/brig/src/Brig/API/Util.hs +++ b/services/brig/src/Brig/API/Util.hs @@ -53,7 +53,7 @@ fetchUserIdentity uid = do (throwM $ UserProfileNotFound uid) (pure . userIdentity . selfUser) -logEmail :: Email -> (Msg -> Msg) +logEmail :: EmailAddress -> (Msg -> Msg) logEmail email = Log.field "email_sha256" (sha256String . T.pack . show $ email) diff --git a/services/brig/src/Brig/AWS/SesNotification.hs b/services/brig/src/Brig/AWS/SesNotification.hs index 261a9fd2ddb..d2e803b34ed 100644 --- a/services/brig/src/Brig/AWS/SesNotification.hs +++ b/services/brig/src/Brig/AWS/SesNotification.hs @@ -35,21 +35,21 @@ onEvent (MailBounce BounceTransient es) = onTransientBounce es onEvent (MailBounce BounceUndetermined es) = onUndeterminedBounce es onEvent (MailComplaint es) = onComplaint es -onPermanentBounce :: (Member UserSubsystem r) => [Email] -> AppT r () +onPermanentBounce :: (Member UserSubsystem r) => [EmailAddress] -> AppT r () onPermanentBounce = mapM_ $ \e -> do logEmailEvent "Permanent bounce" e liftSem $ blockListInsert e -onTransientBounce :: [Email] -> AppT r () +onTransientBounce :: [EmailAddress] -> AppT r () onTransientBounce = mapM_ (logEmailEvent "Transient bounce") -onUndeterminedBounce :: [Email] -> AppT r () +onUndeterminedBounce :: [EmailAddress] -> AppT r () onUndeterminedBounce = mapM_ (logEmailEvent "Undetermined bounce") -onComplaint :: (Member UserSubsystem r) => [Email] -> AppT r () +onComplaint :: (Member UserSubsystem r) => [EmailAddress] -> AppT r () onComplaint = mapM_ $ \e -> do logEmailEvent "Complaint" e liftSem $ blockListInsert e -logEmailEvent :: Text -> Email -> AppT r () +logEmailEvent :: Text -> EmailAddress -> AppT r () logEmailEvent t e = Log.info $ field "email" (fromEmail e) ~~ msg t diff --git a/services/brig/src/Brig/AWS/Types.hs b/services/brig/src/Brig/AWS/Types.hs index 53272094dbb..75603a6ccdb 100644 --- a/services/brig/src/Brig/AWS/Types.hs +++ b/services/brig/src/Brig/AWS/Types.hs @@ -30,8 +30,8 @@ import Wire.API.User.Identity -- Notifications data SESNotification - = MailBounce !SESBounceType [Email] - | MailComplaint [Email] + = MailBounce !SESBounceType [EmailAddress] + | MailComplaint [EmailAddress] deriving (Eq, Show) data SESBounceType diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index d5b051a3508..1df8edefd8e 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -149,7 +149,7 @@ import Util.Options import Wire.API.Federation.Error (federationNotImplemented) import Wire.API.Locale (Locale) import Wire.API.Routes.Version -import Wire.API.User.Identity (Email) +import Wire.API.User.Identity import Wire.EmailSending.SMTP qualified as SMTP import Wire.EmailSubsystem.Template (TemplateBranding, forLocale) import Wire.SessionStore @@ -174,7 +174,7 @@ data Env = Env _federator :: Maybe Endpoint, -- FUTUREWORK: should we use a better type here? E.g. to avoid fresh connections all the time? _casClient :: Cas.ClientState, _smtpEnv :: Maybe SMTP.SMTP, - _emailSender :: Email, + _emailSender :: EmailAddress, _awsEnv :: AWS.Env, _applog :: Logger, _internalEvents :: QueueEnv, diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index d665051b8ce..c4f84d77022 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -65,7 +65,7 @@ data ActivationError = UserKeyExists !LT.Text | InvalidActivationCodeWrongUser | InvalidActivationCodeWrongCode - | InvalidActivationEmail !Email !String + | InvalidActivationEmail !EmailAddress !String | InvalidActivationPhone !Phone activationErrorToRegisterError :: ActivationError -> RegisterError @@ -78,7 +78,7 @@ activationErrorToRegisterError = \case data ActivationEvent = AccountActivated !UserAccount - | EmailActivated !UserId !Email + | EmailActivated !UserId !EmailAddress -- | Max. number of activation attempts per 'ActivationKey'. maxAttempts :: Int32 @@ -126,7 +126,7 @@ activateKey k c u = verifyCode k c >>= pickUser >>= activate for_ oldKey $ lift . adhocUserKeyStoreInterpreter . deleteKey pure . Just $ EmailActivated uid (emailKeyOrig key) where - updateEmailAndDeleteEmailUnvalidated :: UserId -> Email -> m () + updateEmailAndDeleteEmailUnvalidated :: UserId -> EmailAddress -> m () updateEmailAndDeleteEmailUnvalidated u' email = updateEmail u' email <* deleteEmailUnvalidated u' claim key uid = do @@ -180,7 +180,7 @@ verifyCode key code = do | otherwise -> revoke >> throwE invalidCode Nothing -> throwE invalidCode where - mkScope "email" k u = case parseEmail k of + mkScope "email" k u = case emailAddressText k of Just e -> pure (mkEmailKey e, u) Nothing -> throwE invalidCode mkScope _ _ _ = throwE invalidCode diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 4bc2d82f506..c128c294863 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -150,7 +150,7 @@ newAccount u inv tid mbHandle = do prots = fromMaybe defSupportedProtocols (newUserSupportedProtocols u) user uid domain l e = User (Qualified uid domain) ident name Nothing pict assets colour False l Nothing mbHandle e tid managedBy prots -newAccountInviteViaScim :: (MonadReader Env m) => UserId -> TeamId -> Maybe Locale -> Name -> Email -> m UserAccount +newAccountInviteViaScim :: (MonadReader Env m) => UserId -> TeamId -> Maybe Locale -> Name -> EmailAddress -> m UserAccount newAccountInviteViaScim uid tid locale name email = do defLoc <- setDefaultUserLocale <$> view settings let loc = fromMaybe defLoc locale @@ -279,10 +279,10 @@ insertAccount (UserAccount u status) mbConv password activated = retry x5 . batc "INSERT INTO service_team (provider, service, user, conv, team) \ \VALUES (?, ?, ?, ?, ?)" -updateEmail :: (MonadClient m) => UserId -> Email -> m () +updateEmail :: (MonadClient m) => UserId -> EmailAddress -> m () updateEmail u e = retry x5 $ write userEmailUpdate (params LocalQuorum (e, u)) -updateEmailUnvalidated :: (MonadClient m) => UserId -> Email -> m () +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 @@ -453,7 +453,7 @@ type UserRow = Name, Maybe TextStatus, Maybe Pict, - Maybe Email, + Maybe EmailAddress, Maybe UserSSOId, ColourId, Maybe [Asset], @@ -476,7 +476,7 @@ type UserRowInsert = Maybe TextStatus, Pict, [Asset], - Maybe Email, + Maybe EmailAddress, Maybe UserSSOId, ColourId, Maybe Password, @@ -537,10 +537,10 @@ userInsert = \country, provider, service, handle, team, managed_by, supported_protocols) \ \VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" -userEmailUpdate :: PrepQuery W (Email, UserId) () +userEmailUpdate :: PrepQuery W (EmailAddress, UserId) () userEmailUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email = ? WHERE id = ?" -userEmailUnvalidatedUpdate :: PrepQuery W (Email, UserId) () +userEmailUnvalidatedUpdate :: PrepQuery W (EmailAddress, UserId) () userEmailUnvalidatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET email_unvalidated = ? WHERE id = ?" userEmailUnvalidatedDelete :: PrepQuery W (Identity UserId) () @@ -558,7 +558,7 @@ userStatusUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE use userDeactivatedUpdate :: PrepQuery W (Identity UserId) () userDeactivatedUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE user SET activated = false WHERE id = ?" -userActivatedUpdate :: PrepQuery W (Maybe Email, UserId) () +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) () @@ -709,7 +709,7 @@ toLocale l _ = l toIdentity :: -- | Whether the user is activated Bool -> - Maybe Email -> + Maybe EmailAddress -> Maybe UserSSOId -> Maybe UserIdentity toIdentity True (Just e) Nothing = Just $! EmailIdentity e diff --git a/services/brig/src/Brig/Effects/JwtTools.hs b/services/brig/src/Brig/Effects/JwtTools.hs index a344f5b7ae4..03b6d6e4f62 100644 --- a/services/brig/src/Brig/Effects/JwtTools.hs +++ b/services/brig/src/Brig/Effects/JwtTools.hs @@ -5,7 +5,6 @@ module Brig.Effects.JwtTools where import Brig.API.Types (CertEnrollmentError (..)) import Control.Monad.Trans.Except import Data.ByteString.Conversion -import Data.Either.Extra import Data.Handle (Handle, fromHandle) import Data.Id import Data.Jwt.Tools qualified as Jwt diff --git a/services/brig/src/Brig/IO/Journal.hs b/services/brig/src/Brig/IO/Journal.hs index 274a784092c..0cc46ef335a 100644 --- a/services/brig/src/Brig/IO/Journal.hs +++ b/services/brig/src/Brig/IO/Journal.hs @@ -50,16 +50,16 @@ import Wire.API.User userActivate :: (MonadReader Env m, MonadIO m) => User -> m () userActivate u@User {..} = journalEvent UserEvent'USER_ACTIVATE (userId u) (userEmail u) (Just userLocale) userTeam (Just userDisplayName) -userUpdate :: (MonadReader Env m, MonadIO m) => UserId -> Maybe Email -> Maybe Locale -> Maybe Name -> m () +userUpdate :: (MonadReader Env m, MonadIO m) => UserId -> Maybe EmailAddress -> Maybe Locale -> Maybe Name -> m () userUpdate uid em loc = journalEvent UserEvent'USER_UPDATE uid em loc Nothing -userEmailRemove :: (MonadReader Env m, MonadIO m) => UserId -> Email -> m () +userEmailRemove :: (MonadReader Env m, MonadIO m) => UserId -> EmailAddress -> m () userEmailRemove uid em = journalEvent UserEvent'USER_EMAIL_REMOVE uid (Just em) Nothing Nothing Nothing userDelete :: (MonadReader Env m, MonadIO m) => UserId -> m () userDelete uid = journalEvent UserEvent'USER_DELETE uid Nothing Nothing Nothing Nothing -journalEvent :: (MonadReader Env m, MonadIO m) => UserEvent'EventType -> UserId -> Maybe Email -> Maybe Locale -> Maybe TeamId -> Maybe Name -> m () +journalEvent :: (MonadReader Env m, MonadIO m) => UserEvent'EventType -> UserId -> Maybe EmailAddress -> Maybe Locale -> Maybe TeamId -> Maybe Name -> m () journalEvent typ uid em loc tid nm = -- this may be the only place that uses awsEnv from brig Env. refactor it to use the -- DeleteQueue effect instead? diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 42f57313d77..f2c53d5d9bc 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -173,7 +173,7 @@ data EmailSMSGeneralOpts = EmailSMSGeneralOpts { -- | Email, SMS, ... template directory templateDir :: !FilePath, -- | Email sender address - emailSender :: !Email, + emailSender :: !EmailAddress, -- | Twilio sender identifier (sender phone number in E.104 format) -- or twilio messaging sender ID - see -- https://www.twilio.com/docs/sms/send-messages#use-an-alphanumeric-sender-id @@ -225,7 +225,7 @@ data ProviderOpts = ProviderOpts -- | Approval URL template approvalUrl :: !Text, -- | Approval email recipient - approvalTo :: !Email, + approvalTo :: !EmailAddress, -- | Password reset URL template providerPwResetUrl :: !Text } diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index aaaad388b6c..bbd0e6f6940 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -116,7 +116,6 @@ import Wire.API.User.Auth import Wire.API.User.Client import Wire.API.User.Client qualified as Public (Client, ClientCapability (ClientSupportsLegalholdImplicitConsent), PubClient (..), UserClientPrekeyMap, UserClients, userClients) import Wire.API.User.Client.Prekey qualified as Public (PrekeyId) -import Wire.API.User.Identity qualified as Public (Email) import Wire.DeleteQueue import Wire.EmailSending (EmailSending) import Wire.Error @@ -187,9 +186,7 @@ internalProviderAPI = Named @"get-provider-activation-code" getActivationCodeH newAccount :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => Public.NewProvider -> (Handler r) Public.NewProviderResponse newAccount new = do guardSecondFactorDisabled Nothing - email <- case validateEmail (Public.newProviderEmail new) of - Right em -> pure em - Left _ -> throwStd (errorToWai @'E.InvalidEmail) + let email = (Public.newProviderEmail new) let name = Public.newProviderName new let pass = Public.newProviderPassword new let descr = fromRange (Public.newProviderDescr new) @@ -240,12 +237,9 @@ activateAccountKey key val = do lift $ sendApprovalConfirmMail name email pure . Just $ Public.ProviderActivationResponse email -getActivationCodeH :: (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => Public.Email -> (Handler r) Code.KeyValuePair -getActivationCodeH e = do +getActivationCodeH :: (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => EmailAddress -> (Handler r) Code.KeyValuePair +getActivationCodeH email = do guardSecondFactorDisabled Nothing - email <- case validateEmail e of - Right em -> pure em - Left _ -> throwStd (errorToWai @'E.InvalidEmail) let gen = mkVerificationCodeGen email code <- lift . liftSem $ internalLookupCode gen.genKey IdentityVerification maybe (throwStd activationKeyNotFound) (pure . codeToKeyValuePair) code @@ -306,11 +300,8 @@ updateAccountProfile pid upd = do (updateProviderDescr upd) updateAccountEmail :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => ProviderId -> Public.EmailUpdate -> (Handler r) () -updateAccountEmail pid (Public.EmailUpdate new) = do +updateAccountEmail pid (Public.EmailUpdate email) = do guardSecondFactorDisabled Nothing - email <- case validateEmail new of - Right em -> pure em - Left _ -> throwStd (errorToWai @'E.InvalidEmail) let emailKey = mkEmailKey email wrapClientE (DB.lookupKey emailKey) >>= mapM_ (const $ throwStd emailExists) let gen = mkVerificationCodeGen email @@ -814,7 +805,7 @@ guardSecondFactorDisabled mbUserId = do minRsaKeySize :: Int minRsaKeySize = 256 -- Bytes (= 2048 bits) -activate :: ProviderId -> Maybe Public.Email -> Public.Email -> (Handler r) () +activate :: ProviderId -> Maybe EmailAddress -> EmailAddress -> (Handler r) () activate pid old new = do let emailKey = mkEmailKey new taken <- maybe False (/= pid) <$> wrapClientE (DB.lookupKey emailKey) diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index 98d237c9565..67a9454ac6b 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -81,10 +81,10 @@ updateAccountProfile p name url descr = retry x5 . batch $ do lookupAccountData :: (MonadClient m) => ProviderId -> - m (Maybe (Name, Maybe Email, HttpsUrl, Text)) + m (Maybe (Name, Maybe EmailAddress, HttpsUrl, Text)) lookupAccountData p = retry x1 $ query1 cql $ params LocalQuorum (Identity p) where - cql :: PrepQuery R (Identity ProviderId) (Name, Maybe Email, HttpsUrl, Text) + cql :: PrepQuery R (Identity ProviderId) (Name, Maybe EmailAddress, HttpsUrl, Text) cql = "SELECT name, email, url, descr FROM provider WHERE id = ?" lookupAccount :: @@ -93,7 +93,7 @@ lookupAccount :: m (Maybe Provider) lookupAccount p = (>>= mk) <$> lookupAccountData p where - mk :: (Name, Maybe Email, HttpsUrl, Text) -> Maybe Provider + mk :: (Name, Maybe EmailAddress, HttpsUrl, Text) -> Maybe Provider mk (_, Nothing, _, _) = Nothing mk (n, Just e, u, d) = Just $! Provider p n e u d @@ -159,7 +159,7 @@ insertKey p old new = retry x5 . batch $ do cqlKeyDelete :: PrepQuery W (Identity Text) () cqlKeyDelete = "DELETE FROM provider_keys WHERE key = ?" - cqlEmail :: PrepQuery W (Email, ProviderId) () + cqlEmail :: PrepQuery W (EmailAddress, ProviderId) () cqlEmail = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE provider SET email = ? WHERE id = ?" lookupKey :: diff --git a/services/brig/src/Brig/Provider/Email.hs b/services/brig/src/Brig/Provider/Email.hs index 2e95cbb0ded..173d3f164fb 100644 --- a/services/brig/src/Brig/Provider/Email.hs +++ b/services/brig/src/Brig/Provider/Email.hs @@ -43,7 +43,7 @@ import Wire.EmailSubsystem.Template (TemplateBranding, renderHtmlWithBranding, r ------------------------------------------------------------------------------- -- Activation Email -sendActivationMail :: (Member EmailSending r) => Name -> Email -> Code.Key -> Code.Value -> Bool -> (AppT r) () +sendActivationMail :: (Member EmailSending r) => Name -> EmailAddress -> Code.Key -> Code.Value -> Bool -> (AppT r) () sendActivationMail name email key code update = do tpl <- selectTemplate update . snd <$> providerTemplates Nothing branding <- view templateBranding @@ -54,7 +54,7 @@ sendActivationMail name email key code update = do selectTemplate False = activationEmail data ActivationEmail = ActivationEmail - { acmTo :: !Email, + { acmTo :: !EmailAddress, acmName :: !Name, acmKey :: !Code.Key, acmCode :: !Code.Value @@ -95,7 +95,7 @@ renderActivationUrl t (Code.Key k) (Code.Value v) branding = -------------------------------------------------------------------------------- -- Approval Confirmation Email -sendApprovalConfirmMail :: (Member EmailSending r) => Name -> Email -> (AppT r) () +sendApprovalConfirmMail :: (Member EmailSending r) => Name -> EmailAddress -> (AppT r) () sendApprovalConfirmMail name email = do tpl <- approvalConfirmEmail . snd <$> providerTemplates Nothing branding <- view templateBranding @@ -103,7 +103,7 @@ sendApprovalConfirmMail name email = do liftSem $ sendMail $ renderApprovalConfirmMail mail tpl branding data ApprovalConfirmEmail = ApprovalConfirmEmail - { apcTo :: !Email, + { apcTo :: !EmailAddress, apcName :: !Name } @@ -131,7 +131,7 @@ renderApprovalConfirmMail ApprovalConfirmEmail {..} ApprovalConfirmEmailTemplate -------------------------------------------------------------------------------- -- Password Reset Email -sendPasswordResetMail :: (Member EmailSending r) => Email -> Code.Key -> Code.Value -> (AppT r) () +sendPasswordResetMail :: (Member EmailSending r) => EmailAddress -> Code.Key -> Code.Value -> (AppT r) () sendPasswordResetMail to key code = do tpl <- passwordResetEmail . snd <$> providerTemplates Nothing branding <- view templateBranding @@ -139,7 +139,7 @@ sendPasswordResetMail to key code = do liftSem $ sendMail $ renderPwResetMail mail tpl branding data PasswordResetEmail = PasswordResetEmail - { pwrTo :: !Email, + { pwrTo :: !EmailAddress, pwrKey :: !Code.Key, pwrCode :: !Code.Value } diff --git a/services/brig/src/Brig/Provider/Template.hs b/services/brig/src/Brig/Provider/Template.hs index 951ff9add7e..5e74e2f4fae 100644 --- a/services/brig/src/Brig/Provider/Template.hs +++ b/services/brig/src/Brig/Provider/Template.hs @@ -51,16 +51,16 @@ data ApprovalRequestEmailTemplate = ApprovalRequestEmailTemplate approvalRequestEmailSubject :: !Template, approvalRequestEmailBodyText :: !Template, approvalRequestEmailBodyHtml :: !Template, - approvalRequestEmailSender :: !Email, + approvalRequestEmailSender :: !EmailAddress, approvalRequestEmailSenderName :: !Text, - approvalRequestEmailTo :: !Email + approvalRequestEmailTo :: !EmailAddress } data ApprovalConfirmEmailTemplate = ApprovalConfirmEmailTemplate { approvalConfirmEmailSubject :: !Template, approvalConfirmEmailBodyText :: !Template, approvalConfirmEmailBodyHtml :: !Template, - approvalConfirmEmailSender :: !Email, + approvalConfirmEmailSender :: !EmailAddress, approvalConfirmEmailSenderName :: !Text, approvalConfirmEmailHomeUrl :: !HttpsUrl } diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 9151b8ebf80..c208ceb34cd 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -75,7 +75,6 @@ import Wire.API.Team.Role import Wire.API.Team.Role qualified as Public import Wire.API.User hiding (fromEmail) import Wire.API.User qualified as Public -import Wire.API.User.Identity qualified as Email import Wire.BlockListStore import Wire.EmailSending (EmailSending) import Wire.Error @@ -118,7 +117,7 @@ getInvitationCode t r = do data CreateInvitationInviter = CreateInvitationInviter { inviterUid :: UserId, - inviterEmail :: Email + inviterEmail :: EmailAddress } deriving (Eq, Show) @@ -217,17 +216,13 @@ createInvitation' :: Maybe UserId -> Public.Role -> Maybe UserId -> - Email -> + EmailAddress -> Public.InvitationRequest -> Handler r (Public.Invitation, Public.InvitationCode) createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do - -- FUTUREWORK: These validations are nearly copy+paste from accountCreation and - -- sendActivationCode. Refactor this to a single place - - -- Validate e-mail - validatedEmail <- either (const $ throwStd (errorToWai @'E.InvalidEmail)) pure (Email.validateEmail (inviteeEmail body)) - let uke = mkEmailKey validatedEmail - blacklistedEm <- lift $ liftSem $ isBlocked validatedEmail + let email = (inviteeEmail body) + let uke = mkEmailKey email + blacklistedEm <- lift $ liftSem $ isBlocked email when blacklistedEm $ throwStd blacklistedEmail emailTaken <- lift $ liftSem $ isJust <$> lookupKey uke @@ -254,10 +249,10 @@ createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do inviteeRole now mbInviterUid - validatedEmail + email body.inviteeName timeout - (newInv, code) <$ sendInvitationMail validatedEmail tid fromEmail code body.locale + (newInv, code) <$ sendInvitationMail email tid fromEmail code body.locale deleteInvitation :: (Member GalleyAPIAccess r) => UserId -> TeamId -> InvitationId -> (Handler r) () deleteInvitation uid tid iid = do @@ -282,7 +277,7 @@ getInvitationByCode c = do inv <- lift . wrapClient $ DB.lookupInvitationByCode HideInvitationUrl c maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) pure inv -headInvitationByEmail :: Email -> (Handler r) Public.HeadInvitationByEmailResult +headInvitationByEmail :: EmailAddress -> (Handler r) Public.HeadInvitationByEmailResult headInvitationByEmail e = do lift $ wrapClient $ @@ -294,7 +289,7 @@ headInvitationByEmail e = do -- | FUTUREWORK: This should also respond with status 409 in case of -- @DB.InvitationByEmailMoreThanOne@. Refactor so that 'headInvitationByEmailH' and -- 'getInvitationByEmailH' are almost the same thing. -getInvitationByEmail :: Email -> (Handler r) Public.Invitation +getInvitationByEmail :: EmailAddress -> (Handler r) Public.Invitation getInvitationByEmail email = do inv <- lift $ wrapClient $ DB.lookupInvitationByEmail HideInvitationUrl email maybe (throwStd (notFound "Invitation not found")) pure inv diff --git a/services/brig/src/Brig/Team/DB.hs b/services/brig/src/Brig/Team/DB.hs index ed5898c59a5..e6e19e7609d 100644 --- a/services/brig/src/Brig/Team/DB.hs +++ b/services/brig/src/Brig/Team/DB.hs @@ -93,7 +93,7 @@ insertInvitation :: Role -> UTCTime -> Maybe UserId -> - Email -> + EmailAddress -> Maybe Name -> -- | The timeout for the invitation code. Timeout -> @@ -112,10 +112,10 @@ insertInvitation showUrl iid t role (toUTCTimeMillis -> now) minviter email invi where cqlInvitationInfo :: PrepQuery W (InvitationCode, TeamId, InvitationId, Int32) () cqlInvitationInfo = "INSERT INTO team_invitation_info (code, team, id) VALUES (?, ?, ?) USING TTL ?" - cqlInvitation :: PrepQuery W (TeamId, Role, InvitationId, InvitationCode, Email, UTCTimeMillis, Maybe UserId, Maybe Name, Int32) () + cqlInvitation :: PrepQuery W (TeamId, Role, InvitationId, InvitationCode, EmailAddress, UTCTimeMillis, Maybe UserId, Maybe Name, Int32) () cqlInvitation = "INSERT INTO team_invitation (team, role, id, code, email, created_at, created_by, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?) USING TTL ?" -- Note: the edge case of multiple invites to the same team by different admins from the same team results in last-invite-wins in the team_invitation_email table. - cqlInvitationByEmail :: PrepQuery W (Email, TeamId, InvitationId, InvitationCode, Int32) () + cqlInvitationByEmail :: PrepQuery W (EmailAddress, TeamId, InvitationId, InvitationCode, Int32) () cqlInvitationByEmail = "INSERT INTO team_invitation_email (email, team, invitation, code) VALUES (?, ?, ?, ?) USING TTL ?" lookupInvitation :: @@ -131,7 +131,7 @@ lookupInvitation showUrl t r = do inv <- retry x1 (query1 cqlInvitation (params LocalQuorum (t, r))) traverse (toInvitation showUrl) inv where - cqlInvitation :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, InvitationCode) + cqlInvitation :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode) cqlInvitation = "SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id = ?" lookupInvitationByCode :: @@ -155,10 +155,10 @@ lookupInvitationCode t r = cqlInvitationCode :: PrepQuery R (TeamId, InvitationId) (Identity InvitationCode) cqlInvitationCode = "SELECT code FROM team_invitation WHERE team = ? AND id = ?" -lookupInvitationCodeEmail :: (MonadClient m) => TeamId -> InvitationId -> m (Maybe (InvitationCode, Email)) +lookupInvitationCodeEmail :: (MonadClient m) => TeamId -> InvitationId -> m (Maybe (InvitationCode, EmailAddress)) lookupInvitationCodeEmail t r = retry x1 (query1 cqlInvitationCodeEmail (params LocalQuorum (t, r))) where - cqlInvitationCodeEmail :: PrepQuery R (TeamId, InvitationId) (InvitationCode, Email) + cqlInvitationCodeEmail :: PrepQuery R (TeamId, InvitationId) (InvitationCode, EmailAddress) cqlInvitationCodeEmail = "SELECT code, email FROM team_invitation WHERE team = ? AND id = ?" lookupInvitations :: @@ -184,9 +184,9 @@ lookupInvitations showUrl team start (fromRange -> size) = do { result = invs, hasMore = more } - cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, InvitationCode) + cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode) cqlSelect = "SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? ORDER BY id ASC" - cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, InvitationCode) + cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode) cqlSelectFrom = "SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id > ? ORDER BY id ASC" deleteInvitation :: (MonadClient m) => TeamId -> InvitationId -> m () @@ -206,7 +206,7 @@ deleteInvitation t i = do cqlInvitation = "DELETE FROM team_invitation where team = ? AND id = ?" cqlInvitationInfo :: PrepQuery W (Identity InvitationCode) () cqlInvitationInfo = "DELETE FROM team_invitation_info WHERE code = ?" - cqlInvitationEmail :: PrepQuery W (Email, TeamId) () + cqlInvitationEmail :: PrepQuery W (EmailAddress, TeamId) () cqlInvitationEmail = "DELETE FROM team_invitation_email WHERE email = ? AND team = ?" deleteInvitations :: (MonadClient m) => TeamId -> m () @@ -236,14 +236,14 @@ lookupInvitationByEmail :: MonadClient m ) => ShowOrHideInvitationUrl -> - Email -> + EmailAddress -> m (Maybe Invitation) lookupInvitationByEmail showUrl e = lookupInvitationInfoByEmail e >>= \case InvitationByEmail InvitationInfo {..} -> lookupInvitation showUrl iiTeam iiInvId _ -> pure Nothing -lookupInvitationInfoByEmail :: (Log.MonadLogger m, MonadClient m) => Email -> m InvitationByEmail +lookupInvitationInfoByEmail :: (Log.MonadLogger m, MonadClient m) => EmailAddress -> m InvitationByEmail lookupInvitationInfoByEmail email = do res <- retry x1 (query cqlInvitationEmail (params LocalQuorum (Identity email))) case res of @@ -258,7 +258,7 @@ lookupInvitationInfoByEmail email = do Log.~~ Log.field "email" (show email) pure InvitationByEmailMoreThanOne where - cqlInvitationEmail :: PrepQuery R (Identity Email) (TeamId, InvitationId, InvitationCode) + cqlInvitationEmail :: PrepQuery R (Identity EmailAddress) (TeamId, InvitationId, InvitationCode) cqlInvitationEmail = "SELECT team, invitation, code FROM team_invitation_email WHERE email = ?" countInvitations :: (MonadClient m) => TeamId -> m Int64 @@ -281,7 +281,7 @@ toInvitation :: InvitationId, UTCTimeMillis, Maybe UserId, - Email, + EmailAddress, Maybe Name, InvitationCode ) -> diff --git a/services/brig/src/Brig/Team/Email.hs b/services/brig/src/Brig/Team/Email.hs index f13582fd6f4..d76a6671f68 100644 --- a/services/brig/src/Brig/Team/Email.hs +++ b/services/brig/src/Brig/Team/Email.hs @@ -42,14 +42,14 @@ import Wire.EmailSubsystem.Template (TemplateBranding, renderHtmlWithBranding, r ------------------------------------------------------------------------------- -- Invitation Email -sendInvitationMail :: (Member EmailSending r) => Email -> TeamId -> Email -> InvitationCode -> Maybe Locale -> (AppT r) () +sendInvitationMail :: (Member EmailSending r) => EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> (AppT r) () sendInvitationMail to tid from code loc = do tpl <- invitationEmail . snd <$> teamTemplates loc branding <- view templateBranding let mail = InvitationEmail to tid code from liftSem $ sendMail $ renderInvitationEmail mail tpl branding -sendMemberWelcomeMail :: (Member EmailSending r) => Email -> TeamId -> Text -> Maybe Locale -> (AppT r) () +sendMemberWelcomeMail :: (Member EmailSending r) => EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () sendMemberWelcomeMail to tid teamName loc = do tpl <- memberWelcomeEmail . snd <$> teamTemplates loc branding <- view templateBranding @@ -60,10 +60,10 @@ sendMemberWelcomeMail to tid teamName loc = do -- Invitation Email data InvitationEmail = InvitationEmail - { invTo :: !Email, + { invTo :: !EmailAddress, invTeamId :: !TeamId, invInvCode :: !InvitationCode, - invInviter :: !Email + invInviter :: !EmailAddress } renderInvitationEmail :: InvitationEmail -> InvitationEmailTemplate -> TemplateBranding -> Mail @@ -100,7 +100,7 @@ renderInvitationUrl t tid (InvitationCode c) branding = -- Creator Welcome Email data CreatorWelcomeEmail = CreatorWelcomeEmail - { cwTo :: !Email, + { cwTo :: !EmailAddress, cwTid :: !TeamId, cwTeamName :: !Text } @@ -109,7 +109,7 @@ data CreatorWelcomeEmail = CreatorWelcomeEmail -- Member Welcome Email data MemberWelcomeEmail = MemberWelcomeEmail - { mwTo :: !Email, + { mwTo :: !EmailAddress, mwTid :: !TeamId, mwTeamName :: !Text } diff --git a/services/brig/src/Brig/Team/Template.hs b/services/brig/src/Brig/Team/Template.hs index 32f6f803ad4..d725ec556f4 100644 --- a/services/brig/src/Brig/Team/Template.hs +++ b/services/brig/src/Brig/Team/Template.hs @@ -37,7 +37,7 @@ data InvitationEmailTemplate = InvitationEmailTemplate invitationEmailSubject :: !Template, invitationEmailBodyText :: !Template, invitationEmailBodyHtml :: !Template, - invitationEmailSender :: !Email, + invitationEmailSender :: !EmailAddress, invitationEmailSenderName :: !Text } @@ -46,7 +46,7 @@ data CreatorWelcomeEmailTemplate = CreatorWelcomeEmailTemplate creatorWelcomeEmailSubject :: !Template, creatorWelcomeEmailBodyText :: !Template, creatorWelcomeEmailBodyHtml :: !Template, - creatorWelcomeEmailSender :: !Email, + creatorWelcomeEmailSender :: !EmailAddress, creatorWelcomeEmailSenderName :: !Text } @@ -55,7 +55,7 @@ data MemberWelcomeEmailTemplate = MemberWelcomeEmailTemplate memberWelcomeEmailSubject :: !Template, memberWelcomeEmailBodyText :: !Template, memberWelcomeEmailBodyHtml :: !Template, - memberWelcomeEmailSender :: !Email, + memberWelcomeEmailSender :: !EmailAddress, memberWelcomeEmailSenderName :: !Text } diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index e79955c528f..b17ff8e6689 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -149,7 +149,7 @@ verifyCode mbCode action uid = do where getEmailAndTeamId :: UserId -> - ExceptT e (AppT r) (Maybe Email, Maybe TeamId) + ExceptT e (AppT r) (Maybe EmailAddress, Maybe TeamId) getEmailAndTeamId u = do mbAccount <- wrapHttpClientE $ Data.lookupAccount u pure (userEmail <$> accountUser =<< mbAccount, userTeam <$> accountUser =<< mbAccount) @@ -295,13 +295,8 @@ resolveLoginId li = do Just uid -> pure uid validateLoginId :: (MonadReader Env m) => LoginId -> ExceptT LoginError m (Either EmailKey Handle) -validateLoginId (LoginByEmail email) = - either - (const $ throwE LoginFailed) - (pure . Left . mkEmailKey) - (validateEmail email) -validateLoginId (LoginByHandle h) = - pure (Right h) +validateLoginId (LoginByEmail email) = (pure . Left . mkEmailKey) email +validateLoginId (LoginByHandle h) = (pure . Right) h isPendingActivation :: (MonadClient m, MonadReader Env m) => LoginId -> m Bool isPendingActivation ident = case ident of diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 7f687369486..24d8ec75016 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -790,8 +790,8 @@ type ReindexRow = Maybe (Writetime AccountStatus), Maybe Handle, Maybe (Writetime Handle), - Maybe Email, - Maybe (Writetime Email), + Maybe EmailAddress, + Maybe (Writetime EmailAddress), ColourId, Writetime ColourId, Activated, @@ -802,8 +802,8 @@ type ReindexRow = Maybe (Writetime ManagedBy), Maybe UserSSOId, Maybe (Writetime UserSSOId), - Maybe Email, - Maybe (Writetime Email) + Maybe EmailAddress, + Maybe (Writetime EmailAddress) ) -- the _2 lens does not work for a tuple this big diff --git a/services/brig/src/Brig/User/Search/Index/Types.hs b/services/brig/src/Brig/User/Search/Index/Types.hs index 766c5b5df90..2630842be4d 100644 --- a/services/brig/src/Brig/User/Search/Index/Types.hs +++ b/services/brig/src/Brig/User/Search/Index/Types.hs @@ -54,7 +54,7 @@ data IndexUser = IndexUser _iuTeam :: Maybe TeamId, _iuName :: Maybe Name, _iuHandle :: Maybe Handle, - _iuEmail :: Maybe Email, + _iuEmail :: Maybe EmailAddress, _iuColourId :: Maybe ColourId, _iuAccountStatus :: Maybe AccountStatus, _iuSAMLIdP :: Maybe Text, @@ -64,7 +64,7 @@ data IndexUser = IndexUser _iuSearchVisibilityInbound :: Maybe SearchVisibilityInbound, _iuScimExternalId :: Maybe Text, _iuSso :: Maybe Sso, - _iuEmailUnvalidated :: Maybe Email + _iuEmailUnvalidated :: Maybe EmailAddress } data IndexQuery r = IndexQuery Query Filter [DefaultSort] @@ -91,7 +91,7 @@ data UserDoc = UserDoc udName :: Maybe Name, udNormalized :: Maybe Text, udHandle :: Maybe Handle, - udEmail :: Maybe Email, + udEmail :: Maybe EmailAddress, udColourId :: Maybe ColourId, udAccountStatus :: Maybe AccountStatus, udSAMLIdP :: Maybe Text, @@ -101,7 +101,7 @@ data UserDoc = UserDoc udSearchVisibilityInbound :: Maybe SearchVisibilityInbound, udScimExternalId :: Maybe Text, udSso :: Maybe Sso, - udEmailUnvalidated :: Maybe Email + udEmailUnvalidated :: Maybe EmailAddress } deriving (Eq, Show) diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index f803d6a988a..275fee81150 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -263,7 +263,7 @@ testPasswordResetProvider db brig = do loginProvider brig email newPw !!! const 200 === statusCode where - resetPw :: PlainTextPassword6 -> Email -> Http ResponseLBS + resetPw :: PlainTextPassword6 -> EmailAddress -> Http ResponseLBS resetPw newPw email = do -- Get the code directly from the DB let gen = mkVerificationCodeGen email @@ -1109,7 +1109,7 @@ registerProvider brig new = getProviderActivationCodeInternal :: Brig -> - Email -> + EmailAddress -> Http ResponseLBS getProviderActivationCodeInternal brig email = get $ @@ -1131,7 +1131,7 @@ activateProvider brig key val = loginProvider :: Brig -> - Email -> + EmailAddress -> PlainTextPassword6 -> Http ResponseLBS loginProvider brig email pw = @@ -1836,7 +1836,7 @@ defNewService config = liftIO $ do newServiceTags = defServiceTags } -defNewProvider :: Email -> NewProvider +defNewProvider :: EmailAddress -> NewProvider defNewProvider email = NewProvider { newProviderEmail = email, diff --git a/services/brig/test/integration/API/Settings.hs b/services/brig/test/integration/API/Settings.hs index 1d350b08868..c6d2278fb99 100644 --- a/services/brig/test/integration/API/Settings.hs +++ b/services/brig/test/integration/API/Settings.hs @@ -107,7 +107,7 @@ testUsersEmailVisibleIffExpected opts brig galley viewingUserIs visibilitySettin let uids = C8.intercalate "," $ toByteString' <$> [userId userA, userId userB, userId nonTeamUser] - expected :: Set (Maybe UserId, Maybe Email) + expected :: Set (Maybe UserId, Maybe EmailAddress) expected = Set.fromList [ ( Just $ userId userA, @@ -137,7 +137,7 @@ testUsersEmailVisibleIffExpected opts brig galley viewingUserIs visibilitySettin testGetUserEmailShowsEmailsIffExpected :: Opts -> Brig -> Galley -> ViewingUserIs -> EmailVisibilityConfig -> Http () testGetUserEmailShowsEmailsIffExpected opts brig galley viewingUserIs visibilitySetting = do (viewerId, userA, userB, nonTeamUser) <- setup brig galley viewingUserIs - let expectations :: [(UserId, Maybe Email)] + let expectations :: [(UserId, Maybe EmailAddress)] expectations = [ ( userId userA, if expectEmailVisible visibilitySetting viewingUserIs SameTeam @@ -162,7 +162,7 @@ testGetUserEmailShowsEmailsIffExpected opts brig galley viewingUserIs visibility const 200 === statusCode const expectedEmail === emailResult where - emailResult :: Response (Maybe LByteString) -> Maybe Email + emailResult :: Response (Maybe LByteString) -> Maybe EmailAddress emailResult r = responseJsonMaybe r >>= jsonField "email" setup :: Brig -> Galley -> ViewingUserIs -> Http (UserId, User, User, User) diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index a52905354b6..769efcd6a00 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -43,7 +43,7 @@ import Data.LegalHold (UserLegalHoldStatus (UserLegalHoldDisabled)) import Data.String.Conversions (cs) import Data.Text qualified as Text import Data.Text.Ascii qualified as Ascii -import Data.Text.Encoding (encodeUtf8) +import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Time (addUTCTime, getCurrentTime) import Data.UUID qualified as UUID (fromString) import Data.UUID.V4 qualified as UUID @@ -357,7 +357,7 @@ testInvitationEmailLookupNginz brig nginz = do -- expect an invitation to be found querying with email after invite headInvitationByEmail nginz email 200 -headInvitationByEmail :: (Request -> Request) -> Email -> Int -> Http () +headInvitationByEmail :: (Request -> Request) -> EmailAddress -> Int -> Http () headInvitationByEmail service email expectedCode = Bilge.head (service . path "/teams/invitations/by-email" . contentJson . queryItem "email" (toByteString' email)) !!! const expectedCode === statusCode @@ -377,7 +377,7 @@ testInvitationTooManyPending opts brig (TeamSizeLimit limit) = do const 403 === statusCode const (Just "too-many-team-invitations") === fmap Error.label . responseJsonMaybe -registerInvite :: Brig -> TeamId -> Invitation -> Email -> Http UserId +registerInvite :: Brig -> TeamId -> Invitation -> EmailAddress -> Http UserId registerInvite brig tid inv invemail = do Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) rsp <- @@ -437,9 +437,9 @@ testInvitationEmailAccepted brig galley = do -- remove it). testInvitationEmailAcceptedInBlockedDomain :: Opt.Opts -> Brig -> Galley -> Http () testInvitationEmailAcceptedInBlockedDomain opts brig galley = do - email :: Email <- randomEmail + email :: EmailAddress <- randomEmail let invite = stdInvitationRequest email - replacementBrigApp = withDomainsBlockedForRegistration opts [emailDomain email] + replacementBrigApp = withDomainsBlockedForRegistration opts [decodeUtf8 $ domainPart email] void $ createAndVerifyInvitation' (Just replacementBrigApp) (accept invite.inviteeEmail) invite brig galley -- | FUTUREWORK: this is an alternative helper to 'createPopulatedBindingTeam'. it has been @@ -665,7 +665,7 @@ testInvitationMutuallyExclusive brig = do req email (Just code) (Just newTeam) (Just code) !!! const 400 === statusCode where req :: - Email -> + EmailAddress -> Maybe InvitationCode -> Maybe BindingNewTeam -> Maybe InvitationCode -> diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 9a862fba44a..097616cdb57 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -276,10 +276,10 @@ putLHWhitelistTeam galley tid = do . paths ["i", "legalhold", "whitelisted-teams", toByteString' tid] ) -accept :: Email -> InvitationCode -> RequestBody +accept :: EmailAddress -> InvitationCode -> RequestBody accept = acceptWithName (Name "Bob") -acceptWithName :: Name -> Email -> InvitationCode -> RequestBody +acceptWithName :: Name -> EmailAddress -> InvitationCode -> RequestBody acceptWithName name email code = RequestBodyLBS . encode $ object @@ -289,7 +289,7 @@ acceptWithName name email code = "team_code" .= code ] -extAccept :: Email -> Name -> Phone -> ActivationCode -> InvitationCode -> RequestBody +extAccept :: EmailAddress -> Name -> Phone -> ActivationCode -> InvitationCode -> RequestBody extAccept email name phone phoneCode code = RequestBodyLBS . encode $ object @@ -302,7 +302,7 @@ extAccept email name phone phoneCode code = "team_code" .= code ] -register :: Email -> BindingNewTeam -> Brig -> Http (Response (Maybe LByteString)) +register :: EmailAddress -> BindingNewTeam -> Brig -> Http (Response (Maybe LByteString)) register e t brig = post ( brig @@ -319,7 +319,7 @@ register e t brig = ) ) -register' :: Email -> BindingNewTeam -> ActivationCode -> Brig -> Http (Response (Maybe LByteString)) +register' :: EmailAddress -> BindingNewTeam -> ActivationCode -> Brig -> Http (Response (Maybe LByteString)) register' e t c brig = post ( brig @@ -423,10 +423,10 @@ isActivatedUser uid brig = do Just (_ : _) -> True _ -> False -stdInvitationRequest :: Email -> InvitationRequest +stdInvitationRequest :: EmailAddress -> InvitationRequest stdInvitationRequest = stdInvitationRequest' Nothing Nothing -stdInvitationRequest' :: Maybe Locale -> Maybe Role -> Email -> InvitationRequest +stdInvitationRequest' :: Maybe Locale -> Maybe Role -> EmailAddress -> InvitationRequest stdInvitationRequest' loc role email = InvitationRequest loc role Nothing email @@ -463,7 +463,7 @@ setTeamSearchVisibilityInboundAvailable galley tid status = !!! do const 200 === statusCode -setUserEmail :: Brig -> UserId -> UserId -> Email -> Http ResponseLBS +setUserEmail :: Brig -> UserId -> UserId -> EmailAddress -> Http ResponseLBS setUserEmail brig from uid email = do put ( brig diff --git a/services/brig/test/integration/API/TeamUserSearch.hs b/services/brig/test/integration/API/TeamUserSearch.hs index f873b2f65d9..0301b8eaa4e 100644 --- a/services/brig/test/integration/API/TeamUserSearch.hs +++ b/services/brig/test/integration/API/TeamUserSearch.hs @@ -35,7 +35,7 @@ import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, assertEqual, (@?=)) import Util (Brig, Galley, randomEmail, test, withSettingsOverrides) import Wire.API.User (User (..), userEmail, userId) -import Wire.API.User.Identity +import Wire.API.User.Identity hiding (toByteString) import Wire.API.User.Search type TestConstraints m = (MonadFail m, MonadCatch m, MonadIO m, MonadHttp m) diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 3c378e4f4ae..b3e05306608 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -44,7 +44,6 @@ import Data.ByteString qualified as C8 import Data.ByteString.Char8 (pack) import Data.ByteString.Conversion import Data.Domain -import Data.Either.Combinators import Data.Handle import Data.Id import Data.Json.Util (fromUTCTimeMillis) @@ -201,13 +200,13 @@ testUpdateUserEmailByTeamOwner opts brig = do checkUnauthorizedRequests emailOwner otherTeamMember teamOwnerDifferentTeam newEmail checkActivationCode newEmail False where - checkLetActivationExpire :: Email -> Http () + checkLetActivationExpire :: EmailAddress -> Http () checkLetActivationExpire email = do let timeout = round (Opt.setActivationTimeout (Opt.optSettings opts)) threadDelay ((timeout + 1) * 1000_000) checkActivationCode email False - checkActivationCode :: Email -> Bool -> Http () + checkActivationCode :: EmailAddress -> Bool -> Http () checkActivationCode email shouldExist = do maybeActivationCode <- Util.getActivationCode brig (Left email) void $ @@ -216,11 +215,11 @@ testUpdateUserEmailByTeamOwner opts brig = do then assertBool "activation code should exists" (isJust maybeActivationCode) else assertBool "activation code should not exists" (isNothing maybeActivationCode) - checkSetUserEmail :: User -> User -> Email -> Int -> Http () + checkSetUserEmail :: User -> User -> EmailAddress -> Int -> Http () checkSetUserEmail teamOwner emailOwner email expectedStatusCode = setUserEmail brig (userId teamOwner) (userId emailOwner) email !!! (const expectedStatusCode === statusCode) - checkUnauthorizedRequests :: User -> User -> User -> Email -> Http () + checkUnauthorizedRequests :: User -> User -> User -> EmailAddress -> Http () checkUnauthorizedRequests emailOwner otherTeamMember teamOwnerDifferentTeam email = do setUserEmail brig (userId teamOwnerDifferentTeam) (userId emailOwner) email !!! (const 404 === statusCode) setUserEmail brig (userId otherTeamMember) (userId emailOwner) email !!! (const 403 === statusCode) @@ -397,7 +396,9 @@ testCreateUserConflict _ brig = do const (Just "key-exists") === fmap Error.label . responseJsonMaybe -- untrusted email domains u2 <- createUserUntrustedEmail "conflict" brig - let Just (Email loc dom) = userEmail u2 + let Just email = userEmail u2 + dom = T.decodeUtf8 $ domainPart email + loc = T.decodeUtf8 $ localPart email let p2 = RequestBodyLBS . encode $ object @@ -414,30 +415,16 @@ testCreateUserConflict _ brig = do -- The testCreateUserInvalidEmail test conforms to the following testing standards: -- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- --- Test to make sure a new user cannot be created with an invalid email address or invalid phone number. +-- Test to make sure a new user cannot be created with an invalid email address testCreateUserInvalidEmail :: Opt.Opts -> Brig -> Http () testCreateUserInvalidEmail (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () testCreateUserInvalidEmail _ brig = do - email <- randomEmail - let reqEmail = - RequestBodyLBS . encode $ - object - [ "name" .= ("foo" :: Text), - "email" .= fromEmail email, - "password" .= defPassword, - "phone" .= ("123456" :: Text) -- invalid phone number, but ignored - ] - post (brig . path "/register" . contentJson . body reqEmail) - !!! const 201 === statusCode - - phone <- randomPhone let reqPhone = RequestBodyLBS . encode $ object [ "name" .= ("foo" :: Text), - "email" .= ("invalid@email" :: Text), -- invalid since there's only a single label - "password" .= defPassword, - "phone" .= fromPhone phone + "email" .= ("invalid@" :: Text), + "password" .= defPassword ] post (brig . path "/register" . contentJson . body reqPhone) !!! const 400 === statusCode @@ -469,18 +456,18 @@ testCreateUserBlacklist _ brig aws = "password" .= defPassword ] -- If there is no queue available, we need to force it either by publishing an event or using the API - forceBlacklist :: Text -> Email -> Http () + forceBlacklist :: Text -> EmailAddress -> Http () forceBlacklist typ em = case aws ^. AWS.sesQueue of Just queue -> publishMessage typ em queue Nothing -> Bilge.post (brig . path "i/users/blacklist" . queryItem "email" (toByteString' em)) !!! const 200 === statusCode - publishMessage :: Text -> Email -> Text -> Http () + publishMessage :: Text -> EmailAddress -> Text -> Http () publishMessage typ em queue = do let bdy = encode $ case typ of "bounce" -> MailBounce BouncePermanent [em] "complaint" -> MailComplaint [em] x -> error ("Unsupported message type: " ++ show x) void . AWS.execute aws $ AWS.enqueueStandard queue bdy - awaitBlacklist :: Int -> Email -> Http () + awaitBlacklist :: Int -> EmailAddress -> Http () awaitBlacklist n e = do r <- Bilge.head (brig . path "i/users/blacklist" . queryItem "email" (toByteString' e)) when (statusCode r == 404 && n > 0) $ do @@ -668,7 +655,7 @@ testMultipleUsersUnqualified brig = do -- on this endpoint, only from the self profile (/self). expected = Set.fromList - [ (Just $ userDisplayName u1, Nothing :: Maybe Email), + [ (Just $ userDisplayName u1, Nothing :: Maybe EmailAddress), (Just $ userDisplayName u2, Nothing), (Just $ userDisplayName u3, Nothing) ] @@ -700,7 +687,7 @@ testMultipleUsersV3 brig = do q = ListUsersByIds (map userQualifiedId users) expected = Set.fromList - [ (Just $ userDisplayName u1, Nothing :: Maybe Email), + [ (Just $ userDisplayName u1, Nothing :: Maybe EmailAddress), (Just $ userDisplayName u2, Nothing), (Just $ userDisplayName u3, Nothing) ] @@ -754,7 +741,7 @@ testMultipleUsers opts brig = do q = ListUsersByIds $ u5 : u4 : map userQualifiedId users expected = Set.fromList - [ (Just $ userDisplayName u1, Nothing :: Maybe Email), + [ (Just $ userDisplayName u1, Nothing :: Maybe EmailAddress), (Just $ userDisplayName u2, Nothing), (Just $ userDisplayName u3, Nothing), (Just $ profileName u5Profile, profileEmail u5Profile) @@ -910,7 +897,7 @@ testEmailUpdate brig userJournalWatcher = do -- ensure no other user has "test+@example.com" -- if there is such a user, let's delete it first. otherwise -- this test fails since there can be only one user with "test+...@example.com" - ensureNoOtherUserWithEmail (Email "test" "example.com") + ensureNoOtherUserWithEmail (unsafeEmailAddress "test" "example.com") -- we want to use a non-trusted domain in order to verify profile changes flip initiateUpdateAndActivate uid =<< mkEmailRandomLocalSuffix "test@example.com" flip initiateUpdateAndActivate uid =<< mkEmailRandomLocalSuffix "test@example.com" @@ -921,7 +908,7 @@ testEmailUpdate brig userJournalWatcher = do -- In that case, you might need to manually delete the user from the test DB. @elland deleteUserInternal uid brig !!! const 202 === statusCode where - ensureNoOtherUserWithEmail :: Email -> Http () + ensureNoOtherUserWithEmail :: EmailAddress -> Http () ensureNoOtherUserWithEmail eml = do tk :: Maybe AccessToken <- responseJsonMaybe <$> login brig (defEmailLogin eml) SessionCookie @@ -929,7 +916,7 @@ testEmailUpdate brig userJournalWatcher = do deleteUser (Auth.user t) (Just defPassword) brig !!! const 200 === statusCode Util.assertDeleteJournaled userJournalWatcher (Auth.user t) "user deletion" - initiateUpdateAndActivate :: Email -> UserId -> Http () + initiateUpdateAndActivate :: EmailAddress -> UserId -> Http () initiateUpdateAndActivate eml uid = do initiateEmailUpdateNoSend brig eml uid !!! const 202 === statusCode activateEmail brig eml @@ -937,7 +924,7 @@ testEmailUpdate brig userJournalWatcher = do Util.assertEmailUpdateJournaled userJournalWatcher uid eml "user update" -- Ensure login work both with the full email and the "short" version login brig (defEmailLogin eml) SessionCookie !!! const 200 === statusCode - login brig (defEmailLogin (Email "test" "example.com")) SessionCookie !!! const 200 === statusCode + login brig (defEmailLogin (unsafeEmailAddress "test" "example.com")) SessionCookie !!! const 200 === statusCode testUserLocaleUpdate :: Brig -> UserJournalWatcher -> Http () testUserLocaleUpdate brig userJournalWatcher = do @@ -1116,10 +1103,7 @@ testSendActivationCode opts brig = do testSendActivationCodeInvalidEmailOrPhone :: Brig -> Http () testSendActivationCodeInvalidEmailOrPhone brig = do - let Just invalidEmail = parseEmail "?@?" - let invalidPhone = Phone "1234" - -- Code for phone pre-verification - requestActivationCode brig 400 (Right invalidPhone) + let invalidEmail = unsafeEmailAddress "?" "?" -- Code for email pre-verification requestActivationCode brig 400 (Left invalidEmail) @@ -1287,15 +1271,15 @@ testUpdateSSOId brig galley = do testDomainsBlockedForRegistration :: Opt.Opts -> Brig -> Http () testDomainsBlockedForRegistration opts brig = withDomainsBlockedForRegistration opts ["bad1.domain.com", "bad2.domain.com"] $ do - badEmail1 <- randomEmail <&> \e -> e {emailDomain = "bad1.domain.com"} - badEmail2 <- randomEmail <&> \e -> e {emailDomain = "bad2.domain.com"} + badEmail1 <- randomEmail <&> \e -> unsafeEmailAddress (localPart e) "bad1.domain.com" + badEmail2 <- randomEmail <&> \e -> unsafeEmailAddress (localPart e) "bad2.domain.com" post (brig . path "/activate/send" . contentJson . body (p badEmail1)) !!! do const 451 === statusCode const (Just "domain-blocked-for-registration") === (^? AesonL.key "label" . AesonL._String) . (responseJsonUnsafe @Value) post (brig . path "/activate/send" . contentJson . body (p badEmail2)) !!! do const 451 === statusCode const (Just "domain-blocked-for-registration") === (^? AesonL.key "label" . AesonL._String) . (responseJsonUnsafe @Value) - goodEmail <- randomEmail <&> \e -> e {emailDomain = "good.domain.com"} + goodEmail <- randomEmail <&> \e -> unsafeEmailAddress (localPart e) "good.domain.com" post (brig . path "/activate/send" . contentJson . body (p goodEmail)) !!! do const 200 === statusCode where diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 2dbf722c596..d189d6fd4c9 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -46,6 +46,7 @@ import Data.Misc (PlainTextPassword6, plainTextPassword6, plainTextPassword6Unsa import Data.Proxy import Data.Qualified import Data.Text qualified as Text +import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.IO (hPutStrLn) import Data.Text.Lazy qualified as Lazy import Data.Time.Clock @@ -175,7 +176,7 @@ testLoginWith6CharPassword brig db = do checkLogin email defPassword 403 checkLogin email pw6 200 where - checkLogin :: Email -> PlainTextPassword6 -> Int -> Http () + checkLogin :: EmailAddress -> PlainTextPassword6 -> Int -> Http () checkLogin email pw expectedStatusCode = login brig @@ -350,8 +351,9 @@ testEmailLogin brig = do assertSanePersistentCookie @ZAuth.User (decodeCookie rs) assertSaneAccessToken now (userId u) (decodeToken rs) -- Login again, but with capitalised email address - let Email loc dom = email - let email' = Email (Text.toUpper loc) dom + let loc = localPart email + dom = domainPart email + email' = unsafeEmailAddress (encodeUtf8 . Text.toUpper . decodeUtf8 $ loc) dom login brig (defEmailLogin email') PersistentCookie !!! const 200 === statusCode @@ -369,9 +371,11 @@ testHandleLogin brig = do -- untrusted. testLoginUntrustedDomain :: Brig -> Http () testLoginUntrustedDomain brig = do - Just (Email loc dom) <- userEmail <$> createUserUntrustedEmail "Homer" brig + Just email <- userEmail <$> createUserUntrustedEmail "Homer" brig + let loc = decodeUtf8 $ localPart email + dom = domainPart email -- login without "+" suffix - let email' = Email (Text.takeWhile (/= '+') loc) dom + let email' = unsafeEmailAddress (encodeUtf8 $ Text.takeWhile (/= '+') loc) dom login brig (defEmailLogin email') PersistentCookie !!! const 200 === statusCode @@ -390,7 +394,7 @@ testLoginFailure brig = do PersistentCookie !!! const 403 === statusCode -- login with wrong / non-existent email - let badmail = Email "wrong" "wire.com" + let badmail = unsafeEmailAddress "wrong" "wire.com" login brig ( MkLogin (LoginByEmail badmail) defPassword Nothing Nothing diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index c5a77e3de7b..1a4d528d108 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -129,7 +129,7 @@ registerUser name brig = do ] post (brig . path "/register" . contentJson . body p) -initiatePasswordReset :: Brig -> Email -> (MonadHttp m) => m ResponseLBS +initiatePasswordReset :: Brig -> EmailAddress -> (MonadHttp m) => m ResponseLBS initiatePasswordReset brig email = post ( brig @@ -138,7 +138,7 @@ initiatePasswordReset brig email = . body (RequestBodyLBS . encode $ NewPasswordReset email) ) -activateEmail :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> Email -> m () +activateEmail :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> EmailAddress -> m () activateEmail brig email = do act <- getActivationCode brig (Left email) case act of @@ -148,13 +148,13 @@ activateEmail brig email = do const 200 === statusCode const (Just False) === fmap activatedFirst . responseJsonMaybe -checkEmail :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> UserId -> Email -> m () +checkEmail :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> UserId -> EmailAddress -> m () checkEmail brig uid expectedEmail = get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode const (Just expectedEmail) === (userEmail <=< responseJsonMaybe) -initiateEmailUpdateLogin :: Brig -> Email -> Login -> UserId -> (MonadIO m, MonadCatch m, MonadHttp m) => m ResponseLBS +initiateEmailUpdateLogin :: Brig -> EmailAddress -> Login -> UserId -> (MonadIO m, MonadCatch m, MonadHttp m) => m ResponseLBS initiateEmailUpdateLogin brig email loginCreds uid = do (cky, tok) <- do rsp <- @@ -163,7 +163,7 @@ initiateEmailUpdateLogin brig email loginCreds uid = do pure (decodeCookie rsp, decodeToken rsp) initiateEmailUpdateCreds brig email (cky, tok) uid -initiateEmailUpdateCreds :: Brig -> Email -> (Bilge.Cookie, Brig.ZAuth.Token ZAuth.Access) -> UserId -> (MonadHttp m) => m ResponseLBS +initiateEmailUpdateCreds :: Brig -> EmailAddress -> (Bilge.Cookie, Brig.ZAuth.Token ZAuth.Access) -> UserId -> (MonadHttp m) => m ResponseLBS initiateEmailUpdateCreds brig email (cky, tok) uid = do put $ unversioned @@ -174,7 +174,7 @@ initiateEmailUpdateCreds brig email (cky, tok) uid = do . zUser uid . Bilge.json (EmailUpdate email) -initiateEmailUpdateNoSend :: (MonadHttp m, MonadIO m, MonadCatch m) => Brig -> Email -> UserId -> m ResponseLBS +initiateEmailUpdateNoSend :: (MonadHttp m, MonadIO m, MonadCatch m) => Brig -> EmailAddress -> UserId -> m ResponseLBS initiateEmailUpdateNoSend brig email uid = let emailUpdate = RequestBodyLBS . encode $ EmailUpdate email in put (brig . path "/i/self/email" . contentJson . zUser uid . body emailUpdate) @@ -183,7 +183,7 @@ initiateEmailUpdateNoSend brig email uid = preparePasswordReset :: (MonadIO m, MonadHttp m) => Brig -> - Email -> + EmailAddress -> UserId -> PlainTextPassword8 -> m CompletePasswordReset @@ -205,7 +205,7 @@ completePasswordReset brig passwordResetData = . body (RequestBodyLBS $ encode passwordResetData) ) -removeBlacklist :: Brig -> Email -> (MonadIO m, MonadHttp m) => m () +removeBlacklist :: Brig -> EmailAddress -> (MonadIO m, MonadHttp m) => m () removeBlacklist brig email = void $ delete (brig . path "/i/users/blacklist" . queryItem "email" (toByteString' email)) diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs index 81f6e995c51..a0b869d1d97 100644 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ b/services/brig/test/integration/API/UserPendingActivation.hs @@ -56,7 +56,6 @@ import Web.Scim.Schema.Common qualified as Scim import Web.Scim.Schema.Meta (WithMeta) import Web.Scim.Schema.Meta qualified as Scim import Web.Scim.Schema.User qualified as Scim.User -import Web.Scim.Schema.User.Email qualified as Email import Web.Scim.Schema.User.Phone qualified as Phone import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Team hiding (newTeam) @@ -111,7 +110,7 @@ createScimToken spar' owner = do } pure tok -createUserStep :: Spar -> Brig -> ScimToken -> TeamId -> Scim.User.User SparTag -> Email -> HttpT IO (WithMeta (WithId UserId (Scim.User.User SparTag)), Invitation, InvitationCode) +createUserStep :: Spar -> Brig -> ScimToken -> TeamId -> Scim.User.User SparTag -> EmailAddress -> HttpT IO (WithMeta (WithId UserId (Scim.User.User SparTag)), Invitation, InvitationCode) createUserStep spar' brig' tok tid scimUser email = do scimStoredUser <- createUser spar' tok scimUser inv <- getInvitationByEmail brig' email @@ -141,7 +140,7 @@ userExists uid = do usersSelect :: PrepQuery R (Identity UserId) (UserId, Maybe AccountStatus) usersSelect = "SELECT id, status FROM user where id = ?" -getInvitationByEmail :: Brig -> Email -> Http Invitation +getInvitationByEmail :: Brig -> EmailAddress -> Http Invitation getInvitationByEmail brig email = responseJsonUnsafe <$> ( Bilge.get (brig . path "/i/teams/invitations/by-email" . contentJson . queryItem "email" (toByteString' email)) @@ -194,7 +193,7 @@ randomScimUserWithSubjectAndRichInfo :: m (Scim.User.User SparTag, SAML.UnqualifiedNameID) randomScimUserWithSubjectAndRichInfo richInfo = do suffix <- cs <$> replicateM 7 (getRandomR ('0', '9')) - emails <- getRandomR (0, 3) >>= \n -> replicateM n randomScimEmail + _emails <- getRandomR (0, 3) >>= \n -> replicateM n randomScimEmail phones <- getRandomR (0, 3) >>= \n -> replicateM n randomScimPhone -- Related, but non-trivial to re-use here: 'nextSubject' (externalId, subj) <- @@ -213,23 +212,16 @@ randomScimUserWithSubjectAndRichInfo richInfo = do ( (Scim.User.empty @SparTag userSchemas ("scimuser_" <> suffix) (ScimUserExtra richInfo)) { Scim.User.displayName = Just ("ScimUser" <> suffix), Scim.User.externalId = Just externalId, - Scim.User.emails = emails, Scim.User.phoneNumbers = phones }, subj ) -randomScimEmail :: (MonadRandom m) => m Email.Email +randomScimEmail :: (MonadRandom m) => m EmailAddress randomScimEmail = do - let typ :: Maybe Text = Nothing - -- TODO: where should we catch users with more than one - -- primary email? - primary :: Maybe Scim.ScimBool = Nothing - value :: Email.EmailAddress2 <- do - localpart <- cs <$> replicateM 15 (getRandomR ('a', 'z')) - domainpart <- (<> ".com") . cs <$> replicateM 15 (getRandomR ('a', 'z')) - pure . Email.EmailAddress2 $ Email.unsafeEmailAddress localpart domainpart - pure Email.Email {..} + localpart <- cs <$> replicateM 15 (getRandomR ('a', 'z')) + domainpart <- (<> ".com") . cs <$> replicateM 15 (getRandomR ('a', 'z')) + pure $ Email.unsafeEmailAddress localpart domainpart randomScimPhone :: (MonadRandom m) => m Phone.Phone randomScimPhone = do @@ -344,7 +336,7 @@ createToken spar zusr payload = do Email -> Name -> InvitationCode -> Bool -> Http () +registerInvitation :: Brig -> EmailAddress -> Name -> InvitationCode -> Bool -> Http () registerInvitation brig email name inviteeCode shouldSucceed = do void $ post @@ -355,7 +347,7 @@ registerInvitation brig email name inviteeCode shouldSucceed = do ) Email -> InvitationCode -> Aeson.Value +acceptWithName :: Name -> EmailAddress -> InvitationCode -> Aeson.Value acceptWithName name email code = Aeson.object [ "name" Aeson..= fromName name, diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 5445c497c7e..18c676c15b3 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -308,7 +308,7 @@ createUser' hasPwd name brig = do Text -> Email -> Brig -> Http User +createUserWithEmail :: (HasCallStack) => Text -> EmailAddress -> Brig -> Http User createUserWithEmail name email brig = do r <- postUserWithEmail True True name (Just email) False Nothing Nothing brig @@ -329,7 +329,7 @@ createAnonUserExpiry expires name brig = do r <- post (brig . path "/register" . contentJson . body p) Brig -> Int -> Either Email Phone -> Http () +requestActivationCode :: (HasCallStack) => Brig -> Int -> Either EmailAddress Phone -> Http () requestActivationCode brig expectedStatus ep = post (brig . path "/activate/send" . contentJson . body (RequestBodyLBS . encode $ bdy ep)) !!! const expectedStatus === statusCode @@ -340,7 +340,7 @@ requestActivationCode brig expectedStatus ep = getActivationCode :: (MonadCatch m, MonadHttp m, HasCallStack) => Brig -> - Either Email Phone -> + Either EmailAddress Phone -> m (Maybe (ActivationKey, ActivationCode)) getActivationCode brig ep = do let qry = either (queryItem "email" . toByteString') (queryItem "phone" . toByteString') ep @@ -403,7 +403,7 @@ postUserWithEmail :: Bool -> Bool -> Text -> - Maybe Email -> + Maybe EmailAddress -> Bool -> Maybe UserSSOId -> Maybe TeamId -> @@ -800,27 +800,31 @@ zClient = header "Z-Client" . toByteString' zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" -mkEmailRandomLocalSuffix :: (MonadIO m) => Text -> m Email +mkEmailRandomLocalSuffix :: (MonadIO m) => Text -> m EmailAddress mkEmailRandomLocalSuffix e = do uid <- liftIO UUID.nextRandom - case parseEmail e of - Just (Email loc dom) -> pure $ Email (loc <> "+" <> UUID.toText uid) dom + case emailAddressText e of + Just mail -> + pure $ + unsafeEmailAddress + ((localPart mail) <> "+" <> UUID.toASCIIBytes uid) + (domainPart mail) Nothing -> error $ "Invalid email address: " ++ Text.unpack e -- | Generate emails that are in the trusted whitelist of domains whose @+@ suffices count for email -- disambiguation. See also: 'Brig.Email.mkEmailKey'. -randomEmail :: (MonadIO m) => m Email +randomEmail :: (MonadIO m) => m EmailAddress randomEmail = mkSimulatorEmail "success" -- | To test the behavior of email addresses with untrusted domains (two emails are equal even if -- their local part after @+@ differs), we need to generate them. -randomUntrustedEmail :: (MonadIO m) => m Email +randomUntrustedEmail :: (MonadIO m) => m EmailAddress randomUntrustedEmail = do -- NOTE: local part cannot be longer than 64 octets rd <- liftIO (randomIO :: IO Integer) - pure $ Email (Text.pack $ show rd) "zinfra.io" + pure $ unsafeEmailAddress (pack $ show rd) "zinfra.io" -mkSimulatorEmail :: (MonadIO m) => Text -> m Email +mkSimulatorEmail :: (MonadIO m) => Text -> m EmailAddress mkSimulatorEmail loc = mkEmailRandomLocalSuffix (loc <> "@simulator.amazonses.com") randomPhone :: (MonadIO m) => m Phone @@ -838,10 +842,10 @@ randomActivationCode = . printf "%06d" <$> randIntegerZeroToNMinusOne 1000000 -defEmailLogin :: Email -> Login +defEmailLogin :: EmailAddress -> Login defEmailLogin e = emailLogin e defPassword (Just defCookieLabel) -emailLogin :: Email -> PlainTextPassword6 -> Maybe CookieLabel -> Login +emailLogin :: EmailAddress -> PlainTextPassword6 -> Maybe CookieLabel -> Login emailLogin e pw cl = MkLogin (LoginByEmail e) pw cl Nothing somePrekeys :: [Prekey] diff --git a/services/brig/test/integration/Util/AWS.hs b/services/brig/test/integration/Util/AWS.hs index ace8a3f23d1..458bd82d0c4 100644 --- a/services/brig/test/integration/Util/AWS.hs +++ b/services/brig/test/integration/Util/AWS.hs @@ -89,14 +89,14 @@ assertLocaleUpdateJournaled :: (HasCallStack, MonadUnliftIO m) => UserJournalWat assertLocaleUpdateJournaled userJournalWatcher uid loc label = assertMessage userJournalWatcher label (userUpdateMatcher uid) (userLocaleUpdateJournaled uid loc) -userEmailUpdateJournaled :: (HasCallStack, MonadIO m) => UserId -> Email -> String -> Maybe PU.UserEvent -> m () +userEmailUpdateJournaled :: (HasCallStack, MonadIO m) => UserId -> EmailAddress -> String -> Maybe PU.UserEvent -> m () userEmailUpdateJournaled uid em l (Just ev) = liftIO $ do assertEventType l PU.UserEvent'USER_UPDATE ev assertUserId l uid ev assertEmail l (Just em) ev userEmailUpdateJournaled _ _ l Nothing = liftIO $ assertFailure $ l <> ": Expected 1 UserUpdate, got nothing" -assertEmailUpdateJournaled :: (HasCallStack, MonadUnliftIO m) => UserJournalWatcher -> UserId -> Email -> String -> m () +assertEmailUpdateJournaled :: (HasCallStack, MonadUnliftIO m) => UserJournalWatcher -> UserId -> EmailAddress -> String -> m () assertEmailUpdateJournaled userJournalWatcher uid em label = assertMessage userJournalWatcher label (userUpdateMatcher uid) (userEmailUpdateJournaled uid em) @@ -130,8 +130,8 @@ assertName :: String -> Maybe Name -> PU.UserEvent -> IO () assertName l (Just nm) ev = assertEqual (l <> "name should exist") nm (Name $ fromMaybe "failed to decode name" $ fromByteString $ ev ^. PU.name) assertName l Nothing ev = assertEqual (l <> "name should not exist") Nothing (ev ^. PU.maybe'name) -assertEmail :: String -> Maybe Email -> PU.UserEvent -> IO () -assertEmail l (Just em) ev = assertEqual (l <> "email should exist") em (fromMaybe (error "Failed to convert to email") $ parseEmail $ Text.decodeLatin1 $ fromMaybe "failed to decode email value" $ fromByteString $ ev ^. PU.email) +assertEmail :: String -> Maybe EmailAddress -> PU.UserEvent -> IO () +assertEmail l (Just em) ev = assertEqual (l <> "email should exist") em (fromMaybe (error "Failed to convert to email") $ emailAddressText $ Text.decodeLatin1 $ fromMaybe "failed to decode email value" $ fromByteString $ ev ^. PU.email) assertEmail l Nothing ev = assertEqual (l <> "email should not exist") Nothing (ev ^. PU.maybe'email) assertLocale :: String -> Maybe Locale -> PU.UserEvent -> IO () diff --git a/services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs b/services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs index 74c7b92c732..5e6af3a3d0d 100644 --- a/services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs +++ b/services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs @@ -63,7 +63,7 @@ userDoc1 = udName = Just . Name $ "Carl Phoomp", udNormalized = Just $ "carl phoomp", udHandle = Just . fromJust . parseHandle $ "phoompy", - udEmail = Just $ Email "phoompy" "example.com", + udEmail = Just $ unsafeEmailAddress "phoompy" "example.com", udColourId = Just . ColourId $ 32, udAccountStatus = Just Active, udSAMLIdP = Just "https://issuer.net/214234", diff --git a/services/federator/test/integration/Test/Federator/Util.hs b/services/federator/test/integration/Test/Federator/Util.hs index eb352855627..a979a4c5de7 100644 --- a/services/federator/test/integration/Test/Federator/Util.hs +++ b/services/federator/test/integration/Test/Federator/Util.hs @@ -220,7 +220,7 @@ postUserWithEmail :: Bool -> Bool -> Text -> - Maybe Email -> + Maybe EmailAddress -> Bool -> Maybe UserSSOId -> Maybe TeamId -> @@ -310,17 +310,17 @@ defCookieLabel = CookieLabel "auth" -- | Generate emails that are in the trusted whitelist of domains whose @+@ suffices count for email -- disambiguation. See also: 'Brig.Email.mkEmailKey'. -randomEmail :: (MonadIO m) => m Email +randomEmail :: (MonadIO m) => m EmailAddress randomEmail = mkSimulatorEmail "success" -mkSimulatorEmail :: (MonadIO m) => Text -> m Email +mkSimulatorEmail :: (MonadIO m) => Text -> m EmailAddress mkSimulatorEmail loc = mkEmailRandomLocalSuffix (loc <> "@simulator.amazonses.com") -mkEmailRandomLocalSuffix :: (MonadIO m) => Text -> m Email +mkEmailRandomLocalSuffix :: (MonadIO m) => Text -> m EmailAddress mkEmailRandomLocalSuffix e = do uid <- liftIO UUID.nextRandom - case parseEmail e of - Just (Email loc dom) -> pure $ Email (loc <> "+" <> UUID.toText uid) dom + case emailAddressText e of + Just mail -> pure $ unsafeEmailAddress ((localPart mail) <> "+" <> UUID.toASCIIBytes uid) (domainPart mail) Nothing -> error $ "Invalid email address: " ++ Text.unpack e zUser :: UserId -> Bilge.Request -> Bilge.Request diff --git a/services/federator/test/resources/unit/localhost-dot-key.pem b/services/federator/test/resources/unit/localhost-dot-key.pem index 28872746b1a..59b9a0128e3 100644 --- a/services/federator/test/resources/unit/localhost-dot-key.pem +++ b/services/federator/test/resources/unit/localhost-dot-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/localhost-dot.pem b/services/federator/test/resources/unit/localhost-dot.pem index 9eba28b158f..5e656feded7 100644 --- a/services/federator/test/resources/unit/localhost-dot.pem +++ b/services/federator/test/resources/unit/localhost-dot.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDQjCCAiqgAwIBAgIUF3L/4L7MgKXyAs+xO0f0/zpfwSYwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjIwNzIyMDgwODAwWhcN -MjMwNzIyMDgwODAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -vgZ8XblpKvf46pVOL0t9TFAYyXYylP65fFp/Uiim9+3YO27mPcv8CUMLRov08xAU -Upp8ufR0wqfEZ6Q32CwXhuNEX/JESbW4HX+wMLAcOacd117T/4iDQJCYSg/uFoVJ -hCeUVSiigbB2cD6F4XKnFw7xy9AKnVzLOSHo+NdtblNRkoy50JJBJ2hu4rrqtq+N -NE0Fky4vfrSQ+N0wQnAu3+UwZLulJhLhIlDiY5zcZLZ1Tw92/CpK7L7AKFReFAH+ -1lfhKDuOUi+XAerHycOSTYT05Qoye2ekqLwAb0n7EIIqCEIBmAk+7klhe51BfxfO -UHOmhTUPG6C5bKD1+q0ssQIDAQABo4GaMIGXMA4GA1UdDwEB/wQEAwIFoDAdBgNV +MIIDQjCCAiqgAwIBAgIUVSPGRv1KOXw3iQBP1qT56qQCDJgwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjQwODIxMDg0NjAwWhcN +MjUwODIxMDg0NjAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +1/bcWd24MFjCfqpEPdF6pDDafOqpHLBgbs1eD3TN+dDK/hMy3poDUwtglyIJG0XL +Zd3xcgFKcWdr59TXlHdKptyseG9MdvRd5AbKtOSQwmkldNi6ljllVf/iQhd2FmU4 +QkaLaeP3vJKQj5f6zHJy1uKMt1f6XekC4vaUTgp+jZiQ/JpFpsZIs0Ared9SsvLS +aezfUEZSZK8M0ialzBmm2wouGjecjqWcWX4VDxMih7gEaseElUnzzOe/Rl7AuQKj +4J9Bud0c330GNRSg52hXh6ujvpLQpmx0Pu1+bklWpOtK7xM75m3U9EouVB+skMcQ +5lSBwR64+FLdUDXCJDzUqQIDAQABo4GaMIGXMA4GA1UdDwEB/wQEAwIFoDAdBgNV HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E -FgQUuLBJ1uWinp9T4WG27wVudIp7LAkwHwYDVR0jBBgwFoAUmUEefcwWEyqnjNBx -DNucQ7woHRwwGAYDVR0RAQH/BA4wDIIKbG9jYWxob3N0LjANBgkqhkiG9w0BAQsF -AAOCAQEABfH+r39LsPVSU5Kk7/6xk2HTthT1Fy/fPKy1elPtdakh7l6eI4op+lLT -Fwv1FPI0FzNpSYXjzCNfhuiO83+D0hcaDGT+syHrQwHO50P6mRP0pB5gWULfAvd1 -4FGMk5tEzQHpIy+QpLThJErVjmI0x1ufR6d38KLvH87AZE3Hx1k4j/qWFik/C9s8 -qfXJxntlyyDmPQsSgEYMqjffhhhWfKjhJX4AIj/akq7dnnmsqioXO1aXgBCCVprm -RRak6MY9Sb+31nFIZbXDOlCL1/d5IPfT8C7qd19aFt3vFPpI+83OldaskL3isga8 -citSwDCqZppVIFn2s4SM61bS2z6R0g== +FgQUtMGs+LCIf1q+ytJH9WaLt7c89RcwHwYDVR0jBBgwFoAUv4GMQ6kOgRxHoPPJ +oPWQGDxVuCMwGAYDVR0RAQH/BA4wDIIKbG9jYWxob3N0LjANBgkqhkiG9w0BAQsF +AAOCAQEAnprNAyy/Z2mDN4ZaXNVDjBe+T1is5fi9EsaR0CJnN6q61fwo3hBwi5KK +/cADYlvc/7AzlfP39L52oZ7eWKjeuy9BP6l3uZIfHLKsQoUY0SAtx6TBQT8ayiUe +DfBvezg6sKvDa1SPBev90rEwOwbS6xi+M+pRZgptgh2bMkGoUoE0/4rIjRDaimxQ +GdSwLeNsZNAP361fXIpmJKtOw68TRla0PvHffTBRwV85Y6BI6AuLWaFlfhkMYKxK +gm5mqhXutNGGxEGbf24+S4kFgtzYeiYnNidWGUN9LWgcNymdDgZYu8fApY/8geiv +XoSOfarydyovBeuCPyX1AAviW1/N5Q== -----END CERTIFICATE----- diff --git a/services/federator/test/resources/unit/localhost-key.pem b/services/federator/test/resources/unit/localhost-key.pem index 5630060c32e..6e869952931 100644 --- a/services/federator/test/resources/unit/localhost-key.pem +++ b/services/federator/test/resources/unit/localhost-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/localhost.example.com-key.pem b/services/federator/test/resources/unit/localhost.example.com-key.pem index fafd3084540..897d4630535 100644 --- a/services/federator/test/resources/unit/localhost.example.com-key.pem +++ b/services/federator/test/resources/unit/localhost.example.com-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/localhost.example.com.pem b/services/federator/test/resources/unit/localhost.example.com.pem index 25967b394e9..92122ade2b2 100644 --- a/services/federator/test/resources/unit/localhost.example.com.pem +++ b/services/federator/test/resources/unit/localhost.example.com.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/services/federator/test/resources/unit/localhost.pem b/services/federator/test/resources/unit/localhost.pem index 8927bb2a05f..950a2572e3c 100644 --- a/services/federator/test/resources/unit/localhost.pem +++ b/services/federator/test/resources/unit/localhost.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDQTCCAimgAwIBAgIUBd2V4fE6PJFOkVOjw+mpIdwZ5JwwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjIwNzIyMDgwODAwWhcN -MjMwNzIyMDgwODAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -uu8FGRe6JZ5k2avQv1DZKLuYk6wPQHXtI23NSSHCEo5NH+8GAlkzpZNmH25jEWkg -ECd3See9aebfTZByG1BWJkfj+f/0X3FewFdyGnxy6pZvjRblLNhBE/k545w7LOsD -J/AJvYe31j8WEBehzfxRxmC2mWLMK4HKxP3LM0q4zPKW5Ax9WFazzJnR/mu6q6Fd -xZumzMwX/0NocFTLJXN90XzkrxPXhHjZJ6FkiWuzdaOaOUu4cWQar4Ew31mjEst8 -JKTpX9kOjzOZYlBd2Ez7HFV1f5H5TUthV+0mNSAJhHc+b0k/11EF3fgg0lb1+RV8 -938tVKxKPs+aLcMu6YKQFQIDAQABo4GZMIGWMA4GA1UdDwEB/wQEAwIFoDAdBgNV +MIIDQTCCAimgAwIBAgIUDSbc1r9KTiCOa+lKj322ZLi3PGkwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjQwODIxMDg0NjAwWhcN +MjUwODIxMDg0NjAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA ++gITPHZQnXllPO2KYNRbHF961JKELjHIv148M8R6bdFlFtGzVd/4KpnRsIbruQNQ +EIreiF+rD7tWQuN3gWhrxn1qbfHvuvX1QcXzyF2OoW8atIU1c5fNeLGQFGIs2a0u +yrmBg6o6Vet6LNGI3OXIXmGfQC5PkH6uBhZmEnugyRDzOOmVg75fqIPZw1SQdlWb +pVS+7poXc06v+QHfcF0NqUkZBeUnpw83Aj6HVFgAhuHEy5eJfXj8zRmDCqqjItE2 +D2HMVACxZgDEsSe6wDl+WvW1QQU9Kmm4fTOx6FOXtJjjTtDdiaP/F/BnqJoVqkhv +ITGziwqjLJ2NNC0K7RoMbwIDAQABo4GZMIGWMA4GA1UdDwEB/wQEAwIFoDAdBgNV HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E -FgQUctS7mVDQHyRkH0rrqFeASEs5SFEwHwYDVR0jBBgwFoAUmUEefcwWEyqnjNBx -DNucQ7woHRwwFwYDVR0RAQH/BA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUA -A4IBAQBstDMgzQKCqUnikh8FwZu8Sq3ddNDSsT3bD3nK04JVgFe28BNwxGxkKhBw -mp5WvdtGJJPMskEozoqlqPuQ2gPIzZa8yc6jpKgDrPQIaUkQHB+044ZnkGkE1fht -RDtBKZG/nYmqXVQhHgsbMN3HGVEjbfpMk26gCQnvYO8iwXt+XgJKF8hpGMWU7+Rh -1NFHjSYair07VJIpbcB5kVeOgjDF0IJFJFvZe2142BiG7D5vnJRraTPvcH1X4htN -3mOQsV64tFCMGa33DfAj6n0GrOmHAE/eJFVvIwFrYbGQWY0pdraNTa18B9zJu3zc -vlEPMbfftb3oB38RWN6MVGpqBAfp +FgQUCdxcQDi3Q4YzwAdpbStqA3FZAy4wHwYDVR0jBBgwFoAUv4GMQ6kOgRxHoPPJ +oPWQGDxVuCMwFwYDVR0RAQH/BA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUA +A4IBAQCoqmWAPm0WZVbppL57uRY5J04W++0zuoHQzFcVlvuycTKM30AWeW+kx9Il +Cac99zqL11+GG7EDzkCwnYyWsrBOzWhfbWBRbk9VwoVXOeuWqLEv8lGg7ainL8L/ +9M/XpS9HTcljZH5X47laukb+G/WazPZ66WEVvOjmXu5G2sIpxKnNAPzlyO0hHTbD +tOYpVGC4LFRw2ptVsw2zE5+5E0PiKbCDNhnPBJXxAZ1P9BFm6U3HJxBGGz0zUEYI +EFRHK7pIzMOU5uQMf8C8YtEwnBWLQeaTsz4QkAY1MxVX9vEYXFTx+8MbPfkV5DVW +4OMKS6zgGqulkuEPfqDW9f8gUROc -----END CERTIFICATE----- diff --git a/services/federator/test/resources/unit/multidomain-federator.example.com-key.pem b/services/federator/test/resources/unit/multidomain-federator.example.com-key.pem index a87cc702421..4bd8b8fa2d4 100644 --- a/services/federator/test/resources/unit/multidomain-federator.example.com-key.pem +++ b/services/federator/test/resources/unit/multidomain-federator.example.com-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/multidomain-federator.example.com.pem b/services/federator/test/resources/unit/multidomain-federator.example.com.pem index b91600d7487..ddfe8399865 100644 --- a/services/federator/test/resources/unit/multidomain-federator.example.com.pem +++ b/services/federator/test/resources/unit/multidomain-federator.example.com.pem @@ -1,22 +1,22 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/services/federator/test/resources/unit/second-federator.example.com-key.pem b/services/federator/test/resources/unit/second-federator.example.com-key.pem index f71dd93d91d..6c412fe1818 100644 --- a/services/federator/test/resources/unit/second-federator.example.com-key.pem +++ b/services/federator/test/resources/unit/second-federator.example.com-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/second-federator.example.com.pem b/services/federator/test/resources/unit/second-federator.example.com.pem index e8550b57332..44d8f0d823c 100644 --- a/services/federator/test/resources/unit/second-federator.example.com.pem +++ b/services/federator/test/resources/unit/second-federator.example.com.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/services/federator/test/resources/unit/unit-ca-key.pem b/services/federator/test/resources/unit/unit-ca-key.pem index f4849cf8406..5b02eb88316 100644 --- a/services/federator/test/resources/unit/unit-ca-key.pem +++ b/services/federator/test/resources/unit/unit-ca-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/services/federator/test/resources/unit/unit-ca.pem b/services/federator/test/resources/unit/unit-ca.pem index b6e3b1b5311..70528966ff4 100644 --- a/services/federator/test/resources/unit/unit-ca.pem +++ b/services/federator/test/resources/unit/unit-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDAjCCAeqgAwIBAgIUXYv9W35DKGoq2Hpp1nCpVb+f96kwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjIwNzIyMDgwODAwWhcN -MjcwNzIxMDgwODAwWjAZMRcwFQYDVQQDEw5jYS5leGFtcGxlLmNvbTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBANz2l8Bf06Pyr6xKVKVab9ZRqmKYf4cV -WGRe+g+stTR2UiUIQ1ntZCdB5WwQ6ZMt525EZRLnNgguFZ5hsZxoxr1zIh+pHPYY -pHxBJ9Qw68W2ejk2FPMEr2k6wz409hk9OVyZGifa4kLD9uIMMMKmAgsfW4FH2ZEO -XDcLt3lUybProJOY7dgkyiYQQrqDYs/oPlz8T5Eyo5CudYzUfO6zttOPecuy0Fjn -9QURUk/Vt8YQt11+u3JHdVXyC93/EAL0DstEo1Y6YffFlo16KX4o9iADKmb1MDr/ -Qis0PqPM5BrVxkq3/YwyPTbjMLHk0zlcPfus3XfbXVf85UWSVzACs9ECAwEAAaNC -MEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJlB -Hn3MFhMqp4zQcQzbnEO8KB0cMA0GCSqGSIb3DQEBCwUAA4IBAQBhtvb/c5aM5F6M -y5qY9xJPDAbdPBKuQhQrAtUJn41+qKts0qsV4KNPuqv9zM+UIUTa2czt1MU9az4l -7S+fiL0EctBgifKT5bDBtbuiwztOxgFw5SwKyxRhoIx8k0g25O5opavhXwbHPjRx -4AjVbcEzzL9qnGaFfpps5BsznUFGntlBcw1peZQerUq5vbUYwN1qbsd/DFkU/5Z+ -V/6URgKXMqpU0OMuz20e9QNQcIgQKiYD5X7k3Ouscf5aKkZ9vaAC23NVcHAIq/Le -5mGZptEzuLq7Mq60d3vKC79L+cLm6ivSlg90PkMcmBPbQqwlv2mh094NeU5FibTh -SVvltELN +MIIDAjCCAeqgAwIBAgIUWLkk5j79RpKaTsLgjbFh8xteceQwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjQwODIxMDg0NjAwWhcN +MjkwODIwMDg0NjAwWjAZMRcwFQYDVQQDEw5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsmofCQ+LMM1F92MnymQ1IvHNG5izR0 +Ym1Vqens0LTuffA6TSNWJRrqIdR48fOgglCEZMCL60OpN9eD4pQjGNJnx+OrWMQT +1JWwrZLC8xiq/xqeLu4oKn9hPw9KKXhmuHrZlI6ffi37M4/oZR3KeJJZOVqBZ8Wx +nZTecQR3bVjjBRvCfWrq3CeLiu6Upzk4PT13QzfRoym2t90Rxegs+PuY0fuYovdg +KmlxdcadJHxzN8qkzugM2j2mHaAS5bwrZL7eYJepSKzd8jIPnEeFscKvjXfQwx8k +KS9fPcXEIeNWdY++/PS7t81rwwXoM9ZGZHu8kVNV8pnnzbO9OVTUVV0CAwEAAaNC +MEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFL+B +jEOpDoEcR6DzyaD1kBg8VbgjMA0GCSqGSIb3DQEBCwUAA4IBAQCD8JDLG7F2VvMf +WONxbmw3nDlKzMyuOSfRXZr/RdpLJFlpz1uwUYV5fyT9wG9LFhw4aN+tXWi4+LLs +Vi947pqm78jqvI6rpqAcy/EjRf2oA9aGkZn1siM3eVga2ZDw9hstawB8ioW7Fj8C +rdHsGGsJHEkzXL6ntydaVphWTxvITvyHHwyI809Q3TotFn1zleXhFgiHwjHIjMnZ +vqd4juNUlaGyYQzg/QbOOCt65KcJ5mZTELNRXuNjf+QPmpc5lZXyEMrnTirleXza +S3Gb660QldEf0x8UoV/vF3cQZXGevqs7yNWUAMpHTnNjuYiXO646iHVDHUNMgZdZ +CxJC5jBz -----END CERTIFICATE----- diff --git a/services/galley/default.nix b/services/galley/default.nix index 6e1d3c62f7a..3cadf669e2f 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -32,7 +32,6 @@ , currency-codes , data-default , data-timeout -, either , enclosed-exceptions , errors , exceptions @@ -158,7 +157,6 @@ mkDerivation { currency-codes data-default data-timeout - either enclosed-exceptions errors exceptions diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 0344978de3b..790607e9bf6 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -308,7 +308,6 @@ library , currency-codes >=2.0 , data-default , data-timeout - , either , enclosed-exceptions >=1.0 , errors >=2.0 , exceptions >=0.4 diff --git a/services/galley/src/Galley/Cassandra/Instances.hs b/services/galley/src/Galley/Cassandra/Instances.hs index 57ae885b673..1c9307400c6 100644 --- a/services/galley/src/Galley/Cassandra/Instances.hs +++ b/services/galley/src/Galley/Cassandra/Instances.hs @@ -27,7 +27,6 @@ import Cassandra.CQL import Control.Error (note) import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as LBS -import Data.Either.Combinators hiding (fromRight) import Data.Text qualified as T import Data.Text.Encoding qualified as T import Imports diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index ea775d4b40e..0783fce56e7 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -470,7 +470,7 @@ makeOwner owner mem tid = do !!! const 200 === statusCode -acceptInviteBody :: Email -> InvitationCode -> RequestBody +acceptInviteBody :: EmailAddress -> InvitationCode -> RequestBody acceptInviteBody email code = RequestBodyLBS . encode $ object @@ -2218,10 +2218,10 @@ otrRecipients = defPassword :: PlainTextPassword6 defPassword = plainTextPassword6Unsafe "topsecretdefaultpassword" -randomEmail :: (MonadIO m) => m Email +randomEmail :: (MonadIO m) => m EmailAddress randomEmail = do uid <- liftIO nextRandom - pure $ Email ("success+" <> UUID.toText uid) "simulator.amazonses.com" + pure $ unsafeEmailAddress ("success+" <> UUID.toASCIIBytes uid) "simulator.amazonses.com" selfConv :: UserId -> ConvId selfConv u = Id (toUUID u) diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 67d1bd5ace5..569310e0a21 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -73,7 +73,7 @@ import Spar.Error import qualified Spar.Intra.BrigApp as Brig import Spar.Options import Spar.Orphans () -import Spar.Scim +import Spar.Scim hiding (handle) import Spar.Sem.AReqIDStore (AReqIDStore) import Spar.Sem.AssIDStore (AssIDStore) import Spar.Sem.BrigAccess (BrigAccess) diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index ee2c61ccbfe..f0bb0c79e2a 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -94,7 +94,7 @@ import qualified System.Logger as TinyLog import URI.ByteString as URI import Web.Cookie (SetCookie, renderSetCookie) import Wire.API.Team.Role (Role, defaultRole) -import Wire.API.User hiding (validateEmail) +import Wire.API.User import Wire.API.User.IdentityProvider import Wire.API.User.Saml import Wire.API.User.Scim (ValidExternalId (..)) @@ -149,7 +149,7 @@ getUserIdByScimExternalId :: Member ScimExternalIdStore r ) => TeamId -> - Email -> + EmailAddress -> Sem r (Maybe UserId) getUserIdByScimExternalId tid email = do muid <- ScimExternalIdStore.lookup tid email @@ -252,7 +252,7 @@ validateEmail :: ) => Maybe TeamId -> UserId -> - Email -> + EmailAddress -> Sem r () validateEmail mbTid uid email = do enabled <- maybe (pure False) GalleyAccess.isEmailValidationEnabledTeam mbTid diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index aaac39be64b..9f1b8628cce 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -130,7 +130,7 @@ createBrigUserSAML uref (Id buid) teamid name managedBy handle richInfo mLocale createBrigUserNoSAML :: (HasCallStack, MonadSparToBrig m) => - Email -> + EmailAddress -> UserId -> TeamId -> -- | User name @@ -150,7 +150,7 @@ createBrigUserNoSAML email uid teamid uname locale role = do then userId . accountUser <$> parseResponse @UserAccount "brig" resp else rethrow "brig" resp -updateEmail :: (HasCallStack, MonadSparToBrig m) => UserId -> Email -> m () +updateEmail :: (HasCallStack, MonadSparToBrig m) => UserId -> EmailAddress -> m () updateEmail buid email = do resp <- call $ @@ -210,7 +210,7 @@ getBrigUserByHandle handle = do 404 -> pure Nothing _ -> rethrow "brig" resp -getBrigUserByEmail :: (HasCallStack, MonadSparToBrig m) => Email -> m (Maybe UserAccount) +getBrigUserByEmail :: (HasCallStack, MonadSparToBrig m) => EmailAddress -> m (Maybe UserAccount) getBrigUserByEmail email = do resp :: ResponseLBS <- call $ diff --git a/services/spar/src/Spar/Intra/BrigApp.hs b/services/spar/src/Spar/Intra/BrigApp.hs index 9704658039d..da6ee3e01f8 100644 --- a/services/spar/src/Spar/Intra/BrigApp.hs +++ b/services/spar/src/Spar/Intra/BrigApp.hs @@ -78,16 +78,16 @@ veidFromUserSSOId = \case case urefToEmail uref of Nothing -> pure $ UrefOnly uref Just email -> pure $ EmailAndUref email uref + -- FUTUREWORK(elland): account for SCIM emails fields? UserScimExternalId email -> maybe (throwError "externalId not an email and no issuer") (pure . EmailOnly) - (parseEmail email) + (emailAddressText email) -- | If the brig user has a 'UserSSOId', transform that into a 'ValidExternalId' (this is a -- total function as long as brig obeys the api). Otherwise, if the user has an email, we can --- construct a return value from that (and an optional saml issuer). If a user only has a --- phone number, or no identity at all, throw an error. +-- construct a return value from that (and an optional saml issuer). -- -- Note: the saml issuer is only needed in the case where a user has been invited via team -- settings and is now onboarded to saml/scim. If this case can safely be ruled out, it's ok @@ -95,7 +95,7 @@ veidFromUserSSOId = \case veidFromBrigUser :: (MonadError String m) => User -> Maybe SAML.Issuer -> m ValidExternalId veidFromBrigUser usr mIssuer = case (userSSOId usr, userEmail usr, mIssuer) of (Just ssoid, _, _) -> veidFromUserSSOId ssoid - (Nothing, Just email, Just issuer) -> pure $ EmailAndUref email (SAML.UserRef issuer (emailToSAMLNameID email)) + (Nothing, Just email, Just issuer) -> pure $ EmailAndUref email (SAML.UserRef issuer (fromRight' $ emailToSAMLNameID email)) (Nothing, Just email, Nothing) -> pure $ EmailOnly email (Nothing, Nothing, _) -> throwError "user has neither ssoIdentity nor userEmail" diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index 0ce2a38a2fd..e46aa690482 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -4,6 +4,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} @@ -32,10 +33,11 @@ -- | Doing operations with users via SCIM. -- -- Provides a 'Scim.Class.User.UserDB' instance. +-- Exported functions are used in tests. module Spar.Scim.User ( validateScimUser', synthesizeScimUser, - toScimStoredUser', + toScimStoredUser, mkValidExternalId, scimFindUserByEmail, deleteScimUser, @@ -102,6 +104,7 @@ import qualified Web.Scim.Schema.Meta as Scim import qualified Web.Scim.Schema.ResourceType as Scim import qualified Web.Scim.Schema.User as Scim import qualified Web.Scim.Schema.User as Scim.User (schemas) +-- import qualified Web.Scim.Schema.User.Email as Scim.Email import qualified Wire.API.Team.Member as Member import Wire.API.Team.Role import Wire.API.User @@ -253,10 +256,11 @@ validateHandle txt = case parseHandle txt of -- configurable on a per-team basis in the future, to accomodate different legal uses of -- @externalId@ by different teams. -- --- __Emails and phone numbers:__ we'd like to ensure that only verified emails and phone --- numbers end up in our database, and implementing verification requires design decisions +-- __Email verification:__ we'd like to ensure that only verified emails numbers end up +-- in our database, and implementing verification requires design decisions -- that we haven't made yet. We store them in our SCIM blobs, but don't syncronize them with -- Brig. See . +-- FUTUREWORK(elland): verify with fisx if this still applies. validateScimUser' :: forall r. ( Member (Error Scim.ScimError) r, @@ -291,7 +295,9 @@ validateScimUser' errloc midp richInfoLimit user = do let active = Scim.active user lang <- maybe (throw $ badRequest "Could not parse language. Expected format is ISO 639-1.") pure $ mapM parseLanguage $ Scim.preferredLanguage user mRole <- validateRole user - pure $ ST.ValidScimUser veid handl uname richInfo (maybe True Scim.unScimBool active) (flip Locale Nothing <$> lang) mRole + + -- FUTUREWORK(elland): Handle the SCIM emails field. + pure $ ST.ValidScimUser veid handl uname [] richInfo (maybe True Scim.unScimBool active) (flip Locale Nothing <$> lang) mRole where validRoleNames :: Text validRoleNames = @@ -364,7 +370,7 @@ mkValidExternalId Nothing (Just extid) = do Scim.badRequest Scim.InvalidValue (Just "externalId must be a valid email address or (if there is a SAML IdP) a valid SAML NameID") - maybe (throw err) (pure . ST.EmailOnly) $ parseEmail extid + maybe (throw err) (pure . ST.EmailOnly) $ emailAddressText extid mkValidExternalId (Just idp) (Just extid) = do let issuer = idp ^. SAML.idpMetadata . SAML.edIssuer subject <- validateSubject extid @@ -382,7 +388,7 @@ mkValidExternalId (Just idp) (Just extid) = do -- The entry in spar.user_v2 does not exist yet during user -- creation. So we just assume that it will exist momentarily. pure uref - pure $ case parseEmail extid of + pure $ case emailAddressText extid of Just email -> ST.EmailAndUref email indexedUref Nothing -> ST.UrefOnly indexedUref where @@ -424,13 +430,14 @@ logScim context postcontext action = Logger.info $ context . postcontext x . Log.msg @Text "call without exception" pure (Right x) -logEmail :: Email -> (Msg -> Msg) +logEmail :: EmailAddress -> (Msg -> Msg) logEmail email = Log.field "email_sha256" (sha256String . Text.pack . show $ email) logVSU :: ST.ValidScimUser -> (Msg -> Msg) -logVSU (ST.ValidScimUser veid handl _name _richInfo _active _lang _role) = - maybe id logEmail (veidEmail veid) +logVSU (ST.ValidScimUser veid handl _name _emails _richInfo _active _lang _role) = + -- FUTUREWORK(elland): Take SCIM emails field into account. + maybe id logEmail (veidToEmail veid) . logHandle handl logTokenInfo :: ScimTokenInfo -> (Msg -> Msg) @@ -442,10 +449,14 @@ logScimUserId = logUser . Scim.id . Scim.thing logScimUserIds :: Scim.ListResponse (Scim.StoredUser ST.SparTag) -> (Msg -> Msg) logScimUserIds lresp = foldl' (.) id (logScimUserId <$> Scim.resources lresp) -veidEmail :: ST.ValidExternalId -> Maybe Email -veidEmail (ST.EmailAndUref email _) = Just email -veidEmail (ST.UrefOnly _) = Nothing -veidEmail (ST.EmailOnly email) = Just email +veidToEmail :: ST.ValidExternalId -> Maybe EmailAddress +veidToEmail (ST.EmailAndUref email _) = Just email +veidToEmail (ST.UrefOnly _) = Nothing +veidToEmail (ST.EmailOnly email) = Just email + +-- FUTUREWORK(elland): Account for SCIM emails field, if relevant here. +vsUserEmail :: ST.ValidScimUser -> Maybe EmailAddress +vsUserEmail usr = veidToEmail usr.externalId -- in ScimTokenHash (cs @ByteString @Text (convertToBase Base64 digest)) @@ -483,7 +494,7 @@ createValidScimUser :: ScimTokenInfo -> ST.ValidScimUser -> m (Scim.StoredUser ST.SparTag) -createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid handl name richInfo _active language role) = +createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser {..}) = logScim ( logFunction "Spar.Scim.User.createValidScimUser" . logVSU vsu @@ -491,7 +502,7 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid ) logScimUserId $ do - lift (ScimExternalIdStore.lookupStatus stiTeam veid) >>= \case + lift (ScimExternalIdStore.lookupStatus stiTeam externalId) >>= \case Just (buid, ScimUserCreated) -> -- If the user has been created, but can't be found in brig anymore, -- the invitation has timed out and the user has been deleted on brig's side. @@ -499,10 +510,10 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid -- HALF-CREATED ACCOUNT HAS BEEN GARBAGE-COLLECTED. -- Otherwise we return a conflict error. lift (BrigAccess.getStatusMaybe buid) >>= \case - Just Active -> throwError (externalIdTakenError ("user with status Active exists: " <> Text.pack (show (veid, buid)))) - Just Suspended -> throwError (externalIdTakenError ("user with status Suspended exists" <> Text.pack (show (veid, buid)))) - Just Ephemeral -> throwError (externalIdTakenError ("user with status Ephemeral exists" <> Text.pack (show (veid, buid)))) - Just PendingInvitation -> throwError (externalIdTakenError ("user with status PendingInvitation exists" <> Text.pack (show (veid, buid)))) + Just Active -> throwError (externalIdTakenError ("user with status Active exists: " <> Text.pack (show (externalId, buid)))) + Just Suspended -> throwError (externalIdTakenError ("user with status Suspended exists" <> Text.pack (show (externalId, buid)))) + Just Ephemeral -> throwError (externalIdTakenError ("user with status Ephemeral exists" <> Text.pack (show (externalId, buid)))) + Just PendingInvitation -> throwError (externalIdTakenError ("user with status PendingInvitation exists" <> Text.pack (show (externalId, buid)))) Just Deleted -> incompleteUserCreationCleanUp buid Nothing -> incompleteUserCreationCleanUp buid Just (buid, ScimUserCreating) -> @@ -511,13 +522,13 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid -- ensure uniqueness constraints of all affected identifiers. -- {if we crash now, retry POST will just work} - assertExternalIdUnused stiTeam veid - assertHandleUnused handl + assertExternalIdUnused stiTeam externalId + assertHandleUnused handle -- {if we crash now, retry POST will just work, or user gets told the handle -- is already in use and stops POSTing} buid <- lift $ Id <$> Random.uuid - lift $ ScimExternalIdStore.insertStatus stiTeam veid buid ScimUserCreating + lift $ ScimExternalIdStore.insertStatus stiTeam externalId buid ScimUserCreating -- Generate a UserId will be used both for scim user in spar and for brig. lift $ do @@ -526,13 +537,13 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid -- FUTUREWORK: outsource this and some other fragments from -- `createValidScimUser` into a function `createValidScimUserBrig` similar -- to `createValidScimUserSpar`? - void $ BrigAccess.createSAML uref buid stiTeam name ManagedByScim (Just handl) (Just richInfo) language (fromMaybe defaultRole role) + void $ BrigAccess.createSAML uref buid stiTeam name ManagedByScim (Just handle) (Just richInfo) locale (fromMaybe defaultRole role) ) ( \email -> do - void $ BrigAccess.createNoSAML email buid stiTeam name language (fromMaybe defaultRole role) - BrigAccess.setHandle buid handl -- FUTUREWORK: possibly do the same one req as we do for saml? + void $ BrigAccess.createNoSAML email buid stiTeam name locale (fromMaybe defaultRole role) + BrigAccess.setHandle buid handle -- FUTUREWORK: possibly do the same one req as we do for saml? ) - veid + externalId Logger.debug ("createValidScimUser: brig says " <> show buid) BrigAccess.setRichInfo buid richInfo @@ -549,24 +560,25 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid acc <- lift (BrigAccess.getAccount Brig.WithPendingInvitations buid) >>= maybe (throwError $ Scim.serverError "Server error: user vanished") pure - synthesizeStoredUser acc veid + synthesizeStoredUser acc externalId lift $ Logger.debug ("createValidScimUser: spar says " <> show storedUser) -- {(arianvp): these two actions we probably want to make transactional.} - createValidScimUserSpar stiTeam buid storedUser veid + createValidScimUserSpar stiTeam buid storedUser externalId -- If applicable, trigger email validation procedure on brig. - lift $ Spar.App.validateEmail (Just stiTeam) buid `mapM_` veidEmail veid + -- FUTUREWORK: validate fallback emails? + lift $ Spar.App.validateEmail (Just stiTeam) buid `mapM_` vsUserEmail vsu -- TODO: suspension via scim is brittle, and may leave active users behind: if we don't -- reach the following line due to a crash, the user will be active. lift $ do old <- BrigAccess.getStatus buid - let new = ST.scimActiveFlagToAccountStatus old (Scim.unScimBool <$> active) - active = Scim.active . Scim.value . Scim.thing $ storedUser + let new = ST.scimActiveFlagToAccountStatus old (Scim.unScimBool <$> active') + active' = Scim.active . Scim.value . Scim.thing $ storedUser when (new /= old) $ BrigAccess.setStatus buid new - lift $ ScimExternalIdStore.insertStatus stiTeam veid buid ScimUserCreated + lift $ ScimExternalIdStore.insertStatus stiTeam externalId buid ScimUserCreated pure storedUser where incompleteUserCreationCleanUp :: UserId -> Scim.ScimHandler (Sem r) () @@ -644,12 +656,12 @@ updateValidScimUser tokinfo@ScimTokenInfo {stiTeam} uid nvsu = -- if the locale of the new valid SCIM user is not set, -- we set it to default value from brig defLocale <- lift BrigAccess.getDefaultUserLocale - let newValidScimUser = nvsu {ST._vsuLocale = ST._vsuLocale nvsu <|> Just defLocale} + let newValidScimUser = nvsu {ST.locale = ST.locale nvsu <|> Just defLocale} -- assertions about new valid scim user that cannot be checked in 'validateScimUser' because -- they differ from the ones in 'createValidScimUser'. - assertExternalIdNotUsedElsewhere stiTeam (newValidScimUser ^. ST.vsuExternalId) uid - assertHandleNotUsedElsewhere uid (newValidScimUser ^. ST.vsuHandle) + assertExternalIdNotUsedElsewhere stiTeam (newValidScimUser.externalId) uid + assertHandleNotUsedElsewhere uid (newValidScimUser.handle) if oldValidScimUser == newValidScimUser then pure oldScimStoredUser @@ -658,29 +670,29 @@ updateValidScimUser tokinfo@ScimTokenInfo {stiTeam} uid nvsu = newScimStoredUser :: Scim.StoredUser ST.SparTag <- updScimStoredUser (synthesizeScimUser newValidScimUser) oldScimStoredUser - when (oldValidScimUser ^. ST.vsuExternalId /= newValidScimUser ^. ST.vsuExternalId) $ - updateVsuUref stiTeam uid (oldValidScimUser ^. ST.vsuExternalId) (newValidScimUser ^. ST.vsuExternalId) + when (oldValidScimUser.externalId /= newValidScimUser.externalId) $ + updateVsuUref stiTeam uid (oldValidScimUser.externalId) (newValidScimUser.externalId) - when (newValidScimUser ^. ST.vsuName /= oldValidScimUser ^. ST.vsuName) $ - BrigAccess.setName uid (newValidScimUser ^. ST.vsuName) + when (newValidScimUser.name /= oldValidScimUser.name) $ + BrigAccess.setName uid (newValidScimUser.name) - when (oldValidScimUser ^. ST.vsuHandle /= newValidScimUser ^. ST.vsuHandle) $ - BrigAccess.setHandle uid (newValidScimUser ^. ST.vsuHandle) + when (oldValidScimUser.handle /= newValidScimUser.handle) $ + BrigAccess.setHandle uid (newValidScimUser.handle) - when (oldValidScimUser ^. ST.vsuRichInfo /= newValidScimUser ^. ST.vsuRichInfo) $ - BrigAccess.setRichInfo uid (newValidScimUser ^. ST.vsuRichInfo) + when (oldValidScimUser.richInfo /= newValidScimUser.richInfo) $ + BrigAccess.setRichInfo uid (newValidScimUser.richInfo) - when (oldValidScimUser ^. ST.vsuLocale /= newValidScimUser ^. ST.vsuLocale) $ do - BrigAccess.setLocale uid (newValidScimUser ^. ST.vsuLocale) + when (oldValidScimUser.locale /= newValidScimUser.locale) $ do + BrigAccess.setLocale uid (newValidScimUser.locale) - forM_ (newValidScimUser ^. ST.vsuRole) $ \newRole -> do - when (oldValidScimUser ^. ST.vsuRole /= Just newRole) $ do + forM_ (newValidScimUser.role) $ \newRole -> do + when (oldValidScimUser.role /= Just newRole) $ do GalleyAccess.updateTeamMember uid stiTeam newRole BrigAccess.getStatusMaybe uid >>= \case Nothing -> pure () Just old -> do - let new = ST.scimActiveFlagToAccountStatus old (Just $ newValidScimUser ^. ST.vsuActive) + let new = ST.scimActiveFlagToAccountStatus old (Just $ newValidScimUser.active) when (new /= old) $ BrigAccess.setStatus uid new ScimUserTimesStore.write newScimStoredUser @@ -698,7 +710,8 @@ updateVsuUref :: ST.ValidExternalId -> Sem r () updateVsuUref team uid old new = do - case (veidEmail old, veidEmail new) of + -- FUTUREWORK(elland): Account for SCIM emails field. + case (veidToEmail old, veidToEmail new) of (mo, mn@(Just email)) | mo /= mn -> Spar.App.validateEmail (Just team) uid email _ -> pure () @@ -707,7 +720,7 @@ updateVsuUref team uid old new = do BrigAccess.setVeid uid new -toScimStoredUser' :: +toScimStoredUser :: (HasCallStack) => UTCTimeMillis -> UTCTimeMillis -> @@ -715,7 +728,7 @@ toScimStoredUser' :: UserId -> Scim.User ST.SparTag -> Scim.StoredUser ST.SparTag -toScimStoredUser' createdAt lastChangedAt baseuri uid usr = +toScimStoredUser createdAt lastChangedAt baseuri uid usr = Scim.WithMeta meta $ Scim.WithId uid $ usr {Scim.User.schemas = ST.userSchemas} @@ -949,7 +962,7 @@ synthesizeStoredUser usr veid = . logUser (userId . accountUser $ usr) . maybe id logHandle (userHandle . accountUser $ usr) . maybe id logTeam (userTeam . accountUser $ usr) - . maybe id logEmail (veidEmail veid) + . maybe id logEmail (veidToEmail veid) ) logScimUserId $ do @@ -979,12 +992,14 @@ synthesizeStoredUser usr veid = let (createdAt, lastUpdatedAt) = fromMaybe (now, now) accessTimes handle <- lift $ Brig.giveDefaultHandle (accountUser usr) + let emails = catMaybesToList (emailIdentity <$> usr.accountUser.userIdentity) storedUser <- synthesizeStoredUser' uid veid (userDisplayName (accountUser usr)) + emails handle richInfo accStatus @@ -1002,9 +1017,11 @@ synthesizeStoredUser usr veid = maybe (pure defaultRole) (\tid -> tmRoleOrDefault <$> GalleyAccess.getTeamMember tid (userId $ accountUser usr)) (userTeam $ accountUser usr) synthesizeStoredUser' :: + (MonadError Scim.ScimError m) => UserId -> ST.ValidExternalId -> Name -> + [EmailAddress] -> Handle -> RI.RichInfo -> AccountStatus -> @@ -1013,33 +1030,34 @@ synthesizeStoredUser' :: URIBS.URI -> Locale -> Maybe Role -> - (MonadError Scim.ScimError m) => m (Scim.StoredUser ST.SparTag) -synthesizeStoredUser' uid veid dname handle richInfo accStatus createdAt lastUpdatedAt baseuri locale mbRole = do + m (Scim.StoredUser ST.SparTag) +synthesizeStoredUser' uid veid dname _emails handle richInfo accStatus createdAt lastUpdatedAt baseuri locale mbRole = do let scimUser :: Scim.User ST.SparTag scimUser = synthesizeScimUser ST.ValidScimUser - { ST._vsuExternalId = veid, - ST._vsuHandle = handle {- 'Maybe' there is one in @usr@, but we want the type - checker to make sure this exists, so we add it here - redundantly, without the 'Maybe'. -}, - ST._vsuName = dname, - ST._vsuRichInfo = richInfo, - ST._vsuActive = ST.scimActiveFlagFromAccountStatus accStatus, - ST._vsuLocale = Just locale, - ST._vsuRole = mbRole + { ST.externalId = veid, + ST.handle = handle {- 'Maybe' there is one in @usr@, but we want the type + checker to make sure this exists, so we add it here + redundantly, without the 'Maybe'. -}, + ST.emails = [], -- FUTUREWORK(elland): Account for SCIM emails field. + ST.name = dname, + ST.richInfo = richInfo, + ST.active = ST.scimActiveFlagFromAccountStatus accStatus, + ST.locale = Just locale, + ST.role = mbRole } - pure $ toScimStoredUser' createdAt lastUpdatedAt baseuri uid (normalizeLikeStored scimUser) + pure $ toScimStoredUser createdAt lastUpdatedAt baseuri uid (normalizeLikeStored scimUser) synthesizeScimUser :: ST.ValidScimUser -> Scim.User ST.SparTag synthesizeScimUser info = - let userName = info ^. ST.vsuHandle . to fromHandle - in (Scim.empty @ST.SparTag ST.userSchemas userName (ST.ScimUserExtra (info ^. ST.vsuRichInfo))) - { Scim.externalId = Brig.renderValidExternalId $ info ^. ST.vsuExternalId, - Scim.displayName = Just $ fromName (info ^. ST.vsuName), - Scim.active = Just . Scim.ScimBool $ info ^. ST.vsuActive, - Scim.preferredLanguage = lan2Text . lLanguage <$> info ^. ST.vsuLocale, + let userName = info.handle.fromHandle + in (Scim.empty @ST.SparTag ST.userSchemas userName (ST.ScimUserExtra info.richInfo)) + { Scim.externalId = Brig.renderValidExternalId info.externalId, + Scim.displayName = Just $ fromName info.name, + Scim.active = Just . Scim.ScimBool $ info.active, + Scim.preferredLanguage = lan2Text . lLanguage <$> info.locale, Scim.roles = maybe [] @@ -1048,11 +1066,10 @@ synthesizeScimUser info = . toStrict . toByteString ) - (info ^. ST.vsuRole) + (info.role) } -- TODO: now write a test, either in /integration or in spar, whichever is easier. (spar) - getUserById :: forall r. ( Member BrigAccess r, @@ -1159,7 +1176,7 @@ scimFindUserByEmail mIdpConfig stiTeam email = do Nothing -> maybe (pure Nothing) withEmailOnly $ Brig.urefToEmail uref Just uid -> pure (Just uid) - withEmailOnly :: Email -> Sem r (Maybe UserId) + withEmailOnly :: EmailAddress -> Sem r (Maybe UserId) withEmailOnly eml = maybe inbrig (pure . Just) =<< inspar where -- FUTUREWORK: we could also always lookup brig, that's simpler and possibly faster, @@ -1182,58 +1199,3 @@ logFilter (FilterAttrCompare attr op val) = "sha256 " <> sha256String s <> (if isJust (UUID.fromText s) then " original is a UUID" else "") - -{- TODO: might be useful later. -~~~~~~~~~~~~~~~~~~~~~~~~~ - --- | Parse a name from a user profile into an SCIM name (Okta wants given --- name and last name, so we break our names up to satisfy Okta). --- --- TODO: use the same algorithm as Wire clients use. -toScimName :: Name -> Scim.Name -toScimName (Name name) = - Scim.Name - { Scim.formatted = Just name - , Scim.givenName = Just first - , Scim.familyName = if Text.null rest then Nothing else Just rest - , Scim.middleName = Nothing - , Scim.honorificPrefix = Nothing - , Scim.honorificSuffix = Nothing - } - where - (first, Text.drop 1 -> rest) = Text.breakOn " " name - --- | Convert from the Wire phone type to the SCIM phone type. -toScimPhone :: Phone -> Scim.Phone -toScimPhone (Phone phone) = - Scim.Phone - { Scim.typ = Nothing - , Scim.value = Just phone - } - --- | Convert from the Wire email type to the SCIM email type. -toScimEmail :: Email -> Scim.Email -toScimEmail (Email eLocal eDomain) = - Scim.Email - { Scim.typ = Nothing - , Scim.value = Scim.EmailAddress2 - (unsafeEmailAddress (encodeUtf8 eLocal) (encodeUtf8 eDomain)) - , Scim.primary = Just True - } - --} - --- Note [error handling] --- ~~~~~~~~~~~~~~~~~ --- --- FUTUREWORK: There are two problems with error handling here: --- --- 1. We want all errors originating from SCIM handlers to be thrown as SCIM --- errors, not as Spar errors. Currently errors thrown from things like --- 'getTeamMembers' will look like Spar errors and won't be wrapped into --- the 'ScimError' type. This might or might not be important, depending --- on what is expected by apps that use the SCIM interface. --- --- 2. We want generic error descriptions in response bodies, while still --- logging nice error messages internally. The current messages might --- be giving too many internal details away. diff --git a/services/spar/src/Spar/Sem/BrigAccess.hs b/services/spar/src/Spar/Sem/BrigAccess.hs index 1936116030f..ebbc86d7ee4 100644 --- a/services/spar/src/Spar/Sem/BrigAccess.hs +++ b/services/spar/src/Spar/Sem/BrigAccess.hs @@ -63,11 +63,11 @@ import Wire.API.User.Scim (ValidExternalId (..)) data BrigAccess m a where CreateSAML :: SAML.UserRef -> UserId -> TeamId -> Name -> ManagedBy -> Maybe Handle -> Maybe RichInfo -> Maybe Locale -> Role -> BrigAccess m UserId - CreateNoSAML :: Email -> UserId -> TeamId -> Name -> Maybe Locale -> Role -> BrigAccess m UserId - UpdateEmail :: UserId -> Email -> BrigAccess m () + CreateNoSAML :: EmailAddress -> UserId -> TeamId -> Name -> Maybe Locale -> Role -> BrigAccess m UserId + UpdateEmail :: UserId -> EmailAddress -> BrigAccess m () GetAccount :: HavePendingInvitations -> UserId -> BrigAccess m (Maybe UserAccount) GetByHandle :: Handle -> BrigAccess m (Maybe UserAccount) - GetByEmail :: Email -> BrigAccess m (Maybe UserAccount) + GetByEmail :: EmailAddress -> BrigAccess m (Maybe UserAccount) SetName :: UserId -> Name -> BrigAccess m () SetHandle :: UserId -> Handle {- not 'HandleUpdate'! -} -> BrigAccess m () SetManagedBy :: UserId -> ManagedBy -> BrigAccess m () diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore.hs index 604c089d393..5a2ea23f247 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore.hs @@ -32,13 +32,13 @@ import Imports (Maybe, Show) import Polysemy import Polysemy.Check (deriveGenericK) import Spar.Scim.Types -import Wire.API.User.Identity (Email) +import Wire.API.User.Identity import Wire.API.User.Scim data ScimExternalIdStore m a where - Insert :: TeamId -> Email -> UserId -> ScimExternalIdStore m () - Lookup :: TeamId -> Email -> ScimExternalIdStore m (Maybe UserId) - Delete :: TeamId -> Email -> ScimExternalIdStore m () + Insert :: TeamId -> EmailAddress -> UserId -> ScimExternalIdStore m () + Lookup :: TeamId -> EmailAddress -> ScimExternalIdStore m (Maybe UserId) + Delete :: TeamId -> EmailAddress -> ScimExternalIdStore m () -- NB: the fact that we are using `Email` in some cases here and `ValidExternalId` in others has historical reasons (this table was only used for non-saml accounts in the past, now it is used for *all* scim-managed accounts). the interface would work equally well with just `Text` here (for unvalidated scim external id). InsertStatus :: TeamId -> ValidExternalId -> UserId -> ScimUserCreationStatus -> ScimExternalIdStore m () LookupStatus :: TeamId -> ValidExternalId -> ScimExternalIdStore m (Maybe (UserId, ScimUserCreationStatus)) diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs index 73c192dafdf..53734a13222 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs @@ -52,7 +52,7 @@ scimExternalIdStoreToCassandra = -- 'UserId' here. (Note that since there is no associated IdP, the externalId is required to -- be an email address, so we enforce that in the type signature, even though we only use it -- as a 'Text'.) -insertScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Email -> UserId -> m () +insertScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> EmailAddress -> UserId -> m () insertScimExternalId tid (fromEmail -> email) uid = retry x5 . write insert $ params LocalQuorum (tid, email, uid) where @@ -60,14 +60,14 @@ insertScimExternalId tid (fromEmail -> email) uid = insert = "INSERT INTO scim_external (team, external_id, user) VALUES (?, ?, ?)" -- | The inverse of 'insertScimExternalId'. -lookupScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Email -> m (Maybe UserId) +lookupScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> EmailAddress -> m (Maybe UserId) lookupScimExternalId tid (fromEmail -> email) = runIdentity <$$> (retry x1 . query1 sel $ params LocalQuorum (tid, email)) where sel :: PrepQuery R (TeamId, Text) (Identity UserId) sel = "SELECT user FROM scim_external WHERE team = ? and external_id = ?" -- | The other inverse of 'insertScimExternalId' :). -deleteScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Email -> m () +deleteScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> EmailAddress -> m () deleteScimExternalId tid (fromEmail -> email) = retry x5 . write delete $ params LocalQuorum (tid, email) where diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 07670e1d3f4..911376524aa 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -94,7 +94,7 @@ import Wire.API.User import Wire.API.User.Client import Wire.API.User.Client.Prekey import Wire.API.User.IdentityProvider -import Wire.API.User.Scim +import Wire.API.User.Scim hiding (handle) spec :: SpecWith TestEnv spec = do @@ -1274,7 +1274,7 @@ specScimAndSAML = do userid' <- getUserIdViaRef' userref liftIO $ ('i', userid') `shouldBe` ('i', Just userid) userssoid <- getSsoidViaSelf' userid - liftIO $ ('r', preview veidUref <$$> (Intra.veidFromUserSSOId <$> userssoid)) `shouldBe` ('r', Just (Right (Just userref))) + liftIO $ ('r', veidUref <$$> (Intra.veidFromUserSSOId <$> userssoid)) `shouldBe` ('r', Just (Right (Just userref))) -- login a user for the first time with the scim-supplied credentials authnreq <- negotiateAuthnRequest idp spmeta <- getTestSPMetadata tid diff --git a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs index 6f7983a10b9..7d2b945b95f 100644 --- a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs @@ -138,7 +138,7 @@ testCreateTokenWithVerificationCode = do listUsers_ (Just token) (Just fltr) (env ^. teSpar) !!! const 200 === statusCode where - requestVerificationCode :: BrigReq -> Email -> Public.VerificationAction -> TestSpar ResponseLBS + requestVerificationCode :: BrigReq -> EmailAddress -> Public.VerificationAction -> TestSpar ResponseLBS requestVerificationCode brig email action = do call $ post (brig . paths ["verification-code", "send"] . contentJson . json (Public.SendVerificationCode action email)) diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index e138bc76db5..57184a319d3 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -131,7 +131,7 @@ specImportToScimFromSAML = setSamlEmailValidation teamid valemail -- saml-auto-provision a new user - (usr :: Scim.User.User SparTag, email :: Email) <- do + (usr :: Scim.User.User SparTag, email :: EmailAddress) <- do (usr, email) <- randomScimUserWithEmail pure ( -- when auto-provisioning via saml, user display name is set to saml name id. @@ -141,7 +141,7 @@ specImportToScimFromSAML = (uref :: SAML.UserRef, uid :: UserId) <- do let uref = SAML.UserRef tenant subj - subj = emailToSAMLNameID email + subj = fromRight' $ emailToSAMLNameID email tenant = idp ^. SAML.idpMetadata . SAML.edIssuer (Just !uid) <- createViaSaml idp privCreds uref samlUserShouldSatisfy uref isJust @@ -215,7 +215,7 @@ specImportToScimFromInvitation = env <- ask call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) - invite :: (HasCallStack) => UserId -> TeamId -> TestSpar (UserId, Email) + invite :: (HasCallStack) => UserId -> TeamId -> TestSpar (UserId, EmailAddress) invite owner teamid = do env <- ask email <- randomEmail @@ -238,7 +238,7 @@ specImportToScimFromInvitation = Maybe (SAML.IdPConfig User.WireIdP) -> TeamId -> UserId -> - Email -> + EmailAddress -> TestSpar (Scim.UserC.StoredUser SparTag) reProvisionWithScim changeHandle mbidp teamid userid email = do tok :: ScimToken <- do @@ -267,10 +267,10 @@ specImportToScimFromInvitation = (SAML.IdPConfig User.WireIdP, SAML.SignPrivCreds) -> Email -> UserId -> TestSpar () + signInWithSaml :: (HasCallStack) => (SAML.IdPConfig User.WireIdP, SAML.SignPrivCreds) -> EmailAddress -> UserId -> TestSpar () signInWithSaml (idp, privCreds) email userid = do let uref = SAML.UserRef tenant subj - subj = emailToSAMLNameID email + subj = fromRight' $ emailToSAMLNameID email tenant = idp ^. SAML.idpMetadata . SAML.edIssuer mbUid <- createViaSaml idp privCreds uref liftIO $ mbUid `shouldBe` Just userid @@ -294,7 +294,7 @@ specImportToScimFromInvitation = let scimUsr = Scim.value (Scim.thing storedUsr) uid = Scim.id (Scim.thing storedUsr) handle = fromRight undefined . parseHandleEither $ Scim.User.userName scimUsr - email = fromJust . parseEmail . fromJust . Scim.User.externalId $ scimUsr + email = fromJust . emailAddressText . fromJust . Scim.User.externalId $ scimUsr Right idpissuer = idp ^. SAML.idpMetadata . SAML.edIssuer . SAML.fromIssuer . to mkHttpsUrl Just samlNameID = Scim.User.externalId scimUsr Just scimExternalId = Scim.User.externalId scimUsr @@ -324,7 +324,7 @@ specImportToScimFromInvitation = signInWithSaml (idp, privcreds) email userid checkCsvDownload ownerid teamid idp storedusr -findUserByEmail :: ScimToken -> Email -> TestSpar (Scim.UserC.StoredUser SparTag) +findUserByEmail :: ScimToken -> EmailAddress -> TestSpar (Scim.UserC.StoredUser SparTag) findUserByEmail tok email = do let fltr = filterBy "externalid" (fromEmail email) resp <- listUsers_ (Just tok) (Just fltr) =<< view teSpar @@ -338,7 +338,7 @@ assertSparCassandraUref (uref, urefAnswer) = do liftIO . (`shouldBe` urefAnswer) =<< runSpar (SAMLUserStore.get uref) -assertSparCassandraScim :: (HasCallStack) => ((TeamId, Email), Maybe UserId) -> TestSpar () +assertSparCassandraScim :: (HasCallStack) => ((TeamId, EmailAddress), Maybe UserId) -> TestSpar () assertSparCassandraScim ((teamid, email), scimAnswer) = do liftIO . (`shouldBe` scimAnswer) =<< runSpar (ScimExternalIdStore.lookup teamid email) @@ -361,7 +361,7 @@ assertBrigCassandra uid uref usr (valemail, emailValidated) managedBy = do email = case (valemail, emailValidated) of (Feature.FeatureStatusEnabled, True) -> - Just . fromJust . parseEmail . fromJust . Scim.User.externalId $ usr + Just . fromJust . emailAddressText . fromJust . Scim.User.externalId $ usr _ -> Nothing @@ -1132,7 +1132,7 @@ testCreateUserTimeout = do Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) pure (scimStoredUser, inv, inviteeCode) - searchUser :: (HasCallStack) => Spar.Types.ScimToken -> Scim.User.User tag -> Email -> Bool -> TestSpar () + searchUser :: (HasCallStack) => Spar.Types.ScimToken -> Scim.User.User tag -> EmailAddress -> Bool -> TestSpar () searchUser tok scimUser email shouldSucceed = do let handle = fromJust . parseHandle . Scim.User.userName $ scimUser tryquery qry = @@ -1793,7 +1793,7 @@ lookupByValidExternalId tid = Left err -> error $ show err ) -registerUser :: BrigReq -> TeamId -> Email -> TestSpar () +registerUser :: BrigReq -> TeamId -> EmailAddress -> TestSpar () registerUser brig tid email = do let r = call $ get (brig . path "/i/teams/invitations/by-email" . queryItem "email" (toByteString' email)) inv <- responseJsonError =<< r Just (Locale (Language EN) Nothing)} + let scimUserWithDefLocale = validScimUser {Spar.Types.locale = Spar.Types.locale validScimUser <|> Just (Locale (Language EN) Nothing)} brigUser `userShouldMatch` scimUserWithDefLocale testUpdateUserRole :: TestSpar () @@ -2269,7 +2269,7 @@ specAzureQuirks = do specEmailValidation :: SpecWith TestEnv specEmailValidation = do describe "email validation" $ do - let setup :: (HasCallStack) => Bool -> TestSpar (UserId, Email) + let setup :: (HasCallStack) => Bool -> TestSpar (UserId, EmailAddress) setup enabled = do (tok, (_ownerid, teamid, idp)) <- registerIdPAndScimToken if enabled @@ -2280,8 +2280,7 @@ specEmailValidation = do veid <- runSpar . runScimErrorUnsafe $ mkValidExternalId (Just idp) (Scim.User.externalId . Scim.value . Scim.thing $ scimStoredUser) - uid :: UserId <- - getUserIdViaRef (veid ^?! veidUref) + uid :: UserId <- getUserIdViaRef $ fromJust (veidUref veid) brig <- view teBrig -- we intentionally activate the email even if it's not set up to work, to make sure -- it doesn't if the feature is disabled. diff --git a/services/spar/test-integration/Util/Activation.hs b/services/spar/test-integration/Util/Activation.hs index 143e5adbdf5..58e1f05125c 100644 --- a/services/spar/test-integration/Util/Activation.hs +++ b/services/spar/test-integration/Util/Activation.hs @@ -30,7 +30,7 @@ import Wire.API.User.Identity getActivationCode :: (MonadHttp m, MonadIO m) => BrigReq -> - Email -> + EmailAddress -> m (Maybe (ActivationKey, ActivationCode)) getActivationCode brig e = do let qry = queryItem "email" . toByteString' $ e diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 250ef14efd4..b53493cba17 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -36,6 +36,7 @@ module Util.Core -- * Test helpers it, + fit, pending, pendingWith, shouldRespondWith, @@ -184,7 +185,7 @@ import qualified Spar.Sem.SAMLUserStore as SAMLUserStore import qualified Spar.Sem.ScimExternalIdStore as ScimExternalIdStore import qualified System.Logger.Extended as Log import System.Random (randomRIO) -import Test.Hspec hiding (it, pending, pendingWith, xit) +import Test.Hspec hiding (fit, it, pending, pendingWith, xit) import qualified Test.Hspec import qualified Text.XML as XML import qualified Text.XML.Cursor as XML @@ -292,6 +293,15 @@ it :: SpecWith TestEnv it msg bdy = Test.Hspec.it msg $ runReaderT bdy +fit :: + (HasCallStack) => + -- or, more generally: + -- MonadIO m, Example (TestEnv -> m ()), Arg (TestEnv -> m ()) ~ TestEnv + String -> + TestSpar () -> + SpecWith TestEnv +fit msg bdy = Test.Hspec.fit msg $ runReaderT bdy + pending :: (HasCallStack, MonadIO m) => m () pending = liftIO Test.Hspec.pending @@ -394,7 +404,7 @@ inviteAndRegisterUser :: BrigReq -> UserId -> TeamId -> - Email -> + EmailAddress -> m User inviteAndRegisterUser brig u tid inviteeEmail = do let invite = stdInvitationRequest inviteeEmail @@ -415,10 +425,10 @@ inviteAndRegisterUser brig u tid inviteeEmail = do unless (selfTeam == Just tid) $ error "Team ID in self profile and team table do not match" pure invitee where - accept' :: User.Email -> User.InvitationCode -> RequestBody + accept' :: EmailAddress -> User.InvitationCode -> RequestBody accept' email code = acceptWithName (User.Name "Bob") email code -- - acceptWithName :: User.Name -> User.Email -> User.InvitationCode -> RequestBody + acceptWithName :: User.Name -> EmailAddress -> User.InvitationCode -> RequestBody acceptWithName name email code = RequestBodyLBS . Aeson.encode $ object @@ -608,10 +618,10 @@ zAuthAccess u c = header "Z-Type" "access" . zUser u . zConn c newTeam :: Galley.BindingNewTeam newTeam = Galley.BindingNewTeam $ Galley.newNewTeam (unsafeRange "teamName") DefaultIcon -randomEmail :: (MonadIO m) => m Email +randomEmail :: (MonadIO m) => m EmailAddress randomEmail = do uid <- liftIO nextRandom - pure $ Email ("success+" <> UUID.toText uid) "simulator.amazonses.com" + pure $ User.unsafeEmailAddress ("success+" <> UUID.toASCIIBytes uid) "simulator.amazonses.com" randomUser :: (HasCallStack, MonadCatch m, MonadIO m, MonadHttp m) => BrigReq -> m User randomUser brig_ = do @@ -1204,11 +1214,11 @@ checkErrHspec :: (HasCallStack) => Int -> TestErrorLabel -> ResponseLBS -> Bool checkErrHspec status label resp = status == statusCode resp && responseJsonEither resp == Right label -- | copied from brig integration tests -stdInvitationRequest :: User.Email -> TeamInvitation.InvitationRequest +stdInvitationRequest :: EmailAddress -> TeamInvitation.InvitationRequest stdInvitationRequest = stdInvitationRequest' Nothing Nothing -- | copied from brig integration tests -stdInvitationRequest' :: Maybe User.Locale -> Maybe Role -> User.Email -> TeamInvitation.InvitationRequest +stdInvitationRequest' :: Maybe User.Locale -> Maybe Role -> EmailAddress -> TeamInvitation.InvitationRequest stdInvitationRequest' loc role email = TeamInvitation.InvitationRequest loc role Nothing email diff --git a/services/spar/test-integration/Util/Email.hs b/services/spar/test-integration/Util/Email.hs index 3d639e0c3b9..39bfc269152 100644 --- a/services/spar/test-integration/Util/Email.hs +++ b/services/spar/test-integration/Util/Email.hs @@ -43,7 +43,7 @@ changeEmailBrigCreds :: BrigReq -> Cookie -> ZAuth.Token ZAuth.Access -> - Email -> + EmailAddress -> m ResponseLBS changeEmailBrigCreds brig cky tok newEmail = do put @@ -63,7 +63,7 @@ forceCookie cky = header "Cookie" $ cookie_name cky <> "=" <> cookie_value cky activateEmail :: (MonadCatch m, MonadIO m, HasCallStack) => BrigReq -> - Email -> + EmailAddress -> (MonadHttp m) => m () activateEmail brig email = do act <- getActivationCode brig email @@ -77,7 +77,7 @@ activateEmail brig email = do failActivatingEmail :: (MonadCatch m, MonadIO m, HasCallStack) => BrigReq -> - Email -> + EmailAddress -> (MonadHttp m) => m () failActivatingEmail brig email = do act <- getActivationCode brig email @@ -86,7 +86,7 @@ failActivatingEmail brig email = do checkEmail :: (HasCallStack) => UserId -> - Maybe Email -> + Maybe EmailAddress -> TestSpar () checkEmail uid expectedEmail = do brig <- view teBrig diff --git a/services/spar/test-integration/Util/Invitation.hs b/services/spar/test-integration/Util/Invitation.hs index 8a9d0fe6490..6779a67a44d 100644 --- a/services/spar/test-integration/Util/Invitation.hs +++ b/services/spar/test-integration/Util/Invitation.hs @@ -37,12 +37,12 @@ import Util import Wire.API.Team.Invitation (Invitation (..)) import Wire.API.User -headInvitation404 :: (HasCallStack) => BrigReq -> Email -> Http () +headInvitation404 :: (HasCallStack) => BrigReq -> EmailAddress -> Http () headInvitation404 brig email = do Bilge.head (brig . path "/teams/invitations/by-email" . contentJson . queryItem "email" (toByteString' email)) !!! const 404 === statusCode -getInvitation :: (HasCallStack) => BrigReq -> Email -> Http Invitation +getInvitation :: (HasCallStack) => BrigReq -> EmailAddress -> Http Invitation getInvitation brig email = responseJsonUnsafe <$> Bilge.get @@ -70,7 +70,7 @@ getInvitationCode brig t ref = do let lbs = fromMaybe "" $ responseBody r pure $ fromByteString (maybe (error "No code?") encodeUtf8 (lbs ^? key "code" . _String)) -registerInvitation :: (HasCallStack) => Email -> Name -> InvitationCode -> Bool -> TestSpar () +registerInvitation :: (HasCallStack) => EmailAddress -> Name -> InvitationCode -> Bool -> TestSpar () registerInvitation email name inviteeCode shouldSucceed = do env <- ask let brig = env ^. teBrig @@ -84,7 +84,7 @@ registerInvitation email name inviteeCode shouldSucceed = do ) Email -> InvitationCode -> Aeson.Value +acceptWithName :: Name -> EmailAddress -> InvitationCode -> Aeson.Value acceptWithName name email code = Aeson.object [ "name" Aeson..= fromName name, diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index 0dac2a24a75..caf143ab661 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -31,6 +31,7 @@ import Data.Handle (Handle, parseHandle) import Data.Id import Data.LanguageCodes (ISO639_1 (EN)) import Data.String.Conversions +import Data.Text.Encoding (encodeUtf8) import qualified Data.Text.Lazy as Lazy import Data.Time import Data.UUID as UUID @@ -64,7 +65,7 @@ import qualified Web.Scim.Schema.User.Phone as Phone import qualified Wire.API.Team.Member as Member import Wire.API.Team.Role (Role, defaultRole) import Wire.API.User -import Wire.API.User.IdentityProvider hiding (team) +import Wire.API.User.IdentityProvider hiding (handle, team) import Wire.API.User.RichInfo import Wire.API.User.Scim @@ -171,10 +172,10 @@ randomScimUserWithSubjectAndRichInfo richInfo = do -- support externalIds that are not emails, and storing email addresses in `emails` in the -- scim schema. `randomScimUserWithEmail` is from a time where non-idp-authenticated users -- could only be provisioned with email as externalId. we should probably rework all that. -randomScimUserWithEmail :: (MonadRandom m) => m (Scim.User.User SparTag, Email) +randomScimUserWithEmail :: (MonadRandom m) => m (Scim.User.User SparTag, EmailAddress) randomScimUserWithEmail = do suffix <- cs <$> replicateM 7 (getRandomR ('0', '9')) - let email = Email ("email" <> suffix) "example.com" + let email = unsafeEmailAddress ("email" <> encodeUtf8 suffix) "example.com" externalId = fromEmail email pure ( (Scim.User.empty @SparTag userSchemas ("scimuser_" <> suffix) (ScimUserExtra mempty)) @@ -202,10 +203,10 @@ randomScimEmail = do let typ :: Maybe Text = Nothing primary :: Maybe Scim.ScimBool = Nothing -- TODO: where should we catch users with more than one -- primary email? - value :: Email.EmailAddress2 <- do + value <- do localpart <- cs <$> replicateM 15 (getRandomR ('a', 'z')) domainpart <- (<> ".com") . cs <$> replicateM 15 (getRandomR ('a', 'z')) - pure . Email.EmailAddress2 $ Email.unsafeEmailAddress localpart domainpart + pure . Email.EmailAddress $ Email.unsafeEmailAddress localpart domainpart pure Email.Email {..} randomScimPhone :: (MonadRandom m) => m Phone.Phone @@ -632,12 +633,12 @@ class IsUser u where -- is correct and don't aim to verify that name, handle, etc correspond to ones in 'vsuUser'. instance IsUser ValidScimUser where maybeUserId = Nothing - maybeHandle = Just (Just . view vsuHandle) - maybeName = Just (Just . view vsuName) - maybeTenant = Just (^? (vsuExternalId . veidUref . SAML.uidTenant)) - maybeSubject = Just (^? (vsuExternalId . veidUref . SAML.uidSubject)) - maybeScimExternalId = Just (runValidExternalIdEither Intra.urefToExternalId (Just . fromEmail) . view vsuExternalId) - maybeLocale = Just (view vsuLocale) + maybeHandle = Just (Just <$> handle) + maybeName = Just (Just <$> name) + maybeTenant = Just (fmap SAML._uidTenant . veidUref . externalId) + maybeSubject = Just (fmap SAML._uidSubject . veidUref . externalId) + maybeScimExternalId = Just (runValidExternalIdEither Intra.urefToExternalId (Just . fromEmail) . externalId) + maybeLocale = Just locale instance IsUser (WrappedScimStoredUser SparTag) where maybeUserId = Just $ scimUserId . fromWrappedScimStoredUser @@ -675,12 +676,12 @@ instance IsUser User where Intra.veidFromBrigUser usr Nothing & either (const Nothing) - (preview (veidUref . SAML.uidTenant)) + (fmap SAML._uidTenant . veidUref) maybeSubject = Just $ \usr -> Intra.veidFromBrigUser usr Nothing & either (const Nothing) - (preview (veidUref . SAML.uidSubject)) + (fmap SAML._uidSubject . veidUref) maybeScimExternalId = Just $ \usr -> Intra.veidFromBrigUser usr Nothing & either @@ -743,7 +744,7 @@ setDefaultRoleIfEmpty u = } -- this is not always correct, but hopefully for the tests that we're using it in it'll do. -scimifyBrigUserHack :: User -> Email -> User +scimifyBrigUserHack :: User -> EmailAddress -> User scimifyBrigUserHack usr email = usr { userManagedBy = ManagedByScim, diff --git a/services/spar/test/Test/Spar/Scim/UserSpec.hs b/services/spar/test/Test/Spar/Scim/UserSpec.hs index 3cbd3208669..23a1391003c 100644 --- a/services/spar/test/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test/Test/Spar/Scim/UserSpec.hs @@ -150,7 +150,7 @@ someActiveUser tokenInfo = do userPict = noPict, userAssets = [], userHandle = parseHandle "some-handle", - userIdentity = (Just . EmailIdentity . fromJust . parseEmail) "someone@wire.com", + userIdentity = (Just . EmailIdentity . fromJust . emailAddressText) "someone@wire.com", userTeam = Just $ stiTeam tokenInfo } } diff --git a/services/spar/test/Test/Spar/ScimSpec.hs b/services/spar/test/Test/Spar/ScimSpec.hs index 936e2f4f99c..212cfa7631b 100644 --- a/services/spar/test/Test/Spar/ScimSpec.hs +++ b/services/spar/test/Test/Spar/ScimSpec.hs @@ -55,7 +55,7 @@ import qualified Web.Scim.Schema.User.Name as ScimN import Wire.API.User.RichInfo spec :: Spec -spec = describe "toScimStoredUser'" $ do +spec = describe "toScimStoredUser" $ do it "works" $ do let usr :: Scim.User SparTag usr = @@ -115,7 +115,7 @@ spec = describe "toScimStoredUser'" $ do URI.ByteString.parseURI laxURIParserOptions "https://127.0.0.1/scim/v2/" uid = Id . fromJust . UUID.fromText $ "90b5ee1c-088e-11e9-9a16-73f80f483813" result :: ScimC.StoredUser SparTag - result = toScimStoredUser' now now baseuri uid usr + result = toScimStoredUser now now baseuri uid usr Scim.meta result `shouldBe` meta Scim.value (Scim.thing result) `shouldBe` usr it "roundtrips" . property $ do diff --git a/tools/db/inconsistencies/src/DanglingUserKeys.hs b/tools/db/inconsistencies/src/DanglingUserKeys.hs index 1242a1a2972..de0bc70a7a9 100644 --- a/tools/db/inconsistencies/src/DanglingUserKeys.hs +++ b/tools/db/inconsistencies/src/DanglingUserKeys.hs @@ -81,7 +81,7 @@ data Inconsistency = Inconsistency userId :: UserId, time :: Writetime UserId, status :: Maybe (WithWritetime AccountStatus), - userEmail :: Maybe (WithWritetime Email), + userEmail :: Maybe (WithWritetime EmailAddress), inconsistencyCase :: Text } deriving (Generic) @@ -112,7 +112,7 @@ getKeys = paginateC cql (paramsP LocalQuorum () pageSize) x5 cql = "SELECT key, user, writetime(user) from user_keys" parseKey :: Text -> Maybe EmailKey -parseKey t = mkEmailKey <$> parseEmail t +parseKey t = mkEmailKey <$> emailAddressText t instance Cql EmailKey where ctype = Tagged TextColumn @@ -129,7 +129,7 @@ instance Cql EmailKey where instance Aeson.ToJSON EmailKey where toJSON = Aeson.toJSON . emailKeyUniq -type UserDetailsRow = (Maybe AccountStatus, Maybe (Writetime AccountStatus), Maybe Email, Maybe (Writetime Email)) +type UserDetailsRow = (Maybe AccountStatus, Maybe (Writetime AccountStatus), Maybe EmailAddress, Maybe (Writetime EmailAddress)) getUserDetails :: UserId -> Client (Maybe UserDetailsRow) getUserDetails uid = retry x5 $ query1 cql (params LocalQuorum (Identity uid)) diff --git a/tools/db/inconsistencies/src/EmailLessUsers.hs b/tools/db/inconsistencies/src/EmailLessUsers.hs index 021a5064ae3..5d93ad9c5b4 100644 --- a/tools/db/inconsistencies/src/EmailLessUsers.hs +++ b/tools/db/inconsistencies/src/EmailLessUsers.hs @@ -80,7 +80,7 @@ data EmailInfo = EmailInfo { userId :: UserId, status :: WithWritetime AccountStatus, -- | Email in the user table - userEmail :: WithWritetime Email, + userEmail :: WithWritetime EmailAddress, -- | Email in the user_keys table emailKey :: Maybe (WithWritetime UserId), inconsistencyCase :: Text @@ -112,13 +112,13 @@ getUsers = paginateC cql (paramsP LocalQuorum () pageSize) x5 cql :: PrepQuery R () UserDetailsRow cql = "SELECT id, status, writetime(status), email, writetime(email), activated from user" -type UserDetailsRow = (UserId, Maybe AccountStatus, Maybe (Writetime AccountStatus), Maybe Email, Maybe (Writetime Email), Bool) +type UserDetailsRow = (UserId, Maybe AccountStatus, Maybe (Writetime AccountStatus), Maybe EmailAddress, Maybe (Writetime EmailAddress), Bool) -insertMissingEmail :: Logger -> ClientState -> Email -> UserId -> IO () +insertMissingEmail :: Logger -> ClientState -> EmailAddress -> UserId -> IO () insertMissingEmail l brig email uid = do runClient brig $ K.insertKey l uid (mkEmailKey email) -userWithEmailAndStatus :: UserDetailsRow -> Maybe (UserId, AccountStatus, Writetime AccountStatus, Email, Writetime Email) +userWithEmailAndStatus :: UserDetailsRow -> Maybe (UserId, AccountStatus, Writetime AccountStatus, EmailAddress, Writetime EmailAddress) userWithEmailAndStatus (uid, mStatus, mStatusWritetime, mEmail, mEmailWritetime, activated) = do let act = if activated then Just True else Nothing case (,,,,) <$> mStatus <*> mStatusWritetime <*> mEmail <*> mEmailWritetime <*> act of @@ -133,7 +133,7 @@ repairUser l brig repairData uid = do Nothing -> pure Nothing Just x -> checkUser l brig repairData x -checkUser :: Logger -> ClientState -> Bool -> (UserId, AccountStatus, Writetime AccountStatus, Email, Writetime Email) -> IO (Maybe EmailInfo) +checkUser :: Logger -> ClientState -> Bool -> (UserId, AccountStatus, Writetime AccountStatus, EmailAddress, Writetime EmailAddress) -> IO (Maybe EmailInfo) checkUser l brig repairData (uid, statusValue, statusWritetime, userEmailValue, userEmailWriteTime) = do let status = WithWritetime statusValue statusWritetime userEmail = WithWritetime userEmailValue userEmailWriteTime diff --git a/tools/db/phone-users/src/PhoneUsers/Types.hs b/tools/db/phone-users/src/PhoneUsers/Types.hs index 9a19a26f001..087dafd2b70 100644 --- a/tools/db/phone-users/src/PhoneUsers/Types.hs +++ b/tools/db/phone-users/src/PhoneUsers/Types.hs @@ -158,7 +158,7 @@ type Activated = Bool data UserRow = UserRow { id :: UserId, - email :: Maybe Email, + email :: Maybe EmailAddress, phone :: Maybe Phone, activated :: Activated, status :: Maybe AccountStatus, diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 5011becb775..f5675118477 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -207,7 +207,7 @@ suspendUser uid = NoContent <$ Intra.putUserStatus Suspended uid unsuspendUser :: UserId -> Handler NoContent unsuspendUser uid = NoContent <$ Intra.putUserStatus Active uid -usersByEmail :: Email -> Handler [UserAccount] +usersByEmail :: EmailAddress -> Handler [UserAccount] usersByEmail = Intra.getUserProfilesByIdentity usersByIds :: [UserId] -> Handler [UserAccount] @@ -232,13 +232,13 @@ searchOnBehalf (fromMaybe (unsafeRange 10) . checked @1 @100 @Int32 . fromMaybe 10 -> s) = Intra.getContacts uid q (fromRange s) -revokeIdentity :: Email -> Handler NoContent +revokeIdentity :: EmailAddress -> Handler NoContent revokeIdentity e = NoContent <$ Intra.revokeIdentity e changeEmail :: UserId -> EmailUpdate -> Handler NoContent changeEmail uid upd = NoContent <$ Intra.changeEmail uid upd -deleteUser :: UserId -> Email -> Handler NoContent +deleteUser :: UserId -> EmailAddress -> Handler NoContent deleteUser uid email = do usrs <- Intra.getUserProfilesByIdentity email case usrs of @@ -255,7 +255,7 @@ deleteUser uid email = do setTeamStatusH :: Team.TeamStatus -> TeamId -> Handler NoContent setTeamStatusH status tid = NoContent <$ Intra.setStatusBindingTeam tid status -deleteTeam :: TeamId -> Maybe Bool -> Maybe Email -> Handler NoContent +deleteTeam :: TeamId -> Maybe Bool -> Maybe EmailAddress -> Handler NoContent deleteTeam givenTid (fromMaybe False -> False) (Just email) = do acc <- Intra.getUserProfilesByIdentity email >>= handleNoUser . listToMaybe userTid <- (Intra.getUserBindingTeam . userId . accountUser $ acc) >>= handleNoTeam @@ -276,22 +276,22 @@ deleteTeam tid (fromMaybe False -> True) _ = do deleteTeam _ _ _ = throwE $ mkError status400 "Bad Request" "either email or 'force=true' parameter is required" -isUserKeyBlacklisted :: Email -> Handler NoContent +isUserKeyBlacklisted :: EmailAddress -> Handler NoContent isUserKeyBlacklisted email = do bl <- Intra.isBlacklisted email if bl then throwE $ mkError status200 "blacklisted" "The given user key IS blacklisted" else throwE $ mkError status404 "not-blacklisted" "The given user key is NOT blacklisted" -addBlacklist :: Email -> Handler NoContent +addBlacklist :: EmailAddress -> Handler NoContent addBlacklist email = do NoContent <$ Intra.setBlacklistStatus True email -deleteFromBlacklist :: Email -> Handler NoContent +deleteFromBlacklist :: EmailAddress -> Handler NoContent deleteFromBlacklist email = do NoContent <$ Intra.setBlacklistStatus False email -getTeamInfoByMemberEmail :: Email -> Handler TeamInfo +getTeamInfoByMemberEmail :: EmailAddress -> Handler TeamInfo getTeamInfoByMemberEmail e = do acc <- Intra.getUserProfilesByIdentity e >>= handleUser . listToMaybe tid <- (Intra.getUserBindingTeam . userId . accountUser $ acc) >>= handleTeam @@ -391,7 +391,7 @@ setTeamBillingInfo tid billingInfo = do Intra.setTeamBillingInfo tid billingInfo getTeamBillingInfo tid -getConsentLog :: Email -> Handler ConsentLogAndMarketo +getConsentLog :: EmailAddress -> Handler ConsentLogAndMarketo getConsentLog e = do acc <- listToMaybe <$> Intra.getUserProfilesByIdentity e when (isJust acc) $ diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index f0472676e47..38dba2f5817 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -84,7 +84,7 @@ type SternAPI = ( Summary "Displays user's info given an email address" :> "users" :> "by-email" - :> QueryParam' [Required, Strict, Description "Email address"] "email" Email + :> QueryParam' [Required, Strict, Description "Email address"] "email" EmailAddress :> Get '[JSON] [UserAccount] ) :<|> Named @@ -141,7 +141,7 @@ type SternAPI = \If the given identity is not taken / verified, this is a no-op." :> "users" :> "revoke-identity" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Post '[JSON] NoContent ) :<|> Named @@ -161,7 +161,7 @@ type SternAPI = "Email must match UserId's (to prevent copy/paste mistakes)." :> "users" :> Capture "uid" UserId - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Delete '[JSON] NoContent ) :<|> Named @@ -191,7 +191,7 @@ type SternAPI = :> "teams" :> Capture "tid" TeamId :> QueryParam' [Optional, Strict, Description "THIS WILL PERMANENTLY DELETE ALL TEAM MEMBERS! CHECK TEAM MEMBER LIST (SEE ABOVE OR BELOW) IF YOU ARE UNCERTAIN THAT'S WHAT YOU WANT."] "force" Bool - :> QueryParam' [Optional, Strict, Description "Matching verified remaining user address"] "email" Email + :> QueryParam' [Optional, Strict, Description "Matching verified remaining user address"] "email" EmailAddress :> Delete '[JSON] NoContent ) :<|> Named @@ -207,7 +207,7 @@ type SternAPI = ( Summary "Fetch blacklist information on a email (200: blacklisted; 404: not blacklisted)" :> "users" :> "blacklist" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Verb 'GET 200 '[JSON] NoContent ) :<|> Named @@ -215,7 +215,7 @@ type SternAPI = ( Summary "Add the email to our blacklist" :> "users" :> "blacklist" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Post '[JSON] NoContent ) :<|> Named @@ -223,14 +223,14 @@ type SternAPI = ( Summary "Remove the email from our blacklist" :> "users" :> "blacklist" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Delete '[JSON] NoContent ) :<|> Named "get-team-info-by-member-email" ( Summary "Fetch a team information given a member's email" :> "teams" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Get '[JSON] TeamInfo ) :<|> Named @@ -360,7 +360,7 @@ type SternAPI = :> Description "Relevant only internally at Wire" :> "i" :> "consent" - :> QueryParam' [Required, Strict, Description "A verified email address"] "email" Email + :> QueryParam' [Required, Strict, Description "A verified email address"] "email" EmailAddress :> Get '[JSON] ConsentLogAndMarketo ) :<|> Named diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 8051169cfc3..62a230730fa 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -232,7 +232,7 @@ getUserProfiles uidsOrHandles = do fmap (BS.intercalate "," . map toByteString') . chunksOf 50 -getUserProfilesByIdentity :: Email -> Handler [UserAccount] +getUserProfilesByIdentity :: EmailAddress -> Handler [UserAccount] getUserProfilesByIdentity email = do info $ msg "Getting user accounts by identity" b <- view brig @@ -289,7 +289,7 @@ getContacts u q s = do ) parseResponse (mkError status502 "bad-upstream") r -revokeIdentity :: Email -> Handler () +revokeIdentity :: EmailAddress -> Handler () revokeIdentity email = do info $ msg "Revoking user identity" b <- view brig @@ -464,7 +464,7 @@ setTeamBillingInfo tid tbu = do . expect2xx ) -isBlacklisted :: Email -> Handler Bool +isBlacklisted :: EmailAddress -> Handler Bool isBlacklisted email = do info $ msg "Checking blacklist" b <- view brig @@ -482,7 +482,7 @@ isBlacklisted email = do 404 -> pure False _ -> throwE (mkError status502 "bad-upstream" (errorMessage resp)) -setBlacklistStatus :: Bool -> Email -> Handler () +setBlacklistStatus :: Bool -> EmailAddress -> Handler () setBlacklistStatus status email = do info $ msg "Changing blacklist status" b <- view brig @@ -630,7 +630,7 @@ setSearchVisibility tid typ = do stripBS :: ByteString -> ByteString stripBS = encodeUtf8 . strip . decodeUtf8 -userKeyToParam :: Email -> Request -> Request +userKeyToParam :: EmailAddress -> Request -> Request userKeyToParam e = queryItem "email" (stripBS $ toByteString' e) errorMessage :: Response (Maybe LByteString) -> LText @@ -679,7 +679,7 @@ getTeamMembers tid = do ) parseResponse (mkError status502 "bad-upstream") r -getEmailConsentLog :: Email -> Handler ConsentLog +getEmailConsentLog :: EmailAddress -> Handler ConsentLog getEmailConsentLog email = do info $ msg "Getting email consent log" g <- view galeb @@ -713,7 +713,7 @@ getUserConsentValue uid = do ) parseResponse (mkError status502 "bad-upstream") r -getMarketoResult :: Email -> Handler MarketoResult +getMarketoResult :: EmailAddress -> Handler MarketoResult getMarketoResult email = do info $ msg "Getting marketo results" g <- view galeb diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index de4d3917d9c..1cd947747b8 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -500,7 +500,7 @@ getUsersByHandles h = do r <- get (stern . paths ["users", "by-handles"] . queryItem "handles" (cs h) . expect2xx) pure $ responseJsonUnsafe r -getUsersByEmail :: Email -> TestM [UserAccount] +getUsersByEmail :: EmailAddress -> TestM [UserAccount] getUsersByEmail email = do stern <- view tsStern r <- get (stern . paths ["users", "by-email"] . queryItem "email" (toByteString' email) . expect2xx) @@ -539,12 +539,12 @@ searchUsers uid = do r <- get (s . paths ["users", toByteString' uid, "search"] . expect2xx) pure $ responseJsonUnsafe r -revokeIdentity :: Either Email Phone -> TestM () +revokeIdentity :: Either EmailAddress Phone -> TestM () revokeIdentity emailOrPhone = do s <- view tsStern void $ post (s . paths ["users", "revoke-identity"] . mkQueryParam emailOrPhone . expect2xx) -mkQueryParam :: Either Email Phone -> Request -> Request +mkQueryParam :: Either EmailAddress Phone -> Request -> Request mkQueryParam = \case Left email -> queryItem "email" (toByteString' email) Right phone -> queryItem "phone" (toByteString' phone) @@ -554,7 +554,7 @@ putEmail uid emailUpdate = do s <- view tsStern void $ put (s . paths ["users", toByteString' uid, "email"] . json emailUpdate . expect2xx) -deleteUser :: UserId -> Either Email Phone -> TestM () +deleteUser :: UserId -> Either EmailAddress Phone -> TestM () deleteUser uid emailOrPhone = do s <- view tsStern void $ delete (s . paths ["users", toByteString' uid] . mkQueryParam emailOrPhone . expect2xx) @@ -569,7 +569,7 @@ unsuspendTeam tid = do s <- view tsStern void $ put (s . paths ["teams", toByteString' tid, "unsuspend"] . expect2xx) -deleteTeam :: TeamId -> Bool -> Email -> TestM () +deleteTeam :: TeamId -> Bool -> EmailAddress -> TestM () deleteTeam tid force email = do s <- view tsStern void $ delete (s . paths ["teams", toByteString' tid] . queryItem "force" (toByteString' force) . queryItem "email" (toByteString' email) . expect2xx) @@ -580,22 +580,22 @@ ejpdInfo includeContacts handles = do r <- get (s . paths ["ejpd-info"] . queryItem "include_contacts" (toByteString' includeContacts) . queryItem "handles" (toByteString' handles) . expect2xx) pure $ responseJsonUnsafe r -userBlacklistHead :: Either Email Phone -> TestM ResponseLBS +userBlacklistHead :: Either EmailAddress Phone -> TestM ResponseLBS userBlacklistHead emailOrPhone = do s <- view tsStern Bilge.get (s . paths ["users", "blacklist"] . mkQueryParam emailOrPhone) -postUserBlacklist :: Either Email Phone -> TestM () +postUserBlacklist :: Either EmailAddress Phone -> TestM () postUserBlacklist emailOrPhone = do s <- view tsStern void $ post (s . paths ["users", "blacklist"] . mkQueryParam emailOrPhone . expect2xx) -deleteUserBlacklist :: Either Email Phone -> TestM () +deleteUserBlacklist :: Either EmailAddress Phone -> TestM () deleteUserBlacklist emailOrPhone = do s <- view tsStern void $ delete (s . paths ["users", "blacklist"] . mkQueryParam emailOrPhone . expect2xx) -getTeamInfoByMemberEmail :: Email -> TestM TeamInfo +getTeamInfoByMemberEmail :: EmailAddress -> TestM TeamInfo getTeamInfoByMemberEmail email = do s <- view tsStern r <- get (s . paths ["teams"] . queryItem "email" (toByteString' email) . expect2xx) @@ -690,7 +690,7 @@ putSearchVisibility tid vis = do s <- view tsStern void $ put (s . paths ["teams", toByteString' tid, "search-visibility"] . json vis . expect2xx) -getConsentLog :: Email -> TestM ResponseLBS +getConsentLog :: EmailAddress -> TestM ResponseLBS getConsentLog email = do s <- view tsStern get (s . paths ["i", "consent"] . queryItem "email" (toByteString' email)) diff --git a/tools/stern/test/integration/Util.hs b/tools/stern/test/integration/Util.hs index 4fe29bb75a5..ba5ff5c7b49 100644 --- a/tools/stern/test/integration/Util.hs +++ b/tools/stern/test/integration/Util.hs @@ -87,7 +87,7 @@ randomUser'' isCreator hasPassword hasEmail = selfUser <$> randomUserProfile' is randomUserProfile' :: (HasCallStack) => Bool -> Bool -> Bool -> TestM SelfProfile randomUserProfile' isCreator hasPassword hasEmail = randomUserProfile'' isCreator hasPassword hasEmail <&> fst -randomUserProfile'' :: (HasCallStack) => Bool -> Bool -> Bool -> TestM (SelfProfile, Email) +randomUserProfile'' :: (HasCallStack) => Bool -> Bool -> Bool -> TestM (SelfProfile, EmailAddress) randomUserProfile'' isCreator hasPassword hasEmail = do b <- view tsBrig e <- liftIO randomEmail @@ -99,16 +99,16 @@ randomUserProfile'' isCreator hasPassword hasEmail = do <> ["team" .= BindingNewTeam (newNewTeam (unsafeRange "teamName") DefaultIcon) | isCreator] (,e) . responseJsonUnsafe <$> (post (b . path "/i/users" . Bilge.json pl) TestM (UserId, Email) +randomEmailUser :: (HasCallStack) => TestM (UserId, EmailAddress) randomEmailUser = randomUserProfile'' False False True <&> first (User.userId . selfUser) defPassword :: PlainTextPassword8 defPassword = plainTextPassword8Unsafe "topsecretdefaultpassword" -randomEmail :: (MonadIO m) => m Email +randomEmail :: (MonadIO m) => m EmailAddress randomEmail = do uid <- liftIO nextRandom - pure $ Email ("success+" <> UUID.toText uid) "simulator.amazonses.com" + pure $ unsafeEmailAddress ("success+" <> UUID.toASCIIBytes uid) "simulator.amazonses.com" setHandle :: UserId -> Text -> TestM () setHandle uid h = do @@ -163,7 +163,7 @@ addUserToTeamWithRole' role inviter tid = do ) pure (inv, r) -acceptInviteBody :: Email -> InvitationCode -> RequestBody +acceptInviteBody :: EmailAddress -> InvitationCode -> RequestBody acceptInviteBody email code = RequestBodyLBS . encode $ object From c5c3571f88fc2c6c65d19ceb2d3b998e703dcab1 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 22 Aug 2024 10:31:23 +0200 Subject: [PATCH 052/136] WPB-1333 OAuth endpoint to revoke single refresh tokens (#4213) --- changelog.d/2-features/WPB-1333 | 1 + integration/test/API/Brig.hs | 32 +++++ integration/test/Test/OAuth.hs | 134 ++++++++++++++++-- libs/wire-api/src/Wire/API/Password.hs | 18 +++ .../src/Wire/API/Routes/Public/Brig/OAuth.hs | 36 +++++ services/brig/src/Brig/API/OAuth.hs | 61 +++++--- services/brig/test/integration/API/OAuth.hs | 36 +---- 7 files changed, 249 insertions(+), 69 deletions(-) create mode 100644 changelog.d/2-features/WPB-1333 diff --git a/changelog.d/2-features/WPB-1333 b/changelog.d/2-features/WPB-1333 new file mode 100644 index 00000000000..ea1394c3e33 --- /dev/null +++ b/changelog.d/2-features/WPB-1333 @@ -0,0 +1 @@ +New endpoint to revoke an OAuth session diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index e6233bcda94..d933c0c8a1e 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -727,8 +727,40 @@ createOAuthAccessToken user cid code redirectUrl = do ("redirect_uri", redirectUrl) ] +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/post_oauth_token +createOAuthAccessTokenWithRefreshToken :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> String -> App Response +createOAuthAccessTokenWithRefreshToken user cid token = do + cidStr <- asString cid + req <- baseRequest user Brig Versioned "/oauth/token" + submit "POST" $ + req + & addUrlEncodedForm + [ ("grant_type", "refresh_token"), + ("client_id", cidStr), + ("refresh_token", token) + ] + -- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_oauth_applications getOAuthApplications :: (HasCallStack, MakesValue user) => user -> App Response getOAuthApplications user = do req <- baseRequest user Brig Versioned "/oauth/applications" submit "GET" req + +deleteOAuthSession :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> String -> String -> App Response +deleteOAuthSession user cid password tokenId = do + cidStr <- asString cid + req <- baseRequest user Brig Versioned $ joinHttpPath ["oauth", "applications", cidStr, "sessions", tokenId] + submit "DELETE" $ req & addJSONObject ["password" .= password] + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/delete_oauth_applications__OAuthClientId_ +revokeApplicationAccessV6 :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> App Response +revokeApplicationAccessV6 user cid = do + cidStr <- asString cid + req <- baseRequest user Brig (ExplicitVersion 6) $ joinHttpPath ["oauth", "applications", cidStr] + submit "DELETE" req + +revokeApplicationAccess :: (HasCallStack, MakesValue user, MakesValue cid) => user -> cid -> String -> App Response +revokeApplicationAccess user cid password = do + cidStr <- asString cid + req <- baseRequest user Brig Versioned $ joinHttpPath ["oauth", "applications", cidStr, "sessions"] + submit "DELETE" $ req & addJSONObject ["password" .= password] diff --git a/integration/test/Test/OAuth.hs b/integration/test/Test/OAuth.hs index 4a98a235872..2c018dedb61 100644 --- a/integration/test/Test/OAuth.hs +++ b/integration/test/Test/OAuth.hs @@ -2,24 +2,136 @@ module Test.OAuth where import API.Brig import API.BrigInternal +import API.Common (defPassword) import Data.String.Conversions import Network.HTTP.Types import Network.URI import SetupHelpers import Testlib.Prelude -testListApplicationsWithActiveSessions :: (HasCallStack) => App () -testListApplicationsWithActiveSessions = do +testOAuthRevokeSession :: (HasCallStack) => App () +testOAuthRevokeSession = do user <- randomUser OwnDomain def - oauthClient <- createOAuthClient user "foobar" "https://example.com" >>= getJSON 200 - cid <- oauthClient %. "client_id" + let uri = "https://example.com" + cid <- createOAuthClient user "foobar" uri >>= getJSON 200 >>= flip (%.) "client_id" let scopes = ["write:conversations"] - let generateAccessToken = do - authCodeResponse <- generateOAuthAuthorizationCode user cid scopes "https://example.com" - let location = fromMaybe (error "no location header") $ parseURI . cs . snd =<< locationHeader authCodeResponse - let code = maybe "no code query param" cs $ join $ lookup (cs "code") $ parseQuery $ cs location.uriQuery - void $ createOAuthAccessToken user cid code "https://example.com" >>= getJSON 200 - replicateM_ 2 generateAccessToken + + -- create a session that will be revoked later + (tokenToBeRevoked, sessionToBeRevoked) <- do + token <- generateAccessToken user cid scopes uri + [app] <- getOAuthApplications user >>= getJSON 200 >>= asList + [session] <- app %. "sessions" >>= asList + pure (token, session) + + -- create another session and assert that there are two sessions + validToken <- do + token <- generateAccessToken user cid scopes uri + [app] <- getOAuthApplications user >>= getJSON 200 >>= asList + sessions <- app %. "sessions" >>= asList + length sessions `shouldMatchInt` 2 + pure token + + -- attempt to revoke a session with a wrong password should fail + sessionToBeRevoked + %. "refresh_token_id" + >>= asString + >>= deleteOAuthSession user cid "foobar" + >>= assertStatus 403 + + -- revoke the first session and assert that there is only one session left + sessionToBeRevoked + %. "refresh_token_id" + >>= asString + >>= deleteOAuthSession user cid defPassword + >>= assertSuccess [app] <- getOAuthApplications user >>= getJSON 200 >>= asList sessions <- app %. "sessions" >>= asList - length sessions `shouldMatchInt` 2 + length sessions `shouldMatchInt` 1 + + -- try to use the revoked token and assert that it fails + tokenToBeRevoked + %. "refresh_token" + >>= asString + >>= createOAuthAccessTokenWithRefreshToken user cid + >>= assertStatus 403 + + -- try to use the valid token and assert that it works + validToken + %. "refresh_token" + >>= asString + >>= createOAuthAccessTokenWithRefreshToken user cid + >>= assertSuccess + +testRevokeApplicationAccountAccessV6 :: App () +testRevokeApplicationAccountAccessV6 = do + user <- randomUser OwnDomain def + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 0 + let uri = "https://example.com" + let scopes = ["write:conversations"] + replicateM_ 3 $ do + cid <- createOAuthClient user "foobar" uri >>= getJSON 200 >>= flip (%.) "client_id" + generateAccessToken user cid scopes uri + [cid1, cid2, cid3] <- getOAuthApplications user >>= getJSON 200 >>= asList >>= mapM (%. "id") + revokeApplicationAccessV6 user cid1 >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 2 + ids <- for apps $ \app -> app %. "id" + ids `shouldMatchSet` [cid2, cid3] + revokeApplicationAccessV6 user cid2 >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 1 + ids <- for apps $ \app -> app %. "id" + ids `shouldMatchSet` [cid3] + revokeApplicationAccessV6 user cid3 >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 0 + +testRevokeApplicationAccountAccess :: App () +testRevokeApplicationAccountAccess = do + user <- randomUser OwnDomain def + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 0 + let uri = "https://example.com" + let scopes = ["write:conversations"] + replicateM_ 3 $ do + cid <- createOAuthClient user "foobar" uri >>= getJSON 200 >>= flip (%.) "client_id" + generateAccessToken user cid scopes uri + [cid1, cid2, cid3] <- getOAuthApplications user >>= getJSON 200 >>= asList >>= mapM (%. "id") + revokeApplicationAccess user cid1 "foobar" >>= assertStatus 403 + revokeApplicationAccess user cid1 defPassword >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 2 + ids <- for apps $ \app -> app %. "id" + ids `shouldMatchSet` [cid2, cid3] + revokeApplicationAccess user cid2 defPassword >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 1 + ids <- for apps $ \app -> app %. "id" + ids `shouldMatchSet` [cid3] + revokeApplicationAccess user cid3 defPassword >>= assertSuccess + bindResponse (getOAuthApplications user) $ \resp -> do + resp.status `shouldMatchInt` 200 + apps <- resp.json >>= asList + length apps `shouldMatchInt` 0 + +generateAccessToken :: (MakesValue cid, MakesValue user) => user -> cid -> [String] -> String -> App Value +generateAccessToken user cid scopes uri = do + authCodeResponse <- generateOAuthAuthorizationCode user cid scopes uri + let location = fromMaybe (error "no location header") $ parseURI . cs . snd =<< locationHeader authCodeResponse + let code = maybe "no code query param" cs $ join $ lookup (cs "code") $ parseQuery $ cs location.uriQuery + createOAuthAccessToken user cid code uri >>= getJSON 200 diff --git a/libs/wire-api/src/Wire/API/Password.hs b/libs/wire-api/src/Wire/API/Password.hs index 6090f9ae6c7..c7aa15111ff 100644 --- a/libs/wire-api/src/Wire/API/Password.hs +++ b/libs/wire-api/src/Wire/API/Password.hs @@ -29,6 +29,7 @@ module Wire.API.Password unsafeMkPassword, hashPasswordArgon2idWithSalt, hashPasswordArgon2idWithOptions, + PasswordReqBody (..), ) where @@ -37,11 +38,14 @@ import Crypto.Error import Crypto.KDF.Argon2 qualified as Argon2 import Crypto.KDF.Scrypt as Scrypt import Crypto.Random +import Data.Aeson qualified as A import Data.ByteArray hiding (length) import Data.ByteString.Base64 qualified as B64 import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Lazy (fromStrict, toStrict) import Data.Misc +import Data.OpenApi qualified as S +import Data.Schema import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Imports @@ -334,3 +338,17 @@ unsafePad64 t where remains = Text.length t `rem` 4 pad = Text.replicate (4 - remains) "=" + +-------------------------------------------------------------------------------- +-- Type that can be used to pass a plaintext password as a request body + +newtype PasswordReqBody = PasswordReqBody + {fromPasswordReqBody :: Maybe PlainTextPassword6} + deriving stock (Eq, Show) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema PasswordReqBody + +instance ToSchema PasswordReqBody where + schema = + object "PasswordReqBody" $ + PasswordReqBody + <$> fromPasswordReqBody .= maybe_ (optField "password" schema) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs index a3173c0700b..2db4a8320ec 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs @@ -24,11 +24,14 @@ import Servant (JSON) import Servant hiding (Handler, JSON, Tagged, addHeader, respond) import Servant.OpenApi.Internal.Orphans () import Wire.API.Error +import Wire.API.Error.Brig import Wire.API.OAuth +import Wire.API.Password import Wire.API.Routes.API import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named) import Wire.API.Routes.Public +import Wire.API.Routes.Version type OAuthAPI = Named @@ -104,19 +107,52 @@ type OAuthAPI = '[JSON] (Respond 200 "OAuth applications found" [OAuthApplication]) ) + :<|> Named + "revoke-oauth-account-access-v6" + ( Summary "Revoke account access from an OAuth application" + :> ZUser + :> Until 'V7 + :> "oauth" + :> "applications" + :> Capture' '[Description "The ID of the OAuth client"] "OAuthClientId" OAuthClientId + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 204 "OAuth application access revoked"] + () + ) :<|> Named "revoke-oauth-account-access" ( Summary "Revoke account access from an OAuth application" + :> CanThrow 'AccessDenied :> ZUser + :> From 'V7 :> "oauth" :> "applications" :> Capture' '[Description "The ID of the OAuth client"] "OAuthClientId" OAuthClientId + :> "sessions" + :> ReqBody '[JSON] PasswordReqBody :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 204 "OAuth application access revoked"] () ) + :<|> Named + "delete-oauth-refresh-token" + ( Summary "Revoke an active OAuth session" + :> Description "Revoke an active OAuth session by providing the refresh token ID." + :> ZUser + :> CanThrow 'AccessDenied + :> CanThrow 'OAuthClientNotFound + :> "oauth" + :> "applications" + :> Capture' '[Description "The ID of the OAuth client"] "OAuthClientId" OAuthClientId + :> "sessions" + :> Capture' '[Description "The ID of the refresh token"] "RefreshTokenId" OAuthRefreshTokenId + :> ReqBody '[JSON] PasswordReqBody + :> Delete '[JSON] () + ) type CreateOAuthAuthorizationCodeHeaders = '[Header "Location" RedirectUrl] diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index b9e69c4c613..9e06bc14d4d 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -26,12 +26,12 @@ where import Brig.API.Error (throwStd) import Brig.API.Handler (Handler) import Brig.App +import Brig.Data.User import Brig.Options qualified as Opt import Cassandra hiding (Set) import Cassandra qualified as C -import Control.Error (assertMay, failWith, failWithM) +import Control.Error import Control.Lens (view, (?~), (^?)) -import Control.Monad.Except import Crypto.JWT hiding (params, uri) import Data.ByteString.Conversion import Data.Domain @@ -44,15 +44,18 @@ import Data.Text.Ascii import Data.Text.Encoding qualified as T import Data.Time import Imports hiding (exp) +import Network.Wai.Utilities.Error import OpenSSL.Random (randBytes) import Polysemy (Member) import Servant hiding (Handler, Tagged) import Wire.API.Error +import Wire.API.Error.Brig (BrigError (AccessDenied)) import Wire.API.OAuth as OAuth -import Wire.API.Password (Password, mkSafePasswordScrypt) +import Wire.API.Password import Wire.API.Routes.Internal.Brig.OAuth qualified as I import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig.OAuth +import Wire.Error import Wire.Sem.Jwk import Wire.Sem.Jwk qualified as Jwk import Wire.Sem.Now (Now) @@ -78,7 +81,9 @@ oauthAPI = :<|> Named @"create-oauth-access-token" createAccessTokenWith :<|> Named @"revoke-oauth-refresh-token" revokeRefreshToken :<|> Named @"get-oauth-applications" getOAuthApplications + :<|> Named @"revoke-oauth-account-access-v6" revokeOAuthAccountAccessV6 :<|> Named @"revoke-oauth-account-access" revokeOAuthAccountAccess + :<|> Named @"delete-oauth-refresh-token" deleteOAuthRefreshTokenById -------------------------------------------------------------------------------- -- Handlers @@ -117,8 +122,6 @@ deleteOAuthClient cid = do void $ getOAuthClientById cid lift $ wrapClient $ deleteOAuthClient' cid --------------------------------------------------------------------------------- - getOAuthClient :: UserId -> OAuthClientId -> (Handler r) (Maybe OAuthClient) getOAuthClient _ cid = do unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled @@ -186,8 +189,6 @@ validateAndCreateAuthorizationCode uid (CreateOAuthAuthorizationCodeRequest cid lift $ wrapClient $ insertOAuthAuthorizationCode ttl oauthCode cid uid scope redirectUrl chal pure oauthCode --------------------------------------------------------------------------------- - createAccessTokenWith :: (Member Now r, Member Jwk r) => Either OAuthAccessTokenRequest OAuthRefreshAccessTokenRequest -> (Handler r) OAuthAccessTokenResponse createAccessTokenWith req = do unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled @@ -300,14 +301,12 @@ createAccessToken key uid cid scope = do algo <- bestJWSAlg key signClaims key (newJWSHeader ((), algo)) claims --------------------------------------------------------------------------------- - revokeRefreshToken :: (Member Jwk r) => OAuthRevokeRefreshTokenRequest -> (Handler r) () revokeRefreshToken req = do key <- signingKey info <- lookupAndVerifyToken key req.refreshToken void $ getOAuthClient info.userId info.clientId >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure - lift $ wrapClient $ deleteOAuthRefreshToken info + lift $ wrapClient $ deleteOAuthRefreshToken info.userId info.refreshTokenId lookupAndVerifyToken :: JWK -> OAuthRefreshToken -> (Handler r) OAuthRefreshTokenInfo lookupAndVerifyToken key = @@ -317,8 +316,6 @@ lookupAndVerifyToken key = . lookupOAuthRefreshTokenInfo >=> maybe (throwStd $ errorToWai @'OAuthInvalidRefreshToken) pure --------------------------------------------------------------------------------- - getOAuthApplications :: UserId -> (Handler r) [OAuthApplication] getOAuthApplications uid = do activeRefreshTokens <- lift $ wrapClient $ lookupOAuthRefreshTokens uid @@ -332,12 +329,31 @@ getOAuthApplications uid = do pure $ (\client -> OAuthApplication cid client.name ((\i -> OAuthSession i.refreshTokenId (toUTCTimeMillis i.createdAt)) <$> tokens)) <$> mClient pure $ catMaybes mApps --------------------------------------------------------------------------------- - -revokeOAuthAccountAccess :: UserId -> OAuthClientId -> (Handler r) () -revokeOAuthAccountAccess uid cid = do +revokeOAuthAccountAccessV6 :: UserId -> OAuthClientId -> (Handler r) () +revokeOAuthAccountAccessV6 uid cid = do rts <- lift $ wrapClient $ lookupOAuthRefreshTokens uid - for_ rts $ \rt -> when (rt.clientId == cid) $ lift $ wrapClient $ deleteOAuthRefreshToken rt + for_ rts $ \rt -> when (rt.clientId == cid) $ lift $ wrapClient $ deleteOAuthRefreshToken uid rt.refreshTokenId + +revokeOAuthAccountAccess :: UserId -> OAuthClientId -> PasswordReqBody -> (Handler r) () +revokeOAuthAccountAccess uid cid req = do + wrapClientE $ reauthenticate uid req.fromPasswordReqBody !>> toAccessDenied + revokeOAuthAccountAccessV6 uid cid + where + toAccessDenied :: ReAuthError -> HttpError + toAccessDenied _ = StdError $ errorToWai @'AccessDenied + +deleteOAuthRefreshTokenById :: UserId -> OAuthClientId -> OAuthRefreshTokenId -> PasswordReqBody -> (Handler r) () +deleteOAuthRefreshTokenById uid cid tokenId req = do + wrapClientE $ reauthenticate uid req.fromPasswordReqBody !>> toAccessDenied + mInfo <- lift $ wrapClient $ lookupOAuthRefreshTokenInfo tokenId + case mInfo of + Nothing -> pure () + Just info -> do + when (info.clientId /= cid) $ throwStd $ errorToWai @'OAuthClientNotFound + lift $ wrapClient $ deleteOAuthRefreshToken uid tokenId + where + toAccessDenied :: ReAuthError -> HttpError + toAccessDenied _ = StdError $ errorToWai @'AccessDenied -------------------------------------------------------------------------------- -- DB @@ -397,7 +413,7 @@ insertOAuthRefreshToken :: (MonadClient m) => Word32 -> Word64 -> OAuthRefreshTo insertOAuthRefreshToken maxActiveTokens ttl info = do let rid = info.refreshTokenId oldTokes <- determineOldestTokensToBeDeleted <$> lookupOAuthRefreshTokens info.userId - for_ oldTokes deleteOAuthRefreshToken + for_ oldTokes (\t -> deleteOAuthRefreshToken t.userId t.refreshTokenId) retry x5 . write qInsertId $ params LocalQuorum (info.userId, rid, fromIntegral ttl) retry x5 . write qInsertInfo $ params LocalQuorum (rid, info.clientId, info.userId, C.Set (Set.toList (unOAuthScopes info.scopes)), info.createdAt, fromIntegral ttl) where @@ -429,10 +445,9 @@ lookupOAuthRefreshTokenInfo rid = do q :: PrepQuery R (Identity OAuthRefreshTokenId) (OAuthClientId, UserId, C.Set OAuthScope, UTCTime) q = "SELECT client, user, scope, created_at FROM oauth_refresh_token WHERE id = ?" -deleteOAuthRefreshToken :: (MonadClient m) => OAuthRefreshTokenInfo -> m () -deleteOAuthRefreshToken info = do - let rid = info.refreshTokenId - retry x5 . write qDeleteId $ params LocalQuorum (info.userId, rid) +deleteOAuthRefreshToken :: (MonadClient m) => UserId -> OAuthRefreshTokenId -> m () +deleteOAuthRefreshToken uid rid = do + retry x5 . write qDeleteId $ params LocalQuorum (uid, rid) retry x5 . write qDeleteInfo $ params LocalQuorum (Identity rid) where qDeleteId :: PrepQuery W (UserId, OAuthRefreshTokenId) () @@ -444,4 +459,4 @@ deleteOAuthRefreshToken info = do lookupAndDeleteOAuthRefreshToken :: (MonadClient m) => OAuthRefreshTokenId -> m (Maybe OAuthRefreshTokenInfo) lookupAndDeleteOAuthRefreshToken rid = do mInfo <- lookupOAuthRefreshTokenInfo rid - for_ mInfo deleteOAuthRefreshToken $> mInfo + for_ mInfo (\info -> deleteOAuthRefreshToken info.userId info.refreshTokenId) $> mInfo diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 9c6a0a92abf..c4f2311f577 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -122,8 +122,7 @@ tests m db b n o = do ], testGroup "oauth applications" - [ test m "list applications with account access" $ testListApplicationsWithAccountAccess b, - test m "revoke application account access" $ testRevokeApplicationAccountAccess b + [ test m "list applications with account access" $ testListApplicationsWithAccountAccess b ] ] @@ -625,31 +624,6 @@ testListApplicationsWithAccountAccess brig = do bobsApps <- listOAuthApplications brig (User.userId bob) liftIO $ assertEqual "apps" 1 (length bobsApps) -testRevokeApplicationAccountAccess :: Brig -> Http () -testRevokeApplicationAccountAccess brig = do - user <- createUser "alice" brig - do - apps <- listOAuthApplications brig (User.userId user) - liftIO $ assertEqual "apps" 0 (length apps) - for_ [1 .. 3 :: Int] $ const $ createOAuthApplicationWithAccountAccess brig (User.userId user) - cids <- fmap applicationId <$> listOAuthApplications brig (User.userId user) - liftIO $ assertEqual "apps" 3 (length cids) - case cids of - [cid1, cid2, cid3] -> do - revokeOAuthApplicationAccess brig (User.userId user) cid1 - do - apps <- listOAuthApplications brig (User.userId user) - liftIO $ assertEqual "apps" 2 (length apps) - revokeOAuthApplicationAccess brig (User.userId user) cid2 - do - apps <- listOAuthApplications brig (User.userId user) - liftIO $ assertEqual "apps" 1 (length apps) - revokeOAuthApplicationAccess brig (User.userId user) cid3 - do - apps <- listOAuthApplications brig (User.userId user) - liftIO $ assertEqual "apps" 0 (length apps) - _ -> liftIO $ assertFailure "unexpected number of apps" - testWriteConversationsSuccessNginz :: Brig -> Nginz -> Http () testWriteConversationsSuccessNginz brig nginz = do (uid, tid) <- Team.createUserWithTeam brig @@ -786,14 +760,6 @@ listOAuthApplications :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => listOAuthApplications brig uid = responseJsonError =<< listOAuthApplications' brig uid Brig -> UserId -> OAuthClientId -> m ResponseLBS -revokeOAuthApplicationAccess' brig uid cid = - delete (brig . paths ["oauth", "applications", toByteString' cid] . zUser uid) - -revokeOAuthApplicationAccess :: (MonadIO m, MonadHttp m, MonadCatch m, HasCallStack) => Brig -> UserId -> OAuthClientId -> m () -revokeOAuthApplicationAccess brig uid cid = - void $ revokeOAuthApplicationAccess' brig uid cid Brig -> UserId -> OAuthScopes -> RedirectUrl -> m (OAuthClientId, OAuthAuthorizationCode) generateOAuthClientAndAuthorizationCode = generateOAuthClientAndAuthorizationCode' challenge From 0e57e6be7eccaf23f4b69569855d8893e33a5c18 Mon Sep 17 00:00:00 2001 From: Amit Sagtani Date: Fri, 23 Aug 2024 14:32:34 +0200 Subject: [PATCH 053/136] Read sftToken from secrets.yaml (#4214) * add sftToken to secrets * add changelog * rename to sftTokenSecret * fix syntax * add missing sft config in values * set sftToken only when sftTokenSecret is set in secrets * remove default sft config in values.yaml --- changelog.d/5-internal/WPB-10302 | 1 + charts/brig/templates/configmap.yaml | 4 ++-- charts/brig/templates/secret.yaml | 3 +++ charts/brig/values.yaml | 8 ++++++++ 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 changelog.d/5-internal/WPB-10302 diff --git a/changelog.d/5-internal/WPB-10302 b/changelog.d/5-internal/WPB-10302 new file mode 100644 index 00000000000..8780ddd6ac7 --- /dev/null +++ b/changelog.d/5-internal/WPB-10302 @@ -0,0 +1 @@ +Read sftTokenSecret from secrets.yaml and mount to /etc/wire/brig/secrets/sftTokenSecret by default diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index 669e047bdc9..32ccd3acc04 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -233,11 +233,11 @@ data: {{- if .sftDiscoveryIntervalSeconds }} sftDiscoveryIntervalSeconds: {{ .sftDiscoveryIntervalSeconds }} {{- end }} - {{- if .sftToken }} + {{- if $.Values.secrets.sftTokenSecret }} sftToken: {{- with .sftToken }} ttl: {{ .ttl }} - secret: {{ .secret }} + secret: {{ .secret | default "/etc/wire/brig/secrets/sftTokenSecret" }} {{- end }} {{- end }} {{- end }} diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index b596954c7d8..0a566d04a00 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -20,6 +20,9 @@ data: awsKeyId: {{ .awsKeyId | b64enc | quote }} awsSecretKey: {{ .awsSecretKey | b64enc | quote }} {{- end }} + {{- if .sftTokenSecret }} + sftTokenSecret: {{ .sftTokenSecret | b64enc | quote }} + {{- end }} {{- if (not $.Values.config.useSES) }} smtp-password.txt: {{ .smtpPassword | b64enc | quote }} {{- end }} diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index c5f981d63bd..561eb6c3bbd 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -99,6 +99,14 @@ config: providerTokenTimeout: 900 legalholdUserTokenTimeout: 4838400 legalholdAccessTokenTimeout: 900 + # sft: + # sftBaseDomain: sft.wire.example.com + # sftSRVServiceName: sft + # sftDiscoveryIntervalSeconds: 10 + # sftListLength: 20 + # sftToken: + # ttl: 120 + # secret: /etc/wire/brig/secrets/sftTokenSecret # this is the default path for secret.sftTokenSecret optSettings: setActivationTimeout: 1209600 setTeamInvitationTimeout: 1814400 From 2227605c0261839ef85cdbbaf401034409667194 Mon Sep 17 00:00:00 2001 From: Amit Sagtani Date: Mon, 2 Sep 2024 13:27:37 +0200 Subject: [PATCH 054/136] Wpb 10335 | Ensure pods are distributed evenly on each k8s node (#4222) * add node topology spread to brig * add node topology spread to cannon * add node topology spread to cargohold * add node topology spread to demo-smtp * add node topology spread to fake-aws-s3 * add node topology spread to fake-aws-ses * add node topology spread to fake-aws-sns * add node topology spread to fake-aws-sqs * add node topology spread to federator * add node topology spread to galley * add node topology spread to gundeck * add node topology spread to legalhold * add node topology spread to nginz * add node topology spread to openldap * add node topology spread to outlook-addin * add node topology spread to proxy * add node topology spread to reaperwq * add node topology spread to restund * add node topology spread to spar * added changelog --- changelog.d/5-internal/WPB-10335 | 1 + charts/brig/templates/deployment.yaml | 7 +++++++ charts/cannon/templates/statefulset.yaml | 7 +++++++ charts/cargohold/templates/deployment.yaml | 7 +++++++ charts/demo-smtp/templates/deployment.yaml | 7 +++++++ charts/fake-aws-s3/templates/reaper.yaml | 7 +++++++ charts/fake-aws-ses/templates/deployment.yaml | 7 +++++++ charts/fake-aws-sns/templates/deployment.yaml | 7 +++++++ charts/fake-aws-sqs/templates/deployment.yaml | 7 +++++++ charts/federator/templates/deployment.yaml | 7 +++++++ charts/galley/templates/deployment.yaml | 7 +++++++ charts/gundeck/templates/deployment.yaml | 7 +++++++ charts/legalhold/templates/deployment.yaml | 7 +++++++ charts/nginz/templates/deployment.yaml | 7 +++++++ charts/openldap/templates/openldap.yaml | 7 +++++++ charts/outlook-addin/templates/deployment.yaml | 7 +++++++ charts/proxy/templates/deployment.yaml | 7 +++++++ charts/reaper/templates/deployment.yaml | 7 +++++++ charts/restund/templates/statefulset.yaml | 7 +++++++ charts/spar/templates/deployment.yaml | 7 +++++++ 20 files changed, 134 insertions(+) create mode 100644 changelog.d/5-internal/WPB-10335 diff --git a/changelog.d/5-internal/WPB-10335 b/changelog.d/5-internal/WPB-10335 new file mode 100644 index 00000000000..cf6ebf9798a --- /dev/null +++ b/changelog.d/5-internal/WPB-10335 @@ -0,0 +1 @@ +Added node based topology constraint to ensure pods are distributed uniformly on all nodes. diff --git a/charts/brig/templates/deployment.yaml b/charts/brig/templates/deployment.yaml index fa59c13ed36..08403170c07 100644 --- a/charts/brig/templates/deployment.yaml +++ b/charts/brig/templates/deployment.yaml @@ -30,6 +30,13 @@ spec: fluentbit.io/parser: json spec: serviceAccountName: {{ .Values.serviceAccount.name }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: brig volumes: - name: "brig-config" configMap: diff --git a/charts/cannon/templates/statefulset.yaml b/charts/cannon/templates/statefulset.yaml index 2d7db645c36..2931ce01b90 100644 --- a/charts/cannon/templates/statefulset.yaml +++ b/charts/cannon/templates/statefulset.yaml @@ -34,6 +34,13 @@ spec: {{- end }} spec: terminationGracePeriodSeconds: {{ add .Values.config.drainOpts.gracePeriodSeconds 5 }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: cannon containers: {{- if .Values.service.nginz.enabled }} - name: nginz diff --git a/charts/cargohold/templates/deployment.yaml b/charts/cargohold/templates/deployment.yaml index 99222b2092a..fe25a506cc2 100644 --- a/charts/cargohold/templates/deployment.yaml +++ b/charts/cargohold/templates/deployment.yaml @@ -28,6 +28,13 @@ spec: checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} spec: serviceAccountName: {{ .Values.serviceAccount.name }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: cargohold volumes: - name: "cargohold-config" configMap: diff --git a/charts/demo-smtp/templates/deployment.yaml b/charts/demo-smtp/templates/deployment.yaml index 1e132a72ee6..4cc1b36a363 100644 --- a/charts/demo-smtp/templates/deployment.yaml +++ b/charts/demo-smtp/templates/deployment.yaml @@ -19,6 +19,13 @@ spec: app: {{ template "demo-smtp.name" . }} release: {{ .Release.Name }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ template "demo-smtp.name" . }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.image }}" diff --git a/charts/fake-aws-s3/templates/reaper.yaml b/charts/fake-aws-s3/templates/reaper.yaml index 9d7759eaadd..0687875de32 100644 --- a/charts/fake-aws-s3/templates/reaper.yaml +++ b/charts/fake-aws-s3/templates/reaper.yaml @@ -17,6 +17,13 @@ spec: labels: app: {{ template "fullname" . }}-reaper spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ template "fullname" . }}-reaper volumes: - name: minio-configuration projected: diff --git a/charts/fake-aws-ses/templates/deployment.yaml b/charts/fake-aws-ses/templates/deployment.yaml index 11ec6b5501e..43c48b98a01 100644 --- a/charts/fake-aws-ses/templates/deployment.yaml +++ b/charts/fake-aws-ses/templates/deployment.yaml @@ -17,6 +17,13 @@ spec: labels: app: {{ template "fullname" . }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ template "fullname" . }} containers: - name: fake-aws-ses image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" diff --git a/charts/fake-aws-sns/templates/deployment.yaml b/charts/fake-aws-sns/templates/deployment.yaml index f93bfc62167..04ff1e083f2 100644 --- a/charts/fake-aws-sns/templates/deployment.yaml +++ b/charts/fake-aws-sns/templates/deployment.yaml @@ -17,6 +17,13 @@ spec: labels: app: {{ template "fullname" . }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ template "fullname" . }} containers: - name: fake-aws-sns image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" diff --git a/charts/fake-aws-sqs/templates/deployment.yaml b/charts/fake-aws-sqs/templates/deployment.yaml index 39848020c90..c8e024632b1 100644 --- a/charts/fake-aws-sqs/templates/deployment.yaml +++ b/charts/fake-aws-sqs/templates/deployment.yaml @@ -19,6 +19,13 @@ spec: annotations: checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ template "fullname" . }} containers: - name: fake-aws-sqs image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" diff --git a/charts/federator/templates/deployment.yaml b/charts/federator/templates/deployment.yaml index 8b38aa22e71..e5faa860516 100644 --- a/charts/federator/templates/deployment.yaml +++ b/charts/federator/templates/deployment.yaml @@ -30,6 +30,13 @@ spec: {{- end }} fluentbit.io/parser: json spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: federator volumes: - name: "federator-config" configMap: diff --git a/charts/galley/templates/deployment.yaml b/charts/galley/templates/deployment.yaml index 06ad8d1cd21..26d4ab5568b 100644 --- a/charts/galley/templates/deployment.yaml +++ b/charts/galley/templates/deployment.yaml @@ -28,6 +28,13 @@ spec: checksum/aws-secret: {{ include (print .Template.BasePath "/aws-secret.yaml") . | sha256sum }} checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: galley serviceAccountName: {{ .Values.serviceAccount.name }} volumes: - name: "galley-config" diff --git a/charts/gundeck/templates/deployment.yaml b/charts/gundeck/templates/deployment.yaml index 5afbdd9c4cf..ee67ba1ba43 100644 --- a/charts/gundeck/templates/deployment.yaml +++ b/charts/gundeck/templates/deployment.yaml @@ -28,6 +28,13 @@ spec: checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} spec: serviceAccountName: {{ .Values.serviceAccount.name }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: gundeck volumes: - name: "gundeck-config" configMap: diff --git a/charts/legalhold/templates/deployment.yaml b/charts/legalhold/templates/deployment.yaml index 51036248390..7f6b2c320aa 100644 --- a/charts/legalhold/templates/deployment.yaml +++ b/charts/legalhold/templates/deployment.yaml @@ -17,6 +17,13 @@ spec: labels: name: "{{ .Release.Name }}-hold" spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + name: "{{ .Release.Name }}-hold" restartPolicy: Always containers: - name: hold diff --git a/charts/nginz/templates/deployment.yaml b/charts/nginz/templates/deployment.yaml index fd9a0f3cd06..d04610f6020 100644 --- a/charts/nginz/templates/deployment.yaml +++ b/charts/nginz/templates/deployment.yaml @@ -29,6 +29,13 @@ spec: fluentbit.io/parser-nginz: nginz spec: terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: nginz containers: - name: nginz-disco image: "{{ .Values.images.nginzDisco.repository }}:{{ .Values.images.nginzDisco.tag }}" diff --git a/charts/openldap/templates/openldap.yaml b/charts/openldap/templates/openldap.yaml index 28ed001aa62..3a0fdb9f08b 100644 --- a/charts/openldap/templates/openldap.yaml +++ b/charts/openldap/templates/openldap.yaml @@ -8,6 +8,13 @@ metadata: release: {{ .Release.Name }} heritage: {{ .Release.Service }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: openldap securityContext: fsGroup: 911 volumes: diff --git a/charts/outlook-addin/templates/deployment.yaml b/charts/outlook-addin/templates/deployment.yaml index a9679ab816b..3a0ab24413d 100644 --- a/charts/outlook-addin/templates/deployment.yaml +++ b/charts/outlook-addin/templates/deployment.yaml @@ -15,6 +15,13 @@ spec: labels: app: {{ include "outlook.fullname" . }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: {{ include "outlook.fullname" . }} containers: - name: {{ include "outlook.fullname" . }} image: {{ .Values.containerImage }} diff --git a/charts/proxy/templates/deployment.yaml b/charts/proxy/templates/deployment.yaml index 63239a5d413..02676553a1b 100644 --- a/charts/proxy/templates/deployment.yaml +++ b/charts/proxy/templates/deployment.yaml @@ -27,6 +27,13 @@ spec: checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} checksum/secret: {{ include (print .Template.BasePath "/secret.yaml") . | sha256sum }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: proxy volumes: - name: "proxy-config" configMap: diff --git a/charts/reaper/templates/deployment.yaml b/charts/reaper/templates/deployment.yaml index 89b581b0941..a63cdc42fef 100644 --- a/charts/reaper/templates/deployment.yaml +++ b/charts/reaper/templates/deployment.yaml @@ -20,6 +20,13 @@ spec: release: {{ .Release.Name }} spec: serviceAccountName: reaper-role + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: reaper containers: - name: reaper image: bitnami/kubectl:1.24.12 diff --git a/charts/restund/templates/statefulset.yaml b/charts/restund/templates/statefulset.yaml index 87fa6571c21..97f09a60aae 100644 --- a/charts/restund/templates/statefulset.yaml +++ b/charts/restund/templates/statefulset.yaml @@ -26,6 +26,13 @@ spec: labels: {{- include "restund.selectorLabels" . | nindent 8 }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + {{- include "restund.selectorLabels" . | nindent 6 }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} hostNetwork: true diff --git a/charts/spar/templates/deployment.yaml b/charts/spar/templates/deployment.yaml index c09fc2beacd..5176bf3ebb2 100644 --- a/charts/spar/templates/deployment.yaml +++ b/charts/spar/templates/deployment.yaml @@ -26,6 +26,13 @@ spec: # An annotation of the configmap checksum ensures changes to the configmap cause a redeployment upon `helm upgrade` checksum/configmap: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum }} spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: "kubernetes.io/hostname" + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: spar volumes: - name: "spar-config" configMap: From 3700a146db9cbd0206c6c2cdac458a764fa17165 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Tue, 3 Sep 2024 08:26:49 +0200 Subject: [PATCH 055/136] Added data-migration to weed route, removed more dead code. (#4223) --- libs/wire-api/src/Wire/API/Team/Feature.hs | 9 ------ services/cargohold/cargohold.cabal | 1 - services/cargohold/default.nix | 2 -- .../cargohold/test/integration/API/Util.hs | 30 +------------------ services/spar/test-integration/Util/Core.hs | 10 ------- weeder.toml | 6 +++- 6 files changed, 6 insertions(+), 52 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 7c1497edeb4..4aba62549c9 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -37,12 +37,10 @@ module Wire.API.Team.Feature Feature (..), forgetLock, withLockStatus, - withUnlocked, FeatureTTL, FeatureTTLDays, FeatureTTL' (..), FeatureTTLUnit (..), - convertFeatureTTLDaysToSeconds, EnforceAppLock (..), genericComputeFeature, IsFeatureConfig (..), @@ -365,9 +363,6 @@ forgetLock ws = Feature ws.status ws.config withLockStatus :: LockStatus -> Feature a -> LockableFeature a withLockStatus ls (Feature s c) = LockableFeature s ls c -withUnlocked :: Feature a -> LockableFeature a -withUnlocked = withLockStatus LockStatusUnlocked - instance (ToSchema cfg, IsFeatureConfig cfg) => ToSchema (Feature cfg) where schema = object name $ @@ -400,10 +395,6 @@ type FeatureTTL = FeatureTTL' 'FeatureTTLUnitSeconds type FeatureTTLDays = FeatureTTL' 'FeatureTTLUnitDays -convertFeatureTTLDaysToSeconds :: FeatureTTLDays -> FeatureTTL -convertFeatureTTLDaysToSeconds FeatureTTLUnlimited = FeatureTTLUnlimited -convertFeatureTTLDaysToSeconds (FeatureTTLSeconds d) = FeatureTTLSeconds (d * (60 * 60 * 24)) - instance Arbitrary FeatureTTL where arbitrary = (nonZero <$> arbitrary) diff --git a/services/cargohold/cargohold.cabal b/services/cargohold/cargohold.cabal index 39a6dce3593..c906ffb3dda 100644 --- a/services/cargohold/cargohold.cabal +++ b/services/cargohold/cargohold.cabal @@ -283,7 +283,6 @@ executable cargohold-integration , mmorph , mtl , optparse-applicative - , safe , servant-client , tagged >=0.8 , tasty >=1.0 diff --git a/services/cargohold/default.nix b/services/cargohold/default.nix index 2116b776445..585a66d22ff 100644 --- a/services/cargohold/default.nix +++ b/services/cargohold/default.nix @@ -46,7 +46,6 @@ , prometheus-client , resourcet , retry -, safe , servant , servant-client , servant-server @@ -155,7 +154,6 @@ mkDerivation { mmorph mtl optparse-applicative - safe servant-client tagged tasty diff --git a/services/cargohold/test/integration/API/Util.hs b/services/cargohold/test/integration/API/Util.hs index a1feeb7739a..d9579b55ab1 100644 --- a/services/cargohold/test/integration/API/Util.hs +++ b/services/cargohold/test/integration/API/Util.hs @@ -16,32 +16,25 @@ -- with this program. If not, see . module API.Util - ( randomUser, - downloadAsset, + ( downloadAsset, withMockFederator, ) where import Bilge hiding (body, host, port) -import qualified Bilge import CargoHold.Options import CargoHold.Run import Control.Lens hiding ((.=)) import Control.Monad.Codensity -import Data.Aeson (object, (.=)) -import qualified Data.ByteString.Char8 as C import Data.ByteString.Conversion import Data.Default import Data.Id import Data.Qualified -import Data.Text.Encoding (encodeUtf8) import qualified Data.UUID as UUID -import Data.UUID.V4 (nextRandom) import Federator.MockServer import Imports hiding (head) import qualified Network.HTTP.Media as HTTP import Network.Wai.Utilities.MockServer -import Safe (readNote) import TestSetup import Util.Options import Wire.API.Asset @@ -51,27 +44,6 @@ import Wire.API.Asset -- The changes to the asset routes forbidding non-verified users from uploading -- assets breaks a lot of existing tests. -- --- FUTUREWORK: Move all the cargohold tests to the new integration test suite. --- https://wearezeta.atlassian.net/browse/WPB-5382 -randomUser :: TestM UserId -randomUser = do - (Endpoint (encodeUtf8 -> eHost) ePort) <- view tsBrig - e <- liftIO $ mkEmail "success" "simulator.amazonses.com" - let p = - object - [ "name" .= e, - "email" .= e, - "password" .= ("secret-8-chars-long-at-least" :: Text) - ] - r <- post (Bilge.host eHost . Bilge.port ePort . path "/i/users" . json p) - pure - . readNote "unable to parse Location header" - . C.unpack - $ getHeader' "Location" r - where - mkEmail loc dom = do - uid <- nextRandom - pure $ loc <> "+" <> UUID.toText uid <> "@" <> dom zUser :: UserId -> Request -> Request zUser = header "Z-User" . UUID.toASCIIBytes . toUUID diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index b53493cba17..f6affbfa514 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -36,7 +36,6 @@ module Util.Core -- * Test helpers it, - fit, pending, pendingWith, shouldRespondWith, @@ -293,15 +292,6 @@ it :: SpecWith TestEnv it msg bdy = Test.Hspec.it msg $ runReaderT bdy -fit :: - (HasCallStack) => - -- or, more generally: - -- MonadIO m, Example (TestEnv -> m ()), Arg (TestEnv -> m ()) ~ TestEnv - String -> - TestSpar () -> - SpecWith TestEnv -fit msg bdy = Test.Hspec.fit msg $ runReaderT bdy - pending :: (HasCallStack, MonadIO m) => m () pending = liftIO Test.Hspec.pending diff --git a/weeder.toml b/weeder.toml index 7c19d1f538b..4e17e0becfa 100644 --- a/weeder.toml +++ b/weeder.toml @@ -35,8 +35,10 @@ roots = [ # may of the entries here are about general-purpose module "^Data.Range.rinc", "^Data.Range.rsingleton", "^Data.ZAuth.Validation.*$", - "^Galley.Types.UserList.ulDiff", + "^Galley.Cassandra.FeatureTH.generateSOPInstances$", + "^Galley.Cassandra.FeatureTH.generateTupleP$", "^Galley.Types.Teams.canSeePermsOf", # TODO: figure out why weeder is confused by let bindings with curried infix notation + "^Galley.Types.UserList.ulDiff", "^HTTP2.Client.Manager.*$", "^Imports.getChar", "^Imports.getContents", @@ -44,6 +46,7 @@ roots = [ # may of the entries here are about general-purpose module "^Imports.putChar", "^Imports.readIO", "^Imports.readLn", + "^Imports.todo", "^Main.debugMainDebugExportFull", # move-team "^Main.debugMainExport", # move-team "^Main.debugMainImport", # move-team @@ -108,6 +111,7 @@ roots = [ # may of the entries here are about general-purpose module "^Proto.Otr_Fields.vec'userIds", "^Proto.TeamEvents_Fields.currency", "^Proto.TeamEvents_Fields.vec'billingUser", + "^Run.main$", "^Spar.Sem.AReqIDStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? "^Spar.Sem.AssIDStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? "^Spar.Sem.ScimTokenStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? From 03725ef74dd5e39a10f4c8dce3bb019bc4a98f77 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 3 Sep 2024 15:26:06 +0200 Subject: [PATCH 056/136] hack/bin/gen-certs.sh: Also gen certs for federation-v1 (#4225) --- .../docker/elasticsearch-ca.pem | 32 ++++++------ .../docker/elasticsearch-cert.pem | 32 ++++++------ .../docker/elasticsearch-key.pem | 52 +++++++++---------- deploy/dockerephemeral/docker/redis-ca.pem | 34 ++++++------ .../docker/redis-node-1-cert.pem | 32 ++++++------ .../docker/redis-node-1-key.pem | 52 +++++++++---------- .../docker/redis-node-2-cert.pem | 32 ++++++------ .../docker/redis-node-2-key.pem | 52 +++++++++---------- .../docker/redis-node-3-cert.pem | 32 ++++++------ .../docker/redis-node-3-key.pem | 52 +++++++++---------- .../docker/redis-node-4-cert.pem | 32 ++++++------ .../docker/redis-node-4-key.pem | 52 +++++++++---------- .../docker/redis-node-5-cert.pem | 32 ++++++------ .../docker/redis-node-5-key.pem | 52 +++++++++---------- .../docker/redis-node-6-cert.pem | 32 ++++++------ .../docker/redis-node-6-key.pem | 52 +++++++++---------- .../federation-v0/integration-ca.pem | 34 ++++++------ .../federation-v0/integration-leaf-key.pem | 52 +++++++++---------- .../federation-v0/integration-leaf.pem | 30 +++++------ .../federation-v1/integration-ca.pem | 34 ++++++------ .../federation-v1/integration-leaf-key.pem | 52 +++++++++---------- .../federation-v1/integration-leaf.pem | 30 +++++------ .../rabbitmq-config/certificates/ca-key.pem | 52 +++++++++---------- .../rabbitmq-config/certificates/ca.pem | 34 ++++++------ .../rabbitmq-config/certificates/cert.pem | 32 ++++++------ .../rabbitmq-config/certificates/key.pem | 52 +++++++++---------- hack/bin/gen-certs.sh | 2 + hack/helm_vars/certs/elasticsearch-ca-key.pem | 52 +++++++++---------- hack/helm_vars/certs/elasticsearch-ca.pem | 32 ++++++------ .../conf/nginz/integration-ca-key.pem | 52 +++++++++---------- .../conf/nginz/integration-ca.pem | 34 ++++++------ .../conf/nginz/integration-leaf-key.pem | 52 +++++++++---------- .../conf/nginz/integration-leaf.pem | 30 +++++------ 33 files changed, 656 insertions(+), 654 deletions(-) diff --git a/deploy/dockerephemeral/docker/elasticsearch-ca.pem b/deploy/dockerephemeral/docker/elasticsearch-ca.pem index 1c6f3128257..6511f688a73 100644 --- a/deploy/dockerephemeral/docker/elasticsearch-ca.pem +++ b/deploy/dockerephemeral/docker/elasticsearch-ca.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDLzCCAhegAwIBAgIUXMOPFnGTAQ30+xOQ2od/HYZiSwQwDQYJKoZIhvcNAQEL +MIIDLzCCAhegAwIBAgIUUOLn63PL3FEyGdhOK1ocDAn8dC8wDQYJKoZIhvcNAQEL BQAwJzElMCMGA1UEAwwcZWxhc3RpY3NlYXJjaC5jYS5leGFtcGxlLmNvbTAeFw0y -NDA3MTgwNjQ4MjBaFw0zNDA3MTYwNjQ4MjBaMCcxJTAjBgNVBAMMHGVsYXN0aWNz +NDA5MDMxMjAzMzhaFw0zNDA5MDExMjAzMzhaMCcxJTAjBgNVBAMMHGVsYXN0aWNz ZWFyY2guY2EuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQC5RatgsLSQdpLdJyW+kmpOeyIoJgJVm+76D3eSx5tPnSMtsGolXNJHynR4 -AzJ+h5tiSOhK+x/RPiv7ofjD5P+cl6q0cWckhQ089sp+deRjZrhvTskn/0xkg9W1 -Gk2awYl9Oq9ij2hLSQgtw+QRkv0LkdnQLKyGWQ8N7BLksAcvr9N5gTaLE3PVQQop -SaD72IE+gGk4HDQzCque/eeqNjp5qq9umFlXJk8GX3HfB14VRslGmh5OIs8y/HJG -FRMcbVXJ11T+d6xpmzCFfwXYSN5OJghs1AC6SyirqYEgOigJsMnioYbbyKHh+HLA -YKnY+Bi/KQJQ/ipwzibW9G//058FAgMBAAGjUzBRMB0GA1UdDgQWBBRRMD3Fk8ui -R5eRsezFZrT0oMKlpzAfBgNVHSMEGDAWgBRRMD3Fk8uiR5eRsezFZrT0oMKlpzAP -BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAQSW/I5EbTbsBY4rIA -RX+0S7gbx2+d1fhMFEloLagXipn8vd8PGtX9riYSb4k2KmP4BjeQo3TMpJVTUmh8 -RkppbOLabIcyuCI732PJfwlkRHBThguv905uuzil8IC1mDR7qy6Rkg2ByDRlqAgB -icX/3uG7A6XDNsNrwP8Pj0X/YRSbqLIlFtyQ6RCCOYn1CCUDkciHgDMHlgK90r4s -+hrgtB6zKdMI89hnn8MLbQ+eaZ9UDpYbovBbDvZfEI5AaTlSQTL503+gM5bUgZRl -YT8z44Piip8VhkKwb/31C+tht6gNvqEQBFudusrHrg/KphpFTpAHQgW2vA/z9LJt -vzAR +AoIBAQDhfp2hokSJ88qb6Gl8BBwg4jMbkt4l2ynOa/lO6DZFRheFlWZBaUZAcj1o +jBgbd3EoOXygZReL9TGBRrc5iACzytlOJkwMgNUUIq1KsPwWII41VJw3h3NO07tM +Tsf0kFvH1pEllJqorhQ1eZnU1SISyQcjk0oRQEyWd6arF7TLHna69OHF2ybYYXMD +MFWNr+O6t8RUfYs4kb7z1Nx3OnUKrUhIyaYeoyvBBOdXA5/G5GenDu/G4iVozsuL +gofYWu+77EnpProF0KRK+XlKakvF7bD26Qm1ol5qsXbdXOXKYq/c4KDdC938HNd/ +FNPz7EsnW0brBXqtz8TeQmxHix7DAgMBAAGjUzBRMB0GA1UdDgQWBBTWzp/VRTGq +s/IwlrROQGDwavTpyzAfBgNVHSMEGDAWgBTWzp/VRTGqs/IwlrROQGDwavTpyzAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBFue/SGpAVZ0TOVp8l +bdaaY/e9wUBSdCH5b39Nzd3rKmH3lIHqcafLlFCx+scKUIlFbohJr0aTK339wFfl +L0LzBJVUT9JzeDNPhk8pl/jBJk+eGP3fiykFMCgxxGvHtccHu8E/y8U0SeEtKqDn +Xy0ZbC3M54UedhDpHMovfHEsfN24Ev0DK13sBR2T8fmXCyCrfq887cCqJyP2ODgb +xAY/R4F8Ueadn0ywHYSY3MqmDsvDul0QlaOu2J5A0+k5oy4hAfFB8PzPYZrmPkeU +N5oxudTTihIZ+0JiL2JmWGBzMGzgtmD1rHC6lugUlWq+BoPu2+/+hn8RcVHBCFDk +WMSU -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/elasticsearch-cert.pem b/deploy/dockerephemeral/docker/elasticsearch-cert.pem index 302931a0971..99fe3c464aa 100755 --- a/deploy/dockerephemeral/docker/elasticsearch-cert.pem +++ b/deploy/dockerephemeral/docker/elasticsearch-cert.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDMDCCAhigAwIBAgIBADANBgkqhkiG9w0BAQsFADAnMSUwIwYDVQQDDBxlbGFz -dGljc2VhcmNoLmNhLmV4YW1wbGUuY29tMB4XDTI0MDcxODA2NDgyMVoXDTM0MDcx -NjA2NDgyMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAo03bK4ePl/aJdjMVBegNVn/usLAGstuSYx6fjrkCpDoa -xlsWhKTIzIS1Kct2az8PS18a54G2UEC1LG2RsonD/TqEqOzm91B9IvQk+4/KEWFk -bI60yaLkoS5aErqCsKVirjrMY7iUHH2theB2VCzhKQbe/NWq6/9gs7kWis8ORKVw -MZS7Mu2mP1isxkpDL/faFEOO8xICg3vT4tPe0w+WCkcuIG8iP+/EuAvJfS7mzlr9 -Dyfxq2rktZPcofgxzlpKo23iPwxwtoTq2FKYLWaF3WPb6jci3YOrWeAbo45Mv7zx -iZc76ihuah7FfuTu6zcBedhuuqPKY0xyQ93opWKswQIDAQABo3oweDAdBgNVHSUE +dGljc2VhcmNoLmNhLmV4YW1wbGUuY29tMB4XDTI0MDkwMzEyMDMzOFoXDTM0MDkw +MTEyMDMzOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAwDpnRRMmtz2g/5RBTO7FcuOsy3ss5g3E1npSOyQ4MebM +vM3fIZd+xVPNj6ZDxIHotLS17av41YmHCEtSgu0BtOda9hvn62HBR3Gyl6TDEvvC +ptHqxc+5ttrdKc5XqpI2lJsNUOUQhwQjYyTvbGwF5YxtHGc0mtUEJ7d/1qoT7A/y +W0KxTfclo7LPPBMiIF6Qzjn5iEguVPQm7jvQs9UbHad/ffMKDEBkBNs5joYTzLil +NFrnxxQKpxf4qS/cA62zeBS2dVvgZHqNeHfOF+v1DHItB/uoB9zRKFPvArI+Y+RR +uPsXuPNZgN210V55iDyDdU31Eh78ndShCSg6X3rfgQIDAQABo3oweDAdBgNVHSUE FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwFwYDVR0RAQH/BA0wC4IJbG9jYWxob3N0 -MB0GA1UdDgQWBBQYML2G5YnBl34yhh0UEPYr+rGdQjAfBgNVHSMEGDAWgBRRMD3F -k8uiR5eRsezFZrT0oMKlpzANBgkqhkiG9w0BAQsFAAOCAQEALHy5QQLAcThsdt9Q -cL3VJzkPfDSOwFJ4L+hRMZBrPh0flbQn2gziVlbqdpsbh7ou1OTGJRkSnfWQbhqo -3Jeh+i6LhW1YjvX5n/zMq/A+gcE8DgK1m7m0IYaytESmyNbsbYcb8lv3pis+cZWb -ISFH8U/TYvpUgS4PM9+KtdfsPatakSRFlbcy8/3JLe5khnwdmTb0TGKiIlEfIuKh -UxVzJDUB0Fsa7ZRXEBkbQFPlWFIws3oATwMSLzA7AKEHEY3vf67ZzcPxD1gZU1Vn -2W8ABV25cT8EtwRldvZ9E8IwezAeLEi92x9LUaBy2YKBSgmm+wH/0Do8nGG0Zh5/ -hFDF8w== +MB0GA1UdDgQWBBTxqOE025egTOExeT139PqO6dxGKDAfBgNVHSMEGDAWgBTWzp/V +RTGqs/IwlrROQGDwavTpyzANBgkqhkiG9w0BAQsFAAOCAQEAWyJLKHLcz3oKVZnH +KP7AR0ty0m9H4yeHVPT7/IjfUsemDkFhk9xcSHlqVEqNu7CHL/VjZ6wke79yGm4L +zBIqiTGKgHmFRTn+19bNg/K/IodAXaTWayEzAwrJmEU2W6aarxhL6IiyHHnDba2J +u9h/cVV2OGODdg3+QuEr/3UV5XQX6X3hVGa3YUb/sTt1tuj4Rs9e1UCoSL2+4NtM +20De1G5zF0z05SP5z9H98sryf69PysJjmSWc601S4iR22o2nGDA/JrPBnVHfL0Cj +Aah5YYqY4m2llOwTGTrQdrzX2Oe2Qwcm1ofmn0P8Y4uYvqg9sUXKR1yf92PjytoE +/ZIJPA== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/elasticsearch-key.pem b/deploy/dockerephemeral/docker/elasticsearch-key.pem index 0a1e2ce6c90..b4346d3579c 100755 --- a/deploy/dockerephemeral/docker/elasticsearch-key.pem +++ b/deploy/dockerephemeral/docker/elasticsearch-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCjTdsrh4+X9ol2 -MxUF6A1Wf+6wsAay25JjHp+OuQKkOhrGWxaEpMjMhLUpy3ZrPw9LXxrngbZQQLUs -bZGyicP9OoSo7Ob3UH0i9CT7j8oRYWRsjrTJouShLloSuoKwpWKuOsxjuJQcfa2F -4HZULOEpBt781arr/2CzuRaKzw5EpXAxlLsy7aY/WKzGSkMv99oUQ47zEgKDe9Pi -097TD5YKRy4gbyI/78S4C8l9LubOWv0PJ/GrauS1k9yh+DHOWkqjbeI/DHC2hOrY -UpgtZoXdY9vqNyLdg6tZ4Bujjky/vPGJlzvqKG5qHsV+5O7rNwF52G66o8pjTHJD -3eilYqzBAgMBAAECggEADKBUmFaDEeRUIjP2kFODUyimwLmIzbkau0LfDQQdV1ZO -GKpqoMyJi//yGUOr1G9L6ZohCeemZtPB+P1AcnX4Fd25lm1kDu4wZrdKyWUyN0rO -SXMf46aObS9Dc/GWG5NbVbj3xvllVkNEsIu3inBCjnoDXA18iYJgLIqHMCoB4pO/ -tDsuH+2GcfnkMsaaMsWkqxAPhCJvzx0Qwnxgm60xE7FGuqkoECoDHT5vFeVREiEt -iLYUJgAOb/uIQGU96EHBVhfMSRNUmFRc/oOA8kHKO6zjyH1uZquUNkxsiYsphNmP -Qqan+Q+ZaJ5wtgyNzdJL8O0dwzBksLGqQOYeQBH4qwKBgQDZBOJUqeldZ2V7r+IS -n1riDnFD8MTr5UvvJhxypxb3xHM68LcqCidmo+MmksLgRZ1mPvsh2t2yLijKrbWU -GcRshSC1vqLUBUr+ptaNtR4WbEWwlFcS+ky/tJbgEnA5ykxlyvfGPDaNHQnf1i5u -kJBuP8BbjjzxVUhj7d2JD+h0mwKBgQDAowPoOffyaPAGfVZh2tQdquWSQmxqF+yw -kBlnZzewk2TdrQi96uOxovJIt+5L5OvIWLYvtWT3/VhQnNeBAmWbTZufbVwyzu9W -N/FlQqaqoR2F5IDCUuJwHD+d3b1l+R2EVSWhMVBBZgNboqMO32YYvDEmMjZWLumc -WSHAhDJD0wKBgHvnDvV5gNQkGUux0mgBdVkFF+PLThLEekMSxkErZrCVB5kKH/kv -jOlL/n9iYUK8XC4pHSZqGBMHyaBV5wqkX4H5zAAX1E0qrHORe4OyeXgh3vP+7WvX -XgRBrbZGsK498rpXVHlonViZ0K5sUVwsy1k2qrNbFS5QG6F6B/aeD4CrAoGAbfPe -BAwqYRvSfPHHf30m+3QTKyNsvDXBrJnjVikNGWVX+kuMpNJQepD2V9lcU8draWRx -QNP5uK7LXN/ZBdL3aeinPh1utbV12LF/wHuFo/joYAcoE0K56qHFEfeB5pWFaoYr -P1FlbJ6spf4zsgaDQPUR3KpmZ1TJlKsvX7JU+m8CgYAz3H23AnF1MEgKvi4xVIwV -ir6yD/Ezb70BB40NdNIib9XAYy7t8JrTmZMhHRLrC88i7vwy5KFd2m75XE/PGy5n -Bc4/O9zua/0wxAgpKKgsiv92H10XVP6yR5xJ//ATO/mGGFllFrZQae01yZZ+qwlA -MilofPD7tfCLoQl/SQ0KyQ== +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDAOmdFEya3PaD/ +lEFM7sVy46zLeyzmDcTWelI7JDgx5sy8zd8hl37FU82PpkPEgei0tLXtq/jViYcI +S1KC7QG051r2G+frYcFHcbKXpMMS+8Km0erFz7m22t0pzleqkjaUmw1Q5RCHBCNj +JO9sbAXljG0cZzSa1QQnt3/WqhPsD/JbQrFN9yWjss88EyIgXpDOOfmISC5U9Cbu +O9Cz1Rsdp3998woMQGQE2zmOhhPMuKU0WufHFAqnF/ipL9wDrbN4FLZ1W+Bkeo14 +d84X6/UMci0H+6gH3NEoU+8Csj5j5FG4+xe481mA3bXRXnmIPIN1TfUSHvyd1KEJ +KDpfet+BAgMBAAECggEADItTJWgSSPdx2/PcDg3v3yc55b54R9wCqh9p4dejfigq +WLDTnJDTEkP9gGAQgJCcs7wuOiAUmTTEFde6fvZB/AD0B+b6y7rBnuytw6UaINFC +mtnMiROc8jCWqa2APY6Ulr6GkC6elT0BJS1qHWhwOxJepXGbtnXrsz7Pjh3jtm4X +8tZ6/msANBKGzaEKaIg41YxUbr4tWr1G73IlnWkJwVrfLTHm1sAjDTqoYFo3xdlK +Ow3vm5+Zscv6sZmRzZDUuEdaBuyqkHr5X3L9lncfxG6WdpCi5JUFfihxegnV5yi6 +5dL3Spu+ZdOrnHsQ/leaSEwzJOWAQlYvCrYoiOSKxQKBgQDuYeyff9JQkOxSlnzs +UO849VFQceltgjpiNF8D3O4bF6eE09qPljs0zWiGBvpwNLX+L/xJTEVv1625IB8I +W2SvBu+WhfwqlThwNBW0HWAn6V0ehEgu7dWaU2X5cNdyGdr8wqkLyZhFi8BepOyR +ytvO7Azeks/XnpJtZwhJzy1qFQKBgQDOb0MDwnEdYMmEZ3aDSvXO8xoifblrCTBl +ysCtj1jmplp0FV9VeWsuJawovzF8DaDtuCUOLBSimRU/56BflWRPivwSy2b70gJr +LXcvN1Wws2zBd6YJjqNCyu/d3eqLV8+YTL/ZpjyI5uEREfoR6mK3ze2S4KkOxPW1 +snvtcq7WvQKBgQDPfnMtvl/9ergJhy4DsMsZpAb8Y7rQdDuHgZh2z1Z+RI+vAYzL +0POGGYlyqB5Tjr4fG/uYfYgvOuffLQN2Db9Mzle7iLKfCjYPDHcbyToKY4mHZ5NB +Lgnwg8lOXxdZHQJNYs8sEHS3jFaMyzeUC6Rar4LgNaAuSbug+L7xKCGapQKBgHF1 +jWufjvQKojd3dherN3bK/m4+k45Uupj32vaJdt8uR0DODlu4JER0yC6NBvGbu/tr +3lHvwFets5QwBmEChuOBDBJ4YN2/Cz1E++CjlSFNPFUJIeTW1Lx9NWDH+4UieiLG +7Br/1v2Xh9QOAVefbyp+sDit6b0IW9PFiX90LMwxAoGBANorN+N/857GHDfZNadH +3z1TI3vFQCC99OZr98IUVJ2KR5/bxhnlknzY1BOqX0KVZxCelXUlyc1bPc9NZvDv +dpE2tuMf9Yi3QqUDMAz06NsBbJ07b/7Te+nPFFWnts6MLApM8BPDyvkqpWAiWMdd +5kIXKnYdDAyHqhfz46+yzxQa -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-ca.pem b/deploy/dockerephemeral/docker/redis-ca.pem index 0562d9cdc82..1bfe054b3ba 100644 --- a/deploy/dockerephemeral/docker/redis-ca.pem +++ b/deploy/dockerephemeral/docker/redis-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-1-cert.pem b/deploy/dockerephemeral/docker/redis-node-1-cert.pem index a313b0e38f6..7756f82bbd0 100644 --- a/deploy/dockerephemeral/docker/redis-node-1-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-1-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjFaFw0zNDA3MTYwNjQ4MjFa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPy96cVsBu5V6JjKwB -zt+S7QuIXuvwVGAkzNF2R+sOSzBReh4fKP2bhkHp2bKsr9ZxFw7ECndQ8npm7Q46 -P9/Xryj7lSVe9FypB1OAhnpP+cblep+I5y/2hKsvC19y0/ynDRzlBz+2nczjr++A -uQxOHghDuEew3561DLr35K1XFkj7Yg4vHU776/DcmwnWkdcetwxZ2V89q+CHtsMd -Qr/ZZrnod4hwVv9JzV14Z99aJv2aMubchNKZUqRwnv6BHSktzFXR1DPErAGErIVj -IHkTXhhIHSHD9e2VK5Qlk169EzrGNMSP0CT0i+yh4/RQgIJIJfLQPP4uyWppmj/Z -TZYFAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0xhwSsFAAfMB0GA1UdDgQWBBSnv9zqno1wP7lB0bx2 -RSml5I2yEzAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG -9w0BAQsFAAOCAQEAIU2iEbP0fDoDejqMnv38VdumRegI46/HEpWNSmySGyzoCjnH -70FRs8B7qLJd0DZ6ofRUH1p5kmJ3z3SHSngEiCCU7sCwGIl2ahxf3B8tci1wBeRg -qbvcPgCSFqHxR40N7+Uc8Wp3E1UpidWPlQqVHffHAF3KX6M0ooikRb7qNPx4jGPI -gDy0gCFxau1u5V1JiXJ75hjSS3/OFysXweZymTc5t0e8D0/2CQCdvVjEZq4//bvy -xIRHWzOMgoNhW//io4QEUhe2c48QUbTAuXIzzuV23h8BZzIwXiCVlAtQPhJWJsC1 -pHOVb8t/4hdvTAyjmMjwOICSxBqSh5qh80+sgw== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTtp3U0VPwBVJNULQF +S4BBlWBNf/8NMidOq23IsTcjkIFWO1XL+HFZoa1AArUSA/TaLBYyz9WmX7eLWvAU +ADM6mfAf2V6whmIs2H9ZRnY89bFWO2hzLWWp1qq3dXK1ywTLpw7DqU4OT0rtYZbp +QHeVY0mKKspF+YJTZzWB1hs8IX9355wXRlYBLPNQ5oHRb4/16J/UUFPIJjpUyHsq +T1LWmVREqisrq9u50FnNPeLXE6SDnHGRkYGQXzQOM/yAI75/QUOOqo5rt3Et52t5 +pkOT45R0PbAC2UpR1usew0zVjRoQfFk9n38tXUSHKw/tW+ZY1xJqEKEiLGfnhhza +t4kjAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0xhwSsFAAfMB0GA1UdDgQWBBQsOxsq4X8dS/Ddl9l1 +TWDb8Q5KKzAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAaByeD08xOZCV0ZKsx7lHtiem5/XG01rMcDxNVrVguSD+mqhR +/j8ciTW2CruJ2X8ReTjNrI4X1nWLbh4rsrA56q4xkjkgJIfWQAdKCibXTrHOWfk5 +dcYG1pqVdpD5bvsxAsY95jxqoVJHXHGN8ynC+lV39HbDJQFOdHLAP66NUrphp76a +OZKiuzUS6naeiHWoA9eIANFRz/JoQvyp109gdce5MH0iFwGFqNJU2rwilOpzQVc7 +qldx7MHMnW5UYSTqryTOr8PS+xo24TSdHjIXmnOO3Ov0Pw7iPpGVGj56dAKgEisG +yGOAWYto8UBWKLox1vSSlfdkhAoDXluvE8EwRw== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-1-key.pem b/deploy/dockerephemeral/docker/redis-node-1-key.pem index eb3937a20ce..6d8b29bbdee 100644 --- a/deploy/dockerephemeral/docker/redis-node-1-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-1-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCPy96cVsBu5V6J -jKwBzt+S7QuIXuvwVGAkzNF2R+sOSzBReh4fKP2bhkHp2bKsr9ZxFw7ECndQ8npm -7Q46P9/Xryj7lSVe9FypB1OAhnpP+cblep+I5y/2hKsvC19y0/ynDRzlBz+2nczj -r++AuQxOHghDuEew3561DLr35K1XFkj7Yg4vHU776/DcmwnWkdcetwxZ2V89q+CH -tsMdQr/ZZrnod4hwVv9JzV14Z99aJv2aMubchNKZUqRwnv6BHSktzFXR1DPErAGE -rIVjIHkTXhhIHSHD9e2VK5Qlk169EzrGNMSP0CT0i+yh4/RQgIJIJfLQPP4uyWpp -mj/ZTZYFAgMBAAECggEAIshtDPK0Ii8T9uBC9D4DGUKDNWXGmzABwK0Vps+fNWot -IixQsHdlHzNy6rr47CotjFYIQZYJhhhdUNvbQu5T+lN5rZ+GdmlUJ6PoyDBfUkyo -VraadA5+LNqrINpWqItMNGlo2aKvAAC8QMA8Ri4c4qGDnMPs/YUeGgveBxw27NdP -ggLyy9Qjg/x7B2evmOVjo74fOHDma4z4LitwcY3rHHeuvlyDxB1rCbv6VbSqiYX6 -UNEwwmQRW7nOp9EGQTJeErAR+cUV17luFlC8KYD0i8IXoeswmfmd09MLyneSLc61 -C0HzKzk8Vkpk+A3aNmX0lvSfY3f7SzN6ejHc9n1OeQKBgQDH9CDKmZKNG6vdYeRo -nje7chDBnDPCilrfyYqcl6s8H538CIhRErFbX7nJA+dAXz1lwmAgl0gfWqRhkHZn -GVRxNp/uG6Q648hhUtVLrIVtiIRWKUjIQdle3rBekw++yvNHW9/Q6lI+F4OERHpN -VeVMJl+Gt1Umxww82xfNR3QceQKBgQC4Gh8LTT7m1pCypFhYlRLS0FJaHxDH1pc/ -7A2DxKJeJ10qDbpNov6KPVi5HyDy2l1cc6Y6MB4VGMDE+6ggxiquCpLel6ESPSl4 -uTerXQkSqRUnmBirimJa4G/77wQDE3gxfcAq6pTHhcYqd9OfjfnShlAdr6rMqM85 -pjGvjaaK7QKBgEeH4Tc5S0EptgkDnSeD+mIXQ0FP9QBSaIIIYor0gzCGCwl/r+x4 -6HPMwfTUbaUMrTU7HRJrrERzM70nZgQp/phltz8CKnVayXNvo5hnxm/R163PJRdm -3zFeLvAWYhqaFf/gMShWu0c1ODpYGPyTjuz4CVJzQYYWzRz0MAai2jnZAoGAVSoY -RFUmjQijBVDLYacMfyNJhVErpRZa/4IGOneDGQUiruqMzY9iKrb4TSLeThm/6J3D -PtW1hNLfkgBMpWSmp75SdNA1/cb3YVZlL0upf81h8OAGQYyRtTJv+151P6sJBfQD -Kpc73hS/ODQYXI4EDGR/uUvjOiu5ORTtlSV07n0CgYB3QkdSdH84v1OVM1C57kqZ -4cbuWqPUrnb2Mevqtcjy4F6uDZxFO29ifXrOwF7LF1ewBJWGg+4KfWg+7Ja80S6u -u/WTtp1qmt9zOM72AnYTbLA8OlfzSL8fJ7QKv9RnDRiIj1j/MFXSogbxKkBSrufX -LbSwLKtIhQ1nfwZVxGSqrg== +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDTtp3U0VPwBVJN +ULQFS4BBlWBNf/8NMidOq23IsTcjkIFWO1XL+HFZoa1AArUSA/TaLBYyz9WmX7eL +WvAUADM6mfAf2V6whmIs2H9ZRnY89bFWO2hzLWWp1qq3dXK1ywTLpw7DqU4OT0rt +YZbpQHeVY0mKKspF+YJTZzWB1hs8IX9355wXRlYBLPNQ5oHRb4/16J/UUFPIJjpU +yHsqT1LWmVREqisrq9u50FnNPeLXE6SDnHGRkYGQXzQOM/yAI75/QUOOqo5rt3Et +52t5pkOT45R0PbAC2UpR1usew0zVjRoQfFk9n38tXUSHKw/tW+ZY1xJqEKEiLGfn +hhzat4kjAgMBAAECggEAFqMmoixVxMrU34Z7ETve9WRC/VZrz53mvQ8weG6WfjuD +0NQcWuhwOkzCyR7g/JGmuzNOllVJu3Xtmr15ATJ6R9BQ8B7edJKR6cimaUXS+7ar +pRRKGVKn1a6p517sCoswMpRkzEAMpBQPZ21xZPRrNPJ+WQM1SKEiscdN3dmmZNng +MvroH1dPVbyZ49xkjMQ0NaOtk4rvopzdKKZea2qz41w/vXR9hShnfVDs/q86clmx +5mnEvXcEdguioAfUWz+qQ7dXlWsASKa/gAMjUN9GW9uOn4LclFsVCD2MW+IUJMxe ++JtFM0xiQ3HaK0Fem8+XR8mG3BB5a/06ZHBfcv/lsQKBgQD4WSJjZwMBG80uGidR +ls+VhhFjysxm5qrF34MWziLczi1nAStc/PzVcA7tHapKX5JiKYT6d8Ptngz6FLIo +/72OshmLzctxRprlpihWxMIYOqwb2PLB0//ghuUE81Zbxj1MQ6k9WbTGHBwUbaiv +PSzclhmMubypfLLcmMEnHeZFswKBgQDaPIWmyax3Eft8DzC3Om7X3WMN0NXE96z2 +6hUAon5tqinMuWUWa2cyWzPsdBgFM8mCynoiIu08YFpZQivoB6QSal4x2mLg4R+u +aLm3h9f6NS4/VvpWPL5wMUAqeCCbP/2PVKk///0mtQGixUOxeQftTncQeLtfXOXd +4gDJHjfW0QKBgQDND7xnW42Ngsk+wfWpVt981UDSp4dziA+GZ3I0iG0c6Vlv7fVC +SNrz2h1ZCN+tnZCfYS0eK3oqYBDTBfe+Br0ccE7Ls1fC5svLyBES5FBn9TpbnB2G +kmh7mqbMGak7CktfB5dcww+TbW56J7nbSKYcVgwuuMbhI8gEglUq2XNkJQKBgQDV +VojIzSmdlKSlWCwlUif9OdyVKutuizg4gAhcAH1bMxd9nFbnncLaBTIzGiJJI6EA +DHNsX3xOo1pvGzLUtnN71SOT1IsIjsprstCqS0+ktswo+xvppaP9BQhW++vUGLAE +p5x0hgixCA07U1+jZE+NekEGhx+UT7oeN8rQ0IuBoQKBgQC4PF4WwqashYHkYW2j +4LaMu5kWY/0OI9Vh/h1iOcKPzVUn61aabjsx1wF9rummIdxP03/bs7ZpkwPypcVR +v7XnNbi+hDZFEN6s/+Gl4S6RfAbWXs3sgnhVlctlkzzwG8UHCef4DWMPxFI1JQI8 +X+SdDfpmB/ayQb8TlYvke/s8cQ== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-2-cert.pem b/deploy/dockerephemeral/docker/redis-node-2-cert.pem index fb3643482cd..ea4b4507d6b 100644 --- a/deploy/dockerephemeral/docker/redis-node-2-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-2-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjFaFw0zNDA3MTYwNjQ4MjFa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQtr75a8WIjdBEN74y -ZfHUnah2x2Poru8Gia0JhsPjhq7oB4HxmIxujq8XQJrnO0kchiuuCZCzICRucpou -LzKLyAQYJ+NmlUZQvp7/vR3gNp/AwjGQVKZ/uBecYoPrYaufXAZhtkabmUCufxqf -d2KHjo6tIEU2JVL3K76Whov0sreUfo6u4+SIBnbxUa0rePsg/El0DQNmgog+Bu5H -XKuKrm7G8GJ2qIMxH+MgfcLxur+8BX9uyAJkC/eo/pDhk9f6Zsb3h0dCDwZY4yQO -pKaN3YnTwHBAsaQlWQS8xazIidUBJ8mMWaTOM1bL8U4sSvB7QwR9gk2AmSlCV4i+ -vSzBAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0yhwSsFAAgMB0GA1UdDgQWBBTPy51DTA9zTKFPrx2/ -VePzSvmlnDAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG -9w0BAQsFAAOCAQEAAChPwq5/b0mALMwMGMWlL+JVtiw0eq9zmsR47AWBnbZH9Hao -sDAJiNUJxSA34xY7toL6Z2rnN/HmsKIqfLuPWk5mWf1fWEgj0E3gLlO623XziYID -YDBl9plvrrCR6omjC+9jaXWHk+HGCllLx4lHCyI58IF+hmCNydhSCvHWp1inouMM -FEq/l9YElna3SZ238G4YppBHpP7fsKlM59/8zPhxXCE4xD2GKfvvWbUThQcbC5Gb -mbnWb0D8EFl0oCW3I9JBMPsjeC1yiFsAkDG5cvT2V4QbEd1zXqawSJgP93/tgOUo -gxmeu3GfOc77PRDTJvd3eLTSQFvWRrK1Jlv8lQ== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC4mm9GWOVh0ttlzXaJ +11/rQQX3vYQ3zyeMdz/KGTKArOF+pxVCxbETlI3ZufO8Ht9zqa7Doh5R86iNtVMR +LoQZVWeXjQsMATwNUZT3lEOezpDE0ZI8d5JyU946Z+7s0VjIMbXOzjTSjTNSi57N +li59/1NTG5CW9EtgnnYoP5SOrYTpK+fzawXD18tD8kq/VBLt8OoG7xn6DIpGsFr9 +h1Ot/yrUejvrHg2KIi3av/cnqA8twzFpkdvGSEarjRuYG6fHGL67dgSpLvzh/v7h +QiJDFFB8fHnUc5ioZXFw88P4Oq7UlzBhnkC8nhUi1X1vWoF9Xz4FXXJ1P4WkZfWB +Vui7AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0yhwSsFAAgMB0GA1UdDgQWBBQQK2od431iWKznJEQz +zy5GXgt1DDAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAhvNbLzlY4sS/xmn8alzIYjY/uIc5c0PaUaXc7SSjeoChRfNQ +tE5YLmOo86WYNThtaNmiRLFv3yNBXCcqdVgNdL78EIQlKvPxHwzZXxkKDmOcfIZS +nUa4w+OmKJLsdNjphBGmR94h8WycwoFMThw55vnTJ2+AnCFPsLDfjtHiKB8AsW8u +gtSTtVyu+QyvGTDxEFDgqFgyFjJpVp37bOakRuzuZZ8VUssQbb11YHyhnNGTcL3a +hLXeGVSRA7SyDXxxRs5PmmJVsUOWkgbIjguvZK5APpqaGEYwBYo036DFSgt6DTOu +8YsCTeSOmue0xNlPDiVPSP8HUGfq3tTBKMXbUQ== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-2-key.pem b/deploy/dockerephemeral/docker/redis-node-2-key.pem index 53307028d70..fba9118998e 100644 --- a/deploy/dockerephemeral/docker/redis-node-2-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-2-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDQtr75a8WIjdBE -N74yZfHUnah2x2Poru8Gia0JhsPjhq7oB4HxmIxujq8XQJrnO0kchiuuCZCzICRu -cpouLzKLyAQYJ+NmlUZQvp7/vR3gNp/AwjGQVKZ/uBecYoPrYaufXAZhtkabmUCu -fxqfd2KHjo6tIEU2JVL3K76Whov0sreUfo6u4+SIBnbxUa0rePsg/El0DQNmgog+ -Bu5HXKuKrm7G8GJ2qIMxH+MgfcLxur+8BX9uyAJkC/eo/pDhk9f6Zsb3h0dCDwZY -4yQOpKaN3YnTwHBAsaQlWQS8xazIidUBJ8mMWaTOM1bL8U4sSvB7QwR9gk2AmSlC -V4i+vSzBAgMBAAECgf9fJIjqKoOU1d2a1QGSKxn46+XGP7gyDSZPh9mmZHvnvGQy -YaAnm6+b6aKBEKWMj/oQ7RjJI8ZSrm3tHoQqVlmaxUZLvLAGk2wtlesYdmpBsdgh -U5hEf+vM5p6pkdjEWqgufRGe38WDXAxtGOpWx2I8nHMHeMgUM4keih895X4ajVee -34OVAU6/4udDr89sak4dA/BIQIkUfyWRPApYDOcN5C5Y6reyrK/j5S96TCSNrMdo -fJtup4plI7rhXskJKR6/SXAjBfvJw7Mk6IGDFjZEoiE8/6e5hPlp/b5tZyig5KVt -gTGHPEVxe68GQP4eVGgaxbbHQmHszjJwhsq+r/8CgYEA8JruM9ygGUkB1zY09dbV -DCv/2Sf3GlAtPjJM6bx92TyjLJC7ccmq5IhEnEml3yK2se94KTUysNlOuG5idg/3 -5oqOD6iUi77OmvyGlh3BVnAvx8QUekENvuUgfD0TGrI1Nme1jDN0RGxYAkFSvz1d -4bDt0goqljpKoMdRbZ53x/sCgYEA3hFw6Nl2RVAkUN4DU0b71hscWiV/YvvwUnXw -156iLKxifSLoPTOy2U68ZkSQVOPRFwyupZf+rHhbIqrsr/lu2bpqaEjj/YQ+QJDF -vX+2EiJnyOyCvAjox1f89UN9E3GxP1lHotdoHp6AdB0SyOgTnGzt1cRY5giw+uQM -Zf4YVXMCgYEArBd0hr2n+U3htie8a5YUXhdecNkIAdcU9SaPIqNCNE4NvANtPq7q -v3jD8jEvJdEzcUOB4598OUfE6V9yp1U2j7vMbmC6ltWL+wjhzp9LuOKXGkAiEWtU -RJSnzpT0hCSwsNAu5y+qWoJP1JUadVSUQKgHAjNpUHgzBpppoIk2zV8CgYEAqGmF -vbGeNnbO891LnE6LExdAa0Vg1IrI+WCkpIGT8FlT4B8nDbM1ggRqcQyygQ69NcPS -d5dL9zTXuPTzx4ldfhYYOLp+3Xb7Vy/0JwDB7gLVvtVPWJdRIk0idEcYhjSE/cwR -vfeq6P2/4U9jPaZzqQAbZzEfUmVpAv0MQhVwEu0CgYBRJkaBDjot0zSwtfvZ38q7 -iiCN5WpmmXpbsf3bg4fHLITdEimhTgA86SGjCO5kP7TYdxbdd2FquusooWuwOUkh -a+86fKsY8SmDMyBeUezRci4va3jEndl7+Qkk6re6M3GheKf7c837la1Eiyhk49sw -B0q/0CB5YyVsQZeEnEU4oA== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4mm9GWOVh0ttl +zXaJ11/rQQX3vYQ3zyeMdz/KGTKArOF+pxVCxbETlI3ZufO8Ht9zqa7Doh5R86iN +tVMRLoQZVWeXjQsMATwNUZT3lEOezpDE0ZI8d5JyU946Z+7s0VjIMbXOzjTSjTNS +i57Nli59/1NTG5CW9EtgnnYoP5SOrYTpK+fzawXD18tD8kq/VBLt8OoG7xn6DIpG +sFr9h1Ot/yrUejvrHg2KIi3av/cnqA8twzFpkdvGSEarjRuYG6fHGL67dgSpLvzh +/v7hQiJDFFB8fHnUc5ioZXFw88P4Oq7UlzBhnkC8nhUi1X1vWoF9Xz4FXXJ1P4Wk +ZfWBVui7AgMBAAECggEAONB+8r2lSygkEf7cPqwkfzjx5z9SlAKTf22sGj0LCAMt +G1e8+WHyj74msh3C3+D4kJZmjRs2Da7Z71MhD6arTUi1qzTjc3xlyQuUt2XQMe4N +LCX7xdRfJASf3oXiSMxdcK+r7swUAcEnTH5gD5HrGSgdsvRG2c6x7DiY0OZQiGBk +2rPHQDUKeb7Z0YLWc8nldzlnOe2OeWpFVEraAOzmANnV5FVJZP8RNoiKviZnB77O +qbA6Xtpg4ytVhMaymUmjkjdFuxm5XcMCOIz4W9SZVJ9uSzjZqATzgjsiOWYozB7q +2xb1yOyCVPgf+dZj32D8DvqSrwwRBR3LcNhnj2wUIQKBgQDqL4VNp54Lrf+oZ0ZF +h3s6lL2NquY0xHs91YvoO187VetyUlNjOcGXt8ROhSSAf6qTLQvrreVdjYHr52xr +smCohhQ9QDm3d+Inh3ARgr75O577aPwJHBmo0fnu9h6OkDr8nx05SthW4XenHqoE +iWQ9FnibAFz5KLBSYC7x9wfGaQKBgQDJzI3UC6AqQS8ILbcqHm7ZmnpUUjn7vPUm +lkB3/YtV7ewWJhFzdPdaKHKe2YO9WXQTCF7iPRK3+gWt8uh4DWCrSObBkmSUlF66 +wbRof3lsYiWDPed9OTgoDHRwbMPeYrJ3A0TMrGJQsbedljneaat+DM3kNgjgChfW +JiL0g9c5gwKBgQDBi8zMRT/lv0SQVepKBJLf85ZFw3zHF6wTiq46nPcz/uq8bTXl +yBIr5gEkM/3bBahgQtabTflGvHEoGvgMejxQi5+mj7Ij47zRlqoUjs5vBct7VWUX +0lWSpRe/W0Id6S4XIxnwA9+Qzn8pa7pwTWy+4BeFY2NzuSEgs8WYzOVsIQKBgHbI +IPOfpDc7ByQZRKdWIomTlE3t2JOFNgfwiSIX69w4n66p2bvMLYy0IkO+ZP0fmmNZ +mgAxUsNYN9+cC5oexbgMwUdPlESg0OG9AyQ/ZImXe900ov3ioFtyeVdzrhdIoSPM +mMKg9X3qHdp0gruYF4mqn8akx7SYPE+hQxIKSLVhAoGAJP+TshJj8xAeE1Uroyc/ +yIWThbp0Q/EFaXkpS6aJqBjdcLfh2U+Zo9ZaTn9OBlzXHk9WttzeWuMY9PrINodJ +8DSg5f0PslYxJ5DQuKnDWUeqX3zCnXkgnymlvh78t6wWp+BUAEjI8qH5IgKVwKd+ +VJbPX4mzhAl/0kIablU6SqM= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-3-cert.pem b/deploy/dockerephemeral/docker/redis-node-3-cert.pem index bed5a68b5f5..e550d0e30f9 100644 --- a/deploy/dockerephemeral/docker/redis-node-3-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-3-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjFaFw0zNDA3MTYwNjQ4MjFa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGgE9XH/XpZa8t3sxP -LXFdXr34kgvvNcQKgooBYJsb+KusvI3k1ILfpewbApd3jOh4JNc/lC6UvSGGuMGg -pBikz99krcIKL2Ls1TrZF6bcd2khr9c23WQiolvqcFwsuxsL14o39/wVbNKx7A69 -/qK79bcuWq8Fpnq/z5ysqZN1m7gsJTaVL5Md4NIFJcj43WLjOhMAR4M+NWq0iyVf -0RuiMltvuCfylywn7pCA8XFfdXEuGPaKzQethG0JE+PjUdTp6bwGpFNpf6PNUBcl -c1U0pjcLiYkgzMHv+tkr4Qpsha2imhi8Jt8ie8rKYtn6KFKqJozQUK0Xl6Vv3BTM -dx8ZAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy0zhwSsFAAhMB0GA1UdDgQWBBSbSo/v05wDR2uVWtEi -Usu54mOETDAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG -9w0BAQsFAAOCAQEADRE5aBznKA2AEft4YeXsTpNleV6YzjQzilaAtAd2QdtxN7KB -+Xi+J+H4v1WHasZmw9rIpGyzvBRPl/cZoThMJJvabFJsm+waY9j8y5H29w2kJED9 -mywAmy5N9xvTFoAyxB8ivZf1Lo5GiGAEg9ZtERJsdY5mU4VhENwKJB4mWTYbQw+z -u5z0iyRolEJyesrcQxtPVXaRkCmV/m+DBmfVlI/3AHad3927iq2w2ZW7kKcQ2Sp5 -vC48qdUJOsPbqc1V9XIzfUKVhClMtvRZ6T6b9XCHWr9lfUaEY3MXtwdiYa3zhMJF -VG/ouuFToXd0zTR+fJRcL4Cpe0pWTYnLzr2YgQ== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCd72omHFn1mEFw/GBp +gkPM5BkF7giGx7GOLyijCoi4NLNVKJn6mOJt9vX2PbBYedy1OcskObLbEwqUwcZr +7fVim34xrE4AmdJqBWTkcMFnhbjzYIynfvejej/05kWlzp3JuhTpi7i2W+nnZjqb +S6UHgeTwF/iENA1oysuq0jC4oaVGNa2ZCoz3W+uAEbpUYNjN7/uQeEwRyZjSEJUY +KyG69Wrl9KnzBX0mkltq8rJiCqaG+qOZwP+XH7TxjYM1SlAxLHrnjDQHWyZXJzPY +fikRk2Zf8nDobA5thXVR/2PicDxUs1VyGYSg/vK1EMwOIHIZdxalo0x75vFjBJ9T +l+HFAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy0zhwSsFAAhMB0GA1UdDgQWBBQyljx2OR3L7yZLVax4 +MLTDhj4xPjAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAUv3JN0ip/LWmtWHyqzPuq9tbVFs2M5waRO2ZZtEp6Pzudr9x +JKrmtz7IlnwK2E3eqw1Hh3kZYiM5XT2GzqFjPn+Na32i3IsR/S1Y4ZDq6T1WOjht +u+3EjrUvpTXAcLfaO60gJ7DrfC4PsuNuaRr23BiF3lIb7A693hnESg3EnUqGvAvA +ikR/Cv48kAvxpFlXZfnGApFEP49svj676emodRUlk4aCOjIniPByLF318Dl+MwzW +KbnjynzjnOqfcXeD67axFqIBAhZPBDWIDOLNo/ASAROkPntycBGFPUL+Wgdq75vs +8WnftwfCzYtKcASNVSeoSFtJhVy2cAqHK1bd/g== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-3-key.pem b/deploy/dockerephemeral/docker/redis-node-3-key.pem index 8673303ece5..d7be5cf147d 100644 --- a/deploy/dockerephemeral/docker/redis-node-3-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-3-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDGgE9XH/XpZa8t -3sxPLXFdXr34kgvvNcQKgooBYJsb+KusvI3k1ILfpewbApd3jOh4JNc/lC6UvSGG -uMGgpBikz99krcIKL2Ls1TrZF6bcd2khr9c23WQiolvqcFwsuxsL14o39/wVbNKx -7A69/qK79bcuWq8Fpnq/z5ysqZN1m7gsJTaVL5Md4NIFJcj43WLjOhMAR4M+NWq0 -iyVf0RuiMltvuCfylywn7pCA8XFfdXEuGPaKzQethG0JE+PjUdTp6bwGpFNpf6PN -UBclc1U0pjcLiYkgzMHv+tkr4Qpsha2imhi8Jt8ie8rKYtn6KFKqJozQUK0Xl6Vv -3BTMdx8ZAgMBAAECggEAXI6OsDrUYPCLhvF4xcCUOCvJm+KJkxA4aYglzm+b06aX -chN3fEhFAACvf4atVs7KxN60yU4QjEVGITn7+yoY3ZyZ9yl4LWScFX91ka2QHgPF -7zG9QbVokCexgTbEHA1glpx5tBA7KEhWVCUUWK4ndkokEIazToiqes7VKMNnYTIY -nEZAxjJYXou5k3lsRApoemJgotT/p1NfEq//ZjBWIpvtfch17GX/PkdPsry6hMY6 -F/LOQM9UUoJ8vEaeyKzkvu1o94gPN1G6Y6twijRj0ySPmOxH7yB9jH5uCDO+MHUF -9ssPVxKwqlkj/VnLLsQX7dYSHd58MP3m2nzJwfUOMwKBgQDyiXxsM+0sVe1xKnJw -iB8//ZOl1vhdAmNGR3Ofak+Eb2CRTZ1IlKQ+epvY+TgkTyydadwODQFE0tQubRSJ -s2s4j/2vYSRa+V8jaLAiwGNYxsnjNTjyvUSv1m/mn369frkPLdkvvYv9/DwS4zzR -7l5wolejXxp+UjmxP1khv9vc/wKBgQDRhRAvBq+3oLkMwrMLyrskKNvaWsBWD8OY -iCiZjmqc6qGRjgstAFUbGvDLQivZ4hB/tN3E0c7IxjLxLveXFGvql98XKi42rcBb -WnXXAGV0ulwPOxVDp5Th5pMy7nwkNG5sp3QGkduo5A1xYu4mXhGhGz95kSy3PAGw -4eWlOKxL5wKBgAybxyMc4/SNFwXuDfr5qJ48AYP6k/jJ2f1aU5FzBmU9IQkMvuN6 -DrvMxfNWqWuBzjD0wuLcHDfGug8bzpiGAknzel22sBwmoKKHm7iCxedkljRAnRBJ -dJuriy+zFPSm9NnsKUFJGlD+3uSgeZX0TWaPmfy9QfRVM/iZ8XlGrxhjAoGAY2GI -caXsR1+XJvRbVSaOafJvhj0xqiDEGF/NUjj5XQD2LkKADpJvy/GVcfQrNKhERy8V -Wjxip11L4Jb0ndbz8UykZyp8zTbRXQOljZwEg7+51wehaHve5OAnxirU+59bGXK8 -WDlrRcsWjUftyokoN5DjJNi1qxxteOdNtHcTUtUCgYBEU3Q5YwsKTBuemoncQQJ+ -Zl4r7QpBpD21d2dyqvfr+rIDYzl9eFAvWiIOymtOheN6wvYO3UX0WtAgsW53lnua -iHqvGocvPhEGlpWr1spd7EdPdmY7zCgPH/8w/mKlllMtqFPuUavHxYa8h1ThTO7K -CXpmRTh8KFcS7YsWeKQYvQ== +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCd72omHFn1mEFw +/GBpgkPM5BkF7giGx7GOLyijCoi4NLNVKJn6mOJt9vX2PbBYedy1OcskObLbEwqU +wcZr7fVim34xrE4AmdJqBWTkcMFnhbjzYIynfvejej/05kWlzp3JuhTpi7i2W+nn +ZjqbS6UHgeTwF/iENA1oysuq0jC4oaVGNa2ZCoz3W+uAEbpUYNjN7/uQeEwRyZjS +EJUYKyG69Wrl9KnzBX0mkltq8rJiCqaG+qOZwP+XH7TxjYM1SlAxLHrnjDQHWyZX +JzPYfikRk2Zf8nDobA5thXVR/2PicDxUs1VyGYSg/vK1EMwOIHIZdxalo0x75vFj +BJ9Tl+HFAgMBAAECggEABYejI9UiS+MaMiaOtE2x/16NMb6f4Hg600umFJoDJ3qm +PM5rIHHHRn+7JPVhU00RA+y+HB/uZJVKGDigsJloWhzaUkrs1ZXiiYEe2JDKH3cj +KVexamabrRxUA53RxSMdizlPZM4A7axSMvP1YV1IrfadBCW9Ydj2DzvqiFShDWst +asKPAa6MAU63zfZZaBQvicswd1nJUvc8ZNp1p0JiVcwWPWVTYH9d2c+0WZLlfCHm +GxUurHwyVc6b7T4OSrsiDaQN0kdLJDAYowp+T94JDBCH3m4e/NF9W6gkoO2UGXTH +6A9HVDI3FwUBzXdT9rL/Wmp4kKXB4xO2TU/yeZoYAQKBgQDK2Q2vG+BucY2aJxGw +7HNeXov2lLma2Vn4TRr+cyzcXH7Jmc8J/h9RMU7AEfg3CQwMbXE60P561/1q1e0Z +fD55x9ka3FZ2dG+a5CDzjkqnUgnLYOK1bxx5UUq+Sf6IeNjGPikejRPcPBmvFVuu +NvoPU0HwWLm67BnantJIpUFvRQKBgQDHUaPa6SIMGAWHasI6EvZMGgBAy5iJa4s3 +o+DuESF+6lD989ZnOltsPFeYhwbIzm14EzhK/y4MVR46gXLMZ9FwlGCGdXE7LWiN +VKCm9kRcxcH9Sak70LkZ9yv08Nl45f9vTOzBcKzu6bgZ2LOeSJ0oTmiVEb98pL7N +w6XxD2iQgQKBgAVVPYncBsOAksN5wXpQTRwvCij6cgLDMh1YEZyc9JH6kI7GT24o +0zP0QujD0C3KPBnbir2MHxSltxDm/OvNm2riOS/+mPtWRlThKIiethG+E2nYaz1v +5WS/IWLtWRbHbpOPsM8P0HTa06YJvrZO1bYvby1dd8yVRny77jVgut6tAoGAHpMK +ZHkgjORebMBWnNvtxgyy/z1735CMoXNU/I/KKJK+68WsnNcZ0QeMlEwaIVFw/1tL +Zk2wfZnM8kKLHonKWc+Y4uc/AEnd4NgbcKEUKXr4X+cdu5wv2KjOqFsNsPru7N7K +7n1fOaLGZ8iS/PO8j8M/TaaUTgVjc2LQoKKxcoECgYAtPzq1Y0yc22M+m1m6nK/W +L7rsUI0zDs0VZcJ5mrJg8nahOM/f+BsFYN5oAHYxuXPUyynZyD2nPtdsES75DGOH +PEqr9DhgSig4JmHS/6SEBnWql+zyNdn1/FaYOkKRHiY7jNhjTayiDObJrXg0g4OT +BmzY39BABb52ogQbjWslow== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-4-cert.pem b/deploy/dockerephemeral/docker/redis-node-4-cert.pem index 47c57e52a56..185f8f97014 100644 --- a/deploy/dockerephemeral/docker/redis-node-4-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-4-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjFaFw0zNDA3MTYwNjQ4MjFa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9uasXMmYQzllPUAy1 -/2+G7fnq75TSxTOKlJB/al70Pi7sCP+Pwo+4KEaZsLlxBhDa6vTVcKW6T7XRNyqf -bqcAgnKwiVFXs6rVOGCWArYOrP2OV9lmixCll86TYWaCPNjkoar7vlLWRaMxxe6h -LwxJ20xw/L/I9BLJmQ8+0VyuCL25meH/wRILuxsLKiJB5IcwN3Ku6WUAXLcw1+bk -tXXyKhGcr/SwHlPJaH+z94Cqo8yAXecWayCQrI2A01SWkPSb69j1aofO//EoccTf -jsKf+gCsNVYJvdsSj4SeRAk3SgYu94rSPkuEQQrIwAoO9Y8ErqSFCw0ir/0kggbv -NQkbAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy00hwSsFAAiMB0GA1UdDgQWBBRlLmkyRJrl+CRRT+US -42/LoE+lszAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG -9w0BAQsFAAOCAQEAe3dZVFLY27yyNBiaiyzXf6hBG5syraU5EtgzOiU9/+XEExo4 -DYL99BjznimG3B1fNHt6rafum90wtSoajkel0R6yfqh2Lu6mGa5U/5KYeu1IPU2i -PCi24ztGDwBUOr1ZmFoIz0ihp+hMjAheGY/gVqjIXbwLmj/Wuy9cH8U16LK0wkCm -QUHvwM2d3QQtKgrCqrZKospOroEETzbOkpOTPd7sFPZuhj3uecCsf54G7FGH+w/G -ISuKbBsaIkCs3lSl7CSRMyrU1BV5uEaw26pTN9zk/4vZ7lFupPH6rkJdrTjz2tQU -dBMaCG3GLDeEeIJRwIkLP8FZuF+y/j2btQAxlQ== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKxOmFo5c4ae38qC3z +I89R1F6xjaMyR6jjd6k5qsW7eU/y8+trgY4HV/jbzD3CDOjMkj70la5EiV+GTA0i +GeJH/BkKjqEPsIy/vAPux9xt2ZpIO9ieO2BF75ojrcM7tAbeOLQNAgYA7zAyIpQk +J2P8IyOYSJ31ujLJCR7d0zudAbXJXfAAyPUWqUrmmRHIY7hRi1tUv74JARqnU2tH +ZhFgGyBCaLROK69S/Wy+xPKo5w9Ol5L9eIccrK2/JwNpfsFAxJqXawNm1l1M9gGk +2MpQXzZeTg/hlusqCtPieOPUQKoEDXAgYArQy8iYkLuZzOtg2WwcPOhtfsgVRLNE +wXihAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy00hwSsFAAiMB0GA1UdDgQWBBQrI/peejY55qjXOc6W +XUU+/q6R+TAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAdf1N+gPpnkEHzDAMnK4kUCHq2ymLBBWJVAPDcmmtcMjEiEVC +/9BU+hcdqgLXxonEqiA4kEs9Mkj8AcUk0Dzl5Gfk2haZO6yzVEp97zwto+3Tgzya +0l6bvRv4OSfdVeSTYx8T48h23O8FBD/Gp9l5sFOZgc1TCWrb7ReJQS+XThAksIdW +DLvwbOU1I2qRL3ZbT49FAhmVcrMkHJjzkugXDoGG3Rgdzx/HePUjXWdWC1L+7/Kn +U/7w72ymW1mC5PbjoW9zzkVKesj++mhzSb5+sXa/is3hUJ17zy4Bqc71Mb2q8tqM +G/uMrdwfPeoad3qRVPRsK8QlVnJ0eIpiUDk+Ow== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-4-key.pem b/deploy/dockerephemeral/docker/redis-node-4-key.pem index 924d849b73c..355661d6a99 100644 --- a/deploy/dockerephemeral/docker/redis-node-4-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-4-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC9uasXMmYQzllP -UAy1/2+G7fnq75TSxTOKlJB/al70Pi7sCP+Pwo+4KEaZsLlxBhDa6vTVcKW6T7XR -NyqfbqcAgnKwiVFXs6rVOGCWArYOrP2OV9lmixCll86TYWaCPNjkoar7vlLWRaMx -xe6hLwxJ20xw/L/I9BLJmQ8+0VyuCL25meH/wRILuxsLKiJB5IcwN3Ku6WUAXLcw -1+bktXXyKhGcr/SwHlPJaH+z94Cqo8yAXecWayCQrI2A01SWkPSb69j1aofO//Eo -ccTfjsKf+gCsNVYJvdsSj4SeRAk3SgYu94rSPkuEQQrIwAoO9Y8ErqSFCw0ir/0k -ggbvNQkbAgMBAAECggEAHndePg9dzH0WYmIcaG1oX2Z/p3Zpk58PM8W/nnZaYSZL -KqQXReKcaZouHCgA32F1+3GXd17rfgumyr3tHkUKlE5eVHL4mPjFChBPkkdFLP4i -iWUaCBl0xuKlzYzqhSd4PN6pMlvRuY7dMfTy6PdBJesNT2eG9KIdEjp99DxygY+m -0yTnmsf5buDxlKHxYIUrEpLNo4giTHZdIVnf5zF+a7FGZJOOVK3VOVklPiWt60QQ -IEVVfHCpzUls14R1GAbJz+BRjWxE2xYYcrZZu3APerpZlx39fVx5nQ1Hyg2xQrct -gj8l+PysM8gNmO8Xmmzo0/yXY+gRQyq/IXVNCJKMwQKBgQDsc6krsC9q23GcCsD5 -LzSuX0usnoJnDA5/u8f/jFbc0OPepsMvAYCRQtM+/7CJh8JsuvAzXKGIp06rzv2o -cIHRfHY+SbQlNJUZWT9j+/bQuEnxgzpURasZFluHNbNJbmkFnzIOjAMCAYEDMBF6 -ap/xD3usguGdmNympznnQjbUowKBgQDNaRKafYFmBkrY3qimPXAG8XipNL/6b9y5 -dXm8s4HIMeuPi+qPk6jnOm+IuXXc8G4WNAZTQcM22spSJvQ10YHPks6Zp8atJtlK -mvmkFK/oTTnUjIewod9oVVurG5Bga1yRzEbkF3Zq38OYphe7GUc8DQDfq8V6jVQC -//JwGTPJKQKBgFhnmg2Kjv/90glMf//qpWC4onuEvC649EbPt6QVHXjr5PafFQTj -I+WrvX2lbaTODGRItHwPmxmTrDdSacZrYi4nwbHiLqdmdISIuMmyMAKzlHnm3Y0a -izETCd+QtVq0HDIM5lNIB+vdEhZWB4LkkK45Yr0KJj6dI4pvpZeQSx3PAoGAMn1q -SjkhTl+rlCUe1UXyvHIsU4MY0UkfuyJqGv0QoJHMsgsVS9diw/t0IOpdU0Jx/Nkq -2NooTtp2srzKeFQYEVqnl9NKnZMYBCOVy0QefP5Ggb1NORiA3pdkoelzko+xQFEy -96vguqJn5KSm3qF3Bga4OUJylw4YIWiiQfWf6gkCgYB81MbSxLsJ47068bS7oLMj -OrUmYoxzxTpY7SntxpDxYuH4WqfphHYum5xhWsJOsLPxJrEBAOqN70nQqzFtCPP6 -7dV5luVafyFtQZ9zD9bbk7GNxYCu7+HLpXYDqhcqmHP/juZEIe3AZBi9POYFklxt -LVAjPTs2DRn0NgkyaUkYjw== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDKxOmFo5c4ae38 +qC3zI89R1F6xjaMyR6jjd6k5qsW7eU/y8+trgY4HV/jbzD3CDOjMkj70la5EiV+G +TA0iGeJH/BkKjqEPsIy/vAPux9xt2ZpIO9ieO2BF75ojrcM7tAbeOLQNAgYA7zAy +IpQkJ2P8IyOYSJ31ujLJCR7d0zudAbXJXfAAyPUWqUrmmRHIY7hRi1tUv74JARqn +U2tHZhFgGyBCaLROK69S/Wy+xPKo5w9Ol5L9eIccrK2/JwNpfsFAxJqXawNm1l1M +9gGk2MpQXzZeTg/hlusqCtPieOPUQKoEDXAgYArQy8iYkLuZzOtg2WwcPOhtfsgV +RLNEwXihAgMBAAECggEABRxG5XEc0dVro9tKQy9DHaUcWN3Av/bp5QfCSluJPcMe +Nnma1JwQjBNVyJZidRZVtLg34Xq3SG9s6qnWh+Y+m4FZUTiMiyRwO7HdqII9hkA+ +gPUPLdfBwql6CU2rFsFgDfBAa3aCV7ovjQftk2axwKxTDJbB8mxFtObnsgANp9SU +c+MTlNTs1IQ4ev4u1i9ntR8SlFMcYQUA2AxvOiEDu7b4x/Ph9TEGuR6wLxdImRq/ +7hXcPtGAJKYgZLzAwCrZrjGjHILSskTxdii+Tr52Aq75SA3tLYGkJfSxHTJjFe0u +1k4Ot4uSEjRf4DIwohbSFFbK/ZXG2uscn36OphtbUQKBgQDwQY263RPJ/M5mKvME +15DK1JW3DOLWCBiV0XzwXsS+QpE8pKs2YLeyrY7sV/w1tdnfNdfINCknuzC4tG7Y +I+QzCQGhyKrP2nj4K3SsKUcFk6OWxgiPF5CRmlWySJ+H6+yITKcSJt/ZjUvvGQyQ +TV+IQ8s4RbKII9Pvifai6SLJ2QKBgQDYDn4bqIfZKR0I46//AycGXAUl55Yfgeog +8CR5MatNz26crrmDzjnDgsRbKUxK+UZLl/zEXY5Npn06sOG1G0bO/t7wQqcPsXZt +rZTx58lKvW7LQhEBAz48y9QeK3WUvT1E3JMJ6rt+6IfHvbvCLIu9DwyGJ7Zc7N+6 +k5GduC9gCQKBgQC4Zdfd3+hcUwgnKjezM7ARvO/buqwvEa+s7UgzRMlELdtC7C/s +YHcdUFAt3anZn2VFCBJBuqcLs4RFf1bD1WhEM1lpTparSUcnUlMN//Beu14HTp8r +FC8FUasMVuj6bXzxb8ObDvMoCmaJcHRQHNKBx2amHfhUvQrhAsalasIkoQKBgAFo +XsP5XiE5FlpXeW8U6y0sblAn6R99bjQWvHYZr78LCfJ1ZPoJ3vB6KqNZaojWhPG7 +JMd2wJWa7xfxzRar/dMdcABqvsHoaxgd2GmXFAWrpEwouwmhpscooNItgE+eyAZp +1X9sCxqxkyjnAJEsTyDFN1Ssb5C9blu92GYJrC1ZAoGASChIICMp0HWrDSRxRCen +Fddf993aEI4e46NTWY54u2p0Ga62XUcaw5eND9QX6craD8nd7mMhwvdvQ5vuORBk +m+dqt0oU5cloVp0srHDA861CO8topJFaNGWdF4wDgLU8YKRzd6hNX8X0/CCRl1vd +z/YmtxfgU56SaqExe0X65eA= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-5-cert.pem b/deploy/dockerephemeral/docker/redis-node-5-cert.pem index 465d5ae2f1a..e1221b9df77 100644 --- a/deploy/dockerephemeral/docker/redis-node-5-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-5-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjJaFw0zNDA3MTYwNjQ4MjJa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgkGG3QkBQRGOack0q -58F8YD4e03XG17KJwEFH7vCAuAnkeu2RpdN68DSor7yDJr8fpoIekmeYalJSLgR/ -tDknCpUmowWnO51klwK57Q7ovdmzscS/ICOs2zjqV2j9CqNXwCgD9ttjCCU0Sgcd -JBomQUK5dMLAxx8GzlM6AYdI5MKb9S5k9vRUtceZG6bOIBtmdLsFBd+B4iIFElya -J4aEVj3G5oWtSsU+afVzoFXHTTLUG4c22iOBPyF2zFKTHbkxefjY8M/9iShZxELu -SjSRckAA9pAlnwM0cKiP2afxBfw7meqAlZL+d7brCetO6rfTbORa0vOYvoGVo1YE -NWZjAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy01hwSsFAAjMB0GA1UdDgQWBBRhj4NDNIr0kuEwRK9M -uLRma8jGaTAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG -9w0BAQsFAAOCAQEAkV0sRJkSEixB3Dc2xrtm38vItBxfY8gXM+faqlscQsYveqhB -vdQv9Haq6sIp047ZqKppRDxr8CNwAJO/4af0T2bDE4+CkqfLmEtAlPMc0g6+9YbK -W1+U7FGnM43k1deLiDJtMKNAEemdVHox7xCdEL/kkOv51e+3wPs2dU0uIWsa/CWN -WLRvxnahih7dTghDO8J87gHS+hB3LSEJUFPZ3BZsDdJSUDfxFJK5cwV7hvT57/6h -o1zbd1+XN9d0cWFWP1ab2HWx4fDMNvMXaZRI8eYIT5xDWZhVBzxprnASjgUeUvHq -9MP1Rl2iGZEcjFpgtIXh8raa9Cl9R37wlwtbyw== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSXrnTzrNfHudXjC0A +h0CiFRe3yp4j6cN3Hfv4snS6tPWXM4MH9Dka8zMLZvzRVQZK3PxDh/R/DQYBZhpy +LEvT7wYCDsS+F+tie2sPjzSAbdM5dolD8fGwACOqobI4vPz0QrwDqHde/OdVWAZl +h5Pzw5rDUu84CdfPSWRN1pomCFWG7gVkpuFzIcBfz+smPodyw3BfU8969q6tFACE +pjGPF/RufmHoIaHe2q/c+3HBY06ro0oTqTtRe36v4Jp2HLE/jE8wc+YggTmHE670 +uEXIR9N3fF3AbPVnhimEwcQ5fpJtMonUvfj5Z/4KfKo/0Yrh0wljeRiz/tZgTwwb +h5ATAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy01hwSsFAAjMB0GA1UdDgQWBBRkbb1LScfQthztQJ3l +R+QFCKjXUTAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAn8TOFqomU30SmIDIHYBKMRGq3bVDLkDDC2yy6LCCwwG2rpoO +UtnUMig2w3iNQ6nvqR4LJB1ha0hLK5FP3iX/JcqZiO0NaucOTe7aJlt9taCADgAw +4vRW/pDuxtq7H1hc2pOue6i05UtGqy2E12jYowQc8a/5hylfEO3b5t5Z7xoQzyAZ +1ov7sYatBinwhqyDI5qNvZCuyT7SMx7H10T7cPrEec4uq55AJ0ReXnAAy1MLhpGd +nW5FX3F4gnyJcK2xL/V+ScL4NTzA8qWT+qOK33KxU1qrGripAkFaF6Z110nuIDiP +Z2tneIovCKKChgFsmZjy2spRpDw6R3Am6rXjpA== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-5-key.pem b/deploy/dockerephemeral/docker/redis-node-5-key.pem index b6b093ab2c4..467778629cd 100644 --- a/deploy/dockerephemeral/docker/redis-node-5-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-5-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCgkGG3QkBQRGOa -ck0q58F8YD4e03XG17KJwEFH7vCAuAnkeu2RpdN68DSor7yDJr8fpoIekmeYalJS -LgR/tDknCpUmowWnO51klwK57Q7ovdmzscS/ICOs2zjqV2j9CqNXwCgD9ttjCCU0 -SgcdJBomQUK5dMLAxx8GzlM6AYdI5MKb9S5k9vRUtceZG6bOIBtmdLsFBd+B4iIF -ElyaJ4aEVj3G5oWtSsU+afVzoFXHTTLUG4c22iOBPyF2zFKTHbkxefjY8M/9iShZ -xELuSjSRckAA9pAlnwM0cKiP2afxBfw7meqAlZL+d7brCetO6rfTbORa0vOYvoGV -o1YENWZjAgMBAAECggEAMIK+yyf8l2O6NikPkIV5y1KmohigbmWv3veToaCa0EEK -WBod2dHgnbWiK08BJRzZRL5BdOwl2YJSAds+Z7jzRYzoeEZryFV2HbSUUclCJmZp -tmVgvKAAt1J6lS64nS8QH8yCKoR0TyzgVLaDBLZqIiG4f6C70JO4l41RzuY0Ufy1 -E0zGSbm0hjT9lsrmbR+fCy3pu5UsTfZ3BVfwuQVFoZSBe4kxILN2vZLFVfQmFSuX -I8GTav5ENSGqhhAifUOizjIJVOmfQG3jtVftok3NDD2EVSrNJCjwrsv1QbEbbm34 -2G+PL6FjhqQ4WhOf+051HNVb/wkEtrS09orlg3zEyQKBgQDQW82bMBSVPYCyAGT1 -wcw9bmJDqshFmCECw7BVGsmC1C6vxAuwN+EXJxConb2Kibf0FkS4Qke5AKG8InF/ -3h4bds4T2bZCVogUACaUwXOrzXJBR1LAyAdchLkRsXeM8U28SdtjTF3R5sUJyX6y -zZ7pg8oKzbDIikUEWNg8A926KQKBgQDFRvAR4M0CcwQI1P0WRMtUHCudPAOQhmD9 -LVfxn0n3USaT6aXFb0A/+y7UZI6ofw2T0pZZS15AYzqahC1OVCIQgHX5Um1eVpCY -gB+DaoKRqQQyLHDfU7AQI3FH6n/aefbfx6Rt8DXgpttyp+vdLqRbgSrYaJBOjNST -+5aDJCVFqwKBgC5QuduNTIYALeNjgw2+DpB5QQ6Zn/sYXf4nUcMZOUIDuH0Jry90 -vGxRGrrglYl+I432hUAQO7E8GrefUGuEDF0+g4CWHJWSdp07i1f1yKif+o3YNOT1 -ke1W82yjble+K/F22XWxPAm0qogKakeEvZZa3UaZgnqRgdX9idONaHRBAoGAHgLm -rrGWPpMkv/s27VZV4FvQvsDMggYPZzSotldXN0qfJc1brKd6DMG3pBQQJ838UMqu -mLMAiacO2UbWZZ4i+IOybtV9Uea1ZJ3JLYLcjjA6NS/RlAf1Nt9NcnVYMfJv/icu -+pKaf6yiodSt6x4XXtxNmlJ98ZU3GbQid5zeFrUCgYAFcjIwBwY3JBXrk7Ilxpb2 -Gm7VLQfQPMqsIg+3quMaLNRkHziIlA+srs85NvCdD7hpxmX3rTRpK/KpmXG5a0VS -LcnRWtHrY8fXvsAKEc46ZTy9+e8JyTtrsNZbe3FHl/za22YB9/mX/ScpNytl1X74 -/nHN3Slfzn9kpWDYDHmx8Q== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCSXrnTzrNfHudX +jC0Ah0CiFRe3yp4j6cN3Hfv4snS6tPWXM4MH9Dka8zMLZvzRVQZK3PxDh/R/DQYB +ZhpyLEvT7wYCDsS+F+tie2sPjzSAbdM5dolD8fGwACOqobI4vPz0QrwDqHde/OdV +WAZlh5Pzw5rDUu84CdfPSWRN1pomCFWG7gVkpuFzIcBfz+smPodyw3BfU8969q6t +FACEpjGPF/RufmHoIaHe2q/c+3HBY06ro0oTqTtRe36v4Jp2HLE/jE8wc+YggTmH +E670uEXIR9N3fF3AbPVnhimEwcQ5fpJtMonUvfj5Z/4KfKo/0Yrh0wljeRiz/tZg +Twwbh5ATAgMBAAECggEAFEPciJsaseBueUGQItLsZkRzVzVMtdOHW4uhjOpFnRVc +LLCrbe4opeGRaf0P+HpEIm38LewPNDP9ETPYv4FV3PmVTwhKbGNAFLovtXocnmzA +4jjWLRESEaMYombmwlJFghq8kJPCNeWKsIHnyNDU8YVd0mM+JE0V6GjUjq5YA0x4 +co87wiNxAtjdNuAmI8elOqH3YhCwCQjYO1NeEJwIhWz5tgb2J9Rvn85LOh65cU17 +FaxiTBNSrMW44yk+uhEyj8IDbcZax0s8gLbCSLIj/MnuSbm74VkGKXme0U3mJSmn +dY2tpO3DnNZ+qvakpUk50e/LXYFofsJH9cs1BlZ7AQKBgQDJufGOp07KcIG4N3ei +YxH1IRZ8vThOHksbKVnzQLRcJcEY6SHrL3DgxS+kO3IfjkKZGw5vZgV4/jfTfWQP +eDXwIl0t/YVCEDECppfAIN7fyvIVI14quRogbIrn0jn5ijhVzPI8SWvi/viFbFvn +2O/8KUaHudv9yQ6zKItZ1zHAkwKBgQC5wBLKYdeQT5EfvfXT+rHoioUyywFxTpOF +em14JfNwKLdhqEVB99MzGEdRs6HNz88YbhKQpuEQjwJkbBXZUpAXyYPLDN5uQtV7 +Xw1MY7d8O7U5qNevos+Yti8rrv4w8Cb8ppOX0DJ2SD7J4OQjuyiRYx6sE+tQH6p+ +6N2Gt9YigQKBgQCqpnt7s3uK9Aw42+t/2xFo7lnIooYMR8I/swaeKsGpJmMpAKep +/pMeApHf/E359e3O+b2HbaX5ig2OAwhvscDnaRqsekiN74aWeHntlaEVbujGCwpx +V++LOGd13zkeKdiodN0DNRVojUuOC3HgO3whNIWu8gLxuXGPDCB+mvZCswKBgH+I +vh4QgZYG22iE37U0ylQUT5HpSktGnQknXuQAgp1+hzJY+3xosKzDPax9/lk2FkX6 +xWpl+d+JoSXcBEBbbK24YXHXmxzvbG4xfAr36DI3OJ2nLLfdvFVouQhwNPza1pnf +sTSp8Qu/XMT1UQ6rYRY5jQSvBIDVzRUnw3nM3QyBAoGAXs5Mg1jcQme6X56e0Db0 +zDCcEJuYL+nWXSkClsQCaDwafi4PQVP/V351Qruw0n98grD5vacz1HdXosvCaACJ +8P8e4sFJmSGu8SQt4zbReq8DHNTWZyPC8muurnMSKtfg3XulY8SFsoog7dlMzGGY +IMDiEb5jIb6DFcpNxjigXsM= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/docker/redis-node-6-cert.pem b/deploy/dockerephemeral/docker/redis-node-6-cert.pem index 94c1b673e6b..c176eae043d 100644 --- a/deploy/dockerephemeral/docker/redis-node-6-cert.pem +++ b/deploy/dockerephemeral/docker/redis-node-6-cert.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- MIIDGDCCAgCgAwIBAgIBADANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRyZWRp -cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjJaFw0zNDA3MTYwNjQ4MjJa -MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQqFm0DBSO4J1i7QJf -WbCtHyiPe5znhh581B2macsLkAnZVp+tV98scNbyHx4p0lO3+QxogshEg79PnyrX -R9LuSQOGwAyUsy4GLrjKitpGy1ZUkRj2Lg9yK/a+HNdr/UPE9UFAgtyPqljGs/Tg -99NVEZv6W91sB7SH4Mp6gIQcudgCNrZg87RzuDc48F2IFjjDtnVxRHeEQc5iRWYS -6o0mSr8r87RuYNiDCBff4KahHYNAqvF6qKxIQC9Z/VxBSDnyMz76+ber9IhaJRlX -wB45ugdmeUMEnJKmwbIOl275gRNMIj3OVlobZyMLeY2aWEq2Ql51PO2HySjXcy6k -9ywJAgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV -HREBAf8EETAPggdyZWRpcy02hwSsFAAkMB0GA1UdDgQWBBQ/9AV4TxrTpejQNv4b -HO1ULpu8vTAfBgNVHSMEGDAWgBQZAQiz5+eaFiYfCv/3OhzoNZIlxDANBgkqhkiG -9w0BAQsFAAOCAQEAV6eD35KsmU0g8jmtnDB6YgnolGyGbJkWHNrr9q0XRU6qGmb6 -vnlb2yWDcnfDagaFQCTLWOR4zwoSKXt11uhMAWYYGFEIQETZz5CTlPwjy3qf9s20 -n/BEkxiFxRV75aF70rt5SMdA6hPIW+XdRPTlNSpBv8Bu/mMiwZziwpTiTpkkV5pL -J5+lD4Z8teao9aPB3G9z+g6Skt8ieTkTxdH7CJeDiU/TSFJsYDVQqGmD5KVsMClZ -Stz19647dM9XIo50PqyHMZSXe9R1KPnm/mCd8U9XrfymvX75PYTTs8DUIf6U1yVd -R5wIrvAqkh6X84hPv3sOn84fT3BCuz9/JrLVfg== +cy5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzlaFw0zNDA5MDExMjAzMzla +MAAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmN9ktdBsuxTOPFUsU +qAjMnQSyBz/BpYDGMagy9e7PbtniVGTHHOvGgoq5VvPdtiVTerwefNAQaL3nLLvg +24hOEWBlQuBgK0gW48NPZJAbzYvNdF2jOzIzsDu8edEz4TcI8oKvw2WS5HQGl213 +06f2tMN1Ng0O07WoW8cxOYISsKVT9EyQJX4M/Oq5/nzHkXvS97ayFT0OvVdIRzPU +A6VsSyr/X1LgVmZEGfWcdv+cxJGBiXRsiWdW+Y+n6qvRBC2WpTEhCXomtbbDtuSH +e+8EXk9eKSc5QYFNCDWEMk25JuEQpXIMfdiHbMmK+9BgdRTUh8Pm94yD3hkMO6z5 +N5e3AgMBAAGjfjB8MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNV +HREBAf8EETAPggdyZWRpcy02hwSsFAAkMB0GA1UdDgQWBBRyt96xEM6o5VkG9JV5 +vLVxnBELSjAfBgNVHSMEGDAWgBSNcgRaq4sddGR0qWD3eVT168cULDANBgkqhkiG +9w0BAQsFAAOCAQEAmxFmsjenSgrrI1sE7DJahX1CaNVGodx4CwVc2etEq5PBWC6r +DpfCcYDW+Hg64Ac+NiPaLxFaG/8aM7JSePbAa71AQN+2hJpsV3/ANvSUaJfbHSFx +xfTRr8m5l33IV7ynjvZCPXWK4Gc5o7/shPKObHjwb03DLJjW0rvD5SYIjfCLjlOk +na2ufQnrmEP0XO77EvP4G/sHBjUaXrthsYTISO3lBTnGoKWNj8YwTFtXILC3O1to +sKWKYe5A6FB6xathUVBfS+Drp0PIYdAU9N3adymv4tZf52ofMsbJNkDqY3JaWmcO +dYHuYTeYg6ZiVhzZeasd3V+wc/CKAD8U5UfD5A== -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/docker/redis-node-6-key.pem b/deploy/dockerephemeral/docker/redis-node-6-key.pem index 699c15d93ca..0bc3f366189 100644 --- a/deploy/dockerephemeral/docker/redis-node-6-key.pem +++ b/deploy/dockerephemeral/docker/redis-node-6-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQDQqFm0DBSO4J1i -7QJfWbCtHyiPe5znhh581B2macsLkAnZVp+tV98scNbyHx4p0lO3+QxogshEg79P -nyrXR9LuSQOGwAyUsy4GLrjKitpGy1ZUkRj2Lg9yK/a+HNdr/UPE9UFAgtyPqljG -s/Tg99NVEZv6W91sB7SH4Mp6gIQcudgCNrZg87RzuDc48F2IFjjDtnVxRHeEQc5i -RWYS6o0mSr8r87RuYNiDCBff4KahHYNAqvF6qKxIQC9Z/VxBSDnyMz76+ber9Iha -JRlXwB45ugdmeUMEnJKmwbIOl275gRNMIj3OVlobZyMLeY2aWEq2Ql51PO2HySjX -cy6k9ywJAgMBAAECgf8ZaYwVjz6EA6BMB39LnZem67/7uZt94DxWUxFt8xkZ2Ix/ -GYSP2AhxoSf6Qr2ptbP8hk9O2OfmlNv38fPO6OdQwqX9lZ0XNgSAYNTp1hGtMija -o9FLBayAezWtTTFdEQSCOFx6GlFnCU79dpHliyS7CeDZ3Prs7Uxz9uONh+KPLyPO -83EFF3NbVMNIDsFzuadpHC7uvdUP+uMpUrz1USAAxZ6zwVvohUZYpjId4BCIk5ns -jWjHSzg7w5sNxXvUOO+eWZPQKnx/T+tAqh1BTVEwVPWa7/eRjQ2rQVoPFuWDMrcK -HxJlGsX6CdiS2D069iEB+x0cRC37uj7m3OAFkkECgYEA+0suBBs2Y5f1ZJMQIKsv -IMp1JZNaD5Aoidu6Z110jGi//LIaBrwmJL7bKbRGbCPP/wpPofvcdtEjrxK0lXnq -plEJ2cJou2ESQtXJzwXQy2oeDstvaIgavgaQr4VMUOhXqa4gVVk7aK035P8sRgYF -9RiSEBkUQnj1fS0gHmyhX6kCgYEA1JDA2mSNLFV9OPHVYEP8zYxY4UHxB1PFOfr9 -mP0ljq6Nj/UoDuHX18FH20W0TgGRnW0asx60w8G6abpdSem97hSyfWTmf8pWyh3q -mwkEuTpMdXTijVCqeDVDUE30Y+a+kG005a8vQt7eTBVLASwKLfz4U3mWmSxTsjpL -gvIapWECgYAflXJiJ71tRRMdofI7+OgCeg/BOkTugdLmiMxj43Ybk6rVqtjkkc9F -fQt0sWjMfK/OwVAC7vHlqSGQBozV4K3iW3seeHXLX0b5SX+E2plEh8DhYSZOgBTE -X3Td6qYN4TXraKw9repunJ7S1FOPNYCYLo9lIJHQTP2lzv8jc8nQiQKBgE2l4wzk -Fj3PrMKUdKGJtFtRnVYLxIQssasQaHruXj3UvZmMsGlfTn1d+WW7/LVSFWMwa8Rq -vxWTOwlMLq/FVsAVh24O4bRksXd7niusC7Gt/igZ3nhIszzeGAzJrTChJZOUkPIm -IFmJGCMq1A9FiyJpejzj+YNSkfBVIyheUCWBAoGALz8kUbVgdz+tO+Zr2g2b6xI6 -SmvB1wfZJjsua92vZnF0vxxod/Gs+YID7K7HMtobSRptw5RwoE9pdCRyB9WzyQHV -LKa6WZLmK+5Vf3cOGt6B3F04Z7Et4ElNwUlOVgAH9V5rHL3zhiZAahqnhINQLdQy -qWfo1SNim8yk7PD5psM= +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCmN9ktdBsuxTOP +FUsUqAjMnQSyBz/BpYDGMagy9e7PbtniVGTHHOvGgoq5VvPdtiVTerwefNAQaL3n +LLvg24hOEWBlQuBgK0gW48NPZJAbzYvNdF2jOzIzsDu8edEz4TcI8oKvw2WS5HQG +l21306f2tMN1Ng0O07WoW8cxOYISsKVT9EyQJX4M/Oq5/nzHkXvS97ayFT0OvVdI +RzPUA6VsSyr/X1LgVmZEGfWcdv+cxJGBiXRsiWdW+Y+n6qvRBC2WpTEhCXomtbbD +tuSHe+8EXk9eKSc5QYFNCDWEMk25JuEQpXIMfdiHbMmK+9BgdRTUh8Pm94yD3hkM +O6z5N5e3AgMBAAECggEAQ088YB6zX0Y2McvyooPFRG6VVy5+UAGgWyICtdhHg7Kl +AvUf9k2s4K8+U/11NaQsC1kZUtNCQlLYDARedJkR4mNBAOCLEgaU48gJ8F2NyeR7 +p5Bm1tIC61GDbzh5UiPycGocJ+bdfBWNMpohlzObwdjDifSAZy+uUWYRDMr39G7x +9SH6aLL7ZHBg0Oc4dw6K4GQMrU7sdomQcSqNyi5sn6PN8FsuO1wMp8C8V+U6y5sb +36Y1rz90ZOFqmOBnG/IdPR8tFbdql1Yy31tzy/I4thK+1v4QN6JVLPvyw4H0RzFe +j347k5IsNehRdwltplhckeAUzWGGNiTx0zhQPAchuQKBgQDmyGB055GCtRoEIpqN +ANNa8PxTp2sCH+/J7KZma6gSJ9WY73xtGSVXX/Ubz4l8FHiGoA0CQCElARJ9zff/ +tAiNXqvcQeBPVC23CMJL3hxeHLNs0ipoD8qvdQpGit3DAZMjdjtt5jd48CulEmfP +/rVmeHKChZaPPR1EgrMnIytaCwKBgQC4YWygHnDjW9zekpsDRMKkvK5QMIey9ygB +LqXlXw6GANhVDGSr7zOHBtF1aBc6FA1FKlVRXz3Fag4pPZLd2HbEaKnzfCNPH5PL +UTX8fukftrzY03bvpYcr+/YabPO8H5hkeUqHyH9EyIgdj5hOhKEVj9kJkqENt3el +GvohkgdwhQKBgG0itPqTx6wYGIV8F7o2eby32Zt1wJTwpWTIFKi6oHB1hf0cw6qU +CaSYLEFKk6mpxJVlesFlskbdivETRgQWDzVLX9p5DKp3FGdKLRfToXaf+/mqKYOs +dB0lLAbQBK8DP6G1d8Uw6Wq3qOwXGCC0QvSCYSR4KAr0y7JqXG5Vo1qhAoGATLCh +GNxwgfDEpoL+HNbtys18B3iYCLVKm2tGr2fhR5V0ZbOY7/a3TPNmDdp0xsBuYJVi +FU1zCPi62SZ2PvX5OGp8Pf0lRpTQyWGG/fXfi0RbuigCsVz9IytSyt0EZ/wQS8Iz +YNThMr/h9cGzTP1Xbvt8/8FQYb8s8ayN24a8t20CgYBDyjVifJHw6iVl3vu/O+R9 ++AdSe5bEGGDuIKZRDJbEj2ScgD3Nwqdst7X5wC+rcUuyJNW22GihyLiC/+OCaJPl +9fyaRpWWjEUkzpvR+3GhzzykDnemw1z39AJrg3ewSaBdbw9Bvq0ebrGaDF+uCReY +V+yVEYFsBaK0JrbkIffXbA== -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/federation-v0/integration-ca.pem b/deploy/dockerephemeral/federation-v0/integration-ca.pem index a38ae3c9efa..6a33fa9e2c3 100644 --- a/deploy/dockerephemeral/federation-v0/integration-ca.pem +++ b/deploy/dockerephemeral/federation-v0/integration-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIUMtt4ZsS3KWgdBSet+D9ifnmmYiQwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwNzE4MDY0ODIwWhcN -MzQwNzE2MDY0ODIwWjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN0AJjIqUVqnbZbiBEm6b7migSGIA6js -0KHOtg41Ai4PHPId2CHuQ6ZKcSvDE8AegeykkxLCRNagvBMo6ua8u/MRdp/xQ3AG -hTDTGly9hppcZo0nkXQtvXSLcNK8L/supSzrxnt0qD2dcaq8KGzPBYk/RERLpEVq -Z7sARId/QI1S7DMvSSTGgHrqxy0VXBd4E+ziElIdZG6mEMwx4agyDK/Fslzw1Cp4 -Ufv1NXhIzSofrKJ9nI6wuKSAymmJh1G8l72H4POa+YCLXaPdDUVv1j/1HqrsWs9f -M9/z8GfjeqJ94WJ/Z1P65si69eo4zX18TzME7EwZyVKBiK1eA8phTPUCAwEAAaNT -MFEwHQYDVR0OBBYEFL8s7KTIH5P0tLQ9TdQ3bKAatD2DMB8GA1UdIwQYMBaAFL8s -7KTIH5P0tLQ9TdQ3bKAatD2DMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBANIYy7aAUp+UFfcxsPIcNCjAeYdQNi09wJ1cvg6ctU1GzRGinvr9PiCR -f13ZNX0SsK4farNR2UK9TTF1vv++sYiC6EgzJFWBqBW3XqjQMqF12rApf9faHvg9 -bcyWJNgQxuXc6ugxXrUI+Sj4U2LRnnEkh427/Hs1WbD4Bd0zfTHMCVmk6gvi2kcU -e3agZInTAIAwS59afC/6bGIaXb6QVyWlnEhWB2LJRIM0H7aXB2Ot63upin+yDZGU -Esz3c82RNPBzRaUGJezlaQq5ZGZyJjkkBWjYaSYO2RaR3h/PqhLIfshqC223SCCV -Uo2ofMIfAvXNA8hHK/u3f1WVTEJ4ERE= +MIIDEzCCAfugAwIBAgIUEfjIXW9tD1WgwNHJ+kC3r6Cmv5swDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwOTAzMTIwMzM3WhcN +MzQwOTAxMTIwMzM3WjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJkd+ihX0Tp6RG4Ue9kQFvTHFy2DL8rC +8u956hy17/9sTyIcMkTOtZMew+ua+ZrNzX1x1WP4BT5RZQA/wy6ioEpP5BVFxe9s +dmVqYawD9C5+XKTekEFF3gI0MIGZ4Vum2hdUOHTwDatAgdiqsBwuCxHM5terItzZ +SsrkYFlAhISmM9CsUFOR+1rqpMNWRQD5zzZ0Sk5HeyM1/9sHvkrq24rqKqkg0/uR +OTHMydrmuSArG5tKqkdb2zoJwOs8somraRzB1JOm5/i/3pCT5iBr5gf8eLGtb2Gu +XgmnTI129R2a+LbifqTC31jIMg/yNfFuU4MdUG9wE512ghhXZI+Hoc8CAwEAAaNT +MFEwHQYDVR0OBBYEFFqlhDsVqlH8UUKGOtCDE9xmYL3hMB8GA1UdIwQYMBaAFFql +hDsVqlH8UUKGOtCDE9xmYL3hMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBADQyh0k3xhcrNOqhvzAK1/A3TY5Hq1FE/a17Yiq8DIiLCJG/nAN60GBx +m7zuL8xoRJ4ylwIswa4z4rHj9p6M6tIbi2tTfJsbyB+FjyFRWoBmTngqNCiw7QUR +/ofSliuEu/YIjphR8LmTBvy4fVccTwXDaBPEGf2iN+DFmryLHxVpsVh3AA0uUSy0 +e2bZJLRwv1z0saC5KGHpWb6RJbAP2nRw5omcorMtP1KW8XyVESiJm7hDZAx6VLgD +k4GcEOUEq9CJs9UVAkIIDS87CfppHZEGPDK3Ufro5AhIwA3hpSJkTgzkf0TcQKIr +4E/zJSeTld4rMC26ghWodIwGRyofTP0= -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem b/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem index 28266b3e5b3..e4ee0a09ab1 100644 --- a/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem +++ b/deploy/dockerephemeral/federation-v0/integration-leaf-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQClrSse09wlL7P6 -pcsCNQ16CcrYS7+IF5yq9Nul0gnJsHl3XC1SKqNU1shMfcZYa4pEXF1LU414EWkL -/0WWVTs23U0fcUaybZJAG57jXD+571vEkoFCESJxBPLCtEOBUYbLx1IE2bZ9ybHZ -yD+W9XulQ7FgIS04tPgkttunl11CXdRAcMiL796/t8j0eVU049Pta420/hWYiVzv -fz74AyUCQwOlXhl8Io8HgO0NUNgWwu+3kI/2oswPZFcCXmSBTyht6CjSYNDtViVy -btdd+U7InM2XTKczXaweP1oBniWLxem1vRAltkYU0S89jnlcBtfoJHTUDKLBeNal -mys0ebrLAgMBAAECggEACxs0t3hUWwR7ouMtB2ovC8NO9Hj/efa7QJVG3t2EXR+W -Gkj08NEoPy5hdwnncKik4uLzjh0nxVNwIKcYLx+/kcn8EDjebbpSrOGCdpNfN5kI -JIFTEO69Hv50l6t7QFa1cUEHX96IZp5NbIq0A0FUQfFBGW9Kl3E/lpZ7hcdBL+BR -VrL96mTFqKtvYza/8wOOT05XCvVZH2Q/cN75Ih11t84UHtUCZWjAOoRhddcblo/2 -jCj91N2W7/Zut94o/2KcRY8Glf8Dps7gZUE1cB3PaZvHU5eEsCupwyNYwx785Pn1 -zzyCGklxeSu7t84WJh/B8dx6uYGiVRcd+ujc3BLKgQKBgQDerZ+ReVEF6cWYOKQD -6Hv2bv4PdME5LQkq1XoT4AQNRb2pXw2BrA/Crr7xNlOz2u6hN+LnmEFLfzMR/5UE -2UAS4tENTb4eibqgeBPVx3RksfpHWni2y8Dvdax3GgDwx9BVVMycOe3td3IlpnAN -rRU+jYRZuAZr5OQdQESu5bNUUQKBgQC+d+qB43WWtla47nFMEWM0RB6nHqbAo+dw -0BIaoxJyuC6SyE7P7APdBAlOw8P0Hy/Dzwe+ZzGnjXiXYyctTHss/zSLfhf+43/A -Z+Mzi5u0vzL60ING5UzBY9P/0XxrJym0eVr4YYPBvLBojwLhiDgQIXUHkgUO7IBY -UPjM+ggiWwKBgQC+zq3FvNulonxThG1ef+8A6ni/C7+qW6HYV1alAzbVnKX5JN7w -91wF6TDqhi/RFM+Xy8idxMRmiddcG9I4dmRGCp8xtCUuC7ykVmBAtglRY4RfcfGw -SQXI6t9eqySVLdKh2+j8EVOEQO7JvkWUInTqxd7b9ilieJ7TRcfUyjUREQKBgQCR -QWp6fDllItGoX0/QL0J0za6CzQFm0JjklAn6fnrHOmdqUZCpSNj5aOagRvPd7RrE -PdMuBgz8NwvMiDWMelNF0asE5rjuDhmTZqcC3Gl2wonidbpoCt8qbTN0WRKFtWw8 -0n/qBJQy3++5Dbeov/Xhd2KEz3tEEmEe+UGFMPmbGQKBgHcpn8SGrNuxjL/6vB1j -a5+LNOQAtr7akKlJbrl9B7poMVu8483fN4LG2HclWPPILnE3iPU/G559ZMo3QL6G -0+dImewlUSGtQ7GtLssai5wmmgLfXufiewHq/WoLDkd0xdu5RHc7GGgixx8XqpBG -ZTrVeb2nVeiwvIiZkuT13bGf +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPYxemHc21P8y2 +DIDNsuUzVpnc1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwl +R66fg07whBcZspsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v +11v/+Iq6LNerC/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7 +kBgflylEKjapfCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j +1rz3IPfKOOtIgxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfo +wOpkgZPdAgMBAAECggEAVMurFUB1ZkkmXj9hgPnHLoUX10xJ3ZMIy7jlkS1ZRsD8 +EK0jDj2q0OtRSet9xJ7i3nfFTojzE5obqxCSrWUmp0ATI+4789DjuZluv8quAdm1 +0U73zHq43GZNlY7yco2YN1Lh7H5yepXz0dDILLLGolYIfscdw7YoUCvuI2vt8eyY +FaMkOi8Bs32Abn+53MhaNkEOVemmwP/u2rD81IY7pXQtF+1XfSxYBipHCB7phC1g +dr6ITU9CoF3SXvjYL6uP/7W1Du/CuaDSijVZJkBfQSgbkQbBr6pAovumtpNZgZvO +bixqS4oTZqsd+Rgl3YBOpx+JbuUFL0fG+uQBv1yZbwKBgQD8n7s1w/ouUGwW+qGi +4EFTYVDFD2Rgvg3oFPHt72zreaNXSy8YWrK9Mas/Fzu5knSE3f3O0S1WvqWdt/z6 +uPDLTpg9fIWX6v+hPX09F5ekeLlUDBazBT1PQUD0qd8PiyFNB1F+ffSPTxmKScjc +hTqQCtnun6rlICalWO9VvGt6QwKBgQDSKJkjxIGLtXBaXfMQiF9dQDrMk2it5grl +w0OnpPhYvpdp+Cfi01kMUrnfHwF0v92BqeqoKZo4DwJXkrmwf5kNnftxmRqbk9gE +dJq/E/6SELyT/chtzXfxC/wTmyyxhfZUJvJUxlaZ6KP/86t8A22DJcFO2Z85iUGH +8zy2UJUnXwKBgBvSs9m+FeXX8a+uNvMrY8Z9J1os0c9d30Y6WFLuVb6xjO3mV++E +vb7co5G1S1yq5q5jjLqkiyvMn4z5YKF0kQCzTU0oU8ZhmXn2vb5mxMrWiQLauf1J +jHEYLMFFnE2n8yj6r10RHkhSW+vBKKAxBDwtFceUSkwl+FupqeJ1eBjlAoGBAMou ++LWqdZ89HSwzOobrTCPgiTELmCfFKzLE2q/MTIjEQ9NVRLo57m+mnt+DatkxRR9b +oz/JVm8cMXqi1DZza4HoPWGalDic0bPnooC18bIAnAwcmdjZVcz3ZLpQDX10jfmD +xpu8fNBxOmYhvRcADTmg9wqu3zpxTDRI1F3pxLUtAoGAGfsX4bve5cLm49Oa1p0H +kEErLMuAMIKQNVsbzVELepLYr+uwEXBCXyyoIf79ABDvUHbzxMEwgANuet/4PQzS +yB1qzFk6GDvqZ5dfPUgMUWH9wvD1qEGp6yxkyESGt8CNwnu8GI50NAeSh2/JeUIa +r/u+m2vnJjOXpJdOJ+7f6yM= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/federation-v0/integration-leaf.pem b/deploy/dockerephemeral/federation-v0/integration-leaf.pem index 9eca6a70899..abd724df6b1 100644 --- a/deploy/dockerephemeral/federation-v0/integration-leaf.pem +++ b/deploy/dockerephemeral/federation-v0/integration-leaf.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgIBADANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5jYS5l -eGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjBaFw0zNDA3MTYwNjQ4MjBaMAAwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQClrSse09wlL7P6pcsCNQ16CcrY -S7+IF5yq9Nul0gnJsHl3XC1SKqNU1shMfcZYa4pEXF1LU414EWkL/0WWVTs23U0f -cUaybZJAG57jXD+571vEkoFCESJxBPLCtEOBUYbLx1IE2bZ9ybHZyD+W9XulQ7Fg -IS04tPgkttunl11CXdRAcMiL796/t8j0eVU049Pta420/hWYiVzvfz74AyUCQwOl -Xhl8Io8HgO0NUNgWwu+3kI/2oswPZFcCXmSBTyht6CjSYNDtViVybtdd+U7InM2X -TKczXaweP1oBniWLxem1vRAltkYU0S89jnlcBtfoJHTUDKLBeNalmys0ebrLAgMB +eGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzhaFw0zNDA5MDExMjAzMzhaMAAwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPYxemHc21P8y2DIDNsuUzVpnc +1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwlR66fg07whBcZ +spsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v11v/+Iq6LNer +C/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7kBgflylEKjap +fCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j1rz3IPfKOOtI +gxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfowOpkgZPdAgMB AAGjgawwgakwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEgGA1UdEQEB /wQ+MDyCGSouaW50ZWdyYXRpb24uZXhhbXBsZS5jb22CFGhvc3QuZG9ja2VyLmlu -dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFIro61Yvf3swiRDOD/qOkwKJ0+Li -MB8GA1UdIwQYMBaAFL8s7KTIH5P0tLQ9TdQ3bKAatD2DMA0GCSqGSIb3DQEBCwUA -A4IBAQDNWgHWibMJvGI5YzkTlgXEvxjTTdYM6SpyLQFkju/PUuLP4KoiOvl2SY// -OWJH9v1XmZJ1DlnNRdgAHW+Uj8SpXJXRPkm1/5B9d0Eh8kfc+oiapZT7qfrKH5Ln -Wod5A/gzBv8rpaqHP8HP00b5SFdStTnqbQeBPXYMl+cbVwHBtZF3U6NVVJc0VOEe -MWx8bhJ6Vn8KmcLLoPPJVf4/u/toFAm7q61yZWISOMpZmLMbis0M+vJ6t57ImVTv -utffv1HhuCfEWQSc4XHI9JOMc3iexJYfgZQCIZdC7VxEJaOVB1DSkc3bYq/UaEyf -Wo6HKS47x295j7b1rbSO5ZCbhPYb +dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFBkpAu3ILiU4gtEYffAU7zxGHPC6 +MB8GA1UdIwQYMBaAFFqlhDsVqlH8UUKGOtCDE9xmYL3hMA0GCSqGSIb3DQEBCwUA +A4IBAQBB1VsthdoVT9ExXkfKixotbXm6+eBgYenK1R5Qx/UX3JrlI1nF/8rKMg5e +7QfMCydSJwVEQdvnXD3ddVhUTYRActQvnJwWTyXfeiezrfDCTLu4SNpLOP7ojFlq +9ZX/E9GC0axTIUmEIy8YIC3JJ2PAlvw9qMzrsivyAgbof3NX+9XXKfwZHBwSLsO1 +Gxr9zkL+U/qww7TvyJD1LqBR0UEd9pZriorpVVFAa/JlFQX5ip1Smcd6m97nq20N +qpUIalra+K6qHxjHVwA2UxVgbO9bLFIBmp9pNvSm+5umAKkmqFnHRNAHfCy/IFGl +3fw8u9mXJ8LzUR4tiS0cVb6bwQzd -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/federation-v1/integration-ca.pem b/deploy/dockerephemeral/federation-v1/integration-ca.pem index 304fc892245..6a33fa9e2c3 100644 --- a/deploy/dockerephemeral/federation-v1/integration-ca.pem +++ b/deploy/dockerephemeral/federation-v1/integration-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIUQ35aUV70pJjvDTbfgFUj5YmchHQwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwNjE3MTMxNTMxWhcN -MzQwNjE1MTMxNTMxWjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJQlUOLNmd7Ll7iskcSnsv9xcx/+TnMw -qtqkK17w54/Kto+NJJAkD1L+X5EkSPZ7FDKqt2bGfoETWGnlpH/zsUTUpchlf6Jf -w6TJOejQer5FQNLCtQSnOIchlAFKzFxhGSvcOrRWiBAPjTVIkv9eiCNXcJ5PE9Sk -8+bmn2ztz7LVHcv46PmT/+ihRxKJ01T5CsXWPUHOZQRfGvKZmyGf+iTBuhcxMPYC -nXb7/M3rYCQXL8FQZiaqbIVMqNRpMBVkAqU3l2JnSrlNIjIh6Nqowjog8QYGuIz6 -fxwWkw6EU5ZBwHIr2rOakCnQoKeXVqBJdWZNRMX1Vtqeh7O9zDoW4/0CAwEAAaNT -MFEwHQYDVR0OBBYEFHNgZ4nZQoNKnb0AnDkefTXxxYDqMB8GA1UdIwQYMBaAFHNg -Z4nZQoNKnb0AnDkefTXxxYDqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBAIuLuyF7m1SP6PBu29jXnfGtaGi7j0jlqfcAysn7VmAU3StgWvSatlAl -AO6MIasjSQ+ygAbfIQW6W2Wc/U+NLQq5fRVi1cnmlxH5OULOFeQZCVyux8Maq0fT -jj4mmsz62b/iiA4tyS5r+foY4v1u2siSViBJSbfYbMp/VggIimt26RNV2u/ZV6Kf -UrOxazMx1yyuqARiqoA3VOMV8Byv8SEIiteWUSYni6u7xOT4gucPORhbM1HOSQ/S -CVq95x4FeKQnbEMykHI+bpBdkoadMVtrjCbskU49mOrvl/pli9V44R8KK6C1Nv3E -VLLcoOctdw90aT3sIjaXBcZtDTE6p6g= +MIIDEzCCAfugAwIBAgIUEfjIXW9tD1WgwNHJ+kC3r6Cmv5swDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwOTAzMTIwMzM3WhcN +MzQwOTAxMTIwMzM3WjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJkd+ihX0Tp6RG4Ue9kQFvTHFy2DL8rC +8u956hy17/9sTyIcMkTOtZMew+ua+ZrNzX1x1WP4BT5RZQA/wy6ioEpP5BVFxe9s +dmVqYawD9C5+XKTekEFF3gI0MIGZ4Vum2hdUOHTwDatAgdiqsBwuCxHM5terItzZ +SsrkYFlAhISmM9CsUFOR+1rqpMNWRQD5zzZ0Sk5HeyM1/9sHvkrq24rqKqkg0/uR +OTHMydrmuSArG5tKqkdb2zoJwOs8somraRzB1JOm5/i/3pCT5iBr5gf8eLGtb2Gu +XgmnTI129R2a+LbifqTC31jIMg/yNfFuU4MdUG9wE512ghhXZI+Hoc8CAwEAAaNT +MFEwHQYDVR0OBBYEFFqlhDsVqlH8UUKGOtCDE9xmYL3hMB8GA1UdIwQYMBaAFFql +hDsVqlH8UUKGOtCDE9xmYL3hMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBADQyh0k3xhcrNOqhvzAK1/A3TY5Hq1FE/a17Yiq8DIiLCJG/nAN60GBx +m7zuL8xoRJ4ylwIswa4z4rHj9p6M6tIbi2tTfJsbyB+FjyFRWoBmTngqNCiw7QUR +/ofSliuEu/YIjphR8LmTBvy4fVccTwXDaBPEGf2iN+DFmryLHxVpsVh3AA0uUSy0 +e2bZJLRwv1z0saC5KGHpWb6RJbAP2nRw5omcorMtP1KW8XyVESiJm7hDZAx6VLgD +k4GcEOUEq9CJs9UVAkIIDS87CfppHZEGPDK3Ufro5AhIwA3hpSJkTgzkf0TcQKIr +4E/zJSeTld4rMC26ghWodIwGRyofTP0= -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/federation-v1/integration-leaf-key.pem b/deploy/dockerephemeral/federation-v1/integration-leaf-key.pem index 1e7a83068de..e4ee0a09ab1 100644 --- a/deploy/dockerephemeral/federation-v1/integration-leaf-key.pem +++ b/deploy/dockerephemeral/federation-v1/integration-leaf-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCZjOHeUnlauuxD -WgrRnh3hj5Fs+uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47I -RgA5VLvGxI+T1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjG -QBmFF7NxrvjGgerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9K -zNQ7ZTlBQvJG8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzog -D+jgoAD5/9sk3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P -2jMpJ1xxAgMBAAECggEAS3NBjWgDP4T4EUROaqACWNKeB+nmkdt68T0gGtoNVD+D -EN9UPnpFQPdHFngAgWnzF858UIKzq1Pzdg+HjqRHPK1bS67tvua3xP1GHuR/CGPk -28T1hefqPHRen7GqHDAfdwarYBWCGv4Sjz/yCkcSIrtyfMBb5fAya5GO02pckUSK -19sl7XhkPtHJVirRkjQL29R2TCpkNNpQMjkuYLk7mox+6pNTbxgbk0cnT3eGj1pV -mlPqpwzC5GevRziE/VE/WXFLChY+8KB4fDLRqWnyvabDvQ4coaXgzwbdScJyM5hX -+Dxdfni/P2m7xAZXUyfBsr0VUzqUkJfK3WWvvAGTDQKBgQDNi3RUEjVnU/MN4aDz -iZB2VYGfo/K69xTPNEbLQWs1F4ZMpHVtUVXzTfx/xG9ug989ijEm6ncL9OsnhThn -UldSz2ojSJUxLmhgCHZGYHT72v/9rEqfT9JisWpIj44KXufUHCcl3Cozj1ae3EUp -NVhN1HphB2LsCIJvLYfLIGdBNwKBgQC/PhHQMm/MQe4pOHAbdzDrRZWdG2KSRVxp -9mmJ/aT8LOp7BDjq+Dkct6a56JGqlOTeJirMTTmCKiOiTInuB9S+K7kWJJiYg9g4 -UCiuMU+40Px/1Z4/uxRj3DSdGLXG7S6kPeADx9f9BUNpAytGqOnSnfbDiDVvQVbp -0N0+nIXDlwKBgQC2uZOXrXxGOE4pd/ySpCeF2yvZ1HDTnxWjwlBxHt4Em74rYkR2 -A0mKezjOCL4bHCaYWcKqWuOsAHYQcxEaYQv6NSOg7ESdLSlivgMPO26j+yN5yvGn -wNlCHYBjsyLNu2MSoFh5AsmNfo69uQnOwXqX7h1BJsTdGg+CcJJ4lHzWbwKBgQCD -/CRzGbwKrh3eGPNWIUaDuTxudy3qYTBMeSGReJpa5+zUBa/6imFwLldEyvttTOE/ -Z/v1j/52lPqO0mAHBSSQMsDERXGDIMsi4j+RKLsqhCEfYKCcv1JtMNam7RzXM24T -MBjgwxWPrAg/+03ssDrffuGFRQYLyH5hVCK9SW0P9QKBgQDJ1ZSto+RWxv/uOKNr -7FYeQoKpMb2IvNvnGlnYHC8KS9qRq6wUE+FtuKcdLBQP4M9Cgq71VD/dsawrhEw7 -1rAYk3OqmHxBOU5Dcb152NxYHEf53pfEfWc0x4AEVe+Jzynj2EYixRKNWwODNTEx -LKJOYd0CuWywxg6d9G7A7XbgWQ== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPYxemHc21P8y2 +DIDNsuUzVpnc1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwl +R66fg07whBcZspsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v +11v/+Iq6LNerC/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7 +kBgflylEKjapfCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j +1rz3IPfKOOtIgxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfo +wOpkgZPdAgMBAAECggEAVMurFUB1ZkkmXj9hgPnHLoUX10xJ3ZMIy7jlkS1ZRsD8 +EK0jDj2q0OtRSet9xJ7i3nfFTojzE5obqxCSrWUmp0ATI+4789DjuZluv8quAdm1 +0U73zHq43GZNlY7yco2YN1Lh7H5yepXz0dDILLLGolYIfscdw7YoUCvuI2vt8eyY +FaMkOi8Bs32Abn+53MhaNkEOVemmwP/u2rD81IY7pXQtF+1XfSxYBipHCB7phC1g +dr6ITU9CoF3SXvjYL6uP/7W1Du/CuaDSijVZJkBfQSgbkQbBr6pAovumtpNZgZvO +bixqS4oTZqsd+Rgl3YBOpx+JbuUFL0fG+uQBv1yZbwKBgQD8n7s1w/ouUGwW+qGi +4EFTYVDFD2Rgvg3oFPHt72zreaNXSy8YWrK9Mas/Fzu5knSE3f3O0S1WvqWdt/z6 +uPDLTpg9fIWX6v+hPX09F5ekeLlUDBazBT1PQUD0qd8PiyFNB1F+ffSPTxmKScjc +hTqQCtnun6rlICalWO9VvGt6QwKBgQDSKJkjxIGLtXBaXfMQiF9dQDrMk2it5grl +w0OnpPhYvpdp+Cfi01kMUrnfHwF0v92BqeqoKZo4DwJXkrmwf5kNnftxmRqbk9gE +dJq/E/6SELyT/chtzXfxC/wTmyyxhfZUJvJUxlaZ6KP/86t8A22DJcFO2Z85iUGH +8zy2UJUnXwKBgBvSs9m+FeXX8a+uNvMrY8Z9J1os0c9d30Y6WFLuVb6xjO3mV++E +vb7co5G1S1yq5q5jjLqkiyvMn4z5YKF0kQCzTU0oU8ZhmXn2vb5mxMrWiQLauf1J +jHEYLMFFnE2n8yj6r10RHkhSW+vBKKAxBDwtFceUSkwl+FupqeJ1eBjlAoGBAMou ++LWqdZ89HSwzOobrTCPgiTELmCfFKzLE2q/MTIjEQ9NVRLo57m+mnt+DatkxRR9b +oz/JVm8cMXqi1DZza4HoPWGalDic0bPnooC18bIAnAwcmdjZVcz3ZLpQDX10jfmD +xpu8fNBxOmYhvRcADTmg9wqu3zpxTDRI1F3pxLUtAoGAGfsX4bve5cLm49Oa1p0H +kEErLMuAMIKQNVsbzVELepLYr+uwEXBCXyyoIf79ABDvUHbzxMEwgANuet/4PQzS +yB1qzFk6GDvqZ5dfPUgMUWH9wvD1qEGp6yxkyESGt8CNwnu8GI50NAeSh2/JeUIa +r/u+m2vnJjOXpJdOJ+7f6yM= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/federation-v1/integration-leaf.pem b/deploy/dockerephemeral/federation-v1/integration-leaf.pem index 635d332de70..abd724df6b1 100644 --- a/deploy/dockerephemeral/federation-v1/integration-leaf.pem +++ b/deploy/dockerephemeral/federation-v1/integration-leaf.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgIBADANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5jYS5l -eGFtcGxlLmNvbTAeFw0yNDA2MTcxMzE1MzFaFw0yNDA3MTcxMzE1MzFaMAAwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCZjOHeUnlauuxDWgrRnh3hj5Fs -+uh9vyddMX8rSWJIbWFw4QuYzYKY8CQa3MBb6qK1uUwoJ0W1w47IRgA5VLvGxI+T -1wX8E5vljVgfT3CAXHKRB88NrT8A1urQnWpzlq5sNerL6dqgBrjGQBmFF7NxrvjG -gerC2D8+srWfpQ6Jbl9by8c3JDu+T79PM+pW9ycUgdF1AJQBTz9KzNQ7ZTlBQvJG -8WhTMKioJgQsE60oEXD0C8M5yKBBb7DrqkeZInXqCw2y7DZLWzogD+jgoAD5/9sk -3d/gGNqDibzjjwMiJnH/IqBTkZsQ9OdZZPfx5v/p062hQBlM656P2jMpJ1xxAgMB +eGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzhaFw0zNDA5MDExMjAzMzhaMAAwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPYxemHc21P8y2DIDNsuUzVpnc +1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwlR66fg07whBcZ +spsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v11v/+Iq6LNer +C/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7kBgflylEKjap +fCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j1rz3IPfKOOtI +gxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfowOpkgZPdAgMB AAGjgawwgakwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEgGA1UdEQEB /wQ+MDyCGSouaW50ZWdyYXRpb24uZXhhbXBsZS5jb22CFGhvc3QuZG9ja2VyLmlu -dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFPowAfmLPCmdCMdSxQjsR6UQSoyH -MB8GA1UdIwQYMBaAFHNgZ4nZQoNKnb0AnDkefTXxxYDqMA0GCSqGSIb3DQEBCwUA -A4IBAQCMJwbLzUsrkQkgdGKVi/Mb5XAAV0sfkwZch1Fx0vhJI072cZSow5A2ZUHa -LScFNTPmilPKEr6MS4xIKtRQaMHInbfxSsyNViKhpzkSOKoAiJjIJ2xPKFPnbTDI -uV74nxxyf9q/p3SLQfJFk7fxbvNeLqg5bYSrMeklHj4bpMJ9fybS8/mZVc8AkTFK -fsXSu9CW1B3GF+jP3E2GrFF3Zh9MgvWjMlSYg4ljPf5FoMCUq6GmQ17hQeJFvb5h -Jqk6TcgUrp082bcVlPW17XzFwVe3n6uzvWMtwI62EztVUj98+YkBiFL3i4+OQwAU -/noc22fq20OyJtCPJY4FIK7xUcgD +dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFBkpAu3ILiU4gtEYffAU7zxGHPC6 +MB8GA1UdIwQYMBaAFFqlhDsVqlH8UUKGOtCDE9xmYL3hMA0GCSqGSIb3DQEBCwUA +A4IBAQBB1VsthdoVT9ExXkfKixotbXm6+eBgYenK1R5Qx/UX3JrlI1nF/8rKMg5e +7QfMCydSJwVEQdvnXD3ddVhUTYRActQvnJwWTyXfeiezrfDCTLu4SNpLOP7ojFlq +9ZX/E9GC0axTIUmEIy8YIC3JJ2PAlvw9qMzrsivyAgbof3NX+9XXKfwZHBwSLsO1 +Gxr9zkL+U/qww7TvyJD1LqBR0UEd9pZriorpVVFAa/JlFQX5ip1Smcd6m97nq20N +qpUIalra+K6qHxjHVwA2UxVgbO9bLFIBmp9pNvSm+5umAKkmqFnHRNAHfCy/IFGl +3fw8u9mXJ8LzUR4tiS0cVb6bwQzd -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem index c24f0a00333..68009b9f18d 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/ca-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC8A9zUO9zbB/bH -Tat97i3Ypppv3u+FVk6QisaSJLECLcdnyxe9YqIPXA1HsEI+lbnxsNiaXXvD0f+p -aJfJBgN8ff4GCu2x39YKvwaMSVqH6rWljbtA/gVSy+SDrNPZTgP/I0hP/DKA4s9K -xgOQOIFOMsy/epitKTt1Ou1eOTNOo673eHcS0J9V6uwy2+j9VFANmIvRW3w3KwNu -h8vUvKYzsZQx7upX+YyfbhN7YXVnCEBG+SJOR7KuMd9gUxoWqrQVP3lpEQ7b8hXx -0IB2JPgzaTQ9IaDYaGjFiEQHI6mipwHuwTS6dIkXbg2iBLAhA97T+SZkj6wLltxI -cztwaQWlAgMBAAECgf8czfMo+jAM1vMKrFfSJC8lsvcq2aY6Ya+RE5OrUChz3fF9 -6gytvqcsBhAv9sW25303OvshffxHNNnFaZdfC3n9QhP/dx4ZD2pk2TIcU4oO5swx -QdwfSdqdhabL3+6F2IhTRsz+RuUMeA1+xUhkl47dhnnucpJTXpCbhxyS1rCYbj+E -cqkS+oUOne1F/YlQANFKZYnf7nGdjSw9u7fdgZ+gANCvJAnliUfClV89kO9Y+F3E -aVC+yJu8MaBP2mci0M/qbt8YETVZ9c1vC2JKPdHVJBZNKuYfGdtp3fEC/TsvTzfB -wEjqD3IkBq6xK8UQ1bur7+CUjempFV7cZ+cCn3kCgYEA6OELeu1epC2rms4nekqH -nNTvOmKCONHYnT8h/EIYSor7vKKGU4KBTOVtz9Kr36UTpaLYWEsZ+i/eYu2v5X5F -iakNo3yrQ7A6nkoCD2RGXinTDd5MxpKZ5gpPwEhq8lEbDMYv8LcM01H+Hm5cajP6 -XzbO58onSCik7lGL0mICOvkCgYEAzq6JAS+Tijv+JqB4PeEYcxp3jzSvD6Pv25af -MFuE2ePHmzV3pRV8Fdx8YNYhlARsJz/tkWm4IZLpLCoDbFbZQ/sOROaPA6m0Wetr -zdGaT3OGvXKTCn3dPZ76TQ5dSTyMTi/HCPT2NIHBuAZAkSvNSHlnKm29YpSMK/OQ -W69+/w0CgYEAoVME+OtnHKTmtB8MChOHToXUE8YaH/J+9K+/g1jmKv2M1mhgVYma -uQJWyBlRJ2Tb72qYJNIh9Mckb7PonjqTQYHzCMZcfk+ey/jI5JC6jpC6vGi7FvSH -2GxcQv/n1mWJL5g7ra2hHOM3/yzEqG3JjBwTyU6pV7uQRegHzH5IvUECgYEAsLjS -Er6AdDFJ5fNN/PMMOddGpZ9RlJkDTYpjwTBvzvMRyKeWDwTo3bRycUaG3Y5Of90M -oEp6E9MPJyEhXjCAg70V/Vn6rRIdUMmYmxr+y7KnYjOmgNEQLFFUCjEfGLD58xyt -Hf5+ynSslFJcQQTn+XE9Ai1lQvZrSGVxaMQNXb0CgYAqYI15K5qo0tnhIBXesCdY -uPRttUEzk0Ew1ijTHkKiYC5KoN1YGjL4Zvag+1zsW10kr4u4mYNvtumQ2jp/kcOG -P0RN1FHaXO2W2MdEyChjf0xfbuk0Y6AP9BOXOxXLrL4Vh6+EXgu6VZefDKBsYDau -5GdldZlC4GnuCnTUnehJBw== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCT9qwdV6saUQBO +oUmJGG33f6XXzq1W0sLGamI/Ouoo67WRYdPgFrY2FJIixWBolyDWXZ7Oki1T/Gnz +wlysp42f9s8yKbr1+dc4R7a7pcQXCsjfOYHJJtNyhi8+RjRhTZfAnmCX7kViUgPg +DsZEdvApAQQ+XkdHEXPI9BPW7g7WN+sJoKhLZA7wHOD75oACDy9iqL5pjKIRstoR +yWSyggPkBs3i1KkMfDExd0bGo1fLbUYPsH1EDOsHTCW7m/d8JoC1X53GkHTvRP4x +Fa9pFNFnidZJsbONHdW3dNlN//KJhsmskRl/aqn2hd4PqA8wKIhey2w7gu1TVRHP +Pzz9bem9AgMBAAECggEAB53Ld0kzfAvOXb3MqLtysapG8ahoYM9BinAgdYvFUOzt +MDoea2sP3xHJAfQyBcA9xvsTuSNqJOgZ1hvbpGGNlz3fpX/jaVT+gJ2kaN1cDimW +dHMj3KRcfwksNmLrwFR7qsUcSMXRmYGKRIbOKukKbLnqK4Gz3pMl6CK0QLyJU/qB +0v5rTFOX8XH3Krd59naXxWFvZ/5xdaQs4rnRyxS+N5xLEJ5McGyVwto5NHV9Jujs +CHK1Av5RDqY+lOh251ZZE5068+qdXFKuqDnJge/Uii82imNWH9NZMBDJgPr4SsNE +tWt8aU7d3Qrde1FQfxOCwgdgyQvCweTH90z7anrKWwKBgQDOZMn8Oo9OsJFvzhdz +iGrTeqpN9tZq8LrQ8Ob2hYY0+q+RaywbVIRt9A5LfWSSpOAQdGHnnBmm7TS8UiPl +OadgW7+ME0cp9sPLImpAxXnDoct5Edb8/qBN76eTO6jhz5aBYqfubb6SHHCOs5UF +uA7XpvvHD5Nlrzn19S4sSY5lSwKBgQC3hrtBWhtXj6YbfYQFsB+nwYEXqtcw8Azm +8xJtMl5mH+bWGr8zeUjVE+bfFK4CjLnVnwuK8OOq7fDrlQYbAGtJ8DJozkvjzxsY +ztDwnTSl4kubqhJMYWDh8GcM8ovI/y36wd8DKzl+MMPiCFmf66xtevp/KKGShNaO +i/JG9rZwFwKBgQClGodh2E6Peju9nrWv3C7oobXezFjWD2DCiBOanVGwy+DqiTst +WbzeYF+XD+YGURJU12UCbCMxH4wSIftJAYfdU0e1fC5vaVFTDGLHEbHIR4OHhDfh +Bqeh8NaytwTwLqmNMyh3WR8brthzr0DE5GorJQ9APDuDGltZYBrhnq3kZQKBgG2u +YLTy5ApVeGFPhxpbIuAAHmWFnWvK2vsfY/DMvGvuPufQPlrF7kghx8Wkt0Yg0mMf +1ScpRfb+kxBIFMkIXBZpLcdDG0m/maMe3vIeEbvd3W/fmWX6gIsnQH8VaYMrNlB5 +kw1yxL5s6HRqpx2THI6lg5WBM+a76vpwGtBcW7XrAoGACT/DkL63HNnLs01D4aru +YSmRI3eyCBBWd9qOciCgkGMXZJd+kfGCkFX/Z98PzsPpeVkv/kmsPUMLtObT80eq +HTCAGoHeswPzei5/PRvJ3XkKD0x4/YxHjJD6uKjvJGBfBo23zygZStsIO5cB9CFc +evdAe37T8iiUL3PWTaT50s0= -----END PRIVATE KEY----- diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem index 993f3cdc48c..2aa8d89e4ac 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem index 617f4165cac..f055a8ab9b4 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/cert.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDPTCCAiWgAwIBAgIBADANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBdyYWJi -aXRtcS5jYS5leGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjJaFw0zNDA3MTYwNjQ4 -MjJaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBALmQ5cHMo7xPrsCnBX67HULG/QLle0hAp6fQr1qTzHJ6LG4wEsU3 -KDyoQopwxXe8DaloKxKpYBDSQM46FKUbk57el6nHZfQi3IBPe1uEpRgKaDh6dZiB -FJ9JjoTMcacr5bchdtUgLiTggacy5CRfSAw+RCwSefrtVah6ciFm1nVVxtvceE7w -T7SKQrjhWKPUUraiLSKGVBmpNn8pPJQglIVoY4lHvMzH5OMj5nLEIqeNoQeazlL4 -2yRyNqviQYaCOOc1ihqYvT8BXU4nakN5M/tdjTJqKeza2lZ/lz9wHdYrRAIjZjQ/ -7ChjHUWokBjzETkTQhlLSY0uDs/aSqiHslMCAwEAAaOBizCBiDAdBgNVHSUEFjAU +aXRtcS5jYS5leGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzNDBaFw0zNDA5MDExMjAz +NDBaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAL/WoO1EcKQXxxhoQ0l3STTha0N+8d7OVxJbx+rO0+eBtxa9IGnd +0X9yCm5o1dsrDRfVP/NsWAvJ3SieiDuK/bdv9bNjFVV7M+ZvxH73kARrts2Lbdhz +Ip8uv0ReZUnx33JbiT3pIhO0oOPaAtc6nRhcLMT4QX+wBJd4yRgNT5W6r4DruwmM +ss2nqpdv0cPoWGrjv3IcuGgdliRkGVeIqeTT/g4JqQOLN7ocjFquRkZCdYIfXAOl +5gI/qj/Dc27M/Oql497dmO83nC7+naNOjPIVF+CklAHfyf0W8IEqtD8qQ7rnaMEi +opYfsY7Nf45FD9TSoKBYCN3bMLBiojMd8UkCAwEAAaOBizCBiDAdBgNVHSUEFjAU BggrBgEFBQcDAQYIKwYBBQUHAwIwJwYDVR0RAQH/BB0wG4IJbG9jYWxob3N0gghy -YWJiaXRtcYcEfwAAATAdBgNVHQ4EFgQU2jdkp2M6Nljo6K/8GJCM51I+99swHwYD -VR0jBBgwFoAUpyKhnMzMAmNc6EzYZ/14gkiD4HEwDQYJKoZIhvcNAQELBQADggEB -AJV2gv9PZak/1uNaV4C6ev3PgcLui4eBZwBeWM4mTrVNSMoIHsgj4J3lSWBW5KB6 -Ly4Ey9qdHdtx7OdHeRgErDtwkgblUc9jEIvTuxNNmGrgxzX7TlksfXg1dONoYhPI -VzB2qE5entd8xyG1JtzCIOHcDcqUFphqajeAz0mWElaaz0VI4YGE1NDF5fTROOrK -4V227FOSXCHoZeymRAp+ZfDNCjzYiO7euWxtmI2g4utq8VjxuLfLRq/KBxZwxrGK -mtRJV9xNgcgJQxW83Q2nITnOK2DI7Kd/4rbOUGOEgzRRaJIQtEH82dM/cjNxusyO -4ZS62G2D28rd8MDtIc36Bn0= +YWJiaXRtcYcEfwAAATAdBgNVHQ4EFgQUo/Gh8fNHfKZlVVuhKfA2Dvn+u2swHwYD +VR0jBBgwFoAU6//gYr2Xunun2eTaF//n2wM+ERUwDQYJKoZIhvcNAQELBQADggEB +ABtd35pW14Rrxa4SZSo06/CkGqpMnq7+NE2+84OfZyQxqKz8pFpIT6ny3YFfA/IB +S8nquyCdSsdzLbLN15R+iUdoYaKWKUCsntdVYRu68qOzJX/dSf8v6cmTLaYL38TW +g4gkkuXDi2SEId2R7+UtnlRnVqbufqUPxaUxpGnUEpsH9+zLMF5RRmH/l1A2AeqU +eZ7S5TgA9G/WYgpLLzgVSGU2/U6N4wCiIO2mXGnajF7lITLb9Kh7ad8LIR5xhT5S +Yo66IXFR8aWGejqSABLQGrKyiL/puyoGL+qrkNprTNPvF6vtAZQyFTvxo9zE8j0s +cKULlVSVz2IHxFJQAAYz8B0= -----END CERTIFICATE----- diff --git a/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem b/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem index a555f7a5054..d216da8dc97 100644 --- a/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem +++ b/deploy/dockerephemeral/rabbitmq-config/certificates/key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5kOXBzKO8T67A -pwV+ux1Cxv0C5XtIQKen0K9ak8xyeixuMBLFNyg8qEKKcMV3vA2paCsSqWAQ0kDO -OhSlG5Oe3pepx2X0ItyAT3tbhKUYCmg4enWYgRSfSY6EzHGnK+W3IXbVIC4k4IGn -MuQkX0gMPkQsEnn67VWoenIhZtZ1Vcbb3HhO8E+0ikK44Vij1FK2oi0ihlQZqTZ/ -KTyUIJSFaGOJR7zMx+TjI+ZyxCKnjaEHms5S+Nskcjar4kGGgjjnNYoamL0/AV1O -J2pDeTP7XY0yains2tpWf5c/cB3WK0QCI2Y0P+woYx1FqJAY8xE5E0IZS0mNLg7P -2kqoh7JTAgMBAAECggEADpXzlHsAobqEd2ZWQCLDOlT7kGDRgsysmojq+sxXceYY -tb6EmZWfM+/CO/4aOP7O9uWWGrzo4EABuDpur5dBaeezW+gTvPYihwyR5/ZYp1Ey -5cGTNB4G4D0lY43R6++lrXI6OqbxYy4PmUDrfgP/qaqy30ePTyhpjVK2kHNak9VV -+wL89csanvb2uKwhrgiFx+8BksFX/Y4CNInF1LIbNIMbehMCxHyO6TqhmErgwqPU -E2R6l2EsiHaihRurmMGmW2UitamUzFUAP3pnFcxCYWB9faiE4YRm2bemSaS5SufC -3NSzvfSJN7lmvx2q4+LulWiuLJ7EuqtzjmFAxFKgrQKBgQDlgdo5EtpSDbfbP5ME -oj8YBwdA0ZxKfv0yW7y34k7RtagrfC2ddNT7haZlCGQGyJ1leeQHSzhnZYvcWlmU -PH94YjzfoGQHF1a7qAIVldyi9sBkxI4jU0qbxKcSkfoGqEqAdfZo8nUzUoOTA90j -4eQK1ut7qT7Ss6yLE99HjSmpRwKBgQDO/IqvMPNlhzdDaFO+EIgLTD+KmbGFTtsA -PWf6RwFo3tVBgmAv4KqyutZlWxrqC2g5Ksfyw9azUz1U+BOF2pWohNZo/OHrbbkp -4dkrEANHlxCokDqXOVKaVTEKtqoZzT22VW/oFB9GxlPJL7q27PctXjiiB709uNJa -ph5jdsx0lQKBgQCJBjYbzT27r6UNqa9FHPk+hzO1Z3BAqgDRiCPsRZl5a1O0Yrd5 -Qr/GS81ElPXjdvNCGrwh/q72TJJsRSUmc9hHL5/YhBI0iaKm93AHIypPwbKsdw3F -2Xy583cshysXvnJ8r/EmR1viAGm95JirS7qzHg4KDsoLUmq5vmuYdJdjEQKBgELp -fOO5jVVq6sCNv1SX/4K3eWsS2EJiBYYEU9KilaATORleTj3sAQKaR6ioVQEIAv9I -By9Bg+ygohkPwS/qQ6sgljeGWHpFFDCn5A55tLW17hqv1WEBlORzWdE+z6pboPGK -mQyLRLkacAd/uHpeDGHMLb6jhdeoIchQH07EHsApAoGAbLLN6YsiBaP6vSj1gzZZ -ro1eKxyBSjzBixWRaTKFSbe6maUe3VNZ6z7m4013YyOuptanqSb/lqGwIotI0BNG -b4NPpAh7yoWJbcsuaFqa1DFTb20yVL2j3osLMsCOMtuKI/zqCzQgHgyqBnnYmLjp -JuXF8vUCg7hxjhk3pYjhCvg= +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/1qDtRHCkF8cY +aENJd0k04WtDfvHezlcSW8fqztPngbcWvSBp3dF/cgpuaNXbKw0X1T/zbFgLyd0o +nog7iv23b/WzYxVVezPmb8R+95AEa7bNi23YcyKfLr9EXmVJ8d9yW4k96SITtKDj +2gLXOp0YXCzE+EF/sASXeMkYDU+Vuq+A67sJjLLNp6qXb9HD6Fhq479yHLhoHZYk +ZBlXiKnk0/4OCakDize6HIxarkZGQnWCH1wDpeYCP6o/w3NuzPzqpePe3ZjvN5wu +/p2jTozyFRfgpJQB38n9FvCBKrQ/KkO652jBIqKWH7GOzX+ORQ/U0qCgWAjd2zCw +YqIzHfFJAgMBAAECggEAAN392KEwjALe0+07faHIAEdgQIV23x5g0ccurj4H/tD8 +DBG6ttD4hp0ySZhgDuwR6ymFbRuMpIsOx/3ZKtgHm0qG6H7zPncfjZrG1eUPoNLI +FnxIXnDKQ6YxBwKNUA5pFMHViS4sV+o9P2Ivvnfr4Zbf1wVblW/ytqjVhIS8AKIS +fbUS4csP1HN+v/mbFDrz/WMBxAzmrg/3Q4UQOIl9Wqv+Dn+FRQkx0XTKR8dB9XRY +XBVU4fVXFKK5+pm4ChAkQcNjVBJUsmFlWcrF6jP5GhBhL2/0jR4tcyqScsd6nyrb +WIUKlVxHdLU5UUC3iJ92Vw6TCn1UUCWXXTEKGv7PQQKBgQDfyI/j7R+wMSeA6Wdh +KIgzIlNCq9Xa4mvRQGxwLhQgqbTGuIpHmYlohJlPTtLGnrhuKlulKwU9DnK/3hjC +KWpOoZB3ozpDVVip52iQeWD3qcA9gi4C1I34d2EqBB0Sn0jNQaMsey5OktlSmWjA +jPIuE5/Sar7DMfpL8j/QwpRrQQKBgQDbdL79E8tvA3yJ7paSg6Wlqp+kvJn36rz+ +mrohJ58JiHGql7kccuOiALY6mrWBhGR9jXgeA809BgnHGCNJZgQnceXcBgy+3VdU +BWJDwNu86avpOWSDJcOMiZYO7TPD5imsB/GMqt/eKxY14WSZp4mfYd8/SEimgl+l +9HobrrcsCQKBgF/zWt7bmS6upMV9Tjo2as+h9BkuHG/RjXEXMmeXGkI3AbADCCdT +CbuqvyFmJrHK9EEoIEtdes2HGGR0EiGOKGq2k374mc6tFWskMY1gvdbzDd5RpvDH +umfCqAKf6OdHLKv1bMVxu5UtGcl0xZlp1Z00BN7vrgN7tlpB8GLOS10BAoGBAI0t ++OQ0xM4BLzBJBiLxgDIopxVCo8ajA7zDa3SC2cQ3O/CkNNkBbEG/NXxUJOpScpd2 +2Exu460bYlTryV+hupBprJc0aSSsnk6WPBYcTwCkTwz1+ByKwdd6d8fYf1HKkwpx +/coh720syNgWzTIwXs/jFczPQrWj78aClb+TUZGJAoGBAJavH8rWYzo9ivw/2WT8 +3dG4PuB4iGOYLgkQ6q8616XmRThf9CXmz5yfNxLcgLUmIeypeyzMEgJkBcdFDQy1 +WKRFa+FXqKf23nedrW3ojYwM9C14iYVgVjLd53xB++72EHs/s22Rd1h7ZQX+cWrV +Sc0kUf+KOfAdqYnCJYBKea0b -----END PRIVATE KEY----- diff --git a/hack/bin/gen-certs.sh b/hack/bin/gen-certs.sh index cea23db3ff5..462f321e1ba 100755 --- a/hack/bin/gen-certs.sh +++ b/hack/bin/gen-certs.sh @@ -56,6 +56,8 @@ install_certs "$TEMP/federation" "$ROOT_DIR/services/nginz/integration-test/conf integration-ca integration-ca-key integration-leaf integration-leaf-key install_certs "$TEMP/federation" "$ROOT_DIR/deploy/dockerephemeral/federation-v0" \ integration-ca "" integration-leaf integration-leaf-key +install_certs "$TEMP/federation" "$ROOT_DIR/deploy/dockerephemeral/federation-v1" \ + integration-ca "" integration-leaf integration-leaf-key # elasticsearch mkdir -p "$TEMP/es" diff --git a/hack/helm_vars/certs/elasticsearch-ca-key.pem b/hack/helm_vars/certs/elasticsearch-ca-key.pem index f5d62ee5d49..f59d94e52b6 100644 --- a/hack/helm_vars/certs/elasticsearch-ca-key.pem +++ b/hack/helm_vars/certs/elasticsearch-ca-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5RatgsLSQdpLd -JyW+kmpOeyIoJgJVm+76D3eSx5tPnSMtsGolXNJHynR4AzJ+h5tiSOhK+x/RPiv7 -ofjD5P+cl6q0cWckhQ089sp+deRjZrhvTskn/0xkg9W1Gk2awYl9Oq9ij2hLSQgt -w+QRkv0LkdnQLKyGWQ8N7BLksAcvr9N5gTaLE3PVQQopSaD72IE+gGk4HDQzCque -/eeqNjp5qq9umFlXJk8GX3HfB14VRslGmh5OIs8y/HJGFRMcbVXJ11T+d6xpmzCF -fwXYSN5OJghs1AC6SyirqYEgOigJsMnioYbbyKHh+HLAYKnY+Bi/KQJQ/ipwzibW -9G//058FAgMBAAECggEAIL62hnVUxH+gf2PO4PrBvTM4Gz50hSr1Ns8LBC8xPQX5 -1LZsXEQmije3FAsEnqZbCSj3nWD7A6FoZqX+8KiFoOiRbCjq4OJ/L3oy2dz+S685 -A7s6BE6z8sP2Pnbyplp0cWSw4MuV1FCJGIWZxp1jCetyQr/SkkAlUAGcaTzPWFb/ -BjbPM9VIZOgYXWMmvzdG5kQepn+lgHNXXQuZpnLiFAJsZ9esKyfjw4DTaAiZvrXY -gUX16eJlcOq9b+TZWBQKwgKHPCVc4GzPdoY4uUCXV/UuY7XVw8X/VXBzxq0jkqki -VvBR16Xl3hd8OcInUWj7jlf23ryR6yt7gsrCPhLFAQKBgQDk6azL3FCbqqLy5Lju -h/HEIFajl8HiQg1tL5/X/IkBeKw5bO0M7CzQ5WvhUXQViDu2xnFW0q+QT7l2sl4G -2CSTnMpBGCT1FigMBM5i9eEua6B3KXaNtOBX6dlvO2D4EBEToy5Ik6eydtroAS0J -ODdA/pGcjj7XjiMjskb6yV+iwQKBgQDPMgNfdovwikLqs2oJ8aKp/PdtS7K0EbmN -MhpwFV1K4uF/v1GzCcsrhvr4egmec7lhgqV+ODus97k3lxzAys5/VyAwo39cQEAo -QKWwzDr8JP2z5ftaXV0h4aph4DsoPnnf10tES0XVRdKW2mDBuSssU7VoCzU9+7ja -0wd51eoBRQKBgE4g8zkhGOIIe1Ure3LuMzYdU3TCdwoiQTLi7ktphdlatm1jIAUp -FqK1qvxcMKKovLjFQim//uviSgqZFj5/xvwap21QMEz2IvT3LvnXseOGGF6TaEM1 -WNyok+3C9nW0BiANsd5ThwkCR/SncheTeEhWmpw0cH5hpNyqHE+8K0gBAoGANCiQ -9M0w+UK1CcRUo2Ay5LwLxXXS7MWxgjvkr+aQ77MhtTkCZiHHBZQbRcXi+gKD3mo3 -Iwkg7LAH7liaImZriV7zeYsPGrgJ7pgnndQr3SGqxEjW966dLVRTwgPioITpxVG7 -XtvcHo5PLy6WQO5OUgBYoHKB2rKtnFiXfzI8kEkCgYEAtE21IgrJDzCennOfXPn5 -x3MZaO3t6fTKKrKXXUFYzh5JsQiUJQOzf7bEcYBi3VLqLNVZOg4H6FeydzEMbd0U -t+7+tj7vkw+B6q8npXIGZO592dNtKkoUA+DMGrjOOIV3d2GRzYZph7X1qvSZ4Twl -Q2QQlP+7XHJRVYE0oECGYPQ= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDhfp2hokSJ88qb +6Gl8BBwg4jMbkt4l2ynOa/lO6DZFRheFlWZBaUZAcj1ojBgbd3EoOXygZReL9TGB +Rrc5iACzytlOJkwMgNUUIq1KsPwWII41VJw3h3NO07tMTsf0kFvH1pEllJqorhQ1 +eZnU1SISyQcjk0oRQEyWd6arF7TLHna69OHF2ybYYXMDMFWNr+O6t8RUfYs4kb7z +1Nx3OnUKrUhIyaYeoyvBBOdXA5/G5GenDu/G4iVozsuLgofYWu+77EnpProF0KRK ++XlKakvF7bD26Qm1ol5qsXbdXOXKYq/c4KDdC938HNd/FNPz7EsnW0brBXqtz8Te +QmxHix7DAgMBAAECggEAOEemxiG+44OCbRk7wqUv9BEg2l/0rBQgQhH23nfcm7ub +wU6BgA/rZchdhUt59NkB2B1I+qtgjiD7Yx2oO2azbixRwkySrIg3JlhlUgAMWuVz +OOJOPxnCcMkttSTwiRzCm4T1IyEM3M7d4l7gQxuS7odYDcwEL3wR4XgplAhNqmgP +AxBayjX/POTsIoH2xPhjYsILPnRjDihQdxIWJoqZHUfIvM925tFi7WtzaFvyX8VU +s8t2zcByiqi1q9MLDv+uwhweP7tYGUj1RoQYoMuFjVRrm09nCRmAix2Evok1zXqV +6jPqgDe4+p03+816n8EWKgCTcAiZvFb7YhYkAO1gWQKBgQD7IKkUzvK6qKrN6Fg4 +bX4U6tAvKVGRhiNEfPRWv44OatusI1K14ndtLbZesQ+R2WgjwO1P2bhs6BDeV/oo +Y5T2ETBRcAV154NJxv5ktGw5IaI0IAlYBy/daaTY9wZBZ8+Qy5+FHra0ClHOMzKl +qQ3UeSMNZ+E5mH9ofMfhRFs8lwKBgQDl3qMqaBEDg/2O58IqwjWG9QGEGfoyqEi4 +C1DwQhvPAFa8PJ/mqwFZtYd1WpLV+wMm6WZWhVTpjQIYo7jYX54Vz2/y94AqtRC6 +sqlJgMCDSGjVeVgzIw2L1yaxdl+WzhbtGQUPmKxh9BM1WmJysst2/f7rk9MjyTFg +auhtAkH4tQKBgQCqA92UsdrRFjm093Uqlq5CWQqiszV+8TJVPsdpJ3x0NFIOg0eO +zgiOiOEr0HG7C1YexpGjesIKMT6iWSuKRojl4pM0v0NjJF7VBvzZjvCp6SRYZ8wL +pan5G3m4Td0VUMPMwp530GhfEZF6qVzDnOU5EN3zSH3JsX2obrofv1iJdwKBgDnO +iFfkvcqVidFDRRf9qPpcaNowsjPFECyAZAVXiqi+3BEQaeHXRUqrFPqVIXIAYuWJ +Mnw1oYnuNQW/Pn/jY9z2Qp/mT+vthtx8i4f5gfBB6GMu1dheS0zMeWWNcDJ7d1Z+ +wUAP0+H6QE5dgX54qiQtccsKbMGGGg22NOcc9zw1AoGAGiacZEXrdHsg1piMlHjw +LE96b7mynZfOO4LPm/0Xl5FJ8mElNnZGWlpMFVq+Hi2WnHQ6MRRIZmOS3YTdS4FB +cBvwiGGn18QEgFGOI7JonzPDp1LZWnxy5nmZhmTuI4GiXyYAqGw6QLGiqqQo4p08 +J7OLSRyQ7aiG97iUS1QgJFU= -----END PRIVATE KEY----- diff --git a/hack/helm_vars/certs/elasticsearch-ca.pem b/hack/helm_vars/certs/elasticsearch-ca.pem index 1c6f3128257..6511f688a73 100644 --- a/hack/helm_vars/certs/elasticsearch-ca.pem +++ b/hack/helm_vars/certs/elasticsearch-ca.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDLzCCAhegAwIBAgIUXMOPFnGTAQ30+xOQ2od/HYZiSwQwDQYJKoZIhvcNAQEL +MIIDLzCCAhegAwIBAgIUUOLn63PL3FEyGdhOK1ocDAn8dC8wDQYJKoZIhvcNAQEL BQAwJzElMCMGA1UEAwwcZWxhc3RpY3NlYXJjaC5jYS5leGFtcGxlLmNvbTAeFw0y -NDA3MTgwNjQ4MjBaFw0zNDA3MTYwNjQ4MjBaMCcxJTAjBgNVBAMMHGVsYXN0aWNz +NDA5MDMxMjAzMzhaFw0zNDA5MDExMjAzMzhaMCcxJTAjBgNVBAMMHGVsYXN0aWNz ZWFyY2guY2EuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQC5RatgsLSQdpLdJyW+kmpOeyIoJgJVm+76D3eSx5tPnSMtsGolXNJHynR4 -AzJ+h5tiSOhK+x/RPiv7ofjD5P+cl6q0cWckhQ089sp+deRjZrhvTskn/0xkg9W1 -Gk2awYl9Oq9ij2hLSQgtw+QRkv0LkdnQLKyGWQ8N7BLksAcvr9N5gTaLE3PVQQop -SaD72IE+gGk4HDQzCque/eeqNjp5qq9umFlXJk8GX3HfB14VRslGmh5OIs8y/HJG -FRMcbVXJ11T+d6xpmzCFfwXYSN5OJghs1AC6SyirqYEgOigJsMnioYbbyKHh+HLA -YKnY+Bi/KQJQ/ipwzibW9G//058FAgMBAAGjUzBRMB0GA1UdDgQWBBRRMD3Fk8ui -R5eRsezFZrT0oMKlpzAfBgNVHSMEGDAWgBRRMD3Fk8uiR5eRsezFZrT0oMKlpzAP -BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAQSW/I5EbTbsBY4rIA -RX+0S7gbx2+d1fhMFEloLagXipn8vd8PGtX9riYSb4k2KmP4BjeQo3TMpJVTUmh8 -RkppbOLabIcyuCI732PJfwlkRHBThguv905uuzil8IC1mDR7qy6Rkg2ByDRlqAgB -icX/3uG7A6XDNsNrwP8Pj0X/YRSbqLIlFtyQ6RCCOYn1CCUDkciHgDMHlgK90r4s -+hrgtB6zKdMI89hnn8MLbQ+eaZ9UDpYbovBbDvZfEI5AaTlSQTL503+gM5bUgZRl -YT8z44Piip8VhkKwb/31C+tht6gNvqEQBFudusrHrg/KphpFTpAHQgW2vA/z9LJt -vzAR +AoIBAQDhfp2hokSJ88qb6Gl8BBwg4jMbkt4l2ynOa/lO6DZFRheFlWZBaUZAcj1o +jBgbd3EoOXygZReL9TGBRrc5iACzytlOJkwMgNUUIq1KsPwWII41VJw3h3NO07tM +Tsf0kFvH1pEllJqorhQ1eZnU1SISyQcjk0oRQEyWd6arF7TLHna69OHF2ybYYXMD +MFWNr+O6t8RUfYs4kb7z1Nx3OnUKrUhIyaYeoyvBBOdXA5/G5GenDu/G4iVozsuL +gofYWu+77EnpProF0KRK+XlKakvF7bD26Qm1ol5qsXbdXOXKYq/c4KDdC938HNd/ +FNPz7EsnW0brBXqtz8TeQmxHix7DAgMBAAGjUzBRMB0GA1UdDgQWBBTWzp/VRTGq +s/IwlrROQGDwavTpyzAfBgNVHSMEGDAWgBTWzp/VRTGqs/IwlrROQGDwavTpyzAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBFue/SGpAVZ0TOVp8l +bdaaY/e9wUBSdCH5b39Nzd3rKmH3lIHqcafLlFCx+scKUIlFbohJr0aTK339wFfl +L0LzBJVUT9JzeDNPhk8pl/jBJk+eGP3fiykFMCgxxGvHtccHu8E/y8U0SeEtKqDn +Xy0ZbC3M54UedhDpHMovfHEsfN24Ev0DK13sBR2T8fmXCyCrfq887cCqJyP2ODgb +xAY/R4F8Ueadn0ywHYSY3MqmDsvDul0QlaOu2J5A0+k5oy4hAfFB8PzPYZrmPkeU +N5oxudTTihIZ+0JiL2JmWGBzMGzgtmD1rHC6lugUlWq+BoPu2+/+hn8RcVHBCFDk +WMSU -----END CERTIFICATE----- diff --git a/services/nginz/integration-test/conf/nginz/integration-ca-key.pem b/services/nginz/integration-test/conf/nginz/integration-ca-key.pem index 56193dd04ae..1017ec966b5 100644 --- a/services/nginz/integration-test/conf/nginz/integration-ca-key.pem +++ b/services/nginz/integration-test/conf/nginz/integration-ca-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDdACYyKlFap22W -4gRJum+5ooEhiAOo7NChzrYONQIuDxzyHdgh7kOmSnErwxPAHoHspJMSwkTWoLwT -KOrmvLvzEXaf8UNwBoUw0xpcvYaaXGaNJ5F0Lb10i3DSvC/7LqUs68Z7dKg9nXGq -vChszwWJP0RES6RFame7AESHf0CNUuwzL0kkxoB66sctFVwXeBPs4hJSHWRuphDM -MeGoMgyvxbJc8NQqeFH79TV4SM0qH6yifZyOsLikgMppiYdRvJe9h+DzmvmAi12j -3Q1Fb9Y/9R6q7FrPXzPf8/Bn43qifeFif2dT+ubIuvXqOM19fE8zBOxMGclSgYit -XgPKYUz1AgMBAAECggEADVeHdsrYO31VH+FIOf0/5niZjCEue2HEnKgilIv9tDMk -X7eOh0nfmqfu2iH+TMBqvpMW7/B7gGuWvx9ewwxM2nlI7JH/rMEwBEkSU9v7RtFi -PY5QeS+Tuvf6GTbUPLlNrn8TbfuWdpXNOW3/kMYUwvrnT6ozYh9w4Li86mMzzfRB -YkzXq9Ex5Stmb7jPw1XiHyC+cM51V9dtZ8/TJWVi5XlUQjWgHT9keuoJp8470fru -J544z5Su2xWAeLzC0i208C0vZLN86ZtLWJkYhD83XUiHHRq+YrCvlSaaeG/1wj+u -mPnu3P7m0aSpCaVyuAmjPm4bNnUrqtRYkLR/NU/DgQKBgQD2iZXXx5uiHYAa63s3 -rvfNJkc6ehmSQTGPKSeCqCzjLamvJ7eEphzcFdDL53JkRoohYh4diUPOXvmPdobz -mnZ/3FIal64aGiwagor0V4ER6IXU12emH51/RdfaxI+8lfNUUMfYxVmSPlYDR4pg -F2u1gskOedWMN3g85TrE0vSqwQKBgQDle6Un8oMcU9JJ9qjEJuoaISqT7x7jyjbs -xUlECWZs12gWxzwzinio1VFxDJcnXNrdfJZ3bmhTRLJBLXyzjdRN6jIlnDdOjf1e -YJrI1iOXHRyBuL2EKcAzG7L3MToK1rYr13tUAUSW7HQ9DqPr9b+jSuKhg27F9C5Q -Ne+NqJWzNQKBgCY9Ht2yGySg+L60KY9wdwT92+xpBdBWhk5TLsqoNRYjff8p5OAR -N8a3J4SI6Ig/HKui4VLpeHfo6UJkOvhLy/d2/9EaF6n6xz5xYwYVEHLrot5pbq0o -mDAmcB2BgV3Z0D0Srnyj14nEW2j0zrSqzU0A9Rhms0WlUOP5Fg1zPvnBAoGAILjl -zvFssqBdLwDGBdpKrVknWhrRu8d813w2O0Zf3YtFo2Hbern3BJQOXeFeuFUsPELk -rbkHlUAJbvPOgUfrCwUnC2fgFwp2I3wA9jxarNSQ2Qp/s5XEe0Uq2sahMSR2q3+5 -bTwVDLRAyugIhb/wCJfIAyHbrMxpwjQ+qWNtnTUCgYEAuM0uyQX5MLi6X/DwzEFv -2ROJDaLekh1NJfj70J5RMsdRBltG0t12vsBDt2k/vHPoFQLZ0nNm8KBcNWnuRyGB -4yE5Dr8CJofXgmzbFdR9osIT9QsIAiG5GCwP2btn6HPBqcK2fv2IMkQNe1tJfPdN -6SGL/tnl/QRLRXbOwyrz5wM= +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCZHfooV9E6ekRu +FHvZEBb0xxctgy/KwvLveeocte//bE8iHDJEzrWTHsPrmvmazc19cdVj+AU+UWUA +P8MuoqBKT+QVRcXvbHZlamGsA/Quflyk3pBBRd4CNDCBmeFbptoXVDh08A2rQIHY +qrAcLgsRzObXqyLc2UrK5GBZQISEpjPQrFBTkfta6qTDVkUA+c82dEpOR3sjNf/b +B75K6tuK6iqpINP7kTkxzMna5rkgKxubSqpHW9s6CcDrPLKJq2kcwdSTpuf4v96Q +k+Yga+YH/HixrW9hrl4Jp0yNdvUdmvi24n6kwt9YyDIP8jXxblODHVBvcBOddoIY +V2SPh6HPAgMBAAECggEAAKZ48SzrouTxX3T6JazaJsX7BOLMFCoJysHoyvXBhC3b +PtjXJHsAQxSxXlHKgkF3gEiN0HPgNH3iAO3vD/FO3vdB5Q6RIvgsnzekHI0+aegi +z+xwuRDpOsx/8ZJMEQ3qBTQCsrRw9RY8DEXao56qcLPpvbwTVNQeFjMsGZTwOyP7 +4IjnYTwt+YrH4UNZPlBMm6ph/1qukTYqdBK2dEKXwxzAvpo+JPQ6SgcKfU3uHjmN +PofzM39l3+ZVoG7WdHEcqN/0tyFz/q/pQMOveyjRFn6fa7HDBEVrOwTKETBlJyFN +nVgMCFqwL2bltu1NNs90w9+q+6VoNME9Wlk3CUuGSQKBgQDRC7jwSvtA0sWOKft6 +QdEK5s8HEP1RUuNBUOETY6jo6I3HBPLWk1W2CiUtDJxMsKdz5QCrHp//LmB9gULk +bZ4tfEXQbbBOYnlbo2cM6VTBf36znnlosHFgF+NqBiYin2cbkkOMvJTa01efvkq9 +02raQNLMP0z4GfZwnDUdNk/85QKBgQC7glD2cJ7QP7VouVYIrmk42LLBfQxOFsbx +NptMf+UID1ayG3Qf3tiRVGtaQ/DRd3uPG+Wuumy5bxwOSCF1P7UmLY+1Rpz2DBda +JGrcHfuhBxI8WineMUwqnDWMtEPwDi9/9WlAoyXbWw4MzwiHCXgPfV7iBXgrAC1/ +ULB81rVsowKBgQCLSUdBfIRqzcVqExkHfeEeZWmeKLjQvezD8XL2q1m5TnJhIC/5 +vxPGBn58xMFD7BS3COfoHLC4o5sRJNaAQ3W4kuwlk2B86eo4n+ii1rltcFjor3fv +xFjWkTQqycwRF6ro2Qz/Mgvwvg7NVkqQrtSsdbK++pJ7YTkuETbmrvCe7QKBgQCf +SXfvsgInlEdOPEtKuqbmRKet2MWwPIcp+CJ7HRZ5/1W9nbbLMCq3YoiDuL2Fo8OR +8bfu861S5YFm3H2XtdP0J7Yx31eNaP4ZdGBWtx3AUFp8bHeuqiAy/lo7OhOQhOxy +/g44e5+4NSS9Ws66sB+OwQjuZokLtm3v/qK+mkKqkwKBgH9ostvH6Iqy9BRZ0hG7 +/EhQAGB2zekJOaOnMqYElzL1PY53SyVv/7jre3OwhUQLE1O3JQGTaj48/5EPeBUC +0jFg5AUM7nD53pbCtGlaKhbBB27lCN/tGKK5OVx4nRw0uRPGiJJ3ZXQps+lE6ZTa +AtzSX1cRPfA/Ff/lB3RR9I5Y -----END PRIVATE KEY----- diff --git a/services/nginz/integration-test/conf/nginz/integration-ca.pem b/services/nginz/integration-test/conf/nginz/integration-ca.pem index a38ae3c9efa..6a33fa9e2c3 100644 --- a/services/nginz/integration-test/conf/nginz/integration-ca.pem +++ b/services/nginz/integration-test/conf/nginz/integration-ca.pem @@ -1,19 +1,19 @@ -----BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIUMtt4ZsS3KWgdBSet+D9ifnmmYiQwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwNzE4MDY0ODIwWhcN -MzQwNzE2MDY0ODIwWjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN0AJjIqUVqnbZbiBEm6b7migSGIA6js -0KHOtg41Ai4PHPId2CHuQ6ZKcSvDE8AegeykkxLCRNagvBMo6ua8u/MRdp/xQ3AG -hTDTGly9hppcZo0nkXQtvXSLcNK8L/supSzrxnt0qD2dcaq8KGzPBYk/RERLpEVq -Z7sARId/QI1S7DMvSSTGgHrqxy0VXBd4E+ziElIdZG6mEMwx4agyDK/Fslzw1Cp4 -Ufv1NXhIzSofrKJ9nI6wuKSAymmJh1G8l72H4POa+YCLXaPdDUVv1j/1HqrsWs9f -M9/z8GfjeqJ94WJ/Z1P65si69eo4zX18TzME7EwZyVKBiK1eA8phTPUCAwEAAaNT -MFEwHQYDVR0OBBYEFL8s7KTIH5P0tLQ9TdQ3bKAatD2DMB8GA1UdIwQYMBaAFL8s -7KTIH5P0tLQ9TdQ3bKAatD2DMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBANIYy7aAUp+UFfcxsPIcNCjAeYdQNi09wJ1cvg6ctU1GzRGinvr9PiCR -f13ZNX0SsK4farNR2UK9TTF1vv++sYiC6EgzJFWBqBW3XqjQMqF12rApf9faHvg9 -bcyWJNgQxuXc6ugxXrUI+Sj4U2LRnnEkh427/Hs1WbD4Bd0zfTHMCVmk6gvi2kcU -e3agZInTAIAwS59afC/6bGIaXb6QVyWlnEhWB2LJRIM0H7aXB2Ot63upin+yDZGU -Esz3c82RNPBzRaUGJezlaQq5ZGZyJjkkBWjYaSYO2RaR3h/PqhLIfshqC223SCCV -Uo2ofMIfAvXNA8hHK/u3f1WVTEJ4ERE= +MIIDEzCCAfugAwIBAgIUEfjIXW9tD1WgwNHJ+kC3r6Cmv5swDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOY2EuZXhhbXBsZS5jb20wHhcNMjQwOTAzMTIwMzM3WhcN +MzQwOTAxMTIwMzM3WjAZMRcwFQYDVQQDDA5jYS5leGFtcGxlLmNvbTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJkd+ihX0Tp6RG4Ue9kQFvTHFy2DL8rC +8u956hy17/9sTyIcMkTOtZMew+ua+ZrNzX1x1WP4BT5RZQA/wy6ioEpP5BVFxe9s +dmVqYawD9C5+XKTekEFF3gI0MIGZ4Vum2hdUOHTwDatAgdiqsBwuCxHM5terItzZ +SsrkYFlAhISmM9CsUFOR+1rqpMNWRQD5zzZ0Sk5HeyM1/9sHvkrq24rqKqkg0/uR +OTHMydrmuSArG5tKqkdb2zoJwOs8somraRzB1JOm5/i/3pCT5iBr5gf8eLGtb2Gu +XgmnTI129R2a+LbifqTC31jIMg/yNfFuU4MdUG9wE512ghhXZI+Hoc8CAwEAAaNT +MFEwHQYDVR0OBBYEFFqlhDsVqlH8UUKGOtCDE9xmYL3hMB8GA1UdIwQYMBaAFFql +hDsVqlH8UUKGOtCDE9xmYL3hMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBADQyh0k3xhcrNOqhvzAK1/A3TY5Hq1FE/a17Yiq8DIiLCJG/nAN60GBx +m7zuL8xoRJ4ylwIswa4z4rHj9p6M6tIbi2tTfJsbyB+FjyFRWoBmTngqNCiw7QUR +/ofSliuEu/YIjphR8LmTBvy4fVccTwXDaBPEGf2iN+DFmryLHxVpsVh3AA0uUSy0 +e2bZJLRwv1z0saC5KGHpWb6RJbAP2nRw5omcorMtP1KW8XyVESiJm7hDZAx6VLgD +k4GcEOUEq9CJs9UVAkIIDS87CfppHZEGPDK3Ufro5AhIwA3hpSJkTgzkf0TcQKIr +4E/zJSeTld4rMC26ghWodIwGRyofTP0= -----END CERTIFICATE----- diff --git a/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem b/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem index 28266b3e5b3..e4ee0a09ab1 100644 --- a/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem +++ b/services/nginz/integration-test/conf/nginz/integration-leaf-key.pem @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQClrSse09wlL7P6 -pcsCNQ16CcrYS7+IF5yq9Nul0gnJsHl3XC1SKqNU1shMfcZYa4pEXF1LU414EWkL -/0WWVTs23U0fcUaybZJAG57jXD+571vEkoFCESJxBPLCtEOBUYbLx1IE2bZ9ybHZ -yD+W9XulQ7FgIS04tPgkttunl11CXdRAcMiL796/t8j0eVU049Pta420/hWYiVzv -fz74AyUCQwOlXhl8Io8HgO0NUNgWwu+3kI/2oswPZFcCXmSBTyht6CjSYNDtViVy -btdd+U7InM2XTKczXaweP1oBniWLxem1vRAltkYU0S89jnlcBtfoJHTUDKLBeNal -mys0ebrLAgMBAAECggEACxs0t3hUWwR7ouMtB2ovC8NO9Hj/efa7QJVG3t2EXR+W -Gkj08NEoPy5hdwnncKik4uLzjh0nxVNwIKcYLx+/kcn8EDjebbpSrOGCdpNfN5kI -JIFTEO69Hv50l6t7QFa1cUEHX96IZp5NbIq0A0FUQfFBGW9Kl3E/lpZ7hcdBL+BR -VrL96mTFqKtvYza/8wOOT05XCvVZH2Q/cN75Ih11t84UHtUCZWjAOoRhddcblo/2 -jCj91N2W7/Zut94o/2KcRY8Glf8Dps7gZUE1cB3PaZvHU5eEsCupwyNYwx785Pn1 -zzyCGklxeSu7t84WJh/B8dx6uYGiVRcd+ujc3BLKgQKBgQDerZ+ReVEF6cWYOKQD -6Hv2bv4PdME5LQkq1XoT4AQNRb2pXw2BrA/Crr7xNlOz2u6hN+LnmEFLfzMR/5UE -2UAS4tENTb4eibqgeBPVx3RksfpHWni2y8Dvdax3GgDwx9BVVMycOe3td3IlpnAN -rRU+jYRZuAZr5OQdQESu5bNUUQKBgQC+d+qB43WWtla47nFMEWM0RB6nHqbAo+dw -0BIaoxJyuC6SyE7P7APdBAlOw8P0Hy/Dzwe+ZzGnjXiXYyctTHss/zSLfhf+43/A -Z+Mzi5u0vzL60ING5UzBY9P/0XxrJym0eVr4YYPBvLBojwLhiDgQIXUHkgUO7IBY -UPjM+ggiWwKBgQC+zq3FvNulonxThG1ef+8A6ni/C7+qW6HYV1alAzbVnKX5JN7w -91wF6TDqhi/RFM+Xy8idxMRmiddcG9I4dmRGCp8xtCUuC7ykVmBAtglRY4RfcfGw -SQXI6t9eqySVLdKh2+j8EVOEQO7JvkWUInTqxd7b9ilieJ7TRcfUyjUREQKBgQCR -QWp6fDllItGoX0/QL0J0za6CzQFm0JjklAn6fnrHOmdqUZCpSNj5aOagRvPd7RrE -PdMuBgz8NwvMiDWMelNF0asE5rjuDhmTZqcC3Gl2wonidbpoCt8qbTN0WRKFtWw8 -0n/qBJQy3++5Dbeov/Xhd2KEz3tEEmEe+UGFMPmbGQKBgHcpn8SGrNuxjL/6vB1j -a5+LNOQAtr7akKlJbrl9B7poMVu8483fN4LG2HclWPPILnE3iPU/G559ZMo3QL6G -0+dImewlUSGtQ7GtLssai5wmmgLfXufiewHq/WoLDkd0xdu5RHc7GGgixx8XqpBG -ZTrVeb2nVeiwvIiZkuT13bGf +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPYxemHc21P8y2 +DIDNsuUzVpnc1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwl +R66fg07whBcZspsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v +11v/+Iq6LNerC/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7 +kBgflylEKjapfCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j +1rz3IPfKOOtIgxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfo +wOpkgZPdAgMBAAECggEAVMurFUB1ZkkmXj9hgPnHLoUX10xJ3ZMIy7jlkS1ZRsD8 +EK0jDj2q0OtRSet9xJ7i3nfFTojzE5obqxCSrWUmp0ATI+4789DjuZluv8quAdm1 +0U73zHq43GZNlY7yco2YN1Lh7H5yepXz0dDILLLGolYIfscdw7YoUCvuI2vt8eyY +FaMkOi8Bs32Abn+53MhaNkEOVemmwP/u2rD81IY7pXQtF+1XfSxYBipHCB7phC1g +dr6ITU9CoF3SXvjYL6uP/7W1Du/CuaDSijVZJkBfQSgbkQbBr6pAovumtpNZgZvO +bixqS4oTZqsd+Rgl3YBOpx+JbuUFL0fG+uQBv1yZbwKBgQD8n7s1w/ouUGwW+qGi +4EFTYVDFD2Rgvg3oFPHt72zreaNXSy8YWrK9Mas/Fzu5knSE3f3O0S1WvqWdt/z6 +uPDLTpg9fIWX6v+hPX09F5ekeLlUDBazBT1PQUD0qd8PiyFNB1F+ffSPTxmKScjc +hTqQCtnun6rlICalWO9VvGt6QwKBgQDSKJkjxIGLtXBaXfMQiF9dQDrMk2it5grl +w0OnpPhYvpdp+Cfi01kMUrnfHwF0v92BqeqoKZo4DwJXkrmwf5kNnftxmRqbk9gE +dJq/E/6SELyT/chtzXfxC/wTmyyxhfZUJvJUxlaZ6KP/86t8A22DJcFO2Z85iUGH +8zy2UJUnXwKBgBvSs9m+FeXX8a+uNvMrY8Z9J1os0c9d30Y6WFLuVb6xjO3mV++E +vb7co5G1S1yq5q5jjLqkiyvMn4z5YKF0kQCzTU0oU8ZhmXn2vb5mxMrWiQLauf1J +jHEYLMFFnE2n8yj6r10RHkhSW+vBKKAxBDwtFceUSkwl+FupqeJ1eBjlAoGBAMou ++LWqdZ89HSwzOobrTCPgiTELmCfFKzLE2q/MTIjEQ9NVRLo57m+mnt+DatkxRR9b +oz/JVm8cMXqi1DZza4HoPWGalDic0bPnooC18bIAnAwcmdjZVcz3ZLpQDX10jfmD +xpu8fNBxOmYhvRcADTmg9wqu3zpxTDRI1F3pxLUtAoGAGfsX4bve5cLm49Oa1p0H +kEErLMuAMIKQNVsbzVELepLYr+uwEXBCXyyoIf79ABDvUHbzxMEwgANuet/4PQzS +yB1qzFk6GDvqZ5dfPUgMUWH9wvD1qEGp6yxkyESGt8CNwnu8GI50NAeSh2/JeUIa +r/u+m2vnJjOXpJdOJ+7f6yM= -----END PRIVATE KEY----- diff --git a/services/nginz/integration-test/conf/nginz/integration-leaf.pem b/services/nginz/integration-test/conf/nginz/integration-leaf.pem index 9eca6a70899..abd724df6b1 100644 --- a/services/nginz/integration-test/conf/nginz/integration-leaf.pem +++ b/services/nginz/integration-test/conf/nginz/integration-leaf.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgIBADANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5jYS5l -eGFtcGxlLmNvbTAeFw0yNDA3MTgwNjQ4MjBaFw0zNDA3MTYwNjQ4MjBaMAAwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQClrSse09wlL7P6pcsCNQ16CcrY -S7+IF5yq9Nul0gnJsHl3XC1SKqNU1shMfcZYa4pEXF1LU414EWkL/0WWVTs23U0f -cUaybZJAG57jXD+571vEkoFCESJxBPLCtEOBUYbLx1IE2bZ9ybHZyD+W9XulQ7Fg -IS04tPgkttunl11CXdRAcMiL796/t8j0eVU049Pta420/hWYiVzvfz74AyUCQwOl -Xhl8Io8HgO0NUNgWwu+3kI/2oswPZFcCXmSBTyht6CjSYNDtViVybtdd+U7InM2X -TKczXaweP1oBniWLxem1vRAltkYU0S89jnlcBtfoJHTUDKLBeNalmys0ebrLAgMB +eGFtcGxlLmNvbTAeFw0yNDA5MDMxMjAzMzhaFw0zNDA5MDExMjAzMzhaMAAwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPYxemHc21P8y2DIDNsuUzVpnc +1bj81QIQ5a2aFSVtQFr3Dhpo0XXyu0KHKR64ZelmW33Yoe2J0zwlR66fg07whBcZ +spsonXmsUB5uFFF/Dcv9shBNcg67jjynXPDvJK2sC37GCar5ar/v11v/+Iq6LNer +C/OqMJlMYyFDif1BvESbnw+9hsihUT9qK5s6md5krEV1/ro0Noh7kBgflylEKjap +fCrVMnAMgaMV55jrk5BTpoR6KCDuDmXPk3Ed304Op/krGazfGB2j1rz3IPfKOOtI +gxn8n5PDYmShVUw1R3Rps1LeEyhsH1EGuAvVx1mKhB3QGMqFtIfowOpkgZPdAgMB AAGjgawwgakwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEgGA1UdEQEB /wQ+MDyCGSouaW50ZWdyYXRpb24uZXhhbXBsZS5jb22CFGhvc3QuZG9ja2VyLmlu -dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFIro61Yvf3swiRDOD/qOkwKJ0+Li -MB8GA1UdIwQYMBaAFL8s7KTIH5P0tLQ9TdQ3bKAatD2DMA0GCSqGSIb3DQEBCwUA -A4IBAQDNWgHWibMJvGI5YzkTlgXEvxjTTdYM6SpyLQFkju/PUuLP4KoiOvl2SY// -OWJH9v1XmZJ1DlnNRdgAHW+Uj8SpXJXRPkm1/5B9d0Eh8kfc+oiapZT7qfrKH5Ln -Wod5A/gzBv8rpaqHP8HP00b5SFdStTnqbQeBPXYMl+cbVwHBtZF3U6NVVJc0VOEe -MWx8bhJ6Vn8KmcLLoPPJVf4/u/toFAm7q61yZWISOMpZmLMbis0M+vJ6t57ImVTv -utffv1HhuCfEWQSc4XHI9JOMc3iexJYfgZQCIZdC7VxEJaOVB1DSkc3bYq/UaEyf -Wo6HKS47x295j7b1rbSO5ZCbhPYb +dGVybmFsgglsb2NhbGhvc3QwHQYDVR0OBBYEFBkpAu3ILiU4gtEYffAU7zxGHPC6 +MB8GA1UdIwQYMBaAFFqlhDsVqlH8UUKGOtCDE9xmYL3hMA0GCSqGSIb3DQEBCwUA +A4IBAQBB1VsthdoVT9ExXkfKixotbXm6+eBgYenK1R5Qx/UX3JrlI1nF/8rKMg5e +7QfMCydSJwVEQdvnXD3ddVhUTYRActQvnJwWTyXfeiezrfDCTLu4SNpLOP7ojFlq +9ZX/E9GC0axTIUmEIy8YIC3JJ2PAlvw9qMzrsivyAgbof3NX+9XXKfwZHBwSLsO1 +Gxr9zkL+U/qww7TvyJD1LqBR0UEd9pZriorpVVFAa/JlFQX5ip1Smcd6m97nq20N +qpUIalra+K6qHxjHVwA2UxVgbO9bLFIBmp9pNvSm+5umAKkmqFnHRNAHfCy/IFGl +3fw8u9mXJ8LzUR4tiS0cVb6bwQzd -----END CERTIFICATE----- From 8a28e7d938a49b4e80434849518917d6986897ea Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 5 Sep 2024 11:04:02 +0200 Subject: [PATCH 057/136] Handle `emails` field in scim user record (#4221) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Extract email address from emails field in scim user schema. * Refactor: turn findByEmail handler into findByExternalId (which is what it always claimed to be) * Refactor: handle externalIds as Text, not Email in cassandra effect. * Fetch email_unvalidated from brig for scim in spar. * return extended account from internal brig endpoint * ... --------- Co-authored-by: Leif Battermann Co-authored-by: Marko Dimjašević --- changelog.d/0-release-notes/WPB-665 | 20 ++ changelog.d/2-features/WPB-665 | 1 + integration/default.nix | 2 + integration/integration.cabal | 1 + integration/test/API/Brig.hs | 19 ++ integration/test/API/BrigInternal.hs | 16 +- integration/test/API/Common.hs | 9 + integration/test/API/Spar.hs | 33 ++ integration/test/SetupHelpers.hs | 8 +- integration/test/Test/Spar.hs | 285 ++++++++++++++++++ integration/test/Test/User.hs | 4 +- integration/test/Testlib/HTTP.hs | 3 + integration/test/Testlib/Types.hs | 4 + libs/hscim/src/Web/Scim/Schema/Meta.hs | 2 + libs/hscim/src/Web/Scim/Schema/User/Email.hs | 11 + libs/hscim/test/Test/Schema/UserSpec.hs | 40 ++- libs/wire-api/default.nix | 2 + .../src/Wire/API/Routes/Internal/Brig.hs | 2 +- libs/wire-api/src/Wire/API/User.hs | 34 ++- .../src/Wire/API/User/EmailAddress.hs | 20 ++ libs/wire-api/src/Wire/API/User/Identity.hs | 80 +---- libs/wire-api/src/Wire/API/User/Password.hs | 3 +- libs/wire-api/src/Wire/API/User/Phone.hs | 77 +++++ libs/wire-api/src/Wire/API/User/Scim.hs | 97 +++--- libs/wire-api/wire-api.cabal | 2 + services/brig/src/Brig/API/Internal.hs | 18 +- services/brig/src/Brig/API/User.hs | 33 +- services/brig/src/Brig/Data/User.hs | 66 ++-- services/brig/src/Brig/Team/API.hs | 2 +- services/spar/default.nix | 4 + services/spar/spar.cabal | 7 +- services/spar/src/Spar/API.hs | 2 +- services/spar/src/Spar/App.hs | 12 +- services/spar/src/Spar/Intra/Brig.hs | 24 +- services/spar/src/Spar/Intra/BrigApp.hs | 76 +++-- services/spar/src/Spar/Scim/User.hs | 241 +++++++-------- services/spar/src/Spar/Sem/BrigAccess.hs | 11 +- services/spar/src/Spar/Sem/BrigAccess/Http.hs | 4 +- .../spar/src/Spar/Sem/ScimExternalIdStore.hs | 14 +- .../Spar/Sem/ScimExternalIdStore/Cassandra.hs | 33 +- .../src/Spar/Sem/ScimExternalIdStore/Mem.hs | 13 +- .../test-integration/Test/Spar/APISpec.hs | 20 +- .../test-integration/Test/Spar/DataSpec.hs | 9 +- .../Test/Spar/Scim/UserSpec.hs | 189 +++++++----- services/spar/test-integration/Util/Core.hs | 17 +- services/spar/test-integration/Util/Scim.hs | 27 +- .../spar/test/Test/Spar/Intra/BrigSpec.hs | 27 +- services/spar/test/Test/Spar/Scim/UserSpec.hs | 44 +-- 48 files changed, 1122 insertions(+), 546 deletions(-) create mode 100644 changelog.d/0-release-notes/WPB-665 create mode 100644 changelog.d/2-features/WPB-665 create mode 100644 libs/wire-api/src/Wire/API/User/Phone.hs diff --git a/changelog.d/0-release-notes/WPB-665 b/changelog.d/0-release-notes/WPB-665 new file mode 100644 index 00000000000..4068db3f62c --- /dev/null +++ b/changelog.d/0-release-notes/WPB-665 @@ -0,0 +1,20 @@ +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/2-features/WPB-665 b/changelog.d/2-features/WPB-665 new file mode 100644 index 00000000000..97fb03a1462 --- /dev/null +++ b/changelog.d/2-features/WPB-665 @@ -0,0 +1 @@ +SCIM's emails field is now handled and the external ID is not restricted to being an email anymore diff --git a/integration/default.nix b/integration/default.nix index 040ab8db6f5..abff642f97d 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -59,6 +59,7 @@ , regex-base , regex-tdfa , retry +, saml2-web-sso , scientific , split , stm @@ -151,6 +152,7 @@ mkDerivation { regex-base regex-tdfa retry + saml2-web-sso scientific split stm diff --git a/integration/integration.cabal b/integration/integration.cabal index 30bc3c641c3..d6e3384b98c 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -229,6 +229,7 @@ library , regex-base , regex-tdfa , retry + , saml2-web-sso , scientific , split , stm diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index d933c0c8a1e..65e8d1d1961 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -764,3 +764,22 @@ revokeApplicationAccess user cid password = do cidStr <- asString cid req <- baseRequest user Brig Versioned $ joinHttpPath ["oauth", "applications", cidStr, "sessions"] submit "DELETE" $ req & addJSONObject ["password" .= password] + +registerUser :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +registerUser domain email inviteeCode = do + req <- baseRequest domain Brig Versioned "register" + submit "POST" $ + req + & addJSONObject + [ "name" .= "Alice", + "email" .= email, + "password" .= defPassword, + "team_code" .= inviteeCode + ] + +activate :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +activate domain key code = do + req <- rawBaseRequest domain Brig Versioned $ joinHttpPath ["activate"] + submit "GET" $ + req + & addQueryParams [("key", key), ("code", code)] diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 0e840713bc3..38fe56ac943 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -148,9 +148,13 @@ deleteOAuthClient user cid = do getInvitationCode :: (HasCallStack, MakesValue user, MakesValue inv) => user -> inv -> App Response getInvitationCode user inv = do tid <- user %. "team" & asString + getInvitationCodeForTeam user tid inv + +getInvitationCodeForTeam :: (HasCallStack, MakesValue domain, MakesValue inv) => domain -> String -> inv -> App Response +getInvitationCodeForTeam domain tid inv = do invId <- inv %. "id" & asString req <- - baseRequest user Brig Unversioned $ + baseRequest domain Brig Unversioned $ "i/teams/invitation-code?team=" <> tid <> "&invitation_id=" <> invId submit "GET" req @@ -284,3 +288,13 @@ createOAuthClient :: (HasCallStack, MakesValue user) => user -> String -> String createOAuthClient user name url = do req <- baseRequest user Brig Unversioned "i/oauth/clients" submit "POST" $ req & addJSONObject ["application_name" .= name, "redirect_url" .= url] + +getInvitationByEmail :: (HasCallStack, MakesValue domain) => domain -> String -> App Response +getInvitationByEmail domain email = do + req <- baseRequest domain Brig Unversioned "i/teams/invitations/by-email" + submit "GET" $ req & addQueryParams [("email", email)] + +getActivationCode :: (HasCallStack, MakesValue domain) => domain -> String -> App Response +getActivationCode domain email = do + req <- baseRequest domain Brig Unversioned "i/users/activation-code" + submit "GET" $ req & addQueryParams [("email", email)] diff --git a/integration/test/API/Common.hs b/integration/test/API/Common.hs index 6b80f9e5305..e1c91d05b7c 100644 --- a/integration/test/API/Common.hs +++ b/integration/test/API/Common.hs @@ -20,6 +20,15 @@ randomEmail = do u <- randomName pure $ u <> "@example.com" +randomExternalId :: App String +randomExternalId = liftIO $ do + -- external ID has no constraints, but we only generate human-readable samples + n <- randomRIO (8, 15) + replicateM n pick + where + chars = mkArray $ ['A' .. 'Z'] <> ['a' .. 'z'] <> ['0' .. '9'] + pick = (chars !) <$> randomRIO (Array.bounds chars) + randomName :: App String randomName = liftIO $ do n <- randomRIO (8, 15) diff --git a/integration/test/API/Spar.hs b/integration/test/API/Spar.hs index e8d1e7cc2f3..f040889f6c4 100644 --- a/integration/test/API/Spar.hs +++ b/integration/test/API/Spar.hs @@ -1,7 +1,9 @@ module API.Spar where import API.Common (defPassword) +import Data.String.Conversions.Monomorphic (fromLT) import GHC.Stack +import qualified SAML2.WebSSO as SAML import Testlib.Prelude -- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_scim_auth_tokens @@ -21,3 +23,34 @@ createScimUser domain token scimUser = do req <- baseRequest domain Spar Versioned "/scim/v2/Users" body <- make scimUser submit "POST" $ req & addJSON body . addHeader "Authorization" ("Bearer " <> token) + +findUsersByExternalId :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +findUsersByExternalId domain scimToken externalId = do + req <- baseRequest domain Spar Versioned "/scim/v2/Users" + submit "GET" $ req + & addQueryParams [("filter", "externalId eq \"" <> externalId <> "\"")] + & addHeader "Authorization" ("Bearer " <> scimToken) + & addHeader "Accept" "application/scim+json" + +getScimUser :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +getScimUser domain scimToken uid = do + req <- baseRequest domain Spar Versioned $ joinHttpPath ["scim", "v2", "Users", uid] + submit "GET" $ req + & addHeader "Authorization" ("Bearer " <> scimToken) + & addHeader "Accept" "application/scim+json" + +updateScimUser :: (HasCallStack, MakesValue domain, MakesValue scimUser) => domain -> String -> String -> scimUser -> App Response +updateScimUser domain scimToken userId scimUser = do + req <- baseRequest domain Spar Versioned $ joinHttpPath ["scim", "v2", "Users", userId] + body <- make scimUser + submit "PUT" $ req + & addJSON body . addHeader "Authorization" ("Bearer " <> scimToken) + & addHeader "Accept" "application/scim+json" + +createIdp :: (HasCallStack, MakesValue user) => user -> SAML.IdPMetadata -> App Response +createIdp user metadata = do + req <- baseRequest user Spar Unversioned "/identity-providers" + submit "POST" $ req + & addQueryParams [("api_version", "v2")] + & addXML (fromLT $ SAML.encode metadata) + & addHeader "Content-Type" "application/xml" diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 73a7bfe692f..796e6786429 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -24,6 +24,7 @@ import qualified Data.Text as Text import Data.Text.Encoding (decodeUtf8) import Data.UUID.V1 (nextUUID) import Data.UUID.V4 (nextRandom) +import Data.Vector (fromList) import GHC.Stack import Testlib.MockIntegrationService (mkLegalHoldSettings) import Testlib.Prelude @@ -342,11 +343,16 @@ lhDeviceIdOf bob = do randomScimUser :: App Value randomScimUser = do email <- randomEmail + randomScimUserWith email email + +randomScimUserWith :: (HasCallStack) => String -> String -> App Value +randomScimUserWith extId email = do handle <- randomHandleWithRange 12 128 pure $ object [ "schemas" .= ["urn:ietf:params:scim:schemas:core:2.0:User"], - "externalId" .= email, + "externalId" .= extId, + "emails" .= Array (fromList [object ["value" .= email]]), "userName" .= handle, "displayName" .= handle ] diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index ab147901071..12f67d1200e 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -2,9 +2,18 @@ module Test.Spar where +import qualified API.Brig as Brig +import API.BrigInternal as BrigInternal +import API.Common (randomEmail, randomExternalId, randomHandle) +import API.GalleyInternal (setTeamFeatureStatus) import API.Spar import Control.Concurrent (threadDelay) +import Data.Vector (fromList) +import qualified Data.Vector as Vector +import SAML2.WebSSO.Test.Util (SampleIdP (..), makeSampleIdPMetadata) import SetupHelpers +import Testlib.JSON +import Testlib.PTest import Testlib.Prelude testSparUserCreationInvitationTimeout :: (HasCallStack) => App () @@ -26,3 +35,279 @@ testSparUserCreationInvitationTimeout = do -- ...we should be able to create the user again retryT $ bindResponse (createScimUser OwnDomain tok scimUser) $ \res -> do res.status `shouldMatchInt` 201 + +testSparExternalIdDifferentFromEmailWithIdp :: (HasCallStack) => App () +testSparExternalIdDifferentFromEmailWithIdp = do + (owner, tid, _) <- createTeam OwnDomain 1 + void $ setTeamFeatureStatus owner tid "sso" "enabled" + void $ registerTestIdPWithMeta owner >>= getJSON 201 + tok <- createScimToken owner >>= getJSON 200 >>= (%. "token") >>= asString + email <- randomEmail + extId <- randomExternalId + scimUser <- randomScimUserWith extId email + userId <- createScimUser OwnDomain tok scimUser >>= getJSON 201 >>= (%. "id") >>= asString + activateEmail OwnDomain email + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` email + subject <- u %. "sso_id.subject" >>= asString + subject `shouldContainString` extId + u %. "handle" `shouldMatch` (scimUser %. "userName") + + -- Verify that updating `userName` (handle) works + scimUserWith1Update <- do + newHandle <- randomHandle + updatedScimUser <- setField "userName" newHandle scimUser + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "userName" `shouldMatch` newHandle + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "handle" `shouldMatch` newHandle + pure updatedScimUser + + -- Verify that updating the user's external ID works + scimUserWith2Updates <- do + newExtId <- randomExternalId + updatedScimUser <- setField "externalId" newExtId scimUserWith1Update + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "externalId" `shouldMatch` newExtId + checkSparGetUserAndFindByExtId OwnDomain tok newExtId userId $ \u -> do + u %. "externalId" `shouldMatch` newExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` email + subject <- u %. "sso_id.subject" >>= asString + subject `shouldContainString` newExtId + bindResponse (findUsersByExternalId OwnDomain tok extId) $ \res -> do + res.json %. "totalResults" `shouldMatchInt` 0 + res.json %. "Resources" `shouldMatch` ([] :: [Value]) + pure updatedScimUser + + -- Verify that updating the user's email works + do + let oldEmail = email + newEmail <- randomEmail + updatedScimUser <- setField "emails" (Array (Vector.fromList [object ["value" .= newEmail]])) scimUserWith2Updates + currentExtId <- updatedScimUser %. "externalId" >>= asString + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + + -- before activation the old email should still be present + checkSparGetUserAndFindByExtId OwnDomain tok currentExtId userId $ \u -> do + u %. "externalId" `shouldMatch` currentExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` newEmail + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` oldEmail + subject <- u %. "sso_id.subject" >>= asString + subject `shouldContainString` currentExtId + + -- after activation the new email should be present + activateEmail OwnDomain newEmail + checkSparGetUserAndFindByExtId OwnDomain tok currentExtId userId $ \u -> do + u %. "externalId" `shouldMatch` currentExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` newEmail + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` newEmail + subject <- u %. "sso_id.subject" >>= asString + subject `shouldContainString` currentExtId + +registerTestIdPWithMeta :: (HasCallStack, MakesValue owner) => owner -> App Response +registerTestIdPWithMeta owner = do + SampleIdP idpmeta _ _ _ <- makeSampleIdPMetadata + createIdp owner idpmeta + +testSparExternalIdDifferentFromEmail :: (HasCallStack) => App () +testSparExternalIdDifferentFromEmail = do + (owner, tid, _) <- createTeam OwnDomain 1 + tok <- createScimToken owner >>= \resp -> resp.json %. "token" >>= asString + email <- randomEmail + extId <- randomExternalId + scimUser <- randomScimUserWith extId email + userId <- createScimUser OwnDomain tok scimUser >>= getJSON 201 >>= (%. "id") >>= asString + + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + res.json >>= asList >>= shouldBeEmpty + + registerUser OwnDomain tid email + + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` email + u %. "sso_id.scim_external_id" `shouldMatch` extId + u %. "handle" `shouldMatch` (scimUser %. "userName") + + -- Verify that updating the scim user works + scimUserWith1Update <- do + -- FUTUREWORK: test updating other fields besides handle as well + newHandle <- randomHandle + updatedScimUser <- setField "userName" newHandle scimUser + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "userName" `shouldMatch` newHandle + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "handle" `shouldMatch` newHandle + pure updatedScimUser + + -- Verify that updating the user's external ID works + scimUserWith2Updates <- do + newExtId <- randomExternalId + updatedScimUser <- setField "externalId" newExtId scimUserWith1Update + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + res.json %. "externalId" `shouldMatch` newExtId + checkSparGetUserAndFindByExtId OwnDomain tok newExtId userId $ \u -> do + u %. "externalId" `shouldMatch` newExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` email + u %. "sso_id.scim_external_id" `shouldMatch` newExtId + bindResponse (findUsersByExternalId OwnDomain tok extId) $ \res -> do + res.json %. "totalResults" `shouldMatchInt` 0 + res.json %. "Resources" `shouldMatch` ([] :: [Value]) + pure updatedScimUser + + -- Verify that updating the user's email works + do + let oldEmail = email + newEmail <- randomEmail + updatedScimUser <- setField "emails" (Array (Vector.fromList [object ["value" .= newEmail]])) scimUserWith2Updates + currentExtId <- updatedScimUser %. "externalId" >>= asString + bindResponse (updateScimUser OwnDomain tok userId updatedScimUser) $ \res -> do + res.status `shouldMatchInt` 200 + + -- before activation the new email should be returned by the SCIM API + checkSparGetUserAndFindByExtId OwnDomain tok currentExtId userId $ \u -> do + u %. "externalId" `shouldMatch` currentExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` newEmail + -- however brig should still return the old email + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` oldEmail + u %. "sso_id.scim_external_id" `shouldMatch` currentExtId + + -- after activation the new email should be present + activateEmail OwnDomain newEmail + checkSparGetUserAndFindByExtId OwnDomain tok currentExtId userId $ \u -> do + u %. "externalId" `shouldMatch` currentExtId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` newEmail + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` newEmail + u %. "sso_id.scim_external_id" `shouldMatch` currentExtId + +testSparExternalIdUpdateToANonEmail :: (HasCallStack) => App () +testSparExternalIdUpdateToANonEmail = do + (owner, tid, _) <- createTeam OwnDomain 1 + tok <- createScimToken owner >>= \resp -> resp.json %. "token" >>= asString + scimUser <- randomScimUser >>= removeField "emails" + email <- scimUser %. "externalId" >>= asString + userId <- bindResponse (createScimUser OwnDomain tok scimUser) $ \resp -> do + resp.status `shouldMatchInt` 201 + (resp.json %. "emails" >>= asList >>= assertOne >>= (%. "value") >>= asString) `shouldMatch` email + resp.json %. "id" >>= asString + registerUser OwnDomain tid email + + let extId = "notanemailaddress" + updatedScimUser <- setField "externalId" extId scimUser + updateScimUser OwnDomain tok userId updatedScimUser >>= assertStatus 400 + +testSparMigrateFromExternalIdOnlyToEmail :: (HasCallStack) => Tagged "mailUnchanged" Bool -> App () +testSparMigrateFromExternalIdOnlyToEmail (MkTagged emailUnchanged) = do + (owner, tid, _) <- createTeam OwnDomain 1 + tok <- createScimToken owner >>= \resp -> resp.json %. "token" >>= asString + scimUser <- randomScimUser >>= removeField "emails" + email <- scimUser %. "externalId" >>= asString + userId <- createScimUser OwnDomain tok scimUser >>= getJSON 201 >>= (%. "id") >>= asString + registerUser OwnDomain tid email + + -- Verify that updating a user with an empty emails does not change the email + bindResponse (updateScimUser OwnDomain tok userId scimUser) $ \resp -> do + resp.json %. "emails" `shouldMatch` (Array (fromList [object ["value" .= email]])) + resp.status `shouldMatchInt` 200 + + newEmail <- if emailUnchanged then pure email else randomEmail + let newEmails = (Array (fromList [object ["value" .= newEmail]])) + updatedScimUser <- setField "emails" newEmails scimUser + updateScimUser OwnDomain tok userId updatedScimUser `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "externalId" `shouldMatch` (updatedScimUser %. "externalId") + resp.json %. "emails" `shouldMatch` (updatedScimUser %. "emails") + + -- after activation the new email should be present + unless emailUnchanged $ activateEmail OwnDomain newEmail + + extId <- scimUser %. "externalId" >>= asString + checkSparGetUserAndFindByExtId OwnDomain tok extId userId $ \u -> do + u %. "externalId" `shouldMatch` extId + (u %. "emails" >>= asList >>= assertOne >>= (%. "value")) `shouldMatch` newEmail + bindResponse (getUsersId OwnDomain [userId]) $ \res -> do + res.status `shouldMatchInt` 200 + u <- res.json >>= asList >>= assertOne + u %. "email" `shouldMatch` newEmail + u %. "sso_id.scim_external_id" `shouldMatch` extId + +registerUser :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App () +registerUser domain tid email = do + BrigInternal.getInvitationByEmail domain email + >>= getJSON 200 + >>= BrigInternal.getInvitationCodeForTeam domain tid + >>= getJSON 200 + >>= (%. "code") + >>= asString + >>= Brig.registerUser domain email + >>= assertSuccess + +activateEmail :: (HasCallStack, MakesValue domain) => domain -> String -> App () +activateEmail domain email = do + (key, code) <- bindResponse (BrigInternal.getActivationCode domain email) $ \res -> do + (,) + <$> (res.json %. "key" >>= asString) + <*> (res.json %. "code" >>= asString) + Brig.activate domain key code >>= assertSuccess + +checkSparGetUserAndFindByExtId :: (HasCallStack, MakesValue domain) => domain -> String -> String -> String -> (Value -> App ()) -> App () +checkSparGetUserAndFindByExtId domain tok extId uid k = do + usersByExtIdResp <- findUsersByExternalId domain tok extId + usersByExtIdResp.status `shouldMatchInt` 200 + userByIdExtId <- usersByExtIdResp.json %. "Resources" >>= asList >>= assertOne + k userByIdExtId + + userByUidResp <- getScimUser domain tok uid + userByUidResp.status `shouldMatchInt` 200 + userByUid <- userByUidResp.json + k userByUid + + userByUid `shouldMatch` userByIdExtId diff --git a/integration/test/Test/User.hs b/integration/test/Test/User.hs index 3c6cfdb9694..e1400fe254d 100644 --- a/integration/test/Test/User.hs +++ b/integration/test/Test/User.hs @@ -5,7 +5,7 @@ module Test.User where import API.Brig import API.BrigInternal import API.GalleyInternal -import API.Spar +import qualified API.Spar as Spar import qualified Data.Aeson as Aeson import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID @@ -115,7 +115,7 @@ testUpdateHandle = do resp.status `shouldMatchInt` 200 mb <- (assertOne =<< asList resp.json) %. "managed_by" mb `shouldMatch` "wire" - bindResponse (getScimTokens owner) $ \resp -> do + bindResponse (Spar.getScimTokens owner) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "tokens" `shouldMatch` ([] @String) diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index 6f1a9677eec..2cb0158c16f 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -42,6 +42,9 @@ addJSONObject = addJSON . Aeson.object addJSON :: (Aeson.ToJSON a) => a -> HTTP.Request -> HTTP.Request addJSON obj = addBody (HTTP.RequestBodyLBS (Aeson.encode obj)) "application/json" +addXML :: ByteString -> HTTP.Request -> HTTP.Request +addXML xml = addBody (HTTP.RequestBodyBS xml) "application/xml" + addUrlEncodedForm :: [(String, String)] -> HTTP.Request -> HTTP.Request addUrlEncodedForm form req = req diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 5ba37b377b3..23295c1dd55 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -11,6 +11,7 @@ import Control.Monad.Base import Control.Monad.Catch import Control.Monad.Reader import Control.Monad.Trans.Control +import Crypto.Random (MonadRandom (..)) import Data.Aeson import qualified Data.Aeson as Aeson import Data.ByteString (ByteString) @@ -330,6 +331,9 @@ newtype App a = App {unApp :: ReaderT Env IO a} MonadBaseControl IO ) +instance MonadRandom App where + getRandomBytes n = liftIO (getRandomBytes n) + runAppWithEnv :: Env -> App a -> IO a runAppWithEnv e m = runReaderT (unApp m) e diff --git a/libs/hscim/src/Web/Scim/Schema/Meta.hs b/libs/hscim/src/Web/Scim/Schema/Meta.hs index 5f5fc851047..11439c94c6a 100644 --- a/libs/hscim/src/Web/Scim/Schema/Meta.hs +++ b/libs/hscim/src/Web/Scim/Schema/Meta.hs @@ -33,6 +33,8 @@ data ETag = Weak Text | Strong Text instance ToJSON ETag where toJSON (Weak tag) = String $ "W/" <> pack (show tag) + -- (if a strong tag contains a "W/" prefix by accident, it will be parsed as weak tag. this + -- is mildly confusing, but should do no harm.) toJSON (Strong tag) = String $ pack (show tag) instance FromJSON ETag where diff --git a/libs/hscim/src/Web/Scim/Schema/User/Email.hs b/libs/hscim/src/Web/Scim/Schema/User/Email.hs index 664a91550b7..cd52a80a7e8 100644 --- a/libs/hscim/src/Web/Scim/Schema/User/Email.hs +++ b/libs/hscim/src/Web/Scim/Schema/User/Email.hs @@ -17,6 +17,7 @@ module Web.Scim.Schema.User.Email where +import Control.Applicative ((<|>)) import Data.Aeson import Data.Text hiding (dropWhile) import Data.Text.Encoding (decodeUtf8, encodeUtf8) @@ -51,3 +52,13 @@ instance ToJSON Email where emailToEmailAddress :: Email -> Email.EmailAddress emailToEmailAddress = unEmailAddress . value + +scimEmailsToEmailAddress :: [Email] -> Maybe Email.EmailAddress +scimEmailsToEmailAddress es = pickPrimary es <|> pickFirst es + where + pickFirst [] = Nothing + pickFirst (e : _) = Just (unEmailAddress (value e)) + + pickPrimary = pickFirst . Prelude.filter isPrimary + + isPrimary e = primary e == Just (ScimBool True) diff --git a/libs/hscim/test/Test/Schema/UserSpec.hs b/libs/hscim/test/Test/Schema/UserSpec.hs index 8dcaa9d50ed..1885060facc 100644 --- a/libs/hscim/test/Test/Schema/UserSpec.hs +++ b/libs/hscim/test/Test/Schema/UserSpec.hs @@ -38,7 +38,7 @@ import Lens.Micro import Network.URI.Static (uri) import Test.Hspec import Test.Schema.Util (genUri, mk_prop_caseInsensitive) -import Text.Email.Validate (emailAddress) +import Text.Email.Validate (emailAddress, validate) import qualified Web.Scim.Class.User as UserClass import Web.Scim.Filter (AttrPath (..)) import Web.Scim.Schema.Common (ScimBool (ScimBool), URI (..), WithId (..), lowerKey) @@ -69,6 +69,38 @@ type UserExtraPatch = KeyMap.KeyMap Text spec :: Spec spec = do + describe "scimEmailsToEmailAddress" $ do + let Right adr1 = validate "one@example.com" + Right adr2 = validate "two@example.com" + Right adr3 = validate "three@example.com" + + false1 = Nothing + false2 = Just (ScimBool False) + true = Just (ScimBool True) + + it "returns Nothing if empty" $ do + scimEmailsToEmailAddress [] `shouldBe` Nothing + + it "returns first primary if it exists" $ do + scimEmailsToEmailAddress + [ Email Nothing (EmailAddress adr1) false1, + Email Nothing (EmailAddress adr2) false2, + Email (Just "this is ignored") (EmailAddress adr3) true + ] + `shouldBe` Just adr3 + + it "returns first entry if no primary exists" $ do + scimEmailsToEmailAddress + [ Email Nothing (EmailAddress adr1) false1, + Email Nothing (EmailAddress adr2) false2 + ] + `shouldBe` Just adr1 + scimEmailsToEmailAddress + [ Email Nothing (EmailAddress adr1) false2, + Email Nothing (EmailAddress adr2) false1 + ] + `shouldBe` Just adr1 + describe "applyPatch" $ do it "only applies patch for supported fields" $ do let schemas' = [] @@ -127,13 +159,15 @@ spec = do toJSON minimalUser `shouldBe` minimalUserJson eitherDecode (encode minimalUserJson) `shouldBe` Right minimalUser it "treats 'null' and '[]' as absence of fields" $ - eitherDecode (encode minimalUserJsonRedundant) `shouldBe` Right minimalUser + eitherDecode (encode minimalUserJsonRedundant) + `shouldBe` Right minimalUser it "allows casing variations in field names" $ do require $ mk_prop_caseInsensitive genUser require $ mk_prop_caseInsensitive (ListResponse.fromList . (: []) <$> genStoredUser) eitherDecode (encode minimalUserJsonNonCanonical) `shouldBe` Right minimalUser it "doesn't require the 'schemas' field" $ - eitherDecode (encode minimalUserJsonNoSchemas) `shouldBe` Right minimalUser + eitherDecode (encode minimalUserJsonNoSchemas) + `shouldBe` Right minimalUser it "doesn't add 'extra' if it's an empty object" $ do toJSON (extendedUser UserExtraEmpty) `shouldBe` extendedUserEmptyJson eitherDecode (encode extendedUserEmptyJson) diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 8f29e1c12b5..1f765bee115 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -95,6 +95,7 @@ , tasty-hunit , tasty-quickcheck , text +, these , time , tinylog , transitive-anns @@ -194,6 +195,7 @@ mkDerivation { sop-core tagged text + these time tinylog transitive-anns 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 1c583784bbb..f55d8a83dea 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -249,7 +249,7 @@ type AccountAPI = ] "includePendingInvitations" Bool - :> Get '[Servant.JSON] [UserAccount] + :> Get '[Servant.JSON] [ExtendedUserAccount] ) :<|> Named "iGetUserContacts" diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 4875538165b..10d218f34c3 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -59,7 +59,6 @@ module Wire.API.User CreateUserSparInternalResponses, newUserFromSpar, urefToExternalId, - urefToExternalIdUnsafe, urefToEmail, ExpiresIn, newUserTeam, @@ -109,6 +108,7 @@ module Wire.API.User -- * Account UserAccount (..), + ExtendedUserAccount (..), -- * Scim invitations NewUserScimInvitation (..), @@ -855,9 +855,6 @@ urefToEmail uref = case uref ^. SAML.uidSubject . SAML.nameID of SAML.UNameIDEmail email -> emailAddressText . SAMLEmail.render . CI.original $ email _ -> Nothing -urefToExternalIdUnsafe :: SAML.UserRef -> Text -urefToExternalIdUnsafe = CI.original . SAML.unsafeShowNameID . view SAML.uidSubject - data CreateUserSparError = CreateUserSparHandleError ChangeHandleError | CreateUserSparRegistrationError RegisterError @@ -1795,11 +1792,30 @@ data UserAccount = UserAccount deriving (ToJSON, FromJSON, S.ToSchema) via Schema.Schema UserAccount instance Schema.ToSchema UserAccount where + schema = Schema.object "UserAccount" userAccountObjectSchema + +userAccountObjectSchema :: ObjectSchema SwaggerDoc UserAccount +userAccountObjectSchema = + UserAccount + <$> accountUser Schema..= userObjectSchema + <*> accountStatus Schema..= Schema.field "status" Schema.schema + +-- | This can be parsed as UserAccount, but it has an extra field `email_unvalidated` from +-- brig's cassandra that is needed in spar. so we return this from GET /i/users in brig. +data ExtendedUserAccount = ExtendedUserAccount + { account :: UserAccount, + emailUnvalidated :: Maybe EmailAddress + } + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform ExtendedUserAccount) + deriving (ToJSON, FromJSON, S.ToSchema) via Schema.Schema ExtendedUserAccount + +instance Schema.ToSchema ExtendedUserAccount where schema = - Schema.object "UserAccount" $ - UserAccount - <$> accountUser Schema..= userObjectSchema - <*> accountStatus Schema..= Schema.field "status" Schema.schema + Schema.object "ExtendedUserAccount" $ + ExtendedUserAccount + <$> account Schema..= userAccountObjectSchema + <*> emailUnvalidated Schema..= maybe_ (Schema.optField "email_unvalidated" Schema.schema) ------------------------------------------------------------------------------- -- NewUserScimInvitation @@ -1808,6 +1824,7 @@ data NewUserScimInvitation = NewUserScimInvitation -- FIXME: the TID should be captured in the route as usual { newUserScimInvTeamId :: TeamId, newUserScimInvUserId :: UserId, + newUserScimExternalId :: Text, newUserScimInvLocale :: Maybe Locale, newUserScimInvName :: Name, newUserScimInvEmail :: EmailAddress, @@ -1823,6 +1840,7 @@ instance Schema.ToSchema NewUserScimInvitation where NewUserScimInvitation <$> newUserScimInvTeamId Schema..= Schema.field "team_id" Schema.schema <*> newUserScimInvUserId Schema..= Schema.field "user_id" Schema.schema + <*> newUserScimExternalId Schema..= field "external_id" schema <*> newUserScimInvLocale Schema..= maybe_ (optField "locale" Schema.schema) <*> newUserScimInvName Schema..= Schema.field "name" Schema.schema <*> newUserScimInvEmail Schema..= Schema.field "email" Schema.schema diff --git a/libs/wire-api/src/Wire/API/User/EmailAddress.hs b/libs/wire-api/src/Wire/API/User/EmailAddress.hs index 5048230e48c..7c5bc2dacc7 100644 --- a/libs/wire-api/src/Wire/API/User/EmailAddress.hs +++ b/libs/wire-api/src/Wire/API/User/EmailAddress.hs @@ -5,6 +5,9 @@ module Wire.API.User.EmailAddress emailAddress, emailAddressText, module Text.Email.Parser, + emailToSAMLNameID, + emailFromSAMLNameID, + emailFromSAML, ) where @@ -13,7 +16,9 @@ where ----- import Cassandra.CQL qualified as C +import Control.Lens ((^.)) import Data.ByteString.Conversion hiding (toByteString) +import Data.CaseInsensitive qualified as CI import Data.Data (Proxy (..)) import Data.OpenApi hiding (Schema, ToSchema) import Data.Schema @@ -22,6 +27,8 @@ import Data.Text.Encoding import Data.Text.Encoding.Error import Deriving.Aeson import Imports +import SAML2.WebSSO.Types qualified as SAML +import SAML2.WebSSO.Types.Email qualified as SAMLEmail import Servant.API qualified as S import Test.QuickCheck import Text.Email.Parser @@ -108,3 +115,16 @@ arbitraryValidMail = do notNull x && notAt x && isValid (fromString ("me@" <> x)) + +emailFromSAMLNameID :: SAML.NameID -> Maybe EmailAddress +emailFromSAMLNameID nid = case nid ^. SAML.nameID of + SAML.UNameIDEmail eml -> Just . emailFromSAML . CI.original $ eml + _ -> Nothing + +-- | FUTUREWORK(fisx): if saml2-web-sso exported the 'NameID' constructor, we could make this +-- function total without all that praying and hoping. +emailToSAMLNameID :: EmailAddress -> Either String SAML.NameID +emailToSAMLNameID = SAML.emailNameID . fromEmail + +emailFromSAML :: SAMLEmail.Email -> EmailAddress +emailFromSAML = fromJust . emailAddressText . SAMLEmail.render diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index 2929efa269b..a551ea825e2 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -40,27 +40,19 @@ module Wire.API.User.Identity -- * UserSSOId UserSSOId (..), - emailFromSAML, - emailToSAMLNameID, - emailFromSAMLNameID, mkSampleUref, mkSimpleSampleUref, ) where import Cassandra qualified as C -import Control.Applicative (optional) import Control.Error (hush) -import Control.Lens (dimap, over, (.~), (?~), (^.)) +import Control.Lens (dimap, (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Aeson.Types qualified as A -import Data.Attoparsec.Text import Data.ByteString (fromStrict, toStrict) -import Data.ByteString.Conversion import Data.ByteString.UTF8 qualified as UTF8 -import Data.CaseInsensitive qualified as CI -import Data.OpenApi (ToParamSchema (..)) import Data.OpenApi qualified as S import Data.Schema import Data.Text qualified as Text @@ -71,19 +63,17 @@ import Imports import SAML2.WebSSO (UserRef (..)) import SAML2.WebSSO.Test.Arbitrary () import SAML2.WebSSO.Types qualified as SAML -import SAML2.WebSSO.Types.Email qualified as SAMLEmail import SAML2.WebSSO.XML qualified as SAML import Servant -import Servant.API qualified as S import System.FilePath (()) -import Test.QuickCheck qualified as QC import Text.Email.Parser import URI.ByteString qualified as URI import URI.ByteString.QQ (uri) import Web.Scim.Schema.User.Email () import Wire.API.User.EmailAddress +import Wire.API.User.Phone import Wire.API.User.Profile (fromName, mkName) -import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) +import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -------------------------------------------------------------------------------- -- UserIdentity @@ -141,57 +131,6 @@ ssoIdentity :: UserIdentity -> Maybe UserSSOId ssoIdentity (SSOIdentity ssoid _) = Just ssoid ssoIdentity _ = Nothing --------------------------------------------------------------------------------- --- Phone - -newtype Phone = Phone {fromPhone :: Text} - deriving stock (Eq, Ord, Show, Generic) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema Phone) - -instance ToParamSchema Phone where - toParamSchema _ = toParamSchema (Proxy @Text) - -instance ToSchema Phone where - schema = - over doc (S.description ?~ "E.164 phone number") $ - fromPhone - .= parsedText "PhoneNumber" (maybe (Left "Invalid phone number. Expected E.164 format.") Right . parsePhone) - -instance ToByteString Phone where - builder = builder . fromPhone - -instance FromByteString Phone where - parser = parser >>= maybe (fail "Invalid phone") pure . parsePhone - -instance S.FromHttpApiData Phone where - parseUrlPiece = maybe (Left "Invalid phone") Right . fromByteString . encodeUtf8 - -instance S.ToHttpApiData Phone where - toUrlPiece = decodeUtf8With lenientDecode . toByteString' - -instance Arbitrary Phone where - arbitrary = - Phone . Text.pack <$> do - let mkdigits n = replicateM n (QC.elements ['0' .. '9']) - mini <- mkdigits 8 - maxi <- mkdigits =<< QC.chooseInt (0, 7) - pure $ '+' : mini <> maxi - -deriving instance C.Cql Phone - --- | Parses a phone number in E.164 format with a mandatory leading '+'. -parsePhone :: Text -> Maybe Phone -parsePhone p - | isValidPhone p = Just $! Phone p - | otherwise = Nothing - --- | Checks whether a phone number is valid, i.e. it is in E.164 format --- with a mandatory leading '+' followed by 10-15 digits. -isValidPhone :: Text -> Bool -isValidPhone = either (const False) (const True) . parseOnly e164 - where - e164 = char '+' *> count 8 digit *> count 7 (optional digit) *> endOfInput - -------------------------------------------------------------------------------- -- UserSSOId @@ -308,19 +247,6 @@ lenientlyParseSAMLNameID (Just txt) = do (pure . Just) (hush asxml <|> hush asemail <|> hush astxt) -emailFromSAML :: SAMLEmail.Email -> EmailAddress -emailFromSAML = fromJust . emailAddressText . SAMLEmail.render - --- | FUTUREWORK(fisx): if saml2-web-sso exported the 'NameID' constructor, we could make this --- function total without all that praying and hoping. -emailToSAMLNameID :: EmailAddress -> Either String SAML.NameID -emailToSAMLNameID = SAML.emailNameID . fromEmail - -emailFromSAMLNameID :: SAML.NameID -> Maybe EmailAddress -emailFromSAMLNameID nid = case nid ^. SAML.nameID of - SAML.UNameIDEmail email -> Just . emailFromSAML . CI.original $ email - _ -> Nothing - -- | For testing. Create a sample 'SAML.UserRef' value with random seeds to make 'Issuer' and -- 'NameID' unique. FUTUREWORK: move to saml2-web-sso. mkSampleUref :: Text -> Text -> SAML.UserRef diff --git a/libs/wire-api/src/Wire/API/User/Password.hs b/libs/wire-api/src/Wire/API/User/Password.hs index e377939406e..33ad254da73 100644 --- a/libs/wire-api/src/Wire/API/User/Password.hs +++ b/libs/wire-api/src/Wire/API/User/Password.hs @@ -51,7 +51,8 @@ import Data.Text.Ascii import Data.Tuple.Extra import Imports import Servant (FromHttpApiData (..)) -import Wire.API.User.Identity +import Wire.API.User.EmailAddress +import Wire.API.User.Phone import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -------------------------------------------------------------------------------- diff --git a/libs/wire-api/src/Wire/API/User/Phone.hs b/libs/wire-api/src/Wire/API/User/Phone.hs new file mode 100644 index 00000000000..603103cc18c --- /dev/null +++ b/libs/wire-api/src/Wire/API/User/Phone.hs @@ -0,0 +1,77 @@ +module Wire.API.User.Phone + ( Phone (..), + parsePhone, + isValidPhone, + ) +where + +import Cassandra qualified as C +import Control.Applicative (optional) +import Control.Lens (over, (?~)) +import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.Attoparsec.Text +import Data.ByteString.Conversion +import Data.OpenApi (ToParamSchema (..)) +import Data.OpenApi qualified as S +import Data.Schema +import Data.Text qualified as Text +import Data.Text.Encoding +import Data.Text.Encoding.Error +import Imports +import SAML2.WebSSO.Test.Arbitrary () +import Servant +import Servant.API qualified as S +import Test.QuickCheck qualified as QC +import Web.Scim.Schema.User.Email () +import Wire.Arbitrary (Arbitrary (arbitrary)) + +-------------------------------------------------------------------------------- +-- Phone + +newtype Phone = Phone {fromPhone :: Text} + deriving stock (Eq, Ord, Show, Generic) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema Phone) + +instance ToParamSchema Phone where + toParamSchema _ = toParamSchema (Proxy @Text) + +instance ToSchema Phone where + schema = + over doc (S.description ?~ "E.164 phone number") $ + fromPhone + .= parsedText "PhoneNumber" (maybe (Left "Invalid phone number. Expected E.164 format.") Right . parsePhone) + +instance ToByteString Phone where + builder = builder . fromPhone + +instance FromByteString Phone where + parser = parser >>= maybe (fail "Invalid phone") pure . parsePhone + +instance S.FromHttpApiData Phone where + parseUrlPiece = maybe (Left "Invalid phone") Right . fromByteString . encodeUtf8 + +instance S.ToHttpApiData Phone where + toUrlPiece = decodeUtf8With lenientDecode . toByteString' + +instance Arbitrary Phone where + arbitrary = + Phone . Text.pack <$> do + let mkdigits n = replicateM n (QC.elements ['0' .. '9']) + mini <- mkdigits 8 + maxi <- mkdigits =<< QC.chooseInt (0, 7) + pure $ '+' : mini <> maxi + +deriving instance C.Cql Phone + +-- | Parses a phone number in E.164 format with a mandatory leading '+'. +parsePhone :: Text -> Maybe Phone +parsePhone p + | isValidPhone p = Just $! Phone p + | otherwise = Nothing + +-- | Checks whether a phone number is valid, i.e. it is in E.164 format +-- with a mandatory leading '+' followed by 10-15 digits. +isValidPhone :: Text -> Bool +isValidPhone = either (const False) (const True) . parseOnly e164 + where + e164 = char '+' *> count 8 digit *> count 7 (optional digit) *> endOfInput diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index 50f0c95b15d..21b82fb61ee 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, (.~), (?~), (^.)) +import Control.Lens (makeLenses, mapped, to, (.~), (?~), (^.)) import Control.Monad.Except (throwError) import Crypto.Hash (hash) import Crypto.Hash.Algorithms (SHA512) @@ -63,11 +63,14 @@ import Data.OpenApi hiding (Operation) import Data.Proxy import Data.Text qualified as T import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import Data.These +import Data.These.Combinators import Data.Time.Clock (UTCTime) import Imports import SAML2.WebSSO qualified as SAML import SAML2.WebSSO.Test.Arbitrary () import Servant.API (FromHttpApiData (..), ToHttpApiData (..)) +import Test.QuickCheck (Gen) import Test.QuickCheck qualified as QC import Web.HttpApiData (parseHeaderWithPrefix) import Web.Scim.AttrName (AttrName (..)) @@ -85,8 +88,7 @@ import Web.Scim.Schema.User qualified as Scim import Web.Scim.Schema.User qualified as Scim.User import Wire.API.Locale import Wire.API.Team.Role (Role) -import Wire.API.User (emailFromSAMLNameID, urefToExternalIdUnsafe) -import Wire.API.User.Identity (EmailAddress, fromEmail) +import Wire.API.User.EmailAddress (EmailAddress, fromEmail) import Wire.API.User.Profile as BT import Wire.API.User.RichInfo qualified as RI import Wire.API.User.Saml () @@ -328,7 +330,7 @@ instance Scim.Patchable ScimUserExtra where -- and/or ignore POSTed content, returning the full representation can be useful to the -- client, enabling it to correlate the client's and server's views of the new resource." data ValidScimUser = ValidScimUser - { externalId :: ValidExternalId, + { externalId :: ValidScimId, handle :: Handle, name :: BT.Name, emails :: [EmailAddress], @@ -339,51 +341,58 @@ data ValidScimUser = ValidScimUser } deriving (Eq, Show) --- | Note that a 'SAML.UserRef' may contain an email. Even though it is possible to construct a 'ValidExternalId' from such a 'UserRef' with 'UrefOnly', --- this does not represent a valid 'ValidExternalId'. So in case of a 'UrefOnly', we can assume that the 'UserRef' does not contain an email. -data ValidExternalId - = EmailAndUref EmailAddress SAML.UserRef - | UrefOnly SAML.UserRef - | EmailOnly EmailAddress +-- | This type carries externalId, plus email address (validated if present, unvalidated if not) and saml credentials, +-- because those are sometimes derived from the externalId field. +data ValidScimId = ValidScimId + { validScimIdExternal :: Text, + validScimIdAuthInfo :: These EmailAddress SAML.UserRef + } deriving (Eq, Show, Generic) -instance Arbitrary ValidExternalId where - arbitrary = do - muref <- QC.arbitrary - case muref of - Just uref -> case emailFromSAMLNameID $ uref ^. SAML.uidSubject of - Just e -> pure $ EmailAndUref e uref - Nothing -> pure $ UrefOnly uref - Nothing -> EmailOnly <$> QC.arbitrary - --- | Take apart a 'ValidExternalId', using 'SAML.UserRef' if available, otherwise 'Email'. -runValidExternalIdEither :: (SAML.UserRef -> a) -> (EmailAddress -> a) -> ValidExternalId -> a -runValidExternalIdEither doUref doEmail = \case - EmailAndUref _ uref -> doUref uref - UrefOnly uref -> doUref uref - EmailOnly em -> doEmail em - --- | Take apart a 'ValidExternalId', use both 'SAML.UserRef', 'Email' if applicable, and +instance Arbitrary ValidScimId where + arbitrary = + these onlyThis (pure . onlyThat) (\_ uref -> pure (onlyThat uref)) =<< QC.arbitrary + where + onlyThis :: EmailAddress -> Gen ValidScimId + onlyThis em = do + extIdNick <- T.pack . QC.getPrintableString <$> QC.arbitrary + extId <- QC.elements [extIdNick, fromEmail em] + pure $ ValidScimId {validScimIdExternal = extId, validScimIdAuthInfo = This em} + + -- `unsafeShowNameID` can name clash, if this is a problem consider using `arbitraryValidScimIdNoNameIDQualifiers` + onlyThat :: SAML.UserRef -> ValidScimId + onlyThat uref = ValidScimId {validScimIdExternal = uref ^. SAML.uidSubject . to SAML.unsafeShowNameID . to CI.original, validScimIdAuthInfo = That uref} + +newtype ValidScimIdNoNameIDQualifiers = ValidScimIdNoNameIDQualifiers ValidScimId + deriving (Eq, Show) + +instance Arbitrary ValidScimIdNoNameIDQualifiers where + arbitrary = ValidScimIdNoNameIDQualifiers <$> arbitraryValidScimIdNoNameIDQualifiers + +arbitraryValidScimIdNoNameIDQualifiers :: QC.Gen ValidScimId +arbitraryValidScimIdNoNameIDQualifiers = do + veid :: ValidScimId <- QC.arbitrary + pure $ ValidScimId veid.validScimIdExternal (veid.validScimIdAuthInfo & mapThere removeQualifiers) + where + removeQualifiers :: SAML.UserRef -> SAML.UserRef + removeQualifiers = + (SAML.uidSubject . SAML.nameIDNameQ .~ Nothing) + . (SAML.uidSubject . SAML.nameIDSPProvidedID .~ Nothing) + . (SAML.uidSubject . SAML.nameIDSPNameQ .~ Nothing) + +-- | Take apart a 'ValidScimId', use both 'SAML.UserRef', 'Email' if applicable, and -- merge the result with a given function. -runValidExternalIdBoth :: (a -> a -> a) -> (SAML.UserRef -> a) -> (EmailAddress -> a) -> ValidExternalId -> a -runValidExternalIdBoth merge doUref doEmail = \case - EmailAndUref eml uref -> doUref uref `merge` doEmail eml - UrefOnly uref -> doUref uref - EmailOnly em -> doEmail em - --- | Returns either the extracted `UnqualifiedNameID` if present and not qualified, or the email address. --- This throws an exception if there are any qualifiers. -runValidExternalIdUnsafe :: ValidExternalId -> Text -runValidExternalIdUnsafe = runValidExternalIdEither urefToExternalIdUnsafe fromEmail - -veidUref :: ValidExternalId -> Maybe SAML.UserRef -veidUref = \case - EmailAndUref _ uref -> Just uref - UrefOnly uref -> Just uref - EmailOnly _ -> Nothing +runValidScimIdBoth :: (a -> a -> a) -> (SAML.UserRef -> a) -> (EmailAddress -> a) -> ValidScimId -> a +runValidScimIdBoth merge doURefl doEmail = these doEmail doURefl (\em uref -> doEmail em `merge` doURefl uref) . validScimIdAuthInfo + +veidUref :: ValidScimId -> Maybe SAML.UserRef +veidUref = justThere . validScimIdAuthInfo + +isSAMLUser :: ValidScimId -> Bool +isSAMLUser = isJust . justThere . validScimIdAuthInfo makeLenses ''ValidScimUser -makeLenses ''ValidExternalId +makeLenses ''ValidScimId ---------------------------------------------------------------------------- -- Request and response types diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 2aaeef8430d..9d2026da817 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -235,6 +235,7 @@ library Wire.API.User.IdentityProvider Wire.API.User.Orphans Wire.API.User.Password + Wire.API.User.Phone Wire.API.User.Profile Wire.API.User.RichInfo Wire.API.User.Saml @@ -325,6 +326,7 @@ library , sop-core , tagged , text >=0.11 + , these , time >=1.4 , tinylog , transitive-anns diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 500510dc9bf..fd471ba62e7 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -557,7 +557,7 @@ listActivatedAccountsH :: Maybe (CommaSeparatedList Handle) -> Maybe (CommaSeparatedList EmailAddress) -> Maybe Bool -> - Handler r [UserAccount] + Handler r [ExtendedUserAccount] listActivatedAccountsH (maybe [] fromCommaSeparatedList -> uids) (maybe [] fromCommaSeparatedList -> handles) @@ -568,7 +568,7 @@ listActivatedAccountsH lift $ do u1 <- listActivatedAccounts (Left uids) includePendingInvitations u2 <- listActivatedAccounts (Right handles) includePendingInvitations - u3 <- (\email -> API.lookupAccountsByIdentity email includePendingInvitations) `mapM` emails + u3 <- (\email -> API.lookupExtendedAccountsByIdentity email includePendingInvitations) `mapM` emails pure $ u1 <> u2 <> join u3 -- FUTUREWORK: this should use UserStore only through UserSubsystem. @@ -576,7 +576,7 @@ listActivatedAccounts :: (Member DeleteQueue r, Member UserStore r) => Either [UserId] [Handle] -> Bool -> - AppT r [UserAccount] + AppT r [ExtendedUserAccount] listActivatedAccounts elh includePendingInvitations = do Log.debug (Log.msg $ "listActivatedAccounts: " <> show (elh, includePendingInvitations)) case elh of @@ -585,20 +585,20 @@ listActivatedAccounts elh includePendingInvitations = do us <- liftSem $ mapM API.lookupHandle hs byIds (catMaybes us) where - byIds :: (Member DeleteQueue r) => [UserId] -> (AppT r) [UserAccount] - byIds uids = wrapClient (API.lookupAccounts uids) >>= filterM accountValid + byIds :: (Member DeleteQueue r) => [UserId] -> (AppT r) [ExtendedUserAccount] + byIds uids = wrapClient (API.lookupExtendedAccounts uids) >>= filterM accountValid - accountValid :: (Member DeleteQueue r) => UserAccount -> (AppT r) Bool - accountValid account = case userIdentity . accountUser $ account of + accountValid :: (Member DeleteQueue r) => ExtendedUserAccount -> (AppT r) Bool + accountValid (account -> acc) = case userIdentity . accountUser $ acc of Nothing -> pure False Just ident -> - case (accountStatus account, includePendingInvitations, emailIdentity ident) of + case (accountStatus acc, includePendingInvitations, emailIdentity ident) of (PendingInvitation, False, _) -> pure False (PendingInvitation, True, Just email) -> do hasInvitation <- isJust <$> wrapClient (lookupInvitationByEmail HideInvitationUrl email) unless hasInvitation $ do -- user invited via scim should expire together with its invitation - liftSem $ API.deleteUserNoVerify (userId . accountUser $ account) + liftSem $ API.deleteUserNoVerify (userId . accountUser $ acc) pure hasInvitation (PendingInvitation, True, Nothing) -> pure True -- cannot happen, user invited via scim always has an email diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index fd9787edcd3..84059ff5435 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -32,8 +32,10 @@ module Brig.API.User changeAccountStatus, changeSingleAccountStatus, Data.lookupAccounts, + Data.lookupExtendedAccounts, Data.lookupAccount, lookupAccountsByIdentity, + lookupExtendedAccountsByIdentity, getLegalHoldStatus, Data.lookupName, Data.lookupUser, @@ -304,10 +306,13 @@ createUser new = do Just existingAccount -> let existingUser = existingAccount.accountUser mbSSOid = - case (teamInvitation, email, existingUser.userManagedBy) of + case (teamInvitation, email, existingUser.userManagedBy, userSSOId existingUser) of -- isJust teamInvitation And ManagedByScim implies that the -- user invitation has been generated by SCIM and there is no IdP - (Just _, Just em, ManagedByScim) -> + (Just _, _, ManagedByScim, ssoId@(Just (UserScimExternalId _))) -> + -- if the existing user has an external ID, we have to use it because it can differ from the email address + ssoId + (Just _, Just em, ManagedByScim, _) -> Just $ UserScimExternalId (fromEmail em) _ -> newUserSSOId new in ( new @@ -488,10 +493,10 @@ createUserInviteViaScim :: ) => NewUserScimInvitation -> ExceptT HttpError (AppT r) UserAccount -createUserInviteViaScim (NewUserScimInvitation tid uid loc name email _) = do +createUserInviteViaScim (NewUserScimInvitation tid uid eid loc name email _) = do let emKey = mkEmailKey email verifyUniquenessAndCheckBlacklist emKey !>> identityErrorToBrigError - account <- lift . wrapClient $ newAccountInviteViaScim uid tid loc name email + account <- lift . wrapClient $ newAccountInviteViaScim uid eid tid loc name email lift . liftSem . Log.debug $ field "user" (toByteString . userId . accountUser $ account) . field "action" (val "User.createUserInviteViaScim") -- add the expiry table entry first! (if brig creates an account, and then crashes before @@ -1121,19 +1126,29 @@ getLegalHoldStatus' user = -- | Find user accounts for a given identity, both activated and those -- currently pending activation. -lookupAccountsByIdentity :: +lookupExtendedAccountsByIdentity :: (Member UserKeyStore r) => EmailAddress -> Bool -> - AppT r [UserAccount] -lookupAccountsByIdentity email includePendingInvitations = do + AppT r [ExtendedUserAccount] +lookupExtendedAccountsByIdentity email includePendingInvitations = do let uk = mkEmailKey email activeUid <- liftSem $ lookupKey uk uidFromKey <- (>>= fst) <$> wrapClient (Data.lookupActivationCode uk) - result <- wrapClient $ Data.lookupAccounts (nub $ catMaybes [activeUid, uidFromKey]) + result <- wrapClient $ Data.lookupExtendedAccounts (nub $ catMaybes [activeUid, uidFromKey]) if includePendingInvitations then pure result - else pure $ filter ((/= PendingInvitation) . accountStatus) result + else pure $ filter ((/= PendingInvitation) . accountStatus . account) result + +-- | Find user accounts for a given identity, both activated and those +-- currently pending activation. +lookupAccountsByIdentity :: + (Member UserKeyStore r) => + EmailAddress -> + Bool -> + AppT r [UserAccount] +lookupAccountsByIdentity email includePendingInvitations = + account <$$> lookupExtendedAccountsByIdentity email includePendingInvitations isBlacklisted :: (Member BlockListStore r) => EmailAddress -> AppT r Bool isBlacklisted email = do diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index c128c294863..14120bcd932 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -34,6 +34,7 @@ module Brig.Data.User -- * Lookups lookupAccount, lookupAccounts, + lookupExtendedAccounts, lookupUser, lookupUsers, lookupName, @@ -150,8 +151,8 @@ newAccount u inv tid mbHandle = do prots = fromMaybe defSupportedProtocols (newUserSupportedProtocols u) user uid domain l e = User (Qualified uid domain) ident name Nothing pict assets colour False l Nothing mbHandle e tid managedBy prots -newAccountInviteViaScim :: (MonadReader Env m) => UserId -> TeamId -> Maybe Locale -> Name -> EmailAddress -> m UserAccount -newAccountInviteViaScim uid tid locale name email = do +newAccountInviteViaScim :: (MonadReader Env m) => UserId -> Text -> TeamId -> Maybe Locale -> Name -> EmailAddress -> m UserAccount +newAccountInviteViaScim uid externalId tid locale name email = do defLoc <- setDefaultUserLocale <$> view settings let loc = fromMaybe defLoc locale domain <- viewFederationDomain @@ -160,7 +161,7 @@ newAccountInviteViaScim uid tid locale name email = do user domain loc = User (Qualified uid domain) - (Just $ EmailIdentity email) + (Just $ SSOIdentity (UserScimExternalId externalId) (Just email)) name Nothing (Pict []) @@ -394,10 +395,13 @@ lookupAccount :: (MonadClient m, MonadReader Env m) => UserId -> m (Maybe UserAc lookupAccount u = listToMaybe <$> lookupAccounts [u] lookupAccounts :: (MonadClient m, MonadReader Env m) => [UserId] -> m [UserAccount] -lookupAccounts usrs = do +lookupAccounts usrs = account <$$> lookupExtendedAccounts usrs + +lookupExtendedAccounts :: (MonadClient m, MonadReader Env m) => [UserId] -> m [ExtendedUserAccount] +lookupExtendedAccounts usrs = do loc <- setDefaultUserLocale <$> view settings domain <- viewFederationDomain - fmap (toUserAccount domain loc) <$> retry x1 (query accountsSelect (params LocalQuorum (Identity usrs))) + fmap (toExtendedUserAccount domain loc) <$> retry x1 (query accountsSelect (params LocalQuorum (Identity usrs))) lookupServiceUser :: (MonadClient m) => ProviderId -> ServiceId -> BotId -> m (Maybe (ConvId, Maybe TeamId)) lookupServiceUser pid sid bid = retry x1 (query1 cql (params LocalQuorum (pid, sid, bid))) @@ -454,6 +458,7 @@ type UserRow = Maybe TextStatus, Maybe Pict, Maybe EmailAddress, + Maybe EmailAddress, Maybe UserSSOId, ColourId, Maybe [Asset], @@ -500,7 +505,7 @@ type AccountRow = UserRow usersSelect :: PrepQuery R (Identity [UserId]) UserRow usersSelect = - "SELECT id, name, text_status, picture, email, sso_id, accent_id, assets, \ + "SELECT id, name, text_status, picture, email, email_unvalidated, sso_id, accent_id, assets, \ \activated, status, expires, language, country, provider, service, \ \handle, team, managed_by, supported_protocols \ \FROM user where id IN ?" @@ -525,7 +530,7 @@ teamSelect = "SELECT team FROM user WHERE id = ?" accountsSelect :: PrepQuery R (Identity [UserId]) AccountRow accountsSelect = - "SELECT id, name, text_status, picture, email, sso_id, accent_id, assets, \ + "SELECT id, name, text_status, picture, email, email_unvalidated, sso_id, accent_id, assets, \ \activated, status, expires, language, country, provider, \ \service, handle, team, managed_by, supported_protocols \ \FROM user WHERE id IN ?" @@ -571,8 +576,8 @@ userRichInfoUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE r -- Conversions -- | Construct a 'UserAccount' from a raw user record in the database. -toUserAccount :: Domain -> Locale -> AccountRow -> UserAccount -toUserAccount +toExtendedUserAccount :: Domain -> Locale -> AccountRow -> ExtendedUserAccount +toExtendedUserAccount domain defaultLocale ( uid, @@ -580,6 +585,7 @@ toUserAccount textStatus, pict, email, + emailUnvalidated, ssoid, accent, assets, @@ -600,25 +606,27 @@ toUserAccount expiration = if status == Just Ephemeral then expires else Nothing loc = toLocale defaultLocale (lan, con) svc = newServiceRef <$> sid <*> pid - in UserAccount - ( User - (Qualified uid domain) - ident - name - textStatus - (fromMaybe noPict pict) - (fromMaybe [] assets) - accent - deleted - loc - svc - handle - expiration - tid - (fromMaybe ManagedByWire managed_by) - (fromMaybe defSupportedProtocols prots) - ) - (fromMaybe Active status) + account = + UserAccount + ( User + (Qualified uid domain) + ident + name + textStatus + (fromMaybe noPict pict) + (fromMaybe [] assets) + accent + deleted + loc + svc + handle + expiration + tid + (fromMaybe ManagedByWire managed_by) + (fromMaybe defSupportedProtocols prots) + ) + (fromMaybe Active status) + in ExtendedUserAccount account emailUnvalidated toUsers :: Domain -> Locale -> HavePendingInvitations -> [UserRow] -> [User] toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp @@ -633,6 +641,7 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp _textStatus, _pict, _email, + _, _ssoid, _accent, _assets, @@ -657,6 +666,7 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp textStatus, pict, email, + _, ssoid, accent, assets, diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index c208ceb34cd..a6ee0283375 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -166,7 +166,7 @@ createInvitationViaScim :: TeamId -> NewUserScimInvitation -> (Handler r) UserAccount -createInvitationViaScim tid newUser@(NewUserScimInvitation _tid uid loc name email role) = do +createInvitationViaScim tid newUser@(NewUserScimInvitation _tid uid _eid loc name email role) = do env <- ask let inviteeRole = role fromEmail = env ^. emailSender diff --git a/services/spar/default.nix b/services/spar/default.nix index 1a3549b3b90..4115e8cb670 100644 --- a/services/spar/default.nix +++ b/services/spar/default.nix @@ -63,6 +63,7 @@ , tasty-hunit , text , text-latin1 +, these , time , tinylog , transformers @@ -124,6 +125,7 @@ mkDerivation { servant-server text text-latin1 + these time tinylog transformers @@ -187,6 +189,7 @@ mkDerivation { string-conversions tasty-hunit text + these time tinylog transformers @@ -227,6 +230,7 @@ mkDerivation { servant servant-openapi3 string-conversions + these time tinylog types-common diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 3ef0220f044..5c7ba1d5247 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -185,6 +185,7 @@ library , servant-server , text , text-latin1 + , these , time , tinylog , transformers @@ -264,8 +265,8 @@ executable spar executable spar-integration main-is: Main.hs - -- we should not use cabal-fmt expand here because `Main` should not be in `other-modules`, it's wrong - -- and cabal chokes on it + -- we should not use cabal-fmt expand here because `Main` should not be in `other-modules`, it's wrong + -- and cabal chokes on it -- FUTUREWORK(mangoiv): move Main to a different directory such that this one can be expanded other-modules: Test.LoggingSpec @@ -383,6 +384,7 @@ executable spar-integration , string-conversions , tasty-hunit , text + , these , time , tinylog , transformers @@ -630,6 +632,7 @@ test-suite spec , servant-openapi3 , spar , string-conversions + , these , time , tinylog , types-common diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 569310e0a21..c1c307e341c 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -433,7 +433,7 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co assertEmptyOrPurge teamId page = do forM_ (Cas.result page) $ \(uref, uid) -> do mAccount <- BrigAccess.getAccount NoPendingInvitations uid - let mUserTeam = userTeam . accountUser =<< mAccount + let mUserTeam = userTeam . accountUser . account =<< mAccount when (mUserTeam == Just teamId) $ do if purge then do diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index f0bb0c79e2a..6bd4d9ea1b1 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -53,6 +53,7 @@ import qualified Data.Text.Encoding as Text import Data.Text.Encoding.Error import qualified Data.Text.Lazy as LText import qualified Data.Text.Lazy.Encoding as LText +import Data.These import Imports hiding (MonadReader, asks, log) import qualified Network.HTTP.Types.Status as Http import qualified Network.Wai.Utilities.Error as Wai @@ -97,7 +98,6 @@ import Wire.API.Team.Role (Role, defaultRole) import Wire.API.User import Wire.API.User.IdentityProvider import Wire.API.User.Saml -import Wire.API.User.Scim (ValidExternalId (..)) import Wire.Sem.Logger (Logger) import qualified Wire.Sem.Logger as Logger import Wire.Sem.Random (Random) @@ -149,10 +149,10 @@ getUserIdByScimExternalId :: Member ScimExternalIdStore r ) => TeamId -> - EmailAddress -> + Text -> Sem r (Maybe UserId) -getUserIdByScimExternalId tid email = do - muid <- ScimExternalIdStore.lookup tid email +getUserIdByScimExternalId tid eid = do + muid <- ScimExternalIdStore.lookup tid eid case muid of Nothing -> pure Nothing Just uid -> do @@ -189,7 +189,7 @@ createSamlUserWithId :: createSamlUserWithId teamid buid suid role = do uname <- either (throwSparSem . SparBadUserName . LText.pack) pure $ - Intra.mkUserName Nothing (UrefOnly suid) + Intra.mkUserName Nothing (That suid) buid' <- BrigAccess.createSAML suid buid teamid uname ManagedByWire Nothing Nothing Nothing role assert (buid == buid') $ pure () SAMLUserStore.insert suid buid @@ -390,7 +390,7 @@ moveUserToNewIssuer :: Sem r () moveUserToNewIssuer oldUserRef newUserRef uid = do SAMLUserStore.insert newUserRef uid - BrigAccess.setVeid uid (UrefOnly newUserRef) + BrigAccess.setSSOId uid (UserSSOId newUserRef) SAMLUserStore.delete uid oldUserRef verdictHandlerResultCore :: diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index 9f1b8628cce..c11d4dd03f0 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -28,7 +28,7 @@ module Spar.Intra.Brig setBrigUserName, setBrigUserHandle, setBrigUserManagedBy, - setBrigUserVeid, + setBrigUserSSOId, setBrigUserRichInfo, setBrigUserLocale, checkHandleAvailable, @@ -68,14 +68,9 @@ import Wire.API.User import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso import Wire.API.User.RichInfo as RichInfo -import Wire.API.User.Scim (ValidExternalId (..), runValidExternalIdEither) ---------------------------------------------------------------------- --- | FUTUREWORK: this is redundantly defined in "Spar.Intra.BrigApp". -veidToUserSSOId :: ValidExternalId -> UserSSOId -veidToUserSSOId = runValidExternalIdEither UserSSOId (UserScimExternalId . fromEmail) - -- | Similar to 'Network.Wire.Client.API.Auth.tokenResponse', but easier: we just need to set the -- cookie in the response, and the redirect will make the client negotiate a fresh auth token. -- (This is the easiest way, since the login-request that we are in the middle of responding to here @@ -130,6 +125,7 @@ createBrigUserSAML uref (Id buid) teamid name managedBy handle richInfo mLocale createBrigUserNoSAML :: (HasCallStack, MonadSparToBrig m) => + Text -> EmailAddress -> UserId -> TeamId -> @@ -138,8 +134,8 @@ createBrigUserNoSAML :: Maybe Locale -> Role -> m UserId -createBrigUserNoSAML email uid teamid uname locale role = do - let newUser = NewUserScimInvitation teamid uid locale uname email role +createBrigUserNoSAML extId email uid teamid uname locale role = do + let newUser = NewUserScimInvitation teamid uid extId locale uname email role resp :: ResponseLBS <- call $ method POST @@ -165,7 +161,7 @@ updateEmail buid email = do _ -> rethrow "brig" resp -- | Get a user; returns 'Nothing' if the user was not found or has been deleted. -getBrigUserAccount :: (HasCallStack, MonadSparToBrig m) => HavePendingInvitations -> UserId -> m (Maybe UserAccount) +getBrigUserAccount :: (HasCallStack, MonadSparToBrig m) => HavePendingInvitations -> UserId -> m (Maybe ExtendedUserAccount) getBrigUserAccount havePending buid = do resp :: ResponseLBS <- call $ @@ -183,10 +179,10 @@ getBrigUserAccount havePending buid = do case statusCode resp of 200 -> - parseResponse @[UserAccount] "brig" resp >>= \case + parseResponse @[ExtendedUserAccount] "brig" resp >>= \case [account] -> pure $ - if userDeleted $ accountUser account + if userDeleted account.account.accountUser then Nothing else Just account _ -> pure Nothing @@ -273,13 +269,13 @@ setBrigUserManagedBy buid managedBy = do rethrow "brig" resp -- | Set user's UserSSOId. -setBrigUserVeid :: (HasCallStack, MonadSparToBrig m) => UserId -> ValidExternalId -> m () -setBrigUserVeid buid veid = do +setBrigUserSSOId :: (HasCallStack, MonadSparToBrig m) => UserId -> UserSSOId -> m () +setBrigUserSSOId buid ssoId = do resp <- call $ method PUT . paths ["i", "users", toByteString' buid, "sso-id"] - . json (veidToUserSSOId veid) + . json ssoId case statusCode resp of 200 -> pure () _ -> rethrow "brig" resp diff --git a/services/spar/src/Spar/Intra/BrigApp.hs b/services/spar/src/Spar/Intra/BrigApp.hs index da6ee3e01f8..83c377ff6fb 100644 --- a/services/spar/src/Spar/Intra/BrigApp.hs +++ b/services/spar/src/Spar/Intra/BrigApp.hs @@ -27,7 +27,6 @@ module Spar.Intra.BrigApp veidFromBrigUser, veidFromUserSSOId, mkUserName, - renderValidExternalId, HavePendingInvitations (..), getBrigUser, getBrigUserTeam, @@ -53,6 +52,8 @@ import Data.Handle (Handle, parseHandle) import Data.Id (TeamId, UserId) import Data.Text.Encoding import Data.Text.Encoding.Error +import Data.These +import Data.These.Combinators import Imports import Polysemy import Polysemy.Error @@ -64,58 +65,69 @@ import Spar.Sem.GalleyAccess (GalleyAccess) import qualified Spar.Sem.GalleyAccess as GalleyAccess import Wire.API.Team.Member (HiddenPerm (CreateReadDeleteScimToken), IsPerm) import Wire.API.User -import Wire.API.User.Scim (ValidExternalId (..), runValidExternalIdEither) +import Wire.API.User.Scim (ValidScimId (..)) ---------------------------------------------------------------------- --- | FUTUREWORK: this is redundantly defined in "Spar.Intra.Brig" -veidToUserSSOId :: ValidExternalId -> UserSSOId -veidToUserSSOId = runValidExternalIdEither UserSSOId (UserScimExternalId . fromEmail) - -veidFromUserSSOId :: (MonadError String m) => UserSSOId -> m ValidExternalId -veidFromUserSSOId = \case - UserSSOId uref -> - case urefToEmail uref of - Nothing -> pure $ UrefOnly uref - Just email -> pure $ EmailAndUref email uref - -- FUTUREWORK(elland): account for SCIM emails fields? - UserScimExternalId email -> - maybe - (throwError "externalId not an email and no issuer") - (pure . EmailOnly) - (emailAddressText email) - --- | If the brig user has a 'UserSSOId', transform that into a 'ValidExternalId' (this is a +veidToUserSSOId :: ValidScimId -> UserSSOId +veidToUserSSOId (ValidScimId eid authInfo) = maybe (UserScimExternalId eid) UserSSOId (justThere authInfo) + +veidFromUserSSOId :: + (MonadError String m) => + UserSSOId -> + -- | this is either the unvalidated email if exists, or otherwise the validated email. + Maybe EmailAddress -> + m ValidScimId +veidFromUserSSOId ssoId mEmail = case ssoId of + UserSSOId uref -> do + let eid = CI.original $ uref ^. SAML.uidSubject . to SAML.unsafeShowNameID + pure $ case mEmail of + Just email -> ValidScimId eid (These email uref) + Nothing -> ValidScimId eid (That uref) + UserScimExternalId veid -> do + case mEmail of + Just email -> + pure $ ValidScimId veid (This email) + Nothing -> + -- If veid can be parsed as an email, we end up in the case above with email delivered separately. + throwError "internal error: externalId is not an email and there is no SAML issuer" + +-- | If the brig user has a 'UserSSOId', transform that into a 'ValidScimId' (this is a -- total function as long as brig obeys the api). Otherwise, if the user has an email, we can -- construct a return value from that (and an optional saml issuer). -- -- Note: the saml issuer is only needed in the case where a user has been invited via team -- settings and is now onboarded to saml/scim. If this case can safely be ruled out, it's ok -- to just set it to 'Nothing'. -veidFromBrigUser :: (MonadError String m) => User -> Maybe SAML.Issuer -> m ValidExternalId -veidFromBrigUser usr mIssuer = case (userSSOId usr, userEmail usr, mIssuer) of - (Just ssoid, _, _) -> veidFromUserSSOId ssoid - (Nothing, Just email, Just issuer) -> pure $ EmailAndUref email (SAML.UserRef issuer (fromRight' $ emailToSAMLNameID email)) - (Nothing, Just email, Nothing) -> pure $ EmailOnly email +-- +-- `userSSOId usr` can be empty if the user has no SAML credentials and is brought under scim +-- management for the first time. In that case, the externalId is taken to +-- be the email address. +veidFromBrigUser :: (MonadError String m) => User -> Maybe SAML.Issuer -> Maybe EmailAddress -> m ValidScimId +veidFromBrigUser usr mIssuer mUnvalidatedEmail = case (userSSOId usr, userEmail usr, mIssuer) of + (Just ssoid, mValidatedEmail, _) -> do + -- `mEmail` is in synch with SCIM user schema. + let mEmail = mUnvalidatedEmail <|> mValidatedEmail + veidFromUserSSOId ssoid mEmail + (Nothing, Just email, Just issuer) -> pure $ ValidScimId (fromEmail email) (These email (SAML.UserRef issuer (fromRight' $ emailToSAMLNameID email))) + (Nothing, Just email, Nothing) -> pure $ ValidScimId (fromEmail email) (This email) (Nothing, Nothing, _) -> throwError "user has neither ssoIdentity nor userEmail" -- | Take a maybe text, construct a 'Name' from what we have in a scim user. If the text -- isn't present, use an email address or a saml subject (usually also an email address). If -- both are 'Nothing', fail. -mkUserName :: Maybe Text -> ValidExternalId -> Either String Name +mkUserName :: Maybe Text -> These EmailAddress SAML.UserRef -> Either String Name mkUserName (Just n) = const $ mkName n mkUserName Nothing = - runValidExternalIdEither - (\uref -> mkName (CI.original . SAML.unsafeShowNameID $ uref ^. SAML.uidSubject)) + these (mkName . fromEmail) - -renderValidExternalId :: ValidExternalId -> Maybe Text -renderValidExternalId = runValidExternalIdEither urefToExternalId (Just . fromEmail) + (\uref -> mkName (CI.original . SAML.unsafeShowNameID $ uref ^. SAML.uidSubject)) + (\_ uref -> mkName (CI.original . SAML.unsafeShowNameID $ uref ^. SAML.uidSubject)) ---------------------------------------------------------------------- getBrigUser :: (HasCallStack, Member BrigAccess r) => HavePendingInvitations -> UserId -> Sem r (Maybe User) -getBrigUser ifpend = (accountUser <$$>) . BrigAccess.getAccount ifpend +getBrigUser ifpend = ((accountUser . account) <$$>) . BrigAccess.getAccount ifpend -- | Check that an id maps to an user on brig that is 'Active' (or optionally -- 'PendingInvitation') and has a team id. diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index e46aa690482..b7985a3c3cf 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -38,8 +38,8 @@ module Spar.Scim.User ( validateScimUser', synthesizeScimUser, toScimStoredUser, - mkValidExternalId, - scimFindUserByEmail, + mkValidScimId, + scimFindUserByExternalId, deleteScimUser, ) where @@ -63,6 +63,8 @@ import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import Data.Text.Encoding.Error import qualified Data.Text.Lazy as LText +import Data.These +import Data.These.Combinators import qualified Data.UUID as UUID import Imports import Network.URI (URI, parseURI) @@ -104,13 +106,13 @@ import qualified Web.Scim.Schema.Meta as Scim import qualified Web.Scim.Schema.ResourceType as Scim import qualified Web.Scim.Schema.User as Scim import qualified Web.Scim.Schema.User as Scim.User (schemas) --- import qualified Web.Scim.Schema.User.Email as Scim.Email +import qualified Web.Scim.Schema.User.Email as Scim.Email import qualified Wire.API.Team.Member as Member import Wire.API.Team.Role import Wire.API.User import Wire.API.User.IdentityProvider (IdP) import qualified Wire.API.User.RichInfo as RI -import Wire.API.User.Scim (ScimTokenInfo (..)) +import Wire.API.User.Scim (ScimTokenInfo (..), ValidScimId (..)) import qualified Wire.API.User.Scim as ST import Wire.Sem.Logger (Logger) import qualified Wire.Sem.Logger as Logger @@ -157,7 +159,7 @@ instance | Scim.isUserSchema schema -> do x <- runMaybeT $ case attrName of "username" -> scimFindUserByHandle mIdpConfig stiTeam val - "externalid" -> scimFindUserByEmail mIdpConfig stiTeam val + "externalid" -> scimFindUserByExternalId mIdpConfig stiTeam val _ -> throwError (Scim.badRequest Scim.InvalidFilter (Just "Unsupported attribute")) pure $ Scim.fromList (toList x) | otherwise -> throwError $ Scim.badRequest Scim.InvalidFilter (Just "Unsupported schema") @@ -277,7 +279,7 @@ validateScimUser' :: Sem r ST.ValidScimUser validateScimUser' errloc midp richInfoLimit user = do unless (isNothing $ Scim.password user) $ throw $ badRequest "Setting user passwords is not supported for security reasons." - veid <- mkValidExternalId midp (Scim.externalId user) + veid <- mkValidScimId midp (Scim.externalId user) (Scim.Email.scimEmailsToEmailAddress $ Scim.emails user) handl <- validateHandle . Text.toLower . Scim.userName $ user -- FUTUREWORK: 'Scim.userName' should be case insensitive; then the toLower here would -- be a little less brittle. @@ -290,14 +292,13 @@ validateScimUser' errloc midp richInfoLimit user = do <> " (" <> errloc <> ")" - either err pure $ Brig.mkUserName (Scim.displayName user) veid + either err pure $ Brig.mkUserName (Scim.displayName user) (ST.validScimIdAuthInfo veid) richInfo <- validateRichInfo (Scim.extra user ^. ST.sueRichInfo) let active = Scim.active user lang <- maybe (throw $ badRequest "Could not parse language. Expected format is ISO 639-1.") pure $ mapM parseLanguage $ Scim.preferredLanguage user mRole <- validateRole user - -- FUTUREWORK(elland): Handle the SCIM emails field. - pure $ ST.ValidScimUser veid handl uname [] richInfo (maybe True Scim.unScimBool active) (flip Locale Nothing <$> lang) mRole + pure $ ST.ValidScimUser veid handl uname (maybeToList (justHere veid.validScimIdAuthInfo)) richInfo (maybe True Scim.unScimBool active) (flip Locale Nothing <$> lang) mRole where validRoleNames :: Text validRoleNames = @@ -347,11 +348,11 @@ validateScimUser' errloc midp richInfoLimit user = do } pure richInfo --- | Given an 'externalId' and an 'IdP', construct a 'ST.ValidExternalId'. +-- | Given an 'externalId' and an 'IdP', construct a 'ST.ValidScimId'. -- -- This is needed primarily in 'validateScimUser', but also in 'updateValidScimUser' to -- recover the 'SAML.UserRef' of the scim user before the update from the database. -mkValidExternalId :: +mkValidScimId :: forall r. ( Member BrigAccess r, Member SAMLUserStore r, @@ -359,19 +360,22 @@ mkValidExternalId :: ) => Maybe IdP -> Maybe Text -> - Sem r ST.ValidExternalId -mkValidExternalId _ Nothing = + Maybe EmailAddress -> + Sem r ST.ValidScimId +mkValidScimId _ Nothing _ = throw $ Scim.badRequest Scim.InvalidValue (Just "externalId is required") -mkValidExternalId Nothing (Just extid) = do +mkValidScimId Nothing (Just extid) (Just email) = do + pure $ ST.ValidScimId extid (This email) +mkValidScimId Nothing (Just extid) Nothing = do let err = Scim.badRequest Scim.InvalidValue (Just "externalId must be a valid email address or (if there is a SAML IdP) a valid SAML NameID") - maybe (throw err) (pure . ST.EmailOnly) $ emailAddressText extid -mkValidExternalId (Just idp) (Just extid) = do + maybe (throw err) (pure . ST.ValidScimId extid . This) $ emailAddressText extid +mkValidScimId (Just idp) (Just extid) mEmail = do let issuer = idp ^. SAML.idpMetadata . SAML.edIssuer subject <- validateSubject extid let uref = SAML.UserRef issuer subject @@ -388,9 +392,10 @@ mkValidExternalId (Just idp) (Just extid) = do -- The entry in spar.user_v2 does not exist yet during user -- creation. So we just assume that it will exist momentarily. pure uref - pure $ case emailAddressText extid of - Just email -> ST.EmailAndUref email indexedUref - Nothing -> ST.UrefOnly indexedUref + pure . ST.ValidScimId extid $ case (mEmail, emailAddressText extid) of + (Just email, _) -> These email indexedUref + (Nothing, Just email) -> These email indexedUref + (Nothing, Nothing) -> That indexedUref where -- Validate a subject ID (@externalId@). validateSubject :: Text -> Sem r SAML.NameID @@ -437,7 +442,7 @@ logEmail email = logVSU :: ST.ValidScimUser -> (Msg -> Msg) logVSU (ST.ValidScimUser veid handl _name _emails _richInfo _active _lang _role) = -- FUTUREWORK(elland): Take SCIM emails field into account. - maybe id logEmail (veidToEmail veid) + maybe id logEmail (justHere $ ST.validScimIdAuthInfo veid) . logHandle handl logTokenInfo :: ScimTokenInfo -> (Msg -> Msg) @@ -449,14 +454,8 @@ logScimUserId = logUser . Scim.id . Scim.thing logScimUserIds :: Scim.ListResponse (Scim.StoredUser ST.SparTag) -> (Msg -> Msg) logScimUserIds lresp = foldl' (.) id (logScimUserId <$> Scim.resources lresp) -veidToEmail :: ST.ValidExternalId -> Maybe EmailAddress -veidToEmail (ST.EmailAndUref email _) = Just email -veidToEmail (ST.UrefOnly _) = Nothing -veidToEmail (ST.EmailOnly email) = Just email - --- FUTUREWORK(elland): Account for SCIM emails field, if relevant here. vsUserEmail :: ST.ValidScimUser -> Maybe EmailAddress -vsUserEmail usr = veidToEmail usr.externalId +vsUserEmail usr = justHere $ ST.validScimIdAuthInfo usr.externalId -- in ScimTokenHash (cs @ByteString @Text (convertToBase Base64 digest)) @@ -532,18 +531,15 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser {..} -- Generate a UserId will be used both for scim user in spar and for brig. lift $ do - ST.runValidExternalIdEither - ( \uref -> + let doUref uref = do -- FUTUREWORK: outsource this and some other fragments from -- `createValidScimUser` into a function `createValidScimUserBrig` similar -- to `createValidScimUserSpar`? void $ BrigAccess.createSAML uref buid stiTeam name ManagedByScim (Just handle) (Just richInfo) locale (fromMaybe defaultRole role) - ) - ( \email -> do - void $ BrigAccess.createNoSAML email buid stiTeam name locale (fromMaybe defaultRole role) + doEmail email = do + void $ BrigAccess.createNoSAML externalId.validScimIdExternal email buid stiTeam name locale (fromMaybe defaultRole role) BrigAccess.setHandle buid handle -- FUTUREWORK: possibly do the same one req as we do for saml? - ) - externalId + these doEmail doUref (\_ uref -> doUref uref) (validScimIdAuthInfo externalId) Logger.debug ("createValidScimUser: brig says " <> show buid) BrigAccess.setRichInfo buid richInfo @@ -606,18 +602,12 @@ createValidScimUserSpar :: TeamId -> UserId -> Scim.StoredUser ST.SparTag -> - ST.ValidExternalId -> + ST.ValidScimId -> m () createValidScimUserSpar stiTeam uid storedUser veid = lift $ do ScimUserTimesStore.write storedUser - -- This uses the "both" variant to always write all applicable index tables, even if - -- `spar.scim_external` is never consulted as long as there is an IdP. This is hoped to - -- mitigate logic errors in this code and corner cases. (eg., if the IdP is later removed?) - ST.runValidExternalIdBoth - (>>) - (`SAMLUserStore.insert` uid) - (\email -> ScimExternalIdStore.insert stiTeam email uid) - veid + ScimExternalIdStore.insert stiTeam veid.validScimIdExternal uid + for_ (justThere veid.validScimIdAuthInfo) (`SAMLUserStore.insert` uid) -- TODO(arianvp): how do we get this safe w.r.t. race conditions / crashes? updateValidScimUser :: @@ -706,19 +696,21 @@ updateVsuUref :: ) => TeamId -> UserId -> - ST.ValidExternalId -> - ST.ValidExternalId -> + ST.ValidScimId -> + ST.ValidScimId -> Sem r () updateVsuUref team uid old new = do - -- FUTUREWORK(elland): Account for SCIM emails field. - case (veidToEmail old, veidToEmail new) of + case (justHere $ ST.validScimIdAuthInfo old, justHere $ ST.validScimIdAuthInfo new) of (mo, mn@(Just email)) | mo /= mn -> Spar.App.validateEmail (Just team) uid email _ -> pure () - old & ST.runValidExternalIdBoth (>>) (SAMLUserStore.delete uid) (ScimExternalIdStore.delete team) - new & ST.runValidExternalIdBoth (>>) (`SAMLUserStore.insert` uid) (\email -> ScimExternalIdStore.insert team email uid) + ScimExternalIdStore.delete team old.validScimIdExternal + for_ (justThere old.validScimIdAuthInfo) (SAMLUserStore.delete uid) + + ScimExternalIdStore.insert team new.validScimIdExternal uid + for_ (justThere new.validScimIdAuthInfo) (`SAMLUserStore.insert` uid) - BrigAccess.setVeid uid new + BrigAccess.setSSOId uid $ veidToUserSSOId new toScimStoredUser :: (HasCallStack) => @@ -797,8 +789,8 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = -- ("tombstones") would not have the needed values (`userIdentity = -- Nothing`) to delete a user in spar. I.e. `SAML.UserRef` and `Email` -- cannot be figured out when a `User` has status `Deleted`. - mbBrigUser <- lift $ Brig.getBrigUser WithPendingInvitations uid - case mbBrigUser of + mbAccount <- lift $ BrigAccess.getAccount WithPendingInvitations uid + case mbAccount of Nothing -> -- Ensure there's no left-over of this user in brig. This is safe -- because the user has either been deleted (tombstone) or does not @@ -807,7 +799,7 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = -- thing that could happen is that foreign users cleanup partially -- deleted users. void . lift $ BrigAccess.deleteUser uid - Just brigUser -> do + Just acc@(accountUser . account -> brigUser) -> do if userTeam brigUser == Just stiTeam then do -- This deletion needs data from the non-deleted User in brig. So, @@ -816,7 +808,7 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = -- that have been deleted in brig. Deleting scim-managed users in brig -- (via the TM app) is blocked, though, so there is no legal way to enter -- that situation. - deleteUserInSpar brigUser + deleteUserInSpar acc void . lift $ BrigAccess.deleteUser uid else do -- if we find the user in another team, we pretend it wasn't even there, to @@ -829,20 +821,16 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = Member ScimExternalIdStore r, Member ScimUserTimesStore r ) => - User -> + ExtendedUserAccount -> Scim.ScimHandler (Sem r) () - deleteUserInSpar brigUser = do + deleteUserInSpar account = do mIdpConfig <- mapM (lift . IdPConfigStore.getConfig) stiIdP - case Brig.veidFromBrigUser brigUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) of + case Brig.veidFromBrigUser account.account.accountUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) account.emailUnvalidated of Left _ -> pure () - Right veid -> - lift $ - ST.runValidExternalIdBoth - (>>) - (SAMLUserStore.delete uid) - (ScimExternalIdStore.delete stiTeam) - veid + Right veid -> lift $ do + for_ (justThere veid.validScimIdAuthInfo) (SAMLUserStore.delete uid) + ScimExternalIdStore.delete stiTeam veid.validScimIdExternal lift $ ScimUserTimesStore.delete uid ---------------------------------------------------------------------------- @@ -878,7 +866,7 @@ assertExternalIdUnused :: Member SAMLUserStore r ) => TeamId -> - ST.ValidExternalId -> + ST.ValidScimId -> Scim.ScimHandler (Sem r) () assertExternalIdUnused = assertExternalIdInAllowedValues @@ -895,7 +883,7 @@ assertExternalIdNotUsedElsewhere :: Member SAMLUserStore r ) => TeamId -> - ST.ValidExternalId -> + ST.ValidScimId -> UserId -> Scim.ScimHandler (Sem r) () assertExternalIdNotUsedElsewhere tid veid wireUserId = @@ -913,16 +901,14 @@ assertExternalIdInAllowedValues :: [Maybe UserId] -> Text -> TeamId -> - ST.ValidExternalId -> + ST.ValidScimId -> Scim.ScimHandler (Sem r) () assertExternalIdInAllowedValues allowedValues errmsg tid veid = do isGood <- - lift $ - ST.runValidExternalIdBoth - (\ma mb -> (&&) <$> ma <*> mb) - (fmap ((`elem` allowedValues) . fmap userId) . getUserByUrefUnsafe) - (fmap (`elem` allowedValues) . getUserIdByScimExternalId tid) - veid + lift $ do + mViaEid <- getUserIdByScimExternalId tid veid.validScimIdExternal + mViaUref <- join <$> (for (justThere veid.validScimIdAuthInfo) ((userId <$$>) . getUserByUrefUnsafe)) + pure $ all (`elem` allowedValues) [mViaEid, mViaUref] unless isGood $ throwError Scim.conflict {Scim.detail = Just errmsg} @@ -953,21 +939,21 @@ synthesizeStoredUser :: Member GalleyAccess r, Member ScimUserTimesStore r ) => - UserAccount -> - ST.ValidExternalId -> + ExtendedUserAccount -> + ST.ValidScimId -> Scim.ScimHandler (Sem r) (Scim.StoredUser ST.SparTag) -synthesizeStoredUser usr veid = +synthesizeStoredUser acc veid = logScim ( logFunction "Spar.Scim.User.synthesizeStoredUser" - . logUser (userId . accountUser $ usr) - . maybe id logHandle (userHandle . accountUser $ usr) - . maybe id logTeam (userTeam . accountUser $ usr) - . maybe id logEmail (veidToEmail veid) + . logUser (userId acc.account.accountUser) + . maybe id logHandle acc.account.accountUser.userHandle + . maybe id logTeam acc.account.accountUser.userTeam + . maybe id logEmail (justHere $ ST.validScimIdAuthInfo veid) ) logScimUserId $ do - let uid = userId (accountUser usr) - accStatus = accountStatus usr + let uid = userId acc.account.accountUser + accStatus = acc.account.accountStatus let readState :: Sem r (RI.RichInfo, Maybe (UTCTimeMillis, UTCTimeMillis), URIBS.URI, Role) readState = @@ -991,14 +977,17 @@ synthesizeStoredUser usr veid = now <- toUTCTimeMillis <$> lift Now.get let (createdAt, lastUpdatedAt) = fromMaybe (now, now) accessTimes - handle <- lift $ Brig.giveDefaultHandle (accountUser usr) - let emails = catMaybesToList (emailIdentity <$> usr.accountUser.userIdentity) + handle <- lift $ Brig.giveDefaultHandle acc.account.accountUser + + let emails = + maybeToList $ + acc.emailUnvalidated <|> (emailIdentity =<< userIdentity acc.account.accountUser) <|> justHere veid.validScimIdAuthInfo storedUser <- synthesizeStoredUser' uid veid - (userDisplayName (accountUser usr)) + (userDisplayName acc.account.accountUser) emails handle richInfo @@ -1006,20 +995,20 @@ synthesizeStoredUser usr veid = createdAt lastUpdatedAt baseuri - (userLocale (accountUser usr)) + (userLocale acc.account.accountUser) (Just role) - lift $ writeState accessTimes (userManagedBy (accountUser usr)) richInfo storedUser + lift $ writeState accessTimes (userManagedBy acc.account.accountUser) richInfo storedUser pure storedUser where getRole :: Sem r Role getRole = do let tmRoleOrDefault m = fromMaybe defaultRole $ m >>= \member -> member ^. Member.permissions . to Member.permissionsRole - maybe (pure defaultRole) (\tid -> tmRoleOrDefault <$> GalleyAccess.getTeamMember tid (userId $ accountUser usr)) (userTeam $ accountUser usr) + maybe (pure defaultRole) (\tid -> tmRoleOrDefault <$> GalleyAccess.getTeamMember tid (userId acc.account.accountUser)) (userTeam acc.account.accountUser) synthesizeStoredUser' :: (MonadError Scim.ScimError m) => UserId -> - ST.ValidExternalId -> + ST.ValidScimId -> Name -> [EmailAddress] -> Handle -> @@ -1031,7 +1020,7 @@ synthesizeStoredUser' :: Locale -> Maybe Role -> m (Scim.StoredUser ST.SparTag) -synthesizeStoredUser' uid veid dname _emails handle richInfo accStatus createdAt lastUpdatedAt baseuri locale mbRole = do +synthesizeStoredUser' uid veid dname emails handle richInfo accStatus createdAt lastUpdatedAt baseuri locale mbRole = do let scimUser :: Scim.User ST.SparTag scimUser = synthesizeScimUser @@ -1040,21 +1029,20 @@ synthesizeStoredUser' uid veid dname _emails handle richInfo accStatus createdAt ST.handle = handle {- 'Maybe' there is one in @usr@, but we want the type checker to make sure this exists, so we add it here redundantly, without the 'Maybe'. -}, - ST.emails = [], -- FUTUREWORK(elland): Account for SCIM emails field. + ST.emails = emails, ST.name = dname, ST.richInfo = richInfo, ST.active = ST.scimActiveFlagFromAccountStatus accStatus, ST.locale = Just locale, ST.role = mbRole } - pure $ toScimStoredUser createdAt lastUpdatedAt baseuri uid (normalizeLikeStored scimUser) synthesizeScimUser :: ST.ValidScimUser -> Scim.User ST.SparTag synthesizeScimUser info = let userName = info.handle.fromHandle in (Scim.empty @ST.SparTag ST.userSchemas userName (ST.ScimUserExtra info.richInfo)) - { Scim.externalId = Brig.renderValidExternalId info.externalId, + { Scim.externalId = Just $ validScimIdExternal info.externalId, Scim.displayName = Just $ fromName info.name, Scim.active = Just . Scim.ScimBool $ info.active, Scim.preferredLanguage = lan2Text . lLanguage <$> info.locale, @@ -1066,7 +1054,8 @@ synthesizeScimUser info = . toStrict . toByteString ) - (info.role) + (info.role), + Scim.emails = (\e -> Scim.Email.Email Nothing (Scim.Email.EmailAddress e) Nothing) <$> info.emails } -- TODO: now write a test, either in /integration or in spar, whichever is easier. (spar) @@ -1086,27 +1075,28 @@ getUserById :: UserId -> MaybeT (Scim.ScimHandler (Sem r)) (Scim.StoredUser ST.SparTag) getUserById midp stiTeam uid = do - brigUser <- MaybeT . lift $ BrigAccess.getAccount Brig.WithPendingInvitations uid + acc@(accountUser . account -> brigUser) <- MaybeT . lift $ BrigAccess.getAccount Brig.WithPendingInvitations uid let mbveid = Brig.veidFromBrigUser - (accountUser brigUser) + brigUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> midp) + acc.emailUnvalidated case mbveid of - Right veid | userTeam (accountUser brigUser) == Just stiTeam -> lift $ do - storedUser :: Scim.StoredUser ST.SparTag <- synthesizeStoredUser brigUser veid + Right veid | userTeam brigUser == Just stiTeam -> lift $ do + storedUser :: Scim.StoredUser ST.SparTag <- synthesizeStoredUser acc veid -- if we get a user from brig that hasn't been touched by scim yet, we call this -- function to move it under scim control. assertExternalIdNotUsedElsewhere stiTeam veid uid createValidScimUserSpar stiTeam uid storedUser veid lift $ do - when (veidChanged (accountUser brigUser) veid) $ - BrigAccess.setVeid uid veid - when (managedByChanged (accountUser brigUser)) $ + when (veidChanged brigUser veid) $ + BrigAccess.setSSOId uid (veidToUserSSOId veid) + when (managedByChanged brigUser) $ BrigAccess.setManagedBy uid ManagedByScim pure storedUser _ -> Applicative.empty where - veidChanged :: User -> ST.ValidExternalId -> Bool + veidChanged :: User -> ST.ValidScimId -> Bool veidChanged usr veid = case userIdentity usr of Nothing -> True Just (EmailIdentity _) -> True @@ -1135,13 +1125,13 @@ scimFindUserByHandle mIdpConfig stiTeam hndl = do brigUser <- MaybeT . lift . BrigAccess.getByHandle $ handle getUserById mIdpConfig stiTeam . userId . accountUser $ brigUser --- | Construct a 'ValidExternalid'. If it an 'Email', find the non-SAML SCIM user in spar; if +-- | Construct a 'ValidScimId'. If it is an 'Email', find the non-SAML SCIM user in spar; if -- that fails, find the user by email in brig. If it is a 'UserRef', find the SAML user. -- Return the result as a SCIM user. -- -- Note the user won't get an entry in `spar.user`. That will only happen on their first -- successful authentication with their SAML credentials. -scimFindUserByEmail :: +scimFindUserByExternalId :: forall r. ( Member BrigAccess r, Member GalleyAccess r, @@ -1156,34 +1146,21 @@ scimFindUserByEmail :: TeamId -> Text -> MaybeT (Scim.ScimHandler (Sem r)) (Scim.StoredUser ST.SparTag) -scimFindUserByEmail mIdpConfig stiTeam email = do - -- Azure has been observed to search for externalIds that are not emails, even if the - -- mapping is set up like it should be. This is a problem: if there is no SAML IdP, 'mkValidExternalId' - -- only supports external IDs that are emails. This is a missing feature / bug in spar tracked in - -- https://wearezeta.atlassian.net/browse/SQSERVICES-157; once it is fixed, we should go back to - -- throwing errors returned by 'mkValidExternalId' here, but *not* throw an error if the externalId is - -- a UUID, or any other text that is valid according to SCIM. - veid <- MaybeT . lift $ either (const Nothing) Just <$> runError @Scim.ScimError (mkValidExternalId mIdpConfig (pure email)) - uid <- MaybeT . lift $ ST.runValidExternalIdEither withUref withEmailOnly veid - -- since gc on `spar.users{,_v2}` is unreliable, we need to double-check with brig if the - -- user we found actually exists. - brigUser <- MaybeT . lift . BrigAccess.getAccount Brig.WithPendingInvitations $ uid - getUserById mIdpConfig stiTeam . userId . accountUser $ brigUser - where - withUref :: SAML.UserRef -> Sem r (Maybe UserId) - withUref uref = - SAMLUserStore.get uref >>= \case - Nothing -> maybe (pure Nothing) withEmailOnly $ Brig.urefToEmail uref - Just uid -> pure (Just uid) - - withEmailOnly :: EmailAddress -> Sem r (Maybe UserId) - withEmailOnly eml = maybe inbrig (pure . Just) =<< inspar - where - -- FUTUREWORK: we could also always lookup brig, that's simpler and possibly faster, - -- and it never should be visible in spar, but not in brig. - inspar, inbrig :: Sem r (Maybe UserId) - inspar = ScimExternalIdStore.lookup stiTeam eml - inbrig = userId . accountUser <$$> BrigAccess.getByEmail eml +scimFindUserByExternalId mIdpConfig stiTeam eid = do + mViaEid :: Maybe UserId <- MaybeT $ Just <$> lift (ScimExternalIdStore.lookup stiTeam eid) + uid <- case mViaEid of + Nothing -> do + veid <- MaybeT . lift $ either (const Nothing) Just <$> runError @Scim.ScimError (mkValidScimId mIdpConfig (Just eid) (emailAddressText eid)) + MaybeT . lift $ do + -- there are a few ways to find a user. this should all be redundant, especially the where + -- we lookup a user from brig by email, throw it away and only keep the uid, and then use + -- the uid to lookup the account again. but cassandra, and also reasons. + mViaEmail :: Maybe UserId <- join <$> (for (justHere veid.validScimIdAuthInfo) ((userId . accountUser <$$>) . BrigAccess.getByEmail)) + mViaUref :: Maybe UserId <- join <$> (for (justThere veid.validScimIdAuthInfo) SAMLUserStore.get) + pure $ mViaEmail <|> mViaUref + Just uid -> pure uid + acc <- MaybeT . lift . BrigAccess.getAccount Brig.WithPendingInvitations $ uid + getUserById mIdpConfig stiTeam (userId acc.account.accountUser) logFilter :: Filter -> (Msg -> Msg) logFilter (FilterAttrCompare attr op val) = diff --git a/services/spar/src/Spar/Sem/BrigAccess.hs b/services/spar/src/Spar/Sem/BrigAccess.hs index ebbc86d7ee4..53041076773 100644 --- a/services/spar/src/Spar/Sem/BrigAccess.hs +++ b/services/spar/src/Spar/Sem/BrigAccess.hs @@ -28,7 +28,7 @@ module Spar.Sem.BrigAccess setName, setHandle, setManagedBy, - setVeid, + setSSOId, setRichInfo, setLocale, getRichInfo, @@ -55,23 +55,22 @@ import qualified SAML2.WebSSO as SAML import Web.Cookie import Wire.API.Locale import Wire.API.Team.Role -import Wire.API.User (AccountStatus (..), DeleteUserResult, VerificationAction) +import Wire.API.User (AccountStatus (..), DeleteUserResult, ExtendedUserAccount, VerificationAction) import Wire.API.User.Identity import Wire.API.User.Profile import Wire.API.User.RichInfo as RichInfo -import Wire.API.User.Scim (ValidExternalId (..)) data BrigAccess m a where CreateSAML :: SAML.UserRef -> UserId -> TeamId -> Name -> ManagedBy -> Maybe Handle -> Maybe RichInfo -> Maybe Locale -> Role -> BrigAccess m UserId - CreateNoSAML :: EmailAddress -> UserId -> TeamId -> Name -> Maybe Locale -> Role -> BrigAccess m UserId + CreateNoSAML :: Text -> EmailAddress -> UserId -> TeamId -> Name -> Maybe Locale -> Role -> BrigAccess m UserId UpdateEmail :: UserId -> EmailAddress -> BrigAccess m () - GetAccount :: HavePendingInvitations -> UserId -> BrigAccess m (Maybe UserAccount) + GetAccount :: HavePendingInvitations -> UserId -> BrigAccess m (Maybe ExtendedUserAccount) GetByHandle :: Handle -> BrigAccess m (Maybe UserAccount) GetByEmail :: EmailAddress -> BrigAccess m (Maybe UserAccount) SetName :: UserId -> Name -> BrigAccess m () SetHandle :: UserId -> Handle {- not 'HandleUpdate'! -} -> BrigAccess m () SetManagedBy :: UserId -> ManagedBy -> BrigAccess m () - SetVeid :: UserId -> ValidExternalId -> BrigAccess m () + SetSSOId :: UserId -> UserSSOId -> BrigAccess m () SetRichInfo :: UserId -> RichInfo -> BrigAccess m () SetLocale :: UserId -> Maybe Locale -> BrigAccess m () GetRichInfo :: UserId -> BrigAccess m RichInfo diff --git a/services/spar/src/Spar/Sem/BrigAccess/Http.hs b/services/spar/src/Spar/Sem/BrigAccess/Http.hs index a1e5f8d04be..11ab18c6222 100644 --- a/services/spar/src/Spar/Sem/BrigAccess/Http.hs +++ b/services/spar/src/Spar/Sem/BrigAccess/Http.hs @@ -44,7 +44,7 @@ brigAccessToHttp mgr req = interpret $ viaRunHttp (RunHttpEnv mgr req) . \case CreateSAML u itlu itlt n m h ri ml r -> Intra.createBrigUserSAML u itlu itlt n m h ri ml r - CreateNoSAML e uid itlt n ml r -> Intra.createBrigUserNoSAML e uid itlt n ml r + CreateNoSAML eid e uid itlt n ml r -> Intra.createBrigUserNoSAML eid e uid itlt n ml r UpdateEmail itlu e -> Intra.updateEmail itlu e GetAccount h itlu -> Intra.getBrigUserAccount h itlu GetByHandle h -> Intra.getBrigUserByHandle h @@ -52,7 +52,7 @@ brigAccessToHttp mgr req = SetName itlu n -> Intra.setBrigUserName itlu n SetHandle itlu h -> Intra.setBrigUserHandle itlu h SetManagedBy itlu m -> Intra.setBrigUserManagedBy itlu m - SetVeid itlu v -> Intra.setBrigUserVeid itlu v + SetSSOId itlu v -> Intra.setBrigUserSSOId itlu v SetRichInfo itlu r -> Intra.setBrigUserRichInfo itlu r SetLocale itlu l -> Intra.setBrigUserLocale itlu l GetRichInfo itlu -> Intra.getBrigUserRichInfo itlu diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore.hs index 5a2ea23f247..c4bb2b54ed6 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore.hs @@ -28,20 +28,20 @@ module Spar.Sem.ScimExternalIdStore where import Data.Id (TeamId, UserId) +import Data.Text import Imports (Maybe, Show) import Polysemy import Polysemy.Check (deriveGenericK) import Spar.Scim.Types -import Wire.API.User.Identity import Wire.API.User.Scim data ScimExternalIdStore m a where - Insert :: TeamId -> EmailAddress -> UserId -> ScimExternalIdStore m () - Lookup :: TeamId -> EmailAddress -> ScimExternalIdStore m (Maybe UserId) - Delete :: TeamId -> EmailAddress -> ScimExternalIdStore m () - -- NB: the fact that we are using `Email` in some cases here and `ValidExternalId` in others has historical reasons (this table was only used for non-saml accounts in the past, now it is used for *all* scim-managed accounts). the interface would work equally well with just `Text` here (for unvalidated scim external id). - InsertStatus :: TeamId -> ValidExternalId -> UserId -> ScimUserCreationStatus -> ScimExternalIdStore m () - LookupStatus :: TeamId -> ValidExternalId -> ScimExternalIdStore m (Maybe (UserId, ScimUserCreationStatus)) + Insert :: TeamId -> Text -> UserId -> ScimExternalIdStore m () + Lookup :: TeamId -> Text -> ScimExternalIdStore m (Maybe UserId) + Delete :: TeamId -> Text -> ScimExternalIdStore m () + -- NB: the fact that we are using `Email` in some cases here and `ValidScimId` in others has historical reasons (this table was only used for non-saml accounts in the past, now it is used for *all* scim-managed accounts). the interface would work equally well with just `Text` here (for unvalidated scim external id). + InsertStatus :: TeamId -> ValidScimId -> UserId -> ScimUserCreationStatus -> ScimExternalIdStore m () + LookupStatus :: TeamId -> ValidScimId -> ScimExternalIdStore m (Maybe (UserId, ScimUserCreationStatus)) deriving instance Show (ScimExternalIdStore m a) diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs index 53734a13222..42d098dfe33 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs @@ -30,8 +30,7 @@ import Polysemy import Spar.Data.Instances () import Spar.Scim.Types (ScimUserCreationStatus (ScimUserCreated)) import Spar.Sem.ScimExternalIdStore (ScimExternalIdStore (..)) -import Wire.API.User.Identity -import Wire.API.User.Scim (ValidExternalId, runValidExternalIdUnsafe) +import Wire.API.User.Scim (ValidScimId (..)) scimExternalIdStoreToCassandra :: forall m r a. @@ -41,9 +40,9 @@ scimExternalIdStoreToCassandra :: scimExternalIdStoreToCassandra = interpret $ embed @m . \case - Insert tid em uid -> insertScimExternalId tid em uid - Lookup tid em -> lookupScimExternalId tid em - Delete tid em -> deleteScimExternalId tid em + Insert tid eid uid -> insertScimExternalId tid eid uid + Lookup tid eid -> lookupScimExternalId tid eid + Delete tid eid -> deleteScimExternalId tid eid InsertStatus tid veid buid status -> insertScimExternalIdStatus tid veid buid status LookupStatus tid veid -> lookupScimExternalIdStatus tid veid @@ -52,38 +51,38 @@ scimExternalIdStoreToCassandra = -- 'UserId' here. (Note that since there is no associated IdP, the externalId is required to -- be an email address, so we enforce that in the type signature, even though we only use it -- as a 'Text'.) -insertScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> EmailAddress -> UserId -> m () -insertScimExternalId tid (fromEmail -> email) uid = - retry x5 . write insert $ params LocalQuorum (tid, email, uid) +insertScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Text -> UserId -> m () +insertScimExternalId tid eid uid = + retry x5 . write insert $ params LocalQuorum (tid, eid, uid) where insert :: PrepQuery W (TeamId, Text, UserId) () insert = "INSERT INTO scim_external (team, external_id, user) VALUES (?, ?, ?)" -- | The inverse of 'insertScimExternalId'. -lookupScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> EmailAddress -> m (Maybe UserId) -lookupScimExternalId tid (fromEmail -> email) = runIdentity <$$> (retry x1 . query1 sel $ params LocalQuorum (tid, email)) +lookupScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Text -> m (Maybe UserId) +lookupScimExternalId tid eid = runIdentity <$$> (retry x1 . query1 sel $ params LocalQuorum (tid, eid)) where sel :: PrepQuery R (TeamId, Text) (Identity UserId) sel = "SELECT user FROM scim_external WHERE team = ? and external_id = ?" -- | The other inverse of 'insertScimExternalId' :). -deleteScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> EmailAddress -> m () -deleteScimExternalId tid (fromEmail -> email) = - retry x5 . write delete $ params LocalQuorum (tid, email) +deleteScimExternalId :: (HasCallStack, MonadClient m) => TeamId -> Text -> m () +deleteScimExternalId tid eid = + retry x5 . write delete $ params LocalQuorum (tid, eid) where delete :: PrepQuery W (TeamId, Text) () delete = "DELETE FROM scim_external WHERE team = ? and external_id = ?" -insertScimExternalIdStatus :: (HasCallStack, MonadClient m) => TeamId -> ValidExternalId -> UserId -> ScimUserCreationStatus -> m () +insertScimExternalIdStatus :: (HasCallStack, MonadClient m) => TeamId -> ValidScimId -> UserId -> ScimUserCreationStatus -> m () insertScimExternalIdStatus tid veid uid status = - retry x5 . write insert $ params LocalQuorum (tid, runValidExternalIdUnsafe veid, uid, status) + retry x5 . write insert $ params LocalQuorum (tid, validScimIdExternal veid, uid, status) where insert :: PrepQuery W (TeamId, Text, UserId, ScimUserCreationStatus) () insert = "INSERT INTO scim_external (team, external_id, user, creation_status) VALUES (?, ?, ?, ?)" -lookupScimExternalIdStatus :: (HasCallStack, MonadClient m) => TeamId -> ValidExternalId -> m (Maybe (UserId, ScimUserCreationStatus)) +lookupScimExternalIdStatus :: (HasCallStack, MonadClient m) => TeamId -> ValidScimId -> m (Maybe (UserId, ScimUserCreationStatus)) lookupScimExternalIdStatus tid veid = do - mResult <- retry x1 . query1 sel $ params LocalQuorum (tid, runValidExternalIdUnsafe veid) + mResult <- retry x1 . query1 sel $ params LocalQuorum (tid, validScimIdExternal veid) -- if the user exists and the status is not present, we assume the user was created successfully pure $ mResult <&> second (fromMaybe ScimUserCreated) where diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs index 3af1a26437d..5ab14ccd4af 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs @@ -27,18 +27,17 @@ import qualified Data.Map as M import Imports import Polysemy import Polysemy.State -import Spar.Scim (runValidExternalIdUnsafe) import Spar.Scim.Types (ScimUserCreationStatus) import Spar.Sem.ScimExternalIdStore -import Wire.API.User (fromEmail) +import Wire.API.User.Scim (ValidScimId (..)) scimExternalIdStoreToMem :: Sem (ScimExternalIdStore ': r) a -> Sem r (Map (TeamId, Text) (UserId, Maybe ScimUserCreationStatus), a) scimExternalIdStoreToMem = (runState mempty .) $ reinterpret $ \case - Insert tid em uid -> modify $ M.insert (tid, fromEmail em) (uid, Nothing) - Lookup tid em -> fmap fst <$> gets (M.lookup (tid, fromEmail em)) - Delete tid em -> modify $ M.delete (tid, fromEmail em) - InsertStatus tid veid uid status -> modify $ M.insert (tid, runValidExternalIdUnsafe veid) (uid, Just status) - LookupStatus tid veid -> ((=<<) (\(uid, mStatus) -> (uid,) <$> mStatus)) <$> gets (M.lookup (tid, runValidExternalIdUnsafe veid)) + Insert tid eid uid -> modify $ M.insert (tid, eid) (uid, Nothing) + Lookup tid eid -> fmap fst <$> gets (M.lookup (tid, eid)) + Delete tid eid -> modify $ M.delete (tid, eid) + InsertStatus tid veid uid status -> modify $ M.insert (tid, veid.validScimIdExternal) (uid, Just status) + LookupStatus tid veid -> ((=<<) (\(uid, mStatus) -> (uid,) <$> mStatus)) <$> gets (M.lookup (tid, veid.validScimIdExternal)) diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 911376524aa..9f9b0773d14 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -87,6 +87,7 @@ import qualified Web.Scim.Class.User as Scim import qualified Web.Scim.Schema.Common as Scim import qualified Web.Scim.Schema.Meta as Scim import qualified Web.Scim.Schema.User as Scim +import qualified Web.Scim.Schema.User.Email as Scim import Wire.API.Team.Member (newTeamMemberDeleteData, rolePermissions) import Wire.API.Team.Permission hiding (self) import Wire.API.Team.Role @@ -1056,8 +1057,19 @@ specCRUDIdentityProvider = do respId <- listUsers tok (Just (filterBy "externalId" externalId)) respHandle <- listUsers tok (Just (filterBy "userName" handle')) liftIO $ do - respId `shouldBe` [target] - respHandle `shouldBe` [target] + let patched = case target of + Scim.WithMeta _m (Scim.WithId i u) -> + let u' :: Scim.User SparTag + u' = case emailAddress (cs externalId) of + -- if the externalId is an email, and the email field was + -- empty, the scim response from spar contains the externalId + -- (parsed) in the emails field. + Just e -> u {Scim.emails = [Scim.Email Nothing (Scim.EmailAddress e) Nothing]} + Nothing -> u + in -- don't compare meta, or you need to update the ETag in version because email may have changed. + Scim.WithId i u' + (Scim.thing <$> respId) `shouldBe` [patched] + (Scim.thing <$> respHandle) `shouldBe` [patched] checkScimSearch scimStoredUser scimUser updateOrReplaceIdps (owner1, idp1, idpmeta1) @@ -1274,7 +1286,7 @@ specScimAndSAML = do userid' <- getUserIdViaRef' userref liftIO $ ('i', userid') `shouldBe` ('i', Just userid) userssoid <- getSsoidViaSelf' userid - liftIO $ ('r', veidUref <$$> (Intra.veidFromUserSSOId <$> userssoid)) `shouldBe` ('r', Just (Right (Just userref))) + liftIO $ ('r', veidUref <$$> ((`Intra.veidFromUserSSOId` Nothing) <$> userssoid)) `shouldBe` ('r', Just (Right (Just userref))) -- login a user for the first time with the scim-supplied credentials authnreq <- negotiateAuthnRequest idp spmeta <- getTestSPMetadata tid @@ -1516,7 +1528,7 @@ getSsoidViaAuthResp :: (HasCallStack) => SignedAuthnResponse -> TestSpar UserSSO getSsoidViaAuthResp aresp = do parsed :: AuthnResponse <- either error pure . parseFromDocument $ fromSignedAuthnResponse aresp - either error (pure . Intra.veidToUserSSOId . UrefOnly) $ getUserRef parsed + either error (pure . UserSSOId) $ getUserRef parsed specSparUserMigration :: SpecWith TestEnv specSparUserMigration = do diff --git a/services/spar/test-integration/Test/Spar/DataSpec.hs b/services/spar/test-integration/Test/Spar/DataSpec.hs index 25f1ee2f468..f310a265399 100644 --- a/services/spar/test-integration/Test/Spar/DataSpec.hs +++ b/services/spar/test-integration/Test/Spar/DataSpec.hs @@ -48,7 +48,6 @@ import Web.Scim.Schema.Common as Scim.Common import Web.Scim.Schema.Meta as Scim.Meta import Wire.API.User.IdentityProvider import Wire.API.User.Saml -import Wire.API.User.Scim spec :: SpecWith TestEnv spec = do @@ -248,20 +247,20 @@ testDeleteTeam = it "cleans up all the right tables after deletion" $ do liftIO $ tokens `shouldBe` [] -- The users from 'user': do - mbUser1 <- case veidFromUserSSOId ssoid1 of + mbUser1 <- case veidFromUserSSOId ssoid1 Nothing of Right veid -> runSpar $ - runValidExternalIdEither + runValidScimIdEither SAMLUserStore.get undefined -- could be @Data.lookupScimExternalId@, but we don't hit that path. veid Left _email -> undefined -- runSparCass . Data.lookupScimExternalId . fromEmail $ _email liftIO $ mbUser1 `shouldBe` Nothing do - mbUser2 <- case veidFromUserSSOId ssoid2 of + mbUser2 <- case veidFromUserSSOId ssoid2 Nothing of Right veid -> runSpar $ - runValidExternalIdEither + runValidScimIdEither SAMLUserStore.get undefined veid diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 57184a319d3..efc6a1c3556 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -84,6 +84,7 @@ import qualified Web.Scim.Schema.Meta as Scim import Web.Scim.Schema.PatchOp (Operation) import qualified Web.Scim.Schema.PatchOp as PatchOp import qualified Web.Scim.Schema.User as Scim.User +import qualified Web.Scim.Schema.User.Email as Scim.Email import qualified Wire.API.Team.Export as CsvExport import qualified Wire.API.Team.Feature as Feature import Wire.API.Team.Invitation (Invitation (..)) @@ -341,7 +342,7 @@ assertSparCassandraUref (uref, urefAnswer) = do assertSparCassandraScim :: (HasCallStack) => ((TeamId, EmailAddress), Maybe UserId) -> TestSpar () assertSparCassandraScim ((teamid, email), scimAnswer) = do liftIO . (`shouldBe` scimAnswer) - =<< runSpar (ScimExternalIdStore.lookup teamid email) + =<< runSpar (ScimExternalIdStore.lookup teamid (fromEmail email)) assertBrigCassandra :: (HasCallStack) => @@ -352,7 +353,7 @@ assertBrigCassandra :: ManagedBy -> TestSpar () assertBrigCassandra uid uref usr (valemail, emailValidated) managedBy = do - runSpar (BrigAccess.getAccount NoPendingInvitations uid) >>= \(Just acc) -> liftIO $ do + runSpar (BrigAccess.getAccount NoPendingInvitations uid) >>= \(Just (account -> acc)) -> liftIO $ do let handle = fromRight errmsg . parseHandleEither $ Scim.User.userName usr where errmsg = error . show . Scim.User.userName $ usr @@ -619,6 +620,9 @@ testCreateUserNoIdPWithRole brig tid owner tok role = do -- - if the user has a pending invitation, we have to look up the role in the invitation table -- by doing an rpc to brig liftIO $ Scim.User.roles usr `shouldBe` [cs $ toByteString defaultRole] + -- now external ID can differ from email, so emails are also returned + liftIO $ (\(Scim.Email.Email _ e _) -> Scim.Email.unEmailAddress e) <$> Scim.User.emails usr `shouldBe` [email] + liftIO $ Scim.User.externalId usr `shouldBe` (Just (fromEmail email)) -- user follows invitation flow do @@ -650,12 +654,17 @@ testCreateUserNoIdP = do brigUserAccount <- aFewTimes (runSpar $ BrigAccess.getAccount Intra.WithPendingInvitations userid) isJust >>= maybe (error "could not find user in brig") pure - let brigUser = accountUser brigUserAccount + let brigUser = brigUserAccount.account.accountUser brigUser `userShouldMatch` WrappedScimStoredUser scimStoredUser - liftIO $ accountStatus brigUserAccount `shouldBe` PendingInvitation + liftIO $ accountStatus brigUserAccount.account `shouldBe` PendingInvitation liftIO $ userEmail brigUser `shouldBe` Just email liftIO $ userManagedBy brigUser `shouldBe` ManagedByScim - liftIO $ userSSOId brigUser `shouldBe` Nothing + -- Previous to the change that allowed the external ID to be different from the email, `userSSOId brigUser` was `Nothing`. + -- We now store the external id as the sso_id when the user invitation is created, whereas before + -- we stored the email address (as in EmailIdentity) and sso_id was not stored until the user registered. + -- We need to store the external id when the user is invited because in case it is different from the email, we don't have it + -- otherwise when the user registers. + liftIO $ userSSOId brigUser `shouldBe` Just (UserScimExternalId (fromEmail email)) -- searching user in brig should fail -- >>> searchUser brig owner userName False @@ -690,10 +699,10 @@ testCreateUserNoIdP = do brigUser <- aFewTimes (runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations userid) isJust >>= maybe (error "could not find user in brig") pure - liftIO $ accountStatus brigUser `shouldBe` Active - liftIO $ userManagedBy (accountUser brigUser) `shouldBe` ManagedByScim - liftIO $ userHandle (accountUser brigUser) `shouldBe` Just handle - liftIO $ userSSOId (accountUser brigUser) `shouldBe` Just (UserScimExternalId (fromEmail email)) + liftIO $ accountStatus brigUser.account `shouldBe` Active + liftIO $ userManagedBy (accountUser brigUser.account) `shouldBe` ManagedByScim + liftIO $ userHandle (accountUser brigUser.account) `shouldBe` Just handle + liftIO $ userSSOId (accountUser brigUser.account) `shouldBe` Just (UserScimExternalId (fromEmail email)) susr <- getUser tok userid let usr = Scim.value . Scim.thing $ susr liftIO $ Scim.User.active usr `shouldNotBe` Just (Scim.ScimBool False) @@ -1190,7 +1199,8 @@ testFindProvisionedUser = do storedUser <- createUser tok user [storedUser'] <- listUsers tok (Just (filterBy "userName" (Scim.User.userName user))) liftIO $ storedUser' `shouldBe` storedUser - liftIO $ Scim.value (Scim.thing storedUser') `shouldBe` setDefaultRoleIfEmpty (normalizeLikeStored (setPreferredLanguage defLang user {Scim.User.emails = [] {- only after validation -}})) + let expected = setDefaultRoleAndEmailsIfEmpty (normalizeLikeStored (setPreferredLanguage defLang user)) + liftIO $ Scim.value (Scim.thing storedUser') `shouldBe` expected let Just externalId = Scim.User.externalId user users' <- listUsers tok (Just (filterBy "externalId" externalId)) liftIO $ users' `shouldBe` [storedUser] @@ -1225,7 +1235,7 @@ testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO = do runSpar $ BrigAccess.setHandle uid handle pure usr let memberIdWithSSO = userId memberWithSSO - externalId = either error id $ veidToText =<< Intra.veidFromBrigUser memberWithSSO Nothing + externalId = either error id $ veidToText =<< Intra.veidFromBrigUser memberWithSSO Nothing Nothing -- NOTE: once SCIM is enabled, SSO auto-provisioning is disabled tok <- registerScimToken teamid (Just (idp ^. SAML.idpId)) @@ -1236,9 +1246,9 @@ testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO = do Just brigUser' <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations memberIdWithSSO liftIO $ userManagedBy brigUser' `shouldBe` ManagedByScim where - veidToText :: (MonadError String m) => ValidExternalId -> m Text + veidToText :: (MonadError String m) => ValidScimId -> m Text veidToText veid = - runValidExternalIdEither + runValidScimIdEither (\(SAML.UserRef _ subj) -> maybe (throwError "bad uref from brig") (pure . CI.original) $ SAML.shortShowNameID subj) (pure . fromEmail) veid @@ -1605,7 +1615,10 @@ testScimSideIsUpdated = do storedUser <- createUser tok user let userid = scimUserId storedUser -- Overwrite the user with another randomly-generated user - user' <- randomScimUser + user' <- + if isJust (emailAddressText =<< user.externalId) + then fst <$> randomScimUserWithEmail + else fst <$> randomScimUserWithNick updatedUser <- updateUser tok userid user' -- Get the updated user and check that it matches the user returned by -- 'updateUser' @@ -1614,7 +1627,7 @@ testScimSideIsUpdated = do -- Check that the updated user also matches the data that we sent with -- 'updateUser' richInfoLimit <- view (teOpts . to richInfoLimit) - expectedUser <- setDefaultRoleIfEmpty <$$> whatSparReturnsFor idp richInfoLimit (setPreferredLanguage defLang user') + expectedUser <- setDefaultRoleAndEmailsIfEmpty <$$> whatSparReturnsFor idp richInfoLimit (setPreferredLanguage defLang user') liftIO $ do Right (Scim.value (Scim.thing storedUser')) `shouldBe` expectedUser Scim.id (Scim.thing storedUser') `shouldBe` Scim.id (Scim.thing storedUser) @@ -1659,9 +1672,13 @@ testUpdateSameHandle = do let userid = scimUserId storedUser -- Overwrite the user with another randomly-generated user who has the same name and -- handle - user' <- - randomScimUser <&> \u -> - u + user' <- do + rsu <- + if isJust (emailAddressText =<< user.externalId) + then fst <$> randomScimUserWithEmail + else fst <$> randomScimUserWithNick + pure + rsu { Scim.User.userName = Scim.User.userName user, Scim.User.displayName = Scim.User.displayName user } @@ -1671,7 +1688,7 @@ testUpdateSameHandle = do liftIO $ updatedUser `shouldBe` storedUser' -- Check that the updated user also matches the data that we sent with 'updateUser' richInfoLimit <- view (teOpts . to richInfoLimit) - expectedUser <- setDefaultRoleIfEmpty <$$> whatSparReturnsFor idp richInfoLimit (setPreferredLanguage defLang user') + expectedUser <- setDefaultRoleAndEmailsIfEmpty <$$> whatSparReturnsFor idp richInfoLimit (setPreferredLanguage defLang user') liftIO $ do Right (Scim.value (Scim.thing storedUser')) `shouldBe` expectedUser Scim.id (Scim.thing storedUser') `shouldBe` Scim.id (Scim.thing storedUser) @@ -1700,51 +1717,62 @@ testUpdateExternalId withidp = do (_owner, tid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) (,Nothing,tid) <$> registerScimToken tid Nothing - let checkUpdate :: (HasCallStack) => Bool -> TestSpar () - checkUpdate hasChanged {- is externalId updated with a different value, or with itself? -} = do - -- Create a user via SCIM - email <- randomEmail - user <- randomScimUser <&> \u -> u {Scim.User.externalId = Just $ fromEmail email} - storedUser <- createUser tok user - let userid = scimUserId storedUser - if withidp - then call $ activateEmail brig email - else registerUser brig tid email - veid :: ValidExternalId <- - runSpar . runScimErrorUnsafe $ - mkValidExternalId midp (Scim.User.externalId user) - -- Overwrite the user with another randomly-generated user (only controlling externalId) - otherEmail <- randomEmail - user' <- do - let upd u = - u - { Scim.User.externalId = - if hasChanged - then Just $ fromEmail otherEmail - else Scim.User.externalId user - } - randomScimUser <&> upd - veid' <- - runSpar . runScimErrorUnsafe $ - mkValidExternalId midp (Scim.User.externalId user') - - _ <- updateUser tok userid user' - - when hasChanged (call $ activateEmail brig otherEmail) - muserid <- lookupByValidExternalId tid veid - muserid' <- lookupByValidExternalId tid veid' - liftIO $ do - if hasChanged - then do - (hasChanged, muserid) `shouldBe` (hasChanged, Nothing) - (hasChanged, muserid') `shouldBe` (hasChanged, Just userid) - else do - (hasChanged, veid') `shouldBe` (hasChanged, veid) - (hasChanged, muserid') `shouldBe` (hasChanged, Just userid) - eventually $ checkEmail userid (Just $ if hasChanged then otherEmail else email) - - checkUpdate True - checkUpdate False + email <- randomEmail + user <- randomScimUser <&> \u -> u {Scim.User.externalId = Just $ fromEmail email} + storedUser <- createUser tok user + let userid = scimUserId storedUser + if withidp + then call $ activateEmail brig email + else registerUser brig tid email + veid :: ValidScimId <- + runSpar . runScimErrorUnsafe $ + mkValidScimId midp (Scim.User.externalId user) (Just email) + + do + -- idempotency (email changes to itself) + -- Overwrite the user with another randomly-generated user (only controlling externalId) + updatedUser <- do + let upd u = u {Scim.User.externalId = Scim.User.externalId user} + randomScimUser <&> upd + veid' <- + runSpar . runScimErrorUnsafe $ + mkValidScimId midp (Scim.User.externalId updatedUser) (Just email) + _ <- updateUser tok userid updatedUser + + muserid <- lookupByValidScimId tid veid + muserid' <- lookupByValidScimId tid veid' + liftIO $ do + ('i', veid') `shouldBe` ('i', veid) + ('i', muserid) `shouldBe` ('i', Just userid) + ('i', muserid') `shouldBe` ('i', Just userid) + eventually $ checkEmail userid (Just email) + + do + -- email changes to other email + -- Overwrite the user with another randomly-generated user (only controlling externalId) + otherEmail <- randomEmail + updatedUser <- do + let upd u = u {Scim.User.externalId = Just $ fromEmail otherEmail} + randomScimUser <&> upd + veid' <- + runSpar . runScimErrorUnsafe $ + mkValidScimId midp (Scim.User.externalId updatedUser) (Just email) -- otherEmail has not been validated yet. + _ <- updateUser tok userid updatedUser + + call $ activateEmail brig otherEmail + veid'' <- + runSpar . runScimErrorUnsafe $ + mkValidScimId midp (Scim.User.externalId updatedUser) (Just otherEmail) + + muserid <- lookupByValidScimId tid veid + muserid' <- lookupByValidScimId tid veid' + muserid'' <- lookupByValidScimId tid veid'' + + liftIO $ do + ('c', muserid) `shouldBe` ('c', Nothing) + ('c', muserid') `shouldBe` ('c', if withidp then Just userid else Nothing) + ('c', muserid'') `shouldBe` ('c', Just userid) + eventually $ checkEmail userid (Just otherEmail) testUpdateExternalIdOfUnregisteredAccount :: TestSpar () testUpdateExternalIdOfUnregisteredAccount = do @@ -1757,9 +1785,9 @@ testUpdateExternalIdOfUnregisteredAccount = do user <- randomScimUser <&> \u -> u {Scim.User.externalId = Just $ fromEmail email} storedUser <- createUser tok user let userid = scimUserId storedUser - veid :: ValidExternalId <- + veid :: ValidScimId <- runSpar . runScimErrorUnsafe $ - mkValidExternalId Nothing (Scim.User.externalId user) + mkValidScimId Nothing (Scim.User.externalId user) (Just email) -- Overwrite the user with another randomly-generated user (only controlling externalId) -- And update the user before they have registered their account otherEmail <- randomEmail @@ -1768,25 +1796,30 @@ testUpdateExternalIdOfUnregisteredAccount = do randomScimUser <&> upd veid' <- runSpar . runScimErrorUnsafe $ - mkValidExternalId Nothing (Scim.User.externalId user') + mkValidScimId Nothing (Scim.User.externalId user') (Just otherEmail) _ <- updateUser tok userid user' -- Now the user registers their account (via old email) registerUser brig tid email -- Then the user activates their new email address call $ activateEmail brig otherEmail - muserid <- lookupByValidExternalId tid veid - muserid' <- lookupByValidExternalId tid veid' + muserid <- lookupByValidScimId tid veid + muserid' <- lookupByValidScimId tid veid' liftIO $ do muserid `shouldBe` Nothing muserid' `shouldBe` Just userid eventually $ checkEmail userid (Just otherEmail) -lookupByValidExternalId :: TeamId -> ValidExternalId -> TestSpar (Maybe UserId) -lookupByValidExternalId tid = - runValidExternalIdEither +lookupByValidScimId :: TeamId -> ValidScimId -> TestSpar (Maybe UserId) +lookupByValidScimId tid = + -- `SU.scimFindUserByExternalId Nothing tid vsid.validScimIdExternal` would be simpler, but + -- if you want to simplify this you'll have to fix the type errors, and this is one of the + -- abandoned test suites. + + runValidScimIdEither (runSpar . SAMLUserStore.get) ( \email -> do - let action = SU.scimFindUserByEmail Nothing tid $ fromEmail email + -- caution: now ext id and email can differ, in which case this will not work anymore + let action = SU.scimFindUserByExternalId Nothing tid $ fromEmail email result <- runSpar . runExceptT . runMaybeT $ action case result of Right muser -> pure $ Scim.id . Scim.thing <$> muser @@ -2105,10 +2138,10 @@ specDeleteUser = do storedUser <- createUser tok user let uid :: UserId = scimUserId storedUser uref :: SAML.UserRef <- do - usr <- runSpar $ Intra.getBrigUser Intra.WithPendingInvitations uid + mUsr <- runSpar $ Intra.getBrigUser Intra.WithPendingInvitations uid let err = error . ("brig user without UserRef: " <>) . show - case (`Intra.veidFromBrigUser` Nothing) <$> usr of - bad@(Just (Right veid)) -> runValidExternalIdEither pure (const $ err bad) veid + case (\usr -> Intra.veidFromBrigUser usr Nothing Nothing) <$> mUsr of + bad@(Just (Right veid)) -> runValidScimIdEither pure (const $ err bad) veid bad -> err bad spar <- view teSpar deleteUser_ (Just tok) (Just uid) spar @@ -2279,7 +2312,7 @@ specEmailValidation = do scimStoredUser <- createUser tok user veid <- runSpar . runScimErrorUnsafe $ - mkValidExternalId (Just idp) (Scim.User.externalId . Scim.value . Scim.thing $ scimStoredUser) + mkValidScimId (Just idp) (Scim.User.externalId . Scim.value . Scim.thing $ scimStoredUser) (Just email) uid :: UserId <- getUserIdViaRef $ fromJust (veidUref veid) brig <- view teBrig -- we intentionally activate the email even if it's not set up to work, to make sure @@ -2325,7 +2358,7 @@ testDeletedUsersFreeExternalIdNoIdp = do void $ aFewTimes - (runSpar $ ScimExternalIdStore.lookup tid email) + (runSpar $ ScimExternalIdStore.lookup tid (fromEmail email)) (== Nothing) -- | CSV download of team members is mainly tested here: 'API.Teams.testListTeamMembersCsv'. diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index f6affbfa514..5de87fd9579 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -159,6 +159,7 @@ import Data.String.Conversions import Data.Text (pack) import Data.Text.Encoding (encodeUtf8) import qualified Data.Text.Lazy.Encoding as LT +import Data.These import Data.UUID as UUID hiding (fromByteString, null) import Data.UUID.V4 as UUID (nextRandom) import qualified Data.Yaml as Yaml @@ -208,7 +209,7 @@ import Wire.API.User import qualified Wire.API.User as User import Wire.API.User.Auth hiding (Cookie) import Wire.API.User.IdentityProvider -import Wire.API.User.Scim (runValidExternalIdEither) +import Wire.API.User.Scim import Wire.Sem.Logger.TinyLog -- | Call 'mkEnv' with options from config files. @@ -1144,15 +1145,17 @@ callDeleteDefaultSsoCode sparreq_ = do -- helpers talking to spar's cassandra directly --- | Look up 'UserId' under 'UserSSOId' on spar's cassandra directly. +-- | Look up 'UserId' under 'externalId', and if no email address is given, under the saml user ref. +-- +-- This is a bit convoluted, don't try too hard to make sense of it. Better luck when +-- rewriting this in /integration! :-) ssoToUidSpar :: (HasCallStack, MonadIO m, MonadReader TestEnv m) => TeamId -> UserSSOId -> m (Maybe UserId) ssoToUidSpar tid ssoid = do - veid <- either (error . ("could not parse brig sso_id: " <>)) pure $ Intra.veidFromUserSSOId ssoid + veid <- either (error . ("could not parse brig sso_id: " <>)) pure $ Intra.veidFromUserSSOId ssoid Nothing runSpar $ - runValidExternalIdEither - SAMLUserStore.get - (ScimExternalIdStore.lookup tid) - veid + let doThat = SAMLUserStore.get + doThis _ = ScimExternalIdStore.lookup tid veid.validScimIdExternal + in these doThis doThat (const doThat) veid.validScimIdAuthInfo runSimpleSP :: (MonadReader TestEnv m, MonadIO m) => SAML.SimpleSP a -> m a runSimpleSP action = do diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index caf143ab661..a077454467d 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -33,6 +33,7 @@ import Data.LanguageCodes (ISO639_1 (EN)) import Data.String.Conversions import Data.Text.Encoding (encodeUtf8) import qualified Data.Text.Lazy as Lazy +import Data.These import Data.Time import Data.UUID as UUID import Data.UUID.V4 as UUID @@ -61,6 +62,7 @@ import qualified Web.Scim.Schema.PatchOp as Scim.PatchOp import qualified Web.Scim.Schema.User as Scim import qualified Web.Scim.Schema.User as Scim.User import qualified Web.Scim.Schema.User.Email as Email +import qualified Web.Scim.Schema.User.Email as Scim.Email import qualified Web.Scim.Schema.User.Phone as Phone import qualified Wire.API.Team.Member as Member import Wire.API.Team.Role (Role, defaultRole) @@ -69,6 +71,10 @@ import Wire.API.User.IdentityProvider hiding (handle, team) import Wire.API.User.RichInfo import Wire.API.User.Scim +-- | Take apart a 'ValidScimId', using 'SAML.UserRef' if available, otherwise 'Email'. +runValidScimIdEither :: (SAML.UserRef -> a) -> (EmailAddress -> a) -> ValidScimId -> a +runValidScimIdEither doUref doEmail = these doEmail doUref (\_ uref -> doUref uref) . validScimIdAuthInfo + -- | Call 'registerTestIdP', then 'registerScimToken'. The user returned is the owner of the team; -- the IdP is registered with the team; the SCIM token can be used to manipulate the team. registerIdPAndScimToken :: (HasCallStack) => TestSpar (ScimToken, (UserId, TeamId, IdP)) @@ -137,7 +143,6 @@ randomScimUserWithSubjectAndRichInfo :: m (Scim.User.User SparTag, SAML.UnqualifiedNameID) randomScimUserWithSubjectAndRichInfo richInfo = do suffix <- cs <$> replicateM 20 (getRandomR ('a', 'z')) - emails <- getRandomR (0, 3) >>= \n -> replicateM n randomScimEmail phones <- getRandomR (0, 3) >>= \n -> replicateM n randomScimPhone -- Related, but non-trivial to re-use here: 'nextSubject' (externalId, subj) <- @@ -156,7 +161,7 @@ randomScimUserWithSubjectAndRichInfo richInfo = do ( (Scim.User.empty @SparTag userSchemas ("scimuser_" <> suffix) (ScimUserExtra richInfo)) { Scim.User.displayName = Just ("ScimUser" <> suffix), Scim.User.externalId = Just externalId, - Scim.User.emails = emails, + Scim.User.emails = [], Scim.User.phoneNumbers = phones, Scim.User.roles = ["member"] -- if we don't add this role here explicitly, some tests may show confusing failures @@ -637,7 +642,7 @@ instance IsUser ValidScimUser where maybeName = Just (Just <$> name) maybeTenant = Just (fmap SAML._uidTenant . veidUref . externalId) maybeSubject = Just (fmap SAML._uidSubject . veidUref . externalId) - maybeScimExternalId = Just (runValidExternalIdEither Intra.urefToExternalId (Just . fromEmail) . externalId) + maybeScimExternalId = Just (runValidScimIdEither Intra.urefToExternalId (Just . fromEmail) . externalId) maybeLocale = Just locale instance IsUser (WrappedScimStoredUser SparTag) where @@ -673,20 +678,20 @@ instance IsUser User where maybeHandle = Just userHandle maybeName = Just (Just . userDisplayName) maybeTenant = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing + Intra.veidFromBrigUser usr Nothing Nothing & either (const Nothing) (fmap SAML._uidTenant . veidUref) maybeSubject = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing + Intra.veidFromBrigUser usr Nothing Nothing & either (const Nothing) (fmap SAML._uidSubject . veidUref) maybeScimExternalId = Just $ \usr -> - Intra.veidFromBrigUser usr Nothing + Intra.veidFromBrigUser usr Nothing Nothing & either (const Nothing) - (runValidExternalIdEither Intra.urefToExternalId (Just . fromEmail)) + (runValidScimIdEither Intra.urefToExternalId (Just . fromEmail)) maybeLocale = Just $ Just . userLocale -- | For all properties that are present in both @u1@ and @u2@, check that they match. @@ -735,11 +740,15 @@ setPreferredLanguage :: Language -> Scim.User.User SparTag -> Scim.User.User Spa setPreferredLanguage lang u = u {Scim.preferredLanguage = Scim.preferredLanguage u <|> Just (lan2Text lang)} -setDefaultRoleIfEmpty :: Scim.User.User a -> Scim.User.User a -setDefaultRoleIfEmpty u = +setDefaultRoleAndEmailsIfEmpty :: Scim.User.User a -> Scim.User.User a +setDefaultRoleAndEmailsIfEmpty u = u { Scim.User.roles = case Scim.User.roles u of [] -> [cs $ toByteString' defaultRole] + xs -> xs, + -- when the emails field is empty, we try to populate it with the externalId + Scim.User.emails = case Scim.User.emails u of + [] -> maybeToList ((\e -> Scim.Email.Email Nothing (Scim.Email.EmailAddress e) Nothing) <$> (emailAddressText =<< (Scim.User.externalId u))) xs -> xs } diff --git a/services/spar/test/Test/Spar/Intra/BrigSpec.hs b/services/spar/test/Test/Spar/Intra/BrigSpec.hs index 769fc29387f..5eb232d3d70 100644 --- a/services/spar/test/Test/Spar/Intra/BrigSpec.hs +++ b/services/spar/test/Test/Spar/Intra/BrigSpec.hs @@ -21,6 +21,8 @@ module Test.Spar.Intra.BrigSpec where import Arbitrary () import Data.String.Conversions +import Data.These +import Data.These.Combinators import Imports import SAML2.WebSSO as SAML import Spar.Intra.BrigApp @@ -41,32 +43,33 @@ spec = do -- remove them. it "example" $ do - let have = - UrefOnly $ + let veid = + ValidScimId "V" . That $ UserRef (Issuer $ mkuri "http://wire.com/") ( either (error . show) id $ mkNameID (mkUNameIDTransient "V") (Just "kati") (Just "rolli") (Just "jaan") ) - want = UserSSOId (SAML.UserRef iss nam) + ssoId = UserSSOId (SAML.UserRef iss nam) iss :: SAML.Issuer = fromRight undefined $ SAML.decodeElem "http://wire.com/" nam :: SAML.NameID = fromRight undefined $ SAML.decodeElem "V" - veidToUserSSOId have `shouldBe` want - veidFromUserSSOId want `shouldBe` Right have + veidToUserSSOId veid `shouldBe` ssoId + veidFromUserSSOId ssoId Nothing `shouldBe` Right veid + it "another example" $ do - let have = - UrefOnly $ + let veid = + ValidScimId "PWkS" . That $ UserRef (Issuer $ mkuri "http://wire.com/") ( either (error . show) id $ mkNameID (mkUNameIDPersistent "PWkS") (Just "hendrik") Nothing (Just "marye") ) - want = UserSSOId (SAML.UserRef iss nam) + ssoId = UserSSOId (SAML.UserRef iss nam) iss :: SAML.Issuer = fromRight undefined $ SAML.decodeElem "http://wire.com/" nam :: SAML.NameID = fromRight undefined $ SAML.decodeElem "PWkS" - - veidToUserSSOId have `shouldBe` want - veidFromUserSSOId want `shouldBe` Right have + veidToUserSSOId veid `shouldBe` ssoId + veidFromUserSSOId ssoId Nothing `shouldBe` Right veid it "roundtrips" . property $ - \(x :: ValidExternalId) -> (veidFromUserSSOId @(Either String) . veidToUserSSOId) x === Right x + \(ValidScimIdNoNameIDQualifiers x) -> + veidFromUserSSOId @(Either String) (veidToUserSSOId x) (justHere x.validScimIdAuthInfo) === Right x diff --git a/services/spar/test/Test/Spar/Scim/UserSpec.hs b/services/spar/test/Test/Spar/Scim/UserSpec.hs index 23a1391003c..ebc26096d0f 100644 --- a/services/spar/test/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test/Test/Spar/Scim/UserSpec.hs @@ -35,7 +35,7 @@ spec = describe "deleteScimUser" $ do r <- interpretWithBrigAccessMock (mockBrig (withActiveUser acc) AccountDeleted) - (deleteUserAndAssertDeletionInSpar acc tokenInfo) + (deleteUserAndAssertDeletionInSpar acc.account tokenInfo) r `shouldBe` Right () it "is idempotent" $ do tokenInfo <- generate arbitrary @@ -43,7 +43,7 @@ spec = describe "deleteScimUser" $ do r <- interpretWithBrigAccessMock (mockBrig (withActiveUser acc) AccountAlreadyDeleted) - (deleteUserAndAssertDeletionInSpar acc tokenInfo) + (deleteUserAndAssertDeletionInSpar acc.account tokenInfo) r `shouldBe` Right () it "works if there never was an account" $ do uid <- generate arbitrary @@ -82,9 +82,9 @@ deleteUserAndAssertDeletionInSpar acc tokenInfo = do let tid = stiTeam tokenInfo email = (fromJust . emailIdentity . fromJust . userIdentity . accountUser) acc uid = (userId . accountUser) acc - ScimExternalIdStore.insert tid email uid + ScimExternalIdStore.insert tid (fromEmail email) uid r <- runExceptT $ deleteScimUser tokenInfo uid - lr <- ScimExternalIdStore.lookup tid email + lr <- ScimExternalIdStore.lookup tid (fromEmail email) liftIO $ lr `shouldBe` Nothing pure r @@ -120,7 +120,7 @@ ignoringState f = fmap snd . f mockBrig :: forall (r :: EffectRow) a. (Member (Embed IO) r) => - (UserId -> Maybe UserAccount) -> + (UserId -> Maybe ExtendedUserAccount) -> DeleteUserResult -> Sem (BrigAccess ': r) a -> Sem r a @@ -131,26 +131,30 @@ mockBrig lookup_user delete_response = interpret $ \case liftIO $ expectationFailure $ "Unexpected effect (call to brig)" error "Throw error here to avoid implementation of all cases." -withActiveUser :: UserAccount -> UserId -> Maybe UserAccount +withActiveUser :: ExtendedUserAccount -> UserId -> Maybe ExtendedUserAccount withActiveUser acc uid = - if uid == (userId . accountUser) acc + if uid == (userId . accountUser) acc.account then Just acc else Nothing -someActiveUser :: ScimTokenInfo -> IO UserAccount +someActiveUser :: ScimTokenInfo -> IO ExtendedUserAccount someActiveUser tokenInfo = do user <- generate arbitrary pure $ - UserAccount - { accountStatus = Active, - accountUser = - user - { userDisplayName = Name "Some User", - userAccentId = defaultAccentId, - userPict = noPict, - userAssets = [], - userHandle = parseHandle "some-handle", - userIdentity = (Just . EmailIdentity . fromJust . emailAddressText) "someone@wire.com", - userTeam = Just $ stiTeam tokenInfo - } + ExtendedUserAccount + { account = + UserAccount + { accountStatus = Active, + accountUser = + user + { userDisplayName = Name "Some User", + userAccentId = defaultAccentId, + userPict = noPict, + userAssets = [], + userHandle = parseHandle "some-handle", + userIdentity = (Just . EmailIdentity . fromJust . emailAddressText) "someone@wire.com", + userTeam = Just $ stiTeam tokenInfo + } + }, + emailUnvalidated = Nothing } From c0b8cbcc08ebca0e118b25243817c6f62fd3c3c7 Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Thu, 5 Sep 2024 15:49:21 +0200 Subject: [PATCH 058/136] Upgrade to latest stable RabbitMQ (#4227) Use the latest stable Bitnami `rabbitmq` Helm chart and thus the latest stable `rabbitmq` image. --------- Co-authored-by: Leonhardt Wille --- changelog.d/2-features/upgrade-rabbitmq | 6 ++++++ charts/rabbitmq/requirements.yaml | 2 +- deploy/dockerephemeral/docker-compose.yaml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2-features/upgrade-rabbitmq diff --git a/changelog.d/2-features/upgrade-rabbitmq b/changelog.d/2-features/upgrade-rabbitmq new file mode 100644 index 00000000000..cead12bdd3d --- /dev/null +++ b/changelog.d/2-features/upgrade-rabbitmq @@ -0,0 +1,6 @@ +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/charts/rabbitmq/requirements.yaml b/charts/rabbitmq/requirements.yaml index 1742b3e8641..6ac9220940f 100644 --- a/charts/rabbitmq/requirements.yaml +++ b/charts/rabbitmq/requirements.yaml @@ -1,4 +1,4 @@ dependencies: - name: rabbitmq - version: 11.13.0 + version: 14.6.9 repository: https://charts.bitnami.com/bitnami diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index 58ff49b4c30..debbdb32fa4 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -262,7 +262,7 @@ services: rabbitmq: container_name: rabbitmq - image: rabbitmq:3.11-management-alpine + image: rabbitmq:3.13.7-management-alpine environment: - RABBITMQ_USERNAME - RABBITMQ_PASSWORD From dfab9a95a0b9af831437def3b88348549896fce5 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Mon, 9 Sep 2024 15:18:51 +0200 Subject: [PATCH 059/136] Return MLS public keys as part of getting a 1:1 conversation (#4224) * Routes.Public.Galley.Conversation: Remove version overlap in endpionts for getting one2one conversation * integration: Verify remove propsals from the backend in group convos Also add a test where local client is removed and the proposal is sent to remote clients * integration-tests: Use separate mls private keys for each env Pending: keys for dynamic backends when running in K8s * integration: resetClientGroup: Use MLS public keys for creator of the group instead of the conversation This encodes the assumption that the creator's backend always owns the conversation. For one2one conversations, this would break, however we should use a different function to do this setup as the keys are supposed to be returned in getMLSOne2OneConversation endpoint from version 6 onwards. * integration: Add tests for how the new API is supposed to work Also add a test to ensure that proposals for removing clients can be verified. * wire-api{,-federation}: Add new APIs for returning MLS public keys for 1:1 convs * galley: Implement new APIs for returning MLS public keys when getting 1:1 convs * integration: Adjust one2one conv tests to use the new API * galley.integration.yaml: fix path of the backend's public keys * add golden tests * charts/integration: Add mls private keys for dynamic backends * integration: Fix API versions to be used for fed-v0 and v1 envs They were hardcoded to use the dev API version, which is incorrect * BackendNotificationPusherSpec: Fix API version test Make it so it doesn't fail for adding new API versions * hack/bin/gen-certs.sh: Also gen certs for federation-v1 * integration: Describe how one2one conversations should work when talking to older backends * integration/SetupHelpers: Slightly nicer way to detect backend API version * wire-api: Add epoch_timestamp to serialization of conv Protocol It is necessary in older API versions, got removed as a bug. * federation-api-galley: Use Protocol from client API V5 when returning 'RemoteConversation' Federation API V1 expects JSON serialization similar to Client API V5. * galley-integration: Delete test which is already covered by new integration tests It required changes due to change in federation API * galley: Only allow getting remote MLS 1:1 convs if the remote supports federation API v2 * galley: Do not return 1:1 conversations in federation API V1 * galley: Add query param for public key format to "get-one-to-one-mls-conversation" * wire-api: Fix golden test JSONs These were likely generated after refactoring so we actually did break the API (in a small way). This is verified by seeing the results from q1-2024 release, APIs <= V5 return `epoch_timestamp` as `null` instead of omitting it. * MLSOne2OneConversation: Add dynamic param to swagger name * galley: Log warning when getting One2One conv and remote doesn't have MLS enabled * wire-api-federation: Fix wrongly generated golden test * integration/testSupportedProtocols: Don't run for federation-v0 Client API v4 doesn't support this endpoint * galley: Move JWK key format to client APIv7 --------- Co-authored-by: Akshay Mankar --- changelog.d/1-api-changes/jwk | 2 +- changelog.d/1-api-changes/one2one | 1 + charts/integration/templates/configmap.yaml | 12 + charts/integration/values.yaml | 85 +++++- .../wire-server/values-domain1.yaml.gotmpl | 30 ++ .../wire-server/values-domain2.yaml.gotmpl | 30 ++ hack/helmfile.yaml | 2 + integration/test/MLS/Util.hs | 36 ++- integration/test/SetupHelpers.hs | 13 +- integration/test/Test/MLS.hs | 43 ++- integration/test/Test/MLS/One2One.hs | 271 +++++++++++++++--- integration/test/Test/MLS/SubConversation.hs | 22 +- integration/test/Test/User.hs | 2 +- integration/test/Testlib/Env.hs | 8 +- integration/test/Testlib/HTTP.hs | 14 +- integration/test/Testlib/ModService.hs | 7 + integration/test/Testlib/ResourcePool.hs | 28 +- integration/test/Testlib/Types.hs | 9 +- integration/test/Testlib/VersionedFed.hs | 5 +- .../src/Wire/API/Federation/API/Galley.hs | 106 ++++++- .../src/Wire/API/Federation/Client.hs | 4 + .../src/Wire/API/Federation/Version.hs | 7 +- .../Golden/GetOne2OneConversationResponse.hs | 130 +++++++++ .../Wire/API/Federation/Golden/GoldenSpec.hs | 11 + ...neConversationResponseBackendMismatch.json | 3 + ...e2OneConversationResponseNotConnected.json | 3 + ...ject_GetOne2OneConversationResponseOk.json | 39 +++ ...ConversationResponseV2BackendMismatch.json | 3 + ...OneConversationResponseV2NotConnected.json | 3 + ...ct_GetOne2OneConversationResponseV2Ok.json | 47 +++ .../wire-api-federation.cabal | 1 + libs/wire-api/src/Wire/API/Conversation.hs | 19 +- .../src/Wire/API/Conversation/Protocol.hs | 10 +- libs/wire-api/src/Wire/API/Error/Galley.hs | 3 + libs/wire-api/src/Wire/API/MLS/Keys.hs | 17 +- .../API/Routes/Public/Galley/Conversation.hs | 20 +- .../wire-api/src/Wire/API/Routes/Versioned.hs | 2 + .../testObject_Conversation_v2_user_4.json | 1 + .../testObject_Conversation_v5_user_4.json | 1 + ...testObject_PublicSubConversation_v5_2.json | 1 + .../Wire/BackendNotificationPusherSpec.hs | 2 +- services/galley/galley.integration.yaml | 8 +- services/galley/src/Galley/API/Error.hs | 5 + services/galley/src/Galley/API/Federation.hs | 58 +++- services/galley/src/Galley/API/Internal.hs | 2 +- services/galley/src/Galley/API/MLS.hs | 22 +- services/galley/src/Galley/API/MLS/One2One.hs | 26 +- services/galley/src/Galley/API/Mapping.hs | 6 +- .../src/Galley/API/Public/Conversation.hs | 3 +- services/galley/src/Galley/API/Public/MLS.hs | 3 +- services/galley/src/Galley/API/Query.hs | 150 ++++++++-- services/galley/test/integration/API.hs | 48 +--- .../galley/test/integration/API/Federation.hs | 4 +- services/galley/test/integration/API/Util.hs | 4 +- .../{ => backendA}/ecdsa_secp256r1_sha256.pem | 0 .../{ => backendA}/ecdsa_secp384r1_sha384.pem | 0 .../{ => backendA}/ecdsa_secp521r1_sha512.pem | 0 .../test/resources/{ => backendA}/ed25519.pem | 0 .../backendB/ecdsa_secp256r1_sha256.pem | 5 + .../backendB/ecdsa_secp384r1_sha384.pem | 6 + .../backendB/ecdsa_secp521r1_sha512.pem | 8 + .../test/resources/backendB/ed25519.pem | 3 + .../dynBackend1/ecdsa_secp256r1_sha256.pem | 5 + .../dynBackend1/ecdsa_secp384r1_sha384.pem | 6 + .../dynBackend1/ecdsa_secp521r1_sha512.pem | 8 + .../test/resources/dynBackend1/ed25519.pem | 3 + .../dynBackend2/ecdsa_secp256r1_sha256.pem | 5 + .../dynBackend2/ecdsa_secp384r1_sha384.pem | 6 + .../dynBackend2/ecdsa_secp521r1_sha512.pem | 8 + .../test/resources/dynBackend2/ed25519.pem | 3 + .../dynBackend3/ecdsa_secp256r1_sha256.pem | 5 + .../dynBackend3/ecdsa_secp384r1_sha384.pem | 6 + .../dynBackend3/ecdsa_secp521r1_sha512.pem | 8 + .../test/resources/dynBackend3/ed25519.pem | 3 + services/galley/test/resources/foo.sh | 4 + .../galley/test/unit/Test/Galley/Mapping.hs | 2 +- services/integration.yaml | 18 ++ 77 files changed, 1295 insertions(+), 209 deletions(-) create mode 100644 changelog.d/1-api-changes/one2one create mode 100644 hack/helm_vars/wire-server/values-domain1.yaml.gotmpl create mode 100644 hack/helm_vars/wire-server/values-domain2.yaml.gotmpl create mode 100644 libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GetOne2OneConversationResponse.hs create mode 100644 libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseBackendMismatch.json create mode 100644 libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseNotConnected.json create mode 100644 libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseOk.json create mode 100644 libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2BackendMismatch.json create mode 100644 libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2NotConnected.json create mode 100644 libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2Ok.json rename services/galley/test/resources/{ => backendA}/ecdsa_secp256r1_sha256.pem (100%) rename services/galley/test/resources/{ => backendA}/ecdsa_secp384r1_sha384.pem (100%) rename services/galley/test/resources/{ => backendA}/ecdsa_secp521r1_sha512.pem (100%) rename services/galley/test/resources/{ => backendA}/ed25519.pem (100%) create mode 100644 services/galley/test/resources/backendB/ecdsa_secp256r1_sha256.pem create mode 100644 services/galley/test/resources/backendB/ecdsa_secp384r1_sha384.pem create mode 100644 services/galley/test/resources/backendB/ecdsa_secp521r1_sha512.pem create mode 100644 services/galley/test/resources/backendB/ed25519.pem create mode 100644 services/galley/test/resources/dynBackend1/ecdsa_secp256r1_sha256.pem create mode 100644 services/galley/test/resources/dynBackend1/ecdsa_secp384r1_sha384.pem create mode 100644 services/galley/test/resources/dynBackend1/ecdsa_secp521r1_sha512.pem create mode 100644 services/galley/test/resources/dynBackend1/ed25519.pem create mode 100644 services/galley/test/resources/dynBackend2/ecdsa_secp256r1_sha256.pem create mode 100644 services/galley/test/resources/dynBackend2/ecdsa_secp384r1_sha384.pem create mode 100644 services/galley/test/resources/dynBackend2/ecdsa_secp521r1_sha512.pem create mode 100644 services/galley/test/resources/dynBackend2/ed25519.pem create mode 100644 services/galley/test/resources/dynBackend3/ecdsa_secp256r1_sha256.pem create mode 100644 services/galley/test/resources/dynBackend3/ecdsa_secp384r1_sha384.pem create mode 100644 services/galley/test/resources/dynBackend3/ecdsa_secp521r1_sha512.pem create mode 100644 services/galley/test/resources/dynBackend3/ed25519.pem create mode 100755 services/galley/test/resources/foo.sh diff --git a/changelog.d/1-api-changes/jwk b/changelog.d/1-api-changes/jwk index 6c9fd0e647f..a7333811d14 100644 --- a/changelog.d/1-api-changes/jwk +++ b/changelog.d/1-api-changes/jwk @@ -1 +1 @@ -The `mls/public-key` endpoint now takes a `format` query parameter which can be either `raw` (default, for raw base64-encoded keys) or `jwk` (for JWK keys) +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 new file mode 100644 index 00000000000..c22c02444c3 --- /dev/null +++ b/changelog.d/1-api-changes/one2one @@ -0,0 +1 @@ +`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/charts/integration/templates/configmap.yaml b/charts/integration/templates/configmap.yaml index 42297c017c6..77177433dee 100644 --- a/charts/integration/templates/configmap.yaml +++ b/charts/integration/templates/configmap.yaml @@ -6,6 +6,12 @@ metadata: "helm.sh/hook": post-install "helm.sh/hook-delete-policy": before-hook-creation data: + {{- range $name, $dynamicBackend := .Values.config.dynamicBackends }} + {{ $name }}-mls-removal-key-ed25519.pem: {{ $dynamicBackend.mlsPrivateKeys.removal.ed25519 | quote }} + {{ $name }}-mls-removal-key-ecdsa_secp256r1_sha256.pem: {{ $dynamicBackend.mlsPrivateKeys.removal.ecdsa_secp256r1_sha256 | quote }} + {{ $name }}-mls-removal-key-ecdsa_secp384r1_sha384.pem: {{ $dynamicBackend.mlsPrivateKeys.removal.ecdsa_secp384r1_sha384 | quote }} + {{ $name }}-mls-removal-key-ecdsa_secp521r1_sha512.pem: {{ $dynamicBackend.mlsPrivateKeys.removal.ecdsa_secp521r1_sha512 | quote }} + {{- end }} integration.yaml: | brig: host: brig.{{ .Release.Namespace }}.svc.cluster.local @@ -118,6 +124,12 @@ data: {{ $name }}: domain: {{ $dynamicBackend.federatorExternalHostPrefix }}.{{ $.Release.Namespace }}.svc.cluster.local federatorExternalPort: {{ $dynamicBackend.federatorExternalPort }} + mlsPrivateKeyPaths: + removal: + ed25519: "/etc/wire/integration/{{ $name }}-mls-removal-key-ed25519.pem" + ecdsa_secp256r1_sha256: "/etc/wire/integration/{{ $name }}-mls-removal-key-ecdsa_secp256r1_sha256.pem" + ecdsa_secp384r1_sha384: "/etc/wire/integration/{{ $name }}-mls-removal-key-ecdsa_secp384r1_sha384.pem" + ecdsa_secp521r1_sha512: "/etc/wire/integration/{{ $name }}-mls-removal-key-ecdsa_secp521r1_sha512.pem" {{- end }} cassandra: host: {{ .Values.config.cassandra.host }} diff --git a/charts/integration/values.yaml b/charts/integration/values.yaml index f1310f8fa4e..36305b2be75 100644 --- a/charts/integration/values.yaml +++ b/charts/integration/values.yaml @@ -17,13 +17,96 @@ config: dynamic-backend-1: federatorExternalHostPrefix: dynamic-backend-1 federatorExternalPort: 10098 + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIJrGRHzIwjc5byivY2l+/MqbH3ty1yetYG8d5p4GGHhk + -----END PRIVATE KEY----- + ecdsa_secp256r1_sha256: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaeLidXfwi/RVvWZ4 + OHoQhicePLIfyDZI7gMVsyXtec6hRANCAARM6EWywmjaCXtvsQ1M2edrbMescC+j + GSIhBrlE7igzhookThDBvOGAL67vf8xz+hw7tE8NqfzbdJQBL8NQik2L + -----END PRIVATE KEY----- + ecdsa_secp384r1_sha384: | + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDD1rK50pFsZmOomBiNQ + QFRRwAmed8Ox+nFseYbKzjLIAgWH0sMQ5DU8SAK8ks+GROShZANiAATyX0XQ6x6A + pi+HKz+ReWV9iIUOttxJv9u2aTY5ZrQ42IJs3fV1AGz1BE52uDvhbILOD9WfqZ9d + 6MqCjF6OqYT9nmnPkQ+CKC2XPzSVBpqJtuHXiMfFrc7n05E8CdIHOkI= + -----END PRIVATE KEY----- + ecdsa_secp521r1_sha512: | + -----BEGIN PRIVATE KEY----- + MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAQnVcp85/mC6r91yB + XjhiHOp4j450UcThSmDBdva4Pj9ihXvAScEFabus7CeECvUT3auqXKY9iSR45vQq + JuFI/0uhgYkDgYYABAG1paU01rRuYG4K2PWaIIbB9RuiYg5GVsu5mu6VHjYEH+7c + 1AGuCPEsUoM542cn3T1utv0EMtoj4yFPvf0xBs7AowHW04JsgMFzpWm8T1e/91n1 + IEkT5xOnq8obn7p4je9Ui95ojEA/n49gsTKsuO1qv2n79PnStLfn2yT5lAtcTcva + 6Q== + -----END PRIVATE KEY----- dynamic-backend-2: federatorExternalHostPrefix: dynamic-backend-2 federatorExternalPort: 11098 + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIDgG4Dhqfq6KRyGKtEFiPeP+Nq1DBsTY31q3f/tC/lnk + -----END PRIVATE KEY----- + ecdsa_secp256r1_sha256: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgz0IEyU4GYrpkVH2y + iR87BMD1VAfBgl69WedewvA/Vl+hRANCAASTYYD2BF2E1zqPKYZtpHW1quo+YBsv + SAeznMX1bOeoOLD8zyFDHEGb3I9S90iGjYKTUogY+QfbbiqAiBIuSig7 + -----END PRIVATE KEY----- + ecdsa_secp384r1_sha384: | + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDA43epgPhtj4s0G3aXQ + TPXjnQHhiQ7Hfze+K8HgDSUL+Ds31v+g+Ko/OZrAA7povdWhZANiAATXd/dKoFvA + wlISC4MAbBsDV6g2oezzZt0nXUq4uysANJ24s+BNey7tYpB36qAOUhqmCzJW5IFJ + 22ttorUXSTaJeUIUdRiwD7xJ54z3NV5Wj8CUskvp0DIf/ILkOpbxdQY= + -----END PRIVATE KEY----- + ecdsa_secp521r1_sha512: | + -----BEGIN PRIVATE KEY----- + MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAjGOdWinAUUopQCYW + 6Ch4UuwdHhTERbUS90bQiQyoPdnTrTT8+NsYsB8DmPLltxls6h28q0IGCKUmO9ph + 8gFT0l6hgYkDgYYABAD3l73lFiVckI4V8BhR2x83o44dhjZA26d8SVSUBt9iuRbR + Lh0vP+zghhDQZLFLpfcL0Fo0K9H4HdQwe2cMxbOyQwDUC76ot9BdZjfsjKiRK6+k + ZNlnHSWx15yg8gF0dpt2eVn1LBLB0JvRcauYVMfKNox1IU8DY0ZiuO4DJNXRDVEI + 7w== + -----END PRIVATE KEY----- dynamic-backend-3: federatorExternalHostPrefix: dynamic-backend-3 federatorExternalPort: 12098 - + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIB/Jddpef01pYWQXUEFmJ+k6dDQE7fVSKfk7/AyQaOnU + -----END PRIVATE KEY----- + ecdsa_secp256r1_sha256: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgIkeTuHoMtzsuaN3f + zug+mp/IlejrG1W4z/lOU9yGNqKhRANCAASLReHnUMJfSs0pDFxVYIgCOThRsiCD + Fq/6oKzWYnvX+taJgNUCVm7QND7Q9ll+Vy4ymZmE9YH1QuNW4FbVe1X6 + -----END PRIVATE KEY----- + ecdsa_secp384r1_sha384: | + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB0dp/epJB3XhCeRcYJ + C76Ll50HHb/H2GR/UBFyAWYtQ8mbaXWis8NPjvYmqrqd5VWhZANiAARoOjIYWdAP + Y910LsLGcihvmnoFx7atJbOhaGTem57P/DOkYqcYohUcz6WaCuqzk/ZEj8NZtdvF + 4AYt0mnxkl9L5pt2a6i2HWW+4puR+JMmWD9qj0lRc5AQeEtmbuohIfg= + -----END PRIVATE KEY----- + ecdsa_secp521r1_sha512: | + -----BEGIN PRIVATE KEY----- + MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBfyz5LCeeA0seQo1O + jlQiUKxL4tWX23mD5G5Y5nra3Ju/7mNYp/sIX5BS81iWno5N6KfEdgUtgEffa4Xj + nuyF2QqhgYkDgYYABAF3eFOMjqpO7hDdVua9WgquGdFRRd3LWLhY0fyeyiQn/7yr + vLIb01f8dX9UVFKMxw77ZMMcfF+uW5Enxa8kadDcmwHIiIh/6jW0oGlFxkmwmecr + MwfpR6lZMbtQMD4rm8AwQAsFCBCRyPyK8bWanzMYusbnCdS/nBB5YB8x0ejjYFlU + RQ== + -----END PRIVATE KEY----- cassandra: host: cassandra-ephemeral port: 9042 diff --git a/hack/helm_vars/wire-server/values-domain1.yaml.gotmpl b/hack/helm_vars/wire-server/values-domain1.yaml.gotmpl new file mode 100644 index 00000000000..65bc78ca64a --- /dev/null +++ b/hack/helm_vars/wire-server/values-domain1.yaml.gotmpl @@ -0,0 +1,30 @@ +galley: + secrets: + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIAocCDXsKIAjb65gOUn5vEF0RIKnVJkKR4ebQzuZ709c + -----END PRIVATE KEY----- + ecdsa_secp256r1_sha256: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3qjgQ9U+/rTBObn9 + tXSVi2UtHksRDXmQ1VOszFZfjryhRANCAATNkLmZZLyORf5D3PUOxt+rkJTE5vuD + aCqZ7sE5NSN8InRRwuQ1kv0oblDVeQA89ZlHqyxx75JPK+/air7Z1n5I + -----END PRIVATE KEY----- + ecdsa_secp384r1_sha384: | + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBLwv3i5LDz9b++O0iw + QAit/Uq7L5PWPgKN99wCm8xkZnuyqWujXW4wvlVUVlZWgh2hZANiAAT0+RXKE31c + VxdYazaVopY50/nV9c18uRdqoENBvtxuD6oDtJtU6oCS/Htkd8JEArTQ9ZHqq144 + yRjuc3d2CqvJmEA/lzIBk9wnz+lghFhvB4TkSHvvLyEBc9DZvhb4EEQ= + -----END PRIVATE KEY----- + ecdsa_secp521r1_sha512: | + -----BEGIN PRIVATE KEY----- + MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBiaEARm5BMaRct1xj + MlemUHijWGAoHtNMhSttSr4jo0WxMwfMnvnDQJSlO2Zs4Tzum2j5eO34EHu6MUrv + qquZYwyhgYkDgYYABAHuvCV/+gJitvAbDwgrBHZJ41oy8Lc+wPIM7Yp6s/vTzTsG + Klo7aMdkx6DUjv/56tVD9bZNulFAjwS8xoIyWg8NSAE1ofo8CBvN1XGZOWuMYjEh + zLrZADduEnOvayw5sEvm135WC0vWjPJaYwKZPdDIXUz9ILJPgNe3gEUvHsDEXvdX + lw== + -----END PRIVATE KEY----- diff --git a/hack/helm_vars/wire-server/values-domain2.yaml.gotmpl b/hack/helm_vars/wire-server/values-domain2.yaml.gotmpl new file mode 100644 index 00000000000..0c317f59726 --- /dev/null +++ b/hack/helm_vars/wire-server/values-domain2.yaml.gotmpl @@ -0,0 +1,30 @@ +galley: + secrets: + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEINsoqzdpVTv5+odHwwGO1I+Kp1+T24p7URvq50n79iCJ + -----END PRIVATE KEY----- + ecdsa_secp256r1_sha256: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgl2RK2WydNOp7ViAB + 5mhF4N0HdmJQ89f4YtxiCE252LehRANCAASJgocuA+eIaebS+M6t6ouQT3LzObg3 + XkWNvPZWE/4wsm6FAZ7ulLKU02AumSUx4u71d/1x9epAHJyc+RdACUQt + -----END PRIVATE KEY----- + ecdsa_secp384r1_sha384: | + -----BEGIN PRIVATE KEY----- + MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDD/lnMI4VPb7VchrKHj + hiE6ntS71rVDXps8UdnHenKQhBccRlqznBV6vP5QbcKVQcqhZANiAAR3yJC8eV/G + tC6ZDk5uMNaiqlmwVzH0mRNiJPGShVHfL3rBFq99sf3nOTs4v79PXajGwFJXppJC + /TQc7PMy2IbTor5tdWjcNBgSZiPn734IACuLpExqvsPuD6MlV2aHmXQ= + -----END PRIVATE KEY----- + ecdsa_secp521r1_sha512: | + -----BEGIN PRIVATE KEY----- + MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAcHs/PncuxyMlECqE + DeUHeqbd3aktlnenf8q0vemi4DSebEEp3ONCG7ZfFQFZyp5aZQWgZaOj1pwGDzth + FmmOFwShgYkDgYYABAF5QeG/mdn2MNuaHzDc+/6UbAfsb+ddighFuxqobl4731w2 + 2myXfvFGcseoKjymDe8kuv1a4eDmLzLkrUGNZhWIfQD7CP0j0JmBNzVnYAKlRcOd + SU5XbMx1q7oyaiQ51B47IObxT8sVKZzbnE3qZa060cAglu4G0OS3OlJzVOinBAfA + 5w== + -----END PRIVATE KEY----- diff --git a/hack/helmfile.yaml b/hack/helmfile.yaml index a7ed6861883..3581c373a78 100644 --- a/hack/helmfile.yaml +++ b/hack/helmfile.yaml @@ -221,6 +221,7 @@ releases: chart: '../.local/charts/wire-server' values: - './helm_vars/wire-server/values.yaml.gotmpl' + - './helm_vars/wire-server/values-domain1.yaml.gotmpl' set: - name: brig.config.optSettings.setFederationDomain value: {{ .Values.federationDomain1 }} @@ -236,6 +237,7 @@ releases: chart: '../.local/charts/wire-server' values: - './helm_vars/wire-server/values.yaml.gotmpl' + - './helm_vars/wire-server/values-domain2.yaml.gotmpl' set: - name: brig.config.optSettings.setFederationDomain value: {{ .Values.federationDomain2 }} diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 684da6542f3..f5e753cf88b 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -221,7 +221,14 @@ createSubConv cid subId = do resetGroup cid sub void $ createPendingProposalCommit cid >>= sendAndConsumeCommitBundle -resetGroup :: (MakesValue conv) => ClientIdentity -> conv -> App () +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 @@ -233,12 +240,31 @@ resetGroup cid conv = do epoch = 0, newMembers = mempty } - resetClientGroup cid groupId convId + 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") + +-- | 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 + 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 :: (MakesValue conv) => ClientIdentity -> String -> conv -> App () -resetClientGroup cid gid conv = do +resetClientGroup :: (HasCallStack, MakesValue keys) => ClientIdentity -> String -> keys -> App () +resetClientGroup cid gid keys = do mls <- getMLSState - keys <- withAPIVersion 5 $ getMLSPublicKeys conv >>= getJSON 200 removalKey <- asByteString $ keys %. ("removal." <> csSignatureScheme mls.ciphersuite) void $ mlscli diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 796e6786429..2efb0aa8740 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -174,13 +174,22 @@ addUserToTeam u = do -- | Create a user on the given domain, such that the 1-1 conversation with -- 'other' resides on 'convDomain'. This connects the two users as a side-effect. -createMLSOne2OnePartner :: (MakesValue user) => Domain -> user -> Domain -> App Value +createMLSOne2OnePartner :: + (MakesValue user, MakesValue domain, MakesValue convDomain, HasCallStack) => + domain -> + user -> + convDomain -> + App Value createMLSOne2OnePartner domain other convDomain = loop where loop = do u <- randomUser domain def connectTwoUsers u other - conv <- getMLSOne2OneConversation other u >>= getJSON 200 + apiVersion <- getAPIVersionFor domain + conv <- + if apiVersion < 6 + then getMLSOne2OneConversation other u >>= getJSON 200 + else getMLSOne2OneConversation other u >>= getJSON 200 >>= (%. "conversation") desiredConvDomain <- make convDomain & asString actualConvDomain <- conv %. "qualified_id.domain" & asString diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 07534701b85..91b11cb04e0 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -430,10 +430,47 @@ testRemoteRemoveClient suite = do shouldMatch (nPayload n %. "conversation") (objId conv) shouldMatch (nPayload n %. "from") (objId bob) - msg <- asByteString (nPayload n %. "data") >>= showMessage alice1 + mlsMsg <- asByteString (nPayload n %. "data") + + -- Checks that the remove proposal is consumable by alice + void $ mlsCliConsume 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 let leafIndexBob = 1 - msg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob - msg %. "message.content.sender.External" `shouldMatchInt` 0 + 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 + + 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 %. "from") (objId alice) + + mlsMsg <- asByteString (nPayload n %. "data") + + -- Checks that the remove proposal is consumable by alice + void $ mlsCliConsume 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 + 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 diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index cc15d1f0d8f..5c11247ebec 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -31,26 +31,26 @@ import Notifications import SetupHelpers import Test.Version import Testlib.Prelude +import Testlib.VersionedFed -testGetMLSOne2One :: (HasCallStack) => Version5 -> Domain -> App () -testGetMLSOne2One v otherDomain = withVersion5 v $ do - [alice, bob] <- createAndConnectUsers [OwnDomain, otherDomain] - +testGetMLSOne2OneLocalV5 :: (HasCallStack) => App () +testGetMLSOne2OneLocalV5 = withVersion5 Version5 $ do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] let assertConvData conv = do conv %. "epoch" `shouldMatchInt` 0 - case v of - Version5 -> conv %. "cipher_suite" `shouldMatchInt` 1 - NoVersion5 -> assertFieldMissing conv "cipher_suite" + conv %. "cipher_suite" `shouldMatchInt` 1 - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - conv %. "type" `shouldMatchInt` 2 - shouldBeEmpty (conv %. "members.others") + convId <- + getMLSOne2OneConversation alice bob `bindResponse` \resp -> do + conv <- getJSON 200 resp + conv %. "type" `shouldMatchInt` 2 + shouldBeEmpty (conv %. "members.others") - conv %. "members.self.conversation_role" `shouldMatch` "wire_member" - conv %. "members.self.qualified_id" `shouldMatch` (alice %. "qualified_id") - assertConvData conv + conv %. "members.self.conversation_role" `shouldMatch` "wire_member" + conv %. "members.self.qualified_id" `shouldMatch` (alice %. "qualified_id") + assertConvData conv - convId <- conv %. "qualified_id" + conv %. "qualified_id" -- check that the conversation has the same ID on the other side conv2 <- bindResponse (getMLSOne2OneConversation bob alice) $ \resp -> do @@ -61,21 +61,69 @@ testGetMLSOne2One v otherDomain = withVersion5 v $ do conv2 %. "qualified_id" `shouldMatch` convId assertConvData conv2 +testGetMLSOne2OneRemoteV5 :: (HasCallStack) => App () +testGetMLSOne2OneRemoteV5 = withVersion5 Version5 $ do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + getMLSOne2OneConversation alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.jsonBody %. "label" `shouldMatch` "mls-federated-one2one-not-supported" + + getMLSOne2OneConversation bob alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 400 + resp.jsonBody %. "label" `shouldMatch` "mls-federated-one2one-not-supported" + +testGetMLSOne2One :: (HasCallStack) => Domain -> App () +testGetMLSOne2One bobDomain = do + [alice, bob] <- createAndConnectUsers [OwnDomain, bobDomain] + bobDomainStr <- asString bobDomain + let assertConvData conv = do + conv %. "epoch" `shouldMatchInt` 0 + assertFieldMissing conv "cipher_suite" + + mlsOne2OneConv <- + getMLSOne2OneConversation alice bob `bindResponse` \resp -> do + one2oneConv <- getJSON 200 resp + convOwnerDomain <- asString $ one2oneConv %. "conversation.qualified_id.domain" + let user = if convOwnerDomain == bobDomainStr then bob else alice + ownerDomainPublicKeys <- getMLSPublicKeys user >>= getJSON 200 + + one2oneConv %. "public_keys" `shouldMatch` ownerDomainPublicKeys + + conv <- one2oneConv %. "conversation" + conv %. "type" `shouldMatchInt` 2 + shouldBeEmpty (conv %. "members.others") + conv %. "members.self.conversation_role" `shouldMatch` "wire_member" + conv %. "members.self.qualified_id" `shouldMatch` (alice %. "qualified_id") + assertConvData conv + + pure one2oneConv + + -- check that the conversation has the same ID on the other side + mlsOne2OneConv2 <- bindResponse (getMLSOne2OneConversation bob alice) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json + + conv2 <- mlsOne2OneConv2 %. "conversation" + conv2 %. "type" `shouldMatchInt` 2 + conv2 %. "qualified_id" `shouldMatch` (mlsOne2OneConv %. "conversation.qualified_id") + mlsOne2OneConv2 %. "public_keys" `shouldMatch` (mlsOne2OneConv %. "public_keys") + assertConvData conv2 + testMLSOne2OneOtherMember :: (HasCallStack) => One2OneScenario -> App () testMLSOne2OneOtherMember scenario = do alice <- randomUser OwnDomain def let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 do - convId <- conv %. "qualified_id" - bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 - convId `shouldMatch` (bobConv %. "qualified_id") + 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] - resetGroup alice1 conv + resetOne2OneGroup alice1 one2OneConv withWebSocket bob1 $ \ws -> do commit <- createAddCommit alice1 [bob] void $ sendAndConsumeCommitBundle commit @@ -85,14 +133,47 @@ testMLSOne2OneOtherMember scenario = do -- Make sure the membership info is OK both for the MLS 1-to-1 endpoint and -- for the general conversation fetching endpoint. - let assertOthers other resp = do - bdy <- getJSON 200 resp - othersObj <- bdy %. "members.others" & asList + let assertOthers :: (HasCallStack, MakesValue other, MakesValue retrievedConv) => other -> retrievedConv -> App () + assertOthers other retrievedConv = do + othersObj <- retrievedConv %. "members.others" & asList otherActual <- assertOne othersObj otherActual %. "qualified_id" `shouldMatch` (other %. "qualified_id") forM_ [(alice, bob), (bob, alice)] $ \(self, other) -> do - getMLSOne2OneConversation self other `bindResponse` assertOthers other - getConversation self conv `bindResponse` assertOthers other + getMLSOne2OneConversation self other `bindResponse` \resp -> do + retrievedConv <- getJSON 200 resp >>= (%. "conversation") + assertOthers other retrievedConv + getConversation self (one2OneConv %. "conversation") `bindResponse` \resp -> do + retrievedConv <- getJSON 200 resp + assertOthers other retrievedConv + +testMLSOne2OneRemoveClientLocalV5 :: App () +testMLSOne2OneRemoveClientLocalV5 = withVersion5 Version5 $ do + [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] + conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + traverse_ uploadNewKeyPackage [bob1] + resetGroup alice1 conv + + void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle + + withWebSocket alice $ \wsAlice -> do + _ <- 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 %. "from") (objId bob) + + mlsMsg <- asByteString (nPayload n %. "data") + + -- Checks that the remove proposal is consumable by alice + void $ mlsCliConsume alice1 mlsMsg + + parsedMsg <- showMessage alice1 mlsMsg + let leafIndexBob = 1 + -- msg `shouldMatch` "foo" + parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexBob + parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 testGetMLSOne2OneUnconnected :: (HasCallStack) => Domain -> App () testGetMLSOne2OneUnconnected otherDomain = do @@ -116,15 +197,15 @@ testMLSOne2OneBlockedAfterConnected scenario = do let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - convId <- conv %. "qualified_id" + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + convId <- one2OneConv %. "conversation.qualified_id" do bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 - convId `shouldMatch` (bobConv %. "qualified_id") + convId `shouldMatch` (bobConv %. "conversation.qualified_id") [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [bob1] - resetGroup alice1 conv + resetOne2OneGroup alice1 one2OneConv commit <- createAddCommit alice1 [bob] withWebSocket bob1 $ \ws -> do void $ sendAndConsumeCommitBundle commit @@ -155,15 +236,15 @@ testMLSOne2OneUnblocked scenario = do let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 do - convId <- conv %. "qualified_id" + convId <- one2OneConv %. "conversation.qualified_id" bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 - convId `shouldMatch` (bobConv %. "qualified_id") + convId `shouldMatch` (bobConv %. "conversation.qualified_id") [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [bob1] - resetGroup alice1 conv + resetOne2OneGroup alice1 one2OneConv withWebSocket bob1 $ \ws -> do commit <- createAddCommit alice1 [bob] void $ sendAndConsumeCommitBundle commit @@ -243,8 +324,8 @@ testMLSOne2One suite scenario = do [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [bob1] - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetGroup alice1 conv + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetOne2OneGroup alice1 one2OneConv commit <- createAddCommit alice1 [bob] withWebSocket bob1 $ \ws -> do @@ -267,9 +348,9 @@ testMLSOne2One suite scenario = do -- the cipersuite of this conversation. void $ createPendingProposalCommit alice1 >>= sendAndConsumeCommitBundle - conv' <- getMLSOne2OneConversation alice bob >>= getJSON 200 + one2OneConv' <- getMLSOne2OneConversation alice bob >>= getJSON 200 (suiteCode, _) <- assertOne $ T.hexadecimal (T.pack suite.code) - conv' %. "cipher_suite" `shouldMatchInt` suiteCode + one2OneConv' %. "conversation.cipher_suite" `shouldMatchInt` suiteCode -- | This test verifies that one-to-one conversations are created inside the -- commit lock. There used to be an issue where a conversation could be @@ -281,15 +362,16 @@ testMLSGhostOne2OneConv = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetGroup alice1 conv + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetOne2OneGroup alice1 one2OneConv doneVar <- liftIO $ newEmptyMVar let checkConversation = liftIO (tryReadMVar doneVar) >>= \case Nothing -> do - bindResponse (getConversation alice conv) $ \resp -> + bindResponse (getConversation alice (one2OneConv %. "conversation")) $ \resp -> resp.status `shouldMatchOneOf` [404 :: Int, 403, 200] + checkConversation Just _ -> pure () checkConversationIO <- appToIO checkConversation @@ -304,3 +386,114 @@ testMLSGhostOne2OneConv = do createCommit liftIO $ putMVar doneVar () wait a + +-- [NOTE: Federated 1:1 MLS Conversations] +-- 1:1 Conversations shouldn't work when there is no way for the creator to know +-- the MLS public keys of the backend which will host this conversation. In +-- federation API V2, this will always work and has been tested above. When one +-- of the backends doesn't support federation API v2, the 1:1 conversation can +-- still be created but only by the user whose backend hosts this conversation. + +-- | See Note: [Federated 1:1 MLS Conversations] +testMLSFederationV1ConvOnOldBackend :: App () +testMLSFederationV1ConvOnOldBackend = do + alice <- randomUser OwnDomain def + let createBob = do + bobCandidate <- randomUser (StaticFedDomain 1) def + connectUsers [alice, bobCandidate] + getMLSOne2OneConversation alice bobCandidate `bindResponse` \resp -> do + if resp.status == 533 + then pure bobCandidate + else createBob + + bob <- createBob + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + traverse_ uploadNewKeyPackage [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. + getMLSOne2OneConversation alice bob `bindResponse` \resp -> do + fedError <- getJSON 533 resp + fedError %. "label" `shouldMatch` "federation-version-error" + + conv <- getMLSOne2OneConversation bob alice >>= getJSON 200 + keys <- getMLSPublicKeys bob >>= getJSON 200 + resetOne2OneGroupGeneric bob1 conv keys + + withWebSocket alice1 $ \wsAlice -> do + commit <- createAddCommit bob1 [alice] + void $ sendAndConsumeCommitBundle commit + + let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" + n <- awaitMatch isMessage wsAlice + nPayload n %. "data" `shouldMatch` B8.unpack (Base64.encode (fold commit.welcome)) + + withWebSocket bob1 $ \wsBob -> do + _ <- 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 %. "from") (objId alice) + + mlsMsg <- asByteString (nPayload n %. "data") + + -- Checks that the remove proposal is consumable by bob + void $ mlsCliConsume bob1 mlsMsg + + parsedMsg <- showMessage bob1 mlsMsg + let leafIndexAlice = 1 + parsedMsg %. "message.content.body.Proposal.Remove.removed" `shouldMatchInt` leafIndexAlice + parsedMsg %. "message.content.sender.External" `shouldMatchInt` 0 + +-- | See Note: Federated 1:1 MLS Conversations +testMLSFederationV1ConvOnNewBackend :: App () +testMLSFederationV1ConvOnNewBackend = do + alice <- randomUser OwnDomain def + let createBob = do + bobCandidate <- randomUser (StaticFedDomain 1) def + connectUsers [alice, bobCandidate] + getMLSOne2OneConversation alice bobCandidate `bindResponse` \resp -> do + if resp.status == 200 + then pure bobCandidate + else createBob + + bob <- createBob + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + traverse_ uploadNewKeyPackage [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. + getMLSOne2OneConversation bob alice `bindResponse` \resp -> do + fedError <- getJSON 533 resp + fedError %. "label" `shouldMatch` "federation-remote-error" + + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + conv <- one2OneConv %. "conversation" + resetOne2OneGroup alice1 one2OneConv + + withWebSocket bob1 $ \wsBob -> do + commit <- createAddCommit alice1 [bob] + void $ sendAndConsumeCommitBundle commit + + let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" + n <- awaitMatch isMessage wsBob + nPayload n %. "data" `shouldMatch` B8.unpack (Base64.encode (fold commit.welcome)) + + withWebSocket alice1 $ \wsAlice -> do + _ <- 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 %. "from") (objId bob) + + mlsMsg <- asByteString (nPayload n %. "data") + + -- Checks that the remove proposal is consumable by bob + void $ mlsCliConsume alice1 mlsMsg + + parsedMsg <- showMessage 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 db13018017b..83c5376edf3 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -36,14 +36,14 @@ testJoinOne2OneSubConv = do [alice, bob] <- createAndConnectUsers [OwnDomain, OwnDomain] [alice1, bob1, bob2] <- traverse (createMLSClient def) [alice, bob, bob] traverse_ uploadNewKeyPackage [bob1, bob2] - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetGroup alice1 conv + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetOne2OneGroup alice1 one2OneConv void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle - createSubConv bob1 "conference" + createOne2OneSubConv bob1 "conference" (one2OneConv %. "public_keys") -- bob adds his first client to the subconversation - sub' <- getSubConversation bob conv "conference" >>= getJSON 200 + sub' <- getSubConversation bob (one2OneConv %. "conversation") "conference" >>= getJSON 200 do tm <- sub' %. "epoch_timestamp" assertBool "Epoch timestamp should not be null" (tm /= Null) @@ -62,28 +62,28 @@ testLeaveOne2OneSubConv scenario leaver = do bob <- createMLSOne2OnePartner otherDomain alice convDomain [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] traverse_ uploadNewKeyPackage [bob1] - conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 - resetGroup alice1 conv + one2OneConv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + resetOne2OneGroup alice1 one2OneConv void $ createAddCommit alice1 [bob] >>= sendAndConsumeCommitBundle -- create and join subconversation - createSubConv alice1 "conference" + createOne2OneSubConv alice1 "conference" (one2OneConv %. "public_keys") void $ createExternalCommit bob1 Nothing >>= sendAndConsumeCommitBundle -- one of the two clients leaves - let (leaverClient, leaverIndex, otherClient) = case leaver of + let (leaverClient, leaverIndex, remainingClient) = case leaver of Alice -> (alice1, 0, bob1) Bob -> (bob1, 1, alice1) - withWebSocket otherClient $ \ws -> do + withWebSocket remainingClient $ \ws -> do leaveCurrentConv leaverClient - msg <- consumeMessage otherClient Nothing ws + msg <- consumeMessage 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 otherClient >>= sendAndConsumeCommitBundle + void $ createPendingProposalCommit remainingClient >>= sendAndConsumeCommitBundle testDeleteParentOfSubConv :: (HasCallStack) => Domain -> App () testDeleteParentOfSubConv secondDomain = do diff --git a/integration/test/Test/User.hs b/integration/test/Test/User.hs index e1400fe254d..e0429253875 100644 --- a/integration/test/Test/User.hs +++ b/integration/test/Test/User.hs @@ -13,7 +13,7 @@ import SetupHelpers import Testlib.Prelude import Testlib.VersionedFed -testSupportedProtocols :: (HasCallStack) => OneOf Domain AnyFedDomain -> App () +testSupportedProtocols :: (HasCallStack) => OneOf Domain (FedDomain 1) -> App () testSupportedProtocols bobDomain = do alice <- randomUser OwnDomain def alice %. "supported_protocols" `shouldMatchSet` ["proteus"] diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 7c9bd150e9b..b5611178b6f 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -143,11 +143,13 @@ mkEnv ge = do federationV1Domain = gFederationV1Domain ge, dynamicDomains = gDynamicDomains ge, defaultAPIVersion = gDefaultAPIVersion ge, - -- hardcode version 5 for fed 0 backend + -- hardcode API versions for federated domains because they don't have + -- latest things. Ensure we do not use development API versions in + -- those domains. apiVersionByDomain = Map.fromList - [ (gFederationV0Domain ge, 5), - (gFederationV1Domain ge, 6) + [ (gFederationV0Domain ge, 4), + (gFederationV1Domain ge, 5) ], manager = gManager ge, servicesCwdBase = gServicesCwdBase ge, diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index 2cb0158c16f..f9f495abc96 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -145,11 +145,7 @@ rawBaseRequest domain service versioned path = do pathSegsPrefix <- case versioned of Versioned -> do - d <- asString domainV - versionMap <- asks (.apiVersionByDomain) - v <- case Map.lookup d versionMap of - Nothing -> asks (.defaultAPIVersion) - Just v -> pure v + v <- getAPIVersionFor domainV pure ["v" <> show v] Unversioned -> pure [] ExplicitVersion v -> do @@ -161,6 +157,14 @@ rawBaseRequest domain service versioned path = do let HostPort h p = serviceHostPort serviceMap service in "http://" <> h <> ":" <> show p <> ("/" <> joinHttpPath (pathSegsPrefix <> splitHttpPath path)) +getAPIVersionFor :: (MakesValue domain) => domain -> App Int +getAPIVersionFor domain = do + d <- asString domain + versionMap <- asks (.apiVersionByDomain) + case Map.lookup d versionMap of + Nothing -> asks (.defaultAPIVersion) + Just v -> pure v + baseRequest :: (HasCallStack, MakesValue user) => user -> Service -> Versioned -> String -> App HTTP.Request baseRequest user service versioned path = do req <- rawBaseRequest user service versioned path diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 061acca529e..341c770e5fc 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -136,6 +136,7 @@ startDynamicBackend resource beOverrides = do setEsIndex, setFederationSettings, setAwsConfigs, + setMlsPrivateKeyPaths, setLogLevel, beOverrides ] @@ -200,6 +201,12 @@ startDynamicBackend resource beOverrides = do { brigCfg = setField "elasticsearch.index" resource.berElasticsearchIndex } + setMlsPrivateKeyPaths :: ServiceOverrides + setMlsPrivateKeyPaths = + def + { galleyCfg = setField "settings.mlsPrivateKeyPaths" resource.berMlsPrivateKeyPaths + } + setLogLevel :: ServiceOverrides setLogLevel = def diff --git a/integration/test/Testlib/ResourcePool.hs b/integration/test/Testlib/ResourcePool.hs index c67b7031e43..aa518939fef 100644 --- a/integration/test/Testlib/ResourcePool.hs +++ b/integration/test/Testlib/ResourcePool.hs @@ -15,6 +15,7 @@ import Control.Concurrent import Control.Monad.Catch import Control.Monad.Codensity import Control.Monad.IO.Class +import Data.Aeson import Data.Foldable (for_) import Data.Functor import Data.IORef @@ -124,7 +125,8 @@ backendResources dynConfs = berVHost = dynConf.domain, berNginzSslPort = Ports.portForDyn Ports.NginzSSL i, berNginzHttp2Port = Ports.portForDyn Ports.NginzHttp2 i, - berInternalServicePorts = Ports.internalServicePorts name + berInternalServicePorts = Ports.internalServicePorts name, + berMlsPrivateKeyPaths = dynConf.mlsPrivateKeyPaths } ) where @@ -154,7 +156,17 @@ backendA = berVHost = "backendA", berNginzSslPort = Ports.port Ports.NginzSSL BackendA, berInternalServicePorts = Ports.internalServicePorts BackendA, - berNginzHttp2Port = Ports.port Ports.NginzHttp2 BackendA + berNginzHttp2Port = Ports.port Ports.NginzHttp2 BackendA, + berMlsPrivateKeyPaths = + object + [ fromString "removal" + .= object + [ fromString "ed25519" .= "test/resources/backendA/ed25519.pem", + fromString "ecdsa_secp256r1_sha256" .= "test/resources/backendA/ecdsa_secp256r1_sha256.pem", + fromString "ecdsa_secp384r1_sha384" .= "test/resources/backendA/ecdsa_secp384r1_sha384.pem", + fromString "ecdsa_secp521r1_sha512" .= "test/resources/backendA/ecdsa_secp521r1_sha512.pem" + ] + ] } backendB :: BackendResource @@ -183,5 +195,15 @@ backendB = berVHost = "backendB", berNginzSslPort = Ports.port Ports.NginzSSL BackendB, berInternalServicePorts = Ports.internalServicePorts BackendB, - berNginzHttp2Port = Ports.port Ports.NginzHttp2 BackendB + berNginzHttp2Port = Ports.port Ports.NginzHttp2 BackendB, + berMlsPrivateKeyPaths = + object + [ fromString "removal" + .= object + [ fromString "ed25519" .= "test/resources/backendB/ed25519.pem", + fromString "ecdsa_secp256r1_sha256" .= "test/resources/backendB/ecdsa_secp256r1_sha256.pem", + fromString "ecdsa_secp384r1_sha384" .= "test/resources/backendB/ecdsa_secp384r1_sha384.pem", + fromString "ecdsa_secp521r1_sha512" .= "test/resources/backendB/ecdsa_secp521r1_sha512.pem" + ] + ] } diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 23295c1dd55..79762a53df1 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -69,7 +69,8 @@ data BackendResource = BackendResource berVHost :: String, berNginzSslPort :: Word16, berNginzHttp2Port :: Word16, - berInternalServicePorts :: forall a. (Num a) => Service -> a + berInternalServicePorts :: forall a. (Num a) => Service -> a, + berMlsPrivateKeyPaths :: Value } instance Eq BackendResource where @@ -80,7 +81,8 @@ instance Ord BackendResource where data DynamicBackendConfig = DynamicBackendConfig { domain :: String, - federatorExternalPort :: Word16 + federatorExternalPort :: Word16, + mlsPrivateKeyPaths :: Value } deriving (Show, Generic) @@ -240,6 +242,9 @@ data ClientIdentity = ClientIdentity } deriving stock (Show, Eq, Ord, Generic) +instance HasField "qualifiedUserId" ClientIdentity Aeson.Value where + getField cid = object [fromString "id" .= cid.user, fromString "domain" .= cid.domain] + newtype Ciphersuite = Ciphersuite {code :: String} deriving (Eq, Ord, Show, Generic) diff --git a/integration/test/Testlib/VersionedFed.hs b/integration/test/Testlib/VersionedFed.hs index 1dcadc2bffb..7f18da0a401 100644 --- a/integration/test/Testlib/VersionedFed.hs +++ b/integration/test/Testlib/VersionedFed.hs @@ -18,8 +18,9 @@ instance MakesValue (FedDomain 1) where instance (KnownNat n) => TestCases (FedDomain n) where mkTestCases = - map (fmap (const FedDomain)) - <$> mkFedTestCase "" (natVal (Proxy @n)) + let v = natVal (Proxy @n) + in map (fmap (const FedDomain)) + <$> mkFedTestCase ("[domain=fed-v" <> show v <> "]") v mkFedTestCase :: String -> Integer -> IO [TestCase Integer] mkFedTestCase name n = do diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs index 77eaa112cad..a9784a80081 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs @@ -44,11 +44,15 @@ import Wire.API.Federation.API.Common import Wire.API.Federation.API.Galley.Notifications as Notifications import Wire.API.Federation.Endpoint import Wire.API.Federation.Version +import Wire.API.MLS.Keys import Wire.API.MLS.SubConversation import Wire.API.MakesFederatedCall import Wire.API.Message +import Wire.API.Routes.Named import Wire.API.Routes.Public.Galley.Messaging import Wire.API.Routes.SpecialiseToVersion +import Wire.API.Routes.Version qualified as ClientAPI +import Wire.API.Routes.Versioned qualified as ClientAPI import Wire.API.Util.Aeson (CustomEncoded (..)) import Wire.API.VersionInfo import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -66,7 +70,19 @@ type GalleyApi = FedEndpoint "on-conversation-created" (ConversationCreated ConvId) EmptyResponse -- This endpoint is called the first time a user from this backend is -- added to a remote conversation. - :<|> FedEndpoint "get-conversations" GetConversationsRequest GetConversationsResponse + :<|> Named + "get-conversations@v1" + ( UnnamedFedEndpointWithMods + '[Until 'V2] + "get-conversations" + GetConversationsRequest + GetConversationsResponse + ) + :<|> FedEndpointWithMods + '[From 'V2] + "get-conversations" + GetConversationsRequest + GetConversationsResponseV2 :<|> FedEndpointWithMods '[ MakesFederatedCall 'Galley "on-conversation-updated", MakesFederatedCall 'Galley "on-mls-message-sent", @@ -143,11 +159,19 @@ type GalleyApi = "leave-sub-conversation" LeaveSubConversationRequest LeaveSubConversationResponse + :<|> Named + "get-one2one-conversation@v1" + ( UnnamedFedEndpointWithMods + '[From 'V1, Until 'V2] + "get-one2one-conversation" + GetOne2OneConversationRequest + GetOne2OneConversationResponse + ) :<|> FedEndpointWithMods - '[From 'V1] + '[From 'V2] "get-one2one-conversation" GetOne2OneConversationRequest - GetOne2OneConversationResponse + GetOne2OneConversationResponseV2 -- All the notification endpoints that go through the queue-based -- federation client ('fedQueueClient'). :<|> GalleyNotificationAPI @@ -226,7 +250,7 @@ data RemoteConversation = RemoteConversation id :: ConvId, metadata :: ConversationMetadata, members :: RemoteConvMembers, - protocol :: Protocol + protocol :: ClientAPI.Versioned 'ClientAPI.V5 Protocol } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform RemoteConversation) @@ -234,6 +258,42 @@ data RemoteConversation = RemoteConversation instance ToSchema RemoteConversation +-- | A conversation hosted on a remote backend. This contains the same +-- information as a 'Conversation', with the exception that conversation status +-- fields (muted\/archived\/hidden) are omitted, since they are not known by the +-- remote backend. +data RemoteConversationV2 = RemoteConversationV2 + { -- | Id of the conversation, implicitly qualified with the domain of the + -- backend that created this value. + id :: ConvId, + metadata :: ConversationMetadata, + members :: RemoteConvMembers, + protocol :: Protocol + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform RemoteConversationV2) + deriving (FromJSON, ToJSON) via (CustomEncoded RemoteConversationV2) + +instance ToSchema RemoteConversationV2 + +remoteConversationFromV2 :: RemoteConversationV2 -> RemoteConversation +remoteConversationFromV2 rc = + RemoteConversation + { id = rc.id, + metadata = rc.metadata, + members = rc.members, + protocol = ClientAPI.Versioned rc.protocol + } + +remoteConversationToV2 :: RemoteConversation -> RemoteConversationV2 +remoteConversationToV2 rc = + RemoteConversationV2 + { id = rc.id, + metadata = rc.metadata, + members = rc.members, + protocol = rc.protocol.unVersioned + } + newtype GetConversationsResponse = GetConversationsResponse { convs :: [RemoteConversation] } @@ -243,6 +303,21 @@ newtype GetConversationsResponse = GetConversationsResponse instance ToSchema GetConversationsResponse +newtype GetConversationsResponseV2 = GetConversationsResponseV2 + { convs :: [RemoteConversationV2] + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform GetConversationsResponseV2) + deriving (ToJSON, FromJSON) via (CustomEncoded GetConversationsResponseV2) + +instance ToSchema GetConversationsResponseV2 + +getConversationsResponseToV2 :: GetConversationsResponse -> GetConversationsResponseV2 +getConversationsResponseToV2 res = GetConversationsResponseV2 (map remoteConversationToV2 res.convs) + +getConversationsResponseFromV2 :: GetConversationsResponseV2 -> GetConversationsResponse +getConversationsResponseFromV2 res = GetConversationsResponse (map remoteConversationFromV2 res.convs) + data GetOne2OneConversationResponse = GetOne2OneConversationOk RemoteConversation | -- | This is returned when the local backend is asked for a 1-1 conversation @@ -257,6 +332,29 @@ data GetOne2OneConversationResponse instance ToSchema GetOne2OneConversationResponse +data GetOne2OneConversationResponseV2 + = GetOne2OneConversationV2Ok RemoteMLSOne2OneConversation + | -- | This is returned when the local backend is asked for a 1-1 conversation + -- that should reside on the other backend. + GetOne2OneConversationV2BackendMismatch + | -- | This is returned when a 1-1 conversation between two unconnected users + -- is requested. + GetOne2OneConversationV2NotConnected + | GetOne2OneConversationV2MLSNotEnabled + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded GetOne2OneConversationResponseV2) + +instance ToSchema GetOne2OneConversationResponseV2 + +data RemoteMLSOne2OneConversation = RemoteMLSOne2OneConversation + { conversation :: RemoteConversationV2, + publicKeys :: MLSKeysByPurpose MLSPublicKeys + } + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded RemoteMLSOne2OneConversation) + +instance ToSchema RemoteMLSOne2OneConversation + -- | A record type describing a new federated conversation -- -- FUTUREWORK: Think about extracting common conversation metadata into a diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs index 98f653e6083..bc3c56362f4 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs @@ -27,6 +27,7 @@ module Wire.API.Federation.Client runVersionedFederatorClient, runFederatorClientToCodensity, runVersionedFederatorClientToCodensity, + getNegotiatedVersion, performHTTP2Request, consumeStreamingResponseWith, streamingResponseStrictBody, @@ -117,6 +118,9 @@ instance VersionedMonad Version (FederatorClient c) where v <- asks cveVersion guard (maybe True p v) +getNegotiatedVersion :: FederatorClient c (Maybe Version) +getNegotiatedVersion = asks cveVersion + liftCodensity :: Codensity IO a -> FederatorClient c a liftCodensity = FederatorClient . lift . lift . lift 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 d10d00e6c4b..e3c76c36735 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs @@ -22,6 +22,7 @@ module Wire.API.Federation.Version Version (..), V0Sym0, V1Sym0, + V2Sym0, intToVersion, versionInt, versionText, @@ -48,13 +49,14 @@ import Data.Singletons.Base.TH import Data.Text qualified as Text import Imports -data Version = V0 | V1 +data Version = V0 | V1 | V2 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (FromJSON, ToJSON) via (Schema Version) versionInt :: Version -> Int versionInt V0 = 0 versionInt V1 = 1 +versionInt V2 = 2 versionText :: Version -> Text versionText = ("v" <>) . Text.pack . show . versionInt @@ -66,7 +68,8 @@ instance ToSchema Version where schema = enum @Integer "Version" . mconcat $ [ element 0 V0, - element 1 V1 + element 1 V1, + element 2 V2 ] supportedVersions :: Set Version diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GetOne2OneConversationResponse.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GetOne2OneConversationResponse.hs new file mode 100644 index 00000000000..9ea43d45966 --- /dev/null +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GetOne2OneConversationResponse.hs @@ -0,0 +1,130 @@ +module Test.Wire.API.Federation.Golden.GetOne2OneConversationResponse where + +import Data.Domain +import Data.Id +import Data.Json.Util +import Data.Qualified +import Data.Set qualified as Set +import Data.UUID qualified as UUID +import Imports +import Wire.API.Conversation +import Wire.API.Conversation.Protocol +import Wire.API.Conversation.Role +import Wire.API.Federation.API.Galley +import Wire.API.MLS.Keys +import Wire.API.Routes.Versioned qualified as ClientAPI + +testObject_GetOne2OneConversationResponseOk :: GetOne2OneConversationResponse +testObject_GetOne2OneConversationResponseOk = + GetOne2OneConversationOk remoteConversation + +testObject_GetOne2OneConversationResponseBackendMismatch :: GetOne2OneConversationResponse +testObject_GetOne2OneConversationResponseBackendMismatch = GetOne2OneConversationBackendMismatch + +testObject_GetOne2OneConversationResponseNotConnected :: GetOne2OneConversationResponse +testObject_GetOne2OneConversationResponseNotConnected = GetOne2OneConversationNotConnected + +testObject_GetOne2OneConversationResponseV2Ok :: GetOne2OneConversationResponseV2 +testObject_GetOne2OneConversationResponseV2Ok = + GetOne2OneConversationV2Ok $ + RemoteMLSOne2OneConversation + { conversation = remoteConversationV2, + publicKeys = + MLSKeysByPurpose + { removal = + MLSKeys + { ed25519 = + MLSPublicKey + (fromBase64TextLenient "7C8PpP91rzMnD4VHuWTI3yNuInfbzIk937uF0Cg/Piw="), + ecdsa_secp256r1_sha256 = + MLSPublicKey + (fromBase64TextLenient "ArUTSywmqya1wAGwrK+pJuA7KSpKm06y3eZq8Py2NMM="), + ecdsa_secp384r1_sha384 = + MLSPublicKey + (fromBase64TextLenient "7pKiTLf72OfpQIeVeXF0mJKfWsBnhTtMUy0zuKasYjlTQUW5fGtcyAFXinM3FahV"), + ecdsa_secp521r1_sha512 = + MLSPublicKey + (fromBase64TextLenient "9twvhZ57ytiujWXFtSmxd8I5r9iZjgdCtGtReJT3yQL2BCGZ80Vzq/MrmV+O0i7lZEI1gqbr8vL1xKk+2h2LyQ==") + } + } + } + +testObject_GetOne2OneConversationResponseV2BackendMismatch :: GetOne2OneConversationResponseV2 +testObject_GetOne2OneConversationResponseV2BackendMismatch = GetOne2OneConversationV2BackendMismatch + +testObject_GetOne2OneConversationResponseV2NotConnected :: GetOne2OneConversationResponseV2 +testObject_GetOne2OneConversationResponseV2NotConnected = GetOne2OneConversationV2NotConnected + +remoteConversation :: RemoteConversation +remoteConversation = + RemoteConversation + { id = (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200040001"))), + metadata = + ConversationMetadata + { cnvmType = One2OneConv, + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))), + cnvmAccess = [], + cnvmAccessRoles = Set.empty, + cnvmName = Just " 0", + cnvmTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002"))), + cnvmMessageTimer = Nothing, + cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = -2}) + }, + members = + RemoteConvMembers + { selfRole = roleNameWireAdmin, + others = + [ OtherMember + { omQualifiedId = + Qualified + (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))) + (Domain "example.com"), + omService = Nothing, + omConvRoleName = roleNameWireMember + } + ] + }, + protocol = + ClientAPI.Versioned . ProtocolMLS $ + ConversationMLSData + { cnvmlsGroupId = GroupId "group", + cnvmlsActiveData = Nothing + } + } + +remoteConversationV2 :: RemoteConversationV2 +remoteConversationV2 = + RemoteConversationV2 + { id = (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200040001"))), + metadata = + ConversationMetadata + { cnvmType = One2OneConv, + cnvmCreator = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))), + cnvmAccess = [], + cnvmAccessRoles = Set.empty, + cnvmName = Just " 0", + cnvmTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002"))), + cnvmMessageTimer = Nothing, + cnvmReceiptMode = Just (ReceiptMode {unReceiptMode = -2}) + }, + members = + RemoteConvMembers + { selfRole = roleNameWireAdmin, + others = + [ OtherMember + { omQualifiedId = + Qualified + (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001"))) + (Domain "example.com"), + omService = Nothing, + omConvRoleName = roleNameWireMember + } + ] + }, + protocol = + ProtocolMLS $ + ConversationMLSData + { cnvmlsGroupId = GroupId "group", + cnvmlsActiveData = Nothing + } + } diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs index b691cd8e962..038f98b0d1e 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs @@ -21,6 +21,7 @@ import Imports import Test.Hspec import Test.Wire.API.Federation.Golden.ConversationCreated qualified as ConversationCreated import Test.Wire.API.Federation.Golden.ConversationUpdate qualified as ConversationUpdate +import Test.Wire.API.Federation.Golden.GetOne2OneConversationResponse qualified as GetOne2OneConversationResponse import Test.Wire.API.Federation.Golden.LeaveConversationRequest qualified as LeaveConversationRequest import Test.Wire.API.Federation.Golden.LeaveConversationResponse qualified as LeaveConversationResponse import Test.Wire.API.Federation.Golden.MLSMessageSendingStatus qualified as MLSMessageSendingStatus @@ -74,3 +75,13 @@ spec = [ (ConversationCreated.testObject_ConversationCreated1, "testObject_ConversationCreated1.json"), (ConversationCreated.testObject_ConversationCreated2, "testObject_ConversationCreated2.json") ] + testObjects + [ (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseV2Ok, "testObject_GetOne2OneConversationResponseV2Ok.json"), + (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseV2BackendMismatch, "testObject_GetOne2OneConversationResponseV2BackendMismatch.json"), + (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseV2NotConnected, "testObject_GetOne2OneConversationResponseV2NotConnected.json") + ] + testObjects + [ (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseOk, "testObject_GetOne2OneConversationResponseOk.json"), + (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseBackendMismatch, "testObject_GetOne2OneConversationResponseBackendMismatch.json"), + (GetOne2OneConversationResponse.testObject_GetOne2OneConversationResponseNotConnected, "testObject_GetOne2OneConversationResponseNotConnected.json") + ] diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseBackendMismatch.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseBackendMismatch.json new file mode 100644 index 00000000000..be96bcaa144 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseBackendMismatch.json @@ -0,0 +1,3 @@ +{ + "tag": "GetOne2OneConversationBackendMismatch" +} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseNotConnected.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseNotConnected.json new file mode 100644 index 00000000000..3920fe3bed6 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseNotConnected.json @@ -0,0 +1,3 @@ +{ + "tag": "GetOne2OneConversationNotConnected" +} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseOk.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseOk.json new file mode 100644 index 00000000000..dbfa3343671 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseOk.json @@ -0,0 +1,39 @@ +{ + "contents": { + "id": "00000001-0000-0001-0000-000200040001", + "members": { + "others": [ + { + "conversation_role": "wire_member", + "id": "00000001-0000-0001-0000-000200000001", + "qualified_id": { + "domain": "example.com", + "id": "00000001-0000-0001-0000-000200000001" + }, + "status": 0 + } + ], + "self_role": "wire_admin" + }, + "metadata": { + "access": [], + "access_role": [], + "creator": "00000001-0000-0001-0000-000200000001", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "message_timer": null, + "name": " 0", + "receipt_mode": -2, + "team": "00000001-0000-0001-0000-000100000002", + "type": 2 + }, + "protocol": { + "cipher_suite": 1, + "epoch": 0, + "epoch_timestamp": null, + "group_id": "Z3JvdXA=", + "protocol": "mls" + } + }, + "tag": "GetOne2OneConversationOk" +} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2BackendMismatch.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2BackendMismatch.json new file mode 100644 index 00000000000..49390ae4674 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2BackendMismatch.json @@ -0,0 +1,3 @@ +{ + "tag": "GetOne2OneConversationV2BackendMismatch" +} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2NotConnected.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2NotConnected.json new file mode 100644 index 00000000000..22355bf11e6 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2NotConnected.json @@ -0,0 +1,3 @@ +{ + "tag": "GetOne2OneConversationV2NotConnected" +} \ No newline at end of file diff --git a/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2Ok.json b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2Ok.json new file mode 100644 index 00000000000..d14e627e6be --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_GetOne2OneConversationResponseV2Ok.json @@ -0,0 +1,47 @@ +{ + "contents": { + "conversation": { + "id": "00000001-0000-0001-0000-000200040001", + "members": { + "others": [ + { + "conversation_role": "wire_member", + "id": "00000001-0000-0001-0000-000200000001", + "qualified_id": { + "domain": "example.com", + "id": "00000001-0000-0001-0000-000200000001" + }, + "status": 0 + } + ], + "self_role": "wire_admin" + }, + "metadata": { + "access": [], + "access_role": [], + "creator": "00000001-0000-0001-0000-000200000001", + "last_event": "0.0", + "last_event_time": "1970-01-01T00:00:00.000Z", + "message_timer": null, + "name": " 0", + "receipt_mode": -2, + "team": "00000001-0000-0001-0000-000100000002", + "type": 2 + }, + "protocol": { + "epoch": 0, + "group_id": "Z3JvdXA=", + "protocol": "mls" + } + }, + "public_keys": { + "removal": { + "ecdsa_secp256r1_sha256": "ArUTSywmqya1wAGwrK+pJuA7KSpKm06y3eZq8Py2NMM=", + "ecdsa_secp384r1_sha384": "7pKiTLf72OfpQIeVeXF0mJKfWsBnhTtMUy0zuKasYjlTQUW5fGtcyAFXinM3FahV", + "ecdsa_secp521r1_sha512": "9twvhZ57ytiujWXFtSmxd8I5r9iZjgdCtGtReJT3yQL2BCGZ80Vzq/MrmV+O0i7lZEI1gqbr8vL1xKk+2h2LyQ==", + "ed25519": "7C8PpP91rzMnD4VHuWTI3yNuInfbzIk937uF0Cg/Piw=" + } + } + }, + "tag": "GetOne2OneConversationV2Ok" +} \ No newline at end of file diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index e2490419ca0..dc7ea88d9e8 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -132,6 +132,7 @@ test-suite spec Test.Wire.API.Federation.API.BrigSpec Test.Wire.API.Federation.Golden.ConversationCreated Test.Wire.API.Federation.Golden.ConversationUpdate + Test.Wire.API.Federation.Golden.GetOne2OneConversationResponse Test.Wire.API.Federation.Golden.GoldenSpec Test.Wire.API.Federation.Golden.LeaveConversationRequest Test.Wire.API.Federation.Golden.LeaveConversationResponse diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index e4184cb1d2d..fb851d4be9c 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -34,6 +34,7 @@ module Wire.API.Conversation cnvMessageTimer, cnvReceiptMode, cnvAccessRoles, + MLSOne2OneConversation (..), CreateGroupConversation (..), ConversationCoverView (..), ConversationList (..), @@ -96,7 +97,6 @@ import Data.List.NonEmpty (NonEmpty) import Data.List1 import Data.Map qualified as Map import Data.Misc -import Data.OpenApi (deprecated) import Data.OpenApi qualified as S import Data.Qualified import Data.Range (Range, fromRange, rangedSchema) @@ -114,6 +114,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role (RoleName, roleNameWireAdmin) import Wire.API.Event.LeaveReason import Wire.API.MLS.Group +import Wire.API.MLS.Keys import Wire.API.Routes.MultiTablePaging import Wire.API.Routes.MultiVerb import Wire.API.Routes.Version @@ -289,6 +290,20 @@ conversationSchema v = (description ?~ "A conversation object as returned from the server") (conversationObjectSchema v) +data MLSOne2OneConversation a = MLSOne2OneConversation + { conversation :: Conversation, + publicKeys :: MLSKeysByPurpose (MLSKeys a) + } + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema (MLSOne2OneConversation a)) + +instance (ToSchema a) => ToSchema (MLSOne2OneConversation a) where + schema = + let aName = maybe "" ("_" <>) $ getName (schemaDoc (schema @a)) + in object ("MLSOne2OneConversation" <> aName) $ + MLSOne2OneConversation + <$> conversation .= field "conversation" schema + <*> publicKeys .= field "public_keys" schema + -- | The public-facing conversation type extended with information on which -- remote users could not be added when creating the conversation. data CreateGroupConversation = CreateGroupConversation @@ -682,7 +697,7 @@ newConvSchema v sch = <$> newConvUsers .= ( fieldWithDocModifier "users" - ( (deprecated ?~ True) + ( (S.deprecated ?~ True) . (description ?~ usersDesc) ) (array schema) diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 9e213a26fdd..17870c6a249 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -108,12 +108,7 @@ optionalActiveMLSConversationDataSchema (Just v) (description ?~ "The epoch number of the corresponding MLS group") schema <*> fmap (.epochTimestamp) - .= maybe_ - ( optFieldWithDocModifier - "epoch_timestamp" - (description ?~ "The timestamp of the epoch number") - utcTimeSchema - ) + .= field "epoch_timestamp" (named "Epoch Timestamp" . nullable . unnamed $ utcTimeSchema) <*> maybe MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 (.ciphersuite) .= fieldWithDocModifier "cipher_suite" @@ -245,6 +240,9 @@ protocolSchema v = instance ToSchema Protocol where schema = object "Protocol" (protocolSchema Nothing) +instance ToSchema (Versioned 'V5 Protocol) where + schema = object "Protocol" (Versioned <$> unVersioned .= protocolSchema (Just V5)) + deriving via (Schema Protocol) instance FromJSON Protocol deriving via (Schema Protocol) instance ToJSON Protocol diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index ed4d3a0e226..22ad24e1c2d 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -104,6 +104,7 @@ data GalleyError | MLSSubConvUnsupportedConvType | MLSSubConvClientNotInParent | MLSMigrationCriteriaNotSatisfied + | MLSFederatedOne2OneNotSupported | -- NoBindingTeamMembers | NoBindingTeam @@ -253,6 +254,8 @@ type instance MapError 'MLSSubConvClientNotInParent = 'StaticError 403 "mls-subc type instance MapError 'MLSMigrationCriteriaNotSatisfied = 'StaticError 400 "mls-migration-criteria-not-satisfied" "The migration criteria for mixed to MLS protocol transition are not satisfied for this conversation" +type instance MapError 'MLSFederatedOne2OneNotSupported = 'StaticError 400 "mls-federated-one2one-not-supported" "Federated One2One MLS conversations are only supported in API version >= 6" + type instance MapError 'NoBindingTeamMembers = 'StaticError 403 "non-binding-team-members" "Both users must be members of the same binding team" type instance MapError 'NoBindingTeam = 'StaticError 403 "no-binding-team" "Operation allowed only on binding teams" diff --git a/libs/wire-api/src/Wire/API/MLS/Keys.hs b/libs/wire-api/src/Wire/API/MLS/Keys.hs index f070e81f595..d7b8ce85375 100644 --- a/libs/wire-api/src/Wire/API/MLS/Keys.hs +++ b/libs/wire-api/src/Wire/API/MLS/Keys.hs @@ -23,6 +23,7 @@ import Crypto.PubKey.ECDSA qualified as ECDSA import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A import Data.Bifunctor +import Data.ByteArray (ByteArray) import Data.ByteArray qualified as BA import Data.Default import Data.Json.Util @@ -132,10 +133,8 @@ instance ToSchema JWK where type MLSPublicKeysJWK = MLSKeys JWK -mlsKeysToPublicJWK :: - MLSPrivateKeys -> - Maybe MLSPublicKeysJWK -mlsKeysToPublicJWK (MLSPrivateKeys (_, ed) (_, ec256) (_, ec384) (_, ec521)) = +mlsPublicKeysToJWK :: MLSPublicKeys -> Maybe MLSPublicKeysJWK +mlsPublicKeysToJWK (MLSKeys (MLSPublicKey ed) (MLSPublicKey ec256) (MLSPublicKey ec384) (MLSPublicKey ec521)) = -- The kty parameter for ECDSA is "EC", for Ed25519 it's "OKP" (octet key -- pair). -- https://www.rfc-editor.org/rfc/rfc7518.html#section-6.1 @@ -146,16 +145,18 @@ mlsKeysToPublicJWK (MLSPrivateKeys (_, ed) (_, ec256) (_, ec384) (_, ec521)) = -- The x parameter is mandatory for all keys, the y parameter is mandatory for -- all ECDSA keys. -- https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1 - MLSKeys (JWK "OKP" "Ed25519" (BA.convert ed) Nothing) - <$> (uncurry (JWK "EC" "P-256") . second Just <$> splitXY (ECDSA.encodePublic (Proxy @Curve_P256R1) ec256)) - <*> (uncurry (JWK "EC" "P-384") . second Just <$> splitXY (ECDSA.encodePublic (Proxy @Curve_P384R1) ec384)) - <*> (uncurry (JWK "EC" "P-521") . second Just <$> splitXY (ECDSA.encodePublic (Proxy @Curve_P521R1) ec521)) + MLSKeys + (JWK "OKP" "Ed25519" (BA.convert ed) Nothing) + <$> (uncurry (JWK "EC" "P-256") . second Just <$> splitXY ec256) + <*> (uncurry (JWK "EC" "P-384") . second Just <$> splitXY ec384) + <*> (uncurry (JWK "EC" "P-521") . second Just <$> splitXY ec521) where -- Obtaining X and Y from an encoded curve point follows the logic of -- Crypto.ECC's encodeECPoint and decodeECPoint (the module is not -- exported). Points need to be encoded in uncompressed representation. This -- is true for ECDSA.encodePublic. -- https://www.rfc-editor.org/rfc/rfc8422#section-5.4.1 + splitXY :: forall {bs}. (ByteArray bs) => bs -> Maybe (bs, bs) splitXY mxy = do (m, xy) <- BA.uncons mxy -- The first Byte m is 4 for the uncompressed representation of curve points. 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 c228a3c2621..a6d377331dc 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 @@ -35,6 +35,7 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.MLS.GroupInfo +import Wire.API.MLS.Keys import Wire.API.MLS.Servant import Wire.API.MLS.SubConversation import Wire.API.MakesFederatedCall @@ -679,22 +680,37 @@ type ConversationAPI = :> ZLocalUser :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected + :> CanThrow 'MLSFederatedOne2OneNotSupported :> "conversations" :> "one2one" :> QualifiedCapture "usr" UserId :> MultiVerb1 'GET '[JSON] (VersionedRespond 'V5 200 "MLS 1-1 conversation" Conversation) ) + :<|> Named + "get-one-to-one-mls-conversation@v6" + ( Summary "Get an MLS 1:1 conversation" + :> From 'V6 + :> Until 'V7 + :> ZLocalUser + :> CanThrow 'MLSNotEnabled + :> CanThrow 'NotConnected + :> "conversations" + :> "one2one" + :> QualifiedCapture "usr" UserId + :> MultiVerb1 'GET '[JSON] (Respond 200 "MLS 1-1 conversation" (MLSOne2OneConversation MLSPublicKey)) + ) :<|> Named "get-one-to-one-mls-conversation" ( Summary "Get an MLS 1:1 conversation" - :> From 'V5 + :> From 'V7 :> ZLocalUser :> CanThrow 'MLSNotEnabled :> CanThrow 'NotConnected :> "conversations" :> "one2one" :> QualifiedCapture "usr" UserId - :> MultiVerb1 'GET '[JSON] (Respond 200 "MLS 1-1 conversation" Conversation) + :> QueryParam "format" MLSPublicKeyFormat + :> MultiVerb1 'GET '[JSON] (Respond 200 "MLS 1-1 conversation" (MLSOne2OneConversation SomeKey)) ) -- This endpoint can lead to the following events being sent: -- - MemberJoin event to members diff --git a/libs/wire-api/src/Wire/API/Routes/Versioned.hs b/libs/wire-api/src/Wire/API/Routes/Versioned.hs index 685fcbb3291..640d91fd022 100644 --- a/libs/wire-api/src/Wire/API/Routes/Versioned.hs +++ b/libs/wire-api/src/Wire/API/Routes/Versioned.hs @@ -29,6 +29,7 @@ import Servant import Servant.API.ContentTypes import Servant.OpenApi import Servant.OpenApi.Internal +import Test.QuickCheck (Arbitrary) import Wire.API.Routes.MultiVerb import Wire.API.Routes.Version @@ -102,6 +103,7 @@ instance -- Servant. newtype Versioned (v :: Version) a = Versioned {unVersioned :: a} deriving (Eq, Show) + deriving newtype (Arbitrary) instance Functor (Versioned v) where fmap f (Versioned a) = Versioned (f a) diff --git a/libs/wire-api/test/golden/testObject_Conversation_v2_user_4.json b/libs/wire-api/test/golden/testObject_Conversation_v2_user_4.json index 6609a65f3af..cda477d4510 100644 --- a/libs/wire-api/test/golden/testObject_Conversation_v2_user_4.json +++ b/libs/wire-api/test/golden/testObject_Conversation_v2_user_4.json @@ -22,6 +22,7 @@ "cipher_suite": 1, "creator": "00000000-0000-0000-0000-000200000001", "epoch": 0, + "epoch_timestamp": null, "group_id": "dGVzdF9ncm91cA==", "id": "00000000-0000-0000-0000-000000000002", "last_event": "0.0", diff --git a/libs/wire-api/test/golden/testObject_Conversation_v5_user_4.json b/libs/wire-api/test/golden/testObject_Conversation_v5_user_4.json index 430ac682cee..e7351a003ce 100644 --- a/libs/wire-api/test/golden/testObject_Conversation_v5_user_4.json +++ b/libs/wire-api/test/golden/testObject_Conversation_v5_user_4.json @@ -21,6 +21,7 @@ "cipher_suite": 1, "creator": "00000000-0000-0000-0000-000200000001", "epoch": 0, + "epoch_timestamp": null, "group_id": "dGVzdF9ncm91cA==", "id": "00000000-0000-0000-0000-000000000002", "last_event": "0.0", diff --git a/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_2.json b/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_2.json index ac57e7e8e1b..a918c3161ba 100644 --- a/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_2.json +++ b/libs/wire-api/test/golden/testObject_PublicSubConversation_v5_2.json @@ -1,6 +1,7 @@ { "cipher_suite": 1, "epoch": 0, + "epoch_timestamp": null, "group_id": "dGVzdF9ncm91cF8y", "members": [ { diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index 6b53ed6e9e3..351f38f7c4a 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -158,7 +158,7 @@ spec = do } runningFlag <- newMVar () (env, fedReqs) <- - withTempMockFederator def {versions = [0, 2]} . runTestAppT $ do + withTempMockFederator def {versions = [0, 999999]} . runTestAppT $ do wait =<< pushNotification runningFlag targetDomain (msg, envelope) ask diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 5c025935607..6439e5ba7de 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -52,10 +52,10 @@ settings: federationDomain: example.com mlsPrivateKeyPaths: removal: - ed25519: test/resources/ed25519.pem - ecdsa_secp256r1_sha256: test/resources/ecdsa_secp256r1_sha256.pem - ecdsa_secp384r1_sha384: test/resources/ecdsa_secp384r1_sha384.pem - ecdsa_secp521r1_sha512: test/resources/ecdsa_secp521r1_sha512.pem + ed25519: test/resources/backendA/ed25519.pem + ecdsa_secp256r1_sha256: test/resources/backendA/ecdsa_secp256r1_sha256.pem + ecdsa_secp384r1_sha384: test/resources/backendA/ecdsa_secp384r1_sha384.pem + ecdsa_secp521r1_sha512: test/resources/backendA/ecdsa_secp521r1_sha512.pem guestLinkTTLSeconds: 604800 # 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. diff --git a/services/galley/src/Galley/API/Error.hs b/services/galley/src/Galley/API/Error.hs index 5ece86d0a4f..a8241afa1c4 100644 --- a/services/galley/src/Galley/API/Error.hs +++ b/services/galley/src/Galley/API/Error.hs @@ -62,12 +62,14 @@ data InvalidInput | InvalidRange LText | InvalidUUID4 | InvalidPayload LText + | FederationFunctionNotSupported LText instance APIError InvalidInput where toResponse CustomRolesNotSupported = toResponse $ badRequest "Custom roles not supported" toResponse (InvalidRange t) = toResponse $ invalidRange t toResponse InvalidUUID4 = toResponse invalidUUID4 toResponse (InvalidPayload t) = toResponse $ invalidPayload t + toResponse (FederationFunctionNotSupported t) = toResponse $ federationFunctionNotSupported t ---------------------------------------------------------------------------- -- Other errors @@ -84,6 +86,9 @@ invalidPayload = Wai.mkError status400 "invalid-payload" badRequest :: LText -> Wai.Error badRequest = Wai.mkError status400 "bad-request" +federationFunctionNotSupported :: LText -> Wai.Error +federationFunctionNotSupported = Wai.mkError status400 "federation-function-not-supported" + invalidUUID4 :: Wai.Error invalidUUID4 = Wai.mkError status400 "client-error" "Invalid UUID v4 format" diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index ee913e41c13..8ca7057686a 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -39,6 +39,7 @@ import Data.Text.Lazy qualified as LT import Data.Time.Clock import Galley.API.Action import Galley.API.Error +import Galley.API.MLS import Galley.API.MLS.Enabled import Galley.API.MLS.GroupInfo import Galley.API.MLS.Message @@ -89,6 +90,7 @@ import Wire.API.Federation.Error import Wire.API.Federation.Version import Wire.API.MLS.Credential import Wire.API.MLS.GroupInfo +import Wire.API.MLS.Keys import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Message @@ -104,6 +106,7 @@ federationSitemap :: ServerT FederationAPI (Sem GalleyEffects) federationSitemap = Named @"on-conversation-created" onConversationCreated + :<|> Named @"get-conversations@v1" getConversationsV1 :<|> Named @"get-conversations" getConversations :<|> Named @"leave-conversation" (callsFed (exposeAnnotations leaveConversation)) :<|> Named @"send-message" (callsFed (exposeAnnotations sendMessage)) @@ -117,6 +120,7 @@ federationSitemap = :<|> Named @"get-sub-conversation" getSubConversationForRemoteUser :<|> Named @"delete-sub-conversation" (callsFed deleteSubConversationForRemoteUser) :<|> Named @"leave-sub-conversation" (callsFed leaveSubConversation) + :<|> Named @"get-one2one-conversation@v1" getOne2OneConversationV1 :<|> Named @"get-one2one-conversation" getOne2OneConversation :<|> Named @"on-client-removed" onClientRemoved :<|> Named @"on-message-sent" onMessageSent @@ -198,17 +202,27 @@ onConversationCreated domain rc = do pushConversationEvent Nothing event (qualifyAs loc [qUnqualified . Public.memId $ mem]) [] pure EmptyResponse -getConversations :: +getConversationsV1 :: ( Member ConversationStore r, Member (Input (Local ())) r ) => Domain -> GetConversationsRequest -> Sem r GetConversationsResponse +getConversationsV1 domain req = + getConversationsResponseFromV2 <$> getConversations domain req + +getConversations :: + ( Member ConversationStore r, + Member (Input (Local ())) r + ) => + Domain -> + GetConversationsRequest -> + Sem r GetConversationsResponseV2 getConversations domain (GetConversationsRequest uid cids) = do let ruid = toRemoteUnsafe domain uid loc <- qualifyLocal () - GetConversationsResponse + GetConversationsResponseV2 . mapMaybe (Mapping.conversationToRemote (tDomain loc) ruid) <$> E.getConversations cids @@ -735,34 +749,62 @@ deleteSubConversationForRemoteUser domain DeleteSubConversationFedRequest {..} = lconv <- qualifyLocal dscreqConv deleteLocalSubConversation qusr lconv dscreqSubConv dsc +getOne2OneConversationV1 :: + ( Member (Input (Local ())) r, + Member BrigAccess r, + Member (Error InvalidInput) r + ) => + Domain -> + GetOne2OneConversationRequest -> + Sem r GetOne2OneConversationResponse +getOne2OneConversationV1 domain (GetOne2OneConversationRequest self other) = + fmap (Imports.fromRight GetOne2OneConversationNotConnected) + . runError @(Tagged 'NotConnected ()) + $ do + lother <- qualifyLocal other + let rself = toRemoteUnsafe domain self + ensureConnectedToRemotes lother [rself] + foldQualified + lother + (const . throw $ FederationFunctionNotSupported "Getting 1:1 conversations is not supported over federation API < V2.") + (const (pure GetOne2OneConversationBackendMismatch)) + (one2OneConvId BaseProtocolMLSTag (tUntagged lother) (tUntagged rself)) + getOne2OneConversation :: ( Member ConversationStore r, Member (Input (Local ())) r, Member (Error InternalError) r, - Member BrigAccess r + Member BrigAccess r, + Member (Input Env) r ) => Domain -> GetOne2OneConversationRequest -> - Sem r GetOne2OneConversationResponse + Sem r GetOne2OneConversationResponseV2 getOne2OneConversation domain (GetOne2OneConversationRequest self other) = - fmap (Imports.fromRight GetOne2OneConversationNotConnected) + fmap (Imports.fromRight GetOne2OneConversationV2MLSNotEnabled) + . runError @(Tagged 'MLSNotEnabled ()) + . fmap (Imports.fromRight GetOne2OneConversationV2NotConnected) . runError @(Tagged 'NotConnected ()) $ do lother <- qualifyLocal other let rself = toRemoteUnsafe domain self - ensureConnectedToRemotes lother [rself] let getLocal lconv = do mconv <- E.getConversation (tUnqualified lconv) - fmap GetOne2OneConversationOk $ case mconv of + mlsPublicKeys <- mlsKeysToPublic <$$> getMLSPrivateKeys + conv <- case mconv of Nothing -> pure (localMLSOne2OneConversationAsRemote lconv) Just conv -> note (InternalErrorWithDescription "Unexpected member list in 1-1 conversation") (conversationToRemote (tDomain lother) rself conv) + pure . GetOne2OneConversationV2Ok $ RemoteMLSOne2OneConversation conv mlsPublicKeys + + ensureConnectedToRemotes lother [rself] + foldQualified lother getLocal - (const (pure GetOne2OneConversationBackendMismatch)) + (const (pure GetOne2OneConversationV2BackendMismatch)) (one2OneConvId BaseProtocolMLSTag (tUntagged lother) (tUntagged rself)) -------------------------------------------------------------------------------- diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 12a6ff05ac5..ad86b107436 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -179,7 +179,7 @@ conversationAPI = <@> mkNamedAPI @"conversation-unblock-unqualified" Update.unblockConvUnqualified <@> mkNamedAPI @"conversation-unblock" Update.unblockConv <@> mkNamedAPI @"conversation-meta" Query.getConversationMeta - <@> mkNamedAPI @"conversation-mls-one-to-one" Query.getMLSOne2OneConversation + <@> mkNamedAPI @"conversation-mls-one-to-one" Query.getMLSOne2OneConversationInternal <@> mkNamedAPI @"conversation-mls-one-to-one-established" Query.isMLSOne2OneEstablished legalholdWhitelistedTeamsAPI :: API ILegalholdWhitelistedTeamsAPI GalleyEffects diff --git a/services/galley/src/Galley/API/MLS.hs b/services/galley/src/Galley/API/MLS.hs index 0e017c2d606..7a83ac92146 100644 --- a/services/galley/src/Galley/API/MLS.hs +++ b/services/galley/src/Galley/API/MLS.hs @@ -22,12 +22,11 @@ module Galley.API.MLS postMLSCommitBundleFromLocalUser, postMLSMessageFromLocalUser, getMLSPublicKeys, + formatPublicKeys, ) where import Data.Default -import Data.Id -import Data.Qualified import Galley.API.Error import Galley.API.MLS.Enabled import Galley.API.MLS.Message @@ -45,18 +44,25 @@ getMLSPublicKeys :: Member (ErrorS 'MLSNotEnabled) r, Member (Error InternalError) r ) => - Local UserId -> Maybe MLSPublicKeyFormat -> Sem r (MLSKeysByPurpose (MLSKeys SomeKey)) -getMLSPublicKeys _ fmt = do - keys <- getMLSPrivateKeys +getMLSPublicKeys fmt = do + publicKeys <- mlsKeysToPublic <$$> getMLSPrivateKeys + formatPublicKeys fmt publicKeys + +formatPublicKeys :: + (Member (Error InternalError) r) => + Maybe MLSPublicKeyFormat -> + MLSKeysByPurpose MLSPublicKeys -> + Sem r (MLSKeysByPurpose (MLSKeys SomeKey)) +formatPublicKeys fmt publicKeys = case fromMaybe def fmt of - MLSPublicKeyFormatRaw -> pure (fmap (fmap mkSomeKey . mlsKeysToPublic) keys) + MLSPublicKeyFormatRaw -> pure (fmap (fmap mkSomeKey) publicKeys) MLSPublicKeyFormatJWK -> do jwks <- traverse ( note (InternalErrorWithDescription "malformed MLS removal keys") - . mlsKeysToPublicJWK + . mlsPublicKeysToJWK ) - keys + publicKeys pure $ fmap (fmap mkSomeKey) jwks diff --git a/services/galley/src/Galley/API/MLS/One2One.hs b/services/galley/src/Galley/API/MLS/One2One.hs index 00dd1a534de..6aae0c69bb2 100644 --- a/services/galley/src/Galley/API/MLS/One2One.hs +++ b/services/galley/src/Galley/API/MLS/One2One.hs @@ -37,6 +37,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Federation.API.Galley import Wire.API.MLS.Group.Serialisation +import Wire.API.MLS.Keys import Wire.API.MLS.SubConversation import Wire.API.User @@ -64,7 +65,7 @@ localMLSOne2OneConversation lself (tUntagged -> convId) = -- conversation to be returned to a remote backend. localMLSOne2OneConversationAsRemote :: Local ConvId -> - RemoteConversation + RemoteConversationV2 localMLSOne2OneConversationAsRemote lcnv = let members = RemoteConvMembers @@ -72,7 +73,7 @@ localMLSOne2OneConversationAsRemote lcnv = others = [] } (metadata, mlsData) = localMLSOne2OneConversationMetadata (tUntagged lcnv) - in RemoteConversation + in RemoteConversationV2 { id = tUnqualified lcnv, metadata = metadata, members = members, @@ -100,19 +101,24 @@ localMLSOne2OneConversationMetadata convId = remoteMLSOne2OneConversation :: Local UserId -> Remote UserId -> - RemoteConversation -> - Conversation + RemoteMLSOne2OneConversation -> + (MLSOne2OneConversation MLSPublicKey) remoteMLSOne2OneConversation lself rother rc = let members = ConvMembers { cmSelf = defMember (tUntagged lself), - cmOthers = rc.members.others + cmOthers = rc.conversation.members.others } - in Conversation - { cnvQualifiedId = tUntagged (qualifyAs rother rc.id), - cnvMetadata = rc.metadata, - cnvMembers = members, - cnvProtocol = rc.protocol + conv = + Conversation + { cnvQualifiedId = tUntagged (qualifyAs rother rc.conversation.id), + cnvMetadata = rc.conversation.metadata, + cnvMembers = members, + cnvProtocol = rc.conversation.protocol + } + in MLSOne2OneConversation + { conversation = conv, + publicKeys = rc.publicKeys } -- | Create a new record for an MLS 1-1 conversation in the database and add diff --git a/services/galley/src/Galley/API/Mapping.hs b/services/galley/src/Galley/API/Mapping.hs index ec6f0993e12..5a9d7615a6b 100644 --- a/services/galley/src/Galley/API/Mapping.hs +++ b/services/galley/src/Galley/API/Mapping.hs @@ -97,7 +97,7 @@ conversationViewMaybe luid remoteOthers localOthers conv = do remoteConversationView :: Local UserId -> MemberStatus -> - Remote RemoteConversation -> + Remote RemoteConversationV2 -> Conversation remoteConversationView uid status (tUntagged -> Qualified rconv rDomain) = let mems = rconv.members @@ -125,7 +125,7 @@ conversationToRemote :: Domain -> Remote UserId -> Data.Conversation -> - Maybe RemoteConversation + Maybe RemoteConversationV2 conversationToRemote localDomain ruid conv = do let (selfs, rothers) = partition ((== ruid) . rmId) (Data.convRemoteMembers conv) lothers = Data.convLocalMembers conv @@ -134,7 +134,7 @@ conversationToRemote localDomain ruid conv = do map (localMemberToOther localDomain) lothers <> map remoteMemberToOther rothers pure $ - RemoteConversation + RemoteConversationV2 { id = Data.convId conv, metadata = Data.convMetadata conv, members = diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index 487e6893c85..d254ff6c9c8 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -62,7 +62,8 @@ conversationAPI = <@> mkNamedAPI @"get-subconversation-group-info" (callsFed getSubConversationGroupInfo) <@> mkNamedAPI @"create-one-to-one-conversation@v2" (callsFed createOne2OneConversation) <@> mkNamedAPI @"create-one-to-one-conversation" (callsFed createOne2OneConversation) - <@> mkNamedAPI @"get-one-to-one-mls-conversation@v5" getMLSOne2OneConversation + <@> mkNamedAPI @"get-one-to-one-mls-conversation@v5" getMLSOne2OneConversationV5 + <@> mkNamedAPI @"get-one-to-one-mls-conversation@v6" getMLSOne2OneConversationV6 <@> mkNamedAPI @"get-one-to-one-mls-conversation" getMLSOne2OneConversation <@> mkNamedAPI @"add-members-to-conversation-unqualified" (callsFed addMembersUnqualified) <@> mkNamedAPI @"add-members-to-conversation-unqualified2" (callsFed addMembersUnqualifiedV2) diff --git a/services/galley/src/Galley/API/Public/MLS.hs b/services/galley/src/Galley/API/Public/MLS.hs index fa05f9bf5d6..e068923b7fa 100644 --- a/services/galley/src/Galley/API/Public/MLS.hs +++ b/services/galley/src/Galley/API/Public/MLS.hs @@ -19,6 +19,7 @@ module Galley.API.Public.MLS where import Galley.API.MLS import Galley.App +import Imports import Wire.API.MakesFederatedCall import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.MLS @@ -27,4 +28,4 @@ mlsAPI :: API MLSAPI GalleyEffects mlsAPI = mkNamedAPI @"mls-message" (callsFed (exposeAnnotations postMLSMessageFromLocalUser)) <@> mkNamedAPI @"mls-commit-bundle" (callsFed (exposeAnnotations postMLSCommitBundleFromLocalUser)) - <@> mkNamedAPI @"mls-public-keys" getMLSPublicKeys + <@> mkNamedAPI @"mls-public-keys" (const getMLSPublicKeys) diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index f788e2d349f..a0edf6be781 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -1,4 +1,5 @@ {-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. -- @@ -37,6 +38,9 @@ module Galley.API.Query ensureConvAdmin, getMLSSelfConversation, getMLSSelfConversationWithError, + getMLSOne2OneConversationV5, + getMLSOne2OneConversationV6, + getMLSOne2OneConversationInternal, getMLSOne2OneConversation, isMLSOne2OneEstablished, ) @@ -45,6 +49,7 @@ where import Cassandra qualified as C import Control.Lens import Control.Monad.Extra +import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as LBS import Data.Code import Data.CommaSeparatedList @@ -58,6 +63,7 @@ import Data.Range import Data.Set qualified as Set import Galley.API.Error import Galley.API.MLS +import Galley.API.MLS.Enabled import Galley.API.MLS.One2One import Galley.API.MLS.Types import Galley.API.Mapping @@ -81,6 +87,7 @@ import Imports import Polysemy import Polysemy.Error import Polysemy.Input +import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as P import System.Logger.Class qualified as Logger import Wire.API.Conversation hiding (Member) @@ -93,8 +100,10 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Federation.API import Wire.API.Federation.API.Galley -import Wire.API.Federation.Client (FederatorClient) +import Wire.API.Federation.Client (FederatorClient, getNegotiatedVersion) import Wire.API.Federation.Error +import Wire.API.Federation.Version qualified as Federation +import Wire.API.MLS.Keys import Wire.API.Provider.Bot qualified as Public import Wire.API.Routes.MultiTablePaging qualified as Public import Wire.API.Team.Feature as Public @@ -236,7 +245,7 @@ getRemoteConversationsWithFailures :: getRemoteConversationsWithFailures lusr convs = do -- get self member statuses from the database statusMap <- E.getRemoteConversationStatus (tUnqualified lusr) convs - let remoteView :: Remote RemoteConversation -> Conversation + let remoteView :: Remote RemoteConversationV2 -> Conversation remoteView rconv = Mapping.remoteConversationView lusr @@ -252,8 +261,15 @@ getRemoteConversationsWithFailures lusr convs = do | otherwise = [failedGetConversationLocally (map tUntagged locallyNotFound)] -- request conversations from remote backends - let rpc :: GetConversationsRequest -> FederatorClient 'Galley GetConversationsResponse - rpc = fedClient @'Galley @"get-conversations" + let rpc :: GetConversationsRequest -> FederatorClient 'Galley GetConversationsResponseV2 + rpc req = do + mFedVersion <- getNegotiatedVersion + case mFedVersion of + Nothing -> error "impossible" + Just fedVersion -> + if fedVersion < Federation.V2 + then getConversationsResponseToV2 <$> fedClient @'Galley @"get-conversations@v1" req + else fedClient @'Galley @"get-conversations" req resp <- E.runFederatedConcurrentlyEither locallyFound $ \someConvs -> rpc $ GetConversationsRequest (tUnqualified lusr) (tUnqualified someConvs) @@ -263,8 +279,8 @@ getRemoteConversationsWithFailures lusr convs = do where handleFailure :: (Member P.TinyLog r) => - Either (Remote [ConvId], FederationError) (Remote GetConversationsResponse) -> - Sem r (Either FailedGetConversation [Remote RemoteConversation]) + Either (Remote [ConvId], FederationError) (Remote GetConversationsResponseV2) -> + Sem r (Either FailedGetConversation [Remote RemoteConversationV2]) handleFailure (Left (rcids, e)) = do P.warn $ Logger.msg ("Error occurred while fetching remote conversations" :: ByteString) @@ -728,7 +744,28 @@ getMLSSelfConversation lusr = do -- uses the same function to calculate the conversation ID and corresponding -- group ID, however we /do/ assume that the two backends agree on which of the -- two is responsible for hosting the conversation. -getMLSOne2OneConversation :: +getMLSOne2OneConversationV5 :: + ( Member BrigAccess r, + Member ConversationStore r, + Member (Input Env) r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'NotConnected) r, + Member (ErrorS 'MLSFederatedOne2OneNotSupported) r, + Member FederatorAccess r, + Member TeamStore r, + Member P.TinyLog r + ) => + Local UserId -> + Qualified UserId -> + Sem r Conversation +getMLSOne2OneConversationV5 lself qother = do + if isLocal lself qother + then getMLSOne2OneConversationInternal lself qother + else throwS @MLSFederatedOne2OneNotSupported + +getMLSOne2OneConversationInternal :: ( Member BrigAccess r, Member ConversationStore r, Member (Input Env) r, @@ -743,7 +780,25 @@ getMLSOne2OneConversation :: Local UserId -> Qualified UserId -> Sem r Conversation -getMLSOne2OneConversation lself qother = do +getMLSOne2OneConversationInternal lself qother = + (.conversation) <$> getMLSOne2OneConversation lself qother Nothing + +getMLSOne2OneConversationV6 :: + ( Member BrigAccess r, + Member ConversationStore r, + Member (Input Env) r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'NotConnected) r, + Member FederatorAccess r, + Member TeamStore r, + Member P.TinyLog r + ) => + Local UserId -> + Qualified UserId -> + Sem r (MLSOne2OneConversation MLSPublicKey) +getMLSOne2OneConversationV6 lself qother = do assertMLSEnabled ensureConnectedOrSameTeam lself [qother] let convId = one2OneConvId BaseProtocolMLSTag (tUntagged lself) qother @@ -753,30 +808,61 @@ getMLSOne2OneConversation lself qother = do (getRemoteMLSOne2OneConversation lself qother) convId +getMLSOne2OneConversation :: + ( Member BrigAccess r, + Member ConversationStore r, + Member (Input Env) r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'NotConnected) r, + Member FederatorAccess r, + Member TeamStore r, + Member P.TinyLog r + ) => + Local UserId -> + Qualified UserId -> + Maybe MLSPublicKeyFormat -> + Sem r (MLSOne2OneConversation SomeKey) +getMLSOne2OneConversation lself qother fmt = do + convWithUnformattedKeys <- getMLSOne2OneConversationV6 lself qother + MLSOne2OneConversation convWithUnformattedKeys.conversation + <$> formatPublicKeys fmt convWithUnformattedKeys.publicKeys + getLocalMLSOne2OneConversation :: ( Member ConversationStore r, Member (Error InternalError) r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Input Env) r, + Member (ErrorS MLSNotEnabled) r ) => Local UserId -> Local ConvId -> - Sem r Conversation + Sem r (MLSOne2OneConversation MLSPublicKey) getLocalMLSOne2OneConversation lself lconv = do mconv <- E.getConversation (tUnqualified lconv) - case mconv of + keys <- mlsKeysToPublic <$$> getMLSPrivateKeys + conv <- case mconv of Nothing -> pure (localMLSOne2OneConversation lself lconv) Just conv -> conversationView lself conv + pure $ + MLSOne2OneConversation + { conversation = conv, + publicKeys = keys + } getRemoteMLSOne2OneConversation :: ( Member (Error InternalError) r, Member (Error FederationError) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r + Member FederatorAccess r, + Member (ErrorS MLSNotEnabled) r, + Member TinyLog r ) => Local UserId -> Qualified UserId -> Remote conv -> - Sem r Conversation + Sem r (MLSOne2OneConversation MLSPublicKey) getRemoteMLSOne2OneConversation lself qother rconv = do -- a conversation can only be remote if it is hosted on the other user's domain rother <- @@ -785,15 +871,32 @@ getRemoteMLSOne2OneConversation lself qother rconv = do else throw (InternalErrorWithDescription "Unexpected 1-1 conversation domain") resp <- - E.runFederated rconv $ - fedClient @'Galley @"get-one2one-conversation" $ - GetOne2OneConversationRequest (tUnqualified lself) (tUnqualified rother) + E.runFederated rconv $ do + negotiatedVersion <- getNegotiatedVersion + case negotiatedVersion of + Nothing -> error "impossible" + Just Federation.V0 -> pure . Left . FederationCallFailure $ FederatorClientVersionNegotiationError RemoteTooOld + Just Federation.V1 -> pure . Left . FederationCallFailure $ FederatorClientVersionNegotiationError RemoteTooOld + Just _ -> + fmap Right . fedClient @'Galley @"get-one2one-conversation" $ + GetOne2OneConversationRequest (tUnqualified lself) (tUnqualified rother) case resp of - GetOne2OneConversationOk rc -> + Right (GetOne2OneConversationV2Ok rc) -> pure (remoteMLSOne2OneConversation lself rother rc) - GetOne2OneConversationBackendMismatch -> + Right GetOne2OneConversationV2BackendMismatch -> throw (FederationUnexpectedBody "Backend mismatch when retrieving a remote 1-1 conversation") - GetOne2OneConversationNotConnected -> throwS @'NotConnected + Right GetOne2OneConversationV2NotConnected -> throwS @'NotConnected + Right GetOne2OneConversationV2MLSNotEnabled -> do + -- This is confusing to clients because we do not tell them which backend + -- doesn't have MLS enabled, which would nice information for fixing + -- problems in real world. We do the same thing when sending Welcome + -- messages, so for now, let's do the same thing. + P.warn $ + Logger.field "domain" (toByteString' (tDomain rother)) + . Logger.msg + ("Cannot get remote MLSOne2OneConversation because MLS is not enabled on remote" :: ByteString) + throwS @'MLSNotEnabled + Left e -> throw e -- | Check if an MLS 1-1 conversation has been established, namely if its epoch -- is non-zero. The conversation will only be stored in the database when its @@ -810,7 +913,8 @@ isMLSOne2OneEstablished :: Member (Error InternalError) r, Member (ErrorS 'MLSNotEnabled) r, Member (ErrorS 'NotConnected) r, - Member FederatorAccess r + Member FederatorAccess r, + Member TinyLog r ) => Local UserId -> Qualified UserId -> @@ -840,14 +944,16 @@ isRemoteMLSOne2OneEstablished :: ( Member (ErrorS 'NotConnected) r, Member (Error FederationError) r, Member (Error InternalError) r, - Member FederatorAccess r + Member FederatorAccess r, + Member (ErrorS MLSNotEnabled) r, + Member TinyLog r ) => Local UserId -> Qualified UserId -> Remote conv -> Sem r Bool isRemoteMLSOne2OneEstablished lself qother rconv = do - conv <- getRemoteMLSOne2OneConversation lself qother rconv + conv <- (.conversation) <$> getRemoteMLSOne2OneConversation lself qother rconv pure . (> 0) $ case cnvProtocol conv of ProtocolProteus -> 0 ProtocolMLS meta -> ep meta diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 16fa23d97be..1ecd7e4eab9 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -86,7 +86,6 @@ import Wire.API.Conversation import Wire.API.Conversation qualified as C import Wire.API.Conversation.Action import Wire.API.Conversation.Code hiding (Value) -import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Conversation.Typing import Wire.API.Error.Galley @@ -177,7 +176,6 @@ tests s = test s "get conversations/:domain/:cnv - local" testGetQualifiedLocalConv, test s "get conversations/:domain/:cnv - local, not found" testGetQualifiedLocalConvNotFound, test s "get conversations/:domain/:cnv - local, not participating" testGetQualifiedLocalConvNotParticipating, - test s "get conversations/:domain/:cnv - remote" testGetQualifiedRemoteConv, test s "get conversations/:domain/:cnv - remote, not found" testGetQualifiedRemoteConvNotFound, test s "get conversations/:domain/:cnv - remote, not found on remote" testGetQualifiedRemoteConvNotFoundOnRemote, test s "post conversations/list/v2" testBulkGetQualifiedConvs, @@ -2322,42 +2320,6 @@ testGetQualifiedLocalConvNotParticipating = do const 403 === statusCode const (Just "access-denied") === view (at "label") . responseJsonUnsafe @Object -testGetQualifiedRemoteConv :: TestM () -testGetQualifiedRemoteConv = do - aliceQ <- randomQualifiedUser - let aliceId = qUnqualified aliceQ - loc <- flip toLocalUnsafe () <$> viewFederationDomain - bobId <- randomId - convId <- randomId - let remoteDomain = Domain "far-away.example.com" - bobQ = Qualified bobId remoteDomain - remoteConvId = Qualified convId remoteDomain - bobAsOtherMember = OtherMember bobQ Nothing roleNameWireAdmin - aliceAsLocal = - LocalMember aliceId defMemberStatus Nothing roleNameWireAdmin - aliceAsOtherMember = localMemberToOther (qDomain aliceQ) aliceAsLocal - aliceAsSelfMember = localMemberToSelf loc aliceAsLocal - - connectWithRemoteUser aliceId bobQ - registerRemoteConv remoteConvId bobId Nothing (Set.fromList [aliceAsOtherMember]) - - let mockConversation = mkProteusConv convId bobId roleNameWireAdmin [bobAsOtherMember] - remoteConversationResponse = GetConversationsResponse [mockConversation] - expected = - Conversation - remoteConvId - mockConversation.metadata - (ConvMembers aliceAsSelfMember mockConversation.members.others) - ProtocolProteus - - (respAll, _) <- - withTempMockFederator' - (mockReply remoteConversationResponse) - (getConvQualified aliceId remoteConvId) - - conv <- responseJsonUnsafe <$> (pure respAll getRequest asum - [ guard (d == remoteDomainA) *> mockReply (GetConversationsResponse [mockConversationA]), - guard (d == remoteDomainB) *> mockReply (GetConversationsResponse [mockConversationB]), + [ guard (d == remoteDomainA) *> mockReply (GetConversationsResponseV2 [mockConversationA]), + guard (d == remoteDomainB) *> mockReply (GetConversationsResponseV2 [mockConversationB]), guard (d == remoteDomainC) *> liftIO (throw (DiscoveryFailureSrvNotAvailable "domainC")), do r <- getRequest @@ -3145,7 +3107,7 @@ putRemoteConvMemberOk update = do (qUnqualified qbob) roleNameWireMember [localMemberToOther remoteDomain bobAsLocal] - remoteConversationResponse = GetConversationsResponse [mockConversation] + remoteConversationResponse = GetConversationsResponseV2 [mockConversation] (rs, _) <- withTempMockFederator' (mockReply remoteConversationResponse) @@ -3467,7 +3429,7 @@ testOne2OneConversationRequest shouldBeLocal actor desired = do pure . map omQualifiedId . cmOthers . cnvMembers $ conv RemoteActor -> do fedGalleyClient <- view tsFedGalleyClient - GetConversationsResponse convs <- + GetConversationsResponseV2 convs <- runFedClient @"get-conversations" fedGalleyClient (tDomain bob) $ GetConversationsRequest { userId = tUnqualified bob, @@ -3486,7 +3448,7 @@ testOne2OneConversationRequest shouldBeLocal actor desired = do found <- do let rconv = mkProteusConv (qUnqualified convId) (tUnqualified bob) roleNameWireAdmin [] (resp, _) <- - withTempMockFederator' (mockReply (GetConversationsResponse [rconv])) $ + withTempMockFederator' (mockReply (GetConversationsResponseV2 [rconv])) $ getConvQualified (tUnqualified alice) convId pure $ statusCode resp == 200 liftIO $ found @?= ((actor, desired) == (LocalActor, Included)) diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 5a3e0221fff..bb178c6f79b 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -137,7 +137,7 @@ getConversationsAllFound = do fedGalleyClient <- view tsFedGalleyClient - GetConversationsResponse convs <- + GetConversationsResponseV2 convs <- runFedClient @"get-conversations" fedGalleyClient (qDomain aliceQ) $ GetConversationsRequest (qUnqualified aliceQ) @@ -183,7 +183,7 @@ getConversationsNotPartOf = do fedGalleyClient <- view tsFedGalleyClient rando <- Id <$> liftIO nextRandom - GetConversationsResponse convs <- + GetConversationsResponseV2 convs <- runFedClient @"get-conversations" fedGalleyClient localDomain $ GetConversationsRequest rando [qUnqualified . cnvQualifiedId $ cnv1] liftIO $ assertEqual "conversation list not empty" [] convs diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 0783fce56e7..7f7431bb3cc 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -2292,9 +2292,9 @@ mkProteusConv :: UserId -> RoleName -> [OtherMember] -> - RemoteConversation + RemoteConversationV2 mkProteusConv cnvId creator selfRole otherMembers = - RemoteConversation + RemoteConversationV2 cnvId ( ConversationMetadata RegularConv diff --git a/services/galley/test/resources/ecdsa_secp256r1_sha256.pem b/services/galley/test/resources/backendA/ecdsa_secp256r1_sha256.pem similarity index 100% rename from services/galley/test/resources/ecdsa_secp256r1_sha256.pem rename to services/galley/test/resources/backendA/ecdsa_secp256r1_sha256.pem diff --git a/services/galley/test/resources/ecdsa_secp384r1_sha384.pem b/services/galley/test/resources/backendA/ecdsa_secp384r1_sha384.pem similarity index 100% rename from services/galley/test/resources/ecdsa_secp384r1_sha384.pem rename to services/galley/test/resources/backendA/ecdsa_secp384r1_sha384.pem diff --git a/services/galley/test/resources/ecdsa_secp521r1_sha512.pem b/services/galley/test/resources/backendA/ecdsa_secp521r1_sha512.pem similarity index 100% rename from services/galley/test/resources/ecdsa_secp521r1_sha512.pem rename to services/galley/test/resources/backendA/ecdsa_secp521r1_sha512.pem diff --git a/services/galley/test/resources/ed25519.pem b/services/galley/test/resources/backendA/ed25519.pem similarity index 100% rename from services/galley/test/resources/ed25519.pem rename to services/galley/test/resources/backendA/ed25519.pem diff --git a/services/galley/test/resources/backendB/ecdsa_secp256r1_sha256.pem b/services/galley/test/resources/backendB/ecdsa_secp256r1_sha256.pem new file mode 100644 index 00000000000..260cdfe0e3a --- /dev/null +++ b/services/galley/test/resources/backendB/ecdsa_secp256r1_sha256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCUxypWAvn5V0pRz7 +DGYAhCmwZAkDW7Kid0CvsaLwutahRANCAARFmzfgJgQBGVCEJRB1WYGm2J0167aw +YRG7cSb74vHuaHaKio0c24n7o/daaBNOZMaBCi30lhL1YEzLI0wVJF51 +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/backendB/ecdsa_secp384r1_sha384.pem b/services/galley/test/resources/backendB/ecdsa_secp384r1_sha384.pem new file mode 100644 index 00000000000..8d4ede9322f --- /dev/null +++ b/services/galley/test/resources/backendB/ecdsa_secp384r1_sha384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAfG49ge824AADZtVDW +JwWuJj/MoULnpNB1K3G4iRRvODb5E5yH7myhKSb3oeHnKaShZANiAARm+FFL69DT +Qk3tAVFQBP7ND7eu1Oq4VYyxcmynJj/NFIpCCgOs28AcKo6adqXOJizgeGf2/W4P +x+7Vi+Ir0TyIMpWBqo61G2jMDKMF23Yw/85tO1NdcL00As7kLF54nso= +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/backendB/ecdsa_secp521r1_sha512.pem b/services/galley/test/resources/backendB/ecdsa_secp521r1_sha512.pem new file mode 100644 index 00000000000..4dc9d6cb35e --- /dev/null +++ b/services/galley/test/resources/backendB/ecdsa_secp521r1_sha512.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIACsFBjEdDV0Xb6AZM +g8fu/CLS+Tcd6MHQLEZ/G1abf7EIPTr8nCLvJqDEGVtgzqzqiW/ej8YOJLmIX7Xb +WLsPCr6hgYkDgYYABAG4C1/gxxaXR+8y/L3PGd45A3Rc54dpWVUvSWD7M50msW0c +Gs5gzLSMobNGUFeLfjzB0BbSdFALqawAsmBDi/LqrACkHUvBjUp4DfjkJwWdJWwz +/goHs+u1GB9MWVqsFmNUVgHjPDTbC4npMgVXWLELo3O9IzDAuvM2KbTjKo3djd/u +yw== +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/backendB/ed25519.pem b/services/galley/test/resources/backendB/ed25519.pem new file mode 100644 index 00000000000..14ca43a284b --- /dev/null +++ b/services/galley/test/resources/backendB/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIO5vqw3LGpDiQ9AVsLuNp9BoWqXX+dat9PDYtcSgefcr +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend1/ecdsa_secp256r1_sha256.pem b/services/galley/test/resources/dynBackend1/ecdsa_secp256r1_sha256.pem new file mode 100644 index 00000000000..4aa899a53c1 --- /dev/null +++ b/services/galley/test/resources/dynBackend1/ecdsa_secp256r1_sha256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgX3aqY4POOuFTDjI6 +rNyuCUARS0karqX6omz+ZHLBpmChRANCAAQBr9wUSSDssSVQylUfrIRoN6uNxJHu +/IfMnvieXDhS42a/R59G0YnZ+43Z0OHiclfIMYvMT4UzwmKNqF8rKGTh +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend1/ecdsa_secp384r1_sha384.pem b/services/galley/test/resources/dynBackend1/ecdsa_secp384r1_sha384.pem new file mode 100644 index 00000000000..d234e3c8372 --- /dev/null +++ b/services/galley/test/resources/dynBackend1/ecdsa_secp384r1_sha384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBEDdTqi5zs5Zk/guXe +tbonA/9ZACWuoREM0sFPtIj3Sm0oOtRr7XkzHybAzKcTQwqhZANiAARyFcyRUpNM +b8XTSYk9AoVNxQjSFc5ENr99G/WJj6PPXinSo5ixazYbBGXt8N/jSr5U7JJHNQOp +/j0ZE2Ba/ARTU6bBySW5cuPn45o56c7aAv8743wc73Vvx57JOgCZxeY= +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend1/ecdsa_secp521r1_sha512.pem b/services/galley/test/resources/dynBackend1/ecdsa_secp521r1_sha512.pem new file mode 100644 index 00000000000..eabcd72c8ba --- /dev/null +++ b/services/galley/test/resources/dynBackend1/ecdsa_secp521r1_sha512.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIB+aX/6XEvPsiiRep9 +yk9IzN7PWTOGXVB0/8MlvPJZEN/xhmaQQhhs+nCZ5PNo+/03bTe6ge4k+oxnjzLO +m4ST2fyhgYkDgYYABAFpStisSMDJyecgrzj/xyAoCVo1rMq5PqhgpaQ8uiR2Auwn +dVLk4RdC7Zqxx1j6gKy0YihlUeHQt7gr4/+6Q3muBwCt2IghBqyZL0by9A1LKRvS +vUJxwv1Iu8Sl2uljRP62QuE9ETOiH8BnCA+GRmwOUFZnxH/NLvT/OQSClUFQydVK +mQ== +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend1/ed25519.pem b/services/galley/test/resources/dynBackend1/ed25519.pem new file mode 100644 index 00000000000..5fa82d186bb --- /dev/null +++ b/services/galley/test/resources/dynBackend1/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEICBjsYFFgSL4mLwbQKlP1eF/AHyb+1z4Vz6kDr1lMG3/ +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend2/ecdsa_secp256r1_sha256.pem b/services/galley/test/resources/dynBackend2/ecdsa_secp256r1_sha256.pem new file mode 100644 index 00000000000..d71d16694f6 --- /dev/null +++ b/services/galley/test/resources/dynBackend2/ecdsa_secp256r1_sha256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEfaYkGCkcUbE/EhE +BMGZU9RKCyvgi/UESXvhQFgSje+hRANCAATnGRIEOs7xR64zD7yB2zbmwkgHL35o +scFaK0P+zSQ+Q6St4j+cb9+igjn8aFddVJiuWnMPsceLg9KueVXgNSWW +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend2/ecdsa_secp384r1_sha384.pem b/services/galley/test/resources/dynBackend2/ecdsa_secp384r1_sha384.pem new file mode 100644 index 00000000000..271c34cab68 --- /dev/null +++ b/services/galley/test/resources/dynBackend2/ecdsa_secp384r1_sha384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAo8PEN99hC+GQ1L5eO +eQWP9w2/qL/gVOwzb48HloaDWkZlhTlth7N8v0KMkoyh3EmhZANiAARF8+nQz+cX +/fGTX9wrz+kn+mZHV0+Kh2j3n8bNcPp94L3MR/8t1qqQvoAvGFLUZULF7H7cU5st +HBiHxbVbz76Mqol9ZPbwJzCmcI+WAf1qPIiOjOWkceZM/YznUP/RM6Q= +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend2/ecdsa_secp521r1_sha512.pem b/services/galley/test/resources/dynBackend2/ecdsa_secp521r1_sha512.pem new file mode 100644 index 00000000000..95a40e376e3 --- /dev/null +++ b/services/galley/test/resources/dynBackend2/ecdsa_secp521r1_sha512.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAbXN6cs+k71Pd3+gJ +hYTa5yMmmcitnp5myCOoKp3bvULH6naFQkg4PrMH3cnpgPcVaj6ZK+6m0qnlLg+x +PXQ9pQChgYkDgYYABAA6GUqOxi4NHNTSC+tphkagMqygFkrEzLHudYmwk2OwEDhG +cuf+ICuS+FmepZuMAI5QGqMNHLXttH0JXSHoob9eZAEWy8b0xOdj1GZHdNdk4aXL +tvwuzsZpeBKpMNwnTwggcJsepbBzXPANSyCnDyoCmKaHAQwtca2KFlwBB3uMRHve +2Q== +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend2/ed25519.pem b/services/galley/test/resources/dynBackend2/ed25519.pem new file mode 100644 index 00000000000..c73ba496405 --- /dev/null +++ b/services/galley/test/resources/dynBackend2/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIBhKAk3pknMLukvifvnT6ujsMvlLKTa/1IjXS2lD5XTz +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend3/ecdsa_secp256r1_sha256.pem b/services/galley/test/resources/dynBackend3/ecdsa_secp256r1_sha256.pem new file mode 100644 index 00000000000..b3dbe2f828d --- /dev/null +++ b/services/galley/test/resources/dynBackend3/ecdsa_secp256r1_sha256.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNhdHUB18rQWrJU4r +epN5CxOREiAYzKsGNuZ6//q5gbuhRANCAAQUvVXIJuKB81w1HBwgriR3UJ/Dy0se +u88O3pO+X28Uon/LczW9K2hzU5HwR+JF5NiKJTi3QQBzeaQTc1ybljf4 +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend3/ecdsa_secp384r1_sha384.pem b/services/galley/test/resources/dynBackend3/ecdsa_secp384r1_sha384.pem new file mode 100644 index 00000000000..9606a469ff5 --- /dev/null +++ b/services/galley/test/resources/dynBackend3/ecdsa_secp384r1_sha384.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDClNF0H/IIY4Z8WPOs1 +G4JonwQYqNuYnWgRGiDWvhXyXTI+uuhoN0PM9juzYuaJsoKhZANiAARB1q6sJd82 +MMI/4vJQbJBSnzOQWKNr8K+tsNOAGKuFGBBX1kOGsPiUJyEq64nnT97MJRfc/MW2 +A9gjrbMlM4fNoF3V6bqykfMDRBmOQ8oWovNFiH9Csbz46sjAsQKSw9g= +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend3/ecdsa_secp521r1_sha512.pem b/services/galley/test/resources/dynBackend3/ecdsa_secp521r1_sha512.pem new file mode 100644 index 00000000000..00229707a10 --- /dev/null +++ b/services/galley/test/resources/dynBackend3/ecdsa_secp521r1_sha512.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAewieMnAzbnPuliiT +3Qj7v4zoxV308YQ2hDa17AWFEEg4ujeTHfRGZLjyMyh+fGnDVegBzJfdHfuExCcj +2RtSvp2hgYkDgYYABADr9KSC4esqdqkAoQcDvZa9mbVgow0+P/BS8Cj2Q5dnh0cj +2+p+F7cIEnvJJ7AY5heizlAyTxSv5U4Zx4Iein6A8QBqD7B0I0bkY2/ucVS1th0Z +9QAanTIqwxQ3HgJccHctI/M0QPgimOLYSmHpDmFlMSgtjKrJ6nOWgY1D2ev8l4n7 +xQ== +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/dynBackend3/ed25519.pem b/services/galley/test/resources/dynBackend3/ed25519.pem new file mode 100644 index 00000000000..55dc2d42ff8 --- /dev/null +++ b/services/galley/test/resources/dynBackend3/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIF7bh0Ix5rSrpGVYJCn/sZZJO46pPsF8yPO46wDcB6bF +-----END PRIVATE KEY----- diff --git a/services/galley/test/resources/foo.sh b/services/galley/test/resources/foo.sh new file mode 100755 index 00000000000..1d57fccbc5a --- /dev/null +++ b/services/galley/test/resources/foo.sh @@ -0,0 +1,4 @@ +openssl genpkey -algorithm ed25519 > ed25519.pem +openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 > ecdsa_secp256r1_sha256.pem +openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-384 > ecdsa_secp384r1_sha384.pem +openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-521 > ecdsa_secp521r1_sha512.pem diff --git a/services/galley/test/unit/Test/Galley/Mapping.hs b/services/galley/test/unit/Test/Galley/Mapping.hs index c18bb63f903..b52bf67fd92 100644 --- a/services/galley/test/unit/Test/Galley/Mapping.hs +++ b/services/galley/test/unit/Test/Galley/Mapping.hs @@ -41,7 +41,7 @@ import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Federation.API.Galley ( RemoteConvMembers (..), - RemoteConversation (..), + RemoteConversationV2 (..), ) import Wire.Sem.Logger qualified as P diff --git a/services/integration.yaml b/services/integration.yaml index 201b99ce025..174d6db1477 100644 --- a/services/integration.yaml +++ b/services/integration.yaml @@ -129,12 +129,30 @@ dynamicBackends: dynamic-backend-1: domain: d1.example.com federatorExternalPort: 10098 + mlsPrivateKeyPaths: + removal: + ed25519: "test/resources/dynBackend1/ed25519.pem" + ecdsa_secp256r1_sha256: "test/resources/dynBackend1/ecdsa_secp256r1_sha256.pem" + ecdsa_secp384r1_sha384: "test/resources/dynBackend1/ecdsa_secp384r1_sha384.pem" + ecdsa_secp521r1_sha512: "test/resources/dynBackend1/ecdsa_secp521r1_sha512.pem" dynamic-backend-2: domain: d2.example.com federatorExternalPort: 11098 + mlsPrivateKeyPaths: + removal: + ed25519: "test/resources/dynBackend2/ed25519.pem" + ecdsa_secp256r1_sha256: "test/resources/dynBackend2/ecdsa_secp256r1_sha256.pem" + ecdsa_secp384r1_sha384: "test/resources/dynBackend2/ecdsa_secp384r1_sha384.pem" + ecdsa_secp521r1_sha512: "test/resources/dynBackend2/ecdsa_secp521r1_sha512.pem" dynamic-backend-3: domain: d3.example.com federatorExternalPort: 12098 + mlsPrivateKeyPaths: + removal: + ed25519: "test/resources/dynBackend3/ed25519.pem" + ecdsa_secp256r1_sha256: "test/resources/dynBackend3/ecdsa_secp256r1_sha256.pem" + ecdsa_secp384r1_sha384: "test/resources/dynBackend3/ecdsa_secp384r1_sha384.pem" + ecdsa_secp521r1_sha512: "test/resources/dynBackend3/ecdsa_secp521r1_sha512.pem" rabbitmq: host: localhost From e84d929a2a08e830b90633ca0250db102d029459 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 11 Sep 2024 17:15:24 +0200 Subject: [PATCH 060/136] docs for dpop access token signing key config (#4234) --- .../src/developer/reference/config-options.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 1c90bdfcc57..e8b949b6edb 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -332,6 +332,36 @@ mlsE2EId: lockStatus: unlocked ``` +#### Key for DPoP access token signing + +The key for signing DPoP access tokens has to be configured at path `brig.secrets.dpopSigKeyBundle` e.g. as follows: + +```yaml +brig: + secrets: + dpopSigKeyBundle: | + -----BEGIN PRIVATE KEY----- + MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgokD9kGYErMooLqpv + IRUVCtV1l6HmtqTJUFun0/4XLuahRANCAASWH/qkgOLwZz1GvEt0ch4HPRQUoj9U + TL8L7QANF9JztsEQ2omrX9l7RoosjAm+PKwrL+c3GiT63CSd1qrUpoZa + -----END PRIVATE KEY----- +``` + +The corresponding public key has to be known by the ACME server. + +The key must be an ECDSA P-256 key and can be created with the following `openssl` command: + +```shell +openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 --out private.pem +``` + +To get the public key run: + +```shell +openssl ec -in private.pem -pubout --out public.pem +``` + + ### Federation Domain Regardless of whether a backend wants to enable federation or not, the operator From 51169a438295538ca59be693a437cdbba81e29a0 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 11 Sep 2024 17:23:36 +0200 Subject: [PATCH 061/136] [fix docs] truncated key to prevent unintentional usage (#4235) --- docs/src/developer/reference/config-options.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index e8b949b6edb..e6dc72bfa62 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -341,9 +341,7 @@ brig: secrets: dpopSigKeyBundle: | -----BEGIN PRIVATE KEY----- - MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgokD9kGYErMooLqpv - IRUVCtV1l6HmtqTJUFun0/4XLuahRANCAASWH/qkgOLwZz1GvEt0ch4HPRQUoj9U - TL8L7QANF9JztsEQ2omrX9l7RoosjAm+PKwrL+c3GiT63CSd1qrUpoZa + MIGHAgEAMBMGByqGSM49.... -----END PRIVATE KEY----- ``` From e537040b477296ff668ba26b3775c42cd3a27e3b Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 12 Sep 2024 16:57:40 +0200 Subject: [PATCH 062/136] Add rabbitmq gc to `make full-clean`. (#4238) --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index a3aac683f65..de21e381a80 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,7 @@ full-clean: clean rm -rf ~/.cache/hie-bios rm -rf ./dist-newstyle ./.env direnv reload + rabbitmqadmin -f pretty_json list queues vhost name messages | jq -r '.[] | "rabbitmqadmin delete queue name=\(.name) --vhost=\(.vhost)"' | bash @echo -e "\n\n*** NOTE: you may want to also 'rm -rf ~/.cabal/store \$$CABAL_DIR/store', not sure.\n" .PHONY: clean From b755a6d78f9555d8c68e035a7d9978ab95f02abc Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 12 Sep 2024 17:08:27 +0200 Subject: [PATCH 063/136] Update generated swagger.json for client API v6 (#4232) --- changelog.d/1-api-changes/finalise-v6 | 2 +- services/brig/docs/swagger-v6.json | 233 ++++++++++++++++++++++++-- 2 files changed, 217 insertions(+), 18 deletions(-) diff --git a/changelog.d/1-api-changes/finalise-v6 b/changelog.d/1-api-changes/finalise-v6 index 03633def115..c3a5b395701 100644 --- a/changelog.d/1-api-changes/finalise-v6 +++ b/changelog.d/1-api-changes/finalise-v6 @@ -1 +1 @@ -Finalise version 6 and introduce new development version 7 +Finalise version 6 and introduce new development version 7 (#4179, ##) diff --git a/services/brig/docs/swagger-v6.json b/services/brig/docs/swagger-v6.json index 3f0ab04e358..8b4dab3dbef 100644 --- a/services/brig/docs/swagger-v6.json +++ b/services/brig/docs/swagger-v6.json @@ -214,7 +214,7 @@ ], "type": "object" }, - "AllFeatureConfigs": { + "AllTeamFeatures": { "properties": { "appLock": { "$ref": "#/components/schemas/AppLockConfig.LockableFeature" @@ -1499,7 +1499,7 @@ "type": "integer" }, "epoch_timestamp": { - "$ref": "#/components/schemas/UTCTime" + "$ref": "#/components/schemas/Epoch Timestamp" }, "group_id": { "$ref": "#/components/schemas/GroupId" @@ -1553,6 +1553,7 @@ "members", "group_id", "epoch", + "epoch_timestamp", "cipher_suite" ], "type": "object" @@ -1586,7 +1587,7 @@ "type": "integer" }, "epoch_timestamp": { - "$ref": "#/components/schemas/UTCTime" + "$ref": "#/components/schemas/Epoch Timestamp" }, "group_id": { "$ref": "#/components/schemas/GroupId" @@ -1641,6 +1642,7 @@ "members", "group_id", "epoch", + "epoch_timestamp", "cipher_suite" ], "type": "object" @@ -2290,6 +2292,11 @@ ], "type": "object" }, + "Epoch Timestamp": { + "example": "2021-05-12T10:52:02Z", + "format": "yyyy-mm-ddThh:MM:ssZ", + "type": "string" + }, "Event": { "properties": { "conversation": { @@ -2341,7 +2348,7 @@ "type": "integer" }, "epoch_timestamp": { - "$ref": "#/components/schemas/UTCTime" + "$ref": "#/components/schemas/Epoch Timestamp" }, "group_id": { "$ref": "#/components/schemas/GroupId" @@ -2485,6 +2492,7 @@ "members", "group_id", "epoch", + "epoch_timestamp", "cipher_suite", "qualified_recipient", "receipt_mode", @@ -3321,16 +3329,16 @@ "MLSKeys": { "properties": { "ecdsa_secp256r1_sha256": { - "$ref": "#/components/schemas/MLSPublicKey" + "$ref": "#/components/schemas/SomeKey" }, "ecdsa_secp384r1_sha384": { - "$ref": "#/components/schemas/MLSPublicKey" + "$ref": "#/components/schemas/SomeKey" }, "ecdsa_secp521r1_sha512": { - "$ref": "#/components/schemas/MLSPublicKey" + "$ref": "#/components/schemas/SomeKey" }, "ed25519": { - "$ref": "#/components/schemas/MLSPublicKey" + "$ref": "#/components/schemas/SomeKey" } }, "required": [ @@ -3374,6 +3382,21 @@ ], "type": "object" }, + "MLSOne2OneConversation_MLSPublicKey": { + "properties": { + "conversation": { + "$ref": "#/components/schemas/Conversation" + }, + "public_keys": { + "$ref": "#/components/schemas/MLSKeysByPurpose" + } + }, + "required": [ + "conversation", + "public_keys" + ], + "type": "object" + }, "MLSPublicKey": { "example": "ZXhhbXBsZQo=", "type": "string" @@ -4154,11 +4177,19 @@ "maxLength": 256, "minLength": 6, "type": "string" + }, + "sessions": { + "description": "The OAuth client's sessions", + "items": { + "$ref": "#/components/schemas/OAuthSession" + }, + "type": "array" } }, "required": [ "id", - "name" + "name", + "sessions" ], "type": "object" }, @@ -4242,6 +4273,21 @@ ], "type": "object" }, + "OAuthSession": { + "properties": { + "created_at": { + "$ref": "#/components/schemas/UTCTimeMillis" + }, + "refresh_token_id": { + "$ref": "#/components/schemas/UUID" + } + }, + "required": [ + "refresh_token_id", + "created_at" + ], + "type": "object" + }, "Object": { "additionalProperties": true, "description": "A single notification event", @@ -4391,6 +4437,16 @@ ], "type": "object" }, + "PasswordReqBody": { + "properties": { + "password": { + "maxLength": 1024, + "minLength": 6, + "type": "string" + } + }, + "type": "object" + }, "PasswordReset": { "properties": { "email": { @@ -5513,6 +5569,7 @@ ], "type": "object" }, + "SomeKey": {}, "Sso": { "properties": { "issuer": { @@ -5911,6 +5968,7 @@ "type": "string" }, "UTCTimeMillis": { + "description": "The time when the session was created", "example": "2021-05-12T10:52:02.671Z", "format": "yyyy-mm-ddThh:MM:ss.qqqZ", "type": "string" @@ -11398,7 +11456,7 @@ }, "/conversations/one2one/{usr_domain}/{usr}": { "get": { - "description": " [internal route ID: \"get-one-to-one-mls-conversation\"]\n\n", + "description": " [internal route ID: \"get-one-to-one-mls-conversation@v6\"]\n\n", "parameters": [ { "in": "path", @@ -11423,12 +11481,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Conversation" + "$ref": "#/components/schemas/MLSOne2OneConversation_MLSPublicKey" } }, "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/Conversation" + "$ref": "#/components/schemas/MLSOne2OneConversation_MLSPublicKey" } } }, @@ -15911,7 +15969,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AllFeatureConfigs" + "$ref": "#/components/schemas/AllTeamFeatures" } } }, @@ -17179,7 +17237,21 @@ }, "/mls/public-keys": { "get": { - "description": " [internal route ID: \"mls-public-keys-v6\"]\n\n", + "description": " [internal route ID: \"mls-public-keys\"]\n\nThe format of the returned key is determined by the `format` query parameter:\n - raw (default): base64-encoded raw public keys\n - jwk: keys are nested objects in JWK format.", + "parameters": [ + { + "in": "query", + "name": "format", + "required": false, + "schema": { + "enum": [ + "raw", + "jwk" + ], + "type": "string" + } + } + ], "responses": { "200": { "content": { @@ -17231,7 +17303,7 @@ } } }, - "description": "MLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" + "description": "Invalid `format`\n\nMLS is not configured on this backend. See docs.wire.com for instructions on how to enable it (label: `mls-not-enabled`)" } }, "summary": "Get public keys used by the backend to sign external proposals" @@ -17609,7 +17681,7 @@ }, "/oauth/applications/{OAuthClientId}": { "delete": { - "description": " [internal route ID: \"revoke-oauth-account-access\"]\n\n", + "description": " [internal route ID: \"revoke-oauth-account-access-v6\"]\n\n", "parameters": [ { "description": "The ID of the OAuth client", @@ -17630,6 +17702,133 @@ "summary": "Revoke account access from an OAuth application" } }, + "/oauth/applications/{OAuthClientId}/sessions/{RefreshTokenId}": { + "delete": { + "description": " [internal route ID: \"delete-oauth-refresh-token\"]\n\nRevoke an active OAuth session by providing the refresh token ID.", + "parameters": [ + { + "description": "The ID of the OAuth client", + "in": "path", + "name": "OAuthClientId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "description": "The ID of the refresh token", + "in": "path", + "name": "RefreshTokenId", + "required": true, + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "$ref": "#/components/schemas/PasswordReqBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": [], + "items": {}, + "maxItems": 0, + "type": "array" + } + } + }, + "description": "" + }, + "403": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 403, + "label": "access-denied", + "message": "Access denied." + }, + "properties": { + "code": { + "enum": [ + 403 + ], + "type": "integer" + }, + "label": { + "enum": [ + "access-denied" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "Access denied. (label: `access-denied`)" + }, + "404": { + "content": { + "application/json;charset=utf-8": { + "schema": { + "example": { + "code": 404, + "label": "not-found", + "message": "OAuth client not found" + }, + "properties": { + "code": { + "enum": [ + 404 + ], + "type": "integer" + }, + "label": { + "enum": [ + "not-found" + ], + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "label", + "message" + ], + "type": "object" + } + } + }, + "description": "`OAuthClientId` or `RefreshTokenId` not found\n\nOAuth client not found (label: `not-found`)" + } + }, + "summary": "Revoke an active OAuth session" + } + }, "/oauth/authorization/codes": { "post": { "description": " [internal route ID: \"create-oauth-auth-code\"]\n\nCurrently only supports the 'code' response type, which corresponds to the authorization code flow.", @@ -23654,7 +23853,7 @@ "content": { "application/json;charset=utf-8": { "schema": { - "$ref": "#/components/schemas/AllFeatureConfigs" + "$ref": "#/components/schemas/AllTeamFeatures" } } }, From f1bc7b990d6d81f70cbf079a2f2a94932b36a2b7 Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Thu, 12 Sep 2024 18:00:07 +0200 Subject: [PATCH 064/136] [WPB-8887] wire-subsystems: implement the GetBy* account queries, includes InvitationCodeStore. (#4218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [wip] initial impl for GetBy* account queries as Effect and interpreter - new Effect operation GetAccountBy in UserSubsystem - new record GetBy - new stores ActivationCodeStore and InvitationCodeStore - new sql quasiquoter in cassandra-util - some more Ord instances derived - new function tSplit for the use with ViewPatterns * Account for inviteeUrl visibility. * Renamed lookupAccounts to getUsers. * Make route names unique. * weeder. * Get local domain from api in some more places. * Simplify UserSubsystem operations set. * Tweak legacy integration test. --------- Co-authored-by: Igor Ranieri Co-authored-by: Matthias Fischmann Co-authored-by: Marko Dimjašević Co-authored-by: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> --- Makefile | 2 +- changelog.d/5-internal/wpb-8887 | 1 + integration/test/Test/Spar.hs | 4 +- libs/brig-types/src/Brig/Types/User.hs | 1 - libs/cassandra-util/cassandra-util.cabal | 2 + libs/cassandra-util/default.nix | 2 + libs/cassandra-util/src/Cassandra.hs | 1 + libs/cassandra-util/src/Cassandra/QQ.hs | 18 + libs/imports/src/Imports.hs | 4 - libs/types-common/default.nix | 2 + .../src/Data/HavePendingInvitations.hs | 14 + libs/types-common/src/Data/Qualified.hs | 5 + libs/types-common/src/Util/Timeout.hs | 32 + libs/types-common/types-common.cabal | 3 + .../Wire/API/Routes/Internal/Brig/OAuth.hs | 2 +- .../src/Wire/API/Routes/Internal/Galley.hs | 2 +- .../src/Wire/API/Routes/Public/Brig.hs | 8 +- .../src/Wire/API/Routes/Public/Brig/OAuth.hs | 12 +- libs/wire-api/src/Wire/API/Team/Invitation.hs | 39 +- libs/wire-api/src/Wire/API/User.hs | 23 +- .../src/Wire/API/User/EmailAddress.hs | 8 - libs/wire-api/src/Wire/API/User/Identity.hs | 2 +- libs/wire-api/src/Wire/API/User/Scim.hs | 8 - .../Golden/Generated/InvitationList_team.hs | 876 +++++++++--------- .../API/Golden/Generated/Invitation_team.hs | 320 +++---- libs/wire-subsystems/default.nix | 6 + .../src/Wire/ActivationCodeStore.hs | 30 + .../src/Wire/ActivationCodeStore/Cassandra.hs | 37 + .../AuthenticationSubsystem/Interpreter.hs | 19 +- .../src/Wire/GalleyAPIAccess.hs | 3 +- .../src/Wire/GalleyAPIAccess/Rpc.hs | 7 +- .../src/Wire/InvitationCodeStore.hs | 136 +++ .../src/Wire/InvitationCodeStore/Cassandra.hs | 194 ++++ .../src/Wire/PasswordResetCodeStore.hs | 7 +- .../Wire/PasswordResetCodeStore/Cassandra.hs | 17 +- libs/wire-subsystems/src/Wire/StoredUser.hs | 5 + libs/wire-subsystems/src/Wire/UserStore.hs | 5 +- .../src/Wire/UserStore/Cassandra.hs | 24 +- .../wire-subsystems/src/Wire/UserSubsystem.hs | 88 +- .../src/Wire/UserSubsystem/Interpreter.hs | 128 ++- .../InterpreterSpec.hs | 22 +- .../test/unit/Wire/MiniBackend.hs | 64 +- .../MockInterpreters/ActivationCodeStore.hs | 13 + .../MockInterpreters/InvitationCodeStore.hs | 32 + .../unit/Wire/MockInterpreters/UserStore.hs | 2 +- .../Wire/MockInterpreters/UserSubsystem.hs | 11 +- .../Wire/UserSubsystem/InterpreterSpec.hs | 298 ++++++ libs/wire-subsystems/wire-subsystems.cabal | 9 + services/brig/brig.cabal | 2 - services/brig/default.nix | 2 - services/brig/src/Brig/API/Auth.hs | 25 +- services/brig/src/Brig/API/Client.hs | 24 +- services/brig/src/Brig/API/Connection.hs | 11 +- services/brig/src/Brig/API/Internal.hs | 98 +- services/brig/src/Brig/API/OAuth.hs | 51 +- services/brig/src/Brig/API/Public.hs | 69 +- services/brig/src/Brig/API/Types.hs | 2 + services/brig/src/Brig/API/User.hs | 154 +-- services/brig/src/Brig/App.hs | 6 +- .../brig/src/Brig/CanonicalInterpreter.hs | 8 + services/brig/src/Brig/Data/Activation.hs | 46 +- services/brig/src/Brig/Data/Client.hs | 29 +- services/brig/src/Brig/Data/MLS/KeyPackage.hs | 2 +- services/brig/src/Brig/Data/User.hs | 108 +-- .../brig/src/Brig/InternalEvent/Process.hs | 5 +- services/brig/src/Brig/Options.hs | 24 +- services/brig/src/Brig/Provider/API.hs | 14 +- services/brig/src/Brig/Run.hs | 1 + services/brig/src/Brig/Team/API.hs | 250 +++-- services/brig/src/Brig/Team/DB.hs | 323 ------- services/brig/src/Brig/Team/Util.hs | 2 +- services/brig/src/Brig/User/Auth.hs | 79 +- services/brig/src/Brig/User/Auth/Cookie.hs | 1 + services/brig/src/Brig/User/EJPD.hs | 1 + services/brig/test/integration/API/Team.hs | 95 +- .../brig/test/integration/API/Team/Util.hs | 8 +- .../brig/test/integration/API/User/Account.hs | 15 +- .../brig/test/integration/API/User/Auth.hs | 7 +- .../brig/test/integration/API/User/Client.hs | 6 +- .../test/integration/API/User/Connection.hs | 5 +- .../brig/test/integration/API/User/Handles.hs | 5 +- .../integration/API/User/PasswordReset.hs | 3 +- .../test/integration/API/User/RichInfo.hs | 3 +- .../integration/API/UserPendingActivation.hs | 2 +- services/galley/src/Galley/API/Internal.hs | 2 +- services/galley/test/integration/API.hs | 10 +- services/galley/test/integration/API/Util.hs | 4 +- services/spar/default.nix | 4 +- services/spar/spar.cabal | 2 +- services/spar/src/Spar/API.hs | 1 + services/spar/src/Spar/Intra/Brig.hs | 1 + services/spar/src/Spar/Intra/BrigApp.hs | 5 +- services/spar/src/Spar/Sem/BrigAccess.hs | 2 +- .../Test/Spar/Scim/UserSpec.hs | 20 +- services/spar/test-integration/Util/Core.hs | 6 +- services/spar/test-integration/Util/Scim.hs | 13 - services/spar/test/Test/Spar/Scim/UserSpec.hs | 2 +- tools/stern/src/Stern/API.hs | 2 +- tools/stern/src/Stern/API/Routes.hs | 2 +- tools/stern/stern.cabal | 3 - tools/stern/test/integration/Util.hs | 4 +- weeder.toml | 2 + 102 files changed, 2477 insertions(+), 1642 deletions(-) create mode 100644 changelog.d/5-internal/wpb-8887 create mode 100644 libs/cassandra-util/src/Cassandra/QQ.hs create mode 100644 libs/types-common/src/Data/HavePendingInvitations.hs create mode 100644 libs/types-common/src/Util/Timeout.hs create mode 100644 libs/wire-subsystems/src/Wire/ActivationCodeStore.hs create mode 100644 libs/wire-subsystems/src/Wire/ActivationCodeStore/Cassandra.hs create mode 100644 libs/wire-subsystems/src/Wire/InvitationCodeStore.hs create mode 100644 libs/wire-subsystems/src/Wire/InvitationCodeStore/Cassandra.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationCodeStore.hs delete mode 100644 services/brig/src/Brig/Team/DB.hs diff --git a/Makefile b/Makefile index de21e381a80..bf97abef597 100644 --- a/Makefile +++ b/Makefile @@ -163,7 +163,7 @@ lint-all: formatc hlint-check-all lint-common # The extra 'hlint-check-pr' has been witnessed to be necessary due to # some bu in `hlint-inplace-pr`. Details got lost in history. .PHONY: lint-all-shallow -lint-all-shallow: formatf hlint-inplace-pr hlint-check-pr lint-common +lint-all-shallow: lint-common formatf hlint-inplace-pr hlint-check-pr .PHONY: lint-common lint-common: check-local-nix-derivations treefmt-check # weeder (does not work on CI yet) diff --git a/changelog.d/5-internal/wpb-8887 b/changelog.d/5-internal/wpb-8887 new file mode 100644 index 00000000000..087d81745a8 --- /dev/null +++ b/changelog.d/5-internal/wpb-8887 @@ -0,0 +1 @@ +New user subsystem operation `getAccountsBy` for complex account lookups. diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs index 12f67d1200e..7c9d2b8bd77 100644 --- a/integration/test/Test/Spar.hs +++ b/integration/test/Test/Spar.hs @@ -29,8 +29,8 @@ testSparUserCreationInvitationTimeout = do res.status `shouldMatchInt` 409 -- However, if we wait until the invitation timeout has passed - -- (assuming it is configured to 10s locally and in CI)... - liftIO $ threadDelay (11_000_000) + -- It's currently configured to 1s local/CI. + liftIO $ threadDelay (2_000_000) -- ...we should be able to create the user again retryT $ bindResponse (createScimUser OwnDomain tok scimUser) $ \res -> do diff --git a/libs/brig-types/src/Brig/Types/User.hs b/libs/brig-types/src/Brig/Types/User.hs index f3cc87ba048..75dfe18f59a 100644 --- a/libs/brig-types/src/Brig/Types/User.hs +++ b/libs/brig-types/src/Brig/Types/User.hs @@ -19,7 +19,6 @@ module Brig.Types.User ( ManagedByUpdate (..), RichInfoUpdate (..), PasswordResetPair, - HavePendingInvitations (..), ) where diff --git a/libs/cassandra-util/cassandra-util.cabal b/libs/cassandra-util/cassandra-util.cabal index af2e0094209..927498c24f5 100644 --- a/libs/cassandra-util/cassandra-util.cabal +++ b/libs/cassandra-util/cassandra-util.cabal @@ -18,6 +18,7 @@ library Cassandra.Helpers Cassandra.MigrateSchema Cassandra.Options + Cassandra.QQ Cassandra.Schema Cassandra.Settings Cassandra.Util @@ -87,6 +88,7 @@ library , optparse-applicative >=0.10 , retry , split >=0.2 + , template-haskell , text >=0.11 , time >=1.4 , tinylog >=0.7 diff --git a/libs/cassandra-util/default.nix b/libs/cassandra-util/default.nix index c7b1451a36e..e02d098a9b7 100644 --- a/libs/cassandra-util/default.nix +++ b/libs/cassandra-util/default.nix @@ -19,6 +19,7 @@ , optparse-applicative , retry , split +, template-haskell , text , time , tinylog @@ -44,6 +45,7 @@ mkDerivation { optparse-applicative retry split + template-haskell text time tinylog diff --git a/libs/cassandra-util/src/Cassandra.hs b/libs/cassandra-util/src/Cassandra.hs index 6774abbeb56..74dcdfc45f4 100644 --- a/libs/cassandra-util/src/Cassandra.hs +++ b/libs/cassandra-util/src/Cassandra.hs @@ -91,3 +91,4 @@ import Cassandra.Exec as C x1, x5, ) +import Cassandra.QQ as C (sql) diff --git a/libs/cassandra-util/src/Cassandra/QQ.hs b/libs/cassandra-util/src/Cassandra/QQ.hs new file mode 100644 index 00000000000..c15df3f3dca --- /dev/null +++ b/libs/cassandra-util/src/Cassandra/QQ.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE TemplateHaskellQuotes #-} + +module Cassandra.QQ (sql) where + +import Imports +import Language.Haskell.TH +import Language.Haskell.TH.Quote (QuasiQuoter (..)) + +-- | a simple quasi quoter to allow for tree-sitter syntax highlight injection. +-- This uses the name sql because that is known to tree-sitter, unlike cql +sql :: QuasiQuoter +sql = + QuasiQuoter + { quotePat = error "Cassandra.QQ: sql quasiquoter cannot be used as pattern", + quoteType = error "Cassandra.QQ: sql quasiquoter cannot be used as type", + quoteDec = error "Cassandra.QQ: sql quasiquoter cannot be used as declaration", + quoteExp = appE [|fromString|] . stringE + } diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index e2ddf387e25..aee8b7c0e9c 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -111,7 +111,6 @@ module Imports -- * Extra Helpers whenM, unlessM, - catMaybesToList, -- * Functor (<$$>), @@ -385,6 +384,3 @@ infix 4 <$$> (<$$$>) = fmap . fmap . fmap infix 4 <$$$> - -catMaybesToList :: Maybe (Maybe a) -> [a] -catMaybesToList = catMaybes . maybeToList diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index 7421aae499c..c4bbd61c01b 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -40,6 +40,7 @@ , quickcheck-instances , random , schema-profunctor +, scientific , servant-server , string-conversions , tagged @@ -96,6 +97,7 @@ mkDerivation { quickcheck-instances random schema-profunctor + scientific servant-server tagged tasty diff --git a/libs/types-common/src/Data/HavePendingInvitations.hs b/libs/types-common/src/Data/HavePendingInvitations.hs new file mode 100644 index 00000000000..03afbe6c77c --- /dev/null +++ b/libs/types-common/src/Data/HavePendingInvitations.hs @@ -0,0 +1,14 @@ +module Data.HavePendingInvitations where + +import Imports +import Wire.Arbitrary + +data HavePendingInvitations + = WithPendingInvitations + | NoPendingInvitations + deriving (Eq, Show, Ord, Generic) + deriving (Arbitrary) via GenericUniform HavePendingInvitations + +fromBool :: Bool -> HavePendingInvitations +fromBool True = WithPendingInvitations +fromBool False = NoPendingInvitations diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index d6367a1f851..8b06c4ea58f 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -28,6 +28,7 @@ module Data.Qualified tUnqualified, tDomain, tUntagged, + tSplit, qTagUnsafe, Remote, toRemoteUnsafe, @@ -92,6 +93,10 @@ tUnqualified = qUnqualified . tUntagged tDomain :: QualifiedWithTag t a -> Domain tDomain = qDomain . tUntagged +-- | perform 'qUnqualified' and 'tDomain' at once. Useful in ViewPatterns. +tSplit :: QualifiedWithTag t a -> (Domain, a) +tSplit (tUntagged -> q) = (q.qDomain, q.qUnqualified) + -- | A type representing a 'Qualified' value where the domain is guaranteed to -- be remote. type Remote = QualifiedWithTag 'QRemote diff --git a/libs/types-common/src/Util/Timeout.hs b/libs/types-common/src/Util/Timeout.hs new file mode 100644 index 00000000000..e09c358e88d --- /dev/null +++ b/libs/types-common/src/Util/Timeout.hs @@ -0,0 +1,32 @@ +module Util.Timeout + ( Timeout (..), + module Data.Time.Clock, + ) +where + +import Data.Aeson +import Data.Aeson.Types +import Data.Scientific +import Data.Time.Clock +import Imports + +newtype Timeout = Timeout + { timeoutDiff :: NominalDiffTime + } + deriving newtype (Eq, Enum, Ord, Num, Real, Fractional, RealFrac, Show) + +instance Read Timeout where + readsPrec i s = + case readsPrec i s of + [(x :: Int, s')] -> [(Timeout (fromIntegral x), s')] + _ -> [] + +instance FromJSON Timeout where + parseJSON (Number n) = + let defaultV = 3600 + bounded = toBoundedInteger n :: Maybe Int64 + in pure $ + Timeout $ + fromIntegral @Int $ + maybe defaultV fromIntegral bounded + parseJSON v = typeMismatch "activationTimeout" v diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 5fb1c0ca72c..175d3964cdc 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -19,6 +19,7 @@ library Data.Domain Data.ETag Data.Handle + Data.HavePendingInvitations Data.Id Data.Json.Util Data.LegalHold @@ -38,6 +39,7 @@ library Util.Options Util.Options.Common Util.Test + Util.Timeout Wire.Arbitrary other-modules: Paths_types_common @@ -125,6 +127,7 @@ library , quickcheck-instances >=0.3.16 , random >=1.1 , schema-profunctor + , scientific , servant-server , tagged >=0.8 , tasty >=0.11 diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs index 70d478643a0..78d4ddcbf0a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/OAuth.hs @@ -39,7 +39,7 @@ type OAuthAPI = :> Post '[JSON] OAuthClientCredentials ) :<|> Named - "get-oauth-client" + "i-get-oauth-client" ( Summary "Get OAuth client by id" :> CanThrow 'OAuthFeatureDisabled :> CanThrow 'OAuthClientNotFound 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 fb4ab2cc9e5..22f23d50a31 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -566,7 +566,7 @@ type IMiscAPI = (RespondEmpty 200 "OK") ) :<|> Named - "add-bot" + "i-add-bot" ( -- This endpoint can lead to the following events being sent: -- - MemberJoin event to members CanThrow ('ActionDenied 'AddConversationMember) 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 29f05a6b708..72afa66ff3b 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -268,7 +268,7 @@ type UserAPI = "get-rich-info" ( Summary "Get a user's rich info" :> CanThrow 'InsufficientTeamPermissions - :> ZUser + :> ZLocalUser :> "users" :> CaptureUserId "uid" :> "rich-info" @@ -322,7 +322,7 @@ type SelfAPI = :> CanThrow 'MissingAuth :> CanThrow 'DeleteCodePending :> CanThrow 'OwnerDeletingSelf - :> ZUser + :> ZLocalUser :> "self" :> ReqBody '[JSON] DeleteUser :> MultiVerb 'DELETE '[JSON] DeleteSelfResponses (Maybe Timeout) @@ -743,7 +743,7 @@ type UserClientAPI = :> CanThrow 'MalformedPrekeys :> CanThrow 'CodeAuthenticationFailed :> CanThrow 'CodeAuthenticationRequired - :> ZUser + :> ZLocalUser :> ZConn :> "clients" :> ReqBody '[JSON] NewClient @@ -766,7 +766,7 @@ type UserClientAPI = :> CanThrow 'MalformedPrekeys :> CanThrow 'CodeAuthenticationFailed :> CanThrow 'CodeAuthenticationRequired - :> ZUser + :> ZLocalUser :> ZConn :> "clients" :> ReqBody '[JSON] NewClient diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs index 2db4a8320ec..19d20cd30f6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/OAuth.hs @@ -39,7 +39,7 @@ type OAuthAPI = ( Summary "Get OAuth client information" :> CanThrow 'OAuthFeatureDisabled :> CanThrow 'OAuthClientNotFound - :> ZUser + :> ZLocalUser :> "oauth" :> "clients" :> Capture' '[Description "The ID of the OAuth client"] "OAuthClientId" OAuthClientId @@ -55,7 +55,7 @@ type OAuthAPI = "create-oauth-auth-code" ( Summary "Create an OAuth authorization code" :> Description "Currently only supports the 'code' response type, which corresponds to the authorization code flow." - :> ZUser + :> ZLocalUser :> "oauth" :> "authorization" :> "codes" @@ -99,7 +99,7 @@ type OAuthAPI = "get-oauth-applications" ( Summary "Get OAuth applications with account access" :> Description "Get all OAuth applications with active account access for a user." - :> ZUser + :> ZLocalUser :> "oauth" :> "applications" :> MultiVerb1 @@ -110,7 +110,7 @@ type OAuthAPI = :<|> Named "revoke-oauth-account-access-v6" ( Summary "Revoke account access from an OAuth application" - :> ZUser + :> ZLocalUser :> Until 'V7 :> "oauth" :> "applications" @@ -125,7 +125,7 @@ type OAuthAPI = "revoke-oauth-account-access" ( Summary "Revoke account access from an OAuth application" :> CanThrow 'AccessDenied - :> ZUser + :> ZLocalUser :> From 'V7 :> "oauth" :> "applications" @@ -142,7 +142,7 @@ type OAuthAPI = "delete-oauth-refresh-token" ( Summary "Revoke an active OAuth session" :> Description "Revoke an active OAuth session by providing the refresh token ID." - :> ZUser + :> ZLocalUser :> CanThrow 'AccessDenied :> CanThrow 'OAuthClientNotFound :> "oauth" diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index b5c0d1a8096..967adad2832 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE StrictData #-} -- This file is part of the Wire Server implementation. @@ -67,27 +68,27 @@ instance ToSchema InvitationRequest where InvitationRequest <$> locale .= optFieldWithDocModifier "locale" (description ?~ "Locale to use for the invitation.") (maybeWithDefault A.Null schema) - <*> role + <*> (.role) .= optFieldWithDocModifier "role" (description ?~ "Role of the invitee (invited user).") (maybeWithDefault A.Null schema) - <*> inviteeName + <*> (.inviteeName) .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters).") (maybeWithDefault A.Null schema) - <*> inviteeEmail + <*> (.inviteeEmail) .= fieldWithDocModifier "email" (description ?~ "Email of the invitee.") schema -------------------------------------------------------------------------------- -- Invitation data Invitation = Invitation - { inTeam :: TeamId, - inRole :: Role, - inInvitation :: InvitationId, - inCreatedAt :: UTCTimeMillis, + { team :: TeamId, + role :: Role, + invitationId :: InvitationId, + createdAt :: UTCTimeMillis, -- | this is always 'Just' for new invitations, but for -- migration it is allowed to be 'Nothing'. - inCreatedBy :: Maybe UserId, - inInviteeEmail :: EmailAddress, - inInviteeName :: Maybe Name, - inInviteeUrl :: Maybe (URIRef Absolute) + createdBy :: Maybe UserId, + inviteeEmail :: EmailAddress, + inviteeName :: Maybe Name, + inviteeUrl :: Maybe (URIRef Absolute) } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform Invitation) @@ -99,22 +100,22 @@ instance ToSchema Invitation where "Invitation" (description ?~ "An invitation to join a team on Wire") $ Invitation - <$> inTeam + <$> (.team) .= fieldWithDocModifier "team" (description ?~ "Team ID of the inviting team") schema - <*> inRole + <*> (.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) - <*> inInvitation + <*> (.invitationId) .= fieldWithDocModifier "id" (description ?~ "UUID used to refer the invitation") schema - <*> inCreatedAt + <*> (.createdAt) .= fieldWithDocModifier "created_at" (description ?~ "Timestamp of invitation creation") schema - <*> inCreatedBy + <*> (.createdBy) .= optFieldWithDocModifier "created_by" (description ?~ "ID of the inviting user") (maybeWithDefault A.Null schema) - <*> inInviteeEmail + <*> (.inviteeEmail) .= fieldWithDocModifier "email" (description ?~ "Email of the invitee") schema - <*> inInviteeName + <*> (.inviteeName) .= optFieldWithDocModifier "name" (description ?~ "Name of the invitee (1 - 128 characters)") (maybeWithDefault A.Null schema) - <*> (fmap (TE.decodeUtf8 . serializeURIRef') . inInviteeUrl) + <*> (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) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 10d218f34c3..2ff0c3eb3e0 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -59,7 +59,6 @@ module Wire.API.User CreateUserSparInternalResponses, newUserFromSpar, urefToExternalId, - urefToEmail, ExpiresIn, newUserTeam, newUserEmail, @@ -121,7 +120,6 @@ module Wire.API.User GetPasswordResetCodeResp (..), CheckBlacklistResponse (..), ManagedByUpdate (..), - HavePendingInvitations (..), RichInfoUpdate (..), PasswordResetPair, UpdateSSOIdResponse (..), @@ -154,7 +152,7 @@ import Cassandra qualified as C import Control.Applicative import Control.Arrow ((&&&)) import Control.Error.Safe (rightMay) -import Control.Lens (makePrisms, over, view, (.~), (?~), (^.)) +import Control.Lens (makePrisms, over, view, (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..), withText) import Data.Aeson.Types qualified as A import Data.Attoparsec.ByteString qualified as Parser @@ -192,7 +190,6 @@ import GHC.TypeLits import Generics.SOP qualified as GSOP import Imports import SAML2.WebSSO qualified as SAML -import SAML2.WebSSO.Types.Email qualified as SAMLEmail import Servant (FromHttpApiData (..), ToHttpApiData (..), type (.++)) import Test.QuickCheck qualified as QC import URI.ByteString (serializeURIRef) @@ -308,11 +305,6 @@ instance ToSchema ManagedByUpdate where ManagedByUpdate <$> mbuManagedBy .= field "managed_by" schema -data HavePendingInvitations - = WithPendingInvitations - | NoPendingInvitations - deriving (Eq, Show, Generic) - newtype RichInfoUpdate = RichInfoUpdate {riuRichInfo :: RichInfoAssocList} deriving (Eq, Show, Generic) deriving newtype (Arbitrary) @@ -585,7 +577,7 @@ data User = User userManagedBy :: ManagedBy, userSupportedProtocols :: Set BaseProtocolTag } - deriving stock (Eq, Show, Generic) + deriving stock (Eq, Ord, Show, Generic) deriving (Arbitrary) via (GenericUniform User) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema User) @@ -850,11 +842,6 @@ instance (res ~ RegisterInternalResponses) => AsUnion res (Either RegisterError urefToExternalId :: SAML.UserRef -> Maybe Text urefToExternalId = fmap CI.original . SAML.shortShowNameID . view SAML.uidSubject -urefToEmail :: SAML.UserRef -> Maybe EmailAddress -urefToEmail uref = case uref ^. SAML.uidSubject . SAML.nameID of - SAML.UNameIDEmail email -> emailAddressText . SAMLEmail.render . CI.original $ email - _ -> Nothing - data CreateUserSparError = CreateUserSparHandleError ChangeHandleError | CreateUserSparRegistrationError RegisterError @@ -1222,7 +1209,7 @@ maybeNewUserOriginFromComponents hasPassword hasSSO (invcode, teamcode, team, te -- | A random invitation code for use during registration newtype InvitationCode = InvitationCode {fromInvitationCode :: AsciiBase64Url} - deriving stock (Eq, Show, Generic) + deriving stock (Eq, Ord, Show, Generic) deriving newtype (ToSchema, ToByteString, FromByteString, Arbitrary) deriving (FromJSON, ToJSON, S.ToSchema) via Schema InvitationCode @@ -1787,7 +1774,7 @@ data UserAccount = UserAccount { accountUser :: !User, accountStatus :: !AccountStatus } - deriving (Eq, Show, Generic) + deriving (Eq, Ord, Show, Generic) deriving (Arbitrary) via (GenericUniform UserAccount) deriving (ToJSON, FromJSON, S.ToSchema) via Schema.Schema UserAccount @@ -1806,7 +1793,7 @@ data ExtendedUserAccount = ExtendedUserAccount { account :: UserAccount, emailUnvalidated :: Maybe EmailAddress } - deriving (Eq, Show, Generic) + deriving (Eq, Ord, Show, Generic) deriving (Arbitrary) via (GenericUniform ExtendedUserAccount) deriving (ToJSON, FromJSON, S.ToSchema) via Schema.Schema ExtendedUserAccount diff --git a/libs/wire-api/src/Wire/API/User/EmailAddress.hs b/libs/wire-api/src/Wire/API/User/EmailAddress.hs index 7c5bc2dacc7..ffde490b59e 100644 --- a/libs/wire-api/src/Wire/API/User/EmailAddress.hs +++ b/libs/wire-api/src/Wire/API/User/EmailAddress.hs @@ -6,7 +6,6 @@ module Wire.API.User.EmailAddress emailAddressText, module Text.Email.Parser, emailToSAMLNameID, - emailFromSAMLNameID, emailFromSAML, ) where @@ -16,9 +15,7 @@ where ----- import Cassandra.CQL qualified as C -import Control.Lens ((^.)) import Data.ByteString.Conversion hiding (toByteString) -import Data.CaseInsensitive qualified as CI import Data.Data (Proxy (..)) import Data.OpenApi hiding (Schema, ToSchema) import Data.Schema @@ -116,11 +113,6 @@ arbitraryValidMail = do && notAt x && isValid (fromString ("me@" <> x)) -emailFromSAMLNameID :: SAML.NameID -> Maybe EmailAddress -emailFromSAMLNameID nid = case nid ^. SAML.nameID of - SAML.UNameIDEmail eml -> Just . emailFromSAML . CI.original $ eml - _ -> Nothing - -- | FUTUREWORK(fisx): if saml2-web-sso exported the 'NameID' constructor, we could make this -- function total without all that praying and hoping. emailToSAMLNameID :: EmailAddress -> Either String SAML.NameID diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index a551ea825e2..65b6a5ede61 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -83,7 +83,7 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) data UserIdentity = EmailIdentity EmailAddress | SSOIdentity UserSSOId (Maybe EmailAddress) - deriving stock (Eq, Show, Generic) + deriving stock (Eq, Ord, Show, Generic) deriving (Arbitrary) via (GenericUniform UserIdentity) isSSOIdentity :: UserIdentity -> Bool diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index 21b82fb61ee..dd7f4ad8993 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -380,17 +380,9 @@ arbitraryValidScimIdNoNameIDQualifiers = do . (SAML.uidSubject . SAML.nameIDSPProvidedID .~ Nothing) . (SAML.uidSubject . SAML.nameIDSPNameQ .~ Nothing) --- | Take apart a 'ValidScimId', use both 'SAML.UserRef', 'Email' if applicable, and --- merge the result with a given function. -runValidScimIdBoth :: (a -> a -> a) -> (SAML.UserRef -> a) -> (EmailAddress -> a) -> ValidScimId -> a -runValidScimIdBoth merge doURefl doEmail = these doEmail doURefl (\em uref -> doEmail em `merge` doURefl uref) . validScimIdAuthInfo - veidUref :: ValidScimId -> Maybe SAML.UserRef veidUref = justThere . validScimIdAuthInfo -isSAMLUser :: ValidScimId -> Bool -isSAMLUser = isJust . justThere . validScimIdAuthInfo - makeLenses ''ValidScimUser makeLenses ''ValidScimId diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs index 7ad5845c320..1463b0d1136 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/InvitationList_team.hs @@ -37,20 +37,20 @@ testObject_InvitationList_team_2 = InvitationList { ilInvitations = [ Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), - inRole = RoleOwner, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T09:28:36.729Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), + role = RoleOwner, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-08T09:28:36.729Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "fuC9p\1098501A\163554\f\ENQ\SO\21027N\47326_?oCX.U\r\163744W\33096\58996\1038685\DC3\t[\37667\SYN/\8408A\145025\173325\DC4H\135001\STX\166880\EOT\165028o\DC3" } ), - inInviteeUrl = Just (fromRight' (parseURI strictURIParserOptions "https://example.com/inv14")) + inviteeUrl = Just (fromRight' (parseURI strictURIParserOptions "https://example.com/inv14")) } ], ilHasMore = True @@ -64,126 +64,126 @@ testObject_InvitationList_team_4 = InvitationList { ilInvitations = [ Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T19:46:50.121Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T19:46:50.121Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "R6\133444\134053VQ\187682\SUB\SOH\180538\&0C\1088909\ESCR\185800\125002@\38857Z?\STX\169387\1067878e}\SOH\ETB\EOTm\184898\US]\986782\189015\1059374\986508\b\DC1zfw-5\120662\CAN\1064450 \EMe\DC4|\14426Vo{\1076439\DC3#\USS\45051&zz\160719\&9\142411,\SI\f\SOHp\1025840\DLE\163178\1060369.&\997544kZ\50431u\b\50764\1109279n:\1103691D$.Q" } ), - inInviteeUrl = Nothing + inviteeUrl = Nothing }, Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T09:00:02.901Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T09:00:02.901Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\DC2}q\CAN=SA\ETXx\t\ETX\\\v[\b)(\ESC]\135875Y\v@p\41515l\45065\157388\NUL\t\1100066\SOH1\DC1\ENQ\1021763\"i\29460\EM\b\ACK\SI\DC2v\ACK" } ), - inInviteeUrl = Nothing + inviteeUrl = Nothing }, Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), - inRole = RoleMember, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T11:10:31.203Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), + role = RoleMember, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T11:10:31.203Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\58076&\1059325Ec\NUL\16147}k\1036184l\172911\USJ\EM0^.+F\DEL\NUL\f$'`!\ETB[p\1041609}>E0y\96440#4I\a\66593jc\ESCgt\22473\1093208P\DC4!\1095909E93'Y$YL\46886b\r:,\181790\SO\153247y\ETX;\1064633\1099478z4z-D\1096755a\139100\&6\164829r\1033640\987906J\DLE\48134" } ), - inInviteeUrl = Nothing + inviteeUrl = Nothing }, Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), - inRole = RoleOwner, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T23:41:34.529Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), + role = RoleOwner, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T23:41:34.529Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "Ft*O1\b&\SO\CAN<\72219\1092619m\n\DC4\DC2; \ETX\988837\DC1\1059627\"k.T\1023249[[\FS\EOT{j`\GS\997342c\1066411{\SUB\GSQY\182805\t\NAKy\t\132339j\1036225W " } ), - inInviteeUrl = Nothing - }, - Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T00:29:17.658Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = Nothing, - inInviteeUrl = Nothing - }, - Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), - inRole = RoleOwner, - inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T13:34:37.117Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + inviteeUrl = Nothing + }, + Invitation + { team = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T00:29:17.658Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Nothing, + inviteeUrl = Nothing + }, + Invitation + { team = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), + role = RoleOwner, + invitationId = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T13:34:37.117Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "Lo\r\1107113\1111565\1042998\1027480g\"\1055088\SUB\SUB\180703\43419\EOTv\188258,\171408(\GSQT\150160;\1063450\ENQ\ETBB\1106414H\170195\\\1040638,Y" } ), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_6 :: Invitation testObject_Invitation_team_6 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000000")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T08:56:40.919Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000000"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000000")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T08:56:40.919Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "O~\DC4U\RS?V3_\191280Slh\1072236Q1\1011443j|~M7\1092762\1097596\94632\DC1K\1078140Afs\178951lGV\1113159]`o\EMf\34020InvfDDy\\DI\163761\1091945\ETBB\159212F*X\SOH\SUB\50580\ETX\DLE<\ETX\SYNc\DEL\DLE,p\v*\1005720Vn\fI\70201xS\STXV\ESC$\EMu\1002390xl>\aZ\DC44e\DC4aZ" } ), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_7 :: Invitation testObject_Invitation_team_7 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000001")), - inRole = RoleExternalPartner, - inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-07T18:46:22.786Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000000"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000001")), + role = RoleExternalPartner, + invitationId = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002")), + createdAt = fromJust (readUTCTimeMillis "1864-05-07T18:46:22.786Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\CAN.\110967\1085214\DLE\f\DLE\CAN\150564o;Yay:yY $\ETX<\879%@\USre>5L'R\DC3\178035oy#]c4!\99741U\54858\26279\1042232\1062242p_>f\SO\DEL\175240\1077738\995735_Vm\US}\STXPz\r\ENQK\SO+>\991648\NUL\153467?pu?r\ESC\SUB!?\168405;\6533S\18757\a\1071148\b\1023581\996567\17385\120022\b\SUB\FS\SIF%<\125113\SIh\ESC\ETX\SI\994739\USO\NULg_\151272\47274\1026399\EOT\1058084\1089771z~%IA'R\b\1011572Hv^\1043633wrjb\t\166747\ETX" } ), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_12 :: Invitation testObject_Invitation_team_12 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000002")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-12T22:47:35.829Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000000000000"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000002")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000002")), + createdAt = fromJust (readUTCTimeMillis "1864-05-12T22:47:35.829Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000000000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\DLEZ+wd^\67082\1073384\&1\STXYdXt>\1081020LSB7F9\\\135148\ENQ\n\987295\"\127009|\a\61724\157754\DEL'\ESCTygU\1106772R\52822\1071584O4\1035713E9\"\1016016\DC2Re\ENQD}\1051112\161959\1104733\bV\176894%98'\RS9\ACK4yP\83405\14400\345\aw\t\1098022\v\1078003xv/Yl\1005740\158703" } ), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_13 :: Invitation testObject_Invitation_team_13 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001")), - inRole = RoleMember, - inInvitation = Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000200000002")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T01:18:31.982Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000002"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = Just (Name {fromName = "U"}), - inInviteeUrl = Nothing + { team = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001")), + role = RoleMember, + invitationId = Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000200000002")), + createdAt = fromJust (readUTCTimeMillis "1864-05-08T01:18:31.982Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000100000002"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just (Name {fromName = "U"}), + inviteeUrl = Nothing } testObject_Invitation_team_14 :: Invitation testObject_Invitation_team_14 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000100000000")), - inRole = RoleOwner, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000200000002")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-12T23:54:25.090Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000200000000"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = Nothing, - inInviteeUrl = Nothing + { team = Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000100000000")), + role = RoleOwner, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000200000002")), + createdAt = fromJust (readUTCTimeMillis "1864-05-12T23:54:25.090Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000200000000"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Nothing, + inviteeUrl = Nothing } testObject_Invitation_team_15 :: Invitation testObject_Invitation_team_15 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000001")), - inRole = RoleOwner, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T22:22:28.568Z"), - inCreatedBy = Nothing, - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000100000001")), + role = RoleOwner, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000200000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-08T22:22:28.568Z"), + createdBy = Nothing, + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\71448\US&KIL\DC3\1086159![\n6\1111661HEj4E\12136UL\US>2\1070931_\nJ\53410Pv\SO\SIR\30897\&8\bmS\45510mE\ag\SYN\ENQ%\14545\f!\v\US\119306\ENQ\184817\1044744\SO83!j\73854\GS\1071331,\RS\CANF\1062795\1110535U\EMJb\DC1j\EMY\92304O\1007855" } ), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_16 :: Invitation testObject_Invitation_team_16 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002")), - inRole = RoleExternalPartner, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000200000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-09T09:56:33.113Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = Just (Name {fromName = "\GS\DC4Q;6/_f*7\1093966\SI+\1092810\41698\&9"}), - inInviteeUrl = Nothing + { team = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000002")), + role = RoleExternalPartner, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000200000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-09T09:56:33.113Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just (Name {fromName = "\GS\DC4Q;6/_f*7\1093966\SI+\1092810\41698\&9"}), + inviteeUrl = Nothing } testObject_Invitation_team_17 :: Invitation testObject_Invitation_team_17 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000002")), - inRole = RoleAdmin, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-08T06:30:23.239Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000002")), + role = RoleAdmin, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-08T06:30:23.239Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "Z\ESC9E\DEL\NAK\37708\83413}(3m\97177\97764'\1072786.WY;\RS8?v-\1100720\DC2\1015859" } ), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_19 :: Invitation testObject_Invitation_team_19 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000000")), - inRole = RoleMember, - inInvitation = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000000000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-07T15:08:06.796Z"), - inCreatedBy = Nothing, - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = + { team = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000200000000")), + role = RoleMember, + invitationId = Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000000000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-07T15:08:06.796Z"), + createdBy = Nothing, + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Just ( Name { fromName = "\38776r\111317\ETXQi\1000087\1097943\EM\170747\74323+\1067948Q?H=G-\RS;\1103719\SOq^K;a\1052250W\EM X\83384\1073320>M\980\26387jjbU-&\1040136v\NULy\181884\a|\SYNUfJCHjP\SO\1111555\27981DNA:~s" } ), - inInviteeUrl = Nothing + inviteeUrl = Nothing } testObject_Invitation_team_20 :: Invitation testObject_Invitation_team_20 = Invitation - { inTeam = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), - inRole = RoleExternalPartner, - inInvitation = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001")), - inCreatedAt = fromJust (readUTCTimeMillis "1864-05-12T08:07:17.747Z"), - inCreatedBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))), - inInviteeEmail = unsafeEmailAddress "some" "example", - inInviteeName = Nothing, - inInviteeUrl = Nothing + { team = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), + role = RoleExternalPartner, + invitationId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000000000001")), + createdAt = fromJust (readUTCTimeMillis "1864-05-12T08:07:17.747Z"), + createdBy = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))), + inviteeEmail = unsafeEmailAddress "some" "example", + inviteeName = Nothing, + inviteeUrl = Nothing } diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index 890275b857d..29e6263437f 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -14,6 +14,7 @@ , bytestring , bytestring-conversion , cassandra-util +, conduit , containers , cql , crypton @@ -73,10 +74,12 @@ , types-common , unliftio , unordered-containers +, uri-bytestring , uuid , wai-utilities , wire-api , wire-api-federation +, witherable }: mkDerivation { pname = "wire-subsystems"; @@ -94,6 +97,7 @@ mkDerivation { bytestring bytestring-conversion cassandra-util + conduit containers cql crypton @@ -143,10 +147,12 @@ mkDerivation { types-common unliftio unordered-containers + uri-bytestring uuid wai-utilities wire-api wire-api-federation + witherable ]; testHaskellDepends = [ aeson diff --git a/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs b/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs new file mode 100644 index 00000000000..9473bd16f58 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/ActivationCodeStore.hs @@ -0,0 +1,30 @@ +-- 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 . +{-# LANGUAGE TemplateHaskell #-} + +module Wire.ActivationCodeStore where + +import Data.Id +import Imports +import Polysemy +import Wire.API.User.Activation +import Wire.UserKeyStore + +data ActivationCodeStore :: Effect where + LookupActivationCode :: EmailKey -> ActivationCodeStore m (Maybe (Maybe UserId, ActivationCode)) + +makeSem ''ActivationCodeStore diff --git a/libs/wire-subsystems/src/Wire/ActivationCodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/ActivationCodeStore/Cassandra.hs new file mode 100644 index 00000000000..7f0ba27ba03 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/ActivationCodeStore/Cassandra.hs @@ -0,0 +1,37 @@ +module Wire.ActivationCodeStore.Cassandra where + +import Cassandra +import Data.Id +import Data.Text.Ascii qualified as Ascii +import Data.Text.Encoding qualified as T +import Imports +import OpenSSL.EVP.Digest +import Polysemy +import Polysemy.Embed +import Wire.API.User.Activation +import Wire.ActivationCodeStore +import Wire.UserKeyStore (EmailKey, emailKeyUniq) + +interpretActivationCodeStoreToCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor ActivationCodeStore r +interpretActivationCodeStoreToCassandra casClient = + interpret $ + runEmbedded (runClient casClient) . \case + LookupActivationCode ek -> embed do + liftIO (mkActivationKey ek) + >>= retry x1 . query1 cql . params LocalQuorum . Identity + where + cql :: PrepQuery R (Identity ActivationKey) (Maybe UserId, ActivationCode) + cql = + [sql| + SELECT user, code FROM activation_keys WHERE key = ? + |] + +mkActivationKey :: EmailKey -> IO ActivationKey +mkActivationKey k = do + Just d <- getDigestByName "SHA256" + pure do + ActivationKey + . Ascii.encodeBase64Url + . digestBS d + . T.encodeUtf8 + $ emailKeyUniq k diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs index 94024d5b4cf..2d28021a6a1 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs @@ -49,7 +49,8 @@ import Wire.Sem.Now import Wire.Sem.Now qualified as Now import Wire.SessionStore import Wire.UserKeyStore -import Wire.UserSubsystem (UserSubsystem, getLocalUserAccountByUserKey) +import Wire.UserSubsystem (UserSubsystem) +import Wire.UserSubsystem qualified as User interpretAuthenticationSubsystem :: forall r. @@ -141,20 +142,22 @@ lookupActiveUserIdByUserKey target = userId <$$> lookupActiveUserByUserKey target lookupActiveUserByUserKey :: - (Member UserSubsystem r, Member (Input (Local ())) r) => + ( Member UserSubsystem r, + Member (Input (Local ())) r + ) => EmailKey -> Sem r (Maybe User) lookupActiveUserByUserKey target = do localUnit <- input - let ltarget = qualifyAs localUnit target - mUser <- getLocalUserAccountByUserKey ltarget + let ltarget = qualifyAs localUnit [emailKeyOrig target] + mUser <- User.getExtendedAccountsByEmailNoFilter ltarget case mUser of - Just user -> do + [user] -> do pure $ - if user.accountStatus == Active - then Just user.accountUser + if user.account.accountStatus == Active + then Just user.account.accountUser else Nothing - Nothing -> pure Nothing + _ -> pure Nothing internalLookupPasswordResetCodeImpl :: ( Member PasswordResetCodeStore r, diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index 63075543d4a..e129fb5bc2c 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -67,7 +67,8 @@ data GalleyAPIAccess m a where AddTeamMember :: UserId -> TeamId -> - (Maybe (UserId, UTCTimeMillis), Role) -> + Maybe (UserId, UTCTimeMillis) -> + Role -> GalleyAPIAccess m Bool CreateTeam :: UserId -> diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index e226d09bcdd..aa9dcb4dc9e 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -71,7 +71,7 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = GetTeamConv id' id'' id'2 -> getTeamConv v id' id'' id'2 NewClient id' ci -> newClient id' ci CheckUserCanJoinTeam id' -> checkUserCanJoinTeam id' - AddTeamMember id' id'' x0 -> addTeamMember id' id'' x0 + AddTeamMember id' id'' a b -> addTeamMember id' id'' a b CreateTeam id' bnt id'' -> createTeam id' bnt id'' GetTeamMember id' id'' -> getTeamMember id' id'' GetTeamMembers id' -> getTeamMembers id' @@ -234,9 +234,10 @@ addTeamMember :: ) => UserId -> TeamId -> - (Maybe (UserId, UTCTimeMillis), Role) -> + Maybe (UserId, UTCTimeMillis) -> + Role -> Sem r Bool -addTeamMember u tid (minvmeta, role) = do +addTeamMember u tid minvmeta role = do debug $ remote "galley" . msg (val "Adding member to team") diff --git a/libs/wire-subsystems/src/Wire/InvitationCodeStore.hs b/libs/wire-subsystems/src/Wire/InvitationCodeStore.hs new file mode 100644 index 00000000000..a9183ae2da9 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/InvitationCodeStore.hs @@ -0,0 +1,136 @@ +-- 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 . +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TemplateHaskell #-} + +module Wire.InvitationCodeStore where + +import Control.Monad.Trans.Maybe (MaybeT (MaybeT, runMaybeT)) +import Data.Id (InvitationId, TeamId, UserId) +import Data.Json.Util (UTCTimeMillis) +import Data.Range (Range) +import Database.CQL.Protocol (Record (..), TupleType, recordInstance) +import Imports +import Polysemy +import Polysemy.TinyLog (TinyLog) +import System.Logger.Message qualified as Log +import URI.ByteString +import Util.Timeout +import Wire.API.Team.Invitation (Invitation (inviteeEmail)) +import Wire.API.Team.Invitation qualified as Public +import Wire.API.Team.Role (Role, defaultRole) +import Wire.API.User (EmailAddress, InvitationCode, Name) +import Wire.Arbitrary (Arbitrary, GenericUniform (..)) +import Wire.Sem.Logger qualified as Log + +data StoredInvitation = MkStoredInvitation + { teamId :: TeamId, + role :: Maybe Role, + invitationId :: InvitationId, + createdAt :: UTCTimeMillis, + createdBy :: Maybe UserId, + email :: EmailAddress, + name :: Maybe Name, + code :: InvitationCode + } + deriving (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform StoredInvitation) + +recordInstance ''StoredInvitation + +data StoredInvitationInfo = MkStoredInvitationInfo + { teamId :: TeamId, + invitationId :: InvitationId, + code :: InvitationCode + } + deriving (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform StoredInvitationInfo) + +recordInstance ''StoredInvitationInfo + +data InsertInvitation = MkInsertInvitation + { invitationId :: InvitationId, + teamId :: TeamId, + role :: Role, + createdAt :: UTCTime, + createdBy :: Maybe UserId, + inviteeEmail :: EmailAddress, + inviteeName :: Maybe Name + } + deriving (Show, Eq, Generic) + +recordInstance ''InsertInvitation + +data PaginatedResult a + = PaginatedResultHasMore a + | PaginatedResult a + deriving stock (Eq, Ord, Show, Functor, Foldable) + +---------------------------- + +data InvitationCodeStore :: Effect where + InsertInvitation :: InsertInvitation -> Timeout -> InvitationCodeStore m StoredInvitation + LookupInvitation :: TeamId -> InvitationId -> InvitationCodeStore m (Maybe StoredInvitation) + LookupInvitationInfo :: InvitationCode -> InvitationCodeStore m (Maybe StoredInvitationInfo) + LookupInvitationCodesByEmail :: EmailAddress -> InvitationCodeStore m [StoredInvitationInfo] + -- | Range is page size, it defaults to 100 + LookupInvitationsPaginated :: Maybe (Range 1 500 Int32) -> TeamId -> Maybe InvitationId -> InvitationCodeStore m (PaginatedResult [StoredInvitation]) + CountInvitations :: TeamId -> InvitationCodeStore m Int64 + DeleteInvitation :: TeamId -> InvitationId -> InvitationCodeStore m () + DeleteAllTeamInvitations :: TeamId -> InvitationCodeStore m () + +makeSem ''InvitationCodeStore + +---------------------------- + +lookupInvitationByEmail :: (Member InvitationCodeStore r, Member TinyLog r) => EmailAddress -> Sem r (Maybe StoredInvitation) +lookupInvitationByEmail email = runMaybeT do + MkStoredInvitationInfo {teamId, invitationId} <- MaybeT $ lookupSingleInvitationCodeByEmail email + MaybeT $ lookupInvitation teamId invitationId + +lookupInvitationByCode :: (Member InvitationCodeStore r) => InvitationCode -> Sem r (Maybe StoredInvitation) +lookupInvitationByCode code = runMaybeT do + info <- MaybeT $ lookupInvitationInfo code + MaybeT $ lookupInvitation info.teamId info.invitationId + +lookupSingleInvitationCodeByEmail :: (Member TinyLog r, Member InvitationCodeStore r) => EmailAddress -> Sem r (Maybe StoredInvitationInfo) +lookupSingleInvitationCodeByEmail email = do + invs <- lookupInvitationCodesByEmail email + case invs of + [] -> pure Nothing + [inv] -> pure $ Just inv + (_ : _ : _) -> do + -- edge case: more than one pending invite from different teams + Log.info $ + Log.msg (Log.val "team_invidation_email: multiple pending invites from different teams for the same email") + . Log.field "email" (show email) + + pure Nothing + +invitationFromStored :: Maybe (URIRef Absolute) -> StoredInvitation -> Public.Invitation +invitationFromStored maybeUrl MkStoredInvitation {..} = + Public.Invitation + { team = teamId, + role = fromMaybe defaultRole role, + invitationId = invitationId, + createdAt = createdAt, + createdBy = createdBy, + inviteeEmail = email, + inviteeName = name, + inviteeUrl = maybeUrl + } diff --git a/libs/wire-subsystems/src/Wire/InvitationCodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/InvitationCodeStore/Cassandra.hs new file mode 100644 index 00000000000..f8a3bc4a688 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/InvitationCodeStore/Cassandra.hs @@ -0,0 +1,194 @@ +module Wire.InvitationCodeStore.Cassandra where + +import Cassandra +import Data.Conduit (runConduit, (.|)) +import Data.Conduit.List qualified as Conduit +import Data.Id +import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) +import Data.Range (Range, fromRange) +import Data.Text.Ascii (encodeBase64Url) +import Database.CQL.Protocol (TupleType, asRecord) +import Imports +import OpenSSL.Random (randBytes) +import Polysemy +import Polysemy.Embed +import UnliftIO.Async (pooledMapConcurrentlyN_) +import Util.Timeout +import Wire.API.Team.Role (Role) +import Wire.API.User +import Wire.InvitationCodeStore + +interpretInvitationCodeStoreToCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor InvitationCodeStore r +interpretInvitationCodeStoreToCassandra casClient = + interpret $ + runEmbedded (runClient casClient) . \case + InsertInvitation newInv timeout -> embed $ insertInvitationImpl newInv timeout + LookupInvitation tid iid -> embed $ lookupInvitationImpl tid iid + LookupInvitationCodesByEmail email -> embed $ lookupInvitationCodesByEmailImpl email + LookupInvitationInfo code -> embed $ lookupInvitationInfoImpl code + LookupInvitationsPaginated mSize tid miid -> embed $ lookupInvitationsPaginatedImpl mSize tid miid + CountInvitations tid -> embed $ countInvitationsImpl tid + DeleteInvitation tid invId -> embed $ deleteInvitationImpl tid invId + DeleteAllTeamInvitations tid -> embed $ deleteInvitationsImpl tid + +insertInvitationImpl :: + InsertInvitation -> + -- | The timeout for the invitation code. + Timeout -> + Client StoredInvitation +insertInvitationImpl (MkInsertInvitation invId teamId role (toUTCTimeMillis -> now) uid email name) timeout = do + code <- liftIO mkInvitationCode + let inv = + MkStoredInvitation + { teamId = teamId, + role = Just role, + invitationId = invId, + createdAt = now, + createdBy = uid, + email = email, + name = name, + code = code + } + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery cqlInsert (teamId, Just role, invId, now, uid, email, name, code, round timeout) + addPrepQuery cqlInsertInfo (code, teamId, invId, round timeout) + addPrepQuery cqlInsertByEmail (email, teamId, invId, code, round timeout) + pure inv + where + cqlInsert :: PrepQuery W (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode, Int32) () + cqlInsert = + [sql| + INSERT INTO team_invitation (team, role, id, created_at, created_by, email, name, code) VALUES (?, ?, ?, ?, ?, ?, ?, ?) USING TTL ? + |] + cqlInsertInfo :: PrepQuery W (InvitationCode, TeamId, InvitationId, Int32) () + cqlInsertInfo = + [sql| + INSERT INTO team_invitation_info (code, team, id) VALUES (?, ?, ?) USING TTL ? + |] + -- Note: the edge case of multiple invites to the same team by different admins from the + -- same team results in last-invite-wins in the team_invitation_email table. + cqlInsertByEmail :: PrepQuery W (EmailAddress, TeamId, InvitationId, InvitationCode, Int32) () + cqlInsertByEmail = + [sql| + INSERT INTO team_invitation_email (email, team, invitation, code) VALUES (?, ?, ?, ?) USING TTL ? + |] + +lookupInvitationsPaginatedImpl :: Maybe (Range 1 500 Int32) -> TeamId -> Maybe InvitationId -> Client (PaginatedResult [StoredInvitation]) +lookupInvitationsPaginatedImpl mSize tid miid = do + page <- retry x1 case miid of + Just ref -> paginate cqlSelectFrom (paramsP LocalQuorum (tid, ref) (pageSize + 1)) + Nothing -> paginate cqlSelect (paramsP LocalQuorum (Identity tid) (pageSize + 1)) + pure $ mkPage (hasMore page) $ map asRecord $ trim page + where + pageSize :: Int32 + pageSize = maybe 100 fromRange mSize + + trim :: Page a -> [a] + trim p = take (fromIntegral pageSize) (result p) + + mkPage more invs = if more then PaginatedResultHasMore invs else PaginatedResult invs + + cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode) + cqlSelect = + [sql| + SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? ORDER BY id ASC + |] + cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode) + cqlSelectFrom = + [sql| + SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id > ? ORDER BY id ASC + |] + +countInvitationsImpl :: TeamId -> Client (Int64) +countInvitationsImpl t = + maybe 0 runIdentity + <$> retry x1 (query1 cql (params LocalQuorum (Identity t))) + where + cql :: PrepQuery R (Identity TeamId) (Identity Int64) + cql = [sql| SELECT count(*) FROM team_invitation WHERE team = ?|] + +lookupInvitationInfoImpl :: InvitationCode -> Client (Maybe StoredInvitationInfo) +lookupInvitationInfoImpl code = + fmap asRecord <$> retry x1 (query1 cql (params LocalQuorum (Identity code))) + where + cql :: PrepQuery R (Identity InvitationCode) (TupleType StoredInvitationInfo) + cql = + [sql| + SELECT team, id, code FROM team_invitation_info WHERE code = ? + |] + +lookupInvitationCodesByEmailImpl :: EmailAddress -> Client [StoredInvitationInfo] +lookupInvitationCodesByEmailImpl email = map asRecord <$> retry x1 (query cql (params LocalQuorum (Identity email))) + where + cql :: PrepQuery R (Identity EmailAddress) (TeamId, InvitationId, InvitationCode) + cql = + [sql| + SELECT team, invitation, code FROM team_invitation_email WHERE email = ? + |] + +lookupInvitationImpl :: TeamId -> InvitationId -> Client (Maybe StoredInvitation) +lookupInvitationImpl tid iid = + fmap asRecord + <$> retry x1 (query1 cql (params LocalQuorum (tid, iid))) + where + cql :: PrepQuery R (TeamId, InvitationId) (TupleType StoredInvitation) + cql = + [sql| + SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id = ? + |] + +deleteInvitationImpl :: TeamId -> InvitationId -> Client () +deleteInvitationImpl teamId invId = do + codeEmail <- lookupInvitationCodeEmail + case codeEmail of + Just (invCode, invEmail) -> retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery cqlInvitation (teamId, invId) + addPrepQuery cqlInvitationInfo (Identity invCode) + addPrepQuery cqlInvitationEmail (invEmail, teamId) + Nothing -> + retry x5 $ write cqlInvitation (params LocalQuorum (teamId, invId)) + where + lookupInvitationCodeEmail :: Client (Maybe (InvitationCode, EmailAddress)) + lookupInvitationCodeEmail = retry x1 (query1 cqlInvitationCodeEmail (params LocalQuorum (teamId, invId))) + + cqlInvitation :: PrepQuery W (TeamId, InvitationId) () + cqlInvitation = + [sql| + DELETE FROM team_invitation where team = ? AND id = ? + |] + + cqlInvitationInfo :: PrepQuery W (Identity InvitationCode) () + cqlInvitationInfo = + [sql| + DELETE FROM team_invitation_info WHERE code = ? + |] + + cqlInvitationEmail :: PrepQuery W (EmailAddress, TeamId) () + cqlInvitationEmail = + [sql| + DELETE FROM team_invitation_email WHERE email = ? AND team = ? + |] + + cqlInvitationCodeEmail :: PrepQuery R (TeamId, InvitationId) (InvitationCode, EmailAddress) + cqlInvitationCodeEmail = + [sql| + SELECT code, email FROM team_invitation WHERE team = ? AND id = ? + |] + +deleteInvitationsImpl :: TeamId -> Client () +deleteInvitationsImpl teamId = + runConduit $ + paginateC cqlSelect (paramsP LocalQuorum (Identity teamId) 100) x1 + .| Conduit.mapM_ (pooledMapConcurrentlyN_ 16 (deleteInvitationImpl teamId . runIdentity)) + where + cqlSelect :: PrepQuery R (Identity TeamId) (Identity InvitationId) + cqlSelect = "SELECT id FROM team_invitation WHERE team = ? ORDER BY id ASC" + +-- | This function doesn't really belong here, and may want to have return type `Sem (Random : +-- ...)` instead of `IO`. Meh. +mkInvitationCode :: IO InvitationCode +mkInvitationCode = InvitationCode . encodeBase64Url <$> randBytes 24 diff --git a/libs/wire-subsystems/src/Wire/PasswordResetCodeStore.hs b/libs/wire-subsystems/src/Wire/PasswordResetCodeStore.hs index dbf5502fc4a..b1db6098841 100644 --- a/libs/wire-subsystems/src/Wire/PasswordResetCodeStore.hs +++ b/libs/wire-subsystems/src/Wire/PasswordResetCodeStore.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE QuantifiedConstraints #-} -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -33,11 +34,9 @@ data PRQueryData f = PRQueryData prqdTimeout :: f UTCTime } -deriving instance Show (PRQueryData Identity) +deriving instance (forall a. (Show a) => Show (f a)) => Show (PRQueryData f) -deriving instance Eq (PRQueryData Maybe) - -deriving instance Show (PRQueryData Maybe) +deriving instance (forall a. (Eq a) => Eq (f a)) => Eq (PRQueryData f) mapPRQueryData :: (forall a. (f1 a -> f2 a)) -> PRQueryData f1 -> PRQueryData f2 mapPRQueryData f prqd = prqd {prqdRetries = f prqd.prqdRetries, prqdTimeout = f prqd.prqdTimeout} diff --git a/libs/wire-subsystems/src/Wire/PasswordResetCodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/PasswordResetCodeStore/Cassandra.hs index 74bdd0ca1f7..8b923551bc2 100644 --- a/libs/wire-subsystems/src/Wire/PasswordResetCodeStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/PasswordResetCodeStore/Cassandra.hs @@ -58,12 +58,7 @@ passwordResetCodeStoreToCassandra = . write codeInsertQuery . params LocalQuorum $ (prk, prc, uid, runIdentity n, runIdentity ut, ttl) - CodeDelete prk -> - retry x5 - . write codeDeleteQuery - . params LocalQuorum - . Identity - $ prk + CodeDelete prk -> codeDeleteImpl prk where toRecord :: (PasswordResetCode, UserId, Maybe Int32, Maybe UTCTime) -> @@ -79,6 +74,16 @@ genPhoneCode = PasswordResetCode . unsafeFromText . pack . printf "%06d" <$> liftIO (randIntegerZeroToNMinusOne 1000000) +-- FUTUREWORK(fisx,elland): this should be replaced by a method in a +-- future auth subsystem +codeDeleteImpl :: (MonadClient m) => PasswordResetKey -> m () +codeDeleteImpl prk = + retry x5 + . write codeDeleteQuery + . params LocalQuorum + . Identity + $ prk + interpretClientToIO :: (Member (Final IO) r) => ClientState -> diff --git a/libs/wire-subsystems/src/Wire/StoredUser.hs b/libs/wire-subsystems/src/Wire/StoredUser.hs index 38bb072401d..f18502ad591 100644 --- a/libs/wire-subsystems/src/Wire/StoredUser.hs +++ b/libs/wire-subsystems/src/Wire/StoredUser.hs @@ -22,6 +22,7 @@ data StoredUser = StoredUser textStatus :: Maybe TextStatus, pict :: Maybe Pict, email :: Maybe EmailAddress, + emailUnvalidated :: Maybe EmailAddress, ssoId :: Maybe UserSSOId, accentId :: ColourId, assets :: Maybe [Asset], @@ -102,6 +103,10 @@ mkAccountFromStored domain defaultLocale storedUser = (mkUserFromStored domain defaultLocale storedUser) (fromMaybe Active storedUser.status) +mkExtendedAccountFromStored :: Domain -> Locale -> StoredUser -> ExtendedUserAccount +mkExtendedAccountFromStored domain defaultLocale storedUser = + ExtendedUserAccount (mkAccountFromStored domain defaultLocale storedUser) storedUser.emailUnvalidated + toLocale :: Locale -> (Maybe Language, Maybe Country) -> Locale toLocale _ (Just l, c) = Locale l c toLocale l _ = l diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index 3544ec5b35b..6429d60c597 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -46,7 +46,7 @@ data StoredUserUpdateError = StoredUserUpdateHandleExists -- | Effect containing database logic around 'StoredUser'. (Example: claim handle lock is -- database logic; validate handle is application logic.) data UserStore m a where - GetUser :: UserId -> UserStore m (Maybe StoredUser) + GetUsers :: [UserId] -> UserStore m [StoredUser] UpdateUser :: UserId -> StoredUserUpdate -> UserStore m () UpdateUserHandleEither :: UserId -> StoredUserHandleUpdate -> UserStore m (Either StoredUserUpdateError ()) DeleteUser :: User -> UserStore m () @@ -66,6 +66,9 @@ data UserStore m a where makeSem ''UserStore +getUser :: (Member UserStore r) => UserId -> Sem r (Maybe StoredUser) +getUser uid = listToMaybe <$> getUsers [uid] + updateUserHandle :: (Member UserStore r, Member (Error StoredUserUpdateError) r) => UserId -> diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index b62e615220e..9ff0e903abf 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -17,7 +17,7 @@ interpretUserStoreCassandra :: (Member (Embed IO) r) => ClientState -> Interpret interpretUserStoreCassandra casClient = interpret $ runEmbedded (runClient casClient) . \case - GetUser uid -> getUserImpl uid + GetUsers uids -> embed $ getUsersImpl uids UpdateUser uid update -> embed $ updateUserImpl uid update UpdateUserHandleEither uid update -> embed $ updateUserHandleEitherImpl uid update DeleteUser user -> embed $ deleteUserImpl user @@ -27,10 +27,10 @@ interpretUserStoreCassandra casClient = IsActivated uid -> embed $ isActivatedImpl uid LookupLocale uid -> embed $ lookupLocaleImpl uid -getUserImpl :: (Member (Embed Client) r) => UserId -> Sem r (Maybe StoredUser) -getUserImpl uid = embed $ do - mUserTuple <- retry x1 $ query1 selectUser (params LocalQuorum (Identity uid)) - pure $ asRecord <$> mUserTuple +getUsersImpl :: [UserId] -> Client [StoredUser] +getUsersImpl usrs = + map asRecord + <$> retry x1 (query selectUsers (params LocalQuorum (Identity usrs))) updateUserImpl :: UserId -> StoredUserUpdate -> Client () updateUserImpl uid update = @@ -126,12 +126,14 @@ lookupLocaleImpl u = do -------------------------------------------------------------------------------- -- Queries -selectUser :: PrepQuery R (Identity UserId) (TupleType StoredUser) -selectUser = - "SELECT id, name, text_status, picture, email, sso_id, accent_id, assets, \ - \activated, status, expires, language, country, provider, service, \ - \handle, team, managed_by, supported_protocols \ - \FROM user where id = ?" +selectUsers :: PrepQuery R (Identity [UserId]) (TupleType StoredUser) +selectUsers = + [sql| + SELECT id, name, text_status, picture, email, email_unvalidated, sso_id, accent_id, assets, + activated, status, expires, language, country, provider, + service, handle, team, managed_by, supported_protocols + FROM user WHERE id IN ? + |] userDisplayNameUpdate :: PrepQuery W (Name, UserId) () userDisplayNameUpdate = "UPDATE user SET name = ? WHERE id = ?" diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 3a0cab37a6a..b8c6256122f 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -1,9 +1,15 @@ +{-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} -module Wire.UserSubsystem where +module Wire.UserSubsystem + ( module Wire.UserSubsystem, + module Data.HavePendingInvitations, + ) +where import Data.Default import Data.Handle (Handle) +import Data.HavePendingInvitations import Data.Id import Data.Qualified import Imports @@ -11,10 +17,10 @@ import Polysemy import Wire.API.Federation.Error import Wire.API.User import Wire.Arbitrary -import Wire.UserKeyStore +import Wire.UserKeyStore (EmailKey, emailKeyOrig) --- | Who is performing this update operation? (Single source of truth: users managed by SCIM --- can't be updated by clients and vice versa.) +-- | 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.) data UpdateOriginType = -- | Call originates from the SCIM api in spar. UpdateOriginScim @@ -26,7 +32,7 @@ data UpdateOriginType -- | Simple updates (as opposed to, eg., handle, where we need to manage locks). -- -- This is isomorphic to 'StoredUserUpdate', but we keep the two types separate because they --- belong to different abstractions / levels (UserSubsystem vs. UserStore), and they may +-- belong to different abstraction levels (UserSubsystem vs. UserStore), and they may -- change independently in the future ('UserStoreUpdate' may grow more fields for other -- operations). data UserProfileUpdate = MkUserProfileUpdate @@ -53,32 +59,57 @@ instance Default UserProfileUpdate where supportedProtocols = Nothing } +-- | Parameters for `getExternalAccountsBy` operation below. +data GetBy = MkGetBy + { -- | whether or not to include pending invitations when getting users by ids. + includePendingInvitations :: HavePendingInvitations, + -- | get accounts by 'UserId'. + getByUserId :: [UserId], + -- | get accounts by their 'Handle' + getByHandle :: [Handle] + } + deriving stock (Eq, Ord, Show, Generic) + deriving (Arbitrary) via GenericUniform GetBy + +instance Default GetBy where + def = MkGetBy NoPendingInvitations [] [] + data UserSubsystem m a where -- | First arg is for authorization only. GetUserProfiles :: Local UserId -> [Qualified UserId] -> UserSubsystem m [UserProfile] + -- | These give us partial success and hide concurrency in the interpreter. + -- (Nit-pick: a better return type for this might be `([Qualified ([UserId], + -- FederationError)], [UserProfile])`, and then we'd probably need a function of type + -- `([Qualified ([UserId], FederationError)], [UserProfile]) -> ([(Qualified UserId, + -- FederationError)], [UserProfile])` to maintain API compatibility.) + GetUserProfilesWithErrors :: Local UserId -> [Qualified UserId] -> UserSubsystem m ([(Qualified UserId, FederationError)], [UserProfile]) -- | Sometimes we don't have any identity of a requesting user, and local profiles are public. GetLocalUserProfiles :: Local [UserId] -> UserSubsystem m [UserProfile] - -- | Self profile contains things not present in Profile. + -- | Get the union of all user accounts matching the `GetBy` argument *and* having a non-empty UserIdentity. + GetExtendedAccountsBy :: Local GetBy -> UserSubsystem m [ExtendedUserAccount] + -- | Get user accounts matching the `[EmailAddress]` argument (accounts with missing + -- identity and accounts with status /= active included). + GetExtendedAccountsByEmailNoFilter :: Local [EmailAddress] -> UserSubsystem m [ExtendedUserAccount] + -- | Get user account by local user id (accounts with missing identity and accounts with + -- status /= active included). + GetAccountNoFilter :: Local UserId -> UserSubsystem m (Maybe UserAccount) + -- | Get `SelfProfile` (it contains things not present in `UserProfile`). GetSelfProfile :: Local UserId -> UserSubsystem m (Maybe SelfProfile) - -- | These give us partial success and hide concurrency in the interpreter. - -- FUTUREWORK: it would be better to return errors as `Map Domain FederationError`, but would clients like that? - GetUserProfilesWithErrors :: Local UserId -> [Qualified UserId] -> UserSubsystem m ([(Qualified UserId, FederationError)], [UserProfile]) -- | Simple updates (as opposed to, eg., handle, where we need to manage locks). Empty fields are ignored (not deleted). UpdateUserProfile :: Local UserId -> Maybe ConnId -> UpdateOriginType -> UserProfileUpdate -> UserSubsystem m () - -- | parse and lookup a handle, return what the operation has found + -- | Parse and lookup a handle. CheckHandle :: Text {- use Handle here? -} -> UserSubsystem m CheckHandleResp - -- | checks a number of 'Handle's for availability and returns at most 'Word' amount of them + -- | Check a number of 'Handle's for availability and returns at most 'Word' amount of them CheckHandles :: [Handle] -> Word -> UserSubsystem m [Handle] - -- | parses a handle, this may fail so it's effectful + -- | Parse and update a handle. Parsing may fail so this is effectful. UpdateHandle :: Local UserId -> Maybe ConnId -> UpdateOriginType -> Text {- use Handle here? -} -> UserSubsystem m () - GetLocalUserAccountByUserKey :: Local EmailKey -> UserSubsystem m (Maybe UserAccount) - -- | returns the user's locale or the default locale if the users exists + -- | Return the user's locale (or the default locale if the users exists and has none). LookupLocaleWithDefault :: Local UserId -> UserSubsystem m (Maybe Locale) - -- | checks if an email is blocked + -- | Check if an email is blocked. IsBlocked :: EmailAddress -> UserSubsystem m Bool - -- | removes an email from the block list + -- | Remove an email from the block list. BlockListDelete :: EmailAddress -> UserSubsystem m () - -- | adds an email to the block list + -- | Add an email to the block list. BlockListInsert :: EmailAddress -> UserSubsystem m () -- | the return type of 'CheckHandle' @@ -89,6 +120,10 @@ data CheckHandleResp makeSem ''UserSubsystem +-- | given a lookup criteria record ('GetBy'), return the union of the user accounts fulfilling that criteria +getAccountsBy :: (Member UserSubsystem r) => Local GetBy -> Sem r [UserAccount] +getAccountsBy getby = (.account) <$$> getExtendedAccountsBy getby + getUserProfile :: (Member UserSubsystem r) => Local UserId -> Qualified UserId -> Sem r (Maybe UserProfile) getUserProfile luid targetUser = listToMaybe <$> getUserProfiles luid [targetUser] @@ -96,3 +131,22 @@ getUserProfile luid targetUser = getLocalUserProfile :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe UserProfile) getLocalUserProfile targetUser = listToMaybe <$> getLocalUserProfiles ((: []) <$> targetUser) + +getLocalAccountBy :: + (Member UserSubsystem r) => + HavePendingInvitations -> + Local UserId -> + Sem r (Maybe UserAccount) +getLocalAccountBy includePendingInvitations uid = + listToMaybe + <$> getAccountsBy + ( qualifyAs uid $ + def + { getByUserId = [tUnqualified uid], + includePendingInvitations + } + ) + +getLocalUserAccountByUserKey :: (Member UserSubsystem r) => Local EmailKey -> Sem r (Maybe UserAccount) +getLocalUserAccountByUserKey q@(tUnqualified -> ek) = + listToMaybe . fmap (.account) <$> getExtendedAccountsByEmailNoFilter (qualifyAs q [emailKeyOrig ek]) diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 39dd0e179af..d91c6c33dd0 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -13,12 +13,14 @@ import Data.Handle qualified as Handle import Data.Id import Data.Json.Util import Data.LegalHold +import Data.List.Extra (nubOrd) import Data.Qualified import Data.Time.Clock -import Imports hiding (local) +import Imports import Polysemy import Polysemy.Error hiding (try) import Polysemy.Input +import Polysemy.TinyLog (TinyLog) import Servant.Client.Core import Wire.API.Federation.API import Wire.API.Federation.Error @@ -32,6 +34,7 @@ import Wire.DeleteQueue import Wire.Events import Wire.FederationAPIAccess import Wire.GalleyAPIAccess +import Wire.InvitationCodeStore (InvitationCodeStore, lookupInvitationByEmail) import Wire.Sem.Concurrency import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now @@ -41,6 +44,7 @@ import Wire.UserStore as UserStore import Wire.UserSubsystem import Wire.UserSubsystem.Error import Wire.UserSubsystem.HandleBlacklist +import Witherable (wither) data UserSubsystemConfig = UserSubsystemConfig { emailVisibilityConfig :: EmailVisibilityConfig, @@ -65,7 +69,9 @@ runUserSubsystem :: Member Now r, RunClient (fedM 'Brig), FederationMonad fedM, - Typeable fedM + Typeable fedM, + Member (TinyLog) r, + Member InvitationCodeStore r ) => UserSubsystemConfig -> InterpreterFor UserSubsystem r @@ -86,19 +92,23 @@ interpretUserSubsystem :: Member Now r, RunClient (fedM 'Brig), FederationMonad fedM, - Typeable fedM + Typeable fedM, + Member InvitationCodeStore r, + Member TinyLog r ) => InterpreterFor UserSubsystem r interpretUserSubsystem = interpret \case GetUserProfiles self others -> getUserProfilesImpl self others GetLocalUserProfiles others -> getLocalUserProfilesImpl others + GetExtendedAccountsBy getBy -> getExtendedAccountsByImpl getBy + GetExtendedAccountsByEmailNoFilter emails -> getExtendedAccountsByEmailNoFilterImpl emails + GetAccountNoFilter luid -> getAccountNoFilterImpl luid GetSelfProfile self -> getSelfProfileImpl self GetUserProfilesWithErrors self others -> getUserProfilesWithErrorsImpl self others UpdateUserProfile self mconn mb update -> updateUserProfileImpl self mconn mb update CheckHandle uhandle -> checkHandleImpl uhandle CheckHandles hdls cnt -> checkHandlesImpl hdls cnt UpdateHandle uid mconn mb uhandle -> updateHandleImpl uid mconn mb uhandle - GetLocalUserAccountByUserKey userKey -> getLocalUserAccountByUserKeyImpl userKey LookupLocaleWithDefault luid -> lookupLocaleOrDefaultImpl luid IsBlocked email -> isBlockedImpl email BlockListDelete email -> blockListDeleteImpl email @@ -418,19 +428,6 @@ mkProfileUpdateHandleEvent :: UserId -> Handle -> UserEvent mkProfileUpdateHandleEvent uid handle = UserUpdated $ (emptyUserUpdatedData uid) {eupHandle = Just handle} -getLocalUserAccountByUserKeyImpl :: - ( Member UserStore r, - Member UserKeyStore r, - Member (Input UserSubsystemConfig) r - ) => - Local EmailKey -> - Sem r (Maybe UserAccount) -getLocalUserAccountByUserKeyImpl target = runMaybeT $ do - config <- lift input - uid <- MaybeT $ lookupKey (tUnqualified target) - user <- MaybeT $ getUser uid - pure $ mkAccountFromStored (tDomain target) config.defaultLocale user - -------------------------------------------------------------------------------- -- Update Handle @@ -495,3 +492,100 @@ checkHandlesImpl check num = reverse <$> collectFree [] check num case owner of Nothing -> collectFree (h : free) hs (n - 1) Just _ -> collectFree free hs n + +getAccountNoFilterImpl :: + forall r. + ( Member UserStore r, + Member (Input UserSubsystemConfig) r + ) => + Local UserId -> + Sem r (Maybe UserAccount) +getAccountNoFilterImpl (tSplit -> (domain, uid)) = do + cfg <- input + muser <- getUser uid + pure $ (mkAccountFromStored domain cfg.defaultLocale) <$> muser + +getExtendedAccountsByEmailNoFilterImpl :: + forall r. + ( Member UserStore r, + Member UserKeyStore r, + Member (Input UserSubsystemConfig) r + ) => + Local [EmailAddress] -> + Sem r [ExtendedUserAccount] +getExtendedAccountsByEmailNoFilterImpl (tSplit -> (domain, emails)) = do + config <- input + nubOrd <$> flip foldMap emails \ek -> do + mactiveUid <- lookupKey (mkEmailKey ek) + getUsers (nubOrd . catMaybes $ [mactiveUid]) + <&> map (mkExtendedAccountFromStored domain config.defaultLocale) + +-------------------------------------------------------------------------------- +-- getting user accounts by different criteria + +getExtendedAccountsByImpl :: + forall r. + ( Member UserStore r, + Member DeleteQueue r, + Member (Input UserSubsystemConfig) r, + Member InvitationCodeStore r, + Member TinyLog r + ) => + Local GetBy -> + Sem r [ExtendedUserAccount] +getExtendedAccountsByImpl (tSplit -> (domain, MkGetBy {includePendingInvitations, getByHandle, getByUserId})) = do + storedToExtAcc <- do + config <- input + pure $ mkExtendedAccountFromStored domain config.defaultLocale + + handleUserIds :: [UserId] <- + wither lookupHandle getByHandle + + accsByIds :: [ExtendedUserAccount] <- + getUsers (nubOrd $ handleUserIds <> getByUserId) <&> map storedToExtAcc + + filterM want (nubOrd $ accsByIds) + where + -- not wanted: + -- . users without identity + -- . pending users without matching invitation (those are garbage-collected) + -- . TODO: deleted users? + want :: ExtendedUserAccount -> Sem r Bool + want ExtendedUserAccount {account} = + case account.accountUser.userIdentity of + Nothing -> pure False + Just ident -> case account.accountStatus of + PendingInvitation -> + case includePendingInvitations of + WithPendingInvitations -> case emailIdentity ident of + -- TODO(fisx): emailIdentity does not return an unvalidated address in case a + -- validated one cannot be found. that's probably wrong? split up into + -- validEmailIdentity, anyEmailIdentity? + Just email -> do + hasInvitation <- isJust <$> lookupInvitationByEmail email + gcHack hasInvitation (userId account.accountUser) + pure hasInvitation + Nothing -> error "getExtendedAccountsByImpl: should never happen, user invited via scim always has an email" + NoPendingInvitations -> pure False + Active -> pure True + Suspended -> pure True + Deleted -> pure True -- TODO(mangoiv): previous comment said "We explicitly filter out deleted users now." Why? + Ephemeral -> pure True + + -- user invited via scim expires together with its invitation. the UserSubsystem interface + -- semantics hides the fact that pending users have no TTL field. we chose to emulate this + -- in this convoluted way (by making the invitation expire and then checking if it's still + -- there when looking up pending users), because adding TTLs would have been a much bigger + -- change in the database schema (`enqueueUserDeletion` would need to happen purely based + -- on TTL values in cassandra, and there is too much application logic involved there). + -- + -- we could also delete these users here and run a background process that scans for + -- pending users without invitation. we chose not to because enqueuing the user deletion + -- here is very cheap, and avoids database traffic if the user is looked up again. if the + -- background job is reliably taking care of this, there is no strong reason to keep this + -- function. + -- + -- there are certainly other ways to improve this, but they probably involve a non-trivial + -- database schema re-design. + gcHack :: Bool -> UserId -> Sem r () + gcHack hasInvitation uid = unless hasInvitation (enqueueUserDeletion uid) diff --git a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs index 8e5924d2e76..85a9af652a3 100644 --- a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs @@ -55,7 +55,7 @@ type AllEffects = UserSubsystem ] -interpretDependencies :: Domain -> [UserAccount] -> Map UserId Password -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a +interpretDependencies :: Domain -> [ExtendedUserAccount] -> Map UserId Password -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a interpretDependencies localDomain preexistingUsers preexistingPasswords mAllowedEmailDomains = run . userSubsystemTestInterpreter preexistingUsers @@ -84,7 +84,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, cookiesAfterReset) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing + interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing . interpretAuthenticationSubsystem $ do forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) @@ -105,7 +105,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, cookiesAfterReset) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing + interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing . interpretAuthenticationSubsystem $ do forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) @@ -135,7 +135,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do let user = userNoEmail {userIdentity = Just $ EmailIdentity email} localDomain = userNoEmail.userQualifiedId.qDomain createPasswordResetCodeResult = - interpretDependencies localDomain [UserAccount user Active] mempty (Just [decodeUtf8 $ domainPart email]) + interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty (Just [decodeUtf8 $ domainPart email]) . interpretAuthenticationSubsystem $ createPasswordResetCode (mkEmailKey email) in counterexample ("expected Right, got: " <> show createPasswordResetCodeResult) $ @@ -146,7 +146,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do let user = userNoEmail {userIdentity = Just $ EmailIdentity email} localDomain = userNoEmail.userQualifiedId.qDomain createPasswordResetCodeResult = - interpretDependencies localDomain [UserAccount user status] mempty Nothing + interpretDependencies localDomain [ExtendedUserAccount (UserAccount user status) Nothing] mempty Nothing . interpretAuthenticationSubsystem $ createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent @@ -168,7 +168,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, mCaughtException) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing + interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing . interpretAuthenticationSubsystem $ do createPasswordResetCode (mkEmailKey email) @@ -189,7 +189,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing + interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing . interpretAuthenticationSubsystem $ do upsertHashedPassword uid =<< hashPassword oldPassword @@ -209,7 +209,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing + interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing . interpretAuthenticationSubsystem $ do upsertHashedPassword uid =<< hashPassword oldPassword @@ -224,7 +224,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing + interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing . interpretAuthenticationSubsystem $ do hashAndUpsertPassword uid oldPassword @@ -240,7 +240,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordHashInDB, correctResetCode, wrongResetErrors, resetPassworedWithCorectCodeResult) = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing + interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing . interpretAuthenticationSubsystem $ do upsertHashedPassword uid =<< hashPassword oldPassword @@ -274,7 +274,7 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right passwordHashInDB = - interpretDependencies localDomain [UserAccount user Active] mempty Nothing + interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing . interpretAuthenticationSubsystem $ do void $ createPasswordResetCode (mkEmailKey email) diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index bd712dfe489..d5d4a789261 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -19,6 +19,8 @@ module Wire.MiniBackend -- * Quickcheck helpers NotPendingStoredUser (..), + NotPendingEmptyIdentityStoredUser (..), + PendingNotEmptyIdentityStoredUser (..), PendingStoredUser (..), ) where @@ -34,6 +36,7 @@ import Data.Proxy import Data.Qualified import Data.Time import Data.Type.Equality +import GHC.Generics import Imports import Polysemy import Polysemy.Error @@ -51,7 +54,9 @@ import Wire.API.Federation.Error import Wire.API.Team.Feature import Wire.API.Team.Member hiding (userId) import Wire.API.User as User hiding (DeleteUser) +import Wire.API.User.Activation (ActivationCode) import Wire.API.User.Password +import Wire.ActivationCodeStore import Wire.BlockListStore import Wire.DeleteQueue import Wire.DeleteQueue.InMemory @@ -60,7 +65,10 @@ import Wire.FederationAPIAccess import Wire.FederationAPIAccess.Interpreter as FI import Wire.GalleyAPIAccess import Wire.InternalEvent hiding (DeleteUser) +import Wire.InvitationCodeStore import Wire.MockInterpreters +import Wire.MockInterpreters.ActivationCodeStore (inMemoryActivationCodeStoreInterpreter) +import Wire.MockInterpreters.InvitationCodeStore (inMemoryInvitationCodeStoreInterpreter) import Wire.PasswordResetCodeStore import Wire.Sem.Concurrency import Wire.Sem.Concurrency.Sequential @@ -72,6 +80,24 @@ import Wire.UserSubsystem import Wire.UserSubsystem.Error import Wire.UserSubsystem.Interpreter +newtype PendingNotEmptyIdentityStoredUser = PendingNotEmptyIdentityStoredUser StoredUser + deriving (Show, Eq) + +instance Arbitrary PendingNotEmptyIdentityStoredUser where + arbitrary = do + user <- arbitrary `suchThat` \user -> isJust user.identity + pure $ PendingNotEmptyIdentityStoredUser (user {status = Just PendingInvitation}) + +newtype NotPendingEmptyIdentityStoredUser = NotPendingEmptyIdentityStoredUser StoredUser + deriving (Show, Eq) + +-- TODO: make sure this is a valid state +instance Arbitrary NotPendingEmptyIdentityStoredUser where + arbitrary = do + user <- arbitrary `suchThat` \user -> isNothing user.identity + notPendingStatus <- elements (Nothing : map Just [Active, Suspended, Ephemeral]) + pure $ NotPendingEmptyIdentityStoredUser (user {status = notPendingStatus}) + newtype PendingStoredUser = PendingStoredUser StoredUser deriving (Show, Eq) @@ -86,7 +112,7 @@ newtype NotPendingStoredUser = NotPendingStoredUser StoredUser instance Arbitrary NotPendingStoredUser where arbitrary = do user <- arbitrary `suchThat` \user -> isJust user.identity - notPendingStatus <- elements (Nothing : map Just [Active, Suspended, Deleted, Ephemeral]) + notPendingStatus <- elements (Nothing : map Just [Active, Suspended, Ephemeral]) pure $ NotPendingStoredUser (user {status = notPendingStatus}) type AllErrors = @@ -97,6 +123,11 @@ type AllErrors = type MiniBackendEffects = [ UserSubsystem, GalleyAPIAccess, + InvitationCodeStore, + State (Map (TeamId, InvitationId) StoredInvitation), + State (Map InvitationCode StoredInvitationInfo), + ActivationCodeStore, + State (Map EmailKey (Maybe UserId, ActivationCode)), BlockListStore, State [EmailKey], UserStore, @@ -123,8 +154,12 @@ data MiniBackend = MkMiniBackend users :: [StoredUser], userKeys :: Map EmailKey UserId, passwordResetCodes :: Map PasswordResetKey (PRQueryData Identity), - blockList :: [EmailKey] + blockList :: [EmailKey], + activationCodes :: Map EmailKey (Maybe UserId, ActivationCode), + invitationInfos :: Map InvitationCode StoredInvitationInfo, + invitations :: Map (TeamId, InvitationId) StoredInvitation } + deriving stock (Eq, Show, Generic) instance Default MiniBackend where def = @@ -132,7 +167,10 @@ instance Default MiniBackend where { users = mempty, userKeys = mempty, passwordResetCodes = mempty, - blockList = mempty + blockList = mempty, + activationCodes = mempty, + invitationInfos = mempty, + invitations = mempty } -- | represents an entire federated, stateful world of backends @@ -352,9 +390,29 @@ interpretMaybeFederationStackState maybeFederationAPIAccess localBackend teamMem . inMemoryUserStoreInterpreter . liftBlockListStoreState . inMemoryBlockListStoreInterpreter + . liftActivationCodeStoreState + . inMemoryActivationCodeStoreInterpreter + . liftInvitationInfoStoreState + . liftInvitationCodeStoreState + . inMemoryInvitationCodeStoreInterpreter . miniGalleyAPIAccess teamMember galleyConfigs . runUserSubsystem cfg +liftInvitationInfoStoreState :: (Member (State MiniBackend) r) => Sem (State (Map InvitationCode StoredInvitationInfo) : r) a -> Sem r a +liftInvitationInfoStoreState = interpret \case + Polysemy.State.Get -> gets (.invitationInfos) + Put newAcs -> modify $ \b -> b {invitationInfos = newAcs} + +liftInvitationCodeStoreState :: (Member (State MiniBackend) r) => Sem (State (Map (TeamId, InvitationId) StoredInvitation) : r) a -> Sem r a +liftInvitationCodeStoreState = interpret \case + Polysemy.State.Get -> gets (.invitations) + Put newInvs -> modify $ \b -> b {invitations = newInvs} + +liftActivationCodeStoreState :: (Member (State MiniBackend) r) => Sem (State (Map EmailKey (Maybe UserId, ActivationCode)) : r) a -> Sem r a +liftActivationCodeStoreState = interpret \case + Polysemy.State.Get -> gets (.activationCodes) + Put newAcs -> modify $ \b -> b {activationCodes = newAcs} + liftBlockListStoreState :: (Member (State MiniBackend) r) => Sem (State [EmailKey] : r) a -> Sem r a liftBlockListStoreState = interpret $ \case Polysemy.State.Get -> gets (.blockList) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs new file mode 100644 index 00000000000..0265c8d07fe --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/ActivationCodeStore.hs @@ -0,0 +1,13 @@ +module Wire.MockInterpreters.ActivationCodeStore where + +import Data.Id +import Data.Map +import Imports +import Polysemy +import Polysemy.State +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) diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationCodeStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationCodeStore.hs new file mode 100644 index 00000000000..18f00055865 --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationCodeStore.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE RecordWildCards #-} + +module Wire.MockInterpreters.InvitationCodeStore where + +import Data.Id (InvitationId, TeamId) +import Data.Map (elems, (!?)) +import Data.Map qualified as M +import Imports +import Polysemy +import Polysemy.State (State, get, gets) +import Wire.API.User (InvitationCode (..)) +import Wire.InvitationCodeStore + +inMemoryInvitationCodeStoreInterpreter :: + forall r. + ( Member (State (Map (TeamId, InvitationId) StoredInvitation)) r, + Member (State (Map (InvitationCode) StoredInvitationInfo)) r + ) => + InterpreterFor InvitationCodeStore r +inMemoryInvitationCodeStoreInterpreter = interpret \case + InsertInvitation _a _timeout -> error "InsertInvitation" + LookupInvitation tid iid -> gets (!? (tid, iid)) + LookupInvitationInfo iid -> gets (!? iid) + LookupInvitationCodesByEmail em -> + let c MkStoredInvitation {..} + | email == em = Just MkStoredInvitationInfo {..} + | otherwise = Nothing + in mapMaybe c . elems <$> get + LookupInvitationsPaginated {} -> error "LookupInvitationsPaginated" + CountInvitations tid -> gets (fromIntegral . M.size . M.filterWithKey (\(tid', _) _v -> tid == tid')) + DeleteInvitation _tid _invId -> error "DeleteInvitation" + DeleteAllTeamInvitations _tid -> error "DeleteAllTeamInvitations" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index 563b91f4bd1..bb3ad07afc6 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -16,7 +16,7 @@ inMemoryUserStoreInterpreter :: (Member (State [StoredUser]) r) => InterpreterFor UserStore r inMemoryUserStoreInterpreter = interpret $ \case - GetUser uid -> gets $ find (\user -> user.id == uid) + GetUsers uids -> gets $ filter (\user -> user.id `elem` uids) UpdateUser uid update -> modify (map doUpdate) where doUpdate :: StoredUser -> StoredUser diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs index b47bfbd7d25..45dc93a379a 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs @@ -4,12 +4,15 @@ import Data.Qualified import Imports import Polysemy import Wire.API.User -import Wire.UserKeyStore import Wire.UserSubsystem -userSubsystemTestInterpreter :: [UserAccount] -> InterpreterFor UserSubsystem r +-- HINT: This is used to test AuthenticationSubsystem, not to test itself! +userSubsystemTestInterpreter :: [ExtendedUserAccount] -> InterpreterFor UserSubsystem r userSubsystemTestInterpreter initialUsers = interpret \case - GetLocalUserAccountByUserKey localUserKey -> case (tUnqualified localUserKey) of - EmailKey _ email -> pure $ find (\u -> userEmail u.accountUser == Just email) initialUsers + GetExtendedAccountsByEmailNoFilter (tUnqualified -> emails) -> + pure $ + filter + (\u -> userEmail u.account.accountUser `elem` (Just <$> emails)) + initialUsers _ -> error $ "userSubsystemTestInterpreter: implement on demand" diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 9a98d7b1ae5..a7975a867a1 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -28,6 +28,8 @@ import Wire.API.Team.Member import Wire.API.Team.Permission import Wire.API.User hiding (DeleteUser) import Wire.API.UserEvent +import Wire.InvitationCodeStore (StoredInvitation) +import Wire.InvitationCodeStore qualified as InvitationStore import Wire.MiniBackend import Wire.StoredUser import Wire.UserKeyStore @@ -277,6 +279,302 @@ 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 + alice = + alice' + { email = Just email, + teamId = Just teamId + -- For simplicity, so we don't have to match the email with invitation + } + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = NoPendingInvitations + } + localBackend = + def + { users = [alice], + -- We need valid invitations or the user gets deleted by + -- our drive-by cleanup job in the interprter. + -- FUTUREWORK: Remove this if we remove the enqueueDeletion from getAccountsByImpl + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + + prop "GetBy userId works for pending if explicitly queried" $ + \(PendingNotEmptyIdentityStoredUser alice') email teamId invitationInfo localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale + alice = + alice' + { email = Just email, + teamId = Just teamId + -- For simplicity, so we don't have to match the email with invitation + } + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + -- We need valid invitations or the user gets deleted by + -- our drive-by cleanup job in the interprter. + -- FUTUREWORK: Remove this if we remove the enqueueDeletion from getAccountsByImpl + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [mkAccountFromStored 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 + alice = + alice' + { email = Just email, + teamId = Just teamId, + handle = Just handl + -- For simplicity, so we don't have to match the email with invitation + } + getBy = + toLocalUnsafe localDomain $ + def + { getByHandle = [handl], + includePendingInvitations = NoPendingInvitations + } + localBackend = + def + { users = [alice], + -- We need valid invitations or the user gets deleted by + -- our drive-by cleanup job in the interprter. + -- FUTUREWORK: Remove this if we remove the enqueueDeletion from getAccountsByImpl + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + + prop "GetBy handle works for pending if explicitly queried" $ + \(PendingNotEmptyIdentityStoredUser alice') handl email teamId invitationInfo localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale + alice = + alice' + { email = Just email, + teamId = Just teamId, + handle = Just handl + -- For simplicity, so we don't have to match the email with invitation + } + getBy = + toLocalUnsafe localDomain $ + def + { getByHandle = [handl], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + -- We need valid invitations or the user gets deleted by + -- our drive-by cleanup job in the interprter. + -- FUTUREWORK: Remove this if we remove the enqueueDeletion from getAccountsByImpl + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [mkAccountFromStored localDomain locale alice] + + prop "GetBy email does not filter by pending, missing identity or expired invitations" $ + \(alice' :: StoredUser) email localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale + alice = alice' {email = Just email} + localBackend = + def + { users = [alice], + userKeys = Map.singleton (mkEmailKey email) alice.id + } + result = + runNoFederationStack localBackend Nothing config $ + getExtendedAccountsByEmailNoFilter (toLocalUnsafe localDomain [email]) + in result === [mkExtendedAccountFromStored localDomain locale alice] + + prop "GetBy userId does not return missing identity users, pending invitation off" $ + \(NotPendingEmptyIdentityStoredUser alice) localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = NoPendingInvitations + } + localBackend = def {users = [alice]} + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + + prop "GetBy userId does not return missing identity users, pending invtation on" $ + \(NotPendingEmptyIdentityStoredUser alice) localDomain visibility locale -> + let config = UserSubsystemConfig visibility locale + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = WithPendingInvitations + } + localBackend = def {users = [alice]} + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + + 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 + emailKey = mkEmailKey email + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + userKeys = Map.singleton emailKey alice.id, + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + alice = alice' {email = Just email, teamId = Just teamId} + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [mkAccountFromStored localDomain locale alice] + + 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 + emailKey = mkEmailKey email + getBy = + toLocalUnsafe localDomain $ + def + { getByUserId = [alice.id], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + userKeys = Map.singleton emailKey alice.id + } + alice = alice' {email = Just email, teamId = Just teamId} + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + + 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 + emailKey = mkEmailKey email + getBy = + toLocalUnsafe localDomain $ + def + { getByHandle = [handl], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + userKeys = Map.singleton emailKey alice.id, + invitations = + Map.singleton + (teamId, invitationInfo.invitationId) + ( invitationInfo + { InvitationStore.email = email, + InvitationStore.teamId = teamId + } + ) + } + alice = + alice' + { email = Just email, + teamId = Just teamId, + handle = Just handl + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [mkAccountFromStored localDomain locale alice] + + 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 + emailKey = mkEmailKey email + getBy = + toLocalUnsafe localDomain $ + def + { getByHandle = [handl], + includePendingInvitations = WithPendingInvitations + } + localBackend = + def + { users = [alice], + userKeys = Map.singleton emailKey alice.id + } + alice = + alice' + { email = Just email, + teamId = Just teamId, + handle = Just handl + } + result = + runNoFederationStack localBackend Nothing config $ + getAccountsBy getBy + in result === [] + describe "user managed by scim doesn't allow certain update operations, but allows others" $ do prop "happy" $ \(NotPendingStoredUser alice) localDomain update config -> diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index e2763335c9f..a544025aa7b 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -69,6 +69,8 @@ library -- cabal-fmt: expand src exposed-modules: + Wire.ActivationCodeStore + Wire.ActivationCodeStore.Cassandra Wire.AuthenticationSubsystem Wire.AuthenticationSubsystem.Error Wire.AuthenticationSubsystem.Interpreter @@ -92,6 +94,8 @@ library Wire.GundeckAPIAccess Wire.HashPassword Wire.InternalEvent + Wire.InvitationCodeStore + Wire.InvitationCodeStore.Cassandra Wire.NotificationSubsystem Wire.NotificationSubsystem.Interpreter Wire.ParseException @@ -136,6 +140,7 @@ library , bytestring , bytestring-conversion , cassandra-util + , conduit , containers , cql , crypton @@ -185,10 +190,12 @@ library , types-common , unliftio , unordered-containers + , uri-bytestring , uuid , wai-utilities , wire-api , wire-api-federation + , witherable default-language: GHC2021 @@ -206,12 +213,14 @@ test-suite wire-subsystems-tests Wire.AuthenticationSubsystem.InterpreterSpec Wire.MiniBackend Wire.MockInterpreters + Wire.MockInterpreters.ActivationCodeStore Wire.MockInterpreters.BlockListStore Wire.MockInterpreters.EmailSubsystem Wire.MockInterpreters.Error Wire.MockInterpreters.Events Wire.MockInterpreters.GalleyAPIAccess Wire.MockInterpreters.HashPassword + Wire.MockInterpreters.InvitationCodeStore Wire.MockInterpreters.Now Wire.MockInterpreters.PasswordResetCodeStore Wire.MockInterpreters.PasswordStore diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 3f71d1eff87..c723695019b 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -188,7 +188,6 @@ library Brig.Schema.V84_DropTeamInvitationPhone Brig.Schema.V85_DropUserKeysHashed Brig.Team.API - Brig.Team.DB Brig.Team.Email Brig.Team.Template Brig.Team.Util @@ -298,7 +297,6 @@ library , safe-exceptions >=0.1 , saml2-web-sso , schema-profunctor - , scientific >=0.3.4 , servant , servant-openapi3 , servant-server diff --git a/services/brig/default.nix b/services/brig/default.nix index 20eb64d007b..03a8b688aa3 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -105,7 +105,6 @@ , safe-exceptions , saml2-web-sso , schema-profunctor -, scientific , servant , servant-client , servant-client-core @@ -247,7 +246,6 @@ mkDerivation { safe-exceptions saml2-web-sso schema-profunctor - scientific servant servant-openapi3 servant-server diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index cb167140ffb..eace1f730de 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -114,6 +114,7 @@ login :: Member PasswordStore r, Member UserKeyStore r, Member UserStore r, + Member UserSubsystem r, Member VerificationCodeSubsystem r ) => Login -> @@ -169,9 +170,16 @@ listCookies lusr (fold -> labels) = CookieList <$> wrapClientE (Auth.listCookies (tUnqualified lusr) (toList labels)) -removeCookies :: (Member TinyLog r, Member PasswordStore r) => Local UserId -> RemoveCookies -> Handler r () +removeCookies :: + ( Member TinyLog r, + Member PasswordStore r, + Member UserSubsystem r + ) => + Local UserId -> + RemoveCookies -> + Handler r () removeCookies lusr (RemoveCookies pw lls ids) = - Auth.revokeAccess (tUnqualified lusr) pw ids lls !>> authError + Auth.revokeAccess lusr pw ids lls !>> authError legalHoldLogin :: ( Member GalleyAPIAccess r, @@ -208,12 +216,19 @@ ssoLogin l (fromMaybe False -> persist) = do getLoginCode :: Phone -> Handler r PendingLoginCode getLoginCode _ = throwStd loginCodeNotFound -reauthenticate :: (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => UserId -> ReAuthUser -> Handler r () -reauthenticate uid body = do +reauthenticate :: + ( Member GalleyAPIAccess r, + Member VerificationCodeSubsystem r, + Member UserSubsystem r + ) => + Local UserId -> + ReAuthUser -> + Handler r () +reauthenticate luid@(tUnqualified -> uid) body = do wrapClientE (User.reauthenticate uid (reAuthPassword body)) !>> reauthError case reAuthCodeAction body of Just action -> - Auth.verifyCode (reAuthCode body) action uid + Auth.verifyCode (reAuthCode body) action luid `catchE` \case VerificationCodeRequired -> throwE $ reauthError ReAuthCodeVerificationRequired VerificationCodeNoPendingCode -> throwE $ reauthError ReAuthCodeVerificationNoPendingCode diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index eecc682427b..cc513f7bfa8 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -75,6 +75,7 @@ import Data.ByteString (toStrict) import Data.ByteString.Conversion import Data.Code as Code import Data.Domain +import Data.HavePendingInvitations import Data.Id (ClientId, ConnId, UserId) import Data.List.Split (chunksOf) import Data.Map.Strict qualified as Map @@ -115,6 +116,8 @@ import Wire.Sem.Concurrency import Wire.Sem.FromUTC (FromUTC (fromUTCTime)) import Wire.Sem.Now as Now import Wire.Sem.Paging.Cassandra (InternalPaging) +import Wire.UserSubsystem (UserSubsystem) +import Wire.UserSubsystem qualified as User import Wire.VerificationCodeSubsystem (VerificationCodeSubsystem) lookupLocalClient :: UserId -> ClientId -> (AppT r) (Maybe Client) @@ -164,6 +167,7 @@ addClient :: ( Member GalleyAPIAccess r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, + Member UserSubsystem r, Member TinyLog r, Member DeleteQueue r, Member (Input (Local ())) r, @@ -172,7 +176,7 @@ addClient :: Member EmailSubsystem r, Member VerificationCodeSubsystem r ) => - UserId -> + Local UserId -> Maybe ConnId -> NewClient -> ExceptT ClientError (AppT r) Client @@ -191,16 +195,17 @@ addClientWithReAuthPolicy :: Member DeleteQueue r, Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, + Member UserSubsystem r, Member VerificationCodeSubsystem r ) => Data.ReAuthPolicy -> - UserId -> + Local UserId -> Maybe ConnId -> NewClient -> ExceptT ClientError (AppT r) Client -addClientWithReAuthPolicy policy u con new = do - acc <- lift (wrapClient $ Data.lookupAccount u) >>= maybe (throwE (ClientUserNotFound u)) pure - verifyCode (newClientVerificationCode new) (userId . accountUser $ acc) +addClientWithReAuthPolicy policy luid@(tUnqualified -> u) con new = do + usr <- (lift . liftSem $ User.getAccountNoFilter luid) >>= maybe (throwE (ClientUserNotFound u)) (pure . (.accountUser)) + verifyCode (newClientVerificationCode new) luid maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings let caps :: Maybe (Set ClientCapability) caps = updlhdev $ newClientCapabilities new @@ -212,10 +217,9 @@ addClientWithReAuthPolicy policy u con new = do lhcaps = ClientSupportsLegalholdImplicitConsent (clt0, old, count) <- wrapClientE - (Data.addClientWithReAuthPolicy policy u clientId' new maxPermClients caps) + (Data.addClientWithReAuthPolicy policy luid clientId' new maxPermClients caps) !>> ClientDataError let clt = clt0 {clientMLSPublicKeys = newClientMLSPublicKeys new} - let usr = accountUser acc lift $ do for_ old $ execDelete u con liftSem $ GalleyAPIAccess.newClient u (clientId clt) @@ -231,12 +235,12 @@ addClientWithReAuthPolicy policy u con new = do verifyCode :: Maybe Code.Value -> - UserId -> + Local UserId -> ExceptT ClientError (AppT r) () - verifyCode mbCode uid = + verifyCode mbCode luid1 = -- this only happens inside the login flow (in particular, when logging in from a new device) -- the code obtained for logging in is used a second time for adding the device - UserAuth.verifyCode mbCode Code.Login uid `catchE` \case + UserAuth.verifyCode mbCode Code.Login luid1 `catchE` \case VerificationCodeRequired -> throwE ClientCodeAuthenticationRequired VerificationCodeNoPendingCode -> throwE ClientCodeAuthenticationFailed VerificationCodeNoEmail -> throwE ClientCodeAuthenticationFailed diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index f718cd465d1..cb3ed7e3dd0 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -72,6 +72,7 @@ import Wire.GalleyAPIAccess import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.NotificationSubsystem import Wire.UserStore +import Wire.UserSubsystem ensureNotSameTeam :: (Member GalleyAPIAccess r) => Local UserId -> Local UserId -> (ConnectionM r) () ensureNotSameTeam self target = do @@ -86,6 +87,7 @@ createConnection :: Member NotificationSubsystem r, Member TinyLog r, Member UserStore r, + Member UserSubsystem r, Member (Embed HttpClientIO) r ) => Local UserId -> @@ -106,6 +108,7 @@ createConnectionToLocalUser :: Member NotificationSubsystem r, Member TinyLog r, Member UserStore r, + Member UserSubsystem r, Member (Embed HttpClientIO) r ) => Local UserId -> @@ -116,7 +119,7 @@ createConnectionToLocalUser self conn target = do ensureNotSameAndActivated self (tUntagged target) noteT (InvalidUser (tUntagged target)) $ ensureIsActivated target - checkLegalholdPolicyConflict (tUnqualified self) (tUnqualified target) + checkLegalholdPolicyConflict self target ensureNotSameTeam self target s2o <- lift . wrapClient $ Data.lookupConnection self (tUntagged target) o2s <- lift . wrapClient $ Data.lookupConnection target (tUntagged self) @@ -194,9 +197,9 @@ createConnectionToLocalUser self conn target = do -- FUTUREWORK: we may want to move this to the LH application logic, so we can recycle it for -- group conv creation and possibly other situations. checkLegalholdPolicyConflict :: - (Member GalleyAPIAccess r) => - UserId -> - UserId -> + (Member GalleyAPIAccess r, Member UserSubsystem r) => + Local UserId -> + Local UserId -> ExceptT ConnectionError (AppT r) () checkLegalholdPolicyConflict uid1 uid2 = do let catchProfileNotFound = diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index fd471ba62e7..03c9af86610 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -50,7 +50,6 @@ import Brig.IO.Intra qualified as Intra import Brig.Options hiding (internalEvents) import Brig.Provider.API qualified as Provider import Brig.Team.API qualified as Team -import Brig.Team.DB (lookupInvitationByEmail) import Brig.Types.Connection import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) @@ -66,6 +65,7 @@ import Data.CommaSeparatedList import Data.Default import Data.Domain (Domain) import Data.Handle +import Data.HavePendingInvitations import Data.Id as Id import Data.Map.Strict qualified as Map import Data.Qualified @@ -76,7 +76,7 @@ import Data.Time.Clock.System import Imports hiding (head) import Network.Wai.Utilities as Utilities import Polysemy -import Polysemy.Input (Input) +import Polysemy.Input (Input, input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) import Servant.OpenApi.Internal.Orphans () @@ -100,11 +100,13 @@ import Wire.API.User.RichInfo import Wire.API.UserEvent import Wire.AuthenticationSubsystem (AuthenticationSubsystem) import Wire.BlockListStore (BlockListStore) -import Wire.DeleteQueue +import Wire.DeleteQueue (DeleteQueue) import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem (EmailSubsystem) -import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..)) +import Wire.GalleyAPIAccess (GalleyAPIAccess) +import Wire.InvitationCodeStore import Wire.NotificationSubsystem +import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PropertySubsystem import Wire.Rpc import Wire.Sem.Concurrency @@ -132,6 +134,7 @@ servantSitemap :: Member NotificationSubsystem r, Member UserSubsystem r, Member UserStore r, + Member InvitationCodeStore r, Member UserKeyStore r, Member Rpc r, Member TinyLog r, @@ -139,6 +142,7 @@ servantSitemap :: Member EmailSending r, Member EmailSubsystem r, Member VerificationCodeSubsystem r, + Member PasswordResetCodeStore r, Member PropertySubsystem r ) => ServerT BrigIRoutes.API (Handler r) @@ -190,7 +194,9 @@ accountAPI :: Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, Member VerificationCodeSubsystem r, - Member PropertySubsystem r + Member PropertySubsystem r, + Member PasswordResetCodeStore r, + Member InvitationCodeStore r ) => ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = @@ -240,6 +246,7 @@ teamsAPI :: Member TinyLog r, Member (Input (Local ())) r, Member (Input UTCTime) r, + Member InvitationCodeStore r, Member (ConnectionStore InternalPaging) r, Member EmailSending r, Member UserSubsystem r @@ -268,6 +275,7 @@ authAPI :: Member TinyLog r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, + Member UserSubsystem r, Member (Input (Local ())) r, Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, @@ -278,7 +286,13 @@ authAPI = Named @"legalhold-login" (callsFed (exposeAnnotations legalHoldLogin)) :<|> Named @"sso-login" (callsFed (exposeAnnotations ssoLogin)) :<|> Named @"login-code" getLoginCode - :<|> Named @"reauthenticate" reauthenticate + :<|> Named @"reauthenticate" + ( \uid reauth -> + -- changing this end-point would involve providing a `Local` type from a user id that is + -- captured from the path, not pulled from the http header. this is certainly feasible, + -- but running qualifyLocal here is easier. + qualifyLocal uid >>= \luid -> reauthenticate luid reauth + ) federationRemotesAPI :: (Member FederationConfigStore r) => ServerT BrigIRoutes.FederationRemotesAPI (Handler r) federationRemotesAPI = @@ -408,6 +422,7 @@ addClientInternalH :: Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, + Member UserSubsystem r, Member VerificationCodeSubsystem r ) => UserId -> @@ -419,7 +434,8 @@ addClientInternalH usr mSkipReAuth new connId = do let policy | mSkipReAuth == Just True = \_ _ -> False | otherwise = Data.reAuthForNewClients - API.addClientWithReAuthPolicy policy usr connId new !>> clientError + lusr <- qualifyLocal usr + API.addClientWithReAuthPolicy policy lusr connId new !>> clientError legalHoldClientRequestedH :: ( Member (Embed HttpClientIO) r, @@ -465,9 +481,12 @@ createUserNoVerify :: Member TinyLog r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, + Member InvitationCodeStore r, Member UserKeyStore r, + Member UserSubsystem r, Member (Input (Local ())) r, Member (Input UTCTime) r, + Member PasswordResetCodeStore r, Member (ConnectionStore InternalPaging) r ) => NewUser -> @@ -492,6 +511,7 @@ createUserNoVerifySpar :: Member TinyLog r, Member (Input (Local ())) r, Member (Input UTCTime) r, + Member PasswordResetCodeStore r, Member (ConnectionStore InternalPaging) r ) => NewUserSpar -> @@ -515,6 +535,7 @@ deleteUserNoAuthH :: Member UserStore r, Member TinyLog r, Member UserKeyStore r, + Member UserSubsystem r, Member (Input (Local ())) r, Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, @@ -523,7 +544,8 @@ deleteUserNoAuthH :: UserId -> (Handler r) DeleteUserResponse deleteUserNoAuthH uid = do - r <- lift $ API.ensureAccountDeleted uid + luid <- qualifyLocal uid + r <- lift $ API.ensureAccountDeleted luid case r of NoUser -> throwStd (errorToWai @'E.UserNotFound) AccountAlreadyDeleted -> pure UserResponseAccountAlreadyDeleted @@ -549,9 +571,8 @@ changeSelfEmailMaybeSend u DoNotSendEmail email allowScim = do -- handler allows up to 4 lists of various user keys, and returns the union of the lookups. -- Empty list is forbidden for backwards compatibility. listActivatedAccountsH :: - ( Member DeleteQueue r, - Member UserKeyStore r, - Member UserStore r + ( Member (Input (Local ())) r, + Member UserSubsystem r ) => Maybe (CommaSeparatedList UserId) -> Maybe (CommaSeparatedList Handle) -> @@ -562,50 +583,21 @@ listActivatedAccountsH (maybe [] fromCommaSeparatedList -> uids) (maybe [] fromCommaSeparatedList -> handles) (maybe [] fromCommaSeparatedList -> emails) - (fromMaybe False -> includePendingInvitations) = do + (maybe NoPendingInvitations fromBool -> include) = do when (length uids + length handles + length emails == 0) $ do throwStd (notFound "no user keys") - lift $ do - u1 <- listActivatedAccounts (Left uids) includePendingInvitations - u2 <- listActivatedAccounts (Right handles) includePendingInvitations - u3 <- (\email -> API.lookupExtendedAccountsByIdentity email includePendingInvitations) `mapM` emails - pure $ u1 <> u2 <> join u3 - --- FUTUREWORK: this should use UserStore only through UserSubsystem. -listActivatedAccounts :: - (Member DeleteQueue r, Member UserStore r) => - Either [UserId] [Handle] -> - Bool -> - AppT r [ExtendedUserAccount] -listActivatedAccounts elh includePendingInvitations = do - Log.debug (Log.msg $ "listActivatedAccounts: " <> show (elh, includePendingInvitations)) - case elh of - Left us -> byIds us - Right hs -> do - us <- liftSem $ mapM API.lookupHandle hs - byIds (catMaybes us) - where - byIds :: (Member DeleteQueue r) => [UserId] -> (AppT r) [ExtendedUserAccount] - byIds uids = wrapClient (API.lookupExtendedAccounts uids) >>= filterM accountValid - - accountValid :: (Member DeleteQueue r) => ExtendedUserAccount -> (AppT r) Bool - accountValid (account -> acc) = case userIdentity . accountUser $ acc of - Nothing -> pure False - Just ident -> - case (accountStatus acc, includePendingInvitations, emailIdentity ident) of - (PendingInvitation, False, _) -> pure False - (PendingInvitation, True, Just email) -> do - hasInvitation <- isJust <$> wrapClient (lookupInvitationByEmail HideInvitationUrl email) - unless hasInvitation $ do - -- user invited via scim should expire together with its invitation - liftSem $ API.deleteUserNoVerify (userId . accountUser $ acc) - pure hasInvitation - (PendingInvitation, True, Nothing) -> - pure True -- cannot happen, user invited via scim always has an email - (Active, _, _) -> pure True - (Suspended, _, _) -> pure True - (Deleted, _, _) -> pure True - (Ephemeral, _, _) -> pure True + lift $ liftSem do + loc <- input + byEmails <- getExtendedAccountsByEmailNoFilter $ loc $> emails + others <- + getExtendedAccountsBy $ + loc + $> def + { includePendingInvitations = include, + getByUserId = uids, + getByHandle = handles + } + pure $ others <> byEmails getActivationCode :: EmailAddress -> Handler r GetActivationCodeResp getActivationCode email = do diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 9e06bc14d4d..145d0772959 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -39,6 +39,7 @@ import Data.Id import Data.Json.Util (toUTCTimeMillis) import Data.Map qualified as Map import Data.Misc +import Data.Qualified import Data.Set qualified as Set import Data.Text.Ascii import Data.Text.Encoding qualified as T @@ -67,7 +68,7 @@ import Wire.Sem.Now qualified as Now internalOauthAPI :: ServerT I.OAuthAPI (Handler r) internalOauthAPI = Named @"create-oauth-client" registerOAuthClient - :<|> Named @"get-oauth-client" getOAuthClientById + :<|> Named @"i-get-oauth-client" getOAuthClientById :<|> Named @"update-oauth-client" updateOAuthClient :<|> Named @"delete-oauth-client" deleteOAuthClient @@ -122,14 +123,14 @@ deleteOAuthClient cid = do void $ getOAuthClientById cid lift $ wrapClient $ deleteOAuthClient' cid -getOAuthClient :: UserId -> OAuthClientId -> (Handler r) (Maybe OAuthClient) +getOAuthClient :: Local UserId -> OAuthClientId -> (Handler r) (Maybe OAuthClient) getOAuthClient _ cid = do unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled lift $ wrapClient $ lookupOauthClient cid -createNewOAuthAuthorizationCode :: UserId -> CreateOAuthAuthorizationCodeRequest -> (Handler r) CreateOAuthCodeResponse -createNewOAuthAuthorizationCode uid code = do - runExceptT (validateAndCreateAuthorizationCode uid code) >>= \case +createNewOAuthAuthorizationCode :: Local UserId -> CreateOAuthAuthorizationCodeRequest -> (Handler r) CreateOAuthCodeResponse +createNewOAuthAuthorizationCode luid code = do + runExceptT (validateAndCreateAuthorizationCode luid code) >>= \case Right oauthCode -> pure $ CreateOAuthCodeSuccess $ @@ -174,11 +175,11 @@ data CreateNewOAuthCodeError | CreateNewOAuthCodeErrorUnsupportedResponseType | CreateNewOAuthCodeErrorRedirectUrlMissMatch -validateAndCreateAuthorizationCode :: UserId -> CreateOAuthAuthorizationCodeRequest -> ExceptT CreateNewOAuthCodeError (Handler r) OAuthAuthorizationCode -validateAndCreateAuthorizationCode uid (CreateOAuthAuthorizationCodeRequest cid scope responseType redirectUrl _state _ chal) = do +validateAndCreateAuthorizationCode :: Local UserId -> CreateOAuthAuthorizationCodeRequest -> ExceptT CreateNewOAuthCodeError (Handler r) OAuthAuthorizationCode +validateAndCreateAuthorizationCode luid@(tUnqualified -> uid) (CreateOAuthAuthorizationCodeRequest cid scope responseType redirectUrl _state _ chal) = do failWithM CreateNewOAuthCodeErrorFeatureDisabled (assertMay . Opt.setOAuthEnabled <$> view settings) failWith CreateNewOAuthCodeErrorUnsupportedResponseType (assertMay $ responseType == OAuthResponseTypeCode) - client <- failWithM CreateNewOAuthCodeErrorClientNotFound $ getOAuthClient uid cid + client <- failWithM CreateNewOAuthCodeErrorClientNotFound $ getOAuthClient luid cid failWith CreateNewOAuthCodeErrorRedirectUrlMissMatch (assertMay $ client.redirectUrl == redirectUrl) lift mkAuthorizationCode where @@ -201,7 +202,8 @@ createAccessTokenWithRefreshToken req = do unless (req.grantType == OAuthGrantTypeRefreshToken) $ throwStd $ errorToWai @'OAuthInvalidGrantType key <- signingKey (OAuthRefreshTokenInfo _ cid uid scope _) <- lookupVerifyAndDeleteToken key req.refreshToken - void $ getOAuthClient uid cid >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure + luid <- qualifyLocal uid + void $ getOAuthClient luid cid >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure unless (cid == req.clientId) $ throwStd $ errorToWai @'OAuthInvalidClientCredentials createAccessToken key uid cid scope @@ -226,7 +228,8 @@ createAccessTokenWithAuthorizationCode req = do (cid, uid, scope, uri, mChal) <- lift (wrapClient $ lookupAndDeleteByOAuthAuthorizationCode req.code) >>= maybe (throwStd $ errorToWai @'OAuthAuthorizationCodeNotFound) pure - oauthClient <- getOAuthClient uid req.clientId >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure + luid <- qualifyLocal uid + oauthClient <- getOAuthClient luid req.clientId >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure unless (uri == req.redirectUri) $ throwStd $ errorToWai @'OAuthRedirectUrlMissMatch unless (oauthClient.redirectUrl == req.redirectUri) $ throwStd $ errorToWai @'OAuthRedirectUrlMissMatch @@ -305,7 +308,8 @@ revokeRefreshToken :: (Member Jwk r) => OAuthRevokeRefreshTokenRequest -> (Handl revokeRefreshToken req = do key <- signingKey info <- lookupAndVerifyToken key req.refreshToken - void $ getOAuthClient info.userId info.clientId >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure + luid <- qualifyLocal info.userId + void $ getOAuthClient luid info.clientId >>= maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure lift $ wrapClient $ deleteOAuthRefreshToken info.userId info.refreshTokenId lookupAndVerifyToken :: JWK -> OAuthRefreshToken -> (Handler r) OAuthRefreshTokenInfo @@ -316,8 +320,8 @@ lookupAndVerifyToken key = . lookupOAuthRefreshTokenInfo >=> maybe (throwStd $ errorToWai @'OAuthInvalidRefreshToken) pure -getOAuthApplications :: UserId -> (Handler r) [OAuthApplication] -getOAuthApplications uid = do +getOAuthApplications :: Local UserId -> (Handler r) [OAuthApplication] +getOAuthApplications (tUnqualified -> uid) = do activeRefreshTokens <- lift $ wrapClient $ lookupOAuthRefreshTokens uid toApplications activeRefreshTokens where @@ -325,26 +329,27 @@ getOAuthApplications uid = do toApplications infos = do let grouped = Map.fromListWith (<>) $ (\info -> (info.clientId, [info])) <$> infos mApps <- for (Map.toList grouped) $ \(cid, tokens) -> do - mClient <- getOAuthClient uid cid + let luid = undefined uid + mClient <- getOAuthClient luid cid pure $ (\client -> OAuthApplication cid client.name ((\i -> OAuthSession i.refreshTokenId (toUTCTimeMillis i.createdAt)) <$> tokens)) <$> mClient pure $ catMaybes mApps -revokeOAuthAccountAccessV6 :: UserId -> OAuthClientId -> (Handler r) () -revokeOAuthAccountAccessV6 uid cid = do +revokeOAuthAccountAccessV6 :: Local UserId -> OAuthClientId -> (Handler r) () +revokeOAuthAccountAccessV6 (tUnqualified -> uid) cid = do rts <- lift $ wrapClient $ lookupOAuthRefreshTokens uid for_ rts $ \rt -> when (rt.clientId == cid) $ lift $ wrapClient $ deleteOAuthRefreshToken uid rt.refreshTokenId -revokeOAuthAccountAccess :: UserId -> OAuthClientId -> PasswordReqBody -> (Handler r) () -revokeOAuthAccountAccess uid cid req = do - wrapClientE $ reauthenticate uid req.fromPasswordReqBody !>> toAccessDenied - revokeOAuthAccountAccessV6 uid cid +revokeOAuthAccountAccess :: Local UserId -> OAuthClientId -> PasswordReqBody -> (Handler r) () +revokeOAuthAccountAccess luid@(tUnqualified -> uid) cid req = do + wrapClientE (reauthenticate uid req.fromPasswordReqBody) !>> toAccessDenied + revokeOAuthAccountAccessV6 luid cid where toAccessDenied :: ReAuthError -> HttpError toAccessDenied _ = StdError $ errorToWai @'AccessDenied -deleteOAuthRefreshTokenById :: UserId -> OAuthClientId -> OAuthRefreshTokenId -> PasswordReqBody -> (Handler r) () -deleteOAuthRefreshTokenById uid cid tokenId req = do - wrapClientE $ reauthenticate uid req.fromPasswordReqBody !>> toAccessDenied +deleteOAuthRefreshTokenById :: Local UserId -> OAuthClientId -> OAuthRefreshTokenId -> PasswordReqBody -> (Handler r) () +deleteOAuthRefreshTokenById (tUnqualified -> uid) cid tokenId req = do + wrapClientE (reauthenticate uid req.fromPasswordReqBody) !>> toAccessDenied mInfo <- lift $ wrapClient $ lookupOAuthRefreshTokenInfo tokenId case mInfo of Nothing -> pure () diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index dc58cb86e28..554eceb5b85 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -53,7 +53,6 @@ import Brig.Team.API qualified as Team import Brig.Team.Email qualified as Team import Brig.Types.Activation (ActivationPair) import Brig.Types.Intra (UserAccount (UserAccount, accountUser)) -import Brig.Types.User (HavePendingInvitations (..)) import Brig.User.API.Handle qualified as Handle import Brig.User.API.Search (teamUserSearch) import Brig.User.API.Search qualified as Search @@ -75,6 +74,7 @@ import Data.Domain import Data.FileEmbed import Data.Handle (Handle) import Data.Handle qualified as Handle +import Data.HavePendingInvitations import Data.Id import Data.Id qualified as Id import Data.List.NonEmpty (nonEmpty) @@ -152,7 +152,9 @@ import Wire.EmailSubsystem import Wire.Error import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.InvitationCodeStore import Wire.NotificationSubsystem +import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PasswordStore (PasswordStore, lookupHashedPassword) import Wire.PropertySubsystem import Wire.Sem.Concurrency @@ -162,7 +164,7 @@ import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore (UserStore) import Wire.UserSubsystem hiding (checkHandle, checkHandles) -import Wire.UserSubsystem qualified as UserSubsystem +import Wire.UserSubsystem qualified as User import Wire.VerificationCode import Wire.VerificationCodeGen import Wire.VerificationCodeSubsystem @@ -285,7 +287,9 @@ servantSitemap :: Member EmailSubsystem r, Member EmailSending r, Member VerificationCodeSubsystem r, - Member PropertySubsystem r + Member PropertySubsystem r, + Member PasswordResetCodeStore r, + Member InvitationCodeStore r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -559,17 +563,18 @@ addClient :: Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, + Member UserSubsystem r, Member VerificationCodeSubsystem r ) => - UserId -> + Local UserId -> ConnId -> Public.NewClient -> Handler r Public.Client -addClient usr con new = do +addClient lusr con new = do -- Users can't add legal hold clients when (Public.newClientType new == Public.LegalHoldClientType) $ throwE (clientError ClientLegalHoldCannotBeAdded) - API.addClient usr (Just con) new + API.addClient lusr (Just con) new !>> clientError deleteClient :: @@ -623,21 +628,21 @@ getClientCapabilities uid cid = do mclient <- lift (API.lookupLocalClient uid cid) maybe (throwStd (errorToWai @'E.ClientNotFound)) (pure . Public.clientCapabilities) mclient -getRichInfo :: UserId -> UserId -> Handler r Public.RichInfoAssocList -getRichInfo self user = do +getRichInfo :: (Member UserSubsystem r) => Local UserId -> UserId -> Handler r Public.RichInfoAssocList +getRichInfo lself user = do + let luser = qualifyAs lself user -- Check that both users exist and the requesting user is allowed to see rich info of the -- other user - selfUser <- - ifNothing (errorToWai @'E.UserNotFound) - =<< lift (wrapClient $ Data.lookupUser NoPendingInvitations self) - otherUser <- - ifNothing (errorToWai @'E.UserNotFound) - =<< lift (wrapClient $ Data.lookupUser NoPendingInvitations user) + let fetch luid = + ifNothing (errorToWai @'E.UserNotFound) + =<< lift (liftSem $ (.accountUser) <$$> User.getLocalAccountBy NoPendingInvitations luid) + selfUser <- fetch lself + otherUser <- fetch luser case (Public.userTeam selfUser, Public.userTeam otherUser) of (Just t1, Just t2) | t1 == t2 -> pure () _ -> throwStd insufficientTeamPermissions -- Query rich info - wrapClientE $ fromMaybe mempty <$> API.lookupRichInfo user + wrapClientE $ fromMaybe mempty <$> API.lookupRichInfo (tUnqualified luser) getSupportedProtocols :: (Member UserSubsystem r) => @@ -681,6 +686,7 @@ createAccessToken method luid cid proof = do createUser :: ( Member BlockListStore r, Member GalleyAPIAccess r, + Member InvitationCodeStore r, Member (UserPendingActivationStore p) r, Member TinyLog r, Member (Embed HttpClientIO) r, @@ -690,6 +696,8 @@ createUser :: Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, + Member UserSubsystem r, + Member PasswordResetCodeStore r, Member EmailSending r ) => Public.NewUserPublic -> @@ -934,7 +942,7 @@ changeLocale lusr conn l = updateUserProfile lusr (Just conn) - UserSubsystem.UpdateOriginWireClient + User.UpdateOriginWireClient def {locale = Just l.luLocale} changeSupportedProtocols :: @@ -944,7 +952,7 @@ changeSupportedProtocols :: Public.SupportedProtocolUpdate -> Handler r () changeSupportedProtocols u conn (Public.SupportedProtocolUpdate prots) = - lift . liftSem $ UserSubsystem.updateUserProfile u (Just conn) UpdateOriginWireClient upd + lift . liftSem $ User.updateUserProfile u (Just conn) UpdateOriginWireClient upd where upd = def {supportedProtocols = Just prots} @@ -952,7 +960,7 @@ changeSupportedProtocols u conn (Public.SupportedProtocolUpdate prots) = -- *any* account.) checkHandle :: (Member UserSubsystem r) => UserId -> Text -> Handler r () checkHandle _uid hndl = - lift (liftSem $ UserSubsystem.checkHandle hndl) >>= \case + lift (liftSem $ User.checkHandle hndl) >>= \case API.CheckHandleFound -> pure () API.CheckHandleNotFound -> throwStd (errorToWai @'E.HandleNotFound) @@ -981,7 +989,7 @@ getHandleInfoUnqualifiedH self handle = do changeHandle :: (Member UserSubsystem r) => Local UserId -> ConnId -> Public.HandleUpdate -> Handler r () changeHandle u conn (Public.HandleUpdate h) = lift $ liftSem do - UserSubsystem.updateHandle u (Just conn) UpdateOriginWireClient h + User.updateHandle u (Just conn) UpdateOriginWireClient h beginPasswordReset :: (Member AuthenticationSubsystem r) => @@ -1041,6 +1049,7 @@ createConnectionUnqualified :: Member NotificationSubsystem r, Member TinyLog r, Member UserStore r, + Member UserSubsystem r, Member (Embed HttpClientIO) r ) => UserId -> @@ -1057,6 +1066,7 @@ createConnection :: Member GalleyAPIAccess r, Member NotificationSubsystem r, Member UserStore r, + Member UserSubsystem r, Member TinyLog r, Member (Embed HttpClientIO) r ) => @@ -1172,19 +1182,21 @@ deleteSelfUser :: Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, + Member UserSubsystem r, Member VerificationCodeSubsystem r, Member PropertySubsystem r ) => - UserId -> + Local UserId -> Public.DeleteUser -> (Handler r) (Maybe Code.Timeout) -deleteSelfUser u body = do - API.deleteSelfUser u (Public.deleteUserPassword body) !>> deleteUserError +deleteSelfUser lu body = do + API.deleteSelfUser lu (Public.deleteUserPassword body) !>> deleteUserError verifyDeleteUser :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member UserStore r, + Member UserSubsystem r, Member TinyLog r, Member (Input (Local ())) r, Member UserKeyStore r, @@ -1235,8 +1247,10 @@ activate :: Member TinyLog r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, + Member UserSubsystem r, Member (Input (Local ())) r, Member (Input UTCTime) r, + Member PasswordResetCodeStore r, Member (ConnectionStore InternalPaging) r ) => Public.ActivationKey -> @@ -1252,8 +1266,10 @@ activateKey :: Member TinyLog r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, + Member UserSubsystem r, Member (Input (Local ())) r, Member (Input UTCTime) r, + Member PasswordResetCodeStore r, Member (ConnectionStore InternalPaging) r ) => Public.Activate -> @@ -1275,7 +1291,9 @@ sendVerificationCode :: forall r. ( Member GalleyAPIAccess r, Member UserKeyStore r, + Member (Input (Local ())) r, Member EmailSubsystem r, + Member UserSubsystem r, Member VerificationCodeSubsystem r ) => Public.SendVerificationCode -> @@ -1301,9 +1319,10 @@ sendVerificationCode req = do _ -> pure () where getAccount :: Public.EmailAddress -> (Handler r) (Maybe UserAccount) - getAccount email = lift $ do - mbUserId <- liftSem $ lookupKey $ mkEmailKey email - join <$> wrapClient (Data.lookupAccount `traverse` mbUserId) + getAccount email = lift . liftSem $ do + mbUserId <- lookupKey $ mkEmailKey email + mbLUserId <- qualifyLocal' `traverse` mbUserId + join <$> User.getAccountNoFilter `traverse` mbLUserId sendMail :: Public.EmailAddress -> Code.Value -> Maybe Public.Locale -> Public.VerificationAction -> (Handler r) () sendMail email value mbLocale = diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 028b87d541d..076b844e1fc 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -69,6 +69,7 @@ data ActivationResult ActivationSuccess !(Maybe UserIdentity) !Bool | -- | The key/code was valid but already recently activated. ActivationPass + deriving (Show) -- | Outcome of the invariants check in 'Brig.API.User.changeEmail'. data ChangeEmailResult @@ -76,6 +77,7 @@ data ChangeEmailResult ChangeEmailNeedsActivation !(User, Activation, EmailAddress) | -- | The user asked to change the email address to the one already owned ChangeEmailIdempotent + deriving (Show) ------------------------------------------------------------------------------- -- Failures diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 84059ff5435..4f76936fae7 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -31,11 +31,6 @@ module Brig.API.User lookupHandle, changeAccountStatus, changeSingleAccountStatus, - Data.lookupAccounts, - Data.lookupExtendedAccounts, - Data.lookupAccount, - lookupAccountsByIdentity, - lookupExtendedAccountsByIdentity, getLegalHoldStatus, Data.lookupName, Data.lookupUser, @@ -89,8 +84,7 @@ import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore qualified as UserPendingActivationStore import Brig.IO.Intra qualified as Intra -import Brig.Options hiding (Timeout, internalEvents) -import Brig.Team.DB qualified as Team +import Brig.Options hiding (internalEvents) import Brig.Types.Activation (ActivationPair) import Brig.Types.Intra import Brig.User.Auth.Cookie qualified as Auth @@ -102,6 +96,7 @@ import Control.Lens (preview, to, view, (^.), _Just) import Control.Monad.Catch import Data.ByteString.Conversion import Data.Code +import Data.Coerce (coerce) import Data.Currency qualified as Currency import Data.Handle (Handle (fromHandle)) import Data.Id as Id @@ -129,8 +124,6 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Password import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Team hiding (newTeam) -import Wire.API.Team.Invitation -import Wire.API.Team.Invitation qualified as Team import Wire.API.Team.Member (legalHoldStatus) import Wire.API.Team.Role import Wire.API.Team.Size @@ -145,7 +138,10 @@ import Wire.DeleteQueue import Wire.EmailSubsystem import Wire.Error import Wire.GalleyAPIAccess as GalleyAPIAccess +import Wire.InvitationCodeStore (InvitationCodeStore, StoredInvitation, StoredInvitationInfo) +import Wire.InvitationCodeStore qualified as InvitationCodeStore import Wire.NotificationSubsystem +import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PasswordStore (PasswordStore, lookupHashedPassword, upsertHashedPassword) import Wire.PropertySubsystem as PropertySubsystem import Wire.Sem.Concurrency @@ -246,7 +242,7 @@ createUserSpar new = do addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> Role -> ExceptT RegisterError (AppT r) CreateUserTeam addUserToTeamSSO account tid ident role = do let uid = userId (accountUser account) - added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid (Nothing, role) + added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid Nothing role unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do @@ -267,12 +263,15 @@ createUser :: Member GalleyAPIAccess r, Member (UserPendingActivationStore p) r, Member UserKeyStore r, + Member UserSubsystem r, Member TinyLog r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member (Input (Local ())) r, Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member (ConnectionStore InternalPaging) r, + Member PasswordResetCodeStore r, + Member InvitationCodeStore r ) => NewUser -> ExceptT RegisterError (AppT r) CreateUserResult @@ -295,8 +294,14 @@ createUser new = do pure (Nothing, Nothing, Just tid) Nothing -> pure (Nothing, Nothing, Nothing) - let mbInv = Team.inInvitation . fst <$> teamInvitation - mbExistingAccount <- lift $ join <$> for mbInv (\(Id uuid) -> wrapClient $ Data.lookupAccount (Id uuid)) + let mbInv = (.invitationId) . fst <$> teamInvitation + mbExistingAccount <- + lift $ + join + <$> for mbInv do + \invid -> liftSem $ do + luid :: Local UserId <- qualifyLocal' (coerce invid) + User.getLocalAccountBy WithPendingInvitations luid let (new', mbHandle) = case mbExistingAccount of Nothing -> @@ -337,7 +342,7 @@ createUser new = do pure account - let uid = userId (accountUser account) + let uid = qUnqualified account.accountUser.userQualifiedId createUserTeam <- do activatedTeam <- lift $ do @@ -354,10 +359,9 @@ createUser new = do joinedTeamInvite <- case teamInvitation of Just (inv, invInfo) -> do - let em = Team.inInviteeEmail inv - acceptTeamInvitation account inv invInfo (mkEmailKey em) (EmailIdentity em) - Team.TeamName nm <- lift $ liftSem $ GalleyAPIAccess.getTeamName (Team.inTeam inv) - pure (Just $ CreateUserTeam (Team.inTeam inv) nm) + acceptTeamInvitation account inv invInfo (mkEmailKey inv.email) (EmailIdentity inv.email) + Team.TeamName nm <- lift $ liftSem $ GalleyAPIAccess.getTeamName inv.teamId + pure (Just $ CreateUserTeam inv.teamId nm) Nothing -> pure Nothing joinedTeamSSO <- case (newUserIdentity new', tid) of @@ -385,17 +389,25 @@ createUser new = do pure email - findTeamInvitation :: Maybe EmailKey -> InvitationCode -> ExceptT RegisterError (AppT r) (Maybe (Team.Invitation, Team.InvitationInfo, TeamId)) + findTeamInvitation :: + Maybe EmailKey -> + InvitationCode -> + ExceptT + RegisterError + (AppT r) + ( Maybe + (StoredInvitation, StoredInvitationInfo, TeamId) + ) findTeamInvitation Nothing _ = throwE RegisterErrorMissingIdentity findTeamInvitation (Just e) c = - lift (wrapClient $ Team.lookupInvitationInfo c) >>= \case - Just ii -> do - inv <- lift . wrapClient $ Team.lookupInvitation HideInvitationUrl (Team.iiTeam ii) (Team.iiInvId ii) - case (inv, Team.inInviteeEmail <$> inv) of + lift (liftSem $ InvitationCodeStore.lookupInvitationInfo c) >>= \case + Just invitationInfo -> do + inv <- lift . liftSem $ InvitationCodeStore.lookupInvitation invitationInfo.teamId invitationInfo.invitationId + case (inv, (.email) <$> inv) of (Just invite, Just em) | e == mkEmailKey em -> do - _ <- ensureMemberCanJoin (Team.iiTeam ii) - pure $ Just (invite, ii, Team.iiTeam ii) + ensureMemberCanJoin invitationInfo.teamId + pure $ Just (invite, invitationInfo, invitationInfo.teamId) _ -> throwE RegisterErrorInvalidInvitationCode Nothing -> throwE RegisterErrorInvalidInvitationCode @@ -414,37 +426,38 @@ createUser new = do acceptTeamInvitation :: UserAccount -> - Team.Invitation -> - Team.InvitationInfo -> + StoredInvitation -> + StoredInvitationInfo -> EmailKey -> UserIdentity -> ExceptT RegisterError (AppT r) () - acceptTeamInvitation account inv ii uk ident = do + acceptTeamInvitation account inv invitationInfo uk ident = do let uid = userId (accountUser account) ok <- lift $ liftSem $ claimKey uk uid unless ok $ throwE RegisterErrorUserKeyExists - let minvmeta :: (Maybe (UserId, UTCTimeMillis), Role) - minvmeta = ((,inCreatedAt inv) <$> inCreatedBy inv, Team.inRole inv) - added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid (Team.iiTeam ii) minvmeta + let minvmeta :: Maybe (UserId, UTCTimeMillis) + minvmeta = (,inv.createdAt) <$> inv.createdBy + role :: Role + role = fromMaybe defaultRole inv.role + added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid invitationInfo.teamId minvmeta role unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do wrapClient $ activateUser uid ident -- ('insertAccount' sets column activated to False; here it is set to True.) void $ onActivated (AccountActivated account) - liftSem $ + liftSem do Log.info $ field "user" (toByteString uid) - . field "team" (toByteString $ Team.iiTeam ii) + . field "team" (toByteString $ invitationInfo.teamId) . msg (val "Accepting invitation") - liftSem $ UserPendingActivationStore.remove uid - wrapClient $ do - Team.deleteInvitation (Team.inTeam inv) (Team.inInvitation inv) + UserPendingActivationStore.remove uid + InvitationCodeStore.deleteInvitation inv.teamId inv.invitationId addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> ExceptT RegisterError (AppT r) CreateUserTeam addUserToTeamSSO account tid ident = do let uid = userId (accountUser account) - added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid (Nothing, defaultRole) + added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid Nothing defaultRole unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do @@ -493,10 +506,10 @@ createUserInviteViaScim :: ) => NewUserScimInvitation -> ExceptT HttpError (AppT r) UserAccount -createUserInviteViaScim (NewUserScimInvitation tid uid eid loc name email _) = do +createUserInviteViaScim (NewUserScimInvitation tid uid extId loc name email _) = do let emKey = mkEmailKey email verifyUniquenessAndCheckBlacklist emKey !>> identityErrorToBrigError - account <- lift . wrapClient $ newAccountInviteViaScim uid eid tid loc name email + account <- lift . wrapClient $ newAccountInviteViaScim uid extId tid loc name email lift . liftSem . Log.debug $ field "user" (toByteString . userId . accountUser $ account) . field "action" (val "User.createUserInviteViaScim") -- add the expiry table entry first! (if brig creates an account, and then crashes before @@ -683,7 +696,9 @@ activate :: Member NotificationSubsystem r, Member (Input (Local ())) r, Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member (ConnectionStore InternalPaging) r, + Member PasswordResetCodeStore r, + Member UserSubsystem r ) => ActivationTarget -> ActivationCode -> @@ -699,6 +714,8 @@ activateWithCurrency :: Member NotificationSubsystem r, Member (Input (Local ())) r, Member (Input UTCTime) r, + Member PasswordResetCodeStore r, + Member UserSubsystem r, Member (ConnectionStore InternalPaging) r ) => ActivationTarget -> @@ -715,7 +732,7 @@ activateWithCurrency tgt code usr cur = do field "activation.key" (toByteString key) . field "activation.code" (toByteString code) . msg (val "Activating") - event <- wrapClientE $ Data.activateKey key code usr + event <- Data.activateKey key code usr case event of Nothing -> pure ActivationPass Just e -> do @@ -876,13 +893,14 @@ deleteSelfUser :: Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, Member VerificationCodeSubsystem r, + Member UserSubsystem r, Member PropertySubsystem r ) => - UserId -> + Local UserId -> Maybe PlainTextPassword6 -> ExceptT DeleteUserError (AppT r) (Maybe Timeout) -deleteSelfUser uid pwd = do - account <- lift . wrapClient $ Data.lookupAccount uid +deleteSelfUser luid@(tUnqualified -> uid) pwd = do + account <- lift . liftSem $ User.getAccountNoFilter luid case account of Nothing -> throwE DeleteUserInvalid Just a -> case accountStatus a of @@ -948,6 +966,7 @@ verifyDeleteUser :: Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, Member VerificationCodeSubsystem r, + Member UserSubsystem r, Member PropertySubsystem r ) => VerifyDeleteUser -> @@ -957,7 +976,8 @@ verifyDeleteUser d = do let code = verifyDeleteUserCode d c <- lift . liftSem $ verifyCode key VerificationCode.AccountDeletion code a <- maybe (throwE DeleteUserInvalidCode) pure (VerificationCode.codeAccount =<< c) - account <- lift . wrapClient $ Data.lookupAccount (Id a) + luid <- qualifyLocal $ Id a + account <- lift . liftSem $ User.getAccountNoFilter luid for_ account $ lift . liftSem . deleteAccount lift . liftSem $ deleteCode key VerificationCode.AccountDeletion @@ -975,12 +995,13 @@ ensureAccountDeleted :: Member (Input UTCTime) r, Member (ConnectionStore InternalPaging) r, Member UserStore r, + Member UserSubsystem r, Member PropertySubsystem r ) => - UserId -> + Local UserId -> AppT r DeleteUserResult -ensureAccountDeleted uid = do - mbAcc <- wrapClient $ lookupAccount uid +ensureAccountDeleted luid@(tUnqualified -> uid) = do + mbAcc <- liftSem $ User.getAccountNoFilter luid case mbAcc of Nothing -> pure NoUser Just acc -> do @@ -1108,10 +1129,15 @@ enqueueMultiDeleteCallsCounter = } getLegalHoldStatus :: - (Member GalleyAPIAccess r) => - UserId -> + ( Member GalleyAPIAccess r, + Member UserSubsystem r + ) => + Local UserId -> AppT r (Maybe UserLegalHoldStatus) -getLegalHoldStatus uid = traverse (liftSem . getLegalHoldStatus' . accountUser) =<< wrapHttpClient (lookupAccount uid) +getLegalHoldStatus uid = + liftSem $ + traverse (getLegalHoldStatus' . accountUser) + =<< User.getLocalAccountBy NoPendingInvitations uid getLegalHoldStatus' :: (Member GalleyAPIAccess r) => @@ -1124,32 +1150,6 @@ getLegalHoldStatus' user = teamMember <- GalleyAPIAccess.getTeamMember (userId user) tid pure $ maybe defUserLegalHoldStatus (^. legalHoldStatus) teamMember --- | Find user accounts for a given identity, both activated and those --- currently pending activation. -lookupExtendedAccountsByIdentity :: - (Member UserKeyStore r) => - EmailAddress -> - Bool -> - AppT r [ExtendedUserAccount] -lookupExtendedAccountsByIdentity email includePendingInvitations = do - let uk = mkEmailKey email - activeUid <- liftSem $ lookupKey uk - uidFromKey <- (>>= fst) <$> wrapClient (Data.lookupActivationCode uk) - result <- wrapClient $ Data.lookupExtendedAccounts (nub $ catMaybes [activeUid, uidFromKey]) - if includePendingInvitations - then pure result - else pure $ filter ((/= PendingInvitation) . accountStatus . account) result - --- | Find user accounts for a given identity, both activated and those --- currently pending activation. -lookupAccountsByIdentity :: - (Member UserKeyStore r) => - EmailAddress -> - Bool -> - AppT r [UserAccount] -lookupAccountsByIdentity email includePendingInvitations = - account <$$> lookupExtendedAccountsByIdentity email includePendingInvitations - isBlacklisted :: (Member BlockListStore r) => EmailAddress -> AppT r Bool isBlacklisted email = do let uk = mkEmailKey email diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 1df8edefd8e..882204e28d7 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -108,7 +108,7 @@ import Brig.User.Search.Index (IndexEnv (..), MonadIndexIO (..), runIndexIO) import Brig.User.Template import Brig.ZAuth (MonadZAuth (..), runZAuth) import Brig.ZAuth qualified as ZAuth -import Cassandra (MonadClient, runClient) +import Cassandra (runClient) import Cassandra qualified as Cas import Cassandra.Util (initCassandraForService) import Control.AutoUpdate @@ -621,13 +621,13 @@ instance HasRequestId (AppT r) where -- Ad hoc interpreters -- | similarly to `wrapClient`, this function serves as a crutch while Brig is being polysemised. -adhocUserKeyStoreInterpreter :: (MonadClient m, MonadReader Env m) => Sem '[UserKeyStore, UserStore, Embed IO] a -> m a +adhocUserKeyStoreInterpreter :: (MonadIO m, MonadReader Env m) => Sem '[UserKeyStore, UserStore, Embed IO] a -> m a adhocUserKeyStoreInterpreter action = do clientState <- asks (view casClient) liftIO $ runM . interpretUserStoreCassandra clientState . interpretUserKeyStoreCassandra clientState $ action -- | similarly to `wrapClient`, this function serves as a crutch while Brig is being polysemised. -adhocSessionStoreInterpreter :: (MonadClient m, MonadReader Env m) => Sem '[SessionStore, Embed IO] a -> m a +adhocSessionStoreInterpreter :: (MonadIO m, MonadReader Env m) => Sem '[SessionStore, Embed IO] a -> m a adhocSessionStoreInterpreter action = do clientState <- asks (view casClient) liftIO $ runM . interpretSessionStoreCassandra clientState $ action diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index ca597c1063a..88cbc80b02a 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -32,6 +32,8 @@ import Polysemy.TinyLog (TinyLog) import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Federation.Client qualified import Wire.API.Federation.Error +import Wire.ActivationCodeStore (ActivationCodeStore) +import Wire.ActivationCodeStore.Cassandra (interpretActivationCodeStoreToCassandra) import Wire.AuthenticationSubsystem import Wire.AuthenticationSubsystem.Interpreter import Wire.BlockListStore @@ -50,6 +52,8 @@ import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess.Rpc import Wire.GundeckAPIAccess import Wire.HashPassword +import Wire.InvitationCodeStore (InvitationCodeStore) +import Wire.InvitationCodeStore.Cassandra (interpretInvitationCodeStoreToCassandra) import Wire.NotificationSubsystem import Wire.NotificationSubsystem.Interpreter (defaultNotificationSubsystemConfig, runNotificationSubsystemGundeck) import Wire.ParseException @@ -107,6 +111,8 @@ type BrigCanonicalEffects = SessionStore, PasswordStore, VerificationCodeStore, + ActivationCodeStore, + InvitationCodeStore, PropertyStore, SFT, ConnectionStore InternalPaging, @@ -196,6 +202,8 @@ runBrigToIO e (AppT ma) = do . connectionStoreToCassandra . interpretSFT (e ^. httpManager) . interpretPropertyStoreCassandra (e ^. casClient) + . interpretInvitationCodeStoreToCassandra (e ^. casClient) + . interpretActivationCodeStoreToCassandra (e ^. casClient) . interpretVerificationCodeStoreCassandra (e ^. casClient) . interpretPasswordStore (e ^. casClient) . interpretSessionStoreCassandra (e ^. casClient) diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index c4f84d77022..25745846b69 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -29,9 +29,8 @@ module Brig.Data.Activation ) where -import Brig.App (Env, adhocUserKeyStoreInterpreter) +import Brig.App (AppT, adhocUserKeyStoreInterpreter, liftSem, qualifyLocal, wrapClient, wrapClientE) import Brig.Data.User -import Brig.Options import Brig.Types.Intra import Cassandra import Control.Error @@ -45,12 +44,15 @@ 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 -import Wire.PasswordResetCodeStore qualified as E -import Wire.PasswordResetCodeStore.Cassandra +import Wire.PasswordResetCodeStore (PasswordResetCodeStore) +import Wire.PasswordResetCodeStore qualified as Password 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 @@ -79,6 +81,7 @@ activationErrorToRegisterError = \case data ActivationEvent = AccountActivated !UserAccount | EmailActivated !UserId !EmailAddress + deriving (Show) -- | Max. number of activation attempts per 'ActivationKey'. maxAttempts :: Int32 @@ -86,24 +89,30 @@ maxAttempts = 3 -- docs/reference/user/activation.md {#RefActivationSubmit} activateKey :: - forall m. - (MonadClient m, MonadReader Env m) => + forall r. + ( Member UserSubsystem r, + Member PasswordResetCodeStore r + ) => ActivationKey -> ActivationCode -> Maybe UserId -> - ExceptT ActivationError m (Maybe ActivationEvent) -activateKey k c u = verifyCode k c >>= pickUser >>= activate + ExceptT ActivationError (AppT r) (Maybe ActivationEvent) +activateKey k c u = wrapClientE (verifyCode k c) >>= pickUser >>= activate where + pickUser :: (t, Maybe UserId) -> ExceptT ActivationError (AppT r) (t, UserId) pickUser (uk, u') = maybe (throwE invalidUser) (pure . (uk,)) (u <|> u') - activate (key :: EmailKey, uid) = do - a <- lift (lookupAccount uid) >>= maybe (throwE invalidUser) pure + + activate :: (EmailKey, UserId) -> ExceptT ActivationError (AppT r) (Maybe ActivationEvent) + activate (key, uid) = do + luid <- qualifyLocal uid + a <- lift (liftSem $ User.getAccountNoFilter luid) >>= maybe (throwE invalidUser) pure unless (accountStatus a == Active) $ -- this is never 'PendingActivation' in the flow this function is used in. throwE invalidCode case userIdentity (accountUser a) of Nothing -> do claim key uid let ident = EmailIdentity (emailKeyOrig key) - lift $ activateUser uid ident + wrapClientE (activateUser uid ident) let a' = a {accountUser = (accountUser a) {userIdentity = Just ident}} pure . Just $ AccountActivated a' Just _ -> do @@ -111,6 +120,13 @@ activateKey k c u = verifyCode k c >>= pickUser >>= activate profileNeedsUpdate = Just (emailKeyOrig key) /= userEmail usr oldKey :: Maybe EmailKey = mkEmailKey <$> userEmail usr in handleExistingIdentity uid profileNeedsUpdate oldKey key + + handleExistingIdentity :: + UserId -> + Bool -> + Maybe EmailKey -> + EmailKey -> + ExceptT ActivationError (AppT r) (Maybe ActivationEvent) handleExistingIdentity uid profileNeedsUpdate oldKey key | oldKey == Just key && not profileNeedsUpdate = pure Nothing -- activating existing key and exactly same profile @@ -120,15 +136,17 @@ activateKey k c u = verifyCode k c >>= pickUser >>= activate pure . Just $ EmailActivated uid (emailKeyOrig key) -- if the key is the same, we only want to update our profile | otherwise = do - lift (runM (passwordResetCodeStoreToCassandra @m @'[Embed m] (E.codeDelete (mkPasswordResetKey uid)))) + lift . liftSem $ Password.codeDelete (mkPasswordResetKey uid) claim key uid lift $ updateEmailAndDeleteEmailUnvalidated uid (emailKeyOrig key) for_ oldKey $ lift . adhocUserKeyStoreInterpreter . deleteKey pure . Just $ EmailActivated uid (emailKeyOrig key) where - updateEmailAndDeleteEmailUnvalidated :: UserId -> EmailAddress -> m () + updateEmailAndDeleteEmailUnvalidated :: UserId -> EmailAddress -> AppT r () updateEmailAndDeleteEmailUnvalidated u' email = - updateEmail u' email <* deleteEmailUnvalidated u' + wrapClient (updateEmail u' email <* deleteEmailUnvalidated u') + + claim :: EmailKey -> UserId -> ExceptT ActivationError (AppT r) () claim key uid = do ok <- lift $ adhocUserKeyStoreInterpreter (claimKey key uid) unless ok $ diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 4c0c2b3415c..9bae096a0f3 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -73,6 +73,7 @@ import Data.HashMap.Strict qualified as HashMap import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) import Data.Map qualified as Map +import Data.Qualified import Data.Set qualified as Set import Data.Text qualified as Text import Data.Time.Clock @@ -115,8 +116,10 @@ reAuthForNewClients :: ReAuthPolicy reAuthForNewClients count upsert = count > 0 && not upsert addClient :: - (MonadClient m, MonadReader Brig.App.Env m) => - UserId -> + ( MonadClient m, + MonadReader Brig.App.Env m + ) => + Local UserId -> ClientId -> NewClient -> Int -> @@ -125,26 +128,28 @@ addClient :: addClient = addClientWithReAuthPolicy reAuthForNewClients addClientWithReAuthPolicy :: - (MonadClient m, MonadReader Brig.App.Env m) => + ( MonadClient m, + MonadReader Brig.App.Env m + ) => ReAuthPolicy -> - UserId -> + Local UserId -> ClientId -> NewClient -> Int -> Maybe (Imports.Set ClientCapability) -> ExceptT ClientDataError m (Client, [Client], Word) addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients cps = do - clients <- lookupClients u + clients <- lookupClients (tUnqualified u) let typed = filter ((== newClientType c) . clientType) clients let count = length typed let upsert = any exists typed when (reAuthPolicy count upsert) $ fmapLT ClientReAuthError $ - User.reauthenticate u (newClientPassword c) + User.reauthenticate (tUnqualified u) (newClientPassword c) let capacity = fmap (+ (-count)) limit unless (maybe True (> 0) capacity || upsert) $ throwE TooManyClients - new <- insert + new <- insert (tUnqualified u) let !total = fromIntegral (length clients + if upsert then 0 else 1) let old = maybe (filter (not . exists) typed) (const []) limit pure (new, old, total) @@ -158,16 +163,16 @@ addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients cps = do exists :: Client -> Bool exists = (==) newId . clientId - insert :: (MonadClient m, MonadReader Brig.App.Env m) => ExceptT ClientDataError m Client - insert = do + insert :: (MonadClient m, MonadReader Brig.App.Env m) => UserId -> ExceptT ClientDataError m Client + insert uid = do -- Is it possible to do this somewhere else? Otherwise we could use `MonadClient` instead now <- toUTCTimeMillis <$> (liftIO =<< view currentTime) let keys = unpackLastPrekey (newClientLastKey c) : newClientPrekeys c - updatePrekeys u newId keys + updatePrekeys uid newId keys let mdl = newClientModel c - prm = (u, newId, now, newClientType c, newClientLabel c, newClientClass c, newClientCookie c, mdl, C.Set . Set.toList <$> cps) + prm = (uid, newId, now, newClientType c, newClientLabel c, newClientClass c, newClientCookie c, mdl, C.Set . Set.toList <$> cps) retry x5 $ write insertClient (params LocalQuorum prm) - addMLSPublicKeys u newId (Map.assocs (newClientMLSPublicKeys c)) + addMLSPublicKeys uid newId (Map.assocs (newClientMLSPublicKeys c)) pure $! Client { clientId = newId, diff --git a/services/brig/src/Brig/Data/MLS/KeyPackage.hs b/services/brig/src/Brig/Data/MLS/KeyPackage.hs index b5242afd6fc..f2950c27cac 100644 --- a/services/brig/src/Brig/Data/MLS/KeyPackage.hs +++ b/services/brig/src/Brig/Data/MLS/KeyPackage.hs @@ -26,7 +26,7 @@ where import Brig.API.MLS.KeyPackages.Validation import Brig.App -import Brig.Options hiding (Timeout) +import Brig.Options import Cassandra import Control.Arrow import Control.Error diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 14120bcd932..55412d7e069 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -32,9 +32,6 @@ module Brig.Data.User isSamlUser, -- * Lookups - lookupAccount, - lookupAccounts, - lookupExtendedAccounts, lookupUser, lookupUsers, lookupName, @@ -74,6 +71,7 @@ import Control.Lens hiding (from) import Data.Conduit (ConduitM) import Data.Domain import Data.Handle (Handle) +import Data.HavePendingInvitations import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) import Data.Misc @@ -203,7 +201,13 @@ authenticate u pw = -- | Password reauthentication. If the account has a password, reauthentication -- is mandatory. If the account has no password, or is an SSO user, and no password is given, -- reauthentication is a no-op. -reauthenticate :: (MonadClient m, MonadReader Env m) => UserId -> Maybe PlainTextPassword6 -> ExceptT ReAuthError m () +reauthenticate :: + ( MonadClient m, + MonadReader Env m + ) => + UserId -> + Maybe PlainTextPassword6 -> + ExceptT ReAuthError m () reauthenticate u pw = lift (lookupAuth u) >>= \case Nothing -> throwE (ReAuthError AuthInvalidUser) @@ -215,17 +219,18 @@ reauthenticate u pw = Just (Just pw', Ephemeral) -> maybeReAuth pw' where maybeReAuth pw' = case pw of - Nothing -> unlessM (isSamlUser u) $ throwE ReAuthMissingPassword + Nothing -> do + musr <- lookupUser NoPendingInvitations u + unless (maybe False isSamlUser musr) $ throwE ReAuthMissingPassword Just p -> unless (verifyPassword p pw') $ throwE (ReAuthError AuthInvalidCredentials) -isSamlUser :: (MonadClient m, MonadReader Env m) => UserId -> m Bool -isSamlUser uid = do - account <- lookupAccount uid - case userIdentity . accountUser =<< account of - Just (SSOIdentity (UserSSOId _) _) -> pure True - _ -> pure False +isSamlUser :: User -> Bool +isSamlUser usr = do + case usr.userIdentity of + Just (SSOIdentity (UserSSOId _) _) -> True + _ -> False insertAccount :: (MonadClient m) => @@ -391,18 +396,6 @@ lookupUsers hpi usrs = do domain <- viewFederationDomain toUsers domain loc hpi <$> retry x1 (query usersSelect (params LocalQuorum (Identity usrs))) -lookupAccount :: (MonadClient m, MonadReader Env m) => UserId -> m (Maybe UserAccount) -lookupAccount u = listToMaybe <$> lookupAccounts [u] - -lookupAccounts :: (MonadClient m, MonadReader Env m) => [UserId] -> m [UserAccount] -lookupAccounts usrs = account <$$> lookupExtendedAccounts usrs - -lookupExtendedAccounts :: (MonadClient m, MonadReader Env m) => [UserId] -> m [ExtendedUserAccount] -lookupExtendedAccounts usrs = do - loc <- setDefaultUserLocale <$> view settings - domain <- viewFederationDomain - fmap (toExtendedUserAccount domain loc) <$> retry x1 (query accountsSelect (params LocalQuorum (Identity usrs))) - lookupServiceUser :: (MonadClient m) => ProviderId -> ServiceId -> BotId -> m (Maybe (ConvId, Maybe TeamId)) lookupServiceUser pid sid bid = retry x1 (query1 cql (params LocalQuorum (pid, sid, bid))) where @@ -452,6 +445,8 @@ lookupFeatureConferenceCalling uid = do type Activated = Bool +-- UserRow is the same as AccountRow from the user subsystem. when migrating this code there, +-- consider eliminating it instead. type UserRow = ( UserId, Name, @@ -500,9 +495,6 @@ type UserRowInsert = deriving instance Show UserRowInsert --- Represents a 'UserAccount' -type AccountRow = UserRow - usersSelect :: PrepQuery R (Identity [UserId]) UserRow usersSelect = "SELECT id, name, text_status, picture, email, email_unvalidated, sso_id, accent_id, assets, \ @@ -528,13 +520,6 @@ richInfoSelectMulti = "SELECT user, json FROM rich_info WHERE user in ?" teamSelect :: PrepQuery R (Identity UserId) (Identity (Maybe TeamId)) teamSelect = "SELECT team FROM user WHERE id = ?" -accountsSelect :: PrepQuery R (Identity [UserId]) AccountRow -accountsSelect = - "SELECT id, name, text_status, picture, email, email_unvalidated, sso_id, accent_id, assets, \ - \activated, status, expires, language, country, provider, \ - \service, handle, team, managed_by, supported_protocols \ - \FROM user WHERE id IN ?" - userInsert :: PrepQuery W UserRowInsert () userInsert = "INSERT INTO user (id, name, text_status, picture, assets, email, sso_id, \ @@ -575,59 +560,6 @@ userRichInfoUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE r ------------------------------------------------------------------------------- -- Conversions --- | Construct a 'UserAccount' from a raw user record in the database. -toExtendedUserAccount :: Domain -> Locale -> AccountRow -> ExtendedUserAccount -toExtendedUserAccount - domain - defaultLocale - ( uid, - name, - textStatus, - pict, - email, - emailUnvalidated, - ssoid, - accent, - assets, - activated, - status, - expires, - lan, - con, - pid, - sid, - handle, - tid, - managed_by, - prots - ) = - let ident = toIdentity activated email ssoid - deleted = Just Deleted == status - expiration = if status == Just Ephemeral then expires else Nothing - loc = toLocale defaultLocale (lan, con) - svc = newServiceRef <$> sid <*> pid - account = - UserAccount - ( User - (Qualified uid domain) - ident - name - textStatus - (fromMaybe noPict pict) - (fromMaybe [] assets) - accent - deleted - loc - svc - handle - expiration - tid - (fromMaybe ManagedByWire managed_by) - (fromMaybe defSupportedProtocols prots) - ) - (fromMaybe Active status) - in ExtendedUserAccount account emailUnvalidated - toUsers :: Domain -> Locale -> HavePendingInvitations -> [UserRow] -> [User] toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp where @@ -641,7 +573,7 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp _textStatus, _pict, _email, - _, + _emailUnvalidated, _ssoid, _accent, _assets, @@ -666,7 +598,7 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp textStatus, pict, email, - _, + _emailUnvalidated, ssoid, accent, assets, diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 899381faa23..8fa8e91ac7e 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -44,6 +44,7 @@ import Wire.Sem.Delay import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore (UserStore) +import Wire.UserSubsystem -- | Handle an internal event. -- @@ -58,6 +59,7 @@ onEvent :: Member UserKeyStore r, Member (Input UTCTime) r, Member UserStore r, + Member UserSubsystem r, Member (ConnectionStore InternalPaging) r, Member PropertySubsystem r ) => @@ -71,7 +73,8 @@ onEvent n = handleTimeout $ case n of Log.info $ msg (val "Processing user delete event") ~~ field "user" (toByteString uid) - embed (API.lookupAccount uid) >>= mapM_ API.deleteAccount + luid <- qualifyLocal' uid + getAccountNoFilter luid >>= mapM_ API.deleteAccount -- As user deletions are expensive resource-wise in the context of -- bulk user deletions (e.g. during team deletions), -- wait 'delay' ms before processing the next event diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index f2c53d5d9bc..31d586cf165 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -41,16 +41,15 @@ import Data.Misc (HttpsUrl) import Data.Nonce import Data.Range import Data.Schema -import Data.Scientific (toBoundedInteger) import Data.Text qualified as Text import Data.Text.Encoding qualified as Text -import Data.Time.Clock (DiffTime, NominalDiffTime, secondsToDiffTime) import Database.Bloodhound.Types qualified as ES import Imports import Network.AMQP.Extended import Network.DNS qualified as DNS import System.Logger.Extended (Level, LogFormat) import Util.Options +import Util.Timeout import Wire.API.Allowlists (AllowlistEmailDomains (..)) import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Version @@ -58,17 +57,6 @@ import Wire.API.Team.Feature import Wire.API.User import Wire.EmailSending.SMTP (SMTPConnType (..)) -newtype Timeout = Timeout - { timeoutDiff :: NominalDiffTime - } - deriving newtype (Eq, Enum, Ord, Num, Real, Fractional, RealFrac, Show) - -instance Read Timeout where - readsPrec i s = - case readsPrec i s of - [(x :: Int, s')] -> [(Timeout (fromIntegral x), s')] - _ -> [] - data ElasticSearchOpts = ElasticSearchOpts { -- | ElasticSearch URL url :: !ES.Server, @@ -825,16 +813,6 @@ defSrvDiscoveryIntervalSeconds = secondsToDiffTime 10 defSftListLength :: Range 1 100 Int defSftListLength = unsafeRange 5 -instance FromJSON Timeout where - parseJSON (Number n) = - let defaultV = 3600 - bounded = toBoundedInteger n :: Maybe Int64 - in pure $ - Timeout $ - fromIntegral @Int $ - maybe defaultV fromIntegral bounded - parseJSON v = A.typeMismatch "activationTimeout" v - instance FromJSON Settings where parseJSON = genericParseJSON customOptions where diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index bbd0e6f6940..6a36c4f5a1a 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -43,7 +43,6 @@ import Brig.Provider.DB qualified as DB import Brig.Provider.Email import Brig.Provider.RPC qualified as RPC import Brig.Team.Util -import Brig.Types.User import Brig.ZAuth qualified as ZAuth import Cassandra (MonadClient) import Control.Error (throwE) @@ -58,6 +57,7 @@ import Data.CommaSeparatedList (CommaSeparatedList (fromCommaSeparatedList)) import Data.Conduit (runConduit, (.|)) import Data.Conduit.List qualified as C import Data.Hashable (hash) +import Data.HavePendingInvitations import Data.Id import Data.LegalHold import Data.List qualified as List @@ -214,7 +214,14 @@ newAccount new = do lift $ sendActivationMail name email key val False pure $ Public.NewProviderResponse pid newPass -activateAccountKey :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => Code.Key -> Code.Value -> (Handler r) (Maybe Public.ProviderActivationResponse) +activateAccountKey :: + ( Member GalleyAPIAccess r, + Member EmailSending r, + Member VerificationCodeSubsystem r + ) => + Code.Key -> + Code.Value -> + (Handler r) (Maybe Public.ProviderActivationResponse) activateAccountKey key val = do guardSecondFactorDisabled Nothing c <- (lift . liftSem $ verifyCode key IdentityVerification val) >>= maybeInvalidCode @@ -678,7 +685,8 @@ addBot zuid zcon cid add = do -- if we want to protect bots against lh, 'addClient' cannot just send lh capability -- implicitly in the next line. pure $ FutureWork @'UnprotectedBot undefined - wrapClientE (User.addClient (botUserId bid) bcl newClt maxPermClients (Just $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent)) + lbid <- qualifyLocal (botUserId bid) + wrapClientE (User.addClient lbid bcl newClt maxPermClients (Just $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent)) !>> const (StdError $ badGatewayWith "MalformedPrekeys") -- Add the bot to the conversation diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 5f713dd5edb..eda8c9fe88f 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -65,6 +65,7 @@ import Servant qualified import System.Logger (msg, val, (.=), (~~)) import System.Logger.Class (MonadLogger, err) import Util.Options +import Util.Timeout import Wire.API.Routes.API import Wire.API.Routes.Internal.Brig qualified as IAPI import Wire.API.Routes.Public.Brig diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index a6ee0283375..a7e285ad822 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -32,11 +32,12 @@ import Brig.API.User (createUserInviteViaScim, fetchUserIdentity) import Brig.API.User qualified as API import Brig.API.Util (logEmail, logInvitationCode) import Brig.App +import Brig.App qualified as App import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Options (setMaxTeamSize, setTeamInvitationTimeout) -import Brig.Team.DB qualified as DB import Brig.Team.Email +import Brig.Team.Template import Brig.Team.Util (ensurePermissionToAddUser, ensurePermissions) import Brig.Types.Team (TeamSize) import Brig.User.Search.TeamSize qualified as TeamSize @@ -47,7 +48,10 @@ import Data.Id import Data.List1 qualified as List1 import Data.Qualified (Local) 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 Data.Time.Clock (UTCTime) import Data.Tuple.Extra import Imports hiding (head) @@ -55,9 +59,10 @@ import Network.Wai.Utilities hiding (code, message) import Polysemy import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) +import Polysemy.TinyLog qualified as Log import Servant hiding (Handler, JSON, addHeader) -import System.Logger.Class qualified as Log import System.Logger.Message as Log +import URI.ByteString (Absolute, URIRef, laxURIParserOptions, parseURI) import Util.Logging (logFunction, logTeam) import Wire.API.Error import Wire.API.Error.Brig qualified as E @@ -77,9 +82,12 @@ import Wire.API.User hiding (fromEmail) import Wire.API.User qualified as Public import Wire.BlockListStore import Wire.EmailSending (EmailSending) +import Wire.EmailSubsystem.Template import Wire.Error import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..)) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.InvitationCodeStore (InsertInvitation (..), InvitationCodeStore (..), PaginatedResult (..), StoredInvitation (..)) +import Wire.InvitationCodeStore qualified as Store import Wire.NotificationSubsystem import Wire.Sem.Concurrency import Wire.Sem.Paging.Cassandra (InternalPaging) @@ -90,7 +98,9 @@ servantAPI :: ( Member GalleyAPIAccess r, Member UserKeyStore r, Member UserSubsystem r, - Member EmailSending r + Member EmailSending r, + Member TinyLog r, + Member Store.InvitationCodeStore r ) => ServerT TeamsAPI (Handler r) servantAPI = @@ -110,10 +120,14 @@ teamSizePublic uid tid = do teamSize :: TeamId -> (Handler r) TeamSize teamSize t = lift $ TeamSize.teamSize t -getInvitationCode :: TeamId -> InvitationId -> (Handler r) FoundInvitationCode +getInvitationCode :: + (Member Store.InvitationCodeStore r) => + TeamId -> + InvitationId -> + (Handler r) FoundInvitationCode getInvitationCode t r = do - code <- lift . wrapClient $ DB.lookupInvitationCode t r - maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) (pure . FoundInvitationCode) code + inv <- lift . liftSem $ Store.lookupInvitation t r + maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) (pure . FoundInvitationCode . (.code)) inv data CreateInvitationInviter = CreateInvitationInviter { inviterUid :: UserId, @@ -125,7 +139,9 @@ createInvitation :: ( Member GalleyAPIAccess r, Member UserKeyStore r, Member UserSubsystem r, - Member EmailSending r + Member EmailSending r, + Member TinyLog r, + Member InvitationCodeStore r ) => UserId -> TeamId -> @@ -152,7 +168,7 @@ createInvitation uid tid body = do where loc :: Invitation -> InvitationLocation loc inv = - InvitationLocation $ "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' (inInvitation inv) + InvitationLocation $ "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' inv.invitationId createInvitationViaScim :: ( Member BlockListStore r, @@ -161,7 +177,8 @@ createInvitationViaScim :: Member (UserPendingActivationStore p) r, Member TinyLog r, Member EmailSending r, - Member UserSubsystem r + Member UserSubsystem r, + Member InvitationCodeStore r ) => TeamId -> NewUserScimInvitation -> @@ -189,20 +206,21 @@ createInvitationViaScim tid newUser@(NewUserScimInvitation _tid uid _eid loc nam createUserInviteViaScim newUser -logInvitationRequest :: (Msg -> Msg) -> (Handler r) (Invitation, InvitationCode) -> (Handler r) (Invitation, InvitationCode) +logInvitationRequest :: (Member TinyLog r) => (Msg -> Msg) -> (Handler r) (Invitation, InvitationCode) -> Handler r (Invitation, InvitationCode) logInvitationRequest context action = - flip mapExceptT action $ \action' -> do + flip mapExceptT action \action' -> do eith <- action' case eith of Left err' -> do - Log.warn $ - context - . Log.msg @Text - ( "Failed to create invitation, label: " - <> (LT.toStrict . errorLabel) err' - ) + liftSem $ + Log.warn $ + context + . Log.msg @Text + ( "Failed to create invitation, label: " + <> (LT.toStrict . errorLabel) err' + ) pure (Left err') - Right result@(_, code) -> do + Right result@(_, code) -> liftSem do Log.info $ (context . logInvitationCode code) . Log.msg @Text "Successfully created invitation" pure (Right result) @@ -210,7 +228,9 @@ createInvitation' :: ( Member UserSubsystem r, Member GalleyAPIAccess r, Member UserKeyStore r, - Member EmailSending r + Member EmailSending r, + Member TinyLog r, + Member InvitationCodeStore r ) => TeamId -> Maybe UserId -> @@ -220,7 +240,7 @@ createInvitation' :: Public.InvitationRequest -> Handler r (Public.Invitation, Public.InvitationCode) createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do - let email = (inviteeEmail body) + let email = body.inviteeEmail let uke = mkEmailKey email blacklistedEm <- lift $ liftSem $ isBlocked email when blacklistedEm $ @@ -230,69 +250,165 @@ createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do throwStd emailExists maxSize <- setMaxTeamSize <$> view settings - pending <- lift $ wrapClient $ DB.countInvitations tid + pending <- lift $ liftSem $ Store.countInvitations tid when (fromIntegral pending >= maxSize) $ throwStd (errorToWai @'E.TooManyTeamInvitations) showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid - lift $ do - iid <- maybe (liftIO DB.mkInvitationId) (pure . Id . toUUID) mUid - now <- liftIO =<< view currentTime - timeout <- setTeamInvitationTimeout <$> view settings - (newInv, code) <- - wrapClient $ - DB.insertInvitation - showInvitationUrl - iid - tid - inviteeRole - now - mbInviterUid - email - body.inviteeName - timeout - (newInv, code) <$ sendInvitationMail email tid fromEmail code body.locale - -deleteInvitation :: (Member GalleyAPIAccess r) => UserId -> TeamId -> InvitationId -> (Handler r) () + iid <- maybe (liftIO randomId) (pure . Id . toUUID) mUid + now <- liftIO =<< view currentTime + timeout <- setTeamInvitationTimeout <$> view settings + let insertInv = + MkInsertInvitation + { invitationId = iid, + teamId = tid, + role = inviteeRole, + createdAt = now, + createdBy = mbInviterUid, + inviteeEmail = email, + inviteeName = body.inviteeName + } + newInv <- + lift . liftSem $ + Store.insertInvitation + insertInv + timeout + lift $ sendInvitationMail email tid fromEmail newInv.code body.locale + inv <- toInvitation showInvitationUrl newInv + pure (inv, newInv.code) + +deleteInvitation :: + (Member GalleyAPIAccess r, Member InvitationCodeStore r) => + UserId -> + TeamId -> + InvitationId -> + (Handler r) () deleteInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] - lift $ wrapClient $ DB.deleteInvitation tid iid + lift . liftSem $ Store.deleteInvitation tid iid -listInvitations :: (Member GalleyAPIAccess r) => UserId -> TeamId -> Maybe InvitationId -> Maybe (Range 1 500 Int32) -> (Handler r) Public.InvitationList -listInvitations uid tid start mSize = do +listInvitations :: + ( Member GalleyAPIAccess r, + Member TinyLog r, + Member InvitationCodeStore r + ) => + UserId -> + TeamId -> + Maybe InvitationId -> + Maybe (Range 1 500 Int32) -> + (Handler r) Public.InvitationList +listInvitations uid tid startingId mSize = do ensurePermissions uid tid [AddTeamMember] showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid - rs <- lift $ wrapClient $ DB.lookupInvitations showInvitationUrl tid start (fromMaybe (unsafeRange 100) mSize) - pure $! Public.InvitationList (DB.resultList rs) (DB.resultHasMore rs) + let toInvitations is = mapM (toInvitation showInvitationUrl) is + lift (liftSem $ Store.lookupInvitationsPaginated mSize tid startingId) >>= \case + PaginatedResultHasMore storedInvs -> do + invs <- toInvitations storedInvs + pure $ InvitationList invs True + PaginatedResult storedInvs -> do + invs <- toInvitations storedInvs + pure $ InvitationList invs False + +-- | brig used to not store the role, so for migration we allow this to be empty and fill in the +-- default here. +toInvitation :: + ( Member TinyLog r + ) => + ShowOrHideInvitationUrl -> + StoredInvitation -> + (Handler r) Invitation +toInvitation showUrl storedInv = do + url <- 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 + } -getInvitation :: (Member GalleyAPIAccess r) => UserId -> TeamId -> InvitationId -> (Handler r) (Maybe Public.Invitation) +mkInviteUrl :: + (Member TinyLog r) => + ShowOrHideInvitationUrl -> + TeamId -> + InvitationCode -> + (Handler r) (Maybe (URIRef Absolute)) +mkInviteUrl HideInvitationUrl _ _ = pure Nothing +mkInviteUrl ShowInvitationUrl team (InvitationCode c) = do + template <- invitationEmailUrl . invitationEmail . snd <$> teamTemplates Nothing + branding <- view App.templateBranding + let url = Text.toStrict $ renderTextWithBranding template replace branding + parseHttpsUrl url + where + replace "team" = idToText team + replace "code" = toText c + replace x = x + parseHttpsUrl :: (Member TinyLog r) => Text -> (Handler r) (Maybe (URIRef Absolute)) + parseHttpsUrl url = + either (\e -> lift . liftSem $ logError url e >> pure Nothing) (pure . Just) $ + parseURI laxURIParserOptions (encodeUtf8 url) + logError url e = + Log.err $ + Log.msg @Text "Unable to create invitation url. Please check configuration." + . Log.field "url" url + . Log.field "error" (show e) + +getInvitation :: + ( Member GalleyAPIAccess r, + Member InvitationCodeStore r, + Member TinyLog r + ) => + UserId -> + TeamId -> + InvitationId -> + (Handler r) (Maybe Public.Invitation) getInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] - showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid - lift $ wrapClient $ DB.lookupInvitation showInvitationUrl tid iid -getInvitationByCode :: Public.InvitationCode -> (Handler r) Public.Invitation + invitationM <- lift . liftSem $ Store.lookupInvitation tid iid + case invitationM of + Nothing -> pure Nothing + Just invitation -> do + showInvitationUrl <- lift . liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid + maybeUrl <- mkInviteUrl showInvitationUrl tid invitation.code + pure $ Just (Store.invitationFromStored maybeUrl invitation) + +getInvitationByCode :: + (Member Store.InvitationCodeStore r) => + Public.InvitationCode -> + (Handler r) Public.Invitation getInvitationByCode c = do - inv <- lift . wrapClient $ DB.lookupInvitationByCode HideInvitationUrl c - maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) pure inv + inv <- lift . liftSem $ Store.lookupInvitationByCode c + maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) (pure . Store.invitationFromStored Nothing) inv -headInvitationByEmail :: EmailAddress -> (Handler r) Public.HeadInvitationByEmailResult -headInvitationByEmail e = do +headInvitationByEmail :: (Member InvitationCodeStore r, Member TinyLog r) => EmailAddress -> (Handler r) Public.HeadInvitationByEmailResult +headInvitationByEmail email = lift $ - wrapClient $ - DB.lookupInvitationInfoByEmail e <&> \case - DB.InvitationByEmail _ -> Public.InvitationByEmail - DB.InvitationByEmailNotFound -> Public.InvitationByEmailNotFound - DB.InvitationByEmailMoreThanOne -> Public.InvitationByEmailMoreThanOne + liftSem $ + Store.lookupInvitationCodesByEmail email >>= \case + [] -> pure Public.InvitationByEmailNotFound + [_code] -> pure Public.InvitationByEmail + (_ : _ : _) -> do + Log.info $ + Log.msg (Log.val "team_invidation_email: multiple pending invites from different teams for the same email") + . Log.field "email" (show email) + pure Public.InvitationByEmailMoreThanOne -- | FUTUREWORK: This should also respond with status 409 in case of -- @DB.InvitationByEmailMoreThanOne@. Refactor so that 'headInvitationByEmailH' and -- 'getInvitationByEmailH' are almost the same thing. -getInvitationByEmail :: EmailAddress -> (Handler r) Public.Invitation +getInvitationByEmail :: + (Member Store.InvitationCodeStore r, Member TinyLog r) => + EmailAddress -> + (Handler r) Public.Invitation getInvitationByEmail email = do - inv <- lift $ wrapClient $ DB.lookupInvitationByEmail HideInvitationUrl email - maybe (throwStd (notFound "Invitation not found")) pure inv + inv <- lift . liftSem $ Store.lookupInvitationByEmail email + maybe (throwStd (notFound "Invitation not found")) (pure . Store.invitationFromStored Nothing) inv suspendTeam :: ( Member (Embed HttpClientIO) r, @@ -302,15 +418,19 @@ suspendTeam :: Member TinyLog r, Member (Input (Local ())) r, Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member (ConnectionStore InternalPaging) r, + Member InvitationCodeStore r ) => TeamId -> (Handler r) NoContent suspendTeam tid = do - Log.info $ Log.msg (Log.val "Team suspended") ~~ Log.field "team" (toByteString tid) + lift $ liftSem $ Log.info $ Log.msg (Log.val "Team suspended") ~~ Log.field "team" (toByteString tid) + -- Update the status of all users from the given team changeTeamAccountStatuses tid Suspended - lift $ wrapClient $ DB.deleteInvitations tid - lift $ liftSem $ GalleyAPIAccess.changeTeamStatus tid Team.Suspended Nothing + lift . liftSem $ do + Store.deleteAllTeamInvitations tid + -- RPC to galley to change team status there + GalleyAPIAccess.changeTeamStatus tid Team.Suspended Nothing pure NoContent unsuspendTeam :: diff --git a/services/brig/src/Brig/Team/DB.hs b/services/brig/src/Brig/Team/DB.hs deleted file mode 100644 index e6e19e7609d..00000000000 --- a/services/brig/src/Brig/Team/DB.hs +++ /dev/null @@ -1,323 +0,0 @@ -{-# LANGUAGE RecordWildCards #-} - --- 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 Brig.Team.DB - ( module T, - countInvitations, - insertInvitation, - deleteInvitation, - deleteInvitations, - lookupInvitation, - lookupInvitationCode, - lookupInvitations, - lookupInvitationByCode, - lookupInvitationInfo, - lookupInvitationInfoByEmail, - lookupInvitationByEmail, - mkInvitationCode, - mkInvitationId, - InvitationByEmail (..), - InvitationInfo (..), - ) -where - -import Brig.App as App -import Brig.Data.Types as T -import Brig.Options -import Brig.Team.Template -import Cassandra as C -import Control.Lens (view) -import Data.Conduit (runConduit, (.|)) -import Data.Conduit.List qualified as C -import Data.Id -import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) -import Data.Range -import Data.Text.Ascii (encodeBase64Url, toText) -import Data.Text.Encoding -import Data.Text.Lazy (toStrict) -import Data.Time.Clock -import Data.UUID.V4 -import Imports -import OpenSSL.Random (randBytes) -import System.Logger.Class qualified as Log -import URI.ByteString -import UnliftIO.Async (pooledMapConcurrentlyN_) -import Wire.API.Team.Invitation hiding (HeadInvitationByEmailResult (..)) -import Wire.API.Team.Role -import Wire.API.User -import Wire.EmailSubsystem.Template (renderTextWithBranding) -import Wire.GalleyAPIAccess (ShowOrHideInvitationUrl (..)) - -mkInvitationCode :: IO InvitationCode -mkInvitationCode = InvitationCode . encodeBase64Url <$> randBytes 24 - -mkInvitationId :: IO InvitationId -mkInvitationId = Id <$> nextRandom - -data InvitationInfo = InvitationInfo - { iiCode :: InvitationCode, - iiTeam :: TeamId, - iiInvId :: InvitationId - } - deriving (Eq, Show) - -data InvitationByEmail - = InvitationByEmail InvitationInfo - | InvitationByEmailNotFound - | InvitationByEmailMoreThanOne - -insertInvitation :: - ( Log.MonadLogger m, - MonadReader Env m, - MonadClient m - ) => - ShowOrHideInvitationUrl -> - InvitationId -> - TeamId -> - Role -> - UTCTime -> - Maybe UserId -> - EmailAddress -> - Maybe Name -> - -- | The timeout for the invitation code. - Timeout -> - m (Invitation, InvitationCode) -insertInvitation showUrl iid t role (toUTCTimeMillis -> now) minviter email inviteeName timeout = do - code <- liftIO mkInvitationCode - url <- mkInviteUrl showUrl t code - let inv = Invitation t role iid now minviter email inviteeName url - retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery cqlInvitation (t, role, iid, code, email, now, minviter, inviteeName, round timeout) - addPrepQuery cqlInvitationInfo (code, t, iid, round timeout) - addPrepQuery cqlInvitationByEmail (email, t, iid, code, round timeout) - pure (inv, code) - where - cqlInvitationInfo :: PrepQuery W (InvitationCode, TeamId, InvitationId, Int32) () - cqlInvitationInfo = "INSERT INTO team_invitation_info (code, team, id) VALUES (?, ?, ?) USING TTL ?" - cqlInvitation :: PrepQuery W (TeamId, Role, InvitationId, InvitationCode, EmailAddress, UTCTimeMillis, Maybe UserId, Maybe Name, Int32) () - cqlInvitation = "INSERT INTO team_invitation (team, role, id, code, email, created_at, created_by, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?) USING TTL ?" - -- Note: the edge case of multiple invites to the same team by different admins from the same team results in last-invite-wins in the team_invitation_email table. - cqlInvitationByEmail :: PrepQuery W (EmailAddress, TeamId, InvitationId, InvitationCode, Int32) () - cqlInvitationByEmail = "INSERT INTO team_invitation_email (email, team, invitation, code) VALUES (?, ?, ?, ?) USING TTL ?" - -lookupInvitation :: - ( MonadClient m, - MonadReader Env m, - Log.MonadLogger m - ) => - ShowOrHideInvitationUrl -> - TeamId -> - InvitationId -> - m (Maybe Invitation) -lookupInvitation showUrl t r = do - inv <- retry x1 (query1 cqlInvitation (params LocalQuorum (t, r))) - traverse (toInvitation showUrl) inv - where - cqlInvitation :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode) - cqlInvitation = "SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id = ?" - -lookupInvitationByCode :: - ( Log.MonadLogger m, - MonadReader Env m, - MonadClient m - ) => - ShowOrHideInvitationUrl -> - InvitationCode -> - m (Maybe Invitation) -lookupInvitationByCode showUrl i = - lookupInvitationInfo i >>= \case - Just InvitationInfo {..} -> lookupInvitation showUrl iiTeam iiInvId - _ -> pure Nothing - -lookupInvitationCode :: (MonadClient m) => TeamId -> InvitationId -> m (Maybe InvitationCode) -lookupInvitationCode t r = - fmap runIdentity - <$> retry x1 (query1 cqlInvitationCode (params LocalQuorum (t, r))) - where - cqlInvitationCode :: PrepQuery R (TeamId, InvitationId) (Identity InvitationCode) - cqlInvitationCode = "SELECT code FROM team_invitation WHERE team = ? AND id = ?" - -lookupInvitationCodeEmail :: (MonadClient m) => TeamId -> InvitationId -> m (Maybe (InvitationCode, EmailAddress)) -lookupInvitationCodeEmail t r = retry x1 (query1 cqlInvitationCodeEmail (params LocalQuorum (t, r))) - where - cqlInvitationCodeEmail :: PrepQuery R (TeamId, InvitationId) (InvitationCode, EmailAddress) - cqlInvitationCodeEmail = "SELECT code, email FROM team_invitation WHERE team = ? AND id = ?" - -lookupInvitations :: - ( Log.MonadLogger m, - MonadReader Env m, - MonadClient m - ) => - ShowOrHideInvitationUrl -> - TeamId -> - Maybe InvitationId -> - Range 1 500 Int32 -> - m (ResultPage Invitation) -lookupInvitations showUrl team start (fromRange -> size) = do - page <- case start of - Just ref -> retry x1 $ paginate cqlSelectFrom (paramsP LocalQuorum (team, ref) (size + 1)) - Nothing -> retry x1 $ paginate cqlSelect (paramsP LocalQuorum (Identity team) (size + 1)) - toResult (hasMore page) <$> traverse (toInvitation showUrl) (trim page) - where - trim p = take (fromIntegral size) (result p) - toResult more invs = - cassandraResultPage $ - emptyPage - { result = invs, - hasMore = more - } - cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode) - cqlSelect = "SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? ORDER BY id ASC" - cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, EmailAddress, Maybe Name, InvitationCode) - cqlSelectFrom = "SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id > ? ORDER BY id ASC" - -deleteInvitation :: (MonadClient m) => TeamId -> InvitationId -> m () -deleteInvitation t i = do - codeEmail <- lookupInvitationCodeEmail t i - case codeEmail of - Just (invCode, invEmail) -> retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery cqlInvitation (t, i) - addPrepQuery cqlInvitationInfo (Identity invCode) - addPrepQuery cqlInvitationEmail (invEmail, t) - Nothing -> - retry x5 $ write cqlInvitation (params LocalQuorum (t, i)) - where - cqlInvitation :: PrepQuery W (TeamId, InvitationId) () - cqlInvitation = "DELETE FROM team_invitation where team = ? AND id = ?" - cqlInvitationInfo :: PrepQuery W (Identity InvitationCode) () - cqlInvitationInfo = "DELETE FROM team_invitation_info WHERE code = ?" - cqlInvitationEmail :: PrepQuery W (EmailAddress, TeamId) () - cqlInvitationEmail = "DELETE FROM team_invitation_email WHERE email = ? AND team = ?" - -deleteInvitations :: (MonadClient m) => TeamId -> m () -deleteInvitations t = - liftClient $ - runConduit $ - paginateC cqlSelect (paramsP LocalQuorum (Identity t) 100) x1 - .| C.mapM_ (pooledMapConcurrentlyN_ 16 (deleteInvitation t . runIdentity)) - where - cqlSelect :: PrepQuery R (Identity TeamId) (Identity InvitationId) - cqlSelect = "SELECT id FROM team_invitation WHERE team = ? ORDER BY id ASC" - -lookupInvitationInfo :: (MonadClient m) => InvitationCode -> m (Maybe InvitationInfo) -lookupInvitationInfo ic@(InvitationCode c) - | c == mempty = pure Nothing - | otherwise = - fmap (toInvitationInfo ic) - <$> retry x1 (query1 cqlInvitationInfo (params LocalQuorum (Identity ic))) - where - toInvitationInfo i (t, r) = InvitationInfo i t r - cqlInvitationInfo :: PrepQuery R (Identity InvitationCode) (TeamId, InvitationId) - cqlInvitationInfo = "SELECT team, id FROM team_invitation_info WHERE code = ?" - -lookupInvitationByEmail :: - ( Log.MonadLogger m, - MonadReader Env m, - MonadClient m - ) => - ShowOrHideInvitationUrl -> - EmailAddress -> - m (Maybe Invitation) -lookupInvitationByEmail showUrl e = - lookupInvitationInfoByEmail e >>= \case - InvitationByEmail InvitationInfo {..} -> lookupInvitation showUrl iiTeam iiInvId - _ -> pure Nothing - -lookupInvitationInfoByEmail :: (Log.MonadLogger m, MonadClient m) => EmailAddress -> m InvitationByEmail -lookupInvitationInfoByEmail email = do - res <- retry x1 (query cqlInvitationEmail (params LocalQuorum (Identity email))) - case res of - [] -> pure InvitationByEmailNotFound - [(tid, invId, code)] -> - -- one invite pending - pure $ InvitationByEmail (InvitationInfo code tid invId) - _ : _ : _ -> do - -- edge case: more than one pending invite from different teams - Log.info $ - Log.msg (Log.val "team_invidation_email: multiple pending invites from different teams for the same email") - Log.~~ Log.field "email" (show email) - pure InvitationByEmailMoreThanOne - where - cqlInvitationEmail :: PrepQuery R (Identity EmailAddress) (TeamId, InvitationId, InvitationCode) - cqlInvitationEmail = "SELECT team, invitation, code FROM team_invitation_email WHERE email = ?" - -countInvitations :: (MonadClient m) => TeamId -> m Int64 -countInvitations t = - maybe 0 runIdentity - <$> retry x1 (query1 cqlSelect (params LocalQuorum (Identity t))) - where - cqlSelect :: PrepQuery R (Identity TeamId) (Identity Int64) - cqlSelect = "SELECT count(*) FROM team_invitation WHERE team = ?" - --- | brig used to not store the role, so for migration we allow this to be empty and fill in the --- default here. -toInvitation :: - ( MonadReader Env m, - Log.MonadLogger m - ) => - ShowOrHideInvitationUrl -> - ( TeamId, - Maybe Role, - InvitationId, - UTCTimeMillis, - Maybe UserId, - EmailAddress, - Maybe Name, - InvitationCode - ) -> - m Invitation -toInvitation showUrl (t, r, i, tm, minviter, e, inviteeName, code) = do - url <- mkInviteUrl showUrl t code - pure $ Invitation t (fromMaybe defaultRole r) i tm minviter e inviteeName url - -mkInviteUrl :: - ( MonadReader Env m, - Log.MonadLogger m - ) => - ShowOrHideInvitationUrl -> - TeamId -> - InvitationCode -> - m (Maybe (URIRef Absolute)) -mkInviteUrl HideInvitationUrl _ _ = pure Nothing -mkInviteUrl ShowInvitationUrl team (InvitationCode c) = do - template <- invitationEmailUrl . invitationEmail . snd <$> teamTemplates Nothing - branding <- view App.templateBranding - let url = toStrict $ renderTextWithBranding template replace branding - parseHttpsUrl url - where - replace "team" = idToText team - replace "code" = toText c - replace x = x - - parseHttpsUrl :: (Log.MonadLogger m) => Text -> m (Maybe (URIRef Absolute)) - parseHttpsUrl url = - either (\e -> logError url e >> pure Nothing) (pure . Just) $ - parseURI laxURIParserOptions (encodeUtf8 url) - - logError :: (Log.MonadLogger m, Show e) => Text -> e -> m () - logError url e = - Log.err $ - Log.msg - (Log.val "Unable to create invitation url. Please check configuration.") - . Log.field "url" url - . Log.field "error" (show e) diff --git a/services/brig/src/Brig/Team/Util.hs b/services/brig/src/Brig/Team/Util.hs index 6ab5eab896d..a838a3c5fe8 100644 --- a/services/brig/src/Brig/Team/Util.hs +++ b/services/brig/src/Brig/Team/Util.hs @@ -20,9 +20,9 @@ module Brig.Team.Util where -- TODO: remove this module and move contents to Bri import Brig.API.Error import Brig.App import Brig.Data.User qualified as Data -import Brig.Types.User (HavePendingInvitations (NoPendingInvitations)) import Control.Error import Control.Lens +import Data.HavePendingInvitations import Data.Id import Data.Set qualified as Set import Imports diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index b17ff8e6689..03b4fd7895a 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -58,8 +58,7 @@ import Data.List.NonEmpty qualified as NE import Data.List1 (List1) import Data.List1 qualified as List1 import Data.Misc (PlainTextPassword6) -import Data.Qualified (Local) -import Data.Time.Clock (UTCTime) +import Data.Qualified import Data.ZAuth.Token qualified as ZAuth import Imports import Network.Wai.Utilities.Error ((!>>)) @@ -68,6 +67,7 @@ import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log import System.Logger (field, msg, val, (~~)) +import Util.Timeout import Wire.API.Team.Feature import Wire.API.Team.Feature qualified as Public import Wire.API.User @@ -81,6 +81,8 @@ import Wire.PasswordStore (PasswordStore) import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore +import Wire.UserSubsystem (UserSubsystem) +import Wire.UserSubsystem qualified as User import Wire.VerificationCode qualified as VerificationCode import Wire.VerificationCodeGen qualified as VerificationCodeGen import Wire.VerificationCodeSubsystem (VerificationCodeSubsystem) @@ -98,6 +100,7 @@ login :: Member PasswordStore r, Member UserKeyStore r, Member UserStore r, + Member UserSubsystem r, Member VerificationCodeSubsystem r ) => Login -> @@ -117,8 +120,9 @@ login (MkLogin li pw label code) typ = do newAccess @ZAuth.User @ZAuth.Access uid Nothing typ label where verifyLoginCode :: Maybe Code.Value -> UserId -> ExceptT LoginError (AppT r) () - verifyLoginCode mbCode uid = - verifyCode mbCode Login uid + verifyLoginCode mbCode uid = do + luid <- lift $ qualifyLocal uid + verifyCode mbCode Login luid `catchE` \case VerificationCodeNoPendingCode -> wrapHttpClientE $ loginFailedWith LoginCodeInvalid uid VerificationCodeRequired -> wrapHttpClientE $ loginFailedWith LoginCodeRequired uid @@ -126,17 +130,18 @@ login (MkLogin li pw label code) typ = do verifyCode :: forall r. - (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => + (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r, Member UserSubsystem r) => Maybe Code.Value -> VerificationAction -> - UserId -> + Local UserId -> ExceptT VerificationCodeError (AppT r) () -verifyCode mbCode action uid = do - (mbEmail, mbTeamId) <- getEmailAndTeamId uid +verifyCode mbCode action luid = do + (mbEmail, mbTeamId) <- getEmailAndTeamId luid featureEnabled <- lift $ do mbFeatureEnabled <- liftSem $ GalleyAPIAccess.getVerificationCodeEnabled `traverse` mbTeamId pure $ fromMaybe ((def @(Feature Public.SndFactorPasswordChallengeConfig)).status == Public.FeatureStatusEnabled) mbFeatureEnabled - isSsoUser <- wrapHttpClientE $ Data.isSamlUser uid + account <- lift . liftSem $ User.getAccountNoFilter luid + let isSsoUser = maybe False (Data.isSamlUser . ((.accountUser))) account when (featureEnabled && not isSsoUser) $ do case (mbCode, mbEmail) of (Just code, Just email) -> do @@ -148,10 +153,10 @@ verifyCode mbCode action uid = do (_, Nothing) -> throwE VerificationCodeNoEmail where getEmailAndTeamId :: - UserId -> + Local UserId -> ExceptT e (AppT r) (Maybe EmailAddress, Maybe TeamId) getEmailAndTeamId u = do - mbAccount <- wrapHttpClientE $ Data.lookupAccount u + mbAccount <- lift . liftSem $ User.getAccountNoFilter u pure (userEmail <$> accountUser =<< mbAccount, userTeam <$> accountUser =<< mbAccount) loginFailedWith :: (MonadClient m, MonadReader Env m) => LoginError -> UserId -> ExceptT LoginError m () @@ -177,7 +182,7 @@ withRetryLimit action uid = do let bkey = BudgetKey ("login#" <> idToText uid) budget = Budget - (Opt.timeoutDiff $ Opt.timeout opts) + (timeoutDiff $ Opt.timeout opts) (fromIntegral $ Opt.retryLimit opts) bresult <- action bkey budget case bresult of @@ -217,15 +222,21 @@ renewAccess uts at mcid = do pure $ Access at' ck' revokeAccess :: - (Member TinyLog r, Member PasswordStore r) => - UserId -> + ( Member TinyLog r, + Member PasswordStore r, + Member UserSubsystem r + ) => + Local UserId -> PlainTextPassword6 -> [CookieId] -> [CookieLabel] -> ExceptT AuthError (AppT r) () -revokeAccess u pw cc ll = do +revokeAccess luid@(tUnqualified -> u) pw cc ll = do lift . liftSem $ Log.debug $ field "user" (toByteString u) . field "action" (val "User.revokeAccess") - unlessM (lift . wrapHttpClient $ Data.isSamlUser u) $ Data.authenticate u pw + isSaml <- lift . liftSem $ do + account <- User.getAccountNoFilter luid + pure $ maybe False (Data.isSamlUser . ((.accountUser))) account + unless isSaml $ Data.authenticate u pw lift $ wrapHttpClient $ revokeCookies u cc ll -------------------------------------------------------------------------------- @@ -282,32 +293,48 @@ newAccess uid cid ct cl = do t <- lift $ newAccessToken @u @a ck Nothing pure $ Access t (Just ck) -resolveLoginId :: (Member UserKeyStore r, Member UserStore r) => LoginId -> ExceptT LoginError (AppT r) UserId +resolveLoginId :: + ( Member UserKeyStore r, + Member UserStore r, + Member UserSubsystem r, + Member (Input (Local ())) r + ) => + LoginId -> + ExceptT LoginError (AppT r) UserId resolveLoginId li = do - usr <- wrapClientE (validateLoginId li) >>= lift . either (liftSem . lookupKey) (liftSem . lookupHandle) + usr <- lift . liftSem . either lookupKey lookupHandle $ validateLoginId li case usr of Nothing -> do - pending <- wrapClientE $ isPendingActivation li + pending <- lift $ isPendingActivation li throwE $ if pending then LoginPendingActivation else LoginFailed Just uid -> pure uid -validateLoginId :: (MonadReader Env m) => LoginId -> ExceptT LoginError m (Either EmailKey Handle) -validateLoginId (LoginByEmail email) = (pure . Left . mkEmailKey) email -validateLoginId (LoginByHandle h) = (pure . Right) h +validateLoginId :: LoginId -> Either EmailKey Handle +validateLoginId (LoginByEmail email) = (Left . mkEmailKey) email +validateLoginId (LoginByHandle h) = Right h -isPendingActivation :: (MonadClient m, MonadReader Env m) => LoginId -> m Bool +isPendingActivation :: + forall r. + (Member UserSubsystem r, Member (Input (Local ())) r) => + LoginId -> + AppT r Bool isPendingActivation ident = case ident of (LoginByHandle _) -> pure False (LoginByEmail e) -> checkKey (mkEmailKey e) where + checkKey :: EmailKey -> AppT r Bool checkKey k = do - usr <- (>>= fst) <$> Data.lookupActivationCode k - case usr of + musr <- (>>= fst) <$> wrapClient (Data.lookupActivationCode k) + case musr of Nothing -> pure False - Just u -> maybe False (checkAccount k) <$> Data.lookupAccount u + Just usr -> liftSem do + lusr <- qualifyLocal' usr + maybe False (checkAccount k) <$> User.getAccountNoFilter lusr + + checkAccount :: EmailKey -> UserAccount -> Bool checkAccount k a = let i = userIdentity (accountUser a) statusAdmitsPending = case accountStatus a of diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index 23ed4c461bf..f9f621ae4bb 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -57,6 +57,7 @@ import Imports import Prometheus qualified as Prom import System.Logger.Class (field, msg, val, (~~)) import System.Logger.Class qualified as Log +import Util.Timeout import Web.Cookie qualified as WebCookie import Wire.API.User.Auth import Wire.SessionStore qualified as Store diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index 880fc7d4618..23d4095270f 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -32,6 +32,7 @@ import Control.Lens (view, (^.)) import Data.Aeson qualified as A import Data.ByteString.Conversion import Data.Handle (Handle) +import Data.HavePendingInvitations import Data.Qualified import Data.Set qualified as Set import Data.Text qualified as T diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 769efcd6a00..daac2f2e6eb 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -44,7 +44,6 @@ import Data.String.Conversions (cs) import Data.Text qualified as Text import Data.Text.Ascii qualified as Ascii import Data.Text.Encoding (decodeUtf8, encodeUtf8) -import Data.Time (addUTCTime, getCurrentTime) import Data.UUID qualified as UUID (fromString) import Data.UUID.V4 qualified as UUID import Imports @@ -60,6 +59,7 @@ import URI.ByteString import UnliftIO.Async (mapConcurrently_, pooledForConcurrentlyN_, replicateConcurrently) import Util import Util.AWS as Util +import Util.Timeout import Web.Cookie (parseSetCookie, setCookieName) import Wire.API.Asset import Wire.API.Connection @@ -168,8 +168,8 @@ testUpdateEvents brig cannon = do inviteeEmail <- randomEmail -- invite and register Bob let invite = stdInvitationRequest inviteeEmail - inv <- responseJsonError =<< postInvitation brig tid alice invite - Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) + inv :: Invitation <- responseJsonError =<< postInvitation brig tid alice invite + Just inviteeCode <- getInvitationCode brig tid inv.invitationId rsp2 <- post ( brig @@ -204,34 +204,34 @@ testInvitationEmail brig = do const 201 === statusCode inv <- responseJsonError res let actualHeader = getHeader "Location" res - let expectedHeader = "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' (inInvitation inv) + let expectedHeader = "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' inv.invitationId liftIO $ do - Just inviter @=? inCreatedBy inv - tid @=? inTeam inv + Just inviter @=? inv.createdBy + tid @=? inv.team assertInvitationResponseInvariants invite inv - (isNothing . inInviteeUrl) inv @? "No invitation url expected" + (isNothing . (.inviteeUrl)) inv @? "No invitation url expected" actualHeader @?= Just expectedHeader assertInvitationResponseInvariants :: InvitationRequest -> Invitation -> Assertion assertInvitationResponseInvariants invReq inv = do - inviteeName invReq @=? inInviteeName inv - inviteeEmail invReq @=? inInviteeEmail inv + invReq.inviteeName @=? inv.inviteeName + invReq.inviteeEmail @=? inv.inviteeEmail testGetInvitation :: Brig -> Http () testGetInvitation brig = do (inviter, tid) <- createUserWithTeam brig invite <- stdInvitationRequest <$> randomEmail inv1 <- responseJsonError =<< postInvitation brig tid inviter invite Http () testDeleteInvitation brig = do (inviter, tid) <- createUserWithTeam brig invite <- stdInvitationRequest <$> randomEmail - iid <- inInvitation <$> (responseJsonError =<< postInvitation brig tid inviter invite (toStrict . toByteString)) getQueryParam "team" resp @=? (pure . encodeUtf8 . idToText) tid getQueryParam :: ByteString -> ResponseLBS -> Maybe ByteString getQueryParam name r = do - inv <- (eitherToMaybe . responseJsonEither) r - url <- inInviteeUrl inv + inv :: Invitation <- (eitherToMaybe . responseJsonEither) r + url <- inv.inviteeUrl (lookup name . queryPairs . uriQuery) url -- | Mock the feature API because exposeInvitationURLsToTeamAdmin depends on @@ -309,13 +309,13 @@ testNoInvitationUrl opts brig = do Http () testInvitationEmailLookup brig = do @@ -338,6 +338,8 @@ testInvitationEmailLookupRegister brig = do email <- randomEmail (owner, tid) <- createUserWithTeam brig let invite = stdInvitationRequest email + -- This incidentally also tests that sending multiple + -- invites from the same team results in last-invite-wins scenario void $ postInvitation brig tid owner invite inv :: Invitation <- responseJsonError =<< postInvitation brig tid owner invite -- expect an invitation to be found querying with email after invite @@ -379,7 +381,7 @@ testInvitationTooManyPending opts brig (TeamSizeLimit limit) = do registerInvite :: Brig -> TeamId -> Invitation -> EmailAddress -> Http UserId registerInvite brig tid inv invemail = do - Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- getInvitationCode brig tid inv.invitationId rsp <- post ( brig @@ -483,9 +485,9 @@ createAndVerifyInvitation' replacementBrigApp acceptFn invite brig galley = do ) => m' (Maybe (UserId, UTCTimeMillis), Invitation, UserId, ResponseLBS) invitationHandshake = do - inv <- responseJsonError =<< postInvitation brig tid inviter invite - let invmeta = Just (inviter, inCreatedAt inv) - Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) + inv :: Invitation <- responseJsonError =<< postInvitation brig tid inviter invite + let invmeta = Just (inviter, inv.createdAt) + Just inviteeCode <- getInvitationCode brig tid inv.invitationId Just invitation <- getInvitationInfo brig inviteeCode rsp2 <- post @@ -613,9 +615,8 @@ testInvitationCodeExists brig = do (uid, tid) <- createUserWithTeam brig let invite email = stdInvitationRequest email email <- randomEmail - rsp <- postInvitation brig tid uid (invite email) responseJsonMaybe rsp - Just invCode <- getInvitationCode brig tid invId + inv :: Invitation <- responseJsonError =<< postInvitation brig tid uid (invite email) responseJsonError r if more - then (invs :) <$> getPages (count + step) (fmap inInvitation . listToMaybe . reverse $ invs) step + then (invs :) <$> getPages (count + step) (fmap (.invitationId) . listToMaybe . reverse $ invs) step else pure [invs] let checkSize :: (HasCallStack) => Int -> [Int] -> Http () checkSize pageSize expectedSizes = @@ -740,13 +741,13 @@ testInvitationPaging opts brig = do mapM_ validateInv $ concat invss validateInv :: Invitation -> Assertion validateInv inv = do - assertEqual "tid" tid (inTeam inv) - assertBool "email" (inInviteeEmail inv `elem` emails) + assertEqual "tid" tid (inv.team) + assertBool "email" (inv.inviteeEmail `elem` emails) -- (the output list is not ordered chronologically and emails are unique, so we just -- check whether the email is one of the valid ones.) - assertBool "timestamp" (inCreatedAt inv > before && inCreatedAt inv < after1ms) - assertEqual "uid" (Just uid) (inCreatedBy inv) - -- not checked: @inInvitation inv :: InvitationId@ + assertBool "timestamp" (inv.createdAt > before && inv.createdAt < after1ms) + assertEqual "uid" (Just uid) (inv.createdBy) + -- not checked: @invitation inv :: InvitationId@ checkSize 2 [2, 2, 1] checkSize total [total] @@ -758,7 +759,7 @@ testInvitationInfo brig = do (uid, tid) <- createUserWithTeam brig let invite = stdInvitationRequest email inv <- responseJsonError =<< postInvitation brig tid uid invite - Just invCode <- getInvitationCode brig tid (inInvitation inv) + Just invCode <- getInvitationCode brig tid inv.invitationId Just invitation <- getInvitationInfo brig invCode liftIO $ assertEqual "Invitations differ" inv invitation @@ -769,15 +770,15 @@ testInvitationInfoBadCode brig = do get (brig . path ("/teams/invitations/info?code=" <> icode)) !!! const 400 === statusCode -testInvitationInfoExpired :: Brig -> Opt.Timeout -> Http () +testInvitationInfoExpired :: Brig -> Timeout -> Http () testInvitationInfoExpired brig timeout = do email <- randomEmail (uid, tid) <- createUserWithTeam brig let invite = stdInvitationRequest email - inv <- responseJsonError =<< postInvitation brig tid uid invite + inv :: Invitation <- responseJsonError =<< postInvitation brig tid uid invite -- Note: This value must be larger than the option passed as `team-invitation-timeout` - awaitExpiry (round timeout + 5) tid (inInvitation inv) - getCode tid (inInvitation inv) !!! const 400 === statusCode + awaitExpiry (round timeout + 5) tid inv.invitationId + getCode tid inv.invitationId !!! const 400 === statusCode headInvitationByEmail brig email 404 where getCode t i = @@ -801,8 +802,8 @@ testSuspendTeam brig = do (inviter, tid) <- createUserWithTeam brig -- invite and register invitee let invite = stdInvitationRequest inviteeEmail - inv <- responseJsonError =<< postInvitation brig tid inviter invite - Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) + inv :: Invitation <- responseJsonError =<< postInvitation brig tid inviter invite + Just inviteeCode <- getInvitationCode brig tid inv.invitationId rsp2 <- post ( brig @@ -815,8 +816,8 @@ testSuspendTeam brig = do -- invite invitee2 (don't register) let invite2 = stdInvitationRequest inviteeEmail2 - inv2 <- responseJsonError =<< postInvitation brig tid inviter invite2 - Just _ <- getInvitationCode brig tid (inInvitation inv2) + inv2 :: Invitation <- responseJsonError =<< postInvitation brig tid inviter invite2 + Just _ <- getInvitationCode brig tid inv2.invitationId -- suspend team suspendTeam brig tid !!! const 200 === statusCode -- login fails @@ -826,7 +827,7 @@ testSuspendTeam brig = do -- check status chkStatus brig inviter Suspended chkStatus brig invitee Suspended - assertNoInvitationCode brig tid (inInvitation inv2) + assertNoInvitationCode brig tid inv2.invitationId -- unsuspend unsuspendTeam brig tid !!! const 200 === statusCode chkStatus brig inviter Active diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 097616cdb57..defa0f8e5b3 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -90,11 +90,11 @@ createPopulatedBindingTeamWithNames brig names = do invitees <- forM names $ \name -> do inviteeEmail <- randomEmail let invite = stdInvitationRequest inviteeEmail - inv <- + inv :: Invitation <- responseJsonError =<< postInvitation brig tid (userId inviter) invite Opt.Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> CargoHold -> Galley -> AWS.Env -> UserJournalWatcher -> TestTree +tests :: ConnectionLimit -> Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> CargoHold -> Galley -> AWS.Env -> UserJournalWatcher -> TestTree tests _ at opts p b c ch g aws userJournalWatcher = testGroup "account" @@ -490,7 +489,7 @@ testCreateUserExternalSSO brig = do post (brig . path "/register" . contentJson . body (p True True)) !!! const 400 === statusCode -testActivateWithExpiry :: Opt.Opts -> Brig -> Opt.Timeout -> Http () +testActivateWithExpiry :: Opt.Opts -> Brig -> Timeout -> Http () testActivateWithExpiry (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ _ = pure () testActivateWithExpiry _ brig timeout = do u <- responseJsonError =<< registerUser "dilbert" brig @@ -1374,11 +1373,11 @@ testTooManyMembersForLegalhold opts brig = do -- would return in that case. inviteeEmail <- randomEmail let invite = stdInvitationRequest inviteeEmail - inv <- + inv :: Invitation <- responseJsonError =<< postInvitation brig tid owner invite show (retryTimeout, Opts.timeout opts)) @@ -1045,7 +1046,7 @@ testSuspendInactiveUsers config brig cookieType endPoint = do do diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index 4cc3d7e9648..fb6bf3fc06d 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -55,7 +55,6 @@ import Data.Set qualified as Set import Data.String.Conversions import Data.Text.Ascii (AsciiChars (validate), encodeBase64UrlUnpadded, toText) import Data.Text.Encoding qualified as T -import Data.Time (addUTCTime) import Data.Time.Clock.POSIX import Data.UUID (toByteString) import Data.UUID qualified as UUID @@ -65,11 +64,12 @@ import Network.Wai.Utilities.Error qualified as Error import System.Logger qualified as Log import Test.QuickCheck (arbitrary, generate) import Test.Tasty hiding (Timeout) -import Test.Tasty.Cannon hiding (Cannon) +import Test.Tasty.Cannon hiding (Cannon, Timeout) import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit import UnliftIO (mapConcurrently) import Util +import Util.Timeout import Wire.API.Internal.Notification import Wire.API.MLS.CipherSuite import Wire.API.Routes.Version @@ -86,7 +86,7 @@ import Wire.API.Wrapped (Wrapped (..)) import Wire.VerificationCode qualified as Code import Wire.VerificationCodeGen -tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> DB.ClientState -> Nginz -> Brig -> Cannon -> Galley -> TestTree +tests :: ConnectionLimit -> Timeout -> Opt.Opts -> Manager -> DB.ClientState -> Nginz -> Brig -> Cannon -> Galley -> TestTree tests _cl _at opts p db n b c g = testGroup "client" diff --git a/services/brig/test/integration/API/User/Connection.hs b/services/brig/test/integration/API/User/Connection.hs index e9023104eb9..76aebdaff09 100644 --- a/services/brig/test/integration/API/User/Connection.hs +++ b/services/brig/test/integration/API/User/Connection.hs @@ -26,7 +26,6 @@ import API.User.Util import Bilge hiding (accept, timeout) import Bilge.Assert import Brig.Data.Connection (remoteConnectionInsert) -import Brig.Options qualified as Opt import Cassandra qualified as DB import Control.Arrow ((&&&)) import Data.ByteString.Conversion @@ -34,13 +33,13 @@ import Data.Domain import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) import Data.Qualified -import Data.Time.Clock (getCurrentTime) import Data.UUID.V4 qualified as UUID import Imports import Network.Wai.Utilities.Error qualified as Error import Test.Tasty hiding (Timeout) import Test.Tasty.HUnit import Util +import Util.Timeout import Wire.API.Connection import Wire.API.Conversation import Wire.API.Federation.API.Brig @@ -51,7 +50,7 @@ import Wire.API.User as User tests :: ConnectionLimit -> - Opt.Timeout -> + Timeout -> Manager -> Brig -> Cannon -> diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index 8da3c774ef2..d94f3fbe00f 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -41,18 +41,19 @@ import Imports import Network.Wai.Utilities.Error qualified as Error import Network.Wai.Utilities.Error qualified as Wai import Test.Tasty hiding (Timeout) -import Test.Tasty.Cannon hiding (Cannon) +import Test.Tasty.Cannon hiding (Cannon, Timeout) import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit import UnliftIO (mapConcurrently) import Util +import Util.Timeout import Wire.API.Internal.Notification hiding (target) import Wire.API.Team.Feature (FeatureStatus (..)) import Wire.API.Team.SearchVisibility import Wire.API.User import Wire.API.User.Handle -tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> Galley -> TestTree +tests :: ConnectionLimit -> Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> Galley -> TestTree tests _cl _at conf p b c g = testGroup "handles" diff --git a/services/brig/test/integration/API/User/PasswordReset.hs b/services/brig/test/integration/API/User/PasswordReset.hs index 857bb6c48a2..034c6c40ece 100644 --- a/services/brig/test/integration/API/User/PasswordReset.hs +++ b/services/brig/test/integration/API/User/PasswordReset.hs @@ -33,13 +33,14 @@ import Data.Misc import Imports import Test.Tasty hiding (Timeout) import Util +import Util.Timeout import Wire.API.User import Wire.API.User.Auth tests :: DB.ClientState -> ConnectionLimit -> - Opt.Timeout -> + Timeout -> Opt.Opts -> Manager -> Brig -> diff --git a/services/brig/test/integration/API/User/RichInfo.hs b/services/brig/test/integration/API/User/RichInfo.hs index cad0d8053b6..2ce2855a1cc 100644 --- a/services/brig/test/integration/API/User/RichInfo.hs +++ b/services/brig/test/integration/API/User/RichInfo.hs @@ -34,11 +34,12 @@ import Imports import Test.Tasty hiding (Timeout) import Test.Tasty.HUnit import Util +import Util.Timeout import Wire.API.Team.Permission import Wire.API.User import Wire.API.User.RichInfo -tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> Galley -> TestTree +tests :: ConnectionLimit -> Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> Galley -> TestTree tests _cl _at conf p b _c g = testGroup "rich info" diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs index a0b869d1d97..00e2e3e8de8 100644 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ b/services/brig/test/integration/API/UserPendingActivation.hs @@ -114,7 +114,7 @@ createUserStep :: Spar -> Brig -> ScimToken -> TeamId -> Scim.User.User SparTag createUserStep spar' brig' tok tid scimUser email = do scimStoredUser <- createUser spar' tok scimUser inv <- getInvitationByEmail brig' email - Just inviteeCode <- getInvitationCode brig' tid (inInvitation inv) + Just inviteeCode <- getInvitationCode brig' tid inv.invitationId pure (scimStoredUser, inv, inviteeCode) assertUserExist :: (HasCallStack) => String -> ClientState -> UserId -> Bool -> HttpT IO () diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index ad86b107436..d6f731aa195 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -229,7 +229,7 @@ miscAPI = <@> mkNamedAPI @"test-delete-client" Clients.rmClient <@> mkNamedAPI @"add-service" createService <@> mkNamedAPI @"delete-service" deleteService - <@> mkNamedAPI @"add-bot" Update.addBot + <@> mkNamedAPI @"i-add-bot" Update.addBot <@> mkNamedAPI @"delete-bot" Update.rmBot <@> mkNamedAPI @"put-custom-backend" setCustomBackend <@> mkNamedAPI @"delete-custom-backend" deleteCustomBackend diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 1ecd7e4eab9..8a7894ff2ac 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -130,7 +130,7 @@ tests s = test s "metrics" metrics, test s "fetch conversation by qualified ID (v2)" testGetConvQualifiedV2, test s "create Proteus conversation" postProteusConvOk, - test s "create conversation with remote users some unreachable" (postConvWithUnreachableRemoteUsers $ Set.fromList [rb1, rb2, rb3, rb4]), + test s "create conversation with remote users, some unreachable" (postConvWithUnreachableRemoteUsers $ Set.fromList [rb1, rb2, rb3, rb4]), test s "get empty conversations" getConvsOk, test s "get conversations by ids" getConvsOk2, test s "fail to get >500 conversations with v2 API" getConvsFailMaxSizeV2, @@ -367,8 +367,10 @@ postConvWithUnreachableRemoteUsers rbs = do users <- connectBackend alice rb pure (users, participating rb users) pure $ foldr (\(a, p) acc -> bimap ((<>) a) ((<>) p) acc) ([], []) v - liftIO $ - assertBool "No unreachable backend in the test" (allRemotes /= participatingRemotes) + liftIO $ do + let notParticipatingRemotes = allRemotes \\ participatingRemotes + assertBool "No reachable backend in the test" (not (null participatingRemotes)) + assertBool "No unreachable backend in the test" (not (null notParticipatingRemotes)) let convName = "some chat" otherLocals = [qAlex] @@ -405,7 +407,7 @@ postConvWithUnreachableRemoteUsers rbs = do "Alice does have a group conversation, while she should not!" [] groupConvs - WS.assertNoEvent (3 # Second) [wsAlice, wsAlex] + WS.assertNoEvent (3 # Second) [wsAlice, wsAlex] -- TODO: sometimes, (at least?) one of these users gets a "connection accepted" event. -- @SF.Separation @TSFI.RESTfulAPI @S2 -- This test verifies whether a message actually gets sent all the way to diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 7f7431bb3cc..16938edb549 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -426,7 +426,7 @@ addUserToTeamWithRole role inviter tid = do (inv, rsp2) <- addUserToTeamWithRole' role inviter tid let invitee :: User = responseJsonUnsafe rsp2 inviteeId = User.userId invitee - let invmeta = Just (inviter, inCreatedAt inv) + let invmeta = Just (inviter, inv.createdAt) mem <- getTeamMember inviter tid inviteeId liftIO $ assertEqual "Member has no/wrong invitation metadata" invmeta (mem ^. Team.invitation) let zuid = parseSetCookie <$> getHeader "Set-Cookie" rsp2 @@ -440,7 +440,7 @@ addUserToTeamWithRole' role inviter tid = do let invite = InvitationRequest Nothing role Nothing inviteeEmail invResponse <- postInvitation tid inviter invite inv <- responseJsonError invResponse - inviteeCode <- getInvitationCode tid (inInvitation inv) + inviteeCode <- getInvitationCode tid inv.invitationId r <- post ( brig diff --git a/services/spar/default.nix b/services/spar/default.nix index 4115e8cb670..8e5b8b51e4f 100644 --- a/services/spar/default.nix +++ b/services/spar/default.nix @@ -20,7 +20,6 @@ , cookie , crypton , crypton-x509 -, email-validate , exceptions , extended , gitignoreSource @@ -78,6 +77,7 @@ , wai-utilities , warp , wire-api +, wire-subsystems , xml-conduit , yaml , zauth @@ -138,6 +138,7 @@ mkDerivation { wai-utilities warp wire-api + wire-subsystems yaml ]; executableHaskellDepends = [ @@ -157,7 +158,6 @@ mkDerivation { containers cookie crypton - email-validate exceptions extended hscim diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 5c7ba1d5247..2435d71165b 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -198,6 +198,7 @@ library , wai-utilities , warp , wire-api + , wire-subsystems , yaml default-language: Haskell2010 @@ -351,7 +352,6 @@ executable spar-integration , cassava , cookie , crypton - , email-validate , exceptions , extended , hscim diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index c1c307e341c..6ac9a07efae 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -50,6 +50,7 @@ import Cassandra as Cas import Control.Lens hiding ((.=)) import qualified Data.ByteString as SBS import Data.ByteString.Builder (toLazyByteString) +import Data.HavePendingInvitations import Data.Id import Data.Proxy import Data.Range diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index c11d4dd03f0..31333cf34f1 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -68,6 +68,7 @@ import Wire.API.User import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso import Wire.API.User.RichInfo as RichInfo +import Wire.UserSubsystem (HavePendingInvitations (..)) ---------------------------------------------------------------------- diff --git a/services/spar/src/Spar/Intra/BrigApp.hs b/services/spar/src/Spar/Intra/BrigApp.hs index 83c377ff6fb..ec8ed68ed78 100644 --- a/services/spar/src/Spar/Intra/BrigApp.hs +++ b/services/spar/src/Spar/Intra/BrigApp.hs @@ -23,7 +23,6 @@ module Spar.Intra.BrigApp ( veidToUserSSOId, urefToExternalId, - urefToEmail, veidFromBrigUser, veidFromUserSSOId, mkUserName, @@ -37,18 +36,16 @@ module Spar.Intra.BrigApp -- * re-exports, mostly for historical reasons and lazyness emailFromSAML, - emailToSAMLNameID, - emailFromSAMLNameID, ) where import Brig.Types.Intra -import Brig.Types.User import Control.Lens import Control.Monad.Except import Data.ByteString.Conversion import qualified Data.CaseInsensitive as CI import Data.Handle (Handle, parseHandle) +import Data.HavePendingInvitations import Data.Id (TeamId, UserId) import Data.Text.Encoding import Data.Text.Encoding.Error diff --git a/services/spar/src/Spar/Sem/BrigAccess.hs b/services/spar/src/Spar/Sem/BrigAccess.hs index 53041076773..46d208d1b10 100644 --- a/services/spar/src/Spar/Sem/BrigAccess.hs +++ b/services/spar/src/Spar/Sem/BrigAccess.hs @@ -44,9 +44,9 @@ module Spar.Sem.BrigAccess where import Brig.Types.Intra -import Brig.Types.User import Data.Code as Code import Data.Handle (Handle) +import Data.HavePendingInvitations import Data.Id (TeamId, UserId) import Data.Misc (PlainTextPassword6) import Imports diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index efc6a1c3556..f055bc467f5 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -30,7 +30,6 @@ where import Bilge import Bilge.Assert -import Brig.Types.User as Brig import qualified Control.Exception import Control.Lens import Control.Monad.Except (MonadError (throwError)) @@ -46,6 +45,7 @@ import Data.ByteString.Conversion import qualified Data.CaseInsensitive as CI import qualified Data.Csv as Csv import Data.Handle (Handle, fromHandle, parseHandle, parseHandleEither) +import Data.HavePendingInvitations import Data.Id (TeamId, UserId, randomId) import Data.Ix (inRange) import Data.LanguageCodes (ISO639_1 (..)) @@ -627,7 +627,7 @@ testCreateUserNoIdPWithRole brig tid owner tok role = do -- user follows invitation flow do inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId registerInvitation email userName inviteeCode True -- check for correct role do @@ -690,7 +690,7 @@ testCreateUserNoIdP = do -- user should be able to follow old team invitation flow do inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId registerInvitation email userName inviteeCode True call $ headInvitation404 brig email @@ -1138,7 +1138,7 @@ testCreateUserTimeout = do scimStoredUser <- aFewTimesRecover (createUser tok scimUser) inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId pure (scimStoredUser, inv, inviteeCode) searchUser :: (HasCallStack) => Spar.Types.ScimToken -> Scim.User.User tag -> EmailAddress -> Bool -> TestSpar () @@ -1829,8 +1829,8 @@ lookupByValidScimId tid = registerUser :: BrigReq -> TeamId -> EmailAddress -> TestSpar () registerUser brig tid email = do let r = call $ get (brig . path "/i/teams/invitations/by-email" . queryItem "email" (toByteString' email)) - inv <- responseJsonError =<< r maybeToList mUpdatedRole}) @@ -2115,7 +2115,7 @@ createScimUserWithRole brig tid owner tok initialRole = do -- user follows invitation flow do inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId registerInvitation email userName inviteeCode True checkTeamMembersRole tid owner userid initialRole pure userid @@ -2236,7 +2236,7 @@ specDeleteUser = do do inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId registerInvitation email (Name "Alice") inviteeCode True call $ headInvitation404 brig email @@ -2348,7 +2348,7 @@ testDeletedUsersFreeExternalIdNoIdp = do -- accept invitation do inv <- call $ getInvitation brig email - Just inviteeCode <- call $ getInvitationCode brig tid (inInvitation inv) + Just inviteeCode <- call $ getInvitationCode brig tid inv.invitationId registerInvitation email userName inviteeCode True call $ headInvitation404 brig email diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 5de87fd9579..e041c3d0b1c 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -185,7 +185,7 @@ import qualified Spar.Sem.SAMLUserStore as SAMLUserStore import qualified Spar.Sem.ScimExternalIdStore as ScimExternalIdStore import qualified System.Logger.Extended as Log import System.Random (randomRIO) -import Test.Hspec hiding (fit, it, pending, pendingWith, xit) +import Test.Hspec hiding (it, pending, pendingWith, xit) import qualified Test.Hspec import qualified Text.XML as XML import qualified Text.XML.Cursor as XML @@ -399,8 +399,8 @@ inviteAndRegisterUser :: m User inviteAndRegisterUser brig u tid inviteeEmail = do let invite = stdInvitationRequest inviteeEmail - inv <- responseJsonError =<< postInvitation tid u invite - Just inviteeCode <- getInvitationCode tid (TeamInvitation.inInvitation inv) + inv :: TeamInvitation.Invitation <- responseJsonError =<< postInvitation tid u invite + Just inviteeCode <- getInvitationCode tid inv.invitationId rspInvitee <- post ( brig diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index a077454467d..bf2b7ebe9ae 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -46,7 +46,6 @@ import qualified Spar.Intra.BrigApp as Intra import Spar.Scim.User (synthesizeScimUser, validateScimUser') import qualified Spar.Sem.ScimTokenStore as ScimTokenStore import Test.QuickCheck (arbitrary, generate) -import qualified Text.Email.Parser as Email import qualified Text.XML.DSig as SAML import Util.Core import Util.Types @@ -61,7 +60,6 @@ import qualified Web.Scim.Schema.Meta as Scim import qualified Web.Scim.Schema.PatchOp as Scim.PatchOp import qualified Web.Scim.Schema.User as Scim import qualified Web.Scim.Schema.User as Scim.User -import qualified Web.Scim.Schema.User.Email as Email import qualified Web.Scim.Schema.User.Email as Scim.Email import qualified Web.Scim.Schema.User.Phone as Phone import qualified Wire.API.Team.Member as Member @@ -203,17 +201,6 @@ randomScimUserWithNick = do nick ) -randomScimEmail :: (MonadRandom m) => m Email.Email -randomScimEmail = do - let typ :: Maybe Text = Nothing - primary :: Maybe Scim.ScimBool = Nothing -- TODO: where should we catch users with more than one - -- primary email? - value <- do - localpart <- cs <$> replicateM 15 (getRandomR ('a', 'z')) - domainpart <- (<> ".com") . cs <$> replicateM 15 (getRandomR ('a', 'z')) - pure . Email.EmailAddress $ Email.unsafeEmailAddress localpart domainpart - pure Email.Email {..} - randomScimPhone :: (MonadRandom m) => m Phone.Phone randomScimPhone = do let typ :: Maybe Text = Nothing diff --git a/services/spar/test/Test/Spar/Scim/UserSpec.hs b/services/spar/test/Test/Spar/Scim/UserSpec.hs index ebc26096d0f..5c51ed624de 100644 --- a/services/spar/test/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test/Test/Spar/Scim/UserSpec.hs @@ -2,9 +2,9 @@ module Test.Spar.Scim.UserSpec where import Arbitrary () import Brig.Types.Intra -import Brig.Types.User import Control.Monad.Except (runExceptT) import Data.Handle (parseHandle) +import Data.HavePendingInvitations import Data.Id import Imports import Polysemy diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index f5675118477..2e0f7d49efc 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -184,7 +184,7 @@ sitemap' = :<|> Named @"put-sso-domain-redirect" Intra.putSsoDomainRedirect :<|> Named @"delete-sso-domain-redirect" Intra.deleteSsoDomainRedirect :<|> Named @"register-oauth-client" Intra.registerOAuthClient - :<|> Named @"get-oauth-client" Intra.getOAuthClient + :<|> Named @"stern-get-oauth-client" Intra.getOAuthClient :<|> Named @"update-oauth-client" Intra.updateOAuthClient :<|> Named @"delete-oauth-client" Intra.deleteOAuthClient diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index 38dba2f5817..3c78d30ebe5 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -411,7 +411,7 @@ type SternAPI = :> Post '[JSON] OAuthClientCredentials ) :<|> Named - "get-oauth-client" + "stern-get-oauth-client" ( Summary "Get OAuth client by id" :> "i" :> "oauth" diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index ba50b7edb6b..b7e04c9de2b 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -186,11 +186,8 @@ test-suite stern-tests executable stern-integration main-is: Main.hs - - -- cabal-fmt: expand test/integration other-modules: API - Main TestSetup Util diff --git a/tools/stern/test/integration/Util.hs b/tools/stern/test/integration/Util.hs index ba5ff5c7b49..0e533484b96 100644 --- a/tools/stern/test/integration/Util.hs +++ b/tools/stern/test/integration/Util.hs @@ -139,7 +139,7 @@ addUserToTeamWithRole role inviter tid = do (inv, rsp2) <- addUserToTeamWithRole' role inviter tid let invitee :: User = responseJsonUnsafe rsp2 inviteeId = User.userId invitee - let invmeta = Just (inviter, inCreatedAt inv) + let invmeta = Just (inviter, inv.createdAt) mem <- getTeamMember inviter tid inviteeId liftIO $ assertEqual "Member has no/wrong invitation metadata" invmeta (mem ^. Team.invitation) let zuid = parseSetCookie <$> getHeader "Set-Cookie" rsp2 @@ -153,7 +153,7 @@ addUserToTeamWithRole' role inviter tid = do let invite = InvitationRequest Nothing role Nothing email invResponse <- postInvitation tid inviter invite inv <- responseJsonError invResponse - inviteeCode <- getInvitationCode tid (inInvitation inv) + inviteeCode <- getInvitationCode tid inv.invitationId r <- post ( brig diff --git a/weeder.toml b/weeder.toml index 4e17e0becfa..66ab0310a78 100644 --- a/weeder.toml +++ b/weeder.toml @@ -20,6 +20,7 @@ roots = [ # may of the entries here are about general-purpose module "^API.Team.Util.*$", # FUTUREWORK: Consider whether unused utility functions should be kept. "^Bilge.*$", "^Cassandra.Helpers.toOptionFieldName", + "^Cassandra.QQ.sql$", "^Data.ETag._OpaqueDigest", "^Data.ETag._StrictETag", "^Data.ETag._WeakETag", @@ -125,6 +126,7 @@ roots = [ # may of the entries here are about general-purpose module "^Test.Data.Schema.userSchemaWithDefaultName'", "^Test.Federator.JSON.deriveJSONOptions", # This is used inside an instance derivation via TH "^Test.Wire.API.Golden.Run.main$", + "^Run.main$", "^Test.Wire.API.Password.testHashPasswordScrypt", # FUTUREWORK: reworking scrypt/argon2id is planned for next sprint "^TestSetup.runFederationClient", "^TestSetup.viewCargohold", From 64f156a3fe6f0b930fee48dac501d2890a153c00 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 17 Sep 2024 08:26:53 +0200 Subject: [PATCH 065/136] WPB-10658 invitation and acceptance of individual users to teams (#4229) --- changelog.d/0-release-notes/WPB-10658 | 2 + changelog.d/1-api-changes/WPB-10658 | 1 + changelog.d/2-features/WPB-10658 | 1 + charts/brig/templates/configmap.yaml | 3 + charts/nginz/values.yaml | 3 + integration/integration.cabal | 1 + integration/test/API/Brig.hs | 18 +- integration/test/Notifications.hs | 3 + integration/test/SetupHelpers.hs | 34 +-- integration/test/Test/Teams.hs | 166 +++++++++++ integration/test/Testlib/HTTP.hs | 11 + libs/wire-api/src/Wire/API/Error/Brig.hs | 3 + .../src/Wire/API/Routes/Public/Brig.hs | 18 ++ libs/wire-api/src/Wire/API/Team/Invitation.hs | 23 +- libs/wire-api/src/Wire/API/UserEvent.hs | 17 +- .../golden/Test/Wire/API/Golden/Manual.hs | 3 +- .../Test/Wire/API/Golden/Manual/UserEvent.hs | 25 +- .../test/golden/testObject_UserEvent_1.json | 1 + .../test/golden/testObject_UserEvent_18.json | 16 ++ .../test/golden/testObject_UserEvent_2.json | 1 + .../src/Wire/InvitationCodeStore.hs | 3 +- .../src/Wire/InvitationCodeStore/Cassandra.hs | 3 +- services/brig/brig.integration.yaml | 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/API/Error.hs | 3 + services/brig/src/Brig/API/Internal.hs | 7 +- services/brig/src/Brig/API/Public.hs | 4 +- services/brig/src/Brig/API/User.hs | 76 +++--- services/brig/src/Brig/App.hs | 6 + .../brig/src/Brig/CanonicalInterpreter.hs | 3 + services/brig/src/Brig/Data/User.hs | 7 + services/brig/src/Brig/IO/Intra.hs | 3 +- services/brig/src/Brig/Options.hs | 2 + services/brig/src/Brig/Team/API.hs | 258 +++++++++++++----- services/brig/src/Brig/Team/Email.hs | 11 +- services/brig/src/Brig/Team/Template.hs | 9 + services/brig/src/Brig/User/Search/Index.hs | 9 +- .../integration-test/conf/nginz/nginx.conf | 5 + 40 files changed, 815 insertions(+), 154 deletions(-) create mode 100644 changelog.d/0-release-notes/WPB-10658 create mode 100644 changelog.d/1-api-changes/WPB-10658 create mode 100644 changelog.d/2-features/WPB-10658 create mode 100644 integration/test/Test/Teams.hs create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_18.json create mode 100644 services/brig/deb/opt/brig/templates/en/team/email/existing-invitation-subject.txt create mode 100644 services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.html create mode 100644 services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.txt diff --git a/changelog.d/0-release-notes/WPB-10658 b/changelog.d/0-release-notes/WPB-10658 new file mode 100644 index 00000000000..df9e6dc5e17 --- /dev/null +++ b/changelog.d/0-release-notes/WPB-10658 @@ -0,0 +1,2 @@ +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/1-api-changes/WPB-10658 b/changelog.d/1-api-changes/WPB-10658 new file mode 100644 index 00000000000..a40aff74ef1 --- /dev/null +++ b/changelog.d/1-api-changes/WPB-10658 @@ -0,0 +1 @@ +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/2-features/WPB-10658 b/changelog.d/2-features/WPB-10658 new file mode 100644 index 00000000000..e0d4302a688 --- /dev/null +++ b/changelog.d/2-features/WPB-10658 @@ -0,0 +1 @@ +Allow an existing non-team user to migrate to a team diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index 32ccd3acc04..4e1e5393a2e 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -179,14 +179,17 @@ data: team: {{- if .emailSMS.team }} tInvitationUrl: {{ .emailSMS.team.tInvitationUrl }} + tExistingUserInvitationUrl: {{ .emailSMS.team.tExistingUserInvitationUrl }} tActivationUrl: {{ .emailSMS.team.tActivationUrl }} tCreatorWelcomeUrl: {{ .emailSMS.team.tCreatorWelcomeUrl }} tMemberWelcomeUrl: {{ .emailSMS.team.tMemberWelcomeUrl }} {{- else }} {{- if .externalUrls.teamSettings }} tInvitationUrl: {{ .externalUrls.teamSettings }}/join/?team-code=${code} + tExistingUserInvitationUrl: {{ .externalUrls.teamSettings }}/accept-invitation/?team-code=${code} {{- else }} tInvitationUrl: {{ .externalUrls.nginz }}/register?team=${team}&team_code=${code} + tExistingUserInvitationUrl: {{ .externalUrls.nginz }}/accept-invitation/?team-code=${code} {{- end }} tActivationUrl: {{ .externalUrls.nginz }}/register?team=${team}&team_code=${code} tCreatorWelcomeUrl: {{ .externalUrls.teamCreatorWelcome }} diff --git a/charts/nginz/values.yaml b/charts/nginz/values.yaml index c3db69f37fc..12d6708f8d9 100644 --- a/charts/nginz/values.yaml +++ b/charts/nginz/values.yaml @@ -410,6 +410,9 @@ nginx_conf: envs: - all disable_zauth: true + - path: /teams/invitations/accept$ + envs: + - all - path: /i/teams/invitation-code envs: - staging diff --git a/integration/integration.cabal b/integration/integration.cabal index d6e3384b98c..faf1a6a4867 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -149,6 +149,7 @@ library Test.Services Test.Spar Test.Swagger + Test.Teams Test.TeamSettings Test.User Test.Version diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 65e8d1d1961..fae907cd2e3 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -434,11 +434,12 @@ putUserSupportedProtocols user ps = do submit "PUT" (req & addJSONObject ["supported_protocols" .= ps]) data PostInvitation = PostInvitation - { email :: Maybe String + { email :: Maybe String, + role :: Maybe String } instance Default PostInvitation where - def = PostInvitation Nothing + def = PostInvitation Nothing Nothing postInvitation :: (HasCallStack, MakesValue user) => @@ -452,7 +453,7 @@ postInvitation user inv = do joinHttpPath ["teams", tid, "invitations"] email <- maybe randomEmail pure inv.email submit "POST" $ - req & addJSONObject ["email" .= email] + req & addJSONObject (["email" .= email] <> ["role" .= r | r <- toList inv.role]) getApiVersions :: (HasCallStack) => App Response getApiVersions = do @@ -783,3 +784,14 @@ activate domain key code = do submit "GET" $ req & addQueryParams [("key", key), ("code", code)] + +acceptTeamInvitation :: (HasCallStack, MakesValue user) => user -> String -> Maybe String -> App Response +acceptTeamInvitation user code mPw = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", "invitations", "accept"] + submit "POST" $ req & addJSONObject (["code" .= code] <> maybeToList (((.=) "password") <$> mPw)) + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_teams__tid__invitations +listInvitations :: (HasCallStack, MakesValue user) => user -> String -> App Response +listInvitations user tid = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", tid, "invitations"] + submit "GET" req diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index 13dd5a0fb35..548f930fd24 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -175,6 +175,9 @@ isUserActivateNotif = notifTypeIsEqual "user.activate" isUserClientAddNotif :: (MakesValue a) => a -> App Bool isUserClientAddNotif = notifTypeIsEqual "user.client-add" +isUserUpdatedNotif :: (MakesValue a) => a -> App Bool +isUserUpdatedNotif = notifTypeIsEqual "user.update" + isUserClientRemoveNotif :: (MakesValue a) => a -> App Bool isUserClientRemoveNotif = notifTypeIsEqual "user.client-remove" diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 2efb0aa8740..1502844ac41 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -60,30 +60,18 @@ createTeamMemberWithRole :: String -> String -> App Value -createTeamMemberWithRole inviter tid role = do +createTeamMemberWithRole inviter _ role = do newUserEmail <- randomEmail - let invitationJSON = ["role" .= role, "email" .= newUserEmail] - invitationReq <- - baseRequest inviter Brig Versioned $ - joinHttpPath ["teams", tid, "invitations"] - invitation <- getJSON 201 =<< submit "POST" (addJSONObject invitationJSON invitationReq) - invitationId <- objId invitation - invitationCodeReq <- - rawBaseRequest inviter Brig Unversioned "/i/teams/invitation-code" - <&> addQueryParams [("team", tid), ("invitation_id", invitationId)] - invitationCode <- bindResponse (submit "GET" invitationCodeReq) $ \res -> do - res.status `shouldMatchInt` 200 - res.json %. "code" & asString - let registerJSON = - [ "name" .= newUserEmail, - "email" .= newUserEmail, - "password" .= defPassword, - "team_code" .= invitationCode - ] - registerReq <- - rawBaseRequest inviter Brig Versioned "/register" - <&> addJSONObject registerJSON - getJSON 201 =<< submit "POST" registerReq + invitation <- postInvitation inviter (PostInvitation (Just newUserEmail) (Just role)) >>= getJSON 201 + invitationCode <- getInvitationCode inviter invitation >>= getJSON 200 >>= (%. "code") & asString + let body = + AddUser + { name = Just newUserEmail, + email = Just newUserEmail, + password = Just defPassword, + teamCode = Just invitationCode + } + addUser inviter body >>= getJSON 201 connectTwoUsers :: ( HasCallStack, diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs new file mode 100644 index 00000000000..e3394fa769c --- /dev/null +++ b/integration/test/Test/Teams.hs @@ -0,0 +1,166 @@ +-- 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.Teams where + +import API.Brig +import API.BrigInternal (createUser, getInvitationCode, refreshIndex) +import API.Common +import API.Galley (getTeamMembers) +import API.GalleyInternal (setTeamFeatureStatus) +import Control.Monad.Codensity (Codensity (runCodensity)) +import Control.Monad.Extra (findM) +import Control.Monad.Reader (asks) +import Notifications (isUserUpdatedNotif) +import SetupHelpers +import Testlib.JSON +import Testlib.Prelude +import Testlib.ResourcePool (acquireResources) + +testInvitePersonalUserToTeam :: (HasCallStack) => App () +testInvitePersonalUserToTeam = do + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + (owner, tid, tm) <- runCodensity (startDynamicBackend testBackend def) $ \_ -> do + (owner, tid, tm : _) <- createTeam domain 2 + pure (owner, tid, tm) + + runCodensity + ( startDynamicBackend + testBackend + (def {galleyCfg = setField "settings.exposeInvitationURLsTeamAllowlist" [tid]}) + ) + $ \_ -> do + bindResponse (listInvitations owner tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "invitations" `shouldMatch` ([] :: [()]) + ownerId <- owner %. "id" & asString + setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" >>= assertSuccess + user <- createUser domain def >>= getJSON 201 + uid <- user %. "id" >>= asString + email <- user %. "email" >>= asString + inv <- postInvitation owner (PostInvitation (Just email) Nothing) >>= getJSON 201 + checkListInvitations owner tid email + code <- getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + inv %. "url" & asString >>= assertUrlContainsCode code + acceptTeamInvitation user code Nothing >>= assertStatus 400 + acceptTeamInvitation user code (Just "wrong-password") >>= assertStatus 403 + void $ withWebSockets [user] $ \wss -> do + acceptTeamInvitation user code (Just defPassword) >>= assertSuccess + for wss $ \ws -> do + n <- awaitMatch isUserUpdatedNotif ws + n %. "payload.0.user.team" `shouldMatch` tid + bindResponse (getSelf user) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "team" `shouldMatch` tid + -- a team member can now find the former personal user in the team + bindResponse (getTeamMembers tm tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + members <- resp.json %. "members" >>= asList + ids <- for members ((%. "user") >=> asString) + ids `shouldContain` [uid] + -- the former personal user can now see other team members + bindResponse (getTeamMembers user tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + members <- resp.json %. "members" >>= asList + ids <- for members ((%. "user") >=> asString) + tmId <- tm %. "id" & asString + ids `shouldContain` [ownerId] + ids `shouldContain` [tmId] + -- the former personal user can now search for the owner + bindResponse (searchContacts user (owner %. "name") domain) $ \resp -> do + resp.status `shouldMatchInt` 200 + documents <- resp.json %. "documents" >>= asList + ids <- for documents ((%. "id") >=> asString) + ids `shouldContain` [ownerId] + refreshIndex domain + -- a team member can now search for the former personal user + bindResponse (searchContacts tm (user %. "name") domain) $ \resp -> do + resp.status `shouldMatchInt` 200 + document <- resp.json %. "documents" >>= asList >>= assertOne + document %. "id" `shouldMatch` uid + document %. "team" `shouldMatch` tid + where + checkListInvitations :: Value -> String -> String -> App () + checkListInvitations owner tid email = do + newUserEmail <- randomEmail + void $ postInvitation owner (PostInvitation (Just newUserEmail) Nothing) >>= assertSuccess + bindResponse (listInvitations owner tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + invitations <- resp.json %. "invitations" >>= asList + + -- personal user invitations have a different invitation URL than non-existing user invitations + newUserInv <- invitations & findM (\i -> (i %. "email" >>= asString) <&> (== newUserEmail)) + newUserInvUrl <- newUserInv %. "url" & asString + newUserInvUrl `shouldContainString` "/register" + + personalUserInv <- invitations & findM (\i -> (i %. "email" >>= asString) <&> (== email)) + personalUserInvUrl <- personalUserInv %. "url" & asString + personalUserInvUrl `shouldContainString` "/accept-invitation" + + assertUrlContainsCode :: (HasCallStack) => String -> String -> App () + assertUrlContainsCode code url = do + queryParam <- url & asString <&> getQueryParam "team-code" + queryParam `shouldMatch` Just (Just code) + +testInvitePersonalUserToTeamMultipleInvitations :: (HasCallStack) => App () +testInvitePersonalUserToTeamMultipleInvitations = do + (owner, tid, _) <- createTeam OwnDomain 0 + (owner2, _, _) <- createTeam OwnDomain 0 + user <- createUser OwnDomain def >>= getJSON 201 + email <- user %. "email" >>= asString + inv <- postInvitation owner (PostInvitation (Just email) Nothing) >>= getJSON 201 + inv2 <- postInvitation owner2 (PostInvitation (Just email) Nothing) >>= getJSON 201 + code <- getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + acceptTeamInvitation user code (Just defPassword) >>= assertSuccess + bindResponse (getSelf user) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "team" `shouldMatch` tid + code2 <- getInvitationCode owner2 inv2 >>= getJSON 200 >>= (%. "code") & asString + bindResponse (acceptTeamInvitation user code2 (Just defPassword)) $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "cannot-join-multiple-teams" + bindResponse (getSelf user) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "team" `shouldMatch` tid + acceptTeamInvitation user code (Just defPassword) >>= assertStatus 400 + +testInvitationTypesAreDistinct :: (HasCallStack) => App () +testInvitationTypesAreDistinct = do + -- We are only testing one direction because the other is not possible + -- because the non-existing user cannot have a valid session + (owner, _, _) <- createTeam OwnDomain 0 + user <- createUser OwnDomain def >>= getJSON 201 + email <- user %. "email" >>= asString + inv <- postInvitation owner (PostInvitation (Just email) Nothing) >>= getJSON 201 + code <- getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + let body = + AddUser + { name = Just email, + email = Just email, + password = Just defPassword, + teamCode = Just code + } + addUser OwnDomain body >>= assertStatus 409 + +testTeamUserCannotBeInvited :: (HasCallStack) => App () +testTeamUserCannotBeInvited = do + (_, _, tm : _) <- createTeam OwnDomain 2 + (owner2, _, _) <- createTeam OwnDomain 0 + email <- tm %. "email" >>= asString + postInvitation owner2 (PostInvitation (Just email) Nothing) >>= assertStatus 409 diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index f9f495abc96..ae15b01adb1 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -4,6 +4,7 @@ import qualified Control.Exception as E import Control.Monad.Reader import qualified Data.Aeson as Aeson import qualified Data.Aeson.Types as Aeson +import Data.Bifunctor (Bifunctor (bimap)) import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as C8 import qualified Data.ByteString.Lazy as L @@ -23,6 +24,7 @@ import GHC.Stack import qualified Network.HTTP.Client as HTTP import Network.HTTP.Types (hLocation) import qualified Network.HTTP.Types as HTTP +import Network.HTTP.Types.URI (parseQuery) import Network.URI (URI (..), URIAuth (..), parseURI) import Testlib.Assertions import Testlib.Env @@ -221,3 +223,12 @@ locationHeader = findHeader hLocation findHeader :: HTTP.HeaderName -> Response -> Maybe (HTTP.HeaderName, ByteString) findHeader name resp = find (\(name', _) -> name == name') resp.headers + +getQueryParam :: String -> String -> Maybe (Maybe String) +getQueryParam name url = + parseURI url + >>= lookup name + . fmap (bimap cs ((<$>) cs)) + . parseQuery + . cs + . uriQuery diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 8399afaf1a5..16efe68b803 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -70,6 +70,7 @@ data BrigError | ChangePasswordMustDiffer | PasswordAuthenticationFailed | TooManyTeamInvitations + | CannotJoinMultipleTeams | InsufficientTeamPermissions | KeyPackageDecodingError | InvalidKeyPackageRef @@ -251,6 +252,8 @@ type instance MapError 'PasswordAuthenticationFailed = 'StaticError 403 "passwor type instance MapError 'TooManyTeamInvitations = 'StaticError 403 "too-many-team-invitations" "Too many team invitations for this team" +type instance MapError 'CannotJoinMultipleTeams = 'StaticError 403 "cannot-join-multiple-teams" "Cannot accept invitations from multiple teams" + type instance MapError 'InsufficientTeamPermissions = 'StaticError 403 "insufficient-permissions" "Insufficient team permissions" type instance MapError 'KeyPackageDecodingError = 'StaticError 409 "decoding-error" "Key package could not be TLS-decoded" 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 72afa66ff3b..aee06b492ad 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1560,6 +1560,7 @@ type TeamsAPI = :> CanThrow 'BlacklistedEmail :> CanThrow 'TooManyTeamInvitations :> CanThrow 'InsufficientTeamPermissions + :> CanThrow 'InvalidInvitationCode :> ZUser :> "teams" :> Capture "tid" TeamId @@ -1660,6 +1661,23 @@ type TeamsAPI = '[JSON] (Respond 200 "Number of team members" TeamSize) ) + :<|> Named + "accept-team-invitation" + ( Summary "Accept a team invitation, changing a personal account into a team member account." + :> CanThrow 'PendingInvitationNotFound + :> CanThrow 'TooManyTeamMembers + :> CanThrow 'MissingIdentity + :> CanThrow 'InvalidActivationCodeWrongUser + :> CanThrow 'InvalidActivationCodeWrongCode + :> CanThrow 'BadCredentials + :> CanThrow 'MissingAuth + :> ZLocalUser + :> "teams" + :> "invitations" + :> "accept" + :> ReqBody '[JSON] AcceptTeamInvitation + :> MultiVerb 'POST '[JSON] '[RespondEmpty 200 "Team invitation accepted."] () + ) type SystemSettingsAPI = Named diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index 967adad2832..f195b4072ce 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -23,6 +23,7 @@ module Wire.API.Team.Invitation Invitation (..), InvitationList (..), InvitationLocation (..), + AcceptTeamInvitation (..), HeadInvitationByEmailResult (..), HeadInvitationsResponses, ) @@ -33,6 +34,7 @@ import Data.Aeson qualified as A import Data.ByteString.Conversion import Data.Id import Data.Json.Util +import Data.Misc import Data.OpenApi qualified as S import Data.SOP import Data.Schema @@ -42,11 +44,9 @@ import Servant (FromHttpApiData (..), ToHttpApiData (..)) import URI.ByteString import Wire.API.Error import Wire.API.Error.Brig -import Wire.API.Locale (Locale) import Wire.API.Routes.MultiVerb import Wire.API.Team.Role (Role, defaultRole) -import Wire.API.User.Identity (EmailAddress) -import Wire.API.User.Profile (Name) +import Wire.API.User import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -------------------------------------------------------------------------------- @@ -179,3 +179,20 @@ instance ToSchema InvitationList where .= field "invitations" (array schema) <*> ilHasMore .= fieldWithDocModifier "has_more" (description ?~ "Indicator that the server has more invitations than returned.") schema + +-------------------------------------------------------------------------------- +-- AcceptTeamInvitation + +data AcceptTeamInvitation = AcceptTeamInvitation + { code :: InvitationCode, + password :: PlainTextPassword6 + } + deriving stock (Eq, Show, Generic) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema AcceptTeamInvitation) + +instance ToSchema AcceptTeamInvitation where + schema = + objectWithDocModifier "AcceptTeamInvitation" (description ?~ "Accept an invitation to join a team on Wire.") $ + AcceptTeamInvitation + <$> code .= fieldWithDocModifier "code" (description ?~ "Invitation code to accept.") schema + <*> password .= fieldWithDocModifier "password" (description ?~ "The user account password.") schema diff --git a/libs/wire-api/src/Wire/API/UserEvent.hs b/libs/wire-api/src/Wire/API/UserEvent.hs index db9794604c6..db4c69d2c24 100644 --- a/libs/wire-api/src/Wire/API/UserEvent.hs +++ b/libs/wire-api/src/Wire/API/UserEvent.hs @@ -156,7 +156,8 @@ data UserUpdatedData = UserUpdatedData eupManagedBy :: !(Maybe ManagedBy), eupSSOId :: !(Maybe UserSSOId), eupSSOIdRemoved :: Bool, - eupSupportedProtocols :: !(Maybe (Set BaseProtocolTag)) + eupSupportedProtocols :: !(Maybe (Set BaseProtocolTag)), + eupTeam :: !(Maybe TeamId) } deriving stock (Eq, Show) @@ -192,6 +193,9 @@ emailUpdated :: UserId -> EmailAddress -> UserEvent emailUpdated u e = UserIdentityUpdated $ UserIdentityUpdatedData u (Just e) Nothing +teamUpdated :: UserId -> TeamId -> UserEvent +teamUpdated u t = UserUpdated (emptyUserUpdatedData u) {eupTeam = Just t} + emptyUserUpdatedData :: UserId -> UserUpdatedData emptyUserUpdatedData u = UserUpdatedData @@ -206,7 +210,8 @@ emptyUserUpdatedData u = eupManagedBy = Nothing, eupSSOId = Nothing, eupSSOIdRemoved = False, - eupSupportedProtocols = Nothing + eupSupportedProtocols = Nothing, + eupTeam = Nothing } -- Event schema @@ -247,12 +252,8 @@ eventObjectSchema = <*> eupManagedBy .= maybe_ (optField "managed_by" schema) <*> eupSSOId .= maybe_ (optField "sso_id" genericToSchema) <*> eupSSOIdRemoved .= field "sso_id_deleted" schema - <*> eupSupportedProtocols - .= maybe_ - ( optField - "supported_protocols" - (set schema) - ) + <*> eupSupportedProtocols .= maybe_ (optField "supported_protocols" (set schema)) + <*> eupTeam .= maybe_ (optField "team" 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 0ff493d9e85..d85fdd1bbd8 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 @@ -234,7 +234,8 @@ tests = (testObject_UserEvent_14, "testObject_UserEvent_14.json"), (testObject_UserEvent_15, "testObject_UserEvent_15.json"), (testObject_UserEvent_16, "testObject_UserEvent_16.json"), - (testObject_UserEvent_17, "testObject_UserEvent_17.json") + (testObject_UserEvent_17, "testObject_UserEvent_17.json"), + (testObject_UserEvent_18, "testObject_UserEvent_18.json") ], testGroup "MLSPublicKeys" $ testObjects diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs index 0f0443cc710..5d99e783a5c 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs @@ -33,6 +33,7 @@ module Test.Wire.API.Golden.Manual.UserEvent testObject_UserEvent_15, testObject_UserEvent_16, testObject_UserEvent_17, + testObject_UserEvent_18, ) where @@ -99,6 +100,7 @@ testObject_UserEvent_6 = Nothing False (Just mempty) + Nothing ) ) @@ -191,6 +193,27 @@ testObject_UserEvent_16 = testObject_UserEvent_17 :: Event testObject_UserEvent_17 = ClientEvent (ClientRemoved (ClientId 2839)) +testObject_UserEvent_18 :: Event +testObject_UserEvent_18 = + UserEvent + ( UserUpdated + ( UserUpdatedData + (userId alice) + (Just alice.userDisplayName) + alice.userTextStatus + (Just alice.userPict) + (Just alice.userAccentId) + (Just alice.userAssets) + alice.userHandle + (Just alice.userLocale) + (Just alice.userManagedBy) + Nothing + False + (Just mempty) + alice.userTeam + ) + ) + -------------------------------------------------------------------------------- alice :: User @@ -216,7 +239,7 @@ alice = userService = Nothing, userHandle = Nothing, userExpire = Nothing, - userTeam = Nothing, + userTeam = Just $ Id (fromJust (UUID.fromString "bb843450-b2f5-4ec8-90bd-52c7d5f1d22e")), userManagedBy = ManagedByWire, userSupportedProtocols = defSupportedProtocols } diff --git a/libs/wire-api/test/golden/testObject_UserEvent_1.json b/libs/wire-api/test/golden/testObject_UserEvent_1.json index 6938bd328fe..bfe90d9970a 100644 --- a/libs/wire-api/test/golden/testObject_UserEvent_1.json +++ b/libs/wire-api/test/golden/testObject_UserEvent_1.json @@ -16,6 +16,7 @@ "supported_protocols": [ "proteus" ], + "team": "bb843450-b2f5-4ec8-90bd-52c7d5f1d22e", "text_status": "text status" } } diff --git a/libs/wire-api/test/golden/testObject_UserEvent_18.json b/libs/wire-api/test/golden/testObject_UserEvent_18.json new file mode 100644 index 00000000000..96f97d80149 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_18.json @@ -0,0 +1,16 @@ +{ + "type": "user.update", + "user": { + "accent_id": 1, + "assets": [], + "id": "539d9183-32a5-4fc4-ba5c-4634454e7585", + "locale": "tn-SB", + "managed_by": "wire", + "name": "alice", + "picture": [], + "sso_id_deleted": false, + "supported_protocols": [], + "team": "bb843450-b2f5-4ec8-90bd-52c7d5f1d22e", + "text_status": "text status" + } +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_2.json b/libs/wire-api/test/golden/testObject_UserEvent_2.json index 2b051ddd45a..e630fcc9701 100644 --- a/libs/wire-api/test/golden/testObject_UserEvent_2.json +++ b/libs/wire-api/test/golden/testObject_UserEvent_2.json @@ -16,6 +16,7 @@ "supported_protocols": [ "proteus" ], + "team": "bb843450-b2f5-4ec8-90bd-52c7d5f1d22e", "text_status": "text status" } } diff --git a/libs/wire-subsystems/src/Wire/InvitationCodeStore.hs b/libs/wire-subsystems/src/Wire/InvitationCodeStore.hs index a9183ae2da9..78eee5283dc 100644 --- a/libs/wire-subsystems/src/Wire/InvitationCodeStore.hs +++ b/libs/wire-subsystems/src/Wire/InvitationCodeStore.hs @@ -70,7 +70,8 @@ data InsertInvitation = MkInsertInvitation createdAt :: UTCTime, createdBy :: Maybe UserId, inviteeEmail :: EmailAddress, - inviteeName :: Maybe Name + inviteeName :: Maybe Name, + code :: InvitationCode } deriving (Show, Eq, Generic) diff --git a/libs/wire-subsystems/src/Wire/InvitationCodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/InvitationCodeStore/Cassandra.hs index f8a3bc4a688..37463cfb966 100644 --- a/libs/wire-subsystems/src/Wire/InvitationCodeStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/InvitationCodeStore/Cassandra.hs @@ -36,8 +36,7 @@ insertInvitationImpl :: -- | The timeout for the invitation code. Timeout -> Client StoredInvitation -insertInvitationImpl (MkInsertInvitation invId teamId role (toUTCTimeMillis -> now) uid email name) timeout = do - code <- liftIO mkInvitationCode +insertInvitationImpl (MkInsertInvitation invId teamId role (toUTCTimeMillis -> now) uid email name code) timeout = do let inv = MkStoredInvitation { teamId = teamId, diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index 265eca6cfc9..daec729b8c2 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -124,6 +124,7 @@ emailSMS: team: tInvitationUrl: http://127.0.0.1:8080/register?team=${team}&team_code=${code} + tExistingUserInvitationUrl: http://127.0.0.1:8080/accept-invitation?team-code=${code} tActivationUrl: http://127.0.0.1:8080/register?team=${team}&team_code=${code} tCreatorWelcomeUrl: http://127.0.0.1:8080/creator-welcome-website tMemberWelcomeUrl: http://127.0.0.1:8080/member-welcome-website 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 new file mode 100644 index 00000000000..9fef363e407 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation-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/existing-invitation.html b/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.html new file mode 100644 index 00000000000..2985716ea58 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.html @@ -0,0 +1,183 @@ + + + + + + + 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 new file mode 100644 index 00000000000..918c8fde767 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/team/email/existing-invitation.txt @@ -0,0 +1,25 @@ +[${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/API/Error.hs b/services/brig/src/Brig/API/Error.hs index 98618e5dbd0..5f01495d8de 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -235,6 +235,9 @@ verificationCodeThrottledError (VerificationCodeThrottled t) = clientCapabilitiesCannotBeRemoved :: Wai.Error clientCapabilitiesCannotBeRemoved = Wai.mkError status409 "client-capabilities-cannot-be-removed" "You can only add capabilities to a client, not remove them." +-- One of two cases: +-- (1) the email is in use by any other account or invitation; +-- (2) (when posting an invitation) the email is in use by a member of another team (and we can't steal away those, invitee has to be personal user). emailExists :: Wai.Error emailExists = Wai.mkError status409 "email-exists" "The given e-mail address is in use." diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 03c9af86610..cdb90eb56a6 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -50,6 +50,7 @@ import Brig.IO.Intra qualified as Intra import Brig.Options hiding (internalEvents) import Brig.Provider.API qualified as Provider import Brig.Team.API qualified as Team +import Brig.Team.Template (TeamTemplates) import Brig.Types.Connection import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) @@ -143,7 +144,8 @@ servantSitemap :: Member EmailSubsystem r, Member VerificationCodeSubsystem r, Member PasswordResetCodeStore r, - Member PropertySubsystem r + Member PropertySubsystem r, + Member (Input TeamTemplates) r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -249,7 +251,8 @@ teamsAPI :: Member InvitationCodeStore r, Member (ConnectionStore InternalPaging) r, Member EmailSending r, - Member UserSubsystem r + Member UserSubsystem r, + Member (Input TeamTemplates) r ) => ServerT BrigIRoutes.TeamsAPI (Handler r) teamsAPI = diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 554eceb5b85..2ab050fc306 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -51,6 +51,7 @@ import Brig.Options hiding (internalEvents) import Brig.Provider.API import Brig.Team.API qualified as Team import Brig.Team.Email qualified as Team +import Brig.Team.Template (TeamTemplates) import Brig.Types.Activation (ActivationPair) import Brig.Types.Intra (UserAccount (UserAccount, accountUser)) import Brig.User.API.Handle qualified as Handle @@ -289,7 +290,8 @@ servantSitemap :: Member VerificationCodeSubsystem r, Member PropertySubsystem r, Member PasswordResetCodeStore r, - Member InvitationCodeStore r + Member InvitationCodeStore r, + Member (Input TeamTemplates) r ) => ServerT BrigAPI (Handler r) servantSitemap = diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 4f76936fae7..f18ae4b8d30 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -65,6 +65,7 @@ module Brig.API.User -- * Utilities fetchUserIdentity, + findTeamInvitation, ) where @@ -282,12 +283,8 @@ createUser new = do (mNewTeamUser, teamInvitation, tid) <- case newUserTeam new of Just (NewTeamMember i) -> do - mbTeamInv <- findTeamInvitation (mkEmailKey <$> email) i - case mbTeamInv of - Just (inv, info, tid) -> - pure (Nothing, Just (inv, info), Just tid) - Nothing -> - pure (Nothing, Nothing, Nothing) + (inv, info) <- findTeamInvitation (mkEmailKey <$> email) i + pure (Nothing, Just (inv, info), Just info.teamId) Just (NewTeamCreator t) -> do (Just t,Nothing,) <$> (Just . Id <$> liftIO nextRandom) Just (NewTeamMemberSSO tid) -> @@ -386,44 +383,8 @@ createUser new = do let email = newUserEmail newUser for_ (mkEmailKey <$> email) $ \k -> verifyUniquenessAndCheckBlacklist k !>> identityErrorToRegisterError - pure email - findTeamInvitation :: - Maybe EmailKey -> - InvitationCode -> - ExceptT - RegisterError - (AppT r) - ( Maybe - (StoredInvitation, StoredInvitationInfo, TeamId) - ) - findTeamInvitation Nothing _ = throwE RegisterErrorMissingIdentity - findTeamInvitation (Just e) c = - lift (liftSem $ InvitationCodeStore.lookupInvitationInfo c) >>= \case - Just invitationInfo -> do - inv <- lift . liftSem $ InvitationCodeStore.lookupInvitation invitationInfo.teamId invitationInfo.invitationId - case (inv, (.email) <$> inv) of - (Just invite, Just em) - | e == mkEmailKey em -> do - ensureMemberCanJoin invitationInfo.teamId - pure $ Just (invite, invitationInfo, invitationInfo.teamId) - _ -> throwE RegisterErrorInvalidInvitationCode - Nothing -> throwE RegisterErrorInvalidInvitationCode - - ensureMemberCanJoin :: TeamId -> ExceptT RegisterError (AppT r) () - ensureMemberCanJoin tid = do - maxSize <- fromIntegral . setMaxTeamSize <$> view settings - (TeamSize teamSize) <- TeamSize.teamSize tid - when (teamSize >= maxSize) $ - throwE RegisterErrorTooManyTeamMembers - -- FUTUREWORK: The above can easily be done/tested in the intra call. - -- Remove after the next release. - canAdd <- lift $ liftSem $ GalleyAPIAccess.checkUserCanJoinTeam tid - case canAdd of - Just e -> throwM $ API.UserNotAllowedToJoinTeam e - Nothing -> pure () - acceptTeamInvitation :: UserAccount -> StoredInvitation -> @@ -490,6 +451,37 @@ createUser new = do !>> activationErrorToRegisterError pure Nothing +findTeamInvitation :: + ( Member GalleyAPIAccess r, + Member InvitationCodeStore r + ) => + Maybe EmailKey -> + InvitationCode -> + ExceptT RegisterError (AppT r) (StoredInvitation, StoredInvitationInfo) +findTeamInvitation Nothing _ = throwE RegisterErrorMissingIdentity +findTeamInvitation (Just e) c = + lift (liftSem $ InvitationCodeStore.lookupInvitationInfo c) >>= \case + Just invitationInfo -> do + inv <- lift . liftSem $ InvitationCodeStore.lookupInvitation invitationInfo.teamId invitationInfo.invitationId + case (inv, (.email) <$> inv) of + (Just invite, Just em) + | e == mkEmailKey em -> do + ensureMemberCanJoin invitationInfo.teamId + pure (invite, invitationInfo) + _ -> throwE RegisterErrorInvalidInvitationCode + Nothing -> throwE RegisterErrorInvalidInvitationCode + where + ensureMemberCanJoin :: (Member GalleyAPIAccess r) => TeamId -> ExceptT RegisterError (AppT r) () + ensureMemberCanJoin tid = do + maxSize <- fromIntegral . setMaxTeamSize <$> view settings + (TeamSize teamSize) <- TeamSize.teamSize tid + when (teamSize >= maxSize) $ + throwE RegisterErrorTooManyTeamMembers + -- FUTUREWORK: The above can easily be done/tested in the intra call. + -- Remove after the next release. + mAddUserError <- lift $ liftSem $ GalleyAPIAccess.checkUserCanJoinTeam tid + maybe (pure ()) (throwM . API.UserNotAllowedToJoinTeam) mAddUserError + initAccountFeatureConfig :: UserId -> (AppT r) () initAccountFeatureConfig uid = do mStatus <- preview (settings . featureFlags . _Just . to conferenceCalling . to forNew . _Just) diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 882204e28d7..f08502c94cc 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -42,6 +42,7 @@ module Brig.App userTemplates, providerTemplates, teamTemplates, + teamTemplatesNoLocale, templateBranding, requestId, httpManager, @@ -441,6 +442,11 @@ providerTemplates l = forLocale l <$> view provTemplates teamTemplates :: (MonadReader Env m) => Maybe Locale -> m (Locale, TeamTemplates) teamTemplates l = forLocale l <$> view tmTemplates +-- this works because team templates is not affected by `forLocale`; it is useful where we +-- use the `TeamTemplates` only for finding invitation url templates (those are not localized). +teamTemplatesNoLocale :: (MonadReader Env m) => m TeamTemplates +teamTemplatesNoLocale = snd <$> teamTemplates Nothing + closeEnv :: Env -> IO () closeEnv e = do Cas.shutdown $ e ^. casClient diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 88cbc80b02a..fb6d1643cfe 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -15,6 +15,7 @@ import Brig.Effects.UserPendingActivationStore.Cassandra (userPendingActivationS import Brig.IO.Intra (runEvents) import Brig.Options (ImplicitNoFederationRestriction (federationDomainConfig), federationDomainConfigs, federationStrategy) import Brig.Options qualified as Opt +import Brig.Team.Template (TeamTemplates) import Cassandra qualified as Cas import Control.Exception (ErrorCall) import Control.Lens (to, (^.)) @@ -120,6 +121,7 @@ type BrigCanonicalEffects = Input UTCTime, Input (Local ()), Input (Maybe AllowlistEmailDomains), + Input TeamTemplates, NotificationSubsystem, GundeckAPIAccess, FederationConfigStore, @@ -195,6 +197,7 @@ runBrigToIO e (AppT ma) = do . interpretFederationDomainConfig (e ^. settings . federationStrategy) (foldMap (remotesMapFromCfgFile . fmap (.federationDomainConfig)) (e ^. settings . federationDomainConfigs)) . runGundeckAPIAccess (e ^. gundeckEndpoint) . runNotificationSubsystemGundeck (defaultNotificationSubsystemConfig (e ^. App.requestId)) + . runInputConst (teamTemplatesNoLocale e) . runInputConst (e ^. settings . Opt.allowlistEmailDomains) . runInputConst (toLocalUnsafe (e ^. settings . Opt.federationDomain) ()) . runInputSem (embed getCurrentTime) diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 55412d7e069..b71bdd02cfe 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -53,6 +53,7 @@ module Brig.Data.User updateStatus, updateRichInfo, updateFeatureConferenceCalling, + updateUserTeam, -- * Deletions deleteEmail, @@ -382,6 +383,12 @@ lookupUserTeam u = (runIdentity =<<) <$> retry x1 (query1 teamSelect (params LocalQuorum (Identity u))) +updateUserTeam :: (MonadClient m) => UserId -> TeamId -> m () +updateUserTeam u t = retry x5 $ write userTeamUpdate (params LocalQuorum (t, u)) + where + userTeamUpdate :: PrepQuery W (TeamId, UserId) () + userTeamUpdate = "UPDATE user SET team = ? WHERE id = ?" + lookupAuth :: (MonadClient m) => UserId -> m (Maybe (Maybe Password, AccountStatus)) lookupAuth u = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity u))) where diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 716272ccf62..d833def04fe 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -217,7 +217,8 @@ updateSearchIndex orig e = embed $ case e of isJust eupAccentId, isJust eupHandle, isJust eupManagedBy, - isJust eupSSOId || eupSSOIdRemoved + isJust eupSSOId || eupSSOIdRemoved, + isJust eupTeam ] when interesting $ Search.reindex orig diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 31d586cf165..a5a1f761fb6 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -224,6 +224,8 @@ instance FromJSON ProviderOpts data TeamOpts = TeamOpts { -- | Team Invitation URL template tInvitationUrl :: !Text, + -- | Existing User Invitation URL template + tExistingUserInvitationUrl :: !Text, -- | Team Activation URL template tActivationUrl :: !Text, -- | Team Creator Welcome URL diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index a7e285ad822..255cc56a6dc 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -31,11 +31,12 @@ import Brig.API.Handler import Brig.API.User (createUserInviteViaScim, fetchUserIdentity) import Brig.API.User qualified as API import Brig.API.Util (logEmail, logInvitationCode) -import Brig.App -import Brig.App qualified as App +import Brig.App as App +import Brig.Data.User as User import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) -import Brig.Options (setMaxTeamSize, setTeamInvitationTimeout) +import Brig.IO.Intra qualified as Intra +import Brig.Options import Brig.Team.Email import Brig.Team.Template import Brig.Team.Util (ensurePermissionToAddUser, ensurePermissions) @@ -46,7 +47,7 @@ import Control.Monad.Trans.Except (mapExceptT) import Data.ByteString.Conversion (toByteString, toByteString') import Data.Id import Data.List1 qualified as List1 -import Data.Qualified (Local) +import Data.Qualified (Local, tUnqualified) import Data.Range import Data.Text.Ascii import Data.Text.Encoding (encodeUtf8) @@ -57,7 +58,7 @@ import Data.Tuple.Extra import Imports hiding (head) import Network.Wai.Utilities hiding (code, message) import Polysemy -import Polysemy.Input (Input) +import Polysemy.Input (Input, input) import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log import Servant hiding (Handler, JSON, addHeader) @@ -66,6 +67,7 @@ import URI.ByteString (Absolute, URIRef, laxURIParserOptions, parseURI) import Util.Logging (logFunction, logTeam) import Wire.API.Error import Wire.API.Error.Brig qualified as E +import Wire.API.Password import Wire.API.Routes.Internal.Brig (FoundInvitationCode (FoundInvitationCode)) import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Named @@ -80,15 +82,18 @@ import Wire.API.Team.Role import Wire.API.Team.Role qualified as Public import Wire.API.User hiding (fromEmail) import Wire.API.User qualified as Public +import Wire.API.UserEvent import Wire.BlockListStore import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem.Template import Wire.Error import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..)) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess -import Wire.InvitationCodeStore (InsertInvitation (..), InvitationCodeStore (..), PaginatedResult (..), StoredInvitation (..)) +import Wire.InvitationCodeStore (InvitationCodeStore (..), PaginatedResult (..), StoredInvitation (..)) import Wire.InvitationCodeStore qualified as Store +import Wire.InvitationCodeStore.Cassandra qualified as Store (mkInvitationCode) import Wire.NotificationSubsystem +import Wire.PasswordStore import Wire.Sem.Concurrency import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore @@ -98,9 +103,16 @@ servantAPI :: ( Member GalleyAPIAccess r, Member UserKeyStore r, Member UserSubsystem r, + Member Store.InvitationCodeStore r, Member EmailSending r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r, Member TinyLog r, - Member Store.InvitationCodeStore r + Member (Embed HttpClientIO) r, + Member NotificationSubsystem r, + Member PasswordStore r, + Member (Input TeamTemplates) r ) => ServerT TeamsAPI (Handler r) servantAPI = @@ -111,6 +123,7 @@ servantAPI = :<|> Named @"get-team-invitation-info" getInvitationByCode :<|> Named @"head-team-invitations" headInvitationByEmail :<|> Named @"get-team-size" teamSizePublic + :<|> Named @"accept-team-invitation" acceptTeamInvitationByPersonalUser teamSizePublic :: (Member GalleyAPIAccess r) => UserId -> TeamId -> (Handler r) TeamSize teamSizePublic uid tid = do @@ -138,10 +151,12 @@ data CreateInvitationInviter = CreateInvitationInviter createInvitation :: ( Member GalleyAPIAccess r, Member UserKeyStore r, + Member InvitationCodeStore r, Member UserSubsystem r, Member EmailSending r, Member TinyLog r, - Member InvitationCodeStore r + Member (Input (Local ())) r, + Member (Input TeamTemplates) r ) => UserId -> TeamId -> @@ -171,14 +186,16 @@ createInvitation uid tid body = do InvitationLocation $ "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' inv.invitationId createInvitationViaScim :: - ( Member BlockListStore r, - Member GalleyAPIAccess r, + ( Member GalleyAPIAccess r, + Member BlockListStore r, Member UserKeyStore r, + Member InvitationCodeStore r, Member (UserPendingActivationStore p) r, Member TinyLog r, - Member EmailSending r, Member UserSubsystem r, - Member InvitationCodeStore r + Member EmailSending r, + Member (Input (Local ())) r, + Member (Input TeamTemplates) r ) => TeamId -> NewUserScimInvitation -> @@ -186,7 +203,7 @@ createInvitationViaScim :: createInvitationViaScim tid newUser@(NewUserScimInvitation _tid uid _eid loc name email role) = do env <- ask let inviteeRole = role - fromEmail = env ^. emailSender + fromEmail = env ^. App.emailSender invreq = InvitationRequest { locale = loc, @@ -225,12 +242,14 @@ logInvitationRequest context action = pure (Right result) createInvitation' :: - ( Member UserSubsystem r, - Member GalleyAPIAccess r, + ( Member GalleyAPIAccess r, + Member UserSubsystem r, Member UserKeyStore r, + Member InvitationCodeStore r, Member EmailSending r, Member TinyLog r, - Member InvitationCodeStore r + Member (Input (Local ())) r, + Member (Input TeamTemplates) r ) => TeamId -> Maybe UserId -> @@ -239,15 +258,20 @@ createInvitation' :: EmailAddress -> Public.InvitationRequest -> Handler r (Public.Invitation, Public.InvitationCode) -createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do - let email = body.inviteeEmail +createInvitation' tid mUid inviteeRole mbInviterUid fromEmail invRequest = do + let email = invRequest.inviteeEmail let uke = mkEmailKey email blacklistedEm <- lift $ liftSem $ isBlocked email when blacklistedEm $ throwStd blacklistedEmail emailTaken <- lift $ liftSem $ isJust <$> lookupKey uke + isPersonalUserMigration <- + if emailTaken + then lift $ liftSem $ isPersonalUser uke + else pure False when emailTaken $ - throwStd emailExists + unless isPersonalUserMigration $ + throwStd emailExists maxSize <- setMaxTeamSize <$> view settings pending <- lift $ liftSem $ Store.countInvitations tid @@ -256,27 +280,44 @@ createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid - iid <- maybe (liftIO randomId) (pure . Id . toUUID) mUid - now <- liftIO =<< view currentTime - timeout <- setTeamInvitationTimeout <$> view settings - let insertInv = - MkInsertInvitation - { invitationId = iid, - teamId = tid, - role = inviteeRole, - createdAt = now, - createdBy = mbInviterUid, - inviteeEmail = email, - inviteeName = body.inviteeName - } - newInv <- - lift . liftSem $ - Store.insertInvitation - insertInv - timeout - lift $ sendInvitationMail email tid fromEmail newInv.code body.locale - inv <- toInvitation showInvitationUrl newInv - pure (inv, newInv.code) + lift $ do + iid <- maybe randomId (pure . Id . toUUID) mUid + now <- liftIO =<< view currentTime + timeout <- setTeamInvitationTimeout <$> view settings + code <- liftIO $ Store.mkInvitationCode + newInv <- + let insertInv = + Store.MkInsertInvitation + { invitationId = iid, + teamId = tid, + role = inviteeRole, + createdAt = now, + createdBy = mbInviterUid, + inviteeEmail = email, + inviteeName = invRequest.inviteeName, + code = code + -- mUrl = mUrl + } + in liftSem $ Store.insertInvitation insertInv timeout + + let sendOp = + if isPersonalUserMigration + then sendInvitationMailPersonalUser + else sendInvitationMail + + sendOp email tid fromEmail code invRequest.locale + inv <- liftSem $ toInvitation isPersonalUserMigration showInvitationUrl newInv + pure (inv, code) + +isPersonalUser :: (Member UserSubsystem r, Member (Input (Local ())) r) => EmailKey -> Sem r Bool +isPersonalUser uke = do + mAccount <- getLocalUserAccountByUserKey =<< qualifyLocal' uke + pure $ case mAccount of + -- this can e.g. happen if the key is claimed but the account is not yet created + Nothing -> False + Just account -> + account.accountStatus == Active + && isNothing account.accountUser.userTeam deleteInvitation :: (Member GalleyAPIAccess r, Member InvitationCodeStore r) => @@ -289,9 +330,13 @@ deleteInvitation uid tid iid = do lift . liftSem $ Store.deleteInvitation tid iid listInvitations :: + forall r. ( Member GalleyAPIAccess r, Member TinyLog r, - Member InvitationCodeStore r + Member InvitationCodeStore r, + Member (Input TeamTemplates) r, + Member (Input (Local ())) r, + Member UserSubsystem r ) => UserId -> TeamId -> @@ -301,25 +346,38 @@ listInvitations :: listInvitations uid tid startingId mSize = do ensurePermissions uid tid [AddTeamMember] showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid - let toInvitations is = mapM (toInvitation showInvitationUrl) is + let toInvitations is = mapM (toInvitationHack showInvitationUrl) is lift (liftSem $ Store.lookupInvitationsPaginated mSize tid startingId) >>= \case PaginatedResultHasMore storedInvs -> do - invs <- toInvitations storedInvs + invs <- lift . liftSem $ toInvitations storedInvs pure $ InvitationList invs True PaginatedResult storedInvs -> do - invs <- toInvitations storedInvs + invs <- lift . liftSem $ toInvitations storedInvs pure $ InvitationList invs False + where + -- 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 is ignored here + toInvitationHack ShowInvitationUrl si = do + isPersonalUserMigration <- isPersonalUser (mkEmailKey si.email) + toInvitation isPersonalUserMigration 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 :: - ( Member TinyLog r + ( Member TinyLog r, + Member (Input TeamTemplates) r ) => + Bool -> ShowOrHideInvitationUrl -> StoredInvitation -> - (Handler r) Invitation -toInvitation showUrl storedInv = do - url <- mkInviteUrl showUrl storedInv.teamId storedInv.code + 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, @@ -332,36 +390,64 @@ toInvitation showUrl storedInv = do inviteeUrl = url } -mkInviteUrl :: +getInviteUrl :: + forall r. (Member TinyLog r) => - ShowOrHideInvitationUrl -> + InvitationEmailTemplate -> TeamId -> - InvitationCode -> - (Handler r) (Maybe (URIRef Absolute)) -mkInviteUrl HideInvitationUrl _ _ = pure Nothing -mkInviteUrl ShowInvitationUrl team (InvitationCode c) = do - template <- invitationEmailUrl . invitationEmail . snd <$> teamTemplates Nothing - branding <- view App.templateBranding + AsciiText Base64Url -> + 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 parseHttpsUrl url where replace "team" = idToText team - replace "code" = toText c + replace "code" = toText code replace x = x - parseHttpsUrl :: (Member TinyLog r) => Text -> (Handler r) (Maybe (URIRef Absolute)) + + parseHttpsUrl :: Text -> Sem r (Maybe (URIRef Absolute)) parseHttpsUrl url = - either (\e -> lift . liftSem $ logError url e >> pure Nothing) (pure . Just) $ + either (\e -> Nothing <$ logError url e) (pure . Just) $ parseURI laxURIParserOptions (encodeUtf8 url) + logError url e = Log.err $ Log.msg @Text "Unable to create invitation url. Please check configuration." . 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 InvitationCodeStore r, - Member TinyLog r + Member TinyLog r, + Member (Input TeamTemplates) r ) => UserId -> TeamId -> @@ -375,7 +461,7 @@ getInvitation uid tid iid = do Nothing -> pure Nothing Just invitation -> do showInvitationUrl <- lift . liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid - maybeUrl <- mkInviteUrl showInvitationUrl tid invitation.code + maybeUrl <- lift . liftSem $ mkInviteUrl showInvitationUrl tid invitation.code pure $ Just (Store.invitationFromStored maybeUrl invitation) getInvitationByCode :: @@ -475,3 +561,53 @@ changeTeamAccountStatuses tid s = do where toList1 (x : xs) = pure $ List1.list1 x xs toList1 [] = throwStd (notFound "Team not found or no members") + +acceptTeamInvitationByPersonalUser :: + forall r. + ( Member UserSubsystem r, + Member GalleyAPIAccess r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r, + Member TinyLog r, + Member (Embed HttpClientIO) r, + Member NotificationSubsystem r, + Member InvitationCodeStore r, + Member PasswordStore r + ) => + Local UserId -> + AcceptTeamInvitation -> + (Handler r) () +acceptTeamInvitationByPersonalUser luid req = do + (mek, mTid) <- do + mSelfProfile <- lift $ liftSem $ getSelfProfile luid + let mek = mkEmailKey <$> (userEmail . selfUser =<< mSelfProfile) + mTid = mSelfProfile >>= userTeam . selfUser + pure (mek, mTid) + checkPassword + (inv, (.teamId) -> tid) <- API.findTeamInvitation mek req.code !>> toInvitationError + let minvmeta = (,inv.createdAt) <$> inv.createdBy + uid = tUnqualified luid + for_ mTid $ \userTid -> + unless (tid == userTid) $ + throwStd (errorToWai @'E.CannotJoinMultipleTeams) + added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid minvmeta (fromMaybe defaultRole inv.role) + unless added $ throwStd (errorToWai @'E.TooManyTeamMembers) + lift $ do + wrapClient $ User.updateUserTeam uid tid + liftSem $ Store.deleteInvitation inv.teamId inv.invitationId + liftSem $ Intra.onUserEvent uid Nothing (teamUpdated uid tid) + where + checkPassword = do + p <- + lift (liftSem . lookupHashedPassword . tUnqualified $ luid) + >>= maybe (throwStd (errorToWai @'E.MissingAuth)) pure + unless (verifyPassword req.password p) $ + throwStd (errorToWai @'E.BadCredentials) + toInvitationError :: RegisterError -> HttpError + toInvitationError = \case + RegisterErrorMissingIdentity -> StdError (errorToWai @'E.MissingIdentity) + RegisterErrorInvalidActivationCodeWrongUser -> StdError (errorToWai @'E.InvalidActivationCodeWrongUser) + RegisterErrorInvalidActivationCodeWrongCode -> StdError (errorToWai @'E.InvalidActivationCodeWrongCode) + RegisterErrorInvalidInvitationCode -> StdError (errorToWai @'E.InvalidInvitationCode) + _ -> StdError (notFound "Something went wrong, while looking up the invitation") diff --git a/services/brig/src/Brig/Team/Email.hs b/services/brig/src/Brig/Team/Email.hs index d76a6671f68..9bfd2d653f3 100644 --- a/services/brig/src/Brig/Team/Email.hs +++ b/services/brig/src/Brig/Team/Email.hs @@ -22,6 +22,7 @@ module Brig.Team.Email CreatorWelcomeEmail (..), MemberWelcomeEmail (..), sendInvitationMail, + sendInvitationMailPersonalUser, sendMemberWelcomeMail, ) where @@ -39,9 +40,6 @@ import Wire.API.User import Wire.EmailSending import Wire.EmailSubsystem.Template (TemplateBranding, renderHtmlWithBranding, renderTextWithBranding) -------------------------------------------------------------------------------- --- Invitation Email - sendInvitationMail :: (Member EmailSending r) => EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> (AppT r) () sendInvitationMail to tid from code loc = do tpl <- invitationEmail . snd <$> teamTemplates loc @@ -49,6 +47,13 @@ sendInvitationMail to tid from code loc = do let mail = InvitationEmail to tid code from liftSem $ sendMail $ renderInvitationEmail mail tpl branding +sendInvitationMailPersonalUser :: (Member EmailSending r) => EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> (AppT r) () +sendInvitationMailPersonalUser to tid from code loc = do + tpl <- existingUserInvitationEmail . snd <$> teamTemplates loc + branding <- view templateBranding + let mail = InvitationEmail to tid code from + liftSem $ sendMail $ renderInvitationEmail mail tpl branding + sendMemberWelcomeMail :: (Member EmailSending r) => EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () sendMemberWelcomeMail to tid teamName loc = do tpl <- memberWelcomeEmail . snd <$> teamTemplates loc diff --git a/services/brig/src/Brig/Team/Template.hs b/services/brig/src/Brig/Team/Template.hs index d725ec556f4..129ca30ef37 100644 --- a/services/brig/src/Brig/Team/Template.hs +++ b/services/brig/src/Brig/Team/Template.hs @@ -61,6 +61,7 @@ data MemberWelcomeEmailTemplate = MemberWelcomeEmailTemplate data TeamTemplates = TeamTemplates { invitationEmail :: !InvitationEmailTemplate, + existingUserInvitationEmail :: !InvitationEmailTemplate, creatorWelcomeEmail :: !CreatorWelcomeEmailTemplate, memberWelcomeEmail :: !MemberWelcomeEmailTemplate } @@ -75,6 +76,13 @@ loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \ <*> pure (emailSender gOptions) <*> 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" + <*> pure (emailSender gOptions) + <*> readText fp "email/sender.txt" + ) <*> ( CreatorWelcomeEmailTemplate (tCreatorWelcomeUrl tOptions) <$> readTemplate fp "email/new-creator-welcome-subject.txt" <*> readTemplate fp "email/new-creator-welcome.txt" @@ -93,6 +101,7 @@ loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \ gOptions = general (emailSMS o) tOptions = team (emailSMS o) tUrl = template $ tInvitationUrl tOptions + tExistingUrl = template $ tExistingUserInvitationUrl tOptions defLocale = setDefaultTemplateLocale (optSettings o) readTemplate = readTemplateWithDefault (templateDir gOptions) defLocale "team" readText = readTextWithDefault (templateDir gOptions) defLocale "team" diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 24d8ec75016..afaca2554fd 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -706,6 +706,7 @@ lookupForIndex u = do "SELECT \ \id, \ \team, \ + \writetime(team), \ \name, \ \writetime(name), \ \status, \ @@ -757,6 +758,7 @@ scanForIndex num = do "SELECT \ \id, \ \team, \ + \writetime(team), \ \name, \ \writetime(name), \ \status, \ @@ -784,6 +786,7 @@ type Activated = Bool type ReindexRow = ( UserId, Maybe TeamId, + Maybe (Writetime TeamId), Name, Writetime Name, Maybe AccountStatus, @@ -808,12 +811,13 @@ type ReindexRow = -- the _2 lens does not work for a tuple this big teamInReindexRow :: ReindexRow -> Maybe TeamId -teamInReindexRow (_f1, f2, _f3, _f4, _f5, _f6, _f7, _f8, _f9, _f10, _f11, _f12, _f13, _f14, _f15, _f16, _f17, _f18, _f19, _f20, _f21, _f22) = f2 +teamInReindexRow (_f1, f2, _f3, _f4, _f5, _f6, _f7, _f8, _f9, _f10, _f11, _f12, _f13, _f14, _f15, _f16, _f17, _f18, _f19, _f20, _f21, _f22, _f23) = f2 reindexRowToIndexUser :: forall m. (MonadThrow m) => ReindexRow -> SearchVisibilityInbound -> m IndexUser reindexRowToIndexUser ( u, mteam, + tTeam, name, tName, status, @@ -849,7 +853,8 @@ reindexRowToIndexUser v <$> tService, v <$> tManagedBy, v <$> tSsoId, - v <$> tEmailUnvalidated + v <$> tEmailUnvalidated, + v <$> tTeam ] pure $ if shouldIndex diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index 41be5df60bf..95b560f7b1d 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -211,6 +211,11 @@ http { proxy_pass http://brig; } + location ~* ^(/v[0-9]+)?/teams/invitations/accept$ { + include common_response_with_zauth.conf; + proxy_pass http://brig; + } + location ~* ^(/v[0-9]+)?/teams/invitations/([^/]*)$ { include common_response_no_zauth.conf; proxy_pass http://brig; From 089daca23b8a15173e1b20ed4a10c800042d0c30 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 17 Sep 2024 09:57:25 +0200 Subject: [PATCH 066/136] Move test documentation tags from proteus to MLS (#4240) * Parametrise testAccessUpdateGuestRemoved Add `ConversationProtocol` parameter so that this test is run with both supported protocols (Proteus and MLS). * Move test doc tags from proteus to MLS Deleted some of the documentation start and ending tags from Proteus tests and added them to some of the MLS tests. Deleted tags from: - `postCryptoMessageVerifyMsgSentAndRejectIfMissingClient` - `postCryptoMessageVerifyRejectMissingClientAndRepondMissingPrekeysJson` - `postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysProto` - `postMessageClientNotInGroupDoesNotReceiveMsg` - `postMessageRejectIfMissingClients` - `postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam` - `postMessageQualifiedLocalOwningBackendMissingClients` Added tags to - `testSenderNotInConversation` - `testApplicationMessage` - `testExternalCommitNotMember ` - `testCommitNotReferencingAllProposals ` - `testAddUserPartial` * Delete more test tags * Add CHANGELOG entry --- changelog.d/4-docs/mls-test-tags | 1 + integration/test/Test/AccessUpdate.hs | 55 +++++++++++++++------ integration/test/Test/MLS.hs | 12 +++++ integration/test/Test/MLS/Message.hs | 9 +++- services/galley/test/integration/API.hs | 42 ---------------- services/galley/test/integration/API/MLS.hs | 11 +++++ 6 files changed, 73 insertions(+), 57 deletions(-) create mode 100644 changelog.d/4-docs/mls-test-tags diff --git a/changelog.d/4-docs/mls-test-tags b/changelog.d/4-docs/mls-test-tags new file mode 100644 index 00000000000..56e9b4b3b0a --- /dev/null +++ b/changelog.d/4-docs/mls-test-tags @@ -0,0 +1 @@ +Deleted proteus-specific test documentation tags and added some new tags to MLS tests diff --git a/integration/test/Test/AccessUpdate.hs b/integration/test/Test/AccessUpdate.hs index c63c10cbd0b..01113946788 100644 --- a/integration/test/Test/AccessUpdate.hs +++ b/integration/test/Test/AccessUpdate.hs @@ -22,6 +22,7 @@ import API.Galley import Control.Monad.Codensity import Control.Monad.Reader import GHC.Stack +import MLS.Util import Notifications import SetupHelpers import Testlib.Prelude @@ -38,29 +39,55 @@ testBaz :: HasCallStack => App () testBaz = pure () -} +data ConversationProtocol + = ConversationProtocolProteus + | ConversationProtocolMLS + +instance TestCases ConversationProtocol where + mkTestCases = + pure + [ MkTestCase "[proto=proteus]" ConversationProtocolProteus, + MkTestCase "[proto=mls]" ConversationProtocolMLS + ] + -- | @SF.Federation @SF.Separation @TSFI.RESTfulAPI @S2 -- -- The test asserts that, among others, remote users are removed from a -- conversation when an access update occurs that disallows guests from -- accessing. -testAccessUpdateGuestRemoved :: (HasCallStack) => App () -testAccessUpdateGuestRemoved = do +testAccessUpdateGuestRemoved :: (HasCallStack) => ConversationProtocol -> App () +testAccessUpdateGuestRemoved proto = do (alice, tid, [bob]) <- createTeam OwnDomain 2 charlie <- randomUser OwnDomain def dee <- randomUser OtherDomain def mapM_ (connectTwoUsers alice) [charlie, dee] - [aliceClient, bobClient, charlieClient, deeClient] <- - mapM - (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) - [alice, bob, charlie, dee] - conv <- - postConversation - alice - defProteus - { qualifiedUsers = [bob, charlie, dee], - team = Just tid - } - >>= getJSON 201 + + (conv, [aliceClient, bobClient, charlieClient, deeClient]) <- case proto of + ConversationProtocolProteus -> do + clients <- + mapM + (\user -> objId $ bindResponse (addClient user def) $ getJSON 201) + [alice, bob, charlie, dee] + conv <- + postConversation + alice + defProteus + { qualifiedUsers = [bob, charlie, dee], + team = Just tid + } + >>= getJSON 201 + pure (conv, clients) + ConversationProtocolMLS -> do + alice1 <- createMLSClient def alice + clients <- traverse (createMLSClient def) [bob, charlie, dee] + traverse_ uploadNewKeyPackage clients + + conv <- postConversation alice1 defMLS {team = Just tid} >>= getJSON 201 + createGroup alice1 conv + + void $ createAddCommit alice1 [bob, charlie, dee] >>= sendAndConsumeCommitBundle + convId <- conv %. "qualified_id" + pure (convId, 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/MLS.hs b/integration/test/Test/MLS.hs index 91b11cb04e0..f721f9ad06b 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -529,6 +529,10 @@ testFirstCommitAllowsPartialAdds = do resp.status `shouldMatchInt` 409 resp.json %. "label" `shouldMatch` "mls-client-mismatch" +-- @SF.Separation @TSFI.RESTfulAPI @S2 +-- +-- This test verifies that the server rejects a commit containing add proposals +-- that only add a proper subset of the set of clients of a user. testAddUserPartial :: (HasCallStack) => App () testAddUserPartial = do [alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) @@ -556,6 +560,8 @@ testAddUserPartial = do err <- postMLSCommitBundle mp.sender (mkBundle mp) >>= getJSON 409 err %. "label" `shouldMatch` "mls-client-mismatch" +-- @END + -- | admin removes user from a conversation but doesn't list all clients testRemoveClientsIncomplete :: (HasCallStack) => App () testRemoveClientsIncomplete = do @@ -741,6 +747,10 @@ testPropExistingConv = do res <- createAddProposals alice1 [bob] >>= traverse sendAndConsumeMessage >>= assertOne shouldBeEmpty (res %. "events") +-- @SF.Separation @TSFI.RESTfulAPI @S2 +-- +-- This test verifies that the server rejects any commit that does not +-- reference all pending proposals in an MLS group. testCommitNotReferencingAllProposals :: (HasCallStack) => App () testCommitNotReferencingAllProposals = do users@[_alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain) @@ -765,6 +775,8 @@ testCommitNotReferencingAllProposals = do resp.status `shouldMatchInt` 400 resp.json %. "label" `shouldMatch` "mls-commit-missing-references" +-- @END + testUnsupportedCiphersuite :: (HasCallStack) => App () testUnsupportedCiphersuite = do setMLSCiphersuite (Ciphersuite "0x0003") diff --git a/integration/test/Test/MLS/Message.hs b/integration/test/Test/MLS/Message.hs index e15635f4987..81a194d3674 100644 --- a/integration/test/Test/MLS/Message.hs +++ b/integration/test/Test/MLS/Message.hs @@ -26,9 +26,14 @@ import Notifications import SetupHelpers import Testlib.Prelude --- | Test happy case of federated MLS message sending in both directions. +-- @SF.Separation @TSFI.RESTfulAPI @S2 +-- This test verifies whether a message actually gets sent all the way to +-- cannon. + testApplicationMessage :: (HasCallStack) => App () testApplicationMessage = do + -- Test happy case of federated MLS message sending in both directions. + -- local alice and alex, remote bob [alice, alex, bob, betty] <- createUsers @@ -55,6 +60,8 @@ testApplicationMessage = do void $ createApplicationMessage bob1 "hey" >>= sendAndConsumeMessage traverse_ (awaitMatch isNewMLSMessageNotif) wss +-- @END + testAppMessageSomeReachable :: (HasCallStack) => App () testAppMessageSomeReachable = do alice1 <- startDynamicBackends [mempty] $ \[thirdDomain] -> do diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 8a7894ff2ac..9d5ef6b1a4d 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -409,9 +409,6 @@ postConvWithUnreachableRemoteUsers rbs = do groupConvs WS.assertNoEvent (3 # Second) [wsAlice, wsAlex] -- TODO: sometimes, (at least?) one of these users gets a "connection accepted" event. --- @SF.Separation @TSFI.RESTfulAPI @S2 --- This test verifies whether a message actually gets sent all the way to --- cannon. postCryptoMessageVerifyMsgSentAndRejectIfMissingClient :: TestM () postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do localDomain <- viewFederationDomain @@ -498,10 +495,6 @@ postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do liftIO $ assertBool "unexpected equal clients" (bc /= bc2) assertNoMsg wsB2 (wsAssertOtr qconv qalice ac bc cipher) --- @END - --- @SF.Separation @TSFI.RESTfulAPI @S2 --- This test verifies basic mismatch behavior of the the JSON endpoint. postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysJson :: TestM () postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysJson = do (alice, ac) <- randomUserWithClient (head someLastPrekeys) @@ -526,10 +519,6 @@ postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysJson = do Map.keys (userClientMap (getUserClientPrekeyMap p)) @=? [eve] Map.keys <$> Map.lookup eve (userClientMap (getUserClientPrekeyMap p)) @=? Just [ec] --- @END - --- @SF.Separation @TSFI.RESTfulAPI @S2 --- This test verifies basic mismatch behaviour of the protobuf endpoint. postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysProto :: TestM () postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysProto = do (alice, ac) <- randomUserWithClient (head someLastPrekeys) @@ -556,8 +545,6 @@ postCryptoMessageVerifyRejectMissingClientAndRespondMissingPrekeysProto = do Map.keys (userClientMap (getUserClientPrekeyMap p)) @=? [eve] Map.keys <$> Map.lookup eve (userClientMap (getUserClientPrekeyMap p)) @=? Just [ec] --- @END - -- | This test verifies behaviour when an unknown client posts the message. Only -- tests the Protobuf endpoint. postCryptoMessageNotAuthorizeUnknownClient :: TestM () @@ -573,10 +560,6 @@ postCryptoMessageNotAuthorizeUnknownClient = do postProtoOtrMessage alice (ClientId 0x172618352518396) conv m !!! const 403 === statusCode --- @SF.Separation @TSFI.RESTfulAPI @S2 --- This test verifies the following scenario. --- A client sends a message to all clients of a group and one more who is not part of the group. --- The server must not send this message to client ids not part of the group. postMessageClientNotInGroupDoesNotReceiveMsg :: TestM () postMessageClientNotInGroupDoesNotReceiveMsg = do localDomain <- viewFederationDomain @@ -599,11 +582,6 @@ postMessageClientNotInGroupDoesNotReceiveMsg = do checkEveGetsMsg checkChadDoesNotGetMsg --- @END - --- @SF.Separation @TSFI.RESTfulAPI @S2 --- This test verifies that when a client sends a message not to all clients of a group then the server should reject the message and sent a notification to the sender (412 Missing clients). --- The test is somewhat redundant because this is already tested as part of other tests already. This is a stand alone test that solely tests the behavior described above. postMessageRejectIfMissingClients :: TestM () postMessageRejectIfMissingClients = do (sender, senderClient) : allReceivers <- randomUserWithClient `traverse` someLastPrekeys @@ -629,11 +607,6 @@ postMessageRejectIfMissingClients = do mkMsg :: ByteString -> (UserId, ClientId) -> (UserId, ClientId, Text) mkMsg text (uid, clientId) = (uid, clientId, toBase64Text text) --- @END - --- @SF.Separation @TSFI.RESTfulAPI @S2 --- This test verifies behaviour under various values of ignore_missing and --- report_missing. Only tests the JSON endpoint. postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam :: TestM () postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam = do (alice, ac) <- randomUserWithClient (head someLastPrekeys) @@ -689,12 +662,6 @@ postCryptoMessageVerifyCorrectResponseIfIgnoreAndReportMissingQueryParam = do where listToByteString = BS.intercalate "," . map toByteString' --- @END - --- @SF.Separation @TSFI.RESTfulAPI @S2 --- Sets up a conversation on Backend A known as "owning backend". One of the --- users from Backend A will send the message but have a missing client. It is --- expected that the message will not be sent. postMessageQualifiedLocalOwningBackendMissingClients :: TestM () postMessageQualifiedLocalOwningBackendMissingClients = do -- Cannon for local users @@ -752,8 +719,6 @@ postMessageQualifiedLocalOwningBackendMissingClients = do assertMismatchQualified mempty expectedMissing mempty mempty mempty WS.assertNoEvent (1 # Second) [wsBob, wsChad] --- @END - -- | Sets up a conversation on Backend A known as "owning backend". One of the -- users from Backend A will send the message, it is expected that message will -- be sent successfully. @@ -844,11 +809,6 @@ postMessageQualifiedLocalOwningBackendRedundantAndDeletedClients = do -- Wait less for no message WS.assertNoEvent (1 # Second) [wsNonMember] --- @SF.Separation @TSFI.RESTfulAPI @S2 --- Sets up a conversation on Backend A known as "owning backend". One of the --- users from Backend A will send the message but have a missing client. It is --- expected that the message will be sent except when it is specifically --- requested to report on missing clients of a user. postMessageQualifiedLocalOwningBackendIgnoreMissingClients :: TestM () postMessageQualifiedLocalOwningBackendIgnoreMissingClients = do -- WS receive timeout @@ -971,8 +931,6 @@ postMessageQualifiedLocalOwningBackendIgnoreMissingClients = do assertMismatchQualified mempty expectedMissing mempty mempty mempty WS.assertNoEvent (1 # Second) [wsBob, wsChad] --- @END - postMessageQualifiedLocalOwningBackendFailedToSendClients :: TestM () postMessageQualifiedLocalOwningBackendFailedToSendClients = do -- WS receive timeout diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index dcb01c32c56..a8a5e74d4d4 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -252,6 +252,9 @@ postMLSConvOk = do qcid <- assertConv rsp RegularConv (Just alice) qalice [] (Just nameMaxSize) Nothing checkConvCreateEvent (qUnqualified qcid) wsA +-- @SF.Separation @TSFI.RESTfulAPI @S2 +-- +-- This test verifies that a user must be a member of an MLS conversation in order to send messages to it. testSenderNotInConversation :: TestM () testSenderNotInConversation = do -- create users @@ -279,6 +282,8 @@ testSenderNotInConversation = do liftIO $ Wai.label err @?= "no-conversation" +-- @END + testAddUserWithBundle :: TestM () testAddUserWithBundle = do [alice, bob] <- createAndConnectUsers [Nothing, Nothing] @@ -665,6 +670,10 @@ testLocalToRemoteNonMember = do const (Just "no-conversation-member") === fmap Wai.label . responseJsonError +-- @SF.Separation @TSFI.RESTfulAPI @S2 +-- +-- This test verifies that only the members of an MLS conversation are allowed +-- to join via external commit. testExternalCommitNotMember :: TestM () testExternalCommitNotMember = do [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) @@ -683,6 +692,8 @@ testExternalCommitNotMember = do localPostCommitBundle (mpSender mp) bundle !!! const 404 === statusCode +-- @END + testExternalCommitSameClient :: TestM () testExternalCommitSameClient = do [alice, bob] <- createAndConnectUsers (replicate 2 Nothing) From 3b191f05498d75e3ae3b88cb97e2f6b184f82908 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 17 Sep 2024 11:39:52 +0200 Subject: [PATCH 067/136] Fix clrProxy field of MLSE2EId feature flag (#4233) * Fix rowToFeature for MlsE2EI A missing `crlProxy` field should not override the default value. Same for `acmeDiscoveryUrl`. * WIP: add crlProxy to test configuration * hi ci * Make reading config from db more consistent * fixup! WIP: add crlProxy to test configuration * update test config for galley for CI tests * changelog --------- Co-authored-by: Stefan Berthold Co-authored-by: Leif Battermann --- changelog.d/3-bug-fixes/WBP-8790 | 1 + hack/helm_vars/wire-server/values.yaml.gotmpl | 8 +++ integration/test/Test/FeatureFlags.hs | 15 +++-- services/galley/galley.integration.yaml | 1 + .../src/Galley/Cassandra/MakeFeature.hs | 63 ++++++++++++++++--- 5 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WBP-8790 diff --git a/changelog.d/3-bug-fixes/WBP-8790 b/changelog.d/3-bug-fixes/WBP-8790 new file mode 100644 index 00000000000..76b0c27b8a6 --- /dev/null +++ b/changelog.d/3-bug-fixes/WBP-8790 @@ -0,0 +1 @@ +Fix handling of defaults of `mlsE2EID` feature config diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 2ed14739f79..0e1b9604a64 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -278,6 +278,14 @@ galley: usersThreshold: 100 clientsThreshold: 50 lockStatus: locked + mlsE2EId: + defaults: + status: disabled + config: + verificationExpiration: 86400 + acmeDiscoveryUrl: null + crlProxy: https://crlproxy.example.com + lockStatus: unlocked limitedEventFanout: defaults: status: disabled diff --git a/integration/test/Test/FeatureFlags.hs b/integration/test/Test/FeatureFlags.hs index 4b285e6bef2..6d088b1ce84 100644 --- a/integration/test/Test/FeatureFlags.hs +++ b/integration/test/Test/FeatureFlags.hs @@ -228,7 +228,11 @@ testMlsE2EConfigCrlProxyNotRequiredInV5 = do resp.status `shouldMatchInt` 200 -- Assert that the feature config got updated correctly - expectedResponse <- configWithoutCrlProxy & setField "lockStatus" "unlocked" & setField "ttl" "unlimited" + expectedResponse <- + configWithoutCrlProxy + & setField "lockStatus" "unlocked" + & setField "ttl" "unlimited" + & setField "config.crlProxy" "https://crlproxy.example.com" checkFeature "mlsE2EId" owner tid expectedResponse testSSODisabledByDefault :: (HasCallStack) => App () @@ -462,7 +466,8 @@ testAllFeatures = do "config" .= object [ "verificationExpiration" .= A.Number 86400, - "useProxyOnMobile" .= False + "useProxyOnMobile" .= False, + "crlProxy" .= "https://crlproxy.example.com" ] ], "mlsMigration" @@ -747,7 +752,8 @@ mlsE2EIdConfig = do "config" .= object [ "verificationExpiration" .= A.Number 86400, - "useProxyOnMobile" .= False + "useProxyOnMobile" .= False, + "crlProxy" .= "https://crlproxy.example.com" ] ] mlsE2EIdConfig1 :: Value @@ -1028,7 +1034,8 @@ testPatchE2EId = do "config" .= object [ "verificationExpiration" .= A.Number 86400, - "useProxyOnMobile" .= False + "useProxyOnMobile" .= False, + "crlProxy" .= "https://crlproxy.example.com" ] ] _testPatch "mlsE2EId" True defCfg (object ["lockStatus" .= "locked"]) diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index 6439e5ba7de..c07a9c78056 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -93,6 +93,7 @@ settings: config: verificationExpiration: 86400 acmeDiscoveryUrl: null + crlProxy: https://crlproxy.example.com lockStatus: unlocked mlsMigration: defaults: diff --git a/services/galley/src/Galley/Cassandra/MakeFeature.hs b/services/galley/src/Galley/Cassandra/MakeFeature.hs index 2db777f2521..c090a97bdb0 100644 --- a/services/galley/src/Galley/Cassandra/MakeFeature.hs +++ b/services/galley/src/Galley/Cassandra/MakeFeature.hs @@ -23,6 +23,20 @@ import Wire.API.Conversation.Protocol (ProtocolTag) import Wire.API.MLS.CipherSuite import Wire.API.Team.Feature +-- [Note: default values for configuration fields] +-- +-- When reading values for configuration types with multiple fields, we fall +-- back to default values for each field independently, instead of treating the +-- whole configuration as a single value that can be set or not. +-- +-- In most cases, either strategy would produce the same result, because there +-- is no way to set only *some* fields using the public API. However, that can +-- happen when a feature flag changes over time and gains new fields, as it has +-- been the case for mlsE2EId. +-- +-- Therefore, we use the first strategy consistently for all feature flags, +-- even when it does not matter. + -- | This is necessary in order to convert an @NP f xs@ type to something that -- CQL can understand. -- @@ -90,7 +104,13 @@ instance MakeFeature AppLockConfig where rowToFeature (status :* enforce :* timeout :* Nil) = foldMap dbFeatureStatus status - <> foldMap dbFeatureConfig (AppLockConfig <$> enforce <*> timeout) + -- [Note: default values for configuration fields] + <> dbFeatureModConfig + ( \defCfg -> + AppLockConfig + (fromMaybe defCfg.applockEnforceAppLock enforce) + (fromMaybe defCfg.applockInactivityTimeoutSecs timeout) + ) featureToRow feat = Just feat.status @@ -226,13 +246,34 @@ instance MakeFeature MLSConfig where ) = foldMap dbFeatureLockStatus lockStatus <> foldMap dbFeatureStatus status - <> foldMap - dbFeatureConfig - ( MLSConfig (foldMap C.fromSet toggleUsers) - <$> defProto - <*> pure (foldMap C.fromSet ciphersuites) - <*> defCiphersuite - <*> pure (foldMap C.fromSet supportedProtos) + <> dbFeatureModConfig + ( \defCfg -> + -- [Note: default values for configuration fields] + -- + -- This case is a bit special, because Cassandra sets do not + -- distinguish between 'null' and 'empty'. To differentiate + -- between these cases, we use the `mls_default_protocol` field: + -- if set, we interpret null sets as empty, otherwise we use the + -- default. + let configIsSet = isJust defProto + in MLSConfig + ( maybe + (if configIsSet then [] else defCfg.mlsProtocolToggleUsers) + C.fromSet + toggleUsers + ) + (fromMaybe defCfg.mlsDefaultProtocol defProto) + ( maybe + (if configIsSet then [] else defCfg.mlsAllowedCipherSuites) + C.fromSet + ciphersuites + ) + (fromMaybe defCfg.mlsDefaultCipherSuite defCiphersuite) + ( maybe + (if configIsSet then [] else defCfg.mlsSupportedProtocols) + C.fromSet + supportedProtos + ) ) featureToRow feat = @@ -280,8 +321,8 @@ instance MakeFeature MlsE2EIdConfig where defCfg { verificationExpiration = maybe defCfg.verificationExpiration fromIntegral gracePeriod, - acmeDiscoveryUrl = acmeDiscoveryUrl, - crlProxy = crlProxy, + acmeDiscoveryUrl = acmeDiscoveryUrl <|> defCfg.acmeDiscoveryUrl, + crlProxy = crlProxy <|> defCfg.crlProxy, useProxyOnMobile = fromMaybe defCfg.useProxyOnMobile useProxyOnMobile } ) @@ -310,6 +351,7 @@ instance MakeFeature MlsMigrationConfig where rowToFeature (lockStatus :* status :* startTime :* finalizeAfter :* Nil) = foldMap dbFeatureLockStatus lockStatus <> foldMap dbFeatureStatus status + -- FUTUREWORK: allow using the default <> dbFeatureConfig (MlsMigrationConfig startTime finalizeAfter) featureToRow feat = @@ -331,6 +373,7 @@ instance MakeFeature EnforceFileDownloadLocationConfig where rowToFeature (lockStatus :* status :* location :* Nil) = foldMap dbFeatureLockStatus lockStatus <> foldMap dbFeatureStatus status + -- FUTUREWORK: allow using the default <> dbFeatureConfig (EnforceFileDownloadLocationConfig location) featureToRow feat = Just feat.lockStatus From 95ce0d85a43066a3505350d53fa0f9a5b8c29067 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 17 Sep 2024 14:14:57 +0200 Subject: [PATCH 068/136] WPB-10660 Enable and deploy background worker in non federation environments (#4243) --- changelog.d/0-release-notes/WPB-10660 | 1 + changelog.d/5-internal/background-worker | 1 + .../templates/configmap.yaml | 2 + charts/background-worker/values.yaml | 1 + charts/brig/values.yaml | 2 +- charts/cargohold/values.yaml | 2 +- charts/galley/values.yaml | 2 +- charts/wire-server/requirements.yaml | 1 - charts/wire-server/values.yaml | 2 +- docs/src/understand/configure-federation.md | 6 +- hack/helm_vars/wire-server/values.yaml.gotmpl | 3 +- libs/extended/src/Network/AMQP/Extended.hs | 22 +++---- .../src/Wire/BackendNotificationPusher.hs | 61 +++++++++++-------- .../src/Wire/BackgroundWorker.hs | 4 +- .../src/Wire/BackgroundWorker/Env.hs | 6 +- .../src/Wire/BackgroundWorker/Options.hs | 12 +++- .../Wire/BackendNotificationPusherSpec.hs | 8 +-- services/brig/src/Brig/Options.hs | 2 +- services/galley/src/Galley/Options.hs | 2 +- 19 files changed, 84 insertions(+), 56 deletions(-) create mode 100644 changelog.d/0-release-notes/WPB-10660 create mode 100644 changelog.d/5-internal/background-worker diff --git a/changelog.d/0-release-notes/WPB-10660 b/changelog.d/0-release-notes/WPB-10660 new file mode 100644 index 00000000000..17305b2882f --- /dev/null +++ b/changelog.d/0-release-notes/WPB-10660 @@ -0,0 +1 @@ +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/5-internal/background-worker b/changelog.d/5-internal/background-worker new file mode 100644 index 00000000000..d699ef088a6 --- /dev/null +++ b/changelog.d/5-internal/background-worker @@ -0,0 +1 @@ +charts/wire-server: Deploy background-worker even when tags.federation is `false` diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml index fea77ab59d5..8840a43764e 100644 --- a/charts/background-worker/templates/configmap.yaml +++ b/charts/background-worker/templates/configmap.yaml @@ -26,7 +26,9 @@ data: host: {{ .host }} port: {{ .port }} vHost: {{ .vHost }} + {{- if $.Values.config.enableFederation }} adminPort: {{ .adminPort }} + {{- end }} enableTls: {{ .enableTls }} insecureSkipVerifyTls: {{ .insecureSkipVerifyTls }} {{- if .tlsCaSecretRef }} diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml index e38cd9c8225..8b79f6af6be 100644 --- a/charts/background-worker/values.yaml +++ b/charts/background-worker/values.yaml @@ -18,6 +18,7 @@ metrics: config: logLevel: Info logFormat: StructuredJSON + enableFederation: false # keep in sync with brig, cargohold and galley charts' config.enableFederation as well as wire-server chart's tags.federation rabbitmq: host: rabbitmq port: 5672 diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index 561eb6c3bbd..06da5a19401 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -67,7 +67,7 @@ config: useSES: true multiSFT: enabled: false # keep multiSFT default in sync with sft chart's multiSFT.enabled - enableFederation: false # keep enableFederation default in sync with galley and cargohold chart's config.enableFederation as well as wire-server chart's tags.federation + enableFederation: false # keep in sync with background-worker, cargohold and galley charts' config.enableFederation as well as wire-server chart's tags.federation # Not used if enableFederation is false rabbitmq: host: rabbitmq diff --git a/charts/cargohold/values.yaml b/charts/cargohold/values.yaml index 14cfaedce64..0eb8718e0ca 100644 --- a/charts/cargohold/values.yaml +++ b/charts/cargohold/values.yaml @@ -18,7 +18,7 @@ config: logLevel: Info logFormat: StructuredJSON logNetStrings: false - enableFederation: false # keep enableFederation default in sync with brig and galley chart's config.enableFederation as well as wire-server chart's tags.federation + enableFederation: false # keep in sync with background-worker, brig and galley charts' config.enableFederation as well as wire-server chart's tags.federation aws: region: "eu-west-1" s3Bucket: assets diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 947bb42c028..821643510ad 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -33,7 +33,7 @@ config: # tlsCaSecretRef: # name: # key: - enableFederation: false # keep enableFederation default in sync with brig and cargohold chart's config.enableFederation as well as wire-server chart's tags.federation + enableFederation: false # keep in sync with background-worker, brig and cargohold charts' config.enableFederation as well as wire-server chart's tags.federation # Not used if enableFederation is false rabbitmq: host: rabbitmq diff --git a/charts/wire-server/requirements.yaml b/charts/wire-server/requirements.yaml index 2d1fafb9674..60ed93fcbe0 100644 --- a/charts/wire-server/requirements.yaml +++ b/charts/wire-server/requirements.yaml @@ -96,7 +96,6 @@ dependencies: repository: "file://../background-worker" tags: - background-worker - - federation - haskellServices - services - name: integration diff --git a/charts/wire-server/values.yaml b/charts/wire-server/values.yaml index f0488133713..55900d494fc 100644 --- a/charts/wire-server/values.yaml +++ b/charts/wire-server/values.yaml @@ -7,7 +7,7 @@ tags: legalhold: false - federation: false # see also galley.config.enableFederation and brig.config.enableFederation + federation: false # see also {background-worker, brig, cargohold, galley}.config.enableFederation backoffice: false mlsstats: false integration: false diff --git a/docs/src/understand/configure-federation.md b/docs/src/understand/configure-federation.md index 455aafd6437..d97e3182bbc 100644 --- a/docs/src/understand/configure-federation.md +++ b/docs/src/understand/configure-federation.md @@ -371,7 +371,7 @@ certificate. Read {ref}`choose-backend-domain` again, then set the backend domain three times to the same value in the subcharts cargohold, galley and brig. You also need to set `enableFederation` to -`true`. +`true` in background-worker in addition to those charts. ``` yaml # override values for wire-server @@ -393,6 +393,10 @@ cargohold: enableFederation: true settings: federationDomain: example.com # your chosen "backend domain" + +background-worker: + config: + enableFederation: true ``` (configure-federation-strategy-in-brig)= diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 0e1b9604a64..8bb14c72ce1 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -6,7 +6,7 @@ tags: cannon: true cargohold: true spar: true - federation: true # also see galley.config.enableFederation and brig.config.enableFederation + federation: true # also see {background-worker,brig,cargohold,galley}.config.enableFederation backoffice: true proxy: false legalhold: false @@ -493,6 +493,7 @@ background-worker: requests: {} imagePullPolicy: {{ .Values.imagePullPolicy }} config: + enableFederation: true backendNotificationPusher: pushBackoffMinWait: 1000 # 1ms pushBackoffMaxWait: 500000 # 0.5s diff --git a/libs/extended/src/Network/AMQP/Extended.hs b/libs/extended/src/Network/AMQP/Extended.hs index b3131fce2af..3d1e79a218b 100644 --- a/libs/extended/src/Network/AMQP/Extended.hs +++ b/libs/extended/src/Network/AMQP/Extended.hs @@ -3,7 +3,7 @@ module Network.AMQP.Extended ( RabbitMqHooks (..), RabbitMqAdminOpts (..), - RabbitMqOpts (..), + AmqpEndpoint (..), openConnectionWithRetries, mkRabbitMqAdminClientEnv, mkRabbitMqChannelMVar, @@ -103,9 +103,9 @@ mkRabbitMqAdminClientEnv opts = do (either throwM pure <=< flip runClientM clientEnv) (toServant $ adminClient basicAuthData) --- | When admin opts are needed use `RabbitMqOpts Identity`, otherwise use --- `RabbitMqOpts NoAdmin`. -data RabbitMqOpts = RabbitMqOpts +-- | When admin opts are needed use `AmqpEndpoint Identity`, otherwise use +-- `AmqpEndpoint NoAdmin`. +data AmqpEndpoint = AmqpEndpoint { host :: !String, port :: !Int, vHost :: !Text, @@ -113,19 +113,19 @@ data RabbitMqOpts = RabbitMqOpts } deriving (Show) -instance FromJSON RabbitMqOpts where +instance FromJSON AmqpEndpoint where parseJSON = withObject "RabbitMqAdminOpts" $ \v -> - RabbitMqOpts + AmqpEndpoint <$> v .: "host" <*> v .: "port" <*> v .: "vHost" <*> parseTlsJson v -demoteOpts :: RabbitMqAdminOpts -> RabbitMqOpts -demoteOpts RabbitMqAdminOpts {..} = RabbitMqOpts {..} +demoteOpts :: RabbitMqAdminOpts -> AmqpEndpoint +demoteOpts RabbitMqAdminOpts {..} = AmqpEndpoint {..} -- | Useful if the application only pushes into some queues. -mkRabbitMqChannelMVar :: Logger -> RabbitMqOpts -> IO (MVar Q.Channel) +mkRabbitMqChannelMVar :: Logger -> AmqpEndpoint -> IO (MVar Q.Channel) mkRabbitMqChannelMVar l opts = do chanMVar <- newEmptyMVar connThread <- @@ -152,10 +152,10 @@ openConnectionWithRetries :: forall m. (MonadIO m, MonadMask m, MonadBaseControl IO m) => Logger -> - RabbitMqOpts -> + AmqpEndpoint -> RabbitMqHooks m -> m () -openConnectionWithRetries l RabbitMqOpts {..} hooks = do +openConnectionWithRetries l AmqpEndpoint {..} hooks = do (username, password) <- liftIO $ readCredsFromEnv connectWithRetries username password where diff --git a/services/background-worker/src/Wire/BackendNotificationPusher.hs b/services/background-worker/src/Wire/BackendNotificationPusher.hs index f7cfe209ad6..464c93e0cf0 100644 --- a/services/background-worker/src/Wire/BackendNotificationPusher.hs +++ b/services/background-worker/src/Wire/BackendNotificationPusher.hs @@ -17,8 +17,10 @@ import Imports import Network.AMQP qualified as Q import Network.AMQP.Extended import Network.AMQP.Lifted qualified as QL -import Network.RabbitMqAdmin +import Network.RabbitMqAdmin hiding (adminClient) +import Network.RabbitMqAdmin qualified as RabbitMqAdmin import Prometheus +import Servant.Client qualified as Servant import System.Logger.Class qualified as Log import UnliftIO import Wire.API.Federation.API @@ -197,8 +199,8 @@ pairedMaximumOn f = maximumBy (compare `on` snd) . map (id &&& f) -- FUTUREWORK: Recosider using 1 channel for many consumers. It shouldn't matter -- for a handful of remote domains. -- Consumers is passed in explicitly so that cleanup code has a reference to the consumer tags. -startPusher :: IORef (Map Domain (Q.ConsumerTag, MVar ())) -> Q.Channel -> AppT IO () -startPusher consumersRef chan = do +startPusher :: RabbitMqAdmin.AdminAPI (Servant.AsClientT IO) -> IORef (Map Domain (Q.ConsumerTag, MVar ())) -> Q.Channel -> AppT IO () +startPusher adminClient consumersRef chan = do -- This ensures that we receive notifications 1 by 1 which ensures they are -- delivered in order. markAsWorking BackendNotificationPusher @@ -221,7 +223,7 @@ startPusher consumersRef chan = do ] $ forever $ do - remotes <- getRemoteDomains + remotes <- getRemoteDomains adminClient ensureConsumers consumersRef chan remotes threadDelay timeBeforeNextRefresh @@ -259,8 +261,8 @@ ensureConsumer consumers chan domain = do -- let us come down this path if there is an old consumer. liftIO $ forM_ oldTag $ Q.cancelConsumer chan . fst -getRemoteDomains :: AppT IO [Domain] -getRemoteDomains = do +getRemoteDomains :: RabbitMqAdmin.AdminAPI (Servant.AsClientT IO) -> AppT IO [Domain] +getRemoteDomains adminClient = do -- Jittered exponential backoff with 10ms as starting delay and 60s as max -- cumulative delay. When this is reached, the operation fails. -- @@ -279,9 +281,8 @@ getRemoteDomains = do where go :: AppT IO [Domain] go = do - client <- asks rabbitmqAdminClient vhost <- asks rabbitmqVHost - queues <- liftIO $ listQueuesByVHost client vhost + queues <- liftIO $ listQueuesByVHost adminClient vhost let notifQueuesSuffixes = mapMaybe (\q -> Text.stripPrefix "backend-notifications." q.name) queues catMaybes <$> traverse (\d -> either (\e -> logInvalidDomain d e >> pure Nothing) (pure . Just) $ mkDomain d) notifQueuesSuffixes logInvalidDomain d e = @@ -290,7 +291,7 @@ getRemoteDomains = do . Log.field "queue" ("backend-notifications." <> d) . Log.field "error" e -startWorker :: RabbitMqAdminOpts -> AppT IO (IORef (Maybe Q.Channel), IORef (Map Domain (Q.ConsumerTag, MVar ()))) +startWorker :: AmqpEndpoint -> AppT IO (IORef (Maybe Q.Channel), IORef (Map Domain (Q.ConsumerTag, MVar ()))) startWorker rabbitmqOpts = do env <- ask -- These are used in the POSIX signal handlers, so we need to make @@ -304,22 +305,28 @@ startWorker rabbitmqOpts = do clearRefs = do atomicWriteIORef chanRef Nothing atomicWriteIORef consumersRef mempty - -- We can fire and forget this thread because it keeps respawning itself using the 'onConnectionClosedHandler'. - void $ - async $ - liftIO $ - openConnectionWithRetries env.logger (demoteOpts rabbitmqOpts) $ - RabbitMqHooks - { -- The exception handling in `openConnectionWithRetries` won't open a new - -- connection on an explicit close call. - onNewChannel = \chan -> do - atomicWriteIORef chanRef $ pure chan - runAppT env $ startPusher consumersRef chan, - onChannelException = \_ -> do - clearRefs - runAppT env $ markAsNotWorking BackendNotificationPusher, - onConnectionClose = do - clearRefs - runAppT env $ markAsNotWorking BackendNotificationPusher - } + case env.rabbitmqAdminClient of + Nothing -> + Log.info $ + Log.msg $ + Log.val "RabbitMQ admin client not available, skipping backend notification pusher." + Just client -> + -- We can fire and forget this thread because it keeps respawning itself using the 'onConnectionClosedHandler'. + void $ + async $ + liftIO $ + openConnectionWithRetries env.logger rabbitmqOpts $ + RabbitMqHooks + { -- The exception handling in `openConnectionWithRetries` won't open a new + -- connection on an explicit close call. + onNewChannel = \chan -> do + atomicWriteIORef chanRef $ pure chan + runAppT env $ startPusher client consumersRef chan, + onChannelException = \_ -> do + clearRefs + runAppT env $ markAsNotWorking BackendNotificationPusher, + onConnectionClose = do + clearRefs + runAppT env $ markAsNotWorking BackendNotificationPusher + } pure (chanRef, consumersRef) diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index 3a9bc8e298a..7c5241a6bc9 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -8,6 +8,7 @@ import Data.Metrics.Servant qualified as Metrics import Data.Text qualified as T import Imports import Network.AMQP qualified as Q +import Network.AMQP.Extended (demoteOpts) import Network.Wai.Utilities.Server import Servant import Servant.Server.Generic @@ -21,7 +22,8 @@ import Wire.BackgroundWorker.Options run :: Opts -> IO () run opts = do env <- mkEnv opts - (notifChanRef, notifConsumersRef) <- runAppT env $ BackendNotificationPusher.startWorker opts.rabbitmq + let amqpEP = either id demoteOpts opts.rabbitmq.unRabbitMqOpts + (notifChanRef, notifConsumersRef) <- runAppT env $ BackendNotificationPusher.startWorker amqpEP let -- cleanup will run in a new thread when the signal is caught, so we need to use IORefs and -- specific exception types to message threads to clean up l = logger env diff --git a/services/background-worker/src/Wire/BackgroundWorker/Env.hs b/services/background-worker/src/Wire/BackgroundWorker/Env.hs index db968315947..dcf89d56d41 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Env.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Env.hs @@ -31,7 +31,7 @@ data Worker data Env = Env { http2Manager :: Http2Manager, - rabbitmqAdminClient :: RabbitMqAdmin.AdminAPI (Servant.AsClientT IO), + rabbitmqAdminClient :: Maybe (RabbitMqAdmin.AdminAPI (Servant.AsClientT IO)), rabbitmqVHost :: Text, logger :: Logger, federatorInternal :: Endpoint, @@ -66,8 +66,8 @@ mkEnv opts = do responseTimeoutNone (\t -> responseTimeoutMicro $ 1000000 * t) -- seconds to microseconds opts.defederationTimeout - rabbitmqVHost = opts.rabbitmq.vHost - rabbitmqAdminClient <- mkRabbitMqAdminClientEnv opts.rabbitmq + rabbitmqVHost = either (.vHost) (.vHost) opts.rabbitmq.unRabbitMqOpts + rabbitmqAdminClient <- for (rightToMaybe opts.rabbitmq.unRabbitMqOpts) mkRabbitMqAdminClientEnv statuses <- newIORef $ Map.fromList diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs index da31c41255a..cdbeb1e5024 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs @@ -11,7 +11,7 @@ data Opts = Opts logFormat :: !(Maybe (Last LogFormat)), backgroundWorker :: !Endpoint, federatorInternal :: !Endpoint, - rabbitmq :: !RabbitMqAdminOpts, + rabbitmq :: !RabbitMqOpts, -- | Seconds, Nothing for no timeout defederationTimeout :: Maybe Int, backendNotificationPusher :: BackendNotificationsConfig @@ -37,3 +37,13 @@ data BackendNotificationsConfig = BackendNotificationsConfig deriving (Show, Generic) instance FromJSON BackendNotificationsConfig + +newtype RabbitMqOpts = RabbitMqOpts {unRabbitMqOpts :: Either AmqpEndpoint RabbitMqAdminOpts} + deriving (Show) + +instance FromJSON RabbitMqOpts where + parseJSON v = + RabbitMqOpts + <$> ( (Right <$> parseJSON v) + <|> (Left <$> parseJSON v) + ) diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index 351f38f7c4a..29906684cae 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -270,13 +270,13 @@ spec = do let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined - rabbitmqAdminClient = mockRabbitMqAdminClient mockAdmin + rabbitmqAdminClient = Just $ mockRabbitMqAdminClient mockAdmin rabbitmqVHost = "test-vhost" defederationTimeout = responseTimeoutNone backendNotificationsConfig = BackendNotificationsConfig 1000 500000 1000 backendNotificationMetrics <- mkBackendNotificationMetrics - domains <- runAppT Env {..} getRemoteDomains + domains <- runAppT Env {..} $ getRemoteDomains (fromJust rabbitmqAdminClient) domains `shouldBe` map Domain ["foo.example", "bar.example", "baz.example"] readTVarIO mockAdmin.listQueuesVHostCalls `shouldReturn` ["test-vhost"] @@ -287,12 +287,12 @@ spec = do let federatorInternal = Endpoint "localhost" 8097 http2Manager = undefined statuses = undefined - rabbitmqAdminClient = mockRabbitMqAdminClient mockAdmin + rabbitmqAdminClient = Just $ mockRabbitMqAdminClient mockAdmin rabbitmqVHost = "test-vhost" defederationTimeout = responseTimeoutNone backendNotificationsConfig = BackendNotificationsConfig 1000 500000 1000 backendNotificationMetrics <- mkBackendNotificationMetrics - domainsThread <- async $ runAppT Env {..} getRemoteDomains + domainsThread <- async $ runAppT Env {..} $ getRemoteDomains (fromJust rabbitmqAdminClient) -- Wait for first call untilM (readTVarIO mockAdmin.listQueuesVHostCalls >>= \calls -> pure $ not $ null calls) diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index a5a1f761fb6..10cfef98b9e 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -390,7 +390,7 @@ data Opts = Opts -- | SFT Federation multiSFT :: !(Maybe Bool), -- | RabbitMQ settings, required when federation is enabled. - rabbitmq :: !(Maybe RabbitMqOpts), + rabbitmq :: !(Maybe AmqpEndpoint), -- | AWS settings aws :: !AWSOpts, -- | Enable Random Prekey Strategy diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 3e1b97aa1cf..1d602e420f3 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -182,7 +182,7 @@ data Opts = Opts -- | Federator endpoint _federator :: !(Maybe Endpoint), -- | RabbitMQ settings, required when federation is enabled. - _rabbitmq :: !(Maybe RabbitMqOpts), + _rabbitmq :: !(Maybe AmqpEndpoint), -- | Disco URL _discoUrl :: !(Maybe Text), -- | Other settings From ea4bfc110318bca78fabae41000e3a7e698a6bbb Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:26:12 +0200 Subject: [PATCH 069/136] [WPB-10092] open telemetry instrumentation (#3901) * [feat] add initial otel instrumentatoin for brig and galley - add wai and rpc instrumentation for brig - add wai and rpc instrumentation for galley - create new package wire-otel that houses otel related utils * [wip] start building http2 request and response instrumentatoin * [feat] minimal support for instrumenting http/2 clients * [fix] append headers to the end * [feat] add some surrounding span context in wire api federation * [wip] instrument cannon and gundeck * [chore] add developer documentation for open telemetry instrumentation * [chore] remove http2 directive spam * [chore] revert instrumentation of http/2 requests and responeses in fed * [chore] remove spans in services that are not on our request paths * [chore] changelog entry * [chore] don't instrument requests twice (in galley and in bilge) * [chore] remove http2 stub instrumentation and add futurework instead * [chore] keep vertical export lists Co-authored-by: Igor Ranieri <54423+elland@users.noreply.github.com> * Update services/brig/src/Brig/Run.hs Co-authored-by: Igor Ranieri <54423+elland@users.noreply.github.com> * Revert "Update services/brig/src/Brig/Run.hs" This reverts commit 4c782755fa87b8a709c2cbd2d67c666f13781186. * regenerate cabal.configs, default.nixs * Fix deps in brig.cabal * Fixup * hi ci --------- Co-authored-by: Igor Ranieri <54423+elland@users.noreply.github.com> Co-authored-by: Matthias Fischmann --- cabal.project | 1 + ...instrumentation-brig-galley-gundeck-cannon | 1 + .../3-bug-fixes/remove-spam-from-nginx | 1 + .../federation-v0/nginz/conf/integration.conf | 8 +- .../federation-v1/nginz/conf/integration.conf | 8 +- .../src/developer/developer/open-telemetry.md | 47 ++ integration/test/Testlib/ModService.hs | 7 +- libs/bilge/bilge.cabal | 2 + libs/bilge/default.nix | 2 + libs/bilge/src/Bilge/RPC.hs | 11 +- .../src/Wire/API/Federation/Client.hs | 4 +- .../wire-api-federation.cabal | 1 + libs/wire-otel/CHANGELOG.md | 5 + libs/wire-otel/LICENSE | 661 ++++++++++++++++++ libs/wire-otel/default.nix | 34 + libs/wire-otel/src/Wire/OpenTelemetry.hs | 52 ++ libs/wire-otel/test/Main.hs | 4 + libs/wire-otel/wire-otel.cabal | 46 ++ libs/wire-subsystems/default.nix | 2 + libs/wire-subsystems/src/Wire/Rpc.hs | 3 +- libs/wire-subsystems/wire-subsystems.cabal | 1 + nix/haskell-pins.nix | 18 + nix/local-haskell-packages.nix | 1 + services/brig/brig.cabal | 147 ++-- services/brig/default.nix | 6 + services/brig/src/Brig/IO/Intra.hs | 2 +- services/brig/src/Brig/Provider/API.hs | 1 + services/brig/src/Brig/Provider/RPC.hs | 4 +- services/brig/src/Brig/RPC.hs | 8 +- services/brig/src/Brig/Run.hs | 26 +- services/cannon/cannon.cabal | 58 +- services/cannon/default.nix | 6 + services/cannon/src/Cannon/Run.hs | 10 +- services/galley/default.nix | 6 + services/galley/galley.cabal | 100 +-- services/galley/src/Galley/Intra/Util.hs | 2 +- services/galley/src/Galley/Run.hs | 20 +- services/gundeck/default.nix | 6 + services/gundeck/gundeck.cabal | 96 +-- .../gundeck/src/Gundeck/Push/Websocket.hs | 2 +- services/gundeck/src/Gundeck/Run.hs | 31 +- .../conf/nginz/integration.conf | 8 +- tools/stern/src/Stern/Intra.hs | 13 +- 43 files changed, 1210 insertions(+), 262 deletions(-) create mode 100644 changelog.d/2-features/open-telemetry-instrumentation-brig-galley-gundeck-cannon create mode 100644 changelog.d/3-bug-fixes/remove-spam-from-nginx create mode 100644 docs/src/developer/developer/open-telemetry.md create mode 100644 libs/wire-otel/CHANGELOG.md create mode 100644 libs/wire-otel/LICENSE create mode 100644 libs/wire-otel/default.nix create mode 100644 libs/wire-otel/src/Wire/OpenTelemetry.hs create mode 100644 libs/wire-otel/test/Main.hs create mode 100644 libs/wire-otel/wire-otel.cabal diff --git a/cabal.project b/cabal.project index d490134c847..ed3bbc74931 100644 --- a/cabal.project +++ b/cabal.project @@ -29,6 +29,7 @@ packages: , libs/wai-utilities/ , libs/wire-api/ , libs/wire-api-federation/ + , libs/wire-otel/ , libs/wire-message-proto-lens/ , libs/wire-subsystems/ , libs/zauth/ 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 new file mode 100644 index 00000000000..9212911e115 --- /dev/null +++ b/changelog.d/2-features/open-telemetry-instrumentation-brig-galley-gundeck-cannon @@ -0,0 +1 @@ +added open telemetry instrumentation for brig, galley, gundeck and cannon diff --git a/changelog.d/3-bug-fixes/remove-spam-from-nginx b/changelog.d/3-bug-fixes/remove-spam-from-nginx new file mode 100644 index 00000000000..7167a858f0a --- /dev/null +++ b/changelog.d/3-bug-fixes/remove-spam-from-nginx @@ -0,0 +1 @@ +removed spam from nginx (nginz) by using the new style http/2 directive diff --git a/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf b/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf index 12c49ccfe88..fa168d16f4d 100644 --- a/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf +++ b/deploy/dockerephemeral/federation-v0/nginz/conf/integration.conf @@ -7,7 +7,7 @@ listen 8081; # port. # This port is only used for trying out nginx http2 forwarding without TLS locally and should not # be ported to any production nginz config. -listen 8090 http2; +listen 8090; ######## TLS/SSL block start ############## # @@ -15,5 +15,7 @@ listen 8090 http2; # 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 http2; -listen [::]:8443 ssl http2; +listen 8443 ssl; +listen [::]:8443 ssl; + +http2 on; diff --git a/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf b/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf index 12c49ccfe88..fa168d16f4d 100644 --- a/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf +++ b/deploy/dockerephemeral/federation-v1/nginz/conf/integration.conf @@ -7,7 +7,7 @@ listen 8081; # port. # This port is only used for trying out nginx http2 forwarding without TLS locally and should not # be ported to any production nginz config. -listen 8090 http2; +listen 8090; ######## TLS/SSL block start ############## # @@ -15,5 +15,7 @@ listen 8090 http2; # 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 http2; -listen [::]:8443 ssl http2; +listen 8443 ssl; +listen [::]:8443 ssl; + +http2 on; diff --git a/docs/src/developer/developer/open-telemetry.md b/docs/src/developer/developer/open-telemetry.md new file mode 100644 index 00000000000..62381a3818d --- /dev/null +++ b/docs/src/developer/developer/open-telemetry.md @@ -0,0 +1,47 @@ +# OpenTelemetry Instrumentation + +## Current Status + +The following components have been instrumented: +- brig +- galley +- gundeck +- cannon + +## Known Issues and future work + +- Proper HTTP/2 instrumentation is missing for federator & co - this is related to http/2 outobj in the http2 libraray throwing away all structured information +- Some parts of the service, such as background jobs, may need additional instrumentation. It's currently unclear if these are appearing in the tracing data. +- we need to ingest the data into grafana tempo + + +## Setup instructions for local use + +To view the tracing data: + +1. Start Jaeger using Docker: + ```bash + docker run --rm --name jaeger \ + -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ + -p 6831:6831/udp \ + -p 6832:6832/udp \ + -p 5778:5778 \ + -p 16686:16686 \ + -p 4317:4317 \ + -p 4318:4318 \ + -p 14250:14250 \ + -p 14268:14268 \ + -p 14269:14269 \ + -p 9411:9411 \ + jaegertracing/all-in-one:latest + ``` + +2. Start your services or run integration tests. +3. Open the Jaeger UI at [http://localhost:16686/](http://localhost:16686/) + +## Relevant Resources + +We're using the `hs-opentelemetry-*` family of haskell packages available [here](https://github.com/iand675/hs-opentelemetry). + +- [hs-opentelemetry-instrumentation-wai](https://hackage.haskell.org/package/hs-opentelemetry-instrumentation-wai-0.1.0.0/docs/src/OpenTelemetry.Instrumentation.Wai.html#local-6989586621679045744) +- [hs-opentelemetry-sdk](https://hackage.haskell.org/package/hs-opentelemetry-sdk-0.0.3.6) diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 341c770e5fc..385a410b10b 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -456,9 +456,10 @@ startNginzLocal resource = do -- override port configuration let portConfigTemplate = [r|listen {localPort}; -listen {http2_port} http2; -listen {ssl_port} ssl http2; -listen [::]:{ssl_port} ssl http2; +listen {http2_port}; +listen {ssl_port} ssl; +listen [::]:{ssl_port} ssl; +http2 on; |] let portConfig = portConfigTemplate diff --git a/libs/bilge/bilge.cabal b/libs/bilge/bilge.cabal index b3b4154bbc7..8e64bbe92e5 100644 --- a/libs/bilge/bilge.cabal +++ b/libs/bilge/bilge.cabal @@ -31,6 +31,7 @@ library default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -97,5 +98,6 @@ library , uri-bytestring , wai , wai-extra + , wire-otel default-language: GHC2021 diff --git a/libs/bilge/default.nix b/libs/bilge/default.nix index 8c35f0746ad..1844d50b1d2 100644 --- a/libs/bilge/default.nix +++ b/libs/bilge/default.nix @@ -26,6 +26,7 @@ , uri-bytestring , wai , wai-extra +, wire-otel }: mkDerivation { pname = "bilge"; @@ -53,6 +54,7 @@ mkDerivation { uri-bytestring wai wai-extra + wire-otel ]; description = "Library for composing HTTP requests"; license = lib.licenses.agpl3Only; diff --git a/libs/bilge/src/Bilge/RPC.hs b/libs/bilge/src/Bilge/RPC.hs index e07324e172a..182bd303488 100644 --- a/libs/bilge/src/Bilge/RPC.hs +++ b/libs/bilge/src/Bilge/RPC.hs @@ -36,9 +36,11 @@ import Control.Monad.Catch (MonadCatch, MonadThrow (..), try) import Data.Aeson (FromJSON, eitherDecode') import Data.CaseInsensitive (original) import Data.Text.Lazy (pack) +import Data.Text.Lazy qualified as T import Imports hiding (log) import Network.HTTP.Client qualified as HTTP import System.Logger.Class +import Wire.OpenTelemetry (withClientInstrumentation) class HasRequestId m where getRequestId :: m RequestId @@ -69,7 +71,7 @@ instance Show RPCException where . showString "}" rpc :: - (MonadIO m, MonadCatch m, MonadHttp m, HasRequestId m) => + (MonadUnliftIO m, MonadCatch m, MonadHttp m, HasRequestId m) => LText -> (Request -> Request) -> m (Response (Maybe LByteString)) @@ -81,7 +83,7 @@ rpc sys = rpc' sys empty -- Note: 'syncIO' is wrapped around the IO action performing the request -- and any exceptions caught are re-thrown in an 'RPCException'. rpc' :: - (MonadIO m, MonadCatch m, MonadHttp m, HasRequestId m) => + (MonadUnliftIO m, MonadCatch m, MonadHttp m, HasRequestId m) => -- | A label for the remote system in case of 'RPCException's. LText -> Request -> @@ -89,8 +91,9 @@ rpc' :: m (Response (Maybe LByteString)) rpc' sys r f = do rId <- getRequestId - let rq = f . requestId rId $ r - res <- try $ httpLbs rq id + let rq = f $ requestId rId r + res <- try $ withClientInstrumentation ("intra-call-to-" <> T.toStrict sys) \k -> do + k rq \r' -> httpLbs r' id case res of Left x -> throwM $ RPCException sys rq x Right x -> pure x diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs index bc3c56362f4..1a83e8c9adb 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs @@ -138,8 +138,8 @@ withNewHttpRequest target req k = do sendReqMVar <- newEmptyMVar thread <- liftIO . async $ H2Manager.startPersistentHTTP2Connection ctx target cacheLimit sslRemoveTrailingDot tcpConnectionTimeout sendReqMVar let newConn = H2Manager.HTTP2Conn thread (putMVar sendReqMVar H2Manager.CloseConnection) sendReqMVar - H2Manager.sendRequestWithConnection newConn req $ \resp -> do - k resp <* newConn.disconnect + H2Manager.sendRequestWithConnection newConn req \resp -> + k resp `finally` newConn.disconnect performHTTP2Request :: Http2Manager -> diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index dc7ea88d9e8..86207e36a72 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -38,6 +38,7 @@ library default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures diff --git a/libs/wire-otel/CHANGELOG.md b/libs/wire-otel/CHANGELOG.md new file mode 100644 index 00000000000..799270ba251 --- /dev/null +++ b/libs/wire-otel/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for wire-otel + +## 0.1.0.0 -- YYYY-mm-dd + +* First version. Released on an unsuspecting world. diff --git a/libs/wire-otel/LICENSE b/libs/wire-otel/LICENSE new file mode 100644 index 00000000000..dba13ed2ddf --- /dev/null +++ b/libs/wire-otel/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/libs/wire-otel/default.nix b/libs/wire-otel/default.nix new file mode 100644 index 00000000000..e3cec5ff487 --- /dev/null +++ b/libs/wire-otel/default.nix @@ -0,0 +1,34 @@ +# WARNING: GENERATED FILE, DO NOT EDIT. +# This file is generated by running hack/bin/generate-local-nix-packages.sh and +# must be regenerated whenever local packages are added or removed, or +# dependencies are added or removed. +{ mkDerivation +, base +, gitignoreSource +, hs-opentelemetry-instrumentation-http-client +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk +, http-client +, kan-extensions +, lib +, text +, unliftio +}: +mkDerivation { + pname = "wire-otel"; + version = "0.1.0.0"; + src = gitignoreSource ./.; + libraryHaskellDepends = [ + base + hs-opentelemetry-instrumentation-http-client + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk + http-client + kan-extensions + text + unliftio + ]; + testHaskellDepends = [ base ]; + homepage = "https://wire.com/"; + license = lib.licenses.agpl3Only; +} diff --git a/libs/wire-otel/src/Wire/OpenTelemetry.hs b/libs/wire-otel/src/Wire/OpenTelemetry.hs new file mode 100644 index 00000000000..aa76dc500b4 --- /dev/null +++ b/libs/wire-otel/src/Wire/OpenTelemetry.hs @@ -0,0 +1,52 @@ +-- FUTUREWORK(mangoiv): +-- instrument http/2 request similarly to how it was done for http-client here: +-- https://github.com/iand675/hs-opentelemetry/blob/0b3c854a88113fc18df8561202a76357e593a294/instrumentation/http-client/src/OpenTelemetry/Instrumentation/HttpClient/Raw.hs#L60 +-- This is non-trivial because http/2 forgets the structure on the out objs. +module Wire.OpenTelemetry + ( -- * instrumentation helpers + withTracer, + withTracerC, + + -- * outbound instrumentation + + -- ** http client + withClientInstrumentation, + ) +where + +import Control.Monad.Codensity (Codensity (Codensity)) +import Data.Text (Text) +import Network.HTTP.Client (Request, Response) +import OpenTelemetry.Context.ThreadLocal (getContext) +import OpenTelemetry.Instrumentation.HttpClient.Raw +import OpenTelemetry.Trace +import UnliftIO (MonadUnliftIO, bracket, liftIO) + +-- | a tracer for a service like brig, galley, etc. +withTracer :: (MonadUnliftIO m) => (Tracer -> m r) -> m r +withTracer k = + bracket + (liftIO initializeGlobalTracerProvider) + shutdownTracerProvider + \tp -> k $ makeTracer tp "wire-otel" tracerOptions + +-- | like 'withTracer' but in 'Codensity' +withTracerC :: Codensity IO Tracer +withTracerC = Codensity withTracer + +-- | instrument a http client +withClientInstrumentation :: + (MonadUnliftIO m) => + -- | name of the caller + Text -> + -- | continuation that takes a continuation that takes a request and a way to respond to a request + ((Request -> (Request -> m (Response a)) -> m (Response a)) -> m b) -> + m b +withClientInstrumentation info k = do + tracer <- httpTracerProvider + inSpan tracer info defaultSpanArguments {kind = Client} do + otelCtx <- getContext + k \req respond -> do + resp <- respond =<< instrumentRequest httpClientInstrumentationConfig otelCtx req + instrumentResponse httpClientInstrumentationConfig otelCtx resp + pure resp diff --git a/libs/wire-otel/test/Main.hs b/libs/wire-otel/test/Main.hs new file mode 100644 index 00000000000..3e2059e31f5 --- /dev/null +++ b/libs/wire-otel/test/Main.hs @@ -0,0 +1,4 @@ +module Main (main) where + +main :: IO () +main = putStrLn "Test suite not yet implemented." diff --git a/libs/wire-otel/wire-otel.cabal b/libs/wire-otel/wire-otel.cabal new file mode 100644 index 00000000000..450c2dc3ea0 --- /dev/null +++ b/libs/wire-otel/wire-otel.cabal @@ -0,0 +1,46 @@ +cabal-version: 3.4 +name: wire-otel +version: 0.1.0.0 +description: wire open-telemetry-instrumentation +homepage: https://wire.com/ +license: AGPL-3.0-only +license-file: LICENSE +author: Wire Swiss GmbH +maintainer: backend@wire.com +copyright: (c) 2020 Wire Swiss GmbH +build-type: Simple +extra-doc-files: CHANGELOG.md + +common common-all + ghc-options: -O2 -Wall + default-extensions: + BlockArguments + OverloadedLists + OverloadedRecordDot + OverloadedStrings + +library + import: common-all + exposed-modules: Wire.OpenTelemetry + build-depends: + , base + , hs-opentelemetry-instrumentation-http-client + , hs-opentelemetry-instrumentation-wai + , hs-opentelemetry-sdk + , http-client + , kan-extensions + , text + , unliftio + + hs-source-dirs: src + default-language: GHC2021 + +test-suite wire-otel-test + import: common-all + default-language: GHC2021 + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Main.hs + build-depends: + , base + , wire-otel diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index 29e6263437f..24a8758783a 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -79,6 +79,7 @@ , wai-utilities , wire-api , wire-api-federation +, wire-otel , witherable }: mkDerivation { @@ -152,6 +153,7 @@ mkDerivation { wai-utilities wire-api wire-api-federation + wire-otel witherable ]; testHaskellDepends = [ diff --git a/libs/wire-subsystems/src/Wire/Rpc.hs b/libs/wire-subsystems/src/Wire/Rpc.hs index 99f52727867..bd7b3c682ef 100644 --- a/libs/wire-subsystems/src/Wire/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/Rpc.hs @@ -43,7 +43,7 @@ runRpcWithHttp mgr reqId = interpret $ \case embed $ runHttpRpc mgr reqId $ rpcWithRetriesImpl serviceName ep req rpcImpl :: ServiceName -> Endpoint -> (Request -> Request) -> HttpRpc (Response (Maybe LByteString)) -rpcImpl serviceName ep req = +rpcImpl serviceName ep req = do rpc' serviceName empty $ req . Bilge.host (encodeUtf8 ep._host) @@ -81,6 +81,7 @@ newtype HttpRpc a = HttpRpc {unHttpRpc :: ReaderT (Manager, RequestId) IO a} Applicative, Monad, MonadIO, + MonadUnliftIO, MonadThrow, MonadCatch, MonadMask, diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index a544025aa7b..db0cb43facb 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -195,6 +195,7 @@ library , wai-utilities , wire-api , wire-api-federation + , wire-otel , witherable default-language: GHC2021 diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 044b1f62879..810bdbaa8f0 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -281,6 +281,24 @@ let hash = "sha256-L90PQtDw/JFwyltSVFvmfjTAb0ZLhFt9Hl0jbzn+cFQ="; }; }; + + # hs-opentelemetry-* has not been released for a while on hackage + hs-opentelemetry = { + src = fetchgit { + url = "https://github.com/iand675/hs-opentelemetry"; + rev = "0b3c854a88113fc18df8561202a76357e593a294"; + hash = "sha256-N5FzKz6T1sE9xffGCeWa+iTW8a1GCLsy2TlAjzIed34="; + }; + packages = { + hs-opentelemetry-sdk = "sdk"; + hs-opentelemetry-api = "api"; + hs-opentelemetry-propagator-datadog = "propagators/datadog"; + hs-opentelemetry-instrumentation-http-client = "instrumentation/http-client"; + hs-opentelemetry-instrumentation-wai = "instrumentation/wai"; + hs-opentelemetry-exporter-otlp = "exporters/otlp"; + }; + }; + }; hackagePins = { diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index 133fcd9afae..38c381258a4 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -31,6 +31,7 @@ wire-api-federation = hself.callPackage ../libs/wire-api-federation/default.nix { inherit gitignoreSource; }; wire-api = hself.callPackage ../libs/wire-api/default.nix { inherit gitignoreSource; }; wire-message-proto-lens = hself.callPackage ../libs/wire-message-proto-lens/default.nix { inherit gitignoreSource; }; + wire-otel = hself.callPackage ../libs/wire-otel/default.nix { inherit gitignoreSource; }; wire-subsystems = hself.callPackage ../libs/wire-subsystems/default.nix { inherit gitignoreSource; }; zauth = hself.callPackage ../libs/zauth/default.nix { inherit gitignoreSource; }; background-worker = hself.callPackage ../services/background-worker/default.nix { inherit gitignoreSource; }; diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index c723695019b..6d8885cfb71 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -215,124 +215,127 @@ library -Wunused-packages build-depends: - , aeson >=2.0.1.0 - , amazonka >=2 - , amazonka-core >=2 - , amazonka-dynamodb >=2 - , amazonka-ses >=2 - , amazonka-sqs >=2 + , aeson >=2.0.1.0 + , amazonka >=2 + , amazonka-core >=2 + , amazonka-dynamodb >=2 + , amazonka-ses >=2 + , amazonka-sqs >=2 , amqp - , async >=2.1 - , auto-update >=0.1 - , base >=4 && <5 + , async >=2.1 + , auto-update >=0.1 + , base >=4 && <5 , base-prelude - , base16-bytestring >=0.1 - , base64-bytestring >=1.0 - , bilge >=0.21.1 - , bloodhound >=0.13 - , brig-types >=0.91.1 - , bytestring >=0.10 - , bytestring-conversion >=0.2 - , cassandra-util >=0.16.2 + , base16-bytestring >=0.1 + , base64-bytestring >=1.0 + , bilge >=0.21.1 + , bloodhound >=0.13 + , brig-types >=0.91.1 + , bytestring >=0.10 + , bytestring-conversion >=0.2 + , cassandra-util >=0.16.2 , comonad - , conduit >=1.2.8 - , containers >=0.5 - , cookie >=0.4 + , conduit >=1.2.8 + , containers >=0.5 + , cookie >=0.4 , cql - , cryptobox-haskell >=0.1.1 + , cryptobox-haskell >=0.1.1 , crypton - , currency-codes >=2.0 + , currency-codes >=2.0 , data-default , dns , dns-util - , enclosed-exceptions >=1.0 - , errors >=1.4 - , exceptions >=0.5 + , enclosed-exceptions >=1.0 + , errors >=1.4 + , exceptions >=0.5 , extended , extra , file-embed , file-embed-lzma - , filepath >=1.3 - , fsnotify >=0.4 - , galley-types >=0.75.3 - , gundeck-types >=1.32.1 - , hashable >=1.2 - , HsOpenSSL >=0.10 - , http-client >=0.7 - , http-client-openssl >=0.2 + , filepath >=1.3 + , fsnotify >=0.4 + , galley-types >=0.75.3 + , gundeck-types >=1.32.1 + , hashable >=1.2 + , hs-opentelemetry-instrumentation-wai + , hs-opentelemetry-sdk + , HsOpenSSL >=0.10 + , http-client >=0.7 + , http-client-openssl >=0.2 , http-media - , http-types >=0.8 + , http-types >=0.8 , http2-manager , imports , insert-ordered-containers - , iproute >=1.5 - , iso639 >=0.1 + , iproute >=1.5 + , iso639 >=0.1 , jose , jwt-tools - , lens >=3.8 - , lens-aeson >=1.0 + , lens >=3.8 + , lens-aeson >=1.0 , memory - , metrics-core >=0.3 - , metrics-wai >=0.3 + , metrics-core >=0.3 + , metrics-wai >=0.3 , mime - , mime-mail >=0.4 + , mime-mail >=0.4 , mmorph - , MonadRandom >=0.5 - , mtl >=2.1 - , network >=2.4 + , MonadRandom >=0.5 + , mtl >=2.1 + , network >=2.4 , network-conduit-tls , openapi3 - , optparse-applicative >=0.11 + , optparse-applicative >=0.11 , polysemy , polysemy-conc , polysemy-plugin , polysemy-time , polysemy-wire-zoo , prometheus-client - , proto-lens >=0.1 - , random-shuffle >=0.0.3 + , proto-lens >=0.1 + , random-shuffle >=0.0.3 , raw-strings-qq - , resourcet >=1.1 - , retry >=0.7 - , safe-exceptions >=0.1 + , resourcet >=1.1 + , retry >=0.7 + , safe-exceptions >=0.1 , saml2-web-sso , schema-profunctor , servant , servant-openapi3 , servant-server , servant-swagger-ui - , sodium-crypto-sign >=0.1 - , split >=0.2 + , sodium-crypto-sign >=0.1 + , split >=0.2 , ssl-util - , statistics >=0.13 - , stomp-queue >=0.3 - , template >=0.2 + , statistics >=0.13 + , stomp-queue >=0.3 + , template >=0.2 , template-haskell - , text >=0.11 - , text-icu-translit >=0.1 - , time >=1.1 + , text >=0.11 + , text-icu-translit >=0.1 + , time >=1.1 , time-out , time-units - , tinylog >=0.10 - , transformers >=0.3 + , tinylog >=0.10 + , transformers >=0.3 , transitive-anns - , types-common >=0.16 + , types-common >=0.16 , types-common-aws - , types-common-journal >=0.1 - , unliftio >=0.2 - , unordered-containers >=0.2 - , uri-bytestring >=0.2 + , types-common-journal >=0.1 + , unliftio >=0.2 + , unordered-containers >=0.2 + , uri-bytestring >=0.2 , utf8-string - , uuid >=1.3.5 - , vector >=0.11 - , wai >=3.0 - , wai-extra >=3.0 - , wai-middleware-gunzip >=0.0.2 - , wai-utilities >=0.16 + , uuid >=1.3.5 + , vector >=0.11 + , wai >=3.0 + , wai-extra >=3.0 + , wai-middleware-gunzip >=0.0.2 + , wai-utilities >=0.16 , wire-api , wire-api-federation + , wire-otel , wire-subsystems - , zauth >=0.10.3 + , zauth >=0.10.3 executable brig import: common-all diff --git a/services/brig/default.nix b/services/brig/default.nix index 03a8b688aa3..61ebf704692 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -52,6 +52,8 @@ , gitignoreSource , gundeck-types , hashable +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , hscim , HsOpenSSL , http-api-data @@ -153,6 +155,7 @@ , warp-tls , wire-api , wire-api-federation +, wire-otel , wire-subsystems , yaml , zauth @@ -206,6 +209,8 @@ mkDerivation { galley-types gundeck-types hashable + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk HsOpenSSL http-client http-client-openssl @@ -280,6 +285,7 @@ mkDerivation { wai-utilities wire-api wire-api-federation + wire-otel wire-subsystems zauth ]; diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index d833def04fe..f92e18bef38 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -565,7 +565,7 @@ blockConv lusr qcnv = do upsertOne2OneConversation :: ( MonadReader Env m, - MonadIO m, + MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 6a36c4f5a1a..b7ed00018e1 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -825,6 +825,7 @@ deleteBot :: ( MonadHttp m, MonadReader Env m, MonadMask m, + MonadUnliftIO m, HasRequestId m, MonadLogger m, MonadClient m diff --git a/services/brig/src/Brig/Provider/RPC.hs b/services/brig/src/Brig/Provider/RPC.hs index f8abba06133..c41193cee69 100644 --- a/services/brig/src/Brig/Provider/RPC.hs +++ b/services/brig/src/Brig/Provider/RPC.hs @@ -164,7 +164,7 @@ setServiceConn scon = do -- | Remove service connection information from galley. removeServiceConn :: ( MonadReader Env m, - MonadIO m, + MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m, @@ -220,7 +220,7 @@ addBotMember zusr zcon conv bot clt pid sid = do removeBotMember :: ( MonadHttp m, MonadReader Env m, - MonadIO m, + MonadUnliftIO m, MonadMask m, HasRequestId m, MonadLogger m diff --git a/services/brig/src/Brig/RPC.hs b/services/brig/src/Brig/RPC.hs index c421ad468d2..23105e055fe 100644 --- a/services/brig/src/Brig/RPC.hs +++ b/services/brig/src/Brig/RPC.hs @@ -41,21 +41,21 @@ decodeBody :: (Typeable a, FromJSON a, MonadThrow m) => Text -> Response (Maybe decodeBody ctx = responseJsonThrow (ParseException ctx) cargoholdRequest :: - (MonadReader Env m, MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + (MonadReader Env m, MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m) => StdMethod -> (Request -> Request) -> m (Response (Maybe BL.ByteString)) cargoholdRequest = serviceRequest "cargohold" cargohold galleyRequest :: - (MonadReader Env m, MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + (MonadReader Env m, MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m) => StdMethod -> (Request -> Request) -> m (Response (Maybe BL.ByteString)) galleyRequest = serviceRequest "galley" galley serviceRequest :: - (MonadReader Env m, MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + (MonadReader Env m, MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m) => LT.Text -> Control.Lens.Getting Request Env Request -> StdMethod -> @@ -66,7 +66,7 @@ serviceRequest nm svc m r = do serviceRequestImpl nm service m r serviceRequestImpl :: - (MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + (MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m) => LT.Text -> Request -> StdMethod -> diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index eda8c9fe88f..08486c43d31 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -15,11 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.Run - ( run, - mkApp, - ) -where +module Brig.Run (run, mkApp) where import AWS.Util (readAuthExpiration) import Brig.API.Federation @@ -59,6 +55,8 @@ import Network.Wai.Middleware.Gzip qualified as GZip import Network.Wai.Utilities.Request import Network.Wai.Utilities.Server import Network.Wai.Utilities.Server qualified as Server +import OpenTelemetry.Instrumentation.Wai qualified as Otel +import OpenTelemetry.Trace as Otel import Polysemy (Member) import Servant (Context ((:.)), (:<|>) (..)) import Servant qualified @@ -73,6 +71,7 @@ import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai import Wire.API.User (AccountStatus (PendingInvitation)) import Wire.DeleteQueue +import Wire.OpenTelemetry (withTracer) import Wire.Sem.Paging qualified as P import Wire.UserStore @@ -81,7 +80,7 @@ import Wire.UserStore -- thread terminates for any reason. -- https://github.com/zinfra/backend-issues/issues/1647 run :: Opts -> IO () -run o = do +run o = withTracer \tracer -> do (app, e) <- mkApp o s <- Server.newSettings (server e) internalEventListener <- @@ -100,13 +99,11 @@ run o = do authMetrics <- Async.async (runBrigToIO e collectAuthMetrics) pendingActivationCleanupAsync <- Async.async (runBrigToIO e pendingActivationCleanup) - runSettingsWithShutdown s app Nothing `finally` do - mapM_ Async.cancel emailListener - Async.cancel internalEventListener - mapM_ Async.cancel sftDiscovery - Async.cancel pendingActivationCleanupAsync - mapM_ Async.cancel turnDiscovery - Async.cancel authMetrics + inSpan tracer "brig" defaultSpanArguments {kind = Otel.Server} (runSettingsWithShutdown s app Nothing) `finally` do + Async.cancelMany $ + [internalEventListener, pendingActivationCleanupAsync, authMetrics] + <> catMaybes [emailListener, sftDiscovery] + <> turnDiscovery closeEnv e where endpoint' = brig o @@ -115,7 +112,8 @@ run o = do mkApp :: Opts -> IO (Wai.Application, Env) mkApp o = do e <- newEnv o - pure (middleware e $ servantApp e, e) + otelMiddleware <- Otel.newOpenTelemetryWaiMiddleware + pure (otelMiddleware . middleware e $ servantApp e, e) where middleware :: Env -> Wai.Middleware middleware e = diff --git a/services/cannon/cannon.cabal b/services/cannon/cannon.cabal index d0af6581163..b85fd137f9d 100644 --- a/services/cannon/cannon.cabal +++ b/services/cannon/cannon.cabal @@ -32,6 +32,7 @@ library default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -77,44 +78,47 @@ library -Wredundant-constraints -Wunused-packages build-depends: - aeson >=2.0.1.0 - , api-field-json-th >=0.1.0.2 - , async >=2.0 - , base >=4.6 && <5 - , bilge >=0.12 - , bytestring >=0.10 - , bytestring-conversion >=0.2 - , conduit >=1.3.4.2 - , data-timeout >=0.3 - , exceptions >=0.6 + aeson >=2.0.1.0 + , api-field-json-th >=0.1.0.2 + , async >=2.0 + , base >=4.6 && <5 + , bilge >=0.12 + , bytestring >=0.10 + , bytestring-conversion >=0.2 + , conduit >=1.3.4.2 + , data-timeout >=0.3 + , exceptions >=0.6 , extended , extra , gundeck-types - , hashable >=1.2 - , http-types >=0.8 + , hashable >=1.2 + , hs-opentelemetry-instrumentation-wai + , hs-opentelemetry-sdk + , http-types >=0.8 , imports - , lens >=4.4 - , lens-family-core >=1.1 - , metrics-wai >=0.4 - , mwc-random >=0.13 + , lens >=4.4 + , lens-family-core >=1.1 + , metrics-wai >=0.4 + , mwc-random >=0.13 , prometheus-client - , retry >=0.7 + , retry >=0.7 , safe-exceptions , servant-conduit , servant-server - , strict >=0.3.2 - , text >=1.1 - , tinylog >=0.10 - , types-common >=0.16 + , strict >=0.3.2 + , text >=1.1 + , tinylog >=0.10 + , types-common >=0.16 , unix , unliftio - , vector >=0.10 - , wai >=3.0 - , wai-extra >=3.0 - , wai-utilities >=0.11 - , warp >=3.0 - , websockets >=0.11.2 + , vector >=0.10 + , wai >=3.0 + , wai-extra >=3.0 + , wai-utilities >=0.11 + , warp >=3.0 + , websockets >=0.11.2 , wire-api + , wire-otel default-language: GHC2021 diff --git a/services/cannon/default.nix b/services/cannon/default.nix index 9278d2c1c94..db254e8b235 100644 --- a/services/cannon/default.nix +++ b/services/cannon/default.nix @@ -19,6 +19,8 @@ , gitignoreSource , gundeck-types , hashable +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , http-types , imports , lens @@ -50,6 +52,7 @@ , warp , websockets , wire-api +, wire-otel }: mkDerivation { pname = "cannon"; @@ -72,6 +75,8 @@ mkDerivation { extra gundeck-types hashable + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk http-types imports lens @@ -96,6 +101,7 @@ mkDerivation { warp websockets wire-api + wire-otel ]; executableHaskellDepends = [ base imports types-common ]; testHaskellDepends = [ diff --git a/services/cannon/src/Cannon/Run.hs b/services/cannon/src/Cannon/Run.hs index fe86d2c2d8b..eefd22f4af5 100644 --- a/services/cannon/src/Cannon/Run.hs +++ b/services/cannon/src/Cannon/Run.hs @@ -45,6 +45,9 @@ import Network.Wai qualified as Wai import Network.Wai.Handler.Warp hiding (run) import Network.Wai.Middleware.Gzip qualified as Gzip import Network.Wai.Utilities.Server +import OpenTelemetry.Instrumentation.Wai +import OpenTelemetry.Trace hiding (Server) +import OpenTelemetry.Trace qualified as Otel import Prometheus qualified as Prom import Servant import System.IO.Strict qualified as Strict @@ -57,11 +60,12 @@ import Wire.API.Routes.Internal.Cannon qualified as Internal import Wire.API.Routes.Public.Cannon import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai +import Wire.OpenTelemetry (withTracer) type CombinedAPI = CannonAPI :<|> Internal.API run :: Opts -> IO () -run o = do +run o = withTracer \tracer -> do when (o ^. drainOpts . millisecondsBetweenBatches == 0) $ error "drainOpts.millisecondsBetweenBatches must not be set to 0." when (o ^. drainOpts . gracePeriodSeconds == 0) $ @@ -77,11 +81,13 @@ run o = do refreshMetricsThread <- Async.async $ runCannon e refreshMetrics s <- newSettings $ Server (o ^. cannon . host) (o ^. cannon . port) (applog e) (Just idleTimeout) + otelMiddleWare <- newOpenTelemetryWaiMiddleware let middleware :: Wai.Middleware middleware = versionMiddleware (foldMap expandVersionExp (o ^. disabledAPIVersions)) . requestIdMiddleware g defaultRequestIdHeaderName . servantPrometheusMiddleware (Proxy @CombinedAPI) + . otelMiddleWare . Gzip.gzip Gzip.def . catchErrors g defaultRequestIdHeaderName app :: Application @@ -94,7 +100,7 @@ run o = do E.handle uncaughtExceptionHandler $ do void $ installHandler sigTERM (signalHandler (env e) tid) Nothing void $ installHandler sigINT (signalHandler (env e) tid) Nothing - runSettings s app `finally` do + inSpan tracer "cannon" defaultSpanArguments {kind = Otel.Server} (runSettings s app) `finally` do -- FUTUREWORK(@akshaymankar, @fisx): we may want to call `runSettingsWithShutdown` here, -- but it's a sensitive change, and it looks like this is closing all the websockets at -- the same time and then calling the drain script. I suspect this might be due to some diff --git a/services/galley/default.nix b/services/galley/default.nix index 3cadf669e2f..38bf97f86de 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -44,6 +44,8 @@ , gitignoreSource , gundeck-types , hex +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , HsOpenSSL , http-api-data , http-client @@ -124,6 +126,7 @@ , warp-tls , wire-api , wire-api-federation +, wire-otel , wire-subsystems , yaml }: @@ -166,6 +169,8 @@ mkDerivation { generics-sop gundeck-types hex + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk HsOpenSSL http-client http-client-openssl @@ -218,6 +223,7 @@ mkDerivation { wai-utilities wire-api wire-api-federation + wire-otel wire-subsystems ]; executableHaskellDepends = [ diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 790607e9bf6..f0b36539a9b 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -25,6 +25,7 @@ common common-all default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -285,90 +286,93 @@ library other-modules: Paths_galley hs-source-dirs: src build-depends: - , aeson >=2.0.1.0 - , amazonka >=1.4.5 - , amazonka-sqs >=1.4.5 + , aeson >=2.0.1.0 + , amazonka >=1.4.5 + , amazonka-sqs >=1.4.5 , amqp , asn1-encoding , asn1-types - , async >=2.0 - , base >=4.6 && <5 - , base64-bytestring >=1.0 - , bilge >=0.21.1 - , brig-types >=0.73.1 - , bytestring >=0.9 - , bytestring-conversion >=0.2 + , async >=2.0 + , base >=4.6 && <5 + , base64-bytestring >=1.0 + , bilge >=0.21.1 + , brig-types >=0.73.1 + , bytestring >=0.9 + , bytestring-conversion >=0.2 , case-insensitive - , cassandra-util >=0.16.2 - , cassava >=0.5.2 + , cassandra-util >=0.16.2 + , cassava >=0.5.2 , comonad - , containers >=0.5 + , containers >=0.5 , crypton , crypton-x509 - , currency-codes >=2.0 + , currency-codes >=2.0 , data-default , data-timeout - , enclosed-exceptions >=1.0 - , errors >=2.0 - , exceptions >=0.4 + , enclosed-exceptions >=1.0 + , errors >=2.0 + , exceptions >=0.4 , extended - , extra >=1.3 - , galley-types >=0.65.0 + , extra >=1.3 + , galley-types >=0.65.0 , generics-sop - , gundeck-types >=1.35.2 + , gundeck-types >=1.35.2 , hex - , HsOpenSSL >=0.11 - , http-client >=0.7 - , http-client-openssl >=0.2 + , hs-opentelemetry-instrumentation-wai + , hs-opentelemetry-sdk + , HsOpenSSL >=0.11 + , http-client >=0.7 + , http-client-openssl >=0.2 , http-media - , http-types >=0.8 + , http-types >=0.8 , http2-manager , imports , kan-extensions - , lens >=4.4 + , lens >=4.4 , metrics-core - , metrics-wai >=0.4 + , metrics-wai >=0.4 , optparse-applicative , pem , polysemy , polysemy-wire-zoo , prometheus-client - , proto-lens >=0.2 - , raw-strings-qq >=1.0 - , resourcet >=1.1 - , retry >=0.5 - , safe-exceptions >=0.1 - , saml2-web-sso >=0.20 + , proto-lens >=0.2 + , raw-strings-qq >=1.0 + , resourcet >=1.1 + , retry >=0.5 + , safe-exceptions >=0.1 + , saml2-web-sso >=0.20 , servant , servant-client , servant-server , singletons , singletons-base , sop-core - , split >=0.2 - , ssl-util >=0.1 - , stm >=2.4 + , split >=0.2 + , ssl-util >=0.1 + , stm >=2.4 , tagged , template-haskell - , text >=0.11 - , time >=1.4 - , tinylog >=0.10 - , tls >=1.7.0 + , text >=0.11 + , time >=1.4 + , tinylog >=0.10 + , tls >=1.7.0 , transformers , transitive-anns - , types-common >=0.16 + , types-common >=0.16 , types-common-aws - , types-common-journal >=0.1 - , unliftio >=0.2 - , uri-bytestring >=0.2 + , types-common-journal >=0.1 + , unliftio >=0.2 + , uri-bytestring >=0.2 , utf8-string - , uuid >=1.3 - , wai >=3.0 - , wai-extra >=3.0 - , wai-middleware-gunzip >=0.0.2 - , wai-utilities >=0.16 + , uuid >=1.3 + , wai >=3.0 + , wai-extra >=3.0 + , wai-middleware-gunzip >=0.0.2 + , wai-utilities >=0.16 , wire-api , wire-api-federation + , wire-otel , wire-subsystems executable galley diff --git a/services/galley/src/Galley/Intra/Util.hs b/services/galley/src/Galley/Intra/Util.hs index 8947d4a7a4a..c7e1de20920 100644 --- a/services/galley/src/Galley/Intra/Util.hs +++ b/services/galley/src/Galley/Intra/Util.hs @@ -23,7 +23,7 @@ where import Bilge hiding (getHeader, host, options, port, statusCode) import Bilge qualified as B -import Bilge.RPC +import Bilge.RPC (rpc) import Bilge.Retry import Control.Lens (view, (^.)) import Control.Retry diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index 7d78fdcdbdf..f9374903b2d 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -14,7 +14,6 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . - module Galley.Run ( run, mkApp, @@ -56,6 +55,8 @@ import Network.Wai.Middleware.Gzip qualified as GZip import Network.Wai.Utilities.Error import Network.Wai.Utilities.Request import Network.Wai.Utilities.Server +import OpenTelemetry.Instrumentation.Wai qualified as Otel +import OpenTelemetry.Trace as Otel import Prometheus qualified as Prom import Servant hiding (route) import System.Logger qualified as Log @@ -65,9 +66,11 @@ import Wire.API.Routes.API import Wire.API.Routes.Public.Galley import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai +import Wire.OpenTelemetry (withTracerC) run :: Opts -> IO () -run opts = lowerCodensity $ do +run opts = lowerCodensity do + tracer <- withTracerC (app, env) <- mkApp opts settings' <- lift $ @@ -82,25 +85,28 @@ run opts = lowerCodensity $ do void $ Codensity $ Async.withAsync $ runApp env deleteLoop void $ Codensity $ Async.withAsync $ runApp env refreshMetrics - lift $ finally (runSettingsWithShutdown settings' app Nothing) (closeApp env) + lift $ inSpan tracer "galley" defaultSpanArguments {kind = Otel.Server} (runSettingsWithShutdown settings' app Nothing) `finally` closeApp env mkApp :: Opts -> Codensity IO (Application, Env) mkApp opts = do logger <- lift $ mkLogger (opts ^. logLevel) (opts ^. logNetStrings) (opts ^. logFormat) env <- lift $ App.createEnv opts logger + otelMiddleware <- lift Otel.newOpenTelemetryWaiMiddleware lift $ runClient (env ^. cstate) $ versionCheck schemaVersion let middlewares = versionMiddleware (foldMap expandVersionExp (opts ^. settings . disabledAPIVersions)) . requestIdMiddleware logger defaultRequestIdHeaderName . servantPrometheusMiddleware (Proxy @CombinedAPI) + . otelMiddleware . GZip.gunzip . GZip.gzip GZip.def . catchErrors logger defaultRequestIdHeaderName - Codensity $ \k -> finally (k ()) $ do - Log.info logger $ Log.msg @Text "Galley application finished." - Log.flush logger - Log.close logger + Codensity \k -> + k () `finally` do + Log.info logger $ Log.msg @Text "Galley application finished." + Log.flush logger + Log.close logger pure (middlewares $ servantApp env, env) where -- Used as a last case in the servant tree. Previously, there used to be a diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index dedd9cc1dab..79051e52a81 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -30,6 +30,8 @@ , gitignoreSource , gundeck-types , hedis +, hs-opentelemetry-instrumentation-wai +, hs-opentelemetry-sdk , HsOpenSSL , http-client , http-client-tls @@ -83,6 +85,7 @@ , wai-utilities , websockets , wire-api +, wire-otel , yaml }: mkDerivation { @@ -114,6 +117,8 @@ mkDerivation { foldl gundeck-types hedis + hs-opentelemetry-instrumentation-wai + hs-opentelemetry-sdk http-client http-client-tls http-types @@ -147,6 +152,7 @@ mkDerivation { wai-routing wai-utilities wire-api + wire-otel yaml ]; executableHaskellDepends = [ diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index 2c4777a19b2..cce46169df8 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -66,6 +66,7 @@ library default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -111,62 +112,65 @@ library -Wredundant-constraints -Wunused-packages build-depends: - , aeson >=2.0.1.0 - , amazonka >=2 - , amazonka-core >=2 - , amazonka-sns >=2 - , amazonka-sqs >=2 - , async >=2.0 - , attoparsec >=0.10 - , auto-update >=0.1 - , base >=4.7 && <5 - , bilge >=0.21 - , bytestring >=0.9 - , bytestring-conversion >=0.2 - , cassandra-util >=0.16.2 - , containers >=0.5 + , aeson >=2.0.1.0 + , amazonka >=2 + , amazonka-core >=2 + , amazonka-sns >=2 + , amazonka-sqs >=2 + , async >=2.0 + , attoparsec >=0.10 + , auto-update >=0.1 + , base >=4.7 && <5 + , bilge >=0.21 + , bytestring >=0.9 + , bytestring-conversion >=0.2 + , cassandra-util >=0.16.2 + , containers >=0.5 , crypton-x509-store - , errors >=2.0 - , exceptions >=0.4 + , errors >=2.0 + , exceptions >=0.4 , extended - , extra >=1.1 + , extra >=1.1 , foldl - , gundeck-types >=1.0 - , hedis >=0.14.0 - , http-client >=0.7 - , http-client-tls >=0.3 - , http-types >=0.8 + , gundeck-types >=1.0 + , hedis >=0.14.0 + , hs-opentelemetry-instrumentation-wai + , hs-opentelemetry-sdk + , http-client >=0.7 + , http-client-tls >=0.3 + , http-types >=0.8 , imports - , lens >=4.4 - , lens-aeson >=1.0 - , metrics-core >=0.2.1 - , metrics-wai >=0.5.7 - , mtl >=2.2 - , network-uri >=2.6 + , lens >=4.4 + , lens-aeson >=1.0 + , metrics-core >=0.2.1 + , metrics-wai >=0.5.7 + , mtl >=2.2 + , network-uri >=2.6 , prometheus-client - , psqueues >=0.2.2 + , psqueues >=0.2.2 , raw-strings-qq - , resourcet >=1.1 - , retry >=0.5 + , resourcet >=1.1 + , retry >=0.5 , safe-exceptions , servant-server - , text >=1.1 - , time >=1.4 - , tinylog >=0.10 - , tls >=1.7.0 - , types-common >=0.16 + , text >=1.1 + , time >=1.4 + , tinylog >=0.10 + , tls >=1.7.0 + , types-common >=0.16 , types-common-aws - , unliftio >=0.2 - , unordered-containers >=0.2 - , uuid >=1.3 - , wai >=3.2 - , wai-extra >=3.0 - , wai-middleware-gunzip >=0.0.2 - , wai-predicates >=0.8 - , wai-routing >=0.12 - , wai-utilities >=0.16 + , unliftio >=0.2 + , unordered-containers >=0.2 + , uuid >=1.3 + , wai >=3.2 + , wai-extra >=3.0 + , wai-middleware-gunzip >=0.0.2 + , wai-predicates >=0.8 + , wai-routing >=0.12 + , wai-utilities >=0.16 , wire-api - , yaml >=0.8 + , wire-otel + , yaml >=0.8 default-language: GHC2021 diff --git a/services/gundeck/src/Gundeck/Push/Websocket.hs b/services/gundeck/src/Gundeck/Push/Websocket.hs index 2a6ff64e406..1c9d4730c51 100644 --- a/services/gundeck/src/Gundeck/Push/Websocket.hs +++ b/services/gundeck/src/Gundeck/Push/Websocket.hs @@ -165,7 +165,7 @@ bulkSend uri req = (uri,) <$> ((Right <$> bulkSend' uri req) `catch` (pure . Lef bulkSend' :: forall m. - ( MonadIO m, + ( MonadUnliftIO m, MonadMask m, HasRequestId m, MonadHttp m, diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index c63daf3cf68..a896171a13c 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -41,12 +41,15 @@ import Gundeck.Options hiding (host, port) import Gundeck.React import Gundeck.Schema.Run (lastSchemaVersion) import Gundeck.ThreadBudget -import Imports hiding (head) +import Imports import Network.Wai as Wai import Network.Wai.Middleware.Gunzip qualified as GZip import Network.Wai.Middleware.Gzip qualified as GZip import Network.Wai.Utilities.Request import Network.Wai.Utilities.Server hiding (serverPort) +import OpenTelemetry.Instrumentation.Wai (newOpenTelemetryWaiMiddleware) +import OpenTelemetry.Trace (defaultSpanArguments, inSpan, kind) +import OpenTelemetry.Trace qualified as Otel import Servant (Handler (Handler), (:<|>) (..)) import Servant qualified import System.Logger qualified as Log @@ -55,9 +58,10 @@ import Util.Options import Wire.API.Routes.Public.Gundeck (GundeckAPI) import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai +import Wire.OpenTelemetry run :: Opts -> IO () -run o = do +run o = withTracer \tracer -> do (rThreads, e) <- createEnv o runClient (e ^. cstate) $ versionCheck lastSchemaVersion @@ -69,8 +73,8 @@ run o = do wtbs <- forM (e ^. threadBudgetState) $ \tbs -> Async.async $ runDirect e $ watchThreadBudgetState tbs 10 wCollectAuth <- Async.async (collectAuthMetrics (Aws._awsEnv (Env._awsEnv e))) - let app = middleware e $ mkApp e - runSettingsWithShutdown s app Nothing `finally` do + app <- middleware e <*> pure (mkApp e) + inSpan tracer "gundeck" defaultSpanArguments {kind = Otel.Server} (runSettingsWithShutdown s app Nothing) `finally` do Log.info l $ Log.msg (Log.val "Shutting down ...") shutdown (e ^. cstate) Async.cancel lst @@ -81,14 +85,17 @@ run o = do whenJust (e ^. rstateAdditionalWrite) $ (=<<) Redis.disconnect . takeMVar Log.close (e ^. applog) where - middleware :: Env -> Middleware - middleware e = - versionMiddleware (foldMap expandVersionExp (o ^. settings . disabledAPIVersions)) - . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName - . waiPrometheusMiddleware sitemap - . GZip.gunzip - . GZip.gzip GZip.def - . catchErrors (e ^. applog) defaultRequestIdHeaderName + middleware :: Env -> IO Middleware + middleware e = do + otelMiddleWare <- newOpenTelemetryWaiMiddleware + pure $ + versionMiddleware (foldMap expandVersionExp (o ^. settings . disabledAPIVersions)) + . otelMiddleWare + . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName + . waiPrometheusMiddleware sitemap + . GZip.gunzip + . GZip.gzip GZip.def + . catchErrors (e ^. applog) defaultRequestIdHeaderName type CombinedAPI = GundeckAPI :<|> Servant.Raw diff --git a/services/nginz/integration-test/conf/nginz/integration.conf b/services/nginz/integration-test/conf/nginz/integration.conf index baae352c92a..c89469d51ff 100644 --- a/services/nginz/integration-test/conf/nginz/integration.conf +++ b/services/nginz/integration-test/conf/nginz/integration.conf @@ -7,7 +7,7 @@ listen 8081; # port. # This port is only used for trying out nginx http2 forwarding without TLS locally and should not # be ported to any production nginz config. -listen 8090 http2; +listen 8090; ######## TLS/SSL block start ############## # @@ -15,5 +15,7 @@ listen 8090 http2; # 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 http2; -listen [::]:8443 ssl http2; +listen 8443 ssl; +listen [::]:8443 ssl; + +http2 on; diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 62a230730fa..916dbd43e52 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -996,12 +996,13 @@ getOAuthClient :: OAuthClientId -> Handler OAuthClient getOAuthClient cid = do b <- view brig r <- - rpc' - "brig" - b - ( method GET - . Bilge.paths ["i", "oauth", "clients", toByteString' cid] - ) + lift $ + rpc' + "brig" + b + ( method GET + . Bilge.paths ["i", "oauth", "clients", toByteString' cid] + ) case statusCode r of 200 -> parseResponse (mkError status502 "bad-upstream") r 404 -> throwE (mkError status404 "bad-upstream" "not-found") From f0466088872e4bf5ef0ca146bdf52e14eebb8443 Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:38:07 +0200 Subject: [PATCH 070/136] [WPB-10772] Make it impossible for a user under legalhold to join an MLS conversation (#4242) * [feat] add two tests for scenarios with MLS + legalhold * [feat] don't allow claiming key packages of users under legalhold - new client error which returns 409 if someone (client or federating backend) tries to claim a key package of a user under legalhold (including pending) - add Fail effect to Brig and interpret to IO Error (server error 500) * [feat] cater for the scenario when a user doesn't claim key packages - if a user doesn't claim key packages (because they create the group by themselves) the local backend checks whether that user is legalheld and rejects the commit in that case - also adds some utilities (e.g. pattern synonyms fo Local and Remote) and propagates the Fail constraint - introduce new type RelativeTo --- changelog.d/2-features/WPB-10772 | 5 ++ integration/test/Test/LegalHold.hs | 70 +++++++++++++++++ libs/types-common/src/Data/Qualified.hs | 24 ++++-- .../src/Wire/API/Federation/API.hs | 3 +- libs/wire-api/src/Wire/API/Error.hs | 5 ++ libs/wire-api/src/Wire/API/Error/Galley.hs | 5 ++ .../src/Wire/API/Routes/Public/Galley/MLS.hs | 57 +++++++------- .../src/Wire/GalleyAPIAccess.hs | 3 + .../src/Wire/GalleyAPIAccess/Rpc.hs | 20 +++++ services/brig/src/Brig/API/Error.hs | 1 + services/brig/src/Brig/API/Federation.hs | 4 +- services/brig/src/Brig/API/MLS/KeyPackages.hs | 33 ++++++++ services/brig/src/Brig/API/Public.hs | 2 + services/brig/src/Brig/API/Types.hs | 3 + services/brig/src/Brig/App.hs | 4 + .../brig/src/Brig/CanonicalInterpreter.hs | 3 + services/galley/galley.cabal | 1 + services/galley/src/Galley/API/Federation.hs | 23 +++--- services/galley/src/Galley/API/LegalHold.hs | 41 +--------- .../galley/src/Galley/API/LegalHold/Get.hs | 78 +++++++++++++++++++ services/galley/src/Galley/API/MLS/Message.hs | 35 ++++++++- services/galley/src/Galley/App.hs | 3 + .../galley/src/Galley/Effects/TeamStore.hs | 2 + 23 files changed, 340 insertions(+), 85 deletions(-) create mode 100644 changelog.d/2-features/WPB-10772 create mode 100644 services/galley/src/Galley/API/LegalHold/Get.hs diff --git a/changelog.d/2-features/WPB-10772 b/changelog.d/2-features/WPB-10772 new file mode 100644 index 00000000000..97dd0b3286b --- /dev/null +++ b/changelog.d/2-features/WPB-10772 @@ -0,0 +1,5 @@ +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/integration/test/Test/LegalHold.hs b/integration/test/Test/LegalHold.hs index c948bccb649..f359d54d2c3 100644 --- a/integration/test/Test/LegalHold.hs +++ b/integration/test/Test/LegalHold.hs @@ -35,6 +35,7 @@ import Data.ProtoLens.Labels () import qualified Data.Set as Set import qualified Data.Text as T import GHC.Stack +import MLS.Util import Network.Wai (Request (pathInfo, requestMethod)) import Notifications import Numeric.Lens (hex) @@ -904,3 +905,72 @@ testLHDisableBeforeApproval = do disableLegalHold tid alice bob defPassword >>= assertStatus 200 getBob'sStatus `shouldMatch` "disabled" + +-- --------- +-- WPB-10772 +-- --------- + +-- | scenario 2.1: +-- charlie first is put under legalhold and after that wants to join an MLS conversation +-- claiming a keypackage of charlie to add them to a conversation should not be possible +testLegalholdThenMLSThirdParty :: (HasCallStack) => App () +testLegalholdThenMLSThirdParty = do + (alice, tid, [charlie]) <- createTeam OwnDomain 2 + [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] + _ <- uploadNewKeyPackage charlie1 + _ <- createNewGroup alice1 + legalholdWhitelistTeam tid alice >>= assertStatus 200 + withMockServer def lhMockApp \lhDomAndPort _chan -> do + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + requestLegalHoldDevice tid alice charlie >>= assertSuccess + approveLegalHoldDevice tid (charlie %. "qualified_id") defPassword >>= assertSuccess + profile <- getUser alice charlie >>= getJSON 200 + pStatus <- profile %. "legalhold_status" & asString + pStatus `shouldMatch` "enabled" + + mls <- getMLSState + claimKeyPackages mls.ciphersuite alice1 charlie + `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" + +-- | scenario 2.2: +-- charlie is put under legalhold but creates an MLS Group himself +-- since he doesn't need to claim his own keypackage to do so, this would succeed +-- we need to check upon group creation if the user is under legalhold and reject +-- the operation if they are +testLegalholdThenMLSSelf :: (HasCallStack) => App () +testLegalholdThenMLSSelf = do + (alice, tid, [charlie]) <- createTeam OwnDomain 2 + [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] + _ <- uploadNewKeyPackage alice1 + legalholdWhitelistTeam tid alice >>= assertStatus 200 + withMockServer def lhMockApp \lhDomAndPort _chan -> do + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + requestLegalHoldDevice tid alice charlie >>= assertSuccess + approveLegalHoldDevice tid (charlie %. "qualified_id") defPassword >>= assertSuccess + profile <- getUser alice charlie >>= getJSON 200 + pStatus <- profile %. "legalhold_status" & asString + pStatus `shouldMatch` "enabled" + + -- charlie tries to create a group and should fail when POSTing the add commit + _ <- createNewGroup 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] + >>= \mp -> + postMLSCommitBundle mp.sender (mkBundle mp) + `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" + + -- (unsurprisingly) this same thing should also work in the one2one case + + respJson <- getMLSOne2OneConversation alice charlie >>= getJSON 200 + resetGroup alice1 (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] + >>= \mp -> + postMLSCommitBundle mp.sender (mkBundle mp) + `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index 8b06c4ea58f..0dbc73f99ec 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -31,6 +31,7 @@ module Data.Qualified tSplit, qTagUnsafe, Remote, + RelativeTo (Remote, Local, RelativeTo), toRemoteUnsafe, Local, toLocalUnsafe, @@ -121,11 +122,24 @@ qualifyAs :: QualifiedWithTag t x -> a -> QualifiedWithTag t a qualifyAs = ($>) foldQualified :: Local x -> (Local a -> b) -> (Remote a -> b) -> Qualified a -> b -foldQualified loc f g q - | tDomain loc == qDomain q = - f (qTagUnsafe q) - | otherwise = - g (qTagUnsafe q) +foldQualified loc kLocal kRemote q = case q `RelativeTo` loc of + Local l -> kLocal l + Remote r -> kRemote r + +data a `RelativeTo` x = Qualified a `RelativeTo` Local x + +checkRelative :: a `RelativeTo` x -> Either (Local a) (Remote a) +checkRelative (q `RelativeTo` loc) + | tDomain loc == qDomain q = Left (qTagUnsafe q) + | otherwise = Right (qTagUnsafe q) + +pattern Local :: forall a x. Local a -> a `RelativeTo` x +pattern Local loc <- (checkRelative -> Left loc) + +pattern Remote :: forall a x. Remote a -> a `RelativeTo` x +pattern Remote rem <- (checkRelative -> Right rem) + +{-# COMPLETE Local, Remote #-} -- Partition a collection of qualified values into locals and remotes. -- diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API.hs b/libs/wire-api-federation/src/Wire/API/Federation/API.hs index 1c45da47edf..ba46921b04c 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -29,7 +29,7 @@ module Wire.API.Federation.API fedQueueClient, sendBundle, fedClientIn, - module Wire.API.MakesFederatedCall, + module X, -- * Re-exports Component (..), @@ -59,6 +59,7 @@ import Wire.API.Federation.Endpoint import Wire.API.Federation.HasNotificationEndpoint import Wire.API.Federation.Version import Wire.API.MakesFederatedCall +import Wire.API.MakesFederatedCall as X hiding (Location (..)) import Wire.API.Routes.Named -- Note: this type family being injective means that in most cases there is no need diff --git a/libs/wire-api/src/Wire/API/Error.hs b/libs/wire-api/src/Wire/API/Error.hs index f37711ac06f..a1899f9f6ca 100644 --- a/libs/wire-api/src/Wire/API/Error.hs +++ b/libs/wire-api/src/Wire/API/Error.hs @@ -41,11 +41,13 @@ module Wire.API.Error throwS, noteS, mapErrorS, + runErrorS, mapToRuntimeError, mapToDynamicError, ) where +import Control.Error (hush) import Control.Lens (at, (%~), (.~), (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Aeson qualified as A @@ -272,6 +274,9 @@ throwS = throw (Tagged @e ()) noteS :: forall e r a. (Member (ErrorS e) r) => Maybe a -> Sem r a noteS = note (Tagged @e ()) +runErrorS :: forall e r a. Sem (ErrorS e : r) a -> Sem r (Maybe a) +runErrorS = fmap hush . runError @(Tagged e ()) + mapErrorS :: forall e e' r a. (Member (ErrorS e') r) => diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index 22ad24e1c2d..a7bd372b9f1 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -105,6 +105,9 @@ data GalleyError | MLSSubConvClientNotInParent | MLSMigrationCriteriaNotSatisfied | MLSFederatedOne2OneNotSupported + | -- | MLS and federation are incompatible with legalhold - this error is thrown if a user + -- tries to create an MLS group while being under legalhold + MLSLegalholdIncompatible | -- NoBindingTeamMembers | NoBindingTeam @@ -256,6 +259,8 @@ type instance MapError 'MLSMigrationCriteriaNotSatisfied = 'StaticError 400 "mls type instance MapError 'MLSFederatedOne2OneNotSupported = 'StaticError 400 "mls-federated-one2one-not-supported" "Federated One2One MLS conversations are only supported in API version >= 6" +type instance MapError MLSLegalholdIncompatible = 'StaticError 409 "mls-legal-hold-not-allowed" "A user who is under legal-hold may not participate in MLS conversations" + type instance MapError 'NoBindingTeamMembers = 'StaticError 403 "non-binding-team-members" "Both users must be members of the same binding team" type instance MapError 'NoBindingTeam = 'StaticError 403 "no-binding-team" "Operation allowed only on binding teams" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index ccace964c21..347bc01158d 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -72,33 +72,34 @@ type MLSMessagingAPI = :<|> Named "mls-commit-bundle" ( Summary "Post a MLS CommitBundle" - :> From 'V5 - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "mls-welcome" - :> MakesFederatedCall 'Galley "send-mls-commit-bundle" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Brig "get-mls-clients" - :> MakesFederatedCall 'Brig "get-users-by-ids" - :> MakesFederatedCall 'Brig "api-version" - :> CanThrow 'ConvAccessDenied - :> CanThrow 'ConvMemberNotFound - :> CanThrow 'ConvNotFound - :> CanThrow 'LegalHoldNotEnabled - :> CanThrow 'MissingLegalholdConsent - :> CanThrow 'MLSClientMismatch - :> CanThrow 'MLSClientSenderUserMismatch - :> CanThrow 'MLSCommitMissingReferences - :> CanThrow 'MLSGroupConversationMismatch - :> CanThrow 'MLSInvalidLeafNodeIndex - :> CanThrow 'MLSNotEnabled - :> CanThrow 'MLSProposalNotFound - :> CanThrow 'MLSProtocolErrorTag - :> CanThrow 'MLSSelfRemovalNotAllowed - :> CanThrow 'MLSStaleMessage - :> CanThrow 'MLSSubConvClientNotInParent - :> CanThrow 'MLSUnsupportedMessage - :> CanThrow 'MLSUnsupportedProposal - :> CanThrow 'MLSWelcomeMismatch + :> From V5 + :> MakesFederatedCall Galley "on-mls-message-sent" + :> MakesFederatedCall Galley "mls-welcome" + :> MakesFederatedCall Galley "send-mls-commit-bundle" + :> MakesFederatedCall Galley "on-conversation-updated" + :> MakesFederatedCall Brig "get-mls-clients" + :> MakesFederatedCall Brig "get-users-by-ids" + :> MakesFederatedCall Brig "api-version" + :> CanThrow ConvAccessDenied + :> CanThrow ConvMemberNotFound + :> CanThrow ConvNotFound + :> CanThrow LegalHoldNotEnabled + :> CanThrow MissingLegalholdConsent + :> CanThrow MLSClientMismatch + :> CanThrow MLSClientSenderUserMismatch + :> CanThrow MLSCommitMissingReferences + :> CanThrow MLSGroupConversationMismatch + :> CanThrow MLSInvalidLeafNodeIndex + :> CanThrow MLSNotEnabled + :> CanThrow MLSProposalNotFound + :> CanThrow MLSProtocolErrorTag + :> CanThrow MLSSelfRemovalNotAllowed + :> CanThrow MLSStaleMessage + :> CanThrow MLSSubConvClientNotInParent + :> CanThrow MLSUnsupportedMessage + :> CanThrow MLSUnsupportedProposal + :> CanThrow MLSWelcomeMismatch + :> CanThrow MLSLegalholdIncompatible :> CanThrow MLSProposalFailure :> CanThrow NonFederatingBackends :> CanThrow UnreachableBackends @@ -107,7 +108,7 @@ type MLSMessagingAPI = :> ZClient :> ZConn :> ReqBody '[MLS] (RawMLS CommitBundle) - :> MultiVerb1 'POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus) + :> MultiVerb1 POST '[JSON] (Respond 201 "Commit accepted and forwarded" MLSMessageSendingStatus) ) :<|> Named "mls-public-keys" diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index e129fb5bc2c..cbb4f769837 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -31,6 +31,7 @@ import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Team import Wire.API.Team.Conversation qualified as Conv import Wire.API.Team.Feature +import Wire.API.Team.LegalHold import Wire.API.Team.Member qualified as Team import Wire.API.Team.Role import Wire.API.Team.SearchVisibility @@ -94,6 +95,8 @@ data GalleyAPIAccess m a where GetTeamLegalHoldStatus :: TeamId -> GalleyAPIAccess m (LockableFeature LegalholdConfig) + GetUserLegalholdStatus :: + Local UserId -> TeamId -> GalleyAPIAccess m UserLegalHoldStatusResponse GetTeamSearchVisibility :: TeamId -> GalleyAPIAccess m TeamSearchVisibility diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index aa9dcb4dc9e..dcafabedce8 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -46,6 +46,7 @@ import Wire.API.Routes.Version import Wire.API.Team import Wire.API.Team.Conversation qualified as Conv import Wire.API.Team.Feature +import Wire.API.Team.LegalHold import Wire.API.Team.Member as Member import Wire.API.Team.Role import Wire.API.Team.SearchVisibility @@ -80,6 +81,7 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = GetTeamName id' -> getTeamName id' GetTeamLegalHoldStatus id' -> getTeamLegalHoldStatus id' GetTeamSearchVisibility id' -> getTeamSearchVisibility id' + GetUserLegalholdStatus id' tid -> getUserLegalholdStatus id' tid ChangeTeamStatus id' ts m_al -> changeTeamStatus id' ts m_al MemberIsTeamOwner id' id'' -> memberIsTeamOwner id' id'' GetAllTeamFeaturesForUser m_id' -> getAllTeamFeaturesForUser m_id' @@ -89,6 +91,24 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = UnblockConversation lusr mconn qcnv -> unblockConversation v lusr mconn qcnv GetEJPDConvInfo uid -> getEJPDConvInfo uid +getUserLegalholdStatus :: + ( Member TinyLog r, + Member (Error ParseException) r, + Member Rpc r + ) => + Local UserId -> + TeamId -> + Sem (Input Endpoint : r) UserLegalHoldStatusResponse +getUserLegalholdStatus luid tid = do + debug $ + remote "galley" + . msg (val "get legalhold user status") + decodeBodyOrThrow "galley" =<< galleyRequest do + method GET + . paths ["teams", toByteString' tid, "legalhold", toByteString' (tUnqualified luid)] + . zUser (tUnqualified luid) + . expect2xx + galleyRequest :: (Member Rpc r, Member (Input Endpoint) r) => (Request -> Request) -> Sem r (Response (Maybe LByteString)) galleyRequest req = do ep <- input diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index 5f01495d8de..019e3786c1b 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -133,6 +133,7 @@ clientError (ClientDataError e) = clientDataError e clientError (ClientUserNotFound _) = StdError (errorToWai @'E.InvalidUser) clientError ClientLegalHoldCannotBeRemoved = StdError can'tDeleteLegalHoldClient clientError ClientLegalHoldCannotBeAdded = StdError can'tAddLegalHoldClient +clientError ClientLegalHoldIncompatible = StdError $ Wai.mkError status409 "mls-legal-hold-not-allowed" "A user who is under legal-hold may not participate in MLS conversations" clientError (ClientFederationError e) = fedError e clientError ClientCapabilitiesCannotBeRemoved = StdError clientCapabilitiesCannotBeRemoved clientError ClientMissingLegalholdConsentOldClients = StdError (errorToWai @'E.MissingLegalholdConsentOldClients) diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 58af99451bf..370761fb73f 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -52,6 +52,7 @@ import Gundeck.Types.Push qualified as Push import Imports hiding ((\\)) import Network.Wai.Utilities.Error ((!>>)) import Polysemy +import Polysemy.Fail (Fail) import Servant (ServerT) import Servant.API import Wire.API.Connection @@ -87,6 +88,7 @@ federationSitemap :: Member NotificationSubsystem r, Member UserSubsystem r, Member UserStore r, + Member Fail r, Member DeleteQueue r ) => ServerT FederationAPI (Handler r) @@ -193,7 +195,7 @@ claimMultiPrekeyBundle :: Handler r UserClientPrekeyMap claimMultiPrekeyBundle _ uc = API.claimLocalMultiPrekeyBundles LegalholdPlusFederationNotImplemented uc !>> clientError -fedClaimKeyPackages :: Domain -> ClaimKeyPackageRequest -> Handler r (Maybe KeyPackageBundle) +fedClaimKeyPackages :: (Member Fail r, Member GalleyAPIAccess r, Member UserStore r) => Domain -> ClaimKeyPackageRequest -> Handler r (Maybe KeyPackageBundle) fedClaimKeyPackages domain ckpr = isMLSEnabled >>= \case True -> do diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index d27fc78db78..33dcbcc90a1 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -40,9 +40,12 @@ import Control.Monad.Trans.Except import Control.Monad.Trans.Maybe import Data.CommaSeparatedList import Data.Id +import Data.LegalHold import Data.Qualified import Data.Set qualified as Set import Imports +import Polysemy (Member) +import Polysemy.Fail (Fail) import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.MLS.CipherSuite @@ -51,6 +54,9 @@ import Wire.API.MLS.KeyPackage import Wire.API.MLS.Serialisation import Wire.API.Team.LegalHold import Wire.API.User.Client +import Wire.GalleyAPIAccess (GalleyAPIAccess, getUserLegalholdStatus) +import Wire.StoredUser +import Wire.UserStore (UserStore, getUsers) uploadKeyPackages :: Local UserId -> ClientId -> KeyPackageUpload -> Handler r () uploadKeyPackages lusr cid kps = do @@ -60,6 +66,7 @@ uploadKeyPackages lusr cid kps = do lift . wrapClient $ Data.insertKeyPackages (tUnqualified lusr) cid kps' claimKeyPackages :: + (Member GalleyAPIAccess r, Member UserStore r, Member Fail r) => Local UserId -> Maybe ClientId -> Qualified UserId -> @@ -67,6 +74,7 @@ claimKeyPackages :: Handler r KeyPackageBundle claimKeyPackages lusr mClient target mSuite = do assertMLSEnabled + suite <- getCipherSuite mSuite foldQualified lusr @@ -75,12 +83,22 @@ claimKeyPackages lusr mClient target mSuite = do target claimLocalKeyPackages :: + forall r. + (Member GalleyAPIAccess r, Member UserStore r, Member Fail r) => Qualified UserId -> Maybe ClientId -> CipherSuiteTag -> Local UserId -> ExceptT ClientError (AppT r) KeyPackageBundle claimLocalKeyPackages qusr skipOwn suite target = do + -- while we do not support federation + MLS together with legalhold, to make sure that + -- the remote backend is complicit with our legalhold policies, we disallow anyone + -- fetching key packages for users under legalhold + -- + -- This way we prevent both locally and on the remote to add a legalholded user to an MLS + -- conversation + assertUserNotLegalholded + -- skip own client when the target is the requesting user itself let own = guard (qusr == tUntagged target) *> skipOwn clients <- map clientId <$> wrapClientE (Data.lookupClients (tUnqualified target)) @@ -103,6 +121,21 @@ claimLocalKeyPackages qusr skipOwn suite target = do uncurry (KeyPackageBundleEntry (tUntagged target) c) <$> wrapClientM (Data.claimKeyPackage target c suite) + assertUserNotLegalholded :: ExceptT ClientError (AppT r) () + assertUserNotLegalholded = do + -- this is okay because there can only be one StoredUser per UserId + [su] <- lift $ liftSem $ getUsers [tUnqualified target] + for_ su.teamId \tid -> do + resp <- lift $ liftSem $ getUserLegalholdStatus target tid + -- if an admin tries to put a user under legalhold + -- the user has to first reject to be put under legalhold + -- before they can join conversations again + case resp.ulhsrStatus of + UserLegalHoldPending -> throwE ClientLegalHoldIncompatible + UserLegalHoldEnabled -> throwE ClientLegalHoldIncompatible + UserLegalHoldDisabled -> pure () + UserLegalHoldNoConsent -> pure () + claimRemoteKeyPackages :: Local UserId -> CipherSuite -> diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 2ab050fc306..f3dc6426492 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -93,6 +93,7 @@ import Imports hiding (head) import Network.Socket (PortNumber) import Network.Wai.Utilities as Utilities import Polysemy +import Polysemy.Fail (Fail) import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) @@ -268,6 +269,7 @@ servantSitemap :: Member (ConnectionStore InternalPaging) r, Member (Embed HttpClientIO) r, Member (Embed IO) r, + Member Fail r, Member FederationConfigStore r, Member (Input (Local ())) r, Member AuthenticationSubsystem r, diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 076b844e1fc..8eac26e9861 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -176,6 +176,9 @@ data ClientError | ClientUserNotFound !UserId | ClientLegalHoldCannotBeRemoved | ClientLegalHoldCannotBeAdded + | -- | this error is thrown if legalhold if incompatible with different features + -- for now, this is the case for MLS and federation + ClientLegalHoldIncompatible | ClientFederationError FederationError | ClientCapabilitiesCannotBeRemoved | ClientMissingLegalholdConsentOldClients diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index f08502c94cc..3d85954f241 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -138,6 +138,7 @@ import OpenSSL.EVP.Digest (Digest, getDigestByName) import OpenSSL.Session (SSLOption (..)) import OpenSSL.Session qualified as SSL import Polysemy +import Polysemy.Fail import Polysemy.Final import Polysemy.Input (Input, input) import Prometheus @@ -488,6 +489,9 @@ instance MonadMonitor (AppT r) where instance MonadThrow (AppT r) where throwM = liftIO . throwM +instance (Member Fail r) => MonadFail (AppT r) where + fail = AppT . fail + instance (Member (Final IO) r) => MonadThrow (Sem r) where throwM = embedFinal . throwM @IO diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index fb6d1643cfe..65cf6132c0d 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -28,6 +28,7 @@ import Polysemy.Async import Polysemy.Conc import Polysemy.Embed (runEmbedded) import Polysemy.Error (Error, errorToIOFinal, mapError, runError) +import Polysemy.Fail import Polysemy.Input (Input, runInputConst, runInputSem) import Polysemy.TinyLog (TinyLog) import Wire.API.Allowlists (AllowlistEmailDomains) @@ -143,6 +144,7 @@ type BrigCanonicalEffects = Error SomeException, TinyLog, Embed HttpClientIO, + Fail, Embed IO, Race, Async, @@ -176,6 +178,7 @@ runBrigToIO e (AppT ma) = do . asyncToIOFinal . interpretRace . embedToFinal + . failToEmbed @IO -- if a fallible pattern fails, we throw a hard IO error . runEmbedded (runHttpClientIO e) . loggerToTinyLogReqId (e ^. App.requestId) (e ^. applog) . runError @SomeException diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index f0b36539a9b..0fb76ff1384 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -82,6 +82,7 @@ library Galley.API.Internal Galley.API.LegalHold Galley.API.LegalHold.Conflicts + Galley.API.LegalHold.Get Galley.API.LegalHold.Team Galley.API.Mapping Galley.API.Message diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 8ca7057686a..5d23e68b499 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -67,6 +67,7 @@ import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Polysemy import Polysemy.Error +import Polysemy.Fail (Fail) import Polysemy.Input import Polysemy.Internal.Kind (Append) import Polysemy.Resource @@ -605,6 +606,7 @@ sendMLSCommitBundle :: Member (Input UTCTime) r, Member LegalHoldStore r, Member MemberStore r, + Member Fail r, Member Resource r, Member TeamStore r, Member P.TinyLog r, @@ -626,15 +628,18 @@ sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do ibundle <- noteS @'MLSUnsupportedMessage $ mkIncomingBundle bundle (ctype, qConvOrSub) <- getConvFromGroupId ibundle.groupId when (qUnqualified qConvOrSub /= msr.convOrSubId) $ throwS @'MLSGroupConversationMismatch - MLSMessageResponseUpdates . map lcuUpdate - <$> postMLSCommitBundle - loc - (tUntagged sender) - msr.senderClient - ctype - qConvOrSub - Nothing - ibundle + -- this cannot throw the error since we always pass the sender which is qualified to be remote + Just resp <- + runErrorS @MLSLegalholdIncompatible $ + postMLSCommitBundle + loc + (tUntagged @QRemote sender) + msr.senderClient + ctype + qConvOrSub + Nothing + ibundle + pure $ MLSMessageResponseUpdates $ map lcuUpdate resp sendMLSMessage :: ( Member BackendNotificationQueueAccess r, diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 75eceeec319..cd2227ff4e3 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -34,7 +34,7 @@ import Brig.Types.Connection (UpdateConnectionsInternal (..)) import Brig.Types.Team.LegalHold (legalHoldService, viewLegalHoldService) import Control.Exception (assert) import Control.Lens (view, (^.)) -import Data.ByteString.Conversion (toByteString, toByteString') +import Data.ByteString.Conversion (toByteString) import Data.Id import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) import Data.List.Split (chunksOf) @@ -44,6 +44,7 @@ import Data.Qualified import Data.Range (toRange) import Data.Time.Clock import Galley.API.Error +import Galley.API.LegalHold.Get import Galley.API.LegalHold.Team import Galley.API.Query (iterateConversations) import Galley.API.Update (removeMemberFromLocalConv) @@ -290,44 +291,6 @@ removeSettings' tid = LHService.removeLegalHold tid (tUnqualified luid) changeLegalholdStatusAndHandlePolicyConflicts tid luid (member ^. legalHoldStatus) UserLegalHoldDisabled -- (support for withdrawing consent is not planned yet.) --- | Learn whether a user has LH enabled and fetch pre-keys. --- Note that this is accessible to ANY authenticated user, even ones outside the team -getUserStatus :: - forall r. - ( Member (Error InternalError) r, - Member (ErrorS 'TeamMemberNotFound) r, - Member LegalHoldStore r, - Member TeamStore r, - Member P.TinyLog r - ) => - Local UserId -> - TeamId -> - UserId -> - Sem r Public.UserLegalHoldStatusResponse -getUserStatus _lzusr tid uid = do - teamMember <- noteS @'TeamMemberNotFound =<< getTeamMember tid uid - let status = view legalHoldStatus teamMember - (mlk, lcid) <- case status of - UserLegalHoldNoConsent -> pure (Nothing, Nothing) - UserLegalHoldDisabled -> pure (Nothing, Nothing) - UserLegalHoldPending -> makeResponseDetails - UserLegalHoldEnabled -> makeResponseDetails - pure $ UserLegalHoldStatusResponse status mlk lcid - where - makeResponseDetails :: Sem r (Maybe LastPrekey, Maybe ClientId) - makeResponseDetails = do - mLastKey <- fmap snd <$> LegalHoldData.selectPendingPrekeys uid - lastKey <- case mLastKey of - Nothing -> do - P.err . Log.msg $ - "expected to find a prekey for user: " - <> toByteString' uid - <> " but none was found" - throw NoPrekeyForUser - Just lstKey -> pure lstKey - let clientId = clientIdFromPrekey . unpackLastPrekey $ lastKey - pure (Just lastKey, Just clientId) - -- | Change 'UserLegalHoldStatus' from no consent to disabled. FUTUREWORK: -- @withdrawExplicitConsentH@ (lots of corner cases we'd have to implement for that to pan -- out). diff --git a/services/galley/src/Galley/API/LegalHold/Get.hs b/services/galley/src/Galley/API/LegalHold/Get.hs new file mode 100644 index 00000000000..3607c040060 --- /dev/null +++ b/services/galley/src/Galley/API/LegalHold/Get.hs @@ -0,0 +1,78 @@ +-- 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 Galley.API.LegalHold.Get (getUserStatus) where + +import Control.Lens (view) +import Data.ByteString.Conversion (toByteString') +import Data.Id +import Data.LegalHold (UserLegalHoldStatus (..)) +import Data.Qualified +import Galley.API.Error +import Galley.Effects +import Galley.Effects.LegalHoldStore qualified as LegalHoldData +import Galley.Effects.TeamStore +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.TinyLog qualified as P +import System.Logger.Class qualified as Log +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Team.LegalHold +import Wire.API.Team.LegalHold qualified as Public +import Wire.API.Team.Member +import Wire.API.User.Client.Prekey + +-- | Learn whether a user has LH enabled and fetch pre-keys. +-- Note that this is accessible to ANY authenticated user, even ones outside the team +getUserStatus :: + forall r. + ( Member (Error InternalError) r, + Member (ErrorS 'TeamMemberNotFound) r, + Member LegalHoldStore r, + Member TeamStore r, + Member P.TinyLog r + ) => + Local UserId -> + TeamId -> + UserId -> + Sem r Public.UserLegalHoldStatusResponse +getUserStatus _lzusr tid uid = do + teamMember <- noteS @'TeamMemberNotFound =<< getTeamMember tid uid + let status = view legalHoldStatus teamMember + (mlk, lcid) <- case status of + UserLegalHoldNoConsent -> pure (Nothing, Nothing) + UserLegalHoldDisabled -> pure (Nothing, Nothing) + UserLegalHoldPending -> makeResponseDetails + UserLegalHoldEnabled -> makeResponseDetails + pure $ UserLegalHoldStatusResponse status mlk lcid + where + makeResponseDetails :: Sem r (Maybe LastPrekey, Maybe ClientId) + makeResponseDetails = do + mLastKey <- fmap snd <$> LegalHoldData.selectPendingPrekeys uid + lastKey <- case mLastKey of + Nothing -> do + P.err + . Log.msg + $ "expected to find a prekey for user: " + <> toByteString' uid + <> " but none was found" + throw NoPrekeyForUser + Just lstKey -> pure lstKey + let clientId = clientIdFromPrekey . unpackLastPrekey $ lastKey + pure (Just lastKey, Just clientId) diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index b5eef0766c4..f3c43774b29 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -29,16 +29,18 @@ module Galley.API.MLS.Message ) where -import Control.Comonad import Data.Domain import Data.Id import Data.Json.Util +import Data.LegalHold import Data.Qualified import Data.Set qualified as Set +import Data.Tagged (Tagged) import Data.Text.Lazy qualified as LT import Data.Tuple.Extra import Galley.API.Action import Galley.API.Error +import Galley.API.LegalHold.Get (getUserStatus) import Galley.API.MLS.Commit.Core (getCommitData) import Galley.API.MLS.Commit.ExternalCommit import Galley.API.MLS.Commit.InternalCommit @@ -58,9 +60,11 @@ import Galley.Effects.ConversationStore import Galley.Effects.FederatorAccess import Galley.Effects.MemberStore import Galley.Effects.SubConversationStore +import Galley.Effects.TeamStore (getUserTeams) import Imports import Polysemy import Polysemy.Error +import Polysemy.Fail import Polysemy.Input import Polysemy.Internal import Polysemy.Output @@ -81,6 +85,7 @@ import Wire.API.MLS.GroupInfo import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation +import Wire.API.Team.LegalHold import Wire.NotificationSubsystem -- FUTUREWORK @@ -148,6 +153,8 @@ postMLSMessageFromLocalUser lusr c conn smsg = do postMLSCommitBundle :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, + Member Fail r, + Member (ErrorS MLSLegalholdIncompatible) r, Member Random r, Member Resource r, Member SubConversationStore r @@ -171,6 +178,8 @@ postMLSCommitBundleFromLocalUser :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, Member Random r, + Member Fail r, + Member (ErrorS MLSLegalholdIncompatible) r, Member Resource r, Member SubConversationStore r ) => @@ -193,8 +202,10 @@ postMLSCommitBundleToLocalConv :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, Member Resource r, + Member (ErrorS MLSLegalholdIncompatible) r, Member SubConversationStore r, - Member Random r + Member Random r, + Member Fail r ) => Qualified UserId -> ClientId -> @@ -211,6 +222,26 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do note (mlsProtocolError "Unsupported ciphersuite") $ cipherSuiteTag bundle.groupInfo.value.groupContext.cipherSuite + -- when a user tries to join any mls conversation while being legalholded + -- they receive a 409 stating that mls and legalhold are incompatible + case qusr `RelativeTo` lConvOrSubId of + Local luid -> + when (isNothing convOrSub.mlsMeta.cnvmlsActiveData) do + usrTeams <- getUserTeams (tUnqualified luid) + for_ usrTeams \tid -> do + -- this would only return 'Left' if the team member did vanish directly in the process of this + -- request or if the legalhold state was somehow inconsistent. We can safely assume that this + -- should be a server error + Right resp <- runError @(Tagged TeamMemberNotFound ()) $ getUserStatus luid tid (tUnqualified luid) + case resp.ulhsrStatus of + UserLegalHoldPending -> throwS @MLSLegalholdIncompatible + UserLegalHoldEnabled -> throwS @MLSLegalholdIncompatible + UserLegalHoldDisabled -> pure () + UserLegalHoldNoConsent -> pure () + + -- we can skip the remote case because we currently to not support creating conversations on the remote backend + Remote _ -> pure () + ciphersuiteUpdate <- case convOrSub.mlsMeta.cnvmlsActiveData of -- if this is the first commit of the conversation, update ciphersuite Nothing -> pure True diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index a7488032814..baa3284e861 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -92,6 +92,7 @@ import OpenSSL.Session as Ssl import Polysemy import Polysemy.Async import Polysemy.Error +import Polysemy.Fail import Polysemy.Input import Polysemy.Internal (Append) import Polysemy.Resource @@ -124,6 +125,7 @@ type GalleyEffects0 = Error FederationError, Async, Delay, + Fail, Embed IO, Error JSONResponse, Resource, @@ -243,6 +245,7 @@ evalGalley e = . resourceToIOFinal . runError . embedToFinal @IO + . failToEmbed @IO . runDelay . asyncToIOFinal . mapError toResponse diff --git a/services/galley/src/Galley/Effects/TeamStore.hs b/services/galley/src/Galley/Effects/TeamStore.hs index 6c6eca8de17..6ce47062720 100644 --- a/services/galley/src/Galley/Effects/TeamStore.hs +++ b/services/galley/src/Galley/Effects/TeamStore.hs @@ -125,6 +125,8 @@ data TeamStore m a where Maybe (PagingState CassandraPaging TeamMember) -> PagingBounds CassandraPaging TeamMember -> TeamStore m (Page CassandraPaging TeamMember) + -- FUTUREWORK(mangoiv): this should be a single 'TeamId' (@'Maybe' 'TeamId'@), there's no way + -- a user could be part of multiple teams GetUserTeams :: UserId -> TeamStore m [TeamId] GetUsersTeams :: [UserId] -> TeamStore m (Map UserId TeamId) GetOneUserTeam :: UserId -> TeamStore m (Maybe TeamId) From a72c70a9a9b9d71af1b864384e80b6d3eb0827a9 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 18 Sep 2024 14:40:34 +0200 Subject: [PATCH 071/136] Work around legacy integration test resource leak. (#4244) --- services/brig/test/integration/Run.hs | 28 +++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/services/brig/test/integration/Run.hs b/services/brig/test/integration/Run.hs index 1b3e0cd563d..f804d364896 100644 --- a/services/brig/test/integration/Run.hs +++ b/services/brig/test/integration/Run.hs @@ -54,6 +54,7 @@ import Options.Applicative hiding (action) import SMTP qualified import System.Environment (withArgs) import System.Logger qualified as Logger +import System.Mem (performGC) import Test.Tasty import Test.Tasty.Ingredients import Test.Tasty.Runners @@ -150,16 +151,14 @@ runTests iConf brigOpts otherArgs = do let smtp = SMTP.tests mg lg oauthAPI = API.OAuth.tests mg db b n brigOpts + -- run the tests in two parts, with a gc in between. i did this on a hunch, and for some + -- reason this reduces the hunger for open file handles at run time significantly, and makes + -- the suite pass with my ulimit settings. (fisx) + withArgs otherArgs . defaultMainWithIngredients (listingTests : (composeReporters antXMLRunner consoleTestReporter) : defaultIngredients) $ testGroup - "Brig API Integration" - $ [ userApi, - providerApi, - searchApis, - teamApis, - turnApi, - metricsApi, - systemSettingsApi, + "Brig API Integration, part 1" + $ [ systemSettingsApi, settingsApi, createIndex, userPendingActivation, @@ -170,6 +169,19 @@ runTests iConf brigOpts otherArgs = do oauthAPI, federationEnd2End ] + + performGC + + withArgs otherArgs . defaultMainWithIngredients (listingTests : (composeReporters antXMLRunner consoleTestReporter) : defaultIngredients) + $ testGroup + "Brig API Integration, part 2" + $ [ userApi, + providerApi, + searchApis, + teamApis, + turnApi, + metricsApi + ] where mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p From 094d6be91339fab57212a7fe54c845f3c508b91a Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Wed, 18 Sep 2024 14:49:28 +0200 Subject: [PATCH 072/136] Fix FromJSON AmqpEndpoint error message (#4248) Use the name defined by our convention to get better error messages. --- changelog.d/5-internal/background-worker | 2 +- libs/extended/src/Network/AMQP/Extended.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.d/5-internal/background-worker b/changelog.d/5-internal/background-worker index d699ef088a6..35afaff745f 100644 --- a/changelog.d/5-internal/background-worker +++ b/changelog.d/5-internal/background-worker @@ -1 +1 @@ -charts/wire-server: Deploy background-worker even when tags.federation is `false` +charts/wire-server: Deploy background-worker even when tags.federation is `false` (#4342, #4248) diff --git a/libs/extended/src/Network/AMQP/Extended.hs b/libs/extended/src/Network/AMQP/Extended.hs index 3d1e79a218b..955e54c0a33 100644 --- a/libs/extended/src/Network/AMQP/Extended.hs +++ b/libs/extended/src/Network/AMQP/Extended.hs @@ -114,7 +114,7 @@ data AmqpEndpoint = AmqpEndpoint deriving (Show) instance FromJSON AmqpEndpoint where - parseJSON = withObject "RabbitMqAdminOpts" $ \v -> + parseJSON = withObject "AmqpEndpoint" $ \v -> AmqpEndpoint <$> v .: "host" <*> v .: "port" From 708c07af82366edeea01ac180520c4aa193bf19c Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 18 Sep 2024 15:23:18 +0200 Subject: [PATCH 073/136] [WPB-1228] Servantify gundeck internal api (#4246) * [WPB-1228] Servantify gundeck internal api * Fix: make cannon use Content-Type header when posting presence. * Make gundeck backwards compatible with previous cannon behavior. --- .../WPB-1228-servantify-gundeck-internal-api | 1 + libs/gundeck-types/default.nix | 2 + libs/gundeck-types/gundeck-types.cabal | 1 + .../gundeck-types/src/Gundeck/Types/Common.hs | 8 + services/brig/src/Brig/Run.hs | 1 - services/cannon/src/Cannon/WS.hs | 2 +- services/gundeck/default.nix | 8 +- services/gundeck/gundeck.cabal | 12 +- services/gundeck/src/Gundeck/API.hs | 29 ---- services/gundeck/src/Gundeck/API/Internal.hs | 157 +++++++++++------- services/gundeck/src/Gundeck/Presence.hs | 41 ++--- services/gundeck/src/Gundeck/Run.hs | 23 +-- services/gundeck/src/Gundeck/Util.hs | 3 - services/gundeck/test/unit/Main.hs | 12 +- 14 files changed, 139 insertions(+), 161 deletions(-) create mode 100644 changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api delete mode 100644 services/gundeck/src/Gundeck/API.hs diff --git a/changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api b/changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api new file mode 100644 index 00000000000..477a424b664 --- /dev/null +++ b/changelog.d/5-internal/WPB-1228-servantify-gundeck-internal-api @@ -0,0 +1 @@ +Servantify gundeck internal api diff --git a/libs/gundeck-types/default.nix b/libs/gundeck-types/default.nix index 522b4e84b10..6c440616b9a 100644 --- a/libs/gundeck-types/default.nix +++ b/libs/gundeck-types/default.nix @@ -14,6 +14,7 @@ , lens , lib , network-uri +, servant , text , types-common , wire-api @@ -32,6 +33,7 @@ mkDerivation { imports lens network-uri + servant text types-common wire-api diff --git a/libs/gundeck-types/gundeck-types.cabal b/libs/gundeck-types/gundeck-types.cabal index 26e75b33b7f..86c33f01a4a 100644 --- a/libs/gundeck-types/gundeck-types.cabal +++ b/libs/gundeck-types/gundeck-types.cabal @@ -78,6 +78,7 @@ library , imports , lens >=4.11 , network-uri >=2.6 + , servant , text >=0.11 , types-common >=0.16 , wire-api diff --git a/libs/gundeck-types/src/Gundeck/Types/Common.hs b/libs/gundeck-types/src/Gundeck/Types/Common.hs index 1158830d1c8..6649db94ba3 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Common.hs +++ b/libs/gundeck-types/src/Gundeck/Types/Common.hs @@ -24,8 +24,10 @@ import Data.Attoparsec.ByteString (takeByteString) import Data.ByteString.Char8 qualified as Bytes import Data.ByteString.Conversion import Data.Text qualified as Text +import Data.Text.Encoding (decodeUtf8) import Imports import Network.URI qualified as Net +import Servant.API (FromHttpApiData (parseUrlPiece), ToHttpApiData (toUrlPiece)) newtype CannonId = CannonId { cannonId :: Text @@ -40,6 +42,9 @@ newtype CannonId = CannonId ToByteString ) +instance FromHttpApiData CannonId where + parseUrlPiece = pure . CannonId + newtype URI = URI { fromURI :: Net.URI } @@ -57,5 +62,8 @@ instance ToByteString URI where instance FromByteString URI where parser = takeByteString >>= parse . Bytes.unpack +instance ToHttpApiData URI where + toUrlPiece = decodeUtf8 . toByteString' + parse :: (MonadFail m) => String -> m URI parse = maybe (fail "Invalid URI") (pure . URI) . Net.parseURI diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 08486c43d31..3be27a1c937 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -126,7 +126,6 @@ mkApp o = do . GZip.gzip GZip.def . catchErrors (e ^. applog) defaultRequestIdHeaderName - -- the servant API wraps the one defined using wai-routing servantApp :: Env -> Wai.Application servantApp e0 req cont = do let rid = getRequestId defaultRequestIdHeaderName req diff --git a/services/cannon/src/Cannon/WS.hs b/services/cannon/src/Cannon/WS.hs index 2b9a816df20..f551434b599 100644 --- a/services/cannon/src/Cannon/WS.hs +++ b/services/cannon/src/Cannon/WS.hs @@ -329,7 +329,7 @@ regInfo k c = do let h = externalHostname e p = portnum e r = "http://" <> h <> ":" <> pack (show p) <> "/i/push/" - pure . lbytes . encode . object $ + pure . Bilge.json . object $ [ "user_id" .= decodeUtf8 (keyUserBytes k), "device_id" .= decodeUtf8 (keyConnBytes k), "resource" .= decodeUtf8 (r <> keyUserBytes k <> "/" <> keyConnBytes k), diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index 79051e52a81..d797346cf43 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -61,6 +61,7 @@ , safe , safe-exceptions , scientific +, servant , servant-server , string-conversions , tagged @@ -80,8 +81,6 @@ , wai , wai-extra , wai-middleware-gunzip -, wai-predicates -, wai-routing , wai-utilities , websockets , wire-api @@ -135,6 +134,7 @@ mkDerivation { resourcet retry safe-exceptions + servant servant-server text time @@ -148,8 +148,6 @@ mkDerivation { wai wai-extra wai-middleware-gunzip - wai-predicates - wai-routing wai-utilities wire-api wire-otel @@ -210,7 +208,6 @@ mkDerivation { HsOpenSSL imports lens - metrics-wai MonadRandom mtl multiset @@ -226,7 +223,6 @@ mkDerivation { text tinylog types-common - wai-utilities wire-api ]; benchmarkHaskellDepends = [ diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index cce46169df8..b2e14c44908 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -18,7 +18,6 @@ flag static library -- cabal-fmt: expand src exposed-modules: - Gundeck.API Gundeck.API.Internal Gundeck.API.Public Gundeck.Aws @@ -143,7 +142,7 @@ library , lens >=4.4 , lens-aeson >=1.0 , metrics-core >=0.2.1 - , metrics-wai >=0.5.7 + , metrics-wai , mtl >=2.2 , network-uri >=2.6 , prometheus-client @@ -152,6 +151,7 @@ library , resourcet >=1.1 , retry >=0.5 , safe-exceptions + , servant , servant-server , text >=1.1 , time >=1.4 @@ -165,8 +165,6 @@ library , wai >=3.2 , wai-extra >=3.0 , wai-middleware-gunzip >=0.0.2 - , wai-predicates >=0.8 - , wai-routing >=0.12 , wai-utilities >=0.16 , wire-api , wire-otel @@ -297,7 +295,7 @@ executable gundeck-integration build-depends: , aeson , async - , base >=4 && <5 + , base >=4 && <5 , base16-bytestring >=0.1 , bilge , bytestring @@ -328,7 +326,7 @@ executable gundeck-integration , tinylog , types-common , uuid - , wai-utilities + , wai-utilities >=0.16 , websockets >=0.8 , wire-api , yaml @@ -546,7 +544,6 @@ test-suite gundeck-tests , HsOpenSSL , imports , lens - , metrics-wai , MonadRandom , mtl , multiset @@ -562,7 +559,6 @@ test-suite gundeck-tests , text , tinylog , types-common - , wai-utilities , wire-api default-language: GHC2021 diff --git a/services/gundeck/src/Gundeck/API.hs b/services/gundeck/src/Gundeck/API.hs deleted file mode 100644 index eca27d6cfed..00000000000 --- a/services/gundeck/src/Gundeck/API.hs +++ /dev/null @@ -1,29 +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 Gundeck.API - ( sitemap, - ) -where - -import Gundeck.API.Internal qualified as Internal -import Gundeck.Monad (Gundeck) -import Network.Wai.Routing (Routes) - -sitemap :: Routes () Gundeck () -sitemap = do - Internal.sitemap diff --git a/services/gundeck/src/Gundeck/API/Internal.hs b/services/gundeck/src/Gundeck/API/Internal.hs index 357d49bfe83..49c2448bd3a 100644 --- a/services/gundeck/src/Gundeck/API/Internal.hs +++ b/services/gundeck/src/Gundeck/API/Internal.hs @@ -16,76 +16,113 @@ -- with this program. If not, see . module Gundeck.API.Internal - ( sitemap, + ( type GundeckInternalAPI, + servantSitemap, ) where import Cassandra qualified import Control.Lens (view) +import Data.Aeson (eitherDecode) +import Data.CommaSeparatedList import Data.Id +import Data.Metrics.Servant +import Data.Typeable import Gundeck.Client qualified as Client import Gundeck.Monad import Gundeck.Presence qualified as Presence import Gundeck.Push qualified as Push import Gundeck.Push.Data qualified as PushTok import Gundeck.Push.Native.Types qualified as PushTok -import Imports hiding (head) -import Network.Wai -import Network.Wai.Predicate hiding (setStatus) -import Network.Wai.Routing hiding (route) -import Network.Wai.Utilities +import Gundeck.Types.Presence as GD +import Gundeck.Types.Push.V2 +import Imports +import Network.Wai (lazyRequestBody) +import Servant +import Servant.Server.Internal.Delayed +import Servant.Server.Internal.DelayedIO +import Servant.Server.Internal.ErrorFormatter import Wire.API.Push.Token qualified as PushTok - -sitemap :: Routes a Gundeck () -sitemap = do - head "/i/status" (continue $ const (pure empty)) true - get "/i/status" (continue $ const (pure empty)) true - - -- Push API ----------------------------------------------------------- - - post "/i/push/v2" (continue pushH) $ - request .&. accept "application" "json" - - -- Presence API ---------------------------------------------------------- - - get "/i/presences/:uid" (continue Presence.list) $ - param "uid" .&. accept "application" "json" - - get "/i/presences" (continue Presence.listAll) $ - param "ids" .&. accept "application" "json" - - post "/i/presences" (continue Presence.add) $ - request .&. accept "application" "json" - - delete "/i/presences/:uid/devices/:did/cannons/:cannon" (continue Presence.remove) $ - param "uid" .&. param "did" .&. param "cannon" - - -- User-Client API ------------------------------------------------------- - - delete "/i/clients/:cid" (continue unregisterClientH) $ - header "Z-User" .&. param "cid" - - delete "/i/user" (continue removeUserH) $ - header "Z-User" - - get "/i/push-tokens/:uid" (continue getPushTokensH) $ - param "uid" - -type JSON = Media "application" "json" - -pushH :: Request ::: JSON -> Gundeck Response -pushH (req ::: _) = do - ps <- fromJsonBody (JsonRequest req) - empty <$ Push.push ps - -unregisterClientH :: UserId ::: ClientId -> Gundeck Response -unregisterClientH (uid ::: cid) = empty <$ Client.unregister uid cid - -removeUserH :: UserId -> Gundeck Response -removeUserH uid = empty <$ Client.removeUser uid - -getPushTokensH :: UserId -> Gundeck Response -getPushTokensH = fmap json . getPushTokens - -getPushTokens :: UserId -> Gundeck PushTok.PushTokenList -getPushTokens uid = PushTok.PushTokenList <$> (view PushTok.addrPushToken <$$> PushTok.lookup uid Cassandra.All) +import Wire.API.Routes.Public + +-- | this can be replaced by `ReqBody '[JSON] Presence` once the fix in cannon from +-- https://github.com/wireapp/wire-server/pull/4246 has been deployed everywhere. +data ReqBodyHack + +-- | cloned from instance for ReqBody'. +instance + ( HasServer api context, + HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters + ) => + HasServer (ReqBodyHack :> api) context + where + type ServerT (ReqBodyHack :> api) m = Presence -> ServerT api m + + hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s + + route Proxy context subserver = + route (Proxy :: Proxy api) context $ + addBodyCheck subserver ctCheck bodyCheck + where + rep = typeRep (Proxy :: Proxy ReqBodyHack) + formatError = bodyParserErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context) + + ctCheck = pure eitherDecode + + -- Body check, we get a body parsing functions as the first argument. + bodyCheck f = withRequest $ \request -> do + mrqbody <- f <$> liftIO (lazyRequestBody request) + case mrqbody of + Left e -> delayedFailFatal $ formatError rep request e + Right v -> pure v + +-- | cloned from instance for ReqBody'. +instance + (RoutesToPaths rest) => + RoutesToPaths (ReqBodyHack :> rest) + where + getRoutes = getRoutes @rest + +type GundeckInternalAPI = + "i" + :> ( ("status" :> Get '[JSON] NoContent) + :<|> ("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" GD.URI] NoContent)) + :<|> (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) + ) + +servantSitemap :: ServerT GundeckInternalAPI Gundeck +servantSitemap = + statusH + :<|> pushH + :<|> ( Presence.listAllH + :<|> Presence.listH + :<|> Presence.addH + :<|> Presence.removeH + ) + :<|> unregisterClientH + :<|> removeUserH + :<|> getPushTokensH + +statusH :: (Applicative m) => m NoContent +statusH = pure NoContent + +pushH :: [Push] -> Gundeck NoContent +pushH ps = NoContent <$ Push.push ps + +unregisterClientH :: UserId -> ClientId -> Gundeck NoContent +unregisterClientH uid cid = NoContent <$ Client.unregister uid cid + +removeUserH :: UserId -> Gundeck NoContent +removeUserH uid = NoContent <$ Client.removeUser uid + +getPushTokensH :: UserId -> Gundeck PushTok.PushTokenList +getPushTokensH uid = PushTok.PushTokenList <$> (view PushTok.addrPushToken <$$> PushTok.lookup uid Cassandra.All) diff --git a/services/gundeck/src/Gundeck/Presence.hs b/services/gundeck/src/Gundeck/Presence.hs index 4c626fe35ee..ed69bb50515 100644 --- a/services/gundeck/src/Gundeck/Presence.hs +++ b/services/gundeck/src/Gundeck/Presence.hs @@ -16,42 +16,31 @@ -- with this program. If not, see . module Gundeck.Presence - ( list, - listAll, - add, - remove, + ( listH, + listAllH, + addH, + removeH, ) where -import Data.ByteString.Conversion +import Data.CommaSeparatedList import Data.Id -import Data.Predicate import Gundeck.Monad import Gundeck.Presence.Data qualified as Data import Gundeck.Types -import Gundeck.Util import Imports -import Network.HTTP.Types -import Network.Wai (Request, Response) -import Network.Wai.Utilities +import Servant.API -list :: UserId ::: JSON -> Gundeck Response -list (uid ::: _) = setStatus status200 . json <$> runWithDefaultRedis (Data.list uid) +listH :: UserId -> Gundeck [Presence] +listH = runWithDefaultRedis . Data.list -listAll :: List UserId ::: JSON -> Gundeck Response -listAll (uids ::: _) = - setStatus status200 . json . concat - <$> runWithDefaultRedis (Data.listAll (fromList uids)) +listAllH :: CommaSeparatedList UserId -> Gundeck [Presence] +listAllH uids = concat <$> runWithDefaultRedis (Data.listAll (fromCommaSeparatedList uids)) -add :: Request ::: JSON -> Gundeck Response -add (req ::: _) = do - p <- fromJsonBody (JsonRequest req) +addH :: Presence -> Gundeck (Headers '[Header "Location" Gundeck.Types.URI] NoContent) +addH p = do Data.add p - pure $ - ( setStatus status201 - . addHeader hLocation (toByteString' (resource p)) - ) - empty + pure (addHeader (resource p) NoContent) -remove :: UserId ::: ConnId ::: CannonId -> Gundeck Response -remove _ = pure (empty & setStatus status204) +removeH :: UserId -> ConnId -> CannonId -> Gundeck NoContent +removeH _ _ _ = pure NoContent diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index a896171a13c..0f8c7a13fd4 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -27,12 +27,12 @@ import Control.Exception (finally) import Control.Lens ((.~), (^.)) import Control.Monad.Extra import Data.Metrics.AWS (gaugeTokenRemaing) -import Data.Metrics.Middleware.Prometheus (waiPrometheusMiddleware) +import Data.Metrics.Servant qualified as Metrics import Data.Proxy (Proxy (Proxy)) import Data.Text (unpack) import Database.Redis qualified as Redis -import Gundeck.API (sitemap) -import Gundeck.API.Public (servantSitemap) +import Gundeck.API.Internal as Internal (GundeckInternalAPI, servantSitemap) +import Gundeck.API.Public as Public (servantSitemap) import Gundeck.Aws qualified as Aws import Gundeck.Env import Gundeck.Env qualified as Env @@ -92,28 +92,19 @@ run o = withTracer \tracer -> do versionMiddleware (foldMap expandVersionExp (o ^. settings . disabledAPIVersions)) . otelMiddleWare . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName - . waiPrometheusMiddleware sitemap + . Metrics.servantPrometheusMiddleware (Proxy @(GundeckAPI :<|> GundeckInternalAPI)) . GZip.gunzip . GZip.gzip GZip.def . catchErrors (e ^. applog) defaultRequestIdHeaderName -type CombinedAPI = GundeckAPI :<|> Servant.Raw - mkApp :: Env -> Wai.Application mkApp env0 req cont = do let rid = getRequestId defaultRequestIdHeaderName req env = reqId .~ rid $ env0 - Servant.serve - (Proxy @CombinedAPI) - (servantSitemap' env :<|> Servant.Tagged (runGundeckWithRoutes env)) - req - cont - where - runGundeckWithRoutes :: Env -> Wai.Application - runGundeckWithRoutes e r k = runGundeck e r (route (compile sitemap) r k) + Servant.serve (Proxy @(GundeckAPI :<|> GundeckInternalAPI)) (servantSitemap' env) req cont -servantSitemap' :: Env -> Servant.Server GundeckAPI -servantSitemap' env = Servant.hoistServer (Proxy @GundeckAPI) toServantHandler servantSitemap +servantSitemap' :: Env -> Servant.Server (GundeckAPI :<|> GundeckInternalAPI) +servantSitemap' env = Servant.hoistServer (Proxy @(GundeckAPI :<|> GundeckInternalAPI)) toServantHandler (Public.servantSitemap :<|> Internal.servantSitemap) where toServantHandler :: Gundeck a -> Handler a toServantHandler m = Handler . ExceptT $ Right <$> runDirect env m diff --git a/services/gundeck/src/Gundeck/Util.hs b/services/gundeck/src/Gundeck/Util.hs index 5bc0e77f724..831ae3955af 100644 --- a/services/gundeck/src/Gundeck/Util.hs +++ b/services/gundeck/src/Gundeck/Util.hs @@ -23,13 +23,10 @@ import Data.Id import Data.UUID.V1 import Imports import Network.HTTP.Types.Status -import Network.Wai.Predicate.MediaType (Media) import Network.Wai.Utilities import UnliftIO (async, waitCatch) import Wire.API.Internal.Notification -type JSON = Media "application" "json" - -- | 'Data.UUID.V1.nextUUID' is sometimes unsuccessful, so we try a few times. mkNotificationId :: (MonadIO m, MonadThrow m) => m NotificationId mkNotificationId = do diff --git a/services/gundeck/test/unit/Main.hs b/services/gundeck/test/unit/Main.hs index 332418beb38..826e49f401f 100644 --- a/services/gundeck/test/unit/Main.hs +++ b/services/gundeck/test/unit/Main.hs @@ -21,19 +21,14 @@ module Main where import Aws.Arn qualified -import Data.Metrics.Test (pathsConsistencyCheck) -import Data.Metrics.WaiRoute (treeToPaths) import DelayQueue qualified -import Gundeck.API qualified import Imports import Json qualified import Native qualified -import Network.Wai.Utilities.Server (compile) import OpenSSL (withOpenSSL) import ParseExistsError qualified import Push qualified import Test.Tasty -import Test.Tasty.HUnit import ThreadBudget qualified main :: IO () @@ -41,12 +36,7 @@ main = withOpenSSL . defaultMain $ testGroup "Main" - [ testCase "sitemap" $ - assertEqual - "inconcistent sitemap" - mempty - (pathsConsistencyCheck . treeToPaths . compile $ Gundeck.API.sitemap), - DelayQueue.tests, + [ DelayQueue.tests, Json.tests, Native.tests, Push.tests, From dee9f3fcca9e478492f86ab2bb6ae48bb5cd6d3b Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:06:01 +0200 Subject: [PATCH 074/136] [WPB-10783] Prevent MLS-Legalhold interactions (#4245) Co-authored-by: Akshay Mankar Co-authored-by: Igor Ranieri --- changelog.d/2-features/block-lh-for-mls-users | 1 + integration/test/Test/LegalHold.hs | 26 ++++++++++++++++--- .../API/Routes/Public/Galley/LegalHold.hs | 1 + services/galley/src/Galley/API/LegalHold.hs | 9 +++++++ 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 changelog.d/2-features/block-lh-for-mls-users diff --git a/changelog.d/2-features/block-lh-for-mls-users b/changelog.d/2-features/block-lh-for-mls-users new file mode 100644 index 00000000000..cc86b5c4512 --- /dev/null +++ b/changelog.d/2-features/block-lh-for-mls-users @@ -0,0 +1 @@ +Deny requests for a legalhold device for users who are part of any MLS conversations \ No newline at end of file diff --git a/integration/test/Test/LegalHold.hs b/integration/test/Test/LegalHold.hs index f359d54d2c3..4b70fd0d454 100644 --- a/integration/test/Test/LegalHold.hs +++ b/integration/test/Test/LegalHold.hs @@ -906,6 +906,24 @@ testLHDisableBeforeApproval = do >>= assertStatus 200 getBob'sStatus `shouldMatch` "disabled" +-- --------- +-- WPB-10783 +-- --------- +testBlockLHForMLSUsers :: (HasCallStack) => App () +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 + + legalholdWhitelistTeam tid charlie >>= assertStatus 200 + withMockServer def lhMockApp \lhDomAndPort _chan -> do + postLegalHoldSettings tid charlie (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + requestLegalHoldDevice tid charlie charlie `bindResponse` do + assertLabel 409 "mls-legal-hold-not-allowed" + -- --------- -- WPB-10772 -- --------- @@ -913,8 +931,8 @@ testLHDisableBeforeApproval = do -- | scenario 2.1: -- charlie first is put under legalhold and after that wants to join an MLS conversation -- claiming a keypackage of charlie to add them to a conversation should not be possible -testLegalholdThenMLSThirdParty :: (HasCallStack) => App () -testLegalholdThenMLSThirdParty = do +testBlockClaimingKeyPackageForLHUsers :: (HasCallStack) => App () +testBlockClaimingKeyPackageForLHUsers = do (alice, tid, [charlie]) <- createTeam OwnDomain 2 [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] _ <- uploadNewKeyPackage charlie1 @@ -937,8 +955,8 @@ testLegalholdThenMLSThirdParty = do -- since he doesn't need to claim his own keypackage to do so, this would succeed -- we need to check upon group creation if the user is under legalhold and reject -- the operation if they are -testLegalholdThenMLSSelf :: (HasCallStack) => App () -testLegalholdThenMLSSelf = do +testBlockCreateMLSConvForLHUsers :: (HasCallStack) => App () +testBlockCreateMLSConvForLHUsers = do (alice, tid, [charlie]) <- createTeam OwnDomain 2 [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] _ <- uploadNewKeyPackage alice1 diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs index a9d7ebe219d..f4506b7fcfb 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs @@ -133,6 +133,7 @@ type LegalHoldAPI = :> CanThrow 'LegalHoldServiceBadResponse :> CanThrow 'LegalHoldServiceNotRegistered :> CanThrow 'LegalHoldCouldNotBlockConnections + :> CanThrow 'MLSLegalholdIncompatible :> CanThrow 'UserLegalHoldIllegalOperation :> Description "This endpoint can lead to the following events being sent:\n\ diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index cd2227ff4e3..3c15d5c1e53 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -68,6 +68,7 @@ import Polysemy.Input import Polysemy.TinyLog qualified as P import System.Logger.Class qualified as Log import Wire.API.Conversation (ConvType (..)) +import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley @@ -345,6 +346,7 @@ requestDevice :: Member (ErrorS 'LegalHoldNotEnabled) r, Member (ErrorS 'LegalHoldServiceBadResponse) r, Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member (ErrorS 'MLSLegalholdIncompatible) r, Member (ErrorS 'NotATeamMember) r, Member (ErrorS 'NoUserLegalHoldConsent) r, Member (ErrorS OperationDenied) r, @@ -392,6 +394,12 @@ requestDevice lzusr tid uid = do lhs@UserLegalHoldDisabled -> RequestDeviceSuccess <$ provisionLHDevice zusr luid lhs UserLegalHoldNoConsent -> throwS @'NoUserLegalHoldConsent where + disallowIfMLSUser :: Local UserId -> Sem r () + disallowIfMLSUser luid = do + void $ iterateConversations luid (toRange (Proxy @500)) $ \convs -> do + when (any (\c -> c.convProtocol /= ProtocolProteus) convs) $ do + throwS @'MLSLegalholdIncompatible + -- Wire's LH service that galley is usually calling here is idempotent in device creation, -- ie. it returns the existing device on multiple calls to `/init`, like here: -- https://github.com/wireapp/legalhold/blob/e0a241162b9dbc841f12fbc57c8a1e1093c7e83a/src/main/java/com/wire/bots/hold/resource/InitiateResource.java#L42 @@ -401,6 +409,7 @@ requestDevice lzusr tid uid = do -- device at (almost) the same time. provisionLHDevice :: UserId -> Local UserId -> UserLegalHoldStatus -> Sem r () provisionLHDevice zusr luid userLHStatus = do + disallowIfMLSUser luid (lastPrekey', prekeys) <- requestDeviceFromService luid -- We don't distinguish the last key here; brig will do so when the device is added LegalHoldData.insertPendingPrekeys (tUnqualified luid) (unpackLastPrekey lastPrekey' : prekeys) From 7bad42cf4590bc9fb972bb73d573c5bc5d860e9c Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:23:35 +0200 Subject: [PATCH 075/136] Replace pattern synomyn with ADT. (#4252) --- libs/types-common/src/Data/Qualified.hs | 50 +++++++++---------- services/galley/src/Galley/API/MLS/Message.hs | 2 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index 0dbc73f99ec..18407f557fd 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -25,13 +25,14 @@ module Data.Qualified Qualified (..), qToPair, QualifiedWithTag, + RelativeTo (..), + relativeTo, tUnqualified, tDomain, tUntagged, tSplit, qTagUnsafe, Remote, - RelativeTo (Remote, Local, RelativeTo), toRemoteUnsafe, Local, toLocalUnsafe, @@ -111,35 +112,37 @@ toRemoteUnsafe d a = qTagUnsafe $ Qualified a d -- be local. type Local = QualifiedWithTag 'QLocal --- | Convert a 'Domain' and an @a@ to a 'Local' value. This is only safe if we --- already know that the domain is local. -toLocalUnsafe :: Domain -> a -> Local a -toLocalUnsafe d a = qTagUnsafe $ Qualified a d - -- | Convert an unqualified value to a qualified one, with the same tag as the -- given tagged qualified value. qualifyAs :: QualifiedWithTag t x -> a -> QualifiedWithTag t a qualifyAs = ($>) -foldQualified :: Local x -> (Local a -> b) -> (Remote a -> b) -> Qualified a -> b -foldQualified loc kLocal kRemote q = case q `RelativeTo` loc of - Local l -> kLocal l - Remote r -> kRemote r - -data a `RelativeTo` x = Qualified a `RelativeTo` Local x - -checkRelative :: a `RelativeTo` x -> Either (Local a) (Remote a) -checkRelative (q `RelativeTo` loc) - | tDomain loc == qDomain q = Left (qTagUnsafe q) - | otherwise = Right (qTagUnsafe q) +data RelativeTo a + = Local (Local a) + | Remote (Remote a) -pattern Local :: forall a x. Local a -> a `RelativeTo` x -pattern Local loc <- (checkRelative -> Left loc) +foldQualified :: Local x -> (Local a -> b) -> (Remote a -> b) -> Qualified a -> b +foldQualified loc kLocal kRemote q = + case q `relativeTo` loc of + Local l -> kLocal l + Remote r -> kRemote r + +relativeTo :: Qualified a -> Local loc -> RelativeTo a +relativeTo q loc + | tDomain loc == qDomain q = + Local (qTagUnsafe q) + | otherwise = + Remote (qTagUnsafe q) -pattern Remote :: forall a x. Remote a -> a `RelativeTo` x -pattern Remote rem <- (checkRelative -> Right rem) +isLocal :: Local x -> Qualified a -> Bool +isLocal loc q = case q `relativeTo` loc of + Local _ -> True + Remote _ -> False -{-# COMPLETE Local, Remote #-} +-- | Convert a 'Domain' and an @a@ to a 'Local' value. This is only safe if we +-- already know that the domain is local. +toLocalUnsafe :: Domain -> a -> Local a +toLocalUnsafe d a = qTagUnsafe $ Qualified a d -- Partition a collection of qualified values into locals and remotes. -- @@ -174,9 +177,6 @@ bucketRemote = . indexQualified . fmap tUntagged -isLocal :: Local x -> Qualified a -> Bool -isLocal loc = foldQualified loc (const True) (const False) - ---------------------------------------------------------------------- deprecatedSchema :: (S.HasDeprecated doc (Maybe Bool), S.HasDescription doc (Maybe Text)) => Text -> ValueSchema doc a -> ValueSchema doc a diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index f3c43774b29..75de5388c22 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -224,7 +224,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do -- when a user tries to join any mls conversation while being legalholded -- they receive a 409 stating that mls and legalhold are incompatible - case qusr `RelativeTo` lConvOrSubId of + case qusr `relativeTo` lConvOrSubId of Local luid -> when (isNothing convOrSub.mlsMeta.cnvmlsActiveData) do usrTeams <- getUserTeams (tUnqualified luid) From f184788c3704b92ba63245732711efa33762f5c0 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 18 Sep 2024 17:30:48 +0200 Subject: [PATCH 076/136] brig: Make `GET /services/tags` work again (#4250) --- changelog.d/3-bug-fixes/services-tags | 1 + libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 changelog.d/3-bug-fixes/services-tags diff --git a/changelog.d/3-bug-fixes/services-tags b/changelog.d/3-bug-fixes/services-tags new file mode 100644 index 00000000000..9d0ef1900f7 --- /dev/null +++ b/changelog.d/3-bug-fixes/services-tags @@ -0,0 +1 @@ +brig: Make `GET /services/tags` work again \ No newline at end of file diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs index 0fff51c6f9a..df62901e3ee 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Services.hs @@ -129,6 +129,8 @@ type ServicesAPI = ( Summary "Get services tags" :> CanThrow 'AccessDenied :> ZUser + :> "services" + :> "tags" :> Get '[JSON] ServiceTagList ) :<|> Named From c04e58321ffb29a93ea48ea0f9fee176328f338c Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 19 Sep 2024 06:00:26 +0200 Subject: [PATCH 077/136] Move search operations to UserSubsystem (#4188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * UserStore.Cassandra: Dedup embed call * Move all indexing operations to UserSearchSubsystem Pending: - Index Management - Search - Metrics - Update brig to use the new code (currently, brig is just broken) * Add metrics to UserSearchSubsystem and IndexedUserStore Some metrics have been deleted as they are for bulk operations and there is no way for us to get those metrics because these operations don't run in an http request. * Run user serach index data migrations using subsystems * Remove explicit creds from ESConn * Delete leftover code from Brig.Index.Eval * Move UserDoc tests to subsystem Delete test for indexToDoc because it was only used in the test * indexUserRow -> indexUser * brig: Delete bulk reindex operations from internal API * brig: Use the UserSearchSubsystem for syncing with index Query operations are still pending * regen nix * Rename an effect action * WIP: Move search code to subsystem * Move FederationConfigStore.Cassandra out of Brig * Remove dead code from Brig.User.API.Search * Move browseTeam to wire-subsystem * wire-subsystems: Fix compile errors with MiniBackend Also accomodates some changes from future commits * Merge UserSearchSubsystem into UserSubsystem * Wire.BlockListStore.Cassandra: Take ClientState as an arg instead of putting MonadClient constraint * brig: Untangle sending user notifs and maintaining internal state Brig.IO.Intra.onUserEvent used to keep the search index up to date, send user events and send journal events. The keeping the search index up to date part is now bubbled up to all the places which were calling this function. This commit also deals with there no longer being UserSearchSubsystem. * Fix merge mistakes * Delete leftover comment * UserStore.GetIndexUsersPaginated: Allow specifying page size Use 1000 as the size while running index migrations * wire-subsystems: Fix mock interpreters * Dedup IndexError * Remove TODO * Reorganize Bulk operations * UserSearch.Types: Remove comment The JSON instances are already not compatible the conversion happens explicitly in application code * Move expectedMigrationVersion to IndexedUserStore.Bulk * regen nix * Removed duplicated function. * Brig.API.User.onActivated: update the index when email changes * Promote suspected bug to confirmed bug, to be solved in a separate ticket * Another bug reported for another ticket * Resolve todo: Moved a function, drive-by clean-up of lenses overuse. * Update user search index on team changes. * Pass casClient instead of embed. * Error for searcher doesn't exist. * Removed TODO, out of scope. * Upgraded TODO to FUTUREWORK. * Added changelogs. * UserSubsystem: simplify folding over a domain * Bubble up liftSem'ing * Remove commented out code * Error messages for mocks/uninterpreted actions * Remove a UserSubsystemError * Remove unusued MapError instance --------- Co-authored-by: Marko Dimjašević Co-authored-by: Igor Ranieri --- changelog.d/5-internal/WPB-888-2 | 1 + changelog.d/5-internal/WPB-8888 | 1 + integration/test/Test/Teams.hs | 8 + libs/brig-types/brig-types.cabal | 7 +- libs/brig-types/default.nix | 7 - libs/brig-types/src/Brig/Types/Search.hs | 107 ---- .../test/unit/Test/Brig/Types/User.hs | 2 - libs/cassandra-util/src/Cassandra/Exec.hs | 24 + libs/cassandra-util/src/Cassandra/Util.hs | 1 + libs/polysemy-wire-zoo/default.nix | 2 + .../polysemy-wire-zoo/polysemy-wire-zoo.cabal | 6 +- .../polysemy-wire-zoo/src/Wire/Sem/Metrics.hs | 21 + .../src/Wire/Sem/Metrics/IO.hs | 16 + libs/wire-api/src/Wire/API/Error/Brig.hs | 1 + .../API/Routes/Internal/Brig/SearchIndex.hs | 18 - .../src/Wire/API/Routes/Public/Brig.hs | 4 +- libs/wire-api/src/Wire/API/Team/Feature.hs | 3 +- libs/wire-api/src/Wire/API/Team/Member.hs | 8 +- libs/wire-api/src/Wire/API/Team/Permission.hs | 16 +- .../Wire/API/Golden/Generated/Event_team.hs | 8 +- .../Golden/Generated/NewTeamMember_team.hs | 68 +- .../API/Golden/Generated/Permissions_team.hs | 78 +-- .../Golden/Generated/TeamMemberList_team.hs | 234 +++---- .../API/Golden/Generated/TeamMember_team.hs | 70 +-- .../test/unit/Test/Wire/API/Team/Member.hs | 23 +- libs/wire-subsystems/default.nix | 13 + .../src/Wire/BlockListStore/Cassandra.hs | 12 +- .../Wire/FederationAPIAccess/Interpreter.hs | 9 + .../src/Wire}/FederationConfigStore.hs | 4 +- .../Wire}/FederationConfigStore/Cassandra.hs | 15 +- .../src/Wire/GalleyAPIAccess.hs | 6 + .../src/Wire/GalleyAPIAccess/Rpc.hs | 20 + .../src/Wire/IndexedUserStore.hs | 43 ++ .../src/Wire/IndexedUserStore/Bulk.hs | 22 + .../IndexedUserStore/Bulk/ElasticSearch.hs | 133 ++++ .../Wire/IndexedUserStore/ElasticSearch.hs | 500 +++++++++++++++ .../Wire/IndexedUserStore/MigrationStore.hs | 13 + .../MigrationStore/ElasticSearch.hs | 73 +++ .../src/Wire/UserSearch/Metrics.hs | 44 ++ .../src/Wire/UserSearch/Migration.hs | 30 + .../src/Wire/UserSearch/Types.hs | 207 +++++++ libs/wire-subsystems/src/Wire/UserStore.hs | 5 +- .../src/Wire/UserStore/Cassandra.hs | 58 +- .../src/Wire/UserStore/IndexUser.hs | 200 ++++++ .../wire-subsystems/src/Wire/UserSubsystem.hs | 76 +++ .../src/Wire/UserSubsystem/Error.hs | 2 + .../src/Wire/UserSubsystem/Interpreter.hs | 296 ++++++++- .../test/unit/Wire/MiniBackend.hs | 10 + .../test/unit/Wire/MockInterpreters.hs | 2 + .../MockInterpreters/FederationConfigStore.hs | 36 ++ .../Wire/MockInterpreters/GalleyAPIAccess.hs | 6 + .../Wire/MockInterpreters/IndexedUserStore.hs | 15 + .../unit/Wire/MockInterpreters/UserStore.hs | 28 + .../test/unit/Wire/UserSearch/TypesSpec.hs | 54 ++ .../Wire/UserSubsystem/InterpreterSpec.hs | 55 +- libs/wire-subsystems/wire-subsystems.cabal | 22 + services/brig/brig.cabal | 17 - services/brig/default.nix | 5 - services/brig/src/Brig/API/Auth.hs | 45 +- services/brig/src/Brig/API/Client.hs | 51 +- services/brig/src/Brig/API/Connection.hs | 2 +- .../brig/src/Brig/API/Connection/Remote.hs | 2 +- services/brig/src/Brig/API/Federation.hs | 13 +- services/brig/src/Brig/API/Internal.hs | 192 +++--- services/brig/src/Brig/API/Public.hs | 143 ++--- services/brig/src/Brig/API/User.hs | 130 ++-- services/brig/src/Brig/App.hs | 1 + .../brig/src/Brig/CanonicalInterpreter.hs | 35 +- services/brig/src/Brig/IO/Intra.hs | 42 +- services/brig/src/Brig/Index/Eval.hs | 164 +++-- services/brig/src/Brig/Index/Migrations.hs | 173 ------ .../brig/src/Brig/Index/Migrations/Types.hs | 100 --- .../brig/src/Brig/InternalEvent/Process.hs | 15 +- services/brig/src/Brig/Provider/API.hs | 42 +- services/brig/src/Brig/Team/API.hs | 136 ++-- services/brig/src/Brig/Team/Util.hs | 68 -- services/brig/src/Brig/User/API/Search.hs | 190 ------ services/brig/src/Brig/User/Auth.hs | 52 +- services/brig/src/Brig/User/Search/Index.hs | 584 +----------------- .../brig/src/Brig/User/Search/Index/Types.hs | 230 ------- .../brig/src/Brig/User/Search/SearchIndex.hs | 18 +- .../brig/src/Brig/User/Search/TeamSize.hs | 1 + .../src/Brig/User/Search/TeamUserSearch.hs | 175 ------ services/brig/test/unit/Run.hs | 4 +- .../unit/Test/Brig/User/Search/Index/Types.hs | 84 --- services/galley/galley.cabal | 1 - .../src/V1_BackfillBillingTeamMembers.hs | 3 +- services/galley/src/Galley/API/Teams.hs | 4 +- services/galley/src/Galley/Cassandra/Team.hs | 2 +- 89 files changed, 2828 insertions(+), 2639 deletions(-) create mode 100644 changelog.d/5-internal/WPB-888-2 create mode 100644 changelog.d/5-internal/WPB-8888 delete mode 100644 libs/brig-types/src/Brig/Types/Search.hs create mode 100644 libs/polysemy-wire-zoo/src/Wire/Sem/Metrics.hs create mode 100644 libs/polysemy-wire-zoo/src/Wire/Sem/Metrics/IO.hs rename {services/brig/src/Brig/Effects => libs/wire-subsystems/src/Wire}/FederationConfigStore.hs (90%) rename {services/brig/src/Brig/Effects => libs/wire-subsystems/src/Wire}/FederationConfigStore/Cassandra.hs (97%) create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore.hs create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore.hs create mode 100644 libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore/ElasticSearch.hs create mode 100644 libs/wire-subsystems/src/Wire/UserSearch/Metrics.hs create mode 100644 libs/wire-subsystems/src/Wire/UserSearch/Migration.hs create mode 100644 libs/wire-subsystems/src/Wire/UserSearch/Types.hs create mode 100644 libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/FederationConfigStore.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs create mode 100644 libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs delete mode 100644 services/brig/src/Brig/Index/Migrations.hs delete mode 100644 services/brig/src/Brig/Index/Migrations/Types.hs delete mode 100644 services/brig/src/Brig/Team/Util.hs delete mode 100644 services/brig/src/Brig/User/API/Search.hs delete mode 100644 services/brig/src/Brig/User/Search/Index/Types.hs delete mode 100644 services/brig/src/Brig/User/Search/TeamUserSearch.hs delete mode 100644 services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs diff --git a/changelog.d/5-internal/WPB-888-2 b/changelog.d/5-internal/WPB-888-2 new file mode 100644 index 00000000000..b898071cea8 --- /dev/null +++ b/changelog.d/5-internal/WPB-888-2 @@ -0,0 +1 @@ +Removed `indexReindex` and `indexReindexIfSameOrNewer` from internal Brig/SearchIndex. diff --git a/changelog.d/5-internal/WPB-8888 b/changelog.d/5-internal/WPB-8888 new file mode 100644 index 00000000000..f5d3655308a --- /dev/null +++ b/changelog.d/5-internal/WPB-8888 @@ -0,0 +1 @@ +Introduced ElasticSearch effects related to user search. diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs index e3394fa769c..c54a7b18b46 100644 --- a/integration/test/Test/Teams.hs +++ b/integration/test/Test/Teams.hs @@ -49,31 +49,37 @@ testInvitePersonalUserToTeam = do bindResponse (listInvitations owner tid) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "invitations" `shouldMatch` ([] :: [()]) + ownerId <- owner %. "id" & asString setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" >>= assertSuccess user <- createUser domain def >>= getJSON 201 uid <- user %. "id" >>= asString email <- user %. "email" >>= asString + inv <- postInvitation owner (PostInvitation (Just email) Nothing) >>= getJSON 201 checkListInvitations owner tid email code <- getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString inv %. "url" & asString >>= assertUrlContainsCode code acceptTeamInvitation user code Nothing >>= assertStatus 400 acceptTeamInvitation user code (Just "wrong-password") >>= assertStatus 403 + void $ withWebSockets [user] $ \wss -> do acceptTeamInvitation user code (Just defPassword) >>= assertSuccess for wss $ \ws -> do n <- awaitMatch isUserUpdatedNotif ws n %. "payload.0.user.team" `shouldMatch` tid + bindResponse (getSelf user) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "team" `shouldMatch` tid + -- a team member can now find the former personal user in the team bindResponse (getTeamMembers tm tid) $ \resp -> do resp.status `shouldMatchInt` 200 members <- resp.json %. "members" >>= asList ids <- for members ((%. "user") >=> asString) ids `shouldContain` [uid] + -- the former personal user can now see other team members bindResponse (getTeamMembers user tid) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -82,12 +88,14 @@ testInvitePersonalUserToTeam = do tmId <- tm %. "id" & asString ids `shouldContain` [ownerId] ids `shouldContain` [tmId] + -- the former personal user can now search for the owner bindResponse (searchContacts user (owner %. "name") domain) $ \resp -> do resp.status `shouldMatchInt` 200 documents <- resp.json %. "documents" >>= asList ids <- for documents ((%. "id") >=> asString) ids `shouldContain` [ownerId] + refreshIndex domain -- a team member can now search for the former personal user bindResponse (searchContacts tm (user %. "name") domain) $ \resp -> do diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index 2f67b800eb5..161a81ce30c 100644 --- a/libs/brig-types/brig-types.cabal +++ b/libs/brig-types/brig-types.cabal @@ -17,7 +17,6 @@ library Brig.Types.Instances Brig.Types.Intra Brig.Types.Provider.Tag - Brig.Types.Search Brig.Types.Team Brig.Types.Team.LegalHold Brig.Types.Test.Arbitrary @@ -73,16 +72,12 @@ library -funbox-strict-fields -Wredundant-constraints -Wunused-packages build-depends: - aeson >=2.0.1.0 - , attoparsec >=0.10 - , base >=4 && <5 - , bytestring + base >=4 && <5 , bytestring-conversion >=0.2 , cassandra-util , containers >=0.5 , imports , QuickCheck >=2.9 - , text >=0.11 , types-common >=0.16 , wire-api diff --git a/libs/brig-types/default.nix b/libs/brig-types/default.nix index 50587cd4eeb..290305e7c13 100644 --- a/libs/brig-types/default.nix +++ b/libs/brig-types/default.nix @@ -4,9 +4,7 @@ # dependencies are added or removed. { mkDerivation , aeson -, attoparsec , base -, bytestring , bytestring-conversion , cassandra-util , containers @@ -18,7 +16,6 @@ , tasty , tasty-hunit , tasty-quickcheck -, text , types-common , wire-api }: @@ -27,16 +24,12 @@ mkDerivation { version = "1.35.0"; src = gitignoreSource ./.; libraryHaskellDepends = [ - aeson - attoparsec base - bytestring bytestring-conversion cassandra-util containers imports QuickCheck - text types-common wire-api ]; diff --git a/libs/brig-types/src/Brig/Types/Search.hs b/libs/brig-types/src/Brig/Types/Search.hs deleted file mode 100644 index 2a5006968f6..00000000000 --- a/libs/brig-types/src/Brig/Types/Search.hs +++ /dev/null @@ -1,107 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE StrictData #-} - --- 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 Brig.Types.Search - ( TeamSearchInfo (..), - SearchVisibilityInbound (..), - defaultSearchVisibilityInbound, - searchVisibilityInboundFromFeatureStatus, - ) -where - -import Cassandra qualified as C -import Data.Aeson -import Data.Attoparsec.ByteString -import Data.ByteString.Builder -import Data.ByteString.Conversion -import Data.ByteString.Lazy -import Data.Id (TeamId) -import Data.Text.Encoding -import Imports -import Test.QuickCheck -import Wire.API.Team.Feature - --- | Outbound search restrictions configured by team admin of the searcher. This --- value restricts the set of user that are searched. --- --- See 'optionallySearchWithinTeam' for the effect on full-text search. --- --- See 'mkTeamSearchInfo' for the business logic that defines the TeamSearchInfo --- value. --- --- Search results might be affected by the inbound search restriction settings of --- the searched user. ('SearchVisibilityInbound') -data TeamSearchInfo - = -- | Only users that are not part of any team are searched - NoTeam - | -- | Only users from the same team as the searcher are searched - TeamOnly TeamId - | -- | No search restrictions, all users are searched - AllUsers - --- | Inbound search restrictions configured by team to-be-searched. Affects only --- full-text search (i.e. search on the display name and the handle), not exact --- handle search. -data SearchVisibilityInbound - = -- | The user can only be found by users from the same team - SearchableByOwnTeam - | -- | The user can by found by any user of any team - SearchableByAllTeams - deriving (Eq, Show) - -instance Arbitrary SearchVisibilityInbound where - arbitrary = elements [SearchableByOwnTeam, SearchableByAllTeams] - -instance ToByteString SearchVisibilityInbound where - builder SearchableByOwnTeam = "searchable-by-own-team" - builder SearchableByAllTeams = "searchable-by-all-teams" - -instance FromByteString SearchVisibilityInbound where - parser = - SearchableByOwnTeam - <$ string "searchable-by-own-team" - <|> SearchableByAllTeams - <$ string "searchable-by-all-teams" - -instance C.Cql SearchVisibilityInbound where - ctype = C.Tagged C.IntColumn - - toCql SearchableByOwnTeam = C.CqlInt 0 - toCql SearchableByAllTeams = C.CqlInt 1 - - fromCql (C.CqlInt 0) = pure SearchableByOwnTeam - fromCql (C.CqlInt 1) = pure SearchableByAllTeams - fromCql n = Left $ "Unexpected SearchVisibilityInbound: " ++ show n - -defaultSearchVisibilityInbound :: SearchVisibilityInbound -defaultSearchVisibilityInbound = SearchableByOwnTeam - -searchVisibilityInboundFromFeatureStatus :: FeatureStatus -> SearchVisibilityInbound -searchVisibilityInboundFromFeatureStatus FeatureStatusDisabled = SearchableByOwnTeam -searchVisibilityInboundFromFeatureStatus FeatureStatusEnabled = SearchableByAllTeams - -instance ToJSON SearchVisibilityInbound where - toJSON = String . decodeUtf8 . toStrict . toLazyByteString . builder - -instance FromJSON SearchVisibilityInbound where - parseJSON = withText "SearchVisibilityInbound" $ \str -> - case runParser (parser @SearchVisibilityInbound) (encodeUtf8 str) of - Left err -> fail err - Right result -> pure result diff --git a/libs/brig-types/test/unit/Test/Brig/Types/User.hs b/libs/brig-types/test/unit/Test/Brig/Types/User.hs index 6ca50562cb4..e345eb8e9b0 100644 --- a/libs/brig-types/test/unit/Test/Brig/Types/User.hs +++ b/libs/brig-types/test/unit/Test/Brig/Types/User.hs @@ -27,7 +27,6 @@ module Test.Brig.Types.User where import Brig.Types.Connection (UpdateConnectionsInternal (..)) import Brig.Types.Intra (NewUserScimInvitation (..), UserAccount (..)) -import Brig.Types.Search (SearchVisibilityInbound (..)) import Brig.Types.User (ManagedByUpdate (..), RichInfoUpdate (..)) import Data.Aeson import Imports @@ -50,7 +49,6 @@ roundtripTests = testRoundTripWithSwagger @EJPDRequestBody, testRoundTripWithSwagger @EJPDResponseBody, testRoundTrip @UpdateConnectionsInternal, - testRoundTrip @SearchVisibilityInbound, testRoundTripWithSwagger @UserAccount, testGroup "golden tests" $ [testCaseUserAccount] diff --git a/libs/cassandra-util/src/Cassandra/Exec.hs b/libs/cassandra-util/src/Cassandra/Exec.hs index c7d4c352a99..8ef7d64337c 100644 --- a/libs/cassandra-util/src/Cassandra/Exec.hs +++ b/libs/cassandra-util/src/Cassandra/Exec.hs @@ -27,6 +27,7 @@ module Cassandra.Exec paginateC, PageWithState (..), paginateWithState, + paginateWithStateC, paramsPagingState, pwsHasMore, module C, @@ -115,6 +116,29 @@ paginateWithState q p = do pure $ PageWithState b (pagingState m) _ -> throwM $ UnexpectedResponse (hrHost r) (hrResponse r) +-- | Like 'paginateWithState' but returns a conduit instead of one page. +-- +-- This can be used with 'paginateWithState' like this: +-- @ +-- main :: IO () +-- main = do +-- runConduit $ +-- paginateWithStateC getUsers +-- .| mapC doSomethingWithAPageOfUsers +-- where +-- getUsers state = paginateWithState getUsersQuery (paramsPagingState Quorum () 10000 state) +-- @ +paginateWithStateC :: forall m a. (Monad m) => (Maybe Protocol.PagingState -> m (PageWithState a)) -> ConduitT () [a] m () +paginateWithStateC getPage = do + go =<< lift (getPage Nothing) + where + go :: PageWithState a -> ConduitT () [a] m () + go page = do + unless (null page.pwsResults) $ + yield (page.pwsResults) + when (pwsHasMore page) $ + go =<< lift (getPage page.pwsState) + paramsPagingState :: Consistency -> a -> Int32 -> Maybe Protocol.PagingState -> QueryParams a paramsPagingState c p n state = QueryParams c False p (Just n) state Nothing Nothing {-# INLINE paramsPagingState #-} diff --git a/libs/cassandra-util/src/Cassandra/Util.hs b/libs/cassandra-util/src/Cassandra/Util.hs index f8b793f77db..4331da819c5 100644 --- a/libs/cassandra-util/src/Cassandra/Util.hs +++ b/libs/cassandra-util/src/Cassandra/Util.hs @@ -109,6 +109,7 @@ initCassandra settings Nothing logger = do -- | Read cassandra's writetimes https://docs.datastax.com/en/dse/5.1/cql/cql/cql_using/useWritetime.html -- as UTCTime values without any loss of precision newtype Writetime a = Writetime {writetimeToUTC :: UTCTime} + deriving (Functor) instance Cql (Writetime a) where ctype = Tagged BigIntColumn diff --git a/libs/polysemy-wire-zoo/default.nix b/libs/polysemy-wire-zoo/default.nix index ebf7e8de8c5..21bf9204774 100644 --- a/libs/polysemy-wire-zoo/default.nix +++ b/libs/polysemy-wire-zoo/default.nix @@ -19,6 +19,7 @@ , polysemy , polysemy-check , polysemy-plugin +, prometheus-client , QuickCheck , saml2-web-sso , time @@ -45,6 +46,7 @@ mkDerivation { polysemy polysemy-check polysemy-plugin + prometheus-client QuickCheck saml2-web-sso time diff --git a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal index 505874d7b6c..5d8bb12ab31 100644 --- a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal +++ b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal @@ -11,6 +11,7 @@ license: AGPL-3 build-type: Simple library + -- cabal-fmt: expand src exposed-modules: Polysemy.Testing Polysemy.TinyLog @@ -23,6 +24,8 @@ library Wire.Sem.Logger Wire.Sem.Logger.Level Wire.Sem.Logger.TinyLog + Wire.Sem.Metrics + Wire.Sem.Metrics.IO Wire.Sem.Now Wire.Sem.Now.Input Wire.Sem.Now.IO @@ -83,7 +86,7 @@ library build-depends: aeson - , base >=4.6 && <5.0 + , base >=4.6 && <5.0 , bytestring , cassandra-util , crypton @@ -94,6 +97,7 @@ library , polysemy , polysemy-check , polysemy-plugin + , prometheus-client , QuickCheck , saml2-web-sso , time diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics.hs new file mode 100644 index 00000000000..63cba3bce8a --- /dev/null +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.Sem.Metrics where + +import Imports +import Polysemy +import Prometheus (Counter, Gauge) + +-- | NOTE: Vectors would require non trival changes because +-- 'Prometheus.withLabel' take a paramter of type 'metric -> IO ()'. +data Metrics m a where + AddCounter :: Counter -> Double -> Metrics m () + AddGauge :: Gauge -> Double -> Metrics m () + +makeSem ''Metrics + +incCounter :: (Member Metrics r) => Counter -> Sem r () +incCounter c = addCounter c 1 + +incGauge :: (Member Metrics r) => Gauge -> Sem r () +incGauge c = addGauge c 1 diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics/IO.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics/IO.hs new file mode 100644 index 00000000000..1b357927c87 --- /dev/null +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Metrics/IO.hs @@ -0,0 +1,16 @@ +module Wire.Sem.Metrics.IO where + +import Imports +import Polysemy +import qualified Prometheus as Prom +import Wire.Sem.Metrics + +runMetricsToIO :: (Member (Embed IO) r) => InterpreterFor Metrics r +runMetricsToIO = interpret $ \case + AddCounter c n -> embed . void $ Prom.addCounter @IO c n + AddGauge g n -> embed $ Prom.addGauge @IO g n + +ignoreMetrics :: InterpreterFor Metrics r +ignoreMetrics = interpret $ \case + AddCounter _ _ -> pure () + AddGauge _ _ -> pure () diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 16efe68b803..416e5fecaa2 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -40,6 +40,7 @@ data BrigError | NotConnected | InvalidTransition | NoIdentity + | NoUser | HandleExists | InvalidHandle | HandleNotFound diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs index 0b90fd43524..9e0fdabd7dc 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/SearchIndex.hs @@ -30,21 +30,3 @@ type ISearchIndexAPI = :> "refresh" :> Post '[JSON] NoContent ) - :<|> Named - "indexReindex" - ( Summary - "reindex from Cassandra (NB: e.g. integration testing prefer the `brig-index` \ - \executable for actual operations!)" - :> "index" - :> "reindex" - :> Post '[JSON] NoContent - ) - :<|> Named - "indexReindexIfSameOrNewer" - ( Summary - "forcefully reindex from Cassandra, even if nothing has changed (NB: e.g. \ - \integration testing prefer the `brig-index` executable for actual operations!)" - :> "index" - :> "reindex-if-same-or-newer" - :> Post '[JSON] NoContent - ) 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 aee06b492ad..7394673620e 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1178,7 +1178,7 @@ type ConnectionAPI = ( Summary "Search for users" :> MakesFederatedCall 'Brig "get-users-by-ids" :> MakesFederatedCall 'Brig "search-users" - :> ZUser + :> ZLocalUser :> "search" :> "contacts" :> QueryParam' '[Required, Strict, Description "Search query"] "q" Text @@ -1380,7 +1380,7 @@ type SearchAPI = Description "Number of results to return (min: 1, max: 500, default: 15)" ] "size" - (Range 1 500 Int32) + (Range 1 500 Int) :> QueryParam' [ Optional, Strict, diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 4aba62549c9..533bbd8837f 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -182,7 +182,8 @@ class ( Default cfg, ToSchema cfg, Default (LockableFeature cfg), - KnownSymbol (FeatureSymbol cfg) + KnownSymbol (FeatureSymbol cfg), + NpProject cfg Features ) => IsFeatureConfig cfg where diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index d94cbfecc32..98720fab69b 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -585,10 +585,10 @@ class IsPerm perm where instance IsPerm Perm where type PermError p = 'MissingPermission ('Just p) - roleHasPerm r p = p `Set.member` (rolePermissions r ^. self) - roleGrantsPerm r p = p `Set.member` (rolePermissions r ^. copy) - hasPermission tm p = p `Set.member` (tm ^. permissions . self) - mayGrantPermission tm p = p `Set.member` (tm ^. permissions . copy) + roleHasPerm r p = p `Set.member` ((rolePermissions r).self) + roleGrantsPerm r p = p `Set.member` ((rolePermissions r).copy) + hasPermission tm p = p `Set.member` ((tm ^. permissions).self) + mayGrantPermission tm p = p `Set.member` ((tm ^. permissions).copy) instance IsPerm HiddenPerm where type PermError p = OperationDenied diff --git a/libs/wire-api/src/Wire/API/Team/Permission.hs b/libs/wire-api/src/Wire/API/Team/Permission.hs index b4ac0d90455..26ddd8865ba 100644 --- a/libs/wire-api/src/Wire/API/Team/Permission.hs +++ b/libs/wire-api/src/Wire/API/Team/Permission.hs @@ -26,8 +26,6 @@ module Wire.API.Team.Permission ( -- * Permissions Permissions (..), - self, - copy, newPermissions, fullPermissions, noPermissions, @@ -45,7 +43,7 @@ where import Cassandra qualified as Cql import Control.Error.Util qualified as Err -import Control.Lens (makeLenses, (?~), (^.)) +import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Bits (testBit, (.|.)) import Data.OpenApi qualified as S @@ -61,8 +59,8 @@ import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -- Permissions data Permissions = Permissions - { _self :: Set Perm, - _copy :: Set Perm + { self :: Set Perm, + copy :: Set Perm } deriving stock (Eq, Ord, Show, Generic) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Permissions) @@ -71,8 +69,8 @@ permissionsSchema :: ValueSchema NamedSwaggerDoc Permissions permissionsSchema = objectWithDocModifier "Permissions" (description ?~ docs) $ Permissions - <$> (permsToInt . _self) .= field "self" (intToPerms <$> schema) - <*> (permsToInt . _copy) .= field "copy" (intToPerms <$> schema) + <$> (permsToInt . self) .= field "self" (intToPerms <$> schema) + <*> (permsToInt . copy) .= field "copy" (intToPerms <$> schema) where docs = "This is just a complicated way of representing a team role. self and copy \ @@ -198,14 +196,12 @@ intToPerm 0x0800 = Just DeleteTeam intToPerm 0x1000 = Just SetMemberPermissions intToPerm _ = Nothing -makeLenses ''Permissions - instance Cql.Cql Permissions where ctype = Cql.Tagged $ Cql.UdtColumn "permissions" [("self", Cql.BigIntColumn), ("copy", Cql.BigIntColumn)] toCql p = let f = Cql.CqlBigInt . fromIntegral . permsToInt - in Cql.CqlUdt [("self", f (p ^. self)), ("copy", f (p ^. copy))] + in Cql.CqlUdt [("self", f p.self), ("copy", f p.copy)] fromCql (Cql.CqlUdt p) = do let f = intToPerms . fromIntegral :: Int64 -> Set.Set Perm diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs index c80d19bea0e..7a9dbb790c1 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Event_team.hs @@ -230,7 +230,7 @@ testObject_Event_team_18 = (Id (fromJust (UUID.fromString "00007783-0000-7d60-0000-00d30000396e"))) ( Just ( Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -246,7 +246,7 @@ testObject_Event_team_18 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, DeleteConversation, @@ -273,7 +273,7 @@ testObject_Event_team_19 = (Id (fromJust (UUID.fromString "0000382c-0000-1ce7-0000-568b00001fe9"))) ( Just ( Permissions - { _self = + { self = fromList [ DeleteConversation, RemoveTeamMember, @@ -284,7 +284,7 @@ testObject_Event_team_19 = GetMemberPermissions, GetTeamConversations ], - _copy = + copy = fromList [ DeleteConversation, RemoveTeamMember, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeamMember_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeamMember_team.hs index 34294b4bd00..0c5db95a9d3 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeamMember_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeamMember_team.hs @@ -41,14 +41,14 @@ import Wire.API.Team.Permission SetMemberPermissions, SetTeamData ), - Permissions (Permissions, _copy, _self), + Permissions (Permissions, copy, self), ) testObject_NewTeamMember_team_1 :: NewTeamMember testObject_NewTeamMember_team_1 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0007-0000-000200000002"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000004")), fromJust (readUTCTimeMillis "1864-05-04T12:59:54.182Z") @@ -60,7 +60,7 @@ testObject_NewTeamMember_team_2 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000004-0000-0000-0000-000200000003"))) ( Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -69,7 +69,7 @@ testObject_NewTeamMember_team_2 = AddRemoveConvMember, ModifyConvName ], - _copy = fromList [DeleteConversation, AddRemoveConvMember] + copy = fromList [DeleteConversation, AddRemoveConvMember] } ) ( Just @@ -83,10 +83,10 @@ testObject_NewTeamMember_team_3 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0008-0000-000700000005"))) ( Permissions - { _self = + { self = fromList [CreateConversation, DeleteConversation, RemoveTeamMember, GetBilling, DeleteTeam], - _copy = fromList [CreateConversation, DeleteConversation, GetBilling] + copy = fromList [CreateConversation, DeleteConversation, GetBilling] } ) ( Just @@ -100,8 +100,8 @@ testObject_NewTeamMember_team_4 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000700000005"))) ( Permissions - { _self = fromList [CreateConversation, AddTeamMember, SetTeamData], - _copy = fromList [CreateConversation, SetTeamData] + { self = fromList [CreateConversation, AddTeamMember, SetTeamData], + copy = fromList [CreateConversation, SetTeamData] } ) Nothing @@ -110,7 +110,7 @@ testObject_NewTeamMember_team_5 :: NewTeamMember testObject_NewTeamMember_team_5 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000002"))) - (Permissions {_self = fromList [AddTeamMember, SetBilling, GetTeamConversations], _copy = fromList [AddTeamMember]}) + (Permissions {self = fromList [AddTeamMember, SetBilling, GetTeamConversations], copy = fromList [AddTeamMember]}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000600000006")), fromJust (readUTCTimeMillis "1864-05-12T23:29:05.832Z") @@ -122,10 +122,10 @@ testObject_NewTeamMember_team_6 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0006-0000-000400000003"))) ( Permissions - { _self = + { self = fromList [CreateConversation, DeleteConversation, GetBilling, SetTeamData, SetMemberPermissions], - _copy = fromList [CreateConversation, GetBilling] + copy = fromList [CreateConversation, GetBilling] } ) ( Just @@ -139,10 +139,10 @@ testObject_NewTeamMember_team_7 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000007-0000-0004-0000-000500000005"))) ( Permissions - { _self = + { self = fromList [AddTeamMember, RemoveTeamMember, ModifyConvName, GetTeamConversations, DeleteTeam], - _copy = fromList [AddTeamMember] + copy = fromList [AddTeamMember] } ) ( Just @@ -156,8 +156,8 @@ testObject_NewTeamMember_team_8 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0003-0000-000200000003"))) ( Permissions - { _self = fromList [ModifyConvName], - _copy = fromList [ModifyConvName] + { self = fromList [ModifyConvName], + copy = fromList [ModifyConvName] } ) ( Just @@ -170,7 +170,7 @@ testObject_NewTeamMember_team_9 :: NewTeamMember testObject_NewTeamMember_team_9 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0008-0000-000300000004"))) - (Permissions {_self = fromList [SetBilling], _copy = fromList []}) + (Permissions {self = fromList [SetBilling], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000700000000")), fromJust (readUTCTimeMillis "1864-05-08T10:27:23.240Z") @@ -181,7 +181,7 @@ testObject_NewTeamMember_team_10 :: NewTeamMember testObject_NewTeamMember_team_10 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0003-0000-000600000003"))) - (Permissions {_self = fromList [GetBilling], _copy = fromList []}) + (Permissions {self = fromList [GetBilling], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000004-0000-0006-0000-000600000008")), fromJust (readUTCTimeMillis "1864-05-15T10:49:54.418Z") @@ -193,8 +193,8 @@ testObject_NewTeamMember_team_11 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000006-0000-0005-0000-000000000002"))) ( Permissions - { _self = fromList [CreateConversation, ModifyConvName, SetTeamData], - _copy = fromList [] + { self = fromList [CreateConversation, ModifyConvName, SetTeamData], + copy = fromList [] } ) ( Just @@ -207,7 +207,7 @@ testObject_NewTeamMember_team_12 :: NewTeamMember testObject_NewTeamMember_team_12 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0004-0000-000000000007"))) - (Permissions {_self = fromList [SetBilling, SetTeamData, GetTeamConversations], _copy = fromList []}) + (Permissions {self = fromList [SetBilling, SetTeamData, GetTeamConversations], copy = fromList []}) Nothing testObject_NewTeamMember_team_13 :: NewTeamMember @@ -215,8 +215,8 @@ testObject_NewTeamMember_team_13 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0004-0000-000600000001"))) ( Permissions - { _self = fromList [AddTeamMember, AddRemoveConvMember, SetTeamData, GetTeamConversations], - _copy = fromList [AddTeamMember, AddRemoveConvMember, GetTeamConversations] + { self = fromList [AddTeamMember, AddRemoveConvMember, SetTeamData, GetTeamConversations], + copy = fromList [AddTeamMember, AddRemoveConvMember, GetTeamConversations] } ) Nothing @@ -226,10 +226,10 @@ testObject_NewTeamMember_team_14 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000500000004"))) ( Permissions - { _self = + { self = fromList [CreateConversation, DeleteConversation, ModifyConvName, GetBilling], - _copy = fromList [] + copy = fromList [] } ) ( Just @@ -243,8 +243,8 @@ testObject_NewTeamMember_team_15 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0008-0000-000800000007"))) ( Permissions - { _self = fromList [RemoveTeamMember, GetMemberPermissions, DeleteTeam], - _copy = fromList [RemoveTeamMember, GetMemberPermissions] + { self = fromList [RemoveTeamMember, GetMemberPermissions, DeleteTeam], + copy = fromList [RemoveTeamMember, GetMemberPermissions] } ) ( Just @@ -258,8 +258,8 @@ testObject_NewTeamMember_team_16 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0006-0000-000300000005"))) ( Permissions - { _self = fromList [CreateConversation, RemoveTeamMember, GetBilling, GetTeamConversations, DeleteTeam], - _copy = fromList [] + { self = fromList [CreateConversation, RemoveTeamMember, GetBilling, GetTeamConversations, DeleteTeam], + copy = fromList [] } ) Nothing @@ -268,7 +268,7 @@ testObject_NewTeamMember_team_17 :: NewTeamMember testObject_NewTeamMember_team_17 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0008-0000-000400000005"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000004-0000-0008-0000-000800000007")), fromJust (readUTCTimeMillis "1864-05-07T21:53:30.897Z") @@ -279,7 +279,7 @@ testObject_NewTeamMember_team_18 :: NewTeamMember testObject_NewTeamMember_team_18 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000006-0000-0003-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000500000002")), fromJust (readUTCTimeMillis "1864-05-11T12:32:01.417Z") @@ -291,8 +291,8 @@ testObject_NewTeamMember_team_19 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000004-0000-0005-0000-000100000008"))) ( Permissions - { _self = fromList [DeleteConversation, RemoveTeamMember, SetBilling, SetMemberPermissions], - _copy = fromList [DeleteConversation, SetBilling] + { self = fromList [DeleteConversation, RemoveTeamMember, SetBilling, SetMemberPermissions], + copy = fromList [DeleteConversation, SetBilling] } ) Nothing @@ -302,7 +302,7 @@ testObject_NewTeamMember_team_20 = mkNewTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0000-0000-000000000004"))) ( Permissions - { _self = + { self = fromList [ AddTeamMember, AddRemoveConvMember, @@ -311,7 +311,7 @@ testObject_NewTeamMember_team_20 = GetMemberPermissions, GetTeamConversations ], - _copy = fromList [] + copy = fromList [] } ) ( Just diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Permissions_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Permissions_team.hs index fd47570ce6c..2403aff6562 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Permissions_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Permissions_team.hs @@ -40,12 +40,12 @@ import Wire.API.Team.Permission ) testObject_Permissions_team_1 :: Permissions -testObject_Permissions_team_1 = Permissions {_self = fromList [SetBilling], _copy = fromList [SetBilling]} +testObject_Permissions_team_1 = Permissions {self = fromList [SetBilling], copy = fromList [SetBilling]} testObject_Permissions_team_2 :: Permissions testObject_Permissions_team_2 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddTeamMember, @@ -58,7 +58,7 @@ testObject_Permissions_team_2 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ DeleteConversation, AddTeamMember, @@ -75,7 +75,7 @@ testObject_Permissions_team_2 = testObject_Permissions_team_3 :: Permissions testObject_Permissions_team_3 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddTeamMember, @@ -87,7 +87,7 @@ testObject_Permissions_team_3 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ AddTeamMember, RemoveTeamMember, @@ -102,7 +102,7 @@ testObject_Permissions_team_3 = testObject_Permissions_team_4 :: Permissions testObject_Permissions_team_4 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddTeamMember, @@ -113,13 +113,13 @@ testObject_Permissions_team_4 = SetMemberPermissions, DeleteTeam ], - _copy = fromList [GetBilling] + copy = fromList [GetBilling] } testObject_Permissions_team_5 :: Permissions testObject_Permissions_team_5 = Permissions - { _self = + { self = fromList [ CreateConversation, AddTeamMember, @@ -131,7 +131,7 @@ testObject_Permissions_team_5 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, RemoveTeamMember, @@ -145,7 +145,7 @@ testObject_Permissions_team_5 = testObject_Permissions_team_6 :: Permissions testObject_Permissions_team_6 = Permissions - { _self = + { self = fromList [ CreateConversation, AddTeamMember, @@ -158,7 +158,7 @@ testObject_Permissions_team_6 = GetMemberPermissions, GetTeamConversations ], - _copy = + copy = fromList [ CreateConversation, AddTeamMember, @@ -175,7 +175,7 @@ testObject_Permissions_team_6 = testObject_Permissions_team_7 :: Permissions testObject_Permissions_team_7 = Permissions - { _self = + { self = fromList [ AddTeamMember, RemoveTeamMember, @@ -186,13 +186,13 @@ testObject_Permissions_team_7 = GetTeamConversations, DeleteTeam ], - _copy = fromList [AddRemoveConvMember, GetBilling, DeleteTeam] + copy = fromList [AddRemoveConvMember, GetBilling, DeleteTeam] } testObject_Permissions_team_8 :: Permissions testObject_Permissions_team_8 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -207,7 +207,7 @@ testObject_Permissions_team_8 = SetMemberPermissions, GetTeamConversations ], - _copy = + copy = fromList [ AddTeamMember, RemoveTeamMember, @@ -222,20 +222,20 @@ testObject_Permissions_team_8 = testObject_Permissions_team_9 :: Permissions testObject_Permissions_team_9 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, AddRemoveConvMember, GetMemberPermissions ], - _copy = fromList [CreateConversation, AddRemoveConvMember, GetMemberPermissions] + copy = fromList [CreateConversation, AddRemoveConvMember, GetMemberPermissions] } testObject_Permissions_team_10 :: Permissions testObject_Permissions_team_10 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -247,7 +247,7 @@ testObject_Permissions_team_10 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, DeleteConversation, @@ -264,7 +264,7 @@ testObject_Permissions_team_10 = testObject_Permissions_team_11 :: Permissions testObject_Permissions_team_11 = Permissions - { _self = + { self = fromList [ DeleteConversation, RemoveTeamMember, @@ -274,13 +274,13 @@ testObject_Permissions_team_11 = GetTeamConversations, DeleteTeam ], - _copy = fromList [RemoveTeamMember, GetMemberPermissions, GetTeamConversations] + copy = fromList [RemoveTeamMember, GetMemberPermissions, GetTeamConversations] } testObject_Permissions_team_12 :: Permissions testObject_Permissions_team_12 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -295,7 +295,7 @@ testObject_Permissions_team_12 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, DeleteConversation, @@ -314,7 +314,7 @@ testObject_Permissions_team_12 = testObject_Permissions_team_13 :: Permissions testObject_Permissions_team_13 = Permissions - { _self = + { self = fromList [ CreateConversation, AddTeamMember, @@ -324,13 +324,13 @@ testObject_Permissions_team_13 = SetTeamData, SetMemberPermissions ], - _copy = fromList [SetTeamData, SetMemberPermissions] + copy = fromList [SetTeamData, SetMemberPermissions] } testObject_Permissions_team_14 :: Permissions testObject_Permissions_team_14 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -342,7 +342,7 @@ testObject_Permissions_team_14 = GetMemberPermissions, SetMemberPermissions ], - _copy = + copy = fromList [ CreateConversation, DeleteConversation, @@ -359,7 +359,7 @@ testObject_Permissions_team_14 = testObject_Permissions_team_15 :: Permissions testObject_Permissions_team_15 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddTeamMember, @@ -371,13 +371,13 @@ testObject_Permissions_team_15 = SetMemberPermissions, DeleteTeam ], - _copy = fromList [] + copy = fromList [] } testObject_Permissions_team_16 :: Permissions testObject_Permissions_team_16 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddRemoveConvMember, @@ -386,7 +386,7 @@ testObject_Permissions_team_16 = SetMemberPermissions, GetTeamConversations ], - _copy = + copy = fromList [DeleteConversation, GetBilling, SetTeamData, SetMemberPermissions, GetTeamConversations] } @@ -394,7 +394,7 @@ testObject_Permissions_team_16 = testObject_Permissions_team_17 :: Permissions testObject_Permissions_team_17 = Permissions - { _self = + { self = fromList [ DeleteConversation, AddTeamMember, @@ -406,7 +406,7 @@ testObject_Permissions_team_17 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ DeleteConversation, AddTeamMember, @@ -423,7 +423,7 @@ testObject_Permissions_team_17 = testObject_Permissions_team_18 :: Permissions testObject_Permissions_team_18 = Permissions - { _self = + { self = fromList [ CreateConversation, AddTeamMember, @@ -433,7 +433,7 @@ testObject_Permissions_team_18 = SetMemberPermissions, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, AddTeamMember, @@ -447,7 +447,7 @@ testObject_Permissions_team_18 = testObject_Permissions_team_19 :: Permissions testObject_Permissions_team_19 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -462,7 +462,7 @@ testObject_Permissions_team_19 = GetTeamConversations, DeleteTeam ], - _copy = + copy = fromList [ CreateConversation, DeleteConversation, @@ -479,7 +479,7 @@ testObject_Permissions_team_19 = testObject_Permissions_team_20 :: Permissions testObject_Permissions_team_20 = Permissions - { _self = + { self = fromList [ CreateConversation, DeleteConversation, @@ -491,7 +491,7 @@ testObject_Permissions_team_20 = SetMemberPermissions, DeleteTeam ], - _copy = + copy = fromList [ DeleteConversation, AddTeamMember, diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMemberList_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMemberList_team.hs index b540677bd5e..55838c4d159 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMemberList_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMemberList_team.hs @@ -47,7 +47,7 @@ import Wire.API.Team.Permission SetMemberPermissions, SetTeamData ), - Permissions (Permissions, _copy, _self), + Permissions (Permissions, copy, self), ) testObject_TeamMemberList_team_1 :: TeamMemberList @@ -58,7 +58,7 @@ testObject_TeamMemberList_team_2 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000000000002"))) - (Permissions {_self = fromList [GetBilling, SetMemberPermissions], _copy = fromList []}) + (Permissions {self = fromList [GetBilling, SetMemberPermissions], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000002")), fromJust (readUTCTimeMillis "1864-05-10T10:05:44.332Z") @@ -73,7 +73,7 @@ testObject_TeamMemberList_team_3 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T06:07:36.175Z") @@ -82,7 +82,7 @@ testObject_TeamMemberList_team_3 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T14:28:10.448Z") @@ -91,7 +91,7 @@ testObject_TeamMemberList_team_3 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T16:05:37.642Z") @@ -100,12 +100,12 @@ testObject_TeamMemberList_team_3 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T13:06:20.504Z") @@ -114,7 +114,7 @@ testObject_TeamMemberList_team_3 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T16:37:10.774Z") @@ -123,7 +123,7 @@ testObject_TeamMemberList_team_3 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T04:36:55.388Z") @@ -138,7 +138,7 @@ testObject_TeamMemberList_team_4 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [GetTeamConversations], _copy = fromList []}) + (Permissions {self = fromList [GetTeamConversations], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-08T16:05:11.696Z") @@ -147,7 +147,7 @@ testObject_TeamMemberList_team_4 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-08T07:09:26.753Z") @@ -162,7 +162,7 @@ testObject_TeamMemberList_team_5 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T23:10:04.963Z") @@ -171,7 +171,7 @@ testObject_TeamMemberList_team_5 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T15:40:17.119Z") @@ -180,7 +180,7 @@ testObject_TeamMemberList_team_5 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T00:40:38.004Z") @@ -189,7 +189,7 @@ testObject_TeamMemberList_team_5 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T07:30:49.028Z") @@ -204,7 +204,7 @@ testObject_TeamMemberList_team_6 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T17:07:48.156Z") @@ -213,7 +213,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T00:04:10.559Z") @@ -222,7 +222,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T10:39:19.860Z") @@ -231,7 +231,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T13:40:56.648Z") @@ -240,7 +240,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T12:13:40.273Z") @@ -249,7 +249,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T13:28:04.561Z") @@ -258,7 +258,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T02:59:55.584Z") @@ -267,7 +267,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T22:57:33.947Z") @@ -276,7 +276,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T01:02:39.691Z") @@ -285,7 +285,7 @@ testObject_TeamMemberList_team_6 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T13:39:38.488Z") @@ -300,12 +300,12 @@ testObject_TeamMemberList_team_7 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [SetTeamData], _copy = fromList []}) + (Permissions {self = fromList [SetTeamData], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-10T03:11:36.961Z") @@ -314,7 +314,7 @@ testObject_TeamMemberList_team_7 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled ] @@ -325,7 +325,7 @@ testObject_TeamMemberList_team_8 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T07:35:03.629Z") @@ -334,7 +334,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T00:48:38.818Z") @@ -343,7 +343,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T06:12:10.151Z") @@ -352,7 +352,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T03:45:53.520Z") @@ -361,7 +361,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T17:14:59.798Z") @@ -370,7 +370,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T17:51:55.340Z") @@ -379,7 +379,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T01:38:35.880Z") @@ -388,7 +388,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T18:06:10.660Z") @@ -397,7 +397,7 @@ testObject_TeamMemberList_team_8 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T07:30:46.880Z") @@ -406,12 +406,12 @@ testObject_TeamMemberList_team_8 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending ] @@ -422,7 +422,7 @@ testObject_TeamMemberList_team_9 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [AddTeamMember], _copy = fromList []}) + (Permissions {self = fromList [AddTeamMember], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-08T22:16:59.050Z") @@ -431,7 +431,7 @@ testObject_TeamMemberList_team_9 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [CreateConversation], _copy = fromList []}) + (Permissions {self = fromList [CreateConversation], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-08T21:43:37.550Z") @@ -446,7 +446,7 @@ testObject_TeamMemberList_team_10 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T04:44:28.366Z") @@ -455,7 +455,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T06:22:04.036Z") @@ -464,7 +464,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T12:10:11.701Z") @@ -473,7 +473,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T21:54:05.305Z") @@ -482,7 +482,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T00:26:06.221Z") @@ -491,12 +491,12 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T20:12:04.856Z") @@ -505,7 +505,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T23:35:44.986Z") @@ -514,7 +514,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T07:36:17.730Z") @@ -523,7 +523,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T19:36:57.529Z") @@ -532,12 +532,12 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T19:45:56.914Z") @@ -546,7 +546,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T13:42:17.107Z") @@ -555,7 +555,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T03:42:46.106Z") @@ -564,7 +564,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T09:41:44.679Z") @@ -573,7 +573,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T09:26:44.717Z") @@ -582,7 +582,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T00:40:00.056Z") @@ -591,12 +591,12 @@ testObject_TeamMemberList_team_10 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T07:47:20.635Z") @@ -605,7 +605,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T15:58:21.895Z") @@ -614,7 +614,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T19:25:51.873Z") @@ -623,7 +623,7 @@ testObject_TeamMemberList_team_10 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T03:19:55.569Z") @@ -638,7 +638,7 @@ testObject_TeamMemberList_team_11 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T06:08:50.626Z") @@ -647,12 +647,12 @@ testObject_TeamMemberList_team_11 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T08:23:53.653Z") @@ -661,12 +661,12 @@ testObject_TeamMemberList_team_11 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T16:28:42.815Z") @@ -675,17 +675,17 @@ testObject_TeamMemberList_team_11 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T11:47:57.498Z") @@ -694,7 +694,7 @@ testObject_TeamMemberList_team_11 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T17:22:07.538Z") @@ -703,7 +703,7 @@ testObject_TeamMemberList_team_11 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T19:14:48.836Z") @@ -712,7 +712,7 @@ testObject_TeamMemberList_team_11 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T14:53:49.059Z") @@ -721,7 +721,7 @@ testObject_TeamMemberList_team_11 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T10:44:04.209Z") @@ -730,7 +730,7 @@ testObject_TeamMemberList_team_11 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T23:34:24.831Z") @@ -745,12 +745,12 @@ testObject_TeamMemberList_team_12 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T15:59:09.462Z") @@ -759,12 +759,12 @@ testObject_TeamMemberList_team_12 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T00:27:17.631Z") @@ -779,12 +779,12 @@ testObject_TeamMemberList_team_13 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [GetMemberPermissions], _copy = fromList []}) + (Permissions {self = fromList [GetMemberPermissions], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-10T04:37:19.686Z") @@ -793,7 +793,7 @@ testObject_TeamMemberList_team_13 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T13:22:20.368Z") @@ -808,12 +808,12 @@ testObject_TeamMemberList_team_14 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T07:01:56.077Z") @@ -822,7 +822,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T09:34:46.900Z") @@ -831,7 +831,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T10:40:24.034Z") @@ -840,7 +840,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T10:17:53.056Z") @@ -849,7 +849,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T18:37:38.894Z") @@ -858,12 +858,12 @@ testObject_TeamMemberList_team_14 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T06:25:10.534Z") @@ -872,7 +872,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T02:42:16.433Z") @@ -881,7 +881,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T07:25:18.248Z") @@ -890,12 +890,12 @@ testObject_TeamMemberList_team_14 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T15:31:36.237Z") @@ -904,7 +904,7 @@ testObject_TeamMemberList_team_14 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T15:23:38.616Z") @@ -913,12 +913,12 @@ testObject_TeamMemberList_team_14 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled ] @@ -929,7 +929,7 @@ testObject_TeamMemberList_team_15 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T20:33:17.912Z") @@ -938,7 +938,7 @@ testObject_TeamMemberList_team_15 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")), fromJust (readUTCTimeMillis "1864-05-09T09:03:59.579Z") @@ -947,17 +947,17 @@ testObject_TeamMemberList_team_15 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled ] @@ -971,7 +971,7 @@ testObject_TeamMemberList_team_17 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T10:04:36.715Z") @@ -980,12 +980,12 @@ testObject_TeamMemberList_team_17 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T03:02:37.641Z") @@ -994,7 +994,7 @@ testObject_TeamMemberList_team_17 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T23:21:44.944Z") @@ -1003,7 +1003,7 @@ testObject_TeamMemberList_team_17 = UserLegalHoldDisabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T08:47:48.774Z") @@ -1018,7 +1018,7 @@ testObject_TeamMemberList_team_18 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T17:44:12.611Z") @@ -1027,7 +1027,7 @@ testObject_TeamMemberList_team_18 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T05:14:06.040Z") @@ -1036,7 +1036,7 @@ testObject_TeamMemberList_team_18 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")), fromJust (readUTCTimeMillis "1864-05-09T05:24:40.864Z") @@ -1045,7 +1045,7 @@ testObject_TeamMemberList_team_18 = UserLegalHoldPending, mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-09T20:09:48.156Z") @@ -1054,7 +1054,7 @@ testObject_TeamMemberList_team_18 = UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")), fromJust (readUTCTimeMillis "1864-05-09T20:09:31.059Z") @@ -1070,8 +1070,8 @@ testObject_TeamMemberList_team_19 = [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000200000000"))) ( Permissions - { _self = fromList [CreateConversation, SetTeamData, SetMemberPermissions], - _copy = fromList [] + { self = fromList [CreateConversation, SetTeamData, SetMemberPermissions], + copy = fromList [] } ) ( Just @@ -1088,12 +1088,12 @@ testObject_TeamMemberList_team_20 = newTeamMemberList [ mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) Nothing UserLegalHoldEnabled, mkTeamMember (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000100000000"))) - (Permissions {_self = fromList [], _copy = fromList []}) + (Permissions {self = fromList [], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")), fromJust (readUTCTimeMillis "1864-05-08T15:41:51.601Z") diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMember_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMember_team.hs index 358b5cf8810..b810a1cc093 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMember_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/TeamMember_team.hs @@ -48,7 +48,7 @@ import Wire.API.Team.Permission SetMemberPermissions, SetTeamData ), - Permissions (Permissions, _copy, _self), + Permissions (Permissions, copy, self), ) testObject_TeamMember_team_1 :: TeamMember @@ -56,8 +56,8 @@ testObject_TeamMember_team_1 = mkTeamMember (Id (fromJust (UUID.fromString "00000007-0000-0005-0000-000500000002"))) ( Permissions - { _self = fromList [GetBilling, GetMemberPermissions, SetMemberPermissions, DeleteTeam], - _copy = fromList [GetBilling] + { self = fromList [GetBilling, GetMemberPermissions, SetMemberPermissions, DeleteTeam], + copy = fromList [GetBilling] } ) ( Just @@ -71,7 +71,7 @@ testObject_TeamMember_team_2 :: TeamMember testObject_TeamMember_team_2 = mkTeamMember (Id (fromJust (UUID.fromString "00000003-0000-0000-0000-000500000005"))) - (Permissions {_self = fromList [ModifyConvName, SetMemberPermissions], _copy = fromList []}) + (Permissions {self = fromList [ModifyConvName, SetMemberPermissions], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000004")), fromJust (readUTCTimeMillis "1864-05-03T14:56:52.508Z") @@ -84,10 +84,10 @@ testObject_TeamMember_team_3 = mkTeamMember (Id (fromJust (UUID.fromString "00000005-0000-0003-0000-000400000003"))) ( Permissions - { _self = + { self = fromList [DeleteConversation, AddTeamMember, AddRemoveConvMember, GetBilling], - _copy = fromList [GetBilling] + copy = fromList [GetBilling] } ) ( Just @@ -102,8 +102,8 @@ testObject_TeamMember_team_4 = mkTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0005-0000-000100000006"))) ( Permissions - { _self = fromList [ModifyConvName, SetMemberPermissions], - _copy = fromList [SetMemberPermissions] + { self = fromList [ModifyConvName, SetMemberPermissions], + copy = fromList [SetMemberPermissions] } ) ( Just @@ -118,8 +118,8 @@ testObject_TeamMember_team_5 = mkTeamMember (Id (fromJust (UUID.fromString "00000007-0000-0000-0000-000200000001"))) ( Permissions - { _self = fromList [DeleteConversation, GetBilling, SetBilling, GetMemberPermissions], - _copy = fromList [DeleteConversation, GetMemberPermissions] + { self = fromList [DeleteConversation, GetBilling, SetBilling, GetMemberPermissions], + copy = fromList [DeleteConversation, GetMemberPermissions] } ) ( Just @@ -134,10 +134,10 @@ testObject_TeamMember_team_6 = mkTeamMember (Id (fromJust (UUID.fromString "00000006-0000-0007-0000-000800000005"))) ( Permissions - { _self = + { self = fromList [CreateConversation, AddTeamMember, AddRemoveConvMember, SetBilling, SetTeamData], - _copy = fromList [] + copy = fromList [] } ) ( Just @@ -152,7 +152,7 @@ testObject_TeamMember_team_7 = mkTeamMember (Id (fromJust (UUID.fromString "00000007-0000-0000-0000-000200000001"))) ( Permissions - { _self = + { self = fromList [ DeleteConversation, AddRemoveConvMember, @@ -160,7 +160,7 @@ testObject_TeamMember_team_7 = SetMemberPermissions, GetTeamConversations ], - _copy = fromList [] + copy = fromList [] } ) Nothing @@ -171,7 +171,7 @@ testObject_TeamMember_team_8 = mkTeamMember (Id (fromJust (UUID.fromString "00000005-0000-0007-0000-000300000000"))) ( Permissions - { _self = + { self = fromList [ AddRemoveConvMember, ModifyConvName, @@ -179,7 +179,7 @@ testObject_TeamMember_team_8 = SetMemberPermissions, DeleteTeam ], - _copy = fromList [] + copy = fromList [] } ) ( Just @@ -194,8 +194,8 @@ testObject_TeamMember_team_9 = mkTeamMember (Id (fromJust (UUID.fromString "00000008-0000-0006-0000-000300000003"))) ( Permissions - { _self = fromList [AddTeamMember, ModifyConvName], - _copy = fromList [ModifyConvName] + { self = fromList [AddTeamMember, ModifyConvName], + copy = fromList [ModifyConvName] } ) Nothing @@ -205,7 +205,7 @@ testObject_TeamMember_team_10 :: TeamMember testObject_TeamMember_team_10 = mkTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000100000006"))) - (Permissions {_self = fromList [DeleteConversation, AddTeamMember], _copy = fromList []}) + (Permissions {self = fromList [DeleteConversation, AddTeamMember], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000008-0000-0005-0000-000000000002")), fromJust (readUTCTimeMillis "1864-05-03T19:02:13.669Z") @@ -218,9 +218,9 @@ testObject_TeamMember_team_11 = mkTeamMember (Id (fromJust (UUID.fromString "00000004-0000-0001-0000-000400000007"))) ( Permissions - { _self = + { self = fromList [CreateConversation, DeleteConversation, SetTeamData, SetMemberPermissions], - _copy = fromList [] + copy = fromList [] } ) ( Just @@ -234,7 +234,7 @@ testObject_TeamMember_team_12 :: TeamMember testObject_TeamMember_team_12 = mkTeamMember (Id (fromJust (UUID.fromString "00000002-0000-0006-0000-000200000005"))) - (Permissions {_self = fromList [GetTeamConversations], _copy = fromList []}) + (Permissions {self = fromList [GetTeamConversations], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000005-0000-0001-0000-000300000003")), fromJust (readUTCTimeMillis "1864-05-10T22:34:18.259Z") @@ -246,7 +246,7 @@ testObject_TeamMember_team_13 :: TeamMember testObject_TeamMember_team_13 = mkTeamMember (Id (fromJust (UUID.fromString "00000006-0000-0001-0000-000800000006"))) - (Permissions {_self = fromList [CreateConversation, GetMemberPermissions], _copy = fromList [CreateConversation]}) + (Permissions {self = fromList [CreateConversation, GetMemberPermissions], copy = fromList [CreateConversation]}) ( Just ( Id (fromJust (UUID.fromString "00000000-0000-0003-0000-000400000007")), fromJust (readUTCTimeMillis "1864-05-06T08:18:27.514Z") @@ -259,8 +259,8 @@ testObject_TeamMember_team_14 = mkTeamMember (Id (fromJust (UUID.fromString "00000004-0000-0000-0000-000300000007"))) ( Permissions - { _self = fromList [DeleteConversation, AddTeamMember, GetBilling, GetMemberPermissions], - _copy = fromList [GetBilling, GetMemberPermissions] + { self = fromList [DeleteConversation, AddTeamMember, GetBilling, GetMemberPermissions], + copy = fromList [GetBilling, GetMemberPermissions] } ) ( Just @@ -274,7 +274,7 @@ testObject_TeamMember_team_15 :: TeamMember testObject_TeamMember_team_15 = mkTeamMember (Id (fromJust (UUID.fromString "00000005-0000-0006-0000-000800000006"))) - (Permissions {_self = fromList [DeleteTeam], _copy = fromList [DeleteTeam]}) + (Permissions {self = fromList [DeleteTeam], copy = fromList [DeleteTeam]}) ( Just ( Id (fromJust (UUID.fromString "00000008-0000-0000-0000-000500000003")), fromJust (readUTCTimeMillis "1864-05-04T06:15:13.870Z") @@ -286,7 +286,7 @@ testObject_TeamMember_team_16 :: TeamMember testObject_TeamMember_team_16 = mkTeamMember (Id (fromJust (UUID.fromString "00000000-0000-0008-0000-000200000008"))) - (Permissions {_self = fromList [DeleteConversation, GetTeamConversations], _copy = fromList []}) + (Permissions {self = fromList [DeleteConversation, GetTeamConversations], copy = fromList []}) ( Just ( Id (fromJust (UUID.fromString "00000006-0000-0000-0000-000400000002")), fromJust (readUTCTimeMillis "1864-05-10T04:27:37.101Z") @@ -299,7 +299,7 @@ testObject_TeamMember_team_17 = mkTeamMember (Id (fromJust (UUID.fromString "00000006-0000-0006-0000-000500000007"))) ( Permissions - { _self = + { self = fromList [ AddRemoveConvMember, ModifyConvName, @@ -307,7 +307,7 @@ testObject_TeamMember_team_17 = SetTeamData, GetTeamConversations ], - _copy = fromList [AddRemoveConvMember] + copy = fromList [AddRemoveConvMember] } ) ( Just @@ -322,9 +322,9 @@ testObject_TeamMember_team_18 = mkTeamMember (Id (fromJust (UUID.fromString "00000005-0000-0005-0000-000200000008"))) ( Permissions - { _self = + { self = fromList [RemoveTeamMember, ModifyConvName, GetMemberPermissions, SetMemberPermissions], - _copy = fromList [SetMemberPermissions] + copy = fromList [SetMemberPermissions] } ) ( Just @@ -339,9 +339,9 @@ testObject_TeamMember_team_19 = mkTeamMember (Id (fromJust (UUID.fromString "00000003-0000-0002-0000-000200000008"))) ( Permissions - { _self = + { self = fromList [AddTeamMember, ModifyConvName, GetBilling, SetBilling, SetMemberPermissions], - _copy = fromList [SetMemberPermissions] + copy = fromList [SetMemberPermissions] } ) ( Just @@ -356,8 +356,8 @@ testObject_TeamMember_team_20 = mkTeamMember (Id (fromJust (UUID.fromString "00000005-0000-0007-0000-000100000005"))) ( Permissions - { _self = fromList [CreateConversation, AddTeamMember, ModifyConvName, GetBilling], - _copy = fromList [] + { self = fromList [CreateConversation, AddTeamMember, ModifyConvName, GetBilling], + copy = fromList [] } ) ( Just diff --git a/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs b/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs index 8a44da25f23..9795ac54f6c 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs @@ -21,7 +21,6 @@ module Test.Wire.API.Team.Member (tests) where -import Control.Lens ((^.)) import Data.Aeson import Data.Set (isSubsetOf) import Data.Set qualified as Set @@ -57,8 +56,8 @@ permissionTests = -- now it's true, and it's nice to have that written down somewhere. forM_ [(r1, r2) | r1 <- [minBound ..], r2 <- drop 1 [r1 ..]] $ \(r1, r2) -> do - assertBool "owner.self" ((rolePermissions r2 ^. self) `isSubsetOf` (rolePermissions r1 ^. self)) - assertBool "owner.copy" ((rolePermissions r2 ^. copy) `isSubsetOf` (rolePermissions r1 ^. copy)), + assertBool "owner.self" (((rolePermissions r2).self) `isSubsetOf` ((rolePermissions r1).self)) + assertBool "owner.copy" (((rolePermissions r2).copy) `isSubsetOf` ((rolePermissions r1).copy)), testGroup "permissionsRole, rolePermissions" [ testCase "'Role' maps to expected permissions" $ do @@ -76,15 +75,15 @@ permissionTests = case permissionsRole perms of Just role -> do let perms' = rolePermissions role - assertEqual "eq" (perms' ^. self) (perms' ^. copy) - assertBool "self" ((perms' ^. self) `Set.isSubsetOf` (perms ^. self)) - assertBool "copy" ((perms' ^. copy) `Set.isSubsetOf` (perms ^. copy)) + assertEqual "eq" perms'.self perms'.copy + assertBool "self" (perms'.self `Set.isSubsetOf` perms.self) + assertBool "copy" (perms'.copy `Set.isSubsetOf` perms.copy) Nothing -> do let leastPermissions = rolePermissions maxBound assertBool "no role for perms, but strictly more perms than max role" $ not - ( (leastPermissions ^. self) `Set.isSubsetOf` w - && (leastPermissions ^. copy) `Set.isSubsetOf` w' + ( (leastPermissions.self) `Set.isSubsetOf` w + && (leastPermissions.copy) `Set.isSubsetOf` w' ) ] ] @@ -93,8 +92,8 @@ permissionConversionTests :: TestTree permissionConversionTests = testGroup "permsToInt / rolePermissions / serialization of `Role`s" - [ testCase "partner" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleExternalPartner) 1025, - testCase "member" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleMember) 1587, - testCase "admin" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleAdmin) 5951, - testCase "owner" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleOwner) 8191 + [ testCase "partner" $ assertEqual "" (permsToInt . self $ rolePermissions RoleExternalPartner) 1025, + testCase "member" $ assertEqual "" (permsToInt . self $ rolePermissions RoleMember) 1587, + testCase "admin" $ assertEqual "" (permsToInt . self $ rolePermissions RoleAdmin) 5951, + testCase "owner" $ assertEqual "" (permsToInt . self $ rolePermissions RoleOwner) 8191 ] diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index 24a8758783a..17d3f312a20 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -8,9 +8,11 @@ , amazonka-core , amazonka-ses , async +, attoparsec , base , base16-bytestring , bilge +, bloodhound , bytestring , bytestring-conversion , cassandra-util @@ -51,12 +53,15 @@ , polysemy-time , polysemy-wire-zoo , postie +, prometheus-client , QuickCheck , quickcheck-instances , random , resource-pool , resourcet , retry +, saml2-web-sso +, schema-profunctor , scientific , servant , servant-client-core @@ -65,6 +70,7 @@ , string-conversions , template , text +, text-icu-translit , time , time-out , time-units @@ -92,9 +98,11 @@ mkDerivation { amazonka-core amazonka-ses async + attoparsec base base16-bytestring bilge + bloodhound bytestring bytestring-conversion cassandra-util @@ -130,15 +138,19 @@ mkDerivation { polysemy-plugin polysemy-time polysemy-wire-zoo + prometheus-client QuickCheck resource-pool resourcet retry + saml2-web-sso + schema-profunctor servant servant-client-core stomp-queue template text + text-icu-translit time time-out time-units @@ -162,6 +174,7 @@ mkDerivation { base bilge bytestring + cassandra-util containers crypton data-default diff --git a/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs index d8e0e0f077a..f00431d8201 100644 --- a/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/BlockListStore/Cassandra.hs @@ -10,13 +10,13 @@ import Wire.BlockListStore (BlockListStore (..)) import Wire.UserKeyStore interpretBlockListStoreToCassandra :: - forall m r a. - (MonadClient m, Member (Embed m) r) => - Sem (BlockListStore ': r) a -> - Sem r a -interpretBlockListStoreToCassandra = + forall r. + (Member (Embed IO) r) => + ClientState -> + InterpreterFor BlockListStore r +interpretBlockListStoreToCassandra casClient = interpret $ - embed @m . \case + embed @IO . runClient casClient . \case Insert uk -> insert uk Exists uk -> exists uk Delete uk -> delete uk diff --git a/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs b/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs index 8520f11ff69..39f99b10e64 100644 --- a/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/FederationAPIAccess/Interpreter.hs @@ -22,6 +22,15 @@ data FederationAPIAccessConfig = FederationAPIAccessConfig type FederatedActionRunner fedM r = forall c x. Domain -> fedM c x -> Sem r (Either FederationError x) +noFederationAPIAccess :: + forall r fedM. + (Member (Concurrency 'Unsafe) r) => + InterpreterFor (FederationAPIAccess fedM) r +noFederationAPIAccess = + interpretFederationAPIAccessGeneral + (\_ _ -> pure $ Left FederationNotConfigured) + (pure False) + interpretFederationAPIAccess :: forall r. (Member (Embed IO) r, Member (Concurrency 'Unsafe) r) => diff --git a/services/brig/src/Brig/Effects/FederationConfigStore.hs b/libs/wire-subsystems/src/Wire/FederationConfigStore.hs similarity index 90% rename from services/brig/src/Brig/Effects/FederationConfigStore.hs rename to libs/wire-subsystems/src/Wire/FederationConfigStore.hs index 07ace482740..ead299d37c0 100644 --- a/services/brig/src/Brig/Effects/FederationConfigStore.hs +++ b/libs/wire-subsystems/src/Wire/FederationConfigStore.hs @@ -1,6 +1,6 @@ {-# LANGUAGE TemplateHaskell #-} -module Brig.Effects.FederationConfigStore where +module Wire.FederationConfigStore where import Data.Domain import Data.Id @@ -24,6 +24,8 @@ data AddFederationRemoteTeamResult | AddFederationRemoteTeamDomainNotFound | AddFederationRemoteTeamRestrictionAllowAll +-- FUTUREWORK: This store effect is more than just a store, +-- we should break it up in business logic and store data FederationConfigStore m a where GetFederationConfig :: Domain -> FederationConfigStore m (Maybe FederationDomainConfig) GetFederationConfigs :: FederationConfigStore m FederationDomainConfigs diff --git a/services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs similarity index 97% rename from services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs rename to libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs index 32b13005e25..2038fc697ed 100644 --- a/services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/FederationConfigStore/Cassandra.hs @@ -15,14 +15,13 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.Effects.FederationConfigStore.Cassandra +module Wire.FederationConfigStore.Cassandra ( interpretFederationDomainConfig, remotesMapFromCfgFile, AddFederationRemoteResult (..), ) where -import Brig.Effects.FederationConfigStore import Cassandra import Control.Exception (ErrorCall (ErrorCall)) import Control.Lens @@ -34,8 +33,10 @@ import Data.Qualified import Database.CQL.Protocol (SerialConsistency (LocalSerialConsistency), serialConsistency) import Imports import Polysemy +import Polysemy.Embed import Wire.API.Routes.FederationDomainConfig import Wire.API.User.Search +import Wire.FederationConfigStore -- | Interpreter for getting the federation config from the database and the config file. -- The config file is injected into the interpreter and has precedence over the database. @@ -45,17 +46,17 @@ import Wire.API.User.Search -- If a domain is configured in the config file, it is not allowed to add a team restriction to it in the database. -- In the future the config file will be removed and the database will be the only source of truth. interpretFederationDomainConfig :: - forall m r a. - ( MonadClient m, - Member (Embed m) r + forall r a. + ( Member (Embed IO) r ) => + ClientState -> Maybe FederationStrategy -> Map Domain FederationDomainConfig -> Sem (FederationConfigStore ': r) a -> Sem r a -interpretFederationDomainConfig mFedStrategy fedCfgs = +interpretFederationDomainConfig casClient mFedStrategy fedCfgs = interpret $ - embed @m . \case + runEmbedded (runClient casClient) . embed . \case GetFederationConfig d -> getFederationConfig' fedCfgs d GetFederationConfigs -> getFederationConfigs' mFedStrategy fedCfgs AddFederationConfig cnf -> addFederationConfig' fedCfgs cnf diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index cbb4f769837..07003fed93c 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -112,6 +112,12 @@ data GalleyAPIAccess m a where GetAllTeamFeaturesForUser :: Maybe UserId -> GalleyAPIAccess m AllTeamFeatures + GetFeatureConfigForTeam :: + ( IsFeatureConfig feature, + Typeable feature + ) => + TeamId -> + GalleyAPIAccess m (LockableFeature feature) GetVerificationCodeEnabled :: TeamId -> GalleyAPIAccess m Bool diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index dcafabedce8..340018628f7 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -81,6 +81,7 @@ interpretGalleyAPIAccessToRpc disabledVersions galleyEndpoint = GetTeamName id' -> getTeamName id' GetTeamLegalHoldStatus id' -> getTeamLegalHoldStatus id' GetTeamSearchVisibility id' -> getTeamSearchVisibility id' + GetFeatureConfigForTeam tid -> getFeatureConfigForTeam tid GetUserLegalholdStatus id' tid -> getUserLegalholdStatus id' tid ChangeTeamStatus id' ts m_al -> changeTeamStatus id' ts m_al MemberIsTeamOwner id' id'' -> memberIsTeamOwner id' id'' @@ -453,6 +454,25 @@ getTeamSearchVisibility tid = . paths ["i", "teams", toByteString' tid, "search-visibility"] . expect2xx +getFeatureConfigForTeam :: + forall feature r. + ( IsFeatureConfig feature, + Typeable feature, + Member TinyLog r, + Member Rpc r, + Member (Error ParseException) r + ) => + TeamId -> + Sem (Input Endpoint : r) (LockableFeature feature) +getFeatureConfigForTeam tid = do + debug $ remote "galley" . msg (val "Get feature config for team") + galleyRequest req >>= decodeBodyOrThrow "galley" + where + req = + method GET + . paths ["i", "teams", toByteString' tid, "features", featureNameBS @feature] + . expect2xx + getVerificationCodeEnabled :: ( Member (Error ParseException) r, Member Rpc r, diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore.hs new file mode 100644 index 00000000000..92e3c7ea97e --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore.hs @@ -0,0 +1,43 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.IndexedUserStore where + +import Data.Id +import Database.Bloodhound qualified as ES +import Database.Bloodhound.Types hiding (SearchResult) +import Imports +import Polysemy +import Wire.API.User.Search +import Wire.UserSearch.Types + +data IndexedUserStoreError + = IndexUpdateError ES.EsError + | IndexLookupError ES.EsError + | IndexError Text + deriving (Show) + +instance Exception IndexedUserStoreError + +data IndexedUserStore m a where + Upsert :: DocId -> UserDoc -> VersionControl -> IndexedUserStore m () + UpdateTeamSearchVisibilityInbound :: + TeamId -> + SearchVisibilityInbound -> + IndexedUserStore m () + -- | Will only be applied to main ES index and not the additional one + BulkUpsert :: [(DocId, UserDoc, VersionControl)] -> IndexedUserStore m () + DoesIndexExist :: IndexedUserStore m Bool + SearchUsers :: + UserId -> + Maybe TeamId -> + TeamSearchInfo -> + Text -> + Int -> + IndexedUserStore m (SearchResult UserDoc) + PaginateTeamMembers :: + BrowseTeamFilters -> + Int -> + Maybe PagingState -> + IndexedUserStore m (SearchResult UserDoc) + +makeSem ''IndexedUserStore diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs new file mode 100644 index 00000000000..66969fe61d6 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.IndexedUserStore.Bulk where + +import Polysemy +import Wire.UserSearch.Migration + +-- | Increase this number any time you want to force reindexing. +expectedMigrationVersion :: MigrationVersion +expectedMigrationVersion = MigrationVersion 6 + +-- | Bulk operations, must not be used from any web handler +data IndexedUserStoreBulk m a where + -- | Only changes data if it is not updated since last update, use when users + -- need to be synced because of an outage, or migrating to a new ES instance. + SyncAllUsers :: IndexedUserStoreBulk m () + -- | Overwrite all users in the ES index, use it when trying to fix some + -- inconsistency or while introducing a new field in the mapping. + ForceSyncAllUsers :: IndexedUserStoreBulk m () + MigrateData :: IndexedUserStoreBulk m () + +makeSem ''IndexedUserStoreBulk diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs new file mode 100644 index 00000000000..26ccca02987 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs @@ -0,0 +1,133 @@ +module Wire.IndexedUserStore.Bulk.ElasticSearch where + +import Cassandra.Exec (paginateWithStateC) +import Conduit (ConduitT, runConduit, (.|)) +import Data.Conduit.Combinators qualified as Conduit +import Data.Id +import Data.Map qualified as Map +import Data.Set qualified as Set +import Database.Bloodhound qualified as ES +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.TinyLog +import Polysemy.TinyLog qualified as Log +import System.Logger.Message qualified as Log +import Wire.API.Team.Feature +import Wire.GalleyAPIAccess +import Wire.IndexedUserStore (IndexedUserStore) +import Wire.IndexedUserStore qualified as IndexedUserStore +import Wire.IndexedUserStore.Bulk +import Wire.IndexedUserStore.MigrationStore +import Wire.IndexedUserStore.MigrationStore qualified as MigrationStore +import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe), unsafePooledForConcurrentlyN) +import Wire.UserSearch.Migration +import Wire.UserSearch.Types +import Wire.UserStore +import Wire.UserStore.IndexUser + +interpretIndexedUserStoreBulk :: + ( Member TinyLog r, + Member UserStore r, + Member (Concurrency Unsafe) r, + Member GalleyAPIAccess r, + Member IndexedUserStore r, + Member (Error MigrationException) r, + Member IndexedUserMigrationStore r + ) => + InterpreterFor IndexedUserStoreBulk r +interpretIndexedUserStoreBulk = interpret \case + SyncAllUsers -> syncAllUsersImpl + ForceSyncAllUsers -> forceSyncAllUsersImpl + MigrateData -> migrateDataImpl + +syncAllUsersImpl :: + forall r. + ( Member UserStore r, + Member TinyLog r, + Member (Concurrency 'Unsafe) r, + Member GalleyAPIAccess r, + Member IndexedUserStore r + ) => + Sem r () +syncAllUsersImpl = syncAllUsersWithVersion ES.ExternalGT + +forceSyncAllUsersImpl :: + forall r. + ( Member UserStore r, + Member TinyLog r, + Member (Concurrency 'Unsafe) r, + Member GalleyAPIAccess r, + Member IndexedUserStore r + ) => + Sem r () +forceSyncAllUsersImpl = syncAllUsersWithVersion ES.ExternalGTE + +syncAllUsersWithVersion :: + forall r. + ( Member UserStore r, + Member TinyLog r, + Member (Concurrency 'Unsafe) r, + Member GalleyAPIAccess r, + Member IndexedUserStore r + ) => + (ES.ExternalDocVersion -> ES.VersionControl) -> + Sem r () +syncAllUsersWithVersion mkVersion = + runConduit $ + paginateWithStateC (getIndexUsersPaginated 1000) + .| logPage + .| mkUserDocs + .| Conduit.mapM_ IndexedUserStore.bulkUpsert + where + logPage :: ConduitT [IndexUser] [IndexUser] (Sem r) () + logPage = Conduit.iterM $ \page -> do + info $ + Log.field "size" (length page) + . Log.msg (Log.val "Reindex: processing C* page") + + mkUserDocs :: ConduitT [IndexUser] [(ES.DocId, UserDoc, ES.VersionControl)] (Sem r) () + mkUserDocs = Conduit.mapM $ \page -> do + let teamIds = + Set.fromList $ + mapMaybe (fmap value . ((.teamId))) page + visMap <- fmap Map.fromList . unsafePooledForConcurrentlyN 16 teamIds $ \t -> + (t,) <$> teamSearchVisibilityInbound t + let vis indexUser = fromMaybe defaultSearchVisibilityInbound $ (flip Map.lookup visMap . value =<< indexUser.teamId) + mkUserDoc indexUser = indexUserToDoc (vis indexUser) indexUser + mkDocVersion = mkVersion . ES.ExternalDocVersion . docVersion . indexUserToVersion + pure $ map (\u -> (userIdToDocId u.userId, mkUserDoc u, mkDocVersion u)) page + +migrateDataImpl :: + ( Member IndexedUserStore r, + Member (Error MigrationException) r, + Member IndexedUserMigrationStore r, + Member UserStore r, + Member (Concurrency Unsafe) r, + Member GalleyAPIAccess r, + Member TinyLog r + ) => + Sem r () +migrateDataImpl = do + unlessM IndexedUserStore.doesIndexExist $ + throw TargetIndexAbsent + MigrationStore.ensureMigrationIndex + foundVersion <- MigrationStore.getLatestMigrationVersion + if expectedMigrationVersion > foundVersion + then do + Log.info $ + Log.msg (Log.val "Migration necessary.") + . Log.field "expectedVersion" expectedMigrationVersion + . Log.field "foundVersion" foundVersion + forceSyncAllUsersImpl + MigrationStore.persistMigrationVersion expectedMigrationVersion + else do + Log.info $ + Log.msg (Log.val "No migration necessary.") + . Log.field "expectedVersion" expectedMigrationVersion + . Log.field "foundVersion" foundVersion + +teamSearchVisibilityInbound :: (Member GalleyAPIAccess r) => TeamId -> Sem r SearchVisibilityInbound +teamSearchVisibilityInbound tid = + searchVisibilityInboundFromFeatureStatus . (.status) + <$> getFeatureConfigForTeam @_ @SearchVisibilityInboundConfig tid diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs new file mode 100644 index 00000000000..f299017ce2b --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs @@ -0,0 +1,500 @@ +{-# LANGUAGE RecordWildCards #-} + +module Wire.IndexedUserStore.ElasticSearch where + +import Control.Error (lastMay) +import Control.Exception (throwIO) +import Data.Aeson +import Data.Aeson.Key qualified as Key +import Data.ByteString qualified as LBS +import Data.ByteString.Builder +import Data.ByteString.Conversion +import Data.Id +import Data.Text qualified as Text +import Data.Text.Ascii +import Data.Text.Encoding qualified as Text +import Database.Bloodhound qualified as ES +import Imports +import Network.HTTP.Client +import Network.HTTP.Types +import Polysemy +import Wire.API.User.Search +import Wire.IndexedUserStore +import Wire.Sem.Metrics (Metrics) +import Wire.Sem.Metrics qualified as Metrics +import Wire.UserSearch.Metrics +import Wire.UserSearch.Types +import Wire.UserStore.IndexUser + +data ESConn = ESConn + { env :: ES.BHEnv, + indexName :: ES.IndexName + } + +data IndexedUserStoreConfig = IndexedUserStoreConfig + { conn :: ESConn, + additionalConn :: Maybe ESConn + } + +interpretIndexedUserStoreES :: + ( Member (Embed IO) r, + Member Metrics r + ) => + IndexedUserStoreConfig -> + InterpreterFor IndexedUserStore r +interpretIndexedUserStoreES cfg = + interpret $ \case + Upsert docId userDoc versioning -> upsertImpl cfg docId userDoc versioning + UpdateTeamSearchVisibilityInbound tid vis -> + updateTeamSearchVisibilityInboundImpl cfg tid vis + BulkUpsert docs -> bulkUpsertImpl cfg docs + DoesIndexExist -> doesIndexExistImpl cfg + SearchUsers searcherId mSearcherTeam teamSearchInfo term maxResults -> + searchUsersImpl cfg searcherId mSearcherTeam teamSearchInfo term maxResults + PaginateTeamMembers filters maxResults mPagingState -> + paginateTeamMembersImpl cfg filters maxResults mPagingState + +upsertImpl :: + forall r. + ( Member (Embed IO) r, + Member Metrics r + ) => + IndexedUserStoreConfig -> + ES.DocId -> + UserDoc -> + ES.VersionControl -> + Sem r () +upsertImpl cfg docId userDoc versioning = do + void $ runInBothES cfg indexDoc + where + indexDoc :: ES.IndexName -> ES.BH (Sem r) () + indexDoc idx = do + r <- ES.indexDocument idx mappingName settings userDoc docId + unless (ES.isSuccess r || ES.isVersionConflict r) $ do + lift $ Metrics.incCounter indexUpdateErrorCounter + res <- liftIO $ ES.parseEsResponse r + liftIO . throwIO . IndexUpdateError . either id id $ res + lift $ Metrics.incCounter indexUpdateSuccessCounter + + settings = ES.defaultIndexDocumentSettings {ES.idsVersionControl = versioning} + +updateTeamSearchVisibilityInboundImpl :: forall r. (Member (Embed IO) r) => IndexedUserStoreConfig -> TeamId -> SearchVisibilityInbound -> Sem r () +updateTeamSearchVisibilityInboundImpl cfg tid vis = + void $ runInBothES cfg updateAllDocs + where + updateAllDocs :: ES.IndexName -> ES.BH (Sem r) () + updateAllDocs idx = do + r <- ES.updateByQuery idx query (Just script) + unless (ES.isSuccess r || ES.isVersionConflict r) $ do + res <- liftIO $ ES.parseEsResponse r + liftIO . throwIO . IndexUpdateError . either id id $ res + + query :: ES.Query + query = ES.TermQuery (ES.Term "team" $ idToText tid) Nothing + + script :: ES.Script + script = ES.Script (Just (ES.ScriptLanguage "painless")) (Just (ES.ScriptInline scriptText)) Nothing Nothing + + -- Unfortunately ES disallows updating ctx._version with a "Update By Query" + scriptText = + "ctx._source." + <> Key.toText searchVisibilityInboundFieldName + <> " = '" + <> Text.decodeUtf8 (toByteString' vis) + <> "';" + +bulkUpsertImpl :: (Member (Embed IO) r) => IndexedUserStoreConfig -> [(ES.DocId, UserDoc, ES.VersionControl)] -> Sem r () +bulkUpsertImpl cfg docs = do + let bhe = cfg.conn.env + ES.IndexName idx = cfg.conn.indexName + ES.MappingName mpp = mappingName + (ES.Server base) = ES.bhServer bhe + baseReq <- embed $ parseRequest (Text.unpack $ base <> "/" <> idx <> "/" <> mpp <> "/_bulk") + let reqWithoutCreds = + baseReq + { method = "POST", + requestHeaders = [(hContentType, "application/x-ndjson")], + requestBody = RequestBodyLBS (toLazyByteString (foldMap encodeActionAndData docs)) + } + req <- embed $ bhe.bhRequestHook reqWithoutCreds + res <- embed $ httpLbs req (ES.bhManager bhe) + unless (ES.isSuccess res) $ do + parsedRes <- liftIO $ ES.parseEsResponse res + liftIO . throwIO . IndexUpdateError . either id id $ parsedRes + where + encodeJSONToString :: (ToJSON a) => a -> Builder + encodeJSONToString = fromEncoding . toEncoding + + encodeActionAndData :: (ES.DocId, UserDoc, ES.VersionControl) -> Builder + encodeActionAndData (docId, userDoc, versionControl) = + encodeJSONToString (bulkIndexAction docId versionControl) + <> "\n" + <> encodeJSONToString userDoc + <> "\n" + + bulkIndexAction :: ES.DocId -> ES.VersionControl -> Value + bulkIndexAction docId versionControl = + let (versionType :: Maybe Text, version) = case versionControl of + ES.NoVersionControl -> (Nothing, Nothing) + ES.InternalVersion v -> (Nothing, Just v) + ES.ExternalGT (ES.ExternalDocVersion v) -> (Just "external", Just v) + ES.ExternalGTE (ES.ExternalDocVersion v) -> (Just "external_gte", Just v) + ES.ForceVersion (ES.ExternalDocVersion v) -> (Just "force", Just v) + in object + [ "index" + .= object + [ "_id" .= docId, + "_version_type" .= versionType, + "_version" .= version + ] + ] + +doesIndexExistImpl :: (Member (Embed IO) r) => IndexedUserStoreConfig -> Sem r Bool +doesIndexExistImpl cfg = do + (mainExists, fromMaybe True -> additionalExists) <- runInBothES cfg ES.indexExists + pure $ mainExists && additionalExists + +searchUsersImpl :: + (Member (Embed IO) r) => + IndexedUserStoreConfig -> + UserId -> + Maybe TeamId -> + TeamSearchInfo -> + Text -> + Int -> + Sem r (SearchResult UserDoc) +searchUsersImpl cfg searcherId mSearcherTeam teamSearchInfo term maxResults = + queryIndex cfg maxResults $ + defaultUserQuery searcherId mSearcherTeam teamSearchInfo term + +-- | The default or canonical 'IndexQuery'. +-- +-- The intention behind parameterising 'queryIndex' over the 'IndexQuery' is that +-- it allows to experiment with different queries (perhaps in an A/B context). +-- +-- FUTUREWORK: Drop legacyPrefixMatch +defaultUserQuery :: UserId -> Maybe TeamId -> TeamSearchInfo -> Text -> IndexQuery Contact +defaultUserQuery searcher mSearcherTeamId teamSearchInfo (normalized -> term') = + let matchPhraseOrPrefix = + ES.QueryMultiMatchQuery $ + ( ES.mkMultiMatchQuery + [ ES.FieldName "handle.prefix^2", + ES.FieldName "normalized.prefix", + ES.FieldName "normalized^3" + ] + (ES.QueryString term') + ) + { ES.multiMatchQueryType = Just ES.MultiMatchMostFields, + ES.multiMatchQueryOperator = ES.And + } + query = + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustMatch = + [ ES.QueryBoolQuery + boolQuery + { ES.boolQueryShouldMatch = [matchPhraseOrPrefix], + -- This removes exact handle matches, as they are fetched from cassandra + ES.boolQueryMustNotMatch = [termQ "handle" term'] + } + ], + ES.boolQueryShouldMatch = [ES.QueryExistsQuery (ES.FieldName "handle")] + } + -- This reduces relevance on users not in team of search by 90% (no + -- science behind that number). If the searcher is not part of a team the + -- relevance is not reduced for any users. + queryWithBoost = + ES.QueryBoostingQuery + ES.BoostingQuery + { ES.positiveQuery = query, + ES.negativeQuery = maybe ES.QueryMatchNoneQuery matchUsersNotInTeam mSearcherTeamId, + ES.negativeBoost = ES.Boost 0.1 + } + in mkUserQuery searcher mSearcherTeamId teamSearchInfo queryWithBoost + +paginateTeamMembersImpl :: + (Member (Embed IO) r) => + IndexedUserStoreConfig -> + BrowseTeamFilters -> + Int -> + Maybe PagingState -> + Sem r (SearchResult UserDoc) +paginateTeamMembersImpl cfg BrowseTeamFilters {..} maxResults mPagingState = do + let (IndexQuery q f sortSpecs) = + teamUserSearchQuery teamId mQuery mRoleFilter mSortBy mSortOrder + let search = + (ES.mkSearch (Just q) (Just f)) + { -- we are requesting one more result than the page size to determine if there is a next page + ES.size = ES.Size (fromIntegral maxResults + 1), + ES.sortBody = Just (fmap ES.DefaultSortSpec sortSpecs), + ES.searchAfterKey = toSearchAfterKey =<< mPagingState + } + mkResult <$> searchInMainIndex cfg search + where + toSearchAfterKey ps = decode' . LBS.fromStrict =<< (decodeBase64Url . unPagingState) ps + + fromSearchAfterKey :: ES.SearchAfterKey -> PagingState + fromSearchAfterKey = PagingState . encodeBase64Url . LBS.toStrict . encode + + mkResult es = + let hitsPlusOne = ES.hits . ES.searchHits $ es + hits = take (fromIntegral maxResults) hitsPlusOne + mps = fromSearchAfterKey <$> lastMay (mapMaybe ES.hitSort hits) + results = mapMaybe ES.hitSource hits + in SearchResult + { searchFound = ES.hitsTotal . ES.searchHits $ es, + searchReturned = length results, + searchTook = ES.took es, + searchResults = results, + searchPolicy = FullSearch, + searchPagingState = mps, + searchHasMore = Just $ length hitsPlusOne > length hits + } + +searchInMainIndex :: forall r. (Member (Embed IO) r) => IndexedUserStoreConfig -> ES.Search -> Sem r (ES.SearchResult UserDoc) +searchInMainIndex cfg search = do + r <- ES.runBH cfg.conn.env $ do + res <- ES.searchByType cfg.conn.indexName mappingName search + liftIO $ ES.parseEsResponse res + either (embed . throwIO . IndexLookupError) pure r + +queryIndex :: + (Member (Embed IO) r) => + IndexedUserStoreConfig -> + Int -> + IndexQuery x -> + Sem r (SearchResult UserDoc) +queryIndex cfg s (IndexQuery q f _) = do + let search = (ES.mkSearch (Just q) (Just f)) {ES.size = ES.Size (fromIntegral s)} + mkResult <$> searchInMainIndex cfg search + where + mkResult es = + let results = mapMaybe ES.hitSource . ES.hits . ES.searchHits $ es + in SearchResult + { searchFound = ES.hitsTotal . ES.searchHits $ es, + searchReturned = length results, + searchTook = ES.took es, + searchResults = results, + searchPolicy = FullSearch, + searchPagingState = Nothing, + searchHasMore = Nothing + } + +teamUserSearchQuery :: + TeamId -> + Maybe Text -> + Maybe RoleFilter -> + Maybe TeamUserSearchSortBy -> + Maybe TeamUserSearchSortOrder -> + IndexQuery TeamContact +teamUserSearchQuery tid mbSearchText _mRoleFilter mSortBy mSortOrder = + IndexQuery + ( maybe + (ES.MatchAllQuery Nothing) + matchPhraseOrPrefix + mbQStr + ) + teamFilter + -- in combination with pagination a non-unique search specification can lead to missing results + -- therefore we use the unique `_doc` value as a tie breaker + -- - see https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-sort.html for details on `_doc` + -- - see https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-search-after.html for details on pagination and tie breaker + -- in the latter article it "is advised to duplicate (client side or [...]) the content of the _id field + -- in another field that has doc value enabled and to use this new field as the tiebreaker for the sort" + -- so alternatively we could use the user ID as a tie breaker, but this would require a change in the index mapping + (sorting ++ sortingTieBreaker) + where + sorting :: [ES.DefaultSort] + sorting = + maybe + [defaultSort SortByCreatedAt SortOrderDesc | isNothing mbQStr] + (\tuSortBy -> [defaultSort tuSortBy (fromMaybe SortOrderAsc mSortOrder)]) + mSortBy + sortingTieBreaker :: [ES.DefaultSort] + sortingTieBreaker = [ES.DefaultSort (ES.FieldName "_doc") ES.Ascending Nothing Nothing Nothing Nothing] + + mbQStr :: Maybe Text + mbQStr = + case mbSearchText of + Nothing -> Nothing + Just q -> + case normalized q of + "" -> Nothing + term' -> Just term' + + matchPhraseOrPrefix term' = + ES.QueryMultiMatchQuery $ + ( ES.mkMultiMatchQuery + [ ES.FieldName "email^4", + ES.FieldName "handle^4", + ES.FieldName "normalized^3", + ES.FieldName "email.prefix^3", + ES.FieldName "handle.prefix^2", + ES.FieldName "normalized.prefix" + ] + (ES.QueryString term') + ) + { ES.multiMatchQueryType = Just ES.MultiMatchMostFields, + ES.multiMatchQueryOperator = ES.And + } + + teamFilter = + ES.Filter $ + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustMatch = [ES.TermQuery (ES.Term "team" $ idToText tid) Nothing] + } + + defaultSort :: TeamUserSearchSortBy -> TeamUserSearchSortOrder -> ES.DefaultSort + defaultSort tuSortBy sortOrder = + ES.DefaultSort + ( case tuSortBy of + SortByName -> ES.FieldName "name" + SortByHandle -> ES.FieldName "handle.keyword" + SortByEmail -> ES.FieldName "email.keyword" + SortBySAMLIdp -> ES.FieldName "saml_idp" + SortByManagedBy -> ES.FieldName "managed_by" + SortByRole -> ES.FieldName "role" + SortByCreatedAt -> ES.FieldName "created_at" + ) + ( case sortOrder of + SortOrderAsc -> ES.Ascending + SortOrderDesc -> ES.Descending + ) + Nothing + Nothing + Nothing + Nothing + +mkUserQuery :: UserId -> Maybe TeamId -> TeamSearchInfo -> ES.Query -> IndexQuery Contact +mkUserQuery searcher mSearcherTeamId teamSearchInfo q = + IndexQuery + q + ( ES.Filter + . ES.QueryBoolQuery + $ boolQuery + { ES.boolQueryMustNotMatch = maybeToList $ matchSelf searcher, + ES.boolQueryMustMatch = + [ restrictSearchSpace mSearcherTeamId teamSearchInfo, + ES.QueryBoolQuery + boolQuery + { ES.boolQueryShouldMatch = + [ termQ "account_status" "active", + -- Also match entries where the account_status field is not present. + -- These must have been inserted before we added the account_status + -- and at that time we only inserted active users in the first place. + -- This should be unnecessary after re-indexing, but let's be lenient + -- here for a while. + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustNotMatch = + [ES.QueryExistsQuery (ES.FieldName "account_status")] + } + ] + } + ] + } + ) + [] + +termQ :: Text -> Text -> ES.Query +termQ f v = + ES.TermQuery + ES.Term + { ES.termField = f, + ES.termValue = v + } + Nothing + +matchSelf :: UserId -> Maybe ES.Query +matchSelf searcher = Just (termQ "_id" (idToText searcher)) + +-- | See 'TeamSearchInfo' +restrictSearchSpace :: Maybe TeamId -> TeamSearchInfo -> ES.Query +-- restrictSearchSpace (FederatedSearch Nothing) = +-- ES.QueryBoolQuery +-- boolQuery +-- { ES.boolQueryShouldMatch = +-- [ matchNonTeamMemberUsers, +-- matchTeamMembersSearchableByAllTeams +-- ] +-- } +-- restrictSearchSpace (FederatedSearch (Just [])) = +-- ES.QueryBoolQuery +-- boolQuery +-- { ES.boolQueryMustMatch = +-- [ -- if the list of allowed teams is empty, this is impossible to fulfill, and no results will be returned +-- -- this case should be handled earlier, so this is just a safety net +-- ES.TermQuery (ES.Term "team" "must not match any team") Nothing +-- ] +-- } +-- restrictSearchSpace (FederatedSearch (Just teams)) = +-- ES.QueryBoolQuery +-- boolQuery +-- { ES.boolQueryMustMatch = +-- [ matchTeamMembersSearchableByAllTeams, +-- onlyInTeams +-- ] +-- } +-- where +-- onlyInTeams = ES.QueryBoolQuery boolQuery {ES.boolQueryShouldMatch = map matchTeamMembersOf teams} +restrictSearchSpace mteam searchInfo = + case (mteam, searchInfo) of + (Nothing, _) -> matchNonTeamMemberUsers + (Just _, NoTeam) -> matchNonTeamMemberUsers + (Just searcherTeam, TeamOnly team) -> + if searcherTeam == team + then matchTeamMembersOf team + else ES.QueryMatchNoneQuery + (Just searcherTeam, AllUsers) -> + ES.QueryBoolQuery + boolQuery + { ES.boolQueryShouldMatch = + [ matchNonTeamMemberUsers, + matchTeamMembersSearchableByAllTeams, + matchTeamMembersOf searcherTeam + ] + } + +matchTeamMembersOf :: TeamId -> ES.Query +matchTeamMembersOf team = ES.TermQuery (ES.Term "team" $ idToText team) Nothing + +matchTeamMembersSearchableByAllTeams :: ES.Query +matchTeamMembersSearchableByAllTeams = + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustMatch = + [ ES.QueryExistsQuery $ ES.FieldName "team", + ES.TermQuery (ES.Term (Key.toText searchVisibilityInboundFieldName) "searchable-by-all-teams") Nothing + ] + } + +matchNonTeamMemberUsers :: ES.Query +matchNonTeamMemberUsers = + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustNotMatch = [ES.QueryExistsQuery $ ES.FieldName "team"] + } + +matchUsersNotInTeam :: TeamId -> ES.Query +matchUsersNotInTeam tid = + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustNotMatch = [ES.TermQuery (ES.Term "team" $ idToText tid) Nothing] + } + +-------------------------------------------- +-- Utils + +runInBothES :: (Monad m) => IndexedUserStoreConfig -> (ES.IndexName -> ES.BH m a) -> m (a, Maybe a) +runInBothES cfg f = do + x <- ES.runBH cfg.conn.env $ f cfg.conn.indexName + y <- forM cfg.additionalConn $ \additional -> + ES.runBH additional.env $ f additional.indexName + pure (x, y) + +mappingName :: ES.MappingName +mappingName = ES.MappingName "user" + +boolQuery :: ES.BoolQuery +boolQuery = ES.mkBoolQuery [] [] [] [] diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore.hs new file mode 100644 index 00000000000..1cb9c8d51f6 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.IndexedUserStore.MigrationStore where + +import Polysemy +import Wire.UserSearch.Migration + +data IndexedUserMigrationStore m a where + EnsureMigrationIndex :: IndexedUserMigrationStore m () + GetLatestMigrationVersion :: IndexedUserMigrationStore m MigrationVersion + PersistMigrationVersion :: MigrationVersion -> IndexedUserMigrationStore m () + +makeSem ''IndexedUserMigrationStore diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore/ElasticSearch.hs new file mode 100644 index 00000000000..9532a54246c --- /dev/null +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/MigrationStore/ElasticSearch.hs @@ -0,0 +1,73 @@ +module Wire.IndexedUserStore.MigrationStore.ElasticSearch where + +import Data.Aeson +import Data.Text qualified as Text +import Database.Bloodhound qualified as ES +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.TinyLog +import System.Logger.Message qualified as Log +import Wire.IndexedUserStore.MigrationStore +import Wire.Sem.Logger qualified as Log +import Wire.UserSearch.Migration + +interpretIndexedUserMigrationStoreES :: (Member (Embed IO) r, Member (Error MigrationException) r, Member TinyLog r) => ES.BHEnv -> InterpreterFor IndexedUserMigrationStore r +interpretIndexedUserMigrationStoreES env = interpret $ \case + EnsureMigrationIndex -> ensureMigrationIndexImpl env + GetLatestMigrationVersion -> getLatestMigrationVersionImpl env + PersistMigrationVersion v -> persistMigrationVersionImpl env v + +ensureMigrationIndexImpl :: (Member TinyLog r, Member (Embed IO) r, Member (Error MigrationException) r) => ES.BHEnv -> Sem r () +ensureMigrationIndexImpl env = do + unlessM (ES.runBH env $ ES.indexExists migrationIndexName) $ do + Log.info $ + Log.msg (Log.val "Creating migrations index, used for tracking which migrations have run") + ES.runBH env (ES.createIndexWith [] 1 migrationIndexName) + >>= throwIfNotCreated CreateMigrationIndexFailed + ES.runBH env (ES.putMapping migrationIndexName migrationMappingName migrationIndexMapping) + >>= throwIfNotCreated PutMappingFailed + where + throwIfNotCreated mkErr response = + unless (ES.isSuccess response) $ + throw $ + mkErr (show response) + +getLatestMigrationVersionImpl :: (Member (Embed IO) r, Member (Error MigrationException) r) => ES.BHEnv -> Sem r MigrationVersion +getLatestMigrationVersionImpl env = do + reply <- ES.runBH env $ ES.searchByIndex migrationIndexName (ES.mkSearch Nothing Nothing) + resp <- liftIO $ ES.parseEsResponse reply + result <- either (throw . FetchMigrationVersionsFailed . show) pure resp + let versions = map ES.hitSource $ ES.hits . ES.searchHits $ result + case versions of + [] -> + pure $ MigrationVersion 0 + vs -> + if any isNothing vs + then throw $ VersionSourceMissing result + else pure $ maximum $ catMaybes vs + +persistMigrationVersionImpl :: (Member (Embed IO) r, Member TinyLog r, Member (Error MigrationException) r) => ES.BHEnv -> MigrationVersion -> Sem r () +persistMigrationVersionImpl env v = do + let docId = ES.DocId . Text.pack . show $ migrationVersion v + persistResponse <- ES.runBH env $ ES.indexDocument migrationIndexName migrationMappingName ES.defaultIndexDocumentSettings v docId + if ES.isCreated persistResponse + then do + Log.info $ + Log.msg (Log.val "Migration success recorded") + . Log.field "migrationVersion" v + else throw $ PersistVersionFailed v $ show persistResponse + +migrationIndexName :: ES.IndexName +migrationIndexName = ES.IndexName "wire_brig_migrations" + +migrationMappingName :: ES.MappingName +migrationMappingName = ES.MappingName "wire_brig_migrations" + +migrationIndexMapping :: Value +migrationIndexMapping = + object + [ "properties" + .= object + ["migration_version" .= object ["index" .= True, "type" .= ("integer" :: Text)]] + ] diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Metrics.hs b/libs/wire-subsystems/src/Wire/UserSearch/Metrics.hs new file mode 100644 index 00000000000..656186a5f18 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserSearch/Metrics.hs @@ -0,0 +1,44 @@ +module Wire.UserSearch.Metrics where + +import Imports +import Prometheus qualified as Prom + +{-# NOINLINE indexUpdateCounter #-} +indexUpdateCounter :: Prom.Counter +indexUpdateCounter = + Prom.unsafeRegister $ + Prom.counter + Prom.Info + { Prom.metricName = "user_index_update_count", + Prom.metricHelp = "Number of updates on user index" + } + +{-# NOINLINE indexUpdateErrorCounter #-} +indexUpdateErrorCounter :: Prom.Counter +indexUpdateErrorCounter = + Prom.unsafeRegister $ + Prom.counter + Prom.Info + { Prom.metricName = "user_index_update_err", + Prom.metricHelp = "Number of errors during user index update" + } + +{-# NOINLINE indexUpdateSuccessCounter #-} +indexUpdateSuccessCounter :: Prom.Counter +indexUpdateSuccessCounter = + Prom.unsafeRegister $ + Prom.counter + Prom.Info + { Prom.metricName = "user_index_update_ok", + Prom.metricHelp = "Number of successful user index updates" + } + +{-# NOINLINE indexDeleteCounter #-} +indexDeleteCounter :: Prom.Counter +indexDeleteCounter = + Prom.unsafeRegister $ + Prom.counter + Prom.Info + { Prom.metricName = "user_index_delete_count", + Prom.metricHelp = "Number of deletes on user index" + } diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs b/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs new file mode 100644 index 00000000000..da343e721b1 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs @@ -0,0 +1,30 @@ +module Wire.UserSearch.Migration where + +import Data.Aeson +import Database.Bloodhound.Types qualified as ES +import Imports +import Numeric.Natural +import System.Logger.Class (ToBytes (..)) + +newtype MigrationVersion = MigrationVersion {migrationVersion :: Natural} + deriving (Show, Eq, Ord) + +instance ToJSON MigrationVersion where + toJSON (MigrationVersion v) = object ["migration_version" .= v] + +instance FromJSON MigrationVersion where + parseJSON = withObject "MigrationVersion" $ \o -> MigrationVersion <$> o .: "migration_version" + +instance ToBytes MigrationVersion where + bytes = bytes . toInteger . migrationVersion + +data MigrationException + = CreateMigrationIndexFailed String + | FetchMigrationVersionsFailed String + | PersistVersionFailed MigrationVersion String + | PutMappingFailed String + | TargetIndexAbsent + | VersionSourceMissing (ES.SearchResult MigrationVersion) + deriving (Show) + +instance Exception MigrationException diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs new file mode 100644 index 00000000000..fc4d15e434e --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs @@ -0,0 +1,207 @@ +{-# LANGUAGE RecordWildCards #-} + +module Wire.UserSearch.Types where + +import Cassandra qualified as C +import Cassandra.Util +import Data.Aeson +import Data.Attoparsec.ByteString +import Data.ByteString.Builder +import Data.ByteString.Conversion +import Data.ByteString.Lazy +import Data.Handle +import Data.Id +import Data.Json.Util +import Data.Text.Encoding +import Database.Bloodhound.Types +import Imports +import Test.QuickCheck +import Wire.API.Team.Feature +import Wire.API.Team.Role +import Wire.API.User +import Wire.API.User.Search +import Wire.Arbitrary + +newtype IndexVersion = IndexVersion {docVersion :: DocVersion} + +mkIndexVersion :: [Maybe (Writetime x)] -> IndexVersion +mkIndexVersion writetimes = + let maxVersion = getMax . mconcat . fmap (Max . writetimeToInt64) $ catMaybes writetimes + in -- This minBound case would only get triggered when the maxVersion is <= 0 + -- or >= 9.2e+18. First case can happen when the writetimes list is empty + -- or contains a timestamp before the unix epoch, which is unlikely. + -- Second case will happen in a few billion years. It is also not really a + -- restriction in ES, Bloodhound's authors' interpretation of the the ES + -- documentation caused this limiation, otherwise `maxBound :: Int64`, + -- would be acceptable by ES. + IndexVersion . fromMaybe minBound . mkDocVersion . fromIntegral $ maxVersion + +-- | Represents an ES *document*, ie. the subset of user attributes stored in ES. +-- See also 'IndexUser'. +-- +-- If a user is not searchable, e.g. because the account got +-- suspended, all fields except for the user id are set to 'Nothing' and +-- consequently removed from the index. +data UserDoc = UserDoc + { udId :: UserId, + udTeam :: Maybe TeamId, + udName :: Maybe Name, + udNormalized :: Maybe Text, + udHandle :: Maybe Handle, + udEmail :: Maybe EmailAddress, + udColourId :: Maybe ColourId, + udAccountStatus :: Maybe AccountStatus, + udSAMLIdP :: Maybe Text, + udManagedBy :: Maybe ManagedBy, + udCreatedAt :: Maybe UTCTimeMillis, + udRole :: Maybe Role, + udSearchVisibilityInbound :: Maybe SearchVisibilityInbound, + udScimExternalId :: Maybe Text, + udSso :: Maybe Sso, + udEmailUnvalidated :: Maybe EmailAddress + } + deriving (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform UserDoc) + +instance ToJSON UserDoc where + toJSON ud = + object + [ "id" .= udId ud, + "team" .= udTeam ud, + "name" .= udName ud, + "normalized" .= udNormalized ud, + "handle" .= udHandle ud, + "email" .= udEmail ud, + "accent_id" .= udColourId ud, + "account_status" .= udAccountStatus ud, + "saml_idp" .= udSAMLIdP ud, + "managed_by" .= udManagedBy ud, + "created_at" .= udCreatedAt ud, + "role" .= udRole ud, + searchVisibilityInboundFieldName .= udSearchVisibilityInbound ud, + "scim_external_id" .= udScimExternalId ud, + "sso" .= udSso ud, + "email_unvalidated" .= udEmailUnvalidated ud + ] + +instance FromJSON UserDoc where + parseJSON = withObject "UserDoc" $ \o -> + UserDoc + <$> o .: "id" + <*> o .:? "team" + <*> o .:? "name" + <*> o .:? "normalized" + <*> o .:? "handle" + <*> o .:? "email" + <*> o .:? "accent_id" + <*> o .:? "account_status" + <*> o .:? "saml_idp" + <*> o .:? "managed_by" + <*> o .:? "created_at" + <*> o .:? "role" + <*> o .:? searchVisibilityInboundFieldName + <*> o .:? "scim_external_id" + <*> o .:? "sso" + <*> o .:? "email_unvalidated" + +searchVisibilityInboundFieldName :: Key +searchVisibilityInboundFieldName = "search_visibility_inbound" + +userDocToTeamContact :: UserDoc -> TeamContact +userDocToTeamContact UserDoc {..} = + TeamContact + { teamContactUserId = udId, + teamContactTeam = udTeam, + teamContactSso = udSso, + teamContactScimExternalId = udScimExternalId, + teamContactSAMLIdp = udSAMLIdP, + teamContactRole = udRole, + teamContactName = maybe "" fromName udName, + teamContactManagedBy = udManagedBy, + teamContactHandle = fromHandle <$> udHandle, + teamContactEmailUnvalidated = udEmailUnvalidated, + teamContactEmail = udEmail, + teamContactCreatedAt = udCreatedAt, + teamContactColorId = fromIntegral . fromColourId <$> udColourId + } + +-- | Outbound search restrictions configured by team admin of the searcher. This +-- value restricts the set of user that are searched. +-- +-- See 'optionallySearchWithinTeam' for the effect on full-text search. +-- +-- See 'mkTeamSearchInfo' for the business logic that defines the TeamSearchInfo +-- value. +-- +-- Search results might be affected by the inbound search restriction settings of +-- the searched user. ('SearchVisibilityInbound') +data TeamSearchInfo + = -- | Only users that are not part of any team are searched + NoTeam + | -- | Only users from the same team as the searcher are searched + TeamOnly TeamId + | -- | No search restrictions, all users are searched + AllUsers + +-- | Inbound search restrictions configured by team to-be-searched. Affects only +-- full-text search (i.e. search on the display name and the handle), not exact +-- handle search. +data SearchVisibilityInbound + = -- | The user can only be found by users from the same team + SearchableByOwnTeam + | -- | The user can by found by any user of any team + SearchableByAllTeams + deriving (Eq, Show) + +instance Arbitrary SearchVisibilityInbound where + arbitrary = elements [SearchableByOwnTeam, SearchableByAllTeams] + +instance ToByteString SearchVisibilityInbound where + builder SearchableByOwnTeam = "searchable-by-own-team" + builder SearchableByAllTeams = "searchable-by-all-teams" + +instance FromByteString SearchVisibilityInbound where + parser = + SearchableByOwnTeam + <$ string "searchable-by-own-team" + <|> SearchableByAllTeams + <$ string "searchable-by-all-teams" + +instance C.Cql SearchVisibilityInbound where + ctype = C.Tagged C.IntColumn + + toCql SearchableByOwnTeam = C.CqlInt 0 + toCql SearchableByAllTeams = C.CqlInt 1 + + fromCql (C.CqlInt 0) = pure SearchableByOwnTeam + fromCql (C.CqlInt 1) = pure SearchableByAllTeams + fromCql n = Left $ "Unexpected SearchVisibilityInbound: " ++ show n + +defaultSearchVisibilityInbound :: SearchVisibilityInbound +defaultSearchVisibilityInbound = SearchableByOwnTeam + +searchVisibilityInboundFromFeatureStatus :: FeatureStatus -> SearchVisibilityInbound +searchVisibilityInboundFromFeatureStatus FeatureStatusDisabled = SearchableByOwnTeam +searchVisibilityInboundFromFeatureStatus FeatureStatusEnabled = SearchableByAllTeams + +instance ToJSON SearchVisibilityInbound where + toJSON = String . decodeUtf8 . toStrict . toLazyByteString . builder + +instance FromJSON SearchVisibilityInbound where + parseJSON = withText "SearchVisibilityInbound" $ \str -> + case runParser (parser @SearchVisibilityInbound) (encodeUtf8 str) of + Left err -> fail err + Right result -> pure result + +data IndexQuery r = IndexQuery Query Filter [DefaultSort] + +data BrowseTeamFilters = BrowseTeamFilters + { teamId :: TeamId, + mQuery :: Maybe Text, + mRoleFilter :: Maybe RoleFilter, + mSortBy :: Maybe TeamUserSearchSortBy, + mSortOrder :: Maybe TeamUserSearchSortOrder + } + +userIdToDocId :: UserId -> DocId +userIdToDocId uid = DocId (idToText uid) diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index 6429d60c597..1c33abd7e42 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -1,8 +1,8 @@ {-# LANGUAGE TemplateHaskell #-} -{-# OPTIONS_GHC -Wno-ambiguous-fields #-} module Wire.UserStore where +import Cassandra (PageWithState (..), PagingState) import Data.Default import Data.Handle import Data.Id @@ -12,6 +12,7 @@ import Polysemy.Error import Wire.API.User import Wire.Arbitrary import Wire.StoredUser +import Wire.UserStore.IndexUser -- | Update of any "simple" attributes (ones that do not involve locking, like handle, or -- validation protocols, like email). @@ -46,6 +47,8 @@ data StoredUserUpdateError = StoredUserUpdateHandleExists -- | Effect containing database logic around 'StoredUser'. (Example: claim handle lock is -- database logic; validate handle is application logic.) data UserStore m a where + GetIndexUser :: UserId -> UserStore m (Maybe IndexUser) + GetIndexUsersPaginated :: Int32 -> Maybe PagingState -> UserStore m (PageWithState IndexUser) GetUsers :: [UserId] -> UserStore m [StoredUser] UpdateUser :: UserId -> StoredUserUpdate -> UserStore m () UpdateUserHandleEither :: UserId -> StoredUserHandleUpdate -> UserStore m (Either StoredUserUpdateError ()) diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 9ff0e903abf..ee7f51bab8c 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -1,6 +1,7 @@ module Wire.UserStore.Cassandra (interpretUserStoreCassandra) where import Cassandra +import Cassandra.Exec (prepared) import Data.Handle import Data.Id import Database.CQL.Protocol @@ -11,27 +12,64 @@ import Polysemy.Error import Wire.API.User hiding (DeleteUser) import Wire.StoredUser import Wire.UserStore +import Wire.UserStore.IndexUser hiding (userId) import Wire.UserStore.Unique interpretUserStoreCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor UserStore r interpretUserStoreCassandra casClient = interpret $ - runEmbedded (runClient casClient) . \case - GetUsers uids -> embed $ getUsersImpl uids - UpdateUser uid update -> embed $ updateUserImpl uid update - UpdateUserHandleEither uid update -> embed $ updateUserHandleEitherImpl uid update - DeleteUser user -> embed $ deleteUserImpl user - LookupHandle hdl -> embed $ lookupHandleImpl LocalQuorum hdl - GlimpseHandle hdl -> embed $ lookupHandleImpl One hdl - LookupStatus uid -> embed $ lookupStatusImpl uid - IsActivated uid -> embed $ isActivatedImpl uid - LookupLocale uid -> embed $ lookupLocaleImpl uid + runEmbedded (runClient casClient) . embed . \case + GetUsers uids -> getUsersImpl uids + GetIndexUser uid -> getIndexUserImpl uid + GetIndexUsersPaginated pageSize mPagingState -> getIndexUserPaginatedImpl pageSize mPagingState + UpdateUser uid update -> updateUserImpl uid update + UpdateUserHandleEither uid update -> updateUserHandleEitherImpl uid update + DeleteUser user -> deleteUserImpl user + LookupHandle hdl -> lookupHandleImpl LocalQuorum hdl + GlimpseHandle hdl -> lookupHandleImpl One hdl + LookupStatus uid -> lookupStatusImpl uid + IsActivated uid -> isActivatedImpl uid + LookupLocale uid -> lookupLocaleImpl uid getUsersImpl :: [UserId] -> Client [StoredUser] getUsersImpl usrs = map asRecord <$> retry x1 (query selectUsers (params LocalQuorum (Identity usrs))) +getIndexUserImpl :: UserId -> Client (Maybe IndexUser) +getIndexUserImpl u = do + mIndexUserTuple <- retry x1 $ query1 cql (params LocalQuorum (Identity u)) + pure $ asRecord <$> mIndexUserTuple + where + cql :: PrepQuery R (Identity UserId) (TupleType IndexUser) + cql = prepared . QueryString $ getIndexUserBaseQuery <> " WHERE id = ?" + +getIndexUserPaginatedImpl :: Int32 -> Maybe PagingState -> Client (PageWithState IndexUser) +getIndexUserPaginatedImpl pageSize mPagingState = + asRecord <$$> paginateWithState cql (paramsPagingState LocalQuorum () pageSize mPagingState) + where + cql :: PrepQuery R () (TupleType IndexUser) + cql = prepared $ QueryString getIndexUserBaseQuery + +getIndexUserBaseQuery :: LText +getIndexUserBaseQuery = + [sql| + SELECT + id, + team, writetime(team), + name, writetime(name), + status, writetime(status), + handle, writetime(handle), + email, writetime(email), + accent_id, writetime(accent_id), + activated, writetime(activated), + service, writetime(service), + managed_by, writetime(managed_by), + sso_id, writetime(sso_id), + email_unvalidated, writetime(email_unvalidated) + FROM user + |] + updateUserImpl :: UserId -> StoredUserUpdate -> Client () updateUserImpl uid update = retry x5 $ batch do diff --git a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs new file mode 100644 index 00000000000..2334260f447 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs @@ -0,0 +1,200 @@ +{-# LANGUAGE RecordWildCards #-} + +module Wire.UserStore.IndexUser where + +import Cassandra.Util +import Data.ByteString.Builder +import Data.ByteString.Lazy qualified as LBS +import Data.Handle +import Data.Id +import Data.Json.Util +import Data.Text.Encoding qualified as Text +import Data.Text.Encoding.Error qualified as Text +import Data.Text.ICU.Translit +import Database.CQL.Protocol +import Imports +import SAML2.WebSSO qualified as SAML +import URI.ByteString +import Wire.API.User hiding (userId) +import Wire.API.User.Search +import Wire.UserSearch.Types + +type Activated = Bool + +data WithWritetime a = WithWriteTime {value :: a, writetime :: Writetime a} + +data IndexUser = IndexUser + { userId :: UserId, + teamId :: Maybe (WithWritetime TeamId), + name :: WithWritetime Name, + accountStatus :: Maybe (WithWritetime AccountStatus), + handle :: Maybe (WithWritetime Handle), + email :: Maybe (WithWritetime EmailAddress), + colourId :: WithWritetime ColourId, + activated :: WithWritetime Activated, + serviceId :: Maybe (WithWritetime ServiceId), + managedBy :: Maybe (WithWritetime ManagedBy), + ssoId :: Maybe (WithWritetime UserSSOId), + unverifiedEmail :: Maybe (WithWritetime EmailAddress) + } + +{- ORMOLU_DISABLE -} +type instance + TupleType IndexUser = + ( UserId, + Maybe TeamId, Maybe (Writetime TeamId), + Name, Writetime Name, + Maybe AccountStatus, Maybe (Writetime AccountStatus), + Maybe Handle, Maybe (Writetime Handle), + Maybe EmailAddress, Maybe (Writetime EmailAddress), + ColourId, Writetime ColourId, + Activated, Writetime Activated, + Maybe ServiceId, Maybe (Writetime ServiceId), + Maybe ManagedBy, Maybe (Writetime ManagedBy), + Maybe UserSSOId, Maybe (Writetime UserSSOId), + Maybe EmailAddress, Maybe (Writetime EmailAddress) + ) + +instance Record IndexUser where + asTuple (IndexUser {..}) = + ( userId, + value <$> teamId, writetime <$> teamId, + name.value, name.writetime, + value <$> accountStatus, writetime <$> accountStatus, + value <$> handle, writetime <$> handle, + value <$> email, writetime <$> email, + colourId.value, colourId.writetime, + activated.value, activated.writetime, + value <$> serviceId, writetime <$> serviceId, + value <$> managedBy, writetime <$> managedBy, + value <$> ssoId, writetime <$> ssoId, + value <$> unverifiedEmail, writetime <$> unverifiedEmail + ) + + asRecord + ( u, + mTeam, tTeam, + name, tName, + status, tStatus, + handle, tHandle, + email, tEmail, + colour, tColour, + activated, tActivated, + service, tService, + managedBy, tManagedBy, + ssoId, tSsoId, + emailUnvalidated, tEmailUnvalidated + ) = IndexUser { + userId = u, + teamId = WithWriteTime <$> mTeam <*> tTeam, + name = WithWriteTime name tName, + accountStatus = WithWriteTime <$> status <*> tStatus, + handle = WithWriteTime <$> handle <*> tHandle, + email = WithWriteTime <$> email <*> tEmail, + colourId = WithWriteTime colour tColour, + activated = WithWriteTime activated tActivated, + serviceId = WithWriteTime <$> service <*> tService, + managedBy = WithWriteTime <$> managedBy <*> tManagedBy, + ssoId = WithWriteTime <$> ssoId <*> tSsoId, + unverifiedEmail = WithWriteTime <$> emailUnvalidated <*> tEmailUnvalidated + } +{- ORMOLU_ENABLE -} + +indexUserToVersion :: IndexUser -> IndexVersion +indexUserToVersion IndexUser {..} = + mkIndexVersion + [ const () <$$> Just name.writetime, + const () <$$> fmap writetime teamId, + const () <$$> fmap writetime accountStatus, + const () <$$> fmap writetime handle, + const () <$$> fmap writetime email, + const () <$$> Just colourId.writetime, + const () <$$> Just activated.writetime, + const () <$$> fmap writetime serviceId, + const () <$$> fmap writetime managedBy, + const () <$$> fmap writetime ssoId, + const () <$$> fmap writetime unverifiedEmail + ] + +indexUserToDoc :: SearchVisibilityInbound -> IndexUser -> UserDoc +indexUserToDoc searchVisInbound IndexUser {..} = + if shouldIndex + then + UserDoc + { udEmailUnvalidated = value <$> unverifiedEmail, + udSso = sso . value =<< ssoId, + udScimExternalId = join $ scimExternalId <$> (value <$> managedBy) <*> (value <$> ssoId), + udSearchVisibilityInbound = Just searchVisInbound, + -- FUTUREWORK: This is a bug: https://wearezeta.atlassian.net/browse/WPB-11124 + udRole = Nothing, + udCreatedAt = Just . toUTCTimeMillis $ writetimeToUTC activated.writetime, + udManagedBy = value <$> managedBy, + udSAMLIdP = idpUrl . value =<< ssoId, + udAccountStatus = value <$> accountStatus, + udColourId = Just colourId.value, + udEmail = value <$> email, + udHandle = value <$> handle, + udNormalized = Just $ normalized name.value.fromName, + udName = Just name.value, + udTeam = value <$> teamId, + udId = userId + } + else -- We insert a tombstone-style user here, as it's easier than + -- deleting the old one. It's mostly empty, but having the status here + -- might be useful in the future. + emptyUserDoc userId + where + shouldIndex = + ( case value <$> accountStatus of + Nothing -> True + Just Active -> True + Just Suspended -> True + Just Deleted -> False + Just Ephemeral -> False + Just PendingInvitation -> False + ) + && activated.value -- FUTUREWORK: how is this adding to the first case? + && isNothing serviceId + + idpUrl :: UserSSOId -> Maybe Text + idpUrl (UserSSOId (SAML.UserRef (SAML.Issuer uri) _subject)) = + Just $ fromUri uri + idpUrl (UserScimExternalId _) = Nothing + + fromUri :: URI -> Text + fromUri = + Text.decodeUtf8With Text.lenientDecode + . LBS.toStrict + . toLazyByteString + . serializeURIRef + + sso :: UserSSOId -> Maybe Sso + sso userSsoId = do + (issuer, nameid) <- ssoIssuerAndNameId userSsoId + pure $ Sso {ssoIssuer = issuer, ssoNameId = nameid} + +-- Transliteration could also be done by ElasticSearch (ICU plugin), but this would +-- require a data migration. +normalized :: Text -> Text +normalized = transliterate (trans "Any-Latin; Latin-ASCII; Lower") + +emptyUserDoc :: UserId -> UserDoc +emptyUserDoc uid = + UserDoc + { udEmailUnvalidated = Nothing, + udSso = Nothing, + udScimExternalId = Nothing, + udSearchVisibilityInbound = Nothing, + udRole = Nothing, + udCreatedAt = Nothing, + udManagedBy = Nothing, + udSAMLIdP = Nothing, + udAccountStatus = Nothing, + udColourId = Nothing, + udEmail = Nothing, + udHandle = Nothing, + udNormalized = Nothing, + udName = Nothing, + udTeam = Nothing, + udId = uid + } diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index b8c6256122f..95cfcc4ad6e 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -8,16 +8,29 @@ module Wire.UserSubsystem where import Data.Default +import Data.Domain import Data.Handle (Handle) import Data.HavePendingInvitations import Data.Id import Data.Qualified +import Data.Range +import Data.Set qualified as Set import Imports import Polysemy +import Polysemy.Error import Wire.API.Federation.Error +import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus) +import Wire.API.Team.Feature +import Wire.API.Team.Member (IsPerm (..), TeamMember) +import Wire.API.Team.Permission import Wire.API.User +import Wire.API.User.Search import Wire.Arbitrary +import Wire.GalleyAPIAccess (GalleyAPIAccess) +import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.UserKeyStore (EmailKey, emailKeyOrig) +import Wire.UserSearch.Types +import Wire.UserSubsystem.Error (UserSubsystemError (..)) -- | 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.) @@ -111,6 +124,22 @@ data UserSubsystem m a where BlockListDelete :: EmailAddress -> UserSubsystem m () -- | Add an email to the block list. BlockListInsert :: EmailAddress -> UserSubsystem m () + UpdateTeamSearchVisibilityInbound :: TeamStatus SearchVisibilityInboundConfig -> UserSubsystem m () + SearchUsers :: + Local UserId -> + Text -> + Maybe Domain -> + Maybe (Range 1 500 Int32) -> + UserSubsystem m (SearchResult Contact) + BrowseTeam :: + UserId -> + BrowseTeamFilters -> + Maybe (Range 1 500 Int) -> + Maybe PagingState -> + UserSubsystem m (SearchResult TeamContact) + -- | This function exists to support migration in this susbystem, after the + -- migration this would just be an internal detail of the subsystem + InternalUpdateSearchIndex :: UserId -> UserSubsystem m () -- | the return type of 'CheckHandle' data CheckHandleResp @@ -132,6 +161,9 @@ getLocalUserProfile :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe getLocalUserProfile targetUser = listToMaybe <$> getLocalUserProfiles ((: []) <$> targetUser) +getLocalUser :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe User) +getLocalUser = (selfUser <$$>) . getSelfProfile + getLocalAccountBy :: (Member UserSubsystem r) => HavePendingInvitations -> @@ -150,3 +182,47 @@ getLocalAccountBy includePendingInvitations uid = getLocalUserAccountByUserKey :: (Member UserSubsystem r) => Local EmailKey -> Sem r (Maybe UserAccount) getLocalUserAccountByUserKey q@(tUnqualified -> ek) = listToMaybe . fmap (.account) <$> getExtendedAccountsByEmailNoFilter (qualifyAs q [emailKeyOrig ek]) + +------------------------------------------ +-- FUTUREWORK: Pending functions for a team subsystem +------------------------------------------ + +ensurePermissions :: + ( IsPerm perm, + Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r + ) => + UserId -> + TeamId -> + [perm] -> + Sem r () +ensurePermissions u t perms = do + m <- GalleyAPIAccess.getTeamMember u t + unless (check m) $ + throw UserSubsystemInsufficientTeamPermissions + where + check :: Maybe TeamMember -> Bool + check (Just m) = all (hasPermission m) perms + check Nothing = False + +-- | Privilege escalation detection (make sure no `RoleMember` user creates a `RoleOwner`). +-- +-- There is some code duplication with 'Galley.API.Teams.ensureNotElevated'. +ensurePermissionToAddUser :: + ( Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r + ) => + UserId -> + TeamId -> + Permissions -> + Sem r () +ensurePermissionToAddUser u t inviteePerms = do + minviter <- GalleyAPIAccess.getTeamMember u t + unless (check minviter) $ + throw UserSubsystemInsufficientTeamPermissions + where + check :: Maybe TeamMember -> Bool + check (Just inviter) = + hasPermission inviter AddTeamMember + && all (mayGrantPermission inviter) (Set.toList (inviteePerms.self)) + check Nothing = False diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs index 40006412b47..22b1a8e44ec 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs @@ -16,6 +16,7 @@ data UserSubsystemError | UserSubsystemHandleExists | UserSubsystemInvalidHandle | UserSubsystemProfileNotFound + | UserSubsystemInsufficientTeamPermissions deriving (Eq, Show) userSubsystemErrorToHttpError :: UserSubsystemError -> HttpError @@ -28,5 +29,6 @@ userSubsystemErrorToHttpError = UserSubsystemHandleExists -> errorToWai @E.HandleExists UserSubsystemInvalidHandle -> errorToWai @E.InvalidHandle UserSubsystemHandleManagedByScim -> errorToWai @E.HandleManagedByScim + UserSubsystemInsufficientTeamPermissions -> errorToWai @'E.InsufficientTeamPermissions instance Exception UserSubsystemError diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index d91c6c33dd0..769be28ee74 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -1,4 +1,5 @@ {-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} module Wire.UserSubsystem.Interpreter ( runUserSubsystem, @@ -8,6 +9,7 @@ where import Control.Lens (view) import Control.Monad.Trans.Maybe +import Data.Domain import Data.Handle (Handle) import Data.Handle qualified as Handle import Data.Id @@ -15,32 +17,52 @@ import Data.Json.Util import Data.LegalHold import Data.List.Extra (nubOrd) import Data.Qualified +import Data.Range import Data.Time.Clock +import Database.Bloodhound qualified as ES import Imports import Polysemy -import Polysemy.Error hiding (try) +import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog (TinyLog) +import Polysemy.TinyLog qualified as Log import Servant.Client.Core +import System.Logger.Message qualified as Log import Wire.API.Federation.API +import Wire.API.Federation.API.Brig qualified as FedBrig import Wire.API.Federation.Error +import Wire.API.Routes.FederationDomainConfig +import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus (..)) import Wire.API.Team.Feature -import Wire.API.Team.Member hiding (userId) -import Wire.API.User +import Wire.API.Team.Member +import Wire.API.Team.Permission qualified as Permission +import Wire.API.Team.SearchVisibility +import Wire.API.User as User +import Wire.API.User.Search import Wire.API.UserEvent import Wire.Arbitrary import Wire.BlockListStore as BlockList import Wire.DeleteQueue import Wire.Events import Wire.FederationAPIAccess +import Wire.FederationConfigStore import Wire.GalleyAPIAccess +import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.IndexedUserStore (IndexedUserStore) +import Wire.IndexedUserStore qualified as IndexedUserStore +import Wire.IndexedUserStore.Bulk.ElasticSearch (teamSearchVisibilityInbound) import Wire.InvitationCodeStore (InvitationCodeStore, lookupInvitationByEmail) import Wire.Sem.Concurrency +import Wire.Sem.Metrics +import Wire.Sem.Metrics qualified as Metrics import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredUser import Wire.UserKeyStore +import Wire.UserSearch.Metrics +import Wire.UserSearch.Types import Wire.UserStore as UserStore +import Wire.UserStore.IndexUser import Wire.UserSubsystem import Wire.UserSubsystem.Error import Wire.UserSubsystem.HandleBlacklist @@ -48,12 +70,11 @@ import Witherable (wither) data UserSubsystemConfig = UserSubsystemConfig { emailVisibilityConfig :: EmailVisibilityConfig, - defaultLocale :: Locale + defaultLocale :: Locale, + searchSameTeamOnly :: Bool } - deriving (Show) - -instance Arbitrary UserSubsystemConfig where - arbitrary = UserSubsystemConfig <$> arbitrary <*> arbitrary + deriving (Show, Generic) + deriving (Arbitrary) via (GenericUniform UserSubsystemConfig) runUserSubsystem :: ( Member GalleyAPIAccess r, @@ -70,6 +91,9 @@ runUserSubsystem :: RunClient (fedM 'Brig), FederationMonad fedM, Typeable fedM, + Member IndexedUserStore r, + Member FederationConfigStore r, + Member Metrics r, Member (TinyLog) r, Member InvitationCodeStore r ) => @@ -78,9 +102,9 @@ runUserSubsystem :: runUserSubsystem cfg = runInputConst cfg . interpretUserSubsystem . raiseUnder interpretUserSubsystem :: - ( Member GalleyAPIAccess r, - Member UserStore r, + ( Member UserStore r, Member UserKeyStore r, + Member GalleyAPIAccess r, Member BlockListStore r, Member (Concurrency 'Unsafe) r, Member (Error FederationError) r, @@ -93,6 +117,9 @@ interpretUserSubsystem :: RunClient (fedM 'Brig), FederationMonad fedM, Typeable fedM, + Member IndexedUserStore r, + Member FederationConfigStore r, + Member Metrics r, Member InvitationCodeStore r, Member TinyLog r ) => @@ -113,6 +140,14 @@ interpretUserSubsystem = interpret \case IsBlocked email -> isBlockedImpl email BlockListDelete email -> blockListDeleteImpl email BlockListInsert email -> blockListInsertImpl email + UpdateTeamSearchVisibilityInbound status -> + updateTeamSearchVisibilityInboundImpl status + SearchUsers luid query mDomain mMaxResults -> + searchUsersImpl luid query mDomain mMaxResults + BrowseTeam uid browseTeamFilters mMaxResults mPagingState -> + browseTeamImpl uid browseTeamFilters mMaxResults mPagingState + InternalUpdateSearchIndex uid -> + syncUserIndex uid isBlockedImpl :: (Member BlockListStore r) => EmailAddress -> Sem r Bool isBlockedImpl = BlockList.exists . mkEmailKey @@ -340,10 +375,10 @@ getUserProfilesWithErrorsImpl self others = do (outp -> inp -> outp) aggregate acc [] = acc aggregate (accL, accR) (Right prof : buckets) = aggregate (accL, prof <> accR) buckets - aggregate (accL, accR) (Left err : buckets) = aggregate (renderBucketError err <> accL, accR) buckets + aggregate (accL, accR) (Left e : buckets) = aggregate (renderBucketError e <> accL, accR) buckets renderBucketError :: (FederationError, Qualified [UserId]) -> [(Qualified UserId, FederationError)] - renderBucketError (err, qlist) = (,err) . (flip Qualified (qDomain qlist)) <$> qUnqualified qlist + renderBucketError (e, qlist) = (,e) . (flip Qualified (qDomain qlist)) <$> qUnqualified qlist -- | Some fields cannot be overwritten by clients for scim-managed users; some others if e2eid -- is used. If a client attempts to overwrite any of these, throw `UserSubsystem*ManagedByScim`. @@ -385,7 +420,9 @@ updateUserProfileImpl :: ( Member UserStore r, Member (Error UserSubsystemError) r, Member Events r, - Member GalleyAPIAccess r + Member GalleyAPIAccess r, + Member IndexedUserStore r, + Member Metrics r ) => Local UserId -> Maybe ConnId -> @@ -397,6 +434,8 @@ updateUserProfileImpl (tUnqualified -> uid) mconn updateOrigin update = do guardLockedFields user updateOrigin update mapError (\StoredUserUpdateHandleExists -> UserSubsystemHandleExists) $ updateUser uid (storedUserUpdate update) + let interestingToUpdateIndex = isJust update.name || isJust update.accentId + when interestingToUpdateIndex $ syncUserIndex uid generateUserEvent uid mconn (mkProfileUpdateEvent uid update) storedUserUpdate :: UserProfileUpdate -> StoredUserUpdate @@ -435,7 +474,9 @@ updateHandleImpl :: ( Member (Error UserSubsystemError) r, Member GalleyAPIAccess r, Member Events r, - Member UserStore r + Member UserStore r, + Member IndexedUserStore r, + Member Metrics r ) => Local UserId -> Maybe ConnId -> @@ -452,6 +493,7 @@ updateHandleImpl (tUnqualified -> uid) mconn updateOrigin uhandle = do throw UserSubsystemNoIdentity mapError (\StoredUserUpdateHandleExists -> UserSubsystemHandleExists) $ UserStore.updateUserHandle uid (MkStoredUserHandleUpdate user.handle newHandle) + syncUserIndex uid generateUserEvent uid mconn (mkProfileUpdateHandleEvent uid newHandle) checkHandleImpl :: (Member (Error UserSubsystemError) r, Member UserStore r) => Text -> Sem r CheckHandleResp @@ -493,6 +535,230 @@ checkHandlesImpl check num = reverse <$> collectFree [] check num Nothing -> collectFree (h : free) hs (n - 1) Just _ -> collectFree free hs n +------------------------------------------------------------------------------- +-- Search + +syncUserIndex :: + forall r. + ( Member UserStore r, + Member GalleyAPIAccess r, + Member IndexedUserStore r, + Member Metrics r + ) => + UserId -> + Sem r () +syncUserIndex uid = + getIndexUser uid + >>= maybe deleteFromIndex upsert + where + deleteFromIndex :: Sem r () + deleteFromIndex = do + Metrics.incCounter indexDeleteCounter + IndexedUserStore.upsert (userIdToDocId uid) (emptyUserDoc uid) ES.NoVersionControl + + upsert :: IndexUser -> Sem r () + upsert indexUser = do + vis <- + maybe + (pure defaultSearchVisibilityInbound) + (teamSearchVisibilityInbound . value) + indexUser.teamId + let userDoc = indexUserToDoc vis indexUser + version = ES.ExternalGT . ES.ExternalDocVersion . docVersion $ indexUserToVersion indexUser + Metrics.incCounter indexUpdateCounter + IndexedUserStore.upsert (userIdToDocId uid) userDoc version + +updateTeamSearchVisibilityInboundImpl :: (Member IndexedUserStore r) => TeamStatus SearchVisibilityInboundConfig -> Sem r () +updateTeamSearchVisibilityInboundImpl teamStatus = + IndexedUserStore.updateTeamSearchVisibilityInbound teamStatus.team $ + searchVisibilityInboundFromFeatureStatus teamStatus.status + +searchUsersImpl :: + forall r fedM. + ( Member UserStore r, + Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r, + Member IndexedUserStore r, + Member FederationConfigStore r, + RunClient (fedM 'Brig), + Member (FederationAPIAccess fedM) r, + FederationMonad fedM, + Typeable fedM, + Member TinyLog r, + Member (Error FederationError) r, + Member (Input UserSubsystemConfig) r + ) => + Local UserId -> + Text -> + Maybe Domain -> + Maybe (Range 1 500 Int32) -> + Sem r (SearchResult Contact) +searchUsersImpl searcherId searchTerm maybeDomain maybeMaxResults = do + let searcher = tUnqualified searcherId + mSearcherTeamId <- + UserStore.getUser searcher >>= \mTeam -> pure (mTeam >>= (.teamId)) + + for_ mSearcherTeamId $ \tid -> + ensurePermissions searcher tid [SearchContacts] + let qDomain = Qualified () (fromMaybe (tDomain searcherId) maybeDomain) + foldQualified + searcherId + (\_ -> searchLocally ((,mSearcherTeamId) <$> searcherId) searchTerm maybeMaxResults) + (\rdom -> searchRemotely rdom mSearcherTeamId searchTerm) + qDomain + +searchLocally :: + forall r. + ( Member GalleyAPIAccess r, + Member UserStore r, + Member IndexedUserStore r, + Member (Input UserSubsystemConfig) r + ) => + Local (UserId, Maybe TeamId) -> + Text -> + Maybe (Range 1 500 Int32) -> + Sem r (SearchResult Contact) +searchLocally searcher searchTerm maybeMaxResults = do + let maxResults = maybe 15 (fromIntegral . fromRange) maybeMaxResults + let (searcherId, searcherTeamId) = (fst <$> searcher, snd <$> searcher) + teamSearchInfo <- mkTeamSearchInfo (tUnqualified searcherTeamId) + + maybeExactHandleMatch <- exactHandleSearch teamSearchInfo + + let exactHandleMatchCount = length maybeExactHandleMatch + esMaxResults = maxResults - exactHandleMatchCount + + esResult <- + if esMaxResults > 0 + then + IndexedUserStore.searchUsers + (tUnqualified searcherId) + (tUnqualified searcherTeamId) + teamSearchInfo + searchTerm + esMaxResults + else pure $ SearchResult 0 0 0 [] FullSearch Nothing Nothing + + -- Prepend results matching exact handle and results from ES. + pure $ + esResult + { searchResults = maybeToList maybeExactHandleMatch <> map userDocToContact (searchResults esResult), + searchFound = exactHandleMatchCount + searchFound esResult, + searchReturned = exactHandleMatchCount + searchReturned esResult + } + where + handleTeamVisibility :: TeamId -> TeamSearchVisibility -> TeamSearchInfo + handleTeamVisibility _ SearchVisibilityStandard = AllUsers + handleTeamVisibility t SearchVisibilityNoNameOutsideTeam = TeamOnly t + + userDocToContact :: UserDoc -> Contact + userDocToContact userDoc = + Contact + { contactQualifiedId = tUntagged $ qualifyAs searcher userDoc.udId, + contactName = maybe "" fromName userDoc.udName, + contactColorId = fromIntegral . fromColourId <$> userDoc.udColourId, + contactHandle = Handle.fromHandle <$> userDoc.udHandle, + contactTeam = userDoc.udTeam + } + + mkTeamSearchInfo :: Maybe TeamId -> Sem r TeamSearchInfo + mkTeamSearchInfo searcherTeamId = do + config <- input + case searcherTeamId of + Nothing -> pure NoTeam + Just t -> + -- This flag in brig overrules any flag on galley - it is system wide + if config.searchSameTeamOnly + then pure (TeamOnly t) + else do + -- For team users, we need to check the visibility flag + handleTeamVisibility t <$> GalleyAPIAccess.getTeamSearchVisibility t + + exactHandleSearch :: TeamSearchInfo -> Sem r (Maybe Contact) + exactHandleSearch _teamSerachInfo = runMaybeT $ do + handle <- MaybeT . pure $ Handle.parseHandle searchTerm + owner <- MaybeT $ UserStore.lookupHandle handle + storedUser <- MaybeT $ UserStore.getUser owner + config <- lift input + let contact = contactFromStoredUser (tDomain searcher) storedUser + isContactVisible = + (config.searchSameTeamOnly && (snd . tUnqualified $ searcher) == storedUser.teamId) + || (not config.searchSameTeamOnly) + if isContactVisible + then pure contact + else MaybeT $ pure Nothing + + contactFromStoredUser :: Domain -> StoredUser -> Contact + contactFromStoredUser domain storedUser = + Contact + { contactQualifiedId = Qualified storedUser.id domain, + contactName = fromName storedUser.name, + contactHandle = Handle.fromHandle <$> storedUser.handle, + contactColorId = Just . fromIntegral . fromColourId $ storedUser.accentId, + contactTeam = storedUser.teamId + } + +searchRemotely :: + ( Member FederationConfigStore r, + RunClient (fedM 'Brig), + Member (FederationAPIAccess fedM) r, + FederationMonad fedM, + Typeable fedM, + Member TinyLog r, + Member (Error FederationError) r + ) => + Remote x -> + Maybe TeamId -> + Text -> + Sem r (SearchResult Contact) +searchRemotely rDom mTid searchTerm = do + let domain = tDomain rDom + Log.info $ + Log.msg (Log.val "searchRemotely") + . Log.field "domain" (show domain) + . Log.field "searchTerm" searchTerm + mFedCnf <- getFederationConfig domain + let onlyInTeams = case restriction <$> mFedCnf of + Just FederationRestrictionAllowAll -> Nothing + Just (FederationRestrictionByTeam teams) -> Just teams + -- if we are not federating at all, we also do not allow to search any remote teams + Nothing -> Just [] + + searchResponse <- + runFederated rDom $ + fedClient @'Brig @"search-users" (FedBrig.SearchRequest searchTerm mTid onlyInTeams) + let contacts = searchResponse.contacts + let count = length contacts + pure + SearchResult + { searchResults = contacts, + searchFound = count, + searchReturned = count, + searchTook = 0, + searchPolicy = searchResponse.searchPolicy, + searchPagingState = Nothing, + searchHasMore = Nothing + } + +browseTeamImpl :: + ( Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r, + Member IndexedUserStore r + ) => + UserId -> + BrowseTeamFilters -> + Maybe (Range 1 500 Int) -> + Maybe PagingState -> + Sem r (SearchResult TeamContact) +browseTeamImpl uid filters mMaxResults mPagingState = do + -- limit this to team admins to reduce risk of involuntary DOS attacks. (also, + -- this way we don't need to worry about revealing confidential user data to + -- other team members.) + ensurePermissions uid filters.teamId [Permission.AddTeamMember] + + let maxResults = maybe 15 fromRange mMaxResults + userDocToTeamContact <$$> IndexedUserStore.paginateTeamMembers filters maxResults mPagingState + getAccountNoFilterImpl :: forall r. ( Member UserStore r, @@ -563,7 +829,7 @@ getExtendedAccountsByImpl (tSplit -> (domain, MkGetBy {includePendingInvitations -- validEmailIdentity, anyEmailIdentity? Just email -> do hasInvitation <- isJust <$> lookupInvitationByEmail email - gcHack hasInvitation (userId account.accountUser) + gcHack hasInvitation (User.userId account.accountUser) pure hasInvitation Nothing -> error "getExtendedAccountsByImpl: should never happen, user invited via scim always has an email" NoPendingInvitations -> pure False diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index d5d4a789261..415b0117d05 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -63,7 +63,9 @@ import Wire.DeleteQueue.InMemory import Wire.Events import Wire.FederationAPIAccess import Wire.FederationAPIAccess.Interpreter as FI +import Wire.FederationConfigStore import Wire.GalleyAPIAccess +import Wire.IndexedUserStore import Wire.InternalEvent hiding (DeleteUser) import Wire.InvitationCodeStore import Wire.MockInterpreters @@ -72,6 +74,8 @@ import Wire.MockInterpreters.InvitationCodeStore (inMemoryInvitationCodeStoreInt import Wire.PasswordResetCodeStore import Wire.Sem.Concurrency import Wire.Sem.Concurrency.Sequential +import Wire.Sem.Metrics +import Wire.Sem.Metrics.IO (ignoreMetrics) import Wire.Sem.Now hiding (get) import Wire.StoredUser import Wire.UserKeyStore @@ -134,6 +138,8 @@ type MiniBackendEffects = State [StoredUser], UserKeyStore, State (Map EmailKey UserId), + IndexedUserStore, + FederationConfigStore, DeleteQueue, Events, State [InternalNotification], @@ -142,6 +148,7 @@ type MiniBackendEffects = Now, Input UserSubsystemConfig, Input (Local ()), + Metrics, FederationAPIAccess MiniFederationMonad, TinyLog, Concurrency 'Unsafe @@ -376,6 +383,7 @@ interpretMaybeFederationStackState maybeFederationAPIAccess localBackend teamMem sequentiallyPerformConcurrency . noOpLogger . maybeFederationAPIAccess + . ignoreMetrics . runInputConst (toLocalUnsafe (Domain "localdomain") ()) . runInputConst cfg . interpretNowConst (UTCTime (ModifiedJulianDay 0) 0) @@ -384,6 +392,8 @@ interpretMaybeFederationStackState maybeFederationAPIAccess localBackend teamMem . evalState [] . miniEventInterpreter . inMemoryDeleteQueueInterpreter + . runFederationConfigStoreInMemory + . inMemoryIndexedUserStoreInterpreter . liftUserKeyStoreState . inMemoryUserKeyStoreInterpreter . liftUserStoreState diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs index ebd8d4d1ee5..e975ac6a06c 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs @@ -7,8 +7,10 @@ import Wire.MockInterpreters.BlockListStore as MockInterpreters import Wire.MockInterpreters.EmailSubsystem as MockInterpreters import Wire.MockInterpreters.Error as MockInterpreters import Wire.MockInterpreters.Events as MockInterpreters +import Wire.MockInterpreters.FederationConfigStore as MockInterpreters import Wire.MockInterpreters.GalleyAPIAccess as MockInterpreters import Wire.MockInterpreters.HashPassword as MockInterpreters +import Wire.MockInterpreters.IndexedUserStore as MockInterpreters import Wire.MockInterpreters.Now as MockInterpreters import Wire.MockInterpreters.PasswordResetCodeStore as MockInterpreters import Wire.MockInterpreters.PasswordStore as MockInterpreters diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/FederationConfigStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/FederationConfigStore.hs new file mode 100644 index 00000000000..57a9bf5566e --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/FederationConfigStore.hs @@ -0,0 +1,36 @@ +module Wire.MockInterpreters.FederationConfigStore where + +import Imports +import Polysemy +import Polysemy.State +import Wire.API.Routes.FederationDomainConfig +import Wire.FederationConfigStore + +inMemoryFederationConfigStoreInterpreter :: + (Member (State [FederationDomainConfig]) r) => + InterpreterFor FederationConfigStore r +inMemoryFederationConfigStoreInterpreter = + interpret $ \case + GetFederationConfig domain -> gets $ find (\cfg -> cfg.domain == domain) + GetFederationConfigs -> do + remoteConfigs <- get + pure $ FederationDomainConfigs AllowDynamic remoteConfigs 1 + AddFederationConfig newCfg -> do + modify $ (newCfg :) . deleteBy (\a b -> a.domain == b.domain) newCfg + pure AddFederationRemoteSuccess + UpdateFederationConfig _ -> + error "UpdateFederationConfig not implemented in inMemoryFederationConfigStoreInterpreter" + AddFederationRemoteTeam _ _ -> + error "AddFederationRemoteTeam not implemented in inMemoryFederationConfigStoreInterpreter" + RemoveFederationRemoteTeam _ _ -> + error "RemoveFederationRemoteTeam not implemented in inMemoryFederationConfigStoreInterpreter" + GetFederationRemoteTeams _ -> + error "GetFederationRemoteTeams not implemented in inMemoryFederationConfigStoreInterpreter" + BackendFederatesWith _ -> + error "BackendFederatesWith not implemented in inMemoryFederationConfigStoreInterpreter" + +runFederationConfigStoreInMemory :: InterpreterFor FederationConfigStore r +runFederationConfigStoreInMemory = + evalState [] + . inMemoryFederationConfigStoreInterpreter + . raiseUnder diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs index 1cfe41aeaf6..9f37e501b4a 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/GalleyAPIAccess.hs @@ -1,5 +1,7 @@ module Wire.MockInterpreters.GalleyAPIAccess where +import Data.Id +import Data.Proxy import Imports import Polysemy import Wire.API.Team.Feature @@ -16,4 +18,8 @@ miniGalleyAPIAccess :: miniGalleyAPIAccess member configs = interpret $ \case GetTeamMember _ _ -> pure member GetAllTeamFeaturesForUser _ -> pure configs + GetFeatureConfigForTeam tid -> pure $ getFeatureConfigForTeamImpl configs tid _ -> error "uninterpreted effect: GalleyAPIAccess" + +getFeatureConfigForTeamImpl :: forall feature. (IsFeatureConfig feature) => AllTeamFeatures -> TeamId -> LockableFeature feature +getFeatureConfigForTeamImpl allfeatures _ = npProject' (Proxy @(feature)) allfeatures diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs new file mode 100644 index 00000000000..60a186a6d8c --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs @@ -0,0 +1,15 @@ +module Wire.MockInterpreters.IndexedUserStore where + +import Imports +import Polysemy +import Wire.IndexedUserStore + +inMemoryIndexedUserStoreInterpreter :: InterpreterFor IndexedUserStore r +inMemoryIndexedUserStoreInterpreter = + interpret $ \case + Upsert {} -> pure () + UpdateTeamSearchVisibilityInbound {} -> pure () + BulkUpsert {} -> pure () + DoesIndexExist -> pure True + SearchUsers {} -> error "IndexedUserStore: unimplemented in memory interpreter" + PaginateTeamMembers {} -> error "IndexedUserStore: unimplemented in memory interpreter" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index bb3ad07afc6..a1e0e5d96e1 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -1,7 +1,10 @@ module Wire.MockInterpreters.UserStore where +import Cassandra.Util import Data.Handle import Data.Id +import Data.Time +import Data.Time.Calendar.OrdinalDate import Imports import Polysemy import Polysemy.Error @@ -10,6 +13,7 @@ import Wire.API.User hiding (DeleteUser) import Wire.API.User qualified as User import Wire.StoredUser import Wire.UserStore +import Wire.UserStore.IndexUser inMemoryUserStoreInterpreter :: forall r. @@ -31,6 +35,10 @@ inMemoryUserStoreInterpreter = interpret $ \case . maybe Imports.id setStoredUserSupportedProtocols update.supportedProtocols $ u else u + GetIndexUser uid -> + gets $ fmap storedUserToIndexUser . find (\user -> user.id == uid) + GetIndexUsersPaginated _pageSize _pagingState -> + error "GetIndexUsersPaginated not implemented in inMemoryUserStoreInterpreter" UpdateUserHandleEither uid hUpdate -> runError $ modifyLocalUsers (traverse doUpdate) where doUpdate :: StoredUser -> Sem (Error StoredUserUpdateError : r) StoredUser @@ -58,6 +66,26 @@ inMemoryUserStoreInterpreter = interpret $ \case IsActivated uid -> isActivatedImpl uid LookupLocale uid -> lookupLocaleImpl uid +storedUserToIndexUser :: StoredUser -> IndexUser +storedUserToIndexUser storedUser = + -- If we really care about this, we could start storing the writetimes, but we + -- don't need it right now + let withDefaultTime x = WithWriteTime x $ Writetime $ UTCTime (YearDay 0 1) 0 + in IndexUser + { userId = storedUser.id, + teamId = withDefaultTime <$> storedUser.teamId, + name = withDefaultTime storedUser.name, + accountStatus = withDefaultTime <$> storedUser.status, + handle = withDefaultTime <$> storedUser.handle, + email = withDefaultTime <$> storedUser.email, + colourId = withDefaultTime storedUser.accentId, + activated = withDefaultTime storedUser.activated, + serviceId = withDefaultTime <$> storedUser.serviceId, + managedBy = withDefaultTime <$> storedUser.managedBy, + ssoId = withDefaultTime <$> storedUser.ssoId, + unverifiedEmail = Nothing + } + lookupLocaleImpl :: (Member (State [StoredUser]) r) => UserId -> Sem r (Maybe ((Maybe Language, Maybe Country))) lookupLocaleImpl uid = do users <- get diff --git a/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs new file mode 100644 index 00000000000..5e82d9a569e --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs @@ -0,0 +1,54 @@ +module Wire.UserSearch.TypesSpec where + +import Control.Error (hush) +import Data.Aeson as Aeson +import Data.Fixed +import Data.Handle +import Data.Id +import Data.Json.Util +import Data.Time +import Data.Time.Clock.POSIX +import Imports +import Test.Hspec +import Test.Hspec.QuickCheck +import Test.QuickCheck +import Wire.API.Team.Role +import Wire.API.User +import Wire.UserSearch.Types + +spec :: Spec +spec = describe "UserDoc" $ do + describe "JSON" $ do + prop "roundrip to/fromJSON" $ \(userDoc :: UserDoc) -> + fromJSON (toJSON userDoc) === Aeson.Success userDoc + + it "should be backwards comptibile" $ do + eitherDecode (userDoc1ByteString) `shouldBe` Right userDoc1 + +mkTime :: Int -> UTCTime +mkTime = posixSecondsToUTCTime . secondsToNominalDiffTime . MkFixed . (* 1000000000) . fromIntegral + +userDoc1 :: UserDoc +userDoc1 = + UserDoc + { udId = fromJust . hush . parseIdFromText $ "0a96b396-57d6-11ea-a04b-7b93d1a5c19c", + udTeam = hush . parseIdFromText $ "17c59b18-57d6-11ea-9220-8bbf5eee961a", + udName = Just . Name $ "Carl Phoomp", + udNormalized = Just $ "carl phoomp", + udHandle = Just . fromJust . parseHandle $ "phoompy", + udEmail = Just $ unsafeEmailAddress "phoompy" "example.com", + udColourId = Just . ColourId $ 32, + udAccountStatus = Just Active, + udSAMLIdP = Just "https://issuer.net/214234", + udManagedBy = Just ManagedByScim, + udCreatedAt = Just (toUTCTimeMillis (mkTime 1598737800000)), + udRole = Just RoleAdmin, + udSearchVisibilityInbound = Nothing, + udScimExternalId = Nothing, + udSso = Nothing, + udEmailUnvalidated = Nothing + } + +-- Dont touch this. This represents serialized legacy data. +userDoc1ByteString :: LByteString +userDoc1ByteString = "{\"email\":\"phoompy@example.com\",\"account_status\":\"active\",\"handle\":\"phoompy\",\"managed_by\":\"scim\",\"role\":\"admin\",\"accent_id\":32,\"name\":\"Carl Phoomp\",\"created_at\":\"2020-08-29T21:50:00.000Z\",\"team\":\"17c59b18-57d6-11ea-9220-8bbf5eee961a\",\"id\":\"0a96b396-57d6-11ea-a04b-7b93d1a5c19c\",\"normalized\":\"carl phoomp\",\"saml_idp\":\"https://issuer.net/214234\"}" diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index a7975a867a1..ee679f39123 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -56,8 +56,9 @@ spec = describe "UserSubsystem.Interpreter" do target1 = mkUserIds remoteDomain1 targetUsers1 target2 = mkUserIds remoteDomain2 targetUsers2 localBackend = def {users = [viewer] <> localTargetUsers} + config = UserSubsystemConfig visibility miniLocale False retrievedProfiles = - runFederationStack localBackend federation Nothing (UserSubsystemConfig visibility miniLocale) $ + runFederationStack localBackend federation Nothing config $ getUserProfiles (toLocalUnsafe localDomain viewer.id) (localTargets <> target1 <> target2) @@ -83,7 +84,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 + config = UserSubsystemConfig visibility miniLocale False localBackend = def {users = [viewer]} result = run @@ -102,49 +103,45 @@ spec = describe "UserSubsystem.Interpreter" do describe "[without federation]" do prop "returns nothing when none of the users exist" $ - \viewer targetUserIds visibility domain locale -> - let config = UserSubsystemConfig visibility locale - retrievedProfiles = + \viewer targetUserIds config domain -> + let retrievedProfiles = runNoFederationStack def Nothing config $ getUserProfiles (toLocalUnsafe domain viewer) (map (`Qualified` domain) targetUserIds) in retrievedProfiles === [] prop "gets a local user profile when the user exists and both user and viewer have accepted their invitations" $ - \(NotPendingStoredUser viewer) (NotPendingStoredUser targetUserNoTeam) visibility domain locale sameTeam -> + \(NotPendingStoredUser viewer) (NotPendingStoredUser targetUserNoTeam) config domain sameTeam -> let teamMember = mkTeamMember viewer.id fullPermissions Nothing defUserLegalHoldStatus targetUser = if sameTeam then targetUserNoTeam {teamId = viewer.teamId} else targetUserNoTeam - config = UserSubsystemConfig visibility locale localBackend = def {users = [targetUser, viewer]} retrievedProfiles = runNoFederationStack localBackend (Just teamMember) config $ getUserProfiles (toLocalUnsafe domain viewer.id) [Qualified targetUser.id domain] in retrievedProfiles === [ mkUserProfile - (fmap (const $ (,) <$> viewer.teamId <*> Just teamMember) visibility) - (mkUserFromStored domain locale targetUser) + (fmap (const $ (,) <$> viewer.teamId <*> Just teamMember) config.emailVisibilityConfig) + (mkUserFromStored domain config.defaultLocale targetUser) defUserLegalHoldStatus ] prop "gets a local user profile when the target user exists and has accepted their invitation but the viewer has not accepted their invitation" $ - \(PendingStoredUser viewer) (NotPendingStoredUser targetUserNoTeam) visibility domain locale sameTeam -> + \(PendingStoredUser viewer) (NotPendingStoredUser targetUserNoTeam) config domain sameTeam -> let teamMember = mkTeamMember viewer.id fullPermissions Nothing defUserLegalHoldStatus targetUser = if sameTeam then targetUserNoTeam {teamId = viewer.teamId} else targetUserNoTeam - config = UserSubsystemConfig visibility locale localBackend = def {users = [targetUser, viewer]} retrievedProfile = runNoFederationStack localBackend (Just teamMember) config $ getUserProfiles (toLocalUnsafe domain viewer.id) [Qualified targetUser.id domain] in retrievedProfile === [ mkUserProfile - (fmap (const Nothing) visibility) - (mkUserFromStored domain locale targetUser) + (fmap (const Nothing) config.emailVisibilityConfig) + (mkUserFromStored domain config.defaultLocale targetUser) defUserLegalHoldStatus ] prop "returns Nothing if the target user has not accepted their invitation yet" $ - \viewer (PendingStoredUser targetUser) visibility domain locale -> + \viewer (PendingStoredUser targetUser) config domain -> let teamMember = mkTeamMember viewer.id fullPermissions Nothing defUserLegalHoldStatus - config = UserSubsystemConfig visibility locale localBackend = def {users = [targetUser, viewer]} retrievedProfile = runNoFederationStack localBackend (Just teamMember) config $ @@ -156,7 +153,7 @@ spec = describe "UserSubsystem.Interpreter" do \viewer targetUsers visibility domain remoteDomain -> do let remoteBackend = def {users = targetUsers} federation = [(remoteDomain, remoteBackend)] - config = UserSubsystemConfig visibility miniLocale + config = UserSubsystemConfig visibility miniLocale False localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend federation Nothing config $ @@ -177,7 +174,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 + config = UserSubsystemConfig visibility miniLocale False localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend online Nothing config $ @@ -197,7 +194,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 + config = UserSubsystemConfig visibility miniLocale False localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend online Nothing config $ @@ -282,7 +279,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 + let config = UserSubsystemConfig visibility locale False alice = alice' { email = Just email, @@ -317,7 +314,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 + let config = UserSubsystemConfig visibility locale True alice = alice' { email = Just email, @@ -351,7 +348,7 @@ spec = describe "UserSubsystem.Interpreter" do in result === [mkAccountFromStored 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 + let config = UserSubsystemConfig visibility locale True alice = alice' { email = Just email, @@ -387,7 +384,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 + let config = UserSubsystemConfig visibility locale True alice = alice' { email = Just email, @@ -423,7 +420,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 + let config = UserSubsystemConfig visibility locale True alice = alice' {email = Just email} localBackend = def @@ -437,7 +434,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 + let config = UserSubsystemConfig visibility locale True getBy = toLocalUnsafe localDomain $ def @@ -452,7 +449,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 + let config = UserSubsystemConfig visibility locale True getBy = toLocalUnsafe localDomain $ def @@ -467,7 +464,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 + let config = UserSubsystemConfig visibility locale True emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -496,7 +493,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 + let config = UserSubsystemConfig visibility locale True emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -517,7 +514,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 + let config = UserSubsystemConfig visibility locale True emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -551,7 +548,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 + let config = UserSubsystemConfig visibility locale True emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index db0cb43facb..294ead8c5de 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -89,10 +89,18 @@ library Wire.Events Wire.FederationAPIAccess Wire.FederationAPIAccess.Interpreter + Wire.FederationConfigStore + Wire.FederationConfigStore.Cassandra Wire.GalleyAPIAccess Wire.GalleyAPIAccess.Rpc Wire.GundeckAPIAccess Wire.HashPassword + Wire.IndexedUserStore + Wire.IndexedUserStore.Bulk + Wire.IndexedUserStore.Bulk.ElasticSearch + Wire.IndexedUserStore.ElasticSearch + Wire.IndexedUserStore.MigrationStore + Wire.IndexedUserStore.MigrationStore.ElasticSearch Wire.InternalEvent Wire.InvitationCodeStore Wire.InvitationCodeStore.Cassandra @@ -113,8 +121,12 @@ library Wire.StoredUser Wire.UserKeyStore Wire.UserKeyStore.Cassandra + Wire.UserSearch.Metrics + Wire.UserSearch.Migration + Wire.UserSearch.Types Wire.UserStore Wire.UserStore.Cassandra + Wire.UserStore.IndexUser Wire.UserStore.Unique Wire.UserSubsystem Wire.UserSubsystem.Error @@ -134,9 +146,11 @@ library , amazonka-core , amazonka-ses , async + , attoparsec , base , base16-bytestring , bilge + , bloodhound , bytestring , bytestring-conversion , cassandra-util @@ -172,15 +186,19 @@ library , polysemy-plugin , polysemy-time , polysemy-wire-zoo + , prometheus-client , QuickCheck , resource-pool , resourcet , retry + , saml2-web-sso + , schema-profunctor , servant , servant-client-core , stomp-queue , template , text + , text-icu-translit , time , time-out , time-units @@ -219,8 +237,10 @@ test-suite wire-subsystems-tests Wire.MockInterpreters.EmailSubsystem Wire.MockInterpreters.Error Wire.MockInterpreters.Events + Wire.MockInterpreters.FederationConfigStore Wire.MockInterpreters.GalleyAPIAccess Wire.MockInterpreters.HashPassword + Wire.MockInterpreters.IndexedUserStore Wire.MockInterpreters.InvitationCodeStore Wire.MockInterpreters.Now Wire.MockInterpreters.PasswordResetCodeStore @@ -234,6 +254,7 @@ test-suite wire-subsystems-tests Wire.MockInterpreters.VerificationCodeStore Wire.NotificationSubsystem.InterpreterSpec Wire.PropertySubsystem.InterpreterSpec + Wire.UserSearch.TypesSpec Wire.UserStoreSpec Wire.UserSubsystem.InterpreterSpec Wire.VerificationCodeSubsystem.InterpreterSpec @@ -245,6 +266,7 @@ test-suite wire-subsystems-tests , base , bilge , bytestring + , cassandra-util , containers , crypton , data-default diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 6d8885cfb71..14e026face8 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -114,8 +114,6 @@ library Brig.DeleteQueue.Interpreter Brig.Effects.ConnectionStore Brig.Effects.ConnectionStore.Cassandra - Brig.Effects.FederationConfigStore - Brig.Effects.FederationConfigStore.Cassandra Brig.Effects.JwtTools Brig.Effects.PublicKeyBundle Brig.Effects.SFT @@ -123,8 +121,6 @@ library Brig.Effects.UserPendingActivationStore.Cassandra Brig.Federation.Client Brig.Index.Eval - Brig.Index.Migrations - Brig.Index.Migrations.Types Brig.Index.Options Brig.Index.Types Brig.InternalEvent.Process @@ -190,24 +186,19 @@ library Brig.Team.API Brig.Team.Email Brig.Team.Template - Brig.Team.Util Brig.Template Brig.User.API.Handle - Brig.User.API.Search Brig.User.Auth Brig.User.Auth.Cookie Brig.User.Auth.Cookie.Limit Brig.User.EJPD Brig.User.Search.Index - Brig.User.Search.Index.Types Brig.User.Search.SearchIndex Brig.User.Search.TeamSize - Brig.User.Search.TeamUserSearch Brig.User.Template Brig.Version Brig.ZAuth - other-modules: Paths_brig hs-source-dirs: src ghc-options: -funbox-strict-fields -fplugin=Polysemy.Plugin @@ -238,7 +229,6 @@ library , conduit >=1.2.8 , containers >=0.5 , cookie >=0.4 - , cql , cryptobox-haskell >=0.1.1 , crypton , currency-codes >=2.0 @@ -297,7 +287,6 @@ library , resourcet >=1.1 , retry >=0.7 , safe-exceptions >=0.1 - , saml2-web-sso , schema-profunctor , servant , servant-openapi3 @@ -311,7 +300,6 @@ library , template >=0.2 , template-haskell , text >=0.11 - , text-icu-translit >=0.1 , time >=1.1 , time-out , time-units @@ -340,13 +328,11 @@ library executable brig import: common-all main-is: exec/Main.hs - other-modules: Paths_brig ghc-options: -funbox-strict-fields -threaded "-with-rtsopts=-N -T" -rtsopts -Wredundant-constraints -Wunused-packages build-depends: - , base , brig , HsOpenSSL , imports @@ -355,7 +341,6 @@ executable brig executable brig-index import: common-all main-is: index/src/Main.hs - other-modules: Paths_brig ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N build-depends: , base @@ -506,7 +491,6 @@ executable brig-schema ghc-options: -funbox-strict-fields -Wredundant-constraints -threaded default-extensions: TemplateHaskell build-depends: - , base , brig , cassandra-util , extended @@ -525,7 +509,6 @@ test-suite brig-tests Test.Brig.Effects.Delay Test.Brig.InternalNotification Test.Brig.MLS - Test.Brig.User.Search.Index.Types hs-source-dirs: test/unit ghc-options: -funbox-strict-fields -threaded -with-rtsopts=-N diff --git a/services/brig/default.nix b/services/brig/default.nix index 61ebf704692..a52f5219eb8 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -29,7 +29,6 @@ , conduit , containers , cookie -, cql , cryptobox-haskell , crypton , currency-codes @@ -130,7 +129,6 @@ , template-haskell , temporary , text -, text-icu-translit , time , time-out , time-units @@ -190,7 +188,6 @@ mkDerivation { conduit containers cookie - cql cryptobox-haskell crypton currency-codes @@ -249,7 +246,6 @@ mkDerivation { resourcet retry safe-exceptions - saml2-web-sso schema-profunctor servant servant-openapi3 @@ -263,7 +259,6 @@ mkDerivation { template template-haskell text - text-icu-translit time time-out time-units diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index eace1f730de..e61f5409994 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -23,7 +23,6 @@ import Brig.API.Types import Brig.API.User import Brig.App import Brig.Data.User qualified as User -import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Options import Brig.User.Auth qualified as Auth import Brig.ZAuth hiding (Env, settings) @@ -36,14 +35,13 @@ import Data.List1 (List1 (..)) import Data.Qualified import Data.Text qualified as T import Data.Text.Lazy qualified as LT -import Data.Time.Clock (UTCTime) import Data.ZAuth.Token qualified as ZAuth import Imports import Network.HTTP.Types import Network.Wai.Utilities ((!>>)) import Network.Wai.Utilities.Error qualified as Wai import Polysemy -import Polysemy.Input (Input) +import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Wire.API.Error import Wire.API.Error.Brig qualified as E @@ -54,10 +52,9 @@ import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso import Wire.BlockListStore import Wire.EmailSubsystem (EmailSubsystem) +import Wire.Events (Events) import Wire.GalleyAPIAccess -import Wire.NotificationSubsystem import Wire.PasswordStore (PasswordStore) -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem @@ -65,11 +62,8 @@ import Wire.VerificationCodeSubsystem (VerificationCodeSubsystem) accessH :: ( Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => Maybe ClientId -> [Either Text SomeUserToken] -> @@ -84,11 +78,8 @@ accessH mcid ut' mat' = do access :: ( TokenPair u a, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => Maybe ClientId -> NonEmpty (Token u) -> @@ -106,14 +97,11 @@ sendLoginCode _ = login :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member PasswordStore r, Member UserKeyStore r, Member UserStore r, + Member Events r, + Member (Input (Local ())) r, Member UserSubsystem r, Member VerificationCodeSubsystem r ) => @@ -142,7 +130,8 @@ logout uts (Just at) = Auth.logout (List1 uts) at !>> zauthError changeSelfEmailH :: ( Member BlockListStore r, Member UserKeyStore r, - Member EmailSubsystem r + Member EmailSubsystem r, + Member UserSubsystem r ) => [Either Text SomeUserToken] -> Maybe (Either Text SomeAccessToken) -> @@ -183,12 +172,9 @@ removeCookies lusr (RemoveCookies pw lls ids) = legalHoldLogin :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => LegalHoldLogin -> Handler r SomeAccess @@ -199,11 +185,8 @@ legalHoldLogin lhl = do ssoLogin :: ( Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => SsoLogin -> Maybe Bool -> diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index cc513f7bfa8..41b001858f9 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -53,7 +53,6 @@ import Brig.App import Brig.Data.Client qualified as Data import Brig.Data.Nonce as Nonce import Brig.Data.User qualified as Data -import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.JwtTools qualified as JwtTools import Brig.Effects.PublicKeyBundle (PublicKeyBundle) @@ -84,13 +83,10 @@ import Data.Qualified import Data.Set qualified as Set import Data.Text.Encoding qualified as T import Data.Text.Encoding.Error -import Data.Time.Clock (UTCTime) import Imports import Network.HTTP.Types.Method (StdMethod) import Network.Wai.Utilities import Polysemy -import Polysemy.Input (Input) -import Polysemy.TinyLog import Servant (Link, ToHttpApiData (toUrlPiece)) import System.Logger.Class (field, msg, val, (~~)) import System.Logger.Class qualified as Log @@ -109,13 +105,14 @@ import Wire.API.UserEvent import Wire.API.UserMap (QualifiedUserMap (QualifiedUserMap, qualifiedUserMap), UserMap (userMap)) import Wire.DeleteQueue import Wire.EmailSubsystem (EmailSubsystem, sendNewClientEmail) +import Wire.Events (Events) +import Wire.Events qualified as Events import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.NotificationSubsystem import Wire.Sem.Concurrency import Wire.Sem.FromUTC (FromUTC (fromUTCTime)) import Wire.Sem.Now as Now -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserSubsystem (UserSubsystem) import Wire.UserSubsystem qualified as User import Wire.VerificationCodeSubsystem (VerificationCodeSubsystem) @@ -165,16 +162,12 @@ lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk addClient :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member UserSubsystem r, - Member TinyLog r, Member DeleteQueue r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member Events r ) => Local UserId -> Maybe ConnId -> @@ -187,14 +180,10 @@ addClient = addClientWithReAuthPolicy Data.reAuthForNewClients addClientWithReAuthPolicy :: forall r. ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, Member DeleteQueue r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, + Member Events r, Member UserSubsystem r, Member VerificationCodeSubsystem r ) => @@ -224,7 +213,7 @@ addClientWithReAuthPolicy policy luid@(tUnqualified -> u) con new = do for_ old $ execDelete u con liftSem $ GalleyAPIAccess.newClient u (clientId clt) liftSem $ Intra.onClientEvent u con (ClientAdded clt) - when (clientType clt == LegalHoldClientType) $ liftSem $ Intra.onUserEvent u con (UserLegalHoldEnabled u) + when (clientType clt == LegalHoldClientType) $ liftSem $ Events.generateUserEvent u con (UserLegalHoldEnabled u) when (count > 1) $ for_ (userEmail usr) $ \email -> @@ -517,19 +506,9 @@ pubClient c = pubClientClass = clientClass c } -legalHoldClientRequested :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r - ) => - UserId -> - LegalHoldClientRequest -> - AppT r () +legalHoldClientRequested :: (Member Events r) => UserId -> LegalHoldClientRequest -> AppT r () legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPrekey') = - liftSem $ Intra.onUserEvent targetUser Nothing lhClientEvent + liftSem $ Events.generateUserEvent targetUser Nothing lhClientEvent where clientId :: ClientId clientId = clientIdFromPrekey $ unpackLastPrekey lastPrekey' @@ -538,24 +517,14 @@ legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPreke lhClientEvent :: UserEvent lhClientEvent = LegalHoldClientRequested eventData -removeLegalHoldClient :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member DeleteQueue r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r - ) => - UserId -> - AppT r () +removeLegalHoldClient :: (Member DeleteQueue r, Member Events r) => UserId -> AppT r () removeLegalHoldClient uid = do clients <- wrapClient $ Data.lookupClients uid -- Should only be one; but just in case we'll treat it as a list let legalHoldClients = filter ((== LegalHoldClientType) . clientType) clients -- maybe log if this isn't the case forM_ legalHoldClients (execDelete uid Nothing) - liftSem $ Intra.onUserEvent uid Nothing (UserLegalHoldDisabled uid) + liftSem $ Events.generateUserEvent uid Nothing (UserLegalHoldDisabled uid) createAccessToken :: (Member JwtTools r, Member Now r, Member PublicKeyBundle r) => diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index cb3ed7e3dd0..f5efefff8b7 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -41,7 +41,6 @@ import Brig.App import Brig.Data.Connection qualified as Data import Brig.Data.Types (resultHasMore, resultList) import Brig.Data.User qualified as Data -import Brig.Effects.FederationConfigStore import Brig.IO.Intra qualified as Intra import Brig.IO.Logging import Brig.Options @@ -68,6 +67,7 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) import Wire.API.User import Wire.API.UserEvent +import Wire.FederationConfigStore import Wire.GalleyAPIAccess import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.NotificationSubsystem diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index 03b650731c8..3a812f665d6 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -28,7 +28,6 @@ import Brig.API.Types (ConnectionError (..)) import Brig.App import Brig.Data.Connection qualified as Data import Brig.Data.User qualified as Data -import Brig.Effects.FederationConfigStore import Brig.Federation.Client as Federation import Brig.IO.Intra qualified as Intra import Brig.Options @@ -51,6 +50,7 @@ import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) import Wire.API.User import Wire.API.UserEvent +import Wire.FederationConfigStore import Wire.GalleyAPIAccess import Wire.NotificationSubsystem import Wire.UserStore diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 370761fb73f..fd891967200 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -31,8 +31,6 @@ import Brig.API.User qualified as API import Brig.App import Brig.Data.Connection qualified as Data import Brig.Data.User qualified as Data -import Brig.Effects.FederationConfigStore (FederationConfigStore) -import Brig.Effects.FederationConfigStore qualified as E import Brig.IO.Intra (notify) import Brig.Options import Brig.User.API.Handle @@ -73,11 +71,14 @@ import Wire.API.UserEvent import Wire.API.UserMap (UserMap) import Wire.DeleteQueue import Wire.Error +import Wire.FederationConfigStore (FederationConfigStore) +import Wire.FederationConfigStore qualified as E import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.NotificationSubsystem import Wire.Sem.Concurrency import Wire.UserStore -import Wire.UserSubsystem +import Wire.UserSubsystem (UserSubsystem) +import Wire.UserSubsystem qualified as UserSubsystem type FederationAPI = "federation" :> BrigApi @@ -169,7 +170,7 @@ getUserByHandle domain handle = do pure Nothing Just ownerId -> do localOwnerId <- qualifyLocal ownerId - liftSem $ getLocalUserProfile localOwnerId + liftSem $ UserSubsystem.getLocalUserProfile localOwnerId getUsersByIds :: (Member UserSubsystem r) => @@ -178,7 +179,7 @@ getUsersByIds :: ExceptT HttpError (AppT r) [UserProfile] getUsersByIds _ uids = do luids <- qualifyLocal uids - lift $ liftSem $ getLocalUserProfiles luids + lift $ liftSem $ UserSubsystem.getLocalUserProfiles luids claimPrekey :: (Member DeleteQueue r) => Domain -> (UserId, ClientId) -> (Handler r) (Maybe ClientPrekey) claimPrekey _ (user, client) = do @@ -254,7 +255,7 @@ searchUsers domain (SearchRequest searchTerm mTeam mOnlyInTeams) = do mFoundUserTeamId <- lift $ wrapClient $ Data.lookupUserTeam foundUser localFoundUser <- qualifyLocal foundUser if isTeamAllowed mOnlyInTeams mFoundUserTeamId - then lift $ liftSem $ (fmap contactFromProfile . maybeToList) <$> getLocalUserProfile localFoundUser + then lift $ liftSem $ (fmap contactFromProfile . maybeToList) <$> UserSubsystem.getLocalUserProfile localFoundUser else pure [] | otherwise = pure [] diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index cdb90eb56a6..d6b5a13235f 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -37,16 +37,7 @@ import Brig.Data.Client qualified as Data import Brig.Data.Connection qualified as Data import Brig.Data.MLS.KeyPackage qualified as Data import Brig.Data.User qualified as Data -import Brig.Effects.ConnectionStore (ConnectionStore) -import Brig.Effects.FederationConfigStore - ( AddFederationRemoteResult (..), - AddFederationRemoteTeamResult (..), - FederationConfigStore, - UpdateFederationResult (..), - ) -import Brig.Effects.FederationConfigStore qualified as E import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) -import Brig.IO.Intra qualified as Intra import Brig.Options hiding (internalEvents) import Brig.Provider.API qualified as Provider import Brig.Team.API qualified as Team @@ -55,9 +46,8 @@ import Brig.Types.Connection import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.Types.User -import Brig.User.API.Search qualified as Search import Brig.User.EJPD qualified -import Brig.User.Search.Index qualified as Index +import Brig.User.Search.Index qualified as Search import Control.Error hiding (bool) import Control.Lens (preview, to, view, _Just) import Data.ByteString.Conversion (toByteString) @@ -72,7 +62,6 @@ import Data.Map.Strict qualified as Map import Data.Qualified import Data.Set qualified as Set import Data.Text qualified as T -import Data.Time.Clock (UTCTime) import Data.Time.Clock.System import Imports hiding (head) import Network.Wai.Utilities as Utilities @@ -104,6 +93,15 @@ import Wire.BlockListStore (BlockListStore) import Wire.DeleteQueue (DeleteQueue) import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem (EmailSubsystem) +import Wire.Events (Events) +import Wire.Events qualified as Events +import Wire.FederationConfigStore + ( AddFederationRemoteResult (..), + AddFederationRemoteTeamResult (..), + FederationConfigStore, + UpdateFederationResult (..), + ) +import Wire.FederationConfigStore qualified as E import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.InvitationCodeStore import Wire.NotificationSubsystem @@ -111,7 +109,6 @@ import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PropertySubsystem import Wire.Rpc import Wire.Sem.Concurrency -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem @@ -125,13 +122,10 @@ servantSitemap :: ( Member BlockListStore r, Member DeleteQueue r, Member (Concurrency 'Unsafe) r, - Member (ConnectionStore InternalPaging) r, Member (Embed HttpClientIO) r, Member FederationConfigStore r, Member AuthenticationSubsystem r, Member GalleyAPIAccess r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, Member NotificationSubsystem r, Member UserSubsystem r, Member UserStore r, @@ -140,9 +134,11 @@ servantSitemap :: Member Rpc r, Member TinyLog r, Member (UserPendingActivationStore p) r, + Member (Input (Local ())) r, Member EmailSending r, Member EmailSubsystem r, Member VerificationCodeSubsystem r, + Member Events r, Member PasswordResetCodeStore r, Member PropertySubsystem r, Member (Input TeamTemplates) r @@ -189,14 +185,13 @@ accountAPI :: Member NotificationSubsystem r, Member UserSubsystem r, Member UserKeyStore r, + Member (Input (Local ())) r, Member UserStore r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, Member VerificationCodeSubsystem r, Member PropertySubsystem r, + Member Events r, Member PasswordResetCodeStore r, Member InvitationCodeStore r ) => @@ -242,21 +237,19 @@ teamsAPI :: Member (UserPendingActivationStore p) r, Member BlockListStore r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member UserKeyStore r, Member (Concurrency 'Unsafe) r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, Member InvitationCodeStore r, - Member (ConnectionStore InternalPaging) r, Member EmailSending r, Member UserSubsystem r, - Member (Input TeamTemplates) r + Member Events r, + Member (Input TeamTemplates) r, + Member (Input (Local ())) r ) => ServerT BrigIRoutes.TeamsAPI (Handler r) teamsAPI = - Named @"updateSearchVisibilityInbound" Index.updateSearchVisibilityInbound + Named @"updateSearchVisibilityInbound" (lift . liftSem . updateTeamSearchVisibilityInbound) :<|> Named @"get-invitation-by-email" Team.getInvitationByEmail :<|> Named @"get-invitation-code" Team.getInvitationCode :<|> Named @"suspend-team" Team.suspendTeam @@ -276,12 +269,8 @@ clientAPI = Named @"update-client-last-active" updateClientLastActive authAPI :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + Member Events r, Member UserSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member VerificationCodeSubsystem r ) => ServerT BrigIRoutes.AuthAPI (Handler r) @@ -297,7 +286,10 @@ authAPI = qualifyLocal uid >>= \luid -> reauthenticate luid reauth ) -federationRemotesAPI :: (Member FederationConfigStore r) => ServerT BrigIRoutes.FederationRemotesAPI (Handler r) +federationRemotesAPI :: + ( Member FederationConfigStore r + ) => + ServerT BrigIRoutes.FederationRemotesAPI (Handler r) federationRemotesAPI = Named @"add-federation-remotes" addFederationRemote :<|> Named @"get-federation-remotes" getFederationRemotes @@ -314,7 +306,12 @@ getFederationRemoteTeams :: (Member FederationConfigStore r) => Domain -> (Handl getFederationRemoteTeams domain = lift $ liftSem $ E.getFederationRemoteTeams domain -addFederationRemoteTeam :: (Member FederationConfigStore r) => Domain -> FederationRemoteTeam -> (Handler r) () +addFederationRemoteTeam :: + ( Member FederationConfigStore r + ) => + Domain -> + FederationRemoteTeam -> + (Handler r) () addFederationRemoteTeam domain rt = lift (liftSem $ E.addFederationRemoteTeam domain rt.teamId) >>= \case AddFederationRemoteTeamSuccess -> pure () @@ -329,7 +326,11 @@ addFederationRemoteTeam domain rt = getFederationRemotes :: (Member FederationConfigStore r) => (Handler r) FederationDomainConfigs getFederationRemotes = lift $ liftSem $ E.getFederationConfigs -addFederationRemote :: (Member FederationConfigStore r) => FederationDomainConfig -> (Handler r) () +addFederationRemote :: + ( Member FederationConfigStore r + ) => + FederationDomainConfig -> + (Handler r) () addFederationRemote fedDomConf = do lift (liftSem $ E.addFederationConfig fedDomConf) >>= \case AddFederationRemoteSuccess -> pure () @@ -408,8 +409,6 @@ getVerificationCode uid action = runMaybeT do internalSearchIndexAPI :: forall r. ServerT BrigIRoutes.ISearchIndexAPI (Handler r) internalSearchIndexAPI = Named @"indexRefresh" (NoContent <$ lift (wrapClient Search.refreshIndex)) - :<|> Named @"indexReindex" (NoContent <$ lift (wrapClient Search.reindexAll)) - :<|> Named @"indexReindexIfSameOrNewer" (NoContent <$ lift (wrapClient Search.reindexAllIfSameOrNewer)) --------------------------------------------------------------------------- -- Handlers @@ -417,14 +416,10 @@ internalSearchIndexAPI = -- | Add a client without authentication checks addClientInternalH :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member DeleteQueue r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, + Member Events r, Member UserSubsystem r, Member VerificationCodeSubsystem r ) => @@ -440,31 +435,11 @@ addClientInternalH usr mSkipReAuth new connId = do lusr <- qualifyLocal usr API.addClientWithReAuthPolicy policy lusr connId new !>> clientError -legalHoldClientRequestedH :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r - ) => - UserId -> - LegalHoldClientRequest -> - (Handler r) NoContent +legalHoldClientRequestedH :: (Member Events r) => UserId -> LegalHoldClientRequest -> (Handler r) NoContent legalHoldClientRequestedH targetUser clientRequest = do lift $ NoContent <$ API.legalHoldClientRequested targetUser clientRequest -removeLegalHoldClientH :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member DeleteQueue r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r - ) => - UserId -> - (Handler r) NoContent +removeLegalHoldClientH :: (Member DeleteQueue r, Member Events r) => UserId -> (Handler r) NoContent removeLegalHoldClientH uid = do lift $ NoContent <$ API.removeLegalHoldClient uid @@ -482,15 +457,12 @@ createUserNoVerify :: Member GalleyAPIAccess r, Member (UserPendingActivationStore p) r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + Member Events r, Member InvitationCodeStore r, Member UserKeyStore r, Member UserSubsystem r, Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member PasswordResetCodeStore r, - Member (ConnectionStore InternalPaging) r + Member PasswordResetCodeStore r ) => NewUser -> (Handler r) (Either RegisterError SelfProfile) @@ -508,14 +480,10 @@ createUserNoVerify uData = lift . runExceptT $ do createUserNoVerifySpar :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member UserSubsystem r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member PasswordResetCodeStore r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r, + Member PasswordResetCodeStore r ) => NewUserSpar -> (Handler r) (Either CreateUserSparError SelfProfile) @@ -538,10 +506,8 @@ deleteUserNoAuthH :: Member UserStore r, Member TinyLog r, Member UserKeyStore r, + Member Events r, Member UserSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member PropertySubsystem r ) => UserId -> @@ -554,14 +520,33 @@ deleteUserNoAuthH uid = do AccountAlreadyDeleted -> pure UserResponseAccountAlreadyDeleted AccountDeleted -> pure UserResponseAccountDeleted -changeSelfEmailMaybeSendH :: (Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> EmailUpdate -> Maybe Bool -> (Handler r) ChangeEmailResponse +changeSelfEmailMaybeSendH :: + ( Member BlockListStore r, + Member UserKeyStore r, + Member EmailSubsystem r, + Member UserSubsystem r + ) => + UserId -> + EmailUpdate -> + Maybe Bool -> + (Handler r) ChangeEmailResponse changeSelfEmailMaybeSendH u body (fromMaybe False -> validate) = do let email = euEmail body changeSelfEmailMaybeSend u (if validate then ActuallySendEmail else DoNotSendEmail) email UpdateOriginScim data MaybeSendEmail = ActuallySendEmail | DoNotSendEmail -changeSelfEmailMaybeSend :: (Member BlockListStore r, Member UserKeyStore r, Member EmailSubsystem r) => UserId -> MaybeSendEmail -> EmailAddress -> UpdateOriginType -> (Handler r) ChangeEmailResponse +changeSelfEmailMaybeSend :: + ( Member BlockListStore r, + Member UserKeyStore r, + Member EmailSubsystem r, + Member UserSubsystem r + ) => + UserId -> + MaybeSendEmail -> + EmailAddress -> + UpdateOriginType -> + (Handler r) ChangeEmailResponse changeSelfEmailMaybeSend u ActuallySendEmail email allowScim = do API.changeSelfEmail u email allowScim changeSelfEmailMaybeSend u DoNotSendEmail email allowScim = do @@ -624,12 +609,8 @@ getPasswordResetCode email = >>= maybe (throwStd (errorToWai @'E.InvalidPasswordResetKey)) pure changeAccountStatusH :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + ( Member UserSubsystem r, + Member Events r ) => UserId -> AccountStatusUpdate -> @@ -701,39 +682,34 @@ addBlacklist :: (Member BlockListStore r) => EmailAddress -> Handler r NoContent addBlacklist email = lift $ NoContent <$ API.blacklistInsert email updateSSOIdH :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + ( Member UserSubsystem r, + Member Events r ) => UserId -> UserSSOId -> (Handler r) UpdateSSOIdResponse -updateSSOIdH uid ssoid = do - success <- lift $ wrapClient $ Data.updateSSOId uid (Just ssoid) - if success - then do - lift $ liftSem $ Intra.onUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOId = Just ssoid})) - pure UpdateSSOIdSuccess - else pure UpdateSSOIdNotFound +updateSSOIdH uid ssoid = lift $ do + success <- wrapClient $ Data.updateSSOId uid (Just ssoid) + liftSem $ + if success + then do + UserSubsystem.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOId = Just ssoid})) + pure UpdateSSOIdSuccess + else pure UpdateSSOIdNotFound deleteSSOIdH :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + ( Member UserSubsystem r, + Member Events r ) => UserId -> (Handler r) UpdateSSOIdResponse -deleteSSOIdH uid = do - success <- lift $ wrapClient $ Data.updateSSOId uid Nothing +deleteSSOIdH uid = lift $ do + success <- wrapClient $ Data.updateSSOId uid Nothing if success - then do - lift $ liftSem $ Intra.onUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOIdRemoved = True})) + then liftSem $ do + UserSubsystem.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOIdRemoved = True})) pure UpdateSSOIdSuccess else pure UpdateSSOIdNotFound diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index f3dc6426492..1f313fb01c4 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -41,8 +41,6 @@ import Brig.Calling.API qualified as Calling import Brig.Data.Connection qualified as Data import Brig.Data.Nonce as Nonce import Brig.Data.User qualified as Data -import Brig.Effects.ConnectionStore (ConnectionStore) -import Brig.Effects.FederationConfigStore (FederationConfigStore) import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) import Brig.Effects.SFT @@ -55,8 +53,6 @@ import Brig.Team.Template (TeamTemplates) import Brig.Types.Activation (ActivationPair) import Brig.Types.Intra (UserAccount (UserAccount, accountUser)) import Brig.User.API.Handle qualified as Handle -import Brig.User.API.Search (teamUserSearch) -import Brig.User.API.Search qualified as Search import Brig.User.Auth.Cookie qualified as Auth import Cassandra qualified as C import Cassandra qualified as Data @@ -86,15 +82,16 @@ import Data.Qualified import Data.Range import Data.Schema () import Data.Text.Encoding qualified as Text -import Data.Time.Clock (UTCTime) import Data.ZAuth.Token qualified as ZAuth import FileEmbedLzma import Imports hiding (head) import Network.Socket (PortNumber) -import Network.Wai.Utilities as Utilities +import Network.Wai.Utilities (CacheControl (..), (!>>)) +import Network.Wai.Utilities qualified as Utilities import Polysemy +import Polysemy.Error import Polysemy.Fail (Fail) -import Polysemy.Input (Input) +import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) import Servant qualified @@ -144,6 +141,7 @@ import Wire.API.User.Client.Prekey qualified as Public import Wire.API.User.Handle qualified as Public import Wire.API.User.Password qualified as Public import Wire.API.User.RichInfo qualified as Public +import Wire.API.User.Search qualified as Public import Wire.API.UserMap qualified as Public import Wire.API.Wrapped qualified as Public import Wire.AuthenticationSubsystem (AuthenticationSubsystem, createPasswordResetCode, resetPassword) @@ -152,6 +150,8 @@ import Wire.DeleteQueue import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem import Wire.Error +import Wire.Events (Events) +import Wire.FederationConfigStore (FederationConfigStore) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.InvitationCodeStore @@ -162,11 +162,12 @@ import Wire.PropertySubsystem import Wire.Sem.Concurrency import Wire.Sem.Jwk (Jwk) import Wire.Sem.Now (Now) -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore +import Wire.UserSearch.Types import Wire.UserStore (UserStore) import Wire.UserSubsystem hiding (checkHandle, checkHandles) import Wire.UserSubsystem qualified as User +import Wire.UserSubsystem.Error import Wire.VerificationCode import Wire.VerificationCodeGen import Wire.VerificationCodeSubsystem @@ -263,37 +264,37 @@ internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = servantSitemap :: forall r p. - ( Member BlockListStore r, - Member DeleteQueue r, - Member (Concurrency 'Unsafe) r, - Member (ConnectionStore InternalPaging) r, + ( Member (Concurrency 'Unsafe) r, Member (Embed HttpClientIO) r, Member (Embed IO) r, + Member (Error UserSubsystemError) r, Member Fail r, - Member FederationConfigStore r, Member (Input (Local ())) r, + Member (Input TeamTemplates) r, + Member (UserPendingActivationStore p) r, Member AuthenticationSubsystem r, - Member (Input UTCTime) r, - Member Jwk r, + Member DeleteQueue r, + Member EmailSending r, + Member EmailSubsystem r, + Member Events r, + Member FederationConfigStore r, Member GalleyAPIAccess r, + Member InvitationCodeStore r, + Member Jwk r, Member JwtTools r, Member NotificationSubsystem r, - Member UserSubsystem r, - Member UserStore r, - Member PasswordStore r, - Member UserKeyStore r, Member Now r, + Member PasswordResetCodeStore r, + Member PasswordStore r, + Member PropertySubsystem r, Member PublicKeyBundle r, Member SFT r, Member TinyLog r, - Member (UserPendingActivationStore p) r, - Member EmailSubsystem r, - Member EmailSending r, + Member UserKeyStore r, + Member UserStore r, + Member UserSubsystem r, Member VerificationCodeSubsystem r, - Member PropertySubsystem r, - Member PasswordResetCodeStore r, - Member InvitationCodeStore r, - Member (Input TeamTemplates) r + Member BlockListStore r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -403,7 +404,7 @@ servantSitemap = :<|> Named @"get-connection" getConnection :<|> Named @"update-connection-unqualified" (callsFed (exposeAnnotations updateLocalConnection)) :<|> Named @"update-connection" (callsFed (exposeAnnotations updateConnection)) - :<|> Named @"search-contacts" (callsFed (exposeAnnotations Search.search)) + :<|> Named @"search-contacts" (callsFed (exposeAnnotations searchUsersHandler)) propertiesAPI :: ServerT PropertiesAPI (Handler r) propertiesAPI = @@ -430,7 +431,7 @@ servantSitemap = searchAPI :: ServerT SearchAPI (Handler r) searchAPI = - Named @"browse-team" teamUserSearch + Named @"browse-team" browseTeamHandler authAPI :: ServerT AuthAPI (Handler r) authAPI = @@ -462,6 +463,21 @@ servantSitemap = --------------------------------------------------------------------------- -- Handlers +browseTeamHandler :: + (Member UserSubsystem r) => + UserId -> + TeamId -> + Maybe Text -> + Maybe Public.RoleFilter -> + Maybe Public.TeamUserSearchSortBy -> + Maybe Public.TeamUserSearchSortOrder -> + Maybe (Range 1 500 Int) -> + Maybe Public.PagingState -> + Handler r (Public.SearchResult Public.TeamContact) +browseTeamHandler uid tid mQuery mRoleFilter mTeamUserSearchSortBy mTeamUserSearchSortOrder mMaxResults mPagingState = do + let browseTeamFilters = BrowseTeamFilters tid mQuery mRoleFilter mTeamUserSearchSortBy mTeamUserSearchSortOrder + lift . liftSem $ User.browseTeam uid browseTeamFilters mMaxResults mPagingState + setPropertyH :: (Member PropertySubsystem r) => UserId -> ConnId -> Public.PropertyKey -> Public.RawPropertyValue -> Handler r () setPropertyH u c key raw = lift . liftSem $ setProperty u c key raw @@ -559,16 +575,12 @@ getMultiUserPrekeyBundleH zusr qualUserClients = do addClient :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, Member DeleteQueue r, Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, - Member UserSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member Events r, + Member UserSubsystem r ) => Local UserId -> ConnId -> @@ -692,14 +704,11 @@ createUser :: Member GalleyAPIAccess r, Member InvitationCodeStore r, Member (UserPendingActivationStore p) r, + Member (Input (Local ())) r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member UserKeyStore r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, + Member Events r, Member UserSubsystem r, Member PasswordResetCodeStore r, Member EmailSending r @@ -916,14 +925,9 @@ removePhone :: UserId -> Handler r (Maybe Public.RemoveIdentityError) removePhone _ = (lift . pure) Nothing removeEmail :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member UserKeyStore r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member UserSubsystem r + ( Member UserKeyStore r, + Member UserSubsystem r, + Member Events r ) => UserId -> Handler r (Maybe Public.RemoveIdentityError) @@ -1032,6 +1036,16 @@ sendActivationCode ac = do checkAllowlist email API.sendActivationCode email (ac.locale) !>> sendActCodeError +searchUsersHandler :: + (Member UserSubsystem r) => + Local UserId -> + Text -> + Maybe Domain -> + Maybe (Range 1 500 Int32) -> + Handler r (Public.SearchResult Public.Contact) +searchUsersHandler luid term mDomain mMaxResults = + lift . liftSem $ User.searchUsers luid term mDomain mMaxResults + -- | If the user presents an email address from a blocked domain, throw an error. -- -- The tautological constraint in the type signature is added so that once we remove the @@ -1182,13 +1196,11 @@ deleteSelfUser :: Member NotificationSubsystem r, Member UserStore r, Member PasswordStore r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, Member UserSubsystem r, Member VerificationCodeSubsystem r, - Member PropertySubsystem r + Member PropertySubsystem r, + Member Events r ) => Local UserId -> Public.DeleteUser -> @@ -1200,14 +1212,12 @@ verifyDeleteUser :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member UserStore r, - Member UserSubsystem r, Member TinyLog r, - Member (Input (Local ())) r, Member UserKeyStore r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member VerificationCodeSubsystem r, - Member PropertySubsystem r + Member PropertySubsystem r, + Member UserSubsystem r, + Member Events r ) => Public.VerifyDeleteUser -> Handler r () @@ -1218,7 +1228,8 @@ updateUserEmail :: ( Member BlockListStore r, Member UserKeyStore r, Member GalleyAPIAccess r, - Member EmailSubsystem r + Member EmailSubsystem r, + Member UserSubsystem r ) => UserId -> UserId -> @@ -1249,13 +1260,9 @@ updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do activate :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member UserSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member PasswordResetCodeStore r, - Member (ConnectionStore InternalPaging) r + Member Events r, + Member PasswordResetCodeStore r ) => Public.ActivationKey -> Public.ActivationCode -> @@ -1268,13 +1275,9 @@ activate k c = do activateKey :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + Member Events r, Member UserSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member PasswordResetCodeStore r, - Member (ConnectionStore InternalPaging) r + Member PasswordResetCodeStore r ) => Public.Activate -> (Handler r) ActivationRespWithStatus diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index f18ae4b8d30..17331d18ce1 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -81,7 +81,6 @@ import Brig.Data.Connection (countConnections) import Brig.Data.Connection qualified as Data import Brig.Data.User import Brig.Data.User qualified as Data -import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore qualified as UserPendingActivationStore import Brig.IO.Intra qualified as Intra @@ -89,7 +88,6 @@ import Brig.Options hiding (internalEvents) import Brig.Types.Activation (ActivationPair) import Brig.Types.Intra import Brig.User.Auth.Cookie qualified as Auth -import Brig.User.Search.Index (reindex) import Brig.User.Search.TeamSize qualified as TeamSize import Cassandra hiding (Set) import Control.Error @@ -108,12 +106,12 @@ import Data.List1 as List1 (List1, singleton) import Data.Misc import Data.Qualified import Data.Range -import Data.Time.Clock (UTCTime, addUTCTime) +import Data.Time.Clock (addUTCTime) import Data.UUID.V4 (nextRandom) import Imports import Network.Wai.Utilities import Polysemy -import Polysemy.Input (Input) +import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log import Prometheus qualified as Prom @@ -138,6 +136,8 @@ import Wire.BlockListStore as BlockListStore import Wire.DeleteQueue import Wire.EmailSubsystem import Wire.Error +import Wire.Events (Events) +import Wire.Events qualified as Events import Wire.GalleyAPIAccess as GalleyAPIAccess import Wire.InvitationCodeStore (InvitationCodeStore, StoredInvitation, StoredInvitationInfo) import Wire.InvitationCodeStore qualified as InvitationCodeStore @@ -146,7 +146,6 @@ import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PasswordStore (PasswordStore, lookupHashedPassword, upsertHashedPassword) import Wire.PropertySubsystem as PropertySubsystem import Wire.Sem.Concurrency -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem as User @@ -191,13 +190,9 @@ verifyUniquenessAndCheckBlacklist uk = do createUserSpar :: forall r. ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, Member UserSubsystem r, - Member (ConnectionStore InternalPaging) r + Member Events r ) => NewUserSpar -> ExceptT CreateUserSparError (AppT r) CreateUserResult @@ -219,7 +214,8 @@ createUserSpar new = do Just richInfo -> wrapClient $ Data.updateRichInfo uid richInfo Nothing -> pure () -- Nothing to do liftSem $ GalleyAPIAccess.createSelfConv uid - liftSem $ Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) + liftSem $ User.internalUpdateSearchIndex uid + liftSem $ Events.generateUserEvent uid Nothing (UserCreated (accountUser account)) pure account @@ -266,11 +262,8 @@ createUser :: Member UserKeyStore r, Member UserSubsystem r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + Member Events r, Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member PasswordResetCodeStore r, Member InvitationCodeStore r ) => @@ -335,7 +328,7 @@ createUser new = do wrapClient $ Data.insertAccount account Nothing pw False liftSem $ GalleyAPIAccess.createSelfConv uid - liftSem $ Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) + liftSem $ Events.generateUserEvent uid Nothing (UserCreated (accountUser account)) pure account @@ -405,7 +398,8 @@ createUser new = do unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do - wrapClient $ activateUser uid ident -- ('insertAccount' sets column activated to False; here it is set to True.) + -- ('insertAccount' sets column activated to False; here it is set to True.) + wrapClient $ activateUser uid ident void $ onActivated (AccountActivated account) liftSem do Log.info $ @@ -536,7 +530,16 @@ checkRestrictedUserCreation new = do -- | 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) => UserId -> EmailAddress -> UpdateOriginType -> ExceptT HttpError (AppT r) ChangeEmailResponse +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 -> @@ -544,7 +547,7 @@ changeSelfEmail u email allowScim = do ChangeEmailNeedsActivation (usr, adata, en) -> lift $ do liftSem $ sendOutEmail usr adata en wrapClient $ Data.updateEmailUnvalidated u email - wrapClient $ reindex u + liftSem $ User.internalUpdateSearchIndex u pure ChangeEmailResponseNeedsActivation where sendOutEmail usr adata en = do @@ -581,14 +584,9 @@ changeEmail u email updateOrigin = do -- Remove Email removeEmail :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member UserKeyStore r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member UserSubsystem r + ( Member UserKeyStore r, + Member UserSubsystem r, + Member Events r ) => UserId -> ExceptT RemoveIdentityError (AppT r) () @@ -598,7 +596,13 @@ removeEmail uid = do Just (SSOIdentity (UserSSOId _) (Just e)) -> lift $ do liftSem $ deleteKey $ mkEmailKey e wrapClient $ Data.deleteEmail uid - liftSem $ Intra.onUserEvent uid Nothing (emailRemoved uid e) + -- FUTUREWORK: This doesn't delete user's email address from the index, + -- which is a bug, reported here: + -- https://wearezeta.atlassian.net/browse/WPB-11122. + -- + -- Calling User.internalUpdateSearchIndex here wouldn't work as explained + -- in the ticket. + liftSem $ Events.generateUserEvent uid Nothing (emailRemoved uid e) Just _ -> throwE LastIdentity Nothing -> throwE NoIdentity @@ -627,12 +631,9 @@ revokeIdentity key = do changeAccountStatus :: forall r. ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => List1 UserId -> AccountStatus -> @@ -647,15 +648,12 @@ changeAccountStatus usrs status = do Sem r () update ev u = do embed $ Data.updateStatus u status - Intra.onUserEvent u Nothing (ev u) + User.internalUpdateSearchIndex u + Events.generateUserEvent u Nothing (ev u) changeSingleAccountStatus :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + ( Member UserSubsystem r, + Member Events r ) => UserId -> AccountStatus -> @@ -665,7 +663,8 @@ changeSingleAccountStatus uid status = do ev <- mkUserEvent (List1.singleton uid) status lift $ do wrapClient $ Data.updateStatus uid status - liftSem $ Intra.onUserEvent uid Nothing (ev uid) + liftSem $ User.internalUpdateSearchIndex uid + liftSem $ Events.generateUserEvent uid Nothing (ev uid) mkUserEvent :: (Traversable t) => t UserId -> AccountStatus -> ExceptT AccountStatusError (AppT r) (UserId -> UserEvent) mkUserEvent usrs status = @@ -684,11 +683,7 @@ mkUserEvent usrs status = activate :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, + Member Events r, Member PasswordResetCodeStore r, Member UserSubsystem r ) => @@ -702,13 +697,9 @@ activate tgt code usr = activateWithCurrency tgt code usr Nothing activateWithCurrency :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, + Member Events r, Member PasswordResetCodeStore r, - Member UserSubsystem r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r ) => ActivationTarget -> ActivationCode -> @@ -751,11 +742,8 @@ preverify tgt code = do onActivated :: ( Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => ActivationEvent -> AppT r (UserId, Maybe UserIdentity, Bool) @@ -763,10 +751,12 @@ onActivated (AccountActivated account) = liftSem $ do let uid = userId (accountUser account) Log.debug $ field "user" (toByteString uid) . field "action" (val "User.onActivated") Log.info $ field "user" (toByteString uid) . msg (val "User activated") - Intra.onUserEvent uid Nothing $ UserActivated (accountUser account) + User.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing $ UserActivated (accountUser account) pure (uid, userIdentity (accountUser account), True) onActivated (EmailActivated uid email) = do - liftSem $ Intra.onUserEvent uid Nothing (emailUpdated uid email) + liftSem $ User.internalUpdateSearchIndex uid + liftSem $ Events.generateUserEvent uid Nothing (emailUpdated uid email) wrapHttpClient $ Data.deleteEmailUnvalidated uid pure (uid, Just (EmailIdentity email), False) @@ -878,13 +868,11 @@ deleteSelfUser :: Member (Embed HttpClientIO) r, Member UserKeyStore r, Member NotificationSubsystem r, - Member (Input (Local ())) r, Member PasswordStore r, Member UserStore r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member EmailSubsystem r, Member VerificationCodeSubsystem r, + Member Events r, Member UserSubsystem r, Member PropertySubsystem r ) => @@ -953,11 +941,9 @@ verifyDeleteUser :: Member NotificationSubsystem r, Member UserKeyStore r, Member TinyLog r, - Member (Input (Local ())) r, Member UserStore r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member VerificationCodeSubsystem r, + Member Events r, Member UserSubsystem r, Member PropertySubsystem r ) => @@ -983,10 +969,8 @@ ensureAccountDeleted :: Member NotificationSubsystem r, Member TinyLog r, Member UserKeyStore r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member UserStore r, + Member Events r, Member UserSubsystem r, Member PropertySubsystem r ) => @@ -1034,11 +1018,10 @@ deleteAccount :: Member NotificationSubsystem r, Member UserKeyStore r, Member TinyLog r, - Member (Input (Local ())) r, Member UserStore r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member PropertySubsystem r + Member PropertySubsystem r, + Member UserSubsystem r, + Member Events r ) => UserAccount -> Sem r () @@ -1056,7 +1039,8 @@ deleteAccount (accountUser -> user) = do Intra.rmUser uid (userAssets user) embed $ Data.lookupClients uid >>= mapM_ (Data.rmClient uid . clientId) luid <- embed $ qualifyLocal uid - Intra.onUserEvent uid Nothing (UserDeleted (tUntagged luid)) + User.internalUpdateSearchIndex uid + Events.generateUserEvent uid Nothing (UserDeleted (tUntagged luid)) embed do -- Note: Connections can only be deleted afterwards, since -- they need to be notified. diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 3d85954f241..80d38fbb7ea 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -39,6 +39,7 @@ module Brig.App cargoholdEndpoint, federator, casClient, + indexEnv, userTemplates, providerTemplates, teamTemplates, diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 65cf6132c0d..45722ef86df 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -5,8 +5,6 @@ import Brig.App as App import Brig.DeleteQueue.Interpreter as DQ import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.ConnectionStore.Cassandra (connectionStoreToCassandra) -import Brig.Effects.FederationConfigStore (FederationConfigStore) -import Brig.Effects.FederationConfigStore.Cassandra (interpretFederationDomainConfig, remotesMapFromCfgFile) import Brig.Effects.JwtTools import Brig.Effects.PublicKeyBundle import Brig.Effects.SFT (SFT, interpretSFT) @@ -16,6 +14,7 @@ import Brig.IO.Intra (runEvents) import Brig.Options (ImplicitNoFederationRestriction (federationDomainConfig), federationDomainConfigs, federationStrategy) import Brig.Options qualified as Opt import Brig.Team.Template (TeamTemplates) +import Brig.User.Search.Index (IndexEnv (..)) import Cassandra qualified as Cas import Control.Exception (ErrorCall) import Control.Lens (to, (^.)) @@ -50,10 +49,14 @@ import Wire.Error import Wire.Events import Wire.FederationAPIAccess qualified import Wire.FederationAPIAccess.Interpreter (FederationAPIAccessConfig (..), interpretFederationAPIAccess) +import Wire.FederationConfigStore (FederationConfigStore) +import Wire.FederationConfigStore.Cassandra (interpretFederationDomainConfig, remotesMapFromCfgFile) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess.Rpc import Wire.GundeckAPIAccess import Wire.HashPassword +import Wire.IndexedUserStore +import Wire.IndexedUserStore.ElasticSearch import Wire.InvitationCodeStore (InvitationCodeStore) import Wire.InvitationCodeStore.Cassandra (interpretInvitationCodeStoreToCassandra) import Wire.NotificationSubsystem @@ -73,6 +76,8 @@ import Wire.Sem.Concurrency.IO import Wire.Sem.Delay import Wire.Sem.Jwk import Wire.Sem.Logger.TinyLog (loggerToTinyLogReqId) +import Wire.Sem.Metrics +import Wire.Sem.Metrics.IO (runMetricsToIO) import Wire.Sem.Now (Now) import Wire.Sem.Now.IO (nowToIOAction) import Wire.Sem.Paging.Cassandra (InternalPaging) @@ -110,6 +115,7 @@ type BrigCanonicalEffects = HashPassword, UserKeyStore, UserStore, + IndexedUserStore, SessionStore, PasswordStore, VerificationCodeStore, @@ -138,6 +144,7 @@ type BrigCanonicalEffects = GalleyAPIAccess, EmailSending, Rpc, + Metrics, Embed Cas.Client, Error ParseException, Error ErrorCall, @@ -157,7 +164,8 @@ runBrigToIO e (AppT ma) = do let userSubsystemConfig = UserSubsystemConfig { emailVisibilityConfig = e ^. settings . Opt.emailVisibility, - defaultLocale = e ^. settings . to Opt.setDefaultUserLocale + defaultLocale = e ^. settings . to Opt.setDefaultUserLocale, + searchSameTeamOnly = e ^. settings . Opt.searchSameTeamOnly . to (fromMaybe False) } federationApiAccessConfig = FederationAPIAccessConfig @@ -172,6 +180,21 @@ runBrigToIO e (AppT ma) = do maxValueLength = fromMaybe Opt.defMaxValueLen $ e ^. settings . Opt.propertyMaxValueLen, maxProperties = 16 } + mainESEnv = e ^. App.indexEnv . to idxElastic + indexedUserStoreConfig = + IndexedUserStoreConfig + { conn = + ESConn + { env = mainESEnv, + indexName = e ^. App.indexEnv . to idxName + }, + additionalConn = + (e ^. App.indexEnv . to idxAdditionalName) <&> \additionalIndexName -> + ESConn + { env = e ^. App.indexEnv . to idxAdditionalElastic . to (fromMaybe mainESEnv), + indexName = additionalIndexName + } + } ( either throwM pure <=< ( runFinal . unsafelyPerformConcurrency @@ -185,6 +208,7 @@ runBrigToIO e (AppT ma) = do . mapError @ErrorCall SomeException . mapError @ParseException SomeException . interpretClientToIO (e ^. casClient) + . runMetricsToIO . runRpcWithHttp (e ^. httpManager) (e ^. App.requestId) . emailSendingInterpreter e . interpretGalleyAPIAccessToRpc (e ^. disabledVersions) (e ^. galleyEndpoint) @@ -193,11 +217,11 @@ runBrigToIO e (AppT ma) = do . runDelay . nowToIOAction (e ^. currentTime) . userPendingActivationStoreToCassandra - . interpretBlockListStoreToCassandra @Cas.Client + . interpretBlockListStoreToCassandra (e ^. casClient) . interpretJwtTools . interpretPublicKeyBundle . interpretJwk - . interpretFederationDomainConfig (e ^. settings . federationStrategy) (foldMap (remotesMapFromCfgFile . fmap (.federationDomainConfig)) (e ^. settings . federationDomainConfigs)) + . interpretFederationDomainConfig (e ^. casClient) (e ^. settings . federationStrategy) (foldMap (remotesMapFromCfgFile . fmap (.federationDomainConfig)) (e ^. settings . federationDomainConfigs)) . runGundeckAPIAccess (e ^. gundeckEndpoint) . runNotificationSubsystemGundeck (defaultNotificationSubsystemConfig (e ^. App.requestId)) . runInputConst (teamTemplatesNoLocale e) @@ -213,6 +237,7 @@ runBrigToIO e (AppT ma) = do . interpretVerificationCodeStoreCassandra (e ^. casClient) . interpretPasswordStore (e ^. casClient) . interpretSessionStoreCassandra (e ^. casClient) + . interpretIndexedUserStoreES indexedUserStoreConfig . interpretUserStoreCassandra (e ^. casClient) . interpretUserKeyStoreCassandra (e ^. casClient) . runHashPassword diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index f92e18bef38..8309190c88f 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -20,7 +20,7 @@ -- FUTUREWORK: Move to Brig.User.RPC or similar. module Brig.IO.Intra ( -- * Pushing & Journaling Events - onUserEvent, + sendUserEvent, onConnectionEvent, onPropertyEvent, onClientEvent, @@ -61,7 +61,6 @@ import Brig.Federation.Client (notifyUserDeleted, sendConnectionAction) import Brig.IO.Journal qualified as Journal import Brig.IO.Logging import Brig.RPC -import Brig.User.Search.Index qualified as Search import Control.Error (ExceptT, runExceptT) import Control.Lens (view, (.~), (?~), (^.), (^?)) import Control.Monad.Catch @@ -109,7 +108,7 @@ import Wire.Sem.Paging.Cassandra (InternalPaging) ----------------------------------------------------------------------------- -- Event Handlers -onUserEvent :: +sendUserEvent :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member TinyLog r, @@ -121,9 +120,8 @@ onUserEvent :: Maybe ConnId -> UserEvent -> Sem r () -onUserEvent orig conn e = - updateSearchIndex orig e - *> dispatchNotifications orig conn e +sendUserEvent orig conn e = + dispatchNotifications orig conn e *> embed (journalEvent orig e) runEvents :: @@ -137,7 +135,7 @@ runEvents :: InterpreterFor Events r runEvents = interpret \case -- FUTUREWORK(mangoiv): should this be in another module? - GenerateUserEvent uid mconnid event -> onUserEvent uid mconnid event + GenerateUserEvent uid mconnid event -> sendUserEvent uid mconnid event GeneratePropertyEvent uid connid event -> onPropertyEvent uid connid event onConnectionEvent :: @@ -192,36 +190,6 @@ onClientEvent orig conn e = do & pushApsData .~ toApsData event ] -updateSearchIndex :: - (Member (Embed HttpClientIO) r) => - UserId -> - UserEvent -> - Sem r () -updateSearchIndex orig e = embed $ case e of - -- no-ops - UserCreated {} -> pure () - UserIdentityUpdated UserIdentityUpdatedData {..} -> do - when (isJust eiuEmail) $ Search.reindex orig - UserIdentityRemoved {} -> pure () - UserLegalHoldDisabled {} -> pure () - UserLegalHoldEnabled {} -> pure () - LegalHoldClientRequested {} -> pure () - UserSuspended {} -> Search.reindex orig - UserResumed {} -> Search.reindex orig - UserActivated {} -> Search.reindex orig - UserDeleted {} -> Search.reindex orig - UserUpdated UserUpdatedData {..} -> do - let interesting = - or - [ isJust eupName, - isJust eupAccentId, - isJust eupHandle, - isJust eupManagedBy, - isJust eupSSOId || eupSSOIdRemoved, - isJust eupTeam - ] - when interesting $ Search.reindex orig - journalEvent :: (MonadReader Env m, MonadIO m) => UserId -> UserEvent -> m () journalEvent orig e = case e of UserActivated acc -> diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index c19d000c5d9..64dd9ebca59 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -23,13 +21,13 @@ module Brig.Index.Eval where import Brig.App (initHttpManagerWithTLSConfig, mkIndexEnv) -import Brig.Index.Migrations import Brig.Index.Options import Brig.Options import Brig.User.Search.Index -import Cassandra qualified as C +import Cassandra (Client, runClient) import Cassandra.Options import Cassandra.Util (defInitCassandra) +import Control.Exception (throwIO) import Control.Lens import Control.Monad.Catch import Control.Retry @@ -37,11 +35,121 @@ import Data.Aeson (FromJSON) import Data.Aeson qualified as Aeson import Data.ByteString.Lazy.UTF8 qualified as UTF8 import Data.Credentials (Credentials (..)) +import Data.Id import Database.Bloodhound qualified as ES +import Database.Bloodhound.Internal.Client (BHEnv (..)) import Imports +import Polysemy +import Polysemy.Embed (runEmbedded) +import Polysemy.Error +import Polysemy.TinyLog hiding (Logger) import System.Logger qualified as Log -import System.Logger.Class (Logger, MonadLogger (..)) +import System.Logger.Class (Logger) import Util.Options (initCredentials) +import Wire.API.Federation.Client (FederatorClient) +import Wire.API.Federation.Error +import Wire.BlockListStore (BlockListStore) +import Wire.BlockListStore.Cassandra +import Wire.FederationAPIAccess +import Wire.FederationAPIAccess.Interpreter (noFederationAPIAccess) +import Wire.FederationConfigStore (FederationConfigStore) +import Wire.FederationConfigStore.Cassandra (interpretFederationDomainConfig) +import Wire.GalleyAPIAccess +import Wire.GalleyAPIAccess.Rpc +import Wire.IndexedUserStore +import Wire.IndexedUserStore.Bulk (IndexedUserStoreBulk) +import Wire.IndexedUserStore.Bulk qualified as IndexedUserStoreBulk +import Wire.IndexedUserStore.Bulk.ElasticSearch (interpretIndexedUserStoreBulk) +import Wire.IndexedUserStore.ElasticSearch +import Wire.IndexedUserStore.MigrationStore (IndexedUserMigrationStore) +import Wire.IndexedUserStore.MigrationStore.ElasticSearch +import Wire.ParseException +import Wire.Rpc +import Wire.Sem.Concurrency +import Wire.Sem.Concurrency.IO +import Wire.Sem.Logger.TinyLog +import Wire.Sem.Metrics +import Wire.Sem.Metrics.IO +import Wire.UserKeyStore (UserKeyStore) +import Wire.UserKeyStore.Cassandra +import Wire.UserSearch.Migration (MigrationException) +import Wire.UserStore +import Wire.UserStore.Cassandra +import Wire.UserSubsystem.Error + +type BrigIndexEffectStack = + [ IndexedUserStoreBulk, + UserKeyStore, + BlockListStore, + Error UserSubsystemError, + FederationAPIAccess FederatorClient, + Error FederationError, + UserStore, + IndexedUserStore, + Error IndexedUserStoreError, + IndexedUserMigrationStore, + Error MigrationException, + FederationConfigStore, + GalleyAPIAccess, + Error ParseException, + Rpc, + Metrics, + TinyLog, + Concurrency 'Unsafe, + Embed IO, + Final IO + ] + +runSem :: ESConnectionSettings -> CassandraSettings -> Endpoint -> Logger -> Sem BrigIndexEffectStack a -> IO a +runSem esConn cas galleyEndpoint logger action = do + mgr <- initHttpManagerWithTLSConfig esConn.esInsecureSkipVerifyTls esConn.esCaCert + mEsCreds :: Maybe Credentials <- for esConn.esCredentials initCredentials + casClient <- defInitCassandra (toCassandraOpts cas) logger + let bhEnv = + BHEnv + { bhServer = toESServer esConn.esServer, + bhManager = mgr, + bhRequestHook = maybe pure (\creds -> ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)) mEsCreds + } + indexedUserStoreConfig = + IndexedUserStoreConfig + { conn = + ESConn + { indexName = esConn.esIndex, + env = bhEnv + }, + additionalConn = Nothing + } + reqId = (RequestId "brig-index") + runFinal + . embedToFinal + . unsafelyPerformConcurrency + . loggerToTinyLogReqId reqId logger + . ignoreMetrics + . runRpcWithHttp mgr reqId + . throwErrorToIOFinal @ParseException + . interpretGalleyAPIAccessToRpc mempty galleyEndpoint + . runEmbedded (runClient casClient) + . interpretFederationDomainConfig casClient Nothing mempty + . raiseUnder @(Embed Client) + . throwErrorToIOFinal @MigrationException + . interpretIndexedUserMigrationStoreES bhEnv + . throwErrorToIOFinal @IndexedUserStoreError + . interpretIndexedUserStoreES indexedUserStoreConfig + . interpretUserStoreCassandra casClient + . throwErrorToIOFinal @FederationError + . noFederationAPIAccess + . throwErrorToIOFinal @UserSubsystemError + . interpretBlockListStoreToCassandra casClient + . interpretUserKeyStoreCassandra casClient + . interpretIndexedUserStoreBulk + $ action + +throwErrorToIOFinal :: (Exception e, Member (Final IO) r) => InterpreterFor (Error e) r +throwErrorToIOFinal action = do + runError action >>= \case + Left e -> embedFinal $ throwIO e + Right a -> pure a runCommand :: Logger -> Command -> IO () runCommand l = \case @@ -52,18 +160,17 @@ runCommand l = \case e <- initIndex (es ^. esConnection) galley runIndexIO e $ resetIndex (mkCreateIndexSettings es) Reindex es cas galley -> do - e <- initIndex (es ^. esConnection) galley - c <- initDb cas - runReindexIO e c reindexAll + runSem (es ^. esConnection) cas galley l $ + IndexedUserStoreBulk.syncAllUsers ReindexSameOrNewer es cas galley -> do - e <- initIndex (es ^. esConnection) galley - c <- initDb cas - runReindexIO e c reindexAllIfSameOrNewer + runSem (es ^. esConnection) cas galley l $ + IndexedUserStoreBulk.forceSyncAllUsers UpdateMapping esConn galley -> do e <- initIndex esConn galley runIndexIO e updateMapping Migrate es cas galley -> do - migrate l es cas galley + runSem (es ^. esConnection) cas galley l $ + IndexedUserStoreBulk.migrateData ReindexFromAnotherIndex reindexSettings -> do mgr <- initHttpManagerWithTLSConfig @@ -87,7 +194,7 @@ runCommand l = \case Log.info l $ Log.msg ("Reindexing" :: ByteString) . Log.field "from" (show src) . Log.field "to" (show dest) eitherTaskNodeId <- ES.reindexAsync $ ES.mkReindexRequest src dest case eitherTaskNodeId of - Left err -> throwM $ ReindexFromAnotherIndexError $ "Error occurred while running reindex: " <> show err + Left e -> throwM $ ReindexFromAnotherIndexError $ "Error occurred while running reindex: " <> show e Right taskNodeId -> do Log.info l $ Log.field "taskNodeId" (show taskNodeId) waitForTaskToComplete @ES.ReindexResponse timeoutSeconds taskNodeId @@ -116,8 +223,6 @@ runCommand l = \case let env = ES.mkBHEnv (toESServer esURI) mgr in maybe env (\(creds :: Credentials) -> env {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)}) mCreds - initDb cas = defInitCassandra (toCassandraOpts cas) l - waitForTaskToComplete :: forall a m. (ES.MonadBH m, MonadThrow m, FromJSON a) => Int -> ES.TaskNodeId -> m () waitForTaskToComplete timeoutSeconds taskNodeId = do -- Delay is 0.1 seconds, so retries are limited to timeoutSeconds * 10 @@ -144,32 +249,3 @@ newtype ReindexFromAnotherIndexError = ReindexFromAnotherIndexError String deriving (Show) instance Exception ReindexFromAnotherIndexError - --------------------------------------------------------------------------------- --- ReindexIO command monad - -newtype ReindexIO a = ReindexIO (ReaderT C.ClientState IndexIO a) - deriving - ( Functor, - Applicative, - Monad, - MonadIO, - MonadReader C.ClientState, - MonadThrow, - MonadCatch - ) - -runReindexIO :: IndexEnv -> C.ClientState -> ReindexIO a -> IO a -runReindexIO ixe cas (ReindexIO ma) = runIndexIO ixe (runReaderT ma cas) - -instance MonadIndexIO ReindexIO where - liftIndexIO = ReindexIO . ReaderT . const - -instance C.MonadClient ReindexIO where - liftClient ma = ask >>= \e -> C.runClient e ma - localState = local - -instance MonadLogger ReindexIO where - log lvl msg = do - l <- ReindexIO . lift $ asks idxLogger - Log.log l lvl msg diff --git a/services/brig/src/Brig/Index/Migrations.hs b/services/brig/src/Brig/Index/Migrations.hs deleted file mode 100644 index 2fbb8ce5455..00000000000 --- a/services/brig/src/Brig/Index/Migrations.hs +++ /dev/null @@ -1,173 +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 Brig.Index.Migrations - ( migrate, - ) -where - -import Brig.App (initHttpManagerWithTLSConfig) -import Brig.Index.Migrations.Types -import Brig.Index.Options qualified as Opts -import Brig.User.Search.Index qualified as Search -import Cassandra.Util (defInitCassandra) -import Control.Lens (to, view, (^.)) -import Control.Monad.Catch (MonadThrow, catchAll, finally, throwM) -import Data.Aeson (Value, object, (.=)) -import Data.Credentials (Credentials (..)) -import Data.Text qualified as Text -import Database.Bloodhound qualified as ES -import Imports -import Network.HTTP.Client qualified as HTTP -import System.Logger.Class (Logger) -import System.Logger.Class qualified as Log -import System.Logger.Extended (runWithLogger) -import Util.Options qualified as Options - -migrate :: Logger -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO () -migrate l es cas galleyEndpoint = do - env <- mkEnv l es cas galleyEndpoint - finally (go env `catchAll` logAndThrowAgain) (cleanup env) - where - go :: Env -> IO () - go env = - runMigrationAction env $ do - failIfIndexAbsent (es ^. Opts.esConnection . to Opts.esIndex) - createMigrationsIndexIfNotPresent - runMigration expectedMigrationVersion - - logAndThrowAgain :: forall a. SomeException -> IO a - logAndThrowAgain e = do - runWithLogger l $ - Log.err $ - Log.msg (Log.val "Migration failed with exception") . Log.field "exception" (show e) - throwM e - --- | Increase this number any time you want to force reindexing. -expectedMigrationVersion :: MigrationVersion -expectedMigrationVersion = MigrationVersion 6 - -indexName :: ES.IndexName -indexName = ES.IndexName "wire_brig_migrations" - -indexMappingName :: ES.MappingName -indexMappingName = ES.MappingName "wire_brig_migrations" - -indexMapping :: Value -indexMapping = - object - [ "properties" - .= object - ["migration_version" .= object ["index" .= True, "type" .= ("integer" :: Text)]] - ] - -mkEnv :: Logger -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO Env -mkEnv l es cas galleyEndpoint = do - env <- do - esMgr <- initHttpManagerWithTLSConfig (es ^. Opts.esConnection . to Opts.esInsecureSkipVerifyTls) (es ^. Opts.esConnection . to Opts.esCaCert) - pure $ ES.mkBHEnv (Opts.toESServer (es ^. Opts.esConnection . to Opts.esServer)) esMgr - mCreds <- for (es ^. Opts.esConnection . to Opts.esCredentials) Options.initCredentials - let envWithAuth = maybe env (\(creds :: Credentials) -> env {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)}) mCreds - rpcMgr <- HTTP.newManager HTTP.defaultManagerSettings - Env envWithAuth - <$> initCassandra - <*> initLogger - <*> pure (view (Opts.esConnection . to Opts.esIndex) es) - <*> pure mCreds - <*> pure rpcMgr - <*> pure galleyEndpoint - where - initCassandra = defInitCassandra (Opts.toCassandraOpts cas) l - - initLogger = pure l - -createMigrationsIndexIfNotPresent :: (MonadThrow m, ES.MonadBH m, Log.MonadLogger m) => m () -createMigrationsIndexIfNotPresent = - do - unlessM (ES.indexExists indexName) $ do - Log.info $ - Log.msg (Log.val "Creating migrations index, used for tracking which migrations have run") - ES.createIndexWith [] 1 indexName - >>= throwIfNotCreated CreateMigrationIndexFailed - ES.putMapping indexName indexMappingName indexMapping - >>= throwIfNotCreated PutMappingFailed - where - throwIfNotCreated err response = - unless (ES.isSuccess response) $ - throwM $ - err (show response) - -failIfIndexAbsent :: (MonadThrow m, ES.MonadBH m) => ES.IndexName -> m () -failIfIndexAbsent targetIndex = - unlessM - (ES.indexExists targetIndex) - (throwM $ TargetIndexAbsent targetIndex) - --- | Runs only the migrations which need to run -runMigration :: MigrationVersion -> MigrationActionT IO () -runMigration expectedVersion = do - foundVersion <- latestMigrationVersion - if expectedVersion > foundVersion - then do - Log.info $ - Log.msg (Log.val "Migration necessary.") - . Log.field "expectedVersion" expectedVersion - . Log.field "foundVersion" foundVersion - Search.reindexAllIfSameOrNewer - persistVersion expectedVersion - else do - Log.info $ - Log.msg (Log.val "No migration necessary.") - . Log.field "expectedVersion" expectedVersion - . Log.field "foundVersion" foundVersion - -persistVersion :: (MonadThrow m, MonadIO m) => MigrationVersion -> MigrationActionT m () -persistVersion v = - let docId = ES.DocId . Text.pack . show $ migrationVersion v - in do - persistResponse <- ES.indexDocument indexName indexMappingName ES.defaultIndexDocumentSettings v docId - if ES.isCreated persistResponse - then do - Log.info $ - Log.msg (Log.val "Migration success recorded") - . Log.field "migrationVersion" v - else throwM $ PersistVersionFailed v $ show persistResponse - --- | Which version is the table space currently running on? -latestMigrationVersion :: (MonadThrow m, MonadIO m) => MigrationActionT m MigrationVersion -latestMigrationVersion = do - resp <- ES.parseEsResponse =<< ES.searchByIndex indexName (ES.mkSearch Nothing Nothing) - result <- either (throwM . FetchMigrationVersionsFailed . show) pure resp - let versions = map ES.hitSource $ ES.hits . ES.searchHits $ result - case versions of - [] -> - pure $ MigrationVersion 0 - vs -> - if any isNothing vs - then throwM $ VersionSourceMissing result - else pure $ maximum $ catMaybes vs - -data MigrationException - = CreateMigrationIndexFailed String - | FetchMigrationVersionsFailed String - | PersistVersionFailed MigrationVersion String - | PutMappingFailed String - | TargetIndexAbsent ES.IndexName - | VersionSourceMissing (ES.SearchResult MigrationVersion) - deriving (Show) - -instance Exception MigrationException diff --git a/services/brig/src/Brig/Index/Migrations/Types.hs b/services/brig/src/Brig/Index/Migrations/Types.hs deleted file mode 100644 index 7854ce67aae..00000000000 --- a/services/brig/src/Brig/Index/Migrations/Types.hs +++ /dev/null @@ -1,100 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE RecordWildCards #-} - --- 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 Brig.Index.Migrations.Types where - -import Brig.User.Search.Index qualified as Search -import Cassandra qualified as C -import Control.Monad.Catch (MonadThrow) -import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.=)) -import Data.Credentials (Credentials) -import Database.Bloodhound qualified as ES -import Imports -import Network.HTTP.Client (Manager) -import Numeric.Natural (Natural) -import System.Logger qualified as Logger -import System.Logger.Class (MonadLogger (..), ToBytes (..)) -import Util.Options (Endpoint) - -newtype MigrationVersion = MigrationVersion {migrationVersion :: Natural} - deriving (Show, Eq, Ord) - -instance ToJSON MigrationVersion where - toJSON (MigrationVersion v) = object ["migration_version" .= v] - -instance FromJSON MigrationVersion where - parseJSON = withObject "MigrationVersion" $ \o -> MigrationVersion <$> o .: "migration_version" - -instance ToBytes MigrationVersion where - bytes = bytes . toInteger . migrationVersion - -newtype MigrationActionT m a = MigrationActionT {unMigrationAction :: ReaderT Env m a} - deriving - ( Functor, - Applicative, - Monad, - MonadIO, - MonadThrow, - MonadReader Env - ) - -instance MonadTrans MigrationActionT where - lift = MigrationActionT . lift - -instance (MonadIO m, MonadThrow m) => C.MonadClient (MigrationActionT m) where - liftClient = liftCassandra - localState f = local (\env -> env {cassandraClientState = f $ cassandraClientState env}) - -instance (MonadIO m) => MonadLogger (MigrationActionT m) where - log level f = do - env <- ask - Logger.log (logger env) level f - -instance (MonadIO m) => Search.MonadIndexIO (MigrationActionT m) where - liftIndexIO m = do - Env {..} <- ask - let indexEnv = Search.IndexEnv logger bhEnv Nothing searchIndex Nothing Nothing galleyEndpoint httpManager searchIndexCredentials - Search.runIndexIO indexEnv m - -instance (MonadIO m) => ES.MonadBH (MigrationActionT m) where - getBHEnv = bhEnv <$> ask - -data Env = Env - { bhEnv :: ES.BHEnv, - cassandraClientState :: C.ClientState, - logger :: Logger.Logger, - searchIndex :: ES.IndexName, - searchIndexCredentials :: Maybe Credentials, - httpManager :: Manager, - galleyEndpoint :: Endpoint - } - -runMigrationAction :: Env -> MigrationActionT m a -> m a -runMigrationAction env action = - runReaderT (unMigrationAction action) env - -liftCassandra :: (MonadIO m) => C.Client a -> MigrationActionT m a -liftCassandra m = do - env <- ask - lift $ C.runClient (cassandraClientState env) m - -cleanup :: (MonadIO m) => Env -> m () -cleanup env = do - C.shutdown (cassandraClientState env) diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 8fa8e91ac7e..031de77e11f 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -19,7 +19,6 @@ module Brig.InternalEvent.Process (onEvent) where import Brig.API.User qualified as API import Brig.App -import Brig.Effects.ConnectionStore import Brig.IO.Intra (rmClient) import Brig.IO.Intra qualified as Intra import Brig.InternalEvent.Types @@ -29,19 +28,18 @@ import Control.Lens (view) import Control.Monad.Catch import Data.ByteString.Conversion import Data.Qualified (Local) -import Data.Time.Clock (UTCTime) import Imports import Polysemy -import Polysemy.Conc +import Polysemy.Conc hiding (Events) import Polysemy.Input (Input) import Polysemy.Time import Polysemy.TinyLog as Log import System.Logger.Class (field, msg, val, (~~)) import Wire.API.UserEvent +import Wire.Events (Events) import Wire.NotificationSubsystem import Wire.PropertySubsystem import Wire.Sem.Delay -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore (UserStore) import Wire.UserSubsystem @@ -50,18 +48,17 @@ import Wire.UserSubsystem -- -- Has a one-minute timeout that should be enough for anything that it does. onEvent :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + ( Member NotificationSubsystem r, Member TinyLog r, + Member (Embed HttpClientIO) r, Member Delay r, Member Race r, Member (Input (Local ())) r, Member UserKeyStore r, - Member (Input UTCTime) r, Member UserStore r, + Member PropertySubsystem r, Member UserSubsystem r, - Member (ConnectionStore InternalPaging) r, - Member PropertySubsystem r + Member Events r ) => InternalNotification -> Sem r () diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index b7ed00018e1..fcc674600f8 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -42,7 +42,6 @@ import Brig.Provider.DB (ServiceConn (..)) import Brig.Provider.DB qualified as DB import Brig.Provider.Email import Brig.Provider.RPC qualified as RPC -import Brig.Team.Util import Brig.ZAuth qualified as ZAuth import Cassandra (MonadClient) import Control.Error (throwE) @@ -81,6 +80,7 @@ import OpenSSL.PEM qualified as SSL import OpenSSL.RSA qualified as SSL import OpenSSL.Random (randBytes) import Polysemy +import Polysemy.Error import Servant (ServerT, (:<|>) (..)) import Ssl.Util qualified as SSL import System.Logger.Class (MonadLogger) @@ -123,6 +123,8 @@ import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe)) import Wire.UserKeyStore (mkEmailKey) +import Wire.UserSubsystem +import Wire.UserSubsystem.Error import Wire.VerificationCode as VerificationCode import Wire.VerificationCodeGen import Wire.VerificationCodeSubsystem @@ -147,7 +149,10 @@ botAPI = :<|> Named @"bot-get-user-clients" botGetUserClients servicesAPI :: - (Member GalleyAPIAccess r, Member DeleteQueue r) => + ( Member GalleyAPIAccess r, + Member DeleteQueue r, + Member (Error UserSubsystemError) r + ) => ServerT ServicesAPI (Handler r) servicesAPI = Named @"post-provider-services" addService @@ -163,7 +168,12 @@ servicesAPI = :<|> Named @"get-whitelisted-services-by-team-id" searchTeamServiceProfiles :<|> Named @"post-team-whitelist-by-team-id" updateServiceWhitelist -providerAPI :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => ServerT ProviderAPI (Handler r) +providerAPI :: + ( Member GalleyAPIAccess r, + Member EmailSending r, + Member VerificationCodeSubsystem r + ) => + ServerT ProviderAPI (Handler r) providerAPI = Named @"provider-register" newAccount :<|> Named @"provider-activate" activateAccountKey @@ -177,13 +187,23 @@ providerAPI = :<|> Named @"provider-get-account" getAccount :<|> Named @"provider-get-profile" getProviderProfile -internalProviderAPI :: (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => ServerT BrigIRoutes.ProviderAPI (Handler r) +internalProviderAPI :: + ( Member GalleyAPIAccess r, + Member VerificationCodeSubsystem r + ) => + ServerT BrigIRoutes.ProviderAPI (Handler r) internalProviderAPI = Named @"get-provider-activation-code" getActivationCodeH -------------------------------------------------------------------------------- -- Public API (Unauthenticated) -newAccount :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => Public.NewProvider -> (Handler r) Public.NewProviderResponse +newAccount :: + ( Member GalleyAPIAccess r, + Member EmailSending r, + Member VerificationCodeSubsystem r + ) => + Public.NewProvider -> + (Handler r) Public.NewProviderResponse newAccount new = do guardSecondFactorDisabled Nothing let email = (Public.newProviderEmail new) @@ -579,14 +599,22 @@ getServiceTagList _ = do where allTags = [(minBound :: Public.ServiceTag) ..] -updateServiceWhitelist :: (Member GalleyAPIAccess r) => UserId -> ConnId -> TeamId -> Public.UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp +updateServiceWhitelist :: + ( Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r + ) => + UserId -> + ConnId -> + TeamId -> + Public.UpdateServiceWhitelist -> + (Handler r) UpdateServiceWhitelistResp updateServiceWhitelist uid con tid upd = do guardSecondFactorDisabled (Just uid) let pid = updateServiceWhitelistProvider upd sid = updateServiceWhitelistService upd newWhitelisted = updateServiceWhitelistStatus upd -- Preconditions - ensurePermissions uid tid (Set.toList serviceWhitelistPermissions) + lift . liftSem $ ensurePermissions uid tid (Set.toList serviceWhitelistPermissions) _ <- wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound -- Add to various tables whitelisted <- wrapClientE $ DB.getServiceWhitelistStatus tid pid sid diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 255cc56a6dc..bdb0e911b45 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -33,13 +33,10 @@ import Brig.API.User qualified as API import Brig.API.Util (logEmail, logInvitationCode) import Brig.App as App import Brig.Data.User as User -import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) -import Brig.IO.Intra qualified as Intra import Brig.Options import Brig.Team.Email import Brig.Team.Template -import Brig.Team.Util (ensurePermissionToAddUser, ensurePermissions) import Brig.Types.Team (TeamSize) import Brig.User.Search.TeamSize qualified as TeamSize import Control.Lens (view, (^.)) @@ -53,11 +50,11 @@ import Data.Text.Ascii import Data.Text.Encoding (encodeUtf8) import Data.Text.Lazy qualified as LT import Data.Text.Lazy qualified as Text -import Data.Time.Clock (UTCTime) import Data.Tuple.Extra import Imports hiding (head) -import Network.Wai.Utilities hiding (code, message) +import Network.Wai.Utilities hiding (Error, code, message) import Polysemy +import Polysemy.Error import Polysemy.Input (Input, input) import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log @@ -87,17 +84,19 @@ import Wire.BlockListStore import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem.Template import Wire.Error +import Wire.Events (Events) +import Wire.Events qualified as Events import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..)) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.InvitationCodeStore (InvitationCodeStore (..), PaginatedResult (..), StoredInvitation (..)) import Wire.InvitationCodeStore qualified as Store import Wire.InvitationCodeStore.Cassandra qualified as Store (mkInvitationCode) -import Wire.NotificationSubsystem import Wire.PasswordStore import Wire.Sem.Concurrency -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserSubsystem +import Wire.UserSubsystem qualified as User +import Wire.UserSubsystem.Error servantAPI :: ( Member GalleyAPIAccess r, @@ -106,28 +105,36 @@ servantAPI :: Member Store.InvitationCodeStore r, Member EmailSending r, Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member PasswordStore r, - Member (Input TeamTemplates) r + Member (Input TeamTemplates) r, + Member Events r, + Member (Error UserSubsystemError) r ) => ServerT TeamsAPI (Handler r) servantAPI = Named @"send-team-invitation" createInvitation - :<|> Named @"get-team-invitations" listInvitations - :<|> Named @"get-team-invitation" getInvitation - :<|> Named @"delete-team-invitation" deleteInvitation + :<|> 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) :<|> Named @"get-team-invitation-info" getInvitationByCode - :<|> Named @"head-team-invitations" headInvitationByEmail + :<|> Named @"head-team-invitations" (lift . liftSem . headInvitationByEmail) :<|> Named @"get-team-size" teamSizePublic :<|> Named @"accept-team-invitation" acceptTeamInvitationByPersonalUser -teamSizePublic :: (Member GalleyAPIAccess r) => UserId -> TeamId -> (Handler r) TeamSize +teamSizePublic :: + ( Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r + ) => + UserId -> + TeamId -> + (Handler r) TeamSize teamSizePublic uid tid = do - ensurePermissions uid tid [AddTeamMember] -- limit this to team admins to reduce risk of involuntary DOS attacks + -- limit this to team admins to reduce risk of involuntary DOS attacks + lift . liftSem $ ensurePermissions uid tid [AddTeamMember] teamSize tid teamSize :: TeamId -> (Handler r) TeamSize @@ -156,7 +163,8 @@ createInvitation :: Member EmailSending r, Member TinyLog r, Member (Input (Local ())) r, - Member (Input TeamTemplates) r + Member (Input TeamTemplates) r, + Member (Error UserSubsystemError) r ) => UserId -> TeamId -> @@ -168,7 +176,7 @@ createInvitation uid tid body = do let inviteePerms = Teams.rolePermissions inviteeRole idt <- maybe (throwStd (errorToWai @'E.NoIdentity)) pure =<< lift (fetchUserIdentity uid) from <- maybe (throwStd (errorToWai @'E.NoEmail)) pure (emailIdentity idt) - ensurePermissionToAddUser uid tid inviteePerms + lift . liftSem $ ensurePermissionToAddUser uid tid inviteePerms pure $ CreateInvitationInviter uid from let context = @@ -320,14 +328,17 @@ isPersonalUser uke = do && isNothing account.accountUser.userTeam deleteInvitation :: - (Member GalleyAPIAccess r, Member InvitationCodeStore r) => + ( Member GalleyAPIAccess r, + Member InvitationCodeStore r, + Member (Error UserSubsystemError) r + ) => UserId -> TeamId -> InvitationId -> - (Handler r) () + Sem r () deleteInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] - lift . liftSem $ Store.deleteInvitation tid iid + Store.deleteInvitation tid iid listInvitations :: forall r. @@ -336,23 +347,24 @@ listInvitations :: Member InvitationCodeStore r, Member (Input TeamTemplates) r, Member (Input (Local ())) r, - Member UserSubsystem r + Member UserSubsystem r, + Member (Error UserSubsystemError) r ) => UserId -> TeamId -> Maybe InvitationId -> Maybe (Range 1 500 Int32) -> - (Handler r) Public.InvitationList + Sem r Public.InvitationList listInvitations uid tid startingId mSize = do ensurePermissions uid tid [AddTeamMember] - showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid + showInvitationUrl <- GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid let toInvitations is = mapM (toInvitationHack showInvitationUrl) is - lift (liftSem $ Store.lookupInvitationsPaginated mSize tid startingId) >>= \case + Store.lookupInvitationsPaginated mSize tid startingId >>= \case PaginatedResultHasMore storedInvs -> do - invs <- lift . liftSem $ toInvitations storedInvs + invs <- toInvitations storedInvs pure $ InvitationList invs True PaginatedResult storedInvs -> do - invs <- lift . liftSem $ toInvitations storedInvs + invs <- toInvitations storedInvs pure $ InvitationList invs False where -- To create the correct team invitation URL, we need to detect whether the invited account already exists. @@ -447,21 +459,22 @@ getInvitation :: ( Member GalleyAPIAccess r, Member InvitationCodeStore r, Member TinyLog r, - Member (Input TeamTemplates) r + Member (Input TeamTemplates) r, + Member (Error UserSubsystemError) r ) => UserId -> TeamId -> InvitationId -> - (Handler r) (Maybe Public.Invitation) + Sem r (Maybe Public.Invitation) getInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] - invitationM <- lift . liftSem $ Store.lookupInvitation tid iid + invitationM <- Store.lookupInvitation tid iid case invitationM of Nothing -> pure Nothing Just invitation -> do - showInvitationUrl <- lift . liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid - maybeUrl <- lift . liftSem $ mkInviteUrl showInvitationUrl tid invitation.code + showInvitationUrl <- GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid + maybeUrl <- mkInviteUrl showInvitationUrl tid invitation.code pure $ Just (Store.invitationFromStored maybeUrl invitation) getInvitationByCode :: @@ -472,18 +485,19 @@ getInvitationByCode c = do inv <- lift . liftSem $ Store.lookupInvitationByCode c maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) (pure . Store.invitationFromStored Nothing) inv -headInvitationByEmail :: (Member InvitationCodeStore r, Member TinyLog r) => EmailAddress -> (Handler r) Public.HeadInvitationByEmailResult +headInvitationByEmail :: + (Member InvitationCodeStore r, Member TinyLog r) => + EmailAddress -> + Sem r Public.HeadInvitationByEmailResult headInvitationByEmail email = - lift $ - liftSem $ - Store.lookupInvitationCodesByEmail email >>= \case - [] -> pure Public.InvitationByEmailNotFound - [_code] -> pure Public.InvitationByEmail - (_ : _ : _) -> do - Log.info $ - Log.msg (Log.val "team_invidation_email: multiple pending invites from different teams for the same email") - . Log.field "email" (show email) - pure Public.InvitationByEmailMoreThanOne + Store.lookupInvitationCodesByEmail email >>= \case + [] -> pure Public.InvitationByEmailNotFound + [_code] -> pure Public.InvitationByEmail + (_ : _ : _) -> do + Log.info $ + Log.msg (Log.val "team_invidation_email: multiple pending invites from different teams for the same email") + . Log.field "email" (show email) + pure Public.InvitationByEmailMoreThanOne -- | FUTUREWORK: This should also respond with status 409 in case of -- @DB.InvitationByEmailMoreThanOne@. Refactor so that 'headInvitationByEmailH' and @@ -498,13 +512,11 @@ getInvitationByEmail email = do suspendTeam :: ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, + Member UserSubsystem r, + Member Events r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member InvitationCodeStore r ) => TeamId -> @@ -521,13 +533,10 @@ suspendTeam tid = do unsuspendTeam :: ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => TeamId -> (Handler r) NoContent @@ -541,13 +550,10 @@ unsuspendTeam tid = do changeTeamAccountStatuses :: ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, Member GalleyAPIAccess r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => TeamId -> AccountStatus -> @@ -566,14 +572,9 @@ acceptTeamInvitationByPersonalUser :: forall r. ( Member UserSubsystem r, Member GalleyAPIAccess r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, - Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member InvitationCodeStore r, - Member PasswordStore r + Member PasswordStore r, + Member Events r ) => Local UserId -> AcceptTeamInvitation -> @@ -596,7 +597,8 @@ acceptTeamInvitationByPersonalUser luid req = do lift $ do wrapClient $ User.updateUserTeam uid tid liftSem $ Store.deleteInvitation inv.teamId inv.invitationId - liftSem $ Intra.onUserEvent uid Nothing (teamUpdated uid tid) + liftSem $ User.internalUpdateSearchIndex uid + liftSem $ Events.generateUserEvent uid Nothing (teamUpdated uid tid) where checkPassword = do p <- diff --git a/services/brig/src/Brig/Team/Util.hs b/services/brig/src/Brig/Team/Util.hs deleted file mode 100644 index a838a3c5fe8..00000000000 --- a/services/brig/src/Brig/Team/Util.hs +++ /dev/null @@ -1,68 +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 Brig.Team.Util where -- TODO: remove this module and move contents to Brig.IO.Intra? - -import Brig.API.Error -import Brig.App -import Brig.Data.User qualified as Data -import Control.Error -import Control.Lens -import Data.HavePendingInvitations -import Data.Id -import Data.Set qualified as Set -import Imports -import Polysemy (Member) -import Wire.API.Team.Member -import Wire.API.Team.Permission -import Wire.API.User (User (userTeam)) -import Wire.Error -import Wire.GalleyAPIAccess (GalleyAPIAccess) -import Wire.GalleyAPIAccess qualified as GalleyAPIAccess - --- | If the user is in a team, it has to have these permissions. If not, it is a personal --- user with account validation and thus given the permission implicitly. (Used for --- `SearchContactcs`.) -ensurePermissionsOrPersonalUser :: (Member GalleyAPIAccess r, IsPerm perm) => UserId -> [perm] -> ExceptT HttpError (AppT r) () -ensurePermissionsOrPersonalUser u perms = do - mbUser <- lift $ wrapHttp $ Data.lookupUser NoPendingInvitations u - maybe (pure ()) (\tid -> ensurePermissions u tid perms) (userTeam =<< mbUser :: Maybe TeamId) - -ensurePermissions :: (Member GalleyAPIAccess r, IsPerm perm) => UserId -> TeamId -> [perm] -> ExceptT HttpError (AppT r) () -ensurePermissions u t perms = do - m <- lift $ liftSem $ GalleyAPIAccess.getTeamMember u t - unless (check m) $ - throwStd insufficientTeamPermissions - where - check :: Maybe TeamMember -> Bool - check (Just m) = all (hasPermission m) perms - check Nothing = False - --- | Privilege escalation detection (make sure no `RoleMember` user creates a `RoleOwner`). --- --- There is some code duplication with 'Galley.API.Teams.ensureNotElevated'. -ensurePermissionToAddUser :: (Member GalleyAPIAccess r) => UserId -> TeamId -> Permissions -> ExceptT HttpError (AppT r) () -ensurePermissionToAddUser u t inviteePerms = do - minviter <- lift $ liftSem $ GalleyAPIAccess.getTeamMember u t - unless (check minviter) $ - throwStd insufficientTeamPermissions - where - check :: Maybe TeamMember -> Bool - check (Just inviter) = - hasPermission inviter AddTeamMember - && all (mayGrantPermission inviter) (Set.toList (inviteePerms ^. self)) - check Nothing = False diff --git a/services/brig/src/Brig/User/API/Search.hs b/services/brig/src/Brig/User/API/Search.hs deleted file mode 100644 index afb00c1efd6..00000000000 --- a/services/brig/src/Brig/User/API/Search.hs +++ /dev/null @@ -1,190 +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 Brig.User.API.Search - ( search, - teamUserSearch, - refreshIndex, - reindexAll, - reindexAllIfSameOrNewer, - ) -where - -import Brig.API.Error (fedError) -import Brig.API.Handler -import Brig.App -import Brig.Data.User qualified as DB -import Brig.Effects.FederationConfigStore -import Brig.Effects.FederationConfigStore qualified as E -import Brig.Federation.Client qualified as Federation -import Brig.Options qualified as Opts -import Brig.Team.Util (ensurePermissions, ensurePermissionsOrPersonalUser) -import Brig.Types.Search as Search -import Brig.User.API.Handle qualified as HandleAPI -import Brig.User.Search.Index -import Brig.User.Search.SearchIndex qualified as Q -import Brig.User.Search.TeamUserSearch qualified as Q -import Control.Lens (view) -import Data.Domain (Domain) -import Data.Handle qualified as Handle -import Data.Id -import Data.Range -import Imports -import Network.Wai.Utilities ((!>>)) -import Polysemy -import System.Logger (field, msg) -import System.Logger.Class (val, (~~)) -import System.Logger.Class qualified as Log -import Wire.API.Federation.API.Brig qualified as FedBrig -import Wire.API.Federation.API.Brig qualified as S -import Wire.API.Routes.FederationDomainConfig -import Wire.API.Team.Member (HiddenPerm (SearchContacts)) -import Wire.API.Team.Permission qualified as Public -import Wire.API.Team.SearchVisibility (TeamSearchVisibility (..)) -import Wire.API.User.Search -import Wire.API.User.Search qualified as Public -import Wire.GalleyAPIAccess (GalleyAPIAccess) -import Wire.GalleyAPIAccess qualified as GalleyAPIAccess -import Wire.UserStore (UserStore) -import Wire.UserSubsystem - --- FUTUREWORK: Consider augmenting 'SearchResult' with full user profiles --- for all results. This is tracked in https://wearezeta.atlassian.net/browse/SQCORE-599 -search :: - ( Member GalleyAPIAccess r, - Member FederationConfigStore r, - Member UserStore r, - Member UserSubsystem r - ) => - UserId -> - Text -> - Maybe Domain -> - Maybe (Range 1 500 Int32) -> - (Handler r) (Public.SearchResult Public.Contact) -search searcherId searchTerm maybeDomain maybeMaxResults = do - -- FUTUREWORK(fisx): to reduce cassandra traffic, 'ensurePermissionsOrPersonalUser' could be - -- run from `searchLocally` and `searchRemotely`, resp., where the team id is already - -- available (at least in the local case) and can be passed as an argument rather than - -- looked up again. - ensurePermissionsOrPersonalUser searcherId [SearchContacts] - federationDomain <- viewFederationDomain - mSearcherTeamId <- lift $ wrapClient $ DB.lookupUserTeam searcherId - let queryDomain = fromMaybe federationDomain maybeDomain - if queryDomain == federationDomain - then searchLocally searcherId searchTerm maybeMaxResults - else searchRemotely queryDomain mSearcherTeamId searchTerm - -searchRemotely :: (Member FederationConfigStore r) => Domain -> Maybe TeamId -> Text -> (Handler r) (Public.SearchResult Public.Contact) -searchRemotely domain mTid searchTerm = do - lift . Log.info $ - msg (val "searchRemotely") - ~~ field "domain" (show domain) - ~~ field "searchTerm" searchTerm - mFedCnf <- lift $ liftSem $ E.getFederationConfig domain - let onlyInTeams = case restriction <$> mFedCnf of - Just FederationRestrictionAllowAll -> Nothing - Just (FederationRestrictionByTeam teams) -> Just teams - -- if we are not federating at all, we also do not allow to search any remote teams - Nothing -> Just [] - - searchResponse <- Federation.searchUsers domain (FedBrig.SearchRequest searchTerm mTid onlyInTeams) !>> fedError - let contacts = S.contacts searchResponse - let count = length contacts - pure - SearchResult - { searchResults = contacts, - searchFound = count, - searchReturned = count, - searchTook = 0, - searchPolicy = S.searchPolicy searchResponse, - searchPagingState = Nothing, - searchHasMore = Nothing - } - -searchLocally :: - forall r. - ( Member GalleyAPIAccess r, - Member UserSubsystem r, - Member UserStore r - ) => - UserId -> - Text -> - Maybe (Range 1 500 Int32) -> - (Handler r) (Public.SearchResult Public.Contact) -searchLocally searcherId searchTerm maybeMaxResults = do - let maxResults = maybe 15 (fromIntegral . fromRange) maybeMaxResults - searcherTeamId <- lift $ wrapClient $ DB.lookupUserTeam searcherId - teamSearchInfo <- mkTeamSearchInfo searcherTeamId - - maybeExactHandleMatch <- exactHandleSearch - - let exactHandleMatchCount = length maybeExactHandleMatch - esMaxResults = maxResults - exactHandleMatchCount - - esResult <- - if esMaxResults > 0 - then Q.searchIndex (Q.LocalSearch searcherId searcherTeamId teamSearchInfo) searchTerm esMaxResults - else pure $ SearchResult 0 0 0 [] FullSearch Nothing Nothing - - -- Prepend results matching exact handle and results from ES. - pure $ - esResult - { searchResults = maybeToList maybeExactHandleMatch <> searchResults esResult, - searchFound = exactHandleMatchCount + searchFound esResult, - searchReturned = exactHandleMatchCount + searchReturned esResult - } - where - handleTeamVisibility :: TeamId -> TeamSearchVisibility -> Search.TeamSearchInfo - handleTeamVisibility _ SearchVisibilityStandard = Search.AllUsers - handleTeamVisibility t SearchVisibilityNoNameOutsideTeam = Search.TeamOnly t - - mkTeamSearchInfo :: Maybe TeamId -> (Handler r) TeamSearchInfo - mkTeamSearchInfo searcherTeamId = lift $ do - sameTeamSearchOnly <- fromMaybe False <$> view (settings . Opts.searchSameTeamOnly) - case searcherTeamId of - Nothing -> pure Search.NoTeam - Just t -> - -- This flag in brig overrules any flag on galley - it is system wide - if sameTeamSearchOnly - then pure (Search.TeamOnly t) - else do - -- For team users, we need to check the visibility flag - handleTeamVisibility t <$> liftSem (GalleyAPIAccess.getTeamSearchVisibility t) - - exactHandleSearch :: (Handler r) (Maybe Contact) - exactHandleSearch = do - lsearcherId <- qualifyLocal searcherId - case Handle.parseHandle searchTerm of - Nothing -> pure Nothing - Just handle -> do - HandleAPI.contactFromProfile - <$$> HandleAPI.getLocalHandleInfo lsearcherId handle - -teamUserSearch :: - (Member GalleyAPIAccess r) => - UserId -> - TeamId -> - Maybe Text -> - Maybe RoleFilter -> - Maybe TeamUserSearchSortBy -> - Maybe TeamUserSearchSortOrder -> - Maybe (Range 1 500 Int32) -> - Maybe PagingState -> - (Handler r) (Public.SearchResult Public.TeamContact) -teamUserSearch uid tid mQuery mRoleFilter mSortBy mSortOrder size mPagingState = do - ensurePermissions uid tid [Public.AddTeamMember] -- limit this to team admins to reduce risk of involuntary DOS attacks. (also, this way we don't need to worry about revealing confidential user data to other team members.) - Q.teamUserSearch tid mQuery mRoleFilter mSortBy mSortOrder (fromMaybe (unsafeRange 15) size) mPagingState diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 03b4fd7895a..5511a7750b2 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -41,7 +41,6 @@ import Brig.Budget import Brig.Data.Activation qualified as Data import Brig.Data.Client import Brig.Data.User qualified as Data -import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Options qualified as Opt import Brig.Types.Intra import Brig.User.Auth.Cookie @@ -63,7 +62,7 @@ import Data.ZAuth.Token qualified as ZAuth import Imports import Network.Wai.Utilities.Error ((!>>)) import Polysemy -import Polysemy.Input (Input) +import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log import System.Logger (field, msg, val, (~~)) @@ -74,11 +73,10 @@ import Wire.API.User import Wire.API.User.Auth import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.Sso +import Wire.Events (Events) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess -import Wire.NotificationSubsystem import Wire.PasswordStore (PasswordStore) -import Wire.Sem.Paging.Cassandra (InternalPaging) import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem (UserSubsystem) @@ -92,16 +90,13 @@ login :: forall r. ( Member GalleyAPIAccess r, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r, Member PasswordStore r, Member UserKeyStore r, Member UserStore r, + Member VerificationCodeSubsystem r, + Member (Input (Local ())) r, Member UserSubsystem r, - Member VerificationCodeSubsystem r + Member Events r ) => Login -> CookieType -> @@ -202,11 +197,8 @@ renewAccess :: forall r u a. ( ZAuth.TokenPair u a, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => List1 (ZAuth.Token u) -> Maybe (ZAuth.Token a) -> @@ -243,12 +235,9 @@ revokeAccess luid@(tUnqualified -> u) pw cc ll = do -- Internal catchSuspendInactiveUser :: - ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + ( Member TinyLog r, + Member UserSubsystem r, + Member Events r ) => UserId -> e -> @@ -273,11 +262,8 @@ newAccess :: forall u a r. ( ZAuth.TokenPair u a, Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => UserId -> Maybe ClientId -> @@ -390,11 +376,8 @@ validateToken ut at = do -- | Allow to login as any user without having the credentials. ssoLogin :: ( Member TinyLog r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => SsoLogin -> CookieType -> @@ -416,12 +399,9 @@ ssoLogin (SsoLogin uid label) typ = do -- | Log in as a LegalHold service, getting LegalHoldUser/Access Tokens. legalHoldLogin :: ( Member GalleyAPIAccess r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member UserSubsystem r, + Member Events r ) => LegalHoldLogin -> CookieType -> diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index afaca2554fd..a067b296324 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -19,9 +19,7 @@ -- with this program. If not, see . module Brig.User.Search.Index - ( mappingName, - boolQuery, - _TextId, + ( boolQuery, -- * Monad IndexEnv (..), @@ -29,78 +27,41 @@ module Brig.User.Search.Index runIndexIO, MonadIndexIO (..), - -- * Updates - reindex, - updateSearchVisibilityInbound, - -- * Administrative createIndex, createIndexIfNotPresent, resetIndex, - reindexAll, - reindexAllIfSameOrNewer, refreshIndex, updateMapping, -- * Re-exports - module Types, ES.IndexSettings (..), ES.IndexName (..), ) where -import Bilge (expect2xx, header, lbytes, paths) import Bilge.IO (MonadHttp) import Bilge.IO qualified as RPC -import Bilge.RPC (RPCException (RPCException)) -import Bilge.Request qualified as RPC (empty, host, method, port) -import Bilge.Response (responseJsonThrow) -import Bilge.Retry (rpcHandlers) import Brig.Index.Types (CreateIndexSettings (..)) -import Brig.Types.Search (SearchVisibilityInbound, defaultSearchVisibilityInbound, searchVisibilityInboundFromFeatureStatus) -import Brig.User.Search.Index.Types as Types -import Cassandra.CQL qualified as C -import Cassandra.Exec qualified as C -import Cassandra.Util import Control.Lens hiding ((#), (.=)) -import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, throwM, try) +import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, throwM) import Control.Monad.Except -import Control.Retry (RetryPolicy, exponentialBackoff, limitRetries, recovering) import Data.Aeson as Aeson -import Data.Aeson.Encoding -import Data.Aeson.Lens -import Data.ByteString (toStrict) -import Data.ByteString.Builder (Builder, toLazyByteString) -import Data.ByteString.Conversion (toByteString') -import Data.ByteString.Conversion qualified as Bytes -import Data.ByteString.Lazy qualified as BL import Data.Credentials -import Data.Handle (Handle) import Data.Id import Data.Map qualified as Map -import Data.Text qualified as T import Data.Text qualified as Text import Data.Text.Encoding -import Data.Text.Encoding.Error -import Data.Text.Lazy qualified as LT -import Data.Text.Lens hiding (text) -import Data.UUID qualified as UUID import Database.Bloodhound qualified as ES import Imports hiding (log, searchable) import Network.HTTP.Client hiding (host, path, port) -import Network.HTTP.Types (StdMethod (POST), hContentType, statusCode) +import Network.HTTP.Types (statusCode) import Prometheus (MonadMonitor) -import Prometheus qualified as Prom -import SAML2.WebSSO.Types qualified as SAML import System.Logger qualified as Log import System.Logger.Class (Logger, MonadLogger (..), field, info, msg, val, (+++), (~~)) -import URI.ByteString (URI, serializeURIRef) -import Util.Options (Endpoint, host, port) -import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi -import Wire.API.Team.Feature (SearchVisibilityInboundConfig, featureNameBS) -import Wire.API.User -import Wire.API.User qualified as User -import Wire.API.User.Search (Sso (..)) +import Util.Options (Endpoint) +import Wire.IndexedUserStore (IndexedUserStoreError (..)) +import Wire.UserSearch.Types (searchVisibilityInboundFieldName) -------------------------------------------------------------------------------- -- IndexIO Monad @@ -158,141 +119,6 @@ instance MonadHttp IndexIO where manager <- asks idxRpcHttpManager liftIO $ withResponse req manager handler -withDefaultESUrl :: (MonadIndexIO m) => ES.BH m a -> m a -withDefaultESUrl action = do - bhEnv <- liftIndexIO $ asks idxElastic - ES.runBH bhEnv action - --- | When the additional URL is not provided, uses the default url. -withAdditionalESUrl :: (MonadIndexIO m) => ES.BH m a -> m a -withAdditionalESUrl action = do - mAdditionalBHEnv <- liftIndexIO $ asks idxAdditionalElastic - defaultBHEnv <- liftIndexIO $ asks idxElastic - ES.runBH (fromMaybe defaultBHEnv mAdditionalBHEnv) action - --------------------------------------------------------------------------------- --- Updates - -reindex :: (MonadLogger m, MonadIndexIO m, C.MonadClient m) => UserId -> m () -reindex u = do - ixu <- lookupIndexUser u - updateIndex (maybe (IndexDeleteUser u) (IndexUpdateUser IndexUpdateIfNewerVersion) ixu) - -updateIndex :: (MonadIndexIO m) => IndexUpdate -> m () -updateIndex (IndexUpdateUser updateType iu) = liftIndexIO $ do - Prom.incCounter indexUpdateCounter - info $ - field "user" (Bytes.toByteString (view iuUserId iu)) - . msg (val "Indexing user") - idx <- asks idxName - withDefaultESUrl $ indexDoc idx - withAdditionalESUrl $ traverse_ indexDoc =<< asks idxAdditionalName - where - indexDoc :: (MonadIndexIO m, MonadThrow m) => ES.IndexName -> ES.BH m () - indexDoc idx = do - r <- ES.indexDocument idx mappingName versioning (indexToDoc iu) docId - unless (ES.isSuccess r || ES.isVersionConflict r) $ do - liftIO $ Prom.incCounter indexUpdateErrorCounter - ES.parseEsResponse r >>= throwM . IndexUpdateError . either id id - liftIO $ Prom.incCounter indexUpdateSuccessCounter - versioning = - ES.defaultIndexDocumentSettings - { ES.idsVersionControl = indexUpdateToVersionControl updateType (ES.ExternalDocVersion (docVersion (_iuVersion iu))) - } - docId = ES.DocId (view (iuUserId . re _TextId) iu) -updateIndex (IndexUpdateUsers updateType ius) = liftIndexIO $ do - Prom.incCounter indexBulkUpdateCounter - info $ - field "num_users" (length ius) - . msg (val "Bulk indexing users") - -- Sadly, 'bloodhound' is not aware of the versioning capabilities of ES' - -- bulk API, thus we need to stitch everything together by hand. - bhe <- ES.getBHEnv - ES.IndexName idx <- asks idxName - let (ES.MappingName mpp) = mappingName - let (ES.Server base) = ES.bhServer bhe - req <- parseRequest (view unpacked $ base <> "/" <> idx <> "/" <> mpp <> "/_bulk") - authHeaders <- mkAuthHeaders - res <- - liftIO $ - httpLbs - req - { method = "POST", - requestHeaders = [(hContentType, "application/x-ndjson")] <> authHeaders, -- sic - requestBody = RequestBodyLBS (toLazyByteString (foldMap bulkEncode ius)) - } - (ES.bhManager bhe) - unless (ES.isSuccess res) $ do - Prom.incCounter indexBulkUpdateErrorCounter - ES.parseEsResponse res >>= throwM . IndexUpdateError . either id id - Prom.incCounter indexBulkUpdateSuccessCounter - for_ (statuses res) $ \(s, f) -> - Prom.withLabel indexBulkUpdateResponseCounter (Text.pack $ show s) $ (void . flip Prom.addCounter (fromIntegral f)) - where - mkAuthHeaders = do - creds <- asks idxCredentials - pure $ maybe [] ((: []) . mkBasicAuthHeader) creds - - encodeJSONToString :: (ToJSON a) => a -> Builder - encodeJSONToString = fromEncoding . toEncoding - bulkEncode iu = - bulkMeta (view (iuUserId . re _TextId) iu) (docVersion (_iuVersion iu)) - <> "\n" - <> encodeJSONToString (indexToDoc iu) - <> "\n" - bulkMeta :: Text -> ES.DocVersion -> Builder - bulkMeta docId v = - fromEncoding . pairs . pair "index" . pairs $ - "_id" .= docId - <> "_version" .= v - -- "external_gt or external_gte" - <> "_version_type" .= indexUpdateToVersionControlText updateType - statuses :: ES.Reply -> [(Int, Int)] -- [(Status, Int)] - statuses = - Map.toList - . Map.fromListWith (+) - . flip zip [1, 1 ..] - . toListOf (key "items" . values . key "index" . key "status" . _Integral) - . responseBody -updateIndex (IndexDeleteUser u) = liftIndexIO $ do - Prom.incCounter indexDeleteCounter - info $ - field "user" (Bytes.toByteString u) - . msg (val "(Soft) deleting user from index") - idx <- asks idxName - r <- ES.getDocument idx mappingName (ES.DocId (review _TextId u)) - case statusCode (responseStatus r) of - 200 -> case preview (key "_version" . _Integer) (responseBody r) of - Nothing -> throwM $ ES.EsProtocolException "'version' not found" (responseBody r) - Just v -> updateIndex . IndexUpdateUser IndexUpdateIfNewerVersion . mkIndexUser u =<< mkIndexVersion (v + 1) - 404 -> pure () - _ -> ES.parseEsResponse r >>= throwM . IndexUpdateError . either id id - -updateSearchVisibilityInbound :: (MonadIndexIO m) => Multi.TeamStatus SearchVisibilityInboundConfig -> m () -updateSearchVisibilityInbound status = liftIndexIO $ do - withDefaultESUrl . updateAllDocs =<< asks idxName - withAdditionalESUrl $ traverse_ updateAllDocs =<< asks idxAdditionalName - where - updateAllDocs :: (MonadIndexIO m, MonadThrow m) => ES.IndexName -> ES.BH m () - updateAllDocs idx = do - r <- ES.updateByQuery idx query (Just script) - unless (ES.isSuccess r || ES.isVersionConflict r) $ do - ES.parseEsResponse r >>= throwM . IndexUpdateError . either id id - - query :: ES.Query - query = ES.TermQuery (ES.Term "team" $ idToText (Multi.team status)) Nothing - - script :: ES.Script - script = ES.Script (Just (ES.ScriptLanguage "painless")) (Just (ES.ScriptInline scriptText)) Nothing Nothing - - -- Unfortunately ES disallows updating ctx._version with a "Update By Query" - scriptText = - "ctx._source." - <> searchVisibilityInboundFieldName - <> " = '" - <> decodeUtf8 (toByteString' (searchVisibilityInboundFromFeatureStatus (Multi.status status))) - <> "';" - -------------------------------------------------------------------------------- -- Administrative @@ -395,44 +221,9 @@ resetIndex ciSettings = liftIndexIO $ do then createIndex ciSettings else throwM (IndexError "Index deletion failed.") -reindexAllIfSameOrNewer :: (MonadLogger m, MonadIndexIO m, C.MonadClient m) => m () -reindexAllIfSameOrNewer = reindexAllWith IndexUpdateIfSameOrNewerVersion - -reindexAll :: (MonadLogger m, MonadIndexIO m, C.MonadClient m) => m () -reindexAll = reindexAllWith IndexUpdateIfNewerVersion - -reindexAllWith :: (MonadLogger m, MonadIndexIO m, C.MonadClient m) => IndexDocUpdateType -> m () -reindexAllWith updateType = do - idx <- liftIndexIO $ asks idxName - C.liftClient (scanForIndex 1000) >>= loop idx - where - loop idx page = do - info $ - field "size" (length (C.result page)) - . msg (val "Reindex: processing C* page") - unless (null (C.result page)) $ do - let teamsInPage = mapMaybe teamInReindexRow (C.result page) - lookupFn <- liftIndexIO $ getSearchVisibilityInboundMulti teamsInPage - let reindexRow row = - let sv = maybe defaultSearchVisibilityInbound lookupFn (teamInReindexRow row) - in reindexRowToIndexUser row sv - indexUsers <- mapM reindexRow (C.result page) - updateIndex (IndexUpdateUsers updateType indexUsers) - when (C.hasMore page) $ - C.liftClient (C.nextPage page) >>= loop idx - -------------------------------------------------------------------------------- -- Internal --- This is useful and necessary due to the lack of expressiveness in the bulk API -indexUpdateToVersionControlText :: IndexDocUpdateType -> Text -indexUpdateToVersionControlText IndexUpdateIfNewerVersion = "external_gt" -indexUpdateToVersionControlText IndexUpdateIfSameOrNewerVersion = "external_gte" - -indexUpdateToVersionControl :: IndexDocUpdateType -> (ES.ExternalDocVersion -> ES.VersionControl) -indexUpdateToVersionControl IndexUpdateIfNewerVersion = ES.ExternalGT -indexUpdateToVersionControl IndexUpdateIfSameOrNewerVersion = ES.ExternalGTE - traceES :: (MonadIndexIO m) => ByteString -> IndexIO ES.Reply -> m ES.Reply traceES descr act = liftIndexIO $ do info (msg descr) @@ -587,7 +378,7 @@ indexMapping = mpAnalyzer = Nothing, mpFields = mempty }, - (fromString . T.unpack $ searchVisibilityInboundFieldName) + searchVisibilityInboundFieldName .= MappingProperty { mpType = MPKeyword, mpStore = False, @@ -681,283 +472,6 @@ instance ToJSON MappingField where boolQuery :: ES.BoolQuery boolQuery = ES.mkBoolQuery [] [] [] [] -_TextId :: Prism' Text (Id a) -_TextId = prism' (UUID.toText . toUUID) (fmap Id . UUID.fromText) - -mappingName :: ES.MappingName -mappingName = ES.MappingName "user" - -lookupIndexUser :: - (MonadIndexIO m, C.MonadClient m) => - UserId -> - m (Maybe IndexUser) -lookupIndexUser = lookupForIndex - -lookupForIndex :: (C.MonadClient m, MonadIndexIO m) => UserId -> m (Maybe IndexUser) -lookupForIndex u = do - mrow <- C.retry C.x1 (C.query1 cql (C.params C.LocalQuorum (Identity u))) - for mrow $ \row -> do - let mteam = teamInReindexRow row - searchVis <- liftIndexIO $ getSearchVisibilityInbound mteam - reindexRowToIndexUser row searchVis - where - cql :: C.PrepQuery C.R (Identity UserId) ReindexRow - cql = - "SELECT \ - \id, \ - \team, \ - \writetime(team), \ - \name, \ - \writetime(name), \ - \status, \ - \writetime(status), \ - \handle, \ - \writetime(handle), \ - \email, \ - \writetime(email), \ - \accent_id, \ - \writetime(accent_id), \ - \activated, \ - \writetime(activated), \ - \service, \ - \writetime(service), \ - \managed_by, \ - \writetime(managed_by), \ - \sso_id, \ - \writetime(sso_id), \ - \email_unvalidated, \ - \writetime(email_unvalidated) \ - \FROM user \ - \WHERE id = ?" - -getSearchVisibilityInbound :: - Maybe TeamId -> - IndexIO SearchVisibilityInbound -getSearchVisibilityInbound Nothing = pure defaultSearchVisibilityInbound -getSearchVisibilityInbound (Just tid) = do - searchVisibilityInboundFromStatus <$> getTeamSearchVisibilityInbound tid - -getSearchVisibilityInboundMulti :: [TeamId] -> IndexIO (TeamId -> SearchVisibilityInbound) -getSearchVisibilityInboundMulti tids = do - Multi.TeamFeatureNoConfigMultiResponse teamsStatuses <- getTeamSearchVisibilityInboundMulti tids - let lookupMap = Map.fromList (teamsStatuses <&> \x -> (Multi.team x, x)) - pure $ \tid -> - searchVisibilityInboundFromStatus (tid `Map.lookup` lookupMap) - -searchVisibilityInboundFromStatus :: Maybe (Multi.TeamStatus SearchVisibilityInboundConfig) -> SearchVisibilityInbound -searchVisibilityInboundFromStatus = \case - Nothing -> defaultSearchVisibilityInbound - Just tvi -> searchVisibilityInboundFromFeatureStatus . Multi.status $ tvi - -scanForIndex :: Int32 -> C.Client (C.Page ReindexRow) -scanForIndex num = do - C.paginate cql (C.paramsP C.One () (num + 1)) - where - cql :: C.PrepQuery C.R () ReindexRow - cql = - "SELECT \ - \id, \ - \team, \ - \writetime(team), \ - \name, \ - \writetime(name), \ - \status, \ - \writetime(status), \ - \handle, \ - \writetime(handle), \ - \email, \ - \writetime(email), \ - \accent_id, \ - \writetime(accent_id), \ - \activated, \ - \writetime(activated), \ - \service, \ - \writetime(service), \ - \managed_by, \ - \writetime(managed_by), \ - \sso_id, \ - \writetime(sso_id), \ - \email_unvalidated, \ - \writetime(email_unvalidated) \ - \FROM user" - -type Activated = Bool - -type ReindexRow = - ( UserId, - Maybe TeamId, - Maybe (Writetime TeamId), - Name, - Writetime Name, - Maybe AccountStatus, - Maybe (Writetime AccountStatus), - Maybe Handle, - Maybe (Writetime Handle), - Maybe EmailAddress, - Maybe (Writetime EmailAddress), - ColourId, - Writetime ColourId, - Activated, - Writetime Activated, - Maybe ServiceId, - Maybe (Writetime ServiceId), - Maybe ManagedBy, - Maybe (Writetime ManagedBy), - Maybe UserSSOId, - Maybe (Writetime UserSSOId), - Maybe EmailAddress, - Maybe (Writetime EmailAddress) - ) - --- the _2 lens does not work for a tuple this big -teamInReindexRow :: ReindexRow -> Maybe TeamId -teamInReindexRow (_f1, f2, _f3, _f4, _f5, _f6, _f7, _f8, _f9, _f10, _f11, _f12, _f13, _f14, _f15, _f16, _f17, _f18, _f19, _f20, _f21, _f22, _f23) = f2 - -reindexRowToIndexUser :: forall m. (MonadThrow m) => ReindexRow -> SearchVisibilityInbound -> m IndexUser -reindexRowToIndexUser - ( u, - mteam, - tTeam, - name, - tName, - status, - tStatus, - handle, - tHandle, - email, - tEmail, - colour, - tColour, - activated, - tActivated, - service, - tService, - managedBy, - tManagedBy, - ssoId, - tSsoId, - emailUnvalidated, - tEmailUnvalidated - ) - searchVisInbound = - do - iu <- - mkIndexUser u - <$> version - [ Just (v tName), - v <$> tStatus, - v <$> tHandle, - v <$> tEmail, - Just (v tColour), - Just (v tActivated), - v <$> tService, - v <$> tManagedBy, - v <$> tSsoId, - v <$> tEmailUnvalidated, - v <$> tTeam - ] - pure $ - if shouldIndex - then - iu - & set iuTeam mteam - . set iuName (Just name) - . set iuHandle handle - . set iuEmail email - . set iuColourId (Just colour) - . set iuAccountStatus status - . set iuSAMLIdP (idpUrl =<< ssoId) - . set iuManagedBy managedBy - . set iuCreatedAt (Just (writetimeToUTC tActivated)) - . set iuSearchVisibilityInbound (Just searchVisInbound) - . set iuScimExternalId (join $ User.scimExternalId <$> managedBy <*> ssoId) - . set iuSso (sso =<< ssoId) - . set iuEmailUnvalidated emailUnvalidated - else - iu - -- We insert a tombstone-style user here, as it's easier than deleting the old one. - -- It's mostly empty, but having the status here might be useful in the future. - & set iuAccountStatus status - where - v :: Writetime a -> Int64 - v = writetimeToInt64 - - version :: [Maybe Int64] -> m IndexVersion - version = mkIndexVersion . getMax . mconcat . fmap Max . catMaybes - - shouldIndex = - ( case status of - Nothing -> True - Just Active -> True - Just Suspended -> True - Just Deleted -> False - Just Ephemeral -> False - Just PendingInvitation -> False - ) - && activated -- FUTUREWORK: how is this adding to the first case? - && isNothing service - idpUrl :: UserSSOId -> Maybe Text - idpUrl (UserSSOId (SAML.UserRef (SAML.Issuer uri) _subject)) = - Just $ fromUri uri - idpUrl (UserScimExternalId _) = Nothing - - fromUri :: URI -> Text - fromUri = - decodeUtf8With lenientDecode - . toStrict - . toLazyByteString - . serializeURIRef - - sso :: UserSSOId -> Maybe Sso - sso userSsoId = do - (issuer, nameid) <- User.ssoIssuerAndNameId userSsoId - pure $ Sso {ssoIssuer = issuer, ssoNameId = nameid} - -getTeamSearchVisibilityInbound :: - TeamId -> - IndexIO (Maybe (Multi.TeamStatus SearchVisibilityInboundConfig)) -getTeamSearchVisibilityInbound tid = do - Multi.TeamFeatureNoConfigMultiResponse teamsStatuses <- getTeamSearchVisibilityInboundMulti [tid] - case filter ((== tid) . Multi.team) teamsStatuses of - [teamStatus] -> pure (Just teamStatus) - _ -> pure Nothing - -getTeamSearchVisibilityInboundMulti :: - [TeamId] -> - IndexIO (Multi.TeamFeatureNoConfigMultiResponse SearchVisibilityInboundConfig) -getTeamSearchVisibilityInboundMulti tids = do - galley <- asks idxGalley - serviceRequest' "galley" galley POST req >>= responseJsonThrow (ParseException "galley") - where - req = - paths ["i", "features-multi-teams", featureNameBS @SearchVisibilityInboundConfig] - . header "Content-Type" "application/json" - . expect2xx - . lbytes (encode $ Multi.TeamFeatureNoConfigMultiRequest tids) - - serviceRequest' :: - forall m. - (MonadIO m, MonadMask m, MonadHttp m) => - LT.Text -> - Endpoint -> - StdMethod -> - (Request -> Request) -> - m (Response (Maybe BL.ByteString)) - serviceRequest' nm endpoint m r = do - let service = mkEndpoint endpoint - recovering x3 rpcHandlers $ - const $ do - let rq = (RPC.method m . r) service - res <- try $ RPC.httpLbs rq id - case res of - Left x -> throwM $ RPCException nm rq x - Right x -> pure x - where - mkEndpoint service = RPC.host (encodeUtf8 (service ^. host)) . RPC.port (service ^. port) $ RPC.empty - - x3 :: RetryPolicy - x3 = limitRetries 3 <> exponentialBackoff 100000 - data ParseException = ParseException { _parseExceptionRemote :: !Text, _parseExceptionMsg :: String @@ -971,87 +485,3 @@ instance Show ParseException where ++ m instance Exception ParseException - ---------------------------------------------------------------------------------- --- Metrics - -{-# NOINLINE indexUpdateCounter #-} -indexUpdateCounter :: Prom.Counter -indexUpdateCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_count", - Prom.metricHelp = "Number of updates on user index" - } - -{-# NOINLINE indexUpdateErrorCounter #-} -indexUpdateErrorCounter :: Prom.Counter -indexUpdateErrorCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_err", - Prom.metricHelp = "Number of errors during user index update" - } - -{-# NOINLINE indexUpdateSuccessCounter #-} -indexUpdateSuccessCounter :: Prom.Counter -indexUpdateSuccessCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_ok", - Prom.metricHelp = "Number of successful user index updates" - } - -{-# NOINLINE indexBulkUpdateCounter #-} -indexBulkUpdateCounter :: Prom.Counter -indexBulkUpdateCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_bulk_count", - Prom.metricHelp = "Number of bulk updates on user index" - } - -{-# NOINLINE indexBulkUpdateErrorCounter #-} -indexBulkUpdateErrorCounter :: Prom.Counter -indexBulkUpdateErrorCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_bulk_err", - Prom.metricHelp = "Number of errors during bulk updates on user index" - } - -{-# NOINLINE indexBulkUpdateSuccessCounter #-} -indexBulkUpdateSuccessCounter :: Prom.Counter -indexBulkUpdateSuccessCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_bulk_ok", - Prom.metricHelp = "Number of successful bulk updates on user index" - } - -{-# NOINLINE indexBulkUpdateResponseCounter #-} -indexBulkUpdateResponseCounter :: Prom.Vector Prom.Label1 Prom.Counter -indexBulkUpdateResponseCounter = - Prom.unsafeRegister $ - Prom.vector ("status") $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_update_bulk_response", - Prom.metricHelp = "Number of successful bulk updates on user index" - } - -{-# NOINLINE indexDeleteCounter #-} -indexDeleteCounter :: Prom.Counter -indexDeleteCounter = - Prom.unsafeRegister $ - Prom.counter - Prom.Info - { Prom.metricName = "user_index_delete_count", - Prom.metricHelp = "Number of deletes on user index" - } diff --git a/services/brig/src/Brig/User/Search/Index/Types.hs b/services/brig/src/Brig/User/Search/Index/Types.hs deleted file mode 100644 index 2630842be4d..00000000000 --- a/services/brig/src/Brig/User/Search/Index/Types.hs +++ /dev/null @@ -1,230 +0,0 @@ -{-# LANGUAGE StrictData #-} -{-# LANGUAGE TemplateHaskell #-} - --- 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 Brig.User.Search.Index.Types where - -import Brig.Types.Search -import Control.Lens (makeLenses) -import Control.Monad.Catch -import Data.Aeson -import Data.Handle (Handle) -import Data.Id -import Data.Json.Util (UTCTimeMillis (..), toUTCTimeMillis) -import Data.Text qualified as T -import Data.Text.ICU.Translit (trans, transliterate) -import Data.Time (UTCTime) -import Database.Bloodhound hiding (key) -import Database.Bloodhound.Internal.Client (DocVersion (DocVersion)) -import Imports -import Wire.API.Team.Role (Role) -import Wire.API.User -import Wire.API.User.Search (Sso (..)) - -data IndexDocUpdateType - = IndexUpdateIfNewerVersion - | IndexUpdateIfSameOrNewerVersion - -data IndexUpdate - = IndexUpdateUser IndexDocUpdateType IndexUser - | IndexUpdateUsers IndexDocUpdateType [IndexUser] - | IndexDeleteUser UserId - --- | Represents the ES *index*, ie. the attributes of a user that is searchable in ES. See also: --- 'UserDoc'. -data IndexUser = IndexUser - { _iuUserId :: UserId, - _iuVersion :: IndexVersion, - _iuTeam :: Maybe TeamId, - _iuName :: Maybe Name, - _iuHandle :: Maybe Handle, - _iuEmail :: Maybe EmailAddress, - _iuColourId :: Maybe ColourId, - _iuAccountStatus :: Maybe AccountStatus, - _iuSAMLIdP :: Maybe Text, - _iuManagedBy :: Maybe ManagedBy, - _iuCreatedAt :: Maybe UTCTime, - _iuRole :: Maybe Role, - _iuSearchVisibilityInbound :: Maybe SearchVisibilityInbound, - _iuScimExternalId :: Maybe Text, - _iuSso :: Maybe Sso, - _iuEmailUnvalidated :: Maybe EmailAddress - } - -data IndexQuery r = IndexQuery Query Filter [DefaultSort] - -data IndexError - = IndexUpdateError EsError - | IndexLookupError EsError - | IndexError Text - deriving (Show) - -instance Exception IndexError - -newtype IndexVersion = IndexVersion {docVersion :: DocVersion} - --- | Represents an ES *document*, ie. the subset of user attributes stored in ES. --- See also 'IndexUser'. --- --- If a user is not searchable, e.g. because the account got --- suspended, all fields except for the user id are set to 'Nothing' and --- consequently removed from the index. -data UserDoc = UserDoc - { udId :: UserId, - udTeam :: Maybe TeamId, - udName :: Maybe Name, - udNormalized :: Maybe Text, - udHandle :: Maybe Handle, - udEmail :: Maybe EmailAddress, - udColourId :: Maybe ColourId, - udAccountStatus :: Maybe AccountStatus, - udSAMLIdP :: Maybe Text, - udManagedBy :: Maybe ManagedBy, - udCreatedAt :: Maybe UTCTimeMillis, - udRole :: Maybe Role, - udSearchVisibilityInbound :: Maybe SearchVisibilityInbound, - udScimExternalId :: Maybe Text, - udSso :: Maybe Sso, - udEmailUnvalidated :: Maybe EmailAddress - } - deriving (Eq, Show) - --- Note: Keep this compatible with the FromJSON instances --- of 'Contact' and 'TeamContact' from 'Wire.API.User.Search -instance ToJSON UserDoc where - toJSON ud = - object - [ "id" .= udId ud, - "team" .= udTeam ud, - "name" .= udName ud, - "normalized" .= udNormalized ud, - "handle" .= udHandle ud, - "email" .= udEmail ud, - "accent_id" .= udColourId ud, - "account_status" .= udAccountStatus ud, - "saml_idp" .= udSAMLIdP ud, - "managed_by" .= udManagedBy ud, - "created_at" .= udCreatedAt ud, - "role" .= udRole ud, - (fromString . T.unpack $ searchVisibilityInboundFieldName) .= udSearchVisibilityInbound ud, - "scim_external_id" .= udScimExternalId ud, - "sso" .= udSso ud, - "email_unvalidated" .= udEmailUnvalidated ud - ] - -instance FromJSON UserDoc where - parseJSON = withObject "UserDoc" $ \o -> - UserDoc - <$> o .: "id" - <*> o .:? "team" - <*> o .:? "name" - <*> o .:? "normalized" - <*> o .:? "handle" - <*> o .:? "email" - <*> o .:? "accent_id" - <*> o .:? "account_status" - <*> o .:? "saml_idp" - <*> o .:? "managed_by" - <*> o .:? "created_at" - <*> o .:? "role" - <*> o .:? (fromString . T.unpack $ searchVisibilityInboundFieldName) - <*> o .:? "scim_external_id" - <*> o .:? "sso" - <*> o .:? "email_unvalidated" - -searchVisibilityInboundFieldName :: Text -searchVisibilityInboundFieldName = "search_visibility_inbound" - -makeLenses ''IndexUser - -mkIndexVersion :: (MonadThrow m, Integral a) => a -> m IndexVersion -mkIndexVersion i = - if i > fromIntegral (maxBound :: Int) - then throwM $ IndexError "Index overflow" - else pure . IndexVersion . fromMaybe maxBound . mkDocVersion . fromIntegral $ i - -mkIndexUser :: UserId -> IndexVersion -> IndexUser -mkIndexUser u v = - IndexUser - { _iuUserId = u, - _iuVersion = v, - _iuTeam = Nothing, - _iuName = Nothing, - _iuHandle = Nothing, - _iuEmail = Nothing, - _iuColourId = Nothing, - _iuAccountStatus = Nothing, - _iuSAMLIdP = Nothing, - _iuManagedBy = Nothing, - _iuCreatedAt = Nothing, - _iuRole = Nothing, - _iuSearchVisibilityInbound = Nothing, - _iuScimExternalId = Nothing, - _iuSso = Nothing, - _iuEmailUnvalidated = Nothing - } - -indexToDoc :: IndexUser -> UserDoc -indexToDoc iu = - UserDoc - { udId = _iuUserId iu, - udTeam = _iuTeam iu, - udName = _iuName iu, - udAccountStatus = _iuAccountStatus iu, - udNormalized = normalized . fromName <$> _iuName iu, - udHandle = _iuHandle iu, - udEmail = _iuEmail iu, - udColourId = _iuColourId iu, - udSAMLIdP = _iuSAMLIdP iu, - udManagedBy = _iuManagedBy iu, - udCreatedAt = toUTCTimeMillis <$> _iuCreatedAt iu, - udRole = _iuRole iu, - udSearchVisibilityInbound = _iuSearchVisibilityInbound iu, - udScimExternalId = _iuScimExternalId iu, - udSso = _iuSso iu, - udEmailUnvalidated = _iuEmailUnvalidated iu - } - --- | FUTUREWORK: Transliteration should be left to ElasticSearch (ICU plugin), but this will --- require a data migration. -normalized :: Text -> Text -normalized = transliterate (trans "Any-Latin; Latin-ASCII; Lower") - -docToIndex :: UserDoc -> IndexUser -docToIndex ud = - -- (Don't use 'mkIndexUser' here! With 'IndexUser', you get compiler warnings if you - -- forget to add new fields here.) - IndexUser - { _iuUserId = udId ud, - _iuVersion = IndexVersion (DocVersion 1), - _iuTeam = udTeam ud, - _iuName = udName ud, - _iuHandle = udHandle ud, - _iuEmail = udEmail ud, - _iuColourId = udColourId ud, - _iuAccountStatus = udAccountStatus ud, - _iuSAMLIdP = udSAMLIdP ud, - _iuManagedBy = udManagedBy ud, - _iuCreatedAt = fromUTCTimeMillis <$> udCreatedAt ud, - _iuRole = udRole ud, - _iuSearchVisibilityInbound = udSearchVisibilityInbound ud, - _iuScimExternalId = udScimExternalId ud, - _iuSso = udSso ud, - _iuEmailUnvalidated = udEmailUnvalidated ud - } diff --git a/services/brig/src/Brig/User/Search/SearchIndex.hs b/services/brig/src/Brig/User/Search/SearchIndex.hs index 82b76637976..f45006c8387 100644 --- a/services/brig/src/Brig/User/Search/SearchIndex.hs +++ b/services/brig/src/Brig/User/Search/SearchIndex.hs @@ -25,10 +25,10 @@ module Brig.User.Search.SearchIndex where import Brig.App (Env, viewFederationDomain) -import Brig.Types.Search import Brig.User.Search.Index import Control.Lens hiding (setting, (#), (.=)) import Control.Monad.Catch (MonadThrow, throwM) +import Data.Aeson.Key qualified as Key import Data.Domain (Domain) import Data.Handle (Handle (fromHandle)) import Data.Id @@ -37,13 +37,17 @@ import Database.Bloodhound qualified as ES import Imports hiding (log, searchable) import Wire.API.User (ColourId (..), Name (fromName)) import Wire.API.User.Search +import Wire.IndexedUserStore (IndexedUserStoreError (..)) +import Wire.IndexedUserStore.ElasticSearch (mappingName) +import Wire.UserSearch.Types +import Wire.UserStore.IndexUser (normalized) --- | User that is performing the search --- Team of user that is performing the search --- Outgoing search restrictions data SearchSetting = FederatedSearch (Maybe [TeamId]) - | LocalSearch + | -- | User that is performing the search + -- Team of user that is performing the search + -- Outgoing search restrictions + LocalSearch UserId (Maybe TeamId) TeamSearchInfo @@ -186,7 +190,7 @@ termQ f v = matchSelf :: SearchSetting -> Maybe ES.Query matchSelf (FederatedSearch _) = Nothing -matchSelf (LocalSearch searcher _tid _searchInfo) = Just (termQ "_id" (review _TextId searcher)) +matchSelf (LocalSearch searcher _tid _searchInfo) = Just (termQ "_id" (idToText searcher)) -- | See 'TeamSearchInfo' restrictSearchSpace :: SearchSetting -> ES.Query @@ -244,7 +248,7 @@ matchTeamMembersSearchableByAllTeams = boolQuery { ES.boolQueryMustMatch = [ ES.QueryExistsQuery $ ES.FieldName "team", - ES.TermQuery (ES.Term searchVisibilityInboundFieldName "searchable-by-all-teams") Nothing + ES.TermQuery (ES.Term (Key.toText searchVisibilityInboundFieldName) "searchable-by-all-teams") Nothing ] } diff --git a/services/brig/src/Brig/User/Search/TeamSize.hs b/services/brig/src/Brig/User/Search/TeamSize.hs index dce653ab03b..6121ec38178 100644 --- a/services/brig/src/Brig/User/Search/TeamSize.hs +++ b/services/brig/src/Brig/User/Search/TeamSize.hs @@ -28,6 +28,7 @@ import Control.Monad.Catch (throwM) import Data.Id import Database.Bloodhound qualified as ES import Imports hiding (log, searchable) +import Wire.IndexedUserStore (IndexedUserStoreError (..)) teamSize :: (MonadIndexIO m) => TeamId -> m TeamSize teamSize t = liftIndexIO $ do diff --git a/services/brig/src/Brig/User/Search/TeamUserSearch.hs b/services/brig/src/Brig/User/Search/TeamUserSearch.hs deleted file mode 100644 index 90bcb969e96..00000000000 --- a/services/brig/src/Brig/User/Search/TeamUserSearch.hs +++ /dev/null @@ -1,175 +0,0 @@ -{-# LANGUAGE StrictData #-} -{-# OPTIONS_GHC -Wno-orphans #-} --- Disabling to stop warnings on HasCallStack -{-# OPTIONS_GHC -Wno-redundant-constraints #-} - --- 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 Brig.User.Search.TeamUserSearch - ( teamUserSearch, - teamUserSearchQuery, - TeamUserSearchSortBy (..), - TeamUserSearchSortOrder (..), - RoleFilter (..), - ) -where - -import Brig.User.Search.Index -import Control.Error (lastMay) -import Control.Monad.Catch (MonadThrow (throwM)) -import Data.Aeson (decode', encode) -import Data.ByteString (fromStrict, toStrict) -import Data.Id (TeamId, idToText) -import Data.Range (Range (..)) -import Data.Text.Ascii (decodeBase64Url, encodeBase64Url) -import Database.Bloodhound qualified as ES -import Imports hiding (log, searchable) -import Wire.API.User.Search - -teamUserSearch :: - (HasCallStack, MonadIndexIO m) => - TeamId -> - Maybe Text -> - Maybe RoleFilter -> - Maybe TeamUserSearchSortBy -> - Maybe TeamUserSearchSortOrder -> - Range 1 500 Int32 -> - Maybe PagingState -> - m (SearchResult TeamContact) -teamUserSearch tid mbSearchText mRoleFilter mSortBy mSortOrder (fromRange -> size) mPagingState = liftIndexIO $ do - let (IndexQuery q f sortSpecs) = teamUserSearchQuery tid mbSearchText mRoleFilter mSortBy mSortOrder - idx <- asks idxName - let search = - (ES.mkSearch (Just q) (Just f)) - { -- we are requesting one more result than the page size to determine if there is a next page - ES.size = ES.Size (fromIntegral size + 1), - ES.sortBody = Just (fmap ES.DefaultSortSpec sortSpecs), - ES.searchAfterKey = toSearchAfterKey =<< mPagingState - } - r <- - ES.searchByType idx mappingName search - >>= ES.parseEsResponse - either (throwM . IndexLookupError) (pure . mkResult) r - where - toSearchAfterKey :: PagingState -> Maybe ES.SearchAfterKey - toSearchAfterKey ps = decode' . fromStrict =<< (decodeBase64Url . unPagingState $ ps) - - fromSearchAfterKey :: ES.SearchAfterKey -> PagingState - fromSearchAfterKey = PagingState . encodeBase64Url . toStrict . encode - - mkResult es = - let hitsPlusOne = ES.hits . ES.searchHits $ es - hits = take (fromIntegral size) hitsPlusOne - mps = fromSearchAfterKey <$> lastMay (mapMaybe ES.hitSort hits) - results = mapMaybe ES.hitSource hits - in SearchResult - { searchFound = ES.hitsTotal . ES.searchHits $ es, - searchReturned = length results, - searchTook = ES.took es, - searchResults = results, - searchPolicy = FullSearch, - searchPagingState = mps, - searchHasMore = Just $ length hitsPlusOne > length hits - } - --- FUTURWORK: Implement role filter (needs galley data) -teamUserSearchQuery :: - TeamId -> - Maybe Text -> - Maybe RoleFilter -> - Maybe TeamUserSearchSortBy -> - Maybe TeamUserSearchSortOrder -> - IndexQuery TeamContact -teamUserSearchQuery tid mbSearchText _mRoleFilter mSortBy mSortOrder = - IndexQuery - ( maybe - (ES.MatchAllQuery Nothing) - matchPhraseOrPrefix - mbQStr - ) - teamFilter - -- in combination with pagination a non-unique search specification can lead to missing results - -- therefore we use the unique `_doc` value as a tie breaker - -- - see https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-sort.html for details on `_doc` - -- - see https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-search-after.html for details on pagination and tie breaker - -- in the latter article it "is advised to duplicate (client side or [...]) the content of the _id field - -- in another field that has doc value enabled and to use this new field as the tiebreaker for the sort" - -- so alternatively we could use the user ID as a tie breaker, but this would require a change in the index mapping - (sorting ++ sortingTieBreaker) - where - sorting :: [ES.DefaultSort] - sorting = - maybe - [defaultSort SortByCreatedAt SortOrderDesc | isNothing mbQStr] - (\tuSortBy -> [defaultSort tuSortBy (fromMaybe SortOrderAsc mSortOrder)]) - mSortBy - sortingTieBreaker :: [ES.DefaultSort] - sortingTieBreaker = [ES.DefaultSort (ES.FieldName "_doc") ES.Ascending Nothing Nothing Nothing Nothing] - - mbQStr :: Maybe Text - mbQStr = - case mbSearchText of - Nothing -> Nothing - Just q -> - case normalized q of - "" -> Nothing - term' -> Just term' - - matchPhraseOrPrefix term' = - ES.QueryMultiMatchQuery $ - ( ES.mkMultiMatchQuery - [ ES.FieldName "email^4", - ES.FieldName "handle^4", - ES.FieldName "normalized^3", - ES.FieldName "email.prefix^3", - ES.FieldName "handle.prefix^2", - ES.FieldName "normalized.prefix" - ] - (ES.QueryString term') - ) - { ES.multiMatchQueryType = Just ES.MultiMatchMostFields, - ES.multiMatchQueryOperator = ES.And - } - - teamFilter = - ES.Filter $ - ES.QueryBoolQuery - boolQuery - { ES.boolQueryMustMatch = [ES.TermQuery (ES.Term "team" $ idToText tid) Nothing] - } - - defaultSort :: TeamUserSearchSortBy -> TeamUserSearchSortOrder -> ES.DefaultSort - defaultSort tuSortBy sortOrder = - ES.DefaultSort - ( case tuSortBy of - SortByName -> ES.FieldName "name" - SortByHandle -> ES.FieldName "handle.keyword" - SortByEmail -> ES.FieldName "email.keyword" - SortBySAMLIdp -> ES.FieldName "saml_idp" - SortByManagedBy -> ES.FieldName "managed_by" - SortByRole -> ES.FieldName "role" - SortByCreatedAt -> ES.FieldName "created_at" - ) - ( case sortOrder of - SortOrderAsc -> ES.Ascending - SortOrderDesc -> ES.Descending - ) - Nothing - Nothing - Nothing - Nothing diff --git a/services/brig/test/unit/Run.hs b/services/brig/test/unit/Run.hs index 6d658acb536..a371d3130cc 100644 --- a/services/brig/test/unit/Run.hs +++ b/services/brig/test/unit/Run.hs @@ -25,7 +25,6 @@ import Test.Brig.Calling qualified import Test.Brig.Calling.Internal qualified import Test.Brig.InternalNotification qualified import Test.Brig.MLS qualified -import Test.Brig.User.Search.Index.Types qualified import Test.Tasty main :: IO () @@ -33,8 +32,7 @@ main = defaultMain $ testGroup "Tests" - [ Test.Brig.User.Search.Index.Types.tests, - Test.Brig.Calling.tests, + [ Test.Brig.Calling.tests, Test.Brig.Calling.Internal.tests, Test.Brig.MLS.tests, Test.Brig.InternalNotification.tests diff --git a/services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs b/services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs deleted file mode 100644 index 5e6af3a3d0d..00000000000 --- a/services/brig/test/unit/Test/Brig/User/Search/Index/Types.hs +++ /dev/null @@ -1,84 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- 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.Brig.User.Search.Index.Types where - -import Brig.User.Search.Index -import Data.Aeson -import Data.Fixed -import Data.Handle -import Data.Id -import Data.Json.Util -import Data.Time.Clock -import Data.Time.Clock.POSIX -import Data.UUID -import Imports -import Test.Tasty -import Test.Tasty.HUnit -import Wire.API.Team.Role -import Wire.API.User - -tests :: TestTree -tests = - testGroup - "UserDoc, IndexUser: conversion, serialization" - [ testCase "aeson roundtrip: UserDoc" $ - assertEqual - "failed" - (eitherDecode' (encode userDoc1)) - (Right userDoc1), - testCase "backwards comptibility test: UserDoc" $ - assertBool "failed" (isRight (eitherDecode' userDoc1ByteString :: Either String UserDoc)), - testCase "IndexUser to UserDoc" $ - assertEqual - "failed" - (indexToDoc indexUser1) - userDoc1 - ] - -mkTime :: Int -> UTCTime -mkTime = posixSecondsToUTCTime . secondsToNominalDiffTime . MkFixed . (* 1000000000) . fromIntegral - -userDoc1 :: UserDoc -userDoc1 = - UserDoc - { udId = Id . fromJust . fromText $ "0a96b396-57d6-11ea-a04b-7b93d1a5c19c", - udTeam = Just . Id . fromJust . fromText $ "17c59b18-57d6-11ea-9220-8bbf5eee961a", - udName = Just . Name $ "Carl Phoomp", - udNormalized = Just $ "carl phoomp", - udHandle = Just . fromJust . parseHandle $ "phoompy", - udEmail = Just $ unsafeEmailAddress "phoompy" "example.com", - udColourId = Just . ColourId $ 32, - udAccountStatus = Just Active, - udSAMLIdP = Just "https://issuer.net/214234", - udManagedBy = Just ManagedByScim, - udCreatedAt = Just (toUTCTimeMillis (mkTime 1598737800000)), - udRole = Just RoleAdmin, - udSearchVisibilityInbound = Nothing, - udScimExternalId = Nothing, - udSso = Nothing, - udEmailUnvalidated = Nothing - } - --- Dont touch this. This represents serialized legacy data. -userDoc1ByteString :: LByteString -userDoc1ByteString = "{\"email\":\"phoompy@example.com\",\"account_status\":\"active\",\"handle\":\"phoompy\",\"managed_by\":\"scim\",\"role\":\"admin\",\"accent_id\":32,\"name\":\"Carl Phoomp\",\"created_at\":\"2020-08-29T21:50:00.000Z\",\"team\":\"17c59b18-57d6-11ea-9220-8bbf5eee961a\",\"id\":\"0a96b396-57d6-11ea-a04b-7b93d1a5c19c\",\"normalized\":\"carl phoomp\",\"saml_idp\":\"https://issuer.net/214234\"}" - -indexUser1 :: IndexUser -indexUser1 = docToIndex userDoc1 diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 0fb76ff1384..abec40e3606 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -580,7 +580,6 @@ executable galley-migrate-data , exceptions , extended , imports - , lens , optparse-applicative , text , time diff --git a/services/galley/migrate-data/src/V1_BackfillBillingTeamMembers.hs b/services/galley/migrate-data/src/V1_BackfillBillingTeamMembers.hs index 7d46e3f8f13..6903c066cc1 100644 --- a/services/galley/migrate-data/src/V1_BackfillBillingTeamMembers.hs +++ b/services/galley/migrate-data/src/V1_BackfillBillingTeamMembers.hs @@ -19,7 +19,6 @@ module V1_BackfillBillingTeamMembers where import Cassandra import Conduit -import Control.Lens (view) import Data.Conduit.Internal (zipSources) import Data.Conduit.List qualified as C import Data.Id @@ -70,5 +69,5 @@ createBillingTeamMembers pair = cql = "INSERT INTO billing_team_member (team, user) values (?, ?)" isOwner :: (TeamId, UserId, Maybe Permissions) -> Bool -isOwner (_, _, Just p) = SetBilling `Set.member` view self p +isOwner (_, _, Just p) = SetBilling `Set.member` p.self isOwner _ = False diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 02994a77a90..df3015be471 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -1256,8 +1256,8 @@ ensureNonBindingTeam tid = do ensureNotElevated :: (Member (ErrorS 'InvalidPermissions) r) => Permissions -> TeamMember -> Sem r () ensureNotElevated targetPermissions member = unless - ( (targetPermissions ^. self) - `Set.isSubsetOf` (member ^. permissions . copy) + ( targetPermissions.self + `Set.isSubsetOf` (member ^. permissions).copy ) $ throwS @'InvalidPermissions diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index ef5a1b96b5f..484c769ab0c 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -326,7 +326,7 @@ updateTeamMember oldPerms tid uid newPerms = do addPrepQuery Cql.updatePermissions (newPerms, tid, uid) -- update billing_team_member table - let permDiff = Set.difference `on` view self + let permDiff = Set.difference `on` self acquiredPerms = newPerms `permDiff` oldPerms lostPerms = oldPerms `permDiff` newPerms From e28d6fb9ee2c60ae9702408f8b7c48c29dd62747 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 19 Sep 2024 11:57:37 +0200 Subject: [PATCH 078/136] WPB-11000 Test password reset with wrong key/code should fail (#4249) --- changelog.d/5-internal/WPB-11000 | 1 + integration/default.nix | 2 + integration/integration.cabal | 2 + integration/test/API/Brig.hs | 20 +++ integration/test/API/BrigInternal.hs | 5 + integration/test/Test/PasswordReset.hs | 105 +++++++++++++++ integration/test/Testlib/HTTP.hs | 6 + services/brig/brig.cabal | 1 - services/brig/test/integration/API/User.hs | 2 - .../integration/API/User/PasswordReset.hs | 127 ------------------ 10 files changed, 141 insertions(+), 130 deletions(-) create mode 100644 changelog.d/5-internal/WPB-11000 create mode 100644 integration/test/Test/PasswordReset.hs delete mode 100644 services/brig/test/integration/API/User/PasswordReset.hs diff --git a/changelog.d/5-internal/WPB-11000 b/changelog.d/5-internal/WPB-11000 new file mode 100644 index 00000000000..d489cc80d7e --- /dev/null +++ b/changelog.d/5-internal/WPB-11000 @@ -0,0 +1 @@ +Additional test for password reset, port tests to new integration test suite diff --git a/integration/default.nix b/integration/default.nix index abff642f97d..37d66c8daf5 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -19,6 +19,7 @@ , Cabal , case-insensitive , containers +, cookie , cql , cql-io , crypton @@ -115,6 +116,7 @@ mkDerivation { bytestring-conversion case-insensitive containers + cookie cql cql-io crypton diff --git a/integration/integration.cabal b/integration/integration.cabal index faf1a6a4867..a4351796175 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -140,6 +140,7 @@ library Test.MLS.Unreachable Test.Notifications Test.OAuth + Test.PasswordReset Test.Presence Test.Property Test.Provider @@ -193,6 +194,7 @@ library , bytestring-conversion , case-insensitive , containers + , cookie , cql , cql-io , crypton diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index fae907cd2e3..d3e268197ab 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -795,3 +795,23 @@ listInvitations :: (HasCallStack, MakesValue user) => user -> String -> App Resp listInvitations user tid = do req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", tid, "invitations"] submit "GET" req + +passwordReset :: (HasCallStack, MakesValue domain) => domain -> String -> App Response +passwordReset domain email = do + req <- baseRequest domain Brig Versioned "password-reset" + submit "POST" $ req & addJSONObject ["email" .= email] + +completePasswordReset :: (HasCallStack, MakesValue domain) => domain -> String -> String -> String -> App Response +completePasswordReset domain key code pw = do + req <- baseRequest domain Brig Versioned $ joinHttpPath ["password-reset", "complete"] + submit "POST" $ req & addJSONObject ["key" .= key, "code" .= code, "password" .= pw] + +login :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response +login domain email password = do + req <- baseRequest domain Brig Versioned "login" + submit "POST" $ req & addJSONObject ["email" .= email, "password" .= password] & addQueryParams [("persist", "true")] + +updateEmail :: (HasCallStack, MakesValue user) => user -> String -> String -> String -> App Response +updateEmail user email cookie token = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["access", "self", "email"] + submit "PUT" $ req & addJSONObject ["email" .= email] & setCookie cookie & addHeader "Authorization" ("Bearer " <> token) diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 38fe56ac943..7d1ca70230d 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -298,3 +298,8 @@ getActivationCode :: (HasCallStack, MakesValue domain) => domain -> String -> Ap getActivationCode domain email = do req <- baseRequest domain Brig Unversioned "i/users/activation-code" submit "GET" $ req & addQueryParams [("email", email)] + +getPasswordResetCode :: (HasCallStack, MakesValue domain) => domain -> String -> App Response +getPasswordResetCode domain email = do + req <- baseRequest domain Brig Unversioned "i/users/password-reset-code" + submit "GET" $ req & addQueryParams [("email", email)] diff --git a/integration/test/Test/PasswordReset.hs b/integration/test/Test/PasswordReset.hs new file mode 100644 index 00000000000..95a94ea3f27 --- /dev/null +++ b/integration/test/Test/PasswordReset.hs @@ -0,0 +1,105 @@ +module Test.PasswordReset where + +import API.Brig +import API.BrigInternal hiding (activate) +import API.Common +import SetupHelpers +import Testlib.Prelude + +-- @SF.Provisioning @TSFI.RESTfulAPI @S1 +-- +-- This test checks the password reset functionality of the application. +-- Besides a successful password reset the following scenarios are tested: +-- - Subsequent password reset requests should succeed without errors. +-- - Attempting to reset the password with an incorrect key or code should fail. +-- - Attempting to log in with the old password after a successful reset should fail. +-- - Attempting to log in with the new password after a successful reset should succeed. +-- - Attempting to reset the password again to the same new password should fail. +testPasswordResetShouldSucceedButFailOnWrongInputs :: (HasCallStack) => App () +testPasswordResetShouldSucceedButFailOnWrongInputs = do + u <- randomUser OwnDomain def + email <- u %. "email" & asString + passwordReset u email >>= assertSuccess + -- Even though a password reset is now in progress + -- we expect a successful response from a subsequent request to not leak any information + -- about the requested email. + passwordReset u email >>= assertSuccess + + (key, code) <- getPasswordResetData email + let newPassword = "newpassword" + + -- complete password reset with incorrect key/code should fail + completePasswordReset u "wrong-key" code newPassword >>= assertStatus 400 + login u email newPassword >>= assertStatus 403 + completePasswordReset u key "wrong-code" newPassword >>= assertStatus 400 + login u email newPassword >>= assertStatus 403 + + -- complete password reset with correct key and code should succeed + completePasswordReset u key code newPassword >>= assertSuccess + + -- try login with old password should fail + login u email defPassword >>= assertStatus 403 + -- login with new password should succeed + login u email newPassword >>= assertSuccess + -- reset password again to the same new password should fail + passwordReset u email >>= assertSuccess + (nextKey, nextCode) <- getPasswordResetData email + bindResponse (completePasswordReset u nextKey nextCode newPassword) $ \resp -> do + resp.status `shouldMatchInt` 409 + resp.json %. "label" `shouldMatch` "password-must-differ" + +-- @END + +testPasswordResetAfterEmailUpdate :: (HasCallStack) => App () +testPasswordResetAfterEmailUpdate = do + u <- randomUser OwnDomain def + email <- u %. "email" & asString + (cookie, token) <- bindResponse (login u email defPassword) $ \resp -> do + resp.status `shouldMatchInt` 200 + token <- resp.json %. "access_token" & asString + let cookie = fromJust $ getCookie "zuid" resp + pure ("zuid=" <> cookie, token) + + -- initiate email update + newEmail <- randomEmail + updateEmail u newEmail cookie token >>= assertSuccess + + -- initiate password reset + passwordReset u email >>= assertSuccess + (key, code) <- getPasswordResetData email + + -- activate new email + bindResponse (getActivationCode u newEmail) $ \resp -> do + resp.status `shouldMatchInt` 200 + activationKey <- resp.json %. "key" & asString + activationCode <- resp.json %. "code" & asString + activate u activationKey activationCode >>= assertSuccess + + bindResponse (getSelf u) $ \resp -> do + actualEmail <- resp.json %. "email" + actualEmail `shouldMatch` newEmail + + -- attempting to complete password reset should fail + bindResponse (completePasswordReset u key code "newpassword") $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "invalid-code" + +testPasswordResetInvalidPasswordLength :: App () +testPasswordResetInvalidPasswordLength = do + u <- randomUser OwnDomain def + email <- u %. "email" & asString + passwordReset u email >>= assertSuccess + (key, code) <- getPasswordResetData email + + -- complete password reset with a password that is too short should fail + let shortPassword = "123456" + completePasswordReset u key code shortPassword >>= assertStatus 400 + + -- try login with new password should fail + login u email shortPassword >>= assertStatus 403 + +getPasswordResetData :: String -> App (String, String) +getPasswordResetData email = do + bindResponse (getPasswordResetCode OwnDomain email) $ \resp -> do + resp.status `shouldMatchInt` 200 + (,) <$> (resp.json %. "key" & asString) <*> (resp.json %. "code" & asString) diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index ae15b01adb1..ab7e7d237bf 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -30,6 +30,7 @@ import Testlib.Assertions import Testlib.Env import Testlib.JSON import Testlib.Types +import Web.Cookie import Prelude splitHttpPath :: String -> [String] @@ -89,6 +90,11 @@ setCookie :: String -> HTTP.Request -> HTTP.Request setCookie c r = addHeader "Cookie" (cs c) r +getCookie :: String -> Response -> Maybe String +getCookie name resp = do + cookieHeader <- lookup (CI.mk $ cs "set-cookie") resp.headers + cs <$> lookup (cs name) (parseCookies cookieHeader) + addQueryParams :: [(String, String)] -> HTTP.Request -> HTTP.Request addQueryParams params req = HTTP.setQueryString (map (\(k, v) -> (cs k, Just (cs v))) params) req diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 14e026face8..52642552923 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -376,7 +376,6 @@ executable brig-integration API.User.Client API.User.Connection API.User.Handles - API.User.PasswordReset API.User.RichInfo API.User.Util API.UserPendingActivation diff --git a/services/brig/test/integration/API/User.hs b/services/brig/test/integration/API/User.hs index d791df93082..35cf4aef598 100644 --- a/services/brig/test/integration/API/User.hs +++ b/services/brig/test/integration/API/User.hs @@ -26,7 +26,6 @@ import API.User.Auth qualified import API.User.Client qualified import API.User.Connection qualified import API.User.Handles qualified -import API.User.PasswordReset qualified import API.User.RichInfo qualified import API.User.Util import Bilge hiding (accept, timeout) @@ -67,7 +66,6 @@ tests conf fbc p b c ch g n aws db userJournalWatcher = do API.User.Auth.tests conf p z db b g n, API.User.Connection.tests cl at p b c g fbc db, API.User.Handles.tests cl at conf p b c g, - API.User.PasswordReset.tests db cl at conf p b c g, API.User.RichInfo.tests cl at conf p b c g ] diff --git a/services/brig/test/integration/API/User/PasswordReset.hs b/services/brig/test/integration/API/User/PasswordReset.hs deleted file mode 100644 index 034c6c40ece..00000000000 --- a/services/brig/test/integration/API/User/PasswordReset.hs +++ /dev/null @@ -1,127 +0,0 @@ -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} - --- 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 API.User.PasswordReset - ( tests, - ) -where - -import API.User.Util -import Bilge hiding (accept, timeout) -import Bilge.Assert -import Brig.Options qualified as Opt -import Cassandra qualified as DB -import Data.Aeson as A -import Data.Aeson.KeyMap qualified as KeyMap -import Data.Misc -import Imports -import Test.Tasty hiding (Timeout) -import Util -import Util.Timeout -import Wire.API.User -import Wire.API.User.Auth - -tests :: - DB.ClientState -> - ConnectionLimit -> - Timeout -> - Opt.Opts -> - Manager -> - Brig -> - Cannon -> - Galley -> - TestTree -tests _cs _cl _at _conf p b _c _g = - testGroup - "password-reset" - [ test p "post /password-reset[/complete] - 201[/200]" $ testPasswordReset b, - test p "post /password-reset after put /access/self/email - 400" $ testPasswordResetAfterEmailUpdate b, - test p "post /password-reset/complete - password too short - 400" $ testPasswordResetInvalidPasswordLength b - ] - -testPasswordReset :: Brig -> Http () -testPasswordReset brig = do - u <- randomUser brig - let Just email = userEmail u - let uid = userId u - -- initiate reset - let newpw = plainTextPassword8Unsafe "newsecret" - do - initiatePasswordReset brig email !!! const 201 === statusCode - -- even though a password reset is now in progress - -- we expect a successful response from a subsequent request to not leak any information - -- about the requested email - initiatePasswordReset brig email !!! const 201 === statusCode - - passwordResetData <- preparePasswordReset brig email uid newpw - completePasswordReset brig passwordResetData !!! const 200 === statusCode - -- try login - login brig (defEmailLogin email) PersistentCookie - !!! const 403 === statusCode - login - brig - (MkLogin (LoginByEmail email) (plainTextPassword8To6 newpw) Nothing Nothing) - PersistentCookie - !!! const 200 === statusCode - -- reset password again to the same new password, get 400 "must be different" - do - initiatePasswordReset brig email !!! const 201 === statusCode - passwordResetData <- preparePasswordReset brig email uid newpw - completePasswordReset brig passwordResetData !!! const 409 === statusCode - -testPasswordResetAfterEmailUpdate :: Brig -> Http () -testPasswordResetAfterEmailUpdate brig = do - u <- randomUser brig - let uid = userId u - let Just email = userEmail u - eml <- randomEmail - initiateEmailUpdateLogin brig eml (emailLogin email defPassword Nothing) uid !!! const 202 === statusCode - initiatePasswordReset brig email !!! const 201 === statusCode - passwordResetData <- preparePasswordReset brig email uid (plainTextPassword8Unsafe "newsecret") - -- activate new email - activateEmail brig eml - checkEmail brig uid eml - -- attempting to complete password reset should fail - completePasswordReset brig passwordResetData !!! const 400 === statusCode - -testPasswordResetInvalidPasswordLength :: Brig -> Http () -testPasswordResetInvalidPasswordLength brig = do - u <- randomUser brig - let Just email = userEmail u - let uid = userId u - -- for convenience, we create a valid password first that we replace with an invalid one in the JSON later - let newpw = plainTextPassword8Unsafe "newsecret" - initiatePasswordReset brig email !!! const 201 === statusCode - passwordResetData <- preparePasswordReset brig email uid newpw - let shortPassword = String "123456" - let reqBody = toJSON passwordResetData & addJsonKey "password" shortPassword - postCompletePasswordReset reqBody !!! const 400 === statusCode - where - addJsonKey :: Key -> Value -> Value -> Object - addJsonKey key val (Object xs) = KeyMap.insert key val xs - addJsonKey _ _ _ = error "invalid JSON object" - - postCompletePasswordReset :: Object -> (MonadHttp m) => m ResponseLBS - postCompletePasswordReset bdy = - post - ( brig - . path "/password-reset/complete" - . contentJson - . body (RequestBodyLBS (encode bdy)) - ) From 12f8e16f0e4257565e76e7f9b5e9d3cb107f5e18 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:23:53 +0200 Subject: [PATCH 079/136] [chore] Removed implicit failures in favour of explicit error handling. (#4254) --- services/brig/src/Brig/API/Federation.hs | 10 ++-- services/brig/src/Brig/API/MLS/KeyPackages.hs | 44 +++++++++------- services/brig/src/Brig/API/Public.hs | 10 ++-- .../brig/src/Brig/CanonicalInterpreter.hs | 3 -- services/galley/src/Galley/API/Federation.hs | 31 +++++++----- services/galley/src/Galley/API/MLS/Message.hs | 50 +++++++++---------- 6 files changed, 78 insertions(+), 70 deletions(-) diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index fd891967200..1420eacc5f8 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -50,7 +50,6 @@ import Gundeck.Types.Push qualified as Push import Imports hiding ((\\)) import Network.Wai.Utilities.Error ((!>>)) import Polysemy -import Polysemy.Fail (Fail) import Servant (ServerT) import Servant.API import Wire.API.Connection @@ -89,7 +88,6 @@ federationSitemap :: Member NotificationSubsystem r, Member UserSubsystem r, Member UserStore r, - Member Fail r, Member DeleteQueue r ) => ServerT FederationAPI (Handler r) @@ -196,7 +194,13 @@ claimMultiPrekeyBundle :: Handler r UserClientPrekeyMap claimMultiPrekeyBundle _ uc = API.claimLocalMultiPrekeyBundles LegalholdPlusFederationNotImplemented uc !>> clientError -fedClaimKeyPackages :: (Member Fail r, Member GalleyAPIAccess r, Member UserStore r) => Domain -> ClaimKeyPackageRequest -> Handler r (Maybe KeyPackageBundle) +fedClaimKeyPackages :: + ( Member GalleyAPIAccess r, + Member UserStore r + ) => + Domain -> + ClaimKeyPackageRequest -> + Handler r (Maybe KeyPackageBundle) fedClaimKeyPackages domain ckpr = isMLSEnabled >>= \case True -> do diff --git a/services/brig/src/Brig/API/MLS/KeyPackages.hs b/services/brig/src/Brig/API/MLS/KeyPackages.hs index 33dcbcc90a1..f0e96bc1576 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages.hs @@ -45,7 +45,6 @@ import Data.Qualified import Data.Set qualified as Set import Imports import Polysemy (Member) -import Polysemy.Fail (Fail) import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.MLS.CipherSuite @@ -56,7 +55,7 @@ import Wire.API.Team.LegalHold import Wire.API.User.Client import Wire.GalleyAPIAccess (GalleyAPIAccess, getUserLegalholdStatus) import Wire.StoredUser -import Wire.UserStore (UserStore, getUsers) +import Wire.UserStore (UserStore, getUser) uploadKeyPackages :: Local UserId -> ClientId -> KeyPackageUpload -> Handler r () uploadKeyPackages lusr cid kps = do @@ -66,7 +65,9 @@ uploadKeyPackages lusr cid kps = do lift . wrapClient $ Data.insertKeyPackages (tUnqualified lusr) cid kps' claimKeyPackages :: - (Member GalleyAPIAccess r, Member UserStore r, Member Fail r) => + ( Member GalleyAPIAccess r, + Member UserStore r + ) => Local UserId -> Maybe ClientId -> Qualified UserId -> @@ -84,7 +85,9 @@ claimKeyPackages lusr mClient target mSuite = do claimLocalKeyPackages :: forall r. - (Member GalleyAPIAccess r, Member UserStore r, Member Fail r) => + ( Member GalleyAPIAccess r, + Member UserStore r + ) => Qualified UserId -> Maybe ClientId -> CipherSuiteTag -> @@ -95,9 +98,9 @@ claimLocalKeyPackages qusr skipOwn suite target = do -- the remote backend is complicit with our legalhold policies, we disallow anyone -- fetching key packages for users under legalhold -- - -- This way we prevent both locally and on the remote to add a legalholded user to an MLS + -- This way we prevent both locally and on the remote to add a user under legalhold to an MLS -- conversation - assertUserNotLegalholded + assertUserNotUnderLegalHold -- skip own client when the target is the requesting user itself let own = guard (qusr == tUntagged target) *> skipOwn @@ -121,20 +124,23 @@ claimLocalKeyPackages qusr skipOwn suite target = do uncurry (KeyPackageBundleEntry (tUntagged target) c) <$> wrapClientM (Data.claimKeyPackage target c suite) - assertUserNotLegalholded :: ExceptT ClientError (AppT r) () - assertUserNotLegalholded = do + assertUserNotUnderLegalHold :: ExceptT ClientError (AppT r) () + assertUserNotUnderLegalHold = do -- this is okay because there can only be one StoredUser per UserId - [su] <- lift $ liftSem $ getUsers [tUnqualified target] - for_ su.teamId \tid -> do - resp <- lift $ liftSem $ getUserLegalholdStatus target tid - -- if an admin tries to put a user under legalhold - -- the user has to first reject to be put under legalhold - -- before they can join conversations again - case resp.ulhsrStatus of - UserLegalHoldPending -> throwE ClientLegalHoldIncompatible - UserLegalHoldEnabled -> throwE ClientLegalHoldIncompatible - UserLegalHoldDisabled -> pure () - UserLegalHoldNoConsent -> pure () + mSu <- lift $ liftSem $ getUser (tUnqualified target) + case mSu of + Nothing -> pure () -- Legalhold is a team feature. + Just su -> + for_ su.teamId $ \tid -> do + resp <- lift $ liftSem $ getUserLegalholdStatus target tid + -- if an admin tries to put a user under legalhold + -- the user has to first reject to be put under legalhold + -- before they can join conversations again + case resp.ulhsrStatus of + UserLegalHoldPending -> throwE ClientLegalHoldIncompatible + UserLegalHoldEnabled -> throwE ClientLegalHoldIncompatible + UserLegalHoldDisabled -> pure () + UserLegalHoldNoConsent -> pure () claimRemoteKeyPackages :: Local UserId -> diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 1f313fb01c4..e78af13fda1 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -90,8 +90,7 @@ import Network.Wai.Utilities (CacheControl (..), (!>>)) import Network.Wai.Utilities qualified as Utilities import Polysemy import Polysemy.Error -import Polysemy.Fail (Fail) -import Polysemy.Input +import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) import Servant qualified @@ -264,15 +263,14 @@ internalEndpointsSwaggerDocsAPI service examplePort swagger Nothing = servantSitemap :: forall r p. - ( Member (Concurrency 'Unsafe) r, - Member (Embed HttpClientIO) r, + ( Member (Embed HttpClientIO) r, Member (Embed IO) r, Member (Error UserSubsystemError) r, - Member Fail r, Member (Input (Local ())) r, Member (Input TeamTemplates) r, Member (UserPendingActivationStore p) r, Member AuthenticationSubsystem r, + Member BlockListStore r, Member DeleteQueue r, Member EmailSending r, Member EmailSubsystem r, @@ -294,7 +292,7 @@ servantSitemap :: Member UserStore r, Member UserSubsystem r, Member VerificationCodeSubsystem r, - Member BlockListStore r + Member (Concurrency 'Unsafe) r ) => ServerT BrigAPI (Handler r) servantSitemap = diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 45722ef86df..936c1c7dd95 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -27,7 +27,6 @@ import Polysemy.Async import Polysemy.Conc import Polysemy.Embed (runEmbedded) import Polysemy.Error (Error, errorToIOFinal, mapError, runError) -import Polysemy.Fail import Polysemy.Input (Input, runInputConst, runInputSem) import Polysemy.TinyLog (TinyLog) import Wire.API.Allowlists (AllowlistEmailDomains) @@ -151,7 +150,6 @@ type BrigCanonicalEffects = Error SomeException, TinyLog, Embed HttpClientIO, - Fail, Embed IO, Race, Async, @@ -201,7 +199,6 @@ runBrigToIO e (AppT ma) = do . asyncToIOFinal . interpretRace . embedToFinal - . failToEmbed @IO -- if a fallible pattern fails, we throw a hard IO error . runEmbedded (runHttpClientIO e) . loggerToTinyLogReqId (e ^. App.requestId) (e ^. applog) . runError @SomeException diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 5d23e68b499..a78652408fe 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -67,7 +67,6 @@ import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Polysemy import Polysemy.Error -import Polysemy.Fail (Fail) import Polysemy.Input import Polysemy.Internal.Kind (Append) import Polysemy.Resource @@ -606,7 +605,6 @@ sendMLSCommitBundle :: Member (Input UTCTime) r, Member LegalHoldStore r, Member MemberStore r, - Member Fail r, Member Resource r, Member TeamStore r, Member P.TinyLog r, @@ -629,17 +627,24 @@ sendMLSCommitBundle remoteDomain msr = handleMLSMessageErrors $ do (ctype, qConvOrSub) <- getConvFromGroupId ibundle.groupId when (qUnqualified qConvOrSub /= msr.convOrSubId) $ throwS @'MLSGroupConversationMismatch -- this cannot throw the error since we always pass the sender which is qualified to be remote - Just resp <- - runErrorS @MLSLegalholdIncompatible $ - postMLSCommitBundle - loc - (tUntagged @QRemote sender) - msr.senderClient - ctype - qConvOrSub - Nothing - ibundle - pure $ MLSMessageResponseUpdates $ map lcuUpdate resp + MLSMessageResponseUpdates + . fmap lcuUpdate + <$> mapToRuntimeError @MLSLegalholdIncompatible + (InternalErrorWithDescription "expected group conversation while handling policy conflicts") + ( postMLSCommitBundle + loc + -- Type application to prevent future changes from introducing errors. + -- It is only safe to assume that we can discard the error when the sender + -- is actually remote. + -- Since `tUntagged` works on local and remote, a future changed may + -- go unchecked without this. + (tUntagged @QRemote sender) + msr.senderClient + ctype + qConvOrSub + Nothing + ibundle + ) sendMLSMessage :: ( Member BackendNotificationQueueAccess r, diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 75de5388c22..0478b06ad83 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -35,7 +35,7 @@ import Data.Json.Util import Data.LegalHold import Data.Qualified import Data.Set qualified as Set -import Data.Tagged (Tagged) +import Data.Tagged import Data.Text.Lazy qualified as LT import Data.Tuple.Extra import Galley.API.Action @@ -60,11 +60,10 @@ import Galley.Effects.ConversationStore import Galley.Effects.FederatorAccess import Galley.Effects.MemberStore import Galley.Effects.SubConversationStore -import Galley.Effects.TeamStore (getUserTeams) +import Galley.Effects.TeamStore qualified as TeamStore import Imports import Polysemy import Polysemy.Error -import Polysemy.Fail import Polysemy.Input import Polysemy.Internal import Polysemy.Output @@ -151,13 +150,12 @@ postMLSMessageFromLocalUser lusr c conn smsg = do pure $ MLSMessageSendingStatus events t postMLSCommitBundle :: - ( HasProposalEffects r, - Members MLSBundleStaticErrors r, - Member Fail r, - Member (ErrorS MLSLegalholdIncompatible) r, + ( Member (ErrorS MLSLegalholdIncompatible) r, Member Random r, Member Resource r, - Member SubConversationStore r + Member SubConversationStore r, + Members MLSBundleStaticErrors r, + HasProposalEffects r ) => Local x -> Qualified UserId -> @@ -175,13 +173,12 @@ postMLSCommitBundle loc qusr c ctype qConvOrSub conn bundle = qConvOrSub postMLSCommitBundleFromLocalUser :: - ( HasProposalEffects r, - Members MLSBundleStaticErrors r, + ( Member (ErrorS MLSLegalholdIncompatible) r, Member Random r, - Member Fail r, - Member (ErrorS MLSLegalholdIncompatible) r, Member Resource r, - Member SubConversationStore r + Member SubConversationStore r, + Members MLSBundleStaticErrors r, + HasProposalEffects r ) => Local UserId -> ClientId -> @@ -199,13 +196,12 @@ postMLSCommitBundleFromLocalUser lusr c conn bundle = do pure $ MLSMessageSendingStatus events t postMLSCommitBundleToLocalConv :: - ( HasProposalEffects r, - Members MLSBundleStaticErrors r, + ( Member (ErrorS MLSLegalholdIncompatible) r, + Member Random r, Member Resource r, - Member (ErrorS MLSLegalholdIncompatible) r, Member SubConversationStore r, - Member Random r, - Member Fail r + Members MLSBundleStaticErrors r, + HasProposalEffects r ) => Qualified UserId -> ClientId -> @@ -222,22 +218,24 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do note (mlsProtocolError "Unsupported ciphersuite") $ cipherSuiteTag bundle.groupInfo.value.groupContext.cipherSuite - -- when a user tries to join any mls conversation while being legalholded + -- when a user tries to join any mls conversation while being under legalhold -- they receive a 409 stating that mls and legalhold are incompatible case qusr `relativeTo` lConvOrSubId of Local luid -> when (isNothing convOrSub.mlsMeta.cnvmlsActiveData) do - usrTeams <- getUserTeams (tUnqualified luid) + usrTeams <- TeamStore.getUserTeams (tUnqualified luid) for_ usrTeams \tid -> do -- this would only return 'Left' if the team member did vanish directly in the process of this -- request or if the legalhold state was somehow inconsistent. We can safely assume that this -- should be a server error - Right resp <- runError @(Tagged TeamMemberNotFound ()) $ getUserStatus luid tid (tUnqualified luid) - case resp.ulhsrStatus of - UserLegalHoldPending -> throwS @MLSLegalholdIncompatible - UserLegalHoldEnabled -> throwS @MLSLegalholdIncompatible - UserLegalHoldDisabled -> pure () - UserLegalHoldNoConsent -> pure () + resp <- runError @(Tagged TeamMemberNotFound ()) $ getUserStatus luid tid (tUnqualified luid) + case resp of + Left _ -> throw $ InternalErrorWithDescription "Server error. Team member must have vanished with the legal hold check" + Right r -> case r.ulhsrStatus of + UserLegalHoldPending -> throwS @MLSLegalholdIncompatible + UserLegalHoldEnabled -> throwS @MLSLegalholdIncompatible + UserLegalHoldDisabled -> pure () + UserLegalHoldNoConsent -> pure () -- we can skip the remote case because we currently to not support creating conversations on the remote backend Remote _ -> pure () From 023769624fd5b348de0319e9183b5a3149a8fbdf Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 19 Sep 2024 15:48:53 +0200 Subject: [PATCH 080/136] [WPB-10708] personal account to own team (#4251) * Add endpoint to upgrade a personal user to a team * Test upgrading personal user to team * Add CHANGELOG entry --------- Co-authored-by: Paolo Capriotti --- changelog.d/1-api-changes/wpb-10708 | 1 + integration/test/API/Brig.hs | 5 +++ integration/test/API/Galley.hs | 6 +++ integration/test/Test/Teams.hs | 32 +++++++++++++- integration/test/Testlib/Assertions.hs | 3 ++ libs/wire-api/src/Wire/API/Error/Brig.hs | 3 ++ .../src/Wire/API/Routes/Public/Brig.hs | 43 ++++++++++++------- libs/wire-api/src/Wire/API/User.hs | 40 +++++++++++++++++ services/brig/src/Brig/API/Public.hs | 33 +++++++++++--- services/brig/src/Brig/API/Types.hs | 6 --- services/brig/src/Brig/API/User.hs | 42 +++++++++++++++++- 11 files changed, 186 insertions(+), 28 deletions(-) create mode 100644 changelog.d/1-api-changes/wpb-10708 diff --git a/changelog.d/1-api-changes/wpb-10708 b/changelog.d/1-api-changes/wpb-10708 new file mode 100644 index 00000000000..cfbe92afa70 --- /dev/null +++ b/changelog.d/1-api-changes/wpb-10708 @@ -0,0 +1 @@ +Add endpoint to upgrade a personal user to a team owner diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index d3e268197ab..a40dddd3bd9 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -815,3 +815,8 @@ updateEmail :: (HasCallStack, MakesValue user) => user -> String -> String -> St updateEmail user email cookie token = do req <- baseRequest user Brig Versioned $ joinHttpPath ["access", "self", "email"] submit "PUT" $ req & addJSONObject ["email" .= email] & setCookie cookie & addHeader "Authorization" ("Bearer " <> token) + +upgradePersonalToTeam :: (HasCallStack, MakesValue user) => user -> String -> App Response +upgradePersonalToTeam user name = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["upgrade-personal-to-team"] + submit "POST" $ req & addJSONObject ["name" .= name, "icon" .= "default"] diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 6bb55137d82..01df8dc89ea 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -519,6 +519,12 @@ updateMessageTimer user qcnv update = do req <- baseRequest user Galley Versioned path submit "PUT" (addJSONObject ["message_timer" .= updateReq] req) +getTeam :: (HasCallStack, MakesValue user, MakesValue tid) => user -> tid -> App Response +getTeam user tid = do + tidStr <- asString tid + req <- baseRequest user Galley Versioned (joinHttpPath ["teams", tidStr]) + submit "GET" req + getTeamMembers :: (HasCallStack, MakesValue user, MakesValue tid) => user -> tid -> App Response getTeamMembers user tid = do tidStr <- asString tid diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs index c54a7b18b46..dbf08e3e2a7 100644 --- a/integration/test/Test/Teams.hs +++ b/integration/test/Test/Teams.hs @@ -20,7 +20,7 @@ module Test.Teams where import API.Brig import API.BrigInternal (createUser, getInvitationCode, refreshIndex) import API.Common -import API.Galley (getTeamMembers) +import API.Galley (getTeam, getTeamMembers) import API.GalleyInternal (setTeamFeatureStatus) import Control.Monad.Codensity (Codensity (runCodensity)) import Control.Monad.Extra (findM) @@ -172,3 +172,33 @@ testTeamUserCannotBeInvited = do (owner2, _, _) <- createTeam OwnDomain 0 email <- tm %. "email" >>= asString postInvitation owner2 (PostInvitation (Just email) Nothing) >>= assertStatus 409 + +testUpgradePersonalToTeam :: (HasCallStack) => App () +testUpgradePersonalToTeam = do + alice <- randomUser OwnDomain def + let teamName = "wonderland" + tid <- bindResponse (upgradePersonalToTeam alice teamName) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "team_name" `shouldMatch` teamName + resp.json %. "team_id" + + alice' <- getUser alice alice >>= getJSON 200 + alice' %. "team" `shouldMatch` tid + + team <- getTeam alice tid >>= getJSON 200 + team %. "name" `shouldMatch` teamName + + bindResponse (getTeamMembers alice tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + owner <- asList (resp.json %. "members") >>= assertOne + owner %. "user" `shouldMatch` (alice %. "id") + shouldBeNull $ owner %. "created_at" + shouldBeNull $ owner %. "created_by" + +testUpgradePersonalToTeamAlreadyInATeam :: (HasCallStack) => App () +testUpgradePersonalToTeamAlreadyInATeam = do + (alice, _, _) <- createTeam OwnDomain 0 + + bindResponse (upgradePersonalToTeam alice "wonderland") $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "user-already-in-a-team" diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index af7d18900e2..c2f9efef3d1 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -206,6 +206,9 @@ shouldMatchSet a b = do shouldBeEmpty :: (MakesValue a, HasCallStack) => a -> App () shouldBeEmpty a = a `shouldMatch` (mempty :: [Value]) +shouldBeNull :: (MakesValue a, HasCallStack) => a -> App () +shouldBeNull a = a `shouldMatch` Aeson.Null + shouldMatchOneOf :: (MakesValue a, MakesValue b, HasCallStack) => a -> diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 416e5fecaa2..7846f5c51f5 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -99,6 +99,7 @@ data BrigError | TooManyProperties | PropertyKeyTooLarge | PropertyValueTooLarge + | UserAlreadyInATeam instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: BrigError) where addToOpenApi = addStaticErrorToSwagger @(MapError e) @@ -295,3 +296,5 @@ type instance MapError 'TooManyProperties = 'StaticError 403 "too-many-propertie type instance MapError 'PropertyKeyTooLarge = 'StaticError 403 "property-key-too-large" "The property key is too large." type instance MapError 'PropertyValueTooLarge = 'StaticError 403 "property-value-too-large" "The property value is too large" + +type instance MapError 'UserAlreadyInATeam = 'StaticError 403 "user-already-in-a-team" "Switching teams is not allowed" 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 7394673620e..6e457fefa6d 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -471,23 +471,36 @@ type UserHandleAPI = ) type AccountAPI = - -- docs/reference/user/registration.md {#RefRegistration} - -- - -- This endpoint can lead to the following events being sent: - -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID - -- - UserIdentityUpdated event to created user, if email code or phone code is provided Named - "register" - ( Summary "Register a new user." - :> Description - "If the environment where the registration takes \ - \place is private and a registered email address \ - \is not whitelisted, a 403 error is returned." - :> MakesFederatedCall 'Brig "send-connection-action" - :> "register" - :> ReqBody '[JSON] NewUserPublic - :> MultiVerb 'POST '[JSON] RegisterResponses (Either RegisterError RegisterSuccess) + "upgrade-personal-to-team" + ( Summary "Upgrade personal user to team owner" + :> "upgrade-personal-to-team" + :> ZLocalUser + :> ReqBody '[JSON] BindingNewTeamUser + :> MultiVerb + 'POST + '[JSON] + UpgradePersonalToTeamResponses + (Either UpgradePersonalToTeamError CreateUserTeam) ) + :<|> + -- docs/reference/user/registration.md {#RefRegistration} + -- + -- This endpoint can lead to the following events being sent: + -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID + -- - UserIdentityUpdated event to created user, if email code or phone code is provided + Named + "register" + ( Summary "Register a new user." + :> Description + "If the environment where the registration takes \ + \place is private and a registered email address \ + \is not whitelisted, a 403 error is returned." + :> MakesFederatedCall 'Brig "send-connection-action" + :> "register" + :> ReqBody '[JSON] NewUserPublic + :> MultiVerb 'POST '[JSON] RegisterResponses (Either RegisterError RegisterSuccess) + ) -- This endpoint can lead to the following events being sent: -- UserDeleted event to contacts of deleted user -- MemberLeave event to members for all conversations the user was in (via galley) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 2ff0c3eb3e0..4a2f92dd38f 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -46,6 +46,11 @@ module Wire.API.User mkUserProfileWithEmail, userObjectSchema, + -- * UpgradePersonalToTeam + CreateUserTeam (..), + UpgradePersonalToTeamResponses, + UpgradePersonalToTeamError (..), + -- * NewUser NewUserPublic (..), RegisterError (..), @@ -772,6 +777,41 @@ isNewUserTeamMember u = case newUserTeam u of instance Arbitrary NewUserPublic where arbitrary = arbitrary `QC.suchThatMap` (rightMay . validateNewUserPublic) +data CreateUserTeam = CreateUserTeam + { createdTeamId :: !TeamId, + createdTeamName :: !Text + } + deriving (Show) + deriving (FromJSON, ToJSON, S.ToSchema) via Schema CreateUserTeam + +instance ToSchema CreateUserTeam where + schema = + object "CreateUserTeam" $ + CreateUserTeam + <$> createdTeamId .= field "team_id" schema + <*> createdTeamName .= field "team_name" schema + +data UpgradePersonalToTeamError = UpgradePersonalToTeamErrorAlreadyInATeam + deriving (Show) + +type UpgradePersonalToTeamResponses = + '[ ErrorResponse UserAlreadyInATeam, + Respond 200 "Team created" CreateUserTeam + ] + +instance + AsUnion + UpgradePersonalToTeamResponses + (Either UpgradePersonalToTeamError CreateUserTeam) + where + toUnion (Left UpgradePersonalToTeamErrorAlreadyInATeam) = + Z (I (dynError @(MapError UserAlreadyInATeam))) + toUnion (Right x) = S (Z (I x)) + + fromUnion (Z (I _)) = Left UpgradePersonalToTeamErrorAlreadyInATeam + fromUnion (S (Z (I x))) = Right x + fromUnion (S (S x)) = case x of {} + data RegisterError = RegisterErrorAllowlistError | RegisterErrorInvalidInvitationCode diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index e78af13fda1..b557bbc2c1e 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -41,6 +41,7 @@ import Brig.Calling.API qualified as Calling import Brig.Data.Connection qualified as Data import Brig.Data.Nonce as Nonce import Brig.Data.User qualified as Data +import Brig.Effects.ConnectionStore import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) import Brig.Effects.SFT @@ -82,6 +83,7 @@ import Data.Qualified import Data.Range import Data.Schema () import Data.Text.Encoding qualified as Text +import Data.Time.Clock import Data.ZAuth.Token qualified as ZAuth import FileEmbedLzma import Imports hiding (head) @@ -161,6 +163,7 @@ import Wire.PropertySubsystem import Wire.Sem.Concurrency import Wire.Sem.Jwk (Jwk) import Wire.Sem.Now (Now) +import Wire.Sem.Paging.Cassandra import Wire.UserKeyStore import Wire.UserSearch.Types import Wire.UserStore (UserStore) @@ -267,10 +270,10 @@ servantSitemap :: Member (Embed IO) r, Member (Error UserSubsystemError) r, Member (Input (Local ())) r, + Member (Input UTCTime) r, Member (Input TeamTemplates) r, Member (UserPendingActivationStore p) r, Member AuthenticationSubsystem r, - Member BlockListStore r, Member DeleteQueue r, Member EmailSending r, Member EmailSubsystem r, @@ -292,7 +295,9 @@ servantSitemap :: Member UserStore r, Member UserSubsystem r, Member VerificationCodeSubsystem r, - Member (Concurrency 'Unsafe) r + Member (Concurrency 'Unsafe) r, + Member BlockListStore r, + Member (ConnectionStore InternalPaging) r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -346,7 +351,8 @@ servantSitemap = accountAPI :: ServerT AccountAPI (Handler r) accountAPI = - Named @"register" (callsFed (exposeAnnotations createUser)) + Named @"upgrade-personal-to-team" upgradePersonalToTeam + :<|> Named @"register" (callsFed (exposeAnnotations createUser)) :<|> Named @"verify-delete" (callsFed (exposeAnnotations verifyDeleteUser)) :<|> Named @"get-activate" (callsFed (exposeAnnotations activate)) :<|> Named @"post-activate" (callsFed (exposeAnnotations activateKey)) @@ -696,6 +702,23 @@ createAccessToken method luid cid proof = do let link = safeLink (Proxy @api) (Proxy @endpoint) cid API.createAccessToken luid cid method link proof !>> certEnrollmentError +upgradePersonalToTeam :: + ( Member (ConnectionStore InternalPaging) r, + Member (Embed HttpClientIO) r, + Member GalleyAPIAccess r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member NotificationSubsystem r, + Member TinyLog r, + Member UserSubsystem r + ) => + Local UserId -> + Public.BindingNewTeamUser -> + Handler r (Either Public.UpgradePersonalToTeamError Public.CreateUserTeam) +upgradePersonalToTeam luid bNewTeam = + lift . runExceptT $ + API.upgradePersonalToTeam luid bNewTeam + -- | docs/reference/user/registration.md {#RefRegistration} createUser :: ( Member BlockListStore r, @@ -768,9 +791,9 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do | otherwise = liftSem $ sendActivationMail email name key code locale - sendWelcomeEmail :: (Member EmailSending r) => Public.EmailAddress -> CreateUserTeam -> Public.NewTeamUser -> Maybe Public.Locale -> (AppT r) () + sendWelcomeEmail :: (Member EmailSending r) => Public.EmailAddress -> Public.CreateUserTeam -> Public.NewTeamUser -> Maybe Public.Locale -> (AppT r) () -- NOTE: Welcome e-mails for the team creator are not dealt by brig anymore - sendWelcomeEmail e (CreateUserTeam t n) newUser l = case newUser of + sendWelcomeEmail e (Public.CreateUserTeam t n) newUser l = case newUser of Public.NewTeamCreator _ -> pure () Public.NewTeamMember _ -> diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 8eac26e9861..97d15e8c06b 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -58,12 +58,6 @@ data CreateUserResult = CreateUserResult } deriving (Show) -data CreateUserTeam = CreateUserTeam - { createdTeamId :: !TeamId, - createdTeamName :: !Text - } - deriving (Show) - data ActivationResult = -- | The key/code was valid and successfully activated. ActivationSuccess !(Maybe UserIdentity) !Bool diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 17331d18ce1..fecd3001fac 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -20,6 +20,7 @@ -- TODO: Move to Brig.User.Account module Brig.API.User ( -- * User Accounts / Profiles + upgradePersonalToTeam, createUser, createUserSpar, createUserInviteViaScim, @@ -81,6 +82,7 @@ import Brig.Data.Connection (countConnections) import Brig.Data.Connection qualified as Data import Brig.Data.User import Brig.Data.User qualified as Data +import Brig.Effects.ConnectionStore import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore qualified as UserPendingActivationStore import Brig.IO.Intra qualified as Intra @@ -106,7 +108,7 @@ import Data.List1 as List1 (List1, singleton) import Data.Misc import Data.Qualified import Data.Range -import Data.Time.Clock (addUTCTime) +import Data.Time.Clock import Data.UUID.V4 (nextRandom) import Imports import Network.Wai.Utilities @@ -146,6 +148,7 @@ import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PasswordStore (PasswordStore, lookupHashedPassword, upsertHashedPassword) import Wire.PropertySubsystem as PropertySubsystem import Wire.Sem.Concurrency +import Wire.Sem.Paging.Cassandra import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem as User @@ -253,6 +256,43 @@ createUserSpar new = do Team.TeamName nm <- lift $ liftSem $ GalleyAPIAccess.getTeamName tid pure $ CreateUserTeam tid nm +upgradePersonalToTeam :: + forall r. + ( Member GalleyAPIAccess r, + Member UserSubsystem r, + Member TinyLog r, + Member (Embed HttpClientIO) r, + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r + ) => + Local UserId -> + BindingNewTeamUser -> + ExceptT UpgradePersonalToTeamError (AppT r) CreateUserTeam +upgradePersonalToTeam luid bNewTeam = do + -- check that the user is not part of a team + mSelfProfile <- lift $ liftSem $ getSelfProfile luid + let mTid = mSelfProfile >>= userTeam . selfUser + when (isJust mTid) $ + throwE UpgradePersonalToTeamErrorAlreadyInATeam + + lift $ do + -- generate team ID + tid <- randomId + + let uid = tUnqualified luid + createUserTeam <- do + liftSem $ GalleyAPIAccess.createTeam uid (bnuTeam bNewTeam) tid + let BindingNewTeam newTeam = bNewTeam.bnuTeam + pure $ CreateUserTeam tid (fromRange (newTeam ^. newTeamName)) + + wrapClient $ updateUserTeam uid tid + liftSem $ Intra.sendUserEvent uid Nothing (teamUpdated uid tid) + initAccountFeatureConfig uid + + pure $! createUserTeam + -- docs/reference/user/registration.md {#RefRegistration} createUser :: forall r p. From 674b067254627dc4e892f18ec4e0f7b40fa5a364 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 19 Sep 2024 16:37:11 +0200 Subject: [PATCH 081/136] Send confirmation email after upgrade to team owner (#4253) * Send confirmation email * Add placeholder templates * Add CHANGELOG entry --- .../2-features/personal-account-to-team-email | 1 + libs/wire-api/src/Wire/API/Team.hs | 2 +- libs/wire-api/src/Wire/API/User.hs | 14 +++++-- .../src/Wire/EmailSubsystem.hs | 1 + .../src/Wire/EmailSubsystem/Interpreter.hs | 37 +++++++++++++++++++ .../src/Wire/EmailSubsystem/Template.hs | 10 +++++ .../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 | 18 ++++++++- services/brig/src/Brig/User/Template.hs | 8 ++++ 12 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 changelog.d/2-features/personal-account-to-team-email create mode 100644 services/brig/deb/opt/brig/templates/en/user/email/upgrade-subject.txt create mode 100644 services/brig/deb/opt/brig/templates/en/user/email/upgrade.html create mode 100644 services/brig/deb/opt/brig/templates/en/user/email/upgrade.txt diff --git a/changelog.d/2-features/personal-account-to-team-email b/changelog.d/2-features/personal-account-to-team-email new file mode 100644 index 00000000000..c8bbe2bf91b --- /dev/null +++ b/changelog.d/2-features/personal-account-to-team-email @@ -0,0 +1 @@ +Send confirmation email after adding a personal user to a new team diff --git a/libs/wire-api/src/Wire/API/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index 13c09ab567b..cffcd2bac95 100644 --- a/libs/wire-api/src/Wire/API/Team.hs +++ b/libs/wire-api/src/Wire/API/Team.hs @@ -177,7 +177,7 @@ instance ToSchema TeamList where -------------------------------------------------------------------------------- -- NewTeam -newtype BindingNewTeam = BindingNewTeam (NewTeam ()) +newtype BindingNewTeam = BindingNewTeam {bntTeam :: NewTeam ()} deriving stock (Eq, Show, Generic) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema BindingNewTeam) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 4a2f92dd38f..08a8f758db9 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -791,11 +791,14 @@ instance ToSchema CreateUserTeam where <$> createdTeamId .= field "team_id" schema <*> createdTeamName .= field "team_name" schema -data UpgradePersonalToTeamError = UpgradePersonalToTeamErrorAlreadyInATeam +data UpgradePersonalToTeamError + = UpgradePersonalToTeamErrorAlreadyInATeam + | UpgradePersonalToTeamErrorUserNotFound deriving (Show) type UpgradePersonalToTeamResponses = '[ ErrorResponse UserAlreadyInATeam, + ErrorResponse UserNotFound, Respond 200 "Team created" CreateUserTeam ] @@ -806,11 +809,14 @@ instance where toUnion (Left UpgradePersonalToTeamErrorAlreadyInATeam) = Z (I (dynError @(MapError UserAlreadyInATeam))) - toUnion (Right x) = S (Z (I x)) + toUnion (Left UpgradePersonalToTeamErrorUserNotFound) = + S (Z (I (dynError @(MapError UserNotFound)))) + toUnion (Right x) = S (S (Z (I x))) fromUnion (Z (I _)) = Left UpgradePersonalToTeamErrorAlreadyInATeam - fromUnion (S (Z (I x))) = Right x - fromUnion (S (S x)) = case x of {} + fromUnion (S (Z (I _))) = Left UpgradePersonalToTeamErrorAlreadyInATeam + fromUnion (S (S (Z (I x)))) = Right x + fromUnion (S (S (S x))) = case x of {} data RegisterError = RegisterErrorAllowlistError diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs index c604fb36ed8..e4090103799 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs @@ -21,5 +21,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 () makeSem ''EmailSubsystem diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs index 2fda920c11f..a152b166af1 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs @@ -36,6 +36,7 @@ emailSubsystemInterpreter tpls branding = interpret \case SendTeamActivationMail email name key code mLocale teamName -> sendTeamActivationMailImpl tpls branding email name key code mLocale teamName SendNewClientEmail email name client locale -> sendNewClientEmailImpl tpls branding email name client locale SendAccountDeletionEmail email name key code locale -> sendAccountDeletionEmailImpl tpls branding email name key code locale + SendUpgradePersonalToTeamConfirmationEmail email name teamName locale -> sendUpgradePersonalToTeamConfirmationEmailImpl tpls branding email name teamName locale ------------------------------------------------------------------------------- -- Verification Email for @@ -395,6 +396,42 @@ 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 + ------------------------------------------------------------------------------- -- MIME Conversions diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs index 818cdca2e9e..f1c7a996f56 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs @@ -35,6 +35,7 @@ module Wire.EmailSubsystem.Template LoginCallTemplate (..), DeletionSmsTemplate (..), DeletionEmailTemplate (..), + UpgradePersonalToTeamEmailTemplate (..), NewClientEmailTemplate (..), SecondFactorVerificationEmailTemplate (..), @@ -105,6 +106,7 @@ data UserTemplates = UserTemplates loginCall :: LoginCallTemplate, deletionSms :: DeletionSmsTemplate, deletionEmail :: DeletionEmailTemplate, + upgradePersonalToTeamEmail :: UpgradePersonalToTeamEmailTemplate, newClientEmail :: NewClientEmailTemplate, verificationLoginEmail :: SecondFactorVerificationEmailTemplate, verificationScimTokenEmail :: SecondFactorVerificationEmailTemplate, @@ -157,6 +159,14 @@ 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, 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 new file mode 100644 index 00000000000..8a92b9f9a36 --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/user/email/upgrade-subject.txt @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000000..690b0104fdd --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/user/email/upgrade.html @@ -0,0 +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 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 new file mode 100644 index 00000000000..744da7dc05c --- /dev/null +++ b/services/brig/deb/opt/brig/templates/en/user/email/upgrade.txt @@ -0,0 +1,22 @@ +[${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 b557bbc2c1e..8d4d35d653f 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -705,6 +705,7 @@ 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 fecd3001fac..6e516ccca60 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -259,6 +259,7 @@ createUserSpar new = do upgradePersonalToTeam :: forall r. ( Member GalleyAPIAccess r, + Member EmailSubsystem r, Member UserSubsystem r, Member TinyLog r, Member (Embed HttpClientIO) r, @@ -273,8 +274,12 @@ upgradePersonalToTeam :: upgradePersonalToTeam luid bNewTeam = do -- check that the user is not part of a team mSelfProfile <- lift $ liftSem $ getSelfProfile luid - let mTid = mSelfProfile >>= userTeam . selfUser - when (isJust mTid) $ + user <- + maybe + (throwE UpgradePersonalToTeamErrorUserNotFound) + (pure . selfUser) + mSelfProfile + when (isJust user.userTeam) $ throwE UpgradePersonalToTeamErrorAlreadyInATeam lift $ do @@ -291,6 +296,15 @@ upgradePersonalToTeam luid bNewTeam = do liftSem $ Intra.sendUserEvent uid Nothing (teamUpdated uid tid) initAccountFeatureConfig uid + -- send confirmation email + for_ (userEmail user) $ \email -> do + liftSem $ + sendUpgradePersonalToTeamConfirmationEmail + email + user.userDisplayName + bNewTeam.bnuTeam.bntTeam._newTeamName.fromRange + user.userLocale + pure $! createUserTeam -- docs/reference/user/registration.md {#RefRegistration} diff --git a/services/brig/src/Brig/User/Template.hs b/services/brig/src/Brig/User/Template.hs index 0667a4b2cd2..00aa5a5b6d2 100644 --- a/services/brig/src/Brig/User/Template.hs +++ b/services/brig/src/Brig/User/Template.hs @@ -28,6 +28,7 @@ module Brig.User.Template LoginCallTemplate (..), DeletionSmsTemplate (..), DeletionEmailTemplate (..), + UpgradePersonalToTeamEmailTemplate (..), NewClientEmailTemplate (..), SecondFactorVerificationEmailTemplate (..), loadUserTemplates, @@ -109,6 +110,13 @@ 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 ef612baa6ac5cb10e7c7902d86f854d9a176c900 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 19 Sep 2024 22:14:00 +0200 Subject: [PATCH 082/136] [WPB-11186] Translate flaky integration test to /integration. (#4258) Co-authored-by: Leif Battermann --- integration/test/Test/Conversation.hs | 34 +++++++ integration/test/Testlib/Assertions.hs | 9 ++ services/galley/test/integration/API.hs | 88 ------------------- .../test/integration/API/Federation/Util.hs | 28 ------ services/galley/test/integration/API/Util.hs | 7 -- 5 files changed, 43 insertions(+), 123 deletions(-) diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 844fd6e295e..714a75d7254 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -874,3 +874,37 @@ testConversationWithoutFederation = withModifiedBackend $ \domain -> do [alice, bob] <- createAndConnectUsers [domain, domain] void $ postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 + +testPostConvWithUnreachableRemoteUsers :: App () +testPostConvWithUnreachableRemoteUsers = do + [alice, alex] <- createAndConnectUsers [OwnDomain, OtherDomain] + resourcePool <- asks resourcePool + runCodensity (acquireResources 2 resourcePool) $ \[unreachableBackend, reachableBackend] -> do + runCodensity (startDynamicBackend reachableBackend mempty) $ \_ -> do + unreachableUsers <- runCodensity (startDynamicBackend unreachableBackend mempty) $ \_ -> do + let downDomain = unreachableBackend.berDomain + ownDomain <- asString OwnDomain + otherDomain <- asString OtherDomain + void $ BrigI.createFedConn downDomain (BrigI.FedConn ownDomain "full_search" Nothing) + void $ BrigI.createFedConn downDomain (BrigI.FedConn otherDomain "full_search" Nothing) + users <- replicateM 3 (randomUser downDomain def) + for_ users $ \user -> do + connectUsers [alice, user] + connectUsers [alex, user] + -- creating the conv here would work. + pure users + + reachableUsers <- replicateM 2 (randomUser reachableBackend.berDomain def) + for_ reachableUsers $ \user -> do + connectUsers [alice, user] + connectUsers [alex, user] + + withWebSockets [alice, alex] $ \[wssAlice, wssAlex] -> do + -- unreachableBackend is still allocated, but the backend is down. creating the conv here doesn't work. + let payload = defProteus {name = Just "some chat", qualifiedUsers = [alex] <> reachableUsers <> unreachableUsers} + postConversation alice payload >>= assertStatus 533 + + convs <- getAllConvs alice + for_ convs $ \conv -> conv %. "type" `shouldNotMatchInt` 0 + assertNoEvent 2 wssAlice + assertNoEvent 2 wssAlex diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index c2f9efef3d1..bb4e4a5d573 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -180,6 +180,15 @@ shouldMatchInt :: App () shouldMatchInt = shouldMatch +shouldNotMatchInt :: + (MakesValue a, HasCallStack) => + -- | The actual value + a -> + -- | The expected value + Int -> + App () +shouldNotMatchInt = shouldNotMatch + shouldMatchRange :: (MakesValue a, HasCallStack) => -- | The actual value diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 9d5ef6b1a4d..18ce92d9e20 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -27,7 +27,6 @@ where import API.CustomBackend qualified as CustomBackend import API.Federation qualified as Federation -import API.Federation.Util import API.MLS qualified import API.MessageTimer qualified as MessageTimer import API.Roles qualified as Roles @@ -130,7 +129,6 @@ tests s = test s "metrics" metrics, test s "fetch conversation by qualified ID (v2)" testGetConvQualifiedV2, test s "create Proteus conversation" postProteusConvOk, - test s "create conversation with remote users, some unreachable" (postConvWithUnreachableRemoteUsers $ Set.fromList [rb1, rb2, rb3, rb4]), test s "get empty conversations" getConvsOk, test s "get conversations by ids" getConvsOk2, test s "fail to get >500 conversations with v2 API" getConvsFailMaxSizeV2, @@ -249,39 +247,6 @@ tests s = test s "send typing indicators with invalid pyaload" postTypingIndicatorsHandlesNonsense ] ] - rb1, rb2, rb3, rb4 :: Remote Backend - rb1 = - toRemoteUnsafe - (Domain "c.example.com") - ( Backend - { bReachable = BackendReachable, - bUsers = 2 - } - ) - rb2 = - toRemoteUnsafe - (Domain "d.example.com") - ( Backend - { bReachable = BackendReachable, - bUsers = 1 - } - ) - rb3 = - toRemoteUnsafe - (Domain "e.example.com") - ( Backend - { bReachable = BackendUnreachable, - bUsers = 2 - } - ) - rb4 = - toRemoteUnsafe - (Domain "f.example.com") - ( Backend - { bReachable = BackendUnreachable, - bUsers = 1 - } - ) getNotFullyConnectedBackendsMock :: Mock LByteString getNotFullyConnectedBackendsMock = "get-not-fully-connected-backends" ~> NonConnectedBackends mempty @@ -356,59 +321,6 @@ postProteusConvOk = do EdConversation c' -> assertConvEquals cnv c' _ -> assertFailure "Unexpected event data" -postConvWithUnreachableRemoteUsers :: Set (Remote Backend) -> TestM () -postConvWithUnreachableRemoteUsers rbs = do - c <- view tsCannon - (alice, _qAlice) <- randomUserTuple - (alex, qAlex) <- randomUserTuple - connectUsers alice (singleton alex) - (allRemotes, participatingRemotes) <- do - v <- forM (toList rbs) $ \rb -> do - users <- connectBackend alice rb - pure (users, participating rb users) - pure $ foldr (\(a, p) acc -> bimap ((<>) a) ((<>) p) acc) ([], []) v - liftIO $ do - let notParticipatingRemotes = allRemotes \\ participatingRemotes - assertBool "No reachable backend in the test" (not (null participatingRemotes)) - assertBool "No unreachable backend in the test" (not (null notParticipatingRemotes)) - - let convName = "some chat" - otherLocals = [qAlex] - joiners = allRemotes <> otherLocals - unreachableBackends = - Set.fromList $ - foldMap - ( \rb -> - guard (rbReachable rb == BackendUnreachable) - $> tDomain rb - ) - rbs - WS.bracketR2 c alice alex $ \(wsAlice, wsAlex) -> do - void - $ withTempMockFederator' - ( asum - [ "get-not-fully-connected-backends" ~> NonConnectedBackends mempty, - mockUnreachableFor unreachableBackends, - "on-conversation-created" ~> EmptyResponse, - "on-conversation-updated" ~> EmptyResponse - ] - ) - $ postConvQualified - alice - Nothing - defNewProteusConv - { newConvName = checked convName, - newConvQualifiedUsers = joiners - } - getAllConvs alice - liftIO $ - assertEqual - "Alice does have a group conversation, while she should not!" - [] - groupConvs - WS.assertNoEvent (3 # Second) [wsAlice, wsAlex] -- TODO: sometimes, (at least?) one of these users gets a "connection accepted" event. - postCryptoMessageVerifyMsgSentAndRejectIfMissingClient :: TestM () postCryptoMessageVerifyMsgSentAndRejectIfMissingClient = do localDomain <- viewFederationDomain diff --git a/services/galley/test/integration/API/Federation/Util.hs b/services/galley/test/integration/API/Federation/Util.hs index c4e6a41ea49..84fc3acea22 100644 --- a/services/galley/test/integration/API/Federation/Util.hs +++ b/services/galley/test/integration/API/Federation/Util.hs @@ -17,17 +17,10 @@ module API.Federation.Util ( mkHandler, - - -- * the remote backend type - BackendReachability (..), - Backend (..), - rbReachable, - participating, ) where import Data.Kind -import Data.Qualified import Data.SOP import Data.String.Conversions import GHC.TypeLits @@ -111,24 +104,3 @@ instance PartialAPI (Named (name :: Symbol) endpoint :<|> api) (Named name h) where mkHandler h = h :<|> mkHandler @api EmptyAPI - --------------------------------------------------------------------------------- --- The remote backend type - -data BackendReachability = BackendReachable | BackendUnreachable - deriving (Eq, Ord) - -data Backend = Backend - { bReachable :: BackendReachability, - bUsers :: Nat - } - deriving (Eq, Ord) - -rbReachable :: Remote Backend -> BackendReachability -rbReachable = bReachable . tUnqualified - -participating :: Remote Backend -> [a] -> [a] -participating rb users = - if rbReachable rb == BackendReachable - then users - else [] diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 16938edb549..950638f5bab 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -20,7 +20,6 @@ module API.Util where -import API.Federation.Util import API.SQS qualified as SQS import Bilge hiding (timeout) import Bilge.Assert @@ -2774,9 +2773,3 @@ createAndConnectUsers domains = do (False, True) -> connectWithRemoteUser (qUnqualified b) a (False, False) -> pure () pure users - -connectBackend :: UserId -> Remote Backend -> TestM [Qualified UserId] -connectBackend usr (tDomain &&& bUsers . tUnqualified -> (d, c)) = do - users <- replicateM (fromIntegral c) (randomQualifiedId d) - mapM_ (connectWithRemoteUser usr) users - pure users From 7ef9d8fae8be3da41de6c7be88ec555524864d00 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 20 Sep 2024 08:33:32 +0200 Subject: [PATCH 083/136] NewTeam types refactoring (#4257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Separate BindingNewTeam for NonBindingNewTeam * Rename BindingNewTeam → NewTeam * Remove NewTeam lenses --- .../5-internal/new-team-types-refactoring | 1 + .../src/Wire/API/Routes/Internal/Galley.hs | 2 +- .../src/Wire/API/Routes/Public/Galley/Team.hs | 37 +- libs/wire-api/src/Wire/API/Team.hs | 79 +--- libs/wire-api/src/Wire/API/User.hs | 6 +- .../golden/Test/Wire/API/Golden/Generated.hs | 4 +- .../Generated/BindingNewTeamUser_user.hs | 60 ++- .../Golden/Generated/BindingNewTeam_team.hs | 353 ------------------ .../Wire/API/Golden/Generated/NewTeam_team.hs | 283 ++++++++++++++ .../Wire/API/Golden/Generated/NewUser_user.hs | 27 +- ..._1.json => testObject_NewTeam_team_1.json} | 0 ...0.json => testObject_NewTeam_team_10.json} | 0 ...1.json => testObject_NewTeam_team_11.json} | 0 ...2.json => testObject_NewTeam_team_12.json} | 0 ...3.json => testObject_NewTeam_team_13.json} | 0 ...4.json => testObject_NewTeam_team_14.json} | 0 ...5.json => testObject_NewTeam_team_15.json} | 0 ...6.json => testObject_NewTeam_team_16.json} | 0 ...7.json => testObject_NewTeam_team_17.json} | 0 ...8.json => testObject_NewTeam_team_18.json} | 0 ...9.json => testObject_NewTeam_team_19.json} | 0 ..._2.json => testObject_NewTeam_team_2.json} | 0 ...0.json => testObject_NewTeam_team_20.json} | 0 ..._3.json => testObject_NewTeam_team_3.json} | 0 ..._4.json => testObject_NewTeam_team_4.json} | 0 ..._5.json => testObject_NewTeam_team_5.json} | 0 ..._6.json => testObject_NewTeam_team_6.json} | 0 ..._7.json => testObject_NewTeam_team_7.json} | 0 ..._8.json => testObject_NewTeam_team_8.json} | 0 ..._9.json => testObject_NewTeam_team_9.json} | 0 .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 2 +- libs/wire-api/wire-api.cabal | 2 +- .../src/Wire/GalleyAPIAccess.hs | 2 +- .../src/Wire/GalleyAPIAccess/Rpc.hs | 2 +- services/brig/src/Brig/API/Public.hs | 6 +- services/brig/src/Brig/API/User.hs | 10 +- services/brig/test/integration/API/Team.hs | 2 +- .../brig/test/integration/API/Team/Util.hs | 8 +- .../integration/API/UserPendingActivation.hs | 4 +- services/galley/src/Galley/API/Teams.hs | 43 +-- services/galley/test/integration/API/Util.hs | 4 +- services/spar/test-integration/Util/Core.hs | 4 +- tools/stern/test/integration/Util.hs | 2 +- 43 files changed, 409 insertions(+), 534 deletions(-) create mode 100644 changelog.d/5-internal/new-team-types-refactoring delete mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeam_team.hs create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeam_team.hs rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_1.json => testObject_NewTeam_team_1.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_10.json => testObject_NewTeam_team_10.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_11.json => testObject_NewTeam_team_11.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_12.json => testObject_NewTeam_team_12.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_13.json => testObject_NewTeam_team_13.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_14.json => testObject_NewTeam_team_14.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_15.json => testObject_NewTeam_team_15.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_16.json => testObject_NewTeam_team_16.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_17.json => testObject_NewTeam_team_17.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_18.json => testObject_NewTeam_team_18.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_19.json => testObject_NewTeam_team_19.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_2.json => testObject_NewTeam_team_2.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_20.json => testObject_NewTeam_team_20.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_3.json => testObject_NewTeam_team_3.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_4.json => testObject_NewTeam_team_4.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_5.json => testObject_NewTeam_team_5.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_6.json => testObject_NewTeam_team_6.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_7.json => testObject_NewTeam_team_7.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_8.json => testObject_NewTeam_team_8.json} (100%) rename libs/wire-api/test/golden/{testObject_BindingNewTeam_team_9.json => testObject_NewTeam_team_9.json} (100%) diff --git a/changelog.d/5-internal/new-team-types-refactoring b/changelog.d/5-internal/new-team-types-refactoring new file mode 100644 index 00000000000..70b4ade0568 --- /dev/null +++ b/changelog.d/5-internal/new-team-types-refactoring @@ -0,0 +1 @@ +Simplify NewTeam and related types and remove lenses 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 22f23d50a31..ea493672d82 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -216,7 +216,7 @@ type ITeamsAPIBase = :<|> Named "create-binding-team" ( ZUser - :> ReqBody '[JSON] BindingNewTeam + :> ReqBody '[JSON] NewTeam :> MultiVerb1 'PUT '[JSON] diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs index 4c0c61751d4..ae0c36aca68 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Team.hs @@ -17,7 +17,12 @@ module Wire.API.Routes.Public.Galley.Team where +import Control.Lens ((?~)) +import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Id +import Data.OpenApi.Schema qualified as S +import Data.Range +import Data.Schema import Imports import Servant import Servant.OpenApi.Internal.Orphans () @@ -28,8 +33,37 @@ import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.Routes.Version import Wire.API.Team +import Wire.API.Team.Member import Wire.API.Team.Permission +-- | FUTUREWORK: remove when the create-non-binding-team endpoint is deleted +data NonBindingNewTeam = NonBindingNewTeam + { teamName :: Range 1 256 Text, + teamIcon :: Icon, + teamIconKey :: Maybe (Range 1 256 Text), + teamMembers :: Maybe (Range 1 127 [TeamMember]) + } + deriving stock (Eq, Show) + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema NonBindingNewTeam) + +instance ToSchema NonBindingNewTeam where + schema = + object "NonBindingNewTeam" $ + NonBindingNewTeam + <$> (.teamName) .= fieldWithDocModifier "name" (description ?~ "team name") schema + <*> (.teamIcon) .= fieldWithDocModifier "icon" (description ?~ "team icon (asset ID)") schema + <*> (.teamIconKey) .= maybe_ (optFieldWithDocModifier "icon_key" (description ?~ "team icon asset key") schema) + <*> (.teamMembers) + .= maybe_ + ( optFieldWithDocModifier + "members" + (description ?~ "initial team member ids (between 1 and 127)") + sch + ) + where + sch :: ValueSchema SwaggerDoc (Range 1 127 [TeamMember]) + sch = fromRange .= rangedSchema (array schema) + type TeamAPI = Named "create-non-binding-team" @@ -37,8 +71,7 @@ type TeamAPI = :> Until 'V4 :> ZUser :> ZConn - :> CanThrow 'NotConnected - :> CanThrow 'UserBindingExists + :> CanThrow InvalidAction :> "teams" :> ReqBody '[Servant.JSON] NonBindingNewTeam :> MultiVerb diff --git a/libs/wire-api/src/Wire/API/Team.hs b/libs/wire-api/src/Wire/API/Team.hs index cffcd2bac95..a1fc3c99b8a 100644 --- a/libs/wire-api/src/Wire/API/Team.hs +++ b/libs/wire-api/src/Wire/API/Team.hs @@ -40,15 +40,9 @@ module Wire.API.Team teamListHasMore, -- * NewTeam - BindingNewTeam (..), - bindingNewTeamObjectSchema, - NonBindingNewTeam (..), NewTeam (..), + newTeamObjectSchema, newNewTeam, - newTeamName, - newTeamIcon, - newTeamIconKey, - newTeamMembers, -- * TeamUpdateData TeamUpdateData (..), @@ -84,7 +78,6 @@ import Data.Text.Encoding qualified as T import Imports import Test.QuickCheck.Gen (suchThat) import Wire.API.Asset (AssetKey) -import Wire.API.Team.Member (TeamMember) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -------------------------------------------------------------------------------- @@ -177,62 +170,27 @@ instance ToSchema TeamList where -------------------------------------------------------------------------------- -- NewTeam -newtype BindingNewTeam = BindingNewTeam {bntTeam :: NewTeam ()} - deriving stock (Eq, Show, Generic) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema BindingNewTeam) - -instance ToSchema BindingNewTeam where - schema = object "BindingNewTeam" bindingNewTeamObjectSchema - -bindingNewTeamObjectSchema :: ObjectSchema SwaggerDoc BindingNewTeam -bindingNewTeamObjectSchema = - BindingNewTeam <$> unwrap .= newTeamObjectSchema null_ - where - unwrap (BindingNewTeam nt) = nt - --- FUTUREWORK: since new team members do not get serialized, we zero them here. --- it may be worth looking into how this can be solved in the types. -instance Arbitrary BindingNewTeam where - arbitrary = - BindingNewTeam . zeroTeamMembers <$> arbitrary @(NewTeam ()) - where - zeroTeamMembers tms = tms {_newTeamMembers = Nothing} - --- | FUTUREWORK: this is dead code! remove! -newtype NonBindingNewTeam = NonBindingNewTeam (NewTeam (Range 1 127 [TeamMember])) - deriving stock (Eq, Show, Generic) - deriving (FromJSON, ToJSON, S.ToSchema) via (Schema NonBindingNewTeam) - -instance ToSchema NonBindingNewTeam where - schema = - object "NonBindingNewTeam" $ - NonBindingNewTeam - <$> unwrap .= newTeamObjectSchema sch - where - unwrap (NonBindingNewTeam nt) = nt - - sch :: ValueSchema SwaggerDoc (Range 1 127 [TeamMember]) - sch = fromRange .= rangedSchema (array schema) - -data NewTeam a = NewTeam - { _newTeamName :: Range 1 256 Text, - _newTeamIcon :: Icon, - _newTeamIconKey :: Maybe (Range 1 256 Text), - _newTeamMembers :: Maybe a +data NewTeam = NewTeam + { newTeamName :: Range 1 256 Text, + newTeamIcon :: Icon, + newTeamIconKey :: Maybe (Range 1 256 Text) } deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform (NewTeam a)) + deriving (ToJSON, FromJSON, S.ToSchema) via (Schema NewTeam) + deriving (Arbitrary) via (GenericUniform NewTeam) -newNewTeam :: Range 1 256 Text -> Icon -> NewTeam a -newNewTeam nme ico = NewTeam nme ico Nothing Nothing - -newTeamObjectSchema :: ValueSchema SwaggerDoc a -> ObjectSchema SwaggerDoc (NewTeam a) -newTeamObjectSchema sch = +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) - <*> _newTeamMembers .= maybe_ (optFieldWithDocModifier "members" (description ?~ "initial team member ids (between 1 and 127)") sch) + <$> 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) + +instance ToSchema NewTeam where + schema = object "NewTeam" newTeamObjectSchema + +newNewTeam :: Range 1 256 Text -> Icon -> NewTeam +newNewTeam nme ico = NewTeam nme ico Nothing -------------------------------------------------------------------------------- -- TeamUpdateData @@ -322,6 +280,5 @@ instance ToSchema TeamDeleteData where makeLenses ''Team makeLenses ''TeamList -makeLenses ''NewTeam makeLenses ''TeamUpdateData makeLenses ''TeamDeleteData diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 08a8f758db9..5d5a42af3b1 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -206,7 +206,7 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Locale import Wire.API.Provider.Service (ServiceRef) import Wire.API.Routes.MultiVerb -import Wire.API.Team (BindingNewTeam, bindingNewTeamObjectSchema) +import Wire.API.Team import Wire.API.Team.Member (TeamMember) import Wire.API.Team.Member qualified as TeamMember import Wire.API.Team.Role @@ -1305,7 +1305,7 @@ newTeamUserTeamId = \case NewTeamMemberSSO tid -> Just tid data BindingNewTeamUser = BindingNewTeamUser - { bnuTeam :: BindingNewTeam, + { bnuTeam :: NewTeam, bnuCurrency :: Maybe Currency.Alpha -- FUTUREWORK: -- Remove Currency selection once billing supports currency changes after team creation @@ -1319,7 +1319,7 @@ instance ToSchema BindingNewTeamUser where object "BindingNewTeamUser" $ BindingNewTeamUser <$> bnuTeam - .= bindingNewTeamObjectSchema + .= newTeamObjectSchema <*> bnuCurrency .= maybe_ (optField "currency" genericToSchema) 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 ec9eb270c2f..c0827feab02 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 @@ -37,7 +37,6 @@ import Test.Wire.API.Golden.Generated.AssetSize_user qualified import Test.Wire.API.Golden.Generated.AssetToken_user qualified import Test.Wire.API.Golden.Generated.Asset_asset qualified import Test.Wire.API.Golden.Generated.BindingNewTeamUser_user qualified -import Test.Wire.API.Golden.Generated.BindingNewTeam_team qualified import Test.Wire.API.Golden.Generated.BotConvView_provider qualified import Test.Wire.API.Golden.Generated.BotUserView_provider qualified import Test.Wire.API.Golden.Generated.CheckHandles_user qualified @@ -125,6 +124,7 @@ import Test.Wire.API.Golden.Generated.NewProvider_provider qualified import Test.Wire.API.Golden.Generated.NewServiceResponse_provider qualified import Test.Wire.API.Golden.Generated.NewService_provider qualified import Test.Wire.API.Golden.Generated.NewTeamMember_team qualified +import Test.Wire.API.Golden.Generated.NewTeam_team qualified import Test.Wire.API.Golden.Generated.NewUserPublic_user qualified import Test.Wire.API.Golden.Generated.NewUser_user qualified import Test.Wire.API.Golden.Generated.OtherMemberUpdate_user qualified @@ -1156,7 +1156,7 @@ tests = testGroup "Golden: ServiceTagList_provider" $ testObjects [(Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_1, "testObject_ServiceTagList_provider_1.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_2, "testObject_ServiceTagList_provider_2.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_3, "testObject_ServiceTagList_provider_3.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_4, "testObject_ServiceTagList_provider_4.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_5, "testObject_ServiceTagList_provider_5.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_6, "testObject_ServiceTagList_provider_6.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_7, "testObject_ServiceTagList_provider_7.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_8, "testObject_ServiceTagList_provider_8.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_9, "testObject_ServiceTagList_provider_9.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_10, "testObject_ServiceTagList_provider_10.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_11, "testObject_ServiceTagList_provider_11.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_12, "testObject_ServiceTagList_provider_12.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_13, "testObject_ServiceTagList_provider_13.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_14, "testObject_ServiceTagList_provider_14.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_15, "testObject_ServiceTagList_provider_15.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_16, "testObject_ServiceTagList_provider_16.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_17, "testObject_ServiceTagList_provider_17.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_18, "testObject_ServiceTagList_provider_18.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_19, "testObject_ServiceTagList_provider_19.json"), (Test.Wire.API.Golden.Generated.ServiceTagList_provider.testObject_ServiceTagList_provider_20, "testObject_ServiceTagList_provider_20.json")], testGroup "Golden: BindingNewTeam_team" $ - testObjects [(Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_1, "testObject_BindingNewTeam_team_1.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_2, "testObject_BindingNewTeam_team_2.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_3, "testObject_BindingNewTeam_team_3.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_4, "testObject_BindingNewTeam_team_4.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_5, "testObject_BindingNewTeam_team_5.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_6, "testObject_BindingNewTeam_team_6.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_7, "testObject_BindingNewTeam_team_7.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_8, "testObject_BindingNewTeam_team_8.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_9, "testObject_BindingNewTeam_team_9.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_10, "testObject_BindingNewTeam_team_10.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_11, "testObject_BindingNewTeam_team_11.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_12, "testObject_BindingNewTeam_team_12.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_13, "testObject_BindingNewTeam_team_13.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_14, "testObject_BindingNewTeam_team_14.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_15, "testObject_BindingNewTeam_team_15.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_16, "testObject_BindingNewTeam_team_16.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_17, "testObject_BindingNewTeam_team_17.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_18, "testObject_BindingNewTeam_team_18.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_19, "testObject_BindingNewTeam_team_19.json"), (Test.Wire.API.Golden.Generated.BindingNewTeam_team.testObject_BindingNewTeam_team_20, "testObject_BindingNewTeam_team_20.json")], + testObjects [(Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_1, "testObject_NewTeam_team_1.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_2, "testObject_NewTeam_team_2.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_3, "testObject_NewTeam_team_3.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_4, "testObject_NewTeam_team_4.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_5, "testObject_NewTeam_team_5.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_6, "testObject_NewTeam_team_6.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_7, "testObject_NewTeam_team_7.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_8, "testObject_NewTeam_team_8.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_9, "testObject_NewTeam_team_9.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_10, "testObject_NewTeam_team_10.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_11, "testObject_NewTeam_team_11.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_12, "testObject_NewTeam_team_12.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_13, "testObject_NewTeam_team_13.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_14, "testObject_NewTeam_team_14.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_15, "testObject_NewTeam_team_15.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_16, "testObject_NewTeam_team_16.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_17, "testObject_NewTeam_team_17.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_18, "testObject_NewTeam_team_18.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_19, "testObject_NewTeam_team_19.json"), (Test.Wire.API.Golden.Generated.NewTeam_team.testObject_NewTeam_team_20, "testObject_NewTeam_team_20.json")], testGroup "Golden: TeamBinding_team" $ testObjects [(Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_1, "testObject_TeamBinding_team_1.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_2, "testObject_TeamBinding_team_2.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_3, "testObject_TeamBinding_team_3.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_4, "testObject_TeamBinding_team_4.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_5, "testObject_TeamBinding_team_5.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_6, "testObject_TeamBinding_team_6.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_7, "testObject_TeamBinding_team_7.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_8, "testObject_TeamBinding_team_8.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_9, "testObject_TeamBinding_team_9.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_10, "testObject_TeamBinding_team_10.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_11, "testObject_TeamBinding_team_11.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_12, "testObject_TeamBinding_team_12.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_13, "testObject_TeamBinding_team_13.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_14, "testObject_TeamBinding_team_14.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_15, "testObject_TeamBinding_team_15.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_16, "testObject_TeamBinding_team_16.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_17, "testObject_TeamBinding_team_17.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_18, "testObject_TeamBinding_team_18.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_19, "testObject_TeamBinding_team_19.json"), (Test.Wire.API.Golden.Generated.TeamBinding_team.testObject_TeamBinding_team_20, "testObject_TeamBinding_team_20.json")], testGroup "Golden: Team_team" $ diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeamUser_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeamUser_user.hs index 37dc8807bf7..d8151e07736 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeamUser_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeamUser_user.hs @@ -24,36 +24,23 @@ import Data.UUID as UUID import Imports (Maybe (Just, Nothing), fromJust) import Wire.API.Asset import Wire.API.Team - ( BindingNewTeam (BindingNewTeam), - Icon (..), - NewTeam - ( NewTeam, - _newTeamIcon, - _newTeamIconKey, - _newTeamMembers, - _newTeamName - ), - ) import Wire.API.User (BindingNewTeamUser (..)) testObject_BindingNewTeamUser_user_1 :: BindingNewTeamUser testObject_BindingNewTeamUser_user_1 = BindingNewTeamUser { bnuTeam = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\fe\ENQ\1011760zm\166331\&6+)g;5\989956Z\8196\&41\DC1\n\STX\ETX%|\NULM\996272S=`I\59956UK1\1003466]X\r\SUBa\EM!\74407+\ETXepRw\ACK\ENQ#\127835\1061771\1036174\1018930UX\66821]>i&r\137805\1055913Z\1070413\&6\DC4\DC4\1024114\1058863\1044802\ESC\SYNa4\NUL\1059602\1015948\123628\tLZ\ACKw$=\SYNu\ETXE1\63200C'\ENQ\151764\47003\134542$\100516\1112326\&9;#\1044763\1015439&\ESC\1026916k/\tu\\pk\NUL\STX\1083510)\FS/Lni]Q\NUL\SIZ|=\DC1V]]\FS5\156475U6>(\17233'\CAN\179678%'I1-D\"\1098303\n\78699\npkHY#\NUL\1014868u]\1078674\147414\STX\USj'\993967'\CAN\1042144&\35396E\37802=\135058Da\STX\v\1100351=\1083565V#\993183\RS\FSN#`uny\1003178\1094898\&53#\DEL/|,+\243pW\44721i4j", - _newTeamIcon = DefaultIcon, - _newTeamIconKey = - Just - ( unsafeRange - "\ACKc\151665L ,\STX\NAK[\SUB\DC1\63043\GSxe\1000559c\US\DC4<`|\29113\147003Q\1028347\987929<{\NUL^\FST\141040J\1071963U\EOT\SYN\65033\DC3G\1003198+\EM\181213xr\v\32449\ESCyTD@>Ou\70496j\43574E\STX6e\983711\SO\ESC\135327\&34\1063210\41000\1018151\&8\1057958\163400uxW\41951\1080957Y\ACK\141633(\CAN\FS$D\1055410\148196\36291\SI3\1082544#\SYN?\ETX\ACK0*W3\ACK\1085759i\35231h\NAK-\42529\1034909\ACKH?\\Tv\1098776\54330Q\46933\DLE-@k%{=4\SUB!w&\1042435D\DC2cuT^\DC4\GSH\b\137953^]\985924jXA\1010085\133569@fV,OA\185077\38677F\154006Az^g7\177712),C\1020911}.\72736\996321~V\1077077\1024186(9^z\1014725\67354\&3}Gj\1078379\fd>\57781\1088153Y\177269p#^\1054503L`S~\1101440\DC23\EOT\145319\24591\92747\13418as:F\ETX" - ), - _newTeamMembers = Nothing - } - ), + NewTeam + { newTeamName = + unsafeRange + "\fe\ENQ\1011760zm\166331\&6+)g;5\989956Z\8196\&41\DC1\n\STX\ETX%|\NULM\996272S=`I\59956UK1\1003466]X\r\SUBa\EM!\74407+\ETXepRw\ACK\ENQ#\127835\1061771\1036174\1018930UX\66821]>i&r\137805\1055913Z\1070413\&6\DC4\DC4\1024114\1058863\1044802\ESC\SYNa4\NUL\1059602\1015948\123628\tLZ\ACKw$=\SYNu\ETXE1\63200C'\ENQ\151764\47003\134542$\100516\1112326\&9;#\1044763\1015439&\ESC\1026916k/\tu\\pk\NUL\STX\1083510)\FS/Lni]Q\NUL\SIZ|=\DC1V]]\FS5\156475U6>(\17233'\CAN\179678%'I1-D\"\1098303\n\78699\npkHY#\NUL\1014868u]\1078674\147414\STX\USj'\993967'\CAN\1042144&\35396E\37802=\135058Da\STX\v\1100351=\1083565V#\993183\RS\FSN#`uny\1003178\1094898\&53#\DEL/|,+\243pW\44721i4j", + newTeamIcon = DefaultIcon, + newTeamIconKey = + Just + ( unsafeRange + "\ACKc\151665L ,\STX\NAK[\SUB\DC1\63043\GSxe\1000559c\US\DC4<`|\29113\147003Q\1028347\987929<{\NUL^\FST\141040J\1071963U\EOT\SYN\65033\DC3G\1003198+\EM\181213xr\v\32449\ESCyTD@>Ou\70496j\43574E\STX6e\983711\SO\ESC\135327\&34\1063210\41000\1018151\&8\1057958\163400uxW\41951\1080957Y\ACK\141633(\CAN\FS$D\1055410\148196\36291\SI3\1082544#\SYN?\ETX\ACK0*W3\ACK\1085759i\35231h\NAK-\42529\1034909\ACKH?\\Tv\1098776\54330Q\46933\DLE-@k%{=4\SUB!w&\1042435D\DC2cuT^\DC4\GSH\b\137953^]\985924jXA\1010085\133569@fV,OA\185077\38677F\154006Az^g7\177712),C\1020911}.\72736\996321~V\1077077\1024186(9^z\1014725\67354\&3}Gj\1078379\fd>\57781\1088153Y\177269p#^\1054503L`S~\1101440\DC23\EOT\145319\24591\92747\13418as:F\ETX" + ) + }, bnuCurrency = Just XUA } @@ -61,19 +48,16 @@ testObject_BindingNewTeamUser_user_2 :: BindingNewTeamUser testObject_BindingNewTeamUser_user_2 = BindingNewTeamUser { bnuTeam = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "G\EOT\DC47\1030077bCy\83226&5\"\96437B$\STX\DC2QJb_\15727\1104659Y \156055\1044397Y\1004994g\v\991186xkJUi\1028168.=-\1054839\&2\1113630U\ESC]\SUB\1091929\DLE}R\157290\DC1\1111740\1096562+R/\1083774\170894p(M\ENQ5Fw<\144133E\1005699R\DLE44\1060383\SO%@FPG\986135JJ\vE\GSz\RS_\tb]0t_Ax}\rt\1057458h\DC3O\ACK\991050`\1038022vm-?$!)~\152722bh\RS\1011653\1007510\&0x \1092001\1078327+)A&mRfL\1109449\ENQ\1049319>K@\US\1006511\ab\vPDWG,\1062888/J~)%7?aRr\989765\&4*^\1035118K*\996771\EM\"\SO\987994\186383l\n\tE\136474\1037228\NAK\a\n\78251c?\\\ENQj\"\ESCpe\98450\NUL=\EM>J", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "-\ACK\59597v^\SOH_>p\13939\ETX\SYN\EOT\ENQ\2922\1080262]\45888\917616\SI;v}q\47502\190968\a\SI\1113366&~\51980<\GS\1024632`,\1033586sn\2651H\160130\1100746\176758:qNi]\1051932'\1000100#\a#T\171243}\990743\DC2\1008291M_\FS\DC4\988716\1091854\EM,\SO\CAN^]\77867\&9\1112574-\a\SOHID. FAp\EOT\1033411\1004852(S\1052010\68416\129120\DLEsI\ETXe|Mv-\"q\49103zM\14348$H\SOH\139130\1004399D]\SUB\1056469\ESC\151220qW2\ENQ\1104272\RSy\1018323gg\1018839 /\1079527\98975\18928~&y\b\ACK\1084334\1047493\36198\SO\FS\SYN\RSt\\a.V\SO\&Hy8k\US$O\699Xu/=" - ), - _newTeamMembers = Nothing - } - ), + NewTeam + { newTeamName = + unsafeRange + "G\EOT\DC47\1030077bCy\83226&5\"\96437B$\STX\DC2QJb_\15727\1104659Y \156055\1044397Y\1004994g\v\991186xkJUi\1028168.=-\1054839\&2\1113630U\ESC]\SUB\1091929\DLE}R\157290\DC1\1111740\1096562+R/\1083774\170894p(M\ENQ5Fw<\144133E\1005699R\DLE44\1060383\SO%@FPG\986135JJ\vE\GSz\RS_\tb]0t_Ax}\rt\1057458h\DC3O\ACK\991050`\1038022vm-?$!)~\152722bh\RS\1011653\1007510\&0x \1092001\1078327+)A&mRfL\1109449\ENQ\1049319>K@\US\1006511\ab\vPDWG,\1062888/J~)%7?aRr\989765\&4*^\1035118K*\996771\EM\"\SO\987994\186383l\n\tE\136474\1037228\NAK\a\n\78251c?\\\ENQj\"\ESCpe\98450\NUL=\EM>J", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "-\ACK\59597v^\SOH_>p\13939\ETX\SYN\EOT\ENQ\2922\1080262]\45888\917616\SI;v}q\47502\190968\a\SI\1113366&~\51980<\GS\1024632`,\1033586sn\2651H\160130\1100746\176758:qNi]\1051932'\1000100#\a#T\171243}\990743\DC2\1008291M_\FS\DC4\988716\1091854\EM,\SO\CAN^]\77867\&9\1112574-\a\SOHID. FAp\EOT\1033411\1004852(S\1052010\68416\129120\DLEsI\ETXe|Mv-\"q\49103zM\14348$H\SOH\139130\1004399D]\SUB\1056469\ESC\151220qW2\ENQ\1104272\RSy\1018323gg\1018839 /\1079527\98975\18928~&y\b\ACK\1084334\1047493\36198\SO\FS\SYN\RSt\\a.V\SO\&Hy8k\US$O\699Xu/=" + ) + }, bnuCurrency = Nothing } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeam_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeam_team.hs deleted file mode 100644 index 8f97737dfab..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/BindingNewTeam_team.hs +++ /dev/null @@ -1,353 +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 Test.Wire.API.Golden.Generated.BindingNewTeam_team where - -import Data.Id (Id (Id)) -import Data.Range (unsafeRange) -import Data.UUID qualified as UUID (fromString) -import Imports (Maybe (Just, Nothing), fromJust) -import Wire.API.Asset (AssetKey (..), AssetRetention (..)) -import Wire.API.Team - ( BindingNewTeam (..), - Icon (..), - NewTeam - ( NewTeam, - _newTeamIcon, - _newTeamIconKey, - _newTeamMembers, - _newTeamName - ), - ) - -testObject_BindingNewTeam_team_1 :: BindingNewTeam -testObject_BindingNewTeam_team_1 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "UivH&\54922\98185p\USz\11724\r$\DC4j9P\r\"\1070851\3254\986624aF>E\1078807\139041B\EM&\1088459\DC4\174923+'\1103890R;!\GS\1017122\SIvv|\rmbGHz\1005234\95057\&3h\120904\\U|'\ETX;^&G\CAN\f\41076\&42\teq\1049559\SOV1}\RSaT\1014212aO7<;o\179606\f\1111896m)$PC\ESC7;f{\STXt\9533>\EOTX@4|/\tH\ENQ/D\144082\EM\121436C\99696Q\ENQT\1096609?d\ACK\1073806#H\127523\139127*\166004jo4wa\95243leQ*\1000542\1034344>@,\1045947\190894RF4QcNY96\168531\1051528G\1069460&J\\TzHUiG.C\SUB&\FSx\52616\167921\&3\1105098A\1054008B)\29142\31346r\1004296\ENQ&VCPa{\SOH\EMW\DEL\43500\97305\DLE/\1078579\SIc:b\SOH\132266)\35144\1100498\37490@5\983688I02g%%1bJl} :\1021555\SYN\64090\158870\143049" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_3 :: BindingNewTeam -testObject_BindingNewTeam_team_3 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\SUB_F\n\65091\140672\DC2>\1079041\74636t\n)1/% hL\DC2Ad\SOHXq6\DC1)\NUL\f6\fV\DC4r\1097128\DC1n\1107359,@\171217\118996\n\SUB%N\176824\ACK\33856Xv)\SYNz?\DC4\EMY\162050\&2\95792um8}\51420\DC2yW\NULHQ\ENQD[Fe\nk\999106\EM\25079Yk@##u}j\169850\153342\STXq\ESCir7) \27756%\1016104~\993971\&8\1085984je\1099724\&0*Gi3\120829je\CANQr>\1033571k1\63774c\1031586L\1015084\93833t\EOTW\999363\SUBo\fgh\ACK\172057C2\38697c\SUB)uW\r\fB\1042942Sf\SUB\SOH*5l\38586\SI\25991\EMB(\ENQ\133758/)!{\1006380\&9\STXA\DEL\16077fx&\180089T&\187029\DC4\52222[\r\v\n\1071241j2\166180/\1086576\ENQQo\fj\134496\129296\nb6\CAN3\RS9\EM\1000086ub\ETB3CY\GSsIz", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "3d4b563b-016c-49da-bad2-876ad0a5ecd2"))) AssetExpiring), - _newTeamIconKey = - Just - ( unsafeRange - "\FS\RSP\988567Gt\SYN-\47148nJ\1010840g^\n\r\177791\GSR\1010061q\988754\nQ\RS\1054014\GS#w\147936\171735\1064959\136621B\DC4\SUBLv\"S>\121093!]sB+6\DC1oc\ETB7\34513lR\95866\EMr%E\1077999B\98708A\1067109N\ETB?{\1065508/|cU\60733\141259]\92896\1102284\DLE\147332\1075446+\991438\t$F\96714he4\166964|k/!5Z~\83246\ETB\1017589\SOH\ENQ\1056989\&3E!{^\33558\&4fh\1029576N\1111705v\f\GS\998029mde!5\1027807y&\1062155xo,\STXrk\1071672\ENQ\SOHJoS\986695X\18929\994879a\991047\RS\1046020\EM\SOH3j\3901Z4\DC4\1068579l\52972n\ESC@ve#\SYN\GS\183587P4\1077298\ESC\170211:\157706z1*\USs\vd`\1059621/\39172\165682" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_6 :: BindingNewTeam -testObject_BindingNewTeam_team_6 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "v\188076hEWefuu\1006804jPx\158137k#\SOH\986725\STX\ETX^\ESC\n\CAN\8325p1D|S1\1064991\1102106\29079\SYN`\t0g\1034469,t\FSw\fDT\RS#H\SOH\145176\US{\1091499\1025650\984364lW\a,uil\SIN`5e:\SYN Y!\SYN\1025115tb\1085213", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "d7a467c6-8cd4-40cb-9e30-99b64bb11307"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "+&heN\1091941K\f_k\DLE(\33970\DC3\9833M\f\1029853\1098178\SI^s\1101855Ga,$\38078\SIb\DC3\f\"s{\ACK5\1025293\5649\US\DLE\SUB\1085641\70123\CAN,\1036517\158007\DC4 \1109215P\95245|f.>hEa\DLE^\ENQ\b]`\1112948<\GSZG\1004098\SOH\190360\24273*8p\FSF@OLpnXTmW\96553f\68110\1076109\25954Ze1 \SYNEm\27765f\ACK\987143" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_7 :: BindingNewTeam -testObject_BindingNewTeam_team_7 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\145552\1042892iz\1057971FT\14964;\1108369}\188917\1113471\&9\SO\991633\&7>hAC\NULH2O\177259m\187711\&2R(?W,=,\990725M\992456\aM\194790\SUB\47600q\SOlj\EOTj^.s~\rY%5lM,\26492=\ACK\1016899\188843>{\CAN\DLE\15878f=X9\SYN9\51145\159419TI4\17599\v\NAK6\1014936/\DLE\NAK\ACK\23564H<\ENQ\1029703e\ENQz\1017528:\6137\"rS\a\167660\FS\ETX\1059289\1031786\49012\DC4\DC4Q\"\1065200\&1:\1097556\UST.;\1042663\18380}", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "b199431c-e2ee-48c6-8f1b-56726626b493"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "D\RS\168552\SOH\1033444\128689Ll\GS\tW\1056953o\CAN\47716b\ETX|\US*=\1011088\1066392\988391\&6\999812" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_8 :: BindingNewTeam -testObject_BindingNewTeam_team_8 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "YwD\1023517r\NAK}\1083947\ACK\1047823\29742\EOT\1071030iI5g\1012255\t\"r\150087O\DC4?\53005\1100290\1108960\NUL\1060304qgg\DC1X)\NULL\1054528\CAN{\v4\NUL\93999\bvD#\1035811$aYFk\b\1102040\1089491\1042733\47133:1\179810S7\66745V)\1072087\v\96989\&3#\b\1104899c\27119Q/jPy\1015620P@Df\997914\51756H\1113361Xr\SO\ETB3%\1108760aF@3A\SI\ETB\STX mj9T=\DC3'XI\DC2?0\1093231\156858VHp?\1066163YU\42092\33083\72810,)\1113424\ETX96\153338z\42445/4T\136162\ESC\60427\1086321&\ETBS\1098748\14578z[\54638Z\DC2\"e\SUB\173931&rQ\fJG\100066\180037\155435s$\SUB$\50544S\162554E\ETX*\t+\63443WU*\144654\1042128\&8\NAK\999184a\t\EM\1097907_\DELOD\1006385/\23998\1100140SmfX", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "v\70188\46459h\SOH_\991979\DC3\ACKi\1000164\DC1\ETXW\72785\35679\DC2\23266\1026390\EOT\f%_\1064553\GS\SYN\ETB N\NULF\1005467\ENQLUua3\1089232M\8605\"\94879\SOH\RS\n-='\DC1B#\FS\136881>\DC3\132340\SI\GS\1088106G7v6w Z\4678\1051054\182628\170805\ESCP>\131111\1051383\1076729\v}?\5316Jg\SOH\SUB^pl\1101671\&2.\SOV\57380\DC3\22371\64509\ENQB\1045499\1076733\139492<\f\DEL2\19252Tz@6\DC3\71851x?\150161\36913\b\DLE\CANp\1081584\SYN\ETXN\1099776C\SI\SUB\DC1l]R\NULvL\1027446Nz\f-bf}f>\STXH\EM\136484+Zo\1034706\1062880\NAK}\adb\171356-\\-1\DC42\1046344\DC2\78894\&1/\33084b:\ENQ\1038950;Mw\FS\183866\1113547ITuy\1050264`SP\SOH\SO\GS\NAK\a\r7M\1069326\1064150\18615\n\SYN3V\ETXR\n1$e.\1096261B~yd_z\1047817\rV\1091351\RS\SYN\165050l\DC3\47200u\1058674u\"\aTc|sEw\1011190wTC|F\4735B\t\DC4&\bUEN(+M\SOF;\1099746\134573\EM20\nrPW\1017058$\1064809", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "X\1019453;\ENQW\ACKLk\996110\144662\ETB\n]\58553[~\10280&U\20125v`I\ETB\USl\983659\t\1090302?\17227KM3c\1067581\1030643= \ETBt5vKOg\NAK/NC2~i'\1062772Ojb\b\ETX\62742\1090035\DC1\SOH\NULFWc\1014613sU>P\SOH~\EMwUHU\SO#\55006\1081711!Nwn\1005601e\SOH\SUB\f\ETX\ETBT\DELl\110629BYU;a\1012448K7?,m\154276Xpa\48825\138301\EM ,M!~^g6}(\60133\36369\RS\8075gX}\161019)c\n\SOH2E" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_10 :: BindingNewTeam -testObject_BindingNewTeam_team_10 = - BindingNewTeam - ( NewTeam - { _newTeamName = unsafeRange "\b \SOH+\1056054;\t095\42390\n\STX2J\1002251\DC1UzD_\1110746\FS", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "\EOT\131569\ETB:\984737HL\SOH^bs\vG\157476{I\1096053]-J\FS\1107927\vs9\DLE\1000765vI`N\48159MZz" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_11 :: BindingNewTeam -testObject_BindingNewTeam_team_11 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\48005H\1082536\132304\157763\&5\RS\986337-\NAK\ESCR\nL\63954&bD\139428\SUBH\US\1040918\f\t;e\1064224\47101\tc\1087740e\1099415\DLE\ETX\DELI\65746\ETB\133884\SUB \SI\43795~FE\CAN6\162836\DEL\46062u\"\135684\1041611\FSFYI\t/{\ENQ\RS]j\1076782\US22\15884l\42366$\ETB\US\180023kL{\STX*\131382RMj\ESC\1091332W3H\1020399\FS\NAK^\"5\29653\32539*\1099111", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "\1109507I\ACK.\158786@y0\DLE\1083101n\\#skj\1019405Y_\1037580&x\1007219\GS\SIy\1104457B\SYN0\DC3VP1\1086698q\1024822\1081753\28211R\1100307*+\RS,MP\27076*;\n\NAK\47211\t\160463\nGj.\41290\1104539l\12622\FS\61112~\1076042\NUL.\1083842&\SOH}\SI\1080986\DC1+f^ZC\a'T\SOH\n\1020923\1097319U\1107987`W\r\\fX\n\1095366TF\1108756`h\97424[\46315ERdP5<<\1024109;\r\1095899\NULDy\28422\&5N/^\136134(\DC3\1045067\1061604\&6e\f:\SIB\DLEF-\1110200\17393\1064949Rfb\44582\aDrB\987948\13740\26738\NUL+\60859\&2.\a\a}\NAKpsFw\ETB\DC3 \186007\151693k~" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_12 :: BindingNewTeam -testObject_BindingNewTeam_team_12 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - ";\110872M\EOT\164161P]'\1041089\1094514\4118\1054714iFnRQV\43238@\992926\59902l\1099067\aKZ{\51124S\190890\fg*\n,`!V\STX\991695e'\1039967\SO0\37019p4d\STXs\1020471uK(c'\52929hjB\144953\SOt'h^\SYN\SYN0\1009487_\12064\166805thH\SI\1073479:\1019934l; n4c\1101781D[\1014388\&8Y+\1092407\EOTE\1058506\\0\168273KKTc)P1K\1042475\990753W\ETX<|\24888\&0|5{Y\986771M\DC4\vK\DLE\1089150\SOH\DC4\1013653.\ETBg\991717\DLE\"W\NUL9&0yYZ\1094524\v\11606\58174", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "\"C\ESC\SI0\ETB\69608p\12616|/O]\53852\SO \55172C\SYNN\SUB8\NUL\62584BxtH\SO*\1077819\&3.\1061851(\1100810w\GS\152525R{q\990825\&4\180037\150457:\187092\134288>\ETB\nl\1061158g\"\996841,6K\28384\1054272[\1019005\1016209N\24221eB!\188918C\EOT\STXX#El\ETB`\61337e \1096702\ACK\ETXPB\DELC\1111118fa\178975" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_13 :: BindingNewTeam -testObject_BindingNewTeam_team_13 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "G\DEL\51831\70681rLb<\1056047!\RS|RD\161793\ACK\82958\164863\45602Ag\22680 \vy`\v\1045283K\13763e\18467,\144933DQEO\RS|\SI\1076051\1063435gr\1113276\NUL\n*1\47081R\SO\66829-Y\1037937n\1085668]])\1086075C\DC3\146455\"M@(K\15234\RS1\35575\FS\SUB\1025798T?}\SO=*\184770\n\69897\v_\"7\1064561?Lk\150200x\DC4bu:\146992\14577\1036009<\1015572\&6\SO`\1071314U\51409yp\183322\&7%", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "o\64661\1052808\SI[aoM\GS\1110611}q\36535\&4^\ETB-*%\148361\&8\1067531`\1070936#pH}\DC3?w`A/\94009\1108569\995072 \1104313\nX\40987\997490\DC3u\RS\SOH(\1041586\1006481\&6\STX]t{\DC4\";*\r\12492q\1066003\12213\63338+w&\31533(3#\180761PY]\RSf\\?F4\SUB\UST\1108579Rnfq%\66873p\154120\182326j\127981\&0P\bn\SO\FS\t\19400\nN.aGx" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_14 :: BindingNewTeam -testObject_BindingNewTeam_team_14 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "2#\DC2N\b9&A\1030886ZL{f\1011542M\1101172\23517\a\DELv\164961\32470\ACKT7\DC3\DC4\1009557O\1103393C\152202\t\DC4l\RS\SOH]\ESC\ACK\95718X;\149660* &\97401}\1111236T\ESCCLkx,\DLE\63803\nbT\1049269fWJ\992800\136973a\US`\DC3\139728\28948\&8r2']\NAK\DC2\133094\nl\DC2NXB\ENQia\1068046]B\989632\DLE\ENQdf#\64677\t6g\FS\SOH\1029760Fp(\GSQTZ\1015396\8630\153801dUJt\SI\EM\194705`\\#g0Qed@a${=Q.\1048388Ld`\35027 \173216sV\SUB\SO5\150360\41997\1107813i\EM\DC3\988956\1049486\SOH\1030355>\1044179\DC3w\1001979Y}\21603\&1q\NAKY:\25626q \ETB=*#\74975\EM\61277\\\21887y9Tfc\DC1\49327k\1096646\\Oxxn&6NtaZ?k:5G@\46350\DC3H\1097149hu4\178807\995883\USR\161801\1024517v\26381\23905\72161\12881\ACKD\985152[bb<\1111873", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = Nothing, - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_15 :: BindingNewTeam -testObject_BindingNewTeam_team_15 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - ":\44335R_.\4189\v;\t\1039296-\5484PN\r[\32934\SUBY\1102645<\60542\1083602\aW\1099269@\183771\162143\172579\biU\1005268b\DLE=\t8+\993285\1090143\1018670\1107684>\ACK1\bZQ7fmQOQ\986711l!\DC3\44018\27476*\43689*1\f\1097293\&8nk|\NAK\1005998~\fO\162989\100863!:3\ETXn{%\6663\182700if/!\29917] <\1056176Y\1078680\b\DC4~\t\EM\SOH<*\NAK\143397bx4 {\96203\CANVs;g\98929\144388\STXqkI!QJ\1072302J\189512\DC4\64545?_\STX\t\1082190iB3YdKA7@>Q\995699\987049]\1094644\133325>D\1026819wD\ESC|\SI'^\136789\120874Q#q,\"", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = - Just - ( unsafeRange - "\SOH]rj\1053405eA\1046358\tbj\EMk\DC1l\n\988481H~]u\42907\1029099!kjVS{42\NULE?\EMh\61474\35112B!:\DLEX\DC1T\DEL3W\avimhK\1078443\DC1to*P*\DC1}\986362\1081249H\r\1034017B", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = Nothing, - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_17 :: BindingNewTeam -testObject_BindingNewTeam_team_17 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "|\36324P\US\1040589\159812Y\SOHj\RSYrr\49743\&0m\ENQ\1027954*'\72098\1105368P6\SYN\15236\f\DC2\125109e\1031690\RS\1026891\1003083\69946\rA'\GSA\NAK\53778\1067566J\1016490'T\1037603R2? \FS\US\1032454$\NAKGr(\1008673{\ENQ\62451\&0mJ\SID\STX-\CAN_I\132366\f\147665\FSR\1080205hp\143954B6W2\b\f6\1104867\DC2\180998\b1'7-T-#\3953D\1076345\1082129T]v$Gl\1042148\1032818\&5yg\1025280\nQc.`i\14819\24538}\FS&k4\99627\ACK>#\32013\1036954\EM\131987[vBOPu\1108963@\ACK\NUL\1087882\147841\SO\NAK\98755\31702\EOT\ETX&\1032348?z\989374i\fz\n\1029119\ETB3\a\1108955W\1113557E^\1043345\986117S3'4\ACK\74144*m-\ESC4\USj\ETX__6\1046371\6580M\48069\ESC]\EOTDq\DLEuo\28030$\vUWp1=/o\ETBY\173686\&9\DC2\nQ\177317\1051037)\1102455\1010761\NAKaR\145135;\52151\SOH\EM\na\nvt\133143\ETXa\140630 J\134658uX\1077113?Wz&<\DC4C\fx`\1038161#\SI\194737\37045\43620\RS\STX#\SYN\DC4-Oj\EOTd\1037772'FoHqexoh\SUBx\1106683\184912\bi\998453yr\SI\1064751w\1104226\n8T\1008339\&2'\1024124\1110758\1103037\RSnxW[\26817\993050\96723\153423i\13589\&4\1008403YHZ\48771VZ\DLE^0\STXC\1057595\1037144" - ), - _newTeamMembers = Nothing - } - ) - -testObject_BindingNewTeam_team_20 :: BindingNewTeam -testObject_BindingNewTeam_team_20 = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\SOHW+\a#\151172iN6\GS/#mrj4'\rTV]\ETXg>\"br\SOH\NUL\158808+\47718c^\1003405<`\1111751\149060\STX\986585\ETX\162139D\ENQ\30356nqp\1095539\988368c\RSt\1081319G", - _newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), - _newTeamIconKey = Nothing, - _newTeamMembers = Nothing - } - ) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeam_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeam_team.hs new file mode 100644 index 00000000000..a5cb02772ba --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewTeam_team.hs @@ -0,0 +1,283 @@ +-- 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.Generated.NewTeam_team where + +import Data.Id (Id (Id)) +import Data.Range (unsafeRange) +import Data.UUID qualified as UUID (fromString) +import Imports (Maybe (Just, Nothing), fromJust) +import Wire.API.Asset (AssetKey (..), AssetRetention (..)) +import Wire.API.Team + +testObject_NewTeam_team_1 :: NewTeam +testObject_NewTeam_team_1 = + NewTeam + { newTeamName = + unsafeRange + "UivH&\54922\98185p\USz\11724\r$\DC4j9P\r\"\1070851\3254\986624aF>E\1078807\139041B\EM&\1088459\DC4\174923+'\1103890R;!\GS\1017122\SIvv|\rmbGHz\1005234\95057\&3h\120904\\U|'\ETX;^&G\CAN\f\41076\&42\teq\1049559\SOV1}\RSaT\1014212aO7<;o\179606\f\1111896m)$PC\ESC7;f{\STXt\9533>\EOTX@4|/\tH\ENQ/D\144082\EM\121436C\99696Q\ENQT\1096609?d\ACK\1073806#H\127523\139127*\166004jo4wa\95243leQ*\1000542\1034344>@,\1045947\190894RF4QcNY96\168531\1051528G\1069460&J\\TzHUiG.C\SUB&\FSx\52616\167921\&3\1105098A\1054008B)\29142\31346r\1004296\ENQ&VCPa{\SOH\EMW\DEL\43500\97305\DLE/\1078579\SIc:b\SOH\132266)\35144\1100498\37490@5\983688I02g%%1bJl} :\1021555\SYN\64090\158870\143049" + ) + } + +testObject_NewTeam_team_3 :: NewTeam +testObject_NewTeam_team_3 = + NewTeam + { newTeamName = + unsafeRange + "\SUB_F\n\65091\140672\DC2>\1079041\74636t\n)1/% hL\DC2Ad\SOHXq6\DC1)\NUL\f6\fV\DC4r\1097128\DC1n\1107359,@\171217\118996\n\SUB%N\176824\ACK\33856Xv)\SYNz?\DC4\EMY\162050\&2\95792um8}\51420\DC2yW\NULHQ\ENQD[Fe\nk\999106\EM\25079Yk@##u}j\169850\153342\STXq\ESCir7) \27756%\1016104~\993971\&8\1085984je\1099724\&0*Gi3\120829je\CANQr>\1033571k1\63774c\1031586L\1015084\93833t\EOTW\999363\SUBo\fgh\ACK\172057C2\38697c\SUB)uW\r\fB\1042942Sf\SUB\SOH*5l\38586\SI\25991\EMB(\ENQ\133758/)!{\1006380\&9\STXA\DEL\16077fx&\180089T&\187029\DC4\52222[\r\v\n\1071241j2\166180/\1086576\ENQQo\fj\134496\129296\nb6\CAN3\RS9\EM\1000086ub\ETB3CY\GSsIz", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "3d4b563b-016c-49da-bad2-876ad0a5ecd2"))) AssetExpiring), + newTeamIconKey = + Just + ( unsafeRange + "\FS\RSP\988567Gt\SYN-\47148nJ\1010840g^\n\r\177791\GSR\1010061q\988754\nQ\RS\1054014\GS#w\147936\171735\1064959\136621B\DC4\SUBLv\"S>\121093!]sB+6\DC1oc\ETB7\34513lR\95866\EMr%E\1077999B\98708A\1067109N\ETB?{\1065508/|cU\60733\141259]\92896\1102284\DLE\147332\1075446+\991438\t$F\96714he4\166964|k/!5Z~\83246\ETB\1017589\SOH\ENQ\1056989\&3E!{^\33558\&4fh\1029576N\1111705v\f\GS\998029mde!5\1027807y&\1062155xo,\STXrk\1071672\ENQ\SOHJoS\986695X\18929\994879a\991047\RS\1046020\EM\SOH3j\3901Z4\DC4\1068579l\52972n\ESC@ve#\SYN\GS\183587P4\1077298\ESC\170211:\157706z1*\USs\vd`\1059621/\39172\165682" + ) + } + +testObject_NewTeam_team_6 :: NewTeam +testObject_NewTeam_team_6 = + NewTeam + { newTeamName = + unsafeRange + "v\188076hEWefuu\1006804jPx\158137k#\SOH\986725\STX\ETX^\ESC\n\CAN\8325p1D|S1\1064991\1102106\29079\SYN`\t0g\1034469,t\FSw\fDT\RS#H\SOH\145176\US{\1091499\1025650\984364lW\a,uil\SIN`5e:\SYN Y!\SYN\1025115tb\1085213", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "d7a467c6-8cd4-40cb-9e30-99b64bb11307"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "+&heN\1091941K\f_k\DLE(\33970\DC3\9833M\f\1029853\1098178\SI^s\1101855Ga,$\38078\SIb\DC3\f\"s{\ACK5\1025293\5649\US\DLE\SUB\1085641\70123\CAN,\1036517\158007\DC4 \1109215P\95245|f.>hEa\DLE^\ENQ\b]`\1112948<\GSZG\1004098\SOH\190360\24273*8p\FSF@OLpnXTmW\96553f\68110\1076109\25954Ze1 \SYNEm\27765f\ACK\987143" + ) + } + +testObject_NewTeam_team_7 :: NewTeam +testObject_NewTeam_team_7 = + NewTeam + { newTeamName = + unsafeRange + "\145552\1042892iz\1057971FT\14964;\1108369}\188917\1113471\&9\SO\991633\&7>hAC\NULH2O\177259m\187711\&2R(?W,=,\990725M\992456\aM\194790\SUB\47600q\SOlj\EOTj^.s~\rY%5lM,\26492=\ACK\1016899\188843>{\CAN\DLE\15878f=X9\SYN9\51145\159419TI4\17599\v\NAK6\1014936/\DLE\NAK\ACK\23564H<\ENQ\1029703e\ENQz\1017528:\6137\"rS\a\167660\FS\ETX\1059289\1031786\49012\DC4\DC4Q\"\1065200\&1:\1097556\UST.;\1042663\18380}", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "b199431c-e2ee-48c6-8f1b-56726626b493"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "D\RS\168552\SOH\1033444\128689Ll\GS\tW\1056953o\CAN\47716b\ETX|\US*=\1011088\1066392\988391\&6\999812" + ) + } + +testObject_NewTeam_team_8 :: NewTeam +testObject_NewTeam_team_8 = + NewTeam + { newTeamName = + unsafeRange + "YwD\1023517r\NAK}\1083947\ACK\1047823\29742\EOT\1071030iI5g\1012255\t\"r\150087O\DC4?\53005\1100290\1108960\NUL\1060304qgg\DC1X)\NULL\1054528\CAN{\v4\NUL\93999\bvD#\1035811$aYFk\b\1102040\1089491\1042733\47133:1\179810S7\66745V)\1072087\v\96989\&3#\b\1104899c\27119Q/jPy\1015620P@Df\997914\51756H\1113361Xr\SO\ETB3%\1108760aF@3A\SI\ETB\STX mj9T=\DC3'XI\DC2?0\1093231\156858VHp?\1066163YU\42092\33083\72810,)\1113424\ETX96\153338z\42445/4T\136162\ESC\60427\1086321&\ETBS\1098748\14578z[\54638Z\DC2\"e\SUB\173931&rQ\fJG\100066\180037\155435s$\SUB$\50544S\162554E\ETX*\t+\63443WU*\144654\1042128\&8\NAK\999184a\t\EM\1097907_\DELOD\1006385/\23998\1100140SmfX", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "v\70188\46459h\SOH_\991979\DC3\ACKi\1000164\DC1\ETXW\72785\35679\DC2\23266\1026390\EOT\f%_\1064553\GS\SYN\ETB N\NULF\1005467\ENQLUua3\1089232M\8605\"\94879\SOH\RS\n-='\DC1B#\FS\136881>\DC3\132340\SI\GS\1088106G7v6w Z\4678\1051054\182628\170805\ESCP>\131111\1051383\1076729\v}?\5316Jg\SOH\SUB^pl\1101671\&2.\SOV\57380\DC3\22371\64509\ENQB\1045499\1076733\139492<\f\DEL2\19252Tz@6\DC3\71851x?\150161\36913\b\DLE\CANp\1081584\SYN\ETXN\1099776C\SI\SUB\DC1l]R\NULvL\1027446Nz\f-bf}f>\STXH\EM\136484+Zo\1034706\1062880\NAK}\adb\171356-\\-1\DC42\1046344\DC2\78894\&1/\33084b:\ENQ\1038950;Mw\FS\183866\1113547ITuy\1050264`SP\SOH\SO\GS\NAK\a\r7M\1069326\1064150\18615\n\SYN3V\ETXR\n1$e.\1096261B~yd_z\1047817\rV\1091351\RS\SYN\165050l\DC3\47200u\1058674u\"\aTc|sEw\1011190wTC|F\4735B\t\DC4&\bUEN(+M\SOF;\1099746\134573\EM20\nrPW\1017058$\1064809", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "X\1019453;\ENQW\ACKLk\996110\144662\ETB\n]\58553[~\10280&U\20125v`I\ETB\USl\983659\t\1090302?\17227KM3c\1067581\1030643= \ETBt5vKOg\NAK/NC2~i'\1062772Ojb\b\ETX\62742\1090035\DC1\SOH\NULFWc\1014613sU>P\SOH~\EMwUHU\SO#\55006\1081711!Nwn\1005601e\SOH\SUB\f\ETX\ETBT\DELl\110629BYU;a\1012448K7?,m\154276Xpa\48825\138301\EM ,M!~^g6}(\60133\36369\RS\8075gX}\161019)c\n\SOH2E" + ) + } + +testObject_NewTeam_team_10 :: NewTeam +testObject_NewTeam_team_10 = + NewTeam + { newTeamName = unsafeRange "\b \SOH+\1056054;\t095\42390\n\STX2J\1002251\DC1UzD_\1110746\FS", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "\EOT\131569\ETB:\984737HL\SOH^bs\vG\157476{I\1096053]-J\FS\1107927\vs9\DLE\1000765vI`N\48159MZz" + ) + } + +testObject_NewTeam_team_11 :: NewTeam +testObject_NewTeam_team_11 = + NewTeam + { newTeamName = + unsafeRange + "\48005H\1082536\132304\157763\&5\RS\986337-\NAK\ESCR\nL\63954&bD\139428\SUBH\US\1040918\f\t;e\1064224\47101\tc\1087740e\1099415\DLE\ETX\DELI\65746\ETB\133884\SUB \SI\43795~FE\CAN6\162836\DEL\46062u\"\135684\1041611\FSFYI\t/{\ENQ\RS]j\1076782\US22\15884l\42366$\ETB\US\180023kL{\STX*\131382RMj\ESC\1091332W3H\1020399\FS\NAK^\"5\29653\32539*\1099111", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "\1109507I\ACK.\158786@y0\DLE\1083101n\\#skj\1019405Y_\1037580&x\1007219\GS\SIy\1104457B\SYN0\DC3VP1\1086698q\1024822\1081753\28211R\1100307*+\RS,MP\27076*;\n\NAK\47211\t\160463\nGj.\41290\1104539l\12622\FS\61112~\1076042\NUL.\1083842&\SOH}\SI\1080986\DC1+f^ZC\a'T\SOH\n\1020923\1097319U\1107987`W\r\\fX\n\1095366TF\1108756`h\97424[\46315ERdP5<<\1024109;\r\1095899\NULDy\28422\&5N/^\136134(\DC3\1045067\1061604\&6e\f:\SIB\DLEF-\1110200\17393\1064949Rfb\44582\aDrB\987948\13740\26738\NUL+\60859\&2.\a\a}\NAKpsFw\ETB\DC3 \186007\151693k~" + ) + } + +testObject_NewTeam_team_12 :: NewTeam +testObject_NewTeam_team_12 = + NewTeam + { newTeamName = + unsafeRange + ";\110872M\EOT\164161P]'\1041089\1094514\4118\1054714iFnRQV\43238@\992926\59902l\1099067\aKZ{\51124S\190890\fg*\n,`!V\STX\991695e'\1039967\SO0\37019p4d\STXs\1020471uK(c'\52929hjB\144953\SOt'h^\SYN\SYN0\1009487_\12064\166805thH\SI\1073479:\1019934l; n4c\1101781D[\1014388\&8Y+\1092407\EOTE\1058506\\0\168273KKTc)P1K\1042475\990753W\ETX<|\24888\&0|5{Y\986771M\DC4\vK\DLE\1089150\SOH\DC4\1013653.\ETBg\991717\DLE\"W\NUL9&0yYZ\1094524\v\11606\58174", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "\"C\ESC\SI0\ETB\69608p\12616|/O]\53852\SO \55172C\SYNN\SUB8\NUL\62584BxtH\SO*\1077819\&3.\1061851(\1100810w\GS\152525R{q\990825\&4\180037\150457:\187092\134288>\ETB\nl\1061158g\"\996841,6K\28384\1054272[\1019005\1016209N\24221eB!\188918C\EOT\STXX#El\ETB`\61337e \1096702\ACK\ETXPB\DELC\1111118fa\178975" + ) + } + +testObject_NewTeam_team_13 :: NewTeam +testObject_NewTeam_team_13 = + NewTeam + { newTeamName = + unsafeRange + "G\DEL\51831\70681rLb<\1056047!\RS|RD\161793\ACK\82958\164863\45602Ag\22680 \vy`\v\1045283K\13763e\18467,\144933DQEO\RS|\SI\1076051\1063435gr\1113276\NUL\n*1\47081R\SO\66829-Y\1037937n\1085668]])\1086075C\DC3\146455\"M@(K\15234\RS1\35575\FS\SUB\1025798T?}\SO=*\184770\n\69897\v_\"7\1064561?Lk\150200x\DC4bu:\146992\14577\1036009<\1015572\&6\SO`\1071314U\51409yp\183322\&7%", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "o\64661\1052808\SI[aoM\GS\1110611}q\36535\&4^\ETB-*%\148361\&8\1067531`\1070936#pH}\DC3?w`A/\94009\1108569\995072 \1104313\nX\40987\997490\DC3u\RS\SOH(\1041586\1006481\&6\STX]t{\DC4\";*\r\12492q\1066003\12213\63338+w&\31533(3#\180761PY]\RSf\\?F4\SUB\UST\1108579Rnfq%\66873p\154120\182326j\127981\&0P\bn\SO\FS\t\19400\nN.aGx" + ) + } + +testObject_NewTeam_team_14 :: NewTeam +testObject_NewTeam_team_14 = + NewTeam + { newTeamName = + unsafeRange + "2#\DC2N\b9&A\1030886ZL{f\1011542M\1101172\23517\a\DELv\164961\32470\ACKT7\DC3\DC4\1009557O\1103393C\152202\t\DC4l\RS\SOH]\ESC\ACK\95718X;\149660* &\97401}\1111236T\ESCCLkx,\DLE\63803\nbT\1049269fWJ\992800\136973a\US`\DC3\139728\28948\&8r2']\NAK\DC2\133094\nl\DC2NXB\ENQia\1068046]B\989632\DLE\ENQdf#\64677\t6g\FS\SOH\1029760Fp(\GSQTZ\1015396\8630\153801dUJt\SI\EM\194705`\\#g0Qed@a${=Q.\1048388Ld`\35027 \173216sV\SUB\SO5\150360\41997\1107813i\EM\DC3\988956\1049486\SOH\1030355>\1044179\DC3w\1001979Y}\21603\&1q\NAKY:\25626q \ETB=*#\74975\EM\61277\\\21887y9Tfc\DC1\49327k\1096646\\Oxxn&6NtaZ?k:5G@\46350\DC3H\1097149hu4\178807\995883\USR\161801\1024517v\26381\23905\72161\12881\ACKD\985152[bb<\1111873", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = Nothing + } + +testObject_NewTeam_team_15 :: NewTeam +testObject_NewTeam_team_15 = + NewTeam + { newTeamName = + unsafeRange + ":\44335R_.\4189\v;\t\1039296-\5484PN\r[\32934\SUBY\1102645<\60542\1083602\aW\1099269@\183771\162143\172579\biU\1005268b\DLE=\t8+\993285\1090143\1018670\1107684>\ACK1\bZQ7fmQOQ\986711l!\DC3\44018\27476*\43689*1\f\1097293\&8nk|\NAK\1005998~\fO\162989\100863!:3\ETXn{%\6663\182700if/!\29917] <\1056176Y\1078680\b\DC4~\t\EM\SOH<*\NAK\143397bx4 {\96203\CANVs;g\98929\144388\STXqkI!QJ\1072302J\189512\DC4\64545?_\STX\t\1082190iB3YdKA7@>Q\995699\987049]\1094644\133325>D\1026819wD\ESC|\SI'^\136789\120874Q#q,\"", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = + Just + ( unsafeRange + "\SOH]rj\1053405eA\1046358\tbj\EMk\DC1l\n\988481H~]u\42907\1029099!kjVS{42\NULE?\EMh\61474\35112B!:\DLEX\DC1T\DEL3W\avimhK\1078443\DC1to*P*\DC1}\986362\1081249H\r\1034017B", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = Nothing + } + +testObject_NewTeam_team_17 :: NewTeam +testObject_NewTeam_team_17 = + NewTeam + { newTeamName = + unsafeRange + "|\36324P\US\1040589\159812Y\SOHj\RSYrr\49743\&0m\ENQ\1027954*'\72098\1105368P6\SYN\15236\f\DC2\125109e\1031690\RS\1026891\1003083\69946\rA'\GSA\NAK\53778\1067566J\1016490'T\1037603R2? \FS\US\1032454$\NAKGr(\1008673{\ENQ\62451\&0mJ\SID\STX-\CAN_I\132366\f\147665\FSR\1080205hp\143954B6W2\b\f6\1104867\DC2\180998\b1'7-T-#\3953D\1076345\1082129T]v$Gl\1042148\1032818\&5yg\1025280\nQc.`i\14819\24538}\FS&k4\99627\ACK>#\32013\1036954\EM\131987[vBOPu\1108963@\ACK\NUL\1087882\147841\SO\NAK\98755\31702\EOT\ETX&\1032348?z\989374i\fz\n\1029119\ETB3\a\1108955W\1113557E^\1043345\986117S3'4\ACK\74144*m-\ESC4\USj\ETX__6\1046371\6580M\48069\ESC]\EOTDq\DLEuo\28030$\vUWp1=/o\ETBY\173686\&9\DC2\nQ\177317\1051037)\1102455\1010761\NAKaR\145135;\52151\SOH\EM\na\nvt\133143\ETXa\140630 J\134658uX\1077113?Wz&<\DC4C\fx`\1038161#\SI\194737\37045\43620\RS\STX#\SYN\DC4-Oj\EOTd\1037772'FoHqexoh\SUBx\1106683\184912\bi\998453yr\SI\1064751w\1104226\n8T\1008339\&2'\1024124\1110758\1103037\RSnxW[\26817\993050\96723\153423i\13589\&4\1008403YHZ\48771VZ\DLE^0\STXC\1057595\1037144" + ) + } + +testObject_NewTeam_team_20 :: NewTeam +testObject_NewTeam_team_20 = + NewTeam + { newTeamName = + unsafeRange + "\SOHW+\a#\151172iN6\GS/#mrj4'\rTV]\ETXg>\"br\SOH\NUL\158808+\47718c^\1003405<`\1111751\149060\STX\986585\ETX\162139D\ENQ\30356nqp\1095539\988368c\RSt\1081319G", + newTeamIcon = Icon (AssetKeyV3 (Id (fromJust (UUID.fromString "55b9ad19-315c-4bda-8c0f-5d7b0e143008"))) AssetEternal), + newTeamIconKey = Nothing + } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs index 117280d3753..973d0055265 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewUser_user.hs @@ -37,7 +37,7 @@ import Data.Text.Ascii (AsciiChars (validate)) import Data.UUID qualified as UUID (fromString) import Imports (Maybe (Just, Nothing), fromJust, fromRight, undefined, (.)) import Wire.API.Asset -import Wire.API.Team (BindingNewTeam (..), Icon (..), NewTeam (..)) +import Wire.API.Team import Wire.API.User import Wire.API.User.Activation (ActivationCode (ActivationCode, fromActivationCode)) import Wire.API.User.Auth (CookieLabel (CookieLabel, cookieLabelText)) @@ -137,20 +137,17 @@ testObject_NewUser_user_7 = user = BindingNewTeamUser { bnuTeam = - BindingNewTeam - ( NewTeam - { _newTeamName = - unsafeRange - "\fe\ENQ\1011760zm", - _newTeamIcon = DefaultIcon, - _newTeamIconKey = - Just - ( unsafeRange - "\ACKc\151665L ," - ), - _newTeamMembers = Nothing - } - ), + NewTeam + { newTeamName = + unsafeRange + "\fe\ENQ\1011760zm", + newTeamIcon = DefaultIcon, + newTeamIconKey = + Just + ( unsafeRange + "\ACKc\151665L ," + ) + }, bnuCurrency = Just XUA } diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_1.json b/libs/wire-api/test/golden/testObject_NewTeam_team_1.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_1.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_1.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_10.json b/libs/wire-api/test/golden/testObject_NewTeam_team_10.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_10.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_10.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_11.json b/libs/wire-api/test/golden/testObject_NewTeam_team_11.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_11.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_11.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_12.json b/libs/wire-api/test/golden/testObject_NewTeam_team_12.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_12.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_12.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_13.json b/libs/wire-api/test/golden/testObject_NewTeam_team_13.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_13.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_13.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_14.json b/libs/wire-api/test/golden/testObject_NewTeam_team_14.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_14.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_14.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_15.json b/libs/wire-api/test/golden/testObject_NewTeam_team_15.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_15.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_15.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_16.json b/libs/wire-api/test/golden/testObject_NewTeam_team_16.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_16.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_16.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_17.json b/libs/wire-api/test/golden/testObject_NewTeam_team_17.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_17.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_17.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_18.json b/libs/wire-api/test/golden/testObject_NewTeam_team_18.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_18.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_18.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_19.json b/libs/wire-api/test/golden/testObject_NewTeam_team_19.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_19.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_19.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_2.json b/libs/wire-api/test/golden/testObject_NewTeam_team_2.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_2.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_2.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_20.json b/libs/wire-api/test/golden/testObject_NewTeam_team_20.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_20.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_20.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_3.json b/libs/wire-api/test/golden/testObject_NewTeam_team_3.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_3.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_3.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_4.json b/libs/wire-api/test/golden/testObject_NewTeam_team_4.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_4.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_4.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_5.json b/libs/wire-api/test/golden/testObject_NewTeam_team_5.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_5.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_5.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_6.json b/libs/wire-api/test/golden/testObject_NewTeam_team_6.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_6.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_6.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_7.json b/libs/wire-api/test/golden/testObject_NewTeam_team_7.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_7.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_7.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_8.json b/libs/wire-api/test/golden/testObject_NewTeam_team_8.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_8.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_8.json diff --git a/libs/wire-api/test/golden/testObject_BindingNewTeam_team_9.json b/libs/wire-api/test/golden/testObject_NewTeam_team_9.json similarity index 100% rename from libs/wire-api/test/golden/testObject_BindingNewTeam_team_9.json rename to libs/wire-api/test/golden/testObject_NewTeam_team_9.json 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 c9c780681c1..83d59a00b29 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 @@ -206,7 +206,7 @@ tests = testRoundTrip @SystemSettings.SystemSettings, testRoundTrip @SystemSettings.SystemSettingsPublic, testRoundTrip @SystemSettings.SystemSettingsInternal, - testRoundTrip @Team.BindingNewTeam, + testRoundTrip @Team.NewTeam, testRoundTrip @Team.TeamBinding, testRoundTrip @Team.Team, testRoundTrip @Team.TeamList, diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 9d2026da817..1b768dd1fc8 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -373,7 +373,6 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.AssetSettings_user Test.Wire.API.Golden.Generated.AssetSize_user Test.Wire.API.Golden.Generated.AssetToken_user - Test.Wire.API.Golden.Generated.BindingNewTeam_team Test.Wire.API.Golden.Generated.BindingNewTeamUser_user Test.Wire.API.Golden.Generated.BotConvView_provider Test.Wire.API.Golden.Generated.BotUserView_provider @@ -461,6 +460,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.NewProviderResponse_provider Test.Wire.API.Golden.Generated.NewService_provider Test.Wire.API.Golden.Generated.NewServiceResponse_provider + Test.Wire.API.Golden.Generated.NewTeam_team Test.Wire.API.Golden.Generated.NewTeamMember_team Test.Wire.API.Golden.Generated.NewUser_user Test.Wire.API.Golden.Generated.NewUserPublic_user diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs index 07003fed93c..859fe913628 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess.hs @@ -73,7 +73,7 @@ data GalleyAPIAccess m a where GalleyAPIAccess m Bool CreateTeam :: UserId -> - BindingNewTeam -> + NewTeam -> TeamId -> GalleyAPIAccess m () GetTeamMember :: diff --git a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs index 340018628f7..f7ee1c47f65 100644 --- a/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/GalleyAPIAccess/Rpc.hs @@ -284,7 +284,7 @@ createTeam :: Member TinyLog r ) => UserId -> - BindingNewTeam -> + NewTeam -> TeamId -> Sem r () createTeam u t teamid = do diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 8d4d35d653f..fe994a98952 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -58,7 +58,7 @@ import Brig.User.Auth.Cookie qualified as Auth import Cassandra qualified as C import Cassandra qualified as Data import Control.Error hiding (bool, note) -import Control.Lens (view, (.~), (?~), (^.)) +import Control.Lens (view, (.~), (?~)) import Control.Monad.Catch (throwM) import Control.Monad.Except import Data.Aeson hiding (json) @@ -787,8 +787,8 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do sendActivationEmail email name (key, code) locale mTeamUser | Just teamUser <- mTeamUser, Public.NewTeamCreator creator <- teamUser, - let Public.BindingNewTeamUser (Public.BindingNewTeam team) _ = creator = - liftSem $ sendTeamActivationMail email name key code locale (fromRange $ team ^. Public.newTeamName) + let Public.BindingNewTeamUser team _ = creator = + liftSem $ sendTeamActivationMail email name key code locale (fromRange $ team.newTeamName) | otherwise = liftSem $ sendActivationMail email name key code locale diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 6e516ccca60..982c907e338 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -289,8 +289,8 @@ upgradePersonalToTeam luid bNewTeam = do let uid = tUnqualified luid createUserTeam <- do liftSem $ GalleyAPIAccess.createTeam uid (bnuTeam bNewTeam) tid - let BindingNewTeam newTeam = bNewTeam.bnuTeam - pure $ CreateUserTeam tid (fromRange (newTeam ^. newTeamName)) + let newTeam = bNewTeam.bnuTeam + pure $ CreateUserTeam tid (fromRange newTeam.newTeamName) wrapClient $ updateUserTeam uid tid liftSem $ Intra.sendUserEvent uid Nothing (teamUpdated uid tid) @@ -302,7 +302,7 @@ upgradePersonalToTeam luid bNewTeam = do sendUpgradePersonalToTeamConfirmationEmail email user.userDisplayName - bNewTeam.bnuTeam.bntTeam._newTeamName.fromRange + bNewTeam.bnuTeam.newTeamName.fromRange user.userLocale pure $! createUserTeam @@ -394,10 +394,10 @@ createUser new = do (Just tid', Just newTeamUser) -> do liftSem $ GalleyAPIAccess.createTeam uid (bnuTeam newTeamUser) tid' let activating = isJust (newUserEmailCode new) - BindingNewTeam newTeam = newTeamUser.bnuTeam + newTeam = newTeamUser.bnuTeam pure $ if activating - then Just $ CreateUserTeam tid' (fromRange (newTeam ^. newTeamName)) + then Just $ CreateUserTeam tid' (fromRange newTeam.newTeamName) else Nothing _ -> pure Nothing diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index daac2f2e6eb..5cc29c6f86e 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -668,7 +668,7 @@ testInvitationMutuallyExclusive brig = do req :: EmailAddress -> Maybe InvitationCode -> - Maybe BindingNewTeam -> + Maybe NewTeam -> Maybe InvitationCode -> HttpT IO (Response (Maybe LByteString)) req e c t i = diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index defa0f8e5b3..12258c5dbd5 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -257,8 +257,8 @@ deleteTeam g tid u = do !!! const 202 === statusCode -newTeam :: BindingNewTeam -newTeam = BindingNewTeam $ newNewTeam (unsafeRange "teamName") DefaultIcon +newTeam :: NewTeam +newTeam = newNewTeam (unsafeRange "teamName") DefaultIcon putLegalHoldEnabled :: (HasCallStack) => TeamId -> FeatureStatus -> Galley -> Http () putLegalHoldEnabled tid enabled g = do @@ -302,7 +302,7 @@ extAccept email name phone phoneCode code = "team_code" .= code ] -register :: EmailAddress -> BindingNewTeam -> Brig -> Http (Response (Maybe LByteString)) +register :: EmailAddress -> NewTeam -> Brig -> Http (Response (Maybe LByteString)) register e t brig = post ( brig @@ -319,7 +319,7 @@ register e t brig = ) ) -register' :: EmailAddress -> BindingNewTeam -> ActivationCode -> Brig -> Http (Response (Maybe LByteString)) +register' :: EmailAddress -> NewTeam -> ActivationCode -> Brig -> Http (Response (Maybe LByteString)) register' e t c brig = post ( brig diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs index 00e2e3e8de8..da5c80d12cb 100644 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ b/services/brig/test/integration/API/UserPendingActivation.hs @@ -147,8 +147,8 @@ getInvitationByEmail brig email = Brig -> Galley -> m (UserId, TeamId) createUserWithTeamDisableSSO brg gly = do diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index df3015be471..f6105cc46f1 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -228,41 +228,14 @@ lookupTeam zusr tid = do else pure Nothing createNonBindingTeamH :: - forall r. - ( Member BrigAccess r, - Member (ErrorS 'UserBindingExists) r, - Member (ErrorS 'NotConnected) r, - Member NotificationSubsystem r, - Member (Input UTCTime) r, - Member P.TinyLog r, - Member TeamStore r - ) => + (Member (ErrorS InvalidAction) r) => UserId -> ConnId -> - Public.NonBindingNewTeam -> + a -> Sem r TeamId -createNonBindingTeamH zusr zcon (Public.NonBindingNewTeam body) = do - let owner = Public.mkTeamMember zusr fullPermissions Nothing LH.defUserLegalHoldStatus - let others = - filter ((zusr /=) . view userId) - . maybe [] fromRange - $ body ^. newTeamMembers - let zothers = map (view userId) others - ensureUnboundUsers (zusr : zothers) - ensureConnectedToLocals zusr zothers - P.debug $ - Log.field "targets" (toByteString . show $ toByteString <$> zothers) - . Log.field "action" (Log.val "Teams.createNonBindingTeam") - team <- - E.createTeam - Nothing - zusr - (body ^. newTeamName) - (body ^. newTeamIcon) - (body ^. newTeamIconKey) - NonBinding - finishCreateTeam team owner others (Just zcon) - pure (team ^. teamId) +createNonBindingTeamH _ _ _ = do + -- non-binding teams are not supported anymore + throwS @InvalidAction createBindingTeam :: ( Member NotificationSubsystem r, @@ -271,12 +244,12 @@ createBindingTeam :: ) => TeamId -> UserId -> - BindingNewTeam -> + NewTeam -> Sem r TeamId -createBindingTeam tid zusr (BindingNewTeam body) = do +createBindingTeam tid zusr body = do let owner = Public.mkTeamMember zusr fullPermissions Nothing LH.defUserLegalHoldStatus team <- - E.createTeam (Just tid) zusr (body ^. newTeamName) (body ^. newTeamIcon) (body ^. newTeamIconKey) Binding + E.createTeam (Just tid) zusr body.newTeamName body.newTeamIcon body.newTeamIconKey Binding finishCreateTeam team owner [] Nothing pure tid diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 950638f5bab..026a3475fc3 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -275,7 +275,7 @@ createBindingTeamInternalNoActivate :: (HasCallStack) => Text -> UserId -> TestM createBindingTeamInternalNoActivate name owner = do g <- viewGalley tid <- randomId - let nt = BindingNewTeam $ newNewTeam (unsafeRange name) DefaultIcon + let nt = newNewTeam (unsafeRange name) DefaultIcon _ <- put (g . paths ["/i/teams", toByteString' tid] . zUser owner . zConn "conn" . zType "access" . json nt) ["password" .= defPassword | hasPassword] <> ["email" .= fromEmail e | hasEmail] - <> ["team" .= BindingNewTeam (newNewTeam (unsafeRange "teamName") DefaultIcon) | isCreator] + <> ["team" .= newNewTeam (unsafeRange "teamName") DefaultIcon | isCreator] responseJsonUnsafe <$> (post (b . path "/i/users" . json p) TestM UserId diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index e041c3d0b1c..474cc09bb8d 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -606,8 +606,8 @@ getSelfProfile brg usr = do zAuthAccess :: UserId -> ByteString -> Request -> Request zAuthAccess u c = header "Z-Type" "access" . zUser u . zConn c -newTeam :: Galley.BindingNewTeam -newTeam = Galley.BindingNewTeam $ Galley.newNewTeam (unsafeRange "teamName") DefaultIcon +newTeam :: Galley.NewTeam +newTeam = Galley.newNewTeam (unsafeRange "teamName") DefaultIcon randomEmail :: (MonadIO m) => m EmailAddress randomEmail = do diff --git a/tools/stern/test/integration/Util.hs b/tools/stern/test/integration/Util.hs index 0e533484b96..d151434f164 100644 --- a/tools/stern/test/integration/Util.hs +++ b/tools/stern/test/integration/Util.hs @@ -96,7 +96,7 @@ randomUserProfile'' isCreator hasPassword hasEmail = do ["name" .= fromEmail e] <> ["password" .= defPassword | hasPassword] <> ["email" .= fromEmail e | hasEmail] - <> ["team" .= BindingNewTeam (newNewTeam (unsafeRange "teamName") DefaultIcon) | isCreator] + <> ["team" .= newNewTeam (unsafeRange "teamName") DefaultIcon | isCreator] (,e) . responseJsonUnsafe <$> (post (b . path "/i/users" . Bilge.json pl) TestM (UserId, EmailAddress) From beefca5e3856d30657e85a89a2c6d3572b31cefb Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Mon, 23 Sep 2024 11:41:17 +0200 Subject: [PATCH 084/136] [WPB-11122] Disallow searching user by old email (#4260) * Clarify openapi3 summary. * Drive-by haddocks fix. * rm trailing whitespace. * Add some Eq, Show instances. --------- Co-authored-by: Leif Battermann --- cassandra-schema.cql | 3 +- ...11122-disallow-searching-user-by-old-email | 1 + integration/test/API/Brig.hs | 13 ++++++ integration/test/API/Spar.hs | 2 +- integration/test/Test/Brig.hs | 42 ++++++++++++++++++- libs/cassandra-util/src/Cassandra/Util.hs | 2 +- .../src/Wire/API/Routes/Internal/Brig.hs | 7 ++-- .../src/Wire/UserSearch/Types.hs | 15 +++++++ .../src/Wire/UserStore/Cassandra.hs | 5 ++- .../src/Wire/UserStore/IndexUser.hs | 24 +++++++---- .../src/Wire/UserSubsystem/Interpreter.hs | 2 +- .../unit/Wire/MockInterpreters/UserStore.hs | 3 +- services/brig/brig.cabal | 1 + services/brig/src/Brig/API/User.hs | 9 +--- services/brig/src/Brig/Data/User.hs | 2 +- services/brig/src/Brig/Schema/Run.hs | 4 +- .../src/Brig/Schema/V86_WriteTimeBumper.hs | 36 ++++++++++++++++ 17 files changed, 141 insertions(+), 30 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-11122-disallow-searching-user-by-old-email create mode 100644 services/brig/src/Brig/Schema/V86_WriteTimeBumper.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index fbc45dc57bb..7d520e33b1f 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -666,7 +666,8 @@ CREATE TABLE brig_test.user ( status int, supported_protocols int, team uuid, - text_status text + text_status text, + write_time_bumper int ) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' 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 new file mode 100644 index 00000000000..85b54569f90 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-11122-disallow-searching-user-by-old-email @@ -0,0 +1 @@ +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/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index a40dddd3bd9..a82046379a3 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -249,6 +249,13 @@ searchContacts user searchTerm domain = do d <- objDomain domain submit "GET" (req & addQueryParams [("q", q), ("domain", d)]) +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_teams__tid__search +searchTeam :: (HasCallStack, MakesValue user) => user -> String -> App Response +searchTeam user q = do + tid <- user %. "team" & asString + req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", tid, "search"] + submit "GET" (req & addQueryParams [("q", q)]) + getAPIVersion :: (HasCallStack, MakesValue domain) => domain -> App Response getAPIVersion domain = do req <- baseRequest domain Brig Unversioned $ "/api-version" @@ -402,6 +409,12 @@ putSelfEmail caller emailAddress = do req <- baseRequest caller Brig Versioned $ joinHttpPath ["users", callerid, "email"] submit "PUT" $ req & addJSONObject ["email" .= emailAddress] +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/delete_self_email +deleteSelfEmail :: (HasCallStack, MakesValue caller) => caller -> App Response +deleteSelfEmail caller = do + req <- baseRequest caller Brig Versioned $ joinHttpPath ["self", "email"] + submit "DELETE" req + -- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/put_self_handle -- FUTUREWORK: rename to putSelfHandle for consistency putHandle :: (HasCallStack, MakesValue user) => user -> String -> App Response diff --git a/integration/test/API/Spar.hs b/integration/test/API/Spar.hs index f040889f6c4..ee57ef581aa 100644 --- a/integration/test/API/Spar.hs +++ b/integration/test/API/Spar.hs @@ -12,7 +12,7 @@ getScimTokens caller = do req <- baseRequest caller Spar Versioned "/scim/auth-tokens" submit "GET" req --- https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_scim_auth_tokens +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_scim_auth_tokens createScimToken :: (HasCallStack, MakesValue caller) => caller -> App Response createScimToken caller = do req <- baseRequest caller Spar Versioned "/scim/auth-tokens" diff --git a/integration/test/Test/Brig.hs b/integration/test/Test/Brig.hs index 4839e36b286..f55fc952b00 100644 --- a/integration/test/Test/Brig.hs +++ b/integration/test/Test/Brig.hs @@ -1,14 +1,20 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# OPTIONS_GHC -Wno-incomplete-patterns #-} + module Test.Brig where -import API.Brig +import API.Brig as BrigP import qualified API.BrigInternal as BrigI import API.Common +import API.GalleyInternal (setTeamFeatureStatus) +import API.Spar import Data.Aeson.Types hiding ((.=)) import Data.List.Split import Data.String.Conversions import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID import GHC.Stack +import SAML2.WebSSO.Test.Util (SampleIdP (..), makeSampleIdPMetadata) import SetupHelpers import System.IO.Extra import Testlib.Assertions @@ -229,3 +235,37 @@ testSFTFederation = do maybe (assertFailure "is_federating missing") asBool =<< lookupField resp.json "is_federating" when isFederating $ assertFailure "is_federating should be false" + +testDeleteEmail :: (HasCallStack) => App () +testDeleteEmail = do + (owner, tid, [usr]) <- createTeam OwnDomain 2 + putSelf usr (PutSelf Nothing Nothing (Just "Alice") Nothing) >>= assertSuccess + email <- getSelf usr >>= getJSON 200 >>= (%. "email") >>= asString + + let associateUsrWithSSO :: (HasCallStack) => App () + associateUsrWithSSO = do + void $ setTeamFeatureStatus owner tid "sso" "enabled" + registerTestIdPWithMeta owner >>= assertSuccess + tok <- createScimToken owner >>= getJSON 200 >>= (%. "token") >>= asString + void $ findUsersByExternalId owner tok email + + searchShouldBe :: (HasCallStack) => String -> App () + searchShouldBe expected = do + BrigI.refreshIndex OwnDomain + bindResponse (BrigP.searchTeam owner email) $ \resp -> do + resp.status `shouldMatchInt` 200 + numDocs <- length <$> (resp.json %. "documents" >>= asList) + case expected of + "empty" -> numDocs `shouldMatchInt` 0 + "non-empty" -> numDocs `shouldMatchInt` 1 + + deleteSelfEmail usr >>= assertStatus 403 + searchShouldBe "non-empty" + associateUsrWithSSO + deleteSelfEmail usr >>= assertSuccess + searchShouldBe "empty" + +registerTestIdPWithMeta :: (HasCallStack, MakesValue owner) => owner -> App Response +registerTestIdPWithMeta owner = do + SampleIdP idpmeta _ _ _ <- makeSampleIdPMetadata + createIdp owner idpmeta diff --git a/libs/cassandra-util/src/Cassandra/Util.hs b/libs/cassandra-util/src/Cassandra/Util.hs index 4331da819c5..378c6b6f2f0 100644 --- a/libs/cassandra-util/src/Cassandra/Util.hs +++ b/libs/cassandra-util/src/Cassandra/Util.hs @@ -109,7 +109,7 @@ initCassandra settings Nothing logger = do -- | Read cassandra's writetimes https://docs.datastax.com/en/dse/5.1/cql/cql/cql_using/useWritetime.html -- as UTCTime values without any loss of precision newtype Writetime a = Writetime {writetimeToUTC :: UTCTime} - deriving (Functor) + deriving (Eq, Show, Functor) instance Cql (Writetime a) where ctype = Tagged BigIntColumn 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 f55d8a83dea..ec853e7f933 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -186,10 +186,9 @@ type AccountAPI = :<|> Named "putSelfEmail" ( Summary - "internal email activation (used in tests and in spar for validating emails obtained as \ - \SAML user identifiers). if the validate query parameter is false or missing, only set \ - \the activation timeout, but do not send an email, and do not do anything about \ - \activating the email." + "Internal email update and activation. Used in tests and in spar for validating emails \ + \obtained via scim or saml implicit user creation. If the `validate` query parameter is \ + \false or missing, only update the email and do not activate." :> ZUser :> "self" :> "email" diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs index fc4d15e434e..61fab5fe704 100644 --- a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs +++ b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs @@ -205,3 +205,18 @@ data BrowseTeamFilters = BrowseTeamFilters userIdToDocId :: UserId -> DocId userIdToDocId uid = DocId (idToText uid) + +-- | We use cassandra writetimes to construct the ES index version. Since nulling fields in +-- cassandra also nulls the writetime, re-indexing does not happen when nulling a field, and +-- the old search key can still effectively be used. +-- +-- `write_time_bumper type int` is an extra field that we can update whenever we null a field +-- and want to update the write time of the table. `WriteTimeBumper` writes to 'int' fields, +-- but only cares about the field's writetime. +data WriteTimeBumper = WriteTimeBumper + deriving (Eq, Show) + +instance C.Cql WriteTimeBumper where + ctype = C.Tagged C.IntColumn + toCql WriteTimeBumper = C.CqlInt 0 + fromCql _ = pure WriteTimeBumper diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index ee7f51bab8c..f6c71536c65 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -55,7 +55,7 @@ getIndexUserBaseQuery :: LText getIndexUserBaseQuery = [sql| SELECT - id, + id, team, writetime(team), name, writetime(name), status, writetime(status), @@ -66,7 +66,8 @@ getIndexUserBaseQuery = service, writetime(service), managed_by, writetime(managed_by), sso_id, writetime(sso_id), - email_unvalidated, writetime(email_unvalidated) + email_unvalidated, writetime(email_unvalidated), + writetime(write_time_bumper) FROM user |] diff --git a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs index 2334260f447..b5a005036ce 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs @@ -22,6 +22,7 @@ import Wire.UserSearch.Types type Activated = Bool data WithWritetime a = WithWriteTime {value :: a, writetime :: Writetime a} + deriving (Eq, Show) data IndexUser = IndexUser { userId :: UserId, @@ -35,8 +36,10 @@ data IndexUser = IndexUser serviceId :: Maybe (WithWritetime ServiceId), managedBy :: Maybe (WithWritetime ManagedBy), ssoId :: Maybe (WithWritetime UserSSOId), - unverifiedEmail :: Maybe (WithWritetime EmailAddress) + unverifiedEmail :: Maybe (WithWritetime EmailAddress), + writeTimeBumper :: Maybe (Writetime WriteTimeBumper) } + deriving (Eq, Show) {- ORMOLU_DISABLE -} type instance @@ -52,12 +55,13 @@ type instance Maybe ServiceId, Maybe (Writetime ServiceId), Maybe ManagedBy, Maybe (Writetime ManagedBy), Maybe UserSSOId, Maybe (Writetime UserSSOId), - Maybe EmailAddress, Maybe (Writetime EmailAddress) + Maybe EmailAddress, Maybe (Writetime EmailAddress), + Maybe (Writetime WriteTimeBumper) ) instance Record IndexUser where asTuple (IndexUser {..}) = - ( userId, + ( userId, value <$> teamId, writetime <$> teamId, name.value, name.writetime, value <$> accountStatus, writetime <$> accountStatus, @@ -68,11 +72,12 @@ instance Record IndexUser where value <$> serviceId, writetime <$> serviceId, value <$> managedBy, writetime <$> managedBy, value <$> ssoId, writetime <$> ssoId, - value <$> unverifiedEmail, writetime <$> unverifiedEmail + value <$> unverifiedEmail, writetime <$> unverifiedEmail, + writeTimeBumper ) asRecord - ( u, + ( u, mTeam, tTeam, name, tName, status, tStatus, @@ -83,7 +88,8 @@ instance Record IndexUser where service, tService, managedBy, tManagedBy, ssoId, tSsoId, - emailUnvalidated, tEmailUnvalidated + emailUnvalidated, tEmailUnvalidated, + tWriteTimeBumper ) = IndexUser { userId = u, teamId = WithWriteTime <$> mTeam <*> tTeam, @@ -96,7 +102,8 @@ instance Record IndexUser where serviceId = WithWriteTime <$> service <*> tService, managedBy = WithWriteTime <$> managedBy <*> tManagedBy, ssoId = WithWriteTime <$> ssoId <*> tSsoId, - unverifiedEmail = WithWriteTime <$> emailUnvalidated <*> tEmailUnvalidated + unverifiedEmail = WithWriteTime <$> emailUnvalidated <*> tEmailUnvalidated, + writeTimeBumper = tWriteTimeBumper } {- ORMOLU_ENABLE -} @@ -113,7 +120,8 @@ indexUserToVersion IndexUser {..} = const () <$$> fmap writetime serviceId, const () <$$> fmap writetime managedBy, const () <$$> fmap writetime ssoId, - const () <$$> fmap writetime unverifiedEmail + const () <$$> fmap writetime unverifiedEmail, + const () <$$> writeTimeBumper ] indexUserToDoc :: SearchVisibilityInbound -> IndexUser -> UserDoc diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 769be28ee74..a824fe73c92 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -547,7 +547,7 @@ syncUserIndex :: ) => UserId -> Sem r () -syncUserIndex uid = +syncUserIndex uid = do getIndexUser uid >>= maybe deleteFromIndex upsert where diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index a1e0e5d96e1..db318e5366b 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -83,7 +83,8 @@ storedUserToIndexUser storedUser = serviceId = withDefaultTime <$> storedUser.serviceId, managedBy = withDefaultTime <$> storedUser.managedBy, ssoId = withDefaultTime <$> storedUser.ssoId, - unverifiedEmail = Nothing + unverifiedEmail = Nothing, + writeTimeBumper = Nothing } lookupLocaleImpl :: (Member (State [StoredUser]) r) => UserId -> Sem r (Maybe ((Maybe Language, Maybe Country))) diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 52642552923..9b116cb9a2b 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -183,6 +183,7 @@ library Brig.Schema.V83_AddTextStatus Brig.Schema.V84_DropTeamInvitationPhone Brig.Schema.V85_DropUserKeysHashed + Brig.Schema.V86_WriteTimeBumper Brig.Team.API Brig.Team.Email Brig.Team.Template diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 982c907e338..897bc3a4700 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -650,18 +650,11 @@ removeEmail uid = do Just (SSOIdentity (UserSSOId _) (Just e)) -> lift $ do liftSem $ deleteKey $ mkEmailKey e wrapClient $ Data.deleteEmail uid - -- FUTUREWORK: This doesn't delete user's email address from the index, - -- which is a bug, reported here: - -- https://wearezeta.atlassian.net/browse/WPB-11122. - -- - -- Calling User.internalUpdateSearchIndex here wouldn't work as explained - -- in the ticket. liftSem $ Events.generateUserEvent uid Nothing (emailRemoved uid e) + liftSem $ User.internalUpdateSearchIndex uid Just _ -> throwE LastIdentity Nothing -> throwE NoIdentity -------------------------------------------------------------------------------- - ------------------------------------------------------------------------------- -- Forcefully revoke a verified identity diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index b71bdd02cfe..e406f84afb2 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -559,7 +559,7 @@ 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 WHERE id = ?" +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/brig/src/Brig/Schema/Run.hs b/services/brig/src/Brig/Schema/Run.hs index f833b8d97b1..72bbff2b1f2 100644 --- a/services/brig/src/Brig/Schema/Run.hs +++ b/services/brig/src/Brig/Schema/Run.hs @@ -60,6 +60,7 @@ import Brig.Schema.V82_DropPhoneColumn qualified as V82_DropPhoneColumn import Brig.Schema.V83_AddTextStatus qualified as V83_AddTextStatus import Brig.Schema.V84_DropTeamInvitationPhone qualified as V84_DropTeamInvitationPhone import Brig.Schema.V85_DropUserKeysHashed qualified as V85_DropUserKeysHashed +import Brig.Schema.V86_WriteTimeBumper qualified as V86_WriteTimeBumper import Cassandra.MigrateSchema (migrateSchema) import Cassandra.Schema import Control.Exception (finally) @@ -126,7 +127,8 @@ migrations = V82_DropPhoneColumn.migration, V83_AddTextStatus.migration, V84_DropTeamInvitationPhone.migration, - V85_DropUserKeysHashed.migration + V85_DropUserKeysHashed.migration, + V86_WriteTimeBumper.migration -- FUTUREWORK: undo V41 (searchable flag); we stopped using it in -- https://github.com/wireapp/wire-server/pull/964 ] diff --git a/services/brig/src/Brig/Schema/V86_WriteTimeBumper.hs b/services/brig/src/Brig/Schema/V86_WriteTimeBumper.hs new file mode 100644 index 00000000000..e579517ac94 --- /dev/null +++ b/services/brig/src/Brig/Schema/V86_WriteTimeBumper.hs @@ -0,0 +1,36 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 Brig.Schema.V86_WriteTimeBumper + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + -- See 'WriteTimeBumper' for more explanations. + Migration 86 "Add field for keeping track of time of nulling another field" $ + schema' + [r| ALTER TABLE user ADD ( + write_time_bumper int + ) |] From 83ed2a73efa3725ac7306faca8a27865542c363d Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:56:19 +0200 Subject: [PATCH 085/136] [chore] Expose record accessors for Brig.App.Env. (#4256) * Expose record accessors for Brig.App.Env. This reduces lens usage to mostly the cases where we want lens functionality, instead of record accessor functionality. * Refactor Brig.Opt.Settings accessors. * Shorter timeout for integration. --- libs/types-common/default.nix | 2 + libs/types-common/src/Util/SuffixNamer.hs | 10 + libs/types-common/types-common.cabal | 2 + .../VerificationCodeSubsystem/Interpreter.hs | 7 +- services/brig/brig.integration.yaml | 8 +- services/brig/src/Brig/API/Auth.hs | 11 +- services/brig/src/Brig/API/Client.hs | 9 +- services/brig/src/Brig/API/Connection.hs | 5 +- .../brig/src/Brig/API/Connection/Remote.hs | 3 +- services/brig/src/Brig/API/Connection/Util.hs | 5 +- services/brig/src/Brig/API/Federation.hs | 3 +- services/brig/src/Brig/API/Handler.hs | 11 +- services/brig/src/Brig/API/Internal.hs | 10 +- .../Brig/API/MLS/KeyPackages/Validation.hs | 3 +- services/brig/src/Brig/API/MLS/Util.hs | 3 +- services/brig/src/Brig/API/OAuth.hs | 24 +- services/brig/src/Brig/API/Public.hs | 22 +- services/brig/src/Brig/API/User.hs | 18 +- services/brig/src/Brig/AWS.hs | 2 +- services/brig/src/Brig/App.hs | 279 +++++++++--------- services/brig/src/Brig/Calling/API.hs | 12 +- .../brig/src/Brig/CanonicalInterpreter.hs | 80 ++--- services/brig/src/Brig/Data/Client.hs | 14 +- services/brig/src/Brig/Data/MLS/KeyPackage.hs | 5 +- services/brig/src/Brig/Data/User.hs | 10 +- services/brig/src/Brig/Federation/Client.hs | 15 +- services/brig/src/Brig/IO/Journal.hs | 2 +- .../brig/src/Brig/InternalEvent/Process.hs | 5 +- services/brig/src/Brig/Options.hs | 236 +++++++-------- services/brig/src/Brig/Provider/API.hs | 19 +- services/brig/src/Brig/Provider/Email.hs | 13 +- services/brig/src/Brig/Provider/RPC.hs | 4 +- services/brig/src/Brig/RPC.hs | 5 +- services/brig/src/Brig/Run.hs | 33 +-- services/brig/src/Brig/Team/API.hs | 8 +- services/brig/src/Brig/Team/Email.hs | 13 +- services/brig/src/Brig/User/API/Handle.hs | 3 +- services/brig/src/Brig/User/Auth.hs | 3 +- services/brig/src/Brig/User/Auth/Cookie.hs | 28 +- services/brig/src/Brig/User/EJPD.hs | 2 +- services/brig/src/Brig/Version.hs | 5 +- services/brig/test/integration/API/Calling.hs | 6 +- .../brig/test/integration/API/Federation.hs | 6 +- services/brig/test/integration/API/OAuth.hs | 30 +- services/brig/test/integration/API/Search.hs | 22 +- .../brig/test/integration/API/Settings.hs | 4 +- .../test/integration/API/SystemSettings.hs | 4 +- services/brig/test/integration/API/Team.hs | 8 +- services/brig/test/integration/API/User.hs | 4 +- .../brig/test/integration/API/User/Account.hs | 18 +- .../brig/test/integration/API/User/Auth.hs | 12 +- .../brig/test/integration/API/User/Client.hs | 35 ++- .../brig/test/integration/API/User/Handles.hs | 2 +- .../test/integration/API/User/RichInfo.hs | 2 +- .../integration/API/UserPendingActivation.hs | 4 +- .../brig/test/integration/Index/Create.hs | 6 +- services/brig/test/integration/Run.hs | 2 +- services/brig/test/integration/Util.hs | 12 +- 58 files changed, 551 insertions(+), 578 deletions(-) create mode 100644 libs/types-common/src/Util/SuffixNamer.hs diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index c4bbd61c01b..5d16d0cfaf0 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -47,6 +47,7 @@ , tasty , tasty-hunit , tasty-quickcheck +, template-haskell , text , time , time-locale-compat @@ -102,6 +103,7 @@ mkDerivation { tagged tasty tasty-hunit + template-haskell text time time-locale-compat diff --git a/libs/types-common/src/Util/SuffixNamer.hs b/libs/types-common/src/Util/SuffixNamer.hs new file mode 100644 index 00000000000..8fd3ddbaf91 --- /dev/null +++ b/libs/types-common/src/Util/SuffixNamer.hs @@ -0,0 +1,10 @@ +module Util.SuffixNamer where + +import Control.Lens +import Imports +import Language.Haskell.TH (mkName, nameBase) + +suffixNamer :: FieldNamer +suffixNamer _ _ n = + let name = nameBase n + in [TopName (mkName (name <> "Lens"))] diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 175d3964cdc..53ac138e7f2 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -38,6 +38,7 @@ library Util.Logging Util.Options Util.Options.Common + Util.SuffixNamer Util.Test Util.Timeout Wire.Arbitrary @@ -132,6 +133,7 @@ library , tagged >=0.8 , tasty >=0.11 , tasty-hunit + , template-haskell , text >=0.11 , time >=1.6 , time-locale-compat >=0.1 diff --git a/libs/wire-subsystems/src/Wire/VerificationCodeSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/VerificationCodeSubsystem/Interpreter.hs index 156be1cbd90..408be66fc38 100644 --- a/libs/wire-subsystems/src/Wire/VerificationCodeSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/VerificationCodeSubsystem/Interpreter.hs @@ -65,7 +65,12 @@ createCodeOverwritePreviousImpl gen scope retries timeout mId = do code <- generateVerificationCode gen scope retries timeout mId maybe (pure code) (throw . VerificationCodeThrottled) =<< insert code -insert :: (Member VerificationCodeStore r, Member (Input VerificationCodeThrottleTTL) r) => Code -> Sem r (Maybe RetryAfter) +insert :: + ( Member VerificationCodeStore r, + Member (Input VerificationCodeThrottleTTL) r + ) => + Code -> + Sem r (Maybe RetryAfter) insert code = do VerificationCodeThrottleTTL ttl <- input mRetryAfter <- lookupThrottle (codeKey code) (codeScope code) diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index daec729b8c2..6333a9fe1c0 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -155,9 +155,9 @@ turn: tokenTTL: 21600 optSettings: - setActivationTimeout: 10 - setVerificationTimeout: 10 - setTeamInvitationTimeout: 10 + setActivationTimeout: 4 + setVerificationTimeout: 4 + setTeamInvitationTimeout: 4 setExpiredUserCleanupTimeout: 1 # setStomp: test/resources/stomp-credentials.yaml setUserMaxConnections: 16 @@ -171,7 +171,7 @@ optSettings: timeout: 5 # seconds. if you reach the limit, how long do you have to wait to try again. retryLimit: 5 # how many times can you have a failed login in that timeframe. setSuspendInactiveUsers: # if this is omitted: never suspend inactive users. - suspendTimeout: 10 + suspendTimeout: 4 setRichInfoLimit: 5000 # should be in sync with Spar setDefaultUserLocale: en setMaxTeamSize: 32 diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index e61f5409994..578bb4629bc 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -26,7 +26,6 @@ import Brig.Data.User qualified as User import Brig.Options import Brig.User.Auth qualified as Auth import Brig.ZAuth hiding (Env, settings) -import Control.Lens (view) import Control.Monad.Trans.Except import Data.CommaSeparatedList import Data.Id @@ -226,14 +225,14 @@ mkUserTokenCookie :: Cookie (Token u) -> m UserTokenCookie mkUserTokenCookie c = do - s <- view settings + s <- asks (.settings) pure UserTokenCookie { utcExpires = - guard (cookieType c == PersistentCookie) - $> cookieExpires c, - utcToken = mkSomeToken (cookieValue c), - utcSecure = not (setCookieInsecure s) + guard (c.cookieType == PersistentCookie) + $> c.cookieExpires, + utcToken = mkSomeToken c.cookieValue, + utcSecure = not s.cookieInsecure } partitionTokens :: diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 41b001858f9..d739b479a47 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -68,7 +68,6 @@ import Brig.User.Auth qualified as UserAuth import Brig.User.Auth.Cookie qualified as Auth import Cassandra (MonadClient) import Control.Error -import Control.Lens (view) import Control.Monad.Trans.Except (except) import Data.ByteString (toStrict) import Data.ByteString.Conversion @@ -195,7 +194,7 @@ addClientWithReAuthPolicy :: addClientWithReAuthPolicy policy luid@(tUnqualified -> u) con new = do usr <- (lift . liftSem $ User.getAccountNoFilter luid) >>= maybe (throwE (ClientUserNotFound u)) (pure . (.accountUser)) verifyCode (newClientVerificationCode new) luid - maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings + maxPermClients <- fromMaybe Opt.defUserMaxPermClients <$> asks (.settings.userMaxPermClients) let caps :: Maybe (Set ClientCapability) caps = updlhdev $ newClientCapabilities new where @@ -557,12 +556,12 @@ createAccessToken luid cid method link proof = do note MisconfiguredRequestUrl $ fromByteString $ "https://" <> toByteString' domain <> "/" <> T.encodeUtf8 (toUrlPiece link) - maxSkewSeconds <- Opt.setDpopMaxSkewSecs <$> view settings - expiresIn <- Opt.setDpopTokenExpirationTimeSecs <$> view settings + maxSkewSeconds <- Opt.setDpopMaxSkewSecs <$> asks (.settings) + expiresIn <- Opt.setDpopTokenExpirationTimeSecs <$> asks (.settings) now <- fromUTCTime <$> lift (liftSem Now.get) let expiresAt = now & addToEpoch expiresIn pubKeyBundle <- do - pathToKeys <- ExceptT $ note KeyBundleError . Opt.setPublicKeyBundle <$> view settings + pathToKeys <- ExceptT (note KeyBundleError <$> asks (.settings.publicKeyBundle)) ExceptT $ note KeyBundleError <$> liftSem (PublicKeyBundle.get pathToKeys) token <- ExceptT $ diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index f5efefff8b7..7dbbc0b86a6 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -46,7 +46,6 @@ import Brig.IO.Logging import Brig.Options import Brig.Types.Connection import Control.Error -import Control.Lens (view) import Control.Monad.Catch (throwM) import Data.Id as Id import Data.LegalHold qualified as LH @@ -342,7 +341,7 @@ updateConnectionToLocalUser self other newStatus conn = do logLocalConnection (tUnqualified self) (qUnqualified (ucTo s2o)) . msg (val "Blocking connection") traverse_ (liftSem . Intra.blockConv self) (ucConvId s2o) - mlsEnabled <- view (settings . enableMLS) + mlsEnabled <- asks (.settings.enableMLS) liftSem $ when (fromMaybe False mlsEnabled) $ do let mlsConvId = one2OneConvId BaseProtocolMLSTag (tUntagged self) (tUntagged other) isEstablished <- isMLSOne2OneEstablished self (tUntagged other) @@ -358,7 +357,7 @@ updateConnectionToLocalUser self other newStatus conn = do logLocalConnection (tUnqualified self) (qUnqualified (ucTo s2o)) . msg (val "Unblocking connection") cnv <- lift . liftSem $ traverse (unblockConversation self conn) (ucConvId s2o) - mlsEnabled <- view (settings . enableMLS) + mlsEnabled <- asks (.settings.enableMLS) lift . liftSem $ when (fromMaybe False mlsEnabled) $ do let mlsConvId = one2OneConvId BaseProtocolMLSTag (tUntagged self) (tUntagged other) isEstablished <- isMLSOne2OneEstablished self (tUntagged other) diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index 3a812f665d6..0bf4c398d5f 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -33,7 +33,6 @@ import Brig.IO.Intra qualified as Intra import Brig.Options import Control.Comonad import Control.Error.Util ((??)) -import Control.Lens (view) import Control.Monad.Trans.Except import Data.Id as Id import Data.Qualified @@ -195,7 +194,7 @@ transitionTo self mzcon other (Just connection) (Just rel) actor = do $ ucConvId connection desiredMem = desiredMembership actor rel lift $ updateOne2OneConv self Nothing other proteusConvId desiredMem actor - mlsEnabled <- view (settings . enableMLS) + mlsEnabled <- asks (.settings.enableMLS) when (fromMaybe False mlsEnabled) $ do let mlsConvId = one2OneConvId BaseProtocolMLSTag (tUntagged self) (tUntagged other) isEstablished <- lift . liftSem $ isMLSOne2OneEstablished self (tUntagged other) diff --git a/services/brig/src/Brig/API/Connection/Util.hs b/services/brig/src/Brig/API/Connection/Util.hs index 118c03bcc03..e256ff373f4 100644 --- a/services/brig/src/Brig/API/Connection/Util.hs +++ b/services/brig/src/Brig/API/Connection/Util.hs @@ -26,9 +26,8 @@ where import Brig.API.Types import Brig.App import Brig.Data.Connection qualified as Data -import Brig.Options (Settings (setUserMaxConnections)) +import Brig.Options (Settings (userMaxConnections)) import Control.Error (MaybeT, noteT) -import Control.Lens (view) import Control.Monad.Trans.Except import Data.Id (UserId) import Data.Qualified @@ -44,7 +43,7 @@ type ConnectionM r = ExceptT ConnectionError (AppT r) checkLimit :: Local UserId -> ExceptT ConnectionError (AppT r) () checkLimit u = noteT (TooManyConnections (tUnqualified u)) $ do n <- lift . wrapClient $ Data.countConnections u [Accepted, Sent] - l <- setUserMaxConnections <$> view settings + l <- asks (.settings.userMaxConnections) guard (n < l) ensureNotSameAndActivated :: (Member UserStore r) => Local UserId -> Qualified UserId -> ConnectionM r () diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 1420eacc5f8..35ebe1568b3 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -36,7 +36,6 @@ import Brig.Options import Brig.User.API.Handle import Brig.User.Search.SearchIndex qualified as Q import Control.Error.Util -import Control.Lens ((^.)) import Control.Monad.Trans.Except import Data.Domain import Data.Handle (Handle (..)) @@ -112,7 +111,7 @@ federationSitemap = getFederationStatus :: (Member FederationConfigStore r) => Domain -> DomainSet -> Handler r NonConnectedBackends getFederationStatus _ request = do cfg <- ask - case setFederationStrategy (cfg ^. settings) of + case cfg.settings.federationStrategy of Just AllowAll -> pure $ NonConnectedBackends mempty _ -> do fedDomains <- fromList . fmap (.domain) . (.remotes) <$> lift (liftSem $ E.getFederationConfigs) diff --git a/services/brig/src/Brig/API/Handler.hs b/services/brig/src/Brig/API/Handler.hs index 078358b4613..c664cd3f0e0 100644 --- a/services/brig/src/Brig/API/Handler.hs +++ b/services/brig/src/Brig/API/Handler.hs @@ -33,10 +33,9 @@ import Brig.API.Error import Brig.AWS qualified as AWS import Brig.App import Brig.CanonicalInterpreter (BrigCanonicalEffects, runBrigToIO) -import Brig.Options (setAllowlistEmailDomains) +import Brig.Options (allowlistEmailDomains) import Control.Error import Control.Exception (throwIO) -import Control.Lens (view) import Control.Monad.Catch (catches, throwM) import Control.Monad.Catch qualified as Catch import Control.Monad.Except (MonadError, throwError) @@ -64,8 +63,8 @@ type Handler r = ExceptT HttpError (AppT r) toServantHandler :: Env -> (Handler BrigCanonicalEffects) a -> Servant.Handler a toServantHandler env action = do - let logger = view applog env - reqId = unRequestId $ view requestId env + let logger = env.appLogger + reqId = unRequestId $ env.requestId a <- liftIO $ (runBrigToIO env (runExceptT action)) @@ -129,5 +128,5 @@ checkAllowlistWithError e key = do isAllowlisted :: (MonadReader Env m) => EmailAddress -> m Bool isAllowlisted key = do - env <- view settings - pure $ Allowlists.verify (setAllowlistEmailDomains env) key + env <- asks (.settings) + pure $ Allowlists.verify env.allowlistEmailDomains key diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index d6b5a13235f..35248ac45ed 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -49,7 +49,7 @@ import Brig.Types.User import Brig.User.EJPD qualified import Brig.User.Search.Index qualified as Search import Control.Error hiding (bool) -import Control.Lens (preview, to, view, _Just) +import Control.Lens (preview, to, _Just) import Data.ByteString.Conversion (toByteString) import Data.Code qualified as Code import Data.CommaSeparatedList @@ -370,7 +370,7 @@ updateFederationRemote dom fedcfg = do getAccountConferenceCallingConfig :: UserId -> Handler r (Feature ConferenceCallingConfig) getAccountConferenceCallingConfig uid = do mStatus <- lift $ wrapClient $ Data.lookupFeatureConferenceCalling uid - mDefStatus <- preview (settings . featureFlags . _Just . to conferenceCalling . to forNull) + mDefStatus <- preview (settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNull) pure $ def {status = mStatus <|> mDefStatus ?: (def :: LockableFeature ConferenceCallingConfig).status} putAccountConferenceCallingConfig :: UserId -> Feature ConferenceCallingConfig -> Handler r NoContent @@ -721,7 +721,7 @@ updateRichInfoH :: UserId -> RichInfoUpdate -> (Handler r) NoContent updateRichInfoH uid rup = NoContent <$ do let (unRichInfoAssocList -> richInfo) = normalizeRichInfoAssocList . riuRichInfo $ rup - maxSize <- setRichInfoLimit <$> view settings + maxSize <- asks (.settings.richInfoLimit) when (richInfoSize (RichInfo (mkRichInfoAssocList richInfo)) > maxSize) $ throwStd tooLargeRichInfo -- FUTUREWORK: send an event -- Intra.onUserEvent uid (Just conn) (richInfoUpdate uid ri) @@ -735,14 +735,14 @@ updateLocale uid upd@(LocaleUpdate locale) = do deleteLocale :: (Member UserSubsystem r) => UserId -> (Handler r) NoContent deleteLocale uid = do - defLoc <- setDefaultUserLocale <$> view settings + defLoc <- setDefaultUserLocale <$> asks (.settings) qUid <- qualifyLocal uid lift . liftSem $ updateUserProfile qUid Nothing UpdateOriginScim def {locale = Just defLoc} pure NoContent getDefaultUserLocale :: (Handler r) LocaleUpdate getDefaultUserLocale = do - defLocale <- setDefaultUserLocale <$> view settings + defLocale <- setDefaultUserLocale <$> asks (.settings) pure $ LocaleUpdate defLocale updateClientLastActive :: UserId -> ClientId -> Handler r () diff --git a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs index 16707b15ff4..6a2d428748c 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs @@ -29,7 +29,6 @@ import Brig.App import Brig.Data.Client qualified as Data import Brig.Options import Control.Applicative -import Control.Lens import Data.ByteString qualified as LBS import Data.Qualified import Data.Time.Clock @@ -82,7 +81,7 @@ validateUploadedKeyPackage identity kp = do validateLifetime :: Lifetime -> Handler r () validateLifetime lt = do now <- liftIO getPOSIXTime - mMaxLifetime <- setKeyPackageMaximumLifetime <$> view settings + mMaxLifetime <- asks (.settings.keyPackageMaximumLifetime) either mlsProtocolError pure $ validateLifetime' now mMaxLifetime lt diff --git a/services/brig/src/Brig/API/MLS/Util.hs b/services/brig/src/Brig/API/MLS/Util.hs index e36f83babe1..b23308d55b8 100644 --- a/services/brig/src/Brig/API/MLS/Util.hs +++ b/services/brig/src/Brig/API/MLS/Util.hs @@ -23,11 +23,10 @@ import Brig.App import Brig.Data.Client import Brig.Options import Control.Error -import Control.Lens (view) import Imports isMLSEnabled :: Handler r Bool -isMLSEnabled = fromMaybe False . setEnableMLS <$> view settings +isMLSEnabled = fromMaybe False <$> asks (.settings.enableMLS) assertMLSEnabled :: Handler r () assertMLSEnabled = diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 145d0772959..bdbdf63e048 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -91,7 +91,7 @@ oauthAPI = registerOAuthClient :: OAuthClientConfig -> (Handler r) OAuthClientCredentials registerOAuthClient (OAuthClientConfig name uri) = do - unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled + unlessM (Opt.setOAuthEnabled <$> view settingsLens) $ throwStd $ errorToWai @'OAuthFeatureDisabled credentials@(OAuthClientCredentials cid secret) <- OAuthClientCredentials <$> randomId <*> createSecret safeSecret <- liftIO $ hashClientSecret secret lift $ wrapClient $ insertOAuthClient cid name uri safeSecret @@ -108,7 +108,7 @@ rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 getOAuthClientById :: OAuthClientId -> (Handler r) OAuthClient getOAuthClientById cid = do - unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled + unlessM (Opt.setOAuthEnabled <$> view settingsLens) $ throwStd $ errorToWai @'OAuthFeatureDisabled mClient <- lift $ wrapClient $ lookupOauthClient cid maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure mClient @@ -125,7 +125,7 @@ deleteOAuthClient cid = do getOAuthClient :: Local UserId -> OAuthClientId -> (Handler r) (Maybe OAuthClient) getOAuthClient _ cid = do - unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled + unlessM (Opt.setOAuthEnabled <$> view settingsLens) $ throwStd $ errorToWai @'OAuthFeatureDisabled lift $ wrapClient $ lookupOauthClient cid createNewOAuthAuthorizationCode :: Local UserId -> CreateOAuthAuthorizationCodeRequest -> (Handler r) CreateOAuthCodeResponse @@ -177,7 +177,7 @@ data CreateNewOAuthCodeError validateAndCreateAuthorizationCode :: Local UserId -> CreateOAuthAuthorizationCodeRequest -> ExceptT CreateNewOAuthCodeError (Handler r) OAuthAuthorizationCode validateAndCreateAuthorizationCode luid@(tUnqualified -> uid) (CreateOAuthAuthorizationCodeRequest cid scope responseType redirectUrl _state _ chal) = do - failWithM CreateNewOAuthCodeErrorFeatureDisabled (assertMay . Opt.setOAuthEnabled <$> view settings) + failWithM CreateNewOAuthCodeErrorFeatureDisabled (assertMay . Opt.setOAuthEnabled <$> view settingsLens) failWith CreateNewOAuthCodeErrorUnsupportedResponseType (assertMay $ responseType == OAuthResponseTypeCode) client <- failWithM CreateNewOAuthCodeErrorClientNotFound $ getOAuthClient luid cid failWith CreateNewOAuthCodeErrorRedirectUrlMissMatch (assertMay $ client.redirectUrl == redirectUrl) @@ -186,13 +186,13 @@ validateAndCreateAuthorizationCode luid@(tUnqualified -> uid) (CreateOAuthAuthor mkAuthorizationCode :: (Handler r) OAuthAuthorizationCode mkAuthorizationCode = do oauthCode <- OAuthAuthorizationCode <$> rand32Bytes - ttl <- Opt.setOAuthAuthorizationCodeExpirationTimeSecs <$> view settings + ttl <- Opt.setOAuthAuthorizationCodeExpirationTimeSecs <$> view settingsLens lift $ wrapClient $ insertOAuthAuthorizationCode ttl oauthCode cid uid scope redirectUrl chal pure oauthCode createAccessTokenWith :: (Member Now r, Member Jwk r) => Either OAuthAccessTokenRequest OAuthRefreshAccessTokenRequest -> (Handler r) OAuthAccessTokenResponse createAccessTokenWith req = do - unlessM (Opt.setOAuthEnabled <$> view settings) $ throwStd $ errorToWai @'OAuthFeatureDisabled + unlessM (Opt.setOAuthEnabled <$> view settingsLens) $ throwStd $ errorToWai @'OAuthFeatureDisabled case req of Left reqAC -> createAccessTokenWithAuthorizationCode reqAC Right reqRT -> createAccessTokenWithRefreshToken reqRT @@ -240,18 +240,18 @@ createAccessTokenWithAuthorizationCode req = do signingKey :: (Member Jwk r) => (Handler r) JWK signingKey = do - fp <- view settings >>= maybe (throwStd $ errorToWai @'OAuthJwtError) pure . Opt.setOAuthJwkKeyPair + fp <- maybe (throwStd $ errorToWai @'OAuthJwtError) pure =<< asks (.settings.oAuthJwkKeyPair) lift (liftSem $ Jwk.get fp) >>= maybe (throwStd $ errorToWai @'OAuthJwtError) pure createAccessToken :: (Member Now r) => JWK -> UserId -> OAuthClientId -> OAuthScopes -> (Handler r) OAuthAccessTokenResponse createAccessToken key uid cid scope = do - exp <- fromIntegral . Opt.setOAuthAccessTokenExpirationTimeSecs <$> view settings + exp <- fromIntegral . Opt.setOAuthAccessTokenExpirationTimeSecs <$> view settingsLens accessToken <- mkAccessToken (rid, refreshToken) <- mkRefreshToken now <- lift (liftSem Now.get) let refreshTokenInfo = OAuthRefreshTokenInfo rid cid uid scope now - refreshTokenExpiration <- Opt.setOAuthRefreshTokenExpirationTimeSecs <$> view settings - maxActiveTokens <- Opt.setOAuthMaxActiveRefreshTokens <$> view settings + refreshTokenExpiration <- Opt.setOAuthRefreshTokenExpirationTimeSecs <$> view settingsLens + maxActiveTokens <- Opt.setOAuthMaxActiveRefreshTokens <$> view settingsLens lift $ wrapClient $ insertOAuthRefreshToken maxActiveTokens refreshTokenExpiration refreshTokenInfo pure $ OAuthAccessTokenResponse accessToken OAuthAccessTokenTypeBearer exp refreshToken where @@ -264,8 +264,8 @@ createAccessToken key uid cid scope = do mkAccessToken :: (Member Now r) => (Handler r) OAuthAccessToken mkAccessToken = do - domain <- Opt.setFederationDomain <$> view settings - exp <- fromIntegral . Opt.setOAuthAccessTokenExpirationTimeSecs <$> view settings + domain <- asks (.settings.federationDomain) + exp <- fromIntegral . Opt.setOAuthAccessTokenExpirationTimeSecs <$> view settingsLens claims <- mkAccessTokenClaims uid domain scope exp OAuthToken <$> signAccessToken claims diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index fe994a98952..d413d2493e2 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -58,7 +58,7 @@ import Brig.User.Auth.Cookie qualified as Auth import Cassandra qualified as C import Cassandra qualified as Data import Control.Error hiding (bool, note) -import Control.Lens (view, (.~), (?~)) +import Control.Lens ((.~), (?~)) import Control.Monad.Catch (throwM) import Control.Monad.Except import Data.Aeson hiding (json) @@ -537,7 +537,7 @@ getMultiUserPrekeyBundleUnqualifiedH :: Public.UserClients -> Handler r Public.UserClientPrekeyMap getMultiUserPrekeyBundleUnqualifiedH zusr userClients = do - maxSize <- fromIntegral . setMaxConvSize <$> view settings + maxSize <- fromIntegral <$> asks (.settings.maxConvSize) when (Map.size (Public.userClients userClients) > maxSize) $ throwStd (errorToWai @'E.TooManyClients) API.claimLocalMultiPrekeyBundles (ProtectedUser zusr) userClients !>> clientError @@ -547,7 +547,7 @@ getMultiUserPrekeyBundleHInternal :: Public.QualifiedUserClients -> m () getMultiUserPrekeyBundleHInternal qualUserClients = do - maxSize <- fromIntegral . setMaxConvSize <$> view settings + maxSize <- fromIntegral <$> asks (.settings.maxConvSize) let Sum (size :: Int) = Map.foldMapWithKey (\_ v -> Sum . Map.size $ v) @@ -679,7 +679,7 @@ getClientPrekeys usr clt = lift (wrapClient $ API.lookupPrekeyIds usr clt) newNonce :: UserId -> ClientId -> (Handler r) (Nonce, CacheControl) newNonce uid cid = do - ttl <- setNonceTtlSecs <$> view settings + ttl <- setNonceTtlSecs <$> asks (.settings) nonce <- randomNonce lift $ wrapClient $ Nonce.insertNonce ttl uid (Id.clientToText cid) nonce pure (nonce, NoStore) @@ -1074,7 +1074,7 @@ searchUsersHandler luid term mDomain mMaxResults = -- feature, ghc will guide us here. customerExtensionCheckBlockedDomains :: Public.EmailAddress -> (Handler r) () customerExtensionCheckBlockedDomains email = do - mBlockedDomains <- asks (fmap domainsBlockedForRegistration . setCustomerExtensions . view settings) + mBlockedDomains <- fmap (.domainsBlockedForRegistration) <$> asks (.settings.customerExtensions) for_ mBlockedDomains $ \(DomainsBlockedForRegistration blockedDomains) -> do case mkDomain (Text.decodeUtf8 $ Public.domainPart email) of Left _ -> @@ -1335,7 +1335,7 @@ sendVerificationCode req = do case (mbAccount, featureEnabled) of (Just account, True) -> do let gen = mk6DigitVerificationCodeGen email - timeout <- setVerificationTimeout <$> view settings + timeout <- verificationTimeout <$> asks (.settings) code <- lift . liftSem $ createCodeOverwritePrevious @@ -1367,16 +1367,16 @@ sendVerificationCode req = do getSystemSettings :: (Handler r) SystemSettingsPublic getSystemSettings = do - optSettings <- view settings + optSettings <- asks (.settings) pure $ SystemSettingsPublic $ - fromMaybe False (setRestrictUserCreation optSettings) + fromMaybe False optSettings.restrictUserCreation getSystemSettingsInternal :: UserId -> (Handler r) SystemSettings getSystemSettingsInternal _ = do - optSettings <- view settings - let pSettings = SystemSettingsPublic $ fromMaybe False (setRestrictUserCreation optSettings) - let iSettings = SystemSettingsInternal $ fromMaybe False (setEnableMLS optSettings) + optSettings <- asks (.settings) + let pSettings = SystemSettingsPublic $ fromMaybe False optSettings.restrictUserCreation + let iSettings = SystemSettingsInternal $ fromMaybe False optSettings.enableMLS pure $ SystemSettings pSettings iSettings -- Deprecated diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 897bc3a4700..6f94a7e6fd7 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -93,7 +93,7 @@ import Brig.User.Auth.Cookie qualified as Auth import Brig.User.Search.TeamSize qualified as TeamSize import Cassandra hiding (Set) import Control.Error -import Control.Lens (preview, to, view, (^.), _Just) +import Control.Lens (preview, to, (^.), _Just) import Control.Monad.Catch import Data.ByteString.Conversion import Data.Code @@ -485,7 +485,7 @@ createUser new = do handleEmailActivation email uid newTeam = do fmap join . for (mkEmailKey <$> email) $ \ek -> case newUserEmailCode new of Nothing -> do - timeout <- setActivationTimeout <$> view settings + timeout <- asks (.settings.activationTimeout) edata <- lift . wrapClient $ Data.newActivation ek timeout (Just uid) lift . liftSem . Log.info $ field "user" (toByteString uid) @@ -521,7 +521,7 @@ findTeamInvitation (Just e) c = where ensureMemberCanJoin :: (Member GalleyAPIAccess r) => TeamId -> ExceptT RegisterError (AppT r) () ensureMemberCanJoin tid = do - maxSize <- fromIntegral . setMaxTeamSize <$> view settings + maxSize <- fromIntegral <$> asks (.settings.maxTeamSize) (TeamSize teamSize) <- TeamSize.teamSize tid when (teamSize >= maxSize) $ throwE RegisterErrorTooManyTeamMembers @@ -532,7 +532,7 @@ findTeamInvitation (Just e) c = initAccountFeatureConfig :: UserId -> (AppT r) () initAccountFeatureConfig uid = do - mStatus <- preview (settings . featureFlags . _Just . to conferenceCalling . to forNew . _Just) + mStatus <- preview (settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNew . _Just) wrapClient $ traverse_ (Data.updateFeatureConferenceCalling uid . Just) mStatus -- | 'createUser' is becoming hard to maintain, and instead of adding more case distinctions @@ -555,8 +555,8 @@ createUserInviteViaScim (NewUserScimInvitation tid uid extId loc name email _) = -- add the expiry table entry first! (if brig creates an account, and then crashes before -- creating the expiry table entry, gc will miss user data.) expiresAt <- do - ttl <- setTeamInvitationTimeout <$> view settings - now <- liftIO =<< view currentTime + ttl <- asks (.settings.teamInvitationTimeout) + now <- liftIO =<< asks (.currentTime) pure $ addUTCTime (realToFrac ttl) now lift . liftSem $ UserPendingActivationStore.add (UserPendingActivation uid expiresAt) @@ -571,7 +571,7 @@ createUserInviteViaScim (NewUserScimInvitation tid uid extId loc name email _) = -- | docs/reference/user/registration.md {#RefRestrictRegistration}. checkRestrictedUserCreation :: NewUser -> ExceptT RegisterError (AppT r) () checkRestrictedUserCreation new = do - restrictPlease <- lift . asks $ fromMaybe False . setRestrictUserCreation . view settings + restrictPlease <- fromMaybe False <$> asks (.settings.restrictUserCreation) when ( restrictPlease && not (isNewUserTeamMember new) @@ -630,7 +630,7 @@ changeEmail u email updateOrigin = do _ -> do unless (userManagedBy usr /= ManagedByScim || updateOrigin == UpdateOriginScim) $ throwE EmailManagedByScim - timeout <- setActivationTimeout <$> view settings + timeout <- asks (.settings.activationTimeout) act <- lift . wrapClient $ Data.newActivation ek timeout (Just u) pure $ ChangeEmailNeedsActivation (usr, act, email) @@ -834,7 +834,7 @@ sendActivationCode email loc = do where notFound = throwM . UserDisplayNameNotFound mkPair k c u = do - timeout <- setActivationTimeout <$> view settings + timeout <- asks (.settings.activationTimeout) case c of Just c' -> liftIO $ (,c') <$> Data.mkActivationKey k Nothing -> lift $ do diff --git a/services/brig/src/Brig/AWS.hs b/services/brig/src/Brig/AWS.hs index 48e927124be..f6c96a7bdf8 100644 --- a/services/brig/src/Brig/AWS.hs +++ b/services/brig/src/Brig/AWS.hs @@ -20,7 +20,7 @@ module Brig.AWS ( -- * Monad - Env, + Env (..), mkEnv, Amazon, amazonkaEnv, diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 80d38fbb7ea..098faa3ee11 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -1,6 +1,5 @@ {-# LANGUAGE DeepSubsumption #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} -- FUTUREWORK: Get rid of this option once Polysemy is fully introduced to Brig @@ -27,45 +26,47 @@ module Brig.App ( schemaVersion, -- * App Environment - Env, + Env (..), + mkIndexEnv, newEnv, closeEnv, - awsEnv, - smtpEnv, - cargohold, - galley, - galleyEndpoint, - gundeckEndpoint, - cargoholdEndpoint, - federator, - casClient, - indexEnv, - userTemplates, - providerTemplates, - teamTemplates, + providerTemplatesWithLocale, + teamTemplatesWithLocale, teamTemplatesNoLocale, - templateBranding, - requestId, - httpManager, - http2Manager, - extGetManager, - settings, - currentTime, - zauthEnv, - digestSHA256, - digestMD5, - applog, - turnEnv, - sftEnv, - internalEvents, - emailSender, - randomPrekeyLocalLock, - keyPackageLocalLock, - rabbitmqChannel, - fsWatcher, - disabledVersions, - enableSFTFederation, - mkIndexEnv, + cargoholdLens, + galleyLens, + galleyEndpointLens, + gundeckEndpointLens, + cargoholdEndpointLens, + federatorLens, + casClientLens, + smtpEnvLens, + emailSenderLens, + awsEnvLens, + appLoggerLens, + internalEventsLens, + requestIdLens, + userTemplatesLens, + providerTemplatesLens, + teamTemplatesLens, + templateBrandingLens, + httpManagerLens, + http2ManagerLens, + extGetManagerLens, + settingsLens, + fsWatcherLens, + turnEnvLens, + sftEnvLens, + currentTimeLens, + zauthEnvLens, + digestSHA256Lens, + digestMD5Lens, + indexEnvLens, + randomPrekeyLocalLockLens, + keyPackageLocalLockLens, + rabbitmqChannelLens, + disabledVersionsLens, + enableSFTFederationLens, -- * App Monad AppT (..), @@ -149,6 +150,7 @@ import System.Logger.Class hiding (Settings, settings) import System.Logger.Class qualified as LC import System.Logger.Extended qualified as Log import Util.Options +import Util.SuffixNamer import Wire.API.Federation.Error (federationNotImplemented) import Wire.API.Locale (Locale) import Wire.API.Routes.Version @@ -169,43 +171,43 @@ schemaVersion = Migrations.lastSchemaVersion -- Environment data Env = Env - { _cargohold :: RPC.Request, - _galley :: RPC.Request, - _galleyEndpoint :: Endpoint, - _gundeckEndpoint :: Endpoint, - _cargoholdEndpoint :: Endpoint, - _federator :: Maybe Endpoint, -- FUTUREWORK: should we use a better type here? E.g. to avoid fresh connections all the time? - _casClient :: Cas.ClientState, - _smtpEnv :: Maybe SMTP.SMTP, - _emailSender :: EmailAddress, - _awsEnv :: AWS.Env, - _applog :: Logger, - _internalEvents :: QueueEnv, - _requestId :: RequestId, - _userTemplates :: Localised UserTemplates, - _provTemplates :: Localised ProviderTemplates, - _tmTemplates :: Localised TeamTemplates, - _templateBranding :: TemplateBranding, - _httpManager :: Manager, - _http2Manager :: Http2Manager, - _extGetManager :: (Manager, [Fingerprint Rsa] -> SSL.SSL -> IO ()), - _settings :: Settings, - _fsWatcher :: FS.WatchManager, - _turnEnv :: Calling.TurnEnv, - _sftEnv :: Maybe Calling.SFTEnv, - _currentTime :: IO UTCTime, - _zauthEnv :: ZAuth.Env, - _digestSHA256 :: Digest, - _digestMD5 :: Digest, - _indexEnv :: IndexEnv, - _randomPrekeyLocalLock :: Maybe (MVar ()), - _keyPackageLocalLock :: MVar (), - _rabbitmqChannel :: Maybe (MVar Q.Channel), - _disabledVersions :: Set Version, - _enableSFTFederation :: Maybe Bool + { cargohold :: RPC.Request, + galley :: RPC.Request, + galleyEndpoint :: Endpoint, + gundeckEndpoint :: Endpoint, + cargoholdEndpoint :: Endpoint, + federator :: Maybe Endpoint, -- FUTUREWORK: should we use a better type here? E.g. to avoid fresh connections all the time? + casClient :: Cas.ClientState, + smtpEnv :: Maybe SMTP.SMTP, + emailSender :: EmailAddress, + awsEnv :: AWS.Env, + appLogger :: Logger, + internalEvents :: QueueEnv, + requestId :: RequestId, + userTemplates :: Localised UserTemplates, + providerTemplates :: Localised ProviderTemplates, + teamTemplates :: Localised TeamTemplates, + templateBranding :: TemplateBranding, + httpManager :: Manager, + http2Manager :: Http2Manager, + extGetManager :: (Manager, [Fingerprint Rsa] -> SSL.SSL -> IO ()), + settings :: Settings, + fsWatcher :: FS.WatchManager, + turnEnv :: Calling.TurnEnv, + sftEnv :: Maybe Calling.SFTEnv, + currentTime :: IO UTCTime, + zauthEnv :: ZAuth.Env, + digestSHA256 :: Digest, + digestMD5 :: Digest, + indexEnv :: IndexEnv, + randomPrekeyLocalLock :: Maybe (MVar ()), + keyPackageLocalLock :: MVar (), + rabbitmqChannel :: Maybe (MVar Q.Channel), + disabledVersions :: Set Version, + enableSFTFederation :: Maybe Bool } -makeLenses ''Env +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Env validateOptions :: Opts -> IO () validateOptions o = @@ -242,14 +244,14 @@ newEnv o = do let sett = Opt.optSettings o eventsQueue :: QueueEnv <- case Opt.internalEventsQueue (Opt.internalEvents o) of StompQueueOpts q -> do - stomp :: Stomp.Env <- case (Opt.stomp o, Opt.setStomp sett) of + stomp :: Stomp.Env <- case (o.stomp, sett.stomp) of (Just s, Just c) -> Stomp.mkEnv s <$> initCredentials c (Just _, Nothing) -> error "STOMP is configured but 'setStomp' is not set" (Nothing, Just _) -> error "'setStomp' is present but STOMP is not configured" (Nothing, Nothing) -> error "stomp is selected for internal events, but not configured in 'setStomp', STOMP" pure (StompQueueEnv (Stomp.broker stomp) q) SqsQueueOpts q -> do - let throttleMillis = fromMaybe Opt.defSqsThrottleMillis (view Opt.sqsThrottleMillis $ Opt.optSettings o) + let throttleMillis = fromMaybe Opt.defSqsThrottleMillis o.optSettings.sqsThrottleMillis SqsQueueEnv aws throttleMillis <$> AWS.getQueueUrl (aws ^. AWS.amazonkaEnv) q mSFTEnv <- mapM (Calling.mkSFTEnv sha512) $ Opt.sft o prekeyLocalLock <- case Opt.randomPrekeys o of @@ -261,44 +263,44 @@ newEnv o = do pure Nothing kpLock <- newMVar () rabbitChan <- traverse (Q.mkRabbitMqChannelMVar lgr) o.rabbitmq - let allDisabledVersions = foldMap expandVersionExp (Opt.setDisabledAPIVersions sett) + let allDisabledVersions = foldMap expandVersionExp sett.disabledAPIVersions idxEnv <- mkIndexEnv o.elasticsearch lgr (Opt.galley o) mgr pure $! Env - { _cargohold = mkEndpoint $ Opt.cargohold o, - _galley = mkEndpoint $ Opt.galley o, - _galleyEndpoint = Opt.galley o, - _gundeckEndpoint = Opt.gundeck o, - _cargoholdEndpoint = Opt.cargohold o, - _federator = Opt.federatorInternal o, - _casClient = cas, - _smtpEnv = emailSMTP, - _emailSender = Opt.emailSender . Opt.general . Opt.emailSMS $ o, - _awsEnv = aws, -- used by `journalEvent` directly - _applog = lgr, - _internalEvents = (eventsQueue :: QueueEnv), - _requestId = RequestId "N/A", - _userTemplates = utp, - _provTemplates = ptp, - _tmTemplates = ttp, - _templateBranding = branding, - _httpManager = mgr, - _http2Manager = h2Mgr, - _extGetManager = ext, - _settings = sett, - _turnEnv = turn, - _sftEnv = mSFTEnv, - _fsWatcher = w, - _currentTime = clock, - _zauthEnv = zau, - _digestMD5 = md5, - _digestSHA256 = sha256, - _indexEnv = idxEnv, - _randomPrekeyLocalLock = prekeyLocalLock, - _keyPackageLocalLock = kpLock, - _rabbitmqChannel = rabbitChan, - _disabledVersions = allDisabledVersions, - _enableSFTFederation = Opt.multiSFT o + { cargohold = mkEndpoint $ Opt.cargohold o, + galley = mkEndpoint $ Opt.galley o, + galleyEndpoint = Opt.galley o, + gundeckEndpoint = Opt.gundeck o, + cargoholdEndpoint = Opt.cargohold o, + federator = Opt.federatorInternal o, + casClient = cas, + smtpEnv = emailSMTP, + emailSender = Opt.emailSender . Opt.general . Opt.emailSMS $ o, + awsEnv = aws, -- used by `journalEvent` directly + appLogger = lgr, + internalEvents = (eventsQueue :: QueueEnv), + requestId = RequestId "N/A", + userTemplates = utp, + providerTemplates = ptp, + teamTemplates = ttp, + templateBranding = branding, + httpManager = mgr, + http2Manager = h2Mgr, + extGetManager = ext, + settings = sett, + turnEnv = turn, + sftEnv = mSFTEnv, + fsWatcher = w, + currentTime = clock, + zauthEnv = zau, + digestMD5 = md5, + digestSHA256 = sha256, + indexEnv = idxEnv, + randomPrekeyLocalLock = prekeyLocalLock, + keyPackageLocalLock = kpLock, + rabbitmqChannel = rabbitChan, + disabledVersions = allDisabledVersions, + enableSFTFederation = Opt.multiSFT o } where emailConn _ (Opt.EmailAWS aws) = pure (Just aws, Nothing) @@ -438,23 +440,23 @@ initCassandra o g = (Just schemaVersion) g -providerTemplates :: (MonadReader Env m) => Maybe Locale -> m (Locale, ProviderTemplates) -providerTemplates l = forLocale l <$> view provTemplates +teamTemplatesWithLocale :: (MonadReader Env m) => Maybe Locale -> m (Locale, TeamTemplates) +teamTemplatesWithLocale l = forLocale l <$> asks (.teamTemplates) -teamTemplates :: (MonadReader Env m) => Maybe Locale -> m (Locale, TeamTemplates) -teamTemplates l = forLocale l <$> view tmTemplates +providerTemplatesWithLocale :: (MonadReader Env m) => Maybe Locale -> m (Locale, ProviderTemplates) +providerTemplatesWithLocale l = forLocale l <$> asks (.providerTemplates) -- this works because team templates is not affected by `forLocale`; it is useful where we -- use the `TeamTemplates` only for finding invitation url templates (those are not localized). teamTemplatesNoLocale :: (MonadReader Env m) => m TeamTemplates -teamTemplatesNoLocale = snd <$> teamTemplates Nothing +teamTemplatesNoLocale = snd <$> teamTemplatesWithLocale Nothing closeEnv :: Env -> IO () closeEnv e = do - Cas.shutdown $ e ^. casClient - FS.stopManager $ e ^. fsWatcher - Log.flush $ e ^. applog - Log.close $ e ^. applog + Cas.shutdown $ e.casClient + FS.stopManager $ e.fsWatcher + Log.flush $ e.appLogger + Log.close $ e.appLogger ------------------------------------------------------------------------------- -- App Monad @@ -517,14 +519,14 @@ liftSem sem = AppT $ lift sem instance (MonadIO m) => MonadLogger (ReaderT Env m) where log l m = do - g <- view applog - r <- view requestId + g <- asks (.appLogger) + r <- asks (.requestId) Log.log g l $ field "request" (unRequestId r) ~~ m instance MonadLogger (AppT r) where log l m = do - g <- view applog - r <- view requestId + g <- asks (.appLogger) + r <- asks (.requestId) AppT $ lift $ embedFinal @IO $ @@ -536,23 +538,22 @@ instance MonadLogger (ExceptT err (AppT r)) where instance MonadHttp (AppT r) where handleRequestWithCont req handler = do - manager <- view httpManager + manager <- asks (.httpManager) liftIO $ withResponse req manager handler instance MonadZAuth (AppT r) where - liftZAuth za = view zauthEnv >>= \e -> runZAuth e za + liftZAuth za = asks (.zauthEnv) >>= \e -> runZAuth e za instance MonadZAuth (ExceptT err (AppT r)) where - liftZAuth za = lift (view zauthEnv) >>= flip runZAuth za + liftZAuth za = lift (asks (.zauthEnv)) >>= flip runZAuth za -- | The function serves as a crutch while Brig is being polysemised. Use it -- whenever the compiler complains that there is no instance of `MonadClient` -- for `AppT r`. It can be removed once there is no `AppT` anymore. wrapClient :: ReaderT Env Cas.Client a -> AppT r a wrapClient m = do - c <- view casClient env <- ask - runClient c $ runReaderT m env + runClient env.casClient $ runReaderT m env wrapClientE :: ExceptT e (ReaderT Env Cas.Client) a -> ExceptT e (AppT r) a wrapClientE = mapExceptT wrapClient @@ -587,22 +588,22 @@ newtype HttpClientIO a = HttpClientIO runHttpClientIO :: (MonadIO m) => Env -> HttpClientIO a -> m a runHttpClientIO env = - runClient (env ^. casClient) - . runHttpT (env ^. httpManager) + runClient (env.casClient) + . runHttpT (env.httpManager) . flip runReaderT env . unHttpClientIO instance MonadZAuth HttpClientIO where - liftZAuth za = view zauthEnv >>= flip runZAuth za + liftZAuth za = asks (.zauthEnv) >>= flip runZAuth za instance HasRequestId HttpClientIO where - getRequestId = view requestId + getRequestId = asks (.requestId) instance Cas.MonadClient HttpClientIO where liftClient cl = do env <- ask - liftIO $ runClient (view casClient env) cl - localState f = local (casClient %~ f) + liftIO $ runClient (asks (.casClient) env) cl + localState f = local (casClientLens %~ f) instance MonadMonitor HttpClientIO where doIO = liftIO @@ -616,17 +617,17 @@ wrapHttpClientE :: ExceptT e HttpClientIO a -> ExceptT e (AppT r) a wrapHttpClientE = mapExceptT wrapHttpClient instance (MonadIO m) => MonadIndexIO (ReaderT Env m) where - liftIndexIO m = view indexEnv >>= \e -> runIndexIO e m + liftIndexIO m = asks (.indexEnv) >>= \e -> runIndexIO e m instance MonadIndexIO (AppT r) where liftIndexIO m = do AppT $ mapReaderT (embedToFinal @IO) $ liftIndexIO m instance (MonadIndexIO (AppT r)) => MonadIndexIO (ExceptT err (AppT r)) where - liftIndexIO m = view indexEnv >>= \e -> runIndexIO e m + liftIndexIO m = asks (.indexEnv) >>= \e -> runIndexIO e m instance HasRequestId (AppT r) where - getRequestId = view requestId + getRequestId = asks (.requestId) ------------------------------------------------------------------------------- -- Ad hoc interpreters @@ -634,20 +635,20 @@ instance HasRequestId (AppT r) where -- | similarly to `wrapClient`, this function serves as a crutch while Brig is being polysemised. adhocUserKeyStoreInterpreter :: (MonadIO m, MonadReader Env m) => Sem '[UserKeyStore, UserStore, Embed IO] a -> m a adhocUserKeyStoreInterpreter action = do - clientState <- asks (view casClient) + clientState <- asks (.casClient) liftIO $ runM . interpretUserStoreCassandra clientState . interpretUserKeyStoreCassandra clientState $ action -- | similarly to `wrapClient`, this function serves as a crutch while Brig is being polysemised. adhocSessionStoreInterpreter :: (MonadIO m, MonadReader Env m) => Sem '[SessionStore, Embed IO] a -> m a adhocSessionStoreInterpreter action = do - clientState <- asks (view casClient) + clientState <- asks (.casClient) liftIO $ runM . interpretSessionStoreCassandra clientState $ action -------------------------------------------------------------------------------- -- Federation viewFederationDomain :: (MonadReader Env m) => m Domain -viewFederationDomain = view (settings . Opt.federationDomain) +viewFederationDomain = asks (.settings.federationDomain) -- FUTUREWORK: rename to 'qualifyLocalMtl' qualifyLocal :: (MonadReader Env m) => a -> m (Local a) diff --git a/services/brig/src/Brig/Calling/API.hs b/services/brig/src/Brig/Calling/API.hs index 3618f038093..bc5ae8d6013 100644 --- a/services/brig/src/Brig/Calling/API.hs +++ b/services/brig/src/Brig/Calling/API.hs @@ -76,11 +76,11 @@ getCallsConfigV2 :: Maybe (Range 1 10 Int) -> (Handler r) Public.RTCConfiguration getCallsConfigV2 uid _ limit = do - env <- view turnEnv - staticUrl <- view $ settings . Opt.sftStaticUrl - sftListAllServers <- fromMaybe Opt.HideAllSFTServers <$> view (settings . Opt.sftListAllServers) - sftEnv' <- view sftEnv - sftFederation <- view enableSFTFederation + env <- asks (.turnEnv) + staticUrl <- asks (.settings.sftStaticUrl) + sftListAllServers <- fromMaybe Opt.HideAllSFTServers <$> asks (.settings.sftListAllServers) + sftEnv' <- asks (.sftEnv) + sftFederation <- asks (.enableSFTFederation) discoveredServers <- turnServersV2 (env ^. turnServers) shared <- do ccStatus <- lift $ liftSem $ ((.status) . npProject @ConferenceCallingConfig <$> getAllTeamFeaturesForUser (Just uid)) @@ -115,7 +115,7 @@ getCallsConfig :: ConnId -> (Handler r) Public.RTCConfiguration getCallsConfig uid _ = do - env <- view turnEnv + env <- asks (.turnEnv) discoveredServers <- turnServersV1 (env ^. turnServers) shared <- do ccStatus <- lift $ liftSem $ ((.status) . npProject @ConferenceCallingConfig <$> getAllTeamFeaturesForUser (Just uid)) diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 936c1c7dd95..6f427928974 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -11,7 +11,7 @@ import Brig.Effects.SFT (SFT, interpretSFT) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore.Cassandra (userPendingActivationStoreToCassandra) import Brig.IO.Intra (runEvents) -import Brig.Options (ImplicitNoFederationRestriction (federationDomainConfig), federationDomainConfigs, federationStrategy) +import Brig.Options (federationDomainConfigs, federationStrategy) import Brig.Options qualified as Opt import Brig.Team.Template (TeamTemplates) import Brig.User.Search.Index (IndexEnv (..)) @@ -161,35 +161,35 @@ runBrigToIO :: App.Env -> AppT BrigCanonicalEffects a -> IO a runBrigToIO e (AppT ma) = do let userSubsystemConfig = UserSubsystemConfig - { emailVisibilityConfig = e ^. settings . Opt.emailVisibility, - defaultLocale = e ^. settings . to Opt.setDefaultUserLocale, - searchSameTeamOnly = e ^. settings . Opt.searchSameTeamOnly . to (fromMaybe False) + { emailVisibilityConfig = e.settings.emailVisibility, + defaultLocale = e.settings ^. to Opt.setDefaultUserLocale, + searchSameTeamOnly = fromMaybe False e.settings.searchSameTeamOnly } federationApiAccessConfig = FederationAPIAccessConfig - { ownDomain = e ^. settings . Opt.federationDomain, - federatorEndpoint = e ^. federator, - http2Manager = e ^. App.http2Manager, - requestId = e ^. App.requestId + { ownDomain = e.settings.federationDomain, + federatorEndpoint = e.federator, + http2Manager = e.http2Manager, + requestId = e.requestId } propertySubsystemConfig = PropertySubsystemConfig - { maxKeyLength = fromMaybe Opt.defMaxKeyLen $ e ^. settings . Opt.propertyMaxKeyLen, - maxValueLength = fromMaybe Opt.defMaxValueLen $ e ^. settings . Opt.propertyMaxValueLen, + { maxKeyLength = fromMaybe Opt.defMaxKeyLen e.settings.propertyMaxKeyLen, + maxValueLength = fromMaybe Opt.defMaxValueLen e.settings.propertyMaxValueLen, maxProperties = 16 } - mainESEnv = e ^. App.indexEnv . to idxElastic + mainESEnv = e.indexEnv ^. to idxElastic indexedUserStoreConfig = IndexedUserStoreConfig { conn = ESConn { env = mainESEnv, - indexName = e ^. App.indexEnv . to idxName + indexName = e.indexEnv ^. to idxName }, additionalConn = - (e ^. App.indexEnv . to idxAdditionalName) <&> \additionalIndexName -> + (e.indexEnv ^. to idxAdditionalName) <&> \additionalIndexName -> ESConn - { env = e ^. App.indexEnv . to idxAdditionalElastic . to (fromMaybe mainESEnv), + { env = e.indexEnv ^. to idxAdditionalElastic . to (fromMaybe mainESEnv), indexName = additionalIndexName } } @@ -200,43 +200,43 @@ runBrigToIO e (AppT ma) = do . interpretRace . embedToFinal . runEmbedded (runHttpClientIO e) - . loggerToTinyLogReqId (e ^. App.requestId) (e ^. applog) + . loggerToTinyLogReqId e.requestId e.appLogger . runError @SomeException . mapError @ErrorCall SomeException . mapError @ParseException SomeException - . interpretClientToIO (e ^. casClient) + . interpretClientToIO e.casClient . runMetricsToIO - . runRpcWithHttp (e ^. httpManager) (e ^. App.requestId) + . runRpcWithHttp e.httpManager e.requestId . emailSendingInterpreter e - . interpretGalleyAPIAccessToRpc (e ^. disabledVersions) (e ^. galleyEndpoint) + . interpretGalleyAPIAccessToRpc e.disabledVersions e.galleyEndpoint . passwordResetCodeStoreToCassandra @Cas.Client . randomToIO . runDelay - . nowToIOAction (e ^. currentTime) + . nowToIOAction e.currentTime . userPendingActivationStoreToCassandra - . interpretBlockListStoreToCassandra (e ^. casClient) + . interpretBlockListStoreToCassandra e.casClient . interpretJwtTools . interpretPublicKeyBundle . interpretJwk - . interpretFederationDomainConfig (e ^. casClient) (e ^. settings . federationStrategy) (foldMap (remotesMapFromCfgFile . fmap (.federationDomainConfig)) (e ^. settings . federationDomainConfigs)) - . runGundeckAPIAccess (e ^. gundeckEndpoint) - . runNotificationSubsystemGundeck (defaultNotificationSubsystemConfig (e ^. App.requestId)) + . interpretFederationDomainConfig e.casClient e.settings.federationStrategy (foldMap (remotesMapFromCfgFile . fmap (.federationDomainConfig)) e.settings.federationDomainConfigs) + . runGundeckAPIAccess e.gundeckEndpoint + . runNotificationSubsystemGundeck (defaultNotificationSubsystemConfig e.requestId) . runInputConst (teamTemplatesNoLocale e) - . runInputConst (e ^. settings . Opt.allowlistEmailDomains) - . runInputConst (toLocalUnsafe (e ^. settings . Opt.federationDomain) ()) + . runInputConst e.settings.allowlistEmailDomains + . runInputConst (toLocalUnsafe e.settings.federationDomain ()) . runInputSem (embed getCurrentTime) - . runInputConst (e ^. settings . to Opt.set2FACodeGenerationDelaySecs . to fromIntegral) + . runInputConst (fromIntegral $ Opt.twoFACodeGenerationDelaySecs e.settings) . connectionStoreToCassandra - . interpretSFT (e ^. httpManager) - . interpretPropertyStoreCassandra (e ^. casClient) - . interpretInvitationCodeStoreToCassandra (e ^. casClient) - . interpretActivationCodeStoreToCassandra (e ^. casClient) - . interpretVerificationCodeStoreCassandra (e ^. casClient) - . interpretPasswordStore (e ^. casClient) - . interpretSessionStoreCassandra (e ^. casClient) + . interpretSFT e.httpManager + . interpretPropertyStoreCassandra e.casClient + . interpretInvitationCodeStoreToCassandra e.casClient + . interpretActivationCodeStoreToCassandra e.casClient + . interpretVerificationCodeStoreCassandra e.casClient + . interpretPasswordStore e.casClient + . interpretSessionStoreCassandra e.casClient . interpretIndexedUserStoreES indexedUserStoreConfig - . interpretUserStoreCassandra (e ^. casClient) - . interpretUserKeyStoreCassandra (e ^. casClient) + . interpretUserStoreCassandra e.casClient + . interpretUserKeyStoreCassandra e.casClient . runHashPassword . interpretFederationAPIAccess federationApiAccessConfig . rethrowHttpErrorIO @@ -246,10 +246,10 @@ runBrigToIO e (AppT ma) = do . mapError authenticationSubsystemErrorToHttpError . mapError userSubsystemErrorToHttpError . runEvents - . runDeleteQueue (e ^. internalEvents) + . runDeleteQueue e.internalEvents . interpretPropertySubsystem propertySubsystemConfig . interpretVerificationCodeSubsystem - . emailSubsystemInterpreter (e ^. userTemplates) (e ^. templateBranding) + . emailSubsystemInterpreter e.userTemplates e.templateBranding . runUserSubsystem userSubsystemConfig . interpretAuthenticationSubsystem ) @@ -265,6 +265,6 @@ rethrowHttpErrorIO act = do emailSendingInterpreter :: (Member (Embed IO) r) => Env -> InterpreterFor EmailSending r emailSendingInterpreter e = do - case (e ^. smtpEnv) of - Just smtp -> emailViaSMTPInterpreter (e ^. applog) smtp - Nothing -> emailViaSESInterpreter (e ^. awsEnv . amazonkaEnv) + case e.smtpEnv of + Just smtp -> emailViaSMTPInterpreter e.appLogger smtp + Nothing -> emailViaSESInterpreter (e.awsEnv ^. amazonkaEnv) diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 9bae096a0f3..2f7fb87921b 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -166,7 +166,7 @@ addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients cps = do insert :: (MonadClient m, MonadReader Brig.App.Env m) => UserId -> ExceptT ClientDataError m Client insert uid = do -- Is it possible to do this somewhere else? Otherwise we could use `MonadClient` instead - now <- toUTCTimeMillis <$> (liftIO =<< view currentTime) + now <- toUTCTimeMillis <$> (liftIO =<< asks (.currentTime)) let keys = unpackLastPrekey (newClientLastKey c) : newClientPrekeys c updatePrekeys uid newId keys let mdl = newClientModel c @@ -253,7 +253,7 @@ rmClient :: rmClient u c = do retry x5 $ write removeClient (params LocalQuorum (u, c)) retry x5 $ write removeClientKeys (params LocalQuorum (u, c)) - unlessM (isJust <$> view randomPrekeyLocalLock) $ deleteOptLock u c + unlessM (isJust <$> asks (.randomPrekeyLocalLock)) $ deleteOptLock u c updateClientLabel :: (MonadClient m) => UserId -> ClientId -> Maybe Text -> m () updateClientLabel u c l = retry x5 $ write updateClientLabelQuery (params LocalQuorum (l, u, c)) @@ -296,7 +296,7 @@ claimPrekey :: ClientId -> m (Maybe ClientPrekey) claimPrekey u c = - view randomPrekeyLocalLock >>= \case + asks (.randomPrekeyLocalLock) >>= \case -- Use random prekey selection strategy Just localLock -> withLocalLock localLock $ do prekeys <- retry x1 $ query userPrekeys (params LocalQuorum (u, c)) @@ -488,8 +488,8 @@ deleteOptLock :: ClientId -> m () deleteOptLock u c = do - t <- view (awsEnv . prekeyTable) - e <- view (awsEnv . amazonkaEnv) + t <- asks ((.awsEnv) <&> view prekeyTable) + e <- asks ((.awsEnv) <&> view amazonkaEnv) void $ exec e (AWS.newDeleteItem t & AWS.deleteItem_key .~ key u c) withOptLock :: @@ -559,8 +559,8 @@ withOptLock u c ma = go (10 :: Int) (Text -> r) -> m (Maybe x) execDyn cnv mkCmd = do - cmd <- mkCmd <$> view (awsEnv . prekeyTable) - e <- view (awsEnv . amazonkaEnv) + cmd <- mkCmd <$> asks ((.awsEnv) <&> view prekeyTable) + e <- asks ((.awsEnv) <&> view amazonkaEnv) liftIO $ execDyn' e cnv cmd where execDyn' :: diff --git a/services/brig/src/Brig/Data/MLS/KeyPackage.hs b/services/brig/src/Brig/Data/MLS/KeyPackage.hs index f2950c27cac..20f95a40afa 100644 --- a/services/brig/src/Brig/Data/MLS/KeyPackage.hs +++ b/services/brig/src/Brig/Data/MLS/KeyPackage.hs @@ -30,7 +30,6 @@ import Brig.Options import Cassandra import Control.Arrow import Control.Error -import Control.Lens import Control.Monad.Random (randomRIO) import Data.Functor import Data.Id @@ -70,7 +69,7 @@ claimKeyPackage :: MaybeT m (KeyPackageRef, KeyPackageData) claimKeyPackage u c suite = do -- FUTUREWORK: investigate better locking strategies - lock <- lift $ view keyPackageLocalLock + lock <- lift $ asks (.keyPackageLocalLock) -- get a random key package and delete it (ref, kpd) <- MaybeT . withMVar lock . const $ do kps <- getNonClaimedKeyPackages u c suite @@ -98,7 +97,7 @@ getNonClaimedKeyPackages u c suite = do let decodedKps = foldMap (keepDecoded . (decodeKp &&& id)) kps now <- liftIO getPOSIXTime - mMaxLifetime <- setKeyPackageMaximumLifetime <$> view settings + mMaxLifetime <- asks (.settings.keyPackageMaximumLifetime) let (kpsExpired, kpsNonExpired) = partition (hasExpired now mMaxLifetime) decodedKps diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index e406f84afb2..bf26066e582 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -115,7 +115,7 @@ data ReAuthError -- there, it was claimed properly. newAccount :: (MonadClient m, MonadReader Env m) => NewUser -> Maybe InvitationId -> Maybe TeamId -> Maybe Handle -> m (UserAccount, Maybe Password) newAccount u inv tid mbHandle = do - defLoc <- setDefaultUserLocale <$> view settings + defLoc <- setDefaultUserLocale <$> asks (.settings) domain <- viewFederationDomain uid <- Id <$> do @@ -127,10 +127,10 @@ newAccount u inv tid mbHandle = do expiry <- case status of Ephemeral -> do -- Ephemeral users' expiry time is in expires_in (default sessionTokenTimeout) seconds - e <- view zauthEnv + e <- asks (.zauthEnv) let ZAuth.SessionTokenTimeout defTTL = e ^. ZAuth.settings . ZAuth.sessionTokenTimeout ttl = maybe defTTL fromRange (newUserExpiresIn u) - now <- liftIO =<< view currentTime + now <- liftIO =<< asks (.currentTime) pure . Just . toUTCTimeMillis $ addUTCTime (fromIntegral ttl) now _ -> pure Nothing pure (UserAccount (user uid domain (locale defLoc) expiry) status, passwd) @@ -152,7 +152,7 @@ newAccount u inv tid mbHandle = do newAccountInviteViaScim :: (MonadReader Env m) => UserId -> Text -> TeamId -> Maybe Locale -> Name -> EmailAddress -> m UserAccount newAccountInviteViaScim uid externalId tid locale name email = do - defLoc <- setDefaultUserLocale <$> view settings + defLoc <- setDefaultUserLocale <$> asks (.settings) let loc = fromMaybe defLoc locale domain <- viewFederationDomain pure (UserAccount (user domain loc) PendingInvitation) @@ -399,7 +399,7 @@ lookupAuth u = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Ident -- Skips nonexistent users. /Does not/ skip users who have been deleted. lookupUsers :: (MonadClient m, MonadReader Env m) => HavePendingInvitations -> [UserId] -> m [User] lookupUsers hpi usrs = do - loc <- setDefaultUserLocale <$> view settings + loc <- setDefaultUserLocale <$> asks (.settings) domain <- viewFederationDomain toUsers domain loc hpi <$> retry x1 (query usersSelect (params LocalQuorum (Identity usrs))) diff --git a/services/brig/src/Brig/Federation/Client.hs b/services/brig/src/Brig/Federation/Client.hs index d86f706169f..19e0193e1ce 100644 --- a/services/brig/src/Brig/Federation/Client.hs +++ b/services/brig/src/Brig/Federation/Client.hs @@ -18,8 +18,7 @@ -- FUTUREWORK: Remove this module all together. module Brig.Federation.Client where -import Brig.App as Brig -import Control.Lens +import Brig.App import Control.Monad import Control.Monad.Catch (MonadMask, throwM) import Control.Monad.Trans.Except (ExceptT (..), throwE) @@ -154,7 +153,7 @@ notifyUserDeleted self remotes = do let remoteConnections = tUnqualified remotes let notif = UserDeletedConnectionsNotification (tUnqualified self) remoteConnections remoteDomain = tDomain remotes - view rabbitmqChannel >>= \case + asks (.rabbitmqChannel) >>= \case Just chanVar -> do enqueueNotification (tDomain self) remoteDomain Q.Persistent chanVar $ fedQueueClient @'OnUserDeletedConnectionsTag notif @@ -172,7 +171,7 @@ enqueueNotification ownDomain remoteDomain deliveryMode chanVar action = do recovering policy [logRetries (const $ pure True) logError] (const go) where logError willRetry (SomeException e) status = do - rid <- view Brig.requestId + rid <- asks (.requestId) Log.err $ Log.msg @Text "failed to enqueue notification in RabbitMQ" . Log.field "error" (displayException e) @@ -180,7 +179,7 @@ enqueueNotification ownDomain remoteDomain deliveryMode chanVar action = do . Log.field "retryCount" status.rsIterNumber . Log.field "request" rid go = do - rid <- view Brig.requestId + rid <- asks (.requestId) mChan <- timeout (1 :: Second) (readMVar chanVar) case mChan of Nothing -> throwM NoRabbitMqChannel @@ -198,9 +197,9 @@ runBrigFederatorClient :: ExceptT FederationError m a runBrigFederatorClient targetDomain action = do ownDomain <- viewFederationDomain - endpoint <- view federator >>= maybe (throwE FederationNotConfigured) pure - mgr <- view http2Manager - rid <- view Brig.requestId + endpoint <- asks (.federator) >>= maybe (throwE FederationNotConfigured) pure + mgr <- asks (.http2Manager) + rid <- asks (.requestId) let env = FederatorClientEnv { ceOriginDomain = ownDomain, diff --git a/services/brig/src/Brig/IO/Journal.hs b/services/brig/src/Brig/IO/Journal.hs index 0cc46ef335a..ccaba403926 100644 --- a/services/brig/src/Brig/IO/Journal.hs +++ b/services/brig/src/Brig/IO/Journal.hs @@ -63,7 +63,7 @@ journalEvent :: (MonadReader Env m, MonadIO m) => UserEvent'EventType -> UserId journalEvent typ uid em loc tid nm = -- this may be the only place that uses awsEnv from brig Env. refactor it to use the -- DeleteQueue effect instead? - view awsEnv >>= \env -> for_ (view AWS.userJournalQueue env) $ \queue -> do + asks (.awsEnv) >>= \env -> for_ (view AWS.userJournalQueue env) $ \queue -> do ts <- now rnd <- liftIO nextRandom let userEvent :: UserEvent = diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 031de77e11f..d9c6f32abd7 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -22,9 +22,8 @@ import Brig.App import Brig.IO.Intra (rmClient) import Brig.IO.Intra qualified as Intra import Brig.InternalEvent.Types -import Brig.Options (defDeleteThrottleMillis, setDeleteThrottleMillis) +import Brig.Options (defDeleteThrottleMillis, deleteThrottleMillis) import Brig.Provider.API qualified as API -import Control.Lens (view) import Control.Monad.Catch import Data.ByteString.Conversion import Data.Qualified (Local) @@ -75,7 +74,7 @@ onEvent n = handleTimeout $ case n of -- As user deletions are expensive resource-wise in the context of -- bulk user deletions (e.g. during team deletions), -- wait 'delay' ms before processing the next event - deleteThrottleMillis <- embed $ fromMaybe defDeleteThrottleMillis . setDeleteThrottleMillis <$> view settings + deleteThrottleMillis <- embed $ fromMaybe defDeleteThrottleMillis <$> asks (.settings.deleteThrottleMillis) delay (1000 * deleteThrottleMillis) DeleteService pid sid -> do Log.info $ diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 10cfef98b9e..98de6f404bd 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -28,7 +28,7 @@ import Brig.Queue.Types (QueueOpts (..)) import Brig.User.Auth.Cookie.Limit import Brig.ZAuth qualified as ZAuth import Control.Applicative -import Control.Lens qualified as Lens +import Control.Lens as Lens hiding (Level, element, enum) import Data.Aeson import Data.Aeson.Types qualified as A import Data.Char qualified as Char @@ -49,6 +49,7 @@ import Network.AMQP.Extended import Network.DNS qualified as DNS import System.Logger.Extended (Level, LogFormat) import Util.Options +import Util.SuffixNamer import Util.Timeout import Wire.API.Allowlists (AllowlistEmailDomains (..)) import Wire.API.Routes.FederationDomainConfig @@ -435,73 +436,73 @@ data Opts = Opts -- | Options that persist as runtime settings. data Settings = Settings { -- | Activation timeout, in seconds - setActivationTimeout :: !Timeout, + activationTimeout :: !Timeout, -- | Default verification code timeout, in seconds - -- use `setVerificationTimeout` as the getter function which always provides a default value - setVerificationCodeTimeoutInternal :: !(Maybe Code.Timeout), + -- use `verificationTimeout` as the getter function which always provides a default value + verificationCodeTimeoutInternal :: !(Maybe Code.Timeout), -- | Team invitation timeout, in seconds - setTeamInvitationTimeout :: !Timeout, + teamInvitationTimeout :: !Timeout, -- | Check for expired users every so often, in seconds - setExpiredUserCleanupTimeout :: !(Maybe Timeout), + expiredUserCleanupTimeout :: !(Maybe Timeout), -- | STOMP broker credentials - setStomp :: !(Maybe FilePathSecrets), + stomp :: !(Maybe FilePathSecrets), -- | Whitelist of allowed emails/phones - setAllowlistEmailDomains :: !(Maybe AllowlistEmailDomains), + allowlistEmailDomains :: !(Maybe AllowlistEmailDomains), -- | Max. number of sent/accepted -- connections per user - setUserMaxConnections :: !Int64, + userMaxConnections :: !Int64, -- | Max. number of permanent clients per user - setUserMaxPermClients :: !(Maybe Int), + userMaxPermClients :: !(Maybe Int), -- | Whether to allow plain HTTP transmission -- of cookies (for testing purposes only) - setCookieInsecure :: !Bool, + cookieInsecure :: !Bool, -- | Minimum age of a user cookie before -- it is renewed during token refresh - setUserCookieRenewAge :: !Integer, + userCookieRenewAge :: !Integer, -- | Max. # of cookies per user and cookie type - setUserCookieLimit :: !Int, - -- | Throttling settings (not to be confused + userCookieLimit :: !Int, + -- | Throttling tings (not to be confused -- with 'LoginRetryOpts') - setUserCookieThrottle :: !CookieThrottle, + userCookieThrottle :: !CookieThrottle, -- | Block user from logging in -- for m minutes after n failed -- logins - setLimitFailedLogins :: !(Maybe LimitFailedLogins), + limitFailedLogins :: !(Maybe LimitFailedLogins), -- | If last cookie renewal is too long ago, -- suspend the user. - setSuspendInactiveUsers :: !(Maybe SuspendInactiveUsers), + suspendInactiveUsers :: !(Maybe SuspendInactiveUsers), -- | Max size of rich info (number of chars in -- field names and values), should be in sync -- with Spar - setRichInfoLimit :: !Int, + richInfoLimit :: !Int, -- | Default locale to use when selecting templates - -- use `setDefaultTemplateLocale` as the getter function which always provides a default value - setDefaultTemplateLocaleInternal :: !(Maybe Locale), + -- use `defaultTemplateLocale` as the getter function which always provides a default value + defaultTemplateLocaleInternal :: !(Maybe Locale), -- | Default locale to use for users - -- use `setDefaultUserLocale` as the getter function which always provides a default value - setDefaultUserLocaleInternal :: !(Maybe Locale), + -- use `defaultUserLocale` as the getter function which always provides a default value + defaultUserLocaleInternal :: !(Maybe Locale), -- | Max. # of members in a team. -- NOTE: This must be in sync with galley - setMaxTeamSize :: !Word32, + maxTeamSize :: !Word32, -- | Max. # of members in a conversation. -- NOTE: This must be in sync with galley - setMaxConvSize :: !Word16, + maxConvSize :: !Word16, -- | Filter ONLY services with -- the given provider id - setProviderSearchFilter :: !(Maybe ProviderId), + providerSearchFilter :: !(Maybe ProviderId), -- | Whether to expose user emails and to whom - setEmailVisibility :: !EmailVisibilityConfig, - setPropertyMaxKeyLen :: !(Maybe Int64), - setPropertyMaxValueLen :: !(Maybe Int64), + emailVisibility :: !EmailVisibilityConfig, + propertyMaxKeyLen :: !(Maybe Int64), + propertyMaxValueLen :: !(Maybe Int64), -- | How long, in milliseconds, to wait -- in between processing delete events -- from the internal delete queue - setDeleteThrottleMillis :: !(Maybe Int), + deleteThrottleMillis :: !(Maybe Int), -- | When true, search only -- returns users from the same team - setSearchSameTeamOnly :: !(Maybe Bool), + searchSameTeamOnly :: !(Maybe Bool), -- | FederationDomain is required, even when not wanting to federate with other backends - -- (in that case the 'setFederationStrategy' can be set to `allowNone` below, or to + -- (in that case the 'federationStrategy' can be set to `allowNone` below, or to -- `allowDynamic` while keeping the list of allowed domains empty, see -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections) -- Federation domain is used to qualify local IDs and handles, @@ -510,20 +511,20 @@ data Settings = Settings -- >>> _wire-server-federator._tcp. -- 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 all services. - setFederationDomain :: !Domain, + federationDomain :: !Domain, -- | See https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections -- default: AllowNone - setFederationStrategy :: !(Maybe FederationStrategy), - -- | 'setFederationDomainConfigs' is introduced in + federationStrategy :: !(Maybe FederationStrategy), + -- | 'federationDomainConfigs' is introduced in -- https://github.com/wireapp/wire-server/pull/3260 for the sole purpose of transitioning -- to dynamic federation remote configuration. See -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections -- for details. -- default: [] - setFederationDomainConfigs :: !(Maybe [ImplicitNoFederationRestriction]), + federationDomainConfigs :: !(Maybe [ImplicitNoFederationRestriction]), -- | In seconds. Default: 10 seconds. Values <1 are silently replaced by 1. See -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections - setFederationDomainConfigsUpdateFreq :: !(Maybe Int), + federationDomainConfigsUpdateFreq :: !(Maybe Int), -- | The amount of time in milliseconds to wait after reading from an SQS queue -- returns no message, before asking for messages from SQS again. -- defaults to 'defSqsThrottleMillis'. @@ -532,60 +533,60 @@ data Settings = Settings -- ensures that there is only one request every 20 seconds. -- However, that parameter is not honoured when using fake-sqs -- (where throttling can thus make sense) - setSqsThrottleMillis :: !(Maybe Int), + sqsThrottleMillis :: !(Maybe Int), -- | Do not allow certain user creation flows. -- docs/reference/user/registration.md {#RefRestrictRegistration}. - setRestrictUserCreation :: !(Maybe Bool), - -- | The analog to `Galley.Options.setFeatureFlags`. See 'AccountFeatureConfigs'. - setFeatureFlags :: !(Maybe UserFeatureFlags), + restrictUserCreation :: !(Maybe Bool), + -- | The analog to `Galley.Options.featureFlags`. See 'AccountFeatureConfigs'. + featureFlags :: !(Maybe UserFeatureFlags), -- | Customer extensions. Read 'CustomerExtensions' docs carefully! - setCustomerExtensions :: !(Maybe CustomerExtensions), + customerExtensions :: !(Maybe CustomerExtensions), -- | When set; instead of using SRV lookups to discover SFTs the calls -- config will always return this entry. This is useful in Kubernetes -- where SFTs are deployed behind a load-balancer. In the long-run the SRV -- fetching logic can go away completely - setSftStaticUrl :: !(Maybe HttpsUrl), + sftStaticUrl :: !(Maybe HttpsUrl), -- | When set the /calls/config/v2 endpoint will include all the - -- loadbalanced servers of `setSftStaticUrl` under the @sft_servers_all@ - -- field. The default setting is to exclude and omit the field from the + -- loadbalanced servers of `sftStaticUrl` under the @sft_servers_all@ + -- field. The default ting is to exclude and omit the field from the -- response. - setSftListAllServers :: Maybe ListAllSFTServers, - setEnableMLS :: Maybe Bool, - setKeyPackageMaximumLifetime :: Maybe NominalDiffTime, + sftListAllServers :: Maybe ListAllSFTServers, + enableMLS :: Maybe Bool, + keyPackageMaximumLifetime :: Maybe NominalDiffTime, -- | Disabled versions are not advertised and are completely disabled. - setDisabledAPIVersions :: !(Set VersionExp), + disabledAPIVersions :: !(Set VersionExp), -- | Minimum delay in seconds between consecutive attempts to generate a new verification code. - -- use `set2FACodeGenerationDelaySecs` as the getter function which always provides a default value - set2FACodeGenerationDelaySecsInternal :: !(Maybe Int), + -- use `2FACodeGenerationDelaySecs` as the getter function which always provides a default value + twoFACodeGenerationDelaySecsInternal :: !(Maybe Int), -- | The time-to-live of a nonce in seconds. - -- use `setNonceTtlSecs` as the getter function which always provides a default value - setNonceTtlSecsInternal :: !(Maybe NonceTtlSecs), + -- use `nonceTtlSecs` as the getter function which always provides a default value + nonceTtlSecsInternal :: !(Maybe NonceTtlSecs), -- | The maximum number of seconds of clock skew the implementation of generate_dpop_access_token in jwt-tools will allow - -- use `setDpopMaxSkewSecs` as the getter function which always provides a default value - setDpopMaxSkewSecsInternal :: !(Maybe Word16), + -- use `dpopMaxSkewSecs` as the getter function which always provides a default value + dpopMaxSkewSecsInternal :: !(Maybe Word16), -- | The expiration time of a JWT DPoP token in seconds. - -- use `setDpopTokenExpirationTimeSecs` as the getter function which always provides a default value - setDpopTokenExpirationTimeSecsInternal :: !(Maybe Word64), + -- use `dpopTokenExpirationTimeSecs` as the getter function which always provides a default value + dpopTokenExpirationTimeSecsInternal :: !(Maybe Word64), -- | Path to a .pem file containing the server's public key and private key -- e.g. to sign JWT tokens - setPublicKeyBundle :: !(Maybe FilePath), + publicKeyBundle :: !(Maybe FilePath), -- | Path to the public and private JSON web key pair used to sign OAuth access tokens - setOAuthJwkKeyPair :: !(Maybe FilePath), + oAuthJwkKeyPair :: !(Maybe FilePath), -- | The expiration time of an OAuth access token in seconds. - -- use `setOAuthAccessTokenExpirationTimeSecs` as the getter function which always provides a default value - setOAuthAccessTokenExpirationTimeSecsInternal :: !(Maybe Word64), + -- use `oAuthAccessTokenExpirationTimeSecs` as the getter function which always provides a default value + oAuthAccessTokenExpirationTimeSecsInternal :: !(Maybe Word64), -- | The expiration time of an OAuth authorization code in seconds. - -- use `setOAuthAuthorizationCodeExpirationTimeSecs` as the getter function which always provides a default value - setOAuthAuthorizationCodeExpirationTimeSecsInternal :: !(Maybe Word64), + -- use `oAuthAuthorizationCodeExpirationTimeSecs` as the getter function which always provides a default value + oAuthAuthorizationCodeExpirationTimeSecsInternal :: !(Maybe Word64), -- | En-/Disable OAuth - -- use `setOAuthEnabled` as the getter function which always provides a default value - setOAuthEnabledInternal :: !(Maybe Bool), + -- use `oAuthEnabled` as the getter function which always provides a default value + oAuthEnabledInternal :: !(Maybe Bool), -- | The expiration time of an OAuth refresh token in seconds. - -- use `setOAuthRefreshTokenExpirationTimeSecs` as the getter function which always provides a default value - setOAuthRefreshTokenExpirationTimeSecsInternal :: !(Maybe Word64), + -- use `oAuthRefreshTokenExpirationTimeSecs` as the getter function which always provides a default value + oAuthRefreshTokenExpirationTimeSecsInternal :: !(Maybe Word64), -- | The maximum number of active OAuth refresh tokens a user is allowed to have. - -- use `setOAuthMaxActiveRefreshTokens` as the getter function which always provides a default value - setOAuthMaxActiveRefreshTokensInternal :: !(Maybe Word32) + -- use `oAuthMaxActiveRefreshTokens` as the getter function which always provides a default value + oAuthMaxActiveRefreshTokensInternal :: !(Maybe Word32) } deriving (Show, Generic) @@ -611,70 +612,70 @@ defaultUserLocale :: Locale defaultUserLocale = defaultTemplateLocale setDefaultUserLocale :: Settings -> Locale -setDefaultUserLocale = fromMaybe defaultUserLocale . setDefaultUserLocaleInternal +setDefaultUserLocale = fromMaybe defaultUserLocale . defaultUserLocaleInternal defVerificationTimeout :: Code.Timeout defVerificationTimeout = Code.Timeout (60 * 10) -- 10 minutes -setVerificationTimeout :: Settings -> Code.Timeout -setVerificationTimeout = fromMaybe defVerificationTimeout . setVerificationCodeTimeoutInternal +verificationTimeout :: Settings -> Code.Timeout +verificationTimeout = fromMaybe defVerificationTimeout . verificationCodeTimeoutInternal setDefaultTemplateLocale :: Settings -> Locale -setDefaultTemplateLocale = fromMaybe defaultTemplateLocale . setDefaultTemplateLocaleInternal +setDefaultTemplateLocale = fromMaybe defaultTemplateLocale . defaultTemplateLocaleInternal def2FACodeGenerationDelaySecs :: Int def2FACodeGenerationDelaySecs = 5 * 60 -- 5 minutes -set2FACodeGenerationDelaySecs :: Settings -> Int -set2FACodeGenerationDelaySecs = fromMaybe def2FACodeGenerationDelaySecs . set2FACodeGenerationDelaySecsInternal +twoFACodeGenerationDelaySecs :: Settings -> Int +twoFACodeGenerationDelaySecs = fromMaybe def2FACodeGenerationDelaySecs . twoFACodeGenerationDelaySecsInternal defaultNonceTtlSecs :: NonceTtlSecs defaultNonceTtlSecs = NonceTtlSecs $ 5 * 60 -- 5 minutes setNonceTtlSecs :: Settings -> NonceTtlSecs -setNonceTtlSecs = fromMaybe defaultNonceTtlSecs . setNonceTtlSecsInternal +setNonceTtlSecs = fromMaybe defaultNonceTtlSecs . nonceTtlSecsInternal defaultDpopMaxSkewSecs :: Word16 defaultDpopMaxSkewSecs = 1 setDpopMaxSkewSecs :: Settings -> Word16 -setDpopMaxSkewSecs = fromMaybe defaultDpopMaxSkewSecs . setDpopMaxSkewSecsInternal +setDpopMaxSkewSecs = fromMaybe defaultDpopMaxSkewSecs . dpopMaxSkewSecsInternal defaultDpopTokenExpirationTimeSecs :: Word64 defaultDpopTokenExpirationTimeSecs = 30 setDpopTokenExpirationTimeSecs :: Settings -> Word64 -setDpopTokenExpirationTimeSecs = fromMaybe defaultDpopTokenExpirationTimeSecs . setDpopTokenExpirationTimeSecsInternal +setDpopTokenExpirationTimeSecs = fromMaybe defaultDpopTokenExpirationTimeSecs . dpopTokenExpirationTimeSecsInternal defaultOAuthAccessTokenExpirationTimeSecs :: Word64 defaultOAuthAccessTokenExpirationTimeSecs = 60 * 60 * 24 * 7 * 3 -- 3 weeks setOAuthAccessTokenExpirationTimeSecs :: Settings -> Word64 -setOAuthAccessTokenExpirationTimeSecs = fromMaybe defaultOAuthAccessTokenExpirationTimeSecs . setOAuthAccessTokenExpirationTimeSecsInternal +setOAuthAccessTokenExpirationTimeSecs = fromMaybe defaultOAuthAccessTokenExpirationTimeSecs . oAuthAccessTokenExpirationTimeSecsInternal defaultOAuthAuthorizationCodeExpirationTimeSecs :: Word64 defaultOAuthAuthorizationCodeExpirationTimeSecs = 300 -- 5 minutes setOAuthAuthorizationCodeExpirationTimeSecs :: Settings -> Word64 -setOAuthAuthorizationCodeExpirationTimeSecs = fromMaybe defaultOAuthAuthorizationCodeExpirationTimeSecs . setOAuthAuthorizationCodeExpirationTimeSecsInternal +setOAuthAuthorizationCodeExpirationTimeSecs = fromMaybe defaultOAuthAuthorizationCodeExpirationTimeSecs . oAuthAuthorizationCodeExpirationTimeSecsInternal defaultOAuthEnabled :: Bool defaultOAuthEnabled = False setOAuthEnabled :: Settings -> Bool -setOAuthEnabled = fromMaybe defaultOAuthEnabled . setOAuthEnabledInternal +setOAuthEnabled = fromMaybe defaultOAuthEnabled . oAuthEnabledInternal defaultOAuthRefreshTokenExpirationTimeSecs :: Word64 defaultOAuthRefreshTokenExpirationTimeSecs = 60 * 60 * 24 * 7 * 4 * 6 -- 24 weeks setOAuthRefreshTokenExpirationTimeSecs :: Settings -> Word64 -setOAuthRefreshTokenExpirationTimeSecs = fromMaybe defaultOAuthRefreshTokenExpirationTimeSecs . setOAuthRefreshTokenExpirationTimeSecsInternal +setOAuthRefreshTokenExpirationTimeSecs = fromMaybe defaultOAuthRefreshTokenExpirationTimeSecs . oAuthRefreshTokenExpirationTimeSecsInternal defaultOAuthMaxActiveRefreshTokens :: Word32 defaultOAuthMaxActiveRefreshTokens = 10 setOAuthMaxActiveRefreshTokens :: Settings -> Word32 -setOAuthMaxActiveRefreshTokens = fromMaybe defaultOAuthMaxActiveRefreshTokens . setOAuthMaxActiveRefreshTokensInternal +setOAuthMaxActiveRefreshTokens = fromMaybe defaultOAuthMaxActiveRefreshTokens . oAuthMaxActiveRefreshTokensInternal -- | The analog to `FeatureFlags`. At the moment, only status flags for -- conferenceCalling are stored. @@ -818,22 +819,27 @@ defSftListLength = unsafeRange 5 instance FromJSON Settings where parseJSON = genericParseJSON customOptions where + -- Convert a word to title case by capitalising the first letter + capitalise :: String -> String + capitalise [] = [] + capitalise (c : cs) = toUpper c : cs + customOptions = defaultOptions { fieldLabelModifier = \case - "setDefaultUserLocaleInternal" -> "setDefaultUserLocale" - "setDefaultTemplateLocaleInternal" -> "setDefaultTemplateLocale" - "setVerificationCodeTimeoutInternal" -> "setVerificationTimeout" - "set2FACodeGenerationDelaySecsInternal" -> "set2FACodeGenerationDelaySecs" - "setNonceTtlSecsInternal" -> "setNonceTtlSecs" - "setDpopMaxSkewSecsInternal" -> "setDpopMaxSkewSecs" - "setDpopTokenExpirationTimeSecsInternal" -> "setDpopTokenExpirationTimeSecs" - "setOAuthAuthorizationCodeExpirationTimeSecsInternal" -> "setOAuthAuthorizationCodeExpirationTimeSecs" - "setOAuthAccessTokenExpirationTimeSecsInternal" -> "setOAuthAccessTokenExpirationTimeSecs" - "setOAuthEnabledInternal" -> "setOAuthEnabled" - "setOAuthRefreshTokenExpirationTimeSecsInternal" -> "setOAuthRefreshTokenExpirationTimeSecs" - "setOAuthMaxActiveRefreshTokensInternal" -> "setOAuthMaxActiveRefreshTokens" - other -> other + "defaultUserLocaleInternal" -> "setDefaultUserLocale" + "defaultTemplateLocaleInternal" -> "setDefaultTemplateLocale" + "verificationCodeTimeoutInternal" -> "setVerificationTimeout" + "twoFACodeGenerationDelaySecsInternal" -> "set2FACodeGenerationDelaySecs" + "nonceTtlSecsInternal" -> "setNonceTtlSecs" + "dpopMaxSkewSecsInternal" -> "setDpopMaxSkewSecs" + "dpopTokenExpirationTimeSecsInternal" -> "setDpopTokenExpirationTimeSecs" + "oAuthAuthorizationCodeExpirationTimeSecsInternal" -> "setOAuthAuthorizationCodeExpirationTimeSecs" + "oAuthAccessTokenExpirationTimeSecsInternal" -> "setOAuthAccessTokenExpirationTimeSecs" + "oAuthEnabledInternal" -> "setOAuthEnabled" + "oAuthRefreshTokenExpirationTimeSecsInternal" -> "setOAuthRefreshTokenExpirationTimeSecs" + "oAuthMaxActiveRefreshTokensInternal" -> "setOAuthMaxActiveRefreshTokens" + other -> "set" <> capitalise other } instance FromJSON Opts @@ -848,42 +854,8 @@ Lens.makeLensesFor ] ''Opts -Lens.makeLensesFor - [ ("setEmailVisibility", "emailVisibility"), - ("setPropertyMaxKeyLen", "propertyMaxKeyLen"), - ("setPropertyMaxValueLen", "propertyMaxValueLen"), - ("setSearchSameTeamOnly", "searchSameTeamOnly"), - ("setUserMaxPermClients", "userMaxPermClients"), - ("setFederationDomain", "federationDomain"), - ("setSqsThrottleMillis", "sqsThrottleMillis"), - ("setSftStaticUrl", "sftStaticUrl"), - ("setSftListAllServers", "sftListAllServers"), - ("setFederationDomainConfigs", "federationDomainConfigs"), - ("setFederationStrategy", "federationStrategy"), - ("setRestrictUserCreation", "restrictUserCreation"), - ("setFeatureFlags", "featureFlags"), - ("setEnableMLS", "enableMLS"), - ("setOAuthEnabledInternal", "oauthEnabledInternal"), - ("setOAuthAuthorizationCodeExpirationTimeSecsInternal", "oauthAuthorizationCodeExpirationTimeSecsInternal"), - ("setOAuthAccessTokenExpirationTimeSecsInternal", "oauthAccessTokenExpirationTimeSecsInternal"), - ("setDisabledAPIVersions", "disabledAPIVersions"), - ("setOAuthRefreshTokenExpirationTimeSecsInternal", "oauthRefreshTokenExpirationTimeSecsInternal"), - ("setOAuthMaxActiveRefreshTokensInternal", "oauthMaxActiveRefreshTokensInternal"), - ("setAllowlistEmailDomains", "allowlistEmailDomains"), - ("setAllowlistPhonePrefixes", "allowlistPhonePrefixes") - ] - ''Settings +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Settings -Lens.makeLensesFor - [ ("url", "urlL"), - ("index", "indexL"), - ("caCert", "caCertL"), - ("insecureSkipVerifyTls", "insecureSkipVerifyTlsL"), - ("additionalWriteIndex", "additionalWriteIndexL"), - ("additionalWriteIndexUrl", "additionalWriteIndexUrlL"), - ("additionalCaCert", "additionalCaCertL"), - ("additionalInsecureSkipVerifyTls", "additionalInsecureSkipVerifyTlsL") - ] - ''ElasticSearchOpts +makeLensesWith (lensRules & lensField .~ suffixNamer) ''ElasticSearchOpts -Lens.makeLensesFor [("serversSource", "serversSourceL")] ''TurnOpts +makeLensesWith (lensRules & lensField .~ suffixNamer) ''TurnOpts diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index fcc674600f8..95ce84a5670 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -46,7 +46,7 @@ import Brig.ZAuth qualified as ZAuth import Cassandra (MonadClient) import Control.Error (throwE) import Control.Exception.Enclosed (handleAny) -import Control.Lens (view, (^.)) +import Control.Lens ((^.)) import Control.Monad.Catch (MonadMask) import Control.Monad.Except import Data.ByteString.Conversion @@ -279,8 +279,8 @@ login l = do unless (verifyPassword (providerLoginPassword l) pass) $ throwStd (errorToWai @'E.BadCredentials) token <- ZAuth.newProviderToken pid - s <- view settings - pure $ ProviderTokenCookie (ProviderToken token) (not (setCookieInsecure s)) + s <- asks (.settings) + pure $ ProviderTokenCookie (ProviderToken token) (not s.cookieInsecure) beginPasswordReset :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => Public.PasswordReset -> (Handler r) () beginPasswordReset (Public.PasswordReset target) = do @@ -563,11 +563,12 @@ searchServiceProfiles _ Nothing (Just start) mSize = do guardSecondFactorDisabled Nothing prefix :: Range 1 128 Text <- rangeChecked start let size = fromMaybe (unsafeRange 20) mSize - wrapClientE . DB.paginateServiceNames (Just prefix) (fromRange size) . setProviderSearchFilter =<< view settings + wrapClientE . DB.paginateServiceNames (Just prefix) (fromRange size) =<< asks (.settings.providerSearchFilter) searchServiceProfiles _ (Just tags) start mSize = do guardSecondFactorDisabled Nothing let size = fromMaybe (unsafeRange 20) mSize - (wrapClientE . DB.paginateServiceTags tags start (fromRange size)) . setProviderSearchFilter =<< view settings + (wrapClientE . DB.paginateServiceTags tags start (fromRange size)) + =<< asks (.settings.providerSearchFilter) searchServiceProfiles _ Nothing Nothing _ = do guardSecondFactorDisabled Nothing throwStd $ badRequest "At least `tags` or `start` must be provided." @@ -664,7 +665,7 @@ addBot zuid zcon cid add = do let mems = cnvMembers cnv unless (cnvType cnv == RegularConv) $ throwStd (errorToWai @'E.InvalidConversation) - maxSize <- fromIntegral . setMaxConvSize <$> view settings + maxSize <- fromIntegral <$> asks (.settings.maxConvSize) unless (length (cmOthers mems) < maxSize - 1) $ throwStd (errorToWai @'E.TooManyConversationMembers) -- For team conversations: bots are not allowed in @@ -695,7 +696,7 @@ addBot zuid zcon cid add = do let botReq = NewBotRequest bid bcl busr bcnv btk bloc rs <- RPC.createBot scon botReq !>> StdError . serviceError -- Insert the bot user and client - locale <- Opt.setDefaultUserLocale <$> view settings + locale <- Opt.setDefaultUserLocale <$> asks (.settings) let name = fromMaybe (serviceProfileName svp) (Ext.rsNewBotName rs) let assets = fromMaybe (serviceProfileAssets svp) (Ext.rsNewBotAssets rs) let colour = fromMaybe defaultAccentId (Ext.rsNewBotColour rs) @@ -707,7 +708,7 @@ addBot zuid zcon cid add = do { newClientPrekeys = Ext.rsNewBotPrekeys rs } lift $ wrapClient $ User.insertAccount (UserAccount usr Active) (Just (cid, cnvTeam cnv)) Nothing True - maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings + maxPermClients <- fromMaybe Opt.defUserMaxPermClients <$> asks (.settings.userMaxPermClients) (clt, _, _) <- do _ <- do -- if we want to protect bots against lh, 'addClient' cannot just send lh capability @@ -795,7 +796,7 @@ botClaimUsersPrekeys :: Handler r Public.UserClientPrekeyMap botClaimUsersPrekeys _ body = do guardSecondFactorDisabled Nothing - maxSize <- fromIntegral . setMaxConvSize <$> view settings + maxSize <- fromIntegral <$> asks (.settings.maxConvSize) when (Map.size (Public.userClients body) > maxSize) $ throwStd (errorToWai @'E.TooManyClients) Client.claimLocalMultiPrekeyBundles UnprotectedBot body !>> clientError diff --git a/services/brig/src/Brig/Provider/Email.hs b/services/brig/src/Brig/Provider/Email.hs index 173d3f164fb..b5c168d4e33 100644 --- a/services/brig/src/Brig/Provider/Email.hs +++ b/services/brig/src/Brig/Provider/Email.hs @@ -26,7 +26,6 @@ where import Brig.App import Brig.Provider.Template -import Control.Lens (view) import Data.Code qualified as Code import Data.Range import Data.Text (pack) @@ -45,8 +44,8 @@ import Wire.EmailSubsystem.Template (TemplateBranding, renderHtmlWithBranding, r sendActivationMail :: (Member EmailSending r) => Name -> EmailAddress -> Code.Key -> Code.Value -> Bool -> (AppT r) () sendActivationMail name email key code update = do - tpl <- selectTemplate update . snd <$> providerTemplates Nothing - branding <- view templateBranding + tpl <- selectTemplate update . snd <$> providerTemplatesWithLocale Nothing + branding <- asks (.templateBranding) let mail = ActivationEmail email name key code liftSem $ sendMail $ renderActivationMail mail tpl branding where @@ -97,8 +96,8 @@ renderActivationUrl t (Code.Key k) (Code.Value v) branding = sendApprovalConfirmMail :: (Member EmailSending r) => Name -> EmailAddress -> (AppT r) () sendApprovalConfirmMail name email = do - tpl <- approvalConfirmEmail . snd <$> providerTemplates Nothing - branding <- view templateBranding + tpl <- approvalConfirmEmail . snd <$> providerTemplatesWithLocale Nothing + branding <- asks (.templateBranding) let mail = ApprovalConfirmEmail email name liftSem $ sendMail $ renderApprovalConfirmMail mail tpl branding @@ -133,8 +132,8 @@ renderApprovalConfirmMail ApprovalConfirmEmail {..} ApprovalConfirmEmailTemplate sendPasswordResetMail :: (Member EmailSending r) => EmailAddress -> Code.Key -> Code.Value -> (AppT r) () sendPasswordResetMail to key code = do - tpl <- passwordResetEmail . snd <$> providerTemplates Nothing - branding <- view templateBranding + tpl <- passwordResetEmail . snd <$> providerTemplatesWithLocale Nothing + branding <- asks (.templateBranding) let mail = PasswordResetEmail to key code liftSem $ sendMail $ renderPwResetMail mail tpl branding diff --git a/services/brig/src/Brig/Provider/RPC.hs b/services/brig/src/Brig/Provider/RPC.hs index c41193cee69..7c1e710f9cb 100644 --- a/services/brig/src/Brig/Provider/RPC.hs +++ b/services/brig/src/Brig/Provider/RPC.hs @@ -35,7 +35,7 @@ import Brig.App import Brig.Provider.DB (ServiceConn (..)) import Brig.RPC import Control.Error -import Control.Lens (set, view, (^.)) +import Control.Lens (set, (^.)) import Control.Monad.Catch import Control.Retry (recovering) import Data.Aeson @@ -74,7 +74,7 @@ data ServiceError createBot :: ServiceConn -> NewBotRequest -> ExceptT ServiceError (AppT r) NewBotResponse createBot scon new = do let fprs = toList (sconFingerprints scon) - (man, verifyFingerprints) <- view extGetManager + (man, verifyFingerprints) <- asks (.extGetManager) extHandleAll onExc $ do rs <- lift $ wrapHttp $ diff --git a/services/brig/src/Brig/RPC.hs b/services/brig/src/Brig/RPC.hs index 23105e055fe..bd95ce10263 100644 --- a/services/brig/src/Brig/RPC.hs +++ b/services/brig/src/Brig/RPC.hs @@ -22,7 +22,6 @@ import Bilge import Bilge.RPC import Bilge.Retry import Brig.App -import Control.Lens import Control.Monad.Catch import Control.Retry import Data.Aeson @@ -57,12 +56,12 @@ galleyRequest = serviceRequest "galley" galley serviceRequest :: (MonadReader Env m, MonadUnliftIO m, MonadMask m, MonadHttp m, HasRequestId m) => LT.Text -> - Control.Lens.Getting Request Env Request -> + (Env -> Request) -> StdMethod -> (Request -> Request) -> m (Response (Maybe BL.ByteString)) serviceRequest nm svc m r = do - service <- view svc + service <- asks svc serviceRequestImpl nm service m r serviceRequestImpl :: diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 3be27a1c937..05d7a287a0a 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -23,7 +23,6 @@ import Brig.API.Handler import Brig.API.Internal qualified as IAPI import Brig.API.Public import Brig.API.User qualified as API -import Brig.AWS (amazonkaEnv, sesQueue) import Brig.AWS qualified as AWS import Brig.AWS.SesNotification qualified as SesNotification import Brig.App @@ -37,7 +36,7 @@ import Brig.Queue qualified as Queue import Brig.Version import Control.Concurrent.Async qualified as Async import Control.Exception.Safe (catchAny) -import Control.Lens (view, (.~), (^.)) +import Control.Lens ((.~), (^.)) import Control.Monad.Catch (MonadCatch, finally) import Control.Monad.Random (randomRIO) import Data.Aeson qualified as Aeson @@ -87,15 +86,15 @@ run o = withTracer \tracer -> do Async.async $ runBrigToIO e $ wrapHttpClient $ - Queue.listen (e ^. internalEvents) $ + Queue.listen e.internalEvents $ liftIO . runBrigToIO e . liftSem . Internal.onEvent - let throttleMillis = fromMaybe defSqsThrottleMillis $ setSqsThrottleMillis (optSettings o) - emailListener <- for (e ^. awsEnv . sesQueue) $ \q -> + let throttleMillis = fromMaybe defSqsThrottleMillis o.optSettings.sqsThrottleMillis + emailListener <- for e.awsEnv._sesQueue $ \q -> Async.async $ - AWS.execute (e ^. awsEnv) $ + AWS.execute e.awsEnv $ AWS.listen throttleMillis q (runBrigToIO e . SesNotification.onEvent) - sftDiscovery <- forM (e ^. sftEnv) $ Async.async . Calling.startSFTServiceDiscovery (e ^. applog) - turnDiscovery <- Calling.startTurnDiscovery (e ^. applog) (e ^. fsWatcher) (e ^. turnEnv) + sftDiscovery <- forM e.sftEnv $ Async.async . Calling.startSFTServiceDiscovery e.appLogger + turnDiscovery <- Calling.startTurnDiscovery e.appLogger e.fsWatcher e.turnEnv authMetrics <- Async.async (runBrigToIO e collectAuthMetrics) pendingActivationCleanupAsync <- Async.async (runBrigToIO e pendingActivationCleanup) @@ -107,7 +106,7 @@ run o = withTracer \tracer -> do closeEnv e where endpoint' = brig o - server e = defaultServer (unpack $ endpoint' ^. host) (endpoint' ^. port) (e ^. applog) + server e = defaultServer (unpack $ endpoint' ^. host) (endpoint' ^. port) e.appLogger mkApp :: Opts -> IO (Wai.Application, Env) mkApp o = do @@ -118,19 +117,19 @@ mkApp o = do middleware :: Env -> Wai.Middleware middleware e = -- this rewrites the request, so it must be at the top (i.e. applied last) - versionMiddleware (e ^. disabledVersions) + versionMiddleware e.disabledVersions -- this also rewrites the request - . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName + . requestIdMiddleware e.appLogger defaultRequestIdHeaderName . Metrics.servantPrometheusMiddleware (Proxy @ServantCombinedAPI) . GZip.gunzip . GZip.gzip GZip.def - . catchErrors (e ^. applog) defaultRequestIdHeaderName + . catchErrors e.appLogger defaultRequestIdHeaderName servantApp :: Env -> Wai.Application servantApp e0 req cont = do let rid = getRequestId defaultRequestIdHeaderName req - let e = requestId .~ rid $ e0 - let localDomain = view (settings . federationDomain) e + let e = requestIdLens .~ rid $ e0 + let localDomain = e.settings.federationDomain Servant.serveWithContext (Proxy @ServantCombinedAPI) (customFormatters :. localDomain :. Servant.EmptyContext) @@ -185,7 +184,7 @@ pendingActivationCleanup :: AppT r () pendingActivationCleanup = do safeForever "pendingActivationCleanup" $ do - now <- liftIO =<< view currentTime + now <- liftIO =<< asks (.currentTime) forExpirationsPaged $ \exps -> do uids <- for exps $ \(UserPendingActivation uid expiresAt) -> do @@ -232,7 +231,7 @@ pendingActivationCleanup = do threadDelayRandom :: (AppT r) () threadDelayRandom = do - cleanupTimeout <- fromMaybe (hours 24) . setExpiredUserCleanupTimeout <$> view settings + cleanupTimeout <- fromMaybe (hours 24) <$> asks (.settings.expiredUserCleanupTimeout) let d = realToFrac cleanupTimeout randomSecs :: Int <- liftIO (round <$> randomRIO @Double (0.5 * d, d)) threadDelay (randomSecs * 1_000_000) @@ -242,7 +241,7 @@ pendingActivationCleanup = do collectAuthMetrics :: forall r. AppT r () collectAuthMetrics = do - env <- view (awsEnv . amazonkaEnv) + env <- asks (.awsEnv._amazonkaEnv) liftIO $ forever $ do mbRemaining <- readAuthExpiration env diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index bdb0e911b45..cf4f98dbce2 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -211,7 +211,7 @@ createInvitationViaScim :: createInvitationViaScim tid newUser@(NewUserScimInvitation _tid uid _eid loc name email role) = do env <- ask let inviteeRole = role - fromEmail = env ^. App.emailSender + fromEmail = env.emailSender invreq = InvitationRequest { locale = loc, @@ -281,7 +281,7 @@ createInvitation' tid mUid inviteeRole mbInviterUid fromEmail invRequest = do unless isPersonalUserMigration $ throwStd emailExists - maxSize <- setMaxTeamSize <$> view settings + maxSize <- asks (.settings.maxTeamSize) pending <- lift $ liftSem $ Store.countInvitations tid when (fromIntegral pending >= maxSize) $ throwStd (errorToWai @'E.TooManyTeamInvitations) @@ -290,8 +290,8 @@ createInvitation' tid mUid inviteeRole mbInviterUid fromEmail invRequest = do lift $ do iid <- maybe randomId (pure . Id . toUUID) mUid - now <- liftIO =<< view currentTime - timeout <- setTeamInvitationTimeout <$> view settings + now <- liftIO =<< asks (.currentTime) + timeout <- asks (.settings.teamInvitationTimeout) code <- liftIO $ Store.mkInvitationCode newInv <- let insertInv = diff --git a/services/brig/src/Brig/Team/Email.hs b/services/brig/src/Brig/Team/Email.hs index 9bfd2d653f3..042843132e9 100644 --- a/services/brig/src/Brig/Team/Email.hs +++ b/services/brig/src/Brig/Team/Email.hs @@ -29,7 +29,6 @@ where import Brig.App import Brig.Team.Template -import Control.Lens (view) import Data.Id (TeamId, idToText) import Data.Text.Ascii qualified as Ascii import Data.Text.Lazy (toStrict) @@ -42,22 +41,22 @@ import Wire.EmailSubsystem.Template (TemplateBranding, renderHtmlWithBranding, r sendInvitationMail :: (Member EmailSending r) => EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> (AppT r) () sendInvitationMail to tid from code loc = do - tpl <- invitationEmail . snd <$> teamTemplates loc - branding <- view templateBranding + tpl <- invitationEmail . snd <$> teamTemplatesWithLocale loc + branding <- asks (.templateBranding) let mail = InvitationEmail to tid code from liftSem $ sendMail $ renderInvitationEmail mail tpl branding sendInvitationMailPersonalUser :: (Member EmailSending r) => EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> (AppT r) () sendInvitationMailPersonalUser to tid from code loc = do - tpl <- existingUserInvitationEmail . snd <$> teamTemplates loc - branding <- view templateBranding + tpl <- existingUserInvitationEmail . snd <$> teamTemplatesWithLocale loc + branding <- asks (.templateBranding) let mail = InvitationEmail to tid code from liftSem $ sendMail $ renderInvitationEmail mail tpl branding sendMemberWelcomeMail :: (Member EmailSending r) => EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () sendMemberWelcomeMail to tid teamName loc = do - tpl <- memberWelcomeEmail . snd <$> teamTemplates loc - branding <- view templateBranding + tpl <- memberWelcomeEmail . snd <$> teamTemplatesWithLocale loc + branding <- asks (.templateBranding) let mail = MemberWelcomeEmail to tid teamName liftSem $ sendMail $ renderMemberWelcomeMail mail tpl branding diff --git a/services/brig/src/Brig/User/API/Handle.hs b/services/brig/src/Brig/User/API/Handle.hs index bfa3407059a..0b97637255e 100644 --- a/services/brig/src/Brig/User/API/Handle.hs +++ b/services/brig/src/Brig/User/API/Handle.hs @@ -30,7 +30,6 @@ import Brig.App import Brig.Data.User qualified as Data import Brig.Federation.Client qualified as Federation import Brig.Options (searchSameTeamOnly) -import Control.Lens (view) import Data.Handle (Handle, fromHandle) import Data.Id (UserId) import Data.Qualified @@ -86,7 +85,7 @@ getLocalHandleInfo self handle = do -- | Checks search permissions and filters accordingly filterHandleResults :: Local UserId -> [Public.UserProfile] -> (Handler r) [Public.UserProfile] filterHandleResults searchingUser us = do - sameTeamSearchOnly <- fromMaybe False <$> view (settings . searchSameTeamOnly) + sameTeamSearchOnly <- fromMaybe False <$> asks (.settings.searchSameTeamOnly) if sameTeamSearchOnly then do fromTeam <- lift . wrapClient $ Data.lookupUserTeam (tUnqualified searchingUser) diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 5511a7750b2..7ef94976f13 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -47,7 +47,6 @@ import Brig.User.Auth.Cookie import Brig.ZAuth qualified as ZAuth import Cassandra import Control.Error hiding (bool) -import Control.Lens (to, view) import Data.ByteString.Conversion (toByteString) import Data.Code qualified as Code import Data.Default @@ -172,7 +171,7 @@ withRetryLimit :: UserId -> ExceptT LoginError m () withRetryLimit action uid = do - mLimitFailedLogins <- view (settings . to Opt.setLimitFailedLogins) + mLimitFailedLogins <- asks (.settings.limitFailedLogins) forM_ mLimitFailedLogins $ \opts -> do let bkey = BudgetKey ("login#" <> idToText uid) budget = diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index f9f621ae4bb..081d23c1d38 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -45,7 +45,7 @@ import Brig.User.Auth.Cookie.Limit import Brig.ZAuth qualified as ZAuth import Cassandra import Control.Error -import Control.Lens (to, view) +import Control.Lens (view) import Control.Monad.Except import Data.ByteString.Conversion import Data.Id @@ -77,7 +77,7 @@ newCookie :: Maybe CookieLabel -> m (Cookie (ZAuth.Token u)) newCookie uid cid typ label = do - now <- liftIO =<< view currentTime + now <- liftIO =<< asks (.currentTime) tok <- if typ == PersistentCookie then ZAuth.newUserToken uid cid @@ -116,10 +116,10 @@ nextCookie c mNewCid = runMaybeT $ do -- Keep old client ID by default, but use new one if none was set. let mcid = mOldCid <|> mNewCid - s <- view settings - now <- liftIO =<< view currentTime + s <- asks (.settings) + now <- liftIO =<< asks (.currentTime) let created = cookieCreated c - let renewAge = fromInteger (setUserCookieRenewAge s) + let renewAge = fromInteger s.userCookieRenewAge -- Renew the cookie if the client ID has changed, regardless of age. -- FUTUREWORK: Also renew the cookie if it was signed with a different zauth -- key index, regardless of age. @@ -158,7 +158,7 @@ renewCookie old mcid = do -- around only for another renewal period so as not to build -- an ever growing chain of superseded cookies. let old' = old {cookieSucc = Just (cookieId new)} - ttl <- setUserCookieRenewAge <$> view settings + ttl <- asks (.settings.userCookieRenewAge) adhocSessionStoreInterpreter $ Store.insertCookie uid (toUnitCookie old') (Just (Store.TTL (fromIntegral ttl))) pure new @@ -168,10 +168,10 @@ renewCookie old mcid = do -- implicitly because of cyclical dependencies). mustSuspendInactiveUser :: (MonadReader Env m, MonadClient m) => UserId -> m Bool mustSuspendInactiveUser uid = - view (settings . to setSuspendInactiveUsers) >>= \case + asks (.settings.suspendInactiveUsers) >>= \case Nothing -> pure False Just (SuspendInactiveUsers (Timeout suspendAge)) -> do - now <- liftIO =<< view currentTime + now <- liftIO =<< asks (.currentTime) let suspendHere :: UTCTime suspendHere = addUTCTime (-suspendAge) now youngEnough :: Cookie () -> Bool @@ -193,7 +193,7 @@ newAccessToken c mt = do t' <- case mt of Nothing -> ZAuth.newAccessToken (cookieValue c) Just t -> ZAuth.renewAccessToken (ZAuth.userTokenClient (cookieValue c)) t - zSettings <- view (zauthEnv . ZAuth.settings) + zSettings <- asks ((.zauthEnv) <&> view ZAuth.settings) let ttl = view (ZAuth.settingsTTL (Proxy @a)) zSettings pure $ bearerToken @@ -247,9 +247,9 @@ newCookieLimited :: m (Either RetryAfter (Cookie (ZAuth.Token t))) newCookieLimited u c typ label = do cs <- filter ((typ ==) . cookieType) <$> adhocSessionStoreInterpreter (Store.listCookies u) - now <- liftIO =<< view currentTime - lim <- CookieLimit . setUserCookieLimit <$> view settings - thr <- setUserCookieThrottle <$> view settings + now <- liftIO =<< asks (.currentTime) + lim <- CookieLimit <$> asks (.settings.userCookieLimit) + thr <- asks (.settings.userCookieThrottle) let evict = map cookieId (limitCookies lim now cs) if null evict then Right <$> newCookie u c typ label @@ -264,7 +264,7 @@ newCookieLimited u c typ label = do toWebCookie :: (MonadReader Env m, ZAuth.UserTokenLike u) => Cookie (ZAuth.Token u) -> m WebCookie.SetCookie toWebCookie c = do - s <- view settings + s <- asks (.settings) pure $ WebCookie.def { WebCookie.setCookieName = "zuid", @@ -274,7 +274,7 @@ toWebCookie c = do if cookieType c == PersistentCookie then Just (cookieExpires c) else Nothing, - WebCookie.setCookieSecure = not (setCookieInsecure s), + WebCookie.setCookieSecure = not s.cookieInsecure, WebCookie.setCookieHttpOnly = True } diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index 23d4095270f..9b54d5bb20d 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -125,7 +125,7 @@ ejpdRequest (fromMaybe False -> includeContacts) (EJPDRequestBody handles) = do mbAssets <- do urls <- forM (userAssets target) $ \(asset :: Asset) -> do - cgh <- asks (view cargoholdEndpoint) + cgh <- asks (.cargoholdEndpoint) let key = toByteString' $ assetKey asset resp <- liftSem $ rpcWithRetries "cargohold" cgh (method GET . paths ["/i/assets", key]) pure $ diff --git a/services/brig/src/Brig/Version.hs b/services/brig/src/Brig/Version.hs index 9d16efedd7d..86f3a33b937 100644 --- a/services/brig/src/Brig/Version.hs +++ b/services/brig/src/Brig/Version.hs @@ -19,7 +19,6 @@ module Brig.Version where import Brig.API.Handler import Brig.App -import Control.Lens import Data.Set qualified as Set import Imports import Servant (ServerT) @@ -28,9 +27,9 @@ import Wire.API.Routes.Version versionAPI :: ServerT VersionAPI (Handler r) versionAPI = Named $ do - fed <- view federator + fed <- asks (.federator) dom <- viewFederationDomain - disabled <- view disabledVersions + disabled <- asks (.disabledVersions) let allVersions = Set.difference (Set.fromList supportedVersions) disabled devVersions = Set.difference (Set.fromList developmentVersions) disabled supported = Set.difference allVersions devVersions diff --git a/services/brig/test/integration/API/Calling.hs b/services/brig/test/integration/API/Calling.hs index 442dcfca55b..7867a5afe9f 100644 --- a/services/brig/test/integration/API/Calling.hs +++ b/services/brig/test/integration/API/Calling.hs @@ -116,7 +116,7 @@ testSFT b opts = do testSFTUnavailable :: Brig -> Opts.Opts -> String -> Http () testSFTUnavailable b opts domain = do uid <- userId <$> randomUser b - withSettingsOverrides (opts {Opts.optSettings = (Opts.optSettings opts) {Opts.setSftStaticUrl = fromByteString (cs domain), Opts.setSftListAllServers = Just Opts.ListAllSFTServers}}) $ do + withSettingsOverrides (opts {Opts.optSettings = (Opts.optSettings opts) {Opts.sftStaticUrl = fromByteString (cs domain), Opts.sftListAllServers = Just Opts.ListAllSFTServers}}) $ do cfg <- getTurnConfigurationV2 uid b liftIO $ do assertEqual @@ -178,7 +178,7 @@ testCallsConfigSRV b opts = do uid <- userId <$> randomUser b let dnsOpts = Opts.TurnSourceDNS (Opts.TurnDnsOpts "integration-tests.zinfra.io" (Just 0.5)) config <- - withSettingsOverrides (opts & Opts.turnL . Opts.serversSourceL .~ dnsOpts) $ + withSettingsOverrides (opts & Opts.turnL . Opts.serversSourceLens .~ dnsOpts) $ responseJsonError =<< ( retryWhileN 10 (\r -> statusCode r /= 200) (getTurnConfiguration "" uid b) randomUser b let dnsOpts = Opts.TurnSourceDNS (Opts.TurnDnsOpts "integration-tests.zinfra.io" (Just 0.5)) config <- - withSettingsOverrides (opts & Opts.turnL . Opts.serversSourceL .~ dnsOpts) $ + withSettingsOverrides (opts & Opts.turnL . Opts.serversSourceLens .~ dnsOpts) $ responseJsonError =<< ( retryWhileN 10 (\r -> statusCode r /= 200) (getTurnConfiguration "v2" uid b) Opt.Opts -> Opt.Opts allowFullSearch domain opts = - opts & Opt.optionSettings . Opt.federationDomainConfigs ?~ [Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domain FullSearch FederationRestrictionAllowAll] + opts & Opt.optionSettings . Opt.federationDomainConfigsLens ?~ [Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domain FullSearch FederationRestrictionAllowAll] testSearchSuccess :: Opt.Opts -> Brig -> Http () testSearchSuccess opts brig = do @@ -176,7 +176,7 @@ testSearchRestrictions opts brig = do let opts' = opts - & Opt.optionSettings . Opt.federationDomainConfigs + & Opt.optionSettings . Opt.federationDomainConfigsLens ?~ [ Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainNoSearch NoSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainExactHandle ExactHandleSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainFullSearch FullSearch FederationRestrictionAllowAll @@ -220,7 +220,7 @@ testGetUserByHandleRestrictions opts brig = do let opts' = opts - & Opt.optionSettings . Opt.federationDomainConfigs + & Opt.optionSettings . Opt.federationDomainConfigsLens ?~ [ Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainNoSearch NoSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainExactHandle ExactHandleSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainFullSearch FullSearch FederationRestrictionAllowAll diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index c4f2311f577..4b5219208f2 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -192,10 +192,10 @@ testCreateAccessTokenSuccess opts brig = do createOAuthAccessToken' brig accessTokenRequest !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe - k <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + k <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") verifiedOrError <- liftIO $ verify k (unOAuthToken $ resp.accessToken) verifiedOrErrorWithotherKey <- liftIO $ verify badKey (unOAuthToken $ resp.accessToken) - let expectedDomain = domainText $ Opt.setFederationDomain $ Opt.optSettings opts + let expectedDomain = domainText opts.optSettings.federationDomain liftIO $ do isRight verifiedOrError @?= True isLeft verifiedOrErrorWithotherKey @?= True @@ -247,7 +247,7 @@ testCreateAccessTokenWrongUrl brig = do testCreateAccessTokenExpiredCode :: Opt.Opts -> Brig -> Http () testCreateAccessTokenExpiredCode opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthAuthorizationCodeExpirationTimeSecsInternal ?~ 1) $ do + withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthAuthorizationCodeExpirationTimeSecsInternalLens ?~ 1) $ do uid <- randomId let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] @@ -297,14 +297,14 @@ testCreateAccessTokenWrongCodeVerifier brig = do testGetOAuthClientInfoAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testGetOAuthClientInfoAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do + withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthEnabledInternalLens ?~ False) $ do cid <- randomId uid <- randomId getOAuthClientInfo' brig uid cid !!! assertAccessDenied testCreateCodeOAuthClientAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testCreateCodeOAuthClientAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do + withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthEnabledInternalLens ?~ False) $ do cid <- randomId uid <- randomId state <- UUID.toText <$> liftIO nextRandom @@ -318,7 +318,7 @@ testCreateCodeOAuthClientAccessDeniedWhenDisabled opts brig = testCreateAccessTokenAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testCreateAccessTokenAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do + withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthEnabledInternalLens ?~ False) $ do cid <- randomId let code = OAuthAuthorizationCode $ encodeBase16 "eb32eb9e2aa36c081c89067dddf81bce83c1c57e0b74cfb14c9f026f145f2b1f" let url = mkUrl "https://example.com" @@ -333,13 +333,13 @@ testRefreshAccessTokenAccessDeniedWhenDisabled opts brig = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do + withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthEnabledInternalLens ?~ False) $ do let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid resp.refreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! assertAccessDenied testRegisterOAuthClientAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testRegisterOAuthClientAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthEnabledInternal ?~ False) $ do + withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthEnabledInternalLens ?~ False) $ do let newOAuthClient = newOAuthClientRequestBody "E Corp" "https://example.com" registerNewOAuthClient' brig newOAuthClient !!! assertAccessDenied @@ -412,7 +412,7 @@ testAccessResourceInvalidSignature opts brig nginz = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") claimSet <- fromRight (error "token invalid") <$> liftIO (verify key (unOAuthToken $ resp.accessToken)) tokenSignedWithotherKey <- signAccessToken badKey claimSet get (nginz . paths ["self"] . authHeader (OAuthToken tokenSignedWithotherKey)) !!! do @@ -421,9 +421,9 @@ testAccessResourceInvalidSignature opts brig nginz = do testRefreshTokenMaxActiveTokens :: Opts -> C.ClientState -> Brig -> Http () testRefreshTokenMaxActiveTokens opts db brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthMaxActiveRefreshTokensInternal ?~ 2) $ do + withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthMaxActiveRefreshTokensInternalLens ?~ 2) $ do uid <- randomId - jwk <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + jwk <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] let delayOneSec = @@ -501,7 +501,7 @@ testRefreshTokenWrongSignature opts brig = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ do claims <- verifyRefreshToken key (unOAuthToken $ resp.refreshToken) OAuthToken <$> signRefreshToken badKey claims @@ -516,7 +516,7 @@ testRefreshTokenNoTokenId opts brig = do let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, _) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ OAuthToken <$> signRefreshToken key emptyClaimsSet let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid badRefreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! do @@ -531,7 +531,7 @@ testRefreshTokenNonExistingId opts brig = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ OAuthToken <$> do @@ -575,7 +575,7 @@ testRefreshTokenWrongGrantType brig = do testRefreshTokenExpiredToken :: Opts -> Brig -> Http () testRefreshTokenExpiredToken opts brig = -- overriding settings and set refresh token to expire in 2 seconds - withSettingsOverrides (opts & Opt.optionSettings . Opt.oauthRefreshTokenExpirationTimeSecsInternal ?~ 2) $ do + withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthRefreshTokenExpirationTimeSecsInternalLens ?~ 2) $ do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] diff --git a/services/brig/test/integration/API/Search.hs b/services/brig/test/integration/API/Search.hs index 54111e832f5..7a6bbc64c46 100644 --- a/services/brig/test/integration/API/Search.hs +++ b/services/brig/test/integration/API/Search.hs @@ -520,7 +520,7 @@ testSearchSameTeamOnly brig opts = do nonTeamMember <- setRandomHandle brig nonTeamMember' (_, _, [teamMember]) <- createPopulatedBindingTeam brig 1 refreshIndex brig - let newOpts = opts & Opt.optionSettings . Opt.searchSameTeamOnly ?~ True + let newOpts = opts & Opt.optionSettings . Opt.searchSameTeamOnlyLens ?~ True withSettingsOverrides newOpts $ do assertCan'tFind brig (userId teamMember) (userQualifiedId nonTeamMember) (fromName (userDisplayName nonTeamMember)) let nonTeamMemberHandle = fromMaybe (error "nonTeamMember must have a handle") (userHandle nonTeamMember) @@ -613,8 +613,8 @@ testMigrationToNewIndex opts brig = do withOldESProxy opts $ \oldESUrl oldESIndex -> do let optsOldIndex = opts - & Opt.elasticsearchL . Opt.indexL .~ (ES.IndexName oldESIndex) - & Opt.elasticsearchL . Opt.urlL .~ (ES.Server oldESUrl) + & Opt.elasticsearchL . Opt.indexLens .~ (ES.IndexName oldESIndex) + & Opt.elasticsearchL . Opt.urlLens .~ (ES.Server oldESUrl) -- Phase 1: Using old index only (phase1NonTeamUser, teamOwner, phase1TeamUser1, phase1TeamUser2, tid) <- withSettingsOverrides optsOldIndex $ do nonTeamUser <- randomUser brig @@ -624,10 +624,10 @@ testMigrationToNewIndex opts brig = do -- Phase 2: Using old index for search, writing to both indices, migrations have not run let phase2OptsWhile = optsOldIndex - & Opt.elasticsearchL . Opt.additionalWriteIndexL ?~ (opts ^. Opt.elasticsearchL . Opt.indexL) - & Opt.elasticsearchL . Opt.additionalWriteIndexUrlL ?~ (opts ^. Opt.elasticsearchL . Opt.urlL) - & Opt.elasticsearchL . Opt.additionalCaCertL .~ (opts ^. Opt.elasticsearchL . Opt.caCertL) - & Opt.elasticsearchL . Opt.additionalInsecureSkipVerifyTlsL .~ (opts ^. Opt.elasticsearchL . Opt.insecureSkipVerifyTlsL) + & Opt.elasticsearchL . Opt.additionalWriteIndexLens ?~ (opts ^. Opt.elasticsearchL . Opt.indexLens) + & Opt.elasticsearchL . Opt.additionalWriteIndexUrlLens ?~ (opts ^. Opt.elasticsearchL . Opt.urlLens) + & Opt.elasticsearchL . Opt.additionalCaCertLens .~ (opts ^. Opt.elasticsearchL . Opt.caCertLens) + & Opt.elasticsearchL . Opt.additionalInsecureSkipVerifyTlsLens .~ (opts ^. Opt.elasticsearchL . Opt.insecureSkipVerifyTlsLens) (phase2NonTeamUser, phase2TeamUser) <- withSettingsOverrides phase2OptsWhile $ do phase2NonTeamUser <- randomUser brig phase2TeamUser <- inviteAndRegisterUser teamOwner tid brig @@ -652,7 +652,7 @@ testMigrationToNewIndex opts brig = do assertCanFindByName brig phase1TeamUser1 phase2TeamUser -- Run Migrations - let newIndexName = opts ^. Opt.elasticsearchL . Opt.indexL + let newIndexName = opts ^. Opt.elasticsearchL . Opt.indexLens taskNodeId <- assertRight =<< runBH opts (ES.reindexAsync $ ES.mkReindexRequest (ES.IndexName oldESIndex) newIndexName) runBH opts $ waitForTaskToComplete @ES.ReindexResponse taskNodeId @@ -746,14 +746,14 @@ withOldIndex :: (MonadIO m, HasCallStack) => Opt.Opts -> WaiTest.Session a -> m withOldIndex opts f = do indexName <- randomHandle createIndexWithMapping opts indexName oldMapping - let newOpts = opts & Opt.elasticsearchL . Opt.indexL .~ (ES.IndexName indexName) + let newOpts = opts & Opt.elasticsearchL . Opt.indexLens .~ (ES.IndexName indexName) withSettingsOverrides newOpts f <* deleteIndex opts indexName optsForOldIndex :: (MonadIO m, HasCallStack) => Opt.Opts -> m (Opt.Opts, Text) optsForOldIndex opts = do indexName <- randomHandle createIndexWithMapping opts indexName oldMapping - pure (opts & Opt.elasticsearchL . Opt.indexL .~ (ES.IndexName indexName), indexName) + pure (opts & Opt.elasticsearchL . Opt.indexLens .~ (ES.IndexName indexName), indexName) createIndexWithMapping :: (MonadIO m, HasCallStack) => Opt.Opts -> Text -> Value -> m () createIndexWithMapping opts name val = do @@ -773,7 +773,7 @@ deleteIndex opts name = do runBH :: (MonadIO m, HasCallStack) => Opt.Opts -> ES.BH m a -> m a runBH opts action = do - let (ES.Server esURL) = opts ^. Opt.elasticsearchL . Opt.urlL + let (ES.Server esURL) = opts ^. Opt.elasticsearchL . Opt.urlLens mgr <- liftIO $ initHttpManagerWithTLSConfig opts.elasticsearch.insecureSkipVerifyTls opts.elasticsearch.caCert let bEnv = mkBHEnv esURL mgr ES.runBH bEnv action diff --git a/services/brig/test/integration/API/Settings.hs b/services/brig/test/integration/API/Settings.hs index c6d2278fb99..67b097a9401 100644 --- a/services/brig/test/integration/API/Settings.hs +++ b/services/brig/test/integration/API/Settings.hs @@ -126,7 +126,7 @@ testUsersEmailVisibleIffExpected opts brig galley viewingUserIs visibilitySettin else Nothing ) ] - let newOpts = opts & Opt.optionSettings . Opt.emailVisibility .~ visibilitySetting + let newOpts = opts & Opt.optionSettings . Opt.emailVisibilityLens .~ visibilitySetting withSettingsOverrides newOpts $ do get (apiVersion "v1" . brig . zUser viewerId . path "users" . queryItem "ids" uids) !!! do const 200 === statusCode @@ -155,7 +155,7 @@ testGetUserEmailShowsEmailsIffExpected opts brig galley viewingUserIs visibility else Nothing ) ] - let newOpts = opts & Opt.optionSettings . Opt.emailVisibility .~ visibilitySetting + let newOpts = opts & Opt.optionSettings . Opt.emailVisibilityLens .~ visibilitySetting withSettingsOverrides newOpts $ do forM_ expectations $ \(uid, expectedEmail) -> get (apiVersion "v1" . brig . zUser viewerId . paths ["users", toByteString' uid]) !!! do diff --git a/services/brig/test/integration/API/SystemSettings.hs b/services/brig/test/integration/API/SystemSettings.hs index 40b20c0606f..b2518c1fcd9 100644 --- a/services/brig/test/integration/API/SystemSettings.hs +++ b/services/brig/test/integration/API/SystemSettings.hs @@ -49,7 +49,7 @@ testGetSettings opts = liftIO $ do where expectResultForSetting :: Maybe Bool -> Bool -> IO () expectResultForSetting restrictUserCreationSetting expectedRes = do - let newOpts = opts & (optionSettings . restrictUserCreation) .~ restrictUserCreationSetting + let newOpts = opts & (optionSettings . restrictUserCreationLens) .~ restrictUserCreationSetting -- Run call in `WaiTest.Session` with an adjusted brig `Application`. I.e. -- the response is created by running the brig `Application` (with -- modified options) directly on the `Request`. No real HTTP request is @@ -73,7 +73,7 @@ testGetSettingsInternal opts = liftIO $ do where expectResultForEnableMls :: UserId -> Maybe Bool -> Bool -> IO () expectResultForEnableMls uid setEnableMlsValue expectedRes = do - let newOpts = opts & (optionSettings . enableMLS) .~ setEnableMlsValue + let newOpts = opts & (optionSettings . enableMLSLens) .~ setEnableMlsValue -- Run call in `WaiTest.Session` with an adjusted brig `Application`. I.e. -- the response is created by running the brig `Application` (with -- modified options) directly on the `Request`. No real HTTP request is diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 5cc29c6f86e..8443ec6cb35 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -81,8 +81,8 @@ newtype TeamSizeLimit = TeamSizeLimit Word32 tests :: Opt.Opts -> Manager -> Nginz -> Brig -> Cannon -> Galley -> UserJournalWatcher -> IO TestTree tests conf m n b c g aws = do - let tl = TeamSizeLimit . Opt.setMaxTeamSize . Opt.optSettings $ conf - let it = Opt.setTeamInvitationTimeout . Opt.optSettings $ conf + let tl = TeamSizeLimit conf.optSettings.maxTeamSize + let it = conf.optSettings.teamInvitationTimeout pure $ testGroup "team" @@ -372,7 +372,7 @@ testInvitationTooManyPending opts brig (TeamSizeLimit limit) = do -- If this test takes longer to run than `team-invitation-timeout`, then some of the -- invitations have likely expired already and this test will actually _fail_ -- therefore we increase the timeout from default 10 to 300 seconds - let longerTimeout = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.setTeamInvitationTimeout = 300}} + let longerTimeout = opts {Opt.optSettings = opts.optSettings {Opt.teamInvitationTimeout = 300}} withSettingsOverrides longerTimeout $ do forM_ emails $ postInvitation brig tid inviter . stdInvitationRequest postInvitation brig tid inviter (stdInvitationRequest email) !!! do @@ -715,7 +715,7 @@ testInvitationPaging opts brig = do (uid, tid) <- createUserWithTeam brig let total = 5 invite email = stdInvitationRequest email - longerTimeout = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.setTeamInvitationTimeout = 300}} + longerTimeout = opts {Opt.optSettings = opts.optSettings {Opt.teamInvitationTimeout = 300}} emails <- withSettingsOverrides longerTimeout $ replicateM total $ do diff --git a/services/brig/test/integration/API/User.hs b/services/brig/test/integration/API/User.hs index 35cf4aef598..94417f50270 100644 --- a/services/brig/test/integration/API/User.hs +++ b/services/brig/test/integration/API/User.hs @@ -55,8 +55,8 @@ tests :: UserJournalWatcher -> IO TestTree tests conf fbc p b c ch g n aws db userJournalWatcher = do - let cl = ConnectionLimit $ Opt.setUserMaxConnections (Opt.optSettings conf) - let at = Opt.setActivationTimeout (Opt.optSettings conf) + let cl = ConnectionLimit conf.optSettings.userMaxConnections + let at = conf.optSettings.activationTimeout z <- mkZAuthEnv (Just conf) pure $ testGroup diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 4772091981c..550680aa096 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -201,7 +201,7 @@ testUpdateUserEmailByTeamOwner opts brig = do where checkLetActivationExpire :: EmailAddress -> Http () checkLetActivationExpire email = do - let timeout = round (Opt.setActivationTimeout (Opt.optSettings opts)) + let timeout = round opts.optSettings.activationTimeout threadDelay ((timeout + 1) * 1000_000) checkActivationCode email False @@ -239,7 +239,7 @@ testCreateUserWithPreverified opts brig userJournalWatcher = do "email" .= fromEmail e, "email_code" .= c ] - if Opt.setRestrictUserCreation (Opt.optSettings opts) == Just True + if opts.optSettings.restrictUserCreation == Just True then do postUserRegister' reg brig !!! const 403 === statusCode else do @@ -340,7 +340,7 @@ testCreateUserAnon brig galley = do Search.assertCan'tFind brig suid quid "Mr. Pink" testCreateUserPending :: Opt.Opts -> Brig -> Http () -testCreateUserPending (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () +testCreateUserPending (Opt.restrictUserCreation . Opt.optSettings -> Just True) _ = pure () testCreateUserPending _ brig = do e <- randomEmail let p = @@ -379,7 +379,7 @@ testCreateUserPending _ brig = do -- -- email address must not be taken on @/register@. testCreateUserConflict :: Opt.Opts -> Brig -> Http () -testCreateUserConflict (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () +testCreateUserConflict (Opt.restrictUserCreation . Opt.optSettings -> Just True) _ = pure () testCreateUserConflict _ brig = do -- trusted email domains u <- createUser "conflict" brig @@ -416,7 +416,7 @@ testCreateUserConflict _ brig = do -- -- Test to make sure a new user cannot be created with an invalid email address testCreateUserInvalidEmail :: Opt.Opts -> Brig -> Http () -testCreateUserInvalidEmail (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ = pure () +testCreateUserInvalidEmail (Opt.restrictUserCreation . Opt.optSettings -> Just True) _ = pure () testCreateUserInvalidEmail _ brig = do let reqPhone = RequestBodyLBS . encode $ @@ -431,7 +431,7 @@ testCreateUserInvalidEmail _ brig = do -- @END testCreateUserBlacklist :: Opt.Opts -> Brig -> AWS.Env -> Http () -testCreateUserBlacklist (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ _ = pure () +testCreateUserBlacklist (Opt.restrictUserCreation . Opt.optSettings -> Just True) _ _ = pure () testCreateUserBlacklist _ brig aws = mapM_ ensureBlacklist ["bounce", "complaint"] where @@ -490,7 +490,7 @@ testCreateUserExternalSSO brig = do !!! const 400 === statusCode testActivateWithExpiry :: Opt.Opts -> Brig -> Timeout -> Http () -testActivateWithExpiry (Opt.setRestrictUserCreation . Opt.optSettings -> Just True) _ _ = pure () +testActivateWithExpiry (Opt.restrictUserCreation . Opt.optSettings -> Just True) _ _ = pure () testActivateWithExpiry _ brig timeout = do u <- responseJsonError =<< registerUser "dilbert" brig let email = fromMaybe (error "missing email") (userEmail u) @@ -1091,7 +1091,7 @@ testSendActivationCode opts brig = do -- Code for email pre-verification requestActivationCode brig 200 . Left =<< randomEmail -- Standard email registration flow - if Opt.setRestrictUserCreation (Opt.optSettings opts) == Just True + if opts.optSettings.restrictUserCreation == Just True then do registerUser "Alice" brig !!! const 403 === statusCode else do @@ -1292,7 +1292,7 @@ testRestrictedUserCreation opts brig = do -- We create a team before to help in other tests (teamOwner, createdTeam) <- createUserWithTeam brig - let opts' = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.setRestrictUserCreation = Just True}} + let opts' = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.restrictUserCreation = Just True}} withSettingsOverrides opts' $ do e <- randomEmail diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 79b6ac62bf8..6e77a0b1ba0 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -409,7 +409,7 @@ testThrottleLogins :: Opts.Opts -> Brig -> Http () testThrottleLogins conf b = do -- Get the maximum amount of times we are allowed to login before -- throttling begins - let l = Opts.setUserCookieLimit (Opts.optSettings conf) + let l = Opts.userCookieLimit (Opts.optSettings conf) u <- randomUser b let Just e = userEmail u -- Login exactly that amount of times, as fast as possible @@ -441,7 +441,7 @@ testThrottleLogins conf b = do -- the aforementioned user. testLimitRetries :: (HasCallStack) => Opts.Opts -> Brig -> Http () testLimitRetries conf brig = do - let Just opts = Opts.setLimitFailedLogins . Opts.optSettings $ conf + let Just opts = conf.optSettings.limitFailedLogins unless (Opts.timeout opts <= 30) $ error "`loginRetryTimeout` is the number of seconds this test is running. Please pick a value < 30." usr <- randomUser brig @@ -770,7 +770,7 @@ testNewPersistentCookie config b = getAndTestDBSupersededCookieAndItsValidSuccessor :: Opts.Opts -> Brig -> Nginz -> Http (Http.Cookie, Http.Cookie) getAndTestDBSupersededCookieAndItsValidSuccessor config b n = do u <- randomUser b - let renewAge = Opts.setUserCookieRenewAge $ Opts.optSettings config + let renewAge = config.optSettings.userCookieRenewAge let minAge = fromIntegral $ (renewAge + 1) * 1000000 Just email = userEmail u _rs <- @@ -1017,7 +1017,7 @@ testAccessWithExistingClientId brig = do testNewSessionCookie :: Opts.Opts -> Brig -> Http () testNewSessionCookie config b = do u <- randomUser b - let renewAge = Opts.setUserCookieRenewAge $ Opts.optSettings config + let renewAge = config.optSettings.userCookieRenewAge let minAge = fromIntegral $ renewAge * 1000000 + 1 Just email = userEmail u _rs <- @@ -1035,7 +1035,7 @@ testSuspendInactiveUsers config brig cookieType endPoint = do -- (context information: cookies are stored by user, not by device; so if there is a -- cookie that is old, it means none of the devices of the user has used it for a request.) - let Just suspendAge = Opts.suspendTimeout <$> Opts.setSuspendInactiveUsers (Opts.optSettings config) + let Just suspendAge = Opts.suspendTimeout <$> config.optSettings.suspendInactiveUsers unless (suspendAge <= 30) $ error "`suspendCookiesOlderThanSecs` is the number of seconds this test is running. Please pick a value < 30." @@ -1155,7 +1155,7 @@ testRemoveCookiesByLabelAndId b = do testTooManyCookies :: Opts.Opts -> Brig -> Http () testTooManyCookies config b = do u <- randomUser b - let l = Opts.setUserCookieLimit (Opts.optSettings config) + let l = config.optSettings.userCookieLimit let Just e = userEmail u carry = 2 pwlP = emailLogin e defPassword (Just "persistent") diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index fb6bf3fc06d..c22fd3a613b 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -30,8 +30,7 @@ import API.User.Util import API.User.Util qualified as Util import Bilge hiding (accept, head, timeout) import Bilge.Assert -import Brig.Options qualified as Opt -import Brig.Options qualified as Opts +import Brig.Options as Opt import Cassandra qualified as DB import Control.Lens hiding (Wrapped, (#)) import Crypto.JWT hiding (Ed25519, header, params) @@ -219,8 +218,8 @@ testAddGetClientCodeExpired db opts brig galley = do codeValue <- (.codeValue) <$$> lookupCode db k Code.AccountLogin checkLoginSucceeds $ MkLogin (LoginByEmail email) defPassword (Just defCookieLabel) codeValue - let verificationTimeout = round (Opt.setVerificationTimeout (Opt.optSettings opts)) - threadDelay $ ((verificationTimeout + 1) * 1000_000) + let timeout = round (verificationTimeout opts.optSettings) + threadDelay $ ((timeout + 1) * 1000_000) addClient' codeValue !!! do const 403 === statusCode const (Just "code-authentication-failed") === fmap Error.label . responseJsonMaybe @@ -292,7 +291,7 @@ testGetUserClientsQualified opts brig = do _c11 :: Client <- responseJsonError =<< addClient brig uid1 (defNewClient PermanentClientType [pk11] lk11) _c12 :: Client <- responseJsonError =<< addClient brig uid1 (defNewClient PermanentClientType [pk12] lk12) _c13 :: Client <- responseJsonError =<< addClient brig uid1 (defNewClient TemporaryClientType [pk13] lk13) - let localdomain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let localdomain = opts.optSettings.federationDomain getUserClientsQualified brig uid2 localdomain uid1 !!! do const 200 === statusCode assertTrue_ $ \res -> do @@ -397,7 +396,7 @@ testListClientsBulk opts brig = do c21 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk21] lk21) c22 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk22] lk22) - let domain = Opt.setFederationDomain $ Opt.optSettings opts + let domain = opts.optSettings.federationDomain uid3 <- userId <$> randomUser brig let mkPubClient cl = PubClient (clientId cl) (clientClass cl) let expectedResponse :: QualifiedUserMap (Set PubClient) = @@ -440,7 +439,7 @@ testClientsWithoutPrekeys brig cannon db opts = do uid2 <- userId <$> randomUser brig - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.optSettings.federationDomain let userClients = QualifiedUserClients $ @@ -532,7 +531,7 @@ testClientsWithoutPrekeysV4 brig cannon db opts = do uid2 <- userId <$> randomUser brig - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.optSettings.federationDomain let userClients = QualifiedUserClients $ @@ -627,7 +626,7 @@ testClientsWithoutPrekeysFailToListV4 brig cannon db opts = do uid2 <- fakeRemoteUser - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.optSettings.federationDomain let userClients1 = QualifiedUserClients $ @@ -711,7 +710,7 @@ testListClientsBulkV2 opts brig = do c21 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk21] lk21) c22 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk22] lk22) - let domain = Opt.setFederationDomain $ Opt.optSettings opts + let domain = opts.optSettings.federationDomain uid3 <- userId <$> randomUser brig let mkPubClient cl = PubClient (clientId cl) (clientClass cl) let expectedResponse :: WrappedQualifiedUserMap (Set PubClient) = @@ -775,7 +774,7 @@ testGetUserPrekeys brig = do testGetUserPrekeysQualified :: Brig -> Opt.Opts -> Http () testGetUserPrekeysQualified brig opts = do - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.optSettings.federationDomain [(uid, _c, _lpk, cpk)] <- generateClients 1 brig get (brig . paths ["users", toByteString' domain, toByteString' uid, "prekeys"] . zUser uid) !!! do const 200 === statusCode @@ -796,7 +795,7 @@ testGetClientPrekey brig = do testGetClientPrekeyQualified :: Brig -> Opt.Opts -> Http () testGetClientPrekeyQualified brig opts = do - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.optSettings.federationDomain [(uid, c, _lpk, cpk)] <- generateClients 1 brig get (brig . paths ["users", toByteString' domain, toByteString' uid, "prekeys", toByteString' (clientId c)] . zUser uid) !!! do const 200 === statusCode @@ -833,7 +832,7 @@ testMultiUserGetPrekeys brig = do testMultiUserGetPrekeysQualified :: Brig -> Opt.Opts -> Http () testMultiUserGetPrekeysQualified brig opts = do - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.optSettings.federationDomain xs <- generateClients 3 brig let userClients = @@ -867,7 +866,7 @@ testMultiUserGetPrekeysQualified brig opts = do testMultiUserGetPrekeysQualifiedV4 :: Brig -> Opt.Opts -> Http () testMultiUserGetPrekeysQualifiedV4 brig opts = do - let domain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let domain = opts.optSettings.federationDomain xs <- generateClients 3 brig let userClients = @@ -914,7 +913,7 @@ testTooManyClients :: Opt.Opts -> Brig -> Http () testTooManyClients opts brig = do uid <- userId <$> randomUser brig -- We can always change the permanent client limit - let newOpts = opts & Opt.optionSettings . Opt.userMaxPermClients ?~ 1 + let newOpts = opts & optionSettings . userMaxPermClientsLens ?~ 1 withSettingsOverrides newOpts $ do -- There is only one temporary client, adding a new one -- replaces the previous one. @@ -1158,7 +1157,7 @@ testUpdateClient opts brig = do const Nothing === (preview (key "mls_public_keys") <=< responseJsonMaybe @Value) -- via `/users/:domain/:uid/clients/:client`, only `id` and `class` are visible: - let localdomain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let localdomain = opts.optSettings.federationDomain get (brig . paths ["users", toByteString' localdomain, toByteString' uid, "clients", toByteString' (clientId c)]) !!! do const 200 === statusCode const (Just $ clientId c) === (fmap pubClientId . responseJsonMaybe) @@ -1431,9 +1430,9 @@ instance A.ToJSON DPoPClaimsSet where ins k v (Object o) = Object $ M.insert k (A.toJSON v) o ins _ _ a = a -testCreateAccessToken :: Opts.Opts -> Nginz -> Brig -> Http () +testCreateAccessToken :: Opt.Opts -> Nginz -> Brig -> Http () testCreateAccessToken opts n brig = do - let localDomain = opts ^. Opt.optionSettings & Opt.setFederationDomain + let localDomain = opts.optSettings.federationDomain (u, tid) <- Util.createUserWithTeam' brig handle <- do Just h <- userHandle <$> Util.setRandomHandle brig u diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index d94f3fbe00f..6a451347193 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -197,7 +197,7 @@ testHandleQuery opts brig = do -- Usually, you can search outside your team assertCanFind brig user3 user4 -- Usually, you can search outside your team but not if this config option is set - let newOpts = opts & ((Opt.optionSettings . Opt.searchSameTeamOnly) ?~ True) + let newOpts = opts & ((Opt.optionSettings . Opt.searchSameTeamOnlyLens) ?~ True) withSettingsOverrides newOpts $ assertCannotFind brig user3 user4 diff --git a/services/brig/test/integration/API/User/RichInfo.hs b/services/brig/test/integration/API/User/RichInfo.hs index 2ce2855a1cc..7e20a6ab807 100644 --- a/services/brig/test/integration/API/User/RichInfo.hs +++ b/services/brig/test/integration/API/User/RichInfo.hs @@ -117,7 +117,7 @@ testDedupeDuplicateFieldNames brig = do testRichInfoSizeLimit :: (HasCallStack) => Brig -> Opt.Opts -> Http () testRichInfoSizeLimit brig conf = do - let maxSize :: Int = setRichInfoLimit $ optSettings conf + let maxSize :: Int = conf.optSettings.richInfoLimit (owner, _) <- createUserWithTeam brig let bad1 = mkRichInfoAssocList diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs index da5c80d12cb..a85fa616ed8 100644 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ b/services/brig/test/integration/API/UserPendingActivation.hs @@ -24,7 +24,7 @@ module API.UserPendingActivation where import API.Team.Util (getTeam) import Bilge hiding (query) import Bilge.Assert (( Opts -> m () waitUserExpiration opts' = do - let timeoutSecs = round @Double . realToFrac . setTeamInvitationTimeout . optSettings $ opts' + let timeoutSecs = round @Double . realToFrac $ opts'.optSettings.teamInvitationTimeout Control.Exception.assert (timeoutSecs < 30) $ do threadDelay $ (timeoutSecs + 3) * 1_000_000 diff --git a/services/brig/test/integration/Index/Create.hs b/services/brig/test/integration/Index/Create.hs index 51961e9533d..fab0ae91860 100644 --- a/services/brig/test/integration/Index/Create.hs +++ b/services/brig/test/integration/Index/Create.hs @@ -24,7 +24,7 @@ import Brig.Index.Options import Brig.Index.Options qualified as IndexOpts import Brig.Options (Opts (galley)) import Brig.Options qualified as BrigOpts -import Control.Lens ((.~), (^.)) +import Control.Lens ((.~)) import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Database.Bloodhound qualified as ES @@ -49,7 +49,7 @@ spec brigOpts = testCreateIndexWhenNotPresent :: BrigOpts.Opts -> Assertion testCreateIndexWhenNotPresent brigOpts = do - let (ES.Server esURL) = brigOpts ^. BrigOpts.elasticsearchL . BrigOpts.urlL + let (ES.Server esURL) = brigOpts.elasticsearch.url case parseURI strictURIParserOptions (Text.encodeUtf8 esURL) of Left e -> fail $ "Invalid ES URL: " <> show esURL <> "\nerror: " <> show e Right esURI -> do @@ -90,7 +90,7 @@ testCreateIndexWhenNotPresent brigOpts = do testCreateIndexWhenPresent :: BrigOpts.Opts -> Assertion testCreateIndexWhenPresent brigOpts = do - let (ES.Server esURL) = brigOpts ^. BrigOpts.elasticsearchL . BrigOpts.urlL + let (ES.Server esURL) = brigOpts.elasticsearch.url case parseURI strictURIParserOptions (Text.encodeUtf8 esURL) of Left e -> fail $ "Invalid ES URL: " <> show esURL <> "\nerror: " <> show e Right esURI -> do diff --git a/services/brig/test/integration/Run.hs b/services/brig/test/integration/Run.hs index f804d364896..3ec5652dbff 100644 --- a/services/brig/test/integration/Run.hs +++ b/services/brig/test/integration/Run.hs @@ -124,7 +124,7 @@ runTests iConf brigOpts otherArgs = do let Opts.TurnServersFiles turnFile turnFileV2 = case Opts.serversSource $ Opts.turn brigOpts of Opts.TurnSourceFiles files -> files Opts.TurnSourceDNS _ -> error "The integration tests can only be run when TurnServers are sourced from files" - localDomain = brigOpts ^. Opts.optionSettings . Opts.federationDomain + localDomain = brigOpts.optSettings.federationDomain awsOpts = Opts.aws brigOpts lg <- Logger.new Logger.defSettings -- TODO: use mkLogger'? db <- defInitCassandra (brigOpts.cassandra) lg diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 18c676c15b3..bcb9ca7bd01 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -25,9 +25,9 @@ module Util where import Bilge hiding (host, port) import Bilge.Assert import Brig.AWS.Types -import Brig.App (applog, fsWatcher, sftEnv, turnEnv) +import Brig.App (Env (..)) import Brig.Calling as Calling -import Brig.Options qualified as Opt +import Brig.Options as Opt import Brig.Run qualified as Run import Brig.Types.Activation import Brig.ZAuth qualified as ZAuth @@ -992,9 +992,9 @@ withSettingsOverrides :: (MonadIO m, HasCallStack) => Opt.Opts -> WaiTest.Sessio withSettingsOverrides opts action = liftIO $ do (brigApp, env) <- Run.mkApp opts sftDiscovery <- - forM (env ^. sftEnv) $ \sftEnv' -> - Async.async $ Calling.startSFTServiceDiscovery (env ^. applog) sftEnv' - turnDiscovery <- Calling.startTurnDiscovery (env ^. applog) (env ^. fsWatcher) (env ^. turnEnv) + forM env.sftEnv $ \sftEnv' -> + Async.async $ Calling.startSFTServiceDiscovery env.appLogger sftEnv' + turnDiscovery <- Calling.startTurnDiscovery env.appLogger env.fsWatcher env.turnEnv res <- WaiTest.runSession action brigApp mapM_ Async.cancel sftDiscovery mapM_ Async.cancel turnDiscovery @@ -1004,7 +1004,7 @@ withSettingsOverrides opts action = liftIO $ do -- compile. withDomainsBlockedForRegistration :: (MonadIO m) => Opt.Opts -> [Text] -> WaiTest.Session a -> m a withDomainsBlockedForRegistration opts domains sess = do - let opts' = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.setCustomerExtensions = Just blocked}} + let opts' = opts {Opt.optSettings = opts.optSettings {customerExtensions = Just blocked}} blocked = Opt.CustomerExtensions (Opt.DomainsBlockedForRegistration (unsafeMkDomain <$> domains)) unsafeMkDomain = either error id . mkDomain withSettingsOverrides opts' sess From 264445cbc79f10b3c47f69ae3ec3d700af2fcfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 24 Sep 2024 11:15:30 +0200 Subject: [PATCH 086/136] [WPB-11163] Support for a consumable notifications capability (#4259) --------- Co-authored-by: Akshay Mankar Co-authored-by: Magnus Viernickel --- changelog.d/1-api-changes/WPB-11163 | 1 + ...WPB-11163-consume-notifications-capability | 1 + integration/test/API/Brig.hs | 9 +++ integration/test/Test/Client.hs | 51 +++++++++++++ libs/wire-api/src/Wire/API/User/Client.hs | 73 +++++++++++++------ .../API/Golden/Generated/NewClient_user.hs | 2 +- .../API/Golden/Generated/UpdateClient_user.hs | 2 +- .../golden/Test/Wire/API/Golden/Manual.hs | 13 +++- .../API/Golden/Manual/ClientCapability.hs | 3 + .../API/Golden/Manual/ClientCapabilityList.hs | 33 ++++++++- .../golden/Test/Wire/API/Golden/Runner.hs | 29 +++++--- .../testObject_ClientCapabilityList_3.json | 5 ++ .../testObject_ClientCapabilityList_4.json | 1 + .../testObject_ClientCapabilityList_5.json | 4 + .../golden/testObject_ClientCapability_2.json | 1 + services/brig/src/Brig/API/Client.hs | 8 +- services/brig/src/Brig/Data/Client.hs | 14 ++-- services/brig/src/Brig/Provider/API.hs | 2 +- .../brig/test/integration/API/User/Client.hs | 4 +- .../API/Teams/LegalHold/DisabledByDefault.hs | 6 +- services/galley/test/integration/API/Util.hs | 4 +- 21 files changed, 206 insertions(+), 60 deletions(-) create mode 100644 changelog.d/1-api-changes/WPB-11163 create mode 100644 changelog.d/2-features/WPB-11163-consume-notifications-capability create mode 100644 libs/wire-api/test/golden/testObject_ClientCapabilityList_3.json create mode 100644 libs/wire-api/test/golden/testObject_ClientCapabilityList_4.json create mode 100644 libs/wire-api/test/golden/testObject_ClientCapabilityList_5.json create mode 100644 libs/wire-api/test/golden/testObject_ClientCapability_2.json diff --git a/changelog.d/1-api-changes/WPB-11163 b/changelog.d/1-api-changes/WPB-11163 new file mode 100644 index 00000000000..df2ae5dbcc7 --- /dev/null +++ b/changelog.d/1-api-changes/WPB-11163 @@ -0,0 +1 @@ +The `POST /clients` and `PUT /clients/:cid` endpoints support a new capability "consume-notifications" diff --git a/changelog.d/2-features/WPB-11163-consume-notifications-capability b/changelog.d/2-features/WPB-11163-consume-notifications-capability new file mode 100644 index 00000000000..2300ebef73d --- /dev/null +++ b/changelog.d/2-features/WPB-11163-consume-notifications-capability @@ -0,0 +1 @@ +Clients can declare to be supporting a capability for consuming notifications diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index a82046379a3..39c1a4f48a8 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -136,6 +136,15 @@ getClient u cli = do joinHttpPath ["clients", c] submit "GET" req +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_clients +getSelfClients :: + (HasCallStack, MakesValue user) => + user -> + App Response +getSelfClients u = + baseRequest u Brig Versioned (joinHttpPath ["clients"]) + >>= submit "GET" + -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/delete_self deleteUser :: (HasCallStack, MakesValue user) => user -> App Response deleteUser user = do diff --git a/integration/test/Test/Client.hs b/integration/test/Test/Client.hs index d00e8174710..8c3101737dd 100644 --- a/integration/test/Test/Client.hs +++ b/integration/test/Test/Client.hs @@ -4,6 +4,7 @@ module Test.Client where import API.Brig import qualified API.Brig as API +import API.BrigCommon import API.Gundeck import Control.Lens hiding ((.=)) import Control.Monad.Codensity @@ -70,3 +71,53 @@ testListClientsIfBackendIsOffline = do bindResponse (listUsersClients ownUser1 [ownUser1, ownUser2, downUser]) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "qualified_user_map" `shouldMatch` expectedResponse + +testCreateClientWithCapabilities :: App () +testCreateClientWithCapabilities = do + let allCapabilities = ["legalhold-implicit-consent", "consumable-notifications"] + alice <- randomUser OwnDomain def + addClient alice def {acapabilities = Just allCapabilities} `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "capabilities" `shouldMatchSet` allCapabilities + getSelfClients alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "0.capabilities" `shouldMatchSet` allCapabilities + +testUpdateClientWithConsumableNotificationsCapability :: App () +testUpdateClientWithConsumableNotificationsCapability = do + domain <- asString OwnDomain + let consumeCapability = "consumable-notifications" + alice <- randomUser domain def + aliceId <- alice %. "id" & asString + cid <- + addClient alice def {acapabilities = Nothing} `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "id" & asString + let cli = + ClientIdentity + { domain = domain, + user = aliceId, + client = cid + } + updateClient cli def {capabilities = Just [consumeCapability]} >>= assertSuccess + getSelfClients alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "0.capabilities" `shouldMatch` [consumeCapability] + +testGetClientCapabilitiesV6 :: App () +testGetClientCapabilitiesV6 = do + let allCapabilities = ["legalhold-implicit-consent", "consumable-notifications"] + alice <- randomUser OwnDomain def + addClient alice def {acapabilities = Just allCapabilities} `bindResponse` \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "capabilities" `shouldMatchSet` allCapabilities + + getSelfClients alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "0.capabilities" `shouldMatchSet` allCapabilities + + -- In API v6 and below, the "capabilities" field is an enum, so having a new + -- value for this enum is a breaking change. + withAPIVersion 6 $ getSelfClients alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "0.capabilities.capabilities" `shouldMatchSet` ["legalhold-implicit-consent"] diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index d9b7756419e..683417c0d6b 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -146,6 +146,7 @@ data ClientCapability = -- | Clients have minimum support for LH, but not for explicit consent. Implicit consent -- is granted via the galley server config and cassandra table `galley.legalhold_whitelisted`. ClientSupportsLegalholdImplicitConsent + | ClientSupportsConsumableNotifications deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) deriving (Arbitrary) via (GenericUniform ClientCapability) deriving (ToJSON, FromJSON, Swagger.ToSchema) via Schema ClientCapability @@ -154,14 +155,17 @@ instance ToSchema ClientCapability where schema = enum @Text "ClientCapability" $ element "legalhold-implicit-consent" ClientSupportsLegalholdImplicitConsent + <> element "consumable-notifications" ClientSupportsConsumableNotifications instance C.Cql ClientCapability where ctype = C.Tagged C.IntColumn toCql ClientSupportsLegalholdImplicitConsent = C.CqlInt 1 + toCql ClientSupportsConsumableNotifications = C.CqlInt 2 fromCql (C.CqlInt i) = case i of 1 -> pure ClientSupportsLegalholdImplicitConsent + 2 -> pure ClientSupportsConsumableNotifications n -> Left $ "Unexpected ClientCapability value: " ++ show n fromCql _ = Left "ClientCapability value: int expected" @@ -173,21 +177,27 @@ newtype ClientCapabilityList = ClientCapabilityList {fromClientCapabilityList :: deriving (ToJSON, FromJSON, Swagger.ToSchema) via (Schema ClientCapabilityList) instance ToSchema ClientCapabilityList where + schema = capabilitiesSchema Nothing + +instance ToSchema (Versioned V6 ClientCapabilityList) where schema = - object "ClientCapabilityList" $ - ClientCapabilityList <$> fromClientCapabilityList .= fmap runIdentity capabilitiesFieldSchema - -capabilitiesFieldSchema :: - (FieldFunctor SwaggerDoc f) => - ObjectSchemaP SwaggerDoc (Set ClientCapability) (f (Set ClientCapability)) -capabilitiesFieldSchema = - Set.toList - .= fieldWithDocModifierF "capabilities" mods (Set.fromList <$> array schema) + object "ClientCapabilityListV6" $ + Versioned + <$> unVersioned .= field "capabilities" (capabilitiesSchema (Just V6)) + +capabilitiesSchema :: + Maybe Version -> + ValueSchema NamedSwaggerDoc ClientCapabilityList +capabilitiesSchema mVersion = + named "ClientCapabilityList" $ + ClientCapabilityList + <$> (Set.toList . dropIncompatibleCapabilities . fromClientCapabilityList) .= (Set.fromList <$> array schema) where - mods = - description - ?~ "Hints provided by the client for the backend so it can \ - \behave in a backwards-compatible way." + dropIncompatibleCapabilities :: Set ClientCapability -> Set ClientCapability + dropIncompatibleCapabilities caps = + case mVersion of + Just v | v <= V6 -> Set.delete ClientSupportsConsumableNotifications caps + _ -> caps -------------------------------------------------------------------------------- -- UserClientMap @@ -500,7 +510,7 @@ mlsPublicKeysSchema = mapSchema = map_ base64Schema clientSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc Client -clientSchema mv = +clientSchema mVersion = object "Client" $ Client <$> clientId .= field "id" schema @@ -510,15 +520,17 @@ clientSchema mv = <*> clientLabel .= maybe_ (optField "label" schema) <*> clientCookie .= maybe_ (optField "cookie" schema) <*> clientModel .= maybe_ (optField "model" schema) - <*> clientCapabilities .= (fromMaybe mempty <$> caps) + <*> clientCapabilities .= (fromMaybe mempty <$> optField "capabilities" caps) <*> clientMLSPublicKeys .= mlsPublicKeysFieldSchema <*> clientLastActive .= maybe_ (optField "last_active" utcTimeSchema) where - caps :: ObjectSchemaP SwaggerDoc ClientCapabilityList (Maybe ClientCapabilityList) - caps = case mv of + caps :: ValueSchema NamedSwaggerDoc ClientCapabilityList + caps = case mVersion of -- broken capability serialisation for backwards compatibility - Just v | v <= V6 -> optField "capabilities" schema - _ -> fmap ClientCapabilityList <$> fromClientCapabilityList .= capabilitiesFieldSchema + Just v + | v <= V6 -> + dimap Versioned unVersioned $ schema @(Versioned V6 ClientCapabilityList) + _ -> schema @ClientCapabilityList instance ToSchema Client where schema = clientSchema Nothing @@ -662,7 +674,7 @@ data NewClient = NewClient newClientCookie :: Maybe CookieLabel, newClientPassword :: Maybe PlainTextPassword6, newClientModel :: Maybe Text, - newClientCapabilities :: Maybe (Set ClientCapability), + newClientCapabilities :: Maybe ClientCapabilityList, newClientMLSPublicKeys :: MLSPublicKeys, newClientVerificationCode :: Maybe Code.Value } @@ -726,7 +738,16 @@ instance ToSchema NewClient where schema ) <*> newClientModel .= maybe_ (optField "model" schema) - <*> newClientCapabilities .= maybe_ capabilitiesFieldSchema + <*> newClientCapabilities + .= maybe_ + ( optFieldWithDocModifier + "capabilities" + ( description + ?~ "Hints provided by the client for the backend so it can \ + \behave in a backwards-compatible way." + ) + schema + ) <*> newClientMLSPublicKeys .= mlsPublicKeysFieldSchema <*> newClientVerificationCode .= maybe_ (optField "verification_code" schema) @@ -754,7 +775,7 @@ data UpdateClient = UpdateClient updateClientLastKey :: Maybe LastPrekey, updateClientLabel :: Maybe Text, -- | see haddocks for 'ClientCapability' - updateClientCapabilities :: Maybe (Set ClientCapability), + updateClientCapabilities :: Maybe ClientCapabilityList, updateClientMLSPublicKeys :: MLSPublicKeys } deriving stock (Eq, Show, Generic) @@ -796,7 +817,13 @@ instance ToSchema UpdateClient where (description ?~ "A new name for this client.") schema ) - <*> updateClientCapabilities .= maybe_ capabilitiesFieldSchema + <*> updateClientCapabilities + .= maybe_ + ( optFieldWithDocModifier + "capabilities" + (description ?~ "Hints provided by the client for the backend so it can behave in a backwards-compatible way.") + schema + ) <*> updateClientMLSPublicKeys .= mlsPublicKeysFieldSchema -------------------------------------------------------------------------------- diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs index 17b3893bef6..98bb2187a95 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewClient_user.hs @@ -66,7 +66,7 @@ testObject_NewClient_user_2 = "I\1065423\995547oIC\by\1045956\&1\13659&w>S~z\35967\a{2Dj\v|Z\"\f\1060612*[\65357V\1086491kS\145031A\1106044\1056321(2\DLE\48205\SOi\SI(\1032525\168748f?q\SO5\146557d\1068952^nI\1103535_?\1019210H\119099\SUBf\995865\n\1004095x\ACKdZ\1053945^N\fa\SYN\SUBb=\1112183SP\128516aTd\EM\186127\DC3\ACK\ETB!\1011808\142127o{uoN\CANqL\NAK\ESCc=\v@o2\1043826\EOT\142486\US\1079334\&5v\STX\GS_k,\DC3mAV>$\1029013\1061276\RS\1089843\n\8980-\60552ea}G`r? \DEL\1004551\SOH\US\132757\&9\brl\155069}u\120967\1080794\1062392@M6M\155107\98552\167588|E5Ud\1051152tLjQ\1022837\6734\RS\v\DC1jE\ACK'~f\SIR\1010717\NAKd}}\1059960q\1031766\DC1\151174\&9\160469\RS\100592\ETX\186780\DEL\r\FS\US\36812\14285\NAK/\GS\25526\1090814\61061\NUL(:\1054313n#m9x \1078109\183480}\1052622\54486\GS\991929\b`\1087609G#T\DC2-8\NAK\18310\134655\tp/!\STX4C\SUB'DP'.\a\1110090\&8<9\SYN\NAKEq\168018Ep]\ajZ%\1025589\4170O\35069>\CAN\ACKw*f<\1102303\SOjzpjY\US\SUB\19086\DC1\DC1\ACK|\SO\1064500;\135633F!f\19971b%\1048714t9\DC2\f\121106X! \133247C\RS\1029038\162320C!\20923H(/\GSV)e\SYN2\NUL#H$BAJy\ETB\162654X\137014\FS\SUB\DEL~\f\ESC;\n<\GSf~{\b_" ), newClientModel = Just "om", - newClientCapabilities = Just (Set.fromList [ClientSupportsLegalholdImplicitConsent]), + newClientCapabilities = Just (ClientCapabilityList (Set.fromList [ClientSupportsLegalholdImplicitConsent])), newClientMLSPublicKeys = mempty, newClientVerificationCode = Nothing } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UpdateClient_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UpdateClient_user.hs index be89952db7d..2fd40a5ac05 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UpdateClient_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/UpdateClient_user.hs @@ -287,6 +287,6 @@ testObject_UpdateClient_user_20 = ], updateClientLastKey = Just (lastPrekey "\DC4 }Kg\ve3"), updateClientLabel = Just "\ESC\EOT\SOHccn\US{Y5", - updateClientCapabilities = Just [ClientSupportsLegalholdImplicitConsent], + updateClientCapabilities = Just (ClientCapabilityList [ClientSupportsLegalholdImplicitConsent]), updateClientMLSPublicKeys = mempty } 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 d85fdd1bbd8..2d5c43f18fb 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 @@ -106,12 +106,21 @@ tests = ], testGroup "ClientCapability" $ testObjects - [(testObject_ClientCapability_1, "testObject_ClientCapability_1.json")], - testGroup "ClientCapabilityList" $ + [ (testObject_ClientCapability_1, "testObject_ClientCapability_1.json"), + (testObject_ClientCapability_2, "testObject_ClientCapability_2.json") + ], + testGroup "ClientCapabilityListV6" $ testObjects [ (testObject_ClientCapabilityList_1, "testObject_ClientCapabilityList_1.json"), (testObject_ClientCapabilityList_2, "testObject_ClientCapabilityList_2.json") ], + testGroup "ClientCapabilityListV6 - non-round-trip" $ + [testToJSON testObject_ClientCapabilityList_3 "testObject_ClientCapabilityList_3.json"], + testGroup "ClientCapabilityList" $ + testObjects + [ (testObject_ClientCapabilityList_4, "testObject_ClientCapabilityList_4.json"), + (testObject_ClientCapabilityList_5, "testObject_ClientCapabilityList_5.json") + ], testGroup "Event.FeatureConfig.Event" $ testObjects diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapability.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapability.hs index f739ad108e1..c2c5d152bcc 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapability.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapability.hs @@ -21,3 +21,6 @@ import Wire.API.User.Client (ClientCapability (..)) testObject_ClientCapability_1 :: ClientCapability testObject_ClientCapability_1 = ClientSupportsLegalholdImplicitConsent + +testObject_ClientCapability_2 :: ClientCapability +testObject_ClientCapability_2 = ClientSupportsConsumableNotifications diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapabilityList.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapabilityList.hs index 477e4bbcadf..bbb616c990a 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapabilityList.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/ClientCapabilityList.hs @@ -19,10 +19,35 @@ module Test.Wire.API.Golden.Manual.ClientCapabilityList where import Data.Set qualified as Set import Imports +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.User.Client (ClientCapability (..), ClientCapabilityList (..)) -testObject_ClientCapabilityList_1 :: ClientCapabilityList -testObject_ClientCapabilityList_1 = ClientCapabilityList mempty +testObject_ClientCapabilityList_1 :: Versioned V6 ClientCapabilityList +testObject_ClientCapabilityList_1 = Versioned $ ClientCapabilityList mempty -testObject_ClientCapabilityList_2 :: ClientCapabilityList -testObject_ClientCapabilityList_2 = ClientCapabilityList (Set.fromList [ClientSupportsLegalholdImplicitConsent]) +testObject_ClientCapabilityList_2 :: Versioned V6 ClientCapabilityList +testObject_ClientCapabilityList_2 = Versioned $ ClientCapabilityList (Set.fromList [ClientSupportsLegalholdImplicitConsent]) + +testObject_ClientCapabilityList_3 :: Versioned V6 ClientCapabilityList +testObject_ClientCapabilityList_3 = + Versioned $ + ClientCapabilityList + ( Set.fromList + [ ClientSupportsLegalholdImplicitConsent, + ClientSupportsConsumableNotifications + ] + ) + +testObject_ClientCapabilityList_4 :: ClientCapabilityList +testObject_ClientCapabilityList_4 = + ClientCapabilityList mempty + +testObject_ClientCapabilityList_5 :: ClientCapabilityList +testObject_ClientCapabilityList_5 = + ClientCapabilityList + ( Set.fromList + [ ClientSupportsLegalholdImplicitConsent, + ClientSupportsConsumableNotifications + ] + ) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs index a7db1f7594d..d428e941c32 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs @@ -17,6 +17,7 @@ module Test.Wire.API.Golden.Runner ( testObjects, + testToJSON, protoTestObjects, testFromJSONFailure, testFromJSONFailureWithMsg, @@ -46,8 +47,19 @@ testObjects = fmap (\(obj, path) -> testCase path $ testObject obj path) testObject :: forall a. (Typeable a, ToJSON a, FromJSON a, Eq a, Show a) => a -> FilePath -> Assertion testObject obj path = do + assertJSONIsGolden obj path + assertEqual + (show (typeRep @a) <> ": FromJSON of " <> path <> " should match object") + (Success obj) + (fromJSON $ toJSON obj) + +testToJSON :: forall a. (Typeable a, ToJSON a) => a -> FilePath -> TestTree +testToJSON obj path = testCase path $ assertJSONIsGolden obj path + +assertJSONIsGolden :: forall a. (Typeable a, ToJSON a) => a -> FilePath -> Assertion +assertJSONIsGolden obj path = do let actualValue = toJSON obj :: Value - actualJson = encodePretty' config actualValue + actualJson = encodePretty' encodeConfig actualValue dir = "test/golden" fullPath = dir <> "/" <> path createDirectoryIfMissing True dir @@ -60,20 +72,17 @@ testObject obj path = do <> ": ToJSON should match golden file: " <> path <> "\n\nexpected:\n" - <> cs (encodePretty' config expectedValue) + <> cs (encodePretty' encodeConfig expectedValue) <> "\n\nactual:\n" - <> cs (encodePretty' config actualValue) + <> cs (encodePretty' encodeConfig actualValue) <> "\n\ndiff:\n" - <> cs (encodePretty' config (AD.diff expectedValue actualValue)) + <> cs (encodePretty' encodeConfig (AD.diff expectedValue actualValue)) ) (expectedValue == actualValue) - assertEqual - (show (typeRep @a) <> ": FromJSON of " <> path <> " should match object") - (Success obj) - (fromJSON actualValue) assertBool ("JSON golden file " <> path <> " does not exist") exists - where - config = defConfig {confCompare = compare, confTrailingNewline = True} + +encodeConfig :: Config +encodeConfig = defConfig {confCompare = compare, confTrailingNewline = True} protoTestObjects :: forall m a. diff --git a/libs/wire-api/test/golden/testObject_ClientCapabilityList_3.json b/libs/wire-api/test/golden/testObject_ClientCapabilityList_3.json new file mode 100644 index 00000000000..89c37fb330c --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientCapabilityList_3.json @@ -0,0 +1,5 @@ +{ + "capabilities": [ + "legalhold-implicit-consent" + ] +} diff --git a/libs/wire-api/test/golden/testObject_ClientCapabilityList_4.json b/libs/wire-api/test/golden/testObject_ClientCapabilityList_4.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientCapabilityList_4.json @@ -0,0 +1 @@ +[] diff --git a/libs/wire-api/test/golden/testObject_ClientCapabilityList_5.json b/libs/wire-api/test/golden/testObject_ClientCapabilityList_5.json new file mode 100644 index 00000000000..e7711560546 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientCapabilityList_5.json @@ -0,0 +1,4 @@ +[ + "legalhold-implicit-consent", + "consumable-notifications" +] diff --git a/libs/wire-api/test/golden/testObject_ClientCapability_2.json b/libs/wire-api/test/golden/testObject_ClientCapability_2.json new file mode 100644 index 00000000000..2b3d761f339 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientCapability_2.json @@ -0,0 +1 @@ +"consumable-notifications" diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index d739b479a47..b8c52ef8179 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -195,12 +195,13 @@ addClientWithReAuthPolicy policy luid@(tUnqualified -> u) con new = do usr <- (lift . liftSem $ User.getAccountNoFilter luid) >>= maybe (throwE (ClientUserNotFound u)) (pure . (.accountUser)) verifyCode (newClientVerificationCode new) luid maxPermClients <- fromMaybe Opt.defUserMaxPermClients <$> asks (.settings.userMaxPermClients) - let caps :: Maybe (Set ClientCapability) + let caps :: Maybe ClientCapabilityList caps = updlhdev $ newClientCapabilities new where + updlhdev :: Maybe ClientCapabilityList -> Maybe ClientCapabilityList updlhdev = if newClientType new == LegalHoldClientType - then Just . maybe (Set.singleton lhcaps) (Set.insert lhcaps) + then Just . ClientCapabilityList . maybe (Set.singleton lhcaps) (Set.insert lhcaps . fromClientCapabilityList) else id lhcaps = ClientSupportsLegalholdImplicitConsent (clt0, old, count) <- @@ -238,8 +239,7 @@ updateClient u c r = do client <- lift (Data.lookupClient u c) >>= maybe (throwE ClientNotFound) pure for_ (updateClientLabel r) $ lift . Data.updateClientLabel u c . Just for_ (updateClientCapabilities r) $ \caps' -> do - let ClientCapabilityList caps = clientCapabilities client - if caps `Set.isSubsetOf` caps' + if client.clientCapabilities.fromClientCapabilityList `Set.isSubsetOf` caps'.fromClientCapabilityList then lift . Data.updateClientCapabilities u c . Just $ caps' else throwE ClientCapabilitiesCannotBeRemoved let lk = maybeToList (unpackLastPrekey <$> updateClientLastKey r) diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 2f7fb87921b..42f92476fc9 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -123,7 +123,7 @@ addClient :: ClientId -> NewClient -> Int -> - Maybe (Imports.Set ClientCapability) -> + Maybe ClientCapabilityList -> ExceptT ClientDataError m (Client, [Client], Word) addClient = addClientWithReAuthPolicy reAuthForNewClients @@ -136,9 +136,9 @@ addClientWithReAuthPolicy :: ClientId -> NewClient -> Int -> - Maybe (Imports.Set ClientCapability) -> + Maybe ClientCapabilityList -> ExceptT ClientDataError m (Client, [Client], Word) -addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients cps = do +addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients caps = do clients <- lookupClients (tUnqualified u) let typed = filter ((== newClientType c) . clientType) clients let count = length typed @@ -170,7 +170,7 @@ addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients cps = do let keys = unpackLastPrekey (newClientLastKey c) : newClientPrekeys c updatePrekeys uid newId keys let mdl = newClientModel c - prm = (uid, newId, now, newClientType c, newClientLabel c, newClientClass c, newClientCookie c, mdl, C.Set . Set.toList <$> cps) + prm = (uid, newId, now, newClientType c, newClientLabel c, newClientClass c, newClientCookie c, mdl, C.Set . Set.toList . fromClientCapabilityList <$> caps) retry x5 $ write insertClient (params LocalQuorum prm) addMLSPublicKeys uid newId (Map.assocs (newClientMLSPublicKeys c)) pure $! @@ -182,7 +182,7 @@ addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients cps = do clientLabel = newClientLabel c, clientCookie = newClientCookie c, clientModel = mdl, - clientCapabilities = ClientCapabilityList (fromMaybe mempty cps), + clientCapabilities = fromMaybe mempty caps, clientMLSPublicKeys = mempty, clientLastActive = Nothing } @@ -258,8 +258,8 @@ rmClient u c = do updateClientLabel :: (MonadClient m) => UserId -> ClientId -> Maybe Text -> m () updateClientLabel u c l = retry x5 $ write updateClientLabelQuery (params LocalQuorum (l, u, c)) -updateClientCapabilities :: (MonadClient m) => UserId -> ClientId -> Maybe (Imports.Set ClientCapability) -> m () -updateClientCapabilities u c fs = retry x5 $ write updateClientCapabilitiesQuery (params LocalQuorum (C.Set . Set.toList <$> fs, u, c)) +updateClientCapabilities :: (MonadClient m) => UserId -> ClientId -> Maybe ClientCapabilityList -> m () +updateClientCapabilities u c fs = retry x5 $ write updateClientCapabilitiesQuery (params LocalQuorum (C.Set . Set.toList . fromClientCapabilityList <$> fs, u, c)) -- | If the update fails, which can happen if device does not exist, then ignore the error silently. updateClientLastActive :: (MonadClient m) => UserId -> ClientId -> UTCTime -> m () diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 95ce84a5670..38959425c37 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -715,7 +715,7 @@ addBot zuid zcon cid add = do -- implicitly in the next line. pure $ FutureWork @'UnprotectedBot undefined lbid <- qualifyLocal (botUserId bid) - wrapClientE (User.addClient lbid bcl newClt maxPermClients (Just $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent)) + wrapClientE (User.addClient lbid bcl newClt maxPermClients (Just $ ClientCapabilityList $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent)) !>> const (StdError $ badGatewayWith "MalformedPrekeys") -- Add the bot to the conversation diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index c22fd3a613b..c20f8d492fe 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -1187,7 +1187,7 @@ testUpdateClient opts brig = do -- update supported client capabilities work let checkUpdate :: (HasCallStack) => Maybe [ClientCapability] -> Bool -> [ClientCapability] -> Http () checkUpdate capsIn respStatusOk capsOut = do - let update'' = defUpdateClient {updateClientCapabilities = Set.fromList <$> capsIn} + let update'' = defUpdateClient {updateClientCapabilities = ClientCapabilityList . Set.fromList <$> capsIn} put ( apiVersion "v1" . brig @@ -1236,7 +1236,7 @@ testUpdateClient opts brig = do assertEqual "" (clientId c) cid' assertEqual "" expectedPrekey prekey' - caps = Just $ Set.fromList [ClientSupportsLegalholdImplicitConsent] + caps = Just $ ClientCapabilityList $ Set.fromList [ClientSupportsLegalholdImplicitConsent] label = "label-bc1b7b0c-b7bf-11eb-9a1d-233d397f934a" prekey = somePrekeys !! 4 diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index 923a25b92e7..c0ff9975269 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -657,17 +657,17 @@ testOldClientsBlockDeviceHandshake = do legalholderLHDevice <- doEnableLH legalholder legalholder _legalholder2LHDevice <- doEnableLH legalholder legalholder2 - let caps = Set.singleton Client.ClientSupportsLegalholdImplicitConsent + let caps = Client.ClientCapabilityList $ Set.singleton Client.ClientSupportsLegalholdImplicitConsent legalholderClient <- do clnt <- randomClientWithCaps legalholder (someLastPrekeys !! 1) (Just caps) - ensureClientCaps legalholder clnt (Client.ClientCapabilityList caps) + ensureClientCaps legalholder clnt caps pure clnt legalholder2Client <- do clnt <- randomClient legalholder2 (someLastPrekeys !! 3) -- this another way to do it (instead of providing caps during client creation). ensureClientCaps legalholder2 clnt (Client.ClientCapabilityList mempty) upgradeClientToLH legalholder2 clnt - ensureClientCaps legalholder2 clnt (Client.ClientCapabilityList caps) + ensureClientCaps legalholder2 clnt caps pure clnt grantConsent tid2 peer connectUsers peer (List1.list1 legalholder [legalholder2]) diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 026a3475fc3..1a31fae869e 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -2028,7 +2028,7 @@ ephemeralUser = do randomClient :: (HasCallStack) => UserId -> LastPrekey -> TestM ClientId randomClient uid lk = randomClientWithCaps uid lk Nothing -randomClientWithCaps :: (HasCallStack) => UserId -> LastPrekey -> Maybe (Set Client.ClientCapability) -> TestM ClientId +randomClientWithCaps :: (HasCallStack) => UserId -> LastPrekey -> Maybe ClientCapabilityList -> TestM ClientId randomClientWithCaps uid lk caps = do b <- viewBrig resp <- @@ -2418,7 +2418,7 @@ putCapabilities zusr cid caps = do ( brig . zUser zusr . paths ["clients", toByteString' cid] - . json defUpdateClient {updateClientCapabilities = Just (Set.fromList caps)} + . json defUpdateClient {updateClientCapabilities = Just $ ClientCapabilityList $ Set.fromList caps} . expect2xx ) From 713a386f0dd7b06fd12fb5aacd2540aa925d5507 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 24 Sep 2024 12:16:35 +0200 Subject: [PATCH 087/136] WPB-11101 remove invitation tables from brig (#4263) --- cassandra-schema.cql | 64 +++---------------- changelog.d/5-internal/WPB-11101 | 1 + services/brig/brig.cabal | 1 + services/brig/src/Brig/Schema/Run.hs | 4 +- .../Brig/Schema/V87_DropInvitationTables.hs | 35 ++++++++++ 5 files changed, 50 insertions(+), 55 deletions(-) create mode 100644 changelog.d/5-internal/WPB-11101 create mode 100644 services/brig/src/Brig/Schema/V87_DropInvitationTables.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index 7d520e33b1f..b0fb20beb67 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -353,10 +353,9 @@ CREATE TABLE brig_test.oauth_user_refresh_token ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; -CREATE TABLE brig_test.invitation_info ( - code ascii PRIMARY KEY, - id uuid, - inviter uuid +CREATE TABLE brig_test.users_pending_activation ( + user uuid PRIMARY KEY, + expires_at timestamp ) WITH bloom_filter_fp_chance = 0.01 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' @@ -867,24 +866,6 @@ CREATE TABLE brig_test.connection_remote ( AND speculative_retry = '99PERCENTILE'; CREATE INDEX connection_remote_right_domain_idx ON brig_test.connection_remote (right_domain); -CREATE TABLE brig_test.users_pending_activation ( - user uuid PRIMARY KEY, - expires_at timestamp -) WITH bloom_filter_fp_chance = 0.01 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - CREATE TABLE brig_test.connection ( left uuid, right uuid, @@ -910,27 +891,6 @@ CREATE TABLE brig_test.connection ( AND speculative_retry = '99PERCENTILE'; CREATE INDEX conn_status ON brig_test.connection (status); -CREATE TABLE brig_test.password_reset ( - key ascii PRIMARY KEY, - code ascii, - retries int, - timeout timestamp, - user uuid -) WITH bloom_filter_fp_chance = 0.1 - AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} - AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} - AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} - AND crc_check_chance = 1.0 - AND dclocal_read_repair_chance = 0.1 - AND default_time_to_live = 0 - AND gc_grace_seconds = 864000 - AND max_index_interval = 2048 - AND memtable_flush_period_in_ms = 0 - AND min_index_interval = 128 - AND read_repair_chance = 0.0 - AND speculative_retry = '99PERCENTILE'; - CREATE TABLE brig_test.federation_remotes ( domain text PRIMARY KEY, restriction int, @@ -950,20 +910,16 @@ CREATE TABLE brig_test.federation_remotes ( AND read_repair_chance = 0.0 AND speculative_retry = '99PERCENTILE'; -CREATE TABLE brig_test.invitation ( - inviter uuid, - id uuid, +CREATE TABLE brig_test.password_reset ( + key ascii PRIMARY KEY, code ascii, - created_at timestamp, - email text, - name text, - phone text, - PRIMARY KEY (inviter, id) -) WITH CLUSTERING ORDER BY (id ASC) - AND bloom_filter_fp_chance = 0.01 + retries int, + timeout timestamp, + user uuid +) WITH bloom_filter_fp_chance = 0.1 AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'} AND comment = '' - AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'} + AND compaction = {'class': 'org.apache.cassandra.db.compaction.LeveledCompactionStrategy'} AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'} AND crc_check_chance = 1.0 AND dclocal_read_repair_chance = 0.1 diff --git a/changelog.d/5-internal/WPB-11101 b/changelog.d/5-internal/WPB-11101 new file mode 100644 index 00000000000..09b5c427420 --- /dev/null +++ b/changelog.d/5-internal/WPB-11101 @@ -0,0 +1 @@ +Remove unused invitation tables from brig. diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 9b116cb9a2b..051f18c405f 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -184,6 +184,7 @@ library Brig.Schema.V84_DropTeamInvitationPhone Brig.Schema.V85_DropUserKeysHashed Brig.Schema.V86_WriteTimeBumper + Brig.Schema.V87_DropInvitationTables Brig.Team.API Brig.Team.Email Brig.Team.Template diff --git a/services/brig/src/Brig/Schema/Run.hs b/services/brig/src/Brig/Schema/Run.hs index 72bbff2b1f2..173c5b15bc0 100644 --- a/services/brig/src/Brig/Schema/Run.hs +++ b/services/brig/src/Brig/Schema/Run.hs @@ -61,6 +61,7 @@ import Brig.Schema.V83_AddTextStatus qualified as V83_AddTextStatus import Brig.Schema.V84_DropTeamInvitationPhone qualified as V84_DropTeamInvitationPhone import Brig.Schema.V85_DropUserKeysHashed qualified as V85_DropUserKeysHashed import Brig.Schema.V86_WriteTimeBumper qualified as V86_WriteTimeBumper +import Brig.Schema.V87_DropInvitationTables qualified as V87_DropInvitationTables import Cassandra.MigrateSchema (migrateSchema) import Cassandra.Schema import Control.Exception (finally) @@ -128,7 +129,8 @@ migrations = V83_AddTextStatus.migration, V84_DropTeamInvitationPhone.migration, V85_DropUserKeysHashed.migration, - V86_WriteTimeBumper.migration + V86_WriteTimeBumper.migration, + V87_DropInvitationTables.migration -- FUTUREWORK: undo V41 (searchable flag); we stopped using it in -- https://github.com/wireapp/wire-server/pull/964 ] diff --git a/services/brig/src/Brig/Schema/V87_DropInvitationTables.hs b/services/brig/src/Brig/Schema/V87_DropInvitationTables.hs new file mode 100644 index 00000000000..d0366ffb113 --- /dev/null +++ b/services/brig/src/Brig/Schema/V87_DropInvitationTables.hs @@ -0,0 +1,35 @@ +{-# LANGUAGE QuasiQuotes #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 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 Brig.Schema.V87_DropInvitationTables + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = + Migration 87 "Drop unused invitation tables" $ do + schema' + [r| DROP TABLE IF EXISTS invitation |] + schema' + [r| DROP TABLE IF EXISTS invitation_info; |] From 6c9ccec2fca0f62cb7d5b70b6944c129561a2cc4 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Tue, 24 Sep 2024 11:29:28 +0000 Subject: [PATCH 088/136] Block services whitelist for teams with default protocol MLS (#4266) --------- Co-authored-by: Igor Ranieri --- changelog.d/1-api-changes/WPB-10797 | 1 + integration/integration.cabal | 1 + integration/test/API/Brig.hs | 23 +++++ integration/test/Test/MLS/Services.hs | 95 +++++++++++++++++++ libs/wire-api/src/Wire/API/Error/Brig.hs | 3 + .../src/Wire/UserSubsystem/Error.hs | 4 +- services/brig/src/Brig/Provider/API.hs | 13 ++- .../brig/test/integration/API/Provider.hs | 28 ------ 8 files changed, 138 insertions(+), 30 deletions(-) create mode 100644 changelog.d/1-api-changes/WPB-10797 create mode 100644 integration/test/Test/MLS/Services.hs diff --git a/changelog.d/1-api-changes/WPB-10797 b/changelog.d/1-api-changes/WPB-10797 new file mode 100644 index 00000000000..62f2d18d093 --- /dev/null +++ b/changelog.d/1-api-changes/WPB-10797 @@ -0,0 +1 @@ +Services allowlist are blocked by 409 (mls-services-not-allowed) for teams with default protocol MLS. diff --git a/integration/integration.cabal b/integration/integration.cabal index a4351796175..0310aea8db3 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -136,6 +136,7 @@ library Test.MLS.Message Test.MLS.Notifications Test.MLS.One2One + Test.MLS.Services Test.MLS.SubConversation Test.MLS.Unreachable Test.Notifications diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 39c1a4f48a8..c2c085d069f 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -842,3 +842,26 @@ upgradePersonalToTeam :: (HasCallStack, MakesValue user) => user -> String -> Ap upgradePersonalToTeam user name = do req <- baseRequest user Brig Versioned $ joinHttpPath ["upgrade-personal-to-team"] submit "POST" $ req & addJSONObject ["name" .= name, "icon" .= "default"] + +postServiceWhitelist :: + ( HasCallStack, + MakesValue user, + MakesValue tid, + MakesValue update + ) => + user -> + tid -> + update -> + App Response +postServiceWhitelist user tid update = do + tidStr <- asString tid + updateJson <- make update + req <- + baseRequest user Brig Versioned $ + joinHttpPath + [ "teams", + tidStr, + "services", + "whitelist" + ] + submit "POST" (addJSON updateJson req) diff --git a/integration/test/Test/MLS/Services.hs b/integration/test/Test/MLS/Services.hs new file mode 100644 index 00000000000..1160fe7c423 --- /dev/null +++ b/integration/test/Test/MLS/Services.hs @@ -0,0 +1,95 @@ +module Test.MLS.Services where + +import API.Brig +import API.Common +import API.GalleyInternal (patchTeamFeatureConfig) +import SetupHelpers +import Testlib.JSON +import Testlib.Prelude + +testWhitelistUpdatePermissions :: (HasCallStack) => App () +testWhitelistUpdatePermissions = do + -- Create a team + (owner, tid, []) <- createTeam OwnDomain 1 + + -- Create a team admin + admin <- createTeamMemberWithRole owner tid "admin" + + -- Create a service + email <- randomEmail + provider <- make <$> setupProvider owner def {newProviderEmail = email} + providerId <- provider %. "id" & asString + service <- make <$> newService OwnDomain providerId def + + do + -- Check that a random user can't add the service to the whitelist + uid <- randomUser OwnDomain def + serviceId <- service %. "id" & asString + np <- + make + $ object + [ "id" .= serviceId, + "provider" .= providerId, + "whitelisted" .= True + ] + bindResponse (postServiceWhitelist uid tid np) $ \resp -> do + resp.status `shouldMatchInt` 403 + (resp.jsonBody %. "label") `shouldMatch` Just "insufficient-permissions" + + do + -- Check that an admin can add the service to the whitelist + serviceId <- service %. "id" & asString + np <- + make + $ object + [ "id" .= serviceId, + "provider" .= providerId, + "whitelisted" .= True + ] + postServiceWhitelist admin tid np >>= assertStatus 200 + + -- set team's defaultProtocol to MLS + mlsConfig <- + make + $ object + [ "config" + .= object + [ "allowedCipherSuites" .= [1 :: Int], + "defaultCipherSuite" .= (1 :: Int), + "defaultProtocol" .= "mls", + "protocolToggleUsers" .= ([] :: [String]), + "supportedProtocols" .= ["mls", "proteus"] + ], + "status" .= "enabled", + "ttl" .= "unlimited" + ] + patchTeamFeatureConfig OwnDomain tid "mls" mlsConfig >>= assertStatus 200 + + do + -- Check that a random user can't add the service to the whitelist + uid <- randomUser OwnDomain def + serviceId <- service %. "id" & asString + np <- + make + $ object + [ "id" .= serviceId, + "provider" .= providerId, + "whitelisted" .= True + ] + bindResponse (postServiceWhitelist uid tid np) $ \resp -> do + resp.status `shouldMatchInt` 409 + (resp.jsonBody %. "label") `shouldMatch` Just "mls-services-not-allowed" + + do + -- Check that an admin can't add the service to the whitelist + serviceId <- service %. "id" & asString + np <- + make + $ object + [ "id" .= serviceId, + "provider" .= providerId, + "whitelisted" .= True + ] + postServiceWhitelist admin tid np >>= \resp -> do + resp.status `shouldMatchInt` 409 + (resp.jsonBody %. "label") `shouldMatch` Just "mls-services-not-allowed" diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 7846f5c51f5..15ecfabe55c 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -100,6 +100,7 @@ data BrigError | PropertyKeyTooLarge | PropertyValueTooLarge | UserAlreadyInATeam + | MLSServicesNotAllowed instance (Typeable (MapError e), KnownError (MapError e)) => IsSwaggerError (e :: BrigError) where addToOpenApi = addStaticErrorToSwagger @(MapError e) @@ -298,3 +299,5 @@ type instance MapError 'PropertyKeyTooLarge = 'StaticError 403 "property-key-too type instance MapError 'PropertyValueTooLarge = 'StaticError 403 "property-value-too-large" "The property value is too large" type instance MapError 'UserAlreadyInATeam = 'StaticError 403 "user-already-in-a-team" "Switching teams is not allowed" + +type instance MapError 'MLSServicesNotAllowed = 'StaticError 409 "mls-services-not-allowed" "Services not allowed in MLS" diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs index 22b1a8e44ec..9166fbc4b73 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs @@ -17,6 +17,7 @@ data UserSubsystemError | UserSubsystemInvalidHandle | UserSubsystemProfileNotFound | UserSubsystemInsufficientTeamPermissions + | UserSubsystemMLSServicesNotAllowed deriving (Eq, Show) userSubsystemErrorToHttpError :: UserSubsystemError -> HttpError @@ -29,6 +30,7 @@ userSubsystemErrorToHttpError = UserSubsystemHandleExists -> errorToWai @E.HandleExists UserSubsystemInvalidHandle -> errorToWai @E.InvalidHandle UserSubsystemHandleManagedByScim -> errorToWai @E.HandleManagedByScim - UserSubsystemInsufficientTeamPermissions -> errorToWai @'E.InsufficientTeamPermissions + UserSubsystemInsufficientTeamPermissions -> errorToWai @E.InsufficientTeamPermissions + UserSubsystemMLSServicesNotAllowed -> errorToWai @E.MLSServicesNotAllowed instance Exception UserSubsystemError diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 38959425c37..2161540a22d 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -88,6 +88,7 @@ import UnliftIO.Async (pooledMapConcurrentlyN_) import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Bot import Wire.API.Conversation.Bot qualified as Public +import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Brig qualified as E @@ -289,6 +290,7 @@ beginPasswordReset (Public.PasswordReset target) = do let gen = mkVerificationCodeGen target (lift . liftSem $ createCode gen VerificationCode.PasswordReset (Retries 3) (Timeout 3600) (Just (toUUID pid))) >>= \case Left (CodeAlreadyExists code) -> + -- FUTUREWORK: use subsystem error throwE $ pwResetError (PasswordResetInProgress $ Just code.codeTTL) Right code -> lift $ sendPasswordResetMail target (code.codeKey) (code.codeValue) @@ -610,11 +612,12 @@ updateServiceWhitelist :: Public.UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp updateServiceWhitelist uid con tid upd = do + -- Preconditions guardSecondFactorDisabled (Just uid) + guardMLSNotDefault let pid = updateServiceWhitelistProvider upd sid = updateServiceWhitelistService upd newWhitelisted = updateServiceWhitelistStatus upd - -- Preconditions lift . liftSem $ ensurePermissions uid tid (Set.toList serviceWhitelistPermissions) _ <- wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound -- Add to various tables @@ -641,6 +644,14 @@ updateServiceWhitelist uid con tid upd = do ) wrapClientE $ DB.deleteServiceWhitelist (Just tid) pid sid pure UpdateServiceWhitelistRespChanged + where + guardMLSNotDefault = lift . liftSem $ do + feat <- GalleyAPIAccess.getFeatureConfigForTeam @_ @Feature.MLSConfig tid + let defProtocol = feat.config.mlsDefaultProtocol + case defProtocol of + ProtocolProteusTag -> pure () + ProtocolMLSTag -> throw UserSubsystemMLSServicesNotAllowed + ProtocolMixedTag -> throw UserSubsystemMLSServicesNotAllowed -------------------------------------------------------------------------------- -- Bot API diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 275fee81150..fc4c6a7bda7 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -133,8 +133,6 @@ tests dom conf p db b c g n = do "service whitelist" [ test p "search permissions" $ testWhitelistSearchPermissions conf db b g, - test p "update permissions" $ - testWhitelistUpdatePermissions conf db b g, test p "basic functionality" $ testWhitelistBasic conf db b g, test p "search" $ testSearchWhitelist conf db b g, @@ -879,32 +877,6 @@ testWhitelistSearchPermissions _config _db brig galley = do listTeamServiceProfilesByPrefix brig member tid Nothing True 20 !!! const 200 === statusCode -testWhitelistUpdatePermissions :: Config -> DB.ClientState -> Brig -> Galley -> Http () -testWhitelistUpdatePermissions config db brig galley = do - -- Create a team - (owner, tid) <- Team.createUserWithTeam brig - -- Create a team admin - let Just adminPermissions = newPermissions serviceWhitelistPermissions mempty - admin <- userId <$> Team.createTeamMember brig galley owner tid adminPermissions - -- Create a service - pid <- providerId <$> randomProvider db brig - new <- defNewService config - sid <- serviceId <$> addGetService brig pid new - enableService brig pid sid - -- Check that a random user can't add it to the whitelist - _uid <- userId <$> randomUser brig - updateServiceWhitelist brig _uid tid (UpdateServiceWhitelist pid sid True) !!! do - const 403 === statusCode - const (Just "insufficient-permissions") === fmap Error.label . responseJsonMaybe - -- Check that a member who's not a team admin also can't add it to the whitelist - _uid <- userId <$> Team.createTeamMember brig galley owner tid noPermissions - updateServiceWhitelist brig _uid tid (UpdateServiceWhitelist pid sid True) !!! do - const 403 === statusCode - const (Just "insufficient-permissions") === fmap Error.label . responseJsonMaybe - -- Check that a team admin can add and remove from the whitelist - whitelistService brig admin tid pid sid - dewhitelistService brig admin tid pid sid - testSearchWhitelist :: Config -> DB.ClientState -> Brig -> Galley -> Http () testSearchWhitelist config db brig galley = do -- Create a team, a team owner, and a team member with no permissions From 5754118afe4d2045c3c3ce4c517abac0b871f376 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Wed, 25 Sep 2024 08:50:33 +0200 Subject: [PATCH 089/136] [chore] Continue to reduce lens usage and simplify Opt/Env code. (#4267) * [chore] Continue to reduce lens usage and simply Opt/Env code --- libs/cassandra-util/src/Cassandra/Options.hs | 22 +-- libs/cassandra-util/src/Cassandra/Util.hs | 19 +- libs/wire-subsystems/src/Wire/Rpc.hs | 4 +- .../src/Wire/BackgroundWorker.hs | 2 +- services/brig/src/Brig/API/Client.hs | 2 +- services/brig/src/Brig/API/Internal.hs | 8 +- services/brig/src/Brig/API/OAuth.hs | 28 +-- services/brig/src/Brig/API/Public.hs | 2 +- services/brig/src/Brig/API/User.hs | 4 +- services/brig/src/Brig/App.hs | 65 ++++--- .../brig/src/Brig/CanonicalInterpreter.hs | 2 +- services/brig/src/Brig/Data/User.hs | 16 +- services/brig/src/Brig/Index/Options.hs | 8 +- services/brig/src/Brig/Options.hs | 170 +++++++++--------- services/brig/src/Brig/Provider/API.hs | 2 +- services/brig/src/Brig/Provider/Template.hs | 18 +- services/brig/src/Brig/Queue/Stomp.hs | 6 +- services/brig/src/Brig/Run.hs | 30 ++-- services/brig/src/Brig/Team/Template.hs | 10 +- services/brig/src/Brig/User/Template.hs | 24 +-- services/brig/test/integration/API/Calling.hs | 8 +- .../brig/test/integration/API/Federation.hs | 6 +- services/brig/test/integration/API/OAuth.hs | 30 ++-- services/brig/test/integration/API/Search.hs | 22 +-- .../brig/test/integration/API/Settings.hs | 4 +- .../test/integration/API/SystemSettings.hs | 4 +- services/brig/test/integration/API/Team.hs | 8 +- services/brig/test/integration/API/User.hs | 4 +- .../brig/test/integration/API/User/Account.hs | 18 +- .../brig/test/integration/API/User/Auth.hs | 12 +- .../brig/test/integration/API/User/Client.hs | 28 +-- .../brig/test/integration/API/User/Handles.hs | 2 +- .../test/integration/API/User/RichInfo.hs | 2 +- .../integration/API/UserPendingActivation.hs | 2 +- services/brig/test/integration/Run.hs | 2 +- services/brig/test/integration/Util.hs | 8 +- services/cargohold/src/CargoHold/Run.hs | 4 +- .../cargohold/test/integration/API/Util.hs | 2 +- .../cargohold/test/integration/TestSetup.hs | 2 +- services/federator/src/Federator/Run.hs | 16 +- .../test/integration/Test/Federator/Util.hs | 6 +- .../migrate-data/src/Galley/DataMigration.hs | 8 +- services/galley/src/Galley/Intra/User.hs | 4 +- services/galley/src/Galley/Intra/Util.hs | 16 +- services/galley/src/Galley/Options.hs | 2 +- services/galley/src/Galley/Run.hs | 4 +- services/galley/test/integration/Run.hs | 2 +- services/galley/test/integration/TestSetup.hs | 6 +- .../migrate-data/src/Gundeck/DataMigration.hs | 8 +- services/gundeck/src/Gundeck/Run.hs | 2 +- .../src/Spar/DataMigration/Types.hs | 8 +- services/spar/src/Spar/Run.hs | 8 +- services/spar/test-integration/Util/Core.hs | 2 +- tools/stern/src/Stern/API.hs | 2 +- tools/stern/src/Stern/App.hs | 4 +- 55 files changed, 355 insertions(+), 353 deletions(-) diff --git a/libs/cassandra-util/src/Cassandra/Options.hs b/libs/cassandra-util/src/Cassandra/Options.hs index f1f62056eee..e8aca6a7de5 100644 --- a/libs/cassandra-util/src/Cassandra/Options.hs +++ b/libs/cassandra-util/src/Cassandra/Options.hs @@ -5,34 +5,28 @@ module Cassandra.Options where -import Cassandra.Helpers -import Control.Lens import Data.Aeson.TH import Imports data Endpoint = Endpoint - { _host :: !Text, - _port :: !Word16 + { host :: !Text, + port :: !Word16 } deriving (Show, Generic) -deriveFromJSON toOptionFieldName ''Endpoint - -makeLenses ''Endpoint +deriveFromJSON defaultOptions ''Endpoint data CassandraOpts = CassandraOpts - { _endpoint :: !Endpoint, - _keyspace :: !Text, + { endpoint :: !Endpoint, + keyspace :: !Text, -- | If this option is unset, use all available nodes. -- If this option is set, use only cassandra nodes in the given datacentre -- -- This option is most likely only necessary during a cassandra DC migration -- FUTUREWORK: remove this option again, or support a datacentre migration feature - _filterNodesByDatacentre :: !(Maybe Text), - _tlsCa :: Maybe FilePath + filterNodesByDatacentre :: !(Maybe Text), + tlsCa :: Maybe FilePath } deriving (Show, Generic) -deriveFromJSON toOptionFieldName ''CassandraOpts - -makeLenses ''CassandraOpts +deriveFromJSON defaultOptions ''CassandraOpts diff --git a/libs/cassandra-util/src/Cassandra/Util.hs b/libs/cassandra-util/src/Cassandra/Util.hs index 378c6b6f2f0..d6968ead939 100644 --- a/libs/cassandra-util/src/Cassandra/Util.hs +++ b/libs/cassandra-util/src/Cassandra/Util.hs @@ -28,7 +28,6 @@ import Cassandra.CQL import Cassandra.Options import Cassandra.Schema import Cassandra.Settings (dcFilterPolicyIfConfigured, initialContactsDisco, initialContactsPlain, mkLogger) -import Control.Lens import Data.Aeson import Data.Fixed import Data.List.NonEmpty qualified as NE @@ -46,12 +45,12 @@ defInitCassandra :: CassandraOpts -> Log.Logger -> IO ClientState defInitCassandra opts logger = do let basicCasSettings = setLogger (CT.mkLogger logger) - . setPortNumber (fromIntegral (opts ^. endpoint . port)) - . setContacts (unpack (opts ^. endpoint . host)) [] - . setKeyspace (Keyspace (opts ^. keyspace)) + . setPortNumber (fromIntegral opts.endpoint.port) + . setContacts (unpack opts.endpoint.host) [] + . setKeyspace (Keyspace opts.keyspace) . setProtocolVersion V4 $ defSettings - initCassandra basicCasSettings (opts ^. tlsCa) logger + initCassandra basicCasSettings opts.tlsCa logger -- | Create Cassandra `ClientState` ("connection") for a service initCassandraForService :: @@ -64,22 +63,22 @@ initCassandraForService :: initCassandraForService opts serviceName discoUrl mbSchemaVersion logger = do c <- maybe - (initialContactsPlain (opts ^. endpoint . host)) + (initialContactsPlain opts.endpoint.host) (initialContactsDisco ("cassandra_" ++ serviceName) . unpack) discoUrl let basicCasSettings = setLogger (mkLogger (Log.clone (Just (pack ("cassandra." ++ serviceName))) logger)) . setContacts (NE.head c) (NE.tail c) - . setPortNumber (fromIntegral (opts ^. endpoint . port)) - . setKeyspace (Keyspace (opts ^. keyspace)) + . setPortNumber (fromIntegral opts.endpoint.port) + . setKeyspace (Keyspace opts.keyspace) . setMaxConnections 4 . setPoolStripes 4 . setSendTimeout 3 . setResponseTimeout 10 . setProtocolVersion V4 - . setPolicy (dcFilterPolicyIfConfigured logger (opts ^. filterNodesByDatacentre)) + . setPolicy (dcFilterPolicyIfConfigured logger opts.filterNodesByDatacentre) $ defSettings - p <- initCassandra basicCasSettings (opts ^. tlsCa) logger + p <- initCassandra basicCasSettings opts.tlsCa logger maybe (pure ()) (\v -> runClient p $ (versionCheck v)) mbSchemaVersion pure p diff --git a/libs/wire-subsystems/src/Wire/Rpc.hs b/libs/wire-subsystems/src/Wire/Rpc.hs index bd7b3c682ef..8a954eafddb 100644 --- a/libs/wire-subsystems/src/Wire/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/Rpc.hs @@ -46,8 +46,8 @@ rpcImpl :: ServiceName -> Endpoint -> (Request -> Request) -> HttpRpc (Response rpcImpl serviceName ep req = do rpc' serviceName empty $ req - . Bilge.host (encodeUtf8 ep._host) - . Bilge.port ep._port + . Bilge.host (encodeUtf8 ep.host) + . Bilge.port ep.port rpcWithRetriesImpl :: ServiceName -> Endpoint -> (Request -> Request) -> HttpRpc (Response (Maybe LByteString)) rpcWithRetriesImpl serviceName ep req = diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index 7c5241a6bc9..17b45f71ecd 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -50,7 +50,7 @@ run opts = do -- Close the channel. `extended` will then close the connection, flushing messages to the server. Log.info l $ Log.msg $ Log.val "Closing RabbitMQ channel" Q.closeChannel chan - let server = defaultServer (T.unpack $ opts.backgroundWorker._host) opts.backgroundWorker._port env.logger + let server = defaultServer (T.unpack $ opts.backgroundWorker.host) opts.backgroundWorker.port env.logger settings <- newSettings server -- Additional cleanup when shutting down via signals. runSettingsWithCleanup cleanup settings (servantApp env) Nothing diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index b8c52ef8179..08cac151d6d 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -557,7 +557,7 @@ createAccessToken luid cid method link proof = do fromByteString $ "https://" <> toByteString' domain <> "/" <> T.encodeUtf8 (toUrlPiece link) maxSkewSeconds <- Opt.setDpopMaxSkewSecs <$> asks (.settings) - expiresIn <- Opt.setDpopTokenExpirationTimeSecs <$> asks (.settings) + expiresIn <- Opt.dpopTokenExpirationTimeSecs <$> asks (.settings) now <- fromUTCTime <$> lift (liftSem Now.get) let expiresAt = now & addToEpoch expiresIn pubKeyBundle <- do diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 35248ac45ed..28ae7f50973 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -31,7 +31,7 @@ import Brig.API.MLS.KeyPackages.Validation import Brig.API.OAuth (internalOauthAPI) import Brig.API.Types import Brig.API.User qualified as API -import Brig.App +import Brig.App as App import Brig.Data.Activation import Brig.Data.Client qualified as Data import Brig.Data.Connection qualified as Data @@ -370,7 +370,7 @@ updateFederationRemote dom fedcfg = do getAccountConferenceCallingConfig :: UserId -> Handler r (Feature ConferenceCallingConfig) getAccountConferenceCallingConfig uid = do mStatus <- lift $ wrapClient $ Data.lookupFeatureConferenceCalling uid - mDefStatus <- preview (settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNull) + mDefStatus <- preview (App.settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNull) pure $ def {status = mStatus <|> mDefStatus ?: (def :: LockableFeature ConferenceCallingConfig).status} putAccountConferenceCallingConfig :: UserId -> Feature ConferenceCallingConfig -> Handler r NoContent @@ -735,14 +735,14 @@ updateLocale uid upd@(LocaleUpdate locale) = do deleteLocale :: (Member UserSubsystem r) => UserId -> (Handler r) NoContent deleteLocale uid = do - defLoc <- setDefaultUserLocale <$> asks (.settings) + defLoc <- defaultUserLocale <$> asks (.settings) qUid <- qualifyLocal uid lift . liftSem $ updateUserProfile qUid Nothing UpdateOriginScim def {locale = Just defLoc} pure NoContent getDefaultUserLocale :: (Handler r) LocaleUpdate getDefaultUserLocale = do - defLocale <- setDefaultUserLocale <$> asks (.settings) + defLocale <- defaultUserLocale <$> asks (.settings) pure $ LocaleUpdate defLocale updateClientLastActive :: UserId -> ClientId -> Handler r () diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index bdbdf63e048..e66dc240b14 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -31,7 +31,7 @@ import Brig.Options qualified as Opt import Cassandra hiding (Set) import Cassandra qualified as C import Control.Error -import Control.Lens (view, (?~), (^?)) +import Control.Lens ((?~), (^?)) import Crypto.JWT hiding (params, uri) import Data.ByteString.Conversion import Data.Domain @@ -91,7 +91,7 @@ oauthAPI = registerOAuthClient :: OAuthClientConfig -> (Handler r) OAuthClientCredentials registerOAuthClient (OAuthClientConfig name uri) = do - unlessM (Opt.setOAuthEnabled <$> view settingsLens) $ throwStd $ errorToWai @'OAuthFeatureDisabled + guardOAuthEnabled credentials@(OAuthClientCredentials cid secret) <- OAuthClientCredentials <$> randomId <*> createSecret safeSecret <- liftIO $ hashClientSecret secret lift $ wrapClient $ insertOAuthClient cid name uri safeSecret @@ -108,7 +108,7 @@ rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 getOAuthClientById :: OAuthClientId -> (Handler r) OAuthClient getOAuthClientById cid = do - unlessM (Opt.setOAuthEnabled <$> view settingsLens) $ throwStd $ errorToWai @'OAuthFeatureDisabled + guardOAuthEnabled mClient <- lift $ wrapClient $ lookupOauthClient cid maybe (throwStd $ errorToWai @'OAuthClientNotFound) pure mClient @@ -125,9 +125,15 @@ deleteOAuthClient cid = do getOAuthClient :: Local UserId -> OAuthClientId -> (Handler r) (Maybe OAuthClient) getOAuthClient _ cid = do - unlessM (Opt.setOAuthEnabled <$> view settingsLens) $ throwStd $ errorToWai @'OAuthFeatureDisabled + guardOAuthEnabled lift $ wrapClient $ lookupOauthClient cid +guardOAuthEnabled :: Handler r () +guardOAuthEnabled = do + settings <- asks (.settings) + unless (Opt.oAuthEnabled settings) do + throwStd (errorToWai @'OAuthFeatureDisabled) + createNewOAuthAuthorizationCode :: Local UserId -> CreateOAuthAuthorizationCodeRequest -> (Handler r) CreateOAuthCodeResponse createNewOAuthAuthorizationCode luid code = do runExceptT (validateAndCreateAuthorizationCode luid code) >>= \case @@ -177,7 +183,7 @@ data CreateNewOAuthCodeError validateAndCreateAuthorizationCode :: Local UserId -> CreateOAuthAuthorizationCodeRequest -> ExceptT CreateNewOAuthCodeError (Handler r) OAuthAuthorizationCode validateAndCreateAuthorizationCode luid@(tUnqualified -> uid) (CreateOAuthAuthorizationCodeRequest cid scope responseType redirectUrl _state _ chal) = do - failWithM CreateNewOAuthCodeErrorFeatureDisabled (assertMay . Opt.setOAuthEnabled <$> view settingsLens) + failWithM CreateNewOAuthCodeErrorFeatureDisabled (assertMay . Opt.oAuthEnabled <$> asks (.settings)) failWith CreateNewOAuthCodeErrorUnsupportedResponseType (assertMay $ responseType == OAuthResponseTypeCode) client <- failWithM CreateNewOAuthCodeErrorClientNotFound $ getOAuthClient luid cid failWith CreateNewOAuthCodeErrorRedirectUrlMissMatch (assertMay $ client.redirectUrl == redirectUrl) @@ -186,13 +192,13 @@ validateAndCreateAuthorizationCode luid@(tUnqualified -> uid) (CreateOAuthAuthor mkAuthorizationCode :: (Handler r) OAuthAuthorizationCode mkAuthorizationCode = do oauthCode <- OAuthAuthorizationCode <$> rand32Bytes - ttl <- Opt.setOAuthAuthorizationCodeExpirationTimeSecs <$> view settingsLens + ttl <- Opt.oAuthAuthorizationCodeExpirationTimeSecs <$> asks (.settings) lift $ wrapClient $ insertOAuthAuthorizationCode ttl oauthCode cid uid scope redirectUrl chal pure oauthCode createAccessTokenWith :: (Member Now r, Member Jwk r) => Either OAuthAccessTokenRequest OAuthRefreshAccessTokenRequest -> (Handler r) OAuthAccessTokenResponse createAccessTokenWith req = do - unlessM (Opt.setOAuthEnabled <$> view settingsLens) $ throwStd $ errorToWai @'OAuthFeatureDisabled + guardOAuthEnabled case req of Left reqAC -> createAccessTokenWithAuthorizationCode reqAC Right reqRT -> createAccessTokenWithRefreshToken reqRT @@ -245,13 +251,13 @@ signingKey = do createAccessToken :: (Member Now r) => JWK -> UserId -> OAuthClientId -> OAuthScopes -> (Handler r) OAuthAccessTokenResponse createAccessToken key uid cid scope = do - exp <- fromIntegral . Opt.setOAuthAccessTokenExpirationTimeSecs <$> view settingsLens + exp <- fromIntegral . Opt.oAuthAccessTokenExpirationTimeSecs <$> asks (.settings) accessToken <- mkAccessToken (rid, refreshToken) <- mkRefreshToken now <- lift (liftSem Now.get) let refreshTokenInfo = OAuthRefreshTokenInfo rid cid uid scope now - refreshTokenExpiration <- Opt.setOAuthRefreshTokenExpirationTimeSecs <$> view settingsLens - maxActiveTokens <- Opt.setOAuthMaxActiveRefreshTokens <$> view settingsLens + refreshTokenExpiration <- Opt.oAuthRefreshTokenExpirationTimeSecs <$> asks (.settings) + maxActiveTokens <- Opt.oAuthMaxActiveRefreshTokens <$> asks (.settings) lift $ wrapClient $ insertOAuthRefreshToken maxActiveTokens refreshTokenExpiration refreshTokenInfo pure $ OAuthAccessTokenResponse accessToken OAuthAccessTokenTypeBearer exp refreshToken where @@ -265,7 +271,7 @@ createAccessToken key uid cid scope = do mkAccessToken :: (Member Now r) => (Handler r) OAuthAccessToken mkAccessToken = do domain <- asks (.settings.federationDomain) - exp <- fromIntegral . Opt.setOAuthAccessTokenExpirationTimeSecs <$> view settingsLens + exp <- fromIntegral . Opt.oAuthAccessTokenExpirationTimeSecs <$> asks (.settings) claims <- mkAccessTokenClaims uid domain scope exp OAuthToken <$> signAccessToken claims diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index d413d2493e2..45c8ea24aa0 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -679,7 +679,7 @@ getClientPrekeys usr clt = lift (wrapClient $ API.lookupPrekeyIds usr clt) newNonce :: UserId -> ClientId -> (Handler r) (Nonce, CacheControl) newNonce uid cid = do - ttl <- setNonceTtlSecs <$> asks (.settings) + ttl <- nonceTtlSecs <$> asks (.settings) nonce <- randomNonce lift $ wrapClient $ Nonce.insertNonce ttl uid (Id.clientToText cid) nonce pure (nonce, NoStore) diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 6f94a7e6fd7..4c994cb434f 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -74,7 +74,7 @@ import Brig.API.Error qualified as Error import Brig.API.Handler qualified as API (UserNotAllowedToJoinTeam (..)) import Brig.API.Types import Brig.API.Util -import Brig.App +import Brig.App as App import Brig.Data.Activation (ActivationEvent (..), activationErrorToRegisterError) import Brig.Data.Activation qualified as Data import Brig.Data.Client qualified as Data @@ -532,7 +532,7 @@ findTeamInvitation (Just e) c = initAccountFeatureConfig :: UserId -> (AppT r) () initAccountFeatureConfig uid = do - mStatus <- preview (settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNew . _Just) + mStatus <- preview (App.settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNew . _Just) wrapClient $ traverse_ (Data.updateFeatureConferenceCalling uid . Just) mStatus -- | 'createUser' is becoming hard to maintain, and instead of adding more case distinctions diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 098faa3ee11..28735c7817b 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -217,44 +217,43 @@ validateOptions o = _ -> pure () newEnv :: Opts -> IO Env -newEnv o = do - validateOptions o +newEnv opts = do + validateOptions opts Just md5 <- getDigestByName "MD5" Just sha256 <- getDigestByName "SHA256" Just sha512 <- getDigestByName "SHA512" - lgr <- Log.mkLogger (Opt.logLevel o) (Opt.logNetStrings o) (Opt.logFormat o) - cas <- initCassandra o lgr + lgr <- Log.mkLogger (Opt.logLevel opts) (Opt.logNetStrings opts) (Opt.logFormat opts) + cas <- initCassandra opts lgr mgr <- initHttpManager h2Mgr <- initHttp2Manager ext <- initExtGetManager - utp <- loadUserTemplates o - ptp <- loadProviderTemplates o - ttp <- loadTeamTemplates o - let branding = genTemplateBranding . Opt.templateBranding . Opt.general . Opt.emailSMS $ o - (emailAWSOpts, emailSMTP) <- emailConn lgr $ Opt.email (Opt.emailSMS o) - aws <- AWS.mkEnv lgr (Opt.aws o) emailAWSOpts mgr - zau <- initZAuth o + utp <- loadUserTemplates opts + ptp <- loadProviderTemplates opts + ttp <- loadTeamTemplates opts + let branding = genTemplateBranding . Opt.templateBranding . Opt.general . Opt.emailSMS $ opts + (emailAWSOpts, emailSMTP) <- emailConn lgr $ Opt.email (Opt.emailSMS opts) + aws <- AWS.mkEnv lgr (Opt.aws opts) emailAWSOpts mgr + zau <- initZAuth opts clock <- mkAutoUpdate defaultUpdateSettings {updateAction = getCurrentTime} w <- FS.startManagerConf $ FS.defaultConfig {FS.confWatchMode = FS.WatchModeOS} - let turnOpts = Opt.turn o + let turnOpts = Opt.turn opts turnSecret <- Text.encodeUtf8 . Text.strip <$> Text.readFile (Opt.secret turnOpts) turn <- Calling.mkTurnEnv (Opt.serversSource turnOpts) (Opt.tokenTTL turnOpts) (Opt.configTTL turnOpts) turnSecret sha512 - let sett = Opt.optSettings o - eventsQueue :: QueueEnv <- case Opt.internalEventsQueue (Opt.internalEvents o) of + eventsQueue :: QueueEnv <- case opts.internalEvents.internalEventsQueue of StompQueueOpts q -> do - stomp :: Stomp.Env <- case (o.stomp, sett.stomp) of + stomp :: Stomp.Env <- case (opts.stompOptions, opts.settings.stomp) of (Just s, Just c) -> Stomp.mkEnv s <$> initCredentials c (Just _, Nothing) -> error "STOMP is configured but 'setStomp' is not set" (Nothing, Just _) -> error "'setStomp' is present but STOMP is not configured" (Nothing, Nothing) -> error "stomp is selected for internal events, but not configured in 'setStomp', STOMP" pure (StompQueueEnv (Stomp.broker stomp) q) SqsQueueOpts q -> do - let throttleMillis = fromMaybe Opt.defSqsThrottleMillis o.optSettings.sqsThrottleMillis + let throttleMillis = fromMaybe Opt.defSqsThrottleMillis opts.settings.sqsThrottleMillis SqsQueueEnv aws throttleMillis <$> AWS.getQueueUrl (aws ^. AWS.amazonkaEnv) q - mSFTEnv <- mapM (Calling.mkSFTEnv sha512) $ Opt.sft o - prekeyLocalLock <- case Opt.randomPrekeys o of + mSFTEnv <- mapM (Calling.mkSFTEnv sha512) $ Opt.sft opts + prekeyLocalLock <- case Opt.randomPrekeys opts of Just True -> do Log.info lgr $ Log.msg (Log.val "randomPrekeys: active") Just <$> newMVar () @@ -262,20 +261,20 @@ newEnv o = do Log.info lgr $ Log.msg (Log.val "randomPrekeys: not active; using dynamoDB instead.") pure Nothing kpLock <- newMVar () - rabbitChan <- traverse (Q.mkRabbitMqChannelMVar lgr) o.rabbitmq - let allDisabledVersions = foldMap expandVersionExp sett.disabledAPIVersions - idxEnv <- mkIndexEnv o.elasticsearch lgr (Opt.galley o) mgr + rabbitChan <- traverse (Q.mkRabbitMqChannelMVar lgr) opts.rabbitmq + let allDisabledVersions = foldMap expandVersionExp opts.settings.disabledAPIVersions + idxEnv <- mkIndexEnv opts.elasticsearch lgr (Opt.galley opts) mgr pure $! Env - { cargohold = mkEndpoint $ Opt.cargohold o, - galley = mkEndpoint $ Opt.galley o, - galleyEndpoint = Opt.galley o, - gundeckEndpoint = Opt.gundeck o, - cargoholdEndpoint = Opt.cargohold o, - federator = Opt.federatorInternal o, + { cargohold = mkEndpoint $ opts.cargohold, + galley = mkEndpoint $ opts.galley, + galleyEndpoint = opts.galley, + gundeckEndpoint = opts.gundeck, + cargoholdEndpoint = opts.cargohold, + federator = opts.federatorInternal, casClient = cas, smtpEnv = emailSMTP, - emailSender = Opt.emailSender . Opt.general . Opt.emailSMS $ o, + emailSender = opts.emailSMS.general.emailSender, awsEnv = aws, -- used by `journalEvent` directly appLogger = lgr, internalEvents = (eventsQueue :: QueueEnv), @@ -287,7 +286,7 @@ newEnv o = do httpManager = mgr, http2Manager = h2Mgr, extGetManager = ext, - settings = sett, + settings = opts.settings, turnEnv = turn, sftEnv = mSFTEnv, fsWatcher = w, @@ -300,20 +299,20 @@ newEnv o = do keyPackageLocalLock = kpLock, rabbitmqChannel = rabbitChan, disabledVersions = allDisabledVersions, - enableSFTFederation = Opt.multiSFT o + enableSFTFederation = opts.multiSFT } where emailConn _ (Opt.EmailAWS aws) = pure (Just aws, Nothing) emailConn lgr (Opt.EmailSMTP s) = do - let h = Opt.smtpEndpoint s ^. host - p = Just $ fromInteger $ toInteger $ Opt.smtpEndpoint s ^. port + let h = s.smtpEndpoint.host + p = Just . fromInteger . toInteger $ s.smtpEndpoint.port smtpCredentials <- case Opt.smtpCredentials s of Just (Opt.EmailSMTPCredentials u p') -> do Just . (SMTP.Username u,) . SMTP.Password <$> initCredentials p' _ -> pure Nothing smtp <- SMTP.initSMTP lgr h p smtpCredentials (Opt.smtpConnType s) pure (Nothing, Just smtp) - mkEndpoint service = RPC.host (encodeUtf8 (service ^. host)) . RPC.port (service ^. port) $ RPC.empty + mkEndpoint service = RPC.host (encodeUtf8 service.host) . RPC.port service.port $ RPC.empty mkIndexEnv :: ElasticSearchOpts -> Logger -> Endpoint -> Manager -> IO IndexEnv mkIndexEnv esOpts logger galleyEp rpcHttpManager = do diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 6f427928974..2997eb46907 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -162,7 +162,7 @@ runBrigToIO e (AppT ma) = do let userSubsystemConfig = UserSubsystemConfig { emailVisibilityConfig = e.settings.emailVisibility, - defaultLocale = e.settings ^. to Opt.setDefaultUserLocale, + defaultLocale = Opt.defaultUserLocale e.settings, searchSameTeamOnly = fromMaybe False e.settings.searchSameTeamOnly } federationApiAccessConfig = diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index bf26066e582..cdf4cc7677b 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -115,7 +115,7 @@ data ReAuthError -- there, it was claimed properly. newAccount :: (MonadClient m, MonadReader Env m) => NewUser -> Maybe InvitationId -> Maybe TeamId -> Maybe Handle -> m (UserAccount, Maybe Password) newAccount u inv tid mbHandle = do - defLoc <- setDefaultUserLocale <$> asks (.settings) + defLoc <- defaultUserLocale <$> asks (.settings) domain <- viewFederationDomain uid <- Id <$> do @@ -152,7 +152,7 @@ newAccount u inv tid mbHandle = do newAccountInviteViaScim :: (MonadReader Env m) => UserId -> Text -> TeamId -> Maybe Locale -> Name -> EmailAddress -> m UserAccount newAccountInviteViaScim uid externalId tid locale name email = do - defLoc <- setDefaultUserLocale <$> asks (.settings) + defLoc <- defaultUserLocale <$> asks (.settings) let loc = fromMaybe defLoc locale domain <- viewFederationDomain pure (UserAccount (user domain loc) PendingInvitation) @@ -399,7 +399,7 @@ lookupAuth u = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Ident -- Skips nonexistent users. /Does not/ skip users who have been deleted. lookupUsers :: (MonadClient m, MonadReader Env m) => HavePendingInvitations -> [UserId] -> m [User] lookupUsers hpi usrs = do - loc <- setDefaultUserLocale <$> asks (.settings) + loc <- defaultUserLocale <$> asks (.settings) domain <- viewFederationDomain toUsers domain loc hpi <$> retry x1 (query usersSelect (params LocalQuorum (Identity usrs))) @@ -568,7 +568,7 @@ userRichInfoUpdate = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE r -- Conversions toUsers :: Domain -> Locale -> HavePendingInvitations -> [UserRow] -> [User] -toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp +toUsers domain defLocale havePendingInvitations = fmap mk . filter fp where fp :: UserRow -> Bool fp = @@ -624,7 +624,7 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp let ident = toIdentity activated email ssoid deleted = Just Deleted == status expiration = if status == Just Ephemeral then expires else Nothing - loc = toLocale defaultLocale (lan, con) + loc = toLocaleWithDefault defLocale (lan, con) svc = newServiceRef <$> sid <*> pid in User (Qualified uid domain) @@ -643,9 +643,9 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp (fromMaybe ManagedByWire managed_by) (fromMaybe defSupportedProtocols prots) -toLocale :: Locale -> (Maybe Language, Maybe Country) -> Locale -toLocale _ (Just l, c) = Locale l c -toLocale l _ = l + toLocaleWithDefault :: Locale -> (Maybe Language, Maybe Country) -> Locale + toLocaleWithDefault _ (Just l, c) = Locale l c + toLocaleWithDefault l _ = l -- | Construct a 'UserIdentity'. -- diff --git a/services/brig/src/Brig/Index/Options.hs b/services/brig/src/Brig/Index/Options.hs index c0fe469f0ff..f9f382c0043 100644 --- a/services/brig/src/Brig/Index/Options.hs +++ b/services/brig/src/Brig/Index/Options.hs @@ -113,10 +113,10 @@ makeLenses ''ReindexFromAnotherIndexSettings toCassandraOpts :: CassandraSettings -> CassandraOpts toCassandraOpts cas = CassandraOpts - { _endpoint = Endpoint (Text.pack (cas ^. cHost)) (cas ^. cPort), - _keyspace = C.unKeyspace (cas ^. cKeyspace), - _filterNodesByDatacentre = Nothing, - _tlsCa = cas ^. cTlsCa + { endpoint = Endpoint (Text.pack (cas ^. cHost)) (cas ^. cPort), + keyspace = C.unKeyspace (cas ^. cKeyspace), + filterNodesByDatacentre = Nothing, + tlsCa = cas ^. cTlsCa } mkCreateIndexSettings :: ElasticSettings -> CreateIndexSettings diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 98de6f404bd..7aeacd70efa 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -28,7 +28,7 @@ import Brig.Queue.Types (QueueOpts (..)) import Brig.User.Auth.Cookie.Limit import Brig.ZAuth qualified as ZAuth import Control.Applicative -import Control.Lens as Lens hiding (Level, element, enum) +import Control.Lens hiding (Level, element, enum) import Data.Aeson import Data.Aeson.Types qualified as A import Data.Char qualified as Char @@ -141,14 +141,12 @@ data EmailSMTPOpts = EmailSMTPOpts instance FromJSON EmailSMTPOpts data StompOpts = StompOpts - { stompHost :: !Text, - stompPort :: !Int, - stompTls :: !Bool + { host :: !Text, + port :: !Int, + tls :: !Bool } deriving (Show, Generic) -instance FromJSON StompOpts - data InternalEventsOpts = InternalEventsOpts { internalEventsQueue :: !QueueOpts } @@ -397,7 +395,7 @@ data Opts = Opts -- | Enable Random Prekey Strategy randomPrekeys :: !(Maybe Bool), -- | STOMP broker settings - stomp :: !(Maybe StompOpts), + stompOptions :: !(Maybe StompOpts), -- Email & SMS -- | Email and SMS settings @@ -429,7 +427,7 @@ data Opts = Opts -- | SFT Settings sft :: !(Maybe SFTOptions), -- | Runtime settings - optSettings :: !Settings + settings :: !Settings } deriving (Show, Generic) @@ -605,77 +603,74 @@ instance FromJSON ImplicitNoFederationRestriction where FederationDomainConfig domain searchPolicy FederationRestrictionAllowAll ) -defaultTemplateLocale :: Locale -defaultTemplateLocale = Locale (Language EN) Nothing - -defaultUserLocale :: Locale -defaultUserLocale = defaultTemplateLocale +defaultLocale :: Locale +defaultLocale = Locale (Language EN) Nothing -setDefaultUserLocale :: Settings -> Locale -setDefaultUserLocale = fromMaybe defaultUserLocale . defaultUserLocaleInternal +defaultUserLocale :: Settings -> Locale +defaultUserLocale = fromMaybe defaultLocale . defaultUserLocaleInternal -defVerificationTimeout :: Code.Timeout -defVerificationTimeout = Code.Timeout (60 * 10) -- 10 minutes +defaultTemplateLocale :: Settings -> Locale +defaultTemplateLocale = fromMaybe defaultLocale . defaultTemplateLocaleInternal verificationTimeout :: Settings -> Code.Timeout verificationTimeout = fromMaybe defVerificationTimeout . verificationCodeTimeoutInternal - -setDefaultTemplateLocale :: Settings -> Locale -setDefaultTemplateLocale = fromMaybe defaultTemplateLocale . defaultTemplateLocaleInternal - -def2FACodeGenerationDelaySecs :: Int -def2FACodeGenerationDelaySecs = 5 * 60 -- 5 minutes + where + defVerificationTimeout :: Code.Timeout + defVerificationTimeout = Code.Timeout (60 * 10) -- 10 minutes twoFACodeGenerationDelaySecs :: Settings -> Int twoFACodeGenerationDelaySecs = fromMaybe def2FACodeGenerationDelaySecs . twoFACodeGenerationDelaySecsInternal + where + def2FACodeGenerationDelaySecs :: Int + def2FACodeGenerationDelaySecs = 5 * 60 -- 5 minutes -defaultNonceTtlSecs :: NonceTtlSecs -defaultNonceTtlSecs = NonceTtlSecs $ 5 * 60 -- 5 minutes - -setNonceTtlSecs :: Settings -> NonceTtlSecs -setNonceTtlSecs = fromMaybe defaultNonceTtlSecs . nonceTtlSecsInternal - -defaultDpopMaxSkewSecs :: Word16 -defaultDpopMaxSkewSecs = 1 +nonceTtlSecs :: Settings -> NonceTtlSecs +nonceTtlSecs = fromMaybe defaultNonceTtlSecs . nonceTtlSecsInternal + where + defaultNonceTtlSecs :: NonceTtlSecs + defaultNonceTtlSecs = NonceTtlSecs $ 5 * 60 -- 5 minutes setDpopMaxSkewSecs :: Settings -> Word16 setDpopMaxSkewSecs = fromMaybe defaultDpopMaxSkewSecs . dpopMaxSkewSecsInternal - -defaultDpopTokenExpirationTimeSecs :: Word64 -defaultDpopTokenExpirationTimeSecs = 30 - -setDpopTokenExpirationTimeSecs :: Settings -> Word64 -setDpopTokenExpirationTimeSecs = fromMaybe defaultDpopTokenExpirationTimeSecs . dpopTokenExpirationTimeSecsInternal - -defaultOAuthAccessTokenExpirationTimeSecs :: Word64 -defaultOAuthAccessTokenExpirationTimeSecs = 60 * 60 * 24 * 7 * 3 -- 3 weeks - -setOAuthAccessTokenExpirationTimeSecs :: Settings -> Word64 -setOAuthAccessTokenExpirationTimeSecs = fromMaybe defaultOAuthAccessTokenExpirationTimeSecs . oAuthAccessTokenExpirationTimeSecsInternal - -defaultOAuthAuthorizationCodeExpirationTimeSecs :: Word64 -defaultOAuthAuthorizationCodeExpirationTimeSecs = 300 -- 5 minutes - -setOAuthAuthorizationCodeExpirationTimeSecs :: Settings -> Word64 -setOAuthAuthorizationCodeExpirationTimeSecs = fromMaybe defaultOAuthAuthorizationCodeExpirationTimeSecs . oAuthAuthorizationCodeExpirationTimeSecsInternal - -defaultOAuthEnabled :: Bool -defaultOAuthEnabled = False - -setOAuthEnabled :: Settings -> Bool -setOAuthEnabled = fromMaybe defaultOAuthEnabled . oAuthEnabledInternal - -defaultOAuthRefreshTokenExpirationTimeSecs :: Word64 -defaultOAuthRefreshTokenExpirationTimeSecs = 60 * 60 * 24 * 7 * 4 * 6 -- 24 weeks - -setOAuthRefreshTokenExpirationTimeSecs :: Settings -> Word64 -setOAuthRefreshTokenExpirationTimeSecs = fromMaybe defaultOAuthRefreshTokenExpirationTimeSecs . oAuthRefreshTokenExpirationTimeSecsInternal - -defaultOAuthMaxActiveRefreshTokens :: Word32 -defaultOAuthMaxActiveRefreshTokens = 10 - -setOAuthMaxActiveRefreshTokens :: Settings -> Word32 -setOAuthMaxActiveRefreshTokens = fromMaybe defaultOAuthMaxActiveRefreshTokens . oAuthMaxActiveRefreshTokensInternal + where + defaultDpopMaxSkewSecs :: Word16 + defaultDpopMaxSkewSecs = 1 + +dpopTokenExpirationTimeSecs :: Settings -> Word64 +dpopTokenExpirationTimeSecs = fromMaybe defaultDpopTokenExpirationTimeSecs . dpopTokenExpirationTimeSecsInternal + where + defaultDpopTokenExpirationTimeSecs :: Word64 + defaultDpopTokenExpirationTimeSecs = 30 + +oAuthAccessTokenExpirationTimeSecs :: Settings -> Word64 +oAuthAccessTokenExpirationTimeSecs = fromMaybe defaultOAuthAccessTokenExpirationTimeSecs . oAuthAccessTokenExpirationTimeSecsInternal + where + defaultOAuthAccessTokenExpirationTimeSecs :: Word64 + defaultOAuthAccessTokenExpirationTimeSecs = 60 * 60 * 24 * 7 * 3 -- 3 weeks + +oAuthAuthorizationCodeExpirationTimeSecs :: Settings -> Word64 +oAuthAuthorizationCodeExpirationTimeSecs = fromMaybe defaultOAuthAuthorizationCodeExpirationTimeSecs . oAuthAuthorizationCodeExpirationTimeSecsInternal + where + defaultOAuthAuthorizationCodeExpirationTimeSecs :: Word64 + defaultOAuthAuthorizationCodeExpirationTimeSecs = 300 -- 5 minutes + +oAuthEnabled :: Settings -> Bool +oAuthEnabled = fromMaybe defaultOAuthEnabled . oAuthEnabledInternal + where + defaultOAuthEnabled :: Bool + defaultOAuthEnabled = False + +oAuthRefreshTokenExpirationTimeSecs :: Settings -> Word64 +oAuthRefreshTokenExpirationTimeSecs = fromMaybe defaultOAuthRefreshTokenExpirationTimeSecs . oAuthRefreshTokenExpirationTimeSecsInternal + where + defaultOAuthRefreshTokenExpirationTimeSecs :: Word64 + defaultOAuthRefreshTokenExpirationTimeSecs = 60 * 60 * 24 * 7 * 4 * 6 -- 24 weeks + +oAuthMaxActiveRefreshTokens :: Settings -> Word32 +oAuthMaxActiveRefreshTokens = fromMaybe defaultOAuthMaxActiveRefreshTokens . oAuthMaxActiveRefreshTokensInternal + where + defaultOAuthMaxActiveRefreshTokens :: Word32 + defaultOAuthMaxActiveRefreshTokens = 10 -- | The analog to `FeatureFlags`. At the moment, only status flags for -- conferenceCalling are stored. @@ -816,14 +811,14 @@ defSrvDiscoveryIntervalSeconds = secondsToDiffTime 10 defSftListLength :: Range 1 100 Int defSftListLength = unsafeRange 5 +-- | Convert a word to title case by capitalising the first letter +capitalise :: String -> String +capitalise [] = [] +capitalise (c : cs) = toUpper c : cs + instance FromJSON Settings where parseJSON = genericParseJSON customOptions where - -- Convert a word to title case by capitalising the first letter - capitalise :: String -> String - capitalise [] = [] - capitalise (c : cs) = toUpper c : cs - customOptions = defaultOptions { fieldLabelModifier = \case @@ -842,17 +837,26 @@ instance FromJSON Settings where other -> "set" <> capitalise other } -instance FromJSON Opts - --- TODO: Does it make sense to generate lens'es for all? -Lens.makeLensesFor - [ ("optSettings", "optionSettings"), - ("elasticsearch", "elasticsearchL"), - ("sft", "sftL"), - ("turn", "turnL"), - ("multiSFT", "multiSFTL") - ] - ''Opts +instance FromJSON Opts where + parseJSON = genericParseJSON customOptions + where + customOptions = + defaultOptions + { fieldLabelModifier = \case + "settings" -> "optSettings" + "stompOptions" -> "stomp" + other -> other + } + +instance FromJSON StompOpts where + parseJSON = genericParseJSON customOptions + where + customOptions = + defaultOptions + { fieldLabelModifier = \a -> "stom" <> capitalise a + } + +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Opts makeLensesWith (lensRules & lensField .~ suffixNamer) ''Settings diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 2161540a22d..8630afafef4 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -707,7 +707,7 @@ addBot zuid zcon cid add = do let botReq = NewBotRequest bid bcl busr bcnv btk bloc rs <- RPC.createBot scon botReq !>> StdError . serviceError -- Insert the bot user and client - locale <- Opt.setDefaultUserLocale <$> asks (.settings) + locale <- Opt.defaultUserLocale <$> asks (.settings) let name = fromMaybe (serviceProfileName svp) (Ext.rsNewBotName rs) let assets = fromMaybe (serviceProfileAssets svp) (Ext.rsNewBotAssets rs) let colour = fromMaybe defaultAccentId (Ext.rsNewBotColour rs) diff --git a/services/brig/src/Brig/Provider/Template.hs b/services/brig/src/Brig/Provider/Template.hs index 5e74e2f4fae..2c50f22fcd6 100644 --- a/services/brig/src/Brig/Provider/Template.hs +++ b/services/brig/src/Brig/Provider/Template.hs @@ -115,13 +115,13 @@ loadProviderTemplates o = readLocalesDir defLocale (templateDir gOptions) "provi <*> readText fp "email/sender.txt" ) where - maybeUrl = fromByteString $ encodeUtf8 $ homeUrl pOptions - gOptions = general $ emailSMS o - pOptions = provider $ emailSMS o - defLocale = setDefaultTemplateLocale (optSettings o) - readTemplate = readTemplateWithDefault (templateDir gOptions) defLocale "provider" - readText = readTextWithDefault (templateDir gOptions) defLocale "provider" + maybeUrl = fromByteString . encodeUtf8 $ pOptions.homeUrl + gOptions = o.emailSMS.general + pOptions = o.emailSMS.provider + defLocale = defaultTemplateLocale o.settings + readTemplate = readTemplateWithDefault gOptions.templateDir defLocale "provider" + readText = readTextWithDefault gOptions.templateDir defLocale "provider" -- URL templates - activationUrl' = template $ providerActivationUrl pOptions - approvalUrl' = template $ approvalUrl pOptions - pwResetUrl' = template $ providerPwResetUrl pOptions + activationUrl' = template pOptions.providerActivationUrl + approvalUrl' = template pOptions.approvalUrl + pwResetUrl' = template pOptions.providerPwResetUrl diff --git a/services/brig/src/Brig/Queue/Stomp.hs b/services/brig/src/Brig/Queue/Stomp.hs index d6d8c3abfca..631f790013a 100644 --- a/services/brig/src/Brig/Queue/Stomp.hs +++ b/services/brig/src/Brig/Queue/Stomp.hs @@ -76,10 +76,10 @@ mkEnv o cred = Env { broker = Broker - { host = Opts.stompHost o, - port = Opts.stompPort o, + { host = o.host, + port = o.port, auth = Just cred, - tls = Opts.stompTls o + tls = o.tls } } diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 05d7a287a0a..7f299c65195 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -36,7 +36,7 @@ import Brig.Queue qualified as Queue import Brig.Version import Control.Concurrent.Async qualified as Async import Control.Exception.Safe (catchAny) -import Control.Lens ((.~), (^.)) +import Control.Lens ((.~)) import Control.Monad.Catch (MonadCatch, finally) import Control.Monad.Random (randomRIO) import Data.Aeson qualified as Aeson @@ -79,8 +79,8 @@ import Wire.UserStore -- thread terminates for any reason. -- https://github.com/zinfra/backend-issues/issues/1647 run :: Opts -> IO () -run o = withTracer \tracer -> do - (app, e) <- mkApp o +run opts = withTracer \tracer -> do + (app, e) <- mkApp opts s <- Server.newSettings (server e) internalEventListener <- Async.async $ @@ -88,7 +88,7 @@ run o = withTracer \tracer -> do wrapHttpClient $ Queue.listen e.internalEvents $ liftIO . runBrigToIO e . liftSem . Internal.onEvent - let throttleMillis = fromMaybe defSqsThrottleMillis o.optSettings.sqsThrottleMillis + let throttleMillis = fromMaybe defSqsThrottleMillis opts.settings.sqsThrottleMillis emailListener <- for e.awsEnv._sesQueue $ \q -> Async.async $ AWS.execute e.awsEnv $ @@ -105,12 +105,12 @@ run o = withTracer \tracer -> do <> turnDiscovery closeEnv e where - endpoint' = brig o - server e = defaultServer (unpack $ endpoint' ^. host) (endpoint' ^. port) e.appLogger + brig = opts.brig + server e = defaultServer (unpack $ brig.host) brig.port e.appLogger mkApp :: Opts -> IO (Wai.Application, Env) -mkApp o = do - e <- newEnv o +mkApp opts = do + e <- newEnv opts otelMiddleware <- Otel.newOpenTelemetryWaiMiddleware pure (otelMiddleware . middleware e $ servantApp e, e) where @@ -126,18 +126,18 @@ mkApp o = do . catchErrors e.appLogger defaultRequestIdHeaderName servantApp :: Env -> Wai.Application - servantApp e0 req cont = do + servantApp e req cont = do let rid = getRequestId defaultRequestIdHeaderName req - let e = requestIdLens .~ rid $ e0 - let localDomain = e.settings.federationDomain + let env = requestIdLens .~ rid $ e + let localDomain = env.settings.federationDomain Servant.serveWithContext (Proxy @ServantCombinedAPI) (customFormatters :. localDomain :. Servant.EmptyContext) ( docsAPI - :<|> hoistServerWithDomain @BrigAPI (toServantHandler e) servantSitemap - :<|> hoistServerWithDomain @IAPI.API (toServantHandler e) IAPI.servantSitemap - :<|> hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap - :<|> hoistServerWithDomain @VersionAPI (toServantHandler e) versionAPI + :<|> hoistServerWithDomain @BrigAPI (toServantHandler env) servantSitemap + :<|> hoistServerWithDomain @IAPI.API (toServantHandler env) IAPI.servantSitemap + :<|> hoistServerWithDomain @FederationAPI (toServantHandler env) federationSitemap + :<|> hoistServerWithDomain @VersionAPI (toServantHandler env) versionAPI ) req cont diff --git a/services/brig/src/Brig/Team/Template.hs b/services/brig/src/Brig/Team/Template.hs index 129ca30ef37..6d8c28e2110 100644 --- a/services/brig/src/Brig/Team/Template.hs +++ b/services/brig/src/Brig/Team/Template.hs @@ -98,10 +98,10 @@ loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \ <*> readText fp "email/sender.txt" ) where - gOptions = general (emailSMS o) - tOptions = team (emailSMS o) - tUrl = template $ tInvitationUrl tOptions - tExistingUrl = template $ tExistingUserInvitationUrl tOptions - defLocale = setDefaultTemplateLocale (optSettings o) + gOptions = o.emailSMS.general + tOptions = o.emailSMS.team + tUrl = template tOptions.tInvitationUrl + tExistingUrl = template tOptions.tExistingUserInvitationUrl + defLocale = defaultTemplateLocale o.settings readTemplate = readTemplateWithDefault (templateDir gOptions) defLocale "team" readText = readTextWithDefault (templateDir gOptions) defLocale "team" diff --git a/services/brig/src/Brig/User/Template.hs b/services/brig/src/Brig/User/Template.hs index 00aa5a5b6d2..36436027782 100644 --- a/services/brig/src/Brig/User/Template.hs +++ b/services/brig/src/Brig/User/Template.hs @@ -146,17 +146,17 @@ loadUserTemplates o = readLocalesDir defLocale templateDir "user" $ \fp -> <*> readText fp "email/sender.txt" ) where - gOptions = Opt.general $ Opt.emailSMS o - uOptions = Opt.user $ Opt.emailSMS o - tOptions = Opt.team $ Opt.emailSMS o - emailSender = Opt.emailSender gOptions - smsSender = Opt.smsSender gOptions - smsActivationUrl = template $ Opt.smsActivationUrl uOptions - activationUrl = template $ Opt.activationUrl uOptions - teamActivationUrl = template $ Opt.tActivationUrl tOptions - passwordResetUrl = template $ Opt.passwordResetUrl uOptions - deletionUserUrl = template $ Opt.deletionUrl uOptions - defLocale = Opt.setDefaultTemplateLocale (Opt.optSettings o) - templateDir = Opt.templateDir gOptions + gOptions = o.emailSMS.general + uOptions = o.emailSMS.user + tOptions = o.emailSMS.team + emailSender = gOptions.emailSender + smsSender = gOptions.smsSender + smsActivationUrl = template uOptions.smsActivationUrl + activationUrl = template uOptions.activationUrl + teamActivationUrl = template tOptions.tActivationUrl + passwordResetUrl = template uOptions.passwordResetUrl + deletionUserUrl = template uOptions.deletionUrl + defLocale = Opt.defaultTemplateLocale o.settings + templateDir = gOptions.templateDir readTemplate = readTemplateWithDefault templateDir defLocale "user" readText = readTextWithDefault templateDir defLocale "user" diff --git a/services/brig/test/integration/API/Calling.hs b/services/brig/test/integration/API/Calling.hs index 7867a5afe9f..ce9b2bebbf6 100644 --- a/services/brig/test/integration/API/Calling.hs +++ b/services/brig/test/integration/API/Calling.hs @@ -102,7 +102,7 @@ testSFT b opts = do "when SFT discovery is not enabled, sft_servers shouldn't be returned" Nothing (cfg ^. rtcConfSftServers) - withSettingsOverrides (opts & Opts.sftL ?~ Opts.SFTOptions "integration-tests.zinfra.io" Nothing (Just 0.001) Nothing Nothing) $ do + withSettingsOverrides (opts & Opts.sftLens ?~ Opts.SFTOptions "integration-tests.zinfra.io" Nothing (Just 0.001) Nothing Nothing) $ do cfg1 <- retryWhileN 10 (isNothing . view rtcConfSftServers) (getTurnConfigurationV2 uid b) -- These values are controlled by https://github.com/zinfra/cailleach/tree/77ca2d23cf2959aa183dd945d0a0b13537a8950d/environments/dns-integration-tests let Right server1 = mkHttpsUrl =<< first show (parseURI laxURIParserOptions "https://sft01.integration-tests.zinfra.io:443") @@ -116,7 +116,7 @@ testSFT b opts = do testSFTUnavailable :: Brig -> Opts.Opts -> String -> Http () testSFTUnavailable b opts domain = do uid <- userId <$> randomUser b - withSettingsOverrides (opts {Opts.optSettings = (Opts.optSettings opts) {Opts.sftStaticUrl = fromByteString (cs domain), Opts.sftListAllServers = Just Opts.ListAllSFTServers}}) $ do + withSettingsOverrides (opts {Opts.settings = (Opts.settings opts) {Opts.sftStaticUrl = fromByteString (cs domain), Opts.sftListAllServers = Just Opts.ListAllSFTServers}}) $ do cfg <- getTurnConfigurationV2 uid b liftIO $ do assertEqual @@ -178,7 +178,7 @@ testCallsConfigSRV b opts = do uid <- userId <$> randomUser b let dnsOpts = Opts.TurnSourceDNS (Opts.TurnDnsOpts "integration-tests.zinfra.io" (Just 0.5)) config <- - withSettingsOverrides (opts & Opts.turnL . Opts.serversSourceLens .~ dnsOpts) $ + withSettingsOverrides (opts & Opts.turnLens . Opts.serversSourceLens .~ dnsOpts) $ responseJsonError =<< ( retryWhileN 10 (\r -> statusCode r /= 200) (getTurnConfiguration "" uid b) randomUser b let dnsOpts = Opts.TurnSourceDNS (Opts.TurnDnsOpts "integration-tests.zinfra.io" (Just 0.5)) config <- - withSettingsOverrides (opts & Opts.turnL . Opts.serversSourceLens .~ dnsOpts) $ + withSettingsOverrides (opts & Opts.turnLens . Opts.serversSourceLens .~ dnsOpts) $ responseJsonError =<< ( retryWhileN 10 (\r -> statusCode r /= 200) (getTurnConfiguration "v2" uid b) Opt.Opts -> Opt.Opts allowFullSearch domain opts = - opts & Opt.optionSettings . Opt.federationDomainConfigsLens ?~ [Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domain FullSearch FederationRestrictionAllowAll] + opts & Opt.settingsLens . Opt.federationDomainConfigsLens ?~ [Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domain FullSearch FederationRestrictionAllowAll] testSearchSuccess :: Opt.Opts -> Brig -> Http () testSearchSuccess opts brig = do @@ -176,7 +176,7 @@ testSearchRestrictions opts brig = do let opts' = opts - & Opt.optionSettings . Opt.federationDomainConfigsLens + & Opt.settingsLens . Opt.federationDomainConfigsLens ?~ [ Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainNoSearch NoSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainExactHandle ExactHandleSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainFullSearch FullSearch FederationRestrictionAllowAll @@ -220,7 +220,7 @@ testGetUserByHandleRestrictions opts brig = do let opts' = opts - & Opt.optionSettings . Opt.federationDomainConfigsLens + & Opt.settingsLens . Opt.federationDomainConfigsLens ?~ [ Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainNoSearch NoSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainExactHandle ExactHandleSearch FederationRestrictionAllowAll, Opt.ImplicitNoFederationRestriction $ FD.FederationDomainConfig domainFullSearch FullSearch FederationRestrictionAllowAll diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 4b5219208f2..dc54cd254dd 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -192,10 +192,10 @@ testCreateAccessTokenSuccess opts brig = do createOAuthAccessToken' brig accessTokenRequest !!! do const 404 === statusCode const (Just "not-found") === fmap Error.label . responseJsonMaybe - k <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") + k <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") verifiedOrError <- liftIO $ verify k (unOAuthToken $ resp.accessToken) verifiedOrErrorWithotherKey <- liftIO $ verify badKey (unOAuthToken $ resp.accessToken) - let expectedDomain = domainText opts.optSettings.federationDomain + let expectedDomain = domainText opts.settings.federationDomain liftIO $ do isRight verifiedOrError @?= True isLeft verifiedOrErrorWithotherKey @?= True @@ -247,7 +247,7 @@ testCreateAccessTokenWrongUrl brig = do testCreateAccessTokenExpiredCode :: Opt.Opts -> Brig -> Http () testCreateAccessTokenExpiredCode opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthAuthorizationCodeExpirationTimeSecsInternalLens ?~ 1) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthAuthorizationCodeExpirationTimeSecsInternalLens ?~ 1) $ do uid <- randomId let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] @@ -297,14 +297,14 @@ testCreateAccessTokenWrongCodeVerifier brig = do testGetOAuthClientInfoAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testGetOAuthClientInfoAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthEnabledInternalLens ?~ False) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthEnabledInternalLens ?~ False) $ do cid <- randomId uid <- randomId getOAuthClientInfo' brig uid cid !!! assertAccessDenied testCreateCodeOAuthClientAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testCreateCodeOAuthClientAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthEnabledInternalLens ?~ False) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthEnabledInternalLens ?~ False) $ do cid <- randomId uid <- randomId state <- UUID.toText <$> liftIO nextRandom @@ -318,7 +318,7 @@ testCreateCodeOAuthClientAccessDeniedWhenDisabled opts brig = testCreateAccessTokenAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testCreateAccessTokenAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthEnabledInternalLens ?~ False) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthEnabledInternalLens ?~ False) $ do cid <- randomId let code = OAuthAuthorizationCode $ encodeBase16 "eb32eb9e2aa36c081c89067dddf81bce83c1c57e0b74cfb14c9f026f145f2b1f" let url = mkUrl "https://example.com" @@ -333,13 +333,13 @@ testRefreshAccessTokenAccessDeniedWhenDisabled opts brig = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig uid scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthEnabledInternalLens ?~ False) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthEnabledInternalLens ?~ False) $ do let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid resp.refreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! assertAccessDenied testRegisterOAuthClientAccessDeniedWhenDisabled :: Opt.Opts -> Brig -> Http () testRegisterOAuthClientAccessDeniedWhenDisabled opts brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthEnabledInternalLens ?~ False) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthEnabledInternalLens ?~ False) $ do let newOAuthClient = newOAuthClientRequestBody "E Corp" "https://example.com" registerNewOAuthClient' brig newOAuthClient !!! assertAccessDenied @@ -412,7 +412,7 @@ testAccessResourceInvalidSignature opts brig nginz = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") claimSet <- fromRight (error "token invalid") <$> liftIO (verify key (unOAuthToken $ resp.accessToken)) tokenSignedWithotherKey <- signAccessToken badKey claimSet get (nginz . paths ["self"] . authHeader (OAuthToken tokenSignedWithotherKey)) !!! do @@ -421,9 +421,9 @@ testAccessResourceInvalidSignature opts brig nginz = do testRefreshTokenMaxActiveTokens :: Opts -> C.ClientState -> Brig -> Http () testRefreshTokenMaxActiveTokens opts db brig = - withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthMaxActiveRefreshTokensInternalLens ?~ 2) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthMaxActiveRefreshTokensInternalLens ?~ 2) $ do uid <- randomId - jwk <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") + jwk <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [WriteConversations, WriteConversationsCode] let delayOneSec = @@ -501,7 +501,7 @@ testRefreshTokenWrongSignature opts brig = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ do claims <- verifyRefreshToken key (unOAuthToken $ resp.refreshToken) OAuthToken <$> signRefreshToken badKey claims @@ -516,7 +516,7 @@ testRefreshTokenNoTokenId opts brig = do let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] (cid, _) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ OAuthToken <$> signRefreshToken key emptyClaimsSet let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid badRefreshToken refreshOAuthAccessToken' brig refreshAccessTokenRequest !!! do @@ -531,7 +531,7 @@ testRefreshTokenNonExistingId opts brig = do (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest - key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.optSettings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") + key <- liftIO $ readJwk (fromMaybe "path to jwk not set" opts.settings.oAuthJwkKeyPair) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ OAuthToken <$> do @@ -575,7 +575,7 @@ testRefreshTokenWrongGrantType brig = do testRefreshTokenExpiredToken :: Opts -> Brig -> Http () testRefreshTokenExpiredToken opts brig = -- overriding settings and set refresh token to expire in 2 seconds - withSettingsOverrides (opts & Opt.optionSettings . Opt.oAuthRefreshTokenExpirationTimeSecsInternalLens ?~ 2) $ do + withSettingsOverrides (opts & Opt.settingsLens . Opt.oAuthRefreshTokenExpirationTimeSecsInternalLens ?~ 2) $ do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] diff --git a/services/brig/test/integration/API/Search.hs b/services/brig/test/integration/API/Search.hs index 7a6bbc64c46..14832d5e377 100644 --- a/services/brig/test/integration/API/Search.hs +++ b/services/brig/test/integration/API/Search.hs @@ -520,7 +520,7 @@ testSearchSameTeamOnly brig opts = do nonTeamMember <- setRandomHandle brig nonTeamMember' (_, _, [teamMember]) <- createPopulatedBindingTeam brig 1 refreshIndex brig - let newOpts = opts & Opt.optionSettings . Opt.searchSameTeamOnlyLens ?~ True + let newOpts = opts & Opt.settingsLens . Opt.searchSameTeamOnlyLens ?~ True withSettingsOverrides newOpts $ do assertCan'tFind brig (userId teamMember) (userQualifiedId nonTeamMember) (fromName (userDisplayName nonTeamMember)) let nonTeamMemberHandle = fromMaybe (error "nonTeamMember must have a handle") (userHandle nonTeamMember) @@ -613,8 +613,8 @@ testMigrationToNewIndex opts brig = do withOldESProxy opts $ \oldESUrl oldESIndex -> do let optsOldIndex = opts - & Opt.elasticsearchL . Opt.indexLens .~ (ES.IndexName oldESIndex) - & Opt.elasticsearchL . Opt.urlLens .~ (ES.Server oldESUrl) + & Opt.elasticsearchLens . Opt.indexLens .~ (ES.IndexName oldESIndex) + & Opt.elasticsearchLens . Opt.urlLens .~ (ES.Server oldESUrl) -- Phase 1: Using old index only (phase1NonTeamUser, teamOwner, phase1TeamUser1, phase1TeamUser2, tid) <- withSettingsOverrides optsOldIndex $ do nonTeamUser <- randomUser brig @@ -624,10 +624,10 @@ testMigrationToNewIndex opts brig = do -- Phase 2: Using old index for search, writing to both indices, migrations have not run let phase2OptsWhile = optsOldIndex - & Opt.elasticsearchL . Opt.additionalWriteIndexLens ?~ (opts ^. Opt.elasticsearchL . Opt.indexLens) - & Opt.elasticsearchL . Opt.additionalWriteIndexUrlLens ?~ (opts ^. Opt.elasticsearchL . Opt.urlLens) - & Opt.elasticsearchL . Opt.additionalCaCertLens .~ (opts ^. Opt.elasticsearchL . Opt.caCertLens) - & Opt.elasticsearchL . Opt.additionalInsecureSkipVerifyTlsLens .~ (opts ^. Opt.elasticsearchL . Opt.insecureSkipVerifyTlsLens) + & Opt.elasticsearchLens . Opt.additionalWriteIndexLens ?~ (opts ^. Opt.elasticsearchLens . Opt.indexLens) + & Opt.elasticsearchLens . Opt.additionalWriteIndexUrlLens ?~ (opts ^. Opt.elasticsearchLens . Opt.urlLens) + & Opt.elasticsearchLens . Opt.additionalCaCertLens .~ (opts ^. Opt.elasticsearchLens . Opt.caCertLens) + & Opt.elasticsearchLens . Opt.additionalInsecureSkipVerifyTlsLens .~ (opts ^. Opt.elasticsearchLens . Opt.insecureSkipVerifyTlsLens) (phase2NonTeamUser, phase2TeamUser) <- withSettingsOverrides phase2OptsWhile $ do phase2NonTeamUser <- randomUser brig phase2TeamUser <- inviteAndRegisterUser teamOwner tid brig @@ -652,7 +652,7 @@ testMigrationToNewIndex opts brig = do assertCanFindByName brig phase1TeamUser1 phase2TeamUser -- Run Migrations - let newIndexName = opts ^. Opt.elasticsearchL . Opt.indexLens + let newIndexName = opts ^. Opt.elasticsearchLens . Opt.indexLens taskNodeId <- assertRight =<< runBH opts (ES.reindexAsync $ ES.mkReindexRequest (ES.IndexName oldESIndex) newIndexName) runBH opts $ waitForTaskToComplete @ES.ReindexResponse taskNodeId @@ -746,14 +746,14 @@ withOldIndex :: (MonadIO m, HasCallStack) => Opt.Opts -> WaiTest.Session a -> m withOldIndex opts f = do indexName <- randomHandle createIndexWithMapping opts indexName oldMapping - let newOpts = opts & Opt.elasticsearchL . Opt.indexLens .~ (ES.IndexName indexName) + let newOpts = opts & Opt.elasticsearchLens . Opt.indexLens .~ (ES.IndexName indexName) withSettingsOverrides newOpts f <* deleteIndex opts indexName optsForOldIndex :: (MonadIO m, HasCallStack) => Opt.Opts -> m (Opt.Opts, Text) optsForOldIndex opts = do indexName <- randomHandle createIndexWithMapping opts indexName oldMapping - pure (opts & Opt.elasticsearchL . Opt.indexLens .~ (ES.IndexName indexName), indexName) + pure (opts & Opt.elasticsearchLens . Opt.indexLens .~ (ES.IndexName indexName), indexName) createIndexWithMapping :: (MonadIO m, HasCallStack) => Opt.Opts -> Text -> Value -> m () createIndexWithMapping opts name val = do @@ -773,7 +773,7 @@ deleteIndex opts name = do runBH :: (MonadIO m, HasCallStack) => Opt.Opts -> ES.BH m a -> m a runBH opts action = do - let (ES.Server esURL) = opts ^. Opt.elasticsearchL . Opt.urlLens + let (ES.Server esURL) = opts ^. Opt.elasticsearchLens . Opt.urlLens mgr <- liftIO $ initHttpManagerWithTLSConfig opts.elasticsearch.insecureSkipVerifyTls opts.elasticsearch.caCert let bEnv = mkBHEnv esURL mgr ES.runBH bEnv action diff --git a/services/brig/test/integration/API/Settings.hs b/services/brig/test/integration/API/Settings.hs index 67b097a9401..3647a4463bc 100644 --- a/services/brig/test/integration/API/Settings.hs +++ b/services/brig/test/integration/API/Settings.hs @@ -126,7 +126,7 @@ testUsersEmailVisibleIffExpected opts brig galley viewingUserIs visibilitySettin else Nothing ) ] - let newOpts = opts & Opt.optionSettings . Opt.emailVisibilityLens .~ visibilitySetting + let newOpts = opts & Opt.settingsLens . Opt.emailVisibilityLens .~ visibilitySetting withSettingsOverrides newOpts $ do get (apiVersion "v1" . brig . zUser viewerId . path "users" . queryItem "ids" uids) !!! do const 200 === statusCode @@ -155,7 +155,7 @@ testGetUserEmailShowsEmailsIffExpected opts brig galley viewingUserIs visibility else Nothing ) ] - let newOpts = opts & Opt.optionSettings . Opt.emailVisibilityLens .~ visibilitySetting + let newOpts = opts & Opt.settingsLens . Opt.emailVisibilityLens .~ visibilitySetting withSettingsOverrides newOpts $ do forM_ expectations $ \(uid, expectedEmail) -> get (apiVersion "v1" . brig . zUser viewerId . paths ["users", toByteString' uid]) !!! do diff --git a/services/brig/test/integration/API/SystemSettings.hs b/services/brig/test/integration/API/SystemSettings.hs index b2518c1fcd9..265d2d43dde 100644 --- a/services/brig/test/integration/API/SystemSettings.hs +++ b/services/brig/test/integration/API/SystemSettings.hs @@ -49,7 +49,7 @@ testGetSettings opts = liftIO $ do where expectResultForSetting :: Maybe Bool -> Bool -> IO () expectResultForSetting restrictUserCreationSetting expectedRes = do - let newOpts = opts & (optionSettings . restrictUserCreationLens) .~ restrictUserCreationSetting + let newOpts = opts & (settingsLens . restrictUserCreationLens) .~ restrictUserCreationSetting -- Run call in `WaiTest.Session` with an adjusted brig `Application`. I.e. -- the response is created by running the brig `Application` (with -- modified options) directly on the `Request`. No real HTTP request is @@ -73,7 +73,7 @@ testGetSettingsInternal opts = liftIO $ do where expectResultForEnableMls :: UserId -> Maybe Bool -> Bool -> IO () expectResultForEnableMls uid setEnableMlsValue expectedRes = do - let newOpts = opts & (optionSettings . enableMLSLens) .~ setEnableMlsValue + let newOpts = opts & (settingsLens . enableMLSLens) .~ setEnableMlsValue -- Run call in `WaiTest.Session` with an adjusted brig `Application`. I.e. -- the response is created by running the brig `Application` (with -- modified options) directly on the `Request`. No real HTTP request is diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 8443ec6cb35..d4599cc0fc0 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -81,8 +81,8 @@ newtype TeamSizeLimit = TeamSizeLimit Word32 tests :: Opt.Opts -> Manager -> Nginz -> Brig -> Cannon -> Galley -> UserJournalWatcher -> IO TestTree tests conf m n b c g aws = do - let tl = TeamSizeLimit conf.optSettings.maxTeamSize - let it = conf.optSettings.teamInvitationTimeout + let tl = TeamSizeLimit conf.settings.maxTeamSize + let it = conf.settings.teamInvitationTimeout pure $ testGroup "team" @@ -372,7 +372,7 @@ testInvitationTooManyPending opts brig (TeamSizeLimit limit) = do -- If this test takes longer to run than `team-invitation-timeout`, then some of the -- invitations have likely expired already and this test will actually _fail_ -- therefore we increase the timeout from default 10 to 300 seconds - let longerTimeout = opts {Opt.optSettings = opts.optSettings {Opt.teamInvitationTimeout = 300}} + let longerTimeout = opts {Opt.settings = opts.settings {Opt.teamInvitationTimeout = 300}} withSettingsOverrides longerTimeout $ do forM_ emails $ postInvitation brig tid inviter . stdInvitationRequest postInvitation brig tid inviter (stdInvitationRequest email) !!! do @@ -715,7 +715,7 @@ testInvitationPaging opts brig = do (uid, tid) <- createUserWithTeam brig let total = 5 invite email = stdInvitationRequest email - longerTimeout = opts {Opt.optSettings = opts.optSettings {Opt.teamInvitationTimeout = 300}} + longerTimeout = opts {Opt.settings = opts.settings {Opt.teamInvitationTimeout = 300}} emails <- withSettingsOverrides longerTimeout $ replicateM total $ do diff --git a/services/brig/test/integration/API/User.hs b/services/brig/test/integration/API/User.hs index 94417f50270..95e26d13ef1 100644 --- a/services/brig/test/integration/API/User.hs +++ b/services/brig/test/integration/API/User.hs @@ -55,8 +55,8 @@ tests :: UserJournalWatcher -> IO TestTree tests conf fbc p b c ch g n aws db userJournalWatcher = do - let cl = ConnectionLimit conf.optSettings.userMaxConnections - let at = conf.optSettings.activationTimeout + let cl = ConnectionLimit conf.settings.userMaxConnections + let at = conf.settings.activationTimeout z <- mkZAuthEnv (Just conf) pure $ testGroup diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 550680aa096..4d5db18a72d 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -201,7 +201,7 @@ testUpdateUserEmailByTeamOwner opts brig = do where checkLetActivationExpire :: EmailAddress -> Http () checkLetActivationExpire email = do - let timeout = round opts.optSettings.activationTimeout + let timeout = round opts.settings.activationTimeout threadDelay ((timeout + 1) * 1000_000) checkActivationCode email False @@ -239,7 +239,7 @@ testCreateUserWithPreverified opts brig userJournalWatcher = do "email" .= fromEmail e, "email_code" .= c ] - if opts.optSettings.restrictUserCreation == Just True + if opts.settings.restrictUserCreation == Just True then do postUserRegister' reg brig !!! const 403 === statusCode else do @@ -340,7 +340,7 @@ testCreateUserAnon brig galley = do Search.assertCan'tFind brig suid quid "Mr. Pink" testCreateUserPending :: Opt.Opts -> Brig -> Http () -testCreateUserPending (Opt.restrictUserCreation . Opt.optSettings -> Just True) _ = pure () +testCreateUserPending (Opt.restrictUserCreation . Opt.settings -> Just True) _ = pure () testCreateUserPending _ brig = do e <- randomEmail let p = @@ -379,7 +379,7 @@ testCreateUserPending _ brig = do -- -- email address must not be taken on @/register@. testCreateUserConflict :: Opt.Opts -> Brig -> Http () -testCreateUserConflict (Opt.restrictUserCreation . Opt.optSettings -> Just True) _ = pure () +testCreateUserConflict (Opt.restrictUserCreation . Opt.settings -> Just True) _ = pure () testCreateUserConflict _ brig = do -- trusted email domains u <- createUser "conflict" brig @@ -416,7 +416,7 @@ testCreateUserConflict _ brig = do -- -- Test to make sure a new user cannot be created with an invalid email address testCreateUserInvalidEmail :: Opt.Opts -> Brig -> Http () -testCreateUserInvalidEmail (Opt.restrictUserCreation . Opt.optSettings -> Just True) _ = pure () +testCreateUserInvalidEmail (Opt.restrictUserCreation . Opt.settings -> Just True) _ = pure () testCreateUserInvalidEmail _ brig = do let reqPhone = RequestBodyLBS . encode $ @@ -431,7 +431,7 @@ testCreateUserInvalidEmail _ brig = do -- @END testCreateUserBlacklist :: Opt.Opts -> Brig -> AWS.Env -> Http () -testCreateUserBlacklist (Opt.restrictUserCreation . Opt.optSettings -> Just True) _ _ = pure () +testCreateUserBlacklist (Opt.restrictUserCreation . Opt.settings -> Just True) _ _ = pure () testCreateUserBlacklist _ brig aws = mapM_ ensureBlacklist ["bounce", "complaint"] where @@ -490,7 +490,7 @@ testCreateUserExternalSSO brig = do !!! const 400 === statusCode testActivateWithExpiry :: Opt.Opts -> Brig -> Timeout -> Http () -testActivateWithExpiry (Opt.restrictUserCreation . Opt.optSettings -> Just True) _ _ = pure () +testActivateWithExpiry (Opt.restrictUserCreation . Opt.settings -> Just True) _ _ = pure () testActivateWithExpiry _ brig timeout = do u <- responseJsonError =<< registerUser "dilbert" brig let email = fromMaybe (error "missing email") (userEmail u) @@ -1091,7 +1091,7 @@ testSendActivationCode opts brig = do -- Code for email pre-verification requestActivationCode brig 200 . Left =<< randomEmail -- Standard email registration flow - if opts.optSettings.restrictUserCreation == Just True + if opts.settings.restrictUserCreation == Just True then do registerUser "Alice" brig !!! const 403 === statusCode else do @@ -1292,7 +1292,7 @@ testRestrictedUserCreation opts brig = do -- We create a team before to help in other tests (teamOwner, createdTeam) <- createUserWithTeam brig - let opts' = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.restrictUserCreation = Just True}} + let opts' = opts {Opt.settings = opts.settings {Opt.restrictUserCreation = Just True}} withSettingsOverrides opts' $ do e <- randomEmail diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 6e77a0b1ba0..518e4dfc162 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -409,7 +409,7 @@ testThrottleLogins :: Opts.Opts -> Brig -> Http () testThrottleLogins conf b = do -- Get the maximum amount of times we are allowed to login before -- throttling begins - let l = Opts.userCookieLimit (Opts.optSettings conf) + let l = Opts.userCookieLimit (Opts.settings conf) u <- randomUser b let Just e = userEmail u -- Login exactly that amount of times, as fast as possible @@ -441,7 +441,7 @@ testThrottleLogins conf b = do -- the aforementioned user. testLimitRetries :: (HasCallStack) => Opts.Opts -> Brig -> Http () testLimitRetries conf brig = do - let Just opts = conf.optSettings.limitFailedLogins + let Just opts = conf.settings.limitFailedLogins unless (Opts.timeout opts <= 30) $ error "`loginRetryTimeout` is the number of seconds this test is running. Please pick a value < 30." usr <- randomUser brig @@ -770,7 +770,7 @@ testNewPersistentCookie config b = getAndTestDBSupersededCookieAndItsValidSuccessor :: Opts.Opts -> Brig -> Nginz -> Http (Http.Cookie, Http.Cookie) getAndTestDBSupersededCookieAndItsValidSuccessor config b n = do u <- randomUser b - let renewAge = config.optSettings.userCookieRenewAge + let renewAge = config.settings.userCookieRenewAge let minAge = fromIntegral $ (renewAge + 1) * 1000000 Just email = userEmail u _rs <- @@ -1017,7 +1017,7 @@ testAccessWithExistingClientId brig = do testNewSessionCookie :: Opts.Opts -> Brig -> Http () testNewSessionCookie config b = do u <- randomUser b - let renewAge = config.optSettings.userCookieRenewAge + let renewAge = config.settings.userCookieRenewAge let minAge = fromIntegral $ renewAge * 1000000 + 1 Just email = userEmail u _rs <- @@ -1035,7 +1035,7 @@ testSuspendInactiveUsers config brig cookieType endPoint = do -- (context information: cookies are stored by user, not by device; so if there is a -- cookie that is old, it means none of the devices of the user has used it for a request.) - let Just suspendAge = Opts.suspendTimeout <$> config.optSettings.suspendInactiveUsers + let Just suspendAge = Opts.suspendTimeout <$> config.settings.suspendInactiveUsers unless (suspendAge <= 30) $ error "`suspendCookiesOlderThanSecs` is the number of seconds this test is running. Please pick a value < 30." @@ -1155,7 +1155,7 @@ testRemoveCookiesByLabelAndId b = do testTooManyCookies :: Opts.Opts -> Brig -> Http () testTooManyCookies config b = do u <- randomUser b - let l = config.optSettings.userCookieLimit + let l = config.settings.userCookieLimit let Just e = userEmail u carry = 2 pwlP = emailLogin e defPassword (Just "persistent") diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index c20f8d492fe..c73e81861f0 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -218,7 +218,7 @@ testAddGetClientCodeExpired db opts brig galley = do codeValue <- (.codeValue) <$$> lookupCode db k Code.AccountLogin checkLoginSucceeds $ MkLogin (LoginByEmail email) defPassword (Just defCookieLabel) codeValue - let timeout = round (verificationTimeout opts.optSettings) + let timeout = round (verificationTimeout opts.settings) threadDelay $ ((timeout + 1) * 1000_000) addClient' codeValue !!! do const 403 === statusCode @@ -291,7 +291,7 @@ testGetUserClientsQualified opts brig = do _c11 :: Client <- responseJsonError =<< addClient brig uid1 (defNewClient PermanentClientType [pk11] lk11) _c12 :: Client <- responseJsonError =<< addClient brig uid1 (defNewClient PermanentClientType [pk12] lk12) _c13 :: Client <- responseJsonError =<< addClient brig uid1 (defNewClient TemporaryClientType [pk13] lk13) - let localdomain = opts.optSettings.federationDomain + let localdomain = opts.settings.federationDomain getUserClientsQualified brig uid2 localdomain uid1 !!! do const 200 === statusCode assertTrue_ $ \res -> do @@ -396,7 +396,7 @@ testListClientsBulk opts brig = do c21 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk21] lk21) c22 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk22] lk22) - let domain = opts.optSettings.federationDomain + let domain = opts.settings.federationDomain uid3 <- userId <$> randomUser brig let mkPubClient cl = PubClient (clientId cl) (clientClass cl) let expectedResponse :: QualifiedUserMap (Set PubClient) = @@ -439,7 +439,7 @@ testClientsWithoutPrekeys brig cannon db opts = do uid2 <- userId <$> randomUser brig - let domain = opts.optSettings.federationDomain + let domain = opts.settings.federationDomain let userClients = QualifiedUserClients $ @@ -531,7 +531,7 @@ testClientsWithoutPrekeysV4 brig cannon db opts = do uid2 <- userId <$> randomUser brig - let domain = opts.optSettings.federationDomain + let domain = opts.settings.federationDomain let userClients = QualifiedUserClients $ @@ -626,7 +626,7 @@ testClientsWithoutPrekeysFailToListV4 brig cannon db opts = do uid2 <- fakeRemoteUser - let domain = opts.optSettings.federationDomain + let domain = opts.settings.federationDomain let userClients1 = QualifiedUserClients $ @@ -710,7 +710,7 @@ testListClientsBulkV2 opts brig = do c21 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk21] lk21) c22 <- responseJsonError =<< addClient brig uid2 (defNewClient PermanentClientType [pk22] lk22) - let domain = opts.optSettings.federationDomain + let domain = opts.settings.federationDomain uid3 <- userId <$> randomUser brig let mkPubClient cl = PubClient (clientId cl) (clientClass cl) let expectedResponse :: WrappedQualifiedUserMap (Set PubClient) = @@ -774,7 +774,7 @@ testGetUserPrekeys brig = do testGetUserPrekeysQualified :: Brig -> Opt.Opts -> Http () testGetUserPrekeysQualified brig opts = do - let domain = opts.optSettings.federationDomain + let domain = opts.settings.federationDomain [(uid, _c, _lpk, cpk)] <- generateClients 1 brig get (brig . paths ["users", toByteString' domain, toByteString' uid, "prekeys"] . zUser uid) !!! do const 200 === statusCode @@ -795,7 +795,7 @@ testGetClientPrekey brig = do testGetClientPrekeyQualified :: Brig -> Opt.Opts -> Http () testGetClientPrekeyQualified brig opts = do - let domain = opts.optSettings.federationDomain + let domain = opts.settings.federationDomain [(uid, c, _lpk, cpk)] <- generateClients 1 brig get (brig . paths ["users", toByteString' domain, toByteString' uid, "prekeys", toByteString' (clientId c)] . zUser uid) !!! do const 200 === statusCode @@ -832,7 +832,7 @@ testMultiUserGetPrekeys brig = do testMultiUserGetPrekeysQualified :: Brig -> Opt.Opts -> Http () testMultiUserGetPrekeysQualified brig opts = do - let domain = opts.optSettings.federationDomain + let domain = opts.settings.federationDomain xs <- generateClients 3 brig let userClients = @@ -866,7 +866,7 @@ testMultiUserGetPrekeysQualified brig opts = do testMultiUserGetPrekeysQualifiedV4 :: Brig -> Opt.Opts -> Http () testMultiUserGetPrekeysQualifiedV4 brig opts = do - let domain = opts.optSettings.federationDomain + let domain = opts.settings.federationDomain xs <- generateClients 3 brig let userClients = @@ -913,7 +913,7 @@ testTooManyClients :: Opt.Opts -> Brig -> Http () testTooManyClients opts brig = do uid <- userId <$> randomUser brig -- We can always change the permanent client limit - let newOpts = opts & optionSettings . userMaxPermClientsLens ?~ 1 + let newOpts = opts & settingsLens . userMaxPermClientsLens ?~ 1 withSettingsOverrides newOpts $ do -- There is only one temporary client, adding a new one -- replaces the previous one. @@ -1157,7 +1157,7 @@ testUpdateClient opts brig = do const Nothing === (preview (key "mls_public_keys") <=< responseJsonMaybe @Value) -- via `/users/:domain/:uid/clients/:client`, only `id` and `class` are visible: - let localdomain = opts.optSettings.federationDomain + let localdomain = opts.settings.federationDomain get (brig . paths ["users", toByteString' localdomain, toByteString' uid, "clients", toByteString' (clientId c)]) !!! do const 200 === statusCode const (Just $ clientId c) === (fmap pubClientId . responseJsonMaybe) @@ -1432,7 +1432,7 @@ instance A.ToJSON DPoPClaimsSet where testCreateAccessToken :: Opt.Opts -> Nginz -> Brig -> Http () testCreateAccessToken opts n brig = do - let localDomain = opts.optSettings.federationDomain + let localDomain = opts.settings.federationDomain (u, tid) <- Util.createUserWithTeam' brig handle <- do Just h <- userHandle <$> Util.setRandomHandle brig u diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index 6a451347193..5776fc082b9 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -197,7 +197,7 @@ testHandleQuery opts brig = do -- Usually, you can search outside your team assertCanFind brig user3 user4 -- Usually, you can search outside your team but not if this config option is set - let newOpts = opts & ((Opt.optionSettings . Opt.searchSameTeamOnlyLens) ?~ True) + let newOpts = opts & ((Opt.settingsLens . Opt.searchSameTeamOnlyLens) ?~ True) withSettingsOverrides newOpts $ assertCannotFind brig user3 user4 diff --git a/services/brig/test/integration/API/User/RichInfo.hs b/services/brig/test/integration/API/User/RichInfo.hs index 7e20a6ab807..453ba3394d9 100644 --- a/services/brig/test/integration/API/User/RichInfo.hs +++ b/services/brig/test/integration/API/User/RichInfo.hs @@ -117,7 +117,7 @@ testDedupeDuplicateFieldNames brig = do testRichInfoSizeLimit :: (HasCallStack) => Brig -> Opt.Opts -> Http () testRichInfoSizeLimit brig conf = do - let maxSize :: Int = conf.optSettings.richInfoLimit + let maxSize :: Int = conf.settings.richInfoLimit (owner, _) <- createUserWithTeam brig let bad1 = mkRichInfoAssocList diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs index a85fa616ed8..c5a95445519 100644 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ b/services/brig/test/integration/API/UserPendingActivation.hs @@ -124,7 +124,7 @@ assertUserExist msg db' uid shouldExist = liftIO $ do waitUserExpiration :: (MonadUnliftIO m) => Opts -> m () waitUserExpiration opts' = do - let timeoutSecs = round @Double . realToFrac $ opts'.optSettings.teamInvitationTimeout + let timeoutSecs = round @Double . realToFrac $ opts'.settings.teamInvitationTimeout Control.Exception.assert (timeoutSecs < 30) $ do threadDelay $ (timeoutSecs + 3) * 1_000_000 diff --git a/services/brig/test/integration/Run.hs b/services/brig/test/integration/Run.hs index 3ec5652dbff..9cfadcf53b1 100644 --- a/services/brig/test/integration/Run.hs +++ b/services/brig/test/integration/Run.hs @@ -124,7 +124,7 @@ runTests iConf brigOpts otherArgs = do let Opts.TurnServersFiles turnFile turnFileV2 = case Opts.serversSource $ Opts.turn brigOpts of Opts.TurnSourceFiles files -> files Opts.TurnSourceDNS _ -> error "The integration tests can only be run when TurnServers are sourced from files" - localDomain = brigOpts.optSettings.federationDomain + localDomain = brigOpts.settings.federationDomain awsOpts = Opts.aws brigOpts lg <- Logger.new Logger.defSettings -- TODO: use mkLogger'? db <- defInitCassandra (brigOpts.cassandra) lg diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index bcb9ca7bd01..a1baaaae223 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -33,7 +33,7 @@ import Brig.Types.Activation import Brig.ZAuth qualified as ZAuth import Control.Concurrent.Async import Control.Exception (throw) -import Control.Lens ((^.), (^?), (^?!)) +import Control.Lens ((^?), (^?!)) import Control.Monad.Catch (MonadCatch, MonadMask) import Control.Monad.Catch qualified as Catch import Control.Monad.State qualified as State @@ -184,8 +184,8 @@ runFedClient (FedClient mgr ep) domain = where servantClientMToHttp :: Domain -> Servant.ClientM a -> Http a servantClientMToHttp originDomain action = liftIO $ do - let brigHost = Text.unpack $ ep ^. host - brigPort = fromInteger . toInteger $ ep ^. port + let brigHost = Text.unpack ep.host + brigPort = fromInteger . toInteger $ ep.port baseUrl = Servant.BaseUrl Servant.Http brigHost brigPort "/federation" clientEnv = Servant.ClientEnv mgr baseUrl Nothing (makeClientRequest originDomain) eitherRes <- Servant.runClientM action clientEnv @@ -1004,7 +1004,7 @@ withSettingsOverrides opts action = liftIO $ do -- compile. withDomainsBlockedForRegistration :: (MonadIO m) => Opt.Opts -> [Text] -> WaiTest.Session a -> m a withDomainsBlockedForRegistration opts domains sess = do - let opts' = opts {Opt.optSettings = opts.optSettings {customerExtensions = Just blocked}} + let opts' = opts {Opt.settings = opts.settings {customerExtensions = Just blocked}} blocked = Opt.CustomerExtensions (Opt.DomainsBlockedForRegistration (unsafeMkDomain <$> domains)) unsafeMkDomain = either error id . mkDomain withSettingsOverrides opts' sess diff --git a/services/cargohold/src/CargoHold/Run.hs b/services/cargohold/src/CargoHold/Run.hs index eeee6b32ab2..d3bb3764e39 100644 --- a/services/cargohold/src/CargoHold/Run.hs +++ b/services/cargohold/src/CargoHold/Run.hs @@ -63,8 +63,8 @@ run o = lowerCodensity $ do s <- Server.newSettings $ defaultServer - (unpack $ o ^. cargohold . host) - (o ^. cargohold . port) + (unpack . host $ o ^. cargohold) + (port $ o ^. cargohold) (e ^. appLogger) runSettingsWithShutdown s app Nothing diff --git a/services/cargohold/test/integration/API/Util.hs b/services/cargohold/test/integration/API/Util.hs index d9579b55ab1..6719937f9ab 100644 --- a/services/cargohold/test/integration/API/Util.hs +++ b/services/cargohold/test/integration/API/Util.hs @@ -114,7 +114,7 @@ withSettingsOverrides f action = do liftIO $ runTestM (ts & tsEndpoint %~ setLocalEndpoint p) action setLocalEndpoint :: Word16 -> Endpoint -> Endpoint -setLocalEndpoint p = (port .~ p) . (host .~ "127.0.0.1") +setLocalEndpoint port endpoint = endpoint {port = port, host = "127.0.0.1"} withMockFederator :: (FederatedRequest -> IO (HTTP.MediaType, LByteString)) -> diff --git a/services/cargohold/test/integration/TestSetup.hs b/services/cargohold/test/integration/TestSetup.hs index f5b69fe2fd5..21bdcc431ac 100644 --- a/services/cargohold/test/integration/TestSetup.hs +++ b/services/cargohold/test/integration/TestSetup.hs @@ -138,7 +138,7 @@ createTestSetup optsPath configPath = do tlsManagerSettings { managerResponseTimeout = responseTimeoutMicro 300000000 } - let localEndpoint p = Endpoint {_host = "127.0.0.1", _port = p} + let localEndpoint p = Endpoint {host = "127.0.0.1", port = p} iConf <- handleParseError =<< decodeFileEither configPath opts <- decodeFileThrow optsPath endpoint <- optOrEnv @IntegrationConfig (.cargohold) iConf (localEndpoint . read) "CARGOHOLD_WEB_PORT" diff --git a/services/federator/src/Federator/Run.hs b/services/federator/src/Federator/Run.hs index c02d9f25f7d..d7ddfb27d4f 100644 --- a/services/federator/src/Federator/Run.hs +++ b/services/federator/src/Federator/Run.hs @@ -73,10 +73,10 @@ run opts = do void $ waitAnyCancel [internalServerThread, externalServerThread] where endpointInternal = federatorInternal opts - portInternal = fromIntegral $ endpointInternal ^. port + portInternal = fromIntegral $ endpointInternal.port endpointExternal = federatorExternal opts - portExternal = fromIntegral $ endpointExternal ^. port + portExternal = fromIntegral $ endpointExternal.port mkResolvConf :: RunSettings -> DNS.ResolvConf -> DNS.ResolvConf mkResolvConf settings conf = @@ -93,12 +93,12 @@ run opts = do newEnv :: Opts -> DNS.Resolver -> Log.Logger -> IO Env newEnv o _dnsResolver _applog = do let _requestId = RequestId "N/A" - _runSettings = Opt.optSettings o - _service Brig = Opt.brig o - _service Galley = Opt.galley o - _service Cargohold = Opt.cargohold o - _externalPort = o.federatorExternal._port - _internalPort = o.federatorInternal._port + _runSettings = o.optSettings + _service Brig = o.brig + _service Galley = o.galley + _service Cargohold = o.cargohold + _externalPort = o.federatorExternal.port + _internalPort = o.federatorInternal.port _httpManager <- initHttpManager sslContext <- mkTLSSettingsOrThrow _runSettings _http2Manager <- newIORef =<< mkHttp2Manager o.optSettings.tcpConnectionTimeout sslContext diff --git a/services/federator/test/integration/Test/Federator/Util.hs b/services/federator/test/integration/Test/Federator/Util.hs index a979a4c5de7..dea547dd52b 100644 --- a/services/federator/test/integration/Test/Federator/Util.hs +++ b/services/federator/test/integration/Test/Federator/Util.hs @@ -157,12 +157,12 @@ mkEnv _teTstOpts _teOpts = do let _teBrig = endpointToReq _teTstOpts.brig _teCargohold = endpointToReq _teTstOpts.cargohold -- _teTLSSettings <- mkTLSSettingsOrThrow (optSettings _teOpts) - _teSSLContext <- mkTLSSettingsOrThrow (optSettings _teOpts) - let _teSettings = optSettings _teOpts + _teSSLContext <- mkTLSSettingsOrThrow _teOpts.optSettings + let _teSettings = _teOpts.optSettings pure TestEnv {..} endpointToReq :: Endpoint -> (Bilge.Request -> Bilge.Request) -endpointToReq ep = Bilge.host (ep ^. O.host . to cs) . Bilge.port (ep ^. O.port) +endpointToReq ep = Bilge.host (cs ep.host) . Bilge.port ep.port -- All the code below is copied from brig-integration tests -- FUTUREWORK: This should live in another package and shared by all the integration tests diff --git a/services/galley/migrate-data/src/Galley/DataMigration.hs b/services/galley/migrate-data/src/Galley/DataMigration.hs index e4c27464ee0..5393d52a4d8 100644 --- a/services/galley/migrate-data/src/Galley/DataMigration.hs +++ b/services/galley/migrate-data/src/Galley/DataMigration.hs @@ -40,10 +40,10 @@ data CassandraSettings = CassandraSettings toCassandraOpts :: CassandraSettings -> CassandraOpts toCassandraOpts cas = CassandraOpts - { _endpoint = Endpoint (Text.pack (cas.cHost)) (cas.cPort), - _keyspace = C.unKeyspace (cas.cKeyspace), - _filterNodesByDatacentre = Nothing, - _tlsCa = cas.cTlsCa + { endpoint = Endpoint (Text.pack (cas.cHost)) (cas.cPort), + keyspace = C.unKeyspace (cas.cKeyspace), + filterNodesByDatacentre = Nothing, + tlsCa = cas.cTlsCa } cassandraSettingsParser :: Parser CassandraSettings diff --git a/services/galley/src/Galley/Intra/User.hs b/services/galley/src/Galley/Intra/User.hs index 6e6ef60859e..4221e42dbec 100644 --- a/services/galley/src/Galley/Intra/User.hs +++ b/services/galley/src/Galley/Intra/User.hs @@ -39,7 +39,7 @@ import Bilge hiding (getHeader, host, options, port, statusCode) import Bilge.RPC import Brig.Types.Intra qualified as Brig import Control.Error hiding (bool, isRight) -import Control.Lens (view, (^.)) +import Control.Lens (view) import Control.Monad.Catch import Data.ByteString.Char8 (pack) import Data.ByteString.Char8 qualified as BSC @@ -254,7 +254,7 @@ runHereClientM action = do mgr <- view manager brigep <- view brig let env = Client.mkClientEnv mgr baseurl - baseurl = Client.BaseUrl Client.Http (Text.unpack $ brigep ^. host) (fromIntegral $ brigep ^. port) "" + baseurl = Client.BaseUrl Client.Http (Text.unpack brigep.host) (fromIntegral brigep.port) "" liftIO $ Client.runClientM action env handleServantResp :: diff --git a/services/galley/src/Galley/Intra/Util.hs b/services/galley/src/Galley/Intra/Util.hs index c7e1de20920..fa24f814ae5 100644 --- a/services/galley/src/Galley/Intra/Util.hs +++ b/services/galley/src/Galley/Intra/Util.hs @@ -25,7 +25,8 @@ import Bilge hiding (getHeader, host, options, port, statusCode) import Bilge qualified as B import Bilge.RPC (rpc) import Bilge.Retry -import Control.Lens (view, (^.)) +import Cassandra.Options (Endpoint (..)) +import Control.Lens (view) import Control.Retry import Data.ByteString.Lazy qualified as LB import Data.Misc (portNumber) @@ -36,7 +37,6 @@ import Galley.Monad import Galley.Options import Imports hiding (log) import Network.HTTP.Types -import Util.Options data IntraComponent = Brig | Spar | Gundeck deriving (Show) @@ -48,14 +48,14 @@ componentName Gundeck = "gundeck" componentRequest :: IntraComponent -> Opts -> Request -> Request componentRequest Brig o = - B.host (encodeUtf8 (o ^. brig . host)) - . B.port (portNumber (fromIntegral (o ^. brig . port))) + B.host (encodeUtf8 . host $ o._brig) + . B.port (portNumber $ fromIntegral . port $ o._brig) componentRequest Spar o = - B.host (encodeUtf8 (o ^. spar . host)) - . B.port (portNumber (fromIntegral (o ^. spar . port))) + B.host (encodeUtf8 o._spar.host) + . B.port (portNumber $ fromIntegral . port $ o._spar) componentRequest Gundeck o = - B.host (encodeUtf8 $ o ^. gundeck . host) - . B.port (portNumber $ fromIntegral (o ^. gundeck . port)) + B.host (encodeUtf8 o._gundeck.host) + . B.port (portNumber $ fromIntegral . port $ o._gundeck) . method POST . path "/i/push/v2" . expect2xx diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 1d602e420f3..a2b233b5e13 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -37,7 +37,7 @@ module Galley.Options JournalOpts (JournalOpts), queueName, endpoint, - Opts, + Opts (..), galley, cassandra, brig, diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index f9374903b2d..acd1bba5381 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -76,8 +76,8 @@ run opts = lowerCodensity do lift $ newSettings $ defaultServer - (unpack $ opts ^. galley . host) - (portNumber $ fromIntegral $ opts ^. galley . port) + (unpack $ opts._galley.host) + (portNumber $ fromIntegral opts._galley.port) (env ^. App.applog) forM_ (env ^. aEnv) $ \aws -> diff --git a/services/galley/test/integration/Run.hs b/services/galley/test/integration/Run.hs index a5d212c39b4..54547697b7c 100644 --- a/services/galley/test/integration/Run.hs +++ b/services/galley/test/integration/Run.hs @@ -97,7 +97,7 @@ main = withOpenSSL $ runTests go ] getOpts gFile iFile = do m <- newManager tlsManagerSettings {managerResponseTimeout = responseTimeoutMicro 300000000} - let local p = Endpoint {_host = "127.0.0.1", _port = p} + let local p = Endpoint {host = "127.0.0.1", port = p} gConf <- handleParseError =<< decodeFileEither gFile iConf <- handleParseError =<< decodeFileEither iFile -- FUTUREWORK: we don't support process env setup any more, so both gconf and iConf diff --git a/services/galley/test/integration/TestSetup.hs b/services/galley/test/integration/TestSetup.hs index a6b9ba84f52..66b975c6c81 100644 --- a/services/galley/test/integration/TestSetup.hs +++ b/services/galley/test/integration/TestSetup.hs @@ -48,7 +48,7 @@ where import Bilge (Manager, MonadHttp (..), Request, withResponse) import Cassandra qualified as Cql -import Control.Lens (makeLenses, view, (^.)) +import Control.Lens (makeLenses, view) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) import Data.Aeson import Data.ByteString.Conversion @@ -155,8 +155,8 @@ runFedClient (FedClient mgr ep) domain = where servantClientMToHttp :: Domain -> Servant.ClientM a -> m a servantClientMToHttp originDomain action = liftIO $ do - let h = Text.unpack $ ep ^. host - p = fromInteger . toInteger $ ep ^. port + let h = Text.unpack ep.host + p = fromInteger $ toInteger ep.port baseUrl = Servant.BaseUrl Servant.Http h p "/federation" clientEnv = Servant.ClientEnv mgr baseUrl Nothing (makeClientRequest originDomain) eitherRes <- Servant.runClientM action clientEnv diff --git a/services/gundeck/migrate-data/src/Gundeck/DataMigration.hs b/services/gundeck/migrate-data/src/Gundeck/DataMigration.hs index 7bb99bb3f26..a1b20641cd8 100644 --- a/services/gundeck/migrate-data/src/Gundeck/DataMigration.hs +++ b/services/gundeck/migrate-data/src/Gundeck/DataMigration.hs @@ -40,10 +40,10 @@ data CassandraSettings = CassandraSettings toCassandraOpts :: CassandraSettings -> CassandraOpts toCassandraOpts cas = CassandraOpts - { _endpoint = Endpoint (Text.pack (cas.cHost)) (cas.cPort), - _keyspace = C.unKeyspace (cas.cKeyspace), - _filterNodesByDatacentre = Nothing, - _tlsCa = cas.cTlsCa + { endpoint = Endpoint (Text.pack (cas.cHost)) (cas.cPort), + keyspace = C.unKeyspace (cas.cKeyspace), + filterNodesByDatacentre = Nothing, + tlsCa = cas.cTlsCa } cassandraSettingsParser :: Parser CassandraSettings diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index 0f8c7a13fd4..5543f0a8ad7 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -66,7 +66,7 @@ run o = withTracer \tracer -> do runClient (e ^. cstate) $ versionCheck lastSchemaVersion let l = e ^. applog - s <- newSettings $ defaultServer (unpack $ o ^. gundeck . host) (o ^. gundeck . port) l + s <- newSettings $ defaultServer (unpack . host $ o ^. gundeck) (port $ o ^. gundeck) l let throttleMillis = fromMaybe defSqsThrottleMillis $ o ^. (settings . sqsThrottleMillis) lst <- Async.async $ Aws.execute (e ^. awsEnv) (Aws.listen throttleMillis (runDirect e . onEvent)) diff --git a/services/spar/migrate-data/src/Spar/DataMigration/Types.hs b/services/spar/migrate-data/src/Spar/DataMigration/Types.hs index 64d7a13c0e9..abcdf2d34df 100644 --- a/services/spar/migrate-data/src/Spar/DataMigration/Types.hs +++ b/services/spar/migrate-data/src/Spar/DataMigration/Types.hs @@ -76,8 +76,8 @@ makeLenses ''CassandraSettings toCassandraOpts :: CassandraSettings -> CassandraOpts toCassandraOpts cas = CassandraOpts - { _endpoint = Endpoint (Text.pack (cas ^. cHosts)) (cas ^. cPort), - _keyspace = C.unKeyspace (cas ^. cKeyspace), - _filterNodesByDatacentre = Nothing, - _tlsCa = cas ^. cTlsCa + { endpoint = Endpoint (Text.pack (cas ^. cHosts)) (cas ^. cPort), + keyspace = C.unKeyspace (cas ^. cKeyspace), + filterNodesByDatacentre = Nothing, + tlsCa = cas ^. cTlsCa } diff --git a/services/spar/src/Spar/Run.hs b/services/spar/src/Spar/Run.hs index f07ca3ce871..e577e9ed5b6 100644 --- a/services/spar/src/Spar/Run.hs +++ b/services/spar/src/Spar/Run.hs @@ -92,12 +92,12 @@ mkApp sparCtxOpts = do sparCtxCas <- initCassandra sparCtxOpts sparCtxLogger sparCtxHttpManager <- Bilge.newManager Bilge.defaultManagerSettings let sparCtxHttpBrig = - Bilge.host (sparCtxOpts ^. to brig . host . to encodeUtf8) - . Bilge.port (sparCtxOpts ^. to brig . port) + Bilge.host (sparCtxOpts ^. to brig . to host . to encodeUtf8) + . Bilge.port (sparCtxOpts ^. to brig . to port) $ Bilge.empty let sparCtxHttpGalley = - Bilge.host (sparCtxOpts ^. to galley . host . to encodeUtf8) - . Bilge.port (sparCtxOpts ^. to galley . port) + Bilge.host (sparCtxOpts ^. to galley . to host . to encodeUtf8) + . Bilge.port (sparCtxOpts ^. to galley . to port) $ Bilge.empty let sparCtxRequestId = RequestId "N/A" let ctx0 = Env {..} diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 474cc09bb8d..7f6ae09b7a9 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -665,7 +665,7 @@ zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" endpointToReq :: Endpoint -> (Bilge.Request -> Bilge.Request) -endpointToReq ep = Bilge.host (ep ^. host . to cs) . Bilge.port (ep ^. port) +endpointToReq ep = Bilge.host (cs ep.host) . Bilge.port ep.port -- spar specifics diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 2e0f7d49efc..5ebad513109 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -84,7 +84,7 @@ start o = do Server.runSettingsWithShutdown s (requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName $ servantApp e) Nothing where server :: Env -> Server.Server - server e = Server.defaultServer (unpack $ stern o ^. host) (stern o ^. port) (e ^. applog) + server e = Server.defaultServer (unpack o.stern.host) o.stern.port e._applog servantApp :: Env -> Application servantApp e0 req cont = do diff --git a/tools/stern/src/Stern/App.hs b/tools/stern/src/Stern/App.hs index 3c044890795..8203c87ebcd 100644 --- a/tools/stern/src/Stern/App.hs +++ b/tools/stern/src/Stern/App.hs @@ -26,7 +26,7 @@ module Stern.App where import Bilge qualified import Bilge.RPC (HasRequestId (..)) import Control.Error -import Control.Lens (makeLenses, view, (^.)) +import Control.Lens (makeLenses, view) import Control.Monad.Catch (MonadCatch, MonadThrow) import Control.Monad.IO.Class import Control.Monad.Reader.Class @@ -64,7 +64,7 @@ newEnv o = do Env (mkRequest $ O.brig o) (mkRequest $ O.galley o) (mkRequest $ O.gundeck o) (mkRequest $ O.ibis o) (mkRequest $ O.galeb o) l (RequestId "N/A") <$> newManager where - mkRequest s = Bilge.host (encodeUtf8 (s ^. host)) . Bilge.port (s ^. port) $ Bilge.empty + mkRequest s = Bilge.host (encodeUtf8 s.host) . Bilge.port s.port $ Bilge.empty newManager = Bilge.newManager (Bilge.defaultManagerSettings {Bilge.managerResponseTimeout = responseTimeoutMicro 10000000}) -- Monads From 235eb86d009a505dc190192e9e078efa92e7008b Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:52:50 +0200 Subject: [PATCH 090/136] [chore] Reduce lens usage and clean up Env/Opts for Cargohold and Stern. (#4269) * [chore] Stern Env. * Cargohold. --- services/cargohold/src/CargoHold/API/Util.hs | 5 +- services/cargohold/src/CargoHold/API/V3.hs | 4 +- services/cargohold/src/CargoHold/AWS.hs | 46 +++++----- services/cargohold/src/CargoHold/App.hs | 81 +++++++++--------- .../cargohold/src/CargoHold/Federation.hs | 9 +- services/cargohold/src/CargoHold/Options.hs | 62 +++++++------- services/cargohold/src/CargoHold/Run.hs | 22 ++--- services/cargohold/src/CargoHold/S3.hs | 19 ++--- services/cargohold/src/CargoHold/Util.hs | 3 +- .../cargohold/test/integration/API/Util.hs | 6 +- services/cargohold/test/integration/App.hs | 18 ++-- .../cargohold/test/integration/TestSetup.hs | 37 ++++---- tools/stern/src/Stern/API.hs | 8 +- tools/stern/src/Stern/App.hs | 48 ++++++----- tools/stern/src/Stern/Intra.hs | 84 +++++++++---------- 15 files changed, 228 insertions(+), 224 deletions(-) diff --git a/services/cargohold/src/CargoHold/API/Util.hs b/services/cargohold/src/CargoHold/API/Util.hs index a6df369b1f4..b6d5876da49 100644 --- a/services/cargohold/src/CargoHold/API/Util.hs +++ b/services/cargohold/src/CargoHold/API/Util.hs @@ -23,17 +23,16 @@ where import CargoHold.App import Control.Error -import Control.Lens import Data.Qualified import Imports import Wire.API.Federation.Error ensureLocal :: Qualified a -> Handler (Local a) ensureLocal value = do - loc <- view localUnit + loc <- asks (.localUnit) foldQualified loc pure (\_ -> throwE federationNotImplemented) value qualifyLocal :: a -> Handler (Local a) qualifyLocal x = do - loc <- view localUnit + loc <- asks (.localUnit) pure (qualifyAs loc x) diff --git a/services/cargohold/src/CargoHold/API/V3.hs b/services/cargohold/src/CargoHold/API/V3.hs index fcb9105c7d5..2fb5492e43a 100644 --- a/services/cargohold/src/CargoHold/API/V3.hs +++ b/services/cargohold/src/CargoHold/API/V3.hs @@ -41,7 +41,7 @@ import qualified Codec.MIME.Type as MIME import qualified Conduit import Control.Applicative (optional) import Control.Error -import Control.Lens (set, view, (^.)) +import Control.Lens (set, (^.)) import Control.Monad.Trans.Resource import Crypto.Random (getRandomBytes) import Data.Aeson (eitherDecodeStrict') @@ -70,7 +70,7 @@ upload own bdy = do let cl = fromIntegral $ hdrLength hdrs when (cl <= 0) $ throwE invalidLength - maxBytes <- view (CargoHold.App.settings . maxTotalBytes) + maxBytes <- asks (.options.settings.maxTotalBytes) when (cl > maxBytes) $ throwE assetTooLarge ast <- liftIO $ Id <$> nextRandom diff --git a/services/cargohold/src/CargoHold/AWS.hs b/services/cargohold/src/CargoHold/AWS.hs index 29aa2750f44..a7db87772ea 100644 --- a/services/cargohold/src/CargoHold/AWS.hs +++ b/services/cargohold/src/CargoHold/AWS.hs @@ -20,16 +20,17 @@ module CargoHold.AWS ( -- * Monad - Env, + Env (..), + amazonkaEnvLens, + CargoHold.AWS.s3BucketLens, + CargoHold.AWS.cloudFrontLens, + amazonkaDownloadEndpointLens, + loggerLens, mkEnv, amazonkaEnvWithDownloadEndpoint, Amazon, - amazonkaEnv, execute, - s3Bucket, - cloudFront, Error (..), - amazonkaDownloadEndpoint, -- * AWS sendCatch, @@ -55,25 +56,26 @@ import qualified System.Logger as Logger import System.Logger.Class (Logger, MonadLogger (log), (~~)) import qualified System.Logger.Class as Log import Util.Options (AWSEndpoint (..)) +import Util.SuffixNamer data Env = Env - { _logger :: !Logger, - _s3Bucket :: !Text, - _amazonkaEnv :: !AWS.Env, + { logger :: !Logger, + s3Bucket :: !Text, + amazonkaEnv :: !AWS.Env, -- | Endpoint for downloading assets (for the external world). -- This gets used with Minio, which Cargohold can reach using a cluster-internal endpoint, -- but clients can't, so we need to use a public one for pre-signed URLs we redirect to. - _amazonkaDownloadEndpoint :: !AWSEndpoint, - _cloudFront :: !(Maybe CloudFront) + amazonkaDownloadEndpoint :: !AWSEndpoint, + cloudFront :: !(Maybe CloudFront) } -makeLenses ''Env +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Env -- | Override the endpoint in the '_amazonkaEnv' with '_amazonkaDownloadEndpoint'. -- TODO: Choose the correct s3 addressing style amazonkaEnvWithDownloadEndpoint :: Env -> AWS.Env amazonkaEnvWithDownloadEndpoint e = - AWS.overrideService (setAWSEndpoint (e ^. amazonkaDownloadEndpoint)) (e ^. amazonkaEnv) + AWS.overrideService (setAWSEndpoint e.amazonkaDownloadEndpoint) e.amazonkaEnv setAWSEndpoint :: AWSEndpoint -> AWS.Service -> AWS.Service setAWSEndpoint e = AWS.setEndpoint (_awsSecure e) (_awsHost e) (_awsPort e) @@ -95,7 +97,7 @@ newtype Amazon a = Amazon ) instance MonadLogger Amazon where - log l m = view logger >>= \g -> Logger.log g l m + log l m = asks (.logger) >>= \g -> Logger.log g l m mkEnv :: Logger -> @@ -115,7 +117,7 @@ mkEnv lgr s3End s3AddrStyle s3Download bucket cfOpts mgr = do cf <- mkCfEnv cfOpts pure (Env g bucket e s3Download cf) where - mkCfEnv (Just o) = Just <$> initCloudFront (o ^. privateKey) (o ^. keyPairId) 300 (o ^. domain) + mkCfEnv (Just o) = Just <$> initCloudFront o.privateKey o.keyPairId 300 o.domain mkCfEnv Nothing = pure Nothing mkAwsEnv g s3 = do baseEnv <- @@ -176,11 +178,11 @@ exec :: (Text -> r) -> m (AWSResponse r) exec env request = do - let req = request env._s3Bucket - resp <- execute env (sendCatch (env ^. amazonkaEnv) req) + let req = request env.s3Bucket + resp <- execute env (sendCatch env.amazonkaEnv req) case resp of Left err -> do - Logger.info (view logger env) $ + Logger.info env.logger $ Log.field "remote" (Log.val "S3") ~~ Log.msg (show err) ~~ Log.msg (show req) @@ -199,11 +201,11 @@ execStream :: (Text -> r) -> ResourceT IO (AWSResponse r) execStream env request = do - let req = request env._s3Bucket - resp <- sendCatch (env ^. amazonkaEnv) req + let req = request env.s3Bucket + resp <- sendCatch env.amazonkaEnv req case resp of Left err -> do - Logger.info (view logger env) $ + Logger.info env.logger $ Log.field "remote" (Log.val "S3") ~~ Log.msg (show err) ~~ Log.msg (show req) @@ -224,8 +226,8 @@ execCatch :: (Text -> r) -> m (Maybe (AWSResponse r)) execCatch env request = do - let req = request env._s3Bucket - resp <- execute env (retrying retry5x (const canRetry) (const (sendCatch (env ^. amazonkaEnv) req))) + let req = request env.s3Bucket + resp <- execute env (retrying retry5x (const canRetry) (const (sendCatch env.amazonkaEnv req))) case resp of Left err -> do Log.info $ diff --git a/services/cargohold/src/CargoHold/App.hs b/services/cargohold/src/CargoHold/App.hs index e40d499915b..1495bda34fe 100644 --- a/services/cargohold/src/CargoHold/App.hs +++ b/services/cargohold/src/CargoHold/App.hs @@ -22,18 +22,17 @@ module CargoHold.App ( -- * Environment - Env, + Env (..), newEnv, closeEnv, - aws, - multiIngress, - httpManager, - http2Manager, - appLogger, - requestId, - localUnit, - options, - settings, + awsLens, + multiIngressLens, + httpManagerLens, + http2ManagerLens, + appLoggerLens, + requestIdLens, + localUnitLens, + optionsLens, -- * App Monad AppT, @@ -56,7 +55,7 @@ import CargoHold.Options (AWSOpts, Opts, S3Compatibility (..), brig) import qualified CargoHold.Options as Opt import Control.Error (ExceptT, exceptT) import Control.Exception (throw) -import Control.Lens (Lens', makeLenses, non, view, (?~), (^.)) +import Control.Lens (lensField, lensRules, makeLensesWith, non, (.~), (?~), (^.)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) import qualified Data.Map as Map import Data.Qualified @@ -72,6 +71,7 @@ import qualified Servant.Client as Servant import System.Logger.Class hiding (settings) import qualified System.Logger.Extended as Log import Util.Options +import Util.SuffixNamer import Wire.API.Routes.Internal.Brig (BrigInternalClient) import qualified Wire.API.Routes.Internal.Brig as IBrig @@ -79,30 +79,27 @@ import qualified Wire.API.Routes.Internal.Brig as IBrig -- Environment data Env = Env - { _aws :: AWS.Env, - _appLogger :: Logger, - _httpManager :: Manager, - _http2Manager :: Http2Manager, - _requestId :: RequestId, - _options :: Opt.Opts, - _localUnit :: Local (), - _multiIngress :: Map String AWS.Env + { aws :: AWS.Env, + appLogger :: Logger, + httpManager :: Manager, + http2Manager :: Http2Manager, + requestId :: RequestId, + options :: Opt.Opts, + localUnit :: Local (), + multiIngress :: Map String AWS.Env } -makeLenses ''Env - -settings :: Lens' Env Opt.Settings -settings = options . Opt.settings +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Env newEnv :: Opts -> IO Env newEnv opts = do - logger <- Log.mkLogger (opts ^. Opt.logLevel) (opts ^. Opt.logNetStrings) (opts ^. Opt.logFormat) + logger <- Log.mkLogger opts.logLevel opts.logNetStrings opts.logFormat checkOpts opts logger - httpMgr <- initHttpManager (opts ^. Opt.aws . Opt.s3Compatibility) + httpMgr <- initHttpManager opts.aws.s3Compatibility http2Mgr <- initHttp2Manager - awsEnv <- initAws (opts ^. Opt.aws) logger httpMgr + awsEnv <- initAws opts.aws logger httpMgr multiIngressAWS <- initMultiIngressAWS logger httpMgr - let localDomain = toLocalUnsafe (opts ^. Opt.settings . Opt.federationDomain) () + let localDomain = toLocalUnsafe opts.settings.federationDomain () pure $ Env awsEnv logger httpMgr http2Mgr (RequestId "N/A") opts localDomain multiIngressAWS where initMultiIngressAWS :: Logger -> Manager -> IO (Map String AWS.Env) @@ -112,10 +109,10 @@ newEnv opts = do ( \(k, v) -> initAws (patchS3DownloadEndpoint v) logger httpMgr >>= \v' -> pure (k, v') ) - (Map.assocs (opts ^. Opt.aws . Opt.multiIngress . non Map.empty)) + (Map.assocs (opts ^. Opt.awsLens . Opt.multiIngressLens . non Map.empty)) patchS3DownloadEndpoint :: AWSEndpoint -> AWSOpts - patchS3DownloadEndpoint e = (opts ^. Opt.aws) & Opt.s3DownloadEndpoint ?~ e + patchS3DownloadEndpoint e = (opts ^. Opt.awsLens) & Opt.s3DownloadEndpointLens ?~ e -- | Validate (some) options (`Opts`) -- @@ -132,19 +129,19 @@ checkOpts opts lgr = do error errorMsg where multiIngressConfigured :: Bool - multiIngressConfigured = (not . null) (opts ^. (Opt.aws . Opt.multiIngress . non Map.empty)) + multiIngressConfigured = (not . null) (opts ^. (Opt.awsLens . Opt.multiIngressLens . non Map.empty)) cloudFrontConfigured :: Bool - cloudFrontConfigured = isJust (opts ^. (Opt.aws . Opt.cloudFront)) + cloudFrontConfigured = isJust opts.aws.cloudFront singleAwsDownloadEndpointConfigured :: Bool - singleAwsDownloadEndpointConfigured = isJust (opts ^. (Opt.aws . Opt.s3DownloadEndpoint)) + singleAwsDownloadEndpointConfigured = isJust opts.aws.s3DownloadEndpoint initAws :: AWSOpts -> Logger -> Manager -> IO AWS.Env -initAws o l = AWS.mkEnv l (o ^. Opt.s3Endpoint) addrStyle downloadEndpoint (o ^. Opt.s3Bucket) (o ^. Opt.cloudFront) +initAws o l = AWS.mkEnv l o.s3Endpoint addrStyle downloadEndpoint o.s3Bucket o.cloudFront where - downloadEndpoint = fromMaybe (o ^. Opt.s3Endpoint) (o ^. Opt.s3DownloadEndpoint) - addrStyle = maybe S3AddressingStylePath Opt.unwrapS3AddressingStyle (o ^. Opt.s3AddressingStyle) + downloadEndpoint = fromMaybe o.s3Endpoint o.s3DownloadEndpoint + addrStyle = maybe S3AddressingStylePath Opt.unwrapS3AddressingStyle o.s3AddressingStyle initHttpManager :: Maybe S3Compatibility -> IO Manager initHttpManager s3Compat = @@ -185,7 +182,7 @@ initSSLContext = do pure ctx closeEnv :: Env -> IO () -closeEnv e = Log.close $ e ^. appLogger +closeEnv e = Log.close e.appLogger ------------------------------------------------------------------------------- -- App Monad @@ -207,8 +204,8 @@ type App = AppT IO instance MonadLogger App where log l m = do - g <- view appLogger - r <- view requestId + g <- asks (.appLogger) + r <- asks (.requestId) Log.log g l $ "request" .= unRequestId r ~~ m instance MonadLogger (ExceptT e App) where @@ -216,11 +213,11 @@ instance MonadLogger (ExceptT e App) where instance MonadHttp App where handleRequestWithCont req handler = do - manager <- view httpManager + manager <- asks (.httpManager) liftIO $ withResponse req manager handler instance HasRequestId App where - getRequestId = view requestId + getRequestId = asks (.requestId) instance MonadHttp (ExceptT e App) where handleRequestWithCont req handler = lift $ Bilge.handleRequestWithCont req handler @@ -233,8 +230,8 @@ runAppT e (AppT a) = runReaderT a e executeBrigInteral :: BrigInternalClient a -> App (Either Servant.ClientError a) executeBrigInteral action = do - httpMgr <- view httpManager - brigEndpoint <- view (options . brig) + httpMgr <- asks (.httpManager) + brigEndpoint <- asks (.options.brig) liftIO $ IBrig.runBrigInternalClient httpMgr brigEndpoint action ------------------------------------------------------------------------------- diff --git a/services/cargohold/src/CargoHold/Federation.hs b/services/cargohold/src/CargoHold/Federation.hs index 7ce7d4aae6b..5382e139011 100644 --- a/services/cargohold/src/CargoHold/Federation.hs +++ b/services/cargohold/src/CargoHold/Federation.hs @@ -21,7 +21,6 @@ import CargoHold.App import CargoHold.Options import Control.Error import Control.Exception (throw) -import Control.Lens import Control.Monad.Codensity import Data.Id import Data.Qualified @@ -75,12 +74,12 @@ downloadRemoteAsset usr rkey tok = do mkFederatorClientEnv :: Remote x -> Handler FederatorClientEnv mkFederatorClientEnv remote = do - loc <- view localUnit + loc <- asks (.localUnit) endpoint <- - view (options . federator) + asks (.options.federator) >>= maybe (throwE federationNotConfigured) pure - mgr <- view http2Manager - rid <- view requestId + mgr <- asks (.http2Manager) + rid <- asks (.requestId) pure FederatorClientEnv { ceOriginDomain = tDomain loc, diff --git a/services/cargohold/src/CargoHold/Options.hs b/services/cargohold/src/CargoHold/Options.hs index 7a3f5cd08fa..09edeb065cd 100644 --- a/services/cargohold/src/CargoHold/Options.hs +++ b/services/cargohold/src/CargoHold/Options.hs @@ -29,23 +29,22 @@ import Data.Domain import Imports import System.Logger.Extended (Level, LogFormat) import Util.Options -import Util.Options.Common +import Util.SuffixNamer import Wire.API.Routes.Version -- | AWS CloudFront settings. data CloudFrontOpts = CloudFrontOpts { -- | Domain - _domain :: CF.Domain, + domain :: CF.Domain, -- | Keypair ID - _keyPairId :: CF.KeyPairId, + keyPairId :: CF.KeyPairId, -- | Path to private key - _privateKey :: FilePath + privateKey :: FilePath } deriving (Show, Generic) -deriveFromJSON toOptionFieldName ''CloudFrontOpts - -makeLenses ''CloudFrontOpts +deriveFromJSON defaultOptions ''CloudFrontOpts +makeLensesWith (lensRules & lensField .~ suffixNamer) ''CloudFrontOpts newtype OptS3AddressingStyle = OptS3AddressingStyle { unwrapS3AddressingStyle :: S3AddressingStyle @@ -62,7 +61,7 @@ instance FromJSON OptS3AddressingStyle where other -> fail $ "invalid S3AddressingStyle: " <> show other data AWSOpts = AWSOpts - { _s3Endpoint :: !AWSEndpoint, + { s3Endpoint :: !AWSEndpoint, -- | S3 can either by addressed in path style, i.e. -- https:////, or vhost style, i.e. -- https://./. AWS's S3 offering has @@ -88,18 +87,18 @@ data AWSOpts = AWSOpts -- -- When this option is unspecified, we default to path style addressing to -- ensure smooth transition for older deployments. - _s3AddressingStyle :: !(Maybe OptS3AddressingStyle), + s3AddressingStyle :: !(Maybe OptS3AddressingStyle), -- | S3 endpoint for generating download links. Useful if Cargohold is configured to use -- an S3 replacement running inside the internal network (in which case internally we -- would use one hostname for S3, and when generating an asset link for a client app, we -- would use another hostname). - _s3DownloadEndpoint :: !(Maybe AWSEndpoint), + s3DownloadEndpoint :: !(Maybe AWSEndpoint), -- | S3 bucket name - _s3Bucket :: !Text, + s3Bucket :: !Text, -- | Enable this option for compatibility with specific S3 backends. - _s3Compatibility :: !(Maybe S3Compatibility), + s3Compatibility :: !(Maybe S3Compatibility), -- | AWS CloudFront options - _cloudFront :: !(Maybe CloudFrontOpts), + cloudFront :: !(Maybe CloudFrontOpts), -- | @Z-Host@ header to s3 download endpoint `Map` -- -- This logic is: If the @Z-Host@ header is provided and found in this map, @@ -107,7 +106,7 @@ data AWSOpts = AWSOpts -- otherwise a 404 is retuned. This option is only useful -- in the context of multi-ingress setups where one backend / deployment is -- reachable under several domains. - _multiIngress :: !(Maybe (Map String AWSEndpoint)) + multiIngress :: !(Maybe (Map String AWSEndpoint)) } deriving (Show, Generic) @@ -122,15 +121,16 @@ instance FromJSON S3Compatibility where "scality-ring" -> pure S3CompatibilityScalityRing other -> fail $ "invalid S3Compatibility: " <> show other -deriveFromJSON toOptionFieldName ''AWSOpts +deriveFromJSON defaultOptions ''AWSOpts makeLenses ''AWSOpts +makeLensesWith (lensRules & lensField .~ suffixNamer) ''AWSOpts data Settings = Settings { -- | Maximum allowed size for uploads, in bytes - _maxTotalBytes :: !Int, + maxTotalBytes :: !Int, -- | TTL for download links, in seconds - _downloadLinkTTL :: !Word, + downloadLinkTTL :: !Word, -- | FederationDomain is required, even when not wanting to federate with other backends -- (in that case the 'allowedDomains' can be set to empty in Federator) -- Federation domain is used to qualify local IDs and handles, @@ -141,37 +141,37 @@ data Settings = Settings -- Remember to keep it the same in all services. -- This is referred to as the 'backend domain' in the public documentation; See -- https://docs.wire.com/how-to/install/configure-federation.html#choose-a-backend-domain-name - _federationDomain :: !Domain, - _disabledAPIVersions :: !(Set VersionExp) + federationDomain :: !Domain, + disabledAPIVersions :: !(Set VersionExp) } deriving (Show, Generic) -deriveFromJSON toOptionFieldName ''Settings +deriveFromJSON defaultOptions ''Settings -makeLenses ''Settings +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Settings -- | Options consist of information the server needs to operate, and 'Settings' -- modify the behavior. data Opts = Opts { -- | Hostname and port to bind to - _cargohold :: !Endpoint, - _aws :: !AWSOpts, - _settings :: !Settings, + cargohold :: !Endpoint, + aws :: !AWSOpts, + settings :: !Settings, -- | Federator endpoint - _federator :: Maybe Endpoint, + federator :: Maybe Endpoint, -- | Brig endpoint - _brig :: !Endpoint, + brig :: !Endpoint, -- Logging -- | Log level (Debug, Info, etc) - _logLevel :: !Level, + logLevel :: !Level, -- | Use netstrings encoding: -- - _logNetStrings :: !(Maybe (Last Bool)), - _logFormat :: !(Maybe (Last LogFormat)) --- ^ Log format + logNetStrings :: !(Maybe (Last Bool)), + logFormat :: !(Maybe (Last LogFormat)) --- ^ Log format } deriving (Show, Generic) -deriveFromJSON toOptionFieldName ''Opts +deriveFromJSON defaultOptions ''Opts -makeLenses ''Opts +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Opts diff --git a/services/cargohold/src/CargoHold/Run.hs b/services/cargohold/src/CargoHold/Run.hs index d3bb3764e39..a678a7dd053 100644 --- a/services/cargohold/src/CargoHold/Run.hs +++ b/services/cargohold/src/CargoHold/Run.hs @@ -27,10 +27,10 @@ import qualified Amazonka as AWS import CargoHold.API.Federation import CargoHold.API.Public import CargoHold.AWS (amazonkaEnv) -import CargoHold.App hiding (settings) +import CargoHold.App import CargoHold.Options hiding (aws) import Control.Exception (bracket) -import Control.Lens ((.~), (^.)) +import Control.Lens ((.~)) import Control.Monad.Codensity import Data.Metrics.AWS (gaugeTokenRemaing) import Data.Metrics.Servant @@ -58,14 +58,14 @@ type CombinedAPI = FederationAPI :<|> CargoholdAPI :<|> InternalAPI run :: Opts -> IO () run o = lowerCodensity $ do (app, e) <- mkApp o - void $ Codensity $ Async.withAsync (collectAuthMetrics (e ^. aws . amazonkaEnv)) + void $ Codensity $ Async.withAsync (collectAuthMetrics e.aws.amazonkaEnv) liftIO $ do s <- Server.newSettings $ defaultServer - (unpack . host $ o ^. cargohold) - (port $ o ^. cargohold) - (e ^. appLogger) + (unpack . host $ o.cargohold) + (port o.cargohold) + e.appLogger runSettingsWithShutdown s app Nothing mkApp :: Opts -> Codensity IO (Application, Env) @@ -75,18 +75,18 @@ mkApp o = Codensity $ \k -> where middleware :: Env -> Wai.Middleware middleware e = - versionMiddleware (foldMap expandVersionExp (o ^. settings . disabledAPIVersions)) - . requestIdMiddleware (e ^. appLogger) defaultRequestIdHeaderName + versionMiddleware (foldMap expandVersionExp o.settings.disabledAPIVersions) + . requestIdMiddleware e.appLogger defaultRequestIdHeaderName . servantPrometheusMiddleware (Proxy @CombinedAPI) . GZip.gzip GZip.def - . catchErrors (e ^. appLogger) defaultRequestIdHeaderName + . catchErrors e.appLogger defaultRequestIdHeaderName servantApp :: Env -> Application servantApp e0 r cont = do let rid = getRequestId defaultRequestIdHeaderName r - e = requestId .~ rid $ e0 + e = requestIdLens .~ rid $ e0 Servant.serveWithContext (Proxy @CombinedAPI) - ((o ^. settings . federationDomain) :. Servant.EmptyContext) + (o.settings.federationDomain :. Servant.EmptyContext) ( hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap :<|> hoistServerWithDomain @CargoholdAPI (toServantHandler e) servantSitemap :<|> hoistServerWithDomain @InternalAPI (toServantHandler e) internalSitemap diff --git a/services/cargohold/src/CargoHold/S3.hs b/services/cargohold/src/CargoHold/S3.hs index 849b77dcda8..35b16733a48 100644 --- a/services/cargohold/src/CargoHold/S3.hs +++ b/services/cargohold/src/CargoHold/S3.hs @@ -43,7 +43,7 @@ import CargoHold.API.Error import CargoHold.AWS (amazonkaEnvWithDownloadEndpoint) import qualified CargoHold.AWS as AWS import CargoHold.App hiding (Env, Handler) -import CargoHold.Options (downloadLinkTTL) +import CargoHold.Options import qualified CargoHold.Types.V3 as V3 import qualified Codec.MIME.Parse as MIME import qualified Codec.MIME.Type as MIME @@ -144,7 +144,7 @@ downloadV3 :: V3.AssetKey -> ExceptT Error App (ConduitM () ByteString (ResourceT IO) ()) downloadV3 (s3Key . mkKey -> key) = do - env <- view aws + env <- asks (.aws) pure . flattenResourceT $ view (getObjectResponse_body . _ResponseBody) <$> AWS.execStream env req where req :: Text -> GetObject @@ -216,10 +216,9 @@ updateMetadataV3 (s3Key . mkKey -> key) (S3AssetMeta prc tok _) = do signedURL :: (ToByteString p) => p -> Maybe Text -> ExceptT Error App URI signedURL path mbHost = do e <- awsEnvForHost - let b = view AWS.s3Bucket e now <- liftIO getCurrentTime - ttl <- view (settings . downloadLinkTTL) - let req = newGetObject (BucketName b) (ObjectKey . Text.decodeLatin1 $ toByteString' path) + ttl <- asks (.options.settings.downloadLinkTTL) + let req = newGetObject (BucketName e.s3Bucket) (ObjectKey . Text.decodeLatin1 $ toByteString' path) signed <- presignURL (amazonkaEnvWithDownloadEndpoint e) now (Seconds (fromIntegral ttl)) req toUri signed @@ -235,9 +234,9 @@ signedURL path mbHost = do awsEnvForHost :: ExceptT Error App AWS.Env awsEnvForHost = do - multiIngressConf <- view multiIngress + multiIngressConf <- asks (.multiIngress) if null multiIngressConf - then view aws + then asks (.aws) else awsEnvForHost' mbHost multiIngressConf where awsEnvForHost' :: Maybe Text -> Map String AWS.Env -> ExceptT Error App AWS.Env @@ -262,7 +261,7 @@ signedURL path mbHost = do "host" .= host ~~ "s3DownloadEndpoint" - .= show (hostAwsEnv ^. AWS.amazonkaDownloadEndpoint) + .= show hostAwsEnv.amazonkaDownloadEndpoint ~~ msg (val "awsEnvForHost - multiIngress lookup succeed, using specific AWS env.") pure hostAwsEnv @@ -359,7 +358,7 @@ exec :: (Text -> r) -> ExceptT Error App (AWSResponse r) exec req = do - env <- view aws + env <- asks (.aws) AWS.exec env req execCatch :: @@ -371,7 +370,7 @@ execCatch :: (Text -> r) -> ExceptT Error App (Maybe (AWSResponse r)) execCatch req = do - env <- view aws + env <- asks (.aws) AWS.execCatch env req -------------------------------------------------------------------------------- diff --git a/services/cargohold/src/CargoHold/Util.hs b/services/cargohold/src/CargoHold/Util.hs index 58b27d4fad1..78c2d1a3e6b 100644 --- a/services/cargohold/src/CargoHold/Util.hs +++ b/services/cargohold/src/CargoHold/Util.hs @@ -21,7 +21,6 @@ import CargoHold.AWS import CargoHold.App import qualified CargoHold.CloudFront as CloudFront import qualified CargoHold.S3 as S3 -import Control.Lens import Data.ByteString.Conversion import Imports import URI.ByteString hiding (urlEncode) @@ -29,7 +28,7 @@ import URI.ByteString hiding (urlEncode) genSignedURL :: (ToByteString p) => p -> Maybe Text -> Handler URI genSignedURL path mbHost = do uri <- - view (aws . cloudFront) >>= \case + asks (.aws.cloudFront) >>= \case Nothing -> S3.signedURL path mbHost Just cf -> CloudFront.signedURL cf path pure $! uri diff --git a/services/cargohold/test/integration/API/Util.hs b/services/cargohold/test/integration/API/Util.hs index 6719937f9ab..45aac302da6 100644 --- a/services/cargohold/test/integration/API/Util.hs +++ b/services/cargohold/test/integration/API/Util.hs @@ -107,11 +107,11 @@ downloadAsset = downloadAssetWith id withSettingsOverrides :: (Opts -> Opts) -> TestM a -> TestM a withSettingsOverrides f action = do ts <- ask - let opts = f (view tsOpts ts) + let opts = f ts.opts liftIO . lowerCodensity $ do (app, _) <- mkApp opts p <- withMockServer app - liftIO $ runTestM (ts & tsEndpoint %~ setLocalEndpoint p) action + liftIO $ runTestM (ts & endpointLens %~ setLocalEndpoint p) action setLocalEndpoint :: Word16 -> Endpoint -> Endpoint setLocalEndpoint port endpoint = endpoint {port = port, host = "127.0.0.1"} @@ -123,5 +123,5 @@ withMockFederator :: withMockFederator respond action = do withTempMockFederator def {handler = respond} $ \p -> withSettingsOverrides - (federator . _Just %~ setLocalEndpoint (fromIntegral p)) + (federatorLens . _Just %~ setLocalEndpoint (fromIntegral p)) action diff --git a/services/cargohold/test/integration/App.hs b/services/cargohold/test/integration/App.hs index b5ba0132d88..3f2fb0ec810 100644 --- a/services/cargohold/test/integration/App.hs +++ b/services/cargohold/test/integration/App.hs @@ -30,9 +30,9 @@ testMultiIngressCloudFrontFails :: TestM () testMultiIngressCloudFrontFails = do ts <- ask let opts = - view tsOpts ts - & (Opts.aws . Opts.cloudFront) ?~ cloudFrontOptions - & (Opts.aws . Opts.multiIngress) ?~ multiIngressMap + view optsLens ts + & (Opts.awsLens . Opts.cloudFrontLens) ?~ cloudFrontOptions + & (Opts.awsLens . Opts.multiIngressLens) ?~ multiIngressMap msg <- liftIO $ catch @@ -46,9 +46,9 @@ testMultiIngressCloudFrontFails = do cloudFrontOptions :: CloudFrontOpts cloudFrontOptions = CloudFrontOpts - { _domain = Domain (T.pack "example.com"), - _keyPairId = KeyPairId (T.pack "anyId"), - _privateKey = "any/path" + { domain = Domain (T.pack "example.com"), + keyPairId = KeyPairId (T.pack "anyId"), + privateKey = "any/path" } multiIngressMap :: Map String AWSEndpoint @@ -64,9 +64,9 @@ testMultiIngressS3DownloadEndpointFails :: TestM () testMultiIngressS3DownloadEndpointFails = do ts <- ask let opts = - view tsOpts ts - & (Opts.aws . Opts.s3DownloadEndpoint) ?~ toAWSEndpoint "http://fake-s3:4570" - & (Opts.aws . Opts.multiIngress) ?~ multiIngressMap + view optsLens ts + & (Opts.awsLens . Opts.s3DownloadEndpointLens) ?~ toAWSEndpoint "http://fake-s3:4570" + & (Opts.awsLens . Opts.multiIngressLens) ?~ multiIngressMap msg <- liftIO $ catch diff --git a/services/cargohold/test/integration/TestSetup.hs b/services/cargohold/test/integration/TestSetup.hs index 21bdcc431ac..629158039c0 100644 --- a/services/cargohold/test/integration/TestSetup.hs +++ b/services/cargohold/test/integration/TestSetup.hs @@ -19,10 +19,10 @@ module TestSetup ( test, - tsManager, - tsEndpoint, - tsBrig, - tsOpts, + managerLens, + endpointLens, + TestSetup.brigLens, + optsLens, TestSetup (..), Cargohold, TestM, @@ -56,6 +56,7 @@ import Test.Tasty import Test.Tasty.HUnit import Util.Options (Endpoint (..)) import Util.Options.Common +import Util.SuffixNamer import Util.Test import Web.HttpApiData import Wire.API.Federation.Domain @@ -69,13 +70,13 @@ mkRequest :: Endpoint -> Request -> Request mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p data TestSetup = TestSetup - { _tsManager :: Manager, - _tsEndpoint :: Endpoint, - _tsBrig :: Endpoint, - _tsOpts :: Opts + { manager :: Manager, + endpoint :: Endpoint, + brig :: Endpoint, + opts :: Opts } -makeLenses ''TestSetup +makeLensesWith (lensRules & lensField .~ suffixNamer) ''TestSetup -- | Note: Apply this function last when composing (Request -> Request) functions apiVersion :: ByteString -> Request -> Request @@ -99,7 +100,7 @@ removeVersionPrefix bs = do pure (B8.tail s') viewUnversionedCargohold :: TestM Cargohold -viewUnversionedCargohold = mkRequest <$> view tsEndpoint +viewUnversionedCargohold = mkRequest <$> asks (.endpoint) viewCargohold :: TestM Cargohold viewCargohold = @@ -111,7 +112,7 @@ viewCargohold = latestVersion = maxBound runTestM :: TestSetup -> TestM a -> IO a -runTestM ts action = runHttpT (view tsManager ts) (runReaderT action ts) +runTestM ts action = runHttpT ts.manager (runReaderT action ts) test :: IO TestSetup -> TestName -> TestM () -> TestTree test s name action = testCase name $ do @@ -145,17 +146,17 @@ createTestSetup optsPath configPath = do brigEndpoint <- optOrEnv @IntegrationConfig (.brig) iConf (localEndpoint . read) "BRIG_WEB_PORT" pure $ TestSetup - { _tsManager = m, - _tsEndpoint = endpoint, - _tsBrig = brigEndpoint, - _tsOpts = opts + { manager = m, + endpoint = endpoint, + brig = brigEndpoint, + opts = opts } runFederationClient :: ClientM a -> ReaderT TestSetup (ExceptT ClientError (Codensity IO)) a runFederationClient action = do - man <- view tsManager - Endpoint cHost cPort <- view tsEndpoint - domain <- view (tsOpts . settings . federationDomain) + man <- asks (.manager) + Endpoint cHost cPort <- asks (.endpoint) + domain <- asks (.opts.settings.federationDomain) let base = BaseUrl Http (T.unpack cHost) (fromIntegral cPort) "/federation" let env = (mkClientEnv man base) diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 5ebad513109..036a2d9d16f 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -29,7 +29,7 @@ where import Brig.Types.Intra import Control.Error -import Control.Lens ((.~), (^.)) +import Control.Lens ((.~)) import Control.Monad.Except import Data.Aeson hiding (Error, json) import Data.Aeson.KeyMap qualified as KeyMap @@ -81,15 +81,15 @@ start :: Opts -> IO () start o = do e <- newEnv o s <- Server.newSettings (server e) - Server.runSettingsWithShutdown s (requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName $ servantApp e) Nothing + Server.runSettingsWithShutdown s (requestIdMiddleware e.appLogger defaultRequestIdHeaderName $ servantApp e) Nothing where server :: Env -> Server.Server - server e = Server.defaultServer (unpack o.stern.host) o.stern.port e._applog + server e = Server.defaultServer (unpack o.stern.host) o.stern.port e.appLogger servantApp :: Env -> Application servantApp e0 req cont = do let rid = getRequestId defaultRequestIdHeaderName req - let e = requestId .~ rid $ e0 + let e = requestIdLens .~ rid $ e0 Servant.serve ( Proxy @( SwaggerDocsAPI diff --git a/tools/stern/src/Stern/App.hs b/tools/stern/src/Stern/App.hs index 8203c87ebcd..1056cf37182 100644 --- a/tools/stern/src/Stern/App.hs +++ b/tools/stern/src/Stern/App.hs @@ -26,7 +26,7 @@ module Stern.App where import Bilge qualified import Bilge.RPC (HasRequestId (..)) import Control.Error -import Control.Lens (makeLenses, view) +import Control.Lens (lensField, lensRules, makeLensesWith, (.~)) import Control.Monad.Catch (MonadCatch, MonadThrow) import Control.Monad.IO.Class import Control.Monad.Reader.Class @@ -38,30 +38,38 @@ import Imports import Network.HTTP.Client (responseTimeoutMicro) import Network.Wai (Response, ResponseReceived) import Network.Wai.Utilities (Error (..)) -import Stern.Options as O +import Stern.Options as Opts import System.Logger qualified as Log import System.Logger.Class hiding (Error, info) import System.Logger.Class qualified as LC import System.Logger.Extended qualified as Log import Util.Options +import Util.SuffixNamer data Env = Env - { _brig :: !Bilge.Request, - _galley :: !Bilge.Request, - _gundeck :: !Bilge.Request, - _ibis :: !Bilge.Request, - _galeb :: !Bilge.Request, - _applog :: !Logger, - _requestId :: !Bilge.RequestId, - _httpManager :: !Bilge.Manager + { brig :: !Bilge.Request, + galley :: !Bilge.Request, + gundeck :: !Bilge.Request, + ibis :: !Bilge.Request, + galeb :: !Bilge.Request, + appLogger :: !Logger, + requestId :: !Bilge.RequestId, + httpManager :: !Bilge.Manager } -makeLenses ''Env +makeLensesWith (lensRules & lensField .~ suffixNamer) ''Env newEnv :: Opts -> IO Env -newEnv o = do - l <- Log.mkLogger (O.logLevel o) (O.logNetStrings o) (O.logFormat o) - Env (mkRequest $ O.brig o) (mkRequest $ O.galley o) (mkRequest $ O.gundeck o) (mkRequest $ O.ibis o) (mkRequest $ O.galeb o) l (RequestId "N/A") +newEnv opts = do + l <- Log.mkLogger opts.logLevel opts.logNetStrings opts.logFormat + Env + (mkRequest opts.brig) + (mkRequest opts.galley) + (mkRequest opts.gundeck) + (mkRequest opts.ibis) + (mkRequest opts.galeb) + l + (RequestId "N/A") <$> newManager where mkRequest s = Bilge.host (encodeUtf8 s.host) . Bilge.port s.port $ Bilge.empty @@ -85,8 +93,8 @@ type App = AppT IO instance (MonadIO m) => MonadLogger (AppT m) where log l m = do - g <- view applog - r <- view requestId + g <- asks (.appLogger) + r <- asks (.requestId) Log.log g l $ "request" .= Bilge.unRequestId r ~~ m instance MonadLogger (ExceptT e App) where @@ -94,18 +102,18 @@ instance MonadLogger (ExceptT e App) where instance (MonadIO m) => Bilge.MonadHttp (AppT m) where handleRequestWithCont req h = do - m <- view httpManager + m <- asks (.httpManager) liftIO $ Bilge.withResponse req m h instance (Monad m) => HasRequestId (AppT m) where - getRequestId = view requestId + getRequestId = asks (.requestId) instance HasRequestId (ExceptT e App) where - getRequestId = view requestId + getRequestId = asks (.requestId) instance Bilge.MonadHttp (ExceptT e App) where handleRequestWithCont req h = do - m <- view httpManager + m <- asks (.httpManager) liftIO $ Bilge.withResponse req m h runAppT :: Env -> AppT m a -> m a diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 916dbd43e52..6b9f0d0890a 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -141,7 +141,7 @@ versionedPaths = Bilge.paths . (encodeUtf8 (toUrlPiece backendApiVersion) :) putUserStatus :: AccountStatus -> UserId -> Handler () putUserStatus status uid = do info $ userMsg uid . msg "Changing user status" - b <- view brig + b <- asks (.brig) void $ catchRpcErrors $ rpc' @@ -171,7 +171,7 @@ getUserConnections uid = do else pure (batch ++ xs) fetchBatch :: Maybe UserId -> Handler UserConnectionList fetchBatch start = do - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -190,7 +190,7 @@ getUserConnections uid = do getUsersConnections :: List UserId -> Handler [ConnectionStatus] getUsersConnections uids = do info $ msg "Getting user connections" - b <- view brig + b <- asks (.brig) let reqBody = ConnectionsStatusRequest (fromList uids) Nothing r <- catchRpcErrors $ @@ -208,7 +208,7 @@ getUsersConnections uids = do getUserProfiles :: Either [UserId] [Handle] -> Handler [UserAccount] getUserProfiles uidsOrHandles = do info $ msg "Getting user accounts" - b <- view brig + b <- asks (.brig) concat <$> mapM (doRequest b) (prepareQS uidsOrHandles) where doRequest :: Request -> (Request -> Request) -> Handler [UserAccount] @@ -235,7 +235,7 @@ getUserProfiles uidsOrHandles = do getUserProfilesByIdentity :: EmailAddress -> Handler [UserAccount] getUserProfilesByIdentity email = do info $ msg "Getting user accounts by identity" - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -251,7 +251,7 @@ getUserProfilesByIdentity email = do getEjpdInfo :: [Handle] -> Bool -> Handler EJPD.EJPDResponseBody getEjpdInfo handles includeContacts = do info $ msg "Getting ejpd info on users by handle" - b <- view brig + b <- asks (.brig) let bdy :: Value bdy = object @@ -274,7 +274,7 @@ getEjpdInfo handles includeContacts = do getContacts :: UserId -> Text -> Int32 -> Handler (SearchResult Contact) getContacts u q s = do info $ msg "Getting user contacts" - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -292,7 +292,7 @@ getContacts u q s = do revokeIdentity :: EmailAddress -> Handler () revokeIdentity email = do info $ msg "Revoking user identity" - b <- view brig + b <- asks (.brig) void . catchRpcErrors $ rpc' @@ -307,7 +307,7 @@ revokeIdentity email = do deleteAccount :: UserId -> Handler () deleteAccount uid = do info $ msg "Deleting account" - b <- view brig + b <- asks (.brig) void . catchRpcErrors $ rpc' @@ -325,7 +325,7 @@ setStatusBindingTeam tid status = do ( "Setting team status to " <> UTF8.toString (BS.toStrict . encode $ status) ) - g <- view galley + g <- asks (.galley) void . catchRpcErrors $ rpc' @@ -340,7 +340,7 @@ setStatusBindingTeam tid status = do deleteBindingTeam :: TeamId -> Handler () deleteBindingTeam tid = do info $ msg "Deleting team" - g <- view galley + g <- asks (.galley) void . catchRpcErrors $ rpc' @@ -355,7 +355,7 @@ deleteBindingTeam tid = do deleteBindingTeamForce :: TeamId -> Handler () deleteBindingTeamForce tid = do info $ msg "Deleting team with force flag" - g <- view galley + g <- asks (.galley) void . catchRpcErrors $ rpc' @@ -370,7 +370,7 @@ deleteBindingTeamForce tid = do changeEmail :: UserId -> EmailUpdate -> Handler () changeEmail u upd = do info $ msg "Updating email address" - b <- view brig + b <- asks (.brig) void . catchRpcErrors $ rpc' @@ -395,7 +395,7 @@ getTeamInfo tid = do getUserBindingTeam :: UserId -> Handler (Maybe TeamId) getUserBindingTeam u = do info $ msg "Getting user binding team" - g <- view galley + g <- asks (.galley) r <- catchRpcErrors $ rpc' @@ -418,7 +418,7 @@ getUserBindingTeam u = do getInvoiceUrl :: TeamId -> InvoiceId -> Handler ByteString getInvoiceUrl tid iid = do info $ msg "Getting invoice" - i <- view ibis + i <- asks (.ibis) r <- catchRpcErrors $ rpc' @@ -434,7 +434,7 @@ getInvoiceUrl tid iid = do getTeamBillingInfo :: TeamId -> Handler (Maybe TeamBillingInfo) getTeamBillingInfo tid = do info $ msg "Getting team billing info" - i <- view ibis + i <- asks (.ibis) resp <- catchRpcErrors $ rpc' @@ -451,7 +451,7 @@ getTeamBillingInfo tid = do setTeamBillingInfo :: TeamId -> TeamBillingInfo -> Handler () setTeamBillingInfo tid tbu = do info $ msg "Setting team billing info" - i <- view ibis + i <- asks (.ibis) void . catchRpcErrors $ rpc' @@ -467,7 +467,7 @@ setTeamBillingInfo tid tbu = do isBlacklisted :: EmailAddress -> Handler Bool isBlacklisted email = do info $ msg "Checking blacklist" - b <- view brig + b <- asks (.brig) resp <- catchRpcErrors $ rpc' @@ -485,7 +485,7 @@ isBlacklisted email = do setBlacklistStatus :: Bool -> EmailAddress -> Handler () setBlacklistStatus status email = do info $ msg "Changing blacklist status" - b <- view brig + b <- asks (.brig) void . catchRpcErrors $ rpc' @@ -507,7 +507,7 @@ getTeamFeatureFlag :: Handler (Public.LockableFeature cfg) getTeamFeatureFlag tid = do info $ msg "Getting team feature status" - gly <- view galley + gly <- asks (.galley) let req = method GET . Bilge.paths ["i", "teams", toByteString' tid, "features", Public.featureNameBS @cfg] @@ -547,7 +547,7 @@ patchTeamFeatureFlag tid patch = do galleyRpc :: (Bilge.Request -> Bilge.Request) -> Handler () galleyRpc req = do - gly <- view galley + gly <- asks (.galley) resp <- catchRpcErrors $ rpc' "galley" gly req case statusCode resp of 200 -> pure () @@ -563,7 +563,7 @@ setTeamFeatureLockStatus :: Handler () setTeamFeatureLockStatus tid lstat = do info $ msg ("Setting lock status: " <> featureName @cfg) - gly <- view galley + gly <- asks (.galley) fromResponseBody <=< catchRpcErrors $ rpc' @@ -586,7 +586,7 @@ setTeamFeatureLockStatus tid lstat = do getSearchVisibility :: TeamId -> Handler TeamSearchVisibilityView getSearchVisibility tid = do info $ msg "Getting TeamSearchVisibilityView value" - gly <- view galley + gly <- asks (.galley) fromResponseBody <=< catchRpcErrors $ rpc' @@ -603,7 +603,7 @@ getSearchVisibility tid = do setSearchVisibility :: TeamId -> TeamSearchVisibility -> Handler () setSearchVisibility tid typ = do info $ msg "Setting TeamSearchVisibility value" - gly <- view galley + gly <- asks (.galley) resp <- catchRpcErrors $ rpc' @@ -650,7 +650,7 @@ catchRpcErrors action = ExceptT $ catch (Right <$> action) catchRPCException getTeamData :: TeamId -> Handler TeamData getTeamData tid = do info $ msg "Getting team information" - g <- view galley + g <- asks (.galley) r <- catchRpcErrors $ rpc' @@ -667,7 +667,7 @@ getTeamData tid = do getTeamMembers :: TeamId -> Handler TeamMemberList getTeamMembers tid = do info $ msg "Getting team members" - g <- view galley + g <- asks (.galley) r <- catchRpcErrors $ rpc' @@ -682,7 +682,7 @@ getTeamMembers tid = do getEmailConsentLog :: EmailAddress -> Handler ConsentLog getEmailConsentLog email = do info $ msg "Getting email consent log" - g <- view galeb + g <- asks (.galeb) r <- catchRpcErrors $ rpc' @@ -700,7 +700,7 @@ getEmailConsentLog email = do getUserConsentValue :: UserId -> Handler ConsentValue getUserConsentValue uid = do info $ msg "Getting user consent value" - g <- view galeb + g <- asks (.galeb) r <- catchRpcErrors $ rpc' @@ -716,7 +716,7 @@ getUserConsentValue uid = do getMarketoResult :: EmailAddress -> Handler MarketoResult getMarketoResult email = do info $ msg "Getting marketo results" - g <- view galeb + g <- asks (.galeb) r <- catchRpcErrors $ rpc' @@ -745,7 +745,7 @@ getMarketoResult email = do getUserConsentLog :: UserId -> Handler ConsentLog getUserConsentLog uid = do info $ msg "Getting user consent log" - g <- view galeb + g <- asks (.galeb) r <- catchRpcErrors $ rpc' @@ -760,7 +760,7 @@ getUserConsentLog uid = do getUserCookies :: UserId -> Handler CookieList getUserCookies uid = do info $ msg "Getting user cookies" - g <- view brig + g <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -788,7 +788,7 @@ getUserConversations uid maxConvs = do else pure (batch ++ xs) fetchBatch :: Maybe ConvId -> Int -> Handler (ConversationList Conversation) fetchBatch start batchSize = do - baseReq <- view galley + baseReq <- asks (.galley) r <- catchRpcErrors $ rpc' @@ -806,7 +806,7 @@ getUserConversations uid maxConvs = do getUserClients :: UserId -> Handler [Client] getUserClients uid = do info $ msg "Getting user clients" - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -828,7 +828,7 @@ getUserClients uid = do getUserProperties :: UserId -> Handler UserProperties getUserProperties uid = do info $ msg "Getting user properties" - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -878,7 +878,7 @@ getUserNotifications uid maxNotifs = do else pure (batch ++ xs) fetchBatch :: Maybe NotificationId -> Int -> Handler (Maybe QueuedNotificationList) fetchBatch start batchSize = do - baseReq <- view gundeck + baseReq <- asks (.gundeck) r <- catchRpcErrors $ rpc' @@ -916,7 +916,7 @@ getSsoDomainRedirect :: Text -> Handler (Maybe CustomBackend) getSsoDomainRedirect domain = do info $ msg "getSsoDomainRedirect" -- curl -XGET ${CLOUD_BACKEND}/custom-backend/by-domain/${DOMAIN_EXAMPLE} - g <- view galley + g <- asks (.galley) r <- catchRpcErrors $ rpc' @@ -939,7 +939,7 @@ putSsoDomainRedirect domain config welcome = do -- "webapp_welcome_url": "https://app.wire.example.com/" \ -- }' -- curl -XPUT http://localhost/i/custom-backend/by-domain/${DOMAIN_EXAMPLE} -d "${DOMAIN_ENTRY}" - g <- view galley + g <- asks (.galley) void . catchRpcErrors $ rpc' @@ -960,7 +960,7 @@ deleteSsoDomainRedirect :: Text -> Handler () deleteSsoDomainRedirect domain = do info $ msg "deleteSsoDomainRedirect" -- curl -XDELETE http://localhost/i/custom-backend/by-domain/${DOMAIN_EXAMPLE} - g <- view galley + g <- asks (.galley) void . catchRpcErrors $ rpc' @@ -978,7 +978,7 @@ deleteSsoDomainRedirect domain = do registerOAuthClient :: OAuthClientConfig -> Handler OAuthClientCredentials registerOAuthClient conf = do - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -994,7 +994,7 @@ registerOAuthClient conf = do getOAuthClient :: OAuthClientId -> Handler OAuthClient getOAuthClient cid = do - b <- view brig + b <- asks (.brig) r <- lift $ rpc' @@ -1010,7 +1010,7 @@ getOAuthClient cid = do updateOAuthClient :: OAuthClientId -> OAuthClientConfig -> Handler OAuthClient updateOAuthClient cid conf = do - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' @@ -1026,7 +1026,7 @@ updateOAuthClient cid conf = do deleteOAuthClient :: OAuthClientId -> Handler () deleteOAuthClient cid = do - b <- view brig + b <- asks (.brig) r <- catchRpcErrors $ rpc' From 398a497bf3e4ff42e4f0b2edf511b47f180b3a47 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 26 Sep 2024 10:58:08 +0200 Subject: [PATCH 091/136] WPB-11217 Move accept team invitation to user subsystem (#4264) - 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). --- ...rsonal-users-into-teams-to-wire-subsystems | 10 + libs/types-common/default.nix | 2 + libs/types-common/src/Util/Logging.hs | 5 + libs/types-common/src/Util/Timeout.hs | 4 + libs/types-common/types-common.cabal | 1 + libs/wire-api/src/Wire/API/Error/Brig.hs | 3 + .../src/Wire/API/Routes/Public/Brig.hs | 2 +- .../src/Wire/AuthenticationSubsystem.hs | 3 + .../src/Wire/AuthenticationSubsystem/Error.hs | 4 + .../AuthenticationSubsystem/Interpreter.hs | 26 +- .../src/Wire/EmailSubsystem.hs | 5 + .../src/Wire/EmailSubsystem/Interpreter.hs | 88 +++++- .../src/Wire/EmailSubsystem/Template.hs | 57 ++-- .../src/Wire/IndexedUserStore.hs | 2 + .../Wire/IndexedUserStore/ElasticSearch.hs | 22 ++ .../src/Wire/TeamInvitationSubsystem.hs | 20 ++ .../src/Wire/TeamInvitationSubsystem/Error.hs | 23 ++ .../TeamInvitationSubsystem/Interpreter.hs | 267 +++++++++++++++++ libs/wire-subsystems/src/Wire/UserStore.hs | 1 + .../src/Wire/UserStore/Cassandra.hs | 7 + .../wire-subsystems/src/Wire/UserSubsystem.hs | 31 +- .../src/Wire/UserSubsystem/Error.hs | 18 ++ .../src/Wire/UserSubsystem/Interpreter.hs | 193 ++++++++---- .../InterpreterSpec.hs | 169 +++++------ .../test/unit/Wire/MiniBackend.hs | 89 ++++-- .../Wire/MockInterpreters/EmailSubsystem.hs | 16 + .../Wire/MockInterpreters/IndexedUserStore.hs | 1 + .../PasswordResetCodeStore.hs | 6 + .../Wire/MockInterpreters/PasswordStore.hs | 3 + .../Wire/MockInterpreters/SessionStore.hs | 6 + .../unit/Wire/MockInterpreters/UserStore.hs | 6 + .../Wire/UserSubsystem/InterpreterSpec.hs | 54 ++-- libs/wire-subsystems/wire-subsystems.cabal | 3 + services/brig/brig.cabal | 1 - services/brig/src/Brig/API/Internal.hs | 24 +- services/brig/src/Brig/API/Public.hs | 13 +- services/brig/src/Brig/API/User.hs | 59 +--- .../brig/src/Brig/CanonicalInterpreter.hs | 39 ++- services/brig/src/Brig/Data/User.hs | 7 - services/brig/src/Brig/Team/API.hs | 278 ++++-------------- services/brig/src/Brig/Team/Email.hs | 60 +--- services/brig/src/Brig/Team/Template.hs | 36 +-- .../brig/src/Brig/User/Search/TeamSize.hs | 46 --- 43 files changed, 999 insertions(+), 711 deletions(-) create mode 100644 changelog.d/5-internal/WPB-11217-move-code-for-accepting-invitations-for-personal-users-into-teams-to-wire-subsystems create mode 100644 libs/wire-subsystems/src/Wire/TeamInvitationSubsystem.hs create mode 100644 libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Error.hs create mode 100644 libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs delete mode 100644 services/brig/src/Brig/User/Search/TeamSize.hs 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 new file mode 100644 index 00000000000..0d0f46a242f --- /dev/null +++ b/changelog.d/5-internal/WPB-11217-move-code-for-accepting-invitations-for-personal-users-into-teams-to-wire-subsystems @@ -0,0 +1,10 @@ +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/libs/types-common/default.nix b/libs/types-common/default.nix index 5d16d0cfaf0..3b39ec41402 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -19,6 +19,7 @@ , cryptohash-sha1 , crypton , currency-codes +, email-validate , generic-random , gitignoreSource , hashable @@ -79,6 +80,7 @@ mkDerivation { cryptohash-sha1 crypton currency-codes + email-validate generic-random hashable http-api-data diff --git a/libs/types-common/src/Util/Logging.hs b/libs/types-common/src/Util/Logging.hs index 318785c7578..8a242a3d664 100644 --- a/libs/types-common/src/Util/Logging.hs +++ b/libs/types-common/src/Util/Logging.hs @@ -25,6 +25,7 @@ import Data.Text.Encoding (encodeUtf8) import Imports import System.Logger.Class qualified as Log import System.Logger.Message (Msg) +import Text.Email.Parser sha256String :: Text -> Text sha256String t = @@ -48,3 +49,7 @@ logUser uid = Log.field "user" (T.pack . show $ uid) logTeam :: TeamId -> (Msg -> Msg) logTeam tid = Log.field "team" (T.pack . show $ tid) + +logEmail :: EmailAddress -> (Msg -> Msg) +logEmail email = + Log.field "email_sha256" (sha256String . T.pack . show $ email) diff --git a/libs/types-common/src/Util/Timeout.hs b/libs/types-common/src/Util/Timeout.hs index e09c358e88d..35dcde3a52f 100644 --- a/libs/types-common/src/Util/Timeout.hs +++ b/libs/types-common/src/Util/Timeout.hs @@ -9,12 +9,16 @@ import Data.Aeson.Types import Data.Scientific import Data.Time.Clock import Imports +import Test.QuickCheck (Arbitrary (arbitrary), choose) newtype Timeout = Timeout { timeoutDiff :: NominalDiffTime } deriving newtype (Eq, Enum, Ord, Num, Real, Fractional, RealFrac, Show) +instance Arbitrary Timeout where + arbitrary = Timeout . fromIntegral <$> choose (60 :: Int, 10 * 24 * 3600) + instance Read Timeout where readsPrec i s = case readsPrec i s of diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 53ac138e7f2..528890fe064 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -109,6 +109,7 @@ library , cryptohash-sha1 >=0.11.7.2 , crypton >=0.26 , currency-codes >=3.0.0.1 + , email-validate , generic-random >=1.4.0.0 , hashable >=1.2 , http-api-data diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index 15ecfabe55c..e5e2290b576 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -63,6 +63,7 @@ data BrigError | AccountEphemeral | AccountPending | UserKeyExists + | EmailExists | NameManagedByScim | HandleManagedByScim | LocaleManagedByScim @@ -239,6 +240,8 @@ type instance MapError 'AccountPending = 'StaticError 403 "pending-activation" " type instance MapError 'UserKeyExists = 'StaticError 409 "key-exists" "The given e-mail address is in use." +type instance MapError 'EmailExists = 'StaticError 409 "email-exists" "The given e-mail address is in use." + type instance MapError 'NameManagedByScim = 'StaticError 403 "managed-by-scim" "Updating name is not allowed, because it is managed by SCIM, or E2EId is enabled" type instance MapError 'HandleManagedByScim = 'StaticError 403 "managed-by-scim" "Updating handle is not allowed, because it is managed by SCIM, or E2EId is enabled" 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 6e457fefa6d..53ebe332541 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -1574,7 +1574,7 @@ type TeamsAPI = :> CanThrow 'TooManyTeamInvitations :> CanThrow 'InsufficientTeamPermissions :> CanThrow 'InvalidInvitationCode - :> ZUser + :> ZLocalUser :> "teams" :> Capture "tid" TeamId :> "invitations" diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs index 9b669979bd8..e4200377d92 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs @@ -18,7 +18,9 @@ module Wire.AuthenticationSubsystem where +import Data.Id import Data.Misc +import Data.Qualified import Imports import Polysemy import Wire.API.User @@ -26,6 +28,7 @@ import Wire.API.User.Password import Wire.UserKeyStore data AuthenticationSubsystem m a where + VerifyPassword :: Local UserId -> PlainTextPassword6 -> AuthenticationSubsystem m () CreatePasswordResetCode :: EmailKey -> AuthenticationSubsystem m () ResetPassword :: PasswordResetIdentity -> PasswordResetCode -> PlainTextPassword8 -> AuthenticationSubsystem m () -- For testing diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs index 5efede38c26..095bb9dfdbc 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs @@ -31,6 +31,8 @@ data AuthenticationSubsystemError | AuthenticationSubsystemInvalidPasswordResetCode | AuthenticationSubsystemInvalidPhone | AuthenticationSubsystemAllowListError + | AuthenticationSubsystemMissingAuth + | AuthenticationSubsystemBadCredentials deriving (Eq, Show) instance Exception AuthenticationSubsystemError @@ -43,3 +45,5 @@ authenticationSubsystemErrorToHttpError = AuthenticationSubsystemResetPasswordMustDiffer -> errorToWai @E.ResetPasswordMustDiffer AuthenticationSubsystemInvalidPhone -> errorToWai @E.InvalidPhone AuthenticationSubsystemAllowListError -> errorToWai @E.AllowlistError + AuthenticationSubsystemMissingAuth -> errorToWai @E.MissingAuth + AuthenticationSubsystemBadCredentials -> errorToWai @E.BadCredentials diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs index 2d28021a6a1..fc13fb05a01 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs @@ -39,7 +39,7 @@ import Wire.API.Allowlists qualified as AllowLists import Wire.API.Password import Wire.API.User import Wire.API.User.Password -import Wire.AuthenticationSubsystem +import Wire.AuthenticationSubsystem (AuthenticationSubsystem (..)) import Wire.AuthenticationSubsystem.Error import Wire.EmailSubsystem import Wire.HashPassword @@ -62,15 +62,29 @@ interpretAuthenticationSubsystem :: Member SessionStore r, Member (Input (Local ())) r, Member (Input (Maybe AllowlistEmailDomains)) r, - Member UserSubsystem r, Member PasswordStore r, Member EmailSubsystem r ) => + InterpreterFor UserSubsystem r -> InterpreterFor AuthenticationSubsystem r -interpretAuthenticationSubsystem = interpret $ \case - CreatePasswordResetCode userKey -> createPasswordResetCodeImpl userKey - ResetPassword ident resetCode newPassword -> resetPasswordImpl ident resetCode newPassword - InternalLookupPasswordResetCode userKey -> internalLookupPasswordResetCodeImpl userKey +interpretAuthenticationSubsystem userSubsystemInterpreter = + interpret $ + userSubsystemInterpreter . \case + VerifyPassword luid password -> verifyPasswordImpl luid password + CreatePasswordResetCode userKey -> createPasswordResetCodeImpl userKey + ResetPassword ident resetCode newPassword -> resetPasswordImpl ident resetCode newPassword + InternalLookupPasswordResetCode userKey -> internalLookupPasswordResetCodeImpl userKey + +verifyPasswordImpl :: + ( Member PasswordStore r, + Member (Error AuthenticationSubsystemError) r + ) => + Local UserId -> + PlainTextPassword6 -> + Sem r () +verifyPasswordImpl (tUnqualified -> uid) password = do + p <- lookupHashedPassword uid >>= maybe (throw AuthenticationSubsystemMissingAuth) pure + unless (Wire.API.Password.verifyPassword password p) $ throw AuthenticationSubsystemBadCredentials maxAttempts :: Int32 maxAttempts = 3 diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs index e4090103799..a8fa0f57b5e 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs @@ -3,6 +3,7 @@ module Wire.EmailSubsystem where import Data.Code qualified as Code +import Data.Id import Imports import Polysemy import Wire.API.Locale @@ -22,5 +23,9 @@ data EmailSubsystem m a where 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. + SendTeamInvitationMailPersonalUser :: EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> EmailSubsystem m Text makeSem ''EmailSubsystem diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs index a152b166af1..a78e26f3754 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs @@ -7,6 +7,7 @@ module Wire.EmailSubsystem.Interpreter where import Data.Code qualified as Code +import Data.Id import Data.Json.Util import Data.Range (fromRange) import Data.Text qualified as Text @@ -24,19 +25,21 @@ import Wire.EmailSending (EmailSending, sendMail) import Wire.EmailSubsystem import Wire.EmailSubsystem.Template -emailSubsystemInterpreter :: (Member EmailSending r) => Localised UserTemplates -> TemplateBranding -> InterpreterFor EmailSubsystem r -emailSubsystemInterpreter tpls branding = interpret \case - SendPasswordResetMail email (key, code) mLocale -> sendPasswordResetMailImpl tpls branding email key code mLocale - SendVerificationMail email key code mLocale -> sendVerificationMailImpl tpls branding email key code mLocale - SendTeamDeletionVerificationMail email code mLocale -> sendTeamDeletionVerificationMailImpl tpls branding email code mLocale - SendCreateScimTokenVerificationMail email code mLocale -> sendCreateScimTokenVerificationMailImpl tpls branding email code mLocale - SendLoginVerificationMail email code mLocale -> sendLoginVerificationMailImpl tpls branding email code mLocale - SendActivationMail email name key code mLocale -> sendActivationMailImpl tpls branding email name key code mLocale - SendEmailAddressUpdateMail email name key code mLocale -> sendEmailAddressUpdateMailImpl tpls branding email name key code mLocale - SendTeamActivationMail email name key code mLocale teamName -> sendTeamActivationMailImpl tpls branding email name key code mLocale teamName - SendNewClientEmail email name client locale -> sendNewClientEmailImpl tpls branding email name client locale - SendAccountDeletionEmail email name key code locale -> sendAccountDeletionEmailImpl tpls branding email name key code locale - SendUpgradePersonalToTeamConfirmationEmail email name teamName locale -> sendUpgradePersonalToTeamConfirmationEmailImpl tpls branding email name teamName locale +emailSubsystemInterpreter :: (Member EmailSending r) => Localised UserTemplates -> Localised TeamTemplates -> TemplateBranding -> InterpreterFor EmailSubsystem r +emailSubsystemInterpreter userTpls teamTpls branding = interpret \case + SendPasswordResetMail email (key, code) mLocale -> sendPasswordResetMailImpl userTpls branding email key code mLocale + SendVerificationMail email key code mLocale -> sendVerificationMailImpl userTpls branding email key code mLocale + SendTeamDeletionVerificationMail email code mLocale -> sendTeamDeletionVerificationMailImpl userTpls branding email code mLocale + SendCreateScimTokenVerificationMail email code mLocale -> sendCreateScimTokenVerificationMailImpl userTpls branding email code mLocale + SendLoginVerificationMail email code mLocale -> sendLoginVerificationMailImpl userTpls branding email code mLocale + SendActivationMail email name key code mLocale -> sendActivationMailImpl userTpls branding email name key code mLocale + SendEmailAddressUpdateMail email name key code mLocale -> sendEmailAddressUpdateMailImpl userTpls branding email name key code mLocale + 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 ------------------------------------------------------------------------------- -- Verification Email for @@ -432,6 +435,65 @@ renderUpgradePersonalToTeamConfirmationEmail email name _teamName UpgradePersona replace1 "name" = fromName name replace1 x = x +------------------------------------------------------------------------------- +-- Invitation Email + +sendTeamInvitationMailImpl :: (Member EmailSending r) => Localised TeamTemplates -> TemplateBranding -> EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> Sem r Text +sendTeamInvitationMailImpl teamTemplates branding to tid from code loc = do + let tpl = invitationEmail . snd $ forLocale loc teamTemplates + mail = InvitationEmail to tid code from + (renderedMail, renderedInvitaitonUrl) = renderInvitationEmail mail tpl branding + sendMail renderedMail + pure renderedInvitaitonUrl + +sendTeamInvitationMailPersonalUserImpl :: (Member EmailSending r) => Localised TeamTemplates -> TemplateBranding -> EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> Sem r Text +sendTeamInvitationMailPersonalUserImpl teamTemplates branding to tid from code loc = do + let tpl = existingUserInvitationEmail . snd $ forLocale loc teamTemplates + mail = InvitationEmail to tid code from + (renderedMail, renderedInvitaitonUrl) = renderInvitationEmail mail tpl branding + sendMail renderedMail + pure renderedInvitaitonUrl + +data InvitationEmail = InvitationEmail + { invTo :: !EmailAddress, + invTeamId :: !TeamId, + invInvCode :: !InvitationCode, + invInviter :: !EmailAddress + } + +renderInvitationEmail :: InvitationEmail -> InvitationEmailTemplate -> TemplateBranding -> (Mail, Text) +renderInvitationEmail InvitationEmail {..} InvitationEmailTemplate {..} branding = + ( (emptyMail from) + { mailTo = [to], + mailHeaders = + [ ("Subject", toStrict subj), + ("X-Zeta-Purpose", "TeamInvitation"), + ("X-Zeta-Code", Ascii.toText code) + ], + mailParts = [[plainPart txt, htmlPart html]] + }, + invitationUrl + ) + where + (InvitationCode code) = invInvCode + from = Address (Just invitationEmailSenderName) (fromEmail invitationEmailSender) + to = Address Nothing (fromEmail invTo) + txt = renderTextWithBranding invitationEmailBodyText replace branding + html = renderHtmlWithBranding invitationEmailBodyHtml replace branding + subj = renderTextWithBranding invitationEmailSubject replace branding + invitationUrl = renderInvitationUrl invitationEmailUrl invTeamId invInvCode branding + replace "url" = invitationUrl + replace "inviter" = fromEmail invInviter + replace x = x + +renderInvitationUrl :: Template -> TeamId -> InvitationCode -> TemplateBranding -> Text +renderInvitationUrl t tid (InvitationCode c) branding = + toStrict $ renderTextWithBranding t replace branding + where + replace "team" = idToText tid + replace "code" = Ascii.toText c + replace x = x + ------------------------------------------------------------------------------- -- MIME Conversions diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs index f1c7a996f56..ea0339f74ca 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs @@ -18,31 +18,10 @@ -- with this program. If not, see . module Wire.EmailSubsystem.Template - ( Localised (..), - TemplateBranding, - forLocale, - - -- * templates - UserTemplates (..), - ActivationSmsTemplate (..), - VerificationEmailTemplate (..), - ActivationEmailTemplate (..), - TeamActivationEmailTemplate (..), - ActivationCallTemplate (..), - PasswordResetSmsTemplate (..), - PasswordResetEmailTemplate (..), - LoginSmsTemplate (..), - LoginCallTemplate (..), - DeletionSmsTemplate (..), - DeletionEmailTemplate (..), - UpgradePersonalToTeamEmailTemplate (..), - NewClientEmailTemplate (..), - SecondFactorVerificationEmailTemplate (..), + ( module Wire.EmailSubsystem.Template, -- * Re-exports Template, - renderTextWithBranding, - renderHtmlWithBranding, ) where @@ -212,3 +191,37 @@ data SecondFactorVerificationEmailTemplate = SecondFactorVerificationEmailTempla sndFactorVerificationEmailSender :: EmailAddress, sndFactorVerificationEmailSenderName :: Text } + +data InvitationEmailTemplate = InvitationEmailTemplate + { invitationEmailUrl :: !Template, + invitationEmailSubject :: !Template, + invitationEmailBodyText :: !Template, + invitationEmailBodyHtml :: !Template, + invitationEmailSender :: !EmailAddress, + invitationEmailSenderName :: !Text + } + +data CreatorWelcomeEmailTemplate = CreatorWelcomeEmailTemplate + { creatorWelcomeEmailUrl :: !Text, + creatorWelcomeEmailSubject :: !Template, + creatorWelcomeEmailBodyText :: !Template, + creatorWelcomeEmailBodyHtml :: !Template, + creatorWelcomeEmailSender :: !EmailAddress, + creatorWelcomeEmailSenderName :: !Text + } + +data MemberWelcomeEmailTemplate = MemberWelcomeEmailTemplate + { memberWelcomeEmailUrl :: !Text, + memberWelcomeEmailSubject :: !Template, + memberWelcomeEmailBodyText :: !Template, + memberWelcomeEmailBodyHtml :: !Template, + memberWelcomeEmailSender :: !EmailAddress, + memberWelcomeEmailSenderName :: !Text + } + +data TeamTemplates = TeamTemplates + { invitationEmail :: !InvitationEmailTemplate, + existingUserInvitationEmail :: !InvitationEmailTemplate, + creatorWelcomeEmail :: !CreatorWelcomeEmailTemplate, + memberWelcomeEmail :: !MemberWelcomeEmailTemplate + } diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore.hs index 92e3c7ea97e..c3fe401f4f8 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore.hs @@ -7,6 +7,7 @@ import Database.Bloodhound qualified as ES import Database.Bloodhound.Types hiding (SearchResult) import Imports import Polysemy +import Wire.API.Team.Size import Wire.API.User.Search import Wire.UserSearch.Types @@ -39,5 +40,6 @@ data IndexedUserStore m a where Int -> Maybe PagingState -> IndexedUserStore m (SearchResult UserDoc) + GetTeamSize :: TeamId -> IndexedUserStore m TeamSize makeSem ''IndexedUserStore diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs index f299017ce2b..6f8dd26e89f 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs @@ -18,6 +18,7 @@ import Imports import Network.HTTP.Client import Network.HTTP.Types import Polysemy +import Wire.API.Team.Size (TeamSize (TeamSize)) import Wire.API.User.Search import Wire.IndexedUserStore import Wire.Sem.Metrics (Metrics) @@ -53,6 +54,27 @@ interpretIndexedUserStoreES cfg = searchUsersImpl cfg searcherId mSearcherTeam teamSearchInfo term maxResults PaginateTeamMembers filters maxResults mPagingState -> paginateTeamMembersImpl cfg filters maxResults mPagingState + GetTeamSize tid -> getTeamSizeImpl cfg tid + +getTeamSizeImpl :: + ( Member (Embed IO) r + ) => + IndexedUserStoreConfig -> + TeamId -> + Sem r TeamSize +getTeamSizeImpl cfg tid = do + let indexName = cfg.conn.indexName + countResEither <- embed $ ES.runBH cfg.conn.env $ ES.countByIndex indexName (ES.CountQuery query) + countRes <- either (liftIO . throwIO . IndexLookupError) pure countResEither + pure . TeamSize $ ES.crCount countRes + where + query = + ES.TermQuery + ES.Term + { ES.termField = "team", + ES.termValue = idToText tid + } + Nothing upsertImpl :: forall r. diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem.hs new file mode 100644 index 00000000000..09cb54a6250 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Wire.TeamInvitationSubsystem where + +import Data.Id +import Data.Qualified +import Imports +import Polysemy +import Wire.API.Team.Invitation +import Wire.API.Team.Role +import Wire.API.User (InvitationCode) +import Wire.API.User.EmailAddress + +data TeamInvitationSubsystem m a where + InviteUser :: Local UserId -> TeamId -> InvitationRequest -> TeamInvitationSubsystem m (Invitation, InvitationLocation) + -- | This function exists to support migration in this susbystem, after the + -- migration this would just be an internal detail of the subsystem + InternalCreateInvitation :: TeamId -> Maybe InvitationId -> Role -> Local (Maybe UserId) -> EmailAddress -> InvitationRequest -> TeamInvitationSubsystem m (Invitation, InvitationCode) + +makeSem ''TeamInvitationSubsystem diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Error.hs new file mode 100644 index 00000000000..892450e3354 --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Error.hs @@ -0,0 +1,23 @@ +module Wire.TeamInvitationSubsystem.Error where + +import Imports +import Wire.API.Error +import Wire.API.Error.Brig qualified as E +import Wire.Error + +data TeamInvitationSubsystemError + = TeamInvitationNoEmail + | TeamInvitationInsufficientTeamPermissions + | TooManyTeamInvitations + | TeamInvitationBlacklistedEmail + | TeamInvitationEmailTaken + deriving (Show) + +teamInvitationErrorToHttpError :: TeamInvitationSubsystemError -> HttpError +teamInvitationErrorToHttpError = + StdError . \case + TeamInvitationNoEmail -> errorToWai @E.NoEmail + TeamInvitationInsufficientTeamPermissions -> errorToWai @E.InsufficientTeamPermissions + TooManyTeamInvitations -> errorToWai @E.TooManyTeamInvitations + TeamInvitationBlacklistedEmail -> errorToWai @E.BlacklistedEmail + TeamInvitationEmailTaken -> errorToWai @E.EmailExists diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs new file mode 100644 index 00000000000..336e73d5f9f --- /dev/null +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs @@ -0,0 +1,267 @@ +module Wire.TeamInvitationSubsystem.Interpreter where + +import Control.Arrow ((&&&)) +import Control.Error (MaybeT (..)) +import Data.ByteString.Conversion (toByteString') +import Data.Id +import Data.Qualified +import Data.Set qualified as Set +import Data.Text.Ascii qualified as AsciiText +import Data.Text.Encoding qualified as Text +import Imports +import Polysemy +import Polysemy.Error +import Polysemy.Input (Input, input, runInputConst) +import Polysemy.TinyLog +import System.Logger.Message as Log +import URI.ByteString +import Util.Logging +import Util.Timeout (Timeout (..)) +import Wire.API.Team.Invitation +import Wire.API.Team.Member +import Wire.API.Team.Member qualified as Teams +import Wire.API.Team.Permission +import Wire.API.Team.Role +import Wire.API.User +import Wire.Arbitrary +import Wire.EmailSubsystem +import Wire.GalleyAPIAccess hiding (AddTeamMember) +import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.InvitationCodeStore (InvitationCodeStore, StoredInvitation) +import Wire.InvitationCodeStore qualified as Store +import Wire.Sem.Logger qualified as Log +import Wire.Sem.Now (Now) +import Wire.Sem.Now qualified as Now +import Wire.Sem.Random (Random) +import Wire.Sem.Random qualified as Random +import Wire.TeamInvitationSubsystem +import Wire.TeamInvitationSubsystem.Error +import Wire.UserKeyStore +import Wire.UserSubsystem (UserSubsystem, getLocalUserAccountByUserKey, getSelfProfile, isBlocked) + +data TeamInvitationSubsystemConfig = TeamInvitationSubsystemConfig + { maxTeamSize :: Word32, + teamInvitationTimeout :: Timeout + } + deriving (Show, Generic) + deriving (Arbitrary) via GenericUniform TeamInvitationSubsystemConfig + +runTeamInvitationSubsystem :: + ( Member (Error TeamInvitationSubsystemError) r, + Member TinyLog r, + Member GalleyAPIAccess r, + Member UserSubsystem r, + Member Random r, + Member InvitationCodeStore r, + Member Now r, + Member EmailSubsystem r + ) => + TeamInvitationSubsystemConfig -> + InterpreterFor TeamInvitationSubsystem r +runTeamInvitationSubsystem cfg = interpret $ \case + InviteUser luid tid request -> runInputConst cfg $ inviteUserImpl luid tid request + InternalCreateInvitation tid mExpectedInvId role mbInviterUid inviterEmail invRequest -> + runInputConst cfg $ createInvitation' tid mExpectedInvId role mbInviterUid inviterEmail invRequest + +inviteUserImpl :: + ( Member (Error TeamInvitationSubsystemError) r, + Member GalleyAPIAccess r, + Member UserSubsystem r, + Member TinyLog r, + Member Random r, + Member InvitationCodeStore r, + Member (Input TeamInvitationSubsystemConfig) r, + Member Now r, + Member EmailSubsystem r + ) => + Local UserId -> + TeamId -> + InvitationRequest -> + Sem r (Invitation, InvitationLocation) +inviteUserImpl luid tid request = do + let inviteeRole = fromMaybe defaultRole request.role + + let inviteePerms = Teams.rolePermissions inviteeRole + ensurePermissionToAddUser (tUnqualified luid) tid inviteePerms + + inviterEmail <- + note TeamInvitationNoEmail =<< runMaybeT do + self <- MaybeT $ getSelfProfile luid + MaybeT . pure . userEmail $ selfUser self + + let context = + logFunction "Brig.Team.API.createInvitation" + . logTeam tid + . logEmail request.inviteeEmail + + (id &&& loc) . fst + <$> logInvitationRequest + context + (createInvitation' tid Nothing inviteeRole (Just <$> luid) inviterEmail request) + where + loc :: Invitation -> InvitationLocation + loc inv = + InvitationLocation $ "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' inv.invitationId + +createInvitation' :: + ( Member GalleyAPIAccess r, + Member UserSubsystem r, + Member InvitationCodeStore r, + Member TinyLog r, + Member (Error TeamInvitationSubsystemError) r, + Member Random r, + Member (Input TeamInvitationSubsystemConfig) r, + Member Now r, + Member EmailSubsystem r + ) => + TeamId -> + Maybe InvitationId -> + Role -> + Local (Maybe UserId) -> + EmailAddress -> + InvitationRequest -> + Sem r (Invitation, InvitationCode) +createInvitation' tid mExpectedInvId inviteeRole mbInviterUid inviterEmail invRequest = do + let email = invRequest.inviteeEmail + let uke = qualifyAs mbInviterUid $ mkEmailKey email + blacklistedEm <- isBlocked email + when blacklistedEm $ + throw TeamInvitationBlacklistedEmail + + mEmailOwner <- getLocalUserAccountByUserKey uke + isPersonalUserMigration <- case mEmailOwner of + Nothing -> pure False + Just account -> + if (account.accountStatus == Active && isNothing account.accountUser.userTeam) + then pure True + else throw TeamInvitationEmailTaken + + maxSize <- maxTeamSize <$> input + pending <- Store.countInvitations tid + when (fromIntegral pending >= maxSize) $ + throw TooManyTeamInvitations + + showInvitationUrl <- GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid + + do + iid <- maybe (Id <$> Random.uuid) pure mExpectedInvId + now <- Now.get + timeout <- teamInvitationTimeout <$> input + code <- mkInvitationCode + newInv <- + let insertInv = + Store.MkInsertInvitation + { invitationId = iid, + teamId = tid, + role = inviteeRole, + createdAt = now, + createdBy = tUnqualified mbInviterUid, + inviteeEmail = email, + inviteeName = invRequest.inviteeName, + code = code + -- mUrl = mUrl + } + in Store.insertInvitation insertInv timeout + + let sendOp = + if isPersonalUserMigration + then sendTeamInvitationMailPersonalUser + else sendTeamInvitationMail + + invitationUrl <- sendOp email tid inviterEmail code invRequest.locale + inv <- toInvitation invitationUrl showInvitationUrl newInv + pure (inv, code) + +mkInvitationCode :: (Member Random r) => Sem r InvitationCode +mkInvitationCode = InvitationCode . AsciiText.encodeBase64Url <$> Random.bytes 24 + +isPersonalUser :: (Member UserSubsystem r) => Local EmailKey -> Sem r Bool +isPersonalUser uke = do + mAccount <- getLocalUserAccountByUserKey uke + pure $ case mAccount of + -- this can e.g. happen if the key is claimed but the account is not yet created + Nothing -> False + Just account -> + account.accountStatus == Active + && isNothing account.accountUser.userTeam + +-- | brig used to not store the role, so for migration we allow this to be empty and fill in the +-- default here. +toInvitation :: + forall r. + (Member TinyLog r) => + Text -> + ShowOrHideInvitationUrl -> + StoredInvitation -> + Sem r Invitation +toInvitation urlText showUrl storedInv = do + url <- + case showUrl of + HideInvitationUrl -> pure Nothing + ShowInvitationUrl -> parseHttpsUrl urlText + 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 + } + where + parseHttpsUrl :: Text -> Sem r (Maybe (URIRef Absolute)) + parseHttpsUrl url = + either (\e -> Nothing <$ logError url e) (pure . Just) $ + parseURI laxURIParserOptions (Text.encodeUtf8 url) + + logError url e = + Log.err $ + Log.msg @Text "Unable to create invitation url. Please check configuration." + . Log.field "url" url + . Log.field "error" (show e) + +logInvitationRequest :: + (Member TinyLog r, Member (Error TeamInvitationSubsystemError) r) => + (Msg -> Msg) -> + Sem (Error TeamInvitationSubsystemError : r) (Invitation, InvitationCode) -> + Sem r (Invitation, InvitationCode) +logInvitationRequest context action = + runError action >>= \case + Left e -> do + Log.warn $ + msg @String ("Failed to create invitation: " <> show e) + . context + throw e + Right res@(_, code) -> do + Log.info $ + msg @ByteString "Successfully created invitation" + . context + . logInvitationCode code + pure res + +-- | Privilege escalation detection (make sure no `RoleMember` user creates a `RoleOwner`). +-- +-- There is some code duplication with 'Galley.API.Teams.ensureNotElevated'. +ensurePermissionToAddUser :: + ( Member GalleyAPIAccess r, + Member (Error TeamInvitationSubsystemError) r + ) => + UserId -> + TeamId -> + Permissions -> + Sem r () +ensurePermissionToAddUser u t inviteePerms = do + minviter <- GalleyAPIAccess.getTeamMember u t + unless (check minviter) $ + throw TeamInvitationInsufficientTeamPermissions + where + check :: Maybe TeamMember -> Bool + check (Just inviter) = + hasPermission inviter AddTeamMember + && all (mayGrantPermission inviter) (Set.toList (inviteePerms.self)) + check Nothing = False + +logInvitationCode :: InvitationCode -> (Msg -> Msg) +logInvitationCode code = field "invitation_code" (AsciiText.toText $ fromInvitationCode code) diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index 1c33abd7e42..55373c0a37d 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -66,6 +66,7 @@ data UserStore m a where -- an email address or phone number. IsActivated :: UserId -> UserStore m Bool LookupLocale :: UserId -> UserStore m (Maybe (Maybe Language, Maybe Country)) + UpdateUserTeam :: UserId -> TeamId -> 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 f6c71536c65..66d35568d27 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -30,6 +30,7 @@ interpretUserStoreCassandra casClient = LookupStatus uid -> lookupStatusImpl uid IsActivated uid -> isActivatedImpl uid LookupLocale uid -> lookupLocaleImpl uid + UpdateUserTeam uid tid -> updateUserTeamImpl uid tid getUsersImpl :: [UserId] -> Client [StoredUser] getUsersImpl usrs = @@ -162,6 +163,12 @@ lookupLocaleImpl :: UserId -> Client (Maybe (Maybe Language, Maybe Country)) lookupLocaleImpl u = do retry x1 (query1 localeSelect (params LocalQuorum (Identity u))) +updateUserTeamImpl :: UserId -> TeamId -> Client () +updateUserTeamImpl u t = retry x5 $ write userTeamUpdate (params LocalQuorum (t, u)) + where + userTeamUpdate :: PrepQuery W (TeamId, UserId) () + userTeamUpdate = "UPDATE user SET team = ? WHERE id = ?" + -------------------------------------------------------------------------------- -- Queries diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 95cfcc4ad6e..5ba2119a103 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -12,9 +12,9 @@ import Data.Domain import Data.Handle (Handle) import Data.HavePendingInvitations import Data.Id +import Data.Misc import Data.Qualified import Data.Range -import Data.Set qualified as Set import Imports import Polysemy import Polysemy.Error @@ -22,12 +22,12 @@ import Wire.API.Federation.Error import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus) import Wire.API.Team.Feature import Wire.API.Team.Member (IsPerm (..), TeamMember) -import Wire.API.Team.Permission import Wire.API.User import Wire.API.User.Search import Wire.Arbitrary import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.InvitationCodeStore import Wire.UserKeyStore (EmailKey, emailKeyOrig) import Wire.UserSearch.Types import Wire.UserSubsystem.Error (UserSubsystemError (..)) @@ -137,9 +137,12 @@ data UserSubsystem m a where Maybe (Range 1 500 Int) -> Maybe PagingState -> UserSubsystem m (SearchResult TeamContact) - -- | This function exists to support migration in this susbystem, after the + -- | (... or does `AcceptTeamInvitation` belong into `TeamInvitationSubsystems`?) + AcceptTeamInvitation :: Local UserId -> PlainTextPassword6 -> InvitationCode -> UserSubsystem m () + -- | The following "internal" functions exists to support migration in this susbystem, after the -- migration this would just be an internal detail of the subsystem InternalUpdateSearchIndex :: UserId -> UserSubsystem m () + InternalFindTeamInvitation :: Maybe EmailKey -> InvitationCode -> UserSubsystem m StoredInvitation -- | the return type of 'CheckHandle' data CheckHandleResp @@ -204,25 +207,3 @@ ensurePermissions u t perms = do check :: Maybe TeamMember -> Bool check (Just m) = all (hasPermission m) perms check Nothing = False - --- | Privilege escalation detection (make sure no `RoleMember` user creates a `RoleOwner`). --- --- There is some code duplication with 'Galley.API.Teams.ensureNotElevated'. -ensurePermissionToAddUser :: - ( Member GalleyAPIAccess r, - Member (Error UserSubsystemError) r - ) => - UserId -> - TeamId -> - Permissions -> - Sem r () -ensurePermissionToAddUser u t inviteePerms = do - minviter <- GalleyAPIAccess.getTeamMember u t - unless (check minviter) $ - throw UserSubsystemInsufficientTeamPermissions - where - check :: Maybe TeamMember -> Bool - check (Just inviter) = - hasPermission inviter AddTeamMember - && all (mayGrantPermission inviter) (Set.toList (inviteePerms.self)) - check Nothing = False diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs index 9166fbc4b73..90a2d39a888 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Error.hs @@ -1,6 +1,8 @@ module Wire.UserSubsystem.Error where import Imports +import Network.HTTP.Types (status404) +import Network.Wai.Utilities qualified as Wai import Wire.API.Error import Wire.API.Error.Brig qualified as E import Wire.Error @@ -17,6 +19,14 @@ data UserSubsystemError | UserSubsystemInvalidHandle | UserSubsystemProfileNotFound | UserSubsystemInsufficientTeamPermissions + | UserSubsystemCannotJoinMultipleTeams + | UserSubsystemTooManyTeamMembers + | UserSubsystemMissingIdentity + | UserSubsystemInvalidActivationCodeWrongUser + | UserSubsystemInvalidActivationCodeWrongCode + | UserSubsystemInvalidInvitationCode + | UserSubsystemInvitationNotFound + | UserSubsystemUserNotAllowedToJoinTeam Wai.Error | UserSubsystemMLSServicesNotAllowed deriving (Eq, Show) @@ -31,6 +41,14 @@ userSubsystemErrorToHttpError = UserSubsystemInvalidHandle -> errorToWai @E.InvalidHandle UserSubsystemHandleManagedByScim -> errorToWai @E.HandleManagedByScim UserSubsystemInsufficientTeamPermissions -> errorToWai @E.InsufficientTeamPermissions + UserSubsystemCannotJoinMultipleTeams -> errorToWai @E.CannotJoinMultipleTeams + UserSubsystemTooManyTeamMembers -> errorToWai @E.TooManyTeamMembers + UserSubsystemMissingIdentity -> errorToWai @E.MissingIdentity + UserSubsystemInvalidActivationCodeWrongUser -> errorToWai @E.InvalidActivationCodeWrongUser + UserSubsystemInvalidActivationCodeWrongCode -> errorToWai @E.InvalidActivationCodeWrongCode + UserSubsystemInvalidInvitationCode -> errorToWai @E.InvalidInvitationCode + UserSubsystemInvitationNotFound -> Wai.mkError status404 "not-found" "Something went wrong, while looking up the invitation" + UserSubsystemUserNotAllowedToJoinTeam e -> e UserSubsystemMLSServicesNotAllowed -> errorToWai @E.MLSServicesNotAllowed instance Exception UserSubsystemError diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index a824fe73c92..c0cfd1e02d2 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -16,6 +16,7 @@ import Data.Id import Data.Json.Util import Data.LegalHold import Data.List.Extra (nubOrd) +import Data.Misc (PlainTextPassword6) import Data.Qualified import Data.Range import Data.Time.Clock @@ -36,11 +37,14 @@ import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus (..) import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Permission qualified as Permission +import Wire.API.Team.Role (defaultRole) import Wire.API.Team.SearchVisibility +import Wire.API.Team.Size (TeamSize (TeamSize)) import Wire.API.User as User import Wire.API.User.Search import Wire.API.UserEvent import Wire.Arbitrary +import Wire.AuthenticationSubsystem import Wire.BlockListStore as BlockList import Wire.DeleteQueue import Wire.Events @@ -51,7 +55,7 @@ import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.IndexedUserStore (IndexedUserStore) import Wire.IndexedUserStore qualified as IndexedUserStore import Wire.IndexedUserStore.Bulk.ElasticSearch (teamSearchVisibilityInbound) -import Wire.InvitationCodeStore (InvitationCodeStore, lookupInvitationByEmail) +import Wire.InvitationCodeStore import Wire.Sem.Concurrency import Wire.Sem.Metrics import Wire.Sem.Metrics qualified as Metrics @@ -71,37 +75,13 @@ import Witherable (wither) data UserSubsystemConfig = UserSubsystemConfig { emailVisibilityConfig :: EmailVisibilityConfig, defaultLocale :: Locale, - searchSameTeamOnly :: Bool + searchSameTeamOnly :: Bool, + maxTeamSize :: Word32 } deriving (Show, Generic) deriving (Arbitrary) via (GenericUniform UserSubsystemConfig) runUserSubsystem :: - ( Member GalleyAPIAccess r, - Member UserStore r, - Member UserKeyStore r, - Member BlockListStore r, - Member (Concurrency 'Unsafe) r, -- FUTUREWORK: subsystems should implement concurrency inside interpreters, not depend on this dangerous effect. - Member (Error FederationError) r, - Member (Error UserSubsystemError) r, - Member (FederationAPIAccess fedM) r, - Member DeleteQueue r, - Member Events r, - Member Now r, - RunClient (fedM 'Brig), - FederationMonad fedM, - Typeable fedM, - Member IndexedUserStore r, - Member FederationConfigStore r, - Member Metrics r, - Member (TinyLog) r, - Member InvitationCodeStore r - ) => - UserSubsystemConfig -> - InterpreterFor UserSubsystem r -runUserSubsystem cfg = runInputConst cfg . interpretUserSubsystem . raiseUnder - -interpretUserSubsystem :: ( Member UserStore r, Member UserKeyStore r, Member GalleyAPIAccess r, @@ -110,7 +90,6 @@ interpretUserSubsystem :: Member (Error FederationError) r, Member (Error UserSubsystemError) r, Member (FederationAPIAccess fedM) r, - Member (Input UserSubsystemConfig) r, Member DeleteQueue r, Member Events r, Member Now r, @@ -123,31 +102,107 @@ interpretUserSubsystem :: Member InvitationCodeStore r, Member TinyLog r ) => + UserSubsystemConfig -> + InterpreterFor AuthenticationSubsystem r -> InterpreterFor UserSubsystem r -interpretUserSubsystem = interpret \case - GetUserProfiles self others -> getUserProfilesImpl self others - GetLocalUserProfiles others -> getLocalUserProfilesImpl others - GetExtendedAccountsBy getBy -> getExtendedAccountsByImpl getBy - GetExtendedAccountsByEmailNoFilter emails -> getExtendedAccountsByEmailNoFilterImpl emails - GetAccountNoFilter luid -> getAccountNoFilterImpl luid - GetSelfProfile self -> getSelfProfileImpl self - GetUserProfilesWithErrors self others -> getUserProfilesWithErrorsImpl self others - UpdateUserProfile self mconn mb update -> updateUserProfileImpl self mconn mb update - CheckHandle uhandle -> checkHandleImpl uhandle - CheckHandles hdls cnt -> checkHandlesImpl hdls cnt - UpdateHandle uid mconn mb uhandle -> updateHandleImpl uid mconn mb uhandle - LookupLocaleWithDefault luid -> lookupLocaleOrDefaultImpl luid - IsBlocked email -> isBlockedImpl email - BlockListDelete email -> blockListDeleteImpl email - BlockListInsert email -> blockListInsertImpl email - UpdateTeamSearchVisibilityInbound status -> - updateTeamSearchVisibilityInboundImpl status - SearchUsers luid query mDomain mMaxResults -> - searchUsersImpl luid query mDomain mMaxResults - BrowseTeam uid browseTeamFilters mMaxResults mPagingState -> - browseTeamImpl uid browseTeamFilters mMaxResults mPagingState - InternalUpdateSearchIndex uid -> - syncUserIndex uid +runUserSubsystem cfg authInterpreter = + interpret $ + \case + GetUserProfiles self others -> + runInputConst cfg $ + getUserProfilesImpl self others + GetLocalUserProfiles others -> + runInputConst cfg $ + getLocalUserProfilesImpl others + GetExtendedAccountsBy getBy -> + runInputConst cfg $ + getExtendedAccountsByImpl getBy + GetExtendedAccountsByEmailNoFilter emails -> + runInputConst cfg $ + getExtendedAccountsByEmailNoFilterImpl emails + GetAccountNoFilter luid -> + runInputConst cfg $ + getAccountNoFilterImpl luid + GetSelfProfile self -> + runInputConst cfg $ + getSelfProfileImpl self + GetUserProfilesWithErrors self others -> + runInputConst cfg $ + getUserProfilesWithErrorsImpl self others + UpdateUserProfile self mconn mb update -> + runInputConst cfg $ + updateUserProfileImpl self mconn mb update + CheckHandle uhandle -> + runInputConst cfg $ + checkHandleImpl uhandle + CheckHandles hdls cnt -> + runInputConst cfg $ + checkHandlesImpl hdls cnt + UpdateHandle uid mconn mb uhandle -> + runInputConst cfg $ + updateHandleImpl uid mconn mb uhandle + LookupLocaleWithDefault luid -> + runInputConst cfg $ + lookupLocaleOrDefaultImpl luid + IsBlocked email -> + runInputConst cfg $ + isBlockedImpl email + BlockListDelete email -> + runInputConst cfg $ + blockListDeleteImpl email + BlockListInsert email -> + runInputConst cfg $ + blockListInsertImpl email + UpdateTeamSearchVisibilityInbound status -> + runInputConst cfg $ + updateTeamSearchVisibilityInboundImpl status + SearchUsers luid query mDomain mMaxResults -> + runInputConst cfg $ + searchUsersImpl luid query mDomain mMaxResults + BrowseTeam uid browseTeamFilters mMaxResults mPagingState -> + browseTeamImpl uid browseTeamFilters mMaxResults mPagingState + InternalUpdateSearchIndex uid -> + syncUserIndex uid + AcceptTeamInvitation luid pwd code -> + authInterpreter + . runInputConst cfg + $ acceptTeamInvitationImpl luid pwd code + InternalFindTeamInvitation mEmailKey code -> + runInputConst cfg $ + internalFindTeamInvitationImpl mEmailKey code + +internalFindTeamInvitationImpl :: + ( Member InvitationCodeStore r, + Member (Error UserSubsystemError) r, + Member (Input UserSubsystemConfig) r, + Member (GalleyAPIAccess) r, + Member IndexedUserStore r + ) => + Maybe EmailKey -> + InvitationCode -> + Sem r StoredInvitation +internalFindTeamInvitationImpl Nothing _ = throw UserSubsystemMissingIdentity +internalFindTeamInvitationImpl (Just e) c = + lookupInvitationInfo c >>= \case + Just invitationInfo -> do + inv <- lookupInvitation invitationInfo.teamId invitationInfo.invitationId + case (inv, (.email) <$> inv) of + (Just invite, Just em) + | e == mkEmailKey em -> do + ensureMemberCanJoin invitationInfo.teamId + pure invite + _ -> throw UserSubsystemInvalidInvitationCode + Nothing -> throw UserSubsystemInvalidInvitationCode + where + ensureMemberCanJoin tid = do + maxSize <- maxTeamSize <$> input + (TeamSize teamSize) <- IndexedUserStore.getTeamSize tid + when (teamSize >= fromIntegral maxSize) $ + throw UserSubsystemTooManyTeamMembers + -- FUTUREWORK: The above can easily be done/tested in the intra call. + -- Remove after the next release. + mAddUserError <- checkUserCanJoinTeam tid + maybe (pure ()) (throw . UserSubsystemUserNotAllowedToJoinTeam) mAddUserError isBlockedImpl :: (Member BlockListStore r) => EmailAddress -> Sem r Bool isBlockedImpl = BlockList.exists . mkEmailKey @@ -855,3 +910,37 @@ getExtendedAccountsByImpl (tSplit -> (domain, MkGetBy {includePendingInvitations -- database schema re-design. gcHack :: Bool -> UserId -> Sem r () gcHack hasInvitation uid = unless hasInvitation (enqueueUserDeletion uid) + +acceptTeamInvitationImpl :: + ( Member (Input UserSubsystemConfig) r, + Member UserStore r, + Member GalleyAPIAccess r, + Member (Error UserSubsystemError) r, + Member InvitationCodeStore r, + Member IndexedUserStore r, + Member Metrics r, + Member Events r, + Member AuthenticationSubsystem r + ) => + Local UserId -> + PlainTextPassword6 -> + InvitationCode -> + Sem r () +acceptTeamInvitationImpl luid pw code = do + mSelfProfile <- getSelfProfileImpl luid + let mEmailKey = mkEmailKey <$> (userEmail . selfUser =<< mSelfProfile) + mTid = mSelfProfile >>= userTeam . selfUser + verifyPassword luid pw + inv <- internalFindTeamInvitationImpl mEmailKey code + let tid = inv.teamId + let minvmeta = (,inv.createdAt) <$> inv.createdBy + uid = tUnqualified luid + for_ mTid $ \userTid -> + unless (tid == userTid) $ + throw UserSubsystemCannotJoinMultipleTeams + added <- GalleyAPIAccess.addTeamMember uid tid minvmeta (fromMaybe defaultRole inv.role) + unless added $ throw UserSubsystemTooManyTeamMembers + updateUserTeam uid tid + deleteInvitation inv.teamId inv.invitationId + syncUserIndex uid + generateUserEvent uid Nothing (teamUpdated uid tid) diff --git a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs index 85a9af652a3..4a7572ec306 100644 --- a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs @@ -18,7 +18,7 @@ import Test.Hspec import Test.Hspec.QuickCheck import Test.QuickCheck import Wire.API.Allowlists (AllowlistEmailDomains (AllowlistEmailDomains)) -import Wire.API.Password +import Wire.API.Password as Password import Wire.API.User import Wire.API.User qualified as User import Wire.API.User.Auth @@ -34,10 +34,10 @@ import Wire.Sem.Logger.TinyLog import Wire.Sem.Now (Now) import Wire.SessionStore import Wire.UserKeyStore -import Wire.UserSubsystem type AllEffects = - [ Error AuthenticationSubsystemError, + [ AuthenticationSubsystem, + Error AuthenticationSubsystemError, HashPassword, Now, State UTCTime, @@ -46,26 +46,22 @@ type AllEffects = SessionStore, State (Map UserId [Cookie ()]), PasswordStore, - State (Map UserId Password), PasswordResetCodeStore, State (Map PasswordResetKey (PRQueryData Identity)), TinyLog, EmailSubsystem, - State (Map EmailAddress [SentMail]), - UserSubsystem + State (Map EmailAddress [SentMail]) ] -interpretDependencies :: Domain -> [ExtendedUserAccount] -> Map UserId Password -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a -interpretDependencies localDomain preexistingUsers preexistingPasswords mAllowedEmailDomains = +runAllEffects :: Domain -> [ExtendedUserAccount] -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a +runAllEffects localDomain preexistingUsers mAllowedEmailDomains = run - . userSubsystemTestInterpreter preexistingUsers . evalState mempty . emailSubsystemInterpreter . discardTinyLogs . evalState mempty . inMemoryPasswordResetCodeStore - . evalState preexistingPasswords - . inMemoryPasswordStoreInterpreter + . runInMemoryPasswordStoreInterpreter . evalState mempty . inMemorySessionStoreInterpreter . runInputConst (AllowlistEmailDomains <$> mAllowedEmailDomains) @@ -74,6 +70,7 @@ interpretDependencies localDomain preexistingUsers preexistingPasswords mAllowed . interpretNowAsState . staticHashPasswordInterpreter . runError + . interpretAuthenticationSubsystem (userSubsystemTestInterpreter preexistingUsers) spec :: Spec spec = describe "AuthenticationSubsystem.Interpreter" do @@ -84,19 +81,17 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, cookiesAfterReset) = - interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing - . interpretAuthenticationSubsystem - $ do - forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) - mapM_ (uncurry (insertCookie uid)) cookiesWithTTL + runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) + mapM_ (uncurry (insertCookie uid)) cookiesWithTTL - createPasswordResetCode (mkEmailKey email) - (_, code) <- expect1ResetPasswordEmail email - resetPassword (PasswordResetEmailIdentity email) code newPassword + createPasswordResetCode (mkEmailKey email) + (_, code) <- expect1ResetPasswordEmail email + resetPassword (PasswordResetEmailIdentity email) code newPassword - (,) <$> lookupHashedPassword uid <*> listCookies uid + (,) <$> lookupHashedPassword uid <*> listCookies uid in mPreviousPassword /= Just newPassword ==> - (fmap (verifyPassword newPassword) newPasswordHash === Just True) + (fmap (Password.verifyPassword newPassword) newPasswordHash === Just True) .&&. (cookiesAfterReset === []) prop "password reset should work with the returned password reset key" $ @@ -105,27 +100,24 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, cookiesAfterReset) = - interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing - . interpretAuthenticationSubsystem - $ do - forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) - mapM_ (uncurry (insertCookie uid)) cookiesWithTTL + runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) + mapM_ (uncurry (insertCookie uid)) cookiesWithTTL - createPasswordResetCode (mkEmailKey email) - (passwordResetKey, code) <- expect1ResetPasswordEmail email - resetPassword (PasswordResetIdentityKey passwordResetKey) code newPassword + createPasswordResetCode (mkEmailKey email) + (passwordResetKey, code) <- expect1ResetPasswordEmail email + resetPassword (PasswordResetIdentityKey passwordResetKey) code newPassword - (,) <$> lookupHashedPassword uid <*> listCookies uid + (,) <$> lookupHashedPassword uid <*> listCookies uid in mPreviousPassword /= Just newPassword ==> - (fmap (verifyPassword newPassword) newPasswordHash === Just True) + (fmap (Password.verifyPassword newPassword) newPasswordHash === Just True) .&&. (cookiesAfterReset === []) prop "reset code is not generated when email is not in allow list" $ \email localDomain -> let createPasswordResetCodeResult = - interpretDependencies localDomain [] mempty (Just ["example.com"]) - . interpretAuthenticationSubsystem - $ createPasswordResetCode (mkEmailKey email) + runAllEffects localDomain [] (Just ["example.com"]) $ + createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent in domainPart email /= "example.com" ==> createPasswordResetCodeResult === Right () @@ -135,9 +127,8 @@ spec = describe "AuthenticationSubsystem.Interpreter" do let user = userNoEmail {userIdentity = Just $ EmailIdentity email} localDomain = userNoEmail.userQualifiedId.qDomain createPasswordResetCodeResult = - interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty (Just [decodeUtf8 $ domainPart email]) - . interpretAuthenticationSubsystem - $ createPasswordResetCode (mkEmailKey email) + runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] (Just [decodeUtf8 $ domainPart email]) $ + createPasswordResetCode (mkEmailKey email) in counterexample ("expected Right, got: " <> show createPasswordResetCodeResult) $ isRight createPasswordResetCodeResult @@ -146,9 +137,8 @@ spec = describe "AuthenticationSubsystem.Interpreter" do let user = userNoEmail {userIdentity = Just $ EmailIdentity email} localDomain = userNoEmail.userQualifiedId.qDomain createPasswordResetCodeResult = - interpretDependencies localDomain [ExtendedUserAccount (UserAccount user status) Nothing] mempty Nothing - . interpretAuthenticationSubsystem - $ createPasswordResetCode (mkEmailKey email) + runAllEffects localDomain [ExtendedUserAccount (UserAccount user status) Nothing] Nothing $ + createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent in status /= Active ==> createPasswordResetCodeResult === Right () @@ -156,9 +146,8 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "reset code is not generated for when there is no user for the email" $ \email localDomain -> let createPasswordResetCodeResult = - interpretDependencies localDomain [] mempty Nothing - . interpretAuthenticationSubsystem - $ createPasswordResetCode (mkEmailKey email) + runAllEffects localDomain [] Nothing $ + createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent in createPasswordResetCodeResult === Right () @@ -168,19 +157,17 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, mCaughtException) = - interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing - . interpretAuthenticationSubsystem - $ do - createPasswordResetCode (mkEmailKey email) - (_, code) <- expect1ResetPasswordEmail email + runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + createPasswordResetCode (mkEmailKey email) + (_, code) <- expect1ResetPasswordEmail email - mCaughtExc <- catchExpectedError $ createPasswordResetCode (mkEmailKey email) + mCaughtExc <- catchExpectedError $ createPasswordResetCode (mkEmailKey email) - -- Reset password still works with previously generated reset code - resetPassword (PasswordResetEmailIdentity email) code newPassword + -- Reset password still works with previously generated reset code + resetPassword (PasswordResetEmailIdentity email) code newPassword - (,mCaughtExc) <$> lookupHashedPassword uid - in (fmap (verifyPassword newPassword) newPasswordHash === Just True) + (,mCaughtExc) <$> lookupHashedPassword uid + in (fmap (Password.verifyPassword newPassword) newPasswordHash === Just True) .&&. (mCaughtException === Nothing) prop "reset code is not accepted after expiry" $ @@ -189,17 +176,15 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing - . interpretAuthenticationSubsystem - $ do - upsertHashedPassword uid =<< hashPassword oldPassword - createPasswordResetCode (mkEmailKey email) - (_, code) <- expect1ResetPasswordEmail email + runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + upsertHashedPassword uid =<< hashPassword oldPassword + createPasswordResetCode (mkEmailKey email) + (_, code) <- expect1ResetPasswordEmail email - passTime (passwordResetCodeTtl + 1) + passTime (passwordResetCodeTtl + 1) - mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) code newPassword - (,mCaughtExc) <$> lookupHashedPassword uid + mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) code newPassword + (,mCaughtExc) <$> lookupHashedPassword uid in resetPasswordResult === Just AuthenticationSubsystemInvalidPasswordResetCode .&&. verifyPasswordProp oldPassword passwordInDB @@ -209,12 +194,10 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing - . interpretAuthenticationSubsystem - $ do - upsertHashedPassword uid =<< hashPassword oldPassword - mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) resetCode newPassword - (,mCaughtExc) <$> lookupHashedPassword uid + runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + upsertHashedPassword uid =<< hashPassword oldPassword + mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) resetCode newPassword + (,mCaughtExc) <$> lookupHashedPassword uid in resetPasswordResult === Just AuthenticationSubsystemInvalidPasswordResetCode .&&. verifyPasswordProp oldPassword passwordInDB @@ -224,12 +207,10 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing - . interpretAuthenticationSubsystem - $ do - hashAndUpsertPassword uid oldPassword - mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity wrongEmail) resetCode newPassword - (,mCaughtExc) <$> lookupHashedPassword uid + runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + hashAndUpsertPassword uid oldPassword + mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity wrongEmail) resetCode newPassword + (,mCaughtExc) <$> lookupHashedPassword uid in email /= wrongEmail ==> resetPasswordResult === Just AuthenticationSubsystemInvalidPasswordResetKey .&&. verifyPasswordProp oldPassword passwordInDB @@ -240,20 +221,18 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordHashInDB, correctResetCode, wrongResetErrors, resetPassworedWithCorectCodeResult) = - interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing - . interpretAuthenticationSubsystem - $ do - upsertHashedPassword uid =<< hashPassword oldPassword - createPasswordResetCode (mkEmailKey email) - (_, generatedResetCode) <- expect1ResetPasswordEmail email - - wrongResetErrs <- - replicateM wrongResetAttempts $ - catchExpectedError $ - resetPassword (PasswordResetEmailIdentity email) arbitraryResetCode newPassword - - mFinalResetErr <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) generatedResetCode newPassword - (,generatedResetCode,wrongResetErrs,mFinalResetErr) <$> lookupHashedPassword uid + runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + upsertHashedPassword uid =<< hashPassword oldPassword + createPasswordResetCode (mkEmailKey email) + (_, generatedResetCode) <- expect1ResetPasswordEmail email + + wrongResetErrs <- + replicateM wrongResetAttempts $ + catchExpectedError $ + resetPassword (PasswordResetEmailIdentity email) arbitraryResetCode newPassword + + mFinalResetErr <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) generatedResetCode newPassword + (,generatedResetCode,wrongResetErrs,mFinalResetErr) <$> lookupHashedPassword uid expectedFinalResetResult = if wrongResetAttempts >= 3 then Just AuthenticationSubsystemInvalidPasswordResetCode @@ -274,13 +253,11 @@ spec = describe "AuthenticationSubsystem.Interpreter" do uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right passwordHashInDB = - interpretDependencies localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] mempty Nothing - . interpretAuthenticationSubsystem - $ do - void $ createPasswordResetCode (mkEmailKey email) - mLookupRes <- internalLookupPasswordResetCode (mkEmailKey email) - for_ mLookupRes $ \(_, code) -> resetPassword (PasswordResetEmailIdentity email) code newPassword - lookupHashedPassword uid + runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + void $ createPasswordResetCode (mkEmailKey email) + mLookupRes <- internalLookupPasswordResetCode (mkEmailKey email) + for_ mLookupRes $ \(_, code) -> resetPassword (PasswordResetEmailIdentity email) code newPassword + lookupHashedPassword uid in verifyPasswordProp newPassword passwordHashInDB newtype Upto4 = Upto4 Int @@ -292,7 +269,7 @@ instance Arbitrary Upto4 where verifyPasswordProp :: PlainTextPassword8 -> Maybe Password -> Property verifyPasswordProp plainTextPassword passwordHash = counterexample ("Password doesn't match, plainText=" <> show plainTextPassword <> ", passwordHash=" <> show passwordHash) $ - fmap (verifyPassword plainTextPassword) passwordHash == Just True + fmap (Password.verifyPassword plainTextPassword) passwordHash == Just True hashAndUpsertPassword :: (Member PasswordStore r, Member HashPassword r) => UserId -> PlainTextPassword8 -> Sem r () hashAndUpsertPassword uid password = diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index 415b0117d05..c0d68dadc93 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -48,6 +48,7 @@ import Servant.Client.Core import System.Logger qualified as Log import Test.QuickCheck import Type.Reflection +import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Federation.API import Wire.API.Federation.Component import Wire.API.Federation.Error @@ -57,14 +58,18 @@ import Wire.API.User as User hiding (DeleteUser) import Wire.API.User.Activation (ActivationCode) import Wire.API.User.Password import Wire.ActivationCodeStore +import Wire.AuthenticationSubsystem +import Wire.AuthenticationSubsystem.Interpreter import Wire.BlockListStore import Wire.DeleteQueue import Wire.DeleteQueue.InMemory +import Wire.EmailSubsystem (EmailSubsystem) import Wire.Events import Wire.FederationAPIAccess import Wire.FederationAPIAccess.Interpreter as FI import Wire.FederationConfigStore import Wire.GalleyAPIAccess +import Wire.HashPassword (HashPassword) import Wire.IndexedUserStore import Wire.InternalEvent hiding (DeleteUser) import Wire.InvitationCodeStore @@ -72,11 +77,13 @@ import Wire.MockInterpreters import Wire.MockInterpreters.ActivationCodeStore (inMemoryActivationCodeStoreInterpreter) import Wire.MockInterpreters.InvitationCodeStore (inMemoryInvitationCodeStoreInterpreter) import Wire.PasswordResetCodeStore +import Wire.PasswordStore import Wire.Sem.Concurrency import Wire.Sem.Concurrency.Sequential import Wire.Sem.Metrics import Wire.Sem.Metrics.IO (ignoreMetrics) import Wire.Sem.Now hiding (get) +import Wire.SessionStore (SessionStore) import Wire.StoredUser import Wire.UserKeyStore import Wire.UserStore @@ -121,13 +128,17 @@ instance Arbitrary NotPendingStoredUser where type AllErrors = [ Error UserSubsystemError, - Error FederationError + Error FederationError, + Error AuthenticationSubsystemError ] -type MiniBackendEffects = - [ UserSubsystem, +type MiniBackendEffects = UserSubsystem ': MiniBackendLowerEffects + +type MiniBackendLowerEffects = + [ EmailSubsystem, GalleyAPIAccess, InvitationCodeStore, + PasswordStore, State (Map (TeamId, InvitationId) StoredInvitation), State (Map InvitationCode StoredInvitationInfo), ActivationCodeStore, @@ -140,6 +151,9 @@ type MiniBackendEffects = State (Map EmailKey UserId), IndexedUserStore, FederationConfigStore, + PasswordResetCodeStore, + SessionStore, + HashPassword, DeleteQueue, Events, State [InternalNotification], @@ -148,6 +162,7 @@ type MiniBackendEffects = Now, Input UserSubsystemConfig, Input (Local ()), + Input (Maybe AllowlistEmailDomains), Metrics, FederationAPIAccess MiniFederationMonad, TinyLog, @@ -371,6 +386,7 @@ interpretNoFederationStackState :: interpretNoFederationStackState = interpretMaybeFederationStackState emptyFederationAPIAcesss interpretMaybeFederationStackState :: + forall r a. (Members AllErrors r) => InterpreterFor (FederationAPIAccess MiniFederationMonad) (Logger (Log.Msg -> Log.Msg) : Concurrency 'Unsafe : r) -> MiniBackend -> @@ -380,33 +396,44 @@ interpretMaybeFederationStackState :: Sem (MiniBackendEffects `Append` r) a -> Sem r (MiniBackend, a) interpretMaybeFederationStackState maybeFederationAPIAccess localBackend teamMember galleyConfigs cfg = - sequentiallyPerformConcurrency - . noOpLogger - . maybeFederationAPIAccess - . ignoreMetrics - . runInputConst (toLocalUnsafe (Domain "localdomain") ()) - . runInputConst cfg - . interpretNowConst (UTCTime (ModifiedJulianDay 0) 0) - . evalState [] - . runState localBackend - . evalState [] - . miniEventInterpreter - . inMemoryDeleteQueueInterpreter - . runFederationConfigStoreInMemory - . inMemoryIndexedUserStoreInterpreter - . liftUserKeyStoreState - . inMemoryUserKeyStoreInterpreter - . liftUserStoreState - . inMemoryUserStoreInterpreter - . liftBlockListStoreState - . inMemoryBlockListStoreInterpreter - . liftActivationCodeStoreState - . inMemoryActivationCodeStoreInterpreter - . liftInvitationInfoStoreState - . liftInvitationCodeStoreState - . inMemoryInvitationCodeStoreInterpreter - . miniGalleyAPIAccess teamMember galleyConfigs - . runUserSubsystem cfg + let authSubsystemInterpreter :: InterpreterFor AuthenticationSubsystem (MiniBackendLowerEffects `Append` r) + authSubsystemInterpreter = interpretAuthenticationSubsystem userSubsystemInterpreter + + userSubsystemInterpreter :: InterpreterFor UserSubsystem (MiniBackendLowerEffects `Append` r) + userSubsystemInterpreter = runUserSubsystem cfg authSubsystemInterpreter + in sequentiallyPerformConcurrency + . noOpLogger + . maybeFederationAPIAccess + . ignoreMetrics + . runInputConst Nothing + . runInputConst (toLocalUnsafe (Domain "localdomain") ()) + . runInputConst cfg + . interpretNowConst (UTCTime (ModifiedJulianDay 0) 0) + . evalState [] + . runState localBackend + . evalState [] + . miniEventInterpreter + . inMemoryDeleteQueueInterpreter + . staticHashPasswordInterpreter + . runInMemorySessionStore + . runInMemoryPasswordResetCodeStore + . runFederationConfigStoreInMemory + . inMemoryIndexedUserStoreInterpreter + . liftUserKeyStoreState + . inMemoryUserKeyStoreInterpreter + . liftUserStoreState + . inMemoryUserStoreInterpreter + . liftBlockListStoreState + . inMemoryBlockListStoreInterpreter + . liftActivationCodeStoreState + . inMemoryActivationCodeStoreInterpreter + . liftInvitationInfoStoreState + . liftInvitationCodeStoreState + . runInMemoryPasswordStoreInterpreter + . inMemoryInvitationCodeStoreInterpreter + . miniGalleyAPIAccess teamMember galleyConfigs + . noopEmailSubsystemInterpreter + . userSubsystemInterpreter liftInvitationInfoStoreState :: (Member (State MiniBackend) r) => Sem (State (Map InvitationCode StoredInvitationInfo) : r) a -> Sem r a liftInvitationInfoStoreState = interpret \case @@ -439,7 +466,7 @@ liftUserStoreState = interpret $ \case Put newUsers -> modify $ \b -> b {users = newUsers} runAllErrorsUnsafe :: forall a. (HasCallStack) => Sem AllErrors a -> a -runAllErrorsUnsafe = run . runErrorUnsafe . runErrorUnsafe +runAllErrorsUnsafe = run . runErrorUnsafe . runErrorUnsafe . runErrorUnsafe emptyFederationAPIAcesss :: InterpreterFor (FederationAPIAccess MiniFederationMonad) r emptyFederationAPIAcesss = interpret $ \case diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs index 48d347e3ff2..636027753cd 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs @@ -23,3 +23,19 @@ emailSubsystemInterpreter = interpret \case getEmailsSentTo :: (Member (State (Map EmailAddress [SentMail])) r) => EmailAddress -> Sem r [SentMail] getEmailsSentTo email = gets $ Map.findWithDefault [] email + +noopEmailSubsystemInterpreter :: InterpreterFor EmailSubsystem r +noopEmailSubsystemInterpreter = interpret \case + SendPasswordResetMail {} -> pure () + SendVerificationMail {} -> pure () + SendCreateScimTokenVerificationMail {} -> pure () + SendLoginVerificationMail {} -> pure () + SendActivationMail {} -> pure () + SendEmailAddressUpdateMail {} -> pure () + SendNewClientEmail {} -> pure () + SendAccountDeletionEmail {} -> pure () + SendTeamActivationMail {} -> pure () + SendTeamDeletionVerificationMail {} -> pure () + SendUpgradePersonalToTeamConfirmationEmail {} -> pure () + SendTeamInvitationMail {} -> pure "" + SendTeamInvitationMailPersonalUser {} -> pure "" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs index 60a186a6d8c..06d78cfd24b 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs @@ -13,3 +13,4 @@ inMemoryIndexedUserStoreInterpreter = DoesIndexExist -> pure True SearchUsers {} -> error "IndexedUserStore: unimplemented in memory interpreter" PaginateTeamMembers {} -> error "IndexedUserStore: unimplemented in memory interpreter" + GetTeamSize {} -> error "IndexedUserStore: unimplemented in memory interpreter" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordResetCodeStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordResetCodeStore.hs index 25d6ab11d89..98bb17286bc 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordResetCodeStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordResetCodeStore.hs @@ -8,6 +8,12 @@ import Polysemy.State import Wire.API.User.Password import Wire.PasswordResetCodeStore +runInMemoryPasswordResetCodeStore :: forall r. InterpreterFor PasswordResetCodeStore r +runInMemoryPasswordResetCodeStore = + evalState (mempty :: Map PasswordResetKey (PRQueryData Identity)) + . inMemoryPasswordResetCodeStore + . raiseUnder + inMemoryPasswordResetCodeStore :: forall r. (Member (State (Map PasswordResetKey (PRQueryData Identity))) r) => diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs index be4f1a140d3..a90b9184eab 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs @@ -8,6 +8,9 @@ import Polysemy.State import Wire.API.Password import Wire.PasswordStore +runInMemoryPasswordStoreInterpreter :: InterpreterFor PasswordStore r +runInMemoryPasswordStoreInterpreter = evalState (mempty :: Map UserId Password) . inMemoryPasswordStoreInterpreter . raiseUnder + inMemoryPasswordStoreInterpreter :: (Member (State (Map UserId Password)) r) => InterpreterFor PasswordStore r inMemoryPasswordStoreInterpreter = interpret $ \case UpsertHashedPassword uid password -> modify $ Map.insert uid password diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SessionStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SessionStore.hs index 43e2736ba2e..fcfc136d1ba 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SessionStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/SessionStore.hs @@ -8,6 +8,12 @@ import Polysemy.State import Wire.API.User.Auth import Wire.SessionStore +runInMemorySessionStore :: InterpreterFor SessionStore r +runInMemorySessionStore = + evalState (mempty :: Map UserId [Cookie ()]) + . inMemorySessionStoreInterpreter + . raiseUnder + inMemorySessionStoreInterpreter :: (Member (State (Map UserId [Cookie ()])) r) => InterpreterFor SessionStore r inMemorySessionStoreInterpreter = interpret $ \case InsertCookie uid cookie _ttl -> modify $ Map.insertWith (<>) uid [cookie] diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index db318e5366b..a4c05c44b5c 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + module Wire.MockInterpreters.UserStore where import Cassandra.Util @@ -65,6 +67,10 @@ inMemoryUserStoreInterpreter = interpret $ \case LookupStatus uid -> lookupStatusImpl uid IsActivated uid -> isActivatedImpl uid LookupLocale uid -> lookupLocaleImpl uid + UpdateUserTeam uid tid -> + modify $ + map + (\u -> if u.id == uid then u {teamId = Just tid} :: StoredUser else u) 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 ee679f39123..fbefec47a3f 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -28,6 +28,7 @@ import Wire.API.Team.Member import Wire.API.Team.Permission import Wire.API.User hiding (DeleteUser) import Wire.API.UserEvent +import Wire.AuthenticationSubsystem.Error import Wire.InvitationCodeStore (StoredInvitation) import Wire.InvitationCodeStore qualified as InvitationStore import Wire.MiniBackend @@ -56,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 + config = UserSubsystemConfig visibility miniLocale False 100 retrievedProfiles = runFederationStack localBackend federation Nothing config $ getUserProfiles @@ -84,11 +85,12 @@ 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 + config = UserSubsystemConfig visibility miniLocale False 100 localBackend = def {users = [viewer]} result = run . runErrorUnsafe @UserSubsystemError + . runErrorUnsafe @AuthenticationSubsystemError . runError @FederationError . interpretFederationStack localBackend online Nothing config $ getUserProfiles @@ -153,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 + config = UserSubsystemConfig visibility miniLocale False 100 localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend federation Nothing config $ @@ -174,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 + config = UserSubsystemConfig visibility miniLocale False 100 localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend online Nothing config $ @@ -194,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 + config = UserSubsystemConfig visibility miniLocale False 100 localBackend = def {users = [viewer]} retrievedProfilesWithErrors :: ([(Qualified UserId, FederationError)], [UserProfile]) = runFederationStack localBackend online Nothing config $ @@ -279,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 + let config = UserSubsystemConfig visibility locale False 100 alice = alice' { email = Just email, @@ -314,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 + let config = UserSubsystemConfig visibility locale True 100 alice = alice' { email = Just email, @@ -348,7 +350,7 @@ spec = describe "UserSubsystem.Interpreter" do in result === [mkAccountFromStored 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 + let config = UserSubsystemConfig visibility locale True 100 alice = alice' { email = Just email, @@ -384,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 + let config = UserSubsystemConfig visibility locale True 100 alice = alice' { email = Just email, @@ -420,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 + let config = UserSubsystemConfig visibility locale True 100 alice = alice' {email = Just email} localBackend = def @@ -434,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 + let config = UserSubsystemConfig visibility locale True 100 getBy = toLocalUnsafe localDomain $ def @@ -449,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 + let config = UserSubsystemConfig visibility locale True 100 getBy = toLocalUnsafe localDomain $ def @@ -464,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 + let config = UserSubsystemConfig visibility locale True 100 emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -493,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 + let config = UserSubsystemConfig visibility locale True 100 emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -514,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 + let config = UserSubsystemConfig visibility locale True 100 emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -548,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 + let config = UserSubsystemConfig visibility locale True 100 emailKey = mkEmailKey email getBy = toLocalUnsafe localDomain $ @@ -580,6 +582,7 @@ spec = describe "UserSubsystem.Interpreter" do profileErr :: Either UserSubsystemError (Maybe UserProfile) = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do updateUserProfile lusr Nothing UpdateOriginWireClient update {name = Nothing, locale = Nothing} @@ -594,6 +597,7 @@ spec = describe "UserSubsystem.Interpreter" do profileErr :: Either UserSubsystemError (Maybe UserProfile) = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do updateUserProfile lusr Nothing UpdateOriginWireClient def {name = Just name} @@ -608,6 +612,7 @@ spec = describe "UserSubsystem.Interpreter" do profileErr :: Either UserSubsystemError (Maybe UserProfile) = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do updateUserProfile lusr Nothing UpdateOriginWireClient def {locale = Just locale} @@ -623,6 +628,7 @@ spec = describe "UserSubsystem.Interpreter" do profileErr :: Either UserSubsystemError (Maybe UserProfile) = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend @@ -685,6 +691,7 @@ spec = describe "UserSubsystem.Interpreter" do let res :: Either UserSubsystemError () res = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do updateHandle (toLocalUnsafe domain alice.id) Nothing UpdateOriginWireClient (fromHandle newHandle) @@ -698,6 +705,7 @@ spec = describe "UserSubsystem.Interpreter" do not (isBlacklistedHandle (fromJust (parseHandle newHandle))) ==> let res :: Either UserSubsystemError () = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do updateHandle (toLocalUnsafe domain alice.id) Nothing UpdateOriginScim newHandle @@ -720,6 +728,7 @@ spec = describe "UserSubsystem.Interpreter" do (isJust storedUser.identity && not (isBlacklistedHandle newHandle)) ==> let updateResult :: Either UserSubsystemError () = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack (def {users = [storedUser]}) Nothing def config do let luid = toLocalUnsafe dom storedUser.id @@ -733,6 +742,7 @@ spec = describe "UserSubsystem.Interpreter" do isJust storedUser.identity ==> let updateResult :: Either UserSubsystemError () = run . runErrorUnsafe + . runErrorUnsafe @AuthenticationSubsystemError . runError $ interpretNoFederationStack localBackend Nothing def config do let luid = toLocalUnsafe dom storedUser.id @@ -773,9 +783,7 @@ spec = describe "UserSubsystem.Interpreter" do userKeys = Map.singleton userKey storedUser.id } retrievedUser = - run - . runErrorUnsafe - . runErrorUnsafe @UserSubsystemError + runAllErrorsUnsafe . interpretNoFederationStack localBackend Nothing def config $ getLocalUserAccountByUserKey (toLocalUnsafe localDomain userKey) in retrievedUser === Just (mkAccountFromStored localDomain config.defaultLocale storedUser) @@ -789,9 +797,7 @@ spec = describe "UserSubsystem.Interpreter" do } storedUser = storedUserNoEmail {email = Just email} retrievedUser = - run - . runErrorUnsafe - . runErrorUnsafe @UserSubsystemError + runAllErrorsUnsafe . interpretNoFederationStack localBackend Nothing def config $ getLocalUserAccountByUserKey (toLocalUnsafe localDomain (mkEmailKey email)) in retrievedUser === Nothing @@ -804,9 +810,7 @@ spec = describe "UserSubsystem.Interpreter" do userKeys = Map.singleton userKey nonExistentUserId } retrievedUser = - run - . runErrorUnsafe - . runErrorUnsafe @UserSubsystemError + runAllErrorsUnsafe . interpretNoFederationStack localBackend Nothing def config $ getLocalUserAccountByUserKey (toLocalUnsafe localDomain userKey) in retrievedUser === Nothing diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 294ead8c5de..011bd1e528c 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -119,6 +119,9 @@ library Wire.SessionStore Wire.SessionStore.Cassandra Wire.StoredUser + Wire.TeamInvitationSubsystem + Wire.TeamInvitationSubsystem.Error + Wire.TeamInvitationSubsystem.Interpreter Wire.UserKeyStore Wire.UserKeyStore.Cassandra Wire.UserSearch.Metrics diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 051f18c405f..bc322581589 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -196,7 +196,6 @@ library Brig.User.EJPD Brig.User.Search.Index Brig.User.Search.SearchIndex - Brig.User.Search.TeamSize Brig.User.Template Brig.Version Brig.ZAuth diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 28ae7f50973..33f79d6ec34 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -41,7 +41,6 @@ import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Options hiding (internalEvents) import Brig.Provider.API qualified as Provider import Brig.Team.API qualified as Team -import Brig.Team.Template (TeamTemplates) import Brig.Types.Connection import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) @@ -66,6 +65,7 @@ import Data.Time.Clock.System import Imports hiding (head) import Network.Wai.Utilities as Utilities import Polysemy +import Polysemy.Error qualified import Polysemy.Input (Input, input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) @@ -91,7 +91,6 @@ import Wire.API.UserEvent import Wire.AuthenticationSubsystem (AuthenticationSubsystem) import Wire.BlockListStore (BlockListStore) import Wire.DeleteQueue (DeleteQueue) -import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem (EmailSubsystem) import Wire.Events (Events) import Wire.Events qualified as Events @@ -103,16 +102,19 @@ import Wire.FederationConfigStore ) import Wire.FederationConfigStore qualified as E import Wire.GalleyAPIAccess (GalleyAPIAccess) +import Wire.IndexedUserStore (IndexedUserStore, getTeamSize) import Wire.InvitationCodeStore import Wire.NotificationSubsystem import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PropertySubsystem import Wire.Rpc import Wire.Sem.Concurrency +import Wire.TeamInvitationSubsystem import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem import Wire.UserSubsystem qualified as UserSubsystem +import Wire.UserSubsystem.Error import Wire.VerificationCode import Wire.VerificationCodeGen import Wire.VerificationCodeSubsystem @@ -128,20 +130,21 @@ servantSitemap :: Member GalleyAPIAccess r, Member NotificationSubsystem r, Member UserSubsystem r, + Member TeamInvitationSubsystem r, Member UserStore r, Member InvitationCodeStore r, Member UserKeyStore r, Member Rpc r, Member TinyLog r, Member (UserPendingActivationStore p) r, - Member (Input (Local ())) r, - Member EmailSending r, Member EmailSubsystem r, Member VerificationCodeSubsystem r, Member Events r, Member PasswordResetCodeStore r, Member PropertySubsystem r, - Member (Input TeamTemplates) r + Member (Input (Local ())) r, + Member IndexedUserStore r, + Member (Polysemy.Error.Error UserSubsystemError) r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -241,20 +244,21 @@ teamsAPI :: Member (Concurrency 'Unsafe) r, Member TinyLog r, Member InvitationCodeStore r, - Member EmailSending r, + Member TeamInvitationSubsystem r, Member UserSubsystem r, + Member (Polysemy.Error.Error UserSubsystemError) r, Member Events r, - Member (Input TeamTemplates) r, - Member (Input (Local ())) r + Member (Input (Local ())) r, + Member IndexedUserStore r ) => ServerT BrigIRoutes.TeamsAPI (Handler r) teamsAPI = Named @"updateSearchVisibilityInbound" (lift . liftSem . updateTeamSearchVisibilityInbound) :<|> Named @"get-invitation-by-email" Team.getInvitationByEmail - :<|> Named @"get-invitation-code" Team.getInvitationCode + :<|> Named @"get-invitation-code" (\tid iid -> lift . liftSem $ Team.getInvitationCode tid iid) :<|> Named @"suspend-team" Team.suspendTeam :<|> Named @"unsuspend-team" Team.unsuspendTeam - :<|> Named @"team-size" Team.teamSize + :<|> Named @"team-size" (lift . liftSem . getTeamSize) :<|> Named @"create-invitations-via-scim" Team.createInvitationViaScim userAPI :: (Member UserSubsystem r) => ServerT BrigIRoutes.UserAPI (Handler r) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 45c8ea24aa0..fc460854766 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -50,7 +50,6 @@ import Brig.Options hiding (internalEvents) import Brig.Provider.API import Brig.Team.API qualified as Team import Brig.Team.Email qualified as Team -import Brig.Team.Template (TeamTemplates) import Brig.Types.Activation (ActivationPair) import Brig.Types.Intra (UserAccount (UserAccount, accountUser)) import Brig.User.API.Handle qualified as Handle @@ -150,11 +149,13 @@ import Wire.BlockListStore (BlockListStore) import Wire.DeleteQueue import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem +import Wire.EmailSubsystem.Template import Wire.Error import Wire.Events (Events) import Wire.FederationConfigStore (FederationConfigStore) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.IndexedUserStore (IndexedUserStore) import Wire.InvitationCodeStore import Wire.NotificationSubsystem import Wire.PasswordResetCodeStore (PasswordResetCodeStore) @@ -164,6 +165,7 @@ import Wire.Sem.Concurrency import Wire.Sem.Jwk (Jwk) import Wire.Sem.Now (Now) import Wire.Sem.Paging.Cassandra +import Wire.TeamInvitationSubsystem import Wire.UserKeyStore import Wire.UserSearch.Types import Wire.UserStore (UserStore) @@ -271,7 +273,6 @@ servantSitemap :: Member (Error UserSubsystemError) r, Member (Input (Local ())) r, Member (Input UTCTime) r, - Member (Input TeamTemplates) r, Member (UserPendingActivationStore p) r, Member AuthenticationSubsystem r, Member DeleteQueue r, @@ -293,11 +294,14 @@ servantSitemap :: Member TinyLog r, Member UserKeyStore r, Member UserStore r, + Member (Input TeamTemplates) r, Member UserSubsystem r, + Member TeamInvitationSubsystem r, Member VerificationCodeSubsystem r, Member (Concurrency 'Unsafe) r, Member BlockListStore r, - Member (ConnectionStore InternalPaging) r + Member (ConnectionStore InternalPaging) r, + Member IndexedUserStore r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -711,7 +715,8 @@ upgradePersonalToTeam :: Member (Input UTCTime) r, Member NotificationSubsystem r, Member TinyLog r, - Member UserSubsystem r + Member UserSubsystem r, + Member UserStore r ) => Local UserId -> Public.BindingNewTeamUser -> diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 4c994cb434f..7719f808ac0 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -66,12 +66,10 @@ module Brig.API.User -- * Utilities fetchUserIdentity, - findTeamInvitation, ) where import Brig.API.Error qualified as Error -import Brig.API.Handler qualified as API (UserNotAllowedToJoinTeam (..)) import Brig.API.Types import Brig.API.Util import Brig.App as App @@ -90,7 +88,6 @@ import Brig.Options hiding (internalEvents) import Brig.Types.Activation (ActivationPair) import Brig.Types.Intra import Brig.User.Auth.Cookie qualified as Auth -import Brig.User.Search.TeamSize qualified as TeamSize import Cassandra hiding (Set) import Control.Error import Control.Lens (preview, to, (^.), _Just) @@ -127,7 +124,6 @@ import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Team hiding (newTeam) import Wire.API.Team.Member (legalHoldStatus) import Wire.API.Team.Role -import Wire.API.Team.Size import Wire.API.User import Wire.API.User.Activation import Wire.API.User.Client @@ -141,7 +137,7 @@ import Wire.Error import Wire.Events (Events) import Wire.Events qualified as Events import Wire.GalleyAPIAccess as GalleyAPIAccess -import Wire.InvitationCodeStore (InvitationCodeStore, StoredInvitation, StoredInvitationInfo) +import Wire.InvitationCodeStore (InvitationCodeStore, StoredInvitation) import Wire.InvitationCodeStore qualified as InvitationCodeStore import Wire.NotificationSubsystem import Wire.PasswordResetCodeStore (PasswordResetCodeStore) @@ -260,6 +256,7 @@ upgradePersonalToTeam :: forall r. ( Member GalleyAPIAccess r, Member EmailSubsystem r, + Member UserStore r, Member UserSubsystem r, Member TinyLog r, Member (Embed HttpClientIO) r, @@ -292,7 +289,7 @@ upgradePersonalToTeam luid bNewTeam = do let newTeam = bNewTeam.bnuTeam pure $ CreateUserTeam tid (fromRange newTeam.newTeamName) - wrapClient $ updateUserTeam uid tid + liftSem $ updateUserTeam uid tid liftSem $ Intra.sendUserEvent uid Nothing (teamUpdated uid tid) initAccountFeatureConfig uid @@ -330,15 +327,15 @@ createUser new = do (mNewTeamUser, teamInvitation, tid) <- case newUserTeam new of Just (NewTeamMember i) -> do - (inv, info) <- findTeamInvitation (mkEmailKey <$> email) i - pure (Nothing, Just (inv, info), Just info.teamId) + inv <- lift $ liftSem $ internalFindTeamInvitation (mkEmailKey <$> email) i + pure (Nothing, Just inv, Just inv.teamId) Just (NewTeamCreator t) -> do (Just t,Nothing,) <$> (Just . Id <$> liftIO nextRandom) Just (NewTeamMemberSSO tid) -> pure (Nothing, Nothing, Just tid) Nothing -> pure (Nothing, Nothing, Nothing) - let mbInv = (.invitationId) . fst <$> teamInvitation + let mbInv = (.invitationId) <$> teamInvitation mbExistingAccount <- lift $ join @@ -402,8 +399,8 @@ createUser new = do _ -> pure Nothing joinedTeamInvite <- case teamInvitation of - Just (inv, invInfo) -> do - acceptTeamInvitation account inv invInfo (mkEmailKey inv.email) (EmailIdentity inv.email) + Just inv -> do + acceptInvitationToTeam account inv (mkEmailKey inv.email) (EmailIdentity inv.email) Team.TeamName nm <- lift $ liftSem $ GalleyAPIAccess.getTeamName inv.teamId pure (Just $ CreateUserTeam inv.teamId nm) Nothing -> pure Nothing @@ -432,14 +429,13 @@ createUser new = do verifyUniquenessAndCheckBlacklist k !>> identityErrorToRegisterError pure email - acceptTeamInvitation :: + acceptInvitationToTeam :: UserAccount -> StoredInvitation -> - StoredInvitationInfo -> EmailKey -> UserIdentity -> ExceptT RegisterError (AppT r) () - acceptTeamInvitation account inv invitationInfo uk ident = do + acceptInvitationToTeam account inv uk ident = do let uid = userId (accountUser account) ok <- lift $ liftSem $ claimKey uk uid unless ok $ @@ -448,7 +444,7 @@ createUser new = do minvmeta = (,inv.createdAt) <$> inv.createdBy role :: Role role = fromMaybe defaultRole inv.role - added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid invitationInfo.teamId minvmeta role + added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid inv.teamId minvmeta role unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do @@ -458,7 +454,7 @@ createUser new = do liftSem do Log.info $ field "user" (toByteString uid) - . field "team" (toByteString $ invitationInfo.teamId) + . field "team" (toByteString $ inv.teamId) . msg (val "Accepting invitation") UserPendingActivationStore.remove uid InvitationCodeStore.deleteInvitation inv.teamId inv.invitationId @@ -499,37 +495,6 @@ createUser new = do !>> activationErrorToRegisterError pure Nothing -findTeamInvitation :: - ( Member GalleyAPIAccess r, - Member InvitationCodeStore r - ) => - Maybe EmailKey -> - InvitationCode -> - ExceptT RegisterError (AppT r) (StoredInvitation, StoredInvitationInfo) -findTeamInvitation Nothing _ = throwE RegisterErrorMissingIdentity -findTeamInvitation (Just e) c = - lift (liftSem $ InvitationCodeStore.lookupInvitationInfo c) >>= \case - Just invitationInfo -> do - inv <- lift . liftSem $ InvitationCodeStore.lookupInvitation invitationInfo.teamId invitationInfo.invitationId - case (inv, (.email) <$> inv) of - (Just invite, Just em) - | e == mkEmailKey em -> do - ensureMemberCanJoin invitationInfo.teamId - pure (invite, invitationInfo) - _ -> throwE RegisterErrorInvalidInvitationCode - Nothing -> throwE RegisterErrorInvalidInvitationCode - where - ensureMemberCanJoin :: (Member GalleyAPIAccess r) => TeamId -> ExceptT RegisterError (AppT r) () - ensureMemberCanJoin tid = do - maxSize <- fromIntegral <$> asks (.settings.maxTeamSize) - (TeamSize teamSize) <- TeamSize.teamSize tid - when (teamSize >= maxSize) $ - throwE RegisterErrorTooManyTeamMembers - -- FUTUREWORK: The above can easily be done/tested in the intra call. - -- Remove after the next release. - mAddUserError <- lift $ liftSem $ GalleyAPIAccess.checkUserCanJoinTeam tid - maybe (pure ()) (throwM . API.UserNotAllowedToJoinTeam) mAddUserError - initAccountFeatureConfig :: UserId -> (AppT r) () initAccountFeatureConfig uid = do mStatus <- preview (App.settingsLens . featureFlagsLens . _Just . to conferenceCalling . to forNew . _Just) diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 2997eb46907..4b6af606d58 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -28,6 +28,7 @@ import Polysemy.Conc import Polysemy.Embed (runEmbedded) import Polysemy.Error (Error, errorToIOFinal, mapError, runError) import Polysemy.Input (Input, runInputConst, runInputSem) +import Polysemy.Internal.Kind import Polysemy.TinyLog (TinyLog) import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Federation.Client qualified @@ -84,6 +85,9 @@ import Wire.Sem.Random import Wire.Sem.Random.IO import Wire.SessionStore import Wire.SessionStore.Cassandra (interpretSessionStoreCassandra) +import Wire.TeamInvitationSubsystem +import Wire.TeamInvitationSubsystem.Error +import Wire.TeamInvitationSubsystem.Interpreter import Wire.UserKeyStore import Wire.UserKeyStore.Cassandra import Wire.UserStore @@ -98,13 +102,20 @@ import Wire.VerificationCodeSubsystem.Interpreter type BrigCanonicalEffects = '[ AuthenticationSubsystem, - UserSubsystem, - EmailSubsystem, + TeamInvitationSubsystem, + UserSubsystem + ] + `Append` BrigLowerLevelEffects + +-- | These effects have interpreters which don't depend on each other +type BrigLowerLevelEffects = + '[ EmailSubsystem, VerificationCodeSubsystem, PropertySubsystem, DeleteQueue, Wire.Events.Events, Error UserSubsystemError, + Error TeamInvitationSubsystemError, Error AuthenticationSubsystemError, Error Wire.API.Federation.Error.FederationError, Error VerificationCodeSubsystemError, @@ -163,7 +174,13 @@ runBrigToIO e (AppT ma) = do UserSubsystemConfig { emailVisibilityConfig = e.settings.emailVisibility, defaultLocale = Opt.defaultUserLocale e.settings, - searchSameTeamOnly = fromMaybe False e.settings.searchSameTeamOnly + searchSameTeamOnly = fromMaybe False e.settings.searchSameTeamOnly, + maxTeamSize = e.settings.maxTeamSize + } + teamInvitationSubsystemConfig = + TeamInvitationSubsystemConfig + { maxTeamSize = e.settings.maxTeamSize, + teamInvitationTimeout = e.settings.teamInvitationTimeout } federationApiAccessConfig = FederationAPIAccessConfig @@ -193,6 +210,14 @@ runBrigToIO e (AppT ma) = do indexName = additionalIndexName } } + + -- These interpreters depend on each other, we use let recursion to solve that. + userSubsystemInterpreter :: (Members BrigLowerLevelEffects r) => InterpreterFor UserSubsystem r + userSubsystemInterpreter = runUserSubsystem userSubsystemConfig authSubsystemInterpreter + + authSubsystemInterpreter :: (Members BrigLowerLevelEffects r) => InterpreterFor AuthenticationSubsystem r + authSubsystemInterpreter = interpretAuthenticationSubsystem userSubsystemInterpreter + ( either throwM pure <=< ( runFinal . unsafelyPerformConcurrency @@ -244,14 +269,16 @@ runBrigToIO e (AppT ma) = do . mapError verificationCodeSubsystemErrorToHttpError . mapError (StdError . federationErrorToWai) . mapError authenticationSubsystemErrorToHttpError + . mapError teamInvitationErrorToHttpError . mapError userSubsystemErrorToHttpError . runEvents . runDeleteQueue e.internalEvents . interpretPropertySubsystem propertySubsystemConfig . interpretVerificationCodeSubsystem - . emailSubsystemInterpreter e.userTemplates e.templateBranding - . runUserSubsystem userSubsystemConfig - . interpretAuthenticationSubsystem + . emailSubsystemInterpreter e.userTemplates e.teamTemplates e.templateBranding + . userSubsystemInterpreter + . runTeamInvitationSubsystem teamInvitationSubsystemConfig + . authSubsystemInterpreter ) ) $ runReaderT ma e diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index cdf4cc7677b..fec86b7a4e6 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -53,7 +53,6 @@ module Brig.Data.User updateStatus, updateRichInfo, updateFeatureConferenceCalling, - updateUserTeam, -- * Deletions deleteEmail, @@ -383,12 +382,6 @@ lookupUserTeam u = (runIdentity =<<) <$> retry x1 (query1 teamSelect (params LocalQuorum (Identity u))) -updateUserTeam :: (MonadClient m) => UserId -> TeamId -> m () -updateUserTeam u t = retry x5 $ write userTeamUpdate (params LocalQuorum (t, u)) - where - userTeamUpdate :: PrepQuery W (TeamId, UserId) () - userTeamUpdate = "UPDATE user SET team = ? WHERE id = ?" - lookupAuth :: (MonadClient m) => UserId -> m (Maybe (Maybe Password, AccountStatus)) lookupAuth u = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity u))) where diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index cf4f98dbce2..8b13deab2ef 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -21,36 +21,29 @@ module Brig.Team.API getInvitationCode, suspendTeam, unsuspendTeam, - teamSize, createInvitationViaScim, ) where import Brig.API.Error import Brig.API.Handler -import Brig.API.User (createUserInviteViaScim, fetchUserIdentity) +import Brig.API.User (createUserInviteViaScim) import Brig.API.User qualified as API import Brig.API.Util (logEmail, logInvitationCode) import Brig.App as App -import Brig.Data.User as User import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) -import Brig.Options -import Brig.Team.Email -import Brig.Team.Template import Brig.Types.Team (TeamSize) -import Brig.User.Search.TeamSize qualified as TeamSize import Control.Lens (view, (^.)) import Control.Monad.Trans.Except (mapExceptT) -import Data.ByteString.Conversion (toByteString, toByteString') +import Data.ByteString.Conversion (toByteString) import Data.Id import Data.List1 qualified as List1 -import Data.Qualified (Local, tUnqualified) +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 Data.Tuple.Extra import Imports hiding (head) import Network.Wai.Utilities hiding (Error, code, message) import Polysemy @@ -62,9 +55,6 @@ import Servant hiding (Handler, JSON, addHeader) import System.Logger.Message as Log import URI.ByteString (Absolute, URIRef, laxURIParserOptions, parseURI) import Util.Logging (logFunction, logTeam) -import Wire.API.Error -import Wire.API.Error.Brig qualified as E -import Wire.API.Password import Wire.API.Routes.Internal.Brig (FoundInvitationCode (FoundInvitationCode)) import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Named @@ -76,78 +66,67 @@ 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.Team.Role qualified as Public import Wire.API.User hiding (fromEmail) -import Wire.API.User qualified as Public -import Wire.API.UserEvent import Wire.BlockListStore -import Wire.EmailSending (EmailSending) import Wire.EmailSubsystem.Template import Wire.Error import Wire.Events (Events) -import Wire.Events qualified as Events import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..)) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.IndexedUserStore (IndexedUserStore, getTeamSize) import Wire.InvitationCodeStore (InvitationCodeStore (..), PaginatedResult (..), StoredInvitation (..)) import Wire.InvitationCodeStore qualified as Store -import Wire.InvitationCodeStore.Cassandra qualified as Store (mkInvitationCode) -import Wire.PasswordStore import Wire.Sem.Concurrency +import Wire.TeamInvitationSubsystem import Wire.UserKeyStore import Wire.UserSubsystem -import Wire.UserSubsystem qualified as User import Wire.UserSubsystem.Error servantAPI :: ( Member GalleyAPIAccess r, - Member UserKeyStore r, + Member TeamInvitationSubsystem r, Member UserSubsystem r, Member Store.InvitationCodeStore r, - Member EmailSending r, - Member (Input (Local ())) r, Member TinyLog r, - Member PasswordStore r, Member (Input TeamTemplates) r, - Member Events r, - Member (Error UserSubsystemError) r + Member (Input (Local ())) r, + Member (Error UserSubsystemError) r, + Member IndexedUserStore r ) => ServerT TeamsAPI (Handler r) servantAPI = - Named @"send-team-invitation" createInvitation - :<|> 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) - :<|> Named @"get-team-invitation-info" getInvitationByCode + 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) + :<|> Named @"get-team-invitation-info" (lift . liftSem . getInvitationByCode) :<|> Named @"head-team-invitations" (lift . liftSem . headInvitationByEmail) - :<|> Named @"get-team-size" teamSizePublic - :<|> Named @"accept-team-invitation" acceptTeamInvitationByPersonalUser + :<|> Named @"get-team-size" (\uid tid -> lift . liftSem $ teamSizePublic uid tid) + :<|> Named @"accept-team-invitation" (\luid req -> lift $ liftSem $ acceptTeamInvitation luid req.password req.code) teamSizePublic :: ( Member GalleyAPIAccess r, - Member (Error UserSubsystemError) r + Member (Error UserSubsystemError) r, + Member IndexedUserStore r ) => UserId -> TeamId -> - (Handler r) TeamSize + Sem r TeamSize teamSizePublic uid tid = do -- limit this to team admins to reduce risk of involuntary DOS attacks - lift . liftSem $ ensurePermissions uid tid [AddTeamMember] - teamSize tid - -teamSize :: TeamId -> (Handler r) TeamSize -teamSize t = lift $ TeamSize.teamSize t + ensurePermissions uid tid [AddTeamMember] + getTeamSize tid getInvitationCode :: - (Member Store.InvitationCodeStore r) => + ( Member Store.InvitationCodeStore r, + Member (Error UserSubsystemError) r + ) => TeamId -> InvitationId -> - (Handler r) FoundInvitationCode + Sem r FoundInvitationCode getInvitationCode t r = do - inv <- lift . liftSem $ Store.lookupInvitation t r - maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) (pure . FoundInvitationCode . (.code)) inv + inv <- Store.lookupInvitation t r + maybe (throw UserSubsystemInvalidInvitationCode) (pure . FoundInvitationCode . (.code)) inv data CreateInvitationInviter = CreateInvitationInviter { inviterUid :: UserId, @@ -155,60 +134,18 @@ data CreateInvitationInviter = CreateInvitationInviter } deriving (Eq, Show) -createInvitation :: - ( Member GalleyAPIAccess r, - Member UserKeyStore r, - Member InvitationCodeStore r, - Member UserSubsystem r, - Member EmailSending r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input TeamTemplates) r, - Member (Error UserSubsystemError) r - ) => - UserId -> - TeamId -> - Public.InvitationRequest -> - Handler r (Public.Invitation, Public.InvitationLocation) -createInvitation uid tid body = do - let inviteeRole = fromMaybe defaultRole body.role - inviter <- do - let inviteePerms = Teams.rolePermissions inviteeRole - idt <- maybe (throwStd (errorToWai @'E.NoIdentity)) pure =<< lift (fetchUserIdentity uid) - from <- maybe (throwStd (errorToWai @'E.NoEmail)) pure (emailIdentity idt) - lift . liftSem $ ensurePermissionToAddUser uid tid inviteePerms - pure $ CreateInvitationInviter uid from - - let context = - logFunction "Brig.Team.API.createInvitation" - . logTeam tid - . logEmail body.inviteeEmail - - (id &&& loc) . fst - <$> logInvitationRequest - context - (createInvitation' tid Nothing inviteeRole (Just (inviterUid inviter)) (inviterEmail inviter) body) - where - loc :: Invitation -> InvitationLocation - loc inv = - InvitationLocation $ "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' inv.invitationId - createInvitationViaScim :: - ( Member GalleyAPIAccess r, - Member BlockListStore r, + ( Member BlockListStore r, Member UserKeyStore r, - Member InvitationCodeStore r, Member (UserPendingActivationStore p) r, Member TinyLog r, - Member UserSubsystem r, - Member EmailSending r, - Member (Input (Local ())) r, - Member (Input TeamTemplates) r + Member TeamInvitationSubsystem r, + Member (Input (Local ())) r ) => TeamId -> NewUserScimInvitation -> (Handler r) UserAccount -createInvitationViaScim tid newUser@(NewUserScimInvitation _tid uid _eid loc name email role) = do +createInvitationViaScim tid newUser@(NewUserScimInvitation _tid _uid@(Id (Id -> invId)) _eid loc name email role) = do env <- ask let inviteeRole = role fromEmail = env.emailSender @@ -225,9 +162,12 @@ createInvitationViaScim tid newUser@(NewUserScimInvitation _tid uid _eid loc nam . logTeam tid . logEmail email + localNothing <- lift . liftSem $ qualifyLocal' Nothing void $ logInvitationRequest context $ - createInvitation' tid (Just uid) inviteeRole Nothing fromEmail invreq + lift $ + liftSem $ + internalCreateInvitation tid (Just invId) inviteeRole localNothing fromEmail invreq createUserInviteViaScim newUser @@ -249,84 +189,6 @@ logInvitationRequest context action = Log.info $ (context . logInvitationCode code) . Log.msg @Text "Successfully created invitation" pure (Right result) -createInvitation' :: - ( Member GalleyAPIAccess r, - Member UserSubsystem r, - Member UserKeyStore r, - Member InvitationCodeStore r, - Member EmailSending r, - Member TinyLog r, - Member (Input (Local ())) r, - Member (Input TeamTemplates) r - ) => - TeamId -> - Maybe UserId -> - Public.Role -> - Maybe UserId -> - EmailAddress -> - Public.InvitationRequest -> - Handler r (Public.Invitation, Public.InvitationCode) -createInvitation' tid mUid inviteeRole mbInviterUid fromEmail invRequest = do - let email = invRequest.inviteeEmail - let uke = mkEmailKey email - blacklistedEm <- lift $ liftSem $ isBlocked email - when blacklistedEm $ - throwStd blacklistedEmail - emailTaken <- lift $ liftSem $ isJust <$> lookupKey uke - isPersonalUserMigration <- - if emailTaken - then lift $ liftSem $ isPersonalUser uke - else pure False - when emailTaken $ - unless isPersonalUserMigration $ - throwStd emailExists - - maxSize <- asks (.settings.maxTeamSize) - pending <- lift $ liftSem $ Store.countInvitations tid - when (fromIntegral pending >= maxSize) $ - throwStd (errorToWai @'E.TooManyTeamInvitations) - - showInvitationUrl <- lift $ liftSem $ GalleyAPIAccess.getExposeInvitationURLsToTeamAdmin tid - - lift $ do - iid <- maybe randomId (pure . Id . toUUID) mUid - now <- liftIO =<< asks (.currentTime) - timeout <- asks (.settings.teamInvitationTimeout) - code <- liftIO $ Store.mkInvitationCode - newInv <- - let insertInv = - Store.MkInsertInvitation - { invitationId = iid, - teamId = tid, - role = inviteeRole, - createdAt = now, - createdBy = mbInviterUid, - inviteeEmail = email, - inviteeName = invRequest.inviteeName, - code = code - -- mUrl = mUrl - } - in liftSem $ Store.insertInvitation insertInv timeout - - let sendOp = - if isPersonalUserMigration - then sendInvitationMailPersonalUser - else sendInvitationMail - - sendOp email tid fromEmail code invRequest.locale - inv <- liftSem $ toInvitation isPersonalUserMigration showInvitationUrl newInv - pure (inv, code) - -isPersonalUser :: (Member UserSubsystem r, Member (Input (Local ())) r) => EmailKey -> Sem r Bool -isPersonalUser uke = do - mAccount <- getLocalUserAccountByUserKey =<< qualifyLocal' uke - pure $ case mAccount of - -- this can e.g. happen if the key is claimed but the account is not yet created - Nothing -> False - Just account -> - account.accountStatus == Active - && isNothing account.accountUser.userTeam - deleteInvitation :: ( Member GalleyAPIAccess r, Member InvitationCodeStore r, @@ -370,7 +232,7 @@ 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 is ignored here + toInvitationHack HideInvitationUrl si = toInvitation False HideInvitationUrl si -- isPersonalUserMigration is always ignored here toInvitationHack ShowInvitationUrl si = do isPersonalUserMigration <- isPersonalUser (mkEmailKey si.email) toInvitation isPersonalUserMigration ShowInvitationUrl si @@ -477,13 +339,25 @@ getInvitation uid tid iid = do maybeUrl <- mkInviteUrl showInvitationUrl tid invitation.code pure $ Just (Store.invitationFromStored maybeUrl invitation) +isPersonalUser :: (Member UserSubsystem r, Member (Input (Local ())) r) => EmailKey -> Sem r Bool +isPersonalUser uke = do + mAccount <- getLocalUserAccountByUserKey =<< qualifyLocal' uke + pure $ case mAccount of + -- this can e.g. happen if the key is claimed but the account is not yet created + Nothing -> False + Just account -> + account.accountStatus == Active + && isNothing account.accountUser.userTeam + getInvitationByCode :: - (Member Store.InvitationCodeStore r) => - Public.InvitationCode -> - (Handler r) Public.Invitation + ( Member Store.InvitationCodeStore r, + Member (Error UserSubsystemError) r + ) => + InvitationCode -> + Sem r Public.Invitation getInvitationByCode c = do - inv <- lift . liftSem $ Store.lookupInvitationByCode c - maybe (throwStd $ errorToWai @'E.InvalidInvitationCode) (pure . Store.invitationFromStored Nothing) inv + inv <- Store.lookupInvitationByCode c + maybe (throw UserSubsystemInvalidInvitationCode) (pure . Store.invitationFromStored Nothing) inv headInvitationByEmail :: (Member InvitationCodeStore r, Member TinyLog r) => @@ -567,49 +441,3 @@ changeTeamAccountStatuses tid s = do where toList1 (x : xs) = pure $ List1.list1 x xs toList1 [] = throwStd (notFound "Team not found or no members") - -acceptTeamInvitationByPersonalUser :: - forall r. - ( Member UserSubsystem r, - Member GalleyAPIAccess r, - Member InvitationCodeStore r, - Member PasswordStore r, - Member Events r - ) => - Local UserId -> - AcceptTeamInvitation -> - (Handler r) () -acceptTeamInvitationByPersonalUser luid req = do - (mek, mTid) <- do - mSelfProfile <- lift $ liftSem $ getSelfProfile luid - let mek = mkEmailKey <$> (userEmail . selfUser =<< mSelfProfile) - mTid = mSelfProfile >>= userTeam . selfUser - pure (mek, mTid) - checkPassword - (inv, (.teamId) -> tid) <- API.findTeamInvitation mek req.code !>> toInvitationError - let minvmeta = (,inv.createdAt) <$> inv.createdBy - uid = tUnqualified luid - for_ mTid $ \userTid -> - unless (tid == userTid) $ - throwStd (errorToWai @'E.CannotJoinMultipleTeams) - added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid minvmeta (fromMaybe defaultRole inv.role) - unless added $ throwStd (errorToWai @'E.TooManyTeamMembers) - lift $ do - wrapClient $ User.updateUserTeam uid tid - liftSem $ Store.deleteInvitation inv.teamId inv.invitationId - liftSem $ User.internalUpdateSearchIndex uid - liftSem $ Events.generateUserEvent uid Nothing (teamUpdated uid tid) - where - checkPassword = do - p <- - lift (liftSem . lookupHashedPassword . tUnqualified $ luid) - >>= maybe (throwStd (errorToWai @'E.MissingAuth)) pure - unless (verifyPassword req.password p) $ - throwStd (errorToWai @'E.BadCredentials) - toInvitationError :: RegisterError -> HttpError - toInvitationError = \case - RegisterErrorMissingIdentity -> StdError (errorToWai @'E.MissingIdentity) - RegisterErrorInvalidActivationCodeWrongUser -> StdError (errorToWai @'E.InvalidActivationCodeWrongUser) - RegisterErrorInvalidActivationCodeWrongCode -> StdError (errorToWai @'E.InvalidActivationCodeWrongCode) - RegisterErrorInvalidInvitationCode -> StdError (errorToWai @'E.InvalidInvitationCode) - _ -> StdError (notFound "Something went wrong, while looking up the invitation") diff --git a/services/brig/src/Brig/Team/Email.hs b/services/brig/src/Brig/Team/Email.hs index 042843132e9..e6d0cdaeb7f 100644 --- a/services/brig/src/Brig/Team/Email.hs +++ b/services/brig/src/Brig/Team/Email.hs @@ -18,11 +18,8 @@ -- with this program. If not, see . module Brig.Team.Email - ( InvitationEmail (..), - CreatorWelcomeEmail (..), + ( CreatorWelcomeEmail (..), MemberWelcomeEmail (..), - sendInvitationMail, - sendInvitationMailPersonalUser, sendMemberWelcomeMail, ) where @@ -30,7 +27,6 @@ where import Brig.App import Brig.Team.Template import Data.Id (TeamId, idToText) -import Data.Text.Ascii qualified as Ascii import Data.Text.Lazy (toStrict) import Imports import Network.Mail.Mime @@ -39,20 +35,6 @@ import Wire.API.User import Wire.EmailSending import Wire.EmailSubsystem.Template (TemplateBranding, renderHtmlWithBranding, renderTextWithBranding) -sendInvitationMail :: (Member EmailSending r) => EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> (AppT r) () -sendInvitationMail to tid from code loc = do - tpl <- invitationEmail . snd <$> teamTemplatesWithLocale loc - branding <- asks (.templateBranding) - let mail = InvitationEmail to tid code from - liftSem $ sendMail $ renderInvitationEmail mail tpl branding - -sendInvitationMailPersonalUser :: (Member EmailSending r) => EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> (AppT r) () -sendInvitationMailPersonalUser to tid from code loc = do - tpl <- existingUserInvitationEmail . snd <$> teamTemplatesWithLocale loc - branding <- asks (.templateBranding) - let mail = InvitationEmail to tid code from - liftSem $ sendMail $ renderInvitationEmail mail tpl branding - sendMemberWelcomeMail :: (Member EmailSending r) => EmailAddress -> TeamId -> Text -> Maybe Locale -> (AppT r) () sendMemberWelcomeMail to tid teamName loc = do tpl <- memberWelcomeEmail . snd <$> teamTemplatesWithLocale loc @@ -60,46 +42,6 @@ sendMemberWelcomeMail to tid teamName loc = do let mail = MemberWelcomeEmail to tid teamName liftSem $ sendMail $ renderMemberWelcomeMail mail tpl branding -------------------------------------------------------------------------------- --- Invitation Email - -data InvitationEmail = InvitationEmail - { invTo :: !EmailAddress, - invTeamId :: !TeamId, - invInvCode :: !InvitationCode, - invInviter :: !EmailAddress - } - -renderInvitationEmail :: InvitationEmail -> InvitationEmailTemplate -> TemplateBranding -> Mail -renderInvitationEmail InvitationEmail {..} InvitationEmailTemplate {..} branding = - (emptyMail from) - { mailTo = [to], - mailHeaders = - [ ("Subject", toStrict subj), - ("X-Zeta-Purpose", "TeamInvitation"), - ("X-Zeta-Code", Ascii.toText code) - ], - mailParts = [[plainPart txt, htmlPart html]] - } - where - (InvitationCode code) = invInvCode - from = Address (Just invitationEmailSenderName) (fromEmail invitationEmailSender) - to = Address Nothing (fromEmail invTo) - txt = renderTextWithBranding invitationEmailBodyText replace branding - html = renderHtmlWithBranding invitationEmailBodyHtml replace branding - subj = renderTextWithBranding invitationEmailSubject replace branding - replace "url" = renderInvitationUrl invitationEmailUrl invTeamId invInvCode branding - replace "inviter" = fromEmail invInviter - replace x = x - -renderInvitationUrl :: Template -> TeamId -> InvitationCode -> TemplateBranding -> Text -renderInvitationUrl t tid (InvitationCode c) branding = - toStrict $ renderTextWithBranding t replace branding - where - replace "team" = idToText tid - replace "code" = Ascii.toText c - replace x = x - ------------------------------------------------------------------------------- -- Creator Welcome Email diff --git a/services/brig/src/Brig/Team/Template.hs b/services/brig/src/Brig/Team/Template.hs index 6d8c28e2110..c7072588515 100644 --- a/services/brig/src/Brig/Team/Template.hs +++ b/services/brig/src/Brig/Team/Template.hs @@ -30,41 +30,7 @@ where import Brig.Options import Brig.Template import Imports -import Wire.API.User.Identity - -data InvitationEmailTemplate = InvitationEmailTemplate - { invitationEmailUrl :: !Template, - invitationEmailSubject :: !Template, - invitationEmailBodyText :: !Template, - invitationEmailBodyHtml :: !Template, - invitationEmailSender :: !EmailAddress, - invitationEmailSenderName :: !Text - } - -data CreatorWelcomeEmailTemplate = CreatorWelcomeEmailTemplate - { creatorWelcomeEmailUrl :: !Text, - creatorWelcomeEmailSubject :: !Template, - creatorWelcomeEmailBodyText :: !Template, - creatorWelcomeEmailBodyHtml :: !Template, - creatorWelcomeEmailSender :: !EmailAddress, - creatorWelcomeEmailSenderName :: !Text - } - -data MemberWelcomeEmailTemplate = MemberWelcomeEmailTemplate - { memberWelcomeEmailUrl :: !Text, - memberWelcomeEmailSubject :: !Template, - memberWelcomeEmailBodyText :: !Template, - memberWelcomeEmailBodyHtml :: !Template, - memberWelcomeEmailSender :: !EmailAddress, - memberWelcomeEmailSenderName :: !Text - } - -data TeamTemplates = TeamTemplates - { invitationEmail :: !InvitationEmailTemplate, - existingUserInvitationEmail :: !InvitationEmailTemplate, - creatorWelcomeEmail :: !CreatorWelcomeEmailTemplate, - memberWelcomeEmail :: !MemberWelcomeEmailTemplate - } +import Wire.EmailSubsystem.Template loadTeamTemplates :: Opts -> IO (Localised TeamTemplates) loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \fp -> diff --git a/services/brig/src/Brig/User/Search/TeamSize.hs b/services/brig/src/Brig/User/Search/TeamSize.hs deleted file mode 100644 index 6121ec38178..00000000000 --- a/services/brig/src/Brig/User/Search/TeamSize.hs +++ /dev/null @@ -1,46 +0,0 @@ -{-# LANGUAGE StrictData #-} - --- 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 Brig.User.Search.TeamSize - ( teamSize, - ) -where - -import Brig.Types.Team (TeamSize (..)) -import Brig.User.Search.Index -import Control.Monad.Catch (throwM) -import Data.Id -import Database.Bloodhound qualified as ES -import Imports hiding (log, searchable) -import Wire.IndexedUserStore (IndexedUserStoreError (..)) - -teamSize :: (MonadIndexIO m) => TeamId -> m TeamSize -teamSize t = liftIndexIO $ do - indexName <- asks idxName - countResEither <- ES.countByIndex indexName (ES.CountQuery query) - countRes <- either (throwM . IndexLookupError) pure countResEither - pure . TeamSize $ ES.crCount countRes - where - query = - ES.TermQuery - ES.Term - { ES.termField = "team", - ES.termValue = idToText t - } - Nothing From d65ae1734b3bf65451ea65dd6533cfd70eecdb0d Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 26 Sep 2024 11:35:52 +0200 Subject: [PATCH 092/136] [WPB-10659] Test notifications for personal user to team user migration (#4268) * integration: Separate context from message in AssertionFailure * integration: Allow looking up elements from the end in JSON arrays * integration: Test personal user joining a large team * integration: Assert on notifications for team members when a personal user joins the team * integration: Assert that team admins get team.member-join on the webscoket Co-authored-by: Akshay Mankar --- changelog.d/2-features/WPB-10658 | 2 +- integration/test/API/Galley.hs | 7 +++ integration/test/Notifications.hs | 3 + integration/test/Test/Teams.hs | 86 ++++++++++++++++++++++++-- integration/test/Testlib/Assertions.hs | 10 ++- integration/test/Testlib/Cannon.hs | 10 ++- integration/test/Testlib/HTTP.hs | 4 +- integration/test/Testlib/JSON.hs | 6 +- integration/test/Testlib/Types.hs | 14 +++-- 9 files changed, 124 insertions(+), 18 deletions(-) diff --git a/changelog.d/2-features/WPB-10658 b/changelog.d/2-features/WPB-10658 index e0d4302a688..b8eff34863d 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 +Allow an existing non-team user to migrate to a team (#4229, ##) diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 01df8dc89ea..e7bdbf486f9 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -720,3 +720,10 @@ setTeamFeatureConfigVersioned versioned user team featureName payload = do -- | http://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_feature_configs getFeaturesForUser :: (HasCallStack, MakesValue user) => user -> App Response getFeaturesForUser user = baseRequest user Galley Versioned "feature-configs" >>= submit "GET" + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_teams_notifications +getTeamNotifications :: (HasCallStack, MakesValue user) => user -> Maybe String -> App Response +getTeamNotifications user mSince = + baseRequest user Galley Versioned "teams/notifications" >>= \req -> + submit "GET" + $ addQueryParams [("since", since) | since <- maybeToList mSince] req diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index 548f930fd24..d99b46b8897 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -166,6 +166,9 @@ isConvDeleteNotif n = fieldEquals n "payload.0.type" "conversation.delete" notifTypeIsEqual :: (MakesValue a) => String -> a -> App Bool notifTypeIsEqual typ n = nPayload n %. "type" `isEqual` typ +isTeamMemberJoinNotif :: (MakesValue a) => a -> App Bool +isTeamMemberJoinNotif = notifTypeIsEqual "team.member-join" + isTeamMemberLeaveNotif :: (MakesValue a) => a -> App Bool isTeamMemberLeaveNotif = notifTypeIsEqual "team.member-leave" diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs index dbf08e3e2a7..082186d78df 100644 --- a/integration/test/Test/Teams.hs +++ b/integration/test/Test/Teams.hs @@ -20,12 +20,12 @@ module Test.Teams where import API.Brig import API.BrigInternal (createUser, getInvitationCode, refreshIndex) import API.Common -import API.Galley (getTeam, getTeamMembers) +import API.Galley (getTeam, getTeamMembers, getTeamNotifications) import API.GalleyInternal (setTeamFeatureStatus) import Control.Monad.Codensity (Codensity (runCodensity)) import Control.Monad.Extra (findM) import Control.Monad.Reader (asks) -import Notifications (isUserUpdatedNotif) +import Notifications import SetupHelpers import Testlib.JSON import Testlib.Prelude @@ -63,11 +63,20 @@ testInvitePersonalUserToTeam = do acceptTeamInvitation user code Nothing >>= assertStatus 400 acceptTeamInvitation user code (Just "wrong-password") >>= assertStatus 403 - void $ withWebSockets [user] $ \wss -> do + withWebSockets [owner, user, tm] $ \wss@[wsOwner, _, _] -> do acceptTeamInvitation user code (Just defPassword) >>= assertSuccess - for wss $ \ws -> do - n <- awaitMatch isUserUpdatedNotif ws - n %. "payload.0.user.team" `shouldMatch` tid + + -- When the team is smaller than fanout limit, all members get this + -- notification. + for_ wss $ \ws -> do + updateNotif <- awaitMatch isUserUpdatedNotif ws + updateNotif %. "payload.0.user.team" `shouldMatch` tid + + -- Admins get a team.member-join notif on the websocket for + -- team-settings + memberJobNotif <- awaitMatch isTeamMemberJoinNotif wsOwner + memberJobNotif %. "payload.0.team" `shouldMatch` tid + memberJobNotif %. "payload.0.data.user" `shouldMatch` objId user bindResponse (getSelf user) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -126,6 +135,71 @@ testInvitePersonalUserToTeam = do queryParam <- url & asString <&> getQueryParam "team-code" queryParam `shouldMatch` Just (Just code) +testInvitePersonalUserToLargeTeam :: (HasCallStack) => App () +testInvitePersonalUserToLargeTeam = do + teamSize <- readServiceConfig Galley %. "settings.maxFanoutSize" & asInt <&> (+ 1) + (owner, tid, (alice : otherTeamMembers)) <- createTeam OwnDomain teamSize + -- User to be invited to the team + knut <- createUser OwnDomain def >>= getJSON 201 + + -- Non team friends of knut + dawn <- createUser OwnDomain def >>= getJSON 201 + eli <- createUser OtherDomain def >>= getJSON 201 + + -- knut is also friends with alice, but not any other team members. + traverse_ (connectTwoUsers knut) [alice, dawn, eli] + + addFailureContext ("tid: " <> tid) $ do + uidContext <- mkContextUserIds [("owner", owner), ("alice", alice), ("knut", knut), ("dawn", dawn), ("eli", eli)] + addFailureContext uidContext $ do + lastTeamNotif <- + getTeamNotifications owner Nothing `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "notifications.-1.id" & asString + + knutEmail <- knut %. "email" >>= asString + inv <- postInvitation owner (PostInvitation (Just knutEmail) Nothing) >>= getJSON 201 + code <- getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + + withWebSockets [owner, alice, dawn, eli, head otherTeamMembers] $ \[wsOwner, wsAlice, wsDawn, wsEli, wsOther] -> do + acceptTeamInvitation knut code (Just defPassword) >>= assertSuccess + + for_ [wsAlice, wsDawn] $ \ws -> do + notif <- awaitMatch isUserUpdatedNotif ws + nPayload notif %. "user.id" `shouldMatch` (objId knut) + nPayload notif %. "user.team" `shouldMatch` tid + + -- Admins get a team.member-join notif on the websocket for + -- team-settings + memberJobNotif <- awaitMatch isTeamMemberJoinNotif wsOwner + memberJobNotif %. "payload.0.team" `shouldMatch` tid + memberJobNotif %. "payload.0.data.user" `shouldMatch` objId knut + + -- Other team members don't get notified on the websocket + assertNoEvent 1 wsOther + + -- Remote users are not notified at all + assertNoEvent 1 wsEli + + -- Other team members learn about knut via team notifications + getTeamNotifications (head otherTeamMembers) (Just lastTeamNotif) `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + -- Ignore the first notif because it is always the notif matching the + -- lastTeamNotif id. + resp.json %. "notifications.1.payload.0.type" `shouldMatch` "team.member-join" + resp.json %. "notifications.1.payload.0.team" `shouldMatch` tid + resp.json %. "notifications.1.payload.0.data.user" `shouldMatch` objId knut + +mkContextUserIds :: (MakesValue user) => [(String, user)] -> App String +mkContextUserIds = + fmap (intercalate "\n") + . traverse + ( \(name, user) -> do + uid <- objQidObject user %. "id" & asString + domain <- objDomain user + pure $ name <> ": " <> uid <> "@" <> domain + ) + testInvitePersonalUserToTeamMultipleInvitations :: (HasCallStack) => App () testInvitePersonalUserToTeamMultipleInvitations = do (owner, tid, _) <- createTeam OwnDomain 0 diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index bb4e4a5d573..28ddf0c0af1 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -252,13 +252,21 @@ super `shouldContain` sub = do assertFailure $ "String or List:\n" <> show super <> "\nDoes not contain:\n" <> show sub printFailureDetails :: AssertionFailure -> IO String -printFailureDetails (AssertionFailure stack mbResponse msg) = do +printFailureDetails (AssertionFailure stack mbResponse ctx msg) = do s <- prettierCallStack stack pure . unlines $ colored yellow "assertion failure:" : colored red msg : "\n" <> s : toList (fmap prettyResponse mbResponse) + <> toList (fmap prettyContext ctx) + +prettyContext :: String -> String +prettyContext ctx = do + unlines + [ colored yellow "context:", + colored blue ctx + ] printExceptionDetails :: SomeException -> IO String printExceptionDetails e = do diff --git a/integration/test/Testlib/Cannon.hs b/integration/test/Testlib/Cannon.hs index 7b69cf60cad..3a90d89b170 100644 --- a/integration/test/Testlib/Cannon.hs +++ b/integration/test/Testlib/Cannon.hs @@ -219,7 +219,7 @@ run wsConnect app = do headers = mempty, request = request } - throwIO (AssertionFailure callStack (Just r) (displayException ex)) + throwIO (AssertionFailure callStack (Just r) Nothing (displayException ex)) liftIO $ race_ waitForPresence waitForException pure wsapp @@ -421,7 +421,11 @@ awaitNMatches :: App [Value] awaitNMatches nExpected checkMatch ws = do res <- awaitNMatchesResult nExpected checkMatch ws - assertAwaitResult res + withWebSocketFailureContext ws $ + assertAwaitResult res + +withWebSocketFailureContext :: WebSocket -> App a -> App a +withWebSocketFailureContext ws = addFailureContext ("on websocket for user: " <> ws.wsConnect.user <> "@" <> ws.wsConnect.domain) assertAwaitResult :: (HasCallStack) => AwaitResult -> App [Value] assertAwaitResult res = do @@ -481,7 +485,7 @@ assertNoEvent :: Int -> WebSocket -> App () -assertNoEvent to ws = do +assertNoEvent to ws = withWebSocketFailureContext ws $ do mEvent <- awaitAnyEvent to ws case mEvent of Just event -> assertFailure $ "Expected no event, but got: " <> show event diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index ab7e7d237bf..0b919db3c7c 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -139,8 +139,8 @@ assertLabel status label resp = do onFailureAddResponse :: (HasCallStack) => Response -> App a -> App a onFailureAddResponse r m = App $ do e <- ask - liftIO $ E.catch (runAppWithEnv e m) $ \(AssertionFailure stack _ msg) -> do - E.throw (AssertionFailure stack (Just r) msg) + liftIO $ E.catch (runAppWithEnv e m) $ \(AssertionFailure stack _ ctx msg) -> do + E.throw (AssertionFailure stack (Just r) ctx msg) data Versioned = Versioned | Unversioned | ExplicitVersion Int deriving stock (Generic) diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index a62065ed5f4..31eaf24ce14 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -25,6 +25,7 @@ import Data.String import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Vector ((!?)) +import qualified Data.Vector as V import GHC.Stack import Testlib.Types import Prelude @@ -237,7 +238,10 @@ lookupField val selector = do Object ob -> pure (KM.lookup (KM.fromString k) ob) -- index array Array arr -> case reads k of - [(i, "")] -> pure (arr !? i) + [(i, "")] -> + if i >= 0 + then pure (arr !? i) + else pure (arr !? (V.length arr + i)) _ -> assertFailureWithJSON arr $ "Invalid array index \"" <> k <> "\"" x -> assertFailureWithJSON x ("Object or Array" `typeWasExpectedButGot` x) go k [] v = get v k diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 79762a53df1..1c64fb56f7f 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -312,14 +312,15 @@ getRequestBody req = case HTTP.requestBody req of data AssertionFailure = AssertionFailure { callstack :: CallStack, response :: Maybe Response, + context :: Maybe String, msg :: String } instance Show AssertionFailure where - show (AssertionFailure _ _ msg) = "AssertionFailure _ _ " <> show msg + show (AssertionFailure _ _ _ msg) = "AssertionFailure _ _ _ " <> show msg instance Exception AssertionFailure where - displayException (AssertionFailure _ _ msg) = msg + displayException (AssertionFailure _ _ _ msg) = msg newtype App a = App {unApp :: ReaderT Env IO a} deriving newtype @@ -391,7 +392,7 @@ assertFailure :: (HasCallStack) => String -> App a assertFailure msg = forceList msg $ liftIO $ - E.throw (AssertionFailure callStack Nothing msg) + E.throw (AssertionFailure callStack Nothing Nothing msg) where forceList [] y = y forceList (x : xs) y = seq x (forceList xs y) @@ -404,11 +405,16 @@ assertNothing :: (HasCallStack) => Maybe a -> App () assertNothing = maybe (pure ()) $ const $ assertFailure "Maybe value was Just, not Nothing" addFailureContext :: String -> App a -> App a -addFailureContext msg = modifyFailureMsg (\m -> m <> "\nThis failure happened in this context:\n" <> msg) +addFailureContext ctx = modifyFailureContext (\mCtx0 -> Just $ maybe ctx (\x -> ctx <> "\n" <> x) mCtx0) modifyFailureMsg :: (String -> String) -> App a -> App a modifyFailureMsg modMessage = modifyFailure (\e -> e {msg = modMessage e.msg}) +modifyFailureContext :: (Maybe String -> Maybe String) -> App a -> App a +modifyFailureContext modContext = + modifyFailure + (\e -> e {context = modContext e.context}) + modifyFailure :: (AssertionFailure -> AssertionFailure) -> App a -> App a modifyFailure modifyAssertion action = do env <- ask From 6c1c41e6db6c1b1c2b0efca95e386e37e137acec Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 30 Sep 2024 13:42:20 +0200 Subject: [PATCH 093/136] Document self and copy fields of the Permissions type (#4276) --- libs/wire-api/src/Wire/API/Team/Permission.hs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Team/Permission.hs b/libs/wire-api/src/Wire/API/Team/Permission.hs index 26ddd8865ba..6cf79772019 100644 --- a/libs/wire-api/src/Wire/API/Team/Permission.hs +++ b/libs/wire-api/src/Wire/API/Team/Permission.hs @@ -59,7 +59,9 @@ import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -- Permissions data Permissions = Permissions - { self :: Set Perm, + { -- | User's permissions + self :: Set Perm, + -- | Permissions this user is allowed to grant others copy :: Set Perm } deriving stock (Eq, Ord, Show, Generic) @@ -69,9 +71,11 @@ permissionsSchema :: ValueSchema NamedSwaggerDoc Permissions permissionsSchema = objectWithDocModifier "Permissions" (description ?~ docs) $ Permissions - <$> (permsToInt . self) .= field "self" (intToPerms <$> schema) - <*> (permsToInt . copy) .= field "copy" (intToPerms <$> schema) + <$> (permsToInt . self) .= fieldWithDocModifier "self" selfDoc (intToPerms <$> schema) + <*> (permsToInt . copy) .= fieldWithDocModifier "copy" copyDoc (intToPerms <$> schema) where + selfDoc = S.description ?~ "Permissions that the user has" + copyDoc = S.description ?~ "Permissions that this user is able to grant others" docs = "This is just a complicated way of representing a team role. self and copy \ \always have to contain the same integer, and only the following integers \ From 7f3ccf3ae59318728f6aebcab2218525aaba2a56 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 30 Sep 2024 13:57:23 +0200 Subject: [PATCH 094/136] Fix feature flag defaults (#4265) * Add failing test for flag default issue * Disallow empty download location Since an empty download location is now disallowed, we use the empty string as a value that signifies "no download location" in the database. * Test empty download location * Fix mls migration defaults * Configure enforceFileDownloadLocation on CI * Split feature flag test module * Add global defAllFeatures * Simplify checkPatch * Test non-member access to features * Add CHANGELOG entry * Fix enforceFileDownloadLocation config in chart * Add comment --- changelog.d/3-bug-fixes/flag-defaults | 1 + charts/galley/templates/configmap.yaml | 4 + charts/galley/values.yaml | 5 + hack/helm_vars/wire-server/values.yaml.gotmpl | 6 + integration/integration.cabal | 18 + integration/test/Test/FeatureFlags.hs | 1126 +---------------- integration/test/Test/FeatureFlags/AppLock.hs | 31 + .../Test/FeatureFlags/ClassifiedDomains.hs | 18 + .../Test/FeatureFlags/ConferenceCalling.hs | 26 + .../Test/FeatureFlags/DigitalSignatures.hs | 15 + .../EnforceFileDownloadLocation.hs | 55 + .../test/Test/FeatureFlags/FileSharing.hs | 14 + .../test/Test/FeatureFlags/GuestLinks.hs | 14 + .../test/Test/FeatureFlags/LegalHold.hs | 142 +++ integration/test/Test/FeatureFlags/Mls.hs | 112 ++ .../test/Test/FeatureFlags/MlsE2EId.hs | 121 ++ .../test/Test/FeatureFlags/MlsMigration.hs | 89 ++ .../FeatureFlags/OutlookCalIntegration.hs | 14 + integration/test/Test/FeatureFlags/SSO.hs | 36 + .../FeatureFlags/SearchVisibilityAvailable.hs | 34 + .../FeatureFlags/SearchVisibilityInbound.hs | 32 + .../Test/FeatureFlags/SelfDeletingMessages.hs | 35 + .../SndFactorPasswordChallenge.hs | 16 + integration/test/Test/FeatureFlags/Util.hs | 283 ++++- .../Test/FeatureFlags/ValidateSAMLEmails.hs | 17 + integration/test/Testlib/Cannon.hs | 9 +- libs/wire-api/src/Wire/API/Error/Galley.hs | 65 +- services/galley/galley.integration.yaml | 6 + .../galley/src/Galley/API/Teams/Features.hs | 12 +- .../src/Galley/Cassandra/MakeFeature.hs | 46 +- 30 files changed, 1231 insertions(+), 1171 deletions(-) create mode 100644 changelog.d/3-bug-fixes/flag-defaults create mode 100644 integration/test/Test/FeatureFlags/AppLock.hs create mode 100644 integration/test/Test/FeatureFlags/ClassifiedDomains.hs create mode 100644 integration/test/Test/FeatureFlags/ConferenceCalling.hs create mode 100644 integration/test/Test/FeatureFlags/DigitalSignatures.hs create mode 100644 integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs create mode 100644 integration/test/Test/FeatureFlags/FileSharing.hs create mode 100644 integration/test/Test/FeatureFlags/GuestLinks.hs create mode 100644 integration/test/Test/FeatureFlags/LegalHold.hs create mode 100644 integration/test/Test/FeatureFlags/Mls.hs create mode 100644 integration/test/Test/FeatureFlags/MlsE2EId.hs create mode 100644 integration/test/Test/FeatureFlags/MlsMigration.hs create mode 100644 integration/test/Test/FeatureFlags/OutlookCalIntegration.hs create mode 100644 integration/test/Test/FeatureFlags/SSO.hs create mode 100644 integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs create mode 100644 integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs create mode 100644 integration/test/Test/FeatureFlags/SelfDeletingMessages.hs create mode 100644 integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs create mode 100644 integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs diff --git a/changelog.d/3-bug-fixes/flag-defaults b/changelog.d/3-bug-fixes/flag-defaults new file mode 100644 index 00000000000..52463a2a092 --- /dev/null +++ b/changelog.d/3-bug-fixes/flag-defaults @@ -0,0 +1 @@ +Fix feature flag default calculation for `mlsMigration` and `enforceFileDownloadLocation` diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index ea0cd15354c..85c93804ebe 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -106,6 +106,10 @@ data: fileSharing: {{- toYaml .settings.featureFlags.fileSharing | nindent 10 }} {{- end }} + {{- if .settings.featureFlags.enforceFileDownloadLocation }} + enforceFileDownloadLocation: + {{- toYaml .settings.featureFlags.enforceFileDownloadLocation | nindent 10 }} + {{- end }} {{- if .settings.featureFlags.sndFactorPasswordChallenge }} sndFactorPasswordChallenge: {{- toYaml .settings.featureFlags.sndFactorPasswordChallenge | nindent 10 }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index 821643510ad..f169bb0e93d 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -93,6 +93,11 @@ config: defaults: lockStatus: unlocked status: enabled + enforceFileDownloadLocation: + defaults: + lockStatus: locked + status: disabled + config: {} legalhold: disabled-by-default mls: defaults: diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 8bb14c72ce1..48dec09f67d 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -269,6 +269,12 @@ galley: status: enabled config: domains: ["example.com"] + enforceFileDownloadLocation: + defaults: + status: disabled + lockStatus: unlocked + config: + enforcedDownloadLocation: "downloads" mlsMigration: defaults: status: enabled diff --git a/integration/integration.cabal b/integration/integration.cabal index 0310aea8db3..0688089db8c 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -123,8 +123,26 @@ library Test.Errors Test.ExternalPartner Test.FeatureFlags + Test.FeatureFlags.AppLock + Test.FeatureFlags.ClassifiedDomains + Test.FeatureFlags.ConferenceCalling + Test.FeatureFlags.DigitalSignatures + Test.FeatureFlags.EnforceFileDownloadLocation + Test.FeatureFlags.FileSharing + Test.FeatureFlags.GuestLinks + Test.FeatureFlags.LegalHold + Test.FeatureFlags.Mls + Test.FeatureFlags.MlsE2EId + Test.FeatureFlags.MlsMigration + Test.FeatureFlags.OutlookCalIntegration + Test.FeatureFlags.SearchVisibilityAvailable + Test.FeatureFlags.SearchVisibilityInbound + Test.FeatureFlags.SelfDeletingMessages + Test.FeatureFlags.SndFactorPasswordChallenge + Test.FeatureFlags.SSO Test.FeatureFlags.User Test.FeatureFlags.Util + Test.FeatureFlags.ValidateSAMLEmails Test.Federation Test.Federator Test.LegalHold diff --git a/integration/test/Test/FeatureFlags.hs b/integration/test/Test/FeatureFlags.hs index 6d088b1ce84..e1ecdae4da2 100644 --- a/integration/test/Test/FeatureFlags.hs +++ b/integration/test/Test/FeatureFlags.hs @@ -21,17 +21,13 @@ module Test.FeatureFlags where import qualified API.Galley as Public import qualified API.GalleyInternal as Internal -import Control.Monad.Codensity (Codensity (runCodensity)) -import Control.Monad.Reader import qualified Data.Aeson as A import qualified Data.Aeson.Key as A import qualified Data.Aeson.KeyMap as KM import qualified Data.Set as Set -import Notifications import SetupHelpers import Test.FeatureFlags.Util import Testlib.Prelude -import Testlib.ResourcePool (acquireResources) testLimitedEventFanout :: (HasCallStack) => App () testLimitedEventFanout = do @@ -46,447 +42,14 @@ testLimitedEventFanout = do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "enabled" -testLegalholdDisabledByDefault :: (HasCallStack) => App () -testLegalholdDisabledByDefault = do - let put uid tid st = Internal.setTeamFeatureConfig uid tid "legalhold" (object ["status" .= st]) >>= assertSuccess - let patch uid tid st = Internal.setTeamFeatureStatus uid tid "legalhold" st >>= assertSuccess - forM_ [put, patch] $ \setFeatureStatus -> do - withModifiedBackend - def {galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default"} - $ \domain -> do - (owner, tid, m : _) <- createTeam domain 2 - nonMember <- randomUser domain def - assertForbidden =<< Public.getTeamFeature nonMember tid "legalhold" - -- Test default - checkFeature "legalhold" m tid disabled - -- Test override - setFeatureStatus owner tid "enabled" - checkFeature "legalhold" owner tid enabled - setFeatureStatus owner tid "disabled" - checkFeature "legalhold" owner tid disabled - --- always disabled -testLegalholdDisabledPermanently :: (HasCallStack) => App () -testLegalholdDisabledPermanently = do - let cfgLhDisabledPermanently = - def - { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-permanently" - } - cfgLhDisabledByDefault = - def - { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default" - } - resourcePool <- asks (.resourcePool) - runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do - let domain = testBackend.berDomain - - -- Happy case: DB has no config for the team - runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do - (owner, tid, _) <- createTeam domain 1 - checkFeature "legalhold" owner tid disabled - assertStatus 403 =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" - assertStatus 403 =<< Internal.setTeamFeatureConfig domain tid "legalhold" (object ["status" .= "enabled"]) - - -- Interesting case: The team had LH enabled before backend config was - -- changed to disabled-permanently - (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do - (owner, tid, _) <- createTeam domain 1 - checkFeature "legalhold" owner tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" - checkFeature "legalhold" owner tid enabled - pure (owner, tid) - - runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do - checkFeature "legalhold" owner tid disabled - --- enabled if team is allow listed, disabled in any other case -testLegalholdWhitelistTeamsAndImplicitConsent :: (HasCallStack) => App () -testLegalholdWhitelistTeamsAndImplicitConsent = do - let cfgLhWhitelistTeamsAndImplicitConsent = - def - { galleyCfg = setField "settings.featureFlags.legalhold" "whitelist-teams-and-implicit-consent" - } - cfgLhDisabledByDefault = - def - { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default" - } - resourcePool <- asks (.resourcePool) - runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do - let domain = testBackend.berDomain - - -- Happy case: DB has no config for the team - (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do - (owner, tid, _) <- createTeam domain 1 - checkFeature "legalhold" owner tid disabled - Internal.legalholdWhitelistTeam tid owner >>= assertSuccess - checkFeature "legalhold" owner tid enabled - - -- Disabling it doesn't work - assertStatus 403 =<< Internal.setTeamFeatureStatus domain tid "legalhold" "disabled" - assertStatus 403 =<< Internal.setTeamFeatureConfig domain tid "legalhold" (object ["status" .= "disabled"]) - checkFeature "legalhold" owner tid enabled - pure (owner, tid) - - -- Interesting case: The team had LH disabled before backend config was - -- changed to "whitelist-teams-and-implicit-consent". It should still show - -- enabled when the config gets changed. - runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do - checkFeature "legalhold" owner tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "legalhold" "disabled" - checkFeature "legalhold" owner tid disabled - - runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do - checkFeature "legalhold" owner tid enabled - -testExposeInvitationURLsToTeamAdminConfig :: (HasCallStack) => App () -testExposeInvitationURLsToTeamAdminConfig = do - let cfgExposeInvitationURLsTeamAllowlist tids = - def - { galleyCfg = setField "settings.exposeInvitationURLsTeamAllowlist" tids - } - resourcePool <- asks (.resourcePool) - runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do - let domain = testBackend.berDomain - - let 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 - -- a team that is not in the allow list cannot enable the feature, it will always be disabled and locked - -- even though the internal API request to enable it succeeds - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" - checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabledLocked - -- however, a request to the public API will fail - assertStatus 409 =<< Public.setTeamFeatureConfig owner tid "exposeInvitationURLsToTeamAdmin" (object ["status" .= "enabled"]) - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "disabled" - pure (owner, tid) - - -- Happy case: DB has no config for the team - (owner, tid) <- testNoAllowlistEntry - - -- Interesting case: The team is in the allow list - runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist [tid]) $ \_ -> do - -- when the team is in the allow list the lock status is implicitly unlocked - checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" - checkFeature "exposeInvitationURLsToTeamAdmin" owner tid enabled - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "disabled" - checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" - checkFeature "exposeInvitationURLsToTeamAdmin" owner tid enabled - - -- Interesting case: The team had the feature enabled but is not in allow list - void testNoAllowlistEntry - -testMlsE2EConfigCrlProxyRequired :: (HasCallStack) => App () -testMlsE2EConfigCrlProxyRequired = do - (owner, tid, _) <- createTeam OwnDomain 1 - let configWithoutCrlProxy = - object - [ "config" - .= object - [ "useProxyOnMobile" .= False, - "verificationExpiration" .= A.Number 86400 - ], - "status" .= "enabled" - ] - - -- From API version 6 onwards, the CRL proxy is required, so the request should fail when it's not provided - bindResponse (Public.setTeamFeatureConfig owner tid "mlsE2EId" configWithoutCrlProxy) $ \resp -> do - resp.status `shouldMatchInt` 400 - resp.json %. "label" `shouldMatch` "mls-e2eid-missing-crl-proxy" - - configWithCrlProxy <- - configWithoutCrlProxy - & setField "config.useProxyOnMobile" True - & setField "config.crlProxy" "https://crl-proxy.example.com" - & setField "status" "enabled" - - -- The request should succeed when the CRL proxy is provided - bindResponse (Public.setTeamFeatureConfig owner tid "mlsE2EId" configWithCrlProxy) $ \resp -> do - resp.status `shouldMatchInt` 200 - - -- Assert that the feature config got updated correctly - expectedResponse <- configWithCrlProxy & setField "lockStatus" "unlocked" & setField "ttl" "unlimited" - checkFeature "mlsE2EId" owner tid expectedResponse - -testMlsE2EConfigCrlProxyNotRequiredInV5 :: (HasCallStack) => App () -testMlsE2EConfigCrlProxyNotRequiredInV5 = do - (owner, tid, _) <- createTeam OwnDomain 1 - let configWithoutCrlProxy = - object - [ "config" - .= object - [ "useProxyOnMobile" .= False, - "verificationExpiration" .= A.Number 86400 - ], - "status" .= "enabled" - ] - - -- In API version 5, the CRL proxy is not required, so the request should succeed - bindResponse (Public.setTeamFeatureConfigVersioned (ExplicitVersion 5) owner tid "mlsE2EId" configWithoutCrlProxy) $ \resp -> do - resp.status `shouldMatchInt` 200 - - -- Assert that the feature config got updated correctly - expectedResponse <- - configWithoutCrlProxy - & setField "lockStatus" "unlocked" - & setField "ttl" "unlimited" - & setField "config.crlProxy" "https://crlproxy.example.com" - checkFeature "mlsE2EId" owner tid expectedResponse - -testSSODisabledByDefault :: (HasCallStack) => App () -testSSODisabledByDefault = do - let put uid tid = Internal.setTeamFeatureConfig uid tid "sso" (object ["status" .= "enabled"]) >>= assertSuccess - let patch uid tid = Internal.setTeamFeatureStatus uid tid "sso" "enabled" >>= assertSuccess - forM_ [put, patch] $ \enableFeature -> do - withModifiedBackend - def {galleyCfg = setField "settings.featureFlags.sso" "disabled-by-default"} - $ \domain -> do - (owner, tid, m : _) <- createTeam domain 2 - nonMember <- randomUser domain def - assertForbidden =<< Public.getTeamFeature nonMember tid "sso" - -- Test default - checkFeature "sso" m tid disabled - -- Test override - enableFeature owner tid - checkFeature "sso" owner tid enabled - -testSSOEnabledByDefault :: (HasCallStack) => App () -testSSOEnabledByDefault = do - withModifiedBackend - def {galleyCfg = setField "settings.featureFlags.sso" "enabled-by-default"} - $ \domain -> do - (owner, tid, _m : _) <- createTeam domain 2 - nonMember <- randomUser domain def - assertForbidden =<< Public.getTeamFeature nonMember tid "sso" - checkFeature "sso" owner tid enabled - -- check that the feature cannot be disabled - assertLabel 403 "not-implemented" =<< Internal.setTeamFeatureConfig owner tid "sso" (object ["status" .= "disabled"]) - -testSearchVisibilityDisabledByDefault :: (HasCallStack) => App () -testSearchVisibilityDisabledByDefault = do - withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "disabled-by-default"} $ \domain -> do - (owner, tid, m : _) <- createTeam domain 2 - nonMember <- randomUser domain def - assertForbidden =<< Public.getTeamFeature nonMember tid "searchVisibility" - -- Test default - checkFeature "searchVisibility" m tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "enabled" - checkFeature "searchVisibility" owner tid enabled - assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "disabled" - checkFeature "searchVisibility" owner tid disabled - -testSearchVisibilityEnabledByDefault :: (HasCallStack) => App () -testSearchVisibilityEnabledByDefault = do - withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default"} $ \domain -> do - (owner, tid, m : _) <- createTeam domain 2 - nonMember <- randomUser domain def - assertForbidden =<< Public.getTeamFeature nonMember tid "searchVisibility" - -- Test default - checkFeature "searchVisibility" m tid enabled - assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "disabled" - checkFeature "searchVisibility" owner tid disabled - assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "enabled" - checkFeature "searchVisibility" owner tid enabled - -testSearchVisibilityInbound :: (HasCallStack) => App () -testSearchVisibilityInbound = _testSimpleFlag "searchVisibilityInbound" Public.setTeamFeatureConfig False - -testDigitalSignaturesInternal :: (HasCallStack) => App () -testDigitalSignaturesInternal = _testSimpleFlag "digitalSignatures" Internal.setTeamFeatureConfig False - -testValidateSAMLEmailsInternal :: (HasCallStack) => App () -testValidateSAMLEmailsInternal = _testSimpleFlag "validateSAMLemails" Internal.setTeamFeatureConfig True - -testSearchVisibilityInboundInternal :: (HasCallStack) => App () -testSearchVisibilityInboundInternal = _testSimpleFlag "searchVisibilityInbound" Internal.setTeamFeatureConfig False - -_testSimpleFlag :: (HasCallStack) => String -> (Value -> String -> String -> Value -> App Response) -> Bool -> App () -_testSimpleFlag featureName setFeatureConfig featureEnabledByDefault = do - let defaultStatus = if featureEnabledByDefault then "enabled" else "disabled" - let defaultValue = if featureEnabledByDefault then enabled else disabled - let otherStatus = if featureEnabledByDefault then "disabled" else "enabled" - let otherValue = if featureEnabledByDefault then disabled else enabled - - (owner, tid, m : _) <- createTeam OwnDomain 2 - nonTeamMember <- randomUser OwnDomain def - assertForbidden =<< Public.getTeamFeature nonTeamMember tid featureName - checkFeature featureName m tid defaultValue - -- should receive an event - void $ withWebSocket m $ \ws -> do - assertSuccess =<< setFeatureConfig owner tid featureName (object ["status" .= otherStatus]) - do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` otherValue - - checkFeature featureName m tid otherValue - assertSuccess =<< setFeatureConfig owner tid featureName (object ["status" .= defaultStatus]) - do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` defaultValue - checkFeature featureName m tid defaultValue - -testConversationGuestLinks :: (HasCallStack) => App () -testConversationGuestLinks = _testSimpleFlagWithLockStatus "conversationGuestLinks" Public.setTeamFeatureConfig True True - -testFileSharing :: (HasCallStack) => App () -testFileSharing = _testSimpleFlagWithLockStatus "fileSharing" Public.setTeamFeatureConfig True True - -testSndFactorPasswordChallenge :: (HasCallStack) => App () -testSndFactorPasswordChallenge = _testSimpleFlagWithLockStatus "sndFactorPasswordChallenge" Public.setTeamFeatureConfig False False - -testOutlookCalIntegration :: (HasCallStack) => App () -testOutlookCalIntegration = _testSimpleFlagWithLockStatus "outlookCalIntegration" Public.setTeamFeatureConfig False False - -testConversationGuestLinksInternal :: (HasCallStack) => App () -testConversationGuestLinksInternal = _testSimpleFlagWithLockStatus "conversationGuestLinks" Internal.setTeamFeatureConfig True True - -testFileSharingInternal :: (HasCallStack) => App () -testFileSharingInternal = _testSimpleFlagWithLockStatus "fileSharing" Internal.setTeamFeatureConfig True True - -testSndFactorPasswordChallengeInternal :: (HasCallStack) => App () -testSndFactorPasswordChallengeInternal = _testSimpleFlagWithLockStatus "sndFactorPasswordChallenge" Internal.setTeamFeatureConfig False False - -testOutlookCalIntegrationInternal :: (HasCallStack) => App () -testOutlookCalIntegrationInternal = _testSimpleFlagWithLockStatus "outlookCalIntegration" Internal.setTeamFeatureConfig False False - -_testSimpleFlagWithLockStatus :: - (HasCallStack) => - String -> - (Value -> String -> String -> Value -> App Response) -> - Bool -> - Bool -> - App () -_testSimpleFlagWithLockStatus featureName setFeatureConfig featureEnabledByDefault featureUnlockedByDefault = do - -- let defaultStatus = if featureEnabledByDefault then "enabled" else "disabled" - defaultValue <- (if featureEnabledByDefault then enabled else disabled) & setField "lockStatus" (if featureUnlockedByDefault then "unlocked" else "locked") - let thisStatus = if featureEnabledByDefault then "enabled" else "disabled" - let otherStatus = if featureEnabledByDefault then "disabled" else "enabled" - - (owner, tid, m : _) <- createTeam OwnDomain 2 - nonTeamMember <- randomUser OwnDomain def - assertForbidden =<< Public.getTeamFeature nonTeamMember tid featureName - - checkFeature featureName m tid defaultValue - - -- unlock feature if it is locked - unless featureUnlockedByDefault $ Internal.setTeamFeatureLockStatus OwnDomain tid featureName "unlocked" - - -- change the status - let otherValue = if featureEnabledByDefault then disabled else enabled - void $ withWebSocket m $ \ws -> do - assertSuccess =<< setFeatureConfig owner tid featureName (object ["status" .= otherStatus]) - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` otherValue - - checkFeature featureName m tid otherValue - - bindResponse (setFeatureConfig owner tid featureName (object ["status" .= thisStatus])) $ \resp -> do - resp.status `shouldMatchInt` 200 - checkFeature featureName m tid (object ["status" .= thisStatus, "lockStatus" .= "unlocked", "ttl" .= "unlimited"]) - - bindResponse (setFeatureConfig owner tid featureName (object ["status" .= otherStatus])) $ \resp -> do - resp.status `shouldMatchInt` 200 - checkFeature featureName m tid (object ["status" .= otherStatus, "lockStatus" .= "unlocked", "ttl" .= "unlimited"]) - - -- lock feature - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "locked" - - -- feature status should be the default again - checkFeature featureName m tid =<< setField "lockStatus" "locked" defaultValue - assertStatus 409 =<< setFeatureConfig owner tid featureName (object ["status" .= otherStatus]) - - -- unlock again - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "unlocked" - - -- feature status should be the previously set status again - checkFeature featureName m tid =<< setField "lockStatus" "unlocked" otherValue - -testClassifiedDomainsEnabled :: (HasCallStack) => App () -testClassifiedDomainsEnabled = do - (_, tid, m : _) <- createTeam OwnDomain 2 - expected <- enabled & setField "config.domains" ["example.com"] - checkFeature "classifiedDomains" m tid expected - -testClassifiedDomainsDisabled :: (HasCallStack) => App () -testClassifiedDomainsDisabled = do - withModifiedBackend def {galleyCfg = setField "settings.featureFlags.classifiedDomains" (object ["status" .= "disabled", "config" .= object ["domains" .= ["example.com"]]])} $ \domain -> do - (_, tid, m : _) <- createTeam domain 2 - expected <- disabled & setField "config.domains" ["example.com"] - checkFeature "classifiedDomains" m tid expected - -- | Call 'GET /teams/:tid/features' and 'GET /feature-configs', and check if all -- features are there. testAllFeatures :: (HasCallStack) => App () testAllFeatures = do (_, tid, m : _) <- createTeam OwnDomain 2 - let defEnabledObj :: Value -> Value - defEnabledObj conf = object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= conf] - expected = - object - $ [ "legalhold" .= disabled, - "sso" .= disabled, - "searchVisibility" .= disabled, - "validateSAMLemails" .= enabled, - "digitalSignatures" .= disabled, - "appLock" .= defEnabledObj (object ["enforceAppLock" .= False, "inactivityTimeoutSecs" .= A.Number 60]), - "fileSharing" .= enabled, - "classifiedDomains" .= defEnabledObj (object ["domains" .= ["example.com"]]), - "conferenceCalling" .= confCalling def {lockStatus = Just "locked"}, - "selfDeletingMessages" - .= defEnabledObj (object ["enforcedTimeoutSeconds" .= A.Number 0]), - "conversationGuestLinks" .= enabled, - "sndFactorPasswordChallenge" .= disabledLocked, - "mls" - .= object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "proteus", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ], - "searchVisibilityInbound" .= disabled, - "exposeInvitationURLsToTeamAdmin" .= disabledLocked, - "outlookCalIntegration" .= disabledLocked, - "mlsE2EId" - .= object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "verificationExpiration" .= A.Number 86400, - "useProxyOnMobile" .= False, - "crlProxy" .= "https://crlproxy.example.com" - ] - ], - "mlsMigration" - .= object - [ "lockStatus" .= "locked", - "status" .= "enabled", - "ttl" .= "unlimited", - "config" - .= object - [ "startTime" .= "2029-05-16T10:11:12.123Z", - "finaliseRegardlessAfter" .= "2029-10-17T00:00:00Z" - ] - ], - "enforceFileDownloadLocation" .= object ["lockStatus" .= "locked", "status" .= "disabled", "ttl" .= "unlimited", "config" .= object []], - "limitedEventFanout" .= disabled - ] bindResponse (Public.getTeamFeatures m tid) $ \resp -> do resp.status `shouldMatchInt` 200 - expected `shouldMatch` resp.json + defAllFeatures `shouldMatch` resp.json -- This block catches potential errors in the logic that reverts to default if there is a distinction made between -- 1. there is no row for a team_id in galley.team_features @@ -495,17 +58,17 @@ testAllFeatures = do bindResponse (Public.getTeamFeatures m tid) $ \resp -> do resp.status `shouldMatchInt` 200 - expected `shouldMatch` resp.json + defAllFeatures `shouldMatch` resp.json bindResponse (Public.getFeatureConfigs m) $ \resp -> do resp.status `shouldMatchInt` 200 - expected `shouldMatch` resp.json + defAllFeatures `shouldMatch` resp.json randomPersonalUser <- randomUser OwnDomain def bindResponse (Public.getFeatureConfigs randomPersonalUser) $ \resp -> do resp.status `shouldMatchInt` 200 - expected `shouldMatch` resp.json + defAllFeatures `shouldMatch` resp.json testFeatureConfigConsistency :: (HasCallStack) => App () testFeatureConfigConsistency = do @@ -525,678 +88,9 @@ testFeatureConfigConsistency = do (A.Object hm) -> pure (Set.fromList . map (show . A.toText) . KM.keys $ hm) x -> assertFailure ("JSON was not an object, but " <> show x) -testSelfDeletingMessages :: (HasCallStack) => App () -testSelfDeletingMessages = - _testLockStatusWithConfig - "selfDeletingMessages" - Public.setTeamFeatureConfig - (object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0]]) - (object ["status" .= "disabled", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0]]) - (object ["status" .= "enabled", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 30]]) - (object ["status" .= "enabled", "config" .= object ["enforcedTimeoutSeconds" .= ""]]) - -testSelfDeletingMessagesInternal :: (HasCallStack) => App () -testSelfDeletingMessagesInternal = - _testLockStatusWithConfig - "selfDeletingMessages" - Internal.setTeamFeatureConfig - (object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0]]) - (object ["status" .= "disabled", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0]]) - (object ["status" .= "enabled", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 30]]) - (object ["status" .= "enabled", "config" .= object ["enforcedTimeoutSeconds" .= ""]]) - -testMls :: (HasCallStack) => App () -testMls = do - user <- randomUser OwnDomain def - uid <- asString $ user %. "id" - _testLockStatusWithConfig - "mls" - Public.setTeamFeatureConfig - mlsDefaultConfig - (mlsConfig1 uid) - mlsConfig2 - mlsInvalidConfig - -testMlsInternal :: (HasCallStack) => App () -testMlsInternal = do - user <- randomUser OwnDomain def - uid <- asString $ user %. "id" - _testLockStatusWithConfig - "mls" - Internal.setTeamFeatureConfig - mlsDefaultConfig - (mlsConfig1 uid) - mlsConfig2 - mlsInvalidConfig - -mlsDefaultConfig :: Value -mlsDefaultConfig = - object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "proteus", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - -mlsConfig1 :: String -> Value -mlsConfig1 uid = - object - [ "status" .= "enabled", - "config" - .= object - [ "protocolToggleUsers" .= [uid], - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - -mlsConfig2 :: Value -mlsConfig2 = - object - [ "status" .= "enabled", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - -mlsInvalidConfig :: Value -mlsInvalidConfig = - object - [ "status" .= "enabled", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["proteus"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - -testEnforceDownloadLocation :: (HasCallStack) => App () -testEnforceDownloadLocation = - _testLockStatusWithConfig - "enforceFileDownloadLocation" - Public.setTeamFeatureConfig - (object ["lockStatus" .= "locked", "status" .= "disabled", "ttl" .= "unlimited", "config" .= object []]) - (object ["status" .= "enabled", "config" .= object ["enforcedDownloadLocation" .= "/tmp"]]) - (object ["status" .= "disabled", "config" .= object []]) - (object ["status" .= "enabled", "config" .= object ["enforcedDownloadLocation" .= object []]]) - -testEnforceDownloadLocationInternal :: (HasCallStack) => App () -testEnforceDownloadLocationInternal = - _testLockStatusWithConfig - "enforceFileDownloadLocation" - Internal.setTeamFeatureConfig - (object ["lockStatus" .= "locked", "status" .= "disabled", "ttl" .= "unlimited", "config" .= object []]) - (object ["status" .= "enabled", "config" .= object ["enforcedDownloadLocation" .= "/tmp"]]) - (object ["status" .= "disabled", "config" .= object []]) - (object ["status" .= "enabled", "config" .= object ["enforcedDownloadLocation" .= object []]]) - -testMlsMigration :: (HasCallStack) => App () -testMlsMigration = do - -- first we have to enable mls - (owner, tid, m : _) <- createTeam OwnDomain 2 - assertSuccess =<< Public.setTeamFeatureConfig owner tid "mls" mlsEnableConfig - _testLockStatusWithConfigWithTeam - (owner, tid, m) - "mlsMigration" - Public.setTeamFeatureConfig - mlsMigrationDefaultConfig - mlsMigrationConfig1 - mlsMigrationConfig2 - mlsMigrationInvalidConfig - -testMlsMigrationInternal :: (HasCallStack) => App () -testMlsMigrationInternal = do - -- first we have to enable mls - (owner, tid, m : _) <- createTeam OwnDomain 2 - assertSuccess =<< Public.setTeamFeatureConfig owner tid "mls" mlsEnableConfig - _testLockStatusWithConfigWithTeam - (owner, tid, m) - "mlsMigration" - Internal.setTeamFeatureConfig - mlsMigrationDefaultConfig - mlsMigrationConfig1 - mlsMigrationConfig2 - mlsMigrationInvalidConfig - -mlsEnableConfig :: Value -mlsEnableConfig = - object - [ "status" .= "enabled", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - -mlsMigrationDefaultConfig :: Value -mlsMigrationDefaultConfig = - object - [ "lockStatus" .= "locked", - "status" .= "enabled", - "ttl" .= "unlimited", - "config" - .= object - [ "startTime" .= "2029-05-16T10:11:12.123Z", - "finaliseRegardlessAfter" .= "2029-10-17T00:00:00Z" - ] - ] - -mlsMigrationConfig1 :: Value -mlsMigrationConfig1 = - object - [ "status" .= "enabled", - "config" - .= object - [ "startTime" .= "2029-05-16T10:11:12.123Z", - "finaliseRegardlessAfter" .= "2030-10-17T00:00:00Z" - ] - ] - -mlsMigrationConfig2 :: Value -mlsMigrationConfig2 = - object - [ "status" .= "enabled", - "config" - .= object - [ "startTime" .= "2030-05-16T10:11:12.123Z", - "finaliseRegardlessAfter" .= "2031-10-17T00:00:00Z" - ] - ] - -mlsMigrationInvalidConfig :: Value -mlsMigrationInvalidConfig = - object - [ "status" .= "enabled", - "config" - .= object - [ "startTime" .= A.Number 1 - ] - ] - -mlsE2EIdConfig :: App (Value, Value, Value, Value) -mlsE2EIdConfig = do - cfg2 <- - mlsE2EIdConfig1 - & setField "config.verificationExpiration" (A.Number 86401) - & setField "config.useProxyOnMobile" True - invalidConfig <- cfg2 & removeField "config.crlProxy" - pure (mlsE2EIdDefConfig, mlsE2EIdConfig1, cfg2, invalidConfig) - where - mlsE2EIdDefConfig :: Value - mlsE2EIdDefConfig = - object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "verificationExpiration" .= A.Number 86400, - "useProxyOnMobile" .= False, - "crlProxy" .= "https://crlproxy.example.com" - ] - ] - mlsE2EIdConfig1 :: Value - mlsE2EIdConfig1 = - object - [ "status" .= "enabled", - "config" - .= object - [ "crlProxy" .= "https://example.com", - "verificationExpiration" .= A.Number 86400, - "useProxyOnMobile" .= False - ] - ] - -testMLSE2EId :: (HasCallStack) => App () -testMLSE2EId = do - (defCfg, cfg1, cfg2, invalidCfg) <- mlsE2EIdConfig - _testLockStatusWithConfig - "mlsE2EId" - Public.setTeamFeatureConfig - defCfg - cfg1 - cfg2 - invalidCfg - -testMLSE2EIdInternal :: (HasCallStack) => App () -testMLSE2EIdInternal = do - (defCfg, cfg1, cfg2, invalidCfg) <- mlsE2EIdConfig - -- the internal API is not as strict as the public one, so we need to tweak the invalid config some more - invalidCfg' <- invalidCfg & setField "config.crlProxy" (object []) - _testLockStatusWithConfig - "mlsE2EId" - Internal.setTeamFeatureConfig - defCfg - cfg1 - cfg2 - invalidCfg' - -testConferenceCalling :: (HasCallStack) => App () -testConferenceCalling = do - _testLockStatusWithConfig - "conferenceCalling" - Public.setTeamFeatureConfig - (confCalling def {lockStatus = Just "locked"}) - (confCalling def {sft = toJSON True}) - (confCalling def) - (confCalling def {sft = toJSON (0 :: Int)}) - -testConferenceCallingInternal :: (HasCallStack) => App () -testConferenceCallingInternal = do - let defaultArgs = def {lockStatus = Just "locked"} - - (owner, tid, m : _) <- createTeam OwnDomain 2 - nonTeamMember <- randomUser OwnDomain def - assertForbidden =<< Public.getTeamFeature nonTeamMember tid "conferenceCalling" - checkFeature "conferenceCalling" m tid (confCalling defaultArgs) - - void $ withWebSocket m $ \ws -> do - -- unlock and enable - assertSuccess =<< Internal.patchTeamFeatureConfig owner tid "conferenceCalling" (object ["status" .= "enabled", "lockStatus" .= "unlocked"]) - do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` "conferenceCalling" - notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs {status = "enabled", lockStatus = Just "unlocked"}) - checkFeature "conferenceCalling" m tid (confCalling defaultArgs {status = "enabled", lockStatus = Just "unlocked"}) - - -- just disable - assertSuccess =<< Internal.setTeamFeatureConfig owner tid "conferenceCalling" (confCalling def {status = "disabled"}) - do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` "conferenceCalling" - notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs {status = "disabled", lockStatus = Just "unlocked"}) - checkFeature "conferenceCalling" m tid (confCalling defaultArgs {lockStatus = Just "unlocked"}) - - -- re-enable - assertSuccess =<< Internal.setTeamFeatureConfig owner tid "conferenceCalling" (confCalling def {status = "enabled"}) - do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` "conferenceCalling" - notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs {status = "enabled", lockStatus = Just "unlocked"}) - checkFeature "conferenceCalling" m tid (confCalling defaultArgs {status = "enabled", lockStatus = Just "unlocked"}) - - -- restore initial state - assertSuccess =<< Internal.patchTeamFeatureConfig owner tid "conferenceCalling" (object ["status" .= "disabled", "lockStatus" .= "locked"]) - do - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` "conferenceCalling" - notif %. "payload.0.data" `shouldMatch` (confCalling defaultArgs) - checkFeature "conferenceCalling" m tid (confCalling defaultArgs) - -_testLockStatusWithConfig :: - (HasCallStack) => - String -> - (Value -> String -> String -> Value -> App Response) -> - -- | the default feature config (should include the lock status and ttl, as it is returned by the API) - Value -> - -- | a valid config used to update the feature setting (should not include the lock status and ttl, as these are not part of the request payload) - Value -> - -- | another valid config - Value -> - -- | an invalid config - Value -> - App () -_testLockStatusWithConfig featureName setTeamFeatureConfig defaultFeatureConfig config1 config2 invalidConfig = do - (owner, tid, m : _) <- createTeam OwnDomain 2 - _testLockStatusWithConfigWithTeam (owner, tid, m) featureName setTeamFeatureConfig defaultFeatureConfig config1 config2 invalidConfig - -_testLockStatusWithConfigWithTeam :: - (HasCallStack) => - -- | (owner, tid, member) - (Value, String, Value) -> - String -> - (Value -> String -> String -> Value -> App Response) -> - -- | the default feature config (should include the lock status and ttl, as it is returned by the API) - Value -> - -- | a valid config used to update the feature setting (should not include the lock status and ttl, as these are not part of the request payload) - Value -> - -- | another valid config - Value -> - -- | an invalid config - Value -> - App () -_testLockStatusWithConfigWithTeam (owner, tid, m) featureName setTeamFeatureConfig defaultFeatureConfig config1 config2 invalidConfig = do - -- personal user - randomPersonalUser <- randomUser OwnDomain def - - bindResponse (Public.getFeatureConfigs randomPersonalUser) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. featureName `shouldMatch` defaultFeatureConfig - - -- team user - nonTeamMember <- randomUser OwnDomain def - assertForbidden =<< Public.getTeamFeature nonTeamMember tid featureName - - checkFeature featureName m tid defaultFeatureConfig - - -- lock the feature - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "locked" - bindResponse (Public.getTeamFeature owner tid featureName) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "lockStatus" `shouldMatch` "locked" - - assertStatus 409 =<< setTeamFeatureConfig owner tid featureName config1 - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "unlocked" - - void $ withWebSocket m $ \ws -> do - assertSuccess =<< setTeamFeatureConfig owner tid featureName config1 - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` (config1 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") - - checkFeature featureName m tid =<< (config1 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") - - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "locked" - checkFeature featureName m tid =<< setField "lockStatus" "locked" defaultFeatureConfig - Internal.setTeamFeatureLockStatus OwnDomain tid featureName "unlocked" - - void $ withWebSocket m $ \ws -> do - assertStatus 400 =<< setTeamFeatureConfig owner tid featureName invalidConfig - assertNoEvent 2 ws - - checkFeature featureName m tid =<< (config1 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") - - void $ withWebSocket m $ \ws -> do - assertSuccess =<< setTeamFeatureConfig owner tid featureName config2 - notif <- awaitMatch isFeatureConfigUpdateNotif ws - notif %. "payload.0.name" `shouldMatch` featureName - notif %. "payload.0.data" `shouldMatch` (config2 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") - - checkFeature featureName m tid =<< (config2 & setField "lockStatus" "unlocked" & setField "ttl" "unlimited") - -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"]] - --------------------------------------------------------------------------------- --- Simple flags with implicit lock status - -testPatchSearchVisibility :: (HasCallStack) => App () -testPatchSearchVisibility = _testPatch "searchVisibility" False disabled enabled - -testPatchValidateSAMLEmails :: (HasCallStack) => App () -testPatchValidateSAMLEmails = _testPatch "validateSAMLemails" False enabled disabled - -testPatchDigitalSignatures :: (HasCallStack) => App () -testPatchDigitalSignatures = _testPatch "digitalSignatures" False disabled enabled - --------------------------------------------------------------------------------- --- Simple flags with explicit lock status - -testPatchFileSharing :: (HasCallStack) => App () -testPatchFileSharing = _testPatch "fileSharing" True enabled disabled - -testPatchGuestLinks :: (HasCallStack) => App () -testPatchGuestLinks = _testPatch "conversationGuestLinks" True enabled disabled - -testPatchSndFactorPasswordChallenge :: (HasCallStack) => App () -testPatchSndFactorPasswordChallenge = _testPatch "sndFactorPasswordChallenge" True disabledLocked enabled - -testPatchOutlookCalIntegration :: (HasCallStack) => App () -testPatchOutlookCalIntegration = _testPatch "outlookCalIntegration" True disabledLocked enabled - --------------------------------------------------------------------------------- --- Flags with config & implicit lock status - -testPatchAppLock :: (HasCallStack) => App () -testPatchAppLock = do - let defCfg = - object - [ "lockStatus" .= "unlocked", - "status" .= "enabled", - "ttl" .= "unlimited", - "config" .= object ["enforceAppLock" .= False, "inactivityTimeoutSecs" .= A.Number 60] - ] - _testPatch "appLock" False defCfg (object ["lockStatus" .= "locked"]) - _testPatch "appLock" False defCfg (object ["status" .= "disabled"]) - _testPatch "appLock" False defCfg (object ["lockStatus" .= "locked", "status" .= "disabled"]) - _testPatch "appLock" False defCfg (object ["lockStatus" .= "unlocked", "config" .= object ["enforceAppLock" .= True, "inactivityTimeoutSecs" .= A.Number 120]]) - _testPatch "appLock" False defCfg (object ["config" .= object ["enforceAppLock" .= True, "inactivityTimeoutSecs" .= A.Number 240]]) - --------------------------------------------------------------------------------- --- Flags with config & explicit lock status - -testPatchConferenceCalling :: (HasCallStack) => App () -testPatchConferenceCalling = do - let defCfg = confCalling def {lockStatus = Just "locked"} - _testPatch "conferenceCalling" True defCfg (object ["lockStatus" .= "locked"]) - _testPatch "conferenceCalling" True defCfg (object ["status" .= "disabled"]) - _testPatch "conferenceCalling" True defCfg (object ["lockStatus" .= "locked", "status" .= "disabled"]) - _testPatch "conferenceCalling" True defCfg (object ["lockStatus" .= "unlocked", "config" .= object ["useSFTForOneToOneCalls" .= toJSON True]]) - -testPatchSelfDeletingMessages :: (HasCallStack) => App () -testPatchSelfDeletingMessages = do - let defCfg = - object - [ "lockStatus" .= "unlocked", - "status" .= "enabled", - "ttl" .= "unlimited", - "config" .= object ["enforcedTimeoutSeconds" .= A.Number 0] - ] - _testPatch "selfDeletingMessages" True defCfg (object ["lockStatus" .= "locked"]) - _testPatch "selfDeletingMessages" True defCfg (object ["status" .= "disabled"]) - _testPatch "selfDeletingMessages" True defCfg (object ["lockStatus" .= "locked", "status" .= "disabled"]) - _testPatch "selfDeletingMessages" True defCfg (object ["lockStatus" .= "unlocked", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 30]]) - _testPatch "selfDeletingMessages" True defCfg (object ["config" .= object ["enforcedTimeoutSeconds" .= A.Number 60]]) - -testPatchEnforceFileDownloadLocation :: (HasCallStack) => App () -testPatchEnforceFileDownloadLocation = do - let defCfg = - object - [ "lockStatus" .= "locked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" .= object [] - ] - _testPatch "enforceFileDownloadLocation" True defCfg (object ["lockStatus" .= "unlocked"]) - _testPatch "enforceFileDownloadLocation" True defCfg (object ["status" .= "enabled"]) - _testPatch "enforceFileDownloadLocation" True defCfg (object ["lockStatus" .= "unlocked", "status" .= "enabled"]) - _testPatch "enforceFileDownloadLocation" True defCfg (object ["lockStatus" .= "locked", "config" .= object []]) - _testPatch "enforceFileDownloadLocation" True defCfg (object ["config" .= object ["enforcedDownloadLocation" .= "/tmp"]]) - -testPatchE2EId :: (HasCallStack) => App () -testPatchE2EId = do - let defCfg = - object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "verificationExpiration" .= A.Number 86400, - "useProxyOnMobile" .= False, - "crlProxy" .= "https://crlproxy.example.com" - ] - ] - _testPatch "mlsE2EId" True defCfg (object ["lockStatus" .= "locked"]) - _testPatch "mlsE2EId" True defCfg (object ["status" .= "enabled"]) - _testPatch "mlsE2EId" True defCfg (object ["lockStatus" .= "locked", "status" .= "enabled"]) - _testPatch - "mlsE2EId" - True - defCfg - ( object - [ "lockStatus" .= "unlocked", - "config" - .= object - [ "crlProxy" .= "https://example.com", - "verificationExpiration" .= A.Number 86401, - "useProxyOnMobile" .= True - ] - ] - ) - _testPatch - "mlsE2EId" - True - defCfg - ( object - [ "config" - .= object - [ "crlProxy" .= "https://example.com", - "verificationExpiration" .= A.Number 86401, - "useProxyOnMobile" .= True - ] - ] - ) - -testPatchMLS :: (HasCallStack) => App () -testPatchMLS = do - dom <- asString OwnDomain - (_, tid, _) <- createTeam dom 0 - assertSuccess - =<< Internal.patchTeamFeature - dom - tid - "mlsMigration" - (object ["status" .= "disabled", "lockStatus" .= "unlocked"]) - let defCfg = - object - [ "lockStatus" .= "unlocked", - "status" .= "disabled", - "ttl" .= "unlimited", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "proteus", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - _testPatchWithSetup mlsMigrationSetup dom "mls" True defCfg (object ["lockStatus" .= "locked"]) - _testPatchWithSetup mlsMigrationSetup dom "mls" True defCfg (object ["status" .= "enabled"]) - _testPatchWithSetup mlsMigrationSetup dom "mls" True defCfg (object ["lockStatus" .= "locked", "status" .= "enabled"]) - _testPatchWithSetup - mlsMigrationSetup - dom - "mls" - True - defCfg - ( object - [ "status" .= "enabled", - "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - ) - _testPatchWithSetup - mlsMigrationSetup - dom - "mls" - True - defCfg - ( object - [ "config" - .= object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "mls", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= A.Number 1 - ] - ] - ) - where - mlsMigrationSetup :: (HasCallStack) => String -> String -> App () - mlsMigrationSetup dom tid = - assertSuccess - =<< Internal.patchTeamFeature - dom - tid - "mlsMigration" - (object ["status" .= "disabled", "lockStatus" .= "unlocked"]) - -_testPatch :: (HasCallStack) => String -> Bool -> Value -> Value -> App () -_testPatch featureName hasExplicitLockStatus defaultFeatureConfig patch = do - dom <- asString OwnDomain - _testPatchWithSetup - (\_ _ -> pure ()) - dom - featureName - hasExplicitLockStatus - defaultFeatureConfig - patch - -_testPatchWithSetup :: - (HasCallStack) => - (String -> String -> App ()) -> - String -> - String -> - Bool -> - Value -> - Value -> - App () -_testPatchWithSetup setup domain featureName hasExplicitLockStatus defaultFeatureConfig patch = do - (owner, tid, _) <- createTeam domain 0 - -- run a feature-specific setup. For most features this is a no-op. - setup domain tid - - checkFeature featureName owner tid defaultFeatureConfig - assertSuccess =<< Internal.patchTeamFeature domain tid featureName patch - patched <- (.json) =<< Internal.getTeamFeature domain tid featureName - checkFeature featureName owner tid patched - lockStatus <- patched %. "lockStatus" >>= asString - if lockStatus == "locked" - then do - -- if lock status is locked the feature status should fall back to the default - patched `shouldMatch` (defaultFeatureConfig & setField "lockStatus" "locked") - -- if lock status is locked, it was either locked before or changed by the patch - mPatchedLockStatus <- lookupField patch "lockStatus" - case mPatchedLockStatus of - Just ls -> ls `shouldMatch` "locked" - Nothing -> defaultFeatureConfig %. "lockStatus" `shouldMatch` "locked" - else do - patched %. "status" `shouldMatch` valueOrDefault "status" - mPatchedConfig <- lookupField patched "config" - case mPatchedConfig of - Just patchedConfig -> patchedConfig `shouldMatch` valueOrDefault "config" - Nothing -> do - mDefConfig <- lookupField defaultFeatureConfig "config" - assertBool "patch had an unexpected config field" (isNothing mDefConfig) - - when hasExplicitLockStatus $ do - -- if lock status is unlocked, it was either unlocked before or changed by the patch - mPatchedLockStatus <- lookupField patch "lockStatus" - case mPatchedLockStatus of - Just ls -> ls `shouldMatch` "unlocked" - Nothing -> defaultFeatureConfig %. "lockStatus" `shouldMatch` "unlocked" - where - valueOrDefault :: String -> App Value - valueOrDefault key = do - mValue <- lookupField patch key - maybe (defaultFeatureConfig %. key) pure mValue +testNonMemberAccess :: (HasCallStack) => Feature -> App () +testNonMemberAccess (Feature featureName) = do + (_, tid, _) <- createTeam OwnDomain 0 + nonMember <- randomUser OwnDomain def + Public.getTeamFeature nonMember tid featureName + >>= assertForbidden diff --git a/integration/test/Test/FeatureFlags/AppLock.hs b/integration/test/Test/FeatureFlags/AppLock.hs new file mode 100644 index 00000000000..f031403a98d --- /dev/null +++ b/integration/test/Test/FeatureFlags/AppLock.hs @@ -0,0 +1,31 @@ +module Test.FeatureFlags.AppLock where + +import qualified Data.Aeson as A +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchAppLock :: (HasCallStack) => App () +testPatchAppLock = do + checkPatch OwnDomain "appLock" + $ object ["lockStatus" .= "locked"] + checkPatch OwnDomain "appLock" + $ object ["status" .= "disabled"] + checkPatch OwnDomain "appLock" + $ object ["lockStatus" .= "locked", "status" .= "disabled"] + checkPatch OwnDomain "appLock" + $ object + [ "lockStatus" .= "unlocked", + "config" + .= object + [ "enforceAppLock" .= True, + "inactivityTimeoutSecs" .= A.Number 120 + ] + ] + checkPatch OwnDomain "appLock" + $ object + [ "config" + .= object + [ "enforceAppLock" .= True, + "inactivityTimeoutSecs" .= A.Number 240 + ] + ] diff --git a/integration/test/Test/FeatureFlags/ClassifiedDomains.hs b/integration/test/Test/FeatureFlags/ClassifiedDomains.hs new file mode 100644 index 00000000000..4fe8043f70b --- /dev/null +++ b/integration/test/Test/FeatureFlags/ClassifiedDomains.hs @@ -0,0 +1,18 @@ +module Test.FeatureFlags.ClassifiedDomains where + +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testClassifiedDomainsEnabled :: (HasCallStack) => App () +testClassifiedDomainsEnabled = do + (_, tid, m : _) <- createTeam OwnDomain 2 + expected <- enabled & setField "config.domains" ["example.com"] + checkFeature "classifiedDomains" m tid expected + +testClassifiedDomainsDisabled :: (HasCallStack) => App () +testClassifiedDomainsDisabled = do + withModifiedBackend def {galleyCfg = setField "settings.featureFlags.classifiedDomains" (object ["status" .= "disabled", "config" .= object ["domains" .= ["example.com"]]])} $ \domain -> do + (_, tid, m : _) <- createTeam domain 2 + expected <- disabled & setField "config.domains" ["example.com"] + checkFeature "classifiedDomains" m tid expected diff --git a/integration/test/Test/FeatureFlags/ConferenceCalling.hs b/integration/test/Test/FeatureFlags/ConferenceCalling.hs new file mode 100644 index 00000000000..30cc5621bcc --- /dev/null +++ b/integration/test/Test/FeatureFlags/ConferenceCalling.hs @@ -0,0 +1,26 @@ +module Test.FeatureFlags.ConferenceCalling where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchConferenceCalling :: (HasCallStack) => App () +testPatchConferenceCalling = do + checkPatch OwnDomain "conferenceCalling" + $ object ["lockStatus" .= "locked"] + checkPatch OwnDomain "conferenceCalling" + $ object ["status" .= "disabled"] + checkPatch OwnDomain "conferenceCalling" + $ object ["lockStatus" .= "locked", "status" .= "disabled"] + checkPatch OwnDomain "conferenceCalling" + $ object + [ "lockStatus" .= "unlocked", + "config" .= object ["useSFTForOneToOneCalls" .= toJSON True] + ] + +testConferenceCalling :: (HasCallStack) => APIAccess -> App () +testConferenceCalling access = do + runFeatureTests OwnDomain access + $ mkFeatureTests "conferenceCalling" + & addUpdate (confCalling def {sft = toJSON True}) + & addUpdate (confCalling def {sft = toJSON False}) + & addInvalidUpdate (confCalling def {sft = toJSON (0 :: Int)}) diff --git a/integration/test/Test/FeatureFlags/DigitalSignatures.hs b/integration/test/Test/FeatureFlags/DigitalSignatures.hs new file mode 100644 index 00000000000..0a00bc33926 --- /dev/null +++ b/integration/test/Test/FeatureFlags/DigitalSignatures.hs @@ -0,0 +1,15 @@ +module Test.FeatureFlags.DigitalSignatures where + +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchDigitalSignatures :: (HasCallStack) => App () +testPatchDigitalSignatures = checkPatch OwnDomain "digitalSignatures" enabled + +testDigitalSignaturesInternal :: (HasCallStack) => App () +testDigitalSignaturesInternal = do + (alice, tid, _) <- createTeam OwnDomain 0 + withWebSocket alice $ \ws -> do + setFlag InternalAPI ws tid "digitalSignatures" disabled + setFlag InternalAPI ws tid "digitalSignatures" enabled diff --git a/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs b/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs new file mode 100644 index 00000000000..9bb1a608b4c --- /dev/null +++ b/integration/test/Test/FeatureFlags/EnforceFileDownloadLocation.hs @@ -0,0 +1,55 @@ +module Test.FeatureFlags.EnforceFileDownloadLocation where + +import qualified API.GalleyInternal as Internal +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchEnforceFileDownloadLocation :: (HasCallStack) => App () +testPatchEnforceFileDownloadLocation = do + checkPatch OwnDomain "enforceFileDownloadLocation" + $ object ["lockStatus" .= "unlocked"] + checkPatch OwnDomain "enforceFileDownloadLocation" + $ object ["status" .= "enabled"] + checkPatch OwnDomain "enforceFileDownloadLocation" + $ object ["lockStatus" .= "unlocked", "status" .= "enabled"] + checkPatch OwnDomain "enforceFileDownloadLocation" + $ object ["lockStatus" .= "locked", "config" .= object []] + checkPatch OwnDomain "enforceFileDownloadLocation" + $ object ["config" .= object ["enforcedDownloadLocation" .= "/tmp"]] + + do + (user, tid, _) <- createTeam OwnDomain 0 + bindResponse + ( Internal.patchTeamFeature + user + tid + "enforceFileDownloadLocation" + (object ["config" .= object ["enforcedDownloadLocation" .= ""]]) + ) + $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "empty-download-location" + +testEnforceDownloadLocation :: (HasCallStack) => APIAccess -> App () +testEnforceDownloadLocation access = do + mkFeatureTests + "enforceFileDownloadLocation" + & addUpdate + ( object + [ "status" .= "enabled", + "config" .= object ["enforcedDownloadLocation" .= "/tmp"] + ] + ) + & addUpdate + (object ["status" .= "disabled", "config" .= object []]) + & addInvalidUpdate + ( object + [ "status" .= "enabled", + "config" + .= object + [ "enforcedDownloadLocation" .= object [] + ] + ] + ) + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/FileSharing.hs b/integration/test/Test/FeatureFlags/FileSharing.hs new file mode 100644 index 00000000000..7cc761e64ef --- /dev/null +++ b/integration/test/Test/FeatureFlags/FileSharing.hs @@ -0,0 +1,14 @@ +module Test.FeatureFlags.FileSharing where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchFileSharing :: (HasCallStack) => App () +testPatchFileSharing = checkPatch OwnDomain "fileSharing" disabled + +testFileSharing :: (HasCallStack) => APIAccess -> App () +testFileSharing access = + mkFeatureTests "fileSharing" + & addUpdate disabled + & addUpdate enabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/GuestLinks.hs b/integration/test/Test/FeatureFlags/GuestLinks.hs new file mode 100644 index 00000000000..0c0c84ae387 --- /dev/null +++ b/integration/test/Test/FeatureFlags/GuestLinks.hs @@ -0,0 +1,14 @@ +module Test.FeatureFlags.GuestLinks where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testConversationGuestLinks :: (HasCallStack) => APIAccess -> App () +testConversationGuestLinks access = + mkFeatureTests "conversationGuestLinks" + & addUpdate disabled + & addUpdate enabled + & runFeatureTests OwnDomain access + +testPatchGuestLinks :: (HasCallStack) => App () +testPatchGuestLinks = checkPatch OwnDomain "conversationGuestLinks" disabled diff --git a/integration/test/Test/FeatureFlags/LegalHold.hs b/integration/test/Test/FeatureFlags/LegalHold.hs new file mode 100644 index 00000000000..55743ec4f91 --- /dev/null +++ b/integration/test/Test/FeatureFlags/LegalHold.hs @@ -0,0 +1,142 @@ +module Test.FeatureFlags.LegalHold where + +import qualified API.Galley as Public +import qualified API.GalleyInternal as Internal +import Control.Monad.Codensity (Codensity (runCodensity)) +import Control.Monad.Reader +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude +import Testlib.ResourcePool (acquireResources) + +testLegalholdDisabledByDefault :: (HasCallStack) => App () +testLegalholdDisabledByDefault = do + let put uid tid st = Internal.setTeamFeatureConfig uid tid "legalhold" (object ["status" .= st]) >>= assertSuccess + let patch uid tid st = Internal.setTeamFeatureStatus uid tid "legalhold" st >>= assertSuccess + forM_ [put, patch] $ \setFeatureStatus -> do + withModifiedBackend + def {galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default"} + $ \domain -> do + (owner, tid, m : _) <- createTeam domain 2 + nonMember <- randomUser domain def + assertForbidden =<< Public.getTeamFeature nonMember tid "legalhold" + -- Test default + checkFeature "legalhold" m tid disabled + -- Test override + setFeatureStatus owner tid "enabled" + checkFeature "legalhold" owner tid enabled + setFeatureStatus owner tid "disabled" + checkFeature "legalhold" owner tid disabled + +-- always disabled +testLegalholdDisabledPermanently :: (HasCallStack) => App () +testLegalholdDisabledPermanently = do + let cfgLhDisabledPermanently = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-permanently" + } + cfgLhDisabledByDefault = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default" + } + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + + -- Happy case: DB has no config for the team + runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "legalhold" owner tid disabled + assertStatus 403 =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" + assertStatus 403 =<< Internal.setTeamFeatureConfig domain tid "legalhold" (object ["status" .= "enabled"]) + + -- Interesting case: The team had LH enabled before backend config was + -- changed to disabled-permanently + (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "legalhold" owner tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" + checkFeature "legalhold" owner tid enabled + pure (owner, tid) + + runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do + checkFeature "legalhold" owner tid disabled + +-- enabled if team is allow listed, disabled in any other case +testLegalholdWhitelistTeamsAndImplicitConsent :: (HasCallStack) => App () +testLegalholdWhitelistTeamsAndImplicitConsent = do + let cfgLhWhitelistTeamsAndImplicitConsent = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "whitelist-teams-and-implicit-consent" + } + cfgLhDisabledByDefault = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default" + } + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + + -- Happy case: DB has no config for the team + (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "legalhold" owner tid disabled + Internal.legalholdWhitelistTeam tid owner >>= assertSuccess + checkFeature "legalhold" owner tid enabled + + -- Disabling it doesn't work + assertStatus 403 =<< Internal.setTeamFeatureStatus domain tid "legalhold" "disabled" + assertStatus 403 =<< Internal.setTeamFeatureConfig domain tid "legalhold" (object ["status" .= "disabled"]) + checkFeature "legalhold" owner tid enabled + pure (owner, tid) + + -- Interesting case: The team had LH disabled before backend config was + -- changed to "whitelist-teams-and-implicit-consent". It should still show + -- enabled when the config gets changed. + runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do + checkFeature "legalhold" owner tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "legalhold" "disabled" + checkFeature "legalhold" owner tid disabled + + runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do + checkFeature "legalhold" owner tid enabled + +testExposeInvitationURLsToTeamAdminConfig :: (HasCallStack) => App () +testExposeInvitationURLsToTeamAdminConfig = do + let cfgExposeInvitationURLsTeamAllowlist tids = + def + { galleyCfg = setField "settings.exposeInvitationURLsTeamAllowlist" tids + } + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + + let 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 + -- a team that is not in the allow list cannot enable the feature, it will always be disabled and locked + -- even though the internal API request to enable it succeeds + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabledLocked + -- however, a request to the public API will fail + assertStatus 409 =<< Public.setTeamFeatureConfig owner tid "exposeInvitationURLsToTeamAdmin" (object ["status" .= "enabled"]) + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "disabled" + pure (owner, tid) + + -- Happy case: DB has no config for the team + (owner, tid) <- testNoAllowlistEntry + + -- Interesting case: The team is in the allow list + runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist [tid]) $ \_ -> do + -- when the team is in the allow list the lock status is implicitly unlocked + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid enabled + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "disabled" + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid enabled + + -- Interesting case: The team had the feature enabled but is not in allow list + void testNoAllowlistEntry diff --git a/integration/test/Test/FeatureFlags/Mls.hs b/integration/test/Test/FeatureFlags/Mls.hs new file mode 100644 index 00000000000..40c3783fec0 --- /dev/null +++ b/integration/test/Test/FeatureFlags/Mls.hs @@ -0,0 +1,112 @@ +module Test.FeatureFlags.Mls where + +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testMls :: (HasCallStack) => APIAccess -> App () +testMls access = + do + user <- randomUser OwnDomain def + uid <- asString $ user %. "id" + mkFeatureTests "mls" + & addUpdate (mls1 uid) + & addUpdate mls2 + & addInvalidUpdate mlsInvalidConfig + & runFeatureTests OwnDomain access + +testMlsPatch :: (HasCallStack) => App () +testMlsPatch = do + mlsMigrationDefaultConfig <- defAllFeatures %. "mlsMigration.config" + withModifiedBackend + def + { galleyCfg = + setField + "settings.featureFlags.mlsMigration.defaults" + ( object + [ "lockStatus" .= "locked", + "status" .= "disabled", + "config" .= mlsMigrationDefaultConfig + ] + ) + } + $ \domain -> do + checkPatch domain "mls" $ object ["lockStatus" .= "locked"] + checkPatch domain "mls" $ object ["status" .= "enabled"] + checkPatch domain "mls" + $ object ["lockStatus" .= "locked", "status" .= "enabled"] + checkPatch domain "mls" + $ object + [ "status" .= "enabled", + "config" + .= object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["proteus", "mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= toJSON (1 :: Int) + ] + ] + checkPatch domain "mls" + $ object + [ "config" + .= object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["proteus", "mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= toJSON (1 :: Int) + ] + ] + +mlsDefaultConfig :: Value +mlsDefaultConfig = + object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "proteus", + "supportedProtocols" .= ["proteus", "mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= toJSON (1 :: Int) + ] + +mls1 :: String -> Value +mls1 uid = + object + [ "status" .= "enabled", + "config" + .= object + [ "protocolToggleUsers" .= [uid], + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["proteus", "mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= toJSON (1 :: Int) + ] + ] + +mls2 :: Value +mls2 = + object + [ "status" .= "enabled", + "config" + .= object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= toJSON (1 :: Int) + ] + ] + +mlsInvalidConfig :: Value +mlsInvalidConfig = + object + [ "status" .= "enabled", + "config" + .= object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["proteus"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= toJSON (1 :: Int) + ] + ] diff --git a/integration/test/Test/FeatureFlags/MlsE2EId.hs b/integration/test/Test/FeatureFlags/MlsE2EId.hs new file mode 100644 index 00000000000..dee32f94be2 --- /dev/null +++ b/integration/test/Test/FeatureFlags/MlsE2EId.hs @@ -0,0 +1,121 @@ +module Test.FeatureFlags.MlsE2EId where + +import qualified API.Galley as Public +import qualified Data.Aeson as A +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +mlsE2EId1 :: Value +mlsE2EId1 = + object + [ "status" .= "enabled", + "config" + .= object + [ "crlProxy" .= "https://example.com", + "verificationExpiration" .= A.Number 86400, + "useProxyOnMobile" .= False + ] + ] + +testMLSE2EId :: (HasCallStack) => APIAccess -> App () +testMLSE2EId access = do + invalid <- + mlsE2EId1 + & if (access == InternalAPI) + then -- the internal API is not as strict as the public one, so we need to tweak the invalid config some more + setField "config.crlProxy" (object []) + else removeField "config.crlProxy" + mlsE2EId2 <- + mlsE2EId1 + & setField "config.verificationExpiration" (A.Number 86401) + & setField "config.useProxyOnMobile" True + mkFeatureTests "mlsE2EId" + & addUpdate mlsE2EId1 + & addUpdate mlsE2EId2 + & addInvalidUpdate invalid + & runFeatureTests OwnDomain access + +testPatchE2EId :: (HasCallStack) => App () +testPatchE2EId = do + checkPatch OwnDomain "mlsE2EId" (object ["lockStatus" .= "locked"]) + checkPatch OwnDomain "mlsE2EId" (object ["status" .= "enabled"]) + checkPatch OwnDomain "mlsE2EId" + $ object ["lockStatus" .= "locked", "status" .= "enabled"] + checkPatch OwnDomain "mlsE2EId" + $ object + [ "lockStatus" .= "unlocked", + "config" + .= object + [ "crlProxy" .= "https://example.com", + "verificationExpiration" .= A.Number 86401, + "useProxyOnMobile" .= True + ] + ] + + checkPatch OwnDomain "mlsE2EId" + $ object + [ "config" + .= object + [ "crlProxy" .= "https://example.com", + "verificationExpiration" .= A.Number 86401, + "useProxyOnMobile" .= True + ] + ] + +testMlsE2EConfigCrlProxyRequired :: (HasCallStack) => App () +testMlsE2EConfigCrlProxyRequired = do + (owner, tid, _) <- createTeam OwnDomain 1 + let configWithoutCrlProxy = + object + [ "config" + .= object + [ "useProxyOnMobile" .= False, + "verificationExpiration" .= A.Number 86400 + ], + "status" .= "enabled" + ] + + -- From API version 6 onwards, the CRL proxy is required, so the request should fail when it's not provided + bindResponse (Public.setTeamFeatureConfig owner tid "mlsE2EId" configWithoutCrlProxy) $ \resp -> do + resp.status `shouldMatchInt` 400 + resp.json %. "label" `shouldMatch` "mls-e2eid-missing-crl-proxy" + + configWithCrlProxy <- + configWithoutCrlProxy + & setField "config.useProxyOnMobile" True + & setField "config.crlProxy" "https://crl-proxy.example.com" + & setField "status" "enabled" + + -- The request should succeed when the CRL proxy is provided + bindResponse (Public.setTeamFeatureConfig owner tid "mlsE2EId" configWithCrlProxy) $ \resp -> do + resp.status `shouldMatchInt` 200 + + -- Assert that the feature config got updated correctly + expectedResponse <- configWithCrlProxy & setField "lockStatus" "unlocked" & setField "ttl" "unlimited" + checkFeature "mlsE2EId" owner tid expectedResponse + +testMlsE2EConfigCrlProxyNotRequiredInV5 :: (HasCallStack) => App () +testMlsE2EConfigCrlProxyNotRequiredInV5 = do + (owner, tid, _) <- createTeam OwnDomain 1 + let configWithoutCrlProxy = + object + [ "config" + .= object + [ "useProxyOnMobile" .= False, + "verificationExpiration" .= A.Number 86400 + ], + "status" .= "enabled" + ] + + -- In API version 5, the CRL proxy is not required, so the request should succeed + bindResponse (Public.setTeamFeatureConfigVersioned (ExplicitVersion 5) owner tid "mlsE2EId" configWithoutCrlProxy) $ \resp -> do + resp.status `shouldMatchInt` 200 + + -- Assert that the feature config got updated correctly + expectedResponse <- + configWithoutCrlProxy + & setField "lockStatus" "unlocked" + & setField "ttl" "unlimited" + & setField "config.crlProxy" "https://crlproxy.example.com" + checkFeature "mlsE2EId" owner tid expectedResponse diff --git a/integration/test/Test/FeatureFlags/MlsMigration.hs b/integration/test/Test/FeatureFlags/MlsMigration.hs new file mode 100644 index 00000000000..edabd00fb16 --- /dev/null +++ b/integration/test/Test/FeatureFlags/MlsMigration.hs @@ -0,0 +1,89 @@ +module Test.FeatureFlags.MlsMigration where + +import qualified API.Galley as Public +import qualified API.GalleyInternal as Internal +import qualified Data.Aeson as A +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testMlsMigration :: (HasCallStack) => APIAccess -> App () +testMlsMigration access = do + -- first we have to enable mls + (owner, tid, _) <- createTeam OwnDomain 0 + void $ Public.setTeamFeatureConfig owner tid "mls" mlsEnable >>= getJSON 200 + mkFeatureTests "mlsMigration" + & addUpdate mlsMigrationConfig1 + & addUpdate mlsMigrationConfig2 + & setOwner owner + >>= runFeatureTests OwnDomain access + +testMlsMigrationDefaults :: (HasCallStack) => App () +testMlsMigrationDefaults = do + withModifiedBackend + def + { galleyCfg = setField "settings.featureFlags.mlsMigration.defaults.lockStatus" "unlocked" + } + $ \domain -> do + (owner, tid, _) <- createTeam domain 0 + void + $ Internal.patchTeamFeature owner tid "mls" (object ["status" .= "enabled"]) + >>= getJSON 200 + feat <- Internal.getTeamFeature owner tid "mlsMigration" >>= getJSON 200 + feat %. "config" `shouldMatch` mlsMigrationDefaultConfig + +mlsEnableConfig :: Value +mlsEnableConfig = + object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "mls", + "supportedProtocols" .= ["mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= A.Number 1 + ] + +mlsEnable :: Value +mlsEnable = + object + [ "status" .= "enabled", + "config" .= mlsEnableConfig + ] + +mlsMigrationDefaultConfig :: Value +mlsMigrationDefaultConfig = + object + [ "startTime" .= "2029-05-16T10:11:12.123Z", + "finaliseRegardlessAfter" .= "2029-10-17T00:00:00Z" + ] + +mlsMigrationConfig1 :: Value +mlsMigrationConfig1 = + object + [ "status" .= "enabled", + "config" + .= object + [ "startTime" .= "2029-05-16T10:11:12.123Z", + "finaliseRegardlessAfter" .= "2030-10-17T00:00:00Z" + ] + ] + +mlsMigrationConfig2 :: Value +mlsMigrationConfig2 = + object + [ "status" .= "enabled", + "config" + .= object + [ "startTime" .= "2030-05-16T10:11:12.123Z", + "finaliseRegardlessAfter" .= "2031-10-17T00:00:00Z" + ] + ] + +mlsMigrationInvalidConfig :: Value +mlsMigrationInvalidConfig = + object + [ "status" .= "enabled", + "config" + .= object + [ "startTime" .= A.Number 1 + ] + ] diff --git a/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs b/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs new file mode 100644 index 00000000000..8db8464a8d1 --- /dev/null +++ b/integration/test/Test/FeatureFlags/OutlookCalIntegration.hs @@ -0,0 +1,14 @@ +module Test.FeatureFlags.OutlookCalIntegration where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchOutlookCalIntegration :: (HasCallStack) => App () +testPatchOutlookCalIntegration = checkPatch OwnDomain "outlookCalIntegration" enabled + +testOutlookCalIntegration :: (HasCallStack) => APIAccess -> App () +testOutlookCalIntegration access = + mkFeatureTests "outlookCalIntegration" + & addUpdate enabled + & addUpdate disabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/SSO.hs b/integration/test/Test/FeatureFlags/SSO.hs new file mode 100644 index 00000000000..7b633ddcb10 --- /dev/null +++ b/integration/test/Test/FeatureFlags/SSO.hs @@ -0,0 +1,36 @@ +module Test.FeatureFlags.SSO where + +import qualified API.Galley as Public +import qualified API.GalleyInternal as Internal +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testSSODisabledByDefault :: (HasCallStack) => App () +testSSODisabledByDefault = do + let put uid tid = Internal.setTeamFeatureConfig uid tid "sso" (object ["status" .= "enabled"]) >>= assertSuccess + let patch uid tid = Internal.setTeamFeatureStatus uid tid "sso" "enabled" >>= assertSuccess + forM_ [put, patch] $ \enableFeature -> do + withModifiedBackend + def {galleyCfg = setField "settings.featureFlags.sso" "disabled-by-default"} + $ \domain -> do + (owner, tid, m : _) <- createTeam domain 2 + nonMember <- randomUser domain def + assertForbidden =<< Public.getTeamFeature nonMember tid "sso" + -- Test default + checkFeature "sso" m tid disabled + -- Test override + enableFeature owner tid + checkFeature "sso" owner tid enabled + +testSSOEnabledByDefault :: (HasCallStack) => App () +testSSOEnabledByDefault = do + withModifiedBackend + def {galleyCfg = setField "settings.featureFlags.sso" "enabled-by-default"} + $ \domain -> do + (owner, tid, _m : _) <- createTeam domain 2 + nonMember <- randomUser domain def + assertForbidden =<< Public.getTeamFeature nonMember tid "sso" + checkFeature "sso" owner tid enabled + -- check that the feature cannot be disabled + assertLabel 403 "not-implemented" =<< Internal.setTeamFeatureConfig owner tid "sso" (object ["status" .= "disabled"]) diff --git a/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs b/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs new file mode 100644 index 00000000000..a2ce39cd44e --- /dev/null +++ b/integration/test/Test/FeatureFlags/SearchVisibilityAvailable.hs @@ -0,0 +1,34 @@ +module Test.FeatureFlags.SearchVisibilityAvailable where + +import qualified API.Galley as Public +import qualified API.GalleyInternal as Internal +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchSearchVisibility :: (HasCallStack) => App () +testPatchSearchVisibility = checkPatch OwnDomain "searchVisibility" enabled + +testSearchVisibilityDisabledByDefault :: (HasCallStack) => App () +testSearchVisibilityDisabledByDefault = do + withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "disabled-by-default"} $ \domain -> do + (owner, tid, m : _) <- createTeam domain 2 + -- Test default + checkFeature "searchVisibility" m tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "enabled" + checkFeature "searchVisibility" owner tid enabled + assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "disabled" + checkFeature "searchVisibility" owner tid disabled + +testSearchVisibilityEnabledByDefault :: (HasCallStack) => App () +testSearchVisibilityEnabledByDefault = do + withModifiedBackend def {galleyCfg = setField "settings.featureFlags.teamSearchVisibility" "enabled-by-default"} $ \domain -> do + (owner, tid, m : _) <- createTeam domain 2 + nonMember <- randomUser domain def + assertForbidden =<< Public.getTeamFeature nonMember tid "searchVisibility" + -- Test default + checkFeature "searchVisibility" m tid enabled + assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "disabled" + checkFeature "searchVisibility" owner tid disabled + assertSuccess =<< Internal.setTeamFeatureStatus owner tid "searchVisibility" "enabled" + checkFeature "searchVisibility" owner tid enabled diff --git a/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs b/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs new file mode 100644 index 00000000000..55d40c5c2f7 --- /dev/null +++ b/integration/test/Test/FeatureFlags/SearchVisibilityInbound.hs @@ -0,0 +1,32 @@ +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" + (alice, tid, _) <- createTeam OwnDomain 2 + eve <- randomUser OwnDomain def + assertForbidden =<< Public.getTeamFeature eve tid featureName + checkFeature featureName alice tid disabled + + void $ withWebSocket alice $ \ws -> do + setFlag access ws tid featureName enabled + setFlag access ws tid featureName disabled diff --git a/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs b/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs new file mode 100644 index 00000000000..019bed20341 --- /dev/null +++ b/integration/test/Test/FeatureFlags/SelfDeletingMessages.hs @@ -0,0 +1,35 @@ +module Test.FeatureFlags.SelfDeletingMessages where + +import qualified Data.Aeson.Types as A +import Test.FeatureFlags.Util +import Testlib.Prelude + +feature :: (ToJSON timeout) => [A.Pair] -> timeout -> Value +feature ps timeout = + object + ( ps + <> [ "ttl" .= "unlimited", + "config" .= object ["enforcedTimeoutSeconds" .= toJSON timeout] + ] + ) + +testSelfDeletingMessages :: (HasCallStack) => APIAccess -> App () +testSelfDeletingMessages access = + mkFeatureTests "selfDeletingMessages" + & addUpdate (feature ["status" .= "disabled"] (0 :: Int)) + & addUpdate (feature ["status" .= "enabled"] (30 :: Int)) + & addInvalidUpdate (feature ["status" .= "enabled"] "") + & runFeatureTests OwnDomain access + +testPatchSelfDeletingMessages :: (HasCallStack) => App () +testPatchSelfDeletingMessages = do + checkPatch OwnDomain "selfDeletingMessages" + $ object ["lockStatus" .= "locked"] + checkPatch OwnDomain "selfDeletingMessages" + $ object ["status" .= "disabled"] + checkPatch OwnDomain "selfDeletingMessages" + $ object ["lockStatus" .= "locked", "status" .= "disabled"] + checkPatch OwnDomain "selfDeletingMessages" + $ object ["lockStatus" .= "unlocked", "config" .= object ["enforcedTimeoutSeconds" .= A.Number 30]] + checkPatch OwnDomain "selfDeletingMessages" + $ object ["config" .= object ["enforcedTimeoutSeconds" .= A.Number 60]] diff --git a/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs b/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs new file mode 100644 index 00000000000..7acc3621f4e --- /dev/null +++ b/integration/test/Test/FeatureFlags/SndFactorPasswordChallenge.hs @@ -0,0 +1,16 @@ +module Test.FeatureFlags.SndFactorPasswordChallenge where + +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchSndFactorPasswordChallenge :: (HasCallStack) => App () +testPatchSndFactorPasswordChallenge = + checkPatch OwnDomain "sndFactorPasswordChallenge" enabled + +testSndFactorPasswordChallenge :: (HasCallStack) => APIAccess -> App () +testSndFactorPasswordChallenge access = + do + mkFeatureTests "sndFactorPasswordChallenge" + & addUpdate enabled + & addUpdate disabled + & runFeatureTests OwnDomain access diff --git a/integration/test/Test/FeatureFlags/Util.hs b/integration/test/Test/FeatureFlags/Util.hs index c1e3bc9acc9..bca97070383 100644 --- a/integration/test/Test/FeatureFlags/Util.hs +++ b/integration/test/Test/FeatureFlags/Util.hs @@ -19,8 +19,38 @@ module Test.FeatureFlags.Util where import qualified API.Galley as Public import qualified API.GalleyInternal as Internal +import qualified Data.Aeson as A +import qualified Data.Aeson.KeyMap as KM +import qualified Data.Text as Text +import Notifications +import SetupHelpers import Testlib.Prelude +data APIAccess = InternalAPI | PublicAPI + deriving (Show, Eq) + +instance TestCases APIAccess where + mkTestCases = + pure + [ MkTestCase "[api=internal]" InternalAPI, + MkTestCase "[api=public]" PublicAPI + ] + +newtype Feature = Feature String + +instance TestCases Feature where + mkTestCases = pure $ case defAllFeatures of + Object obj -> do + feat <- KM.keys obj + let A.String nameT = toJSON feat + name = Text.unpack nameT + pure $ MkTestCase ("[feature=" <> name <> "]") (Feature name) + _ -> [] + +setFeature :: APIAccess -> Value -> String -> String -> Value -> App Response +setFeature InternalAPI = Internal.setTeamFeatureConfig +setFeature PublicAPI = Public.setTeamFeatureConfig + disabled :: Value disabled = object ["lockStatus" .= "unlocked", "status" .= "disabled", "ttl" .= "unlimited"] @@ -30,25 +60,110 @@ disabledLocked = object ["lockStatus" .= "locked", "status" .= "disabled", "ttl" enabled :: Value enabled = object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited"] -checkFeature :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App () -checkFeature = checkFeatureWith shouldMatch +defEnabledObj :: Value -> Value +defEnabledObj conf = + object + [ "lockStatus" .= "unlocked", + "status" .= "enabled", + "ttl" .= "unlimited", + "config" .= conf + ] + +defAllFeatures :: Value +defAllFeatures = + object + [ "legalhold" .= disabled, + "sso" .= disabled, + "searchVisibility" .= disabled, + "validateSAMLemails" .= enabled, + "digitalSignatures" .= disabled, + "appLock" .= defEnabledObj (object ["enforceAppLock" .= False, "inactivityTimeoutSecs" .= A.Number 60]), + "fileSharing" .= enabled, + "classifiedDomains" .= defEnabledObj (object ["domains" .= ["example.com"]]), + "conferenceCalling" .= confCalling def {lockStatus = Just "locked"}, + "selfDeletingMessages" + .= defEnabledObj (object ["enforcedTimeoutSeconds" .= A.Number 0]), + "conversationGuestLinks" .= enabled, + "sndFactorPasswordChallenge" .= disabledLocked, + "mls" + .= object + [ "lockStatus" .= "unlocked", + "status" .= "disabled", + "ttl" .= "unlimited", + "config" + .= object + [ "protocolToggleUsers" .= ([] :: [String]), + "defaultProtocol" .= "proteus", + "supportedProtocols" .= ["proteus", "mls"], + "allowedCipherSuites" .= ([1] :: [Int]), + "defaultCipherSuite" .= A.Number 1 + ] + ], + "searchVisibilityInbound" .= disabled, + "exposeInvitationURLsToTeamAdmin" .= disabledLocked, + "outlookCalIntegration" .= disabledLocked, + "mlsE2EId" + .= object + [ "lockStatus" .= "unlocked", + "status" .= "disabled", + "ttl" .= "unlimited", + "config" + .= object + [ "verificationExpiration" .= A.Number 86400, + "useProxyOnMobile" .= False, + "crlProxy" .= "https://crlproxy.example.com" + ] + ], + "mlsMigration" + .= object + [ "lockStatus" .= "locked", + "status" .= "enabled", + "ttl" .= "unlimited", + "config" + .= object + [ "startTime" .= "2029-05-16T10:11:12.123Z", + "finaliseRegardlessAfter" .= "2029-10-17T00:00:00Z" + ] + ], + "enforceFileDownloadLocation" + .= object + [ "lockStatus" .= "unlocked", + "status" .= "disabled", + "ttl" .= "unlimited", + "config" + .= object + [ "enforcedDownloadLocation" .= "downloads" + ] + ], + "limitedEventFanout" .= disabled + ] -checkFeatureWith :: (HasCallStack, MakesValue user, MakesValue tid, MakesValue expected) => ((HasCallStack) => App Value -> expected -> App ()) -> String -> user -> tid -> expected -> App () -checkFeatureWith shouldMatch' feature user tid expected = do +hasExplicitLockStatus :: String -> Bool +hasExplicitLockStatus "fileSharing" = True +hasExplicitLockStatus "conferenceCalling" = True +hasExplicitLockStatus "selfDeletingMessages" = True +hasExplicitLockStatus "guestLinks" = True +hasExplicitLockStatus "sndFactorPasswordChallenge" = True +hasExplicitLockStatus "outlookCalIntegration" = True +hasExplicitLockStatus "enforceFileDownloadLocation" = True +hasExplicitLockStatus _ = False + +checkFeature :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App () +checkFeature feature user tid expected = do tidStr <- asString tid domain <- objDomain user bindResponse (Internal.getTeamFeature domain tidStr feature) $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json `shouldMatch'` expected + resp.json `shouldMatch` expected bindResponse (Public.getTeamFeatures user tid) $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json %. feature `shouldMatch'` expected + resp.json %. feature `shouldMatch` expected bindResponse (Public.getTeamFeature user tid feature) $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json `shouldMatch'` expected + resp.json `shouldMatch` expected bindResponse (Public.getFeatureConfigs user) $ \resp -> do resp.status `shouldMatchInt` 200 - resp.json %. feature `shouldMatch'` expected + resp.json %. feature `shouldMatch` expected assertForbidden :: (HasCallStack) => Response -> App () assertForbidden = assertLabel 403 "no-team-member" @@ -76,3 +191,155 @@ confCalling args = "config" .= object ["useSFTForOneToOneCalls" .= args.sft] ] + +setFlag :: (HasCallStack) => APIAccess -> WebSocket -> String -> String -> Value -> App () +setFlag access ws tid featureName value = do + update <- removeField "ttl" value + void + $ setFeature access ws.user tid featureName update + >>= getJSON 200 + expected <- + setField "ttl" "unlimited" + =<< setField "lockStatus" "unlocked" value + + -- should receive an event + do + notif <- awaitMatch isFeatureConfigUpdateNotif ws + notif %. "payload.0.name" `shouldMatch` featureName + notif %. "payload.0.data" `shouldMatch` expected + + checkFeature featureName ws.user tid expected + +checkPatch :: + (HasCallStack, MakesValue domain) => + domain -> + String -> + Value -> + App () +checkPatch domain featureName patch = do + (owner, tid, _) <- createTeam domain 0 + defFeature <- defAllFeatures %. featureName + + let valueOrDefault :: String -> App Value + valueOrDefault key = do + mValue <- lookupField patch key + maybe (defFeature %. key) pure mValue + + checkFeature featureName owner tid defFeature + void + $ Internal.patchTeamFeature domain tid featureName patch + >>= getJSON 200 + patched <- Internal.getTeamFeature domain tid featureName >>= getJSON 200 + checkFeature featureName owner tid patched + lockStatus <- patched %. "lockStatus" >>= asString + if lockStatus == "locked" + then do + -- if lock status is locked the feature status should fall back to the default + patched `shouldMatch` (defFeature & setField "lockStatus" "locked") + -- if lock status is locked, it was either locked before or changed by the patch + mPatchedLockStatus <- lookupField patch "lockStatus" + case mPatchedLockStatus of + Just ls -> ls `shouldMatch` "locked" + Nothing -> defFeature %. "lockStatus" `shouldMatch` "locked" + else do + patched %. "status" `shouldMatch` valueOrDefault "status" + mPatchedConfig <- lookupField patched "config" + case mPatchedConfig of + Just patchedConfig -> patchedConfig `shouldMatch` valueOrDefault "config" + Nothing -> do + mDefConfig <- lookupField defFeature "config" + assertBool "patch had an unexpected config field" (isNothing mDefConfig) + + when (hasExplicitLockStatus featureName) $ do + -- if lock status is unlocked, it was either unlocked before or changed + -- by the patch + mPatchedLockStatus <- lookupField patch "lockStatus" + case mPatchedLockStatus of + Just ls -> ls `shouldMatch` "unlocked" + Nothing -> defFeature %. "lockStatus" `shouldMatch` "unlocked" + +data FeatureTests = FeatureTests + { name :: String, + -- | valid config values used to update the feature setting (should not + -- include the lock status and ttl, as these are not part of the request + -- payload) + updates :: [Value], + invalidUpdates :: [Value], + owner :: Maybe Value + } + +mkFeatureTests :: String -> FeatureTests +mkFeatureTests name = FeatureTests name [] [] Nothing + +addUpdate :: Value -> FeatureTests -> FeatureTests +addUpdate up ft = ft {updates = ft.updates <> [up]} + +addInvalidUpdate :: Value -> FeatureTests -> FeatureTests +addInvalidUpdate up ft = ft {invalidUpdates = ft.invalidUpdates <> [up]} + +setOwner :: (MakesValue user) => user -> FeatureTests -> App FeatureTests +setOwner owner ft = do + x <- make owner + pure ft {owner = Just x} + +runFeatureTests :: + (HasCallStack, MakesValue domain) => + domain -> + APIAccess -> + FeatureTests -> + App () +runFeatureTests domain access ft = do + defFeature <- defAllFeatures %. ft.name + -- personal user + do + user <- randomUser domain def + bindResponse (Public.getFeatureConfigs user) $ \resp -> do + resp.status `shouldMatchInt` 200 + feat <- resp.json %. ft.name + lockStatus <- feat %. "lockStatus" + expected <- setField "lockStatus" lockStatus defFeature + feat `shouldMatch` expected + + -- make team + (owner, tid) <- case ft.owner of + Nothing -> do + (owner, tid, _) <- createTeam domain 0 + pure (owner, tid) + Just owner -> do + tid <- owner %. "team" & asString + pure (owner, tid) + checkFeature ft.name owner tid defFeature + + -- lock the feature + Internal.setTeamFeatureLockStatus owner tid ft.name "locked" + bindResponse (Public.getTeamFeature owner tid ft.name) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "lockStatus" `shouldMatch` "locked" + expected <- setField "lockStatus" "locked" defFeature + checkFeature ft.name owner tid expected + + for_ ft.updates $ (setFeature access owner tid ft.name >=> getJSON 409) + + -- unlock the feature + Internal.setTeamFeatureLockStatus owner tid ft.name "unlocked" + void $ withWebSocket owner $ \ws -> do + for_ ft.updates $ \update -> do + setFlag access ws tid ft.name update + + for_ ft.invalidUpdates $ \update -> do + void $ setFeature access owner tid ft.name update >>= getJSON 400 + assertNoEvent 2 ws + + -- lock again, should be set to default value + Internal.setTeamFeatureLockStatus owner tid ft.name "locked" + do + expected <- setField "lockStatus" "locked" defFeature + checkFeature ft.name owner tid expected + + -- unlock again, should be set to the last update + Internal.setTeamFeatureLockStatus owner tid ft.name "unlocked" + for_ (take 1 (reverse ft.updates)) $ \update -> do + expected <- + setField "ttl" "unlimited" + =<< setField "lockStatus" "unlocked" update + checkFeature ft.name owner tid expected diff --git a/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs b/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs new file mode 100644 index 00000000000..6177c52be87 --- /dev/null +++ b/integration/test/Test/FeatureFlags/ValidateSAMLEmails.hs @@ -0,0 +1,17 @@ +module Test.FeatureFlags.ValidateSAMLEmails where + +import SetupHelpers +import Test.FeatureFlags.Util +import Testlib.Prelude + +testPatchValidateSAMLEmails :: (HasCallStack) => App () +testPatchValidateSAMLEmails = + checkPatch OwnDomain "validateSAMLemails" + $ object ["status" .= "disabled"] + +testValidateSAMLEmailsInternal :: (HasCallStack) => App () +testValidateSAMLEmailsInternal = do + (alice, tid, _) <- createTeam OwnDomain 0 + withWebSocket alice $ \ws -> do + setFlag InternalAPI ws tid "validateSAMLemails" disabled + setFlag InternalAPI ws tid "validateSAMLemails" enabled diff --git a/integration/test/Testlib/Cannon.hs b/integration/test/Testlib/Cannon.hs index 3a90d89b170..45093c3e252 100644 --- a/integration/test/Testlib/Cannon.hs +++ b/integration/test/Testlib/Cannon.hs @@ -53,7 +53,7 @@ import qualified Control.Monad.Catch as Catch import Control.Monad.IO.Class import Control.Monad.Reader (asks) import Control.Monad.STM -import Data.Aeson (Value (..), decodeStrict') +import Data.Aeson hiding ((.=)) import Data.ByteString (ByteString) import Data.ByteString.Conversion (fromByteString) import Data.ByteString.Conversion.To @@ -95,6 +95,13 @@ instance HasField "client" WebSocket (Maybe ClientIdentity) where client = c } +instance HasField "user" WebSocket Value where + getField ws = + object + [ "domain" .= ws.wsConnect.domain, + "id" .= ws.wsConnect.user + ] + -- Specifies how a Websocket at cannon should be opened data WSConnect = WSConnect { user :: String, diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index a7bd372b9f1..55d122012d4 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -375,50 +375,41 @@ data TeamFeatureError | FeatureLocked | MLSProtocolMismatch | MLSE2EIDMissingCrlProxy + | EmptyDownloadLocation instance IsSwaggerError TeamFeatureError where -- Do not display in Swagger addToOpenApi = id -type instance MapError 'AppLockInactivityTimeoutTooLow = 'StaticError 400 "inactivity-timeout-too-low" "Applock inactivity timeout must be at least 30 seconds" - -type instance MapError 'LegalHoldFeatureFlagNotEnabled = 'StaticError 403 "legalhold-not-enabled" "Legal hold is not enabled for this wire instance" - -type instance MapError 'LegalHoldWhitelistedOnly = 'StaticError 403 "legalhold-whitelisted-only" "Legal hold is enabled for teams via server config and cannot be changed here" - -type instance - MapError 'DisableSsoNotImplemented = - 'StaticError - 403 - "not-implemented" - "The SSO feature flag is disabled by default. It can only be enabled once for any team, never disabled.\n\ - \\n\ - \Rationale: there are two services in the backend that need to keep their status in sync: galley (teams),\n\ - \and spar (SSO). Galley keeps track of team features. If galley creates an idp, the feature flag is\n\ - \checked. For authentication, spar avoids this expensive check and assumes that the idp can only have\n\ - \been created if the SSO is enabled. This assumption does not hold any more if the switch is turned off\n\ - \again, so we do not support this.\n\ - \\n\ - \It is definitely feasible to change this. If you have a use case, please contact customer support, or\n\ - \open an issue on https://github.com/wireapp/wire-server." - -type instance MapError 'FeatureLocked = 'StaticError 409 "feature-locked" "Feature config cannot be updated (e.g. because it is configured to be locked, or because you need to upgrade your plan)" - -type instance MapError 'MLSProtocolMismatch = 'StaticError 400 "mls-protocol-mismatch" "The default protocol needs to be part of the supported protocols" - -type instance MapError 'MLSE2EIDMissingCrlProxy = 'StaticError 400 "mls-e2eid-missing-crl-proxy" "The field 'crlProxy' is missing in the request payload" - -type instance ErrorEffect TeamFeatureError = Error TeamFeatureError - instance (Member (Error DynError) r) => ServerEffect (Error TeamFeatureError) r where interpretServerEffect = mapError $ \case - AppLockInactivityTimeoutTooLow -> dynError @(MapError 'AppLockInactivityTimeoutTooLow) - LegalHoldFeatureFlagNotEnabled -> dynError @(MapError 'LegalHoldFeatureFlagNotEnabled) - LegalHoldWhitelistedOnly -> dynError @(MapError 'LegalHoldWhitelistedOnly) - DisableSsoNotImplemented -> dynError @(MapError 'DisableSsoNotImplemented) - FeatureLocked -> dynError @(MapError 'FeatureLocked) - MLSProtocolMismatch -> dynError @(MapError 'MLSProtocolMismatch) - MLSE2EIDMissingCrlProxy -> dynError @(MapError 'MLSE2EIDMissingCrlProxy) + AppLockInactivityTimeoutTooLow -> + DynError + 400 + "inactivity-timeout-too-low" + "Applock inactivity timeout must be at least 30 seconds" + LegalHoldFeatureFlagNotEnabled -> DynError 403 "legalhold-not-enabled" "Legal hold is not enabled for this wire instance" + LegalHoldWhitelistedOnly -> DynError 403 "legalhold-whitelisted-only" "Legal hold is enabled for teams via server config and cannot be changed here" + DisableSsoNotImplemented -> + DynError + 403 + "not-implemented" + "The SSO feature flag is disabled by default. It can only be enabled once for any team, never disabled.\n\ + \\n\ + \Rationale: there are two services in the backend that need to keep their status in sync: galley (teams),\n\ + \and spar (SSO). Galley keeps track of team features. If galley creates an idp, the feature flag is\n\ + \checked. For authentication, spar avoids this expensive check and assumes that the idp can only have\n\ + \been created if the SSO is enabled. This assumption does not hold any more if the switch is turned off\n\ + \again, so we do not support this.\n\ + \\n\ + \It is definitely feasible to change this. If you have a use case, please contact customer support, or\n\ + \open an issue on https://github.com/wireapp/wire-server." + FeatureLocked -> DynError 409 "feature-locked" "Feature config cannot be updated (e.g. because it is configured to be locked, or because you need to upgrade your plan)" + MLSProtocolMismatch -> DynError 400 "mls-protocol-mismatch" "The default protocol needs to be part of the supported protocols" + MLSE2EIDMissingCrlProxy -> DynError 400 "mls-e2eid-missing-crl-proxy" "The field 'crlProxy' is missing in the request payload" + EmptyDownloadLocation -> DynError 400 "empty-download-location" "Download location cannot be empty" + +type instance ErrorEffect TeamFeatureError = Error TeamFeatureError -------------------------------------------------------------------------------- -- Proposal failure diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index c07a9c78056..2a9e3c10ab1 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -75,6 +75,12 @@ settings: status: enabled config: domains: ["example.com"] + enforceFileDownloadLocation: + defaults: + status: disabled + lockStatus: unlocked + config: + enforcedDownloadLocation: "downloads" fileSharing: defaults: status: enabled diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 2d260c6c223..940b754bd73 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -428,6 +428,16 @@ instance SetFeatureConfig MlsMigrationConfig where $ throw MLSProtocolMismatch pure feat -instance SetFeatureConfig EnforceFileDownloadLocationConfig +instance SetFeatureConfig EnforceFileDownloadLocationConfig where + type + SetFeatureForTeamConstraints EnforceFileDownloadLocationConfig r = + (Member (Error TeamFeatureError) r) + + prepareFeature _ feat = do + -- empty download location is not allowed + -- this is consistent with all other features, and least surprising for clients + when (feat.config.enforcedDownloadLocation == Just "") $ do + throw EmptyDownloadLocation + pure feat instance SetFeatureConfig LimitedEventFanoutConfig diff --git a/services/galley/src/Galley/Cassandra/MakeFeature.hs b/services/galley/src/Galley/Cassandra/MakeFeature.hs index c090a97bdb0..883369c490d 100644 --- a/services/galley/src/Galley/Cassandra/MakeFeature.hs +++ b/services/galley/src/Galley/Cassandra/MakeFeature.hs @@ -14,6 +14,7 @@ import Data.List.Singletons (Length) import Data.Misc (HttpsUrl) import Data.Singletons (demote) import Data.Time +import Data.Time.Clock.POSIX import GHC.TypeNats import Galley.Cassandra.FeatureTH import Galley.Cassandra.Instances () @@ -336,10 +337,22 @@ instance MakeFeature MlsE2EIdConfig where :* Just feat.config.useProxyOnMobile :* Nil +-- Optional time stamp. A 'Nothing' value is represented as 0. +newtype OptionalUTCTime = OptionalUTCTime {unOptionalUTCTime :: Maybe UTCTime} + +instance Cql OptionalUTCTime where + ctype = Tagged (untag (ctype @UTCTime)) + + toCql = toCql . fromMaybe (posixSecondsToUTCTime 0) . unOptionalUTCTime + + fromCql x = do + t <- fromCql x + pure . OptionalUTCTime $ guard (utcTimeToPOSIXSeconds t /= 0) $> t + instance MakeFeature MlsMigrationConfig where type FeatureRow MlsMigrationConfig = - '[LockStatus, FeatureStatus, UTCTime, UTCTime] + '[LockStatus, FeatureStatus, OptionalUTCTime, OptionalUTCTime] featureColumns = K "mls_migration_lock_status" @@ -351,14 +364,23 @@ instance MakeFeature MlsMigrationConfig where rowToFeature (lockStatus :* status :* startTime :* finalizeAfter :* Nil) = foldMap dbFeatureLockStatus lockStatus <> foldMap dbFeatureStatus status - -- FUTUREWORK: allow using the default - <> dbFeatureConfig (MlsMigrationConfig startTime finalizeAfter) + <> dbFeatureModConfig + ( \defCfg -> + defCfg + { startTime = maybe defCfg.startTime unOptionalUTCTime startTime, + finaliseRegardlessAfter = + maybe + defCfg.finaliseRegardlessAfter + unOptionalUTCTime + finalizeAfter + } + ) featureToRow feat = Just feat.lockStatus :* Just feat.status - :* feat.config.startTime - :* feat.config.finaliseRegardlessAfter + :* Just (OptionalUTCTime feat.config.startTime) + :* Just (OptionalUTCTime feat.config.finaliseRegardlessAfter) :* Nil instance MakeFeature EnforceFileDownloadLocationConfig where @@ -373,12 +395,20 @@ instance MakeFeature EnforceFileDownloadLocationConfig where rowToFeature (lockStatus :* status :* location :* Nil) = foldMap dbFeatureLockStatus lockStatus <> foldMap dbFeatureStatus status - -- FUTUREWORK: allow using the default - <> dbFeatureConfig (EnforceFileDownloadLocationConfig location) + <> foldMap + dbFeatureConfig + ( case location of + Nothing -> Nothing + -- convert empty string to 'Nothing' + Just "" -> Just (EnforceFileDownloadLocationConfig Nothing) + Just loc -> Just (EnforceFileDownloadLocationConfig (Just loc)) + ) + featureToRow feat = Just feat.lockStatus :* Just feat.status - :* feat.config.enforcedDownloadLocation + -- represent 'Nothing' as the empty string + :* Just (fromMaybe "" feat.config.enforcedDownloadLocation) :* Nil instance MakeFeature LimitedEventFanoutConfig where From 5ff74460a8865ed9181e05cdd7b6c802c5fd312b Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 30 Sep 2024 18:31:43 +0200 Subject: [PATCH 095/136] Refactor user types (#4275) * Remove UserAccount type * Remove ExtendedUserAccount * Update golden tests * Rename getExtendedAccount* to getAccount* * Remove getBrigUser from spar --- changelog.d/5-internal/user-types-refactoring | 1 + libs/brig-types/brig-types.cabal | 1 - libs/brig-types/default.nix | 2 - libs/brig-types/src/Brig/Types/Intra.hs | 2 +- .../test/unit/Test/Brig/Types/User.hs | 20 +--- .../src/Wire/API/Routes/Internal/Brig.hs | 4 +- libs/wire-api/src/Wire/API/User.hs | 73 ++++---------- .../API/Golden/Generated/SelfProfile_user.hs | 6 +- .../Wire/API/Golden/Generated/User_user.hs | 15 ++- .../Test/Wire/API/Golden/Manual/UserEvent.hs | 6 +- .../golden/testObject_SelfProfile_user_1.json | 1 + .../test/golden/testObject_UserEvent_1.json | 1 + .../test/golden/testObject_UserEvent_2.json | 1 + .../test/golden/testObject_User_user_1.json | 1 + .../test/golden/testObject_User_user_2.json | 1 + .../test/golden/testObject_User_user_3.json | 1 + .../test/golden/testObject_User_user_4.json | 1 + .../test/golden/testObject_User_user_5.json | 1 + .../AuthenticationSubsystem/Interpreter.hs | 6 +- libs/wire-subsystems/src/Wire/StoredUser.hs | 16 +--- .../TeamInvitationSubsystem/Interpreter.hs | 8 +- .../wire-subsystems/src/Wire/UserSubsystem.hs | 16 ++-- .../src/Wire/UserSubsystem/Interpreter.hs | 40 ++++---- .../InterpreterSpec.hs | 95 ++++++++++++++----- .../Wire/MockInterpreters/UserSubsystem.hs | 6 +- .../test/unit/Wire/UserStoreSpec.hs | 2 +- .../Wire/UserSubsystem/InterpreterSpec.hs | 14 +-- services/brig/src/Brig/API/Client.hs | 4 +- services/brig/src/Brig/API/Internal.hs | 16 ++-- services/brig/src/Brig/API/Public.hs | 39 ++++---- services/brig/src/Brig/API/Types.hs | 2 +- services/brig/src/Brig/API/User.hs | 66 +++++++------ services/brig/src/Brig/Data/Activation.hs | 13 ++- services/brig/src/Brig/Data/User.hs | 63 ++++++------ services/brig/src/Brig/Provider/API.hs | 4 +- services/brig/src/Brig/Team/API.hs | 6 +- services/brig/src/Brig/User/Auth.hs | 15 +-- .../brig/test/integration/API/User/Account.hs | 2 +- .../src/Galley/API/LegalHold/Conflicts.hs | 2 +- .../galley/src/Galley/API/MLS/Migration.hs | 3 +- services/galley/src/Galley/API/Teams.hs | 3 +- .../src/Galley/API/Teams/Notifications.hs | 2 +- .../galley/src/Galley/Effects/BrigAccess.hs | 5 +- services/galley/src/Galley/Intra/User.hs | 3 +- services/galley/test/integration/API/Util.hs | 3 +- services/spar/src/Spar/API.hs | 6 +- services/spar/src/Spar/App.hs | 4 +- services/spar/src/Spar/Intra/Brig.hs | 18 ++-- services/spar/src/Spar/Intra/BrigApp.hs | 6 +- services/spar/src/Spar/Scim/User.hs | 48 +++++----- services/spar/src/Spar/Sem/BrigAccess.hs | 10 +- .../Test/Spar/Intra/BrigSpec.hs | 7 +- .../Test/Spar/Scim/UserSpec.hs | 55 +++++------ services/spar/test-integration/Util/Core.hs | 3 +- services/spar/test/Test/Spar/Scim/UserSpec.hs | 44 ++++----- tools/stern/src/Stern/API.hs | 14 +-- tools/stern/src/Stern/API/Routes.hs | 6 +- tools/stern/src/Stern/Intra.hs | 6 +- tools/stern/test/integration/API.hs | 32 +++---- 59 files changed, 412 insertions(+), 439 deletions(-) create mode 100644 changelog.d/5-internal/user-types-refactoring diff --git a/changelog.d/5-internal/user-types-refactoring b/changelog.d/5-internal/user-types-refactoring new file mode 100644 index 00000000000..f3684d7f309 --- /dev/null +++ b/changelog.d/5-internal/user-types-refactoring @@ -0,0 +1 @@ +Remove `UserAccount` and `ExtendedUserAccount` and their fields to the `User` type diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index 161a81ce30c..817720e0b65 100644 --- a/libs/brig-types/brig-types.cabal +++ b/libs/brig-types/brig-types.cabal @@ -150,7 +150,6 @@ test-suite brig-types-tests , openapi3 , QuickCheck >=2.9 , tasty - , tasty-hunit , tasty-quickcheck , wire-api diff --git a/libs/brig-types/default.nix b/libs/brig-types/default.nix index 290305e7c13..d427109a406 100644 --- a/libs/brig-types/default.nix +++ b/libs/brig-types/default.nix @@ -14,7 +14,6 @@ , openapi3 , QuickCheck , tasty -, tasty-hunit , tasty-quickcheck , types-common , wire-api @@ -40,7 +39,6 @@ mkDerivation { openapi3 QuickCheck tasty - tasty-hunit tasty-quickcheck wire-api ]; diff --git a/libs/brig-types/src/Brig/Types/Intra.hs b/libs/brig-types/src/Brig/Types/Intra.hs index e224a62419e..6b0a81ca597 100644 --- a/libs/brig-types/src/Brig/Types/Intra.hs +++ b/libs/brig-types/src/Brig/Types/Intra.hs @@ -16,7 +16,7 @@ -- with this program. If not, see . module Brig.Types.Intra - ( UserAccount (..), + ( User (..), NewUserScimInvitation (..), UserSet (..), ) diff --git a/libs/brig-types/test/unit/Test/Brig/Types/User.hs b/libs/brig-types/test/unit/Test/Brig/Types/User.hs index e345eb8e9b0..79a848fcef8 100644 --- a/libs/brig-types/test/unit/Test/Brig/Types/User.hs +++ b/libs/brig-types/test/unit/Test/Brig/Types/User.hs @@ -26,14 +26,12 @@ module Test.Brig.Types.User where import Brig.Types.Connection (UpdateConnectionsInternal (..)) -import Brig.Types.Intra (NewUserScimInvitation (..), UserAccount (..)) +import Brig.Types.Intra import Brig.Types.User (ManagedByUpdate (..), RichInfoUpdate (..)) -import Data.Aeson import Imports import Test.Brig.Roundtrip (testRoundTrip, testRoundTripWithSwagger) import Test.QuickCheck (Arbitrary (arbitrary)) import Test.Tasty -import Test.Tasty.HUnit import Wire.API.Routes.Internal.Brig.EJPD (EJPDRequestBody (..), EJPDResponseBody (..)) import Wire.API.User.Auth.ReAuth @@ -48,22 +46,8 @@ roundtripTests = testRoundTrip @NewUserScimInvitation, testRoundTripWithSwagger @EJPDRequestBody, testRoundTripWithSwagger @EJPDResponseBody, - testRoundTrip @UpdateConnectionsInternal, - testRoundTripWithSwagger @UserAccount, - testGroup "golden tests" $ - [testCaseUserAccount] + testRoundTrip @UpdateConnectionsInternal ] instance Arbitrary ReAuthUser where arbitrary = ReAuthUser <$> arbitrary <*> arbitrary <*> arbitrary - -testCaseUserAccount :: TestTree -testCaseUserAccount = testCase "UserAcccount" $ do - assertEqual "1" (Just json1) (encode <$> decode @UserAccount json1) - assertEqual "2" (Just json2) (encode <$> decode @UserAccount json2) - where - json1 :: LByteString - json1 = "{\"accent_id\":1,\"assets\":[],\"deleted\":true,\"email\":\"foo@example.com\",\"expires_at\":\"1864-05-09T17:20:22.192Z\",\"handle\":\"-ve\",\"id\":\"00000000-0000-0001-0000-000100000000\",\"locale\":\"lu\",\"managed_by\":\"wire\",\"name\":\"bla\",\"picture\":[],\"qualified_id\":{\"domain\":\"4-o60.j7-i\",\"id\":\"00000000-0000-0001-0000-000100000000\"},\"service\":{\"id\":\"00000000-0000-0001-0000-000000000001\",\"provider\":\"00000001-0000-0001-0000-000000000001\"},\"status\":\"suspended\",\"supported_protocols\":[\"proteus\"],\"team\":\"00000000-0000-0001-0000-000100000001\"}" - - json2 :: LByteString - json2 = "{\"accent_id\":0,\"assets\":[{\"key\":\"3-4-00000000-0000-0001-0000-000000000000\",\"size\":\"preview\",\"type\":\"image\"}],\"email\":\"a@b\",\"expires_at\":\"1864-05-10T22:45:44.823Z\",\"handle\":\"b8m\",\"id\":\"00000000-0000-0000-0000-000000000001\",\"locale\":\"tk-KZ\",\"managed_by\":\"wire\",\"name\":\"name2\",\"picture\":[],\"qualified_id\":{\"domain\":\"1-8wq0.b22k1.w5\",\"id\":\"00000000-0000-0000-0000-000000000001\"},\"service\":{\"id\":\"00000000-0000-0001-0000-000000000001\",\"provider\":\"00000001-0000-0001-0000-000100000000\"},\"status\":\"pending-invitation\",\"supported_protocols\":[\"proteus\"],\"team\":\"00000000-0000-0001-0000-000000000001\"}" 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 ec853e7f933..5c6dc34ffd9 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -248,7 +248,7 @@ type AccountAPI = ] "includePendingInvitations" Bool - :> Get '[Servant.JSON] [ExtendedUserAccount] + :> Get '[Servant.JSON] [User] ) :<|> Named "iGetUserContacts" @@ -594,7 +594,7 @@ type TeamInvitations = :> Capture "tid" TeamId :> "invitations" :> Servant.ReqBody '[JSON] NewUserScimInvitation - :> Post '[JSON] UserAccount + :> Post '[JSON] User ) type UserAPI = diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 5d5a42af3b1..6e037a0489a 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -36,6 +36,7 @@ module Wire.API.User -- User (should not be here) User (..), userId, + userDeleted, userEmail, userSSOId, userIssuer, @@ -110,10 +111,6 @@ module Wire.API.User AccountStatusUpdate (..), AccountStatusResp (..), - -- * Account - UserAccount (..), - ExtendedUserAccount (..), - -- * Scim invitations NewUserScimInvitation (..), @@ -558,6 +555,7 @@ data User = User -- the user is activated, and the email/phone contained in it will be guaranteedly -- verified. {#RefActivation} userIdentity :: Maybe UserIdentity, + userEmailUnvalidated :: Maybe EmailAddress, -- | required; non-unique userDisplayName :: Name, -- | text status @@ -566,7 +564,7 @@ data User = User userPict :: Pict, userAssets :: [Asset], userAccentId :: ColourId, - userDeleted :: Bool, + userStatus :: AccountStatus, userLocale :: Locale, -- | Set if the user represents an external service, -- i.e. it is a "bot". @@ -589,6 +587,9 @@ data User = User userId :: User -> UserId userId = qUnqualified . userQualifiedId +userDeleted :: User -> Bool +userDeleted u = userStatus u == Deleted + -- -- FUTUREWORK: -- -- disentangle json serializations for 'User', 'NewUser', 'UserIdentity', 'NewUserOrigin'. instance ToSchema User where @@ -601,8 +602,8 @@ userObjectSchema = .= field "qualified_id" schema <* userId .= optional (field "id" (deprecatedSchema "qualified_id" schema)) - <*> userIdentity - .= maybeUserIdentityObjectSchema + <*> userIdentity .= maybeUserIdentityObjectSchema + <*> userEmailUnvalidated .= maybe_ (optField "email_unvalidated" schema) <*> userDisplayName .= field "name" schema <*> userTextStatus @@ -611,22 +612,17 @@ userObjectSchema = .= (fromMaybe noPict <$> optField "picture" schema) <*> userAssets .= (fromMaybe [] <$> optField "assets" (array schema)) - <*> userAccentId - .= field "accent_id" schema - <*> (fromMaybe False <$> (\u -> if userDeleted u then Just True else Nothing) .= maybe_ (optField "deleted" schema)) - <*> userLocale - .= field "locale" schema - <*> userService - .= maybe_ (optField "service" schema) - <*> userHandle - .= maybe_ (optField "handle" schema) - <*> userExpire - .= maybe_ (optField "expires_at" schema) - <*> userTeam - .= maybe_ (optField "team" schema) + <*> userAccentId .= field "accent_id" schema + <*> userStatus .= field "status" schema + <*> userLocale .= field "locale" schema + <*> userService .= maybe_ (optField "service" schema) + <*> userHandle .= maybe_ (optField "handle" schema) + <*> userExpire .= maybe_ (optField "expires_at" schema) + <*> userTeam .= maybe_ (optField "team" schema) <*> userManagedBy .= (fromMaybe ManagedByWire <$> optField "managed_by" schema) <*> userSupportedProtocols .= supportedProtocolsObjectSchema + <* (fromMaybe False <$> (\u -> if userDeleted u then Just True else Nothing) .= maybe_ (optField "deleted" schema)) userEmail :: User -> Maybe EmailAddress userEmail = emailIdentity <=< userIdentity @@ -1813,43 +1809,6 @@ instance Schema.ToSchema AccountStatusUpdate where ------------------------------------------------------------------------------- -- UserAccount --- | A UserAccount is targeted to be used by our \"backoffice\" and represents --- all the data related to a user in our system, regardless of whether they --- are active or not, their status, etc. -data UserAccount = UserAccount - { accountUser :: !User, - accountStatus :: !AccountStatus - } - deriving (Eq, Ord, Show, Generic) - deriving (Arbitrary) via (GenericUniform UserAccount) - deriving (ToJSON, FromJSON, S.ToSchema) via Schema.Schema UserAccount - -instance Schema.ToSchema UserAccount where - schema = Schema.object "UserAccount" userAccountObjectSchema - -userAccountObjectSchema :: ObjectSchema SwaggerDoc UserAccount -userAccountObjectSchema = - UserAccount - <$> accountUser Schema..= userObjectSchema - <*> accountStatus Schema..= Schema.field "status" Schema.schema - --- | This can be parsed as UserAccount, but it has an extra field `email_unvalidated` from --- brig's cassandra that is needed in spar. so we return this from GET /i/users in brig. -data ExtendedUserAccount = ExtendedUserAccount - { account :: UserAccount, - emailUnvalidated :: Maybe EmailAddress - } - deriving (Eq, Ord, Show, Generic) - deriving (Arbitrary) via (GenericUniform ExtendedUserAccount) - deriving (ToJSON, FromJSON, S.ToSchema) via Schema.Schema ExtendedUserAccount - -instance Schema.ToSchema ExtendedUserAccount where - schema = - Schema.object "ExtendedUserAccount" $ - ExtendedUserAccount - <$> account Schema..= userAccountObjectSchema - <*> emailUnvalidated Schema..= maybe_ (Schema.optField "email_unvalidated" Schema.schema) - ------------------------------------------------------------------------------- -- NewUserScimInvitation diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs index 1d91629c06c..9841fd014e1 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs @@ -41,14 +41,14 @@ testObject_SelfProfile_user_1 = { qUnqualified = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000002")), qDomain = Domain {_domainText = "n0-994.m-226.f91.vg9p-mj-j2"} }, - userIdentity = - Just (EmailIdentity (unsafeEmailAddress "some" "example")), + userIdentity = Just (EmailIdentity (unsafeEmailAddress "some" "example")), + userEmailUnvalidated = Nothing, userDisplayName = Name {fromName = "@\1457\2598\66242\US\1104967l+\137302\&6\996495^\162211Mu\t"}, userTextStatus = rightToMaybe $ mkTextStatus "text status", userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 1}, - userDeleted = False, + userStatus = Active, userLocale = Locale {lLanguage = Language Data.LanguageCodes.GL, lCountry = Just (Country {fromCountry = PA})}, userService = diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs index a552b90f049..8d45d8da2a2 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs @@ -55,12 +55,13 @@ testObject_User_user_1 = qDomain = Domain {_domainText = "s-f4.s"} }, userIdentity = Nothing, + userEmailUnvalidated = Nothing, userDisplayName = Name {fromName = "\NULuv\996028su\28209lRi"}, userTextStatus = Nothing, userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 1}, - userDeleted = True, + userStatus = Deleted, userLocale = Locale {lLanguage = Language Data.LanguageCodes.TN, lCountry = Just (Country {fromCountry = SB})}, userService = Nothing, userHandle = Nothing, @@ -79,6 +80,7 @@ testObject_User_user_2 = qDomain = Domain {_domainText = "k.vbg.p"} }, userIdentity = Just (EmailIdentity (unsafeEmailAddress "some" "example")), + userEmailUnvalidated = Nothing, userDisplayName = Name { fromName = @@ -92,7 +94,7 @@ testObject_User_user_2 = ImageAsset (AssetKeyV3 (Id (fromJust (UUID.fromString "5cd81cc4-c643-4e9c-849c-c596a88c27fd"))) AssetExpiring) (Just AssetComplete) ], userAccentId = ColourId {fromColourId = -2}, - userDeleted = True, + userStatus = Deleted, userLocale = Locale {lLanguage = Language Data.LanguageCodes.DA, lCountry = Just (Country {fromCountry = TN})}, userService = Just @@ -117,13 +119,14 @@ testObject_User_user_3 = qDomain = Domain {_domainText = "dt.n"} }, userIdentity = Just (EmailIdentity (unsafeEmailAddress "some" "example")), + userEmailUnvalidated = Nothing, userDisplayName = Name {fromName = ",r\EMXEg0$\98187\RS\SI'uS\ETX/\1009222`\228V.J{\fgE(\rK!\SOp8s9gXO\21810Xj\STX\RS\DC2"}, userTextStatus = Nothing, userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = -2}, - userDeleted = True, + userStatus = Deleted, userLocale = Locale {lLanguage = Language Data.LanguageCodes.TG, lCountry = Just (Country {fromCountry = UA})}, userService = Just @@ -149,6 +152,7 @@ testObject_User_user_4 = }, userIdentity = Just (SSOIdentity (UserScimExternalId "") (Just (unsafeEmailAddress "some" "example"))), + userEmailUnvalidated = Nothing, userDisplayName = Name { fromName = @@ -158,7 +162,7 @@ testObject_User_user_4 = userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 0}, - userDeleted = False, + userStatus = Active, userLocale = Locale {lLanguage = Language Data.LanguageCodes.BI, lCountry = Just (Country {fromCountry = MQ})}, userService = Just @@ -185,6 +189,7 @@ testObject_User_user_5 = }, userIdentity = Just (EmailIdentity (unsafeEmailAddress "some" "example")), + userEmailUnvalidated = Nothing, userDisplayName = Name { fromName = @@ -194,7 +199,7 @@ testObject_User_user_5 = userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 0}, - userDeleted = False, + userStatus = Active, userLocale = Locale {lLanguage = Language Data.LanguageCodes.BI, lCountry = Just (Country {fromCountry = MQ})}, userService = Just diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs index 5d99e783a5c..9c8fa6666f5 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs @@ -225,12 +225,13 @@ alice = qDomain = Domain {_domainText = "foo.example.com"} }, userIdentity = Nothing, + userEmailUnvalidated = Nothing, userDisplayName = Name "alice", userTextStatus = rightToMaybe $ mkTextStatus "text status", userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 1}, - userDeleted = True, + userStatus = Deleted, userLocale = Locale { lLanguage = Language L.TN, @@ -253,12 +254,13 @@ bob = qDomain = Domain {_domainText = "baz.example.com"} }, userIdentity = Nothing, + userEmailUnvalidated = Nothing, userDisplayName = Name "bob", userTextStatus = rightToMaybe $ mkTextStatus "text status", userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 2}, - userDeleted = False, + userStatus = Active, userLocale = Locale { lLanguage = Language L.CA, diff --git a/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json b/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json index fb9fd970c7f..a3a2b2be525 100644 --- a/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json +++ b/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json @@ -17,6 +17,7 @@ "id": "00000000-0000-0001-0000-000000000000", "provider": "00000000-0000-0001-0000-000000000001" }, + "status": "active", "supported_protocols": [ "proteus" ], diff --git a/libs/wire-api/test/golden/testObject_UserEvent_1.json b/libs/wire-api/test/golden/testObject_UserEvent_1.json index bfe90d9970a..53d801cd5b5 100644 --- a/libs/wire-api/test/golden/testObject_UserEvent_1.json +++ b/libs/wire-api/test/golden/testObject_UserEvent_1.json @@ -13,6 +13,7 @@ "domain": "foo.example.com", "id": "539d9183-32a5-4fc4-ba5c-4634454e7585" }, + "status": "deleted", "supported_protocols": [ "proteus" ], diff --git a/libs/wire-api/test/golden/testObject_UserEvent_2.json b/libs/wire-api/test/golden/testObject_UserEvent_2.json index e630fcc9701..1909dc0bc38 100644 --- a/libs/wire-api/test/golden/testObject_UserEvent_2.json +++ b/libs/wire-api/test/golden/testObject_UserEvent_2.json @@ -13,6 +13,7 @@ "domain": "foo.example.com", "id": "539d9183-32a5-4fc4-ba5c-4634454e7585" }, + "status": "deleted", "supported_protocols": [ "proteus" ], diff --git a/libs/wire-api/test/golden/testObject_User_user_1.json b/libs/wire-api/test/golden/testObject_User_user_1.json index b3fbc638960..147a90f2c7d 100644 --- a/libs/wire-api/test/golden/testObject_User_user_1.json +++ b/libs/wire-api/test/golden/testObject_User_user_1.json @@ -11,6 +11,7 @@ "domain": "s-f4.s", "id": "00000002-0000-0001-0000-000200000002" }, + "status": "deleted", "supported_protocols": [ "proteus" ] diff --git a/libs/wire-api/test/golden/testObject_User_user_2.json b/libs/wire-api/test/golden/testObject_User_user_2.json index 4d70aed9563..67d0f8f93e6 100644 --- a/libs/wire-api/test/golden/testObject_User_user_2.json +++ b/libs/wire-api/test/golden/testObject_User_user_2.json @@ -32,6 +32,7 @@ "id": "00000000-0000-0000-0000-000000000001", "provider": "00000000-0000-0000-0000-000100000000" }, + "status": "deleted", "supported_protocols": [], "text_status": "text status" } diff --git a/libs/wire-api/test/golden/testObject_User_user_3.json b/libs/wire-api/test/golden/testObject_User_user_3.json index 4c3c8b75cde..e5f00227e8d 100644 --- a/libs/wire-api/test/golden/testObject_User_user_3.json +++ b/libs/wire-api/test/golden/testObject_User_user_3.json @@ -18,6 +18,7 @@ "id": "00000001-0000-0001-0000-000100000000", "provider": "00000001-0000-0000-0000-000100000000" }, + "status": "deleted", "supported_protocols": [ "proteus" ], diff --git a/libs/wire-api/test/golden/testObject_User_user_4.json b/libs/wire-api/test/golden/testObject_User_user_4.json index 2ac47461051..0b17893cf11 100644 --- a/libs/wire-api/test/golden/testObject_User_user_4.json +++ b/libs/wire-api/test/golden/testObject_User_user_4.json @@ -20,6 +20,7 @@ "sso_id": { "scim_external_id": "" }, + "status": "active", "supported_protocols": [ "proteus" ], diff --git a/libs/wire-api/test/golden/testObject_User_user_5.json b/libs/wire-api/test/golden/testObject_User_user_5.json index 4fe7299ab5b..c3d9ce3cae4 100644 --- a/libs/wire-api/test/golden/testObject_User_user_5.json +++ b/libs/wire-api/test/golden/testObject_User_user_5.json @@ -17,6 +17,7 @@ "id": "00000000-0000-0001-0000-000100000000", "provider": "00000000-0000-0000-0000-000000000000" }, + "status": "active", "supported_protocols": [ "proteus" ], diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs index fc13fb05a01..183d1130c6e 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs @@ -164,12 +164,12 @@ lookupActiveUserByUserKey :: lookupActiveUserByUserKey target = do localUnit <- input let ltarget = qualifyAs localUnit [emailKeyOrig target] - mUser <- User.getExtendedAccountsByEmailNoFilter ltarget + mUser <- User.getAccountsByEmailNoFilter ltarget case mUser of [user] -> do pure $ - if user.account.accountStatus == Active - then Just user.account.accountUser + if user.userStatus == Active + then Just user else Nothing _ -> pure Nothing diff --git a/libs/wire-subsystems/src/Wire/StoredUser.hs b/libs/wire-subsystems/src/Wire/StoredUser.hs index f18502ad591..7d8420740af 100644 --- a/libs/wire-subsystems/src/Wire/StoredUser.hs +++ b/libs/wire-subsystems/src/Wire/StoredUser.hs @@ -73,19 +73,19 @@ hasPendingInvitation u = u.status == Just PendingInvitation mkUserFromStored :: Domain -> Locale -> StoredUser -> User mkUserFromStored domain defaultLocale storedUser = - let deleted = Just Deleted == storedUser.status - expiration = if storedUser.status == Just Ephemeral then storedUser.expires else Nothing + let expiration = if storedUser.status == Just Ephemeral then storedUser.expires else Nothing loc = toLocale defaultLocale (storedUser.language, storedUser.country) svc = newServiceRef <$> storedUser.serviceId <*> storedUser.providerId in User { userQualifiedId = (Qualified storedUser.id domain), userIdentity = storedUser.identity, + userEmailUnvalidated = storedUser.emailUnvalidated, userDisplayName = storedUser.name, userTextStatus = storedUser.textStatus, userPict = (fromMaybe noPict storedUser.pict), userAssets = (fromMaybe [] storedUser.assets), userAccentId = storedUser.accentId, - userDeleted = deleted, + userStatus = fromMaybe Active storedUser.status, userLocale = loc, userService = svc, userHandle = storedUser.handle, @@ -97,16 +97,6 @@ mkUserFromStored domain defaultLocale storedUser = Just ps -> if S.null ps then defSupportedProtocols else ps } -mkAccountFromStored :: Domain -> Locale -> StoredUser -> UserAccount -mkAccountFromStored domain defaultLocale storedUser = - UserAccount - (mkUserFromStored domain defaultLocale storedUser) - (fromMaybe Active storedUser.status) - -mkExtendedAccountFromStored :: Domain -> Locale -> StoredUser -> ExtendedUserAccount -mkExtendedAccountFromStored domain defaultLocale storedUser = - ExtendedUserAccount (mkAccountFromStored domain defaultLocale storedUser) storedUser.emailUnvalidated - toLocale :: Locale -> (Maybe Language, Maybe Country) -> Locale toLocale _ (Just l, c) = Locale l c toLocale l _ = l diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs index 336e73d5f9f..6095ba6441d 100644 --- a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs @@ -131,8 +131,8 @@ createInvitation' tid mExpectedInvId inviteeRole mbInviterUid inviterEmail invRe mEmailOwner <- getLocalUserAccountByUserKey uke isPersonalUserMigration <- case mEmailOwner of Nothing -> pure False - Just account -> - if (account.accountStatus == Active && isNothing account.accountUser.userTeam) + Just user -> + if (user.userStatus == Active && isNothing user.userTeam) then pure True else throw TeamInvitationEmailTaken @@ -181,9 +181,7 @@ isPersonalUser uke = do pure $ case mAccount of -- this can e.g. happen if the key is claimed but the account is not yet created Nothing -> False - Just account -> - account.accountStatus == Active - && isNothing account.accountUser.userTeam + Just user -> user.userStatus == Active && isNothing user.userTeam -- | brig used to not store the role, so for migration we allow this to be empty and fill in the -- default here. diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 5ba2119a103..86f4304b064 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -99,13 +99,13 @@ data UserSubsystem m a where -- | Sometimes we don't have any identity of a requesting user, and local profiles are public. GetLocalUserProfiles :: Local [UserId] -> UserSubsystem m [UserProfile] -- | Get the union of all user accounts matching the `GetBy` argument *and* having a non-empty UserIdentity. - GetExtendedAccountsBy :: Local GetBy -> UserSubsystem m [ExtendedUserAccount] + GetAccountsBy :: Local GetBy -> UserSubsystem m [User] -- | Get user accounts matching the `[EmailAddress]` argument (accounts with missing -- identity and accounts with status /= active included). - GetExtendedAccountsByEmailNoFilter :: Local [EmailAddress] -> UserSubsystem m [ExtendedUserAccount] + GetAccountsByEmailNoFilter :: Local [EmailAddress] -> UserSubsystem m [User] -- | Get user account by local user id (accounts with missing identity and accounts with -- status /= active included). - GetAccountNoFilter :: Local UserId -> UserSubsystem m (Maybe UserAccount) + GetAccountNoFilter :: Local UserId -> UserSubsystem m (Maybe User) -- | Get `SelfProfile` (it contains things not present in `UserProfile`). GetSelfProfile :: Local UserId -> UserSubsystem m (Maybe SelfProfile) -- | Simple updates (as opposed to, eg., handle, where we need to manage locks). Empty fields are ignored (not deleted). @@ -152,10 +152,6 @@ data CheckHandleResp makeSem ''UserSubsystem --- | given a lookup criteria record ('GetBy'), return the union of the user accounts fulfilling that criteria -getAccountsBy :: (Member UserSubsystem r) => Local GetBy -> Sem r [UserAccount] -getAccountsBy getby = (.account) <$$> getExtendedAccountsBy getby - getUserProfile :: (Member UserSubsystem r) => Local UserId -> Qualified UserId -> Sem r (Maybe UserProfile) getUserProfile luid targetUser = listToMaybe <$> getUserProfiles luid [targetUser] @@ -171,7 +167,7 @@ getLocalAccountBy :: (Member UserSubsystem r) => HavePendingInvitations -> Local UserId -> - Sem r (Maybe UserAccount) + Sem r (Maybe User) getLocalAccountBy includePendingInvitations uid = listToMaybe <$> getAccountsBy @@ -182,9 +178,9 @@ getLocalAccountBy includePendingInvitations uid = } ) -getLocalUserAccountByUserKey :: (Member UserSubsystem r) => Local EmailKey -> Sem r (Maybe UserAccount) +getLocalUserAccountByUserKey :: (Member UserSubsystem r) => Local EmailKey -> Sem r (Maybe User) getLocalUserAccountByUserKey q@(tUnqualified -> ek) = - listToMaybe . fmap (.account) <$> getExtendedAccountsByEmailNoFilter (qualifyAs q [emailKeyOrig ek]) + listToMaybe <$> getAccountsByEmailNoFilter (qualifyAs q [emailKeyOrig ek]) ------------------------------------------ -- FUTUREWORK: Pending functions for a team subsystem diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index c0cfd1e02d2..98e4bd97b6f 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -114,12 +114,12 @@ runUserSubsystem cfg authInterpreter = GetLocalUserProfiles others -> runInputConst cfg $ getLocalUserProfilesImpl others - GetExtendedAccountsBy getBy -> + GetAccountsBy getBy -> runInputConst cfg $ - getExtendedAccountsByImpl getBy - GetExtendedAccountsByEmailNoFilter emails -> + getAccountsByImpl getBy + GetAccountsByEmailNoFilter emails -> runInputConst cfg $ - getExtendedAccountsByEmailNoFilterImpl emails + getAccountsByEmailNoFilterImpl emails GetAccountNoFilter luid -> runInputConst cfg $ getAccountNoFilterImpl luid @@ -820,31 +820,31 @@ getAccountNoFilterImpl :: Member (Input UserSubsystemConfig) r ) => Local UserId -> - Sem r (Maybe UserAccount) + Sem r (Maybe User) getAccountNoFilterImpl (tSplit -> (domain, uid)) = do cfg <- input muser <- getUser uid - pure $ (mkAccountFromStored domain cfg.defaultLocale) <$> muser + pure $ (mkUserFromStored domain cfg.defaultLocale) <$> muser -getExtendedAccountsByEmailNoFilterImpl :: +getAccountsByEmailNoFilterImpl :: forall r. ( Member UserStore r, Member UserKeyStore r, Member (Input UserSubsystemConfig) r ) => Local [EmailAddress] -> - Sem r [ExtendedUserAccount] -getExtendedAccountsByEmailNoFilterImpl (tSplit -> (domain, emails)) = do + Sem r [User] +getAccountsByEmailNoFilterImpl (tSplit -> (domain, emails)) = do config <- input nubOrd <$> flip foldMap emails \ek -> do mactiveUid <- lookupKey (mkEmailKey ek) getUsers (nubOrd . catMaybes $ [mactiveUid]) - <&> map (mkExtendedAccountFromStored domain config.defaultLocale) + <&> map (mkUserFromStored domain config.defaultLocale) -------------------------------------------------------------------------------- -- getting user accounts by different criteria -getExtendedAccountsByImpl :: +getAccountsByImpl :: forall r. ( Member UserStore r, Member DeleteQueue r, @@ -853,16 +853,16 @@ getExtendedAccountsByImpl :: Member TinyLog r ) => Local GetBy -> - Sem r [ExtendedUserAccount] -getExtendedAccountsByImpl (tSplit -> (domain, MkGetBy {includePendingInvitations, getByHandle, getByUserId})) = do + Sem r [User] +getAccountsByImpl (tSplit -> (domain, MkGetBy {includePendingInvitations, getByHandle, getByUserId})) = do storedToExtAcc <- do config <- input - pure $ mkExtendedAccountFromStored domain config.defaultLocale + pure $ mkUserFromStored domain config.defaultLocale handleUserIds :: [UserId] <- wither lookupHandle getByHandle - accsByIds :: [ExtendedUserAccount] <- + accsByIds :: [User] <- getUsers (nubOrd $ handleUserIds <> getByUserId) <&> map storedToExtAcc filterM want (nubOrd $ accsByIds) @@ -871,11 +871,11 @@ getExtendedAccountsByImpl (tSplit -> (domain, MkGetBy {includePendingInvitations -- . users without identity -- . pending users without matching invitation (those are garbage-collected) -- . TODO: deleted users? - want :: ExtendedUserAccount -> Sem r Bool - want ExtendedUserAccount {account} = - case account.accountUser.userIdentity of + want :: User -> Sem r Bool + want user = + case user.userIdentity of Nothing -> pure False - Just ident -> case account.accountStatus of + Just ident -> case user.userStatus of PendingInvitation -> case includePendingInvitations of WithPendingInvitations -> case emailIdentity ident of @@ -884,7 +884,7 @@ getExtendedAccountsByImpl (tSplit -> (domain, MkGetBy {includePendingInvitations -- validEmailIdentity, anyEmailIdentity? Just email -> do hasInvitation <- isJust <$> lookupInvitationByEmail email - gcHack hasInvitation (User.userId account.accountUser) + gcHack hasInvitation (User.userId user) pure hasInvitation Nothing -> error "getExtendedAccountsByImpl: should never happen, user invited via scim always has an email" NoPendingInvitations -> pure False diff --git a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs index 4a7572ec306..f553aa595dc 100644 --- a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs @@ -53,7 +53,7 @@ type AllEffects = State (Map EmailAddress [SentMail]) ] -runAllEffects :: Domain -> [ExtendedUserAccount] -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a +runAllEffects :: Domain -> [User] -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a runAllEffects localDomain preexistingUsers mAllowedEmailDomains = run . evalState mempty @@ -77,11 +77,16 @@ spec = describe "AuthenticationSubsystem.Interpreter" do describe "password reset" do prop "password reset should work with the email being used as password reset key" $ \email userNoEmail (cookiesWithTTL :: [(Cookie (), Maybe TTL)]) mPreviousPassword newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, cookiesAfterReset) = - runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + runAllEffects localDomain [user] Nothing $ do forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) mapM_ (uncurry (insertCookie uid)) cookiesWithTTL @@ -96,11 +101,16 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "password reset should work with the returned password reset key" $ \email userNoEmail (cookiesWithTTL :: [(Cookie (), Maybe TTL)]) mPreviousPassword newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, cookiesAfterReset) = - runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + runAllEffects localDomain [user] Nothing $ do forM_ mPreviousPassword (hashPassword >=> upsertHashedPassword uid) mapM_ (uncurry (insertCookie uid)) cookiesWithTTL @@ -124,23 +134,32 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "reset code is generated when email is in allow list" $ \email userNoEmail -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } localDomain = userNoEmail.userQualifiedId.qDomain createPasswordResetCodeResult = - runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] (Just [decodeUtf8 $ domainPart email]) $ + runAllEffects localDomain [user] (Just [decodeUtf8 $ domainPart email]) $ createPasswordResetCode (mkEmailKey email) in counterexample ("expected Right, got: " <> show createPasswordResetCodeResult) $ isRight createPasswordResetCodeResult prop "reset code is not generated for when user's status is not Active" $ - \email userNoEmail status -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + \email userNoEmail -> + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing + } localDomain = userNoEmail.userQualifiedId.qDomain createPasswordResetCodeResult = - runAllEffects localDomain [ExtendedUserAccount (UserAccount user status) Nothing] Nothing $ + runAllEffects localDomain [user] Nothing $ createPasswordResetCode (mkEmailKey email) <* expectNoEmailSent - in status /= Active ==> + in userStatus user /= Active ==> createPasswordResetCodeResult === Right () prop "reset code is not generated for when there is no user for the email" $ @@ -153,11 +172,16 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "reset code is only generated once" $ \email userNoEmail newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (newPasswordHash, mCaughtException) = - runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + runAllEffects localDomain [user] Nothing $ do createPasswordResetCode (mkEmailKey email) (_, code) <- expect1ResetPasswordEmail email @@ -172,11 +196,16 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "reset code is not accepted after expiry" $ \email userNoEmail oldPassword newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + runAllEffects localDomain [user] Nothing $ do upsertHashedPassword uid =<< hashPassword oldPassword createPasswordResetCode (mkEmailKey email) (_, code) <- expect1ResetPasswordEmail email @@ -190,11 +219,16 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "password reset is not allowed with arbitrary codes when no other codes exist" $ \email userNoEmail resetCode oldPassword newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + runAllEffects localDomain [user] Nothing $ do upsertHashedPassword uid =<< hashPassword oldPassword mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity email) resetCode newPassword (,mCaughtExc) <$> lookupHashedPassword uid @@ -203,11 +237,16 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "password reset doesn't work if email is wrong" $ \email wrongEmail userNoEmail resetCode oldPassword newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordInDB, resetPasswordResult) = - runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + runAllEffects localDomain [user] Nothing $ do hashAndUpsertPassword uid oldPassword mCaughtExc <- catchExpectedError $ resetPassword (PasswordResetEmailIdentity wrongEmail) resetCode newPassword (,mCaughtExc) <$> lookupHashedPassword uid @@ -217,11 +256,16 @@ spec = describe "AuthenticationSubsystem.Interpreter" do prop "only 3 wrong password reset attempts are allowed" $ \email userNoEmail arbitraryResetCode oldPassword newPassword (Upto4 wrongResetAttempts) -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right (passwordHashInDB, correctResetCode, wrongResetErrors, resetPassworedWithCorectCodeResult) = - runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + runAllEffects localDomain [user] Nothing $ do upsertHashedPassword uid =<< hashPassword oldPassword createPasswordResetCode (mkEmailKey email) (_, generatedResetCode) <- expect1ResetPasswordEmail email @@ -249,11 +293,16 @@ spec = describe "AuthenticationSubsystem.Interpreter" do describe "internalLookupPasswordResetCode" do prop "should find password reset code by email" $ \email userNoEmail newPassword -> - let user = userNoEmail {userIdentity = Just $ EmailIdentity email} + let user = + userNoEmail + { userIdentity = Just $ EmailIdentity email, + userEmailUnvalidated = Nothing, + userStatus = Active + } uid = User.userId user localDomain = userNoEmail.userQualifiedId.qDomain Right passwordHashInDB = - runAllEffects localDomain [ExtendedUserAccount (UserAccount user Active) Nothing] Nothing $ do + runAllEffects localDomain [user] Nothing $ do void $ createPasswordResetCode (mkEmailKey email) mLookupRes <- internalLookupPasswordResetCode (mkEmailKey email) for_ mLookupRes $ \(_, code) -> resetPassword (PasswordResetEmailIdentity email) code newPassword diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs index 45dc93a379a..839ced4e5ad 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs @@ -7,12 +7,12 @@ import Wire.API.User import Wire.UserSubsystem -- HINT: This is used to test AuthenticationSubsystem, not to test itself! -userSubsystemTestInterpreter :: [ExtendedUserAccount] -> InterpreterFor UserSubsystem r +userSubsystemTestInterpreter :: [User] -> InterpreterFor UserSubsystem r userSubsystemTestInterpreter initialUsers = interpret \case - GetExtendedAccountsByEmailNoFilter (tUnqualified -> emails) -> + GetAccountsByEmailNoFilter (tUnqualified -> emails) -> pure $ filter - (\u -> userEmail u.account.accountUser `elem` (Just <$> emails)) + (\u -> userEmail u `elem` (Just <$> emails)) initialUsers _ -> error $ "userSubsystemTestInterpreter: implement on demand" diff --git a/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs index b1cbf972f98..5f9192c469b 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserStoreSpec.hs @@ -20,7 +20,7 @@ spec = do prop "user deleted" $ \domain defaultLocale storedUser -> let user = mkUserFromStored domain defaultLocale storedUser - in user.userDeleted === (storedUser.status == Just Deleted) + in userDeleted user === (storedUser.status == Just Deleted) prop "user expires" $ \domain defaultLocale storedUser -> let user = mkUserFromStored domain defaultLocale storedUser diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index fbefec47a3f..721f8479645 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -347,7 +347,7 @@ spec = describe "UserSubsystem.Interpreter" do result = runNoFederationStack localBackend Nothing config $ getAccountsBy getBy - in result === [mkAccountFromStored localDomain locale alice] + 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 @@ -418,7 +418,7 @@ spec = describe "UserSubsystem.Interpreter" do result = runNoFederationStack localBackend Nothing config $ getAccountsBy getBy - in result === [mkAccountFromStored localDomain locale alice] + in result === [mkUserFromStored localDomain locale alice] prop "GetBy email does not filter by pending, missing identity or expired invitations" $ \(alice' :: StoredUser) email localDomain visibility locale -> @@ -431,8 +431,8 @@ spec = describe "UserSubsystem.Interpreter" do } result = runNoFederationStack localBackend Nothing config $ - getExtendedAccountsByEmailNoFilter (toLocalUnsafe localDomain [email]) - in result === [mkExtendedAccountFromStored localDomain locale alice] + getAccountsByEmailNoFilter (toLocalUnsafe localDomain [email]) + in result === [mkUserFromStored localDomain locale alice] prop "GetBy userId does not return missing identity users, pending invitation off" $ \(NotPendingEmptyIdentityStoredUser alice) localDomain visibility locale -> @@ -491,7 +491,7 @@ spec = describe "UserSubsystem.Interpreter" do result = runNoFederationStack localBackend Nothing config $ getAccountsBy getBy - in result === [mkAccountFromStored localDomain locale alice] + in result === [mkUserFromStored localDomain locale alice] prop "GetBy pending user by id fails if there is no valid invitation" $ \(PendingNotEmptyIdentityStoredUser alice') (email :: EmailAddress) teamId localDomain visibility locale -> @@ -546,7 +546,7 @@ spec = describe "UserSubsystem.Interpreter" do result = runNoFederationStack localBackend Nothing config $ getAccountsBy getBy - in result === [mkAccountFromStored localDomain locale alice] + in result === [mkUserFromStored localDomain locale alice] prop "GetBy pending user by handle fails if there is no valid invitation" $ \(PendingNotEmptyIdentityStoredUser alice') (email :: EmailAddress) handl teamId localDomain visibility locale -> @@ -786,7 +786,7 @@ spec = describe "UserSubsystem.Interpreter" do runAllErrorsUnsafe . interpretNoFederationStack localBackend Nothing def config $ getLocalUserAccountByUserKey (toLocalUnsafe localDomain userKey) - in retrievedUser === Just (mkAccountFromStored localDomain config.defaultLocale storedUser) + in retrievedUser === Just (mkUserFromStored localDomain config.defaultLocale storedUser) prop "doesn't get users if they are not indexed by the UserKeyStore" $ \(config :: UserSubsystemConfig) (localDomain :: Domain) (storedUserNoEmail :: StoredUser) (email :: EmailAddress) -> diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 08cac151d6d..ce9cd717a7e 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -192,7 +192,9 @@ addClientWithReAuthPolicy :: NewClient -> ExceptT ClientError (AppT r) Client addClientWithReAuthPolicy policy luid@(tUnqualified -> u) con new = do - usr <- (lift . liftSem $ User.getAccountNoFilter luid) >>= maybe (throwE (ClientUserNotFound u)) (pure . (.accountUser)) + usr <- + (lift . liftSem $ User.getAccountNoFilter luid) + >>= maybe (throwE (ClientUserNotFound u)) pure verifyCode (newClientVerificationCode new) luid maxPermClients <- fromMaybe Opt.defUserMaxPermClients <$> asks (.settings.userMaxPermClients) let caps :: Maybe ClientCapabilityList diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 33f79d6ec34..3d5c3b16fed 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -473,14 +473,13 @@ createUserNoVerify :: createUserNoVerify uData = lift . runExceptT $ do result <- API.createUser uData let acc = createdAccount result - let usr = accountUser acc - let uid = userId usr + let uid = userId acc let eac = createdEmailActivation result for_ eac $ \adata -> let key = ActivateKey $ activationKey adata code = activationCode adata in API.activate key code (Just uid) !>> activationErrorToRegisterError - pure . SelfProfile $ usr + pure . SelfProfile $ acc createUserNoVerifySpar :: ( Member GalleyAPIAccess r, @@ -495,14 +494,13 @@ createUserNoVerifySpar uData = lift . runExceptT $ do result <- API.createUserSpar uData let acc = createdAccount result - let usr = accountUser acc - let uid = userId usr + let uid = userId acc let eac = createdEmailActivation result for_ eac $ \adata -> let key = ActivateKey $ activationKey adata code = activationCode adata in API.activate key code (Just uid) !>> CreateUserSparRegistrationError . activationErrorToRegisterError - pure . SelfProfile $ usr + pure . SelfProfile $ acc deleteUserNoAuthH :: ( Member (Embed HttpClientIO) r, @@ -570,7 +568,7 @@ listActivatedAccountsH :: Maybe (CommaSeparatedList Handle) -> Maybe (CommaSeparatedList EmailAddress) -> Maybe Bool -> - Handler r [ExtendedUserAccount] + Handler r [User] listActivatedAccountsH (maybe [] fromCommaSeparatedList -> uids) (maybe [] fromCommaSeparatedList -> handles) @@ -580,9 +578,9 @@ listActivatedAccountsH throwStd (notFound "no user keys") lift $ liftSem do loc <- input - byEmails <- getExtendedAccountsByEmailNoFilter $ loc $> emails + byEmails <- getAccountsByEmailNoFilter $ loc $> emails others <- - getExtendedAccountsBy $ + getAccountsBy $ loc $> def { includePendingInvitations = include, diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index fc460854766..98f151e3763 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -51,7 +51,7 @@ import Brig.Provider.API import Brig.Team.API qualified as Team import Brig.Team.Email qualified as Team import Brig.Types.Activation (ActivationPair) -import Brig.Types.Intra (UserAccount (UserAccount, accountUser)) +import Brig.Types.Intra import Brig.User.API.Handle qualified as Handle import Brig.User.Auth.Cookie qualified as Auth import Cassandra qualified as C @@ -659,7 +659,7 @@ getRichInfo lself user = do -- other user let fetch luid = ifNothing (errorToWai @'E.UserNotFound) - =<< lift (liftSem $ (.accountUser) <$$> User.getLocalAccountBy NoPendingInvitations luid) + =<< lift (liftSem $ User.getLocalAccountBy NoPendingInvitations luid) selfUser <- fetch lself otherUser <- fetch luser case (Public.userTeam selfUser, Public.userTeam otherUser) of @@ -754,39 +754,38 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do let epair = (,) <$> (activationKey <$> eac) <*> (activationCode <$> eac) let newUserLabel = Public.newUserLabel new let newUserTeam = Public.newUserTeam new - let usr = accountUser acc let context = let invitationCode = case Public.newUserTeam new of (Just (Public.NewTeamMember code)) -> Just code _ -> Nothing in ( logFunction "Brig.API.Public.createUser" - . logUser (Public.userId usr) - . maybe id logHandle (Public.userHandle usr) - . maybe id logTeam (Public.userTeam usr) - . maybe id logEmail (Public.userEmail usr) + . logUser (Public.userId acc) + . maybe id logHandle (Public.userHandle acc) + . maybe id logTeam (Public.userTeam acc) + . maybe id logEmail (Public.userEmail acc) . maybe id logInvitationCode invitationCode ) lift . Log.info $ context . Log.msg @Text "Sucessfully created user" - let Public.User {userLocale, userDisplayName} = usr - userEmail = Public.userEmail usr - userId = Public.userId usr + let Public.User {userLocale, userDisplayName} = acc + userEmail = Public.userEmail acc + userId = Public.userId acc lift $ do for_ (liftM2 (,) userEmail epair) $ \(e, p) -> sendActivationEmail e userDisplayName p (Just userLocale) newUserTeam for_ (liftM3 (,,) userEmail (createdUserTeam result) newUserTeam) $ \(e, ct, ut) -> sendWelcomeEmail e ct ut (Just userLocale) cok <- - Auth.toWebCookie =<< case acc of - UserAccount _ Public.Ephemeral -> + Auth.toWebCookie =<< case userStatus acc of + Public.Ephemeral -> lift . wrapHttpClient $ Auth.newCookie @ZAuth.User userId Nothing Public.SessionCookie newUserLabel - UserAccount _ _ -> + _ -> lift . wrapHttpClient $ Auth.newCookie @ZAuth.User userId Nothing Public.PersistentCookie newUserLabel - -- pure $ CreateUserResponse cok userId (Public.SelfProfile usr) - pure $ Public.RegisterSuccess cok (Public.SelfProfile usr) + -- pure $ CreateUserResponse cok userId (Public.SelfProfile acc) + pure $ Public.RegisterSuccess cok (Public.SelfProfile acc) where sendActivationEmail :: (Member EmailSubsystem r) => Public.EmailAddress -> Public.Name -> ActivationPair -> Maybe Public.Locale -> Maybe Public.NewTeamUser -> (AppT r) () sendActivationEmail email name (key, code) locale mTeamUser @@ -1348,11 +1347,11 @@ sendVerificationCode req = do (scopeFromAction action) (Retries 3) timeout - (Just $ toUUID $ Public.userId $ accountUser account) - sendMail email code.codeValue (Just $ Public.userLocale $ accountUser account) action + (Just $ toUUID $ Public.userId $ account) + sendMail email code.codeValue (Just (Public.userLocale account)) action _ -> pure () where - getAccount :: Public.EmailAddress -> (Handler r) (Maybe UserAccount) + getAccount :: Public.EmailAddress -> (Handler r) (Maybe User) getAccount email = lift . liftSem $ do mbUserId <- lookupKey $ mkEmailKey email mbLUserId <- qualifyLocal' `traverse` mbUserId @@ -1365,9 +1364,9 @@ sendVerificationCode req = do Public.Login -> sendLoginVerificationMail email value mbLocale Public.DeleteTeam -> sendTeamDeletionVerificationMail email value mbLocale - getFeatureStatus :: Maybe UserAccount -> (Handler r) Bool + getFeatureStatus :: Maybe User -> (Handler r) Bool getFeatureStatus mbAccount = do - mbStatusEnabled <- lift $ liftSem $ GalleyAPIAccess.getVerificationCodeEnabled `traverse` (Public.userTeam <$> accountUser =<< mbAccount) + mbStatusEnabled <- lift $ liftSem $ GalleyAPIAccess.getVerificationCodeEnabled `traverse` (Public.userTeam =<< mbAccount) pure $ fromMaybe False mbStatusEnabled getSystemSettings :: (Handler r) SystemSettingsPublic diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 97d15e8c06b..d83e258932e 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -50,7 +50,7 @@ import Wire.UserKeyStore data CreateUserResult = CreateUserResult { -- | The newly created user account. - createdAccount :: !UserAccount, + createdAccount :: !User, -- | Activation data for the registered email address, if any. createdEmailActivation :: !(Maybe Activation), -- | Info of a team just created/joined diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 7719f808ac0..0477c86f754 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -205,7 +205,7 @@ createUserSpar new = do account <- lift $ do (account, pw) <- wrapClient $ newAccount new' Nothing (Just tid) handle' - let uid = userId (accountUser account) + let uid = userId account -- FUTUREWORK: make this transactional if possible wrapClient $ Data.insertAccount account Nothing pw False @@ -214,7 +214,7 @@ createUserSpar new = do Nothing -> pure () -- Nothing to do liftSem $ GalleyAPIAccess.createSelfConv uid liftSem $ User.internalUpdateSearchIndex uid - liftSem $ Events.generateUserEvent uid Nothing (UserCreated (accountUser account)) + liftSem $ Events.generateUserEvent uid Nothing (UserCreated account) pure account @@ -222,7 +222,7 @@ createUserSpar new = do userTeam <- withExceptT CreateUserSparRegistrationError $ addUserToTeamSSO account tid (SSOIdentity ident Nothing) (newUserSparRole new) -- Set up feature flags - luid <- lift $ ensureLocal (userQualifiedId (accountUser account)) + luid <- lift $ ensureLocal (userQualifiedId account) lift $ initAccountFeatureConfig (tUnqualified luid) -- Set handle @@ -235,9 +235,9 @@ createUserSpar new = do updateHandle' luid (Just h) = liftSem $ User.updateHandle luid Nothing UpdateOriginScim (fromHandle h) - addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> Role -> ExceptT RegisterError (AppT r) CreateUserTeam + addUserToTeamSSO :: User -> TeamId -> UserIdentity -> Role -> ExceptT RegisterError (AppT r) CreateUserTeam addUserToTeamSSO account tid ident role = do - let uid = userId (accountUser account) + let uid = userId account added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid Nothing role unless added $ throwE RegisterErrorTooManyTeamMembers @@ -350,9 +350,8 @@ createUser new = do Nothing ) Just existingAccount -> - let existingUser = existingAccount.accountUser - mbSSOid = - case (teamInvitation, email, existingUser.userManagedBy, userSSOId existingUser) of + let mbSSOid = + case (teamInvitation, email, existingAccount.userManagedBy, userSSOId existingAccount) of -- isJust teamInvitation And ManagedByScim implies that the -- user invitation has been generated by SCIM and there is no IdP (Just _, _, ManagedByScim, ssoId@(Just (UserScimExternalId _))) -> @@ -362,28 +361,28 @@ createUser new = do Just $ UserScimExternalId (fromEmail em) _ -> newUserSSOId new in ( new - { newUserManagedBy = Just existingUser.userManagedBy, + { newUserManagedBy = Just existingAccount.userManagedBy, newUserIdentity = newIdentity email mbSSOid }, - existingUser.userHandle + existingAccount.userHandle ) -- Create account account <- lift $ do (account, pw) <- wrapClient $ newAccount new' mbInv tid mbHandle - let uid = userId (accountUser account) + let uid = userId account liftSem $ do Log.debug $ field "user" (toByteString uid) . field "action" (val "User.createUser") Log.info $ field "user" (toByteString uid) . msg (val "Creating user") wrapClient $ Data.insertAccount account Nothing pw False liftSem $ GalleyAPIAccess.createSelfConv uid - liftSem $ Events.generateUserEvent uid Nothing (UserCreated (accountUser account)) + liftSem $ Events.generateUserEvent uid Nothing (UserCreated account) pure account - let uid = qUnqualified account.accountUser.userQualifiedId + let uid = qUnqualified account.userQualifiedId createUserTeam <- do activatedTeam <- lift $ do @@ -430,13 +429,13 @@ createUser new = do pure email acceptInvitationToTeam :: - UserAccount -> + User -> StoredInvitation -> EmailKey -> UserIdentity -> ExceptT RegisterError (AppT r) () acceptInvitationToTeam account inv uk ident = do - let uid = userId (accountUser account) + let uid = userId account ok <- lift $ liftSem $ claimKey uk uid unless ok $ throwE RegisterErrorUserKeyExists @@ -459,9 +458,9 @@ createUser new = do UserPendingActivationStore.remove uid InvitationCodeStore.deleteInvitation inv.teamId inv.invitationId - addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> ExceptT RegisterError (AppT r) CreateUserTeam + addUserToTeamSSO :: User -> TeamId -> UserIdentity -> ExceptT RegisterError (AppT r) CreateUserTeam addUserToTeamSSO account tid ident = do - let uid = userId (accountUser account) + let uid = userId account added <- lift $ liftSem $ GalleyAPIAccess.addTeamMember uid tid Nothing defaultRole unless added $ throwE RegisterErrorTooManyTeamMembers @@ -510,12 +509,12 @@ createUserInviteViaScim :: Member TinyLog r ) => NewUserScimInvitation -> - ExceptT HttpError (AppT r) UserAccount + ExceptT HttpError (AppT r) User createUserInviteViaScim (NewUserScimInvitation tid uid extId loc name email _) = do let emKey = mkEmailKey email verifyUniquenessAndCheckBlacklist emKey !>> identityErrorToBrigError account <- lift . wrapClient $ newAccountInviteViaScim uid extId tid loc name email - lift . liftSem . Log.debug $ field "user" (toByteString . userId . accountUser $ account) . field "action" (val "User.createUserInviteViaScim") + lift . liftSem . Log.debug $ field "user" (toByteString . userId $ account) . field "action" (val "User.createUserInviteViaScim") -- add the expiry table entry first! (if brig creates an account, and then crashes before -- creating the expiry table entry, gc will miss user data.) @@ -760,12 +759,12 @@ onActivated :: ActivationEvent -> AppT r (UserId, Maybe UserIdentity, Bool) onActivated (AccountActivated account) = liftSem $ do - let uid = userId (accountUser account) + let uid = userId account Log.debug $ field "user" (toByteString uid) . field "action" (val "User.onActivated") Log.info $ field "user" (toByteString uid) . msg (val "User activated") User.internalUpdateSearchIndex uid - Events.generateUserEvent uid Nothing $ UserActivated (accountUser account) - pure (uid, userIdentity (accountUser account), True) + Events.generateUserEvent uid Nothing $ UserActivated account + pure (uid, userIdentity account, True) onActivated (EmailActivated uid email) = do liftSem $ User.internalUpdateSearchIndex uid liftSem $ Events.generateUserEvent uid Nothing (emailUpdated uid email) @@ -895,22 +894,22 @@ deleteSelfUser luid@(tUnqualified -> uid) pwd = do account <- lift . liftSem $ User.getAccountNoFilter luid case account of Nothing -> throwE DeleteUserInvalid - Just a -> case accountStatus a of + Just a -> case userStatus a of Deleted -> pure Nothing Suspended -> ensureNotOwner a >> go a Active -> ensureNotOwner a >> go a Ephemeral -> go a PendingInvitation -> go a where - ensureNotOwner :: UserAccount -> ExceptT DeleteUserError (AppT r) () + ensureNotOwner :: User -> ExceptT DeleteUserError (AppT r) () ensureNotOwner acc = do - case userTeam $ accountUser acc of + case userTeam acc of Nothing -> pure () Just tid -> do isOwner <- lift $ liftSem $ GalleyAPIAccess.memberIsTeamOwner tid uid when isOwner $ throwE DeleteUserOwnerDeletingSelf go a = maybe (byIdentity a) (byPassword a) pwd - byIdentity a = case emailIdentity =<< userIdentity (accountUser a) of + byIdentity a = case emailIdentity =<< userIdentity a of Just email -> sendCode a email Nothing -> case pwd of Just _ -> throwE DeleteUserMissingPassword @@ -937,8 +936,8 @@ deleteSelfUser luid@(tUnqualified -> uid) pwd = do . msg (val "Sending verification code for account deletion") let k = VerificationCode.codeKey c let v = VerificationCode.codeValue c - let l = userLocale (accountUser a) - let n = userDisplayName (accountUser a) + let l = userLocale a + let n = userDisplayName a lift (liftSem $ sendAccountDeletionEmail target n k v l) `onException` lift (liftSem $ deleteCode k VerificationCode.AccountDeletion) pure $! Just $! VerificationCode.codeTTL c @@ -995,7 +994,6 @@ ensureAccountDeleted luid@(tUnqualified -> uid) = do Just acc -> do probs <- liftSem $ getPropertyKeys uid - let accIsDeleted = accountStatus acc == Deleted clients <- wrapClient $ Data.lookupClients uid localUid <- qualifyLocal uid @@ -1003,7 +1001,7 @@ ensureAccountDeleted luid@(tUnqualified -> uid) = do cookies <- wrapClient $ Auth.listCookies uid [] if notNull probs - || not accIsDeleted + || not (userDeleted acc) || notNull clients || conCount > 0 || notNull cookies @@ -1024,7 +1022,7 @@ ensureAccountDeleted luid@(tUnqualified -> uid) = do -- -- FUTUREWORK(mangoiv): this uses 'UserStore', hence it must be moved to 'UserSubsystem' -- as an effet operation --- FUTUREWORK: this does not need the whole UserAccount structure, only the User. +-- FUTUREWORK: this does not need the whole User structure, only the User. deleteAccount :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, @@ -1035,9 +1033,9 @@ deleteAccount :: Member UserSubsystem r, Member Events r ) => - UserAccount -> + User -> Sem r () -deleteAccount (accountUser -> user) = do +deleteAccount user = do let uid = userId user Log.info $ field "user" (toByteString uid) . msg (val "Deleting account") do @@ -1124,7 +1122,7 @@ getLegalHoldStatus :: AppT r (Maybe UserLegalHoldStatus) getLegalHoldStatus uid = liftSem $ - traverse (getLegalHoldStatus' . accountUser) + traverse getLegalHoldStatus' =<< User.getLocalAccountBy NoPendingInvitations uid getLegalHoldStatus' :: diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 25745846b69..a7f94e58205 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -79,7 +79,7 @@ activationErrorToRegisterError = \case InvalidActivationPhone _ -> RegisterErrorInvalidPhone data ActivationEvent - = AccountActivated !UserAccount + = AccountActivated !User | EmailActivated !UserId !EmailAddress deriving (Show) @@ -106,19 +106,18 @@ activateKey k c u = wrapClientE (verifyCode k c) >>= pickUser >>= activate activate (key, uid) = do luid <- qualifyLocal uid a <- lift (liftSem $ User.getAccountNoFilter luid) >>= maybe (throwE invalidUser) pure - unless (accountStatus a == Active) $ -- this is never 'PendingActivation' in the flow this function is used in. + unless (userStatus a == Active) $ -- this is never 'PendingActivation' in the flow this function is used in. throwE invalidCode - case userIdentity (accountUser a) of + case userIdentity a of Nothing -> do claim key uid let ident = EmailIdentity (emailKeyOrig key) wrapClientE (activateUser uid ident) - let a' = a {accountUser = (accountUser a) {userIdentity = Just ident}} + let a' = a {userIdentity = Just ident} pure . Just $ AccountActivated a' Just _ -> do - let usr = accountUser a - profileNeedsUpdate = Just (emailKeyOrig key) /= userEmail usr - oldKey :: Maybe EmailKey = mkEmailKey <$> userEmail usr + let profileNeedsUpdate = Just (emailKeyOrig key) /= userEmail a + oldKey :: Maybe EmailKey = mkEmailKey <$> userEmail a in handleExistingIdentity uid profileNeedsUpdate oldKey key handleExistingIdentity :: diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index fec86b7a4e6..4e3013a19bd 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -112,7 +112,13 @@ data ReAuthError -- Condition (2.) is essential for maintaining handle uniqueness. It is guaranteed by the -- fact that we're setting getting @mbHandle@ from table @"user"@, and when/if it was added -- there, it was claimed properly. -newAccount :: (MonadClient m, MonadReader Env m) => NewUser -> Maybe InvitationId -> Maybe TeamId -> Maybe Handle -> m (UserAccount, Maybe Password) +newAccount :: + (MonadClient m, MonadReader Env m) => + NewUser -> + Maybe InvitationId -> + Maybe TeamId -> + Maybe Handle -> + m (User, Maybe Password) newAccount u inv tid mbHandle = do defLoc <- defaultUserLocale <$> asks (.settings) domain <- viewFederationDomain @@ -132,7 +138,7 @@ newAccount u inv tid mbHandle = do now <- liftIO =<< asks (.currentTime) pure . Just . toUTCTimeMillis $ addUTCTime (fromIntegral ttl) now _ -> pure Nothing - pure (UserAccount (user uid domain (locale defLoc) expiry) status, passwd) + pure (user uid domain (locale defLoc) expiry, passwd) where ident = newUserIdentity u pass = newUserPassword u @@ -147,32 +153,31 @@ newAccount u inv tid mbHandle = do locale defLoc = fromMaybe defLoc (newUserLocale u) managedBy = fromMaybe defaultManagedBy (newUserManagedBy u) prots = fromMaybe defSupportedProtocols (newUserSupportedProtocols u) - user uid domain l e = User (Qualified uid domain) ident name Nothing pict assets colour False l Nothing mbHandle e tid managedBy prots + user uid domain l e = User (Qualified uid domain) ident Nothing name Nothing pict assets colour status l Nothing mbHandle e tid managedBy prots -newAccountInviteViaScim :: (MonadReader Env m) => UserId -> Text -> TeamId -> Maybe Locale -> Name -> EmailAddress -> m UserAccount +newAccountInviteViaScim :: (MonadReader Env m) => UserId -> Text -> TeamId -> Maybe Locale -> Name -> EmailAddress -> m User newAccountInviteViaScim uid externalId tid locale name email = do defLoc <- defaultUserLocale <$> asks (.settings) let loc = fromMaybe defLoc locale domain <- viewFederationDomain - pure (UserAccount (user domain loc) PendingInvitation) - where - user domain loc = - User - (Qualified uid domain) - (Just $ SSOIdentity (UserScimExternalId externalId) (Just email)) - name - Nothing - (Pict []) - [] - defaultAccentId - False - loc - Nothing - Nothing - Nothing - (Just tid) - ManagedByScim - defSupportedProtocols + pure $ + User + (Qualified uid domain) + (Just $ SSOIdentity (UserScimExternalId externalId) (Just email)) + Nothing + name + Nothing + (Pict []) + [] + defaultAccentId + PendingInvitation + loc + Nothing + Nothing + Nothing + (Just tid) + ManagedByScim + defSupportedProtocols -- | Mandatory password authentication. authenticate :: forall r. (Member PasswordStore r) => UserId -> PlainTextPassword6 -> ExceptT AuthError (AppT r) () @@ -234,7 +239,7 @@ isSamlUser usr = do insertAccount :: (MonadClient m) => - UserAccount -> + User -> -- | If a bot: conversation and team -- (if a team conversation) Maybe (ConvId, Maybe TeamId) -> @@ -242,7 +247,7 @@ insertAccount :: -- | Whether the user is activated Bool -> m () -insertAccount (UserAccount u status) mbConv password activated = retry x5 . batch $ do +insertAccount u mbConv password activated = retry x5 . batch $ do setType BatchLogged setConsistency LocalQuorum let Locale l c = userLocale u @@ -258,7 +263,7 @@ insertAccount (UserAccount u status) mbConv password activated = retry x5 . batc userAccentId u, password, activated, - status, + userStatus u, userExpire u, l, c, @@ -598,7 +603,7 @@ toUsers domain defLocale havePendingInvitations = fmap mk . filter fp textStatus, pict, email, - _emailUnvalidated, + emailUnvalidated, ssoid, accent, assets, @@ -615,19 +620,19 @@ toUsers domain defLocale havePendingInvitations = fmap mk . filter fp prots ) = let ident = toIdentity activated email ssoid - deleted = Just Deleted == status expiration = if status == Just Ephemeral then expires else Nothing loc = toLocaleWithDefault defLocale (lan, con) svc = newServiceRef <$> sid <*> pid in User (Qualified uid domain) ident + emailUnvalidated name textStatus (fromMaybe noPict pict) (fromMaybe [] assets) accent - deleted + (fromMaybe Active status) loc svc handle diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 8630afafef4..dea4ba451c5 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -713,12 +713,12 @@ addBot zuid zcon cid add = do let colour = fromMaybe defaultAccentId (Ext.rsNewBotColour rs) let pict = Pict [] -- Legacy let sref = newServiceRef sid pid - let usr = User (Qualified (botUserId bid) domain) Nothing name Nothing pict assets colour False locale (Just sref) Nothing Nothing Nothing ManagedByWire defSupportedProtocols + let usr = User (Qualified (botUserId bid) domain) Nothing Nothing name Nothing pict assets colour Active locale (Just sref) Nothing Nothing Nothing ManagedByWire defSupportedProtocols let newClt = (newClient PermanentClientType (Ext.rsNewBotLastPrekey rs)) { newClientPrekeys = Ext.rsNewBotPrekeys rs } - lift $ wrapClient $ User.insertAccount (UserAccount usr Active) (Just (cid, cnvTeam cnv)) Nothing True + lift $ wrapClient $ User.insertAccount usr (Just (cid, cnvTeam cnv)) Nothing True maxPermClients <- fromMaybe Opt.defUserMaxPermClients <$> asks (.settings.userMaxPermClients) (clt, _, _) <- do _ <- do diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 8b13deab2ef..0622ce07e7b 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -144,7 +144,7 @@ createInvitationViaScim :: ) => TeamId -> NewUserScimInvitation -> - (Handler r) UserAccount + Handler r User createInvitationViaScim tid newUser@(NewUserScimInvitation _tid _uid@(Id (Id -> invId)) _eid loc name email role) = do env <- ask let inviteeRole = role @@ -345,9 +345,7 @@ isPersonalUser uke = do pure $ case mAccount of -- this can e.g. happen if the key is claimed but the account is not yet created Nothing -> False - Just account -> - account.accountStatus == Active - && isNothing account.accountUser.userTeam + Just account -> account.userStatus == Active && isNothing account.userTeam getInvitationByCode :: ( Member Store.InvitationCodeStore r, diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 7ef94976f13..39c5b1ef139 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -135,7 +135,7 @@ verifyCode mbCode action luid = do mbFeatureEnabled <- liftSem $ GalleyAPIAccess.getVerificationCodeEnabled `traverse` mbTeamId pure $ fromMaybe ((def @(Feature Public.SndFactorPasswordChallengeConfig)).status == Public.FeatureStatusEnabled) mbFeatureEnabled account <- lift . liftSem $ User.getAccountNoFilter luid - let isSsoUser = maybe False (Data.isSamlUser . ((.accountUser))) account + let isSsoUser = maybe False Data.isSamlUser account when (featureEnabled && not isSsoUser) $ do case (mbCode, mbEmail) of (Just code, Just email) -> do @@ -151,7 +151,10 @@ verifyCode mbCode action luid = do ExceptT e (AppT r) (Maybe EmailAddress, Maybe TeamId) getEmailAndTeamId u = do mbAccount <- lift . liftSem $ User.getAccountNoFilter u - pure (userEmail <$> accountUser =<< mbAccount, userTeam <$> accountUser =<< mbAccount) + pure + ( userEmail =<< mbAccount, + userTeam =<< mbAccount + ) loginFailedWith :: (MonadClient m, MonadReader Env m) => LoginError -> UserId -> ExceptT LoginError m () loginFailedWith e uid = decrRetryLimit uid >> throwE e @@ -226,7 +229,7 @@ revokeAccess luid@(tUnqualified -> u) pw cc ll = do lift . liftSem $ Log.debug $ field "user" (toByteString u) . field "action" (val "User.revokeAccess") isSaml <- lift . liftSem $ do account <- User.getAccountNoFilter luid - pure $ maybe False (Data.isSamlUser . ((.accountUser))) account + pure $ maybe False Data.isSamlUser account unless isSaml $ Data.authenticate u pw lift $ wrapHttpClient $ revokeCookies u cc ll @@ -319,10 +322,10 @@ isPendingActivation ident = case ident of lusr <- qualifyLocal' usr maybe False (checkAccount k) <$> User.getAccountNoFilter lusr - checkAccount :: EmailKey -> UserAccount -> Bool + checkAccount :: EmailKey -> User -> Bool checkAccount k a = - let i = userIdentity (accountUser a) - statusAdmitsPending = case accountStatus a of + let i = userIdentity a + statusAdmitsPending = case userStatus a of Active -> True Suspended -> False Deleted -> False diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 4d5db18a72d..9c1afb6f703 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -993,7 +993,7 @@ testGetByIdentity brig = do const 200 === statusCode const (Just [uid]) === getUids where - getUids r = fmap (userId . accountUser) <$> responseJsonMaybe r + getUids r = fmap userId <$> responseJsonMaybe r testPasswordSet :: Brig -> Http () testPasswordSet brig = do diff --git a/services/galley/src/Galley/API/LegalHold/Conflicts.hs b/services/galley/src/Galley/API/LegalHold/Conflicts.hs index 4a4f8e27968..1503e390fe4 100644 --- a/services/galley/src/Galley/API/LegalHold/Conflicts.hs +++ b/services/galley/src/Galley/API/LegalHold/Conflicts.hs @@ -128,7 +128,7 @@ guardLegalholdPolicyConflictsUid self (Map.keys . userClients -> otherUids) = do checkAnyConsentMissing :: Sem r Bool checkAnyConsentMissing = do - users :: [User] <- accountUser <$$> getUsers (self : otherUids) + users <- getUsers (self : otherUids) -- NB: `users` can't be empty! let checkUserConsentMissing :: User -> Sem r Bool checkUserConsentMissing user = diff --git a/services/galley/src/Galley/API/MLS/Migration.hs b/services/galley/src/Galley/API/MLS/Migration.hs index 4cb5c35d8a6..2ecf68ed54f 100644 --- a/services/galley/src/Galley/API/MLS/Migration.hs +++ b/services/galley/src/Galley/API/MLS/Migration.hs @@ -65,8 +65,7 @@ checkMigrationCriteria now conv ws localUsersMigrated = ApAll $ do localProfiles <- - map accountUser - <$> getUsers (map lmId conv.mcLocalMembers) + getUsers (map lmId conv.mcLocalMembers) pure $ all (containsMLS . userSupportedProtocols) localProfiles remoteUsersMigrated = ApAll $ do diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index f6105cc46f1..a8e7953feb3 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -57,7 +57,6 @@ module Galley.API.Teams ) where -import Brig.Types.Intra (accountUser) import Brig.Types.Team (TeamSize (..)) import Cassandra (PageWithState (pwsResults), pwsHasMore) import Cassandra qualified as C @@ -583,7 +582,7 @@ getTeamMembersCSV lusr tid = do let inviterIds :: [UserId] inviterIds = nub $ mapMaybe (fmap fst . view invitation) members - userList :: [User] <- accountUser <$$> E.getUsers inviterIds + userList <- E.getUsers inviterIds let userMap :: M.Map UserId Handle.Handle userMap = M.fromList (mapMaybe extract userList) diff --git a/services/galley/src/Galley/API/Teams/Notifications.hs b/services/galley/src/Galley/API/Teams/Notifications.hs index f3e31f9ec33..38e562dacda 100644 --- a/services/galley/src/Galley/API/Teams/Notifications.hs +++ b/services/galley/src/Galley/API/Teams/Notifications.hs @@ -65,7 +65,7 @@ getTeamNotifications :: Range 1 10000 Int32 -> Sem r QueuedNotificationList getTeamNotifications zusr since size = do - tid <- (noteS @'TeamNotFound =<<) $ (userTeam . accountUser =<<) <$> Intra.getUser zusr + tid <- (noteS @'TeamNotFound =<<) $ (userTeam =<<) <$> Intra.getUser zusr page <- E.getTeamNotifications tid since size pure $ queuedNotificationList diff --git a/services/galley/src/Galley/Effects/BrigAccess.hs b/services/galley/src/Galley/Effects/BrigAccess.hs index 2e14a25c104..de7fc43bd5b 100644 --- a/services/galley/src/Galley/Effects/BrigAccess.hs +++ b/services/galley/src/Galley/Effects/BrigAccess.hs @@ -73,7 +73,6 @@ import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi import Wire.API.Team.Feature import Wire.API.Team.Size -import Wire.API.User import Wire.API.User.Auth.ReAuth import Wire.API.User.Client import Wire.API.User.Client.Prekey @@ -99,7 +98,7 @@ data BrigAccess m a where PutConnectionInternal :: UpdateConnectionsInternal -> BrigAccess m Status ReauthUser :: UserId -> ReAuthUser -> BrigAccess m (Either AuthenticationError ()) LookupActivatedUsers :: [UserId] -> BrigAccess m [User] - GetUsers :: [UserId] -> BrigAccess m [UserAccount] + GetUsers :: [UserId] -> BrigAccess m [User] DeleteUser :: UserId -> BrigAccess m () GetContactList :: UserId -> BrigAccess m [UserId] GetRichInfoMultiUser :: [UserId] -> BrigAccess m [(UserId, RichInfo)] @@ -130,7 +129,7 @@ data BrigAccess m a where makeSem ''BrigAccess -getUser :: (Member BrigAccess r) => UserId -> Sem r (Maybe UserAccount) +getUser :: (Member BrigAccess r) => UserId -> Sem r (Maybe User) getUser = fmap listToMaybe . getUsers . pure addLegalHoldClientToUser :: diff --git a/services/galley/src/Galley/Intra/User.hs b/services/galley/src/Galley/Intra/User.hs index 4221e42dbec..8d6c620fd66 100644 --- a/services/galley/src/Galley/Intra/User.hs +++ b/services/galley/src/Galley/Intra/User.hs @@ -37,7 +37,6 @@ where import Bilge hiding (getHeader, host, options, port, statusCode) import Bilge.RPC -import Brig.Types.Intra qualified as Brig import Control.Error hiding (bool, isRight) import Control.Lens (view) import Control.Monad.Catch @@ -198,7 +197,7 @@ chunkify doChunk keys = mconcat <$> (doChunk `mapM` chunks keys) chunks uids = case splitAt maxSize uids of (h, t) -> h : chunks t -- | Calls 'Brig.API.listActivatedAccountsH'. -getUsers :: [UserId] -> App [Brig.UserAccount] +getUsers :: [UserId] -> App [User] getUsers = chunkify $ \uids -> do resp <- call Brig $ diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 1a31fae869e..6fe7bcbd9bd 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -2400,8 +2400,7 @@ getUsersBy keyName = chunkify $ \keys -> do . queryItem keyName users . expect2xx ) - let accounts = fromJust $ responseJsonMaybe @[UserAccount] res - pure $ fmap accountUser accounts + pure $ fromJust $ responseJsonMaybe @[User] res getUsersByHandle :: [Handle.Handle] -> TestM [User] getUsersByHandle = getUsersBy "handles" diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 6ac9a07efae..3d9eca78560 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -77,7 +77,7 @@ import Spar.Orphans () import Spar.Scim hiding (handle) import Spar.Sem.AReqIDStore (AReqIDStore) import Spar.Sem.AssIDStore (AssIDStore) -import Spar.Sem.BrigAccess (BrigAccess) +import Spar.Sem.BrigAccess (BrigAccess, getAccount) import qualified Spar.Sem.BrigAccess as BrigAccess import Spar.Sem.DefaultSsoCode (DefaultSsoCode) import qualified Spar.Sem.DefaultSsoCode as DefaultSsoCode @@ -434,7 +434,7 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co assertEmptyOrPurge teamId page = do forM_ (Cas.result page) $ \(uref, uid) -> do mAccount <- BrigAccess.getAccount NoPendingInvitations uid - let mUserTeam = userTeam . accountUser . account =<< mAccount + let mUserTeam = userTeam =<< mAccount when (mUserTeam == Just teamId) $ do if purge then do @@ -466,7 +466,7 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co idpDoesAuthSelf :: IdP -> UserId -> Sem r Bool idpDoesAuthSelf idp uid = do let idpIssuer = idp ^. SAML.idpMetadata . SAML.edIssuer - mUserIssuer <- (>>= userIssuer) <$> Brig.getBrigUser NoPendingInvitations uid + mUserIssuer <- (>>= userIssuer) <$> getAccount NoPendingInvitations uid pure $ mUserIssuer == Just idpIssuer -- | This handler only does the json parsing, and leaves all authorization checks and diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index 6bd4d9ea1b1..562776433b4 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -75,7 +75,7 @@ import qualified Spar.Intra.BrigApp as Intra import Spar.Options import Spar.Orphans () import Spar.Sem.AReqIDStore (AReqIDStore) -import Spar.Sem.BrigAccess (BrigAccess) +import Spar.Sem.BrigAccess (BrigAccess, getAccount) import qualified Spar.Sem.BrigAccess as BrigAccess import Spar.Sem.GalleyAccess (GalleyAccess) import qualified Spar.Sem.GalleyAccess as GalleyAccess @@ -141,7 +141,7 @@ getUserByUrefUnsafe :: SAML.UserRef -> Sem r (Maybe User) getUserByUrefUnsafe uref = do - maybe (pure Nothing) (Intra.getBrigUser Intra.WithPendingInvitations) =<< SAMLUserStore.get uref + maybe (pure Nothing) (getAccount Intra.WithPendingInvitations) =<< SAMLUserStore.get uref -- FUTUREWORK: Remove and reinstatate getUser, in AuthID refactoring PR getUserIdByScimExternalId :: diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index 31333cf34f1..a059b0232a6 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -144,7 +144,7 @@ createBrigUserNoSAML extId email uid teamid uname locale role = do . json newUser if statusCode resp `elem` [200, 201] - then userId . accountUser <$> parseResponse @UserAccount "brig" resp + then userId <$> parseResponse @User "brig" resp else rethrow "brig" resp updateEmail :: (HasCallStack, MonadSparToBrig m) => UserId -> EmailAddress -> m () @@ -162,7 +162,7 @@ updateEmail buid email = do _ -> rethrow "brig" resp -- | Get a user; returns 'Nothing' if the user was not found or has been deleted. -getBrigUserAccount :: (HasCallStack, MonadSparToBrig m) => HavePendingInvitations -> UserId -> m (Maybe ExtendedUserAccount) +getBrigUserAccount :: (HasCallStack, MonadSparToBrig m) => HavePendingInvitations -> UserId -> m (Maybe User) getBrigUserAccount havePending buid = do resp :: ResponseLBS <- call $ @@ -180,10 +180,10 @@ getBrigUserAccount havePending buid = do case statusCode resp of 200 -> - parseResponse @[ExtendedUserAccount] "brig" resp >>= \case + parseResponse @[User] "brig" resp >>= \case [account] -> pure $ - if userDeleted account.account.accountUser + if userDeleted account then Nothing else Just account _ -> pure Nothing @@ -194,7 +194,7 @@ getBrigUserAccount havePending buid = do -- -- TODO: currently this is not used, but it might be useful later when/if -- @hscim@ stops doing checks during user creation. -getBrigUserByHandle :: (HasCallStack, MonadSparToBrig m) => Handle -> m (Maybe UserAccount) +getBrigUserByHandle :: (HasCallStack, MonadSparToBrig m) => Handle -> m (Maybe User) getBrigUserByHandle handle = do resp :: ResponseLBS <- call $ @@ -203,11 +203,11 @@ getBrigUserByHandle handle = do . queryItem "handles" (toByteString' handle) . queryItem "includePendingInvitations" "true" case statusCode resp of - 200 -> listToMaybe <$> parseResponse @[UserAccount] "brig" resp + 200 -> listToMaybe <$> parseResponse @[User] "brig" resp 404 -> pure Nothing _ -> rethrow "brig" resp -getBrigUserByEmail :: (HasCallStack, MonadSparToBrig m) => EmailAddress -> m (Maybe UserAccount) +getBrigUserByEmail :: (HasCallStack, MonadSparToBrig m) => EmailAddress -> m (Maybe User) getBrigUserByEmail email = do resp :: ResponseLBS <- call $ @@ -217,8 +217,8 @@ getBrigUserByEmail email = do . queryItem "includePendingInvitations" "true" case statusCode resp of 200 -> do - macc <- listToMaybe <$> parseResponse @[UserAccount] "brig" resp - case userEmail . accountUser =<< macc of + macc <- listToMaybe <$> parseResponse @[User] "brig" resp + case userEmail =<< macc of Just email' | email' == email -> pure macc _ -> pure Nothing 404 -> pure Nothing diff --git a/services/spar/src/Spar/Intra/BrigApp.hs b/services/spar/src/Spar/Intra/BrigApp.hs index ec8ed68ed78..f394ed12cbb 100644 --- a/services/spar/src/Spar/Intra/BrigApp.hs +++ b/services/spar/src/Spar/Intra/BrigApp.hs @@ -27,7 +27,6 @@ module Spar.Intra.BrigApp veidFromUserSSOId, mkUserName, HavePendingInvitations (..), - getBrigUser, getBrigUserTeam, getZUsrCheckPerm, authorizeScimTokenManagement, @@ -123,13 +122,10 @@ mkUserName Nothing = ---------------------------------------------------------------------- -getBrigUser :: (HasCallStack, Member BrigAccess r) => HavePendingInvitations -> UserId -> Sem r (Maybe User) -getBrigUser ifpend = ((accountUser . account) <$$>) . BrigAccess.getAccount ifpend - -- | Check that an id maps to an user on brig that is 'Active' (or optionally -- 'PendingInvitation') and has a team id. getBrigUserTeam :: (HasCallStack, Member BrigAccess r) => HavePendingInvitations -> UserId -> Sem r (Maybe TeamId) -getBrigUserTeam ifpend = fmap (userTeam =<<) . getBrigUser ifpend +getBrigUserTeam ifpend = fmap (userTeam =<<) . BrigAccess.getAccount ifpend -- | Pull team id for z-user from brig. Check permission in galley. Return team id. Fail if -- permission check fails or the user is not in status 'Active'. diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index b7985a3c3cf..019c20902a4 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -80,7 +80,7 @@ import Spar.Options import Spar.Scim.Auth () import Spar.Scim.Types import qualified Spar.Scim.Types as ST -import Spar.Sem.BrigAccess (BrigAccess) +import Spar.Sem.BrigAccess (BrigAccess, getAccount) import qualified Spar.Sem.BrigAccess as BrigAccess import Spar.Sem.GalleyAccess as GalleyAccess import Spar.Sem.IdPConfigStore (IdPConfigStore) @@ -799,7 +799,7 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = -- thing that could happen is that foreign users cleanup partially -- deleted users. void . lift $ BrigAccess.deleteUser uid - Just acc@(accountUser . account -> brigUser) -> do + Just brigUser -> do if userTeam brigUser == Just stiTeam then do -- This deletion needs data from the non-deleted User in brig. So, @@ -808,7 +808,7 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = -- that have been deleted in brig. Deleting scim-managed users in brig -- (via the TM app) is blocked, though, so there is no legal way to enter -- that situation. - deleteUserInSpar acc + deleteUserInSpar brigUser void . lift $ BrigAccess.deleteUser uid else do -- if we find the user in another team, we pretend it wasn't even there, to @@ -821,12 +821,12 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = Member ScimExternalIdStore r, Member ScimUserTimesStore r ) => - ExtendedUserAccount -> + User -> Scim.ScimHandler (Sem r) () deleteUserInSpar account = do mIdpConfig <- mapM (lift . IdPConfigStore.getConfig) stiIdP - case Brig.veidFromBrigUser account.account.accountUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) account.emailUnvalidated of + case Brig.veidFromBrigUser account ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) account.userEmailUnvalidated of Left _ -> pure () Right veid -> lift $ do for_ (justThere veid.validScimIdAuthInfo) (SAMLUserStore.delete uid) @@ -923,7 +923,7 @@ assertHandleUnused' msg hndl = assertHandleNotUsedElsewhere :: (Member BrigAccess r) => UserId -> Handle -> Scim.ScimHandler (Sem r) () assertHandleNotUsedElsewhere uid hndl = do - musr <- lift $ Brig.getBrigUser Brig.WithPendingInvitations uid + musr <- lift $ getAccount Brig.WithPendingInvitations uid unless ((userHandle =<< musr) == Just hndl) $ assertHandleUnused' "userName already in use by another wire user" hndl @@ -939,21 +939,21 @@ synthesizeStoredUser :: Member GalleyAccess r, Member ScimUserTimesStore r ) => - ExtendedUserAccount -> + User -> ST.ValidScimId -> Scim.ScimHandler (Sem r) (Scim.StoredUser ST.SparTag) synthesizeStoredUser acc veid = logScim ( logFunction "Spar.Scim.User.synthesizeStoredUser" - . logUser (userId acc.account.accountUser) - . maybe id logHandle acc.account.accountUser.userHandle - . maybe id logTeam acc.account.accountUser.userTeam + . logUser (userId acc) + . maybe id logHandle acc.userHandle + . maybe id logTeam acc.userTeam . maybe id logEmail (justHere $ ST.validScimIdAuthInfo veid) ) logScimUserId $ do - let uid = userId acc.account.accountUser - accStatus = acc.account.accountStatus + let uid = userId acc + accStatus = acc.userStatus let readState :: Sem r (RI.RichInfo, Maybe (UTCTimeMillis, UTCTimeMillis), URIBS.URI, Role) readState = @@ -977,17 +977,17 @@ synthesizeStoredUser acc veid = now <- toUTCTimeMillis <$> lift Now.get let (createdAt, lastUpdatedAt) = fromMaybe (now, now) accessTimes - handle <- lift $ Brig.giveDefaultHandle acc.account.accountUser + handle <- lift $ Brig.giveDefaultHandle acc let emails = maybeToList $ - acc.emailUnvalidated <|> (emailIdentity =<< userIdentity acc.account.accountUser) <|> justHere veid.validScimIdAuthInfo + acc.userEmailUnvalidated <|> (emailIdentity =<< userIdentity acc) <|> justHere veid.validScimIdAuthInfo storedUser <- synthesizeStoredUser' uid veid - (userDisplayName acc.account.accountUser) + acc.userDisplayName emails handle richInfo @@ -995,15 +995,15 @@ synthesizeStoredUser acc veid = createdAt lastUpdatedAt baseuri - (userLocale acc.account.accountUser) + acc.userLocale (Just role) - lift $ writeState accessTimes (userManagedBy acc.account.accountUser) richInfo storedUser + lift $ writeState accessTimes acc.userManagedBy richInfo storedUser pure storedUser where getRole :: Sem r Role getRole = do let tmRoleOrDefault m = fromMaybe defaultRole $ m >>= \member -> member ^. Member.permissions . to Member.permissionsRole - maybe (pure defaultRole) (\tid -> tmRoleOrDefault <$> GalleyAccess.getTeamMember tid (userId acc.account.accountUser)) (userTeam acc.account.accountUser) + maybe (pure defaultRole) (\tid -> tmRoleOrDefault <$> GalleyAccess.getTeamMember tid (userId acc)) (userTeam acc) synthesizeStoredUser' :: (MonadError Scim.ScimError m) => @@ -1075,15 +1075,15 @@ getUserById :: UserId -> MaybeT (Scim.ScimHandler (Sem r)) (Scim.StoredUser ST.SparTag) getUserById midp stiTeam uid = do - acc@(accountUser . account -> brigUser) <- MaybeT . lift $ BrigAccess.getAccount Brig.WithPendingInvitations uid + brigUser <- MaybeT . lift $ BrigAccess.getAccount Brig.WithPendingInvitations uid let mbveid = Brig.veidFromBrigUser brigUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> midp) - acc.emailUnvalidated + brigUser.userEmailUnvalidated case mbveid of Right veid | userTeam brigUser == Just stiTeam -> lift $ do - storedUser :: Scim.StoredUser ST.SparTag <- synthesizeStoredUser acc veid + storedUser :: Scim.StoredUser ST.SparTag <- synthesizeStoredUser brigUser veid -- if we get a user from brig that hasn't been touched by scim yet, we call this -- function to move it under scim control. assertExternalIdNotUsedElsewhere stiTeam veid uid @@ -1123,7 +1123,7 @@ scimFindUserByHandle :: scimFindUserByHandle mIdpConfig stiTeam hndl = do handle <- MaybeT . pure . parseHandle . Text.toLower $ hndl brigUser <- MaybeT . lift . BrigAccess.getByHandle $ handle - getUserById mIdpConfig stiTeam . userId . accountUser $ brigUser + getUserById mIdpConfig stiTeam . userId $ brigUser -- | Construct a 'ValidScimId'. If it is an 'Email', find the non-SAML SCIM user in spar; if -- that fails, find the user by email in brig. If it is a 'UserRef', find the SAML user. @@ -1155,12 +1155,12 @@ scimFindUserByExternalId mIdpConfig stiTeam eid = do -- there are a few ways to find a user. this should all be redundant, especially the where -- we lookup a user from brig by email, throw it away and only keep the uid, and then use -- the uid to lookup the account again. but cassandra, and also reasons. - mViaEmail :: Maybe UserId <- join <$> (for (justHere veid.validScimIdAuthInfo) ((userId . accountUser <$$>) . BrigAccess.getByEmail)) + mViaEmail :: Maybe UserId <- join <$> (for (justHere veid.validScimIdAuthInfo) ((userId <$$>) . BrigAccess.getByEmail)) mViaUref :: Maybe UserId <- join <$> (for (justThere veid.validScimIdAuthInfo) SAMLUserStore.get) pure $ mViaEmail <|> mViaUref Just uid -> pure uid acc <- MaybeT . lift . BrigAccess.getAccount Brig.WithPendingInvitations $ uid - getUserById mIdpConfig stiTeam (userId acc.account.accountUser) + getUserById mIdpConfig stiTeam (userId acc) logFilter :: Filter -> (Msg -> Msg) logFilter (FilterAttrCompare attr op val) = diff --git a/services/spar/src/Spar/Sem/BrigAccess.hs b/services/spar/src/Spar/Sem/BrigAccess.hs index 46d208d1b10..a9f3b69fe5a 100644 --- a/services/spar/src/Spar/Sem/BrigAccess.hs +++ b/services/spar/src/Spar/Sem/BrigAccess.hs @@ -55,18 +55,16 @@ import qualified SAML2.WebSSO as SAML import Web.Cookie import Wire.API.Locale import Wire.API.Team.Role -import Wire.API.User (AccountStatus (..), DeleteUserResult, ExtendedUserAccount, VerificationAction) -import Wire.API.User.Identity -import Wire.API.User.Profile +import Wire.API.User import Wire.API.User.RichInfo as RichInfo data BrigAccess m a where CreateSAML :: SAML.UserRef -> UserId -> TeamId -> Name -> ManagedBy -> Maybe Handle -> Maybe RichInfo -> Maybe Locale -> Role -> BrigAccess m UserId CreateNoSAML :: Text -> EmailAddress -> UserId -> TeamId -> Name -> Maybe Locale -> Role -> BrigAccess m UserId UpdateEmail :: UserId -> EmailAddress -> BrigAccess m () - GetAccount :: HavePendingInvitations -> UserId -> BrigAccess m (Maybe ExtendedUserAccount) - GetByHandle :: Handle -> BrigAccess m (Maybe UserAccount) - GetByEmail :: EmailAddress -> BrigAccess m (Maybe UserAccount) + GetAccount :: HavePendingInvitations -> UserId -> BrigAccess m (Maybe User) + GetByHandle :: Handle -> BrigAccess m (Maybe User) + GetByEmail :: EmailAddress -> BrigAccess m (Maybe User) SetName :: UserId -> Name -> BrigAccess m () SetHandle :: UserId -> Handle {- not 'HandleUpdate'! -} -> BrigAccess m () SetManagedBy :: UserId -> ManagedBy -> BrigAccess m () diff --git a/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs b/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs index 822a4ee99bf..c97ad084a90 100644 --- a/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs +++ b/services/spar/test-integration/Test/Spar/Intra/BrigSpec.hs @@ -25,6 +25,7 @@ import Data.Id (Id (Id), UserId) import qualified Data.UUID as UUID import Imports hiding (head) import qualified Spar.Intra.BrigApp as Intra +import Spar.Sem.BrigAccess (getAccount) import qualified Spar.Sem.BrigAccess as BrigAccess import Test.QuickCheck import Util @@ -45,9 +46,9 @@ spec = do r <- runSpar $ BrigAccess.deleteUser uid liftIO $ r `shouldBe` NoUser - describe "getBrigUser" $ do + describe "getAccount" $ do it "return Nothing if n/a" $ do - musr <- runSpar $ Intra.getBrigUser Intra.WithPendingInvitations (Id . fromJust $ UUID.fromText "29546d9e-ed5b-11ea-8228-c324b1ea1030") + musr <- runSpar $ getAccount Intra.WithPendingInvitations (Id . fromJust $ UUID.fromText "29546d9e-ed5b-11ea-8228-c324b1ea1030") liftIO $ musr `shouldSatisfy` isNothing it "return Just if /a" $ do @@ -60,5 +61,5 @@ spec = do scimUserId <$> createUser tok scimUser uid <- setup - musr <- runSpar $ Intra.getBrigUser Intra.WithPendingInvitations uid + musr <- runSpar $ getAccount Intra.WithPendingInvitations uid liftIO $ musr `shouldSatisfy` isJust diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index f055bc467f5..71a8eaee3fe 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -353,7 +353,7 @@ assertBrigCassandra :: ManagedBy -> TestSpar () assertBrigCassandra uid uref usr (valemail, emailValidated) managedBy = do - runSpar (BrigAccess.getAccount NoPendingInvitations uid) >>= \(Just (account -> acc)) -> liftIO $ do + runSpar (BrigAccess.getAccount NoPendingInvitations uid) >>= \(Just acc) -> liftIO $ do let handle = fromRight errmsg . parseHandleEither $ Scim.User.userName usr where errmsg = error . show . Scim.User.userName $ usr @@ -366,14 +366,12 @@ assertBrigCassandra uid uref usr (valemail, emailValidated) managedBy = do _ -> Nothing - accountStatus acc `shouldBe` Active - userId (accountUser acc) `shouldBe` uid - userHandle (accountUser acc) `shouldBe` Just handle - userDisplayName (accountUser acc) `shouldBe` name - userManagedBy (accountUser acc) `shouldBe` managedBy - - userIdentity (accountUser acc) - `shouldBe` Just (SSOIdentity (UserSSOId uref) email) + userStatus acc `shouldBe` Active + userId acc `shouldBe` uid + userHandle acc `shouldBe` Just handle + userDisplayName acc `shouldBe` name + userManagedBy acc `shouldBe` managedBy + userIdentity acc `shouldBe` Just (SSOIdentity (UserSSOId uref) email) specSuspend :: SpecWith TestEnv specSuspend = do @@ -651,12 +649,11 @@ testCreateUserNoIdP = do do aFewTimes (runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations userid) isJust >>= maybe (pure ()) (error "pending user in brig is visible, even though it should not be") - brigUserAccount <- + brigUser <- aFewTimes (runSpar $ BrigAccess.getAccount Intra.WithPendingInvitations userid) isJust >>= maybe (error "could not find user in brig") pure - let brigUser = brigUserAccount.account.accountUser brigUser `userShouldMatch` WrappedScimStoredUser scimStoredUser - liftIO $ accountStatus brigUserAccount.account `shouldBe` PendingInvitation + liftIO $ brigUser.userStatus `shouldBe` PendingInvitation liftIO $ userEmail brigUser `shouldBe` Just email liftIO $ userManagedBy brigUser `shouldBe` ManagedByScim -- Previous to the change that allowed the external ID to be different from the email, `userSSOId brigUser` was `Nothing`. @@ -699,10 +696,10 @@ testCreateUserNoIdP = do brigUser <- aFewTimes (runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations userid) isJust >>= maybe (error "could not find user in brig") pure - liftIO $ accountStatus brigUser.account `shouldBe` Active - liftIO $ userManagedBy (accountUser brigUser.account) `shouldBe` ManagedByScim - liftIO $ userHandle (accountUser brigUser.account) `shouldBe` Just handle - liftIO $ userSSOId (accountUser brigUser.account) `shouldBe` Just (UserScimExternalId (fromEmail email)) + liftIO $ brigUser.userStatus `shouldBe` Active + liftIO $ userManagedBy brigUser `shouldBe` ManagedByScim + liftIO $ userHandle brigUser `shouldBe` Just handle + liftIO $ userSSOId brigUser `shouldBe` Just (UserScimExternalId (fromEmail email)) susr <- getUser tok userid let usr = Scim.value . Scim.thing $ susr liftIO $ Scim.User.active usr `shouldNotBe` Just (Scim.ScimBool False) @@ -1230,7 +1227,7 @@ testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO = do -- auto-provision user via saml memberWithSSO <- do uid <- loginSsoUserFirstTime idp privCreds - Just usr <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations uid + Just usr <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations uid handle <- nextHandle runSpar $ BrigAccess.setHandle uid handle pure usr @@ -1243,7 +1240,7 @@ testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO = do liftIO $ userManagedBy memberWithSSO `shouldBe` ManagedByWire users <- listUsers tok (Just (filterBy "externalId" externalId)) liftIO $ (scimUserId <$> users) `shouldContain` [memberIdWithSSO] - Just brigUser' <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations memberIdWithSSO + Just brigUser' <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations memberIdWithSSO liftIO $ userManagedBy brigUser' `shouldBe` ManagedByScim where veidToText :: (MonadError String m) => ValidScimId -> m Text @@ -1265,7 +1262,7 @@ testFindTeamSettingsInvitedUserMigratedWithEmailInTeamWithSSO = do users' <- listUsers tok (Just (filterBy "externalId" emailInvited)) liftIO $ (scimUserId <$> users') `shouldContain` [memberIdInvited] - Just brigUserInvited' <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations memberIdInvited + Just brigUserInvited' <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations memberIdInvited liftIO $ userManagedBy brigUserInvited' `shouldBe` ManagedByScim testFindTeamSettingsInvitedUserMigratedWithEmailInTeamWithSSOViaUserId :: TestSpar () @@ -1278,7 +1275,7 @@ testFindTeamSettingsInvitedUserMigratedWithEmailInTeamWithSSOViaUserId = do let memberIdInvited = userId memberInvited _ <- getUser tok memberIdInvited - Just brigUserInvited' <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations memberIdInvited + Just brigUserInvited' <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations memberIdInvited liftIO $ userManagedBy brigUserInvited' `shouldBe` ManagedByScim testFindProvisionedUserNoIdP :: TestSpar () @@ -1299,7 +1296,7 @@ testFindNonProvisionedUserNoIdP findBy = do uid <- userId <$> call (inviteAndRegisterUser (env ^. teBrig) owner teamid email) handle <- nextHandle runSpar $ BrigAccess.setHandle uid handle - Just brigUser <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations uid + Just brigUser <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations uid do -- inspect brig user @@ -1313,7 +1310,7 @@ testFindNonProvisionedUserNoIdP findBy = do do liftIO $ users `shouldBe` [uid] - Just brigUser' <- runSpar $ Intra.getBrigUser Intra.NoPendingInvitations uid + Just brigUser' <- runSpar $ BrigAccess.getAccount Intra.NoPendingInvitations uid liftIO $ userManagedBy brigUser' `shouldBe` ManagedByScim liftIO $ brigUser' `shouldBe` scimifyBrigUserHack brigUser email @@ -1328,7 +1325,7 @@ testListNoDeletedUsers = do -- Delete the user _ <- deleteUser tok userid -- Make sure it is deleted in brig before pulling via SCIM (which would recreate it!) - Nothing <- aFewTimes (runSpar (Intra.getBrigUser Intra.WithPendingInvitations userid)) isNothing + Nothing <- aFewTimes (runSpar (BrigAccess.getAccount Intra.WithPendingInvitations userid)) isNothing -- Get all users users <- listUsers tok (Just (filterForStoredUser storedUser)) -- Check that the user is absent @@ -1400,7 +1397,7 @@ testGetUser = do shouldBeManagedBy :: (HasCallStack) => UserId -> ManagedBy -> TestSpar () shouldBeManagedBy uid flag = do - managedBy <- maybe (error "user not found") userManagedBy <$> runSpar (Intra.getBrigUser Intra.WithPendingInvitations uid) + managedBy <- maybe (error "user not found") userManagedBy <$> runSpar (BrigAccess.getAccount Intra.WithPendingInvitations uid) liftIO $ managedBy `shouldBe` flag -- | This is (roughly) the behavior on develop as well as on the branch where this test was @@ -1459,12 +1456,12 @@ testGetUserWithNoHandle = do uid <- loginSsoUserFirstTime idp privcreds tok <- registerScimToken tid (Just (idp ^. SAML.idpId)) - mhandle :: Maybe Handle <- maybe (error "user not found") userHandle <$> runSpar (Intra.getBrigUser Intra.WithPendingInvitations uid) + mhandle :: Maybe Handle <- maybe (error "user not found") userHandle <$> runSpar (BrigAccess.getAccount Intra.WithPendingInvitations uid) liftIO $ mhandle `shouldSatisfy` isNothing storedUser <- getUser tok uid liftIO $ (Scim.User.displayName . Scim.value . Scim.thing) storedUser `shouldSatisfy` isJust - mhandle' :: Maybe Handle <- aFewTimes (maybe (error "user not found") userHandle <$> runSpar (Intra.getBrigUser Intra.WithPendingInvitations uid)) isJust + mhandle' :: Maybe Handle <- aFewTimes (maybe (error "user not found") userHandle <$> runSpar (BrigAccess.getAccount Intra.WithPendingInvitations uid)) isJust liftIO $ mhandle' `shouldSatisfy` isJust liftIO $ (fromHandle <$> mhandle') `shouldBe` (Just . Scim.User.userName . Scim.value . Scim.thing $ storedUser) @@ -1847,7 +1844,7 @@ testBrigSideIsUpdated = do validScimUser <- runSpar . runScimErrorUnsafe $ validateScimUser' "testBrigSideIsUpdated" (Just idp) 999999 user' - brigUser <- maybe (error "no brig user") pure =<< runSpar (Intra.getBrigUser Intra.WithPendingInvitations userid) + brigUser <- maybe (error "no brig user") pure =<< runSpar (BrigAccess.getAccount Intra.WithPendingInvitations userid) let scimUserWithDefLocale = validScimUser {Spar.Types.locale = Spar.Types.locale validScimUser <|> Just (Locale (Language EN) Nothing)} brigUser `userShouldMatch` scimUserWithDefLocale @@ -2138,7 +2135,7 @@ specDeleteUser = do storedUser <- createUser tok user let uid :: UserId = scimUserId storedUser uref :: SAML.UserRef <- do - mUsr <- runSpar $ Intra.getBrigUser Intra.WithPendingInvitations uid + mUsr <- runSpar $ BrigAccess.getAccount Intra.WithPendingInvitations uid let err = error . ("brig user without UserRef: " <>) . show case (\usr -> Intra.veidFromBrigUser usr Nothing Nothing) <$> mUsr of bad@(Just (Right veid)) -> runValidScimIdEither pure (const $ err bad) veid @@ -2147,7 +2144,7 @@ specDeleteUser = do deleteUser_ (Just tok) (Just uid) spar !!! const 204 === statusCode brigUser :: Maybe User <- - aFewTimes (runSpar $ Intra.getBrigUser Intra.WithPendingInvitations uid) isNothing + aFewTimes (runSpar $ BrigAccess.getAccount Intra.WithPendingInvitations uid) isNothing samlUser :: Maybe UserId <- aFewTimes (getUserIdViaRef' uref) isNothing scimUser <- diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 7f6ae09b7a9..74aacb800cb 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -180,6 +180,7 @@ import Spar.Error (SparError) import qualified Spar.Intra.BrigApp as Intra import Spar.Options import Spar.Run +import Spar.Sem.BrigAccess (getAccount) import qualified Spar.Sem.IdPConfigStore as IdPConfigStore import qualified Spar.Sem.SAMLUserStore as SAMLUserStore import qualified Spar.Sem.ScimExternalIdStore as ScimExternalIdStore @@ -1186,7 +1187,7 @@ getSsoidViaSelf uid = maybe (error "not found") pure =<< getSsoidViaSelf' uid getSsoidViaSelf' :: (HasCallStack) => UserId -> TestSpar (Maybe UserSSOId) getSsoidViaSelf' uid = do - musr <- aFewTimes (runSpar $ Intra.getBrigUser Intra.NoPendingInvitations uid) isJust + musr <- aFewTimes (runSpar $ getAccount Intra.NoPendingInvitations uid) isJust pure $ ssoIdentity =<< (userIdentity =<< musr) getUserIdViaRef :: (HasCallStack) => UserRef -> TestSpar UserId diff --git a/services/spar/test/Test/Spar/Scim/UserSpec.hs b/services/spar/test/Test/Spar/Scim/UserSpec.hs index 5c51ed624de..9d759d600ac 100644 --- a/services/spar/test/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test/Test/Spar/Scim/UserSpec.hs @@ -35,7 +35,7 @@ spec = describe "deleteScimUser" $ do r <- interpretWithBrigAccessMock (mockBrig (withActiveUser acc) AccountDeleted) - (deleteUserAndAssertDeletionInSpar acc.account tokenInfo) + (deleteUserAndAssertDeletionInSpar acc tokenInfo) r `shouldBe` Right () it "is idempotent" $ do tokenInfo <- generate arbitrary @@ -43,7 +43,7 @@ spec = describe "deleteScimUser" $ do r <- interpretWithBrigAccessMock (mockBrig (withActiveUser acc) AccountAlreadyDeleted) - (deleteUserAndAssertDeletionInSpar acc.account tokenInfo) + (deleteUserAndAssertDeletionInSpar acc tokenInfo) r `shouldBe` Right () it "works if there never was an account" $ do uid <- generate arbitrary @@ -75,13 +75,13 @@ deleteUserAndAssertDeletionInSpar :: ] r ) => - UserAccount -> + User -> ScimTokenInfo -> Sem r (Either ScimError ()) deleteUserAndAssertDeletionInSpar acc tokenInfo = do let tid = stiTeam tokenInfo - email = (fromJust . emailIdentity . fromJust . userIdentity . accountUser) acc - uid = (userId . accountUser) acc + email = (fromJust . emailIdentity . fromJust . userIdentity) acc + uid = userId acc ScimExternalIdStore.insert tid (fromEmail email) uid r <- runExceptT $ deleteScimUser tokenInfo uid lr <- ScimExternalIdStore.lookup tid (fromEmail email) @@ -120,7 +120,7 @@ ignoringState f = fmap snd . f mockBrig :: forall (r :: EffectRow) a. (Member (Embed IO) r) => - (UserId -> Maybe ExtendedUserAccount) -> + (UserId -> Maybe User) -> DeleteUserResult -> Sem (BrigAccess ': r) a -> Sem r a @@ -131,30 +131,24 @@ mockBrig lookup_user delete_response = interpret $ \case liftIO $ expectationFailure $ "Unexpected effect (call to brig)" error "Throw error here to avoid implementation of all cases." -withActiveUser :: ExtendedUserAccount -> UserId -> Maybe ExtendedUserAccount +withActiveUser :: User -> UserId -> Maybe User withActiveUser acc uid = - if uid == (userId . accountUser) acc.account + if uid == userId acc then Just acc else Nothing -someActiveUser :: ScimTokenInfo -> IO ExtendedUserAccount +someActiveUser :: ScimTokenInfo -> IO User someActiveUser tokenInfo = do user <- generate arbitrary pure $ - ExtendedUserAccount - { account = - UserAccount - { accountStatus = Active, - accountUser = - user - { userDisplayName = Name "Some User", - userAccentId = defaultAccentId, - userPict = noPict, - userAssets = [], - userHandle = parseHandle "some-handle", - userIdentity = (Just . EmailIdentity . fromJust . emailAddressText) "someone@wire.com", - userTeam = Just $ stiTeam tokenInfo - } - }, - emailUnvalidated = Nothing + user + { userDisplayName = Name "Some User", + userEmailUnvalidated = Nothing, + userAccentId = defaultAccentId, + userStatus = Active, + userPict = noPict, + userAssets = [], + userHandle = parseHandle "some-handle", + userIdentity = (Just . EmailIdentity . fromJust . emailAddressText) "someone@wire.com", + userTeam = Just $ stiTeam tokenInfo } diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 036a2d9d16f..3e915b2a69e 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -207,13 +207,13 @@ suspendUser uid = NoContent <$ Intra.putUserStatus Suspended uid unsuspendUser :: UserId -> Handler NoContent unsuspendUser uid = NoContent <$ Intra.putUserStatus Active uid -usersByEmail :: EmailAddress -> Handler [UserAccount] +usersByEmail :: EmailAddress -> Handler [User] usersByEmail = Intra.getUserProfilesByIdentity -usersByIds :: [UserId] -> Handler [UserAccount] +usersByIds :: [UserId] -> Handler [User] usersByIds = Intra.getUserProfiles . Left -usersByHandles :: [Handle] -> Handler [UserAccount] +usersByHandles :: [Handle] -> Handler [User] usersByHandles = Intra.getUserProfiles . Right ejpdInfoByHandles :: Maybe Bool -> [Handle] -> Handler EJPD.EJPDResponseBody @@ -242,7 +242,7 @@ deleteUser :: UserId -> EmailAddress -> Handler NoContent deleteUser uid email = do usrs <- Intra.getUserProfilesByIdentity email case usrs of - [accountUser -> u] -> + [u] -> if userId u == uid then do info $ userMsg uid . msg (val "Deleting account") @@ -258,7 +258,7 @@ setTeamStatusH status tid = NoContent <$ Intra.setStatusBindingTeam tid status deleteTeam :: TeamId -> Maybe Bool -> Maybe EmailAddress -> Handler NoContent deleteTeam givenTid (fromMaybe False -> False) (Just email) = do acc <- Intra.getUserProfilesByIdentity email >>= handleNoUser . listToMaybe - userTid <- (Intra.getUserBindingTeam . userId . accountUser $ acc) >>= handleNoTeam + userTid <- (Intra.getUserBindingTeam . userId $ acc) >>= handleNoTeam when (givenTid /= userTid) $ throwE bindingTeamMismatch tInfo <- Intra.getTeamInfo givenTid @@ -294,7 +294,7 @@ deleteFromBlacklist email = do getTeamInfoByMemberEmail :: EmailAddress -> Handler TeamInfo getTeamInfoByMemberEmail e = do acc <- Intra.getUserProfilesByIdentity e >>= handleUser . listToMaybe - tid <- (Intra.getUserBindingTeam . userId . accountUser $ acc) >>= handleTeam + tid <- (Intra.getUserBindingTeam . userId $ acc) >>= handleTeam Intra.getTeamInfo tid where handleUser = ifNothing (mkError status404 "no-user" "No such user with that email") @@ -427,7 +427,7 @@ getUserData uid mMaxConvs mMaxNotifs = do consentLog <- (Intra.getUserConsentLog uid <&> toJSON @ConsentLog) `catchE` (pure . String . T.pack . show) - let em = userEmail $ accountUser account + let em = userEmail account marketo <- do let noEmail = MarketoResult $ KeyMap.singleton "results" emptyArray maybe diff --git a/tools/stern/src/Stern/API/Routes.hs b/tools/stern/src/Stern/API/Routes.hs index 3c78d30ebe5..777bd118c5d 100644 --- a/tools/stern/src/Stern/API/Routes.hs +++ b/tools/stern/src/Stern/API/Routes.hs @@ -85,7 +85,7 @@ type SternAPI = :> "users" :> "by-email" :> QueryParam' [Required, Strict, Description "Email address"] "email" EmailAddress - :> Get '[JSON] [UserAccount] + :> Get '[JSON] [User] ) :<|> Named "get-users-by-ids" @@ -93,7 +93,7 @@ type SternAPI = :> "users" :> "by-ids" :> QueryParam' [Required, Strict, Description "List of IDs of the users, separated by comma"] "ids" [UserId] - :> Get '[JSON] [UserAccount] + :> Get '[JSON] [User] ) :<|> Named "get-users-by-handles" @@ -101,7 +101,7 @@ type SternAPI = :> "users" :> "by-handles" :> QueryParam' [Required, Strict, Description "List of Handles of the users, without '@', separated by comma"] "handles" [Handle] - :> Get '[JSON] [UserAccount] + :> Get '[JSON] [User] ) :<|> Named "get-user-connections" diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 6b9f0d0890a..f72649bba90 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -205,13 +205,13 @@ getUsersConnections uids = do info $ msg ("Response" ++ show r) parseResponse (mkError status502 "bad-upstream") r -getUserProfiles :: Either [UserId] [Handle] -> Handler [UserAccount] +getUserProfiles :: Either [UserId] [Handle] -> Handler [User] getUserProfiles uidsOrHandles = do info $ msg "Getting user accounts" b <- asks (.brig) concat <$> mapM (doRequest b) (prepareQS uidsOrHandles) where - doRequest :: Request -> (Request -> Request) -> Handler [UserAccount] + doRequest :: Request -> (Request -> Request) -> Handler [User] doRequest b qry = do r <- catchRpcErrors $ @@ -232,7 +232,7 @@ getUserProfiles uidsOrHandles = do fmap (BS.intercalate "," . map toByteString') . chunksOf 50 -getUserProfilesByIdentity :: EmailAddress -> Handler [UserAccount] +getUserProfilesByIdentity :: EmailAddress -> Handler [User] getUserProfilesByIdentity email = do info $ msg "Getting user accounts by identity" b <- asks (.brig) diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index 1cd947747b8..0e8b84cb5f1 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -182,7 +182,7 @@ testDeleteUser = do (uid, email) <- randomEmailUser do [ua] <- getUsersByIds [uid] - liftIO $ ua.accountStatus @?= Active + liftIO $ ua.userStatus @?= Active deleteUser uid (Left email) do uas <- getUsersByIds [uid] @@ -215,7 +215,7 @@ testDeleteTeam :: TestM () testDeleteTeam = do (uid, tid, _) <- createTeamWithNMembers 10 [ua] <- getUsersByIds [uid] - let email = fromMaybe (error "user has no email") $ emailIdentity =<< ua.accountUser.userIdentity + let email = fromMaybe (error "user has no email") $ emailIdentity =<< ua.userIdentity do info <- getTeamInfo tid liftIO $ info.tiData.tdStatus @?= Team.Active @@ -245,7 +245,7 @@ testGetTeamInfoByMemberEmail :: TestM () testGetTeamInfoByMemberEmail = do (_, tid, member : _) <- createTeamWithNMembers 10 [ua] <- getUsersByIds [member] - let email = fromMaybe (error "user has no email") $ emailIdentity =<< ua.accountUser.userIdentity + let email = fromMaybe (error "user has no email") $ emailIdentity =<< ua.userIdentity info <- getTeamInfoByMemberEmail email liftIO $ (info.tiData.tdTeam ^. teamId) @?= tid @@ -399,13 +399,13 @@ testGetUsersByHandles = do h <- randomHandle void $ setHandle uid h [ua] <- getUsersByHandles h - liftIO $ userId ua.accountUser @?= uid + liftIO $ userId ua @?= uid testGetUsersByEmail :: TestM () testGetUsersByEmail = do (uid, email) <- randomEmailUser [ua] <- getUsersByEmail email - liftIO $ userId ua.accountUser @?= uid + liftIO $ userId ua @?= uid testUnsuspendUser :: TestM () testUnsuspendUser = do @@ -413,18 +413,18 @@ testUnsuspendUser = do void $ postSupendUser uid do [ua] <- getUsersByIds [uid] - liftIO $ ua.accountStatus @?= Suspended + liftIO $ ua.userStatus @?= Suspended void $ postUnsuspendUser uid do [ua] <- getUsersByIds [uid] - liftIO $ ua.accountStatus @?= Active + liftIO $ ua.userStatus @?= Active testSuspendUser :: TestM () testSuspendUser = do uid <- randomUser void $ postSupendUser uid [ua] <- getUsersByIds [uid] - liftIO $ ua.accountStatus @?= Suspended + liftIO $ ua.userStatus @?= Suspended testGetStatus :: TestM () testGetStatus = do @@ -439,7 +439,7 @@ testGetUsersByIds = do uas <- getUsersByIds [uid1, uid2] liftIO $ do length uas @?= 2 - Set.fromList (userId . (.accountUser) <$> uas) @?= Set.fromList [uid1, uid2] + Set.fromList (userId <$> uas) @?= Set.fromList [uid1, uid2] testGetTeamInfo :: TestM () testGetTeamInfo = do @@ -460,14 +460,14 @@ testRevokeIdentity = do do [ua] <- getUsersByEmail email liftIO $ do - ua.accountStatus @?= Active - isJust ua.accountUser.userIdentity @?= True + ua.userStatus @?= Active + isJust ua.userIdentity @?= True void $ revokeIdentity (Left email) do [ua] <- getUsersByEmail email liftIO $ do - ua.accountStatus @?= Active - isJust ua.accountUser.userIdentity @?= False + ua.userStatus @?= Active + isJust ua.userIdentity @?= False testPutEmail :: TestM () testPutEmail = do @@ -494,13 +494,13 @@ getConnections uid = do r <- get (s . paths ["users", toByteString' uid, "connections"] . expect2xx) pure $ responseJsonUnsafe r -getUsersByHandles :: Text -> TestM [UserAccount] +getUsersByHandles :: Text -> TestM [User] getUsersByHandles h = do stern <- view tsStern r <- get (stern . paths ["users", "by-handles"] . queryItem "handles" (cs h) . expect2xx) pure $ responseJsonUnsafe r -getUsersByEmail :: EmailAddress -> TestM [UserAccount] +getUsersByEmail :: EmailAddress -> TestM [User] getUsersByEmail email = do stern <- view tsStern r <- get (stern . paths ["users", "by-email"] . queryItem "email" (toByteString' email) . expect2xx) @@ -521,7 +521,7 @@ getStatus = do stern <- view tsStern get (stern . paths ["i", "status"] . expect2xx) -getUsersByIds :: [UserId] -> TestM [UserAccount] +getUsersByIds :: [UserId] -> TestM [User] getUsersByIds uids = do stern <- view tsStern r <- get (stern . paths ["users", "by-ids"] . queryItem "ids" (toByteString' uids) . expect2xx) From 433cc0d652eb4637c191e687df081426b7d9721b Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 1 Oct 2024 10:05:35 +0200 Subject: [PATCH 096/136] Initial MLS configuration for new teams (#4262) * Add initialConfig to mls flag configuration * Simplify createBindingTeam * Initialise MLS feature flag for new teams * Test mls flag initialisation * Document initialConfig for mls feature flag * Test mls initial configuration when locked * Add CHANGELOG entry * Regenerate nix packages --- changelog.d/2-features/new-teams-mls | 1 + .../src/developer/reference/config-options.md | 13 ++++ integration/integration.cabal | 1 + integration/test/SetupHelpers.hs | 3 +- .../test/Test/FeatureFlags/Initialisation.hs | 68 +++++++++++++++++++ integration/test/Testlib/ModService.hs | 8 ++- integration/test/Testlib/RunServices.hs | 2 +- libs/galley-types/default.nix | 2 + libs/galley-types/galley-types.cabal | 1 + libs/galley-types/src/Galley/Types/Teams.hs | 32 ++++++++- services/galley/src/Galley/API/Teams.hs | 33 +++------ .../galley/src/Galley/API/Teams/Features.hs | 20 +++++- 12 files changed, 152 insertions(+), 32 deletions(-) create mode 100644 changelog.d/2-features/new-teams-mls create mode 100644 integration/test/Test/FeatureFlags/Initialisation.hs diff --git a/changelog.d/2-features/new-teams-mls b/changelog.d/2-features/new-teams-mls new file mode 100644 index 00000000000..97480b3bcc0 --- /dev/null +++ b/changelog.d/2-features/new-teams-mls @@ -0,0 +1 @@ +Add `initialConfig` setting for the `mls` feature flag diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index e6dc72bfa62..9808bfec21e 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -307,6 +307,19 @@ mls: This default configuration can be overriden on a per-team basis through the [feature config API](../developer/features.md) +This flag also supports setting an `initialConfig` value, which is applied when a team is created: + +```yaml +# galley.yaml +mls: + initialConfig: + protocolToggleUsers: [] + defaultProtocol: mls + supportedProtocols: [proteus, mls] # must contain defaultProtocol + allowedCipherSuites: [1] + defaultCipherSuite: 1 +``` + ### MLS End-to-End Identity The MLS end-to-end identity team feature adds an extra level of security and practicability. If turned on, automatic device authentication ensures that team members know they are communicating with people using authenticated devices. Team members get a certificate on all their devices. diff --git a/integration/integration.cabal b/integration/integration.cabal index 0688089db8c..a3989f28e76 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -130,6 +130,7 @@ library Test.FeatureFlags.EnforceFileDownloadLocation Test.FeatureFlags.FileSharing Test.FeatureFlags.GuestLinks + Test.FeatureFlags.Initialisation Test.FeatureFlags.LegalHold Test.FeatureFlags.Mls Test.FeatureFlags.MlsE2EId diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 1502844ac41..43851fd04d7 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -41,8 +41,7 @@ deleteUser user = bindResponse (API.Brig.deleteUser user) $ \resp -> do -- | returns (owner, team id, members) createTeam :: (HasCallStack, MakesValue domain) => domain -> Int -> App (Value, String, [Value]) createTeam domain memberCount = do - res <- createUser domain def {team = True} - owner <- res.json + owner <- createUser domain def {team = True} >>= getJSON 201 tid <- owner %. "team" & asString members <- for [2 .. memberCount] $ \_ -> createTeamMember owner tid pure (owner, tid, members) diff --git a/integration/test/Test/FeatureFlags/Initialisation.hs b/integration/test/Test/FeatureFlags/Initialisation.hs new file mode 100644 index 00000000000..ac84a57ac91 --- /dev/null +++ b/integration/test/Test/FeatureFlags/Initialisation.hs @@ -0,0 +1,68 @@ +module Test.FeatureFlags.Initialisation where + +import API.GalleyInternal +import Control.Monad.Codensity +import Control.Monad.Extra +import Control.Monad.Reader +import SetupHelpers +import Testlib.Prelude +import Testlib.ResourcePool + +testMLSInitialisation :: (HasCallStack) => App () +testMLSInitialisation = do + let override = + def + { galleyCfg = + setField + "settings.featureFlags.mls" + ( object + [ "initialConfig" + .= object + [ "protocolToggleUsers" .= ([] :: [Int]), + "defaultProtocol" .= "mls", + "allowedCipherSuites" .= [1, 2 :: Int], + "defaultCipherSuite" .= (1 :: Int), + "supportedProtocols" .= ["mls", "proteus"] + ] + ] + ) + >=> removeField "settings.featureFlags.mlsMigration" + } + + pool <- asks (.resourcePool) + lowerCodensity do + [resource] <- acquireResources 1 pool + + (alice, aliceTeam) <- lift $ lowerCodensity do + -- start a dynamic backend with default configuration + domain <- startDynamicBackend resource def + + -- create a team + lift do + (alice, tid, _) <- createTeam domain 0 + feat <- getTeamFeature alice tid "mls" >>= getJSON 200 + feat %. "config.defaultProtocol" `shouldMatch` "proteus" + pure (alice, tid) + + lift $ lowerCodensity do + -- now start the backend again, this time with an initial mls + -- configuration set + domain <- startDynamicBackend resource override + + -- a pre-existing team should get the default configuration + lift do + feat <- getTeamFeature alice aliceTeam "mls" >>= getJSON 200 + feat %. "config.defaultProtocol" `shouldMatch` "proteus" + + -- a new team should get the initial mls configuration + lift do + (bob, tid, _) <- createTeam domain 0 + feat <- getTeamFeature bob tid "mls" >>= getJSON 200 + feat %. "config.defaultProtocol" `shouldMatch` "mls" + + -- if the mls feature is locked, the config reverts back to default + void + $ patchTeamFeature bob tid "mls" (object ["lockStatus" .= "locked"]) + >>= getJSON 200 + feat' <- getTeamFeature bob tid "mls" >>= getJSON 200 + feat' %. "config.defaultProtocol" `shouldMatch` "proteus" diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 385a410b10b..379547c4d2b 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -124,11 +124,14 @@ startDynamicBackends beOverrides k = 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 (uncurry startDynamicBackend) (zip resources beOverrides) + void $ + traverseConcurrentlyCodensity + (void . uncurry startDynamicBackend) + (zip resources beOverrides) pure $ map (.berDomain) resources k -startDynamicBackend :: BackendResource -> ServiceOverrides -> Codensity App () +startDynamicBackend :: BackendResource -> ServiceOverrides -> Codensity App String startDynamicBackend resource beOverrides = do let overrides = mconcat @@ -141,6 +144,7 @@ startDynamicBackend resource beOverrides = do beOverrides ] startBackend resource overrides + pure resource.berDomain where setAwsConfigs :: ServiceOverrides setAwsConfigs = diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index e4641a21983..c2ee022185a 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -57,6 +57,6 @@ main = do $ do _modifyEnv <- traverseConcurrentlyCodensity - (\r -> startDynamicBackend r mempty) + (\r -> void $ startDynamicBackend r mempty) [backendA, backendB] liftIO run diff --git a/libs/galley-types/default.nix b/libs/galley-types/default.nix index 4edd7e398d8..05f50010a65 100644 --- a/libs/galley-types/default.nix +++ b/libs/galley-types/default.nix @@ -16,6 +16,7 @@ , lens , lib , memory +, schema-profunctor , sop-core , text , types-common @@ -39,6 +40,7 @@ mkDerivation { imports lens memory + schema-profunctor sop-core text types-common diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index eb99c1afbb8..a3c6cea0cfe 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -80,6 +80,7 @@ library , imports , lens >=4.12 , memory + , schema-profunctor , sop-core , text >=0.11 , types-common >=0.16 diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 5e50181ecd7..28be18a4b0a 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -29,6 +29,8 @@ module Galley.Types.Teams GetFeatureDefaults (..), FeatureDefaults (..), FeatureFlags, + DefaultsInitial (..), + initialFeature, featureDefaults, notTeamMember, findTeamMember, @@ -47,6 +49,7 @@ import Data.ByteString.UTF8 qualified as UTF8 import Data.Default import Data.Id (UserId) import Data.SOP +import Data.Schema qualified as S import Data.Set qualified as Set import Imports import Wire.API.Team.Feature @@ -214,10 +217,10 @@ newtype instance FeatureDefaults SndFactorPasswordChallengeConfig deriving (FromJSON) via Defaults (LockableFeature SndFactorPasswordChallengeConfig) deriving (ParseFeatureDefaults) via OptionalField SndFactorPasswordChallengeConfig -newtype instance FeatureDefaults MLSConfig = MLSDefaults (LockableFeature MLSConfig) +newtype instance FeatureDefaults MLSConfig = MLSDefaults (DefaultsInitial MLSConfig) deriving stock (Eq, Show) deriving newtype (Default, GetFeatureDefaults) - deriving (FromJSON) via Defaults (LockableFeature MLSConfig) + deriving (FromJSON) via DefaultsInitial MLSConfig deriving (ParseFeatureDefaults) via OptionalField MLSConfig data instance FeatureDefaults ExposeInvitationURLsToTeamAdminConfig @@ -328,6 +331,31 @@ instance (FromJSON a) => FromJSON (Defaults a) where parseJSON = withObject "default object" $ \ob -> Defaults <$> (ob .: "defaults") +data DefaultsInitial cfg = DefaultsInitial + { defFeature :: LockableFeature cfg, + initial :: cfg + } + deriving (Eq, Show) + +instance (IsFeatureConfig cfg) => Default (DefaultsInitial cfg) where + def = DefaultsInitial def def + +type instance ConfigOf (DefaultsInitial cfg) = cfg + +instance GetFeatureDefaults (DefaultsInitial cfg) where + featureDefaults1 = defFeature + +instance (IsFeatureConfig cfg) => FromJSON (DefaultsInitial cfg) where + parseJSON = withObject "default with initial" $ \ob -> do + feat <- ob .:? "defaults" .!= def + mc <- + fromMaybe feat.config + <$> A.explicitParseFieldMaybe S.schemaParseJSON ob "initialConfig" + pure $ DefaultsInitial feat mc + +initialFeature :: DefaultsInitial cfg -> LockableFeature cfg +initialFeature d = d.defFeature {config = d.initial} + makeLenses ''TeamCreationTime notTeamMember :: [UserId] -> [TeamMember] -> [UserId] diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index a8e7953feb3..b59465923af 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -86,6 +86,7 @@ import Data.Time.Clock (UTCTime) import Galley.API.Action import Galley.API.Error as Galley import Galley.API.LegalHold.Team +import Galley.API.Teams.Features import Galley.API.Teams.Features.Get import Galley.API.Teams.Notifications qualified as APITeamQueue import Galley.API.Update qualified as API @@ -239,6 +240,8 @@ createNonBindingTeamH _ _ _ = do createBindingTeam :: ( Member NotificationSubsystem r, Member (Input UTCTime) r, + Member (Input Opts) r, + Member TeamFeatureStore r, Member TeamStore r ) => TeamId -> @@ -249,7 +252,13 @@ createBindingTeam tid zusr body = do let owner = Public.mkTeamMember zusr fullPermissions Nothing LH.defUserLegalHoldStatus team <- E.createTeam (Just tid) zusr body.newTeamName body.newTeamIcon body.newTeamIconKey Binding - finishCreateTeam team owner [] Nothing + initialiseTeamFeatures tid + + E.createTeamMember tid owner + now <- input + let e = newEvent tid now (EdTeamCreate team) + pushNotifications + [newPushLocal1 zusr (toJSONObject e) (userRecipient zusr :| [])] pure tid updateTeamStatus :: @@ -1313,28 +1322,6 @@ addTeamMemberInternal tid origin originConn (ntmNewTeamMember -> new) = do APITeamQueue.pushTeamEvent tid e pure sizeBeforeAdd -finishCreateTeam :: - ( Member NotificationSubsystem r, - Member (Input UTCTime) r, - Member TeamStore r - ) => - Team -> - TeamMember -> - [TeamMember] -> - Maybe ConnId -> - Sem r () -finishCreateTeam team owner others zcon = do - let zusr = owner ^. userId - for_ (owner : others) $ - E.createTeamMember (team ^. teamId) - now <- input - let e = newEvent (team ^. teamId) now (EdTeamCreate team) - let r = membersToRecipients Nothing others - pushNotifications - [ newPushLocal1 zusr (toJSONObject e) (userRecipient zusr :| r) - & pushConn .~ zcon - ] - getBindingTeamMembers :: ( Member (ErrorS 'TeamNotFound) r, Member (ErrorS 'NonBindingTeam) r, diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 940b754bd73..56c2ceaddd0 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -30,6 +30,7 @@ module Galley.API.Teams.Features guardSecondFactorDisabled, featureEnabledForTeam, guardMlsE2EIdConfig, + initialiseTeamFeatures, ) where @@ -43,7 +44,7 @@ import Data.Qualified (Local) import Data.Time (UTCTime) import Galley.API.Error (InternalError) import Galley.API.LegalHold qualified as LegalHold -import Galley.API.Teams (ensureNotTooLargeToActivateLegalHold) +import Galley.API.LegalHold.Team qualified as LegalHold import Galley.API.Teams.Features.Get import Galley.API.Util (assertTeamExists, getTeamMembersForFanout, membersToRecipients, permissionCheck) import Galley.App @@ -243,6 +244,21 @@ setFeatureForTeam tid feat = do pushFeatureEvent tid (mkUpdateEvent newFeat) pure newFeat +initialiseTeamFeatures :: + ( Member (Input Opts) r, + Member TeamFeatureStore r + ) => + TeamId -> + Sem r () +initialiseTeamFeatures tid = do + flags :: FeatureFlags <- inputs $ view (settings . featureFlags) + + -- set MLS initial config + let MLSDefaults fdef = npProject flags + let feat = initialFeature fdef + setDbFeature tid feat + pure () + ------------------------------------------------------------------------------- -- SetFeatureConfig instances @@ -349,7 +365,7 @@ instance SetFeatureConfig LegalholdConfig where case feat.status of FeatureStatusDisabled -> LegalHold.removeSettings' @InternalPaging tid - FeatureStatusEnabled -> ensureNotTooLargeToActivateLegalHold tid + FeatureStatusEnabled -> LegalHold.ensureNotTooLargeToActivateLegalHold tid pure feat instance SetFeatureConfig FileSharingConfig From 4569bd3100450338fc08a4b8585f3e6f8bcbb9cf Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 1 Oct 2024 13:54:22 +0200 Subject: [PATCH 097/136] [WPB-11301] db script for collecting last login times of all team members (#4274) Authored-by: Matthias Fischmann --- cabal.project | 1 + .../5-internal/WPB-11301-db-tool-team-info | 1 + nix/local-haskell-packages.nix | 1 + nix/wire-server.nix | 1 + tools/db/team-info/.ormolu | 1 + tools/db/team-info/README.md | 48 +++++++ tools/db/team-info/app/Main.hs | 23 +++ tools/db/team-info/default.nix | 40 ++++++ tools/db/team-info/src/TeamInfo/Lib.hs | 93 ++++++++++++ tools/db/team-info/src/TeamInfo/Types.hs | 134 ++++++++++++++++++ tools/db/team-info/team-info.cabal | 92 ++++++++++++ 11 files changed, 435 insertions(+) create mode 100644 changelog.d/5-internal/WPB-11301-db-tool-team-info create mode 120000 tools/db/team-info/.ormolu create mode 100644 tools/db/team-info/README.md create mode 100644 tools/db/team-info/app/Main.hs create mode 100644 tools/db/team-info/default.nix create mode 100644 tools/db/team-info/src/TeamInfo/Lib.hs create mode 100644 tools/db/team-info/src/TeamInfo/Types.hs create mode 100644 tools/db/team-info/team-info.cabal diff --git a/cabal.project b/cabal.project index ed3bbc74931..61843de1a3a 100644 --- a/cabal.project +++ b/cabal.project @@ -50,6 +50,7 @@ packages: , tools/db/move-team/ , tools/db/phone-users/ , tools/db/repair-handles/ + , tools/db/team-info/ , tools/db/repair-brig-clients-table/ , tools/db/service-backfill/ , tools/fedcalls/ diff --git a/changelog.d/5-internal/WPB-11301-db-tool-team-info b/changelog.d/5-internal/WPB-11301-db-tool-team-info new file mode 100644 index 00000000000..e1cda09aa88 --- /dev/null +++ b/changelog.d/5-internal/WPB-11301-db-tool-team-info @@ -0,0 +1 @@ +tools/db/team-info: collects last login times of all team members \ No newline at end of file diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index 38c381258a4..5a7d488c79a 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -53,6 +53,7 @@ repair-brig-clients-table = hself.callPackage ../tools/db/repair-brig-clients-table/default.nix { inherit gitignoreSource; }; repair-handles = hself.callPackage ../tools/db/repair-handles/default.nix { inherit gitignoreSource; }; service-backfill = hself.callPackage ../tools/db/service-backfill/default.nix { inherit gitignoreSource; }; + team-info = hself.callPackage ../tools/db/team-info/default.nix { inherit gitignoreSource; }; fedcalls = hself.callPackage ../tools/fedcalls/default.nix { inherit gitignoreSource; }; mlsstats = hself.callPackage ../tools/mlsstats/default.nix { inherit gitignoreSource; }; rabbitmq-consumer = hself.callPackage ../tools/rabbitmq-consumer/default.nix { inherit gitignoreSource; }; diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 9f3eed3d4e4..bf1593940f4 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -86,6 +86,7 @@ let integration = [ "integration" ]; rabbitmq-consumer = [ "rabbitmq-consumer" ]; test-stats = [ "test-stats" ]; + team-info = [ "team-info" ]; }; inherit (lib) attrsets; diff --git a/tools/db/team-info/.ormolu b/tools/db/team-info/.ormolu new file mode 120000 index 00000000000..ffc2ca9745e --- /dev/null +++ b/tools/db/team-info/.ormolu @@ -0,0 +1 @@ +../../../.ormolu \ No newline at end of file diff --git a/tools/db/team-info/README.md b/tools/db/team-info/README.md new file mode 100644 index 00000000000..81961bae4de --- /dev/null +++ b/tools/db/team-info/README.md @@ -0,0 +1,48 @@ +# Team info + +This program scans brig's and galley's cassandra for members of a team, their clients, and those clients' last access times. + +Useful for finding out which accounts you don't want to pay license fees any more. + +Example usage: + +```shell +team-info \ + --brig-cassandra-port 9048 --brig-cassandra-keyspace brig \ + --galley-cassandra-port 9049 --galley-cassandra-keyspace galley \ + --team-id=904912aa-7c10-11ef-9c85-8bfd758593f6 +``` + +Display usage: + +```shell +team-info -h +``` + +```text +team-info + +Usage: team-info [--brig-cassandra-host HOST] [--brig-cassandra-port PORT] + [--brig-cassandra-keyspace STRING] + [--galley-cassandra-host HOST] [--galley-cassandra-port PORT] + [--galley-cassandra-keyspace STRING] (-t|--team-id ID) + + get team info + +Available options: + -h,--help Show this help text + --brig-cassandra-host HOST + Cassandra Host for brig (default: "localhost") + --brig-cassandra-port PORT + Cassandra Port for brig (default: 9042) + --brig-cassandra-keyspace STRING + Cassandra Keyspace for brig (default: "brig_test") + --galley-cassandra-host HOST + Cassandra Host for galley (default: "localhost") + --galley-cassandra-port PORT + Cassandra Port for galley (default: 9043) + --galley-cassandra-keyspace STRING + Cassandra Keyspace for galley + (default: "galley_test") + -t,--team-id ID Team ID +``` diff --git a/tools/db/team-info/app/Main.hs b/tools/db/team-info/app/Main.hs new file mode 100644 index 00000000000..46b640ea4b6 --- /dev/null +++ b/tools/db/team-info/app/Main.hs @@ -0,0 +1,23 @@ +-- 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 Main where + +import qualified TeamInfo.Lib as Lib + +main :: IO () +main = Lib.main diff --git a/tools/db/team-info/default.nix b/tools/db/team-info/default.nix new file mode 100644 index 00000000000..d939d1c1fe4 --- /dev/null +++ b/tools/db/team-info/default.nix @@ -0,0 +1,40 @@ +# WARNING: GENERATED FILE, DO NOT EDIT. +# This file is generated by running hack/bin/generate-local-nix-packages.sh and +# must be regenerated whenever local packages are added or removed, or +# dependencies are added or removed. +{ mkDerivation +, base +, cassandra-util +, conduit +, cql +, gitignoreSource +, imports +, lens +, lib +, optparse-applicative +, time +, tinylog +, types-common +}: +mkDerivation { + pname = "team-info"; + version = "1.0.0"; + src = gitignoreSource ./.; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + cassandra-util + conduit + cql + imports + lens + optparse-applicative + time + tinylog + types-common + ]; + executableHaskellDepends = [ base ]; + description = "get team info from cassandra"; + license = lib.licenses.agpl3Only; + mainProgram = "team-info"; +} diff --git a/tools/db/team-info/src/TeamInfo/Lib.hs b/tools/db/team-info/src/TeamInfo/Lib.hs new file mode 100644 index 00000000000..f68fa9fa153 --- /dev/null +++ b/tools/db/team-info/src/TeamInfo/Lib.hs @@ -0,0 +1,93 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- 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 TeamInfo.Lib where + +import Cassandra as C +import Cassandra.Settings as C +import Data.Conduit +import qualified Data.Conduit.Combinators as Conduit +import qualified Data.Conduit.List as CL +import Data.Id (TeamId, UserId) +import Data.Time +import qualified Database.CQL.Protocol as CQL +import Imports +import Options.Applicative +import qualified System.Logger as Log +import TeamInfo.Types + +lookupClientsLastActiveTimestamps :: ClientState -> UserId -> IO [Maybe UTCTime] +lookupClientsLastActiveTimestamps client u = do + runClient client $ runIdentity <$$> retry x1 (query selectClients (params One (Identity u))) + where + selectClients :: PrepQuery R (Identity UserId) (Identity (Maybe UTCTime)) + selectClients = "SELECT last_active from clients where user = ?" + +selectTeamMembers :: ClientState -> TeamId -> ConduitM () [TeamMemberRow] IO () +selectTeamMembers client teamId = + transPipe (runClient client) (paginateC cql (paramsP One (Identity teamId) 1000) x5) + .| Conduit.map (fmap CQL.asRecord) + where + cql :: C.PrepQuery C.R (Identity TeamId) (CQL.TupleType TeamMemberRow) + cql = + "SELECT user, legalhold_status FROM team_member WHERE team = ?" + +lookUpActivity :: ClientState -> TeamMemberRow -> IO TeamMember +lookUpActivity brigClient tmr = do + lastActiveTimestamps <- catMaybes <$> lookupClientsLastActiveTimestamps brigClient tmr.id + if null lastActiveTimestamps + then do + pure $ TeamMember tmr.id tmr.legalhold Nothing + else do + let lastActive = maximum lastActiveTimestamps + pure $ TeamMember tmr.id tmr.legalhold (Just lastActive) + +process :: TeamId -> ClientState -> ClientState -> IO [TeamMember] +process teamId brigClient galleyClient = + runConduit + $ selectTeamMembers galleyClient teamId + .| Conduit.concat + .| Conduit.mapM (lookUpActivity brigClient) + .| CL.consume + +main :: IO () +main = do + opts <- execParser (info (helper <*> optsParser) desc) + logger <- initLogger + brigClient <- initCas opts.brigDb logger + galleyClient <- initCas opts.galleyDb logger + teamMembers <- process opts.teamId brigClient galleyClient + for_ teamMembers $ \tm -> Log.info logger $ Log.msg (show tm) + where + initLogger = + Log.new + . Log.setLogLevel Log.Info + . Log.setOutput Log.StdOut + . Log.setFormat Nothing + . Log.setBufSize 0 + $ Log.defSettings + initCas settings l = + C.init + . C.setLogger (C.mkLogger l) + . C.setContacts settings.host [] + . C.setPortNumber (fromIntegral settings.port) + . C.setKeyspace settings.keyspace + . C.setProtocolVersion C.V4 + $ C.defSettings + desc = header "team-info" <> progDesc "get team info" <> fullDesc diff --git a/tools/db/team-info/src/TeamInfo/Types.hs b/tools/db/team-info/src/TeamInfo/Types.hs new file mode 100644 index 00000000000..e9112a7b1bc --- /dev/null +++ b/tools/db/team-info/src/TeamInfo/Types.hs @@ -0,0 +1,134 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE TemplateHaskell #-} + +-- 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 TeamInfo.Types where + +import Cassandra as C +import Control.Lens +import Data.Id +import Data.LegalHold (UserLegalHoldStatus) +import Data.Text.Strict.Lens +import Data.Time +import Database.CQL.Protocol hiding (Result) +import Imports +import Options.Applicative + +data CassandraSettings = CassandraSettings + { host :: String, + port :: Int, + keyspace :: C.Keyspace + } + +data Opts = Opts + { brigDb :: CassandraSettings, + galleyDb :: CassandraSettings, + teamId :: TeamId + } + +optsParser :: Parser Opts +optsParser = + Opts + <$> brigCassandraParser + <*> galleyCassandraParser + <*> ( option + auto + ( long "team-id" + <> short 't' + <> metavar "ID" + <> help "Team ID" + ) + ) + +galleyCassandraParser :: Parser CassandraSettings +galleyCassandraParser = + CassandraSettings + <$> strOption + ( long "galley-cassandra-host" + <> metavar "HOST" + <> help "Cassandra Host for galley" + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long "galley-cassandra-port" + <> metavar "PORT" + <> help "Cassandra Port for galley" + <> value 9043 + <> showDefault + ) + <*> ( C.Keyspace + . view packed + <$> strOption + ( long "galley-cassandra-keyspace" + <> metavar "STRING" + <> help "Cassandra Keyspace for galley" + <> value "galley_test" + <> showDefault + ) + ) + +brigCassandraParser :: Parser CassandraSettings +brigCassandraParser = + CassandraSettings + <$> strOption + ( long "brig-cassandra-host" + <> metavar "HOST" + <> help "Cassandra Host for brig" + <> value "localhost" + <> showDefault + ) + <*> option + auto + ( long "brig-cassandra-port" + <> metavar "PORT" + <> help "Cassandra Port for brig" + <> value 9042 + <> showDefault + ) + <*> ( C.Keyspace + . view packed + <$> strOption + ( long "brig-cassandra-keyspace" + <> metavar "STRING" + <> help "Cassandra Keyspace for brig" + <> value "brig_test" + <> showDefault + ) + ) + +data TeamMemberRow = TeamMemberRow + { id :: UserId, + legalhold :: Maybe UserLegalHoldStatus + } + deriving (Show, Generic) + +recordInstance ''TeamMemberRow + +data TeamMember = TeamMember + { id :: UserId, + legalhold :: Maybe UserLegalHoldStatus, + lastActive :: Maybe UTCTime + } + deriving (Generic) + +-- output as csv +instance Show TeamMember where + show tm = show tm.id <> "," <> maybe " " show tm.legalhold <> "," <> maybe " " show tm.lastActive diff --git a/tools/db/team-info/team-info.cabal b/tools/db/team-info/team-info.cabal new file mode 100644 index 00000000000..c96cb3485c1 --- /dev/null +++ b/tools/db/team-info/team-info.cabal @@ -0,0 +1,92 @@ +cabal-version: 3.0 +name: team-info +version: 1.0.0 +synopsis: get team info from cassandra +category: Network +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2024 Wire Swiss GmbH +license: AGPL-3.0-only +build-type: Simple + +library + hs-source-dirs: src + exposed-modules: + TeamInfo.Lib + TeamInfo.Types + + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints -Wunused-packages + + build-depends: + , cassandra-util + , conduit + , cql + , imports + , lens + , optparse-applicative + , time + , tinylog + , types-common + + default-extensions: + AllowAmbiguousTypes + BangPatterns + ConstraintKinds + DataKinds + DefaultSignatures + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + DerivingVia + DuplicateRecordFields + EmptyCase + FlexibleContexts + FlexibleInstances + FunctionalDependencies + GADTs + GeneralizedNewtypeDeriving + InstanceSigs + KindSignatures + LambdaCase + MultiParamTypeClasses + MultiWayIf + NamedFieldPuns + NoImplicitPrelude + OverloadedLabels + OverloadedRecordDot + OverloadedStrings + PackageImports + PatternSynonyms + PolyKinds + QuasiQuotes + RankNTypes + RecordWildCards + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeFamilyDependencies + TypeOperators + UndecidableInstances + ViewPatterns + +executable team-info + main-is: Main.hs + build-depends: + , base + , team-info + + hs-source-dirs: app + default-language: Haskell2010 + ghc-options: + -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates + -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + -funbox-strict-fields -threaded -with-rtsopts=-N + -Wredundant-constraints -Wunused-packages From 8cc863f67a282a049b10d5adff9a37465aa40c9c Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Tue, 1 Oct 2024 14:57:02 +0200 Subject: [PATCH 098/136] Gundeck internal API swagger (#4247) --- cabal.project | 1 - .../5-internal/gundeck-internal-swagger | 1 + .../api-client-perspective/swagger.md | 2 + libs/gundeck-types/.ormolu | 1 - libs/gundeck-types/LICENSE | 661 ------------------ libs/gundeck-types/default.nix | 42 -- libs/gundeck-types/gundeck-types.cabal | 86 --- .../gundeck-types/src/Gundeck/Types/Common.hs | 69 -- libs/gundeck-types/src/Gundeck/Types/Event.hs | 43 -- .../src/Gundeck/Types/Presence.hs | 69 -- libs/wire-api/default.nix | 2 + libs/wire-api/src/Wire/API/CannonId.hs | 18 + libs/wire-api/src/Wire/API/Event/Gundeck.hs | 24 + libs/wire-api/src/Wire/API/Presence.hs | 98 +++ .../src/Wire/API}/Push/V2.hs | 232 +++--- .../src/Wire/API/Routes/Internal/Gundeck.hs | 102 +++ .../golden/Test/Wire/API/Golden/FromJSON.hs | 5 +- .../golden/Test/Wire/API/Golden/Manual.hs | 24 + .../Test/Wire/API/Golden/Manual/CannonId.hs} | 17 +- .../Test/Wire/API/Golden/Manual/Presence.hs | 58 ++ .../Test/Wire/API/Golden/Manual/Push.hs | 82 +++ .../Wire/API/Golden/Manual/PushRemove.hs} | 12 +- .../fromJSON/testObject_Presence_3.json | 6 + .../test/golden/testObject_CannonId_1.json | 1 + .../test/golden/testObject_CannonId_2.json | 1 + .../test/golden/testObject_CannonId_3.json | 1 + .../test/golden/testObject_Presence_1.json | 7 + .../test/golden/testObject_Presence_2.json | 7 + .../test/golden/testObject_PushRemove_1.json | 9 + .../test/golden/testObject_Push_1.json | 13 + .../test/golden/testObject_Push_2.json | 52 ++ libs/wire-api/wire-api.cabal | 10 + libs/wire-subsystems/default.nix | 3 - .../src/Wire/GundeckAPIAccess.hs | 2 +- .../src/Wire/NotificationSubsystem.hs | 2 +- .../Wire/NotificationSubsystem/Interpreter.hs | 4 +- .../NotificationSubsystem/InterpreterSpec.hs | 2 +- libs/wire-subsystems/wire-subsystems.cabal | 2 - nix/local-haskell-packages.nix | 1 - services/brig/brig.cabal | 1 - services/brig/default.nix | 2 - services/brig/src/Brig/API/Federation.hs | 2 +- services/brig/src/Brig/API/Public.hs | 10 +- services/brig/src/Brig/API/Public/Swagger.hs | 1 + services/brig/src/Brig/IO/Intra.hs | 4 +- services/cannon/cannon.cabal | 1 - services/cannon/default.nix | 2 - services/cannon/src/Cannon/WS.hs | 2 +- services/galley/default.nix | 2 - services/galley/galley.cabal | 1 - services/galley/src/Galley/API/Action.hs | 2 +- services/galley/src/Galley/API/Create.hs | 2 +- services/galley/src/Galley/API/Federation.hs | 2 +- services/galley/src/Galley/API/Internal.hs | 2 +- .../galley/src/Galley/API/MLS/Propagate.hs | 2 +- services/galley/src/Galley/API/MLS/Welcome.hs | 2 +- services/galley/src/Galley/API/Push.hs | 2 +- services/galley/src/Galley/API/Util.hs | 2 +- services/gundeck/default.nix | 6 +- services/gundeck/gundeck.cabal | 5 +- services/gundeck/src/Gundeck/API/Internal.hs | 71 +- services/gundeck/src/Gundeck/Aws.hs | 4 +- services/gundeck/src/Gundeck/Aws/Arn.hs | 2 +- services/gundeck/src/Gundeck/Instances.hs | 2 +- services/gundeck/src/Gundeck/Presence.hs | 7 +- services/gundeck/src/Gundeck/Presence/Data.hs | 2 +- services/gundeck/src/Gundeck/Push.hs | 5 +- services/gundeck/src/Gundeck/Push/Data.hs | 2 +- services/gundeck/src/Gundeck/Push/Native.hs | 3 +- .../src/Gundeck/Push/Native/Serialise.hs | 2 +- .../gundeck/src/Gundeck/Push/Native/Types.hs | 2 +- .../gundeck/src/Gundeck/Push/Websocket.hs | 2 +- services/gundeck/src/Gundeck/React.hs | 3 +- services/gundeck/src/Gundeck/Run.hs | 10 +- services/gundeck/test/bench/Main.hs | 2 +- services/gundeck/test/integration/API.hs | 7 +- services/gundeck/test/unit/Aws/Arn.hs | 2 +- services/gundeck/test/unit/Json.hs | 2 +- services/gundeck/test/unit/MockGundeck.hs | 3 +- services/gundeck/test/unit/Native.hs | 2 +- .../gundeck/test/unit/ParseExistsError.hs | 2 +- services/gundeck/test/unit/Push.hs | 3 +- 82 files changed, 721 insertions(+), 1246 deletions(-) create mode 100644 changelog.d/5-internal/gundeck-internal-swagger delete mode 120000 libs/gundeck-types/.ormolu delete mode 100644 libs/gundeck-types/LICENSE delete mode 100644 libs/gundeck-types/default.nix delete mode 100644 libs/gundeck-types/gundeck-types.cabal delete mode 100644 libs/gundeck-types/src/Gundeck/Types/Common.hs delete mode 100644 libs/gundeck-types/src/Gundeck/Types/Event.hs delete mode 100644 libs/gundeck-types/src/Gundeck/Types/Presence.hs create mode 100644 libs/wire-api/src/Wire/API/CannonId.hs create mode 100644 libs/wire-api/src/Wire/API/Event/Gundeck.hs create mode 100644 libs/wire-api/src/Wire/API/Presence.hs rename libs/{gundeck-types/src/Gundeck/Types => wire-api/src/Wire/API}/Push/V2.hs (59%) create mode 100644 libs/wire-api/src/Wire/API/Routes/Internal/Gundeck.hs rename libs/{gundeck-types/src/Gundeck/Types/Push.hs => wire-api/test/golden/Test/Wire/API/Golden/Manual/CannonId.hs} (67%) create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Push.hs rename libs/{gundeck-types/src/Gundeck/Types.hs => wire-api/test/golden/Test/Wire/API/Golden/Manual/PushRemove.hs} (75%) create mode 100644 libs/wire-api/test/golden/fromJSON/testObject_Presence_3.json create mode 100644 libs/wire-api/test/golden/testObject_CannonId_1.json create mode 100644 libs/wire-api/test/golden/testObject_CannonId_2.json create mode 100644 libs/wire-api/test/golden/testObject_CannonId_3.json create mode 100644 libs/wire-api/test/golden/testObject_Presence_1.json create mode 100644 libs/wire-api/test/golden/testObject_Presence_2.json create mode 100644 libs/wire-api/test/golden/testObject_PushRemove_1.json create mode 100644 libs/wire-api/test/golden/testObject_Push_1.json create mode 100644 libs/wire-api/test/golden/testObject_Push_2.json diff --git a/cabal.project b/cabal.project index 61843de1a3a..ca8e1277db5 100644 --- a/cabal.project +++ b/cabal.project @@ -11,7 +11,6 @@ packages: , libs/dns-util/ , libs/deriving-swagger2/ , libs/galley-types/ - , libs/gundeck-types/ , libs/hscim/ , libs/http2-manager/ , libs/imports/ diff --git a/changelog.d/5-internal/gundeck-internal-swagger b/changelog.d/5-internal/gundeck-internal-swagger new file mode 100644 index 00000000000..da4ac4f9e1f --- /dev/null +++ b/changelog.d/5-internal/gundeck-internal-swagger @@ -0,0 +1 @@ +Expose gundeck internal API on swagger. Mv some types and routes to wire-api. \ 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 b466fc7f8b8..773a89fe071 100644 --- a/docs/src/understand/api-client-perspective/swagger.md +++ b/docs/src/understand/api-client-perspective/swagger.md @@ -75,6 +75,8 @@ Internal APIs are not under version control. 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) diff --git a/libs/gundeck-types/.ormolu b/libs/gundeck-types/.ormolu deleted file mode 120000 index 157b212d7cd..00000000000 --- a/libs/gundeck-types/.ormolu +++ /dev/null @@ -1 +0,0 @@ -../../.ormolu \ No newline at end of file diff --git a/libs/gundeck-types/LICENSE b/libs/gundeck-types/LICENSE deleted file mode 100644 index dba13ed2ddf..00000000000 --- a/libs/gundeck-types/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - 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 . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/libs/gundeck-types/default.nix b/libs/gundeck-types/default.nix deleted file mode 100644 index 6c440616b9a..00000000000 --- a/libs/gundeck-types/default.nix +++ /dev/null @@ -1,42 +0,0 @@ -# WARNING: GENERATED FILE, DO NOT EDIT. -# This file is generated by running hack/bin/generate-local-nix-packages.sh and -# must be regenerated whenever local packages are added or removed, or -# dependencies are added or removed. -{ mkDerivation -, aeson -, attoparsec -, base -, bytestring -, bytestring-conversion -, containers -, gitignoreSource -, imports -, lens -, lib -, network-uri -, servant -, text -, types-common -, wire-api -}: -mkDerivation { - pname = "gundeck-types"; - version = "1.45.0"; - src = gitignoreSource ./.; - libraryHaskellDepends = [ - aeson - attoparsec - base - bytestring - bytestring-conversion - containers - imports - lens - network-uri - servant - text - types-common - wire-api - ]; - license = lib.licenses.agpl3Only; -} diff --git a/libs/gundeck-types/gundeck-types.cabal b/libs/gundeck-types/gundeck-types.cabal deleted file mode 100644 index 86c33f01a4a..00000000000 --- a/libs/gundeck-types/gundeck-types.cabal +++ /dev/null @@ -1,86 +0,0 @@ -cabal-version: 1.12 -name: gundeck-types -version: 1.45.0 -description: API types of Gundeck. -category: Network -author: Wire Swiss GmbH -maintainer: Wire Swiss GmbH -copyright: (c) 2017 Wire Swiss GmbH -license: AGPL-3 -license-file: LICENSE -build-type: Simple - -library - exposed-modules: - Gundeck.Types - Gundeck.Types.Common - Gundeck.Types.Event - Gundeck.Types.Presence - Gundeck.Types.Push - Gundeck.Types.Push.V2 - - other-modules: Paths_gundeck_types - hs-source-dirs: src - default-extensions: - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - NoImplicitPrelude - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -Wredundant-constraints -Wunused-packages - - build-depends: - aeson >=2.0.1.0 - , attoparsec >=0.10 - , base >=4 && <5 - , bytestring >=0.10 - , bytestring-conversion >=0.2 - , containers >=0.5 - , imports - , lens >=4.11 - , network-uri >=2.6 - , servant - , text >=0.11 - , types-common >=0.16 - , wire-api - - default-language: GHC2021 diff --git a/libs/gundeck-types/src/Gundeck/Types/Common.hs b/libs/gundeck-types/src/Gundeck/Types/Common.hs deleted file mode 100644 index 6649db94ba3..00000000000 --- a/libs/gundeck-types/src/Gundeck/Types/Common.hs +++ /dev/null @@ -1,69 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - --- 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 Gundeck.Types.Common where - -import Data.Aeson -import Data.Attoparsec.ByteString (takeByteString) -import Data.ByteString.Char8 qualified as Bytes -import Data.ByteString.Conversion -import Data.Text qualified as Text -import Data.Text.Encoding (decodeUtf8) -import Imports -import Network.URI qualified as Net -import Servant.API (FromHttpApiData (parseUrlPiece), ToHttpApiData (toUrlPiece)) - -newtype CannonId = CannonId - { cannonId :: Text - } - deriving - ( Eq, - Ord, - Show, - FromJSON, - ToJSON, - FromByteString, - ToByteString - ) - -instance FromHttpApiData CannonId where - parseUrlPiece = pure . CannonId - -newtype URI = URI - { fromURI :: Net.URI - } - deriving (Eq, Ord, Show) - -instance FromJSON URI where - parseJSON = withText "URI" (parse . Text.unpack) - -instance ToJSON URI where - toJSON uri = String $ Text.pack (show (fromURI uri)) - -instance ToByteString URI where - builder = builder . show . fromURI - -instance FromByteString URI where - parser = takeByteString >>= parse . Bytes.unpack - -instance ToHttpApiData URI where - toUrlPiece = decodeUtf8 . toByteString' - -parse :: (MonadFail m) => String -> m URI -parse = maybe (fail "Invalid URI") (pure . URI) . Net.parseURI diff --git a/libs/gundeck-types/src/Gundeck/Types/Event.hs b/libs/gundeck-types/src/Gundeck/Types/Event.hs deleted file mode 100644 index a5d1ebf688e..00000000000 --- a/libs/gundeck-types/src/Gundeck/Types/Event.hs +++ /dev/null @@ -1,43 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- 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 Gundeck.Types.Event where - -import Data.Aeson -import Data.Aeson.KeyMap qualified as KeyMap -import Data.Json.Util -import Gundeck.Types.Push -import Imports - -newtype PushRemove = PushRemove PushToken - deriving (Eq, Show) - -instance FromJSON PushRemove where - parseJSON = withObject "push-removed object" $ \o -> - PushRemove <$> o .: "token" - -instance ToJSON PushRemove where - toJSON = Object . toJSONObject - -instance ToJSONObject PushRemove where - toJSONObject (PushRemove t) = - KeyMap.fromList - [ "type" .= ("user.push-remove" :: Text), - "token" .= t - ] diff --git a/libs/gundeck-types/src/Gundeck/Types/Presence.hs b/libs/gundeck-types/src/Gundeck/Types/Presence.hs deleted file mode 100644 index 04aa78b28ca..00000000000 --- a/libs/gundeck-types/src/Gundeck/Types/Presence.hs +++ /dev/null @@ -1,69 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} - --- 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 Gundeck.Types.Presence - ( module Gundeck.Types.Presence, - module Common, - ) -where - -import Data.Aeson -import Data.ByteString.Lazy qualified as Lazy -import Data.Id -import Data.Misc (Milliseconds) -import Gundeck.Types.Common as Common -import Imports - --- | This is created in gundeck by cannon every time the client opens a new websocket connection. --- (That's why we always have a 'ConnId' from the most recent connection by that client.) -data Presence = Presence - { userId :: !UserId, - connId :: !ConnId, - -- | cannon instance hosting the presence - resource :: !URI, - -- | This is 'Nothing' if either (a) the presence is older - -- than mandatory end-to-end encryption, or (b) the client is - -- operating the team settings pages without the need for - -- end-to-end crypto. - clientId :: !(Maybe ClientId), - createdAt :: !Milliseconds, - -- | REFACTOR: temp. addition to ease migration - __field :: !Lazy.ByteString - } - deriving (Eq, Ord, Show) - -instance ToJSON Presence where - toJSON p = - object - [ "user_id" .= userId p, - "device_id" .= connId p, - "resource" .= resource p, - "client_id" .= clientId p, - "created_at" .= createdAt p - ] - -instance FromJSON Presence where - parseJSON = withObject "Presence" $ \o -> - Presence - <$> o .: "user_id" - <*> o .: "device_id" - <*> o .: "resource" - <*> o .:? "client_id" - <*> o .:? "created_at" .!= 0 - <*> pure "" diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 1f765bee115..2fd02d1acf6 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -63,6 +63,7 @@ , metrics-wai , mime , mtl +, network-uri , openapi3 , pem , polysemy @@ -169,6 +170,7 @@ mkDerivation { metrics-wai mime mtl + network-uri openapi3 pem polysemy diff --git a/libs/wire-api/src/Wire/API/CannonId.hs b/libs/wire-api/src/Wire/API/CannonId.hs new file mode 100644 index 00000000000..fb537887876 --- /dev/null +++ b/libs/wire-api/src/Wire/API/CannonId.hs @@ -0,0 +1,18 @@ +module Wire.API.CannonId where + +import Data.Aeson +import Data.OpenApi +import Data.Proxy +import Imports +import Web.HttpApiData + +newtype CannonId = CannonId + { cannonId :: Text + } + deriving (Eq, Ord, Show, FromJSON, ToJSON) + +instance ToParamSchema CannonId where + toParamSchema _ = toParamSchema (Proxy @Text) + +instance FromHttpApiData CannonId where + parseUrlPiece = pure . CannonId diff --git a/libs/wire-api/src/Wire/API/Event/Gundeck.hs b/libs/wire-api/src/Wire/API/Event/Gundeck.hs new file mode 100644 index 00000000000..86ec39c2e27 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Event/Gundeck.hs @@ -0,0 +1,24 @@ +module Wire.API.Event.Gundeck where + +import Data.Aeson +import Data.Aeson.KeyMap qualified as KeyMap +import Data.Json.Util +import Imports +import Wire.API.Push.V2.Token + +newtype PushRemove = PushRemove PushToken + deriving (Eq, Show) + +instance FromJSON PushRemove where + parseJSON = withObject "push-removed object" $ \o -> + PushRemove <$> o .: "token" + +instance ToJSON PushRemove where + toJSON = Object . toJSONObject + +instance ToJSONObject PushRemove where + toJSONObject (PushRemove t) = + KeyMap.fromList + [ "type" .= ("user.push-remove" :: Text), + "token" .= t + ] diff --git a/libs/wire-api/src/Wire/API/Presence.hs b/libs/wire-api/src/Wire/API/Presence.hs new file mode 100644 index 00000000000..d2e8cb6a3d0 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Presence.hs @@ -0,0 +1,98 @@ +module Wire.API.Presence (Presence (..), URI (..), parse) where + +import Control.Lens ((?~)) +import Data.Aeson qualified as A +import Data.Aeson.Types qualified as A +import Data.Attoparsec.ByteString (takeByteString) +import Data.ByteString.Char8 qualified as Bytes +import Data.ByteString.Conversion +import Data.ByteString.Lazy qualified as Lazy +import Data.Id +import Data.Misc (Milliseconds) +import Data.OpenApi qualified as S +import Data.Proxy +import Data.Schema +import Data.Text qualified as Text +import Data.Text.Encoding (decodeUtf8) +import Imports +import Network.URI qualified as Net +import Servant.API (ToHttpApiData (toUrlPiece)) + +-- FUTUREWORK: use Network.URI and toss this newtype. servant should have all these instances for us these days. +newtype URI = URI + { fromURI :: Net.URI + } + deriving (Eq, Ord, Show) + +instance A.FromJSON URI where + parseJSON = A.withText "URI" (parse . Text.unpack) + +instance A.ToJSON URI where + toJSON uri = A.String $ Text.pack (show (fromURI uri)) + +instance ToByteString URI where + builder = builder . show . fromURI + +instance FromByteString URI where + parser = takeByteString >>= parse . Bytes.unpack + +instance ToHttpApiData URI where + toUrlPiece = decodeUtf8 . toByteString' + +instance S.ToParamSchema URI where + toParamSchema _ = + S.toParamSchema (Proxy @Text) + & S.type_ ?~ S.OpenApiString + & S.description ?~ "Valid URI" + +parse :: (MonadFail m) => String -> m URI +parse = maybe (fail "Invalid URI") (pure . URI) . Net.parseURI + +-- | This is created in gundeck by cannon every time the client opens a new websocket connection. +-- (That's why we always have a 'ConnId' from the most recent connection by that client.) +data Presence = Presence + { userId :: !UserId, + connId :: !ConnId, + -- | cannon instance hosting the presence + resource :: !URI, + -- | This is 'Nothing' if either (a) the presence is older + -- than mandatory end-to-end encryption, or (b) the client is + -- operating the team settings pages without the need for + -- end-to-end crypto. + clientId :: !(Maybe ClientId), + createdAt :: !Milliseconds, + -- | REFACTOR: temp. addition to ease migration + __field :: !Lazy.ByteString + } + deriving (Eq, Ord, Show) + deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema Presence) + +instance ToSchema Presence where + schema = + object "Presence" $ + ( Presence + <$> userId .= field "user_id" schema + <*> connId .= field "device_id" schema + <*> resource .= field "resource" uriSchema + <*> clientId .= optField "client_id" (maybeWithDefault A.Null schema) -- keep null for backwards compat + <*> createdAt .= (fromMaybe 0 <$> (optField "created_at" schema)) + ) + <&> ($ ("" :: Lazy.ByteString)) + +uriSchema :: ValueSchema NamedSwaggerDoc URI +uriSchema = mkSchema desc uriFromJSON (Just . uriToJSON) + where + desc :: NamedSwaggerDoc + desc = + swaggerDoc @Text + & (S.schema . S.type_ ?~ S.OpenApiString) + & (S.schema . S.description ?~ "Valid URI.") + +uriFromJSON :: A.Value -> A.Parser URI +uriFromJSON = A.withText "URI" (p . Text.unpack) + where + p :: (MonadFail m) => String -> m URI + p = maybe (fail "Invalid URI") pure . parse + +uriToJSON :: URI -> A.Value +uriToJSON (URI uri) = A.String . Text.pack $ Net.uriToString id uri mempty diff --git a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs b/libs/wire-api/src/Wire/API/Push/V2.hs similarity index 59% rename from libs/gundeck-types/src/Gundeck/Types/Push/V2.hs rename to libs/wire-api/src/Wire/API/Push/V2.hs index b9539ae81ac..1d24c9099d1 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs +++ b/libs/wire-api/src/Wire/API/Push/V2.hs @@ -1,28 +1,10 @@ -{-# LANGUAGE DataKinds #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} --- 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 Gundeck.Types.Push.V2 +module Wire.API.Push.V2 ( Push (..), newPush, pushRecipients, @@ -71,13 +53,17 @@ module Gundeck.Types.Push.V2 ) where -import Control.Lens (makeLenses) -import Data.Aeson +import Control.Lens (makeLenses, (?~)) +import Data.Aeson (FromJSON (..), Object, ToJSON (..)) +import Data.Aeson qualified as A +import Data.Aeson.Types qualified as A import Data.Id import Data.Json.Util import Data.List1 import Data.List1 qualified as List1 +import Data.OpenApi qualified as S import Data.Range +import Data.Schema import Data.Set qualified as Set import Imports import Wire.API.Message (Priority (..)) @@ -96,25 +82,28 @@ data Route RouteDirect deriving (Eq, Ord, Enum, Bounded, Show, Generic) deriving (Arbitrary) via GenericUniform Route + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Route) -instance FromJSON Route where - parseJSON (String "any") = pure RouteAny - parseJSON (String "direct") = pure RouteDirect - parseJSON x = fail $ "Invalid routing: " ++ show (encode x) - -instance ToJSON Route where - toJSON RouteAny = String "any" - toJSON RouteDirect = String "direct" +instance ToSchema Route where + schema = + enum @Text "Route" $ + mconcat + [ element "any" RouteAny, + element "direct" RouteDirect + ] ----------------------------------------------------------------------------- -- Recipient +-- FUTUREWORK: this is a duplicate of the type in "Wire.NotificationSubsystem" (even though +-- the latter lacks a few possibly deprecated fields). consolidate! data Recipient = Recipient { _recipientId :: !UserId, _recipientRoute :: !Route, _recipientClients :: !RecipientClients } deriving (Show, Eq, Ord, Generic) + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Recipient) data RecipientClients = -- | All clients of some user @@ -123,6 +112,7 @@ data RecipientClients RecipientClientsSome (List1 ClientId) deriving (Eq, Show, Ord, Generic) deriving (Arbitrary) via GenericUniform RecipientClients + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema RecipientClients) instance Semigroup RecipientClients where RecipientClientsAll <> _ = RecipientClientsAll @@ -130,51 +120,73 @@ instance Semigroup RecipientClients where RecipientClientsSome cs1 <> RecipientClientsSome cs2 = RecipientClientsSome (cs1 <> cs2) +instance ToSchema Recipient where + schema = + object "Recipient" $ + Recipient + <$> _recipientId .= field "user_id" schema + <*> _recipientRoute .= field "route" schema + <*> _recipientClients .= field "clients" schema + +instance ToSchema RecipientClients where + schema = mkSchema d i o + where + d :: NamedSwaggerDoc + d = + swaggerDoc @[ClientId] + & (S.schema . S.type_ ?~ S.OpenApiArray) + & (S.schema . S.description ?~ "List of clientIds. Empty means `all clients`.") + + i :: A.Value -> A.Parser RecipientClients + i v = + parseJSON @[ClientId] v >>= \case + [] -> pure RecipientClientsAll + c : cs -> pure (RecipientClientsSome (list1 c cs)) + + o :: RecipientClients -> Maybe A.Value + o = + pure . toJSON . \case + RecipientClientsSome cs -> toList cs + RecipientClientsAll -> [] + makeLenses ''Recipient recipient :: UserId -> Route -> Recipient recipient u r = Recipient u r RecipientClientsAll -instance FromJSON Recipient where - parseJSON = withObject "Recipient" $ \p -> - Recipient - <$> p .: "user_id" - <*> p .: "route" - <*> p .:? "clients" .!= RecipientClientsAll - -instance ToJSON Recipient where - toJSON (Recipient u r c) = - object $ - "user_id" - .= u - # "route" - .= r - # "clients" - .= c - # [] - --- "All clients" is encoded in the API as an empty list. -instance FromJSON RecipientClients where - parseJSON x = - parseJSON @[ClientId] x >>= \case - [] -> pure RecipientClientsAll - c : cs -> pure (RecipientClientsSome (list1 c cs)) - -instance ToJSON RecipientClients where - toJSON = - toJSON . \case - RecipientClientsAll -> [] - RecipientClientsSome cs -> toList cs - ----------------------------------------------------------------------------- -- ApsData newtype ApsSound = ApsSound {fromSound :: Text} deriving (Eq, Show, ToJSON, FromJSON, Arbitrary) +instance ToSchema ApsSound where + schema = + mkSchema d i o + where + d = + swaggerDoc @Text + & (S.schema . S.type_ ?~ S.OpenApiString) + & (S.schema . S.description ?~ "ApsSound") + + i = A.withText "ApsSound" (pure . ApsSound) + o = pure . A.String . fromSound + newtype ApsLocKey = ApsLocKey {fromLocKey :: Text} deriving (Eq, Show, ToJSON, FromJSON, Arbitrary) +instance ToSchema ApsLocKey where + schema = + mkSchema d i o + where + d = + swaggerDoc @Text + & (S.schema . S.type_ ?~ S.OpenApiString) + & (S.schema . S.description ?~ "ApsLocKey") + + i = A.withText "ApsLocKey" (pure . ApsLocKey) + o = pure . A.String . fromLocKey + data ApsData = ApsData { _apsLocKey :: !ApsLocKey, _apsLocArgs :: [Text], @@ -183,36 +195,29 @@ data ApsData = ApsData } deriving (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform ApsData - -makeLenses ''ApsData + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema ApsData) apsData :: ApsLocKey -> [Text] -> ApsData apsData lk la = ApsData lk la Nothing True -instance ToJSON ApsData where - toJSON (ApsData k a s b) = - object $ - "loc_key" - .= k - # "loc_args" - .= a - # "sound" - .= s - # "badge" - .= b - # [] - -instance FromJSON ApsData where - parseJSON = withObject "ApsData" $ \o -> - ApsData - <$> o .: "loc_key" - <*> o .:? "loc_args" .!= [] - <*> o .:? "sound" - <*> o .:? "badge" .!= True +instance ToSchema ApsData where + schema = + object "ApsData" $ + ApsData + <$> _apsLocKey .= field "loc_key" schema + <*> withDefault "loc_args" _apsLocArgs (array schema) [] + <*> _apsSound .= optField "sound" (maybeWithDefault A.Null schema) -- keep null for backwards compat + <*> withDefault "badge" _apsBadge schema True + where + withDefault fn f s def = ((Just . f) .= maybe_ (optField fn s)) <&> fromMaybe def + +makeLenses ''ApsData ----------------------------------------------------------------------------- -- Push +-- FUTUREWORK: this is a duplicate of the type in "Wire.NotificationSubsystem" (even though +-- the latter lacks a few possibly deprecated fields). consolidate! data Push = Push { -- | Recipients -- @@ -253,8 +258,7 @@ data Push = Push _pushPayload :: !(List1 Object) } deriving (Eq, Show) - -makeLenses ''Push + deriving (FromJSON, ToJSON, S.ToSchema) via (Schema Push) newPush :: Maybe UserId -> Range 1 1024 (Set Recipient) -> List1 Object -> Push newPush from to pload = @@ -274,43 +278,27 @@ newPush from to pload = singletonPayload :: (ToJSONObject a) => a -> List1 Object singletonPayload = List1.singleton . toJSONObject -instance FromJSON Push where - parseJSON = withObject "Push" $ \p -> - Push - <$> p .: "recipients" - <*> p .:? "origin" - <*> p .:? "connections" .!= Set.empty - <*> p .:? "origin_connection" - <*> p .:? "transient" .!= False - <*> p .:? "native_include_origin" .!= True - <*> p .:? "native_encrypt" .!= True - <*> p .:? "native_aps" - <*> p .:? "native_priority" .!= HighPriority - <*> p .: "payload" - -instance ToJSON Push where - toJSON p = - object $ - "recipients" - .= _pushRecipients p - # "origin" - .= _pushOrigin p - # "connections" - .= ifNot Set.null (_pushConnections p) - # "origin_connection" - .= _pushOriginConnection p - # "transient" - .= ifNot not (_pushTransient p) - # "native_include_origin" - .= ifNot id (_pushNativeIncludeOrigin p) - # "native_encrypt" - .= ifNot id (_pushNativeEncrypt p) - # "native_aps" - .= _pushNativeAps p - # "native_priority" - .= ifNot (== HighPriority) (_pushNativePriority p) - # "payload" - .= _pushPayload p - # [] +instance ToSchema Push where + schema = + object "Push" $ + Push + <$> (fromRange . _pushRecipients) .= field "recipients" (rangedSchema (set schema)) + <*> _pushOrigin .= maybe_ (optField "origin" schema) + <*> (ifNot Set.null . _pushConnections) + .= maybe_ (fmap (fromMaybe mempty) (optField "connections" (set schema))) + <*> _pushOriginConnection .= maybe_ (optField "origin_connection" schema) + <*> (ifNot not . _pushTransient) + .= maybe_ + (fmap (fromMaybe False) (optField "transient" schema)) + <*> (ifNot id . _pushNativeIncludeOrigin) + .= maybe_ (fmap (fromMaybe True) (optField "native_include_origin" schema)) + <*> (ifNot id . _pushNativeEncrypt) + .= maybe_ (fmap (fromMaybe True) (optField "native_encrypt" schema)) + <*> _pushNativeAps .= maybe_ (optField "native_aps" schema) + <*> (ifNot (== HighPriority) . _pushNativePriority) + .= maybe_ (fromMaybe HighPriority <$> optField "native_priority" schema) + <*> _pushPayload .= field "payload" schema where ifNot f a = if f a then Nothing else Just a + +makeLenses ''Push diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Gundeck.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Gundeck.hs new file mode 100644 index 00000000000..9287611a5e9 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Gundeck.hs @@ -0,0 +1,102 @@ +module Wire.API.Routes.Internal.Gundeck where + +import Control.Lens ((%~), (.~), (?~)) +import Data.Aeson +import Data.CommaSeparatedList +import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap +import Data.Id +import Data.Metrics.Servant +import Data.OpenApi qualified as S hiding (HasServer, Header) +import Data.OpenApi.Declare qualified as S +import Data.Text qualified as Text +import Data.Typeable +import Imports +import Network.Wai +import Servant hiding (URI (..)) +import Servant.API.Description +import Servant.OpenApi +import Servant.OpenApi.Internal +import Servant.Server.Internal.Delayed +import Servant.Server.Internal.DelayedIO +import Servant.Server.Internal.ErrorFormatter +import Wire.API.CannonId +import Wire.API.Presence +import Wire.API.Push.V2 +import Wire.API.Routes.Public + +-- | this can be replaced by `ReqBody '[JSON] Presence` once the fix in cannon from +-- https://github.com/wireapp/wire-server/pull/4246 has been deployed everywhere. +-- +-- Background: Cannon.WS.regInfo called gundeck without setting the content-type header here. +-- wai-routes and wai-predicates were able to work with that; servant is less lenient. +data ReqBodyHack + +-- | cloned from instance for ReqBody'. +instance + ( HasServer api context, + HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters + ) => + HasServer (ReqBodyHack :> api) context + where + type ServerT (ReqBodyHack :> api) m = Presence -> ServerT api m + + hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s + + route Proxy context subserver = + route (Proxy :: Proxy api) context $ + addBodyCheck subserver ctCheck bodyCheck + where + rep = typeRep (Proxy :: Proxy ReqBodyHack) + formatError = bodyParserErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context) + + ctCheck = pure eitherDecode + + -- Body check, we get a body parsing functions as the first argument. + bodyCheck f = withRequest $ \request -> do + mrqbody <- f <$> liftIO (lazyRequestBody request) + case mrqbody of + Left e -> delayedFailFatal $ formatError rep request e + Right v -> pure v + +-- | cloned from instance for ReqBody'. +instance (RoutesToPaths route) => RoutesToPaths (ReqBodyHack :> route) where + getRoutes = getRoutes @route + +-- | cloned from instance for ReqBody'. +instance (HasOpenApi sub) => HasOpenApi (ReqBodyHack :> sub) where + toOpenApi _ = + toOpenApi (Proxy @sub) + & addRequestBody reqBody + & addDefaultResponse400 tname + & S.components . S.schemas %~ (<> defs) + where + tname = "body" + transDesc "" = Nothing + transDesc desc = Just (Text.pack desc) + (defs, ref) = S.runDeclare (S.declareSchemaRef (Proxy @Presence)) mempty + reqBody = + (mempty :: S.RequestBody) + & S.description .~ transDesc (reflectDescription (Proxy :: Proxy '[])) + & S.required ?~ True + & S.content .~ InsOrdHashMap.fromList [(t, mempty & S.schema ?~ ref) | t <- allContentType (Proxy :: Proxy '[JSON])] + +type InternalAPI = + "i" + :> ( ("status" :> Get '[JSON] NoContent) + :<|> ("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) + ) + ) + :<|> (ZUser :> "clients" :> Capture "cid" ClientId :> Delete '[JSON] NoContent) + :<|> (ZUser :> "user" :> Delete '[JSON] NoContent) + :<|> ("push-tokens" :> Capture "uid" UserId :> Get '[JSON] PushTokenList) + ) + +swaggerDoc :: S.OpenApi +swaggerDoc = + toOpenApi (Proxy @InternalAPI) + & S.info . S.title .~ "Wire-Server internal gundeck API" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs index 9aece18a8cb..7ef64cf58f4 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/FromJSON.hs @@ -26,6 +26,7 @@ import Test.Wire.API.Golden.Generated.MemberUpdateData_user import Test.Wire.API.Golden.Generated.NewOtrMessage_user import Test.Wire.API.Golden.Generated.RmClient_user import Test.Wire.API.Golden.Generated.SimpleMember_user +import Test.Wire.API.Golden.Manual.Presence import Test.Wire.API.Golden.Runner import Wire.API.Conversation (Conversation, MemberUpdate, OtherMemberUpdate) import Wire.API.User (NewUser, NewUserPublic) @@ -91,5 +92,7 @@ tests = "testObject_NewUserPublic_user_1-3.json" ], testCase "LockableFeature_ConferenceCallingConfig" $ - testFromJSONObject testObject_LockableFeature_team_14 "testObject_LockableFeature_team_14.json" + testFromJSONObject testObject_LockableFeature_team_14 "testObject_LockableFeature_team_14.json", + testCase "LockableFeature_ConferenceCallingConfig" $ + testFromJSONObject testObject_Presence_3 "testObject_Presence_3.json" ] 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 2d5c43f18fb..e57d209f02d 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 @@ -20,6 +20,7 @@ module Test.Wire.API.Golden.Manual where import Imports import Test.Tasty import Test.Wire.API.Golden.Manual.Activate_user +import Test.Wire.API.Golden.Manual.CannonId import Test.Wire.API.Golden.Manual.ClientCapability import Test.Wire.API.Golden.Manual.ClientCapabilityList import Test.Wire.API.Golden.Manual.Contact @@ -42,6 +43,9 @@ import Test.Wire.API.Golden.Manual.ListUsersById import Test.Wire.API.Golden.Manual.LoginId_user import Test.Wire.API.Golden.Manual.Login_user import Test.Wire.API.Golden.Manual.MLSKeys +import Test.Wire.API.Golden.Manual.Presence +import Test.Wire.API.Golden.Manual.Push +import Test.Wire.API.Golden.Manual.PushRemove import Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap import Test.Wire.API.Golden.Manual.SearchResultContact import Test.Wire.API.Golden.Manual.SendActivationCode_user @@ -276,6 +280,26 @@ tests = (testObject_Login_user_4, "testObject_Login_user_4.json"), (testObject_Login_user_5, "testObject_Login_user_5.json") ], + testGroup "CannonId" $ + testObjects + [ (testObject_CannonId_1, "testObject_CannonId_1.json"), + (testObject_CannonId_2, "testObject_CannonId_2.json"), + (testObject_CannonId_3, "testObject_CannonId_3.json") + ], + testGroup "Presence" $ + testObjects + [ (testObject_Presence_1, "testObject_Presence_1.json"), + (testObject_Presence_2, "testObject_Presence_2.json") + ], + testGroup "Push" $ + testObjects + [ (testObject_Push_1, "testObject_Push_1.json"), + (testObject_Push_2, "testObject_Push_2.json") + ], + testGroup "PushRemove" $ + testObjects + [ (testObject_PushRemove_1, "testObject_PushRemove_1.json") + ], testGroup "Activate" $ testObjects [ (testObject_Activate_user_1, "testObject_Activate_user_1.json"), diff --git a/libs/gundeck-types/src/Gundeck/Types/Push.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CannonId.hs similarity index 67% rename from libs/gundeck-types/src/Gundeck/Types/Push.hs rename to libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CannonId.hs index fcd4d6af18c..9f9de73dbab 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Push.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/CannonId.hs @@ -15,9 +15,20 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Gundeck.Types.Push - ( module V2, +module Test.Wire.API.Golden.Manual.CannonId + ( testObject_CannonId_1, + testObject_CannonId_2, + testObject_CannonId_3, ) where -import Gundeck.Types.Push.V2 as V2 +import Wire.API.CannonId + +testObject_CannonId_1 :: CannonId +testObject_CannonId_1 = CannonId "" + +testObject_CannonId_2 :: CannonId +testObject_CannonId_2 = CannonId "sdfiou" + +testObject_CannonId_3 :: CannonId +testObject_CannonId_3 = CannonId "1!_*`'\"" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs new file mode 100644 index 00000000000..97005af0a0d --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Presence.hs @@ -0,0 +1,58 @@ +-- 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.Presence + ( testObject_Presence_1, + testObject_Presence_2, + testObject_Presence_3, + ) +where + +import Data.Id +import Data.UUID qualified as UUID +import Imports +import Wire.API.Presence + +testObject_Presence_1 :: Presence +testObject_Presence_1 = + Presence + (Id . fromJust $ UUID.fromString "174ccaea-7f26-11ef-86cc-27bb6bf3b319") + (ConnId "wef") + (fromJust $ parse "http://example.com/") + Nothing + 0 + "" + +testObject_Presence_2 :: Presence +testObject_Presence_2 = + Presence + (Id . fromJust $ UUID.fromString "174ccaea-7f26-11ef-86cc-37bb6bf3b319") + (ConnId "wef3") + (fromJust $ parse "http://example.com/3") + (Just (ClientId 1)) + 12323 + "" -- __field always has to be "", see ToSchema instance. + +testObject_Presence_3 :: Presence +testObject_Presence_3 = + Presence + (Id . fromJust $ UUID.fromString "174ccaea-7f26-11ef-86cc-37bb6bf3b319") + (ConnId "wef3") + (fromJust $ parse "http://example.com/3") + (Just (ClientId 1)) + 0 + "" -- __field always has to be "", see ToSchema instance. diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Push.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Push.hs new file mode 100644 index 00000000000..fb91cd9cfc6 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Push.hs @@ -0,0 +1,82 @@ +-- 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.Push + ( testObject_Push_1, + testObject_Push_2, + ) +where + +import Data.Aeson qualified as A +import Data.Aeson.KeyMap qualified as KM +import Data.Id +import Data.List1 +import Data.Range +import Data.Set qualified as Set +import Data.UUID qualified as UUID +import Imports +import Wire.API.Push.V2 + +rcp1, rcp2, rcp3 :: Recipient +rcp1 = + Recipient + (Id . fromJust $ UUID.fromString "15441ff8-7f14-11ef-aeec-bbe21dc8a204") + RouteAny + RecipientClientsAll +rcp2 = + Recipient + (Id . fromJust $ UUID.fromString "2e18540e-7f14-11ef-9886-d3c2ff21d3d1") + RouteDirect + (RecipientClientsSome (list1 (ClientId 0) [])) +rcp3 = + Recipient + (Id . fromJust $ UUID.fromString "316924ee-7f14-11ef-b6a2-036a4f646914") + RouteDirect + (RecipientClientsSome (list1 (ClientId 234) [ClientId 123])) + +testObject_Push_1 :: Push +testObject_Push_1 = + Push + { _pushRecipients = unsafeRange (Set.fromList [rcp1]), + _pushOrigin = Nothing, + _pushConnections = mempty, + _pushOriginConnection = Nothing, + _pushTransient = False, + _pushNativeIncludeOrigin = False, + _pushNativeEncrypt = True, + _pushNativeAps = Nothing, + _pushNativePriority = HighPriority, + _pushPayload = singleton mempty + } + +testObject_Push_2 :: Push +testObject_Push_2 = + Push + { _pushRecipients = unsafeRange (Set.fromList [rcp2, rcp3]), + _pushOrigin = Just (Id . fromJust $ UUID.fromString "dec9b47a-7f12-11ef-b634-6710e7ae3d33"), + _pushConnections = Set.fromList [ConnId "sdf", ConnId "mempty", ConnId "wire-client"], + _pushOriginConnection = Just (ConnId "123"), + _pushTransient = True, + _pushNativeIncludeOrigin = True, + _pushNativeEncrypt = False, + _pushNativeAps = Just (apsData (ApsLocKey "asdf") ["1", "22", "333"]), + _pushNativePriority = LowPriority, + _pushPayload = + list1 + (KM.fromList [("foo" :: KM.Key) A..= '3', "bar" A..= True]) + [KM.fromList [], KM.fromList ["growl" A..= ("foooood" :: Text)], KM.fromList ["lunchtime" A..= ("imminent" :: Text)]] + } diff --git a/libs/gundeck-types/src/Gundeck/Types.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/PushRemove.hs similarity index 75% rename from libs/gundeck-types/src/Gundeck/Types.hs rename to libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/PushRemove.hs index 2658731e13e..bf4a823e47d 100644 --- a/libs/gundeck-types/src/Gundeck/Types.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/PushRemove.hs @@ -15,11 +15,13 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Gundeck.Types - ( module G, +module Test.Wire.API.Golden.Manual.PushRemove + ( testObject_PushRemove_1, ) where -import Gundeck.Types.Event as G -import Gundeck.Types.Presence as G -import Gundeck.Types.Push as G +import Test.Wire.API.Golden.Manual.Token (testObject_Token_1) +import Wire.API.Event.Gundeck + +testObject_PushRemove_1 :: PushRemove +testObject_PushRemove_1 = PushRemove testObject_Token_1 diff --git a/libs/wire-api/test/golden/fromJSON/testObject_Presence_3.json b/libs/wire-api/test/golden/fromJSON/testObject_Presence_3.json new file mode 100644 index 00000000000..1089a740c73 --- /dev/null +++ b/libs/wire-api/test/golden/fromJSON/testObject_Presence_3.json @@ -0,0 +1,6 @@ +{ + "client_id": "1", + "device_id": "wef3", + "resource": "http://example.com/3", + "user_id": "174ccaea-7f26-11ef-86cc-37bb6bf3b319" +} diff --git a/libs/wire-api/test/golden/testObject_CannonId_1.json b/libs/wire-api/test/golden/testObject_CannonId_1.json new file mode 100644 index 00000000000..e16c76dff88 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_CannonId_1.json @@ -0,0 +1 @@ +"" diff --git a/libs/wire-api/test/golden/testObject_CannonId_2.json b/libs/wire-api/test/golden/testObject_CannonId_2.json new file mode 100644 index 00000000000..5149c12b38f --- /dev/null +++ b/libs/wire-api/test/golden/testObject_CannonId_2.json @@ -0,0 +1 @@ +"sdfiou" diff --git a/libs/wire-api/test/golden/testObject_CannonId_3.json b/libs/wire-api/test/golden/testObject_CannonId_3.json new file mode 100644 index 00000000000..56bcab3744e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_CannonId_3.json @@ -0,0 +1 @@ +"1!_*`'\"" diff --git a/libs/wire-api/test/golden/testObject_Presence_1.json b/libs/wire-api/test/golden/testObject_Presence_1.json new file mode 100644 index 00000000000..184b9f0809b --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Presence_1.json @@ -0,0 +1,7 @@ +{ + "client_id": null, + "created_at": 0, + "device_id": "wef", + "resource": "http://example.com/", + "user_id": "174ccaea-7f26-11ef-86cc-27bb6bf3b319" +} diff --git a/libs/wire-api/test/golden/testObject_Presence_2.json b/libs/wire-api/test/golden/testObject_Presence_2.json new file mode 100644 index 00000000000..be0b1e54ebb --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Presence_2.json @@ -0,0 +1,7 @@ +{ + "client_id": "1", + "created_at": 12323, + "device_id": "wef3", + "resource": "http://example.com/3", + "user_id": "174ccaea-7f26-11ef-86cc-37bb6bf3b319" +} diff --git a/libs/wire-api/test/golden/testObject_PushRemove_1.json b/libs/wire-api/test/golden/testObject_PushRemove_1.json new file mode 100644 index 00000000000..6fdce76e9d4 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_PushRemove_1.json @@ -0,0 +1,9 @@ +{ + "token": { + "app": "j{𛂚\u0001_􈷉M", + "client": "6", + "token": "K", + "transport": "APNS_VOIP_SANDBOX" + }, + "type": "user.push-remove" +} diff --git a/libs/wire-api/test/golden/testObject_Push_1.json b/libs/wire-api/test/golden/testObject_Push_1.json new file mode 100644 index 00000000000..9680be52df8 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Push_1.json @@ -0,0 +1,13 @@ +{ + "native_include_origin": false, + "payload": [ + {} + ], + "recipients": [ + { + "clients": [], + "route": "any", + "user_id": "15441ff8-7f14-11ef-aeec-bbe21dc8a204" + } + ] +} diff --git a/libs/wire-api/test/golden/testObject_Push_2.json b/libs/wire-api/test/golden/testObject_Push_2.json new file mode 100644 index 00000000000..cc5e168b15e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_Push_2.json @@ -0,0 +1,52 @@ +{ + "connections": [ + "mempty", + "sdf", + "wire-client" + ], + "native_aps": { + "badge": true, + "loc_args": [ + "1", + "22", + "333" + ], + "loc_key": "asdf", + "sound": null + }, + "native_encrypt": false, + "native_priority": "low", + "origin": "dec9b47a-7f12-11ef-b634-6710e7ae3d33", + "origin_connection": "123", + "payload": [ + { + "bar": true, + "foo": "3" + }, + {}, + { + "growl": "foooood" + }, + { + "lunchtime": "imminent" + } + ], + "recipients": [ + { + "clients": [ + "0" + ], + "route": "direct", + "user_id": "2e18540e-7f14-11ef-9886-d3c2ff21d3d1" + }, + { + "clients": [ + "ea", + "7b" + ], + "route": "direct", + "user_id": "316924ee-7f14-11ef-b6a2-036a4f646914" + } + ], + "transient": true +} diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 1b768dd1fc8..1091c12f7f3 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -74,6 +74,7 @@ library Wire.API.Bot Wire.API.Bot.Service Wire.API.Call.Config + Wire.API.CannonId Wire.API.Connection Wire.API.Conversation Wire.API.Conversation.Action @@ -96,6 +97,7 @@ library Wire.API.Event.Conversation Wire.API.Event.FeatureConfig Wire.API.Event.Federation + Wire.API.Event.Gundeck Wire.API.Event.LeaveReason Wire.API.Event.Team Wire.API.FederationStatus @@ -136,6 +138,7 @@ library Wire.API.Notification Wire.API.OAuth Wire.API.Password + Wire.API.Presence Wire.API.Properties Wire.API.Provider Wire.API.Provider.Bot @@ -143,6 +146,7 @@ library Wire.API.Provider.Service Wire.API.Provider.Service.Tag Wire.API.Push.Token + Wire.API.Push.V2 Wire.API.Push.V2.Token Wire.API.RawJson Wire.API.Routes.API @@ -164,6 +168,7 @@ library Wire.API.Routes.Internal.Galley.ConversationsIntra 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 @@ -300,6 +305,7 @@ library , metrics-wai , mime >=0.4 , mtl + , network-uri , openapi3 , pem >=0.2 , polysemy @@ -571,6 +577,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Generated.Wrapped_20_22some_5fint_22_20Int_user Test.Wire.API.Golden.Manual Test.Wire.API.Golden.Manual.Activate_user + Test.Wire.API.Golden.Manual.CannonId Test.Wire.API.Golden.Manual.ClientCapability Test.Wire.API.Golden.Manual.ClientCapabilityList Test.Wire.API.Golden.Manual.Contact @@ -593,6 +600,9 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.Login_user Test.Wire.API.Golden.Manual.LoginId_user Test.Wire.API.Golden.Manual.MLSKeys + Test.Wire.API.Golden.Manual.Presence + Test.Wire.API.Golden.Manual.Push + Test.Wire.API.Golden.Manual.PushRemove Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap Test.Wire.API.Golden.Manual.SearchResultContact Test.Wire.API.Golden.Manual.SendActivationCode_user diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index 17d3f312a20..ead2f2a9c9d 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -28,7 +28,6 @@ , extended , extra , gitignoreSource -, gundeck-types , HaskellNet , HaskellNet-SSL , HsOpenSSL @@ -117,7 +116,6 @@ mkDerivation { exceptions extended extra - gundeck-types HaskellNet HaskellNet-SSL HsOpenSSL @@ -180,7 +178,6 @@ mkDerivation { data-default errors extended - gundeck-types hspec imports iso639 diff --git a/libs/wire-subsystems/src/Wire/GundeckAPIAccess.hs b/libs/wire-subsystems/src/Wire/GundeckAPIAccess.hs index 1d0666880cf..c153cf22364 100644 --- a/libs/wire-subsystems/src/Wire/GundeckAPIAccess.hs +++ b/libs/wire-subsystems/src/Wire/GundeckAPIAccess.hs @@ -5,11 +5,11 @@ module Wire.GundeckAPIAccess where import Bilge import Data.ByteString.Conversion import Data.Id -import Gundeck.Types.Push.V2 qualified as V2 import Imports import Network.HTTP.Types import Polysemy import Util.Options +import Wire.API.Push.V2 qualified as V2 import Wire.Rpc data GundeckAPIAccess m a where diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs index 96d9d4c221b..5e1c35b5fbb 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs @@ -7,9 +7,9 @@ import Control.Lens (makeLenses) import Data.Aeson import Data.Id import Data.List.NonEmpty (NonEmpty ((:|))) -import Gundeck.Types hiding (Push (..), Recipient, newPush) import Imports import Polysemy +import Wire.API.Push.V2 hiding (Push (..), Recipient, newPush) import Wire.Arbitrary data Recipient = Recipient diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs index 7f14c802389..1b0d4dd6c25 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs @@ -11,8 +11,6 @@ import Data.Proxy import Data.Range import Data.Set qualified as Set import Data.Time.Clock.DiffTime -import Gundeck.Types hiding (Push (..), Recipient, newPush) -import Gundeck.Types.Push.V2 qualified as V2 import Imports import Numeric.Natural (Natural) import Polysemy @@ -22,6 +20,8 @@ import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P import System.Logger.Class as Log +import Wire.API.Push.V2 hiding (Push (..), Recipient, newPush) +import Wire.API.Push.V2 qualified as V2 import Wire.API.Team.Member import Wire.GundeckAPIAccess (GundeckAPIAccess) import Wire.GundeckAPIAccess qualified as GundeckAPIAccess diff --git a/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs index 1c633c201ab..9486394b019 100644 --- a/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs @@ -10,7 +10,6 @@ import Data.Range (fromRange, toRange) import Data.Set qualified as Set import Data.String.Conversions import Data.Time.Clock.DiffTime -import Gundeck.Types.Push.V2 qualified as V2 import Imports import Numeric.Natural (Natural) import Polysemy @@ -21,6 +20,7 @@ import System.Timeout (timeout) import Test.Hspec import Test.QuickCheck import Test.QuickCheck.Instances () +import Wire.API.Push.V2 qualified as V2 import Wire.GundeckAPIAccess import Wire.GundeckAPIAccess qualified as GundeckAPIAccess import Wire.NotificationSubsystem diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 011bd1e528c..668e20b9a17 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -168,7 +168,6 @@ library , exceptions , extended , extra - , gundeck-types , HaskellNet , HaskellNet-SSL , HsOpenSSL @@ -275,7 +274,6 @@ test-suite wire-subsystems-tests , data-default , errors , extended - , gundeck-types , hspec , imports , iso639 diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index 5a7d488c79a..ad191ee1573 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -12,7 +12,6 @@ dns-util = hself.callPackage ../libs/dns-util/default.nix { inherit gitignoreSource; }; extended = hself.callPackage ../libs/extended/default.nix { inherit gitignoreSource; }; galley-types = hself.callPackage ../libs/galley-types/default.nix { inherit gitignoreSource; }; - gundeck-types = hself.callPackage ../libs/gundeck-types/default.nix { inherit gitignoreSource; }; hscim = hself.callPackage ../libs/hscim/default.nix { inherit gitignoreSource; }; http2-manager = hself.callPackage ../libs/http2-manager/default.nix { inherit gitignoreSource; }; imports = hself.callPackage ../libs/imports/default.nix { inherit gitignoreSource; }; diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index bc322581589..103cfd80c8a 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -246,7 +246,6 @@ library , filepath >=1.3 , fsnotify >=0.4 , galley-types >=0.75.3 - , gundeck-types >=1.32.1 , hashable >=1.2 , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk diff --git a/services/brig/default.nix b/services/brig/default.nix index a52f5219eb8..ac7206ad390 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -49,7 +49,6 @@ , fsnotify , galley-types , gitignoreSource -, gundeck-types , hashable , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk @@ -204,7 +203,6 @@ mkDerivation { filepath fsnotify galley-types - gundeck-types hashable hs-opentelemetry-instrumentation-wai hs-opentelemetry-sdk diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 35ebe1568b3..8a96e6c2d33 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -45,7 +45,6 @@ import Data.List.NonEmpty (nonEmpty) import Data.Qualified import Data.Range import Data.Set (fromList, (\\)) -import Gundeck.Types.Push qualified as Push import Imports hiding ((\\)) import Network.Wai.Utilities.Error ((!>>)) import Polysemy @@ -57,6 +56,7 @@ import Wire.API.Federation.API.Common import Wire.API.Federation.Endpoint import Wire.API.Federation.Version import Wire.API.MLS.KeyPackage +import Wire.API.Push.V2 qualified as Push import Wire.API.Routes.FederationDomainConfig as FD import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Named diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 98f151e3763..3d50eea50e1 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -113,6 +113,7 @@ import Wire.API.Routes.Internal.Brig qualified as BrigInternalAPI import Wire.API.Routes.Internal.Cannon qualified as CannonInternalAPI import Wire.API.Routes.Internal.Cargohold qualified as CargoholdInternalAPI import Wire.API.Routes.Internal.Galley qualified as GalleyInternalAPI +import Wire.API.Routes.Internal.Gundeck qualified as GundeckInternalAPI import Wire.API.Routes.Internal.Spar qualified as SparInternalAPI import Wire.API.Routes.MultiTablePaging qualified as Public import Wire.API.Routes.Named (Named (Named)) @@ -194,10 +195,11 @@ federatedEndpointsSwaggerDocsAPIs = internalEndpointsSwaggerDocsAPIs :: Servant.Server InternalEndpointsSwaggerDocsAPI internalEndpointsSwaggerDocsAPIs = internalEndpointsSwaggerDocsAPI @"brig" "brig" 9082 BrigInternalAPI.swaggerDoc - :<|> internalEndpointsSwaggerDocsAPI @"cannon" "cannon" 9093 CannonInternalAPI.swaggerDoc - :<|> internalEndpointsSwaggerDocsAPI @"cargohold" "cargohold" 9094 CargoholdInternalAPI.swaggerDoc - :<|> internalEndpointsSwaggerDocsAPI @"galley" "galley" 9095 GalleyInternalAPI.swaggerDoc - :<|> internalEndpointsSwaggerDocsAPI @"spar" "spar" 9098 SparInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI @"cannon" "cannon" 9083 CannonInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI @"cargohold" "cargohold" 9084 CargoholdInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI @"galley" "galley" 9085 GalleyInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI @"spar" "spar" 9088 SparInternalAPI.swaggerDoc + :<|> internalEndpointsSwaggerDocsAPI @"gundeck" "gundeck" 9086 GundeckInternalAPI.swaggerDoc -- | Serves Swagger docs for public endpoints -- diff --git a/services/brig/src/Brig/API/Public/Swagger.hs b/services/brig/src/Brig/API/Public/Swagger.hs index 92304d5dfa0..ebe1287c905 100644 --- a/services/brig/src/Brig/API/Public/Swagger.hs +++ b/services/brig/src/Brig/API/Public/Swagger.hs @@ -54,6 +54,7 @@ type InternalEndpointsSwaggerDocsAPI = :<|> VersionedSwaggerDocsAPIBase "cargohold" :<|> VersionedSwaggerDocsAPIBase "galley" :<|> VersionedSwaggerDocsAPIBase "spar" + :<|> VersionedSwaggerDocsAPIBase "gundeck" ) type NotificationSchemasAPI = "api" :> "event-notification-schemas" :> Get '[JSON] [S.Definitions S.Schema] diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 8309190c88f..d4eef4df895 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -77,8 +77,6 @@ import Data.Proxy import Data.Qualified import Data.Range import Data.Time.Clock (UTCTime) -import Gundeck.Types.Push.V2 (RecipientClients (RecipientClientsAll)) -import Gundeck.Types.Push.V2 qualified as V2 import Imports import Network.HTTP.Types.Method import Network.HTTP.Types.Status @@ -91,6 +89,8 @@ import Wire.API.Conversation hiding (Member) import Wire.API.Event.Conversation (Connect (Connect)) import Wire.API.Federation.API.Brig import Wire.API.Federation.Error +import Wire.API.Push.V2 (RecipientClients (RecipientClientsAll)) +import Wire.API.Push.V2 qualified as V2 import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Internal.Galley.TeamsIntra (GuardLegalholdPolicyConflicts (GuardLegalholdPolicyConflicts)) import Wire.API.Team.LegalHold (LegalholdProtectee) diff --git a/services/cannon/cannon.cabal b/services/cannon/cannon.cabal index b85fd137f9d..1eb1b4cdd26 100644 --- a/services/cannon/cannon.cabal +++ b/services/cannon/cannon.cabal @@ -90,7 +90,6 @@ library , exceptions >=0.6 , extended , extra - , gundeck-types , hashable >=1.2 , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk diff --git a/services/cannon/default.nix b/services/cannon/default.nix index db254e8b235..c0e94ff02f7 100644 --- a/services/cannon/default.nix +++ b/services/cannon/default.nix @@ -17,7 +17,6 @@ , extended , extra , gitignoreSource -, gundeck-types , hashable , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk @@ -73,7 +72,6 @@ mkDerivation { exceptions extended extra - gundeck-types hashable hs-opentelemetry-instrumentation-wai hs-opentelemetry-sdk diff --git a/services/cannon/src/Cannon/WS.hs b/services/cannon/src/Cannon/WS.hs index f551434b599..0ad9820df96 100644 --- a/services/cannon/src/Cannon/WS.hs +++ b/services/cannon/src/Cannon/WS.hs @@ -66,7 +66,6 @@ import Data.Id (ClientId, ConnId (..), UserId) import Data.List.Extra (chunksOf) import Data.Text.Encoding (decodeUtf8) import Data.Timeout (TimeoutUnit (..), (#)) -import Gundeck.Types import Imports hiding (threadDelay) import Network.HTTP.Types.Method import Network.HTTP.Types.Status @@ -76,6 +75,7 @@ import System.Logger qualified as Logger import System.Logger.Class hiding (Error, Settings, close, (.=)) import System.Random.MWC (GenIO, uniform) import UnliftIO.Async (async, cancel, pooledMapConcurrentlyN_) +import Wire.API.Presence ----------------------------------------------------------------------------- -- Key diff --git a/services/galley/default.nix b/services/galley/default.nix index 38bf97f86de..337edc485eb 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -42,7 +42,6 @@ , galley-types , generics-sop , gitignoreSource -, gundeck-types , hex , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk @@ -167,7 +166,6 @@ mkDerivation { extra galley-types generics-sop - gundeck-types hex hs-opentelemetry-instrumentation-wai hs-opentelemetry-sdk diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index abec40e3606..d0f0d567a5e 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -317,7 +317,6 @@ library , extra >=1.3 , galley-types >=0.65.0 , generics-sop - , gundeck-types >=1.35.2 , hex , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 62f1f5120f3..e96f060855e 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -91,7 +91,6 @@ import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.UserList import Galley.Validation -import Gundeck.Types.Push.V2 qualified as PushV2 import Imports hiding ((\\)) import Network.AMQP qualified as Q import Polysemy @@ -116,6 +115,7 @@ import Wire.API.Federation.API.Galley import Wire.API.Federation.API.Galley qualified as F import Wire.API.Federation.Error import Wire.API.FederationStatus +import Wire.API.Push.V2 qualified as PushV2 import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Team.Feature import Wire.API.Team.LegalHold diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index fa65410616d..4b71e3fcf85 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -63,7 +63,6 @@ import Galley.Types.Teams (notTeamMember) import Galley.Types.ToUserRole import Galley.Types.UserList import Galley.Validation -import Gundeck.Types.Push.V2 qualified as PushV2 import Imports hiding ((\\)) import Polysemy import Polysemy.Error @@ -76,6 +75,7 @@ import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.Federation.Error import Wire.API.FederationStatus +import Wire.API.Push.V2 qualified as PushV2 import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util import Wire.API.Team diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index a78652408fe..a6ae06e0339 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -63,7 +63,6 @@ import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.Conversations.One2One import Galley.Types.UserList (UserList (UserList)) -import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Polysemy import Polysemy.Error @@ -94,6 +93,7 @@ import Wire.API.MLS.Keys import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Message +import Wire.API.Push.V2 (RecipientClients (..)) import Wire.API.Routes.Named import Wire.API.ServantProto import Wire.API.User (BaseProtocolTag (..)) diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index d6f731aa195..fff8e161a7d 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -68,7 +68,6 @@ import Galley.Options hiding (brig) import Galley.Queue qualified as Q import Galley.Types.Conversations.Members (RemoteMember (rmId)) import Galley.Types.UserList -import Gundeck.Types.Push.V2 qualified as PushV2 import Imports hiding (head) import Network.AMQP qualified as Q import Polysemy @@ -87,6 +86,7 @@ import Wire.API.Event.LeaveReason import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error +import Wire.API.Push.V2 qualified as PushV2 import Wire.API.Routes.API import Wire.API.Routes.Internal.Brig.EJPD import Wire.API.Routes.Internal.Galley diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index b0fe16e6c8c..d256ab23cec 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -31,7 +31,6 @@ import Galley.Data.Services import Galley.Effects import Galley.Effects.BackendNotificationQueueAccess import Galley.Types.Conversations.Members -import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Network.AMQP qualified as Q import Polysemy @@ -47,6 +46,7 @@ import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.Message +import Wire.API.Push.V2 (RecipientClients (..)) import Wire.NotificationSubsystem -- | Propagate a message. diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index c9a182d890b..c661a9dc5f5 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -33,7 +33,6 @@ import Data.Time import Galley.API.Push import Galley.Effects.ExternalAccess import Galley.Effects.FederatorAccess -import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Network.Wai.Utilities.JSONResponse import Polysemy @@ -52,6 +51,7 @@ import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.MLS.Welcome import Wire.API.Message +import Wire.API.Push.V2 (RecipientClients (..)) import Wire.NotificationSubsystem sendWelcomes :: diff --git a/services/galley/src/Galley/API/Push.hs b/services/galley/src/Galley/API/Push.hs index 5d379999bf0..79a70c56281 100644 --- a/services/galley/src/Galley/API/Push.hs +++ b/services/galley/src/Galley/API/Push.hs @@ -37,13 +37,13 @@ import Data.Map qualified as Map import Data.Qualified import Galley.Data.Services import Galley.Effects.ExternalAccess -import Gundeck.Types.Push (RecipientClients (RecipientClientsSome), Route (..)) import Imports import Polysemy import Polysemy.TinyLog import System.Logger.Class qualified as Log import Wire.API.Event.Conversation import Wire.API.Message +import Wire.API.Push.V2 (RecipientClients (RecipientClientsSome), Route (..)) import Wire.NotificationSubsystem data MessagePush diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index fac4e08102f..617c7c231cc 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -59,7 +59,6 @@ import Galley.Types.Conversations.Members import Galley.Types.Conversations.Roles import Galley.Types.Teams import Galley.Types.UserList -import Gundeck.Types.Push.V2 qualified as PushV2 import Imports hiding (forkIO) import Network.AMQP qualified as Q import Polysemy @@ -79,6 +78,7 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.Password +import Wire.API.Push.V2 qualified as PushV2 import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Util import Wire.API.Team.Feature diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index d797346cf43..7629d0712c5 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -28,7 +28,6 @@ , extra , foldl , gitignoreSource -, gundeck-types , hedis , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk @@ -114,7 +113,6 @@ mkDerivation { extended extra foldl - gundeck-types hedis hs-opentelemetry-instrumentation-wai hs-opentelemetry-sdk @@ -166,7 +164,6 @@ mkDerivation { containers exceptions extended - gundeck-types HsOpenSSL http-client http-client-tls @@ -204,7 +201,6 @@ mkDerivation { bytestring-conversion containers exceptions - gundeck-types HsOpenSSL imports lens @@ -229,7 +225,6 @@ mkDerivation { amazonka base criterion - gundeck-types HsOpenSSL imports lens @@ -237,6 +232,7 @@ mkDerivation { text types-common uuid + wire-api ]; description = "Push Notification Hub"; license = lib.licenses.agpl3Only; diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index b2e14c44908..19ce66fb3e2 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -131,7 +131,6 @@ library , extended , extra >=1.1 , foldl - , gundeck-types >=1.0 , hedis >=0.14.0 , hs-opentelemetry-instrumentation-wai , hs-opentelemetry-sdk @@ -304,7 +303,6 @@ executable gundeck-integration , containers , exceptions , gundeck - , gundeck-types , HsOpenSSL , http-client , http-client-tls @@ -540,7 +538,6 @@ test-suite gundeck-tests , containers , exceptions , gundeck - , gundeck-types , HsOpenSSL , imports , lens @@ -620,7 +617,6 @@ benchmark gundeck-bench , base , criterion , gundeck - , gundeck-types , HsOpenSSL , imports , lens @@ -628,5 +624,6 @@ benchmark gundeck-bench , text , types-common , uuid + , wire-api default-language: GHC2021 diff --git a/services/gundeck/src/Gundeck/API/Internal.hs b/services/gundeck/src/Gundeck/API/Internal.hs index 49c2448bd3a..58a1043cd4b 100644 --- a/services/gundeck/src/Gundeck/API/Internal.hs +++ b/services/gundeck/src/Gundeck/API/Internal.hs @@ -16,90 +16,27 @@ -- with this program. If not, see . module Gundeck.API.Internal - ( type GundeckInternalAPI, + ( type InternalAPI, servantSitemap, ) where import Cassandra qualified import Control.Lens (view) -import Data.Aeson (eitherDecode) -import Data.CommaSeparatedList import Data.Id -import Data.Metrics.Servant -import Data.Typeable import Gundeck.Client qualified as Client import Gundeck.Monad import Gundeck.Presence qualified as Presence import Gundeck.Push qualified as Push import Gundeck.Push.Data qualified as PushTok import Gundeck.Push.Native.Types qualified as PushTok -import Gundeck.Types.Presence as GD -import Gundeck.Types.Push.V2 import Imports -import Network.Wai (lazyRequestBody) import Servant -import Servant.Server.Internal.Delayed -import Servant.Server.Internal.DelayedIO -import Servant.Server.Internal.ErrorFormatter import Wire.API.Push.Token qualified as PushTok -import Wire.API.Routes.Public +import Wire.API.Push.V2 +import Wire.API.Routes.Internal.Gundeck --- | this can be replaced by `ReqBody '[JSON] Presence` once the fix in cannon from --- https://github.com/wireapp/wire-server/pull/4246 has been deployed everywhere. -data ReqBodyHack - --- | cloned from instance for ReqBody'. -instance - ( HasServer api context, - HasContextEntry (MkContextWithErrorFormatter context) ErrorFormatters - ) => - HasServer (ReqBodyHack :> api) context - where - type ServerT (ReqBodyHack :> api) m = Presence -> ServerT api m - - hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api) pc nt . s - - route Proxy context subserver = - route (Proxy :: Proxy api) context $ - addBodyCheck subserver ctCheck bodyCheck - where - rep = typeRep (Proxy :: Proxy ReqBodyHack) - formatError = bodyParserErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context) - - ctCheck = pure eitherDecode - - -- Body check, we get a body parsing functions as the first argument. - bodyCheck f = withRequest $ \request -> do - mrqbody <- f <$> liftIO (lazyRequestBody request) - case mrqbody of - Left e -> delayedFailFatal $ formatError rep request e - Right v -> pure v - --- | cloned from instance for ReqBody'. -instance - (RoutesToPaths rest) => - RoutesToPaths (ReqBodyHack :> rest) - where - getRoutes = getRoutes @rest - -type GundeckInternalAPI = - "i" - :> ( ("status" :> Get '[JSON] NoContent) - :<|> ("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" GD.URI] NoContent)) - :<|> (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) - ) - -servantSitemap :: ServerT GundeckInternalAPI Gundeck +servantSitemap :: ServerT InternalAPI Gundeck servantSitemap = statusH :<|> pushH diff --git a/services/gundeck/src/Gundeck/Aws.hs b/services/gundeck/src/Gundeck/Aws.hs index 21d4ada077b..71014205f40 100644 --- a/services/gundeck/src/Gundeck/Aws.hs +++ b/services/gundeck/src/Gundeck/Aws.hs @@ -83,8 +83,6 @@ import Gundeck.Aws.Sns import Gundeck.Instances () import Gundeck.Options (Opts) import Gundeck.Options qualified as O -import Gundeck.Types.Push hiding (token) -import Gundeck.Types.Push qualified as Push import Imports import Network.HTTP.Client import Network.HTTP.Types @@ -94,6 +92,8 @@ import System.Logger.Class import UnliftIO.Async import UnliftIO.Exception import Util.Options +import Wire.API.Push.V2 hiding (token) +import Wire.API.Push.V2 qualified as Push data Error where EndpointNotFound :: EndpointArn -> Error diff --git a/services/gundeck/src/Gundeck/Aws/Arn.hs b/services/gundeck/src/Gundeck/Aws/Arn.hs index 0ff914c5d57..6723ba223af 100644 --- a/services/gundeck/src/Gundeck/Aws/Arn.hs +++ b/services/gundeck/src/Gundeck/Aws/Arn.hs @@ -57,8 +57,8 @@ import Control.Foldl qualified as Foldl import Control.Lens import Data.Attoparsec.Text import Data.Text qualified as Text -import Gundeck.Types (AppName (..), Transport (..)) import Imports +import Wire.API.Push.V2 (AppName (..), Transport (..)) newtype ArnEnv = ArnEnv {arnEnvText :: Text} deriving (Show, ToText, FromJSON) diff --git a/services/gundeck/src/Gundeck/Instances.hs b/services/gundeck/src/Gundeck/Instances.hs index 83ab2a692b4..ee225e82aed 100644 --- a/services/gundeck/src/Gundeck/Instances.hs +++ b/services/gundeck/src/Gundeck/Instances.hs @@ -31,8 +31,8 @@ import Data.Id import Data.Text.Encoding qualified as Text import Data.UUID qualified as Uuid import Gundeck.Aws.Arn (EndpointArn) -import Gundeck.Types import Imports +import Wire.API.Push.V2 instance Cql Transport where ctype = Tagged IntColumn diff --git a/services/gundeck/src/Gundeck/Presence.hs b/services/gundeck/src/Gundeck/Presence.hs index ed69bb50515..aa8fb778095 100644 --- a/services/gundeck/src/Gundeck/Presence.hs +++ b/services/gundeck/src/Gundeck/Presence.hs @@ -27,9 +27,10 @@ import Data.CommaSeparatedList import Data.Id import Gundeck.Monad import Gundeck.Presence.Data qualified as Data -import Gundeck.Types import Imports -import Servant.API +import Servant.API hiding (URI) +import Wire.API.CannonId +import Wire.API.Presence listH :: UserId -> Gundeck [Presence] listH = runWithDefaultRedis . Data.list @@ -37,7 +38,7 @@ listH = runWithDefaultRedis . Data.list listAllH :: CommaSeparatedList UserId -> Gundeck [Presence] listAllH uids = concat <$> runWithDefaultRedis (Data.listAll (fromCommaSeparatedList uids)) -addH :: Presence -> Gundeck (Headers '[Header "Location" Gundeck.Types.URI] NoContent) +addH :: Presence -> Gundeck (Headers '[Header "Location" URI] NoContent) addH p = do Data.add p pure (addHeader (resource p) NoContent) diff --git a/services/gundeck/src/Gundeck/Presence/Data.hs b/services/gundeck/src/Gundeck/Presence/Data.hs index bfe1773ba9c..6173ace303d 100644 --- a/services/gundeck/src/Gundeck/Presence/Data.hs +++ b/services/gundeck/src/Gundeck/Presence/Data.hs @@ -35,10 +35,10 @@ import Data.List.NonEmpty qualified as NonEmpty import Data.Misc (Milliseconds) import Database.Redis import Gundeck.Monad (Gundeck, posixTime, runWithAdditionalRedis) -import Gundeck.Types import Gundeck.Util.Redis import Imports import System.Logger.Class (MonadLogger) +import Wire.API.Presence -- Note [Migration] --------------------------------------------------------- -- diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 6f3bcbcf684..d7a738ad0e9 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -60,8 +60,6 @@ import Gundeck.Push.Native qualified as Native import Gundeck.Push.Native.Types import Gundeck.Push.Websocket qualified as Web import Gundeck.ThreadBudget -import Gundeck.Types -import Gundeck.Types.Presence qualified as Presence import Gundeck.Util import Imports import Network.HTTP.Types @@ -69,7 +67,10 @@ import Network.Wai.Utilities import System.Logger.Class (msg, val, (+++), (.=), (~~)) import System.Logger.Class qualified as Log import Wire.API.Internal.Notification +import Wire.API.Presence (Presence (..)) +import Wire.API.Presence qualified as Presence import Wire.API.Push.Token qualified as Public +import Wire.API.Push.V2 push :: [Push] -> Gundeck () push ps = do diff --git a/services/gundeck/src/Gundeck/Push/Data.hs b/services/gundeck/src/Gundeck/Push/Data.hs index 5c3fc33cd34..01adce9b11d 100644 --- a/services/gundeck/src/Gundeck/Push/Data.hs +++ b/services/gundeck/src/Gundeck/Push/Data.hs @@ -30,10 +30,10 @@ import Data.ByteString.Conversion import Data.Id (ClientId, ConnId, UserId) import Gundeck.Instances () import Gundeck.Push.Native.Types -import Gundeck.Types hiding (token) import Imports hiding (lookup) import System.Logger.Class (MonadLogger, field, msg, val, (~~)) import System.Logger.Class qualified as Log +import Wire.API.Push.V2 hiding (token) lookup :: (MonadClient m, MonadLogger m) => UserId -> Consistency -> m [Address] lookup u c = foldM mk [] =<< retry x1 (query q (params c (Identity u))) diff --git a/services/gundeck/src/Gundeck/Push/Native.hs b/services/gundeck/src/Gundeck/Push/Native.hs index 0b9c6660eb4..3353568de84 100644 --- a/services/gundeck/src/Gundeck/Push/Native.hs +++ b/services/gundeck/src/Gundeck/Push/Native.hs @@ -39,14 +39,15 @@ import Gundeck.Options import Gundeck.Push.Data qualified as Data import Gundeck.Push.Native.Serialise import Gundeck.Push.Native.Types as Types -import Gundeck.Types import Gundeck.Util import Imports import Prometheus qualified as Prom import System.Logger.Class (MonadLogger, field, msg, val, (.=), (~~)) import System.Logger.Class qualified as Log import UnliftIO (handleAny, mapConcurrently, pooledMapConcurrentlyN_) +import Wire.API.Event.Gundeck import Wire.API.Internal.Notification +import Wire.API.Push.V2 push :: NativePush -> [Address] -> Gundeck () push _ [] = pure () diff --git a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs index 648a888f834..6be13d4d80b 100644 --- a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs +++ b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs @@ -32,8 +32,8 @@ import Data.Text.Encoding (encodeUtf8) import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Builder qualified as LTB import Gundeck.Push.Native.Types -import Gundeck.Types import Imports +import Wire.API.Push.V2 serialise :: (HasCallStack) => NativePush -> UserId -> Transport -> Either Failure LT.Text serialise (NativePush nid prio _aps) uid transport = do diff --git a/services/gundeck/src/Gundeck/Push/Native/Types.hs b/services/gundeck/src/Gundeck/Push/Native/Types.hs index b58726da4a6..cf28f90ebbb 100644 --- a/services/gundeck/src/Gundeck/Push/Native/Types.hs +++ b/services/gundeck/src/Gundeck/Push/Native/Types.hs @@ -44,9 +44,9 @@ where import Control.Lens (Lens', makeLenses, (^.)) import Data.Id (ClientId, ConnId, UserId) import Gundeck.Aws.Arn -import Gundeck.Types import Imports import Wire.API.Internal.Notification +import Wire.API.Push.V2 -- | Native push address information of a device. data Address = Address diff --git a/services/gundeck/src/Gundeck/Push/Websocket.hs b/services/gundeck/src/Gundeck/Push/Websocket.hs index 1c9d4730c51..97600d2720d 100644 --- a/services/gundeck/src/Gundeck/Push/Websocket.hs +++ b/services/gundeck/src/Gundeck/Push/Websocket.hs @@ -41,7 +41,6 @@ import Data.Set qualified as Set import Data.Time.Clock.POSIX import Gundeck.Monad import Gundeck.Presence.Data qualified as Presence -import Gundeck.Types.Presence import Gundeck.Util import Imports import Network.HTTP.Client (HttpExceptionContent (..)) @@ -54,6 +53,7 @@ import System.Logger.Class qualified as Log import UnliftIO (handleAny, mapConcurrently) import Wire.API.Internal.BulkPush import Wire.API.Internal.Notification +import Wire.API.Presence class (Monad m, MonadThrow m, Log.MonadLogger m) => MonadBulkPush m where mbpBulkSend :: URI -> BulkPushRequest -> m (URI, Either SomeException BulkPushResponse) diff --git a/services/gundeck/src/Gundeck/React.hs b/services/gundeck/src/Gundeck/React.hs index 9ffdf521cca..098eefa1288 100644 --- a/services/gundeck/src/Gundeck/React.hs +++ b/services/gundeck/src/Gundeck/React.hs @@ -41,12 +41,13 @@ import Gundeck.Options (notificationTTL, settings) import Gundeck.Push.Data qualified as Push import Gundeck.Push.Native.Types import Gundeck.Push.Websocket qualified as Web -import Gundeck.Types import Gundeck.Util import Imports import System.Logger.Class (Msg, msg, val, (+++), (.=), (~~)) import System.Logger.Class qualified as Log +import Wire.API.Event.Gundeck import Wire.API.Internal.Notification +import Wire.API.Push.V2 onEvent :: Event -> Gundeck () onEvent ev = case ev ^. evType of diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index 5543f0a8ad7..b978b9d6b13 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -31,7 +31,7 @@ import Data.Metrics.Servant qualified as Metrics import Data.Proxy (Proxy (Proxy)) import Data.Text (unpack) import Database.Redis qualified as Redis -import Gundeck.API.Internal as Internal (GundeckInternalAPI, servantSitemap) +import Gundeck.API.Internal as Internal (InternalAPI, servantSitemap) import Gundeck.API.Public as Public (servantSitemap) import Gundeck.Aws qualified as Aws import Gundeck.Env @@ -92,7 +92,7 @@ run o = withTracer \tracer -> do versionMiddleware (foldMap expandVersionExp (o ^. settings . disabledAPIVersions)) . otelMiddleWare . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName - . Metrics.servantPrometheusMiddleware (Proxy @(GundeckAPI :<|> GundeckInternalAPI)) + . Metrics.servantPrometheusMiddleware (Proxy @(GundeckAPI :<|> InternalAPI)) . GZip.gunzip . GZip.gzip GZip.def . catchErrors (e ^. applog) defaultRequestIdHeaderName @@ -101,10 +101,10 @@ mkApp :: Env -> Wai.Application mkApp env0 req cont = do let rid = getRequestId defaultRequestIdHeaderName req env = reqId .~ rid $ env0 - Servant.serve (Proxy @(GundeckAPI :<|> GundeckInternalAPI)) (servantSitemap' env) req cont + Servant.serve (Proxy @(GundeckAPI :<|> InternalAPI)) (servantSitemap' env) req cont -servantSitemap' :: Env -> Servant.Server (GundeckAPI :<|> GundeckInternalAPI) -servantSitemap' env = Servant.hoistServer (Proxy @(GundeckAPI :<|> GundeckInternalAPI)) toServantHandler (Public.servantSitemap :<|> Internal.servantSitemap) +servantSitemap' :: Env -> Servant.Server (GundeckAPI :<|> InternalAPI) +servantSitemap' env = Servant.hoistServer (Proxy @(GundeckAPI :<|> InternalAPI)) toServantHandler (Public.servantSitemap :<|> Internal.servantSitemap) where toServantHandler :: Gundeck a -> Handler a toServantHandler m = Handler . ExceptT $ Right <$> runDirect env m diff --git a/services/gundeck/test/bench/Main.hs b/services/gundeck/test/bench/Main.hs index 79fd1d6a9a7..c18d2b26932 100644 --- a/services/gundeck/test/bench/Main.hs +++ b/services/gundeck/test/bench/Main.hs @@ -33,10 +33,10 @@ import Gundeck.Options import Gundeck.Push.Native.Serialise import Gundeck.Push.Native.Types import Gundeck.ThreadBudget.Internal -import Gundeck.Types.Push import Imports import OpenSSL (withOpenSSL) import System.Random (randomRIO) +import Wire.API.Push.V2 main :: IO () main = withOpenSSL $ do diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index e5e3a3cdbcc..a9fedf99269 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -52,8 +52,6 @@ import Data.UUID qualified as UUID import Data.UUID.V4 import Gundeck.Options hiding (bulkPush) import Gundeck.Options qualified as O -import Gundeck.Types -import Gundeck.Types.Common qualified import Imports import Network.HTTP.Client qualified as Http import Network.URI (parseURI) @@ -65,7 +63,10 @@ import Test.Tasty import Test.Tasty.HUnit import TestSetup import Util (runRedisProxy, withEnvOverrides, withSettingsOverrides) +import Wire.API.Event.Gundeck import Wire.API.Internal.Notification +import Wire.API.Presence +import Wire.API.Push.V2 import Prelude qualified tests :: IO TestSetup -> TestTree @@ -871,7 +872,7 @@ testRedisMigration :: TestM () testRedisMigration = do uid <- randomUser con <- randomConnId - cannonURI <- Gundeck.Types.Common.parse "http://cannon.example" + cannonURI <- Wire.API.Presence.parse "http://cannon.example" let presence = Presence uid con cannonURI Nothing 1 "" redis2 <- view tsRedis2 diff --git a/services/gundeck/test/unit/Aws/Arn.hs b/services/gundeck/test/unit/Aws/Arn.hs index 9d20bfaeec0..dde384d6b35 100644 --- a/services/gundeck/test/unit/Aws/Arn.hs +++ b/services/gundeck/test/unit/Aws/Arn.hs @@ -3,10 +3,10 @@ module Aws.Arn where import Amazonka.Data.Text import Control.Lens import Gundeck.Aws.Arn -import Gundeck.Types import Imports import Test.Tasty import Test.Tasty.HUnit +import Wire.API.Push.V2 tests :: TestTree tests = diff --git a/services/gundeck/test/unit/Json.hs b/services/gundeck/test/unit/Json.hs index b83dbf006be..b84b925c4f1 100644 --- a/services/gundeck/test/unit/Json.hs +++ b/services/gundeck/test/unit/Json.hs @@ -22,13 +22,13 @@ import Data.Aeson import Data.Aeson.KeyMap (fromList) import Data.Id import Data.List1 -import Gundeck.Types.Push import Imports import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck import Wire.API.Internal.BulkPush import Wire.API.Internal.Notification +import Wire.API.Push.V2 tests :: TestTree tests = diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index d662a62aa10..e1c345fed73 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -67,7 +67,6 @@ import Gundeck.Options import Gundeck.Push import Gundeck.Push.Native as Native import Gundeck.Push.Websocket as Web -import Gundeck.Types hiding (recipient) import Imports import Network.URI qualified as URI import System.Logger.Class as Log hiding (trace) @@ -75,6 +74,8 @@ import Test.QuickCheck as QC import Test.QuickCheck.Instances () import Wire.API.Internal.BulkPush import Wire.API.Internal.Notification +import Wire.API.Presence +import Wire.API.Push.V2 hiding (recipient) ---------------------------------------------------------------------- -- env diff --git a/services/gundeck/test/unit/Native.hs b/services/gundeck/test/unit/Native.hs index 2e525f7cf1f..5b0d4daf1b9 100644 --- a/services/gundeck/test/unit/Native.hs +++ b/services/gundeck/test/unit/Native.hs @@ -28,11 +28,11 @@ import Data.Text.Encoding qualified as T import Data.Text.Lazy.Encoding qualified as LT import Gundeck.Push.Native.Serialise import Gundeck.Push.Native.Types -import Gundeck.Types.Push import Imports import Test.Tasty import Test.Tasty.QuickCheck import Wire.API.Internal.Notification +import Wire.API.Push.V2 tests :: TestTree tests = diff --git a/services/gundeck/test/unit/ParseExistsError.hs b/services/gundeck/test/unit/ParseExistsError.hs index 1c370be3eed..02ae7fd1408 100644 --- a/services/gundeck/test/unit/ParseExistsError.hs +++ b/services/gundeck/test/unit/ParseExistsError.hs @@ -3,10 +3,10 @@ module ParseExistsError where import Amazonka.Types import Gundeck.Aws import Gundeck.Aws.Arn -import Gundeck.Types.Push.V2 (Transport (APNS)) import Imports import Test.Tasty import Test.Tasty.HUnit +import Wire.API.Push.V2 (Transport (APNS)) tests :: TestTree tests = diff --git a/services/gundeck/test/unit/Push.hs b/services/gundeck/test/unit/Push.hs index 3214c72bdca..a65b85445e6 100644 --- a/services/gundeck/test/unit/Push.hs +++ b/services/gundeck/test/unit/Push.hs @@ -23,7 +23,6 @@ module Push where import Data.Aeson qualified as Aeson import Gundeck.Push (pushAll, pushAny) import Gundeck.Push.Websocket as Web (bulkPush) -import Gundeck.Types import Imports import MockGundeck import Test.QuickCheck @@ -31,6 +30,8 @@ import Test.QuickCheck.Instances () import Test.Tasty import Test.Tasty.QuickCheck import Wire.API.Internal.Notification +import Wire.API.Presence +import Wire.API.Push.V2 tests :: TestTree tests = From ef67359a23cd2477700280a93e9cd0856692bccf Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 2 Oct 2024 10:58:48 +0200 Subject: [PATCH 099/136] [WPB-11386] Introduce length-preserving function mapRange to replace fmap. (#4279) --- changelog.d/5-internal/WPB-11386-map-range | 1 + libs/types-common/src/Data/Range.hs | 6 +++++- services/brig/src/Brig/IO/Intra.hs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelog.d/5-internal/WPB-11386-map-range diff --git a/changelog.d/5-internal/WPB-11386-map-range b/changelog.d/5-internal/WPB-11386-map-range new file mode 100644 index 00000000000..a01f45001c9 --- /dev/null +++ b/changelog.d/5-internal/WPB-11386-map-range @@ -0,0 +1 @@ +Introduce length-preserving function mapRange to replace Functor instance for Range data type. \ No newline at end of file diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index 73cdda9fea7..c4401541756 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -26,6 +26,7 @@ module Data.Range ( Range, toRange, + mapRange, Within, Bounds (..), checked, @@ -98,7 +99,10 @@ import Test.QuickCheck qualified as QC newtype Range (n :: Nat) (m :: Nat) a = Range { fromRange :: a } - deriving (Eq, Ord, Show, Functor) + deriving (Eq, Ord, Show) + +mapRange :: forall (n :: Nat) (m :: Nat) a b. (a -> b) -> Range n m [a] -> Range n m [b] +mapRange f (Range as) = Range (f `map` as) toRange :: (n <= x, x <= m, KnownNat x, Num a) => Proxy x -> Range n m a toRange = Range . fromIntegral . natVal diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index d4eef4df895..39f2250eea1 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -326,7 +326,7 @@ notifyUserDeletionRemotes deleted = do pure () Just rangedUcs -> do luidDeleted <- qualifyLocal' deleted - embed $ notifyUserDeleted luidDeleted (qualifyAs ucs ((fmap (fmap (qUnqualified . ucTo))) rangedUcs)) + embed $ notifyUserDeleted luidDeleted (qualifyAs ucs (mapRange (qUnqualified . ucTo) rangedUcs)) -- also sent connection cancelled events to the connections that are pending let remotePendingConnections = qualifyAs ucs <$> filter ((==) Sent . ucStatus) (fromRange rangedUcs) forM_ remotePendingConnections $ sendCancelledEvent luidDeleted From ae2879d7ea51c344d9bcaafd69944a09d2a601bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 2 Oct 2024 16:29:39 +0200 Subject: [PATCH 100/136] [WPB-11101] Refactor invitation store (#4280) * Drop the `StoredInvitationInfo` type * Rename 'LookupInvitationInfo' to 'LookupInvitationByCode' * Rename the 'LookupInvitationCodesByEmail' action * Add a change log * Rename effect 'InvitationCodeStore' --- .../5-internal/WPB-11101-internal-types | 1 + ...itationCodeStore.hs => InvitationStore.hs} | 60 ++++-------------- .../Cassandra.hs | 62 ++++++++++++------- .../TeamInvitationSubsystem/Interpreter.hs | 10 +-- .../wire-subsystems/src/Wire/UserSubsystem.hs | 2 +- .../src/Wire/UserSubsystem/Interpreter.hs | 27 ++++---- .../test/unit/Wire/MiniBackend.hs | 20 +++--- ...itationCodeStore.hs => InvitationStore.hs} | 24 +++---- .../Wire/UserSubsystem/InterpreterSpec.hs | 4 +- libs/wire-subsystems/wire-subsystems.cabal | 6 +- services/brig/src/Brig/API/Internal.hs | 10 +-- services/brig/src/Brig/API/Public.hs | 6 +- services/brig/src/Brig/API/User.hs | 8 +-- .../brig/src/Brig/CanonicalInterpreter.hs | 8 +-- services/brig/src/Brig/Team/API.hs | 42 +++++++------ 15 files changed, 132 insertions(+), 158 deletions(-) create mode 100644 changelog.d/5-internal/WPB-11101-internal-types rename libs/wire-subsystems/src/Wire/{InvitationCodeStore.hs => InvitationStore.hs} (54%) rename libs/wire-subsystems/src/Wire/{InvitationCodeStore => InvitationStore}/Cassandra.hs (77%) rename libs/wire-subsystems/test/unit/Wire/MockInterpreters/{InvitationCodeStore.hs => InvitationStore.hs} (55%) diff --git a/changelog.d/5-internal/WPB-11101-internal-types b/changelog.d/5-internal/WPB-11101-internal-types new file mode 100644 index 00000000000..bf92f52b5ce --- /dev/null +++ b/changelog.d/5-internal/WPB-11101-internal-types @@ -0,0 +1 @@ +Improve abstraction in the invitation store and hide DB interaction-specific internal types from the application code. diff --git a/libs/wire-subsystems/src/Wire/InvitationCodeStore.hs b/libs/wire-subsystems/src/Wire/InvitationStore.hs similarity index 54% rename from libs/wire-subsystems/src/Wire/InvitationCodeStore.hs rename to libs/wire-subsystems/src/Wire/InvitationStore.hs index 78eee5283dc..e691f516bf7 100644 --- a/libs/wire-subsystems/src/Wire/InvitationCodeStore.hs +++ b/libs/wire-subsystems/src/Wire/InvitationStore.hs @@ -18,17 +18,14 @@ {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} -module Wire.InvitationCodeStore where +module Wire.InvitationStore where -import Control.Monad.Trans.Maybe (MaybeT (MaybeT, runMaybeT)) import Data.Id (InvitationId, TeamId, UserId) import Data.Json.Util (UTCTimeMillis) import Data.Range (Range) import Database.CQL.Protocol (Record (..), TupleType, recordInstance) import Imports import Polysemy -import Polysemy.TinyLog (TinyLog) -import System.Logger.Message qualified as Log import URI.ByteString import Util.Timeout import Wire.API.Team.Invitation (Invitation (inviteeEmail)) @@ -36,7 +33,6 @@ import Wire.API.Team.Invitation qualified as Public import Wire.API.Team.Role (Role, defaultRole) import Wire.API.User (EmailAddress, InvitationCode, Name) import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -import Wire.Sem.Logger qualified as Log data StoredInvitation = MkStoredInvitation { teamId :: TeamId, @@ -53,16 +49,6 @@ data StoredInvitation = MkStoredInvitation recordInstance ''StoredInvitation -data StoredInvitationInfo = MkStoredInvitationInfo - { teamId :: TeamId, - invitationId :: InvitationId, - code :: InvitationCode - } - deriving (Show, Eq, Generic) - deriving (Arbitrary) via (GenericUniform StoredInvitationInfo) - -recordInstance ''StoredInvitationInfo - data InsertInvitation = MkInsertInvitation { invitationId :: InvitationId, teamId :: TeamId, @@ -84,45 +70,21 @@ data PaginatedResult a ---------------------------- -data InvitationCodeStore :: Effect where - InsertInvitation :: InsertInvitation -> Timeout -> InvitationCodeStore m StoredInvitation - LookupInvitation :: TeamId -> InvitationId -> InvitationCodeStore m (Maybe StoredInvitation) - LookupInvitationInfo :: InvitationCode -> InvitationCodeStore m (Maybe StoredInvitationInfo) - LookupInvitationCodesByEmail :: EmailAddress -> InvitationCodeStore m [StoredInvitationInfo] +data InvitationStore :: Effect where + InsertInvitation :: InsertInvitation -> Timeout -> InvitationStore m StoredInvitation + LookupInvitation :: TeamId -> InvitationId -> InvitationStore m (Maybe StoredInvitation) + LookupInvitationByCode :: InvitationCode -> InvitationStore m (Maybe StoredInvitation) + LookupInvitationsByEmail :: EmailAddress -> InvitationStore m [StoredInvitation] -- | Range is page size, it defaults to 100 - LookupInvitationsPaginated :: Maybe (Range 1 500 Int32) -> TeamId -> Maybe InvitationId -> InvitationCodeStore m (PaginatedResult [StoredInvitation]) - CountInvitations :: TeamId -> InvitationCodeStore m Int64 - DeleteInvitation :: TeamId -> InvitationId -> InvitationCodeStore m () - DeleteAllTeamInvitations :: TeamId -> InvitationCodeStore m () + LookupInvitationsPaginated :: Maybe (Range 1 500 Int32) -> TeamId -> Maybe InvitationId -> InvitationStore m (PaginatedResult [StoredInvitation]) + CountInvitations :: TeamId -> InvitationStore m Int64 + DeleteInvitation :: TeamId -> InvitationId -> InvitationStore m () + DeleteAllTeamInvitations :: TeamId -> InvitationStore m () -makeSem ''InvitationCodeStore +makeSem ''InvitationStore ---------------------------- -lookupInvitationByEmail :: (Member InvitationCodeStore r, Member TinyLog r) => EmailAddress -> Sem r (Maybe StoredInvitation) -lookupInvitationByEmail email = runMaybeT do - MkStoredInvitationInfo {teamId, invitationId} <- MaybeT $ lookupSingleInvitationCodeByEmail email - MaybeT $ lookupInvitation teamId invitationId - -lookupInvitationByCode :: (Member InvitationCodeStore r) => InvitationCode -> Sem r (Maybe StoredInvitation) -lookupInvitationByCode code = runMaybeT do - info <- MaybeT $ lookupInvitationInfo code - MaybeT $ lookupInvitation info.teamId info.invitationId - -lookupSingleInvitationCodeByEmail :: (Member TinyLog r, Member InvitationCodeStore r) => EmailAddress -> Sem r (Maybe StoredInvitationInfo) -lookupSingleInvitationCodeByEmail email = do - invs <- lookupInvitationCodesByEmail email - case invs of - [] -> pure Nothing - [inv] -> pure $ Just inv - (_ : _ : _) -> do - -- edge case: more than one pending invite from different teams - Log.info $ - Log.msg (Log.val "team_invidation_email: multiple pending invites from different teams for the same email") - . Log.field "email" (show email) - - pure Nothing - invitationFromStored :: Maybe (URIRef Absolute) -> StoredInvitation -> Public.Invitation invitationFromStored maybeUrl MkStoredInvitation {..} = Public.Invitation diff --git a/libs/wire-subsystems/src/Wire/InvitationCodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/InvitationStore/Cassandra.hs similarity index 77% rename from libs/wire-subsystems/src/Wire/InvitationCodeStore/Cassandra.hs rename to libs/wire-subsystems/src/Wire/InvitationStore/Cassandra.hs index 37463cfb966..b9fc0173858 100644 --- a/libs/wire-subsystems/src/Wire/InvitationCodeStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/InvitationStore/Cassandra.hs @@ -1,31 +1,33 @@ -module Wire.InvitationCodeStore.Cassandra where +module Wire.InvitationStore.Cassandra + ( interpretInvitationStoreToCassandra, + ) +where import Cassandra +import Control.Monad.Trans.Maybe import Data.Conduit (runConduit, (.|)) import Data.Conduit.List qualified as Conduit import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) import Data.Range (Range, fromRange) -import Data.Text.Ascii (encodeBase64Url) -import Database.CQL.Protocol (TupleType, asRecord) +import Database.CQL.Protocol (Record (..), TupleType, asRecord) import Imports -import OpenSSL.Random (randBytes) import Polysemy import Polysemy.Embed import UnliftIO.Async (pooledMapConcurrentlyN_) import Util.Timeout import Wire.API.Team.Role (Role) import Wire.API.User -import Wire.InvitationCodeStore +import Wire.InvitationStore -interpretInvitationCodeStoreToCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor InvitationCodeStore r -interpretInvitationCodeStoreToCassandra casClient = +interpretInvitationStoreToCassandra :: (Member (Embed IO) r) => ClientState -> InterpreterFor InvitationStore r +interpretInvitationStoreToCassandra casClient = interpret $ runEmbedded (runClient casClient) . \case InsertInvitation newInv timeout -> embed $ insertInvitationImpl newInv timeout LookupInvitation tid iid -> embed $ lookupInvitationImpl tid iid - LookupInvitationCodesByEmail email -> embed $ lookupInvitationCodesByEmailImpl email - LookupInvitationInfo code -> embed $ lookupInvitationInfoImpl code + LookupInvitationsByEmail email -> embed $ lookupInvitationsByEmailImpl email + LookupInvitationByCode code -> embed $ lookupInvitationByCodeImpl code LookupInvitationsPaginated mSize tid miid -> embed $ lookupInvitationsPaginatedImpl mSize tid miid CountInvitations tid -> embed $ countInvitationsImpl tid DeleteInvitation tid invId -> embed $ deleteInvitationImpl tid invId @@ -108,24 +110,41 @@ countInvitationsImpl t = cql :: PrepQuery R (Identity TeamId) (Identity Int64) cql = [sql| SELECT count(*) FROM team_invitation WHERE team = ?|] -lookupInvitationInfoImpl :: InvitationCode -> Client (Maybe StoredInvitationInfo) -lookupInvitationInfoImpl code = - fmap asRecord <$> retry x1 (query1 cql (params LocalQuorum (Identity code))) +lookupInvitationByCodeImpl :: InvitationCode -> Client (Maybe StoredInvitation) +lookupInvitationByCodeImpl code = runMaybeT do + (teamId, invId, _) <- + MaybeT $ + retry x1 (query1 cqlInfo (params LocalQuorum (Identity code))) + MaybeT $ fmap asRecord <$> retry x1 (query1 cqlMain (params LocalQuorum (teamId, invId))) where - cql :: PrepQuery R (Identity InvitationCode) (TupleType StoredInvitationInfo) - cql = + cqlInfo :: PrepQuery R (Identity InvitationCode) (TeamId, InvitationId, InvitationCode) + cqlInfo = [sql| - SELECT team, id, code FROM team_invitation_info WHERE code = ? + SELECT team, id, code FROM team_invitation_info WHERE code = ? + |] + cqlMain :: PrepQuery R (TeamId, InvitationId) (TupleType StoredInvitation) + cqlMain = + [sql| + SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id = ? |] -lookupInvitationCodesByEmailImpl :: EmailAddress -> Client [StoredInvitationInfo] -lookupInvitationCodesByEmailImpl email = map asRecord <$> retry x1 (query cql (params LocalQuorum (Identity email))) +lookupInvitationsByEmailImpl :: EmailAddress -> Client [StoredInvitation] +lookupInvitationsByEmailImpl email = do + infoList <- + retry x1 (query cqlInfo (params LocalQuorum (Identity email))) + fmap catMaybes $ forM infoList $ \(tid, invId, _invCode) -> + fmap asRecord <$> retry x1 (query1 cqlMain (params LocalQuorum (tid, invId))) where - cql :: PrepQuery R (Identity EmailAddress) (TeamId, InvitationId, InvitationCode) - cql = + cqlInfo :: PrepQuery R (Identity EmailAddress) (TeamId, InvitationId, InvitationCode) + cqlInfo = [sql| SELECT team, invitation, code FROM team_invitation_email WHERE email = ? |] + cqlMain :: PrepQuery R (TeamId, InvitationId) (TupleType StoredInvitation) + cqlMain = + [sql| + SELECT team, role, id, created_at, created_by, email, name, code FROM team_invitation WHERE team = ? AND id = ? + |] lookupInvitationImpl :: TeamId -> InvitationId -> Client (Maybe StoredInvitation) lookupInvitationImpl tid iid = @@ -186,8 +205,3 @@ deleteInvitationsImpl teamId = where cqlSelect :: PrepQuery R (Identity TeamId) (Identity InvitationId) cqlSelect = "SELECT id FROM team_invitation WHERE team = ? ORDER BY id ASC" - --- | This function doesn't really belong here, and may want to have return type `Sem (Random : --- ...)` instead of `IO`. Meh. -mkInvitationCode :: IO InvitationCode -mkInvitationCode = InvitationCode . encodeBase64Url <$> randBytes 24 diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs index 6095ba6441d..bac8f052635 100644 --- a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs @@ -27,8 +27,8 @@ import Wire.Arbitrary import Wire.EmailSubsystem import Wire.GalleyAPIAccess hiding (AddTeamMember) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess -import Wire.InvitationCodeStore (InvitationCodeStore, StoredInvitation) -import Wire.InvitationCodeStore qualified as Store +import Wire.InvitationStore (InvitationStore, StoredInvitation) +import Wire.InvitationStore qualified as Store import Wire.Sem.Logger qualified as Log import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now @@ -52,7 +52,7 @@ runTeamInvitationSubsystem :: Member GalleyAPIAccess r, Member UserSubsystem r, Member Random r, - Member InvitationCodeStore r, + Member InvitationStore r, Member Now r, Member EmailSubsystem r ) => @@ -69,7 +69,7 @@ inviteUserImpl :: Member UserSubsystem r, Member TinyLog r, Member Random r, - Member InvitationCodeStore r, + Member InvitationStore r, Member (Input TeamInvitationSubsystemConfig) r, Member Now r, Member EmailSubsystem r @@ -106,7 +106,7 @@ inviteUserImpl luid tid request = do createInvitation' :: ( Member GalleyAPIAccess r, Member UserSubsystem r, - Member InvitationCodeStore r, + Member InvitationStore r, Member TinyLog r, Member (Error TeamInvitationSubsystemError) r, Member Random r, diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 86f4304b064..10357641b71 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -27,7 +27,7 @@ import Wire.API.User.Search import Wire.Arbitrary import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess -import Wire.InvitationCodeStore +import Wire.InvitationStore import Wire.UserKeyStore (EmailKey, emailKeyOrig) import Wire.UserSearch.Types import Wire.UserSubsystem.Error (UserSubsystemError (..)) diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index 98e4bd97b6f..f0318d5bff7 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -55,7 +55,7 @@ import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.IndexedUserStore (IndexedUserStore) import Wire.IndexedUserStore qualified as IndexedUserStore import Wire.IndexedUserStore.Bulk.ElasticSearch (teamSearchVisibilityInbound) -import Wire.InvitationCodeStore +import Wire.InvitationStore import Wire.Sem.Concurrency import Wire.Sem.Metrics import Wire.Sem.Metrics qualified as Metrics @@ -99,7 +99,7 @@ runUserSubsystem :: Member IndexedUserStore r, Member FederationConfigStore r, Member Metrics r, - Member InvitationCodeStore r, + Member InvitationStore r, Member TinyLog r ) => UserSubsystemConfig -> @@ -172,7 +172,7 @@ runUserSubsystem cfg authInterpreter = internalFindTeamInvitationImpl mEmailKey code internalFindTeamInvitationImpl :: - ( Member InvitationCodeStore r, + ( Member InvitationStore r, Member (Error UserSubsystemError) r, Member (Input UserSubsystemConfig) r, Member (GalleyAPIAccess) r, @@ -183,15 +183,11 @@ internalFindTeamInvitationImpl :: Sem r StoredInvitation internalFindTeamInvitationImpl Nothing _ = throw UserSubsystemMissingIdentity internalFindTeamInvitationImpl (Just e) c = - lookupInvitationInfo c >>= \case - Just invitationInfo -> do - inv <- lookupInvitation invitationInfo.teamId invitationInfo.invitationId - case (inv, (.email) <$> inv) of - (Just invite, Just em) - | e == mkEmailKey em -> do - ensureMemberCanJoin invitationInfo.teamId - pure invite - _ -> throw UserSubsystemInvalidInvitationCode + lookupInvitationByCode c >>= \case + Just inv -> do + if e == mkEmailKey (inv.email) + then ensureMemberCanJoin inv.teamId $> inv + else throw UserSubsystemInvalidInvitationCode Nothing -> throw UserSubsystemInvalidInvitationCode where ensureMemberCanJoin tid = do @@ -849,8 +845,7 @@ getAccountsByImpl :: ( Member UserStore r, Member DeleteQueue r, Member (Input UserSubsystemConfig) r, - Member InvitationCodeStore r, - Member TinyLog r + Member InvitationStore r ) => Local GetBy -> Sem r [User] @@ -883,7 +878,7 @@ getAccountsByImpl (tSplit -> (domain, MkGetBy {includePendingInvitations, getByH -- validated one cannot be found. that's probably wrong? split up into -- validEmailIdentity, anyEmailIdentity? Just email -> do - hasInvitation <- isJust <$> lookupInvitationByEmail email + hasInvitation <- isJust . listToMaybe <$> lookupInvitationsByEmail email gcHack hasInvitation (User.userId user) pure hasInvitation Nothing -> error "getExtendedAccountsByImpl: should never happen, user invited via scim always has an email" @@ -916,7 +911,7 @@ acceptTeamInvitationImpl :: Member UserStore r, Member GalleyAPIAccess r, Member (Error UserSubsystemError) r, - Member InvitationCodeStore r, + Member InvitationStore r, Member IndexedUserStore r, Member Metrics r, Member Events r, diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index c0d68dadc93..a13271b863a 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -72,10 +72,10 @@ import Wire.GalleyAPIAccess import Wire.HashPassword (HashPassword) import Wire.IndexedUserStore import Wire.InternalEvent hiding (DeleteUser) -import Wire.InvitationCodeStore +import Wire.InvitationStore import Wire.MockInterpreters import Wire.MockInterpreters.ActivationCodeStore (inMemoryActivationCodeStoreInterpreter) -import Wire.MockInterpreters.InvitationCodeStore (inMemoryInvitationCodeStoreInterpreter) +import Wire.MockInterpreters.InvitationStore (inMemoryInvitationStoreInterpreter) import Wire.PasswordResetCodeStore import Wire.PasswordStore import Wire.Sem.Concurrency @@ -137,10 +137,10 @@ type MiniBackendEffects = UserSubsystem ': MiniBackendLowerEffects type MiniBackendLowerEffects = [ EmailSubsystem, GalleyAPIAccess, - InvitationCodeStore, + InvitationStore, PasswordStore, State (Map (TeamId, InvitationId) StoredInvitation), - State (Map InvitationCode StoredInvitationInfo), + State (Map InvitationCode StoredInvitation), ActivationCodeStore, State (Map EmailKey (Maybe UserId, ActivationCode)), BlockListStore, @@ -178,7 +178,7 @@ data MiniBackend = MkMiniBackend passwordResetCodes :: Map PasswordResetKey (PRQueryData Identity), blockList :: [EmailKey], activationCodes :: Map EmailKey (Maybe UserId, ActivationCode), - invitationInfos :: Map InvitationCode StoredInvitationInfo, + invitationInfos :: Map InvitationCode StoredInvitation, invitations :: Map (TeamId, InvitationId) StoredInvitation } deriving stock (Eq, Show, Generic) @@ -428,20 +428,20 @@ interpretMaybeFederationStackState maybeFederationAPIAccess localBackend teamMem . liftActivationCodeStoreState . inMemoryActivationCodeStoreInterpreter . liftInvitationInfoStoreState - . liftInvitationCodeStoreState + . liftInvitationStoreState . runInMemoryPasswordStoreInterpreter - . inMemoryInvitationCodeStoreInterpreter + . inMemoryInvitationStoreInterpreter . miniGalleyAPIAccess teamMember galleyConfigs . noopEmailSubsystemInterpreter . userSubsystemInterpreter -liftInvitationInfoStoreState :: (Member (State MiniBackend) r) => Sem (State (Map InvitationCode StoredInvitationInfo) : r) a -> Sem r a +liftInvitationInfoStoreState :: (Member (State MiniBackend) r) => Sem (State (Map InvitationCode StoredInvitation) : r) a -> Sem r a liftInvitationInfoStoreState = interpret \case Polysemy.State.Get -> gets (.invitationInfos) Put newAcs -> modify $ \b -> b {invitationInfos = newAcs} -liftInvitationCodeStoreState :: (Member (State MiniBackend) r) => Sem (State (Map (TeamId, InvitationId) StoredInvitation) : r) a -> Sem r a -liftInvitationCodeStoreState = interpret \case +liftInvitationStoreState :: (Member (State MiniBackend) r) => Sem (State (Map (TeamId, InvitationId) StoredInvitation) : r) a -> Sem r a +liftInvitationStoreState = interpret \case Polysemy.State.Get -> gets (.invitations) Put newInvs -> modify $ \b -> b {invitations = newInvs} diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationCodeStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationStore.hs similarity index 55% rename from libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationCodeStore.hs rename to libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationStore.hs index 18f00055865..3de35c3da6a 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationCodeStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/InvitationStore.hs @@ -1,6 +1,4 @@ -{-# LANGUAGE RecordWildCards #-} - -module Wire.MockInterpreters.InvitationCodeStore where +module Wire.MockInterpreters.InvitationStore where import Data.Id (InvitationId, TeamId) import Data.Map (elems, (!?)) @@ -9,23 +7,21 @@ import Imports import Polysemy import Polysemy.State (State, get, gets) import Wire.API.User (InvitationCode (..)) -import Wire.InvitationCodeStore +import Wire.InvitationStore -inMemoryInvitationCodeStoreInterpreter :: +inMemoryInvitationStoreInterpreter :: forall r. ( Member (State (Map (TeamId, InvitationId) StoredInvitation)) r, - Member (State (Map (InvitationCode) StoredInvitationInfo)) r + Member (State (Map (InvitationCode) StoredInvitation)) r ) => - InterpreterFor InvitationCodeStore r -inMemoryInvitationCodeStoreInterpreter = interpret \case + InterpreterFor InvitationStore r +inMemoryInvitationStoreInterpreter = interpret \case InsertInvitation _a _timeout -> error "InsertInvitation" LookupInvitation tid iid -> gets (!? (tid, iid)) - LookupInvitationInfo iid -> gets (!? iid) - LookupInvitationCodesByEmail em -> - let c MkStoredInvitation {..} - | email == em = Just MkStoredInvitationInfo {..} - | otherwise = Nothing - in mapMaybe c . elems <$> get + LookupInvitationByCode iid -> gets (!? iid) + LookupInvitationsByEmail em -> + let c i = guard (i.email == em) $> i + in mapMaybe c . elems <$> get @(Map (TeamId, InvitationId) _) LookupInvitationsPaginated {} -> error "LookupInvitationsPaginated" CountInvitations tid -> gets (fromIntegral . M.size . M.filterWithKey (\(tid', _) _v -> tid == tid')) DeleteInvitation _tid _invId -> error "DeleteInvitation" diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 721f8479645..c573d4709c5 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -29,8 +29,8 @@ import Wire.API.Team.Permission import Wire.API.User hiding (DeleteUser) import Wire.API.UserEvent import Wire.AuthenticationSubsystem.Error -import Wire.InvitationCodeStore (StoredInvitation) -import Wire.InvitationCodeStore qualified as InvitationStore +import Wire.InvitationStore (StoredInvitation) +import Wire.InvitationStore qualified as InvitationStore import Wire.MiniBackend import Wire.StoredUser import Wire.UserKeyStore diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 668e20b9a17..54ff613f5e4 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -102,8 +102,8 @@ library Wire.IndexedUserStore.MigrationStore Wire.IndexedUserStore.MigrationStore.ElasticSearch Wire.InternalEvent - Wire.InvitationCodeStore - Wire.InvitationCodeStore.Cassandra + Wire.InvitationStore + Wire.InvitationStore.Cassandra Wire.NotificationSubsystem Wire.NotificationSubsystem.Interpreter Wire.ParseException @@ -243,7 +243,7 @@ test-suite wire-subsystems-tests Wire.MockInterpreters.GalleyAPIAccess Wire.MockInterpreters.HashPassword Wire.MockInterpreters.IndexedUserStore - Wire.MockInterpreters.InvitationCodeStore + Wire.MockInterpreters.InvitationStore Wire.MockInterpreters.Now Wire.MockInterpreters.PasswordResetCodeStore Wire.MockInterpreters.PasswordStore diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 3d5c3b16fed..d5abf271fc1 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -103,7 +103,7 @@ import Wire.FederationConfigStore import Wire.FederationConfigStore qualified as E import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.IndexedUserStore (IndexedUserStore, getTeamSize) -import Wire.InvitationCodeStore +import Wire.InvitationStore import Wire.NotificationSubsystem import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PropertySubsystem @@ -132,7 +132,7 @@ servantSitemap :: Member UserSubsystem r, Member TeamInvitationSubsystem r, Member UserStore r, - Member InvitationCodeStore r, + Member InvitationStore r, Member UserKeyStore r, Member Rpc r, Member TinyLog r, @@ -196,7 +196,7 @@ accountAPI :: Member PropertySubsystem r, Member Events r, Member PasswordResetCodeStore r, - Member InvitationCodeStore r + Member InvitationStore r ) => ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = @@ -243,7 +243,7 @@ teamsAPI :: Member UserKeyStore r, Member (Concurrency 'Unsafe) r, Member TinyLog r, - Member InvitationCodeStore r, + Member InvitationStore r, Member TeamInvitationSubsystem r, Member UserSubsystem r, Member (Polysemy.Error.Error UserSubsystemError) r, @@ -462,7 +462,7 @@ createUserNoVerify :: Member (UserPendingActivationStore p) r, Member TinyLog r, Member Events r, - Member InvitationCodeStore r, + Member InvitationStore r, Member UserKeyStore r, Member UserSubsystem r, Member (Input (Local ())) r, diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 3d50eea50e1..838d5979403 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -157,7 +157,7 @@ import Wire.FederationConfigStore (FederationConfigStore) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.IndexedUserStore (IndexedUserStore) -import Wire.InvitationCodeStore +import Wire.InvitationStore import Wire.NotificationSubsystem import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PasswordStore (PasswordStore, lookupHashedPassword) @@ -283,7 +283,7 @@ servantSitemap :: Member Events r, Member FederationConfigStore r, Member GalleyAPIAccess r, - Member InvitationCodeStore r, + Member InvitationStore r, Member Jwk r, Member JwtTools r, Member NotificationSubsystem r, @@ -731,7 +731,7 @@ upgradePersonalToTeam luid bNewTeam = createUser :: ( Member BlockListStore r, Member GalleyAPIAccess r, - Member InvitationCodeStore r, + Member InvitationStore r, Member (UserPendingActivationStore p) r, Member (Input (Local ())) r, Member TinyLog r, diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 0477c86f754..6d40b450a4c 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -137,8 +137,8 @@ import Wire.Error import Wire.Events (Events) import Wire.Events qualified as Events import Wire.GalleyAPIAccess as GalleyAPIAccess -import Wire.InvitationCodeStore (InvitationCodeStore, StoredInvitation) -import Wire.InvitationCodeStore qualified as InvitationCodeStore +import Wire.InvitationStore (InvitationStore, StoredInvitation) +import Wire.InvitationStore qualified as InvitationStore import Wire.NotificationSubsystem import Wire.PasswordResetCodeStore (PasswordResetCodeStore) import Wire.PasswordStore (PasswordStore, lookupHashedPassword, upsertHashedPassword) @@ -316,7 +316,7 @@ createUser :: Member Events r, Member (Input (Local ())) r, Member PasswordResetCodeStore r, - Member InvitationCodeStore r + Member InvitationStore r ) => NewUser -> ExceptT RegisterError (AppT r) CreateUserResult @@ -456,7 +456,7 @@ createUser new = do . field "team" (toByteString $ inv.teamId) . msg (val "Accepting invitation") UserPendingActivationStore.remove uid - InvitationCodeStore.deleteInvitation inv.teamId inv.invitationId + InvitationStore.deleteInvitation inv.teamId inv.invitationId addUserToTeamSSO :: User -> TeamId -> UserIdentity -> ExceptT RegisterError (AppT r) CreateUserTeam addUserToTeamSSO account tid ident = do diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 4b6af606d58..b2967854fd6 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -57,8 +57,8 @@ import Wire.GundeckAPIAccess import Wire.HashPassword import Wire.IndexedUserStore import Wire.IndexedUserStore.ElasticSearch -import Wire.InvitationCodeStore (InvitationCodeStore) -import Wire.InvitationCodeStore.Cassandra (interpretInvitationCodeStoreToCassandra) +import Wire.InvitationStore (InvitationStore) +import Wire.InvitationStore.Cassandra (interpretInvitationStoreToCassandra) import Wire.NotificationSubsystem import Wire.NotificationSubsystem.Interpreter (defaultNotificationSubsystemConfig, runNotificationSubsystemGundeck) import Wire.ParseException @@ -130,7 +130,7 @@ type BrigLowerLevelEffects = PasswordStore, VerificationCodeStore, ActivationCodeStore, - InvitationCodeStore, + InvitationStore, PropertyStore, SFT, ConnectionStore InternalPaging, @@ -254,7 +254,7 @@ runBrigToIO e (AppT ma) = do . connectionStoreToCassandra . interpretSFT e.httpManager . interpretPropertyStoreCassandra e.casClient - . interpretInvitationCodeStoreToCassandra e.casClient + . interpretInvitationStoreToCassandra e.casClient . interpretActivationCodeStoreToCassandra e.casClient . interpretVerificationCodeStoreCassandra e.casClient . interpretPasswordStore e.casClient diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 0622ce07e7b..e6fcd9f0d43 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -34,7 +34,7 @@ import Brig.App as App import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Types.Team (TeamSize) import Control.Lens (view, (^.)) -import Control.Monad.Trans.Except (mapExceptT) +import Control.Monad.Trans.Except import Data.ByteString.Conversion (toByteString) import Data.Id import Data.List1 qualified as List1 @@ -55,6 +55,8 @@ import Servant hiding (Handler, JSON, addHeader) import System.Logger.Message as Log import URI.ByteString (Absolute, URIRef, laxURIParserOptions, parseURI) import Util.Logging (logFunction, logTeam) +import Wire.API.Error +import Wire.API.Error.Brig import Wire.API.Routes.Internal.Brig (FoundInvitationCode (FoundInvitationCode)) import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Named @@ -74,8 +76,8 @@ import Wire.Events (Events) import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..)) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess import Wire.IndexedUserStore (IndexedUserStore, getTeamSize) -import Wire.InvitationCodeStore (InvitationCodeStore (..), PaginatedResult (..), StoredInvitation (..)) -import Wire.InvitationCodeStore qualified as Store +import Wire.InvitationStore (InvitationStore (..), PaginatedResult (..), StoredInvitation (..)) +import Wire.InvitationStore qualified as Store import Wire.Sem.Concurrency import Wire.TeamInvitationSubsystem import Wire.UserKeyStore @@ -86,7 +88,7 @@ servantAPI :: ( Member GalleyAPIAccess r, Member TeamInvitationSubsystem r, Member UserSubsystem r, - Member Store.InvitationCodeStore r, + Member Store.InvitationStore r, Member TinyLog r, Member (Input TeamTemplates) r, Member (Input (Local ())) r, @@ -118,7 +120,7 @@ teamSizePublic uid tid = do getTeamSize tid getInvitationCode :: - ( Member Store.InvitationCodeStore r, + ( Member Store.InvitationStore r, Member (Error UserSubsystemError) r ) => TeamId -> @@ -191,7 +193,7 @@ logInvitationRequest context action = deleteInvitation :: ( Member GalleyAPIAccess r, - Member InvitationCodeStore r, + Member InvitationStore r, Member (Error UserSubsystemError) r ) => UserId -> @@ -206,7 +208,7 @@ listInvitations :: forall r. ( Member GalleyAPIAccess r, Member TinyLog r, - Member InvitationCodeStore r, + Member InvitationStore r, Member (Input TeamTemplates) r, Member (Input (Local ())) r, Member UserSubsystem r, @@ -319,7 +321,7 @@ mkInviteUrlPersonalUser ShowInvitationUrl team (InvitationCode c) = do getInvitation :: ( Member GalleyAPIAccess r, - Member InvitationCodeStore r, + Member InvitationStore r, Member TinyLog r, Member (Input TeamTemplates) r, Member (Error UserSubsystemError) r @@ -348,7 +350,7 @@ isPersonalUser uke = do Just account -> account.userStatus == Active && isNothing account.userTeam getInvitationByCode :: - ( Member Store.InvitationCodeStore r, + ( Member Store.InvitationStore r, Member (Error UserSubsystemError) r ) => InvitationCode -> @@ -358,28 +360,32 @@ getInvitationByCode c = do maybe (throw UserSubsystemInvalidInvitationCode) (pure . Store.invitationFromStored Nothing) inv headInvitationByEmail :: - (Member InvitationCodeStore r, Member TinyLog r) => + (Member InvitationStore r, Member TinyLog r) => EmailAddress -> Sem r Public.HeadInvitationByEmailResult headInvitationByEmail email = - Store.lookupInvitationCodesByEmail email >>= \case + Store.lookupInvitationsByEmail email >>= \case [] -> pure Public.InvitationByEmailNotFound [_code] -> pure Public.InvitationByEmail (_ : _ : _) -> do Log.info $ - Log.msg (Log.val "team_invidation_email: multiple pending invites from different teams for the same email") + Log.msg (Log.val "team_invitation_email: multiple pending invites from different teams for the same email") . Log.field "email" (show email) pure Public.InvitationByEmailMoreThanOne --- | FUTUREWORK: This should also respond with status 409 in case of --- @DB.InvitationByEmailMoreThanOne@. Refactor so that 'headInvitationByEmailH' and --- 'getInvitationByEmailH' are almost the same thing. +-- | FUTUREWORK: Refactor so that 'headInvitationByEmail' and +-- 'getInvitationByEmail' are almost the same thing. getInvitationByEmail :: - (Member Store.InvitationCodeStore r, Member TinyLog r) => + (Member Store.InvitationStore r) => EmailAddress -> (Handler r) Public.Invitation getInvitationByEmail email = do - inv <- lift . liftSem $ Store.lookupInvitationByEmail email + inv <- do + invs <- lift . liftSem $ Store.lookupInvitationsByEmail email + case invs of + [] -> pure Nothing + [inv] -> pure . Just $ inv + (_ : _ : _) -> throwStd $ errorToWai @'ConflictingInvitations maybe (throwStd (notFound "Invitation not found")) (pure . Store.invitationFromStored Nothing) inv suspendTeam :: @@ -389,7 +395,7 @@ suspendTeam :: Member UserSubsystem r, Member Events r, Member TinyLog r, - Member InvitationCodeStore r + Member InvitationStore r ) => TeamId -> (Handler r) NoContent From 3a240c7534ebc4ea37e07b1b849451e1d2941f66 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:42:00 +0200 Subject: [PATCH 101/136] [chore] Extract RabbitMQ queue clean-up step in Makefile (#4281) * Dummy * Extract rabbit queue clean-up step in Makefile. --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bf97abef597..ad9e1eb4f1a 100644 --- a/Makefile +++ b/Makefile @@ -49,13 +49,17 @@ install: init ./hack/bin/cabal-run-all-tests.sh ./hack/bin/cabal-install-artefacts.sh all +.PHONY: clean-rabbit +clean-rabbit: + rabbitmqadmin -f pretty_json list queues vhost name messages | jq -r '.[] | "rabbitmqadmin delete queue name=\(.name) --vhost=\(.vhost)"' | bash + # Clean .PHONY: full-clean full-clean: clean rm -rf ~/.cache/hie-bios rm -rf ./dist-newstyle ./.env direnv reload - rabbitmqadmin -f pretty_json list queues vhost name messages | jq -r '.[] | "rabbitmqadmin delete queue name=\(.name) --vhost=\(.vhost)"' | bash + clean-rabbit @echo -e "\n\n*** NOTE: you may want to also 'rm -rf ~/.cabal/store \$$CABAL_DIR/store', not sure.\n" .PHONY: clean From 4de3d3a3920979e0de1fb8e472a17fbb90a59253 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Mon, 7 Oct 2024 15:55:44 +0200 Subject: [PATCH 102/136] Misc(tm) (#4282) * Add `make c` variant without treefmt (it's slow!). * rm long-obsolete stack config files. * Source comments. Co-authored-by: Sven Tennie --- Makefile | 5 +- libs/wire-api/src/Wire/API/Message/Proto.hs | 1 + services/gundeck/test/unit/ThreadBudget.hs | 3 + snapshots/README.md | 24 ---- snapshots/wire-1.0.yaml | 122 -------------------- snapshots/wire-1.1.yaml | 8 -- snapshots/wire-1.2.yaml | 8 -- snapshots/wire-1.3.yaml | 7 -- snapshots/wire-1.4.yaml | 20 ---- snapshots/wire-2.0.yaml | 101 ---------------- snapshots/wire-2.1.yaml | 24 ---- snapshots/wire-2.2.yaml | 8 -- snapshots/wire-3.0.yaml | 104 ----------------- 13 files changed, 8 insertions(+), 427 deletions(-) delete mode 100644 snapshots/README.md delete mode 100644 snapshots/wire-1.0.yaml delete mode 100644 snapshots/wire-1.1.yaml delete mode 100644 snapshots/wire-1.2.yaml delete mode 100644 snapshots/wire-1.3.yaml delete mode 100644 snapshots/wire-1.4.yaml delete mode 100644 snapshots/wire-2.0.yaml delete mode 100644 snapshots/wire-2.1.yaml delete mode 100644 snapshots/wire-2.2.yaml delete mode 100644 snapshots/wire-3.0.yaml diff --git a/Makefile b/Makefile index ad9e1eb4f1a..1413ff10e82 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,10 @@ cabal.project.local: # Usage: make c package=brig test=1 .PHONY: c -c: treefmt +c: treefmt c-fast + +.PHONY: c +c-fast: cabal build $(WIRE_CABAL_BUILD_OPTIONS) $(package) || ( make clean-hint; false ) ifeq ($(test), 1) ./hack/bin/cabal-run-tests.sh $(package) $(testargs) diff --git a/libs/wire-api/src/Wire/API/Message/Proto.hs b/libs/wire-api/src/Wire/API/Message/Proto.hs index 21e698b0abd..68632c74b47 100644 --- a/libs/wire-api/src/Wire/API/Message/Proto.hs +++ b/libs/wire-api/src/Wire/API/Message/Proto.hs @@ -146,6 +146,7 @@ userEntryClients f c = (\x -> c {_userVal = x}) <$> field f (_userVal c) -------------------------------------------------------------------------------- -- Priority +-- | See also `Wire.API.Message.Priority` data Priority = LowPriority | HighPriority deriving stock (Eq, Show, Ord, Generic) diff --git a/services/gundeck/test/unit/ThreadBudget.hs b/services/gundeck/test/unit/ThreadBudget.hs index b5953867211..727bae4a8a7 100644 --- a/services/gundeck/test/unit/ThreadBudget.hs +++ b/services/gundeck/test/unit/ThreadBudget.hs @@ -52,6 +52,9 @@ newtype NumberOfThreads = NumberOfThreads {fromNumberOfThreads :: Int} -- | 'microseconds' determines how long one unit lasts. there is a trade-off of fast -- vs. robust in this whole setup. this type is supposed to help us find a good sweet spot. +-- +-- There is also `Milliseconds` (with small `s` after `Milli`) in "Data.Misc". maybe this +-- should be cleaned up... newtype MilliSeconds = MilliSeconds {fromMilliSeconds :: Int} deriving (Eq, Ord, Show, Generic, ToExpr) diff --git a/snapshots/README.md b/snapshots/README.md deleted file mode 100644 index 0a9f240cac0..00000000000 --- a/snapshots/README.md +++ /dev/null @@ -1,24 +0,0 @@ -This directory contains [custom Stack snapshots][custom] used for Wire code. - -[custom]: https://docs.haskellstack.org/en/stable/custom_snapshot/ - -Snapshot definitions should never be changed (once committed to `develop`), because in other -repositories we refer to snapshot definitions by URL. This goes for *ANY* change! What -matters is that the sha256 hash of the file remains intact! - -(Rationale: Stack only downloads snapshot definitions once, and never checks whether they have -changed. If a snapshot changes and you have a repo that depends on it, you will get -inconsistent results depending on whether you've built that repo before or not.) - -To add, modify, or remove packages, a new snapshot should be created. It can be based on the -previous snapshot version. For major changes, e.g. LTS bumps, it's better to create a snapshot -from scratch. - -Some packages in this snapshot reference tar files instead of Git repos. This is due to -several issues in Stack that make working with big Git repositories unpleasant: - - * https://github.com/commercialhaskell/stack/issues/4345 - * https://github.com/commercialhaskell/stack/issues/3551 - -Unless the fixes to those are released, it's recommended to use GitHub's tar releases for -packages with big repos. diff --git a/snapshots/wire-1.0.yaml b/snapshots/wire-1.0.yaml deleted file mode 100644 index 9902d55ac68..00000000000 --- a/snapshots/wire-1.0.yaml +++ /dev/null @@ -1,122 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: lts-12.10 -name: wire-1.0 - -packages: - -############################################################ -# Packages where we need specific lower/upper bounds -############################################################ - -- async-2.2.1 -- hinotify-0.4 -- fsnotify-0.3.0.1 -- base-prelude-1.3 -- base58-bytestring-0.1.0 -- cql-4.0.1 -- currency-codes-2.0.0.0 -- data-timeout-0.3 -- geoip2-0.3.1.0 -- mime-0.4.0.2 -- multiset-0.3.4.1 -- text-icu-translit-0.1.0.7 -- wai-middleware-gunzip-0.0.2 -- network-uri-static-0.1.1.0 # includes 'relativeReference' -- list-t-1.0.1 # v1.0.0.1 doesn't build -- unliftio-0.2.10 # for pooled concurrency utils in UnliftIO.Async -- network-2.7.0.2 # to get nicer errors when connections fail -- HaskellNet-SSL-0.3.4.1 # first version to support network-2.7 - -# DEPRECATED: hs-collectd not in use anymore, remove in next stack snapshot -- git: https://github.com/kim/hs-collectd - commit: 885da222be2375f78c7be36127620ed772b677c9 - -- git: https://github.com/kim/snappy-framing - commit: d99f702c0086729efd6848dea8a01e5266c3a61c - -- git: https://gitlab.com/twittner/wai-routing - commit: 7e996a93fec5901767f845a50316b3c18e51a61d - -# Includes the changes from -- git: https://gitlab.com/twittner/cql-io.git - commit: 8b91d053c469887a427e8c075cef43139fa189c4 - -############################################################ -# Packages that are not on Stackage -############################################################ - -- bloodhound-0.16.0.0 -- template-0.2.0.10 -- wai-route-0.4.0 -- text-format-0.3.2 -- redis-io-1.0.0 -- redis-resp-1.0.0 -- servant-multipart-0.11.2 -- wai-middleware-prometheus-1.0.0 -- prometheus-client-1.0.0 -- hedgehog-quickcheck-0.1 -- invertible-hxt-0.1 -- stomp-queue-0.3.1 -- stompl-0.5.0 - -############################################################ -# Forks -############################################################ - -# Our fork of multihash with relaxed upper bounds -- git: https://github.com/wireapp/haskell-multihash.git - commit: 300a6f46384bfca33e545c8bab52ef3717452d12 - -# Our fork of aws with minor fixes -- git: https://github.com/wireapp/aws - commit: 42695688fc20f80bf89cec845c57403954aab0a2 - -# https://github.com/hspec/hspec-wai/pull/49 -- git: https://github.com/wireapp/hspec-wai - commit: ca10d13deab929f1cc3a569abea2e7fbe35fdbe3 - -# Our fork of http-client gives us access to some guts that the upstream http-client doesn't -# expose; see -# -# The important commits for us are: -# -# * https://github.com/snoyberg/http-client/compare/master...wireapp:connection-guts -# -# The archive corresponds to commit 6a4ac55edf5e62574210c77a1468fa7accb81670. -- archive: https://github.com/wireapp/http-client/archive/wire-2019-01-25.tar.gz - subdirs: - - http-client - - http-client-openssl - - http-client-tls - - http-conduit - -# amazonka-1.6.0 is buggy: https://github.com/brendanhay/amazonka/issues/466 -# amazonka-HEAD is also buggy: https://github.com/brendanhay/amazonka/issues/490 -# -# Therefore we use our own fork of amazonka here. More precisely, we pull two libraries out of -# it: amazonka and amazonka-core. Other packages weren't changed between 1.6.0 and this -# commit, so we can use Stackage-supplied versions for them. -# -# The important commits for us are: -# -# * https://github.com/brendanhay/amazonka/commit/2688190f -# * https://github.com/brendanhay/amazonka/pull/493/files -# -# The archive corresponds to commit 52896fd46ef6812708e9e4d7456becc692698f6b. -- archive: https://github.com/wireapp/amazonka/archive/wire-2019-01-25.tar.gz - subdirs: - - amazonka - - core - -############################################################ -# Wire packages (only ones that change infrequently) -############################################################ - -- git: https://github.com/wireapp/cryptobox-haskell - commit: 7546a1a25635ef65183e3d44c1052285e8401608 # master (Jul 21, 2016) - -- git: https://github.com/wireapp/hsaml2 - commit: 000868849efd85ba82d2bf0ac5757f801d49ad5a # master (Sep 10, 2018) - -# DO NOT MODIFY THIS FILE. See README.md to learn why. diff --git a/snapshots/wire-1.1.yaml b/snapshots/wire-1.1.yaml deleted file mode 100644 index c3e61914ba6..00000000000 --- a/snapshots/wire-1.1.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-1.0.yaml -name: wire-1.1 - -packages: -- git: https://github.com/wireapp/hsaml2 - commit: 678997815033584e023205fe774d16201ccf8f62 # master (Feb 13, 2019) diff --git a/snapshots/wire-1.2.yaml b/snapshots/wire-1.2.yaml deleted file mode 100644 index ce06e85e192..00000000000 --- a/snapshots/wire-1.2.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-1.1.yaml -name: wire-1.2 - -packages: -- cql-io-1.1.0 # the MR in wire-1.0.yaml has been released on hackage. -- cql-io-tinylog-0.1.0 diff --git a/snapshots/wire-1.3.yaml b/snapshots/wire-1.3.yaml deleted file mode 100644 index f3774ab20a9..00000000000 --- a/snapshots/wire-1.3.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-1.2.yaml -name: wire-1.3 - -packages: -- tinylog-0.15.0 diff --git a/snapshots/wire-1.4.yaml b/snapshots/wire-1.4.yaml deleted file mode 100644 index 023c078c9ba..00000000000 --- a/snapshots/wire-1.4.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-1.3.yaml -name: wire-1.4 - -packages: - # http-client forked by wire, commit 032b6503ab0c47f8f85bf48e0beb1f895a95bb27 - # Contains patches on top of http-client-openssl-0.2.2.0: - # - 89136497b8e0fa0624c1451883eb011347203532 - # - 916b04313c6864e02ebed4278b43b971189c61cd - # - 64ebec4fe7b48c131b7d6f0f8d7a6c6cacae70e6 - # - 78ebeead1a2efb17c55fb72d2d0041295d1271b8 - # - 032b6503ab0c47f8f85bf48e0beb1f895a95bb27 - # These provide a hacky way to implement TLS certificate pinning. -- archive: https://github.com/wireapp/http-client/archive/wire-2019-11-04.tar.gz - subdirs: - - http-client - - http-client-openssl - - http-client-tls - - http-conduit diff --git a/snapshots/wire-2.0.yaml b/snapshots/wire-2.0.yaml deleted file mode 100644 index fffe8c8f6d0..00000000000 --- a/snapshots/wire-2.0.yaml +++ /dev/null @@ -1,101 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: lts-14.12 -name: wire-2.0 - -# compiler: ghc-8.6.5 - -packages: -- git: https://github.com/kim/hs-collectd - commit: 885da222be2375f78c7be36127620ed772b677c9 - -- git: https://github.com/kim/snappy-framing - commit: d99f702c0086729efd6848dea8a01e5266c3a61c - -- git: https://gitlab.com/twittner/wai-routing - commit: 7e996a93fec5901767f845a50316b3c18e51a61d - -# Our fork of multihash with relaxed upper bounds -- git: https://github.com/wireapp/haskell-multihash.git - commit: 300a6f46384bfca33e545c8bab52ef3717452d12 - -# Our fork of aws with minor fixes -- git: https://github.com/wireapp/aws - commit: 42695688fc20f80bf89cec845c57403954aab0a2 - -# https://github.com/hspec/hspec-wai/pull/49 -- git: https://github.com/wireapp/hspec-wai - commit: 0a5142cd3ba48116ff059c041348b817fb7bdb25 - -# amazonka-1.6.0 is buggy: https://github.com/brendanhay/amazonka/issues/466 -# amazonka-HEAD is also buggy: https://github.com/brendanhay/amazonka/issues/490 -# -# Therefore we use our own fork of amazonka here. More precisely, we pull two libraries out of -# it: amazonka and amazonka-core. Other packages weren't changed between 1.6.0 and this -# commit, so we can use Stackage-supplied versions for them. -# -# The important commits for us are: -# -# * https://github.com/brendanhay/amazonka/commit/2688190f -# * https://github.com/brendanhay/amazonka/pull/493/files -# -# The archive corresponds to commit 52896fd46ef6812708e9e4d7456becc692698f6b. -- archive: https://github.com/wireapp/amazonka/archive/wire-2019-01-25.tar.gz - sha256: b1cecd0e5e17cd41395ec56e4f6926e0c8bbeef493ff3a575bf7561b72db0525 - size: 11128501 - subdirs: - - amazonka - - core - -############################################################ -# Wire packages (only ones that change infrequently) -############################################################ - -- git: https://github.com/wireapp/cryptobox-haskell - commit: 7546a1a25635ef65183e3d44c1052285e8401608 # master (Jul 21, 2016) - -- git: https://github.com/wireapp/hsaml2 - commit: 2d56f432464e9bf6be8ee214d7f5bb28639457ac # master (Feb 4, 2020) - -- git: https://github.com/wireapp/http-client - commit: a160cef95d9daaff7d9cfe616d95754c2f8202bf # master (Feb 4, 2020) - subdirs: - - http-client - - http-client-openssl - - http-client-tls - - http-conduit - -# Dropped from upstream snapshot -- bloodhound-0.16.0.0 -- template-0.2.0.10 -- HaskellNet-0.5.1 -- HaskellNet-SSL-0.3.4.1 -- snappy-0.2.0.2 -- smtp-mail-0.2.0.0 -- stm-containers-1.1.0.4 -- redis-io-1.0.0 -- redis-resp-1.0.0 -- hedgehog-quickcheck-0.1.1 - -# Only in nightly -- stm-hamt-1.2.0.4 -- optics-th-0.2 -- primitive-unlifted-0.1.2.0 - -# Not on stackage -- currency-codes-3.0.0.1 -- mime-0.4.0.2 -- data-timeout-0.3.1 -- geoip2-0.4.0.1 -- stomp-queue-0.3.1 -- text-icu-translit-0.1.0.7 -- wai-middleware-gunzip-0.0.2 -- cql-io-tinylog-0.1.0 -- invertible-hxt-0.1 -- network-uri-static-0.1.2.1 -- base58-bytestring-0.1.0 -- stompl-0.5.0 -- pattern-trie-0.1.0 - -# Not latest as latst one breaks wai-routing -- wai-route-0.4.0 diff --git a/snapshots/wire-2.1.yaml b/snapshots/wire-2.1.yaml deleted file mode 100644 index 057d5182856..00000000000 --- a/snapshots/wire-2.1.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-2.0.yaml -name: wire-2.1 - -packages: -# amazonka-1.6.1 is buggy: https://github.com/brendanhay/amazonka/issues/466 -# Therefore we pin an unreleased commit directly. -# -# More precisely, we pull just some libraries out of it, -# the other packages weren't changed between 1.6.1 and this commit, -# so we can use Stackage-supplied versions for them. -# See https://github.com/brendanhay/amazonka/compare/1.6.1...9cf5b5777b69ac494d23d43a692294882927df34 -# -# Once there has been made a new hackage release, we can use that instead. -- archive: https://github.com/brendanhay/amazonka/archive/9cf5b5777b69ac494d23d43a692294882927df34.tar.gz - sha256: c3044f803a7652aee88fe600a97321175cdc1443d671246ba7ff78e14bf5b49f - size: 11137527 - subdirs: - - amazonka - - amazonka-elb - - amazonka-redshift - - amazonka-route53 - - core diff --git a/snapshots/wire-2.2.yaml b/snapshots/wire-2.2.yaml deleted file mode 100644 index 26b49c246a1..00000000000 --- a/snapshots/wire-2.2.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: https://raw.githubusercontent.com/wireapp/wire-server/develop/snapshots/wire-2.1.yaml -name: wire-2.2 - -packages: -- git: https://github.com/wireapp/hsaml2 - commit: cc47da1d097b0b26595b8889e40c33c6c0c1c551 # master (Feb 27, 2020) diff --git a/snapshots/wire-3.0.yaml b/snapshots/wire-3.0.yaml deleted file mode 100644 index 655d191c63e..00000000000 --- a/snapshots/wire-3.0.yaml +++ /dev/null @@ -1,104 +0,0 @@ -# DO NOT MODIFY THIS FILE. See README.md to learn why. - -resolver: lts-14.12 -name: wire-3.0 - -# compiler: ghc-8.6.5 - -packages: -- git: https://github.com/kim/hs-collectd - commit: 885da222be2375f78c7be36127620ed772b677c9 - -- git: https://github.com/kim/snappy-framing - commit: d99f702c0086729efd6848dea8a01e5266c3a61c - -- git: https://gitlab.com/twittner/wai-routing - commit: 7e996a93fec5901767f845a50316b3c18e51a61d - -# Includes the changes from -# - git: https://gitlab.com/twittner/cql-io.git -# commit: 8b91d053c469887a427e8c075cef43139fa189c4 - -# Our fork of multihash with relaxed upper bounds -- git: https://github.com/wireapp/haskell-multihash.git - commit: 300a6f46384bfca33e545c8bab52ef3717452d12 - -# Our fork of aws with minor fixes -- git: https://github.com/wireapp/aws - commit: 42695688fc20f80bf89cec845c57403954aab0a2 - -# https://github.com/hspec/hspec-wai/pull/49 -- git: https://github.com/wireapp/hspec-wai - commit: 0a5142cd3ba48116ff059c041348b817fb7bdb25 - -# amazonka-1.6.1 is buggy: https://github.com/brendanhay/amazonka/issues/466 -# Therefore we pin an unreleased commit directly. -# -# More precisely, we pull just some libraries out of it, -# the other packages weren't changed between 1.6.1 and this commit, -# so we can use Stackage-supplied versions for them. -# See https://github.com/brendanhay/amazonka/compare/1.6.1...9cf5b5777b69ac494d23d43a692294882927df34 -# -# Once there has been made a new hackage release, we can use that instead. -- archive: https://github.com/brendanhay/amazonka/archive/9cf5b5777b69ac494d23d43a692294882927df34.tar.gz - sha256: c3044f803a7652aee88fe600a97321175cdc1443d671246ba7ff78e14bf5b49f - size: 11137527 - subdirs: - - amazonka - - amazonka-elb - - amazonka-redshift - - amazonka-route53 - - core - -############################################################ -# Wire packages (only ones that change infrequently) -############################################################ - -- git: https://github.com/wireapp/cryptobox-haskell - commit: 7546a1a25635ef65183e3d44c1052285e8401608 # master (Jul 21, 2016) - -- git: https://github.com/wireapp/hsaml2 - commit: cc47da1d097b0b26595b8889e40c33c6c0c1c551 # master (Feb 27, 2020) - -- git: https://github.com/wireapp/http-client - commit: a160cef95d9daaff7d9cfe616d95754c2f8202bf # master (Feb 4, 2020) - subdirs: - - http-client - - http-client-openssl - - http-client-tls - - http-conduit - -# Dropped from upstream snapshot -- bloodhound-0.16.0.0 -- template-0.2.0.10 -- HaskellNet-0.5.1 -- HaskellNet-SSL-0.3.4.1 -- snappy-0.2.0.2 -- smtp-mail-0.2.0.0 -- stm-containers-1.1.0.4 -- redis-io-1.0.0 -- redis-resp-1.0.0 -- hedgehog-quickcheck-0.1.1 - -# Only in nightly -- stm-hamt-1.2.0.4 -- optics-th-0.2 -- primitive-unlifted-0.1.2.0 - -# Not on stackage -- currency-codes-3.0.0.1 -- mime-0.4.0.2 -- data-timeout-0.3.1 -- geoip2-0.4.0.1 -- stomp-queue-0.3.1 -- text-icu-translit-0.1.0.7 -- wai-middleware-gunzip-0.0.2 -- cql-io-tinylog-0.1.0 -- invertible-hxt-0.1 -- network-uri-static-0.1.2.1 -- base58-bytestring-0.1.0 -- stompl-0.5.0 -- pattern-trie-0.1.0 - -# Not latest as latst one breaks wai-routing -- wai-route-0.4.0 From 1f13ef219b7b6e5d8948b369b767540612994c9d Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Wed, 9 Oct 2024 08:49:08 +0200 Subject: [PATCH 103/136] [Brig] Move password verification to the AuthenticationSubsystem, move to Argon2id with new settings. (#4271) --- changelog.d/5-internal/pwd | 1 + libs/types-common/src/Data/Misc.hs | 6 +- libs/wire-api/src/Wire/API/Password.hs | 262 ++++++++++-------- libs/wire-api/src/Wire/API/Provider.hs | 1 + .../test/unit/Test/Wire/API/Password.hs | 15 +- .../src/Wire/AuthenticationSubsystem.hs | 8 +- .../AuthenticationSubsystem/Interpreter.hs | 73 +++-- libs/wire-subsystems/src/Wire/HashPassword.hs | 2 +- .../wire-subsystems/src/Wire/PasswordStore.hs | 1 + .../src/Wire/PasswordStore/Cassandra.hs | 10 + .../src/Wire/UserSubsystem/Interpreter.hs | 2 +- .../Wire/MockInterpreters/HashPassword.hs | 11 +- .../Wire/MockInterpreters/PasswordStore.hs | 1 + services/brig/brig.integration.yaml | 2 +- services/brig/src/Brig/API/Auth.hs | 14 +- services/brig/src/Brig/API/Client.hs | 12 +- services/brig/src/Brig/API/Internal.hs | 6 +- services/brig/src/Brig/API/OAuth.hs | 29 +- services/brig/src/Brig/API/Public.hs | 5 +- services/brig/src/Brig/API/User.hs | 2 +- services/brig/src/Brig/Data/Client.hs | 18 +- services/brig/src/Brig/Data/User.hs | 28 +- services/brig/src/Brig/Provider/API.hs | 139 +++++++--- services/brig/src/Brig/Provider/DB.hs | 15 +- services/brig/src/Brig/User/Auth.hs | 15 +- .../brig/test/integration/API/User/Auth.hs | 4 +- services/galley/src/Galley/API/Update.hs | 4 +- 27 files changed, 427 insertions(+), 259 deletions(-) create mode 100644 changelog.d/5-internal/pwd diff --git a/changelog.d/5-internal/pwd b/changelog.d/5-internal/pwd new file mode 100644 index 00000000000..d0789bc9df4 --- /dev/null +++ b/changelog.d/5-internal/pwd @@ -0,0 +1 @@ +Changed default password hashing from Scrypt to Argon2id. diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index fc896fb1e59..2ee31511d75 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -378,8 +378,6 @@ showT = Text.pack . show {-# INLINE showT #-} -- | Decodes a base64 'Text' to a regular 'ByteString' (if possible) -from64 :: Text -> Maybe ByteString -from64 = hush . B64.decode . encodeUtf8 - where - hush = either (const Nothing) Just +from64 :: Text -> Either String ByteString +from64 = B64.decode . encodeUtf8 {-# INLINE from64 #-} diff --git a/libs/wire-api/src/Wire/API/Password.hs b/libs/wire-api/src/Wire/API/Password.hs index c7aa15111ff..0935b4ca5a5 100644 --- a/libs/wire-api/src/Wire/API/Password.hs +++ b/libs/wire-api/src/Wire/API/Password.hs @@ -1,4 +1,6 @@ {-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StrictData #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -15,25 +17,25 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} -{-# OPTIONS_GHC -Wno-unused-top-binds #-} module Wire.API.Password - ( Password, + ( Password (..), PasswordStatus (..), genPassword, - mkSafePasswordScrypt, - mkSafePasswordArgon2id, + mkSafePassword, verifyPassword, verifyPasswordWithStatus, - unsafeMkPassword, + PasswordReqBody (..), + + -- * Only for testing hashPasswordArgon2idWithSalt, hashPasswordArgon2idWithOptions, - PasswordReqBody (..), + mkSafePasswordScrypt, + parsePassword, ) where -import Cassandra +import Cassandra hiding (params) import Crypto.Error import Crypto.KDF.Argon2 qualified as Argon2 import Crypto.KDF.Scrypt as Scrypt @@ -52,8 +54,9 @@ import Imports import OpenSSL.Random (randBytes) -- | A derived, stretched password that can be safely stored. -newtype Password = Password - {fromPassword :: Text} +data Password + = Argon2Password Argon2HashedPassword + | ScryptPassword ScryptHashedPassword instance Show Password where show _ = "" @@ -61,13 +64,26 @@ instance Show Password where instance Cql Password where ctype = Tagged BlobColumn - fromCql (CqlBlob lbs) = pure . Password . Text.decodeUtf8 . toStrict $ lbs + fromCql (CqlBlob lbs) = parsePassword . Text.decodeUtf8 . toStrict $ lbs fromCql _ = Left "password: expected blob" - toCql = CqlBlob . fromStrict . Text.encodeUtf8 . fromPassword + toCql pw = CqlBlob . fromStrict $ Text.encodeUtf8 encoded + where + encoded = case pw of + Argon2Password argon2pw -> encodeArgon2HashedPassword argon2pw + ScryptPassword scryptpw -> encodeScryptPassword scryptpw -unsafeMkPassword :: Text -> Password -unsafeMkPassword = Password +data Argon2HashedPassword = Argon2HashedPassword + { opts :: Argon2.Options, + salt :: ByteString, + hashedKey :: ByteString + } + +data ScryptHashedPassword = ScryptHashedPassword + { params :: ScryptParameters, + salt :: ByteString, + hashedKey :: ByteString + } data PasswordStatus = PasswordStatusOk @@ -76,8 +92,6 @@ data PasswordStatus ------------------------------------------------------------------------------- -type Argon2idOptions = Argon2.Options - data ScryptParameters = ScryptParameters { -- | Bytes to randomly generate as a unique salt, default is __32__ saltLength :: Word32, @@ -106,13 +120,15 @@ defaultScryptParams = outputLength = 64 } --- | These are the default values suggested, as extracted from the crypton library. -defaultOptions :: Argon2idOptions +-- | Recommended in the RFC as the second choice: https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice +-- The first choice takes ~1s to hash passwords which seems like too much. +defaultOptions :: Argon2.Options defaultOptions = Argon2.Options - { iterations = 5, + { iterations = 1, + -- TODO: fix this after meeting with Security memory = 2 ^ (17 :: Int), - parallelism = 4, + parallelism = 32, variant = Argon2.Argon2id, version = Argon2.Version13 } @@ -136,10 +152,10 @@ genPassword = randBytes 12 mkSafePasswordScrypt :: (MonadIO m) => PlainTextPassword' t -> m Password -mkSafePasswordScrypt = fmap Password . hashPasswordScrypt . Text.encodeUtf8 . fromPlainTextPassword +mkSafePasswordScrypt = fmap ScryptPassword . hashPasswordScrypt . Text.encodeUtf8 . fromPlainTextPassword -mkSafePasswordArgon2id :: (MonadIO m) => PlainTextPassword' t -> m Password -mkSafePasswordArgon2id = fmap Password . hashPasswordArgon2id . Text.encodeUtf8 . fromPlainTextPassword +mkSafePassword :: (MonadIO m) => PlainTextPassword' t -> m Password +mkSafePassword = fmap Argon2Password . hashPasswordArgon2id . Text.encodeUtf8 . fromPlainTextPassword -- | Verify a plaintext password from user input against a stretched -- password from persistent storage. @@ -147,37 +163,49 @@ verifyPassword :: PlainTextPassword' t -> Password -> Bool verifyPassword = (fst .) . verifyPasswordWithStatus verifyPasswordWithStatus :: PlainTextPassword' t -> Password -> (Bool, PasswordStatus) -verifyPasswordWithStatus plain opaque = - let actual = fromPlainTextPassword plain - expected = fromPassword opaque - in checkPassword actual expected +verifyPasswordWithStatus (fromPlainTextPassword -> plain) hashed = + case hashed of + (Argon2Password Argon2HashedPassword {..}) -> + let producedKey = hashPasswordWithOptions opts (Text.encodeUtf8 plain) salt + in (hashedKey `constEq` producedKey, PasswordStatusOk) + (ScryptPassword ScryptHashedPassword {..}) -> + let producedKey = hashPasswordWithParams params (Text.encodeUtf8 plain) salt + in (hashedKey `constEq` producedKey, PasswordStatusNeedsUpdate) -hashPasswordScrypt :: (MonadIO m) => ByteString -> m Text +hashPasswordScrypt :: (MonadIO m) => ByteString -> m ScryptHashedPassword hashPasswordScrypt password = do salt <- newSalt $ fromIntegral defaultScryptParams.saltLength - let key = hashPasswordWithParams defaultScryptParams password salt - pure $ - Text.intercalate - "|" - [ showT defaultScryptParams.rounds, - showT defaultScryptParams.blockSize, - showT defaultScryptParams.parallelism, - Text.decodeUtf8 . B64.encode $ salt, - Text.decodeUtf8 . B64.encode $ key - ] - -hashPasswordArgon2id :: (MonadIO m) => ByteString -> m Text + let params = defaultScryptParams + let hashedKey = hashPasswordWithParams params password salt + pure $! ScryptHashedPassword {..} + +encodeScryptPassword :: ScryptHashedPassword -> Text +encodeScryptPassword ScryptHashedPassword {..} = + Text.intercalate + "|" + [ showT defaultScryptParams.rounds, + showT defaultScryptParams.blockSize, + showT defaultScryptParams.parallelism, + Text.decodeUtf8 . B64.encode $ salt, + Text.decodeUtf8 . B64.encode $ hashedKey + ] + +hashPasswordArgon2id :: (MonadIO m) => ByteString -> m Argon2HashedPassword hashPasswordArgon2id pwd = do - salt <- newSalt 32 - pure $ hashPasswordArgon2idWithSalt salt pwd + salt <- newSalt 16 + pure $! hashPasswordArgon2idWithSalt salt pwd -hashPasswordArgon2idWithSalt :: ByteString -> ByteString -> Text +hashPasswordArgon2idWithSalt :: ByteString -> ByteString -> Argon2HashedPassword hashPasswordArgon2idWithSalt = hashPasswordArgon2idWithOptions defaultOptions -hashPasswordArgon2idWithOptions :: Argon2idOptions -> ByteString -> ByteString -> Text +hashPasswordArgon2idWithOptions :: Argon2.Options -> ByteString -> ByteString -> Argon2HashedPassword hashPasswordArgon2idWithOptions opts salt pwd = do - let key = hashPasswordWithOptions opts pwd salt - optsStr = + let hashedKey = hashPasswordWithOptions opts pwd salt + in Argon2HashedPassword {..} + +encodeArgon2HashedPassword :: Argon2HashedPassword -> Text +encodeArgon2HashedPassword Argon2HashedPassword {..} = + let optsStr = Text.intercalate "," [ "m=" <> showT opts.memory, @@ -191,96 +219,100 @@ hashPasswordArgon2idWithOptions opts salt pwd = do "v=" <> versionToNum opts.version, optsStr, encodeWithoutPadding salt, - encodeWithoutPadding key + encodeWithoutPadding hashedKey ] where encodeWithoutPadding = Text.dropWhileEnd (== '=') . Text.decodeUtf8 . B64.encode -checkPassword :: Text -> Text -> (Bool, PasswordStatus) -checkPassword actual expected = +parsePassword :: Text -> Either String Password +parsePassword expected = case parseArgon2idPasswordHashOptions expected of - Just (opts, salt, hashedKey) -> - let producedKey = hashPasswordWithOptions opts (Text.encodeUtf8 actual) salt - in (hashedKey `constEq` producedKey, PasswordStatusOk) - Nothing -> + Right hashedPassword -> Right $ Argon2Password hashedPassword + Left argon2ParseError -> case parseScryptPasswordHashParams $ Text.encodeUtf8 expected of - Just (sparams, saltS, hashedKeyS) -> - let producedKeyS = hashPasswordWithParams sparams (Text.encodeUtf8 actual) saltS - in (hashedKeyS `constEq` producedKeyS, PasswordStatusNeedsUpdate) - Nothing -> (False, PasswordStatusNeedsUpdate) + Right hashedPassword -> Right $ ScryptPassword hashedPassword + Left scryptParseError -> + Left $ + "Failed to parse Argon2 or Scrypt. Argon2 parse error: " + <> argon2ParseError + <> ", Scrypt parse error: " + <> scryptParseError newSalt :: (MonadIO m) => Int -> m ByteString newSalt i = liftIO $ getRandomBytes i {-# INLINE newSalt #-} -parseArgon2idPasswordHashOptions :: Text -> Maybe (Argon2idOptions, ByteString, ByteString) +parseArgon2idPasswordHashOptions :: Text -> Either String Argon2HashedPassword parseArgon2idPasswordHashOptions passwordHash = do - let paramList = Text.split (== '$') passwordHash - guard (length paramList >= 5) - let (_ : variantT : vp : ps : sh : rest) = paramList - variant <- parseVariant variantT - case rest of - [hashedKey64] -> do - version <- parseVersion vp - parseAll variant version ps sh hashedKey64 - [] -> parseAll variant Argon2.Version10 vp ps sh - _ -> Nothing - where - parseVariant = splitMaybe "argon2" letterToVariant - parseVersion = splitMaybe "v=" numToVersion - -parseAll :: Argon2.Variant -> Argon2.Version -> Text -> Text -> Text -> Maybe (Argon2idOptions, ByteString, ByteString) -parseAll variant version parametersT salt64 hashedKey64 = do - (memory, iterations, parallelism) <- parseParameters parametersT - salt <- from64 $ unsafePad64 salt64 - hashedKey <- from64 $ unsafePad64 hashedKey64 - pure (Argon2.Options {..}, salt, hashedKey) + let paramsList = Text.split (== '$') passwordHash + -- The first param is empty string b/c the string begins with a separator `$`. + case paramsList of + ["", variantStr, verStr, opts, salt, hashedKey64] -> do + version <- parseVersion verStr + parseAll variantStr version opts salt hashedKey64 + ["", variantStr, opts, salt, hashedKey64] -> do + parseAll variantStr Argon2.Version10 opts salt hashedKey64 + _ -> Left $ "failed to parse argon2id hashed password, expected 5 or 6 params, got: " <> show (length paramsList) where - parseParameters paramsT = do - let paramsL = Text.split (== ',') paramsT - guard $ Imports.length paramsL == 3 - go paramsL (Nothing, Nothing, Nothing) + parseVersion = + maybe (Left "failed to parse argon2 version") Right + . splitMaybe "v=" numToVersion + + parseAll :: Text -> Argon2.Version -> Text -> Text -> Text -> Either String Argon2HashedPassword + parseAll variantStr version parametersStr salt64 hashedKey64 = do + variant <- parseVariant variantStr + (memory, iterations, parallelism) <- parseParameters parametersStr + -- We pad the Base64 with '=' chars because we drop them while encoding this. + -- At the time of implementation we've opted to be consistent with how the + -- CLI of the reference implementation of Argon2id outputs this. + salt <- from64 $ unsafePad64 salt64 + hashedKey <- from64 $ unsafePad64 hashedKey64 + pure $ Argon2HashedPassword {opts = (Argon2.Options {..}), ..} where - go [] (Just m, Just t, Just p) = Just (m, t, p) - go [] _ = Nothing - go (x : xs) (m, t, p) = - case Text.splitAt 2 x of - ("m=", i) -> go xs (readT i, t, p) - ("t=", i) -> go xs (m, readT i, p) - ("p=", i) -> go xs (m, t, readT i) - _ -> Nothing - -parseScryptPasswordHashParams :: ByteString -> Maybe (ScryptParameters, ByteString, ByteString) + parseVariant = + maybe (Left "failed to parse argon2 variant") Right + . splitMaybe "argon2" letterToVariant + parseParameters paramsT = + let paramsList = Text.split (== ',') paramsT + in go paramsList (Nothing, Nothing, Nothing) + where + go [] (Just m, Just t, Just p) = Right (m, t, p) + go [] (Nothing, _, _) = Left "failed to parse Argon2Options: failed to read parameter 'm'" + go [] (_, Nothing, _) = Left "failed to parse Argon2Options: failed to read parameter 't'" + go [] (_, _, Nothing) = Left "failed to parse Argon2Options: failed to read parameter 'p'" + go (x : xs) (m, t, p) = + case Text.splitAt 2 x of + ("m=", i) -> go xs (readT i, t, p) + ("t=", i) -> go xs (m, readT i, p) + ("p=", i) -> go xs (m, t, readT i) + (unknownParam, _) -> Left $ "failed to parse Argon2Options: Unknown param: " <> Text.unpack unknownParam + +parseScryptPasswordHashParams :: ByteString -> Either String ScryptHashedPassword parseScryptPasswordHashParams passwordHash = do let paramList = Text.split (== '|') . Text.decodeUtf8 $ passwordHash - guard (length paramList == 5) - let [ scryptRoundsT, - scryptBlockSizeT, - scryptParallelismT, - salt64, - hashedKey64 - ] = paramList - rounds <- readT scryptRoundsT - blockSize <- readT scryptBlockSizeT - parallelism <- readT scryptParallelismT - salt <- from64 salt64 - hashedKey <- from64 hashedKey64 - let outputLength = fromIntegral $ C8.length hashedKey - saltLength = fromIntegral $ C8.length salt - pure - ( ScryptParameters {..}, - salt, - hashedKey - ) + case paramList of + [roundsStr, blockSizeStr, parallelismStr, salt64, hashedKey64] -> do + rounds <- eitherFromMaybe "rounds" $ readT roundsStr + blockSize <- eitherFromMaybe "blockSize" $ readT blockSizeStr + parallelism <- eitherFromMaybe "parellelism" $ readT parallelismStr + salt <- from64 salt64 + hashedKey <- from64 hashedKey64 + let outputLength = fromIntegral $ C8.length hashedKey + saltLength = fromIntegral $ C8.length salt + pure $ ScryptHashedPassword {params = ScryptParameters {..}, ..} + _ -> Left $ "failed to parse ScryptHashedPassword: expected exactly 5 params" + where + eitherFromMaybe :: String -> Maybe a -> Either String a + eitherFromMaybe paramName = maybe (Left $ "failed to parse scrypt parameter: " <> paramName) Right ------------------------------------------------------------------------------- -hashPasswordWithOptions :: Argon2idOptions -> ByteString -> ByteString -> ByteString -hashPasswordWithOptions opts password salt = - case (Argon2.hash opts password salt 64) of +hashPasswordWithOptions :: Argon2.Options -> ByteString -> ByteString -> ByteString +hashPasswordWithOptions opts password salt = do + let tagSize = 16 + case (Argon2.hash opts password salt tagSize) of -- CryptoFailed occurs when salt, output or input are too small/big. -- since we control those values ourselves, it should never have a runtime error - -- unless we've caused it ourselves. CryptoFailed cErr -> error $ "Impossible error: " <> show cErr CryptoPassed hash -> hash diff --git a/libs/wire-api/src/Wire/API/Provider.hs b/libs/wire-api/src/Wire/API/Provider.hs index 8923fc6e5ed..fbde37da2ce 100644 --- a/libs/wire-api/src/Wire/API/Provider.hs +++ b/libs/wire-api/src/Wire/API/Provider.hs @@ -202,6 +202,7 @@ instance ToSchema ProviderLogin where -- DeleteProvider -- | Input data for a provider deletion request. +-- | FUTUREWORK: look into a phase out of PlainTextPassword6 newtype DeleteProvider = DeleteProvider {deleteProviderPassword :: PlainTextPassword6} deriving stock (Eq, Show) diff --git a/libs/wire-api/test/unit/Test/Wire/API/Password.hs b/libs/wire-api/test/unit/Test/Wire/API/Password.hs index 8850a377c79..2915365ef2b 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Password.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Password.hs @@ -14,6 +14,7 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} module Test.Wire.API.Password where @@ -32,12 +33,10 @@ tests = testCase "verify old scrypt password still works" testHashingOldScrypt ] --- TODO: Address password hashing being wrong --- https://wearezeta.atlassian.net/browse/WPB-9746 testHashPasswordScrypt :: IO () testHashPasswordScrypt = do pwd <- genPassword - hashed <- mkSafePasswordScrypt pwd + hashed <- mkSafePassword pwd let (correct, status) = verifyPasswordWithStatus pwd hashed assertBool "Password could not be verified" correct assertEqual "Password could not be verified" status PasswordStatusOk @@ -45,21 +44,21 @@ testHashPasswordScrypt = do testHashPasswordArgon2id :: IO () testHashPasswordArgon2id = do pwd <- genPassword - hashed <- mkSafePasswordArgon2id pwd + hashed <- mkSafePassword pwd let (correct, status) = verifyPasswordWithStatus pwd hashed - assertBool "Password could not be verified" correct assertEqual "Password could not be verified" status PasswordStatusOk + assertBool "Password could not be verified" correct testUpdateHash :: IO () testUpdateHash = do let orig = plainTextPassword8Unsafe "Test password scrypt to argon2id." -- password hashed with scrypt and random salt - expected = unsafeMkPassword "14|8|1|ktYx5i1DMOEfm+tXpw9i7ZVPdeqbxgxYxUbmDVLSAzQ=|Fzy0sNfXQQnJW98ncyN51PUChFWH1tpVJCxjz5JRZEReVa0//zJ6MeopiEh84Ny8lzwdvRPHDqnSS/lkPEB7Ow==" + Right expected = parsePassword "14|8|1|ktYx5i1DMOEfm+tXpw9i7ZVPdeqbxgxYxUbmDVLSAzQ=|Fzy0sNfXQQnJW98ncyN51PUChFWH1tpVJCxjz5JRZEReVa0//zJ6MeopiEh84Ny8lzwdvRPHDqnSS/lkPEB7Ow==" -- password re-hashed with argon2id and re-used salt for simplicity - newHash = unsafeMkPassword "$argon2id$v=19$m=131072,t=5,p=4$ktYx5i1DMOEfm+tXpw9i7ZVPdeqbxgxYxUbmDVLSAzQ=$iS/9tVk49W8bO/APETqNzMmREerdETTvSXcA7nSpqrsGrV1N33+MVaKnhWhBHqIxM92HFPsV5GP0dpgCUHmJRg==" -- verify password with scrypt (correct, status) = verifyPasswordWithStatus orig expected + newHash <- either assertFailure pure $ parsePassword "$argon2id$v=19$m=4194304,t=1,p=8$lj6+HdIcCpO1zvz8An56fg$Qx8OzYTq0hDNqGG9tW1dug" assertBool "Password did not match hash." correct assertEqual "Password could not be verified" status PasswordStatusNeedsUpdate @@ -72,7 +71,7 @@ testHashingOldScrypt :: IO () testHashingOldScrypt = forConcurrently_ pwds $ \pwd -> do let orig = plainTextPassword8Unsafe (fst pwd) - expected = unsafeMkPassword (snd pwd) + Right expected = parsePassword (snd pwd) (correct, status) = verifyPasswordWithStatus orig expected assertBool "Password did not match hash." correct assertEqual "Password could not be verified" status PasswordStatusNeedsUpdate diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs index e4200377d92..3b593a746c8 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs @@ -23,14 +23,18 @@ import Data.Misc import Data.Qualified import Imports import Polysemy +import Wire.API.Password (Password, PasswordStatus) import Wire.API.User -import Wire.API.User.Password +import Wire.API.User.Password (PasswordResetCode, PasswordResetIdentity) import Wire.UserKeyStore data AuthenticationSubsystem m a where - VerifyPassword :: Local UserId -> PlainTextPassword6 -> AuthenticationSubsystem m () CreatePasswordResetCode :: EmailKey -> AuthenticationSubsystem m () ResetPassword :: PasswordResetIdentity -> PasswordResetCode -> PlainTextPassword8 -> AuthenticationSubsystem m () + VerifyPassword :: PlainTextPassword6 -> Password -> AuthenticationSubsystem m (Bool, PasswordStatus) + VerifyUserPassword :: UserId -> PlainTextPassword6 -> AuthenticationSubsystem r (Bool, PasswordStatus) + VerifyUserPasswordError :: Local UserId -> PlainTextPassword6 -> AuthenticationSubsystem m () + VerifyProviderPassword :: ProviderId -> PlainTextPassword6 -> AuthenticationSubsystem r (Bool, PasswordStatus) -- For testing InternalLookupPasswordResetCode :: EmailKey -> AuthenticationSubsystem m (Maybe PasswordResetPair) diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs index 183d1130c6e..89dc1f3b39a 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs @@ -36,7 +36,7 @@ import Polysemy.TinyLog qualified as Log import System.Logger import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Allowlists qualified as AllowLists -import Wire.API.Password +import Wire.API.Password as Password import Wire.API.User import Wire.API.User.Password import Wire.AuthenticationSubsystem (AuthenticationSubsystem (..)) @@ -44,7 +44,8 @@ import Wire.AuthenticationSubsystem.Error import Wire.EmailSubsystem import Wire.HashPassword import Wire.PasswordResetCodeStore -import Wire.PasswordStore +import Wire.PasswordStore (PasswordStore) +import Wire.PasswordStore qualified as PasswordStore import Wire.Sem.Now import Wire.Sem.Now qualified as Now import Wire.SessionStore @@ -70,22 +71,15 @@ interpretAuthenticationSubsystem :: interpretAuthenticationSubsystem userSubsystemInterpreter = interpret $ userSubsystemInterpreter . \case - VerifyPassword luid password -> verifyPasswordImpl luid password CreatePasswordResetCode userKey -> createPasswordResetCodeImpl userKey ResetPassword ident resetCode newPassword -> resetPasswordImpl ident resetCode newPassword + VerifyPassword plaintext pwd -> verifyPasswordImpl plaintext pwd + VerifyUserPassword uid plaintext -> verifyUserPasswordImpl uid plaintext + VerifyUserPasswordError luid plaintext -> verifyUserPasswordErrorImpl luid plaintext + VerifyProviderPassword pid plaintext -> verifyProviderPasswordImpl pid plaintext + -- Testing InternalLookupPasswordResetCode userKey -> internalLookupPasswordResetCodeImpl userKey -verifyPasswordImpl :: - ( Member PasswordStore r, - Member (Error AuthenticationSubsystemError) r - ) => - Local UserId -> - PlainTextPassword6 -> - Sem r () -verifyPasswordImpl (tUnqualified -> uid) password = do - p <- lookupHashedPassword uid >>= maybe (throw AuthenticationSubsystemMissingAuth) pure - unless (Wire.API.Password.verifyPassword password p) $ throw AuthenticationSubsystemBadCredentials - maxAttempts :: Int32 maxAttempts = 3 @@ -149,7 +143,9 @@ createPasswordResetCodeImpl target = Right v -> pure v lookupActiveUserIdByUserKey :: - (Member UserSubsystem r, Member (Input (Local ())) r) => + ( Member UserSubsystem r, + Member (Input (Local ())) r + ) => EmailKey -> Sem r (Maybe UserId) lookupActiveUserIdByUserKey target = @@ -230,7 +226,7 @@ resetPasswordImpl ident code pw = do Log.debug $ field "user" (toByteString uid) . field "action" (val "User.completePasswordReset") checkNewIsDifferent uid pw hashedPw <- hashPassword pw - upsertHashedPassword uid hashedPw + PasswordStore.upsertHashedPassword uid hashedPw codeDelete key deleteAllCookies uid where @@ -246,10 +242,10 @@ resetPasswordImpl ident code pw = do checkNewIsDifferent :: UserId -> PlainTextPassword' t -> Sem r () checkNewIsDifferent uid newPassword = do - mCurrentPassword <- lookupHashedPassword uid + mCurrentPassword <- PasswordStore.lookupHashedPassword uid case mCurrentPassword of Just currentPassword - | (verifyPassword newPassword currentPassword) -> throw AuthenticationSubsystemResetPasswordMustDiffer + | (Password.verifyPassword newPassword currentPassword) -> throw AuthenticationSubsystemResetPasswordMustDiffer _ -> pure () verify :: PasswordResetPair -> Sem r (Maybe UserId) @@ -266,3 +262,44 @@ resetPasswordImpl ident code pw = do pure Nothing Just PRQueryData {} -> codeDelete k $> Nothing Nothing -> pure Nothing + +verifyPasswordImpl :: + PlainTextPassword6 -> + Password -> + Sem r (Bool, PasswordStatus) +verifyPasswordImpl plaintext password = do + pure $ Password.verifyPasswordWithStatus plaintext password + +verifyProviderPasswordImpl :: + (Member PasswordStore r, Member (Error AuthenticationSubsystemError) r) => + ProviderId -> + PlainTextPassword6 -> + Sem r (Bool, PasswordStatus) +verifyProviderPasswordImpl pid plaintext = do + -- We type-erase uid here + password <- + PasswordStore.lookupHashedProviderPassword pid + >>= maybe (throw AuthenticationSubsystemBadCredentials) pure + verifyPasswordImpl plaintext password + +verifyUserPasswordImpl :: + (Member PasswordStore r, Member (Error AuthenticationSubsystemError) r) => + UserId -> + PlainTextPassword6 -> + Sem r (Bool, PasswordStatus) +verifyUserPasswordImpl uid plaintext = do + password <- + PasswordStore.lookupHashedPassword uid + >>= maybe (throw AuthenticationSubsystemBadCredentials) pure + verifyPasswordImpl plaintext password + +verifyUserPasswordErrorImpl :: + ( Member PasswordStore r, + Member (Error AuthenticationSubsystemError) r + ) => + Local UserId -> + PlainTextPassword6 -> + Sem r () +verifyUserPasswordErrorImpl (tUnqualified -> uid) password = do + unlessM (fst <$> verifyUserPasswordImpl uid password) do + throw AuthenticationSubsystemBadCredentials diff --git a/libs/wire-subsystems/src/Wire/HashPassword.hs b/libs/wire-subsystems/src/Wire/HashPassword.hs index 54c65c3ee74..48444c0d691 100644 --- a/libs/wire-subsystems/src/Wire/HashPassword.hs +++ b/libs/wire-subsystems/src/Wire/HashPassword.hs @@ -15,4 +15,4 @@ makeSem ''HashPassword runHashPassword :: (Member (Embed IO) r) => InterpreterFor HashPassword r runHashPassword = interpret $ \case - HashPassword pw -> liftIO $ Password.mkSafePasswordScrypt pw + HashPassword pw -> liftIO $ Password.mkSafePassword pw diff --git a/libs/wire-subsystems/src/Wire/PasswordStore.hs b/libs/wire-subsystems/src/Wire/PasswordStore.hs index 48a358aa827..54b66aa02ea 100644 --- a/libs/wire-subsystems/src/Wire/PasswordStore.hs +++ b/libs/wire-subsystems/src/Wire/PasswordStore.hs @@ -10,5 +10,6 @@ import Wire.API.Password data PasswordStore m a where UpsertHashedPassword :: UserId -> Password -> PasswordStore m () LookupHashedPassword :: UserId -> PasswordStore m (Maybe Password) + LookupHashedProviderPassword :: ProviderId -> PasswordStore m (Maybe Password) makeSem ''PasswordStore diff --git a/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs index 933faeb298d..1503e2152cb 100644 --- a/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/PasswordStore/Cassandra.hs @@ -16,6 +16,12 @@ interpretPasswordStore casClient = runEmbedded (runClient casClient) . \case UpsertHashedPassword uid password -> embed $ updatePasswordImpl uid password LookupHashedPassword uid -> embed $ lookupPasswordImpl uid + LookupHashedProviderPassword pid -> embed $ lookupProviderPasswordImpl pid + +lookupProviderPasswordImpl :: (MonadClient m) => ProviderId -> m (Maybe Password) +lookupProviderPasswordImpl u = + (runIdentity =<<) + <$> retry x1 (query1 providerPasswordSelect (params LocalQuorum (Identity u))) lookupPasswordImpl :: (MonadClient m) => UserId -> m (Maybe Password) lookupPasswordImpl u = @@ -29,6 +35,10 @@ updatePasswordImpl u p = do ------------------------------------------------------------------------ -- Queries +providerPasswordSelect :: PrepQuery R (Identity ProviderId) (Identity (Maybe Password)) +providerPasswordSelect = + "SELECT password FROM provider WHERE id = ?" + passwordSelect :: PrepQuery R (Identity UserId) (Identity (Maybe Password)) passwordSelect = "SELECT password FROM user WHERE id = ?" diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index f0318d5bff7..bbcbe719eb8 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -925,7 +925,7 @@ acceptTeamInvitationImpl luid pw code = do mSelfProfile <- getSelfProfileImpl luid let mEmailKey = mkEmailKey <$> (userEmail . selfUser =<< mSelfProfile) mTid = mSelfProfile >>= userTeam . selfUser - verifyPassword luid pw + verifyUserPasswordError luid pw inv <- internalFindTeamInvitationImpl mEmailKey code let tid = inv.teamId let minvmeta = (,inv.createdAt) <$> inv.createdBy diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs index 05c15259bec..84c8897292a 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs @@ -10,11 +10,12 @@ import Wire.HashPassword staticHashPasswordInterpreter :: InterpreterFor HashPassword r staticHashPasswordInterpreter = interpret $ \case - HashPassword password -> go (hashPasswordArgon2idWithOptions fastArgon2IdOptions) "9bytesalt" password - where - go alg salt password = do - let passwordBS = Text.encodeUtf8 (fromPlainTextPassword password) - pure $ unsafeMkPassword $ alg salt passwordBS + HashPassword password -> + pure . Argon2Password $ + hashPasswordArgon2idWithOptions + fastArgon2IdOptions + "9bytesalt" + (Text.encodeUtf8 (fromPlainTextPassword password)) fastArgon2IdOptions :: Argon2.Options fastArgon2IdOptions = diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs index a90b9184eab..a0eb7fc845c 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/PasswordStore.hs @@ -15,3 +15,4 @@ inMemoryPasswordStoreInterpreter :: (Member (State (Map UserId Password)) r) => inMemoryPasswordStoreInterpreter = interpret $ \case UpsertHashedPassword uid password -> modify $ Map.insert uid password LookupHashedPassword uid -> gets $ Map.lookup uid + LookupHashedProviderPassword _uid -> error ("Implement as needed" :: String) diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index 6333a9fe1c0..3aa2ea8ba36 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -171,7 +171,7 @@ optSettings: timeout: 5 # seconds. if you reach the limit, how long do you have to wait to try again. retryLimit: 5 # how many times can you have a failed login in that timeframe. setSuspendInactiveUsers: # if this is omitted: never suspend inactive users. - suspendTimeout: 4 + suspendTimeout: 10 setRichInfoLimit: 5000 # should be in sync with Spar setDefaultUserLocale: en setMaxTeamSize: 32 diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index 578bb4629bc..4c3e9c4a563 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -49,6 +49,7 @@ import Wire.API.User.Auth hiding (access) import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso +import Wire.AuthenticationSubsystem (AuthenticationSubsystem) import Wire.BlockListStore import Wire.EmailSubsystem (EmailSubsystem) import Wire.Events (Events) @@ -102,7 +103,8 @@ login :: Member Events r, Member (Input (Local ())) r, Member UserSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member AuthenticationSubsystem r ) => Login -> Maybe Bool -> @@ -161,7 +163,8 @@ listCookies lusr (fold -> labels) = removeCookies :: ( Member TinyLog r, Member PasswordStore r, - Member UserSubsystem r + Member UserSubsystem r, + Member AuthenticationSubsystem r ) => Local UserId -> RemoveCookies -> @@ -173,7 +176,8 @@ legalHoldLogin :: ( Member GalleyAPIAccess r, Member TinyLog r, Member UserSubsystem r, - Member Events r + Member Events r, + Member AuthenticationSubsystem r ) => LegalHoldLogin -> Handler r SomeAccess @@ -184,6 +188,7 @@ legalHoldLogin lhl = do ssoLogin :: ( Member TinyLog r, + Member AuthenticationSubsystem r, Member UserSubsystem r, Member Events r ) => @@ -201,13 +206,14 @@ getLoginCode _ = throwStd loginCodeNotFound reauthenticate :: ( Member GalleyAPIAccess r, Member VerificationCodeSubsystem r, + Member AuthenticationSubsystem r, Member UserSubsystem r ) => Local UserId -> ReAuthUser -> Handler r () reauthenticate luid@(tUnqualified -> uid) body = do - wrapClientE (User.reauthenticate uid (reAuthPassword body)) !>> reauthError + User.reauthenticate uid body.reAuthPassword !>> reauthError case reAuthCodeAction body of Just action -> Auth.verifyCode (reAuthCode body) action luid diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index ce9cd717a7e..d5282714d12 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -102,6 +102,7 @@ import Wire.API.User.Client.DPoPAccessToken import Wire.API.User.Client.Prekey import Wire.API.UserEvent import Wire.API.UserMap (QualifiedUserMap (QualifiedUserMap, qualifiedUserMap), UserMap (userMap)) +import Wire.AuthenticationSubsystem (AuthenticationSubsystem) import Wire.DeleteQueue import Wire.EmailSubsystem (EmailSubsystem, sendNewClientEmail) import Wire.Events (Events) @@ -165,6 +166,7 @@ addClient :: Member UserSubsystem r, Member DeleteQueue r, Member EmailSubsystem r, + Member AuthenticationSubsystem r, Member VerificationCodeSubsystem r, Member Events r ) => @@ -184,6 +186,7 @@ addClientWithReAuthPolicy :: Member EmailSubsystem r, Member Events r, Member UserSubsystem r, + Member AuthenticationSubsystem r, Member VerificationCodeSubsystem r ) => Data.ReAuthPolicy -> @@ -207,8 +210,7 @@ addClientWithReAuthPolicy policy luid@(tUnqualified -> u) con new = do else id lhcaps = ClientSupportsLegalholdImplicitConsent (clt0, old, count) <- - wrapClientE - (Data.addClientWithReAuthPolicy policy luid clientId' new maxPermClients caps) + Data.addClientWithReAuthPolicy policy luid clientId' new maxPermClients caps !>> ClientDataError let clt = clt0 {clientMLSPublicKeys = newClientMLSPublicKeys new} lift $ do @@ -251,7 +253,9 @@ updateClient u c r = do -- nb. We must ensure that the set of clients known to brig is always -- a superset of the clients known to galley. rmClient :: - (Member DeleteQueue r) => + ( Member DeleteQueue r, + Member AuthenticationSubsystem r + ) => UserId -> ConnId -> ClientId -> @@ -267,7 +271,7 @@ rmClient u con clt pw = -- Temporary clients don't need to re-auth TemporaryClientType -> pure () -- All other clients must authenticate - _ -> wrapClientE (Data.reauthenticate u pw) !>> ClientDataError . ClientReAuthError + _ -> Data.reauthenticate u pw !>> ClientDataError . ClientReAuthError lift $ execDelete u (Just con) client claimPrekey :: diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index d5abf271fc1..052c5cdb59f 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -275,7 +275,8 @@ authAPI :: Member TinyLog r, Member Events r, Member UserSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member AuthenticationSubsystem r ) => ServerT BrigIRoutes.AuthAPI (Handler r) authAPI = @@ -425,7 +426,8 @@ addClientInternalH :: Member EmailSubsystem r, Member Events r, Member UserSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member AuthenticationSubsystem r ) => UserId -> Maybe Bool -> diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index e66dc240b14..0ab2f89a4fe 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -56,6 +56,7 @@ import Wire.API.Password import Wire.API.Routes.Internal.Brig.OAuth qualified as I import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig.OAuth +import Wire.AuthenticationSubsystem (AuthenticationSubsystem) import Wire.Error import Wire.Sem.Jwk import Wire.Sem.Jwk qualified as Jwk @@ -75,7 +76,12 @@ internalOauthAPI = -------------------------------------------------------------------------------- -- API Public -oauthAPI :: (Member Now r, Member Jwk r) => ServerT OAuthAPI (Handler r) +oauthAPI :: + ( Member Now r, + Member Jwk r, + Member AuthenticationSubsystem r + ) => + ServerT OAuthAPI (Handler r) oauthAPI = Named @"get-oauth-client" getOAuthClient :<|> Named @"create-oauth-auth-code" createNewOAuthAuthorizationCode @@ -101,7 +107,7 @@ registerOAuthClient (OAuthClientConfig name uri) = do createSecret = OAuthClientPlainTextSecret <$> rand32Bytes hashClientSecret :: (MonadIO m) => OAuthClientPlainTextSecret -> m Password - hashClientSecret = mkSafePasswordScrypt . plainTextPassword8Unsafe . toText . unOAuthClientPlainTextSecret + hashClientSecret = mkSafePassword . plainTextPassword8Unsafe . toText . unOAuthClientPlainTextSecret rand32Bytes :: (MonadIO m) => m AsciiBase16 rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 @@ -345,17 +351,28 @@ revokeOAuthAccountAccessV6 (tUnqualified -> uid) cid = do rts <- lift $ wrapClient $ lookupOAuthRefreshTokens uid for_ rts $ \rt -> when (rt.clientId == cid) $ lift $ wrapClient $ deleteOAuthRefreshToken uid rt.refreshTokenId -revokeOAuthAccountAccess :: Local UserId -> OAuthClientId -> PasswordReqBody -> (Handler r) () +revokeOAuthAccountAccess :: + (Member AuthenticationSubsystem r) => + Local UserId -> + OAuthClientId -> + PasswordReqBody -> + (Handler r) () revokeOAuthAccountAccess luid@(tUnqualified -> uid) cid req = do - wrapClientE (reauthenticate uid req.fromPasswordReqBody) !>> toAccessDenied + reauthenticate uid req.fromPasswordReqBody !>> toAccessDenied revokeOAuthAccountAccessV6 luid cid where toAccessDenied :: ReAuthError -> HttpError toAccessDenied _ = StdError $ errorToWai @'AccessDenied -deleteOAuthRefreshTokenById :: Local UserId -> OAuthClientId -> OAuthRefreshTokenId -> PasswordReqBody -> (Handler r) () +deleteOAuthRefreshTokenById :: + (Member AuthenticationSubsystem r) => + Local UserId -> + OAuthClientId -> + OAuthRefreshTokenId -> + PasswordReqBody -> + (Handler r) () deleteOAuthRefreshTokenById (tUnqualified -> uid) cid tokenId req = do - wrapClientE (reauthenticate uid req.fromPasswordReqBody) !>> toAccessDenied + reauthenticate uid req.fromPasswordReqBody !>> toAccessDenied mInfo <- lift $ wrapClient $ lookupOAuthRefreshTokenInfo tokenId case mInfo of Nothing -> pure () diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 838d5979403..017f225a190 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -588,6 +588,7 @@ addClient :: Member DeleteQueue r, Member NotificationSubsystem r, Member EmailSubsystem r, + Member AuthenticationSubsystem r, Member VerificationCodeSubsystem r, Member Events r, Member UserSubsystem r @@ -604,7 +605,9 @@ addClient lusr con new = do !>> clientError deleteClient :: - (Member DeleteQueue r) => + ( Member AuthenticationSubsystem r, + Member DeleteQueue r + ) => UserId -> ConnId -> ClientId -> diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 6d40b450a4c..bccba92ac46 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -846,7 +846,7 @@ changePassword uid cp = do throwE ChangePasswordNoIdentity currpw <- lift $ liftSem $ lookupHashedPassword uid let newpw = cpNewPassword cp - hashedNewPw <- mkSafePasswordScrypt newpw + hashedNewPw <- mkSafePassword newpw case (currpw, cpOldPassword cp) of (Nothing, _) -> lift . liftSem $ upsertHashedPassword uid hashedNewPw (Just _, Nothing) -> throwE InvalidCurrentPassword diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 42f92476fc9..201f60ab49d 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -79,6 +79,7 @@ import Data.Text qualified as Text import Data.Time.Clock import Data.UUID qualified as UUID import Imports +import Polysemy (Member) import Prometheus qualified as Prom import System.CryptoBox (Result (Success)) import System.CryptoBox qualified as CryptoBox @@ -90,6 +91,7 @@ import Wire.API.User.Auth import Wire.API.User.Client hiding (UpdateClient (..)) import Wire.API.User.Client.Prekey import Wire.API.UserMap (UserMap (..)) +import Wire.AuthenticationSubsystem (AuthenticationSubsystem) data ClientDataError = TooManyClients @@ -116,20 +118,20 @@ reAuthForNewClients :: ReAuthPolicy reAuthForNewClients count upsert = count > 0 && not upsert addClient :: - ( MonadClient m, - MonadReader Brig.App.Env m + ( MonadReader Brig.App.Env (AppT r), + Member AuthenticationSubsystem r ) => Local UserId -> ClientId -> NewClient -> Int -> Maybe ClientCapabilityList -> - ExceptT ClientDataError m (Client, [Client], Word) + ExceptT ClientDataError (AppT r) (Client, [Client], Word) addClient = addClientWithReAuthPolicy reAuthForNewClients addClientWithReAuthPolicy :: - ( MonadClient m, - MonadReader Brig.App.Env m + ( MonadReader Brig.App.Env (AppT r), + Member AuthenticationSubsystem r ) => ReAuthPolicy -> Local UserId -> @@ -137,9 +139,9 @@ addClientWithReAuthPolicy :: NewClient -> Int -> Maybe ClientCapabilityList -> - ExceptT ClientDataError m (Client, [Client], Word) + ExceptT ClientDataError (AppT r) (Client, [Client], Word) addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients caps = do - clients <- lookupClients (tUnqualified u) + clients <- wrapClientE $ lookupClients (tUnqualified u) let typed = filter ((== newClientType c) . clientType) clients let count = length typed let upsert = any exists typed @@ -149,7 +151,7 @@ addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients caps = do let capacity = fmap (+ (-count)) limit unless (maybe True (> 0) capacity || upsert) $ throwE TooManyClients - new <- insert (tUnqualified u) + new <- wrapClientE $ insert (tUnqualified u) let !total = fromIntegral (length clients + if upsert then 0 else 1) let old = maybe (filter (not . exists) typed) (const []) limit pure (new, old, total) diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 4e3013a19bd..caaa7c160cc 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -86,6 +86,7 @@ import Wire.API.Provider.Service import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.RichInfo +import Wire.AuthenticationSubsystem as AuthenticationSubsystem import Wire.PasswordStore -- | Authentication errors. @@ -180,8 +181,14 @@ newAccountInviteViaScim uid externalId tid locale name email = do defSupportedProtocols -- | Mandatory password authentication. -authenticate :: forall r. (Member PasswordStore r) => UserId -> PlainTextPassword6 -> ExceptT AuthError (AppT r) () +authenticate :: + forall r. + (Member PasswordStore r, Member AuthenticationSubsystem r) => + UserId -> + PlainTextPassword6 -> + ExceptT AuthError (AppT r) () authenticate u pw = + -- FUTUREWORK: Move this logic into auth subsystem. lift (wrapHttp $ lookupAuth u) >>= \case Nothing -> throwE AuthInvalidUser Just (_, Deleted) -> throwE AuthInvalidUser @@ -189,8 +196,9 @@ authenticate u pw = Just (_, Ephemeral) -> throwE AuthEphemeral Just (_, PendingInvitation) -> throwE AuthPendingInvitation Just (Nothing, _) -> throwE AuthInvalidCredentials - Just (Just pw', Active) -> - case verifyPasswordWithStatus pw pw' of + Just (Just pw', Active) -> do + res <- lift $ liftSem (AuthenticationSubsystem.verifyPassword pw pw') + case res of (False, _) -> throwE AuthInvalidCredentials (True, PasswordStatusNeedsUpdate) -> do -- FUTUREWORK(elland): 6char pwd allowed for now @@ -200,21 +208,19 @@ authenticate u pw = where hashAndUpdatePwd :: UserId -> PlainTextPassword8 -> AppT r () hashAndUpdatePwd uid pwd = do - hashed <- mkSafePasswordScrypt pwd + hashed <- mkSafePassword pwd liftSem $ upsertHashedPassword uid hashed -- | Password reauthentication. If the account has a password, reauthentication -- is mandatory. If the account has no password, or is an SSO user, and no password is given, -- reauthentication is a no-op. reauthenticate :: - ( MonadClient m, - MonadReader Env m - ) => + (Member AuthenticationSubsystem r) => UserId -> Maybe PlainTextPassword6 -> - ExceptT ReAuthError m () + ExceptT ReAuthError (AppT r) () reauthenticate u pw = - lift (lookupAuth u) >>= \case + wrapClientE (lookupAuth u) >>= \case Nothing -> throwE (ReAuthError AuthInvalidUser) Just (_, Deleted) -> throwE (ReAuthError AuthInvalidUser) Just (_, Suspended) -> throwE (ReAuthError AuthSuspended) @@ -225,10 +231,10 @@ reauthenticate u pw = where maybeReAuth pw' = case pw of Nothing -> do - musr <- lookupUser NoPendingInvitations u + musr <- wrapClientE $ lookupUser NoPendingInvitations u unless (maybe False isSamlUser musr) $ throwE ReAuthMissingPassword Just p -> - unless (verifyPassword p pw') $ + unlessM (fst <$> lift (liftSem (AuthenticationSubsystem.verifyPassword p pw'))) do throwE (ReAuthError AuthInvalidCredentials) isSamlUser :: User -> Bool diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index dea4ba451c5..65070d3b420 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -62,7 +62,11 @@ import Data.LegalHold import Data.List qualified as List import Data.List1 (maybeList1) import Data.Map.Strict qualified as Map -import Data.Misc (Fingerprint (..), FutureWork (FutureWork), Rsa) +import Data.Misc + ( Fingerprint (Fingerprint), + FutureWork (FutureWork), + Rsa, + ) import Data.Qualified import Data.Range import Data.Set qualified as Set @@ -117,6 +121,7 @@ import Wire.API.User.Auth import Wire.API.User.Client import Wire.API.User.Client qualified as Public (Client, ClientCapability (ClientSupportsLegalholdImplicitConsent), PubClient (..), UserClientPrekeyMap, UserClients, userClients) import Wire.API.User.Client.Prekey qualified as Public (PrekeyId) +import Wire.AuthenticationSubsystem as Authentication import Wire.DeleteQueue import Wire.EmailSending (EmailSending) import Wire.Error @@ -133,7 +138,8 @@ import Wire.VerificationCodeSubsystem botAPI :: ( Member GalleyAPIAccess r, Member (Concurrency 'Unsafe) r, - Member DeleteQueue r + Member DeleteQueue r, + Member AuthenticationSubsystem r ) => ServerT BotAPI (Handler r) botAPI = @@ -151,6 +157,7 @@ botAPI = servicesAPI :: ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r, Member DeleteQueue r, Member (Error UserSubsystemError) r ) => @@ -171,6 +178,7 @@ servicesAPI = providerAPI :: ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r, Member EmailSending r, Member VerificationCodeSubsystem r ) => @@ -193,7 +201,7 @@ internalProviderAPI :: Member VerificationCodeSubsystem r ) => ServerT BrigIRoutes.ProviderAPI (Handler r) -internalProviderAPI = Named @"get-provider-activation-code" getActivationCodeH +internalProviderAPI = Named @"get-provider-activation-code" getActivationCode -------------------------------------------------------------------------------- -- Public API (Unauthenticated) @@ -207,18 +215,18 @@ newAccount :: (Handler r) Public.NewProviderResponse newAccount new = do guardSecondFactorDisabled Nothing - let email = (Public.newProviderEmail new) - let name = Public.newProviderName new - let pass = Public.newProviderPassword new - let descr = fromRange (Public.newProviderDescr new) - let url = Public.newProviderUrl new + let email = new.newProviderEmail + let name = new.newProviderName + let pass = new.newProviderPassword + let descr = fromRange new.newProviderDescr + let url = new.newProviderUrl let emailKey = mkEmailKey email wrapClientE (DB.lookupKey emailKey) >>= mapM_ (const $ throwStd emailExists) (safePass, newPass) <- case pass of - Just newPass -> (,Nothing) <$> mkSafePasswordScrypt newPass + Just newPass -> (,Nothing) <$> mkSafePassword newPass Nothing -> do newPass <- genPassword - safePass <- mkSafePasswordScrypt newPass + safePass <- mkSafePassword newPass pure (safePass, Just newPass) pid <- wrapClientE $ DB.insertAccount name safePass url descr let gen = mkVerificationCodeGen email @@ -265,20 +273,31 @@ activateAccountKey key val = do lift $ sendApprovalConfirmMail name email pure . Just $ Public.ProviderActivationResponse email -getActivationCodeH :: (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => EmailAddress -> (Handler r) Code.KeyValuePair -getActivationCodeH email = do +getActivationCode :: + ( Member GalleyAPIAccess r, + Member VerificationCodeSubsystem r + ) => + EmailAddress -> + (Handler r) Code.KeyValuePair +getActivationCode email = do guardSecondFactorDisabled Nothing let gen = mkVerificationCodeGen email code <- lift . liftSem $ internalLookupCode gen.genKey IdentityVerification maybe (throwStd activationKeyNotFound) (pure . codeToKeyValuePair) code -login :: (Member GalleyAPIAccess r) => ProviderLogin -> Handler r ProviderTokenCookie +login :: + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r + ) => + ProviderLogin -> + Handler r ProviderTokenCookie login l = do guardSecondFactorDisabled Nothing - pid <- wrapClientE (DB.lookupKey (mkEmailKey (providerLoginEmail l))) >>= maybeBadCredentials - pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - unless (verifyPassword (providerLoginPassword l) pass) $ - throwStd (errorToWai @'E.BadCredentials) + pid <- + wrapClientE (DB.lookupKey (mkEmailKey (providerLoginEmail l))) + >>= maybeBadCredentials + unlessM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid l.providerLoginPassword)) do + throwStd (errorToWai @E.BadCredentials) token <- ZAuth.newProviderToken pid s <- asks (.settings) pure $ ProviderTokenCookie (ProviderToken token) (not s.cookieInsecure) @@ -295,16 +314,21 @@ beginPasswordReset (Public.PasswordReset target) = do Right code -> lift $ sendPasswordResetMail target (code.codeKey) (code.codeValue) -completePasswordReset :: (Member GalleyAPIAccess r, Member VerificationCodeSubsystem r) => Public.CompletePasswordReset -> (Handler r) () +completePasswordReset :: + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r, + Member VerificationCodeSubsystem r + ) => + Public.CompletePasswordReset -> + (Handler r) () completePasswordReset (Public.CompletePasswordReset key val newpwd) = do guardSecondFactorDisabled Nothing code <- (lift . liftSem $ verifyCode key VerificationCode.PasswordReset val) >>= maybeInvalidCode case Id <$> code.codeAccount of - Nothing -> throwStd (errorToWai @'E.InvalidPasswordResetCode) + Nothing -> throwStd (errorToWai @E.InvalidPasswordResetCode) Just pid -> do - oldpass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - when (verifyPassword newpwd oldpass) $ do - throwStd (errorToWai @'E.ResetPasswordMustDiffer) + whenM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid newpwd)) do + throwStd (errorToWai @E.ResetPasswordMustDiffer) wrapClientE $ do DB.updateAccountPassword pid newpwd lift . liftSem $ deleteCode key VerificationCode.PasswordReset @@ -328,7 +352,14 @@ updateAccountProfile pid upd = do (updateProviderUrl upd) (updateProviderDescr upd) -updateAccountEmail :: (Member GalleyAPIAccess r, Member EmailSending r, Member VerificationCodeSubsystem r) => ProviderId -> Public.EmailUpdate -> (Handler r) () +updateAccountEmail :: + ( Member GalleyAPIAccess r, + Member EmailSending r, + Member VerificationCodeSubsystem r + ) => + ProviderId -> + Public.EmailUpdate -> + (Handler r) () updateAccountEmail pid (Public.EmailUpdate email) = do guardSecondFactorDisabled Nothing let emailKey = mkEmailKey email @@ -344,14 +375,19 @@ updateAccountEmail pid (Public.EmailUpdate email) = do (Just (toUUID pid)) lift $ sendActivationMail (Name "name") email code.codeKey code.codeValue True -updateAccountPassword :: (Member GalleyAPIAccess r) => ProviderId -> Public.PasswordChange -> (Handler r) () +updateAccountPassword :: + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r + ) => + ProviderId -> + Public.PasswordChange -> + (Handler r) () updateAccountPassword pid upd = do guardSecondFactorDisabled Nothing - pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - unless (verifyPassword (oldPassword upd) pass) $ - throwStd (errorToWai @'E.BadCredentials) - when (verifyPassword (newPassword upd) pass) $ - throwStd (errorToWai @'E.ResetPasswordMustDiffer) + unlessM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid upd.oldPassword)) do + throwStd (errorToWai @E.BadCredentials) + whenM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid upd.newPassword)) do + throwStd (errorToWai @E.ResetPasswordMustDiffer) wrapClientE $ DB.updateAccountPassword pid (newPassword upd) addService :: @@ -424,16 +460,17 @@ updateService pid sid upd = do (serviceEnabled svc) updateServiceConn :: - (Member GalleyAPIAccess r) => + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r + ) => ProviderId -> ServiceId -> Public.UpdateServiceConn -> Handler r () updateServiceConn pid sid upd = do guardSecondFactorDisabled Nothing - pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - unless (verifyPassword (updateServiceConnPassword upd) pass) $ - throwStd (errorToWai @'E.BadCredentials) + unlessM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid upd.updateServiceConnPassword)) $ + throwStd (errorToWai @E.BadCredentials) scon <- wrapClientE (DB.lookupServiceConn pid sid) >>= maybeServiceNotFound svc <- wrapClientE (DB.lookupServiceProfile pid sid) >>= maybeServiceNotFound let newBaseUrl = updateServiceConnUrl upd @@ -472,6 +509,7 @@ updateServiceConn pid sid upd = do -- delete the service. See 'finishDeleteService'. deleteService :: ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r, Member DeleteQueue r ) => ProviderId -> @@ -480,10 +518,8 @@ deleteService :: (Handler r) () deleteService pid sid del = do guardSecondFactorDisabled Nothing - pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - -- We don't care about pwd status when deleting things - unless (verifyPassword (deleteServicePassword del) pass) $ - throwStd (errorToWai @'E.BadCredentials) + unlessM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid del.deleteServicePassword)) do + throwStd (errorToWai @E.BadCredentials) _ <- wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound -- Disable the service wrapClientE $ DB.updateServiceConn pid sid Nothing Nothing Nothing (Just False) @@ -516,7 +552,8 @@ finishDeleteService pid sid = do kick (bid, cid, _) = deleteBot (botUserId bid) Nothing bid cid deleteAccount :: - ( Member GalleyAPIAccess r + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r ) => ProviderId -> Public.DeleteProvider -> @@ -524,10 +561,9 @@ deleteAccount :: deleteAccount pid del = do guardSecondFactorDisabled Nothing prov <- wrapClientE (DB.lookupAccount pid) >>= maybeInvalidProvider - pass <- wrapClientE (DB.lookupPassword pid) >>= maybeBadCredentials - -- We don't care about pwd status when deleting things - unless (verifyPassword (deleteProviderPassword del) pass) $ - throwStd (errorToWai @'E.BadCredentials) + -- We don't care about pwd update status (scrypt, argon2id etc) when deleting things + unlessM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid del.deleteProviderPassword)) do + throwStd (errorToWai @E.BadCredentials) svcs <- wrapClientE $ DB.listServices pid forM_ svcs $ \svc -> do let sid = serviceId svc @@ -656,7 +692,15 @@ updateServiceWhitelist uid con tid upd = do -------------------------------------------------------------------------------- -- Bot API -addBot :: (Member GalleyAPIAccess r) => UserId -> ConnId -> ConvId -> Public.AddBot -> (Handler r) Public.AddBotResponse +addBot :: + ( Member GalleyAPIAccess r, + Member AuthenticationSubsystem r + ) => + UserId -> + ConnId -> + ConvId -> + Public.AddBot -> + (Handler r) Public.AddBotResponse addBot zuid zcon cid add = do guardSecondFactorDisabled (Just zuid) zusr <- lift (wrapClient $ User.lookupUser NoPendingInvitations zuid) >>= maybeInvalidUser @@ -726,7 +770,14 @@ addBot zuid zcon cid add = do -- implicitly in the next line. pure $ FutureWork @'UnprotectedBot undefined lbid <- qualifyLocal (botUserId bid) - wrapClientE (User.addClient lbid bcl newClt maxPermClients (Just $ ClientCapabilityList $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent)) + ( User.addClient + lbid + bcl + newClt + maxPermClients + ( Just $ ClientCapabilityList $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent + ) + ) !>> const (StdError $ badGatewayWith "MalformedPrekeys") -- Add the bot to the conversation diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index 67a9454ac6b..b5bb0243120 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -103,19 +103,6 @@ lookupAccountProfile :: m (Maybe ProviderProfile) lookupAccountProfile p = fmap ProviderProfile <$> lookupAccount p -lookupPassword :: - (MonadClient m) => - ProviderId -> - m (Maybe Password) -lookupPassword p = - fmap (fmap runIdentity) $ - retry x1 $ - query1 cql $ - params LocalQuorum (Identity p) - where - cql :: PrepQuery R (Identity ProviderId) (Identity Password) - cql = "SELECT password FROM provider WHERE id = ?" - deleteAccount :: (MonadClient m) => ProviderId -> @@ -131,7 +118,7 @@ updateAccountPassword :: PlainTextPassword6 -> m () updateAccountPassword pid pwd = do - p <- liftIO $ mkSafePasswordScrypt pwd + p <- liftIO $ mkSafePassword pwd retry x5 $ write cql $ params LocalQuorum (p, pid) where cql :: PrepQuery W (Password, ProviderId) () diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 39c5b1ef139..597c8156554 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -72,6 +72,7 @@ import Wire.API.User import Wire.API.User.Auth import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.Sso +import Wire.AuthenticationSubsystem (AuthenticationSubsystem) import Wire.Events (Events) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess @@ -95,7 +96,8 @@ login :: Member VerificationCodeSubsystem r, Member (Input (Local ())) r, Member UserSubsystem r, - Member Events r + Member Events r, + Member AuthenticationSubsystem r ) => Login -> CookieType -> @@ -218,7 +220,8 @@ renewAccess uts at mcid = do revokeAccess :: ( Member TinyLog r, Member PasswordStore r, - Member UserSubsystem r + Member UserSubsystem r, + Member AuthenticationSubsystem r ) => Local UserId -> PlainTextPassword6 -> @@ -379,13 +382,14 @@ validateToken ut at = do ssoLogin :: ( Member TinyLog r, Member UserSubsystem r, - Member Events r + Member Events r, + Member AuthenticationSubsystem r ) => SsoLogin -> CookieType -> ExceptT LoginError (AppT r) (Access ZAuth.User) ssoLogin (SsoLogin uid label) typ = do - wrapHttpClientE (Data.reauthenticate uid Nothing) `catchE` \case + (Data.reauthenticate uid Nothing) `catchE` \case ReAuthMissingPassword -> pure () ReAuthCodeVerificationRequired -> pure () ReAuthCodeVerificationNoPendingCode -> pure () @@ -403,13 +407,14 @@ legalHoldLogin :: ( Member GalleyAPIAccess r, Member TinyLog r, Member UserSubsystem r, + Member AuthenticationSubsystem r, Member Events r ) => LegalHoldLogin -> CookieType -> ExceptT LegalHoldLoginError (AppT r) (Access ZAuth.LegalHoldUser) legalHoldLogin (LegalHoldLogin uid pw label) typ = do - wrapHttpClientE (Data.reauthenticate uid pw) !>> LegalHoldReAuthError + (Data.reauthenticate uid pw) !>> LegalHoldReAuthError -- legalhold login is only possible if -- the user is a team user -- and the team has legalhold enabled diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 518e4dfc162..f37ad772048 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -62,7 +62,7 @@ import UnliftIO.Async hiding (wait) import Util import Util.Timeout import Wire.API.Conversation (Conversation (..)) -import Wire.API.Password (Password, mkSafePasswordScrypt) +import Wire.API.Password (Password, mkSafePassword) import Wire.API.User as Public import Wire.API.User.Auth as Auth import Wire.API.User.Auth.LegalHold @@ -193,7 +193,7 @@ testLoginWith6CharPassword brig db = do updatePassword :: (MonadClient m) => UserId -> PlainTextPassword6 -> m () updatePassword u t = do - p <- liftIO $ mkSafePasswordScrypt t + p <- liftIO $ mkSafePassword t retry x5 $ write userPasswordUpdate (params LocalQuorum (p, u)) userPasswordUpdate :: PrepQuery W (Password, UserId) () diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index a05438a3e10..e447c96c71b 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -125,7 +125,7 @@ import Wire.API.Federation.API import Wire.API.Federation.API.Galley import Wire.API.Federation.Error import Wire.API.Message -import Wire.API.Password (mkSafePasswordScrypt) +import Wire.API.Password (mkSafePassword) import Wire.API.Routes.Public (ZHostValue) import Wire.API.Routes.Public.Galley.Messaging import Wire.API.Routes.Public.Util (UpdateResult (..)) @@ -569,7 +569,7 @@ addCode lusr mbZHost mZcon lcnv mReq = do Nothing -> do ttl <- realToFrac . unGuestLinkTTLSeconds . fromMaybe defGuestLinkTTLSeconds . view (settings . guestLinkTTLSeconds) <$> input code <- E.generateCode (tUnqualified lcnv) ReusableCode (Timeout ttl) - mPw <- for (mReq >>= (.password)) mkSafePasswordScrypt + mPw <- for (mReq >>= (.password)) mkSafePassword E.createCode code mPw now <- input let event = Event (tUntagged lcnv) Nothing (tUntagged lusr) now (EdConvCodeUpdate (mkConversationCodeInfo (isJust mPw) (codeKey code) (codeValue code) convUri)) From 1abad76eb007d117645504b4284264708128022d Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:57:41 +0200 Subject: [PATCH 104/136] Removed redundant MonadReader constraint. (#4286) Seems to be a mistake when rebasing a PR without aligning with develop ahead of time, weird. --- services/brig/src/Brig/Data/Client.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 201f60ab49d..320d096d6e2 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -118,8 +118,7 @@ reAuthForNewClients :: ReAuthPolicy reAuthForNewClients count upsert = count > 0 && not upsert addClient :: - ( MonadReader Brig.App.Env (AppT r), - Member AuthenticationSubsystem r + ( Member AuthenticationSubsystem r ) => Local UserId -> ClientId -> From bd423b29a9c9299260e80d0d5b204e9a5ed7e6e4 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 9 Oct 2024 14:42:10 +0200 Subject: [PATCH 105/136] Revert "Work around legacy integration test resource leak. (#4244)" This reverts commit a72c70a9a9b9d71af1b864384e80b6d3eb0827a9. (it turns out this only helps with resource consumption because after running the first bach of tests, defaultMainWithIngredients exits... m| --- services/brig/test/integration/Run.hs | 28 ++++++++------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/services/brig/test/integration/Run.hs b/services/brig/test/integration/Run.hs index 9cfadcf53b1..36adc72a8ec 100644 --- a/services/brig/test/integration/Run.hs +++ b/services/brig/test/integration/Run.hs @@ -54,7 +54,6 @@ import Options.Applicative hiding (action) import SMTP qualified import System.Environment (withArgs) import System.Logger qualified as Logger -import System.Mem (performGC) import Test.Tasty import Test.Tasty.Ingredients import Test.Tasty.Runners @@ -151,14 +150,16 @@ runTests iConf brigOpts otherArgs = do let smtp = SMTP.tests mg lg oauthAPI = API.OAuth.tests mg db b n brigOpts - -- run the tests in two parts, with a gc in between. i did this on a hunch, and for some - -- reason this reduces the hunger for open file handles at run time significantly, and makes - -- the suite pass with my ulimit settings. (fisx) - withArgs otherArgs . defaultMainWithIngredients (listingTests : (composeReporters antXMLRunner consoleTestReporter) : defaultIngredients) $ testGroup - "Brig API Integration, part 1" - $ [ systemSettingsApi, + "Brig API Integration" + $ [ userApi, + providerApi, + searchApis, + teamApis, + turnApi, + metricsApi, + systemSettingsApi, settingsApi, createIndex, userPendingActivation, @@ -169,19 +170,6 @@ runTests iConf brigOpts otherArgs = do oauthAPI, federationEnd2End ] - - performGC - - withArgs otherArgs . defaultMainWithIngredients (listingTests : (composeReporters antXMLRunner consoleTestReporter) : defaultIngredients) - $ testGroup - "Brig API Integration, part 2" - $ [ userApi, - providerApi, - searchApis, - teamApis, - turnApi, - metricsApi - ] where mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p From 1941f53ca815ffcae3ab3b952a2ac37fa849f2b4 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 9 Oct 2024 15:56:39 +0200 Subject: [PATCH 106/136] Revert "Revert "Work around legacy integration test resource leak. (#4244)"" This reverts commit bd423b29a9c9299260e80d0d5b204e9a5ed7e6e4. --- services/brig/test/integration/Run.hs | 28 +++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/services/brig/test/integration/Run.hs b/services/brig/test/integration/Run.hs index 36adc72a8ec..9cfadcf53b1 100644 --- a/services/brig/test/integration/Run.hs +++ b/services/brig/test/integration/Run.hs @@ -54,6 +54,7 @@ import Options.Applicative hiding (action) import SMTP qualified import System.Environment (withArgs) import System.Logger qualified as Logger +import System.Mem (performGC) import Test.Tasty import Test.Tasty.Ingredients import Test.Tasty.Runners @@ -150,16 +151,14 @@ runTests iConf brigOpts otherArgs = do let smtp = SMTP.tests mg lg oauthAPI = API.OAuth.tests mg db b n brigOpts + -- run the tests in two parts, with a gc in between. i did this on a hunch, and for some + -- reason this reduces the hunger for open file handles at run time significantly, and makes + -- the suite pass with my ulimit settings. (fisx) + withArgs otherArgs . defaultMainWithIngredients (listingTests : (composeReporters antXMLRunner consoleTestReporter) : defaultIngredients) $ testGroup - "Brig API Integration" - $ [ userApi, - providerApi, - searchApis, - teamApis, - turnApi, - metricsApi, - systemSettingsApi, + "Brig API Integration, part 1" + $ [ systemSettingsApi, settingsApi, createIndex, userPendingActivation, @@ -170,6 +169,19 @@ runTests iConf brigOpts otherArgs = do oauthAPI, federationEnd2End ] + + performGC + + withArgs otherArgs . defaultMainWithIngredients (listingTests : (composeReporters antXMLRunner consoleTestReporter) : defaultIngredients) + $ testGroup + "Brig API Integration, part 2" + $ [ userApi, + providerApi, + searchApis, + teamApis, + turnApi, + metricsApi + ] where mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p From 05cffed730369b4a8fd43c509def2d649ae4caa9 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 10 Oct 2024 08:48:27 +0200 Subject: [PATCH 107/136] brig: Remove unnecessary List1 Event when pushing notifications (#4289) * brig: Remove unnecesssary List1 Event when pushing Brig only ever pushes 1 notification at a time * NotificationSubsystem: Make PushNotificationsAsync only push 1 notif We only ever use it for 1 notification --- .../src/Wire/NotificationSubsystem.hs | 2 +- .../Wire/NotificationSubsystem/Interpreter.hs | 8 ++--- .../NotificationSubsystem/InterpreterSpec.hs | 3 +- services/brig/src/Brig/API/Federation.hs | 2 +- services/brig/src/Brig/IO/Intra.hs | 29 +++++++++---------- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs index 5e1c35b5fbb..d854c0acb1b 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem.hs @@ -45,7 +45,7 @@ data NotificationSubsystem m a where -- send notifications is not critical. -- -- See 'Polysemy.Async' to know more about the 'Maybe' - PushNotificationsAsync :: [Push] -> NotificationSubsystem m (Async (Maybe ())) + PushNotificationAsync :: Push -> NotificationSubsystem m (Async (Maybe ())) CleanupUser :: UserId -> NotificationSubsystem m () UnregisterPushClient :: UserId -> ClientId -> NotificationSubsystem m () GetPushTokens :: UserId -> NotificationSubsystem m [PushToken] diff --git a/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs index 1b0d4dd6c25..5b2859d1ff1 100644 --- a/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/NotificationSubsystem/Interpreter.hs @@ -42,7 +42,7 @@ runNotificationSubsystemGundeck :: runNotificationSubsystemGundeck cfg = interpret $ \case PushNotifications ps -> runInputConst cfg $ pushImpl ps PushNotificationsSlowly ps -> runInputConst cfg $ pushSlowlyImpl ps - PushNotificationsAsync ps -> runInputConst cfg $ pushAsyncImpl ps + PushNotificationAsync ps -> runInputConst cfg $ pushAsyncImpl ps CleanupUser uid -> GundeckAPIAccess.userDeleted uid UnregisterPushClient uid cid -> GundeckAPIAccess.unregisterPushClient uid cid GetPushTokens uid -> GundeckAPIAccess.getPushTokens uid @@ -75,11 +75,11 @@ pushAsyncImpl :: Member (Final IO) r, Member P.TinyLog r ) => - [Push] -> + Push -> Sem r (Async (Maybe ())) -pushAsyncImpl ps = async $ do +pushAsyncImpl p = async $ do reqId <- inputs requestId - errorToIOFinal @SomeException (fromExceptionSem @SomeException $ pushImpl ps) >>= \case + errorToIOFinal @SomeException (fromExceptionSem @SomeException $ pushImpl [p]) >>= \case Left e -> P.err $ Log.msg (Log.val "Error while pushing notifications") diff --git a/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs index 9486394b019..880a213d25b 100644 --- a/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs @@ -228,9 +228,8 @@ spec = describe "NotificationSubsystem.Interpreter" do pushJson = payload1, _pushApsData = Nothing } - pushes = [push1] (_, attemptedPushes, logs) <- runMiniStackAsync mockConfig $ do - thread <- pushAsyncImpl pushes + thread <- pushAsyncImpl push1 await thread attemptedPushes `shouldBe` [[toV2Push push1]] diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 8a96e6c2d33..02e1c040317 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -287,7 +287,7 @@ onUserDeleted :: onUserDeleted origDomain udcn = lift $ do let deletedUser = toRemoteUnsafe origDomain udcn.user connections = udcn.connections - event = pure . UserEvent $ UserDeleted (tUntagged deletedUser) + event = UserEvent $ UserDeleted (tUntagged deletedUser) acceptedLocals <- map csv2From . filter (\x -> csv2Status x == Accepted) diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 39f2250eea1..95d49a402b8 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -72,7 +72,6 @@ import Data.ByteString.Lazy qualified as BL import Data.Id import Data.Json.Util import Data.List.NonEmpty (NonEmpty (..)) -import Data.List1 (List1, singleton) import Data.Proxy import Data.Qualified import Data.Range @@ -150,7 +149,7 @@ onConnectionEvent :: onConnectionEvent orig conn evt = do let from = ucFrom (ucConn evt) notify - (singleton $ ConnectionEvent evt) + (ConnectionEvent evt) orig V2.RouteAny conn @@ -166,7 +165,7 @@ onPropertyEvent :: Sem r () onPropertyEvent orig conn e = notify - (singleton $ PropertyEvent e) + (PropertyEvent e) orig V2.RouteDirect (Just conn) @@ -245,7 +244,7 @@ dispatchNotifications orig conn e = case e of notifyUserDeletionLocals orig conn event notifyUserDeletionRemotes orig where - event = singleton $ UserEvent e + event = UserEvent e notifyUserDeletionLocals :: forall r. @@ -256,7 +255,7 @@ notifyUserDeletionLocals :: ) => UserId -> Maybe ConnId -> - List1 Event -> + Event -> Sem r () notifyUserDeletionLocals deleted conn event = do luid <- qualifyLocal' deleted @@ -344,7 +343,7 @@ notifyUserDeletionRemotes deleted = do -- | (Asynchronously) notifies other users of events. notify :: (Member NotificationSubsystem r) => - List1 Event -> + Event -> -- | Origin user, TODO: Delete UserId -> -- | Push routing strategy. @@ -354,18 +353,18 @@ notify :: -- | Users to notify. Sem r (NonEmpty UserId) -> Sem r () -notify (toList -> events) orig route conn recipients = do +notify event orig route conn recipients = do rs <- (\u -> Recipient u RecipientClientsAll) <$$> recipients - let pushes = flip map events $ \event -> + let push = newPush1 (Just orig) (toJSONObject event) rs & pushConn .~ conn & pushRoute .~ route & pushApsData .~ toApsData event - void $ pushNotificationsAsync pushes + void $ pushNotificationAsync push notifySelf :: (Member NotificationSubsystem r) => - List1 Event -> + Event -> -- | Origin user. UserId -> -- | Push routing strategy. @@ -373,8 +372,8 @@ notifySelf :: -- | Origin device connection, if any. Maybe ConnId -> Sem r () -notifySelf events orig route conn = - notify events orig route conn (pure (orig :| [])) +notifySelf event orig route conn = + notify event orig route conn (pure (orig :| [])) notifyContacts :: forall r. @@ -382,7 +381,7 @@ notifyContacts :: Member NotificationSubsystem r, Member TinyLog r ) => - List1 Event -> + Event -> -- | Origin user. UserId -> -- | Push routing strategy. @@ -390,8 +389,8 @@ notifyContacts :: -- | Origin device connection, if any. Maybe ConnId -> Sem r () -notifyContacts events orig route conn = do - notify events orig route conn $ +notifyContacts event orig route conn = do + notify event orig route conn $ (:|) orig <$> liftA2 (++) contacts teamContacts where contacts :: Sem r [UserId] From 2f5d10ee4e4d87a1498d03250f0dcd4467acbef2 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 10 Oct 2024 08:48:55 +0200 Subject: [PATCH 108/136] gundeck: Remove bulkPush config option (#4290) It has been set to `true` for a few years now and seems to work well. It is being removed to reduce work for RabbitMq based notifications. Related: https://github.com/wireapp/wire-server/pull/4272 Also: https://wearezeta.atlassian.net/browse/WPB-10308 --- changelog.d/0-release-notes/gundeck-bulk-push | 3 + charts/gundeck/templates/configmap.yaml | 1 - charts/gundeck/values.yaml | 1 - hack/helm_vars/wire-server/values.yaml.gotmpl | 1 - services/gundeck/gundeck.integration.yaml | 1 - services/gundeck/src/Gundeck/Options.hs | 3 - services/gundeck/src/Gundeck/Push.hs | 70 +------------------ services/gundeck/test/integration/API.hs | 2 +- services/gundeck/test/unit/MockGundeck.hs | 50 ------------- services/gundeck/test/unit/Push.hs | 7 +- 10 files changed, 9 insertions(+), 130 deletions(-) create mode 100644 changelog.d/0-release-notes/gundeck-bulk-push diff --git a/changelog.d/0-release-notes/gundeck-bulk-push b/changelog.d/0-release-notes/gundeck-bulk-push new file mode 100644 index 00000000000..8a2fb1ade4f --- /dev/null +++ b/changelog.d/0-release-notes/gundeck-bulk-push @@ -0,0 +1,3 @@ +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/charts/gundeck/templates/configmap.yaml b/charts/gundeck/templates/configmap.yaml index 446fa7bab39..cf7c37e1a7c 100644 --- a/charts/gundeck/templates/configmap.yaml +++ b/charts/gundeck/templates/configmap.yaml @@ -68,7 +68,6 @@ data: settings: httpPoolSize: 1024 notificationTTL: {{ required "config.notificationTTL" .notificationTTL }} - bulkPush: {{ .bulkPush }} {{- if hasKey . "perNativePushConcurrency" }} perNativePushConcurrency: {{ .perNativePushConcurrency }} {{- end }} diff --git a/charts/gundeck/values.yaml b/charts/gundeck/values.yaml index ea8b6406a51..9749dd94be8 100644 --- a/charts/gundeck/values.yaml +++ b/charts/gundeck/values.yaml @@ -56,7 +56,6 @@ config: # # tlsCaSecretRef: # # name: # # key: - bulkPush: true aws: region: "eu-west-1" proxy: {} diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 48dec09f67d..83cd888dbf9 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -373,7 +373,6 @@ gundeck: sqsEndpoint: http://fake-aws-sqs:4568 snsEndpoint: http://fake-aws-sns:4575 disabledAPIVersions: [] - bulkPush: true setMaxConcurrentNativePushes: hard: 30 soft: 10 diff --git a/services/gundeck/gundeck.integration.yaml b/services/gundeck/gundeck.integration.yaml index 6c4c2ca748a..adf2914f6aa 100644 --- a/services/gundeck/gundeck.integration.yaml +++ b/services/gundeck/gundeck.integration.yaml @@ -37,7 +37,6 @@ aws: settings: httpPoolSize: 1024 notificationTTL: 24192200 - bulkPush: true perNativePushConcurrency: 32 sqsThrottleMillis: 1000 maxConcurrentNativePushes: diff --git a/services/gundeck/src/Gundeck/Options.hs b/services/gundeck/src/Gundeck/Options.hs index f5882a2a708..5f67081e178 100644 --- a/services/gundeck/src/Gundeck/Options.hs +++ b/services/gundeck/src/Gundeck/Options.hs @@ -57,9 +57,6 @@ data Settings = Settings _httpPoolSize :: !Int, -- | TTL (seconds) of stored notifications _notificationTTL :: !NotificationTTL, - -- | Use this option to group push notifications and send them in bulk to Cannon, instead - -- of in individual requests - _bulkPush :: !Bool, -- | Maximum number of concurrent threads calling SNS. _maxConcurrentNativePushes :: !(Maybe MaxConcurrentNativePushes), -- | Maximum number of parallel requests to SNS and cassandra diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index d7a738ad0e9..098223a5547 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -23,15 +23,12 @@ module Gundeck.Push deleteToken, -- (for testing) pushAll, - pushAny, MonadPushAll (..), MonadNativeTargets (..), MonadMapAsync (..), - MonadPushAny (..), ) where -import Control.Arrow ((&&&)) import Control.Error import Control.Exception (ErrorCall (ErrorCall)) import Control.Lens (to, view, (.~), (^.)) @@ -43,7 +40,6 @@ import Data.List.Extra qualified as List import Data.List1 (List1, list1) import Data.Map qualified as Map import Data.Range -import Data.Sequence qualified as Seq import Data.Set qualified as Set import Data.Text qualified as Text import Data.UUID qualified as UUID @@ -74,16 +70,9 @@ import Wire.API.Push.V2 push :: [Push] -> Gundeck () push ps = do - bulk :: Bool <- view (options . settings . bulkPush) - rs <- - if bulk - then (Right <$> pushAll ps) `catch` (pure . Left . Seq.singleton) - else pushAny ps - case rs of - Right () -> pure () - Left exs -> do - forM_ exs $ Log.err . msg . (val "Push failed: " +++) . show - throwM (mkError status500 "server-error" "Server Error") + pushAll ps `catch` \(ex :: SomeException) -> do + Log.err $ msg (val "Push failed") . Log.field "error" (displayException ex) + throwM (mkError status500 "server-error" "Server Error") -- | Abstract over all effects in 'pushAll' (for unit testing). class (MonadThrow m) => MonadPushAll m where @@ -134,59 +123,6 @@ instance MonadMapAsync Gundeck where Nothing -> mapAsync f l Just chunkSize -> concat <$> mapM (mapAsync f) (List.chunksOf chunkSize l) --- | Abstract over all effects in 'pushAny' (for unit testing). -class (MonadPushAll m, MonadNativeTargets m, MonadMapAsync m) => MonadPushAny m where - mpyPush :: - Notification -> - List1 NotificationTarget -> - Maybe UserId -> - Maybe ConnId -> - Set ConnId -> - m [Presence] - -instance MonadPushAny Gundeck where - mpyPush = Web.push - --- | Send individual HTTP requests to cannon for every device and notification. --- --- REFACTOR: This should go away in the future, once 'pushAll' has been proven to always do the same --- thing. also check what types this removal would make unnecessary. -pushAny :: - forall m. - (MonadPushAny m) => - [Push] -> - m (Either (Seq.Seq SomeException) ()) -pushAny ps = collectErrors <$> mntgtMapAsync pushAny' ps - where - collectErrors :: [Either SomeException ()] -> Either (Seq.Seq SomeException) () - collectErrors = runAllE . foldMap (AllE . fmapL Seq.singleton) - -pushAny' :: - forall m. - (MonadPushAny m) => - Push -> - m () -pushAny' p = do - i <- mpaMkNotificationId - let pload = p ^. pushPayload - let notif = Notification i (p ^. pushTransient) pload - let rcps = fromRange (p ^. pushRecipients) - let uniq = uncurry list1 $ head &&& tail $ toList rcps - let tgts = mkTarget <$> uniq - unless (p ^. pushTransient) $ - mpaStreamAdd i tgts pload =<< mpaNotificationTTL - mpaForkIO $ do - alreadySent <- mpyPush notif tgts (p ^. pushOrigin) (p ^. pushOriginConnection) (p ^. pushConnections) - unless (p ^. pushTransient) $ - mpaPushNative notif (p ^. pushNativePriority) =<< nativeTargets p (nativeTargetsRecipients p) alreadySent - where - mkTarget :: Recipient -> NotificationTarget - mkTarget r = - target (r ^. recipientId) - & targetClients .~ case r ^. recipientClients of - RecipientClientsAll -> [] - RecipientClientsSome cs -> toList cs - -- | Construct and send a single bulk push request to the client. Write the 'Notification's from -- the request to C*. Trigger native pushes for all delivery failures notifications. pushAll :: (MonadPushAll m, MonadNativeTargets m, MonadMapAsync m) => [Push] -> m () diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index a9fedf99269..5db3cbb086c 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -50,7 +50,7 @@ import Data.Set qualified as Set import Data.Text.Encoding qualified as T import Data.UUID qualified as UUID import Data.UUID.V4 -import Gundeck.Options hiding (bulkPush) +import Gundeck.Options import Gundeck.Options qualified as O import Imports import Network.HTTP.Client qualified as Http diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index e1c345fed73..10bc5806bb6 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -439,9 +439,6 @@ instance MonadMapAsync MockGundeck where mntgtPerPushConcurrency = pure Nothing -- (unbounded) mntgtMapAsync f xs = Right <$$> mapM f xs -- (no concurrency) -instance MonadPushAny MockGundeck where - mpyPush = mockOldSimpleWebPush - instance MonadBulkPush MockGundeck where mbpBulkSend = mockBulkSend mbpDeleteAllPresences _ = pure () -- FUTUREWORK: test presence deletion logic @@ -644,53 +641,6 @@ mockBulkSend uri notifs = do BulkPushResponse [(ntfId ntif, trgt, getstatus trgt) | (ntif, trgt) <- flat] -mockOldSimpleWebPush :: - (HasCallStack, m ~ MockGundeck) => - Notification -> - List1 NotificationTarget -> - Maybe UserId -> - Maybe ConnId -> - Set ConnId -> - m [Presence] -mockOldSimpleWebPush notif tgts _senderid mconnid connWhitelist = do - env <- ask - getstatus <- mkWSStatus - let clients :: [(UserId, ClientId)] - clients = - -- reformat - fmap (\(PushTarget uid connid) -> (uid, clientIdFromConnId connid)) - -- drop all broken web sockets - . filter ((== PushStatusOk) . getstatus) - -- do not push to sending device - . filter ((/= mconnid) . Just . ptConnId) - -- reformat - . mconcat - . fmap - ( ( \tgt -> - PushTarget (tgt ^. targetUser) - . fakeConnId - <$> (tgt ^. targetClients) - ) - -- apply filters - . connWhitelistSieve - . emptyMeansFullHack - ) - $ toList tgts - connWhitelistSieve :: NotificationTarget -> NotificationTarget - connWhitelistSieve = - if null connWhitelist - then id - else targetClients %~ filter ((`elem` connWhitelist) . fakeConnId) - emptyMeansFullHack :: NotificationTarget -> NotificationTarget - emptyMeansFullHack tgt = - tgt - & targetClients %~ \case - [] -> clientIdsOfUser env (tgt ^. targetUser) - same@(_ : _) -> same - forM_ clients $ \(userid, clientid) -> do - msWSQueue %= deliver (userid, clientid) (ntfPayload notif) - pure $ uncurry fakePresence <$> clients - ---------------------------------------------------------------------- -- helpers diff --git a/services/gundeck/test/unit/Push.hs b/services/gundeck/test/unit/Push.hs index a65b85445e6..69c4b777cb6 100644 --- a/services/gundeck/test/unit/Push.hs +++ b/services/gundeck/test/unit/Push.hs @@ -21,7 +21,7 @@ module Push where import Data.Aeson qualified as Aeson -import Gundeck.Push (pushAll, pushAny) +import Gundeck.Push (pushAll) import Gundeck.Push.Websocket as Web (bulkPush) import Imports import MockGundeck @@ -83,11 +83,8 @@ pushAllProp env (Pretty pushes) = where ((), realst) = runMockGundeck env (pushAll pushes) ((), mockst) = runMockGundeck env (mockPushAll pushes) - (errs, oldst) = runMockGundeck env (pushAny pushes) props = [ (Aeson.eitherDecode . Aeson.encode) pushes === Right pushes, (Aeson.eitherDecode . Aeson.encode) env === Right env, - counterexample "real vs. mock:" $ realst === mockst, - counterexample "real vs. old:" $ realst === oldst, - counterexample "old errors:" $ isRight errs === True + counterexample "real vs. mock:" $ realst === mockst ] From bd5694c0641db9a31a30e3afb16e977980371547 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 10 Oct 2024 13:13:24 +0200 Subject: [PATCH 109/136] [WPB-11188] LegalHold support V1 (#4284) --- changelog.d/5-internal/WBP-11188 | 1 + integration/test/Test/LegalHold.hs | 148 ++++++++++---- .../test/Testlib/MockIntegrationService.hs | 89 ++++++-- .../src/Wire/API/Team/LegalHold/External.hs | 193 ++++++++++-------- ...n-test-generation-patch-DO-NOT-MERGE.patch | 12 +- .../golden/Test/Wire/API/Golden/Generated.hs | 30 ++- .../Generated/LegalHoldServiceConfirm_team.hs | 184 ++--------------- .../Generated/LegalHoldServiceRemove_team.hs | 144 ++----------- .../RequestNewLegalHoldClient_team.hs | 124 ++--------- ...ject_LegalHoldServiceConfirmV0_team_1.json | 6 + ...ject_LegalHoldServiceConfirmV0_team_2.json | 6 + ...Object_LegalHoldServiceConfirm_team_1.json | 11 +- ...bject_LegalHoldServiceConfirm_team_10.json | 6 - ...bject_LegalHoldServiceConfirm_team_11.json | 6 - ...bject_LegalHoldServiceConfirm_team_12.json | 6 - ...bject_LegalHoldServiceConfirm_team_13.json | 6 - ...bject_LegalHoldServiceConfirm_team_14.json | 6 - ...bject_LegalHoldServiceConfirm_team_15.json | 6 - ...bject_LegalHoldServiceConfirm_team_16.json | 6 - ...bject_LegalHoldServiceConfirm_team_17.json | 6 - ...bject_LegalHoldServiceConfirm_team_18.json | 6 - ...bject_LegalHoldServiceConfirm_team_19.json | 6 - ...Object_LegalHoldServiceConfirm_team_2.json | 11 +- ...bject_LegalHoldServiceConfirm_team_20.json | 6 - ...Object_LegalHoldServiceConfirm_team_3.json | 6 - ...Object_LegalHoldServiceConfirm_team_4.json | 6 - ...Object_LegalHoldServiceConfirm_team_5.json | 6 - ...Object_LegalHoldServiceConfirm_team_6.json | 6 - ...Object_LegalHoldServiceConfirm_team_7.json | 6 - ...Object_LegalHoldServiceConfirm_team_8.json | 6 - ...Object_LegalHoldServiceConfirm_team_9.json | 6 - ...bject_LegalHoldServiceRemoveV0_team_1.json | 4 + ...bject_LegalHoldServiceRemoveV0_team_2.json | 4 + ...tObject_LegalHoldServiceRemove_team_1.json | 7 +- ...Object_LegalHoldServiceRemove_team_10.json | 4 - ...Object_LegalHoldServiceRemove_team_11.json | 4 - ...Object_LegalHoldServiceRemove_team_12.json | 4 - ...Object_LegalHoldServiceRemove_team_13.json | 4 - ...Object_LegalHoldServiceRemove_team_14.json | 4 - ...Object_LegalHoldServiceRemove_team_15.json | 4 - ...Object_LegalHoldServiceRemove_team_16.json | 4 - ...Object_LegalHoldServiceRemove_team_17.json | 4 - ...Object_LegalHoldServiceRemove_team_18.json | 4 - ...Object_LegalHoldServiceRemove_team_19.json | 4 - ...tObject_LegalHoldServiceRemove_team_2.json | 7 +- ...Object_LegalHoldServiceRemove_team_20.json | 4 - ...tObject_LegalHoldServiceRemove_team_3.json | 4 - ...tObject_LegalHoldServiceRemove_team_4.json | 4 - ...tObject_LegalHoldServiceRemove_team_5.json | 4 - ...tObject_LegalHoldServiceRemove_team_6.json | 4 - ...tObject_LegalHoldServiceRemove_team_7.json | 4 - ...tObject_LegalHoldServiceRemove_team_8.json | 4 - ...tObject_LegalHoldServiceRemove_team_9.json | 4 - ...ct_RequestNewLegalHoldClientV0_team_1.json | 4 + ...ct_RequestNewLegalHoldClientV0_team_2.json | 4 + ...ject_RequestNewLegalHoldClient_team_1.json | 7 +- ...ect_RequestNewLegalHoldClient_team_10.json | 4 - ...ect_RequestNewLegalHoldClient_team_11.json | 4 - ...ect_RequestNewLegalHoldClient_team_12.json | 4 - ...ect_RequestNewLegalHoldClient_team_13.json | 4 - ...ect_RequestNewLegalHoldClient_team_14.json | 4 - ...ect_RequestNewLegalHoldClient_team_15.json | 4 - ...ect_RequestNewLegalHoldClient_team_16.json | 4 - ...ect_RequestNewLegalHoldClient_team_17.json | 4 - ...ect_RequestNewLegalHoldClient_team_18.json | 4 - ...ect_RequestNewLegalHoldClient_team_19.json | 4 - ...ject_RequestNewLegalHoldClient_team_2.json | 7 +- ...ect_RequestNewLegalHoldClient_team_20.json | 4 - ...ject_RequestNewLegalHoldClient_team_3.json | 4 - ...ject_RequestNewLegalHoldClient_team_4.json | 4 - ...ject_RequestNewLegalHoldClient_team_5.json | 4 - ...ject_RequestNewLegalHoldClient_team_6.json | 4 - ...ject_RequestNewLegalHoldClient_team_7.json | 4 - ...ject_RequestNewLegalHoldClient_team_8.json | 4 - ...ject_RequestNewLegalHoldClient_team_9.json | 4 - .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 3 + services/galley/src/Galley/API/LegalHold.hs | 30 +-- .../galley/src/Galley/API/Teams/Features.hs | 3 +- .../src/Galley/External/LegalHoldService.hs | 164 +++++++++++++-- .../API/Teams/LegalHold/DisabledByDefault.hs | 10 +- 80 files changed, 607 insertions(+), 858 deletions(-) create mode 100644 changelog.d/5-internal/WBP-11188 create mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirmV0_team_1.json create mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirmV0_team_2.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_10.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_11.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_12.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_13.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_14.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_15.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_16.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_17.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_18.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_19.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_20.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_3.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_4.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_5.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_6.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_7.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_8.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_9.json create mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_1.json create mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_2.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_10.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_11.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_12.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_13.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_14.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_15.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_16.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_17.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_18.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_19.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_20.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_3.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_4.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_5.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_6.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_7.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_8.json delete mode 100644 libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_9.json create mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_1.json create mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_2.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_10.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_11.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_12.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_13.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_14.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_15.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_16.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_17.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_18.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_19.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_20.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_3.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_4.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_5.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_6.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_7.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_8.json delete mode 100644 libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_9.json diff --git a/changelog.d/5-internal/WBP-11188 b/changelog.d/5-internal/WBP-11188 new file mode 100644 index 00000000000..9965120d794 --- /dev/null +++ b/changelog.d/5-internal/WBP-11188 @@ -0,0 +1 @@ +Introduced API versioning and version negotiation for external LegalHold Service supporting `v0` and `v1` diff --git a/integration/test/Test/LegalHold.hs b/integration/test/Test/LegalHold.hs index 4b70fd0d454..e8cc0b22743 100644 --- a/integration/test/Test/LegalHold.hs +++ b/integration/test/Test/LegalHold.hs @@ -47,9 +47,9 @@ import Testlib.Prekeys import Testlib.Prelude import UnliftIO (Chan, readChan, timeout) -testLHPreventAddingNonConsentingUsers :: App () -testLHPreventAddingNonConsentingUsers = do - withMockServer def lhMockApp $ \lhDomAndPort _chan -> do +testLHPreventAddingNonConsentingUsers :: LhApiVersion -> App () +testLHPreventAddingNonConsentingUsers v = do + withMockServer def (lhMockAppV v) $ \lhDomAndPort _chan -> do (owner, tid, [alice, alex]) <- createTeam OwnDomain 3 legalholdWhitelistTeam tid owner >>= assertSuccess @@ -278,8 +278,8 @@ testLHDeleteClientManually = do -- other unspecific client error. resp.json %. "message" `shouldMatch` "LegalHold clients cannot be deleted. LegalHold must be disabled on this user by an admin" -testLHRequestDevice :: App () -testLHRequestDevice = do +testLHRequestDevice :: LhApiVersion -> App () +testLHRequestDevice v = do (alice, tid, [bob]) <- createTeam OwnDomain 2 let reqNotEnabled requester requestee = requestLegalHoldDevice tid requester requestee @@ -290,7 +290,7 @@ testLHRequestDevice = do lpk <- getLastPrekey pks <- replicateM 3 getPrekey - withMockServer def (lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhDomAndPort _chan -> do + withMockServer def (lhMockAppWithPrekeys v MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhDomAndPort _chan -> do let statusShouldBe :: String -> App () statusShouldBe status = legalholdUserStatus tid alice bob `bindResponse` \resp -> do @@ -425,8 +425,8 @@ testLHApproveDevice = do outsiderClient <- objId $ addClient outsider def `bindResponse` getJSON 201 assertNoNotifications outsider outsiderClient Nothing isUserLegalholdEnabledNotif -testLHGetDeviceStatus :: App () -testLHGetDeviceStatus = do +testLHGetDeviceStatus :: LhApiVersion -> App () +testLHGetDeviceStatus v = do -- team users -- alice (team owner) and bob (member) (alice, tid, [bob]) <- createTeam OwnDomain 2 @@ -440,7 +440,7 @@ testLHGetDeviceStatus = do withMockServer def - do lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks} + do lhMockAppWithPrekeys v MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks} \lhDomAndPort _chan -> do legalholdWhitelistTeam tid alice >>= assertStatus 200 @@ -527,8 +527,8 @@ testLHDisableForUser = do shouldBeEmpty lhClients -testLHEnablePerTeam :: App () -testLHEnablePerTeam = do +testLHEnablePerTeam :: LhApiVersion -> App () +testLHEnablePerTeam v = do -- team users -- alice (team owner) and bob (member) (alice, tid, [bob]) <- createTeam OwnDomain 2 @@ -537,7 +537,7 @@ testLHEnablePerTeam = do resp.json %. "lockStatus" `shouldMatch` "unlocked" resp.json %. "status" `shouldMatch` "disabled" - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do setUpLHDevice tid alice bob lhDomAndPort legalholdUserStatus tid alice bob `bindResponse` \resp -> do @@ -552,8 +552,8 @@ testLHEnablePerTeam = do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "enabled" -testLHGetMembersIncludesStatus :: App () -testLHGetMembersIncludesStatus = do +testLHGetMembersIncludesStatus :: LhApiVersion -> App () +testLHGetMembersIncludesStatus v = do -- team users -- alice (team owner) and bob (member) (alice, tid, [bob]) <- createTeam OwnDomain 2 @@ -569,7 +569,7 @@ testLHGetMembersIncludesStatus = do bobMember %. "legalhold_status" `shouldMatch` status statusShouldBe "no_consent" - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do statusShouldBe "no_consent" legalholdWhitelistTeam tid alice @@ -594,8 +594,8 @@ testLHGetMembersIncludesStatus = do -- bob has accepted the legalhold device statusShouldBe "enabled" -testLHConnectionsWithNonConsentingUsers :: App () -testLHConnectionsWithNonConsentingUsers = do +testLHConnectionsWithNonConsentingUsers :: LhApiVersion -> App () +testLHConnectionsWithNonConsentingUsers v = do (alice, tid, []) <- createTeam OwnDomain 1 bob <- randomUser OwnDomain def carl <- randomUser OwnDomain def @@ -604,7 +604,7 @@ testLHConnectionsWithNonConsentingUsers = do legalholdWhitelistTeam tid alice >>= assertStatus 200 - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 @@ -655,8 +655,8 @@ testLHConnectionsWithNonConsentingUsers = do resp.status `shouldMatchInt` 200 resp.json %. "members.others.0.qualified_id" `shouldMatch` objQidObject alice -testLHConnectionsWithConsentingUsers :: App () -testLHConnectionsWithConsentingUsers = do +testLHConnectionsWithConsentingUsers :: LhApiVersion -> App () +testLHConnectionsWithConsentingUsers v = do (alice, teamA, []) <- createTeam OwnDomain 1 (bob, teamB, [barbara]) <- createTeam OwnDomain 2 @@ -665,7 +665,7 @@ testLHConnectionsWithConsentingUsers = do legalholdWhitelistTeam teamB bob >>= assertStatus 200 - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings teamA alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 @@ -765,8 +765,8 @@ testLHNoConsentRemoveFromGroup approvedOrPending admin = do LHApproved -> assertLabel 403 "access-denied" LHPending -> assertStatus 200 -testLHHappyFlow :: App () -testLHHappyFlow = do +testLHHappyFlow :: LhApiVersion -> App () +testLHHappyFlow v = do (alice, tid, [bob]) <- createTeam OwnDomain 2 let statusShouldBe :: String -> App () statusShouldBe status = @@ -778,7 +778,7 @@ testLHHappyFlow = do lpk <- getLastPrekey pks <- replicateM 3 getPrekey - withMockServer def (lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhDomAndPort _chan -> do + withMockServer def (lhMockAppWithPrekeys v MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 -- implicit consent @@ -810,8 +810,8 @@ testLHHappyFlow = do >>= assertJust "client id is present" resp.json %. "last_prekey" `shouldMatch` lpk -testLHGetStatus :: App () -testLHGetStatus = do +testLHGetStatus :: LhApiVersion -> App () +testLHGetStatus v = do (alice, tid, [bob]) <- createTeam OwnDomain 2 (charlie, _tidCharlie, [debora]) <- createTeam OwnDomain 2 emil <- randomUser OwnDomain def @@ -826,7 +826,7 @@ testLHGetStatus = do check u bob "no_consent" check u emil "no_consent" legalholdWhitelistTeam tid alice >>= assertStatus 200 - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 for_ [alice, bob, charlie, debora, emil] \u -> do check u bob "disabled" @@ -835,14 +835,14 @@ testLHGetStatus = do approveLegalHoldDevice tid bob defPassword >>= assertStatus 200 check debora bob "enabled" -testLHCannotCreateGroupWithUsersInConflict :: App () -testLHCannotCreateGroupWithUsersInConflict = do +testLHCannotCreateGroupWithUsersInConflict :: LhApiVersion -> App () +testLHCannotCreateGroupWithUsersInConflict v = do (alice, tidAlice, [bob]) <- createTeam OwnDomain 2 (charlie, _tidCharlie, [debora]) <- createTeam OwnDomain 2 legalholdWhitelistTeam tidAlice alice >>= assertStatus 200 connectTwoUsers bob charlie connectTwoUsers bob debora - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tidAlice alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 postConversation bob defProteus {qualifiedUsers = [charlie, alice], newUsersRole = "wire_member", team = Just tidAlice} >>= assertStatus 201 @@ -856,8 +856,8 @@ testLHCannotCreateGroupWithUsersInConflict = do postConversation bob defProteus {qualifiedUsers = [debora, alice], newUsersRole = "wire_member", team = Just tidAlice} >>= assertLabel 403 "missing-legalhold-consent" -testLHNoConsentCannotBeInvited :: (HasCallStack) => App () -testLHNoConsentCannotBeInvited = do +testLHNoConsentCannotBeInvited :: (HasCallStack) => LhApiVersion -> App () +testLHNoConsentCannotBeInvited v = do -- team that is legalhold whitelisted (legalholder, tidLH, userLHNotActivated : _) <- createTeam OwnDomain 2 legalholdWhitelistTeam tidLH legalholder >>= assertStatus 200 @@ -868,7 +868,7 @@ testLHNoConsentCannotBeInvited = do connectUsers [peer, userLHNotActivated] connectUsers [peer2, userLHNotActivated] - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tidLH legalholder (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 cid <- postConversation userLHNotActivated defProteus {qualifiedUsers = [legalholder], newUsersRole = "wire_admin", team = Just tidLH} >>= getJSON 201 addMembers userLHNotActivated cid (def {users = [peer], role = Just "wire_admin"}) >>= assertSuccess @@ -888,12 +888,12 @@ testLHNoConsentCannotBeInvited = do addMembers userLHNotActivated cid (def {users = [peer3]}) >>= assertLabel 403 "not-connected" -testLHDisableBeforeApproval :: (HasCallStack) => App () -testLHDisableBeforeApproval = do +testLHDisableBeforeApproval :: (HasCallStack) => LhApiVersion -> App () +testLHDisableBeforeApproval v = do (alice, tid, [bob]) <- createTeam OwnDomain 2 legalholdWhitelistTeam tid alice >>= assertStatus 200 - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 -- alice requests a legalhold device for bob and sets his status to "pending" @@ -955,13 +955,13 @@ testBlockClaimingKeyPackageForLHUsers = do -- since he doesn't need to claim his own keypackage to do so, this would succeed -- we need to check upon group creation if the user is under legalhold and reject -- the operation if they are -testBlockCreateMLSConvForLHUsers :: (HasCallStack) => App () -testBlockCreateMLSConvForLHUsers = do +testBlockCreateMLSConvForLHUsers :: (HasCallStack) => LhApiVersion -> App () +testBlockCreateMLSConvForLHUsers v = do (alice, tid, [charlie]) <- createTeam OwnDomain 2 [alice1, charlie1] <- traverse (createMLSClient def) [alice, charlie] _ <- uploadNewKeyPackage alice1 legalholdWhitelistTeam tid alice >>= assertStatus 200 - withMockServer def lhMockApp \lhDomAndPort _chan -> do + withMockServer def (lhMockAppV v) \lhDomAndPort _chan -> do postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 requestLegalHoldDevice tid alice charlie >>= assertSuccess approveLegalHoldDevice tid (charlie %. "qualified_id") defPassword >>= assertSuccess @@ -992,3 +992,73 @@ testBlockCreateMLSConvForLHUsers = do >>= \mp -> postMLSCommitBundle mp.sender (mkBundle mp) `bindResponse` assertLabel 409 "mls-legal-hold-not-allowed" + +testLHApiV1 :: App () +testLHApiV1 = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 + + legalholdWhitelistTeam tid alice >>= assertSuccess + + withMockServer def (lhMockAppV V1) \lhDomAndPort chan -> do + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + + checkChan chan \(req, _) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "GET" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "status"]) + + requestLegalHoldDevice tid alice bob >>= assertStatus 201 + + checkChan chan \(req, _) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "GET" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "api-version"]) + + checkChan chan \(req, body) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "POST" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "v1", "initiate"]) + let (Just (value :: Value)) = decode body + value %. "team_id" `shouldMatch` tid + value %. "qualified_user_id.id" `shouldMatch` objId bob + value %. "qualified_user_id.domain" `shouldMatch` objDomain bob + + approveLegalHoldDevice tid (bob %. "qualified_id") defPassword >>= assertSuccess + + checkChan chan \(req, _) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "GET" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "api-version"]) + + checkChan chan \(req, body) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "POST" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "v1", "confirm"]) + let (Just (value :: Value)) = decode body + value %. "team_id" `shouldMatch` tid + value %. "qualified_user_id.id" `shouldMatch` objId bob + value %. "qualified_user_id.domain" `shouldMatch` objDomain bob + (isJust <$> value `lookupField` "client_id") `shouldMatch` True + + disableLegalHold tid alice bob defPassword >>= assertStatus 200 + + checkChan chan \(req, _) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "GET" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "api-version"]) + + checkChan chan \(req, body) -> runMaybeT . lift $ do + BS8.unpack req.requestMethod `shouldMatch` "POST" + req.pathInfo `shouldMatch` (T.pack <$> ["legalhold", "v1", "remove"]) + let (Just (value :: Value)) = decode body + value %. "team_id" `shouldMatch` tid + value %. "qualified_user_id.id" `shouldMatch` objId bob + value %. "qualified_user_id.domain" `shouldMatch` objDomain bob + +testNoCommonVersion :: App () +testNoCommonVersion = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 + + legalholdWhitelistTeam tid alice >>= assertSuccess + + withMockServer def lhMockNoCommonVersion \lhDomAndPort _ -> do + legalholdWhitelistTeam tid alice >>= assertStatus 200 + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertSuccess + + bindResponse (requestLegalHoldDevice tid alice bob) $ \resp -> do + resp.status `shouldMatchInt` 500 + resp.json %. "label" `shouldMatch` "server-error" diff --git a/integration/test/Testlib/MockIntegrationService.hs b/integration/test/Testlib/MockIntegrationService.hs index 95dccb2fff7..3af81e87783 100644 --- a/integration/test/Testlib/MockIntegrationService.hs +++ b/integration/test/Testlib/MockIntegrationService.hs @@ -2,10 +2,13 @@ module Testlib.MockIntegrationService ( withMockServer, lhMockAppWithPrekeys, lhMockApp, + lhMockAppV, + lhMockNoCommonVersion, mkLegalHoldSettings, CreateMock (..), LiftedApplication, MockServerSettings (..), + LhApiVersion (..), ) where @@ -65,7 +68,10 @@ withMockServer settings mkApp go = withFreePortAnyAddr \(sPort, sock) -> do Nothing -> error . show =<< poll srv lhMockApp :: Chan (Wai.Request, LBS.ByteString) -> LiftedApplication -lhMockApp = lhMockAppWithPrekeys def +lhMockApp = lhMockAppWithPrekeys V0 def + +lhMockAppV :: LhApiVersion -> Chan (Wai.Request, LBS.ByteString) -> LiftedApplication +lhMockAppV v = lhMockAppWithPrekeys v def data MockServerSettings = MkMockServerSettings { -- | the certificate the mock service uses @@ -98,25 +104,47 @@ instance (App ~ f) => Default (CreateMock f) where somePrekeys = replicateM 3 getPrekey } +data LhApiVersion = V0 | V1 + deriving (Show, Generic) + -- | LegalHold service. Just fake the API, do not maintain any internal state. lhMockAppWithPrekeys :: - CreateMock App -> Chan (Wai.Request, LBS.ByteString) -> LiftedApplication -lhMockAppWithPrekeys mks ch req cont = withRunInIO \inIO -> do + LhApiVersion -> CreateMock App -> Chan (Wai.Request, LBS.ByteString) -> LiftedApplication +lhMockAppWithPrekeys version mks ch req cont = withRunInIO \inIO -> do reqBody <- Wai.strictRequestBody req writeChan ch (req, reqBody) inIO do - (nextLastPrekey, threePrekeys) <- - (,) - <$> mks.nextLastPrey - <*> mks.somePrekeys - case (cs <$> pathInfo req, cs $ requestMethod req, cs @_ @String <$> getRequestHeader "Authorization" req) of - (["legalhold", "status"], "GET", _) -> cont respondOk - (_, _, Nothing) -> cont missingAuth - (["legalhold", "initiate"], "POST", Just _) -> cont (initiateResp nextLastPrekey threePrekeys) - (["legalhold", "confirm"], "POST", Just _) -> cont respondOk - (["legalhold", "remove"], "POST", Just _) -> cont respondOk - _ -> cont respondBad + case version of + V0 -> + case (cs <$> pathInfo req, cs $ requestMethod req, cs @_ @String <$> getRequestHeader "Authorization" req) of + (["legalhold", "status"], "GET", _) -> cont respondOk + (_, _, Nothing) -> cont missingAuth + (["legalhold", "initiate"], "POST", Just _) -> do + (nextLastPrekey, threePrekeys) <- getPreyKeys + cont (initiateResp nextLastPrekey threePrekeys) + (["legalhold", "confirm"], "POST", Just _) -> cont respondOk + (["legalhold", "remove"], "POST", Just _) -> cont respondOk + _ -> cont respondBad + V1 -> + case (cs <$> pathInfo req, cs $ requestMethod req, cs @_ @String <$> getRequestHeader "Authorization" req) of + (["legalhold", "status"], "GET", _) -> cont respondOk + (["legalhold", "api-version"], "GET", _) -> cont $ apiVersionResp [0, 1] + (_, _, Nothing) -> cont missingAuth + (["legalhold", "initiate"], "POST", Just _) -> do + (nextLastPrekey, threePrekeys) <- getPreyKeys + cont (initiateResp nextLastPrekey threePrekeys) + (["legalhold", "confirm"], "POST", Just _) -> cont respondOk + (["legalhold", "remove"], "POST", Just _) -> cont respondOk + (["legalhold", "v1", "initiate"], "POST", Just _) -> do + (nextLastPrekey, threePrekeys) <- getPreyKeys + cont (initiateResp nextLastPrekey threePrekeys) + (["legalhold", "v1", "confirm"], "POST", Just _) -> cont respondOk + (["legalhold", "v1", "remove"], "POST", Just _) -> cont respondOk + _ -> cont respondBad where + getPreyKeys :: App (Value, [Value]) + getPreyKeys = (,) <$> mks.nextLastPrey <*> mks.somePrekeys + initiateResp :: Value -> [Value] -> Wai.Response initiateResp npk pks = responseLBS status200 [(hContentType, cs "application/json")] @@ -126,17 +154,34 @@ lhMockAppWithPrekeys mks ch req cont = withRunInIO \inIO -> do "last_prekey" .= npk ] - respondOk :: Wai.Response - respondOk = responseLBS status200 mempty mempty +apiVersionResp :: [Int] -> Wai.Response +apiVersionResp versions = + responseLBS status200 [(hContentType, cs "application/json")] + . encode + . Data.Aeson.object + $ [ "supported" .= versions + ] - respondBad :: Wai.Response - respondBad = responseLBS status404 mempty mempty +respondOk :: Wai.Response +respondOk = responseLBS status200 mempty mempty - missingAuth :: Wai.Response - missingAuth = responseLBS status400 mempty (cs "no authorization header") +respondBad :: Wai.Response +respondBad = responseLBS status404 mempty mempty - getRequestHeader :: String -> Wai.Request -> Maybe ByteString - getRequestHeader name = lookup (fromString name) . requestHeaders +missingAuth :: Wai.Response +missingAuth = responseLBS status400 mempty (cs "no authorization header") + +getRequestHeader :: String -> Wai.Request -> Maybe ByteString +getRequestHeader name = lookup (fromString name) . requestHeaders + +lhMockNoCommonVersion :: + Chan () -> LiftedApplication +lhMockNoCommonVersion _ req cont = withRunInIO \inIO -> do + inIO do + case (cs <$> pathInfo req, cs $ requestMethod req) of + (["legalhold", "status"], "GET") -> cont respondOk + (["legalhold", "api-version"], "GET") -> cont $ apiVersionResp [9999999] + _ -> cont respondBad mkLegalHoldSettings :: (String, Warp.Port) -> Value mkLegalHoldSettings (botHost, lhPort) = diff --git a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs b/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs index a38b8ee5096..1cb233020fd 100644 --- a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs +++ b/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs @@ -20,21 +20,28 @@ -- | Types used by the Wire server for outbound requests to a LegalHold service. module Wire.API.Team.LegalHold.External ( -- * initiate + RequestNewLegalHoldClientV0 (..), RequestNewLegalHoldClient (..), NewLegalHoldClient (..), -- * confirm + LegalHoldServiceConfirmV0 (..), LegalHoldServiceConfirm (..), -- * remove + LegalHoldServiceRemoveV0 (..), LegalHoldServiceRemove (..), + + -- * SupportedVersions + SupportedVersions (..), ) where -import Data.Aeson hiding (fieldLabelModifier) +import Data.Aeson qualified as A hiding (fieldLabelModifier) import Data.Id -import Data.Json.Util ((#)) -import Data.OpenApi +import Data.OpenApi qualified as OpenApi +import Data.Qualified +import Data.Schema import Imports import Wire.API.User.Client.Prekey import Wire.Arbitrary (Arbitrary, GenericUniform (..)) @@ -43,38 +50,35 @@ import Wire.Arbitrary (Arbitrary, GenericUniform (..)) -- initiate -- | Request payload that the LH service endpoint @/initiate@ expects -data RequestNewLegalHoldClient = RequestNewLegalHoldClient +data RequestNewLegalHoldClientV0 = RequestNewLegalHoldClientV0 { userId :: UserId, teamId :: TeamId } deriving stock (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform RequestNewLegalHoldClientV0) + deriving (A.ToJSON, A.FromJSON) via (Schema RequestNewLegalHoldClientV0) + +instance ToSchema RequestNewLegalHoldClientV0 where + schema = + object "RequestNewLegalHoldClientV0" $ + RequestNewLegalHoldClientV0 + <$> (.userId) .= field "user_id" schema + <*> (.teamId) .= field "team_id" schema + +data RequestNewLegalHoldClient = RequestNewLegalHoldClient + { userId :: Qualified UserId, + teamId :: TeamId + } + deriving stock (Show, Eq, Generic) + deriving (A.ToJSON, A.FromJSON) via (Schema RequestNewLegalHoldClient) deriving (Arbitrary) via (GenericUniform RequestNewLegalHoldClient) instance ToSchema RequestNewLegalHoldClient where - declareNamedSchema = genericDeclareNamedSchema opts - where - opts = - defaultSchemaOptions - { fieldLabelModifier = \case - "userId" -> "user_id" - "teamId" -> "team_id" - _ -> "" - } - -instance ToJSON RequestNewLegalHoldClient where - toJSON (RequestNewLegalHoldClient userId teamId) = - object $ - "user_id" - .= userId - # "team_id" - .= teamId - # [] - -instance FromJSON RequestNewLegalHoldClient where - parseJSON = withObject "RequestNewLegalHoldClient" $ \o -> - RequestNewLegalHoldClient - <$> o .: "user_id" - <*> o .: "team_id" + schema = + object "RequestNewLegalHoldClient" $ + RequestNewLegalHoldClient + <$> (.userId) .= field "qualified_user_id" schema + <*> (.teamId) .= field "team_id" schema -- | Response payload that the LH service returns upon calling @/initiate@ data NewLegalHoldClient = NewLegalHoldClient @@ -83,38 +87,51 @@ data NewLegalHoldClient = NewLegalHoldClient } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform NewLegalHoldClient) + deriving (A.ToJSON, A.FromJSON) via (Schema NewLegalHoldClient) -instance ToSchema NewLegalHoldClient where - declareNamedSchema = genericDeclareNamedSchema opts +instance OpenApi.ToSchema NewLegalHoldClient where + declareNamedSchema = OpenApi.genericDeclareNamedSchema opts where opts = - defaultSchemaOptions - { fieldLabelModifier = \case + OpenApi.defaultSchemaOptions + { OpenApi.fieldLabelModifier = \case "newLegalHoldClientPrekeys" -> "prekeys" "newLegalHoldClientLastKey" -> "last_prekey" _ -> "" } -instance ToJSON NewLegalHoldClient where - toJSON c = - object $ - "prekeys" - .= newLegalHoldClientPrekeys c - # "last_prekey" - .= newLegalHoldClientLastKey c - # [] - -instance FromJSON NewLegalHoldClient where - parseJSON = withObject "NewLegalHoldClient" $ \o -> - NewLegalHoldClient - <$> o .: "prekeys" - <*> o .: "last_prekey" +instance ToSchema NewLegalHoldClient where + schema = + object "NewLegalHoldClient" $ + NewLegalHoldClient + <$> (.newLegalHoldClientPrekeys) .= field "prekeys" (array schema) + <*> (.newLegalHoldClientLastKey) .= field "last_prekey" schema -------------------------------------------------------------------------------- -- confirm -- Request payload for the @/confirm@ endpoint on the LegalHold Service data LegalHoldServiceConfirm = LegalHoldServiceConfirm + { clientId :: ClientId, + userId :: Qualified UserId, + teamId :: TeamId, + -- | Replace with Legal Hold Token Type + refreshToken :: Text + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform LegalHoldServiceConfirm) + deriving (A.ToJSON, A.FromJSON) via (Schema LegalHoldServiceConfirm) + +instance ToSchema LegalHoldServiceConfirm where + schema = + object "LegalHoldServiceConfirm" $ + LegalHoldServiceConfirm + <$> (.clientId) .= field "client_id" schema + <*> (.userId) .= field "qualified_user_id" schema + <*> (.teamId) .= field "team_id" schema + <*> (.refreshToken) .= field "refresh_token" schema + +data LegalHoldServiceConfirmV0 = LegalHoldServiceConfirmV0 { lhcClientId :: ClientId, lhcUserId :: UserId, lhcTeamId :: TeamId, @@ -122,51 +139,61 @@ data LegalHoldServiceConfirm = LegalHoldServiceConfirm lhcRefreshToken :: Text } deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform LegalHoldServiceConfirm) - -instance ToJSON LegalHoldServiceConfirm where - toJSON (LegalHoldServiceConfirm clientId userId teamId refreshToken) = - object $ - "client_id" - .= clientId - # "user_id" - .= userId - # "team_id" - .= teamId - # "refresh_token" - .= refreshToken - # [] - -instance FromJSON LegalHoldServiceConfirm where - parseJSON = withObject "LegalHoldServiceConfirm" $ \o -> - LegalHoldServiceConfirm - <$> o .: "client_id" - <*> o .: "user_id" - <*> o .: "team_id" - <*> o .: "refresh_token" + deriving (Arbitrary) via (GenericUniform LegalHoldServiceConfirmV0) + deriving (A.ToJSON, A.FromJSON) via (Schema LegalHoldServiceConfirmV0) + +instance ToSchema LegalHoldServiceConfirmV0 where + schema = + object "LegalHoldServiceConfirmV0" $ + LegalHoldServiceConfirmV0 + <$> (.lhcClientId) .= field "client_id" schema + <*> (.lhcUserId) .= field "user_id" schema + <*> (.lhcTeamId) .= field "team_id" schema + <*> (.lhcRefreshToken) .= field "refresh_token" schema -------------------------------------------------------------------------------- -- remove -- Request payload for the @/remove@ endpoint on the LegalHold Service data LegalHoldServiceRemove = LegalHoldServiceRemove + { userId :: Qualified UserId, + teamId :: TeamId + } + deriving stock (Eq, Show, Generic) + deriving (Arbitrary) via (GenericUniform LegalHoldServiceRemove) + deriving (A.ToJSON, A.FromJSON) via (Schema LegalHoldServiceRemove) + +instance ToSchema LegalHoldServiceRemove where + schema = + object "LegalHoldServiceRemove" $ + LegalHoldServiceRemove + <$> (.userId) .= field "qualified_user_id" schema + <*> (.teamId) .= field "team_id" schema + +data LegalHoldServiceRemoveV0 = LegalHoldServiceRemoveV0 { lhrUserId :: UserId, lhrTeamId :: TeamId } deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform LegalHoldServiceRemove) + deriving (Arbitrary) via (GenericUniform LegalHoldServiceRemoveV0) + deriving (A.ToJSON, A.FromJSON) via (Schema LegalHoldServiceRemoveV0) + +instance ToSchema LegalHoldServiceRemoveV0 where + schema = + object "LegalHoldServiceRemoveV0" $ + LegalHoldServiceRemoveV0 + <$> (.lhrUserId) .= field "user_id" schema + <*> (.lhrTeamId) .= field "team_id" schema + +-------------------------------------------------------------------------------- +-- SupportedVersions + +newtype SupportedVersions = SupportedVersions {supported :: [Int]} + deriving (A.FromJSON) via (Schema SupportedVersions) -instance ToJSON LegalHoldServiceRemove where - toJSON (LegalHoldServiceRemove userId teamId) = - object $ - "user_id" - .= userId - # "team_id" - .= teamId - # [] - -instance FromJSON LegalHoldServiceRemove where - parseJSON = withObject "LegalHoldServiceRemove" $ \o -> - LegalHoldServiceRemove - <$> o .: "user_id" - <*> o .: "team_id" +instance ToSchema SupportedVersions where + schema = + object "SupportedVersions " $ + SupportedVersions + <$> supported + .= field "supported" (array schema) diff --git a/libs/wire-api/test/golden/0001-Golden-test-generation-patch-DO-NOT-MERGE.patch b/libs/wire-api/test/golden/0001-Golden-test-generation-patch-DO-NOT-MERGE.patch index 853e11bdf0b..e42d5f15251 100644 --- a/libs/wire-api/test/golden/0001-Golden-test-generation-patch-DO-NOT-MERGE.patch +++ b/libs/wire-api/test/golden/0001-Golden-test-generation-patch-DO-NOT-MERGE.patch @@ -569,19 +569,19 @@ diff --git a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs b/libs/wire-a index feb034ada..4305a35d5 100644 --- a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs +++ b/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs -@@ -48,9 +48,12 @@ data RequestNewLegalHoldClient = RequestNewLegalHoldClient +@@ -48,9 +48,12 @@ data RequestNewLegalHoldClientV0 = RequestNewLegalHoldClientV0 { userId :: UserId, teamId :: TeamId } - deriving stock (Show, Eq, Generic) + deriving stock (Eq, Generic) - deriving (Arbitrary) via (GenericUniform RequestNewLegalHoldClient) + deriving (Arbitrary) via (GenericUniform RequestNewLegalHoldClientV0) -+instance Show RequestNewLegalHoldClient where -+ show (RequestNewLegalHoldClient uid tid) = "(RequestNewLegalHoldClient (" <> show uid <> ") (" <> show tid <> "))" ++instance Show RequestNewLegalHoldClientV0 where ++ show (RequestNewLegalHoldClientV0 uid tid) = "(RequestNewLegalHoldClientV0 (" <> show uid <> ") (" <> show tid <> "))" + - instance ToJSON RequestNewLegalHoldClient where - toJSON (RequestNewLegalHoldClient userId teamId) = + instance ToJSON RequestNewLegalHoldClientV0 where + toJSON (RequestNewLegalHoldClientV0 userId teamId) = object $ diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index a106c02cc..9503860ac 100644 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 c0827feab02..63fbe936877 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 @@ -1310,14 +1310,38 @@ tests = testObjects [(Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_1, "testObject_DisableLegalHoldForUserRequest_team_1.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_2, "testObject_DisableLegalHoldForUserRequest_team_2.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_3, "testObject_DisableLegalHoldForUserRequest_team_3.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_4, "testObject_DisableLegalHoldForUserRequest_team_4.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_5, "testObject_DisableLegalHoldForUserRequest_team_5.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_6, "testObject_DisableLegalHoldForUserRequest_team_6.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_7, "testObject_DisableLegalHoldForUserRequest_team_7.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_8, "testObject_DisableLegalHoldForUserRequest_team_8.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_9, "testObject_DisableLegalHoldForUserRequest_team_9.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_10, "testObject_DisableLegalHoldForUserRequest_team_10.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_11, "testObject_DisableLegalHoldForUserRequest_team_11.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_12, "testObject_DisableLegalHoldForUserRequest_team_12.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_13, "testObject_DisableLegalHoldForUserRequest_team_13.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_14, "testObject_DisableLegalHoldForUserRequest_team_14.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_15, "testObject_DisableLegalHoldForUserRequest_team_15.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_16, "testObject_DisableLegalHoldForUserRequest_team_16.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_17, "testObject_DisableLegalHoldForUserRequest_team_17.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_18, "testObject_DisableLegalHoldForUserRequest_team_18.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_19, "testObject_DisableLegalHoldForUserRequest_team_19.json"), (Test.Wire.API.Golden.Generated.DisableLegalHoldForUserRequest_team.testObject_DisableLegalHoldForUserRequest_team_20, "testObject_DisableLegalHoldForUserRequest_team_20.json")], testGroup "Golden: ApproveLegalHoldForUserRequest_team" $ testObjects [(Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_1, "testObject_ApproveLegalHoldForUserRequest_team_1.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_2, "testObject_ApproveLegalHoldForUserRequest_team_2.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_3, "testObject_ApproveLegalHoldForUserRequest_team_3.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_4, "testObject_ApproveLegalHoldForUserRequest_team_4.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_5, "testObject_ApproveLegalHoldForUserRequest_team_5.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_6, "testObject_ApproveLegalHoldForUserRequest_team_6.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_7, "testObject_ApproveLegalHoldForUserRequest_team_7.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_8, "testObject_ApproveLegalHoldForUserRequest_team_8.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_9, "testObject_ApproveLegalHoldForUserRequest_team_9.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_10, "testObject_ApproveLegalHoldForUserRequest_team_10.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_11, "testObject_ApproveLegalHoldForUserRequest_team_11.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_12, "testObject_ApproveLegalHoldForUserRequest_team_12.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_13, "testObject_ApproveLegalHoldForUserRequest_team_13.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_14, "testObject_ApproveLegalHoldForUserRequest_team_14.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_15, "testObject_ApproveLegalHoldForUserRequest_team_15.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_16, "testObject_ApproveLegalHoldForUserRequest_team_16.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_17, "testObject_ApproveLegalHoldForUserRequest_team_17.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_18, "testObject_ApproveLegalHoldForUserRequest_team_18.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_19, "testObject_ApproveLegalHoldForUserRequest_team_19.json"), (Test.Wire.API.Golden.Generated.ApproveLegalHoldForUserRequest_team.testObject_ApproveLegalHoldForUserRequest_team_20, "testObject_ApproveLegalHoldForUserRequest_team_20.json")], + testGroup "Golden: RequestNewLegalHoldClientV0_team" $ + testObjects + [ (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClientV0_team_1, "testObject_RequestNewLegalHoldClientV0_team_1.json"), + (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClientV0_team_2, "testObject_RequestNewLegalHoldClientV0_team_2.json") + ], testGroup "Golden: RequestNewLegalHoldClient_team" $ - testObjects [(Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_1, "testObject_RequestNewLegalHoldClient_team_1.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_2, "testObject_RequestNewLegalHoldClient_team_2.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_3, "testObject_RequestNewLegalHoldClient_team_3.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_4, "testObject_RequestNewLegalHoldClient_team_4.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_5, "testObject_RequestNewLegalHoldClient_team_5.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_6, "testObject_RequestNewLegalHoldClient_team_6.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_7, "testObject_RequestNewLegalHoldClient_team_7.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_8, "testObject_RequestNewLegalHoldClient_team_8.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_9, "testObject_RequestNewLegalHoldClient_team_9.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_10, "testObject_RequestNewLegalHoldClient_team_10.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_11, "testObject_RequestNewLegalHoldClient_team_11.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_12, "testObject_RequestNewLegalHoldClient_team_12.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_13, "testObject_RequestNewLegalHoldClient_team_13.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_14, "testObject_RequestNewLegalHoldClient_team_14.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_15, "testObject_RequestNewLegalHoldClient_team_15.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_16, "testObject_RequestNewLegalHoldClient_team_16.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_17, "testObject_RequestNewLegalHoldClient_team_17.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_18, "testObject_RequestNewLegalHoldClient_team_18.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_19, "testObject_RequestNewLegalHoldClient_team_19.json"), (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_20, "testObject_RequestNewLegalHoldClient_team_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_1, "testObject_RequestNewLegalHoldClient_team_1.json"), + (Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team.testObject_RequestNewLegalHoldClient_team_2, "testObject_RequestNewLegalHoldClient_team_2.json") + ], testGroup "Golden: NewLegalHoldClient_team" $ testObjects [(Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_1, "testObject_NewLegalHoldClient_team_1.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_2, "testObject_NewLegalHoldClient_team_2.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_3, "testObject_NewLegalHoldClient_team_3.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_4, "testObject_NewLegalHoldClient_team_4.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_5, "testObject_NewLegalHoldClient_team_5.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_6, "testObject_NewLegalHoldClient_team_6.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_7, "testObject_NewLegalHoldClient_team_7.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_8, "testObject_NewLegalHoldClient_team_8.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_9, "testObject_NewLegalHoldClient_team_9.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_10, "testObject_NewLegalHoldClient_team_10.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_11, "testObject_NewLegalHoldClient_team_11.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_12, "testObject_NewLegalHoldClient_team_12.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_13, "testObject_NewLegalHoldClient_team_13.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_14, "testObject_NewLegalHoldClient_team_14.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_15, "testObject_NewLegalHoldClient_team_15.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_16, "testObject_NewLegalHoldClient_team_16.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_17, "testObject_NewLegalHoldClient_team_17.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_18, "testObject_NewLegalHoldClient_team_18.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_19, "testObject_NewLegalHoldClient_team_19.json"), (Test.Wire.API.Golden.Generated.NewLegalHoldClient_team.testObject_NewLegalHoldClient_team_20, "testObject_NewLegalHoldClient_team_20.json")], + testGroup "Golden: LegalHoldServiceConfirmV0_team" $ + testObjects + [ (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirmV0_team_1, "testObject_LegalHoldServiceConfirmV0_team_1.json"), + (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirmV0_team_2, "testObject_LegalHoldServiceConfirmV0_team_2.json") + ], testGroup "Golden: LegalHoldServiceConfirm_team" $ - testObjects [(Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_1, "testObject_LegalHoldServiceConfirm_team_1.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_2, "testObject_LegalHoldServiceConfirm_team_2.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_3, "testObject_LegalHoldServiceConfirm_team_3.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_4, "testObject_LegalHoldServiceConfirm_team_4.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_5, "testObject_LegalHoldServiceConfirm_team_5.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_6, "testObject_LegalHoldServiceConfirm_team_6.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_7, "testObject_LegalHoldServiceConfirm_team_7.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_8, "testObject_LegalHoldServiceConfirm_team_8.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_9, "testObject_LegalHoldServiceConfirm_team_9.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_10, "testObject_LegalHoldServiceConfirm_team_10.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_11, "testObject_LegalHoldServiceConfirm_team_11.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_12, "testObject_LegalHoldServiceConfirm_team_12.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_13, "testObject_LegalHoldServiceConfirm_team_13.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_14, "testObject_LegalHoldServiceConfirm_team_14.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_15, "testObject_LegalHoldServiceConfirm_team_15.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_16, "testObject_LegalHoldServiceConfirm_team_16.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_17, "testObject_LegalHoldServiceConfirm_team_17.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_18, "testObject_LegalHoldServiceConfirm_team_18.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_19, "testObject_LegalHoldServiceConfirm_team_19.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_20, "testObject_LegalHoldServiceConfirm_team_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_1, "testObject_LegalHoldServiceConfirm_team_1.json"), + (Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team.testObject_LegalHoldServiceConfirm_team_2, "testObject_LegalHoldServiceConfirm_team_2.json") + ], + testGroup "Golden: LegalHoldServiceRemoveV0_team" $ + testObjects + [ (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemoveV0_team_1, "testObject_LegalHoldServiceRemoveV0_team_1.json"), + (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemoveV0_team_2, "testObject_LegalHoldServiceRemoveV0_team_2.json") + ], testGroup "Golden: LegalHoldServiceRemove_team" $ - testObjects [(Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_1, "testObject_LegalHoldServiceRemove_team_1.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_2, "testObject_LegalHoldServiceRemove_team_2.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_3, "testObject_LegalHoldServiceRemove_team_3.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_4, "testObject_LegalHoldServiceRemove_team_4.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_5, "testObject_LegalHoldServiceRemove_team_5.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_6, "testObject_LegalHoldServiceRemove_team_6.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_7, "testObject_LegalHoldServiceRemove_team_7.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_8, "testObject_LegalHoldServiceRemove_team_8.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_9, "testObject_LegalHoldServiceRemove_team_9.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_10, "testObject_LegalHoldServiceRemove_team_10.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_11, "testObject_LegalHoldServiceRemove_team_11.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_12, "testObject_LegalHoldServiceRemove_team_12.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_13, "testObject_LegalHoldServiceRemove_team_13.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_14, "testObject_LegalHoldServiceRemove_team_14.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_15, "testObject_LegalHoldServiceRemove_team_15.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_16, "testObject_LegalHoldServiceRemove_team_16.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_17, "testObject_LegalHoldServiceRemove_team_17.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_18, "testObject_LegalHoldServiceRemove_team_18.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_19, "testObject_LegalHoldServiceRemove_team_19.json"), (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_20, "testObject_LegalHoldServiceRemove_team_20.json")], + testObjects + [ (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_1, "testObject_LegalHoldServiceRemove_team_1.json"), + (Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team.testObject_LegalHoldServiceRemove_team_2, "testObject_LegalHoldServiceRemove_team_2.json") + ], testGroup "Golden: TeamMember_team" $ testObjects [(Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_1, "testObject_TeamMember_team_1.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_2, "testObject_TeamMember_team_2.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_3, "testObject_TeamMember_team_3.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_4, "testObject_TeamMember_team_4.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_5, "testObject_TeamMember_team_5.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_6, "testObject_TeamMember_team_6.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_7, "testObject_TeamMember_team_7.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_8, "testObject_TeamMember_team_8.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_9, "testObject_TeamMember_team_9.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_10, "testObject_TeamMember_team_10.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_11, "testObject_TeamMember_team_11.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_12, "testObject_TeamMember_team_12.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_13, "testObject_TeamMember_team_13.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_14, "testObject_TeamMember_team_14.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_15, "testObject_TeamMember_team_15.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_16, "testObject_TeamMember_team_16.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_17, "testObject_TeamMember_team_17.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_18, "testObject_TeamMember_team_18.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_19, "testObject_TeamMember_team_19.json"), (Test.Wire.API.Golden.Generated.TeamMember_team.testObject_TeamMember_team_20, "testObject_TeamMember_team_20.json")], testGroup "Golden: ListType_team" $ diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceConfirm_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceConfirm_team.hs index 70d77b0edbf..4e0a5d13558 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceConfirm_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceConfirm_team.hs @@ -17,187 +17,45 @@ module Test.Wire.API.Golden.Generated.LegalHoldServiceConfirm_team where +import Data.Domain (Domain (Domain)) import Data.Id +import Data.Qualified (Qualified (Qualified)) import Data.UUID qualified as UUID (fromString) import Imports (fromJust) -import Wire.API.Team.LegalHold.External (LegalHoldServiceConfirm (..)) +import Wire.API.Team.LegalHold.External (LegalHoldServiceConfirm (..), LegalHoldServiceConfirmV0 (..)) -testObject_LegalHoldServiceConfirm_team_1 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_1 = - LegalHoldServiceConfirm +testObject_LegalHoldServiceConfirmV0_team_1 :: LegalHoldServiceConfirmV0 +testObject_LegalHoldServiceConfirmV0_team_1 = + LegalHoldServiceConfirmV0 { lhcClientId = ClientId 0x1d, lhcUserId = Id (fromJust (UUID.fromString "00000003-0000-0004-0000-000100000000")), lhcTeamId = Id (fromJust (UUID.fromString "00000007-0000-0000-0000-000600000005")), lhcRefreshToken = "i>\ACKO" } -testObject_LegalHoldServiceConfirm_team_2 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_2 = - LegalHoldServiceConfirm +testObject_LegalHoldServiceConfirmV0_team_2 :: LegalHoldServiceConfirmV0 +testObject_LegalHoldServiceConfirmV0_team_2 = + LegalHoldServiceConfirmV0 { lhcClientId = ClientId 0x15, lhcUserId = Id (fromJust (UUID.fromString "00000002-0000-0008-0000-000200000007")), lhcTeamId = Id (fromJust (UUID.fromString "00000007-0000-0004-0000-000600000002")), lhcRefreshToken = "\\i" } -testObject_LegalHoldServiceConfirm_team_3 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_3 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 4, - lhcUserId = Id (fromJust (UUID.fromString "00000001-0000-0004-0000-000600000005")), - lhcTeamId = Id (fromJust (UUID.fromString "00000003-0000-0005-0000-000100000001")), - lhcRefreshToken = ")" - } - -testObject_LegalHoldServiceConfirm_team_4 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_4 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x1b, - lhcUserId = Id (fromJust (UUID.fromString "00000008-0000-0002-0000-000300000001")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0008-0000-000300000004")), - lhcRefreshToken = "W" - } - -testObject_LegalHoldServiceConfirm_team_5 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_5 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x12, - lhcUserId = Id (fromJust (UUID.fromString "00000000-0000-0005-0000-000300000006")), - lhcTeamId = Id (fromJust (UUID.fromString "00000002-0000-0008-0000-000400000007")), - lhcRefreshToken = "\1021908hL\1101997\23856\180103" - } - -testObject_LegalHoldServiceConfirm_team_6 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_6 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 1, - lhcUserId = Id (fromJust (UUID.fromString "00000005-0000-0002-0000-000300000003")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0008-0000-000200000006")), - lhcRefreshToken = "\1089885\983521b" - } - -testObject_LegalHoldServiceConfirm_team_7 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_7 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x1c, - lhcUserId = Id (fromJust (UUID.fromString "00000005-0000-0001-0000-000600000001")), - lhcTeamId = Id (fromJust (UUID.fromString "00000006-0000-0004-0000-000500000003")), - lhcRefreshToken = "\1048812[\ETBu\r" - } - -testObject_LegalHoldServiceConfirm_team_8 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_8 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x1f, - lhcUserId = Id (fromJust (UUID.fromString "00000003-0000-0008-0000-000200000001")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0004-0000-000500000004")), - lhcRefreshToken = "ZU\990363;\US\ESC" - } - -testObject_LegalHoldServiceConfirm_team_9 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_9 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 3, - lhcUserId = Id (fromJust (UUID.fromString "00000003-0000-0008-0000-000100000003")), - lhcTeamId = Id (fromJust (UUID.fromString "00000008-0000-0006-0000-000000000006")), - lhcRefreshToken = "Y\1088702" - } - -testObject_LegalHoldServiceConfirm_team_10 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_10 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x20, - lhcUserId = Id (fromJust (UUID.fromString "00000006-0000-0005-0000-000500000006")), - lhcTeamId = Id (fromJust (UUID.fromString "00000001-0000-0005-0000-000700000001")), - lhcRefreshToken = "" - } - -testObject_LegalHoldServiceConfirm_team_11 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_11 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0, - lhcUserId = Id (fromJust (UUID.fromString "00000006-0000-0002-0000-000700000007")), - lhcTeamId = Id (fromJust (UUID.fromString "00000002-0000-0005-0000-000400000007")), - lhcRefreshToken = "\153567@-c\ENQ" - } - -testObject_LegalHoldServiceConfirm_team_12 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_12 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0, - lhcUserId = Id (fromJust (UUID.fromString "00000005-0000-0006-0000-000500000004")), - lhcTeamId = Id (fromJust (UUID.fromString "00000007-0000-0008-0000-000600000006")), - lhcRefreshToken = "" - } - -testObject_LegalHoldServiceConfirm_team_13 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_13 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0xc, - lhcUserId = Id (fromJust (UUID.fromString "00000002-0000-0005-0000-000600000005")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0000-0000-000100000007")), - lhcRefreshToken = "DXD[" - } - -testObject_LegalHoldServiceConfirm_team_14 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_14 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 2, - lhcUserId = Id (fromJust (UUID.fromString "00000007-0000-0003-0000-000200000003")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0001-0000-000400000003")), - lhcRefreshToken = "T\1068224\DC3\177787\STX" - } - -testObject_LegalHoldServiceConfirm_team_15 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_15 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x1a, - lhcUserId = Id (fromJust (UUID.fromString "00000005-0000-0005-0000-000300000007")), - lhcTeamId = Id (fromJust (UUID.fromString "00000004-0000-0003-0000-000100000004")), - lhcRefreshToken = "\n' \FS~\137351)" - } - -testObject_LegalHoldServiceConfirm_team_16 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_16 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0xe, - lhcUserId = Id (fromJust (UUID.fromString "00000003-0000-0002-0000-000000000000")), - lhcTeamId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000300000000")), - lhcRefreshToken = "\65915\163144\n" - } - -testObject_LegalHoldServiceConfirm_team_17 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_17 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0xe, - lhcUserId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000600000004")), - lhcTeamId = Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000400000008")), - lhcRefreshToken = "" - } - -testObject_LegalHoldServiceConfirm_team_18 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_18 = - LegalHoldServiceConfirm - { lhcClientId = ClientId 0x11, - lhcUserId = Id (fromJust (UUID.fromString "00000006-0000-0000-0000-000800000004")), - lhcTeamId = Id (fromJust (UUID.fromString "00000006-0000-0003-0000-000100000005")), - lhcRefreshToken = "Y\1029262" - } - -testObject_LegalHoldServiceConfirm_team_19 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_19 = +testObject_LegalHoldServiceConfirm_team_1 :: LegalHoldServiceConfirm +testObject_LegalHoldServiceConfirm_team_1 = LegalHoldServiceConfirm - { lhcClientId = ClientId 0x1c, - lhcUserId = Id (fromJust (UUID.fromString "00000003-0000-0006-0000-000700000002")), - lhcTeamId = Id (fromJust (UUID.fromString "00000001-0000-0003-0000-000600000000")), - lhcRefreshToken = "[" + { clientId = ClientId 4, + userId = Qualified (Id (fromJust (UUID.fromString "00000001-0000-0004-0000-000600000005"))) (Domain "example.com"), + teamId = Id (fromJust (UUID.fromString "00000003-0000-0005-0000-000100000001")), + refreshToken = ")" } -testObject_LegalHoldServiceConfirm_team_20 :: LegalHoldServiceConfirm -testObject_LegalHoldServiceConfirm_team_20 = +testObject_LegalHoldServiceConfirm_team_2 :: LegalHoldServiceConfirm +testObject_LegalHoldServiceConfirm_team_2 = LegalHoldServiceConfirm - { lhcClientId = ClientId 1, - lhcUserId = Id (fromJust (UUID.fromString "00000001-0000-0004-0000-000600000005")), - lhcTeamId = Id (fromJust (UUID.fromString "00000006-0000-0001-0000-000500000008")), - lhcRefreshToken = "i\FS" + { clientId = ClientId 0x1b, + userId = Qualified (Id (fromJust (UUID.fromString "00000008-0000-0002-0000-000300000001"))) (Domain "example.com"), + teamId = Id (fromJust (UUID.fromString "00000004-0000-0008-0000-000300000004")), + refreshToken = "W" } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceRemove_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceRemove_team.hs index 9506233523a..20d979d1dc3 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceRemove_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/LegalHoldServiceRemove_team.hs @@ -17,147 +17,37 @@ module Test.Wire.API.Golden.Generated.LegalHoldServiceRemove_team where +import Data.Domain (Domain (Domain)) import Data.Id (Id (Id)) +import Data.Qualified (Qualified (Qualified)) import Data.UUID qualified as UUID (fromString) import Imports (fromJust) -import Wire.API.Team.LegalHold.External (LegalHoldServiceRemove (..)) +import Wire.API.Team.LegalHold.External -testObject_LegalHoldServiceRemove_team_1 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_1 = - LegalHoldServiceRemove +testObject_LegalHoldServiceRemoveV0_team_1 :: LegalHoldServiceRemoveV0 +testObject_LegalHoldServiceRemoveV0_team_1 = + LegalHoldServiceRemoveV0 { lhrUserId = Id (fromJust (UUID.fromString "00000034-0000-0016-0000-003c00000024")), lhrTeamId = Id (fromJust (UUID.fromString "0000001e-0000-000f-0000-007100000079")) } -testObject_LegalHoldServiceRemove_team_2 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_2 = - LegalHoldServiceRemove +testObject_LegalHoldServiceRemoveV0_team_2 :: LegalHoldServiceRemoveV0 +testObject_LegalHoldServiceRemoveV0_team_2 = + LegalHoldServiceRemoveV0 { lhrUserId = Id (fromJust (UUID.fromString "0000004f-0000-0076-0000-001f00000019")), lhrTeamId = Id (fromJust (UUID.fromString "00000050-0000-0059-0000-004d00000067")) } -testObject_LegalHoldServiceRemove_team_3 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_3 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000001a-0000-0072-0000-003e00000008")), - lhrTeamId = Id (fromJust (UUID.fromString "0000006c-0000-005c-0000-002100000019")) - } - -testObject_LegalHoldServiceRemove_team_4 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_4 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000003c-0000-0013-0000-003b00000001")), - lhrTeamId = Id (fromJust (UUID.fromString "0000007c-0000-0060-0000-007400000077")) - } - -testObject_LegalHoldServiceRemove_team_5 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_5 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000000-0000-005e-0000-00680000007c")), - lhrTeamId = Id (fromJust (UUID.fromString "0000003f-0000-002e-0000-003900000032")) - } - -testObject_LegalHoldServiceRemove_team_6 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_6 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000004b-0000-0014-0000-007e00000010")), - lhrTeamId = Id (fromJust (UUID.fromString "0000005d-0000-0053-0000-005f00000044")) - } - -testObject_LegalHoldServiceRemove_team_7 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_7 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000002c-0000-0020-0000-003900000073")), - lhrTeamId = Id (fromJust (UUID.fromString "0000002d-0000-002b-0000-005c0000003c")) - } - -testObject_LegalHoldServiceRemove_team_8 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_8 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000003a-0000-0066-0000-001a0000001e")), - lhrTeamId = Id (fromJust (UUID.fromString "00000060-0000-007d-0000-002c00000059")) - } - -testObject_LegalHoldServiceRemove_team_9 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_9 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000037-0000-0024-0000-005e00000067")), - lhrTeamId = Id (fromJust (UUID.fromString "0000006e-0000-0072-0000-00260000000a")) - } - -testObject_LegalHoldServiceRemove_team_10 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_10 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000077-0000-0003-0000-001b00000033")), - lhrTeamId = Id (fromJust (UUID.fromString "0000000d-0000-0013-0000-007100000063")) - } - -testObject_LegalHoldServiceRemove_team_11 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_11 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000062-0000-0018-0000-007b0000002e")), - lhrTeamId = Id (fromJust (UUID.fromString "00000009-0000-007b-0000-00050000004b")) - } - -testObject_LegalHoldServiceRemove_team_12 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_12 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000017-0000-0030-0000-002d0000002b")), - lhrTeamId = Id (fromJust (UUID.fromString "00000023-0000-0000-0000-004100000061")) - } - -testObject_LegalHoldServiceRemove_team_13 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_13 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000055-0000-005d-0000-00140000001a")), - lhrTeamId = Id (fromJust (UUID.fromString "00000055-0000-0050-0000-000600000019")) - } - -testObject_LegalHoldServiceRemove_team_14 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_14 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000015-0000-0061-0000-003e00000067")), - lhrTeamId = Id (fromJust (UUID.fromString "0000001b-0000-005f-0000-006b00000040")) - } - -testObject_LegalHoldServiceRemove_team_15 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_15 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000006a-0000-005d-0000-005d00000072")), - lhrTeamId = Id (fromJust (UUID.fromString "0000004e-0000-0066-0000-002c00000021")) - } - -testObject_LegalHoldServiceRemove_team_16 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_16 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000005c-0000-0064-0000-00120000002a")), - lhrTeamId = Id (fromJust (UUID.fromString "0000000d-0000-0001-0000-000500000049")) - } - -testObject_LegalHoldServiceRemove_team_17 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_17 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000068-0000-001b-0000-006a0000005a")), - lhrTeamId = Id (fromJust (UUID.fromString "00000019-0000-002e-0000-005c00000010")) - } - -testObject_LegalHoldServiceRemove_team_18 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_18 = - LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "0000007d-0000-0044-0000-004d00000004")), - lhrTeamId = Id (fromJust (UUID.fromString "00000019-0000-003f-0000-007000000071")) - } - -testObject_LegalHoldServiceRemove_team_19 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_19 = +testObject_LegalHoldServiceRemove_team_1 :: LegalHoldServiceRemove +testObject_LegalHoldServiceRemove_team_1 = LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000040-0000-0053-0000-00060000001b")), - lhrTeamId = Id (fromJust (UUID.fromString "00000014-0000-0022-0000-005a00000075")) + { userId = Qualified (Id (fromJust (UUID.fromString "00000034-0000-0016-0000-003c00000024"))) (Domain "example.com"), + teamId = Id (fromJust (UUID.fromString "0000001e-0000-000f-0000-007100000079")) } -testObject_LegalHoldServiceRemove_team_20 :: LegalHoldServiceRemove -testObject_LegalHoldServiceRemove_team_20 = +testObject_LegalHoldServiceRemove_team_2 :: LegalHoldServiceRemove +testObject_LegalHoldServiceRemove_team_2 = LegalHoldServiceRemove - { lhrUserId = Id (fromJust (UUID.fromString "00000012-0000-005d-0000-00790000003e")), - lhrTeamId = Id (fromJust (UUID.fromString "0000006d-0000-006f-0000-007c0000006e")) + { userId = Qualified (Id (fromJust (UUID.fromString "0000004f-0000-0076-0000-001f00000019"))) (Domain "example.com"), + teamId = Id (fromJust (UUID.fromString "00000050-0000-0059-0000-004d00000067")) } diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RequestNewLegalHoldClient_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RequestNewLegalHoldClient_team.hs index ffa7d4d915f..9b74a35c8b0 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RequestNewLegalHoldClient_team.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RequestNewLegalHoldClient_team.hs @@ -17,127 +17,33 @@ module Test.Wire.API.Golden.Generated.RequestNewLegalHoldClient_team where +import Data.Domain import Data.Id (Id (Id)) +import Data.Qualified import Data.UUID qualified as UUID (fromString) import Imports (fromJust) -import Wire.API.Team.LegalHold.External (RequestNewLegalHoldClient (RequestNewLegalHoldClient)) +import Wire.API.Team.LegalHold.External -testObject_RequestNewLegalHoldClient_team_1 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_1 = - RequestNewLegalHoldClient +testObject_RequestNewLegalHoldClientV0_team_1 :: RequestNewLegalHoldClientV0 +testObject_RequestNewLegalHoldClientV0_team_1 = + RequestNewLegalHoldClientV0 (Id (fromJust (UUID.fromString "0000003d-0000-0049-0000-003b00000055"))) (Id (fromJust (UUID.fromString "0000002e-0000-006e-0000-004a0000001b"))) -testObject_RequestNewLegalHoldClient_team_2 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_2 = - RequestNewLegalHoldClient +testObject_RequestNewLegalHoldClientV0_team_2 :: RequestNewLegalHoldClientV0 +testObject_RequestNewLegalHoldClientV0_team_2 = + RequestNewLegalHoldClientV0 (Id (fromJust (UUID.fromString "0000001c-0000-0064-0000-003a0000000b"))) (Id (fromJust (UUID.fromString "00000049-0000-0059-0000-004e0000001f"))) -testObject_RequestNewLegalHoldClient_team_3 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_3 = +testObject_RequestNewLegalHoldClient_team_1 :: RequestNewLegalHoldClient +testObject_RequestNewLegalHoldClient_team_1 = RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "0000007d-0000-0054-0000-000900000018"))) + (Qualified ((Id (fromJust (UUID.fromString "0000007d-0000-0054-0000-000900000018")))) (Domain "example.com")) (Id (fromJust (UUID.fromString "0000005d-0000-001f-0000-006300000019"))) -testObject_RequestNewLegalHoldClient_team_4 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_4 = +testObject_RequestNewLegalHoldClient_team_2 :: RequestNewLegalHoldClient +testObject_RequestNewLegalHoldClient_team_2 = RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000025-0000-0077-0000-002d00000045"))) + (Qualified ((Id (fromJust (UUID.fromString "00000025-0000-0077-0000-002d00000045")))) (Domain "example.com")) (Id (fromJust (UUID.fromString "0000001a-0000-002c-0000-004e0000005c"))) - -testObject_RequestNewLegalHoldClient_team_5 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_5 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000066-0000-0055-0000-007f0000002f"))) - (Id (fromJust (UUID.fromString "0000000c-0000-0003-0000-00750000006f"))) - -testObject_RequestNewLegalHoldClient_team_6 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_6 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "0000005c-0000-0039-0000-007b0000005d"))) - (Id (fromJust (UUID.fromString "00000018-0000-0074-0000-004800000077"))) - -testObject_RequestNewLegalHoldClient_team_7 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_7 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "0000000d-0000-0057-0000-00270000003b"))) - (Id (fromJust (UUID.fromString "00000077-0000-005f-0000-00290000006e"))) - -testObject_RequestNewLegalHoldClient_team_8 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_8 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000033-0000-0004-0000-00670000003f"))) - (Id (fromJust (UUID.fromString "00000064-0000-0008-0000-004400000064"))) - -testObject_RequestNewLegalHoldClient_team_9 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_9 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000007-0000-0062-0000-006600000015"))) - (Id (fromJust (UUID.fromString "00000005-0000-0079-0000-003300000036"))) - -testObject_RequestNewLegalHoldClient_team_10 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_10 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000063-0000-004c-0000-00730000000a"))) - (Id (fromJust (UUID.fromString "00000029-0000-003f-0000-004d00000076"))) - -testObject_RequestNewLegalHoldClient_team_11 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_11 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000006-0000-0058-0000-005500000045"))) - (Id (fromJust (UUID.fromString "00000025-0000-005e-0000-00800000007b"))) - -testObject_RequestNewLegalHoldClient_team_12 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_12 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000019-0000-0066-0000-003e0000005b"))) - (Id (fromJust (UUID.fromString "0000005e-0000-0005-0000-007900000008"))) - -testObject_RequestNewLegalHoldClient_team_13 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_13 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000007-0000-0024-0000-005700000006"))) - (Id (fromJust (UUID.fromString "0000000f-0000-007b-0000-00390000005b"))) - -testObject_RequestNewLegalHoldClient_team_14 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_14 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000004-0000-0007-0000-003500000079"))) - (Id (fromJust (UUID.fromString "0000002d-0000-0028-0000-004500000077"))) - -testObject_RequestNewLegalHoldClient_team_15 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_15 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000001-0000-002b-0000-001900000031"))) - (Id (fromJust (UUID.fromString "0000005f-0000-0072-0000-005a00000009"))) - -testObject_RequestNewLegalHoldClient_team_16 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_16 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000073-0000-006d-0000-006100000043"))) - (Id (fromJust (UUID.fromString "00000070-0000-0020-0000-004d00000058"))) - -testObject_RequestNewLegalHoldClient_team_17 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_17 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "0000003b-0000-006c-0000-006e00000048"))) - (Id (fromJust (UUID.fromString "00000059-0000-001e-0000-005b00000033"))) - -testObject_RequestNewLegalHoldClient_team_18 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_18 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "0000004a-0000-000e-0000-005900000065"))) - (Id (fromJust (UUID.fromString "0000002c-0000-0017-0000-002d00000008"))) - -testObject_RequestNewLegalHoldClient_team_19 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_19 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000024-0000-006b-0000-006000000011"))) - (Id (fromJust (UUID.fromString "00000078-0000-005c-0000-004900000023"))) - -testObject_RequestNewLegalHoldClient_team_20 :: RequestNewLegalHoldClient -testObject_RequestNewLegalHoldClient_team_20 = - RequestNewLegalHoldClient - (Id (fromJust (UUID.fromString "00000059-0000-003b-0000-00410000006c"))) - (Id (fromJust (UUID.fromString "00000020-0000-0044-0000-002200000020"))) diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirmV0_team_1.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirmV0_team_1.json new file mode 100644 index 00000000000..fdaebfe23a5 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirmV0_team_1.json @@ -0,0 +1,6 @@ +{ + "client_id": "1d", + "refresh_token": "i>\u0006O", + "team_id": "00000007-0000-0000-0000-000600000005", + "user_id": "00000003-0000-0004-0000-000100000000" +} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirmV0_team_2.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirmV0_team_2.json new file mode 100644 index 00000000000..04e8d21059a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirmV0_team_2.json @@ -0,0 +1,6 @@ +{ + "client_id": "15", + "refresh_token": "\\i", + "team_id": "00000007-0000-0004-0000-000600000002", + "user_id": "00000002-0000-0008-0000-000200000007" +} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_1.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_1.json index fdaebfe23a5..7fa3ed5707b 100644 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_1.json +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_1.json @@ -1,6 +1,9 @@ { - "client_id": "1d", - "refresh_token": "i>\u0006O", - "team_id": "00000007-0000-0000-0000-000600000005", - "user_id": "00000003-0000-0004-0000-000100000000" + "client_id": "4", + "qualified_user_id": { + "domain": "example.com", + "id": "00000001-0000-0004-0000-000600000005" + }, + "refresh_token": ")", + "team_id": "00000003-0000-0005-0000-000100000001" } diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_10.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_10.json deleted file mode 100644 index 0db069bd046..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_10.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "20", - "refresh_token": "", - "team_id": "00000001-0000-0005-0000-000700000001", - "user_id": "00000006-0000-0005-0000-000500000006" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_11.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_11.json deleted file mode 100644 index 4b0fa0f0f41..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_11.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "0", - "refresh_token": "𥟟@-c\u0005", - "team_id": "00000002-0000-0005-0000-000400000007", - "user_id": "00000006-0000-0002-0000-000700000007" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_12.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_12.json deleted file mode 100644 index 672cc4cae73..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_12.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "0", - "refresh_token": "", - "team_id": "00000007-0000-0008-0000-000600000006", - "user_id": "00000005-0000-0006-0000-000500000004" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_13.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_13.json deleted file mode 100644 index 9130ffaaa1e..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_13.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "c", - "refresh_token": "DXD[", - "team_id": "00000004-0000-0000-0000-000100000007", - "user_id": "00000002-0000-0005-0000-000600000005" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_14.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_14.json deleted file mode 100644 index 7423e75fbcc..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_14.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "2", - "refresh_token": "T􄳀\u0013𫙻\u0002", - "team_id": "00000004-0000-0001-0000-000400000003", - "user_id": "00000007-0000-0003-0000-000200000003" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_15.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_15.json deleted file mode 100644 index 1085765f45f..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_15.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1a", - "refresh_token": "\n' \u001c~𡢇)", - "team_id": "00000004-0000-0003-0000-000100000004", - "user_id": "00000005-0000-0005-0000-000300000007" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_16.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_16.json deleted file mode 100644 index 0e1faaefeb5..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_16.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "e", - "refresh_token": "𐅻𧵈\n", - "team_id": "00000002-0000-0001-0000-000300000000", - "user_id": "00000003-0000-0002-0000-000000000000" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_17.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_17.json deleted file mode 100644 index 3dafa3dea0a..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_17.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "e", - "refresh_token": "", - "team_id": "00000002-0000-0000-0000-000400000008", - "user_id": "00000002-0000-0001-0000-000600000004" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_18.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_18.json deleted file mode 100644 index 131733c04f9..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_18.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "11", - "refresh_token": "Y󻒎", - "team_id": "00000006-0000-0003-0000-000100000005", - "user_id": "00000006-0000-0000-0000-000800000004" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_19.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_19.json deleted file mode 100644 index 6f4b9be7a24..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_19.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1c", - "refresh_token": "[", - "team_id": "00000001-0000-0003-0000-000600000000", - "user_id": "00000003-0000-0006-0000-000700000002" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_2.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_2.json index 04e8d21059a..f83dc811076 100644 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_2.json +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_2.json @@ -1,6 +1,9 @@ { - "client_id": "15", - "refresh_token": "\\i", - "team_id": "00000007-0000-0004-0000-000600000002", - "user_id": "00000002-0000-0008-0000-000200000007" + "client_id": "1b", + "qualified_user_id": { + "domain": "example.com", + "id": "00000008-0000-0002-0000-000300000001" + }, + "refresh_token": "W", + "team_id": "00000004-0000-0008-0000-000300000004" } diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_20.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_20.json deleted file mode 100644 index 154b8783add..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_20.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1", - "refresh_token": "i\u001c", - "team_id": "00000006-0000-0001-0000-000500000008", - "user_id": "00000001-0000-0004-0000-000600000005" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_3.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_3.json deleted file mode 100644 index d8fe0054b34..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_3.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "4", - "refresh_token": ")", - "team_id": "00000003-0000-0005-0000-000100000001", - "user_id": "00000001-0000-0004-0000-000600000005" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_4.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_4.json deleted file mode 100644 index 96f2d5c2980..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_4.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1b", - "refresh_token": "W", - "team_id": "00000004-0000-0008-0000-000300000004", - "user_id": "00000008-0000-0002-0000-000300000001" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_5.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_5.json deleted file mode 100644 index 94aec8f7721..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_5.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "12", - "refresh_token": "󹟔hL􍂭崰𫾇", - "team_id": "00000002-0000-0008-0000-000400000007", - "user_id": "00000000-0000-0005-0000-000300000006" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_6.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_6.json deleted file mode 100644 index 4674ca57a65..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_6.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1", - "refresh_token": "􊅝󰇡b", - "team_id": "00000004-0000-0008-0000-000200000006", - "user_id": "00000005-0000-0002-0000-000300000003" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_7.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_7.json deleted file mode 100644 index 2fd9f36310b..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_7.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1c", - "refresh_token": "􀃬[\u0017u\r", - "team_id": "00000006-0000-0004-0000-000500000003", - "user_id": "00000005-0000-0001-0000-000600000001" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_8.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_8.json deleted file mode 100644 index c2c75b63951..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_8.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "1f", - "refresh_token": "ZU󱲛;\u001f\u001b", - "team_id": "00000004-0000-0004-0000-000500000004", - "user_id": "00000003-0000-0008-0000-000200000001" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_9.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_9.json deleted file mode 100644 index 659092595c6..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceConfirm_team_9.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "client_id": "3", - "refresh_token": "Y􉲾", - "team_id": "00000008-0000-0006-0000-000000000006", - "user_id": "00000003-0000-0008-0000-000100000003" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_1.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_1.json new file mode 100644 index 00000000000..0486a9283bb --- /dev/null +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_1.json @@ -0,0 +1,4 @@ +{ + "team_id": "0000001e-0000-000f-0000-007100000079", + "user_id": "00000034-0000-0016-0000-003c00000024" +} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_2.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_2.json new file mode 100644 index 00000000000..68cf570501e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemoveV0_team_2.json @@ -0,0 +1,4 @@ +{ + "team_id": "00000050-0000-0059-0000-004d00000067", + "user_id": "0000004f-0000-0076-0000-001f00000019" +} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_1.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_1.json index 0486a9283bb..6ffcd4de35b 100644 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_1.json +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_1.json @@ -1,4 +1,7 @@ { - "team_id": "0000001e-0000-000f-0000-007100000079", - "user_id": "00000034-0000-0016-0000-003c00000024" + "qualified_user_id": { + "domain": "example.com", + "id": "00000034-0000-0016-0000-003c00000024" + }, + "team_id": "0000001e-0000-000f-0000-007100000079" } diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_10.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_10.json deleted file mode 100644 index 03bdfa92961..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_10.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000000d-0000-0013-0000-007100000063", - "user_id": "00000077-0000-0003-0000-001b00000033" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_11.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_11.json deleted file mode 100644 index d0e2464f10a..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_11.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000009-0000-007b-0000-00050000004b", - "user_id": "00000062-0000-0018-0000-007b0000002e" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_12.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_12.json deleted file mode 100644 index 8c99f2a8e65..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_12.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000023-0000-0000-0000-004100000061", - "user_id": "00000017-0000-0030-0000-002d0000002b" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_13.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_13.json deleted file mode 100644 index 67e2140a274..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_13.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000055-0000-0050-0000-000600000019", - "user_id": "00000055-0000-005d-0000-00140000001a" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_14.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_14.json deleted file mode 100644 index 388c2e9a932..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_14.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000001b-0000-005f-0000-006b00000040", - "user_id": "00000015-0000-0061-0000-003e00000067" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_15.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_15.json deleted file mode 100644 index 79591c3552d..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_15.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000004e-0000-0066-0000-002c00000021", - "user_id": "0000006a-0000-005d-0000-005d00000072" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_16.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_16.json deleted file mode 100644 index 6a71c6ec5c6..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_16.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000000d-0000-0001-0000-000500000049", - "user_id": "0000005c-0000-0064-0000-00120000002a" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_17.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_17.json deleted file mode 100644 index 130cc163be3..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_17.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000019-0000-002e-0000-005c00000010", - "user_id": "00000068-0000-001b-0000-006a0000005a" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_18.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_18.json deleted file mode 100644 index 17ba095a31d..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_18.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000019-0000-003f-0000-007000000071", - "user_id": "0000007d-0000-0044-0000-004d00000004" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_19.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_19.json deleted file mode 100644 index fb62a066aaa..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_19.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000014-0000-0022-0000-005a00000075", - "user_id": "00000040-0000-0053-0000-00060000001b" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_2.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_2.json index 68cf570501e..0ecc091028d 100644 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_2.json +++ b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_2.json @@ -1,4 +1,7 @@ { - "team_id": "00000050-0000-0059-0000-004d00000067", - "user_id": "0000004f-0000-0076-0000-001f00000019" + "qualified_user_id": { + "domain": "example.com", + "id": "0000004f-0000-0076-0000-001f00000019" + }, + "team_id": "00000050-0000-0059-0000-004d00000067" } diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_20.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_20.json deleted file mode 100644 index ba02dfea16f..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_20.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000006d-0000-006f-0000-007c0000006e", - "user_id": "00000012-0000-005d-0000-00790000003e" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_3.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_3.json deleted file mode 100644 index 059084d01ed..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000006c-0000-005c-0000-002100000019", - "user_id": "0000001a-0000-0072-0000-003e00000008" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_4.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_4.json deleted file mode 100644 index 84c9f3ee38f..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000007c-0000-0060-0000-007400000077", - "user_id": "0000003c-0000-0013-0000-003b00000001" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_5.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_5.json deleted file mode 100644 index e7fe336fae4..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000003f-0000-002e-0000-003900000032", - "user_id": "00000000-0000-005e-0000-00680000007c" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_6.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_6.json deleted file mode 100644 index 1a4931a609c..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000005d-0000-0053-0000-005f00000044", - "user_id": "0000004b-0000-0014-0000-007e00000010" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_7.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_7.json deleted file mode 100644 index 3f58bdb6099..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000002d-0000-002b-0000-005c0000003c", - "user_id": "0000002c-0000-0020-0000-003900000073" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_8.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_8.json deleted file mode 100644 index 4d8757d78e7..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000060-0000-007d-0000-002c00000059", - "user_id": "0000003a-0000-0066-0000-001a0000001e" -} diff --git a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_9.json b/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_9.json deleted file mode 100644 index af27be7b6d1..00000000000 --- a/libs/wire-api/test/golden/testObject_LegalHoldServiceRemove_team_9.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000006e-0000-0072-0000-00260000000a", - "user_id": "00000037-0000-0024-0000-005e00000067" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_1.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_1.json new file mode 100644 index 00000000000..520fd99c869 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_1.json @@ -0,0 +1,4 @@ +{ + "team_id": "0000002e-0000-006e-0000-004a0000001b", + "user_id": "0000003d-0000-0049-0000-003b00000055" +} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_2.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_2.json new file mode 100644 index 00000000000..e38364be589 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClientV0_team_2.json @@ -0,0 +1,4 @@ +{ + "team_id": "00000049-0000-0059-0000-004e0000001f", + "user_id": "0000001c-0000-0064-0000-003a0000000b" +} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_1.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_1.json index 520fd99c869..d6d5854d098 100644 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_1.json +++ b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_1.json @@ -1,4 +1,7 @@ { - "team_id": "0000002e-0000-006e-0000-004a0000001b", - "user_id": "0000003d-0000-0049-0000-003b00000055" + "qualified_user_id": { + "domain": "example.com", + "id": "0000007d-0000-0054-0000-000900000018" + }, + "team_id": "0000005d-0000-001f-0000-006300000019" } diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_10.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_10.json deleted file mode 100644 index ec0971407a0..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_10.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000029-0000-003f-0000-004d00000076", - "user_id": "00000063-0000-004c-0000-00730000000a" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_11.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_11.json deleted file mode 100644 index 6c1111da5f4..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_11.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000025-0000-005e-0000-00800000007b", - "user_id": "00000006-0000-0058-0000-005500000045" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_12.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_12.json deleted file mode 100644 index 91d9ffa5a64..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_12.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000005e-0000-0005-0000-007900000008", - "user_id": "00000019-0000-0066-0000-003e0000005b" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_13.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_13.json deleted file mode 100644 index cdb55dc5a51..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_13.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000000f-0000-007b-0000-00390000005b", - "user_id": "00000007-0000-0024-0000-005700000006" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_14.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_14.json deleted file mode 100644 index 8628292655b..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_14.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000002d-0000-0028-0000-004500000077", - "user_id": "00000004-0000-0007-0000-003500000079" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_15.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_15.json deleted file mode 100644 index e50c3f74ae6..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_15.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000005f-0000-0072-0000-005a00000009", - "user_id": "00000001-0000-002b-0000-001900000031" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_16.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_16.json deleted file mode 100644 index b6ad4023017..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_16.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000070-0000-0020-0000-004d00000058", - "user_id": "00000073-0000-006d-0000-006100000043" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_17.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_17.json deleted file mode 100644 index 979de7c1e16..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_17.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000059-0000-001e-0000-005b00000033", - "user_id": "0000003b-0000-006c-0000-006e00000048" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_18.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_18.json deleted file mode 100644 index 067f03d5829..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_18.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000002c-0000-0017-0000-002d00000008", - "user_id": "0000004a-0000-000e-0000-005900000065" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_19.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_19.json deleted file mode 100644 index e6a872b49be..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_19.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000078-0000-005c-0000-004900000023", - "user_id": "00000024-0000-006b-0000-006000000011" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_2.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_2.json index e38364be589..eb30cd98300 100644 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_2.json +++ b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_2.json @@ -1,4 +1,7 @@ { - "team_id": "00000049-0000-0059-0000-004e0000001f", - "user_id": "0000001c-0000-0064-0000-003a0000000b" + "qualified_user_id": { + "domain": "example.com", + "id": "00000025-0000-0077-0000-002d00000045" + }, + "team_id": "0000001a-0000-002c-0000-004e0000005c" } diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_20.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_20.json deleted file mode 100644 index 44512e4c9da..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_20.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000020-0000-0044-0000-002200000020", - "user_id": "00000059-0000-003b-0000-00410000006c" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_3.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_3.json deleted file mode 100644 index 7ad5e1808ab..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000005d-0000-001f-0000-006300000019", - "user_id": "0000007d-0000-0054-0000-000900000018" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_4.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_4.json deleted file mode 100644 index f5c4b003ee2..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_4.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000001a-0000-002c-0000-004e0000005c", - "user_id": "00000025-0000-0077-0000-002d00000045" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_5.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_5.json deleted file mode 100644 index ad84d5ccce2..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_5.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "0000000c-0000-0003-0000-00750000006f", - "user_id": "00000066-0000-0055-0000-007f0000002f" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_6.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_6.json deleted file mode 100644 index d6893b562e2..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_6.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000018-0000-0074-0000-004800000077", - "user_id": "0000005c-0000-0039-0000-007b0000005d" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_7.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_7.json deleted file mode 100644 index 6880ebd7819..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_7.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000077-0000-005f-0000-00290000006e", - "user_id": "0000000d-0000-0057-0000-00270000003b" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_8.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_8.json deleted file mode 100644 index 64ff30b8973..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_8.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000064-0000-0008-0000-004400000064", - "user_id": "00000033-0000-0004-0000-00670000003f" -} diff --git a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_9.json b/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_9.json deleted file mode 100644 index a9ef3873622..00000000000 --- a/libs/wire-api/test/golden/testObject_RequestNewLegalHoldClient_team_9.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "team_id": "00000005-0000-0079-0000-003300000036", - "user_id": "00000007-0000-0062-0000-006600000015" -} 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 83d59a00b29..65be7b6ef80 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 @@ -231,9 +231,12 @@ tests = testRoundTrip @Team.LegalHold.RemoveLegalHoldSettingsRequest, testRoundTrip @Team.LegalHold.DisableLegalHoldForUserRequest, testRoundTrip @Team.LegalHold.ApproveLegalHoldForUserRequest, + testRoundTrip @Team.LegalHold.External.RequestNewLegalHoldClientV0, testRoundTrip @Team.LegalHold.External.RequestNewLegalHoldClient, testRoundTrip @Team.LegalHold.External.NewLegalHoldClient, + testRoundTrip @Team.LegalHold.External.LegalHoldServiceConfirmV0, testRoundTrip @Team.LegalHold.External.LegalHoldServiceConfirm, + testRoundTrip @Team.LegalHold.External.LegalHoldServiceRemoveV0, testRoundTrip @Team.LegalHold.External.LegalHoldServiceRemove, testRoundTrip @Team.LegalHold.LegalholdProtectee, testRoundTrip @Team.Member.TeamMember, diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 3c15d5c1e53..0f3b88bb7a9 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -111,9 +111,9 @@ createSettings lzusr tid newService = do -- . Log.field "action" (Log.val "LegalHold.createSettings") void $ permissionCheck ChangeLegalHoldTeamSettings zusrMembership (key :: ServiceKey, fpr :: Fingerprint Rsa) <- - LegalHoldData.validateServiceKey (newLegalHoldServiceKey newService) + LegalHoldData.validateServiceKey newService.newLegalHoldServiceKey >>= noteS @'LegalHoldServiceInvalidKey - LHService.checkLegalHoldServiceStatus fpr (newLegalHoldServiceUrl newService) + LHService.checkLegalHoldServiceStatus fpr newService.newLegalHoldServiceUrl let service = legalHoldService tid fpr newService key LegalHoldData.createSettings service pure . viewLegalHoldService $ service @@ -171,7 +171,8 @@ removeSettingsInternalPaging :: Member SubConversationStore r, Member TeamFeatureStore r, Member (TeamMemberStore InternalPaging) r, - Member TeamStore r + Member TeamStore r, + Member (Embed IO) r ) => Local UserId -> TeamId -> @@ -213,7 +214,8 @@ removeSettings :: Member ProposalStore r, Member P.TinyLog r, Member Random r, - Member SubConversationStore r + Member SubConversationStore r, + Member (Embed IO) r ) => UserId -> TeamId -> @@ -268,7 +270,8 @@ removeSettings' :: Member ProposalStore r, Member Random r, Member P.TinyLog r, - Member SubConversationStore r + Member SubConversationStore r, + Member (Embed IO) r ) => TeamId -> Sem r () @@ -289,7 +292,7 @@ removeSettings' tid = removeLHForUser member = do luid <- qualifyLocal (member ^. userId) removeLegalHoldClientFromUser (tUnqualified luid) - LHService.removeLegalHold tid (tUnqualified luid) + LHService.removeLegalHold tid luid changeLegalholdStatusAndHandlePolicyConflicts tid luid (member ^. legalHoldStatus) UserLegalHoldDisabled -- (support for withdrawing consent is not planned yet.) -- | Change 'UserLegalHoldStatus' from no consent to disabled. FUTUREWORK: @@ -367,7 +370,8 @@ requestDevice :: Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Embed IO) r ) => Local UserId -> TeamId -> @@ -419,7 +423,7 @@ requestDevice lzusr tid uid = do requestDeviceFromService :: Local UserId -> Sem r (LastPrekey, [Prekey]) requestDeviceFromService luid = do LegalHoldData.dropPendingPrekeys (tUnqualified luid) - lhDevice <- LHService.requestNewDevice tid (tUnqualified luid) + lhDevice <- LHService.requestNewDevice tid luid let NewLegalHoldClient prekeys lastKey = lhDevice pure (lastKey, prekeys) @@ -460,7 +464,8 @@ approveDevice :: Member Random r, Member SubConversationStore r, Member TeamFeatureStore r, - Member TeamStore r + Member TeamStore r, + Member (Embed IO) r ) => Local UserId -> ConnId -> @@ -494,7 +499,7 @@ approveDevice lzusr connId tid uid (Public.ApproveLegalHoldForUserRequest mPassw -- checks that the user is part of a binding team -- FUTUREWORK: reduce double checks legalHoldAuthToken <- getLegalHoldAuthToken (tUnqualified luid) mPassword - LHService.confirmLegalHold clientId tid (tUnqualified luid) legalHoldAuthToken + LHService.confirmLegalHold clientId tid luid legalHoldAuthToken -- TODO: send event at this point (see also: -- https://github.com/wireapp/wire-server/pull/802#pullrequestreview-262280386) changeLegalholdStatusAndHandlePolicyConflicts tid luid userLHStatus UserLegalHoldEnabled @@ -536,7 +541,8 @@ disableForUser :: Member P.TinyLog r, Member Random r, Member SubConversationStore r, - Member TeamStore r + Member TeamStore r, + Member (Embed IO) r ) => Local UserId -> TeamId -> @@ -570,7 +576,7 @@ disableForUser lzusr tid uid (Public.DisableLegalHoldForUserRequest mPassword) = disableLH zusr luid userLHStatus = do ensureReAuthorised zusr mPassword Nothing Nothing removeLegalHoldClientFromUser uid - LHService.removeLegalHold tid uid + LHService.removeLegalHold tid luid -- TODO: send event at this point (see also: related TODO in this module in -- 'approveDevice' and -- https://github.com/wireapp/wire-server/pull/802#pullrequestreview-262280386) diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index 56c2ceaddd0..01edafee051 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -347,7 +347,8 @@ instance SetFeatureConfig LegalholdConfig where Member TeamStore r, Member (TeamMemberStore InternalPaging) r, Member P.TinyLog r, - Member Random r + Member Random r, + Member (Embed IO) r ) prepareFeature tid feat = do diff --git a/services/galley/src/Galley/External/LegalHoldService.hs b/services/galley/src/Galley/External/LegalHoldService.hs index cca80ae8800..ddc157b017f 100644 --- a/services/galley/src/Galley/External/LegalHoldService.hs +++ b/services/galley/src/Galley/External/LegalHoldService.hs @@ -30,11 +30,15 @@ where import Bilge qualified import Bilge.Response import Brig.Types.Team.LegalHold +import Control.Monad.Catch (MonadThrow (throwM)) import Data.Aeson +import Data.ByteString.Char8 qualified as BS8 import Data.ByteString.Conversion.To import Data.ByteString.Lazy.Char8 qualified as LC8 import Data.Id import Data.Misc +import Data.Qualified (Local, QualifiedWithTag (tUntagged), tUnqualified) +import Data.Set qualified as Set import Galley.Effects.LegalHoldStore as LegalHoldData import Galley.External.LegalHoldService.Types import Imports @@ -49,6 +53,30 @@ import Wire.API.Team.LegalHold.External ---------------------------------------------------------------------- -- api +data LhApiVersion = V0 | V1 + deriving stock (Eq, Ord, Show, Enum, Bounded, Generic) + +-- | Get /api-version from legal hold service; this does not throw an error because the api-version endpoint may not exist. +getLegalHoldApiVersions :: + ( Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member LegalHoldStore r + ) => + TeamId -> + Sem r (Maybe (Set LhApiVersion)) +getLegalHoldApiVersions tid = + fmap toLhApiVersion . decode . (.responseBody) <$> makeLegalHoldServiceRequest tid params + where + params = + Bilge.paths ["api-version"] + . Bilge.method GET + . Bilge.acceptJson + + toLhApiVersion :: SupportedVersions -> Set LhApiVersion + toLhApiVersion (SupportedVersions supported) = Set.fromList $ mapMaybe toVersion supported + where + toVersion 0 = Just V0 + toVersion 1 = Just V1 + toVersion _ = Nothing -- | Get /status from legal hold service; throw 'Wai.Error' if things go wrong. checkLegalHoldServiceStatus :: @@ -78,66 +106,119 @@ requestNewDevice :: ( Member (ErrorS 'LegalHoldServiceBadResponse) r, Member (ErrorS 'LegalHoldServiceNotRegistered) r, Member LegalHoldStore r, - Member P.TinyLog r + Member P.TinyLog r, + Member (Embed IO) r ) => TeamId -> - UserId -> + Local UserId -> Sem r NewLegalHoldClient -requestNewDevice tid uid = do - resp <- makeLegalHoldServiceRequest tid reqParams +requestNewDevice tid luid = do + apiVersion <- negotiateVersion tid + resp <- makeLegalHoldServiceRequest tid (reqParams apiVersion) case eitherDecode (responseBody resp) of Left e -> do P.info . Log.msg $ "Error decoding NewLegalHoldClient: " <> e throwS @'LegalHoldServiceBadResponse Right client -> pure client where - reqParams = - Bilge.paths ["initiate"] - . Bilge.json (RequestNewLegalHoldClient uid tid) + reqParams v = + versionedPaths v ["initiate"] + . mkBody v . Bilge.method POST . Bilge.acceptJson . Bilge.expect2xx + mkBody :: LhApiVersion -> Bilge.Request -> Bilge.Request + mkBody V0 = + Bilge.json + RequestNewLegalHoldClientV0 + { userId = tUnqualified luid, + teamId = tid + } + mkBody V1 = + Bilge.json + RequestNewLegalHoldClient + { userId = tUntagged luid, + teamId = tid + } + -- | @POST /confirm@ -- Confirm that a device has been linked to a user and provide an authorization token confirmLegalHold :: ( Member (ErrorS 'LegalHoldServiceNotRegistered) r, - Member LegalHoldStore r + Member P.TinyLog r, + Member LegalHoldStore r, + Member (Embed IO) r ) => ClientId -> TeamId -> - UserId -> + Local UserId -> -- | TODO: Replace with 'LegalHold' token type OpaqueAuthToken -> Sem r () -confirmLegalHold clientId tid uid legalHoldAuthToken = do - void $ makeLegalHoldServiceRequest tid reqParams +confirmLegalHold clientId tid luid legalHoldAuthToken = do + apiVersion <- negotiateVersion tid + void $ makeLegalHoldServiceRequest tid (reqParams apiVersion) where - reqParams = - Bilge.paths ["confirm"] - . Bilge.json (LegalHoldServiceConfirm clientId uid tid (opaqueAuthTokenToText legalHoldAuthToken)) + reqParams v = + versionedPaths v ["confirm"] + . mkBody v . Bilge.method POST . Bilge.acceptJson . Bilge.expect2xx + mkBody :: LhApiVersion -> Bilge.Request -> Bilge.Request + mkBody V0 = + Bilge.json + LegalHoldServiceConfirmV0 + { lhcClientId = clientId, + lhcUserId = tUnqualified luid, + lhcTeamId = tid, + lhcRefreshToken = opaqueAuthTokenToText legalHoldAuthToken + } + mkBody V1 = + Bilge.json + LegalHoldServiceConfirm + { clientId = clientId, + userId = tUntagged luid, + teamId = tid, + refreshToken = opaqueAuthTokenToText legalHoldAuthToken + } + -- | @POST /remove@ -- Inform the LegalHold Service that a user's legalhold has been disabled. removeLegalHold :: ( Member (ErrorS 'LegalHoldServiceNotRegistered) r, - Member LegalHoldStore r + Member P.TinyLog r, + Member LegalHoldStore r, + Member (Embed IO) r ) => TeamId -> - UserId -> + Local UserId -> Sem r () removeLegalHold tid uid = do - void $ makeLegalHoldServiceRequest tid reqParams + apiVersion <- negotiateVersion tid + void $ makeLegalHoldServiceRequest tid (reqParams apiVersion) where - reqParams = - Bilge.paths ["remove"] - . Bilge.json (LegalHoldServiceRemove uid tid) + reqParams v = + versionedPaths v ["remove"] + . mkBody v . Bilge.method POST . Bilge.acceptJson . Bilge.expect2xx + mkBody :: LhApiVersion -> Bilge.Request -> Bilge.Request + mkBody V0 = + Bilge.json + LegalHoldServiceRemoveV0 + { lhrUserId = tUnqualified uid, + lhrTeamId = tid + } + mkBody V1 = + Bilge.json + LegalHoldServiceRemove + { userId = tUntagged uid, + teamId = tid + } ---------------------------------------------------------------------- -- helpers @@ -167,3 +248,46 @@ makeLegalHoldServiceRequest tid reqBuilder = do mkReqBuilder token = reqBuilder . Bilge.header "Authorization" ("Bearer " <> toByteString' token) + +versionToInt :: LhApiVersion -> Int +versionToInt V0 = 0 +versionToInt V1 = 1 + +versionToBS :: LhApiVersion -> ByteString +versionToBS = ("v" <>) . BS8.pack . show . versionToInt + +versionedPaths :: LhApiVersion -> [ByteString] -> Http.Request -> Http.Request +versionedPaths V0 paths = Bilge.paths paths +versionedPaths v paths = Bilge.paths (versionToBS v : paths) + +supportedByWireServer :: Set LhApiVersion +supportedByWireServer = Set.fromList [minBound .. maxBound] + +-- | Find the highest common version between wire-server and the legalhold service. +-- If the legalhold service does not support the `/api-version` endpoint, we assume it's `v0`. +negotiateVersion :: + ( Member (ErrorS 'LegalHoldServiceNotRegistered) r, + Member LegalHoldStore r, + Member P.TinyLog r, + Member (Embed IO) r + ) => + TeamId -> + Sem r LhApiVersion +negotiateVersion tid = do + mSupportedByExternalLhService <- getLegalHoldApiVersions tid + case mSupportedByExternalLhService of + Nothing -> pure V0 + Just supportedByLhService -> do + let commonVersions = Set.intersection supportedByWireServer supportedByLhService + case Set.lookupMax commonVersions of + Nothing -> do + P.warn $ + Log.msg (Log.val "Version negotiation with legal hold service failed. No common versions found.") + . Log.field "team_id" (show tid) + liftIO $ throwM LegalHoldNoCommonVersions + Just v -> pure v + +data LegalHoldVersionNegotiationException = LegalHoldNoCommonVersions + deriving (Show) + +instance Exception LegalHoldVersionNegotiationException diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index c0ff9975269..507cfaacdcf 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -212,7 +212,7 @@ testApproveLegalHoldDevice = do WS.bracketRN cannon [owner, member, member, member2, outsideContact, stranger] $ \[ows, mws, mws', member2Ws, outsideContactWs, strangerWs] -> withDummyTestServiceForTeam' owner tid $ \_ chan -> do requestLegalHoldDevice owner member tid !!! testResponse 201 Nothing - liftIO . assertMatchJSON chan $ \(RequestNewLegalHoldClient userId' teamId') -> do + liftIO . assertMatchJSON chan $ \(RequestNewLegalHoldClientV0 userId' teamId') -> do assertEqual "userId == member" userId' member assertEqual "teamId == tid" teamId' tid -- Only the user themself can approve adding a LH device @@ -236,7 +236,7 @@ testApproveLegalHoldDevice = do userStatus let pluck = \case Ev.ClientAdded eClient -> do - clientId eClient @?= someClientId + eClient.clientId @?= someClientId clientType eClient @?= LegalHoldClientType clientClass eClient @?= Just LegalHoldClient _ -> assertBool "Unexpected event" False @@ -315,7 +315,7 @@ testDisableLegalHoldForUser = do approveLegalHoldDevice (Just defPassword) member member tid !!! testResponse 200 Nothing assertNotification mws $ \case Ev.ClientAdded client -> do - clientId client @?= someClientId + client.clientId @?= someClientId clientType client @?= LegalHoldClientType clientClass client @?= Just LegalHoldClient _ -> assertBool "Unexpected event" False @@ -648,7 +648,7 @@ testOldClientsBlockDeviceHandshake = do >>> Set.unions >>> Set.toList >>> head - >>> clientId + >>> (.clientId) withDummyTestServiceForTeam' legalholder tid $ \_ _chan -> do grantConsent tid legalholder @@ -726,7 +726,7 @@ testClaimKeys testcase = do >>> Set.unions >>> Set.toList >>> head - >>> clientId + >>> (.clientId) let makePeerClient :: TestM () makePeerClient = case testcase of From 54cdc266c80fadf4543a8bb7de4bb6a79e1b9146 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Mon, 14 Oct 2024 14:55:19 +0200 Subject: [PATCH 110/136] [WPB-11472] Revert "Work around legacy integration test resource leak. (#4244)" (#4287) * Revert "Work around legacy integration test resource leak. (#4244)" This reverts commit a72c70a9a9b9d71af1b864384e80b6d3eb0827a9. (it turns out this only helps with resource consumption because after running the first bach of tests, defaultMainWithIngredients exits... m| * hi ci --- services/brig/test/integration/Run.hs | 28 ++++++++------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/services/brig/test/integration/Run.hs b/services/brig/test/integration/Run.hs index 9cfadcf53b1..36adc72a8ec 100644 --- a/services/brig/test/integration/Run.hs +++ b/services/brig/test/integration/Run.hs @@ -54,7 +54,6 @@ import Options.Applicative hiding (action) import SMTP qualified import System.Environment (withArgs) import System.Logger qualified as Logger -import System.Mem (performGC) import Test.Tasty import Test.Tasty.Ingredients import Test.Tasty.Runners @@ -151,14 +150,16 @@ runTests iConf brigOpts otherArgs = do let smtp = SMTP.tests mg lg oauthAPI = API.OAuth.tests mg db b n brigOpts - -- run the tests in two parts, with a gc in between. i did this on a hunch, and for some - -- reason this reduces the hunger for open file handles at run time significantly, and makes - -- the suite pass with my ulimit settings. (fisx) - withArgs otherArgs . defaultMainWithIngredients (listingTests : (composeReporters antXMLRunner consoleTestReporter) : defaultIngredients) $ testGroup - "Brig API Integration, part 1" - $ [ systemSettingsApi, + "Brig API Integration" + $ [ userApi, + providerApi, + searchApis, + teamApis, + turnApi, + metricsApi, + systemSettingsApi, settingsApi, createIndex, userPendingActivation, @@ -169,19 +170,6 @@ runTests iConf brigOpts otherArgs = do oauthAPI, federationEnd2End ] - - performGC - - withArgs otherArgs . defaultMainWithIngredients (listingTests : (composeReporters antXMLRunner consoleTestReporter) : defaultIngredients) - $ testGroup - "Brig API Integration, part 2" - $ [ userApi, - providerApi, - searchApis, - teamApis, - turnApi, - metricsApi - ] where mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p From e0296a9dd90b85aafd46c94f220aff315bad0a0a Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 15 Oct 2024 16:04:33 +0200 Subject: [PATCH 111/136] Makefile: Add target to template helmfile (#4201) --- Makefile | 2 +- hack/bin/helm-template.sh | 54 ++++++++++++++++++------ hack/bin/integration-setup-federation.sh | 1 + hack/lib/dirs.sh | 8 ++++ hack/lib/helm-overrides.sh | 15 +++++++ hack/lib/helmfile-env-vars.sh | 14 ++++++ hack/lib/kube-metadata.sh | 8 ++++ nix/wire-server.nix | 1 + 8 files changed, 89 insertions(+), 14 deletions(-) create mode 100755 hack/lib/dirs.sh create mode 100755 hack/lib/helm-overrides.sh create mode 100755 hack/lib/helmfile-env-vars.sh create mode 100755 hack/lib/kube-metadata.sh diff --git a/Makefile b/Makefile index 1413ff10e82..56f2aa58bd4 100644 --- a/Makefile +++ b/Makefile @@ -586,7 +586,7 @@ kind-restart-%: .local/kind-kubeconfig # templating issues without actually installing anything, and without needing # access to a kubernetes cluster. e.g.: # make helm-template-wire-server -helm-template-%: clean-charts charts-integration +helm-template-%: # clean-charts charts-integration ./hack/bin/helm-template.sh $(*) # Ask the security team for the `DEPENDENCY_TRACK_API_KEY` (if you need it) diff --git a/hack/bin/helm-template.sh b/hack/bin/helm-template.sh index d72684ce9ba..b64bcde43df 100755 --- a/hack/bin/helm-template.sh +++ b/hack/bin/helm-template.sh @@ -6,23 +6,51 @@ # access to a kubernetes cluster USAGE="Usage: $0" +set -x + set -e -chart=${1:?$USAGE} +release=${1:?$USAGE} DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TOP_LEVEL="$DIR/../.." CHARTS_DIR="${TOP_LEVEL}/.local/charts" -valuesfile="${DIR}/../helm_vars/${chart}/values.yaml" -certificatesfile="${DIR}/../helm_vars/${chart}/certificates.yaml" -declare -a options=() -if [ -f "$valuesfile" ]; then - options+=(-f "$valuesfile") -fi -if [ -f "$certificatesfile" ]; then - options+=(-f "$certificatesfile") -fi - -"$DIR/update.sh" "$CHARTS_DIR/$chart" -helm template $"chart" "$CHARTS_DIR/$chart" ${options[*]} + +# for f in "${DIR}/../helm_vars/${chart}"/*; do +# options+=(-f "$f") +# done + +# valuesfile="${DIR}/../helm_vars/${chart}/values.yaml" +# valuestmplfile="${DIR}/../helm_vars/${chart}/values.yaml.gotmpl" +# certificatesfile="${DIR}/../helm_vars/${chart}/certificates.yaml" +# if [ -f "$valuesfile" ]; then +# options+=(-f "$valuesfile") +# fi +# if [ -f "$certificatesfile" ]; then +# options+=(-f "$certificatesfile") +# fi + +# "$DIR/update.sh" "$CHARTS_DIR/$chart" +# helm template "$chart" "$CHARTS_DIR/$chart" ${options[*]} + +export NAMESPACE_1="test-template" +export FEDERATION_DOMAIN_BASE_1="$NAMESPACE_1.svc.cluster.local" +export FEDERATION_DOMAIN_1="federation-test-helper.$FEDERATION_DOMAIN_BASE_1" + +export NAMESPACE_2="test-template-fed2" +export FEDERATION_DOMAIN_BASE_2="$NAMESPACE_2.svc.cluster.local" +export FEDERATION_DOMAIN_2="federation-test-helper.$FEDERATION_DOMAIN_BASE_2" + +FEDERATION_CA_CERTIFICATE=$(cat "$TOP_LEVEL/deploy/dockerephemeral/federation-v0/integration-ca.pem") +export FEDERATION_CA_CERTIFICATE + +export INGRESS_CHART="ingress-nginx-controller" + +charts=(fake-aws databases-ephemeral redis-cluster rabbitmq wire-server ingress-nginx-controller nginx-ingress-controller nginx-ingress-services) + +# for chart in "${charts[@]}"; do +# "$DIR"/update.sh "$CHARTS_DIR/$chart" +# done + +helmfile template --environment kind --skip-deps -f "$TOP_LEVEL/hack/helmfile.yaml" "$release" diff --git a/hack/bin/integration-setup-federation.sh b/hack/bin/integration-setup-federation.sh index 939f1d4f56d..d25ae2138dd 100755 --- a/hack/bin/integration-setup-federation.sh +++ b/hack/bin/integration-setup-federation.sh @@ -35,6 +35,7 @@ else export INGRESS_CHART="nginx-ingress-controller" fi echo "kubeVersion: $KUBERNETES_VERSION and ingress controller=$INGRESS_CHART" + export NAMESPACE_1="$NAMESPACE" export FEDERATION_DOMAIN_BASE_1="$NAMESPACE_1.svc.cluster.local" export FEDERATION_DOMAIN_1="federation-test-helper.$FEDERATION_DOMAIN_BASE_1" diff --git a/hack/lib/dirs.sh b/hack/lib/dirs.sh new file mode 100755 index 00000000000..358491801f0 --- /dev/null +++ b/hack/lib/dirs.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TOP_LEVEL="$(cd "$LIB_DIR/../.." && pwd)" +BIN_DIR="${TOP_LEVEL}/hack/bin" +CHARTS_DIR="${TOP_LEVEL}/.local/charts" + +export LIB_DIR TOP_LEVEL BIN_DIR CHARTS_DIR diff --git a/hack/lib/helm-overrides.sh b/hack/lib/helm-overrides.sh new file mode 100755 index 00000000000..63498978315 --- /dev/null +++ b/hack/lib/helm-overrides.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# Helm (v3) writes into XDG folders only these days. They don't honor HELM_ vars +# anymore. +# Derive a helm-specific folder inside the wire-server/.local to avoid polluting +# ~. + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TOP_LEVEL="$DIR/../.." +LOCAL_HELM_FOLDER="$TOP_LEVEL/.local/helm" + +[[ -e $LOCAL_HELM_FOLDER ]] || mkdir -p "$LOCAL_HELM_FOLDER" +export XDG_CACHE_HOME=${LOCAL_HELM_FOLDER}/cache +export XDG_CONFIG_HOME=${LOCAL_HELM_FOLDER}/config +export XDG_DATA_HOME=${LOCAL_HELM_FOLDER}/data diff --git a/hack/lib/helmfile-env-vars.sh b/hack/lib/helmfile-env-vars.sh new file mode 100755 index 00000000000..a12d40c32fc --- /dev/null +++ b/hack/lib/helmfile-env-vars.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +export NAMESPACE=${NAMESPACE:-test-integration} +# Available $HELMFILE_ENV profiles: default, default-ssl, kind, kind-ssl +export HELMFILE_ENV=${HELMFILE_ENV:-default} +export HELM_PARALLELISM=${HELM_PARALLELISM:-1} + +export NAMESPACE_1="$NAMESPACE" +export FEDERATION_DOMAIN_BASE_1="$NAMESPACE_1.svc.cluster.local" +export FEDERATION_DOMAIN_1="federation-test-helper.$FEDERATION_DOMAIN_BASE_1" + +export NAMESPACE_2="$NAMESPACE-fed2" +export FEDERATION_DOMAIN_BASE_2="$NAMESPACE_2.svc.cluster.local" +export FEDERATION_DOMAIN_2="federation-test-helper.$FEDERATION_DOMAIN_BASE_2" diff --git a/hack/lib/kube-metadata.sh b/hack/lib/kube-metadata.sh new file mode 100755 index 00000000000..0cd482d0708 --- /dev/null +++ b/hack/lib/kube-metadata.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + + +KUBERNETES_VERSION_MAJOR="$(kubectl version -o json | jq -r .serverVersion.major)" +KUBERNETES_VERSION_MINOR="$(kubectl version -o json | jq -r .serverVersion.minor)" +KUBERNETES_VERSION_MINOR="${KUBERNETES_VERSION_MINOR//[!0-9]/}" # some clusters report minor versions as a string like '27+'. Strip any non-digit characters. + +export KUBERNETES_VERSION_MAJOR KUBERNETES_VERSION_MINOR diff --git a/nix/wire-server.nix b/nix/wire-server.nix index bf1593940f4..aa2e0ef79a3 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -315,6 +315,7 @@ let openssl nix-output-monitor which + awscli2 ]; images = localMods@{ enableOptimization, enableDocs, enableTests }: From 1e0fab0fb8e06cf9eee463929c93436c78a9e336 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 15 Oct 2024 18:16:53 +0200 Subject: [PATCH 112/136] Revert "Makefile: Add target to template helmfile (#4201)" (#4294) This reverts commit e0296a9dd90b85aafd46c94f220aff315bad0a0a. --- Makefile | 2 +- hack/bin/helm-template.sh | 54 ++++++------------------ hack/bin/integration-setup-federation.sh | 1 - hack/lib/dirs.sh | 8 ---- hack/lib/helm-overrides.sh | 15 ------- hack/lib/helmfile-env-vars.sh | 14 ------ hack/lib/kube-metadata.sh | 8 ---- nix/wire-server.nix | 1 - 8 files changed, 14 insertions(+), 89 deletions(-) delete mode 100755 hack/lib/dirs.sh delete mode 100755 hack/lib/helm-overrides.sh delete mode 100755 hack/lib/helmfile-env-vars.sh delete mode 100755 hack/lib/kube-metadata.sh diff --git a/Makefile b/Makefile index 56f2aa58bd4..1413ff10e82 100644 --- a/Makefile +++ b/Makefile @@ -586,7 +586,7 @@ kind-restart-%: .local/kind-kubeconfig # templating issues without actually installing anything, and without needing # access to a kubernetes cluster. e.g.: # make helm-template-wire-server -helm-template-%: # clean-charts charts-integration +helm-template-%: clean-charts charts-integration ./hack/bin/helm-template.sh $(*) # Ask the security team for the `DEPENDENCY_TRACK_API_KEY` (if you need it) diff --git a/hack/bin/helm-template.sh b/hack/bin/helm-template.sh index b64bcde43df..d72684ce9ba 100755 --- a/hack/bin/helm-template.sh +++ b/hack/bin/helm-template.sh @@ -6,51 +6,23 @@ # access to a kubernetes cluster USAGE="Usage: $0" -set -x - set -e -release=${1:?$USAGE} +chart=${1:?$USAGE} DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TOP_LEVEL="$DIR/../.." CHARTS_DIR="${TOP_LEVEL}/.local/charts" - -# for f in "${DIR}/../helm_vars/${chart}"/*; do -# options+=(-f "$f") -# done - -# valuesfile="${DIR}/../helm_vars/${chart}/values.yaml" -# valuestmplfile="${DIR}/../helm_vars/${chart}/values.yaml.gotmpl" -# certificatesfile="${DIR}/../helm_vars/${chart}/certificates.yaml" -# if [ -f "$valuesfile" ]; then -# options+=(-f "$valuesfile") -# fi -# if [ -f "$certificatesfile" ]; then -# options+=(-f "$certificatesfile") -# fi - -# "$DIR/update.sh" "$CHARTS_DIR/$chart" -# helm template "$chart" "$CHARTS_DIR/$chart" ${options[*]} - -export NAMESPACE_1="test-template" -export FEDERATION_DOMAIN_BASE_1="$NAMESPACE_1.svc.cluster.local" -export FEDERATION_DOMAIN_1="federation-test-helper.$FEDERATION_DOMAIN_BASE_1" - -export NAMESPACE_2="test-template-fed2" -export FEDERATION_DOMAIN_BASE_2="$NAMESPACE_2.svc.cluster.local" -export FEDERATION_DOMAIN_2="federation-test-helper.$FEDERATION_DOMAIN_BASE_2" - -FEDERATION_CA_CERTIFICATE=$(cat "$TOP_LEVEL/deploy/dockerephemeral/federation-v0/integration-ca.pem") -export FEDERATION_CA_CERTIFICATE - -export INGRESS_CHART="ingress-nginx-controller" - -charts=(fake-aws databases-ephemeral redis-cluster rabbitmq wire-server ingress-nginx-controller nginx-ingress-controller nginx-ingress-services) - -# for chart in "${charts[@]}"; do -# "$DIR"/update.sh "$CHARTS_DIR/$chart" -# done - -helmfile template --environment kind --skip-deps -f "$TOP_LEVEL/hack/helmfile.yaml" "$release" +valuesfile="${DIR}/../helm_vars/${chart}/values.yaml" +certificatesfile="${DIR}/../helm_vars/${chart}/certificates.yaml" +declare -a options=() +if [ -f "$valuesfile" ]; then + options+=(-f "$valuesfile") +fi +if [ -f "$certificatesfile" ]; then + options+=(-f "$certificatesfile") +fi + +"$DIR/update.sh" "$CHARTS_DIR/$chart" +helm template $"chart" "$CHARTS_DIR/$chart" ${options[*]} diff --git a/hack/bin/integration-setup-federation.sh b/hack/bin/integration-setup-federation.sh index d25ae2138dd..939f1d4f56d 100755 --- a/hack/bin/integration-setup-federation.sh +++ b/hack/bin/integration-setup-federation.sh @@ -35,7 +35,6 @@ else export INGRESS_CHART="nginx-ingress-controller" fi echo "kubeVersion: $KUBERNETES_VERSION and ingress controller=$INGRESS_CHART" - export NAMESPACE_1="$NAMESPACE" export FEDERATION_DOMAIN_BASE_1="$NAMESPACE_1.svc.cluster.local" export FEDERATION_DOMAIN_1="federation-test-helper.$FEDERATION_DOMAIN_BASE_1" diff --git a/hack/lib/dirs.sh b/hack/lib/dirs.sh deleted file mode 100755 index 358491801f0..00000000000 --- a/hack/lib/dirs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TOP_LEVEL="$(cd "$LIB_DIR/../.." && pwd)" -BIN_DIR="${TOP_LEVEL}/hack/bin" -CHARTS_DIR="${TOP_LEVEL}/.local/charts" - -export LIB_DIR TOP_LEVEL BIN_DIR CHARTS_DIR diff --git a/hack/lib/helm-overrides.sh b/hack/lib/helm-overrides.sh deleted file mode 100755 index 63498978315..00000000000 --- a/hack/lib/helm-overrides.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -# Helm (v3) writes into XDG folders only these days. They don't honor HELM_ vars -# anymore. -# Derive a helm-specific folder inside the wire-server/.local to avoid polluting -# ~. - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TOP_LEVEL="$DIR/../.." -LOCAL_HELM_FOLDER="$TOP_LEVEL/.local/helm" - -[[ -e $LOCAL_HELM_FOLDER ]] || mkdir -p "$LOCAL_HELM_FOLDER" -export XDG_CACHE_HOME=${LOCAL_HELM_FOLDER}/cache -export XDG_CONFIG_HOME=${LOCAL_HELM_FOLDER}/config -export XDG_DATA_HOME=${LOCAL_HELM_FOLDER}/data diff --git a/hack/lib/helmfile-env-vars.sh b/hack/lib/helmfile-env-vars.sh deleted file mode 100755 index a12d40c32fc..00000000000 --- a/hack/lib/helmfile-env-vars.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -export NAMESPACE=${NAMESPACE:-test-integration} -# Available $HELMFILE_ENV profiles: default, default-ssl, kind, kind-ssl -export HELMFILE_ENV=${HELMFILE_ENV:-default} -export HELM_PARALLELISM=${HELM_PARALLELISM:-1} - -export NAMESPACE_1="$NAMESPACE" -export FEDERATION_DOMAIN_BASE_1="$NAMESPACE_1.svc.cluster.local" -export FEDERATION_DOMAIN_1="federation-test-helper.$FEDERATION_DOMAIN_BASE_1" - -export NAMESPACE_2="$NAMESPACE-fed2" -export FEDERATION_DOMAIN_BASE_2="$NAMESPACE_2.svc.cluster.local" -export FEDERATION_DOMAIN_2="federation-test-helper.$FEDERATION_DOMAIN_BASE_2" diff --git a/hack/lib/kube-metadata.sh b/hack/lib/kube-metadata.sh deleted file mode 100755 index 0cd482d0708..00000000000 --- a/hack/lib/kube-metadata.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - - -KUBERNETES_VERSION_MAJOR="$(kubectl version -o json | jq -r .serverVersion.major)" -KUBERNETES_VERSION_MINOR="$(kubectl version -o json | jq -r .serverVersion.minor)" -KUBERNETES_VERSION_MINOR="${KUBERNETES_VERSION_MINOR//[!0-9]/}" # some clusters report minor versions as a string like '27+'. Strip any non-digit characters. - -export KUBERNETES_VERSION_MAJOR KUBERNETES_VERSION_MINOR diff --git a/nix/wire-server.nix b/nix/wire-server.nix index aa2e0ef79a3..bf1593940f4 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -315,7 +315,6 @@ let openssl nix-output-monitor which - awscli2 ]; images = localMods@{ enableOptimization, enableDocs, enableTests }: From 47681a31394ce13fa2535ed06fe91063991538c9 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 17 Oct 2024 10:43:29 +0200 Subject: [PATCH 113/136] [WPB-11368] Test for export team member CSV (#4292) * integration test * Check more fields * Add assertions * Refactor createTeamMember * Test sso_id field * Remove debug output * Suppress warning * Remove old CSV export test * Add CHANGELOG entry * add comment Co-authored-by: Matthias Fischmann * Regenerate nix packages --------- Co-authored-by: Paolo Capriotti Co-authored-by: Matthias Fischmann --- changelog.d/5-internal/test-csv-export | 1 + integration/test/API/BrigInternal.hs | 26 ++++++ integration/test/API/Galley.hs | 6 ++ integration/test/SetupHelpers.hs | 35 +++++--- integration/test/Test/Conversation.hs | 2 +- integration/test/Test/ExternalPartner.hs | 12 +-- integration/test/Test/MLS/Services.hs | 3 +- integration/test/Test/Search.hs | 7 +- integration/test/Test/TeamSettings.hs | 4 +- integration/test/Test/Teams.hs | 81 ++++++++++++++--- services/galley/default.nix | 4 - services/galley/galley.cabal | 3 - services/galley/test/integration/API/Teams.hs | 89 +------------------ 13 files changed, 138 insertions(+), 135 deletions(-) create mode 100644 changelog.d/5-internal/test-csv-export diff --git a/changelog.d/5-internal/test-csv-export b/changelog.d/5-internal/test-csv-export new file mode 100644 index 00000000000..a8df725542b --- /dev/null +++ b/changelog.d/5-internal/test-csv-export @@ -0,0 +1 @@ +Move CSV export test to integration diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 7d1ca70230d..ff8b6f40f61 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -303,3 +303,29 @@ getPasswordResetCode :: (HasCallStack, MakesValue domain) => domain -> String -> getPasswordResetCode domain email = do req <- baseRequest domain Brig Unversioned "i/users/password-reset-code" submit "GET" $ req & addQueryParams [("email", email)] + +data PutSSOId = PutSSOId + { scimExternalId :: Maybe String, + subject :: Maybe String, + tenant :: Maybe String + } + +instance Default PutSSOId where + def = + PutSSOId + { scimExternalId = Nothing, + subject = Nothing, + tenant = Nothing + } + +putSSOId :: (HasCallStack, MakesValue user) => user -> PutSSOId -> App Response +putSSOId user args = do + uid <- objId user + req <- baseRequest user Brig Unversioned (joinHttpPath ["i", "users", uid, "sso-id"]) + submit "PUT" $ + req + & addJSONObject + [ "scim_external_id" .= args.scimExternalId, + "subject" .= args.subject, + "tenant" .= args.tenant + ] diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index e7bdbf486f9..6299fc97f8f 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -727,3 +727,9 @@ getTeamNotifications user mSince = baseRequest user Galley Versioned "teams/notifications" >>= \req -> submit "GET" $ addQueryParams [("since", since) | since <- maybeToList mSince] req + +-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/get_teams__tid__members_csv +getTeamMembersCsv :: (HasCallStack, MakesValue user) => user -> String -> App Response +getTeamMembersCsv user tid = do + req <- baseRequest user Galley Versioned (joinHttpPath ["teams", tid, "members", "csv"]) + submit "GET" req diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 43851fd04d7..4e19ae9b0a6 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -43,26 +43,35 @@ 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 tid + members <- for [2 .. memberCount] $ \_ -> createTeamMember owner def pure (owner, tid, members) -createTeamMember :: - (HasCallStack, MakesValue inviter) => - inviter -> - String -> - App Value -createTeamMember inviter tid = createTeamMemberWithRole inviter tid "member" +data CreateTeamMember = CreateTeamMember + { role :: String + } -createTeamMemberWithRole :: +instance Default CreateTeamMember where + def = CreateTeamMember {role = "member"} + +createTeamMember :: (HasCallStack, MakesValue inviter) => inviter -> - String -> - String -> + CreateTeamMember -> App Value -createTeamMemberWithRole inviter _ role = do +createTeamMember inviter args = do newUserEmail <- randomEmail - invitation <- postInvitation inviter (PostInvitation (Just newUserEmail) (Just role)) >>= getJSON 201 - invitationCode <- getInvitationCode inviter invitation >>= getJSON 200 >>= (%. "code") & asString + invitation <- + postInvitation + inviter + def + { email = Just newUserEmail, + role = Just args.role + } + >>= getJSON 201 + invitationCode <- + (getInvitationCode inviter invitation >>= getJSON 200) + %. "code" + & asString let body = AddUser { name = Just newUserEmail, diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 714a75d7254..e6ae83d1519 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -667,7 +667,7 @@ testDeleteTeamMemberLimitedEventFanout :: (HasCallStack) => App () testDeleteTeamMemberLimitedEventFanout = do -- Alex will get removed from the team (alice, team, [alex, alison]) <- createTeam OwnDomain 3 - ana <- createTeamMemberWithRole alice team "admin" + ana <- createTeamMember alice def {role = "admin"} [amy, bob] <- for [OwnDomain, OtherDomain] $ flip randomUser def forM_ [amy, bob] $ connectTwoUsers alice [aliceId, alexId, amyId, alisonId, anaId, bobId] <- do diff --git a/integration/test/Test/ExternalPartner.hs b/integration/test/Test/ExternalPartner.hs index ae6381f4187..01bdd629834 100644 --- a/integration/test/Test/ExternalPartner.hs +++ b/integration/test/Test/ExternalPartner.hs @@ -29,7 +29,7 @@ testExternalPartnerPermissions :: (HasCallStack) => App () testExternalPartnerPermissions = do (owner, tid, u1 : u2 : u3 : _) <- createTeam OwnDomain 4 - partner <- createTeamMemberWithRole owner tid "partner" + partner <- createTeamMember owner def {role = "partner"} -- a partner should not be able to create conversation with 2 additional users or more void $ postConversation partner (defProteus {team = Just tid, qualifiedUsers = [u1, u2]}) >>= getJSON 403 @@ -58,23 +58,23 @@ testExternalPartnerPermissions = do testExternalPartnerPermissionsMls :: (HasCallStack) => App () testExternalPartnerPermissionsMls = do -- external partners should not be able to create (MLS) conversations - (owner, tid, _) <- createTeam OwnDomain 2 - bobExt <- createTeamMemberWithRole owner tid "partner" + (owner, _, _) <- createTeam OwnDomain 2 + bobExt <- createTeamMember owner def {role = "partner"} bobExtClient <- createMLSClient def bobExt bindResponse (postConversation bobExtClient defMLS) $ \resp -> do resp.status `shouldMatchInt` 403 testExternalPartnerPermissionMlsOne2One :: (HasCallStack) => App () testExternalPartnerPermissionMlsOne2One = do - (owner, tid, alice : _) <- createTeam OwnDomain 2 - bobExternal <- createTeamMemberWithRole owner tid "partner" + (owner, _, alice : _) <- createTeam OwnDomain 2 + bobExternal <- createTeamMember owner def {role = "partner"} void $ getMLSOne2OneConversation alice bobExternal >>= getJSON 200 testExternalPartnerPermissionsConvName :: (HasCallStack) => App () testExternalPartnerPermissionsConvName = do (owner, tid, u1 : _) <- createTeam OwnDomain 2 - partner <- createTeamMemberWithRole owner tid "partner" + partner <- createTeamMember owner def {role = "partner"} conv <- postConversation partner (defProteus {team = Just tid, qualifiedUsers = [u1]}) >>= getJSON 201 diff --git a/integration/test/Test/MLS/Services.hs b/integration/test/Test/MLS/Services.hs index 1160fe7c423..153023d1a36 100644 --- a/integration/test/Test/MLS/Services.hs +++ b/integration/test/Test/MLS/Services.hs @@ -1,3 +1,4 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} module Test.MLS.Services where import API.Brig @@ -13,7 +14,7 @@ testWhitelistUpdatePermissions = do (owner, tid, []) <- createTeam OwnDomain 1 -- Create a team admin - admin <- createTeamMemberWithRole owner tid "admin" + admin <- createTeamMember owner def {role = "admin"} -- Create a service email <- randomEmail diff --git a/integration/test/Test/Search.hs b/integration/test/Test/Search.hs index af3f00d4e56..7ca6b48e4c6 100644 --- a/integration/test/Test/Search.hs +++ b/integration/test/Test/Search.hs @@ -1,3 +1,4 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} module Test.Search where import qualified API.Brig as BrigP @@ -19,9 +20,9 @@ testSearchContactForExternalUsers = do owner <- randomUser OwnDomain def {BrigI.team = True} tid <- owner %. "team" & asString - partner <- createTeamMemberWithRole owner tid "partner" - tm1 <- createTeamMember owner tid - tm2 <- createTeamMember owner tid + partner <- createTeamMember owner def {role = "partner"} + tm1 <- createTeamMember owner def + tm2 <- createTeamMember owner def -- a team member can search for contacts bindResponse (BrigP.searchContacts tm1 (owner %. "name") OwnDomain) $ \resp -> diff --git a/integration/test/Test/TeamSettings.hs b/integration/test/Test/TeamSettings.hs index 03a667cf78e..74e8eaa65a6 100644 --- a/integration/test/Test/TeamSettings.hs +++ b/integration/test/Test/TeamSettings.hs @@ -26,7 +26,7 @@ import Testlib.Prelude testTeamSettingsUpdate :: (HasCallStack) => App () testTeamSettingsUpdate = do (ownerA, tidA, [mem]) <- createTeam OwnDomain 2 - partner <- createTeamMemberWithRole ownerA tidA "partner" + partner <- createTeamMember ownerA def {role = "partner"} bindResponse (putAppLockSettings tidA ownerA def) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -45,7 +45,7 @@ testTeamSettingsUpdate = do testTeamPropertiesUpdate :: (HasCallStack) => App () testTeamPropertiesUpdate = do (ownerA, tidA, [mem]) <- createTeam OwnDomain 2 - partner <- createTeamMemberWithRole ownerA tidA "partner" + partner <- createTeamMember ownerA def {role = "partner"} bindResponse (putTeamProperties tidA ownerA def) $ \resp -> do resp.status `shouldMatchInt` 200 diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs index 082186d78df..623983abcba 100644 --- a/integration/test/Test/Teams.hs +++ b/integration/test/Test/Teams.hs @@ -18,13 +18,17 @@ module Test.Teams where import API.Brig -import API.BrigInternal (createUser, getInvitationCode, refreshIndex) +import qualified API.BrigInternal as I import API.Common -import API.Galley (getTeam, getTeamMembers, getTeamNotifications) +import API.Galley (getTeam, getTeamMembers, getTeamMembersCsv, getTeamNotifications) import API.GalleyInternal (setTeamFeatureStatus) import Control.Monad.Codensity (Codensity (runCodensity)) import Control.Monad.Extra (findM) import Control.Monad.Reader (asks) +import qualified Data.ByteString.Char8 as B8 +import qualified Data.Map as Map +import Data.Time.Clock +import Data.Time.Format import Notifications import SetupHelpers import Testlib.JSON @@ -52,13 +56,13 @@ testInvitePersonalUserToTeam = do ownerId <- owner %. "id" & asString setTeamFeatureStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" >>= assertSuccess - user <- createUser domain def >>= getJSON 201 + user <- I.createUser domain def >>= getJSON 201 uid <- user %. "id" >>= asString email <- user %. "email" >>= asString inv <- postInvitation owner (PostInvitation (Just email) Nothing) >>= getJSON 201 checkListInvitations owner tid email - code <- getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString inv %. "url" & asString >>= assertUrlContainsCode code acceptTeamInvitation user code Nothing >>= assertStatus 400 acceptTeamInvitation user code (Just "wrong-password") >>= assertStatus 403 @@ -105,7 +109,7 @@ testInvitePersonalUserToTeam = do ids <- for documents ((%. "id") >=> asString) ids `shouldContain` [ownerId] - refreshIndex domain + I.refreshIndex domain -- a team member can now search for the former personal user bindResponse (searchContacts tm (user %. "name") domain) $ \resp -> do resp.status `shouldMatchInt` 200 @@ -140,11 +144,11 @@ testInvitePersonalUserToLargeTeam = do teamSize <- readServiceConfig Galley %. "settings.maxFanoutSize" & asInt <&> (+ 1) (owner, tid, (alice : otherTeamMembers)) <- createTeam OwnDomain teamSize -- User to be invited to the team - knut <- createUser OwnDomain def >>= getJSON 201 + knut <- I.createUser OwnDomain def >>= getJSON 201 -- Non team friends of knut - dawn <- createUser OwnDomain def >>= getJSON 201 - eli <- createUser OtherDomain def >>= getJSON 201 + dawn <- I.createUser OwnDomain def >>= getJSON 201 + eli <- I.createUser OtherDomain def >>= getJSON 201 -- knut is also friends with alice, but not any other team members. traverse_ (connectTwoUsers knut) [alice, dawn, eli] @@ -159,7 +163,7 @@ testInvitePersonalUserToLargeTeam = do knutEmail <- knut %. "email" >>= asString inv <- postInvitation owner (PostInvitation (Just knutEmail) Nothing) >>= getJSON 201 - code <- getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString withWebSockets [owner, alice, dawn, eli, head otherTeamMembers] $ \[wsOwner, wsAlice, wsDawn, wsEli, wsOther] -> do acceptTeamInvitation knut code (Just defPassword) >>= assertSuccess @@ -204,16 +208,16 @@ testInvitePersonalUserToTeamMultipleInvitations :: (HasCallStack) => App () testInvitePersonalUserToTeamMultipleInvitations = do (owner, tid, _) <- createTeam OwnDomain 0 (owner2, _, _) <- createTeam OwnDomain 0 - user <- createUser OwnDomain def >>= getJSON 201 + user <- I.createUser OwnDomain def >>= getJSON 201 email <- user %. "email" >>= asString inv <- postInvitation owner (PostInvitation (Just email) Nothing) >>= getJSON 201 inv2 <- postInvitation owner2 (PostInvitation (Just email) Nothing) >>= getJSON 201 - code <- getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString acceptTeamInvitation user code (Just defPassword) >>= assertSuccess bindResponse (getSelf user) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "team" `shouldMatch` tid - code2 <- getInvitationCode owner2 inv2 >>= getJSON 200 >>= (%. "code") & asString + code2 <- I.getInvitationCode owner2 inv2 >>= getJSON 200 >>= (%. "code") & asString bindResponse (acceptTeamInvitation user code2 (Just defPassword)) $ \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "cannot-join-multiple-teams" @@ -227,10 +231,10 @@ testInvitationTypesAreDistinct = do -- We are only testing one direction because the other is not possible -- because the non-existing user cannot have a valid session (owner, _, _) <- createTeam OwnDomain 0 - user <- createUser OwnDomain def >>= getJSON 201 + user <- I.createUser OwnDomain def >>= getJSON 201 email <- user %. "email" >>= asString inv <- postInvitation owner (PostInvitation (Just email) Nothing) >>= getJSON 201 - code <- getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString + code <- I.getInvitationCode owner inv >>= getJSON 200 >>= (%. "code") & asString let body = AddUser { name = Just email, @@ -276,3 +280,52 @@ testUpgradePersonalToTeamAlreadyInATeam = do bindResponse (upgradePersonalToTeam alice "wonderland") $ \resp -> do resp.status `shouldMatchInt` 403 resp.json %. "label" `shouldMatch` "user-already-in-a-team" + +-- for additional tests of the CSV download particularly with SCIM users, please refer to 'Test.Spar.Scim.UserSpec' +testTeamMemberCsvExport :: (HasCallStack) => App () +testTeamMemberCsvExport = do + (owner, tid, members) <- createTeam OwnDomain 10 + let numClients = [0, 1, 2] <> repeat 0 + modifiedMembers <- for (zip numClients (owner : members)) $ \(n, m) -> do + handle <- randomHandle + putHandle m handle >>= assertSuccess + replicateM_ n $ addClient m def + void $ I.putSSOId m def {I.scimExternalId = Just "foo"} >>= getBody 200 + setField "handle" handle m + >>= setField "role" (if m == owner then "owner" else "member") + >>= setField "num_clients" (show n) + + memberMap :: Map.Map String Value <- fmap Map.fromList $ for (modifiedMembers) $ \m -> do + uid <- m %. "id" & asString + pure (uid, m) + + bindResponse (getTeamMembersCsv owner tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + let rows = sort $ tail $ B8.lines $ resp.body + length rows `shouldMatchInt` 10 + for_ rows $ \row -> do + let cols = B8.split ',' row + let uid = read $ B8.unpack $ cols !! 11 + let mem = memberMap Map.! uid + + ownerId <- owner %. "id" & asString + let ownerMember = memberMap Map.! ownerId + + let parseField = unquote . read . B8.unpack . (cols !!) + + parseField 0 `shouldMatch` (mem %. "name") + parseField 1 `shouldMatch` (mem %. "handle") + parseField 2 `shouldMatch` (mem %. "email") + role <- mem %. "role" & asString + parseField 3 `shouldMatch` role + when (role /= "owner") $ do + now <- formatTime defaultTimeLocale "%Y-%m-%d" <$> liftIO getCurrentTime + take 10 (parseField 4) `shouldMatch` now + parseField 5 `shouldMatch` (ownerMember %. "handle") + parseField 7 `shouldMatch` "wire" + parseField 9 `shouldMatch` "foo" + parseField 12 `shouldMatch` (mem %. "num_clients") + where + unquote :: String -> String + unquote ('\'' : x) = x + unquote x = x diff --git a/services/galley/default.nix b/services/galley/default.nix index 337edc485eb..446b4c9450e 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -116,7 +116,6 @@ , utf8-string , uuid , uuid-types -, vector , wai , wai-extra , wai-middleware-gunzip @@ -237,7 +236,6 @@ mkDerivation { bytestring-conversion call-stack cassandra-util - cassava cereal conduit containers @@ -276,7 +274,6 @@ mkDerivation { quickcheck-instances random retry - saml2-web-sso servant-client servant-client-core servant-server @@ -302,7 +299,6 @@ mkDerivation { unliftio unordered-containers uuid - vector wai wai-utilities warp diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index d0f0d567a5e..ae6bdfc65a4 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -483,7 +483,6 @@ executable galley-integration , bytestring-conversion , call-stack , cassandra-util - , cassava , cereal , containers , cookie @@ -521,7 +520,6 @@ executable galley-integration , quickcheck-instances , random , retry - , saml2-web-sso >=0.20 , servant-client , servant-client-core , servant-server @@ -547,7 +545,6 @@ executable galley-integration , unliftio , unordered-containers , uuid - , vector , wai , wai-utilities , warp diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index ceb2dbb7f51..cc49154eddb 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -31,13 +31,11 @@ import API.Util qualified as Util import API.Util.TeamFeature qualified as Util import Bilge hiding (head, timeout) import Bilge.Assert -import Control.Arrow ((>>>)) import Control.Lens hiding ((#), (.=)) import Control.Monad.Catch import Data.Aeson hiding (json) import Data.ByteString.Conversion import Data.Code qualified as Code -import Data.Csv (FromNamedRecord (..), decodeByName) import Data.Currency qualified as Currency import Data.Default import Data.Id @@ -46,8 +44,7 @@ import Data.LegalHold qualified as LH import Data.List.NonEmpty (NonEmpty ((:|))) import Data.List1 hiding (head) import Data.List1 qualified as List1 -import Data.Map qualified as Map -import Data.Misc (HttpsUrl, PlainTextPassword6, mkHttpsUrl, plainTextPassword6) +import Data.Misc import Data.Qualified import Data.Range import Data.Set qualified as Set @@ -56,7 +53,6 @@ import Data.Text.Ascii (AsciiChars (validate)) import Data.UUID qualified as UUID import Data.UUID.Util qualified as UUID import Data.UUID.V1 qualified as UUID -import Data.Vector qualified as V import Galley.Env qualified as Galley import Galley.Options (featureFlags, maxConvSize, maxFanoutSize, settings) import Galley.Types.Conversations.Roles @@ -65,7 +61,6 @@ import Imports import Network.HTTP.Types.Status (status403) import Network.Wai.Utilities.Error qualified as Error import Network.Wai.Utilities.Error qualified as Wai -import SAML2.WebSSO.Types qualified as SAML import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import Test.Tasty.Cannon qualified as WS @@ -82,19 +77,15 @@ import Wire.API.Internal.Notification hiding (target) import Wire.API.Routes.Internal.Galley.TeamsIntra as TeamsIntra import Wire.API.Routes.Version import Wire.API.Team -import Wire.API.Team.Export (TeamExportUser (..)) import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Member qualified as Member -import Wire.API.Team.Member qualified as TM import Wire.API.Team.Member qualified as Teams import Wire.API.Team.Permission as P import Wire.API.Team.Role import Wire.API.Team.SearchVisibility import Wire.API.User qualified as Public import Wire.API.User qualified as U -import Wire.API.User.Client qualified as C -import Wire.API.User.Client.Prekey qualified as PC tests :: IO TestSetup -> TestTree tests s = @@ -104,11 +95,6 @@ tests s = test s "create binding team with currency" testCreateBindingTeamWithCurrency, testGroup "List Team Members" $ [ test s "a member should be able to list their team" testListTeamMembersDefaultLimit, - let numMembers = 5 - in test - s - ("admins should be able to get a csv stream with their team (" <> show numMembers <> " members)") - (testListTeamMembersCsv numMembers), test s "the list should be limited to the number requested (hard truncation is not tested here)" testListTeamMembersTruncated, test s "pagination" testListTeamMembersPagination ], @@ -232,79 +218,6 @@ testListTeamMembersDefaultLimit = do "member list indicates that there are no more members" (listFromServer ^. teamMemberListType == ListComplete) --- | for ad-hoc load-testing, set @numMembers@ to, say, 10k and see what --- happens. but please don't give that number to our ci! :) --- for additional tests of the CSV download particularly with SCIM users, please refer to 'Test.Spar.Scim.UserSpec' -testListTeamMembersCsv :: (HasCallStack) => Int -> TestM () -testListTeamMembersCsv numMembers = do - let teamSize = numMembers + 1 - - (owner, tid, mbs) <- Util.createBindingTeamWithNMembersWithHandles True numMembers - let numClientMappings = Map.fromList $ (owner : mbs) `zip` (cycle [1, 2, 3] :: [Int]) - addClients numClientMappings - resp <- Util.getTeamMembersCsv owner tid - let rbody = fromMaybe (error "no body") . responseBody $ resp - usersInCsv <- either (error "could not decode csv") pure (decodeCSV @TeamExportUser rbody) - liftIO $ do - assertEqual "total number of team members" teamSize (length usersInCsv) - assertEqual "owners in team" 1 (countOn tExportRole (Just RoleOwner) usersInCsv) - assertEqual "members in team" numMembers (countOn tExportRole (Just RoleMember) usersInCsv) - - do - let someUsersInCsv = take 50 usersInCsv - someHandles = tExportHandle <$> someUsersInCsv - users <- Util.getUsersByHandle (catMaybes someHandles) - mbrs <- view teamMembers <$> Util.bulkGetTeamMembers owner tid (U.userId <$> users) - - let check :: (Eq a) => String -> (TeamExportUser -> Maybe a) -> UserId -> Maybe a -> IO () - check msg getTeamExportUserAttr uid userAttr = do - assertBool msg (isJust userAttr) - assertEqual (msg <> ": " <> show uid) 1 (countOn getTeamExportUserAttr userAttr usersInCsv) - - liftIO . forM_ (zip users mbrs) $ \(user, mbr) -> do - assertEqual "user/member id match" (U.userId user) (mbr ^. TM.userId) - check "tExportDisplayName" (Just . tExportDisplayName) (U.userId user) (Just $ U.userDisplayName user) - check "tExportEmail" tExportEmail (U.userId user) (U.userEmail user) - - liftIO . forM_ (zip3 someUsersInCsv users mbrs) $ \(export, user, mbr) -> do - -- FUTUREWORK: there are a lot of cases we don't cover here (manual invitation, saml, other roles, ...). - assertEqual ("tExportDisplayName: " <> show (U.userId user)) (U.userDisplayName user) (tExportDisplayName export) - assertEqual ("tExportHandle: " <> show (U.userId user)) (U.userHandle user) (tExportHandle export) - assertEqual ("tExportEmail: " <> show (U.userId user)) (U.userEmail user) (tExportEmail export) - assertEqual ("tExportRole: " <> show (U.userId user)) (permissionsRole $ view permissions mbr) (tExportRole export) - assertEqual ("tExportCreatedOn: " <> show (U.userId user)) (snd <$> view invitation mbr) (tExportCreatedOn export) - assertEqual ("tExportInvitedBy: " <> show (U.userId user)) Nothing (tExportInvitedBy export) - assertEqual ("tExportIdpIssuer: " <> show (U.userId user)) (userToIdPIssuer user) (tExportIdpIssuer export) - assertEqual ("tExportManagedBy: " <> show (U.userId user)) (U.userManagedBy user) (tExportManagedBy export) - assertEqual ("tExportUserId: " <> show (U.userId user)) (U.userId user) (tExportUserId export) - assertEqual "tExportNumDevices: " (Map.findWithDefault (-1) (U.userId user) numClientMappings) (tExportNumDevices export) - where - userToIdPIssuer :: (HasCallStack) => U.User -> Maybe HttpsUrl - userToIdPIssuer usr = case (U.userIdentity >=> U.ssoIdentity) usr of - Just (U.UserSSOId (SAML.UserRef (SAML.Issuer issuer) _)) -> either (const $ error "shouldn't happen") Just $ mkHttpsUrl issuer - Just _ -> Nothing - Nothing -> Nothing - - decodeCSV :: (FromNamedRecord a) => LByteString -> Either String [a] - decodeCSV bstr = decodeByName bstr <&> (snd >>> V.toList) - - countOn :: (Eq b) => (a -> b) -> b -> [a] -> Int - countOn prop val xs = sum $ fmap (bool 0 1 . (== val) . prop) xs - - addClients :: Map.Map UserId Int -> TestM () - addClients xs = forM_ (Map.toList xs) addClientForUser - - addClientForUser :: (UserId, Int) -> TestM () - addClientForUser (uid, n) = forM_ [0 .. (n - 1)] (addClient uid) - - addClient :: UserId -> Int -> TestM () - addClient uid i = do - brig <- viewBrig - post (brig . paths ["i", "clients", toByteString' uid] . contentJson . json (newClient (someLastPrekeys !! i)) . queryItem "skip_reauth" "true") !!! const 201 === statusCode - - newClient :: PC.LastPrekey -> C.NewClient - newClient lpk = C.newClient C.PermanentClientType lpk - testListTeamMembersPagination :: TestM () testListTeamMembersPagination = do (owner, tid, _) <- Util.createBindingTeamWithNMembers 18 From 0f192f83b94c578b57176d415ae9551a7a8f7bef Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 17 Oct 2024 17:19:56 +0200 Subject: [PATCH 114/136] Enable manual usage of locally running wire-server (#4176) * dockerephemeral: Use inbucket for SMTP * nginz/local-conf: Update list of endpoints * run-services: Add config tuned for manual usage and an option to turn it on * Makefile: Add target to run services tuned for manual usage * Docs * changelog Co-authored-by: Sven Tennie --- Makefile | 5 +- changelog.d/5-internal/fix-nginx-paths | 1 + changelog.d/5-internal/inbucket | 1 + changelog.d/5-internal/make-crm | 1 + deploy/dockerephemeral/docker-compose.yaml | 6 +- docs/src/developer/developer/building.md | 23 +++++ hack/bin/cabal-run-integration.sh | 6 +- integration/test/Testlib/JSON.hs | 17 ++++ integration/test/Testlib/RunServices.hs | 69 ++++++++++++-- .../integration-test/conf/nginz/nginx.conf | 93 ++++++++++--------- 10 files changed, 164 insertions(+), 58 deletions(-) create mode 100644 changelog.d/5-internal/fix-nginx-paths create mode 100644 changelog.d/5-internal/inbucket create mode 100644 changelog.d/5-internal/make-crm diff --git a/Makefile b/Makefile index 1413ff10e82..faee19b3dff 100644 --- a/Makefile +++ b/Makefile @@ -126,6 +126,9 @@ ci: cr: c db-migrate ./dist/run-services +crm: c db-migrate + ./dist/run-services -m + # Run integration from new test suite # Usage: make devtest # Usage: TEST_INCLUDE=test1,test2 make devtest @@ -134,7 +137,7 @@ devtest: ghcid --command 'cabal repl integration' --test='Testlib.Run.mainI []' .PHONY: sanitize-pr -sanitize-pr: +sanitize-pr: make lint-all-shallow make git-add-cassandra-schema @git diff-files --quiet -- || ( echo "There are unstaged changes, please take a look, consider committing them, and try again."; exit 1 ) diff --git a/changelog.d/5-internal/fix-nginx-paths b/changelog.d/5-internal/fix-nginx-paths new file mode 100644 index 00000000000..0d7bd115c65 --- /dev/null +++ b/changelog.d/5-internal/fix-nginx-paths @@ -0,0 +1 @@ +nginz/local-conf: Update list of endpoints \ No newline at end of file diff --git a/changelog.d/5-internal/inbucket b/changelog.d/5-internal/inbucket new file mode 100644 index 00000000000..12334d3b1d6 --- /dev/null +++ b/changelog.d/5-internal/inbucket @@ -0,0 +1 @@ +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 new file mode 100644 index 00000000000..eb4df600ece --- /dev/null +++ b/changelog.d/5-internal/make-crm @@ -0,0 +1 @@ +Makefile: Add target `crm` to run services tuned for manual usage \ No newline at end of file diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index debbdb32fa4..13061660d8c 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -58,9 +58,11 @@ services: basic_smtp: # needed for demo setup container_name: demo_wire_smtp - image: ixdotai/smtp:v0.5.2 + image: inbucket/inbucket:latest ports: - - 127.0.0.1:2500:25 + - 127.0.0.1:2500:2500 + - 127.0.0.1:1100:1100 + - 127.0.0.1:9000:9000 networks: - demo_wire diff --git a/docs/src/developer/developer/building.md b/docs/src/developer/developer/building.md index 2096fe9d93f..223026e8ca8 100644 --- a/docs/src/developer/developer/building.md +++ b/docs/src/developer/developer/building.md @@ -218,3 +218,26 @@ After all containers are up you can use these Makefile targets to run the tests ``` `TASTY_NUM_THREADS` can also be set to other values, it defaults to number of cores available. + +## How to run the webapp locally against locally running backend + +1. Clone the webapp from: https://github.com/wireapp/wire-webapp +2. Install these depedencies needed for the webapp: + 1. nodejs + 2. yarn + 3. mkcert +3. Copy `.env.localhost` to `.env` and uncomment the local section +4. Run the webapp using: + ```bash + yarn + yarn start + ``` +4. From wire-server repo start the dependencies using: + ```bash + ./deploy/dockerephemeral/run.sh + ``` +5. From wire-server repo start the backend using: + ```bash + make crm + ``` +6. Go to http://localhost:8081 in the browser. diff --git a/hack/bin/cabal-run-integration.sh b/hack/bin/cabal-run-integration.sh index 66daccfb538..582b2de874e 100755 --- a/hack/bin/cabal-run-integration.sh +++ b/hack/bin/cabal-run-integration.sh @@ -49,13 +49,15 @@ run_integration_tests() { then cd "$TOP_LEVEL" "$TOP_LEVEL/dist/run-services" \ - "$TOP_LEVEL/dist/integration" \ - "${@:2}" + -- \ + "$TOP_LEVEL/dist/integration" \ + "${@:2}" else service_dir="$TOP_LEVEL/services/$package" cd "$service_dir" "$TOP_LEVEL/dist/run-services" \ + -- \ "$TOP_LEVEL/dist/$package-integration" \ -s "$service_dir/$package.integration.yaml" \ -i "$TOP_LEVEL/services/integration.yaml" \ diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index 31eaf24ce14..96ee6da2492 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -262,6 +262,23 @@ setField :: setField selector v x = do modifyField @a @Value selector (\_ -> pure (toJSON v)) x +-- | Merges fields if the old and new are both Objects or Arrays. Otherwise new +-- field overwrites the old completely +mergeField :: forall a b. (HasCallStack, MakesValue a, ToJSON b) => String -> b -> a -> App Value +mergeField selector v x = do + modifyField @a @Value + selector + ( \case + Just (Object old) -> case toJSON v of + (Object new) -> pure $ Object (new <> old) + nonObjectNew -> pure nonObjectNew + Just (Array old) -> case toJSON v of + (Array new) -> pure $ Array (old <> new) + nonArrayNew -> pure nonArrayNew + _ -> pure (toJSON v) + ) + x + member :: (HasCallStack, MakesValue a) => String -> a -> App Bool member k x = KM.member (KM.fromString k) <$> (make x >>= asObject) diff --git a/integration/test/Testlib/RunServices.hs b/integration/test/Testlib/RunServices.hs index c2ee022185a..a88686b2979 100644 --- a/integration/test/Testlib/RunServices.hs +++ b/integration/test/Testlib/RunServices.hs @@ -1,10 +1,10 @@ -module Testlib.RunServices where +module Testlib.RunServices (main) where import Control.Concurrent import Control.Monad.Codensity +import Options.Applicative import System.Directory -import System.Environment (getArgs) -import System.Exit (exitWith) +import System.Exit import System.FilePath import System.Posix (getWorkingDirectory) import System.Process @@ -31,23 +31,43 @@ findProjectRoot path = do Nothing -> pure Nothing Just p -> findProjectRoot p +data Opts = Opts + { withManualTestingOverrides :: Bool, + runSubprocess :: [String] + } + deriving (Show) + +optsParser :: Parser Opts +optsParser = + Opts + <$> switch + ( long "with-manual-testing-overrides" + <> short 'm' + <> help "Run services with settings tuned for manual app usage (not recommended for running integration tests)" + ) + <*> many + ( strArgument + ( metavar "COMMAND_WITH_ARGS" + <> help "When specified, the command will be run after services have started and service will be killed after the command exits" + ) + ) + main :: IO () main = do cwd <- getWorkingDirectory mbProjectRoot <- findProjectRoot cwd + opts <- execParser (info (optsParser <**> helper) fullDesc) cfg <- case mbProjectRoot of Nothing -> error "Could not find project root. Please make sure you call run-services from somewhere in wire-server." Just projectRoot -> pure $ joinPath [projectRoot, "services/integration.yaml"] - args <- getArgs - - let run = case args of + let run = case opts.runSubprocess of [] -> do putStrLn "services started" - forever (threadDelay 1000000000) + forever (threadDelay maxBound) _ -> do - let cp = proc "sh" (["-c", "exec \"$@\"", "--"] <> args) + let cp = proc "sh" (["-c", "exec \"$@\"", "--"] <> opts.runSubprocess) (_, _, _, ph) <- createProcess cp exitWith =<< waitForProcess ph @@ -57,6 +77,37 @@ main = do $ do _modifyEnv <- traverseConcurrentlyCodensity - (\r -> void $ startDynamicBackend r mempty) + ( \r -> + void + $ if opts.withManualTestingOverrides + then startDynamicBackend r manualTestingOverrides + else startDynamicBackend r mempty + ) [backendA, backendB] liftIO run + +manualTestingOverrides :: ServiceOverrides +manualTestingOverrides = + let smtpEndpoint = object ["host" .= "localhost", "port" .= (2500 :: Int)] + authSettings = + object + [ "userTokenTimeout" .= (4838400 :: Int), + "sessionTokenTimeout" .= (86400 :: Int), + "accessTokenTimeout" .= (900 :: Int), + "providerTokenTimeout" .= (900 :: Int), + "legalHoldUserTokenTimeout" .= (4838400 :: Int), + "legalHoldAccessTokenTimeout" .= (900 :: Int) + ] + in def + { brigCfg = + mergeField "emailSMS.email.smtpEndpoint" smtpEndpoint + >=> setField "emailSMS.email.smtpConnType" "plain" + >=> removeField "emailSMS.email.sesQueue" + >=> removeField "emailSMS.email.sesEndpoint" + >=> mergeField "zauth.authSettings" authSettings + >=> setField @_ @Int "optSettings.setActivationTimeout" 3600 + >=> setField @_ @Int "optSettings.setVerificationTimeout" 3600 + >=> setField @_ @Int "optSettings.setTeamInvitationTimeout" 3600 + >=> setField @_ @Int "optSettings.setUserCookieRenewAge" 1209600 + >=> removeField "optSettings.setSuspendInactiveUsers" + } diff --git a/services/nginz/integration-test/conf/nginz/nginx.conf b/services/nginz/integration-test/conf/nginz/nginx.conf index 95b560f7b1d..c887411e18f 100644 --- a/services/nginz/integration-test/conf/nginz/nginx.conf +++ b/services/nginz/integration-test/conf/nginz/nginx.conf @@ -155,6 +155,11 @@ http { # FUTUREWORK(federation): are any other settings # (e.g. timeouts, body size, buffers, headers,...) # useful/recommended/important-for-security?) + } + + location /api-version { + include common_response_no_zauth.conf; + proxy_pass http://brig; } # Brig Endpoints @@ -201,12 +206,12 @@ http { proxy_pass http://brig; } - location /activate { + location ~* ^(/v[0-9]+)?/activate { include common_response_no_zauth.conf; proxy_pass http://brig; } - location /login { + location ~* ^(/v[0-9]+)?/login { include common_response_no_zauth.conf; proxy_pass http://brig; } @@ -221,50 +226,50 @@ http { proxy_pass http://brig; } - location /verification-code/send { + location ~* ^(/v[0-9]+)?/verification-code/send { include common_response_no_zauth.conf; proxy_pass http://brig; } ## brig authenticated endpoints - location ~* ^(/v[0-9]+)?/self$ { + location ~* ^(/v[0-9]+)?/self { include common_response_with_zauth.conf; oauth_scope self; proxy_pass http://brig; } - location /users { + location ~* ^(/v[0-9]+)?/users { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /list-users { + location ~* ^(/v[0-9]+)?/list-users { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /search { + location ~* ^(/v[0-9]+)?/search { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /list-connections { + location ~* ^(/v[0-9]+)?/list-connections { include common_response_with_zauth.conf; proxy_pass http://brig; } - location ~* ^/teams/([^/]+)/search$ { + location ~* ^(/v[0-9]+)?/teams/([^/]+)/search$ { include common_response_with_zauth.conf; proxy_pass http://brig; } - location ~* /teams/([^/]+)/services { + location ~* ^(/v[0-9]+)?/teams/([^/]+)/services { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /connections { + location ~* ^(/v[0-9]+)?/connections { include common_response_with_zauth.conf; proxy_pass http://brig; } @@ -279,17 +284,17 @@ http { proxy_pass http://brig; } - location /properties { + location ~* ^(/v[0-9]+)?/properties { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /calls/config { + location ~* ^(/v[0-9]+)?/calls/config { include common_response_with_zauth.conf; proxy_pass http://brig; } - location ~* ^/teams/([^/]*)/size$ { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/size$ { include common_response_with_zauth.conf; proxy_pass http://brig; } @@ -304,39 +309,39 @@ http { proxy_pass http://brig; } - location ~* ^/oauth/clients/([^/]*)$ { + location ~* ^(/v[0-9]+)?/oauth/clients/([^/]*)$ { include common_response_with_zauth.conf; proxy_pass http://brig; } - location ~* ^/oauth/authorization/codes$ { + location ~* ^(/v[0-9]+)?/oauth/authorization/codes$ { include common_response_with_zauth.conf; proxy_pass http://brig; } - location /oauth/token { + location ~* ^(/v[0-9]+)?/oauth/token { include common_response_no_zauth.conf; proxy_pass http://brig; } - location /oauth/revoke { + location ~* ^(/v[0-9]+)?/oauth/revoke { include common_response_no_zauth.conf; proxy_pass http://brig; } - location /oauth/applications { + location ~* ^(/v[0-9]+)?/oauth/applications { include common_response_with_zauth.conf; proxy_pass http://brig; } # Cargohold Endpoints - location /assets { + location ~* ^(/v[0-9]+)?/assets { include common_response_with_zauth.conf; proxy_pass http://cargohold; } - location /bot/assets { + location ~* ^(/v[0-9]+)?/bot/assets { include common_response_with_zauth.conf; proxy_pass http://cargohold; } @@ -370,62 +375,62 @@ http { proxy_pass http://galley; } - location ~* ^/conversations/([^/]*)/otr/messages { + location ~* ^(/v[0-9]+)?/conversations/([^/]*)/otr/messages { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/conversations/([^/]*)/([^/]*)/proteus/messages { + location ~* ^(/v[0-9]+)?/conversations/([^/]*)/([^/]*)/proteus/messages { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/conversations/([^/]*)/([^/]*)/protocol { + location ~* ^(/v[0-9]+)?/conversations/([^/]*)/([^/]*)/protocol { include common_response_with_zauth.conf; proxy_pass http://galley; } - location /broadcast { + location ~* ^(/v[0-9]+)?/broadcast { include common_response_with_zauth.conf; proxy_pass http://galley; } - location /bot/conversation { + location ~* ^(/v[0-9]+)?/bot/conversation { include common_response_with_zauth.conf; proxy_pass http://galley; } - location /bot/messages { + location ~* ^(/v[0-9]+)?/bot/messages { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams$ { + location ~* ^(/v[0-9]+)?/teams$ { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)$ { + location ~* ^(/v[0-9]+)?/teams/([^/]*)$ { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/members(.*) { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/members(.*) { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/conversations(.*) { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/conversations(.*) { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/features { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/features { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/features/([^/]*) { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/features/([^/]*) { include common_response_with_zauth.conf; proxy_pass http://galley; } @@ -441,22 +446,22 @@ http { proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/legalhold(.*) { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/legalhold(.*) { include common_response_with_zauth.conf; proxy_pass http://galley; } - location ~* ^/teams/([^/]*)/members/csv$ { + location ~* ^(/v[0-9]+)?/teams/([^/]*)/members/csv$ { include common_response_with_zauth.conf; proxy_pass http://galley; } - location /mls/welcome { + location ~* ^(/v[0-9]+)?/mls/welcome { include common_response_with_zauth.conf; proxy_pass http://galley; } - location /mls/messages { + location ~* ^(/v[0-9]+)?/mls/messages { include common_response_with_zauth.conf; proxy_pass http://galley; } @@ -473,31 +478,31 @@ http { # Gundeck Endpoints - location /push { + location ~* ^(/v[0-9]+)?/push { include common_response_with_zauth.conf; proxy_pass http://gundeck; } - location /presences { + location ~* ^(/v[0-9]+)?/presences { include common_response_with_zauth.conf; proxy_pass http://gundeck; } - location ~* ^(/v[0-9]+)?/notifications$ { + location ~* ^(/v[0-9]+)?/notifications { include common_response_with_zauth.conf; proxy_pass http://gundeck; } # Proxy Endpoints - location /proxy { + location ~* ^(/v[0-9]+)?/proxy { include common_response_with_zauth.conf; proxy_pass http://proxy; } # Cannon Endpoints - location /await { + location ~* ^(/v[0-9]+)?/await { include common_response_with_zauth.conf; proxy_pass http://cannon; @@ -508,12 +513,12 @@ http { # Spar Endpoints - location /sso { + location ~* ^(/v[0-9]+)?/sso { include common_response_no_zauth.conf; proxy_pass http://spar; } - location /identity-providers { + location ~* ^(/v[0-9]+)?/identity-providers { include common_response_with_zauth.conf; proxy_pass http://spar; } From 0290140d67c69158f72bbeb75148671e3dd47f47 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Fri, 18 Oct 2024 18:57:38 +0200 Subject: [PATCH 115/136] [WPB-1220] servantify proxy internal (#4296) * Servantify internal routing table for proxy. * Allow for combined wai-routing + servant metrics. * Always use defRequestId, not "N/A". --------- Co-authored-by: Sven Tennie --- .../WPB-1220-servantify-proxy-internal | 1 + libs/metrics-wai/default.nix | 2 + libs/metrics-wai/metrics-wai.cabal | 1 + .../src/Data/Metrics/Middleware/Prometheus.hs | 13 +++-- libs/metrics-wai/src/Data/Metrics/Servant.hs | 3 +- libs/metrics-wai/src/Data/Metrics/Types.hs | 1 + libs/types-common/src/Data/Id.hs | 4 ++ .../src/Network/Wai/Utilities/Request.hs | 3 +- .../src/Network/Wai/Utilities/Server.hs | 11 ++-- .../src/Wire/EmailSubsystem/Interpreter.hs | 4 +- .../NotificationSubsystem/InterpreterSpec.hs | 10 ++-- .../src/Wire/BackendNotificationPusher.hs | 6 +-- .../Wire/BackendNotificationPusherSpec.hs | 10 ++-- services/brig/src/Brig/App.hs | 4 +- services/cannon/src/Cannon/App.hs | 4 +- services/cannon/src/Cannon/Types.hs | 5 +- services/cannon/src/Cannon/WS.hs | 4 +- services/cargohold/src/CargoHold/App.hs | 5 +- services/federator/src/Federator/Run.hs | 2 +- .../integration/Test/Federator/IngressSpec.hs | 2 +- .../test/unit/Test/Federator/Client.hs | 6 +-- services/galley/src/Galley/App.hs | 2 +- services/gundeck/src/Gundeck/Env.hs | 3 +- services/proxy/default.nix | 4 ++ services/proxy/proxy.cabal | 4 +- .../src/Proxy/{API.hs => API/Internal.hs} | 30 ++++------- services/proxy/src/Proxy/API/Public.hs | 27 ++++++++-- services/proxy/src/Proxy/Env.hs | 4 +- services/proxy/src/Proxy/Proxy.hs | 9 ++-- services/proxy/src/Proxy/Run.hs | 54 ++++++++++++++++--- services/proxy/test/scripts/proxy-test.sh | 3 +- services/spar/src/Spar/Run.hs | 2 +- tools/stern/src/Stern/App.hs | 2 +- 33 files changed, 162 insertions(+), 83 deletions(-) create mode 100644 changelog.d/5-internal/WPB-1220-servantify-proxy-internal rename services/proxy/src/Proxy/{API.hs => API/Internal.hs} (55%) diff --git a/changelog.d/5-internal/WPB-1220-servantify-proxy-internal b/changelog.d/5-internal/WPB-1220-servantify-proxy-internal new file mode 100644 index 00000000000..f161136a346 --- /dev/null +++ b/changelog.d/5-internal/WPB-1220-servantify-proxy-internal @@ -0,0 +1 @@ +Servantify internal routing table for proxy. diff --git a/libs/metrics-wai/default.nix b/libs/metrics-wai/default.nix index eb3a260e929..8bb74088e5e 100644 --- a/libs/metrics-wai/default.nix +++ b/libs/metrics-wai/default.nix @@ -14,6 +14,7 @@ , servant , servant-multipart , text +, types-common , utf8-string , wai , wai-middleware-prometheus @@ -32,6 +33,7 @@ mkDerivation { servant servant-multipart text + types-common utf8-string wai wai-middleware-prometheus diff --git a/libs/metrics-wai/metrics-wai.cabal b/libs/metrics-wai/metrics-wai.cabal index ed848c893cb..1b6e5cfa03b 100644 --- a/libs/metrics-wai/metrics-wai.cabal +++ b/libs/metrics-wai/metrics-wai.cabal @@ -76,6 +76,7 @@ library , servant , servant-multipart , text >=0.11 + , types-common , utf8-string , wai >=3 , wai-middleware-prometheus diff --git a/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs b/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs index f1f7c1ca562..39b73e351e9 100644 --- a/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs +++ b/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs @@ -17,10 +17,12 @@ module Data.Metrics.Middleware.Prometheus ( waiPrometheusMiddleware, + waiPrometheusMiddlewarePaths, normalizeWaiRequestRoute, ) where +import Data.Id import Data.Metrics.Types (Paths, treeLookup) import Data.Metrics.WaiRoute (treeToPaths) import Data.Text.Encoding qualified as T @@ -33,12 +35,17 @@ import Network.Wai.Routing.Route (Routes, prepare) -- This middleware requires your servers 'Routes' because it does some normalization -- (e.g. removing params from calls) waiPrometheusMiddleware :: (Monad m) => Routes a m b -> Wai.Middleware -waiPrometheusMiddleware routes = +waiPrometheusMiddleware routes = waiPrometheusMiddlewarePaths $ treeToPaths $ prepare routes + +-- | Helper function that should only be needed as long as we have wai-routing code left in +-- proxy: run 'treeToPaths' on old routing tables and 'routeToPaths' on the servant ones, and +-- feed both to this function. +waiPrometheusMiddlewarePaths :: Paths -> Wai.Middleware +waiPrometheusMiddlewarePaths paths = Promth.prometheus conf . instrument (normalizeWaiRequestRoute paths) where -- See Note [Raw Response] instrument = Promth.instrumentHandlerValueWithFilter Promth.ignoreRawResponses - paths = treeToPaths $ prepare routes conf = Promth.def { Promth.prometheusEndPoint = ["i", "metrics"], @@ -57,4 +64,4 @@ normalizeWaiRequestRoute paths req = pathInfo -- Use the normalized path info if available; otherwise dump the raw path info for -- debugging purposes pathInfo :: Text - pathInfo = T.decodeUtf8 $ fromMaybe "N/A" mPathInfo + pathInfo = T.decodeUtf8 $ fromMaybe defRequestId mPathInfo diff --git a/libs/metrics-wai/src/Data/Metrics/Servant.hs b/libs/metrics-wai/src/Data/Metrics/Servant.hs index a66da6837a2..490ed13ded2 100644 --- a/libs/metrics-wai/src/Data/Metrics/Servant.hs +++ b/libs/metrics-wai/src/Data/Metrics/Servant.hs @@ -27,6 +27,7 @@ module Data.Metrics.Servant where import Data.ByteString.UTF8 qualified as UTF8 +import Data.Id import Data.Metrics.Types import Data.Metrics.Types qualified as Metrics import Data.Proxy @@ -49,7 +50,7 @@ servantPrometheusMiddleware _ = Promth.prometheus conf . instrument promthNormal promthNormalize req = pathInfo where mPathInfo = Metrics.treeLookup (routesToPaths @api) $ encodeUtf8 <$> Wai.pathInfo req - pathInfo = decodeUtf8With lenientDecode $ fromMaybe "N/A" mPathInfo + pathInfo = decodeUtf8With lenientDecode $ fromMaybe defRequestId mPathInfo -- See Note [Raw Response] instrument = Promth.instrumentHandlerValueWithFilter Promth.ignoreRawResponses diff --git a/libs/metrics-wai/src/Data/Metrics/Types.hs b/libs/metrics-wai/src/Data/Metrics/Types.hs index 0d1a70903d0..4d83874789b 100644 --- a/libs/metrics-wai/src/Data/Metrics/Types.hs +++ b/libs/metrics-wai/src/Data/Metrics/Types.hs @@ -41,6 +41,7 @@ newtype PathTemplate = PathTemplate Text -- (e.g. user id). newtype Paths = Paths (Forest PathSegment) deriving (Eq, Show) + deriving newtype (Semigroup) type PathSegment = Either ByteString ByteString diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index 3ef7152c913..0a1dbe22ad3 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -48,6 +48,7 @@ module Data.Id -- * Other IDs ConnId (..), RequestId (..), + defRequestId, BotId (..), NoId, OAuthClientId, @@ -418,6 +419,9 @@ newtype RequestId = RequestId ToBytes ) +defRequestId :: (IsString s) => s +defRequestId = "N/A" + instance ToSchema RequestId where schema = RequestId . encodeUtf8 diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs index 2450bfd7b47..484c1b34643 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Request.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wno-orphans #-} -- This file is part of the Wire Server implementation. @@ -56,7 +55,7 @@ lookupRequestId reqIdHeaderName = getRequestId :: HeaderName -> Request -> RequestId getRequestId reqIdHeaderName req = - RequestId $ fromMaybe "N/A" $ lookupRequestId reqIdHeaderName req + RequestId $ fromMaybe defRequestId $ lookupRequestId reqIdHeaderName req ---------------------------------------------------------------------------- -- Typed JSON 'Request' diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs index dd3306f4a65..20f8fc9b934 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs @@ -60,6 +60,7 @@ import Data.ByteString.Builder import Data.ByteString.Char8 qualified as C import Data.ByteString.Lazy qualified as LBS import Data.Domain (domainText) +import Data.Id import Data.Metrics.GC (spawnGCMetricsCollector) import Data.Streaming.Zlib (ZlibException (..)) import Data.Text.Encoding qualified as Text @@ -168,7 +169,7 @@ compile routes = Route.prepare (Route.renderer predicateError >> routes) r = reasonStr <$> reason e t = message e in case catMaybes [l, s, r] of - [] -> maybe "N/A" (LT.decodeUtf8With lenientDecode . LBS.fromStrict) t + [] -> maybe defRequestId (LT.decodeUtf8With lenientDecode . LBS.fromStrict) t bs -> LT.decodeUtf8With lenientDecode . toLazyByteString $ mconcat bs <> messageStr t labelStr [] = Nothing labelStr ls = @@ -311,7 +312,7 @@ heavyDebugLogging sanitizeReq lvl lgr reqIdHeaderName app = \req cont -> do logMostlyEverything req bdy resp = Log.debug lgr logMsg where logMsg = - field "request" (fromMaybe "N/A" $ lookupRequestId reqIdHeaderName req) + field "request" (fromMaybe defRequestId $ lookupRequestId reqIdHeaderName req) . field "request_details" (show req) . field "request_body" bdy . field "response_status" (show $ responseStatus resp) @@ -350,7 +351,7 @@ rethrow5xx getRequestId logger app req k = app req k' let logMsg = field "canoncalpath" (show $ pathInfo req) . field "rawpath" (rawPathInfo req) - . field "request" (fromMaybe "N/A" $ getRequestId req) + . field "request" (fromMaybe defRequestId $ getRequestId req) . msg (val "ResponseRaw - cannot collect metrics or log info on errors") Log.log logger Log.Debug logMsg k resp @@ -436,7 +437,7 @@ logError' g mr e = liftIO $ doLog g (logErrorMsgWithRequest mr e) logJSONResponse :: (MonadIO m) => Logger -> Maybe ByteString -> JSONResponse -> m () logJSONResponse g mReqId e = do - let r = fromMaybe "N/A" mReqId + let r = fromMaybe defRequestId mReqId liftIO $ doLog g $ field "request" r @@ -462,7 +463,7 @@ logErrorMsg (Wai.Error c l m md inner) = logErrorMsgWithRequest :: Maybe ByteString -> Wai.Error -> Msg -> Msg logErrorMsgWithRequest mr e = - field "request" (fromMaybe "N/A" mr) . logErrorMsg e + field "request" (fromMaybe defRequestId mr) . logErrorMsg e runHandlers :: SomeException -> [Handler IO a] -> IO a runHandlers e [] = throwIO e diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs index a78e26f3754..f3451750e47 100644 --- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs @@ -345,8 +345,8 @@ renderNewClientEmail email name locale Client {..} NewClientEmailTemplate {..} b html = renderHtmlWithBranding newClientEmailBodyHtml replace branding subj = renderTextWithBranding newClientEmailSubject replace branding replace "name" = fromName name - replace "label" = fromMaybe "N/A" clientLabel - replace "model" = fromMaybe "N/A" clientModel + replace "label" = fromMaybe defRequestId clientLabel + replace "model" = fromMaybe defRequestId clientModel replace "date" = formatDateTime "%A %e %B %Y, %H:%M - %Z" diff --git a/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs index 880a213d25b..9fbd3babe89 100644 --- a/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs @@ -1,9 +1,9 @@ module Wire.NotificationSubsystem.InterpreterSpec (spec) where -import Bilge (RequestId (..)) import Control.Concurrent.Async (async, wait) import Control.Exception (throwIO) import Data.Data (Proxy (Proxy)) +import Data.Id import Data.List.NonEmpty (NonEmpty ((:|)), fromList) import Data.List1 qualified as List1 import Data.Range (fromRange, toRange) @@ -37,7 +37,7 @@ spec = describe "NotificationSubsystem.Interpreter" do { fanoutLimit = toRange $ Proxy @30, chunkSize = 12, slowPushDelay = 0, - requestId = RequestId "N/A" + requestId = RequestId defRequestId } connId2 <- generate arbitrary @@ -98,7 +98,7 @@ spec = describe "NotificationSubsystem.Interpreter" do { fanoutLimit = toRange $ Proxy @30, chunkSize = 12, slowPushDelay = 0, - requestId = RequestId "N/A" + requestId = RequestId defRequestId } connId2 <- generate arbitrary @@ -153,7 +153,7 @@ spec = describe "NotificationSubsystem.Interpreter" do { fanoutLimit = toRange $ Proxy @30, chunkSize = 12, slowPushDelay = 1, - requestId = RequestId "N/A" + requestId = RequestId defRequestId } connId2 <- generate arbitrary @@ -211,7 +211,7 @@ spec = describe "NotificationSubsystem.Interpreter" do { fanoutLimit = toRange $ Proxy @30, chunkSize = 12, slowPushDelay = 1, - requestId = RequestId "N/A" + requestId = RequestId defRequestId } user1 <- generate arbitrary diff --git a/services/background-worker/src/Wire/BackendNotificationPusher.hs b/services/background-worker/src/Wire/BackendNotificationPusher.hs index 464c93e0cf0..6a6cf2f7f62 100644 --- a/services/background-worker/src/Wire/BackendNotificationPusher.hs +++ b/services/background-worker/src/Wire/BackendNotificationPusher.hs @@ -116,7 +116,7 @@ pushNotification runningFlag targetDomain (msg, envelope) = do ceHttp2Manager <- asks http2Manager let ceOriginDomain = notif.ownDomain ceTargetDomain = targetDomain - ceOriginRequestId = fromMaybe (RequestId "N/A") notif.requestId + ceOriginRequestId = fromMaybe (RequestId defRequestId) notif.requestId cveEnv = FederatorClientEnv {..} cveVersion = Just V0 -- V0 is assumed for non-versioned queue messages fcEnv = FederatorClientVersionedEnv {..} @@ -135,7 +135,7 @@ pushNotification runningFlag targetDomain (msg, envelope) = do ceFederator = federator, ceHttp2Manager = manager, ceOriginRequestId = - fromMaybe (RequestId "N/A") . (.requestId) . NE.head $ bundle.notifications + fromMaybe (RequestId defRequestId) . (.requestId) . NE.head $ bundle.notifications } remoteVersions :: Set Int <- liftIO @@ -166,7 +166,7 @@ pushNotification runningFlag targetDomain (msg, envelope) = do ceHttp2Manager <- asks http2Manager let ceOriginDomain = notif.ownDomain ceTargetDomain = targetDomain - ceOriginRequestId = fromMaybe (RequestId "N/A") notif.requestId + ceOriginRequestId = fromMaybe (RequestId defRequestId) notif.requestId cveEnv = FederatorClientEnv {..} fcEnv = FederatorClientVersionedEnv {..} sendNotificationIgnoringVersionMismatch fcEnv notif.targetComponent notif.path notif.body diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index 29906684cae..416a2653f82 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -67,7 +67,7 @@ spec = do path = "/on-user-deleted-connections", body = RawJson $ Aeson.encode notifContent, bodyVersions = Nothing, - requestId = Just $ RequestId "N/A" + requestId = Just $ RequestId defRequestId } envelope <- newMockEnvelope let msg = @@ -104,7 +104,7 @@ spec = do notifContent <- generate $ ClientRemovedRequest <$> arbitrary <*> arbitrary <*> arbitrary - let bundle = toBundle @'OnClientRemovedTag (RequestId "N/A") origDomain notifContent + let bundle = toBundle @'OnClientRemovedTag (RequestId defRequestId) origDomain notifContent envelope <- newMockEnvelope let msg = Q.newMsg @@ -148,8 +148,8 @@ spec = do } let update0 = conversationUpdateToV0 update let bundle = - toBundle (RequestId "N/A") origDomain update - <> toBundle (RequestId "N/A") origDomain update0 + toBundle (RequestId defRequestId) origDomain update + <> toBundle (RequestId defRequestId) origDomain update0 envelope <- newMockEnvelope let msg = Q.newMsg @@ -215,7 +215,7 @@ spec = do path = "/on-user-deleted-connections", body = RawJson $ Aeson.encode notifContent, bodyVersions = Nothing, - requestId = Just $ RequestId "N/A" + requestId = Just $ RequestId defRequestId } envelope <- newMockEnvelope let msg = diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 28735c7817b..eb7b06457e2 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -92,7 +92,6 @@ module Brig.App ) where -import Bilge (RequestId (..)) import Bilge qualified as RPC import Bilge.IO import Bilge.RPC (HasRequestId (..)) @@ -122,6 +121,7 @@ import Control.Monad.Trans.Resource import Data.ByteString.Conversion import Data.Credentials (Credentials (..)) import Data.Domain +import Data.Id import Data.Misc import Data.Qualified import Data.Text qualified as Text @@ -278,7 +278,7 @@ newEnv opts = do awsEnv = aws, -- used by `journalEvent` directly appLogger = lgr, internalEvents = (eventsQueue :: QueueEnv), - requestId = RequestId "N/A", + requestId = RequestId defRequestId, userTemplates = utp, providerTemplates = ptp, teamTemplates = ttp, diff --git a/services/cannon/src/Cannon/App.hs b/services/cannon/src/Cannon/App.hs index 842d38135a3..770bf0ff499 100644 --- a/services/cannon/src/Cannon/App.hs +++ b/services/cannon/src/Cannon/App.hs @@ -29,7 +29,7 @@ import Control.Monad.Catch import Data.Aeson hiding (Error, Key, (.=)) import Data.ByteString.Conversion import Data.ByteString.Lazy (toStrict) -import Data.Id (ClientId) +import Data.Id import Data.Text.Lazy qualified as Text import Data.Timeout import Imports hiding (threadDelay) @@ -155,7 +155,7 @@ rejectOnError :: PendingConnection -> HandshakeException -> IO a rejectOnError p x = do let f lb mg = toStrict . encode $ mkError status400 lb mg case x of - NotSupported -> rejectRequest p (f "protocol not supported" "N/A") + NotSupported -> rejectRequest p (f "protocol not supported" defRequestId) MalformedRequest _ m -> rejectRequest p (f "malformed-request" (Text.pack m)) OtherHandshakeException m -> rejectRequest p (f "other-error" (Text.pack m)) _ -> pure () diff --git a/services/cannon/src/Cannon/Types.hs b/services/cannon/src/Cannon/Types.hs index eec8d20ac4b..6fa37b78a65 100644 --- a/services/cannon/src/Cannon/Types.hs +++ b/services/cannon/src/Cannon/Types.hs @@ -33,7 +33,7 @@ module Cannon.Types ) where -import Bilge (Manager, RequestId (..)) +import Bilge (Manager) import Bilge.RPC (HasRequestId (..)) import Cannon.Dict (Dict) import Cannon.Options @@ -42,6 +42,7 @@ import Cannon.WS qualified as WS import Control.Concurrent.Async (mapConcurrently) import Control.Lens ((^.)) import Control.Monad.Catch +import Data.Id import Data.Text.Encoding import Imports import Prometheus @@ -100,7 +101,7 @@ mkEnv :: Clock -> Env mkEnv external o l d p g t = - Env o l d (RequestId "N/A") $ + Env o l d (RequestId defRequestId) $ WS.env external (o ^. cannon . port) (encodeUtf8 $ o ^. gundeck . host) (o ^. gundeck . port) l p d g t (o ^. drainOpts) runCannon :: Env -> Cannon a -> IO a diff --git a/services/cannon/src/Cannon/WS.hs b/services/cannon/src/Cannon/WS.hs index 0ad9820df96..ea106f4cf03 100644 --- a/services/cannon/src/Cannon/WS.hs +++ b/services/cannon/src/Cannon/WS.hs @@ -62,7 +62,7 @@ import Data.ByteString.Char8 (pack) import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as L import Data.Hashable -import Data.Id (ClientId, ConnId (..), UserId) +import Data.Id (ClientId, ConnId (..), UserId, defRequestId) import Data.List.Extra (chunksOf) import Data.Text.Encoding (decodeUtf8) import Data.Timeout (TimeoutUnit (..), (#)) @@ -192,7 +192,7 @@ env :: Clock -> DrainOpts -> Env -env leh lp gh gp = Env leh lp (host gh . port gp $ empty) (RequestId "N/A") +env leh lp gh gp = Env leh lp (host gh . port gp $ empty) (RequestId defRequestId) runWS :: (MonadIO m) => Env -> WS a -> m a runWS e m = liftIO $ runReaderT (_conn m) e diff --git a/services/cargohold/src/CargoHold/App.hs b/services/cargohold/src/CargoHold/App.hs index 1495bda34fe..5acb66a57ed 100644 --- a/services/cargohold/src/CargoHold/App.hs +++ b/services/cargohold/src/CargoHold/App.hs @@ -47,7 +47,7 @@ module CargoHold.App where import Amazonka (S3AddressingStyle (S3AddressingStylePath)) -import Bilge (Manager, MonadHttp, RequestId (..), newManager, withResponse) +import Bilge (Manager, MonadHttp, newManager, withResponse) import qualified Bilge import Bilge.RPC (HasRequestId (..)) import qualified CargoHold.AWS as AWS @@ -57,6 +57,7 @@ import Control.Error (ExceptT, exceptT) import Control.Exception (throw) import Control.Lens (lensField, lensRules, makeLensesWith, non, (.~), (?~), (^.)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) +import Data.Id import qualified Data.Map as Map import Data.Qualified import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) @@ -100,7 +101,7 @@ newEnv opts = do awsEnv <- initAws opts.aws logger httpMgr multiIngressAWS <- initMultiIngressAWS logger httpMgr let localDomain = toLocalUnsafe opts.settings.federationDomain () - pure $ Env awsEnv logger httpMgr http2Mgr (RequestId "N/A") opts localDomain multiIngressAWS + pure $ Env awsEnv logger httpMgr http2Mgr (RequestId defRequestId) opts localDomain multiIngressAWS where initMultiIngressAWS :: Logger -> Manager -> IO (Map String AWS.Env) initMultiIngressAWS logger httpMgr = diff --git a/services/federator/src/Federator/Run.hs b/services/federator/src/Federator/Run.hs index d7ddfb27d4f..83b9883b414 100644 --- a/services/federator/src/Federator/Run.hs +++ b/services/federator/src/Federator/Run.hs @@ -92,7 +92,7 @@ run opts = do newEnv :: Opts -> DNS.Resolver -> Log.Logger -> IO Env newEnv o _dnsResolver _applog = do - let _requestId = RequestId "N/A" + let _requestId = RequestId defRequestId _runSettings = o.optSettings _service Brig = o.brig _service Galley = o.galley diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs index babbaca3a43..41b9f42af3c 100644 --- a/services/federator/test/integration/Test/Federator/IngressSpec.hs +++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs @@ -150,7 +150,7 @@ inwardBrigCallViaIngressWithSettings sslCtx requestPath payload = mgr <- liftToCodensity . liftIO $ http2ManagerWithSSLCtx sslCtx liftToCodensity . runInputConst mgr - . runInputConst (RequestId "N/A") + . runInputConst (RequestId defRequestId) . assertNoError @DiscoveryFailure . discoverConst target . interpretRemote diff --git a/services/federator/test/unit/Test/Federator/Client.hs b/services/federator/test/unit/Test/Federator/Client.hs index a816f7710c9..36c2717a6b4 100644 --- a/services/federator/test/unit/Test/Federator/Client.hs +++ b/services/federator/test/unit/Test/Federator/Client.hs @@ -97,7 +97,7 @@ withMockFederatorClient mock action = withTempMockFederator mock $ \port -> do ceTargetDomain = targetDomain, ceFederator = Endpoint "127.0.0.1" (fromIntegral port), ceHttp2Manager = mgr, - ceOriginRequestId = RequestId "N/A" + ceOriginRequestId = RequestId defRequestId } a <- runFederatorClient env action case a of @@ -137,7 +137,7 @@ testClientStreaming = withInfiniteMockServer $ \port -> do ceTargetDomain = targetDomain, ceFederator = Endpoint "127.0.0.1" (fromIntegral port), ceHttp2Manager = mgr, - ceOriginRequestId = RequestId "N/A" + ceOriginRequestId = RequestId defRequestId } venv = FederatorClientVersionedEnv env Nothing let c = clientIn (Proxy @StreamingAPI) (Proxy @(FederatorClient 'Brig)) @@ -202,7 +202,7 @@ testClientConnectionError = do ceTargetDomain = targetDomain, ceFederator = Endpoint "127.0.0.1" 1, ceHttp2Manager = mgr, - ceOriginRequestId = RequestId "N/A" + ceOriginRequestId = RequestId defRequestId } result <- runFederatorClient env (fedClient @'Brig @"get-user-by-handle" handle) case result of diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index baa3284e861..a9a02e660ea 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -167,7 +167,7 @@ createEnv o l = do mgr <- initHttpManager o h2mgr <- initHttp2Manager codeURIcfg <- validateOptions o - Env (RequestId "N/A") o l mgr h2mgr (o ^. O.federator) (o ^. O.brig) cass + Env (RequestId defRequestId) o l mgr h2mgr (o ^. O.federator) (o ^. O.brig) cass <$> Q.new 16000 <*> initExtEnv <*> maybe (pure Nothing) (fmap Just . Aws.mkEnv l mgr) (o ^. journal) diff --git a/services/gundeck/src/Gundeck/Env.hs b/services/gundeck/src/Gundeck/Env.hs index e3d1fcbe148..2397005c68a 100644 --- a/services/gundeck/src/Gundeck/Env.hs +++ b/services/gundeck/src/Gundeck/Env.hs @@ -27,6 +27,7 @@ import Control.Concurrent.Async (Async) import Control.Lens (makeLenses, (^.)) import Control.Retry (capDelay, exponentialBackoff) import Data.ByteString.Char8 qualified as BSChar8 +import Data.Id import Data.Misc (Milliseconds (..)) import Data.Text qualified as Text import Data.Time.Clock @@ -100,7 +101,7 @@ createEnv o = do { updateAction = Ms . round . (* 1000) <$> getPOSIXTime } mtbs <- mkThreadBudgetState `mapM` (o ^. settings . maxConcurrentNativePushes) - pure $! (rThread : rAdditionalThreads,) $! Env (RequestId "N/A") o l n p r rAdditional a io mtbs + pure $! (rThread : rAdditionalThreads,) $! Env (RequestId defRequestId) o l n p r rAdditional a io mtbs reqIdMsg :: RequestId -> Logger.Msg -> Logger.Msg reqIdMsg = ("request" Logger..=) . unRequestId diff --git a/services/proxy/default.nix b/services/proxy/default.nix index b6205a6acee..8b689661b9c 100644 --- a/services/proxy/default.nix +++ b/services/proxy/default.nix @@ -9,6 +9,7 @@ , bytestring , case-insensitive , configurator +, errors , exceptions , extended , gitignoreSource @@ -21,6 +22,7 @@ , lib , metrics-wai , retry +, servant-server , text , tinylog , types-common @@ -46,6 +48,7 @@ mkDerivation { bytestring case-insensitive configurator + errors exceptions extended http-client @@ -56,6 +59,7 @@ mkDerivation { lens metrics-wai retry + servant-server text tinylog types-common diff --git a/services/proxy/proxy.cabal b/services/proxy/proxy.cabal index e92831949f6..5da48e93a8b 100644 --- a/services/proxy/proxy.cabal +++ b/services/proxy/proxy.cabal @@ -17,7 +17,7 @@ flag static library exposed-modules: - Proxy.API + Proxy.API.Internal Proxy.API.Public Proxy.Env Proxy.Options @@ -80,6 +80,7 @@ library , bytestring >=0.10 , case-insensitive >=1.2 , configurator >=0.3 + , errors , exceptions >=0.8 , extended , http-client >=0.7 @@ -90,6 +91,7 @@ library , lens >=4.11 , metrics-wai >=0.5 , retry >=0.7 + , servant-server , text >=1.2 , tinylog >=0.12 , types-common >=0.8 diff --git a/services/proxy/src/Proxy/API.hs b/services/proxy/src/Proxy/API/Internal.hs similarity index 55% rename from services/proxy/src/Proxy/API.hs rename to services/proxy/src/Proxy/API/Internal.hs index d3ba31ca4f3..c7128b0bfdb 100644 --- a/services/proxy/src/Proxy/API.hs +++ b/services/proxy/src/Proxy/API/Internal.hs @@ -15,29 +15,19 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Proxy.API - ( sitemap, +module Proxy.API.Internal + ( InternalAPI, + servantSitemap, ) where import Imports hiding (head) -import Network.Wai.Predicate (true) -import Network.Wai.Routing (Routes, continue, get, head) -import Network.Wai.Utilities (empty) -import Proxy.API.Public qualified as Public -import Proxy.Env (Env) -import Proxy.Proxy (Proxy) +import Proxy.Proxy qualified +import Servant +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named (Named (Named)) -sitemap :: Env -> Routes a Proxy () -sitemap e = do - Public.sitemap e - routesInternal +type InternalAPI = Named "status" ("i" :> "status" :> MultiVerb 'GET '[Servant.JSON] '[RespondEmpty 200 "OK"] ()) --- | IF YOU MODIFY THIS, BE AWARE OF: --- --- >>> /libs/wire-api/src/Wire/API/Routes/Public/Proxy.hs --- >>> https://wearezeta.atlassian.net/browse/SQSERVICES-1647 -routesInternal :: Routes a Proxy () -routesInternal = do - head "/i/status" (continue $ const (pure empty)) true - get "/i/status" (continue $ const (pure empty)) true +servantSitemap :: ServerT InternalAPI Proxy.Proxy.Proxy +servantSitemap = Named @"status" (pure ()) diff --git a/services/proxy/src/Proxy/API/Public.hs b/services/proxy/src/Proxy/API/Public.hs index a33bafb1d9c..24989369d48 100644 --- a/services/proxy/src/Proxy/API/Public.hs +++ b/services/proxy/src/Proxy/API/Public.hs @@ -16,7 +16,9 @@ -- with this program. If not, see . module Proxy.API.Public - ( sitemap, + ( PublicAPI, + servantSitemap, + waiRoutingSitemap, ) where @@ -41,18 +43,35 @@ import Network.Wai.Internal qualified as I import Network.Wai.Predicate hiding (Error, err, setStatus) import Network.Wai.Predicate.Request (getRequest) import Network.Wai.Routing hiding (path, route) +import Network.Wai.Routing qualified as Routing import Network.Wai.Utilities +import Network.Wai.Utilities.Server (compile) import Proxy.Env import Proxy.Proxy +import Servant qualified import System.Logger.Class hiding (Error, info, render) import System.Logger.Class qualified as Logger +type PublicAPI = Servant.Raw -- see https://wearezeta.atlassian.net/browse/WPB-1216 + +servantSitemap :: Env -> Servant.ServerT PublicAPI Proxy.Proxy.Proxy +servantSitemap e = Servant.Tagged app + where + app :: Application + app r k = appInProxy e r (Routing.route tree r k') + where + tree :: Tree (App Proxy) + tree = compile (waiRoutingSitemap e) + + k' :: Response -> Proxy.Proxy.Proxy ResponseReceived + k' = liftIO . k + -- | IF YOU MODIFY THIS, BE AWARE OF: -- -- >>> /libs/wire-api/src/Wire/API/Routes/Public/Proxy.hs -- >>> https://wearezeta.atlassian.net/browse/SQSERVICES-1647 -sitemap :: Env -> Routes a Proxy () -sitemap e = do +waiRoutingSitemap :: Env -> Routes a Proxy () +waiRoutingSitemap e = do get "/proxy/youtube/v3/:path" (proxy e "key" "secrets.youtube" Prefix "/youtube/v3" youtube) @@ -107,7 +126,7 @@ proxy e qparam keyname reroute path phost rq k = do then do threadDelay 5000 loop runInIO (n - 1) waiReq req - else runProxy e waiReq (k res) + else appInProxy e waiReq (k res) onUpstreamError runInIO x _ next = do void . runInIO $ Logger.warn (msg (val "gateway error") ~~ field "error" (show x)) next (errorRs error502) diff --git a/services/proxy/src/Proxy/Env.hs b/services/proxy/src/Proxy/Env.hs index d429787d1be..7b50325ed80 100644 --- a/services/proxy/src/Proxy/Env.hs +++ b/services/proxy/src/Proxy/Env.hs @@ -32,7 +32,7 @@ where import Control.Lens (makeLenses, (^.)) import Data.Configurator import Data.Configurator.Types -import Data.Id (RequestId (..)) +import Data.Id (RequestId (..), defRequestId) import Imports import Network.HTTP.Client import Network.HTTP.Client.TLS (tlsManagerSettings) @@ -62,7 +62,7 @@ createEnv o = do } let ac = AutoConfig 60 (reloadError g) (c, t) <- autoReload ac [Required $ o ^. secretsConfig] - let rid = RequestId "N/A" + let rid = RequestId defRequestId pure $! Env rid o g n c t where reloadError g x = diff --git a/services/proxy/src/Proxy/Proxy.hs b/services/proxy/src/Proxy/Proxy.hs index fe65dc4b920..348c96eb39b 100644 --- a/services/proxy/src/Proxy/Proxy.hs +++ b/services/proxy/src/Proxy/Proxy.hs @@ -17,7 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Proxy.Proxy (Proxy, runProxy) where +module Proxy.Proxy (Proxy, appInProxy, runProxy) where import Bilge.Request (requestIdName) import Control.Lens hiding ((.=)) @@ -51,11 +51,14 @@ newtype Proxy a = Proxy instance MonadLogger Proxy where log l m = ask >>= \e -> Logger.log (e ^. applog) l (reqIdMsg (e ^. reqId) . m) -runProxy :: Env -> Request -> Proxy ResponseReceived -> IO ResponseReceived -runProxy e r m = do +appInProxy :: Env -> Request -> Proxy ResponseReceived -> IO ResponseReceived +appInProxy e r m = do rid <- lookupReqId (e ^. applog) r runReaderT (unProxy m) (reqId .~ rid $ e) +runProxy :: Env -> Proxy a -> IO a +runProxy e m = runReaderT (unProxy m) e + reqIdMsg :: RequestId -> Msg -> Msg reqIdMsg = ("request" .=) . unRequestId {-# INLINE reqIdMsg #-} diff --git a/services/proxy/src/Proxy/Run.hs b/services/proxy/src/Proxy/Run.hs index 16d43994006..14ebb11f691 100644 --- a/services/proxy/src/Proxy/Run.hs +++ b/services/proxy/src/Proxy/Run.hs @@ -20,29 +20,69 @@ module Proxy.Run ) where +import Bilge.Request (requestIdName) +import Control.Error import Control.Lens hiding ((.=)) import Control.Monad.Catch -import Data.Metrics.Middleware.Prometheus (waiPrometheusMiddleware) +import Data.Id (RequestId (RequestId), defRequestId) +import Data.Metrics.Middleware.Prometheus (waiPrometheusMiddlewarePaths) +import Data.Metrics.Servant +import Data.Metrics.Types +import Data.Metrics.WaiRoute import Imports hiding (head) +import Network.Wai (Middleware, Request, requestHeaders) import Network.Wai.Middleware.Gunzip qualified as GZip +import Network.Wai.Routing.Route import Network.Wai.Utilities.Server hiding (serverPort) -import Proxy.API (sitemap) +import Proxy.API.Internal as I +import Proxy.API.Public as P import Proxy.Env import Proxy.Options import Proxy.Proxy +import Servant qualified import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai +type CombinedAPI = PublicAPI Servant.:<|> InternalAPI + +combinedSitemap :: Env -> Servant.ServerT CombinedAPI Proxy +combinedSitemap env = P.servantSitemap env Servant.:<|> I.servantSitemap + run :: Opts -> IO () run o = do e <- createEnv o s <- newSettings $ defaultServer (o ^. host) (o ^. port) (e ^. applog) - let rtree = compile (sitemap e) - let app r k = runProxy e r (route rtree r k) - let middleware = + + let metricsMW :: Middleware + metricsMW = + -- FUTUREWORK: once wai-routing has been removed from proxy: use `servantPrometheusMiddleware + -- (Servant.Proxy @CombinedAPI)` here (and probably inline the whole thing). + waiPrometheusMiddlewarePaths (pub <> int) + where + pub, int :: Paths + pub = treeToPaths $ prepare (P.waiRoutingSitemap e) + int = routesToPaths @InternalAPI + + middleware :: Middleware + middleware = versionMiddleware (foldMap expandVersionExp (o ^. disabledAPIVersions)) . requestIdMiddleware (e ^. applog) defaultRequestIdHeaderName - . waiPrometheusMiddleware (sitemap e) + . metricsMW . GZip.gunzip . catchErrors (e ^. applog) defaultRequestIdHeaderName - runSettingsWithShutdown s (middleware app) Nothing `finally` destroyEnv e + + runSettingsWithShutdown s (middleware (mkApp e)) Nothing `finally` destroyEnv e + +mkApp :: Env -> Servant.Application +mkApp env req = Servant.serve (Servant.Proxy @CombinedAPI) toServantSitemap req + where + toServantSitemap :: Servant.Server CombinedAPI + toServantSitemap = Servant.hoistServer (Servant.Proxy @CombinedAPI) toServantHandler (combinedSitemap env) + + toServantHandler :: Proxy a -> Servant.Handler a + toServantHandler p = Servant.Handler . ExceptT $ Right <$> runProxy (injectReqId req env) p + + injectReqId :: Request -> Env -> Env + injectReqId r = reqId .~ lookupReqId r + where + lookupReqId = RequestId . fromMaybe defRequestId . lookup requestIdName . requestHeaders diff --git a/services/proxy/test/scripts/proxy-test.sh b/services/proxy/test/scripts/proxy-test.sh index 3f8ee9ed3ba..ea7b89fe403 100755 --- a/services/proxy/test/scripts/proxy-test.sh +++ b/services/proxy/test/scripts/proxy-test.sh @@ -11,7 +11,8 @@ instance. this replaces more thorough integration tests, since integration tests for just proxy without the proxied services installed is hard and inadequate. -WIRE_BACKEND: $WIRE_BACKEND +WIRE_BACKEND: $WIRE_BACKEND (do not append a / to host:port!) + WIRE_ADMIN: $WIRE_ADMIN WIRE_PASSWD: " diff --git a/services/spar/src/Spar/Run.hs b/services/spar/src/Spar/Run.hs index e577e9ed5b6..170721df48b 100644 --- a/services/spar/src/Spar/Run.hs +++ b/services/spar/src/Spar/Run.hs @@ -99,7 +99,7 @@ mkApp sparCtxOpts = do Bilge.host (sparCtxOpts ^. to galley . to host . to encodeUtf8) . Bilge.port (sparCtxOpts ^. to galley . to port) $ Bilge.empty - let sparCtxRequestId = RequestId "N/A" + let sparCtxRequestId = RequestId defRequestId let ctx0 = Env {..} let heavyLogOnly :: (Wai.Request, LByteString) -> Maybe (Wai.Request, LByteString) heavyLogOnly out@(req, _) = diff --git a/tools/stern/src/Stern/App.hs b/tools/stern/src/Stern/App.hs index 1056cf37182..e0f021a0932 100644 --- a/tools/stern/src/Stern/App.hs +++ b/tools/stern/src/Stern/App.hs @@ -69,7 +69,7 @@ newEnv opts = do (mkRequest opts.ibis) (mkRequest opts.galeb) l - (RequestId "N/A") + (RequestId defRequestId) <$> newManager where mkRequest s = Bilge.host (encodeUtf8 s.host) . Bilge.port s.port $ Bilge.empty From d70fcee73e3e3db5d651e8786390326b29fd0c0a Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 21 Oct 2024 11:16:05 +0200 Subject: [PATCH 116/136] Refactor CSV export (#4293) * Initial endpoint skeleton * Set up finalisation for CSV streaming * Implement internal API to get user activity * Test activity endpoint * Initial refactoring of CSV export * getUserRecord implemented * fix integration package * New implementation of getTeamMembersCSV * Implement inviter handle cache * Remove old CSV export handler * Add activity timestamp to csv export * Regenerate nix packages * Linter * Remove new stern endpoint * Add status field to CSV export * Remove new brig internal endpoint This is not needed anymore since the stern endpoint to get user activity has been removed. * Add CHANGELOG entry * Regenerate nix packages * Fix CSV roundtrip test * Remove lookupRichInfo * Remove stern endpoint test * Simplify SCIM user info lookup * fixup! Simplify SCIM user info lookup --------- Co-authored-by: Leif Battermann --- .../1-api-changes/add-columns-to-export | 1 + integration/integration.cabal | 1 + integration/test/API/Stern.hs | 8 + integration/test/Test/Teams.hs | 45 +++-- libs/wire-api/default.nix | 2 + .../src/Wire/API/Routes/Internal/Brig.hs | 9 + .../src/Wire/API/Routes/Internal/Spar.hs | 2 +- .../src/Wire/API/Routes/LowLevelStream.hs | 38 +++- .../wire-api/src/Wire/API/Routes/MultiVerb.hs | 16 +- .../src/Wire/API/Routes/Public/Cargohold.hs | 1 - libs/wire-api/src/Wire/API/Team/Export.hs | 76 +++++++- libs/wire-api/src/Wire/API/Team/Member.hs | 1 + libs/wire-api/src/Wire/API/User.hs | 13 -- .../test/unit/Test/Wire/API/Roundtrip/CSV.hs | 21 ++- libs/wire-api/wire-api.cabal | 2 + libs/wire-subsystems/default.nix | 2 + libs/wire-subsystems/src/Wire/UserStore.hs | 4 + .../src/Wire/UserStore/Cassandra.hs | 19 ++ .../wire-subsystems/src/Wire/UserSubsystem.hs | 2 + .../src/Wire/UserSubsystem/Interpreter.hs | 175 ++++++++++------- .../test/unit/Wire/MiniBackend.hs | 2 +- .../unit/Wire/MockInterpreters/UserStore.hs | 2 + libs/wire-subsystems/wire-subsystems.cabal | 1 + services/brig/src/Brig/API/Internal.hs | 16 +- services/brig/src/Brig/API/Public.hs | 11 +- services/brig/src/Brig/API/User.hs | 1 - .../brig/src/Brig/CanonicalInterpreter.hs | 4 +- services/brig/src/Brig/Data/User.hs | 9 - services/galley/default.nix | 4 - services/galley/galley.cabal | 3 +- .../src/Galley/API/Public/TeamMember.hs | 3 +- services/galley/src/Galley/API/Teams.hs | 143 +------------- .../galley/src/Galley/API/Teams/Export.hs | 177 ++++++++++++++++++ .../galley/src/Galley/Effects/BrigAccess.hs | 3 + .../galley/src/Galley/Effects/SparAccess.hs | 2 +- services/galley/src/Galley/Intra/Effects.hs | 9 +- services/galley/src/Galley/Intra/Spar.hs | 14 +- services/galley/src/Galley/Intra/User.hs | 12 ++ services/spar/src/Spar/API.hs | 10 +- tools/stern/default.nix | 2 + tools/stern/src/Stern/API.hs | 2 +- tools/stern/src/Stern/App.hs | 3 + tools/stern/src/Stern/Intra.hs | 17 ++ tools/stern/stern.cabal | 1 + 44 files changed, 580 insertions(+), 309 deletions(-) create mode 100644 changelog.d/1-api-changes/add-columns-to-export create mode 100644 integration/test/API/Stern.hs create mode 100644 services/galley/src/Galley/API/Teams/Export.hs diff --git a/changelog.d/1-api-changes/add-columns-to-export b/changelog.d/1-api-changes/add-columns-to-export new file mode 100644 index 00000000000..04633327ba1 --- /dev/null +++ b/changelog.d/1-api-changes/add-columns-to-export @@ -0,0 +1 @@ +The team CSV export endpoint has gained two extra columns: `last_active` and `status`. The streaming behaviour has also been improved. diff --git a/integration/integration.cabal b/integration/integration.cabal index a3989f28e76..edada8586df 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -100,6 +100,7 @@ library API.GundeckInternal API.Nginz API.Spar + API.Stern MLS.Util Notifications RunAllTests diff --git a/integration/test/API/Stern.hs b/integration/test/API/Stern.hs new file mode 100644 index 00000000000..b7d93d07178 --- /dev/null +++ b/integration/test/API/Stern.hs @@ -0,0 +1,8 @@ +module API.Stern where + +import Testlib.Prelude + +getTeamActivity :: (HasCallStack, MakesValue domain) => domain -> String -> App Response +getTeamActivity domain tid = + baseRequest domain Stern Unversioned (joinHttpPath ["team-activity-info", tid]) + >>= submit "GET" diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs index 623983abcba..5ce10031fba 100644 --- a/integration/test/Test/Teams.hs +++ b/integration/test/Test/Teams.hs @@ -1,3 +1,4 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2024 Wire Swiss GmbH @@ -22,6 +23,7 @@ import qualified API.BrigInternal as I import API.Common import API.Galley (getTeam, getTeamMembers, getTeamMembersCsv, getTeamNotifications) import API.GalleyInternal (setTeamFeatureStatus) +import API.Gundeck import Control.Monad.Codensity (Codensity (runCodensity)) import Control.Monad.Extra (findM) import Control.Monad.Reader (asks) @@ -284,16 +286,28 @@ testUpgradePersonalToTeamAlreadyInATeam = do -- for additional tests of the CSV download particularly with SCIM users, please refer to 'Test.Spar.Scim.UserSpec' testTeamMemberCsvExport :: (HasCallStack) => App () testTeamMemberCsvExport = do - (owner, tid, members) <- createTeam OwnDomain 10 - let numClients = [0, 1, 2] <> repeat 0 - modifiedMembers <- for (zip numClients (owner : members)) $ \(n, m) -> do - handle <- randomHandle - putHandle m handle >>= assertSuccess - replicateM_ n $ addClient m def - void $ I.putSSOId m def {I.scimExternalId = Just "foo"} >>= getBody 200 - setField "handle" handle m - >>= setField "role" (if m == owner then "owner" else "member") - >>= setField "num_clients" (show n) + (owner, tid, members) <- createTeam OwnDomain 5 + + modifiedMembers <- for + ( zip + ([0, 1, 2] <> repeat 0) + (owner : members) + ) + $ \(n, m) -> do + handle <- randomHandle + putHandle m handle >>= assertSuccess + clients <- + replicateM n + $ addClient m def + >>= getJSON 201 + >>= (%. "id") + >>= asString + for_ (listToMaybe clients) $ \c -> + getNotifications m def {client = Just c} + void $ I.putSSOId m def {I.scimExternalId = Just "foo"} >>= getBody 200 + setField "handle" handle m + >>= setField "role" (if m == owner then "owner" else "member") + >>= setField "num_clients" n memberMap :: Map.Map String Value <- fmap Map.fromList $ for (modifiedMembers) $ \m -> do uid <- m %. "id" & asString @@ -302,7 +316,7 @@ testTeamMemberCsvExport = do bindResponse (getTeamMembersCsv owner tid) $ \resp -> do resp.status `shouldMatchInt` 200 let rows = sort $ tail $ B8.lines $ resp.body - length rows `shouldMatchInt` 10 + length rows `shouldMatchInt` 5 for_ rows $ \row -> do let cols = B8.split ',' row let uid = read $ B8.unpack $ cols !! 11 @@ -310,6 +324,8 @@ testTeamMemberCsvExport = do ownerId <- owner %. "id" & asString let ownerMember = memberMap Map.! ownerId + now <- formatTime defaultTimeLocale "%Y-%m-%d" <$> liftIO getCurrentTime + numClients <- mem %. "num_clients" & asInt let parseField = unquote . read . B8.unpack . (cols !!) @@ -319,12 +335,15 @@ testTeamMemberCsvExport = do role <- mem %. "role" & asString parseField 3 `shouldMatch` role when (role /= "owner") $ do - now <- formatTime defaultTimeLocale "%Y-%m-%d" <$> liftIO getCurrentTime take 10 (parseField 4) `shouldMatch` now parseField 5 `shouldMatch` (ownerMember %. "handle") parseField 7 `shouldMatch` "wire" parseField 9 `shouldMatch` "foo" - parseField 12 `shouldMatch` (mem %. "num_clients") + parseField 12 `shouldMatch` show numClients + (if numClients > 0 then shouldNotMatch else shouldMatch) + (parseField 13) + "" + parseField 14 `shouldMatch` "active" where unquote :: String -> String unquote ('\'' : x) = x diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 2fd02d1acf6..c4f7828d2c3 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -57,6 +57,7 @@ , iso3166-country-codes , iso639 , jose +, kan-extensions , lens , lib , memory @@ -165,6 +166,7 @@ mkDerivation { iso3166-country-codes iso639 jose + kan-extensions lens memory metrics-wai 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 5c6dc34ffd9..31c1b018e33 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -76,6 +76,7 @@ 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) +import Wire.API.Team.Export (TeamExportUser) import Wire.API.Team.Feature import Wire.API.Team.Invitation (Invitation) import Wire.API.Team.LegalHold.Internal @@ -601,6 +602,14 @@ type UserAPI = UpdateUserLocale :<|> DeleteUserLocale :<|> GetDefaultLocale + :<|> Named + "get-user-export-data" + ( Summary "Get user export data" + :> "users" + :> Capture "uid" UserId + :> "export-data" + :> MultiVerb1 'GET '[JSON] (Respond 200 "User export data" (Maybe TeamExportUser)) + ) type UpdateUserLocale = Summary 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 8cc2207031c..b5bc7b34380 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Spar.hs @@ -31,7 +31,7 @@ type InternalAPI = :> ( "status" :> Get '[JSON] NoContent :<|> "teams" :> Capture "team" TeamId :> DeleteNoContent :<|> "sso" :> "settings" :> ReqBody '[JSON] SsoSettings :> Put '[JSON] NoContent - :<|> "scim" :> "userinfos" :> ReqBody '[JSON] UserSet :> Post '[JSON] ScimUserInfos + :<|> "scim" :> "userinfo" :> Capture "user" UserId :> Post '[JSON] ScimUserInfo ) swaggerDoc :: OpenApi diff --git a/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs b/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs index f39080b54f7..0313c04ced8 100644 --- a/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs +++ b/libs/wire-api/src/Wire/API/Routes/LowLevelStream.hs @@ -18,6 +18,8 @@ module Wire.API.Routes.LowLevelStream where import Control.Lens (at, (.~), (?~), _Just) +import Control.Monad.Codensity +import Control.Monad.Trans.Resource import Data.ByteString.Char8 as B8 import Data.CaseInsensitive qualified as CI import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap @@ -39,6 +41,10 @@ import Servant.Server hiding (respond) import Servant.Server.Internal import Wire.API.Routes.Version +-- | Used as the return type of a streaming handler. The 'Codensity' wrapper +-- makes it possible to add finalisation logic to the streaming action. +type LowLevelStreamingBody = Codensity IO StreamingBody + -- FUTUREWORK: make it possible to generate headers at runtime data LowLevelStream method status (headers :: [(Symbol, Symbol)]) desc ctype @@ -63,7 +69,9 @@ instance (ReflectMethod method, KnownNat status, RenderHeaders headers, Accept ctype) => HasServer (LowLevelStream method status headers desc ctype) context where - type ServerT (LowLevelStream method status headers desc ctype) m = m StreamingBody + type + ServerT (LowLevelStream method status headers desc ctype) m = + m LowLevelStreamingBody hoistServerWithContext _ _ nt s = nt s route Proxy _ action = leafRouter $ \env request respond -> @@ -71,15 +79,25 @@ instance cmediatype = HTTP.matchAccept [contentType (Proxy @ctype)] accH accCheck = when (isNothing cmediatype) $ delayedFail err406 contentHeader = (hContentType, HTTP.renderHeader . maybeToList $ cmediatype) - in runAction - ( action - `addMethodCheck` methodCheck method request - `addAcceptCheck` accCheck - ) - env - request - respond - $ Route . responseStream status (contentHeader : extraHeaders) + in runResourceT $ do + r <- + runDelayed + ( action + `addMethodCheck` methodCheck method request + `addAcceptCheck` accCheck + ) + env + request + liftIO $ case r of + Route h -> + runHandler h >>= \case + Left e -> respond $ FailFatal e + Right getStreamingBody -> lowerCodensity $ do + body <- getStreamingBody + let resp = responseStream status (contentHeader : extraHeaders) body + lift $ respond $ Route resp + Fail e -> respond $ Fail e + FailFatal e -> respond $ FailFatal e where method = reflectMethod (Proxy :: Proxy method) status = statusFromNat (Proxy :: Proxy status) diff --git a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs index 2dfeb16685a..ebda942c2cd 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs @@ -110,9 +110,9 @@ type RespondEmpty s desc = RespondAs '() s desc () -- | A type to describe a streaming 'MultiVerb' response. -- --- Includes status code, description, framing strategy and content type. Note --- that the handler return type is hardcoded to be 'SourceIO ByteString'. -data RespondStreaming (s :: Nat) (desc :: Symbol) (framing :: Type) (ct :: Type) +-- Includes status code, description and content type. Note that the handler +-- return type is hardcoded to be 'SourceIO ByteString'. +data RespondStreaming (s :: Nat) (desc :: Symbol) (ct :: Type) -- | The result of parsing a response as a union alternative of type 'a'. -- @@ -268,14 +268,14 @@ instance mempty & S.description .~ Text.pack (symbolVal (Proxy @desc)) -type instance ResponseType (RespondStreaming s desc framing ct) = SourceIO ByteString +type instance ResponseType (RespondStreaming s desc ct) = SourceIO ByteString instance (Accept ct, KnownStatus s) => - IsResponse cs (RespondStreaming s desc framing ct) + IsResponse cs (RespondStreaming s desc ct) where - type ResponseStatus (RespondStreaming s desc framing ct) = s - type ResponseBody (RespondStreaming s desc framing ct) = SourceIO ByteString + type ResponseStatus (RespondStreaming s desc ct) = s + type ResponseBody (RespondStreaming s desc ct) = SourceIO ByteString responseRender _ x = pure . addContentType @ct $ Response @@ -289,7 +289,7 @@ instance guard (responseStatusCode resp == statusVal (Proxy @s)) pure $ responseBody resp -instance (KnownSymbol desc) => IsSwaggerResponse (RespondStreaming s desc framing ct) where +instance (KnownSymbol desc) => IsSwaggerResponse (RespondStreaming s desc ct) where responseSwagger = pure $ mempty diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs index d9c7ca0ed3e..4b15e9d1df2 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs @@ -80,7 +80,6 @@ type AssetStreaming = RespondStreaming 200 "Asset returned directly with content type `application/octet-stream`" - NoFraming OctetStream type GetAsset = diff --git a/libs/wire-api/src/Wire/API/Team/Export.hs b/libs/wire-api/src/Wire/API/Team/Export.hs index 7a37047c307..c31040c5e42 100644 --- a/libs/wire-api/src/Wire/API/Team/Export.hs +++ b/libs/wire-api/src/Wire/API/Team/Export.hs @@ -17,6 +17,7 @@ module Wire.API.Team.Export (TeamExportUser (..), quoted, unquoted) where +import Data.Aeson qualified as A import Data.Aeson qualified as Aeson import Data.Attoparsec.ByteString.Lazy (parseOnly) import Data.ByteString.Char8 qualified as C @@ -24,18 +25,27 @@ import Data.ByteString.Conversion (FromByteString (..), ToByteString, toByteStri import Data.Csv (DefaultOrdered (..), FromNamedRecord (..), Parser, ToNamedRecord (..), namedRecord, (.:)) import Data.Handle (Handle) import Data.Id (UserId) -import Data.Json.Util (UTCTimeMillis) +import Data.Json.Util (UTCTimeMillis, utcTimeSchema) import Data.Misc (HttpsUrl) +import Data.OpenApi qualified as OpenApi +import Data.Schema +import Data.Text qualified as T +import Data.Text.Encoding qualified as T +import Data.Time.Clock +import Data.Time.Format import Data.Vector (fromList) import Imports import Test.QuickCheck import Wire.API.Team.Role (Role) -import Wire.API.User (Name) +import Wire.API.User (AccountStatus (..), Name) import Wire.API.User.Identity (EmailAddress) import Wire.API.User.Profile (ManagedBy) import Wire.API.User.RichInfo (RichInfo) import Wire.Arbitrary +timestampFormat :: String +timestampFormat = "%Y-%m-%d" + data TeamExportUser = TeamExportUser { tExportDisplayName :: Name, tExportHandle :: Maybe Handle, @@ -49,10 +59,33 @@ data TeamExportUser = TeamExportUser tExportSCIMExternalId :: Text, tExportSCIMRichInfo :: Maybe RichInfo, tExportUserId :: UserId, - tExportNumDevices :: Int + tExportNumDevices :: Int, + tExportLastActive :: Maybe UTCTime, + tExportStatus :: Maybe AccountStatus } deriving (Show, Eq, Generic) deriving (Arbitrary) via (GenericUniform TeamExportUser) + deriving (A.ToJSON, A.FromJSON, OpenApi.ToSchema) via (Schema TeamExportUser) + +instance ToSchema TeamExportUser where + schema = + object "TeamExportUser" $ + TeamExportUser + <$> tExportDisplayName .= field "display_name" schema + <*> tExportHandle .= maybe_ (optField "handle" schema) + <*> tExportEmail .= maybe_ (optField "email" schema) + <*> tExportRole .= maybe_ (optField "role" schema) + <*> tExportCreatedOn .= maybe_ (optField "created_on" schema) + <*> tExportInvitedBy .= maybe_ (optField "invited_by" schema) + <*> tExportIdpIssuer .= maybe_ (optField "idp_issuer" schema) + <*> tExportManagedBy .= field "managed_by" schema + <*> tExportSAMLNamedId .= field "saml_name_id" schema + <*> tExportSCIMExternalId .= field "scim_external_id" schema + <*> tExportSCIMRichInfo .= maybe_ (optField "scim_rich_info" schema) + <*> tExportUserId .= field "user_id" schema + <*> tExportNumDevices .= field "num_devices" schema + <*> tExportLastActive .= maybe_ (optField "last_active" utcTimeSchema) + <*> tExportStatus .= maybe_ (optField "status" schema) instance ToNamedRecord TeamExportUser where toNamedRecord row = @@ -69,7 +102,16 @@ instance ToNamedRecord TeamExportUser where ("scim_external_id", secureCsvFieldToByteString (tExportSCIMExternalId row)), ("scim_rich_info", maybe "" (C.toStrict . Aeson.encode) (tExportSCIMRichInfo row)), ("user_id", secureCsvFieldToByteString (tExportUserId row)), - ("num_devices", secureCsvFieldToByteString (tExportNumDevices row)) + ("num_devices", secureCsvFieldToByteString (tExportNumDevices row)), + ( "last_active", + C.pack + ( maybe + "" + (formatTime defaultTimeLocale timestampFormat) + (tExportLastActive row) + ) + ), + ("status", maybe "" formatAccountStatus (tExportStatus row)) ] secureCsvFieldToByteString :: forall a. (ToByteString a) => a -> ByteString @@ -91,7 +133,9 @@ instance DefaultOrdered TeamExportUser where "scim_external_id", "scim_rich_info", "user_id", - "num_devices" + "num_devices", + "last_active", + "status" ] allowEmpty :: (ByteString -> Parser a) -> ByteString -> Parser (Maybe a) @@ -104,6 +148,26 @@ parseByteString bstr = Left err -> fail err Right thing -> pure thing +parseUTCTime :: ByteString -> Parser UTCTime +parseUTCTime b = do + s <- either (fail . displayException) pure $ T.decodeUtf8' b + parseTimeM False defaultTimeLocale timestampFormat (T.unpack s) + +parseAccountStatus :: ByteString -> Parser AccountStatus +parseAccountStatus "active" = pure Active +parseAccountStatus "suspended" = pure Suspended +parseAccountStatus "deleted" = pure Deleted +parseAccountStatus "ephemeral" = pure Ephemeral +parseAccountStatus "pending-invitation" = pure PendingInvitation +parseAccountStatus _ = fail "invalid account status" + +formatAccountStatus :: AccountStatus -> ByteString +formatAccountStatus Active = "active" +formatAccountStatus Suspended = "suspended" +formatAccountStatus Deleted = "deleted" +formatAccountStatus Ephemeral = "ephemeral" +formatAccountStatus PendingInvitation = "pending-invitation" + instance FromNamedRecord TeamExportUser where parseNamedRecord nrec = TeamExportUser @@ -126,6 +190,8 @@ instance FromNamedRecord TeamExportUser where ) <*> (nrec .: "user_id" >>= parseByteString) <*> (nrec .: "num_devices" >>= parseByteString) + <*> (nrec .: "last_active" >>= allowEmpty parseUTCTime) + <*> (nrec .: "status" >>= allowEmpty parseAccountStatus) quoted :: ByteString -> ByteString quoted bs = case C.uncons bs of diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index 98720fab69b..108e19adb98 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -22,6 +22,7 @@ module Wire.API.Team.Member ( -- * TeamMember TeamMember, + newTeamMember, mkTeamMember, userId, permissions, diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 6e037a0489a..f55459746f7 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -28,7 +28,6 @@ module Wire.API.User qualifiedUserIdListObjectSchema, LimitedQualifiedUserIdList (..), ScimUserInfo (..), - ScimUserInfos (..), UserSet (..), -- Profiles UserProfile (..), @@ -1339,18 +1338,6 @@ instance ToSchema ScimUserInfo where <*> suiCreatedOn .= maybe_ (optField "created_on" schema) -newtype ScimUserInfos = ScimUserInfos {scimUserInfos :: [ScimUserInfo]} - deriving stock (Eq, Show, Generic) - deriving (Arbitrary) via (GenericUniform ScimUserInfos) - deriving (ToJSON, FromJSON, S.ToSchema) via (Schema ScimUserInfos) - -instance ToSchema ScimUserInfos where - schema = - object "ScimUserInfos" $ - ScimUserInfos - <$> scimUserInfos - .= field "scim_user_infos" (array schema) - ------------------------------------------------------------------------------- -- UserSet diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs index 3844143e128..477b154cf67 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/CSV.hs @@ -19,17 +19,32 @@ module Test.Wire.API.Roundtrip.CSV where import Control.Arrow ((>>>)) import Data.Csv +import Data.Time.Clock import Data.Vector qualified as V import Imports import Test.Tasty qualified as T -import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===)) +import Test.Tasty.QuickCheck import Type.Reflection (typeRep) -import Wire.API.Team.Export qualified as Team.Export +import Wire.API.Team.Export + +newtype ValidTeamExportUser = ValidTeamExportUser + {unValidTeamExportUser :: TeamExportUser} + deriving newtype (FromNamedRecord, ToNamedRecord, DefaultOrdered, Eq, Show) + +instance Arbitrary ValidTeamExportUser where + arbitrary = do + u <- arbitrary + let resetTime (UTCTime d _) = UTCTime d 0 + pure $ + ValidTeamExportUser + u + { tExportLastActive = fmap resetTime (tExportLastActive u) + } tests :: T.TestTree tests = T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "CSV roundtrip tests" $ - [testRoundTrip @Team.Export.TeamExportUser] + [testRoundTrip @ValidTeamExportUser] testRoundTrip :: forall a. diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 1091c12f7f3..c0c933bec66 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -300,6 +300,7 @@ library , iso3166-country-codes >=0.2 , iso639 >=0.1 , jose + , kan-extensions , lens >=4.12 , memory , metrics-wai @@ -713,6 +714,7 @@ test-suite wire-api-tests , tasty-hunit , tasty-quickcheck , text + , time , types-common >=0.16 , unliftio , uuid diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index ead2f2a9c9d..7582b8b4228 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -15,6 +15,7 @@ , bloodhound , bytestring , bytestring-conversion +, case-insensitive , cassandra-util , conduit , containers @@ -104,6 +105,7 @@ mkDerivation { bloodhound bytestring bytestring-conversion + case-insensitive cassandra-util conduit containers diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index 55373c0a37d..6ebb55c71cf 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -6,10 +6,12 @@ import Cassandra (PageWithState (..), PagingState) import Data.Default import Data.Handle import Data.Id +import Data.Time.Clock import Imports import Polysemy import Polysemy.Error import Wire.API.User +import Wire.API.User.RichInfo import Wire.Arbitrary import Wire.StoredUser import Wire.UserStore.IndexUser @@ -67,6 +69,8 @@ data UserStore m a where IsActivated :: UserId -> UserStore m Bool LookupLocale :: UserId -> UserStore m (Maybe (Maybe Language, Maybe Country)) UpdateUserTeam :: UserId -> TeamId -> UserStore m () + GetActivityTimestamps :: UserId -> UserStore m [Maybe UTCTime] + GetRichInfo :: UserId -> UserStore m (Maybe RichInfoAssocList) makeSem ''UserStore diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index 66d35568d27..db15b04f4b4 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -4,12 +4,14 @@ import Cassandra import Cassandra.Exec (prepared) import Data.Handle import Data.Id +import Data.Time.Clock import Database.CQL.Protocol import Imports import Polysemy import Polysemy.Embed import Polysemy.Error import Wire.API.User hiding (DeleteUser) +import Wire.API.User.RichInfo import Wire.StoredUser import Wire.UserStore import Wire.UserStore.IndexUser hiding (userId) @@ -31,6 +33,8 @@ interpretUserStoreCassandra casClient = IsActivated uid -> isActivatedImpl uid LookupLocale uid -> lookupLocaleImpl uid UpdateUserTeam uid tid -> updateUserTeamImpl uid tid + GetActivityTimestamps uid -> getActivityTimestampsImpl uid + GetRichInfo uid -> getRichInfoImpl uid getUsersImpl :: [UserId] -> Client [StoredUser] getUsersImpl usrs = @@ -169,6 +173,21 @@ updateUserTeamImpl u t = retry x5 $ write userTeamUpdate (params LocalQuorum (t, userTeamUpdate :: PrepQuery W (TeamId, UserId) () userTeamUpdate = "UPDATE user SET team = ? WHERE id = ?" +getActivityTimestampsImpl :: UserId -> Client [Maybe UTCTime] +getActivityTimestampsImpl uid = do + runIdentity <$$> retry x1 (query q (params LocalQuorum (Identity uid))) + where + q :: PrepQuery R (Identity UserId) (Identity (Maybe UTCTime)) + q = "SELECT last_active from clients where user = ?" + +getRichInfoImpl :: UserId -> Client (Maybe RichInfoAssocList) +getRichInfoImpl uid = + fmap runIdentity + <$> retry x1 (query1 q (params LocalQuorum (Identity uid))) + where + q :: PrepQuery R (Identity UserId) (Identity RichInfoAssocList) + q = "SELECT json FROM rich_info WHERE user = ?" + -------------------------------------------------------------------------------- -- Queries diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index 10357641b71..c8237825748 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -20,6 +20,7 @@ import Polysemy import Polysemy.Error 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 @@ -143,6 +144,7 @@ data UserSubsystem m a where -- migration this would just be an internal detail of the subsystem InternalUpdateSearchIndex :: UserId -> UserSubsystem m () InternalFindTeamInvitation :: Maybe EmailKey -> InvitationCode -> UserSubsystem m StoredInvitation + GetUserExportData :: UserId -> UserSubsystem m (Maybe TeamExportUser) -- | the return type of 'CheckHandle' data CheckHandleResp diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index bbcbe719eb8..8f9ba2566e1 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -7,8 +7,10 @@ module Wire.UserSubsystem.Interpreter ) where -import Control.Lens (view) +import Control.Error.Util (hush) +import Control.Lens (view, (^.)) import Control.Monad.Trans.Maybe +import Data.CaseInsensitive qualified as CI import Data.Domain import Data.Handle (Handle) import Data.Handle qualified as Handle @@ -16,7 +18,7 @@ import Data.Id import Data.Json.Util import Data.LegalHold import Data.List.Extra (nubOrd) -import Data.Misc (PlainTextPassword6) +import Data.Misc (HttpsUrl, PlainTextPassword6, mkHttpsUrl) import Data.Qualified import Data.Range import Data.Time.Clock @@ -27,6 +29,7 @@ import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log +import SAML2.WebSSO qualified as SAML import Servant.Client.Core import System.Logger.Message qualified as Log import Wire.API.Federation.API @@ -34,6 +37,7 @@ import Wire.API.Federation.API.Brig qualified as FedBrig import Wire.API.Federation.Error import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus (..)) +import Wire.API.Team.Export import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Permission qualified as Permission @@ -41,6 +45,7 @@ import Wire.API.Team.Role (defaultRole) import Wire.API.Team.SearchVisibility import Wire.API.Team.Size (TeamSize (TeamSize)) import Wire.API.User as User +import Wire.API.User.RichInfo import Wire.API.User.Search import Wire.API.UserEvent import Wire.Arbitrary @@ -100,76 +105,78 @@ runUserSubsystem :: Member FederationConfigStore r, Member Metrics r, Member InvitationStore r, - Member TinyLog r + Member TinyLog r, + Member (Input UserSubsystemConfig) r ) => - UserSubsystemConfig -> InterpreterFor AuthenticationSubsystem r -> - InterpreterFor UserSubsystem r -runUserSubsystem cfg authInterpreter = - interpret $ - \case - GetUserProfiles self others -> - runInputConst cfg $ - getUserProfilesImpl self others - GetLocalUserProfiles others -> - runInputConst cfg $ - getLocalUserProfilesImpl others - GetAccountsBy getBy -> - runInputConst cfg $ - getAccountsByImpl getBy - GetAccountsByEmailNoFilter emails -> - runInputConst cfg $ - getAccountsByEmailNoFilterImpl emails - GetAccountNoFilter luid -> - runInputConst cfg $ - getAccountNoFilterImpl luid - GetSelfProfile self -> - runInputConst cfg $ - getSelfProfileImpl self - GetUserProfilesWithErrors self others -> - runInputConst cfg $ - getUserProfilesWithErrorsImpl self others - UpdateUserProfile self mconn mb update -> - runInputConst cfg $ - updateUserProfileImpl self mconn mb update - CheckHandle uhandle -> - runInputConst cfg $ - checkHandleImpl uhandle - CheckHandles hdls cnt -> - runInputConst cfg $ - checkHandlesImpl hdls cnt - UpdateHandle uid mconn mb uhandle -> - runInputConst cfg $ - updateHandleImpl uid mconn mb uhandle - LookupLocaleWithDefault luid -> - runInputConst cfg $ - lookupLocaleOrDefaultImpl luid - IsBlocked email -> - runInputConst cfg $ - isBlockedImpl email - BlockListDelete email -> - runInputConst cfg $ - blockListDeleteImpl email - BlockListInsert email -> - runInputConst cfg $ - blockListInsertImpl email - UpdateTeamSearchVisibilityInbound status -> - runInputConst cfg $ - updateTeamSearchVisibilityInboundImpl status - SearchUsers luid query mDomain mMaxResults -> - runInputConst cfg $ - searchUsersImpl luid query mDomain mMaxResults - BrowseTeam uid browseTeamFilters mMaxResults mPagingState -> - browseTeamImpl uid browseTeamFilters mMaxResults mPagingState - InternalUpdateSearchIndex uid -> - syncUserIndex uid - AcceptTeamInvitation luid pwd code -> - authInterpreter - . runInputConst cfg - $ acceptTeamInvitationImpl luid pwd code - InternalFindTeamInvitation mEmailKey code -> - runInputConst cfg $ - internalFindTeamInvitationImpl mEmailKey code + Sem (UserSubsystem ': r) a -> + Sem r a +runUserSubsystem authInterpreter = interpret $ + \case + GetUserProfiles self others -> + getUserProfilesImpl self others + GetLocalUserProfiles others -> + getLocalUserProfilesImpl others + GetAccountsBy getBy -> + getAccountsByImpl getBy + GetAccountsByEmailNoFilter emails -> + getAccountsByEmailNoFilterImpl emails + GetAccountNoFilter luid -> + getAccountNoFilterImpl luid + GetSelfProfile self -> + getSelfProfileImpl self + GetUserProfilesWithErrors self others -> + getUserProfilesWithErrorsImpl self others + UpdateUserProfile self mconn mb update -> + updateUserProfileImpl self mconn mb update + CheckHandle uhandle -> + checkHandleImpl uhandle + CheckHandles hdls cnt -> + checkHandlesImpl hdls cnt + UpdateHandle uid mconn mb uhandle -> + updateHandleImpl uid mconn mb uhandle + LookupLocaleWithDefault luid -> + lookupLocaleOrDefaultImpl luid + IsBlocked email -> + isBlockedImpl email + BlockListDelete email -> + blockListDeleteImpl email + BlockListInsert email -> + blockListInsertImpl email + UpdateTeamSearchVisibilityInbound status -> + updateTeamSearchVisibilityInboundImpl status + SearchUsers luid query mDomain mMaxResults -> + searchUsersImpl luid query mDomain mMaxResults + BrowseTeam uid browseTeamFilters mMaxResults mPagingState -> + browseTeamImpl uid browseTeamFilters mMaxResults mPagingState + InternalUpdateSearchIndex uid -> + syncUserIndex uid + AcceptTeamInvitation luid pwd code -> + authInterpreter $ + acceptTeamInvitationImpl luid pwd code + InternalFindTeamInvitation mEmailKey code -> + internalFindTeamInvitationImpl mEmailKey code + GetUserExportData uid -> getUserExportDataImpl uid + +scimExtId :: StoredUser -> Maybe Text +scimExtId su = do + m <- su.managedBy + i <- su.identity + sso <- ssoIdentity i + scimExternalId m sso + +userToIdPIssuer :: StoredUser -> Maybe HttpsUrl +userToIdPIssuer su = case su.identity >>= ssoIdentity of + Just (UserSSOId (SAML.UserRef issuer _)) -> + either (const Nothing) Just . mkHttpsUrl $ issuer ^. SAML.fromIssuer + Just _ -> Nothing + Nothing -> Nothing + +samlNamedId :: StoredUser -> Maybe Text +samlNamedId su = + su.identity >>= ssoIdentity >>= \case + (UserSSOId (SAML.UserRef _idp nameId)) -> Just . CI.original . SAML.unsafeShowNameID $ nameId + (UserScimExternalId _) -> Nothing internalFindTeamInvitationImpl :: ( Member InvitationStore r, @@ -939,3 +946,31 @@ acceptTeamInvitationImpl luid pw code = do deleteInvitation inv.teamId inv.invitationId syncUserIndex uid generateUserEvent uid Nothing (teamUpdated uid tid) + +getUserExportDataImpl :: (Member UserStore r) => UserId -> Sem r (Maybe TeamExportUser) +getUserExportDataImpl uid = fmap hush . runError @() $ do + su <- UserStore.getUser uid >>= note () + mRichInfo <- UserStore.getRichInfo uid + timestamps <- UserStore.getActivityTimestamps uid + -- Make sure the list of timestamps is non-empty so that 'maximum' is + -- well-defined and returns 'Nothing' when no valid timestamps are present. + let lastActive = maximum (Nothing : timestamps) + let numClients = length timestamps + pure $ + TeamExportUser + { tExportDisplayName = su.name, + tExportHandle = su.handle, + tExportEmail = su.email, + tExportRole = Nothing, + tExportCreatedOn = Nothing, + tExportInvitedBy = Nothing, + tExportIdpIssuer = userToIdPIssuer su, + tExportManagedBy = fromMaybe ManagedByWire su.managedBy, + tExportSAMLNamedId = fromMaybe "" (samlNamedId su), + tExportSCIMExternalId = fromMaybe "" (scimExtId su), + tExportSCIMRichInfo = fmap RichInfo mRichInfo, + tExportUserId = uid, + tExportNumDevices = numClients, + tExportLastActive = lastActive, + tExportStatus = su.status + } diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index a13271b863a..ee951963bbc 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -400,7 +400,7 @@ interpretMaybeFederationStackState maybeFederationAPIAccess localBackend teamMem authSubsystemInterpreter = interpretAuthenticationSubsystem userSubsystemInterpreter userSubsystemInterpreter :: InterpreterFor UserSubsystem (MiniBackendLowerEffects `Append` r) - userSubsystemInterpreter = runUserSubsystem cfg authSubsystemInterpreter + userSubsystemInterpreter = runUserSubsystem authSubsystemInterpreter in sequentiallyPerformConcurrency . noOpLogger . maybeFederationAPIAccess diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index a4c05c44b5c..650aeb60dfa 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -71,6 +71,8 @@ inMemoryUserStoreInterpreter = interpret $ \case modify $ map (\u -> if u.id == uid then u {teamId = Just tid} :: StoredUser else u) + GetActivityTimestamps _ -> pure [] + GetRichInfo _ -> error "rich info not implemented" storedUserToIndexUser :: StoredUser -> IndexUser storedUserToIndexUser storedUser = diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index 54ff613f5e4..b19f48031fc 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -156,6 +156,7 @@ library , bloodhound , bytestring , bytestring-conversion + , case-insensitive , cassandra-util , conduit , containers diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 052c5cdb59f..c27c5ba7a00 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -82,6 +82,7 @@ import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Internal.Brig qualified as BrigIRoutes import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Named +import Wire.API.Team.Export import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.Activation @@ -111,7 +112,7 @@ import Wire.Rpc import Wire.Sem.Concurrency import Wire.TeamInvitationSubsystem import Wire.UserKeyStore -import Wire.UserStore +import Wire.UserStore as UserStore import Wire.UserSubsystem import Wire.UserSubsystem qualified as UserSubsystem import Wire.UserSubsystem.Error @@ -266,6 +267,7 @@ userAPI = updateLocale :<|> deleteLocale :<|> getDefaultUserLocale + :<|> Named @"get-user-export-data" getUserExportDataH clientAPI :: ServerT BrigIRoutes.ClientAPI (Handler r) clientAPI = Named @"update-client-last-active" updateClientLastActive @@ -762,8 +764,10 @@ updateClientLastActive u c = do } lift . wrapClient $ Data.updateClientLastActive u c now -getRichInfoH :: UserId -> (Handler r) RichInfo -getRichInfoH uid = RichInfo . fromMaybe mempty <$> lift (wrapClient $ API.lookupRichInfo uid) +getRichInfoH :: (Member UserStore r) => UserId -> Handler r RichInfo +getRichInfoH uid = + RichInfo . fromMaybe mempty + <$> lift (liftSem $ UserStore.getRichInfo uid) getRichInfoMultiH :: Maybe (CommaSeparatedList UserId) -> (Handler r) [(UserId, RichInfo)] getRichInfoMultiH (maybe [] fromCommaSeparatedList -> uids) = @@ -800,3 +804,9 @@ checkHandleInternalH h = lift $ liftSem do getContactListH :: UserId -> (Handler r) UserIds getContactListH uid = lift . wrapClient $ UserIds <$> API.lookupContactList uid + +getUserExportDataH :: + (Member UserSubsystem r) => + UserId -> + Handler r (Maybe TeamExportUser) +getUserExportDataH = lift . liftSem . getUserExportData diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 017f225a190..7c9128f96ae 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -170,6 +170,7 @@ import Wire.TeamInvitationSubsystem 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 qualified as User import Wire.UserSubsystem.Error @@ -657,7 +658,13 @@ getClientCapabilities uid cid = do mclient <- lift (API.lookupLocalClient uid cid) maybe (throwStd (errorToWai @'E.ClientNotFound)) (pure . Public.clientCapabilities) mclient -getRichInfo :: (Member UserSubsystem r) => Local UserId -> UserId -> Handler r Public.RichInfoAssocList +getRichInfo :: + ( Member UserSubsystem r, + Member UserStore r + ) => + Local UserId -> + UserId -> + Handler r Public.RichInfoAssocList getRichInfo lself user = do let luser = qualifyAs lself user -- Check that both users exist and the requesting user is allowed to see rich info of the @@ -671,7 +678,7 @@ getRichInfo lself user = do (Just t1, Just t2) | t1 == t2 -> pure () _ -> throwStd insufficientTeamPermissions -- Query rich info - wrapClientE $ fromMaybe mempty <$> API.lookupRichInfo (tUnqualified luser) + lift $ liftSem $ fold <$> UserStore.getRichInfo (tUnqualified luser) getSupportedProtocols :: (Member UserSubsystem r) => diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index bccba92ac46..8ea615fc830 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -35,7 +35,6 @@ module Brig.API.User getLegalHoldStatus, Data.lookupName, Data.lookupUser, - Data.lookupRichInfo, Data.lookupRichInfoMultiUsers, removeEmail, revokeIdentity, diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index b2967854fd6..e0f9672bf57 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -134,6 +134,7 @@ type BrigLowerLevelEffects = PropertyStore, SFT, ConnectionStore InternalPaging, + Input UserSubsystemConfig, Input VerificationCodeThrottleTTL, Input UTCTime, Input (Local ()), @@ -213,7 +214,7 @@ runBrigToIO e (AppT ma) = do -- These interpreters depend on each other, we use let recursion to solve that. userSubsystemInterpreter :: (Members BrigLowerLevelEffects r) => InterpreterFor UserSubsystem r - userSubsystemInterpreter = runUserSubsystem userSubsystemConfig authSubsystemInterpreter + userSubsystemInterpreter = runUserSubsystem authSubsystemInterpreter authSubsystemInterpreter :: (Members BrigLowerLevelEffects r) => InterpreterFor AuthenticationSubsystem r authSubsystemInterpreter = interpretAuthenticationSubsystem userSubsystemInterpreter @@ -251,6 +252,7 @@ runBrigToIO e (AppT ma) = do . runInputConst (toLocalUnsafe e.settings.federationDomain ()) . runInputSem (embed getCurrentTime) . runInputConst (fromIntegral $ Opt.twoFACodeGenerationDelaySecs e.settings) + . runInputConst userSubsystemConfig . connectionStoreToCassandra . interpretSFT e.httpManager . interpretPropertyStoreCassandra e.casClient diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index caaa7c160cc..aed64c559bc 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -35,7 +35,6 @@ module Brig.Data.User lookupUser, lookupUsers, lookupName, - lookupRichInfo, lookupRichInfoMultiUsers, lookupUserTeam, lookupServiceUsers, @@ -374,11 +373,6 @@ lookupName u = fmap runIdentity <$> retry x1 (query1 nameSelect (params LocalQuorum (Identity u))) -lookupRichInfo :: (MonadClient m) => UserId -> m (Maybe RichInfoAssocList) -lookupRichInfo u = - fmap runIdentity - <$> retry x1 (query1 richInfoSelect (params LocalQuorum (Identity u))) - -- | Returned rich infos are in the same order as users lookupRichInfoMultiUsers :: (MonadClient m) => [UserId] -> m [(UserId, RichInfo)] lookupRichInfoMultiUsers users = do @@ -522,9 +516,6 @@ nameSelect = "SELECT name FROM user WHERE id = ?" authSelect :: PrepQuery R (Identity UserId) (Maybe Password, Maybe AccountStatus) authSelect = "SELECT password, status FROM user WHERE id = ?" -richInfoSelect :: PrepQuery R (Identity UserId) (Identity RichInfoAssocList) -richInfoSelect = "SELECT json FROM rich_info WHERE user = ?" - richInfoSelectMulti :: PrepQuery R (Identity [UserId]) (UserId, Maybe RichInfoAssocList) richInfoSelectMulti = "SELECT user, json FROM rich_info WHERE user in ?" diff --git a/services/galley/default.nix b/services/galley/default.nix index 446b4c9450e..602549b250b 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -19,7 +19,6 @@ , bytestring , bytestring-conversion , call-stack -, case-insensitive , cassandra-util , cassava , cereal @@ -79,7 +78,6 @@ , resourcet , retry , safe-exceptions -, saml2-web-sso , servant , servant-client , servant-client-core @@ -148,7 +146,6 @@ mkDerivation { brig-types bytestring bytestring-conversion - case-insensitive cassandra-util cassava comonad @@ -189,7 +186,6 @@ mkDerivation { resourcet retry safe-exceptions - saml2-web-sso servant servant-client servant-server diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index ae6bdfc65a4..1db3f95e89c 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -122,6 +122,7 @@ library Galley.API.Push Galley.API.Query Galley.API.Teams + Galley.API.Teams.Export Galley.API.Teams.Features Galley.API.Teams.Features.Get Galley.API.Teams.Notifications @@ -300,7 +301,6 @@ library , brig-types >=0.73.1 , bytestring >=0.9 , bytestring-conversion >=0.2 - , case-insensitive , cassandra-util >=0.16.2 , cassava >=0.5.2 , comonad @@ -341,7 +341,6 @@ library , resourcet >=1.1 , retry >=0.5 , safe-exceptions >=0.1 - , saml2-web-sso >=0.20 , servant , servant-client , servant-server diff --git a/services/galley/src/Galley/API/Public/TeamMember.hs b/services/galley/src/Galley/API/Public/TeamMember.hs index 91956a21712..c6b7d5cd059 100644 --- a/services/galley/src/Galley/API/Public/TeamMember.hs +++ b/services/galley/src/Galley/API/Public/TeamMember.hs @@ -18,6 +18,7 @@ module Galley.API.Public.TeamMember where import Galley.API.Teams +import Galley.API.Teams.Export qualified as Export import Galley.App import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.TeamMember @@ -31,4 +32,4 @@ teamMemberAPI = <@> mkNamedAPI @"delete-team-member" deleteTeamMember <@> mkNamedAPI @"delete-non-binding-team-member" deleteNonBindingTeamMember <@> mkNamedAPI @"update-team-member" updateTeamMember - <@> mkNamedAPI @"get-team-members-csv" getTeamMembersCSV + <@> mkNamedAPI @"get-team-members-csv" Export.getTeamMembersCSV diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index b59465923af..e51070d5f5a 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -31,7 +31,6 @@ module Galley.API.Teams addTeamMember, getTeamConversationRoles, getTeamMembers, - getTeamMembersCSV, bulkGetTeamMembers, getTeamMember, deleteTeamMember, @@ -61,13 +60,9 @@ import Brig.Types.Team (TeamSize (..)) import Cassandra (PageWithState (pwsResults), pwsHasMore) import Cassandra qualified as C import Control.Lens -import Data.ByteString.Builder (lazyByteString) import Data.ByteString.Conversion (List, toByteString) import Data.ByteString.Conversion qualified import Data.ByteString.Lazy qualified as LBS -import Data.CaseInsensitive qualified as CI -import Data.Csv (EncodeOptions (..), Quoting (QuoteAll), encodeDefaultOrderedByNameWith) -import Data.Handle qualified as Handle import Data.Id import Data.Json.Util import Data.LegalHold qualified as LH @@ -75,8 +70,6 @@ import Data.List.Extra qualified as List import Data.List.NonEmpty (NonEmpty (..)) import Data.List1 (list1) import Data.Map qualified as Map -import Data.Map.Strict qualified as M -import Data.Misc (HttpsUrl, mkHttpsUrl) import Data.Proxy import Data.Qualified import Data.Range as Range @@ -112,14 +105,10 @@ import Galley.Types.Conversations.Members qualified as Conv import Galley.Types.Teams import Galley.Types.UserList import Imports hiding (forkIO) -import Network.Wai import Polysemy import Polysemy.Error -import Polysemy.Final import Polysemy.Input -import Polysemy.Output import Polysemy.TinyLog qualified as P -import SAML2.WebSSO qualified as SAML import System.Logger qualified as Log import Wire.API.Conversation (ConversationRemoveMembers (..)) import Wire.API.Conversation.Role (wireConvRoles) @@ -130,7 +119,6 @@ import Wire.API.Event.Conversation qualified as Conv import Wire.API.Event.LeaveReason import Wire.API.Event.Team import Wire.API.Federation.Error -import Wire.API.Message qualified as Conv import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Routes.MultiTablePaging (MultiTablePage (MultiTablePage), MultiTablePagingState (mtpsState)) import Wire.API.Routes.Public.Galley.TeamMember @@ -138,7 +126,6 @@ import Wire.API.Team import Wire.API.Team qualified as Public import Wire.API.Team.Conversation import Wire.API.Team.Conversation qualified as Public -import Wire.API.Team.Export (TeamExportUser (..)) import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Member qualified as M @@ -147,12 +134,8 @@ import Wire.API.Team.Permission (Perm (..), Permissions (..), SPerm (..), copy, import Wire.API.Team.Role import Wire.API.Team.SearchVisibility import Wire.API.Team.SearchVisibility qualified as Public -import Wire.API.User (ScimUserInfo (..), User, UserIdList, UserSSOId (UserScimExternalId), userSCIMExternalId, userSSOId) import Wire.API.User qualified as U -import Wire.API.User.Identity (UserSSOId (UserSSOId)) -import Wire.API.User.RichInfo (RichInfo) import Wire.NotificationSubsystem -import Wire.Sem.Paging qualified as E import Wire.Sem.Paging.Cassandra getTeamH :: @@ -500,130 +483,6 @@ getTeamMembers lzusr tid mbMaxResults mbPagingState = do (pwsHasMore p) (teamMemberPagingState p) -outputToStreamingBody :: (Member (Final IO) r) => Sem (Output LByteString ': r) () -> Sem r StreamingBody -outputToStreamingBody action = withWeavingToFinal @IO $ \state weave _inspect -> - pure . (<$ state) $ \write flush -> do - let writeChunk c = embedFinal $ do - write (lazyByteString c) - flush - void . weave . (<$ state) $ runOutputSem writeChunk action - -getTeamMembersCSV :: - ( Member BrigAccess r, - Member (ErrorS 'AccessDenied) r, - Member (TeamMemberStore InternalPaging) r, - Member TeamStore r, - Member (Final IO) r, - Member SparAccess r - ) => - Local UserId -> - TeamId -> - Sem r StreamingBody -getTeamMembersCSV lusr tid = do - E.getTeamMember tid (tUnqualified lusr) >>= \case - Nothing -> throwS @'AccessDenied - Just member -> unless (member `hasPermission` DownloadTeamMembersCsv) $ throwS @'AccessDenied - - -- In case an exception is thrown inside the StreamingBody of responseStream - -- the response will not contain a correct error message, but rather be an - -- http error such as 'InvalidChunkHeaders'. The exception however still - -- reaches the middleware and is being tracked in logging and metrics. - outputToStreamingBody $ do - output headerLine - E.withChunks (\mps -> E.listTeamMembers @InternalPaging tid mps maxBound) $ - \members -> do - let uids = fmap (view userId) members - teamExportUser <- - mkTeamExportUser - <$> (lookupUser <$> E.lookupActivatedUsers uids) - <*> lookupInviterHandle members - <*> (lookupRichInfo <$> E.getRichInfoMultiUser uids) - <*> (lookupClients <$> E.lookupClients uids) - <*> (lookupScimUserInfo <$> Spar.lookupScimUserInfos uids) - output @LByteString - ( encodeDefaultOrderedByNameWith - defaultEncodeOptions - (mapMaybe teamExportUser members) - ) - where - headerLine :: LByteString - headerLine = encodeDefaultOrderedByNameWith (defaultEncodeOptions {encIncludeHeader = True}) ([] :: [TeamExportUser]) - - defaultEncodeOptions :: EncodeOptions - defaultEncodeOptions = - EncodeOptions - { encDelimiter = fromIntegral (ord ','), - encUseCrLf = True, -- to be compatible with Mac and Windows - encIncludeHeader = False, -- (so we can flush when the header is on the wire) - encQuoting = QuoteAll - } - - mkTeamExportUser :: - (UserId -> Maybe User) -> - (UserId -> Maybe Handle.Handle) -> - (UserId -> Maybe RichInfo) -> - (UserId -> Int) -> - (UserId -> Maybe ScimUserInfo) -> - TeamMember -> - Maybe TeamExportUser - mkTeamExportUser users inviters richInfos numClients scimUserInfo member = do - let uid = member ^. userId - user <- users uid - pure $ - TeamExportUser - { tExportDisplayName = U.userDisplayName user, - tExportHandle = U.userHandle user, - tExportEmail = U.userIdentity user >>= U.emailIdentity, - tExportRole = permissionsRole . view permissions $ member, - tExportCreatedOn = maybe (scimUserInfo uid >>= suiCreatedOn) (Just . snd) (view invitation member), - tExportInvitedBy = inviters . fst =<< member ^. invitation, - tExportIdpIssuer = userToIdPIssuer user, - tExportManagedBy = U.userManagedBy user, - tExportSAMLNamedId = fromMaybe "" (samlNamedId user), - tExportSCIMExternalId = fromMaybe "" (userSCIMExternalId user), - tExportSCIMRichInfo = richInfos uid, - tExportUserId = U.userId user, - tExportNumDevices = numClients uid - } - - lookupInviterHandle :: (Member BrigAccess r) => [TeamMember] -> Sem r (UserId -> Maybe Handle.Handle) - lookupInviterHandle members = do - let inviterIds :: [UserId] - inviterIds = nub $ mapMaybe (fmap fst . view invitation) members - - userList <- E.getUsers inviterIds - - let userMap :: M.Map UserId Handle.Handle - userMap = M.fromList (mapMaybe extract userList) - where - extract u = (U.userId u,) <$> U.userHandle u - - pure (`M.lookup` userMap) - - userToIdPIssuer :: U.User -> Maybe HttpsUrl - userToIdPIssuer usr = case (U.userIdentity >=> U.ssoIdentity) usr of - Just (U.UserSSOId (SAML.UserRef issuer _)) -> either (const Nothing) Just . mkHttpsUrl $ issuer ^. SAML.fromIssuer - Just _ -> Nothing - Nothing -> Nothing - - lookupScimUserInfo :: [ScimUserInfo] -> (UserId -> Maybe ScimUserInfo) - lookupScimUserInfo infos = (`M.lookup` M.fromList (infos <&> (\sui -> (suiUserId sui, sui)))) - - lookupUser :: [U.User] -> (UserId -> Maybe U.User) - lookupUser users = (`M.lookup` M.fromList (users <&> \user -> (U.userId user, user))) - - lookupRichInfo :: [(UserId, RichInfo)] -> (UserId -> Maybe RichInfo) - lookupRichInfo pairs = (`M.lookup` M.fromList pairs) - - lookupClients :: Conv.UserClients -> UserId -> Int - lookupClients userClients uid = maybe 0 length (M.lookup uid (Conv.userClients userClients)) - - samlNamedId :: User -> Maybe Text - samlNamedId = - userSSOId >=> \case - (UserSSOId (SAML.UserRef _idp nameId)) -> Just . CI.original . SAML.unsafeShowNameID $ nameId - (UserScimExternalId _) -> Nothing - -- | like 'getTeamMembers', but with an explicit list of users we are to return. bulkGetTeamMembers :: ( Member (ErrorS 'BulkGetMemberLimitExceeded) r, @@ -633,7 +492,7 @@ bulkGetTeamMembers :: Local UserId -> TeamId -> Maybe (Range 1 HardTruncationLimit Int32) -> - UserIdList -> + U.UserIdList -> Sem r TeamMemberListOptPerms bulkGetTeamMembers lzusr tid mbMaxResults uids = do unless (length (U.mUsers uids) <= fromIntegral (fromRange (fromMaybe (unsafeRange Public.hardTruncationLimit) mbMaxResults))) $ diff --git a/services/galley/src/Galley/API/Teams/Export.hs b/services/galley/src/Galley/API/Teams/Export.hs new file mode 100644 index 00000000000..f4f3d57c245 --- /dev/null +++ b/services/galley/src/Galley/API/Teams/Export.hs @@ -0,0 +1,177 @@ +module Galley.API.Teams.Export (getTeamMembersCSV) where + +import Control.Concurrent +import Control.Concurrent.Async qualified as Async +import Control.Lens (view, (^.)) +import Control.Monad.Codensity +import Data.ByteString (toStrict) +import Data.ByteString.Builder +import Data.Csv +import Data.Handle +import Data.IORef (atomicModifyIORef, newIORef) +import Data.Id +import Data.Map qualified as Map +import Data.Qualified (Local, tUnqualified) +import Galley.Effects +import Galley.Effects.BrigAccess +import Galley.Effects.SparAccess qualified as Spar +import Galley.Effects.TeamMemberStore (listTeamMembers) +import Galley.Effects.TeamStore +import Imports hiding (atomicModifyIORef, newEmptyMVar, newIORef, putMVar, readMVar, takeMVar, threadDelay, tryPutMVar) +import Polysemy +import Polysemy.Async +import Polysemy.Resource +import Wire.API.Error +import Wire.API.Error.Galley +import Wire.API.Routes.LowLevelStream (LowLevelStreamingBody) +import Wire.API.Team.Export +import Wire.API.Team.Member +import Wire.API.User (ScimUserInfo (suiCreatedOn), User (..)) +import Wire.Sem.Concurrency +import Wire.Sem.Concurrency.IO +import Wire.Sem.Paging qualified as E +import Wire.Sem.Paging.Cassandra (InternalPaging) + +-- | Cache of inviter handles. +-- +-- This is used to make sure that inviters are only looked up once in brig, +-- even if they appear as inviters of several users in the team. +type InviterCache = IORef (Map UserId (MVar (Maybe Handle))) + +lookupInviter :: + (Member Resource r, Member BrigAccess r, Member (Final IO) r) => + InviterCache -> + UserId -> + Sem r (Maybe Handle) +lookupInviter cache uid = flip onException ensureCache $ do + empty <- embedFinal newEmptyMVar + (cached, var) <- + embedFinal $ atomicModifyIORef cache $ \m -> case Map.lookup uid m of + Nothing -> (Map.insert uid empty m, (False, empty)) + Just v -> (m, (True, v)) + -- the cache did not contain this user, so write it in the corresponding MVar + unless cached $ do + u <- listToMaybe <$> getUsers [uid] + embedFinal $ putMVar var (u >>= userHandle) + -- at this point, we know that the MVar contains a value or some other thread + -- is about to write one, so it is safe to just read from the MVar with a + -- blocking call + embedFinal $ readMVar var + where + -- this is run in case of errors to guarantee that other threads will never + -- deadlock while reading the cache + ensureCache = embedFinal $ do + m <- readIORef cache + for_ (Map.lookup uid m) $ \var -> + tryPutMVar var Nothing + +getUserRecord :: + ( Member BrigAccess r, + Member Spar.SparAccess r, + Member (ErrorS TeamMemberNotFound) r, + Member (Final IO) r, + Member Resource r + ) => + InviterCache -> + TeamMember -> + Sem r TeamExportUser +getUserRecord cache member = do + let uid = member ^. userId + export <- getUserExportData uid >>= noteS @TeamMemberNotFound + mCreatedOn <- do + let mFromInvitation = snd <$> member ^. invitation + case mFromInvitation of + Just ts -> pure $ Just ts + Nothing -> suiCreatedOn <$> Spar.lookupScimUserInfo uid + -- look up inviter handle from the cache + let mInviterId = fst <$> member ^. invitation + invitedBy <- join <$> traverse (lookupInviter cache) mInviterId + pure + export + { tExportInvitedBy = invitedBy, + tExportRole = permissionsRole . view permissions $ member, + tExportCreatedOn = mCreatedOn + } + +-- | Export team info as a CSV, and stream it to the client. +-- +-- We paginate through the team member list, then spawn a thread for each user +-- (out of a thread pool) in order to fetch information for that user from brig +-- and spar. Inviter IDs are resolved to handles via a brig request, then +-- stored in a cache so that they can be reused by subsequent requests. +getTeamMembersCSV :: + forall r. + ( Member BrigAccess r, + Member (ErrorS 'AccessDenied) r, + Member (TeamMemberStore InternalPaging) r, + Member TeamStore r, + Member (Final IO) r, + Member SparAccess r + ) => + Local UserId -> + TeamId -> + Sem r LowLevelStreamingBody +getTeamMembersCSV lusr tid = do + getTeamMember tid (tUnqualified lusr) >>= \case + Nothing -> throwS @'AccessDenied + Just member -> unless (member `hasPermission` DownloadTeamMembersCsv) $ throwS @'AccessDenied + + chan <- embedFinal newChan + cache <- embedFinal $ newIORef mempty + + let encodeRow r = encodeDefaultOrderedByNameWith customEncodeOptions [r] + let produceTeamExportUsers = do + embedFinal $ writeChan chan (Just headerLine) + E.withChunks (\mps -> listTeamMembers @InternalPaging tid mps maxBound) $ + \members -> unsafePooledForConcurrentlyN_ 8 members $ \member -> do + mRecord <- + runErrorS @TeamMemberNotFound $ + getUserRecord cache member + let mRow = encodeRow <$> mRecord + when (isJust mRow) $ + embedFinal $ + writeChan chan mRow + + -- In case an exception is thrown inside the producer thread, the response + -- will not contain a correct error message, but rather be an http error such + -- as 'InvalidChunkHeaders'. The exception however still reaches the + -- middleware and is being tracked in logging and metrics. + let producerThread = + produceTeamExportUsers + `finally` embedFinal (writeChan chan Nothing) + + asyncToIOFinal . resourceToIOFinal . unsafelyPerformConcurrency @_ @Unsafe $ do + -- Here we should really capture the Wai continuation and run the finaliser + -- after that. Unfortunately, this is not really possible with Servant, + -- because the continuation is not exposed by the Handler monad. The best + -- we can do is return a Codensity value with the correct finaliser, but + -- that still leaves a short window between when the resource is acquired + -- and when the finaliser is installed where the resource might be leaked. + -- I don't have a good solution for that. + bracketOnError + (async producerThread) + cancel + $ \producer -> do + pure $ do + void $ Codensity $ \k -> do + r <- k () + Async.cancel producer + pure r + pure $ \write flush -> do + let go = do + readChan chan >>= \case + Nothing -> write "" >> flush + Just line -> write (byteString (toStrict line)) >> flush >> go + go + +headerLine :: LByteString +headerLine = encodeDefaultOrderedByNameWith (customEncodeOptions {encIncludeHeader = True}) ([] :: [TeamExportUser]) + +customEncodeOptions :: EncodeOptions +customEncodeOptions = + EncodeOptions + { encDelimiter = fromIntegral (ord ','), + encUseCrLf = True, -- to be compatible with Mac and Windows + encIncludeHeader = False, -- (so we can flush when the header is on the wire) + encQuoting = QuoteAll + } diff --git a/services/galley/src/Galley/Effects/BrigAccess.hs b/services/galley/src/Galley/Effects/BrigAccess.hs index de7fc43bd5b..0e5725513c4 100644 --- a/services/galley/src/Galley/Effects/BrigAccess.hs +++ b/services/galley/src/Galley/Effects/BrigAccess.hs @@ -35,6 +35,7 @@ module Galley.Effects.BrigAccess deleteUser, getContactList, getRichInfoMultiUser, + getUserExportData, -- * Teams getSize, @@ -71,6 +72,7 @@ import Wire.API.Error.Galley import Wire.API.MLS.CipherSuite import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi +import Wire.API.Team.Export import Wire.API.Team.Feature import Wire.API.Team.Size import Wire.API.User.Auth.ReAuth @@ -126,6 +128,7 @@ data BrigAccess m a where UpdateSearchVisibilityInbound :: Multi.TeamStatus SearchVisibilityInboundConfig -> BrigAccess m () + GetUserExportData :: UserId -> BrigAccess m (Maybe TeamExportUser) makeSem ''BrigAccess diff --git a/services/galley/src/Galley/Effects/SparAccess.hs b/services/galley/src/Galley/Effects/SparAccess.hs index 4b9b0df882d..f84e3ac87ec 100644 --- a/services/galley/src/Galley/Effects/SparAccess.hs +++ b/services/galley/src/Galley/Effects/SparAccess.hs @@ -25,6 +25,6 @@ import Wire.API.User (ScimUserInfo) data SparAccess m a where DeleteTeam :: TeamId -> SparAccess m () - LookupScimUserInfos :: [UserId] -> SparAccess m [ScimUserInfo] + LookupScimUserInfo :: UserId -> SparAccess m ScimUserInfo makeSem ''SparAccess diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs index ef071400ab0..88fc983c8e1 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/services/galley/src/Galley/Intra/Effects.hs @@ -83,6 +83,9 @@ interpretBrigAccess = interpret $ \case GetRichInfoMultiUser uids -> do logEffect "BrigAccess.GetRichInfoMultiUser" embedApp $ getRichInfoMultiUser uids + GetUserExportData uid -> do + logEffect "BrigAccess.GetUserExportData" + embedApp $ getUserExportData uid GetSize tid -> do logEffect "BrigAccess.GetSize" embedApp $ getSize tid @@ -125,9 +128,9 @@ interpretSparAccess = interpret $ \case DeleteTeam tid -> do logEffect "SparAccess.DeleteTeam" embedApp $ deleteTeam tid - LookupScimUserInfos uids -> do - logEffect "SparAccess.LookupScimUserInfos" - embedApp $ lookupScimUserInfos uids + LookupScimUserInfo uid -> do + logEffect "SparAccess.LookupScimUserInfo" + embedApp $ lookupScimUserInfo uid interpretBotAccess :: ( Member (Embed IO) r, diff --git a/services/galley/src/Galley/Intra/Spar.hs b/services/galley/src/Galley/Intra/Spar.hs index 989df2563ed..3fede63dc16 100644 --- a/services/galley/src/Galley/Intra/Spar.hs +++ b/services/galley/src/Galley/Intra/Spar.hs @@ -17,19 +17,18 @@ module Galley.Intra.Spar ( deleteTeam, - lookupScimUserInfos, + lookupScimUserInfo, ) where import Bilge import Data.ByteString.Conversion import Data.Id -import Data.Set qualified as Set import Galley.Intra.Util import Galley.Monad import Imports import Network.HTTP.Types.Method -import Wire.API.User (ScimUserInfo, UserSet (..), scimUserInfos) +import Wire.API.User (ScimUserInfo) -- | Notify Spar that a team is being deleted. deleteTeam :: TeamId -> App () @@ -40,11 +39,10 @@ deleteTeam tid = do . expect2xx -- | Get the SCIM user info for a user. -lookupScimUserInfos :: [UserId] -> App [ScimUserInfo] -lookupScimUserInfos uids = do +lookupScimUserInfo :: UserId -> App ScimUserInfo +lookupScimUserInfo uid = do response <- call Spar $ method POST - . paths ["i", "scim", "userinfos"] - . json (UserSet $ Set.fromList uids) - pure $ foldMap scimUserInfos $ responseJsonMaybe response + . paths ["i", "scim", "userinfo", toByteString' uid] + responseJsonError response diff --git a/services/galley/src/Galley/Intra/User.hs b/services/galley/src/Galley/Intra/User.hs index 8d6c620fd66..27ced33fdee 100644 --- a/services/galley/src/Galley/Intra/User.hs +++ b/services/galley/src/Galley/Intra/User.hs @@ -30,6 +30,7 @@ module Galley.Intra.User getContactList, chunkify, getRichInfoMultiUser, + getUserExportData, getAccountConferenceCallingConfigClient, updateSearchVisibilityInbound, ) @@ -66,6 +67,7 @@ import Wire.API.Routes.Internal.Brig qualified as IAPI import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi import Wire.API.Routes.Named +import Wire.API.Team.Export import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.Auth.ReAuth @@ -237,6 +239,16 @@ getRichInfoMultiUser = chunkify $ \uids -> do . expect2xx parseResponse (mkError status502 "server-error: could not parse response to `GET brig:/i/users/rich-info`") resp +-- | Calls 'Brig.API.Internal.getUserExportDataH' +getUserExportData :: UserId -> App (Maybe TeamExportUser) +getUserExportData uid = do + resp <- + call Brig $ + method GET + . paths ["i/users", toByteString' uid, "export-data"] + . expect2xx + parseResponse (mkError status502 "server-error: could not parse response to `GET brig:/i/users/:uid/export-data`") resp + getAccountConferenceCallingConfigClient :: (HasCallStack) => UserId -> App (Feature ConferenceCallingConfig) getAccountConferenceCallingConfigClient uid = runHereClientM (namedClient @IAPI.API @"get-account-conference-calling-config" uid) diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 3d9eca78560..f814f211402 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -54,7 +54,6 @@ import Data.HavePendingInvitations import Data.Id import Data.Proxy import Data.Range -import qualified Data.Set as Set import Data.Text.Encoding.Error import qualified Data.Text.Lazy as T import Data.Text.Lazy.Encoding @@ -791,8 +790,7 @@ internalPutSsoSettings SsoSettings {defaultSsoCode = Just code} = *> DefaultSsoCode.store code $> NoContent -internalGetScimUserInfo :: (Member ScimUserTimesStore r) => UserSet -> Sem r ScimUserInfos -internalGetScimUserInfo (UserSet uids) = do - results <- ScimUserTimesStore.readMulti (Set.toList uids) - let scimUserInfos = results <&> (\(uid, t, _) -> ScimUserInfo uid (Just t)) - pure $ ScimUserInfos scimUserInfos +internalGetScimUserInfo :: (Member ScimUserTimesStore r) => UserId -> Sem r ScimUserInfo +internalGetScimUserInfo uid = do + t <- fmap fst <$> ScimUserTimesStore.read uid + pure $ ScimUserInfo uid t diff --git a/tools/stern/default.nix b/tools/stern/default.nix index 18246b4fc52..81032346144 100644 --- a/tools/stern/default.nix +++ b/tools/stern/default.nix @@ -42,6 +42,7 @@ , tasty-ant-xml , tasty-hunit , text +, time , tinylog , transformers , types-common @@ -83,6 +84,7 @@ mkDerivation { servant-swagger-ui split text + time tinylog transformers types-common diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 3e915b2a69e..53616908c64 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -62,7 +62,7 @@ import Stern.App import Stern.Intra qualified as Intra import Stern.Options import Stern.Types -import System.Logger.Class hiding (Error, name, trace, (.=)) +import System.Logger.Class hiding (Error, flush, name, trace, (.=)) import Util.Options import Wire.API.Connection import Wire.API.Internal.Notification (QueuedNotification) diff --git a/tools/stern/src/Stern/App.hs b/tools/stern/src/Stern/App.hs index e0f021a0932..7f9ba34850b 100644 --- a/tools/stern/src/Stern/App.hs +++ b/tools/stern/src/Stern/App.hs @@ -124,6 +124,9 @@ runAppT e (AppT ma) = runReaderT ma e type Handler = ExceptT Error App +runHandler :: Env -> Handler a -> IO (Either Error a) +runHandler env = runAppT env . runExceptT + type Continue m = Response -> m ResponseReceived userMsg :: UserId -> Msg -> Msg diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index f72649bba90..d226f49f252 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -38,6 +38,7 @@ module Stern.Intra setStatusBindingTeam, deleteBindingTeam, deleteBindingTeamForce, + getTeamMembers, getTeamInfo, getUserBindingTeam, isBlacklisted, @@ -66,6 +67,7 @@ module Stern.Intra getOAuthClient, updateOAuthClient, deleteOAuthClient, + getActivityTimestamp, ) where @@ -92,6 +94,7 @@ import Data.Text.Encoding import Data.Text.Encoding.Error import Data.Text.Lazy as LT (pack) import Data.Text.Lazy.Encoding qualified as TL +import Data.Time.Clock import Imports import Network.HTTP.Types (urlEncode) import Network.HTTP.Types.Method @@ -1037,3 +1040,17 @@ deleteOAuthClient cid = do . expect2xx ) parseResponse (mkError status502 "bad-upstream") r + +getActivityTimestamp :: UserId -> Handler (Maybe UTCTime) +getActivityTimestamp uid = do + b <- asks (.brig) + r <- + catchRpcErrors $ + rpc' + "brig" + b + ( method GET + . Bilge.paths ["i", "users", toByteString' uid, "activity"] + . expect2xx + ) + parseResponse (mkError status502 "bad-upstream") r diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index b7e04c9de2b..36b5a86ca65 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -96,6 +96,7 @@ library , servant-swagger-ui , split >=0.2 , text >=1.1 + , time , tinylog >=0.10 , transformers >=0.3 , types-common >=0.4.13 From d8b001646debbe510114cf5979ec2a39f660a9df Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 21 Oct 2024 12:07:42 +0200 Subject: [PATCH 117/136] Bump nixpkgs (#4273) --- nix/manual-overrides.nix | 4 ++-- nix/sources.json | 6 +++--- nix/wire-server.nix | 2 +- services/galley/test/resources/{foo.sh => generate_keys.sh} | 1 + 4 files changed, 7 insertions(+), 6 deletions(-) rename services/galley/test/resources/{foo.sh => generate_keys.sh} (94%) diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 7545ed0032b..d0aa544405c 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -84,8 +84,8 @@ hself: hsuper: { # ----------------- cryptostore = hlib.addBuildDepends (hlib.dontCheck (hlib.appendConfigureFlags hsuper.cryptostore [ "-fuse_crypton" ])) [ hself.crypton hself.crypton-x509 hself.crypton-x509-validation ]; - # Make hoogle static to reduce size of the hoogle image - hoogle = hlib.justStaticExecutables hsuper.hoogle; + # doJailbreak because upstreams requires a specific crypton-connection version we don't have + hoogle = hlib.justStaticExecutables (hlib.doJailbreak (hlib.dontCheck (hsuper.hoogle))); http2-manager = hlib.enableCabalFlag hsuper.http2-manager "-f-test-trailing-dot"; sodium-crypto-sign = hlib.addPkgconfigDepend hsuper.sodium-crypto-sign libsodium.dev; types-common-journal = hlib.addBuildTool hsuper.types-common-journal protobuf; diff --git a/nix/sources.json b/nix/sources.json index 05e854e4f6a..8bfa8cf9928 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -5,10 +5,10 @@ "homepage": "https://github.com/NixOS/nixpkgs", "owner": "NixOS", "repo": "nixpkgs", - "rev": "154bcb95ad51bc257c2ce4043a725de6ca700ef6", - "sha256": "0gv8wgjqldh9nr3lvpjas7sk0ffyahmvfrz5g4wd8l2r15wyk67f", + "rev": "4f31540079322e6013930b5b2563fd10f96917f0", + "sha256": "12748r3h44hy3a41slm5hcihn1nhrxjlgp75qz6iwzazkxnclx00", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/154bcb95ad51bc257c2ce4043a725de6ca700ef6.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/4f31540079322e6013930b5b2563fd10f96917f0.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/nix/wire-server.nix b/nix/wire-server.nix index bf1593940f4..2429a3a78b7 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -519,7 +519,7 @@ in pkgs.netcat pkgs.niv pkgs.haskellPackages.apply-refact - (pkgs.python310.withPackages + (pkgs.python3.withPackages (ps: with ps; [ black bokeh diff --git a/services/galley/test/resources/foo.sh b/services/galley/test/resources/generate_keys.sh similarity index 94% rename from services/galley/test/resources/foo.sh rename to services/galley/test/resources/generate_keys.sh index 1d57fccbc5a..5b40e8db369 100755 --- a/services/galley/test/resources/foo.sh +++ b/services/galley/test/resources/generate_keys.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash openssl genpkey -algorithm ed25519 > ed25519.pem openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-256 > ecdsa_secp256r1_sha256.pem openssl genpkey -algorithm ec -pkeyopt ec_paramgen_curve:P-384 > ecdsa_secp384r1_sha384.pem From 04ac98a2baaa1712cdb4f934167da0127fddd522 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 21 Oct 2024 16:20:30 +0200 Subject: [PATCH 118/136] proxy: Make sure Servant.Raw is the last thing in the servant API type (#4298) Making it first causes all requests to be routed into the handler for Servant.Raw --- services/proxy/src/Proxy/Run.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/proxy/src/Proxy/Run.hs b/services/proxy/src/Proxy/Run.hs index 14ebb11f691..706ce328bd6 100644 --- a/services/proxy/src/Proxy/Run.hs +++ b/services/proxy/src/Proxy/Run.hs @@ -43,10 +43,10 @@ import Servant qualified import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai -type CombinedAPI = PublicAPI Servant.:<|> InternalAPI +type CombinedAPI = InternalAPI Servant.:<|> PublicAPI combinedSitemap :: Env -> Servant.ServerT CombinedAPI Proxy -combinedSitemap env = P.servantSitemap env Servant.:<|> I.servantSitemap +combinedSitemap env = I.servantSitemap Servant.:<|> P.servantSitemap env run :: Opts -> IO () run o = do From b94e3c695a9f47b4211de3f4acd331ecfb9a7ac3 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Mon, 21 Oct 2024 17:51:07 +0200 Subject: [PATCH 119/136] [feat] Allow configuring argon2id parameters (#4291) --- Makefile | 6 +- .../0-release-notes/configurable-argon | 18 +++ .../2-features/add-config-for-pwd-hash | 1 + charts/brig/templates/configmap.yaml | 1 + charts/brig/values.yaml | 7 +- charts/galley/templates/configmap.yaml | 1 + charts/galley/values.yaml | 5 + .../src/developer/reference/config-options.md | 47 ++++++++ hack/helm_vars/wire-server/values.yaml.gotmpl | 13 +++ libs/types-common/src/Data/Misc.hs | 5 - libs/types-common/src/Util/Options.hs | 19 ++++ libs/wire-api/src/Wire/API/Error/Brig.hs | 3 - libs/wire-api/src/Wire/API/Password.hs | 43 ++++--- .../src/Wire/API/Routes/Public/Spar.hs | 1 - libs/wire-api/src/Wire/API/User.hs | 15 ++- .../Golden/Generated/PasswordChange_user.hs | 80 ++++++------- .../test/unit/Test/Wire/API/Password.hs | 18 ++- .../src/Wire/AuthenticationSubsystem.hs | 22 ++++ .../src/Wire/AuthenticationSubsystem/Error.hs | 30 +++-- .../AuthenticationSubsystem/Interpreter.hs | 93 +++++++++++++-- libs/wire-subsystems/src/Wire/HashPassword.hs | 26 ++++- libs/wire-subsystems/src/Wire/UserStore.hs | 2 + .../src/Wire/UserStore/Cassandra.hs | 12 ++ .../wire-subsystems/src/Wire/UserSubsystem.hs | 3 - .../InterpreterSpec.hs | 8 +- .../Wire/MockInterpreters/HashPassword.hs | 24 ++-- .../unit/Wire/MockInterpreters/UserStore.hs | 1 + services/brig/brig.integration.yaml | 4 + services/brig/src/Brig/API/Auth.hs | 10 +- services/brig/src/Brig/API/Client.hs | 5 +- services/brig/src/Brig/API/Internal.hs | 11 +- services/brig/src/Brig/API/OAuth.hs | 28 +++-- services/brig/src/Brig/API/Public.hs | 14 ++- services/brig/src/Brig/API/Types.hs | 2 +- services/brig/src/Brig/API/User.hs | 24 ++-- .../brig/src/Brig/CanonicalInterpreter.hs | 3 +- services/brig/src/Brig/Data/Client.hs | 10 +- services/brig/src/Brig/Data/User.hs | 107 +----------------- services/brig/src/Brig/Options.hs | 4 +- services/brig/src/Brig/Provider/API.hs | 37 +++--- services/brig/src/Brig/Provider/DB.hs | 9 +- services/brig/src/Brig/User/Auth.hs | 103 +++++++++-------- .../brig/test/integration/API/User/Auth.hs | 42 +++++-- services/galley/galley.integration.yaml | 5 + services/galley/src/Galley/API/Update.hs | 12 +- services/galley/src/Galley/App.hs | 4 + services/galley/src/Galley/Options.hs | 6 +- 47 files changed, 606 insertions(+), 338 deletions(-) create mode 100644 changelog.d/0-release-notes/configurable-argon create mode 100644 changelog.d/2-features/add-config-for-pwd-hash diff --git a/Makefile b/Makefile index faee19b3dff..7e8e2aa7b29 100644 --- a/Makefile +++ b/Makefile @@ -49,8 +49,8 @@ install: init ./hack/bin/cabal-run-all-tests.sh ./hack/bin/cabal-install-artefacts.sh all -.PHONY: clean-rabbit -clean-rabbit: +.PHONY: rabbit-clean +rabbit-clean: rabbitmqadmin -f pretty_json list queues vhost name messages | jq -r '.[] | "rabbitmqadmin delete queue name=\(.name) --vhost=\(.vhost)"' | bash # Clean @@ -59,7 +59,7 @@ full-clean: clean rm -rf ~/.cache/hie-bios rm -rf ./dist-newstyle ./.env direnv reload - clean-rabbit + make rabbit-clean @echo -e "\n\n*** NOTE: you may want to also 'rm -rf ~/.cabal/store \$$CABAL_DIR/store', not sure.\n" .PHONY: clean diff --git a/changelog.d/0-release-notes/configurable-argon b/changelog.d/0-release-notes/configurable-argon new file mode 100644 index 00000000000..b9e2a74cd8c --- /dev/null +++ b/changelog.d/0-release-notes/configurable-argon @@ -0,0 +1,18 @@ +Password hashing is now done using argon2id instead of scrypt. The argon2id parameters can be configured using these options: + +```yaml +brig: + optSettings: + setPasswordHashingOptions: + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... +galley: + settings: + passwordHashingOptions: + 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. diff --git a/changelog.d/2-features/add-config-for-pwd-hash b/changelog.d/2-features/add-config-for-pwd-hash new file mode 100644 index 00000000000..79ba9c55f09 --- /dev/null +++ b/changelog.d/2-features/add-config-for-pwd-hash @@ -0,0 +1 @@ +Allow configuring Argon2id parameters diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index 4e1e5393a2e..7c732c7b590 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -368,5 +368,6 @@ data: {{- if .setOAuthMaxActiveRefreshTokens }} setOAuthMaxActiveRefreshTokens: {{ .setOAuthMaxActiveRefreshTokens }} {{- end }} + setPasswordHashingOptions: {{ toYaml .setPasswordHashingOptions | nindent 8 }} {{- end }} {{- end }} diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index 06da5a19401..bba7408c6a5 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -16,7 +16,7 @@ metrics: enabled: false # This is not supported for production use, only here for testing: # preStop: -# exec: +# exec: # command: ["sh", "-c", "curl http://acme.example"] config: logLevel: Info @@ -150,6 +150,11 @@ config: setDisabledAPIVersions: [ development ] setFederationStrategy: allowNone setFederationDomainConfigsUpdateFreq: 10 + # Options for Argon2id version 19 + setPasswordHashingOptions: + iterations: 1 + parallelism: 32 + memory: 180224 # 176 MiB smtp: passwordFile: /etc/wire/brig/secrets/smtp-password.txt proxy: {} diff --git a/charts/galley/templates/configmap.yaml b/charts/galley/templates/configmap.yaml index 85c93804ebe..cf1426e8adb 100644 --- a/charts/galley/templates/configmap.yaml +++ b/charts/galley/templates/configmap.yaml @@ -96,6 +96,7 @@ data: {{- if .settings.guestLinkTTLSeconds }} guestLinkTTLSeconds: {{ .settings.guestLinkTTLSeconds }} {{- end }} + passwordHashingOptions: {{ toYaml .settings.passwordHashingOptions | nindent 8 }} featureFlags: sso: {{ .settings.featureFlags.sso }} legalhold: {{ .settings.featureFlags.legalhold }} diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml index f169bb0e93d..877a2734039 100644 --- a/charts/galley/values.yaml +++ b/charts/galley/values.yaml @@ -70,6 +70,11 @@ 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 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 9808bfec21e..ae843566522 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -707,6 +707,53 @@ optSettings: setOAuthMaxActiveRefreshTokens: 10 ``` +#### Argon2id password hashing parameters + +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. + +The password hashing options are set for brig and galley: + +```yaml +brig: + optSettings: + setPasswordHashingOptions: + iterations: ... + memory: ... # memory needed in KiB + parallelism: ... +galley: + settings: + passwordHashingOptions: + 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.) + +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. + #### Disabling API versions It is possible to disable one ore more API versions. When an API version is disabled it won't be advertised on the `GET /api-version` endpoint, neither in the `supported`, nor in the `development` section. Requests made to any endpoint of a disabled API version will result in the same error response as a request made to an API version that does not exist. diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 83cd888dbf9..d6db927d92d 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -134,6 +134,12 @@ brig: setOAuthEnabled: true setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks setOAuthMaxActiveRefreshTokens: 10 + # These values are insecure, against anyone getting hold of the hash, + # but its not a concern for the integration tests. + setPasswordHashingOptions: + iterations: 1 + parallelism: 4 + memory: 32 # This needs to be at least 8 * parallelism. aws: sesEndpoint: http://fake-aws-ses:4569 sqsEndpoint: http://fake-aws-sqs:4568 @@ -258,6 +264,13 @@ galley: federationDomain: integration.example.com disabledAPIVersions: [] + # These values are insecure, against anyone getting hold of the hash, + # but its not a concern for the integration tests. + passwordHashingOptions: + iterations: 1 + parallelism: 4 + memory: 32 # This needs to be at least 8 * parallelism. + featureFlags: sso: disabled-by-default # this needs to be the default; tests can enable it when needed. legalhold: whitelist-teams-and-implicit-consent diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index 2ee31511d75..81f9ddc02e6 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -53,7 +53,6 @@ module Data.Misc fromPlainTextPassword, plainTextPassword8Unsafe, plainTextPassword6Unsafe, - plainTextPassword8To6, -- * Typesafe FUTUREWORKS FutureWork (..), @@ -333,10 +332,6 @@ plainTextPassword8Unsafe = PlainTextPassword' . unsafeRange fromPlainTextPassword :: PlainTextPassword' t -> Text fromPlainTextPassword = fromRange . fromPlainTextPassword' --- | Convert a 'PlainTextPassword8' to a legacy 'PlainTextPassword'. -plainTextPassword8To6 :: PlainTextPassword8 -> PlainTextPassword6 -plainTextPassword8To6 = PlainTextPassword' . unsafeRange . fromPlainTextPassword - newtype PlainTextPassword' (minLen :: Nat) = PlainTextPassword' {fromPlainTextPassword' :: Range minLen (1024 :: Nat) Text} deriving stock (Eq, Generic) diff --git a/libs/types-common/src/Util/Options.hs b/libs/types-common/src/Util/Options.hs index f82600dc00b..7b6cd88b08d 100644 --- a/libs/types-common/src/Util/Options.hs +++ b/libs/types-common/src/Util/Options.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. @@ -146,3 +147,21 @@ getOptions desc mp defaultPath = do parseAWSEndpoint :: ReadM AWSEndpoint parseAWSEndpoint = readerAsk >>= maybe (error "Could not parse AWS endpoint") pure . fromByteString . fromString + +data PasswordHashingOptions = PasswordHashingOptions + { iterations :: !Word32, + memory :: !Word32, + parallelism :: !Word32 + } + deriving (Show, Generic) + +instance FromJSON PasswordHashingOptions where + parseJSON = + withObject + "PasswordHashingOptions" + ( \obj -> do + iterations <- obj .: "iterations" + memory <- obj .: "memory" + parallelism <- obj .: "parallelism" + pure (PasswordHashingOptions {..}) + ) diff --git a/libs/wire-api/src/Wire/API/Error/Brig.hs b/libs/wire-api/src/Wire/API/Error/Brig.hs index e5e2290b576..9c397736cc2 100644 --- a/libs/wire-api/src/Wire/API/Error/Brig.hs +++ b/libs/wire-api/src/Wire/API/Error/Brig.hs @@ -70,7 +70,6 @@ data BrigError | LastIdentity | NoPassword | ChangePasswordMustDiffer - | PasswordAuthenticationFailed | TooManyTeamInvitations | CannotJoinMultipleTeams | InsufficientTeamPermissions @@ -254,8 +253,6 @@ type instance MapError 'NoPassword = 'StaticError 403 "no-password" "The user ha type instance MapError 'ChangePasswordMustDiffer = 'StaticError 409 "password-must-differ" "For password change, new and old password must be different." -type instance MapError 'PasswordAuthenticationFailed = 'StaticError 403 "password-authentication-failed" "Password authentication failed." - type instance MapError 'TooManyTeamInvitations = 'StaticError 403 "too-many-team-invitations" "Too many team invitations for this team" type instance MapError 'CannotJoinMultipleTeams = 'StaticError 403 "cannot-join-multiple-teams" "Cannot accept invitations from multiple teams" diff --git a/libs/wire-api/src/Wire/API/Password.hs b/libs/wire-api/src/Wire/API/Password.hs index 0935b4ca5a5..78f2ea0697f 100644 --- a/libs/wire-api/src/Wire/API/Password.hs +++ b/libs/wire-api/src/Wire/API/Password.hs @@ -26,10 +26,10 @@ module Wire.API.Password verifyPassword, verifyPasswordWithStatus, PasswordReqBody (..), + argon2OptsFromHashingOpts, -- * Only for testing hashPasswordArgon2idWithSalt, - hashPasswordArgon2idWithOptions, mkSafePasswordScrypt, parsePassword, ) @@ -52,6 +52,7 @@ import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Imports import OpenSSL.Random (randBytes) +import Util.Options -- | A derived, stretched password that can be safely stored. data Password @@ -120,19 +121,6 @@ defaultScryptParams = outputLength = 64 } --- | Recommended in the RFC as the second choice: https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice --- The first choice takes ~1s to hash passwords which seems like too much. -defaultOptions :: Argon2.Options -defaultOptions = - Argon2.Options - { iterations = 1, - -- TODO: fix this after meeting with Security - memory = 2 ^ (17 :: Int), - parallelism = 32, - variant = Argon2.Argon2id, - version = Argon2.Version13 - } - fromScrypt :: ScryptParameters -> Parameters fromScrypt scryptParams = Parameters @@ -142,6 +130,16 @@ fromScrypt scryptParams = outputLength = 64 } +argon2OptsFromHashingOpts :: PasswordHashingOptions -> Argon2.Options +argon2OptsFromHashingOpts PasswordHashingOptions {..} = + Argon2.Options + { variant = Argon2.Argon2id, + version = Argon2.Version13, + iterations = iterations, + memory = memory, + parallelism = parallelism + } + ------------------------------------------------------------------------------- -- | Generate a strong, random plaintext password of length 16 @@ -154,8 +152,8 @@ genPassword = mkSafePasswordScrypt :: (MonadIO m) => PlainTextPassword' t -> m Password mkSafePasswordScrypt = fmap ScryptPassword . hashPasswordScrypt . Text.encodeUtf8 . fromPlainTextPassword -mkSafePassword :: (MonadIO m) => PlainTextPassword' t -> m Password -mkSafePassword = fmap Argon2Password . hashPasswordArgon2id . Text.encodeUtf8 . fromPlainTextPassword +mkSafePassword :: (MonadIO m) => Argon2.Options -> PlainTextPassword' t -> m Password +mkSafePassword opts = fmap Argon2Password . hashPasswordArgon2id opts . Text.encodeUtf8 . fromPlainTextPassword -- | Verify a plaintext password from user input against a stretched -- password from persistent storage. @@ -190,16 +188,13 @@ encodeScryptPassword ScryptHashedPassword {..} = Text.decodeUtf8 . B64.encode $ hashedKey ] -hashPasswordArgon2id :: (MonadIO m) => ByteString -> m Argon2HashedPassword -hashPasswordArgon2id pwd = do +hashPasswordArgon2id :: (MonadIO m) => Argon2.Options -> ByteString -> m Argon2HashedPassword +hashPasswordArgon2id opts pwd = do salt <- newSalt 16 - pure $! hashPasswordArgon2idWithSalt salt pwd - -hashPasswordArgon2idWithSalt :: ByteString -> ByteString -> Argon2HashedPassword -hashPasswordArgon2idWithSalt = hashPasswordArgon2idWithOptions defaultOptions + pure $! hashPasswordArgon2idWithSalt opts salt pwd -hashPasswordArgon2idWithOptions :: Argon2.Options -> ByteString -> ByteString -> Argon2HashedPassword -hashPasswordArgon2idWithOptions opts salt pwd = do +hashPasswordArgon2idWithSalt :: Argon2.Options -> ByteString -> ByteString -> Argon2HashedPassword +hashPasswordArgon2idWithSalt opts salt pwd = do let hashedKey = hashPasswordWithOptions opts pwd salt in Argon2HashedPassword {..} 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 e1f92b07998..4c8282f8d71 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -151,7 +151,6 @@ sparResponseURI (Just tid) = type APIScim = OmitDocs :> "v2" :> ScimSiteAPI SparTag :<|> "auth-tokens" - :> CanThrow 'PasswordAuthenticationFailed :> CanThrow 'CodeAuthenticationFailed :> CanThrow 'CodeAuthenticationRequired :> APIScimToken diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index f55459746f7..47f3e5d56e0 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -34,6 +34,7 @@ module Wire.API.User SelfProfile (..), -- User (should not be here) User (..), + isSamlUser, userId, userDeleted, userEmail, @@ -583,6 +584,12 @@ data User = User deriving (Arbitrary) via (GenericUniform User) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema User) +isSamlUser :: User -> Bool +isSamlUser usr = do + case usr.userIdentity of + Just (SSOIdentity (UserSSOId _) _) -> True + _ -> False + userId :: User -> UserId userId = qUnqualified . userQualifiedId @@ -1405,8 +1412,8 @@ instance (res ~ PutSelfResponses) => AsUnion res (Maybe UpdateProfileError) wher -- | The payload for setting or changing a password. data PasswordChange = PasswordChange - { cpOldPassword :: Maybe PlainTextPassword6, - cpNewPassword :: PlainTextPassword8 + { oldPassword :: Maybe PlainTextPassword6, + newPassword :: PlainTextPassword8 } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform PasswordChange) @@ -1422,9 +1429,9 @@ instance ToSchema PasswordChange where ) . object "PasswordChange" $ PasswordChange - <$> cpOldPassword + <$> oldPassword .= maybe_ (optField "old_password" schema) - <*> cpNewPassword + <*> newPassword .= field "new_password" schema data ChangePasswordError diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs index f376b73704f..2fcca87a73e 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/PasswordChange_user.hs @@ -24,8 +24,8 @@ import Wire.API.User (PasswordChange (..)) testObject_PasswordChange_user_1 :: PasswordChange testObject_PasswordChange_user_1 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\SOHf;0B+CKY\1040633W\ENQ\178683\66681\1079258\1036336f\SOH&\166643\1050584\1022602\1091853\145882\tX\190821\FShl\1020866^A\153646=\151238\EM_ow?\\h4\155435\68388\SYN\11851\&4O\NAKnJT\EOTr\CAN~\ACK\ETBR'\992244sX\1001766mlCHvg\1112425ba\1003664R\\\1034092o\989312\1056334*k\r)H\180403\1051096\n#\14366~\\9Q|\v;\USd.\1066580\&0SHP%\1019462\22215'!\1044148N\SUB!Kz@\NUL\74079\1087771\SUBVp\1100111\38836\STX3#\DEL\DC4}}\1094237N\120442`\169346\&7\1036101\DLE\154725^\STX{`i:\rUT!\DC3\1111700\152543\NAKWK\NUL\1098445\1102182eA\140938\ETX\172001\1034473t@?\1014650\SOHJ\1074486\&7\RSg{\78258\&5R_\DC3u\SI\153435\1082441`}\DEL\66836X\DC1\175200D\25079b\176836\&6T\141840\167124p*7\n\\'\vO#\FS\174827(H\NAKn\178850\1015713}2s\143401\&8GA&\1004513\CAN\1068132d\9056\SUB\1059104t @\1056816I/\175842\30192\DC35\28889c\EOT\1046281\22594Uk\SYN\DLE\1099103\&8\GS\1034138\94316R-x\999901\1007697\1008634\DLEO,Z\ETX\1073959\63275f*\f^>\EOTD\r\SI_AQPO33\96451/F\RS\185177y\77854|Fn\1010492E<\1047147\&9\ETX[y`e\168776\65402L\SUB@4i/*\1011887\1102541\9070Ih\SIC1\1031432\t%?kFt\ACK\DLE\US\GSN\171039\f\1094027:\aV\ETXj\18014\SYN\SYN\150071\EMK\1083674\162115\40502Uez)\1080936\FS)8vT;\GS\21613ay\ETX\SI\GS{C=\EM,\SOH\ETXO\162859\ETX&\SOH2%<2s\f\SYN\r6ivo{\1028087WN\1053937R\1039894\1030129\995717\98891[ :\USu\180666^f\1087790\CAN\137895\183333N\SI\145270\EM@pK\1078668\&9\r@Ze\152611f\DC2x\59319M\30205;j\SYN\29669K_~:v&Dpx~_\STX:b;bv\DC3=\14812\&6\SUB\41242\ESCy0Ho.B\"u*{\1018548Vw\SUBW\138263\173995rbY\51982I{q\1041374\ENQ&_Pt\182926'\917559\&5\v\150891\35898\35323Ue@YM\164633)\n\EM\GSn\EOTZ\SO\DEL\\\f`f3T+_\RS@\a\RS\186662}" } @@ -33,12 +33,12 @@ testObject_PasswordChange_user_1 = testObject_PasswordChange_user_2 :: PasswordChange testObject_PasswordChange_user_2 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "7\16927K>\97741\186669m\vG9\tO]kp\63012\SUBVQs\t\984613\1108746\ENQ\1021022!O\998098\EOT=\abrgK_D\1033730<\SYN_\1100470\1086629\ETXH\SO#w9" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\r]Gy3T\1026217\EM\1020078|R\\@\1056800Xq\155479t\EM\NAK\45450\1031406GU+p\1028583\1037856G46\1111047\a\145730u\EM\SI)@\2452\nk7\989251\22005D\11178\1075520\1105369\&7,h\154963\r\1014527\&3\a\13276ki\SIuUB3=X$\138590]\1046903\bSaAr8*t\DLEX:\1023144KA\SYNu$^rK~`\1062546)\174565MJ\1062282\1020633\SOk\SI\EOTF]\DC2\997860\b\CAN\f=p\1041758&S`\b^\179839;S\\\DC4N:\SO\f\NUL\1076187\&5f\127761~K/\ESC\137715*:\1033030\ENQB5\158024\NUL~m\DLE2\12820\1079647\NUL%\DC1{H" } @@ -46,12 +46,12 @@ testObject_PasswordChange_user_2 = testObject_PasswordChange_user_3 :: PasswordChange testObject_PasswordChange_user_3 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "\DC1\1008131\"iQg2?.V\nR\ad#NZu\SI\154091\"B\USm\1066170\DC4?_-%\SO\DEL,\b\SYN\78542\1070480%U\RS\95262%1\2330\STX#\SUBV-\163363J\142686O\ETXV\DC2Ga\DC3,\1094317O3J\1098970n0\1052934j|\23339cF?\1019037x-\1069855\1094636\&160jp^\179153\FS>\\&\ENQ\EOTg\62450]\1073387\RS\169810\US%\990256\1042714$\985984R\1044140'-^I\1083467 kT\bZ\999047F\t\1084750F8R7\SIYN\EOT:N\SYN\SI\vd\57930Uo\1017473\1052974\vi/KA/\1004923\1051639\DC1e-\47612E\SOH\SO\v\SUB\1057038c\1090019\1003618Z#\991058e'\RS\120431\"\CAN\EOT\SYN?wO\1084580\DELI\2368\1005674\1041651gYJ\147444\&9p\CAN\187441Rn1\187124A\GS1x)\146547k\23622\DC1%S\1016329C>\134586\19597G6\1003504\RS\97878~\996492avKH\GS<\1082858sNVe\7956\152082\DLE\188847\f\ETBmpc'Xi&\150774E|V\1073099}\"\NAK5\96146&\t\f\DC4l|p\a\1024356\1036737UOM%a/9\r\n\1095590\1055708P*K\1073690%\NAKyXE\165112\987387L\DLE$f\ETX\DC1L\\l\11245\49768\\Q\SI\1002707()\58946w.\172820\SI.&\31267nk\vF\143976:\1038638\2606\1016120\SO\RS^\ENQ\DC4\b\1035479\1045289\RS\EOT\EME\1072274I\"W\1104244l\ESC\131418fB\23703+R\1113063\59494?\1061998^TD\46012{k\181947n\60196[|g;\71853\1095649\18432\173156\16164` \9356\1082477\174851dT\1015692p_\13046tN._\1042851\\\52588T%\98330\DLEC\96142\1019008\9148D\NAKrPk\170211\SOwLe\1032698\20202\1022050Jj" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\EM\GSjg\rcpq\ENQydjzBvM\EM-\181560x\62219qO[p\RSF\1043280\&2\DC4\160364\1019066\EM'L+z\SYN-K\94385\&0\ENQ\US\ETXP\DLE\ESC&h8\141548\1084128c\143493\1009984 \a~=|hx\1031253)V\152928J\991022!@;*_\US]\ESCd\FSI_\nK[\DC1\b/dS\1020193v1(h\ETX\152908-UL(U\nm\1062628\\\1049985\t>\FS\EM\190594~*$\1056230\31211\148228\991805$ch\ACKyCFOIo\DLEvHeF%\168128\&8w3I-\77839(\177181\161298r.\998529s>\155909@\ACKb\EOTa\DLEf\68669_;[-.\1058443q\GS9\SI\145931U\1085428\CAN\ETX\SYNbfMq3]N\160390of?\987479\&2QU#cY\DC2\ETB\a\134728\&8c`\DC4-\1035600\&0_,\61186\DELd^\DELM\1082727\&3\NUL\SYN\DC2`\DC2(z\1073614R\1073511\158846Vqn\94033\CAN\186179Ap,\68655~:>9\SOH\986818L|\26590\984726\&6\1020946c\31513^\1077430\NUL#\68875\7357SD\t0\GS^P\nmg,oVnT%\1074906#\1079052\185568f\32331\FSG\NUL\aPl_\EOT \1071732x\DLEZ\EM\SUB\DLE\1082444\CAN\9126\NAKnSq^lw" } @@ -59,12 +59,12 @@ testObject_PasswordChange_user_3 = testObject_PasswordChange_user_4 :: PasswordChange testObject_PasswordChange_user_4 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "\996042\1013508\137442y8z\188910cH@ge\96750\bJ\ETB\RS|Z5j\f\USJ\DC3\27867#\5822 =\ETX[[#ch1T\SUB\1062971\159509Fr#\CAN\1067286=Q\1110054\162733\n\RSL-X%\1070501h\1065080\1089630xDM'\12493aC@n \SYN\RS6\"e\985673\1062591\ENQE'H\DC3\131230o%5\996218\DEL{\1010852/KU\STX[\153798\1110733/#\1111047H\DC2_bNy\1099854\vH\RS]\986900wdg\1006378\ENQ4|\991191%\GSzgvb?QC3U\vOL\1090175\1009217\171249Us\985275\1007556\1056022y\ETX\1006666a\1089443\1064461IQ\NAK\1102475/\1025821[\146525Y\1110273\&0hg\NUL~:x\DELd\fZDQU\SO;\a9m\\~\f\167899=*|0\1089233\40380R\FS^\70516B2\DC2\1019556y\a\985058/\129335@>Vh\40618\1019580\DC1h4\n;Q&P\DC2A,f(B\SOH\1028143 \138873\1052427\f\140570$3\158205\t\t\DELs\133507Vp\SUBnDA\nsv\151492!'\1098710\144726X\r2\139117r\186851!@\51165\DC2\1073571%\1026015o\"\bi\1075769tV*\1089261\1000193\SYN\52519\1026058i\"+jB-g\40752\RSL\v'\1089204Faf\988489^\997807\69921E\fo\1041666\1032996_\1042556'\1071888\&9 ,F\95367d\121251\161394\DELY\7850)\n\RS(^\"L\GS\993283\1028777.@\DEL0\DC2,w\136018\ENQ:U\US3\1074021(\26102B>\SUBLh}8\36317\1071795\&1\DC3\FS,\NUL\1036218\164959\ENQ\1101169:\1105205J\1060042\n\NUL f0O\1023842m\36567\ETX\b\STXg>bl\1028623\44691p\SOH\45834\ACKE\NUL:fQC!K\1013456\32733(Va]\FST&B\EOT\b_#`\1041118o\DC1\165469\CAN\DC1>\138365H\1018054^\983454\SO\1088879\1112501H_a\1019703M\1094145taIx!c\64005\ACK\GS$i[\147426r=\ETB\30388Dbpc\GSt\96715\51391\25397\1098750H\1008635U..\160586\136531K\131733M`u|V\1083030#s\7110v\EMP\1008700h:H\ni=\150174\49091\ACKK\63386A\SI3b\EMd\EOTk.t\FS]f\132877\ETB\22782\DLE\f\1013087}6\17773l\\\1063285D3\DC3\USa?FR sHm\ETX\1105953\b[\DC2\STX\1091150\78391O|#\STX\GS+\145799\1109990Tf\6422\1036975`\SYNNL\RS\144764\&9\SYN\97231\988154\EM\1019553\ENQ\989472.DKMf\991253}c0\US\rFZ\1025650.\1068209SK\DC3Isq$>\128748\149897^+\1101484\1014800\n\ACK\"^\177274N2Uo|\GSM\27950|nZ\1078716G\tQ\41315\1068764zzGp\FS(y\22194\13258Wg\1110206\15989\ETB\a\142998\83001K\1041605\140118\138647\1044203G\1017800" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "6%CdZ\NAK:]a\160757G\1100807\aHT_\ETX\184817=\1094974H\NAKV\n\188284\43568\SI`\GS4},m5C,)pOU-z,m)G\1085731:\13371\68388\58925\NAKVBlt:'\SYNr\161012\&9\SYNZ~\53239\r\131378\"\b`}l\ENQg\49807x\EMsO\SUB\r\ACK\ETB\1012862*\119158\ACK'\NAK0u\1063315+.N\155568\&5\DLE\996563\1059464\1095806OE[\1066634cJJZ.>#mY=\DEL \SO\1020809\161961Fi~8JT\RS\t\DC2[E\70439\SYNO\SI@\1012929J\STX=\DLE\SYN.\1056562#da\1101967\SO\FSrCR\SOH_!\ENQ-Wtm\140222u\STX\1093627\&1\SYN<\1086071Y\99519F\1092290\174518\165124,H\152431 \1016376\1086967\65320\1078045\100936\161880\64562\n\RS07e\DC4.\1017260\USl-W\1036127\169524_\rSidOZT%46Me]\bf3X\SUB\30968\r]E%uW\1037702\120955c`\SI\1084987 a`/\DC3\1066414}\EOT5\EOTCMY\SUBY\1018010l>xH\RS\169677\26707}vy\SYN\DC1 A\FS\15039\&59N\29728\1000117\SUB@\1007505G\187702!xi\59210\&2JK\ESC\a^YMk\CANQ\t0:\DLEzo\NUL\DLEx1gU\SI\1005915\&8\142146P3V&\146215l)A\168185>\SI\tz\40878R\171716,\rLb\187682\CAN\983254_G\1019834\1008637EY;\20022\DELNvs9fmb#\1103912\46381g\1086578\54419\986014fJ\60290\v\1003578\180699SFA\STXA$\188361\135582\NUL\RSA\1069366E\SUB~\997873t%.P\t\SOHN\23780=\1058283\ENQ*\42808Fm\987705^gW\STXBN?\1062464`AUpn\SO\58276i\ETB}\NAK\35802\&8GN\71264FAxE\\&q^al$\1099577\DC2`z\67120\131492f\ETXnux\149811q\FS\CANy\ACKb\1075992\61816\nWZ\24019oFZ{to\EOT\a\58806\b.\141033\1061510/'\\bL\ETB{fp\983623\1076286O\46626\GS8\1055057\1088721;Z|< \153326\1088059\1111453\&4aC\SOH\161524\SUB.*K\"\"\129454Y\167276v\986403c($`\SOHK4d\125249\FS\122897L\992931\EM\1063797/AnK\163512\&4\44876:\FS\1071653\1048482$\DC3/Ug\143227iyBpz\CAN\ESC\50988M\153299\t)p?\160170[{K\1064379C\187515/i\129567\1015971k\SOVyO\EM\1027000_\SYN\1092978\137534\37394\ENQ{+\150519\CANp\EM\120158\DC3\1039610}\ETX\ACK\rpf&:\SI\EM{[\47214\141578Pj\DC2\1042947\175183;\tz\13562\&57*\ETX\149429\a\1099670`\rM\b\1065597\a\1061713W5\146248v\61801 \63453>Z\127207\177364t\SOH\99385 \24048@Vd\1098979'6`/\RSv\ENQ<\EM\1046071:74s[\SI^rcI55&\DC4(\1044403}5\1072105\t\SUB\1019144g\1055613\ETX[\1049131\1027231\v[i\1106618\ESC$\574\31775#\bq\1086447T@8\183810\1018524\1080923\DC4.o`\f_27^6>\1018938\20504s\175505q[\161155aeG\1042361HB[\FSs\92188t\RS2)[Qc?-\1006821/z\993159-\US.k\32238\DC4Bc\72192c \b1\SOHCE\DC47\171040\ESCw2.{\1014032~|,\EOT^\1106499}x\1099466\ENQL>>P\168482,.T\1049248`\1106998b9u.(z=@&b|\1039337\DC2\21581\ETX\SI~\vz\159863E`\FSe\US\15482=\"QwNN\129353lzq\190036eiq\SYN-d\137123.-n\SOH?B( T=wf\995467\RS\DC2\179872\SYN*|\147417DM\37567*:\STX\189754AS\t7o\64289(\994294O~kP\68006z\f\ESCe\a\987232I\DC4\DC3!EL+G[\r[_\1091777\9539W<\131337\1098445iA\168912'F\RS(SE\SI^\14294l\1054709\US\DC2\SOH\n\41372j\DC3\156318c\17177DN%\140618&\1004034\ACK~^\35003\58010\EM\991741&D\156963\NAKP\SO,\SIk#\bb\33222\t\33164W\54708\b\169426li\155619my\GS\133694f\US\58936z7fFkx\181089=\96578\ETX\1045516;pk\1103897\1096717.\SOH9j\1106500$\1083366\DC1oc" } @@ -72,12 +72,12 @@ testObject_PasswordChange_user_4 = testObject_PasswordChange_user_5 :: PasswordChange testObject_PasswordChange_user_5 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe ".vT\149065\158692){\74556g\1110206\1091301s\1012653\157052\ENQ,~\ENQ\132963\ETX\SYN\EOT\1103480\155219=1\DELo\989094\ENQ{}S;\DC1 \NAK\987299Y\129547Z\GSqPuPV=I\FS\ETX\1043494\60432\SOmRV` '-#\RSGcv]\47869>~ \ETBN\DLE\155012\1109063\181243\1002700\NAKF\CANu\US\43300\78315\DC3\1075822>z\\.k\SYN[?\\\100534\&3TJWN\1033469\\\23429=PJh1\991408\1081195\47549\DC1\1021540\1100099\36799\99980yyf\SI{%a\CANK\165733?Wl%\185431;Y\"\177839~\SYN\124986n2*F\983249\&0\63886Z\ETX\SOnp\t\1008554rz=x\DC3N\50460*8\tj\1085763\1069586\1021364R>\1094815\ACK\1052450Q\US\1016757`kE [\94447;\94463c\bK\f\1003111-WW\SOHr\SOHndW=s\1064135\FS\SIO\176630\142291\1022975l\14890<\SOGx'Z\41402\26364\1054258\STXW\1047089\1022246hS\144850\EM\134018k`mW\58467\25020\&9\DLEE\995366XON+\DLE`]\SO~g\1044869,9\t;\DC3\1050886\3363pD6s\157184\ENQeem\1045132\SOH\24377Lo\1082536ctA\DLE\1113917`B\EM\94062[1<:}7]&\v\44512R\177157a\RS\63093\&6\FS\10794f\ESC\1076238\52233v!t\DLEG\1015620\\f\t^\SOHB\SOH\180364_N+\v\ETXux\NUL.d\2283\STX%{\120714\1085733\134796\1048671uO\1061770\n\EM\r\a\r\36309\DLE4\1043749Hp\1091440q\1079376bJT<\STXVw\985328I+\1034709C\t\27376\SOh.,\1103086:\917965\9480{\ETB\995773zqY\STXE\GS\51683\&8vF}\170082X\42566\983317U\NULWGiN*v\173195\162226\154581\fR?i\1049259\DC3\a\DC3O(\187320xa\NUL.\133821\1058197\1098767j`\"\64700V\176930\69639M+m\STXJx\FS\GSrVs\SYN\GSJ8Q>l\tJj\EOTDGHj\CAN$X\RS\119922D%\DELV\EM\SO8\988454Z%ah\1074629\2919KB\1036581\ETX\SIP\1041071B\142456\ACKe\1093894Re2\1077169}Q:\1006282C\ENQ\1034308<\170708yS=qL\SUBd0{a\2279s\1075662R\1019777\133916NS7\SYNG\1052457N6Z\1026683\1010570\36133mP\DELO\RS 3\1004867G\96938:,\991792\US\1040258\ETXpNgH\"i\190411\169538\CAN50H\RS\51809@jiHF\18488\1089326S=#\EOT\24653&M\186999\ENQ\188436sB\EM\NULVuJ%wk\US\USJY\US\SYN\DC4@[\133710\2562\1102116\170261N|\25196L/Fs\EOT\b~khlJ\\*\1083562tv3\STXsg\ENQ\DC2c>\48829X\985867\1024387\nRg\NAK ;\51240'{~\\\1070452oSr3\DC1P\998414K,\1058087zN\r8\27838\\\165356\SUB@\DC1\SYN\vF\DC2V\ENQU\1077217\ENQy\1105981alR_\73963W\SYN-V\DC3\1058513|\RS\NUL\14311\1069223\DELV0\aHa\162915?PXj\SYN\DEL\985879\49021&t\"V\57972hA\42234\t8\NULk.\189070 \1112762\ETB\59270\185654U\GSQ\1063565\44619p\1061081\GS\DC3\1108003\NAKKR`\57737\40884\&6\r\101067io@o\ACKkrla\174009,\1070019\&0A6#\CAN=\DC2\DC2\161497&\RSan\149845\&6y\SYND\22050a\f\149068h\162218\&3\ETB\178246#O*\ESCy\168142a\5632b\DLEL:\axng6\59689\&1\1040365\181996\65902\ETB%\164339\ENQtfJq\1045673\&2T\SO\DEL\126474q\NULq#\1012957\1002852\\\r\DC2P\1024058(C\1050472Ph\GSBthwz+'\EM" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "S\1034541[2\119932\&0h!\1064848.\987603*P:\NAK(\GS\ETB\24253$X\179274K\NUL9\b]\170369\1020647\1051557K0\138808\DLE94_\26880\SUB\994542a\176494((\998954TXm\DC1_}\994198m\\o\120794yJ\ETX#\b.K,\151241\126477\r:bj\158134.\14517[Dg\49015\152214BH mH\181369\990387[QnJ\EOTo\\\98736*r\CAN\984313m\146285s\SUBD\17341{A\78451p[\131098U\RS\ETX\"%Y\1089637\v%\21671\1105935e|\67637\DC2\ACK,}\176528u&\v\1067595\US@8+\917796?shAqmaA\DC4I\NAK\988836\SI\SUBl\at\1097599\14469vd\187527s\ETB\SI1,\1026043\1092581\68088\10003U#\NAK\NUL8\993973\SOH\165172\178585\&25L7l4K\ETB\ETB\US\183298(\141108\CAN\SYN`!=d\16001\DEL\37607\990640g \1007747\"\DC1\1035551)\ENQG\1075268JZ3\29025/\147766\RS\ACK\28620\DC2\DC2i\DC3\NAK)\DEL\ACK\NULXs`\15691MUmZ!y3\1107617\188523`n\USs<)n9\1030989\GSB\1029508g\1055800\DLEz\ETB\110827T3F\140208\&3\1088347\SOHnx\a\57612\&07\DC4;H%\SI\SYNsQS{%\tj\46313czj\DEL&\DEL\r\1044089\165481\190596,\12670L" } @@ -85,12 +85,12 @@ testObject_PasswordChange_user_5 = testObject_PasswordChange_user_6 :: PasswordChange testObject_PasswordChange_user_6 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "&\1107610,\EM\DC1\SO\GS\NUL\\Q\143835?&%e\751K\991656g\142735]6\1072568XDu\989822.N-Y\SYN\118870\NAK\177961\1082599Z\1067051\41571\"IIzef\172747\157154\1100946\US+h\1063035\156268@T\n\DC3e+ FG\1040063}\1007879p[\1019675~s\58897W\1002225\131986[h\163754$\1014199d\1001302\135635\1083326\r\15121\&9\1054919\SUB\1033452\48331B\146637\1071032g\RS\34856\SIpN(\n_\DC3\CAN\ETB\994340u\"\1055984cq\148292%\168571@_vDc\1073055CW\ENQP\136867\STXHfp;\nM\1110028\154200)mg\1000362}l\1072450h\t\ETX\14968Q\1021295Tj\\b4\FSK!J\\\996951\1037918\ETX\16997[$\1006298f\US\FS\97025h`f#cq0t=4\DC3\v(n\CANb\SUB\CAN\FS\RS\US\157568\1112545W\ETB|\DC4\26469\SOH`\152656O*E_\1014509_4Lrc\1067039\68473\FSE[\GS\95227vbvn\121463\176466WFW^\1109674\&2\1092465\1101465l=\191025\1020663R\1107046p\189999+T\36798\vy$\EOT\184549LpY}\EOTZc\118805zLS\1099150\\\119989\&9Gzc\120792\1050858_\DC2H!#\169248\DC2d\177928!229\NAK\ACK(\1096427c'\142061\"{\b7\tM\63131,#IRi\1091628\n\994326\155033`\DLE \ENQT|!\1097357p\CAN\FS\138789\STX,\94330\r2\1082495s\1097275\\|\35843\ESC\1078746o\DC2i\b\11053gkx\994356kd\1066993\EMi|\13736\65150\160960\ESC/\1010989\&8\1069363:j\1028017\"RM!\96723`()\63658\&2\135558.\1049513B\171714E\1017316\1070909\1028371\RSR\NAKJn\1032860[VZQ\127514W\NULiz`Ie\1058604I\DELMY#)R6\64879\178752\&2b6\tX\r\1048312\1069402N\171772W=\STXAS;q\123203\1083930w\a\SYNptS\NULT\fj\143164\194759T\fSp\68448\ACKR*In\DEL=X\NUL\66188\vM7\121298s\1024216v!\1084042?\1022676I\1082108hQ\1062292.\SO\\\151754j\147624\a\1077885\ETX\1074145bE\1091072\ETX'\1023670\48208\SUBK\GSP\DC3\1081278\DC1\1085046\159684\a\139723n&\1108740Z\ESC\179659 wA>\141155\NUL-\NUL#\"|\165468A\t\ETB\1041615_u)\165061\143580Le'%*\1107600Q\SI\RS\111344\"2\vc3yVbV\1042395\\e\168551\STX\1090925esJYso\163169o\FS$I\1091068oz !\DC3\119098r\RSzt7X\8274%$-\1046768?\SUB\ACKA\SUBkZk3E\t\1067050\ETB\1019523\&1D-G}\1056157\&2Y\DLE\a\at\GS\7200\nQ\182489\1094286et\USK\DLEv\tN@?}>\CANz\987816j>w\DELc@\EMw" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "b\170229n\1018971\SUB\985694C{_\70741&\vMo6gl\STX\"8\1028959\GS\165216\SI(\138759\ETB\DC4\152692\&1\996992I]\v\RS#\"\1112677&aGfR\ENQ\1018847\&1:\ENQ\183934\1047759\&4J}\GStZ\DC31b\by._\26020\165646bK\SIE\1034932\149906dq\128297\"\"\67990L\94037\SOH&\DC3AIx\1009451z\11657z_\68118<\1083603=>FA5Q\1025568]\SUBS\1067075\t\1027792b!\1011202PY\1058512\SYN\1097813\aCK\RS\151398>\1637;u\ACK\ENQ+n}NzAP\69805RXe3Q4+!5-{\43538xEx\173125\USU\ENQd\187890,\DC3]c\164824\&9YkkCUR\1052545zz\a_\US\1052913\FSh\bX:\1097581\EOTs0DC5.UEt\1082356.~\51232\188301#\993299\DC4LwU\27171\1014215q(r_\SIAq\7019\ESCg \1034226G(\SUB\b/)K\1022799\1006348z\166521\&6\1073570M(mHu\1072369B\141057}^Xz@\27397da]VDH=xZ`E|\SYNEr$P\b\SOH.\30581;8/E\1056666\156080G+\USF\1046048\189590\1079895^\1072919/\DC3\SUB\SUB\STXy\ETXl\1012320<\f\159886\DEL\EOT\47816\&5\1010161 .u\ESC\f\1084279a5\US\100760h\988443\20830_\1112230y#]R$a/\SUBg-^:,\134242t\NUL\DLEFd(\SI_r=6&R\39368t#/\30862\1083006\55251\DELd\139094\f\bLL\SYNrl(\95410_jgp^B\148359pZf\131184!\1100088\1079773\191219/S\60206\157985\SIP!\SI\1030276We\DC1Q]\DELMi\SOH2\164247s\64188\59175\179637\&6_fIJ0-E\51588\39286 \b#\99545\52587\GS\1063696\57533\1094025C\1039590\STX[m|O-)\54684\132598\189752\FS\FS\31494\v^r,^PBK\175477r,U;p&~O\1003644\154009\DC1*/f'L)\146351b1mmbu\1070260\DC4X\ACK|q\ETB\186400i$\998123Q\170080\DC1:\NULa\179425v\1057890\ESC\1046601O?\144872\1001618\&9 m&\185419G\NUL'*k7>y\185109)c\1026066\DLE\t,}I\SIzT!R\1051585\&9u\EM\DC42ixf\ETBh\1093277\45899_\ETX&\t\9508\57743F\1054634K\151449\t\ESC8\n{2\1060622\59202IL\SUB\1114011dp}_9\67990:Xd\66188\134097v\ETXjk\52228\&0x\SYNi7y_\DC4\94598La\tK\SO.\SO:#\158037ZsIp6\DC3\tG\21697j@\SO\140605V\171781\1004444\1095580\n-0x3\1070457JsH\49717\&0.g}vU\985649\175749\t\1108868txWw$p-)NErg\DC1\RS\v\61996S\97223eK~\18154\1087578\&7\139648]:\SOW\f+C\ESC\1052448\131579\1070786pH\1082515g\ENQ\DC2W77\35594Il\SO!<\1029111\ETX\42368p)}`\47291}\143330\&9\US=$SXcjkM\186140Fp.8h\1047276['Ta)Zq\175154\4734\fW\51765C\1027418\1103868\1101167\1052280u\nc/\1112595\156385\1015057c0I2]\SOH]g\161033*g)\th\1024978G\1053938\GSv\USXf5!U\54369=#\vG\DC3\ETX?\SOq%]\147834\&7\SYNW+6\t&\1113103\1100216-l&\FSv\129474\DC4\1062308\1053188\ACK\1016990a\1024054m\1046519\NUL@~\1087194RfG>\154171q\CAN\ENQ\68901\DC4`o\r\DLEt\DC3\92906\1069460\1048347\53182\STXf\1094150\1068082i\1014049\1037453u\\\f(h,`\1047778m6\DC4f\SOx\157831" } @@ -98,8 +98,8 @@ testObject_PasswordChange_user_6 = testObject_PasswordChange_user_7 :: PasswordChange testObject_PasswordChange_user_7 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\1078147\&7\65218\RSA\999884k\v\ESC\NAKg&:\NUL6K\NULn>\SUB\119974$\ESC8B'z\"Yc\1032069\&6\ESCU)\NUL\DLE\1101164Zj\44385\83195bJ-\US\"\131804L\a\1067731y\DLEs-}\141826}\GS\CAN\ACK\rR}R\CANL\FSqkZ\n\t\189000\&1|\f5\984053L\ETB8gL\18292\&5\10771cf\\V~\DC1\11412\EM\120833\990084\&1n\"\60837*\ETB\SIaTxU\DELcZ\r#5/\bk\v[\a`\1106514\NULR;(\CAN\rFMN>\995764\ESCt\ACK{'(\141540\ETX\NULT\1057079m\f6\63805B\n\987874" } @@ -107,12 +107,12 @@ testObject_PasswordChange_user_7 = testObject_PasswordChange_user_8 :: PasswordChange testObject_PasswordChange_user_8 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "b\US\1102445X\1020217\EOT.m\"\DC4\DC4\ETX:1c8\1003324>QU]\ETXRe\1032621\SOe\SI_~\t\EM\182748^BGb\991684\SUB\1032747,4Ux\44572\nA\19832\1062925\fo\CAN\1001285#+@\14237J\SYN>\SOH[Q\31322P-\ESC\DC3\1017636\64940\NAKQmD]\f\" \23277\26752\DC3F\1087665W2\GS\FS_\145580S\34366/\SOH@\1008116u\US \DC4x\989165\138589\&5\60472\47193@\995028u\DC3\127467\169198g!\174297HZM\178770\1109385j_;\39894v_~\1020633U\nzDxZ1RN\2467mdb}\152593b;s\ae|&\"\18230l\SO\1075495\73747\165342\418'\DELj\993281UB[\14633\NAK\"F\1071892\71739\&9I\1086187\SUB,+\169163\EOT\EMBtZ}\SUBZRdm\DELA\ahEj\NAK\164812\1045403\49808!i4\160130,,-4T\39327E#\FS\129309\SIKj<\1109332\1019724`R>\111006\139594\DC2Vq4\DLE\131391\&1\51249\ENQM\42303av\181926\STX\1016985\NUL5\164635!&]\22190p\v`k\67413\&6(GB\1042616\DC2L\996758a'7L\1096604[\ETBR\1022507|\1020702bZ\1060760;6\GS`\NAK\1055957\SOn\128679\1080437\1000675L\70839Vn\189246nw\EM!*bw\r\1102406\ACK3\25917J\1100924\DLE\1079071\RS;9\ETB\51636Ts\n?l\171848'y*{ G?>y\166331\&4\1028518\143808M\ENQ@\1106697\&19\62848:\GSfI%%;p\1057791j\STX\52156{7\1045649mR\170180 \1045874`b+\189602\1095783\29108<\997493b'\1113133G\1113924\187365\1018965\DLE\t)\ETB_\STX\188043\ETBq\ETBD\14549\178567\&8\GS9S%t-;~A{\1098493\1009689t\RS\997797\rD\SOH\"\1036045\1080223R\r\r\SO;b\1079046\RS\96789\64328/*m?~G\1005579Z\1029293)\141393\134174\1004939lL=\1066280O,,j;x\SOH\74911\a\n2.+-\16525&bd\142521.\NUL\1105545\DC1\61097d\1016348q\ACK\v\"\155055\1051009\1111466c\DEL\a(./\STX\10580E\1095607P;\"\1100473h\15195{\21638\1108997\1001215Wme=ny\DC2\997396(\153889\990739 \rC\DC3?>ZD\SI}j\SUB\SI\GS\177033\1081156 )V>\1073618\1110301)*l^ip&^\EOT\991196&Y\SO(L&\FSs\1025953\SUBm\194690OR*\1083553\984637UQ9a\173357\RS\"\170635pt\DC3hxbnb\144388\SI\1096629M\36441\183861fJ?}t\1042071T\21290\1041177?\GSg&V\1107865D\NAK\58427M\1083184\&0U\b\53742\1049758\1019549hjT\"\1047744\EOT^\GS#\NAK\tv+t\FS\USL{\1011965\126503x,\1024988\DC4\1026933<7\1074268\ETBw\rz\159720\985242\NAK\SYNq\65888\1019932$\1038698|:\v\SUBd\n3]0P~Q\FS+&@[\v\92526M\v\136444\DLE\US\a_S#H\ESC\1000365\178961\66613\\\"\b}8\USF\t\ENQ2|,\t\US\36910\996072\DLE+\166272D:\148639\&7 \GSe\1085048#'\DC4MpE_n\95537~\1018210@\1059219&=1\1058223\1085407)R\1035591\SOHt\39215?\94500\32949E\171322\EOTzX\1061392/mZ7\39206\ETX`m\1055925!$\v\DC1Z\SI \DC3,\14510\STXil\DEL\DLE\7164\1027803A~EU;:\ri\1083540l\35399vc$\SUB\DEL_\1081553EP4\1103837l~|\a\1051360\ENQ\SYN\1096952+\1074553J\19836\t\GS\aA\EOT\NUL4g!\62787\150045\22255\183201\STXN\ETBrrpN5ks\bZM\ACK*z\DC4(Pl\DC1\"\70701\1067954K[\1008837RH\1032341\CAN[x.\1048119ac9\1111224\59370$\19588\ACK\f\74197Jy!\29122" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\f\CANj?\ENQ\100674\27294`\v\60820S\aUZ<\190604\v\n\98721\SYN8,\SO`\ETB!\1023917\SUB\988878 \149166~\25356\&7k:\SOHo,w\US`d\1095991E\170702w\ACKl\FSOUl\DC1r\DC1\4696\1005535\177324Y\NUL:*\SO\51294x?>O@mGy+Z,\SI\31196&\EOTZ\14202.w\\\GS\CAN\DEL\1061426\FS#!\100667NFM/\b\168841 T\183374\1014354K\nG\95679\1071981G\1014345!]\GSN}\1071684m\CANH\1032944Nk?\61487K\EM\131256ov\48786V\r\184775p\61887L\1085562e\175014\ESC\173236\USr\135299\NAK\SYN\NAK\DC33r^E\43094Z\bP<\135606\138117\1066630\178853\27013\1078589\ACK\SUB\NULEa'*W\177921\163435\176746\EOTLV4\26629\"-/\118982\1108171\&3\19533\GS\SOH/xiy\1004921\31236\DELZm7s8l\1032610BT({b\59819n\DC1\154649vD\59996\95915r\66886_#d\34706~\53775;BKF\993228] 6f\126105iN^\132202\CANeN\1050181X:\"+\STX\1000519C\DC4*\164663j5\1087078=\49843\163443\&8\46178\1005505\1086358\67354\&9}SK\132067\\0\120968#v~J mt3Xx\RS\US\1053047\v3\997095\SI\DC2Tc\996715\DC4m,\SIn!b\26969'ac\29011oV\18582W\100115\1029633\t\SOH\149270\SO\1052983\&3%hmTnE+\\%C\24956\137609\&3\986293$\1010528\983647\&04|En(\17123\SO\174091`Xy\1069572\984775 \132546~\1054660]\DC3\167285\&9Y\166240\&3T/\1057195\58265\tBS3;A]\DC3\2765P;.\1046618\US\DC3\NAK\GS2[\25411\1061324 \13123\165595\"Q^0\STXT6\18123yB\DC1?\NULv\1081840xd\1060136Y\DLE\1094984\RS\168967\a\SOH4/R\113820\1029185V\DC2A6\1016176(T\\O\USTu\152631\ENQ)\22634R\ETX\vR{\r\STXm\1044646\146582\SUB\154494\32280\1053397\EM\SI\n]&<\NAK-=\1052283|S)\165884\43834Rq5e/wH\SOTh\145516tjJ\1032777g\CAN\DLE\ae\SIV3\94033`\1100278s[KR|Y\CAN\1079014!\ETBM\1091893\157876U\n\f}!K-\ACK\ACKmp\DC28:\GS\EOT\r\1063024\1023444)(\ETX\1017315\983729\60554^\1097871\101098+e\30627\t\160747N\EMi\nfN\98956\149454\&4Q\n\r.3\31727|H\ESC\990983\998110\159749" } @@ -120,12 +120,12 @@ testObject_PasswordChange_user_8 = testObject_PasswordChange_user_9 :: PasswordChange testObject_PasswordChange_user_9 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "\121027il\134081)R{5\ACKXm\1041631\\T\f\DLEp tq8\1070074\&5Q*\"\1087921@\DC4\182306\CANN,\162026\1112739;3.p\STX\fLA-\149790j\bON\ENQn\984593U:\EOT7@\GS\1102637ky)\74361eri5CF_\1086719Y\25273^Q\SYN\NAKQ\DC3[\31615\ESCaY\180511~\t\100087'\DC4M^\EMI\994154\&4\96550\1036840b\DC4r\1078078r\RS\140751\1084467\SIuHc\rg\DC1\ENQd3\SOH=p%ry\1003698\&3mhoP\1106864\v\162715yXMw<\59204\&1\f\1016334\ESC\3501lL\69237\GS$\a\1039285w\985184{)\NAKw82K)\DC36\155645v:\SOHC9i\1039062\17926R\1072663/w\99462\15991\185843e30R@T\121319_\NAK\ESC\\\1092892\n_\1069021\aiinb2I\RS\1098801\138282\SOH\992030Zep>H\1079810uUC%\tS_fH \SO\1084851OH\a\ETXx\a/\ACK\ACK\993315\165332\72410\183658b`%\31687\DC3:UIj\1021763\ETX\1053142\NAK\STXS\DEL[\1028930\&1Z\SUB\US\1088016h%\ACKw\EOT[yh\ag\RS\994622N^{g\1003836\993133\DC2\a-\1061684\rrJ\1036806}~;8\ACK0\1021569\SI0\ACK\ETXfXG\SO}H4\DC2zC\SUBz\DC2\DLEz\DC1\149550\61518H25*Q\1083850\10672\NUL$h\SO\134582\43597$,\65487\61824O)^\\nvK<\989262\&1R ,k\985467k&\1012054\1072126V\98741\170921E\ETBEP\RSR[7F(H\1006507\EOT0\\\CANk\DC1\1085575ql\150344,\DC2\NULv\SO\DLEp\SYNIqpU:$\1051572|+\DC1\SIT\1043680v:\54535\133122\SI\167063\190640\r\NUL\1080625--\NUL\1000447J\158492G\1043941\ENQ7\NUL'o8\1055620\ESC5wuH" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "<>f:;-RJq\6050x\161753QT\100505;X5.\1024124;1\fP={7\136758\151452v\DC4b\ETX\1041908\NAKV/R\SO\1003791!\SO\ENQv\SOC\v\FS[Q\ETBh\CANX-\66455\22025\1087386:BVj\1104389\ENQ\41001\97464\DC2\DC2zRZE\"\DC2\EOT0\164473+i\USE\52704R\GS\1015386\FS\EOT\154897\185499\USew\178235\vR_=\1105788\78173\ETBkB\172309\995208\&7;!?'ZN(n)\1003220|\151096\DC3\n\DC1\ESC\1047644\EM\14646wov\ENQ;\1082140]\118864\1041829\131956\&4\1085467h\42033?iD\DC2V\96442t\78699 %\177867t}%\168450\1068330N\EM\b\7477(\6702\&9~\168927*A\ETB\35836\1087213\ESC\EM7`s\988223G\CAN\171597Q\1032850\"\EMi|\ACK\151936\&4\49571\&1 ^\1034297\38608\1080861BBO\ETB\188460sz\GS\1113432\20959_PsX\152878-)\1013286\f\11345ZAT\ETX^\1103065$\1002688\1102176M\DC3\1106060\1083723\31676\135940\1010227}+p)H\EOT]\61870(fiL\74358\STX]m(c\1099516\1058859wN\135817Zs;&;\1101239\STXc?\nP\164370R\1073337\8218\DEL$\62817\1035797'(v\94886\RS\131427z\USV\DC1\995931+J:\1044870\28567<\161564\EOT\t\SI;Ll\31033Bl\1035926g]\EOT4yZ\143213)Qs\127306b] \149682\NAK{\\\\]\48933\1040819b{\1049468F\182947\&1\"O\NAK*\24604\US\989988\52604\&35>>|\CANH\GSZ_4}6\1056732Ty{?\CAN\"\1061905\1004010dM\a\21624.@\120724\DC3\984067hX\DC299hs}\ESC\n$\ENQ\1044696o+\3801Z\66465h7\172119fa$gT\t\1000627\1111076\1075382X9!\40902\a\EOTH\100163\1019832[\"\SO\1061034|};\1001901\55166D([N\SO\1037726g\180696\22235\142179Bxq\r\DEL\1109671\"o\4735\165730\ACK\20074T\38821do\au\151559\39351\SOH\SOo" } @@ -133,12 +133,12 @@ testObject_PasswordChange_user_9 = testObject_PasswordChange_user_10 :: PasswordChange testObject_PasswordChange_user_10 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "^g\165188\183325!D\1071796\&8I\rv4MC%k\\X\NAK@]\990411\EOTkKg\46739h\v-\EM\1003941d\r\n\46818\164542\DC1Yf\1031947\100661SN\t_q\34663\1024752nn,9\1002494\134950;vL\1005623?\vL#\1004806\159319\1005755Y\49264k\185970\EOTt\54951\&8\SYN\GS\DC1W\ACKxK\100106Dc\1028596k:\1088790\DC4\1024192\&2D?pHS@i\1055560\&5\"A!\127257\DC4\57827M+CS\EOTK\DLE\DEL?y\1098181\162494\47866<\41529y\FSk\SYN${\r\1005146\190068/\DLEY\20575\b\1039849A&\STX\b\998416E\a\1032363J2\120490\1018750\GSa+\nL\ETBE\DEL\NAKnAd\ETB>\ENQO&\b\1011115@:I~-a\SYNk\DEL}\USE)C\DLE)ts\SOH&\128490f\1031578O)8\83270e\59254\&9\58057O\v\r \bt\188311\n\1013246\46070l\SIWGb\1008559@\1059413\22227u\1026214B\1029435t\1109601Sx4bL\f\171190^:6\ETB\EOTz5x\FS\RS_\ESC\1105088\DC3\1111332|(w\1030422\ENQ:\45632\72881k\1036191\RSwC\186931E\1106146\RS\NAKJ\1043833)\120159\1023499a\1068709S\DLE-p\142797Y8~\ACK\f\SYNq\SYN\191139\1061750Vq\SOWh\EM\6136\EOTA\"}P^M1 \150446;\ENQ[\83011\78574cG]o\EM):r\185345\1099699bCeS\1095638E\SYN\39700\42082<\ESC\31948\83108\142987\&0(`\SOH\rr%Y\ACK+\1082430\142137\RSK\38850\1042506\&2ZK\EMc\EM\NAK\US\69438\51321\tyI~b\983734\&4\\t\CANu\1070201W\SYN\53093O\SIwT/\1054638Q\1005484\157400\&0\a\1040610a?\95679\NAK\SIpZi\2699o\RS}R\bm?\137670}\70000\42449\32037\ACK`\NUL\ETB\36412zO$\CANo\tX,gCWQ\FS\137405X\aiI\22269\1099880\SI\1056230" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe ";\STXW\3658\&9e\61104\1010281u\1054825;z\1102201.\CANm\GS8HiR[\1090721\113679h\30385\1099071P\13786ux\EOT=\EOT@-\NAKU~\30622nm\181728\v\SI\"87xzsd\t/\SIABdv\SYN\1079721\38516ov\r\61169\DELC=|EJ.I1ZH\r\1109850A\157023I\DC3h,\150284\157340\40343\DC1\1101518\SIvxV<\100481\CAN\992513k79\6439\&7\1105382\29451\51856\ACKw(d.\r\986761tHWFI\b\2063|c\139209.ZR\ESC\1043464XOH\b\1065603\185045\DELKC\SOe\ENQb\STXtwM;\1063586Qb\EM+C\a*\991991\SI\68430#DEv\70295\&5p\EOT\1017741~&\92197\6498?\DC3A%4\111178\"v\NULg\tmD\991529\DEL?\148659\v\\7\n\n\US\1052346EbL\NAK!` \996371\a]\1050364:\99420o\98763\1038145\r\US}nre\190462=\RS!\1026220\RS_\n\r^+\1112053\155114j\144557h\164197\EOT\1456\1080248\&4jpQ\ETBv\1058697\&6XK\DLE{X\182304|\FS\30623c?\ACKW[\FS)_~,\187940c\133750Ihm\68052\GS\NULJ\"#\b\41024\&0Zh\147884\1013140F&\ETBJ`O\191380H\917827&'Ox!4\ESC\DC1\1038348+DgC\95526d\r'\US{\1062664\47822?z\DC2\17591~\96360\155417\1068401l\14806r\NULN6<\57679\1055613\141513]Hp8\1063393\&0WrR(\25161\19762'gc#_^\997158\31893\23078\179623E$zk\t\NAK4g\STX6)\993627]\DC3\187429\34110p\1012714C\1078346Z2\DC4M\FSJ68E\37649H6\EM&\131250/\DC3ovwm\14557&m\152064M\1027607F\146051>\f\21330\NULG\994703l1*bd\SUB.\ETBWy\154255yo\1081513 zbzr\1034720\32843\EOT\1000002\16121A\133186\btwr(t\ETX\NUL\1038295?9\CAN~j\1018782E\176705$\EM(&\DC4f#eP\EM~\CANCT0i\1007344e\1041535.\1023395u=\DELu1\173333\DEL=\NAKYL" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\\2~\tLsG\1010318N\73893\19920\SIN\bA\1084446\149836\n\96723\&3\1005158%\53931F\SOH?2\985088\1043578H\184226\tWe(B\26887\SUBU\ENQ\v\188995\"|\53505U\1043137QJ\FSN\1098083\1056930eN\DC1pc\SUBY)\STXR,w\1068893q_\SYN\ETXM\179588\ETB5\19176*\182041\aig2\n-\au" } @@ -159,12 +159,12 @@ testObject_PasswordChange_user_11 = testObject_PasswordChange_user_12 :: PasswordChange testObject_PasswordChange_user_12 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "cA\1013876}1\986971`\11039gQ(6O\148533\ACK\ENQ\144020\1097898S\b\1026784\185340\1076893\1038191.tZ" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "*~\EM\SI\1035297\65924\ETXSi\917783ua\DLE\DC4\SOHgPRF\50551}J\SOH?.\35757\26118S\99244Wb\US{\191213\DC3PD6z;bU\SOc\168740{\NAKC|!\134012w\24191_\15393'XFa>Ds\1032722\&6&\160998BY\1105510\987553u/\146729\187725\NUL\DC2>A.>\1026064\&4~\1066860\983616\1065813\194763\41670n\NAKe4`v#\54249\154796+L\au[\119894H\1003679^\1052741PIo\EM/R\RS\DC3\150872>\a\1011374\&8}ahwE3\1009691k\19141\1096715\23648\131344\SOH>BwWU#\135314} \b\SYNK\1035065\157575\27111\&98-\995803\167979Y\SYNRkE~MQr.\47537\a\EOT\100066FE\SI2re&eE1\SUB\CAN3\30141eKHFCl6\185160;/k\r\SUB\t\41097@&\159450 \47257\vM\\r\SYN$\NAK\40078n\983737\1100061u?D>vA\95624CnF\989092(\71111\176451\62790RhM[\nB>*A\SUBgDpp\NUL\137828\1044414\DC3mQ\ACK\1088526e\164816r\NUL\129550\3404\62114\GS-lk\140775/\1031265>\RS\ETB\DC3\128406LFAs\176184*M\1085893\NULpt\b\100127JN4QU{Jev\EOT\STX\1108765E\t\153121\1033318I4K\\\163474L )RYPBk ~\v\RSn\1013852Xxv6#\1111946|\1067819\f\134655\987741\ESCY\31029z\NUL\ac%i< \n,\128826l\RS\1004102\119256\94776\986312P\1051689V-p\twc\CANV\185920;4\41787\1020199.\b_\1035216)\DC4K\EM,IX\DC13\NUL\1035747\&9h\US:\SYN\ETB81\160334\&1\36963\SI\RSC`\141966\fn\f\100236(\164834\180065-\SI\DC1$g\1046824x\DC1\99084~\181210ADm\NUL\1033535\40647z\999919Q[\STX\v\188766)q\DC14\134546j\DC1$\1038869\178209\1020722\ACKi\995076\&6by\986338E\DEL5\31674\1053862=I\ETB\FSD@C0`\n\1108426JR\97512j\STX\1011610\132328q9\12587\1110037%\ENQ\DC1)\SI \133259S\EOT\an?L\17808\ENQi#6:\39370\984528$VZ\ETB.{m\1105413\ETB\1096254\n\1029048yP?CQN\58229\DC2c\1016719\28430\37793\1021922\1037171b@<\FS0T\SUBs\NUL:\EOT+\NAKe\EMv{u\35899aS5ztr\1095275T\1076768\1067480\1055258\t\138199p\24191!\63947\57751\184259W'/t'\998026Zf?Kaa cX[\DLE@K\rP\DLE\SI6F.\\\1105071\EM$\ETB\185348\995728\1111114\1056306{\CAN\131737\SOF\"+J3g\14443W|\1025079gsE\b\RS\146118\1044328eZJ\rN3\v\r\\\RSTp?\1047550l?|\1052685H\STXI\1041763f\119524>)\150862\1004663v\997187\DC4\62145E\r\NUL:\DELi<\ETB\NAK\1034255\SOHyq\189759\39702\99276\1069813`\SOHt\DC2\1027302\1081467 ^I8\DC4\SUB\t\47100>\DLE\1007609-\154421\US;&\996462jK\\%Bg\170965:\a%\1030923QF*0\173510\1106863HW\52749\ETXs\1026228aAS\1916NbLWp\1051310^{T\29700\179151YL+~\DC2Zv,}\1111746\167987\59316\t\38658v\60679\176523\ETXM\52344,\DC3\1111272~\RS?_y_\DC4u\a\30652\SIOiiFo\1051777\DLEO\DC3{\40043o\1047361\ETB\27903\DC37S'\ENQ;|\1046730Eq!\1073488\160026\1006141It\175865Z\1065806\DC1h#\DEL\138456\7911'\ACK\169193ei\STXs\1104017\1064860sf-\57677\1006904\992638\RS8\ACK\SOH\CAN\146274GJ\141048\GSHp\61180\GS\EOT\186295z\EOT\tp\STX\FS\SUB\96038:)\1029325\42428\SYN\SIg$\139209M\1014224\DC4\ACK8^\NAKt\9753e\1093154\1043578\1079420[\NAKi\66187Q\48349\&3IV3\171979\v\DC3\SUBxDHJ}MT\r\SO\179759\145196\f**Z\64475\1056846]7R\ETB2Ez\DC2$\176439\1000728BM'9\ETXEa\SI\1065849I\1098032\98480" } @@ -172,8 +172,8 @@ testObject_PasswordChange_user_12 = testObject_PasswordChange_user_13 :: PasswordChange testObject_PasswordChange_user_13 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\tY6b\1071064\CANJ0\RS\DC3\fg]\8556^\DC2\1029089S<\ETXn\DLE\161054\NUL\1009413\"\NUL\135103\SIU\1028385K;la\SO\132923`\50089$0#\19913\f~\RS\1050195kyBx\a\ENQ<\DELbY%\1106346{\144787\ENQ\1006226\SUB\n\1111798=\1082031\nl\1027190\49972\t:Z\\\45927\&7,\ETB\\\1043520\1040129m^\ENQ+\ETXepC\CANVO2:!\155826\n\CAN6g}\1100418q6\1056075\&6<\171664\SOH%yP\175359\STX\ACKo\179550g\1071640p\1006475T\1018644\ENQ\SO0'F#^IVd\n\157140\141227A,@\1053337C-\181395g``\166195\NUL\7801\1049487\138364\&5}q\50268\SYN\1089481\134438u(P\33463\SOP\47384e\n\DC1\164033\&6\1083698\SYNM\168121\&7\1027817\DC49\1039185O9 ,Op\983226UR\DC3\NUL\48061\1049901>(\1025638\EM_Nn8\FSb\DEL\92741r[p\1113723za1\DC2w\DC1\24935\SUB\169669\FS_\CAN\EOTN6Vyi&)\1012450^\135732\EOT\CAN\41126BuJ\DC2`\78370)qq^@$*\SYN\119136P\v\7875\ACKg\134713Mg}\ESC\EM\993564\1036198w\983924-e\31379p&\SOd\1022808\74004a\15280\1040139\1056286\RS\143232\1056072'E\181014\98120\&9\DC4\DC4A$\180660h/A`\DC1l]3Qv\14807MR3W\FSsn] a\NUL:3`\95284{`\32597\n\US\DC2.\172218:?y`\DC4\1085202_%S\155378:\NUL\171483\EMk\"\fWYu8-jr)\184?D\12340c\1107469\1096889\1089369^x\SOH{b\DEL3Sl:&0xgT321\180495FU\1068409N\1113930P*L\145663\64596i:\48860\SYN\164807\&0#\ACK\48791\&0v\1049613n\SO\159015P:" } @@ -181,8 +181,8 @@ testObject_PasswordChange_user_13 = testObject_PasswordChange_user_14 :: PasswordChange testObject_PasswordChange_user_14 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "6go<\1060200f\41213Y\1084615*KE\1038629])9\1028527\1090910K\2404\1065550\&1\99810>qb2`\NAK7*al\v3I*\156801\SO\1090154\f}\DLE\139358~\129615@jjd\SOHO\SUB\SOH\94999lv\16578B\b3\1066265?Ih1bv\v\SUBz\SOH\FS\128520\&0.\ENQb\50990\FSR<\1111211\EOT\DC1l\"E\125002X6\NAKS\1011858?\ESC\EOTEK\210h\54053\16688\f6\917624\59462>\v_Zd n<\DC2k\1086856s\1069883~\SOH\1011269\CANr\DLEG\998802\NUL`.)lj\DLERr/\149432\\\176664\999860\187741\59007\96806kI\1040467\&9\\.!\STXpj&X?=r\1072676A5\169615\18716\NULex`\SOH{ \121420\ETX\999279^\98959D\DEL\1051244\163196\132146m\58414\35040\&61JU\NULtn@pCF3\fM\155170ZrHM\1024580i\136496zhn\1010172\983207\ENQ\ESC\v\US\ETX\1078490\1027708\DC4lU\DEL\GS\10612[\DC1B\EOT Y\162831&cnLev\20431awe\n\175441+O\69646N\1039476\986854\59235=x[7\"B\RS\DC3\DC1\bQy\DLENq~\1100372G*\1040946C\191033\DELHo[\96055\&9'\1018134\&6\186449e#\ETBK\49381W\SO\1108069&kH7MQ\ETX\EM,v2\SOHN/\1044045\tO\169061\SO\1010256b\185510\1081515\148501q\1037709\1091186ww}=$D\ETXw\DLEb\1069094\EM~\142428T\ESC\CAN\74821Y~{\f{p\138353*w\1062006Juo\n0\150906sYXHT\USK\a\1009732\ENQ\n%q iF\95870\ESC\GS.c\GS\1014409\1066933\USW\EM%x\1003810\NAK>\DC3\1058760" } @@ -190,12 +190,12 @@ testObject_PasswordChange_user_14 = testObject_PasswordChange_user_15 :: PasswordChange testObject_PasswordChange_user_15 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "n\DLE\SOH1\ESC\1030226%\1069394\RS\182899Guy\1039539+\1113955\1023913@\NAK\5561U-dZ'\fB\1055523\30303\SI\US\CAN;V\SYN=\FSz\"\1085023#`\USt]KYs{\v\45407\"\8592\1064953\1006367PP\n\t\31925\1041417\44390\&5u\60622O\29903\SOy\SOH\1051143\184117j2\60717\1030594\14253K\100794\SUB]A\DEL#VL\RS[\\\1044640F\b$UXg5](A\f#b%\1086075\NUL\1041235?\45258\1073954\SOf\1095011\GS\140034b\1052232\&1\998007\996181>\49135rnE \ETB\f\f\FSM\984153DQ\GS" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\137821\"AZ~;\1071515sK>\1003094\32681rdU\SYNZ\FS\1013170|~\68860{W=X9*\1094479^]h\v\1037179\SI\1105700\ETXy\b\121173\EOTB\RS\SUB\ETB\1087347$\1086752\&4V\DLE*,JI\EM3d\169902\&8`0&\182876r\61161*T\ETX\151630_\"\SUBT\EOT\SYNyw\43410\100742\nP\1111807\vJ\GS;'\a\1019800b}\USu\15085\b\SOH\DC1>f\ESC\185821\SYN\ESC\29398\&6B\FS?*HjHc\1072255\1058427}\1624h\n:\154398\"\1051991\1099837\&9d\128882AXf.Y\139982\&2>3\985533~\DEL\RSaL\DC2!\n}\DEL\DLE\aV\b5\1004615w\44033\ENQ\STX\986416oe\47326\&9\"\1012652\EM\\Z\31242\\\1054641\SOu\1042537\CAN-\GS`7&aJP\1066356\999545_Di1\ap \SUB\STXv2;\170127Fmg\SOHX\1102996;d%\ETXx\99896\EMOr\EOT\DC2\162508\GSvo\1035769\ETX\51961\&8M\NUL~bI\1096210L{p\aq\1026887W{PVOSq\132165\96511!n\t\16523U\t\ESC\1014032!\EM-IFP\1096087\97677G\1056015+l\a\EM.\1051294\f\1031336H\1049728\ESC$f\CAN\NUL*\bnr\1049928\1075881(\189737^\DC3\98799B^\170344[F\999872\ETX\NAKJ9;y\ACK\SYN\66458!51w\64275?Y\EM\DC4-1(x1?(:\SO\181899P\59702_\27711\163618Vx\ETXuN\v\1055926r\CAN\FS\SUB\SIu{\1026849\136958{ \CAN. \1004662\1081463\ENQ\SOHD\DEL@\179223J\ACK:\167491Wy\14989\147498j0(\ENQ\1102373\1014240\&7\SOHTI\RS.\118990#\"\162391/tg ,\1019276\1069245\&1m\186668i4\67826Di:\SOU3M\144745\1112930\1006102\&7A1B\47962\159987\ESC[O}\1028140\1033214\1061595\1000273G?~\SO\1105814\n\23793\CAN\132894K\1109537\157688\&4\\C'\171760t\1092105\1069028^\154207\NUL&aW9OlY%1\t\163491bT\n\133769_DO\70287\&7\EM(z_B\14519\153806Fg7\SYN\EME\1096879' \1105838\ESC2\fm(WW\1091836we?\1088332\26513\CAN\155517LZJ\NUL\DEL\FSYIk}\120430\EOT\4637+kZ\SI\156899x\SUB_\DC4%\177759\1057446VC\1097314\1074153\1072386Oqn\a\RS\1056654\"\18164%8a\28468\132645Tb\"C3\1103957vG\1089945LF#{\96210\998246\160936i\STX :\163339\61888i|\DC1\1011444T\t2\RS2?IHPdw@fhLKXq\61905\1046908qz\1038449z(\189299,\b\SOV\\8C\DC4\5575\&1>\SO\ETX!\131673&O%}XKML\43288j6\NAK\1080490\SO9t9Ku\141219\154727\RS:\1022834df\ETB\996821s\16300\US\173093\986989\ETBL2\"\64028\1047440\ta\bZ\185810,u\1054582\1022464\991444\n\SI\1090918\ESC.B\1024218B$\SOH\SOHv\23330yD\1082294UZ\996426Zy\1031823oX\EOT\SOH\174801'\a\125038ub" } @@ -203,9 +203,9 @@ testObject_PasswordChange_user_15 = testObject_PasswordChange_user_16 :: PasswordChange testObject_PasswordChange_user_16 = PasswordChange - { cpOldPassword = + { oldPassword = Just (plainTextPassword6Unsafe "S+OT\38751b\DLE/B[\100483\&3\47760\GS\180067O#o\25466\&5T,8M~\GST#\987895U{y"), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "5\1103104\NAK\1014216?M\ETBj`-\30597\181188\1026387Z\1094596[\1092626\te=\991832E\DC1'RlS\DLEZJ&L\1107431~G\CAN$\vd6 5\US\1006596\ENQd\r\b\SO\1100302\1110521i)jc@S\156632\1002333\v7\24501tU\a\1049077\\hD\1110213\v\DEL;P0\SYN\\q:\\I\990426Ty\1097835wk\154857z\DELC\36957\GS\3138]Z\16454\SO\US)\133053g\DLE\DLE\GS\ETB|\44640-\STX~\1024260\a\1000452{\ETBK\DC3 ~.Z3\SUBC\986330f(_/\1110859\1055634\1003279\&7\183j-\171356zX\STX+;zT@?\amp\SUB!\36089h\r\992554\SO\SIt?\b\54803/ y\NUL\95035\1077028%\1099069\NUL\1063994\DC2\DC2!|!\DEL\t;D\ESCD\1041733OT\1061393UJWf\1113505\178024\ESC\154767\1050223\FSX\1026016\1020780\CAN|ix\1091727jZ\187257b\NAK\SO\1030980r\DC1)\1053891:\163447\45030\&9<{e\1079093\30596L\NUL\STX\1019960;~\985116}\1052410?+&\NULz\144674\1086689\&7\1030068%x\FS\1036306~\120570\RS\US*\ETXp\1034462\&1\149891\13986\1055542\STX@7yY+\ETX\NUL\1062210$J\1067009T:\EOTzl,!\SUB\DC1%O\DC3\SOHX\FS]\1013399$\152121\1104444\\\139341PX40\CAN\v_,yU^R%\DC3e+-g\172222\SI\DEL%f+h&W\ESC9,Jg,'x|\51952/{Y\r\NAK\1057765\DC2[\1038364\SI\28850Nl\46666\1885\NAK\NAK'\DC2/H<\180011]\ACK\1090504@9\127306Y\150151(\US8\53321\993078c\n'8]\SO\186951q7RH5L\1028090\165@\8885\30083NB2x-\1014943\985470{7o\94409)\1031807#\ETX\42922rid_^Wy7\1029256i\1062709\SYN\99669\n=\21963\&5\8639\1035935\1067300\53855\NUL\vO<\175839\&6\67816\ESC\ETB\EMJpG\ETX}\nM\177929\96385\ENQ\NUL[\1007534\US\1085889\"bl6\\`6\EOT\RS\",6?>Jk\1044669\160533\993117!?4\FS>,\DC1%\61901?y_\ETX\1016387\v\FS\DLEUQ\27172\187044\73101\24011\SOH\169041\NAK\1044569aK\r)f\SUB~N\1020859\DC3k\1012707$B\DEL\SO\ETB\ACK\EM\73084\58832~tVD\RS\SI\n\RS\1037043G<\52368\1007888o\ESCftC\186158\n\36317\b,~\ESCM!\ESC\174873\134091`\1046265\998677b}k\67343\1077779`]1^-\NULO\1013355r\24494\149416\36343`\127285\ACKW\1097424\996658\&3.tS=\983895\ACKs6p[\989667\SOb\180485\1076744W\CANdO\128541da\1063827=\1113561n;\180045\&8Wn=\SI)\1025924%\EOTj\1043094\NUL~D#W%\NAK\b \64862\DC2jr\27380tb\GS\1014983r\bD\ACK\175197oH\4243lJ\51936\1017192\59111\1024329L<=\v\78854*\54478 97q\1013840nS'{->/t'\1065169Xq\917836tmel\1025953\1010549\1013101\SO\DC4\"$\US\SUB\1098531\18016\r\DC3\140813\95239s\28689omb\SOH\1102241P_&\67318X\f*lfw~n!\SYN\ACK\a\60339\1012508U\1104365Y-d\126581\1068676\NAK5\DC1\SYNO\1060779z:\RS)\188550\NAK\1026997\59211\5670n\CANh\1072150F\9559\a\133215\165806\NAK*C/\44946.)\SYN\aP\1107161\1043226\DC4\1087020\515\67972\DLEL\n\180263y5a\146153\54746Iy\11497a(\SIv\SO!GW#g4\EOTb\SI$(\ACK\niKxu\DLEQ>\1038539wGc_NKl\r\13222x\83063z\DC37\RS\1096948\\\NULB\vC\141810\GS\169437C?&q\1009432)+PhcHd\186025\DLEA/F\1035548Y\47461\14070J\1012685jIQ>y\2014\1058904N\98611y\SO~\26014@e\1061608&x\189240\1080205\"Yh%g\SYN$\1069145\1046629|\EOTT\EMP\1011180\1084918v\RS-.e8\SI" } @@ -213,8 +213,8 @@ testObject_PasswordChange_user_16 = testObject_PasswordChange_user_17 :: PasswordChange testObject_PasswordChange_user_17 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "\1046443X2cZf\tI\DLE.3\27153\41641\987805\SO\167150\31997\157768U\23766\159716\DC2\993933xy\1103378DB\1095912\USP!\8776\&3\46231\f\14600.1\1020378\1043279ji\135553C\95086\16967\37206\19099\US\NUL~BYB\RSUYc\ETX\1091112e\127528\187472\4411\"cN&\t.9\1098365?\GS^\v\SOH\b\CAN\41627&\13579\1108825a\1014432)z\62357\&6Z\179494\1092724\ESCX\SI\13823xJ\SYN\1428\EM-\DC1I\nB=\1040975n!l\131479~U\1069398;\113684X\187497\59277\EOT\159297\1023481uY\40199z\1054394D\1020153\fFbZtt\CAN:\CANYQ\SOHh\1006361W\1110330\DLE\168743!1}k\\\1055615z+\NAK\1106543\SOH\1094136%\17474?v\1108035h\fN\f\DC2\NUL\SUB\189591\996341P\GSbP\DEL\1107736>ie\1100530\7924i\168174-\30280]4i-\STX/\GSA\b&\v\1043901<\1102709\1106671M\\\991694-pG\FS\169333\DLEHEJO\a|\t(\9209D=x<\ETB aV\1012721O\999045n|mdg\1043448@\1110847f\a\1025181W\190988\19816\DLEh\166909\1092096\ETB~\10652K~\1072426\ESC|\rdi\GS\64637\94773\1081217;\1026647\&8e^\142140\DELT/D\US\NAK\983847hTTe|N8\1077575\&1\1092491tJR\a\155288iJ2\998006}\36187\28713\25201\SO\1109108\&0\2753!y\SOH1W\USzX\SOH0\991532s\119987h\78486\135733#\1074355\138222SR\988575,V\180455v\NAK\164938\&1g\SUB\ESC\97713\1081062\STXQ):\US/E'\131476\DLEz!\SO@\1020670Hy5*R\1010303\SUB\990422\1044281\1014588\1063943\178348\1062043`\49558\DC2.M\1113770W\171312\ACK\1024710Imf#dHF\ETBc\194659\DC3QcG\1070916B\NULW<\RSC\1059704\988425\1022019L7" } @@ -222,8 +222,8 @@ testObject_PasswordChange_user_17 = testObject_PasswordChange_user_18 :: PasswordChange testObject_PasswordChange_user_18 = PasswordChange - { cpOldPassword = Nothing, - cpNewPassword = + { oldPassword = Nothing, + newPassword = plainTextPassword8Unsafe "$\1043357izIh\65323E\152268b\fi\165052v=:\t9\1029608\r\10484!\1051779:\1003340&/#\1091275G\188407$W\990383>\EMDo`F\nY\EML\EOT\t\NUL5\996488bC8\5233Bq\1018037$p\NUL\v\9478R^\SYNGF&\1012032+]\156711]\22754\38792;:\131701\155917w\1065591\NAK\DC4\SYN\1060773\1015476gi\SI\"\vq\6329vV\1040593\DLEYya\1102677};,K3\DLEn\ETX\ACK\DC2+\184693\142191\SO^q\DC34+Iby-\ENQ\1053606\162697_" } @@ -231,12 +231,12 @@ testObject_PasswordChange_user_18 = testObject_PasswordChange_user_19 :: PasswordChange testObject_PasswordChange_user_19 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "k\ENQ\SIW\142801|YQ\999097H)\EMa\35968gXC|&\fE`\176817UQ\1096875\GS\1042874\ACKj\94562\142093\ENQc\t\1015620\SYN/8\SOHL\986768\&6\132434\1071731\34028\SOHy& \ETB\52652\SIf\1005119\&5\t\1060616K6A\a FxP\26949i\35802rc\18038\186543\172362\151462H\149276h[GU\nuX\SI%~I\184399Sv\r\DC494\DC3\SOH\989634E~q\DC2\990048\120529\tR\SI1$\NAK\ETX1\165481\1009573#\nD{\1034729@\1045950q\1036461J\97887\au\SUBB#4\EOT\8381\1087000\161668g\1011547q6(=\SUB\58393\n\13236\58038g%\SO\1066841l\1003446\1011686\997871\153172\NAK\f\CAN~\1051732qs\155291I0|\62022\SUB\161505\1084819\\Dq\SUB{z=\CANKL\53422\GS\DC4\1095233G7ewkJ1\35446J8 O\152777\96173V(\n\SOHuT\184493\142630\&4-\988150\&0\v#\1008772$qO-\SOH/T1\NUL@\53323\1012898\n2s8Bfh\"{vy\EOTG\28934\ETB\DC2g\NAKx\40967$\1111313:\1096564z\984205\r\1113615\50569\1016459\1089112z\1059587\62507U\992158ksD\DC2W,%\STX}\SYNY\1063541\EM\148916\1026506\SYNu\1068118a\DLExoH\b\96516ro^\ESC|\14524\137137\174774\&2\1015701!ReL.)\GS\995824a\134494\111281\38182\ETX\1055512\DLE\53907\DLE5?\DC3\988857Y:\1077940\t)\96370\48426\147806*\158714\1042527`\STX\NAK\FS\GSg\t\1084955*fM\994607\1029549,\ESCTL\STX\NUL\986074\1096953:\a_9i\524\168231\986631Wxh%\1104374`t\1062137i\139608mD\30436\ESC\18940\RSzJ\1014566" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "L`\EOT,Xp\US\SUB\DC39\SOH\986402+\ACKQ\1011739\163475\SYN(\126117S8c\EOT\SOH=d\152742\FSL\34501\EOT\US]\94933~6\DLE:\1038349\46131\RS\DELU}dYj\vm\DC33\ESC\EOT\ESC\STX\SOho\n[P)\EOT\"r\148842s\132918\&6\1013939\1054104X%g^\1111091MDA\GS=\131957I8\r\1059039\DC4-\1093354\95894\992259\155020\".\146604~-\24057]\bBLv\ACK-u5\1099612^st\26172;\SUBq\FS\ESCd\998793\&3\GSII\STXS\177535\DC20\SUBHy\1108265\18293>\DC1e4';\ESCv\f\SYNxF\RSWD\40069\NUL\15936WB\FS\145512/v\1094497\&9\SUB[a\1031802\t\n\187075y%\1065833\&5B,hyc\"!b#h\1092617XC\GS7\995391mZ\NULECj:O:\v/J\SO\1102347\996658\&9\EMs\DC4\a\1059269d>HEz\FS\171554/n\NULeC\1004734\CAN\65713\&2\181341\STX\ACK\1013277v\1000956\94105\986760E`xtrZWt7\164746wMA9r<\1021337\15097Ovo{\1112295\v#f\1040937\991008\SOH\63011j\FSb\r\1011414\v\FS9e\136229?\1019925q\1021008\f\172280/X\24799\STX\ENQl\FS\v\74972\131088RC>Y\ENQ\1073582\&2v\GS\ETB\US-\\,\1041777e\nf\1021970\GSA\DC1\DC4y\1007481\1102343q8\SYN#\NAK\984437\43846j.\n6Is\SUB\1049642\t\1020034tL\1049999\DELT{\173861\1059180Sz\68055\988553\EM\US[\DLE\48766\r<6CnyQ\DLE\RS146\1059541J\DC4\1059543ceb\NULr6(P\917894(\1072768ic\34855/\ENQ\50857\18315\&7\DC2^b\CAN\1000777\f\US#\15234r8-\154704u\r\1016712\SYN\NAKH\SO\985948\27600\1011459\"'\46452\ESC'=\SOH\19188b_\DC1\186563\SUB\174895x`\a\1041293\140522c\EM|\984810\aA\rQV?\1058487\fZ\f\ESC\SI0D\SOHQI\ETX\990028prt\163629\94675\36885\171880\1096809\&9\46899u\EM\1102387\n\13498\DC3}h\1032138o\DLE\1063962tFT\1095317%Az\1086440\&3\US\SOH\EM\38682'\DC3^1\14526(>E\DLE(\n\1066401z[Wg\1100054ad\1007846Mnv\8290\1091875|e\190345\\g\DC4\51159jIsn\DC2\16061\178290\&0\DLE>Lr*Q:\ETX\"\183845\DLE\98183\STXq\DEL" } @@ -244,12 +244,12 @@ testObject_PasswordChange_user_19 = testObject_PasswordChange_user_20 :: PasswordChange testObject_PasswordChange_user_20 = PasswordChange - { cpOldPassword = + { oldPassword = Just ( plainTextPassword6Unsafe "{dNa\GSEIDDNi\"&P\"Dx~\96634s \NAK&\ESC\SOHe\917580,/}}@\1024844G\USRi\177540\vG\EOT\1068093\"dIcX\128456?\53433h4\RS8}E\b\fAI+\138835\DLEN\ETBg'l\DC4\DEL1lg}\1002968\GS9u\t=\186263)\1048038A)\ETBBD\ENQ\1741%\CAN3\b+,\151430G)0%\74936\78333t8\1105056\CAN\988091oU\DC2N\NUL\DC1U~\1100670[\138598\1110439\&6##\151597<\a\SYN\986482V6\vb>\NULh\NAKq\f\176602<5dHa\tg\DEL\24672\66025\&2=tZ\1050161L9M\a2k\1001329\987951vOkA\r)\r\60697O \63131lNli\34835\\\"b`G\52957\1039861\161828n\DLEP\1077887i0k\1015841w\1040786?\\\ENQg\1005909\RS[Z\SOHN\SOH\CAN\186595:\FS\185811\40960_kBD\"C\DLEB4]w\DEL/JF\NUL?L9V\\9\1096654W\1104044'\FS={e\153126>\1098415\139415D\1112130A\a7G\ETXb\983698Crt*Y\nhD\150279\&5\151537)F\NUL\ETB*\1035725yCu\ETX\SUB%\SIbZ; G\1079499)\SO\1012440_\NAK\DC3,~\175703\SO\153562d\1101051\1084728\&4\1018181R\1059397\19127\1099372\1004409^\161681\32886\&1\DC3\USn\1102891*!\FS|\r=\166562[ql\189334S\NAKr\ESC'\SUBE(\SYN\f{\1112073 |\b\50511\42582\155138\1009867E\NAK\139848\&4\151681\t\68617X\1000541\EOT\1104748.Z\1085819\177246\176778\DC4t\CAN\DC3\23081\&5HV\ESC:$e,L\STX\992003\&7R\1012763.wq\62951\24985:\60845\SOH\SOH\a\67714\8047\&0*.\1022795\1087787\120217P\r\b\167713\1096692\&5\147092\121232\149850\DC1\bsc-\1082366i\DC1\2721\183884\154420A\NAK$\190574jNR\917908\SI\120778\16684\989256;\5681\1057323\SYNRd\STXI;\EM\aK\20933\59636,\EM%\1073632\ENQ\1089709J\1061355nR$Spf\1093436Lsp\1046367[\\\1105079\97069y\t\SYNbC7}|\DLE\SUBg5@]2\1017800S4E\be[\1054254\&6\RS4\146792z\DC2d\nm\83369/JqK\SOHQf\1081923\1079670\&0\95005\SOHHa\1014928&8\111343\61186)~m\101024\RS\vG\SYNz(p\EOT\1052203V\f4\"^+?A|\1037820\n\340r\USF&\CANt\1037756r\tP\SYNDW\DLE2\DC1E|PgQ/\1055897\1034173P\rNH\SYNS\30936\1050463\29463" ), - cpNewPassword = + newPassword = plainTextPassword8Unsafe "\tNYm7k;\985171|w~ue]St\52529[\GS\983717e\DC21\EM\SI\"C\1059834\&0\1003638!\995247|xiw\1027219~YT\57860+\ESC'\185609\&6\1010421(;\ETX!\b\1071987&Y9tW\984137\72988\GS\ACK\1083519\1086906\1107857&a,\NAK\31149\1088114y/d\1080408\SUB\169799\150046wOS\atp\1000950B\181672\ETBi\DC2\1090827\1080180\DELek=r\138679\60557\ESCrf\3126UkFh\ENQ9YA\t]\NULUJS@1?o_-P\ETBxW\171817\139732\48291<\1060487\133433&\DC4\SO6na\1000867!Z}'H\1052135w\r0W*\24217J\SYNIwk\9238AZ\1023004\30337\1013798w\1015506\ETX>\1080073S\158446\1061588o\190641\175249\1070034J\EOTu\STX:(\1066396\172284\1054181@\1030039\n\DC3xMJ\30746\147879Oxj@Np\1066698\1000349\1087808x\SI\ACK\US\988847T\v_w,)w7j\ACK\1046770\1038846\US'h\31697\&4\NAK\138144V\37643g\f\1099746\&3\129560\ETXR\SIPdc{a\STX\191154\DLE\ETX((\CANf\EOT\f\188879e~[+\RSg=g#&MQ\DC2%4\r\r\ETX\65235<\170329#\1109142\&5\36874\USv\bpt\DC3'\EMF\"2\1113106\SUBe\1087311$\1010352 \1068376bK>m>\f.\1052106m\64101MvQ\1065915Q\70336\177129)/\1056483<\CANy\995545J!\DEL#1\v\aq\DC2\1102215\DLE\CAN\1089020D\ACK7W\EMw\"\151987\&3\STXv\21304\126082\ETXxW\189371\1054427<^~\993642\r:WGlhl-!|W.\3598;n\1077840`<\CAN\1109050;NJi\DC3\53248\t ]\DLEH\100145_Z\996436\24307\"\185147\1002533\71437\24999a\DEL\US\1084155\132179\&4U\1017349v\1098626S\166457S.\36067i(\ENQB|VD\43028gW\"->N4\153954R\190825\992013\DC1\NAK\59376\20565%\160113[`\120495@B\168437qjKW\DLEm z\1034188\167428j\1029865P%\SI\98769._\r\DC1(N\990561\DC3\b\DC1\1072625e\41522'olW\ACK>\SYNp\988282H\RSe{\"RN\51331\ETB\DC2\">\1007951Q\DLEYoj+~\FSSMU\"ubD\142953KtW\FST\99243\20978\SOHQm:\RS8)g\1040404\ayZ\156789\1022349E\99162j n83Hf\163774\DC3\47323/2C\DC4\FS]A8-\1067911vp` PlainTextPassword6 -> AuthenticationSubsystem m (Either AuthError ()) + ReauthenticateEither :: UserId -> Maybe PlainTextPassword6 -> AuthenticationSubsystem m (Either ReAuthError ()) CreatePasswordResetCode :: EmailKey -> AuthenticationSubsystem m () ResetPassword :: PasswordResetIdentity -> PasswordResetCode -> PlainTextPassword8 -> AuthenticationSubsystem m () VerifyPassword :: PlainTextPassword6 -> Password -> AuthenticationSubsystem m (Bool, PasswordStatus) @@ -39,3 +43,21 @@ data AuthenticationSubsystem m a where InternalLookupPasswordResetCode :: EmailKey -> AuthenticationSubsystem m (Maybe PasswordResetPair) makeSem ''AuthenticationSubsystem + +authenticate :: + ( Member (Error AuthError) r, + Member AuthenticationSubsystem r + ) => + UserId -> + PlainTextPassword6 -> + Sem r () +authenticate uid pwd = authenticateEither uid pwd >>= either throw pure + +reauthenticate :: + ( Member (Error ReAuthError) r, + Member AuthenticationSubsystem r + ) => + UserId -> + Maybe PlainTextPassword6 -> + Sem r () +reauthenticate uid pwd = reauthenticateEither uid pwd >>= either throw pure diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs index 095bb9dfdbc..79d4d57dfd5 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Error.hs @@ -14,24 +14,41 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.AuthenticationSubsystem.Error - ( AuthenticationSubsystemError (..), - authenticationSubsystemErrorToHttpError, - ) -where +module Wire.AuthenticationSubsystem.Error where import Imports import Wire.API.Error import Wire.API.Error.Brig qualified as E import Wire.Error +-- | Authentication errors. +data AuthError + = AuthInvalidUser + | AuthInvalidCredentials + | AuthSuspended + | AuthEphemeral + | AuthPendingInvitation + deriving (Show, Eq) + +instance Exception AuthError + +-- | Re-authentication errors. +data ReAuthError + = ReAuthError !AuthError + | ReAuthMissingPassword + | ReAuthCodeVerificationRequired + | ReAuthCodeVerificationNoPendingCode + | ReAuthCodeVerificationNoEmail + deriving (Show, Eq) + +instance Exception ReAuthError + data AuthenticationSubsystemError = AuthenticationSubsystemInvalidPasswordResetKey | AuthenticationSubsystemResetPasswordMustDiffer | AuthenticationSubsystemInvalidPasswordResetCode | AuthenticationSubsystemInvalidPhone | AuthenticationSubsystemAllowListError - | AuthenticationSubsystemMissingAuth | AuthenticationSubsystemBadCredentials deriving (Eq, Show) @@ -45,5 +62,4 @@ authenticationSubsystemErrorToHttpError = AuthenticationSubsystemResetPasswordMustDiffer -> errorToWai @E.ResetPasswordMustDiffer AuthenticationSubsystemInvalidPhone -> errorToWai @E.InvalidPhone AuthenticationSubsystemAllowListError -> errorToWai @E.AllowlistError - AuthenticationSubsystemMissingAuth -> errorToWai @E.MissingAuth AuthenticationSubsystemBadCredentials -> errorToWai @E.BadCredentials diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs index 89dc1f3b39a..3c6b5a63280 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem/Interpreter.hs @@ -23,11 +23,12 @@ module Wire.AuthenticationSubsystem.Interpreter where import Data.ByteString.Conversion +import Data.HavePendingInvitations import Data.Id import Data.Misc import Data.Qualified import Data.Time -import Imports hiding (lookup) +import Imports hiding (local, lookup) import Polysemy import Polysemy.Error import Polysemy.Input @@ -36,21 +37,24 @@ import Polysemy.TinyLog qualified as Log import System.Logger import Wire.API.Allowlists (AllowlistEmailDomains) import Wire.API.Allowlists qualified as AllowLists -import Wire.API.Password as Password +import Wire.API.Password (Password, PasswordStatus (..)) +import Wire.API.Password qualified as Password +import Wire.API.Password qualified as Pasword import Wire.API.User import Wire.API.User.Password -import Wire.AuthenticationSubsystem (AuthenticationSubsystem (..)) +import Wire.AuthenticationSubsystem import Wire.AuthenticationSubsystem.Error import Wire.EmailSubsystem import Wire.HashPassword import Wire.PasswordResetCodeStore -import Wire.PasswordStore (PasswordStore) +import Wire.PasswordStore (PasswordStore, upsertHashedPassword) import Wire.PasswordStore qualified as PasswordStore import Wire.Sem.Now import Wire.Sem.Now qualified as Now import Wire.SessionStore import Wire.UserKeyStore -import Wire.UserSubsystem (UserSubsystem) +import Wire.UserStore +import Wire.UserSubsystem (UserSubsystem, getLocalAccountBy) import Wire.UserSubsystem qualified as User interpretAuthenticationSubsystem :: @@ -64,13 +68,16 @@ interpretAuthenticationSubsystem :: Member (Input (Local ())) r, Member (Input (Maybe AllowlistEmailDomains)) r, Member PasswordStore r, - Member EmailSubsystem r + Member EmailSubsystem r, + Member UserStore r ) => InterpreterFor UserSubsystem r -> InterpreterFor AuthenticationSubsystem r interpretAuthenticationSubsystem userSubsystemInterpreter = interpret $ userSubsystemInterpreter . \case + AuthenticateEither uid pwd -> authenticateEitherImpl uid pwd + ReauthenticateEither uid pwd -> reauthenticateEitherImpl uid pwd CreatePasswordResetCode userKey -> createPasswordResetCodeImpl userKey ResetPassword ident resetCode newPassword -> resetPasswordImpl ident resetCode newPassword VerifyPassword plaintext pwd -> verifyPasswordImpl plaintext pwd @@ -98,6 +105,70 @@ instance Exception PasswordResetError where displayException InvalidResetKey = "invalid reset key for password reset" displayException InProgress = "password reset already in progress" +authenticateEitherImpl :: + ( Member UserStore r, + Member HashPassword r, + Member PasswordStore r + ) => + UserId -> + PlainTextPassword6 -> + Sem r (Either AuthError ()) +authenticateEitherImpl uid plaintext = do + runError $ + getUserAuthenticationInfo uid >>= \case + Nothing -> throw AuthInvalidUser + Just (_, Deleted) -> throw AuthInvalidUser + Just (_, Suspended) -> throw AuthSuspended + Just (_, Ephemeral) -> throw AuthEphemeral + Just (_, PendingInvitation) -> throw AuthPendingInvitation + Just (Nothing, _) -> throw AuthInvalidCredentials + Just (Just password, Active) -> do + case Pasword.verifyPasswordWithStatus plaintext password of + (False, _) -> throw AuthInvalidCredentials + (True, PasswordStatusNeedsUpdate) -> do + (hashAndUpdatePwd uid plaintext) + (True, _) -> pure () + where + hashAndUpdatePwd u pwd = do + hashed <- hashPassword6 pwd + upsertHashedPassword u hashed + +-- | Password reauthentication. If the account has a password, reauthentication +-- is mandatory. If +-- * User has no password, re-auth is a no-op +-- * User is an SSO user and no password is given, re-auth is a no-op. +reauthenticateEitherImpl :: + ( Member UserStore r, + Member UserSubsystem r, + Member (Input (Local ())) r + ) => + UserId -> + Maybe (PlainTextPassword' t) -> + Sem r (Either ReAuthError ()) +reauthenticateEitherImpl user plaintextMaybe = + getUserAuthenticationInfo user + >>= runError + . \case + Nothing -> throw (ReAuthError AuthInvalidUser) + Just (_, Deleted) -> throw (ReAuthError AuthInvalidUser) + Just (_, Suspended) -> throw (ReAuthError AuthSuspended) + Just (_, PendingInvitation) -> throw (ReAuthError AuthPendingInvitation) + Just (Nothing, _) -> for_ plaintextMaybe $ const (throw $ ReAuthError AuthInvalidCredentials) + Just (Just pw', Active) -> maybeReAuth pw' + Just (Just pw', Ephemeral) -> maybeReAuth pw' + where + maybeReAuth pw' = case plaintextMaybe of + Nothing -> do + local <- input + musr <- getLocalAccountBy NoPendingInvitations (qualifyAs local user) + let isSaml = maybe False isSamlUser musr + -- If this is a SAML user, re-auth should be no-op so no error is thrown. + unless isSaml $ + throw ReAuthMissingPassword + Just p -> + unless (Password.verifyPassword p pw') do + throw (ReAuthError AuthInvalidCredentials) + createPasswordResetCodeImpl :: forall r. ( Member PasswordResetCodeStore r, @@ -225,7 +296,7 @@ resetPasswordImpl ident code pw = do Just uid -> do Log.debug $ field "user" (toByteString uid) . field "action" (val "User.completePasswordReset") checkNewIsDifferent uid pw - hashedPw <- hashPassword pw + hashedPw <- hashPassword8 pw PasswordStore.upsertHashedPassword uid hashedPw codeDelete key deleteAllCookies uid @@ -271,7 +342,9 @@ verifyPasswordImpl plaintext password = do pure $ Password.verifyPasswordWithStatus plaintext password verifyProviderPasswordImpl :: - (Member PasswordStore r, Member (Error AuthenticationSubsystemError) r) => + ( Member PasswordStore r, + Member (Error AuthenticationSubsystemError) r + ) => ProviderId -> PlainTextPassword6 -> Sem r (Bool, PasswordStatus) @@ -283,7 +356,9 @@ verifyProviderPasswordImpl pid plaintext = do verifyPasswordImpl plaintext password verifyUserPasswordImpl :: - (Member PasswordStore r, Member (Error AuthenticationSubsystemError) r) => + ( Member PasswordStore r, + Member (Error AuthenticationSubsystemError) r + ) => UserId -> PlainTextPassword6 -> Sem r (Bool, PasswordStatus) diff --git a/libs/wire-subsystems/src/Wire/HashPassword.hs b/libs/wire-subsystems/src/Wire/HashPassword.hs index 48444c0d691..c91854f4316 100644 --- a/libs/wire-subsystems/src/Wire/HashPassword.hs +++ b/libs/wire-subsystems/src/Wire/HashPassword.hs @@ -1,7 +1,9 @@ +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} module Wire.HashPassword where +import Crypto.KDF.Argon2 qualified as Argon2 import Data.Misc import Imports import Polysemy @@ -9,10 +11,26 @@ import Wire.API.Password (Password) import Wire.API.Password qualified as Password data HashPassword m a where - HashPassword :: PlainTextPassword8 -> HashPassword m Password + HashPassword6 :: PlainTextPassword6 -> HashPassword m Password + HashPassword8 :: PlainTextPassword8 -> HashPassword m Password makeSem ''HashPassword -runHashPassword :: (Member (Embed IO) r) => InterpreterFor HashPassword r -runHashPassword = interpret $ \case - HashPassword pw -> liftIO $ Password.mkSafePassword pw +runHashPassword :: + ( Member (Embed IO) r + ) => + Argon2.Options -> + 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 diff --git a/libs/wire-subsystems/src/Wire/UserStore.hs b/libs/wire-subsystems/src/Wire/UserStore.hs index 6ebb55c71cf..a5189d29818 100644 --- a/libs/wire-subsystems/src/Wire/UserStore.hs +++ b/libs/wire-subsystems/src/Wire/UserStore.hs @@ -10,6 +10,7 @@ import Data.Time.Clock import Imports import Polysemy import Polysemy.Error +import Wire.API.Password import Wire.API.User import Wire.API.User.RichInfo import Wire.Arbitrary @@ -71,6 +72,7 @@ data UserStore m a where UpdateUserTeam :: UserId -> TeamId -> UserStore m () GetActivityTimestamps :: UserId -> UserStore m [Maybe UTCTime] GetRichInfo :: UserId -> UserStore m (Maybe RichInfoAssocList) + GetUserAuthenticationInfo :: UserId -> UserStore m (Maybe (Maybe Password, AccountStatus)) makeSem ''UserStore diff --git a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs index db15b04f4b4..96e78df99d3 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/Cassandra.hs @@ -10,6 +10,7 @@ import Imports import Polysemy import Polysemy.Embed import Polysemy.Error +import Wire.API.Password (Password) import Wire.API.User hiding (DeleteUser) import Wire.API.User.RichInfo import Wire.StoredUser @@ -35,6 +36,17 @@ interpretUserStoreCassandra casClient = UpdateUserTeam uid tid -> updateUserTeamImpl uid tid GetActivityTimestamps uid -> getActivityTimestampsImpl uid GetRichInfo uid -> getRichInfoImpl uid + GetUserAuthenticationInfo uid -> getUserAuthenticationInfoImpl uid + +getUserAuthenticationInfoImpl :: UserId -> Client (Maybe (Maybe Password, AccountStatus)) +getUserAuthenticationInfoImpl uid = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity uid))) + where + f (pw, st) = (pw, fromMaybe Active st) + authSelect :: PrepQuery R (Identity UserId) (Maybe Password, Maybe AccountStatus) + authSelect = + [sql| + SELECT password, status FROM user WHERE id = ? + |] getUsersImpl :: [UserId] -> Client [StoredUser] getUsersImpl usrs = diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem.hs b/libs/wire-subsystems/src/Wire/UserSubsystem.hs index c8237825748..f53da756a00 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem.hs @@ -162,9 +162,6 @@ getLocalUserProfile :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe getLocalUserProfile targetUser = listToMaybe <$> getLocalUserProfiles ((: []) <$> targetUser) -getLocalUser :: (Member UserSubsystem r) => Local UserId -> Sem r (Maybe User) -getLocalUser = (selfUser <$$>) . getSelfProfile - getLocalAccountBy :: (Member UserSubsystem r) => HavePendingInvitations -> diff --git a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs index f553aa595dc..87509b688de 100644 --- a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs @@ -33,7 +33,9 @@ import Wire.PasswordStore import Wire.Sem.Logger.TinyLog import Wire.Sem.Now (Now) import Wire.SessionStore +import Wire.StoredUser import Wire.UserKeyStore +import Wire.UserStore type AllEffects = [ AuthenticationSubsystem, @@ -50,6 +52,8 @@ type AllEffects = State (Map PasswordResetKey (PRQueryData Identity)), TinyLog, EmailSubsystem, + UserStore, + State [StoredUser], State (Map EmailAddress [SentMail]) ] @@ -57,6 +61,8 @@ runAllEffects :: Domain -> [User] -> Maybe [Text] -> Sem AllEffects a -> Either runAllEffects localDomain preexistingUsers mAllowedEmailDomains = run . evalState mempty + . evalState mempty + . inMemoryUserStoreInterpreter . emailSubsystemInterpreter . discardTinyLogs . evalState mempty @@ -320,7 +326,7 @@ verifyPasswordProp plainTextPassword passwordHash = counterexample ("Password doesn't match, plainText=" <> show plainTextPassword <> ", passwordHash=" <> show passwordHash) $ fmap (Password.verifyPassword plainTextPassword) passwordHash == Just True -hashAndUpsertPassword :: (Member PasswordStore r, Member HashPassword r) => UserId -> PlainTextPassword8 -> Sem r () +hashAndUpsertPassword :: (Member PasswordStore r) => UserId -> PlainTextPassword8 -> Sem r () hashAndUpsertPassword uid password = upsertHashedPassword uid =<< hashPassword password diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs index 84c8897292a..6684ca34c47 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/HashPassword.hs @@ -5,23 +5,29 @@ import Data.Misc import Data.Text.Encoding qualified as Text import Imports import Polysemy -import Wire.API.Password +import Wire.API.Password as Password import Wire.HashPassword staticHashPasswordInterpreter :: InterpreterFor HashPassword r staticHashPasswordInterpreter = interpret $ \case - HashPassword password -> - pure . Argon2Password $ - hashPasswordArgon2idWithOptions - fastArgon2IdOptions - "9bytesalt" - (Text.encodeUtf8 (fromPlainTextPassword password)) + HashPassword6 password -> hashPassword password + HashPassword8 password -> hashPassword password + +hashPassword :: (Monad m) => PlainTextPassword' t -> m Password +hashPassword password = + pure . Argon2Password $ + hashPasswordArgon2idWithSalt + fastArgon2IdOptions + "9bytesalt" + (Text.encodeUtf8 (fromPlainTextPassword password)) fastArgon2IdOptions :: Argon2.Options fastArgon2IdOptions = let hashParallelism = 4 - in defaultOptions - { iterations = 1, + in Argon2.Options + { variant = Argon2.Argon2id, + version = Argon2.Version13, + iterations = 1, parallelism = hashParallelism, -- This needs to be min 8 * hashParallelism, otherewise we get an -- unsafe error diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs index 650aeb60dfa..133365cf986 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs @@ -73,6 +73,7 @@ inMemoryUserStoreInterpreter = interpret $ \case (\u -> if u.id == uid then u {teamId = Just tid} :: StoredUser else u) GetActivityTimestamps _ -> pure [] GetRichInfo _ -> error "rich info not implemented" + GetUserAuthenticationInfo _uid -> error "Not implemented" storedUserToIndexUser :: StoredUser -> IndexUser storedUserToIndexUser storedUser = diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index 3aa2ea8ba36..f814af8c799 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -228,6 +228,10 @@ optSettings: setOAuthEnabled: true setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks setOAuthMaxActiveRefreshTokens: 10 + setPasswordHashingOptions: # in testing, we want these settings to be faster, not secure against attacks. + iterations: 1 + memory: 128 + parallelism: 1 logLevel: Warn logNetStrings: false diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index 4c3e9c4a563..b0f7295cacf 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -22,7 +22,6 @@ import Brig.API.Handler import Brig.API.Types import Brig.API.User import Brig.App -import Brig.Data.User qualified as User import Brig.Options import Brig.User.Auth qualified as Auth import Brig.ZAuth hiding (Env, settings) @@ -49,12 +48,12 @@ import Wire.API.User.Auth hiding (access) import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso -import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem +import Wire.AuthenticationSubsystem qualified as Authentication import Wire.BlockListStore import Wire.EmailSubsystem (EmailSubsystem) import Wire.Events (Events) import Wire.GalleyAPIAccess -import Wire.PasswordStore (PasswordStore) import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem @@ -97,7 +96,6 @@ sendLoginCode _ = login :: ( Member GalleyAPIAccess r, Member TinyLog r, - Member PasswordStore r, Member UserKeyStore r, Member UserStore r, Member Events r, @@ -162,7 +160,6 @@ listCookies lusr (fold -> labels) = removeCookies :: ( Member TinyLog r, - Member PasswordStore r, Member UserSubsystem r, Member AuthenticationSubsystem r ) => @@ -213,7 +210,8 @@ reauthenticate :: ReAuthUser -> Handler r () reauthenticate luid@(tUnqualified -> uid) body = do - User.reauthenticate uid body.reAuthPassword !>> reauthError + (lift . liftSem $ Authentication.reauthenticateEither uid body.reAuthPassword) + >>= either (throwE . reauthError) (const $ pure ()) case reAuthCodeAction body of Just action -> Auth.verifyCode (reAuthCode body) action luid diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index d5282714d12..9a94f880659 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -103,6 +103,7 @@ import Wire.API.User.Client.Prekey import Wire.API.UserEvent import Wire.API.UserMap (QualifiedUserMap (QualifiedUserMap, qualifiedUserMap), UserMap (userMap)) import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem qualified as Authentication import Wire.DeleteQueue import Wire.EmailSubsystem (EmailSubsystem, sendNewClientEmail) import Wire.Events (Events) @@ -271,7 +272,9 @@ rmClient u con clt pw = -- Temporary clients don't need to re-auth TemporaryClientType -> pure () -- All other clients must authenticate - _ -> Data.reauthenticate u pw !>> ClientDataError . ClientReAuthError + _ -> + (lift . liftSem $ Authentication.reauthenticateEither u pw) + >>= either (throwE . ClientDataError . ClientReAuthError) (const $ pure ()) lift $ execDelete u (Just con) client claimPrekey :: diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index c27c5ba7a00..9cb812d48b7 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -65,7 +65,7 @@ import Data.Time.Clock.System import Imports hiding (head) import Network.Wai.Utilities as Utilities import Polysemy -import Polysemy.Error qualified +import Polysemy.Error qualified as Polysemy import Polysemy.Input (Input, input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) @@ -103,6 +103,7 @@ import Wire.FederationConfigStore ) import Wire.FederationConfigStore qualified as E import Wire.GalleyAPIAccess (GalleyAPIAccess) +import Wire.HashPassword (HashPassword) import Wire.IndexedUserStore (IndexedUserStore, getTeamSize) import Wire.InvitationStore import Wire.NotificationSubsystem @@ -145,7 +146,8 @@ servantSitemap :: Member PropertySubsystem r, Member (Input (Local ())) r, Member IndexedUserStore r, - Member (Polysemy.Error.Error UserSubsystemError) r + Member (Polysemy.Error UserSubsystemError) r, + Member HashPassword r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -197,6 +199,7 @@ accountAPI :: Member PropertySubsystem r, Member Events r, Member PasswordResetCodeStore r, + Member HashPassword r, Member InvitationStore r ) => ServerT BrigIRoutes.AccountAPI (Handler r) @@ -247,7 +250,7 @@ teamsAPI :: Member InvitationStore r, Member TeamInvitationSubsystem r, Member UserSubsystem r, - Member (Polysemy.Error.Error UserSubsystemError) r, + Member (Polysemy.Error UserSubsystemError) r, Member Events r, Member (Input (Local ())) r, Member IndexedUserStore r @@ -470,6 +473,7 @@ createUserNoVerify :: Member UserKeyStore r, Member UserSubsystem r, Member (Input (Local ())) r, + Member HashPassword r, Member PasswordResetCodeStore r ) => NewUser -> @@ -490,6 +494,7 @@ createUserNoVerifySpar :: Member TinyLog r, Member UserSubsystem r, Member Events r, + Member HashPassword r, Member PasswordResetCodeStore r ) => NewUserSpar -> diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 0ab2f89a4fe..9176f14553e 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -26,7 +26,6 @@ where import Brig.API.Error (throwStd) import Brig.API.Handler (Handler) import Brig.App -import Brig.Data.User import Brig.Options qualified as Opt import Cassandra hiding (Set) import Cassandra qualified as C @@ -45,7 +44,6 @@ import Data.Text.Ascii import Data.Text.Encoding qualified as T import Data.Time import Imports hiding (exp) -import Network.Wai.Utilities.Error import OpenSSL.Random (randBytes) import Polysemy (Member) import Servant hiding (Handler, Tagged) @@ -57,7 +55,11 @@ import Wire.API.Routes.Internal.Brig.OAuth qualified as I import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig.OAuth import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem qualified as Authentication +import Wire.AuthenticationSubsystem.Error import Wire.Error +import Wire.HashPassword (HashPassword) +import Wire.HashPassword qualified as HashPassword import Wire.Sem.Jwk import Wire.Sem.Jwk qualified as Jwk import Wire.Sem.Now (Now) @@ -66,7 +68,7 @@ import Wire.Sem.Now qualified as Now -------------------------------------------------------------------------------- -- API Internal -internalOauthAPI :: ServerT I.OAuthAPI (Handler r) +internalOauthAPI :: (Member HashPassword r) => ServerT I.OAuthAPI (Handler r) internalOauthAPI = Named @"create-oauth-client" registerOAuthClient :<|> Named @"i-get-oauth-client" getOAuthClientById @@ -95,19 +97,25 @@ oauthAPI = -------------------------------------------------------------------------------- -- Handlers -registerOAuthClient :: OAuthClientConfig -> (Handler r) OAuthClientCredentials +registerOAuthClient :: (Member HashPassword r) => OAuthClientConfig -> (Handler r) OAuthClientCredentials registerOAuthClient (OAuthClientConfig name uri) = do guardOAuthEnabled credentials@(OAuthClientCredentials cid secret) <- OAuthClientCredentials <$> randomId <*> createSecret - safeSecret <- liftIO $ hashClientSecret secret + safeSecret <- hashClientSecret secret lift $ wrapClient $ insertOAuthClient cid name uri safeSecret pure credentials where createSecret :: (MonadIO m) => m OAuthClientPlainTextSecret createSecret = OAuthClientPlainTextSecret <$> rand32Bytes - hashClientSecret :: (MonadIO m) => OAuthClientPlainTextSecret -> m Password - hashClientSecret = mkSafePassword . plainTextPassword8Unsafe . toText . unOAuthClientPlainTextSecret + hashClientSecret :: (Member HashPassword r) => OAuthClientPlainTextSecret -> (Handler r) Password + hashClientSecret = + lift + . liftSem + . HashPassword.hashPassword8 + . plainTextPassword8Unsafe + . toText + . unOAuthClientPlainTextSecret rand32Bytes :: (MonadIO m) => m AsciiBase16 rand32Bytes = liftIO . fmap encodeBase16 $ randBytes 32 @@ -358,7 +366,8 @@ revokeOAuthAccountAccess :: PasswordReqBody -> (Handler r) () revokeOAuthAccountAccess luid@(tUnqualified -> uid) cid req = do - reauthenticate uid req.fromPasswordReqBody !>> toAccessDenied + (lift . liftSem $ Authentication.reauthenticateEither uid req.fromPasswordReqBody) + >>= either (throwE . toAccessDenied) (const $ pure ()) revokeOAuthAccountAccessV6 luid cid where toAccessDenied :: ReAuthError -> HttpError @@ -372,7 +381,8 @@ deleteOAuthRefreshTokenById :: PasswordReqBody -> (Handler r) () deleteOAuthRefreshTokenById (tUnqualified -> uid) cid tokenId req = do - reauthenticate uid req.fromPasswordReqBody !>> toAccessDenied + (lift . liftSem $ Authentication.reauthenticateEither uid req.fromPasswordReqBody) + >>= either (throwE . toAccessDenied) (const $ pure ()) mInfo <- lift $ wrapClient $ lookupOAuthRefreshTokenInfo tokenId case mInfo of Nothing -> pure () diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 7c9128f96ae..c49323beb6f 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -156,6 +156,7 @@ import Wire.Events (Events) import Wire.FederationConfigStore (FederationConfigStore) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.HashPassword (HashPassword) import Wire.IndexedUserStore (IndexedUserStore) import Wire.InvitationStore import Wire.NotificationSubsystem @@ -304,7 +305,8 @@ servantSitemap :: Member (Concurrency 'Unsafe) r, Member BlockListStore r, Member (ConnectionStore InternalPaging) r, - Member IndexedUserStore r + Member IndexedUserStore r, + Member HashPassword r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -750,6 +752,7 @@ createUser :: Member Events r, Member UserSubsystem r, Member PasswordResetCodeStore r, + Member HashPassword r, Member EmailSending r ) => Public.NewUserPublic -> @@ -974,7 +977,14 @@ removeEmail self = lift . exceptTToMaybe $ API.removeEmail self checkPasswordExists :: (Member PasswordStore r) => UserId -> (Handler r) Bool checkPasswordExists = fmap isJust . lift . liftSem . lookupHashedPassword -changePassword :: (Member PasswordStore r, Member UserStore r) => UserId -> Public.PasswordChange -> (Handler r) (Maybe Public.ChangePasswordError) +changePassword :: + ( Member PasswordStore r, + Member UserStore r, + Member HashPassword r + ) => + UserId -> + Public.PasswordChange -> + (Handler r) (Maybe Public.ChangePasswordError) changePassword u cp = lift . exceptTToMaybe $ API.changePassword u cp changeLocale :: diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index d83e258932e..5da615a530f 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -32,7 +32,6 @@ where import Brig.Data.Activation (Activation (..), ActivationError (..)) import Brig.Data.Client (ClientDataError (..)) -import Brig.Data.User (AuthError (..), ReAuthError (..)) import Brig.Types.Intra import Data.Code import Data.Id @@ -43,6 +42,7 @@ import Imports import Network.Wai.Utilities.Error qualified as Wai import Wire.API.Federation.Error import Wire.API.User +import Wire.AuthenticationSubsystem.Error import Wire.UserKeyStore ------------------------------------------------------------------------------- diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 8ea615fc830..daf3109c63e 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -46,7 +46,6 @@ module Brig.API.User deleteAccount, checkHandles, isBlacklistedHandle, - Data.reauthenticate, -- * Activation sendActivationCode, @@ -136,6 +135,8 @@ import Wire.Error import Wire.Events (Events) import Wire.Events qualified as Events import Wire.GalleyAPIAccess as GalleyAPIAccess +import Wire.HashPassword (HashPassword) +import Wire.HashPassword qualified as HashPassword import Wire.InvitationStore (InvitationStore, StoredInvitation) import Wire.InvitationStore qualified as InvitationStore import Wire.NotificationSubsystem @@ -190,6 +191,7 @@ createUserSpar :: ( Member GalleyAPIAccess r, Member TinyLog r, Member UserSubsystem r, + Member HashPassword r, Member Events r ) => NewUserSpar -> @@ -202,7 +204,7 @@ createUserSpar new = do -- Create account account <- lift $ do - (account, pw) <- wrapClient $ newAccount new' Nothing (Just tid) handle' + (account, pw) <- newAccount new' Nothing (Just tid) handle' let uid = userId account @@ -315,6 +317,7 @@ createUser :: Member Events r, Member (Input (Local ())) r, Member PasswordResetCodeStore r, + Member HashPassword r, Member InvitationStore r ) => NewUser -> @@ -368,7 +371,7 @@ createUser new = do -- Create account account <- lift $ do - (account, pw) <- wrapClient $ newAccount new' mbInv tid mbHandle + (account, pw) <- newAccount new' mbInv tid mbHandle let uid = userId account liftSem $ do @@ -838,15 +841,22 @@ mkActivationKey (ActivateEmail e) = ------------------------------------------------------------------------------- -- Password Management -changePassword :: (Member PasswordStore r, Member UserStore r) => UserId -> PasswordChange -> ExceptT ChangePasswordError (AppT r) () +changePassword :: + ( Member PasswordStore r, + Member UserStore r, + Member HashPassword r + ) => + UserId -> + PasswordChange -> + ExceptT ChangePasswordError (AppT r) () changePassword uid cp = do activated <- lift $ liftSem $ isActivated uid unless activated $ throwE ChangePasswordNoIdentity currpw <- lift $ liftSem $ lookupHashedPassword uid - let newpw = cpNewPassword cp - hashedNewPw <- mkSafePassword newpw - case (currpw, cpOldPassword cp) of + let newpw = cp.newPassword + hashedNewPw <- lift . liftSem $ HashPassword.hashPassword8 newpw + case (currpw, cp.oldPassword) of (Nothing, _) -> lift . liftSem $ upsertHashedPassword uid hashedNewPw (Just _, Nothing) -> throwE InvalidCurrentPassword (Just pw, Just pw') -> do diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index e0f9672bf57..0ceacfba5ee 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -33,6 +33,7 @@ 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 @@ -264,7 +265,7 @@ runBrigToIO e (AppT ma) = do . interpretIndexedUserStoreES indexedUserStoreConfig . interpretUserStoreCassandra e.casClient . interpretUserKeyStoreCassandra e.casClient - . runHashPassword + . runHashPassword (argon2OptsFromHashingOpts e.settings.passwordHashingOptions) . interpretFederationAPIAccess federationApiAccessConfig . rethrowHttpErrorIO . mapError propertySubsystemErrorToHttpError diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 320d096d6e2..d82762802e5 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -55,8 +55,6 @@ import Amazonka.DynamoDB.Lens qualified as AWS import Bilge.Retry (httpHandlers) import Brig.AWS import Brig.App -import Brig.Data.User (AuthError (..), ReAuthError (..)) -import Brig.Data.User qualified as User import Brig.Types.Instances () import Cassandra as C hiding (Client) import Cassandra.Settings as C hiding (Client) @@ -92,6 +90,8 @@ import Wire.API.User.Client hiding (UpdateClient (..)) import Wire.API.User.Client.Prekey import Wire.API.UserMap (UserMap (..)) import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem qualified as Authentication +import Wire.AuthenticationSubsystem.Error data ClientDataError = TooManyClients @@ -144,9 +144,9 @@ addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients caps = do let typed = filter ((== newClientType c) . clientType) clients let count = length typed let upsert = any exists typed - when (reAuthPolicy count upsert) $ - fmapLT ClientReAuthError $ - User.reauthenticate (tUnqualified u) (newClientPassword c) + when (reAuthPolicy count upsert) do + (lift . liftSem $ Authentication.reauthenticateEither (tUnqualified u) (newClientPassword c)) + >>= either (throwE . ClientReAuthError) pure let capacity = fmap (+ (-count)) limit unless (maybe True (> 0) capacity || upsert) $ throwE TooManyClients diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index aed64c559bc..7e1f8e57656 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -18,18 +18,10 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . --- for Show UserRowInsert - --- TODO: Move to Brig.User.Account.DB module Brig.Data.User - ( AuthError (..), - ReAuthError (..), - newAccount, + ( newAccount, newAccountInviteViaScim, insertAccount, - authenticate, - reauthenticate, - isSamlUser, -- * Lookups lookupUser, @@ -73,7 +65,6 @@ import Data.Handle (Handle) import Data.HavePendingInvitations import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) -import Data.Misc import Data.Qualified import Data.Range (fromRange) import Data.Time (addUTCTime) @@ -85,24 +76,7 @@ import Wire.API.Provider.Service import Wire.API.Team.Feature import Wire.API.User import Wire.API.User.RichInfo -import Wire.AuthenticationSubsystem as AuthenticationSubsystem -import Wire.PasswordStore - --- | Authentication errors. -data AuthError - = AuthInvalidUser - | AuthInvalidCredentials - | AuthSuspended - | AuthEphemeral - | AuthPendingInvitation - --- | Re-authentication errors. -data ReAuthError - = ReAuthError !AuthError - | ReAuthMissingPassword - | ReAuthCodeVerificationRequired - | ReAuthCodeVerificationNoPendingCode - | ReAuthCodeVerificationNoEmail +import Wire.HashPassword -- | Preconditions: -- @@ -113,12 +87,12 @@ data ReAuthError -- fact that we're setting getting @mbHandle@ from table @"user"@, and when/if it was added -- there, it was claimed properly. newAccount :: - (MonadClient m, MonadReader Env m) => + (Member HashPassword r) => NewUser -> Maybe InvitationId -> Maybe TeamId -> Maybe Handle -> - m (User, Maybe Password) + AppT r (User, Maybe Password) newAccount u inv tid mbHandle = do defLoc <- defaultUserLocale <$> asks (.settings) domain <- viewFederationDomain @@ -128,7 +102,7 @@ newAccount u inv tid mbHandle = do (Just (toUUID -> uuid), _) -> pure uuid (_, Just uuid) -> pure uuid (Nothing, Nothing) -> liftIO nextRandom - passwd <- maybe (pure Nothing) (fmap Just . liftIO . mkSafePasswordScrypt) pass + passwd <- maybe (pure Nothing) (fmap Just . liftSem . hashPassword8) pass expiry <- case status of Ephemeral -> do -- Ephemeral users' expiry time is in expires_in (default sessionTokenTimeout) seconds @@ -179,69 +153,6 @@ newAccountInviteViaScim uid externalId tid locale name email = do ManagedByScim defSupportedProtocols --- | Mandatory password authentication. -authenticate :: - forall r. - (Member PasswordStore r, Member AuthenticationSubsystem r) => - UserId -> - PlainTextPassword6 -> - ExceptT AuthError (AppT r) () -authenticate u pw = - -- FUTUREWORK: Move this logic into auth subsystem. - lift (wrapHttp $ lookupAuth u) >>= \case - Nothing -> throwE AuthInvalidUser - Just (_, Deleted) -> throwE AuthInvalidUser - Just (_, Suspended) -> throwE AuthSuspended - Just (_, Ephemeral) -> throwE AuthEphemeral - Just (_, PendingInvitation) -> throwE AuthPendingInvitation - Just (Nothing, _) -> throwE AuthInvalidCredentials - Just (Just pw', Active) -> do - res <- lift $ liftSem (AuthenticationSubsystem.verifyPassword pw pw') - case res of - (False, _) -> throwE AuthInvalidCredentials - (True, PasswordStatusNeedsUpdate) -> do - -- FUTUREWORK(elland): 6char pwd allowed for now - -- throwE AuthStalePassword in the future - for_ (plainTextPassword8 . fromPlainTextPassword $ pw) (lift . hashAndUpdatePwd u) - (True, _) -> pure () - where - hashAndUpdatePwd :: UserId -> PlainTextPassword8 -> AppT r () - hashAndUpdatePwd uid pwd = do - hashed <- mkSafePassword pwd - liftSem $ upsertHashedPassword uid hashed - --- | Password reauthentication. If the account has a password, reauthentication --- is mandatory. If the account has no password, or is an SSO user, and no password is given, --- reauthentication is a no-op. -reauthenticate :: - (Member AuthenticationSubsystem r) => - UserId -> - Maybe PlainTextPassword6 -> - ExceptT ReAuthError (AppT r) () -reauthenticate u pw = - wrapClientE (lookupAuth u) >>= \case - Nothing -> throwE (ReAuthError AuthInvalidUser) - Just (_, Deleted) -> throwE (ReAuthError AuthInvalidUser) - Just (_, Suspended) -> throwE (ReAuthError AuthSuspended) - Just (_, PendingInvitation) -> throwE (ReAuthError AuthPendingInvitation) - Just (Nothing, _) -> for_ pw $ const (throwE $ ReAuthError AuthInvalidCredentials) - Just (Just pw', Active) -> maybeReAuth pw' - Just (Just pw', Ephemeral) -> maybeReAuth pw' - where - maybeReAuth pw' = case pw of - Nothing -> do - musr <- wrapClientE $ lookupUser NoPendingInvitations u - unless (maybe False isSamlUser musr) $ throwE ReAuthMissingPassword - Just p -> - unlessM (fst <$> lift (liftSem (AuthenticationSubsystem.verifyPassword p pw'))) do - throwE (ReAuthError AuthInvalidCredentials) - -isSamlUser :: User -> Bool -isSamlUser usr = do - case usr.userIdentity of - Just (SSOIdentity (UserSSOId _) _) -> True - _ -> False - insertAccount :: (MonadClient m) => User -> @@ -387,11 +298,6 @@ lookupUserTeam u = (runIdentity =<<) <$> retry x1 (query1 teamSelect (params LocalQuorum (Identity u))) -lookupAuth :: (MonadClient m) => UserId -> m (Maybe (Maybe Password, AccountStatus)) -lookupAuth u = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity u))) - where - f (pw, st) = (pw, fromMaybe Active st) - -- | Return users with given IDs. -- -- Skips nonexistent users. /Does not/ skip users who have been deleted. @@ -513,9 +419,6 @@ idSelect = "SELECT id FROM user WHERE id = ?" nameSelect :: PrepQuery R (Identity UserId) (Identity Name) nameSelect = "SELECT name FROM user WHERE id = ?" -authSelect :: PrepQuery R (Identity UserId) (Maybe Password, Maybe AccountStatus) -authSelect = "SELECT password, status FROM user WHERE id = ?" - richInfoSelectMulti :: PrepQuery R (Identity [UserId]) (UserId, Maybe RichInfoAssocList) richInfoSelectMulti = "SELECT user, json FROM rich_info WHERE user in ?" diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 7aeacd70efa..04da3707661 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -584,7 +584,9 @@ data Settings = Settings oAuthRefreshTokenExpirationTimeSecsInternal :: !(Maybe Word64), -- | The maximum number of active OAuth refresh tokens a user is allowed to have. -- use `oAuthMaxActiveRefreshTokens` as the getter function which always provides a default value - oAuthMaxActiveRefreshTokensInternal :: !(Maybe Word32) + oAuthMaxActiveRefreshTokensInternal :: !(Maybe Word32), + -- | Options to override the default Argon2id settings for specific operators. + passwordHashingOptions :: !(PasswordHashingOptions) } deriving (Show, Generic) diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 65070d3b420..5d2f9c8e313 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -115,7 +115,7 @@ import Wire.API.Routes.Public.Brig.Services (ServicesAPI) import Wire.API.Team.Feature qualified as Feature import Wire.API.Team.LegalHold (LegalholdProtectee (UnprotectedBot)) import Wire.API.Team.Permission -import Wire.API.User hiding (cpNewPassword, cpOldPassword) +import Wire.API.User import Wire.API.User qualified as Public (UserProfile, mkUserProfile) import Wire.API.User.Auth import Wire.API.User.Client @@ -127,6 +127,8 @@ import Wire.EmailSending (EmailSending) import Wire.Error import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess +import Wire.HashPassword (HashPassword) +import Wire.HashPassword qualified as HashPassword import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe)) import Wire.UserKeyStore (mkEmailKey) import Wire.UserSubsystem @@ -180,6 +182,7 @@ providerAPI :: ( Member GalleyAPIAccess r, Member AuthenticationSubsystem r, Member EmailSending r, + Member HashPassword r, Member VerificationCodeSubsystem r ) => ServerT ProviderAPI (Handler r) @@ -209,6 +212,7 @@ internalProviderAPI = Named @"get-provider-activation-code" getActivationCode newAccount :: ( Member GalleyAPIAccess r, Member EmailSending r, + Member HashPassword r, Member VerificationCodeSubsystem r ) => Public.NewProvider -> @@ -223,10 +227,12 @@ newAccount new = do let emailKey = mkEmailKey email wrapClientE (DB.lookupKey emailKey) >>= mapM_ (const $ throwStd emailExists) (safePass, newPass) <- case pass of - Just newPass -> (,Nothing) <$> mkSafePassword newPass + Just newPass -> do + hashed <- lift . liftSem $ HashPassword.hashPassword6 newPass + pure (hashed, Nothing) Nothing -> do newPass <- genPassword - safePass <- mkSafePassword newPass + safePass <- lift . liftSem $ HashPassword.hashPassword8 newPass pure (safePass, Just newPass) pid <- wrapClientE $ DB.insertAccount name safePass url descr let gen = mkVerificationCodeGen email @@ -239,8 +245,8 @@ newAccount new = do (Timeout (3600 * 24)) -- 24h (Just (toUUID pid)) let key = codeKey code - let val = codeValue code - lift $ sendActivationMail name email key val False + let value = codeValue code + lift $ sendActivationMail name email key value False pure $ Public.NewProviderResponse pid newPass activateAccountKey :: @@ -251,9 +257,9 @@ activateAccountKey :: Code.Key -> Code.Value -> (Handler r) (Maybe Public.ProviderActivationResponse) -activateAccountKey key val = do +activateAccountKey key value = do guardSecondFactorDisabled Nothing - c <- (lift . liftSem $ verifyCode key IdentityVerification val) >>= maybeInvalidCode + c <- (lift . liftSem $ verifyCode key IdentityVerification value) >>= maybeInvalidCode (pid, email) <- case (codeAccount c, Just (codeFor c)) of (Just p, Just e) -> pure (Id p, e) _ -> throwStd (errorToWai @'E.InvalidCode) @@ -317,20 +323,21 @@ beginPasswordReset (Public.PasswordReset target) = do completePasswordReset :: ( Member GalleyAPIAccess r, Member AuthenticationSubsystem r, - Member VerificationCodeSubsystem r + Member VerificationCodeSubsystem r, + Member HashPassword r ) => Public.CompletePasswordReset -> (Handler r) () -completePasswordReset (Public.CompletePasswordReset key val newpwd) = do +completePasswordReset (Public.CompletePasswordReset key value newpwd) = do guardSecondFactorDisabled Nothing - code <- (lift . liftSem $ verifyCode key VerificationCode.PasswordReset val) >>= maybeInvalidCode + code <- (lift . liftSem $ verifyCode key VerificationCode.PasswordReset value) >>= maybeInvalidCode case Id <$> code.codeAccount of Nothing -> throwStd (errorToWai @E.InvalidPasswordResetCode) Just pid -> do whenM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid newpwd)) do throwStd (errorToWai @E.ResetPasswordMustDiffer) - wrapClientE $ do - DB.updateAccountPassword pid newpwd + hashedPwd <- lift . liftSem $ HashPassword.hashPassword6 newpwd + wrapClientE $ DB.updateAccountPassword pid hashedPwd lift . liftSem $ deleteCode key VerificationCode.PasswordReset -------------------------------------------------------------------------------- @@ -377,7 +384,8 @@ updateAccountEmail pid (Public.EmailUpdate email) = do updateAccountPassword :: ( Member GalleyAPIAccess r, - Member AuthenticationSubsystem r + Member AuthenticationSubsystem r, + Member HashPassword r ) => ProviderId -> Public.PasswordChange -> @@ -388,7 +396,8 @@ updateAccountPassword pid upd = do throwStd (errorToWai @E.BadCredentials) whenM (fst <$> (lift . liftSem $ Authentication.verifyProviderPassword pid upd.newPassword)) do throwStd (errorToWai @E.ResetPasswordMustDiffer) - wrapClientE $ DB.updateAccountPassword pid (newPassword upd) + hashedPwd <- lift . liftSem $ HashPassword.hashPassword6 upd.newPassword + wrapClientE $ DB.updateAccountPassword pid hashedPwd addService :: (Member GalleyAPIAccess r) => diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index b5bb0243120..60a26fb0063 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -29,7 +29,7 @@ import Data.Set qualified as Set import Data.Text qualified as Text import Imports import UnliftIO (mapConcurrently) -import Wire.API.Password +import Wire.API.Password as Password import Wire.API.Provider import Wire.API.Provider.Service hiding (updateServiceTags) import Wire.API.Provider.Service.Tag @@ -115,14 +115,13 @@ deleteAccount pid = retry x5 $ write cql $ params LocalQuorum (Identity pid) updateAccountPassword :: (MonadClient m) => ProviderId -> - PlainTextPassword6 -> + Password -> m () updateAccountPassword pid pwd = do - p <- liftIO $ mkSafePassword pwd - retry x5 $ write cql $ params LocalQuorum (p, pid) + retry x5 $ write cql $ params LocalQuorum (pwd, pid) where cql :: PrepQuery W (Password, ProviderId) () - cql = {- `IF EXISTS`, but that requires benchmarking -} "UPDATE provider SET password = ? where id = ?" + cql = "UPDATE provider SET password = ? where id = ?" -------------------------------------------------------------------------------- -- Unique (Natural) Keys diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 597c8156554..9f4168727a5 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -40,7 +40,6 @@ import Brig.App import Brig.Budget import Brig.Data.Activation qualified as Data import Brig.Data.Client -import Brig.Data.User qualified as Data import Brig.Options qualified as Opt import Brig.Types.Intra import Brig.User.Auth.Cookie @@ -72,11 +71,11 @@ import Wire.API.User import Wire.API.User.Auth import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.Sso -import Wire.AuthenticationSubsystem (AuthenticationSubsystem) +import Wire.AuthenticationSubsystem +import Wire.AuthenticationSubsystem qualified as Authentication import Wire.Events (Events) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess qualified as GalleyAPIAccess -import Wire.PasswordStore (PasswordStore) import Wire.UserKeyStore import Wire.UserStore import Wire.UserSubsystem (UserSubsystem) @@ -90,7 +89,6 @@ login :: forall r. ( Member GalleyAPIAccess r, Member TinyLog r, - Member PasswordStore r, Member UserKeyStore r, Member UserStore r, Member VerificationCodeSubsystem r, @@ -105,13 +103,16 @@ login :: login (MkLogin li pw label code) typ = do uid <- resolveLoginId li lift . liftSem . Log.debug $ field "user" (toByteString uid) . field "action" (val "User.login") - wrapHttpClientE $ checkRetryLimit uid - Data.authenticate uid pw `catchE` \case - AuthInvalidUser -> wrapHttpClientE $ loginFailed uid - AuthInvalidCredentials -> wrapHttpClientE $ loginFailed uid - AuthSuspended -> throwE LoginSuspended - AuthEphemeral -> throwE LoginEphemeral - AuthPendingInvitation -> throwE LoginPendingActivation + wrapClientE $ checkRetryLimit uid + + (lift . liftSem $ Authentication.authenticateEither uid pw) >>= \case + Right a -> pure a + Left e -> case e of + AuthInvalidUser -> lift (decrRetryLimit uid) >> throwE LoginFailed + AuthInvalidCredentials -> lift (decrRetryLimit uid) >> throwE LoginFailed + AuthSuspended -> throwE LoginSuspended + AuthEphemeral -> throwE LoginEphemeral + AuthPendingInvitation -> throwE LoginPendingActivation verifyLoginCode code uid newAccess @ZAuth.User @ZAuth.Access uid Nothing typ label where @@ -120,9 +121,9 @@ login (MkLogin li pw label code) typ = do luid <- lift $ qualifyLocal uid verifyCode mbCode Login luid `catchE` \case - VerificationCodeNoPendingCode -> wrapHttpClientE $ loginFailedWith LoginCodeInvalid uid - VerificationCodeRequired -> wrapHttpClientE $ loginFailedWith LoginCodeRequired uid - VerificationCodeNoEmail -> wrapHttpClientE $ loginFailed uid + VerificationCodeNoPendingCode -> lift (decrRetryLimit uid) >> throwE LoginCodeInvalid + VerificationCodeRequired -> lift (decrRetryLimit uid) >> throwE LoginCodeRequired + VerificationCodeNoEmail -> lift (decrRetryLimit uid) >> throwE LoginFailed verifyCode :: forall r. @@ -137,7 +138,7 @@ verifyCode mbCode action luid = do mbFeatureEnabled <- liftSem $ GalleyAPIAccess.getVerificationCodeEnabled `traverse` mbTeamId pure $ fromMaybe ((def @(Feature Public.SndFactorPasswordChallengeConfig)).status == Public.FeatureStatusEnabled) mbFeatureEnabled account <- lift . liftSem $ User.getAccountNoFilter luid - let isSsoUser = maybe False Data.isSamlUser account + let isSsoUser = maybe False isSamlUser account when (featureEnabled && not isSsoUser) $ do case (mbCode, mbEmail) of (Just code, Just email) -> do @@ -158,23 +159,26 @@ verifyCode mbCode action luid = do userTeam =<< mbAccount ) -loginFailedWith :: (MonadClient m, MonadReader Env m) => LoginError -> UserId -> ExceptT LoginError m () -loginFailedWith e uid = decrRetryLimit uid >> throwE e +decrRetryLimit :: UserId -> (AppT r) () +decrRetryLimit = wrapClient . withRetryLimit (\k b -> withBudget k b $ pure ()) -loginFailed :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () -loginFailed = loginFailedWith LoginFailed - -decrRetryLimit :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () -decrRetryLimit = withRetryLimit (\k b -> withBudget k b $ pure ()) - -checkRetryLimit :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () -checkRetryLimit = withRetryLimit checkBudget +checkRetryLimit :: + ( MonadReader Env m, + MonadClient m + ) => + UserId -> + ExceptT LoginError m () +checkRetryLimit uid = + flip withRetryLimit uid $ \budgetKey budget -> + checkBudget budgetKey budget >>= \case + BudgetExhausted ttl -> throwE . LoginBlocked . RetryAfter . floor $ ttl + BudgetedValue () remaining -> pure $ BudgetedValue () remaining withRetryLimit :: (MonadReader Env m) => - (BudgetKey -> Budget -> ExceptT LoginError m (Budgeted ())) -> + (BudgetKey -> Budget -> m (Budgeted ())) -> UserId -> - ExceptT LoginError m () + m () withRetryLimit action uid = do mLimitFailedLogins <- asks (.settings.limitFailedLogins) forM_ mLimitFailedLogins $ \opts -> do @@ -183,10 +187,7 @@ withRetryLimit action uid = do Budget (timeoutDiff $ Opt.timeout opts) (fromIntegral $ Opt.retryLimit opts) - bresult <- action bkey budget - case bresult of - BudgetExhausted ttl -> throwE . LoginBlocked . RetryAfter . floor $ ttl - BudgetedValue () _ -> pure () + action bkey budget logout :: (ZAuth.TokenPair u a) => @@ -219,7 +220,6 @@ renewAccess uts at mcid = do revokeAccess :: ( Member TinyLog r, - Member PasswordStore r, Member UserSubsystem r, Member AuthenticationSubsystem r ) => @@ -232,8 +232,10 @@ revokeAccess luid@(tUnqualified -> u) pw cc ll = do lift . liftSem $ Log.debug $ field "user" (toByteString u) . field "action" (val "User.revokeAccess") isSaml <- lift . liftSem $ do account <- User.getAccountNoFilter luid - pure $ maybe False Data.isSamlUser account - unless isSaml $ Data.authenticate u pw + pure $ maybe False isSamlUser account + unless isSaml do + (lift . liftSem $ Authentication.authenticateEither u pw) + >>= either throwE pure lift $ wrapHttpClient $ revokeCookies u cc ll -------------------------------------------------------------------------------- @@ -389,17 +391,25 @@ ssoLogin :: CookieType -> ExceptT LoginError (AppT r) (Access ZAuth.User) ssoLogin (SsoLogin uid label) typ = do - (Data.reauthenticate uid Nothing) `catchE` \case - ReAuthMissingPassword -> pure () - ReAuthCodeVerificationRequired -> pure () - ReAuthCodeVerificationNoPendingCode -> pure () - ReAuthCodeVerificationNoEmail -> pure () - ReAuthError e -> case e of - AuthInvalidUser -> throwE LoginFailed - AuthInvalidCredentials -> pure () - AuthSuspended -> throwE LoginSuspended - AuthEphemeral -> throwE LoginEphemeral - AuthPendingInvitation -> throwE LoginPendingActivation + lift + (liftSem $ Authentication.reauthenticateEither uid Nothing) + >>= \case + Right a -> pure a + Left loginErr -> case loginErr of + -- Important: We throw on Missing Password here because this can only be thrown + -- for non-SSO users, so if we got this error, someone tried to authenticate + -- a regular user as if they were an SSO user, bypassing pwd requirements. + -- This would be a serious security issue if this weren't an internal endpoint. + ReAuthMissingPassword -> throwE LoginFailed + ReAuthCodeVerificationRequired -> pure () + ReAuthCodeVerificationNoPendingCode -> pure () + ReAuthCodeVerificationNoEmail -> pure () + ReAuthError e -> case e of + AuthInvalidUser -> throwE LoginFailed + AuthInvalidCredentials -> pure () + AuthSuspended -> throwE LoginSuspended + AuthEphemeral -> throwE LoginEphemeral + AuthPendingInvitation -> throwE LoginPendingActivation newAccess @ZAuth.User @ZAuth.Access uid Nothing typ label -- | Log in as a LegalHold service, getting LegalHoldUser/Access Tokens. @@ -414,7 +424,8 @@ legalHoldLogin :: CookieType -> ExceptT LegalHoldLoginError (AppT r) (Access ZAuth.LegalHoldUser) legalHoldLogin (LegalHoldLogin uid pw label) typ = do - (Data.reauthenticate uid pw) !>> LegalHoldReAuthError + (lift . liftSem $ Authentication.reauthenticateEither uid pw) + >>= either (throwE . LegalHoldReAuthError) (const $ pure ()) -- legalhold login is only possible if -- the user is a team user -- and the team has legalhold enabled diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index f37ad772048..f3b0ffeb4eb 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -62,7 +62,7 @@ import UnliftIO.Async hiding (wait) import Util import Util.Timeout import Wire.API.Conversation (Conversation (..)) -import Wire.API.Password (Password, mkSafePassword) +import Wire.API.Password as Password import Wire.API.User as Public import Wire.API.User.Auth as Auth import Wire.API.User.Auth.LegalHold @@ -101,10 +101,11 @@ tests conf m z db b g n = test m "testLoginFailure - failure" (testLoginFailure b), test m "throttle" (testThrottleLogins conf b), test m "testLimitRetries - limit-retry" (testLimitRetries conf b), - test m "login with 6 character password" (testLoginWith6CharPassword b db), + test m "login with 6 character password" (testLoginWith6CharPassword conf b db), testGroup "sso-login" [ test m "email" (testEmailSsoLogin b), + test m "login-non-sso-fails" (testEmailSsoLoginNonSsoUser b), test m "failure-suspended" (testSuspendedSsoLogin b), test m "failure-no-user" (testNoUserSsoLogin b) ], @@ -168,8 +169,8 @@ tests conf m z db b g n = ] ] -testLoginWith6CharPassword :: Brig -> DB.ClientState -> Http () -testLoginWith6CharPassword brig db = do +testLoginWith6CharPassword :: Opts.Opts -> Brig -> DB.ClientState -> Http () +testLoginWith6CharPassword opts brig db = do (uid, Just email) <- (userId &&& userEmail) <$> randomUser brig checkLogin email defPassword 200 let pw6 = plainTextPassword6Unsafe "123456" @@ -193,7 +194,7 @@ testLoginWith6CharPassword brig db = do updatePassword :: (MonadClient m) => UserId -> PlainTextPassword6 -> m () updatePassword u t = do - p <- liftIO $ mkSafePassword t + p <- mkSafePassword (argon2OptsFromHashingOpts opts.settings.passwordHashingOptions) t retry x5 $ write userPasswordUpdate (params LocalQuorum (p, u)) userPasswordUpdate :: PrepQuery W (Password, UserId) () @@ -553,7 +554,7 @@ testWrongPasswordLegalHoldLogin brig galley = do legalHoldLogin brig (LegalHoldLogin alice (plainTextPassword6 "wrong-password") Nothing) PersistentCookie !!! do const 403 === statusCode const (Just "invalid-credentials") === errorLabel - -- attempt a legalhold login with a no password + -- attempt a legalhold login without a password legalHoldLogin brig (LegalHoldLogin alice Nothing Nothing) PersistentCookie !!! do const 403 === statusCode const (Just "missing-auth") === errorLabel @@ -577,16 +578,35 @@ testLegalHoldLogout brig galley = do -- right password. testEmailSsoLogin :: Brig -> Http () testEmailSsoLogin brig = do - -- Create a user - uid <- Public.userId <$> randomUser brig + teamid <- snd <$> createUserWithTeam brig + let ssoid = UserSSOId mkSimpleSampleUref + -- creating user with sso_id, team_id + profile :: SelfProfile <- + responseJsonError + =<< postUser "dummy" True False (Just ssoid) (Just teamid) brig Http () +testEmailSsoLoginNonSsoUser brig = do + -- Create a user + uid <- Public.userId <$> randomUser brig + -- Login and do some checks + void $ + ssoLogin brig (SsoLogin uid Nothing) PersistentCookie + @@ -522,7 +522,7 @@ addCodeUnqualified :: Member (Input (Local ())) r, Member (Input UTCTime) r, Member (Input Opts) r, - Member (Embed IO) r, + Member HashPassword r, Member TeamFeatureStore r ) => Maybe CreateConversationCodeRequest -> @@ -545,11 +545,11 @@ addCode :: Member (ErrorS 'GuestLinksDisabled) r, Member (ErrorS 'CreateConversationCodeConflict) r, Member ExternalAccess r, + Member HashPassword r, Member NotificationSubsystem r, Member (Input UTCTime) r, Member (Input Opts) r, - Member TeamFeatureStore r, - Member (Embed IO) r + Member TeamFeatureStore r ) => Local UserId -> Maybe ZHostValue -> @@ -569,7 +569,7 @@ addCode lusr mbZHost mZcon lcnv mReq = do Nothing -> do ttl <- realToFrac . unGuestLinkTTLSeconds . fromMaybe defGuestLinkTTLSeconds . view (settings . guestLinkTTLSeconds) <$> input code <- E.generateCode (tUnqualified lcnv) ReusableCode (Timeout ttl) - mPw <- for (mReq >>= (.password)) mkSafePassword + mPw <- for (mReq >>= (.password)) HashPassword.hashPassword8 E.createCode code mPw now <- input let event = Event (tUntagged lcnv) Nothing (tUntagged lusr) now (EdConvCodeUpdate (mkConversationCodeInfo (isJust mPw) (codeKey code) (codeValue code) convUri)) diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index a9a02e660ea..4bfccae43cf 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -106,8 +106,10 @@ 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 import Wire.NotificationSubsystem.Interpreter (runNotificationSubsystemGundeck) import Wire.Rpc import Wire.Sem.Delay @@ -118,6 +120,7 @@ import Wire.Sem.Random.IO type GalleyEffects0 = '[ Input ClientState, Input Env, + HashPassword, Error InvalidInput, Error InternalError, -- federation errors can be thrown by almost every endpoint, so we avoid @@ -251,6 +254,7 @@ evalGalley e = . mapError toResponse . mapError toResponse . mapError toResponse + . runHashPassword (argon2OptsFromHashingOpts e._options._settings._passwordHashingOptions) . runInputConst e . runInputConst (e ^. cstate) . mapError toResponse -- DynError diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index a2b233b5e13..be813e6ee3d 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -18,7 +18,7 @@ -- with this program. If not, see . module Galley.Options - ( Settings, + ( Settings (..), httpPoolSize, maxTeamSize, maxFanoutSize, @@ -53,6 +53,7 @@ module Galley.Options logFormat, guestLinkTTLSeconds, defGuestLinkTTLSeconds, + passwordHashingOptions, GuestLinkTTLSeconds (..), ) where @@ -141,7 +142,8 @@ data Settings = Settings _disabledAPIVersions :: !(Set VersionExp), -- | The lifetime of a conversation guest link in seconds with the maximum of 1 year (31536000 seconds). -- If not set use the default `defGuestLinkTTLSeconds` - _guestLinkTTLSeconds :: !(Maybe GuestLinkTTLSeconds) + _guestLinkTTLSeconds :: !(Maybe GuestLinkTTLSeconds), + _passwordHashingOptions :: !(PasswordHashingOptions) } deriving (Show, Generic) From 56799ccb2d66b1844110cb62cc48ed2b9ee19629 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 22 Oct 2024 16:07:11 +0200 Subject: [PATCH 120/136] [WPB-11502] Remove transitive-anns (#4299) * remove all MakesFederationCall contraints * remove transitive-anns package everywhere * remove from nix haskell pins * remove fed calls tool * update docs * changelog * deleted obsolete files * removed obsolete type --- cabal.project | 1 - changelog.d/4-docs/WPB-11502 | 1 + changelog.d/5-internal/WPB-11502 | 1 + docs/src/developer/developer/FedCalls.png | Bin 769301 -> 0 bytes .../developer/federation-api-conventions.md | 4 - docs/src/understand/federation/fedcalls.md | 18 - .../federation/img/wire-fedcalls.csv | 122 ------ .../federation/img/wire-fedcalls.dot | 219 ---------- .../federation/img/wire-fedcalls.png | Bin 728020 -> 0 bytes libs/wire-api-federation/default.nix | 2 - .../src/Wire/API/Federation/API.hs | 14 +- .../src/Wire/API/Federation/API/Galley.hs | 42 +- .../src/Wire/API/Federation/Component.hs | 2 +- .../wire-api-federation.cabal | 2 - libs/wire-api/default.nix | 4 - libs/wire-api/src/Wire/API/Component.hs | 69 +++ .../src/Wire/API/MakesFederatedCall.hs | 403 ------------------ .../src/Wire/API/Routes/Internal/Brig.hs | 9 - .../src/Wire/API/Routes/Internal/Galley.hs | 11 - .../src/Wire/API/Routes/Public/Brig.hs | 46 -- .../src/Wire/API/Routes/Public/Cargohold.hs | 5 - .../src/Wire/API/Routes/Public/Galley/Bot.hs | 5 +- .../API/Routes/Public/Galley/Conversation.hs | 89 ---- .../API/Routes/Public/Galley/LegalHold.hs | 16 - .../src/Wire/API/Routes/Public/Galley/MLS.hs | 12 - .../API/Routes/Public/Galley/Messaging.hs | 6 - .../Routes/Public/Galley/TeamConversation.hs | 4 - .../Wire/API/Routes/SpecialiseToVersion.hs | 5 - libs/wire-api/wire-api.cabal | 4 +- libs/wire-subsystems/default.nix | 2 - libs/wire-subsystems/wire-subsystems.cabal | 3 +- nix/haskell-pins.nix | 8 - nix/local-haskell-packages.nix | 1 - nix/manual-overrides.nix | 4 +- services/brig/brig.cabal | 4 +- services/brig/default.nix | 2 - services/brig/src/Brig/API/Internal.hs | 9 +- services/brig/src/Brig/API/Public.hs | 79 ++-- services/cargohold/cargohold.cabal | 4 +- services/cargohold/default.nix | 2 - .../cargohold/src/CargoHold/API/Public.hs | 5 +- .../federator/src/Federator/Interpreter.hs | 2 +- .../test/unit/Test/Federator/Client.hs | 2 - services/galley/default.nix | 2 - services/galley/galley.cabal | 3 +- services/galley/src/Galley/API/Federation.hs | 16 +- services/galley/src/Galley/API/Internal.hs | 6 +- services/galley/src/Galley/API/Public/Bot.hs | 3 +- .../src/Galley/API/Public/Conversation.hs | 79 ++-- .../galley/src/Galley/API/Public/LegalHold.hs | 11 +- services/galley/src/Galley/API/Public/MLS.hs | 5 +- .../galley/src/Galley/API/Public/Messaging.hs | 5 +- .../src/Galley/API/Public/TeamConversation.hs | 3 +- .../test/integration/API/Federation/Util.hs | 4 - tools/fedcalls/.ormolu | 1 - tools/fedcalls/README.md | 38 -- tools/fedcalls/default.nix | 36 -- tools/fedcalls/example.png | Bin 110931 -> 0 bytes tools/fedcalls/fedcalls.cabal | 75 ---- tools/fedcalls/src/Main.hs | 195 --------- .../src/RabbitMQConsumer/Lib.hs | 2 +- 61 files changed, 197 insertions(+), 1530 deletions(-) create mode 100644 changelog.d/4-docs/WPB-11502 create mode 100644 changelog.d/5-internal/WPB-11502 delete mode 100644 docs/src/developer/developer/FedCalls.png delete mode 100644 docs/src/understand/federation/fedcalls.md delete mode 100644 docs/src/understand/federation/img/wire-fedcalls.csv delete mode 100644 docs/src/understand/federation/img/wire-fedcalls.dot delete mode 100644 docs/src/understand/federation/img/wire-fedcalls.png create mode 100644 libs/wire-api/src/Wire/API/Component.hs delete mode 100644 libs/wire-api/src/Wire/API/MakesFederatedCall.hs delete mode 120000 tools/fedcalls/.ormolu delete mode 100644 tools/fedcalls/README.md delete mode 100644 tools/fedcalls/default.nix delete mode 100644 tools/fedcalls/example.png delete mode 100644 tools/fedcalls/fedcalls.cabal delete mode 100644 tools/fedcalls/src/Main.hs diff --git a/cabal.project b/cabal.project index ca8e1277db5..2daabf40f47 100644 --- a/cabal.project +++ b/cabal.project @@ -52,7 +52,6 @@ packages: , tools/db/team-info/ , tools/db/repair-brig-clients-table/ , tools/db/service-backfill/ - , tools/fedcalls/ , tools/rabbitmq-consumer , tools/rex/ , tools/stern/ diff --git a/changelog.d/4-docs/WPB-11502 b/changelog.d/4-docs/WPB-11502 new file mode 100644 index 00000000000..30382d30c3e --- /dev/null +++ b/changelog.d/4-docs/WPB-11502 @@ -0,0 +1 @@ +Call graph of federated endpoints was removed from the docs diff --git a/changelog.d/5-internal/WPB-11502 b/changelog.d/5-internal/WPB-11502 new file mode 100644 index 00000000000..73be54702fe --- /dev/null +++ b/changelog.d/5-internal/WPB-11502 @@ -0,0 +1 @@ +TransitiveAnns compiler plugin was removed diff --git a/docs/src/developer/developer/FedCalls.png b/docs/src/developer/developer/FedCalls.png deleted file mode 100644 index 50070640b771eae5f99123716d9927d77a29bf6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 769301 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%0AJbJ$? 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 z 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

  • tPYtP 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∨Rnc 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%0ArkkXSFY3V_mG-*Q7#|umc*}{K`er00zN$_%F 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

    ;Reqr*P4M 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 z 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

    PwE) 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 Wd35|DrV}d^CPq^XqsPwK{Qm%xh+Gc< diff --git a/docs/src/developer/developer/federation-api-conventions.md b/docs/src/developer/developer/federation-api-conventions.md index f27e148ff73..612a4eb67ed 100644 --- a/docs/src/developer/developer/federation-api-conventions.md +++ b/docs/src/developer/developer/federation-api-conventions.md @@ -31,7 +31,3 @@ this request has authority on, like a conversation got created, or a message is sent, then use the second format like `on-conversation-created` or `on-message-sent` - - A call graph of the API endpoints the can call to federation members is included below. - - ![Federation call graph](FedCalls.png) \ No newline at end of file diff --git a/docs/src/understand/federation/fedcalls.md b/docs/src/understand/federation/fedcalls.md deleted file mode 100644 index 80fdb3c03e3..00000000000 --- a/docs/src/understand/federation/fedcalls.md +++ /dev/null @@ -1,18 +0,0 @@ -# Federated API calls by client API end-point (generated) - -**Updated manually using using [the fedcalls tool](https://github.com/wireapp/wire-server/blob/8760b4978ccb039b229d458b7a08136a05e12ff9/tools/fedcalls/README.md); last change: 2023-01-16.** - -This is most likely only interesting for backend developers. - -This graph and csv file describe which public (client) API end-points trigger calls to which end-points at backends federating with the one that is called. The data is correct by construction (see [the fedcalls tool](https://github.com/wireapp/wire-server/blob/8760b4978ccb039b229d458b7a08136a05e12ff9/tools/fedcalls/README.md) for more details). - -The target can only be understood in the context of the [backend code base](https://github.com/wireapp/wire-server/). It is described by component (sub-directory in `/services`) and end-point name (use grep to find it). - -links: - -- [dot](img/wire-fedcalls.dot) -- [png](img/wire-fedcalls.png) -- [csv](img/wire-fedcalls.csv) - -```{image} img/wire-fedcalls.png -``` diff --git a/docs/src/understand/federation/img/wire-fedcalls.csv b/docs/src/understand/federation/img/wire-fedcalls.csv deleted file mode 100644 index bfc571a6d85..00000000000 --- a/docs/src/understand/federation/img/wire-fedcalls.csv +++ /dev/null @@ -1,122 +0,0 @@ -source method,source path,target component,target name -get,/users/{uid_domain}/{uid},brig,get-users-by-ids -post,/list-users,brig,get-users-by-ids -put,/self,brig,on-user-deleted-connections -delete,/self,brig,on-user-deleted-connections -delete,/self/phone,brig,on-user-deleted-connections -delete,/self/email,brig,on-user-deleted-connections -put,/self/locale,brig,on-user-deleted-connections -put,/self/handle,brig,on-user-deleted-connections -post,/register,brig,on-user-deleted-connections -post,/delete,brig,on-user-deleted-connections -get,/activate,brig,on-user-deleted-connections -post,/activate,brig,on-user-deleted-connections -get,/users/{uid_domain}/{uid}/clients,brig,get-user-clients -get,/users/{uid_domain}/{uid}/clients/{client},brig,get-user-clients -post,/users/list-clients,brig,get-user-clients -get,/users/{uid_domain}/{uid}/prekeys/{client},brig,claim-prekey -get,/users/{uid_domain}/{uid}/prekeys,brig,claim-prekey-bundle -post,/users/list-prekeys,brig,claim-multi-prekey-bundle -post,/clients,brig,on-user-deleted-connections -put,/connections/{uid_domain}/{uid},brig,send-connection-action -post,/connections/{uid_domain}/{uid},brig,send-connection-action -get,/search/contacts,brig,get-users-by-ids -get,/search/contacts,brig,search-users -post,/mls/key-packages/claim/{user_domain}/{user},brig,claim-key-packages -post,/access,brig,on-user-deleted-connections -post,/login,brig,on-user-deleted-connections -get,/assets/{key_domain}/{key},cargohold,get-asset -get,/assets/{key_domain}/{key},cargohold,stream-asset -put,/conversations/{cnv},galley,on-conversation-updated -put,/conversations/{cnv},galley,on-mls-message-sent -put,/conversations/{cnv},galley,on-new-remote-conversation -get,/conversations/{cnv_domain}/{cnv},galley,get-conversations -get,/conversations/{cnv_domain}/{cnv}/groupinfo,galley,query-group-info -post,/conversations/list,galley,get-conversations -post,/conversations/join,galley,on-conversation-updated -post,/conversations/join,galley,on-new-remote-conversation -post,/conversations,galley,on-conversation-created -post,/conversations/one2one,galley,on-conversation-created -post,/conversations/{cnv_domain}/{cnv}/members,galley,on-conversation-updated -post,/conversations/{cnv_domain}/{cnv}/members,galley,on-mls-message-sent -post,/conversations/{cnv_domain}/{cnv}/members,galley,on-new-remote-conversation -post,/conversations/{cnv}/join,galley,on-conversation-updated -post,/conversations/{cnv}/join,galley,on-new-remote-conversation -post,/conversations/{cnv_domain}/{cnv}/typing,galley,on-typing-indicator-updated -put,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-conversation-updated -put,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-mls-message-sent -put,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-new-remote-conversation -delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,leave-conversation -delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-conversation-updated -delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-mls-message-sent -delete,/conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr},galley,on-new-remote-conversation -put,/conversations/{cnv}/members/{usr},galley,on-conversation-updated -put,/conversations/{cnv}/members/{usr},galley,on-mls-message-sent -put,/conversations/{cnv}/members/{usr},galley,on-new-remote-conversation -put,/conversations/{cnv}/name,galley,on-conversation-updated -put,/conversations/{cnv}/name,galley,on-mls-message-sent -put,/conversations/{cnv}/name,galley,on-new-remote-conversation -put,/conversations/{cnv_domain}/{cnv}/name,galley,on-conversation-updated -put,/conversations/{cnv_domain}/{cnv}/name,galley,on-mls-message-sent -put,/conversations/{cnv_domain}/{cnv}/name,galley,on-new-remote-conversation -put,/conversations/{cnv}/message-timer,galley,on-conversation-updated -put,/conversations/{cnv}/message-timer,galley,on-mls-message-sent -put,/conversations/{cnv}/message-timer,galley,on-new-remote-conversation -put,/conversations/{cnv_domain}/{cnv}/message-timer,galley,on-conversation-updated -put,/conversations/{cnv_domain}/{cnv}/message-timer,galley,on-mls-message-sent -put,/conversations/{cnv_domain}/{cnv}/message-timer,galley,on-new-remote-conversation -put,/conversations/{cnv}/receipt-mode,galley,on-conversation-updated -put,/conversations/{cnv}/receipt-mode,galley,on-mls-message-sent -put,/conversations/{cnv}/receipt-mode,galley,on-new-remote-conversation -put,/conversations/{cnv}/receipt-mode,galley,update-conversation -put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,on-conversation-updated -put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,on-mls-message-sent -put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,on-new-remote-conversation -put,/conversations/{cnv_domain}/{cnv}/receipt-mode,galley,update-conversation -put,/conversations/{cnv_domain}/{cnv}/access,galley,on-conversation-updated -put,/conversations/{cnv_domain}/{cnv}/access,galley,on-mls-message-sent -put,/conversations/{cnv_domain}/{cnv}/access,galley,on-new-remote-conversation -delete,/teams/{tid}/conversations/{cid},galley,on-conversation-updated -delete,/teams/{tid}/conversations/{cid},galley,on-mls-message-sent -delete,/teams/{tid}/conversations/{cid},galley,on-new-remote-conversation -post,/conversations/{cnv}/otr/messages,galley,on-message-sent -post,/conversations/{cnv}/otr/messages,brig,get-user-clients -post,/conversations/{cnv_domain}/{cnv}/proteus/messages,brig,get-user-clients -post,/conversations/{cnv_domain}/{cnv}/proteus/messages,galley,on-message-sent -post,/conversations/{cnv_domain}/{cnv}/proteus/messages,galley,send-message -post,/bot/messages,galley,on-message-sent -post,/bot/messages,brig,get-user-clients -put,/teams/{tid}/features/legalhold,galley,on-conversation-updated -put,/teams/{tid}/features/legalhold,galley,on-mls-message-sent -put,/teams/{tid}/features/legalhold,galley,on-new-remote-conversation -post,/mls/welcome,galley,mls-welcome -post,/mls/messages,galley,on-mls-message-sent -post,/mls/messages,galley,send-mls-message -post,/mls/messages,galley,on-conversation-updated -post,/mls/messages,galley,on-new-remote-conversation -post,/mls/messages,brig,get-mls-clients -post,/mls/commit-bundles,galley,on-mls-message-sent -post,/mls/commit-bundles,galley,mls-welcome -post,/mls/commit-bundles,galley,send-mls-commit-bundle -post,/mls/commit-bundles,galley,on-conversation-updated -post,/mls/commit-bundles,galley,on-new-remote-conversation -post,/mls/commit-bundles,brig,get-mls-clients -delete,/teams/{tid}/legalhold/settings,galley,on-conversation-updated -delete,/teams/{tid}/legalhold/settings,galley,on-mls-message-sent -delete,/teams/{tid}/legalhold/settings,galley,on-new-remote-conversation -post,/teams/{tid}/legalhold/{uid},galley,on-conversation-updated -post,/teams/{tid}/legalhold/{uid},galley,on-mls-message-sent -post,/teams/{tid}/legalhold/{uid},galley,on-new-remote-conversation -delete,/teams/{tid}/legalhold/{uid},galley,on-conversation-updated -delete,/teams/{tid}/legalhold/{uid},galley,on-mls-message-sent -delete,/teams/{tid}/legalhold/{uid},galley,on-new-remote-conversation -post,/teams/{tid}/legalhold/consent,galley,on-conversation-updated -post,/teams/{tid}/legalhold/consent,galley,on-mls-message-sent -post,/teams/{tid}/legalhold/consent,galley,on-new-remote-conversation -put,/teams/{tid}/legalhold/{uid}/approve,galley,on-conversation-updated -put,/teams/{tid}/legalhold/{uid}/approve,galley,on-mls-message-sent -put,/teams/{tid}/legalhold/{uid}/approve,galley,on-new-remote-conversation -post,/i/users,brig,on-user-deleted-connections -post,/i/users/spar,brig,on-user-deleted-connections -post,/i/legalhold-login,brig,on-user-deleted-connections -post,/i/sso-login,brig,on-user-deleted-connections \ No newline at end of file diff --git a/docs/src/understand/federation/img/wire-fedcalls.dot b/docs/src/understand/federation/img/wire-fedcalls.dot deleted file mode 100644 index 77648a9d950..00000000000 --- a/docs/src/understand/federation/img/wire-fedcalls.dot +++ /dev/null @@ -1,219 +0,0 @@ -strict digraph { - graph [rankdir=LR] - node [shape=rectangle] - edge [style=dashed] - subgraph { - "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w - "3: delete /self":w - "5: delete /self/email":w - "4: delete /self/phone":w - "46: delete /teams/{tid}/conversations/{cid}":w - "54: delete /teams/{tid}/legalhold/settings":w - "56: delete /teams/{tid}/legalhold/{uid}":w - "10: get /activate":w - "25: get /assets/{key_domain}/{key}":w - "27: get /conversations/{cnv_domain}/{cnv}":w - "28: get /conversations/{cnv_domain}/{cnv}/groupinfo":w - "21: get /search/contacts":w - "0: get /users/{uid_domain}/{uid}":w - "12: get /users/{uid_domain}/{uid}/clients":w - "13: get /users/{uid_domain}/{uid}/clients/{client}":w - "16: get /users/{uid_domain}/{uid}/prekeys":w - "15: get /users/{uid_domain}/{uid}/prekeys/{client}":w - "23: post /access":w - "11: post /activate":w - "49: post /bot/messages":w - "18: post /clients":w - "20: post /connections/{uid_domain}/{uid}":w - "31: post /conversations":w - "30: post /conversations/join":w - "29: post /conversations/list":w - "32: post /conversations/one2one":w - "33: post /conversations/{cnv_domain}/{cnv}/members":w - "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w - "35: post /conversations/{cnv_domain}/{cnv}/typing":w - "34: post /conversations/{cnv}/join":w - "47: post /conversations/{cnv}/otr/messages":w - "9: post /delete":w - "61: post /i/legalhold-login":w - "62: post /i/sso-login":w - "59: post /i/users":w - "60: post /i/users/spar":w - "1: post /list-users":w - "24: post /login":w - "53: post /mls/commit-bundles":w - "22: post /mls/key-packages/claim/{user_domain}/{user}":w - "52: post /mls/messages":w - "51: post /mls/welcome":w - "8: post /register":w - "57: post /teams/{tid}/legalhold/consent":w - "55: post /teams/{tid}/legalhold/{uid}":w - "14: post /users/list-clients":w - "17: post /users/list-prekeys":w - "19: put /connections/{uid_domain}/{uid}":w - "45: put /conversations/{cnv_domain}/{cnv}/access":w - "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w - "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w - "40: put /conversations/{cnv_domain}/{cnv}/name":w - "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w - "26: put /conversations/{cnv}":w - "38: put /conversations/{cnv}/members/{usr}":w - "41: put /conversations/{cnv}/message-timer":w - "39: put /conversations/{cnv}/name":w - "43: put /conversations/{cnv}/receipt-mode":w - "2: put /self":w - "7: put /self/handle":w - "6: put /self/locale":w - "50: put /teams/{tid}/features/legalhold":w - "58: put /teams/{tid}/legalhold/{uid}/approve":w - } - subgraph { - "71: [brig]:claim-key-packages":e - "68: [brig]:claim-multi-prekey-bundle":e - "66: [brig]:claim-prekey":e - "67: [brig]:claim-prekey-bundle":e - "87: [brig]:get-mls-clients":e - "65: [brig]:get-user-clients":e - "63: [brig]:get-users-by-ids":e - "64: [brig]:on-user-deleted-connections":e - "70: [brig]:search-users":e - "69: [brig]:send-connection-action":e - "72: [cargohold]:get-asset":e - "73: [cargohold]:stream-asset":e - "77: [galley]:get-conversations":e - "81: [galley]:leave-conversation":e - "85: [galley]:mls-welcome":e - "79: [galley]:on-conversation-created":e - "74: [galley]:on-conversation-updated":e - "83: [galley]:on-message-sent":e - "75: [galley]:on-mls-message-sent":e - "76: [galley]:on-new-remote-conversation":e - "80: [galley]:on-typing-indicator-updated":e - "78: [galley]:query-group-info":e - "84: [galley]:send-message":e - "88: [galley]:send-mls-commit-bundle":e - "86: [galley]:send-mls-message":e - "82: [galley]:update-conversation":e - } - "0: get /users/{uid_domain}/{uid}":w -> "63: [brig]:get-users-by-ids":e - "1: post /list-users":w -> "63: [brig]:get-users-by-ids":e - "2: put /self":w -> "64: [brig]:on-user-deleted-connections":e - "3: delete /self":w -> "64: [brig]:on-user-deleted-connections":e - "4: delete /self/phone":w -> "64: [brig]:on-user-deleted-connections":e - "5: delete /self/email":w -> "64: [brig]:on-user-deleted-connections":e - "6: put /self/locale":w -> "64: [brig]:on-user-deleted-connections":e - "7: put /self/handle":w -> "64: [brig]:on-user-deleted-connections":e - "8: post /register":w -> "64: [brig]:on-user-deleted-connections":e - "9: post /delete":w -> "64: [brig]:on-user-deleted-connections":e - "10: get /activate":w -> "64: [brig]:on-user-deleted-connections":e - "11: post /activate":w -> "64: [brig]:on-user-deleted-connections":e - "12: get /users/{uid_domain}/{uid}/clients":w -> "65: [brig]:get-user-clients":e - "13: get /users/{uid_domain}/{uid}/clients/{client}":w -> "65: [brig]:get-user-clients":e - "14: post /users/list-clients":w -> "65: [brig]:get-user-clients":e - "15: get /users/{uid_domain}/{uid}/prekeys/{client}":w -> "66: [brig]:claim-prekey":e - "16: get /users/{uid_domain}/{uid}/prekeys":w -> "67: [brig]:claim-prekey-bundle":e - "17: post /users/list-prekeys":w -> "68: [brig]:claim-multi-prekey-bundle":e - "18: post /clients":w -> "64: [brig]:on-user-deleted-connections":e - "19: put /connections/{uid_domain}/{uid}":w -> "69: [brig]:send-connection-action":e - "20: post /connections/{uid_domain}/{uid}":w -> "69: [brig]:send-connection-action":e - "21: get /search/contacts":w -> "63: [brig]:get-users-by-ids":e - "21: get /search/contacts":w -> "70: [brig]:search-users":e - "22: post /mls/key-packages/claim/{user_domain}/{user}":w -> "71: [brig]:claim-key-packages":e - "23: post /access":w -> "64: [brig]:on-user-deleted-connections":e - "24: post /login":w -> "64: [brig]:on-user-deleted-connections":e - "25: get /assets/{key_domain}/{key}":w -> "72: [cargohold]:get-asset":e - "25: get /assets/{key_domain}/{key}":w -> "73: [cargohold]:stream-asset":e - "26: put /conversations/{cnv}":w -> "74: [galley]:on-conversation-updated":e - "26: put /conversations/{cnv}":w -> "75: [galley]:on-mls-message-sent":e - "26: put /conversations/{cnv}":w -> "76: [galley]:on-new-remote-conversation":e - "27: get /conversations/{cnv_domain}/{cnv}":w -> "77: [galley]:get-conversations":e - "28: get /conversations/{cnv_domain}/{cnv}/groupinfo":w -> "78: [galley]:query-group-info":e - "29: post /conversations/list":w -> "77: [galley]:get-conversations":e - "30: post /conversations/join":w -> "74: [galley]:on-conversation-updated":e - "30: post /conversations/join":w -> "76: [galley]:on-new-remote-conversation":e - "31: post /conversations":w -> "79: [galley]:on-conversation-created":e - "32: post /conversations/one2one":w -> "79: [galley]:on-conversation-created":e - "33: post /conversations/{cnv_domain}/{cnv}/members":w -> "74: [galley]:on-conversation-updated":e - "33: post /conversations/{cnv_domain}/{cnv}/members":w -> "75: [galley]:on-mls-message-sent":e - "33: post /conversations/{cnv_domain}/{cnv}/members":w -> "76: [galley]:on-new-remote-conversation":e - "34: post /conversations/{cnv}/join":w -> "74: [galley]:on-conversation-updated":e - "34: post /conversations/{cnv}/join":w -> "76: [galley]:on-new-remote-conversation":e - "35: post /conversations/{cnv_domain}/{cnv}/typing":w -> "80: [galley]:on-typing-indicator-updated":e - "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "74: [galley]:on-conversation-updated":e - "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "75: [galley]:on-mls-message-sent":e - "36: put /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "76: [galley]:on-new-remote-conversation":e - "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "81: [galley]:leave-conversation":e - "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "74: [galley]:on-conversation-updated":e - "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "75: [galley]:on-mls-message-sent":e - "37: delete /conversations/{cnv_domain}/{cnv}/members/{usr_domain}/{usr}":w -> "76: [galley]:on-new-remote-conversation":e - "38: put /conversations/{cnv}/members/{usr}":w -> "74: [galley]:on-conversation-updated":e - "38: put /conversations/{cnv}/members/{usr}":w -> "75: [galley]:on-mls-message-sent":e - "38: put /conversations/{cnv}/members/{usr}":w -> "76: [galley]:on-new-remote-conversation":e - "39: put /conversations/{cnv}/name":w -> "74: [galley]:on-conversation-updated":e - "39: put /conversations/{cnv}/name":w -> "75: [galley]:on-mls-message-sent":e - "39: put /conversations/{cnv}/name":w -> "76: [galley]:on-new-remote-conversation":e - "40: put /conversations/{cnv_domain}/{cnv}/name":w -> "74: [galley]:on-conversation-updated":e - "40: put /conversations/{cnv_domain}/{cnv}/name":w -> "75: [galley]:on-mls-message-sent":e - "40: put /conversations/{cnv_domain}/{cnv}/name":w -> "76: [galley]:on-new-remote-conversation":e - "41: put /conversations/{cnv}/message-timer":w -> "74: [galley]:on-conversation-updated":e - "41: put /conversations/{cnv}/message-timer":w -> "75: [galley]:on-mls-message-sent":e - "41: put /conversations/{cnv}/message-timer":w -> "76: [galley]:on-new-remote-conversation":e - "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w -> "74: [galley]:on-conversation-updated":e - "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w -> "75: [galley]:on-mls-message-sent":e - "42: put /conversations/{cnv_domain}/{cnv}/message-timer":w -> "76: [galley]:on-new-remote-conversation":e - "43: put /conversations/{cnv}/receipt-mode":w -> "74: [galley]:on-conversation-updated":e - "43: put /conversations/{cnv}/receipt-mode":w -> "75: [galley]:on-mls-message-sent":e - "43: put /conversations/{cnv}/receipt-mode":w -> "76: [galley]:on-new-remote-conversation":e - "43: put /conversations/{cnv}/receipt-mode":w -> "82: [galley]:update-conversation":e - "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "74: [galley]:on-conversation-updated":e - "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "75: [galley]:on-mls-message-sent":e - "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "76: [galley]:on-new-remote-conversation":e - "44: put /conversations/{cnv_domain}/{cnv}/receipt-mode":w -> "82: [galley]:update-conversation":e - "45: put /conversations/{cnv_domain}/{cnv}/access":w -> "74: [galley]:on-conversation-updated":e - "45: put /conversations/{cnv_domain}/{cnv}/access":w -> "75: [galley]:on-mls-message-sent":e - "45: put /conversations/{cnv_domain}/{cnv}/access":w -> "76: [galley]:on-new-remote-conversation":e - "46: delete /teams/{tid}/conversations/{cid}":w -> "74: [galley]:on-conversation-updated":e - "46: delete /teams/{tid}/conversations/{cid}":w -> "75: [galley]:on-mls-message-sent":e - "46: delete /teams/{tid}/conversations/{cid}":w -> "76: [galley]:on-new-remote-conversation":e - "47: post /conversations/{cnv}/otr/messages":w -> "83: [galley]:on-message-sent":e - "47: post /conversations/{cnv}/otr/messages":w -> "65: [brig]:get-user-clients":e - "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w -> "65: [brig]:get-user-clients":e - "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w -> "83: [galley]:on-message-sent":e - "48: post /conversations/{cnv_domain}/{cnv}/proteus/messages":w -> "84: [galley]:send-message":e - "49: post /bot/messages":w -> "83: [galley]:on-message-sent":e - "49: post /bot/messages":w -> "65: [brig]:get-user-clients":e - "50: put /teams/{tid}/features/legalhold":w -> "74: [galley]:on-conversation-updated":e - "50: put /teams/{tid}/features/legalhold":w -> "75: [galley]:on-mls-message-sent":e - "50: put /teams/{tid}/features/legalhold":w -> "76: [galley]:on-new-remote-conversation":e - "51: post /mls/welcome":w -> "85: [galley]:mls-welcome":e - "52: post /mls/messages":w -> "75: [galley]:on-mls-message-sent":e - "52: post /mls/messages":w -> "86: [galley]:send-mls-message":e - "52: post /mls/messages":w -> "74: [galley]:on-conversation-updated":e - "52: post /mls/messages":w -> "76: [galley]:on-new-remote-conversation":e - "52: post /mls/messages":w -> "87: [brig]:get-mls-clients":e - "53: post /mls/commit-bundles":w -> "75: [galley]:on-mls-message-sent":e - "53: post /mls/commit-bundles":w -> "85: [galley]:mls-welcome":e - "53: post /mls/commit-bundles":w -> "88: [galley]:send-mls-commit-bundle":e - "53: post /mls/commit-bundles":w -> "74: [galley]:on-conversation-updated":e - "53: post /mls/commit-bundles":w -> "76: [galley]:on-new-remote-conversation":e - "53: post /mls/commit-bundles":w -> "87: [brig]:get-mls-clients":e - "54: delete /teams/{tid}/legalhold/settings":w -> "74: [galley]:on-conversation-updated":e - "54: delete /teams/{tid}/legalhold/settings":w -> "75: [galley]:on-mls-message-sent":e - "54: delete /teams/{tid}/legalhold/settings":w -> "76: [galley]:on-new-remote-conversation":e - "55: post /teams/{tid}/legalhold/{uid}":w -> "74: [galley]:on-conversation-updated":e - "55: post /teams/{tid}/legalhold/{uid}":w -> "75: [galley]:on-mls-message-sent":e - "55: post /teams/{tid}/legalhold/{uid}":w -> "76: [galley]:on-new-remote-conversation":e - "56: delete /teams/{tid}/legalhold/{uid}":w -> "74: [galley]:on-conversation-updated":e - "56: delete /teams/{tid}/legalhold/{uid}":w -> "75: [galley]:on-mls-message-sent":e - "56: delete /teams/{tid}/legalhold/{uid}":w -> "76: [galley]:on-new-remote-conversation":e - "57: post /teams/{tid}/legalhold/consent":w -> "74: [galley]:on-conversation-updated":e - "57: post /teams/{tid}/legalhold/consent":w -> "75: [galley]:on-mls-message-sent":e - "57: post /teams/{tid}/legalhold/consent":w -> "76: [galley]:on-new-remote-conversation":e - "58: put /teams/{tid}/legalhold/{uid}/approve":w -> "74: [galley]:on-conversation-updated":e - "58: put /teams/{tid}/legalhold/{uid}/approve":w -> "75: [galley]:on-mls-message-sent":e - "58: put /teams/{tid}/legalhold/{uid}/approve":w -> "76: [galley]:on-new-remote-conversation":e - "59: post /i/users":w -> "64: [brig]:on-user-deleted-connections":e - "60: post /i/users/spar":w -> "64: [brig]:on-user-deleted-connections":e - "61: post /i/legalhold-login":w -> "64: [brig]:on-user-deleted-connections":e - "62: post /i/sso-login":w -> "64: [brig]:on-user-deleted-connections":e -} \ No newline at end of file diff --git a/docs/src/understand/federation/img/wire-fedcalls.png b/docs/src/understand/federation/img/wire-fedcalls.png deleted file mode 100644 index 35fb0ed9d2a6b2b09c69bcea79b3d6d6b527d72c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 728020 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

    w^>cA@ z8gzu&1dCuuSs-e+yq}rropiIf+v4^aiPPld^f%`LUh+0Cn%aEo^Jw_3Jwf;kh0lD0 zc(Y|fy(&Z1F&a7*-^MchS@ol?uJUc%m9uBh#>7l&+3SGhY??!vcxyXK0m}H_)N-sT zy_tp^25a7SHF@{$a5v-lA3p^HK=NR2v15Z~aWMjf(ro(LXfK1_cV9;>r2+Wb=_y}$}7zTjR@3?{p zu!;47^S~{@)I4j-DRG(c)`yW&O)V#;3Kx3tZs^m}= z@!EE{DsPL51itB5HUpW`hPhwzBeXRTOq4zCJz-I2L+c9VS2Ez!(DxX0R}uoX*}Ipa)wm z##oLux@MWw2pyw$*L}Putzmauz_4W}xpPXIrsPg#z;nRC@5Sx#a8QA^OUlUqk>O~Eat=L!8W6ql6r z-iHtC&!0a*PVVlG(r1+0#F{OSF0=N4OWMCa?58*_hA_ePK&A@}%IeBc7P23L#!Qu! zow-?Rx6OK`4MPphTm6O~^Yty^?&f>AR?)NZ#Ib3Nt+E(>ov~aSxS)`uk5IMA)}hok z2#hSBFk6u7P-8Y21lmcmN#F8Xw6~ue8}<{nL$3o5zV6X;pDGbEb&AzTm2*sTa&casV6zFo>kLjTePX@NgbTfsk2h#wa!CM$ zu!CV*|7>g=p@ybp@p){&w-%3#h$R!sIq=im^J32yZIQ0!CNxIeOiF9m30=4iJGumm z-Q^}v=05tsJ4G@H9iRm(9jkd=ozAZ{9MsXQ6(s|Pk}wte#clip_m1e)HcMI2U;os< z|Ke70pFqS&-m7%@dfIu-?dhNoWCLv`qPV$8BG)th_E@!M5@TlLw$y0IVOlp5piLg7_s2a%(RsY*CZ;f?yZ~S zJz(ya}LVQjm;|s z-}r1h042p9y4PeH0!Ezf+pQktS$HweS6hnj-r%<4urpPe5p3^oS}!1jqwzYRVCOAX zfj|Xv%%X0i%H?CH`-=VgAC!(c=xXc~(mRyig2tBtAD{`U&c+KF)%^l=3~=~1MFcb* z;06^XrCPGGbl={$|K~THAv0ZVctHUL``;UE@r6DBN=vmInPV%V_h z$d3=`tu3zg6zU+pT%8OI4B#IsGnIzqBJIr$-Mp@y?_>1^Bi)4zF7X1R+qw3UH68qI? z$L{V`vvy5U2vTew=C|&~(0*?_+t~6hYImDcnbCiM5rywBe<67o%AD%~175S7RAzC^ zi+xBip)NXiA?t~Y? zF^YVF3uB<4j%XDJQ%MFU46`n z_9rDFPt>-@w0F_z)VZtJ+WwukKkgHAbYD{VDEaReRZFhyd3Vhz>aDzDoJG~^D^8oL zmTW7?kBPZ=wDlc7kpLwa|SEeyY9ig}2JVekC&pEuKDmra_OM{Y2UwNcxDyIjmjBCB_Z{unN?}snurw zwc)sd8qz8U6V3>qJ6uCwUw_%H4GX!2;dUXlV4PeLiw(XRv8~%$rm@!1mGO~MgGw)< zG34jLs#xLYjHQvgHACB`fWr<-m(1kClBn!qohoQ*(SHC<(;L6;v~dTOOV?>+U?ze& z{V@YH`it(Rpp54&3Fd_u)eYpA?tjbfPy0j5?Q)(i)fKi7-;>CsxPywFBR-*MHC?jg z!B*;-SVVV^KQG+6tJ0N1UUG2&#!F1<4Pk~7ZDO>idY8J?Dw2QG0B0QtQ};=}dWZN1 z&-~U)v?j5l+3Vf8nq|MiJLxM6BqpW{&Jn9*s7B2@HEw%4n#GMUO6g)dbeuNWKCoIs z8kzZ#7i)QTxZurmF*0%r+4MPcJmGJWC-3;oPyRwdYS+S&3|F;D+scA8Q_(m|k(3F- zR|2HnA1Gh6ksfTN5vOGThngJ1Pr^4LcGuN?yo041E>Xve1RIh)wfB1zJJVtSy>%!8 zNGf37fZZ-SK>Yp5R)lI;lFZq?&|B!(0 zm>|TxT!8jvcq&5r5J7nbcS^gtVsy#)yJS|{*xD48(4ga~*a^-BlD)dg&MvQ?O?Kfb z6aP@FDWc}=Kel2qWTlr555IV4Bh@W?aiM(4; zL&%!aNuE7rn?rNnBsgp=ky`ft_OQ`&kA2V@OwZ<{PZ;g7MT`2zzr*rUeGf4=xxBAy zzv7;0e#`3l-3Q1>|G7$o<7qk#mvAMC9E5@DV+QSQkg<2G>e8A;58JTt1u60=Aom@jS2>5=5q)s)t%$$$9^$I7liB<9bqd02-P$@L*d>bL!oH z@9(?PrJLtXK&scDn?MyCd9;yTK_iJ=NKU%%g~vT z02QbGPq~iZE|xBsQaygWy@;(}6l*!-^Ji>G=mJ04D#MjX(pcD|m!seC!GrTJ%pYj? zhyH3CoY=(Fjqz1ioN&(x^!LA?EO}&hz3eM;28Mz@_POLY2sFdVR~&5W(XL;=nj1Dd zFV7fr@K%dxO}=mm<2NmMik+UnFFa`y?vSh9tt+Zz01n=iYX?~*GF33yZ^3jrIeZX; z7o^t0Z@@ZbAxds&dO-ln6j#4@s`hBDTJ6!^8m||zX3=0MmVbHvwV{h_=~vb`wQ<-< zqR$kSaO4=@z_fSqp~pi*?bg<{hwWKsZ*Syr3_RK<_i}&rHuN-da&i*hj}47%VTqI6 z*Zkbw@HJrB#EXFwPCgA96ub|&p2-%sGZ0E^Ylk*HQm@ec*2`ErTk9XKlw84Pb3a3e zJ(z(v_x#hBFE4Fgkhn`QB6oda*!VTCiff=gZsr}P3h_6G&%Ex?;lmfKzG>BKJeM-n zXl?#EM0(%YUSd}QM|tUwIlxS)lGcu`*gj2C1o*r=g*%H-rr+G_CQ7)j`505En)7DyW-jOe_ySlEmRX! zOD4}*-W+FfF&TaeG(y3oN~?r=v>$2kBfA@r8yy`fH-R0ds|yJ^s1{v55=E?AwJ(qo zQvZL_LRawHA;wH0z+mivr!zD=mlJXzr6tTBm9V3<*RulU2Ygjrl*ReHjFX&DP_Dt`@RSYv; zy{ZGCyB;5Z?dprJf`bnIR&o#);@$19#k&{#e#R zxgc#RySFHb8W*^61umh1`IR}Xuw}pklAkP~wBGpLKVovX%=OAqCOhdLmaJ_Trif!* zcGqN$TQwm-{m6!IA71BUSN#Sqg%J_pOIVmpFBAl zfMQ7$kHkbivc{6KWaieMSnO`%?f}=phY3~^`RfWw?EU-py~VQW*u_ptlj~O=LD>T5 z1imgT)x-zfWm&tvMNn_^SKwW9k`dA9!gXhx<+4D9stI+(co(uU5M`ve#`fe$3C)P{0-5yvoLR?DYHM;?@R zTk};YZNBDCkc{mf?%Yix6z}3h2c_50+PxNG3_fqehB62Pl<4kle|{5_;EUYYY_6xz zw#qK-G&Fok6l~}PV)%3~2P#l{GG@qJfki1wn;}1G664PM_3azG^imdcGT_s&dQE_` z#5%DMDbM9guH=E_u(*RY`Y-h&&3ce+8+SJk`o(j>m4y3*SD_wz&7n0MEM`1&-3?b9 zG=w^W2TR=WTJ+@hhwNdxB7rbPW9O&gHeUKCH%$|4T4oV+KA@l*DRi%(zd_f74m;9O z@nW|)1zppnQ6|vxHodtn+rjrJ`$U5FeAYy#HQKv9pINV6djsx6fh zlQ#7V=}n2SN^%a19?Nd{%NIdfK;C{ldw!?fk(9?!hNV?D9LqiuB#|2{G8M>FuC(ph zSBf34yA!h;drDBDpQk@Y134BjAOpvQbkwbri3dAkp6@@eY~#Rk*d|o{12sku-1l}+ zxB;J$(7dOQl&S=IsYrCM?Ppl~?Jb4M^xb$c(3AUXNP9|sL4yP&0NZX>POcfJV5DgN z?3pGs!qsd~VRGQ>J6L1F(!EjZMA3`bAn;O%OA|ZZ+(EjmV*Pe>k?>lnINr-9?rlbf zexfz*kwWuWl^?Pn>lw`v8^7>Ou285A1hrA>dO*R`n7h%*B6elM5-wiC?84)_4SS^o z@d|u)YQ)M9A4Yb(f|taJGiPAi%5y|&b%*KuXp*uvh1Y^TB_jXCU04pFj zEeQ*~AeD`a0>)a|ZhJQm{45HG%vbn?$@=9v!yBnH4&va;gSjuOtz$m_^_1#{Y_=E*6(P7Z@`-MvQ-=rRxnVVDuGu8+o^A_c4w zG8*k5YDWQf<$`~nBp+Z4G|HJH4`F{%s$KM(8f40hUoi3gtIUIQ3__-DKSw5z>~~K9 z8CBf7IlR8NBwz4L)$p9IM^QiV9Z_JC)tytgcd1c6vGXiwIwd5~qSYj|JG&bO7H}G> zi^@QXZ)-)yd3R6T)ubrqcGf{ImOp}~ zA!t74IMk#R>We0C`jF%fBv7pFy($;PuVM}yq&hHT`Y9J5f)ZGiTz$6k)L!4HarGyX zXDm_-^j-!fd7&7@Gs8@VncVgMq$RetrW>gubqVC;kkwb$40-A6=;XAJ+~KbGXsiZl3JpzJbDsl8fD1AO zQ*)`xe-X6xX+{P}6Uw}emmsP=_8q3-C?|`S;O^^!ET8(OCZK5RRZ^=8=p#v~+I;x_ zoyi=V4(JJ-UXo7+6A|W?j{nWy1*Rd%$+&UpuU{*3be_j#k7v#hbAr;hNQ-ipaU+pM z4b<@VQ2LSN-Ii5fTRRk}9Gnv{92_6%jxZrGFwea`-SAz&~r)D)uoqwXOEh1WOPX`dhz+W!0y(QZ-L;}m}$MCdcA)C!zPoeHFPfNi7aoi++&%YFtrtP^cH6orA=zYrq|Gws|{S8e_5({`mR7 zx2$g+4Rl3jWjNYBK(L@Hb_JFO8=`-i%S2_8@ap(y)e?PjA-p)hx{~uh>!`Nli@sh4 zC{eH{88Zjk%GNw&q0)xM>5hhZae)&Zo)BeyC6pb?49AU>mj^t(a^b?Gv~`)q5u_Ag zt7~fxxP1Rq71B9!>Ld*fi}SCxnXlcQI*o1-ZV(k0Hr{o1KNN?S z?)sCn3}aoe`of}XTU;%f0M8sr_dy}&7>%u?r;ic#jNgDZA@3*sw_@F^->1IM$|4_K zMCv_IBT(3mo4K9qT}CI>0fBiP2QilVB~Y)AOYFdqe-B%l^!4u=m6a^`nwcLYT>t}*)!X0%glRp(_=5e7^y+L9-&;)& zX1LVB4ype^A3mW=AJ21g;-TFy+alTKc3EI#g-Um~bS0Rn9F3r9qFMk&pvU^794&kL-t?)jT}#M&W#ln*BX#26}@@G;7}?xW4Sk-C|A8CC^Ga8(_z%v z^M_2dbjNR+QWK6!fh+bZ%B*6FAnVjK9TObB`A-Qv0M zV&KGr9RwY6@Fh_b<>Wpn#=IV4z5a=b&#tG+F2!8%)c%$8Itc`QPD81NU%36`ia#Gs zFL=k_zHMk=aIRmxXK(6~g;*ysDmFNfo(C+Nx`qY@jF0ik9(|1(>_ykhCegIk&?q)9 z8bj1;o^FM09BE$t?A_I_Y`cr`n@f^^vl(1-;JVZ{HriorpK#)u@Ubysg6^3 z|9<_;iK}$w`BxQ&uaXghk`QfHJ=4pso5-`y?CP>t+<K_g_U6DSMk9#K#!9)Dyi4*dnnlI2Q8TMFc;(znd zpe|`GV{o%j4xTt|+M6J^gCI`9ZU-sdXZFBFjZ3IF$;BwjWW)OPSC`$`CvlTxV#6Rx zhm%j&v*Ad{FG3}!P`JB^B#6NcL>|Ahp&wf;6|BGAhC4S ztwtvX1rKZ$fR@6Nk`$s2y*kGQk(rSU;c#VH)-FVn}v|vS*2~)QSG{```zsNOro>RV%|$mW(6fl z6$ON7K2@y1*PcRV1rK*~?q&ZDX(>h)bm`WW`1tvO2kZpsuILfygxHc2v%0W9%nM{l z+~wq}7>!LWQLQMCxN+&-3c-{r2aly_2M;0DRoMJZ6)Xbr4+7aEvj<2>NI*2JsWG~? znHE|4Rqs6#u~6cdO>}l}sAAZQ%2J&5Z|`+DV6C`ZhWEe5_gTX`C*Bwql}w}Fy7k-V z&$re+?&ZAr3_>{iWc+(`R0P!kCK)IMuPi-q|`)md6Bi0P4d;*^2rT-BXI( zS|%Pv33Y-LvkY6CjT;M~h6>HL>Oz4LdG^GjZ_nO!wHj<4ZgtKyRaojL$P-lcGWa&} zEi`f5NZ83fdIbM|(k(fkfp9Lxg1iNxr(pzP`od(1@=b7#+f1nI8rqTc|w z4*$AtlIuCCcH4JvUCy>;s+MeUiXTSt~V?Ih{lj;_2c7aV6+76&;VF41hj8{oJ;0 zo>G1P{@f7)+i-Gr(o9&(699}voge;f;SS6L34|2UD})Fz6 zFaIJjbk1Vh8pxLEkJ8@z+iw(`pr+;oS6W~OQp5r>i_#qb`+utAK?i@tNa{yXwloZ6#Bf8haGRkGkPjD7?%q2vvFg_5MRO7 zM7qI%A^d!JbR7L<^z%&2xsZK?1t7QN-$kZdVNL5@!oXmIaHSh51{M_-_6+G3)PpYD zw{Md=AkI5{Y#c6b&r} z`JlkNg3(m=5wU6_DpzHHjozZA8?CHJ9fwBTQnLKJa`G6y`~gsDgGNBnMaADMq_4=c z@S!Hyyhs#K$*;ix!XC2^+7|*G#`e&yI3^NBclrA<0uW`_!@)%_OmT+h^(aa;Z*NA< z1PSe3HIe66(O;Wp`I%$~tTR4v5W5_TPxTcUSN?rKCVd$;Ze=xE_{@0&|8A619OWtY zGkTeeJezv_-64yYEM3YY#B|NGH1iWb>uuS%V!cd0&E=iG|=aNd%TBBKmV5lNlB(fd#_cQHer&BjX6X!q#^vXMnjL&y$ z7tb?uV$1;X`C7kmBYzRNsDJIU>F7!FX+_z+o02+v4$I(Ry@V@{OGIORD4Wj3!QpfI zdkB*ZMia!dAY(y|d=QkEJ3b41w}As6Ze_%(T+J*g;6ge#rp~?REZ&i@8-E_Imf~L& zG+{Gb7LCV{34KSOVai^^Qy~g~TWl**P*^oy=Ai#?NT1|AdKnx)rlmgh*{Cfu?!?8K zpT|gde0_l&@;9Nm@Cyr*Q5S|*2>p^fl)K+(Mtr#QNi(+ePPX6At!2wf+Tem)kMBX6=MJ|~#83Jc?Q;=!M%R|50 z&(-`)hKEE-QDNUbVdbL5lC5*5F!QLyg0UBcJOrzF>?>P-{o3XN8W|+X%BP*jSo!+( zrwtByU4ATC7j&I3cjzG2kd{|Os|~AjVS)!%})xKaq=ZXc}ciGX?|Y5)Yv%O>`JgR z$w|pC=2me?UVCV6AswW%Orp(=^+Qb4@Y{^o$ z?rOnVPKyqvRGmvMSlm;VDwUe5s152S4X^J~zq2`iswtAD57mLE6nB9etY6a4jQ-1d2boAlu#J44MD#}g^-HB?TV#|`n>t0jN6 z6LlU6=;$WqD7tL>fZf}O{{3YcYp}zO;trC5UId>X%%+sUnNSC8E(ilGmr%w*w*<;q zuppBHv8giW(C~J!3*6wF&-GEMUWx>nsW(01glHr(?HQ-wIUkm5I?V!zkuY5mm+^&N zILvgQKrr_Oc7MoyxH-z8PY_Z_qv;}&X4%`nr(763$->0FO;kqAL4tR@eO317?qABw zRSIM`)n_!)FL`a%%Q2aS(RzD&?X=Kg$6wkM-W*%=!J%x=B5A$HPaZwWmI?ReGPpHw zZqSxCjk@O_zCL@G3Zs`mQ0;8hRua@{A~#f*IEA#9fRiVGvg$+H^T^!bNNsz_B@K>> z=%kGLMEJilGf+pu3nWi4I#c(z#^aioot&_DXtS@ilW#-txky53z3V1vZs5q83D(&U z4d`Q-Bsp)j>oAf#eL8(B5iIu7rSb8?OtI%mCXJI0)2BiH(n(zyY+JlycTd^>I@@ogvLetM9$Bu33=-^Umf#Yd7Z0_*Eb3GK%y=&L(hsK3gHa%rQSncVWtEYfUQymsJ zatPu!X4>*tX%75^fmL%+5y^9M)%XUS0+DXC^?FlA%4qz+5mq%l;o_GfA9o}wg|$?j*5 zWIfu@LC{cYqo7vmQjIYX6-TQBREP^pCaAa34rf6%e1OgBJ;&K;icAJS&+A&d3LZl{ z96vV7%i#02XyB2G)Q+GeWtAV#C_nBNvGLahj}7D8r>x9`X$n%2cp&x#b^{+jeR|-t z(bw|L5V@n?-e7~);rX9Re&7i#TaweM*lNa3hkEX%nvzz~o8m@P2dh`FwrdfrRbzh< z)Y;gc@ATfwO=E^($=e3vOtsa_bOA5*`$od~B6d#j^gRtB*fiYv0mWskB;HCU?3FQ-t9R7zh)3+&Z`V8M&2r8s`Psb#eLRy(w-1O=)K& zZZd%3yVIN_vmfmGut;n=q^BAuozb5Q3tC-2Q>Dk$B!-Qw6WY{SPMgl8v@BjY$wMP_ z*Sn+vP4*Z%ok5}%NPgM`}6sA>K9CG5S5MNA{)@cNh4QSaU$5plcJOYknW|3fqg zJ$33ro~@JfOkG`3%{VxZsVmFxK$p%MzPc?*UEv2vPlU*Ed-=-<{utC0Pg*3tKX4>ub6&0Pv z37l^Gb~Rk?7~GiXaA`%wrgjWq)Jt;4g7ijSyHaJ?jrk;9N-T(L`5NU*`aB^onWUx` zFg-HkR1yWs$B@u(g5dY%OB8B`fd&O7%~{sO`3zu9?c4|*-D9_}O(&f^WrX((WgU74 z()myT3R>uF+<@OJ2;mXNsN^D-Ad{U{@^+l2CNaL;ao(uFOf{)K@A9ZVeVR1ZQm_+B z9&h#Y@xjtwD_)o@MT$1=>U_`2x0N_%e7rZunQqtfN{_~)_3-jh=n+ zn)1p_)0Xk3q@_XHkxM2F_IBIc0MwxTQprkZ{({vpZ8W?1_I<-l9{+hZWcBX*Gwf{W zAEJ|?!T&s=OJbw%ks}|q+BOl*-8Ih(cAm*kR0O@pf(7UO=0okm+=3wn0HvuEge4JL z5N9o7PHQha@pL`GWt#C6Y0~>+7!wJ#3!w{}wVeF?sd93N{9ZnJa)~BAHTefEdkwUL zG%#MPh;H?`#Sj$7uenEt3x)~6XE{R#N%JCCuU6dhqogjo7wcN~eAv!o!`vdcul7&BT}JQMGIX|fH0ILzhwPfAzR1mJxYnV;)ti=#QDN=S|mrq^$> z@mL~n-b5Z!@8FucH_#~_t(&5?`Ur0CDJ!hPm%z19GQfilZ8TaR@ckuG&$+ZOWSwsIB^sEXD9@ce!A|;{7VDr}x-IB0!m9epIzYsE-e`+SZhfF|L zWEHCdM4CNE;KjOKQ<*h~my&x(X-R8)q1fUpd6ILEv>lPyw_w;$2S7j4|_`eU5$8Gi3K^z1sib0*L9FE=%2Wk8UVs z*X38)KlS4Fu8P}1-XV9E6G(5Ff;n9NpK054DYWq<)` zIulo?tc)=el>S0F&Q~7f0IdA9W!Hm*m_uh^+*nbm1!(64)Rh)q?-2jx(3NU+crDrjGH@=0vYL<7;2#>gilGQJsR*P5L3e6J zy^ZSy*6|-EI@M9mu+*}gazAOHu4e8hG{;U$t|x9lwI{FkLIO4|i;q!7|MK#`)> ziZS=fuu%5?P*yS18!x;%~=dJ_a|HC1e zh{4B&c_@zy{b6bWAUJYR3?jjiQqVfO6##~G+VBww(Caw&dG=(Ar4^gbuJ__IsRSd4 zNRT(T++O-E>dHyF7e$B?=% zk({&mRAgl7jB6Hh!YA+nSYxtz$XvX1(4PZHHE$6cO3JGn8H|Gus5&%v(r-L&`r%7& zVUY+i*!em7g+%9XJ2^slGMR*m)0^92YKOlZi>aEF`MBAnuIyA(&S2*oBGHDmEJCgh z?)iikx^g~!iQ`Kz8WNq}(uBNpX(MNds!a+Y9~hJhcFPaRIizt=4!1}dcLT;W0 zT|(>nYjyQReS;00AKlV7d?095g_G!YW99~Q;J&$yTb`nIi*M;_xF?hoansE$UQF#l zhu;=w1ca=MK65e!!1m+EnW?Z8nmIMF67L^-D$HtD*JN>C?z7Tw9;XWo$bCjA#sA zbkn7vU{Ng#Ngu)f`tsn>LMS+i2XX)Yb)aUZ&t4O1T=X&sK?>HJG(gdjf8hYhBhzrM zb_IJRx6L^zV;G_l;EU$r3{)iED%T5rZql|9GBQS~b6zSfUbf8jb9$fV_-DzI7^)nB zqw^|$Jq;uBKu`#rEO5ibbNd&unR40i`d-t4da6<{sL9_#W2cM8VI(&pN%9}2`ZzR0 z7TgD2AF`2peEqlR=|i94G8ft*@a6eVXzIIQy=cVyV;?5qzn~3yc~0z9&PMmCm!4ZI z*Ff8#E%hHcq7B|X=(1iCA(f|K zM2N}q8w27>NF=#IUx(~4HkMEsa%%60tz>=4w+0W(NgaM(sy_Fakm~h@5IX%o>PU=1 z;rfRwOzKvt@d&=3$5{z3s9@%+rRCG^^h>6fs7+z~_$PFB;p^_{K4g-nX7SUfQOVv+ zj-taa2nkX|w1D8amc(xPI9rwSZnz3;p2#{>QL*j z{lIxoRXPV^0J2oF59KM~nxs=I8ypxWNBa?*iON87QWy;ab&ybJWVHJFnohD`sGH}; zHdCQ^9T#S!lG;$g@hk3JzaBpR-kR+5ZoBF$w$sJuxCI3brBo+XV%N|0kpdOR@j{vA z_uq-uUdM%Gztx+55V05n$$c_LvZ@)56>HWwh)+Jbr{MpQJd*qL^zurCpvZ)?4BT|S zDeD|njt_LXanR-XT9mIGVwG_NL?5)=;zP}^Z3}ZqQ)(|fyu}6Wy{`0EGZ(tecG&0C7hT&C<8E$fg%oj5l4+VYu0>L{}~AEMgAg( zZjh1T{niCKl)t&;ar5S+-!F}xQQmcxr<~tl#xM`0y6;_b86_u@#|UY?8F7oAE{07$ zeZMkf=VukAe2F*h@lu=@)dM-IQoS90qBI$l$65X1z9z$Ot}nenffk$IGI#y@%L^1a z6o$DsBhdfxlsHxL^AK)K>p!(%-6@j)xu|{R_x>vtTPkS3``pIK0E~H9uX($6-A(dh zl&|)tkR~hB3CJo6{*!Y_S@6u zzQrh#v`it#K#?lh*Hz(kc;a-6nKSos+A)1;ZQ!<$?pZM#f{R9GmB5f}e(&O^ugIfb z{cGZm?()N^V)j@%mkYDZ>bt&SqJfk)z4yKd^FQjZGl^bDz;&0Zh<{BOsd&^?9-&jy z;to-@_RZ?>Dp3j}FOu-eSOL1gxnCjgo;bB)RoTGi-OS|~` z;-`XC8xVcB-X|8J;D^9c=_V=Zuw%!M?5p{tKF3zsuUz>FXo@0?6{vrI{!TMG#cENv zZr{%SP@kBnK@?HGI6-IU1r^-+v_*E_CtmTeLX&ouH1^pMDzW}*W=@Xpy~QrwU3)*> zdR4N6x=1Y0OmD#gO-zDzpHuXul94YY{QY=Jk}IuEJH%vEROr@Y>u)2Q@sm1(G|DEk zYIye4+Q=paF} zkKbbrw58-#inqqPjcKT7=jrR~4_Ac53aOrKpk?!p&Iu=DuU{u&03n2(l($4+o9;Nj zU{%y>LJi;!#G!x3;W)OU4$c|q52EQ<>hY!(b+pbGih`E*kbZl+ocXS#FulbuX^t5> z`B{~6-90~s+ltl%JY3!V)56@UxocAEYsO_gXgg`Jz7nSOkUx~hX0!<8_xHztzAPvx z&|Ah^2w2eVIz09D@LtRZZfz_)b6 zzOa3NCYydLJJR&4a-m2#gfh_N$4pxF1^AS@d(u1kErfrXB-s`!Kj#ZP4OxqScp9eU z0iVmu)Be1|F*$gX4e0`H)u2s9bXQ|1M$st1z2hLGZ{RV~JS~1Ze^bTG4m4C}X&-u( zQ3WGr?#VZ*op-hcbng#p+K2Q)aJN4L3oCo5!<~z_CjN~d(rg~;9mN?lTHy`_;BEIS znw~%8?;3czsVkDm2>>A?s79bfLW&>{70St#bo{`(pJr$Ol2bgR8tOXb%(7pZiI7Q3 zx_ysj+zZ6+8y%Exyf|oTUcEAow$;-vZt~w`)^s=QFu*B0jpU`SW&BtF+t(Coy9VIzC$`s-yIYdRz#wR?ZlELOBho5xuW6^ z@TbfDJA!6dcg2beNof-k_3Jf~30bKc$so5APL16i(pmF=ITZyL`;Mlcewzzi4oD53 z=3DiiJ$vq4`lxt`KBHjg2!g|L`^F7{+^QbRqre}yOUI66HfTWmpWk#o_tN%!#S{Vg1C&HAr64RW()n5X6D`t3Qtl=SU`xn!FM3?>C+DYR8-UCCAo{;;A= z3JrWHix~d0e^2@{U?UpdF9ISSRYd~!Bcoe+b>;NGGhz{xP+VmUK5+lOD|C&|OE&+{ z?2&q=Owxg6wC1QIZyTSGh%9a$n4^M={-?Bw*YqnA;PoJZ@~KZg?>2NqoJ4#}CEh2~3m-V>J9HuejQ zv{bpojnTeaUi}tQLq0AYp0^W=+}pK2QPA+igU69wK&YQYWo6$diHQRqwJplrtLFhg z@)^=bEk=I-D^DYRsI!vkUnWPSL5QhP42ea0M*hfem)g_HRP5&2e@n*jN(#PwdDyN% zqdLRCKlPSQKT3sHlQt`v`m_kVl#-$Zs2WN4RWN?$m(_lys#=)IzTHU&7`imo=T8z* zh$-4(c(of!&_X*A|8+spfXQCZ&;N00V_fu~&|Ouc^Q(5}Lx`m*yC%qi0h|~8tQ6~9 zaJNm_&3~$qzO;Q-mvV}?eZ|C!HmwH#5&D-U!B_t`zHxMrwAh3WF#a&%EgmBlxgWT$ zzP_&jUX=h&)$QfkAI9lGt&5oFf3gn3)kx`ufu8Bh#I({51oVrP7I3kDP!KMZOrh;Q z{x4tsfBoQI=@m0t#43h#m4Ob2b=F#2zjV2M=g!gyBir1+jmg_BR1(cpuO#Rp`ocJmfVX z%OOxM4||i65=ll85D<{pKzIjy+MZjM8Idfz>Qn2k+TnaaZg@L^cNCT63lh##}pwme#j`e`5ElB%7Ss2bS;Cl!&HeGjO8^}N* z`6+uS-%%xhxwX(&rNncg=I2|jw3xwRs;j|7^y||H?~1^TmEYrKDRg;Xq2~GVCxv=! z{rX}RwQ19Eke1QR9@6({{IYfrb}Iyr)GHn+))Ah*PpgOnkz=^YYFd-V^x2Dk+$`Y_ zK%kpWyaV*q79l@_x2*r0wSuJg!SSLjv!6W!lGYud=wsO&-)8laXG9f!QMMJI9j~&b z+c@lKWK7oF16l~AU3g2t_IYmayzdLOM#Hm~ZcVqme>XzcMO`bX;-GHlH9-7;nrV}e z^jZ+|kd1`8hF%rDnSr?gn5UQQ)ylibyaTJVPgBbY9!1Ue^4f=z%5?9H>-Yd`63FVq znp!oTS_80hr7FZ?IpVlACK0;&`h~Zk+T_qY&5g0MVF`dT_Y~}S-yo53fxTa4Ds$8)*$=G?nhU=>x=FDW zqObgEeK*P_u7#3`9hjDQS$#=xYvoNgezeKWN$sRMTA79~#z+uyR8Wt8=PQuCp@2RA zYiTQ_Cy?h5*NuZW6-P}MPzER)->>>c6l>y0#eLa*t$VjoxW>|`t9Qzw2&{^;@>+1o z-~S^@Havji1tvQe80@-Ht+TdZ%R=e>^7st00MdmIW2x&&Cg4q!qg|Tv=k<(Ma!5sa zghY6Pov`G@6POxP0+x4sGvZj?yUAQ7A9pG0HkzlgmrURumb&k@*`E}4Db-FwE$(iT zT^B%~qfAh~ogQZT)4$tP^w}=W$rm;Ry41WF0V4MGso-}8CQR3JQcaW9Z}Jhr5HMF@ z_tD+kp6iVdGV(EpZZ4me<;M%dkQuXlQuAQY^5Xd|O*NZdc})nrSu*K4K@E=m*eL;; zQwotC_C4BFp1a|mehd!0OYzfMMP3hR@Wyz!U4&tfA)y9uExWJy$kev6+_4soH5c z?NbmlrZSnHcvHT;)&_z+X8`?(-XZU-Ahi<)oGz|4E`OJP#(Z1v+32u64jzP7zgXy? z;daxvFJf`O-Rga{`e6F3aQe`m)v{uVTaf?=B&6+Mbf~}uvmXD*gE_{!LiE5-nAA&b zZ=>K5GR|S&VMe0|pdRE8GkXgouZ|wYXhym+Sv_)!MqyinQ0##Q+BC@l%0dTDdxU~m zW{k03)H`wizZvKGi4jI-lftaE%grdbZG~shuiuEj4KO+#o#U+EEsMA1aoK=`9aunO z#pgk4=40!${k(zM7!R_Wc+3LH_V52IxNF@u^lkiCSXxX0in~RK$E?16?Hc5a)22nIJH7upzZq&S&X=xY-V5#d38&XHXGAFhKnkz0qVbY;* zENIAnyuNNOGbLcExw;Ax9ju;3XY8E7%!F|;rdP%NMO6hK)mfT?!apZAB072t$3iE) z=rfK2DYEQ>)QQ5rEjOc1CpdZf)A=>EsZ8yYy3??MPsJ)go4Va842c!!}B`_88vy| zPs%Z-$Gq}h)6wxs;RQ7Y{W@tsfAIpF_dDAjZhMd20|5RW+)V-T!SqRBu~;ILui>75 zT;s{~)c&j4K8;0~%||uu=ZHT(744% z&Z$DPR8ymzw|E(fR>4P6^IZ4J2Rcd3fW4BE6|nYsGp}5kYY~gR$B{7~ety0i6@in& zzv)#f77!mPoh@k53huqo?O=9-8q#qnBAjBaXnYah;92o=M(MOkztdC1ld~IVs2vIB z6#jMYVB|x_$d|1HFhaH5YuMXXjSo{glQ?R3kz5m9*(JSI zd4se>IYGlhLvJzC#$~-|weLYmxk$VIz85ZRhG&inMD0PQq@~z<{nD4hFqp7>;n*Z< zbHXlpINT3YB1NH7_0voGAms_#&w2BLMjo-5XJ9bgop%3zImYqM5;@U2G~Up3g5C-E z2uF?54u#P9ZAx*-1STz!b>$H429lx^x#~vwL>#Hq5wu?lvg4)mmsFl>yJ|gGb3A#I zJT8mrn*)>3?6sF+At`TuKY`9(Q^>NxZ?)n!Enf9UXtIpe65$*Jp!gl7qv7GG2?pWj z`3W$1s2(z^%2z(crF_h|acIZTwgfNdsGxbfcCCW?n28e?4eJ%h`dDnz^oNIySx!Ma z?a+DopKz-0GwhcVkW5o}wWh3&0)|;g^ zo$9Q0@mKXX1j?69qDn4edqE_*PeG`+>S$N(@}^IeAAe@lSE$Dvu5}e_kk%G;JvBi` zOJ~C&3v&zWPC7dK_a1S}GQM6Dtzcg{Ju=H|#0j-+Tg$BEL~-3sXE$U;Pt{t~d*%eu zFnO(|jV;yAyYE|9^g7`qcJbHXq%%bUc`g-BkDM;%eXK|25eCH!jDL=s<=599z{Nh!1fcLJU4BNLXkN@G>TrY+t(x^4IR&@NVZ}!e zio|n3|NhW0I(O-vS}T&!XioVasjjipC&qmg@nkvqfU69tPQQYv%JyD zl?%=~6T*xuhB%ETd!q`ieudBI4~L{GVStdER=*^bmQ+w7K$Wu5P;P zT4T8E>|qtD_wgedXZa?$(>VDa3AV5k1`kH!B&~U|vb@~?)TzB?nOA0Q_lK>e;9j*< z?s3CtnYEn*Eoo^QQdi>SH~(zr=TQ>TO0Vry2EX=MvUA*`A`io~-H!|JgiTw0xwF?m*9=nvx)f<>I^rb9j^W@#Y0)&^u=a~4-~e{y$J=>~jQG%B7Wjez zKD6>Kk5j|wdE!%Z01?T7Qv(-21jfzF(_mYCOj(uWY-wd>g-^e<^ia z{r9uMt~w&-Kpa?VwP8aa$PA}-F^;A$y>v_#{OUt}zT`?0H_$+p6N8zvGU(*5C;e7Q z^`Ui%d=w_YY{}%bv~t?YsXL{%K6&{P*9>dhc#Tn`4ySEA`b6Mg(#Sf0{yeBJ`2>4V z-kmuM@-@bOOihhGAhy$Peh0%~&ik_bzdI)g0 zv-`ZF$Q=rvSwk2XSd9H!xx&ZFQQsd9yPiSEo(okaP9;q4ctCsN>IY)uyw%}D$j9bUOnSE|qcS{TIW#6}hLApC38{c2(2pTobe& z?^Ka|O?5E~W5u}Jr1UynYxC?09Ao;9o-|4NKCRH1A`BsYS3JwlAAE7)*AV=WMqdv{ zY(f)LW6*eYby@F%S+mx!AGWAV>-Q0Tk5=d2xii&0o#f+?I0%PTMySLaLwyISt;5mV zZ`g_NK62$3!>Q8>^Wwl33PWi850sp%a9V13ce`vgOYyWfN~7-0%3_o z8GFfHZdcds0^P)Kwl7w#{%!!mG&{5L$IaJL+K4WO=<$2msalB}-o}^yxt)Y;Yl5D^U;!PQ+uwkCGvKj?7Cf#lduk zPMU2tCL4qXAN@Jwu0-)t^Soa4JoutIF(of9#qKc8W6xb8L?N6j#vgimN9u^?g)GiViJkxKxU7ei8oy`H(H;_o_Zta0L*4m#owJOG zsA+5_bNezBkmX&@%)~xxCEQVD40Q*25$?EI>=xt(cPY>LSxO#>YL-WoE*Y1|{h-6d z8Kor>O30?3Y15w%Q`%t6J1OsX2S$?8s?`S#ygfZ<8IhpP7V(aP1-pMmMMatYK@Kr@ zTgz2$cm7P)Ii{~Qm_=r)$*Svoq zDNc-H%xQX0)tBbb0&pFPT)fmYOeHpcbn?(+;-$jd@Olxi!)an986*?|Hs-Yhr*;1o zp7M3&mIZrMDqljWk?LAeJ!gA(*zpw+vRz!6Kltt20jZi_znJiAY+|#vz$v?Pa9Efy z;GWu5W7p4-@C40fNiwbTWdDiu-|Jyy4|S(xdf|LhN}JG3k^ZWUL|In&#t5^6<@z0fS$*xpgPp$FDnwu)7qQI0s^n`_re6OiZ z3kiBsC0g>k^~!*N`8aAEbr#`VRZ4ACy6$AD(&}ToYA!jg62=a_f|2(A%~Hv_%;&0C zBlq?ALne>$1NZnW*UDtb&Mk%<`8*jpydwTMC{yr<8jd8XoYmb0MIpGq`zmBe_Jc_; z*Wc+n&f9wJS`M<{nw~X>WnieQs=mw44!ie@fj_dQ>l!XQQk$WX+@6#2%@(KbdGoqD zi3Zk$T6%vkEF9u~=z5(`f5@@SXx7{%7$B;soSnLDLf%Y!jzxq?bUH|hNSFeRlyK~$ zc&mPkH4n>TqKb-&s%jiega{*>4>vl=UmkXR>#NHprIDjl=0?3$e@{anqeMEx{1BLa z|IQ}nG8}65s|sksDh<8z@V0Iz&sWhDnBH)a)>Alhx;5lUTL0_GOZ2b58W6COA=B~N zn4;PX?zoFfGzWHW%s!P9b-!|$n3@fzwE%HsMtJHu`CoXOzShw_qNV!6=lI8LLRjgC zW)-9LjPkQnp0UwNv*KM%Vc6Z@Z@c%XGm4whz~H|gjH*vhzm}w(x#m0RnFvdG>G zht6CsAjvwSC(oXFb_1Y3BBgLpc^x!dv6foT5sWtR_x06_k?!4_EHOq|luK6}3}sg8 zzK2D%hriAyk${djKOvqZ3y;`SVH<(WrfTbM$Qnb@?OfJrr3_(x43-}-Z#4#8HM?nD zTvqn_!n9fre3)to`_HBrk5W_*Kfy%EL0K1*KQ}ZreL4MY+XLWp>Pm1_)O=W6Kc1#E zagXx8H5u>bEwMP%_f4IoqLfHb{gjsOg2rmUFQRPbL6x1SI%kgG1|9=uqWUPu_2~2d z4mM+30ME&i9Nov-I$bLMnucz$Mnf~h-FLm0$S0sGkOC%_e zA72;MV$F7V?#;8^JAd1aXK8%=w241*34LT}^xNm}X&wR}k+}+3m#V4VMS@8%hm6Z! z*C;*;|IKiuGpmdoo8p>T>2p4s;64uvm~{X70mtX6Ffu7X5Kkgb=BS{pe9z^#Nn>TZ z4=A%H4iQ>&hgA&+k66m|lcry84JbvFyuQe;OKPcK?)BjOk0JJ>&l`^XSyRJ9Swx)* z*ApsYPi_4EAI>LEo|MXA@Y>Qb<6txGs#Dt2;L|K;n7u>h>42E+yu5R_c0R8Xwac_^ zJ9YGD5xZ(Sp>2x*%|#LY?)aqC#1h6}7}aMpd8PSOch6VGzufh`qW68s$yq%2q{Wzj z&i|aM=f6YZU6~7VP^t;)AP!~JR&!1aw6wOK?;_G0*}JKsA#?SGHEG8~oxXK}CVy0VXV;@}2QrP-% z4Vh@UEq(>!I)>t)po{3fgv>X>$q*ro&zUnqy}eKCK~XLF6J9TGuCAKlHEnX$W+SNR z10|nfz=Xn2X*cINwz{jW@ zRZnqI5hida`ymxQBfAMh#T3{4j>;D4Luw9IeH|U^_Qm4;Ot0wh=AX5cviMUWy8JR= z3AyOKt@)bH&!{9|79lc28OepDq$mv=wjR3;$Kj8s?#T=tI>YUVlniYT()oiH@9HFH z4(s)1qmIn3Ym(}^9lmrs<{`MoDDTk|mserrtBi_VW6@Igbox5YaF|ce7XU0Jb?=`+-j zMmo7IZA72*=c^nwu`p0hiFy%cJM&{w9U@y?XWiqaSN9yHxh$pl8MvQ^jX zKA>X_3afPBcu1I4UyY!E6WTI{<0FehpVqXBENZMKzuIpcbY>)mU<>EZPyY2fGn$A?Re7!PQ^eKW z$RGYRUZ^=Vg2gPv7=Hr-0oG|UiqqXVx7G3&0v&AGa?fUBem}V% zqVz)~$bVK5h(PS?6PJnsmjFy1NnN<=Fz1xeaI1#5dUSC=mzjViE(M{LZKOBz~&sdAlzF;cr z8M95oN(%Nx@_+DH^xgA6x{Wn27NzSl1mzDTYnj*Q-`?K3YSN^eut{Q>g$Xa&VcWKJ z2Mev+OcmM;J5->@GfZjF*lTO9WhSpPP@Rq=0M-OV9?EJbnz8mXVxj{cSxF^koz$k( z$Iw7fWu&IAzrK_JV|~87y;!LUt{vMwo(>oYwi8|_C?Jqb zSvi@%g9Nz)>dl@l78bfczS2h^%o#+xf=&Yh4LZ4rKOTqoA|q=9vs|&YnLWGH9_TGw z*RI{a3RKE&>OdBV_?-NA5T>PJ7OrrwB7hoB5Li+Ch}RfJmoLF9JKX zG<{FJ6~ss0-b3AS6sY<0aN|wOvJr}kHaW2`st5BMn2{TmEZ6a_n5}8xXe|Ef>MWB^ zk=-veFz|Z$jbxu8OjoYxD|yj!xndTVf|Y@pD?sJnF}F(Js3;KfWHzz&ut>Rd4g;Jc zIto@!5cRaR+wV!+c(YUcc6;P{_tr^RB|H(J_K0ux#;uT_fy16&-Jm=`^YKu3H9!Kt zgcv4!;q_@2W8+GvPd8VS@{H3tv*s!;L4=@&IG96*BOhM{*MX)p-l|2_Itwwo)K&~P zmyPMW_+r%8yyYW%mzI>syF*B>$_^#p)>$PyyTGJ*A#V|2gn*HR3ZEnl!meEDE)Qli z$1eeYFqej^eT_||;li+j?{)k>zqAu4Pv)a{(ovWxA~aB^)>{0jSci6o`@iNkHfLJgF>dYLYnsL<7@nuHymSF>3V zY|lQQvuW@$$ox{)v-_$A*&Gyf!W-;?Td@I&6MQBxIjJgRNPP1}32h6GTfYBtmM}=1 zrhE7|=stDz^`YmG2<6?ifBF2msO9(VgJhqCTAXyc79#Crb>}#Es83TKnjtjPY zRUp>n^CTlVCq$VN&&_9U!RLtvK@n&PANs-kbM5u1Q>Ogl5RJWmH=3bv06_pNBYXRv zKQHRo9vFm={jcKoTC*<;*kZ$w(24*-pn)J8ufL&-$C)o%HjT+}n08pTP^f(=`_wR3 zLcyQV0p2$ zeQl12TCI5{$7@&8v2-39dj#Dnh9{A+vAy#P42H3LuR3hs9+dT~zCLMBi&`AObO|-= z<#YJ=H1pycvMg+OT^Topg>~-bw>z3i60%{>coGNQcpJsOLK@1gu&|pEY9Nc_YClFA zkKWS77*ow7s0d*t?x4*ERr$*oWl9_(AwyrR)m5~55bE?4J^iO{|AMFh5!x;USJc3H zA3rASC{bn>86r^q+&AsSN;@S<`01YH=~c&eRDxZ1!PC=i{CLlduauVf$r+YmkJ9t? zBu~MZ6;erN4c%K}CTlt3asnim>v3@*<~_7Uo8GJ#=l!~9$+L8>Udx2lXJ)3i`xI8% zFx-RXT7xZ_JP!Lz1Eh&ZL+888q*29Vo}2HB6u5v;59OF0h*%i&4GlOC;lnSKeVKb` z`>rb$cdJPnv0|ZfZmem`ScIJIVbHV?lu27PvQ!cD&KV20Q5d{IC=RH4@!)}2(F@Ss z*k6uVVSjr5{E)`l$s8_o$gx?nM~)u{3$nsto>t)m(6=pJI%&lTiJdS*0>)7CRoObv zUo0lKhVlB}4&0p?@G>Rktlsd(8Jf+$FTcGV^pjQn+^ago+})5D!IKTI6DCM}Pm?Vj znE}W2vj4`agvzAfRi8Ud+@idOWG*(_ho$WA<1=rpsc9?!Z|LipPlg5Tif(1Zt90!m zX@a@HmGE+m%ng`<62ie@a(u^Dkuqs;PQDY64OXDKEt>ilrk<|9eP*<2Oo3nX#4mHD z(dp&n@2W~JFYMK9*Zr8Cq-gE+1|lSt7H11inz&6twW!2c9|m}Ly~ToYrL*SXm|_d_ zn+e0k!k3tsJY9R8aa0lITD&?_P8-Z8Sz;In{)J1JyfxIFG{d6Iweg!b>-F)}_Z{tT zU*x=ys?~kh1?@+ULDbQ(Bw?|fzIstwA|%=|v&H#pMfP%yV9ofByc=94GP3Tob*bnh z%r)o;P*j=uLDJL6$#L8d@Pb@CZOZXIiR29yzUBRea-d?3$*g zCT<;C!R4$O%-&it?pY7}bcu zixm%~kWWkAo*{WE6gw2HxKb#+p=Nai-T8@%-^FH$7F8a3Mpr04}lXGGDaEQ^vU~&)%qLy!tSR(rNd-oVL zd5snhzk60i&I4fL3NUVAuqh5(b946z*DsdoBQVFK{?N#aG;Rd}rQRiYas%%bn%ZppK{KopjCr`d6-;a}PbAQmvAl2s+K5o@;MC`kus{u(4>4%gJ4!@=4 zJav<6B=AeeVC&&;qy!ZMxEdU4{SBD#IlV{IQhN54T*=4J&$AWA&Hk_#RwXRT*ssU; zlANY8wdFrSv5e%zpg`nRNJU{^CahKYHDh5)rfWokl3r?o|9DUO%0cg zF+q2)w}!3^_<8x<-r}=CuE0~leQa#T1>8en{LPlZkKw{{#-v&I@*S?lLbEo7xK}}Y z8aNi%{5iI{9nuy)&`8yk7$%7qDQ_G2T;J7moh+ajWM-*3Kk zw~PTM-p)PQHl(4ZWBSe?)++}?(neA>ZNJp5#c@w-S=S0-uy?JpyO-u=AdpBPD3lzxoKl$wCt5@G^8*?Nt%Q$M#7ZyHu z#kmn$Y%g2pnwVtKwP6uguShZ?F9Flx@8ko9E?6$K!c1b===D$aml$X|AbLf74PJo( z$k!1J((M<{pzn6o@qe@`a`f>E+eoPa1^ZtsovB&tDcN9m2dG${!IvXW`>WoH0dKRj zZ`0Meb!+2NCyF~(7!KUR_J8&24@5tjqnj?eAd=CKMgmWU$VA!bueZN)EH+Y_@U$KO zG*25AW8dC3d!rJgK!yy_%3aj4Jt$@}{O#-gucJXb8Mc<&dja_ZOWmvzyNBayMc%^)#ujKQ#d61+*lQ_hM zadI_7LN;=E1CvfNEFH@zX(i$P*u;pbsUf3}U!IT36k zQ1Gg+?4`o=JZN?I!;Ja}O~wCCJyeEHe~O>#kq3 z25-o;MldY5+qv3tDEaI984CSuVTviPt^D*-ZCb6*vu6S(TJ&xR#tKVhli z(M%Fyr7rbx!`iiH&YcVOGKebkC*LD2p@@#wdDs74k~E+R$kb(JL#jSp;^=KciEL(o_W|uiw`Ikw8$o@qr(& z@{1ZgYEm2s;S8weeC#eRzN?{1ekDJw*!Spg@0XYlRE0$*N;ly8!e2JRrX6IG3>caC zbHuN(VgEv>=ca@@?i|D>&fHENJ4(IITXkpKzWzZ;!@RcWEi( z<^N&-6%1}xsr+w}{|bW=6M20Z#0MfRJzb(CUCE&~3dY$x3df$An-F2JWVP?5ka9W2 z%(~Tb4yQUl?)M&>>x_(AB8`kfX5J3L7Jc1Flrtt!!2C@ z!C+N0*}gj*96rTad+WvHE)VmOIgodI;7a+H{egF!qT(DyPb`igaYD7NF`UAA%ZUR^ zfcox{J>5SR7I$foIZ;`;rmAZ74Rwx1_ztvb8_J20u$zc5q|7wH92|tJI3$&GV?(#= znRMHfl@}|VCILq(M4%_z={cH7X(P2Qa8_bH2~)9-AOBYExXQV@)1rs@`KuN#gaL0_ zI04jb0Y|TMmueF(f$SLh9f%$&%F;1{62frgvBQT)F_7ieE0i6Bw2Mi28kRIDH}LP; znwrOJtC4qA?`>D8o2e@IX)Sb$_#mqfy?L4Sl0J|(FTM^tXI=47tz?LVQPKm@%s~t{ zrWAPIM&Rn*TiC>q3aJ7W+!=UH|Fj8SmXH3yE`xm;;$GcT(Ld_#VSO{VR7Q4ysKy@M zaX(C@6Un+FVbrjJGev{8G0(Rgy%ynNUoX#*fc79LpVJW~%5Jadx+p6Di7Ry9s@m zSF0rgNH z++^}W4#<&o*Y+^b=d_*W*;fZUS<;#ENG8JRbWP5STi~m07ptdNc`9y8ZWfkxW-;+y zd~}RAbg|TGX%9<@Vxx^RN%Gs%UXXyer=wgt)CyQC z-g2w>b@pI^>fXFQdqWG!Z#4#C_)%Ou6)O(N)`eKAs*g+`BdEL~1>`PLF@dvajcO6A z>>f$e01)q)#an~Na!}uI{4SezIBIdk&)Ql)HG3B^*3h9| za7Z*SUi9;u?>tOHBhgI#&~5E2`zD}SbvL9I0wMDH$OsfR`yr{dwlBWE!s||%M@k`6kmUQWZl!raWM9|327~wT|;#&9U=yHm3cH}|dzpN_wBM~;6|2^|;-)%C_Hs;x~ z=&^nK^nsKAdCB^<|G_&ua`DycHr83KZ7TSjyiS1MdOYh`4s1+qQAlVg367`Z+1qC7 z7Pt)c5@;@9bxq8jh55EmtDv$HJl$z+gpR_p>uYC2zDrF_t+L{zKAF33(e8Kl|F>vo zA>$myv^*&&g&|=CPZ#2N?AIe@B_+oP&k*F|5`$$ezH4Y`@SfTH(C8?xiO|S8;6%)> znV_I0M>@}(1VKob9V1tJ(*3{zYDHGD)U3h0U4ZOG);6>ih{Pntkv2x;2ag{USp{SD z6@RL~&b5^TzktV{v%3}eVY}+Vh4<(~Vd^Gblt-C6vNzboDUS#8TYViY@u1WWH(SyU zPbUJX4T(7t1u|tQAdd%VB+D6jJXy=u189qZlf$$Q(qo1YYiJA{FVVd_iS%IdbN)Jv zvl(iD(osrV@2zO1zHdy7u*a3|xyfldFI6~w0rOpMn<&3#^}CQDHx#;YL;0JWg3kskuZwg+|J`x218QH1|bg`QX|4+Kuk{|z`E>;4FDy2=qmz~}3 zWcHB`G2r03x`O6!;<|%S>0d26T#`7Bsyv;c+fS@WIM3l^NKM!SVxc#Y^_c5phh>kb$4>R{cH<0D#o!oSx8HfSYma z#*Ie{-+RtIMDg}mbe&Ujc$SvS~e| z5z`@w^K_h%&H2qF{Tgf`)mN?u|dOyoLtn=Su_9R#}?FEa7&i>tn09_ z8}nlyeBMS|98H3j=1iDg__kF2{HX$$_~y-ACU2J?ta|KMxM<6DQZAk}lVE7I!>bdv zMri2UaY&je% zzdkfzVjKt%MSt?nQ%s4zzI0&Uz8g1fdiHr6+ZQ|-1kK%4yZc)GEowEB!bL$C)MP;m zK4fBEDMf#0%bd$WUCOSxsSHVa;Hd3QM}S6cK^^l7f;t=QL?2`O;=e({ihXF>)z(xj zHY6}`$mGfO9)tJv2a=7CZ=@iJmFp=cF${7JXd#$!UZT?^k7V{cg06QChB=QHXMOy- zFmO>W+9&{Y()u_#-9DUucy{3)wET9IIi3dCGlI<>8sWIFBK*cqRH^JA1NF;N;Ug$0 z;Atuy-thNjV=Bn3*vya9dWePFk({lhyTpfOULUM_b5rm_D($Zf`N`!&ainF$=%&STqoo-IB!o8o~w+`lcyUfgFk_O+fVG^ED zIiI>y8mhEAjJw0ExX1R6GFl||tVuS#amqiDAkdnoJCFeH1D{9AV4-X#Uj+0T(91L( z$DXClID}9^VpHkD+dul`E;&32dYqhhmY_#4`>*L%DZJKF=eWQHMjzB2eLcCDHw)aw zN*UPIwMUQfqeowQHSW@HK@cUTx9EuMZO9}}{I zxIg3XH@^+6ZYSp3_h0DWJz5VGQUKRAjv6b@T)eo|+WP3K(SjFxL|B-|VDAYNCak`` zl;uI%@OI^MF%@;7)gI#FZykC|v}1D!pS@Dr;s?IaN(LM;0Krv&egPxv{I~w--jlaJ9~u@QYvd6bwst< zrArqpU$xZ_#2BOHHKd)GJ4GE2fMK(5a&y%Ic5`!==k|Oz{-4_ts9D>2I2`&AIv0Bj zW2>pB=!s4EJWcHQb20`Pyt$k`N1m)5=N(nPt+Z$AX%G>pTW3)rfvzAtzz++a2DeV) zaQ4=BGF!neLU%&o2gnf;hqEPQwRp;2BbxccSHCdZ+}SBZMI!{O0l27^vMBYDZ8vJRNx z8dJWsv|Hg{|ES{&`K_Bb|KLOyz)5Q2jT<&tsw;@?Jtz1pg1W%nI(n-^#Q1HmC$wGA zCB~izK0T~6{~4~Dl#=x4^Vt&+8hD~1<>@R@&r_#_42HuOS`tRb7U~z8+W#f8X#er$ zCLFjdH9l?EBxd0NE{#l?U$mRrinAJIa9G?9^dnZFlax>Zztg zDvpD{>Z-kw=MY4)NWoeyM@C3I0Jj{_STm{|V@+BGs zsp5RvBrcQ^QN~>|$F5uV zm0H(A{m7iXw}NnmZTp0nXfGyyc$isOT4M3AdE>^cs_M+aj-P}XY+`}qHf-F8{;7tP zhi-Y{wy88uKsc*^{kmFPW`8%8)xxl+kSb<73}A9uT%2mHnpkmfaBHyREpvlPxiY2U z?e`UcjdPTm9S);s)V4njy&W|^f%0OVfG+D>MTHF|XZfL6Esk;>1>3{=it4;Zm=JNL zn9@xgr|2#w_LmC)qBx>vx#q=IkYUobEl-xX76hyjE_bgL{jK>Jl;Rg8oN7u+;eArm z(wIpRw=qp-)vEmOK6l>20{lnRyZohED)S`Pa&Tz}oqApLtMpwNN0h>8QIEqa*oL?p z{vXB5CT_VipRBFMZ;7qBY4z&WmX-s0_x{O{dUr#bicVsYLdJW%*Knap`my5l!PV(A z))^2r1%37?7Z-!<>%52mzApR6x?Q`Pu-77{B2gs?IyT-+dFud)gjt{HB&Ps~@W)l? zg;yNVzRti(xKNrGx7yeY>fhh7o5^bM)L1-N`RL#r{QdMz2FanQdvx}68e}PX^ ziBfEO{QUf2d`A8*AS2;+ReN$~{~u+`-Y&wLQ+Da!eG+h_rgj$I>=l8-aT#3$D#Pp{uw!ylrynp;;4H9Pa6C z9&iA2eDh^xUw*A-Abq#s{ZMzdyu5%dPGdzuG)EtlSA%ihBZdzLe?*qgdI>V@&)7^% zBmMoSLuqnPr#(^?c7Dgby9XaF$M`^TiqTj<%6FN9`_Dj6zowo@{w(-nq%2fj`A_UH z3Ig*-m(Mn%iSVA}m_kOgXxSbZ~9&2JfG|Q*Pqgj@rd| z^6R`H`=A@8SCK5LkQ@cwu!wp+bfQE)BnEuSeR}~2}PR-`{fn;rEmQOGJTc) z0Wz&#lEXlLJtjOaow;+P9^)XfQ)8rg?;f zcf{UQ_>)ytj~_l96upXE`{q5HT~_B3Xtx{qtHssZTfF%mgHBZ>@1=i-gS$}wq!p*G zNZvMJ#fA##5@_6?PwVkXN_!-HmHXvyL%Lmj{W>k}$yV5AdZ|Zeg`P=>TzzA%{{EoQ z^_)oZ`Cf;2&m7rX!5wYXmI?op7WcZJNM6B}=+66`BceP$G!njuv84}_ORdT!KZ#%O z{D`)CPi^oaUN7M=cK6IV<+~2XTl=fbw!bu-=K~csER3!Ftl8~&Z$NOuoyB4^Gy02% zZ_|4pDCuD~7S&I9xJcfJdqr+>R$Wbv2{Vh!81|!!rn|Rz^QiqE9&_QFhSvdLG8(Cy zxHz1;cnwCw+*y9?2OVqn%&@s941AOiIK<{M<3bQB^hPlOYdQysm64vI${^f)f@GCnfcqQtbiV zVQ1*b2y{bAJeWK>@hPi9zDqE^+FfeFYNJHw#$GcVrV43?wBiq9G&u;E6RTo6yW79A zXY%TBRHs*LS3itWR#AD}b&_iToDj!tfvjU&`Dy)~?@w<${DnLd8n*OUJ7_M=^L6ZZ zdNlRB3pbY}k47TA{vqy|6{F&0R>BGItfaCRGqaM=c`hFwve>EVr)BepPzfPzx5%Ql zHP*}gwh`yC!qx(UaMd9>*5^%uLAOv>_jyVRd{?DQ2HpEl2qdUvHnA36>VFKVKBw!U zxG(@;>77Jk+y9!@S?1Wg;d-hxSNq)>IMNCjX;W- zHJq+J=+~MhL7K68H^bov^oO&jPxsXPDld4~PX4$HdJ<1EV$yHYjm4vfG6P4@aq4B? zTzZ9MCp`Y4`4o?}NCc z1iPXW+CdMt;zYhcjtOSjZDIkcJn0UyXC}RcmWGseui1=|Ba2B!GHWOd{OfOjVg^ey zD$~@muNPwvpLI)zP@I!jM}DN!eCVuX#LGC2I4Nxq^fqjD2O8EZB{;k7M@G`{qapfu6ta;hH<3{2?>1ZdEsM_6HAZfHXX<0v1LZ4T3Qc$CT$X&HZNpgzyLngtB;VI z_v&>eIvOMEnZ0|t6)R(fx`V`&bf!j5oRS|ylOq1L{q2))whY{?N$xR&v4etP&12no z`-HjO-9Fa}uAYvI2%OBDQ^!ZF^gH`z52X(dbGWzmJit5;Lcnq)JWZ{d{ zN`)?^4N<*NPOuf&QUQ;_x$@I=+rI7>n-}ez83^vXd9v*NzM_tg%h(8SvxHy<#fyk% z^yjzfuT=%S*s8Bu{r%GT9^ICEXJe0FYUlJ#KhD-wY*0Y+B98*Yfw*lj-IK2Y+GH!<3{9lxi$i!-$lfix1;w zDEy!r`dL?Qd^N5hz{1Tqjql6p)74xJ9fpiQ9=cboDBd`#hzyVpNdQ~mX74eS9xq5! z;YvfLr6`A=)C@y`?8NAoM;Pjf77V_rf3a@8fZ>`%j)^}yKS2yln*cRT!}enSilI-< zS4AI;bXgL9v~Tm#TZ3Xdi4`YN;PQ5#K6^%{FlY8`_?UR_PJ)=E%RIQ~`xLI2O2hRz z)_-FA*HiOBy(mB}K!C}~fbgRpfvdK}j@(LeVhN zPNX5Uh)-!JNsE$E$w*pAk|u4k5}{Jrm8>G1_m@83-}O7s+wZ*oyRP>iefxfd*LXgU z=W#q9kK=Ll2LVy`9WK;oTnY7%bXFHphPS2Tm2kAPl<7by+2KbSb0nVne?^%~YJbmw z+6Ud;_x%(`@2Fq|m2|6JIF~{uJrgt@LxxNma+Kz6uMVB;3r1*aN-4Ua z8*D1`<+;{uBaEXYN~|s<7vIv7;;(O}DcXP)<`LqZRk&Re$S~p)LDo=aoeQDkKi^ z_@RzrId--i;%R9Gk8vJ)DC>nVE$r(dbn{BnI_~fG>~qaF(Cb@?j>usU)YZ7l?vnI- zvVs@!CiO104?Q;Bix(KZSBQE56sNh!>0=YeJibd&(OGAqnKCMu3$`BG!ISl!ysg@TUEet<5NQzc!nVA(tPLNKpEHqd@?;X9azxT3T;hXr*p_%Rf-ky}W>C68X3wu2gP4{$Z;~6{b7yC- z0S~4=5?MTgYH@gP4kzE*bs^NRFJO(YXvBfk_BF?HOy`A8gwRdVq z?Mr=Hrsj5suiUZ-XbJF=E& zzx&mUEjo+3>Ydg~NSS?mt+b{iRV;&+{$c26R+iv0MJWdGL&ZtPG27Y`zJU2>>+_`B z;X9ECH0^7}zl|4HmVXdD6-y_MtXg}2-Fd*)wxYseA=7@7-!YtW~4U2v?E z=o;w^yVGg5@Z-F`c65eIz!1)2>w8;~=p*rl&RV`AdUfD@d^>0>{2x)2C~>s?EJW^c zvVVobq7Y>YtygFe)~@AU8WSq!FkJrP_4bU+@U`PN@)Ie<{U@Es4Szi5ZS z_jtwf<)PJ!+kZTAIhLIL03op}sUdN_A~dtKf3%<0@E-Lq?kx$NGylqq*}4Yw9gDo$R(`)%5_qSz?Vn)pK4MDC zr5;&ym>xa!)2tyX5?6*A#x+Iehvit!equY@(blr^yz0WrqWpz3>K4q-G3~#oaK)gp zo!<>pTz~w1v3z4ev+)*xNlC+FZ)6>`yYMWq^2;4AAVODW>hBl2|;I2;DDE12H7Tp04Gi- zaWxIne)3pF>JWAgWK(m`s3~e`)w6>r1>})8LrjXF+3qwMWFjcYmgKNk&;dWPK;os$ z_*Dm{f1S562t}5;Fi6mG%0XetWD7GGEQKKyyjmJWgVVpiyIIOHjw>MXI<^+t<${z-bt72t3QqkFf`wTV9(KUH-`XQ zpQ!w0(>*auOczV_ZT`uDke9O6{}~@gb8kG7frm&C})@Ga^FV_HI3w zoZP~?!1{)S3y`R@qF|3<@?z)r_-sHhk!NdEZ! zhj=7d&uNVZNT*SYNDqdJfF5f24z?M!>%b?0=&j=%o|M)y;|cU+9^;_RRL(4+R~3V` zp!17PF6u~JyTb~DzH_+wwPxGDnKniF78||UE6`_r`cneuD402th9xA(k6-na+OfN0 z--F&4Rv@GEmQ1vHeXAF(iA%1713{N@QY9iYz_pk+o419*On=HQ_##y-M*^9so!e__ zEKF7X_u^tgDE?7tX=?!$Uv^0#eXAIPoV)JbEX%8hMkQQ`kGWv9az)&~@3mOPYil8| z1jQp^{X)KU|Ne{uT~Ig&9Qx3cPZabK6Tr()O8$h+jhet=;U}31=@c&XnX!cscx9-2 zpwt2w_VjEc?}ZzL>$~On_VxSsfAF4h(POSA1<>L#AoqSom%v-FL!(`q)7|AhdHjr_ zZ^(}+(=q%heHmaNmdK;l=m{w4g?ajb37C|<_(D1v`I;$z>dMODD6};+ws7WoA0sYT z_%m#?E++=GqAt@GGv3V18`K3RdPw!)NE)gF9AxnCBCdwpUB-RPG1p|w01{OCZ;*1- zeJd>BBpo|;p3_)b2#}FF!wa%8k-Nf#yTsBXAg%wD`=I{_*I9Z9&=_->7XlB)Th300 zXthXpx|kjn;j9zoBzq_fmd)12#zkr2Vb8(rgb*;(3VAf-q8}5?k{%|-l3QUNGevGA z)B?$k>e#X24i-tmGJ}0!DjAx=xMY%7-X^Cj=?YddbLY$nKqJLISG-6TYO12E$>KuluOx5 zs@YV{)Egr)+@K}<9rN>hP*Owv`}9`EmdQICuH=6zTdF}X3ZO$&@XDVbSMJOfIGs|PpNFfvD^I9m-o@Lu zw{e=o2nkdei>G2XT4~@(dQWWV`C^_k8sd?M!p5nFk6xS)rU3i{Z0ed0&J#uH^U@kE z9TRX^0BcsQI_805qxM|ZS8=LnuEz(~xx5+t!J`g;{D#nge zRKmxJ=nHvPXT2krNHE`LGNB$eqHfL+VSvQfIGSG+m8MP&+BH~83S~nA?-jKF&T2t7 zzUJk!t9DaeE2K`pu=MzL_`-#i03w(tpGCi~jiT>PgzC|6TEdBvV;tLE@o#GffZv_B zeXV1z2pcv(_swk8Rwtg7D0^D=)eIHtJj$w4^r1}RK5+1$xvE6H;^`N35R=pMb1aboqwibO%i8H?ZcC)Yie7$(kMB;HIB|q`gl^hu%1v~A zbnd(NFF6)Ztkh` z^jU2wuMc_K6jf~*s$(4&bK!D+Q-N^jYGT~^0UVnkmFPOi*dwakNI3{{OKP-so+#e_ z%_npW+V}K*)rR1HQ>cV!I>Ph(Jp($kl)W+4_D(CoR{X{d+#JGX^m_N38C=5o!Zs5I zqR$vgnS%j7kqIN4WxE4D`ne`CUl-%I1+weT#(guj>_-*nH;krq@-2G3>2GghZ;zj; z;G1^y`gL_Jt<_@{7v^BXU$`JQc?~B6Bd!Eq3}M!#`E$&(fyBBiMiuA>TQ0JW*54ZM zV10RE_?)`d|GGVqPBA5`deN^ zd)1_=Qw!g|J$*HSQM0qeu;*Rf5rqSDZC25CIBhiYHDyJ`eP^Wl$?zp=oXPa)Y zow@n+6zuZ8m`-cCsP2QruXk@>Zfd-0-Ibj411m~PCvN#_;?XzDKQfZcaTOYPl4=5s zHijoTrsSavSjoTtf}+g>wwC|%ZDi$Q4FnhbGx%8!tgV|ig&0q2*mAPY#+7aFvT=4p z?)K(&6)9fAyX?xyGriMM$4Ng#e*~bhxEHu!_ z(5+zyELXVJvAZHQ2P{1QP-X1cR;0Y(nkzSK099Jklts65BC?h<>Lu(ErjfA@vEz9C z^l2V~C=bg~il51ewqcG&hQ^&yd8r&C)f=bL0uDt>r69_s?SagXBw|%5o`cvz2;D_F zvv%YHxn4~BHP_hq?D*A2vu1U*O{(xeb6%JEmVSnHG__NjLGDL<`A}XynSu2jL)ep0 ziIFsop`rtXZkm!cvmlTF*}YdU0<#U4yjWG;p3ORG6AVr`ScW&D{hwlcLBqIQ@pKSSoW-AB%$9*lkOGIkR@2dvsJ?tM0!Iw3>!N1 zD;71T-h&>WNv_ELHjqiLE>1B$WZ&ILcnzzN6CSC4-RgIN+1|~9DkDU9bA;#j2^}>1 z(rtQXn&&R2oQC%MeVWKs`t@s+cz|^q0ihAxVD0MFyi#`?n4bvUB2x20 ze2q-rU~PwtYeHvmadC0`yoevZmrDPm1<qudOwpS+z3Og2HRRfhJEw!f$i^!FLdu|l4 zx!o89=eCn5P5qo#&G&;m4oym?CIS6fnIH@|S61FvDA%_y#fGHYP7@R2<5~-Aj030{ z545AQ4~{at1r}jMUiH31#VMX`fGBy@{0?ExamBQvyjvlIjC783= z{z&Y%;!;HATt(`J-eO|FKKoOD0l6=+wvLW-Y;Jq<$5WF4a^uE|eEw@;_}sy6e>f!= z`BwfmDrIv1E_skc*wJ^@lz1dsRc=jUY;+_rYOl9FMYIfM#i z0~iTXd!~C5$lTm1B}mVy9w~IlcgnOmHEnEOme#(j33mTEhi0jkz{uE1LtYcvHFMV> zFXj++p;K$_$=^G-ZL`0k>MGD4oQ+GCJd?U^GV<3cp5KGd3l=YadUq)}Q)<2|@s_I8 z<(k7zclW!gR!@tIQ|{h9S131dVCnbo=^M7XxrJXg6~iy*WmDV7{qll=dkuap2zCf> zu*F5U>lWdUIEkimw?DhP^hBWAU6FUf@(WYhG57Tu^v12uE6vLZ4T54L}QUD=y4?}Ch?tx~YRJ2Fm1W!c*X@iLWz6oILh%N-pZ zk8K;4_UB@>tY`)4tRu2i-8(a)dC6cymukKjXJ*b{S-pdY4xoOmAg-k?tZmwj-uqP< z=gofyd~|4OWqi{GtCVS+dXc%`yEiu;yXs8e$~0seZo(KVp;vQ*?~xFV2Yo|Q2|SGpBg|C5+gM-Vw^_GMFF4t)!rxP-fX#Siv%EcA2&sRV9l~4u;K>Qa zmY}$XIDS_H>X!n>l)SF2--lb2Fb-E{!UV7*0-`KK&CEzlvrz%>6Ca#4T$x zlhkm+Kibe(S9dW>jaxXpz3EsQ<~|vhj0d_2g*WXAy!ozngxxY+g7PtR*4%y zMRuq`H#c|J*pl0v<46$16xl~Fr<>|OMZrUd!pozA85d}hrFGe`hx9-`A^*>-;kNVd zxQ^c0l<)9eShzT3R5kGJWd6UZO8{e%QH$O24Y4lk_Y;CAaMI?XI_DGTO|S2L|K-c| zprS;`s%<%y8z4mkYCGg#&BAPRoT}<-2?=^?-XtqE4U6bNeX#6yQc}u}`a|1C_dQ6% zBzG}TRyOAPcd+%(wZCDnmHL)z0L_Fq8#h&PEU1hblf7^>iwa|$z=%3amp+pSZKTB~ zWO*I=q>_Z{kDJO2)LgDn$)!$PLvk~*PIsR}&%)LAIs@j&_f%-B8)uRP{*jcVqtgl{ zOm%nq*{YX)7VkKU{IrY=Sf>@YmXtp~d;V^mo{kEefs9yY9g%H=kBqTPIlpV}cda+E zRNhx>v@Zw^`X#QN^FNXM-Cmg{(bP-hX3B$)n|OP5M86v)ywn|((m&H@ZgP3E0fRMq zNH(|7x`>v2-4xXhslDSyOP|*dz0Fw$b05suAaGN1ZcWGBBeD&cV+q!Lw{MH>)!(wE zma+|zqW?1+TU)K%c|;x^F1m85 zGy0x%+*USFurb*7ssj8BX~A5Jv1;LVnjmYKpHyw*hx-{?^xT-UbNa_Zz3;JVBXX#U z8EvR@BrF1Tp;|ZHvYUe_9V0CS?X_$y-RRZT?@sFg*W_%;j>>BM`SVll@4skaU7sCf z%yPc;-CRK}g`-%CsufQqu?33hF@vs^`eDC%bCz99{u$)U1o%iaF)+_!o zQ_Szh7ZhMJjj_~5jqLOBwn_{B5$=sf{;FRnku#fJUD{@TzsnPP3D!x#t@_p0^4*hn z5#lSO@wTRW=iSJ1v1F-;a|me&aqF^&4dd(Mb1C-ov-I?VJ@&E|VcR%^fJLwWZf)1Af)2lj7Uv}8#n%YA7l`iTDEutBg4P75Cw$Os&?gPGe3 z-(gol5zGJ;sUCYPE^R@36ri(ptj;c$22MUYw#E4c1ryh03T7VkGqaPS|Ke^R@gb2= zI|D4o6VGhyOf#Xz+jF>Ptp@e4`X%I5Xnub5CgH<0Nm7h>qjmIBXOXLSSOl*3bZ;@G zqOoOuK~k>F(%i@+B}~yf!FTSr*`(a$1{ECsSNUH@X*VMc6(NL3B@LP!--t zW-61~X31om1&hpPe|DZ+I5o1>SZw*UALTdGb*<^b0Y*pJ&I{&)E^G_kdp3`HEqhru zwF8;GnQ7TllajID5X|2a_vx^{EdPNBh69f5)=j~)M8^z zs1FUPN1GbIPoo}V%i6n#s7E}koe*+ft@of#BDE{bt-&vV*#g`l;|V9y(is*(6P}!o z+sll|4kEn^wYWgJ^4NzC)vq>}xxcUWs+l~)fPiF$b<*~yblN^f2hwW7047|iQoB_| zy@Q|PIf4OuFl@`+ozfplvO>{-qO#X*n{UW^`4W#nz?ls$ugu<5)W6~p$@gdH?KEMD z^ZfPVgt1)21BOo!6IKhXX8RZyV6{hd**_94?tapJ+8sf_c|^vhrW0Mp&Jr)R4Sh|) z!g#7ph|N}9&&x;@i7c#fL(|g}wP>ST=UfJWxX%v zH}>DDC$Xb8-skO&9BPpEK-T#O#ytfGr!XLk@DXa3N8W|-Z1(Kgr%#?7q^(6=Jc|=k z!=;CKC#Kw5bGB+Jt^z07$Hc~v^Z54H0tteJksHFop#{n!{s{^|p$g4|;S>fx($m3F zV^F%H$}CC05SZ##baXUl9c-N_Vi7W>f`XmoqrADD06)8S-ShDg*D(n-`>;s|hVYc1 zAFMqtX`se@fO~|5(trH?2;0yN40Ycnw1~d$D7t^YbH{&D8%T2yBTv@RVbrGsKN8Ui z9gFqnSfJ8oU_y$>k|p)QuJH!>R9>D4DrFP%XkUK+id<42Dgb-tmWz&@z(WizMYw4& zO*?4+vYD@h6+LbfzbMT1&BLm?!hj#Q|52!2R{ttCArbux)^V$zq1RuLQFWweJ7Tbh z)lbbO03Zl;ckOEB1w&8x2L*Yc6fzJdz1J&ZLX}--*V{8xg(?_BFhnd6G*^jT`<}nb z`!HxS?{d}y+OJ{lQTd#ixbb}a8IoLVioc4%pK7|$p(-O^=|I0Xz&zcxluB(&28}qk zwM&VmnVDq&{yE@tm?9GOFnov6oaX+^O!W#0%8*movq&qIR^0h-Eb8{rGN%b;V-{b# z4*<=~%8ZcdE+q)k%fvrdR5*CeSb{O6QRzx5z%m;LQjE=0>+MppxCfgtn8aNZ;A&==oM1_t=L+l`L)p~J$dga*(uwO1>A<`bQ)(=jf6wWf0K{r zwEEw1Z}S!jL#qRJ3S*NdYz?6+IOBAW=)YsnZi5Ij=#Pnu^N79MUXd*cRy^Y3G!(8| zy0nN*gcV)})i=bzAniD-E&O-1z{DA8ZNG%D%_DbVx(mbS$wXrc6%M5fg9U2K{6x*4 z9>R#&un1svgAw{-iU167&<6W@MSdGdZ=`_GL6Qpk4|;cJU+lACWQ%{dd?x!& zk&;p3ME;G-7#UA=St_cJ;s4eh&M&n7MJM~nLxk?AoK=-G(9_Q}hZozZ(Z^#~wN}GP zR#CXhp)kfR`6tW~i7BFx75JvTFTRkKVYqP>#B$&B=aXQ{FH8WyL}AWWWiB4+6*UTq z=7t;>&fU;fCl05sa~RNi$7-*=i;DpIbQYxid0(Sg(|{ULmgtQrT97 zV1y;8{N|Zw>ugF|1Rg(sekN6sc+gYpH}O*ZLTc~)^F2GRr&)%4aic~TdW4~EoPuS3 zpUMoHat-tXJeLnGLr5<%e=0j7a2i^cb9clP}8nz>)AmeOcU zmP78^rE6E2vBOICa%SwO&?ih-A2!$t14F+(PU#*L;aT;%Yk>TP9Zz7{a- z6=lkG=MIeWXfm3bQ`&lR^S8O>3$3i4uX_4%6LDCoZ{M)*&z-zgyzU5FLSnyQ6_%5V zE|Qg6F^_4#3&sqfgM50-R0V})9B`y1_~{rK8cM@AD1IhpS@@tu@nzgQMER|`QzlOq z^q^0kd_fyydxZ60@RMiHpVLu5 zrdwC=Kat|)eWdKMnv*;@1H5M3$dS)pzC5R~RzmSJ`@6~As^q1s>8WI?C|(=`2DQc} z5n&G>?%866hjO4+Gz*1ruw#u5%4qfwc4Il~nwp#GbucW*UlZX}#!(`UU>^ZZ)L`vl z!;r_?!s9=FYZ!*s9R+4ONYbP4fVYitmm8^j~6U90|B8_oWyjpNhYbCa&$ zDR!`+U09etMLy1s)<2Mt&bB)|g;A_d@u!qA}p3>#b+BAc<)&u3rFl9{ zcW26BW)nosn!1o(Ovytp;K(&o~!Qzy$#0wo8lbJD0h zfjWeoWRdufhK4n}OkF+yYbC(FeZ^~Q^Sc1u8zCXfct$v!N1cs(zP_E4e@a|C<3*YR zrDkDaVSg@YRKq^#d2!#g1%(;7b#W!FI0nB2H_yyOJmSA0w6Q<((f}k+`I7o{&^*5d zA|#Xv+pWJShu?a8l`_Pnr$_4X*4T^TEGS|N*1Ii!H5*6DKCAS~xAgqWVshLui0pT; zXY0B$7hdU-YFx%gONvpU#fxNj_O$%g-TsPgxtuU&FUkj$*Q7eCw0AR^ZO)p@=|}gs zw)iyh7P~2RTb+NVqg*^|2AwrxZh6$2brY6oU0((*z82bn?@%kgv?r59d&(n#`J>TI<*>4 zGq$7|q_-NzojpUY;xI8ZeMYv!BBJL$t7@y8Pp&*z`W#KMWvo(8!~W&Hb}kV1EZyRV zAAKY^8nkWHT64R&f5o}T$a110nW`!KnOX1^OCOK>@A8SM!HNu~n;`~*oV#TJ5`*yp zFBvpay-*Jke~vVU!GHA(KEf!kc0EI7(iCqTgN%$oG$Wc!CPZ4?q0JTD3E!~exl1t) zHu~Kd=1s?T3pl(O4fj3vvTT?!Fu2xLHu&D$uXolyU)n?85VKqaY=md}TgdoaoED5# zQ4u54!0wpDi$piTZR*V+^_8>mYy|Dq6PsgX)C5Q*kkCQVf7+Cpc=V5CT@nc!A! z#n9{zAC`qDPhUUM>6wA;2YZ?O;TJEiq5>0K0{nUvcL`C3TjxsiyS|BdP^5qIR=rrS z7SBA+=Irn*rAPOl7dF8IMu<);oDruSrZX|;-h5NnJDxctLpjFuT!ivNxynm}w3xGBwht_Nv2vY2+*P=DONt+2dH7 zUNfQTpK_|OHCAP7MfAZ?nw`}hegj`x{ihU*+4ro$X3!UR=+sqGKR4DcHvRI(;KbBt zH%{*+KJkqk92}ymz0YtSnW>l<8)K%+I)~FEwOXT_Gok?S@%rNTq_2RYf^wP@UwgMD z+X1f-EuGgfJ9Y~mraagvyYj-Mgp()x*Deg6d!_nrw8IOGq-TmHdba8AET{A{6ayi+ zJY%T2(?&^aBkP}cKHhay<-BH+=Wy=>Pd-t71M|&9Z=*eO+uEEDcPnYTA-9jI&XTE8 zjfi}({cT{&wJe85Z3WrF8BRf4Eh|S4(wg+$YHRbNS@$MB86YL4$$Y4mk`AS|4_ayHCp<{*%(yGA`>lB?>&g3?psFVq|WYay9=K zEEJjeiS6;-g41IoB0}tlqCbHNGbbhemtX{~S#t)x_M~rQgnGe!L(|k&kv%CXDM{+X z#DKn-`Yxs2FgpFR)@z%_RXL}2D@yecbV{YA7%$E7IczG%bWNZhC<=Z`xcq_uigC|) zV1?%=MiSS_E2p8oK;#-fD02D6viI-b(;AU;X8EIiee};fQSxSXsNEfxXK4OXduwK{ z?U#}agpk7|4LJw>xPKRK@;byWz`hXr%%c;W>FE@@bLY-3+L%cSCb~F?Fdf^(-lUw% z`L*uk&YoJc8pQyUES&`RJa}L0)@zkFOUy^Nn2Rfinc78OZhD_GK0kS6VXs#A`mU)N zPc$@V?T~XSyt&plKE?X0VFd_vcSVGA4B#89sydgHZ0Zip$ks`ZKszW^SEfDyw%v^C zjFij5wo)iK)Y{UAZy_i9K|o_c0i|vf6b%8gJE?_!x(R z@EKN5Q8K9h^$pgES4IUsOMcwj5!Q#oP-oz>GdOg*n-z5ggYT&dQlC<*=zJj*znXMvnIP3ZX2hjLyw7i$9V2IoiASs%t64HfN1+ zrjcui2lc$WCu(Pr&Yizd=*B-&j0m-kdq14o#2d zWvxxSu%^Y&sy+1Ys;(~|J?Ku_psVREqdnpeOm7Hx(HpnWR=`W^s;<+xZ?k%%#lnSd zaM~=GH@%nkJ2(S=4~S~vSqol-tM6Dta|pre$ho64Pv`B98ozm-)Ru$QTbt{R}6Do8go5#{Yal@i@GQ8 zni}!$+CPF z%{N^tg}IU}3ad(g0f0aN?;6}?#ri0lL!5P6CY{rMwVYEns#6w2E5G43X0_7b-FzAU z*uFbFItimJD_YV;IV-$(cFcEp+hj3YN6yu#vE{0a*5fBvhLt^u>e-moQBvQKR5aN$ z{np(b^%Wegbx%gbjn>v~f}mR_CzGvbnJ9%uTWTiUM2@Wl6#|7A{}@5rq*(l>)Z6>_Nx({d$_&)+d%M*YjDW9$Ey~@@pC`($JW2d#WQx zCL%#48)q^bemDy%HH=J5F82O5!QOIgmzFdBpdm~i%lYKa>?&%?`Dw(FE zY5c2zoc2Kn%f&s^ALMO{@GQ;|H|s4lDFKTCNg9mkC=$oLs|9IH-F8NmcDghl|N9>& zhGleX=qwUrX+3J!Tt#%DrPuo74l))X+w3j6NU)DiuNJ&eTq~jd5sV-awSt0Y*8?4b zavHYli$XROLbk)b0X~8na0L6~HO@hPsbT#pzPpfC2+WZRKW&=CG@>QI(#Umq8yYJf zAE1e7HDKkw*UVIrIliyOdk)`VzgVEod33yOntTTXjEn3If&TeguMk=rWC}o^w<4`) z!dv=pK{jU4ex0*Eqa9)`I=i~oH#WkXxY9EO@E{WXtHL_+l!+ffXU__d6STn&O!IhO z_YxAeqT5`!@Dyu7Brd-eMa9Jm#^E?t!LeoJeQ1Bh1w%;nz= z!(2R*^#x}Jtz$RT?|>{KNV4V1Y3k6*o`0WFs1RL}lZAvfg~=Hl-ZI)~=OPUL{W85P zQ2x<|O4u@43fudZy*k<=Ktz0TWdz_qV)eiO6cKVWv*mF|h9_mHZjOL~a~nqw96+;q zQ@95&t9{o4@2mZnfGy)1!(rN2X-~Bt^drM4v2xzh+=81OfU?%Uky1SXId{Ak*L-+h zBw|r~GLu?eHYDT?=`#OD0~OqlvG(t7Tw*3~=uNM#E5Qh8Xai%VG%y9w@x~iM!{Oq! z5h6W8ObI%SPHcUK77B{?qf9^cs z8sdN^DDSIRJ}u>f*y7~LFECsf6Tmu#nk{Y{#H>;UL9)o_Lm~D=NZM|BG=Yr}))+Tt zH1>2q{l_b4oJru~X?KDHTH?Eo@9H&4FsTzQQzLLgvH@DoqbTFgdGi?Ln3Rw(QO6lh zl*R^p4B)dc{#TG}&s7obz|`f{0AesIrfT-v_zi{mnVH9B?aa*XZ@`C%K3%OPY9giMj?e+z?h=vfQzH>M*_#omlQ9essIU2LP8a5W%Ue%fA7#+R%!tlhVVn8QIjX+sX zP$eip^f$ui-McsAZrf}q95;xCrUjq~)EsGH1s~3Hr)(4(IeK(6NuB2T%V6%WSe)+! zK_EQZu4chq6x22%{><64)=QSOG^PmiV^*Yf>(&j6sVrGzyFS-YC$1oQ3qgHJXN(Gk6p3BSAqWt{a2yoy{?9<2WK_*pYyGDX0c`ag8GyZ8Pf&y# z0Qbbxr?m($X-an!u|c}-jQ`NKb&kfGK3DpCMaA>6f5a4<20HiHJ5*IQhPm=3m+62- zwgwwM-!YisDyhF%;HX8trpA$6J6^F}`zr8K&EosK7eNp2)=Ud$YiskymJ)XS+%%05 zAJ?JE$k;Y!C0=tZ$y@R93uBIfQ%_u<#~`TZM^O+l_bh(z-Yy=eiXK1qzBk<3Iz~=j zo>@q<#QOBuTelD2l3UlVEi3AHcPbIuehjiON&!C5Vm0)he)_Zm=o1_RouKrn;Vhnz zq0^=z-nCn~eG2~u0StOG76eod@&fb#XfXxd<`gUXl9nxQA0d9DATGxl;Sp-Y!XgBk zKBm1|u^Bc_gQq=t>eTpZd5=23)E9TL76_(`{G<^k9EF(HBOj$s{9IM_@@U(SN4)_$ z%1Qv{n(k{VekM;Md0dv3;E*#)UEQzn{IO#p*H6JGzXk*sB(gBia=x>Jvx>0!qq#|^ zmAgTGJ4MbnG6zu0H_b68s{d=_iE>0=nZ0B=J~R{o_NJwcP*4aVu@d4EUSshW3RCyp z>o*4e5m)okqbm`kLT&M3Uvi39{et+ym<^{09>9VHHp@6d%@XD>SWxbt`SRVaJKuy^ zY0dhM`cENe${gX=(O~4l;`y|ud0g-d2w1|J zW_z;4`4h-gc$Z~WM6i&3V1H$Y3-Ak~6M~!4I=@wLjX;Zt(TK&Un23mD&sM?Tf=&VD z$T*h34hwV3Bp;U6(rZIV79>sJy#_q*D)^&qoZ#&22G#dwTS;KrjkWczj>|m(Q>kw2r!i!-u z_2I)NxFw&)P6jc^KYkn)yceIQDH83sHIdkZh;WyOc#q{F`ecr%h##B_ zmya3U!YOF~gYy@WV4h8;CG>9sg+NxFKi^7$O4&U2w6&Ghp?@aVaeRezxS--duNej;C-oE-bIjtu zdq8KbgI|0{rb}ef(KKd2R_U7LyLYYr7?f<1;%-C^C_C1y2~X`NMJ5j@(9F~P{o56r zI0XgK?3e3=l^w+?%yWjU+(r69IpnCcM{Up^Vjy4&CK*`0MttST=;Dv2H4+m8Av3A_DdS% z_`8huFawIY3$%$K-a+It-LtbHWUQjTyTZNoSD=1mq{`-y%q^%v(iaQPd`s;|ZD+JQ_nI&I8S z5o;Re)#OTBx54FQYC=+Y6o#MUWF+gyX8S?LPS}|{Gth$ zoQ>M&;0#vEPugnuOF%&6b+@V1MW8@9P?OsusH+up6ch|A(k@@T$kZvOCNpODz>J|| zPdlz$Ny9o>6Fz*ePWR(&%1k5)6&LLOa3O;DM6A>+sHf(7ZY6M<=e^b%pc0Zfm^>_` z@yUGI6FG=a4LyrZr0aRrz%~U;l}nncth@#b2r?Fu#g6}YJ^Glj06A+D{mFSX#9+Kg zP0lcliNy=ZQnPK_y|L6V)6@nQ#409(u^}7_ zX}+fom853&my$A*8t!L!!st@`Y-DAhAjZ&?F0`=VTz~OJn5rqM5B?RU&hW{bto{`V z5Twg_jxr?_SoErr*K;9U4me&hKZDvApw4NRgqJ;}- z7KG*;$k6fAr%{D|`TEu2imfo189=P3;I^UC_aN$>vbWS95Ax_3S~ev=VWZGSfE$-E zU&HU*j+rcCkst~gGeCHB83#qu=h>WWPsPv4jYU}_ba6&jQ!{HTr$G1}9ea=Roexh6 zi$=O6MvjbxJC!LxWKxS^)Ff?f5P4fhL zVwp#$Pw5~E`3RsxJNXhQ8ba7#`w4nHHj`9Hu z%72}OOF#=r3EjQW8jZEJS#AZYY=Tj&6~z5gL9db_9EIScG{?DktfkX$lVcF?KvffZGSJ1xQ>J{ zta{j?FoJmzHDuGv%S9oJI&C{`F6GL3115q)eC@Wof^3}Hkvh0)rE88eD!q1p4c2en zpXY)sELQr@gd+racxHTR?!KDgGUhe_Al}}}PlNK%dr9d*j z6_~g&+DmFCS5)415)Q#k@b74zP#e=9v?TuK%`A_fQAYU#N>NM;PGB@g9`pCFMfk91 zU4 z9cIN_=LR^STM{}wTA#%B$B;5ag^^g9)UjDv^wBT}Dc)6mm`Ik2mX$*B@7Y|P094Em zgN=HeF{i_^j104iV$Ly|aA-9PIJ2*4t=5{jBD;xlT8mxDXdTjIP|AX9CDUVKHTPAS zaCN5{7#l@BR{!oA`5)p1@?NcbqavsObr{+4Vr7B$Ma=W`QTkNcH;HoCPIS;9MbO$M zXrTm4Wzv(|5z-Y_q?_4t;xonG0eb-aQ+Tut@EG8A9xnijn%YyebwUi0hG{G_s_zs> zQ|Dn{-(VVkRU6**J*b3+hBf_kdfsvNPs9NQxxCBtKc`#28vi7g~r99o?w)Q@5wy+2hCiCfdwPfu057D7wA* z!tw=C@?;lBaBgYgMGMl927dxN^JSaJ>1AbQN%HIpWB*IDgX@_m&e3}2n+BWBIIUvdNO_5tbK)Ol6r`_PU0f)2>uPG=(W<>=sj=&rOYAU^ z@TWyZ=XY^4#uj%B|2J`(1ZVs1{P+Kx^2<%2GyhL(6|J$mza5Ooyxg3-|LD90)}FRs zc4QT%SEQK#9}+QRU#o$A`wo>9W6h#BjA&A^m{6#ZnOtSLD{7~@>-UN>GbWnajYH-# z9)%-E(S%doE}M&%y%%hjoSZ!H`2Sa-HMnKO4dgvdDelXbDNHSkh$8vUd-o2zXM=*b zydy|#nivtGj`M#;6wzcV15cDLL}lfvQkTBz(lb^FhY)Me~SIt)yXw9BT9 z`fNZWU4MKgukU#RD_KaFaQG;>4;oYhDLv?3Z*VS(h0nQJ-!&xNgDQ zD0D)C_RV8xhl<%aU6X-LLh=ELXHeQve3onTu)zP)FKO;-I^~f|8|YIkrkR&Fo>G;? z$L&-2lT?%^u77{qFD&AJ)Pue8m986y4P!F62e;~uMjLbo0aDyp0T%izJu|BwTAoNE z$4`i~AX$}`lY9Mf6BhE36IwDdGJsAA*&IvV_dcY2TI4v?LDpZlaN;Ax${Yvzxi`Pf z^Uut*t6aR%vf*aa#uL<(DfIkCL>$n4sm^WjGgzfBR^320r~edb^}B=WQRn==t@dm# z?Cumj?<_(bU@U!nG@jx4O*=_zu~QkxqjVCb6`Fw`+wP+3XQ?LKzI`U@%{_fQfP@as zCj;$J%G1_wQh$N37UbtwsQkg&aJty!?LU>We&Ven=X*K-w}+saItF;5u8N2n=X%Zu zlf(FVr!UfM4XLc18UNqpQykJ{5m#l9;k%3_{^c2Ux=rc6Sr_X0k%BjRf{xq3fS{mPrL~qxhC9BI zFD$pBusjzOgz}9SXn9$_U%y9>1zyJc7_X*fL|CqRd6tI;!kW1HD@j=wqoQozRmMEu zwXn^nFYes8FoD@8$uL06@F#f6AXKV)EN%JPu<=Gp%eNWg4vTh#gyTyH)xb1>(c=08wK}Bk& zYgxuWlE(uBjZ!Qt7u5eglq&DyaqsB>M3Fm0JJIBOAEQ&5U}-S+^xI_v5bK)@exTjt z-ElSOAY0#6Q(vpnm$|`{aJTaq>X@;19g;1bHT5as%g9#|kM#&>-HW}n<>BnSnQmnn z(_;>=O7}o19SwJRzPGD=wSTqO{kab-fA9DG{Pq17yNS0Fx0e>}lvs3k6>X56 zryrYE=PzI-(b2}bkPw$X*I)K&nZ0i}`RgKz9L`}aQytHRGF7n(U^#Q#D`AH(bCj>YIXG< zsR2A00ngUXF8I&=5tXS(`n6S`Icg2xx#5T8$jMgDE3GF^n4l~+6fn~{^mBxTi*}HL z*w54#Yg{v3COmtTP!~AK-snPWbHlTeD|Mbyz-KipwS%q_NH8;8%Kr?#w>{_CvpF{; zn`q&`_|7b=xv14{L{wB1MNR9klHQhjXK;*l$ zF*Z6H#C^%yeOsT&W#{B988cwd1N$HOC&5)U^I|lkv*mY&bSW4rHk5Is3X+DoS2bl+ zm6Se1&!F$bWoD?KikjLc5^~j|&R))2HgCpV4x-{Uc>6i*gruc$92oDIJLXSdr&n=g zPmtlB4e!=wYHs+Z9({e@850yE7EYpA ziH(ecSdG+l)q=gs+J0Yus3t2*1b*t8*{J+#bNmy}6JMJ??+Ywy%=HYi|E6Z#tLq~y z{25`fX5&Af08T<)Vi1dWht1mME-sqWCD|G$@5y8MwOavK`f)YKfeo;@c{TyGKub%D zBkFihAf)w3pXE}MCDtv(jf@97R33A5IrIjr3l1GP0RHdl?A&+U^6QIf?CCr`A}UG( zP3(EJ00Owc6bKhmzx4bzrVIgYzozY+6@@`w@4!Tu#_AnDD#RUe<6 z?CcY}^5X}%41M*{+Gwu4=D}G<-_EsfnpE(7+M&9?3!^Ev-z$$wdc`gdIl8!eP8*Fm zpMy^{4y5cnFzW&t0Qw%Ng7K=VPE@&_r*mSVRy}am!}(?Ei4#&x&aKhgOHCp}oyP9_ zEUv+%L-ru^v;=YmltV=&FU2zj*zxmyY1eO${MPhGIjHRa8%U2#TTS{1pu2Hm*hR{I z{DaxV#$yqpBCCa%h`n+JzKWA@xY!B~RMjHcK8WbNj$Te|OahF~;+TApWhOi(_1D)p z;@Db%pagynNg$yTYI1yFVE%V22-apLHQ>&a1#^z7D{c>VS{eMqRl<+so22aejT;W@ z&wZrPvqtg(*R?*Zjo{y>a}5<`xlZJkWye`sxpk8s@fc_N42eCvtyp*aIj48>tHERW zsmBL1kL7J{?$wl(6~XU?Eyt7fNp|%qe_AXr>@=m3DWhq!90DuDcYMCGLR(wFfFO^L z|LLpH_aNGuK)IXHKNJZ}H!LeJXWj)1gefA?gq+W&%gAI$6dbF44MNJ2yYX&40}yIT zbthN-lSk*rw8S1G)z#l%r)F)9=bK=0`{_%tjd|b6K|!B49RMu{+5`aLI37M?Rb_EX z8|T>5!+Wi+>C|s*UiWEd|HxtTJN3$;o>M%UPgO`|$OT#^QIFGV%KN?ZV9{#Vmk!t5 z`X8JD#7hRogcx&6OD(*9dh}p#E8Bgl*~ZT&k>lzB15umV-gBS)d-=CdpZdzn7uO}q z*C@`%0uePK7*5TP9~qgM4(lsTlm$o|r@+s~u1zC)`yDrC&*{FKvodYTI*H7zFvF6V$1qA8+m$6>z90^1erCK!B=v?c4)&3poD8;6 zU0OPOUa81vj)d=QDBWI+9O9e+l8<;Uco6)fhdJDIORoXT)TAj#<`^1=Xp6b)&u(q? zYPRj5_nU(Ng}MIK9*aa@&@H;)UqaZ$@(+w2qyUHED1#2Sc?T$0?%p`pJ+Z@T+6wx~U5?3DGT8C#PysQ;OlJJ)&FP@mi=68HzI8{;EPEyq6E71yx1>m_=Sj)S|eO zEnc-sDQ7wu^wqzV|0sN}dUoE8FIYXB0JxLzYk@jOwB5Viq9+3Hj;-(l5X$t6kR)-u&%?}bZY1m7p84;TIS+V#|_So7b%cL!VJ9O9XZGK$)rZ_-uv%WcYg(SF38RI^$OL-8R}`*zb;9u7g$*}QOXljj!LTw z7C#ngyxc(?vE0Y=Sr}Th$GfYPz;yotc&9CXED{}(XnzkXm#M*+Z}lB{il7O}jVZvV znoJ~80`-TgH#>@j@yycFl8(nsK$akIm2jFln|Tz&+aI{zeltr;(4Sdcwa8xBKI(CFrgl28OO7Xw9DjUS0V4jiSfym!rr!n7-ymKe8!VHDh zG_}{ls5dMCY;A?&f-mcmf%>Y^0;F zT#*v%44O*M8BeqREX}Mcz*3}quR{Vs(cCs|iYVSi`j5(az_itB(#U{2PsbxWg3_VB z0GFh%01=;8+S=N7-9u6ZS5V$QDa#^dDs#xKC#+n=uM~ysS#HKQ))ibF#*ItIbm-Zt zTu)bYrabr23>mInkPX+lzt^DoDIWUz4>=4_N5tK_rF0s&f89eQFE!hy>{gIDYx;?k z2|&H~?=vPdbDsh9I9_wBudU@(%l%$V2CN_^u)kVy)J=HWChcnGkOL$j0)g#9xX|e= z=r|(5YzLkjAzYl7Zs2oVlP5mDUA$^*m4Rqxp6J3sMP7OGeW?L0-M98Lb{p{CX-<$o1xo^+eT*?|nr=@WhQv z8H39QlbxeSC74LZy}o%512G#9nOwzA-0+1~gJ<%q#dD>kTw1 z*eqyjM$8*w$H53r1f0QN_(H|GIm%d%v=Wt9w~b_=llJ~67 zFLHhWU?Oikku{}9r=1^A*=ymh0&0mJ3c^LG|N{G>yg3_ zV)rxp;QoQ)Avq+(E3E`CC;vxd`gQj6_s{yUN|1P2E?V>*@gf6g1h48z;!9=3Mb!o> zQUYxSHo(0D@-w#3uZybBbmd;~j~d6(aA z6ehsqDJrCbeROn+DLCpbvW=t>By%E`^0FsV$!>;2Huass?{)~mer4Xpe`n+PG^hF`D zTM*PmZ_YIwNDk-ZMI#DF_d4$PVg<=#GuN{!R(`Q}cK%5oX$L}l{`GKzD{zM_^6XvB zh_#5W(lRn`71`YfF7P_RbEL|`bJl{K3qWvgpyt7f86uHU$Jr}H0g2dUa2HsEMM`wc z=tG%9d%l*3brt>3XbZaUR-wBVwpB$%VfHy>$l~$7Skp*ID9hh`w;rI7@(!0iXis9@ z3-^I~>lpO~Gq{F(g_k7OkWgNLUG*Bzk*uJ>=51`R?}Yv3-jV*N@d2oY=1v^U)}y9= z+BAWdsuy2y$^YlY7k~$?JYCfD2p!n2l&zFNs3ZB44wf6m|BJFWkIS+D*1yk4g-S9u z%2>#pqR5pgQ^^=5LR1t=3Q@_BDN@Fg3JEDx5}`X8id!Nf850c>Whz5G?@zvaKl|CQ z=l93&`s3dBzQ0_uB~A z^V*0eQg#dRea@w%48zL0pr<68%XRYNo0rd?3a3-IuAV+9=q3L2X{A6Gw9=oLb8X$F z0|#t`K<2(64FkT>E`z!yen&r7I~*+RBAsRbLMXs_@+`c7ArlWh8Xoo)&^ zGI7b%vl|YoSZ)<11VX=DD~+tzGf~&oHfG)aXAs``E-484FP;eN;a!S@3VZ{|%&BR$ zQOv-?MCF|O^W+VDP=}H8d{?i=k-6XOq~mlJfkV`4u5Qm4hD;WQ4Cw5UhKU;$?MP%X zNQN!nGp?k(yyu@@z^V4zM&sD!mOg#zWtJnb+%no?qh0>^h3RcNbTFaCK>e+|`vCim zX1BlNl%6nU=FIo6U-xItXw9kLzsCf4688!8&p_}*q#ZkUv}x03w~pESM#MFGm66L9 zY{nL&rzdZ)x6!Czwm|&!okP0vI92AywJPXoheO5f!a_loBvtfh>jUOAnJ!_wPodzU z5rX7_jmNy`g{0p1j>TesiBPRAJ=H>LvB5enY*d@c;YO!271o)*0(j6#!rvHWa*k8# z%8@S08#9M6Keb_l1>;r(;ofJ@%1LEIxDo-wx>%kmWOb+3x5@3}$?6}kIJlxp1CdSb zDD53ZcopNG^d9N2=aq{cnhX;W{jltwg%?+amyS;G*diqI=g(L3pzqkeose%S>YrvD z5t%VAT=*H4-7=nIPqj3kT^Oe<6H? zPg*O=Tc~k|$Tq0H)>>F(0v^k~p69R;sqMN6O|liD?$5(*ovO($KL<&B6@rz7gnLc{ zae1=y?k%RGWHKo%ht%r5a^=#`UFPcUHbI)er6a2uN9c6ySWC@$BHevKJJ0?x^h)pw z@SNT`r{yyHbA1B!=(r9fl+r8;)FV&8(J(Uu|GLQ0@%5)q{_pOz%PCHt9ih~!Ronv? z6f|(#O$W2TAS3f59k;?(J-c>Yi17pml8cK+1Q)ctyc{Ec{tKg)K=XCx^R8^;RD;|Z zk=dE`#%0M8Y_iq^!Hauu>YmOZ>hBV*W}n9cCkU}(9eq8cEYL`z?5`O8+5g0(L!K3n zdTx-($SBeny(ZFZrf=9KlN=u}H1{Q*SRhiOyRKcaYL!a!=4*b}dH`IxG~>o8S*QuP z=M({6f~-W7YVtUTM{c&^{zHcf(RvU*xc;m%L0kv7<(=u8l2`~SucvhV$rB&c6+GVWzSI#L^pzIi zBv(9x_I&fer<~#W^E>ap-F0$mNYjCXTc54(YqS{Jz_9(q@cmM#C^`_kQ|p0GrV3l* zXgT7Z(|eE!hY-vUOu~ce?CI0$gX6ptD6IVCEudbu9^kZp`0&AGN-cfxU3y!&8ru)7 zW2-I2Rg(9gn!Ej)2hy9_yP6rvUEEck|JCCoCyWoDp?St3cJ}#e@K=N}4t~gFnx%Ad z3<{&U*~QN;G)wQgH_hG&ekQW=7(je8?Bwp~ zn0H52U~)D2HQ~Fw__<6D&=@pIy#q5cMr~8c$8a8dG#~rj7@@U}%fC78oSHmlhzM7{ zqg}hTc$-^huY}nHx^y|pP2kU#OzYGsN~kEX7RR9d%Jd;5^sKC7`wGo3Ih6bE*VYN2 zWt|HBZXz+ojMm z3IBf2`Dh&PoHNC~-0L#E8?N?h?$P#=O*kHQ2TxG>rvnKJOQA5brH66NT)LvHu>)a} zj$T29voBa-3x4f^gnd#egDL%`1lVM74#YR@7p)N zFY400ZdnV?cT^rrljcwF0b7(c9y(rpcxL3W2X?6obP!2nGoR!g`@n@@LI(@$c-2;| zkOI@Uz(LBTy2o)6qiGDWDsl>H`d zBhAB~pPnW-G3^kk?DPQgimtr_71wZQ7;J8m*M*iC8Vj2WwF z1(uAvQas+=?p*Vp7g2t6U;n>)9?eE<>DX*@8x0K{M4#e~Qr+-P&z=fOleMLF52!i1 zfo^Mxxw)+9L-volZW7y~Zfn=0`$OQj(j$U`Q&Us3N4IW9k8@~S(O+hWkdk8B%$Z^z z0Gs?w52!7FmV!${6>7unyLR=ZacF2bjP@(W1n1m~jB_rzho?^3xKfAC0hKk?9Q5No zwA)JZNDI5%?xJ9$2b2S0v!afhTSulvJ-^~jfE#7&9k66U!8g*{2^hF zBHi}N@37tzx;P(ex4ZKs3~&VZL7K{`bKrUOFU#wk<2o$-f?2WeqMNM$(|W!ve>fX4 zo|M;dph73&X5xhpVNQ3ZyjyP@JxrIwE(|Fs=bk=Y&1508y}lNGr$^gN(81lR?-HCj zBr9*(OCBZH{2rce?x0^9Kde1FJ*ww!dR5>u9i2!#&IL)5-7H*b#NH+;-5I|^u3FQ? zuJ$;9lflwWci*Mf-Z-(q{bC>OAuGg|Ig-zG#SN+@yc+{f7=+#+>_MlW+;waiSsolL z0q6HD=;!4H9(t@(2m5WUmZfGyLXTc_4@Ew6bR^W4dyJG=-?pyF38X-N=kUn zMhiai=I1dgQpvc;`LTQb(xp4c>mA=7r-DG{K-1$5!o zR~K()2eznoWPAR;(f4C#&x$p^5Hj;W$IQg-A=R08 zk9u05?L0p)Cd|MUrRL2w8ma%|- zC)xq1#kdn3&*}Q1^9a$y_#K=ib|-^~h7kTaN66R3&lEVzf0;_s&Z{k!XE^`dlfVk7_wdca`+3;(4BxJF@27iL=II6s(T zT3j3)F}g>|e1e-R)B4X(&wlu@2C1#s&2eqLS;6d*aHb0z2UlleuqeR(GqsHs{5FOc zS*ynDxasCAU%q~2H>5%E*kghX()zn#!Dbvp(^1nvZHqT8qzQtNktBm7AX2t|(#aCP zppnG>{1(8w30r`>-V5rY&qo~NjS z>cY$pzY@~_u8Pqg@3ESTD}u;`gefky%h2bIvzT|zrlyM76=bxa#8xd^vOH}clw?87 zvvw>~NG_ZnM?2y_^RQFsCuy_w1EK<2igzM)Hny}}8@o9HyJA_NMU{-OaFDUFi0#ri zjFb4Gyt2*yZfWOVYyN)Oe;Ry<^)N|OB9BIb&?$A>vUgiYUXt}{tlcH0Kt0xv;T6U- zkyuX0{MG5VDMRTFg1UXzt`(PZ^WQTQpr;f#_82R5s#pw&`hf44&Z4q{ZO`_z0`_nH z%IJd4wI%_-|Av{*4G+q{@=PHR4%Qy(2}t9l5GSSwEg9uhXE%Mi5fqI$R0K++W^a|7 z8uZ^Fv!VP)l*m74&b(Fn1;cvgQlT;&GODrYU`Tp=YUhUT2NV?*Y1(D@{Q@MDgiqy; zG>tT8NQnO3qvigvph(mlfE8CZlc_JBMFJ9==^ z2`Z{bpJ+PTUxH&MKam6(NdR%4+LgbBZ^XR)5N+#8lXeO=hXbK`E1Io)M(hiPvE`-N zv7=FKq${mbvg4VIbzFIs$BD`c8}*b_gN_lUXHrkmf>dbwwX#qw242;ErmZci5aXwF z3B;pD9ZOB!zpj`1Hd+?V1IuW0Z2Ty8UiNjc^A}6^Nc;Q?v=KwJr%4?#F_{a_b6&g} zUyyfyyDccyd0HcP#ZQ!?H$!KOJs1##_$%zIFm%8B@}-#UqB+-GRdv_)?eN=@EQ{Wy zO#HcGC@nCIQ%*Ro$i8AbDN*yoMKJ|HQwxm`)Y$o1z>d&%r$(Y#>D8|v0J7wuK}n$_ zv>P*RKZ8a6!{ukZj8Wz}a}3+IVEhzmF~Jv?*XbF577{lsud_lc$(C z57@0ncgbmoma46*!%p+`$_;K_V5IW{FIaFewFfV0yZd(U+R5~U;6Ac_*MG22auTu_ z6&0167ZOqEz&rxJ+3x6+(niV~Dadb97+q+3?8i=>8ZQE`svNi`1$Dk2u@hb#-XXHW zFxrukT^xa(O&HWJbfwJsWv%(>-kKD}{P_=_{=Q1Ha5efuri+5I_Wbri972Wu@vw%>Vz!+fCh5+cac0W5 z1_!^u^Jsz9^zY+o-%wZaZ8T9c-?5dVwON^zvJF!j!+F=PAjqXt;{jcTI!6e>GHq#l z-bX6tv}h3nQP9fo;7pyJku-2)jEAHzWT?wRjm;HVZ#rsdoH~6v)qSnB8AHMxOpK_W z_##sy$9#u4d0$>Gq$fbRy=Q4*Q`)!#ifj@|ofe!a9$1yfv788eM>p5@Tr;9iP!{XC z(kLpFi<=k>VZTCRqV=lnscayaM&d_f&mGrz*ITk_CK!Q9?(Tget>a9a$?lfJXMBGM zJpfn8s9+kJhP(H2b6N69w}&YRmDEp9&PHKfKVZQ80#be7@OXZ>V~Q zM0pEgjbLhuX2=(Mi@Bs5`#MOn=6vTQR@=O;s8~yAq%@im*(}=1W$DtQqN0gIk5r(# z&2AaGX4qU-k?@g_|Dg3=y^tG$F|MYjdQeHn{s6?*O2i2Y)P|b;UITtMxH7(Bly%;k8HnVUqnRF>+&<) zAo><)sWgLo#Q!VhrNDg5x$LzKAd}}oRu0(!1tDfnUtYDhv;=TVHSybecIjlkZk)Mj zQ^2U+zpo#c;(l&L!s*i$IQRf$^UEt6SkHw14DgvcV{O1<#wEpEZB*1NW&uv+!Z#}u zhO++w z6JPY}N5~ci7?>r=*?!>eJA_;6^7EH05n18qM@HFz?m5kud))7n(s5(vQ%)cfD&jn2 zH0vPINw2F6pmQ;dh`xX^;H+uW#KtHAKQwPGI8Vsva|w0z_S`8MiHY8gCN$U;GzFQ<>bUhTeSCa=@UTpw#qYEw(^2&E>GTzv z4;@;E!47$^OB4++Y-7bJAV-$n!1opecHzmO`27XbK*hfIlsAQn1KF1&PS*vrW<`Xx z@dTjReV{WJN3~?*irS#DS2?0<*RBX6_$M#uB))g%cb>xAZ*l1|8nS($L2te%W^c5k zrs|Bo1;ty#9S4P>qh#c17xJIze;X^9>;MW9m%TRdd*;OD9FcN4{uZ*HuK449};aRx6c%X#y;sNi&GkCjHUL_ z_nJh+r-9cIhcK#29wfH;*-V`pdid}#hy;Zq%Q0hg7_LRq#|`+X-A;yy{=Fi|JzDhC z6XGGf(;V{c3T6Ii9QY<(rnSv;iq`Vs*(Cfe_l$eQkk27`mtX|CaNvy9og5r#lxjw0 z2xPzlm$(R?3f-6RwTR*#XJC%>GOLl&c&;-8lIQ2`5($&y8-zvlppP@u(6Ewf8p$bm zV4+#*rd00yMw~4SZqE}l=c4a}SuHv^*-#OETGHev9JgxTJjQffGBjaIQtlZx3I3-I zWTx47P8!-#Fkc+2SDl>?96HqVn%Q})i!Jjs!58np)KNJ8{CM#d)7E58;ju|EOsz0Y zN5x5xo*!|I?h_M|F{V3|iQPLYl%(hs5t}xn-lLJpQqE~Qn--v!Zy9R$w8QgM zM|*h-!tElOjpSi@3#HB10b4%h567VM#6bYM6$Mo2P3L4|3)ORNTer4QlOJj`4J`9v zxvghN99RG%j&FimNkwB|H^fzbV&yg9!}0tqdPCeDYy$VKyMTo-H3yF3d?zFfl#Y}P zq5qDWFnRLwqhb56hULvC8N9f*-WU6EA|!MTB6J-8k>`pNaB`P|71k8c0##Vuuw&wy z^L<@Wn`L)zEv4jQ@l2(mYrf9llCX*t?U4e$97UH~O*K!Swb~7|3A5tvVWOa{tSorE z4<(d^hV8M2uY@tBqD7YgaOSe{dy0)Yy3p4&QBV)?KT@$VyzwM2Z}sQb8`7+(yjW)z zeqlQ;7TQsf!ycZQ)#XKyT=62@Dk-40^n<`jUQSOmziL0y<1tKs! z>1y;$(7+$MF~xvcOOm*X-9Xoz{K>{my8|q~Fj>p80ya;FF5J1{aC}071524vKKc2X z7tD6*F>KhJ-w#xzJRt|Qn`qFMabJ=uc)x_G%t7gLYUvZAHQ^8%f6rvCcn6{=l8^`k zTS#Y1wHsWrn-k5~)kY^K_MM$BEIZ{bum_%Sg{HM7;F}eVSB_hs*T2_`YLiR!isa1K z=BH-IK}di;_qyeQSF1Zjx3AoFa?i61x!z+MRBV2O^{PXKUx54rv~{KqW5$l%eqa(h z3_T@GY0N1*dReGpx7ga``)y8X7%ke=*u{W$&Y`tWoP;bscJ% zOXW1#oMq>GPIUUOo>X?x=$in{ZbeK6(28bfQurnsUTX5yTrGZk6@9hyDDgNfprif@alp{>uJ*?Ei4%9nKp9r$bbWhH*CPwdRZP1xgiMlU(`9rmLK^)>)CO|xuWeoy8#*_m&($H%4 z`Aq^IH!5npOs_&36Pc(3_MG^G2Bu<;(v;Hh`M$osu0N;H-z_Mh7i`$Oci5b{RLq52 zuxakNlc?~sgQGI52Qrt%&fW|faJy+r_+{^mlgjw^U;h8wR?qvMijJ0j%{Z1EIwQWM zUlS{GgY!Vd>ifWmoKZ7I=zrwI(c}dAv zu0Ba`Z%}ynGakCg!-t_88G9Nh(Aaby-@P07jn%LAWgTby!1N1UEYZe#q+#g88`9(> zq^fxLzSj%!4S=F#A^DsqJJrpe;*(>C2U~G>sEyy-kDWek%J`qB<4d~j?LY0>Thtx* z5T0=!O1jE~MQXLZH9^e-^{&+xw+z&~v7vdOT8qszc4+`SoP4Wm{0sToZZNX6af(3; z)LvMk1zYL1+?6x#Bm1h}9VT#h!^e*OL-->UxjIlX*;J`(?nvJ72dHm$BMckAeC%+#reE7S> zL9#CbE&?j18wd6d4{J`xn|_-=bC%{&Y(X35`tUfglIF@DTh)n$<3ISciJo2NwU$gY zWBT-pjOi3Wty@31qmdBP4F9oDup`hUEYCZgoN}1?2I5HpD;;W5mFQe7_3WmP1iZX7 zyE6f&pw3^+mlWI5@($N^p7lQlZ1#kmnM1RgrID3O&?Gr&d0w7Ei-_B69DxW`r%jtt z*HU1La}p@2YQo|}z>T~MoFbbiQ4aK;Tc6Xe`<~Ec7$ArJ*M=tuB}@mh8HZ_W+4aOtv&SmJI|uJISUpRt@8HfVX^(XVI74$ih!K> zG&v_snMAi6#vD`!B%X_|h0p^D`SA4`yO0Fx7)akx>o~ggjI$&PwZF@9v2gliht{nJ z;1&8kLuvo%k4+5vG8E0VAq)Sajs^w%p`XV>_f(M9>6NRLs8m^7ghoW8D*Hl}nE zPM@_S(2E^=;38%}KEG)iJQiZU9gVA!NG5!)UGv-6ui#KxOQ(_Kc<6h=8Ca$tDP5SL zwHU6@PD;^&w2&H{K^~!_nw()nBnrmuy)Xl~x0q5?xK{{O|Bwj;vSx~k`Q;O8`YdA# z3FP*Ve>ds#_z5+q=tN7No3q~hh5@6R<9|ArlqBpN0cHg;GF|Mh|A5`8CT^q@NO?x$ z6D2)gkR%{F1EdD3W>yhHs(keC?6C*aPt{Q=naFd2vQWfqNXU~43FAAr8 zGWY0V!WFQh6hmeko#UHX&FT#SD!8s$=K>%>^!_@mNH;mcusLH^wYW+1#T0QJJ$JfC z`zs9lv{h64jK2=PS-&`O;?w>!xg%VN24i?ec?~ovK3cp#=7Ct{Fr3yppTy3v#OSQ= z*5O7%CQPSVNCW@tuNrf*&^tn{umS>zO+Y1oPt%#u8SObrsJUq?GVk9%o077(Vo09> z?0sjT7X?GWu)nROwwwsa6pr;|w%jugfwB=#4Q_wr-n~fjZc;$B(bD<_A#br!ffY7W znahwZZY-z+t{{>WrbXzRF}!iGvpXFdJ7!gv&5xi)P`fat#r=z()N#7F?NV(g+Laup zP9yw=@zfnmNcc#Tu*~altR%hLFj=TqY`V2DZ87E7wD9YLiSa09;5%f)yVu!ZYa5`t z0PY!X6Xt7V>mm!pFgF1lJW1kmY;SBBNn?QFn1l)XnEhOoPb{priBx*_n3m5oHdf$V z2f<|n(|&UF-#H(fk;H<77*!y=EU<`vi)NprhJl72HRrR@jOe06=Kg#GzcSH|Fg1_y zj|!U>uSrB?6LuONGadJZ5zyM&TAE{sU3|fz;_+W1h?+KJ4C=jT(`H^?kPM(b{#B;c zt7p%l++D~_wga&C?R_(MJM`Y91<;Q$@CJULertrg#;V4 za7VNXNV3xvr%24qQvUEjJ$Uc{3Lx6l`pf6l!>~Uzjr9BV-JK_fx<=-k)o2VH{dD?_ z8B@|Gz|$}gMGcvomGu_Y*Yw-2y2cUfY1ET-0m|Yk83cS0PhfNksDzda6D=oIyUe5W z&#o3MRGBEahyjI-PTU&KeMe;lRJ8$3{+hDh*>=Aaap&V_&wiq>K{IgN6u)y@+Yvky zJ2eL30MwS@^Hqo6)56W0|F@fO&_{RoMY1e-fbY2eTY)Vivea|uqD}W|3uO!FlL7~k zwDegx>4|=4b=_}J2TyOEV(?C>H)H)k8MGFi;vi+yll58o>$t!7TPg!hGq1P5>4J}boifdSt ztL<1afIk~NY9eV~OkctuxQodV#Tw!^0c$ZWbtfmFfM@4OYvImx&ilgo^M}x7(p(q%4TsgX8_k%2yYE!_2IRGGMSupr)xoM&B?d=f`Zp=IgU?EzcvR0HPh`H0*(DM)>(96Qw z2p}6+G5QhEvWSmGS|g;LV@|yp(`AZ_OBoa$?Zw84Lv1N!0710HnG+`#vn-l+6;D0s zP?)K#TBQR*Whm?5q#gz3&t@WoBiS`JXHPI`2kG-cEZ@A(QF8a)o zY>X)S_(e{rXwfM&^*PtDTX@@n;!x}|2Eji8F9axg3s8a|eCYJJrcyH-2M0O=6IQk8MN@s3 z#-R78fK*g>@OT~JU1gT=^|v`QDJe!IYZOdBRd88sr=_(N4|N1gy!aPhPEL>Nr^rbq zmc&bP0kt?T&Fy5ZLF_b@zCn0Y=uN1CJ)?ZW%Wph&@5G5+gBCo_&o{$bCnIA-K{?ug zF+((LjVH%bbjnD^Wiq}-_F(fUL{uj*b{BA;6M4eaT5+nqeFQ&=xELBb5LYxN4}ZEd zKR*@YKL%}=FK4jTSv4rXeyCjZAA*+~11&sW93}DC?cd*C2!u@Kxsf%!yL2h$L0}CE z8vjVP`Y}RRu}LvC2J7ZWIi0r=t|*#pBdB67No#Wd(3n))x{tD$;ALz7?t z6h4Kjv})y?T*#>zGOA^?HWppb#)QfE6DJsGgBPaLRz5}NbmZ~4xZZiYz0fld93Y)_ z)|K=@bHZh>B2qoaAZyg9w;VWtF`4hs03#y;KH^1gBTsQ|?ylcH2wt`itdo}>+1lA% z=F`#1qZ$UrlRbvBsNpo1Tb#5J{t!vWh3h566KgyoNBGZWc8PK>ZzM6l#F|MNb{74h z$xrM2baH?pE7mzTl%~9RlRXU#u5^Z#6{BlR?z+)c?w6~7SHGFH-WM#;d5i=_i$q{TV8l+TfUl&JTZ zIW{mEa)A@&i&@b+X3RUnkJGk^s_RJ!FsedJ_46b9JfR_Zl-@Ob=n*iI+MxjO#th5M zGqdJ9J6o>O)_{V6PGpu<{LA+3p$&VQQCYB+3UE2XDLaIEM}Wt0MFczn1A&f2$Sbuy zVOqG?U{KzC<42&lG2VxPdN6R9c3Po7*|`%P3MDof^;u6Nnc@lpIxw8ec^-8*a?sfq zmpO1dPpNjF2)1A*%@5jY)vCc9qk-`IeD2W|&1}SI=t*09`yKJ77O7sVRxzGH=sj>t zbBLK4n8XO(7DJUZwRydJN4aP6xSr@q{KM!D-6vPvDPf}dKmTS)R?t9^0~g(4nQ~5x z$2mE!jI3Y@3#EG0H2hySV;DRhr_zY@nn)=Eux%$@FQY_3II65r@4x9gx`J|aK9!Y* zi+uNtJI+u51e5OW+wVMafX;q*Y-})`KLdIc1w)4(I8pKmC)pWjduG;~s;XVr+m+1WnlV+<)z1#u>wyEjHz8TetrJzW zy81|SxChi&93^d4XO2DHDc9XMq?#b>IEp#S;i*&lo<)$-yY{lOES5rN#ii$*sXY~X z(me>BC5>t3@vMzFN{VTWGs{W3IlaReUR-$Kb`(DsAW#yU?PhGh&dQU!gRHq|iHUSl zcI<)aqe_c6uNbycNbE-%jcb>w~* zCGtqfdl*WXEjoevW=c{WyK}@a@~6HieoQV-_FTQX7IZLFTlI%M8571Bl??L}IGXz` zn;DRs>DZq!OUq!*ZxoC}O-->IOlq?#8>EX+><9xswV&t9z1kg;^@e4PsqmrX&RJ2d z^s>cA-(sv8BMM91S*`x*(C6^*bb=QHDy6(c$G-$iZO;by)sn(`PB2f%18g>^goQmt z9%SMZCT2rI%2H~j5sPWTas5#iH-Pwg6pfcX7lx%Ytdmw=$cF$~{yc2}tU2;z@BodC zq&9nRWc;^m1M)^?A+;grA|fq9WwLDbK?^bnQ{4ytzXZ| zf~KY~ed&oPq?bz{PNXWV6ugv*kkR@!nPIz%KkjHNV*N`v8L&T`E zU9Hh}FKQ6DN9HZK;fVd0FY90`>2-*mf4!aL70Z{06QQ3|n||Mi(+FDPqjFj(1m3!R z+nLgm31BD1@+nLh;*U!S59 zViN(O&xfmJ^RBEO;QCOIl?cSi=6iJFy5opUP(euWDk?(o+djtFnL-rc)(qZZ>PX79=m#oL53 zmq&sQ$@WS^>GRcPzX#y8E7NBphL>AyDwT)*4Y8DDSP@%wkFxlHY{ZK@jXX0bTAKGY z;QBs2B}`Yaw-Z{ye_GlG>XE*G|M^2OInSio;U4)*k;-^s>N5?)(?7MgcI3+{>YVD z8FJD#`L%aFC6^ft%Q1G>a$*cEF8Afj`vKs)cVFc04(FJ^uiCVWscHDg7_@MEbmXH~ zkuA8+FEwy}_pQ9moFgHg!qqJ?_U4Rw$T~3w1ta6r7hd8O}r?gR|Rlj0qMmKEWQ9 zu&`;!9SKTfyp76?>C>>X1F1tTmAdFDg4(g0f>m%TZ6cM8p7p6koQe`(i91<^Mq zm9b|`GrW1rmiG@0x{c?Ap`jb)gM)f=#v8>D-!;nM{R+PbS| zBD0yync(AH<2KLU{_f&r`}$8hYxNPRL9`(DVtRe`nl*5NLS0>HGvYPTi-<#_5V6#& z)#|DivR#zYh-{B{9iH-YMbacw ziC6QqCvB9zdh_^ZPnkRS+Nuwafu6ZqU3&K1rnmJ|YAs4zeWfn!X|PNW$MBVLq2RGv zgO^Olc$T-_48StL&Gxw%DiM=WP^|kV%$PbA3-f2Bic1%%XUE0Zz#L$?nRa%`svaxG zjoXTgU8J?XXdpl>76ASbx#2SM^l2^K*jM+yu>>0R*Hw-Lk1#AS0V67E9szFUC`CoZ zY4w&stMTKT$F@d8R+Xk=i<9YtC87IYEp9oYQvlCY$>8HoW{4bvCiy1I`qa-_HkiY>j1@)R%azKh!ot9K&>u608R9k-V~Zp0K0eWSen>CGJ=Fa z)dOWiwuJS~rE5nahjB=Z6$5g-l9Fmr)2F%9p(SinENeX*H(`P>?R%#~cZGmUKX>~t zcNZ4}3pH+}a64iqmdBeei66!QTfRduQb#ipz@tfpfW!duuDen(BtoB7!|Cb<>Ya{{ zzx0Y$b&KZB8Mr#{5rHKNfcoNq@hcZzcvqT#m_P=LmikP2?Kb`x@q@?qUZmL%8U;?b zH!RcZ+qa6u>=fscgKV1*A?+X}%*lOthL&3WpI^Wn|6kvOiQHE^023cFcQ=#YZfpAk z#(>nztc*_Fm!-BM0$q zUjC3@Mv4elT;lr?aiK&>84anp&VSqhJt#R zkJK~c)ovgj($pMnX}Q$Z6}|F*|5!{=J|9Eao9oyg@o0`}lP2@2>>KAQnesYNX?PFS zdL*?ek2p=3Yxbt{50MR(ak2oA!jq1)$_=Y4%Af$UP@{<% z+|M)P-OFOa+)rA*Xj;NQlx8&4cjc!&oCd{-(&m;sTG`E;HfctgFk8F8$>}TfwD;lX z3<+V#8gt^rtv`=5GvDB$7_23_tJkh^t$#dUzA-RRDexRPIOXRm`U#QA(ZPV{ZKT@T^x+tz>t_WHWs7m@zlG2g{$H>uhLv=8{8nw8;h^*{c)t{9X;fiUrm9^0;zEk$Szz5LoR}dUHbaKyFrI0CB2otjujE)I3-|dDLzTD;lSg0Te+SUHveI2&|Mtf<5Y*52s~lt0*2HIKUaLZu4>3i?)7LzZ z*vz(;DOrIpXNDOCWe^EwA}t+0VnhS}89bUKSlDHL1Z{1%S{sG6TEtLNKizl+uSL2; z;seE^XZGj^`WK|9q@~G88&hs%XB#j)(5u%S-+kWc_C8XBHJ?zzO-P-k_eNjgf1q-#dEn@q0rNIkO zHA!?paY39Pe7IyH>3e`^LlhovYJlH@8lkRGpGpX%WfsE4R3;nb-*dTsa- z>ar$UU4+2x1CnCX=BJcVJ$y(K}{SHi<{}~>9?Bkym zrNy4ksw*im{Vq-u>PJjH@jQCF19$)oQ1MS^`uXx@)2b&N)*s)#y&I~%F*C2AK*z|% zX4HFnJlLwjAhJK%qjwh4fm{6!HELF%V}kps;(SFtIKl|aPKbE~e>wN>i&eVp`rrxI zT6UrP-=NhrZ+zQ!!jZn}H~D9G@!Ppp+N%0}`&xqoQSUKs#U@>5qWDbqM$UU{oKQ9BH^N&?leetj z!ov-wMDa_TJ@{uEUd+Ryilp2N)Z;_bRMLJaW7x4}^JZcPB+2FUbf6U%gZ*PX#@v3hg5QqqEJH#FC>RmTTp5kidY~TFXtS&Jug%LvrV*pNL~JZ39t@}vbbR12N{+O;0wm;%xI;e858fi7H8dGKSn$hcO0rh)p^{z9 z(WB}b8ndTP9Z0E6Dkki&U;l>(j>{OTqat0NZe9d z^%*;M3mZrwr_f8}+)MU+cZd7c$tDKY9bC%)$bLaXO_EZYgL&S_^WiX+SKTm%= zRHxJzlt%QQ-b*->Q3X5QmMc5-`MGF{`$?NeY7F%(5i}5oI3(mX4!eW`Z-X+!CR5YY z<=>Q*b);g_)P&zPPfGqAee&e$zxD}`jKOGW;>$r)kVlU?xo~X=U?GTox>if-Bof!_~ll z3}cOERE@^4j6Vv55HbUT9cvK14PLkE+&Kp5oq?em6yu;lUw|V{`b`3kDEkf;t%t;i zn)Vtpd-4<&pjn+z7-=PO4IAoCH;p@7nbk z#E_T~z%q*U->jIU2s$|C`3q`1^t@Z7XDf1WE|=Ryw9) z14rbPOnkoT;Azl5Fci}BmDwjHkR$L9x3WnQ#zfrxl|!T#?dr|Bq>{y>xXFT$O`0aX z0;vkWaM2pUcM*JL>RSI9Fpi+W1nMxi>`GTC9iHMZKfj5d*i>UDg)%vVA)T~k(a7GA z_fI)BJ!xY=v?EULp`nd*mYkX%o?P9Ot``){U|OfrFi)4P!(_cvoL(k12>_E6BkZ)r zZ~nz-o!%^A)EOCR<=sNfkTL>ZxU*Ta07(p7p$(xgLAM|Sy*SALkKYiT7SUG1+6=@2 zP&_l|QYh8T3+2D3*$>4ik0m@Cod8t2g7ml_1&K1jyKh-YcJ?@qfNn;19Y31~o!vnM zZJCJ}UXrBC=lPl-YIf~}-doJ#lMo%nXNZOt(Ocv83f7ojfq(B=J;GQHdi}PlT=a); zpLa8rE=4;tbf&A8eD3SwtW;_#EGRz<~4c5bUnuMMd>F-u@ciM-ZK7 z5)(=60kSOfEWTWxJ^gP;#ye2U_<#JZZ@*K^;tsOxcFRJy@BAk&^^e`n9(VjNEx?;B z#a{dF{BZmjcJQDJp=ZGYPr7K8LCSf;P?I-JC$ApZzyI{flOZDhWWAv@9A{_8ZlRTs zh96h2nLGDL^q-b$ojNVYgbvynmkD?VB?JDtOig}@g9AIT^g4IWTzM>)pNp9U_v}+5 zHb25q)b$so$`${Hxq$0&VLQ{Hy zr8_=PK&@-Ocf#tL6=%z$f^`^lXKl4rQP7qx*VcR)OLfHyUISEuBMaw-8fWy{^M0~AS&ZNjJ^?MNDcSs^8)Lceap@khDrA`Drp5+#7^aD z;tkTx`$%4vkf9>`16e^IYaw_9l@_UF`h+GGxft{0{tntQM^Q*NhmLh=T(s)w>}&#K zH`hMqe4$(C&bqf}?nXR84KHtD!16ug;*yeX{rbInH$X1Bh|o{}8Vok>tXXX#Ju1uTsN^4Q>@j zL-{w5#4at9Dj<&4MSXecq4;{%WEfZ-r$vi$R3DC_1$Sg(1d=V9fR|di5isWlI>=R#y)k^+J+Y*`2dW z(hJ}tn2lZNAnBRu}i~;0twm8DOPS_e1bDSzAr#@`n^WO-?W$3Ca`x07arWR z2KVc?n{=nn#zSNQRQCK>Qs93YJ@*DbR@af@8>1Mj1;w9WAV3OdOXJASvK5Fo-1xniIhryoxO-T|2}^uQc?_6HUkGJIe<5FC-nDhK z<|B_B(e+VDNK9PDH9Q~JU3L-88;P_Sr5wAB_wV0QI3Q00{ZJ?5yj&wfcxYs^0BxQ@ z`6mPmX7Q7#kf;?$z_e99zcdKb5u|dwTUVu#+<7Wg02+|XDXwY2_qQvC(6`{;IVo$L z&vZlqJAC+6QZH4bkQOi~cQKr)peo9-@M{%d7)W7051i$ot0Aq%a=z+S#)!h6(d}?r zM>2+!RzA^kO`<(Vv&32{&c>ioem0Z>n=Z`o#c`vSi$EWk4Mk3a6m!%{Jy{Rj=1w>d zC8#4_=9FL+g4XoJSyNg-K|zWZYJ!pI)r%~{KM9*07|e4kj)L{G!DozPF~yC8#$YPg zq_?t(aEqvBe{;S;y5fQ09;sZUheGdl-TL+8{rlTbnX)G`(%sQfwoEx7F=+Bq@LL-g zM8XWuX|>^;=7F`$LW6i&NdG~m#` zx$6|AtCV2YNYsMI;@CyC4S4Ei*4pSU!cwq3a%gl8n*AOBfj2=VdFdh#9pjDqlMbpe zT)1qUX(VEZwfg^RD5$kPet&e}5erHL>=E6UM;$rhM}bTNthzNt-hzd$coLBCp^$v6 z#|F&BX_=Q%Sf?>8bL1m~{N7B1 zkUrot#jH)^3#9;hWO9s`yLZ5{E%uq_4E9k~fh2h7jRz0zZr1km*x0GI33l(8F5&`h z*+G7zGuftHyU4*yAERMs&Sdg}kpXj+81|KIMSrHOJm;LF9K6R5b#guI)`JJGmkuQ) z;DP|!%l-G?by%#EpqV+rNda*gQ(cWvhlh>h^R%5tz*5KtzJ@PNZ214|+$IUwd~?l$ z&*ErB8m6my>GI{rPo8}F-JmOZsgv`Y%tv_1yAnhM?C(tx@Z-heLHuA)+P&dkFQ7>2 z6EqLB;f}&R6U9i{wzh~%kiO2f?WUkM&R|Y@zZRQo5XOPyRF^LxVPxfK`Jk*d zd>6`XIu&`B1Od&yg~r&ZJ6wF6FZhKf{rf5af*a`qY93&2X7-D8jvyE|(8$E3bawjO z%U-9hTsdskZZ>ev!Xjo+a#N+lKYy+jQio}>-eeVFzrtv}g#m}0skvv!=BWC`+(>Sd zylvu>KLP70nFkLV1WhD05F2=D|sRx|2MkL(4+5Ykw*5$uJ5DIEXrs2Cfd3+RVPhkx?;Nlo%j8DQj5sVrT^QtCd) zn7XmtYi-t;MrM&sU-Q&A%9Zn%sJRwl?$gmoU!T&AD7 z(pCzES9E`I`GNE3~aebkNX>wjNLOx&fXL|MYSI64x@0yq?5k*Wbq=4n##P$Pe` zUc%Iey}d8hKNX*Xyga?_J?-97!IP+1Z~i=KQOnsHl4{lMiWWBW(SG zRk`K<*NG}9D2Ueb!Gp8-J#={bu^@NQ%BKUCF8zRG zlfQoh=t)CM>#h74HF;%)uKoL;bN+q*iau=6Q!XDGL{^vK&nP-J;=5W}!bU~wgOc-t zVR=2+G(%3p2g|TmFH?>k;Q>gjtF8S!uLm-4@&nC&G9%C)HyuhKcB$7B%EnL1?FW;^ zQ^p+viyRqG$DRyA-;4ebq$l+{ygRAjJH82p^;9`zb`C7Z)avxO{?;j+@wxajE5ob@o@jyjXQO#cM6n4++8KI)q;f$v)7jX62Jg5h>r zs+nJ^cs!_wofa(Ug7`T;9wl`NAP|bMl$8q})@3R<9|Ot6{{$i=TmwRh8TAL4Jmm?+ zaEw3zkk(Kh+vq8F4TsGivVM|_A}8v_Q=ToNf#y7ltgV|iji#u}G8_o9Bhu3M++sYv z*9^)vI{)bn-wr|1p~I#{Pk8(758}eEp^o`(jDIOUfe+|K02LT~429{qZv^<^MH{ts z>pet~!lzGB9>ZMKkHAiZyD22D@)j_r6o`IQCtM+;fdfH}4%+Nyg;+ecL$TRwGrp+P zf`NxvP*(hQ;6W-q02D(}7tZxrLKVMcNd=pFBg~}abnY&Qjv+RX;6Dv)nWusxL6h!) z-)M_QG1Y_C@BS?UtZMV-NC<>jDQlG-x=B^sZ=U(>%S^heiO!I-6B2W2fTL#Ak@!)QXF^4)H+3 zy%}j*&7J!a2f%LKHr-q>Sb56q*&2*m>ckfu`iC=5hwS+%rGt~+L)S8Rz^8J#Vb4$w z-C3NxW-&;bM)Em&PhY;US7F)R7ZmsW*&iQ^4orv@t+EK?kK97{HS-c)Zq}VVVFK*H z{?Y_)^rS-r$1u%ALmi9)2}4=J)p0niwpM}XU7)U_i@SkV>fj{)I8!D^cihJ0V~%i} z$_Y#VwYPWK$03sJG!k#bOE6X!tH*Glk!SyQu_e=SSJCGT`@}PM0Fq+!W+Alk@!>GT zx^w~^jckCS-^1FvL5l@j0k=!d!XB_mu~X3^qzPXISXvhT@HIgUz`8iAh_Nv?49MMc z7Y&oHU00GriT;!oOzG~^+bWKmG6};K51{=f5qLCr>xP9{&#H?^Q{*iwnVqFlKrw_a z431xzmufv*nT!?~(IXaOZ;Nq+LMd9Dk?WSPUg}f{^3@u z*rv6N$FlkQA8+`iM#qrY7z-L>P&qvQeC41l?yv@B=38^2g|JOOGIZsZkuiL8j|`CT zg>&azo;%W%rx!}-4Ni7o?AAkz>6>M%p4GfS5~(Kd0l~7xo*%&J=PQ-^71jJ&^3B0S z@xs!6d!wT6JNB>m@L|nMqygo{!}10RWxeM&lG-#-d0kqYEYJg%$`A}v`h$K z+|T{%+2!&k@h28^<1zodf71P|gN}|4E)RtxH-a9*^tNtUa@aXlA>m~^b@e3%8!rK! z#;T2mJdOVpU*Pis;xvyuc^+*o(3Y&t@}htK__T`d1Mn=!@Nf9%ADMB4mG}OayUaXH zdh|F^-Uki|fVy(T^{QdiN?@$#lNZZf);R+fQR2>!QLNcdfGl^J{ekzCj6`C10%WKd z6{Pj)2e;TwsS7SWmMe`NQVFY_0`(}fNd25{dJNR$nkF`2OR=8yfZkT3p1_h6=sxl3TdRsa3kYu;al;L~KUzRXX;6jT_Iak5B%w^r}J}<)$gkg2&PyHAi;9Z%K=H5`Q%^ zQb${RlDCnX7pRN&&eq+#574aDjr~&f!Ypajx2ZIDpadW|HZtIpp@sIHTP&&CUg^qEd2MZ9%Ir|2(?vzEO?!@;QC{o#GKIBz6;t>O`U<$!L@*g|cDUAH!( zmKF2kTc|kJ(?%NZl&wYz<3~)}11ndM7fpQNCz8&dhq>`IurUZO z`d=~_jg6U(Y}!!OG}`*B&yJhhw~6Iuvw7S@L)9-z%bs>QxFe#^uwhrKZ^AMsvZt;f z9lzLx0}s9WcoIo$LTv0!r-AnC=OtWQ8qldbVRrs}M9tqhiu~TsZei#G4jn4InJEil zfWL-tND4!_51=hNzfiGo+uEM<=L_sUuvA4cKQ<>HUkE=^tmevlg?098GlM_j{~l(!&^T;aKh zi8rnqWXK8(<`ed>-$KUL{I2mG4UjQQHBtSE1$(^chX9)_)X+bxi~>{tZ_!LJ7O8+E z!Qs`4-NzPcU`_hpc$Z%GZ@9)F+5;D>ekKvjZ$O-Jo=^j)r>7&8WiK&EV==&@6 zu4;zm@E);KPx0VeePV?zN0Y2Re||67Lh*=A(OPQj>-%fwQv56*(jBL0-{WiCGtwG| zt6HJvNL%r4#Smhid!h3z2Z!MU8aM7h^>b*5SmcG^B8L57FK1d>S^_T^ilSdcR$ZEg zlKKN{gm}mUd>@w6(zz7BR#OwP4fl9Fc7aQl92w#f=)4oj&CwiFLAf=DoG-wM z38EFZZ$fva>p;$|tgeFx@2hl?SQk3Pu>DeWXVZJ_D^40Wda2XrHzR z2VbSLK@lc8@rYs3F-X{I1!Vzo-Y$z)k`k`3wUx>icT~Q0e0+{BqLvaKGPA+1iaH3VxScNrZB;+}QU5e^6oL9;k9BrxXOp z-rBJWkqPKP`7z8X4fFkOn6bYe68H~HCFPnBOw2QtFYAVkU2m)W%B#J-29W)xqC&^m zRESKVa>1;$arDtFvFm0%lVLDVG|T90ub~kKZ%N;%VFug5c!a^v{7ASgCT2qVy-37R zj;ty8;M=?2O$?IO1;NO&+cJZOEAA+iTnu$l-DK}-+D%Pe-P6N^pXlM^BXlDSaA#XT zN=Nr5Nd3(U9C2p4gg#4%+7PB1R31-^yR6zhd6g*%U;-X z?Ew-@dFURcObNRDt_+I@ygJ*=5L(gL+%(+N`=_B3=92q8@SU z?%m$dZsz8G7$lNf#ezf|n}ZDHQcn~B&4#3N@B{IilG#h zB^gPUv9_U6L`9oQN?C_gLWPK|gBGDpn-C){8m&rAC|ZOTY5P8|d_MR6JC5J?kKZ4^ z_i@~F&u3=T^}epx^<2*Le4bDA9S~)N8LMg^RlrRweP>YE3E*99lMx%0j`2sR&FF!U z;^|*9N!($BphI=cz>32a3D^jToQ^P^L#IWLXmPa*hm{m$hF28T3`d|^1uJfm1#rhT9WIVnG zp$pS0#JV-SB_)M9FfR0x#GSS=(iMvdNE}X8*4wf?EE0XwH#-{ak=4CY!dKpW**T-{ z?&8u??i6!!m5dN#&a-o|G=0`P2TkA>V)b7Ce!0RF2iIkSYlkr%B?SQ;C50=JHyINF zT)niYN*E{wGN8o7|EN^`JYfdZmUfW6FGV|CwB1D&1A88};zaN&bi#t=!MkUJfenVm z%=J!9?IymF&YwxWa@>pA6a)gV1+i1N=@FIotB*}BgG#8|nbk({#7h$w)zAAxM8&gT zMZK(;ei}Y}(bbhe*aGlCO(%Gb<%lmbLb~8V19F8Wdy0vOGhJL}6;JZs zOPTvq{38lQhlmb_>U2gAXRoJ?EC2_9v0A$qGOK{uU=N1BVtK%+QcefF}*Vk{~=^zIzQh~h0DmoF=IC} z%6k6%sGzT+xjMtJ5-Z?fYzleJ2M8S(dt# zT5$NTMn;yq-4w;6gXW5w2JraEO8o~7x)a5?>cGbEJ9p$+u89s2{25`Y;)=?92zm!gal+5E7o_zxchy8$-ZM2;HlJ`Jjlm)m>-cTutDqppq%nFq2yLCdB+ ztYa($4T+uQ5{?cInl5!Aa9G7h6E7}0HlhsXTp&Z5#O{}^n_9r_VUUW5Ls?kZcl`X% zKV_Tl5nMe}W(?_#)!7qJH#KbmS3yrSS4W4rTLyGG5fXyh@m=R>XuQH|f4V&9l1G`4 zAWryzF$E98Lyqmcwcn43mh}WA^n8E_DoDF{SS|r}$SD72wG+vh;()l@U}+DZA-uVb zjqDu_K+~Tyi$aI8s30$oTEy6Kr)W;^S|KcnQUIG1D7A^^0rFSY()~^}EoBP|woVL* z6+L<+@Imr*^?(pGPQua^#3>B2KaOQEYrCQ0Gma4)VnKSPXwOftr^aNC0hP*P%33N& z>gbT`;$?&V3DY}camiJJ0Nss|8dk9U!1`DjLq~1h$Hq)E*}BYlx}&PJ?PvE`OnKNEOg2G!jHSY z+yb}7pyxYEo-#tbbm^_CExl$Q?0K6-)99`!$bE3^2s6eBX`HsPJ^Fckw;U_ddV%mE z_TcUXhTRxr8CyJme#c%bXBgJsZ>+d@UUCuM+{(SbT5 zm4cD-jGvSvnF4D$06roTAJ9iojb)|6l3sF9WkHZ%HwyHbIVB|IC2QBleHl)F%Fp5y zUT_;WY#85*6fheZFt1De?BU8|=?1r!c5M($NT3faSAGVtuR@w+wU1e(arU5gaSJP0BU>HsNl zddO{_E-C=4>Q9SI;^!A;IIi`!+Xi7mKANpBL#G7z!2l5-T1muY*hlRs!Kli7_3C>s zwmdlTWHYe{`g`R{EKccL(HNecH4t~sqerpp5G*m!x7@ll1(cqE#qb^_$GHar|I3l+ zAqhzmJPn*ZS^89#RM?~XeHr-FUNjqdB}&t>zI}xCiOg6iDvI)lhWk=nNd!RSF~TD5 zJ6J&hs7`3{kst|(a-Hd13tDeJONKHOsHUN@y`;Rne4dWZ`S$xD{G?UTDIcFl7rnX! zvDdYFzJ$%p(O#|3iC}`PoQ9oN_!0(G{5d?RsvpbHP>#Xi_}ybmwLNxA^Xv#@JbH>L zzv%{x7BOeL3%~?jO?B0}+<`rqDc~S;-hw{J&MuO~fCLzeK8%D24uYZ&S;EubRb8LQ z^w4_x`7QKdIHj3hn|Rz#d|OD8(@V`Zl^vKSzM6K)>J~ zxRT2T0Q3}?Z|-#>Zh)=EIF5&d&g(CpJ9mt~iHh=Xo-Zm}WM(E$gRi|P>gm?lk@)s7 zLVQ76UUck2@k7NN(gKCF0fg2rEh`jXgxu?DTx9HoWqVl%lpKnaq z{Y&vAy=b*iBXC<8v1%`esAETuGQc_wnGt?xex-QP=@El~e>fITrg?i0EqeLQ{Q~G0 z8<{9hr~sG^F~p*%Zej1kPd4+qD}wpJ&G--RBDJ6$yc8M9*yiYYfz=M0q#ue$=C|Z> zw0;xaAvNe}pcf)PUIZDbQ&h}l`WS2%%W`Z&H&Nmg+~|ffUBcN+(zdN?pu_2ZQvX1N0GYBP&PW&F5~iEF5yVqY z(7Zw%wB`t~UdObToE}0)^#K17d;pqz6xOx(Fj!vz#S`S-}F5Iv62UHVup*cCzCr)H!hh-l0_DT{Ib!V$jRnguMI=tfY zhV3gAuQ@_tavl#M5|)^L7Gfl#tj^lhK4bA0U?qhKf)qT3-)wh0bq8{X5mcEvkf z&|h&Nypj%@3;VIOSPjzpco-AA3%;*Rn)N$30lvdAf6IttzcLfppt=f`&7i_3k00|K z_b7C*T!Ty!UoAdq;>5-;tM+;;TqF1@Dpqg+spq&6W0zyM6yq35O5ZOp~%oZ5kw`W-l_S%-W2~k8ty3omL51D)=-Ew1L=%nn-7G(&cH&dXl!a{rXVMa;0Rs2 zhB{jS&0o-6CqCd zD+SllkUf5AJ{}$hBoNa26z5Cr(6mM4*GS1{k`YOz#CE(z7MT5VUQBnX69MSTXGdp zvwzC3-jSo`}7XHnb8l!k2PD zg+Z{FFPAM?;7cdqGoR;UY#iz5N1eECDvR{o7Mw&JYdo3SfNVQO>YM8o;>eSW(Net-rX z3VOxFVep=ZRt@S^a-DDtYCxCA1QyM$U<}1l@Mlu5XJDbk%il$mB`pZHC^>xxCl4Y@ zIy#G(2a<;jWyJgph<=DnkpWTt5~+%d6SlQ; z3}CK&6mes&ayKX134(MF+Cg$PmZe&^dK)fVR!?sI($bQGM=yF@22PkW$IkA3P0f*i z|4aoW=;9y4_&M@^|ICt)c^RHdEz%fpkYY%A`GvhzPY3nu*Kc0SL(6nZOJVshDpVph z2b{q~P{}D%r%JxK8U9C+wQTs(;*t`IEXes;rs?xC8W+sjc5zcpJg5-&iQ`UfB zoFG717Byz{X#CYKeQaiX6G8v8DsBMWt`O^PPSoAFIJ~oW#6R+!1Fh;t2Xn0+O*o3r z*WU-k-B`17WeFqb`7LzTY?c1yC-&tvJ z^_NlOo|nrV&Pdm)Zq2Or$oXtKVb^(I^Bi4wxv^2-_NC7I6!`7O!?jLM_n_-w7w0cn zz;`6DkDRtaEVWFpwW3&DbRL`l*%6*F@^&h{MV7iOAiE%GHst7Inv>>->%s331vl%M z_bkF2v1!ur6}PL$#Na4+{OHk5Bc9&C=7UhGTGe0d5g@|4%+NgbPZy`zUt4)o{=?RY zqn$e}YSD)|jUG1?SCN%B!RTt(vi+s%aA@fI>RM*8vYtJw*-LLBtZ%Ea)lu*tkxYPq zh}IPGb6KC)u}|cwQea1aXaz!R4XP^0caJE}E6^mfoZ$3$c!u zA33_K?s~r;#xy^kb5Jwjam*I;HWlez-x?cDlU(TXNLsUJ&UB*ep_+R7R1kp(>l@Y8 z!6Bama8>hvlqCE$HfE&SV__Vjnv$LA;LWUUdt)pnQnMo8I(>1F4wj)arI2ra=-I#j zQIbT94pxBd+Pzl&C95RqF!(xJm331bKTNGrt)gP%Nw7)htNriC6%EmY z(%?b1_+Q?>y-?;#a&o72&>s-k+I8prMl+EE@fG}qHX9&a_z)`KSc!+jhB3|%Hrw7j z*vw|zHfJ~a=z-$C>+jhFCMSCu7(Bgb8h<(W>u~)50)KS!UStt`7Nafh&b4_2OLrwI(Kl^+si~RRy^K#P+Ry+^-?Lp9I!Nl+fJ}&y&hY;NkjY~ax8w)2Up#@Xpz$+q$tcxjbqeZ^YNZLs8v4)$jr1UB{ zq!+Uo-{S`4;qHLAA?E$~`N3v0WF>{xJiooXYkE6pNJ^F!St#E&p4~h*FZeXC5?>Jv zgE zAV`3RqUQGAzUj|;J{`NqkA+F0ug3Pgv)#4n{5WKz)2FA>G=Ysgn2<4wVgQC{grq&NX6Z|_s^R*=j#O-9Xb&FO(=swvZ9kc)eMFC(rOafbxOt;AJ@&z z3z8nS;I6|WnF%^Na{igiLp2VTVilWa5>2|NN#_m3aYmo|eM=`3*?hvIup3G3uC?xW z`M}}B_*~dB$Z2DPj$m*2v}+sy7PAWoEmI9~j+H04Fa5;CbYM~+J@QGO25^p{)hmFo ztgIb~xR4~$VO!gev{dua$`Cn7g&MiIx=x=osnSKMObbFr9Px!3g$0E_?Kzwq$C~jn z`D=@)H^WnxI%u9pi-d_OrCxT2>gnIgn9Bvgf*7OTPc8deQP<43*`0%PL2|%fk z#p?tr06n$C-gM(eZRFCpdMPTV6%?FVs1%btQ0=>#NOT^TFH#f!21_Ok`F5EtDuIR_ zcU%aZ(d%|ZZ2=qxielI~(JhG|4FL(Pg-#FzbFgR&= zh2+abzol9T{cU(JvW>Eg1Q;&)fE9E6F$o{6c+VFfWTGY}X4mqDxXYJ)pce`YM|OhL z4ABn|U}ok&No(4!n1ERGXR`LpoJ}Xo!KZR$0u=G6m_DYV`JRP*UGil@!l=$ye7nPk zRlvJ{6?7Kq>FIN`-dTH|?wQ=>kJ7FlFjA&Fe!{tDQwKVMVBJA<5zO9M9O@-Xo4(@a zKLDI#WMwg^V#Oks^mf+&+~;D1rXQAs^MijX+Zj-a)@$6odi7vN!hWzN+BINFp-W%f zPVA({N!uKQ2A@j3Gq11gLg*3i2pC5iESeBiixg86lU!smZ6&X-h<^UG%Vw;k5=l9283&mJ`@B+qIu@FFmz*L5)E82s-nKW?3>ZPf|2|6Dk>{CP1`j}@+pwxK}ADa z&e`=nq5Tf32kuYns0xtra{Al4RgUS{~9d&hK2hg#jH_CKQV$ejCgbtSKu zuCCYNpgp8|88P;_FP3=+Gs2tzLIy$>ftGND^!U@F(ZdG(GHcd|JCj#T7_yD@#`xY~ zHrf;hCMG0Y3Y75am>O4J%x3z7^SOi}K8n;&Sbvx%yE{wIXd5+i0jW$;X5HG?v_HM{ z1IXVg*9VD2Ue~x|pG)r70!bn=veM9EBQtFj*(~_v$xj*=+@|jIYHV$_-?nWOx_!Fc z0NvVyxWH;^?tua6bh=IzNU-^dHXS{7tkz#OEgUjd^aiIfB*r?7@Y<<-T}2fmDel|+ z8}r;giqx2UbS~&AQaL+)AO8D_73eF0YK2iRG{cA>sJqhm6zQdV`C@6SZZ@SOl+dB>9h;cSvI{%Ey zIsImyQMf$vzgmEqGtr1JHzqh`z>dRZ)Xn|39m}5IjE$uvBqXGyhzty+S#7E6BZKEA z?=(DKd(<(~EOwLCq}$HIzOoepIqHPG4LG<2dk zM}c@;I=zY8R=Y9x4d-dybb4xpN6m4G1~LawWxalVA45W}k&f<;61qcm$k7So#>HSN2|`U4l)9R19<%N?uO5~_fQv{`XY-!_9EBm< zoG~+_pqjezK^^ZoSOZuO&w|UY+uk94Al(vi5oa{3exazsxnQDOU$SVz z5I{rJ&5T68tUq3K`~LlA1l*j)b*OBCx~y{7 zDNQHnTD3a0*Ie|2`ucD$j(?xRBSeWfkA9FKeOsMc8TYCczyJQes*0447IssaWg~PX zy^*~7W-<4aTkHjAD0rQ#X;yYNgOv!fSCX@Z6>8g1Qz&)MU4J;;k|q(0vNb;e75YNV zz>z!DglV*X{6n}wb`;dakus;UP172POY1!ru8je{aW;(Nl+?}28)`!##fhX};ay)h zrQA;Fh%3&3QE@y3BhZ*4rVH(^XKbSeLl{JY`PW4Wds8)RTgf`y!1yqUiiwg{skOzF z{U55U;}EH#7$9hj#a4w83sqIs4|b!em?b47Xl*J;8+OWl4#U2kJ-cD{xAPcbSlo|` zgK0?1&Suv3RB3|4DYPrd+@4w8Wjgwb7wgq_Ye(%TP%#U2TX%zCVvzjsA<$e5E_^~p z(i5D#)Eo{TK3rN{jFdto>eG*<1!RG)dynXp8A@X3LJIOF-WSF`2zb196kCj^y?5E( zr;tYs%RoABkN5(9QCD)G`2!H?8@eU^`pE265)(<9B0P8x04)-^gZCXbh(O}7TRX7p z5db7>N-Qm-d49ZhR0D8*2>BmQ-%07j2pFYa4m7@*o0J^+fKmtxu;c#z)^<3YZxHn& zUy~XR&Pu>gBn6QQ4S|P^26<6iy)4K_U1P~@X7E=Y3MPAG(Zkcj#FFKhE@EmK_j3_OY zmXsVu7k>IQ+fh+@SbDTu>IsO2@QLmvb=c9#X)p3)rnYX}r~xA6Zhfq+ZSQx%blSv; z>r740pE<)@g>PxX)|aC>>lhf&pgWDi!iEX!6ix@`Z!9!safgFu!x=nqmkvgEP1T7|- zFq{;A_3G=FY2)|n!2`O15eU=5H&>OGlxT)-_hRtbv!yN++@A8Gxcoa3qt95Oojmz9 zc5xreXWlb*iz@E!cQcTZjjlK@CrYcBACU+07#AyRWHWOE+~tD@v*LzsO684WC;FsG z#bf;hj)>Qss^YBr76YMJ@zHHk$M}X%g=nIw$7UKUjv-aZgdzQpJ&}6O3m`+cuy3;I zV6k0}jvLWy5Xa7*JJ)`5V4CxP02Yxk>zKE&kw@m-s8Ua}d%Ws74CQI<@noP6Vd)#X zc}PzhGmgUy{MRdx4VxP6uE?l#xPATEh>;_a%N{z`CVKGGg-IlG7&dmS1#cFBA%-ic z6+p_cPBY-*MPaiW>&r-qWI0Feo4xiMPN159nDJmQgIWSzi3{?XGi%s~9=Cf- z6rmjr0z)`Wi>n~0k#$ipBx{CTXSRSmMCPKdmrnr}{`meqo{*4Px3658$yhz235s!! z2gWO|>=G>BcOxw*1bj{tU3hgYp$ zO?xQ}JTWnh)9X2V55bkA=M*@M`f1=weoY8_rl*3FrggR)J9&USPOyRi5mo0*o9Cmz zJ*dqPvvB7+cbU_!3%g=K)KILjBwUu=k_m=vi%H_0=Ux+((FmXqaX&P1zutEwpkjUd zYCi_?|3PC1I0?vOGjk3QDT9airM|#n!FpcF|5y`DgTW2voN!ASKq4kvwVO$Aa!8pX zG8L~*@_F048hEj5rmz7$0-3y>iiUUY$0mG^cmR~NlTQm<^tg{O98#}_9z_BQ)DCsY zZQ?!_y;2S{#@_iHj)^2p@-dfhcs*ZT)`+@M=&{D@V)p|#Gj*z*oZM|x0~{aH7$#6) z180mvs8gt>`6f*imcU)0et_AeDwiVJHsUz)4hm_|xBCPFb@lYBFLf`i8rfbUQsu69 zj1UBHUT3M@>|m$?ruf-)lXo0CPV)gfPoGZ8OC;-d8!xZ7CwS9Hu3WRG3j@m^<})cQ zXo2|U!+hH;VgOsf2EWs}eE$ydsV*z2eYYWqR|hT|tg+x2Hxm^o)6l>F&IDZa!-qn{ zOBO-O4|q<0T(RasAF&<*9mk;$uO}l1OBv1O;8UEMv3|P#xVQ{l1X%)dt^+jDHxB&C z3ZX1b1HYND#ToG2HW{eIZ~|Ah8e|#fFF1Ilz6?PCq*EAy*U;GQEm;N<;yjU%#@xwt z^M?*jPAsvxaq}ks6PyvIb}q_v;w{%cLqj9}WW6Z;MLjvx5JIf_^)QjsQHa%>H+N|d zdRKtH_w7*l{w!z|n}PfpbR>^(?L zRv4wmI_o@U%wxyWvplof&2U;u%Dlupp#X?;e5V{TLsAsXK&(^wNhxnTSOLLZzH~h| zdZfwCe;D`R>hZscxOZ~=8lwpeo`2QQp#DR1+<-V3 z(S-+s6UM!f$m5F;TDq<40zs+v(9Y61DvBv13#D4=o-rW#b5< z1mlpdxrTS$fu|4Ao_$d0$nhDYaNkT8kBaI>sC;RzTK_6RD@-AG1ji?ZU$+kf%WzTZ z^N8+JkZ(NMOj*lVOF?d~1B5mqdgaZ3FskVlFLma(t3VX+YIx26+3c>Tr^l8i@H?t- zG7RJoA9C%FA~Q?NaQ+GVi^i5#vRU#P8XaNzGT0%d(OzgwYF(a@7nIngIpTt3L-@!J zkd-`yjS$1iya574S@utE7kks{KYYLkv=dB=WGkjGD`|k<18=CfRqbA!G2TL+h$j+7 z2w89~n(E9BJBG!0p5ngS?Cpv3J4_ovHC{1kNYF&g9DQq9wl)EP76)_?ZY6d{v7iG2 zVrB+GPIuqlny{OF9D(onq?yofFr4LpwvEHb8_ug#C#`G33UIUzH1q`xk5(J!TBl0lQ?Lp(b;4D^6OKN0LE653mPp zUQ{&oFl#KKq3yY2v^_kBF@+ZcmT@&rMr-WA%2TAh(zmGGG-A08=(L z&+4y;@28r;pn@84_q8OnuT1zuR$~@H(!=HgsDZlkUR<2YFTYUHMiAvU2@(cum;C*u zOC1#h4Z#{EeCYP1Ufuk_)Fi4L2I>&H+>42!`tBm(FpFtnZmv3gdQN>^k>t}J>8>a( zz$dc`Vj4Xth(h_BHys^qj&{~FCGeDCR0bjJW(aIK&Ey=6j(D&VKBxlGM!)DW!LInS z$7z+)*2+H>pD`XJEEEONVRj~F?x5cLF*TxVy^pS-rN!kzTu!e&I)Y^Z$P?TeSzi!6 ztX*4OSvku6;FR9~J?b{-&wM*=^1JZ81C`oLe0ezkdVa>EM@X96kWb-#gW(IrO>_Pk zV@QLMbOZqbXscxt+hYKy&zvd9`IjLLjHDkyDamO%c$;Zazv0gL3SrZ>2a~{c0AvWaa<$cr%V0G2MwY5n&r||kPVXgq7bWSL)VKa$@fItc?1u1NE zViy2xa3OFCNF+`y$)4>3M1N~nB+Fjc(5Z8byjn7{XdHL!c;~QYhZy&&<0B^%T@Mlu zOg9rd>S1jEhKMILUou%2MSmRr;`BHgAQBv}s-7aPRk#=Z8@qY?`)9Tm?iAjZRMbs1 z2+sL>qmTF7clQOoqu_q16e#G&`H4~sZc{li+LFtBlKZYdvxGXZql3jNk6*kJ z)UY&PArb2{YDTpFw{P!wyK#Zc1OP$Mc>xGX-RJ4yp^XTA<5_tdQXm&W(+hr1xTGUe z6EddC>oIf|jhT42T7VrfPPF0ftl6_81ZT?7b24{#ySOL^8Zwy%0(rZTHoUBIz~5*g zqoZZ?1Be|TE=u^2KbW|6K4F1S5j8IIQ9hKX^u>Ijs7{`ICDL)1B1q&m0)a8I#X4{> z5f^fkS(kXnP|m!dv!)@VqXA{(Vm-GBu$`vncc?JB0P?Bs^KNJEKL^_ z5|vGb^SDhInO38fnStZStXbOOSn3JUkYQ!@S+g!svC--SadB6k)j!-PY^j!9hLr<9 z6~HGf^#7{NVdFR7qeh+&exzeF3|wg;{yg zfn+uB-!-f(ym*(6G*_Qn2NEcC7$b&H9c6^}KmQz1VAv$+YQa*f5qV z2!^}HQ&m;FV7%yfStE^Ic;-_$o^;Ep8}4B%@&g%#U~S+srxSrzLs3i>@=7$wEpR!m zuDXm6z$;Mx^(!=)A>5b2t0z-R2+NiE{PN3HatQXRSy{(R-8vsouyW7%lX&H3zh!=x z+gIad%E<%@CQozmd_eH!mX9EzAEs*NAP|N$LIHjYii(c($cZ@tPA8C7edJf?PCj*i z1l109s`YYXM~@a9bg~PJhj-(XM5P;^=|Kqu#lv5T`*^Q9g?eq=NCjq5U2ArE;8k>$P#F#MzcCFo@QIqfgq}U%Gx^cN$^j~8wz$d5g{|snlNFZ ziAg|daWi^Kk)%6yX@X1r224IcERdB$4aLNKp+sW&_;fj`OwxeAkuoYM2pH@EhQpT9 z29F#W^!%Ob>>#Q}vh|Ag!i#OYP`WfR`Xb)9@2_->vt}V_4Sa6Z)QqHvdp;)c&MP60AL4F(#6cpH-5Bw#vX#SLb`<#CLF z570UADkgp3uO!hkAd-NngKau>^-W8Oyl)9+;FXA0lhFcf%kNwzlXd#O&A5oiLtsV{1+TSA&w6lR#aMATT{ar>~8I( z3&LhV{m%pw233J%&`ZZEv3XbkE3B+Y#)(#H*NWs z*4C)@wVs}Hf3O`%bMoZB96QJg48IDyA{fen<&;s;bl-`|fvPD!oLWbQu71F994l$8$!Y^5ZOq zRnh;(Bb9X86X1N0+R>B5EVsPd9}vL_G?>bmXwR#Qc&zCMmo?oqKCSGH_$mGclrQlM z_K;cRJv4BHCtO;22qX_4Q}y3}7BE$YS(e%{YZp3t$nR!ee`n`E>|eZLAoxIJ9a8V@ zg3@iShj>}UC|YA#Ioc5-lCtsnXY~sY#S^Pbi;5_^&{ZnRgar9PA2352A0{vIw&kmU zIc(&eis{vPOv(J(g^njxsy{?F$>T6reH$G!QE9wF+Xk`k`LtM zZ{NJp0V@*IA2M`kSy>tN3BymC=M}HIZdn-&&PmoI-^ zyzFToOPg?$i8h_<&CFijHl!mbW)^FGr{t z{1F`qubQK_wy4X5U!C>mJ!%@Az@*DT?1-5%5gy?u`@(Omf>ML%=(ftU*#*{p$9^4?yV3@fEUoVbQ?xo!>+wAOsYp$A1Vlk^v%jnfwHU83YIq% zKoj`@5NMIO#*eRJa>eX|)=UR57!Co8jGyM^1%firGZ53*OVEZVG2jL?*7S{y&CSVJ zB{BM?**Gla(xvp05-dG{dt*LZ3lWAEGu%U&Uj5P2r^-EZU4KryDWwN&2k1tvfK?I- z7Y0&+b4!;v3vYOz!Q8i9yBIe5Nal_Y8)$hQ;1<#12aOVbbW`I61Ut9Qlh2kG_saEW zV8QXN8QNh`J*vl;7GKu&)bwufV&2o%s zcxkb<4d<$3_NNf^__|=naW7Qnfrtvln(dr0RsiC)Ul6nvBqW;kgghnQ-$u$uYPUM0 z#2|77E*UiV=cfgQ&fZz#L<-@0c>$?K?yAm|bw{8?%S`aI-M*bRh@JwJ+l>XCVTE48 z3HNpcskuNI(7@MkbG*IdVNFWFj{F2GTkmyLq!f$LyQOZn;$-PB#6$mts6F5kKH^669f4#i)8Wq;1e z(vMA1zdXZ-*a zC3ez{lQBRGF+@Kb5MaquAM2Y8O7WAUL&Q9Xl9vvvxO|Zp!6vV;5X*c1(I{G>xcrY6 zRJk}RTVTNoSi`oU7+O=H9GdR)v;(Q*x<-o96c1)py2p3+ku8B|rolwm>>=9?BZ7qI z@?y{M>;GIvtG3C`u3T##!=7e(M{w;|l#vkYeLG4)g8)reMFm6*65aqoGUWSI@z0*<9CGYhgsvMt`gauQC zYyN=7JrDQq@8x)CFT4izQE`Yhe?t-i#2~`b(BY#)xQ9+87c(N@;c5b!ZVNG)H%y!@+@ul^X$Bp}fDq+7 zLG}HrTSfzCs8xW~*OwU>? zIS@H&xZf-YT3h`ct=^$S;!1&(lEQ}yRZ#|Jh4^;3Bi|Btn7ln0DgzMb- z_egGEEI3CTz_2R5FO3rjJzg+;sJQ!!`LHMqY%)0LEoc}2@aRINc6&*0-7LN>y&!X# z6hhFatZvWq^6yJ=gAN40V&TJs%y7OypdmI<_>gCjn^1#rRzTNEscWKIOK^}!FqB2Z zCBD?G#F0T$5U68Ej?{e3x^syaIUPew3L9!z=#%A!hH7eRA>bVf{!CF1j*D#{E7XND zM8aO1Trvr5mD;`mp%CHS;WUca7ukLbIr!C}W z#!f7Y*|Ip;%Ywqh_m35$L%0bKmH15#>A{K6UMGEyk zF97H*UTT7Pa0Cwf9@lT)WbISH^LNk!bQdrssu!DpCm1=qo}4T{E$5k)#)d&SpF6ET zC)i|YYbSkc<_?3NI;Ggz*o2@SOTi+*s%y93i4%Oe&vN!#wg@XbvJ1V=_|pN$dafxh zE{59-i{cgZ>ihv##}Q$PAneFr-{b_<*Gk6pjj`;UUi&KG9fQI?WNX3wDP9HGN^?)7>aJw$GXKM;aQkTV9q^jZhQ`zCoxnTso&Oi^$_8223F#9?l*!pU{;#{a1 zW}NE!8U6s`(uQ{Wn_zLxZ{rbmJNuD`1n`A!!b4W@Rv{P>=Wt(*o*PP)O8*CR!tMi? z`|E+m1>t?ovq69BeEPHj zuCnDRG7aPkBc`rXV^E#Uq}ES-$%K^@Uj#mO_#rN0p+IQt+M;YmK`2b;>fRMR{W)?Z zjhN`@bq{u|-??+zn4lQIO70#+fuI6;eT^NV4i1g{325+~$XWe-sKZIYO_W(Y-8b1+ z#|v-GHI5UR1n@om<^(P;Z7fVT%PRy=X6jOMvh~`w5vb`W!lX2>r|@o$6{G8T1h zPnQdO&VVydlKK96wzZ5OKZCM#Uy0$Fix<1&TuQ*#JupK95Zk-A16T#%x;O-lMko8r zgbhUeJD2-f+g@FvqM#m_tEClcwo^(BRToolqr5K~Q-FJ)MX2=B=OB~4X)7Xxwo_f*Z_`ukUmGaUUv!qOp13jOP^8CLJ^Ju zgy0jk5qn|`cUefq+GkLP<42B!ust4BoEgSom8}2~cUUb+FeiX~%2?L%0P+Da#jZDS z0c8z?(VPGH<4L)o=Vlp6dF#vltBf%kzn5-Qg|xZ13h;_ea14B8m76s1A#!FXRv_yY15 z-w?IBvt9KoVQe*pHV&u@V3XPCwM=dCpiG~OAz17Ah|i=Q`0-1qQG@AphQC63OcyC& zT@Wzs_=q8`@{}pIn8;BwkP^_kw0`-5^sZq`EU}tl2HqZE{XAy-%lu9hCH389k%deO zlx57U9ecXF=EgaGwBV&fj$XtN4Kib@cOG&2>5Yf}BY^SlwUc|zhxse%brj&OO-=Ck@3@WRk`dE3U_T$fB~vQfT2K=C;&XC}tO|`8{B|d^ zE)o(PTpz$y$N~-`^;6Y=!Z z0AO^^ltMt*z-MPNLWQ1I@d3#JUjo_*3R$O7h|<{Hc7-p-PH7YAv&(g~wn>e39WbF~-V#qYQVFvxy~IDoDnZ8alY^!?)f z`bDG`Cxn%CgFOn$d+QMqXG20j!8S1vz-7T{nN!q^kwLIsfVSm_Y#iaI)>oLRY{Y|bH`^d#>w9u zE3Gm50U%~w#!(iyO2FJ_o!5#T`I*7jwwC&ht5**zjTPoSix58Ydmk|2pO$9%_{d;X zHmn`O0)YDeU+QBQm+t)Xa*)$_Z1bs#s2P!Qw7U9Jo$(^`fGAZlRQj}K6*vA#r2`xX zkeFvqJ#`wWJ9KefrBeBRd$dPHEWwGHY{wAw<2y5jz-G%A2fEKC_|=ZUtt(L)JnBPL z)r;)xt;qtemu^W|$*A-zS^tp~uL`SfWUU z_^z+(!p!|M{SjC%xmH&s1{*y%;FvPAJYuZB&>>sbBgq;B3%9~3}fk#o$QC$L(s`?*?d&}rJ)|{^Ye;uKl zlX^8g+!ApQoY4IFf_Oz3n(3V6>^89MjV31~cP=##Ak_6MS3smqQ}$|MEhUUO#W`u3 z91fykVVnUg3I6>R82f4K?^-bmo@|F(^Ilj98d>_|vIA7TgO5HoZ+&#-#tqq_Lj`#w zK_o?TS~!o_5g7of508MRxpDjTuvvNix*0U+!_HVXpfB^ z!{mGqudEL--)C*2a3ko}kc*D1P$ zjE!4Se|3oohT~#le7*JO>c5WI=T=q5cn9W<4NIs=y~innAksWcpN69?G5k$VPLzLr z^b8$UlVg}>c@1J1 zedETFjs1l)Je#$$%qHq6d}i1Ji6j|Ke537Jpy#@}tjx@6_PF6%0wi5st)i%?=GC>g zz+xMUaIX5c^9JjhHOY|LR_1LTZ8Lln>ELRDQNv3308AYo#k;Kp8l|XcVs4H@5!Z+= zXk95uxX!HJo%L6e;tWbYDsleLu-(oa+d^u{@e;dHd~M}HjZ*M0%+IfFJcB6f!)dgM zN)js(4s=fI`1&+Sh=9z7u*L+qjCL5O-jZM!z`ja+tTHn>G#@^HW^Zxv>2YzVf`cP) z#9$AOa*$T;_%P$oTLu!rh^;gTyfnCy?%KV3^U}>CYMpo{55nd+|Hki^AL;k!pPRST zyVHb;>0@J<<^Bbo+3hVS=N>qbA+$VK1>Kii%&I69Ht)tIE4=l z@b!3!e;LF=NP!F>;n;g(@6c5EO=gr85xHfh*zZ%eHr zm;-fcGs=0pn@zG1449>$SG(|-WiBY8yzuvk(`xA-W_%H!eU2Ip9=bN@-?gtQB;lIs z>mfXBrJDL;Ral{+pdiTfz%J$j$fFB|d;_fKcS-oHTfaWWoJ(9N^Nui(TDqb7$pns{ zgb((Lczoc70o_)4Pk|XQF@0VqCKQp$p*9DvCkjHsGc`fIxo>tqwp|YU4Yjl5;mV87 zWsWuw7GZjdmg`8HJ=1um1(x@W4G<8E>F;wZkV>+X6C^3CEG$05>D$>ED+SU_(87LT zrx2yUyji!H2&k%}_Y#^hN(E#Cd-cMm3toZiW4C^+sllng)uGQ3^bEYk;LjRkTV197 zGwHCw62f%7E~J0yn-89_Vs z;Vy!<*ERCh_HElxDjzePKte`%!;m_}uAkZcGaM3LA~-|Az5-v}GqZ{zi@(zKfdx=S zVtLJs1%4x`dxMcY5-#&H7I`gMu|i!{H9I3?3y3)^vek>{vr`wLX6L;F&qeV1-l0%V zux|mDN7c*>D?J)vp~=->pbJld-zZApXO`)%K5w3az6?6Xw>$seqkS6$=_u6;Rk>Op zWPiwPy}d>5CkF)@E?iz)Syjb`AcNVT|L{Xm02Z>y!gkv>R=K-|8o*valT%WLuFZ|q zK3~X+i(}az>-+y_O0QJ;=agP|!G|uh@<-F{g~N04BgdK>xloDAV+Yog%*?Tx4izN4 znkajhz@Q-9-WN-cm2u3XXJ-(PY7Q%8l$coGe5|Um1PVA4=KHoH!r)#|NlGjr&Mwuu8d-vwgYGi>Du)1<1^k&Xbi43qw*JgCjR+{ z8V~Uluq0+6j3cgZ_4?E}LfkjTwovt5YWv!r`B@#1Rs$3f-H?(BBsyD zC2c2!jdUfyXFPebi1DI?f#W?urk90k^!3k-wfnos5hO;?6jEM(cE`4xn$x;We#=C9 z*Yg^G4Dd~FY-(at%i=isdR^fdRWv6%YJm@b>;_%6$-1X1IotEJeII9>t zO?oZN^gQn7L_@CP)QKY?l?n+K`3ind>$h+=kg;5b7RUZkQ3kZ*QDQ_V;5mC0v^PJN zXbM@y`GnMqwtFAU*30eNCr_9_iQ@$KshUqWw=i*z<)Qnwlosrvfm>6PJuY=?dKV*e zQnt&0$&Nqpz94gpIa4HJEHvM;gR4@C@qb-A{+~ciqpj_|EbVeU9v&mTvAkX^5Vs}bPh=3kdb={ z%5EY6+MA?p!VXpnD(nS-nv+5u4xK(d-2GXvIL!M+X)x_>B+F)j0(oJuKvdVzpm9JfRr55|cLV-C zh)vK7@P+~NxKphWq=EF2^0!!A?-@M|!4QMd!_ILBN&DWHDmlU2p^pxMD1`K72)L2M zA|-qXUx-%kQrQv#=}s7i)xzi)Zffnxk0=xkS~vdS-e}LAYlfnPn@+ggYNwNKP&4|* zk|5=mA?o89C78a9ZUCzuc6r~OGTV+5NzKyq?c2tf%}>bar6nau!vxwgs`~)Fj7Uhsd+g|17*6dz1Bo11eUrRa) zAJg1D)d>G1RaIe<6uDzA{wGV9Zt+m&;ejrqDn5YZ9LOW8lwq4)E-r%eqW7HpH%2nw z0A9s>;anu&)V9>W0$z6N6#NLckC#WZud)Ywz;5);l`CSt=>Y48OGot*b@?YJoj9h{ zJ+|Mx*r`*?%KpfmPFzoe)%aiZdpiQ^O{CYP1P z3>=fH`@y5VR66P3-UA2z*w?j7I>arPBbM8Y4?Ei)K*HoN4(ssaEd!wD-HfP!~nK8P)OZ2de?KKLDD z0UeBUk9FB=Hafo17Xxf5y;O`?g`Z1Odk!5)u&Q%W+k%26??2 zlgf!BJqUBIE-n~DF@g|sUfbu*&(SUd={2067B8-5IDooTtT%fxFm>GvQ^uE@Ul8AW zOL#=Yx1YI2PHHwNA6P=?jbBFlwV*j%vEtJ8jTGDTBd`fD;d90VtI}2cv1!w#*tz#H zGytK2W0Rljevd{etJA12vTVq|q3TP{DK~!WBb;15l6^AN#C<=0{=D}0-x0g;V)2`V zZL6ow9@E|c>(9|7yII@Gg7<-)@S6=+y!ld9S?NX(3Ez+EfZeXwu!21lclfi$f3*Nk z+II;tNM6BCNi2~plizJOM<4wGXF+A>^dSL11>{`97B;%D{|pgk76>KTDLXq7w_2=f zi+B00g~kFG#19PQ$mhxCP}%;H8PqP;6^Xye9-_gAl0*4U!3)1K>h(8&ysoUM)4uQ+~u(1iU$|I;8MBO?PVe=c&dN7T*F zFaiLivX`#qh3x;Ib7$bEr}(|Seet429P}l>j2kCxR-=q32mv*zYoG6yX44t!D|`?6 zE@0sQ45F2T^T1t&JnX6A@T!t|y-0Oq;EvE zkoDxzqutnE@nlF&pax{?866`O@hRx}c&`9KKvvfUW`;c`#+cZz#g6Z-DB+W9(?pSv zMQD9pUDx+2X+i-h`_opw5Tz31s-$WGM_gd%i6$I!i3`Tn?!>404B!wz7`8feAl+0= z5=>YV$C5*vm>ES-bm(nwAxQOn^n&Tbsa~0zxy$sMgcvTp%<`Ntv2B|DPg$8fuYV|JjL50Ez(;2}fdVi8u>)8=+GVq1;;G}uCEN1p z5Pw94`7n@n5+5^6qHJy3>HaYjc-kVp`#*TU=zBs|mJ!_}Rj=ljQNzsX8| z;GBU~;ukER@hlcl9n=ce0BOiN-#>87xK3a}(28<-fP@4KCm!x?I~nu;IIbO!c`AHm zMr+ZjqQPQp^-)F!=O9$Ra5YoYFu&YjZA@PHVf0?cj~~np68AHB%RnLu0;-Sl`?mP) z$X;S1Ifq<^<--2?esgkgnd0B^DmAyZj$QoFsH&m@>^F{idyY4{t)k-M|9yp(&;4xw zVMP)c;h}nmebCUpd=ra;Z+u09I2T5^XOJT6L`^pi#iJ|hxSSh>VGSksr|9T(faUSq z8$NyxX3tj&WVP5nJTHHA_uRwji8e1S*W09^{$<-yhMvXF-E-IL3eO{VOKOoA_Ha#?0Zb_+_uM z->qA=I0oAc?3*Y3Kd*437o9gfMLUQCWjbOOHSO~jrc0N?KuD~zGh4ADhSf7XOiJWR zj>12~*YNK0zZqKMmwR%wpC8{z2qXQnco?HF*^I>X%J?Z2V=Y;|D@{FBpx=}Iq;LsY{MQQ zh~w$QNL%XKkrtsq;=q$a9S9oJ&@k`8*&9{8GNQS=OgN&)0fBYyGilOIm^lV}Q~2jP zvk|76tXdV3S~N-A*R97UQE=ePbbR}qfg2Z&BmaH>Ph zY>HC`;Ypbmq_e82b>V~|Y;L85h5`ea7aG-ZFE~0y;&gTW(^RFD3imtgz3P`zt8M7c7{uHEZIODL9A=$fu#HO1}XIHvl2? zO-SX9JWq7PcuX(bCtGD?%EO>z+qe41@I#fHg;A_)EiLr|lgXYZY)ZmG!^+y)UnTmc z<-3#fBYOX5&JcWHExCvOMl$2sb#8RH%amANea5XULFn>reL;;#_Ed;l?CrbJxbc3T z-T9-vom+rx0rjYgs;cqwxP?l)N<1e`94v7F>RKpd@JZ_!KT9G1yRATE37;yE>aAP0 z#KM#9+*wad4T#qmOyXcR>!0TZ)H_EO{_CI3fKpKFN}{R zwMLB2%~fNc4%#k0#DGbYj+&0BflEPoO?P7G!5aZZ2gO3o^Qx|!Dusa|?$^*jbCw!Z zI>b|KkT#Kws6Ff+%cjp4Sy_O+Rj|@>^770uOv+)k$`ra7LL;Q7SL^K`0$tkS5!(jmp8IG3t@Ty-~u4XMu?aObx^hWap zF4RD!?20%#uIETD+oRJ(b|6Y^q?QD!;9iHw=lvhH-aDY@{eS;|Ata%)lA=Xsk`7U! zBq1StM2kX{)i5e8QdB}IJ9{UUh)QV~4H+d$Q=Cf0Nt)l=GoSbGuU~(h_c`z5)T`(7 z@wkucy6)HAg@IEIUEKu2FQQCcCyA6g2vaINVl7XcFBF5CW1tVs*Q|L-@td99Pk)iK za~vSX?l>|%V4IZKKTN%2-qp7+J2>^F0IMrebU%>Ba)m6(IybPl<;z*!eL&F;z{1#% ze_9UN35?AbxfIbc-TkEUU~E$02EmIFbPJo-^8(yG*=c)cRW#G19AE~T?DGqtO-y zA?DV(Bd2R3p>WR-LGahw+HLZEKTr{Z6*F=5y^#Sv(%IW|*0IL0mh^iWL{awZ*Nn*v zUsOiTf#e_dz_u`tChi$BgzyU~M)`oCcbC^U5ZS|G+z7T&Y>mzF{?ZFkmq}H=~1s77il!VKd1s4y_ zC9ksol#YGKNzkP5c2|L>h<)~4j6wnz2%Z3a4WA5+EU}s zi;ateczL5cTRE6sGX31yBci}`q|aPXjQ5WfcQsi+>I(4pk3j0d38R^0aDaC}BbxsA z#f18#m!Z(+ez3`cFD8t;>!W|UhlY*Ad}`=W)afmO6bO`wyCaa527h?KWC!t+_rPDV zCRx8?h4YRwp#{8h8J!SP-vzHu&>#_l3}obtl}w>N=L5bST5cfqtK#M0@-1*b*XFGH z_BJms1x$NqKyFG(Bp;N!_w?!6wUWMc5G-qCgV&okGM+%Q7hx@#1;Pvti#%49GCMwE z1a5EhejfRA_cl`QCr_v^>#vRnmfXr9>x~<$S(@?enP#u3Bf=txkdQo&(nT6GgMVdm=y7ljlEOO^;Rj5GbcQ9y*Ms|~N1 zX1;jAFeTlUiCaDoF=>Flq7=E0FEAdm6vW1UDu?j0ppYx{V;3)CmlUy2te`;L#{h>q z$Z237B&y|a(QCq{K}IZFx^&Sf7D40^GtpLEdyEBfF;_soJNIM949T|z1#?g7@FOK( zUcAoMLx5V&1dB9K8~Q#9M}aDt!fT=Rn=)YjT+idS`6*(BkCA)i2IOM+o@Pq)Y2!75tslTrDvWn=W8y^VuhqyL~}C@NZ4 zeuxi=+>S>GyPx?o z$d;qTj&?i?S}C27h){t}8WBLK|Mlw^zZt(e?;IZqhIux&e?{Mls*ad`JXA{Dn;X8b zwFwie#~gaTqNDqJp2v9xckCW%$m(NACU-gz7AOxFuzU4 zLMe)P$`v-@=)9PamVaceyzIdgrItOI4Up%KCR7Z(OqAeqQKlh*%=x)<5AY%dGXG%G zaC}oULjvS5Ovci({u97Clf9JP$c0`VwUPiTXWHd@Q#Rz|rgm3n)YB;H>=iDYKd(_a zcZo=3qNy4R#-ku7XRy3jU&{Nr70}2g^-9*oxG^2$`gp0hCno>_5%Ott8@pS0d9W3Y zzki1WesO$=hXH{#_WBlwp z;Actsj4QczTCR_qh%D?WjCi!4=_RDHM*_alXdIg)Plt%F%Az>5iUb_}>$(B{X+nr%#=(Wh{H+bx!xO}S0VCZ+LL zPBO_9{RMgpJnOOZw4yj<_i1SkzvMj14i#}i+6Fd=mwB_5qx+Z3gA*zL) zJeAqz4B{c7P+)K*Q-QSmD2>>Z@sV4gO%asr87IVbd_0_Ot5p^_w56L}hl=#x- zE*`;3bLBP^8C=J24vaX*J+F7>T*0FFb(ez#7oOCP=(A^s=?+_{D1~~HNyk~Ar&F3^ zdT?pFU@1cX_`M8j3&h#h(o!i9%u<#L36zRqTK`A;DZ!zhN4xh^>=uR|kYVxByIaWG zpj_gf#G+9hrF3JHH8n48E6&Z;kNzGB4gUJww#co;M3}{kyXZ@~&QcI$rexaPEkqJf zx^ipPn{%d9s9ss+g&T&D@-iGu`Tvf>@gW`$dep|x!k315P|S(N^`-uFgQ$mdTpY)$ zO+3+9OJLYOMJeSu*ZWROYu zu7IHywOh7zJ&?mQ?(AkTmNEVrlS>2-4$cLaC#*ww2pbR3?e(3%fVNVohC#_Gaex2! zABtplz-FDAbZ|$7Eru~OI`{7&Moxw*-0R|EQuSyN2nDDb~Y6) z3ts`mHknTs!zX0BV&1!VG%Iy1m%bJ!FIIIRr9}8%orVKQVL;$8q%isE579CKXpmSB zXQQq@{m+H2qBB9kK|D7tx$z@M8hs|;ms0O!3@}uEQe9h%FFa5N8T!&T?k%dV9&HX* z-SB^pv_{;yTNrvHZ5v2oHuI8CHta=9?!yZHPyCqYZes)nbFdyMT*XGFE3OqnMbUjL z#rF}@&oF))D6`38vq9049Qj_iZrqs1PC6Nfaf9Z!&m`riq>P&z$@T!W+VXw>+*BCynPd0y(eAOkTZju-#wiCMx^%w0 zjPG0i`q60A;twA^Ldlb&5pXG#R)tGTbrp*$EpmJG`R{0XK6HLjUlbSrsIBFR8h>u5 zD4}hUK9679;&tnW4IK*Z+&~{EY?`NL;13h_c+#AQ%hi{#*W=Q_m(xPfCUR;iWGF(Y zytLM9ECvyvUNji=RQbdX8a=ih8>(5qVj>kb21Ykn`2jNvS9Nu?w5V4@h;p5}`lBf& z598y5-OmF^fT_ar@Oyy$ak-Fq$qyL(QDEOA)U_6uJ}Ezz`^4NcUu|0JN9Z8p zhFK67^Q2~?VBx>xz3?}vuT*c9pDyLn^i`N7r=B?k)hi>@ zToyRv^qDipQ-}E;^6ba%f5*LyK9_*DdIn%YkGY~C$u0+q=J8R_!Ng>_kAQgHA$3^ zd_Z_fJw)L+OzP5 z=pJFwEy;zAfo=8a!#NB9)gV+Tmo+ufv+nlX_M2RUWF3-8TU)!8KZL@T`){ZW1?hx8 z9LjuL-LhZ3!d-B2Pao<}&N5+=rpWhEHq8O!lLRxHq3f6^;BOkjGpX+25a~++5qj;p z_D#?PFyf8?Rh7yn>RbSBMf{DwVY+)i&q|H zG51m{^!F?&in|Y;GI?htyc`t41^25HQG=Tgj?TuHVGC#UKZxu6;K4XeaBrdDy%&mw zxBO2FP&ssEv%p?mIYKhXlYi6YisqXRPsKwU&rDNYg>18|veGE6ASm+`Jr)pPS{P&d<4*bE+GV?E5~*A66|z$fRo1s=D$Ivv{mKSZzJ%QDVIXOzxu2tC9#Vt%Dk z{s;B)y?Yw)R2bpf$9YLwwA(oh1tIW6IwJV?F($BmI~L%`r#>aP?lcJ1Bc|?es!L}1 zU~7af>sHOppHTPm9f($VQCzxYh`DFJkGA~LnTnDT<9Tu?jvd>9z6@&C=3>u3$C*8$ zKev0j1=pnWpZdyqC|-+acZHPku4?eP%A*Tt;r{^Rm zUqFvIV}|?YLC)8 z(k$QrGqepn!n#MmD1;N?8?Q{M%o-h%Qd(+7%KIMhfOkN)=SxQ?f`vd_f#hUMS_75x zY-Zk(L$fSu53@f&PQk`pW*p$xve515QBx+T!5;Vmfak){In4U(#Tf?0BMQ;Al1$M6 zGf&9hlXGTl@0OJ(e!oLLK1?du7#t`77q~usw%XYdds+1|YnHVzV49oN zK-bhe&sUW}Ne^iAlY>Ehg*xFNdb~vU8ER?=_oN9WH(V-Kmnf?4;m-i%xToP9)-PFS z^+;s7z!^yG7;s&=(q*YWT?BJ6ETX$kfF}(A*wHI9DU_WJti1be4fR`Iem?xySSmf} zw2v2cj&l|XB}9--N1KI1rl+Sc+R4Yhr$_bt->BAk*h}IBhb7AD;1dHVSIWCa3yQ}> z_uW-xGI{oYH?RTr^6xYx9SUQ|YiKOuh<^A$oO#!O^*mbx1#}f;NumOP38hAVy$O3B z2=tewWio4oa3!E$so(Waqr-Y|7mFJgPCcX$23d~oTOJq?s|O@y(@euoQUx(0u~VFE zTJAsO1l<>m29)+)%3it;k~8W}CUHT^C~D~Qx#qG?nP{_UaOpI(Z^-SYk{g45A=^n{ z#NVnUsW8^_5Ic|i)x(b%K@BLoR`c9B+eQ!Z;`vU`2MV@(_wEck4KwAjBS-pfJkpK- zhXZdjeUfIMo~sHwA`Jz!kYku()gN4+VNrrZLBZ5&g|U2;C9bYhWpoVyEFX!g193KD zeu9zbqM|$K9XltBXnNJ@3K(@mimd=qNOz?5uf{|PCT4Pb_m%NOn1N>K`*1w~`(Uzm z;2yYksE(4#N|Nq>RG{=nufq-vAw)e*Nce>E3ZC|~`GM`Q>TeDiD*qWiTcftQxAj}W zpf$;%bWWa$(O5RkF)G5tWGaNo{QEI66khVOviJBK?C|j*0fD==qm0~05hrwUE_EJ{ zvecHaw1sm=gaw_TlHjlWJup5hL6S#y_&@$TW;MW90nVUJrf|eGDU$%IZZ!MNt zWA^hyLXP+1$&z|jsR`3uP&-=d_4YA>LWUsck@)~FQnDERV(JQY>9NWysfYMIX#H>u zX@t{bwjfNo`zvaeD@>)8mNtCLQ)4EBQc3y0n`xg)My(srMWm6*Eu*YJ9+QebLO7tC zEsaZYNWonol30>UXy@6);64kc1AzcAL2*Q%hK^=-{OzPbU_UHTP1stmuV2Dx=W%De zuUgVY!UxkZEYf78q&zGivTlY!Oo9p<|A_m5L&uL00};E~B1V>%$LlE_%8YA99TOHF z4q=@GqB`1dte8G;n~y*{RL?zS7acksnhXl~*2vEvh0%Bl6~6NeFFUP|Ob&o=!NBZo zYmPZ^K%a975LsQ;y>O9J-9G#JEv`e`J=Z*MNcvHVRGu-yMYCLg1-@+|8{r8+29pZQ`!RqNj?@*QbMn%@z+WM+buP{Yg4f?oF(TByD=DHGc#4P$A)lX~^QPkD_l$?Bk zR-Nnp{CNej37=i>`NU6^v15Dd2ccV_%>{A#hjOgfg`@9zVoKu7S~F}~02T#K&S+Kf zyLWW7z{$R%U#Ov2tKLMwT7*5zHav(Uc=kVdhJOxWxUL+Z?70%jaPiKR=m~It(4m3w~ zc$fHpCrEZGRkX;?z*BT`+{5}`UgK^UPT#h5tAMi5Ao<4pVHtpcjjUXWOM@daO772| zJ2-=rlzOJv4*&Kpa3dfv4*h z9wmK41^^k7DiMULTyI&}F@@M@p5U^WIoBaynG;Od`&&J-Xbv%gwmU^5fqj9OmQVeM zURc}{X$7U_sKB!NCeYgX#>TBxdR= zp}4?&!syQX?(ZMBKkFP}@UXnMvYHo;g8Fu3BmuwV`**={f8wv70Dxb+)rhxF3bhaG(C#JZ5A8R}Msrw3Z%L zn|7|ft0>8ls6y&Qh<8a>Jk8wQ(n64ddYIvi$!;wMMujyEx#WL%+sGOqo99JE!+b5l zVTd{Wf8w6l|8k!*{@q@6;P@qaNbA~+?;6BE4@FEnmaSj!j#&_ATYcQP(-M(5kE{RVfG@L5t7JU(-9z z2!4WeNbPnbb%Tw`sce+l%VWK_gy2pX1jdoI6@_f%r*q;0zV{It!huL1tu?|hS$pB7 z=-lShDVXyZ#|+Z-KU5q(1{V-=8YxOuC7y<^-{1T;Cr8Rt5u6rf6fB67QzPLCS_sVz zk0Lj3A@nvQzCkh*04(7;LBFl{Ux{j2Jne8wiRPq9bJitIeMjGAUz9+olN~b2+j1qe zIF6N6mA2a~0Hz5`T*8cvri?iNHq0~?cJ*NVU}7S5n%EkJWl?6|%5bo?CunToeo z$IN3Fe6u=z_>4U|8r{iOWJ1jwQv~_M6=lY4=)kyjFT5Ly5b%A#h{2+wG5Qkga_ z9EbprKub$#=$7j7*=ParKfS53_6gb|ec%W}%s2nP|EMYgKYKZyl+{wSpTn9IY({*gy662cqCu~sxmVjwfWtR#qXLf4<8ozqpAu^^izkj z)LwIH=#o>e%~&r8WtIe!-M@EF(2i?q4Jfd(eL%sO z;a=b0FBP>QT2FS@1CH+9vxi&7$_9Jym%PbD_m0}qQcVV2wdC|pstrN?eT=Ip5a-}E z78Wz{Ddj;B<_Zc;z||p=B~xJbxA?5Gj2{CW z+Du1vnoD~r9y)WTjiGS_nITg7*U0J=M)2{Irm)cwn)cbXzYSWC3~p$rB-uj|{}cdk zmXKhxmsL>Uy400W)5`ENbx?)y%)uogB41}^ZKH9gVCk=Am?j0XO)8IsIi`xBNkn;b zN z8t1er2C#U0gL%lDqHDc8F)UPKw+C~SYZKk}8_Wu~S@QepV4=LBv%7shw~8Kx90^F! zrnMc@ro%^$_$b<8noF9Zg62gbqPS@9WpDw0W;z2brO$hlE?%Ubn5oE!2kfV%oRs$r2Euq9O z?lylqF#anK2&iedvvbF_WSnYfW9ba}>AE{sP#2*T7nhXec^Mvk`{6_2R1G7%6AGaY z((<~|zIFa?9^V9a7oIf}k-C_@LmQ(En;sjYG+&1cRg6p`rxU=IFb(B8n;d){_ug*IQXioKVHRMI%+t z2JSC65+Vb0Ap6Mwr zcO1ia2ag8aM97^;yZLeG@{Sh+GEKMt%&;Oe=2=k)e22a&rW*yHhuEM!pvLoW5--A%r)GY}jx z7R(7p*hh~aBWj*0RbE{kuypKs`v8Lr!dWC#LjBVNl{r4-(H{MpoG!@y!08~kNYZ8B z4=o-#sGCS5x9V1~S9%#a7^4SGYzLy_g>>TcP(6)Oy* zcNaj;SrDaSFm&)Dm7Zurp@feBu>%H?7ppc=jqXM+%dkHEym0T^ei!rTm^pNY4C5Qp zk};Ar^;reQ8ldT@0WTOZ!eo9<$5D;txM*pX(^);_fz!rbUy}Q4Fe?S>8EvB2Wo8nd z!QcYNSFLIt7M=9UtR7Pr(`WQ>gp@eKKQ4>0f`XRZ6S^V}-s1lXas?BYz z;+rZj{m&d>YR2N@`AN>R*IE3a;@#(MtSUelGiNdzWv$#zzBjN81w(V8pt0lG&X}e% zHU3{a>nlBld)ssyod?A-Wjvb)ft+$f=B|t1;lnT_whC9T9yC-2Vn(NkYdhHS#az5V zz9P4g#!VxDJNT8#Mz&FM7qkALd8qAAonAWR{QHj|8QTVeQEN5=Py*KL{5E9josN!x zBn}1lXG3WmJSeOvj~vq%8y>Pqj@>ST;Jqcrb1(47O6r>+O-@BbAks!K2UaWVOI4+P ziyi6+q2qBF^)rNnWo?HY;)@TFM>8Su8T{kefVrgS_50tGF^5hT(`RJtqHa}9O&|Rn z-rvY^k0#138@6)oT1ovy6c>SV9zxw`@%=HZou&qq;Fa6AQ4Hy4FIzb-_Q?~4KPWb+ z2(VI$j*4P?yXmKEa%T{dvN><3waAF=nRI)bK*0pzgTr{E>$h)zez;vl!iOqo)|Etf zU#`HLbF=&V8FJ+bzr1J5t4yj;#xQ-WS=~fcO6d=OJ9Gc1%uIY2Ii7TT6wF7yok|-) z;`UUuIapr*tJ`~4As?q5c)*>-5-Ij&gdm-f894B=E~egH^cS&QzA44}A~t-IJtp0% z9#7!WF#l5D#4k?1^f6X7kRo>doiUm5Yy`idAdOLE@4KR+NPiI&B(JDbr}3jIR%q@zcthFhO|fFYtnWMC!lR zc;~$LfD1;NEANAOh7)+jt$(D!CCxkt4tmoY7H2 z!=0=9lkWo3n};Ft*{HSl>Obic3m=&=D}qQ2PJmLL!XLQ=m4-_n|U_WSoP@d zcOq-w16@QG*XW?(fNVExSUl{~&a65bi3=U(KjtY1)4M2&zxnV%Z8&4aq_};ksm0C7 z?l9YQ|774_jg35HRt7H3;d*(6?-%wi=m%k{6n_fx5Z}3sQv(}fe}Foas}KsL#CSVO=k~a`%#<>W_nJ7! zpW)N;FJJ8J?Nx(~d6Lj%k<|jPVExiOc!4-5X3#=GRNVIC<`L)(y(#AdN@?AR9Qw192%^WN44hrSBwxB3@#lot^T_z)v)grS9st#Zj$ecD z1E`hAq9pEV*#xR>X&(d^X{?Iq7Q5B(61_lS@{(VVCN(W_m7EY?4pRaGvLW)DeZ?3_ zmuJ=|mda5&({W^yAnxDi^}`GNGk$y@&-1WMN5}NA`LsKq3^7;SGbrv0B4R1AK85w} zh%|&POz-YM_7-z zI2K`okD@SL(Hd|(eym<-PQaGrXEJ+&YA-cMLRTw97UE=f^20J z-PWQyzp!W&Gc7P$zH|QhW4$D4z*jT~$;pf);Zg|WMq8hjGZ%LnqB&AV^bknFnfD~@ zfekJA>5~;0P&Z2-DTbloNXalI{3)oStf$RGDqjXMCS4jUxD{MsnPv(gMImA02DR= z@(`~CMMU)0&*6Dr|DowSKh0H1!0&4*SUFEb-U2QHFr^=aCbx2)ep zIEKIyj2k9T-e_UW0R^|ARvFI-S~M6 z7{dgk>a>pDj5Ipq8ASm;#J0t_L>Vf#Jvc})Y-eW|aId7lWD0kg=o;wnKWXAbfTJrj z5_%zOoc*=A=v-GpVzmjUAjHRjKN&{J*1px&L9WD6Ektb~tg#qkm_(~Ll z5KPOLzh~GT6%)DuXwz~!y_9uxdmbsQXh*mT-mjo=hHePV9C485jh$aUBqb+D zF~Z5wqm=_L<_LpuQBdVF5}DOke-Szv&}d89%90XHG}yz6Es!8M%J@KJ5vEj!4n-Co znDdJ29x&VgIYur5ZF?3u>*z?$xzorqZNFA@IU{huiQxw7zry^_$mwlqa2zPz%eFQa;zJqZ3?}vum5`ae_8+}cQ1N&H2*iYVi{EfIDCFu z2H_5mrRQb!S`LQUBeeeau$RHVfQ6DLxK1i@(7TI~8JcOQa%A8x86U=tnAzg;7gKm< zDQ=H7>FAEp2_tl;0KI+d7KIF>jXV*ZthltKy9=QDii>AVpYHZb4?@so?_SP64SIT7 z8h?g$2apXMgur`4z^*O$Ui@@62xZI|Otk_q#-SE8t=m2u^yPm^5U2I_p1eOouk1-# zG87_%YsXK?PVjxD+e2g_lM`PwaP+}wY;%Di*!zxLiSkiMEh^V+cGXV8ZOwM;)&<$q zQvl7%_xsXj1^3_-Q^$albC!W<;C!US_@`h8iq-V=bj%RW1P325ioD+2krUnZ0J@v| z^z|>iIG1#Pd>e=|%#wn0Eq34nHhaZ5&^8;_Q68G?Q!} z6%p27qNWY#DTNYMTs%o5;MKzmf!t~6)vKWa@;?_&_`iJLoh}9R zfyMMG8ieeM<#idWWc=P@HAo$RnMFc^QPm9`+JI2Nd#Hd3(8J{APv_@!@wkAGbnp!g z4k8#-3kCGT37jS1cAO?a%1A(-=V_q>?~Ts`Yeiv24aI1BW>4fbxpqPf8NH?`erHc# z%Z8V`23jPiQoKg%*Htrql8G9w2NsqAJscx$7qMj%iyTF83>VpT^Nl0Szk}BGxWka zno7iyWdst*%t_m|*D_55YXNEpiSx(7Srn!P6~n?zdSkYM?|T2tFECKpW_9eTfz*So zg_Fa5dPu+Bk63~mV`kbeCohk(B`7p>j@x9oG@w4*1cIYkM}v_CSnh*BqZyFcVPP&9 zm3}YvkL=oc_T36U{pZ|S`CN$sHc^U}XJTh=)>O3KL5GWp)}T9V4P(U(Bn#jO)YBYu zO18i{+i7ipO}O?dsj7M#T&T=GCm56E_%MNy8H5mn{&<71VNnMaPyyTiNXEOI8LME5yE-jMX zL^D$;3@l``z*%nI^s;PXZ}h99;-t{9i*+uopg<4=VJA;I@oTaxJiUy6)W&lM^$qS1 zd{4-E<~{^nq_#XQ=TU$Ef9E_w{}OaCA`LP37Pxhg`cH8V!!;M=zJA^3Z9wsw^7ANSQ2_V?nQYRj%c4JNz5H51wbh9;2_qlem#~htN zZ1BZCcNfW)BD3WvviebO`S*UPT~3@j#ndU|qppkzgD+lc{(?toKFv2;CQ7=Dp6IEl zW0`(O&WNmnNCD9Tp`T%iU=0&#pT*g;dyNk2CKS-Zmq(_B?3LD~vu9s^^r#tT10a-= z0$mx9a0`)#d0tCl^ygV9 z|AG&qx(HDh8N~tL&Wn4FTJZc%)kVcIukSmVFN$)OPWF530EL{{r$bBF=RKlg+vwS55D|g00xSaGVtRGwR z*luzJjyO{Rg&>(pc0?LCie0*Topar09<AAKxUpup_8x|aNYI&aXr$HGY;ru$GYT1NN(?C3?> z>Y@MQ=k+D2i;Ul;yLa6YNKjCV~0r= z6*RVXU^=l{He*E!UUgfXQF-RQdBd+32P{mBpZo0>IOdTdlX~Kejn5&Mn#lb=NKWO%xupnRb(9?7a-p={61T%Q<%;}RSncrjz zw7R_f0r|Kn`WC-wBC}7R2W(|N7~>d^8bnmR_@?$C|K~QEaLnHO+zWf{otZ_``*GDn zUC+z7(R5Biq)tgWZV~-Ng>= zqSn@x*xz|OWNilC5$+%J^X_@;zFW0#(f?VKB|ltSOASnY zfuWGU`7NPgM%2QSw3@)8B?wFgYKbwl4cw{-o^$1MJ@y&{4WU+)S=(JSO%mToaBf}z zFg81~8R>0cV}y;SD;NuHHAK5$ip0I4){jarQuG!ER#0^k{gl0#G$x*-kjCe%-nALQ z6iRl~clZndLb>c^NDEoWa`w`+KLGV*nTpQ2>ca>A@3@MyOX4vH3qoQ9>>`5_WbY{x zCxTn3gk*wPCct#lhqJ%#?AdiJnc{)9<7+v2tt+eC(MoKzw4BH;3oyT@(g9sW2>`7K zT!rr{bOsSqveVKkiK;f5e>k4RcH$Ft33fvKeX_dJuxYzSO}E*O!jZM4ju6uvnzEfb zd`EOSBNv1VbuLV{bqzfQ9*M$xB$w&*-xys$LQAy_DfH|uYMOl-1rPPy$5kFbtT3Gg z?Fn+i1V@o^9;Nmv2UC*Uft2B1l~~HVBOb>(B15L zo{lIe?^2ZycX8F^R09xZg3og+q|h=-Kqp323TMtNIK3Gw9%2sNvx`3O#aNB*?foVQQU}XEsoEV0K4FzI=wdN4ySaF-j=V z>jacC%MlcY@dEj_I=Gcu~)-3+C85hm?khhL_z_?FtG>Jn+5b3yog z%O7lAI_Buh3c;LX^5jO&Ilu2r@pm6S^cy<))`bh4^nMVq@GFMmB;y5P8YR3fuJhv^ z6^92b_Y&P1(z#Mb)DigHs($*|V<{!_EN4mb=t59Wuw+Jo)kaKzy{zCSvJdZ8AC*Jk zEQr3D*G1VcCb2@KnnBOXpLxBJ<%(8BpnX4%8DHXF5c3o|)}dHw$MliLmGTXAcSi2DH6{@O3vZ@MQdy=H&X)8w~Up9&;sZu)T*Gm>L5hu`t2R z@wJ91F@W3819!igZq)4?WyfixS8FFpih?de@$lfWm0?E*jDZV>p8kzvOsUOu<))wZ zyy;zlKuGUsfoFLZbQO80Jk3JfwfeEs*1t37XxSUXD$v97i{c4W#1q!GWMNZrZY6{+ z1HTvsx>e;|Ag0DDox(K72C5s!?7izuBa&TLh}?!CDa7~XO;*=^6S&{Uog_paS^x5h zNrOPVgp)S*uerEr8(f3&fN-RzUodoIMu{VM=Y9eN&?N<~cA`X5KLK|KFTs(Gbj(YSPHZJQER02qo zFlj&LAGTHoCA;xzYU|8s2z})2m=80 z+2BnJPia;VM6I%JZ}l1LmCQ0Uo2ZpUn5|)_2eYb7_&T!`;17t}SoIBzI7bQRA(Y09 zcG1^9Vk}9UxIbY9u!CQys^OiDq;o@o`p&s=`JzRn=9v~0= z3!o>CMn@ob@#{Hv*7Y5hV#oYeSP|NIZ%LVT$iFVnAFT92Cc400!sl6uBRj7jf|wv; zZoJgzZxK7wb^u}r9zNrhb$Y)pGU>xbHnlOt|lxsqlTMRG!$et4m*yu3Zlp@T!Vm0-AK#Li@{ z>B$!#$P5%cxFNC5i#;_?m69K5yZPG1YC5=FJwgHK6o#1Pdsm>JU-=L>FF|R|7!z|H zoKjfqMGw2y&4{5^*Guml?kYtYTkP+%wCR99au0p+n2yXlvJW@P3J>Od85Zy9 zNXqaal9lv@C^xVpEizUo&*J%vP1aUc#8VPH9Z*HYZfR<&d)-Q-$(3=ivkS^7>X!Al zwA^JwlBIa3$gt6j2O2UmO3TQw!+(o%O&F@S?MtUPe9}hA0e3{ZjDaX|4x>9*t1hf& zplBwKt>Umg-S-EQc-GZCtWXfiAw43D+?$In;2}VwpibN)5%1`nnw&?=m7&rxlkVW(JkuWAOhxa;l-tI`|xTN*y;|FkT9gmHwbo{C6xW4UUL-sq>Xv z$O$qvb2a$e$mUFBnE>NWycoJGro)wkkxT7n;+z5iu&FhEFrSGCI)x3Snj7XZ(Xwl2 z50UXpN@da))RdqQMtF#*JlOOqmYXVidS^^V;yw%njQh=X4>?M`t=r*Ts=(crGx`Xba}fWta%vM+*}Ax(w#B1ZhG_Nu8;>kbU;;NndR~cQlHv`K0Bjtqe|W$5hGz52KA|||k>Cm?B*Vv#t-ERS z-x}C~y>mDOjy5nb(Wxe!8DJ)!snO{%_x!FM-@cEIo-4`Plo95>ydGMgt(x8ERgG0^Go+3(`Py&e0QJyk+au#}8YH^P)-3U~i#ey5BPEX%2 zsN4P$HZ==-xmgg7>W0H6@znE+?I#aS%$P$3jQ&Cv>(@mqR~p;@op>4%OFIsBot>~V zjmZP?*3rMA?#Oe>u`uF%$rN%r5e^@!p|%~4^g9ZD1Nd68TC5He8`|1Zg~Gt|#fyh( zz%hXfQyZ~!L<&gF3ZzifI9k|psOcjsEZWfdwS`2`XxK#Px2CyHZ6Y6G%Hfx0Mb4eTh0Rddk&G5Fr)jv4fLJfn}OG1n!!-M)GiCm<&T^cNN#>X^Yc zE|zE>;>U7t`+o&UuB@&$v3IEXeL1E${k>Z;vt1;d>_X4mcVVd5QZ(`D<+c-iT#j7S z6EgAr`!X$SAkE-RTGn(LlmyH6n>nW;eYmji8TBg543^CrraJjRp#BS(8kpc|DisB}lXWRGqR4%cJj zd0()yf8w^n>P zxJX9!#gBI1Olky|jv3R=zvRWJ#bS9Rp?eath2Bgf%ADWl%h&(@{IorgUfQ)|lA<>< z3OWr~i}ib15|`xC!>gWu>n+$YP<|xIuG`m7W)a($oI>RLvZg$;(0RSSQDL^QMY7aL+@c2vx>+K0x8r{j|TG8wKH+xQ6Cz&0n} zL^QC^+rNr;+&&PN9i%jignp<#-yyhZenP_FhBcy)%u9LSTzWUI={8ZORTcP#@|Df+ zj=Vc~4WZ^qbW>mT*fnc_!~Jb-wm zHlYky`*h!smI&v8gC?x2uBezDz=lV@xO*jA!4>G2u+LCS15DXK!IIi|(yezsRqIVR z9c{#VPUfO@yH&jTyu79f_m_Wdo0*Pym>B@8ee>o?{P4NeWVX6_{9|tUg#4;qkF-y$ zNta>~jY1uLOf^ZLL0d2*zzwDV7vZw1Y3bt91UxKl000NYy2X7Yk-Dxo&B80{H+%ck zh2zOKf}(%v-cJ~ZXllwN8U;VMvTVy?Xb#@f!%Rjt#?etE z6+i7Qv0z{mu3d_vxrX{nB`9B>t+{EP=)ov)ZJd`~$AT*zXKNY+|(!hmOe_6WXmM zoGz8r)~aiCP#uvle`^2UfB9!4Eg`*GesNRa7p4OFjRv^P_{!;ohw+lSar^f3rx)^vlO-r9 zr%iMNq2&^dh!qq&h|^L9S1e!^>jJA#P5K)03(AK8mTk&*0C`ZP6NW1~HoZ$q1T4$H zTuEGyG&<5ySJ#B~2k{*GjI{vH^k_&~T;Gb=oZQh)ud_SDl3Faxp+T$yF9} zw=$lB^?w>f%jg1(=AK)z0pec(I<+*6FKg*y62|8rsNgLUB}@Vq!rEqMe*lJJoR@*) zMWgr)K(z87bC(4QO8qy#GZ@b_meNoLQ;xa*NL#UZF?0jeD!RxM4MY7}vfobY-vGa3bZI{nbNnUb`k( zYH`d_x-2-`xnC9y9fBsRos_J*NFUzOm|%j!m=jkaA9REdSLj-y!5wH#r);Ah0*ePC zoZI2Ede{ePkE2_;Ctc57>;f)EXDqL`XMmC+Rgza!7mkJ7G@Hh(GgAtzux3-Tsld%&}3Hpt88tHr*rD)^ZJB~#IJv#nkz5TU4 zri;#m0P<4NC8=q#c;^y93&8`LI7R`G6hWTDQ3#UiuEXE<5{ZfzQtuEs4UCP9DtYpZ z!O(BOvoJo2R0YEKV%|4OSYVI*jO=4woeeq2?6P?aH_Ia9B0yHM#hO*CDE08Y;g@*) zCabq7=lZGe@EzKxDb=h4hgQG2{1Vs*Uj`nbv;ny#q-nwy@`ndc{sAXMBoceP^XpN> zO$2&EBTXC7H5~_dgXAn!zn8WEsqq}p2&WBeJ1iAWbq|igOrSv(F&g$Eq*{heTZ&0l#{T#0FS@-C@- z-_;Q`fzuw7jWtf0aEy7G{Bdy&n~WeT{=X)eU=b*U=IW?G9=h{GXa>bEz;5;z!lLc# za34J-!Ni8j{aAG=m+8z$J^(%Q6J!{Gm7Bf_-?Hv8v5jj?QwX%i4#s!4*G$=DOSI@% z%mSQ|A@OT#F`}+d!?xJNLv-s9ZDk<_{6NJmq1|J~R=s5@=dH_^|DjX*`yuQ6j~qc` znAVqCvi!Vbch(N!V@hv$Kxz*Lltm;B*B)LIx0f1vFJj3B@qoNh?@VuOdREu(^Ps{p ziynJsLA!pZ2P!Hm8Xz(IV?3W!)QaLK>e)b@ZxY$t`L0VDTB7h6+P{A^!aVz5+Qfb| z;xrmOp*MC9h{Y=2j7IH+ed3e1x=w{;0^b1YyKS!g_RjgAwB>UC&%@a{_CVO~CBk<< z1h-Tkq1O`#t`@?aw7~$04TL-{-{YmxG;o6Kn*ky;k4;Gvfmn&X%u_)c$ubs*f6e>p zs5Z!A>T6mN>_K^yduXH*}-$v8GG-s7f7Ty+kWz1Ixb9u;E43a zqwwU!KjWW&F!}sd`*|<7?2V}uZx`p*RtNP^|F-S;anlN_c+>d0cyZPX!Q}S+x}r48 zS3rl)%`*|6-InrDEKh8#D_I0|+isDM)9QcuuI1 zClsAax@t1%)T{sHqESUool;$4{F@{`REZD*m&t#+I>zvtx&GIRA3uJSm#fx{<9FHR z#5rT4fc@L@({|bI-`_@kT+GU0%GAFGqqtkn)zKG;>=gPQjAY?0Tt<-*j1TUAWGqGR zd+Gb(ey%TzK5@e0Fy9e?Y8fCe@VknN3O3XQ_Kg%(xCTNbQIA&C)NEycLXB7#(fRS* zVr*;SJJRYT8%n?Z=*9g(a_iDJpRC|EsST@L#eTz!+a(RfrELJ~X_g zz?D4C4OwqKFSTL^fG{~{)Yz7eWwx|CQ8VuKEVtXdId-$3zAL*kypv%HgVJ8VK4Pd$ zEWa5upC&-_X~Z%!GlMsUAHWPtP9;VAG#$#DTOYjb+&f?I z@OFg4U{O*Tu~vYA3|XTjIea>z>SlTssyBM7=ff-ndQP{JSVc!pIw$%PVT#9Z8I5`Q zV)o@ktt8$;v^hFC4U$Qz*a7CpV2Zm-c-NEv*47@Cx^eezXtli`?BVzc6N=maJUAJ& z@X}WP@rgxp*SW9zh-Ri*MeW+POUi%=!P14xAQGg64G0;=+oz5DgbwN6tW?rQtd+vU zwd$dJzr7jZodZqX^szAd?v}697Z@3B%sH>WH6qr!|J9IEmD_jZ^L)K^Wc-|KYrJdR zQw^`?0g;TjzgGK)#LS_8nwk(jFnr?-%!?)+*a8fuVyMKM;$nCBBcK&CI^*SMt;nR$ z&JG`cg?T4@7Es!%hrTRv6kDg|nQpAUKruXMp_r&{XZQ?vH{e7c1AH!9hW#9THRTxt zD!0}>X}-FrVQO`r+os!kk3`pejx_qaNoUw-8XU^B0eYEdVafH_Ex`s#Q!Px9C=C#o9Us*F9(t)jGb1eE2<(LO)#oDYFSSpR6( zrU-{UxSvqYDZ9_B4)Pf*6om$ai>Cg*seVKOQ#h!C_wNTOJpi00O*49RDA~f;Nt&M} zeu1hO_P2M!q&_0iYM++k!f(P&2`&)Q)#yHbj_~gf`ufIhZ6MdEc*D=4;^A4dn3o{3 z{Lb1{ix(?pj_c5wW&mUjOU@O7WPI`jnxU(M4U`A*2fhjwdWa zocDq^Cwy>fah`$GYhz{S*Inzzu7+qhc8vQW;}98U{f}i?8_>inIClsjG_6!IT5?Pv z2P9iam4R`tk-M2$RJ6|Ayp68OU=Yqbvm$eSC5Idf3tLH=J$~E~cXotQW zudY5aBojePWuaU`q?Qomqa`oD^jK}W{k|0!#7tsu{My>HT4@=sB zW{r!nR{aB#P~M}-$R)2<`AuCO5~gqxTOKqqQHfE#ef_Ne@vFY)9q^8bMiL~bA+GUz zB762L+6w^@U$dsA_0LY27S@KTTb>(Pc~&ZA%JTljV-7{9I25*rV zR7Cxic2&F>sEfOTKK`OCi@J90(8~&r$$sK>!cVP9leVm>Iou|JS4Uu+)YuVLlIstC zWpYZYSzr|ePh#qDdX4G|HsDGbfXTVcWV-Ieyo#IV?kzP_iauf74DOM>xBP=y&0#(H zu9Glj@|G$p_Wu-?S|AfmA4~h?u(f8)j@(lFLEB!&jQz3NIx;{)_eDuJ=uR0H)XG;U z)x)2OD{-hA8g{6Mu0h~=>(;)i9Hn~O5gTj9m>wJmUx~l{k|kbG?5{fwEU@f#-CWur zVfKXe6Vlp>x8dOHtG(vw{_wE929oP*Uc^b1R@W8Ab_`hcSmHy3_6FZrQSe9pn*45hx?4{WCd2cyvJa zR>e0NN13?g1DjWsyqo$~MJc`Q0jLu&*zED)ZaP0+d|++qK`AyFeAyAdYnpP*!8h}ImIv; zJSuzXGRxCq{~s0(HRV;Yd&l@uqmVWcT1nOUYw%us^X}bq zx7h)pq>G&YPjO zqWa@9ryx0^kcVL=k8}95@4OP*boTGbNKLgxQVd-tjkgIrtota zO6@3Ru(e6ke-=#J-R^7Fg7ts_gk-XC;LGE=y@r~}Jihyd?vk9}N;5>d1n^+R`d%QZ z$(Ja@oIBe0+poy4lpSZ;_aLn2T-|#S#FhKZ;}a%uE@|2px@^gkvi+Ghn$XQwxDYTu z_`1tY1KKUaJL0kF=@uWNET+6T96izRri#Xz>Tb_gu3p_x>xvIe?wgI$_w6Q5@ekW_ zq>pX$%OkPFejHTp`b*su_bZW^SThRIe68b4--0rE*niN@Xra7wXXctnNY?|*4D|y{IK?7KRDBy&3b=cWQ;^a`vO(RF z|3})NN9DM`;s5Y$N{Ag{XGlsyilm}JB~y~L6VX6IvrLsJl`TWNl8_R~R0#|nwVr1^_aC1RTiy3{U)THnKF{+wj`KKoP0#&$aoLU+ zrY4F9lY2E5T4~7k`PApxr5k_B9x=h-@AbM@BW79|wp{nVba2PT*?O+Iy@G6X_!u`XX2UVkk%h#81NIzihS z?Fm~SxxGQomnz%hG8^xoA(h%9qmsE({ z!Di{5Jao`1H?n;1^$&(=xC}OY2U)-)q@+YpobPV?Hs{y*xL>wg8+);K=IG8^6(mI3 zFKdq;KhAKGz?ebf+~>+2o%7~YVTE${Cx5-M>euaOR`(J=AYigFc$C@=)6oJlolGUD;PKcUfu6w;kCcL7OUipB^esiG5v;0gAh~Uv$PD~ zv2>|iLH483E#M#1*naW)ueIShXFjXk;;=D3Ha0zdVqN+MkB613MN@o))Tq2Xbl<*< zApJ!}wpZ2!kx`N&XB_x-Va~0+Haf4irl&sK8+`b8&<$$-B}*Q_+)>b@J-c=VNhyyM zfZ;vOX%i#wQQ3HG+C)A(xNjj1c{~c=+uff2@yyrz_N2N^8Capd4w-r(IT>9jxX3sj zHV6#*Bc}6yy#<$+gw_qb4jrzC_6wXUkofyg8#{I_1UTg9_2bnVWbL4opGRp?MKYj? zkt~!fsH^&#^zg|X-uP~_@3c8&?ZMJJLt_J{+jMNX z+^5n@Ug0u5_a%Fcz4-V39Cx1|u@Yi8?%i7e?EKeX8`)_DSQ=HIS|CETnbNveF6YdC zmB)iNYEO_`;I`Sa>e0hp&9_DNKh8;4En?Wy7WCanLkOZH{wtPv*n_57Em#@&qqA{H zOXsDrNtK;Ol0nb*jkqCt=S{;Y&A=#A;$r^w=W8%x6)pRsOlL-mb)W}g8)b+6xqg<( zQG}9+3H;kzm%QHG5x2du=rudl&F%RkjRhjnM%_N{CcqYM*Ew8Sy-IGNzMN9XQ5nW{O0N>eYRL#T8_RPrdiPDf~_KgME-=a zSj;Y0M=>8mk+zD^xA6Mz{rfjB_I7m)iQ>!Rd?DBXSX%x>*hNnV1AFLMU9jeD-lV6> zqP9)sBDDHqN1{l{1z-2(jpCx?$_;07Pve;eN|D?9n9<>XXPk_UmHpv1Nb_iW`y;Sk zJAvF?tiuqNpxF%KAR8YSZX^h;yyoif#j8=%{uiVkF&28kWZ^XG}Lj}XJotGKz%-$7h#zNSo+S7G5h(I7 zg$EIk#+O-Co}lFh;2G@Zn*kaev zN5X?p$1N{xJ3E4A=1Kgg;4`>RssMYq0w<67cui*f zpbWzh55SZc$f@GNVHn~|OOJ0*?gM46UOm8{dfoHfs zpRPO_)79DX3&9`GIb)eQS?srZQ8eEw-R%fExZpzEwWX#@czb(aOG%kZ0BOqTCdyCl z_utP2fpGX^KQ{~uvgDQeU_yK=67MPznifu97!IR}vp}`y{7+?kfMFWB7;ljzq06OK zW#A-1MGuqYX^PCRo-H4WXwquFa5a7Z`;~yowKlvI{rG~n0OCVHY zY5C5YO&q{HckolX@nF0cGprIds!z>C-N!t;%Kx<=L}g3Vn~>bnZSTc2KN=FJZ^RJv)Ujj7@aH4=!s4HaQDjS9f9Yo_ zYDQCtx(Cd@c{%f}1miB9ej-0FPDFA7l?fA8rBvXo1t?4hE6~`}ROf?y3(!u(cRs!m zfZ6}9nLebO=-pr#rI-5nCjq6OiRmTMkAws^*6Ahkn+pzeoZw3D80i?z>HGFQK!Jh8 z#sg^y<1A*+nIjh{=;WB6z&J+vaDGhTng^WJyGtUG`I=R$ns!EtbDspVpO<16!_f8- zx{rLyixCcFl?>teW4QV|!T@wB0TE{B#tpX7bkujOQcutC2+tVhhed#`d>^(BFhbCM zc8_154dL*PY`lq=Dn6oM6}mR3PxpRwW@N$tQU`~n zHZ~@wA2Ls~`uTI^7jJkNPkPn;ZsfYTWchN;O+LasA%-JGlaWgWv>G^OE^-|O`pD=f zp%n4nfP|9N7IzgSK$3zmhK{hJ^$O|PQQ~jmLsz98X3>T%uE=V<;|%Q zVjK(mf}+%64BQ`zqV@q`pFZ2#oibfE}+9xri9|rtu*xD5&Dns##EK ztcUj=t>mph7%H2t3-(aLdL(!=u`o!Ihz87fB{lVCcUn$XbYX-mSF9MY$cuy1>u=ZH z0#jJh#G~5r$4Z)abD2zGc$W9#cH*3JbE01mn*zBJhdom4TC9}0AaJqKfsptClGh(( zp||*Bn3@8e!l;}pDl)*yjN^>GlepBWLa$oMFcjJF_|!<;me*NG_ohvYd^)L1rDNoV zavE!>ap~9ce*#7Ie5fZ?ACk;?fs1cK@6*utPr`^(R;n zMo~!*w3>&s< z&mO57xP!W)qx0Mv%g;YSm%O)uhy+s$$6f1+*?0xlH6@p3bExp<1sss97)f(WOJ-A7AC@T zPxtZFbw*m-cqtQL{;4c@C$d|)Ti6E0;FD5dXE*tXi6Z@aud&^m7t%>=gz#9Vyp(&u z<)Wi{?9Ng6>4lEHFaL@3CUh=OuAXr?3~UAJK&L>qGFJjU==t-wruOkYKb9MPB|Y8Y z@-jXW)dV&IPVVlv7<+BHFFbtUkJxOKqk{MZ4O*Kj8@WN)rZEGL7IjbtvJc@Lb{6FK z#a!H&ddl^&vV9u~2F;0qGI{p%_AlGz?BGBxJwIgf`0@F!LnL+?o_y#;^y?n>ohOZD z!ll-3O^2qb^#3A^b>8sGFmQi%wr*ph#K*m{tlF^TAN$#&=H@Q%)r|;>pI2FWV?`+* zLK`L#Ofj8lklVw&Kkk$~5XSW$*7w@hcYVS`r|va`Y~b`gF~M)wt$*)CfwkB!bmoOo zQvfJ`Wnb#l_0B?;o$pPEr>Q1)?nm8>d~XV|l`B`GntehNt*n|$<%S-&c>!YJ7kix$ z_gxYq9u9+J(vLF!xvle9Z0zJqIdRq9i8Hr;ZXYL2xKuWAy$2v zgDD?^haAK^bXD=}wtamYYB5NRE9^m1=GTogf%pvmMUNX5r=l2~C=QaV{W+*gIuoc{ zZ{C;?h{$3w9THGWya0k2g7MMX8=7pgl4rQ0H&?2^K*$AuaKT zNTkqRxYt=aoUjyoaWTE#DdV8xPxFMcW4EsEuRIh7%usO8YxY#9aBV{mx67oN z4tzo*1=*|wezfbxNySmH= z-xU5fYVHMFXaLS5XlYIaf2Ui#&p_dQ0$KDB=H94ONw&kiCh=Yh0#QspD}IvHjGe(A zS=9h6N73mWl`j}8F;As#ocJLN)5Et>RgqeK|}EMKBYm5N*)mH1qVsGckOGsE?6}$!zid z!_<1p9XfuTv17YpZGhm*A3eH0MrbBM+S|64J^0T*#`w)N&bZ9)R7%h#HhMl{M7}Np zgGeuV;fvM4EDH(2y3wQ{$SaolklNYW$~11ZTtV_n3FxMob?#hWp~Wm87k zxhZ)cnXEMKpS#p!_+Y?(xp7 z>c((BJuefmmWewb8$oYhHdrXpbGJ)nq0rKS3os}gSOx9D4Ox!V8Zjc_#vTytYy_?0 z==o?ic_qcvLr~Em1ZBdHSN;5#@M`}*nz&8F$} zFP+s2g1}EuD9INrA1n%x1pepj$MJoHKy zPM$sc6hRl%P3{y!KISTpR%_@sSm~V4|ZOCB4-WLHjygM~CEQQ-%#h z;HwRRH<9%?54sZ1`H3M-UqgiJedWGkKSJMJm$dSonfAX;vLYD+*bBN?pzgQE_B1;E zkP82EB&THU(Ysyq%Le-FA00Hib!LWNZ}N6vLzHNL7#dL(>gZT|o7^sW#L4X^hR_5- zBQjc_7M2R|DS7C}S~6zAXo8BhrYXCtZVK8-Z@AHYAbF(n=JcJX-{BA@t9MP_jfq=Q za`IKi+r;&=3^ao*$EHEjhX$lTME{~y-^ z;!^+6fwd9{VYLv#_w^?PD9lA`r=XyquAaK?Ai;FF+?n|Jx9IFBI8K@o-qLY6;U>u~ z76epsw4^IG{drU83sxon|44Zvs+;9G4}}hywL#f;r(u}~y~z2-mQZN_qfyJTO8V7; zKU*&TyF}{XzekY0nnbDYFilvTSVoF~mnA)KcIOHCb8HrOiEm8)U$i>COpCjJuF2W{ z3m>Guzp^+c2phDM_($G&wf|1r?Y}UGB0xW_=5!$d(yV_8H}tUiEPCzflSBraNQgM< z4jnso|Cu+cYKp4x(#))^F@b;!2v~OPg8TSRm?GJYYU&auV0^h z@nQ!>j9f5Q;^fLW_{=vkxsaF$R70j7Z#EvJ;d*z(tQ5Z&*of%x5vIMjNa#Sj_d@&MSM@cPjdF);-2_jYyJlC)5La|jnY#%t;wmD1Q zGAT|aJ~=roZ?M)u@Mx|dZ6_14)b6imcNYl~#;459__2kQ(@=n3nzKNO$>v`ly-D*f3hAfSkez8)uhIhNimJ}f zrB`V_ic+JKg00p+_uFfK+UZj@` zbG^of8zGYP?sxfAP4K{6k5`TV*l6r%cCiefdPHq)U&Habx+DL9mp*BpX#e=))0A*k z5MLd`v!v^B&o^)aj-J2)mR&vjqw^Z}xOYA!EFF8*7GO_D)tv|ZxC-%@m*>A>!zHr) zusg$8Lz=>AJ|7JlRTOrEb>sz9SJ#!XfG#&1MyW6PF(I+#gV^55~1L&9&piEtl6$y8CE+L z^**@|LP|aVj*(d-X~p>phaR#9)4lK6@#0Gd&fY(B_A#C1NeKhe(mmbIFI~5Y zvE++_A6Gsu)h^e5ywpG8lv(doQ}OXiXZQZ|_n5DT_6I6R-1%zXd32nj+zE*t&F$Hr z@2>dppz7y<94Gh3m97IZq&-Pi^R)TPy~$Y_84XV_&&u1dZpIFS6Qzc>7qhc%5-sQ~ zq<|>uesr))|2M!MhW`Sg(dQ>jdY6&G%6lq^+__^uRy;_|El!Ayu$EojRzG)}b+!hg zr-r=G?ezb&wxoIwiZd4s*preH6E6H<%9rgP9W(Fo z`>a+iOZm5VTz2-cw#5UT?}LRVT68d6H+o$mT546XKmDN(hP*=tlTaF*B zy}B2;cc3I7o1u8<+@mZw@K$E=(l%f2LYvG%pl$1g8zx5T;nEm!ulAO!WxI5Z<+Ml7 zqOD#33LdwLR;E#lPMR6TnKy{)rNja*Hd1!OSVimWGwp*}sRGFyP*BO-$$L0>)kfmc zLYI(`(3d|$|60S%0KEMy4zjA!%H!5>Xc(bi{&>mCiB;^#RgCn+OsXqxwdt{jD+{sE+0I#?7+0ZmP5dPADW@oUqQ@(k#Uuoy-|y z3diL7c$nAb&7caXcxsxw!9r=tR8d)|6OTF?e*>l*0{;2x=}(&UiKQ7`#m(THC4-Bs zoTZ_lu!inkr3hoG4bXawVj8fg=GCjqbSdw2MiL%AZp-6`Tb;bX7+R;OtjH;R9#S2> z4}ECEA-&djrZIh2c-7#3s=)Riv$3`xpakX%P{-Q zekiCiY&zhJ259@6n1tZHw7E2;+X*S8sEk*}tjhrbd;Ei5qWlZOEP^QHxR0WytPc4z zE-C2!-|%v~3|Ap;iLYh9kyutr9qpFiW?^Y5l{2RXh_)fKV#`dC>QrKpdt}r_xGV=~ zz}W3SFoCh97l)>I0E3S9M*pMQlSS_g5ZKMw8r`27>_mj>r@o(`FNY!uotuR_FN9F& z++Un%3uD5!5`5w{-w&H&A4u8*jQQ)^d;H1g&{wiJK};k=@@Q6^e^i@NX7kRCmH~AS zFljriyxhKC`FKBJ@aMya@${-_Q`MFMye(5coCqwTI!zkZS|su|BwZ!fNV5hl`u5X_ zK`S+j^xMy9!H9VS0x>JWJdw7nPa(6riH?u^T`C>lfYuQOPQzhh=r(94?Nu~1H=_+z z-y;xn$;rt%IWy$mKweaalVm?}1_w#l)kOfl>_eGp(dS%&ohtaX=XH)oDu%O9T++00 zNvG+WQ$u$Yj@Sk|o2s(6a!56l9P1{!KkE2@*UL0Pv+-r_QSjw}}}a7+wrlwG+wR zEG}+@m#z%&IwS{l)d=}gjO2b`SRm$V353@zYMSn=$~!gNARRrXqn+^PI~k z_t?C1M>Z<^tND zcPlEEX9c^I>CcC{CS6^kG{h5WLSePvucVqyVj@+a|F)PxLaBzvIJr0E5{L|dh47MR zP{FvT%tGeaY#rA}LgJ8d94u(u04~H~gEt_mRiFQ!DWW@^+`ebx{4r_LgUU*J11lh> zS_?=DfgxF-+6`17mr_&9Oh&x@+G2nC+N@GOef@u*o?p8u@rAp%9GEc3GFDB;00x|$ z6a7@vuKgs;dQ>h~e%x0(E_x z%@STbdW1cA2R1EZ1C8PyaZST?$EB%X?kKZok&O*OjrtylCCzGKj=P)NAtQxA$x&f- zFJGRJC3FMYTPQ^=Q+7~0;--1jW&Nbw+~CX6X#g)>z5ux^6c3{0+F?+#Y6}m%(5Ru6 z7{M9;!K0U$n0ay9CsIQY%gWX1!`VN9k{LEta>IIvbbFI&=a<6Y0=YtlEL&z){lg|H*MRByog1qf?P3E4 z3zb8>DJo^m{skwu?=_&LvLQQjr21ff(S$Yhc;9*(W!ShcTNYI?&9C)+LYc`+bR&ufomH93{!d@0 zCxA#&8?basaxG&t9?mL(yv7^Aw>24ka zt#;Oh{qv7DwiHVynnO+QEVzVng^ta(W>TtwhH!2`j&FlcpzSGW1~Ee=ZGJjA8vk2u zWCJ^!z)xZ7Pp&|}RNuzZjP7OWaSWXWxlpS>cUIx};^#v!hT=)|87$tDltgz_F&5;y zx@~chD;|X9X@8|TbtwJ+~x;G@NR`ZVT5#>VQ?*Sn=^>uoy|^2~xCl-y>M zDZbu$-1uV+_|asmRHM4(90yJ_q8*Np66uZu52s|KBIpDn22z7s{>%x3&=O-M&YoDm zHk1tjC?64)VE{oc&;Po*-m||<*+7b1ZZ{?zg#m4>tA!%^?KKvnH~53t#*=Ax)LhTC|n zuyCLuqpQFrsd&R^S0a7H`CK|gR*gF(H%oik?N7HM8JTJzY1r<1lPZWd*2k?=7jbZL z>T)heZ&w)<1QLMDA`q6uEv!@cH@e1nnNHn&R1%i%djAoc?{1@;`IXg}TYBgkVSd54N{`S4vO}h+dE(~>Vw-tHJ92B&MAH}o)kLXcc2YO8U7+>7le6HMOc-&Z5lLrd|HI5%KVY0I$drLVuXlpRb>Qg=paa!}FvDik2q zkmj_~Ye(N&uVQUuqa1lJ&OFg;b9U#4$`)6S;7 z8Lc73%ckig`Im*eLLU8@**$3XmxA81CTP_K4OVhqNuq^POlLY`f2IWL*xwW{x(@O2 z*^Oht!)H^5FS#)fh!-cW`tT%_DM*s>!d3M{;2`VYcG>S2E^xW6^%L8FfMj-pwR2;YJT!)^XM3~ktYXIn zh<%qh*Rerh``4}=``y6L=b@}%`5YP+hQ8VM~;3yUH9Z0*!xBb68oW~Z9xU+_#MBA=hMNO5S{VRLcl1fXZQLEE1_8?MWPaL{#-l_R5;Fj)uSC-83&6B5{rYm ztWlOf;Tc2@UzP7;o?YEAHV~-bTYdS0EJ&$R9^trg0*h`iVpS44t9Oq$kJhN+d>2t5 zvkh=hK-n{F0KI30iul+gCh1NOPl3f)>vxry`r^Q!~|DICOeMdl{ea^2>S;#rd z!-~-%6&EMeuNoRWXU3rW#6KJNtm+A=FVNpI(`zM7zv8c&yh$!))SXI*bgHYa2fjTl zzkZ%eS|)`^rSd#$pkbVf0LFE+*=JvOlYF}yBrWYE3t{Q3;I`7{mRQ>Dokf|G%jO*R zoZq?co>ks8q0m$d;vVssGi)ZI)bQ zi-;H!d5_VNNDs>Vj4(VIuI{wo&lh|FWFJYvr+H%hSTpa-ez4H~gzl$0T- z-N8F(p*5zD`@w-ll21Dy(aPmyl~K<5@3FIj)hAj>83JyD`SsV!-oRewzMJ0=x+ew& z1#YW3pPcJ*X{arh?l^~u%eiSv02xDtH;Q^`U z>yhcv?d4P-B04^gqz!d%;ft{G0)&b6?uSJk@DMcrgjaq9&77}1luj1X86k!?!5YHb zDg=&?xJzI(+-spEFastg98?{3`kgI}{+;KmJ21Ii6|f?A^Qf&S$BuEiHIoj__sP{lDbi-=C3659wP-53K-#?B6d4IA2!0+zNtZlxP9l z<;ua$QG_xe2Y4rOYWD#5MB1a$)3HN4Z=rN<&Pg-uQCW11uy1dMTErxAR^ALuo=N3e z8(4fCiDNrI9+|DdIg79T+xS&m{uBU#^zcZ+v~ikR!j=S<{cx3pAH_HW!{+&?OmWFdL`4fz<8*bZbclc<9XzY!FB&GwcM~?l zChlD%H$}=Yx3j}%ab5TLX>2c9zvco;3f(PuM+_LHd$yV{u#!=Ekk0LuOU4goCd-E> z7X$Z$8q*kE;-r4k?CD=5kx@FtHd|AKLBcqZO=86<{KVViJ$eQVV4&MyHa5?nK6P?# zxfwZ@u?)EU?woqQj~royEN#kDRFTeblbbb#+@-C1uXq1QOW*`K)g-1@)bRq(m7)_; zU_3-ZX72HacODoM)xCW?+!xvA3*dDLLB+;nNZWi^Br2BTN_b_0dchV^oPW>4g?5>Z zjoW8iM6~cYVi3OPj@MVfgq1)So>*X(Xu%>EN~!$Ln)>?6=PRbqO`mCgzMO7lJ4@mX zE9BWYWHxn!awqnPI&PsPgP|ZTpD(uPXvqR$pxe&>hCZ0{ zix6xgxS6L08OOP}3o~Ua*>V9vAQ=}p7pdR92lN%4$eOC=*vtXJLnE(>IW`Eq7Czd} z9)KkWQ3#z#zuzWe*Dg$p4i-!r9{S_PANpv%JvJGW@zZb`FkwQ(-B`a?XUG`5Qp+-g z;!tb)^yxY}BR+20vIQ7q9_8=n`^5Gr!3-Ad45`Ap`q`j2GRn&?jn>eBQ^*p~4?lXr zM^ga?IAS!Fe+UD6q9C>n?!ph}2XMsWkaN%O36x*-v)OgDE&xNAgB3LF6zeazlm7Q9 z0JBtu>lQn5*6iN?xu7>tzF5{iS^eDg)p|Q^(Lwj!^_ouR=))vjz_LBX#GvVLX5V7O zgM15$5imjcH8zwwwYA7@8Hoaj)apwUS=>{nmfBhJc;NKW@g_Lq%2q`1CJIwLN9)Lm zMA87PtcUzdh#EM2hkfkZr;jk$c;dvVPj&DKT-X5CybKrdL4#g@_`q<}t>|9&8RLF& z;dk+`D(rLkZm}%V$hirncrw^snnuo!7^Tcl3zCOKp0#YExEW~^_@7MF6j4BnfVGGA zm(@EK??_=>IR$lI_4c2(+|;8*l#F1vFaWAct~X31vzdE3MsIs&=oFEtsOjg=WTtZ| zDLrZ&B;J!kw`k#rQJ`kalyezyEcb?^ylnC+!+(P(=kER90z4$JvYXLjt7E+evXj&D z_9zL(-8*;QBV#02jkaES#z)KJ-F~z2BlUc*StwzDSq%Iideg&io5)T~OhVc~e2Ut) z=T}9G|go3_s+`*&VKs({?`Nh2=%k2nb~IwzhOb$MBbW&5k?8C zKNU0(G%F4djdwoTVLo?B3YJpXQ?(K?SvYeU^{9o}eAE=avf{zW$e#~%8vm{RvM;M= znkb&u0k6{n0H%P-yyoJwKIj{wOdOECkKjlNvCj71h#zdhB*b_WOWe+dm@W)yN$`q; z5NKvY0@O@Wn(<|8X5k`?6)P22i;|4eQ{0rCh>lbw-+G$Y9 zipvQ>qIN8N^Ma)AwVys6=Z$4#ifWpky8WQ$Ac7~$Ap84He8x2gyxiLQS#xtUxuWwn z+S3N2V?a8k7q6oPVvQi5^}V#b`xdd+2skwC(Wco zT3!cFJ}W9Z_J%-Xu2MFy%zk%tm^)e)lmX0wgtS4*M+fuC%+%6r91P(2_KuG8WY`z! z-VP$wKP(z5OG-{SUrtMtfj)Sz)?K7J#zmwWL(&m@uNEKK`B_aABSXb|CT;a$<~)5- z2&t4KjD?Y@v>6!+BjVJ%>9>8}X&P*m#0M${N{rH7R;q?s13K04@KtZDaA3p48>Sl( zpn0PD#EGsnrAmki9yS&TQG&1}!83u}k_iqV7-T8v^l9WY?3K-y@zoQwwE#IJFyPK| zfycBxjW_ohu!rFtkE2JxHlHqiug_SZ_+8P`R9~St-^J=P9O%IV)6Gu0H!yJ|X9CKN zJ9EZJ;Q)mU?l7*dADE*WqYfE?Rma$accAR)V&g$tW&eCTd)~Za#$IZ~_7QpXDl_~< zEfA03C|`nOM|EtRIlVFW)~F%j-NwAHBPQ{F(UnW8sc*&Q(Bze!w3+nBfC1@OuCzkd z!4Alx4r5T0z{eTUbo160xQ#NWQX?uiAV6ejghxz#Ey}6D{rt6Fa9R%B&$+x%iq?1p z%gdFQf{R%t{rL8cT(w*;AuOyorTilCDej>Y{9*J^;=g`;VP(1Z?)BMCVsdM!Fr%yJ ze4u@3vZbZS4+E#K{3@vz4UX5)!sRm+C~>HZ;{6HI=&%vfyDFZ$Pnqc)>l**eH-Pa@oK zDcB+D0P18*v{}$^l#PID;(hw|-Bx+9U(fC_LtTcP#UBFRcW5x$ovpgZy=YXBk~>42 zwIn}&3%n9FCsG!iZbE20QQ8uVW5NsS*O25<&!eTc&s96)ZWd?W)Z!a`7+V`jL!#vA z?V~^zTBr61(0a9DI@OU&TBVT*0dtQ0Uo7^7wY@}k0@d!($3uI@=+Sn}sIX{ixepK% zRCWEj_Sqr3*(YtO;}9G_YB8KUq{YQ`69@MVnte*fKt*a!XcJCqh=pROZ!}aG zgiO&aX(@JPEpUtFJzX28u9^)UZ8K@wipb$1CrjT`)>Nu!AALUj&i*@V`u6NjdI^Zv zK1^BVli$PFIXw)n#t(B65-g;QjHQhK07kHqwlc2`Ty`HhlI@235>P1!QUg8-6KBfK zEg0^S)=Kd;+G<25ht8w-9_LQPq1C0J^Spa#0zq_<&zxO`NCV@v1QZgPmVtq2$@)cD z$VhT}pXvk2!53wsMA;;b?mJ$Bk_SqH8`AUYvT8+xHxmXDRj{A~EOS#0WsWek6@>km z#Ni>lmk_K+f-wWInd#(v@8&cFzAamB)dH7B}V3-U|5vIq>})DWYTye>+ z0JueCic8uyDo){_lp-i_M$Xul$|(ornpP||WC(3Ak<7qr+vKa~k1ZUzirNu;4i`Xp zX~VC8wUuW<9?=m95=b-g0)+#ltH?1AGu|4<^=K^<@vXlauF~QkZ=nRin%DAnM4A=) zqc|;p{K)|3{ufA_4l_n*)HSL(wBnJdq5IQ)9pSDn<*MC{6Q+S6cEnI?` zEH!k$AUI~i%HG1)Kq1tX+i^W2`mNhIFLr37j3`r$dK&Q9Cfoa=U1LDE9uQIw;ww6T zRd-^_hVY9yoj_;wX2iW`{wfB@BGd+GTi|%ncX~x$@%0UP#=8 zcq+w65^ctyaZ(Nq#+?pGyPirlCW>~<#}}s3gL*hJ5?j>UHBCUivuDpH&-XIHN*W(` zh-gGL&AyDudk*TTk%KaYnww}cIqkMH1t)@7(i=r#{gBvp%>-IaIw@9qB3H&Gx#axU zm+ufPeOop#gN4=b2``dw@(ULW%R`+)WqlnB#{Iop~`rR<<+Q zzy179^IjrVOZBvewLNK-S_j{S3JV@3-t61IM44n;6Cj7~Y&X>v!Dl|PdHk@p7fhX&X;T*C#0Zgevh zCw9Ec>xT6diBtzbm7UN*%{Ckhq^47;8f3bQ)&dw3-cJ3v_UO-n1FByd5E;1vECSC##fN|p*?;2w+~Mm83-S^pJEf$oJ-?F4Pa8ke<6Xi$1S1S6F6b3 zOL*QiB)rLMe%)^|-k2p7H2z}7d-Oas5>|g~JV-FA|De5lPn33cCND$0Vt;v=PcLuy zYq6*<-l^mYBP>^pAFTQ0XjGI@&ZVnYpHv$hEppgm=Rj%On<&FeWoq-8^Ll!*$!6oV zwTDraO8#b1Q%4L99*chfjg|q!gbGZcIWY)k{&6DKA*!=}vtpGCC*HOd#xOiuFxk^k z?|0?M&l+SouWdMIo(NA=-SnQJqe4*QRXl0Nj2!%aH#4u?+0ijUHr_lD^Q8sG#%E{g z#nSurZ2kmNgeQU6u@4wr8FqkrkwYs3~m(W6yB zz@CIpb}d2(4B}npdc=f2msG4O+di!9JY^-QP3#N*q5q?O+N1a1fIZl5f9(;z$nl}S z>B+~a&FD2}+Xe1tS}FWw1J(NV>wnkFXu3s(hAt%d6#U%1XAeBY;cZ2AUn7jidX55W z=>PkIlZ0oPVM8YUoj}9-ZiH^C=g^J^rBl+?eQwl1^XN9@ZieEq#p4#4>LX99A!Ksl zsY?k-wDF;%6Cyv#l+aN#tj3LI#2C-f-NogRe@}@EEQDKlR`z0^9z)oVATHra`U*A7iE?p|r!mr+QfBP+1JWeDM+dEqxx43%c z{BMVh$L3ac_t4+~7A6CM4Zic}v0B6?1*pQ`QbujfUcW{f;4#EnD9&_(O&%T}KhuSG zpII{jlN|pK0bR1BxwSP~yB8{oHEYh8CxSG*CANI`i8FULGbGs5n&)&L*e9g{aNr)2_qztNshTK7nEoEtCg@da>y3q_p)C%8#v4hDr?d_U!4;&P2m7RT?GEeZ zSyZ1V&Af$t(*Zfa^ZaYV3JkaiQHFsA$0tHg#B8Y@p-Hk^k~n*rSovcx%Wdj^tnPI( zTOtsGOtSgda{&E#2cUh#Yo!Y$hPd-H*|x0}OEGadP`W+(;cOvX~!4%z=jJaM20ijH|_o+5S^#DwVvxM(x?;jUv%h;E$JEAEJIo!b{F< zYpW7@j|%qt>YMgWh=p$2Z`{5;2NChT+P=F^0eT{Y13=Wz&CIMs_nY^kswx*li9B0{ zLBe3q?@fXju+C9cx@#8C13NvK&dIsW_#gu9j}7F>(W%4>$JRx9EwlA$bbQ72ler?^ zBS(Vm(az&{nkHWm+1n zzUFHSPZRKbs)s>nIGB%g$knto@{*GE>nBy+x}^xdIQ(9m)!9v%8P~xne70_#Ft~4q zjj8z7Z|}mbl9nn(kY#B|ucTWO=d5b%Xe_L(&Q-(NOhs)1v+0mR-rlQwH+Y)M%jl~_ zW<4!xQE>J4ZYRzU`^`;SNm14P5(tQ_EyeIccKHA_^wFlntH*dzFCuMX%4SkVHAnA> zu-&~?M41C4j6FGDesy|-1p`!*888b05@GT|=wCsI)o+)fwe_j%s-6GS0$9H~sCv<; z;?#B#Wv6QQnW1^u^<j1;gXV7HuE>hS0Xgu^eevTKx%l&sGV00n+!2^m;!w0)anoD*Q996SENuAHFLL_O}`olIpm1 zEDVAUh7oV}%?%E|htrkzk6WV51N0F9TYh|=@fG(8g5l{=k=~lL^AH|ADXKRY>ycj~ z+<(6)?cd_!pWnX=5|x68V}wLmgksEq$?}5CNMNwzpMNs$0d4~=2QIyb3hbmq=Dl*M zI-7W%DnF@@V37?n=Y>us+&B)bu_z~w+bth{_S&^5r7eg}3+*=ooJu^t2wT55DCiZ; zu3q)6Tf0e$=B$SW$L;-$?4qLF^vjo>KP}J=aE5=Qb4zXP*jgZ55_vK_s5O@RVr!Qt zP~bVlN~Fffa>aT8fA}@O>fN4DjjV6=`t|;J@e1KJ363`ZCfUMTwF;6%L4<&#yzcJ4 z>k4X`LX@rR(OD909>*4u>r$Yg@=_iO_hw$}j^P>yQ$aHW8(emg*~}S|$v35pq)LU* zZ*Afb;Z7#VCq8_cen?Vui`heVC?)&ulAk`kIq7N@-c9wT?$2G!sNjjw)apoT$?eg) z{c33$Xb1$d8ROJ&qcdNpO&A{PMHvMTn>wR68C6dqqcZq~^9+i>6Sw@v3Y|{_gJzp2 zGP1B8)V(ZBpS4bKO|_;=8aU{mf7F`4*|AdS3=F$nvthcM=Cj9-mwxR&BGe$*>(ld! zl9K)V_rLu3@p-PJ(5VU4^uxZ9GF_M0jd-{6ry;4977`$jYaHfuBvIzW4?F=MT24Kogo7Xal&$IAptSTO0JajD6hBNx() zu~{u`d^HIf?QecAoCHF`eI&{XNYFw2r9fC~sJI zhhxZA;EN3Cj_t%ZlYt7>d>je$MET-uwFbP6!`DPz+ z4Rv*FS}2Pz@#uJ6ytr@M6I4P-Qoq$OT5VR+T9C%Xm{8d5JvaJ@i$@_F^2-M5_V3gS zyPeX-6ptK`ka4^0q)N1PUnXxBv1Q{m)0hhWJM~D;{$dP6lTdf|-mP0gs$h_X5V1s# z%&`rbLmTKA38+8CC4lFp9sLb_R;fy()G8d3BnCfx?AV3ss^?!b5q_XoAwY!RSpzOI zDCiA9A%8f0cz5SYp+$}?s9qhNH6{&MWLxTYaGnY1*jp`ntzkw8c{>bV*4GaoI@F7m-Xi z&Zb|_U{z54)P4Mj3IaMzaJHeI zL*^`2!#+o5Z8Iq$en#-@Q*__p7ti3sfC}$kUz&Gg7 z8lGmXejbwYdI$Qa&u(0gl{p0%xeMy9?5&&ITfYMwVK9Il7tf79^D0`m^kZXV_1d3P z-Zpu2ulW*1?XPCVnImJ1iP(c*&~(m_hT_7)S}1+%j7?*|hf0a=5889%NG*^$OY0G?gSMofE7Oz z^TTrGc27=ou^L8lTzql8b`u3KN5kq%UUBKhn`qNPE24a%u%?N?=cw9A*-uQYu(P~6 zOJx+Z+@p`mYyfpcV+6UxqxN)n5s6+%KOQFLG!1RsJx)CevVc`W_ui{LvT&ODo8`c! zQ7VB(d|+^KK>#oZc5B#1(T$Do2M7J@4Cue1tDX-z@d6i6n(HYwqtQm&^4yK%9zj(` zWf0)Zd~Q`fd)B*GulLG(+YA3(Nue4x+?w8}c-lxA#!9|S0?%2(mR)#IU1ac6;5%nb zk%nD$h|=ySvL)@+M0)D*i6~hOZg0L~#iYS~sp`QzFG!Jhc>+G4^D4S`@AO;K#cA$d zUYY{TGIZ77LV0OmUD!29lcqS0KP8!0!`KxURZV0G^dbjJ!eFC(1&-PpM9MDEHkHQy zaZ;4KRfuD-%NX`tI|^0dkBj zNrMO>t}rWp^4Dm z9JRvNH#@viLrv|`_3Nn4r7A<&5{4%)(VSbu_=tB^m*x$~PqEJP+fL-V%~I^5XBT*X zFSkSm#V2rLMw zT;;4}dJrL6W2SGEmz%9TlnrmK)A3sj`R_)=~0UpT=8o7Fo$F}Ou_G+lI#2ai)Vct_&nbT{= zRW{K=Dy7g@RI**j0^EpXeEH*=>!Nu8@p$La+XxU|{~6&9iKX}ZxX5M7L(A>)q^WdG z=9Ij^SzA|!Ys3yos1Pt4dXy%QnPR*0=iQ+q?c2lm!2<2B=Yd4kGHq5W>nDw$P4`o4ViiWU%b=@}?SL0%W$ zr}0)?4&UR-^$egJ6Nnqc)h~uTaCkK2BrjgffUHBSf4c_n4pgG`%D=Ea`u~!!J^RIa zb02m#BR}e`R303i0V`%i+SpPp;M^H2i1TKRRfIHFR2JtFZ=9GYoFWq7~fTwHN^b?-elKuXE8ElhASl0)6G z_*W=-wp?J(UcH>Hpyesa84ev{Gimf_EN>)6Xe!T`VXG8@?M#2B8W-3xP&K+QXIT@( z>!wZGzRjv>-)d?30_FvqKN~=z19>G2tVqU}n(Bc%F>t>?k59tl2vj3@QMJxl{0%{ga+Zaa@%i zr`lwBg-QUMZMhA_zWv z)~NK<%B=d^smrp47k>_&Ry8`1(_mgHM)#mlY5Mb(LQyv?824&uqwTn7Lu;(tHo}fh z$J*?d1Vofze6>v3eTwxMzKKWfl`Crf`US(Zm)a$pC}I;tiV{B{F;R--JSJ*13u>nj zuiTM1A)t{AE@bg}dU%i^IJ#C9zS{BFHJ_j*9 z?m^#Cgj;3Xlia@Dfa4wufl3*VC%HhuYcX(tU|`RMB8~EelOh+lK(uOOU(4|f#(X9| zkF-KZP3?-twDV&oOmP1EvVh-S7xHkG?hRm0c*lkq?_)7BG>awYg!ElYlDEjyq20~; z>+)fp8yWw%)aSaUoWkhQ!pH)j&NFX`P?HKDKHLZs$Ac5|VSW)b6Ydm=vrd{#hu-E^ zn3_n6P6+n=C5rH6CJPq$@UfI5b3>#bMqfo@OxG%~{0pP)+p$$WF#xY!aXIY>!H!W5 zckVjpgJqMpq)oP5vErhI5=lGC)v(*815P|Pn53dIHqZ^8gy4wGhI5gEk#i&d?u!Ig z>6X_w`_z1BN>`j48!nMJ!XnXv=8X7V+v=mDcPGtkUw$&eQ8Q|ttKP&h`U4F2E>rG! zb8r!2B78;`#Nl*hUT>w{_^5cODDx1RcoOWiXY0=848JVNLiUa)A9tk$MAj(7%^ zv~#*U^~M~JtFZX1Qr@LP&Sn3?o#(98v)-DmTuE@aATvvbLq~tzHjbA1aZe7NnnX@O zQ(TL0E?Olch01PSEy*tPO>iZnZ35_J@9a}NmviOy9C=K1xEgJT7(ZH_p7`2L8BLjG zb`mkBwD~=8=gg_xGgl2sHccVulzeul&kbHF6#+UX1<5Rf@(-- z!^;50_{2nKG7-LX$u+CJ`5Kx+KPfrZ%?f3sXK@E&L z?SSLvDM--KLks=xa2+#e7N9p#U$oa*`pyRI#Hy;QP!nozdc)dgn^(cyB2vo5fN1EJ zZ&NnCeS1ZtWBI_-`UBXc=tyWeLG+3zUWSbyT5R-b8HL=neoPmb0~uFxT-$N}_<*{Z z6Q2x=$FJsgVIgU;*v?lZ2teRn6f|%UI~L$)L&kIZOrTF<0{%bH(t!^W1z%;x?!wA3 zbFJ=c&Fj}t0EzF5?%X-T*iIH!;*13tuL=^Ug^qE8C{9=R7(IgC-t75@cEf4ucys54 zy~n7YG30Z85_1#=3F1MJYbZRb1%Lnqvt?PN{Wl)3FIH}UQI|odlrEFN*HjGOLHo1za!elR(U6wd z-POM5~a2p;*Z7@iU(H~#R!vwf44tRRV%BAj=ES=aYds% zYz$8G_wJp~c>i2f@(JW|52;@s1gJ z!rXGj)?Jc+JKvxSyH|B4uA1aRS+@3>`#wdeh&*~pisIzPVdryIawH<+jUkN>Z{@^RPZjdW$e-Ke~+V%nZ&TTu}6-ykc0yarEMw|F0TJ?TC9?KS4Zg3XEJ%ro>v4) z(Dz3O^g0hi6Db}P#z_qgj$3*jou=bh#$*-=nK!AhEQbleAyTtTroSjm3hk$)T|GR2 z1d+^{jqCEueV5_GVM4M0KdyNXa5z=P_h+%}pNYm(vKf~|#Au$-v;gvZ}-{Ww!Yi@x7N%;L-aWxMQbAJGY| zr`27>=8gWqoLn5cx7C(yD`3Qr$HyPv##h+>n*rCJ%~t0K7$Yw)E{E<(Q!_ooh$_a= zh(e-nEYawN&^ZrNFjX6t4Pu8b@n zebVgg((|BbFpSz$XNW{8f>r_4A|b`C;tD(k1TWmwus%|KcLnZ;0eWz^pD6#>Z>_R- zU+S!OA`#Y$!;$~!9yn*~Ich7lAD%X1!dNJtsZVVmk)4AoI+7Wn+Ffk2$b%-&n$xpH zCC13-sB_!ez7|tQSJ_`GsQi9!a^7>MGr_``GRhCb=g(LZu*NEQFAC7Qjtv&Cm_1Cf z7_OnjP~U`{D_0&B6o93Q3^xC_a?bG%!iidEN2S9Vh0Z;pbbv@S`1jL_QNqT@O78HwpQS*-V1DmSM#SH;wY9NPT_-KdAN~8C z#3(|xA>5%brH7!w?+blgKBR)tvnVYrEZ%(jG)h)>(dmbk?ey6>1Uw#?w@+ZJGt>B< zd9C&5NoZfBIEKM_&Rc1UmpuQ8-s~W+nSe)?|1G^$sEQb-Nw9s)NkRkO!^4ZYD;&=Y zk4?Xvv#E#3+kMsVHQaf6>3Pc}t2DcUzyBZ3-UO`Xwf*~E84`*RQZ~X4nL-(&VTXjI z87VR)D|020v=ufvvt}`JGPTPvQh#HO#5K7FfgkHp({(#kCrw1GpQ zaUUBTfK6ky=}FzegUwTKk`8_Oc6<4l=X`h2?&|821>aix`_L9=lT~t=E|+Y7Stib; zMbO|=`m-ai2~whc=l9);R_TmE>INg2j5{t!^;@7r;Z8RG33GuI{THpjh1v5j*UR#(TzWqX zbMu@KGLZkA5p8$aZ=}R?mS7GT3DCLon)BUkQb#fCP41-^9`_s!DD&*>TKuGaxQawZ z4V!<(!uUNHKM#MPD)|0M(!o0r6%&*OCak-$7^zcBOpdi;0Ij2v7FJ#tUvkF?cJSNm z5ftN=Q6#rcSIVNJUoP}SHKE=6M}+R3F=thR7Tl~1xnjZ`3l`%C2ro7 zZCOoSH;<((R<&VQf_dFAX*lkdjOSZ2^u21DTQ>*p`|}qQ1qJK1CtYd}Nz<&mTd7UW zT_8K>&^CH<{HnB5vJWY1?GhcTyynaw7g^ZWetsM8b*+2Eow_vLw1 zE)0cEeHn^G5ekUgm4H4|t%lzQcJby35gX9d!(TBqRikHO`=B)GKe@YHWB3}cMtl9iga6PN zSLmk4JMOUEG};CmQhxpHJMb!4AGJe?k_BgrwbR*ds&#M%frE=KY%s)&!LjGlyo_dO zOZn5}8rX@meNk*-dXBxIJ-%O?$X$lm@o}Lm4w|Khy`N`Tt4EN^0K#{b&Vkz6+Hk|CPg8Q8xp3i6)mI-rEXaHLJDAVIkQVp8BY~irKYzY7*lD=$mMtQYqwM+?su_anq4`-wsCvx@rgT;;V2`S3QAg8Nhv3F_g~$^yN2FBz2Vqc?XP@Xp|Fq3c1%1JztMd{>&pbWGF|1}yDI6D zBNh%eHlxulRK7nY#Vg#mTQ{;U+q44rf3cSQgA7ccTbq9>hK;wmF14O$s@at8(iIad z0ft0ta$}6(fXNGJI?OES(D*qCczIO=?!;cPh@`=zHSmmXBXJ4;*qkGA#ZUQ3%qb{L zybc`#ysEFEQqoHyzi&g(=IWWg+zJ)TYx%$Iu$zS*lzJV(u#gwVj~_Q~PXB%MUFj}A zaPCH(I(2XB>@_R^yK9!&|Cy4xJM3QvF*eDE~f}=K>NAJxJd-;@}M6cbhoC!O`kl zIZ)&-Xkl{Y4(6|EeiOD3SEYHu-C;dudcPKqckZrX(?)~^OpSvEebf<}iLhSx*S$?vhys+tn&8(Sdt(wyd1@mS3J z@Qgv*zlnqQa5tHwA!)62q!XBT3SJA2=efL1js?CzUYdPVL5IuB-K5VieM-%j%E&>! zgPI0~^c;Dl*}{f<3n>#XY$!n*NEhz45TR4t$r6+s0`@ijiB1b)Yi@93rR7*r+!x%P z<0%@@@1IvSS-%P|?98aTk0?|ctM${^&wm2QBB)Aq;GzkKFpMjpX(fpV^=01hCIqzV zcyN~CrKR32FQ1#&%bMs-UWy}+MDStx{ASvrps;886?ReAuU|*@Vm|O6m(CIsT=xDG zP*p=VgAXdHs63`@Ea_Yl*dj_Lc;g#+U=F+Ek@0L`RrK5YHD}HN84$nQ>xYK9P?n(b!BC$~U+)Ghc z6PWY3zIzY$lqF1zetKzHN3p#SYactn?BxC4(cn5>wz;N2tVqk!U5 zY$g8b`)L((r}af#RJdZ6zC>cT9cev=6A9e zCHr$#{xA>Oap|V2M2rzB!V=pUC7p9|u7!7h_DdD+N5$@4yL`TV`nA(K7#w#_nvAku zV?KmKM0C!7heZLY z?V_X}Eq8lY@eQBLXa}%@n~7ifF@|`Mc>9x3$?p;3%Q}d;Dioe4jYunCeh`U(`p#H| zW5in|6r^S$sQG2{e9s zzz13GXlnkL!5kOEdN9c?R~UWu{DmblptDiV7Qz{fd7cr3#c!tryEykOzbim6U~=Zx zGE^I(++X4uJ;kAq!1AAhf`j5(N4~@X1H%FSoZ>j~Xx4`(m(Q#&z7Q3)1gB}}@^%uJ zQ)kPm*_CkuETqvW7YbaBG|GuV7Kd-YTw6a}@f##%%QBQ*vN(=0Kc8a2MI_(aGcQ?Fa3NJd=OGk30lyDFFS&hsm}8O~Cq(*` zpHLcwr9tYF$TZP4K4U=3R5LT|2WYneqNegZkvvJAEwnM}Bp&>P=FwlbKbSu6`J+c8 zpsErI7%i)}v6*l${tlYe?c;HU$ZI-7hJ33ya{_F#T=?rkEV#uOBcl-`R)e8AI0VbIO%n>(6?l!cX_n;t;PVrD znA-gFsQjr?FTCDLZ=B%e<7Id?nK$(Cp+%xYri*U;f^ZNZHdpwBt+4sp4=;8BMkS-3 z@v4_<``md~N8*U)`K=N+KbE|b7FDb#thb_j--3Y862y1j}k z4Rd!DbJUJae-8O0C}_#WxvVPjogbUQB(+0#wO+%c*G4?A}+KGPS*FaxL zW_wMl>srXHYC>)>?26N?o97Trmr6l2A54 zc5uu;c7{-*IQ>*#|9b6sS^@TD$Ulu>voCZq>lph~Iq!T#``&%~*3{Hwm(@x;#2WT8 zHAjo@<`!!;nD1dN8bIi{xb$nN2PMU;mA(~!RaevaE1(y~JiI-3O`#g*#@_xoOj7>u zQoYe-xa9D0-qHSyGW=l)(lSa5>~PN4ncWDJq};jl`NBTTuwf0@oQP|Gmu!XeJ;qR% zF~wX&gg1Vvtwlb`)Ge$|>#zNw2_<2!N9cWm;zxI9bV(WXZ+ZE~%KRa5cHzg58@WDI zXHk2+v()J~X_C`mwFOt#{uXB^VlpEphN-ScD_Jst;1&+dV4o*ULIF?a0hu!k<(Ln`n+2-WuJxUlV3v z05wlmAIRVN;#<^qbO&eYzCJ$%M~O2YBJFHDSPhJ}uCA_Z{WX#INwT05W%0zDYpHzL zHboVRRI*L0-yoVW80V8C!q-d^^K+Q>UjF6{BPm_R9<4^=(XCq^T}0!}8j7AI$5mKH zW+|MXv`Ewt*D4?zP@7~SXtR9wGjaB4>JAX&x!UaX{G+Ai^jn z02>B0G`kOR`drlb9y`|y3LgLIH>zeRFyrMvU&bts(+ z9DyN~v^cw@GDk9o|M|ymzI|1Bxw#TuGvpx;Q1j0w_C^&YB~_(1Jmb+i4^xPNpxeoR z<{Yr#=pLGn`%57f3l3${Fl!tMAop?$A$EbPy`x*_{Ixk%dJDd(D{;N ztLM0Y;z1O1(=T7R@Uga*vRZZ1 z9dx^@{w$l0v~D+Rvb`2)2=(@0HQWadn&Tq1%du&9F^zpSK}+km{`;(guJe3cM!V>| zR#v^woxIqo2WQyNtZpn`ij?IpMZ;B`Pn3(8XIp#VCk4xSX#QUsF^-Kg3_JGWB-0JxRuLL_09@i zEUH=rFonk}-9zVHTQ9r?_)*xs28U$|H~Xq$J*h9p`#F^9fFnY|vgsM8Vr*~~$D8Ei z*6@|nmPj^EHaou0)@#5iY5nZ$SnFbYbW>A1eZ4q!>sFj;v=5Gbru||n#-gLRlAAQz zDXaf^;DDWe%!LbUxdVJCuD2ZGWQ?7)H;jUQ|Nf`IGV{NkEVsbjWCSH&hf?A8$UYw`Kuk<*E;71hcsyn<$GVm@1&*dD$=G}yHYXTQZFuw z@KcazFJ@W8@}&2@n4<>-NG9{wS7{wL@^G`w!W<(=LniufpY&UiF za>94=Jl!O^w#?2>TU@aDMLm7EsBbN_6R!N;TVj$KIB&2dr#IL|+ji|zeO?l#?@r%` zSioJ(=)~ic@MsaC8(oiP`GxjMMX5+?z;M}$2^5bT+WNokXJ{BvJp5bME_qQy(p$8R z2v7m$>DXo)b?}wn42Z-OyEWYtNirNZ{G)OO8wc48lp+XCm+GoFJl*qmK5(4%nT8wW z#3I^7>+n85E)QyrCb!&DMe=aIkIy%L2Tv|)e=eYBl!J(s(^%fRO|lN{&h-9P-oFW96Ek{fvrDd z+bDkgH??Hi=16XlXo9MKh}!P(R#aM*>mJPmb&5Kk%PkslL7p1>VpbCuj9LMqLlbM> z+18>d-I?PStcfg*udeOs7Q_?CYYh^_*^Co_6yV8_1wJV*E?n`fL0~yldu-%rGeS6Eol(PgwE&s*q){5(Df1P|2l z~aC}7cNmFMv7X;^pAjm)>;InM;wizGS9xQO{{3rjF49zxJ~t$~bw{oqNU-oGh7ua$d`&&kc*Cz=}z_F{<#zyN3# zz4Tq%gaW#Lu{zF&9Xs|6Ku~*Q4&ja>t!C_<0N!2O$q5CIBU($;f5k~WVZxCQ!y<*R zGc7wigdZ3=mJ&!rT#5mEbL^#h;ZRr*PVs~bsvOspm|Sjo6E7O}GD{rBo`NJvPqg1$GEZM}Ll)Hrm{yTzU6#(|9`sU9#|*AHPzA;vu{7@|(u}iS#r|s7vUF$G7?%P~`2yL# zw8aw)49M8^KlE_-2NcG9$lbVz;BpK5c+@;MG`&YEynSLxR@QLp?2DtRUwm5LML0#^ zJ$GRmVJC2uN#$Q8R(Q^SS~LG0XOIp{Wo&4%vA76n8pRtGkW|wP@G~%prVZfa;@C-s zb}UKVn(7!uQ%En((^+2z^!jG~1i9PfTz00!B#6--$lm;N|M~mp*Rsw5TN^oC;Xfe4 zPl+wBXz;LM7SpG9nM17ES%(Bp*ef`{o4%s(S8Ug=D!H@f*!M7gd21M*@`3JJIbHKd zbv2umSEaF*)=B9NS~!z_{a_;l-#<;BZEU2k??qt?D#9?~Yfg#*T3vT3X|X?!8Siq+ zCJs$=OB~2gdx>sODy;}W9jn1N*1l{Nyy5^FWpv`Kp~f(AC=TxpInX1X=ZZ#k;>7P1 z1C$(_e*U<|rzMogPq4>8b+{r|HP0N_mGNf+*jvu*APjVnDR0(8p^4_7#yk8KyEgfy z!P%2&H;~XeMxj!_H2Oz7N5BB4Mh-qWgBGB@7OZ23?b}org9hs^h{|OUp#zbThYrQt z#WM-0(qyVgY?Qe(o8??YmMiiWoS9&;unO$%l^G0~&>lDt%z5llysuZCMjXu|35br+ zZlAJbmIY7k__h5|%a@dF!N*@OR7b;+>wH}Ef#`+?Og?V1V#Q|@JJ71@R@M(4Akk|L z#R!zoeaqkald=ym4l4v$p>y);2tQxYfvHosV+{{oF6~2`FQmwnD|R~F#nh!Eq*aU@ z>Fwchl+nI^Kj=o7MSj*!$6m+2;4}SrZ(~0vW8$Zmam?Xn#a*>=((fiNQN1ZKZ6FGaCvu)i-GCdj1?5{@~|O8Fn3WB=7tC<3C0wN2J#JXT^-q@vwx?G z<_3}vn!X}{V^_cu*e4|8U7gRQU^NSo2aI~0&Kv-Zw8fQm6KifPS^KCCszGIsV?NRQ|CACK%}hKZ?Q|x{PnMuahXfpgP9wt1 zk(>XTl3PM8M~+=(1|hOIOJ)q~F~|LczWbp&6RN2XjJ!YePVKPI{O@;lC8S&3}X?duD3iCLJLn6Sy7VV{BEI{ZwRLXQxnaV04T=8n0jD1G~sITGyfJVeLW zS}yIUZ@weKW@asVyZ&15PVZnh8ft6fiiU3MHPPqOQfdVe!pVadXD4M;W?31<<=h1e zEWM@8JMs67hP2?g77IqxN4#%{sJPbXuVK#EDB2LyCy18*r&YO zYx&ovEWY@aO$y&96bg&ew3aSM8DN;2>j#cOE!>& zb~mLq`76xQZw}Qv_u~ieo_)Mwhp5i-rTZb_jA& zhhx4FSG0IJA*JBhh!}f*b!-y4t_v|dOO9}E{)tTF>-ooX@kZZC z9eNGX83nF;-@!)ol3W!kPMQ#G!fISK{8aes0C9Ii+dC$K-iurF^(&h&2WVL1S^0U$ z=Fc`w__cZ|qadaF+V-D_2&8rX>n~H{;dOrkWzfGBtXYVD6UwUoDf96z0Zo>W>`SIv zdo31o11L;G86V{a6?M~R_6{oYsi|Aospnr=37$qV&9W@I1x_jk+!z}%pq;x(hP1(w zU9aB1cgpKfN%1{={*{L>eyv-^m|(~?q7~_ZA)e+IZB~#tDpBuiFX!{}M*0V;Kil5j zG5o>>-5*C?V#ozPnsshV{6`|)b}08+?0bTHH1Ed7i;juf&A+@H7zotJElPLsC1#<-YYHH_#yaLwMcw(@Qq|r{c+>WC z-5{u+mKg!_VRs`JZtXK+ec=72RA!exZx$8&Op);Unp%ROSg!cv2&J5n+S)tfZ6uzQ z&NA86Q61e-l~*QZ&37*9j5%*DaA0~yYptc|TpOF;v)id@XgKB_UuriL~SlbxM50Rc0}k0wKHCbXRJ_VHTA;*U@pTfO?)*TMvz-f>Y*)Pjfipr%f2E_SbYnWQ@kC(e03%WF z)r%duLXcbGe^E1@ii#qUreqi2-HY$`+#M2vua4(j;PH}K0~zR!7MX%Yj_kYgxa3rj z&cY?yKvU-!mMK5mBdUNfowpvTk&h_Zw^;3UOyvQZIdxj?ali}eAvCON-AI;*+kNo^6ND&) zowvEMDAnPdQw8FcHEYCJWEKn1O(V#@=khtfgk&VxYH9TBIgWUhp$TTC9i9I}=xA)6 z>0H`YV)70#79>sSg+&2O7%^NeQg(;;6746mA7?35fY270F(tzE9^F#K{kY$muK|sd zzvS1shM4nlSA(h~wm%L%if1Y~J+<;W=F#mHb3SFjOp;!`2Sp(x&E+r7h{KAlf=hPtT>;K%(R;vEyokKe~|)}43pn00L5epg6Hk(*x1w83h-K`m^u zH&t7_pz}a{+b5(aftO#;qn1T$^8`F{p*)F}Cl29)4?8cXDGCw{vE-gO$`XL)kt-9< z{c2$*J)>VS?hDY@>Ru`7}Z{u&%hLg8!l0&!00Nc>uPX=Z0WRTW~%QjG=HD zv4^IkBr+RQ{Qa_8_IZ2RI{6NN?_M8v$5)k=Gn(E#V<-U7k9|jVvg#Rug~w`m1U%i^ zYfi8wh(?h6vs$Ro^;e7SXTEIP<7umUeZsm?;Rv)hO$kEpyYm2yQRL1)@x)qDThb` zA+fKjYUUPx`7o{5wD>9h6n>p{+HS6PO#RR2;$!e}tk5_6TUuz>QAM%6AV9fr{ya)I zAsX5Dg^tpNOHb`vi(m3(dHFxwWm1;#7eS4F-PzYKXFF+JP;UQZXP^K49CnVA*K;8a z4$~}rIna4CruV3K+G~jitStzSzrTc}9;}(B_t>AQ?;wSJn5`$>w2C+=N|IpTuy}$} zo|9=$=8UwOEit~ZtEQ&=qD9sX8#Sr4NZSKqvdtKDe4_VWRU5C8l`QzU>a$Yoe?J}S z0*T~?NInZEE5K~(R9f%on3&`Ab5ezQ6XSIWH$GczK0IplElFYSLPNY7xcjoR(OVK- zf;=kw-5iy*o}L2(=|Q`ivNBSraM=Zkp;BtIjYM#gSDXnE#{(BIQQ70#v-pK^XE||E zyG8BhHmRE8K|1A-K%P)=unHUFrYHm&)f4i|oEIFm7(TL^f=RV4kqNK%qEG#CVo%y_9#1T0umlHF3Ef2A$;LS}G#_Y>O!fpO65(db%Us&XK$S*W8ZAw{CO6>*!dJPx!$~#MwD=x*%|;aHi7S zYmnlKDG;cKUf>$3I8Gz@oOdXDV}E~DTp-qzvLwj_90xMDz_?>@mMfAm1CbxSJUFGB znj}AJmVJB;xHEMo9YYt80LCfJr9-m-%x8z}_i>S?$!Sh7{pV#TMc z_2}{F$e4_hk`iRRgVhwcN1aQx4G)LWJL{PEl5gzBa<-~5NDJ8BN_J^VE ziHdp5LF(VUSz{I_LiN_A)@L4V+PHDVcbY?%e<2}3uOM~`qwbQ)B5tad4Unj@a&_~$wXG$^l=)0Bab)(!lWy4b;e+O` zn)ob6^fP=hStRrr~4mmh#l=Y)MJWgV^jpN>-Rdjir0#(J;6m)G{ z{cbhb_wsufgzkJs1Qk2-X(G6LV>c$_py2Uy|G1hf(6%M0^YHKd{w0uCNlRNo1$A44 zB?4bZP81f87PpmdYXmW#XK+3ZYT@65g3?~4XEI&lTg%u(tJ!Fi7oxACvL&q~I5ijw8XXn<;a-H3yB9_`hv0cf*)OU~*^lcX4THGbQ->s+;0{R0`7# z@4AHN3p|`j$-fx+{_`OI(V$L+vJF`eANC%%S|AC|eaT2Y*78|nk8+4&iifZxkF2ol zb4uYkVZSFR1$rWb@Y3*q(>lHCAo?jxp5D20(l5D4w&5l|cn z?SB7~K9VNntaJ?AJ%5EYqdPbj6*q7*m~PPE2l-jYFqqq~IVs!#wCR;%PC?~}Yp$|~ zu`CO2rf{><+YD9P4S)5JfTMk(>^xq|i4HTpJMkGMEqOtJP7CEc%_?hI%?Fx{I6!h^sv8Q~abzGi0sA8GT?`mCtZNNWy6o zQ&mCl*t$y2^C*L?DH|d;Z-hCGA1*H43t-E!L9z)U2@y=#h%{$fB)u_1ya;^ z$($(EN(@R45G0wus*viT?j#vnMlVGVIo9SS2tSA+W8zBHp}zFy%&ig8J|B(IBSnye z%+yNrCW1)mHTocb;A~uTdK!z)xFxanqcQ4rP1#oIYyUegdX=`^196_33_{b4cgDts zVu8U%5yy_zFX1Hgq&_MsLRHV~G^Jh>*1txg*iaS%5k?wn(`WX9CZX$FV~o-rm;B(zvIL>(*=&TN;VEg9l;6HSi`344w;J4`{da{Br{NB%lBkj)hue z=R+T@chuH?6(u{k9Z)Ty zB+Hipv(lXCo8X*4_r@%m#62-!`_N+w;<~fuA}&} zHA+I~oVGYEJsnstKISmRAWsui!_6UZp}Nc1GhT?<79>D%N-w4VMrqgN4|q#)Dubrv zP2z184G(n!99D8NmDQ{h7p?(}x9F0XgFCRRoEd?h232Ka2HE4_AV%ZhjHzi=Kkc|Q zO|*G&eepw-o-pv*EoS-k-e*3gK%@%tAf&&a;x>T|P6XFurVvRGtT#?Hw+Cku3Z#UQ zLx2gn%7NnqT5&ySAD$jyrpV#ZY|s9cs0Vm-`PgI&@It_X=!-i9;((EhiihWLW!}3d zPAGqo*8Be8Q6^4uVfiha);E~jhM-xoj9P@gnGT=PjX*=Or{)anF>%6#UT2*-YU{+$ zpT|HtR{a({=XGDc6oruXXCG$=Ch6FI<&PkV-tXWl>@HkAE~XxMKUMRnQMoTt?*~`P zJa&+a%_X|?Ya=PrV#-t`1JC}ZZH6yx`4$>mUPMv?#&TL};2N*uX#fb;4#oGxw06jx zuuRbner&Ukj)@rqp^Yn-EMZ^?2tCW=>e`ne>J4Racg+C_LNX{jTZzQZMY~4o?cnqg zs2-AOx#>4#nx1dKlvf-p!(|;vjqzGr5#=b@fF@xg=qY? zeQ>({gBi9ug9c6G*jqQwTOrth2as?e>B`r0VNMyZoCU)!AEz7$e(T$Owp4vn* zDeVFdnoEWUIgKb~H4sWAP9C;72;Xtz#-)UAJG57Lxts=gPsJb3>!cBjS8hzfP@}vW z-Uk{WGdp`L+n78w1Ii`N4%(4PrGLHX{g(cOBM<{;`~v{x{Fi3Jc?xdl=gW~z)}#K* zUbfxCpOYWQ<3(QY^oVN(ED@T0B=1jQ`+VJtK&43i$?!mj1pTYFkJ#wxy619M*y+SMpN;LzE?V<6`MjSm#6N!-m!1D1D}RFn z^Is;THe{IWF9rv|)gnsyupAMxxP#Ci^r3feMNQxVc#xePHY0lR#qmsO7&q<<+anzW zJAFldT&UjqCavC#2+5ew zWFUTgy*Te|?Ch4VSTS8pXM|u1R7F)T=At#fgbmj?`;x={uIuH7rzj{$Hvf>o0cGR~ z7WZOP8Lbe|AG_I*hv89~o!Iml*bpG3=`)une~57B(UFi^jYswF zOTt2tmlIzqb6lm{jF*L`#cHCZCTy;MkB$gBvS=65SL-E>2iLlDd98w*R_OhIR$jTY zejMD@)vI$;Z}NDXp0rAB0(lWk>-m(ECE@8{%@U0cY-izP5*-9Ued-iz(#(6r@a?82 z55v2zzR>ZuTK@V?f8rG4RqNafV~`i76=Lv5;qbQzMB&y4>BQSLbZ(49@LZm2LtDv| z;~<1Tw@*)xevH$a&nD_s-98)Y+EF68+0fqy{wd9HUwD1PtT?;J2f7fvPqCLX$=<=? zaRjB7y4foGw56lHhBLDnsU{1!^_JoL;0B z(Nf7tgdO&p-pF3Iu3r5CGT+kOhAp*|xZJxP63B!;f{v5@OioECCpcbbL=2Ux1u2*K zVL#lUc+wEp_8?#-jsoEylmAqG2Fi(?f3FV=J9&@Vo>gt?E&thUmX|NlJF}-FGnboU z@?)u`DPQy(d}y8#_x*k1sYG%fT~g$?KQrF|6!L(c6m*7eArB6otp6fZvUYM884Exw zSAiizvInl6G~WO5_fb)0bmejn45jo-xp9RXM6KS^8F9trY)1bcL13&O07aHBX9^aL zp%}CasP{Nx&M*H(MWjw2#9KuAq^x+CQC-E+>;+%Sa|5szDu}H)v>oeBV&W|Lk*)wY zy?RZM=Erj4^56I&CNi?OsEA1SLm%?@bnn}j-vt1lI<8_VXCeHS5p%qop1PCP2KJ*DPNtxYm%ET~01 zj80(9n&u-cYvee^`~ zA%XKdDp<1oBBA7CEwBslS0cr|zP`7< zG@lJ%x-&sQNMhbmwDNE`I63)Zb$In^F{}lS-GGRlJ9h9R<2@*x3#9Iwwn+Soi0~vV zcW#O&BcYKLd7dHcL@OR0ZxF6?3||kj?n*`$o`tQ!-r|`|sm^u>I&uBCw~nnP`Agx2IOUG% zE^4LKhNnc*0NxOJjp=~T7^_M_g=9jccl-!u=ip@XTDR_r7|Z@rI;rVSn|AHmnVMrV zy7~R=E}J-!XfaD&`~T#)FvM-*1#yGBk@P7DvDjCXV1a)kSkl{wW!-=W?itoF zqCbsS=(_8);~=vSYkXTg6S57Yt3HKNdvaHjYp<)A64gg@d;;~Qw=3RUh`-k>(-*`aMm{eXm%sZFUr z0|4>(Z|ubPq}76nqN0aSAl!^X3^`(IsO`r*YAQY2C z*AvG;Nz5omb~w9-A~m3J{u;09VlSj&!gEtBOZXW;YfEfvyd{g!>Dqdxn%7}*y>R(* z74ZX9GGyFebGg&1GCm;SE($i&$&=A7&>OfjT#RlAgMsxK1?y><0yw4rQ&wC;wpQc2 z>#yX96{zci@9u*ohtIP^ZQlHyQ$SHsactJV*!Djh8o1KdfT{9?Y3na7t~W-fEb%`i z;kj}-2d&1arCITSZ8`IpSdRZ*lm~i!zeaSHJhb1m$)JQ{O8qnQ*v`$p_-bKuV^2A9 z+L!*sf5*wxxd+7FQN9njzDt);JTLfjsunik*JtZ2tVeDwpKzR`i`ig)!_|B-u!J-K ziOTEs-^jD)__)FtwEzWStTs1ydvz8VOrZs~btN>HmgU-PCEk;tV6%(vp(uWqeCy=N37A_&uOT zB`w+69b`R+w*azk{Q=Qw&)tg7{UVp9^!@k4^e0Fte1eCzy!iUH1y0P*K(i%OIe3sG zqyGgvmjk5K*3~^_(7N~mNop;hqB15n){USIU?-tg7d2;O^lel)d;$aHsT78G2?dDr zFj$J?C&%VrG@S926{KMtpX5VP(jBlXB~L-(_M14Iwqo$SaN%|hM zI2{fO+|#j#D7Sm@P+VT#xny3_6t-SD%DVJ9b0visfldr9x#=&M|xc6My z-iDh*$cak9@Srq@vwMp^h+80O?kkWYBef1w zaS1Pb)4ZhSR#|w;m*(HMw%Tu^INfS&C#!Y+9g|MIYSX2APuZNrS8maF9j65)ChvVZ zf7*PRPT{<1RVlj@?fFUJ>-Xs@a|eos+d=f3v` z)f~(IJ|*k^cVDA-9!Zq>XY!bQVq49gLF}0B9%5DSOrAMW>1lxIO#sMC*%DOBz)WjE zZ0)2;B#ppLd3x&Y898_`K~}hI164t+UaqZ7SuV-h1h-9VDkxr%+CD^<*{;Kd!y|8R3j(qVv|q ziZfhd^(!LLl(@?s)zo0u7EL;uWklt`X{B7o@#=>j0A!Xd9=WEK-Fbkj65iF#4VBB( zPnz%wBNp*f9=7_63lz zLCm8qEiKjk3V=Cj-K#+JfrvnfmDG5~p+>3mt{(o3{cP?b=P%nrgsZfYKI zXv6wz#{kevesX95#>S4+L!^tuS9pt2C{{eXc6pI69lMj$G1HScMZg^yZ!gE4_O4*lM%TyXE+b-|dGjfC;jUYz^JNA7Aw{&%Gb8nQ>is3lPdk@<(MI{7sIY zp>U70t9<)bTdjl2lGt@G7>Wz-U^SRgYVzN~H5q5n=?BL!Jp|ZapO8U4g23ds2uf{m zz*iztUUb7wM}OS7j6Z4OC3*`*MYfas8o|L2%PEr8KfmuKV$KRVcLjt!YKdarli!w1 z-F{!xi*}! z#)}yNcDF{=q>M49@}LqGjivRk5M;Vt61!_>Dt(nsoluPNwB(3TrZcnqvWQ|rx48~r zZIbUrUeQ20A6{KS<3jHXIW+LIdO9B19~3k7wkd@oH3g~AB3}!OJ*Fok7Zd+Fwf{bo ze$WAKx1C$XMsst8bCv8uN==?x;DB$O5Soc!(6WvgGNgj~Cb)0_EG+e5qz@P?Pj~(% zbBW4uMnwP)wE)xuyH=tMKRRZ)CC{=-+0C0b5mhyIpeWR?|FVES%*gB)VYf25mmHUc8OJQ-@57W6tdNl5$|S~-<-}J9&~U<#mj|cFZBq5P2Rb)U07yf!MWeu2ZY`y*F_kVA&+45 zczDbni#du!?x|OxiA`hYx_zTXkj0=WTkTNf0<&}uUU4tlTX9+I;8M8^*)g*%Fi58Kh02mF9mPr>`_kw5nC z9l;?zWXMU^qwiP(NWU1to}R98^C#hHPpGp*mqCUbg$O(VGiS@rtV#iZV^alR93^zI zbvxjB(}8oDa{U5Snw`kipr^mXDq0WO?$iy{jud=mQPw&K0Yw39`6p9i%Ck1N*P^1Q zGXi@d6hOXLTU9f(*6jRHWElj$ad}`*^G~4h%_w6!#$V-EYpG^260v_B@`zfDG?n9S zLhrq~KRC7GGVHp~GW?KtCM0*%YpBNr`Zt>>o4DSLbhVu@AZ~9T1J8RGE)JP#Zhkv(5oMg13x>wVrRC#2R+aH^%Z-0gW~bY0ao4h3*ZgzgPI=`mo4BO-OR395 zw2q6*3pCM(fgK}#?lLN}HSEINVTJTy{8@N1jgC}LBj&Py3YlqU(}oQJw~Z|*1QU^V zKXK@g*@G|~f>AvFuASLe#{9GL zGi%|=s4wEt-2X^33 z4Gqf}+v9&o4Akb24ZqU_f+t{Tkl)rf|28byBoF)p1%qfNh72pW3F5*!gAKddFM=Sk zSiW3n4~g?Yf1$0(F4Ij`#9}|VphM@*f~;ZyX7`T_OKI}MY+|}~F%!vWt&dFv7q@Lf zJ5hMGrzGbS#OiPY2rIyMDzFx3tYOs)0{gQ6Im2 zp_nzFJ-g_9`#8UGtBOzORjAJ!?2Zb;Ai}cQCOQ6dD~aQ25Q6NZ<3j%HhtDde81igp z>`n09mv9%l`#Yb=K5wt3L5quio)S=R%4nFJqn2l2rbeB}rhW9liir3`^W;LY5Zru4 zA##omiP>>UHf4ZSdbq(rrp#>Op@R?54(-tV%pNf?FbTMC-*-_#`KRyvt-b#a0UoeD zK=z&LRMSvbmyl!@ZJ|98*%c|NhRcdB^zYw)d~aq=EYWcL&W5q49`O_Q{LXarDh$I) zq*15*Znz{{==Uv}ofj-{Ow;hjnQhX*b{iWJIgALK2zo*?q>d|RxSg_LUqTUr>}G-s z)+a|<4d$GR4U-<*x0!+*uQBA4=^tiM^BA-PF?G%99HC)EbBP#^g}y!1)X0{KWuKBy z?i)LJFkx{g0));gV4ld#$4v;ww;XvZ6rE$=akcO9s1NULzCrzWK4{-x%MK)}>h3efx8&2`l zmoGK>aovFKN-d*r>MqMjcv)4Y*|qE983Va4CLW01BJUXbe$C;;$jVv;-p`3fDof)0 z>P^g*p=b)duOp+5al&W0u-Yo~DU|q$)o={`12=Bk^mur~{Occ}J<=9)o?6dl1TH<^ z*}g9@a~6rIe_DYw#s~cLsV_*!XBubCwjnb2XupaJG!Q9b_S}F#)T36j@emO2dNOq% zsQWxDk0(~^KD$Tl*D&rA5dvLJ{m4!;=toNF-IsO?uZLeMD`0QQ$rr{d$Uu{z?L^Y6 zh+!i{oI~j|N(Cb>Df4kpHvlt@1$LTYyQsaGU&7``d{+F=>PdahQ@_^RB3{J9j#z^- zIpvM*P3Ts-IRIzt*|;%4JZe!qJWufo0-0vMJtM6v3s{v$j~qev`FM@@+O?!^7jfs^ zCzhnxq$c4i$O!@DXs-n*nV)z2_H96*%66MKZ8~)DAS_>OcyQ91(gj-w&{Yo%EMK^^ z3hlg%2d_1|~$fq$S@C;gyBwdZ3u0V?Nd}cQN zzmgURh)<~B3SJFm^?-AW>HwvdpLkZ+0O_IP^Xx{LqMb zkw!1$^ULyUxt#WfYs&#u{$K4i(MM+Qz$>H(CD0$@4=!cCxQGn z(+D-x#Gn7Aoqx;=!p;Sr?3%S$ewd-aG#O)MW3!%1$F5vIYb-juva+&@Gcc6*9zB8w z^7QmP?MR=>h{tk&OdH}27b_vBt9lA{+=T7P?ECIN{#fhbQB`t%TnQLC+mn(OxvKO& zF+FOSMccbU3SZ*mGvVfkQEvdX`DK%Re6Ce|cl&3JIDFY(>2*XtkGuQK?(dL))DO2x z-}}qVpA$U}f5J1JpPye@YQJz|C&{ck=;G0jw!} z5v0O~jTtat32$}BFDqoloQWEpMuM(q@X&#mkxj%oVi2)f&}TD+EsUQI;N3*Zr+M^a zDmNS=zF<4x-to1Tlk`8&H}vqR!$^i>LOf^i0XhdOv5NCa&J~y`Y2ma>zIc(D%J+e_ z2}Z^%O%}THNXv7l7i{P)2^WT59!0t@d1d2xPSLr22g#oJ0)xAbiC0kDu!7I&xbTK! z&W^z@%M(6{#5nR#$*CkGh^nJWoq3vc`qU<%M}7OAK%zy3zUf`FK)P*2G=2;M@`yMfTI&Lqyf0r; ziLPsS4Di{ID#K*|+rptUuQNt+QK~p;>di`;En8!@yJ2|-Z`s%Ks1SbB8a#@q^*EOJ z+_nl}s@ki;>#(CRh6KVPJY~xxN)kBCxWj;ox7Uu8KC9JO6tT!ZW(oHhyHjlJAM&Sg z*r7+SJElk?Z_#-!hol512`*ehBM#_QGJa-}AKo&EaX3Kt*ZU^NG%ouSsO;#``SxRy z6(VyPh7H&t1`q%R3XCjgu}Hqpzsw#<)2#2OaNjOfB0+({YU3_1l|Mkr#-AjS#|y5S zSa+=IDG{&;yBDp75Xmrhb34#nx1h8c%ocp`fN|ciWlIePh;!QCkv_nLp~(NRAT&qZQe z@L=&<=A)1d2$&4&DXQ6|9#^Mz;?GU0nHTIv(@cr8`0^9^$8{9Lre;I}BX5Dym7b~@|}Msm~Pni%xUZqC%beAnSh zst+C$C{$5v=Tx=?eEP?+(#SMiy=c)L`P=Q(@`I!b$h{;3Fqqy}-Zh0Os-lA#7i?Dc z_HBC0H46ScIN4%|w!u>5kk+x2JwtyBCod299x^w;=yWjH6 z-CcfMO98s@B?oT-^|DIDB@_Wv;%h*wdpaaK>O;qh%3b0l7cje6@E|g22JSfm5>{7? z-fZ~>U8}isu`ruVo0h?CMS~lY$9J_59A6j0&%+BEM10_2AMB(c3IAiP#UOr-Bq#9R zn@^|&UcY&RV>n6RD{u=hnp>{^i)O+PAtGq^A;QMwT@$GCPNdtBeuyTCI^JgTml+>;Y?1bvgl(2N3RmseGLnipNxR?a?#?o%$J-CTH zXOm@gPUw&!124*RkMmnc+k9h&5m`hJbVbOy2%1)YivC#O;+j{se!SDptY`HkY};6$ zEke#qzoTP#0>>rCM<~k#rQ4AKObjBzdD<&od>?88*UL}%OE3aN%$aX4J$X;rpq>~l zh>plwVZj*wdc6-79>QD)heGx-k?T%OMpc!&`ZuCS-;;-!`G#&CVJ3Ke|1M#fO(giz zdMTHQti*l$v{aRd=vo4evU;_pOq93-yY@=QH7UjEdIj8#j)Lz}!9OcG*~2 zQR>yt7aG^~K5%V&_Erh#o6a>*b-SsxqvH%E>x2Tf=f2HfGl&yfrk)BLi>47uKxYBT zDFsr~do2Kdu%8hzdejnGt{p`q0YSi-Ljc%VOul!&wy1eP+x{D|9*6vrr%5W{;cq~8 zUqIP&-t^shcO4&p{rbjNK_mI>E7i~2#L*BD{#F+AB#A_B;Poh3Js~5p>nZ@Msb|76 zNz#HmqEQ@8J9PIe$JoxDJLgE%c1_`2+e|FKav9amk>kfLyQesfD3XsF2+w@8p)$Yy z)R87P0dCB)FDXb&Y~@%e`I}U~7QD>OG1~diu`4eTXH|Lb0FYW*ZT`thO3%Kmk7ydu z`lOZG&Yx$p{-XteV(H9~FFPH~*@(FH_bj}$bo$bzv)s*ng=J^&YJOT}lS{9Qq6SFpgjwsL!nY^dN@OjOo}^!9(tyLiXLN>j_J zVlitLY7P)-hF@=O{%KZ5QNGf1lBz~Kn$&`g6&i z#(Ge;f_IC|q)VKy5X|a{&J4T4v@l#%DOLyJv~37Y^%D#l2Jics)M1YoP8LLIEYl4& z&nYdk%(DB-ui-j`fA8phq429pQ=187plO;!s`s;J2p98weyhW^d;HijCmS2ngIDdY zG1%}E=O;zu-@1Nm76RvWcShI9GFMfC`KFRzp?)GXbO{Pi=jLZ0H@CLxNQvyHDecag{0ZpFT6PXlUF812cO;)W z^_SZZQWQmisM5ttmrl8kgu*##brnY)WPy4*t_K1WY#KP%nLM)Wmkk|mtKp%hDSM72 zKxE)}Mvr4BdB9$sqs9z78oXvU59ZRDg>`J@c8O|&H(2gSY|iw7T`6(dOf({ni1~J) znt8K_4;w~MnfX8x+YjVAenARPwqX3uw#~~{tt#=q-X?1r`cV)M1{3E_&BYNjk8M3C z2|ExQqX+Ao9+%t*O5`hycrh5_ugF9$=$88P`OyzVrhKoajGF3dk_xHMSA>C>lqa1H%_z;Wl_zmL>8sNy|) z)p4;`>Cm7%Z?)afv9hrw#B_z_{k!GHu^Z%+-(&g#`7Eh{KZzCnYCblT~GQ8enz3C!P- zf=s-{kgT64=$zT68&dCPW$|3$J7>BM(qdkW-^1b3UW?hW<>P&@w6LwNJ;JDPccE39 zF2byFDu_t_XQ$gl`bGBc9i1_f{pz(g03*SsdG6H z1frfrzOWx;{rGj174eLp-gI`Lo?cR9BomOWm#eUa#=W^kUAnTq&WDnnBsY9UbR=+e z6~;Gj+}QJ$EjzIt71dkXV9ec=4SJmyUsUh;N#C2K={P%w#yq~ut(r&JApSE@85>^x zQ;xpXNuR5zGw`WfXcKrscfB=WsWwzVheu!H#R@bakTz%IiorbNaCA@}UX zRq{l_$8%F__Hi8K&NN$i%a0ID6%A7dF$3@ z*gM+wruF+Ozp%5LmHx9feuf;Nmv$7FeGs??Fd3Nj<>cf+fyfC&Z_L|Ys?*o#L@9e3 zEn>GarrD*3^erVK+qdoqn>EFI?W;6`%(CC%DHv->55#p-65IYtYFGgPpd3HlVNQpT z45BgFpImiEb&F;XjgHoTHhf#`V&T3DekK@ciC!yl8{VNP7sC=FO8r1o*|WzTTbmh) z>zDa%Sn?H%{iaTBJD^GQ4<-Wy_O%fJad-EjJTCUhg6#}ZMS`6QJ&XeC@{{+_C?mjJ zV`J~t2bZ7kqej+yRn<^crK?UKTIM{(9>xP_Gs-hUC=BDH;ford5=~46kv~)a(LF=Z zJ48W9CbY1toyD-pCQH4Pp6<%XD7$5r?Rx(IM|Iup5NQcXFq4EyB4nu6-e1sV*heWa z42Rt~`~M;C&EslbzwqyEIx>fZjEN)}(twbJkWw^Jh=fFh%uPs$WD1p3(yUCSC>bh| zp;Cz=6jDwSMN)d+cg}Bmey`{6XTQF$^YT4M?Y-~MeXo11YhBm1va4Bkf}Y1wC?+J8 zmC5kJ@mi9xeZmRsi{bMHBtlp>nfR{$!7;I zlI`6at=Q4bvKz_CH0ib0{V#xbmC6540Iz1FGtph6NiJ1&`0z~TV&R{{v}R>rwDHK~ znPS0asccU_a)zEB+98mUxJB1IrO7X31Lw_JvMvQCI7kZnnjHjeHGi(USwZNCT^7C| zkcVcRmrE0CWI0gmsmgb0`$?ejK;YasL%<^7J$+x~%OX`)S~7`yJQ|%-(pR~GZmvWX zg;uUX_kiWEAK;%(b4fn4dHTx|_hTisRuy~q+` zXeseDA3d7u*4rRz$;i63_4g^4d+4$g^%6^9y{!|6O9%QYY)g2vFBM`u_%XRhw=h^ph?vFOQfyX&c+o0ZNluy@yTm{sZ#jZVR(7Sv0T*MlDlFBtYI(N8QwHv>tkqpl_*@v7Bqugt#g2hP<#Li#uuzImYWnm8FYAJd8P7zi(QX-7f4?C}Iro46Kl=P%xCwz=^9&R3IoB<|1YlIg4<&IoxtkPgZYwz;93uu45}SL-HE%0oyN zio$UzRZcJtKx=iWD%_q@f$2=O77ecvZ%|=DltCAaaVgM7WRGLg=X33#!0gc`~`4Z#i2kGA^d#%F-VK{RlRL zOn0ZVb`S!qqXbz0KeF~8F6ft?H;syNE9~DBGg&%kX@>KAsmP~4ek>Z^Q+0+MGcA%j zlrb*s5>(+BQ{^VP;~6%XUE>)6Z3SfRv(wGs;9w*MJ#|?ynmT#m-hcA#gx+%oOW0cribtogBCd8Xg~nI z29Nan2`Z$Ea2$tG_R`Lp8=u3{Fg^r<9(3XsHa5@5q*J>fTw``Jt}c?2dFjtH-zZSI zEJ9LJUe&s&|67=Ia)b%@t$_B9mgYAUF)BoQD1!LTO-RSo zv@CGD{lAIF;SxP%4k6rfej5`gNw27um%_qWxUi3gzhOHS+Ow9j^)wzaOTo!$5nS>c zH-5qI$CZUXTYW>_T3`JwNIeNMt_Q%J%e7^G|N^r0YV*92+aBfAmK^yA2xHuV2g{#vbB~TFn?PdWZOHauW3oK{5_kUjZO#4kb#?E}8sq|BsaxfrxRR0*ZosgQ zuNFjFMc=$}gD%wz!T9=3Ch50x4Iiw*+6L`O@Fm|chI!>(woV(rpo-dX=~df1(~6E~4Qo-Inn^ww;O`L%5YX<8z# z=h$CyGFyDIoy7Zob~gIt*ys+4T|#)uyv_QvW)8?O#8argA~_b-ZW0BN-Q!o3gJz@W7W0H$dHmECozLQ$5tZrvK{4n~(z zQxQR99GTk%51BJ1yPzX+NU&Exr#mm!=q16CtC1GM28jKNC1n!Jq1=J7kHidxen)%g zLM`3 zu^YSq!l%D0$`V!Y65t44z_I*3|Dz1E!<3kOjGU!y+qSevd9`rGQjk3HdPIsBbMWEg z$E1f-XL-vp-SWBJ!Bz?_w;l1!5p<0bl0e1&AMacTYR+o;jqb4#P#Q7_&>aoz{|4mM z)z;c-jj)qWy4?bV>vXzcti+y4fx^ul;WgttO?fe`SWf`}FksC)eJPgquQ2bS>HzzlBTW5C)PL_8l1l?JV% zX#LRaev`7}gt85()*Yv%#@YU#f3+molCgO6{;bGb4bKSh)^XD&8?C_mX*O5r^Yi*k zwKy}ceWmd7hQHjE(USd{aN-Za`WKF`u8A z7LoUw)53s=3WOimu6^g$5G;FD$_!qis>;~bj z#IED^dPQz{c@=%pXVP{J1o>$hkP z$h&c;dHK6BZQM5kV~}$}d=n)(C4PC$cC!2yVo~_B3MUvT4n@nZuTV3=v5LvtkWexh z*iIWyR7zScg*tWXKw0RfD-xeO+kByt)v;VE1&z7w{D*d~*1sPSYnh$NPwATa>q$8R_K+)Qtfo)`7r0i&p>n89e0u%sP0M0$!dWz`96maA4dB~bqCIX zcVT$mny~+#MgCrjR`NysZ^Jl2^pxrVuP~2VAr(07zwKx*4{HvG6&n7Q170Zz`LiT= z6(%H|J9X+eWoLW~oHP!sH(qODY`>QOs?T&R3YvL>hR5L-u?Hv`}`zU zQD)FU^7DJQVA5EDF9Pe*w*$AUF1$1-uOi;I-lzq4h{a zRa;*EE=LR!4clpjR^X}5La4_rCPyTJp~R~cdN5`J4TH3AVQj$@O=~@`?!Ojc z{1BMQWE+IPQhtIQe|N&^U{{oV@5#=?Y!hR}Ecc8(}WiHOnSYmZ42EY{GR-J7#EyUQ=-U#~A*RNv_ z<71hPi%jQY96Ns3R;+mOrJ};`4}Hyx$A&}9$XeTKO|8$XVCHEHZNS+uQXZv zUC%zFT1F2me+7Ot*48(11jo!HtsXRqJ$Uc(t>|jZWX6;{_Hm&hKn_!ks_icjGbO3 zhN=WFhiwzKV9pfg%PPTC9%y0mi1?SAu-`l;%vH;@u6PI(W~^3j_qJER82v^ISD^WN zdU|2^4ohR)bcG6C?!HBX!>mFtvnJX8On_z9!}49ax_fY-TmGB5Hj88GA#XG}lki4P zdLV{w+g0rZ;crL`O%>#TPaBf?Pmq9`Mwvuw<-R1#OskuR`?OD{nw)S15-V~w<(bix zBFfj=wOjnTo^$Kz?vU4g`_-sFeYu(UoknVd{Dc3DJ{{wx)+Ti7#SZd3tlX)EI&S)U z%{9`Z%3tNw|59{45@R!sdBrlNe-$__%22&Bkp~w{2sv1j-~2)G>TQktg+Hnj7ffzy zT2*>`yw|*l&w$do^%Ugh(Fsas8BD-<@u_(Xq>RuS#@@{fEAn}U3s`6k59Gp4{OSs( zhME?5Pe1IMebU!=ezRzD0B0ljsaP=!I9^}BW7CJ`_YZqkeqR5l@fUe0O!A2E@XBqo znts9oRNd+Pi>4y!rris;Rc%5)wBNG+>E+k8n^%JvQ{F~z-kg7<4Psn<-_2&3m&ol% zCl>hB-7Yo1v=kq|b$yr`*@{kTw~9RL3ub|H!|b;-ZP-vb>P+utrrbgB%_*R^0#6Pa zG>8j9_(SK6%iV8#GcqRH9QRt8Ix95LXFfIETZl1vZqOJ~iIBC!D@UD#85f#aL{>3u z=+Hjb={emd!4FS2DZkOoFpbd6KV&MPMNE^*n|frC%Bk5yix?sUh|sx=$OkQLPT<om$oW6}P&|UUdtb7hc=&Mmsb@jcgvV?1qLBF2{e8g6d_yqo#6j zdX*Yo&%H6(wed`2_29`qnb=2|LLNeW#vE}9*}TkcP~kdR}NM3g(BXh^!bO1SBrpctXcCz`Y8*_?W715ilPyw)e$nVP~N zxOx2*y7JtKr?@DRHkTh+{+s*o@W5(@edq;RgwOl=to=bcd4F3}G&0%C;R8Pp>IP1` z!l^%Y?%{v}uKI&~Vxm6GE(k>dks}9X<~=DQRV{OKNaugj{;jJUz>Ed6&FaB5kg4F` zHGKDhANyUmP|*4AMIMV(&&{TGA}ME zF-(NGqN}TGU$=BHX-MVi7WNT7lMsP+eU42$JkSP-@fL1;E+#*NaO-@2?^;Mco;9S! zl=R`zGnhi0{qlGD)aEr#i;tyh@7KCCCzCArR`i;)T&0Hk)W^iZ$m0c{+FH2lN9vwl zI@5@@mb#qo11cNub0gneT{(B@(t%TqyuHSZ81a&gp6j-WYZh1M@#}E(9jeNJhe* zEuO6^BMILbHrSF1zJ8?*P(!}0vcz5QD=iIl`osyw^oW6*_zp3d2sYu^o9?4@M($p* z(SbrWauRnJHqf+kA`kslK9K3qg+Y3_DAoBFmUwXEXr7-qbB2P~Cj5KrWj$O}xa@C> zdwtc3HL{P-%oF*M#PR?$pF)2=_~1AJiW(J)#?A98I%^TAPoxLKSym04ttPsgTO)S` ziehV&s%i~Hx=IS_h=_6$cPhT%iG4{$*aMvKwY4eTKGDQB?gXs?6u)8ob$-k)fo~&Z z_(IWMRs2&7)&qp1486R5tWNA1KR=fGE!jmIi>& z$jWNkq#tYQ|}{yq|w<4 z-8UbTRPKWV$ZsKcfkNu{G^_$cfn~>e+N)NsY|$%-Zl9cwZpaH#UjisPk>Fv;n?7`u ze^0|ibNqPg#PtL6D%6*VulWsq1$NR-|GqfZ0_Vu>HC0{$nGNrFBDSh@+Z@NGw_U4H zK`|p2(k+M~@J5A`R`?0~^JAa130_5h3-y(>c?DEGl=KQ)G;c)?tX1WLvmITXA6%ft3(f< zwr$Q$lDaIr{|(}fQe>=-6M9Y@&XDs6zo zR1XVjL2~l0PRSNv^(u!ln6w_1whQ25hRfnDNwHr7YKwJ7;36=9En*O{>^Kk>wTUom zV>x>r$^heM=fI{f@KJ4=VCBu5fWdIw-L^kbz{8DtGa9=_w%%a zkBJNlPb+418I&)$#%_HA+(DO(LLHM}?y%p890h&vpUU+l4M<(^3@`krOUnoQ#mVP4 z&Wk`)b?MEh@ps!;_cW+5l);Y40d)E|RY(b1Y;ta&=AbShK~RMtS@NIYHKTRw}C#^L7pTiX9H}yN%HR@ z+dh8Z*7%L`likBT1W`F5u(m{By{=jXmN-cT=!rwlXp(6;r{4z948SE;+&{c#7+qQQ zXCCbY`|eTe{NcKd%B)pt_$-Ui>;I+H zX?g&wh2hUBWI5|JtyE-v224WS8B-gBtJPmry?oORBrZJ_4X}YTprQO4eiPtiXhZbI zU;!=BT`2wj3uqy{@F>pB8IYy0QKWolWD^J`P^&ho^W8dc87JMN$B+{bqhGGl=qvX_ z46?du-B460Sf2|olEPM2P)vL}13OT6Ht;sifb5ENX5%?H?A@~m`FHdilLiy+5$+-2 zA|RU>{jlg7HMbK+;azvM))|SJCK3mR-}s+7BQ`~NUQ|1vajtprT36;wz`UXlfdkF~ zFE7hmI}>o9bG1zyu$mE)f0aNt7Jq@^UVLlU_}99IrCmC9yh(Pucr=|WX?a_2Kayl-6fmz={1tgy zVutV5EF76!EmwH@igsiBme#J*9l)Os@vk z7U}p_-nmBq+C=;NO_y=u?-zApCL(u=$&gmT?Sk)b_3xVJ69OxkTfTPFRTiIG!Yxc< za^#xE!0Cd0`6t=DXBBnKkYf*CzWhZiEq{D0GJL{_e)`e!S!A=s&c6Dr&D zyqT7LpX>SczG6Z=?OZ_u@y3mMcJn7acu`_BxW=TC37Mt(jw+g2g@xMy#GICx7x+vM z<`x+HkTWcXYrT4f+`kT-2*HizfNdiM6{>?gu*Ep%DF31@|Mko_Ot*h>i&P?S6ha6O zX;Rn95nCcBZ2h;-0Q4sCrU)6dxH#)v)Z{&Ni52L5692cZ`udO~9m$45t>Dzol={Vu z^-0#kqL2*8tY}2jUK8)W=9P(%GoP$#-?pu>5!#%EDqo(39l}>5l7)k?h((B_vLD?TRjVy>Gz0Pqf1aBvO zuFc+7xG?#7z3(5AmL9sYDhqYmr{BM?HE9xdcjiUM^b=y<9U1@hN>Y#hhrE%Ex=>?8 z-qXNT<78s|CXw3s1_Vr6{j~o;pIO3odc%U`^)D{(mcD9}(J}3Gbkq{VY2L}^_D*7C z1@7ygowS5Mb%;+-nMxUrEqChQpD>bw03r|hRRv?xfA7^6ZGjg+L6@_ErG-@J^azrI z;NtCB&yCexs;%!<{pPo(D?B}$gr?%vE5>@xipYcgcUbFqcU=OEMc_QLBPF^Ic?)Q5!z+1FjFbo;pFx2?g$YjS)G_i2jkSPal6^r@?MYYrG_ z5Tmb=`7#;;Ed`johqjJ!$OU#llGZcUL3`bUE{Y45+{v+_QKz7cVGMIAR$Yf9Wq|VXII$FkVZR{I_yU&uH!Gp-pItlOdzr zY^pnu#_mC!JH|Zf;(|rJ}8ogu1t^TVue= z)=XU=$2g9(wCOOCtux376g-%lnm1|OhmiuA)DPRlnesRadzHotc;w}$y9ymMODQTo z_wGk(7E!cOMH5#y2Kg+Am=`Q63H$%0Sle**7&yYXe}`|&cQ~nVwq61$pIdrM(DEfw11_V zdQmmty2HA4U6EDNf`EQYY-371rizrf@Lc;(-tI^_W;%3#TK9u9;L56M^?UM3Zn&9C zbiwWDX1~H|1HQF&m#qfQhS*^3Qr9c*V19u8`t9p$Po0V1Iq@lej4e{Pk#%NKL(h#} zeh;BN)jmNi^0QaeyLA1`(#n=q0>a>OVJX-eqB%lpr)-w%9d+NjlP7_(628Fs-WT`C ziOhH*4MWT;o^8mjaAFGIU=K_-L*#c7LO4*hdGxqAnXq0K3=d(xfv(gY~msV97PI1U7V+XHz=(gW& z1g8WpLv0=yBYJ4=HOeYPSPLSyRqS&xT%Bd1wLfjq?#O$Zh`T{N1tg$@ywGre!i zc7C^TooV36a`Wi$IT?~=XAc%u{>$wxeHHB{r~>BLqzK?v$2}LXUZ^C~(9qz2^{FF4 z{npbGayf%`Qv2?Klr>NN@nuq6)HRnr^?@^I>q_m~ATw>|49C6h52V{?m0b)gGJj>6 z&@iDgGA1FmsOaXb*w8OGrQ0i&>gdR1bWoDIy6u5`+i@jRzgU0&_^?C24sYw4(u_8h zXH6`em_F3v=@gvRkwfEv2jb)-UBr|QWvJtlV4VRZKyibpkpaoe}rZuaGxQ1C(JlzlRV1f0%kRMZ-PCX=T-9z8WM`L<4S zcw4*RM+}BxfNnCTBB_l9Bh+iuaHYBm?w~Fw#LF}psm7@%ZN~W_t=ZnA{f-k;S6gXj zeR$mugY^{ALpc9=z%gBR4$wQ8PB0-|dml@-|aYBER8_N4= zkEp8A=O=GSUOf8ek00x^Ca3X%Si`OzJ0dQ_{x&nSoccOTl@Clof|qPWu3i7G!x?l* z58P{35Js~Q!r?@x*c79+!d5dfB}te0BeZ}HW#Fzu7)vZt$28ad&$hTmrb>O74o|20 zPYHgk)smMcl%TwuLla^i&)A#bOZ=n80(=r(L#IJ+?2tvs2oh-MhC9HfEZd zT|0L+b#6C5;CR*|nma+`R_fD+_`Cd}JMeWJ)9O=e)=5-mVGF1SYGft^&de*HbP-ZB z^SrKIQwy9|`0N>X&*IC=cH7u26l>qFsk50?GKnxM0+?DAm6hZC9s~8hy0wej2Cj&* z7+l81L0w=Nnu~G;Qyk23^&7WVy?ZyUF+Z|;^M!*ODhLgq+*YTWg)B%`9<=?pv+VfJ z+J}7auHLx$*n7h-hq_6OucLIKj*7mh6uXg{6Y~?;=XvYo1cEFC?28v0ge{o5uW75E zU?Vs>MqYX}UJ`7AX-~hg%kEIDkhJ8aKa|os(6zqrPQVjrsDvWs7)89Bjk65Xn9-H} zm}8KE>nkc3VFBIHn)6HLP3vLTs&|`Gow3XQHqF$Htt6E|r{2vT= z-%D`daGahoWW*RR9C;=JT2WE(SOE78&rM!rVX=(_y2=l?{QWeA$4E=Dn=JT?Yn~0Y zc}$^M(|QyhEK@eO37!JsPp9`4)bTwIp5)sF1sM%FMrILOAqej7^U8qlEe=PSsCcbd zwaWAK?!KDq&ZMUPAu4j1m?5A*B-}brC?yD596?;e;)}k_q)0Y<-)(^BG>djqK1)6g z5r&#rqZo>DnQucQpSaL?u3Cl46M*DD zzPTfy5NLWBvan^6&2J^@Ic;l#ipu9TJhYXuFTD_m_%{G?2y_R%iHl|m36D2t#u*#q8 zTyKnHP0l7zls=QUUlwWsXRo(vG7^qwz+b(s#9h zdi%{9Eh`j%cA*N>xkXz|Jf52q{(C%f!+RVg<3Bi~Ihjd zK6V7(mTed`Iy$`N%Sk>t(%wfWh%PYe_z|)s8+nrap3(nwJw{6{uH_yvc(59P1t3p~AE5Sz}O7$p|LW~gxOgoI- zA8_Tdhqh87{{C{uW_uL-8GN*MYnpkucgKD73KXuZ`8@{K?OD9m-k$LvN2IxcOe>q0 zl;;%|M&wn1pcn@xQu8uj8N8 zbGvr%e_xR*+`MTyv&uXl7;xogllsY-@{m;ei@( z49JJd-YT+>C=wj1lutYadY&GdE6oMR;I&SPh1;evjQ|?Fbn$1a4E-yCpa{-Z%DxyK zY6iq=p35#b@>f6j;^{j_nXsg*EEHO?PZbqcUP3yGCCT8_VmoSMQ(9IQy0y1XtGE{B z^;?d{iSLvud_$5rcD1C)(^%C;D%p$chfI0+t!x_B{tJU)JC zH&vGo?eT70x87ceMlo~0yUcOIS?mIAQf3SRskDAzdo?vxo5gpWfT5>_#cAD;$S1RY zZ5p{ht>VI50}wnYmfAsMj?Laa`JAb8YzwfXsJH3HD=Zi|gS&Oyn-@ZCfAj&!3$(R+ z*)Qim@EQy;6U34QxI;w+{s^ad>L1lDGsIEJA-dOO`tsk90vtRz2Y6Nk)`=v3*g&7t z#Pl;~&cIAMbA}3^MkmAJwm1KAIiuCbNePDsI!{T_FrX3C%|G%7@o@U~gEMTHe_>`8 z_H15=F{^rD%mxTgE3;%!((#3D$JKPFZ}FC!#RVcDXiij=C(>eG;C((Tclk8 z&6V@TKY8&NGI92pcEj)rj=6r8i6O#6CGR|L3hyii)c+ zQceHC4ayiNT-la!!X3`;{QP#~80eqMOMEfj!1euL4H1ugCM~hXuD|o7!Gm$H2+49=Yu|_r5AahuShIBE6opnXyom_Yb-cXnFm!clTa8iX5)O!sbtz{}eH2W(+Rc zj~)rvhp+G2?9=s|v~9Gi3jP~JJK{MnKsMU0Cck<-i`o*X(CV#;$^7}d%i9drW`v@K zhctazL@-(d+>ZC{(+9P1%DfpUZbrmRNUzAVw9I6TR4$T9K{^00Ld z4kV)Mmo3ZrC-cW8+^dADYI_O>=dVt8uvkkiHu2o7X&GP+7q$-E$1aH(a01S>AUrtt z`?>aBBCB8UJdTHf8r8nI;3IzP=X`y`zD2f_=cZQ4$N`&~N}JvV5NA1GA-$f^NBZaO zmFzWrAxkFE2pwhLR5Ck0 z1vGa*jkU5hzqR}f@Pzq0^15KQXYiqku8rG)|By~^xd|$y;CrFk1}}nVlkg=wQ&@ zEarXVj1&_cjr{S?<;?NqsF!iWwrQk4$`If2llQ|x$;-{XyVpVQNdKsZiNy66FW9q*+H?pswVHH_+ zvNNZN;p(b=PPT`2$NLHK6a{p#4%CqI!LC+6cd7NymvLWTMv*=Ymdu$k%``eXIyRQx zZ4*tagEh2G8ez(xF;FBkD<~i;!X2lkCF!G+&VcOczVQ*L}vmU)x3<@2d4S5#F? zUcOwUB%^E*Qs2`azR%b5t(O$DYgwy;)F)eYxLtBOU@Iu-mZ$_c#7u2r-5_{yx8@ZT z414;-UHZ6w7+`bK(u`)!6c;}OcJFYRjxcpL)(DxprDfK#u_V%`0|MrT7^CLxRl}`7 za~SOdhh9t&l-5mpmC@|)=lARP@BRDsy?cv2hj8v!Q|ozpO42cs{TDA%=utS#lm2uT znk5@xw6qjkhq;C40_M}>8aTz$A*c!x9}YL|vr8jir6NEbHusctnHw|qXoVqa^qpIS z#MyMx(MfD35RH1TE2Sg0?|gh+>%CF`fp7Qkuj)Q%2~`S>Wwx%&?L2u8_5`d&Ehv*) z4SG7wfL?_hYxs{`F%%O&Le*6AX_C$?Go{oTzQ;YIB@(6IP|A)Bnp#41#p=U_8FZ|i z|IZqS1Erx)_WAK+gI5*#5ENM`@G`jS{qT2cQ_PsIDH}`E`4U72y@))(x}vc= zzRnRDOs6jgGi(||J1_)9QBhOVoPNH+n*=Z@=Dgc9(ubvNoViZ)&N=6^*NG8mT);Iy zmpNy!H=pmb=L*7Q6>D~nfA$ll;iIJ$5H#tfZ6=}4?qb!9{pU7U37 zo3NDBnbZ8Uw$&%Cp*`j-Ui?ZF$k6Gug9i2JAqY|lF@3kRWnqJNPy_$s@UT|6n#~@% zV3NYT1q+~*ntUGKS@3PMvdGrWi_5>0EP*57k_yF_Genz%KO?*2pZ2${diUxzrMVC+ zrU9Q2q@w1l`gHHUkz@iuhsz=+_I<1R+0ijE4A&*qF;FzgzmK5@kIpzbuIY_ivv+xq z=~C~!x@Id~$Lpn;Og_Dy&U;jq$8Bh*34zUmp8oCq9PZGNJBcvECg2ij)5!TC`@i&R z&+0vkU;-97Q-W`<%pYO%o60#HAaz|34X|e5otJ%$VQ0~rXJl?nysqtVN@WoS`Y-?dY9pb`NxIKX;l>;e z2BT9HZn%OaxN zg#~NS8Vs7UGv9pI8Znwz=+L%*eT$tM_C<1-nw@?GLjl_o zlF6o*tAh#>j~jMMaryn6p-~TQZ!+>BDGAHFYsI&VHh((^1}O3#&Oc1b?K{rkD$P8V z$iQ8)kf0lI0^za(y>@gMBqLMXA?|J$t?rALmOi?kRYiBjKJ)DDT|ehlZLj0AjX1T| z-mnpnh@zRr&7WaA^0~R*Fz9*Gr;qA=R6<5-&BAz{fNv&o!MXlY_#T+y#BI{QJuJluk(H7DCRxSa00L3k{t zwRg7Kc(8}OIs}6Rm#*wVsIxvBMxJV%CdC}4MO9`E6E}avrct_>TzmpnLN>p_@!5}y zO-w?c4P;ap_FXoOB>r@T^w;gLKhk$9qi(s=dK4Q4YW0$lnX!(cqX)db&0d+Q+^wU@ zx^vgAo@H1CZ6YhO+2vA0rEX|oF!Va(BV+s`cp^n-Z~FMV-qK&@?O=VX(p1_cdrt;U0h5(V1GE|YUFfT~#Z=RYH{ z=Rw~@euPs(u}b07&fRXLuh8hOizq)gcYf8D%OnxVI{#@Z-rTEc*9S-F4)&K~5M9>t z*tdHQOzG`h24N7!iv0a-e~ppj#vOm*`s>@uguMG)K&JEODrNgrRa8V>-#Ti{7>iem z$7T~6>qjOPB{+81Eh#CnZ=MvXq7*i^#aO|uG|HqnNLD_c2q#3CJC-S%TkjWbb7lKo zE9+t>y?Ip+dNkr@-;psJ_Kdfg{HI+=?6--C%4h;)_0Ns`^ZgpTnjiIi5qJHzx~?5F zeyyEr+Ff}~KhHtf@D#ZAGC60mWD&W|rf9Dqi^_h<_`HX2ja=wbLz!;B-|X5#m4q6& z6sE47GQNArdvJ?l00qi%YK*nGT4%a5wf@-boV)!bIV!p#j(7L8^V%{*y(?hA*Nu=x z6CDf-m;f-)XFQS~0+twR|N8Z<)D2#Ng^(giSWt_+W2Bv(-3uMt`SXuaf-jxF7Y}?t zKf^O)m(y`{nAG$w)7Z)i(Z|MIJA3=+AD=5|SaMTm*r=XsFEwVIM?`Bm+N-MCDk}fd z*|bkoqgt`~ZDH+#Q|^)rrb^@bbY_+?1d&Osi-&^7`TfhC&M&b4ZH5);mZ~}liInDslK!0Uvu9OKJOpeQ4`UKchCtn* zw}3Nqaq$h|Hi=H9e##U6!>{wNW*w*W<9anQQ4n^!=SuH$4hxP9O3S;i9CtwZEJRe` z^YnVw;r{*me7(s7x(@sp5gXftKTzbYlXy{B8ymaF=mQEBuFqu-;N{Da)z7*}fEl!j!H{U*J)K@faC(+x9iB_hbs)Y1>cu?GF5jm!e9pIo_ zqs_zy0pT8D;G8FgfZ0mTr$&oNR4~i&@$u15Hc-S1uik%X%_7EC7joTV&@p-zQ?!UF zhK5f-{aY|W>l>RsK+P0r*ppRP76P6!ktD;$Fi@+2!IOQgv2#;AvC+V)|1hwk7A)Ff z=C3AO3I?v+c?Jd-Hw@gpRrmrrgj0O8C-tQK{QduMgTX;`^B>ouY)32DQA)h%l(_Fq z2oLu(P>cyIBXv^1wfOF%&ZTY}%ZrJ)APRlA9W(m@p&0d#d#o*eS!T-V2^*r=CCTPh zho^x>M~P`Zb|bT@hi|&!3~u10)kJROpCvqoiiAoVZ&&hArU~dTwg^_}>{u;Y%S?$R z6S1TP%ql?^T(oF}>>&ooj8FJ}NT`W5HD$-BMK0X%707`k9=kkx#9cac=u67XaF@#B zGGZ2*4p=&eBMOF{cu1QLu^}Ku7lYd!B%kDjv0;2K-=8Pw{lWyNaMfI?{wh-}&Y<2t znGBVF#ztQXkD4$WcguV!c@Z_6qrCV`mX=yScxh&lj zkLK%%|Lf?hSHD3Q;0ur#(v!NDkidt;PCEgLkBbp}6}Z7d$j|47vGjXJJ(b7EId<^bf@&5^2mBU=-s0i--l3G=IFw`dOqkB<>_7IPn6$ zQ?|1adrC{|9DcipC3OWTTJhx zeKU3IdOnNx-_^?ajpBXe-n%&k1+J|kx6(5Vx!^oH!t#aW0=@t!nWuz-i=*eP%xb3d2H+vTIK4+goG{0D`|ePeBD8Pfr+6Hi~sRRNYFNJ+GO9XE}B<{39!Q;OJsf6L3^SWYdSPHGO|6Yu%(h{s#XkfDSqhe#hBVvk{ z=44T>B(o}TDA?U>_CzB*|8f=!Y`n;;v`J`iP^eqP^abeB6h#bYz_Iif#h#8?$sN$|%1@o8-N)PUCM80K=>ikktb zV(Wu)liNR9>x5$Nj0=hw^bu#7fQz3A7nn19fL3QVf@?(RnwQMLCy79aOY69l2_2;N zqt(>%($j}dnG)FN-!EUDmVe*kfEb4JwNG5$*)+&*RPwH62(Z|%;`%D zc>a(A7_7$W|Ni4gxUmxN#jz2*kLdLY1pB|UWT*RrAyWC-d(|vZ1TC-MXKIf z8%x_(SUc2T!AMIp#W2IZB>j}8;0?_DgS)$Z+Ni{5(6Ba|JCwBhS0V6W+^lQEz3N-n zZ~w(kk?yxf-5+AVsj1A~yLQE|3+8r}*zF-$w(BH|-_rF}3Y;94R*S7h`xSh$=nPvc z;=%(VQ>F3l5WUe^5-+#?6%C!k+mB$bnb2Rex26ooZ%6b5*3cv=;4l=~ue$$YVC zEtW2M6}3KBKKN`?oFs=k8KD4^hlGDC{HM%M@2%7C$@H3b|u{~_q2R1ifRi3`WBH3{g;E%QTAu_@@jCEDlYn&?e6>eu*Moo$<4uVef*FBl>N#{ukWGU3AcS zn!Fu9c>AEL#Hkg1V__;f?t>nnI?Nv|CUi^|ZRt<0r#wnsE|zFYb?Y_gb8&k*xNn;S zkO=czi?;>$t|gv!z!K>_F1zy2f

    ?D~Xh>c5@b$gVDo{+5pxhKHi|GJx^=*-qa4bTca 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

    <(zmRKK_v$W-p~*+u)?m_v2Q(ux~^ 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

    4hh5H=T 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�FIXX2>A#etW=a_Q2t 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 FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Brig "get-users-by-ids", - MakesFederatedCall 'Brig "api-version" - ] + :<|> FedEndpoint "leave-conversation" LeaveConversationRequest LeaveConversationResponse -- used by a remote backend to send a message to a conversation owned by -- this backend - :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-message-sent", - MakesFederatedCall 'Brig "get-user-clients" - ] + :<|> FedEndpoint "send-message" ProteusMessageSendRequest MessageSendResponse - :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Brig "get-users-by-ids", - MakesFederatedCall 'Galley "on-mls-message-sent" - ] + :<|> FedEndpoint "update-conversation" ConversationUpdateRequest ConversationUpdateResponse :<|> FedEndpoint "mls-welcome" MLSWelcomeRequest MLSWelcomeResponse - :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "send-mls-message", - MakesFederatedCall 'Brig "get-mls-clients" - ] + :<|> FedEndpoint "send-mls-message" MLSMessageSendRequest MLSMessageResponse - :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "mls-welcome", - MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent", - MakesFederatedCall 'Galley "send-mls-commit-bundle", - MakesFederatedCall 'Brig "get-mls-clients", - MakesFederatedCall 'Brig "get-users-by-ids", - MakesFederatedCall 'Brig "api-version" - ] + :<|> FedEndpoint "send-mls-commit-bundle" MLSMessageSendRequest MLSMessageResponse :<|> FedEndpoint "query-group-info" GetGroupInfoRequest GetGroupInfoResponse :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-typing-indicator-updated" + '[ ] "update-typing-indicator" TypingDataUpdateRequest @@ -153,8 +126,7 @@ type GalleyApi = DeleteSubConversationFedRequest DeleteSubConversationResponse :<|> FedEndpointWithMods - '[ MakesFederatedCall 'Galley "on-mls-message-sent", - From 'V1 + '[ From 'V1 ] "leave-sub-conversation" LeaveSubConversationRequest diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Component.hs b/libs/wire-api-federation/src/Wire/API/Federation/Component.hs index dcf029f1d8a..ecf07fad60a 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Component.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Component.hs @@ -23,7 +23,7 @@ where import Data.Proxy import Imports -import Wire.API.MakesFederatedCall (Component (..)) +import Wire.API.Component (Component (..)) componentName :: Component -> Text componentName Brig = "brig" diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index 86207e36a72..73d028de763 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -82,7 +82,6 @@ library -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -Wredundant-constraints -Wunused-packages - -fplugin=TransitiveAnns.Plugin build-depends: aeson >=2.0.1.0 @@ -116,7 +115,6 @@ library , text >=0.11 , time >=1.8 , transformers - , transitive-anns , types-common , wai-utilities , wire-api diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index c4f7828d2c3..b7a9e127622 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -23,7 +23,6 @@ , cereal , comonad , conduit -, constraints , containers , cookie , crypton @@ -100,7 +99,6 @@ , these , time , tinylog -, transitive-anns , types-common , unliftio , unordered-containers @@ -136,7 +134,6 @@ mkDerivation { cereal comonad conduit - constraints containers cookie crypton @@ -202,7 +199,6 @@ mkDerivation { these time tinylog - transitive-anns types-common unordered-containers uri-bytestring diff --git a/libs/wire-api/src/Wire/API/Component.hs b/libs/wire-api/src/Wire/API/Component.hs new file mode 100644 index 00000000000..486c870d655 --- /dev/null +++ b/libs/wire-api/src/Wire/API/Component.hs @@ -0,0 +1,69 @@ +-- 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 . +{-# LANGUAGE OverloadedLists #-} +{-# OPTIONS_GHC -Wno-redundant-constraints #-} + +module Wire.API.Component + ( Component (..), + ShowComponent, + ) +where + +import Data.Aeson +import Data.Schema +import GHC.TypeLits +import Imports +import Servant.API +import Test.QuickCheck (Arbitrary) +import Wire.Arbitrary (GenericUniform (..)) + +data Component + = Brig + | Galley + | Cargohold + deriving (Show, Eq, Generic) + deriving (Arbitrary) via (GenericUniform Component) + deriving (ToJSON, FromJSON) via (Schema Component) + +instance ToSchema Component where + schema = + enum @Text "Component" $ + mconcat + [ element "brig" Brig, + element "galley" Galley, + element "cargohold" Cargohold + ] + +instance FromHttpApiData Component where + parseUrlPiece :: Text -> Either Text Component + parseUrlPiece = \case + "brig" -> Right Brig + "galley" -> Right Galley + "cargohold" -> Right Cargohold + c -> Left $ "Invalid component: " <> c + +instance ToHttpApiData Component where + toUrlPiece = \case + Brig -> "brig" + Galley -> "galley" + Cargohold -> "cargohold" + +-- | Get a symbol representation of our component. +type family ShowComponent (x :: Component) = (res :: Symbol) | res -> x where + ShowComponent 'Brig = "brig" + ShowComponent 'Galley = "galley" + ShowComponent 'Cargohold = "cargohold" diff --git a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs b/libs/wire-api/src/Wire/API/MakesFederatedCall.hs deleted file mode 100644 index a076e39ea85..00000000000 --- a/libs/wire-api/src/Wire/API/MakesFederatedCall.hs +++ /dev/null @@ -1,403 +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 . -{-# LANGUAGE OverloadedLists #-} -{-# OPTIONS_GHC -Wno-redundant-constraints #-} - -module Wire.API.MakesFederatedCall - ( CallsFed, - MakesFederatedCall, - Component (..), - callsFed, - AddAnnotation, - Location (..), - ShowComponent, - Annotation, - HasFeds (..), - FedCallFrom' (..), - Calls (..), - Wire.API.MakesFederatedCall.exposeAnnotations, - ) -where - -import Control.Lens ((%~)) -import Control.Monad.State (State, evalState, get, gets, modify) -import Data.Aeson -import Data.ByteString.Char8 (unpack) -import Data.Constraint -import Data.Kind -import Data.Map qualified as M -import Data.Metrics.Servant -import Data.OpenApi qualified as S -import Data.Proxy -import Data.Schema -import Data.Text qualified as T -import GHC.TypeLits -import Imports -import Servant.API -import Servant.API.Extended (ReqBodyCustomError') -import Servant.API.Extended.RawM qualified as RawM -import Servant.Client -import Servant.Multipart -import Servant.OpenApi -import Servant.Server -import Test.QuickCheck (Arbitrary) -import TransitiveAnns.Types -import Unsafe.Coerce (unsafeCoerce) -import Wire.API.Deprecated (Deprecated) -import Wire.API.Error (CanThrow, CanThrowMany) -import Wire.API.Routes.Bearer (Bearer) -import Wire.API.Routes.Cookies (Cookies) -import Wire.API.Routes.LowLevelStream (LowLevelStream) -import Wire.API.Routes.MultiVerb (MultiVerb) -import Wire.API.Routes.Named -import Wire.API.Routes.Public -import Wire.API.Routes.QualifiedCapture (QualifiedCapture', WithDomain) -import Wire.API.Routes.Version -import Wire.API.Routes.Versioned (VersionedReqBody) -import Wire.API.Routes.WebSocket (WebSocketPending) -import Wire.API.SwaggerServant (OmitDocs) -import Wire.Arbitrary (GenericUniform (..)) - --- | This function exists only to provide a convenient place for the --- @transitive-anns@ plugin to solve the 'ToHasAnnotations' constraint. This is --- highly magical and warrants a note. --- --- The call @'exposeAnnotations' (some expr here)@ will expand to @some expr --- here@, additionally generating wanted 'HasAnnotation' constraints for every --- 'AddAnnotation' constraint in the _transitive call closure_ of @some expr --- here@. --- --- The use case is always going to be @'callsFed' ('exposeAnnotations' expr)@, --- where 'exposeAnnotations' re-introduces all of the constraints we've been --- squirreling away, and 'callsFed' is responsible for discharging them. It --- would be very desirable to combine these into one call, but the semantics of --- solving 'ToHasAnnotations' attaches the wanted calls to the same place as --- the call itself, which means the wanteds appear just after our opportunity --- to solve them via 'callsFed'. This is likely not a hard limitation. --- --- The @x@ parameter here is intentionally ambiguous, existing as a unique --- skolem to prevent GHC from caching the results of solving --- 'ToHasAnnotations'. Callers needn't worry about it. -exposeAnnotations :: (ToHasAnnotations x) => a -> a -exposeAnnotations = id - -data Component - = Brig - | Galley - | Cargohold - deriving (Show, Eq, Generic) - deriving (Arbitrary) via (GenericUniform Component) - deriving (ToJSON, FromJSON) via (Schema Component) - -instance ToSchema Component where - schema = - enum @Text "Component" $ - mconcat - [ element "brig" Brig, - element "galley" Galley, - element "cargohold" Cargohold - ] - -instance FromHttpApiData Component where - parseUrlPiece :: Text -> Either Text Component - parseUrlPiece = \case - "brig" -> Right Brig - "galley" -> Right Galley - "cargohold" -> Right Cargohold - c -> Left $ "Invalid component: " <> c - -instance ToHttpApiData Component where - toUrlPiece = \case - Brig -> "brig" - Galley -> "galley" - Cargohold -> "cargohold" - --- | A typeclass corresponding to calls to federated services. This class has --- no methods, and exists only to automatically propagate information up to --- servant. --- --- The only way to discharge this constraint is via 'callsFed', which should be --- invoked for each federated call when connecting handlers to the server --- definition. -type CallsFed (comp :: Component) = HasAnnotation 'Remote (ShowComponent comp) - --- | A typeclass with the same layout as 'CallsFed', which exists only so we --- can discharge 'CallsFeds' constraints by unsafely coercing this one. -class Nullary - -instance Nullary - --- | Construct a dictionary for 'CallsFed'. -synthesizeCallsFed :: forall (comp :: Component) (name :: Symbol). Dict (CallsFed comp name) -synthesizeCallsFed = unsafeCoerce $ Dict @Nullary - --- | Servant combinator for tracking calls to federated calls. Annotating API --- endpoints with 'MakesFederatedCall' is the only way to eliminate 'CallsFed' --- constraints on handlers. -data MakesFederatedCall (comp :: Component) (name :: Symbol) - -instance (HasServer api ctx) => HasServer (MakesFederatedCall comp name :> api :: Type) ctx where - -- \| This should have type @CallsFed comp name => ServerT api m@, but GHC - -- complains loudly thinking this is a polytype. We need to introduce the - -- 'CallsFed' constraint so that we can eliminate it via - -- 'synthesizeCallsFed', which otherwise is too-high rank for GHC to notice - -- we've solved our constraint. - type ServerT (MakesFederatedCall comp name :> api) m = Dict (CallsFed comp name) -> ServerT api m - route _ ctx f = route (Proxy @api) ctx $ fmap ($ synthesizeCallsFed @comp @name) f - hoistServerWithContext _ ctx f s = hoistServerWithContext (Proxy @api) ctx f . s - -instance (HasLink api) => HasLink (MakesFederatedCall comp name :> api :: Type) where - type MkLink (MakesFederatedCall comp name :> api) x = MkLink api x - toLink f _ l = toLink f (Proxy @api) l - -instance (RoutesToPaths api) => RoutesToPaths (MakesFederatedCall comp name :> api :: Type) where - getRoutes = getRoutes @api - --- | Get a symbol representation of our component. -type family ShowComponent (x :: Component) = (res :: Symbol) | res -> x where - ShowComponent 'Brig = "brig" - ShowComponent 'Galley = "galley" - ShowComponent 'Cargohold = "cargohold" - -type instance - SpecialiseToVersion v (MakesFederatedCall comp name :> api) = - MakesFederatedCall comp name :> SpecialiseToVersion v api - --- | 'MakesFederatedCall' annotates the swagger documentation with an extension --- tag @x-wire-makes-federated-calls-to@. -instance (HasOpenApi api, KnownSymbol name, KnownSymbol (ShowComponent comp)) => HasOpenApi (MakesFederatedCall comp name :> api :: Type) where - toOpenApi _ = - toOpenApi (Proxy @api) - -- Append federated call line to the description of routes - -- that perform calls to federation members. - & S.allOperations - . S.description - %~ pure . maybe call (\d -> d <> "
    " <> call) - where - call :: Text - call = - T.pack "Calls federation service " - <> T.pack (symbolVal $ Proxy @(ShowComponent comp)) - <> T.pack " on " - <> T.pack (symbolVal $ Proxy @name) - -instance (HasClient m api) => HasClient m (MakesFederatedCall comp name :> api :: Type) where - type Client m (MakesFederatedCall comp name :> api) = Client m api - clientWithRoute p _ = clientWithRoute p $ Proxy @api - hoistClientMonad p _ f c = hoistClientMonad p (Proxy @api) f c - --- | Type class to automatically lift a function of the form @(c1, c2, ...) => --- r@ into @Dict c1 -> Dict c2 -> ... -> r@. -class SolveCallsFed c r a where - -- | Safely discharge a 'CallsFed' constraint. Intended to be used when - -- connecting your handler to the server router. - -- - -- This function should always be called with an argument of - -- 'exposeAnnotations'. See the documentation there for more information on - -- why. - callsFed :: ((c) => r) -> a - -instance (c ~ ((k, d) :: Constraint), SolveCallsFed d r a) => SolveCallsFed c r (Dict k -> a) where - callsFed f Dict = callsFed @d @r @a f - -instance {-# OVERLAPPABLE #-} (c ~ (() :: Constraint), r ~ a) => SolveCallsFed c r a where - callsFed f = f - -data FedCallFrom' f = FedCallFrom - { name :: f String, - method :: f String, - fedCalls :: Calls - } - -deriving instance Show (FedCallFrom' Maybe) - -deriving instance Show (FedCallFrom' Identity) - -type FedCallFrom = FedCallFrom' Maybe - --- Merge the maps, perserving as much unique info as possible. -instance Semigroup (FedCallFrom' Maybe) where - a <> b = - FedCallFrom - (name a <|> name b) - (method a <|> method b) - (fedCalls a <> fedCalls b) - -instance Semigroup (FedCallFrom' Identity) where - a <> b = - FedCallFrom - (name a) - (method a) - (fedCalls a <> fedCalls b) - -instance Monoid FedCallFrom where - mempty = FedCallFrom mempty mempty mempty - -newtype Calls = Calls - { unCalls :: Map String [String] - } - deriving (Eq, Ord, Show) - -instance Semigroup Calls where - Calls a <> Calls b = Calls $ M.unionWith (\na nb -> nub . sort $ na <> nb) a b - -instance Monoid Calls where - mempty = Calls mempty - -class HasFeds a where - getFedCalls :: Proxy a -> State FedCallFrom [FedCallFrom] - --- Here onwards are all of the interesting instances that have something we care about -instance (KnownSymbol seg, HasFeds rest) => HasFeds (seg :> rest) where - getFedCalls _ = do - let segString = "/" <> T.unpack (T.dropAround (== '"') $ renderSymbol @seg) - modify $ appendName segString - getFedCalls $ Proxy @rest - -instance (KnownSymbol capture, HasFeds rest) => HasFeds (Capture' mods capture a :> rest) where - getFedCalls _ = do - let segString = "/{" <> T.unpack (T.dropAround (== '"') $ renderSymbol @capture) <> "}" - modify $ appendName segString - getFedCalls $ Proxy @rest - -instance (KnownSymbol capture, KnownSymbol (AppendSymbol capture "_domain"), HasFeds rest) => HasFeds (QualifiedCapture' mods capture a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @(WithDomain mods capture a rest) - -instance (ReflectMethod method) => HasFeds (LowLevelStream method status headers desc ctype) where - getFedCalls _ = do - modify $ \s -> s {method = getMethod @method} - gets pure - -instance (HasFeds rest, KnownSymbol (ShowComponent comp), KnownSymbol name) => HasFeds (MakesFederatedCall comp name :> rest) where - getFedCalls _ = do - let call = - M.singleton - (symbolVal $ Proxy @(ShowComponent comp)) - (pure (symbolVal $ Proxy @name)) - modify $ \s -> s {fedCalls = fedCalls s <> Calls call} - getFedCalls $ Proxy @rest - -instance (ReflectMethod method) => HasFeds (MultiVerb method cs as r) where - getFedCalls _ = do - modify $ \s -> s {method = getMethod @method} - gets pure - -instance (ReflectMethod method) => HasFeds (Verb method status cts a) where - getFedCalls _ = do - modify $ \s -> s {method = getMethod @method} - gets pure - -instance (ReflectMethod method) => HasFeds (NoContentVerb method) where - getFedCalls _ = do - modify $ \s -> s {method = getMethod @method} - gets pure - -instance (ReflectMethod method) => HasFeds (Stream method status framing ct a) where - getFedCalls _ = do - modify $ \s -> s {method = getMethod @method} - gets pure - -instance HasFeds WebSocketPending where - getFedCalls _ = do - modify $ \s -> s {method = pure $ show GET} - gets pure - -instance (HasFeds route, HasFeds routes) => HasFeds (route :<|> routes) where - getFedCalls _ = do - s <- get - -- Use what state we have up until now, as it might be a funky style of endpoint. - -- Routes will usually specify their own name, as we don't have a style of sharing - -- a route name between several HTTP methods. - let a = evalState (getFedCalls $ Proxy @route) s - b = evalState (getFedCalls $ Proxy @routes) s - pure $ a <> b - -instance HasFeds EmptyAPI where - getFedCalls _ = gets pure - -instance HasFeds Raw where - getFedCalls _ = gets pure - -instance HasFeds RawM.RawM where - getFedCalls _ = gets pure - -getMethod :: forall method. (ReflectMethod method) => Maybe String -getMethod = pure . fmap toLower . unpack . reflectMethod $ Proxy @method - -appendName :: String -> FedCallFrom -> FedCallFrom -appendName toAppend s = s {name = pure $ maybe toAppend (<> toAppend) $ name s} - --- All of the boring instances live here. -instance (RenderableSymbol name, HasFeds rest) => HasFeds (Named name rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Header' mods name a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (ReqBody' mods cts a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (StreamBody' opts framing ct a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Summary summary :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (QueryParam' mods name a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (MultipartForm tag a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (QueryFlag a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Description desc :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Deprecated :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (CanThrow e :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (CanThrowMany es :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Bearer a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (Cookies cs :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (ZHostOpt :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (ZAuthServant ztype opts :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (ReqBodyCustomError' mods cts tag a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (DescriptionOAuthScope scope :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (OmitDocs :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest - -instance (HasFeds rest) => HasFeds (VersionedReqBody v cts a :> rest) where - getFedCalls _ = getFedCalls $ Proxy @rest 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 31c1b018e33..144b8db270f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -65,7 +65,6 @@ import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig import Wire.API.MLS.CipherSuite -import Wire.API.MakesFederatedCall import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Brig.EJPD @@ -170,8 +169,6 @@ type AccountAPI = -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID -- - UserIdentityUpdated event to created user, if email or phone get activated ( "users" - :> MakesFederatedCall 'Brig "on-user-deleted-connections" - :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[Servant.JSON] NewUser :> MultiVerb 'POST '[Servant.JSON] RegisterInternalResponses (Either RegisterError SelfProfile) ) @@ -179,8 +176,6 @@ type AccountAPI = "createUserNoVerifySpar" ( "users" :> "spar" - :> MakesFederatedCall 'Brig "on-user-deleted-connections" - :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[Servant.JSON] NewUserSpar :> MultiVerb 'POST '[Servant.JSON] CreateUserSparInternalResponses (Either CreateUserSparError SelfProfile) ) @@ -649,16 +644,12 @@ type AuthAPI = Named "legalhold-login" ( "legalhold-login" - :> MakesFederatedCall 'Brig "on-user-deleted-connections" - :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[JSON] LegalHoldLogin :> MultiVerb1 'POST '[JSON] TokenResponse ) :<|> Named "sso-login" ( "sso-login" - :> MakesFederatedCall 'Brig "on-user-deleted-connections" - :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[JSON] SsoLogin :> QueryParam' [Optional, Strict] "persist" Bool :> MultiVerb1 'POST '[JSON] TokenResponse 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 ea493672d82..cdb072d749f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -35,7 +35,6 @@ import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Conversation import Wire.API.FederationStatus -import Wire.API.MakesFederatedCall import Wire.API.Provider.Service (ServiceRef) import Wire.API.Routes.Features import Wire.API.Routes.Internal.Brig.EJPD @@ -56,11 +55,6 @@ import Wire.API.Team.Member import Wire.API.Team.SearchVisibility import Wire.API.User.Client -type LegalHoldFeaturesStatusChangeFederatedCalls = - '[ MakesFederatedCall 'Galley "on-conversation-updated", - MakesFederatedCall 'Galley "on-mls-message-sent" - ] - type family IFeatureAPI1 cfg where -- special case for classified domains, since it cannot be set IFeatureAPI1 ClassifiedDomainsConfig = IFeatureStatusGet ClassifiedDomainsConfig @@ -121,8 +115,6 @@ type InternalAPIBase = "delete-user" ( Summary "Remove a user from their teams and conversations and erase their clients" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" :> ZLocalUser :> ZOptConn :> "user" @@ -134,9 +126,6 @@ type InternalAPIBase = :<|> Named "connect" ( Summary "Create a connect conversation (deprecated)" - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Galley "on-conversation-created" - :> MakesFederatedCall 'Galley "on-conversation-updated" :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation :> CanThrow 'NotConnected 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 53ebe332541..424ac403d81 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -49,7 +49,6 @@ import Wire.API.Error.Empty import Wire.API.MLS.CipherSuite import Wire.API.MLS.KeyPackage import Wire.API.MLS.Servant -import Wire.API.MakesFederatedCall import Wire.API.OAuth import Wire.API.Properties (PropertyKey, PropertyKeysAndValues, RawPropertyValue) import Wire.API.Routes.API @@ -153,7 +152,6 @@ type UserAPI = Named "get-user-unqualified" ( Summary "Get a user by UserId" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> Until 'V2 :> ZLocalUser :> "users" @@ -165,7 +163,6 @@ type UserAPI = Named "get-user-qualified" ( Summary "Get a user by Domain and UserId" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> "users" :> QualifiedCaptureUserId "uid" @@ -186,8 +183,6 @@ type UserAPI = "get-handle-info-unqualified" ( Summary "(deprecated, use /search/contacts) Get information on a user handle" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-by-handle" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZUser :> "users" :> "handles" @@ -204,8 +199,6 @@ type UserAPI = "get-user-by-handle-qualified" ( Summary "(deprecated, use /search/contacts) Get information on a user handle" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-by-handle" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZUser :> "users" :> "by-handle" @@ -225,7 +218,6 @@ type UserAPI = ( Summary "List users (deprecated)" :> Until 'V2 :> Description "The 'ids' and 'handles' parameters are mutually exclusive." - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZUser :> "users" :> QueryParam' [Optional, Strict, Description "User IDs of users to fetch"] "ids" (CommaSeparatedList UserId) @@ -236,7 +228,6 @@ type UserAPI = "list-users-by-ids-or-handles" ( Summary "List users" :> Description "The 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive." - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZUser :> From 'V4 :> "list-users" @@ -249,7 +240,6 @@ type UserAPI = "list-users-by-ids-or-handles@V3" ( Summary "List users" :> Description "The 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive." - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZUser :> Until 'V4 :> "list-users" @@ -315,7 +305,6 @@ type SelfAPI = \password, it must be provided. if password is correct, or if neither \ \a verified identity nor a password exists, account deletion \ \is scheduled immediately." - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'InvalidUser :> CanThrow 'InvalidCode :> CanThrow 'BadCredentials @@ -333,7 +322,6 @@ type SelfAPI = Named "put-self" ( Summary "Update your profile." - :> MakesFederatedCall 'Brig "send-connection-action" :> ZLocalUser :> ZConn :> "self" @@ -361,7 +349,6 @@ type SelfAPI = :> Description "Your phone number can only be removed if you also have an \ \email address and a password." - :> MakesFederatedCall 'Brig "send-connection-action" :> ZUser :> "self" :> "phone" @@ -376,7 +363,6 @@ type SelfAPI = :> Description "Your email address can only be removed if you also have a \ \phone number." - :> MakesFederatedCall 'Brig "send-connection-action" :> ZUser :> "self" :> "email" @@ -408,7 +394,6 @@ type SelfAPI = :<|> Named "change-locale" ( Summary "Change your locale." - :> MakesFederatedCall 'Brig "send-connection-action" :> ZLocalUser :> ZConn :> "self" @@ -419,8 +404,6 @@ type SelfAPI = :<|> Named "change-handle" ( Summary "Change your handle." - :> MakesFederatedCall 'Brig "send-connection-action" - :> MakesFederatedCall 'Brig "send-connection-action" :> ZLocalUser :> ZConn :> "self" @@ -496,7 +479,6 @@ type AccountAPI = "If the environment where the registration takes \ \place is private and a registered email address \ \is not whitelisted, a 403 error is returned." - :> MakesFederatedCall 'Brig "send-connection-action" :> "register" :> ReqBody '[JSON] NewUserPublic :> MultiVerb 'POST '[JSON] RegisterResponses (Either RegisterError RegisterSuccess) @@ -507,7 +489,6 @@ type AccountAPI = :<|> Named "verify-delete" ( Summary "Verify account deletion with a code." - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'InvalidCode :> "delete" :> ReqBody '[JSON] VerifyDeleteUser @@ -519,7 +500,6 @@ type AccountAPI = :<|> Named "get-activate" ( Summary "Activate (i.e. confirm) an email address." - :> MakesFederatedCall 'Brig "send-connection-action" :> Description "See also 'POST /activate' which has a larger feature set." :> CanThrow 'UserKeyExists :> CanThrow 'InvalidActivationCodeWrongUser @@ -546,7 +526,6 @@ type AccountAPI = :> Description "Activation only succeeds once and the number of \ \failed attempts for a valid key is limited." - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'UserKeyExists :> CanThrow 'InvalidActivationCodeWrongUser :> CanThrow 'InvalidActivationCodeWrongCode @@ -659,7 +638,6 @@ type PrekeyAPI = "get-users-prekeys-client-unqualified" ( Summary "(deprecated) Get a prekey for a specific client of a user." :> Until 'V2 - :> MakesFederatedCall 'Brig "claim-prekey" :> ZUser :> "users" :> CaptureUserId "uid" @@ -670,7 +648,6 @@ type PrekeyAPI = :<|> Named "get-users-prekeys-client-qualified" ( Summary "Get a prekey for a specific client of a user." - :> MakesFederatedCall 'Brig "claim-prekey" :> ZUser :> "users" :> QualifiedCaptureUserId "uid" @@ -682,7 +659,6 @@ type PrekeyAPI = "get-users-prekey-bundle-unqualified" ( Summary "(deprecated) Get a prekey for each client of a user." :> Until 'V2 - :> MakesFederatedCall 'Brig "claim-prekey-bundle" :> ZUser :> "users" :> CaptureUserId "uid" @@ -692,7 +668,6 @@ type PrekeyAPI = :<|> Named "get-users-prekey-bundle-qualified" ( Summary "Get a prekey for each client of a user." - :> MakesFederatedCall 'Brig "claim-prekey-bundle" :> ZUser :> "users" :> QualifiedCaptureUserId "uid" @@ -716,7 +691,6 @@ type PrekeyAPI = ( Summary "(deprecated) Given a map of user IDs to client IDs return a prekey for each one." :> Description "You can't request information for more users than maximum conversation size." - :> MakesFederatedCall 'Brig "claim-multi-prekey-bundle" :> ZUser :> Until 'V4 :> "users" @@ -729,7 +703,6 @@ type PrekeyAPI = ( Summary "(deprecated) Given a map of user IDs to client IDs return a prekey for each one." :> Description "You can't request information for more users than maximum conversation size." - :> MakesFederatedCall 'Brig "claim-multi-prekey-bundle" :> ZUser :> From 'V4 :> "users" @@ -750,7 +723,6 @@ type UserClientAPI = "add-client-v6" ( Summary "Register a new client" :> Until 'V7 - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'TooManyClients :> CanThrow 'MissingAuth :> CanThrow 'MalformedPrekeys @@ -773,7 +745,6 @@ type UserClientAPI = "add-client" ( Summary "Register a new client" :> From 'V6 - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'TooManyClients :> CanThrow 'MissingAuth :> CanThrow 'MalformedPrekeys @@ -947,7 +918,6 @@ type ClientAPI = "get-user-clients-unqualified" ( Summary "Get all of a user's clients" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-clients" :> "users" :> CaptureUserId "uid" :> "clients" @@ -956,7 +926,6 @@ type ClientAPI = :<|> Named "get-user-clients-qualified" ( Summary "Get all of a user's clients" - :> MakesFederatedCall 'Brig "get-user-clients" :> "users" :> QualifiedCaptureUserId "uid" :> "clients" @@ -966,7 +935,6 @@ type ClientAPI = "get-user-client-unqualified" ( Summary "Get a specific client of a user" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-clients" :> "users" :> CaptureUserId "uid" :> "clients" @@ -976,7 +944,6 @@ type ClientAPI = :<|> Named "get-user-client-qualified" ( Summary "Get a specific client of a user" - :> MakesFederatedCall 'Brig "get-user-clients" :> "users" :> QualifiedCaptureUserId "uid" :> "clients" @@ -987,7 +954,6 @@ type ClientAPI = "list-clients-bulk" ( Summary "List all clients for a set of user ids" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-clients" :> ZUser :> "users" :> "list-clients" @@ -998,7 +964,6 @@ type ClientAPI = "list-clients-bulk-v2" ( Summary "List all clients for a set of user ids" :> Until 'V2 - :> MakesFederatedCall 'Brig "get-user-clients" :> ZUser :> "users" :> "list-clients" @@ -1011,7 +976,6 @@ type ClientAPI = ( Summary "List all clients for a set of user ids" :> Description "If a backend is unreachable, the clients from that backend will be omitted from the response" :> From 'V2 - :> MakesFederatedCall 'Brig "get-user-clients" :> ZUser :> "users" :> "list-clients" @@ -1032,7 +996,6 @@ type ConnectionAPI = "create-connection-unqualified" ( Summary "Create a connection to another user" :> Until 'V2 - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'MissingLegalholdConsentOldClients :> CanThrow 'MissingLegalholdConsent :> CanThrow 'InvalidUser @@ -1056,8 +1019,6 @@ type ConnectionAPI = :<|> Named "create-connection" ( Summary "Create a connection to another user" - :> MakesFederatedCall 'Brig "get-users-by-ids" - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'MissingLegalholdConsentOldClients :> CanThrow 'MissingLegalholdConsent :> CanThrow 'InvalidUser @@ -1137,7 +1098,6 @@ type ConnectionAPI = "update-connection-unqualified" ( Summary "Update a connection to another user" :> Until 'V2 - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'MissingLegalholdConsentOldClients :> CanThrow 'MissingLegalholdConsent :> CanThrow 'InvalidUser @@ -1166,8 +1126,6 @@ type ConnectionAPI = Named "update-connection" ( Summary "Update a connection to another user" - :> MakesFederatedCall 'Brig "get-users-by-ids" - :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'MissingLegalholdConsentOldClients :> CanThrow 'MissingLegalholdConsent :> CanThrow 'InvalidUser @@ -1189,8 +1147,6 @@ type ConnectionAPI = :<|> Named "search-contacts" ( Summary "Search for users" - :> MakesFederatedCall 'Brig "get-users-by-ids" - :> MakesFederatedCall 'Brig "search-users" :> ZLocalUser :> "search" :> "contacts" @@ -1420,7 +1376,6 @@ type AuthAPI = \ Every other combination is invalid.\ \ Access tokens can be given as query parameter or authorisation\ \ header, with the latter being preferred." - :> MakesFederatedCall 'Brig "send-connection-action" :> QueryParam "client_id" ClientId :> Cookies '["zuid" ::: SomeUserToken] :> Bearer SomeAccessToken @@ -1452,7 +1407,6 @@ type AuthAPI = ( "login" :> Summary "Authenticate a user to obtain a cookie and first access token" :> Description "Logins are throttled at the server's discretion" - :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[JSON] Login :> QueryParam' [ Optional, diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs index 4b15e9d1df2..7b305f63c95 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs @@ -29,7 +29,6 @@ import URI.ByteString import Wire.API.Asset import Wire.API.Error import Wire.API.Error.Cargohold -import Wire.API.MakesFederatedCall import Wire.API.Routes.API import Wire.API.Routes.AssetBody import Wire.API.Routes.MultiVerb @@ -171,8 +170,6 @@ type QualifiedAPI = :> Description "**Note**: local assets result in a redirect, \ \while remote assets are streamed directly." - :> MakesFederatedCall 'Cargohold "get-asset" - :> MakesFederatedCall 'Cargohold "stream-asset" :> ZLocalUser :> "assets" :> "v4" @@ -281,8 +278,6 @@ type MainAPI = :> Description "**Note**: local assets result in a redirect, \ \while remote assets are streamed directly." - :> MakesFederatedCall 'Cargohold "get-asset" - :> MakesFederatedCall 'Cargohold "stream-asset" :> CanThrow 'NoMatchingAssetEndpoint :> ZLocalUser :> "assets" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs index 06b1df74de1..46c89d9c530 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Bot.hs @@ -21,7 +21,6 @@ import Servant import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.MakesFederatedCall import Wire.API.Message import Wire.API.Provider.Bot import Wire.API.Routes.MultiVerb @@ -32,9 +31,7 @@ import Wire.API.Routes.Public.Galley.Messaging type BotAPI = Named "post-bot-message-unqualified" - ( MakesFederatedCall 'Galley "on-message-sent" - :> MakesFederatedCall 'Brig "get-user-clients" - :> ZBot + ( ZBot :> ZConversation :> CanThrow 'ConvNotFound :> "bot" 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 a6d377331dc..7cbc9adeb16 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 @@ -38,7 +38,6 @@ import Wire.API.MLS.GroupInfo import Wire.API.MLS.Keys import Wire.API.MLS.Servant import Wire.API.MLS.SubConversation -import Wire.API.MakesFederatedCall import Wire.API.OAuth import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named @@ -143,7 +142,6 @@ type ConversationAPI = "get-conversation@v2" ( Summary "Get a conversation by ID" :> Until 'V3 - :> MakesFederatedCall 'Galley "get-conversations" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> ZLocalUser @@ -156,7 +154,6 @@ type ConversationAPI = ( Summary "Get a conversation by ID" :> From 'V3 :> Until 'V6 - :> MakesFederatedCall 'Galley "get-conversations" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> ZLocalUser @@ -168,7 +165,6 @@ type ConversationAPI = "get-conversation" ( Summary "Get a conversation by ID" :> From 'V6 - :> MakesFederatedCall 'Galley "get-conversations" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> ZLocalUser @@ -191,7 +187,6 @@ type ConversationAPI = "get-group-info" ( Summary "Get MLS group information" :> From 'V5 - :> MakesFederatedCall 'Galley "query-group-info" :> CanThrow 'ConvNotFound :> CanThrow 'MLSMissingGroupInfo :> CanThrow 'MLSNotEnabled @@ -298,7 +293,6 @@ type ConversationAPI = :<|> Named "list-conversations@v1" ( Summary "Get conversation metadata for a list of conversation ids" - :> MakesFederatedCall 'Galley "get-conversations" :> Until 'V2 :> ZLocalUser :> "conversations" @@ -310,7 +304,6 @@ type ConversationAPI = :<|> Named "list-conversations@v2" ( Summary "Get conversation metadata for a list of conversation ids" - :> MakesFederatedCall 'Galley "get-conversations" :> From 'V2 :> Until 'V3 :> ZLocalUser @@ -330,7 +323,6 @@ type ConversationAPI = :<|> Named "list-conversations@v5" ( Summary "Get conversation metadata for a list of conversation ids" - :> MakesFederatedCall 'Galley "get-conversations" :> From 'V3 :> Until 'V6 :> ZLocalUser @@ -350,7 +342,6 @@ type ConversationAPI = :<|> Named "list-conversations" ( Summary "Get conversation metadata for a list of conversation ids" - :> MakesFederatedCall 'Galley "get-conversations" :> From 'V6 :> ZLocalUser :> "conversations" @@ -380,9 +371,6 @@ type ConversationAPI = "create-group-conversation@v2" ( Summary "Create a new conversation" :> DescriptionOAuthScope 'WriteConversations - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Galley "on-conversation-created" - :> MakesFederatedCall 'Galley "on-conversation-updated" :> Until 'V3 :> CanThrow 'ConvAccessDenied :> CanThrow 'MLSNonEmptyMemberList @@ -403,9 +391,6 @@ type ConversationAPI = "create-group-conversation@v3" ( Summary "Create a new conversation" :> DescriptionOAuthScope 'WriteConversations - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Galley "on-conversation-created" - :> MakesFederatedCall 'Galley "on-conversation-updated" :> From 'V3 :> Until 'V4 :> CanThrow 'ConvAccessDenied @@ -426,10 +411,6 @@ type ConversationAPI = :<|> Named "create-group-conversation@v5" ( Summary "Create a new conversation" - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Brig "get-not-fully-connected-backends" - :> MakesFederatedCall 'Galley "on-conversation-created" - :> MakesFederatedCall 'Galley "on-conversation-updated" :> From 'V4 :> Until 'V6 :> CanThrow 'ConvAccessDenied @@ -451,10 +432,6 @@ type ConversationAPI = :<|> Named "create-group-conversation" ( Summary "Create a new conversation" - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Brig "get-not-fully-connected-backends" - :> MakesFederatedCall 'Galley "on-conversation-created" - :> MakesFederatedCall 'Galley "on-conversation-updated" :> From 'V6 :> CanThrow 'ConvAccessDenied :> CanThrow 'MLSNonEmptyMemberList @@ -540,7 +517,6 @@ type ConversationAPI = "get-subconversation" ( Summary "Get information about an MLS subconversation" :> From 'V5 - :> MakesFederatedCall 'Galley "get-sub-conversation" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> CanThrow 'MLSSubConvUnsupportedConvType @@ -562,8 +538,6 @@ type ConversationAPI = "leave-subconversation" ( Summary "Leave an MLS subconversation" :> From 'V5 - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "leave-sub-conversation" :> CanThrow 'ConvNotFound :> CanThrow 'ConvAccessDenied :> CanThrow 'MLSProtocolErrorTag @@ -585,7 +559,6 @@ type ConversationAPI = "delete-subconversation" ( Summary "Delete an MLS subconversation" :> From 'V5 - :> MakesFederatedCall 'Galley "delete-sub-conversation" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'MLSNotEnabled @@ -605,7 +578,6 @@ type ConversationAPI = "get-subconversation-group-info" ( Summary "Get MLS group information of subconversation" :> From 'V5 - :> MakesFederatedCall 'Galley "query-group-info" :> CanThrow 'ConvNotFound :> CanThrow 'MLSMissingGroupInfo :> CanThrow 'MLSNotEnabled @@ -630,8 +602,6 @@ type ConversationAPI = :<|> Named "create-one-to-one-conversation@v2" ( Summary "Create a 1:1 conversation" - :> MakesFederatedCall 'Brig "api-version" - :> MakesFederatedCall 'Galley "on-conversation-created" :> Until 'V3 :> CanThrow 'ConvAccessDenied :> CanThrow 'InvalidOperation @@ -653,7 +623,6 @@ type ConversationAPI = :<|> Named "create-one-to-one-conversation" ( Summary "Create a 1:1 conversation" - :> MakesFederatedCall 'Galley "on-conversation-created" :> From 'V3 :> CanThrow 'ConvAccessDenied :> CanThrow 'InvalidOperation @@ -717,8 +686,6 @@ type ConversationAPI = :<|> Named "add-members-to-conversation-unqualified" ( Summary "Add members to an existing conversation (deprecated)" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" :> Until 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -742,8 +709,6 @@ type ConversationAPI = :<|> Named "add-members-to-conversation-unqualified2" ( Summary "Add qualified members to an existing conversation." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" :> Until 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -768,8 +733,6 @@ type ConversationAPI = :<|> Named "add-members-to-conversation" ( Summary "Add qualified members to an existing conversation." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" :> From 'V2 :> CanThrow ('ActionDenied 'AddConversationMember) :> CanThrow ('ActionDenied 'LeaveConversation) @@ -796,7 +759,6 @@ type ConversationAPI = "join-conversation-by-id-unqualified" ( Summary "Join a conversation by its ID (if link access enabled) (deprecated)" :> Until 'V5 - :> MakesFederatedCall 'Galley "on-conversation-updated" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -817,7 +779,6 @@ type ConversationAPI = :> Description "If the guest links team feature is disabled, this will fail with 409 GuestLinksDisabled.\ \Note that this is currently inconsistent (for backwards compatibility reasons) with `POST /conversations/code-check` which responds with 404 CodeNotFound if guest links are disabled." - :> MakesFederatedCall 'Galley "on-conversation-updated" :> CanThrow 'CodeNotFound :> CanThrow 'InvalidConversationPassword :> CanThrow 'ConvAccessDenied @@ -944,8 +905,6 @@ type ConversationAPI = "member-typing-unqualified" ( Summary "Sending typing notifications" :> Until 'V3 - :> MakesFederatedCall 'Galley "update-typing-indicator" - :> MakesFederatedCall 'Galley "on-typing-indicator-updated" :> CanThrow 'ConvNotFound :> ZLocalUser :> ZConn @@ -958,8 +917,6 @@ type ConversationAPI = :<|> Named "member-typing-qualified" ( Summary "Sending typing notifications" - :> MakesFederatedCall 'Galley "update-typing-indicator" - :> MakesFederatedCall 'Galley "on-typing-indicator-updated" :> CanThrow 'ConvNotFound :> ZLocalUser :> ZConn @@ -974,10 +931,6 @@ type ConversationAPI = :<|> Named "remove-member-unqualified" ( Summary "Remove a member from a conversation (deprecated)" - :> MakesFederatedCall 'Galley "leave-conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> Until 'V2 :> ZLocalUser :> ZConn @@ -995,10 +948,6 @@ type ConversationAPI = :<|> Named "remove-member" ( Summary "Remove a member from a conversation" - :> MakesFederatedCall 'Galley "leave-conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'RemoveConversationMember) @@ -1017,9 +966,6 @@ type ConversationAPI = ( Summary "Update membership of the specified user (deprecated)" :> Deprecated :> Description "Use `PUT /conversations/:cnv_domain/:cnv/members/:usr_domain/:usr` instead" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow 'ConvNotFound @@ -1042,9 +988,6 @@ type ConversationAPI = "update-other-member" ( Summary "Update membership of the specified user" :> Description "**Note**: at least one field has to be provided." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow 'ConvNotFound @@ -1070,9 +1013,6 @@ type ConversationAPI = ( Summary "Update conversation name (deprecated)" :> Deprecated :> Description "Use `/conversations/:domain/:conv/name` instead." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -1092,9 +1032,6 @@ type ConversationAPI = ( Summary "Update conversation name (deprecated)" :> Deprecated :> Description "Use `/conversations/:domain/:conv/name` instead." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -1113,9 +1050,6 @@ type ConversationAPI = :<|> Named "update-conversation-name" ( Summary "Update conversation name" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'ModifyConversationName) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation @@ -1138,9 +1072,6 @@ type ConversationAPI = ( Summary "Update the message timer for a conversation (deprecated)" :> Deprecated :> Description "Use `/conversations/:domain/:cnv/message-timer` instead." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) @@ -1160,9 +1091,6 @@ type ConversationAPI = :<|> Named "update-conversation-message-timer" ( Summary "Update the message timer for a conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationMessageTimer) @@ -1186,10 +1114,6 @@ type ConversationAPI = ( Summary "Update receipt mode for a conversation (deprecated)" :> Deprecated :> Description "Use `PUT /conversations/:domain/:cnv/receipt-mode` instead." - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "update-conversation" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationReceiptMode) @@ -1209,10 +1133,6 @@ type ConversationAPI = :<|> Named "update-conversation-receipt-mode" ( Summary "Update receipt mode for a conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "update-conversation" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> ZLocalUser :> ZConn :> CanThrow ('ActionDenied 'ModifyConversationReceiptMode) @@ -1235,9 +1155,6 @@ type ConversationAPI = :<|> Named "update-conversation-access-unqualified" ( Summary "Update access modes for a conversation (deprecated)" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> Until 'V3 :> Description "Use PUT `/conversations/:domain/:cnv/access` instead." :> ZLocalUser @@ -1261,9 +1178,6 @@ type ConversationAPI = :<|> Named "update-conversation-access@v2" ( Summary "Update access modes for a conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> Until 'V3 :> ZLocalUser :> ZConn @@ -1286,9 +1200,6 @@ type ConversationAPI = :<|> Named "update-conversation-access" ( Summary "Update access modes for a conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> From 'V3 :> ZLocalUser :> ZConn diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs index f4506b7fcfb..c1e2882ffe5 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/LegalHold.hs @@ -25,7 +25,6 @@ import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.MakesFederatedCall import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -63,9 +62,6 @@ type LegalHoldAPI = :<|> Named "delete-legal-hold-settings" ( Summary "Delete legal hold service settings" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow AuthenticationError :> CanThrow OperationDenied :> CanThrow 'NotATeamMember @@ -102,9 +98,6 @@ type LegalHoldAPI = :<|> Named "consent-to-legal-hold" ( Summary "Consent to legal hold" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'InvalidOperation :> CanThrow 'TeamMemberNotFound @@ -120,9 +113,6 @@ type LegalHoldAPI = :<|> Named "request-legal-hold-device" ( Summary "Request legal hold device" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'NotATeamMember :> CanThrow OperationDenied @@ -152,9 +142,6 @@ type LegalHoldAPI = :<|> Named "disable-legal-hold-for-user" ( Summary "Disable legal hold for user" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow AuthenticationError :> CanThrow ('ActionDenied 'RemoveConversationMember) :> CanThrow 'NotATeamMember @@ -181,9 +168,6 @@ type LegalHoldAPI = :<|> Named "approve-legal-hold-device" ( Summary "Approve legal hold device" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow AuthenticationError :> CanThrow 'AccessDenied :> CanThrow ('ActionDenied 'RemoveConversationMember) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs index 347bc01158d..8913227982f 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/MLS.hs @@ -26,7 +26,6 @@ import Wire.API.MLS.Keys import Wire.API.MLS.Message import Wire.API.MLS.Serialisation import Wire.API.MLS.Servant -import Wire.API.MakesFederatedCall import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -37,10 +36,6 @@ type MLSMessagingAPI = "mls-message" ( Summary "Post an MLS message" :> From 'V5 - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Galley "send-mls-message" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Brig "get-mls-clients" :> CanThrow 'ConvAccessDenied :> CanThrow 'ConvMemberNotFound :> CanThrow 'ConvNotFound @@ -73,13 +68,6 @@ type MLSMessagingAPI = "mls-commit-bundle" ( Summary "Post a MLS CommitBundle" :> From V5 - :> MakesFederatedCall Galley "on-mls-message-sent" - :> MakesFederatedCall Galley "mls-welcome" - :> MakesFederatedCall Galley "send-mls-commit-bundle" - :> MakesFederatedCall Galley "on-conversation-updated" - :> MakesFederatedCall Brig "get-mls-clients" - :> MakesFederatedCall Brig "get-users-by-ids" - :> MakesFederatedCall Brig "api-version" :> CanThrow ConvAccessDenied :> CanThrow ConvMemberNotFound :> CanThrow ConvNotFound diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs index c862d5863d0..5604c3d74f7 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/Messaging.hs @@ -28,7 +28,6 @@ import Servant.OpenApi.Internal.Orphans () import Wire.API.Error import Wire.API.Error.Brig qualified as BrigError import Wire.API.Error.Galley -import Wire.API.MakesFederatedCall import Wire.API.Message import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named @@ -41,8 +40,6 @@ type MessagingAPI = "post-otr-message-unqualified" ( Summary "Post an encrypted message to a conversation (accepts JSON or Protobuf)" :> Description PostOtrDescriptionUnqualified - :> MakesFederatedCall 'Galley "on-message-sent" - :> MakesFederatedCall 'Brig "get-user-clients" :> ZLocalUser :> ZConn :> "conversations" @@ -83,9 +80,6 @@ type MessagingAPI = "post-proteus-message" ( Summary "Post an encrypted message to a conversation (accepts only Protobuf)" :> Description PostOtrDescription - :> MakesFederatedCall 'Brig "get-user-clients" - :> MakesFederatedCall 'Galley "on-message-sent" - :> MakesFederatedCall 'Galley "send-message" :> ZLocalUser :> ZConn :> "conversations" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs index 98573abb02e..f77aab90644 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Galley/TeamConversation.hs @@ -23,7 +23,6 @@ import Servant.OpenApi.Internal.Orphans () import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Galley -import Wire.API.MakesFederatedCall import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.Routes.Public @@ -68,9 +67,6 @@ type TeamConversationAPI = :<|> Named "delete-team-conversation" ( Summary "Remove a team conversation" - :> MakesFederatedCall 'Galley "on-conversation-updated" - :> MakesFederatedCall 'Galley "on-mls-message-sent" - :> MakesFederatedCall 'Brig "get-users-by-ids" :> CanThrow ('ActionDenied 'DeleteConversation) :> CanThrow 'ConvNotFound :> CanThrow 'InvalidOperation diff --git a/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs b/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs index a2f29573f43..dfa669ca138 100644 --- a/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs +++ b/libs/wire-api/src/Wire/API/Routes/SpecialiseToVersion.hs @@ -23,7 +23,6 @@ import GHC.TypeLits import Servant import Servant.API.Extended.RawM qualified as RawM import Wire.API.Deprecated -import Wire.API.MakesFederatedCall import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named import Wire.API.VersionInfo @@ -84,10 +83,6 @@ type instance SpecialiseToVersion v (Description desc :> api) = Description desc :> SpecialiseToVersion v api -type instance - SpecialiseToVersion v (MakesFederatedCall comp rpc :> api) = - MakesFederatedCall comp rpc :> SpecialiseToVersion v api - type instance SpecialiseToVersion v (StreamBody' opts f t x :> api) = StreamBody' opts f t x :> SpecialiseToVersion v api diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index c0c933bec66..f5eac2bf6d2 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -75,6 +75,7 @@ library Wire.API.Bot.Service Wire.API.Call.Config Wire.API.CannonId + Wire.API.Component Wire.API.Connection Wire.API.Conversation Wire.API.Conversation.Action @@ -105,7 +106,6 @@ library Wire.API.Internal.BulkPush Wire.API.Internal.Notification Wire.API.Locale - Wire.API.MakesFederatedCall Wire.API.Message Wire.API.Message.Proto Wire.API.MLS.AuthenticatedContent @@ -270,7 +270,6 @@ library , cereal , comonad , conduit - , constraints , containers >=0.5 , cookie , crypton @@ -336,7 +335,6 @@ library , these , time >=1.4 , tinylog - , transitive-anns , types-common >=0.16 , unordered-containers >=0.2 , uri-bytestring >=0.2 diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index 7582b8b4228..768fcdc5cac 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -76,7 +76,6 @@ , time-units , tinylog , transformers -, transitive-anns , types-common , unliftio , unordered-containers @@ -156,7 +155,6 @@ mkDerivation { time-units tinylog transformers - transitive-anns types-common unliftio unordered-containers diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index b19f48031fc..d9000793018 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -65,7 +65,7 @@ common common-all library import: common-all - ghc-options: -fplugin=Polysemy.Plugin -fplugin=TransitiveAnns.Plugin + ghc-options: -fplugin=Polysemy.Plugin -- cabal-fmt: expand src exposed-modules: @@ -207,7 +207,6 @@ library , time-units , tinylog , transformers - , transitive-anns , types-common , unliftio , unordered-containers diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 810bdbaa8f0..fbf3d3ca699 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -63,14 +63,6 @@ let # maintained by us # ---------------- - transitive-anns = { - src = fetchgit { - url = "https://github.com/wireapp/transitive-anns"; - rev = "5e0cad1f411a0c92e6445404c205ddd4a0229c4d"; - hash = "sha256-/P4KJ4yZgqhZhzmg1GcE+Ti4kdsWUQX8q++RhgCUDKI="; - }; - }; - cryptobox-haskell = { src = fetchgit { url = "https://github.com/wireapp/cryptobox-haskell"; diff --git a/nix/local-haskell-packages.nix b/nix/local-haskell-packages.nix index ad191ee1573..414a5443410 100644 --- a/nix/local-haskell-packages.nix +++ b/nix/local-haskell-packages.nix @@ -53,7 +53,6 @@ repair-handles = hself.callPackage ../tools/db/repair-handles/default.nix { inherit gitignoreSource; }; service-backfill = hself.callPackage ../tools/db/service-backfill/default.nix { inherit gitignoreSource; }; team-info = hself.callPackage ../tools/db/team-info/default.nix { inherit gitignoreSource; }; - fedcalls = hself.callPackage ../tools/fedcalls/default.nix { inherit gitignoreSource; }; mlsstats = hself.callPackage ../tools/mlsstats/default.nix { inherit gitignoreSource; }; rabbitmq-consumer = hself.callPackage ../tools/rabbitmq-consumer/default.nix { inherit gitignoreSource; }; rex = hself.callPackage ../tools/rex/default.nix { inherit gitignoreSource; }; diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index d0aa544405c..54a5ce23fbf 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -23,8 +23,6 @@ hself: hsuper: { # these are okay, the only issue is that the compiler underlines # errors differently than before singletons-base = hlib.markUnbroken (hlib.dontCheck hsuper.singletons-base); - # one of the tests is flaky - transitive-anns = hlib.dontCheck hsuper.transitive-anns; # Tests require a running redis hedis = hlib.dontCheck hsuper.hedis; @@ -38,7 +36,7 @@ hself: hsuper: { bytestring-arbitrary = hlib.markUnbroken (hlib.doJailbreak hsuper.bytestring-arbitrary); lens-datetime = hlib.markUnbroken (hlib.doJailbreak hsuper.lens-datetime); - # the libsodium haskell library is incompatible with the new version of the libsodium c library + # the libsodium haskell library is incompatible with the new version of the libsodium c library # that nixpkgs has - this downgrades libsodium from 1.0.19 to 1.0.18 libsodium = hlib.markUnbroken (hlib.addPkgconfigDepend hsuper.libsodium ( libsodium.overrideAttrs (old: diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 103cfd80c8a..29cc881005c 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -203,8 +203,7 @@ library hs-source-dirs: src ghc-options: -funbox-strict-fields -fplugin=Polysemy.Plugin - -fplugin=TransitiveAnns.Plugin -Wredundant-constraints - -Wunused-packages + -Wredundant-constraints -Wunused-packages build-depends: , aeson >=2.0.1.0 @@ -305,7 +304,6 @@ library , time-units , tinylog >=0.10 , transformers >=0.3 - , transitive-anns , types-common >=0.16 , types-common-aws , types-common-journal >=0.1 diff --git a/services/brig/default.nix b/services/brig/default.nix index ac7206ad390..ed77acac6e5 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -133,7 +133,6 @@ , time-units , tinylog , transformers -, transitive-anns , types-common , types-common-aws , types-common-journal @@ -262,7 +261,6 @@ mkDerivation { time-units tinylog transformers - transitive-anns types-common types-common-aws types-common-journal diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 9cb812d48b7..94ee5b43021 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -75,7 +75,6 @@ import UnliftIO.Async (pooledMapConcurrentlyN) import Wire.API.Connection import Wire.API.Error import Wire.API.Error.Brig qualified as E -import Wire.API.Federation.API import Wire.API.Federation.Error (FederationError (..)) import Wire.API.MLS.CipherSuite import Wire.API.Routes.FederationDomainConfig @@ -209,8 +208,8 @@ accountAPI = :<|> deleteAccountConferenceCallingConfig :<|> getConnectionsStatusUnqualified :<|> getConnectionsStatus - :<|> Named @"createUserNoVerify" (callsFed (exposeAnnotations createUserNoVerify)) - :<|> Named @"createUserNoVerifySpar" (callsFed (exposeAnnotations createUserNoVerifySpar)) + :<|> Named @"createUserNoVerify" createUserNoVerify + :<|> Named @"createUserNoVerifySpar" createUserNoVerifySpar :<|> Named @"putSelfEmail" changeSelfEmailMaybeSendH :<|> Named @"iDeleteUser" deleteUserNoAuthH :<|> Named @"iPutUserStatus" changeAccountStatusH @@ -285,8 +284,8 @@ authAPI :: ) => ServerT BrigIRoutes.AuthAPI (Handler r) authAPI = - Named @"legalhold-login" (callsFed (exposeAnnotations legalHoldLogin)) - :<|> Named @"sso-login" (callsFed (exposeAnnotations ssoLogin)) + Named @"legalhold-login" legalHoldLogin + :<|> Named @"sso-login" ssoLogin :<|> Named @"login-code" getLoginCode :<|> Named @"reauthenticate" ( \uid reauth -> diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index c49323beb6f..2485961ce68 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -102,7 +102,6 @@ import Util.Logging (logFunction, logHandle, logTeam, logUser) import Wire.API.Connection qualified as Public import Wire.API.Error import Wire.API.Error.Brig qualified as E -import Wire.API.Federation.API 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 @@ -332,14 +331,14 @@ servantSitemap = where userAPI :: ServerT UserAPI (Handler r) userAPI = - Named @"get-user-unqualified" (callsFed (exposeAnnotations getUserUnqualifiedH)) - :<|> Named @"get-user-qualified" (callsFed (exposeAnnotations getUserProfileH)) + Named @"get-user-unqualified" getUserUnqualifiedH + :<|> Named @"get-user-qualified" getUserProfileH :<|> Named @"update-user-email" updateUserEmail - :<|> Named @"get-handle-info-unqualified" (callsFed (exposeAnnotations getHandleInfoUnqualifiedH)) - :<|> Named @"get-user-by-handle-qualified" (callsFed (exposeAnnotations Handle.getHandleInfo)) - :<|> Named @"list-users-by-unqualified-ids-or-handles" (callsFed (exposeAnnotations listUsersByUnqualifiedIdsOrHandles)) - :<|> Named @"list-users-by-ids-or-handles" (callsFed (exposeAnnotations listUsersByIdsOrHandles)) - :<|> Named @"list-users-by-ids-or-handles@V3" (callsFed (exposeAnnotations listUsersByIdsOrHandlesV3)) + :<|> Named @"get-handle-info-unqualified" getHandleInfoUnqualifiedH + :<|> Named @"get-user-by-handle-qualified" Handle.getHandleInfo + :<|> Named @"list-users-by-unqualified-ids-or-handles" listUsersByUnqualifiedIdsOrHandles + :<|> Named @"list-users-by-ids-or-handles" listUsersByIdsOrHandles + :<|> Named @"list-users-by-ids-or-handles@V3" listUsersByIdsOrHandlesV3 :<|> Named @"send-verification-code" sendVerificationCode :<|> Named @"get-rich-info" getRichInfo :<|> Named @"get-supported-protocols" getSupportedProtocols @@ -347,24 +346,24 @@ servantSitemap = selfAPI :: ServerT SelfAPI (Handler r) selfAPI = Named @"get-self" getSelf - :<|> Named @"delete-self" (callsFed (exposeAnnotations deleteSelfUser)) - :<|> Named @"put-self" (callsFed (exposeAnnotations updateUser)) + :<|> Named @"delete-self" deleteSelfUser + :<|> Named @"put-self" updateUser :<|> Named @"change-phone" changePhone - :<|> Named @"remove-phone" (callsFed (exposeAnnotations removePhone)) - :<|> Named @"remove-email" (callsFed (exposeAnnotations removeEmail)) + :<|> Named @"remove-phone" removePhone + :<|> Named @"remove-email" removeEmail :<|> Named @"check-password-exists" checkPasswordExists :<|> Named @"change-password" changePassword - :<|> Named @"change-locale" (callsFed (exposeAnnotations changeLocale)) - :<|> Named @"change-handle" (callsFed (exposeAnnotations changeHandle)) + :<|> Named @"change-locale" changeLocale + :<|> Named @"change-handle" changeHandle :<|> Named @"change-supported-protocols" changeSupportedProtocols accountAPI :: ServerT AccountAPI (Handler r) accountAPI = Named @"upgrade-personal-to-team" upgradePersonalToTeam - :<|> Named @"register" (callsFed (exposeAnnotations createUser)) - :<|> Named @"verify-delete" (callsFed (exposeAnnotations verifyDeleteUser)) - :<|> Named @"get-activate" (callsFed (exposeAnnotations activate)) - :<|> Named @"post-activate" (callsFed (exposeAnnotations activateKey)) + :<|> Named @"register" createUser + :<|> Named @"verify-delete" verifyDeleteUser + :<|> Named @"get-activate" activate + :<|> Named @"post-activate" activateKey :<|> Named @"post-activate-send" sendActivationCode :<|> Named @"post-password-reset" beginPasswordReset :<|> Named @"post-password-reset-complete" completePasswordReset @@ -373,28 +372,28 @@ servantSitemap = clientAPI :: ServerT ClientAPI (Handler r) clientAPI = - Named @"get-user-clients-unqualified" (callsFed (exposeAnnotations getUserClientsUnqualified)) - :<|> Named @"get-user-clients-qualified" (callsFed (exposeAnnotations getUserClientsQualified)) - :<|> Named @"get-user-client-unqualified" (callsFed (exposeAnnotations getUserClientUnqualified)) - :<|> Named @"get-user-client-qualified" (callsFed (exposeAnnotations getUserClientQualified)) - :<|> Named @"list-clients-bulk" (callsFed (exposeAnnotations listClientsBulk)) - :<|> Named @"list-clients-bulk-v2" (callsFed (exposeAnnotations listClientsBulkV2)) - :<|> Named @"list-clients-bulk@v2" (callsFed (exposeAnnotations listClientsBulkV2)) + Named @"get-user-clients-unqualified" getUserClientsUnqualified + :<|> Named @"get-user-clients-qualified" getUserClientsQualified + :<|> Named @"get-user-client-unqualified" getUserClientUnqualified + :<|> Named @"get-user-client-qualified" getUserClientQualified + :<|> Named @"list-clients-bulk" listClientsBulk + :<|> Named @"list-clients-bulk-v2" listClientsBulkV2 + :<|> Named @"list-clients-bulk@v2" listClientsBulkV2 prekeyAPI :: ServerT PrekeyAPI (Handler r) prekeyAPI = - Named @"get-users-prekeys-client-unqualified" (callsFed (exposeAnnotations getPrekeyUnqualifiedH)) - :<|> Named @"get-users-prekeys-client-qualified" (callsFed (exposeAnnotations getPrekeyH)) - :<|> Named @"get-users-prekey-bundle-unqualified" (callsFed (exposeAnnotations getPrekeyBundleUnqualifiedH)) - :<|> Named @"get-users-prekey-bundle-qualified" (callsFed (exposeAnnotations getPrekeyBundleH)) + Named @"get-users-prekeys-client-unqualified" getPrekeyUnqualifiedH + :<|> Named @"get-users-prekeys-client-qualified" getPrekeyH + :<|> Named @"get-users-prekey-bundle-unqualified" getPrekeyBundleUnqualifiedH + :<|> Named @"get-users-prekey-bundle-qualified" getPrekeyBundleH :<|> Named @"get-multi-user-prekey-bundle-unqualified" getMultiUserPrekeyBundleUnqualifiedH - :<|> Named @"get-multi-user-prekey-bundle-qualified@v3" (callsFed (exposeAnnotations getMultiUserPrekeyBundleHV3)) - :<|> Named @"get-multi-user-prekey-bundle-qualified" (callsFed (exposeAnnotations getMultiUserPrekeyBundleH)) + :<|> Named @"get-multi-user-prekey-bundle-qualified@v3" getMultiUserPrekeyBundleHV3 + :<|> Named @"get-multi-user-prekey-bundle-qualified" getMultiUserPrekeyBundleH userClientAPI :: ServerT UserClientAPI (Handler r) userClientAPI = - Named @"add-client-v6" (callsFed (exposeAnnotations addClient)) - :<|> Named @"add-client" (callsFed (exposeAnnotations addClient)) + Named @"add-client-v6" addClient + :<|> Named @"add-client" addClient :<|> Named @"update-client" updateClient :<|> Named @"delete-client" deleteClient :<|> Named @"list-clients-v6" listClients @@ -409,15 +408,15 @@ servantSitemap = connectionAPI :: ServerT ConnectionAPI (Handler r) connectionAPI = - Named @"create-connection-unqualified" (callsFed (exposeAnnotations createConnectionUnqualified)) - :<|> Named @"create-connection" (callsFed (exposeAnnotations createConnection)) + Named @"create-connection-unqualified" createConnectionUnqualified + :<|> Named @"create-connection" createConnection :<|> Named @"list-local-connections" listLocalConnections :<|> Named @"list-connections" listConnections :<|> Named @"get-connection-unqualified" getLocalConnection :<|> Named @"get-connection" getConnection - :<|> Named @"update-connection-unqualified" (callsFed (exposeAnnotations updateLocalConnection)) - :<|> Named @"update-connection" (callsFed (exposeAnnotations updateConnection)) - :<|> Named @"search-contacts" (callsFed (exposeAnnotations searchUsersHandler)) + :<|> Named @"update-connection-unqualified" updateLocalConnection + :<|> Named @"update-connection" updateConnection + :<|> Named @"search-contacts" searchUsersHandler propertiesAPI :: ServerT PropertiesAPI (Handler r) propertiesAPI = @@ -448,9 +447,9 @@ servantSitemap = authAPI :: ServerT AuthAPI (Handler r) authAPI = - Named @"access" (callsFed (exposeAnnotations accessH)) + Named @"access" accessH :<|> Named @"send-login-code" sendLoginCode - :<|> Named @"login" (callsFed (exposeAnnotations login)) + :<|> Named @"login" login :<|> Named @"logout" logoutH :<|> Named @"change-self-email" changeSelfEmailH :<|> Named @"list-cookies" listCookies diff --git a/services/cargohold/cargohold.cabal b/services/cargohold/cargohold.cabal index c906ffb3dda..39b953edecd 100644 --- a/services/cargohold/cargohold.cabal +++ b/services/cargohold/cargohold.cabal @@ -81,8 +81,7 @@ library ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -fplugin=TransitiveAnns.Plugin -Wredundant-constraints - -Wunused-packages + -Wredundant-constraints -Wunused-packages build-depends: aeson >=2.0.1.0 @@ -125,7 +124,6 @@ library , time >=1.4 , tinylog >=0.10 , transformers - , transitive-anns , types-common >=0.16 , types-common-aws , unliftio diff --git a/services/cargohold/default.nix b/services/cargohold/default.nix index 585a66d22ff..69316cc540f 100644 --- a/services/cargohold/default.nix +++ b/services/cargohold/default.nix @@ -57,7 +57,6 @@ , time , tinylog , transformers -, transitive-anns , types-common , types-common-aws , unliftio @@ -118,7 +117,6 @@ mkDerivation { time tinylog transformers - transitive-anns types-common types-common-aws unliftio diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 607e8947087..5177e19e4a5 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -41,7 +41,6 @@ import Servant.API import Servant.Server hiding (Handler) import URI.ByteString as URI import Wire.API.Asset -import Wire.API.Federation.API import Wire.API.Routes.AssetBody import Wire.API.Routes.Internal.Brig (brigInternalClient) import Wire.API.Routes.Internal.Cargohold @@ -68,13 +67,13 @@ servantSitemap = providerAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag legacyAPI = legacyDownloadPlain :<|> legacyDownloadPlain :<|> legacyDownloadOtr qualifiedAPI :: ServerT QualifiedAPI Handler - qualifiedAPI = callsFed (exposeAnnotations downloadAssetV4) :<|> deleteAssetV4 + qualifiedAPI = downloadAssetV4 :<|> deleteAssetV4 mainAPI :: ServerT MainAPI Handler mainAPI = renewTokenV3 :<|> deleteTokenV3 :<|> uploadAssetV3 @'UserPrincipalTag - :<|> callsFed (exposeAnnotations downloadAssetV4) + :<|> downloadAssetV4 :<|> deleteAssetV4 internalSitemap :: ServerT InternalAPI Handler diff --git a/services/federator/src/Federator/Interpreter.hs b/services/federator/src/Federator/Interpreter.hs index 2042ab1e043..e59f6a4cb0c 100644 --- a/services/federator/src/Federator/Interpreter.hs +++ b/services/federator/src/Federator/Interpreter.hs @@ -42,8 +42,8 @@ import Servant hiding (ServerError, respond, serve) import Servant.Client (mkClientEnv) import Servant.Client.Core import Util.Options (Endpoint (..)) +import Wire.API.Component (Component (Brig)) import Wire.API.FederationUpdate qualified as FedUp (getFederationDomainConfigs) -import Wire.API.MakesFederatedCall (Component (Brig)) import Wire.API.Routes.FederationDomainConfig qualified as FedUp (FederationDomainConfigs) import Wire.Network.DNS.Effect import Wire.Sem.Logger.TinyLog diff --git a/services/federator/test/unit/Test/Federator/Client.hs b/services/federator/test/unit/Test/Federator/Client.hs index 36c2717a6b4..4801240ece8 100644 --- a/services/federator/test/unit/Test/Federator/Client.hs +++ b/services/federator/test/unit/Test/Federator/Client.hs @@ -54,8 +54,6 @@ import Wire.API.Federation.Client import Wire.API.Federation.Error import Wire.API.User (UserProfile) -instance AddAnnotation loc comp name x - targetDomain :: Domain targetDomain = Domain "target.example.com" diff --git a/services/galley/default.nix b/services/galley/default.nix index 602549b250b..7cec256aa46 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -103,7 +103,6 @@ , tinylog , tls , transformers -, transitive-anns , types-common , types-common-aws , types-common-journal @@ -202,7 +201,6 @@ mkDerivation { tinylog tls transformers - transitive-anns types-common types-common-aws types-common-journal diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 1db3f95e89c..0d7a3da6931 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -284,7 +284,7 @@ library Galley.Types.UserList Galley.Validation - ghc-options: -fplugin=TransitiveAnns.Plugin + ghc-options: other-modules: Paths_galley hs-source-dirs: src build-depends: @@ -357,7 +357,6 @@ library , tinylog >=0.10 , tls >=1.7.0 , transformers - , transitive-anns , types-common >=0.16 , types-common-aws , types-common-journal >=0.1 diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index a6ae06e0339..d46f818b856 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -108,18 +108,18 @@ federationSitemap = Named @"on-conversation-created" onConversationCreated :<|> Named @"get-conversations@v1" getConversationsV1 :<|> Named @"get-conversations" getConversations - :<|> Named @"leave-conversation" (callsFed (exposeAnnotations leaveConversation)) - :<|> Named @"send-message" (callsFed (exposeAnnotations sendMessage)) - :<|> Named @"update-conversation" (callsFed (exposeAnnotations updateConversation)) + :<|> Named @"leave-conversation" leaveConversation + :<|> Named @"send-message" sendMessage + :<|> Named @"update-conversation" updateConversation :<|> Named @"mls-welcome" mlsSendWelcome - :<|> Named @"send-mls-message" (callsFed (exposeAnnotations sendMLSMessage)) - :<|> Named @"send-mls-commit-bundle" (callsFed (exposeAnnotations sendMLSCommitBundle)) + :<|> Named @"send-mls-message" sendMLSMessage + :<|> Named @"send-mls-commit-bundle" sendMLSCommitBundle :<|> Named @"query-group-info" queryGroupInfo - :<|> Named @"update-typing-indicator" (callsFed (exposeAnnotations updateTypingIndicator)) + :<|> Named @"update-typing-indicator" updateTypingIndicator :<|> Named @"on-typing-indicator-updated" onTypingIndicatorUpdated :<|> Named @"get-sub-conversation" getSubConversationForRemoteUser - :<|> Named @"delete-sub-conversation" (callsFed deleteSubConversationForRemoteUser) - :<|> Named @"leave-sub-conversation" (callsFed leaveSubConversation) + :<|> Named @"delete-sub-conversation" deleteSubConversationForRemoteUser + :<|> Named @"leave-sub-conversation" leaveSubConversation :<|> Named @"get-one2one-conversation@v1" getOne2OneConversationV1 :<|> Named @"get-one2one-conversation" getOne2OneConversation :<|> Named @"on-client-removed" onClientRemoved diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index fff8e161a7d..2e273aef847 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -103,8 +103,8 @@ internalAPI :: API InternalAPI GalleyEffects internalAPI = hoistAPI @InternalAPIBase Imports.id $ mkNamedAPI @"status" (pure ()) - <@> mkNamedAPI @"delete-user" (callsFed (exposeAnnotations rmUser)) - <@> mkNamedAPI @"connect" (callsFed (exposeAnnotations Create.createConnectConversation)) + <@> mkNamedAPI @"delete-user" rmUser + <@> mkNamedAPI @"connect" Create.createConnectConversation <@> mkNamedAPI @"get-conversation-clients" iGetMLSClientListForConv <@> mkNamedAPI @"guard-legalhold-policy-conflicts" guardLegalholdPolicyConflictsH <@> legalholdWhitelistedTeamsAPI @@ -117,7 +117,7 @@ internalAPI = <@> iEJPDAPI iEJPDAPI :: API IEJPDAPI GalleyEffects -iEJPDAPI = mkNamedAPI @"get-conversations-by-user" (callsFed (exposeAnnotations ejpdGetConvInfo)) +iEJPDAPI = mkNamedAPI @"get-conversations-by-user" ejpdGetConvInfo -- | An unpaginated, internal http interface to `Query.conversationIdsPageFrom`. Used for -- EJPD reports. Called locally with very little data for each conv, so we don't expect diff --git a/services/galley/src/Galley/API/Public/Bot.hs b/services/galley/src/Galley/API/Public/Bot.hs index 0465c5903e8..ad839a6f24f 100644 --- a/services/galley/src/Galley/API/Public/Bot.hs +++ b/services/galley/src/Galley/API/Public/Bot.hs @@ -31,14 +31,13 @@ import Polysemy.Input import Wire.API.Error import Wire.API.Error.Galley import Wire.API.Event.Team qualified as Public () -import Wire.API.Federation.API import Wire.API.Provider.Bot import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Bot botAPI :: API BotAPI GalleyEffects botAPI = - mkNamedAPI @"post-bot-message-unqualified" (callsFed (exposeAnnotations postBotMessageUnqualified)) + mkNamedAPI @"post-bot-message-unqualified" postBotMessageUnqualified <@> mkNamedAPI @"get-bot-conversation" getBotConversation getBotConversation :: diff --git a/services/galley/src/Galley/API/Public/Conversation.hs b/services/galley/src/Galley/API/Public/Conversation.hs index d254ff6c9c8..a234ec89b92 100644 --- a/services/galley/src/Galley/API/Public/Conversation.hs +++ b/services/galley/src/Galley/API/Public/Conversation.hs @@ -25,7 +25,6 @@ import Galley.API.Query import Galley.API.Update import Galley.App import Imports -import Wire.API.Federation.API import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Conversation @@ -33,65 +32,65 @@ conversationAPI :: API ConversationAPI GalleyEffects conversationAPI = mkNamedAPI @"get-unqualified-conversation" getUnqualifiedConversation <@> mkNamedAPI @"get-unqualified-conversation-legalhold-alias" getUnqualifiedConversation - <@> mkNamedAPI @"get-conversation@v2" (callsFed (exposeAnnotations getConversation)) - <@> mkNamedAPI @"get-conversation@v5" (callsFed (exposeAnnotations getConversation)) - <@> mkNamedAPI @"get-conversation" (callsFed (exposeAnnotations getConversation)) + <@> mkNamedAPI @"get-conversation@v2" getConversation + <@> mkNamedAPI @"get-conversation@v5" getConversation + <@> mkNamedAPI @"get-conversation" getConversation <@> mkNamedAPI @"get-conversation-roles" getConversationRoles - <@> mkNamedAPI @"get-group-info" (callsFed (exposeAnnotations getGroupInfo)) + <@> mkNamedAPI @"get-group-info" getGroupInfo <@> mkNamedAPI @"list-conversation-ids-unqualified" conversationIdsPageFromUnqualified <@> mkNamedAPI @"list-conversation-ids-v2" (conversationIdsPageFromV2 DoNotListGlobalSelf) <@> mkNamedAPI @"list-conversation-ids" conversationIdsPageFrom <@> mkNamedAPI @"get-conversations" getConversations - <@> mkNamedAPI @"list-conversations@v1" (callsFed (exposeAnnotations listConversations)) - <@> mkNamedAPI @"list-conversations@v2" (callsFed (exposeAnnotations listConversations)) - <@> mkNamedAPI @"list-conversations@v5" (callsFed (exposeAnnotations listConversations)) - <@> mkNamedAPI @"list-conversations" (callsFed (exposeAnnotations listConversations)) + <@> mkNamedAPI @"list-conversations@v1" listConversations + <@> mkNamedAPI @"list-conversations@v2" listConversations + <@> mkNamedAPI @"list-conversations@v5" listConversations + <@> mkNamedAPI @"list-conversations" listConversations <@> mkNamedAPI @"get-conversation-by-reusable-code" getConversationByReusableCode - <@> mkNamedAPI @"create-group-conversation@v2" (callsFed (exposeAnnotations createGroupConversationUpToV3)) - <@> mkNamedAPI @"create-group-conversation@v3" (callsFed (exposeAnnotations createGroupConversationUpToV3)) - <@> mkNamedAPI @"create-group-conversation@v5" (callsFed (exposeAnnotations createGroupConversation)) - <@> mkNamedAPI @"create-group-conversation" (callsFed (exposeAnnotations createGroupConversation)) + <@> mkNamedAPI @"create-group-conversation@v2" createGroupConversationUpToV3 + <@> mkNamedAPI @"create-group-conversation@v3" createGroupConversationUpToV3 + <@> mkNamedAPI @"create-group-conversation@v5" createGroupConversation + <@> mkNamedAPI @"create-group-conversation" createGroupConversation <@> mkNamedAPI @"create-self-conversation@v2" createProteusSelfConversation <@> mkNamedAPI @"create-self-conversation@v5" createProteusSelfConversation <@> mkNamedAPI @"create-self-conversation" createProteusSelfConversation <@> mkNamedAPI @"get-mls-self-conversation@v5" getMLSSelfConversationWithError <@> mkNamedAPI @"get-mls-self-conversation" getMLSSelfConversationWithError - <@> mkNamedAPI @"get-subconversation" (callsFed getSubConversation) - <@> mkNamedAPI @"leave-subconversation" (callsFed leaveSubConversation) - <@> mkNamedAPI @"delete-subconversation" (callsFed deleteSubConversation) - <@> mkNamedAPI @"get-subconversation-group-info" (callsFed getSubConversationGroupInfo) - <@> mkNamedAPI @"create-one-to-one-conversation@v2" (callsFed createOne2OneConversation) - <@> mkNamedAPI @"create-one-to-one-conversation" (callsFed createOne2OneConversation) + <@> mkNamedAPI @"get-subconversation" getSubConversation + <@> mkNamedAPI @"leave-subconversation" leaveSubConversation + <@> mkNamedAPI @"delete-subconversation" deleteSubConversation + <@> mkNamedAPI @"get-subconversation-group-info" getSubConversationGroupInfo + <@> mkNamedAPI @"create-one-to-one-conversation@v2" 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 <@> mkNamedAPI @"get-one-to-one-mls-conversation" getMLSOne2OneConversation - <@> mkNamedAPI @"add-members-to-conversation-unqualified" (callsFed addMembersUnqualified) - <@> mkNamedAPI @"add-members-to-conversation-unqualified2" (callsFed addMembersUnqualifiedV2) - <@> mkNamedAPI @"add-members-to-conversation" (callsFed addMembers) - <@> mkNamedAPI @"join-conversation-by-id-unqualified" (callsFed joinConversationById) - <@> mkNamedAPI @"join-conversation-by-code-unqualified" (callsFed joinConversationByReusableCode) + <@> mkNamedAPI @"add-members-to-conversation-unqualified" addMembersUnqualified + <@> mkNamedAPI @"add-members-to-conversation-unqualified2" addMembersUnqualifiedV2 + <@> mkNamedAPI @"add-members-to-conversation" addMembers + <@> mkNamedAPI @"join-conversation-by-id-unqualified" joinConversationById + <@> mkNamedAPI @"join-conversation-by-code-unqualified" joinConversationByReusableCode <@> mkNamedAPI @"code-check" checkReusableCode <@> mkNamedAPI @"create-conversation-code-unqualified@v3" (addCodeUnqualified Nothing) <@> mkNamedAPI @"create-conversation-code-unqualified" addCodeUnqualifiedWithReqBody <@> mkNamedAPI @"get-conversation-guest-links-status" getConversationGuestLinksStatus <@> mkNamedAPI @"remove-code-unqualified" rmCodeUnqualified <@> mkNamedAPI @"get-code" getCode - <@> mkNamedAPI @"member-typing-unqualified" (callsFed (exposeAnnotations memberTypingUnqualified)) - <@> mkNamedAPI @"member-typing-qualified" (callsFed (exposeAnnotations memberTyping)) - <@> mkNamedAPI @"remove-member-unqualified" (callsFed (exposeAnnotations removeMemberUnqualified)) - <@> mkNamedAPI @"remove-member" (callsFed (exposeAnnotations removeMemberQualified)) - <@> mkNamedAPI @"update-other-member-unqualified" (callsFed (exposeAnnotations updateOtherMemberUnqualified)) - <@> mkNamedAPI @"update-other-member" (callsFed (exposeAnnotations updateOtherMember)) - <@> mkNamedAPI @"update-conversation-name-deprecated" (callsFed (exposeAnnotations updateUnqualifiedConversationName)) - <@> mkNamedAPI @"update-conversation-name-unqualified" (callsFed (exposeAnnotations updateUnqualifiedConversationName)) - <@> mkNamedAPI @"update-conversation-name" (callsFed (exposeAnnotations updateConversationName)) - <@> mkNamedAPI @"update-conversation-message-timer-unqualified" (callsFed (exposeAnnotations updateConversationMessageTimerUnqualified)) - <@> mkNamedAPI @"update-conversation-message-timer" (callsFed (exposeAnnotations updateConversationMessageTimer)) - <@> mkNamedAPI @"update-conversation-receipt-mode-unqualified" (callsFed (exposeAnnotations updateConversationReceiptModeUnqualified)) - <@> mkNamedAPI @"update-conversation-receipt-mode" (callsFed (exposeAnnotations updateConversationReceiptMode)) - <@> mkNamedAPI @"update-conversation-access-unqualified" (callsFed (exposeAnnotations updateConversationAccessUnqualified)) - <@> mkNamedAPI @"update-conversation-access@v2" (callsFed (exposeAnnotations updateConversationAccess)) - <@> mkNamedAPI @"update-conversation-access" (callsFed (exposeAnnotations updateConversationAccess)) + <@> mkNamedAPI @"member-typing-unqualified" memberTypingUnqualified + <@> mkNamedAPI @"member-typing-qualified" memberTyping + <@> mkNamedAPI @"remove-member-unqualified" removeMemberUnqualified + <@> mkNamedAPI @"remove-member" removeMemberQualified + <@> mkNamedAPI @"update-other-member-unqualified" updateOtherMemberUnqualified + <@> mkNamedAPI @"update-other-member" updateOtherMember + <@> mkNamedAPI @"update-conversation-name-deprecated" updateUnqualifiedConversationName + <@> mkNamedAPI @"update-conversation-name-unqualified" updateUnqualifiedConversationName + <@> mkNamedAPI @"update-conversation-name" updateConversationName + <@> mkNamedAPI @"update-conversation-message-timer-unqualified" updateConversationMessageTimerUnqualified + <@> mkNamedAPI @"update-conversation-message-timer" updateConversationMessageTimer + <@> mkNamedAPI @"update-conversation-receipt-mode-unqualified" updateConversationReceiptModeUnqualified + <@> mkNamedAPI @"update-conversation-receipt-mode" updateConversationReceiptMode + <@> mkNamedAPI @"update-conversation-access-unqualified" updateConversationAccessUnqualified + <@> mkNamedAPI @"update-conversation-access@v2" updateConversationAccess + <@> mkNamedAPI @"update-conversation-access" updateConversationAccess <@> mkNamedAPI @"get-conversation-self-unqualified" getLocalSelf <@> mkNamedAPI @"update-conversation-self-unqualified" updateUnqualifiedSelfMember <@> mkNamedAPI @"update-conversation-self" updateSelfMember diff --git a/services/galley/src/Galley/API/Public/LegalHold.hs b/services/galley/src/Galley/API/Public/LegalHold.hs index b313b84e972..04afca327fb 100644 --- a/services/galley/src/Galley/API/Public/LegalHold.hs +++ b/services/galley/src/Galley/API/Public/LegalHold.hs @@ -19,7 +19,6 @@ module Galley.API.Public.LegalHold where import Galley.API.LegalHold import Galley.App -import Wire.API.Federation.API import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.LegalHold @@ -27,9 +26,9 @@ legalHoldAPI :: API LegalHoldAPI GalleyEffects legalHoldAPI = mkNamedAPI @"create-legal-hold-settings" createSettings <@> mkNamedAPI @"get-legal-hold-settings" getSettings - <@> mkNamedAPI @"delete-legal-hold-settings" (callsFed (exposeAnnotations removeSettingsInternalPaging)) + <@> mkNamedAPI @"delete-legal-hold-settings" removeSettingsInternalPaging <@> mkNamedAPI @"get-legal-hold" getUserStatus - <@> mkNamedAPI @"consent-to-legal-hold" (callsFed (exposeAnnotations grantConsent)) - <@> mkNamedAPI @"request-legal-hold-device" (callsFed (exposeAnnotations requestDevice)) - <@> mkNamedAPI @"disable-legal-hold-for-user" (callsFed (exposeAnnotations disableForUser)) - <@> mkNamedAPI @"approve-legal-hold-device" (callsFed (exposeAnnotations approveDevice)) + <@> mkNamedAPI @"consent-to-legal-hold" grantConsent + <@> mkNamedAPI @"request-legal-hold-device" requestDevice + <@> mkNamedAPI @"disable-legal-hold-for-user" disableForUser + <@> mkNamedAPI @"approve-legal-hold-device" approveDevice diff --git a/services/galley/src/Galley/API/Public/MLS.hs b/services/galley/src/Galley/API/Public/MLS.hs index e068923b7fa..d99270c8485 100644 --- a/services/galley/src/Galley/API/Public/MLS.hs +++ b/services/galley/src/Galley/API/Public/MLS.hs @@ -20,12 +20,11 @@ module Galley.API.Public.MLS where import Galley.API.MLS import Galley.App import Imports -import Wire.API.MakesFederatedCall import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.MLS mlsAPI :: API MLSAPI GalleyEffects mlsAPI = - mkNamedAPI @"mls-message" (callsFed (exposeAnnotations postMLSMessageFromLocalUser)) - <@> mkNamedAPI @"mls-commit-bundle" (callsFed (exposeAnnotations postMLSCommitBundleFromLocalUser)) + mkNamedAPI @"mls-message" postMLSMessageFromLocalUser + <@> mkNamedAPI @"mls-commit-bundle" postMLSCommitBundleFromLocalUser <@> mkNamedAPI @"mls-public-keys" (const getMLSPublicKeys) diff --git a/services/galley/src/Galley/API/Public/Messaging.hs b/services/galley/src/Galley/API/Public/Messaging.hs index efbbd7482f7..806484ae908 100644 --- a/services/galley/src/Galley/API/Public/Messaging.hs +++ b/services/galley/src/Galley/API/Public/Messaging.hs @@ -19,13 +19,12 @@ module Galley.API.Public.Messaging where import Galley.API.Update import Galley.App -import Wire.API.Federation.API import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.Messaging messagingAPI :: API MessagingAPI GalleyEffects messagingAPI = - mkNamedAPI @"post-otr-message-unqualified" (callsFed (exposeAnnotations postOtrMessageUnqualified)) + mkNamedAPI @"post-otr-message-unqualified" postOtrMessageUnqualified <@> mkNamedAPI @"post-otr-broadcast-unqualified" postOtrBroadcastUnqualified - <@> mkNamedAPI @"post-proteus-message" (callsFed (exposeAnnotations postProteusMessage)) + <@> mkNamedAPI @"post-proteus-message" postProteusMessage <@> mkNamedAPI @"post-proteus-broadcast" postProteusBroadcast diff --git a/services/galley/src/Galley/API/Public/TeamConversation.hs b/services/galley/src/Galley/API/Public/TeamConversation.hs index 173d7aba61a..359c69f1db2 100644 --- a/services/galley/src/Galley/API/Public/TeamConversation.hs +++ b/services/galley/src/Galley/API/Public/TeamConversation.hs @@ -19,7 +19,6 @@ module Galley.API.Public.TeamConversation where import Galley.API.Teams import Galley.App -import Wire.API.Federation.API import Wire.API.Routes.API import Wire.API.Routes.Public.Galley.TeamConversation @@ -28,4 +27,4 @@ teamConversationAPI = mkNamedAPI @"get-team-conversation-roles" getTeamConversationRoles <@> mkNamedAPI @"get-team-conversations" getTeamConversations <@> mkNamedAPI @"get-team-conversation" getTeamConversation - <@> mkNamedAPI @"delete-team-conversation" (callsFed (exposeAnnotations deleteTeamConversation)) + <@> mkNamedAPI @"delete-team-conversation" deleteTeamConversation diff --git a/services/galley/test/integration/API/Federation/Util.hs b/services/galley/test/integration/API/Federation/Util.hs index 84fc3acea22..0ee214fa84a 100644 --- a/services/galley/test/integration/API/Federation/Util.hs +++ b/services/galley/test/integration/API/Federation/Util.hs @@ -27,7 +27,6 @@ import GHC.TypeLits import Imports import Servant import Wire.API.Federation.Domain -import Wire.API.MakesFederatedCall import Wire.API.Routes.Named import Wire.API.VersionInfo @@ -43,9 +42,6 @@ instance (HasTrivialHandler api) => HasTrivialHandler ((path :: Symbol) :> api) instance (HasTrivialHandler api) => HasTrivialHandler (OriginDomainHeader :> api) where trivialHandler name _ = trivialHandler @api name -instance (HasTrivialHandler api) => HasTrivialHandler (MakesFederatedCall comp name :> api) where - trivialHandler name _ = trivialHandler @api name - instance (HasTrivialHandler api) => HasTrivialHandler (ReqBody cs a :> api) where trivialHandler name _ = trivialHandler @api name diff --git a/tools/fedcalls/.ormolu b/tools/fedcalls/.ormolu deleted file mode 120000 index 157b212d7cd..00000000000 --- a/tools/fedcalls/.ormolu +++ /dev/null @@ -1 +0,0 @@ -../../.ormolu \ No newline at end of file diff --git a/tools/fedcalls/README.md b/tools/fedcalls/README.md deleted file mode 100644 index bb62be4be4c..00000000000 --- a/tools/fedcalls/README.md +++ /dev/null @@ -1,38 +0,0 @@ -our swaggger docs contain information about which end-points call -which federation end-points internally. this command line tool -extracts that information from the swagger json and converts it into -two files: dot (for feeding into graphviz), and csv. - -### try it out - -``` -cabal run fedcalls -ls wire-fedcalls.* # these names are hard-coded (sorry!) -dot -Tpng wire-fedcalls.dot > wire-fedcalls.png -``` - -`dot` layouts only work for small data sets (at least without tweaking). for a better one paste into [sketchvis](https://sketchviz.com/new). - -### links - -for users: - -- blog post explaining the technology: https://reasonablypolymorphic.com/blog/abusing-constraints/index.html -- https://sketchviz.com/new -- https://graphviz.org/doc/info/lang.html - -for developers: - -- `./example.png` -- [MakesFederatedCall.hs (as of 2023-01-16)](https://github.com/wireapp/wire-server/blob/8760b4978ccb039b229d458b7a08136a05e12ff9/libs/wire-api/src/Wire/API/MakesFederatedCall.hs) -- PRs: https://github.com/wireapp/wire-server/pull/2973, https://github.com/wireapp/wire-server/pull/2940, https://github.com/wireapp/wire-server/pull/2950, https://github.com/wireapp/wire-server/pull/2957 - -### swagger-ui - -you can get the same data for the public API in the swagger-ui output. just load the page, open your javascript console, and type: - -``` -window.ui.getConfigs().showExtensions = true -``` - -then drop down on things like normal, and you'll see federated calls. diff --git a/tools/fedcalls/default.nix b/tools/fedcalls/default.nix deleted file mode 100644 index f1738ca4dfe..00000000000 --- a/tools/fedcalls/default.nix +++ /dev/null @@ -1,36 +0,0 @@ -# WARNING: GENERATED FILE, DO NOT EDIT. -# This file is generated by running hack/bin/generate-local-nix-packages.sh and -# must be regenerated whenever local packages are added or removed, or -# dependencies are added or removed. -{ mkDerivation -, base -, containers -, gitignoreSource -, imports -, language-dot -, lens -, lib -, mtl -, servant -, wire-api -}: -mkDerivation { - pname = "fedcalls"; - version = "1.0.0"; - src = gitignoreSource ./.; - isLibrary = false; - isExecutable = true; - executableHaskellDepends = [ - base - containers - imports - language-dot - lens - mtl - servant - wire-api - ]; - description = "Generate a dot file from swagger docs representing calls to federated instances"; - license = lib.licenses.agpl3Only; - mainProgram = "fedcalls"; -} diff --git a/tools/fedcalls/example.png b/tools/fedcalls/example.png deleted file mode 100644 index 26bc63134fc15f6e76dcc857c138989e2a94a0ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 110931 zcmeFZbx@wmvM>A+f;+(_5Fog_ySuvtcX#*TA-FpPclQJd?iSqL-QhmTT6^t%>fWkb zr|P>^-yi2mAiVSR^i22k^mPAvNSM5=7y>K~EC>WbkPsJE1c4ygK%h6f&~Jbi$?^bY z5a=DJhqAhpqP{DkorA53xs@@Yle?WUp|P8}2?*r2P>^csg4>!H{A!5C^EN+_9~#0C(qW+VU6#j>qn2pWU^j2 zMe)qO_zg=i3Ew=aKqR7Q29z&ja~XZ<51#aIE|k)FeVaygXjop|Cg?Qoy{>7r>N^at zc(gb==pZW=fjtq z&z>`|Z<~0Ekt&~l7JaIPH0H5NNIJ-;#l04NTY45X}OUo9GpbzG>vMDk(Q`fNm^COVv%fii;{Vj z(t_Cqi^{x$#I*WJ3(NAx2}3!*Z`YHSw(YkJ$&U0FYbQS-42RjLhNqUOYe!ih@ggoX z+~+AvD;gefxK8SRMC-ULTi^1?vd2#j^vQBu*G-Kz#V^R5>{vD~c`P2gdYK$&%&z*p z5?QWi)ZZCNV$P10ou7c?jv))!84O~>M9@Mp>L$E_<)k$22u{yqEKI+DzYiNhJsDq% zAJ4#;EEnq1Eq0>qWvra?=({~K^CY#h@yg;TTl_dQ{G8dCRns6MbU5&<6b@ZEYp+9T zQO91FNYF*nvaWx03Eie8>6`W31!bKZtY{V{V<0S)2L03cB5FxMZ2;^C%%jNP4YY)zVUaLMn-_`BWl5 zDw8682KmwM!D8ELb`BQXaAbU9px*CTf0?Pavo<~9A!8p;-Dj?fpP>2>|3!i`z0S=vmFNdHxocAS=Ri?^#}V`G|%&|5oIm+(*X zC$UqbQkEBGG>FzhVo;7@)m}X@s^Ls68ss)N%=uK1Aw@XBE>=y{7CF0D1 zWXf_xHn_wnI1%;uy3CZiABBx8qj5^OqUsN|L#$A)VU_c>$ai4;b>V7Rd-wUOp#88W*kwhSy9=YIkpj1sV)kS2%~ED#$D`cYn$F<~_;jeB0NqsM~@|>wQwF zzv;$0ORW^%hUXM#p&R_5+^#gQweVrlpnkjX*s}OE_{E*_R?`~4^2?|8)y*d~BTtiO zq4cbqQg@cmy2EE>8<0c3sr~JyEqPh28>US!SVw#^+q}CEbMnv0 za~De^MkiVpd;tllW&!+$pIN99bV#Yw_@(ndhH^4Jq+i2beZ8m*c!JpP0MkLg>U^W# zRSFs&_mu;a7fwU(oRst0iF9VbmUKrY=TF!nv4hDKkzDvvrv&4PpC))YaAx-YGg2PL z`YUIZc&W=LrlMoe>KfeJ7VEGOPrAPB<+u2C^nF;*0j}CBM9*D>E0CW^rXBORA89^8 zW{6x(7Ws>YFi8%aDMa*;?rGmTdM2TQyu=oOvFUN{W z#&Dkc=JB&T^BS}ZN1^l*oGS<48vAtDN3&}9rhG6d4tlKbTOlmyAkC+$UEE$MhlH`Q z7`ZwgHWcSn3S6`izpbS;8;X$g67|px;S(t&OsiT7Wg^@5py6^qjxGI(Iu;kvUX@R+ zIl2ceMtFgX~ZPq3{%Np!s# zQF_frKIz`0Wi2Mmq@4I%!OMvY6br^`P~A}Q(?OyAy}7>|KkgDhExg6mWS^rk z4*jd{XTO%ABVa*Wi``+IY^0_ZgONLA-)axLD!^jvSLF|JSIy->Nw{W{F9jx;z5T=w zJKJ88vLj5akanX;DemIW_fk71-lpFmT#GI&XoaN_|Bj_Ke3%01YzR5%>FMa@IDn70 zyJ*utbiS09MKC zEPTsukdzO_7yqMKbgYWof>-X{pjIM8#1C@1=jIcQ?=;8UbON?Duc%KWFSRG83yTv94hGw8J~TiF%;wM%N?k-T zp8ER`hMgG9`7KR^qEI~Gqe|~ z(msx2En-Uzu_*dR7KI7F2BB8I^gHe|4PKA`4T@*qcdf`U57InBmt_c#kw)jn+ z5nbU&a)Q-^`zMzZ8--FP64+(kt?$O4Wm^m1BJ#KhLdn}E*wbFgy@#U#n?>Y*9Nd

    Tbxy{$pAmxnVa6KLeBbjq+C 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 diff --git a/tools/fedcalls/fedcalls.cabal b/tools/fedcalls/fedcalls.cabal deleted file mode 100644 index aa2f03e2d4e..00000000000 --- a/tools/fedcalls/fedcalls.cabal +++ /dev/null @@ -1,75 +0,0 @@ -cabal-version: 1.12 -name: fedcalls -version: 1.0.0 -synopsis: - Generate a dot file from swagger docs representing calls to federated instances. - -category: Network -author: Wire Swiss GmbH -maintainer: Wire Swiss GmbH -copyright: (c) 2020 Wire Swiss GmbH -license: AGPL-3 -build-type: Simple - -executable fedcalls - main-is: Main.hs - hs-source-dirs: src - default-extensions: - AllowAmbiguousTypes - BangPatterns - ConstraintKinds - DataKinds - DefaultSignatures - DeriveFunctor - DeriveGeneric - DeriveLift - DeriveTraversable - DerivingStrategies - DerivingVia - DuplicateRecordFields - EmptyCase - FlexibleContexts - FlexibleInstances - FunctionalDependencies - GADTs - InstanceSigs - KindSignatures - LambdaCase - MultiParamTypeClasses - MultiWayIf - NamedFieldPuns - NoImplicitPrelude - OverloadedRecordDot - OverloadedStrings - PackageImports - PatternSynonyms - PolyKinds - QuasiQuotes - RankNTypes - ScopedTypeVariables - StandaloneDeriving - TupleSections - TypeApplications - TypeFamilies - TypeFamilyDependencies - TypeOperators - UndecidableInstances - ViewPatterns - - ghc-options: - -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates - -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path - -funbox-strict-fields -threaded "-with-rtsopts=-N -T" -rtsopts - -Wredundant-constraints -Wunused-packages - - build-depends: - base - , containers - , imports - , language-dot - , lens - , mtl - , servant - , wire-api - - default-language: GHC2021 diff --git a/tools/fedcalls/src/Main.hs b/tools/fedcalls/src/Main.hs deleted file mode 100644 index ad14e495706..00000000000 --- a/tools/fedcalls/src/Main.hs +++ /dev/null @@ -1,195 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE RecordWildCards #-} - --- 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 Main - ( main, - ) -where - -import Control.Exception (assert) -import Control.Lens -import Control.Monad.State (evalState) -import Data.Data (Proxy (Proxy)) -import Data.Map qualified as M -import Imports -import Language.Dot as D -import Servant.API -import Wire.API.MakesFederatedCall (Calls (..), FedCallFrom' (..), HasFeds (..)) -import Wire.API.Routes.API -import Wire.API.Routes.Internal.Brig qualified as BrigIRoutes -import Wire.API.Routes.Public.Brig -import Wire.API.Routes.Public.Cannon -import Wire.API.Routes.Public.Cargohold -import Wire.API.Routes.Public.Galley -import Wire.API.Routes.Public.Gundeck -import Wire.API.Routes.Public.Proxy -import Wire.API.Routes.Public.Spar -import Wire.API.Routes.Version - ------------------------------- - -main :: IO () -main = do - writeFile "wire-fedcalls.dot" . D.renderDot . mkDotGraph $ calls - writeFile "wire-fedcalls.csv" . toCsv $ calls - -calls :: [MakesCallTo] -calls = assert (calls' == nub calls') calls' - where - calls' = parse $ Proxy @Swaggers - -type Swaggers = - -- TODO: introduce allSwaggerApis in wire-api that collects these for all - -- services, use that in /services/brig/src/Brig/API/Public.hs instead of - -- doing it by hand. - SpecialisedAPIRoutes 'V5 BrigAPITag - :<|> SpecialisedAPIRoutes 'V5 CannonAPITag - :<|> SpecialisedAPIRoutes 'V5 CargoholdAPITag - :<|> SpecialisedAPIRoutes 'V5 GalleyAPITag - :<|> SpecialisedAPIRoutes 'V5 GundeckAPITag - :<|> SpecialisedAPIRoutes 'V5 ProxyAPITag - :<|> SpecialisedAPIRoutes 'V5 SparAPITag - -- TODO: collect all internal apis somewhere else (brig?) - :<|> BrigIRoutes.API - --- :<|> CannonIRoutes.API --- :<|> CargoholdIRoutes.API --- :<|> LegalHoldIRoutes.API - ------------------------------- - -data MakesCallTo = MakesCallTo - { -- who is calling? - sourcePath :: String, - sourceMethod :: String, - -- where does the call go? - targetComp :: String, - targetName :: String - } - deriving (Eq, Show) - ------------------------------- - -fromFedCall :: FedCallFrom' Identity -> [MakesCallTo] -fromFedCall FedCallFrom {..} = do - (comp, names) <- M.assocs $ unCalls fedCalls - MakesCallTo - (runIdentity name) - (runIdentity method) - comp - <$> names - -filterCalls :: FedCallFrom' Maybe -> Maybe (FedCallFrom' Identity) -filterCalls fedCall = - FedCallFrom - <$> fmap pure (name fedCall) - <*> fmap pure (method fedCall) - <*> pure (fedCalls fedCall) - -parse :: (HasFeds api) => Proxy api -> [MakesCallTo] -parse p = do - fedCallM <- evalState (getFedCalls p) mempty - fedCallI <- maybeToList $ filterCalls fedCallM - fromFedCall fedCallI - ------------------------------- - --- | (this function can be simplified by tossing the serial numbers for nodes, but they might --- be useful for fine-tuning the output or rendering later.) --- --- the layout isn't very useful on realistic data sets. maybe we can tweak it with --- [layers](https://www.graphviz.org/docs/attr-types/layerRange/)? -mkDotGraph :: [MakesCallTo] -> D.Graph -mkDotGraph inbound = Graph StrictGraph DirectedGraph Nothing (mods <> nodes <> edges) - where - mods = - [ AttributeStatement GraphAttributeStatement [AttributeSetValue (NameId "rankdir") (NameId "LR")], - AttributeStatement NodeAttributeStatement [AttributeSetValue (NameId "shape") (NameId "rectangle")], - AttributeStatement EdgeAttributeStatement [AttributeSetValue (NameId "style") (NameId "dashed")] - ] - nodes = - [ SubgraphStatement (NewSubgraph Nothing (mkCallingNode <$> M.toList callingNodes)), - SubgraphStatement (NewSubgraph Nothing (mkCalledNode <$> M.toList calledNodes)) - ] - edges = mkEdge <$> inbound - - itemSourceNode :: MakesCallTo -> String - itemSourceNode (MakesCallTo path method _ _) = method <> " " <> path - - itemTargetNode :: MakesCallTo -> String - itemTargetNode (MakesCallTo _ _ comp rpcName) = "[" <> comp <> "]:" <> rpcName - - callingNodes :: Map String Integer - callingNodes = - foldl - (\mp (i, caller) -> M.insert caller i mp) - mempty - ((zip [0 ..] . nub $ itemSourceNode <$> inbound) :: [(Integer, String)]) - - calledNodes :: Map String Integer - calledNodes = - foldl - (\mp (i, called) -> M.insert called i mp) - mempty - ((zip [(fromIntegral $ M.size callingNodes) ..] . nub $ itemTargetNode <$> inbound) :: [(Integer, String)]) - - mkCallingNode :: (String, Integer) -> Statement - mkCallingNode n = - NodeStatement (mkCallingNodeId n) [] - - mkCallingNodeId :: (String, Integer) -> NodeId - mkCallingNodeId (caller, i) = - NodeId (NameId . show $ show i <> ": " <> caller) (Just (PortC CompassW)) - - mkCalledNode :: (String, Integer) -> Statement - mkCalledNode n = - NodeStatement (mkCalledNodeId n) [] - - mkCalledNodeId :: (String, Integer) -> NodeId - mkCalledNodeId (callee, i) = - NodeId (NameId . show $ show i <> ": " <> callee) (Just (PortC CompassE)) - - mkEdge :: MakesCallTo -> Statement - mkEdge item = - EdgeStatement - [ ENodeId NoEdge (mkCallingNodeId (caller, callerId)), - ENodeId DirectedEdge (mkCalledNodeId (callee, calleeId)) - ] - [] - where - caller = itemSourceNode item - callee = itemTargetNode item - callerId = fromMaybe (error "impossible") $ M.lookup caller callingNodes - calleeId = fromMaybe (error "impossible") $ M.lookup callee calledNodes - ------------------------------- - -toCsv :: [MakesCallTo] -> String -toCsv = - intercalate "\n" - . fmap (intercalate ",") - . addhdr - . fmap dolines - where - addhdr :: [[String]] -> [[String]] - addhdr = (["source method", "source path", "target component", "target name"] :) - - dolines :: MakesCallTo -> [String] - dolines (MakesCallTo spath smeth tcomp tname) = [smeth, spath, tcomp, tname] diff --git a/tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs b/tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs index c5fb64196c8..e779f01a8e8 100644 --- a/tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs +++ b/tools/rabbitmq-consumer/src/RabbitMQConsumer/Lib.hs @@ -29,8 +29,8 @@ import Imports import Network.AMQP import Network.Socket import Options.Applicative +import Wire.API.Component (Component) import Wire.API.Federation.BackendNotifications (BackendNotification (..)) -import Wire.API.MakesFederatedCall (Component) main :: IO () main = do From 601d71eae7eacf130a7ff51420a27185793e72c8 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 23 Oct 2024 09:44:50 +0200 Subject: [PATCH 121/136] Process bounce and complaint notifications from SES correctly (#4301) * types-common: Add type to represent an email address with name * brig: Use Mailbox type to parse a SESNotification * brig: Print parser error when failing to parse a message from SQS * Add Orphan ToJSON instance for Mailbox in integration tests The instance is probably not correct enough for prod uses so it lives in brig-integration. Co-authored-by: Matthias Fischmann --- changelog.d/3-bug-fixes/ses-notifications | 1 + libs/types-common/default.nix | 2 + libs/types-common/src/Data/Mailbox.hs | 214 ++++++++++++++++++ libs/types-common/test/Main.hs | 4 +- libs/types-common/test/Test/Data/Mailbox.hs | 69 ++++++ libs/types-common/types-common.cabal | 8 + services/brig/src/Brig/AWS.hs | 8 +- services/brig/src/Brig/AWS/SesNotification.hs | 41 ++-- services/brig/src/Brig/AWS/Types.hs | 6 +- .../brig/test/integration/API/User/Account.hs | 5 +- services/brig/test/integration/Util.hs | 7 + 11 files changed, 335 insertions(+), 30 deletions(-) create mode 100644 changelog.d/3-bug-fixes/ses-notifications create mode 100644 libs/types-common/src/Data/Mailbox.hs create mode 100644 libs/types-common/test/Test/Data/Mailbox.hs diff --git a/changelog.d/3-bug-fixes/ses-notifications b/changelog.d/3-bug-fixes/ses-notifications new file mode 100644 index 00000000000..be2735b450d --- /dev/null +++ b/changelog.d/3-bug-fixes/ses-notifications @@ -0,0 +1 @@ +Process bounce and complaint notifications from SES correctly. \ No newline at end of file diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index 3b39ec41402..6e45c4c4d3c 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -123,6 +123,7 @@ mkDerivation { bytestring bytestring-conversion cereal + email-validate imports protobuf string-conversions @@ -132,6 +133,7 @@ mkDerivation { text time unordered-containers + utf8-string uuid ]; description = "Shared type definitions"; diff --git a/libs/types-common/src/Data/Mailbox.hs b/libs/types-common/src/Data/Mailbox.hs new file mode 100644 index 00000000000..c9889d051f4 --- /dev/null +++ b/libs/types-common/src/Data/Mailbox.hs @@ -0,0 +1,214 @@ +module Data.Mailbox where + +import Control.Applicative (optional) +import Data.Aeson +import Data.Attoparsec.ByteString (Parser) +import Data.Attoparsec.ByteString qualified as BSParser +import Data.Attoparsec.ByteString.Char8 qualified as Char8Parser +import Data.Char qualified as Char +import Data.Text qualified as Text +import Data.Text.Encoding qualified as Text +import Imports +import Text.Email.Parser + +-- | Mailbox address according to +-- https://www.rfc-editor.org/rfc/rfc5322#section-3.4 +data Mailbox = Mailbox + { name :: Maybe [Text], + address :: EmailAddress + } + deriving (Show, Eq) + +parseMailbox :: ByteString -> Either String Mailbox +parseMailbox = BSParser.parseOnly (mailboxParser <* BSParser.endOfInput) + +instance FromJSON Mailbox where + parseJSON = + withText "Mailbox" $ + either fail pure . parseMailbox . Text.encodeUtf8 + +-- * Internal + +newtype Comment = Comment [CommentContent] + +data CommentContent = CommentChar Char | SubComment Comment + +atextParser :: Parser Char +atextParser = + alpha + <|> num + <|> allowedSpecials + where + alpha = Char8Parser.satisfy (\c -> Char.isAlpha c && Char.isAscii c) + num = Char8Parser.satisfy Char.isNumber + allowedSpecials = + Char8Parser.satisfy $ + -- Make sure the - is the first or the last symbol, otherwise inClass + -- treats it as a signifier of range + Char8Parser.inClass "-!#$%&'*+/=?^_`{|}~" + +wspParser :: Parser Char +wspParser = Char8Parser.satisfy (\c -> c == ' ' || c == '\t') + +crlfParser :: Parser String +crlfParser = do + void $ Char8Parser.string "\r\n" + pure "\r\n" + +fwsParser :: Parser String +fwsParser = + let wspsAndCrlf = do + wsps <- Char8Parser.many' wspParser + crlf <- crlfParser + pure $ wsps <> crlf + notObs = do + mWspsAndCrlf <- optional wspsAndCrlf + wsps <- Char8Parser.many1' wspParser + pure $ fromMaybe "" mWspsAndCrlf <> wsps + in notObs <|> obsFwsParser + +obsFwsParser :: Parser String +obsFwsParser = do + wsps <- Char8Parser.many1' wspParser + crlfWsps <- Char8Parser.many' $ do + crlf <- crlfParser + wspsAfterCrlf <- Char8Parser.many1' wspParser + pure $ crlf <> wspsAfterCrlf + pure $ concat $ wsps : crlfWsps + +ctextParser :: Parser Char +ctextParser = do + let isAllowedChar w = + (w >= 33 && w <= 39) + || (w >= 42 && w <= 91) + || (w >= 93 && w <= 126) + Char8Parser.satisfy (isAllowedChar . Char.ord) <|> obsNoWsCtl + +-- | US-ASCII control characters that do not include the carriage return, line +-- feed, and white space characters +obsNoWsCtl :: Parser Char +obsNoWsCtl = do + Char8Parser.satisfy + ( \(ord -> c) -> + (c >= 1 && c <= 8) + || c == 11 + || c == 12 + || (c >= 14 && c <= 31) + || (c == 127) + ) + +obsCtextParser, obsQtextParser :: Parser Char +obsCtextParser = obsNoWsCtl +obsQtextParser = obsNoWsCtl + +quotedPairParser :: Parser Char +quotedPairParser = do + void $ Char8Parser.char '\\' + vCharParser <|> wspParser + +vCharParser :: Parser Char +vCharParser = + Char8Parser.satisfy (\c -> ord c >= 0x21 && ord c <= 0x7E) + +ccontentParser :: Parser CommentContent +ccontentParser = + fmap CommentChar ctextParser + <|> fmap CommentChar quotedPairParser + <|> fmap SubComment commentParser + +commentParser :: Parser Comment +commentParser = do + _ <- Char8Parser.char '(' + comment <- Char8Parser.many' $ do + _ <- optional fwsParser + ccontentParser + _ <- Char8Parser.char ')' + pure $ Comment comment + +cfwsParser :: Parser [Comment] +cfwsParser = do + let commentWithFws = do + comment <- Char8Parser.many1' $ do + _ <- optional fwsParser + commentParser + _ <- optional fwsParser + pure comment + commentWithFws <|> fmap (const []) fwsParser + +atomParser :: Parser String +atomParser = do + _ <- optional cfwsParser + atom <- Char8Parser.many1' atextParser + _ <- optional cfwsParser + pure atom + +qtextParser :: Parser Char +qtextParser = + let newParser = Char8Parser.satisfy $ \(ord -> c) -> + c == 33 || (c >= 35 && c <= 91) || (c >= 93 && c <= 126) + in newParser <|> obsQtextParser + +qcontentParser :: Parser Char +qcontentParser = qtextParser <|> quotedPairParser + +quotedStringParser :: Parser String +quotedStringParser = do + _ <- optional cfwsParser + _ <- Char8Parser.char '"' + str <- fmap concat . Char8Parser.many' $ do + mLeadingSpace <- optional fwsParser + c <- qcontentParser + pure $ fromMaybe "" mLeadingSpace <> [c] + mTrailingSpace <- optional fwsParser + _ <- Char8Parser.char '"' + pure $ str <> fromMaybe "" mTrailingSpace + +wordParser :: Parser String +wordParser = atomParser <|> quotedStringParser + +-- | The spec says +-- +-- @ +-- phrase = 1*word / obs-phrase +-- @ +-- +-- Here if we tried to write it using '<|>', parising "John Q. Doe" would +-- succeed with a 'many1 wordParser' while having parsed up to "John Q" and the +-- rest of the string will be left for next parsers, which would likely fail. To +-- avoid all that we can use just the obsPhraseParser, which forces the first +-- thing to be a word and then allows for dots and CFWS. +phraseParser :: Parser [String] +phraseParser = obsPhraseParser + +-- | Ignores comments +obsPhraseParser :: Parser [String] +obsPhraseParser = do + w1 <- wordParser + ws <- fmap catMaybes . Char8Parser.many' $ do + fmap Just wordParser + <|> fmap (Just . (: [])) (Char8Parser.char '.') + <|> fmap (const Nothing) cfwsParser + pure $ w1 : ws + +nameParser :: Parser [Text] +nameParser = map Text.pack <$> phraseParser + +-- | Does not implement parsing for obs-angle-addr +angleAddrParser :: Parser EmailAddress +angleAddrParser = do + _ <- optional cfwsParser + _ <- Char8Parser.char '<' + addr <- addrSpec + _ <- Char8Parser.char '>' + _ <- optional cfwsParser + pure addr + +nameAddrParser :: Parser Mailbox +nameAddrParser = + Mailbox + <$> optional nameParser + <*> angleAddrParser + +mailboxParser :: Parser Mailbox +mailboxParser = + nameAddrParser <|> fmap (Mailbox Nothing) addrSpec diff --git a/libs/types-common/test/Main.hs b/libs/types-common/test/Main.hs index 045e49e5e7e..4814492dfd0 100644 --- a/libs/types-common/test/Main.hs +++ b/libs/types-common/test/Main.hs @@ -21,6 +21,7 @@ module Main where import Imports +import Test.Data.Mailbox qualified as Mailbox import Test.Data.PEMKeys qualified as PEMKeys import Test.Domain qualified as Domain import Test.Handle qualified as Handle @@ -39,5 +40,6 @@ main = Domain.tests, Handle.tests, Qualified.tests, - PEMKeys.tests + PEMKeys.tests, + Mailbox.tests ] diff --git a/libs/types-common/test/Test/Data/Mailbox.hs b/libs/types-common/test/Test/Data/Mailbox.hs new file mode 100644 index 00000000000..caa01307f34 --- /dev/null +++ b/libs/types-common/test/Test/Data/Mailbox.hs @@ -0,0 +1,69 @@ +module Test.Data.Mailbox (tests) where + +import Data.ByteString.UTF8 qualified as UTF8BS +import Data.Mailbox +import Imports +import Test.Tasty +import Test.Tasty.HUnit +import Text.Email.Parser + +validAddresses :: [(ByteString, Mailbox)] +validAddresses = + [ ("john@doe.example", Mailbox Nothing $ unsafeEmailAddress "john" "doe.example"), + ("", Mailbox Nothing $ unsafeEmailAddress "john" "doe.example"), + ("John Doe", Mailbox (Just ["John", "Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("John Doe ", Mailbox (Just ["John", "Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("John Q. Doe ", Mailbox (Just ["John", "Q", ".", "Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" (My Best Friend) ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John@Doe.Example\" (My Friend @ Doe) ", Mailbox (Just ["John@Doe.Example"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" (My Best Friend) ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John \\\"The J\\\" Doe\" ", Mailbox (Just ["John \"The J\" Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John not \\tab\" ", Mailbox (Just ["John not tab"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John [Quoted Special]\" ", Mailbox (Just ["John [Quoted Special]"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John \" ", Mailbox (Just ["John "]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John \r\n NewLine\" ", Mailbox (Just ["John \r\n NewLine"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" <(local comment)john(local trailing comment)@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" <(local comment)\"john\"(local trailing comment)@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john\"" "doe.example"), + ("\"John Doe\" <\"john@funkylocal\"@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john@funkylocal\"" "doe.example"), + ("\"John Doe\" (trailing comments)", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "doe.example"), + ("\"John Doe\" ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "[funky@domain.example]"), + ("\"John Doe\" ", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "john" "[doe.example]"), + -- This is wrong, but its how the `email-validate` library does it + ("\"John Doe\" <\"john (not comment)\"@doe.example>", Mailbox (Just ["John Doe"]) $ unsafeEmailAddress "\"john(notcomment)\"" "doe.example") + ] + +invalidAddresses :: [ByteString] +invalidAddresses = + [ "john", + "john@", + "@doe.example", + "\"john@doe.example\"", + "(john@doe.example)", + "\"John UnendingQuote ", + "John [Unquoted Special] ", + " ", + "\"John \n NoCR\" ", + "\"John \r NoLF\" " + ] + +tests :: TestTree +tests = + testGroup "Mailbox" $ + [ testGroup "valid addressses" $ + map + ( \(addr, expected) -> + testCase (UTF8BS.toString addr) $ + Right expected @=? parseMailbox addr + ) + validAddresses, + testGroup "invalid addresses" $ + map + ( \addr -> + testCase (UTF8BS.toString addr) $ + case parseMailbox addr of + Left _ -> pure () + Right mb -> assertFailure $ "Expected to fail parising, but got: " <> show mb + ) + invalidAddresses + ] diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 528890fe064..5144c76d5d9 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -12,6 +12,7 @@ license-file: LICENSE build-type: Simple library + -- cabal-fmt: expand src exposed-modules: Data.Code Data.CommaSeparatedList @@ -24,6 +25,7 @@ library Data.Json.Util Data.LegalHold Data.List1 + Data.Mailbox Data.Misc Data.Nonce Data.PEMKeys @@ -151,8 +153,12 @@ library test-suite tests type: exitcode-stdio-1.0 main-is: Main.hs + + -- cabal-fmt: expand test other-modules: + Main Paths_types_common + Test.Data.Mailbox Test.Data.PEMKeys Test.Domain Test.Handle @@ -214,6 +220,7 @@ test-suite tests , bytestring , bytestring-conversion , cereal + , email-validate , imports , protobuf , string-conversions @@ -224,6 +231,7 @@ test-suite tests , time , types-common , unordered-containers + , utf8-string , uuid default-language: GHC2021 diff --git a/services/brig/src/Brig/AWS.hs b/services/brig/src/Brig/AWS.hs index f6c96a7bdf8..1d86abdc0fe 100644 --- a/services/brig/src/Brig/AWS.hs +++ b/services/brig/src/Brig/AWS.hs @@ -174,10 +174,10 @@ listen throttleMillis url callback = forever . handleAny unexpectedError $ do & set SQS.receiveMessage_waitTimeSeconds (Just 20) . set SQS.receiveMessage_maxNumberOfMessages (Just 10) onMessage m = - case decodeStrict . Text.encodeUtf8 =<< (m ^. SQS.message_body) of - Nothing -> err $ msg ("Failed to parse SQS event: " ++ show m) - Just n -> do - debug $ msg ("Received SQS event: " ++ show n) + case eitherDecodeStrict . Text.encodeUtf8 =<< maybe (Left "No message body received") Right (m ^. SQS.message_body) of + Left e -> err $ msg (val "Failed to parse SQS event") . field "error" e . field "message" (show m) + Right n -> do + debug $ msg (val "Received SQS event") . field "event" (show n) liftIO $ callback n for_ (m ^. SQS.message_receiptHandle) (void . send . SQS.newDeleteMessage url) unexpectedError x = do diff --git a/services/brig/src/Brig/AWS/SesNotification.hs b/services/brig/src/Brig/AWS/SesNotification.hs index d2e803b34ed..37aca042cc5 100644 --- a/services/brig/src/Brig/AWS/SesNotification.hs +++ b/services/brig/src/Brig/AWS/SesNotification.hs @@ -22,6 +22,7 @@ where import Brig.AWS.Types import Brig.App +import Data.Mailbox import Imports import Polysemy (Member) import System.Logger.Class (field, msg, (~~)) @@ -30,26 +31,26 @@ import Wire.API.User.Identity import Wire.UserSubsystem onEvent :: (Member UserSubsystem r) => SESNotification -> AppT r () -onEvent (MailBounce BouncePermanent es) = onPermanentBounce es -onEvent (MailBounce BounceTransient es) = onTransientBounce es -onEvent (MailBounce BounceUndetermined es) = onUndeterminedBounce es -onEvent (MailComplaint es) = onComplaint es - -onPermanentBounce :: (Member UserSubsystem r) => [EmailAddress] -> AppT r () -onPermanentBounce = mapM_ $ \e -> do - logEmailEvent "Permanent bounce" e - liftSem $ blockListInsert e - -onTransientBounce :: [EmailAddress] -> AppT r () -onTransientBounce = mapM_ (logEmailEvent "Transient bounce") - -onUndeterminedBounce :: [EmailAddress] -> AppT r () -onUndeterminedBounce = mapM_ (logEmailEvent "Undetermined bounce") - -onComplaint :: (Member UserSubsystem r) => [EmailAddress] -> AppT r () -onComplaint = mapM_ $ \e -> do - logEmailEvent "Complaint" e - liftSem $ blockListInsert e +onEvent (MailBounce BouncePermanent recipients) = onPermanentBounce recipients +onEvent (MailBounce BounceTransient recipients) = onTransientBounce recipients +onEvent (MailBounce BounceUndetermined recipients) = onUndeterminedBounce recipients +onEvent (MailComplaint recipients) = onComplaint recipients + +onPermanentBounce :: (Member UserSubsystem r) => [Mailbox] -> AppT r () +onPermanentBounce = mapM_ $ \mailbox -> do + logEmailEvent "Permanent bounce" mailbox.address + liftSem $ blockListInsert mailbox.address + +onTransientBounce :: [Mailbox] -> AppT r () +onTransientBounce = mapM_ (logEmailEvent "Transient bounce" . (.address)) + +onUndeterminedBounce :: [Mailbox] -> AppT r () +onUndeterminedBounce = mapM_ (logEmailEvent "Undetermined bounce" . (.address)) + +onComplaint :: (Member UserSubsystem r) => [Mailbox] -> AppT r () +onComplaint = mapM_ $ \mailbox -> do + logEmailEvent "Complaint" mailbox.address + liftSem $ blockListInsert mailbox.address logEmailEvent :: Text -> EmailAddress -> AppT r () logEmailEvent t e = Log.info $ field "email" (fromEmail e) ~~ msg t diff --git a/services/brig/src/Brig/AWS/Types.hs b/services/brig/src/Brig/AWS/Types.hs index 75603a6ccdb..c2201d59d2f 100644 --- a/services/brig/src/Brig/AWS/Types.hs +++ b/services/brig/src/Brig/AWS/Types.hs @@ -23,15 +23,15 @@ module Brig.AWS.Types where import Data.Aeson +import Data.Mailbox import Imports -import Wire.API.User.Identity ------------------------------------------------------------------------------- -- Notifications data SESNotification - = MailBounce !SESBounceType [EmailAddress] - | MailComplaint [EmailAddress] + = MailBounce !SESBounceType [Mailbox] + | MailComplaint [Mailbox] deriving (Eq, Show) data SESBounceType diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 9c1afb6f703..b0ba8de47d4 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -50,6 +50,7 @@ import Data.Json.Util (fromUTCTimeMillis) import Data.LegalHold import Data.List.NonEmpty qualified as NonEmpty import Data.List1 (singleton) +import Data.Mailbox import Data.Misc (plainTextPassword6Unsafe) import Data.Proxy import Data.Qualified @@ -462,8 +463,8 @@ testCreateUserBlacklist _ brig aws = publishMessage :: Text -> EmailAddress -> Text -> Http () publishMessage typ em queue = do let bdy = encode $ case typ of - "bounce" -> MailBounce BouncePermanent [em] - "complaint" -> MailComplaint [em] + "bounce" -> MailBounce BouncePermanent [Mailbox Nothing em] + "complaint" -> MailComplaint [Mailbox Nothing em] x -> error ("Unsupported message type: " ++ show x) void . AWS.execute aws $ AWS.enqueueStandard queue bdy awaitBlacklist :: Int -> EmailAddress -> Http () diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index a1baaaae223..8c39a9a7e4c 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -56,6 +56,7 @@ import Data.Handle (Handle (..)) import Data.Id import Data.List1 (List1) import Data.List1 qualified as List1 +import Data.Mailbox import Data.Misc import Data.Proxy import Data.Qualified @@ -229,6 +230,12 @@ instance ToJSON SESNotification where ] ] +instance ToJSON Mailbox where + toJSON (Mailbox mName addr) = + case mName of + Nothing -> toJSON addr + Just ns -> String $ "\"" <> T.unwords ns <> "\" <" <> T.decodeUtf8 (toByteString' addr) <> ">" + test :: Manager -> TestName -> Http a -> TestTree test m n h = testCase n (void $ runHttpT m h) From 6f96123bc2d3a99269d903fa4f7c22736aeeafe2 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 23 Oct 2024 10:17:41 +0200 Subject: [PATCH 122/136] Validate swagger (#4295) * Fix simple openapi spec violations * Add operation IDs to swagger * Add names to cargohold API * Add names to more endpoints * Fix more swagger validation errors * Add make rule to run openapi validator --------- Co-authored-by: Matthias Fischmann --- Makefile | 9 +- changelog.d/4-docs/openapi-validation | 1 + libs/types-common/src/Data/Range.hs | 12 +- .../src/Wire/API/Federation/Endpoint.hs | 1 + libs/wire-api/src/Wire/API/Call/Config.hs | 4 +- .../src/Wire/API/Conversation/Protocol.hs | 2 +- .../src/Wire/API/Event/Conversation.hs | 2 +- .../wire-api/src/Wire/API/Provider/Service.hs | 2 +- .../src/Wire/API/Provider/Service/Tag.hs | 2 +- libs/wire-api/src/Wire/API/Routes/Named.hs | 8 +- .../src/Wire/API/Routes/Public/Brig/Bot.hs | 8 +- .../src/Wire/API/Routes/Public/Cargohold.hs | 374 ++++++++++-------- .../src/Wire/API/Routes/Public/Spar.hs | 80 ++-- libs/wire-api/src/Wire/API/Team/Invitation.hs | 2 +- libs/wire-api/src/Wire/API/User/Orphans.hs | 6 +- nix/wire-server.nix | 1 + .../cargohold/src/CargoHold/API/Public.hs | 35 +- services/spar/src/Spar/API.hs | 27 +- services/spar/src/Spar/Scim/Auth.hs | 7 +- 19 files changed, 335 insertions(+), 248 deletions(-) create mode 100644 changelog.d/4-docs/openapi-validation diff --git a/Makefile b/Makefile index 7e8e2aa7b29..8f7e39530ff 100644 --- a/Makefile +++ b/Makefile @@ -85,7 +85,7 @@ cabal.project.local: c: treefmt c-fast .PHONY: c -c-fast: +c-fast: cabal build $(WIRE_CABAL_BUILD_OPTIONS) $(package) || ( make clean-hint; false ) ifeq ($(test), 1) ./hack/bin/cabal-run-tests.sh $(package) $(testargs) @@ -173,7 +173,7 @@ lint-all: formatc hlint-check-all lint-common # The extra 'hlint-check-pr' has been witnessed to be necessary due to # some bu in `hlint-inplace-pr`. Details got lost in history. .PHONY: lint-all-shallow -lint-all-shallow: lint-common formatf hlint-inplace-pr hlint-check-pr +lint-all-shallow: lint-common formatf hlint-inplace-pr hlint-check-pr .PHONY: lint-common lint-common: check-local-nix-derivations treefmt-check # weeder (does not work on CI yet) @@ -602,3 +602,8 @@ upload-bombon: --project-version $(HELM_SEMVER) \ --api-key $(DEPENDENCY_TRACK_API_KEY) \ --auto-create + +.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) diff --git a/changelog.d/4-docs/openapi-validation b/changelog.d/4-docs/openapi-validation new file mode 100644 index 00000000000..a70ca12d5e5 --- /dev/null +++ b/changelog.d/4-docs/openapi-validation @@ -0,0 +1 @@ +Fix openapi validation errors diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index c4401541756..b2dd1b60332 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -225,20 +225,20 @@ instance (ToParamSchema a, KnownNat n, KnownNat m) => ToParamSchema (Range n m [ instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m String) where toParamSchema _ = toParamSchema (Proxy @String) - & S.maxLength ?~ fromKnownNat (Proxy @n) - & S.minLength ?~ fromKnownNat (Proxy @m) + & S.minLength ?~ fromKnownNat (Proxy @n) + & S.maxLength ?~ fromKnownNat (Proxy @m) instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m T.Text) where toParamSchema _ = toParamSchema (Proxy @T.Text) - & S.maxLength ?~ fromKnownNat (Proxy @n) - & S.minLength ?~ fromKnownNat (Proxy @m) + & S.minLength ?~ fromKnownNat (Proxy @n) + & S.maxLength ?~ fromKnownNat (Proxy @m) instance (KnownNat n, KnownNat m) => ToParamSchema (Range n m TL.Text) where toParamSchema _ = toParamSchema (Proxy @TL.Text) - & S.maxLength ?~ fromKnownNat (Proxy @n) - & S.minLength ?~ fromKnownNat (Proxy @m) + & S.minLength ?~ fromKnownNat (Proxy @n) + & S.maxLength ?~ fromKnownNat (Proxy @m) instance (KnownNat n, S.ToSchema a, KnownNat m) => S.ToSchema (Range n m a) where declareNamedSchema _ = diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs index 910a6c2d4b1..cc565259e44 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs @@ -35,6 +35,7 @@ data Versioned v name instance {-# OVERLAPPING #-} (RenderableSymbol a) => RenderableSymbol (Versioned v a) where renderSymbol = renderSymbol @a + renderOperationId = renderOperationId @a type family FedPath (name :: k) :: Symbol diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index 889b8ffd1bf..e0fafcf1f6f 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -466,7 +466,7 @@ mkSFTUsername shared expires rnd = } instance ToSchema SFTUsername where - schema = toText .= parsedText "" fromText + schema = toText .= parsedText "SFTUsername" fromText where fromText :: Text -> Either String SFTUsername fromText = parseOnly (parseSFTUsername <* endOfInput) @@ -543,7 +543,7 @@ turnUsername expires rnd = } instance ToSchema TurnUsername where - schema = toText .= parsedText "" fromText + schema = toText .= parsedText "TurnUsername" fromText where fromText :: Text -> Either String TurnUsername fromText = parseOnly (parseTurnUsername <* endOfInput) diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs index 17870c6a249..ef4be957f28 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs @@ -108,7 +108,7 @@ optionalActiveMLSConversationDataSchema (Just v) (description ?~ "The epoch number of the corresponding MLS group") schema <*> fmap (.epochTimestamp) - .= field "epoch_timestamp" (named "Epoch Timestamp" . nullable . unnamed $ utcTimeSchema) + .= field "epoch_timestamp" (named "EpochTimestamp" . nullable . unnamed $ utcTimeSchema) <*> maybe MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 (.ciphersuite) .= fieldWithDocModifier "cipher_suite" diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index f06e8d62973..74d537136a4 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -409,7 +409,7 @@ taggedEventDataSchema = memberLeaveSchema :: ValueSchema NamedSwaggerDoc (EdMemberLeftReason, QualifiedUserIdList) memberLeaveSchema = - object "QualifiedUserIdList with EdMemberLeftReason" $ + object "QualifiedUserIdList_with_EdMemberLeftReason" $ (,) <$> fst .= field "reason" schema <*> snd .= qualifiedUserIdListObjectSchema instance ToSchema Event where diff --git a/libs/wire-api/src/Wire/API/Provider/Service.hs b/libs/wire-api/src/Wire/API/Provider/Service.hs index 7b181183e1e..4589dc8dc69 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service.hs @@ -282,7 +282,7 @@ data ServiceProfilePage = ServiceProfilePage instance ToSchema ServiceProfilePage where schema = - object "ServiceProfile" $ + object "ServiceProfilePage" $ ServiceProfilePage <$> serviceProfilePageHasMore .= field "has_more" schema <*> serviceProfilePageResults .= field "services" (array schema) diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index 4b0d8e1c848..95aaaebab1d 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -180,7 +180,7 @@ instance ToByteString ServiceTag where builder WeatherTag = "weather" instance ToSchema ServiceTag where - schema = enum @Text "" . mconcat $ (\a -> element (decodeUtf8With lenientDecode $ toStrict $ toByteString a) a) <$> [minBound ..] + schema = enum @Text "ServiceTag" . mconcat $ (\a -> element (decodeUtf8With lenientDecode $ toStrict $ toByteString a) a) <$> [minBound ..] instance S.ToParamSchema ServiceTag where toParamSchema _ = diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index 91f702dd412..5978774da2a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -19,7 +19,7 @@ module Wire.API.Routes.Named where -import Control.Lens ((%~)) +import Control.Lens ((%~), (?~)) import Data.Kind import Data.Metrics.Servant import Data.OpenApi.Lens hiding (HasServer) @@ -42,17 +42,22 @@ newtype Named name x = Named {unnamed :: x} -- types other than string literals in some places. class RenderableSymbol a where renderSymbol :: Text + renderOperationId :: Text + renderOperationId = renderSymbol @a instance (KnownSymbol a) => RenderableSymbol a where renderSymbol = T.pack . show $ symbolVal (Proxy @a) + renderOperationId = T.pack $ symbolVal (Proxy @a) instance (RenderableSymbol a, RenderableSymbol b) => RenderableSymbol '(a, b) where renderSymbol = "(" <> (renderSymbol @a) <> ", " <> (renderSymbol @b) <> ")" + renderOperationId = renderOperationId @a <> "_" <> renderOperationId @b newtype RenderableTypeName a = RenderableTypeName a instance (GRenderableSymbol (Rep a)) => RenderableSymbol (RenderableTypeName a) where renderSymbol = grenderSymbol @(Rep a) + renderOperationId = grenderSymbol @(Rep a) class GRenderableSymbol f where grenderSymbol :: Text @@ -64,6 +69,7 @@ instance (HasOpenApi api, RenderableSymbol name) => HasOpenApi (Named name api) toOpenApi _ = toOpenApi (Proxy @api) & allOperations . description %~ (Just (dscr <> "\n\n") <>) + & allOperations . operationId ?~ renderOperationId @name where dscr :: Text dscr = 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 8f5719ad2d4..782e29fbb59 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 @@ -52,7 +52,7 @@ type BotAPI = :> ZAccess :> ZConn :> "conversations" - :> Capture "Conversation ID" ConvId + :> Capture "conv" ConvId :> "bots" :> ReqBody '[JSON] AddBot :> MultiVerb1 'POST '[JSON] (Respond 201 "" AddBotResponse) @@ -65,9 +65,9 @@ type BotAPI = :> ZAccess :> ZConn :> "conversations" - :> Capture "Conversation ID" ConvId + :> Capture "conv" ConvId :> "bots" - :> Capture "Bot ID" BotId + :> Capture "bot" BotId :> MultiVerb 'DELETE '[JSON] DeleteResponses (Maybe RemoveBotResponse) ) :<|> Named @@ -178,7 +178,7 @@ type BotAPI = :> ZBot :> "bot" :> "users" - :> Capture "User ID" UserId + :> Capture "user" UserId :> "clients" :> Get '[JSON] [PubClient] ) diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs index 7b305f63c95..22cd1ddf5f0 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Cargohold.hs @@ -32,6 +32,7 @@ import Wire.API.Error.Cargohold import Wire.API.Routes.API import Wire.API.Routes.AssetBody import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.Routes.QualifiedCapture import Wire.API.Routes.Version @@ -39,6 +40,15 @@ import Wire.API.Routes.Version data PrincipalTag = UserPrincipalTag | BotPrincipalTag | ProviderPrincipalTag deriving (Eq, Show) +instance RenderableSymbol UserPrincipalTag where + renderSymbol = "user" + +instance RenderableSymbol BotPrincipalTag where + renderSymbol = "bot" + +instance RenderableSymbol ProviderPrincipalTag where + renderSymbol = "provider" + type family PrincipalId (tag :: PrincipalTag) = (id :: Type) | id -> tag where PrincipalId 'UserPrincipalTag = Local UserId PrincipalId 'BotPrincipalTag = BotId @@ -126,188 +136,214 @@ type CargoholdAPI = -- This was introduced before API versioning, and the user endpoints contain a -- v3 suffix, which is removed starting from API V2. type BaseAPIv3 (tag :: PrincipalTag) = - ( Summary "Upload an asset" - :> CanThrow 'AssetTooLarge - :> CanThrow 'InvalidLength - :> tag - :> AssetBody - :> MultiVerb - 'POST - '[JSON] - '[ WithHeaders - (AssetLocationHeader Relative) - (Asset, AssetLocation Relative) - (Respond 201 "Asset posted" Asset) - ] - (Asset, AssetLocation Relative) - ) - :<|> ( Summary "Download an asset" - :> tag - :> Capture "key" AssetKey - :> Header "Asset-Token" AssetToken - :> QueryParam "asset_token" AssetToken - :> ZHostOpt - :> GetAsset - ) - :<|> ( Summary "Delete an asset" - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> tag - :> Capture "key" AssetKey - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset deleted"] - () - ) + Named + '("assets-upload-v3", tag) + ( Summary "Upload an asset" + :> CanThrow 'AssetTooLarge + :> CanThrow 'InvalidLength + :> tag + :> AssetBody + :> MultiVerb + 'POST + '[JSON] + '[ WithHeaders + (AssetLocationHeader Relative) + (Asset, AssetLocation Relative) + (Respond 201 "Asset posted" Asset) + ] + (Asset, AssetLocation Relative) + ) + :<|> Named + '("assets-download-v3", tag) + ( Summary "Download an asset" + :> tag + :> Capture "key" AssetKey + :> Header "Asset-Token" AssetToken + :> QueryParam "asset_token" AssetToken + :> ZHostOpt + :> GetAsset + ) + :<|> Named + '("assets-delete-v3", tag) + ( Summary "Delete an asset" + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> tag + :> Capture "key" AssetKey + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset deleted"] + () + ) -- | Qualified asset API. Only download and delete endpoints are supported, as -- upload has stayed unqualified. These endpoints also predate API versioning, -- and contain a v4 suffix. type QualifiedAPI = - ( Summary "Download an asset" - :> Until 'V2 - :> Description - "**Note**: local assets result in a redirect, \ - \while remote assets are streamed directly." - :> ZLocalUser - :> "assets" - :> "v4" - :> QualifiedCapture "key" AssetKey - :> Header "Asset-Token" AssetToken - :> QueryParam "asset_token" AssetToken - :> ZHostOpt - :> MultiVerb - 'GET - '() - '[ ErrorResponse 'AssetNotFound, - AssetRedirect, - AssetStreaming - ] - (Maybe LocalOrRemoteAsset) - ) - :<|> ( Summary "Delete an asset" - :> Until 'V2 - :> Description "**Note**: only local assets can be deleted." - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> ZLocalUser - :> "assets" - :> "v4" - :> QualifiedCapture "key" AssetKey - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset deleted"] - () - ) + Named + "assets-download-v4" + ( Summary "Download an asset" + :> Until 'V2 + :> Description + "**Note**: local assets result in a redirect, \ + \while remote assets are streamed directly." + :> ZLocalUser + :> "assets" + :> "v4" + :> QualifiedCapture "key" AssetKey + :> Header "Asset-Token" AssetToken + :> QueryParam "asset_token" AssetToken + :> ZHostOpt + :> MultiVerb + 'GET + '() + '[ ErrorResponse 'AssetNotFound, + AssetRedirect, + AssetStreaming + ] + (Maybe LocalOrRemoteAsset) + ) + :<|> Named + "assets-delete-v4" + ( Summary "Delete an asset" + :> Until 'V2 + :> Description "**Note**: only local assets can be deleted." + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> ZLocalUser + :> "assets" + :> "v4" + :> QualifiedCapture "key" AssetKey + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset deleted"] + () + ) -- Old endpoints, predating BaseAPIv3, and therefore API versioning. type LegacyAPI = - ( ZLocalUser - :> Until 'V2 - :> "assets" - :> QueryParam' [Required, Strict] "conv_id" ConvId - :> Capture "id" AssetId - :> GetAsset - ) - :<|> ( ZLocalUser - :> Until 'V2 - :> "conversations" - :> Capture "cnv" ConvId - :> "assets" - :> Capture "id" AssetId - :> GetAsset - ) - :<|> ( ZLocalUser - :> Until 'V2 - :> "conversations" - :> Capture "cnv" ConvId - :> "otr" - :> "assets" - :> Capture "id" AssetId - :> GetAsset - ) + Named + "assets-download-legacy" + ( ZLocalUser + :> Until 'V2 + :> "assets" + :> QueryParam' [Required, Strict] "conv_id" ConvId + :> Capture "id" AssetId + :> GetAsset + ) + :<|> Named + "assets-conv-download-legacy" + ( ZLocalUser + :> Until 'V2 + :> "conversations" + :> Capture "cnv" ConvId + :> "assets" + :> Capture "id" AssetId + :> GetAsset + ) + :<|> Named + "assets-conv-otr-download-legacy" + ( ZLocalUser + :> Until 'V2 + :> "conversations" + :> Capture "cnv" ConvId + :> "otr" + :> "assets" + :> Capture "id" AssetId + :> GetAsset + ) -- | With API versioning, the previous ad-hoc v3/v4 versioning is abandoned, and -- asset endpoints are versioned normally as part of the public API, without any -- explicit prefix. type MainAPI = - ( Summary "Renew an asset token" - :> From 'V2 - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> ZLocalUser - :> "assets" - :> Capture "key" AssetKey - :> "token" - :> Post '[JSON] NewAssetToken - ) - :<|> ( Summary "Delete an asset token" - :> From 'V2 - :> Description "**Note**: deleting the token makes the asset public." - :> ZLocalUser - :> "assets" - :> Capture "key" AssetKey - :> "token" - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset token deleted"] - () - ) - :<|> ( Summary "Upload an asset" - :> From 'V2 - :> CanThrow 'AssetTooLarge - :> CanThrow 'InvalidLength - :> ZLocalUser - :> "assets" - :> AssetBody - :> MultiVerb - 'POST - '[JSON] - '[ WithHeaders - (AssetLocationHeader Relative) - (Asset, AssetLocation Relative) - (Respond 201 "Asset posted" Asset) - ] - (Asset, AssetLocation Relative) - ) - :<|> ( Summary "Download an asset" - :> From 'V2 - :> Description - "**Note**: local assets result in a redirect, \ - \while remote assets are streamed directly." - :> CanThrow 'NoMatchingAssetEndpoint - :> ZLocalUser - :> "assets" - :> QualifiedCapture "key" AssetKey - :> Header "Asset-Token" AssetToken - :> QueryParam "asset_token" AssetToken - :> ZHostOpt - :> MultiVerb - 'GET - '() - '[ ErrorResponse 'AssetNotFound, - AssetRedirect, - AssetStreaming - ] - (Maybe LocalOrRemoteAsset) - ) - :<|> ( Summary "Delete an asset" - :> From 'V2 - :> Description "**Note**: only local assets can be deleted." - :> CanThrow 'AssetNotFound - :> CanThrow 'Unauthorised - :> ZLocalUser - :> "assets" - :> QualifiedCapture "key" AssetKey - :> MultiVerb - 'DELETE - '[JSON] - '[RespondEmpty 200 "Asset deleted"] - () - ) + Named + "tokens-renew" + ( Summary "Renew an asset token" + :> From 'V2 + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> ZLocalUser + :> "assets" + :> Capture "key" AssetKey + :> "token" + :> Post '[JSON] NewAssetToken + ) + :<|> Named + "tokens-delete" + ( Summary "Delete an asset token" + :> From 'V2 + :> Description "**Note**: deleting the token makes the asset public." + :> ZLocalUser + :> "assets" + :> Capture "key" AssetKey + :> "token" + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset token deleted"] + () + ) + :<|> Named + "assets-upload" + ( Summary "Upload an asset" + :> From 'V2 + :> CanThrow 'AssetTooLarge + :> CanThrow 'InvalidLength + :> ZLocalUser + :> "assets" + :> AssetBody + :> MultiVerb + 'POST + '[JSON] + '[ WithHeaders + (AssetLocationHeader Relative) + (Asset, AssetLocation Relative) + (Respond 201 "Asset posted" Asset) + ] + (Asset, AssetLocation Relative) + ) + :<|> Named + "assets-download" + ( Summary "Download an asset" + :> From 'V2 + :> Description + "**Note**: local assets result in a redirect, \ + \while remote assets are streamed directly." + :> CanThrow 'NoMatchingAssetEndpoint + :> ZLocalUser + :> "assets" + :> QualifiedCapture "key" AssetKey + :> Header "Asset-Token" AssetToken + :> QueryParam "asset_token" AssetToken + :> ZHostOpt + :> MultiVerb + 'GET + '() + '[ ErrorResponse 'AssetNotFound, + AssetRedirect, + AssetStreaming + ] + (Maybe LocalOrRemoteAsset) + ) + :<|> Named + "assets-delete" + ( Summary "Delete an asset" + :> From 'V2 + :> Description "**Note**: only local assets can be deleted." + :> CanThrow 'AssetNotFound + :> CanThrow 'Unauthorised + :> ZLocalUser + :> "assets" + :> QualifiedCapture "key" AssetKey + :> MultiVerb + 'DELETE + '[JSON] + '[RespondEmpty 200 "Asset deleted"] + () + ) data CargoholdAPITag 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 4c8282f8d71..bf87bfb3fef 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs @@ -35,6 +35,7 @@ import Wire.API.Error import Wire.API.Error.Brig import Wire.API.Routes.API import Wire.API.Routes.Internal.Spar +import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.SwaggerServant import Wire.API.User.IdentityProvider @@ -58,8 +59,8 @@ type DeprecateSSOAPIV1 = \Details: https://docs.wire.com/understand/single-sign-on/trouble-shooting.html#can-i-use-the-same-sso-login-code-for-multiple-teams" type APISSO = - DeprecateSSOAPIV1 :> Deprecated :> "metadata" :> SAML.APIMeta - :<|> "metadata" :> Capture "team" TeamId :> SAML.APIMeta + Named "sso-metadata" (DeprecateSSOAPIV1 :> Deprecated :> "metadata" :> SAML.APIMeta) + :<|> Named "sso-team-metadata" ("metadata" :> Capture "team" TeamId :> SAML.APIMeta) :<|> "initiate-login" :> APIAuthReqPrecheck :<|> "initiate-login" :> APIAuthReq :<|> APIAuthRespLegacy @@ -69,40 +70,52 @@ type APISSO = type CheckOK = Verb 'HEAD 200 type APIAuthReqPrecheck = - QueryParam "success_redirect" URI.URI - :> QueryParam "error_redirect" URI.URI - :> Capture "idp" SAML.IdPId - :> CheckOK '[PlainText] NoContent + Named + "auth-req-precheck" + ( QueryParam "success_redirect" URI.URI + :> QueryParam "error_redirect" URI.URI + :> Capture "idp" SAML.IdPId + :> CheckOK '[PlainText] NoContent + ) type APIAuthReq = - QueryParam "success_redirect" URI.URI - :> QueryParam "error_redirect" URI.URI - -- (SAML.APIAuthReq from here on, except for the cookies) - :> Capture "idp" SAML.IdPId - :> Get '[SAML.HTML] (SAML.FormRedirect SAML.AuthnRequest) + Named + "auth-req" + ( QueryParam "success_redirect" URI.URI + :> QueryParam "error_redirect" URI.URI + -- (SAML.APIAuthReq from here on, except for the cookies) + :> Capture "idp" SAML.IdPId + :> Get '[SAML.HTML] (SAML.FormRedirect SAML.AuthnRequest) + ) type APIAuthRespLegacy = - DeprecateSSOAPIV1 - :> Deprecated - :> "finalize-login" - -- (SAML.APIAuthResp from here on, except for response) - :> MultipartForm Mem SAML.AuthnResponseBody - :> Post '[PlainText] Void + Named + "auth-resp-legacy" + ( DeprecateSSOAPIV1 + :> Deprecated + :> "finalize-login" + -- (SAML.APIAuthResp from here on, except for response) + :> MultipartForm Mem SAML.AuthnResponseBody + :> Post '[PlainText] Void + ) type APIAuthResp = - "finalize-login" - :> Capture "team" TeamId - -- (SAML.APIAuthResp from here on, except for response) - :> MultipartForm Mem SAML.AuthnResponseBody - :> Post '[PlainText] Void + Named + "auth-resp" + ( "finalize-login" + :> Capture "team" TeamId + -- (SAML.APIAuthResp from here on, except for response) + :> MultipartForm Mem SAML.AuthnResponseBody + :> Post '[PlainText] Void + ) type APIIDP = - ZOptUser :> IdpGet - :<|> ZOptUser :> IdpGetRaw - :<|> ZOptUser :> IdpGetAll - :<|> ZOptUser :> IdpCreate - :<|> ZOptUser :> IdpUpdate - :<|> ZOptUser :> IdpDelete + Named "idp-get" (ZOptUser :> IdpGet) + :<|> Named "idp-get-raw" (ZOptUser :> IdpGetRaw) + :<|> Named "idp-get-all" (ZOptUser :> IdpGetAll) + :<|> Named "idp-create" (ZOptUser :> IdpCreate) + :<|> Named "idp-update" (ZOptUser :> IdpUpdate) + :<|> Named "idp-delete" (ZOptUser :> IdpDelete) type IdpGetRaw = Capture "id" SAML.IdPId :> "raw" :> Get '[RawXML] RawIdPMetadata @@ -132,7 +145,10 @@ type IdpDelete = :> DeleteNoContent type SsoSettingsGet = - Get '[JSON] SsoSettings + Named + "sso-settings" + ( Get '[JSON] SsoSettings + ) sparSPIssuer :: (Functor m, SAML.HasConfig m) => Maybe TeamId -> m SAML.Issuer sparSPIssuer Nothing = @@ -172,9 +188,9 @@ data ScimSite tag route = ScimSite deriving (Generic) type APIScimToken = - ZOptUser :> APIScimTokenCreate - :<|> ZOptUser :> APIScimTokenDelete - :<|> ZOptUser :> APIScimTokenList + Named "auth-tokens-create" (ZOptUser :> APIScimTokenCreate) + :<|> Named "auth-tokens-delete" (ZOptUser :> APIScimTokenDelete) + :<|> Named "auth-tokens-list" (ZOptUser :> APIScimTokenList) type APIScimTokenCreate = ReqBody '[JSON] CreateScimToken diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index f195b4072ce..49fe051705a 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -118,7 +118,7 @@ instance ToSchema Invitation where <*> (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) + urlSchema = parsedText "URIRef_Absolute" (runParser (uriParser strictURIParserOptions) . TE.encodeUtf8) newtype InvitationLocation = InvitationLocation { unInvitationLocation :: ByteString diff --git a/libs/wire-api/src/Wire/API/User/Orphans.hs b/libs/wire-api/src/Wire/API/User/Orphans.hs index 0f019fdc1f9..316889c115a 100644 --- a/libs/wire-api/src/Wire/API/User/Orphans.hs +++ b/libs/wire-api/src/Wire/API/User/Orphans.hs @@ -103,7 +103,11 @@ instance ToSchema (SAML.FormRedirect SAML.AuthnRequest) where & properties . at "xml" ?~ authnReqSchema instance ToSchema (SAML.ID SAML.AuthnRequest) where - declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions + declareNamedSchema = + genericDeclareNamedSchema + samlSchemaOptions + { datatypeNameModifier = const "Id_AuthnRequest" + } instance ToSchema SAML.Time where declareNamedSchema = genericDeclareNamedSchema samlSchemaOptions diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 2429a3a78b7..e3ee19364cc 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -414,6 +414,7 @@ let pkgs.nixpkgs-fmt pkgs.openssl pkgs.ormolu + pkgs.vacuum-go pkgs.shellcheck pkgs.treefmt pkgs.gawk diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 5177e19e4a5..8b9c7cfccfd 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -60,21 +60,36 @@ servantSitemap = :<|> mainAPI where userAPI :: forall tag. (tag ~ 'UserPrincipalTag) => ServerT (BaseAPIv3 tag) Handler - userAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag + userAPI = + Named @'("assets-upload-v3", tag) uploadAssetV3 + :<|> Named @'("assets-download-v3", tag) downloadAssetV3 + :<|> Named @'("assets-delete-v3", tag) deleteAssetV3 botAPI :: forall tag. (tag ~ 'BotPrincipalTag) => ServerT (BaseAPIv3 tag) Handler - botAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag + botAPI = + Named @'("assets-upload-v3", tag) uploadAssetV3 + :<|> Named @'("assets-download-v3", tag) downloadAssetV3 + :<|> Named @'("assets-delete-v3", tag) deleteAssetV3 providerAPI :: forall tag. (tag ~ 'ProviderPrincipalTag) => ServerT (BaseAPIv3 tag) Handler - providerAPI = uploadAssetV3 @tag :<|> downloadAssetV3 @tag :<|> deleteAssetV3 @tag - legacyAPI = legacyDownloadPlain :<|> legacyDownloadPlain :<|> legacyDownloadOtr + providerAPI = + Named @'("assets-upload-v3", tag) uploadAssetV3 + :<|> Named @'("assets-download-v3", tag) downloadAssetV3 + :<|> Named @'("assets-delete-v3", tag) deleteAssetV3 + legacyAPI = + Named @"assets-download-legacy" legacyDownloadPlain + :<|> Named @"assets-conv-download-legacy" legacyDownloadPlain + :<|> Named @"assets-conv-otr-download-legacy" legacyDownloadOtr qualifiedAPI :: ServerT QualifiedAPI Handler - qualifiedAPI = downloadAssetV4 :<|> deleteAssetV4 + qualifiedAPI = + Named @"assets-download-v4" + downloadAssetV4 + :<|> Named @"assets-delete-v4" deleteAssetV4 mainAPI :: ServerT MainAPI Handler mainAPI = - renewTokenV3 - :<|> deleteTokenV3 - :<|> uploadAssetV3 @'UserPrincipalTag - :<|> downloadAssetV4 - :<|> deleteAssetV4 + Named @"tokens-renew" renewTokenV3 + :<|> Named @"tokens-delete" deleteTokenV3 + :<|> Named @"assets-upload" (uploadAssetV3 @'UserPrincipalTag) + :<|> Named @"assets-download" downloadAssetV4 + :<|> Named @"assets-delete" deleteAssetV4 internalSitemap :: ServerT InternalAPI Handler internalSitemap = diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index f814f211402..49399d77be3 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -103,6 +103,7 @@ import qualified Spar.Sem.VerdictFormatStore as VerdictFormatStore import System.Logger (Msg) import qualified URI.ByteString as URI import Wire.API.Routes.Internal.Spar +import Wire.API.Routes.Named import Wire.API.Routes.Public.Spar import Wire.API.Team.Member (HiddenPerm (CreateUpdateDeleteIdp, ReadIdp)) import Wire.API.User @@ -183,13 +184,13 @@ apiSSO :: Opts -> ServerT APISSO (Sem r) apiSSO opts = - SAML2.meta appName (SamlProtocolSettings.spIssuer Nothing) (SamlProtocolSettings.responseURI Nothing) - :<|> (\tid -> SAML2.meta appName (SamlProtocolSettings.spIssuer (Just tid)) (SamlProtocolSettings.responseURI (Just tid))) - :<|> authreqPrecheck - :<|> authreq (maxttlAuthreqDiffTime opts) - :<|> authresp Nothing - :<|> authresp . Just - :<|> ssoSettings + Named @"sso-metadata" (SAML2.meta appName (SamlProtocolSettings.spIssuer Nothing) (SamlProtocolSettings.responseURI Nothing)) + :<|> Named @"sso-team-metadata" (\tid -> SAML2.meta appName (SamlProtocolSettings.spIssuer (Just tid)) (SamlProtocolSettings.responseURI (Just tid))) + :<|> Named @"auth-req-precheck" authreqPrecheck + :<|> Named @"auth-req" (authreq (maxttlAuthreqDiffTime opts)) + :<|> Named @"auth-resp-legacy" (authresp Nothing) + :<|> Named @"auth-resp" (authresp . Just) + :<|> Named @"sso-settings" ssoSettings apiIDP :: ( Member Random r, @@ -204,12 +205,12 @@ apiIDP :: ) => ServerT APIIDP (Sem r) apiIDP = - idpGet -- get, json, captures idp id - :<|> idpGetRaw -- get, raw xml, capture idp id - :<|> idpGetAll -- get, json - :<|> idpCreate -- post, created - :<|> idpUpdate -- put, okay - :<|> idpDelete -- delete, no content + Named @"idp-get" idpGet -- get, json, captures idp id + :<|> Named @"idp-get-raw" idpGetRaw -- get, raw xml, capture idp id + :<|> Named @"idp-get-all" idpGetAll -- get, json + :<|> Named @"idp-create" idpCreate -- post, created + :<|> Named @"idp-update" idpUpdate -- put, okay + :<|> Named @"idp-delete" idpDelete -- delete, no content apiINTERNAL :: ( Member ScimTokenStore r, diff --git a/services/spar/src/Spar/Scim/Auth.hs b/services/spar/src/Spar/Scim/Auth.hs index 35e2b6a394f..45d34e667af 100644 --- a/services/spar/src/Spar/Scim/Auth.hs +++ b/services/spar/src/Spar/Scim/Auth.hs @@ -60,6 +60,7 @@ import qualified Spar.Sem.ScimTokenStore as ScimTokenStore import qualified Web.Scim.Class.Auth as Scim.Class.Auth import qualified Web.Scim.Handler as Scim import qualified Web.Scim.Schema.Error as Scim +import Wire.API.Routes.Named import Wire.API.Routes.Public.Spar (APIScimToken) import Wire.API.User as User import Wire.API.User.Scim as Api @@ -97,9 +98,9 @@ apiScimToken :: ) => ServerT APIScimToken (Sem r) apiScimToken = - createScimToken - :<|> deleteScimToken - :<|> listScimTokens + Named @"auth-tokens-create" createScimToken + :<|> Named @"auth-tokens-delete" deleteScimToken + :<|> Named @"auth-tokens-list" listScimTokens -- | > docs/reference/provisioning/scim-token.md {#RefScimTokenCreate} -- From c1ed1c433b57511e65cb72eaf9c25ccf7ebc49f2 Mon Sep 17 00:00:00 2001 From: Igor Ranieri <54423+elland@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:38:19 +0200 Subject: [PATCH 123/136] [chore] Weed out dead code, add weeder to sanitize-pr. (#4300) --- Makefile | 3 +- hack/bin/check-weed.sh | 18 +++++++++ integration/integration.cabal | 1 - integration/test/API/Stern.hs | 8 ---- integration/test/Test/FeatureFlags/Mls.hs | 10 ----- .../test/Test/FeatureFlags/MlsMigration.hs | 10 ----- integration/test/Testlib/Types.hs | 3 -- libs/metrics-wai/default.nix | 2 - libs/metrics-wai/metrics-wai.cabal | 1 - .../src/Data/Metrics/Middleware/Prometheus.hs | 11 +----- libs/types-common/default.nix | 2 - libs/types-common/src/Data/Credentials.hs | 6 --- libs/types-common/src/Data/Misc.hs | 4 -- libs/types-common/types-common.cabal | 1 - libs/wire-api/src/Wire/API/User.hs | 4 -- .../test/unit/Test/Wire/API/Password.hs | 3 +- .../src/Wire/AuthenticationSubsystem.hs | 19 ---------- .../TeamInvitationSubsystem/Interpreter.hs | 8 ---- services/brig/src/Brig/API/Auth.hs | 2 + services/brig/src/Brig/API/Internal.hs | 18 +++++++-- services/brig/src/Brig/API/Public.hs | 5 ++- services/brig/src/Brig/API/User.hs | 13 +++++-- services/brig/src/Brig/Data/Activation.hs | 10 ----- services/brig/src/Brig/Federation/Client.hs | 12 ------ services/brig/src/Brig/User/Auth.hs | 18 ++++++--- .../brig/test/integration/API/User/Util.hs | 37 ------------------- services/cargohold/src/CargoHold/AWS.hs | 9 ----- services/cargohold/src/CargoHold/Options.hs | 11 +++--- services/galley/test/integration/API/Util.hs | 26 ------------- services/gundeck/src/Gundeck/Monad.hs | 9 ----- tools/stern/default.nix | 2 - tools/stern/src/Stern/Intra.hs | 16 -------- tools/stern/stern.cabal | 1 - weeder.toml | 20 ++++++++-- 34 files changed, 86 insertions(+), 237 deletions(-) create mode 100755 hack/bin/check-weed.sh delete mode 100644 integration/test/API/Stern.hs diff --git a/Makefile b/Makefile index 8f7e39530ff..e422703c453 100644 --- a/Makefile +++ b/Makefile @@ -56,10 +56,10 @@ rabbit-clean: # Clean .PHONY: full-clean full-clean: clean + make rabbit-clean rm -rf ~/.cache/hie-bios rm -rf ./dist-newstyle ./.env direnv reload - make rabbit-clean @echo -e "\n\n*** NOTE: you may want to also 'rm -rf ~/.cabal/store \$$CABAL_DIR/store', not sure.\n" .PHONY: clean @@ -138,6 +138,7 @@ devtest: .PHONY: sanitize-pr sanitize-pr: + ./hack/bin/check-weed.sh make lint-all-shallow make git-add-cassandra-schema @git diff-files --quiet -- || ( echo "There are unstaged changes, please take a look, consider committing them, and try again."; exit 1 ) diff --git a/hack/bin/check-weed.sh b/hack/bin/check-weed.sh new file mode 100755 index 00000000000..a55e4c26ca7 --- /dev/null +++ b/hack/bin/check-weed.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Define ANSI color code for red +RED='\033[0;31m' +NC='\033[0m' # No Color (reset) + +echo "Checking for weed…" +echo "Make sure you have compiled everything with the correct settings." + +output=$(weeder -N) + +# Check if the output is empty +if [[ -z "$output" ]]; then + echo "No weed found! 🚫🪴" +else + echo "We found some weed!" + echo -e "${RED}$output${NC}" +fi diff --git a/integration/integration.cabal b/integration/integration.cabal index edada8586df..a3989f28e76 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -100,7 +100,6 @@ library API.GundeckInternal API.Nginz API.Spar - API.Stern MLS.Util Notifications RunAllTests diff --git a/integration/test/API/Stern.hs b/integration/test/API/Stern.hs deleted file mode 100644 index b7d93d07178..00000000000 --- a/integration/test/API/Stern.hs +++ /dev/null @@ -1,8 +0,0 @@ -module API.Stern where - -import Testlib.Prelude - -getTeamActivity :: (HasCallStack, MakesValue domain) => domain -> String -> App Response -getTeamActivity domain tid = - baseRequest domain Stern Unversioned (joinHttpPath ["team-activity-info", tid]) - >>= submit "GET" diff --git a/integration/test/Test/FeatureFlags/Mls.hs b/integration/test/Test/FeatureFlags/Mls.hs index 40c3783fec0..73cc96eaf12 100644 --- a/integration/test/Test/FeatureFlags/Mls.hs +++ b/integration/test/Test/FeatureFlags/Mls.hs @@ -59,16 +59,6 @@ testMlsPatch = do ] ] -mlsDefaultConfig :: Value -mlsDefaultConfig = - object - [ "protocolToggleUsers" .= ([] :: [String]), - "defaultProtocol" .= "proteus", - "supportedProtocols" .= ["proteus", "mls"], - "allowedCipherSuites" .= ([1] :: [Int]), - "defaultCipherSuite" .= toJSON (1 :: Int) - ] - mls1 :: String -> Value mls1 uid = object diff --git a/integration/test/Test/FeatureFlags/MlsMigration.hs b/integration/test/Test/FeatureFlags/MlsMigration.hs index edabd00fb16..bac309fa5bb 100644 --- a/integration/test/Test/FeatureFlags/MlsMigration.hs +++ b/integration/test/Test/FeatureFlags/MlsMigration.hs @@ -77,13 +77,3 @@ mlsMigrationConfig2 = "finaliseRegardlessAfter" .= "2031-10-17T00:00:00Z" ] ] - -mlsMigrationInvalidConfig :: Value -mlsMigrationInvalidConfig = - object - [ "status" .= "enabled", - "config" - .= object - [ "startTime" .= A.Number 1 - ] - ] diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 1c64fb56f7f..e25b33d06f8 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -407,9 +407,6 @@ assertNothing = maybe (pure ()) $ const $ assertFailure "Maybe value was Just, n addFailureContext :: String -> App a -> App a addFailureContext ctx = modifyFailureContext (\mCtx0 -> Just $ maybe ctx (\x -> ctx <> "\n" <> x) mCtx0) -modifyFailureMsg :: (String -> String) -> App a -> App a -modifyFailureMsg modMessage = modifyFailure (\e -> e {msg = modMessage e.msg}) - modifyFailureContext :: (Maybe String -> Maybe String) -> App a -> App a modifyFailureContext modContext = modifyFailure diff --git a/libs/metrics-wai/default.nix b/libs/metrics-wai/default.nix index 8bb74088e5e..5a64f6b4f93 100644 --- a/libs/metrics-wai/default.nix +++ b/libs/metrics-wai/default.nix @@ -19,7 +19,6 @@ , wai , wai-middleware-prometheus , wai-route -, wai-routing }: mkDerivation { pname = "metrics-wai"; @@ -38,7 +37,6 @@ mkDerivation { wai wai-middleware-prometheus wai-route - wai-routing ]; testHaskellDepends = [ base containers hspec imports ]; testToolDepends = [ hspec-discover ]; diff --git a/libs/metrics-wai/metrics-wai.cabal b/libs/metrics-wai/metrics-wai.cabal index 1b6e5cfa03b..779eda44ec6 100644 --- a/libs/metrics-wai/metrics-wai.cabal +++ b/libs/metrics-wai/metrics-wai.cabal @@ -81,7 +81,6 @@ library , wai >=3 , wai-middleware-prometheus , wai-route >=0.3 - , wai-routing default-language: GHC2021 diff --git a/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs b/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs index 39b73e351e9..b185b6b60da 100644 --- a/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs +++ b/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs @@ -16,26 +16,17 @@ -- with this program. If not, see . module Data.Metrics.Middleware.Prometheus - ( waiPrometheusMiddleware, - waiPrometheusMiddlewarePaths, + ( waiPrometheusMiddlewarePaths, normalizeWaiRequestRoute, ) where import Data.Id import Data.Metrics.Types (Paths, treeLookup) -import Data.Metrics.WaiRoute (treeToPaths) import Data.Text.Encoding qualified as T import Imports import Network.Wai qualified as Wai import Network.Wai.Middleware.Prometheus qualified as Promth -import Network.Wai.Routing.Route (Routes, prepare) - --- | Adds a prometheus metrics endpoint at @/i/metrics@ --- This middleware requires your servers 'Routes' because it does some normalization --- (e.g. removing params from calls) -waiPrometheusMiddleware :: (Monad m) => Routes a m b -> Wai.Middleware -waiPrometheusMiddleware routes = waiPrometheusMiddlewarePaths $ treeToPaths $ prepare routes -- | Helper function that should only be needed as long as we have wai-routing code left in -- proxy: run 'treeToPaths' on old routing tables and 'routeToPaths' on the servant ones, and diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index 6e45c4c4d3c..0cafcabbfd7 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -24,7 +24,6 @@ , gitignoreSource , hashable , http-api-data -, http-types , imports , iproute , iso3166-country-codes @@ -84,7 +83,6 @@ mkDerivation { generic-random hashable http-api-data - http-types imports iproute iso3166-country-codes diff --git a/libs/types-common/src/Data/Credentials.hs b/libs/types-common/src/Data/Credentials.hs index 52c632f9307..5423b574e7a 100644 --- a/libs/types-common/src/Data/Credentials.hs +++ b/libs/types-common/src/Data/Credentials.hs @@ -18,11 +18,8 @@ module Data.Credentials where import Data.Aeson (FromJSON) -import Data.ByteString.Base64 qualified as B64 import Data.Text -import Data.Text.Encoding qualified as TE import Imports -import Network.HTTP.Types.Header -- | Generic credentials for authenticating a user. Usually used for deserializing from a secret yaml file. data Credentials = Credentials @@ -32,6 +29,3 @@ data Credentials = Credentials deriving stock (Generic) instance FromJSON Credentials - -mkBasicAuthHeader :: Credentials -> Header -mkBasicAuthHeader (Credentials u p) = (hAuthorization, "Basic " <> B64.encode (TE.encodeUtf8 (u <> ":" <> p))) diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index 81f9ddc02e6..a72995b8667 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -49,7 +49,6 @@ module Data.Misc PlainTextPassword6, PlainTextPassword8, plainTextPassword6, - plainTextPassword8, fromPlainTextPassword, plainTextPassword8Unsafe, plainTextPassword6Unsafe, @@ -323,9 +322,6 @@ plainTextPassword6 = fmap PlainTextPassword' . checked plainTextPassword6Unsafe :: Text -> PlainTextPassword6 plainTextPassword6Unsafe = PlainTextPassword' . unsafeRange -plainTextPassword8 :: Text -> Maybe PlainTextPassword8 -plainTextPassword8 = fmap PlainTextPassword' . checked - plainTextPassword8Unsafe :: Text -> PlainTextPassword8 plainTextPassword8Unsafe = PlainTextPassword' . unsafeRange diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 5144c76d5d9..369659432cc 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -115,7 +115,6 @@ library , generic-random >=1.4.0.0 , hashable >=1.2 , http-api-data - , http-types , imports , iproute >=1.5 , iso3166-country-codes >=0.20140203.8 diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 47f3e5d56e0..25cf9e88172 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -40,7 +40,6 @@ module Wire.API.User userEmail, userSSOId, userIssuer, - userSCIMExternalId, scimExternalId, ssoIssuerAndNameId, mkUserProfile, @@ -636,9 +635,6 @@ userEmail = emailIdentity <=< userIdentity userSSOId :: User -> Maybe UserSSOId userSSOId = ssoIdentity <=< userIdentity -userSCIMExternalId :: User -> Maybe Text -userSCIMExternalId usr = scimExternalId (userManagedBy usr) =<< userSSOId usr - -- FUTUREWORK: this is only ignoring case in the email format, and emails should be -- handled case-insensitively. https://wearezeta.atlassian.net/browse/SQSERVICES-909 scimExternalId :: ManagedBy -> UserSSOId -> Maybe Text diff --git a/libs/wire-api/test/unit/Test/Wire/API/Password.hs b/libs/wire-api/test/unit/Test/Wire/API/Password.hs index c9edea60ef4..5693e5c54fe 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Password.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Password.hs @@ -31,7 +31,8 @@ tests = testGroup "Password" $ [ testCase "hash password argon2id" testHashPasswordArgon2id, testCase "update pwd hash" testUpdateHash, - testCase "verify old scrypt password still works" testHashingOldScrypt + testCase "verify old scrypt password still works" testHashingOldScrypt, + testCase "test hash scrypt" testHashPasswordScrypt ] defaultOptions :: Argon2.Options diff --git a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs index 1d3e18b8411..9d098c0ead9 100644 --- a/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs +++ b/libs/wire-subsystems/src/Wire/AuthenticationSubsystem.hs @@ -23,7 +23,6 @@ import Data.Misc import Data.Qualified import Imports import Polysemy -import Polysemy.Error import Wire.API.Password (Password, PasswordStatus) import Wire.API.User import Wire.API.User.Password (PasswordResetCode, PasswordResetIdentity) @@ -43,21 +42,3 @@ data AuthenticationSubsystem m a where InternalLookupPasswordResetCode :: EmailKey -> AuthenticationSubsystem m (Maybe PasswordResetPair) makeSem ''AuthenticationSubsystem - -authenticate :: - ( Member (Error AuthError) r, - Member AuthenticationSubsystem r - ) => - UserId -> - PlainTextPassword6 -> - Sem r () -authenticate uid pwd = authenticateEither uid pwd >>= either throw pure - -reauthenticate :: - ( Member (Error ReAuthError) r, - Member AuthenticationSubsystem r - ) => - UserId -> - Maybe PlainTextPassword6 -> - Sem r () -reauthenticate uid pwd = reauthenticateEither uid pwd >>= either throw pure diff --git a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs index bac8f052635..46445645c9d 100644 --- a/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Interpreter.hs @@ -175,14 +175,6 @@ createInvitation' tid mExpectedInvId inviteeRole mbInviterUid inviterEmail invRe mkInvitationCode :: (Member Random r) => Sem r InvitationCode mkInvitationCode = InvitationCode . AsciiText.encodeBase64Url <$> Random.bytes 24 -isPersonalUser :: (Member UserSubsystem r) => Local EmailKey -> Sem r Bool -isPersonalUser uke = do - mAccount <- getLocalUserAccountByUserKey uke - pure $ case mAccount of - -- this can e.g. happen if the key is claimed but the account is not yet created - Nothing -> False - Just user -> user.userStatus == Active && isNothing user.userTeam - -- | brig used to not store the role, so for migration we allow this to be empty and fill in the -- default here. toInvitation :: diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index b0f7295cacf..021ca38aabc 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -48,6 +48,7 @@ import Wire.API.User.Auth hiding (access) import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso +import Wire.ActivationCodeStore (ActivationCodeStore) import Wire.AuthenticationSubsystem import Wire.AuthenticationSubsystem qualified as Authentication import Wire.BlockListStore @@ -101,6 +102,7 @@ login :: Member Events r, Member (Input (Local ())) r, Member UserSubsystem r, + Member ActivationCodeStore r, Member VerificationCodeSubsystem r, Member AuthenticationSubsystem r ) => diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 94ee5b43021..21d73c16e99 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -88,6 +88,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.AuthenticationSubsystem (AuthenticationSubsystem) import Wire.BlockListStore (BlockListStore) import Wire.DeleteQueue (DeleteQueue) @@ -146,7 +147,9 @@ servantSitemap :: Member (Input (Local ())) r, Member IndexedUserStore r, Member (Polysemy.Error UserSubsystemError) r, - Member HashPassword r + Member HashPassword r, + Member (Embed IO) r, + Member ActivationCodeStore r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -199,7 +202,9 @@ accountAPI :: Member Events r, Member PasswordResetCodeStore r, Member HashPassword r, - Member InvitationStore r + Member InvitationStore r, + Member (Embed IO) r, + Member ActivationCodeStore r ) => ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = @@ -597,9 +602,14 @@ listActivatedAccountsH } pure $ others <> byEmails -getActivationCode :: EmailAddress -> Handler r GetActivationCodeResp +getActivationCode :: + ( Member ActivationCodeStore r, + Member (Embed IO) r + ) => + EmailAddress -> + Handler r GetActivationCodeResp getActivationCode email = do - apair <- lift . wrapClient $ API.lookupActivationCode email + apair <- lift . liftSem $ API.lookupActivationCode email maybe (throwStd activationKeyNotFound) (pure . GetActivationCodeResp) apair getPasswordResetCodeH :: diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 2485961ce68..a576af85158 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -144,6 +144,7 @@ import Wire.API.User.RichInfo qualified as Public import Wire.API.User.Search qualified as Public import Wire.API.UserMap qualified as Public import Wire.API.Wrapped qualified as Public +import Wire.ActivationCodeStore (ActivationCodeStore) import Wire.AuthenticationSubsystem (AuthenticationSubsystem, createPasswordResetCode, resetPassword) import Wire.BlockListStore (BlockListStore) import Wire.DeleteQueue @@ -296,6 +297,7 @@ servantSitemap :: Member SFT r, Member TinyLog r, Member UserKeyStore r, + Member ActivationCodeStore r, Member UserStore r, Member (Input TeamTemplates) r, Member UserSubsystem r, @@ -1073,7 +1075,8 @@ sendActivationCode :: ( Member BlockListStore r, Member EmailSubsystem r, Member GalleyAPIAccess r, - Member UserKeyStore r + Member UserKeyStore r, + Member ActivationCodeStore r ) => Public.SendActivationCode -> Handler r () diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index daf3109c63e..e081822b271 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -127,6 +127,8 @@ 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 qualified as ActivationCode import Wire.AuthenticationSubsystem (AuthenticationSubsystem, internalLookupPasswordResetCode) import Wire.BlockListStore as BlockListStore import Wire.DeleteQueue @@ -778,6 +780,7 @@ sendActivationCode :: ( Member BlockListStore r, Member EmailSubsystem r, Member GalleyAPIAccess r, + Member ActivationCodeStore r, Member UserKeyStore r ) => EmailAddress -> @@ -792,7 +795,7 @@ sendActivationCode email loc = do blacklisted <- lift . liftSem $ BlockListStore.exists ek when blacklisted $ throwE (ActivationBlacklistedUserKey ek) - uc <- lift . wrapClient $ Data.lookupActivationCode ek + uc <- lift . liftSem $ ActivationCode.lookupActivationCode ek case uc of Nothing -> sendVerificationEmail ek Nothing -- Fresh code request, no user Just (Nothing, c) -> sendVerificationEmail ek (Just c) -- Re-requesting existing code @@ -1070,13 +1073,15 @@ deleteAccount user = do -- Lookups lookupActivationCode :: - (MonadClient m) => + ( Member ActivationCodeStore r, + Member (Embed IO) r + ) => EmailAddress -> - m (Maybe ActivationPair) + Sem r (Maybe ActivationPair) lookupActivationCode email = do let uk = mkEmailKey email k <- liftIO $ Data.mkActivationKey uk - c <- fmap snd <$> Data.lookupActivationCode uk + c <- fmap snd <$> ActivationCode.lookupActivationCode uk pure $ (k,) <$> c lookupPasswordResetCode :: diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index a7f94e58205..ae9ce48899f 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -23,7 +23,6 @@ module Brig.Data.Activation activationErrorToRegisterError, newActivation, mkActivationKey, - lookupActivationCode, activateKey, verifyCode, ) @@ -175,12 +174,6 @@ newActivation uk timeout u = do ActivationCode . Ascii.unsafeFromText . pack . printf "%06d" <$> randIntegerZeroToNMinusOne 1000000 --- | Lookup an activation code and it's associated owner (if any) for a 'UserKey'. -lookupActivationCode :: (MonadClient m) => EmailKey -> m (Maybe (Maybe UserId, ActivationCode)) -lookupActivationCode k = - liftIO (mkActivationKey k) - >>= retry x1 . query1 codeSelect . params LocalQuorum . Identity - -- | Verify an activation code. verifyCode :: (MonadClient m) => @@ -229,8 +222,5 @@ keyInsert = 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 = ?" -codeSelect :: PrepQuery R (Identity ActivationKey) (Maybe UserId, ActivationCode) -codeSelect = "SELECT user, code FROM activation_keys WHERE key = ?" - keyDelete :: PrepQuery W (Identity ActivationKey) () keyDelete = "DELETE FROM activation_keys WHERE key = ?" diff --git a/services/brig/src/Brig/Federation/Client.hs b/services/brig/src/Brig/Federation/Client.hs index 19e0193e1ce..9c5302689d2 100644 --- a/services/brig/src/Brig/Federation/Client.hs +++ b/services/brig/src/Brig/Federation/Client.hs @@ -99,18 +99,6 @@ claimMultiPrekeyBundle domain uc = do lift . Log.info $ Log.msg @Text "Brig-federation: claiming remote multi-user prekey bundle" runBrigFederatorClient domain $ fedClient @'Brig @"claim-multi-prekey-bundle" uc -searchUsers :: - ( MonadReader Env m, - MonadIO m, - Log.MonadLogger m - ) => - Domain -> - SearchRequest -> - ExceptT FederationError m SearchResponse -searchUsers domain searchTerm = do - lift $ Log.info $ Log.msg $ T.pack "Brig-federation: search call on remote backend" - runBrigFederatorClient domain $ fedClient @'Brig @"search-users" searchTerm - getUserClients :: ( MonadReader Env m, MonadIO m, diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 9f4168727a5..b8e6556f4aa 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -38,7 +38,6 @@ import Brig.API.Types import Brig.API.User (changeSingleAccountStatus) import Brig.App import Brig.Budget -import Brig.Data.Activation qualified as Data import Brig.Data.Client import Brig.Options qualified as Opt import Brig.Types.Intra @@ -71,6 +70,8 @@ import Wire.API.User import Wire.API.User.Auth import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.Sso +import Wire.ActivationCodeStore (ActivationCodeStore) +import Wire.ActivationCodeStore qualified as ActivationCode import Wire.AuthenticationSubsystem import Wire.AuthenticationSubsystem qualified as Authentication import Wire.Events (Events) @@ -88,13 +89,14 @@ import Wire.VerificationCodeSubsystem qualified as VerificationCodeSubsystem login :: forall r. ( Member GalleyAPIAccess r, + Member (Input (Local ())) r, + Member ActivationCodeStore r, + Member Events r, Member TinyLog r, Member UserKeyStore r, Member UserStore r, - Member VerificationCodeSubsystem r, - Member (Input (Local ())) r, Member UserSubsystem r, - Member Events r, + Member VerificationCodeSubsystem r, Member AuthenticationSubsystem r ) => Login -> @@ -290,6 +292,7 @@ resolveLoginId :: ( Member UserKeyStore r, Member UserStore r, Member UserSubsystem r, + Member ActivationCodeStore r, Member (Input (Local ())) r ) => LoginId -> @@ -311,7 +314,10 @@ validateLoginId (LoginByHandle h) = Right h isPendingActivation :: forall r. - (Member UserSubsystem r, Member (Input (Local ())) r) => + ( Member UserSubsystem r, + Member ActivationCodeStore r, + Member (Input (Local ())) r + ) => LoginId -> AppT r Bool isPendingActivation ident = case ident of @@ -320,7 +326,7 @@ isPendingActivation ident = case ident of where checkKey :: EmailKey -> AppT r Bool checkKey k = do - musr <- (>>= fst) <$> wrapClient (Data.lookupActivationCode k) + musr <- (>>= fst) <$> liftSem (ActivationCode.lookupActivationCode k) case musr of Nothing -> pure False Just usr -> liftSem do diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 1a4d528d108..c05174bd505 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -40,11 +40,9 @@ import Data.Handle (parseHandle) import Data.Id import Data.Kind import Data.List1 qualified as List1 -import Data.Misc import Data.Qualified import Data.Range (unsafeRange) import Data.String.Conversions -import Data.Text.Ascii qualified as Ascii import Data.Vector qualified as Vec import Data.ZAuth.Token qualified as ZAuth import Imports @@ -69,7 +67,6 @@ import Wire.API.User.Auth import Wire.API.User.Client import Wire.API.User.Client.DPoPAccessToken (Proof) import Wire.API.User.Handle -import Wire.API.User.Password import Wire.VerificationCode qualified as Code import Wire.VerificationCodeStore.Cassandra qualified as VerificationCodeStore @@ -129,15 +126,6 @@ registerUser name brig = do ] post (brig . path "/register" . contentJson . body p) -initiatePasswordReset :: Brig -> EmailAddress -> (MonadHttp m) => m ResponseLBS -initiatePasswordReset brig email = - post - ( brig - . path "/password-reset" - . contentJson - . body (RequestBodyLBS . encode $ NewPasswordReset email) - ) - activateEmail :: (MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> EmailAddress -> m () activateEmail brig email = do act <- getActivationCode brig (Left email) @@ -180,31 +168,6 @@ initiateEmailUpdateNoSend brig email uid = in put (brig . path "/i/self/email" . contentJson . zUser uid . body emailUpdate) - Brig -> - EmailAddress -> - UserId -> - PlainTextPassword8 -> - m CompletePasswordReset -preparePasswordReset brig email uid newpw = do - let qry = queryItem "email" (toByteString' email) - r <- get $ brig . path "/i/users/password-reset-code" . qry - let lbs = fromMaybe "" $ responseBody r - let Just pwcode = PasswordResetCode . Ascii.unsafeFromText <$> (lbs ^? key "code" . _String) - let ident = PasswordResetIdentityKey (mkPasswordResetKey uid) - let complete = CompletePasswordReset ident pwcode newpw - pure complete - -completePasswordReset :: Brig -> CompletePasswordReset -> (MonadHttp m) => m ResponseLBS -completePasswordReset brig passwordResetData = - post - ( brig - . path "/password-reset/complete" - . contentJson - . body (RequestBodyLBS $ encode passwordResetData) - ) - removeBlacklist :: Brig -> EmailAddress -> (MonadIO m, MonadHttp m) => m () removeBlacklist brig email = void $ delete (brig . path "/i/users/blacklist" . queryItem "email" (toByteString' email)) diff --git a/services/cargohold/src/CargoHold/AWS.hs b/services/cargohold/src/CargoHold/AWS.hs index a7db87772ea..e94392d5755 100644 --- a/services/cargohold/src/CargoHold/AWS.hs +++ b/services/cargohold/src/CargoHold/AWS.hs @@ -1,5 +1,4 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. -- @@ -21,11 +20,6 @@ module CargoHold.AWS ( -- * Monad Env (..), - amazonkaEnvLens, - CargoHold.AWS.s3BucketLens, - CargoHold.AWS.cloudFrontLens, - amazonkaDownloadEndpointLens, - loggerLens, mkEnv, amazonkaEnvWithDownloadEndpoint, Amazon, @@ -56,7 +50,6 @@ import qualified System.Logger as Logger import System.Logger.Class (Logger, MonadLogger (log), (~~)) import qualified System.Logger.Class as Log import Util.Options (AWSEndpoint (..)) -import Util.SuffixNamer data Env = Env { logger :: !Logger, @@ -69,8 +62,6 @@ data Env = Env cloudFront :: !(Maybe CloudFront) } -makeLensesWith (lensRules & lensField .~ suffixNamer) ''Env - -- | Override the endpoint in the '_amazonkaEnv' with '_amazonkaDownloadEndpoint'. -- TODO: Choose the correct s3 addressing style amazonkaEnvWithDownloadEndpoint :: Env -> AWS.Env diff --git a/services/cargohold/src/CargoHold/Options.hs b/services/cargohold/src/CargoHold/Options.hs index 09edeb065cd..4b5ccb1d7bc 100644 --- a/services/cargohold/src/CargoHold/Options.hs +++ b/services/cargohold/src/CargoHold/Options.hs @@ -44,7 +44,6 @@ data CloudFrontOpts = CloudFrontOpts deriving (Show, Generic) deriveFromJSON defaultOptions ''CloudFrontOpts -makeLensesWith (lensRules & lensField .~ suffixNamer) ''CloudFrontOpts newtype OptS3AddressingStyle = OptS3AddressingStyle { unwrapS3AddressingStyle :: S3AddressingStyle @@ -123,8 +122,12 @@ instance FromJSON S3Compatibility where deriveFromJSON defaultOptions ''AWSOpts -makeLenses ''AWSOpts -makeLensesWith (lensRules & lensField .~ suffixNamer) ''AWSOpts +makeLensesFor + [ ("multiIngress", "multiIngressLens"), + ("s3DownloadEndpoint", "s3DownloadEndpointLens"), + ("cloudFront", "cloudFrontLens") + ] + ''AWSOpts data Settings = Settings { -- | Maximum allowed size for uploads, in bytes @@ -148,8 +151,6 @@ data Settings = Settings deriveFromJSON defaultOptions ''Settings -makeLensesWith (lensRules & lensField .~ suffixNamer) ''Settings - -- | Options consist of information the server needs to operate, and 'Settings' -- modify the behavior. data Opts = Opts diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 6fe7bcbd9bd..a12bf6d2e10 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -42,7 +42,6 @@ import Data.Code qualified as Code import Data.Currency qualified as Currency import Data.Default import Data.Domain -import Data.Handle qualified as Handle import Data.HashMap.Strict qualified as HashMap import Data.Id import Data.Json.Util hiding ((#)) @@ -69,7 +68,6 @@ import Data.UUID.V4 import Federator.MockServer import Federator.MockServer qualified as Mock import GHC.TypeNats -import Galley.Intra.User (chunkify) import Galley.Options qualified as Opts import Galley.Run qualified as Run import Galley.Types.Conversations.One2One @@ -304,14 +302,6 @@ getTeamMembers usr tid = do r <- get (g . paths ["teams", toByteString' tid, "members"] . zUser usr) UserId -> TeamId -> TestM ResponseLBS -getTeamMembersCsv usr tid = do - g <- viewGalley - get (g . accept "text/csv" . paths ["teams", toByteString' tid, "members/csv"] . zUser usr) UserId -> TeamId -> Int -> TestM TeamMemberList getTeamMembersTruncated usr tid n = do g <- viewGalley @@ -2389,22 +2379,6 @@ deleteTeam owner tid = do !!! do const 202 === statusCode -getUsersBy :: forall uidsOrHandles. (ToByteString uidsOrHandles) => ByteString -> [uidsOrHandles] -> TestM [User] -getUsersBy keyName = chunkify $ \keys -> do - brig <- viewBrig - let users = BS.intercalate "," $ toByteString' <$> keys - res <- - get - ( brig - . path "/i/users" - . queryItem keyName users - . expect2xx - ) - pure $ fromJust $ responseJsonMaybe @[User] res - -getUsersByHandle :: [Handle.Handle] -> TestM [User] -getUsersByHandle = getUsersBy "handles" - upgradeClientToLH :: (HasCallStack) => UserId -> ClientId -> TestM () upgradeClientToLH zusr cid = putCapabilities zusr cid [ClientSupportsLegalholdImplicitConsent] diff --git a/services/gundeck/src/Gundeck/Monad.hs b/services/gundeck/src/Gundeck/Monad.hs index 1ccce16a55b..6d4147ea70a 100644 --- a/services/gundeck/src/Gundeck/Monad.hs +++ b/services/gundeck/src/Gundeck/Monad.hs @@ -31,7 +31,6 @@ module Gundeck.Monad Gundeck, runDirect, runGundeck, - fromJsonBody, posixTime, -- * Select which redis to target @@ -44,11 +43,9 @@ import Bilge hiding (Request, header, options, statusCode) import Bilge.RPC import Cassandra import Control.Concurrent.Async (AsyncCancelled) -import Control.Error import Control.Exception (throwIO) import Control.Lens (view, (.~), (^.)) import Control.Monad.Catch hiding (tryJust) -import Data.Aeson (FromJSON) import Data.Misc (Milliseconds (..)) import Data.UUID as UUID import Data.UUID.V4 as UUID @@ -56,9 +53,7 @@ import Database.Redis qualified as Redis import Gundeck.Env import Gundeck.Redis qualified as Redis import Imports -import Network.HTTP.Types import Network.Wai -import Network.Wai.Utilities import Prometheus import System.Logger qualified as Log import System.Logger qualified as Logger @@ -201,10 +196,6 @@ lookupReqId l r = case lookup requestIdName (requestHeaders r) of ~~ msg (val "generated a new request id for local request") pure localRid -fromJsonBody :: (FromJSON a) => JsonRequest a -> Gundeck a -fromJsonBody r = exceptT (throwM . mkError status400 "bad-request") pure (parseBody r) -{-# INLINE fromJsonBody #-} - posixTime :: Gundeck Milliseconds posixTime = view time >>= liftIO {-# INLINE posixTime #-} diff --git a/tools/stern/default.nix b/tools/stern/default.nix index 81032346144..18246b4fc52 100644 --- a/tools/stern/default.nix +++ b/tools/stern/default.nix @@ -42,7 +42,6 @@ , tasty-ant-xml , tasty-hunit , text -, time , tinylog , transformers , types-common @@ -84,7 +83,6 @@ mkDerivation { servant-swagger-ui split text - time tinylog transformers types-common diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index d226f49f252..a5e17507ff2 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -67,7 +67,6 @@ module Stern.Intra getOAuthClient, updateOAuthClient, deleteOAuthClient, - getActivityTimestamp, ) where @@ -94,7 +93,6 @@ import Data.Text.Encoding import Data.Text.Encoding.Error import Data.Text.Lazy as LT (pack) import Data.Text.Lazy.Encoding qualified as TL -import Data.Time.Clock import Imports import Network.HTTP.Types (urlEncode) import Network.HTTP.Types.Method @@ -1040,17 +1038,3 @@ deleteOAuthClient cid = do . expect2xx ) parseResponse (mkError status502 "bad-upstream") r - -getActivityTimestamp :: UserId -> Handler (Maybe UTCTime) -getActivityTimestamp uid = do - b <- asks (.brig) - r <- - catchRpcErrors $ - rpc' - "brig" - b - ( method GET - . Bilge.paths ["i", "users", toByteString' uid, "activity"] - . expect2xx - ) - parseResponse (mkError status502 "bad-upstream") r diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index 36b5a86ca65..b7e04c9de2b 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -96,7 +96,6 @@ library , servant-swagger-ui , split >=0.2 , text >=1.1 - , time , tinylog >=0.10 , transformers >=0.3 , types-common >=0.4.13 diff --git a/weeder.toml b/weeder.toml index 66ab0310a78..64669d11e89 100644 --- a/weeder.toml +++ b/weeder.toml @@ -19,6 +19,7 @@ roots = [ # may of the entries here are about general-purpose module "^API.Search._testOrderName", "^API.Team.Util.*$", # FUTUREWORK: Consider whether unused utility functions should be kept. "^Bilge.*$", + "^Cannon.run$", "^Cassandra.Helpers.toOptionFieldName", "^Cassandra.QQ.sql$", "^Data.ETag._OpaqueDigest", @@ -35,11 +36,14 @@ roots = [ # may of the entries here are about general-purpose module "^Data.Range.rcons", "^Data.Range.rinc", "^Data.Range.rsingleton", + "^Data.Schema.fieldWithDocModifierF$", "^Data.ZAuth.Validation.*$", "^Galley.Cassandra.FeatureTH.generateSOPInstances$", "^Galley.Cassandra.FeatureTH.generateTupleP$", "^Galley.Types.Teams.canSeePermsOf", # TODO: figure out why weeder is confused by let bindings with curried infix notation - "^Galley.Types.UserList.ulDiff", + "^Galley.Types.UserList.ulDiff$", + "^Gundeck.Monad.runGundeck$", + "^Gundeck.run$", "^HTTP2.Client.Manager.*$", "^Imports.getChar", "^Imports.getContents", @@ -52,6 +56,8 @@ roots = [ # may of the entries here are about general-purpose module "^Main.debugMainExport", # move-team "^Main.debugMainImport", # move-team "^Main.main$", + "^Network.Wai.Utilities.Request.parseBody$", + "^Network.Wai.Utilities.Server.route$", "^Network.Wai.Utilities.ZAuth.*$", "^Notifications.*$", # new integration tests "^ParseSchema._printAllTables", @@ -112,12 +118,16 @@ roots = [ # may of the entries here are about general-purpose module "^Proto.Otr_Fields.vec'userIds", "^Proto.TeamEvents_Fields.currency", "^Proto.TeamEvents_Fields.vec'billingUser", + "^Proxy.run$", + "^Run.main$", "^Run.main$", "^Spar.Sem.AReqIDStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? "^Spar.Sem.AssIDStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? "^Spar.Sem.ScimTokenStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? "^Spar.Sem.VerdictFormatStore.Mem.*$", # FUTUREWORK: @fisx can we delete this? "^Spec.main$", + "^Stern.App.runHandler$", + "^System.Logger.Extended.runWithLogger$", "^Test.Cargohold.API.Util.shouldMatchALittle", "^Test.Cargohold.API.Util.shouldMatchLeniently", "^Test.Cargohold.API.Util.shouldMatchSloppily", @@ -126,8 +136,6 @@ roots = [ # may of the entries here are about general-purpose module "^Test.Data.Schema.userSchemaWithDefaultName'", "^Test.Federator.JSON.deriveJSONOptions", # This is used inside an instance derivation via TH "^Test.Wire.API.Golden.Run.main$", - "^Run.main$", - "^Test.Wire.API.Password.testHashPasswordScrypt", # FUTUREWORK: reworking scrypt/argon2id is planned for next sprint "^TestSetup.runFederationClient", "^TestSetup.viewCargohold", "^Testlib.App.*$", # FUTUREWORK: See how we can have weeder parse operators in the config file. @@ -148,6 +156,7 @@ roots = [ # may of the entries here are about general-purpose module "^Testlib.Run.mainI$", "^Testlib.RunServices.main$", "^ThreadBudget.extractLogHistory", + "^Util.SuffixNamer.*", "^Util.assertOne", "^Util.randomActivationCode", "^Util.zClient", @@ -174,8 +183,11 @@ roots = [ # may of the entries here are about general-purpose module "^Web.Scim.Test.Util.scim", "^Web.Scim.Test.Util.shouldEventuallyRespondWith", "^Wire.API.MLS.Serialisation.traceMLS", # Debug + "^Wire.API.Password.hashPasswordScrypt$", # Used in testing + "^Wire.API.Password.mkSafePasswordScrypt$", # Used in testing "^Wire.Sem.Concurrency.IO.performConcurrency", - "^Wire.Sem.Logger.fatal" + "^Wire.Sem.Logger.fatal", + "^Wire.Sem.Metrics.incGauge$" # Used in wai-utilities ] type-class-roots = true # `root-instances` is more precise, but requires more config maintenance. From 99003f669c81db0c04a01232a7835988fac18af4 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 24 Oct 2024 18:50:20 +0200 Subject: [PATCH 124/136] 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 125/136] 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 126/136] 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 127/136] 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 128/136] [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 129/136] 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 130/136] 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 131/136] [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 132/136] 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 133/136] 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 134/136] 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 135/136] [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 136/136] 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.