From 3c28b98dc949a3d77d3ce4bf3de42d56bf7fd127 Mon Sep 17 00:00:00 2001
From: Akshay Mankar
Date: Tue, 13 Jan 2026 15:41:55 +0100
Subject: [PATCH 01/26] flake.nix: Fix URL for tom-bombadil (#4950)
---
flake.lock | 15 +++++++++------
flake.nix | 2 +-
2 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/flake.lock b/flake.lock
index 68686e15d1..e6994cbce0 100644
--- a/flake.lock
+++ b/flake.lock
@@ -354,14 +354,17 @@
]
},
"locked": {
- "lastModified": 1767870783,
- "narHash": "sha256-0QStp+uH05bnGltPnOJM2FdeTJdgVIWkVM5wSFYVceM=",
- "path": "/home/axeman/workspace/tom-bombadil",
- "type": "path"
+ "lastModified": 1739181719,
+ "narHash": "sha256-W4g0e9u5CagGY8qqsHIZbP28iVkvvmz+hCkNz3b5GkE=",
+ "owner": "wireapp",
+ "repo": "tom-bombadil",
+ "rev": "53842ffb11e8d357b7d7bdf21b800c71a414ed06",
+ "type": "github"
},
"original": {
- "path": "/home/axeman/workspace/tom-bombadil",
- "type": "path"
+ "owner": "wireapp",
+ "repo": "tom-bombadil",
+ "type": "github"
}
},
"wai-predicates": {
diff --git a/flake.nix b/flake.nix
index 698c5b23d2..0be37264b9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -7,7 +7,7 @@
nixpkgs_24_11.url = "github:nixos/nixpkgs?ref=nixos-24.11";
flake-utils.url = "github:numtide/flake-utils";
tom-bombadil = {
- url = "path:/home/axeman/workspace/tom-bombadil";
+ url = "github:wireapp/tom-bombadil";
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
};
From 3a9ffecd6a3ea7ba66c4967af7cfa6433dddeff1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Markus=20L=C3=A4ll?=
Date: Tue, 13 Jan 2026 17:33:34 +0200
Subject: [PATCH 02/26] WPB-21366: Add user type field to `GET
/search/contacts` (#4913)
---
.../0-release-notes/add-type-field-to-contact | 3 +
.../1-api-changes/add-get-app-endpoint | 1 -
.../1-api-changes/add-type-field-to-contact | 1 +
integration/test/Test/Apps.hs | 20 ++--
libs/wire-api/src/Wire/API/User.hs | 55 +++++-----
libs/wire-api/src/Wire/API/User/Search.hs | 11 +-
.../Wire/API/Golden/Generated/Contact_user.hs | 61 +++++++----
.../Generated/SearchResult_20Contact_user.hs | 103 ++++++++++++------
.../Test/Wire/API/Golden/Manual/Contact.hs | 7 +-
.../test/golden/testObject_Contact_1.json | 3 +-
.../test/golden/testObject_Contact_2.json | 3 +-
.../golden/testObject_Contact_user_1.json | 3 +-
.../golden/testObject_Contact_user_10.json | 3 +-
.../golden/testObject_Contact_user_11.json | 3 +-
.../golden/testObject_Contact_user_12.json | 3 +-
.../golden/testObject_Contact_user_13.json | 3 +-
.../golden/testObject_Contact_user_14.json | 3 +-
.../golden/testObject_Contact_user_15.json | 3 +-
.../golden/testObject_Contact_user_16.json | 3 +-
.../golden/testObject_Contact_user_17.json | 3 +-
.../golden/testObject_Contact_user_18.json | 3 +-
.../golden/testObject_Contact_user_19.json | 3 +-
.../golden/testObject_Contact_user_2.json | 3 +-
.../golden/testObject_Contact_user_20.json | 3 +-
.../golden/testObject_Contact_user_3.json | 3 +-
.../golden/testObject_Contact_user_4.json | 3 +-
.../golden/testObject_Contact_user_5.json | 3 +-
.../golden/testObject_Contact_user_6.json | 3 +-
.../golden/testObject_Contact_user_7.json | 3 +-
.../golden/testObject_Contact_user_8.json | 3 +-
.../golden/testObject_Contact_user_9.json | 3 +-
.../testObject_SearchResultContact_1.json | 6 +-
.../testObject_SearchResultContact_2.json | 6 +-
...Object_SearchResult_20Contact_user_11.json | 6 +-
...Object_SearchResult_20Contact_user_13.json | 12 +-
...Object_SearchResult_20Contact_user_14.json | 6 +-
...Object_SearchResult_20Contact_user_19.json | 6 +-
...Object_SearchResult_20Contact_user_20.json | 39 ++++---
...tObject_SearchResult_20Contact_user_3.json | 3 +-
...tObject_SearchResult_20Contact_user_4.json | 18 ++-
...tObject_SearchResult_20Contact_user_5.json | 3 +-
...tObject_SearchResult_20Contact_user_7.json | 6 +-
...tObject_SearchResult_20Contact_user_8.json | 3 +-
libs/wire-api/test/unit/Test/Wire/API/User.hs | 12 +-
.../IndexedUserStore/Bulk/ElasticSearch.hs | 36 +++++-
.../src/Wire/UserSearch/Types.hs | 25 +++++
.../src/Wire/UserStore/IndexUser.hs | 14 ++-
.../src/Wire/UserSubsystem/Interpreter.hs | 85 +++++++++------
.../InterpreterSpec.hs | 5 +-
.../test/unit/Wire/MiniBackend.hs | 9 +-
.../Wire/MockInterpreters/IndexedUserStore.hs | 14 ---
.../unit/Wire/MockInterpreters/UserStore.hs | 11 +-
.../Wire/MockInterpreters/UserSubsystem.hs | 2 +-
.../test/unit/Wire/UserSearch/TypesSpec.hs | 3 +-
.../Wire/UserSubsystem/InterpreterSpec.hs | 23 +++-
postgres-schema.sql | 4 +-
services/brig/src/Brig/Index/Eval.hs | 5 +-
services/brig/src/Brig/Provider/API.hs | 2 +-
services/brig/src/Brig/User/API/Handle.hs | 3 +-
services/brig/src/Brig/User/Search/Index.hs | 8 ++
.../brig/src/Brig/User/Search/SearchIndex.hs | 20 ++--
services/brig/test/integration/API/Search.hs | 2 +-
.../integration/Test/Federator/IngressSpec.hs | 2 +-
.../integration/Test/Federator/InwardSpec.hs | 2 +-
64 files changed, 483 insertions(+), 246 deletions(-)
create mode 100644 changelog.d/0-release-notes/add-type-field-to-contact
delete mode 100644 changelog.d/1-api-changes/add-get-app-endpoint
create mode 100644 changelog.d/1-api-changes/add-type-field-to-contact
diff --git a/changelog.d/0-release-notes/add-type-field-to-contact b/changelog.d/0-release-notes/add-type-field-to-contact
new file mode 100644
index 0000000000..10d7af72c0
--- /dev/null
+++ b/changelog.d/0-release-notes/add-type-field-to-contact
@@ -0,0 +1,3 @@
+Since the index mapping has been updated, the elastic search index
+needs to be refilled from Cassandra, see
+https://docs.wire.com/latest/developer/reference/elastic-search.html?h=index#refill-es-documents-from-cassandra
\ No newline at end of file
diff --git a/changelog.d/1-api-changes/add-get-app-endpoint b/changelog.d/1-api-changes/add-get-app-endpoint
deleted file mode 100644
index e433e9cc22..0000000000
--- a/changelog.d/1-api-changes/add-get-app-endpoint
+++ /dev/null
@@ -1 +0,0 @@
-Add "get app" endpoint to Brig (`GET /teams/:tid/apps/:id`)
\ No newline at end of file
diff --git a/changelog.d/1-api-changes/add-type-field-to-contact b/changelog.d/1-api-changes/add-type-field-to-contact
new file mode 100644
index 0000000000..cb58adb825
--- /dev/null
+++ b/changelog.d/1-api-changes/add-type-field-to-contact
@@ -0,0 +1 @@
+Add `type` field to search results received from `GET /search/contacts`
\ No newline at end of file
diff --git a/integration/test/Test/Apps.hs b/integration/test/Test/Apps.hs
index 4af7b903b3..14991a5375 100644
--- a/integration/test/Test/Apps.hs
+++ b/integration/test/Test/Apps.hs
@@ -73,7 +73,7 @@ testCreateApp = do
(resp.json %. "category") `shouldMatch` "ai"
-- A teamless user can't get the app
- outsideUser <- randomUser OwnDomain def
+ outsideUser <- randomUser domain def
bindResponse (getApp outsideUser tid appId) $ \resp -> do
resp.status `shouldMatchInt` 403
resp.json %. "label" `shouldMatch` "app-no-permission"
@@ -88,13 +88,19 @@ testCreateApp = do
void $ bindResponse (createApp owner tid new {category = "notinenum"}) $ \resp -> do
resp.status `shouldMatchInt` 400
+ let foundUserType exactMatchTerm aType =
+ searchContacts owner exactMatchTerm OwnDomain `bindResponse` \resp -> do
+ resp.status `shouldMatchInt` 200
+ foundDoc <- resp.json %. "documents" >>= asList >>= assertOne
+ foundDoc %. "type" `shouldMatch` aType
+
-- App's user is findable from /search/contacts
- BrigI.refreshIndex OwnDomain
- searchContacts owner new.name OwnDomain `bindResponse` \resp -> do
- resp.status `shouldMatchInt` 200
- docs <- resp.json %. "documents" >>= asList
- foundUids <- for docs objId
- foundUids `shouldMatch` [appId]
+ BrigI.refreshIndex domain
+ foundUserType new.name "app"
+
+ -- Regular members still have the type "regular"
+ memberName <- regularMember %. "name" & asString
+ foundUserType memberName "regular"
testRefreshAppCookie :: (HasCallStack) => App ()
testRefreshAppCookie = do
diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs
index 138ded4356..defa322a59 100644
--- a/libs/wire-api/src/Wire/API/User.hs
+++ b/libs/wire-api/src/Wire/API/User.hs
@@ -472,6 +472,7 @@ instance (1 <= max) => ToJSON (LimitedQualifiedUserIdList max) where
data UserType = UserTypeRegular | UserTypeApp | UserTypeBot
deriving (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform UserType)
+ deriving (A.FromJSON, A.ToJSON) via (Schema UserType)
instance Default UserType where
def = UserTypeRegular
@@ -714,34 +715,32 @@ instance FromJSON (EmailVisibility ()) where
"visible_to_self" -> pure EmailVisibleToSelf
_ -> fail "unexpected value for EmailVisibility settings"
-mkUserProfileWithEmail :: Maybe EmailAddress -> User -> UserLegalHoldStatus -> UserProfile
-mkUserProfileWithEmail memail u legalHoldStatus =
- let ty = case userService u of
- Nothing -> UserTypeRegular
- Just _ -> UserTypeBot
- in -- 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.
- UserProfile
- { profileQualifiedId = userQualifiedId u,
- profileHandle = userHandle u,
- profileName = userDisplayName u,
- profileTextStatus = userTextStatus u,
- profilePict = userPict u,
- profileAssets = userAssets u,
- profileAccentId = userAccentId u,
- profileService = userService u,
- profileDeleted = userDeleted u,
- profileExpire = userExpire u,
- profileTeam = userTeam u,
- profileEmail = memail,
- profileLegalholdStatus = legalHoldStatus,
- profileSupportedProtocols = userSupportedProtocols u,
- profileType = ty,
- profileSearchable = userSearchable u
- }
+-- | Create profile, overwriting the email field. Called `mkUserProfile`.
+mkUserProfileWithEmail :: Maybe EmailAddress -> UserType -> User -> UserLegalHoldStatus -> UserProfile
+mkUserProfileWithEmail memail userType 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.
+ UserProfile
+ { profileQualifiedId = userQualifiedId u,
+ profileHandle = userHandle u,
+ profileName = userDisplayName u,
+ profileTextStatus = userTextStatus u,
+ profilePict = userPict u,
+ profileAssets = userAssets u,
+ profileAccentId = userAccentId u,
+ profileService = userService u,
+ profileDeleted = userDeleted u,
+ profileExpire = userExpire u,
+ profileTeam = userTeam u,
+ profileEmail = memail,
+ profileLegalholdStatus = legalHoldStatus,
+ profileSupportedProtocols = userSupportedProtocols u,
+ profileType = userType,
+ profileSearchable = userSearchable u
+ }
-mkUserProfile :: EmailVisibilityConfigWithViewer -> User -> UserLegalHoldStatus -> UserProfile
-mkUserProfile emailVisibilityConfigAndViewer u legalHoldStatus =
+mkUserProfile :: EmailVisibilityConfigWithViewer -> UserType -> User -> UserLegalHoldStatus -> UserProfile
+mkUserProfile emailVisibilityConfigAndViewer userType u legalHoldStatus =
let isEmailVisible = case emailVisibilityConfigAndViewer of
EmailVisibleToSelf -> False
EmailVisibleIfOnTeam -> isJust (userTeam u)
@@ -749,7 +748,7 @@ mkUserProfile emailVisibilityConfigAndViewer u legalHoldStatus =
EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership)) ->
Just viewerTeamId == userTeam u
&& TeamMember.hasPermission viewerMembership TeamMember.ViewSameTeamEmails
- in mkUserProfileWithEmail (if isEmailVisible then userEmail u else Nothing) u legalHoldStatus
+ in mkUserProfileWithEmail (if isEmailVisible then userEmail u else Nothing) userType u legalHoldStatus
--------------------------------------------------------------------------------
-- NewUser
diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs
index bddf084994..96e01c24fe 100644
--- a/libs/wire-api/src/Wire/API/User/Search.hs
+++ b/libs/wire-api/src/Wire/API/User/Search.hs
@@ -42,8 +42,7 @@ import Data.Aeson hiding (object, (.=))
import Data.Aeson qualified as Aeson
import Data.Attoparsec.ByteString.Char8 (string)
import Data.ByteString.Char8 qualified as C8
-import Data.ByteString.Conversion
-import Data.ByteString.Conversion qualified as BS
+import Data.ByteString.Conversion as BS
import Data.Id (TeamId, UserGroupId, UserId)
import Data.Json.Util (UTCTimeMillis)
import Data.OpenApi (ToParamSchema (..))
@@ -59,7 +58,7 @@ import Imports
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 (ManagedBy, UserType)
import Wire.API.User.Identity (EmailAddress)
import Wire.Arbitrary (Arbitrary, GenericUniform (..))
@@ -138,14 +137,15 @@ deriving via (Schema (SearchResult TeamContact)) instance S.ToSchema (SearchResu
--------------------------------------------------------------------------------
-- Contact
--- | Returned by 'searchIndex' under @/contacts/search@.
+-- | Returned by 'searchIndex' under @/search/contacts@.
-- This is a subset of 'User' and json instances should reflect that.
data Contact = Contact
{ contactQualifiedId :: Qualified UserId,
contactName :: Text,
contactColorId :: Maybe Int,
contactHandle :: Maybe Text,
- contactTeam :: Maybe TeamId
+ contactTeam :: Maybe TeamId,
+ contactType :: UserType
}
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform Contact)
@@ -161,6 +161,7 @@ instance ToSchema Contact where
<*> contactColorId .= optField "accent_id" (maybeWithDefault Aeson.Null schema)
<*> contactHandle .= optField "handle" (maybeWithDefault Aeson.Null schema)
<*> contactTeam .= optField "team" (maybeWithDefault Aeson.Null schema)
+ <*> contactType .= field "type" schema
--------------------------------------------------------------------------------
-- TeamContact
diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Contact_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Contact_user.hs
index b3f7c67e29..88b04796f6 100644
--- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Contact_user.hs
+++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Contact_user.hs
@@ -22,6 +22,7 @@ import Data.Id (Id (Id))
import Data.Qualified (Qualified (Qualified, qDomain, qUnqualified))
import Data.UUID qualified as UUID (fromString)
import Imports (Maybe (Just, Nothing), fromJust)
+import Wire.API.User
import Wire.API.User.Search (Contact (..))
testObject_Contact_user_1 :: Contact
@@ -35,7 +36,8 @@ testObject_Contact_user_1 =
contactName = "",
contactColorId = Just 6,
contactHandle = Just "\1089530\NUL|\SO",
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
}
testObject_Contact_user_2 :: Contact
@@ -49,7 +51,8 @@ testObject_Contact_user_2 =
contactName = "\SYND",
contactColorId = Just (-5),
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000002-0000-0008-0000-000400000002")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000002-0000-0008-0000-000400000002"))),
+ contactType = UserTypeApp
}
testObject_Contact_user_3 :: Contact
@@ -63,7 +66,8 @@ testObject_Contact_user_3 =
contactName = "S\1037187D\GS",
contactColorId = Just (-4),
contactHandle = Just "\175177~\35955c",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0005-0000-000700000008")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0005-0000-000700000008"))),
+ contactType = UserTypeBot
}
testObject_Contact_user_4 :: Contact
@@ -77,7 +81,8 @@ testObject_Contact_user_4 =
contactName = "@=\ETX",
contactColorId = Nothing,
contactHandle = Just "6",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000500000004")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000500000004"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_5 :: Contact
@@ -91,7 +96,8 @@ testObject_Contact_user_5 =
contactName = "5m~\DC4`",
contactColorId = Nothing,
contactHandle = Nothing,
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
}
testObject_Contact_user_6 :: Contact
@@ -105,7 +111,8 @@ testObject_Contact_user_6 =
contactName = "Cst\995547U",
contactColorId = Nothing,
contactHandle = Just "qI",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0004-0000-000600000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0004-0000-000600000000"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_7 :: Contact
@@ -119,7 +126,8 @@ testObject_Contact_user_7 =
contactName = "\b74\ENQ",
contactColorId = Just 5,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000008-0000-0001-0000-000400000008")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000008-0000-0001-0000-000400000008"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_8 :: Contact
@@ -133,7 +141,8 @@ testObject_Contact_user_8 =
contactName = "w\1050194\993461#\\",
contactColorId = Just (-2),
contactHandle = Nothing,
- contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0007-0000-000500000002")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0007-0000-000500000002"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_9 :: Contact
@@ -147,7 +156,8 @@ testObject_Contact_user_9 =
contactName = ",\1041199 \v\1077257",
contactColorId = Just 5,
contactHandle = Nothing,
- contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0002-0000-000500000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0002-0000-000500000000"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_10 :: Contact
@@ -161,7 +171,8 @@ testObject_Contact_user_10 =
contactName = "(\1103086\1105553H/",
contactColorId = Just 0,
contactHandle = Nothing,
- contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0006-0000-000700000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0006-0000-000700000000"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_11 :: Contact
@@ -175,7 +186,8 @@ testObject_Contact_user_11 =
contactName = "+\DC4\1063683<",
contactColorId = Just 6,
contactHandle = Nothing,
- contactTeam = Just (Id (fromJust (UUID.fromString "00000007-0000-0008-0000-000600000004")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000007-0000-0008-0000-000600000004"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_12 :: Contact
@@ -189,7 +201,8 @@ testObject_Contact_user_12 =
contactName = "l\DC1\ETB`\ETX",
contactColorId = Just (-4),
contactHandle = Just "",
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
}
testObject_Contact_user_13 :: Contact
@@ -203,7 +216,8 @@ testObject_Contact_user_13 =
contactName = "\SYN\1030541\v8z",
contactColorId = Just (-3),
contactHandle = Just "E\EM\US[58",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0003-0000-000000000005")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0003-0000-000000000005"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_14 :: Contact
@@ -217,7 +231,8 @@ testObject_Contact_user_14 =
contactName = "7",
contactColorId = Just (-2),
contactHandle = Just "h\CAN",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0008-0000-000700000008")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000005-0000-0008-0000-000700000008"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_15 :: Contact
@@ -231,7 +246,8 @@ testObject_Contact_user_15 =
contactName = "U6\ESC*\SO",
contactColorId = Nothing,
contactHandle = Nothing,
- contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0006-0000-000800000006")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0006-0000-000800000006"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_16 :: Contact
@@ -245,7 +261,8 @@ testObject_Contact_user_16 =
contactName = "l",
contactColorId = Nothing,
contactHandle = Nothing,
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0006-0000-000200000007")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0006-0000-000200000007"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_17 :: Contact
@@ -259,7 +276,8 @@ testObject_Contact_user_17 =
contactName = "fI\8868\&3z",
contactColorId = Nothing,
contactHandle = Just "3",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000004-0000-0007-0000-000000000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000004-0000-0007-0000-000000000001"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_18 :: Contact
@@ -273,7 +291,8 @@ testObject_Contact_user_18 =
contactName = "\"jC\74801\144577\DC2",
contactColorId = Nothing,
contactHandle = Nothing,
- contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000000000007")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0002-0000-000000000007"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_19 :: Contact
@@ -287,7 +306,8 @@ testObject_Contact_user_19 =
contactName = "I",
contactColorId = Just (-1),
contactHandle = Just "\"7\ACK!",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0004-0000-000000000003")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000006-0000-0004-0000-000000000003"))),
+ contactType = UserTypeRegular
}
testObject_Contact_user_20 :: Contact
@@ -301,5 +321,6 @@ testObject_Contact_user_20 =
contactName = "|K\n\n\t",
contactColorId = Nothing,
contactHandle = Nothing,
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
}
diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SearchResult_20Contact_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SearchResult_20Contact_user.hs
index bb79681738..1b9dc63b00 100644
--- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SearchResult_20Contact_user.hs
+++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SearchResult_20Contact_user.hs
@@ -24,6 +24,7 @@ import Data.Id (Id (Id))
import Data.Qualified (Qualified (Qualified, qDomain, qUnqualified))
import Data.UUID qualified as UUID (fromString)
import Imports (Bool (..), Maybe (Just, Nothing), fromJust)
+import Wire.API.User (UserType (UserTypeRegular))
import Wire.API.User.Search (Contact (..), FederatedUserSearchPolicy (ExactHandleSearch, FullSearch), PagingState (..), SearchResult (..))
testObject_SearchResult_20Contact_user_1 :: SearchResult Contact
@@ -66,7 +67,8 @@ testObject_SearchResult_20Contact_user_3 =
contactName = "",
contactColorId = Nothing,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000"))),
+ contactType = UserTypeRegular
}
],
searchPolicy = FullSearch,
@@ -90,7 +92,8 @@ testObject_SearchResult_20Contact_user_4 =
contactName = "",
contactColorId = Nothing,
contactHandle = Nothing,
- contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -101,7 +104,8 @@ testObject_SearchResult_20Contact_user_4 =
contactName = "",
contactColorId = Nothing,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -112,7 +116,8 @@ testObject_SearchResult_20Contact_user_4 =
contactName = "",
contactColorId = Just 0,
contactHandle = Nothing,
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -123,7 +128,8 @@ testObject_SearchResult_20Contact_user_4 =
contactName = "",
contactColorId = Nothing,
contactHandle = Just "",
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -134,7 +140,8 @@ testObject_SearchResult_20Contact_user_4 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -145,7 +152,8 @@ testObject_SearchResult_20Contact_user_4 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))),
+ contactType = UserTypeRegular
}
],
searchPolicy = FullSearch,
@@ -169,7 +177,8 @@ testObject_SearchResult_20Contact_user_5 =
contactName = "z",
contactColorId = Just 1,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000100000001"))),
+ contactType = UserTypeRegular
}
],
searchPolicy = FullSearch,
@@ -205,7 +214,8 @@ testObject_SearchResult_20Contact_user_7 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000001"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -216,7 +226,8 @@ testObject_SearchResult_20Contact_user_7 =
contactName = "",
contactColorId = Just 0,
contactHandle = Nothing,
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))),
+ contactType = UserTypeRegular
}
],
searchPolicy = FullSearch,
@@ -240,7 +251,8 @@ testObject_SearchResult_20Contact_user_8 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))),
+ contactType = UserTypeRegular
}
],
searchPolicy = FullSearch,
@@ -288,7 +300,8 @@ testObject_SearchResult_20Contact_user_11 =
contactName = "",
contactColorId = Just 0,
contactHandle = Nothing,
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -299,7 +312,8 @@ testObject_SearchResult_20Contact_user_11 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
}
],
searchPolicy = ExactHandleSearch,
@@ -335,7 +349,8 @@ testObject_SearchResult_20Contact_user_13 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000000"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -346,7 +361,8 @@ testObject_SearchResult_20Contact_user_13 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -357,7 +373,8 @@ testObject_SearchResult_20Contact_user_13 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -368,7 +385,8 @@ testObject_SearchResult_20Contact_user_13 =
contactName = "",
contactColorId = Nothing,
contactHandle = Just "",
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
}
],
searchPolicy = ExactHandleSearch,
@@ -392,7 +410,8 @@ testObject_SearchResult_20Contact_user_14 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000000"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -403,7 +422,8 @@ testObject_SearchResult_20Contact_user_14 =
contactName = "",
contactColorId = Nothing,
contactHandle = Just "",
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
}
],
searchPolicy = ExactHandleSearch,
@@ -475,7 +495,8 @@ testObject_SearchResult_20Contact_user_19 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -486,7 +507,8 @@ testObject_SearchResult_20Contact_user_19 =
contactName = "",
contactColorId = Nothing,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))),
+ contactType = UserTypeRegular
}
],
searchPolicy = ExactHandleSearch,
@@ -510,7 +532,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Nothing,
contactHandle = Just "",
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -521,7 +544,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -532,7 +556,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000001"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -543,7 +568,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Nothing,
contactHandle = Just "",
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -554,7 +580,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Nothing,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000000"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -565,7 +592,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000001-0000-0001-0000-000000000001"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -576,7 +604,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000000000001"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -587,7 +616,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -598,7 +628,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Nothing,
contactHandle = Just "",
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -609,7 +640,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000001"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -620,7 +652,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -631,7 +664,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Just 0,
contactHandle = Just "",
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000000000000"))),
+ contactType = UserTypeRegular
},
Contact
{ contactQualifiedId =
@@ -642,7 +676,8 @@ testObject_SearchResult_20Contact_user_20 =
contactName = "",
contactColorId = Just 0,
contactHandle = Nothing,
- contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")))
+ contactTeam = Just (Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000"))),
+ contactType = UserTypeRegular
}
],
searchPolicy = ExactHandleSearch,
diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Contact.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Contact.hs
index 513f6d30af..3b2bc6fed9 100644
--- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Contact.hs
+++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Contact.hs
@@ -22,6 +22,7 @@ import Data.Id (Id (Id))
import Data.Qualified (Qualified (Qualified))
import Data.UUID qualified as UUID
import Imports
+import Wire.API.User (UserType (UserTypeRegular))
import Wire.API.User.Search (Contact (..))
testObject_Contact_1 :: Contact
@@ -31,7 +32,8 @@ testObject_Contact_1 =
contactName = "Foobar",
contactColorId = Just 1,
contactHandle = Just "foobar1",
- contactTeam = Just $ Id (fromJust (UUID.fromString "00000018-0000-0020-0000-000e00000002"))
+ contactTeam = Just $ Id (fromJust (UUID.fromString "00000018-0000-0020-0000-000e00000002")),
+ contactType = UserTypeRegular
}
testObject_Contact_2 :: Contact
@@ -41,5 +43,6 @@ testObject_Contact_2 =
contactName = "Foobar2",
contactColorId = Nothing,
contactHandle = Nothing,
- contactTeam = Nothing
+ contactTeam = Nothing,
+ contactType = UserTypeRegular
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_1.json b/libs/wire-api/test/golden/testObject_Contact_1.json
index fb1bdac6dd..01906d05a2 100644
--- a/libs/wire-api/test/golden/testObject_Contact_1.json
+++ b/libs/wire-api/test/golden/testObject_Contact_1.json
@@ -7,5 +7,6 @@
"domain": "example.com",
"id": "00000018-0000-0020-0000-000e00000002"
},
- "team": "00000018-0000-0020-0000-000e00000002"
+ "team": "00000018-0000-0020-0000-000e00000002",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_2.json b/libs/wire-api/test/golden/testObject_Contact_2.json
index aca2362249..a7171d11e6 100644
--- a/libs/wire-api/test/golden/testObject_Contact_2.json
+++ b/libs/wire-api/test/golden/testObject_Contact_2.json
@@ -7,5 +7,6 @@
"domain": "another.example.com",
"id": "00000018-0000-0020-0000-000e00000003"
},
- "team": null
+ "team": null,
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_1.json b/libs/wire-api/test/golden/testObject_Contact_user_1.json
index 825287dc78..44a3c28869 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_1.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_1.json
@@ -7,5 +7,6 @@
"domain": "j00.8y.yr3isy2m",
"id": "00000007-0000-0003-0000-000300000005"
},
- "team": null
+ "team": null,
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_10.json b/libs/wire-api/test/golden/testObject_Contact_user_10.json
index 7618387345..57c4e71fa2 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_10.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_10.json
@@ -7,5 +7,6 @@
"domain": "avs-82k0.quv1k-5",
"id": "00000000-0000-0000-0000-000800000007"
},
- "team": "00000005-0000-0006-0000-000700000000"
+ "team": "00000005-0000-0006-0000-000700000000",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_11.json b/libs/wire-api/test/golden/testObject_Contact_user_11.json
index f6831f331b..dfd13cc9a2 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_11.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_11.json
@@ -7,5 +7,6 @@
"domain": "156y.t.qxp-y26x",
"id": "00000002-0000-0005-0000-000700000004"
},
- "team": "00000007-0000-0008-0000-000600000004"
+ "team": "00000007-0000-0008-0000-000600000004",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_12.json b/libs/wire-api/test/golden/testObject_Contact_user_12.json
index 38ca139c60..0c504b2229 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_12.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_12.json
@@ -7,5 +7,6 @@
"domain": "d2wnzbn.8.k2d4-103",
"id": "00000004-0000-0002-0000-000300000003"
},
- "team": null
+ "team": null,
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_13.json b/libs/wire-api/test/golden/testObject_Contact_user_13.json
index 3ee4544fc4..81835b8a16 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_13.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_13.json
@@ -7,5 +7,6 @@
"domain": "902cigj.v2t56",
"id": "00000002-0000-0006-0000-000800000006"
},
- "team": "00000001-0000-0003-0000-000000000005"
+ "team": "00000001-0000-0003-0000-000000000005",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_14.json b/libs/wire-api/test/golden/testObject_Contact_user_14.json
index 876b00ee25..14ef9ebd56 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_14.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_14.json
@@ -7,5 +7,6 @@
"domain": "6z.ml.80ps6j5r.l",
"id": "00000000-0000-0003-0000-000300000006"
},
- "team": "00000005-0000-0008-0000-000700000008"
+ "team": "00000005-0000-0008-0000-000700000008",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_15.json b/libs/wire-api/test/golden/testObject_Contact_user_15.json
index 5664eab786..619c62f22a 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_15.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_15.json
@@ -7,5 +7,6 @@
"domain": "739.e-h8g",
"id": "00000002-0000-0000-0000-000200000002"
},
- "team": "00000006-0000-0006-0000-000800000006"
+ "team": "00000006-0000-0006-0000-000800000006",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_16.json b/libs/wire-api/test/golden/testObject_Contact_user_16.json
index a04c2bf386..1ba59c042a 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_16.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_16.json
@@ -7,5 +7,6 @@
"domain": "t82.x5i8-i",
"id": "00000000-0000-0006-0000-000500000006"
},
- "team": "00000000-0000-0006-0000-000200000007"
+ "team": "00000000-0000-0006-0000-000200000007",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_17.json b/libs/wire-api/test/golden/testObject_Contact_user_17.json
index fd68c31c3a..54bb4c0fab 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_17.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_17.json
@@ -7,5 +7,6 @@
"domain": "o5b0hrjp3x0b96.v1gxp3",
"id": "00000003-0000-0008-0000-000700000002"
},
- "team": "00000004-0000-0007-0000-000000000001"
+ "team": "00000004-0000-0007-0000-000000000001",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_18.json b/libs/wire-api/test/golden/testObject_Contact_user_18.json
index b5f25e02fc..5c2357f91b 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_18.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_18.json
@@ -7,5 +7,6 @@
"domain": "72n2x7x0.ztb0s51",
"id": "00000004-0000-0006-0000-000800000006"
},
- "team": "00000001-0000-0002-0000-000000000007"
+ "team": "00000001-0000-0002-0000-000000000007",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_19.json b/libs/wire-api/test/golden/testObject_Contact_user_19.json
index 7cf17bc4f5..799bff0386 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_19.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_19.json
@@ -7,5 +7,6 @@
"domain": "h664l.dio6",
"id": "00000005-0000-0003-0000-000700000007"
},
- "team": "00000006-0000-0004-0000-000000000003"
+ "team": "00000006-0000-0004-0000-000000000003",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_2.json b/libs/wire-api/test/golden/testObject_Contact_user_2.json
index 4ad15e595a..523e300330 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_2.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_2.json
@@ -7,5 +7,6 @@
"domain": "z.l--66-i8g8a9",
"id": "00000006-0000-0004-0000-000100000007"
},
- "team": "00000002-0000-0008-0000-000400000002"
+ "team": "00000002-0000-0008-0000-000400000002",
+ "type": "app"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_20.json b/libs/wire-api/test/golden/testObject_Contact_user_20.json
index 079817785a..0021c4f0b6 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_20.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_20.json
@@ -7,5 +7,6 @@
"domain": "pam223.b6",
"id": "00000000-0000-0000-0000-000500000001"
},
- "team": null
+ "team": null,
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_3.json b/libs/wire-api/test/golden/testObject_Contact_user_3.json
index e8cb5a2eee..d5a5f47407 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_3.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_3.json
@@ -7,5 +7,6 @@
"domain": "h.y-2k71.rh",
"id": "00000005-0000-0003-0000-000700000003"
},
- "team": "00000006-0000-0005-0000-000700000008"
+ "team": "00000006-0000-0005-0000-000700000008",
+ "type": "bot"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_4.json b/libs/wire-api/test/golden/testObject_Contact_user_4.json
index 13b1b0c89a..c9f77f4eaa 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_4.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_4.json
@@ -7,5 +7,6 @@
"domain": "2347.cye2i7.sn.r2z83.d03",
"id": "00000003-0000-0002-0000-000000000004"
},
- "team": "00000000-0000-0000-0000-000500000004"
+ "team": "00000000-0000-0000-0000-000500000004",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_5.json b/libs/wire-api/test/golden/testObject_Contact_user_5.json
index 4442242a90..ac359467ef 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_5.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_5.json
@@ -7,5 +7,6 @@
"domain": "v0u29n3.er",
"id": "00000004-0000-0000-0000-000300000005"
},
- "team": null
+ "team": null,
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_6.json b/libs/wire-api/test/golden/testObject_Contact_user_6.json
index 9ca25db0d5..c5a1adbc52 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_6.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_6.json
@@ -7,5 +7,6 @@
"domain": "6k.p",
"id": "00000003-0000-0001-0000-000400000000"
},
- "team": "00000005-0000-0004-0000-000600000000"
+ "team": "00000005-0000-0004-0000-000600000000",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_7.json b/libs/wire-api/test/golden/testObject_Contact_user_7.json
index df0499cc7e..093a117d6f 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_7.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_7.json
@@ -7,5 +7,6 @@
"domain": "yr.e1-d",
"id": "00000001-0000-0002-0000-000800000008"
},
- "team": "00000008-0000-0001-0000-000400000008"
+ "team": "00000008-0000-0001-0000-000400000008",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_8.json b/libs/wire-api/test/golden/testObject_Contact_user_8.json
index 67ad84dfe7..be0997d3be 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_8.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_8.json
@@ -7,5 +7,6 @@
"domain": "51r9in-k6i5l8-7y6.t205p-gl2",
"id": "00000002-0000-0002-0000-000600000008"
},
- "team": "00000001-0000-0007-0000-000500000002"
+ "team": "00000001-0000-0007-0000-000500000002",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_Contact_user_9.json b/libs/wire-api/test/golden/testObject_Contact_user_9.json
index 9318c77233..862db1f6de 100644
--- a/libs/wire-api/test/golden/testObject_Contact_user_9.json
+++ b/libs/wire-api/test/golden/testObject_Contact_user_9.json
@@ -7,5 +7,6 @@
"domain": "37-p6v67.g",
"id": "00000000-0000-0000-0000-000600000008"
},
- "team": "00000005-0000-0002-0000-000500000000"
+ "team": "00000005-0000-0002-0000-000500000000",
+ "type": "regular"
}
diff --git a/libs/wire-api/test/golden/testObject_SearchResultContact_1.json b/libs/wire-api/test/golden/testObject_SearchResultContact_1.json
index 3dfe76acce..78b27a72f0 100644
--- a/libs/wire-api/test/golden/testObject_SearchResultContact_1.json
+++ b/libs/wire-api/test/golden/testObject_SearchResultContact_1.json
@@ -9,7 +9,8 @@
"domain": "example.com",
"id": "00000018-0000-0020-0000-000e00000002"
},
- "team": "00000018-0000-0020-0000-000e00000002"
+ "team": "00000018-0000-0020-0000-000e00000002",
+ "type": "regular"
},
{
"accent_id": null,
@@ -20,7 +21,8 @@
"domain": "another.example.com",
"id": "00000018-0000-0020-0000-000e00000003"
},
- "team": null
+ "team": null,
+ "type": "regular"
}
],
"found": 2,
diff --git a/libs/wire-api/test/golden/testObject_SearchResultContact_2.json b/libs/wire-api/test/golden/testObject_SearchResultContact_2.json
index 01117c7003..875534f79a 100644
--- a/libs/wire-api/test/golden/testObject_SearchResultContact_2.json
+++ b/libs/wire-api/test/golden/testObject_SearchResultContact_2.json
@@ -9,7 +9,8 @@
"domain": "example.com",
"id": "00000018-0000-0020-0000-000e00000002"
},
- "team": "00000018-0000-0020-0000-000e00000002"
+ "team": "00000018-0000-0020-0000-000e00000002",
+ "type": "regular"
},
{
"accent_id": null,
@@ -20,7 +21,8 @@
"domain": "another.example.com",
"id": "00000018-0000-0020-0000-000e00000003"
},
- "team": null
+ "team": null,
+ "type": "regular"
}
],
"found": 2,
diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_11.json b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_11.json
index e373317287..b05e65f929 100644
--- a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_11.json
+++ b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_11.json
@@ -9,7 +9,8 @@
"domain": "bza.j",
"id": "00000000-0000-0000-0000-000000000001"
},
- "team": null
+ "team": null,
+ "type": "regular"
},
{
"accent_id": 0,
@@ -20,7 +21,8 @@
"domain": "zwv.u6-f",
"id": "00000001-0000-0000-0000-000000000001"
},
- "team": null
+ "team": null,
+ "type": "regular"
}
],
"found": -1,
diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_13.json b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_13.json
index 01c6c1eee8..1148666947 100644
--- a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_13.json
+++ b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_13.json
@@ -9,7 +9,8 @@
"domain": "795n1zf6-he8-97ur4w.o7r---053",
"id": "00000000-0000-0001-0000-000000000000"
},
- "team": "00000000-0000-0000-0000-000100000000"
+ "team": "00000000-0000-0000-0000-000100000000",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -20,7 +21,8 @@
"domain": "v-t6qc.e.so7jqwv",
"id": "00000000-0000-0001-0000-000000000000"
},
- "team": "00000000-0000-0001-0000-000000000001"
+ "team": "00000000-0000-0001-0000-000000000001",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -31,7 +33,8 @@
"domain": "335.a3.p49c--e-fjz337",
"id": "00000000-0000-0000-0000-000100000001"
},
- "team": "00000000-0000-0001-0000-000000000001"
+ "team": "00000000-0000-0001-0000-000000000001",
+ "type": "regular"
},
{
"accent_id": null,
@@ -42,7 +45,8 @@
"domain": "g.g3n",
"id": "00000001-0000-0000-0000-000000000000"
},
- "team": null
+ "team": null,
+ "type": "regular"
}
],
"found": 3,
diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_14.json b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_14.json
index 7e80395ae6..daf4f793c8 100644
--- a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_14.json
+++ b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_14.json
@@ -9,7 +9,8 @@
"domain": "c00y0ks9-6.q",
"id": "00000000-0000-0001-0000-000100000000"
},
- "team": "00000000-0000-0000-0000-000000000000"
+ "team": "00000000-0000-0000-0000-000000000000",
+ "type": "regular"
},
{
"accent_id": null,
@@ -20,7 +21,8 @@
"domain": "g.44.s3dq77",
"id": "00000001-0000-0001-0000-000000000001"
},
- "team": null
+ "team": null,
+ "type": "regular"
}
],
"found": 1,
diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_19.json b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_19.json
index c34194c529..2c656608d4 100644
--- a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_19.json
+++ b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_19.json
@@ -9,7 +9,8 @@
"domain": "5de.v-6",
"id": "00000000-0000-0001-0000-000100000001"
},
- "team": null
+ "team": null,
+ "type": "regular"
},
{
"accent_id": null,
@@ -20,7 +21,8 @@
"domain": "z76.kcuxql-9",
"id": "00000000-0000-0001-0000-000100000000"
},
- "team": "00000000-0000-0000-0000-000000000001"
+ "team": "00000000-0000-0000-0000-000000000001",
+ "type": "regular"
}
],
"found": 4,
diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_20.json b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_20.json
index 471a9a11c8..f5fe22a975 100644
--- a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_20.json
+++ b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_20.json
@@ -9,7 +9,8 @@
"domain": "66h.j",
"id": "00000000-0000-0001-0000-000000000000"
},
- "team": null
+ "team": null,
+ "type": "regular"
},
{
"accent_id": 0,
@@ -20,7 +21,8 @@
"domain": "7s.k881-q-42",
"id": "00000000-0000-0000-0000-000100000000"
},
- "team": "00000000-0000-0000-0000-000100000001"
+ "team": "00000000-0000-0000-0000-000100000001",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -31,7 +33,8 @@
"domain": "1ux.dy",
"id": "00000001-0000-0001-0000-000100000001"
},
- "team": "00000000-0000-0001-0000-000000000001"
+ "team": "00000000-0000-0001-0000-000000000001",
+ "type": "regular"
},
{
"accent_id": null,
@@ -42,7 +45,8 @@
"domain": "o.xi",
"id": "00000001-0000-0000-0000-000000000000"
},
- "team": null
+ "team": null,
+ "type": "regular"
},
{
"accent_id": null,
@@ -53,7 +57,8 @@
"domain": "x5c.v",
"id": "00000000-0000-0001-0000-000100000000"
},
- "team": "00000001-0000-0000-0000-000000000000"
+ "team": "00000001-0000-0000-0000-000000000000",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -64,7 +69,8 @@
"domain": "9p-8z5.i",
"id": "00000001-0000-0000-0000-000100000001"
},
- "team": "00000001-0000-0001-0000-000000000001"
+ "team": "00000001-0000-0001-0000-000000000001",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -75,7 +81,8 @@
"domain": "h1t7.9.j492",
"id": "00000000-0000-0000-0000-000100000000"
},
- "team": "00000000-0000-0000-0000-000000000001"
+ "team": "00000000-0000-0000-0000-000000000001",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -86,7 +93,8 @@
"domain": "p9.y",
"id": "00000000-0000-0000-0000-000100000001"
},
- "team": null
+ "team": null,
+ "type": "regular"
},
{
"accent_id": null,
@@ -97,7 +105,8 @@
"domain": "saz.d0v8",
"id": "00000000-0000-0000-0000-000100000001"
},
- "team": null
+ "team": null,
+ "type": "regular"
},
{
"accent_id": 0,
@@ -108,7 +117,8 @@
"domain": "gpz.28--u.1646.v5",
"id": "00000000-0000-0001-0000-000000000000"
},
- "team": "00000000-0000-0001-0000-000100000001"
+ "team": "00000000-0000-0001-0000-000100000001",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -119,7 +129,8 @@
"domain": "8p.5.x11-s",
"id": "00000001-0000-0000-0000-000100000001"
},
- "team": "00000000-0000-0001-0000-000100000000"
+ "team": "00000000-0000-0001-0000-000100000000",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -130,7 +141,8 @@
"domain": "q4x5z.mwi3",
"id": "00000000-0000-0001-0000-000000000001"
},
- "team": "00000000-0000-0001-0000-000000000000"
+ "team": "00000000-0000-0001-0000-000000000000",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -141,7 +153,8 @@
"domain": "38.b7",
"id": "00000000-0000-0000-0000-000000000000"
},
- "team": "00000000-0000-0001-0000-000100000000"
+ "team": "00000000-0000-0001-0000-000100000000",
+ "type": "regular"
}
],
"found": 7,
diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_3.json b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_3.json
index d3a743390b..eb3ce536ab 100644
--- a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_3.json
+++ b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_3.json
@@ -9,7 +9,8 @@
"domain": "guh.e",
"id": "00000001-0000-0001-0000-000100000000"
},
- "team": "00000000-0000-0000-0000-000100000000"
+ "team": "00000000-0000-0000-0000-000100000000",
+ "type": "regular"
}
],
"found": 4,
diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_4.json b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_4.json
index b635210594..5118eee5c3 100644
--- a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_4.json
+++ b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_4.json
@@ -9,7 +9,8 @@
"domain": "2.60--1n1.ds",
"id": "00000000-0000-0000-0000-000000000000"
},
- "team": "00000001-0000-0000-0000-000100000001"
+ "team": "00000001-0000-0000-0000-000100000001",
+ "type": "regular"
},
{
"accent_id": null,
@@ -20,7 +21,8 @@
"domain": "onrg.u",
"id": "00000000-0000-0000-0000-000100000001"
},
- "team": "00000000-0000-0000-0000-000100000000"
+ "team": "00000000-0000-0000-0000-000100000000",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -31,7 +33,8 @@
"domain": "660.v1.8z2.a-4dv.y",
"id": "00000001-0000-0000-0000-000000000000"
},
- "team": "00000000-0000-0001-0000-000100000001"
+ "team": "00000000-0000-0001-0000-000100000001",
+ "type": "regular"
},
{
"accent_id": null,
@@ -42,7 +45,8 @@
"domain": "t102d9m3.tb-dryc9.ws300w5xc4",
"id": "00000001-0000-0001-0000-000000000001"
},
- "team": null
+ "team": null,
+ "type": "regular"
},
{
"accent_id": 0,
@@ -53,7 +57,8 @@
"domain": "54up.l8h-b-g-i.x-c.9-7.we35781l0b",
"id": "00000000-0000-0000-0000-000100000000"
},
- "team": "00000000-0000-0001-0000-000000000000"
+ "team": "00000000-0000-0001-0000-000000000000",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -64,7 +69,8 @@
"domain": "a.h9-1",
"id": "00000000-0000-0001-0000-000100000000"
},
- "team": "00000000-0000-0000-0000-000000000000"
+ "team": "00000000-0000-0000-0000-000000000000",
+ "type": "regular"
}
],
"found": -5,
diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_5.json b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_5.json
index c958b20589..318d927d81 100644
--- a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_5.json
+++ b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_5.json
@@ -9,7 +9,8 @@
"domain": "1b-y90e265f.l-c",
"id": "00000001-0000-0000-0000-000000000001"
},
- "team": "00000001-0000-0000-0000-000100000001"
+ "team": "00000001-0000-0000-0000-000100000001",
+ "type": "regular"
}
],
"found": -6,
diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_7.json b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_7.json
index 4ec4137a7f..a978842e1b 100644
--- a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_7.json
+++ b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_7.json
@@ -9,7 +9,8 @@
"domain": "1386---3-nddry.o",
"id": "00000001-0000-0000-0000-000000000000"
},
- "team": "00000001-0000-0000-0000-000000000001"
+ "team": "00000001-0000-0000-0000-000000000001",
+ "type": "regular"
},
{
"accent_id": 0,
@@ -20,7 +21,8 @@
"domain": "j-cz923pu.l6.73-6.qq05n.4ig.dl3",
"id": "00000001-0000-0000-0000-000100000001"
},
- "team": "00000000-0000-0001-0000-000100000000"
+ "team": "00000000-0000-0001-0000-000100000000",
+ "type": "regular"
}
],
"found": 7,
diff --git a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_8.json b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_8.json
index e2881a4de6..302b38265d 100644
--- a/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_8.json
+++ b/libs/wire-api/test/golden/testObject_SearchResult_20Contact_user_8.json
@@ -9,7 +9,8 @@
"domain": "6n.n08ejr-a",
"id": "00000001-0000-0001-0000-000100000001"
},
- "team": "00000000-0000-0001-0000-000100000000"
+ "team": "00000000-0000-0001-0000-000100000000",
+ "type": "regular"
}
],
"found": -7,
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 11ce3d914e..684a5f9fb8 100644
--- a/libs/wire-api/test/unit/Test/Wire/API/User.hs
+++ b/libs/wire-api/test/unit/Test/Wire/API/User.hs
@@ -63,7 +63,7 @@ testEmailVisibleToSelf :: TestTree
testEmailVisibleToSelf =
testProperty "should not contain email when email visibility is EmailVisibleToSelf" $
\user lhStatus ->
- let profile = mkUserProfile EmailVisibleToSelf user lhStatus
+ let profile = mkUserProfile EmailVisibleToSelf UserTypeRegular user lhStatus
in profileEmail profile === Nothing
.&&. profileLegalholdStatus profile === lhStatus
@@ -71,7 +71,7 @@ testEmailVisibleIfOnTeam :: TestTree
testEmailVisibleIfOnTeam =
testProperty "should contain email only if the user has one and is part of a team when email visibility is EmailVisibleIfOnTeam" $
\user lhStatus ->
- let profile = mkUserProfile EmailVisibleIfOnTeam user lhStatus
+ let profile = mkUserProfile EmailVisibleIfOnTeam UserTypeRegular user lhStatus
in (profileEmail profile === (userTeam user *> userEmail user))
.&&. profileLegalholdStatus profile === lhStatus
@@ -81,13 +81,13 @@ testEmailVisibleIfOnSameTeam =
where
testNoViewerTeam = testProperty "should not contain email when viewer is not part of a team" $
\user lhStatus ->
- let profile = mkUserProfile (EmailVisibleIfOnSameTeam Nothing) user lhStatus
+ let profile = mkUserProfile (EmailVisibleIfOnSameTeam Nothing) UserTypeRegular user lhStatus
in (profileEmail profile === Nothing)
.&&. profileLegalholdStatus profile === lhStatus
testViewerDifferentTeam = testProperty "should not contain email when viewer is not part of the same team" $
\viewerTeamId viewerMembership user lhStatus ->
- let profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) user lhStatus
+ let profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) UserTypeRegular user lhStatus
in Just viewerTeamId /= userTeam user ==>
( profileEmail profile === Nothing
.&&. profileLegalholdStatus profile === lhStatus
@@ -97,7 +97,7 @@ testEmailVisibleIfOnSameTeam =
\viewerTeamId (viewerMembershipNoRole :: TeamMember) userNoTeam lhStatus ->
let user = userNoTeam {userTeam = Just viewerTeamId}
viewerMembership = viewerMembershipNoRole & TeamMember.permissions .~ TeamMember.rolePermissions RoleExternalPartner
- profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) user lhStatus
+ profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) UserTypeRegular user lhStatus
in ( profileEmail profile === Nothing
.&&. profileLegalholdStatus profile === lhStatus
)
@@ -106,7 +106,7 @@ testEmailVisibleIfOnSameTeam =
\viewerTeamId (viewerMembershipNoRole :: TeamMember) viewerRole userNoTeam lhStatus ->
let user = userNoTeam {userTeam = Just viewerTeamId}
viewerMembership = viewerMembershipNoRole & TeamMember.permissions .~ TeamMember.rolePermissions viewerRole
- profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) user lhStatus
+ profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) UserTypeRegular user lhStatus
in viewerRole /= RoleExternalPartner ==>
( profileEmail profile === userEmail user
.&&. profileLegalholdStatus profile === lhStatus
diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs
index 84c137d9c3..e5585735bd 100644
--- a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs
+++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs
@@ -34,6 +34,7 @@ import System.Logger.Message qualified as Log
import Wire.API.Team.Feature
import Wire.API.Team.Member.Info
import Wire.API.Team.Role
+import Wire.API.User
import Wire.GalleyAPIAccess
import Wire.IndexedUserStore (IndexedUserStore)
import Wire.IndexedUserStore qualified as IndexedUserStore
@@ -108,15 +109,29 @@ syncAllUsersWithVersion mkVersion =
mkUserDocs :: ConduitT [IndexUser] [(ES.DocId, UserDoc, ES.VersionControl)] (Sem r) ()
mkUserDocs = Conduit.mapM $ \page -> do
+ -- FUTUREWORK: extract team visibilities, roles and user type
+ -- more efficiently sending one query per page
+
+ -- FUTUREWORK: introduce type ExtendedUser (or something), which
+ -- contains User, Maybe Role, UserType, ..., and pass around
+ -- ExtendedUser. this should make the code less convoluted.
+
let teams :: Map TeamId [IndexUser] = Map.fromListWith (<>) $ mapMaybe (\u -> (,[u]) . value <$> u.teamId) page
teamIds = Map.keys teams
visMap <- fmap Map.fromList . unsafePooledForConcurrentlyN 16 teamIds $ \t ->
(t,) <$> teamSearchVisibilityInbound t
+ userTypes :: Map UserId UserType <- fmap Map.fromList . unsafePooledForConcurrentlyN 16 page $ \iu ->
+ (iu.userId,) <$> getUserType iu
roles :: Map UserId (WithWritetime Role) <- fmap (Map.fromList . concat) . unsafePooledForConcurrentlyN 16 (Map.toList teams) $ \(t, us) -> do
tms <- (.members) <$> selectTeamMemberInfos t (fmap (.userId) us)
pure $ mapMaybe mkRoleWithWriteTime tms
let vis indexUser = fromMaybe defaultSearchVisibilityInbound $ (flip Map.lookup visMap . value =<< indexUser.teamId)
- mkUserDoc indexUser = indexUserToDoc (vis indexUser) ((.value) <$> Map.lookup indexUser.userId roles) indexUser
+ mkUserDoc indexUser =
+ indexUserToDoc
+ (vis indexUser)
+ (Map.lookup indexUser.userId userTypes)
+ ((.value) <$> Map.lookup indexUser.userId roles)
+ indexUser
mkDocVersion u = mkVersion . ES.ExternalDocVersion . docVersion $ indexUserToVersion (Map.lookup u.userId roles) u
pure $ map (\u -> (userIdToDocId u.userId, mkUserDoc u, mkDocVersion u)) page
@@ -165,3 +180,22 @@ teamSearchVisibilityInbound :: (Member GalleyAPIAccess r) => TeamId -> Sem r Sea
teamSearchVisibilityInbound tid =
searchVisibilityInboundFromFeatureStatus . (.status)
<$> getFeatureConfigForTeam @_ @SearchVisibilityInboundConfig tid
+
+-- | FUTUREWORK: this is duplicated code from UserSubsystem, we should
+-- probably expose it as an action there.
+getUserType ::
+ forall r.
+ IndexUser ->
+ Sem r UserType
+getUserType iu = case iu.serviceId of
+ Just _ -> pure UserTypeBot
+ Nothing -> do
+ {-
+ FUTUREWORK: *correct* type fields from search are coming in a separate PR:
+
+ mmApp <- mapM (getApp iu.userId) (iu.teamId <&> (.value))
+ case join mmApp of
+ Just _ -> pure UserTypeApp
+ Nothing -> pure UserTypeRegular
+ -}
+ pure UserTypeRegular
diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs
index 317a309def..28b766f0db 100644
--- a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs
+++ b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs
@@ -29,6 +29,7 @@ import Data.ByteString.Lazy
import Data.Handle
import Data.Id
import Data.Json.Util
+import Data.Qualified
import Data.Text.Encoding
import Database.Bloodhound.Types
import Imports
@@ -61,6 +62,7 @@ mkIndexVersion writetimes =
-- consequently removed from the index.
data UserDoc = UserDoc
{ udId :: UserId,
+ udType :: Maybe UserType,
udTeam :: Maybe TeamId,
udName :: Maybe Name,
udNormalized :: Maybe Text,
@@ -85,6 +87,7 @@ instance ToJSON UserDoc where
toJSON ud =
object
[ "id" .= udId ud,
+ "type" .= udType ud,
"team" .= udTeam ud,
"name" .= udName ud,
"normalized" .= udNormalized ud,
@@ -107,6 +110,7 @@ instance FromJSON UserDoc where
parseJSON = withObject "UserDoc" $ \o ->
UserDoc
<$> o .: "id"
+ <*> o .:? "type"
<*> o .:? "team"
<*> o .:? "name"
<*> o .:? "normalized"
@@ -127,6 +131,27 @@ instance FromJSON UserDoc where
searchVisibilityInboundFieldName :: Key
searchVisibilityInboundFieldName = "search_visibility_inbound"
+-- Qualified UserId is not included in `UserDoc`, so it needs to be
+-- provided here. Monad will most likely be Identity (I promise we'll
+-- always make up some name if missing) or Maybe (if no name, then no
+-- contact).
+userDocToContact :: (Monad m) => Qualified UserId -> (Maybe Name -> m Text) -> UserDoc -> m Contact
+userDocToContact contactQualifiedId getName userDoc =
+ getName userDoc.udName <&> \name ->
+ Contact
+ { contactQualifiedId,
+ contactName = name,
+ contactColorId = fromIntegral . fromColourId <$> userDoc.udColourId,
+ contactHandle = fromHandle <$> userDoc.udHandle,
+ contactTeam = userDoc.udTeam,
+ contactType =
+ -- NB: after wire release upgrade and before ES reindexing,
+ -- apps may identify as regular users in the search result.
+ -- this is an accepted limitation and will be fixed in
+ -- https://github.com/wireapp/wire-server/pull/4947
+ fromMaybe UserTypeRegular userDoc.udType
+ }
+
userDocToTeamContact :: [UserGroupId] -> UserDoc -> TeamContact
userDocToTeamContact userGroups UserDoc {..} =
TeamContact
diff --git a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs
index b833bbfaad..824fe49e24 100644
--- a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs
+++ b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs
@@ -149,12 +149,14 @@ indexUserToVersion role IndexUser {..} =
const () <$$> writeTimeBumper
]
-indexUserToDoc :: SearchVisibilityInbound -> Maybe Role -> IndexUser -> UserDoc
-indexUserToDoc searchVisInbound mRole IndexUser {..} =
+indexUserToDoc :: SearchVisibilityInbound -> Maybe UserType -> Maybe Role -> IndexUser -> UserDoc
+indexUserToDoc searchVisInbound mUserType mRole IndexUser {..} =
if shouldIndex
then
UserDoc
- { udSearchable = value <$> searchable,
+ { udId = userId,
+ udType = mUserType,
+ udSearchable = value <$> searchable,
udEmailUnvalidated = value <$> unverifiedEmail,
udSso = sso . value =<< ssoId,
udScimExternalId = join $ scimExternalId <$> (value <$> managedBy) <*> (value <$> ssoId),
@@ -169,8 +171,7 @@ indexUserToDoc searchVisInbound mRole IndexUser {..} =
udHandle = value <$> handle,
udNormalized = Just $ normalized name.value.fromName,
udName = Just name.value,
- udTeam = value <$> teamId,
- udId = userId
+ udTeam = value <$> teamId
}
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
@@ -214,7 +215,8 @@ normalized = transliterate (trans "Any-Latin; Latin-ASCII; Lower")
emptyUserDoc :: UserId -> UserDoc
emptyUserDoc uid =
UserDoc
- { udSearchable = Nothing,
+ { udType = Nothing,
+ udSearchable = Nothing,
udEmailUnvalidated = Nothing,
udSso = Nothing,
udScimExternalId = Nothing,
diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs
index 86be662b3c..c6524d8e6b 100644
--- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs
+++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs
@@ -451,13 +451,11 @@ getLocalUserProfileImpl emailVisibilityConfigWithViewer luid = do
lhs :: UserLegalHoldStatus <- do
teamMember <- lift $ join <$> (internalGetTeamMember storedUser.id `mapM` storedUser.teamId)
pure $ maybe defUserLegalHoldStatus (view legalHoldStatus) teamMember
+ userType <- lift $ getUserType storedUser.id storedUser.teamId storedUser.serviceId
let user = mkUserFromStored domain locale storedUser
- usrProfile = mkUserProfile emailVisibilityConfigWithViewer user lhs
- app <- lift $ mapM (getApp storedUser.id) storedUser.teamId
+ usrProfile = mkUserProfile emailVisibilityConfigWithViewer userType user lhs
lift $ deleteLocalIfExpired user
- pure $ case join app of
- Nothing -> usrProfile
- Just _ -> usrProfile {profileType = UserTypeApp}
+ pure $ usrProfile
getSelfProfileImpl ::
( Member (Input UserSubsystemConfig) r,
@@ -578,6 +576,7 @@ guardLockedHandleField user updateOrigin handle = do
updateUserProfileImpl ::
( Member UserStore r,
+ Member AppStore r,
Member (Error UserSubsystemError) r,
Member Events r,
Member GalleyAPIAccess r,
@@ -642,6 +641,7 @@ updateHandleImpl ::
Member GalleyAPIAccess r,
Member Events r,
Member UserStore r,
+ Member AppStore r,
Member IndexedUserStore r,
Member Metrics r
) =>
@@ -708,13 +708,14 @@ checkHandlesImpl check num = reverse <$> collectFree [] check num
syncUserIndex ::
forall r.
( Member UserStore r,
+ Member AppStore r,
Member GalleyAPIAccess r,
Member IndexedUserStore r,
Member Metrics r
) =>
UserId ->
Sem r ()
-syncUserIndex uid = do
+syncUserIndex uid =
getIndexUser uid
>>= maybe deleteFromIndex upsert
where
@@ -731,8 +732,9 @@ syncUserIndex uid = do
(teamSearchVisibilityInbound . value)
indexUser.teamId
tm <- maybe (pure Nothing) (selectTeamMember . value) indexUser.teamId
+ userType <- getUserType indexUser.userId (indexUser.teamId <&> (.value)) (indexUser.serviceId <&> (.value))
let mRole = tm >>= mkRoleWithWriteTime
- userDoc = indexUserToDoc vis (value <$> mRole) indexUser
+ userDoc = indexUserToDoc vis (Just userType) (value <$> mRole) indexUser
version = ES.ExternalGT . ES.ExternalDocVersion . docVersion $ indexUserToVersion mRole indexUser
Metrics.incCounter indexUpdateCounter
IndexedUserStore.upsert (userIdToDocId uid) userDoc version
@@ -760,6 +762,7 @@ searchUsersImpl ::
forall r fedM.
( Member UserStore r,
Member GalleyAPIAccess r,
+ Member AppStore r,
Member (Error UserSubsystemError) r,
Member IndexedUserStore r,
Member FederationConfigStore r,
@@ -795,6 +798,7 @@ searchUsersImpl searcherId searchTerm maybeDomain maybeMaxResults = do
searchLocally ::
forall r.
( Member GalleyAPIAccess r,
+ Member AppStore r,
Member UserStore r,
Member IndexedUserStore r,
Member (Input UserSubsystemConfig) r
@@ -823,7 +827,7 @@ searchLocally searcher searchTerm maybeMaxResults = do
esMaxResults
else pure $ SearchResult 0 0 0 [] FullSearch Nothing Nothing
- let esContacts = map userDocToContact (searchResults esResult)
+ let esContacts = map userDocToContact' (searchResults esResult)
-- Prepend results matching exact handle and results from ES.
allContacts = case maybeExactHandleMatch of
Nothing -> esContacts
@@ -839,15 +843,13 @@ searchLocally searcher searchTerm maybeMaxResults = do
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
- }
+ userDocToContact' :: UserDoc -> Contact
+ userDocToContact' userDoc =
+ runIdentity $
+ userDocToContact
+ (tUntagged $ qualifyAs searcher userDoc.udId)
+ (Identity . maybe "" fromName)
+ userDoc
mkTeamSearchInfo :: Maybe TeamId -> Sem r TeamSearchInfo
mkTeamSearchInfo searcherTeamId = do
@@ -864,27 +866,26 @@ searchLocally searcher searchTerm maybeMaxResults = do
exactHandleSearch :: Sem r (Maybe Contact)
exactHandleSearch = runMaybeT $ do
- handle <- MaybeT . pure $ Handle.parseHandle searchTerm
+ handle <- hoistMaybe $ Handle.parseHandle searchTerm
owner <- MaybeT $ UserStore.lookupHandle handle
storedUser <- MaybeT $ UserStore.getUser owner
config <- lift input
- let contact = contactFromStoredUser (tDomain searcher) storedUser
- isContactVisible =
+ let isContactVisible =
(config.searchSameTeamOnly && (snd . tUnqualified $ searcher) == storedUser.teamId)
|| (not config.searchSameTeamOnly)
if isContactVisible && fromMaybe True storedUser.searchable
- 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
- }
+ then do
+ userType <- lift $ getUserType storedUser.id storedUser.teamId storedUser.serviceId
+ pure $
+ Contact
+ { contactQualifiedId = Qualified storedUser.id (tDomain searcher),
+ contactName = fromName storedUser.name,
+ contactHandle = Handle.fromHandle <$> storedUser.handle,
+ contactColorId = Just . fromIntegral . fromColourId $ storedUser.accentId,
+ contactTeam = storedUser.teamId,
+ contactType = userType
+ }
+ else hoistMaybe Nothing
searchRemotely ::
( Member FederationConfigStore r,
@@ -1053,6 +1054,7 @@ getAccountsByImpl (tSplit -> (domain, GetBy {includePendingInvitations, getByHan
acceptTeamInvitationImpl ::
( Member (Input UserSubsystemConfig) r,
Member UserStore r,
+ Member AppStore r,
Member GalleyAPIAccess r,
Member (Error UserSubsystemError) r,
Member InvitationStore r,
@@ -1117,6 +1119,7 @@ getUserExportDataImpl uid = fmap hush . runError @() $ do
removeEmailEitherImpl ::
( Member UserKeyStore r,
Member UserStore r,
+ Member AppStore r,
Member Events r,
Member IndexedUserStore r,
Member (Input UserSubsystemConfig) r,
@@ -1151,6 +1154,7 @@ checkUserIsAdminImpl uid = do
setUserSearchableImpl ::
( Member UserStore r,
+ Member AppStore r,
Member (Error UserSubsystemError) r,
Member TeamSubsystem r,
Member GalleyAPIAccess r,
@@ -1166,3 +1170,20 @@ setUserSearchableImpl luid uid searchable = do
ensurePermissions (tUnqualified luid) tid [SetMemberSearchable]
UserStore.setUserSearchable uid searchable
syncUserIndex uid
+
+-- * Helpers
+
+getUserType ::
+ forall r.
+ (Member AppStore r) =>
+ UserId ->
+ Maybe TeamId ->
+ Maybe ServiceId ->
+ Sem r UserType
+getUserType uid mTid mbServiceId = case mbServiceId of
+ Just _ -> pure UserTypeBot
+ Nothing -> do
+ mmApp <- mapM (getApp uid) mTid
+ case join mmApp of
+ Just _ -> pure UserTypeApp
+ Nothing -> pure UserTypeRegular
diff --git a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs
index 1ff35582da..dfb4329076 100644
--- a/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs
+++ b/libs/wire-subsystems/test/unit/Wire/AuthenticationSubsystem/InterpreterSpec.hs
@@ -42,6 +42,7 @@ import Wire.API.User
import Wire.API.User qualified as User
import Wire.API.User.Auth
import Wire.API.User.Password
+import Wire.AppStore
import Wire.AuthenticationSubsystem
import Wire.AuthenticationSubsystem.Config
import Wire.AuthenticationSubsystem.Interpreter
@@ -81,7 +82,8 @@ type AllEffects =
EmailSubsystem,
UserStore,
State [StoredUser],
- State (Map EmailAddress [SentMail])
+ State (Map EmailAddress [SentMail]),
+ State [StoredApp]
]
runAllEffects :: Domain -> [User] -> Maybe [Text] -> Sem AllEffects a -> Either AuthenticationSubsystemError a
@@ -92,6 +94,7 @@ runAllEffects localDomain preexistingUsers mAllowedEmailDomains =
local = toLocalUnsafe localDomain ()
}
in run
+ . evalState mempty
. evalState mempty
. evalState mempty
. inMemoryUserStoreInterpreter
diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs
index 204fc5ed73..fbd01bdd5b 100644
--- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs
+++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs
@@ -522,10 +522,17 @@ miniGetAllProfiles ::
Sem r [UserProfile]
miniGetAllProfiles = do
users <- gets (.users)
+ apps <- gets (.apps)
dom <- input
pure $
map
- (\u -> mkUserProfileWithEmail Nothing (mkUserFromStored dom miniLocale u) defUserLegalHoldStatus)
+ ( \u ->
+ let userType
+ | any ((== u.id) . (.id)) apps = UserTypeApp
+ | isJust u.serviceId = UserTypeBot
+ | otherwise = UserTypeRegular
+ in mkUserProfileWithEmail Nothing userType (mkUserFromStored dom miniLocale u) defUserLegalHoldStatus
+ )
users
miniGetUsersByIds :: [UserId] -> MiniFederationMonad 'Brig [UserProfile]
diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs
index 85b27a86b3..f481ed7367 100644
--- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs
+++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/IndexedUserStore.hs
@@ -30,10 +30,7 @@ import Polysemy.State
import Wire.API.Team.Size
import Wire.API.User.Search
import Wire.IndexedUserStore
-import Wire.MockInterpreters.UserStore (storedUserToIndexUser)
-import Wire.StoredUser
import Wire.UserSearch.Types
-import Wire.UserStore.IndexUser
newtype OrdDocId = OrdDocId Text
deriving (Show, Eq, Ord)
@@ -54,17 +51,6 @@ emptyIndex =
docs = mempty
}
-storedUserToDoc :: StoredUser -> UserDoc
-storedUserToDoc user =
- let indexUser = storedUserToIndexUser user
- in indexUserToDoc defaultSearchVisibilityInbound Nothing indexUser
-
-indexFromStoredUsers :: [StoredUser] -> UserIndex
-indexFromStoredUsers storedUsers = do
- run . execState emptyIndex . inMemoryIndexedUserStoreInterpreter $ do
- for_ storedUsers $ \storedUser ->
- upsert (userIdToDocId storedUser.id) (storedUserToDoc storedUser) ES.NoVersionControl
-
runInMemoryIndexedUserStoreIntepreter :: InterpreterFor IndexedUserStore r
runInMemoryIndexedUserStoreIntepreter =
evalState emptyIndex
diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs
index 391e8305d1..6e2086242c 100644
--- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs
+++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserStore.hs
@@ -63,8 +63,9 @@ inMemoryUserStoreInterpreter = interpret $ \case
if u.id == uid
then u {emailUnvalidated = Just email} :: StoredUser
else u
- GetIndexUser uid ->
- gets $ fmap storedUserToIndexUser . find (\user -> user.id == uid)
+ GetIndexUser uid -> do
+ mUser <- gets @[StoredUser] $ find (\user -> user.id == uid)
+ pure $ storedUserToIndexUser <$> mUser
GetIndexUsersPaginated _pageSize _pagingState ->
error "GetIndexUsersPaginated not implemented in inMemoryUserStoreInterpreter"
UpdateUserHandleEither uid hUpdate -> runError $ modifyLocalUsers (traverse doUpdate)
@@ -72,7 +73,7 @@ inMemoryUserStoreInterpreter = interpret $ \case
doUpdate :: StoredUser -> Sem (Error StoredUserUpdateError : r) StoredUser
doUpdate u
| u.id == uid = do
- handles <- gets $ mapMaybe (.handle)
+ handles <- gets @[StoredUser] $ mapMaybe (.handle)
when
( hUpdate.old
/= Just hUpdate.new
@@ -87,7 +88,7 @@ inMemoryUserStoreInterpreter = interpret $ \case
us <- get
us' <- f us
put us'
- DeleteUser user -> modify $ filter (\u -> u.id /= User.userId user)
+ DeleteUser user -> modify @[StoredUser] $ filter (\u -> u.id /= User.userId user)
LookupHandle h -> lookupHandleImpl h
GlimpseHandle h -> lookupHandleImpl h
LookupStatus uid -> lookupStatusImpl uid
@@ -105,7 +106,7 @@ inMemoryUserStoreInterpreter = interpret $ \case
doUpdate :: StoredUser -> StoredUser
doUpdate u = if u.id == uid then u {email = Nothing} else u
GetUserTeam uid -> do
- gets $ \users -> do
+ gets @[StoredUser] $ \users -> do
user <- find (\user -> user.id == uid) users
user.teamId
SetUserSearchable uid (SetSearchable searchable) -> modify $ map f
diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs
index 71dc964440..fe979c1cf1 100644
--- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs
+++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs
@@ -69,4 +69,4 @@ userSubsystemTestInterpreter initialUsers =
SetUserSearchable {} -> error "SetUserSearchable: implement on demand (userSubsystemInterpreter)"
toProfile :: User -> UserProfile
-toProfile u = mkUserProfileWithEmail (userEmail u) u UserLegalHoldDisabled
+toProfile u = mkUserProfileWithEmail (userEmail u) UserTypeRegular u UserLegalHoldDisabled
diff --git a/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs
index 4fae73f196..a09d56bd8f 100644
--- a/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs
+++ b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs
@@ -64,7 +64,8 @@ userDoc1 =
udScimExternalId = Nothing,
udSso = Nothing,
udEmailUnvalidated = Nothing,
- udSearchable = Nothing
+ udSearchable = Nothing,
+ udType = Nothing
}
-- Dont touch this. This represents serialized legacy data.
diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs
index edfb8af1f8..3c93c88d4d 100644
--- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs
+++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs
@@ -38,6 +38,7 @@ import Data.Set (insert, member, notMember)
import Data.Set qualified as S
import Data.String.Conversions (cs)
import Data.Text.Encoding (encodeUtf8)
+import Database.Bloodhound.Internal.Client qualified as ES
import Imports
import Polysemy
import Polysemy.Error
@@ -60,6 +61,7 @@ import Wire.API.User.Search
import Wire.API.UserEvent
import Wire.AuthenticationSubsystem.Error
import Wire.DomainRegistrationStore qualified as DRS
+import Wire.IndexedUserStore qualified as IU
import Wire.InvitationStore (InsertInvitation, StoredInvitation)
import Wire.InvitationStore qualified as InvitationStore
import Wire.MiniBackend
@@ -67,6 +69,8 @@ import Wire.MockInterpreters
import Wire.RateLimit
import Wire.StoredUser
import Wire.UserKeyStore
+import Wire.UserSearch.Types
+import Wire.UserStore.IndexUser
import Wire.UserSubsystem
import Wire.UserSubsystem.Error
import Wire.UserSubsystem.HandleBlacklist
@@ -100,6 +104,7 @@ spec = describe "UserSubsystem.Interpreter" do
mkExpectedProfiles domain users =
[ mkUserProfileWithEmail
Nothing
+ (if isJust targetUser.serviceId then UserTypeBot else UserTypeRegular)
(mkUserFromStored domain miniLocale targetUser)
defUserLegalHoldStatus
| targetUser <- users
@@ -159,6 +164,7 @@ spec = describe "UserSubsystem.Interpreter" do
in retrievedProfiles
=== [ mkUserProfile
(fmap (const $ (,) <$> viewer.teamId <*> Just teamMember) config.emailVisibilityConfig)
+ (if isJust targetUser.serviceId then UserTypeBot else UserTypeRegular)
(mkUserFromStored domain config.defaultLocale targetUser)
defUserLegalHoldStatus
]
@@ -175,6 +181,7 @@ spec = describe "UserSubsystem.Interpreter" do
in retrievedProfile
=== [ mkUserProfile
(fmap (const Nothing) config.emailVisibilityConfig)
+ (if isJust targetUser.serviceId then UserTypeBot else UserTypeRegular)
(mkUserFromStored domain config.defaultLocale targetUser)
defUserLegalHoldStatus
]
@@ -1091,6 +1098,19 @@ spec = describe "UserSubsystem.Interpreter" do
\(ActiveStoredUser searcheeNoHandle) (searcheeHandle :: Handle) (ActiveStoredUser searcher) localDomain configBase ->
let teamMember = mkTeamMember searcher.id fullPermissions Nothing defUserLegalHoldStatus
searchee = searcheeNoHandle {handle = Just searcheeHandle} :: StoredUser
+
+ storedUserToDoc :: StoredUser -> UserDoc
+ storedUserToDoc user =
+ let indexUser = storedUserToIndexUser user
+ userType = if isJust user.serviceId then UserTypeBot else UserTypeRegular
+ in indexUserToDoc defaultSearchVisibilityInbound (Just userType) Nothing indexUser
+
+ indexFromStoredUsers :: [StoredUser] -> UserIndex
+ indexFromStoredUsers storedUsers = do
+ run . execState emptyIndex . inMemoryIndexedUserStoreInterpreter $ do
+ for_ storedUsers $ \storedUser ->
+ IU.upsert (userIdToDocId storedUser.id) (storedUserToDoc storedUser) ES.NoVersionControl
+
localBackend =
def
{ users = [searchee, searcher],
@@ -1111,6 +1131,7 @@ spec = describe "UserSubsystem.Interpreter" do
contactQualifiedId = Qualified searchee.id localDomain,
contactName = fromName searchee.name,
contactHandle = fromHandle <$> searchee.handle,
- contactColorId = Just . fromIntegral $ searchee.accentId.fromColourId
+ contactColorId = Just . fromIntegral $ searchee.accentId.fromColourId,
+ contactType = UserTypeRegular
}
pure $ result.searchResults === [expectedContact | fromMaybe True searchee.searchable]
diff --git a/postgres-schema.sql b/postgres-schema.sql
index 378195989b..8cfc62bd5d 100644
--- a/postgres-schema.sql
+++ b/postgres-schema.sql
@@ -9,8 +9,8 @@
\restrict 79bbfb4630959c48307653a5cd3d83f2582b3c2210f75f10d79e3ebf0015620
--- Dumped from database version 17.6
--- Dumped by pg_dump version 17.6
+-- Dumped from database version 17.7
+-- Dumped by pg_dump version 17.7
SET statement_timeout = 0;
SET lock_timeout = 0;
diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs
index dbc6a74f95..ad58b6d53f 100644
--- a/services/brig/src/Brig/Index/Eval.hs
+++ b/services/brig/src/Brig/Index/Eval.hs
@@ -39,6 +39,7 @@ import Data.Credentials (Credentials (..))
import Data.Id
import Database.Bloodhound qualified as ES
import Database.Bloodhound.Internal.Client (BHEnv (..))
+import Hasql.Pool
import Imports
import Polysemy
import Polysemy.Embed (runEmbedded)
@@ -46,7 +47,7 @@ import Polysemy.Error
import Polysemy.TinyLog hiding (Logger)
import System.Logger qualified as Log
import System.Logger.Class (Logger)
-import Util.Options (initCredentials)
+import Util.Options
import Wire.API.Federation.Client (FederatorClient)
import Wire.API.Federation.Error
import Wire.BlockListStore (BlockListStore)
@@ -97,6 +98,7 @@ type BrigIndexEffectStack =
Metrics,
TinyLog,
Concurrency 'Unsafe,
+ Error UsageError,
Embed IO,
Final IO
]
@@ -125,6 +127,7 @@ runSem esConn cas galleyEndpoint logger action = do
migrationIndexName = fromMaybe defaultMigrationIndexName (esMigrationIndexName esConn)
runFinal
. embedToFinal
+ . throwErrorToIOFinal @UsageError
. unsafelyPerformConcurrency
. loggerToTinyLogReqId reqId logger
. ignoreMetrics
diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs
index 603aeaa5e4..86c7ffb13e 100644
--- a/services/brig/src/Brig/Provider/API.hs
+++ b/services/brig/src/Brig/Provider/API.hs
@@ -898,7 +898,7 @@ guardConvAdmin conv = do
botGetSelf :: BotId -> (Handler r) Public.UserProfile
botGetSelf bot = do
p <- lift $ wrapClient $ User.lookupUser NoPendingInvitations (botUserId bot)
- maybe (throwStd (errorToWai @'E.UserNotFound)) (\u -> pure $ Public.mkUserProfile EmailVisibleToSelf u UserLegalHoldNoConsent) p
+ maybe (throwStd (errorToWai @'E.UserNotFound)) (\u -> pure $ Public.mkUserProfile EmailVisibleToSelf UserTypeBot u UserLegalHoldNoConsent) p
botGetClient :: (Member GalleyAPIAccess r) => BotId -> (Handler r) (Maybe Public.Client)
botGetClient bot = do
diff --git a/services/brig/src/Brig/User/API/Handle.hs b/services/brig/src/Brig/User/API/Handle.hs
index 0b97637255..ecad744a94 100644
--- a/services/brig/src/Brig/User/API/Handle.hs
+++ b/services/brig/src/Brig/User/API/Handle.hs
@@ -101,5 +101,6 @@ contactFromProfile profile =
contactName = fromName $ profileName profile,
contactHandle = fromHandle <$> profileHandle profile,
contactColorId = Just . fromIntegral . fromColourId $ profileAccentId profile,
- contactTeam = profileTeam profile
+ contactTeam = profileTeam profile,
+ contactType = profileType profile
}
diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs
index 634f059559..4c4919729d 100644
--- a/services/brig/src/Brig/User/Search/Index.hs
+++ b/services/brig/src/Brig/User/Search/Index.hs
@@ -466,6 +466,14 @@ indexMapping =
mpIndex = True,
mpAnalyzer = Nothing,
mpFields = mempty
+ },
+ "type"
+ .= MappingProperty
+ { mpType = MPKeyword,
+ mpStore = False,
+ mpIndex = True,
+ mpAnalyzer = Nothing,
+ mpFields = mempty
}
]
]
diff --git a/services/brig/src/Brig/User/Search/SearchIndex.hs b/services/brig/src/Brig/User/Search/SearchIndex.hs
index da1458aa41..b648bb0cf7 100644
--- a/services/brig/src/Brig/User/Search/SearchIndex.hs
+++ b/services/brig/src/Brig/User/Search/SearchIndex.hs
@@ -1,4 +1,3 @@
-{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StrictData #-}
-- This file is part of the Wire Server implementation.
@@ -30,12 +29,11 @@ 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
import Data.Qualified (Qualified (Qualified))
import Database.Bloodhound qualified as ES
import Imports hiding (log, searchable)
-import Wire.API.User (ColourId (..), Name (fromName))
+import Wire.API.User (Name (fromName))
import Wire.API.User.Search
import Wire.IndexedUserStore (IndexedUserStoreError (..))
import Wire.IndexedUserStore.ElasticSearch (mappingName)
@@ -80,7 +78,7 @@ queryIndex (IndexQuery q f _) s = do
r <-
ES.searchByType idx mappingName search
>>= ES.parseEsResponse @_ @(ES.SearchResult UserDoc)
- either (throwM . IndexLookupError) (traverse (userDocToContact localDomain) . mkResult) r
+ either (throwM . IndexLookupError) (traverse (userDocToContact' localDomain) . mkResult) r
where
mkResult es =
let results = mapMaybe ES.hitSource . ES.hits . ES.searchHits $ es
@@ -94,14 +92,12 @@ queryIndex (IndexQuery q f _) s = do
searchHasMore = Nothing
}
-userDocToContact :: (MonadThrow m) => Domain -> UserDoc -> m Contact
-userDocToContact localDomain UserDoc {..} = do
- let contactQualifiedId = Qualified udId localDomain
- contactName <- maybe (throwM $ IndexError "Name not found") (pure . fromName) udName
- let contactColorId = fromIntegral . fromColourId <$> udColourId
- contactHandle = fromHandle <$> udHandle
- contactTeam = udTeam
- pure $ Contact {..}
+ userDocToContact' :: (MonadThrow m) => Domain -> UserDoc -> m Contact
+ userDocToContact' localDomain userDoc = do
+ userDocToContact
+ (Qualified userDoc.udId localDomain)
+ (maybe (throwM $ IndexError "Name not found") (pure . fromName))
+ userDoc
-- | The default or canonical 'IndexQuery'.
--
diff --git a/services/brig/test/integration/API/Search.hs b/services/brig/test/integration/API/Search.hs
index a4809993f0..1e87cb4b7a 100644
--- a/services/brig/test/integration/API/Search.hs
+++ b/services/brig/test/integration/API/Search.hs
@@ -40,7 +40,7 @@ import Brig.App (initHttpManagerWithTLSConfig)
import Brig.Index.Eval (initIndex, runCommand)
import Brig.Index.Options
import Brig.Index.Options qualified as IndexOpts
-import Brig.Options (ElasticSearchOpts)
+import Brig.Options
import Brig.Options qualified as Opt
import Brig.Options qualified as Opts
import Brig.User.Search.Index
diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs
index 41b9f42af3..be2b1c1627 100644
--- a/services/federator/test/integration/Test/Federator/IngressSpec.hs
+++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs
@@ -59,7 +59,7 @@ spec env = do
brig <- view teBrig <$> ask
user <- randomUser brig
- let expectedProfile = mkUserProfile EmailVisibleToSelf user UserLegalHoldNoConsent
+ let expectedProfile = mkUserProfile EmailVisibleToSelf UserTypeRegular user UserLegalHoldNoConsent
runTestSem $ do
resp <-
liftToCodensity
diff --git a/services/federator/test/integration/Test/Federator/InwardSpec.hs b/services/federator/test/integration/Test/Federator/InwardSpec.hs
index 1daee8e77e..180d41b981 100644
--- a/services/federator/test/integration/Test/Federator/InwardSpec.hs
+++ b/services/federator/test/integration/Test/Federator/InwardSpec.hs
@@ -70,7 +70,7 @@ spec env =
brig <- view teBrig <$> ask
user <- randomUser brig
- let expectedProfile = mkUserProfile EmailVisibleToSelf user UserLegalHoldNoConsent
+ let expectedProfile = mkUserProfile EmailVisibleToSelf UserTypeRegular user UserLegalHoldNoConsent
bdy <-
responseJsonError
=<< inwardCall "/federation/brig/get-users-by-ids" (encode [userId user])
From c894071aa18d7adcd548ee3105ca22873fc33827 Mon Sep 17 00:00:00 2001
From: Leif Battermann
Date: Wed, 14 Jan 2026 11:53:26 +0100
Subject: [PATCH 03/26] WPB-19804 Email template validation (#4699)
---
changelog.d/5-internal/WPB-9935 | 3 +
hack/helm_vars/wire-server/values.yaml.gotmpl | 11 +
integration/test/Test/Teams.hs | 2 +-
.../src/Wire/EmailSubsystem.hs | 2 +
.../src/Wire/EmailSubsystem/Interpreter.hs | 580 +++++++++++-------
.../src/Wire/EmailSubsystem/Template.hs | 195 +-----
.../src/Wire/EmailSubsystem/Templates/Team.hs | 58 ++
.../src/Wire/EmailSubsystem/Templates/User.hs | 98 +++
.../templates/ar/user/call/activation.txt | 1 -
.../templates/ar/user/call/login.txt | 1 -
.../templates/ar/user/sms/activation.txt | 3 -
.../templates/ar/user/sms/deletion.txt | 2 -
.../templates/ar/user/sms/login.txt | 3 -
.../templates/ar/user/sms/password-reset.txt | 3 -
.../email/new-creator-welcome-subject.txt | 1 -
.../de/team/email/new-creator-welcome.html | 1 -
.../de/team/email/new-creator-welcome.txt | 24 -
.../templates/de/user/call/activation.txt | 1 -
.../templates/de/user/call/login.txt | 1 -
.../templates/de/user/sms/activation.txt | 3 -
.../templates/de/user/sms/deletion.txt | 2 -
.../templates/de/user/sms/login.txt | 4 -
.../templates/de/user/sms/password-reset.txt | 3 -
.../email/new-creator-welcome-subject.txt | 1 -
.../en/team/email/new-creator-welcome.html | 1 -
.../en/team/email/new-creator-welcome.txt | 23 -
.../templates/en/user/call/activation.txt | 1 -
.../templates/en/user/call/login.txt | 1 -
.../templates/en/user/sms/activation.txt | 3 -
.../templates/en/user/sms/deletion.txt | 2 -
.../templates/en/user/sms/login.txt | 3 -
.../templates/en/user/sms/password-reset.txt | 3 -
.../templates/es-ES/user/call/activation.txt | 1 -
.../templates/es-ES/user/call/login.txt | 1 -
.../templates/es-ES/user/sms/activation.txt | 3 -
.../templates/es-ES/user/sms/deletion.txt | 2 -
.../templates/es-ES/user/sms/login.txt | 3 -
.../es-ES/user/sms/password-reset.txt | 3 -
.../templates/et/user/call/activation.txt | 1 -
.../templates/et/user/call/login.txt | 1 -
.../templates/et/user/sms/activation.txt | 3 -
.../templates/et/user/sms/deletion.txt | 2 -
.../templates/et/user/sms/login.txt | 3 -
.../templates/et/user/sms/password-reset.txt | 3 -
.../templates/fa/user/call/activation.txt | 1 -
.../templates/fa/user/call/login.txt | 1 -
.../templates/fa/user/sms/activation.txt | 3 -
.../templates/fa/user/sms/deletion.txt | 2 -
.../templates/fa/user/sms/login.txt | 3 -
.../templates/fa/user/sms/password-reset.txt | 3 -
.../templates/fr/user/call/activation.txt | 1 -
.../templates/fr/user/call/login.txt | 1 -
.../templates/fr/user/sms/activation.txt | 3 -
.../templates/fr/user/sms/deletion.txt | 2 -
.../templates/fr/user/sms/login.txt | 3 -
.../templates/fr/user/sms/password-reset.txt | 3 -
.../templates/it/user/call/activation.txt | 1 -
.../templates/it/user/call/login.txt | 1 -
.../templates/it/user/sms/activation.txt | 3 -
.../templates/it/user/sms/deletion.txt | 2 -
.../templates/it/user/sms/login.txt | 3 -
.../templates/it/user/sms/password-reset.txt | 3 -
.../templates/ja/user/call/activation.txt | 1 -
.../templates/ja/user/call/login.txt | 1 -
.../templates/ja/user/sms/activation.txt | 3 -
.../templates/ja/user/sms/deletion.txt | 2 -
.../templates/ja/user/sms/login.txt | 3 -
.../templates/ja/user/sms/password-reset.txt | 3 -
.../templates/lt/user/call/activation.txt | 1 -
.../templates/lt/user/call/login.txt | 1 -
.../templates/lt/user/sms/activation.txt | 3 -
.../templates/lt/user/sms/deletion.txt | 2 -
.../templates/lt/user/sms/login.txt | 3 -
.../templates/lt/user/sms/password-reset.txt | 3 -
.../templates/pl/user/call/activation.txt | 1 -
.../templates/pl/user/call/login.txt | 1 -
.../templates/pl/user/sms/activation.txt | 3 -
.../templates/pl/user/sms/deletion.txt | 2 -
.../templates/pl/user/sms/login.txt | 3 -
.../templates/pl/user/sms/password-reset.txt | 3 -
.../templates/pt-BR/user/call/activation.txt | 1 -
.../templates/pt-BR/user/call/login.txt | 1 -
.../templates/pt-BR/user/sms/activation.txt | 3 -
.../templates/pt-BR/user/sms/deletion.txt | 2 -
.../templates/pt-BR/user/sms/login.txt | 3 -
.../pt-BR/user/sms/password-reset.txt | 3 -
.../templates/pt/user/call/activation.txt | 1 -
.../templates/pt/user/call/login.txt | 1 -
.../templates/pt/user/sms/activation.txt | 3 -
.../templates/pt/user/sms/deletion.txt | 2 -
.../templates/pt/user/sms/login.txt | 3 -
.../templates/pt/user/sms/password-reset.txt | 3 -
.../templates/ru/user/call/activation.txt | 1 -
.../templates/ru/user/call/login.txt | 1 -
.../templates/ru/user/sms/activation.txt | 3 -
.../templates/ru/user/sms/deletion.txt | 2 -
.../templates/ru/user/sms/login.txt | 3 -
.../templates/ru/user/sms/password-reset.txt | 3 -
.../templates/si/user/call/activation.txt | 1 -
.../templates/si/user/call/login.txt | 1 -
.../templates/si/user/sms/activation.txt | 3 -
.../templates/si/user/sms/deletion.txt | 2 -
.../templates/si/user/sms/login.txt | 3 -
.../templates/si/user/sms/password-reset.txt | 3 -
.../templates/sv-SE/user/call/activation.txt | 1 -
.../templates/sv-SE/user/call/login.txt | 1 -
.../templates/sv-SE/user/sms/activation.txt | 3 -
.../templates/sv-SE/user/sms/deletion.txt | 2 -
.../templates/sv-SE/user/sms/login.txt | 3 -
.../sv-SE/user/sms/password-reset.txt | 3 -
.../templates/tr/user/call/activation.txt | 1 -
.../templates/tr/user/call/login.txt | 1 -
.../templates/tr/user/sms/activation.txt | 3 -
.../templates/tr/user/sms/deletion.txt | 2 -
.../templates/tr/user/sms/login.txt | 3 -
.../templates/tr/user/sms/password-reset.txt | 3 -
.../templates/uk/user/call/activation.txt | 1 -
.../templates/uk/user/call/login.txt | 1 -
.../templates/uk/user/sms/activation.txt | 3 -
.../templates/uk/user/sms/deletion.txt | 2 -
.../templates/uk/user/sms/login.txt | 3 -
.../templates/uk/user/sms/password-reset.txt | 3 -
.../templates/vi/user/call/activation.txt | 1 -
.../templates/vi/user/call/login.txt | 1 -
.../templates/vi/user/sms/activation.txt | 3 -
.../templates/vi/user/sms/deletion.txt | 2 -
.../templates/vi/user/sms/login.txt | 3 -
.../templates/vi/user/sms/password-reset.txt | 3 -
.../templates/zh-TW/user/call/activation.txt | 1 -
.../templates/zh-TW/user/call/login.txt | 1 -
.../templates/zh-TW/user/sms/activation.txt | 3 -
.../templates/zh-TW/user/sms/deletion.txt | 2 -
.../templates/zh-TW/user/sms/login.txt | 3 -
.../zh-TW/user/sms/password-reset.txt | 3 -
.../Wire/MockInterpreters/EmailSubsystem.hs | 2 +
libs/wire-subsystems/wire-subsystems.cabal | 2 +
services/brig/brig.cabal | 2 +-
services/brig/brig.integration.yaml | 24 +-
services/brig/src/Brig/API/Public.hs | 14 +-
services/brig/src/Brig/API/User.hs | 17 +-
services/brig/src/Brig/App.hs | 23 +-
.../brig/src/Brig/CanonicalInterpreter.hs | 8 +-
services/brig/src/Brig/Provider/Email.hs | 1 +
services/brig/src/Brig/Provider/Template.hs | 16 +-
services/brig/src/Brig/Team/API.hs | 21 +-
services/brig/src/Brig/Team/Email.hs | 98 ---
services/brig/src/Brig/Team/Template.hs | 14 +-
services/brig/src/Brig/Template.hs | 29 +-
services/brig/src/Brig/User/Template.hs | 51 +-
services/brig/test/integration/API/Team.hs | 4 +-
.../brig/test/integration/API/Template.hs | 235 +++++++
services/brig/test/integration/Run.hs | 5 +-
152 files changed, 886 insertions(+), 941 deletions(-)
create mode 100644 changelog.d/5-internal/WPB-9935
create mode 100644 libs/wire-subsystems/src/Wire/EmailSubsystem/Templates/Team.hs
create mode 100644 libs/wire-subsystems/src/Wire/EmailSubsystem/Templates/User.hs
delete mode 100644 libs/wire-subsystems/templates/ar/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/ar/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/ar/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/ar/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/ar/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/ar/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/de/team/email/new-creator-welcome-subject.txt
delete mode 100644 libs/wire-subsystems/templates/de/team/email/new-creator-welcome.html
delete mode 100644 libs/wire-subsystems/templates/de/team/email/new-creator-welcome.txt
delete mode 100644 libs/wire-subsystems/templates/de/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/de/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/de/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/de/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/de/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/de/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/en/team/email/new-creator-welcome-subject.txt
delete mode 100644 libs/wire-subsystems/templates/en/team/email/new-creator-welcome.html
delete mode 100644 libs/wire-subsystems/templates/en/team/email/new-creator-welcome.txt
delete mode 100644 libs/wire-subsystems/templates/en/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/en/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/en/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/en/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/en/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/en/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/es-ES/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/es-ES/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/es-ES/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/es-ES/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/es-ES/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/es-ES/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/et/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/et/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/et/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/et/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/et/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/et/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/fa/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/fa/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/fa/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/fa/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/fa/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/fa/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/fr/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/fr/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/fr/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/fr/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/fr/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/fr/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/it/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/it/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/it/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/it/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/it/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/it/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/ja/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/ja/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/ja/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/ja/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/ja/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/ja/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/lt/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/lt/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/lt/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/lt/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/lt/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/lt/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/pl/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/pl/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/pl/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/pl/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/pl/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/pl/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/pt-BR/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/pt-BR/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/pt-BR/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/pt-BR/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/pt-BR/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/pt-BR/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/pt/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/pt/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/pt/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/pt/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/pt/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/pt/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/ru/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/ru/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/ru/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/ru/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/ru/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/ru/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/si/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/si/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/si/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/si/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/si/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/si/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/sv-SE/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/sv-SE/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/sv-SE/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/sv-SE/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/sv-SE/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/sv-SE/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/tr/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/tr/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/tr/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/tr/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/tr/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/tr/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/uk/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/uk/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/uk/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/uk/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/uk/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/uk/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/vi/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/vi/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/vi/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/vi/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/vi/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/vi/user/sms/password-reset.txt
delete mode 100644 libs/wire-subsystems/templates/zh-TW/user/call/activation.txt
delete mode 100644 libs/wire-subsystems/templates/zh-TW/user/call/login.txt
delete mode 100644 libs/wire-subsystems/templates/zh-TW/user/sms/activation.txt
delete mode 100644 libs/wire-subsystems/templates/zh-TW/user/sms/deletion.txt
delete mode 100644 libs/wire-subsystems/templates/zh-TW/user/sms/login.txt
delete mode 100644 libs/wire-subsystems/templates/zh-TW/user/sms/password-reset.txt
delete mode 100644 services/brig/src/Brig/Team/Email.hs
create mode 100644 services/brig/test/integration/API/Template.hs
diff --git a/changelog.d/5-internal/WPB-9935 b/changelog.d/5-internal/WPB-9935
new file mode 100644
index 0000000000..13874f33f7
--- /dev/null
+++ b/changelog.d/5-internal/WPB-9935
@@ -0,0 +1,3 @@
+- Test for team and user email templates added
+- Refactoring to make email rendering testable
+- Removed SMS and call templates
diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl
index 490c70deb6..ec4ea1670a 100644
--- a/hack/helm_vars/wire-server/values.yaml.gotmpl
+++ b/hack/helm_vars/wire-server/values.yaml.gotmpl
@@ -164,6 +164,17 @@ brig:
general:
emailSender: backend-integrationk8s@wire.com
smsSender: dummy
+ user:
+ activationUrl: https://example.com/verify/?key=${key}&code=${code}
+ smsActivationUrl: https://example.com/v/${code}
+ passwordResetUrl: https://example.com/reset/?key=${key}&code=${code}
+ deletionUrl: https://example.com/d/?key=${key}&code=${code}
+ team:
+ tInvitationUrl: https://example.com/join/?team-code=${code}
+ tExistingUserInvitationUrl: https://example.com/accept-invitation/?team-code=${code}
+ tActivationUrl: https://example.com/verify/?key=${key}&code=${code}
+ tCreatorWelcomeUrl: https://example.com/login
+ tMemberWelcomeUrl: https://example.com/download
test:
elasticsearch:
additionalHost: {{ .Values.elasticsearch.additionalHost }}
diff --git a/integration/test/Test/Teams.hs b/integration/test/Test/Teams.hs
index 0066ef7017..f8e91c5b8a 100644
--- a/integration/test/Test/Teams.hs
+++ b/integration/test/Test/Teams.hs
@@ -141,7 +141,7 @@ testInvitePersonalUserToTeam = do
-- 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"
+ newUserInvUrl `shouldContainString` "/join"
personalUserInv <- invitations & findM (\i -> (i %. "email" >>= asString) <&> (== email))
personalUserInvUrl <- personalUserInv %. "url" & asString
diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs
index 837ed89a1d..fa9f1bc765 100644
--- a/libs/wire-subsystems/src/Wire/EmailSubsystem.hs
+++ b/libs/wire-subsystems/src/Wire/EmailSubsystem.hs
@@ -43,5 +43,7 @@ data EmailSubsystem m a where
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
+ SendMemberWelcomeEmail :: EmailAddress -> TeamId -> Text -> Maybe Locale -> EmailSubsystem m ()
+ SendNewTeamOwnerWelcomeEmail :: EmailAddress -> TeamId -> Text -> Maybe Locale -> Name -> 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 58206ef46d..3225190c16 100644
--- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs
+++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Interpreter.hs
@@ -17,23 +17,22 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
-module Wire.EmailSubsystem.Interpreter
- ( emailSubsystemInterpreter,
- mkMimeAddress,
- renderInvitationUrl,
- )
-where
+module Wire.EmailSubsystem.Interpreter where
import Data.Code qualified as Code
import Data.Id
import Data.Json.Util
+import Data.Map as Map
import Data.Range (fromRange)
import Data.Text qualified as Text
import Data.Text.Ascii qualified as Ascii
import Data.Text.Lazy (toStrict)
+import Data.Text.Template
import Imports
import Network.Mail.Mime
import Polysemy
+import Polysemy.Output (Output)
+import Polysemy.TinyLog (TinyLog)
import Wire.API.Locale
import Wire.API.User
import Wire.API.User.Activation
@@ -42,9 +41,17 @@ import Wire.API.User.Password
import Wire.EmailSending (EmailSending, sendMail)
import Wire.EmailSubsystem
import Wire.EmailSubsystem.Template
+import Wire.EmailSubsystem.Templates.Team
+import Wire.EmailSubsystem.Templates.User
-emailSubsystemInterpreter :: (Member EmailSending r) => Localised UserTemplates -> Localised TeamTemplates -> TemplateBranding -> InterpreterFor EmailSubsystem r
+emailSubsystemInterpreter ::
+ (Member EmailSending r, Member TinyLog r) =>
+ Localised UserTemplates ->
+ Localised TeamTemplates ->
+ Map Text Text ->
+ InterpreterFor EmailSubsystem r
emailSubsystemInterpreter userTpls teamTpls branding = interpret \case
+ -- USER EMAILS
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
@@ -55,8 +62,11 @@ 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
+ -- TEAM EMAILS
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
+ SendMemberWelcomeEmail email tid teamName loc -> sendMemberWelcomeEmailImpl teamTpls branding email tid teamName loc
+ SendNewTeamOwnerWelcomeEmail email tid teamName loc name -> sendNewTeamOwnerWelcomeEmailImpl teamTpls branding email tid teamName loc name
-------------------------------------------------------------------------------
-- Verification Email for
@@ -65,76 +75,81 @@ emailSubsystemInterpreter userTpls teamTpls branding = interpret \case
-- - Team Deletion
sendTeamDeletionVerificationMailImpl ::
- (Member EmailSending r) =>
+ (Member EmailSending r, Member TinyLog r) =>
Localised UserTemplates ->
- TemplateBranding ->
+ Map Text Text ->
EmailAddress ->
Code.Value ->
Maybe Locale ->
Sem r ()
sendTeamDeletionVerificationMailImpl userTemplates branding email code mLocale = do
let tpl = verificationTeamDeletionEmail . snd $ forLocale mLocale userTemplates
- sendMail $ renderSecondFactorVerificationEmail email code tpl branding
+ mail <- logEmailRenderErrors "team deletion verification email" $ renderSecondFactorVerificationEmail email code tpl branding
+ sendMail mail
sendCreateScimTokenVerificationMailImpl ::
- (Member EmailSending r) =>
+ (Member EmailSending r, Member TinyLog r) =>
Localised UserTemplates ->
- TemplateBranding ->
+ Map Text Text ->
EmailAddress ->
Code.Value ->
Maybe Locale ->
Sem r ()
sendCreateScimTokenVerificationMailImpl userTemplates branding email code mLocale = do
let tpl = verificationScimTokenEmail . snd $ forLocale mLocale userTemplates
- sendMail $ renderSecondFactorVerificationEmail email code tpl branding
+ mail <- logEmailRenderErrors "scim token verification email" $ renderSecondFactorVerificationEmail email code tpl branding
+ sendMail mail
sendLoginVerificationMailImpl ::
- (Member EmailSending r) =>
+ (Member EmailSending r, Member TinyLog r) =>
Localised UserTemplates ->
- TemplateBranding ->
+ Map Text Text ->
EmailAddress ->
Code.Value ->
Maybe Locale ->
Sem r ()
sendLoginVerificationMailImpl userTemplates branding email code mLocale = do
let tpl = verificationLoginEmail . snd $ forLocale mLocale userTemplates
- sendMail $ renderSecondFactorVerificationEmail email code tpl branding
+ mail <- logEmailRenderErrors "login verification email" $ renderSecondFactorVerificationEmail email code tpl branding
+ sendMail mail
renderSecondFactorVerificationEmail ::
+ (Member (Output Text) r) =>
EmailAddress ->
Code.Value ->
SecondFactorVerificationEmailTemplate ->
- TemplateBranding ->
- Mail
-renderSecondFactorVerificationEmail email codeValue SecondFactorVerificationEmailTemplate {..} branding =
- (emptyMail from)
- { mailTo = [to],
- mailHeaders =
- [ ("Subject", toStrict subj),
- ("X-Zeta-Purpose", "SecondFactorVerification"),
- ("X-Zeta-Code", code)
- ],
- mailParts = [[plainPart txt, htmlPart html]]
- }
+ Map Text Text ->
+ Sem r Mail
+renderSecondFactorVerificationEmail email codeValue SecondFactorVerificationEmailTemplate {..} branding = do
+ let replace =
+ branding
+ & Map.insert "email" (fromEmail email)
+ & Map.insert "code" code
+ txt <- renderTextWithBrandingSem sndFactorVerificationEmailBodyText replace
+ html <- renderHtmlWithBrandingSem sndFactorVerificationEmailBodyHtml replace
+ subj <- renderTextWithBrandingSem sndFactorVerificationEmailSubject replace
+ pure
+ (emptyMail from)
+ { mailTo = [to],
+ mailHeaders =
+ [ ("Subject", toStrict subj),
+ ("X-Zeta-Purpose", "SecondFactorVerification"),
+ ("X-Zeta-Code", code)
+ ],
+ mailParts = [[plainPart txt, htmlPart html]]
+ }
where
from = Address (Just sndFactorVerificationEmailSenderName) (fromEmail sndFactorVerificationEmailSender)
to = Address Nothing (fromEmail email)
- txt = renderTextWithBranding sndFactorVerificationEmailBodyText replace branding
- html = renderHtmlWithBranding sndFactorVerificationEmailBodyHtml replace branding
- subj = renderTextWithBranding sndFactorVerificationEmailSubject replace branding
code = Ascii.toText (fromRange codeValue.asciiValue)
- replace :: Text -> Text
- replace "email" = fromEmail email
- replace "code" = code
- replace x = x
-------------------------------------------------------------------------------
-- Activation Email
sendActivationMailImpl ::
- (Member EmailSending r) =>
+ (Member EmailSending r, Member TinyLog r) =>
Localised UserTemplates ->
- TemplateBranding ->
+ Map Text Text ->
EmailAddress ->
Name ->
ActivationKey ->
@@ -143,12 +158,13 @@ sendActivationMailImpl ::
Sem r ()
sendActivationMailImpl userTemplates branding email name akey acode mLocale = do
let tpl = activationEmail . snd $ forLocale mLocale userTemplates
- sendMail $ renderActivationMail email name akey acode tpl branding
+ mail <- logEmailRenderErrors "activation email" $ renderActivationMail email name akey acode tpl branding
+ sendMail mail
sendEmailAddressUpdateMailImpl ::
- (Member EmailSending r) =>
+ (Member EmailSending r, Member TinyLog r) =>
Localised UserTemplates ->
- TemplateBranding ->
+ Map Text Text ->
EmailAddress ->
Name ->
ActivationKey ->
@@ -157,54 +173,52 @@ sendEmailAddressUpdateMailImpl ::
Sem r ()
sendEmailAddressUpdateMailImpl userTemplates branding email name akey acode mLocale = do
let tpl = activationEmailUpdate . snd $ forLocale mLocale userTemplates
- sendMail $ renderActivationMail email name akey acode tpl branding
-
-renderActivationMail :: EmailAddress -> Name -> ActivationKey -> ActivationCode -> ActivationEmailTemplate -> TemplateBranding -> Mail
-renderActivationMail email name akey@(ActivationKey key) acode@(ActivationCode code) ActivationEmailTemplate {..} branding =
- (emptyMail from)
- { mailTo = [to],
- -- To make automated processing possible, the activation code is also added to
- -- headers. {#RefActivationEmailHeaders}
- mailHeaders =
- [ ("Subject", toStrict subj),
- ("X-Zeta-Purpose", "Activation"),
- ("X-Zeta-Key", Ascii.toText key),
- ("X-Zeta-Code", Ascii.toText code)
- ],
- mailParts = [[plainPart txt, htmlPart html]]
- }
+ mail <- logEmailRenderErrors "email address update email" $ renderActivationMail email name akey acode tpl branding
+ sendMail mail
+
+renderActivationMail :: (Member (Output Text) r) => EmailAddress -> Name -> ActivationKey -> ActivationCode -> ActivationEmailTemplate -> Map Text Text -> Sem r Mail
+renderActivationMail email name akey@(ActivationKey key) acode@(ActivationCode code) ActivationEmailTemplate {..} branding = do
+ url <- renderActivationUrl activationEmailUrl akey acode branding
+ let replace =
+ branding
+ & Map.insert "url" url
+ & Map.insert "email" (fromEmail email)
+ & Map.insert "name" (fromName name)
+ txt <- renderTextWithBrandingSem activationEmailBodyText replace
+ html <- renderHtmlWithBrandingSem activationEmailBodyHtml replace
+ subj <- renderTextWithBrandingSem activationEmailSubject replace
+ pure
+ (emptyMail from)
+ { mailTo = [to],
+ -- To make automated processing possible, the activation code is also added to
+ -- headers. {#RefActivationEmailHeaders}
+ mailHeaders =
+ [ ("Subject", toStrict subj),
+ ("X-Zeta-Purpose", "Activation"),
+ ("X-Zeta-Key", Ascii.toText key),
+ ("X-Zeta-Code", Ascii.toText code)
+ ],
+ mailParts = [[plainPart txt, htmlPart html]]
+ }
where
- from, to :: Address
from = Address (Just activationEmailSenderName) (fromEmail activationEmailSender)
to = mkMimeAddress name email
- txt, html, subj :: LText
- txt = renderTextWithBranding activationEmailBodyText replace branding
- html = renderHtmlWithBranding activationEmailBodyHtml replace branding
- subj = renderTextWithBranding activationEmailSubject replace branding
-
- replace :: Text -> Text
- replace "url" = renderActivationUrl activationEmailUrl akey acode branding
- replace "email" = fromEmail email
- replace "name" = fromName name
- replace x = x
-
-renderActivationUrl :: Template -> ActivationKey -> ActivationCode -> TemplateBranding -> Text
-renderActivationUrl t (ActivationKey k) (ActivationCode c) branding =
- toStrict $ renderTextWithBranding t replace branding
- where
- replace :: Text -> Text
- replace "key" = Ascii.toText k
- replace "code" = Ascii.toText c
- replace x = x
+renderActivationUrl :: (Member (Output Text) r) => Template -> ActivationKey -> ActivationCode -> Map Text Text -> Sem r Text
+renderActivationUrl t (ActivationKey k) (ActivationCode c) branding = do
+ let replace =
+ branding
+ & Map.insert "key" (Ascii.toText k)
+ & Map.insert "code" (Ascii.toText c)
+ toStrict <$> renderTextWithBrandingSem t replace
-------------------------------------------------------------------------------
-- Team Activation Email
sendTeamActivationMailImpl ::
- (Member EmailSending r) =>
+ (Member EmailSending r, Member TinyLog r) =>
Localised UserTemplates ->
- TemplateBranding ->
+ Map Text Text ->
EmailAddress ->
Name ->
ActivationKey ->
@@ -214,42 +228,43 @@ sendTeamActivationMailImpl ::
Sem r ()
sendTeamActivationMailImpl userTemplates branding email name akey acode mLocale teamName = do
let tpl = teamActivationEmail . snd $ forLocale mLocale userTemplates
- sendMail $ renderTeamActivationMail email name teamName akey acode tpl branding
-
-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],
- mailHeaders =
- [ ("Subject", toStrict subj),
- ("X-Zeta-Purpose", "Activation"),
- ("X-Zeta-Key", Ascii.toText key),
- ("X-Zeta-Code", Ascii.toText code)
- ],
- mailParts = [[plainPart txt, htmlPart html]]
- }
+ mail <- logEmailRenderErrors "team activation email" $ renderTeamActivationMail email name teamName akey acode tpl branding
+ sendMail mail
+
+renderTeamActivationMail :: (Member (Output Text) r) => EmailAddress -> Name -> Text -> ActivationKey -> ActivationCode -> TeamActivationEmailTemplate -> Map Text Text -> Sem r Mail
+renderTeamActivationMail email name teamName akey@(ActivationKey key) acode@(ActivationCode code) TeamActivationEmailTemplate {..} branding = do
+ url <- renderActivationUrl teamActivationEmailUrl akey acode branding
+ let replace =
+ branding
+ & Map.insert "url" url
+ & Map.insert "email" (fromEmail email)
+ & Map.insert "name" (fromName name)
+ & Map.insert "team" teamName
+ txt <- renderTextWithBrandingSem teamActivationEmailBodyText replace
+ html <- renderHtmlWithBrandingSem teamActivationEmailBodyHtml replace
+ subj <- renderTextWithBrandingSem teamActivationEmailSubject replace
+ pure
+ (emptyMail from)
+ { mailTo = [to],
+ mailHeaders =
+ [ ("Subject", toStrict subj),
+ ("X-Zeta-Purpose", "Activation"),
+ ("X-Zeta-Key", Ascii.toText key),
+ ("X-Zeta-Code", Ascii.toText code)
+ ],
+ mailParts = [[plainPart txt, htmlPart html]]
+ }
where
- from, to :: Address
from = Address (Just teamActivationEmailSenderName) (fromEmail teamActivationEmailSender)
to = mkMimeAddress name email
- txt, html, subj :: LText
- txt = renderTextWithBranding teamActivationEmailBodyText replace branding
- html = renderHtmlWithBranding teamActivationEmailBodyHtml replace branding
- subj = renderTextWithBranding teamActivationEmailSubject replace branding
- replace :: Text -> Text
- replace "url" = renderActivationUrl teamActivationEmailUrl akey acode branding
- replace "email" = fromEmail email
- replace "name" = fromName name
- replace "team" = teamName
- replace x = x
-------------------------------------------------------------------------------
-- Verification Email
sendVerificationMailImpl ::
- (Member EmailSending r) =>
+ (Member EmailSending r, Member TinyLog r) =>
Localised UserTemplates ->
- TemplateBranding ->
+ Map Text Text ->
EmailAddress ->
ActivationKey ->
ActivationCode ->
@@ -257,39 +272,42 @@ sendVerificationMailImpl ::
Sem r ()
sendVerificationMailImpl userTemplates branding email akey acode mLocale = do
let tpl = verificationEmail . snd $ forLocale mLocale userTemplates
- sendMail $ renderVerificationMail email akey acode tpl branding
-
-renderVerificationMail :: EmailAddress -> ActivationKey -> ActivationCode -> VerificationEmailTemplate -> TemplateBranding -> Mail
-renderVerificationMail email akey acode VerificationEmailTemplate {..} branding =
- (emptyMail from)
- { mailTo = [to],
- -- To make automated processing possible, the activation code is also added to
- -- headers. {#RefActivationEmailHeaders}
- mailHeaders =
- [ ("Subject", toStrict subj),
- ("X-Zeta-Purpose", "Verification"),
- ("X-Zeta-Code", Ascii.toText code)
- ],
- mailParts = [[plainPart txt, htmlPart html]]
- }
+ mail <- logEmailRenderErrors "verification email" $ renderVerificationMail email akey acode tpl branding
+ sendMail mail
+
+renderVerificationMail :: (Member (Output Text) r) => EmailAddress -> ActivationKey -> ActivationCode -> VerificationEmailTemplate -> Map Text Text -> Sem r Mail
+renderVerificationMail email akey acode VerificationEmailTemplate {..} branding = do
+ let replace =
+ branding
+ & Map.insert "code" (Ascii.toText code)
+ & Map.insert "email" (fromEmail email)
+ txt <- renderTextWithBrandingSem verificationEmailBodyText replace
+ html <- renderHtmlWithBrandingSem verificationEmailBodyHtml replace
+ subj <- renderTextWithBrandingSem verificationEmailSubject replace
+ pure
+ (emptyMail from)
+ { mailTo = [to],
+ -- To make automated processing possible, the activation code is also added to
+ -- headers. {#RefActivationEmailHeaders}
+ mailHeaders =
+ [ ("Subject", toStrict subj),
+ ("X-Zeta-Purpose", "Verification"),
+ ("X-Zeta-Code", Ascii.toText code)
+ ],
+ mailParts = [[plainPart txt, htmlPart html]]
+ }
where
(ActivationKey _, ActivationCode code) = (akey, acode)
from = Address (Just verificationEmailSenderName) (fromEmail verificationEmailSender)
to = Address Nothing (fromEmail email)
- txt = renderTextWithBranding verificationEmailBodyText replace branding
- html = renderHtmlWithBranding verificationEmailBodyHtml replace branding
- subj = renderTextWithBranding verificationEmailSubject replace branding
- replace "code" = Ascii.toText code
- replace "email" = fromEmail email
- replace x = x
-------------------------------------------------------------------------------
-- Password Reset Email
sendPasswordResetMailImpl ::
- (Member EmailSending r) =>
+ (Member EmailSending r, Member TinyLog r) =>
Localised UserTemplates ->
- TemplateBranding ->
+ Map Text Text ->
EmailAddress ->
PasswordResetKey ->
PasswordResetCode ->
@@ -297,45 +315,46 @@ sendPasswordResetMailImpl ::
Sem r ()
sendPasswordResetMailImpl userTemplates branding email pkey pcode mLocale = do
let tpl = passwordResetEmail . snd $ forLocale mLocale userTemplates
- sendMail $ renderPwResetMail email pkey pcode tpl branding
-
-renderPwResetMail :: EmailAddress -> PasswordResetKey -> PasswordResetCode -> PasswordResetEmailTemplate -> TemplateBranding -> Mail
-renderPwResetMail email pkey pcode PasswordResetEmailTemplate {..} branding =
- (emptyMail from)
- { mailTo = [to],
- mailHeaders =
- [ ("Subject", toStrict subj),
- ("X-Zeta-Purpose", "PasswordReset"),
- ("X-Zeta-Key", Ascii.toText key),
- ("X-Zeta-Code", Ascii.toText code)
- ],
- mailParts = [[plainPart txt, htmlPart html]]
- }
+ mail <- logEmailRenderErrors "password reset email" $ renderPwResetMail email pkey pcode tpl branding
+ sendMail mail
+
+renderPwResetMail :: (Member (Output Text) r) => EmailAddress -> PasswordResetKey -> PasswordResetCode -> PasswordResetEmailTemplate -> Map Text Text -> Sem r Mail
+renderPwResetMail email pkey pcode PasswordResetEmailTemplate {..} branding = do
+ url <- renderPwResetUrl passwordResetEmailUrl pkey pcode
+ let replace = branding & Map.insert "url" url
+ txt <- renderTextWithBrandingSem passwordResetEmailBodyText replace
+ html <- renderHtmlWithBrandingSem passwordResetEmailBodyHtml replace
+ subj <- renderTextWithBrandingSem passwordResetEmailSubject replace
+ pure
+ (emptyMail from)
+ { mailTo = [to],
+ mailHeaders =
+ [ ("Subject", toStrict subj),
+ ("X-Zeta-Purpose", "PasswordReset"),
+ ("X-Zeta-Key", Ascii.toText key),
+ ("X-Zeta-Code", Ascii.toText code)
+ ],
+ mailParts = [[plainPart txt, htmlPart html]]
+ }
where
(PasswordResetKey key, PasswordResetCode code) = (pkey, pcode)
from = Address (Just passwordResetEmailSenderName) (fromEmail passwordResetEmailSender)
to = Address Nothing (fromEmail email)
- txt = renderTextWithBranding passwordResetEmailBodyText replace branding
- html = renderHtmlWithBranding passwordResetEmailBodyHtml replace branding
- subj = renderTextWithBranding passwordResetEmailSubject replace branding
- replace "url" = renderPwResetUrl passwordResetEmailUrl (pkey, pcode) branding
- replace x = x
-
-renderPwResetUrl :: Template -> PasswordResetPair -> TemplateBranding -> Text
-renderPwResetUrl t (PasswordResetKey k, PasswordResetCode c) branding =
- toStrict $ renderTextWithBranding t replace branding
- where
- replace "key" = Ascii.toText k
- replace "code" = Ascii.toText c
- replace x = x
+
+ renderPwResetUrl t (PasswordResetKey k) (PasswordResetCode c) = do
+ let replace =
+ branding
+ & Map.insert "key" (Ascii.toText k)
+ & Map.insert "code" (Ascii.toText c)
+ toStrict <$> renderTextWithBrandingSem t replace
-------------------------------------------------------------------------------
-- New Client Email
sendNewClientEmailImpl ::
- (Member EmailSending r) =>
+ (Member EmailSending r, Member TinyLog r) =>
Localised UserTemplates ->
- TemplateBranding ->
+ Map Text Text ->
EmailAddress ->
Name ->
Client ->
@@ -343,41 +362,45 @@ sendNewClientEmailImpl ::
Sem r ()
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 :: EmailAddress -> Name -> Locale -> Client -> NewClientEmailTemplate -> TemplateBranding -> Mail
-renderNewClientEmail email name locale Client {..} NewClientEmailTemplate {..} branding =
- (emptyMail from)
- { mailTo = [to],
- mailHeaders =
- [ ("Subject", toStrict subj),
- ("X-Zeta-Purpose", "NewDevice")
- ],
- mailParts = [[plainPart txt, htmlPart html]]
- }
+ mail <- logEmailRenderErrors "new client email" $ renderNewClientEmail email name locale client tpl branding
+ sendMail mail
+
+renderNewClientEmail :: (Member (Output Text) r) => EmailAddress -> Name -> Locale -> Client -> NewClientEmailTemplate -> Map Text Text -> Sem r Mail
+renderNewClientEmail email name locale Client {..} NewClientEmailTemplate {..} branding = do
+ let replace =
+ branding
+ & Map.insert "name" (fromName name)
+ & Map.insert "label" (fromMaybe defRequestId clientLabel)
+ & Map.insert "model" (fromMaybe defRequestId clientModel)
+ & Map.insert "date" formattedDate
+ txt <- renderTextWithBrandingSem newClientEmailBodyText replace
+ html <- renderHtmlWithBrandingSem newClientEmailBodyHtml replace
+ subj <- renderTextWithBrandingSem newClientEmailSubject replace
+ pure
+ (emptyMail from)
+ { mailTo = [to],
+ mailHeaders =
+ [ ("Subject", toStrict subj),
+ ("X-Zeta-Purpose", "NewDevice")
+ ],
+ mailParts = [[plainPart txt, htmlPart html]]
+ }
where
from = Address (Just newClientEmailSenderName) (fromEmail newClientEmailSender)
to = mkMimeAddress name email
- txt = renderTextWithBranding newClientEmailBodyText replace branding
- html = renderHtmlWithBranding newClientEmailBodyHtml replace branding
- subj = renderTextWithBranding newClientEmailSubject replace branding
- replace "name" = fromName name
- replace "label" = fromMaybe defRequestId clientLabel
- replace "model" = fromMaybe defRequestId clientModel
- replace "date" =
+ formattedDate =
formatDateTime
"%A %e %B %Y, %H:%M - %Z"
(timeLocale locale)
(fromUTCTimeMillis clientTime)
- replace x = x
-------------------------------------------------------------------------------
-- Deletion Email
sendAccountDeletionEmailImpl ::
- (Member EmailSending r) =>
+ (Member EmailSending r, Member TinyLog r) =>
Localised UserTemplates ->
- TemplateBranding ->
+ Map Text Text ->
EmailAddress ->
Name ->
Code.Key ->
@@ -386,54 +409,81 @@ sendAccountDeletionEmailImpl ::
Sem r ()
sendAccountDeletionEmailImpl userTemplates branding email name key code locale = do
let tpl = deletionEmail . snd $ forLocale (Just locale) userTemplates
- sendMail $ renderDeletionEmail email name key code tpl branding
-
-renderDeletionEmail :: EmailAddress -> Name -> Code.Key -> Code.Value -> DeletionEmailTemplate -> TemplateBranding -> Mail
-renderDeletionEmail email name cKey cValue DeletionEmailTemplate {..} branding =
- (emptyMail from)
- { mailTo = [to],
- mailHeaders =
- [ ("Subject", toStrict subj),
- ("X-Zeta-Purpose", "Delete"),
- ("X-Zeta-Key", key),
- ("X-Zeta-Code", code)
- ],
- mailParts = [[plainPart txt, htmlPart html]]
- }
+ mail <- logEmailRenderErrors "account deletion email" $ renderDeletionEmail email name key code tpl branding
+ sendMail mail
+
+renderDeletionEmail :: (Member (Output Text) r) => EmailAddress -> Name -> Code.Key -> Code.Value -> DeletionEmailTemplate -> Map Text Text -> Sem r Mail
+renderDeletionEmail email name cKey cValue DeletionEmailTemplate {..} branding = do
+ url <- renderDeletionUrl deletionEmailUrl cKey cValue branding
+ let replace =
+ branding
+ & Map.insert "url" url
+ & Map.insert "email" (fromEmail email)
+ & Map.insert "name" (fromName name)
+ txt <- renderTextWithBrandingSem deletionEmailBodyText replace
+ html <- renderHtmlWithBrandingSem deletionEmailBodyHtml replace
+ subj <- renderTextWithBrandingSem deletionEmailSubject replace
+ pure
+ (emptyMail from)
+ { mailTo = [to],
+ mailHeaders =
+ [ ("Subject", toStrict subj),
+ ("X-Zeta-Purpose", "Delete"),
+ ("X-Zeta-Key", key),
+ ("X-Zeta-Code", code)
+ ],
+ mailParts = [[plainPart txt, htmlPart html]]
+ }
where
from = Address (Just deletionEmailSenderName) (fromEmail deletionEmailSender)
to = mkMimeAddress name email
- txt = renderTextWithBranding deletionEmailBodyText replace1 branding
- html = renderHtmlWithBranding deletionEmailBodyHtml replace1 branding
- subj = renderTextWithBranding deletionEmailSubject replace1 branding
key = Ascii.toText (fromRange (Code.asciiKey cKey))
code = Ascii.toText (fromRange (Code.asciiValue cValue))
- replace1 "url" = toStrict (renderTextWithBranding deletionEmailUrl replace2 branding)
- replace1 "email" = fromEmail email
- replace1 "name" = fromName name
- replace1 x = x
- replace2 "key" = key
- replace2 "code" = code
- replace2 x = x
+
+renderDeletionUrl :: (Member (Output Text) r) => Template -> Code.Key -> Code.Value -> Map Text Text -> Sem r Text
+renderDeletionUrl t cKey cValue branding = do
+ let replace =
+ branding
+ & Map.insert "key" (Ascii.toText (fromRange (Code.asciiKey cKey)))
+ & Map.insert "code" (Ascii.toText (fromRange (Code.asciiValue cValue)))
+ toStrict <$> renderTextWithBrandingSem t replace
-------------------------------------------------------------------------------
-- Invitation Email
-sendTeamInvitationMailImpl :: (Member EmailSending r) => Localised TeamTemplates -> TemplateBranding -> EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> Sem r Text
+sendTeamInvitationMailImpl ::
+ (Member EmailSending r, Member TinyLog r) =>
+ Localised TeamTemplates ->
+ Map Text Text ->
+ 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
+ (renderedMail, renderedInvitationUrl) <- logEmailRenderErrors "invitation" $ renderInvitationEmail mail tpl branding
sendMail renderedMail
- pure renderedInvitaitonUrl
+ pure renderedInvitationUrl
-sendTeamInvitationMailPersonalUserImpl :: (Member EmailSending r) => Localised TeamTemplates -> TemplateBranding -> EmailAddress -> TeamId -> EmailAddress -> InvitationCode -> Maybe Locale -> Sem r Text
+sendTeamInvitationMailPersonalUserImpl ::
+ (Member EmailSending r, Member TinyLog r) =>
+ Localised TeamTemplates ->
+ Map Text Text ->
+ 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
+ (renderedMail, renderedInvitationUrl) <- logEmailRenderErrors "personal user invitation" $ renderInvitationEmail mail tpl branding
sendMail renderedMail
- pure renderedInvitaitonUrl
+ pure renderedInvitationUrl
data InvitationEmail = InvitationEmail
{ invTo :: !EmailAddress,
@@ -442,38 +492,100 @@ data InvitationEmail = InvitationEmail
invInviter :: !EmailAddress
}
-renderInvitationEmail :: InvitationEmail -> InvitationEmailTemplate -> TemplateBranding -> (Mail, Text)
-renderInvitationEmail InvitationEmail {..} InvitationEmailTemplate {..} branding =
- ( (emptyMail from)
+renderInvitationEmail :: (Member (Output Text) r) => InvitationEmail -> InvitationEmailTemplate -> Map Text Text -> Sem r (Mail, Text)
+renderInvitationEmail InvitationEmail {..} InvitationEmailTemplate {..} branding = do
+ invitationUrl <- renderInvitationUrl invitationEmailUrl invTeamId invInvCode
+ let replace = branding & Map.insert "inviter" (fromEmail invInviter) & Map.insert "url" invitationUrl
+ txt <- renderTextWithBrandingSem invitationEmailBodyText replace
+ html <- renderHtmlWithBrandingSem invitationEmailBodyHtml replace
+ subj <- renderTextWithBrandingSem invitationEmailSubject replace
+ pure
+ ( (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)
+
+renderInvitationUrl :: (Member (Output Text) r) => Template -> TeamId -> InvitationCode -> Sem r Text
+renderInvitationUrl t tid (InvitationCode c) =
+ toStrict <$> renderTextWithBrandingSem t (Map.fromList [("team", idToText tid), ("code", Ascii.toText c)])
+
+-------------------------------------------------------------------------------
+-- Member Welcome Email
+
+sendMemberWelcomeEmailImpl :: (Member EmailSending r, Member TinyLog r) => Localised TeamTemplates -> Map Text Text -> EmailAddress -> TeamId -> Text -> Maybe Locale -> Sem r ()
+sendMemberWelcomeEmailImpl teamTemplates branding to tid teamName loc = do
+ let tpl = memberWelcomeEmail . snd $ forLocale loc teamTemplates
+ mail <- logEmailRenderErrors "member welcome email" $ renderMemberWelcomeMail to tid teamName tpl branding
+ sendMail mail
+
+renderMemberWelcomeMail :: (Member (Output Text) r) => EmailAddress -> TeamId -> Text -> MemberWelcomeEmailTemplate -> Map Text Text -> Sem r Mail
+renderMemberWelcomeMail emailTo tid teamName MemberWelcomeEmailTemplate {..} branding = do
+ let replace =
+ branding
+ & Map.insert "url" memberWelcomeEmailUrl
+ & Map.insert "email" (fromEmail emailTo)
+ & Map.insert "team_id" (idToText tid)
+ & Map.insert "team_name" teamName
+ txt <- renderTextWithBrandingSem memberWelcomeEmailBodyText replace
+ html <- renderHtmlWithBrandingSem memberWelcomeEmailBodyHtml replace
+ subj <- renderTextWithBrandingSem memberWelcomeEmailSubject replace
+ pure
+ (emptyMail from)
{ mailTo = [to],
mailHeaders =
[ ("Subject", toStrict subj),
- ("X-Zeta-Purpose", "TeamInvitation"),
- ("X-Zeta-Code", Ascii.toText code)
+ ("X-Zeta-Purpose", "Welcome")
],
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
+ from = Address (Just memberWelcomeEmailSenderName) (fromEmail memberWelcomeEmailSender)
+ to = Address Nothing (fromEmail emailTo)
+
+-------------------------------------------------------------------------------
+-- New Team Owner Welcome Email
+
+sendNewTeamOwnerWelcomeEmailImpl :: (Member EmailSending r, Member TinyLog r) => Localised TeamTemplates -> Map Text Text -> EmailAddress -> TeamId -> Text -> Maybe Locale -> Name -> Sem r ()
+sendNewTeamOwnerWelcomeEmailImpl teamTemplates branding to tid teamName loc profileName = do
+ let tpl = newTeamOwnerWelcomeEmail . snd $ forLocale loc teamTemplates
+ mail <- logEmailRenderErrors "new team owner welcome email" $ renderNewTeamOwnerWelcomeEmail to tid teamName profileName tpl branding
+ sendMail mail
+
+renderNewTeamOwnerWelcomeEmail :: (Member (Output Text) r) => EmailAddress -> TeamId -> Text -> Name -> NewTeamOwnerWelcomeEmailTemplate -> Map Text Text -> Sem r Mail
+renderNewTeamOwnerWelcomeEmail emailTo tid teamName profileName NewTeamOwnerWelcomeEmailTemplate {..} branding = do
+ let replace =
+ branding
+ & Map.insert "url" newTeamOwnerWelcomeEmailUrl
+ & Map.insert "email" (fromEmail emailTo)
+ & Map.insert "team_id" (idToText tid)
+ & Map.insert "team_name" teamName
+ & Map.insert "name" profileName.fromName
+ txt <- renderTextWithBrandingSem newTeamOwnerWelcomeEmailBodyText replace
+ html <- renderHtmlWithBrandingSem newTeamOwnerWelcomeEmailBodyHtml replace
+ subj <- renderTextWithBrandingSem newTeamOwnerWelcomeEmailSubject replace
+ pure
+ (emptyMail from)
+ { mailTo = [to],
+ mailHeaders =
+ [ ("Subject", toStrict subj),
+ ("X-Zeta-Purpose", "Welcome")
+ ],
+ mailParts = [[plainPart txt, htmlPart html]]
+ }
where
- replace "team" = idToText tid
- replace "code" = Ascii.toText c
- replace x = x
+ from = Address (Just newTeamOwnerWelcomeEmailSenderName) (fromEmail newTeamOwnerWelcomeEmailSender)
+ to = Address Nothing (fromEmail emailTo)
-------------------------------------------------------------------------------
-- MIME Conversions
diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs
index 15a31a04eb..cfdecce899 100644
--- a/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs
+++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Template.hs
@@ -17,21 +17,20 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
-module Wire.EmailSubsystem.Template
- ( module Wire.EmailSubsystem.Template,
-
- -- * Re-exports
- Template,
- )
-where
+module Wire.EmailSubsystem.Template where
import Data.Map qualified as Map
+import Data.Text.Encoding qualified as T
import Data.Text.Lazy qualified as Lazy
import Data.Text.Template
import HTMLEntities.Text qualified as HTML
import Imports
+import Polysemy
+import Polysemy.Output
+import Polysemy.TinyLog (TinyLog)
+import Polysemy.TinyLog qualified as Log
+import System.Logger (field, msg, val)
import Wire.API.Locale
-import Wire.API.User
-- | Lookup a localised item from a 'Localised' structure.
forLocale ::
@@ -72,157 +71,29 @@ renderTextWithBranding tpl replace branding = render tpl (replace . branding)
renderHtmlWithBranding :: Template -> (Text -> Text) -> TemplateBranding -> Lazy.Text
renderHtmlWithBranding tpl replace branding = render tpl (HTML.text . replace . branding)
-data UserTemplates = UserTemplates
- { activationSms :: ActivationSmsTemplate,
- activationCall :: ActivationCallTemplate,
- verificationEmail :: VerificationEmailTemplate,
- activationEmail :: ActivationEmailTemplate,
- activationEmailUpdate :: ActivationEmailTemplate,
- teamActivationEmail :: TeamActivationEmailTemplate,
- passwordResetSms :: PasswordResetSmsTemplate,
- passwordResetEmail :: PasswordResetEmailTemplate,
- loginSms :: LoginSmsTemplate,
- loginCall :: LoginCallTemplate,
- deletionSms :: DeletionSmsTemplate,
- deletionEmail :: DeletionEmailTemplate,
- newClientEmail :: NewClientEmailTemplate,
- verificationLoginEmail :: SecondFactorVerificationEmailTemplate,
- verificationScimTokenEmail :: SecondFactorVerificationEmailTemplate,
- verificationTeamDeletionEmail :: SecondFactorVerificationEmailTemplate
- }
-
-data ActivationSmsTemplate = ActivationSmsTemplate
- { activationSmslUrl :: Template,
- activationSmsText :: Template,
- activationSmsSender :: Text
- }
-
-data ActivationCallTemplate = ActivationCallTemplate
- { activationCallText :: Template
- }
-
-data VerificationEmailTemplate = VerificationEmailTemplate
- { verificationEmailUrl :: Template,
- verificationEmailSubject :: Template,
- verificationEmailBodyText :: Template,
- verificationEmailBodyHtml :: Template,
- verificationEmailSender :: EmailAddress,
- verificationEmailSenderName :: Text
- }
-
-data ActivationEmailTemplate = ActivationEmailTemplate
- { activationEmailUrl :: Template,
- activationEmailSubject :: Template,
- activationEmailBodyText :: Template,
- activationEmailBodyHtml :: Template,
- activationEmailSender :: EmailAddress,
- activationEmailSenderName :: Text
- }
-
-data TeamActivationEmailTemplate = TeamActivationEmailTemplate
- { teamActivationEmailUrl :: Template,
- teamActivationEmailSubject :: Template,
- teamActivationEmailBodyText :: Template,
- teamActivationEmailBodyHtml :: Template,
- teamActivationEmailSender :: EmailAddress,
- teamActivationEmailSenderName :: Text
- }
-
-data DeletionEmailTemplate = DeletionEmailTemplate
- { deletionEmailUrl :: Template,
- deletionEmailSubject :: Template,
- deletionEmailBodyText :: Template,
- deletionEmailBodyHtml :: Template,
- deletionEmailSender :: EmailAddress,
- deletionEmailSenderName :: Text
- }
-
-data PasswordResetEmailTemplate = PasswordResetEmailTemplate
- { passwordResetEmailUrl :: Template,
- passwordResetEmailSubject :: Template,
- passwordResetEmailBodyText :: Template,
- passwordResetEmailBodyHtml :: Template,
- passwordResetEmailSender :: EmailAddress,
- passwordResetEmailSenderName :: Text
- }
-
-data PasswordResetSmsTemplate = PasswordResetSmsTemplate
- { passwordResetSmsText :: Template,
- passwordResetSmsSender :: Text
- }
-
-data LoginSmsTemplate = LoginSmsTemplate
- { loginSmsUrl :: Template,
- loginSmsText :: Template,
- loginSmsSender :: Text
- }
-
-data LoginCallTemplate = LoginCallTemplate
- { loginCallText :: Template
- }
-
-data DeletionSmsTemplate = DeletionSmsTemplate
- { deletionSmsUrl :: Template,
- deletionSmsText :: Template,
- deletionSmsSender :: Text
- }
-
-data NewClientEmailTemplate = NewClientEmailTemplate
- { newClientEmailSubject :: Template,
- newClientEmailBodyText :: Template,
- newClientEmailBodyHtml :: Template,
- newClientEmailSender :: EmailAddress,
- newClientEmailSenderName :: Text
- }
-
-data SecondFactorVerificationEmailTemplate = SecondFactorVerificationEmailTemplate
- { sndFactorVerificationEmailSubject :: Template,
- sndFactorVerificationEmailBodyText :: Template,
- sndFactorVerificationEmailBodyHtml :: Template,
- 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 NewTeamOwnerWelcomeEmailTemplate = NewTeamOwnerWelcomeEmailTemplate
- { newTeamOwnerWelcomeEmailUrl :: !Text,
- newTeamOwnerWelcomeEmailSubject :: !Template,
- newTeamOwnerWelcomeEmailBodyText :: !Template,
- newTeamOwnerWelcomeEmailBodyHtml :: !Template,
- newTeamOwnerWelcomeEmailSender :: !EmailAddress,
- newTeamOwnerWelcomeEmailSenderName :: !Text
- }
-
-data TeamTemplates = TeamTemplates
- { invitationEmail :: !InvitationEmailTemplate,
- existingUserInvitationEmail :: !InvitationEmailTemplate,
- creatorWelcomeEmail :: !CreatorWelcomeEmailTemplate,
- memberWelcomeEmail :: !MemberWelcomeEmailTemplate,
- newTeamOwnerWelcomeEmail :: !NewTeamOwnerWelcomeEmailTemplate
- }
+renderHtmlWithBrandingSem :: (Member (Output Text) r) => Template -> Map Text Text -> Sem r Lazy.Text
+renderHtmlWithBrandingSem = renderWithBrandingSem HTML.text
+
+renderTextWithBrandingSem :: (Member (Output Text) r) => Template -> Map Text Text -> Sem r Lazy.Text
+renderTextWithBrandingSem = renderWithBrandingSem id
+
+-- If a template field is not declared, do not replace it and drop key to `Output` effect. This way we catch all errors, not just the first, and the caller gets to decide what to do with the error.
+renderWithBrandingSem :: (Member (Output Text) r) => (Text -> Text) -> Template -> Map Text Text -> Sem r Lazy.Text
+renderWithBrandingSem escapeHtml tpl replace = do
+ let f x = case Map.lookup x replace of
+ Just v -> pure v
+ Nothing -> do
+ output x
+ pure x
+ renderA tpl (escapeHtml <$$> f)
+
+logEmailRenderErrors :: (Member TinyLog r) => Text -> Sem (Output Text : r) a -> Sem r a
+logEmailRenderErrors tplName =
+ runOutputSem $
+ ( \warn ->
+ do
+ Log.warn $
+ msg (val "Email template rendering failure")
+ . field "template_name" (val (T.encodeUtf8 tplName))
+ . field "unreplaced_variable" (val (T.encodeUtf8 warn))
+ )
diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Templates/Team.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Templates/Team.hs
new file mode 100644
index 0000000000..76187fe56d
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Templates/Team.hs
@@ -0,0 +1,58 @@
+{-# 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 Wire.EmailSubsystem.Templates.Team where
+
+import Data.Text.Template
+import Imports
+import Wire.API.User
+
+data InvitationEmailTemplate = InvitationEmailTemplate
+ { invitationEmailUrl :: !Template,
+ invitationEmailSubject :: !Template,
+ invitationEmailBodyText :: !Template,
+ invitationEmailBodyHtml :: !Template,
+ invitationEmailSender :: !EmailAddress,
+ invitationEmailSenderName :: !Text
+ }
+
+data MemberWelcomeEmailTemplate = MemberWelcomeEmailTemplate
+ { memberWelcomeEmailUrl :: !Text,
+ memberWelcomeEmailSubject :: !Template,
+ memberWelcomeEmailBodyText :: !Template,
+ memberWelcomeEmailBodyHtml :: !Template,
+ memberWelcomeEmailSender :: !EmailAddress,
+ memberWelcomeEmailSenderName :: !Text
+ }
+
+data NewTeamOwnerWelcomeEmailTemplate = NewTeamOwnerWelcomeEmailTemplate
+ { newTeamOwnerWelcomeEmailUrl :: !Text,
+ newTeamOwnerWelcomeEmailSubject :: !Template,
+ newTeamOwnerWelcomeEmailBodyText :: !Template,
+ newTeamOwnerWelcomeEmailBodyHtml :: !Template,
+ newTeamOwnerWelcomeEmailSender :: !EmailAddress,
+ newTeamOwnerWelcomeEmailSenderName :: !Text
+ }
+
+data TeamTemplates = TeamTemplates
+ { invitationEmail :: !InvitationEmailTemplate,
+ existingUserInvitationEmail :: !InvitationEmailTemplate,
+ memberWelcomeEmail :: !MemberWelcomeEmailTemplate,
+ newTeamOwnerWelcomeEmail :: !NewTeamOwnerWelcomeEmailTemplate
+ }
diff --git a/libs/wire-subsystems/src/Wire/EmailSubsystem/Templates/User.hs b/libs/wire-subsystems/src/Wire/EmailSubsystem/Templates/User.hs
new file mode 100644
index 0000000000..38077ab802
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/EmailSubsystem/Templates/User.hs
@@ -0,0 +1,98 @@
+{-# 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 Wire.EmailSubsystem.Templates.User where
+
+import Data.Text.Template
+import Imports
+import Wire.API.User
+
+data UserTemplates = UserTemplates
+ { verificationEmail :: VerificationEmailTemplate,
+ activationEmail :: ActivationEmailTemplate,
+ activationEmailUpdate :: ActivationEmailTemplate,
+ teamActivationEmail :: TeamActivationEmailTemplate,
+ passwordResetEmail :: PasswordResetEmailTemplate,
+ deletionEmail :: DeletionEmailTemplate,
+ newClientEmail :: NewClientEmailTemplate,
+ verificationLoginEmail :: SecondFactorVerificationEmailTemplate,
+ verificationScimTokenEmail :: SecondFactorVerificationEmailTemplate,
+ verificationTeamDeletionEmail :: SecondFactorVerificationEmailTemplate
+ }
+
+data VerificationEmailTemplate = VerificationEmailTemplate
+ { verificationEmailUrl :: Template,
+ verificationEmailSubject :: Template,
+ verificationEmailBodyText :: Template,
+ verificationEmailBodyHtml :: Template,
+ verificationEmailSender :: EmailAddress,
+ verificationEmailSenderName :: Text
+ }
+
+data ActivationEmailTemplate = ActivationEmailTemplate
+ { activationEmailUrl :: Template,
+ activationEmailSubject :: Template,
+ activationEmailBodyText :: Template,
+ activationEmailBodyHtml :: Template,
+ activationEmailSender :: EmailAddress,
+ activationEmailSenderName :: Text
+ }
+
+data TeamActivationEmailTemplate = TeamActivationEmailTemplate
+ { teamActivationEmailUrl :: Template,
+ teamActivationEmailSubject :: Template,
+ teamActivationEmailBodyText :: Template,
+ teamActivationEmailBodyHtml :: Template,
+ teamActivationEmailSender :: EmailAddress,
+ teamActivationEmailSenderName :: Text
+ }
+
+data DeletionEmailTemplate = DeletionEmailTemplate
+ { deletionEmailUrl :: Template,
+ deletionEmailSubject :: Template,
+ deletionEmailBodyText :: Template,
+ deletionEmailBodyHtml :: Template,
+ deletionEmailSender :: EmailAddress,
+ deletionEmailSenderName :: Text
+ }
+
+data PasswordResetEmailTemplate = PasswordResetEmailTemplate
+ { passwordResetEmailUrl :: Template,
+ passwordResetEmailSubject :: Template,
+ passwordResetEmailBodyText :: Template,
+ passwordResetEmailBodyHtml :: Template,
+ passwordResetEmailSender :: EmailAddress,
+ passwordResetEmailSenderName :: Text
+ }
+
+data NewClientEmailTemplate = NewClientEmailTemplate
+ { newClientEmailSubject :: Template,
+ newClientEmailBodyText :: Template,
+ newClientEmailBodyHtml :: Template,
+ newClientEmailSender :: EmailAddress,
+ newClientEmailSenderName :: Text
+ }
+
+data SecondFactorVerificationEmailTemplate = SecondFactorVerificationEmailTemplate
+ { sndFactorVerificationEmailSubject :: Template,
+ sndFactorVerificationEmailBodyText :: Template,
+ sndFactorVerificationEmailBodyHtml :: Template,
+ sndFactorVerificationEmailSender :: EmailAddress,
+ sndFactorVerificationEmailSenderName :: Text
+ }
diff --git a/libs/wire-subsystems/templates/ar/user/call/activation.txt b/libs/wire-subsystems/templates/ar/user/call/activation.txt
deleted file mode 100644
index 6ffbabae46..0000000000
--- a/libs/wire-subsystems/templates/ar/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-رمز واير لك هو ${code}.
diff --git a/libs/wire-subsystems/templates/ar/user/call/login.txt b/libs/wire-subsystems/templates/ar/user/call/login.txt
deleted file mode 100644
index f10b7e17e4..0000000000
--- a/libs/wire-subsystems/templates/ar/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-رمز دخول واير الخاص بك هو ${code}.
diff --git a/libs/wire-subsystems/templates/ar/user/sms/activation.txt b/libs/wire-subsystems/templates/ar/user/sms/activation.txt
deleted file mode 100644
index 8525e4f964..0000000000
--- a/libs/wire-subsystems/templates/ar/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-رمز واير الخاص بك هو ${code}.
-
- قم بإدخال الرمز أعلاه لإتمام التسجيل في واير.
diff --git a/libs/wire-subsystems/templates/ar/user/sms/deletion.txt b/libs/wire-subsystems/templates/ar/user/sms/deletion.txt
deleted file mode 100644
index c9ce376421..0000000000
--- a/libs/wire-subsystems/templates/ar/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-انقر لحذف حسابك الخاص في واير.
-${url}
diff --git a/libs/wire-subsystems/templates/ar/user/sms/login.txt b/libs/wire-subsystems/templates/ar/user/sms/login.txt
deleted file mode 100644
index b328bd89a1..0000000000
--- a/libs/wire-subsystems/templates/ar/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-رمز دخول واير الخاص بك هو ${code}.
-
-قم بإدخاله في تطبيق واير لتتمكن من تسجيل الدخول : ${code}.
diff --git a/libs/wire-subsystems/templates/ar/user/sms/password-reset.txt b/libs/wire-subsystems/templates/ar/user/sms/password-reset.txt
deleted file mode 100644
index 912478bdee..0000000000
--- a/libs/wire-subsystems/templates/ar/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-رمز الاسترداد الخاص بك في واير هو ${code}.
-
-افتح تطبيق واير واستخدم هذا الرمز لإتمام إعادة تعيين كلمة المرور.
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/de/team/email/new-creator-welcome-subject.txt b/libs/wire-subsystems/templates/de/team/email/new-creator-welcome-subject.txt
deleted file mode 100644
index 34083e277c..0000000000
--- a/libs/wire-subsystems/templates/de/team/email/new-creator-welcome-subject.txt
+++ /dev/null
@@ -1 +0,0 @@
-Neues Team auf Wire
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/de/team/email/new-creator-welcome.html b/libs/wire-subsystems/templates/de/team/email/new-creator-welcome.html
deleted file mode 100644
index 89fba8a6f5..0000000000
--- a/libs/wire-subsystems/templates/de/team/email/new-creator-welcome.html
+++ /dev/null
@@ -1 +0,0 @@
-Neues Team auf WireDein neues Team.Du hast soeben mit ${email} ein Team namens ${team_name} auf Wire erstellt. Deine 30-tägige Testversion beginnt heute. Wire vereint sichere Verschlüsselung mit reichhaltigem Funktionsumfang und einfacher Bedienung in einer einzigen App. Unterstützt alle gängigen Plattformen. Falls du nicht auf den Button klicken kannst, kopiere den Link und füge ihn in deinem Browser ein: ${url} Wenn Du Fragen hast, bitte kontaktiere uns. Team ID: ${team_id} | |
|---|
|
|---|
|
|
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/de/team/email/new-creator-welcome.txt b/libs/wire-subsystems/templates/de/team/email/new-creator-welcome.txt
deleted file mode 100644
index dd8638c137..0000000000
--- a/libs/wire-subsystems/templates/de/team/email/new-creator-welcome.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-DEIN NEUES TEAM.
-Du hast soeben mit ${email} ein Team namens ${team_name} auf Wire erstellt.
-
-Deine 30-tägige Testversion beginnt heute.
-
-Wire vereint sichere Verschlüsselung mit reichhaltigem Funktionsumfang und
-einfacher Bedienung in einer einzigen App. Unterstützt alle gängigen
-Plattformen.
-
-Lade deine Teamkameraden ein [${url}]Falls du nicht auf den Button klicken
-kannst, kopiere den Link und füge ihn in deinem Browser ein:
-
-${url}
-
-Wenn Du Fragen hast, bitte kontaktiere uns
-[https://support.wire.com/hc/de/requests/new].
-
-Team ID: ${team_id}
-
-
---------------------------------------------------------------------------------
-
-Datenschutz [https://wire.com/legal/] · Missbrauch melden [misuse@wire.com]
-© WIRE SWISS GmbH. ALLE RECHTE VORBEHALTEN.
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/de/user/call/activation.txt b/libs/wire-subsystems/templates/de/user/call/activation.txt
deleted file mode 100644
index c2051759f6..0000000000
--- a/libs/wire-subsystems/templates/de/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Guten Tag, Ihr Bestätigungscode für Wire lautet: ${code}. Noch einmal, Ihr Code lautet: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/de/user/call/login.txt b/libs/wire-subsystems/templates/de/user/call/login.txt
deleted file mode 100644
index da209aae60..0000000000
--- a/libs/wire-subsystems/templates/de/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Guten Tag, Ihr Login-Code für Wire lautet: ${code}. Noch einmal, Ihr Code lautet: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/de/user/sms/activation.txt b/libs/wire-subsystems/templates/de/user/sms/activation.txt
deleted file mode 100644
index 98a94de540..0000000000
--- a/libs/wire-subsystems/templates/de/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Ihr ${brand}-Code ist ${code}.
-
-Öffnen Sie ${url}, um Ihre Nummer zu bestätigen.
diff --git a/libs/wire-subsystems/templates/de/user/sms/deletion.txt b/libs/wire-subsystems/templates/de/user/sms/deletion.txt
deleted file mode 100644
index 596d8e46ad..0000000000
--- a/libs/wire-subsystems/templates/de/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Tippen Sie hier, um Ihr ${brand}-Benutzerkonto zu löschen.
-${url}
diff --git a/libs/wire-subsystems/templates/de/user/sms/login.txt b/libs/wire-subsystems/templates/de/user/sms/login.txt
deleted file mode 100644
index fd91e0edbd..0000000000
--- a/libs/wire-subsystems/templates/de/user/sms/login.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-Ihr Login-Code für ${brand} lautet ${code}.
-
-
-Öffnen Sie ${url}, um sich anzumelden.
diff --git a/libs/wire-subsystems/templates/de/user/sms/password-reset.txt b/libs/wire-subsystems/templates/de/user/sms/password-reset.txt
deleted file mode 100644
index 89ab00d5cd..0000000000
--- a/libs/wire-subsystems/templates/de/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Ihr Wiederherstellungscode für ${brand} ist ${code}.
-
-Verwenden Sie diesen Code, um das Zurücksetzen des Passworts abzuschließen.
diff --git a/libs/wire-subsystems/templates/en/team/email/new-creator-welcome-subject.txt b/libs/wire-subsystems/templates/en/team/email/new-creator-welcome-subject.txt
deleted file mode 100644
index a2f9b0fce1..0000000000
--- a/libs/wire-subsystems/templates/en/team/email/new-creator-welcome-subject.txt
+++ /dev/null
@@ -1 +0,0 @@
-New team on Wire
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/en/team/email/new-creator-welcome.html b/libs/wire-subsystems/templates/en/team/email/new-creator-welcome.html
deleted file mode 100644
index 839eaba980..0000000000
--- a/libs/wire-subsystems/templates/en/team/email/new-creator-welcome.html
+++ /dev/null
@@ -1 +0,0 @@
-New team on WireYour new team.You have just created a team called ${team_name} on Wire with ${email}. Your 30–day free trial starts today. Wire combines strong encryption, a rich feature set and ease-of-use in one app like never before. Works on all popular platforms. 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
diff --git a/libs/wire-subsystems/templates/en/team/email/new-creator-welcome.txt b/libs/wire-subsystems/templates/en/team/email/new-creator-welcome.txt
deleted file mode 100644
index 209b41899e..0000000000
--- a/libs/wire-subsystems/templates/en/team/email/new-creator-welcome.txt
+++ /dev/null
@@ -1,23 +0,0 @@
-YOUR NEW TEAM.
-You have just created a team called ${team_name} on Wire with ${email}.
-
-Your 30–day free trial starts today.
-
-Wire combines strong encryption, a rich feature set and ease-of-use in one app
-like never before. Works on all popular platforms.
-
-Invite your teammates [${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
-[https://support.wire.com/hc/en-us/requests/new].
-
-Team ID: ${team_id}
-
-
---------------------------------------------------------------------------------
-
-Privacy [https://wire.com/legal/] · Report Misuse [misuse@wire.com]
-© WIRE SWISS GmbH. ALL RIGHTS RESERVED.
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/en/user/call/activation.txt b/libs/wire-subsystems/templates/en/user/call/activation.txt
deleted file mode 100644
index 4ad404bbb1..0000000000
--- a/libs/wire-subsystems/templates/en/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Hello, your Wire verification code is: ${code}. Once again, your code is: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/en/user/call/login.txt b/libs/wire-subsystems/templates/en/user/call/login.txt
deleted file mode 100644
index 931afe85ee..0000000000
--- a/libs/wire-subsystems/templates/en/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Hello, your Wire login code is: ${code}. Once again, your code is: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/en/user/sms/activation.txt b/libs/wire-subsystems/templates/en/user/sms/activation.txt
deleted file mode 100644
index 69d66e37b0..0000000000
--- a/libs/wire-subsystems/templates/en/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Your ${brand} code is ${code}.
-
-Open ${url} to verify your number.
diff --git a/libs/wire-subsystems/templates/en/user/sms/deletion.txt b/libs/wire-subsystems/templates/en/user/sms/deletion.txt
deleted file mode 100644
index 07a9b8e54c..0000000000
--- a/libs/wire-subsystems/templates/en/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Tap to delete your ${brand} account.
-${url}
diff --git a/libs/wire-subsystems/templates/en/user/sms/login.txt b/libs/wire-subsystems/templates/en/user/sms/login.txt
deleted file mode 100644
index 26a0a43632..0000000000
--- a/libs/wire-subsystems/templates/en/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Your ${brand} login code is ${code}.
-
-Open ${url} to log in.
diff --git a/libs/wire-subsystems/templates/en/user/sms/password-reset.txt b/libs/wire-subsystems/templates/en/user/sms/password-reset.txt
deleted file mode 100644
index d99d8bff89..0000000000
--- a/libs/wire-subsystems/templates/en/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Your ${brand} recovery code is ${code}.
-
-Use this code to complete the password reset.
diff --git a/libs/wire-subsystems/templates/es-ES/user/call/activation.txt b/libs/wire-subsystems/templates/es-ES/user/call/activation.txt
deleted file mode 100644
index 03ffdcfb8a..0000000000
--- a/libs/wire-subsystems/templates/es-ES/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Hola, su código de verificación de Wire es: ${code} una vez más, su código es: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/es-ES/user/call/login.txt b/libs/wire-subsystems/templates/es-ES/user/call/login.txt
deleted file mode 100644
index a5706ff4fb..0000000000
--- a/libs/wire-subsystems/templates/es-ES/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Hola, su código de inicio de sesion Wire es: ${code} una vez más, su código es: ${code}
diff --git a/libs/wire-subsystems/templates/es-ES/user/sms/activation.txt b/libs/wire-subsystems/templates/es-ES/user/sms/activation.txt
deleted file mode 100644
index df0bc231c2..0000000000
--- a/libs/wire-subsystems/templates/es-ES/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Su código de Wire es ${code}.
-
-Abrir ${url} para verificar tu número o introducir manualmente el código Wire en la parte superior.
diff --git a/libs/wire-subsystems/templates/es-ES/user/sms/deletion.txt b/libs/wire-subsystems/templates/es-ES/user/sms/deletion.txt
deleted file mode 100644
index 856b839281..0000000000
--- a/libs/wire-subsystems/templates/es-ES/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Toca para eliminar tu cuenta de Wire.
-${url}
diff --git a/libs/wire-subsystems/templates/es-ES/user/sms/login.txt b/libs/wire-subsystems/templates/es-ES/user/sms/login.txt
deleted file mode 100644
index 3b1db9c4b3..0000000000
--- a/libs/wire-subsystems/templates/es-ES/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-El código de inicio de sesion de Wire es ${code}.
-
-Abra ${url} para iniciar sesión, o introduzca este código en la aplicación de Wire: ${code}.
diff --git a/libs/wire-subsystems/templates/es-ES/user/sms/password-reset.txt b/libs/wire-subsystems/templates/es-ES/user/sms/password-reset.txt
deleted file mode 100644
index c0a623d789..0000000000
--- a/libs/wire-subsystems/templates/es-ES/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Su código de recuperación de Wire es ${code}.
-
-Abra la aplicación Wire y use este código para completar el restablecimiento de contraseña.
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/et/user/call/activation.txt b/libs/wire-subsystems/templates/et/user/call/activation.txt
deleted file mode 100644
index 939e891fb6..0000000000
--- a/libs/wire-subsystems/templates/et/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Hei, sinu Wire kinnituskood on: ${code}. Veelkord, kood on: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/et/user/call/login.txt b/libs/wire-subsystems/templates/et/user/call/login.txt
deleted file mode 100644
index 5249ea6caa..0000000000
--- a/libs/wire-subsystems/templates/et/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Hei, sinu Wire sisselogimiskood on: ${code}. Veelkord, kood on: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/et/user/sms/activation.txt b/libs/wire-subsystems/templates/et/user/sms/activation.txt
deleted file mode 100644
index ddc0b07463..0000000000
--- a/libs/wire-subsystems/templates/et/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Your ${brand} code is ${code}.
-
-Toksa ${url} et number kinnitada.
diff --git a/libs/wire-subsystems/templates/et/user/sms/deletion.txt b/libs/wire-subsystems/templates/et/user/sms/deletion.txt
deleted file mode 100644
index 07a9b8e54c..0000000000
--- a/libs/wire-subsystems/templates/et/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Tap to delete your ${brand} account.
-${url}
diff --git a/libs/wire-subsystems/templates/et/user/sms/login.txt b/libs/wire-subsystems/templates/et/user/sms/login.txt
deleted file mode 100644
index 34755c3343..0000000000
--- a/libs/wire-subsystems/templates/et/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Your ${brand} login code is ${code}.
-
-Sisselogimiseks ava aadress ${url}
diff --git a/libs/wire-subsystems/templates/et/user/sms/password-reset.txt b/libs/wire-subsystems/templates/et/user/sms/password-reset.txt
deleted file mode 100644
index 067c0e51f6..0000000000
--- a/libs/wire-subsystems/templates/et/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Your ${brand} recovery code is ${code}.
-
-Kasuta seda koodi parooli muutmiseks.
diff --git a/libs/wire-subsystems/templates/fa/user/call/activation.txt b/libs/wire-subsystems/templates/fa/user/call/activation.txt
deleted file mode 100644
index cec5faf036..0000000000
--- a/libs/wire-subsystems/templates/fa/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-سلام، کد تایید حساب شما: ${code} است. یکبار دیگه، این کد ${code} برای تایید حساب شما است.
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/fa/user/call/login.txt b/libs/wire-subsystems/templates/fa/user/call/login.txt
deleted file mode 100644
index 6a2d09f484..0000000000
--- a/libs/wire-subsystems/templates/fa/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-سلام، کد تایید حساب شما: ${code} است. یکبار دیگه، این کد ${code} برای تایید حساب شما است.
diff --git a/libs/wire-subsystems/templates/fa/user/sms/activation.txt b/libs/wire-subsystems/templates/fa/user/sms/activation.txt
deleted file mode 100644
index 80fb8deec6..0000000000
--- a/libs/wire-subsystems/templates/fa/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-کد Wire شما ${code} .
-
-از طریق ${url} شماره تلفن را تایید و یا کد بالا را در Wire بصورت دستی وارد کنید.
diff --git a/libs/wire-subsystems/templates/fa/user/sms/deletion.txt b/libs/wire-subsystems/templates/fa/user/sms/deletion.txt
deleted file mode 100644
index dda9c0e3c3..0000000000
--- a/libs/wire-subsystems/templates/fa/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-اینجا بزنید تا حساب Wire خود را پاک کنید.
-${url}
diff --git a/libs/wire-subsystems/templates/fa/user/sms/login.txt b/libs/wire-subsystems/templates/fa/user/sms/login.txt
deleted file mode 100644
index fd2fc9d878..0000000000
--- a/libs/wire-subsystems/templates/fa/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-این کد ${code} برای ورود شما قابل استفاده است.
-
-پیوند ${url} را برای وارد شدن باز کنید و یا این کد ${code} را در Wire خود بصورت دستی وارد کنید.
diff --git a/libs/wire-subsystems/templates/fa/user/sms/password-reset.txt b/libs/wire-subsystems/templates/fa/user/sms/password-reset.txt
deleted file mode 100644
index 120b40adca..0000000000
--- a/libs/wire-subsystems/templates/fa/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-این کد ${code} برای بازیابی حساب Wire شما است.
-
-برای تکمیل مراحل بازیابی رمزعبور خود، در اپ Wire این کد را وارد کنید.
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/fr/user/call/activation.txt b/libs/wire-subsystems/templates/fr/user/call/activation.txt
deleted file mode 100644
index 391fe95d6a..0000000000
--- a/libs/wire-subsystems/templates/fr/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Bonjour, votre code de vérification pour Wire est : ${code}. Je répète, votre code est : ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/fr/user/call/login.txt b/libs/wire-subsystems/templates/fr/user/call/login.txt
deleted file mode 100644
index 20a123ba2c..0000000000
--- a/libs/wire-subsystems/templates/fr/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Bonjour, votre code de connexion Wire est : ${code}. Je répète, votre code est : ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/fr/user/sms/activation.txt b/libs/wire-subsystems/templates/fr/user/sms/activation.txt
deleted file mode 100644
index be11bb1e3c..0000000000
--- a/libs/wire-subsystems/templates/fr/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Votre code ${brand} est ${code}.
-
-Ouvrez ${url} pour vérifier votre numéro.
diff --git a/libs/wire-subsystems/templates/fr/user/sms/deletion.txt b/libs/wire-subsystems/templates/fr/user/sms/deletion.txt
deleted file mode 100644
index 8fb5c0c3b5..0000000000
--- a/libs/wire-subsystems/templates/fr/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Cliquez pour supprimer votre compte ${brand}.
-${url}
diff --git a/libs/wire-subsystems/templates/fr/user/sms/login.txt b/libs/wire-subsystems/templates/fr/user/sms/login.txt
deleted file mode 100644
index 0b859e561a..0000000000
--- a/libs/wire-subsystems/templates/fr/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Votre code de connexion ${brand} est ${code}.
-
-Ouvrez ${url} pour vous connecter.
diff --git a/libs/wire-subsystems/templates/fr/user/sms/password-reset.txt b/libs/wire-subsystems/templates/fr/user/sms/password-reset.txt
deleted file mode 100644
index 5ad45f6dd8..0000000000
--- a/libs/wire-subsystems/templates/fr/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Votre code de récupération ${brand} est ${code}.
-
-Utilisez ce code pour finir de réinitialiser votre mot de passe.
diff --git a/libs/wire-subsystems/templates/it/user/call/activation.txt b/libs/wire-subsystems/templates/it/user/call/activation.txt
deleted file mode 100644
index c1b51fc95b..0000000000
--- a/libs/wire-subsystems/templates/it/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Ciao, il tuo codice verifica di Wire è: ${code}. Ancora una volta, il codice è: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/it/user/call/login.txt b/libs/wire-subsystems/templates/it/user/call/login.txt
deleted file mode 100644
index fd6cfc78e6..0000000000
--- a/libs/wire-subsystems/templates/it/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Ciao, il tuo codice di accesso di Wire è: ${code}. Ancora una volta, il codice è: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/it/user/sms/activation.txt b/libs/wire-subsystems/templates/it/user/sms/activation.txt
deleted file mode 100644
index 2f831a186d..0000000000
--- a/libs/wire-subsystems/templates/it/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Il codice del tuo ${brand} è ${code}.
-
-Apri ${url} per verificare il tuo numero.
diff --git a/libs/wire-subsystems/templates/it/user/sms/deletion.txt b/libs/wire-subsystems/templates/it/user/sms/deletion.txt
deleted file mode 100644
index 954020d32c..0000000000
--- a/libs/wire-subsystems/templates/it/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Tocca per eliminare il tuo profilo di ${brand}.
-${url}
diff --git a/libs/wire-subsystems/templates/it/user/sms/login.txt b/libs/wire-subsystems/templates/it/user/sms/login.txt
deleted file mode 100644
index 84a0b9861d..0000000000
--- a/libs/wire-subsystems/templates/it/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Il tuo codice di accesso di ${brand} è ${code}.
-
-Apri ${url} per accedere.
diff --git a/libs/wire-subsystems/templates/it/user/sms/password-reset.txt b/libs/wire-subsystems/templates/it/user/sms/password-reset.txt
deleted file mode 100644
index b9aa4d7a94..0000000000
--- a/libs/wire-subsystems/templates/it/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Il tuo codice di recupero di ${brand} è ${code}.
-
-Usa questo codice per completare il ripristino della password.
diff --git a/libs/wire-subsystems/templates/ja/user/call/activation.txt b/libs/wire-subsystems/templates/ja/user/call/activation.txt
deleted file mode 100644
index d62a4fa1c5..0000000000
--- a/libs/wire-subsystems/templates/ja/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-こんにちは、あなたのWire確認コードは、${code} です。 もう一度、あなたのコードは${code} です。
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/ja/user/call/login.txt b/libs/wire-subsystems/templates/ja/user/call/login.txt
deleted file mode 100644
index 443ac057de..0000000000
--- a/libs/wire-subsystems/templates/ja/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-こんにちは、あなたのWire確認コードは、${code} です。 もう一度、あなたのコードは ${code} です。
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/ja/user/sms/activation.txt b/libs/wire-subsystems/templates/ja/user/sms/activation.txt
deleted file mode 100644
index 5b27432624..0000000000
--- a/libs/wire-subsystems/templates/ja/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-あなたの ${brand} の ログインコードは ${code} です
-
-${url} を開いて、あなたの番号を認証してください。
diff --git a/libs/wire-subsystems/templates/ja/user/sms/deletion.txt b/libs/wire-subsystems/templates/ja/user/sms/deletion.txt
deleted file mode 100644
index f552addbd5..0000000000
--- a/libs/wire-subsystems/templates/ja/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-タップして、あなたの ${brand} のアカウントを削除します
-${url}
diff --git a/libs/wire-subsystems/templates/ja/user/sms/login.txt b/libs/wire-subsystems/templates/ja/user/sms/login.txt
deleted file mode 100644
index ac0ec8b47d..0000000000
--- a/libs/wire-subsystems/templates/ja/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-あなたの ${brand} ログインコードは ${code} です
-
-${url} を開いて、ログインしてください。
diff --git a/libs/wire-subsystems/templates/ja/user/sms/password-reset.txt b/libs/wire-subsystems/templates/ja/user/sms/password-reset.txt
deleted file mode 100644
index 9476d182cb..0000000000
--- a/libs/wire-subsystems/templates/ja/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-あなたの ${brand} の リカバリーコードは ${code} です。
-
-このコードを使ってパスワードのリセットを完了してください。
diff --git a/libs/wire-subsystems/templates/lt/user/call/activation.txt b/libs/wire-subsystems/templates/lt/user/call/activation.txt
deleted file mode 100644
index 579b007b3e..0000000000
--- a/libs/wire-subsystems/templates/lt/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Sveiki, jūsų „Wire“ patvirtinimo kodas yra: ${code}. Dar kartą, jūsų kodas yra: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/lt/user/call/login.txt b/libs/wire-subsystems/templates/lt/user/call/login.txt
deleted file mode 100644
index 807fd23846..0000000000
--- a/libs/wire-subsystems/templates/lt/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Sveiki, jūsų „Wire“ prisijungimo kodas yra: ${code}. Dar kartą, jūsų kodas yra: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/lt/user/sms/activation.txt b/libs/wire-subsystems/templates/lt/user/sms/activation.txt
deleted file mode 100644
index 68bfeccaac..0000000000
--- a/libs/wire-subsystems/templates/lt/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Jūsų ${brand} kodas yra ${code}.
-
-Atverkite ${url} norėdami patvirtinti savo numerį.
diff --git a/libs/wire-subsystems/templates/lt/user/sms/deletion.txt b/libs/wire-subsystems/templates/lt/user/sms/deletion.txt
deleted file mode 100644
index b918b6f022..0000000000
--- a/libs/wire-subsystems/templates/lt/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Bakstelėkite, norėdami ištrinti savo ${brand} paskyrą.
-${url}
diff --git a/libs/wire-subsystems/templates/lt/user/sms/login.txt b/libs/wire-subsystems/templates/lt/user/sms/login.txt
deleted file mode 100644
index 36ad720403..0000000000
--- a/libs/wire-subsystems/templates/lt/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Jūsų ${brand} prisijungimo kodas yra ${code}.
-
-Atverkite ${url} norėdami prisijungti.
diff --git a/libs/wire-subsystems/templates/lt/user/sms/password-reset.txt b/libs/wire-subsystems/templates/lt/user/sms/password-reset.txt
deleted file mode 100644
index 5baca0ba65..0000000000
--- a/libs/wire-subsystems/templates/lt/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Jūsų ${brand} atkūrimo kodas yra ${code}.
-
-Naudokite šį kodą norėdami užbaigti slaptažodžio atstatymą.
diff --git a/libs/wire-subsystems/templates/pl/user/call/activation.txt b/libs/wire-subsystems/templates/pl/user/call/activation.txt
deleted file mode 100644
index 8b52566e1e..0000000000
--- a/libs/wire-subsystems/templates/pl/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Witaj, Twój kod weryfikacyjny Wire to: ${code}. Jeszcze raz twój kod to: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/pl/user/call/login.txt b/libs/wire-subsystems/templates/pl/user/call/login.txt
deleted file mode 100644
index b2d8c07e3b..0000000000
--- a/libs/wire-subsystems/templates/pl/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Witaj, Twój kod logowania Wire to: ${code}. Jeszcze raz twój kod to: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/pl/user/sms/activation.txt b/libs/wire-subsystems/templates/pl/user/sms/activation.txt
deleted file mode 100644
index 7fc97d25b1..0000000000
--- a/libs/wire-subsystems/templates/pl/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Twój kod ${brand} to ${code}.
-
-Otwórz ${url} aby zweryfikować swój numer.
diff --git a/libs/wire-subsystems/templates/pl/user/sms/deletion.txt b/libs/wire-subsystems/templates/pl/user/sms/deletion.txt
deleted file mode 100644
index 97abae34dd..0000000000
--- a/libs/wire-subsystems/templates/pl/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Dotknij, aby usunąć swoje konto ${brand}.
-${url}
diff --git a/libs/wire-subsystems/templates/pl/user/sms/login.txt b/libs/wire-subsystems/templates/pl/user/sms/login.txt
deleted file mode 100644
index f3b86d6a96..0000000000
--- a/libs/wire-subsystems/templates/pl/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Twój kod logowania ${brand} to ${code}.
-
-Otwórz ${url} aby się zalogować.
diff --git a/libs/wire-subsystems/templates/pl/user/sms/password-reset.txt b/libs/wire-subsystems/templates/pl/user/sms/password-reset.txt
deleted file mode 100644
index 046e6e86c5..0000000000
--- a/libs/wire-subsystems/templates/pl/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Twój kod odzyskiwania ${brand} to ${code}.
-
-Użyj tego kodu, aby ukończyć resetowanie hasła.
diff --git a/libs/wire-subsystems/templates/pt-BR/user/call/activation.txt b/libs/wire-subsystems/templates/pt-BR/user/call/activation.txt
deleted file mode 100644
index cb2c9e8fd1..0000000000
--- a/libs/wire-subsystems/templates/pt-BR/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-O seu código Wire é ${code} Mais uma vez, o seu código é: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/pt-BR/user/call/login.txt b/libs/wire-subsystems/templates/pt-BR/user/call/login.txt
deleted file mode 100644
index f195a6dee9..0000000000
--- a/libs/wire-subsystems/templates/pt-BR/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Olá, seu código de login do Wire é: ${code} Mais uma vez, o seu código é: ${code}
diff --git a/libs/wire-subsystems/templates/pt-BR/user/sms/activation.txt b/libs/wire-subsystems/templates/pt-BR/user/sms/activation.txt
deleted file mode 100644
index 4ca44857e0..0000000000
--- a/libs/wire-subsystems/templates/pt-BR/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Seu código do Wire é ${code}.
-
-Abra ${url} para verificar o seu número ou manualmente insira o código acima no Wire.
diff --git a/libs/wire-subsystems/templates/pt-BR/user/sms/deletion.txt b/libs/wire-subsystems/templates/pt-BR/user/sms/deletion.txt
deleted file mode 100644
index 2d8932d3dc..0000000000
--- a/libs/wire-subsystems/templates/pt-BR/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Toque para apagar a sua conta no Wire.
-${url}
diff --git a/libs/wire-subsystems/templates/pt-BR/user/sms/login.txt b/libs/wire-subsystems/templates/pt-BR/user/sms/login.txt
deleted file mode 100644
index d6a6f1aeb7..0000000000
--- a/libs/wire-subsystems/templates/pt-BR/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Seu código de login do Wire é ${code}.
-
-Abra ${url} para entrar, ou insira esse código no aplicativo do Wire: ${code}.
diff --git a/libs/wire-subsystems/templates/pt-BR/user/sms/password-reset.txt b/libs/wire-subsystems/templates/pt-BR/user/sms/password-reset.txt
deleted file mode 100644
index 62aaa79309..0000000000
--- a/libs/wire-subsystems/templates/pt-BR/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Seu código de recuperação do Wire é ${code}.
-
-Abra o Wire e use esse código para concluir a redefinição de senha.
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/pt/user/call/activation.txt b/libs/wire-subsystems/templates/pt/user/call/activation.txt
deleted file mode 100644
index 9198dbdff8..0000000000
--- a/libs/wire-subsystems/templates/pt/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Olá, seu código de verificação do Wire é: ${code}. Novamente, seu código é: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/pt/user/call/login.txt b/libs/wire-subsystems/templates/pt/user/call/login.txt
deleted file mode 100644
index 7da656975e..0000000000
--- a/libs/wire-subsystems/templates/pt/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Olá, seu código de login do Wire é: ${code}. Novamente, seu código é: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/pt/user/sms/activation.txt b/libs/wire-subsystems/templates/pt/user/sms/activation.txt
deleted file mode 100644
index 520eb63c92..0000000000
--- a/libs/wire-subsystems/templates/pt/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Seu código ${brand} é ${code}.
-
-Acesse ${url} para verificar seu número.
diff --git a/libs/wire-subsystems/templates/pt/user/sms/deletion.txt b/libs/wire-subsystems/templates/pt/user/sms/deletion.txt
deleted file mode 100644
index 7faf69e663..0000000000
--- a/libs/wire-subsystems/templates/pt/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Toque para excluir sua conta no ${brand}.
-${url}
diff --git a/libs/wire-subsystems/templates/pt/user/sms/login.txt b/libs/wire-subsystems/templates/pt/user/sms/login.txt
deleted file mode 100644
index ef5e8fd5d1..0000000000
--- a/libs/wire-subsystems/templates/pt/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Seu código de login ${brand} é ${code}.
-
-Acesse ${url} para entrar.
diff --git a/libs/wire-subsystems/templates/pt/user/sms/password-reset.txt b/libs/wire-subsystems/templates/pt/user/sms/password-reset.txt
deleted file mode 100644
index 3667edd333..0000000000
--- a/libs/wire-subsystems/templates/pt/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Seu código de recuperação ${brand} é ${code}.
-
-Use este código para concluir a redefinição de senha.
diff --git a/libs/wire-subsystems/templates/ru/user/call/activation.txt b/libs/wire-subsystems/templates/ru/user/call/activation.txt
deleted file mode 100644
index abecab36f3..0000000000
--- a/libs/wire-subsystems/templates/ru/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Здравствуйте, ваш код для подтверждения Wire: ${code}. Еще разок, ваш код подтверждения: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/ru/user/call/login.txt b/libs/wire-subsystems/templates/ru/user/call/login.txt
deleted file mode 100644
index fb8b3ac2ff..0000000000
--- a/libs/wire-subsystems/templates/ru/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Здравствуйте, ваш код для входа в Wire: ${code}. Еще раз, ваш код подтверждения: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/ru/user/sms/activation.txt b/libs/wire-subsystems/templates/ru/user/sms/activation.txt
deleted file mode 100644
index 414a58f338..0000000000
--- a/libs/wire-subsystems/templates/ru/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Ваш код ${brand}: ${code}.
-
-Откройте ${url}, чтобы подтвердить ваш номер.
diff --git a/libs/wire-subsystems/templates/ru/user/sms/deletion.txt b/libs/wire-subsystems/templates/ru/user/sms/deletion.txt
deleted file mode 100644
index 194f5ceb1f..0000000000
--- a/libs/wire-subsystems/templates/ru/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Нажмите для удаления вашего аккаунта ${brand}.
-${url}
diff --git a/libs/wire-subsystems/templates/ru/user/sms/login.txt b/libs/wire-subsystems/templates/ru/user/sms/login.txt
deleted file mode 100644
index 5cf0687df4..0000000000
--- a/libs/wire-subsystems/templates/ru/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Ваш код входа ${brand}: ${code}.
-
-Откройте ${url}, чтобы войти.
diff --git a/libs/wire-subsystems/templates/ru/user/sms/password-reset.txt b/libs/wire-subsystems/templates/ru/user/sms/password-reset.txt
deleted file mode 100644
index e19f76b4f9..0000000000
--- a/libs/wire-subsystems/templates/ru/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Ваш код восстановления ${brand}: ${code}.
-
-Используйте этот код, чтобы завершить сброс пароля.
diff --git a/libs/wire-subsystems/templates/si/user/call/activation.txt b/libs/wire-subsystems/templates/si/user/call/activation.txt
deleted file mode 100644
index c7754ab63c..0000000000
--- a/libs/wire-subsystems/templates/si/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-ආයුබෝවන්, ඔබගේ වයර් සත්යාපන කේතය: ${code}. නැවත වරක්, ඔබගේ කේතය: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/si/user/call/login.txt b/libs/wire-subsystems/templates/si/user/call/login.txt
deleted file mode 100644
index ccb91205ee..0000000000
--- a/libs/wire-subsystems/templates/si/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-ආයුබෝවන්, ඔබගේ වයර් කේතය: ${code}. නැවත වරක්, ඔබගේ කේතය: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/si/user/sms/activation.txt b/libs/wire-subsystems/templates/si/user/sms/activation.txt
deleted file mode 100644
index e029a03074..0000000000
--- a/libs/wire-subsystems/templates/si/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-ඔබගේ ${brand} කේතය ${code} වේ.
-
-ඔබගේ අංකය සත්යාපනයට ${url} අරින්න.
diff --git a/libs/wire-subsystems/templates/si/user/sms/deletion.txt b/libs/wire-subsystems/templates/si/user/sms/deletion.txt
deleted file mode 100644
index 15b1622cad..0000000000
--- a/libs/wire-subsystems/templates/si/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-${brand} ගිණුම මැකීමට තට්ටු කරන්න.
-${url}
diff --git a/libs/wire-subsystems/templates/si/user/sms/login.txt b/libs/wire-subsystems/templates/si/user/sms/login.txt
deleted file mode 100644
index 5b0be13a59..0000000000
--- a/libs/wire-subsystems/templates/si/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-ඔබගේ ${brand} කේතය ${code} වේ.
-
-පිවිසීමට ${url} අරින්න.
diff --git a/libs/wire-subsystems/templates/si/user/sms/password-reset.txt b/libs/wire-subsystems/templates/si/user/sms/password-reset.txt
deleted file mode 100644
index d39d022711..0000000000
--- a/libs/wire-subsystems/templates/si/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-ඔබගේ ${brand} ප්රතිසාධන කේතය ${code} වේ.
-
-මුරපදය යළි සැකසීම සඳහා මෙම කේතය භාවිතා කරන්න.
diff --git a/libs/wire-subsystems/templates/sv-SE/user/call/activation.txt b/libs/wire-subsystems/templates/sv-SE/user/call/activation.txt
deleted file mode 100644
index c30dc2c5f3..0000000000
--- a/libs/wire-subsystems/templates/sv-SE/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Hej, din verifieringskod för Wire är: ${code} En gång till, din kod är: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/sv-SE/user/call/login.txt b/libs/wire-subsystems/templates/sv-SE/user/call/login.txt
deleted file mode 100644
index ae1683918b..0000000000
--- a/libs/wire-subsystems/templates/sv-SE/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Hej, din inloggningskod för Wire är: ${code} En gång till, koden är: ${code}
diff --git a/libs/wire-subsystems/templates/sv-SE/user/sms/activation.txt b/libs/wire-subsystems/templates/sv-SE/user/sms/activation.txt
deleted file mode 100644
index f69ec87f5c..0000000000
--- a/libs/wire-subsystems/templates/sv-SE/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Din kod för Wire är ${code}.
-
-Öppna ${url} för att verifiera ditt nummer eller skriv in koden manuellt i Wire.
diff --git a/libs/wire-subsystems/templates/sv-SE/user/sms/deletion.txt b/libs/wire-subsystems/templates/sv-SE/user/sms/deletion.txt
deleted file mode 100644
index 3579926fd6..0000000000
--- a/libs/wire-subsystems/templates/sv-SE/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Tryck för att radera ditt Wire konto.
-${url}
diff --git a/libs/wire-subsystems/templates/sv-SE/user/sms/login.txt b/libs/wire-subsystems/templates/sv-SE/user/sms/login.txt
deleted file mode 100644
index b9625b6215..0000000000
--- a/libs/wire-subsystems/templates/sv-SE/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Din inloggningskod till Wire är ${code}.
-
-Gå in på ${url} för att logga in, eller skriv in denna kod i Wire appen: ${code}.
diff --git a/libs/wire-subsystems/templates/sv-SE/user/sms/password-reset.txt b/libs/wire-subsystems/templates/sv-SE/user/sms/password-reset.txt
deleted file mode 100644
index 2bace00127..0000000000
--- a/libs/wire-subsystems/templates/sv-SE/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Din återställningkod för Wire är ${code}.
-
-Öppna Wire och använd denna kod för att slutföra din lösenords återställning.
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/tr/user/call/activation.txt b/libs/wire-subsystems/templates/tr/user/call/activation.txt
deleted file mode 100644
index d120890736..0000000000
--- a/libs/wire-subsystems/templates/tr/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Merhaba, Wire doğrulama kodunuz: ${code}. Bir kez daha, kodunuz: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/tr/user/call/login.txt b/libs/wire-subsystems/templates/tr/user/call/login.txt
deleted file mode 100644
index 0a7091f6d2..0000000000
--- a/libs/wire-subsystems/templates/tr/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Merhaba, Wire giriş kodunuz: ${code}. Birkez daha, kodunuz şudur: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/tr/user/sms/activation.txt b/libs/wire-subsystems/templates/tr/user/sms/activation.txt
deleted file mode 100644
index 0f297cd946..0000000000
--- a/libs/wire-subsystems/templates/tr/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-${brand} kodunuz ${code}.
-
-Numaranızı doğrulamak için: ${url}.
diff --git a/libs/wire-subsystems/templates/tr/user/sms/deletion.txt b/libs/wire-subsystems/templates/tr/user/sms/deletion.txt
deleted file mode 100644
index 41a4be3755..0000000000
--- a/libs/wire-subsystems/templates/tr/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-${brand} hesabınızı silmek için tıklayın.
-${url}
diff --git a/libs/wire-subsystems/templates/tr/user/sms/login.txt b/libs/wire-subsystems/templates/tr/user/sms/login.txt
deleted file mode 100644
index 7cd50436ad..0000000000
--- a/libs/wire-subsystems/templates/tr/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-${brand}} giriş kodunuz ${code}.
-
-Giriş yapmak için: ${url}.
diff --git a/libs/wire-subsystems/templates/tr/user/sms/password-reset.txt b/libs/wire-subsystems/templates/tr/user/sms/password-reset.txt
deleted file mode 100644
index 2b4f0933ef..0000000000
--- a/libs/wire-subsystems/templates/tr/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-${brand} kurtarma kodunuz ${code}.
-
-Şifre sıfırlama işlemini tamamlamak için bu kodu kullanın.
diff --git a/libs/wire-subsystems/templates/uk/user/call/activation.txt b/libs/wire-subsystems/templates/uk/user/call/activation.txt
deleted file mode 100644
index ba6af2fca7..0000000000
--- a/libs/wire-subsystems/templates/uk/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Привіт, ваш код підтвердження Wire ${code}. Ще раз, ваш код підтвердження: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/uk/user/call/login.txt b/libs/wire-subsystems/templates/uk/user/call/login.txt
deleted file mode 100644
index 6225ce7e68..0000000000
--- a/libs/wire-subsystems/templates/uk/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Привіт, ваш код для входу в Wire ${code}. Ще раз, ваш код для входу: ${code}
diff --git a/libs/wire-subsystems/templates/uk/user/sms/activation.txt b/libs/wire-subsystems/templates/uk/user/sms/activation.txt
deleted file mode 100644
index 06aee57582..0000000000
--- a/libs/wire-subsystems/templates/uk/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Ваш код Wire: ${code}.
-
-Використовуйте його, щоб завершити реєстрацію.
diff --git a/libs/wire-subsystems/templates/uk/user/sms/deletion.txt b/libs/wire-subsystems/templates/uk/user/sms/deletion.txt
deleted file mode 100644
index 3950a2bd4e..0000000000
--- a/libs/wire-subsystems/templates/uk/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Торкніться, щоб видалити ваш Wire-акаунт.
-${url}
diff --git a/libs/wire-subsystems/templates/uk/user/sms/login.txt b/libs/wire-subsystems/templates/uk/user/sms/login.txt
deleted file mode 100644
index 1bd6bcb75a..0000000000
--- a/libs/wire-subsystems/templates/uk/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Ваш код Wire: ${code}.
-
-Використовуйте його, щоб увійти в Wire.
diff --git a/libs/wire-subsystems/templates/uk/user/sms/password-reset.txt b/libs/wire-subsystems/templates/uk/user/sms/password-reset.txt
deleted file mode 100644
index c40a0ecfed..0000000000
--- a/libs/wire-subsystems/templates/uk/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Ваш код Wire: ${code}.
-
-Використовуйте його, щоб завершити скидання паролю.
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/vi/user/call/activation.txt b/libs/wire-subsystems/templates/vi/user/call/activation.txt
deleted file mode 100644
index bc29d9b108..0000000000
--- a/libs/wire-subsystems/templates/vi/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-Xin chào, mã xác thực Wire của bạn là: ${code}. Một lần nữa, mã của bạn là: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/vi/user/call/login.txt b/libs/wire-subsystems/templates/vi/user/call/login.txt
deleted file mode 100644
index d1e101d5e5..0000000000
--- a/libs/wire-subsystems/templates/vi/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-Xin chào, mã đăng nhập Wire của bạn là: ${code}. Một lần nữa, mã của bạn là: ${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/vi/user/sms/activation.txt b/libs/wire-subsystems/templates/vi/user/sms/activation.txt
deleted file mode 100644
index e9987182a5..0000000000
--- a/libs/wire-subsystems/templates/vi/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Mã ${brand} của bạn là ${code}.
-
-Mở ${url} để xác minh số điện thoại của bạn.
diff --git a/libs/wire-subsystems/templates/vi/user/sms/deletion.txt b/libs/wire-subsystems/templates/vi/user/sms/deletion.txt
deleted file mode 100644
index 63b7431b40..0000000000
--- a/libs/wire-subsystems/templates/vi/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-Chạm để xoá tài khoản ${brand} của bạn.
-${url}
diff --git a/libs/wire-subsystems/templates/vi/user/sms/login.txt b/libs/wire-subsystems/templates/vi/user/sms/login.txt
deleted file mode 100644
index e12fa3949b..0000000000
--- a/libs/wire-subsystems/templates/vi/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Mã đăng nhập ${brand} của bạn là ${code}.
-
-Mở ${url} để đăng nhập.
diff --git a/libs/wire-subsystems/templates/vi/user/sms/password-reset.txt b/libs/wire-subsystems/templates/vi/user/sms/password-reset.txt
deleted file mode 100644
index de5c4600cd..0000000000
--- a/libs/wire-subsystems/templates/vi/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Mã khôi phục ${brand} của bạn là ${code}.
-
-Sử dụng mã này để hoàn tất việc đặt lại mật khẩu.
diff --git a/libs/wire-subsystems/templates/zh-TW/user/call/activation.txt b/libs/wire-subsystems/templates/zh-TW/user/call/activation.txt
deleted file mode 100644
index 614602910a..0000000000
--- a/libs/wire-subsystems/templates/zh-TW/user/call/activation.txt
+++ /dev/null
@@ -1 +0,0 @@
-您好,您的Wire驗證碼是:${code} 再重複一次,您的驗證碼是:${code}
\ No newline at end of file
diff --git a/libs/wire-subsystems/templates/zh-TW/user/call/login.txt b/libs/wire-subsystems/templates/zh-TW/user/call/login.txt
deleted file mode 100644
index c9454cab36..0000000000
--- a/libs/wire-subsystems/templates/zh-TW/user/call/login.txt
+++ /dev/null
@@ -1 +0,0 @@
-您好,您的Wire驗證碼是:${code} 再重複一次,您的驗證碼是:${code}
diff --git a/libs/wire-subsystems/templates/zh-TW/user/sms/activation.txt b/libs/wire-subsystems/templates/zh-TW/user/sms/activation.txt
deleted file mode 100644
index 8a05f20575..0000000000
--- a/libs/wire-subsystems/templates/zh-TW/user/sms/activation.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-您的Wire驗證碼是${code}。
-
-請開啟${url} 或手動輸入以上的驗證碼來完成驗證程序。
diff --git a/libs/wire-subsystems/templates/zh-TW/user/sms/deletion.txt b/libs/wire-subsystems/templates/zh-TW/user/sms/deletion.txt
deleted file mode 100644
index dadb28e6f9..0000000000
--- a/libs/wire-subsystems/templates/zh-TW/user/sms/deletion.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-點一下來刪除您的Wire帳號
-${url}
diff --git a/libs/wire-subsystems/templates/zh-TW/user/sms/login.txt b/libs/wire-subsystems/templates/zh-TW/user/sms/login.txt
deleted file mode 100644
index 4f7f65b11a..0000000000
--- a/libs/wire-subsystems/templates/zh-TW/user/sms/login.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-您的Wire驗證碼是${code}。
-
-開啟${url} 來登入,或手動輸入此代碼來完成程序。
diff --git a/libs/wire-subsystems/templates/zh-TW/user/sms/password-reset.txt b/libs/wire-subsystems/templates/zh-TW/user/sms/password-reset.txt
deleted file mode 100644
index 75973fa737..0000000000
--- a/libs/wire-subsystems/templates/zh-TW/user/sms/password-reset.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-您的Wire恢復代碼是${code}。
-
-請開啟Wire然後輸入此代碼來完成密碼重設程序。
\ No newline at end of file
diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs
index 558c9742df..fcb8cb2716 100644
--- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs
+++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/EmailSubsystem.hs
@@ -55,3 +55,5 @@ noopEmailSubsystemInterpreter = interpret \case
SendTeamDeletionVerificationMail {} -> pure ()
SendTeamInvitationMail {} -> pure ""
SendTeamInvitationMailPersonalUser {} -> pure ""
+ SendMemberWelcomeEmail {} -> pure ()
+ SendNewTeamOwnerWelcomeEmail {} -> pure ()
diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal
index ff58ed1943..6a2bc1b7ae 100644
--- a/libs/wire-subsystems/wire-subsystems.cabal
+++ b/libs/wire-subsystems/wire-subsystems.cabal
@@ -246,6 +246,8 @@ library
Wire.EmailSubsystem
Wire.EmailSubsystem.Interpreter
Wire.EmailSubsystem.Template
+ Wire.EmailSubsystem.Templates.Team
+ Wire.EmailSubsystem.Templates.User
Wire.EnterpriseLoginSubsystem
Wire.EnterpriseLoginSubsystem.Error
Wire.EnterpriseLoginSubsystem.Interpreter
diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal
index 67449f7bb0..fd039667b1 100644
--- a/services/brig/brig.cabal
+++ b/services/brig/brig.cabal
@@ -191,7 +191,6 @@ library
Brig.Schema.V90_DomainRegistrationTeamIndex
Brig.Schema.V91_UpdateDomainRegistrationSchema_AddWebappUrl
Brig.Team.API
- Brig.Team.Email
Brig.Team.Template
Brig.Template
Brig.User.API.Handle
@@ -366,6 +365,7 @@ executable brig-integration
API.Team
API.Team.Util
API.TeamUserSearch
+ API.Template
API.User
API.User.Account
API.User.Auth
diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml
index a711f0e382..7d39b7166d 100644
--- a/services/brig/brig.integration.yaml
+++ b/services/brig/brig.integration.yaml
@@ -126,25 +126,25 @@ emailSMS:
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}
+ activationUrl: https://example.com/verify/?key=${key}&code=${code}
+ smsActivationUrl: https://example.com/v/${code}
+ passwordResetUrl: https://example.com/reset/?key=${key}&code=${code}
+ invitationUrl: https://example.com/register?invitation_code=${code}
+ deletionUrl: https://example.com/d/?key=${key}&code=${code}
provider:
homeUrl: https://provider.localhost/
- providerActivationUrl: http://127.0.0.1:8080/provider/activate?key=${key}&code=${code}
+ providerActivationUrl: http://127.0.0.1:8080/verify/bot/?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}
+ providerPwResetUrl: http://127.0.0.1:8080/reset/bot/?key=${key}&code=${code}
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
+ tInvitationUrl: https://example.com/join/?team-code=${code}
+ tExistingUserInvitationUrl: https://example.com/accept-invitation/?team-code=${code}
+ tActivationUrl: https://example.com/verify/?key=${key}&code=${code}
+ tCreatorWelcomeUrl: https://example.com/creator-welcome-website
+ tMemberWelcomeUrl: https://example.com/member-welcome-website
zauth:
privateKeys: ../../libs/wire-subsystems/test/resources/zauth/privkeys.txt
diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs
index e4fbf8ed6c..4ffc108dbf 100644
--- a/services/brig/src/Brig/API/Public.hs
+++ b/services/brig/src/Brig/API/Public.hs
@@ -52,7 +52,7 @@ import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore)
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.Template (InvitationUrlTemplates)
import Brig.Types.Activation (ActivationPair)
import Brig.Types.Intra
import Brig.User.API.Handle qualified as Handle
@@ -167,7 +167,6 @@ import Wire.DeleteQueue
import Wire.DomainRegistrationStore (DomainRegistrationStore)
import Wire.EmailSending (EmailSending)
import Wire.EmailSubsystem
-import Wire.EmailSubsystem.Template
import Wire.EnterpriseLoginSubsystem (EnterpriseLoginSubsystem)
import Wire.EnterpriseLoginSubsystem qualified as EnterpriseLogin
import Wire.Error
@@ -393,7 +392,7 @@ servantSitemap ::
Member UserKeyStore r,
Member ActivationCodeStore r,
Member UserStore r,
- Member (Input TeamTemplates) r,
+ Member (Input InvitationUrlTemplates) r,
Member UserSubsystem r,
Member TeamInvitationSubsystem r,
Member VerificationCodeSubsystem r,
@@ -906,7 +905,7 @@ upgradePersonalToTeam ::
Member TinyLog r,
Member UserSubsystem r,
Member UserStore r,
- Member EmailSending r
+ Member EmailSubsystem r
) =>
Local UserId ->
Public.BindingNewTeamUser ->
@@ -930,7 +929,6 @@ createUser ::
Member UserSubsystem r,
Member PasswordResetCodeStore r,
Member HashPassword r,
- Member EmailSending r,
Member ActivationCodeStore r,
Member RateLimit r,
Member AuthenticationSubsystem r
@@ -992,15 +990,15 @@ createUser ip (Public.NewUserPublic new) = lift . runExceptT $ do
| otherwise =
liftSem $ sendActivationMail email name key code locale
- sendWelcomeEmail :: (Member EmailSending r) => Public.EmailAddress -> Public.CreateUserTeam -> Public.NewTeamUser -> Maybe Public.Locale -> (AppT r) ()
+ sendWelcomeEmail :: (Member EmailSubsystem 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 (Public.CreateUserTeam t n) newUser l = case newUser of
Public.NewTeamCreator _ ->
pure ()
Public.NewTeamMember _ ->
- Team.sendMemberWelcomeMail e t n l
+ liftSem $ sendMemberWelcomeEmail e t n l
Public.NewTeamMemberSSO _ ->
- Team.sendMemberWelcomeMail e t n l
+ liftSem $ sendMemberWelcomeEmail e t n l
getSelf :: (Member UserSubsystem r) => Local UserId -> Handler r Public.SelfProfile
getSelf self =
diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs
index 7c0397cefe..8ef4f40337 100644
--- a/services/brig/src/Brig/API/User.hs
+++ b/services/brig/src/Brig/API/User.hs
@@ -80,7 +80,6 @@ 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
@@ -130,7 +129,6 @@ import Wire.ActivationCodeStore qualified as ActivationCode
import Wire.AuthenticationSubsystem (AuthenticationSubsystem, internalLookupPasswordResetCode)
import Wire.BlockListStore as BlockListStore
import Wire.DeleteQueue
-import Wire.EmailSending
import Wire.EmailSubsystem
import Wire.Error
import Wire.Events (Events)
@@ -273,7 +271,7 @@ upgradePersonalToTeam ::
Member (Input (Local ())) r,
Member Now r,
Member (ConnectionStore InternalPaging) r,
- Member EmailSending r
+ Member EmailSubsystem r
) =>
Local UserId ->
BindingNewTeamUser ->
@@ -301,12 +299,13 @@ upgradePersonalToTeam luid bNewTeam = do
-- send confirmation email
for_ (userEmail user) $ \email -> do
- sendNewTeamOwnerWelcomeEmail
- email
- tid
- bNewTeam.bnuTeam.newTeamName.fromRange
- (Just user.userLocale)
- user.userDisplayName
+ liftSem $
+ sendNewTeamOwnerWelcomeEmail
+ email
+ tid
+ bNewTeam.bnuTeam.newTeamName.fromRange
+ (Just user.userLocale)
+ user.userDisplayName
pure $! createUserTeam
where
diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs
index a4be8cebff..cbdcbe1469 100644
--- a/services/brig/src/Brig/App.hs
+++ b/services/brig/src/Brig/App.hs
@@ -32,7 +32,7 @@ module Brig.App
closeEnv,
providerTemplatesWithLocale,
teamTemplatesWithLocale,
- teamTemplatesNoLocale,
+ invitationUrlTemplates,
cargoholdLens,
galleyLens,
galleyEndpointLens,
@@ -53,6 +53,7 @@ module Brig.App
providerTemplatesLens,
teamTemplatesLens,
templateBrandingLens,
+ templateBrandingAsMapLens,
httpManagerLens,
http2ManagerLens,
extGetManagerLens,
@@ -113,7 +114,7 @@ import Brig.Queue.Stomp qualified as Stomp
import Brig.Queue.Types
import Brig.Schema.Run qualified as Migrations
import Brig.Team.Template
-import Brig.Template (Localised, genTemplateBranding)
+import Brig.Template (InvitationUrlTemplates (..), Localised, genTemplateBranding, genTemplateBrandingMap)
import Brig.User.Search.Index (IndexEnv (..), MonadIndexIO (..), runIndexIO)
import Brig.User.Template
import Cassandra (runClient)
@@ -165,6 +166,7 @@ import Wire.AuthenticationSubsystem.Config (ZAuthEnv)
import Wire.AuthenticationSubsystem.Config qualified as AuthenticationSubsystem
import Wire.EmailSending.SMTP qualified as SMTP
import Wire.EmailSubsystem.Template (TemplateBranding, forLocale)
+import Wire.EmailSubsystem.Templates.User
import Wire.ExternalAccess.External
import Wire.RateLimit.Interpreter
import Wire.SessionStore
@@ -201,6 +203,7 @@ data Env = Env
providerTemplates :: Localised ProviderTemplates,
teamTemplates :: Localised TeamTemplates,
templateBranding :: TemplateBranding,
+ templateBrandingAsMap :: Map Text Text,
httpManager :: Manager,
http2Manager :: Http2Manager,
extGetManager :: (Manager, [Fingerprint Rsa] -> SSL.SSL -> IO ()),
@@ -238,6 +241,7 @@ newEnv opts = do
ptp <- loadProviderTemplates opts
ttp <- loadTeamTemplates opts
let branding = genTemplateBranding . Opt.templateBranding . Opt.general . Opt.emailSMS $ opts
+ brandingAsMap = genTemplateBrandingMap . 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
@@ -296,6 +300,7 @@ newEnv opts = do
providerTemplates = ptp,
teamTemplates = ttp,
templateBranding = branding,
+ templateBrandingAsMap = brandingAsMap,
httpManager = mgr,
http2Manager = h2Mgr,
extGetManager = ext,
@@ -444,10 +449,16 @@ teamTemplatesWithLocale l = forLocale l <$> asks (.teamTemplates)
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 <$> teamTemplatesWithLocale Nothing
+invitationUrlTemplates :: (MonadReader Env m) => m InvitationUrlTemplates
+invitationUrlTemplates = do
+ -- 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).
+ teamTemplates <- snd <$> teamTemplatesWithLocale Nothing
+ pure $
+ InvitationUrlTemplates
+ { personalUser = teamTemplates.existingUserInvitationEmail.invitationEmailUrl,
+ newUser = teamTemplates.invitationEmail.invitationEmailUrl
+ }
closeEnv :: Env -> IO ()
closeEnv e = do
diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs
index 13bef0bfbc..727526a406 100644
--- a/services/brig/src/Brig/CanonicalInterpreter.hs
+++ b/services/brig/src/Brig/CanonicalInterpreter.hs
@@ -30,7 +30,7 @@ import Brig.Effects.UserPendingActivationStore.Cassandra (userPendingActivationS
import Brig.IO.Intra (runEvents)
import Brig.Options (federationDomainConfigs, federationStrategy)
import Brig.Options qualified as Opt
-import Brig.Team.Template (TeamTemplates)
+import Brig.Template (InvitationUrlTemplates)
import Brig.User.Search.Index (IndexEnv (..))
import Cassandra qualified as Cas
import Control.Exception (ErrorCall)
@@ -210,7 +210,7 @@ type BrigLowerLevelEffects =
Input VerificationCodeThrottleTTL,
Input (Local ()),
Input (AuthenticationSubsystemConfig),
- Input TeamTemplates,
+ Input InvitationUrlTemplates,
GundeckAPIAccess,
FederationConfigStore,
Jwk,
@@ -345,7 +345,7 @@ runBrigToIO e (AppT ma) = do
. interpretJwk
. interpretFederationDomainConfig e.casClient e.settings.federationStrategy (foldMap (remotesMapFromCfgFile . fmap (.federationDomainConfig)) e.settings.federationDomainConfigs)
. runGundeckAPIAccess e.gundeckEndpoint
- . runInputConst (teamTemplatesNoLocale e)
+ . runInputConst (invitationUrlTemplates e)
. runInputConst authenticationSubsystemConfig
. runInputConst localUnit
. runInputConst (fromIntegral $ Opt.twoFACodeGenerationDelaySecs e.settings)
@@ -389,7 +389,7 @@ runBrigToIO e (AppT ma) = do
. runDeleteQueue e.internalEvents
. interpretPropertySubsystem propertySubsystemConfig
. interpretVerificationCodeSubsystem
- . emailSubsystemInterpreter e.userTemplates e.teamTemplates e.templateBranding
+ . emailSubsystemInterpreter e.userTemplates e.teamTemplates e.templateBrandingAsMap
. interpretAppStoreToPostgres
. interpretTeamCollaboratorsStoreToPostgres
. interpretTeamSubsystemToGalleyAPI
diff --git a/services/brig/src/Brig/Provider/Email.hs b/services/brig/src/Brig/Provider/Email.hs
index b5c168d4e3..5d8b558dce 100644
--- a/services/brig/src/Brig/Provider/Email.hs
+++ b/services/brig/src/Brig/Provider/Email.hs
@@ -31,6 +31,7 @@ import Data.Range
import Data.Text (pack)
import Data.Text.Ascii qualified as Ascii
import Data.Text.Lazy qualified as LT
+import Data.Text.Template
import Imports
import Network.Mail.Mime
import Polysemy
diff --git a/services/brig/src/Brig/Provider/Template.hs b/services/brig/src/Brig/Provider/Template.hs
index 2c50f22fcd..50a0eab4c9 100644
--- a/services/brig/src/Brig/Provider/Template.hs
+++ b/services/brig/src/Brig/Provider/Template.hs
@@ -21,11 +21,7 @@ module Brig.Provider.Template
ApprovalRequestEmailTemplate (..),
ApprovalConfirmEmailTemplate (..),
PasswordResetEmailTemplate (..),
- -- , TODO: NewServiceEmailTemplate (..)
loadProviderTemplates,
-
- -- * Re-exports
- Template,
)
where
@@ -34,9 +30,10 @@ import Brig.Template
import Data.ByteString.Conversion (fromByteString)
import Data.Misc (HttpsUrl)
import Data.Text.Encoding (encodeUtf8)
+import Data.Text.Template
import Imports
import Wire.API.User.Identity
-import Wire.EmailSubsystem.Template
+import Wire.EmailSubsystem.Templates.User
data ProviderTemplates = ProviderTemplates
{ activationEmail :: !ActivationEmailTemplate,
@@ -65,15 +62,6 @@ data ApprovalConfirmEmailTemplate = ApprovalConfirmEmailTemplate
approvalConfirmEmailHomeUrl :: !HttpsUrl
}
--- TODO
--- data NewServiceEmailTemplate = NewServiceEmailTemplate
--- { newServiceEmailSubject :: !Template
--- , newServiceEmailBodyText :: !Template
--- , newServiceEmailBodyHtml :: !Template
--- , newServiceEmailSender :: !Email
--- , newServiceEmailSenderName :: !Text
--- }
-
loadProviderTemplates :: Opts -> IO (Localised ProviderTemplates)
loadProviderTemplates o = readLocalesDir defLocale (templateDir gOptions) "provider" $ \fp ->
ProviderTemplates
diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs
index 5b8022812e..2ee190aa81 100644
--- a/services/brig/src/Brig/Team/API.hs
+++ b/services/brig/src/Brig/Team/API.hs
@@ -32,6 +32,7 @@ import Brig.API.User qualified as API
import Brig.API.Util (logEmail, logInvitationCode)
import Brig.App as App
import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore)
+import Brig.Template
import Brig.Types.Team (TeamSize)
import Control.Lens (view, (^.))
import Control.Monad.Trans.Except
@@ -47,6 +48,7 @@ import Network.Wai.Utilities hiding (Error, code, message)
import Polysemy
import Polysemy.Error
import Polysemy.Input (Input, input)
+import Polysemy.Output (ignoreOutput)
import Polysemy.TinyLog (TinyLog)
import Polysemy.TinyLog qualified as Log
import Servant hiding (Handler, JSON, addHeader)
@@ -70,7 +72,6 @@ import Wire.API.User hiding (fromEmail)
import Wire.AuthenticationSubsystem
import Wire.BlockListStore
import Wire.EmailSubsystem.Interpreter (renderInvitationUrl)
-import Wire.EmailSubsystem.Template
import Wire.Error
import Wire.Events (Events)
import Wire.GalleyAPIAccess (GalleyAPIAccess, ShowOrHideInvitationUrl (..))
@@ -95,7 +96,7 @@ servantAPI ::
Member UserSubsystem r,
Member Store.InvitationStore r,
Member TinyLog r,
- Member (Input TeamTemplates) r,
+ Member (Input InvitationUrlTemplates) r,
Member (Input (Local ())) r,
Member (Error UserSubsystemError) r,
Member IndexedUserStore r,
@@ -221,7 +222,7 @@ listInvitations ::
( Member GalleyAPIAccess r,
Member TinyLog r,
Member InvitationStore r,
- Member (Input TeamTemplates) r,
+ Member (Input InvitationUrlTemplates) r,
Member (Input (Local ())) r,
Member UserSubsystem r,
Member (Error UserSubsystemError) r,
@@ -253,15 +254,15 @@ listInvitations uid tid startingId mSize = do
isPersonalUserMigration <- isPersonalUser (mkEmailKey si.email)
template <-
if isPersonalUserMigration
- then invitationEmailUrl . existingUserInvitationEmail <$> input
- else invitationEmailUrl . invitationEmail <$> input
- let url = renderInvitationUrl template tid si.code id
+ then (.personalUser) <$> input @InvitationUrlTemplates
+ else (.newUser) <$> input @InvitationUrlTemplates
+ url <- ignoreOutput $ renderInvitationUrl template tid si.code
toInvitation url ShowInvitationUrl si
mkInviteUrl ::
forall r.
( Member TinyLog r,
- Member (Input TeamTemplates) r
+ Member (Input InvitationUrlTemplates) r
) =>
ShowOrHideInvitationUrl ->
TeamId ->
@@ -269,8 +270,8 @@ mkInviteUrl ::
Sem r (Maybe (URIRef Absolute))
mkInviteUrl HideInvitationUrl _ _ = pure Nothing
mkInviteUrl ShowInvitationUrl team c = do
- template <- invitationEmailUrl . invitationEmail <$> input
- let url = renderInvitationUrl template team c id
+ template <- (.newUser) <$> input
+ url <- ignoreOutput $ renderInvitationUrl template team c
parseHttpsUrl url
where
parseHttpsUrl :: Text -> Sem r (Maybe (URIRef Absolute))
@@ -288,7 +289,7 @@ getInvitation ::
( Member GalleyAPIAccess r,
Member InvitationStore r,
Member TinyLog r,
- Member (Input TeamTemplates) r,
+ Member (Input InvitationUrlTemplates) r,
Member (Error UserSubsystemError) r,
Member TeamSubsystem r
) =>
diff --git a/services/brig/src/Brig/Team/Email.hs b/services/brig/src/Brig/Team/Email.hs
deleted file mode 100644
index 90dc351948..0000000000
--- a/services/brig/src/Brig/Team/Email.hs
+++ /dev/null
@@ -1,98 +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.Email
- ( sendMemberWelcomeMail,
- sendNewTeamOwnerWelcomeEmail,
- )
-where
-
-import Brig.App
-import Brig.Team.Template
-import Data.Id (TeamId, idToText)
-import Data.Text.Lazy (toStrict)
-import Imports
-import Network.Mail.Mime
-import Polysemy
-import Wire.API.User
-import Wire.EmailSending
-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)
- liftSem $ sendMail $ renderMemberWelcomeMail to tid teamName tpl branding
-
-sendNewTeamOwnerWelcomeEmail :: (Member EmailSending r) => EmailAddress -> TeamId -> Text -> Maybe Locale -> Name -> (AppT r) ()
-sendNewTeamOwnerWelcomeEmail to tid teamName loc profileName = do
- tpl <- newTeamOwnerWelcomeEmail . snd <$> teamTemplatesWithLocale loc
- branding <- asks (.templateBranding)
- liftSem $ sendMail $ renderNewTeamOwnerWelcomeEmail to tid teamName profileName tpl branding
-
--------------------------------------------------------------------------------
--- Member Welcome Email
-
-renderMemberWelcomeMail :: EmailAddress -> TeamId -> Text -> MemberWelcomeEmailTemplate -> TemplateBranding -> Mail
-renderMemberWelcomeMail emailTo tid teamName MemberWelcomeEmailTemplate {..} branding =
- (emptyMail from)
- { mailTo = [to],
- mailHeaders =
- [ ("Subject", toStrict subj),
- ("X-Zeta-Purpose", "Welcome")
- ],
- mailParts = [[plainPart txt, htmlPart html]]
- }
- where
- from = Address (Just memberWelcomeEmailSenderName) (fromEmail memberWelcomeEmailSender)
- 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 emailTo
- replace "team_id" = idToText tid
- replace "team_name" = teamName
- replace x = x
-
--------------------------------------------------------------------------------
--- New Team Owner Welcome Email
-
-renderNewTeamOwnerWelcomeEmail :: EmailAddress -> TeamId -> Text -> Name -> NewTeamOwnerWelcomeEmailTemplate -> TemplateBranding -> Mail
-renderNewTeamOwnerWelcomeEmail emailTo tid teamName profileName NewTeamOwnerWelcomeEmailTemplate {..} branding =
- (emptyMail from)
- { mailTo = [to],
- mailHeaders =
- [ ("Subject", toStrict subj),
- ("X-Zeta-Purpose", "Welcome")
- ],
- mailParts = [[plainPart txt, htmlPart html]]
- }
- where
- from = Address (Just newTeamOwnerWelcomeEmailSenderName) (fromEmail newTeamOwnerWelcomeEmailSender)
- to = Address Nothing (fromEmail emailTo)
- txt = renderTextWithBranding newTeamOwnerWelcomeEmailBodyText replace branding
- html = renderHtmlWithBranding newTeamOwnerWelcomeEmailBodyHtml replace branding
- subj = renderTextWithBranding newTeamOwnerWelcomeEmailSubject replace branding
- replace "url" = newTeamOwnerWelcomeEmailUrl
- replace "email" = fromEmail emailTo
- replace "team_id" = idToText tid
- replace "team_name" = teamName
- replace "name" = profileName.fromName
- replace x = x
diff --git a/services/brig/src/Brig/Team/Template.hs b/services/brig/src/Brig/Team/Template.hs
index 4ca5feccfd..2d2526d558 100644
--- a/services/brig/src/Brig/Team/Template.hs
+++ b/services/brig/src/Brig/Team/Template.hs
@@ -18,19 +18,16 @@
module Brig.Team.Template
( TeamTemplates (..),
InvitationEmailTemplate (..),
- CreatorWelcomeEmailTemplate (..),
MemberWelcomeEmailTemplate (..),
loadTeamTemplates,
-
- -- * Re-exports
- Template,
)
where
import Brig.Options
import Brig.Template
+import Data.Text.Template
import Imports
-import Wire.EmailSubsystem.Template
+import Wire.EmailSubsystem.Templates.Team
loadTeamTemplates :: Opts -> IO (Localised TeamTemplates)
loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \fp ->
@@ -49,13 +46,6 @@ loadTeamTemplates o = readLocalesDir defLocale (templateDir gOptions) "team" $ \
<*> 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"
- <*> readTemplate fp "email/new-creator-welcome.html"
- <*> pure (emailSender gOptions)
- <*> readText fp "email/sender.txt"
- )
<*> ( MemberWelcomeEmailTemplate (tMemberWelcomeUrl tOptions)
<$> readTemplate fp "email/new-member-welcome-subject.txt"
<*> readTemplate fp "email/new-member-welcome.txt"
diff --git a/services/brig/src/Brig/Template.hs b/services/brig/src/Brig/Template.hs
index 906f395d8e..778c59815a 100644
--- a/services/brig/src/Brig/Template.hs
+++ b/services/brig/src/Brig/Template.hs
@@ -19,18 +19,13 @@
-- | Common templating utilities.
module Brig.Template
- ( -- * Reading templates
+ ( InvitationUrlTemplates (..),
Localised,
readLocalesDir,
readTemplateWithDefault,
readTextWithDefault,
-
- -- * Rendering templates
genTemplateBranding,
-
- -- * Re-exports
- Template,
- template,
+ genTemplateBrandingMap,
)
where
@@ -46,6 +41,11 @@ import System.IO.Error (isDoesNotExistError)
import Wire.API.User
import Wire.EmailSubsystem.Template (Localised (Localised))
+data InvitationUrlTemplates = InvitationUrlTemplates
+ { personalUser :: Template,
+ newUser :: Template
+ }
+
-- | See 'genTemplateBranding'.
type TemplateBranding = Text -> Text
@@ -150,3 +150,18 @@ genTemplateBranding BrandingOpts {..} = fn
fn "forgot" = forgot
fn "support" = support
fn other = other
+
+genTemplateBrandingMap :: BrandingOpts -> Map Text Text
+genTemplateBrandingMap opts =
+ Map.fromList
+ [ ("brand", opts.brand),
+ ("brand_url", opts.brandUrl),
+ ("brand_label_url", opts.brandLabelUrl),
+ ("brand_logo", opts.brandLogoUrl),
+ ("brand_service", opts.brandService),
+ ("copyright", opts.copyright),
+ ("misuse", opts.misuse),
+ ("legal", opts.legal),
+ ("forgot", opts.forgot),
+ ("support", opts.support)
+ ]
diff --git a/services/brig/src/Brig/User/Template.hs b/services/brig/src/Brig/User/Template.hs
index 110d979bc8..ff5304519e 100644
--- a/services/brig/src/Brig/User/Template.hs
+++ b/services/brig/src/Brig/User/Template.hs
@@ -15,44 +15,18 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
-module Brig.User.Template
- ( UserTemplates (..),
- ActivationSmsTemplate (..),
- VerificationEmailTemplate (..),
- ActivationEmailTemplate (..),
- TeamActivationEmailTemplate (..),
- ActivationCallTemplate (..),
- PasswordResetSmsTemplate (..),
- PasswordResetEmailTemplate (..),
- LoginSmsTemplate (..),
- LoginCallTemplate (..),
- DeletionSmsTemplate (..),
- DeletionEmailTemplate (..),
- NewClientEmailTemplate (..),
- SecondFactorVerificationEmailTemplate (..),
- loadUserTemplates,
-
- -- * Re-exports
- Template,
- )
-where
+module Brig.User.Template (loadUserTemplates) where
import Brig.Options qualified as Opt
import Brig.Template
+import Data.Text.Template
import Imports
-import Wire.EmailSubsystem.Template
+import Wire.EmailSubsystem.Templates.User
loadUserTemplates :: Opt.Opts -> IO (Localised UserTemplates)
loadUserTemplates o = readLocalesDir defLocale templateDir "user" $ \fp ->
UserTemplates
- <$> ( ActivationSmsTemplate smsActivationUrl
- <$> readTemplate fp "sms/activation.txt"
- <*> pure smsSender
- )
- <*> ( ActivationCallTemplate
- <$> readTemplate fp "call/activation.txt"
- )
- <*> ( VerificationEmailTemplate activationUrl
+ <$> ( VerificationEmailTemplate activationUrl
<$> readTemplate fp "email/verification-subject.txt"
<*> readTemplate fp "email/verification.txt"
<*> readTemplate fp "email/verification.html"
@@ -80,10 +54,6 @@ loadUserTemplates o = readLocalesDir defLocale templateDir "user" $ \fp ->
<*> pure emailSender
<*> readText fp "email/sender.txt"
)
- <*> ( PasswordResetSmsTemplate
- <$> readTemplate fp "sms/password-reset.txt"
- <*> pure smsSender
- )
<*> ( PasswordResetEmailTemplate passwordResetUrl
<$> readTemplate fp "email/password-reset-subject.txt"
<*> readTemplate fp "email/password-reset.txt"
@@ -91,17 +61,6 @@ loadUserTemplates o = readLocalesDir defLocale templateDir "user" $ \fp ->
<*> pure emailSender
<*> readText fp "email/sender.txt"
)
- <*> ( LoginSmsTemplate smsActivationUrl
- <$> readTemplate fp "sms/login.txt"
- <*> pure smsSender
- )
- <*> ( LoginCallTemplate
- <$> readTemplate fp "call/login.txt"
- )
- <*> ( DeletionSmsTemplate deletionUserUrl
- <$> readTemplate fp "sms/deletion.txt"
- <*> pure smsSender
- )
<*> ( DeletionEmailTemplate deletionUserUrl
<$> readTemplate fp "email/deletion-subject.txt"
<*> readTemplate fp "email/deletion.txt"
@@ -142,8 +101,6 @@ loadUserTemplates o = readLocalesDir defLocale templateDir "user" $ \fp ->
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
diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs
index a8afd847fa..174eb48a4b 100644
--- a/services/brig/test/integration/API/Team.hs
+++ b/services/brig/test/integration/API/Team.hs
@@ -43,7 +43,6 @@ 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.UUID qualified as UUID (fromString)
import Data.UUID.V4 qualified as UUID
import Imports
@@ -251,8 +250,7 @@ testInvitationUrl opts brig = do
isJust invCode @? "Expect an invitation code in the backend"
Just inviter @=? inv.createdBy
tid @=? inv.team
- getQueryParam "team_code" resp @=? (invCode <&> (toStrict . toByteString))
- getQueryParam "team" resp @=? (pure . encodeUtf8 . idToText) tid
+ getQueryParam "team-code" resp @=? (invCode <&> (toStrict . toByteString))
getQueryParam :: ByteString -> ResponseLBS -> Maybe ByteString
getQueryParam name r = do
diff --git a/services/brig/test/integration/API/Template.hs b/services/brig/test/integration/API/Template.hs
new file mode 100644
index 0000000000..d5ef52cc0e
--- /dev/null
+++ b/services/brig/test/integration/API/Template.hs
@@ -0,0 +1,235 @@
+module API.Template (tests) where
+
+import Bilge
+import Brig.Options
+import Brig.Team.Template (loadTeamTemplates)
+import Brig.Template
+import Brig.User.Template (loadUserTemplates)
+import Data.Code
+import Data.Id
+import Data.Json.Util
+import Data.Map qualified as Map
+import Data.Range
+import Data.Text.Ascii (AsciiChars (validate), encodeBase64Url)
+import Data.Text.Ascii qualified as Ascii
+import Data.Time (UTCTime (..), fromGregorian, secondsToDiffTime)
+import Data.UUID qualified as UUID
+import Imports
+import Network.Mail.Mime
+import Polysemy
+import Polysemy.Output
+import Test.Tasty
+import Test.Tasty.HUnit
+import Util
+import Wire.API.Locale
+import Wire.API.User (InvitationCode (InvitationCode, fromInvitationCode))
+import Wire.API.User.Activation
+import Wire.API.User.Client (Client (..), ClientCapabilityList (..), ClientType (..))
+import Wire.API.User.EmailAddress
+import Wire.API.User.Password
+import Wire.API.User.Profile
+import Wire.EmailSubsystem.Interpreter
+import Wire.EmailSubsystem.Template
+import Wire.EmailSubsystem.Templates.Team
+import Wire.EmailSubsystem.Templates.User
+
+tests :: Opts -> Manager -> IO TestTree
+tests opts m = do
+ team <- liftIO $ loadTeamTemplates opts
+ user <- liftIO $ loadUserTemplates opts
+ let teamTemplates = Map.assocs $ uncurry Map.insert team.locDefault team.locOther
+ userTemplates = Map.assocs $ uncurry Map.insert user.locDefault user.locOther
+ b = genTemplateBrandingMap opts.emailSMS.general.templateBranding
+ pure $
+ testGroup
+ "email templates"
+ [ testGroup
+ "team"
+ $ fmap
+ ( \(loc, ts) ->
+ testGroup
+ (show loc)
+ [ test m "team invitation" $ testTeamInvitationEmail b ts,
+ test m "team invitation existing user" $ testTeamInvitationEmailExistingUser b ts,
+ test m "member welcome" $ testMemberWelcomeEmail b ts,
+ test m "new team owner welcome" $ testNewTeamOwnerWelcomeEmail b ts
+ ]
+ )
+ teamTemplates,
+ testGroup "user" $
+ fmap
+ ( \(loc, ts) ->
+ testGroup
+ (show loc)
+ [ test m "password reset email" $ testPasswordResetEmail b ts,
+ test m "verification email" $ testVerificationEmail b ts,
+ test m "team deletion verification email" $ testTeamDeletionVerificationEmail b ts,
+ test m "scim token verification email" $ testScimTokenVerificationEmail b ts,
+ test m "login verification email" $ testLoginVerificationEmail b ts,
+ test m "new client email" $ testNewClientEmail b loc ts,
+ test m "account deletion email" $ testAccountDeletionEmail b ts,
+ test m "activation email" $ testActivationEmail b ts,
+ test m "activation email update" $ testActivationEmailUpdate b ts,
+ test m "team activation email" $ testTeamActivationEmail b ts
+ ]
+ )
+ userTemplates
+ ]
+
+testTeamInvitationEmailExistingUser :: (HasCallStack) => Map Text Text -> TeamTemplates -> Http ()
+testTeamInvitationEmailExistingUser branding templates = do
+ let tpl = templates.existingUserInvitationEmail
+ (errs, (mail, url)) = run $ runOutputList @Text $ renderInvitationEmail input tpl branding
+ input =
+ InvitationEmail
+ { invTo = fromJust $ emailAddressText "test@example.com",
+ invTeamId = Id (fromJust $ UUID.fromString "123e4567-e89b-12d3-a456-426614174000"),
+ invInvCode = InvitationCode {fromInvitationCode = fromRight undefined (validate "ZoMX0xs=")},
+ invInviter = fromJust $ emailAddressText "inviter@example.com"
+ }
+ liftIO $ mail.mailFrom.addressEmail @?= (fromEmail tpl.invitationEmailSender)
+ liftIO $ url @?= "https://example.com/accept-invitation/?team-code=ZoMX0xs="
+ assertNoErrors errs
+
+testTeamInvitationEmail :: (HasCallStack) => Map Text Text -> TeamTemplates -> Http ()
+testTeamInvitationEmail branding templates = do
+ let tpl = templates.invitationEmail
+ (errs, (mail, url)) = run $ runOutputList @Text $ renderInvitationEmail input tpl branding
+ input =
+ InvitationEmail
+ { invTo = fromJust $ emailAddressText "test@example.com",
+ invTeamId = Id (fromJust $ UUID.fromString "123e4567-e89b-12d3-a456-426614174000"),
+ invInvCode = InvitationCode {fromInvitationCode = fromRight undefined (validate "ZoMX0xs=")},
+ invInviter = fromJust $ emailAddressText "inviter@example.com"
+ }
+ liftIO $ mail.mailFrom.addressEmail @?= (fromEmail tpl.invitationEmailSender)
+ liftIO $ url @?= "https://example.com/join/?team-code=ZoMX0xs="
+ assertNoErrors errs
+
+testMemberWelcomeEmail :: (HasCallStack) => Map Text Text -> TeamTemplates -> Http ()
+testMemberWelcomeEmail branding templates = do
+ let tpl = templates.memberWelcomeEmail
+ to = fromJust $ emailAddressText "test@example.com"
+ tid = Id (fromJust $ UUID.fromString "123e4567-e89b-12d3-a456-426614174000")
+ tname = "funky team"
+ (errs, _) = run $ runOutputList @Text $ renderMemberWelcomeMail to tid tname tpl branding
+ assertNoErrors errs
+
+testNewTeamOwnerWelcomeEmail :: (HasCallStack) => Map Text Text -> TeamTemplates -> Http ()
+testNewTeamOwnerWelcomeEmail branding templates = do
+ let tpl = templates.newTeamOwnerWelcomeEmail
+ to = fromJust $ emailAddressText "test@example.com"
+ tid = Id (fromJust $ UUID.fromString "123e4567-e89b-12d3-a456-426614174000")
+ tname = "funky team"
+ name = Name "name"
+ (errs, _) = run $ runOutputList @Text $ renderNewTeamOwnerWelcomeEmail to tid tname name tpl branding
+ assertNoErrors errs
+
+testPasswordResetEmail :: (HasCallStack) => Map Text Text -> UserTemplates -> Http ()
+testPasswordResetEmail branding templates = do
+ let tpl = templates.passwordResetEmail
+ to = fromJust $ emailAddressText "test@example.com"
+ key = mkPasswordResetKey (Id UUID.nil)
+ code = PasswordResetCode . encodeBase64Url $ "bar"
+ (errs, _) = run $ runOutputList @Text $ renderPwResetMail to key code tpl branding
+ assertNoErrors errs
+
+testVerificationEmail :: (HasCallStack) => Map Text Text -> UserTemplates -> Http ()
+testVerificationEmail branding templates = do
+ let tpl = templates.verificationEmail
+ to = fromJust $ emailAddressText "test@example.com"
+ key = ActivationKey . Ascii.unsafeFromText $ "key"
+ code = ActivationCode . Ascii.unsafeFromText $ "code"
+ (errs, _) = run $ runOutputList @Text $ renderVerificationMail to key code tpl branding
+ assertNoErrors errs
+
+testTeamDeletionVerificationEmail :: (HasCallStack) => Map Text Text -> UserTemplates -> Http ()
+testTeamDeletionVerificationEmail branding templates = do
+ let tpl = templates.verificationTeamDeletionEmail
+ to = fromJust $ emailAddressText "test@example.com"
+ code = Value . unsafeRange . Ascii.unsafeFromText $ "code"
+ (errs, _) = run $ runOutputList @Text $ renderSecondFactorVerificationEmail to code tpl branding
+ assertNoErrors errs
+
+testScimTokenVerificationEmail :: (HasCallStack) => Map Text Text -> UserTemplates -> Http ()
+testScimTokenVerificationEmail branding templates = do
+ let tpl = templates.verificationScimTokenEmail
+ to = fromJust $ emailAddressText "test@example.com"
+ code = Value . unsafeRange . Ascii.unsafeFromText $ "code"
+ (errs, _) = run $ runOutputList @Text $ renderSecondFactorVerificationEmail to code tpl branding
+ assertNoErrors errs
+
+testLoginVerificationEmail :: (HasCallStack) => Map Text Text -> UserTemplates -> Http ()
+testLoginVerificationEmail branding templates = do
+ let tpl = templates.verificationLoginEmail
+ to = fromJust $ emailAddressText "test@example.com"
+ code = Value . unsafeRange . Ascii.unsafeFromText $ "code"
+ (errs, _) = run $ runOutputList @Text $ renderSecondFactorVerificationEmail to code tpl branding
+ assertNoErrors errs
+
+testActivationEmail :: (HasCallStack) => Map Text Text -> UserTemplates -> Http ()
+testActivationEmail branding templates = do
+ let tpl = templates.activationEmail
+ to = fromJust $ emailAddressText "test@example.com"
+ name = Name "name"
+ key = ActivationKey . Ascii.unsafeFromText $ "key"
+ code = ActivationCode . Ascii.unsafeFromText $ "code"
+ (errs, _) = run $ runOutputList @Text $ renderActivationMail to name key code tpl branding
+ assertNoErrors errs
+
+testActivationEmailUpdate :: (HasCallStack) => Map Text Text -> UserTemplates -> Http ()
+testActivationEmailUpdate branding templates = do
+ let tpl = templates.activationEmailUpdate
+ to = fromJust $ emailAddressText "test@example.com"
+ name = Name "name"
+ key = ActivationKey . Ascii.unsafeFromText $ "key"
+ code = ActivationCode . Ascii.unsafeFromText $ "code"
+ (errs, _) = run $ runOutputList @Text $ renderActivationMail to name key code tpl branding
+ assertNoErrors errs
+
+testTeamActivationEmail :: (HasCallStack) => Map Text Text -> UserTemplates -> Http ()
+testTeamActivationEmail branding templates = do
+ let tpl = templates.teamActivationEmail
+ to = fromJust $ emailAddressText "test@example.com"
+ name = Name "name"
+ teamName = "team-name"
+ key = ActivationKey . Ascii.unsafeFromText $ "key"
+ code = ActivationCode . Ascii.unsafeFromText $ "code"
+ (errs, _) = run $ runOutputList @Text $ renderTeamActivationMail to name teamName key code tpl branding
+ assertNoErrors errs
+
+testNewClientEmail :: (HasCallStack) => Map Text Text -> Locale -> UserTemplates -> Http ()
+testNewClientEmail branding loc templates = do
+ let tpl = templates.newClientEmail
+ to = fromJust $ emailAddressText "test@example.com"
+ name = Name "name"
+ client =
+ Client
+ { clientId = ClientId 1,
+ clientType = PermanentClientType,
+ clientTime = toUTCTimeMillis (UTCTime (fromGregorian 2020 1 1) (secondsToDiffTime 0)),
+ clientClass = Nothing,
+ clientLabel = Just "label",
+ clientCookie = Nothing,
+ clientModel = Just "model",
+ clientCapabilities = ClientCapabilityList mempty,
+ clientMLSPublicKeys = Map.empty,
+ clientLastActive = Nothing
+ }
+ (errs, _) = run $ runOutputList @Text $ renderNewClientEmail to name loc client tpl branding
+ assertNoErrors errs
+
+testAccountDeletionEmail :: (HasCallStack) => Map Text Text -> UserTemplates -> Http ()
+testAccountDeletionEmail branding templates = do
+ let tpl = templates.deletionEmail
+ to = fromJust $ emailAddressText "test@example.com"
+ name = Name "name"
+ key = Key . unsafeRange . Ascii.unsafeFromText $ "ABCDEFGHIJKLMNOPQRST"
+ code = Value . unsafeRange . Ascii.unsafeFromText $ "code123"
+ (errs, _) = run $ runOutputList @Text $ renderDeletionEmail to name key code tpl branding
+ assertNoErrors errs
+
+assertNoErrors :: [Text] -> Http ()
+assertNoErrors errs =
+ liftIO $
+ assertBool ("The following variables were not replaced: " <> show (nub errs)) (null errs)
diff --git a/services/brig/test/integration/Run.hs b/services/brig/test/integration/Run.hs
index dbbbafff99..660ed693b6 100644
--- a/services/brig/test/integration/Run.hs
+++ b/services/brig/test/integration/Run.hs
@@ -31,6 +31,7 @@ import API.Settings qualified as Settings
import API.SystemSettings qualified as SystemSettings
import API.Team qualified as Team
import API.TeamUserSearch qualified as TeamUserSearch
+import API.Template qualified
import API.User qualified as User
import API.UserPendingActivation qualified as UserPendingActivation
import Bilge hiding (header, host, port)
@@ -152,6 +153,7 @@ runTests iConf brigOpts otherArgs = do
federationEnd2End <- Federation.End2end.spec brigOpts mg b g ch c f brigTwo galleyTwo ch2 cannonTwo
federationEndpoints <- API.Federation.tests mg brigOpts b fedBrigClient
internalApi <- API.Internal.tests brigOpts mg db b (brig iConf) gd g
+ emailTemplates <- API.Template.tests brigOpts mg
let smtp = SMTP.tests mg lg
oauthAPI = API.OAuth.tests mg db b n brigOpts
@@ -174,7 +176,8 @@ runTests iConf brigOpts otherArgs = do
internalApi,
smtp,
oauthAPI,
- federationEnd2End
+ federationEnd2End,
+ emailTemplates
]
where
mkRequest (Endpoint h p) = Bilge.host (encodeUtf8 h) . Bilge.port p
From 732cefc80c2e93add018821e5be8f608c519576a Mon Sep 17 00:00:00 2001
From: Matthias Fischmann
Date: Wed, 14 Jan 2026 11:55:12 +0100
Subject: [PATCH 04/26] Fix `make psql` target. (#4956)
---
Makefile | 5 ++---
nix/default.nix | 1 +
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Makefile b/Makefile
index af09b6ce00..eda5ea8ba1 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,6 @@ ingress-nginx-controller nginx-ingress-services reaper restund \
k8ssandra-test-cluster ldap-scim-bridge wire-server-enterprise
KIND_CLUSTER_NAME := wire-server
HELM_PARALLELISM ?= 1 # 1 for sequential tests; 6 for all-parallel tests
-# (run `psql -h localhost -p 5432 -d backendA -U wire-server -w` for the list of options for PSQL_DB)
PSQL_DB ?= backendA
export PSQL_DB
@@ -355,8 +354,8 @@ cqlsh:
.PHONY: psql
psql:
@grep -q wire-server:wire-server ~/.pgpass || \
- echo "consider running 'echo localhost:5432:wire-server:wire-server:posty-the-gres > ~/.pgpass ; chmod 600 ~/.pgpass '"
- pg_dump -h localhost -p 5432 $(PSQL_DB) -U wire-server -w --schema-only || \
+ echo "consider running 'echo localhost:5432:$(PSQL_DB):wire-server:posty-the-gres > ~/.pgpass ; chmod 600 ~/.pgpass '"
+ psql -h localhost -p 5432 $(PSQL_DB) -U wire-server -w || \
echo 'if the database is missing, consider running "make postgres-reset", or setting $$PSQL_DB to the correct table space.'
.PHONY: db-reset-package
diff --git a/nix/default.nix b/nix/default.nix
index 71f9845b8d..a1cc06954f 100644
--- a/nix/default.nix
+++ b/nix/default.nix
@@ -65,6 +65,7 @@ let
pkgs.niv
pkgs.zip
pkgs.entr
+ pkgs.postgresql
] ++ docsPkgs;
};
inherit (pkgs) mls-test-cli;
From 51dca25f1ada1d8115be521a0cf2fd9c7aba0df9 Mon Sep 17 00:00:00 2001
From: Leif Battermann
Date: Wed, 14 Jan 2026 11:59:21 +0100
Subject: [PATCH 05/26] WPB-22541 prevent performance issues with TVar Map
(#4948)
---
changelog.d/5-internal/WPB-22541 | 1 +
libs/http2-manager/default.nix | 3 +++
libs/http2-manager/http2-manager.cabal | 2 ++
.../src/HTTP2/Client/Manager/Internal.hs | 23 ++++++++++---------
.../test/Test/HTTP2/Client/ManagerSpec.hs | 6 +++--
5 files changed, 22 insertions(+), 13 deletions(-)
create mode 100644 changelog.d/5-internal/WPB-22541
diff --git a/changelog.d/5-internal/WPB-22541 b/changelog.d/5-internal/WPB-22541
new file mode 100644
index 0000000000..4638c9d945
--- /dev/null
+++ b/changelog.d/5-internal/WPB-22541
@@ -0,0 +1 @@
+Circumvent potential performance issue with `TVar (Map ...)`
diff --git a/libs/http2-manager/default.nix b/libs/http2-manager/default.nix
index 782b3605f1..1a41e8ebac 100644
--- a/libs/http2-manager/default.nix
+++ b/libs/http2-manager/default.nix
@@ -17,6 +17,7 @@
, network
, random
, stm
+, stm-containers
, streaming-commons
, text
, time-manager
@@ -34,6 +35,7 @@ mkDerivation {
http2
network
stm
+ stm-containers
streaming-commons
text
time-manager
@@ -50,6 +52,7 @@ mkDerivation {
network
random
stm
+ stm-containers
streaming-commons
time-manager
];
diff --git a/libs/http2-manager/http2-manager.cabal b/libs/http2-manager/http2-manager.cabal
index 87d6d58241..28b03b783b 100644
--- a/libs/http2-manager/http2-manager.cabal
+++ b/libs/http2-manager/http2-manager.cabal
@@ -50,6 +50,7 @@ library
, http2
, network
, stm
+ , stm-containers
, streaming-commons
, text
, time-manager
@@ -95,5 +96,6 @@ test-suite http2-manager-tests
, network
, random
, stm
+ , stm-containers
, streaming-commons
, time-manager
diff --git a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs
index 5de12d91da..a892452026 100644
--- a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs
+++ b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs
@@ -48,6 +48,7 @@ import qualified Network.HTTP2.Client as HTTP2
import qualified Network.HTTP2.Client.Internal as HTTP2
import qualified Network.Socket as NS
import qualified OpenSSL.Session as SSL
+import qualified StmContainers.Map as StmMap
import System.IO.Error
import qualified System.TimeManager
import System.Timeout
@@ -95,7 +96,7 @@ data Request = Request
-- HTTP1. I think HTTP1 vs HTTP2 can not be negotated without TLS, so perhaps
-- this manager will default to HTTP2.
data Http2Manager = Http2Manager
- { connections :: TVar (Map Target HTTP2Conn),
+ { connections :: StmMap.Map Target HTTP2Conn,
cacheLimit :: Int,
-- | In microseconds, defaults to 30s
tcpConnectionTimeout :: Int,
@@ -118,7 +119,7 @@ defaultHttp2Manager = do
http2ManagerWithSSLCtx :: SSL.SSLContext -> IO Http2Manager
http2ManagerWithSSLCtx sslContext = do
- connections <- newTVarIO mempty
+ connections <- StmMap.newIO
let cacheLimit = 20
tcpConnectionTimeout = 30_000_000
sslRemoveTrailingDot = False
@@ -234,10 +235,11 @@ getOrMakeConnection mgr@Http2Manager {..} target = do
-- leak.
insertNewConn :: HTTP2Conn -> STM (Bool, HTTP2Conn)
insertNewConn newConn = do
- stateTVar connections $ \conns ->
- case Map.lookup target conns of
- Nothing -> ((True, newConn), Map.insert target newConn conns)
- Just alreadyEstablishedConn -> ((False, alreadyEstablishedConn), conns)
+ StmMap.lookup target connections >>= \case
+ Just existing -> pure (False, existing)
+ Nothing -> do
+ StmMap.insert newConn target connections
+ pure (True, newConn)
connect :: IO HTTP2Conn
connect = do
@@ -256,8 +258,7 @@ getOrMakeConnection mgr@Http2Manager {..} target = do
-- | Removes connection from map if it is not alive anymore
getConnection :: Http2Manager -> Target -> STM (Maybe HTTP2Conn)
getConnection mgr target = do
- conns <- readTVar (connections mgr)
- case Map.lookup target conns of
+ StmMap.lookup target (connections mgr) >>= \case
Nothing -> pure Nothing
Just conn ->
-- If there is a connection for the target, ensure that it is alive
@@ -269,7 +270,7 @@ getConnection mgr target = do
-- recieve here. But logging in STM will be tricky, and the threads
-- running requests on the connection which got an exception would've
-- anyway recieved the exception, so maybe it is not as valueable.
- writeTVar (connections mgr) $ Map.delete target conns
+ StmMap.delete target (connections mgr)
pure Nothing
-- | Disconnects HTTP2 connection if there exists one. Will hang around until
@@ -284,7 +285,7 @@ disconnectTarget mgr target = do
Just conn -> do
disconnect conn
wait (backgroundThread conn)
- `finally` (atomically . modifyTVar' (connections mgr) $ Map.delete target)
+ `finally` atomically (StmMap.delete target (connections mgr))
-- | Disconnects HTTP2 connection if there exists one. If the background thread
-- running the connection does not finish within 1 second, it is canceled.
@@ -313,7 +314,7 @@ disconnectTargetWithTimeout mgr target microSeconds = do
void $ waitAnyCatchCancel [waitOneSec, backgroundThread conn]
waitWithTimeout
- `finally` (atomically . modifyTVar' (connections mgr) $ Map.delete target)
+ `finally` atomically (StmMap.delete target (connections mgr))
startPersistentHTTP2Connection ::
SSL.SSLContext ->
diff --git a/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs b/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs
index 15b99661f2..ceb1781012 100644
--- a/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs
+++ b/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs
@@ -56,6 +56,7 @@ import qualified Network.HTTP2.Server.Internal as Server
import Network.Socket
import qualified Network.Socket as NS
import qualified OpenSSL.Session as SSL
+import qualified StmContainers.Map as StmMap
import System.Random (randomRIO)
import qualified System.TimeManager
import Test.Hspec
@@ -201,7 +202,7 @@ specTemplate mCtx = do
Just deadConn <- withTestServer mCtx $ \TestServer {..} -> do
echoTest mgr (isJust mCtx) serverPort
readIORef acceptedConns `shouldReturn` 1
- Map.lookup (isJust mCtx, "localhost", serverPort) <$> readTVarIO (connections mgr)
+ atomically $ StmMap.lookup (isJust mCtx, "localhost", serverPort) (connections mgr)
let brokenRequest = sendRequestWithConnection deadConn (Client.requestBuilder "GET" "/echo" [] "some body") $ \_ -> do
expectationFailure "Expected no response when request is made to a dead server"
@@ -222,7 +223,8 @@ specTemplate mCtx = do
-- See "should fail with appropriate error when a dead connection is used"
-- to know what happens when we don't wait for the background thread to go
-- away.
- Just deadConn <- Map.lookup (isJust mCtx, "localhost", port) <$> readTVarIO (connections mgr)
+ Just deadConn <-
+ atomically $ StmMap.lookup (isJust mCtx, "localhost", port) (connections mgr)
void $ waitCatch $ backgroundThread deadConn
withTestServerOnPort mCtx port $ \TestServer {..} -> do
From 1d1d83b90d148cf444e93eb5e5f788a00c222bc2 Mon Sep 17 00:00:00 2001
From: Akshay Mankar
Date: Wed, 14 Jan 2026 15:55:24 +0100
Subject: [PATCH 06/26] PostgresMarshall: Flip parameters so the insatnces can
be easily derived (#4958)
This also makes it more consistent with PostgresUnmarshall
---
libs/wire-api/src/Wire/API/Conversation.hs | 12 +++----
.../src/Wire/API/Conversation/CellsState.hs | 2 +-
.../src/Wire/API/Conversation/Member.hs | 5 +--
.../src/Wire/API/Conversation/Protocol.hs | 2 +-
.../src/Wire/API/Conversation/Role.hs | 5 +--
libs/wire-api/src/Wire/API/MLS/CipherSuite.hs | 2 +-
libs/wire-api/src/Wire/API/MLS/Epoch.hs | 2 +-
libs/wire-api/src/Wire/API/MLS/Group.hs | 5 +--
libs/wire-api/src/Wire/API/MLS/GroupInfo.hs | 8 +----
.../src/Wire/API/MLS/SubConversation.hs | 6 ++--
.../wire-api/src/Wire/API/PostgresMarshall.hs | 34 +++++++++----------
libs/wire-subsystems/src/Wire/AppStore.hs | 2 +-
.../src/Wire/ConversationStore/Migration.hs | 12 +++----
.../ConversationStore/Migration/Cleanup.hs | 2 +-
.../Wire/ConversationStore/MigrationLock.hs | 4 +--
.../src/Wire/ConversationStore/Postgres.hs | 28 +++++++--------
16 files changed, 57 insertions(+), 74 deletions(-)
diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs
index d10ad9c6f0..ff83fd2602 100644
--- a/libs/wire-api/src/Wire/API/Conversation.hs
+++ b/libs/wire-api/src/Wire/API/Conversation.hs
@@ -592,7 +592,7 @@ instance C.Cql Access where
fromCql (C.CqlInt i) = mapLeft Text.unpack $ accessFromInt32 i
fromCql _ = Left "Access value: int expected"
-instance PostgresMarshall Access Int32 where
+instance PostgresMarshall Int32 Access where
postgresMarshall = accessToInt32
instance PostgresUnmarshall Int32 Access where
@@ -677,7 +677,7 @@ instance C.Cql AccessRole where
fromCql (C.CqlInt i) = mapLeft Text.unpack $ accessRoleFromInt32 i
fromCql _ = Left "AccessRoleV2 value: int expected"
-instance PostgresMarshall AccessRole Int32 where
+instance PostgresMarshall Int32 AccessRole where
postgresMarshall = accessRoleToInt32
instance PostgresUnmarshall Int32 AccessRole where
@@ -792,7 +792,7 @@ instance C.Cql ConvType where
fromCql (C.CqlInt i) = mapLeft Text.unpack $ convTypeFromInt32 i
fromCql _ = Left "conv-type: int expected"
-instance PostgresMarshall ConvType Int32 where
+instance PostgresMarshall Int32 ConvType where
postgresMarshall = convTypeToInt32
instance PostgresUnmarshall Int32 ConvType where
@@ -834,7 +834,7 @@ instance ToSchema ReceiptMode where
(S.schema . description ?~ "Conversation receipt mode") $
ReceiptMode <$> unReceiptMode .= schema
-instance PostgresMarshall ReceiptMode Int32 where
+instance PostgresMarshall Int32 ReceiptMode where
postgresMarshall = unReceiptMode
--------------------------------------------------------------------------------
@@ -859,7 +859,7 @@ instance C.Cql GroupConvType where
fromCql (C.CqlInt i) = Right . toEnum . fromIntegral $ i
fromCql _ = Left "GroupConvType: int expected"
-instance PostgresMarshall GroupConvType Int32 where
+instance PostgresMarshall Int32 GroupConvType where
postgresMarshall = fromIntegral . fromEnum
instance PostgresUnmarshall Int32 GroupConvType where
@@ -1294,7 +1294,7 @@ instance C.Cql AddPermission where
fromCql (C.CqlInt i) = Right . toEnum . fromIntegral $ i
fromCql _ = Left "AddPermission: int expected"
-instance PostgresMarshall AddPermission Int32 where
+instance PostgresMarshall Int32 AddPermission where
postgresMarshall = fromIntegral . fromEnum
instance PostgresUnmarshall Int32 AddPermission where
diff --git a/libs/wire-api/src/Wire/API/Conversation/CellsState.hs b/libs/wire-api/src/Wire/API/Conversation/CellsState.hs
index 63b73576ab..b6084cb440 100644
--- a/libs/wire-api/src/Wire/API/Conversation/CellsState.hs
+++ b/libs/wire-api/src/Wire/API/Conversation/CellsState.hs
@@ -58,7 +58,7 @@ instance Cql CellsState where
fromCql (CqlInt i) = mapLeft Text.unpack $ cellsStateFromInt32 i
fromCql _ = Left "cells_state: int expected"
-instance PostgresMarshall CellsState Int32 where
+instance PostgresMarshall Int32 CellsState where
postgresMarshall = cellsStateToInt32
instance PostgresUnmarshall Int32 CellsState where
diff --git a/libs/wire-api/src/Wire/API/Conversation/Member.hs b/libs/wire-api/src/Wire/API/Conversation/Member.hs
index 22a01c38b2..e496b1708e 100644
--- a/libs/wire-api/src/Wire/API/Conversation/Member.hs
+++ b/libs/wire-api/src/Wire/API/Conversation/Member.hs
@@ -163,12 +163,9 @@ instance ToSchema Member where
-- the server will not interpret this value in any way.
newtype MutedStatus = MutedStatus {fromMutedStatus :: Int32}
deriving stock (Eq, Ord, Show, Generic)
- deriving newtype (Num, ToSchema, Arbitrary, C.Cql, PostgresUnmarshall Int32)
+ deriving newtype (Num, ToSchema, Arbitrary, C.Cql, PostgresUnmarshall Int32, PostgresMarshall Int32)
deriving (FromJSON, ToJSON, S.ToSchema) via Schema MutedStatus
-instance PostgresMarshall MutedStatus Int32 where
- postgresMarshall = fromMutedStatus
-
data OtherMember = OtherMember
{ omQualifiedId :: Qualified UserId,
omService :: Maybe ServiceRef,
diff --git a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs
index aa32728e92..e41ad2f720 100644
--- a/libs/wire-api/src/Wire/API/Conversation/Protocol.hs
+++ b/libs/wire-api/src/Wire/API/Conversation/Protocol.hs
@@ -73,7 +73,7 @@ instance C.Cql ProtocolTag where
fromCql (C.CqlInt i) = mapLeft Text.unpack $ protocolTagFromInt32 i
fromCql _ = Left "protocol: int expected"
-instance PostgresMarshall ProtocolTag Int32 where
+instance PostgresMarshall Int32 ProtocolTag where
postgresMarshall = fromIntegral . fromEnum
instance PostgresUnmarshall Int32 ProtocolTag where
diff --git a/libs/wire-api/src/Wire/API/Conversation/Role.hs b/libs/wire-api/src/Wire/API/Conversation/Role.hs
index 074dd2e742..e6b54d5b79 100644
--- a/libs/wire-api/src/Wire/API/Conversation/Role.hs
+++ b/libs/wire-api/src/Wire/API/Conversation/Role.hs
@@ -226,7 +226,7 @@ instance FromJSON ConversationRolesList where
-- expose this constructor outside of this module.
newtype RoleName = RoleName {fromRoleName :: Text}
deriving stock (Eq, Ord, Show, Generic)
- deriving newtype (ToByteString, Hashable, Cql, PostgresUnmarshall Text)
+ deriving newtype (ToByteString, Hashable, Cql, PostgresUnmarshall Text, PostgresMarshall Text)
deriving (FromJSON, ToJSON, S.ToSchema) via Schema RoleName
instance ToSchema RoleName where
@@ -242,9 +242,6 @@ instance ToSchema RoleName where
instance FromByteString RoleName where
parser = parser >>= maybe (fail "Invalid RoleName") pure . parseRoleName
-instance PostgresMarshall RoleName Text where
- postgresMarshall = fromRoleName
-
instance Arbitrary RoleName where
arbitrary =
RoleName . fromRange
diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs
index 71574971db..f4914b0065 100644
--- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs
+++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs
@@ -157,7 +157,7 @@ instance C.Cql CipherSuiteTag where
fromCql (CqlInt index) = first Text.unpack $ cipherSuiteTagFromInt32 index
fromCql _ = Left "CipherSuiteTag: int expected"
-instance PostgresMarshall CipherSuiteTag Int32 where
+instance PostgresMarshall Int32 CipherSuiteTag where
postgresMarshall = cipherSuitTagToInt32
instance PostgresUnmarshall Int32 CipherSuiteTag where
diff --git a/libs/wire-api/src/Wire/API/MLS/Epoch.hs b/libs/wire-api/src/Wire/API/MLS/Epoch.hs
index 68430c6e4e..364fd2337c 100644
--- a/libs/wire-api/src/Wire/API/MLS/Epoch.hs
+++ b/libs/wire-api/src/Wire/API/MLS/Epoch.hs
@@ -51,7 +51,7 @@ instance C.Cql Epoch where
fromCql (C.CqlBigInt n) = pure (Epoch (fromIntegral n))
fromCql _ = Left "epoch: bigint expected"
-instance PostgresMarshall Epoch Int64 where
+instance PostgresMarshall Int64 Epoch where
postgresMarshall = fromIntegral . epochNumber
instance PostgresUnmarshall Int64 Epoch where
diff --git a/libs/wire-api/src/Wire/API/MLS/Group.hs b/libs/wire-api/src/Wire/API/MLS/Group.hs
index 26e01397fa..050b203630 100644
--- a/libs/wire-api/src/Wire/API/MLS/Group.hs
+++ b/libs/wire-api/src/Wire/API/MLS/Group.hs
@@ -32,7 +32,7 @@ import Wire.Arbitrary
newtype GroupId = GroupId {unGroupId :: ByteString}
deriving (Eq, Show, Generic, Ord)
- deriving newtype (PostgresUnmarshall ByteString)
+ deriving newtype (PostgresUnmarshall ByteString, PostgresMarshall ByteString)
deriving (Arbitrary) via (GenericUniform GroupId)
deriving (FromHttpApiData, ToHttpApiData, S.ToParamSchema) via Base64ByteString
deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema GroupId)
@@ -60,9 +60,6 @@ instance C.Cql GroupId where
fromCql (C.CqlBlob b) = Right . GroupId . LBS.toStrict $ b
fromCql _ = Left "group_id: blob expected"
-instance PostgresMarshall GroupId ByteString where
- postgresMarshall = unGroupId
-
newtype GroupIdGen = GroupIdGen {unGroupIdGen :: Word32}
deriving (Eq, Show, Generic, Ord)
deriving (Arbitrary) via (GenericUniform GroupIdGen)
diff --git a/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs b/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs
index edba7c2650..57b73842f3 100644
--- a/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs
+++ b/libs/wire-api/src/Wire/API/MLS/GroupInfo.hs
@@ -131,7 +131,7 @@ instance HasField "signer" GroupInfo Word32 where
newtype GroupInfoData = GroupInfoData {unGroupInfoData :: ByteString}
deriving stock (Eq, Ord, Show)
- deriving newtype (Arbitrary)
+ deriving newtype (Arbitrary, PostgresMarshall ByteString, PostgresUnmarshall ByteString)
instance ParseMLS GroupInfoData where
parseMLS = GroupInfoData . LBS.toStrict <$> getRemainingLazyByteString
@@ -148,9 +148,3 @@ instance C.Cql GroupInfoData where
toCql = C.CqlBlob . LBS.fromStrict . unGroupInfoData
fromCql (C.CqlBlob b) = Right $ GroupInfoData (LBS.toStrict b)
fromCql _ = Left "GroupInfoData: blob expected"
-
-instance PostgresMarshall GroupInfoData ByteString where
- postgresMarshall = unGroupInfoData
-
-instance PostgresUnmarshall ByteString GroupInfoData where
- postgresUnmarshall = Right . GroupInfoData
diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs
index d0d8f8833b..be1199c819 100644
--- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs
+++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs
@@ -49,7 +49,8 @@ import Wire.Arbitrary
-- conversation. The pair of a qualified conversation ID and a subconversation
-- ID identifies globally.
newtype SubConvId = SubConvId {unSubConvId :: Text}
- deriving newtype (Eq, ToSchema, Ord, S.ToParamSchema, ToByteString, ToJSON, FromJSON, S.ToSchema, PostgresUnmarshall Text)
+ deriving newtype (Eq, ToSchema, Ord, S.ToParamSchema, ToByteString)
+ deriving newtype (ToJSON, FromJSON, S.ToSchema, PostgresUnmarshall Text, PostgresMarshall Text)
deriving stock (Generic)
deriving stock (Show)
@@ -69,9 +70,6 @@ instance Arbitrary SubConvId where
cs <- replicateM n (arbitrary `suchThat` isValidSubConvChar)
pure $ SubConvId (T.pack cs)
-instance PostgresMarshall SubConvId Text where
- postgresMarshall = unSubConvId
-
isValidSubConvChar :: Char -> Bool
isValidSubConvChar c = isPrint c && isAscii c && not (isSpace c)
diff --git a/libs/wire-api/src/Wire/API/PostgresMarshall.hs b/libs/wire-api/src/Wire/API/PostgresMarshall.hs
index 7b6a059b01..ee78384312 100644
--- a/libs/wire-api/src/Wire/API/PostgresMarshall.hs
+++ b/libs/wire-api/src/Wire/API/PostgresMarshall.hs
@@ -41,8 +41,8 @@ import Data.Vector qualified as V
import Hasql.Statement
import Imports
-class PostgresMarshall a b where
- postgresMarshall :: a -> b
+class PostgresMarshall db domain where
+ postgresMarshall :: domain -> db
instance {-# OVERLAPPABLE #-} (a ~ b) => PostgresMarshall a b where
postgresMarshall = id
@@ -500,31 +500,31 @@ instance (PostgresMarshall a1 b1, PostgresMarshall a2 b2, PostgresMarshall a3 b3
postgresMarshall a20
)
-instance PostgresMarshall (Id a) UUID where
+instance PostgresMarshall UUID (Id a) where
postgresMarshall = toUUID
-instance PostgresMarshall BotId UUID where
+instance PostgresMarshall UUID BotId where
postgresMarshall = toUUID . botUserId
-instance PostgresMarshall ClientId Text where
+instance PostgresMarshall Text ClientId where
postgresMarshall = clientToText
-instance PostgresMarshall Object Value where
+instance PostgresMarshall Value Object where
postgresMarshall = Object
-instance PostgresMarshall Milliseconds Int64 where
+instance PostgresMarshall Int64 Milliseconds where
postgresMarshall = msToInt64
-instance PostgresMarshall Domain Text where
+instance PostgresMarshall Text Domain where
postgresMarshall = domainText
instance (PostgresMarshall a b) => PostgresMarshall (Maybe a) (Maybe b) where
postgresMarshall = fmap postgresMarshall
-instance (PostgresMarshall a b) => PostgresMarshall [a] (Vector b) where
+instance (PostgresMarshall b a) => PostgresMarshall (Vector b) [a] where
postgresMarshall = V.fromList . map postgresMarshall
-instance (PostgresMarshall a b) => PostgresMarshall (Set a) (Vector b) where
+instance (PostgresMarshall b a) => PostgresMarshall (Vector b) (Set a) where
postgresMarshall = V.fromList . map postgresMarshall . Set.toList
instance (PostgresMarshall a b) => PostgresMarshall (Vector a) (Vector b) where
@@ -532,8 +532,8 @@ instance (PostgresMarshall a b) => PostgresMarshall (Vector a) (Vector b) where
---
-class PostgresUnmarshall a b where
- postgresUnmarshall :: a -> Either Text b
+class PostgresUnmarshall db domain where
+ postgresUnmarshall :: db -> Either Text domain
instance {-# OVERLAPPABLE #-} (a ~ b) => PostgresUnmarshall a b where
postgresUnmarshall = Right
@@ -857,14 +857,14 @@ instance PostgresUnmarshall Int64 Milliseconds where
---
-lmapPG :: (PostgresMarshall a b, Profunctor p) => p b x -> p a x
+lmapPG :: (PostgresMarshall db domain, Profunctor p) => p db x -> p domain x
lmapPG = lmap postgresMarshall
-rmapPG :: (PostgresUnmarshall x y) => Statement a x -> Statement a y
+rmapPG :: (PostgresUnmarshall db domain) => Statement x db -> Statement x domain
rmapPG = refineResult postgresUnmarshall
dimapPG ::
- (PostgresMarshall a b, PostgresUnmarshall x y) =>
- Statement b x ->
- Statement a y
+ (PostgresMarshall dbIn domainIn, PostgresUnmarshall dbOut domainOut) =>
+ Statement dbIn dbOut ->
+ Statement domainIn domainOut
dimapPG = refineResult postgresUnmarshall . lmapPG
diff --git a/libs/wire-subsystems/src/Wire/AppStore.hs b/libs/wire-subsystems/src/Wire/AppStore.hs
index bc347da166..99fc25f1da 100644
--- a/libs/wire-subsystems/src/Wire/AppStore.hs
+++ b/libs/wire-subsystems/src/Wire/AppStore.hs
@@ -41,7 +41,7 @@ data StoredApp = StoredApp
-- The `PostgresMarshall` instances are here in this module -- as
-- having them elsewhere would make them orphan instances of
-- `StoredApp`.
-instance PostgresMarshall StoredApp (UUID, UUID, Value, Text, Text, UUID) where
+instance PostgresMarshall (UUID, UUID, Value, Text, Text, UUID) StoredApp where
postgresMarshall app =
( postgresMarshall app.id,
postgresMarshall app.teamId,
diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs
index 6cae2fc9cf..cfc60b0331 100644
--- a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs
+++ b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs
@@ -319,7 +319,7 @@ saveConvToPostgres allConvData = do
)
()
insertConv =
- lmapPG @_ @(_, _, _, Vector Int32, Vector Int32, _, _, _, _, _, _, _, _, _, _, _, _, _, _)
+ lmapPG @(_, _, _, Vector Int32, Vector Int32, _, _, _, _, _, _, _, _, _, _, _, _, _, _) @_
[resultlessStatement|INSERT INTO conversation
(id, type, creator, access, access_roles_v2,
name, team, message_timer, receipt_mode, protocol,
@@ -385,7 +385,7 @@ saveConvToPostgres allConvData = do
)
()
insertLocalMembers =
- lmapPG @_ @(Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _)
+ lmapPG @(Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _) @_
[resultlessStatement|INSERT INTO conversation_member
(conv, "user", service, provider, otr_muted_status, otr_muted_ref,
otr_archived, otr_archived_ref, hidden, hidden_ref, conversation_role)
@@ -397,7 +397,7 @@ saveConvToPostgres allConvData = do
|]
insertRemoteMembers :: Hasql.Statement ([ConvId], [Domain], [UserId], [RoleName]) ()
insertRemoteMembers =
- lmapPG @_ @(Vector _, Vector _, Vector _, Vector _)
+ lmapPG @(Vector _, Vector _, Vector _, Vector _)
[resultlessStatement|INSERT INTO local_conversation_remote_member
(conv, user_remote_domain, user_remote_id, conversation_role)
SELECT * FROM UNNEST($1 :: uuid[], $2 :: text[], $3 :: uuid[], $4 :: text[])
@@ -424,7 +424,7 @@ saveConvToPostgres allConvData = do
insertMLSClients :: Hasql.Statement ([GroupId], [Domain], [UserId], [ClientId], [Int32], [Bool]) ()
insertMLSClients =
- lmapPG @_ @(Vector _, Vector _, Vector _, Vector _, Vector _, Vector _)
+ lmapPG @(Vector _, Vector _, Vector _, Vector _, Vector _, Vector _)
[resultlessStatement|INSERT INTO mls_group_member_client
(group_id, user_domain, "user", client, leaf_node_index, removal_pending)
SELECT *
@@ -454,7 +454,7 @@ saveConvToPostgres allConvData = do
insertSubConvs :: Hasql.Statement ([ConvId], [SubConvId], [Maybe CipherSuiteTag], [Epoch], [UTCTime], [GroupId], [Maybe GroupInfoData]) ()
insertSubConvs =
- lmapPG @_ @(Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _)
+ lmapPG @(Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _)
[resultlessStatement|INSERT INTO subconversation
(conv_id, subconv_id, cipher_suite, epoch, epoch_timestamp, group_id, public_group_state)
SELECT *
@@ -495,7 +495,7 @@ saveRemoteMemberStatusToPostgres uid statusses =
where
insertStatuses :: Hasql.Statement ([UserId], [Domain], [ConvId], [Maybe MutedStatus], [Maybe Text], [Bool], [Maybe Text], [Bool], [Maybe Text]) ()
insertStatuses =
- lmapPG @_ @(Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _)
+ lmapPG @(Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _, Vector _)
[resultlessStatement|INSERT INTO remote_conversation_local_member
("user", conv_remote_domain, conv_remote_id, otr_muted_status, otr_muted_ref, otr_archived, otr_archived_ref, hidden, hidden_ref)
SELECT *
diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Migration/Cleanup.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Migration/Cleanup.hs
index 68ea6ac3b4..e26533da7b 100644
--- a/libs/wire-subsystems/src/Wire/ConversationStore/Migration/Cleanup.hs
+++ b/libs/wire-subsystems/src/Wire/ConversationStore/Migration/Cleanup.hs
@@ -39,7 +39,7 @@ import Wire.Util
data DeletionType = DeleteConv | DeleteUser
-instance PostgresMarshall DeletionType Text where
+instance PostgresMarshall Text DeletionType where
postgresMarshall = \case
DeleteConv -> "conv"
DeleteUser -> "user"
diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/MigrationLock.hs b/libs/wire-subsystems/src/Wire/ConversationStore/MigrationLock.hs
index 4f74501741..e0830b5b17 100644
--- a/libs/wire-subsystems/src/Wire/ConversationStore/MigrationLock.hs
+++ b/libs/wire-subsystems/src/Wire/ConversationStore/MigrationLock.hs
@@ -110,7 +110,7 @@ withMigrationLocks lockType maxWait convOrUsers action = do
acquireLocks :: Hasql.Statement [Int64] ()
acquireLocks =
- lmapPG @[_] @(Vector _)
+ lmapPG @(Vector _)
case lockType of
LockExclusive ->
[resultlessStatement|SELECT (1 :: int)
@@ -123,7 +123,7 @@ withMigrationLocks lockType maxWait convOrUsers action = do
releaseLocks :: Hasql.Statement [Int64] ()
releaseLocks =
- lmapPG @[_] @(Vector _)
+ lmapPG @(Vector _)
case lockType of
LockExclusive ->
[resultlessStatement|SELECT (1 :: int)
diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs
index ff1c89490c..f1e6d17569 100644
--- a/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs
+++ b/libs/wire-subsystems/src/Wire/ConversationStore/Postgres.hs
@@ -158,7 +158,7 @@ upsertConversationImpl lcnv nc = do
pure storedConv
where
insertConvStatement =
- lmapPG @_ @(_, _, _, Vector Int32, Vector Int32, _, _, _, _, _, _, _, _, _, _)
+ lmapPG @(_, _, _, Vector Int32, Vector Int32, _, _, _, _, _, _, _, _, _, _) @_
[resultlessStatement|INSERT INTO conversation
(id, type, creator, access, access_roles_v2,
name, team, message_timer, receipt_mode, protocol,
@@ -255,7 +255,7 @@ getConversationsImpl cids = do
where
selectMetadata :: Hasql.Statement [ConvId] [ConvRowWithId]
selectMetadata =
- dimapPG @[_] @(Vector _)
+ dimapPG @(Vector _) @[_]
@(Vector (_, _, _, Maybe (Vector _), Maybe (Vector _), _, _, _, _, _, _, _, _, _, _, _, _, _))
@[ConvRowWithId]
[vectorStatement|SELECT (id :: uuid), (type :: integer), (creator :: uuid?), (access :: integer[]?), (access_roles_v2 :: integer[]?),
@@ -267,7 +267,7 @@ getConversationsImpl cids = do
|]
selectAllLocalMembers :: Hasql.Statement [ConvId] [LocalMemberRow]
selectAllLocalMembers =
- dimapPG @[_] @(Vector _)
+ dimapPG @(Vector _) @[_]
[vectorStatement|SELECT (conv :: uuid), ("user" :: uuid), (service :: uuid?), (provider :: uuid?), (otr_muted_status :: integer?), (otr_muted_ref :: text?),
(otr_archived :: boolean?), (otr_archived_ref :: text?), (hidden :: boolean?), (hidden_ref :: text?), (conversation_role :: text?)
FROM conversation_member
@@ -282,7 +282,7 @@ getConversationsImpl cids = do
|]
selectAllRemoteMembers :: Hasql.Statement [ConvId] [RemoteMemberRow]
selectAllRemoteMembers =
- dimapPG @[_] @(Vector _)
+ dimapPG @(Vector _) @[_]
[vectorStatement|SELECT (conv :: uuid), (user_remote_domain :: text), (user_remote_id :: uuid), (conversation_role :: text)
FROM local_conversation_remote_member
WHERE conv = ANY ($1 :: uuid[])
@@ -393,7 +393,7 @@ getRemoteConversationStatusImpl uid remoteConvs = do
where
select :: Hasql.Statement (UserId, Domain, [ConvId]) [(ConvId, Maybe MutedStatus, Maybe Text, Maybe Bool, Maybe Text, Maybe Bool, Maybe Text)]
select =
- dimapPG @_ @(_, _, Vector _)
+ dimapPG @(_, _, Vector _)
[vectorStatement|SELECT (conv_remote_id :: uuid),
(otr_muted_status :: integer?), (otr_muted_ref :: text?),
(otr_archived :: boolean?), (otr_archived_ref :: text?),
@@ -410,7 +410,7 @@ selectConversationsImpl uid cids =
where
select :: Hasql.Statement (UserId, [ConvId]) [ConvId]
select =
- dimapPG @_ @(_, Vector _)
+ dimapPG @(_, Vector _)
[vectorStatement|SELECT (conv :: uuid) from conversation_member
WHERE "user" = ($1 :: uuid)
AND conv = ANY ($2 :: uuid[])
@@ -445,7 +445,7 @@ setConversationAccessImpl convId accessData =
where
update :: Hasql.Statement (ConvId, Set Access, Set AccessRole) ()
update =
- lmapPG @_ @(_, Vector _, Vector _)
+ lmapPG @(_, Vector _, Vector _)
[resultlessStatement|UPDATE conversation
SET access = ($2 :: integer[]), access_roles_v2 = ($3 :: integer[])
WHERE id = ($1 :: uuid)|]
@@ -638,7 +638,7 @@ upsertMembersInRemoteConversationImpl (tUntagged -> Qualified cnv domain) users
where
upsert :: Hasql.Statement ([UserId], [Domain], [ConvId]) ()
upsert =
- lmapPG @_ @(Vector _, Vector _, Vector _)
+ lmapPG @(Vector _, Vector _, Vector _)
[resultlessStatement|INSERT INTO remote_conversation_local_member ("user", conv_remote_domain, conv_remote_id)
SELECT * FROM UNNEST($1 :: uuid[], $2 :: text[], $3 :: uuid[])
ON CONFLICT ("user", conv_remote_domain, conv_remote_id) DO NOTHING
@@ -829,7 +829,7 @@ haveRemoteConvsImpl uid =
where
select :: Hasql.Statement [UserId] [UserId]
select =
- dimapPG @[_] @(Vector _) @(Vector _) @[_]
+ dimapPG @(Vector _) @[_] @(Vector _) @[_]
[vectorStatement|SELECT DISTINCT "user" :: uuid
FROM remote_conversation_local_member
WHERE "user" = ANY ($1 :: uuid[])
@@ -842,7 +842,7 @@ selectRemoteMembersImpl uids (tUntagged -> Qualified cid domain) = do
where
select :: Hasql.Statement (Domain, ConvId, [UserId]) [UserId]
select =
- dimapPG @_ @(_, _, Vector _)
+ dimapPG @(_, _, Vector _)
[vectorStatement|SELECT ("user" :: uuid)
FROM remote_conversation_local_member
WHERE conv_remote_domain = ($1 :: text)
@@ -944,7 +944,7 @@ deleteMembersImpl cid users =
where
deleteLocalsStmt :: Hasql.Statement (ConvId, [UserId]) ()
deleteLocalsStmt =
- lmapPG @_ @(_, Vector _)
+ lmapPG @(_, Vector _)
[resultlessStatement|DELETE FROM conversation_member
WHERE conv = ($1 :: uuid)
AND "user" = ANY($2 :: uuid[])
@@ -952,7 +952,7 @@ deleteMembersImpl cid users =
deleteRemotesStmt :: Hasql.Statement (ConvId, Domain, [UserId]) ()
deleteRemotesStmt =
- lmapPG @_ @(_, _, Vector _)
+ lmapPG @(_, _, Vector _)
[resultlessStatement|DELETE FROM local_conversation_remote_member
WHERE conv = ($1 :: uuid)
AND user_remote_domain = ($2 :: text)
@@ -965,7 +965,7 @@ deleteMembersInRemoteConversationImpl (tUntagged -> Qualified cid domain) uids =
where
delete :: Hasql.Statement (Domain, ConvId, [UserId]) ()
delete =
- lmapPG @_ @(_, _, Vector _)
+ lmapPG @(_, _, Vector _)
[resultlessStatement|DELETE FROM remote_conversation_local_member
WHERE conv_remote_domain = ($1 :: text)
AND conv_remote_id = ($2 :: uuid)
@@ -1270,7 +1270,7 @@ searchConversationsImpl req =
literal "with conv as (select id, name, access from conversation"
<> where_
( [ clause1 "team" "=" req.team,
- clause1 "group_conv_type" "=" (postgresMarshall @_ @Int32 Channel)
+ clause1 "group_conv_type" "=" (postgresMarshall @Int32 Channel)
]
<> [ clause
(sortOrderOperator req.sortOrder)
From f2a234f70f3e8cec29692306324b286b58963711 Mon Sep 17 00:00:00 2001
From: Sven Tennie
Date: Thu, 15 Jan 2026 09:35:17 +0100
Subject: [PATCH 07/26] Simplify rusty-jwt-tools Nix setup and upgrade to
0.14.0 (#4952)
Looks like `buildRustPackage` improved. Thus, we can simplify this Nix
derivation and - because rusty-jwt-tool now has a `Cargo.lock` file on toplevel
- drop cargo lock file related logic.
It does not hurt to ensure that our toolchain is correct by running
`rusty-jwt-tool`'s tests. This can be overridden on higher level.
The FFI bindings have been updated: The `now` parameter of
`generate_dpop_access_token` has been removed; See
https://github.com/wireapp/rusty-jwt-tools/blob/b5de03a239d41b51e7cf9cd7c2674fa6be205134/CHANGELOG.md#L22
This commit upgrade `rusty-jwt-tools` to version 0.14.0.
---
.../5-internal/simplify_rusty-jwt-tools_setup | 2 +
libs/jwt-tools/default.nix | 7 +-
libs/jwt-tools/jwt-tools.cabal | 1 +
libs/jwt-tools/src/Data/Jwt/Tools.hs | 12 +-
libs/jwt-tools/test/Spec.hs | 153 ++++++++++++------
nix/pkgs/rusty_jwt_tools_ffi/default.nix | 32 +---
services/brig/src/Brig/API/Client.hs | 1 -
services/brig/src/Brig/Effects/JwtTools.hs | 5 +-
8 files changed, 127 insertions(+), 86 deletions(-)
create mode 100644 changelog.d/5-internal/simplify_rusty-jwt-tools_setup
diff --git a/changelog.d/5-internal/simplify_rusty-jwt-tools_setup b/changelog.d/5-internal/simplify_rusty-jwt-tools_setup
new file mode 100644
index 0000000000..5853dee370
--- /dev/null
+++ b/changelog.d/5-internal/simplify_rusty-jwt-tools_setup
@@ -0,0 +1,2 @@
+Simplify and modernize the Nix setup of `rusty-jwt-tools`. This includes
+updating to version `0.14.0`.
diff --git a/libs/jwt-tools/default.nix b/libs/jwt-tools/default.nix
index 26a5f5f455..2a8caa3f5a 100644
--- a/libs/jwt-tools/default.nix
+++ b/libs/jwt-tools/default.nix
@@ -28,7 +28,12 @@ mkDerivation {
utf8-string
];
librarySystemDepends = [ rusty_jwt_tools_ffi ];
- testHaskellDepends = [ hspec imports string-conversions ];
+ testHaskellDepends = [
+ hspec
+ imports
+ string-conversions
+ transformers
+ ];
description = "FFI to rusty-jwt-tools";
license = lib.licenses.agpl3Only;
}
diff --git a/libs/jwt-tools/jwt-tools.cabal b/libs/jwt-tools/jwt-tools.cabal
index 4cc7800ef9..5e98d6fe32 100644
--- a/libs/jwt-tools/jwt-tools.cabal
+++ b/libs/jwt-tools/jwt-tools.cabal
@@ -84,6 +84,7 @@ test-suite jwt-tools-tests
, imports
, jwt-tools
, string-conversions
+ , transformers
hs-source-dirs: test
default-language: GHC2021
diff --git a/libs/jwt-tools/src/Data/Jwt/Tools.hs b/libs/jwt-tools/src/Data/Jwt/Tools.hs
index 777485f642..4ffc885320 100644
--- a/libs/jwt-tools/src/Data/Jwt/Tools.hs
+++ b/libs/jwt-tools/src/Data/Jwt/Tools.hs
@@ -72,8 +72,6 @@ type MaxSkewSecsWord16 = Word16
type ExpiryEpochWord64 = Word64
-type EpochWord64 = Word64
-
type BackendBundleCStr = CString
type DisplayNameCStr = CString
@@ -92,7 +90,6 @@ foreign import ccall unsafe "generate_dpop_access_token"
MethodCStr ->
MaxSkewSecsWord16 ->
ExpiryEpochWord64 ->
- EpochWord64 ->
BackendBundleCStr ->
IO (Ptr HsResult)
@@ -115,11 +112,10 @@ generateDpopAccessTokenFfi ::
MethodCStr ->
MaxSkewSecsWord16 ->
ExpiryEpochWord64 ->
- EpochWord64 ->
BackendBundleCStr ->
IO (Maybe (Ptr HsResult))
-generateDpopAccessTokenFfi dpopProof user client handle displayName tid domain nonce uri method maxSkewSecs expiration now backendKeys = do
- ptr <- generate_dpop_access_token dpopProof user client handle displayName tid domain nonce uri method maxSkewSecs expiration now backendKeys
+generateDpopAccessTokenFfi dpopProof user client handle displayName tid domain nonce uri method maxSkewSecs expiration backendKeys = do
+ ptr <- generate_dpop_access_token dpopProof user client handle displayName tid domain nonce uri method maxSkewSecs expiration backendKeys
if ptr /= nullPtr
then pure $ Just ptr
else pure Nothing
@@ -152,10 +148,9 @@ generateDpopToken ::
StdMethod ->
MaxSkewSecs ->
ExpiryEpoch ->
- NowEpoch ->
PemBundle ->
ExceptT DPoPTokenGenerationError m ByteString
-generateDpopToken dpopProof uid cid handle displayName tid domain nonce uri method maxSkewSecs maxExpiration now backendPubkeyBundle = do
+generateDpopToken dpopProof uid cid handle displayName tid domain nonce uri method maxSkewSecs maxExpiration backendPubkeyBundle = do
dpopProofCStr <- toCStr dpopProof
uidCStr <- toCStr uid
handleCStr <- toCStr handle
@@ -181,7 +176,6 @@ generateDpopToken dpopProof uid cid handle displayName tid domain nonce uri meth
methodCStr
(_unMaxSkewSecs maxSkewSecs)
(_unExpiryEpoch maxExpiration)
- (_unNowEpoch now)
backendPubkeyBundleCStr
let mkAccessToken response = do
diff --git a/libs/jwt-tools/test/Spec.hs b/libs/jwt-tools/test/Spec.hs
index 03c9e53ba7..4e28225cbf 100644
--- a/libs/jwt-tools/test/Spec.hs
+++ b/libs/jwt-tools/test/Spec.hs
@@ -15,6 +15,7 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
+import Control.Monad.Trans.Except
import Data.Jwt.Tools
import Data.String.Conversions
import Imports
@@ -24,68 +25,130 @@ main :: IO ()
main = hspec $ do
describe "toResult" $ do
it "should convert to correct error" $ do
- toResult Nothing (Just token) `shouldBe` Right (cs token)
+ toResult Nothing (Just emptyToken) `shouldBe` Right (cs emptyToken)
toResult (Just 1) Nothing `shouldBe` Left UnknownError
- toResult (Just 1) (Just token) `shouldBe` Left UnknownError
+ toResult (Just 1) (Just emptyToken) `shouldBe` Left UnknownError
toResult (Just 2) Nothing `shouldBe` Left FfiError
- toResult (Just 2) (Just token) `shouldBe` Left FfiError
+ toResult (Just 2) (Just emptyToken) `shouldBe` Left FfiError
toResult (Just 3) Nothing `shouldBe` Left ImplementationError
- toResult (Just 3) (Just token) `shouldBe` Left ImplementationError
+ toResult (Just 3) (Just emptyToken) `shouldBe` Left ImplementationError
toResult (Just 4) Nothing `shouldBe` Left DpopSyntaxError
- toResult (Just 4) (Just token) `shouldBe` Left DpopSyntaxError
+ toResult (Just 4) (Just emptyToken) `shouldBe` Left DpopSyntaxError
toResult (Just 5) Nothing `shouldBe` Left DpopTypError
- toResult (Just 5) (Just token) `shouldBe` Left DpopTypError
+ toResult (Just 5) (Just emptyToken) `shouldBe` Left DpopTypError
toResult (Just 6) Nothing `shouldBe` Left DpopUnsupportedAlgorithmError
- toResult (Just 6) (Just token) `shouldBe` Left DpopUnsupportedAlgorithmError
+ toResult (Just 6) (Just emptyToken) `shouldBe` Left DpopUnsupportedAlgorithmError
toResult (Just 7) Nothing `shouldBe` Left DpopInvalidSignatureError
- toResult (Just 7) (Just token) `shouldBe` Left DpopInvalidSignatureError
+ toResult (Just 7) (Just emptyToken) `shouldBe` Left DpopInvalidSignatureError
toResult (Just 8) Nothing `shouldBe` Left ClientIdMismatchError
- toResult (Just 8) (Just token) `shouldBe` Left ClientIdMismatchError
+ toResult (Just 8) (Just emptyToken) `shouldBe` Left ClientIdMismatchError
toResult (Just 9) Nothing `shouldBe` Left BackendNonceMismatchError
- toResult (Just 9) (Just token) `shouldBe` Left BackendNonceMismatchError
+ toResult (Just 9) (Just emptyToken) `shouldBe` Left BackendNonceMismatchError
toResult (Just 10) Nothing `shouldBe` Left HtuMismatchError
- toResult (Just 10) (Just token) `shouldBe` Left HtuMismatchError
+ toResult (Just 10) (Just emptyToken) `shouldBe` Left HtuMismatchError
toResult (Just 11) Nothing `shouldBe` Left HtmMismatchError
- toResult (Just 11) (Just token) `shouldBe` Left HtmMismatchError
+ toResult (Just 11) (Just emptyToken) `shouldBe` Left HtmMismatchError
toResult (Just 12) Nothing `shouldBe` Left MissingJtiError
- toResult (Just 12) (Just token) `shouldBe` Left MissingJtiError
+ toResult (Just 12) (Just emptyToken) `shouldBe` Left MissingJtiError
toResult (Just 13) Nothing `shouldBe` Left MissingChallengeError
- toResult (Just 13) (Just token) `shouldBe` Left MissingChallengeError
+ toResult (Just 13) (Just emptyToken) `shouldBe` Left MissingChallengeError
toResult (Just 14) Nothing `shouldBe` Left MissingIatError
- toResult (Just 14) (Just token) `shouldBe` Left MissingIatError
+ toResult (Just 14) (Just emptyToken) `shouldBe` Left MissingIatError
toResult (Just 15) Nothing `shouldBe` Left IatError
- toResult (Just 15) (Just token) `shouldBe` Left IatError
+ toResult (Just 15) (Just emptyToken) `shouldBe` Left IatError
toResult (Just 16) Nothing `shouldBe` Left MissingExpError
- toResult (Just 16) (Just token) `shouldBe` Left MissingExpError
+ toResult (Just 16) (Just emptyToken) `shouldBe` Left MissingExpError
toResult (Just 17) Nothing `shouldBe` Left ExpMismatchError
- toResult (Just 17) (Just token) `shouldBe` Left ExpMismatchError
+ toResult (Just 17) (Just emptyToken) `shouldBe` Left ExpMismatchError
toResult (Just 18) Nothing `shouldBe` Left Expired
- toResult (Just 18) (Just token) `shouldBe` Left Expired
- toResult (Just 19) (Just token) `shouldBe` Left InvalidUserId
- toResult (Just 20) (Just token) `shouldBe` Left NotYetValid
- toResult (Just 21) (Just token) `shouldBe` Left JwtSimpleError
- toResult (Just 22) (Just token) `shouldBe` Left RandError
- toResult (Just 23) (Just token) `shouldBe` Left Sec1Error
- toResult (Just 24) (Just token) `shouldBe` Left UrlParseError
- toResult (Just 25) (Just token) `shouldBe` Left UuidError
- toResult (Just 26) (Just token) `shouldBe` Left Utf8Error
- toResult (Just 27) (Just token) `shouldBe` Left Base64DecodeError
- toResult (Just 28) (Just token) `shouldBe` Left JsonError
- toResult (Just 29) (Just token) `shouldBe` Left InvalidJsonPath
- toResult (Just 30) (Just token) `shouldBe` Left JsonPathError
- toResult (Just 31) (Just token) `shouldBe` Left InvalidJwkThumbprint
- toResult (Just 32) (Just token) `shouldBe` Left MissingDpopHeader
- toResult (Just 33) (Just token) `shouldBe` Left MissingIssuer
- toResult (Just 34) (Just token) `shouldBe` Left DpopChallengeMismatch
- toResult (Just 35) (Just token) `shouldBe` Left DpopHtuMismatch
- toResult (Just 36) (Just token) `shouldBe` Left DpopHtmMismatch
- toResult (Just 37) (Just token) `shouldBe` Left InvalidBackendKeys
- toResult (Just 38) (Just token) `shouldBe` Left InvalidClientId
- toResult (Just 39) (Just token) `shouldBe` Left UnsupportedApiVersion
- toResult (Just 40) (Just token) `shouldBe` Left UnsupportedScope
- toResult (Just 41) (Just token) `shouldBe` Left DpopHandleMismatch
- toResult (Just 42) (Just token) `shouldBe` Left DpopTeamMismatch
- toResult (Just 43) (Just token) `shouldBe` Left DpopDisplayNameMismatch
+ toResult (Just 18) (Just emptyToken) `shouldBe` Left Expired
+ toResult (Just 19) (Just emptyToken) `shouldBe` Left InvalidUserId
+ toResult (Just 20) (Just emptyToken) `shouldBe` Left NotYetValid
+ toResult (Just 21) (Just emptyToken) `shouldBe` Left JwtSimpleError
+ toResult (Just 22) (Just emptyToken) `shouldBe` Left RandError
+ toResult (Just 23) (Just emptyToken) `shouldBe` Left Sec1Error
+ toResult (Just 24) (Just emptyToken) `shouldBe` Left UrlParseError
+ toResult (Just 25) (Just emptyToken) `shouldBe` Left UuidError
+ toResult (Just 26) (Just emptyToken) `shouldBe` Left Utf8Error
+ toResult (Just 27) (Just emptyToken) `shouldBe` Left Base64DecodeError
+ toResult (Just 28) (Just emptyToken) `shouldBe` Left JsonError
+ toResult (Just 29) (Just emptyToken) `shouldBe` Left InvalidJsonPath
+ toResult (Just 30) (Just emptyToken) `shouldBe` Left JsonPathError
+ toResult (Just 31) (Just emptyToken) `shouldBe` Left InvalidJwkThumbprint
+ toResult (Just 32) (Just emptyToken) `shouldBe` Left MissingDpopHeader
+ toResult (Just 33) (Just emptyToken) `shouldBe` Left MissingIssuer
+ toResult (Just 34) (Just emptyToken) `shouldBe` Left DpopChallengeMismatch
+ toResult (Just 35) (Just emptyToken) `shouldBe` Left DpopHtuMismatch
+ toResult (Just 36) (Just emptyToken) `shouldBe` Left DpopHtmMismatch
+ toResult (Just 37) (Just emptyToken) `shouldBe` Left InvalidBackendKeys
+ toResult (Just 38) (Just emptyToken) `shouldBe` Left InvalidClientId
+ toResult (Just 39) (Just emptyToken) `shouldBe` Left UnsupportedApiVersion
+ toResult (Just 40) (Just emptyToken) `shouldBe` Left UnsupportedScope
+ toResult (Just 41) (Just emptyToken) `shouldBe` Left DpopHandleMismatch
+ toResult (Just 42) (Just emptyToken) `shouldBe` Left DpopTeamMismatch
+ toResult (Just 43) (Just emptyToken) `shouldBe` Left DpopDisplayNameMismatch
toResult Nothing Nothing `shouldBe` Left UnknownError
+ describe "generateDpopToken" $ do
+ -- These two tests are ported from `rusty-jwt-tools` because they were
+ -- dropped there. See:
+ -- https://github.com/wireapp/rusty-jwt-tools/commit/e86242e8c4faf7dd77319254e2c5e2c79345a46d
+ it "should return an error when given wrong nonce" $ do
+ actual <-
+ runExceptT
+ $ generateDpopToken
+ proof
+ uid
+ clientId
+ handle
+ displayName
+ teamId
+ domain
+ (Nonce "foobar")
+ url
+ method
+ maxSkewSeconds
+ expiration
+ pubKeyBundle
+
+ actual `shouldBe` Left BackendNonceMismatchError
+
+ it "should return a valid access token" $ do
+ actual <-
+ runExceptT
+ $ generateDpopToken
+ proof
+ uid
+ clientId
+ handle
+ displayName
+ teamId
+ domain
+ nonce
+ url
+ method
+ maxSkewSeconds
+ expiration
+ pubKeyBundle
+
+ isRight actual `shouldBe` True
where
- token = ""
+ pubKeyBundle =
+ PemBundle
+ "-----BEGIN PRIVATE KEY-----\n\
+ \MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5i88D4XpjBudqAkS\n\
+ \3r4zMK0hEXT7i+xR3PyGfrPHcqahRANCAAQ84mdGFohHioIhOG/s8S2mHNXiKzdV\n\
+ \ZTvpq663q4ErPGj7OP0P7Ef1QrXvHmTDOTx5YwUJ3OAxDXDOdSkD0zPt\n\
+ \-----END PRIVATE KEY-----"
+ clientId = ClientId 1223
+ domain = Domain "example.com"
+ url = Uri "https://wire.example.com/client/token"
+ method = POST
+ maxSkewSeconds = MaxSkewSecs 1
+ proof = Proof "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiLUE2T3ZqNFVzRmFrbFZMUHZhZDhYNF80MXRBTW55ZnR3aGVXbnNSMzVvbyIsInkiOiI3S3E3UzQxUjh4NUVzTnVjY1J4Y3ItcjN2SWhYVmloR3BLUFAweThIczBvIn19.eyJpYXQiOjE3MjcyMTI5NDIsImV4cCI6MjA0MjU3NjU0MiwibmJmIjoxNzI3MjEyOTQyLCJzdWIiOiJ3aXJlYXBwOi8vU3ZQZkxsd0JRaS02b2RkVlJya3FwdyE0YzdAZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL3N0ZXBjYS9hY21lL3dpcmUvY2hhbGxlbmdlL2FhYS9iYmIiLCJqdGkiOiJlNzg1MGYxNy1jYzc3LTQ0ZmYtYThiNi0wODMyYjA1NTdkNmUiLCJub25jZSI6IldFODhFdk9CemJxR2Vyem5NKzJQL0FhZFZmNzM3NHkwY0gxOXNEU1pBMkEiLCJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6Ly93aXJlLmV4YW1wbGUuY29tL2NsaWVudC90b2tlbiIsImNoYWwiOiJva0FKMzNZbS9YUzJxbW1oaGg3YVdTYkJsWXk0VHRtMUV5c3FXOEkvOW5nIiwiaGFuZGxlIjoid2lyZWFwcDovLyU0MGpvaG5fZG9lQGV4YW1wbGUuY29tIiwidGVhbSI6IjZlODVlMDUzLTUzNmYtNDU4NS04ZmM4LWNhZGE4NzZlNWVjNyIsIm5hbWUiOiJKb2huIERvZSJ9.M7Zc0FIHazWbWg6PeFK1DVJoLiLeqx09Y9KQSLPgrp5DzGnvj2Gxo4z0ELwzpIUv9pfuw4f-tImRQSS7_RKmww"
+ uid = UserId "4af3df2e-5c01-422f-baa1-d75546b92aa7"
+ nonce = Nonce "WE88EvOBzbqGerznM+2P/AadVf7374y0cH19sDSZA2A"
+ expiration = ExpiryEpoch 2042742401
+ handle = Handle "john_doe"
+ displayName = DisplayName "John Doe"
+ teamId = TeamId "6e85e053-536f-4585-8fc8-cada876e5ec7"
+ emptyToken = ""
diff --git a/nix/pkgs/rusty_jwt_tools_ffi/default.nix b/nix/pkgs/rusty_jwt_tools_ffi/default.nix
index adb3ef3b80..f411582197 100644
--- a/nix/pkgs/rusty_jwt_tools_ffi/default.nix
+++ b/nix/pkgs/rusty_jwt_tools_ffi/default.nix
@@ -6,39 +6,19 @@
, gitMinimal
}:
-# TODO: update to crate2nix once https://github.com/wireapp/rusty-jwt-tools as a
-# Cargo.lock file in its root (not at the ffi/ subpath).
-
let
- version = "0.9.0";
+ version = "0.14.0";
src = fetchFromGitHub {
owner = "wireapp";
repo = "rusty-jwt-tools";
- rev = "05441e98d9c7c5ec9bfcfba84e885988278f10e6";
- sha256 = "sha256-HVq2BpPKp3cfdlKrS1AYWQ+a5VigFsYfSecZ60SFATI=";
+ rev = "b5de03a239d41b51e7cf9cd7c2674fa6be205134";
+ sha256 = "sha256-k7zxtdpLYWP+EYa+Vf6enViS+jIR2zt07yBZdozhKuE=";
};
- cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/Cargo.lock");
-
in
rustPlatform.buildRustPackage {
name = "rusty_jwt-tools_ffi-${version}";
inherit version src;
-
- cargoLock = {
- lockFile = cargoLockFile;
- outputHashes = {
- # if any of these need updating, replace / create new key with
- # lib.fakeSha256, rebuild, and replace with actual hash.
- "certval-0.1.4" = "sha256-4BWvSzFZhlA+mKj+Y6GNEwNSKikNGVjDoPxyxiw9TFE=";
- "biscuit-0.6.0-beta1" = "sha256-no7b4Un+7AES7EwWdZh/oeIa4w0caKLAUFsHWqgJOrg=";
- "jwt-simple-0.13.0" = "sha256-QkVi7EGrU3nF+/32tNjTtAILo8sjasR27nyRgBH+xoA=";
- "rcgen-0.9.2" = "sha256-3jFzInwdzFBot+L2Vm5NLF1ml33GH2+Iv3LqqGhLxFs=";
- "ring-0.17.0-not-released-yet" = "sha256-TP8yZo64J/d1fw8l2J4+ol70EcHvpvHJBdpF3A+6Dgo=";
- };
- };
-
- postPatch = ''
- cp ${cargoLockFile} Cargo.lock
- '';
- doCheck = false;
+ # `buildRustPackage` requires `cargoHash`. So we have to update it as well
+ # when the Git `rev` is changed.
+ cargoHash = "sha256-gvFEwb+Cxnk7GhWrKs4hEhfyAI/QiE8Zqd2ZXXLbvuE=";
}
diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs
index 51100f5867..c3ebf2899b 100644
--- a/services/brig/src/Brig/API/Client.hs
+++ b/services/brig/src/Brig/API/Client.hs
@@ -622,6 +622,5 @@ createAccessToken luid cid method link proof = do
method
maxSkewSeconds
expiresAt
- now
pubKeyBundle
pure $ (DPoPAccessTokenResponse token DPoP expiresIn, NoStore)
diff --git a/services/brig/src/Brig/Effects/JwtTools.hs b/services/brig/src/Brig/Effects/JwtTools.hs
index d50b4090e7..4163490c28 100644
--- a/services/brig/src/Brig/Effects/JwtTools.hs
+++ b/services/brig/src/Brig/Effects/JwtTools.hs
@@ -63,8 +63,6 @@ data JwtTools m a where
Word16 ->
-- | The expiration date and time, in seconds since "the epoch"
Epoch ->
- -- | Current time in seconds since "the epoch"
- Epoch ->
-- | PEM format concatenated private key and public key of the Wire backend
PEMKeys ->
JwtTools m (Either CertEnrollmentError DPoPAccessToken)
@@ -73,7 +71,7 @@ makeSem ''JwtTools
interpretJwtTools :: (Member (Embed IO) r) => Sem (JwtTools ': r) a -> Sem r a
interpretJwtTools = interpret $ \case
- GenerateDPoPAccessToken proof cid handle displayName tid nonce uri method skew ex now pem ->
+ GenerateDPoPAccessToken proof cid handle displayName tid nonce uri method skew ex pem ->
mapLeft RustError
<$> runExceptT
( DPoPAccessToken
@@ -90,7 +88,6 @@ interpretJwtTools = interpret $ \case
method
(Jwt.MaxSkewSecs skew)
(Jwt.ExpiryEpoch (epochNumber ex))
- (Jwt.NowEpoch (epochNumber now))
(Jwt.PemBundle (toByteString' pem))
)
where
From 80f423607fb8f8eae5d08e7142f43ea1751eb86b Mon Sep 17 00:00:00 2001
From: Sven Tennie
Date: Fri, 16 Jan 2026 09:27:49 +0100
Subject: [PATCH 08/26] Log SAML IdP changes (#4935)
Log all changes to SAML IdPs that are triggered via the IdP REST API in
Spar.
---
changelog.d/2-features/log-saml-idp-changes | 1 +
libs/extended/default.nix | 12 +
libs/extended/extended.cabal | 9 +
libs/extended/src/Data/X509/Extended.hs | 53 ++
.../test/Test/Data/X509/ExtendedSpec.hs | 36 ++
libs/extended/test/data/sven-test.pem | 3 +
libs/extended/test/data/test-cert.pem | 4 +
.../src/Wire/API/Routes/Public/Spar.hs | 4 +-
.../src/Wire/API/User/IdentityProvider.hs | 5 +-
services/spar/default.nix | 2 +
services/spar/spar.cabal | 2 +
services/spar/src/Spar/API.hs | 138 ++++-
.../spar/src/Spar/Sem/SAMLUserStore/Mem.hs | 13 +-
services/spar/test/Test/Spar/Saml/IdPSpec.hs | 479 ++++++++++++++++++
.../spar/test/resources/okta-keyinfo-1.xml | 21 +
15 files changed, 755 insertions(+), 27 deletions(-)
create mode 100644 changelog.d/2-features/log-saml-idp-changes
create mode 100644 libs/extended/src/Data/X509/Extended.hs
create mode 100644 libs/extended/test/Test/Data/X509/ExtendedSpec.hs
create mode 100644 libs/extended/test/data/sven-test.pem
create mode 100644 libs/extended/test/data/test-cert.pem
create mode 100644 services/spar/test/Test/Spar/Saml/IdPSpec.hs
create mode 100644 services/spar/test/resources/okta-keyinfo-1.xml
diff --git a/changelog.d/2-features/log-saml-idp-changes b/changelog.d/2-features/log-saml-idp-changes
new file mode 100644
index 0000000000..130a4281a1
--- /dev/null
+++ b/changelog.d/2-features/log-saml-idp-changes
@@ -0,0 +1 @@
+Log changes to IdP configurations made via the IdP REST API to syslog.
diff --git a/libs/extended/default.nix b/libs/extended/default.nix
index 4090a02a77..3ec398e8d1 100644
--- a/libs/extended/default.nix
+++ b/libs/extended/default.nix
@@ -5,11 +5,15 @@
{ mkDerivation
, aeson
, amqp
+, asn1-types
, base
, bytestring
, cassandra-util
, containers
+, crypton
, crypton-connection
+, crypton-pem
+, crypton-x509
, crypton-x509-store
, data-default
, errors
@@ -24,6 +28,7 @@
, http-types
, imports
, lib
+, memory
, metrics-wai
, monad-control
, prometheus-client
@@ -52,11 +57,14 @@ mkDerivation {
libraryHaskellDepends = [
aeson
amqp
+ asn1-types
base
bytestring
cassandra-util
containers
+ crypton
crypton-connection
+ crypton-x509
crypton-x509-store
data-default
errors
@@ -67,6 +75,7 @@ mkDerivation {
http-client-tls
http-types
imports
+ memory
metrics-wai
monad-control
prometheus-client
@@ -89,6 +98,9 @@ mkDerivation {
testHaskellDepends = [
aeson
base
+ bytestring
+ crypton-pem
+ crypton-x509
hspec
imports
string-conversions
diff --git a/libs/extended/extended.cabal b/libs/extended/extended.cabal
index 3828324caa..980338c38a 100644
--- a/libs/extended/extended.cabal
+++ b/libs/extended/extended.cabal
@@ -28,6 +28,7 @@ library
-- cabal-fmt: expand src
exposed-modules:
Data.Time.Clock.DiffTime
+ Data.X509.Extended
Hasql.Pool.Extended
Network.AMQP.Extended
Network.RabbitMqAdmin
@@ -88,11 +89,14 @@ library
build-depends:
aeson
, amqp
+ , asn1-types
, base
, bytestring
, cassandra-util
, containers
+ , crypton
, crypton-connection
+ , crypton-x509
, crypton-x509-store
, data-default
, errors
@@ -103,6 +107,7 @@ library
, http-client-tls
, http-types
, imports
+ , memory
, metrics-wai
, monad-control
, prometheus-client
@@ -129,6 +134,7 @@ test-suite extended-tests
main-is: Spec.hs
other-modules:
Paths_extended
+ Test.Data.X509.ExtendedSpec
Test.System.Logger.ExtendedSpec
hs-source-dirs: test
@@ -186,6 +192,9 @@ test-suite extended-tests
build-depends:
aeson
, base
+ , bytestring
+ , crypton-pem
+ , crypton-x509
, extended
, hspec
, imports
diff --git a/libs/extended/src/Data/X509/Extended.hs b/libs/extended/src/Data/X509/Extended.hs
new file mode 100644
index 0000000000..964c2ee302
--- /dev/null
+++ b/libs/extended/src/Data/X509/Extended.hs
@@ -0,0 +1,53 @@
+module Data.X509.Extended (certToString) where
+
+import Crypto.Hash
+import Data.ASN1.OID
+import Data.ASN1.Types
+import Data.ByteArray.Encoding qualified as BAE
+import Data.Map qualified as Map
+import Data.Text qualified as T
+import Data.Text.Encoding qualified as T
+import Data.X509
+import Imports
+
+certToString :: SignedCertificate -> String
+certToString signedCert =
+ let cert = getCertificate signedCert
+ issuer = dnToString $ certIssuerDN cert
+ subject = dnToString $ certSubjectDN cert
+ der = encodeSignedObject signedCert
+ fingerprint :: ByteString = BAE.convertToBase BAE.Base16 (hash der :: Digest SHA1)
+ -- Split into pairs and join with ':'
+ fingerprintStr =
+ let hex = (T.decodeUtf8 fingerprint)
+ pairs = T.unpack <$> T.chunksOf 2 hex
+ in map toUpper (intercalate ":" pairs)
+ in mconcat . intersperse "; " $
+ [ "Issuer: " <> issuer,
+ "Subject: " <> subject,
+ "SHA1 Fingerprint: " <> fingerprintStr
+ ]
+
+dnToString :: DistinguishedName -> String
+dnToString (getDistinguishedElements -> es) =
+ let dess :: [String] = mapMaybe distinguishedElementString es
+ in mconcat $ intersperse "," dess
+ where
+ distinguishedElementString :: (OID, ASN1CharacterString) -> Maybe String
+ distinguishedElementString (oid, aSN1CharacterString) = do
+ (_element, desc) <- Map.lookup oid dnElementMap
+ val <- asn1CharacterToString aSN1CharacterString
+ pure $ desc <> "=" <> val
+
+ dnElementMap :: Map OID (DnElement, String)
+ dnElementMap =
+ Map.fromList
+ [ (mkEntry DnCommonName "CN"),
+ (mkEntry DnCountry "Country"),
+ (mkEntry DnOrganization "O"),
+ (mkEntry DnOrganizationUnit "OU"),
+ (mkEntry DnEmailAddress "Email Address")
+ ]
+ where
+ mkEntry :: DnElement -> String -> (OID, (DnElement, String))
+ mkEntry e s = (getObjectID e, (e, s))
diff --git a/libs/extended/test/Test/Data/X509/ExtendedSpec.hs b/libs/extended/test/Test/Data/X509/ExtendedSpec.hs
new file mode 100644
index 0000000000..21d5316799
--- /dev/null
+++ b/libs/extended/test/Test/Data/X509/ExtendedSpec.hs
@@ -0,0 +1,36 @@
+module Test.Data.X509.ExtendedSpec where
+
+import Data.ByteString qualified as BS
+import Data.PEM
+import Data.String.Conversions
+import Data.X509
+import Data.X509.Extended
+import Imports
+import Test.Hspec
+
+spec :: Spec
+spec =
+ describe "Data.X509.Extended" $ do
+ describe "certToString" $ do
+ it "should render a representative string of a certificate from stars' Keycloak" $ do
+ let pemFilePath = "test/data/" <> "sven-test.pem"
+ expected = "Issuer: CN=sven-test; Subject: CN=sven-test; SHA1 Fingerprint: F4:A2:73:D7:B7:2E:EA:66:E1:CB:81:E9:58:BC:1A:E9:CF:3C:95:C4"
+ checkDecodingWithPEMFile pemFilePath expected
+
+ it "should render a representative string of a certificate from unit test data (saml2-web-sso)" $ do
+ let pemFilePath = "test/data/" <> "test-cert.pem"
+ expected = "Issuer: CN=accounts.accesscontrol.windows.net; Subject: CN=accounts.accesscontrol.windows.net; SHA1 Fingerprint: 15:28:A6:B8:5A:C5:36:80:B4:B0:95:C6:9A:FD:77:9C:D6:5C:78:37"
+ checkDecodingWithPEMFile pemFilePath expected
+
+checkDecodingWithPEMFile :: FilePath -> String -> IO ()
+checkDecodingWithPEMFile pemFilePath expected = do
+ -- sanity check if the file even exists
+ exists <- doesFileExist pemFilePath
+ exists `shouldBe` True
+
+ file <- BS.readFile pemFilePath
+ let decoded :: SignedCertificate = either error id $ do
+ pemBS <- pemContent . fromMaybe (error "Empty PEM list") . listToMaybe <$> pemParseBS file
+ decodeSignedCertificate pemBS
+
+ certToString decoded `shouldBe` expected
diff --git a/libs/extended/test/data/sven-test.pem b/libs/extended/test/data/sven-test.pem
new file mode 100644
index 0000000000..cabff31960
--- /dev/null
+++ b/libs/extended/test/data/sven-test.pem
@@ -0,0 +1,3 @@
+-----BEGIN CERTIFICATE-----
+MIICoTCCAYkCBgGaxY9gbjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlzdmVuLXRlc3QwHhcNMjUxMTI3MTM0MzE5WhcNMzUxMTI3MTM0NDU5WjAUMRIwEAYDVQQDDAlzdmVuLXRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVkM30EqGkdEIjF6ZDzS7mEMtsHmEXXT6bzkrOddzz8fKmle2tb6Rn7uI/pkfbTdMXKlaPQohDSed5907xn3v8TAHc/FA9lf3Mo+o7pl/aQlEHm9RedNnm1DRiuH/zZx60e6ctVFqYu4sTwJxGnM81ojrrQRXU+u4FEnAh0p1aUvXG+3iCz0NHRErYxzYLvnLSziQg70yO1qlxy/K+M04gNKe7ZGxeZbu56ysllWUhrysvGg4/rp3iu4OTb8N5U+iH0ZSDcrUUeOJP2sSNRVYr4cgkcLDI+npr8WmqfqWgc+yRQ9iPAuNYi+nE9aB4ZXf7SyAGs5gmJtT6Cm4hoUa5AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGfKx/PeiFgLStaPlN+9n7+hW/iy50qhLDtEPuXA3m1XnBLO8sB7ebyJVL1QvO33A3MQdJi1E8R1uQd7ompuQ0+62vAe/bX/EZEzbwMHyM26F+r18BJKf3Dla6ot1CKnVIJuocc9qbuhkeTaeCkFF1HyvnlN/i/oMa+KwK0OP6GRkFG/m53biq9p+jbdKK2/fVvDklt5Vma6sp6KG1HhFJQMaeL/hGGelzS84qL7H9+eSBu5krCZBLfx4L88poDiY3JudM0tS6Kzj8IFDNspXRxHy8sacWn/8ulMVXGEQhw3+u5jN/yCxkxogFg7bE9uR5JhbkZ4J7X6J9uEaU/Sobo=
+-----END CERTIFICATE-----
diff --git a/libs/extended/test/data/test-cert.pem b/libs/extended/test/data/test-cert.pem
new file mode 100644
index 0000000000..ff32fa8028
--- /dev/null
+++ b/libs/extended/test/data/test-cert.pem
@@ -0,0 +1,4 @@
+-----BEGIN CERTIFICATE-----
+MIIDBTCCAe2gAwIBAgIQev76BWqjWZxChmKkGqoAfDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE4MDIxODAwMDAwMFoXDTIwMDIxOTAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgmGiRfLh6Fdi99XI2VA3XKHStWNRLEy5Aw/gxFxchnh2kPdk/bejFOs2swcx7yUWqxujjCNRsLBcWfaKUlTnrkY7i9x9noZlMrijgJy/Lk+HH5HX24PQCDf+twjnHHxZ9G6/8VLM2e5ZBeZm+t7M3vhuumEHG3UwloLF6cUeuPdW+exnOB1U1fHBIFOG8ns4SSIoq6zw5rdt0CSI6+l7b1DEjVvPLtJF+zyjlJ1Qp7NgBvAwdiPiRMU4l8IRVbuSVKoKYJoyJ4L3eXsjczoBSTJ6VjV2mygz96DC70MY3avccFrk7tCEC6ZlMRBfY1XPLyldT7tsR3EuzjecSa1M8CAwEAAaMhMB8wHQYDVR0OBBYEFIks1srixjpSLXeiR8zES5cTY6fBMA0GCSqGSIb3DQEBCwUAA4IBAQCKthfK4C31DMuDyQZVS3F7+4Evld3hjiwqu2uGDK+qFZas/D/eDunxsFpiwqC01RIMFFN8yvmMjHphLHiBHWxcBTS+tm7AhmAvWMdxO5lzJLS+UWAyPF5ICROe8Mu9iNJiO5JlCo0Wpui9RbB1C81Xhax1gWHK245ESL6k7YWvyMYWrGqr1NuQcNS0B/AIT1Nsj1WY7efMJQOmnMHkPUTWryVZlthijYyd7P2Gz6rY5a81DAFqhDNJl2pGIAE6HWtSzeUEh3jCsHEkoglKfm4VrGJEuXcALmfCMbdfTvtu4rlsaP2hQad+MG/KJFlenoTK34EMHeBPDCpqNDz8UVNk
+-----END CERTIFICATE-----
+
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 642b9dc522..5390149a7c 100644
--- a/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs
+++ b/libs/wire-api/src/Wire/API/Routes/Public/Spar.hs
@@ -135,8 +135,8 @@ type APIIDP =
Named "idp-get" (ZOptUser :> IdpGet)
:<|> Named "idp-get-raw" (ZOptUser :> IdpGetRaw)
:<|> Named "idp-get-all" (ZOptUser :> IdpGetAll)
- :<|> Named "idp-create@v7" (Until 'V8 :> AuthProtect "TeamAdmin" :> IdpCreate) -- (change is semantic, see handler)
- :<|> Named "idp-create" (From 'V8 :> AuthProtect "TeamAdmin" :> ZHostOpt :> IdpCreate)
+ :<|> Named "idp-create@v7" (Until 'V8 :> AuthProtect "TeamAdmin" :> ZOptUser :> IdpCreate) -- (change is semantic, see handler)
+ :<|> Named "idp-create" (From 'V8 :> AuthProtect "TeamAdmin" :> ZOptUser :> ZHostOpt :> IdpCreate)
:<|> Named "idp-update" (ZOptUser :> ZHostOpt :> IdpUpdate)
:<|> Named "idp-delete" (ZOptUser :> IdpDelete)
diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs
index b6ffbd7129..441d5dc1a3 100644
--- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs
+++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs
@@ -164,7 +164,10 @@ deriveJSON (defaultOptsDropChar '_') ''IdPList
-- implement @{"uri": , "cert": }@. check both the certificate we get
-- from the server against the pinned one and the metadata url in the metadata against the one
-- we fetched the xml from, but it's unclear what the benefit would be.)
-data IdPMetadataInfo = IdPMetadataValue Text SAML.IdPMetadata
+data IdPMetadataInfo = IdPMetadataValue
+ { _rawIdpMetadataText :: Text,
+ _idpMetadataRecord :: SAML.IdPMetadata
+ }
deriving (Eq, Show, Generic)
-- | We want to store the raw xml text from the registration request in the database for
diff --git a/services/spar/default.nix b/services/spar/default.nix
index 4b4b7bf58b..60ad6c717a 100644
--- a/services/spar/default.nix
+++ b/services/spar/default.nix
@@ -22,6 +22,7 @@
, crypton-x509
, exceptions
, extended
+, filepath
, gitignoreSource
, hscim
, HsOpenSSL
@@ -212,6 +213,7 @@ mkDerivation {
bytestring-conversion
containers
cookie
+ filepath
hscim
hspec
imports
diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal
index 6d26c0d0f0..f4252f1c4f 100644
--- a/services/spar/spar.cabal
+++ b/services/spar/spar.cabal
@@ -562,6 +562,7 @@ test-suite spec
Test.Spar.DataSpec
Test.Spar.Intra.BrigSpec
Test.Spar.Roundtrip.ByteString
+ Test.Spar.Saml.IdPSpec
Test.Spar.Scim.UserSpec
Test.Spar.ScimSpec
Test.Spar.Sem.DefaultSsoCodeSpec
@@ -633,6 +634,7 @@ test-suite spec
, bytestring-conversion
, containers
, cookie
+ , filepath
, hscim
, hspec
, imports
diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs
index f5f9de0d1e..f36f6f8066 100644
--- a/services/spar/src/Spar/API.hs
+++ b/services/spar/src/Spar/API.hs
@@ -42,6 +42,15 @@ module Spar.API
IdpGetAll,
IdpCreate,
IdpDelete,
+
+ -- * published to enable testing
+
+ -- FUTUREWORK: This module should be split into two: Servant handler
+ -- subtilities and the functions that do the actual work.
+ idpCreate,
+ idpCreateV7,
+ idpDelete,
+ idpUpdate,
)
where
@@ -62,6 +71,9 @@ import Data.Text.Encoding.Error
import qualified Data.Text.Lazy as T
import Data.Text.Lazy.Encoding
import Data.Time
+import qualified Data.UUID as UUID
+import qualified Data.X509 as X509
+import Data.X509.Extended
import Imports
import Network.Wai (Request, requestHeaders)
import Network.Wai.Utilities.Request
@@ -107,6 +119,7 @@ import qualified Spar.Sem.ScimUserTimesStore as ScimUserTimesStore
import Spar.Sem.VerdictFormatStore (VerdictFormatStore)
import qualified Spar.Sem.VerdictFormatStore as VerdictFormatStore
import System.Logger (Msg)
+import qualified System.Logger as Log
import qualified URI.ByteString as URI
import Wire.API.Routes.Internal.Spar
import Wire.API.Routes.Named
@@ -213,6 +226,7 @@ apiSSO opts =
apiIDP ::
( Member Random r,
Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member GalleyAccess r,
Member BrigAccess r,
Member ScimTokenStore r,
@@ -239,7 +253,7 @@ apiINTERNAL ::
Member (Error SparError) r,
Member SAMLUserStore r,
Member ScimUserTimesStore r,
- Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member Random r,
Member GalleyAccess r,
Member BrigAccess r
@@ -472,7 +486,7 @@ authContext e = authHandler e :. EmptyContext
idpGet ::
( Member Random r,
- Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member GalleyAccess r,
Member BrigAccess r,
Member IdPConfigStore r,
@@ -505,7 +519,7 @@ idpGetRaw zusr idpid = do
idpGetAll ::
( Member Random r,
- Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member GalleyAccess r,
Member BrigAccess r,
Member IdPConfigStore r,
@@ -519,7 +533,7 @@ idpGetAll zusr = withDebugLog "idpGetAll" (const Nothing) $ do
idpGetAllByTeamId ::
( Member Random r,
- Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member GalleyAccess r,
Member BrigAccess r,
Member IdPConfigStore r,
@@ -542,7 +556,7 @@ idpGetAllByTeamId tid = do
idpDelete ::
forall r.
( Member Random r,
- Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member GalleyAccess r,
Member BrigAccess r,
Member ScimTokenStore r,
@@ -573,6 +587,11 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co
do
IdPConfigStore.deleteConfig idp
IdPRawMetadataStore.delete idpid
+ logIdPAction
+ "IdP deleted"
+ idp
+ mbzusr
+ id
pure NoContent
where
assertEmptyOrPurge :: TeamId -> Cas.Page (SAML.UserRef, UserId) -> Sem r ()
@@ -626,7 +645,7 @@ idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (co
-- (internal) https://wearezeta.atlassian.net/wiki/spaces/PAD/pages/1107001440/2024-03-27+scim+user+provisioning+and+saml2+sso+associating+scim+peers+and+saml2+idps
idpCreate ::
( Member Random r,
- Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member GalleyAccess r,
Member BrigAccess r,
Member ScimTokenStore r,
@@ -636,13 +655,14 @@ idpCreate ::
) =>
SAML.Config ->
TeamId ->
+ Maybe UserId ->
Maybe ZHostValue ->
IdPMetadataInfo ->
Maybe SAML.IdPId ->
Maybe WireIdPAPIVersion ->
Maybe (Range 1 32 Text) ->
Sem r IdP
-idpCreate samlConfig tid uncheckedMbHost (IdPMetadataValue rawIdpMetadata idpmeta) mReplaces (fromMaybe defWireIdPAPIVersion -> apiversion) mHandle = withDebugLog "idpCreateXML" (Just . show . (^. SAML.idpId)) $ do
+idpCreate samlConfig tid zUser uncheckedMbHost (IdPMetadataValue rawIdpMetadata idpmeta) mReplaces (fromMaybe defWireIdPAPIVersion -> apiversion) mHandle = withDebugLog "idpCreateXML" (Just . show . (^. SAML.idpId)) $ do
let mbHost = filterMultiIngressZHost (samlConfig._cfgDomainConfigs) uncheckedMbHost
GalleyAccess.assertSSOEnabled tid
guardMultiIngressDuplicateDomain tid mbHost
@@ -653,6 +673,11 @@ idpCreate samlConfig tid uncheckedMbHost (IdPMetadataValue rawIdpMetadata idpmet
IdPConfigStore.insertConfig idp
forM_ mReplaces $ \replaces ->
IdPConfigStore.setReplacedBy (Replaced replaces) (Replacing (idp ^. SAML.idpId))
+ logIdPAction
+ "IdP created"
+ idp
+ zUser
+ (Log.field "replaces" (maybe "None" (UUID.toString . SAML.fromIdPId) mReplaces))
pure idp
where
-- Ensure that the domain is not in use by an existing IDP
@@ -670,6 +695,19 @@ idpCreate samlConfig tid uncheckedMbHost (IdPMetadataValue rawIdpMetadata idpmet
when (zHost `elem` domains) $
throwSparSem SparIdPDomainInUse
+logIdPAction :: (Member (Logger (Msg -> Msg)) r) => String -> IdP -> Maybe UserId -> (Msg -> Msg) -> Sem r ()
+logIdPAction msg idp zUser additionalFields =
+ Logger.info $
+ Log.msg (msg)
+ . Log.field "team" (idp ^. SAML.idpExtraInfo . team . to idToText)
+ . Log.field "idpId" (idp ^. SAML.idpId . to SAML.fromIdPId . to UUID.toString)
+ . Log.field "issuer" (idp ^. SAML.idpMetadata . SAML.edIssuer . SAML.fromIssuer . to URI.serializeURIRef')
+ . Log.field "domain" (idp ^. SAML.idpExtraInfo . domain . to (fromMaybe "None"))
+ . Log.field "user" (maybe "None" idToText zUser)
+ . Log.field "certificates" (idp ^. SAML.idpMetadata . SAML.edCertAuthnResponse . to (intercalate ";; " . map certToString . toList))
+ . Log.field "idp-endpoint" (idp ^. SAML.idpMetadata . SAML.edRequestURI . to URI.serializeURIRef')
+ . additionalFields
+
-- | Only return a ZHost when multi-ingress is configured and the host value is a configured domain
filterMultiIngressZHost :: Either SAML.MultiIngressDomainConfig (Map Domain SAML.MultiIngressDomainConfig) -> Maybe ZHostValue -> Maybe ZHostValue
filterMultiIngressZHost (Right domainMap) (Just zHost) | (Domain zHost) `Map.member` domainMap = Just zHost
@@ -677,7 +715,7 @@ filterMultiIngressZHost _ _ = Nothing
idpCreateV7 ::
( Member Random r,
- Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member GalleyAccess r,
Member BrigAccess r,
Member ScimTokenStore r,
@@ -687,14 +725,15 @@ idpCreateV7 ::
) =>
SAML.Config ->
TeamId ->
+ Maybe UserId ->
IdPMetadataInfo ->
Maybe SAML.IdPId ->
Maybe WireIdPAPIVersion ->
Maybe (Range 1 32 Text) ->
Sem r IdP
-idpCreateV7 samlConfig tid idpmeta mReplaces mApiversion mHandle = do
+idpCreateV7 samlConfig tid zUser idpmeta mReplaces mApiversion mHandle = do
assertNoScimOrNoIdP
- idpCreate samlConfig tid Nothing idpmeta mReplaces mApiversion mHandle
+ idpCreate samlConfig tid zUser Nothing idpmeta mReplaces mApiversion mHandle
where
-- In teams with a scim access token, only one IdP is allowed. The reason is that scim user
-- data contains no information about the idp issuer, only the user name, so no valid saml
@@ -736,7 +775,7 @@ validateNewIdP ::
forall m r.
(HasCallStack, m ~ Sem r) =>
( Member Random r,
- Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member IdPConfigStore r,
Member (Error SparError) r
) =>
@@ -760,8 +799,8 @@ validateNewIdP apiversion _idpMetadata teamId mReplaces idpDomain idHandle = wit
mbIdp <- case apiversion of
WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1Maybe (_idpMetadata ^. SAML.edIssuer)
WireIdPAPIV2 -> IdPConfigStore.getIdPByIssuerV2Maybe (_idpMetadata ^. SAML.edIssuer) teamId
- Logger.log Logger.Debug $ show (apiversion, _idpMetadata, teamId, mReplaces)
- Logger.log Logger.Debug $ show (_idpId, oldIssuersList, mbIdp)
+ Logger.log Logger.Debug . Log.msg $ show (apiversion, _idpMetadata, teamId, mReplaces)
+ Logger.log Logger.Debug . Log.msg $ show (_idpId, oldIssuersList, mbIdp)
let failWithIdPClash :: m ()
failWithIdPClash = throwSparSem . SparNewIdPAlreadyInUse $ case apiversion of
@@ -780,7 +819,7 @@ validateNewIdP apiversion _idpMetadata teamId mReplaces idpDomain idHandle = wit
-- 'IdPMetadataInfo' directly where convenient.
idpUpdate ::
( Member Random r,
- Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member GalleyAccess r,
Member BrigAccess r,
Member IdPConfigStore r,
@@ -800,7 +839,7 @@ idpUpdate samlConfig zusr uncheckedMbHost (IdPMetadataValue raw xml) =
idpUpdateXML ::
( Member Random r,
- Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member GalleyAccess r,
Member BrigAccess r,
Member IdPConfigStore r,
@@ -815,7 +854,7 @@ idpUpdateXML ::
Maybe (Range 1 32 Text) ->
Sem r IdP
idpUpdateXML zusr mDomain raw idpmeta idpid mHandle = withDebugLog "idpUpdateXML" (Just . show . (^. SAML.idpId)) $ do
- (teamid, idp) <- validateIdPUpdate zusr idpmeta idpid
+ (teamid, idp, previousIdP) <- validateIdPUpdate zusr idpmeta idpid
GalleyAccess.assertSSOEnabled teamid
guardMultiIngressDuplicateDomain teamid mDomain idpid
IdPRawMetadataStore.store (idp ^. SAML.idpId) raw
@@ -833,6 +872,7 @@ idpUpdateXML zusr mDomain raw idpmeta idpid mHandle = withDebugLog "idpUpdateXML
WireIdPAPIV1 -> Nothing
WireIdPAPIV2 -> Just teamid
forM_ (idp'' ^. SAML.idpExtraInfo . oldIssuers) (flip IdPConfigStore.deleteIssuer mbteamid)
+ logIdPUpdate idp'' previousIdP
pure idp''
where
-- Ensure that the domain is not in use by an existing IDP
@@ -854,6 +894,60 @@ idpUpdateXML zusr mDomain raw idpmeta idpid mHandle = withDebugLog "idpUpdateXML
when otherIdpsOnSameDomain $
throwSparSem SparIdPDomainInUse
+ -- We cannot simply call `logIdPAction` here, because we need diffs for
+ -- some values (old vs. new)
+ logIdPUpdate :: (Member (Logger (Msg -> Msg)) r) => IdP -> IdP -> Sem r ()
+ logIdPUpdate idp previousIdP =
+ let (removedCerts, newCerts) =
+ compareNonEmpty
+ (previousIdP ^. SAML.idpMetadata . SAML.edCertAuthnResponse)
+ (idp ^. SAML.idpMetadata . SAML.edCertAuthnResponse)
+ in Logger.info $
+ Log.msg ("IdP updated" :: String)
+ . Log.field "team" (idp ^. SAML.idpExtraInfo . team . to idToText)
+ . Log.field "idpId" (idp ^. SAML.idpId . to SAML.fromIdPId . to UUID.toString)
+ . logChangeableScalar
+ "issuer"
+ URI.serializeURIRef'
+ (previousIdP ^. SAML.idpMetadata . SAML.edIssuer . SAML.fromIssuer)
+ (idp ^. SAML.idpMetadata . SAML.edIssuer . SAML.fromIssuer)
+ . logChangeableScalar
+ "domain"
+ (fromMaybe "None")
+ (previousIdP ^. SAML.idpExtraInfo . domain)
+ (idp ^. SAML.idpExtraInfo . domain)
+ . Log.field "user" (maybe "None" idToText zusr)
+ . logChangeableScalar
+ "idp-endpoint"
+ URI.serializeURIRef'
+ (previousIdP ^. SAML.idpMetadata . SAML.edRequestURI)
+ (idp ^. SAML.idpMetadata . SAML.edRequestURI)
+ . logCertField "certificates" (idp ^. SAML.idpMetadata . SAML.edCertAuthnResponse . to toList)
+ . logCertField "new-certificates" newCerts
+ . logCertField "removed-certificates" removedCerts
+
+ logChangeableScalar :: (Eq a, Log.ToBytes b) => ByteString -> (a -> b) -> a -> a -> Msg -> Msg
+ logChangeableScalar baseFieldName toFieldVal old new
+ | old /= new =
+ Log.field ("old-" <> baseFieldName) (toFieldVal old)
+ . Log.field ("new-" <> baseFieldName) (toFieldVal new)
+ logChangeableScalar baseFieldName toFieldVal old _new =
+ Log.field baseFieldName (toFieldVal old)
+
+ logCertField :: ByteString -> [X509.SignedCertificate] -> Msg -> Msg
+ logCertField fieldName certs
+ | not (null certs) =
+ Log.field fieldName ((intercalate ";; " . map certToString) certs)
+ logCertField _ _ = id
+
+ compareNonEmpty :: (Eq a) => NonEmpty a -> NonEmpty a -> ([a], [a])
+ compareNonEmpty xs ys =
+ let l = nub . toList $ xs
+ r = nub . toList $ ys
+ onlyL = l \\ r
+ onlyR = r \\ l
+ in (onlyL, onlyR)
+
-- | Check that: idp id is valid; calling user is admin in that idp's home team; team id in
-- new metainfo doesn't change; new issuer (if changed) is not in use anywhere else (except as
-- an earlier IdP under the same ID); request uri is https. Keep track of old issuer in extra
@@ -862,7 +956,7 @@ validateIdPUpdate ::
forall m r.
(HasCallStack, m ~ Sem r) =>
( Member Random r,
- Member (Logger String) r,
+ Member (Logger (Msg -> Msg)) r,
Member GalleyAccess r,
Member BrigAccess r,
Member IdPConfigStore r,
@@ -871,7 +965,7 @@ validateIdPUpdate ::
Maybe UserId ->
SAML.IdPMetadata ->
SAML.IdPId ->
- m (TeamId, IdP)
+ m (TeamId, IdP, IdP)
validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateIdPUpdate" (Just . show . (_2 %~ (^. SAML.idpId))) $ do
previousIdP <- IdPConfigStore.getConfig _idpId
(_, teamId) <- authorizeIdP zusr previousIdP
@@ -904,7 +998,7 @@ validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateIdPUpdate" (J
let requri = _idpMetadata ^. SAML.edRequestURI
enforceHttps requri
- pure (teamId, SAML.IdPConfig {..})
+ pure (teamId, SAML.IdPConfig {..}, previousIdP)
where
-- If the new issuer was previously used, it has to be removed from the list of old issuers,
-- to prevent it from getting deleted in a later step
@@ -919,12 +1013,12 @@ validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateIdPUpdate" (J
. URI.serializeURIRef
uri = _idpMetadata ^. SAML.edIssuer . SAML.fromIssuer
-withDebugLog :: (Member (Logger String) r) => String -> (a -> Maybe String) -> Sem r a -> Sem r a
+withDebugLog :: (Member (Logger (Msg -> Msg)) r) => String -> (a -> Maybe String) -> Sem r a -> Sem r a
withDebugLog msg showval action = do
- Logger.log Logger.Debug $ "entering " ++ msg
+ Logger.log Logger.Debug . Log.msg $ "entering " ++ msg
val <- action
let mshowedval = showval val
- Logger.log Logger.Debug $ "leaving " ++ msg ++ mconcat [": " ++ fromJust mshowedval | isJust mshowedval]
+ Logger.log Logger.Debug . Log.msg $ "leaving " ++ msg ++ mconcat [": " ++ fromJust mshowedval | isJust mshowedval]
pure val
authorizeIdP ::
diff --git a/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs b/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs
index 131ae26681..a123f099de 100644
--- a/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs
+++ b/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs
@@ -23,7 +23,9 @@ module Spar.Sem.SAMLUserStore.Mem
)
where
+import Cassandra (Page (..), emptyPage)
import Control.Lens (view)
+import qualified Data.Bifunctor
import Data.Id
import qualified Data.Map as M
import Imports
@@ -49,8 +51,15 @@ samlUserStoreToMem = (runState @(Map UserRefOrd UserId) mempty .) $
Delete _uid ur -> modify $ M.delete $ UserRefOrd ur
-- 'GetAllByIssuerPaginated' and 'NextPage' are workarounds, please also see docs at
-- 'Spar.Sem.SAMLUserStore.Cassandra.getAllSAMLUsersByIssuerPaginated'
- GetAllByIssuerPaginated _is -> error "not implemented as this has a dependency to Cassandra"
- NextPage _ -> error "not implemented as this has a dependency to Cassandra"
+ --
+ -- This mock only returns one `Page` for all results. This should be fine
+ -- for tests with small test samples.
+ GetAllByIssuerPaginated is -> gets $ \userMap ->
+ let entries =
+ Data.Bifunctor.first unUserRefOrd
+ <$> M.assocs (M.filterWithKey (\ref _ -> eqIssuer is ref) userMap)
+ in emptyPage {result = entries}
+ NextPage _ -> pure emptyPage
where
eqIssuer :: SAML.Issuer -> UserRefOrd -> Bool
eqIssuer is = (== is) . view uidTenant . unUserRefOrd
diff --git a/services/spar/test/Test/Spar/Saml/IdPSpec.hs b/services/spar/test/Test/Spar/Saml/IdPSpec.hs
new file mode 100644
index 0000000000..ac0c67cf8e
--- /dev/null
+++ b/services/spar/test/Test/Spar/Saml/IdPSpec.hs
@@ -0,0 +1,479 @@
+module Test.Spar.Saml.IdPSpec where
+
+import Arbitrary ()
+import Data.Domain
+import Data.Id (idToText, parseIdFromText)
+import qualified Data.List.NonEmpty as NonEmptyL
+import qualified Data.Map as Map
+import Data.Range
+import qualified Data.Text.Lazy as TL
+import qualified Data.Text.Lazy.Encoding as TL
+import qualified Data.Text.Lazy.IO as TL
+import Imports
+import Polysemy
+import qualified Polysemy.Error
+import Polysemy.TinyLog
+import SAML2.WebSSO
+import qualified SAML2.WebSSO as SAML
+import Spar.API (idpCreate, idpCreateV7, idpDelete, idpUpdate)
+import Spar.Error
+import Spar.Sem.BrigAccess
+import Spar.Sem.GalleyAccess
+import Spar.Sem.IdPConfigStore
+import Spar.Sem.IdPConfigStore.Mem
+import Spar.Sem.IdPRawMetadataStore
+import Spar.Sem.IdPRawMetadataStore.Mem
+import Spar.Sem.SAMLUserStore
+import Spar.Sem.SAMLUserStore.Mem
+import Spar.Sem.ScimTokenStore
+import Spar.Sem.ScimTokenStore.Mem
+import System.FilePath ((>))
+import System.Logger (Msg)
+import System.Logger.Class (Level (..))
+import Test.Hspec
+import Test.QuickCheck
+import qualified Text.XML.DSig as DSig
+import URI.ByteString (parseURI, strictURIParserOptions)
+import URI.ByteString.QQ (uri)
+import Wire.API.User (User (..))
+import Wire.API.User.IdentityProvider (IdPMetadataInfo (..), WireIdPAPIVersion (..))
+import Wire.Sem.Logger.TinyLog (LogRecorder (..), newLogRecorder, recordLogs)
+import Wire.Sem.Random
+import Wire.Sem.Random.Null
+
+spec :: Spec
+spec =
+ let tid = either error id $ parseIdFromText "6861026d-cdee-3da5-22fc-6612bb1360b8"
+ zUser = either error Just $ parseIdFromText "59128ccc-d38a-1d23-67d9-4f529ee7ca9f"
+ anyMultiIngressDomainCfg =
+ MultiIngressDomainConfig
+ { _cfgSPAppURI = [uri|https://example-sp.com/landing|],
+ _cfgSPSsoURI = [uri|https://example-sp.com/sso|],
+ _cfgContacts = [fallbackContact]
+ }
+ singleIngressSamlConfig =
+ Config
+ { -- The log level only matters for log output, not production.
+ -- Thus, we could put anything here, it just needs to be a valid
+ -- value.
+ _cfgLogLevel = Trace,
+ _cfgSPHost = "localhost",
+ _cfgSPPort = 8081,
+ _cfgDomainConfigs = Left anyMultiIngressDomainCfg
+ }
+ host = Just "backend.example.com"
+ miHost1AsText = "backend-1.example.com"
+ miDomain1 = either (error . show) id $ mkDomain miHost1AsText
+ miHost1 = Just miHost1AsText
+ miHost2AsText = "backend-2.example.com"
+ miDomain2 = either (error . show) id $ mkDomain miHost2AsText
+ miHost2 = Just miHost2AsText
+ multiIngressSamlConfig =
+ Config
+ { -- The log level only matters for log output, not production.
+ -- Thus, we could put anything here, it just needs to be a valid
+ -- value.
+ _cfgLogLevel = Trace,
+ _cfgSPHost = "localhost",
+ _cfgSPPort = 8081,
+ _cfgDomainConfigs =
+ Right $
+ Map.fromList [(miDomain1, anyMultiIngressDomainCfg), (miDomain2, anyMultiIngressDomainCfg)]
+ }
+ idpHandle = Just $ unsafeRange "some-idp"
+ apiVersionV2 = Just WireIdPAPIV2
+ issuerString = "https://accounts.accesscontrol.windows.net/auth"
+ issuer =
+ either (error . show) Issuer
+ . parseURI strictURIParserOptions
+ . fromString
+ $ issuerString
+ idpEndpointString = "https://idp-endpoint.example.com"
+ idpEndpoint =
+ either (error . show) id
+ . parseURI strictURIParserOptions
+ . fromString
+ $ idpEndpointString
+ in describe "SAML IdP change logging" $ do
+ describe "idp-create" $ do
+ it "should log IdP creation" $ do
+ idPMetadataInfo :: IdPMetadataInfo <- generate arbitrary
+ let idPMetadataInfo' =
+ idPMetadataInfo
+ { _idpMetadataRecord =
+ (idPMetadataInfo._idpMetadataRecord)
+ { SAML._edIssuer = issuer,
+ SAML._edRequestURI = idpEndpoint
+ }
+ }
+
+ expectedLogLine =
+ ( Info,
+ "IdP created, team="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText) tid
+ <> ", idpId=00000000-0000-0000-0000-000000000000, issuer="
+ <> fromString issuerString
+ <> ", domain=None, user="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText . fromJust) zUser
+ <> ", certificates=Issuer: CN=accounts.accesscontrol.windows.net; Subject: CN=accounts.accesscontrol.windows.net; SHA1 Fingerprint: 15:28:A6:B8:5A:C5:36:80:B4:B0:95:C6:9A:FD:77:9C:D6:5C:78:37"
+ <> ", idp-endpoint="
+ <> fromString idpEndpointString
+ <> ", replaces=None"
+ <> "\n"
+ )
+
+ forM_ [(minBound :: WireIdPAPIVersion) .. maxBound] $ \apiVersion -> do
+ (logs, _res) <-
+ interpretWithLoggingMock
+ Nothing
+ (idpCreate singleIngressSamlConfig tid zUser host idPMetadataInfo' Nothing (Just apiVersion) idpHandle)
+ logs `shouldContain` [expectedLogLine]
+
+ (logsV7, _res) <-
+ interpretWithLoggingMock
+ Nothing
+ (idpCreateV7 singleIngressSamlConfig tid zUser idPMetadataInfo' Nothing (Just apiVersion) idpHandle)
+ logsV7 `shouldContain` [expectedLogLine]
+
+ it "should log IdP creation with domain for multi-ingress" $ do
+ idPMetadataInfo :: IdPMetadataInfo <- generate arbitrary
+ let idPMetadataInfo' =
+ idPMetadataInfo
+ { _idpMetadataRecord =
+ (idPMetadataInfo._idpMetadataRecord)
+ { SAML._edIssuer = issuer,
+ SAML._edRequestURI = idpEndpoint
+ }
+ }
+
+ expectedLogLine :: LByteString -> LogLine
+ expectedLogLine domainPart =
+ ( Info,
+ "IdP created, team="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText) tid
+ <> ", idpId=00000000-0000-0000-0000-000000000000, issuer="
+ <> fromString issuerString
+ <> ", domain="
+ <> domainPart
+ <> ", user="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText . fromJust) zUser
+ <> ", certificates=Issuer: CN=accounts.accesscontrol.windows.net; Subject: CN=accounts.accesscontrol.windows.net; SHA1 Fingerprint: 15:28:A6:B8:5A:C5:36:80:B4:B0:95:C6:9A:FD:77:9C:D6:5C:78:37"
+ <> ", idp-endpoint="
+ <> fromString idpEndpointString
+ <> ", replaces=None"
+ <> "\n"
+ )
+ expectedLogLineWithDomain = expectedLogLine . TL.encodeUtf8 . TL.fromStrict $ miHost1AsText
+ expectedLogLineWithoutDomain = expectedLogLine "None"
+
+ forM_ [(minBound :: WireIdPAPIVersion) .. maxBound] $ \apiVersion -> do
+ (logs, _res) <-
+ interpretWithLoggingMock
+ Nothing
+ (idpCreate multiIngressSamlConfig tid zUser miHost1 idPMetadataInfo' Nothing (Just apiVersion) idpHandle)
+ logs `shouldContain` [expectedLogLineWithDomain]
+
+ -- >=V7 does not bother with multi-ingress domains for IdPs as it can
+ -- only have one IdP per team anyways.
+ (logsV7, _res) <-
+ interpretWithLoggingMock
+ Nothing
+ (idpCreateV7 multiIngressSamlConfig tid zUser idPMetadataInfo' Nothing (Just apiVersion) idpHandle)
+ logsV7 `shouldContain` [expectedLogLineWithoutDomain]
+
+ describe "idp-delete" $ do
+ it "should log IdP deletion" $ do
+ idPMetadataInfo :: IdPMetadataInfo <- generate arbitrary
+ user :: User <- generate arbitrary
+ let idPMetadataInfo' =
+ idPMetadataInfo
+ { _idpMetadataRecord =
+ (idPMetadataInfo._idpMetadataRecord)
+ { SAML._edIssuer = issuer,
+ SAML._edRequestURI = idpEndpoint
+ }
+ }
+
+ expectedLogLine =
+ ( Info,
+ "IdP deleted, team="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText) tid
+ <> ", idpId=00000000-0000-0000-0000-000000000000, issuer="
+ <> fromString issuerString
+ <> ", domain=None, user="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText . fromJust) zUser
+ <> ", certificates=Issuer: CN=accounts.accesscontrol.windows.net; Subject: CN=accounts.accesscontrol.windows.net; SHA1 Fingerprint: 15:28:A6:B8:5A:C5:36:80:B4:B0:95:C6:9A:FD:77:9C:D6:5C:78:37"
+ <> ", idp-endpoint="
+ <> fromString idpEndpointString
+ <> "\n"
+ )
+
+ (logs, _res) <- interpretWithLoggingMock (Just user) $ do
+ idp <- idpCreate singleIngressSamlConfig tid zUser host idPMetadataInfo' Nothing apiVersionV2 idpHandle
+ idpDelete zUser (idp._idpId) Nothing
+ logs `shouldContain` [expectedLogLine]
+
+ it "should log IdP deletion with domain for multi-ingress" $ do
+ idPMetadataInfo :: IdPMetadataInfo <- generate arbitrary
+ user :: User <- generate arbitrary
+ let idPMetadataInfo' =
+ idPMetadataInfo
+ { _idpMetadataRecord =
+ (idPMetadataInfo._idpMetadataRecord)
+ { SAML._edIssuer = issuer,
+ SAML._edRequestURI = idpEndpoint
+ }
+ }
+
+ expectedLogLine =
+ ( Info,
+ "IdP deleted, team="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText) tid
+ <> ", idpId=00000000-0000-0000-0000-000000000000, issuer="
+ <> fromString issuerString
+ <> ", domain="
+ <> (TL.encodeUtf8 . TL.fromStrict) miHost1AsText
+ <> ", user="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText . fromJust) zUser
+ <> ", certificates=Issuer: CN=accounts.accesscontrol.windows.net; Subject: CN=accounts.accesscontrol.windows.net; SHA1 Fingerprint: 15:28:A6:B8:5A:C5:36:80:B4:B0:95:C6:9A:FD:77:9C:D6:5C:78:37"
+ <> ", idp-endpoint="
+ <> fromString idpEndpointString
+ <> "\n"
+ )
+
+ (logs, _res) <- interpretWithLoggingMock (Just user) $ do
+ idp <- idpCreate multiIngressSamlConfig tid zUser miHost1 idPMetadataInfo' Nothing apiVersionV2 idpHandle
+ idpDelete zUser (idp._idpId) Nothing
+ logs `shouldContain` [expectedLogLine]
+
+ describe "idp-update" $ do
+ it "should log IdP update" $ do
+ idPMetadataInfo :: IdPMetadataInfo <- generate arbitrary
+ user :: User <- generate arbitrary
+ let idPMetadataInfo' =
+ idPMetadataInfo
+ { _idpMetadataRecord =
+ (idPMetadataInfo._idpMetadataRecord)
+ { SAML._edIssuer = issuer,
+ SAML._edRequestURI = idpEndpoint
+ }
+ }
+
+ expectedLogLine =
+ ( Info,
+ "IdP updated, team="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText) tid
+ <> ", idpId=00000000-0000-0000-0000-000000000000, issuer="
+ <> fromString issuerString
+ <> ", domain=None, user="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText . fromJust) zUser
+ <> ", idp-endpoint="
+ <> fromString idpEndpointString
+ <> ", certificates=Issuer: CN=accounts.accesscontrol.windows.net; Subject: CN=accounts.accesscontrol.windows.net; SHA1 Fingerprint: 15:28:A6:B8:5A:C5:36:80:B4:B0:95:C6:9A:FD:77:9C:D6:5C:78:37"
+ <> "\n"
+ )
+
+ (logs, _res) <- interpretWithLoggingMock (Just user) $ do
+ idp <- idpCreate singleIngressSamlConfig tid zUser host idPMetadataInfo' Nothing apiVersionV2 idpHandle
+ idpUpdate singleIngressSamlConfig zUser host idPMetadataInfo' (idp._idpId) Nothing
+ logs `shouldContain` [expectedLogLine]
+
+ it "should log IdP update with domain for multi-ingress" $ do
+ idPMetadataInfo :: IdPMetadataInfo <- generate arbitrary
+ user :: User <- generate arbitrary
+ let idPMetadataInfo' =
+ idPMetadataInfo
+ { _idpMetadataRecord =
+ (idPMetadataInfo._idpMetadataRecord)
+ { SAML._edIssuer = issuer,
+ SAML._edRequestURI = idpEndpoint
+ }
+ }
+
+ expectedLogLine =
+ ( Info,
+ "IdP updated, team="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText) tid
+ <> ", idpId=00000000-0000-0000-0000-000000000000, issuer="
+ <> fromString issuerString
+ <> ", domain="
+ <> (TL.encodeUtf8 . TL.fromStrict) miHost1AsText
+ <> ", user="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText . fromJust) zUser
+ <> ", idp-endpoint="
+ <> fromString idpEndpointString
+ <> ", certificates=Issuer: CN=accounts.accesscontrol.windows.net; Subject: CN=accounts.accesscontrol.windows.net; SHA1 Fingerprint: 15:28:A6:B8:5A:C5:36:80:B4:B0:95:C6:9A:FD:77:9C:D6:5C:78:37"
+ <> "\n"
+ )
+
+ (logs, _res) <- interpretWithLoggingMock (Just user) $ do
+ idp <- idpCreate multiIngressSamlConfig tid zUser miHost1 idPMetadataInfo' Nothing apiVersionV2 idpHandle
+ idpUpdate multiIngressSamlConfig zUser miHost1 idPMetadataInfo' (idp._idpId) Nothing
+ logs `shouldContain` [expectedLogLine]
+
+ it "should log IdP update with changed domain for multi-ingress" $ do
+ idPMetadataInfo :: IdPMetadataInfo <- generate arbitrary
+ user :: User <- generate arbitrary
+ let idPMetadataInfo' =
+ idPMetadataInfo
+ { _idpMetadataRecord =
+ (idPMetadataInfo._idpMetadataRecord)
+ { SAML._edIssuer = issuer,
+ SAML._edRequestURI = idpEndpoint
+ }
+ }
+
+ expectedLogLine =
+ ( Info,
+ "IdP updated, team="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText) tid
+ <> ", idpId=00000000-0000-0000-0000-000000000000, issuer="
+ <> fromString issuerString
+ <> ", old-domain="
+ <> (TL.encodeUtf8 . TL.fromStrict) miHost1AsText
+ <> ", new-domain="
+ <> (TL.encodeUtf8 . TL.fromStrict) miHost2AsText
+ <> ", user="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText . fromJust) zUser
+ <> ", idp-endpoint="
+ <> fromString idpEndpointString
+ <> ", certificates=Issuer: CN=accounts.accesscontrol.windows.net; Subject: CN=accounts.accesscontrol.windows.net; SHA1 Fingerprint: 15:28:A6:B8:5A:C5:36:80:B4:B0:95:C6:9A:FD:77:9C:D6:5C:78:37"
+ <> "\n"
+ )
+
+ (logs, _res) <- interpretWithLoggingMock (Just user) $ do
+ idp <- idpCreate multiIngressSamlConfig tid zUser miHost1 idPMetadataInfo' Nothing apiVersionV2 idpHandle
+ idpUpdate multiIngressSamlConfig zUser miHost2 idPMetadataInfo' (idp._idpId) Nothing
+ logs `shouldContain` [expectedLogLine]
+
+ it "should log IdP update (changed cert)" $ do
+ idPMetadataInfo :: IdPMetadataInfo <- generate arbitrary
+ user :: User <- generate arbitrary
+ newKeyInfo <- readSampleIO "okta-keyinfo-1.xml"
+ let newIssuerString = "https://new.idp.example.com/auth"
+ newIssuer = Issuer . (either (error . show) id) . parseURI strictURIParserOptions . fromString $ newIssuerString
+ newIdpEndpointString = "https://new.idp.example.com/login"
+ newRequestURI = either (error . show) id . parseURI strictURIParserOptions . fromString $ newIdpEndpointString
+ idPMetadataInfo' =
+ idPMetadataInfo
+ { _idpMetadataRecord =
+ (idPMetadataInfo._idpMetadataRecord)
+ { SAML._edIssuer = issuer,
+ SAML._edRequestURI = idpEndpoint
+ }
+ }
+
+ newCert = either (error . show) id $ DSig.parseKeyInfo False newKeyInfo
+ newIdPMetadata :: IdPMetadata =
+ IdPMetadata
+ { _edIssuer = newIssuer,
+ _edRequestURI = newRequestURI,
+ _edCertAuthnResponse = NonEmptyL.singleton newCert
+ }
+ idPMetadataInfo'' = IdPMetadataValue ((TL.toStrict . encode) newIdPMetadata) newIdPMetadata
+ expectedLogLine =
+ ( Info,
+ "IdP updated, team="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText) tid
+ <> ", idpId=00000000-0000-0000-0000-000000000000"
+ <> ", old-issuer="
+ <> fromString issuerString
+ <> ", new-issuer="
+ <> fromString newIssuerString
+ <> ", domain=None, user="
+ <> (TL.encodeUtf8 . TL.fromStrict . idToText . fromJust) zUser
+ <> ", old-idp-endpoint="
+ <> fromString idpEndpointString
+ <> ", new-idp-endpoint="
+ <> fromString newIdpEndpointString
+ <> ", certificates=Issuer: Country=US,O=Okta,OU=SSOProvider,CN=dev-500508,Email Address=info@okta.com; Subject: Country=US,O=Okta,OU=SSOProvider,CN=dev-500508,Email Address=info@okta.com; SHA1 Fingerprint: 5C:42:5B:27:B3:96:CC:9D:1B:1F:0E:4F:2B:8A:B8:E4:3C:9E:96:34"
+ <> ", new-certificates=Issuer: Country=US,O=Okta,OU=SSOProvider,CN=dev-500508,Email Address=info@okta.com; Subject: Country=US,O=Okta,OU=SSOProvider,CN=dev-500508,Email Address=info@okta.com; SHA1 Fingerprint: 5C:42:5B:27:B3:96:CC:9D:1B:1F:0E:4F:2B:8A:B8:E4:3C:9E:96:34"
+ <> ", removed-certificates=Issuer: CN=accounts.accesscontrol.windows.net; Subject: CN=accounts.accesscontrol.windows.net; SHA1 Fingerprint: 15:28:A6:B8:5A:C5:36:80:B4:B0:95:C6:9A:FD:77:9C:D6:5C:78:37"
+ <> "\n"
+ )
+
+ (logs, _res) <- interpretWithLoggingMock (Just user) $ do
+ idp <- idpCreate singleIngressSamlConfig tid zUser host idPMetadataInfo' Nothing apiVersionV2 idpHandle
+ idpUpdate singleIngressSamlConfig zUser host idPMetadataInfo'' (idp._idpId) Nothing
+ logs `shouldContain` [expectedLogLine]
+
+type LogLine = (Level, LByteString)
+
+interpretWithLoggingMock ::
+ Maybe User ->
+ Sem (Effs) a ->
+ IO ([LogLine], a)
+interpretWithLoggingMock mbAccount action = do
+ lr <- newLogRecorder
+ a <-
+ runFinal
+ . embedToFinal @IO
+ . Polysemy.Error.errorToIOFinal
+ . recordLogs lr
+ . ignoringState idpRawMetadataStoreToMem
+ . ignoringState idPToMem
+ . ignoringState scimTokenStoreToMem
+ . brigAccessMock mbAccount
+ . galleyAccessMock
+ . ignoringState samlUserStoreToMem
+ . randomToNull
+ $ action
+ logs <- readIORef lr.recordedLogs
+ pure (logs, either (error . show) id a)
+
+galleyAccessMock :: Sem (GalleyAccess ': r) a -> Sem r a
+galleyAccessMock = interpret $ \case
+ GetTeamMembers _teamId -> undefined
+ GetTeamMember _teamId _userId -> undefined
+ AssertHasPermission _teamId _perm _userId -> pure ()
+ AssertSSOEnabled _teamId -> pure ()
+ IsEmailValidationEnabledTeam _teamId -> undefined
+ UpdateTeamMember _userId _teamId _role -> undefined
+
+brigAccessMock :: Maybe User -> Sem (BrigAccess ': r) a -> Sem r a
+brigAccessMock mbAccount = interpret $ \case
+ CreateSAML _userRef _userId _teamId _name _managedBy _mHandle _mRichInfo _mLocale _role -> undefined
+ CreateNoSAML _txt _email _userId _teamId _name _mLocale _role -> undefined
+ UpdateEmail _userId _email _activation -> undefined
+ GetAccount _havePendingInvitations _userId -> pure mbAccount
+ GetByHandle _handle -> undefined
+ GetByEmail _email -> undefined
+ SetName _userId _name -> undefined
+ SetHandle _userId _handle -> undefined
+ SetManagedBy _userId _managedBy -> undefined
+ SetSSOId _userId _ssoId -> undefined
+ SetRichInfo _userId _richInfo -> undefined
+ SetLocale _userId _mLocale -> undefined
+ GetRichInfo _userId -> undefined
+ CheckHandleAvailable _handle -> undefined
+ DeleteUser _userId -> undefined
+ EnsureReAuthorised _mUserId _mPassword _mCode _mAction -> undefined
+ SsoLogin _userId -> undefined
+ GetStatus _userId -> undefined
+ GetStatusMaybe _userId -> undefined
+ SetStatus _userId _status -> undefined
+ GetDefaultUserLocale -> undefined
+ CheckAdminGetTeamId _userId -> undefined
+
+ignoringState :: (Functor f) => (a -> f (c, b)) -> a -> f b
+ignoringState f = fmap snd . f
+
+type Effs =
+ '[ Random,
+ SAMLUserStore,
+ GalleyAccess,
+ BrigAccess,
+ ScimTokenStore,
+ IdPConfigStore,
+ IdPRawMetadataStore,
+ Logger (Msg -> Msg),
+ Polysemy.Error.Error SparError,
+ Embed IO,
+ Final IO
+ ]
+
+readSampleIO :: (MonadIO m) => FilePath -> m TL.Text
+readSampleIO fpath =
+ liftIO $
+ TL.readFile $
+ "test/resources" > fpath
diff --git a/services/spar/test/resources/okta-keyinfo-1.xml b/services/spar/test/resources/okta-keyinfo-1.xml
new file mode 100644
index 0000000000..5eacdb5901
--- /dev/null
+++ b/services/spar/test/resources/okta-keyinfo-1.xml
@@ -0,0 +1,21 @@
+
+
+MIIDpDCCAoygAwIBAgIGAWSx7x1HMA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEG
+A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
+MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi01MDA1MDgxHDAaBgkqhkiG9w0BCQEW
+DWluZm9Ab2t0YS5jb20wHhcNMTgwNzE5MDk0NTM1WhcNMjgwNzE5MDk0NjM0WjCBkjELMAkGA1UE
+BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV
+BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtNTAwNTA4MRwwGgYJ
+KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+hUaQm/3dgPws1A5IjFK9ZQpj170vIqENuDG0tapAzkvk6+9vyhduGckHTeZF3k5MMlW9iix2Eg0q
+a1oS/Wrq/aBf7+BH6y1MJlQnaKQ3hPL+OFvYzbnrN8k2uC2LivP7Y90dXwtN3P63rA4QSyDPYEMv
+dKSubUKX/HNsUg4I2PwHmpfWBNgoMkqe0bxQILBv+84L62IYSd6k77XXnCFb/usHpG/gY6sJsTQ2
+aFl9FuJ51uf67AOj8RzPXstgtUaXbdJI0kAqKIb3j9Zv3mpPCy/GHnyB3PMalvtc1uaz1ZnwO2el
+iqhwB6/8W6CPutFo1Bhq1glQIX+1OD7906iORwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQB0h6vK
+AywJwH3g0RnocOpBvT42QW57TZ3Wzm9gbg6dQL0rB+NHDx2V0VIh51E3YHL1os9W09MreM7I74D/
+fX27r1Q3+qAsL1v3CN8WIVh9eYitBCtF7DwZmL2UXTia+GWPrabO14qAztFmTXfqNuCZej7gJd/K
+2r0KBiZtZ6o58WBREW2F70a6nN6Nk1yjzBkDTJMMf8OMXHphTaalMBXojN9W6HEDpGBE0qY7c70P
+qvfUEzd8wHWcDxo6+3jajajelk0V4rg7Cqxccr+WwjYtENEuQypNG2mbI52iPZked0QWKy0WzhSM
+w5wjJ+QDG31vJInAB2769C2KmhPDyNhU
+
+
From 2524ba983e23a2e7e305401535caf611188d865d Mon Sep 17 00:00:00 2001
From: Leif Battermann
Date: Fri, 16 Jan 2026 09:39:46 +0100
Subject: [PATCH 09/26] WPB-22811 Move CodeStore to wire-subsystems (#4959)
---
changelog.d/5-internal/WPB-22811 | 1 +
.../wire-subsystems/src/Wire}/CodeStore.hs | 24 ++-----------
.../src/Wire/CodeStore/Cassandra.hs | 35 +++++++------------
.../src/Wire/CodeStore/Cassandra/Queries.hs | 34 ++++++++++++++++++
.../src/Wire/CodeStore/Code.hs | 14 +++-----
.../src/Wire/CodeStore}/Scope.hs | 6 ++--
libs/wire-subsystems/wire-subsystems.cabal | 5 +++
services/galley/galley.cabal | 4 ---
services/galley/src/Galley/API/Action.hs | 5 +--
services/galley/src/Galley/API/Federation.hs | 1 +
services/galley/src/Galley/API/MLS/Util.hs | 2 +-
services/galley/src/Galley/API/Query.hs | 5 +--
services/galley/src/Galley/API/Teams.hs | 1 +
.../galley/src/Galley/API/Teams/Features.hs | 1 +
services/galley/src/Galley/API/Update.hs | 5 +--
services/galley/src/Galley/API/Util.hs | 4 +--
services/galley/src/Galley/App.hs | 3 +-
.../galley/src/Galley/Cassandra/Queries.hs | 17 ---------
services/galley/src/Galley/Effects.hs | 8 +++--
19 files changed, 83 insertions(+), 92 deletions(-)
create mode 100644 changelog.d/5-internal/WPB-22811
rename {services/galley/src/Galley/Effects => libs/wire-subsystems/src/Wire}/CodeStore.hs (79%)
rename services/galley/src/Galley/Cassandra/Code.hs => libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs (74%)
create mode 100644 libs/wire-subsystems/src/Wire/CodeStore/Cassandra/Queries.hs
rename services/galley/src/Galley/Data/Types.hs => libs/wire-subsystems/src/Wire/CodeStore/Code.hs (90%)
rename {services/galley/src/Galley/Data => libs/wire-subsystems/src/Wire/CodeStore}/Scope.hs (89%)
diff --git a/changelog.d/5-internal/WPB-22811 b/changelog.d/5-internal/WPB-22811
new file mode 100644
index 0000000000..6d1749a04b
--- /dev/null
+++ b/changelog.d/5-internal/WPB-22811
@@ -0,0 +1 @@
+Moved CodeStore from galley to subsystems
diff --git a/services/galley/src/Galley/Effects/CodeStore.hs b/libs/wire-subsystems/src/Wire/CodeStore.hs
similarity index 79%
rename from services/galley/src/Galley/Effects/CodeStore.hs
rename to libs/wire-subsystems/src/Wire/CodeStore.hs
index 15d71162f3..79946cc04d 100644
--- a/services/galley/src/Galley/Effects/CodeStore.hs
+++ b/libs/wire-subsystems/src/Wire/CodeStore.hs
@@ -17,35 +17,15 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
-module Galley.Effects.CodeStore
- ( -- * Code store effect
- CodeStore (..),
-
- -- * Create code
- createCode,
-
- -- * Read code
- getCode,
-
- -- * Delete code
- deleteCode,
-
- -- * Code generation
- makeKey,
- generateCode,
-
- -- * Configuration
- getConversationCodeURI,
- )
-where
+module Wire.CodeStore where
import Data.Code
import Data.Id
import Data.Misc
-import Galley.Data.Types
import Imports
import Polysemy
import Wire.API.Password
+import Wire.CodeStore.Code
data CodeStore m a where
CreateCode :: Code -> Maybe Password -> CodeStore m ()
diff --git a/services/galley/src/Galley/Cassandra/Code.hs b/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs
similarity index 74%
rename from services/galley/src/Galley/Cassandra/Code.hs
rename to libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs
index 407e6ceede..a49d7c2a57 100644
--- a/services/galley/src/Galley/Cassandra/Code.hs
+++ b/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs
@@ -15,56 +15,45 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
-module Galley.Cassandra.Code
+module Wire.CodeStore.Cassandra
( interpretCodeStoreToCassandra,
)
where
import Cassandra
-import Control.Lens
import Data.Code
import Data.Map qualified as Map
-import Galley.Cassandra.Queries qualified as Cql
-import Galley.Cassandra.Store
-import Galley.Cassandra.Util
-import Galley.Data.Types
-import Galley.Data.Types qualified as Code
-import Galley.Effects.CodeStore (CodeStore (..))
-import Galley.Env
+import Data.Misc (HttpsUrl)
import Imports
import Polysemy
import Polysemy.Input
-import Polysemy.TinyLog
import Wire.API.Password
+import Wire.CodeStore (CodeStore (..))
+import Wire.CodeStore.Cassandra.Queries qualified as Cql
+import Wire.CodeStore.Code as Code
+import Wire.Util (embedClientInput)
interpretCodeStoreToCassandra ::
( Member (Embed IO) r,
Member (Input ClientState) r,
- Member (Input Env) r,
- Member TinyLog r
+ Member (Input (Either HttpsUrl (Map Text HttpsUrl))) r
) =>
Sem (CodeStore ': r) a ->
Sem r a
interpretCodeStoreToCassandra = interpret $ \case
GetCode k s -> do
- logEffect "CodeStore.GetCode"
- embedClient $ lookupCode k s
+ embedClientInput $ lookupCode k s
CreateCode code mPw -> do
- logEffect "CodeStore.CreateCode"
- embedClient $ insertCode code mPw
+ embedClientInput $ insertCode code mPw
DeleteCode k s -> do
- logEffect "CodeStore.DeleteCode"
- embedClient $ deleteCode k s
+ embedClientInput $ deleteCode k s
MakeKey cid -> do
- logEffect "CodeStore.MakeKey"
Code.mkKey cid
GenerateCode cid s t -> do
- logEffect "CodeStore.GenerateCode"
Code.generate cid s t
GetConversationCodeURI mbHost -> do
- logEffect "CodeStore.GetConversationCodeURI"
- env <- input
- case env ^. convCodeURI of
+ convCodeURI <- input
+ case convCodeURI of
Left uri -> pure (Just uri)
Right map' ->
case mbHost of
diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Cassandra/Queries.hs b/libs/wire-subsystems/src/Wire/CodeStore/Cassandra/Queries.hs
new file mode 100644
index 0000000000..e0eed6d0e8
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/CodeStore/Cassandra/Queries.hs
@@ -0,0 +1,34 @@
+-- This file is part of the Wire Server implementation.
+--
+-- Copyright (C) 2026 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.CodeStore.Cassandra.Queries where
+
+import Cassandra as C hiding (Value)
+import Data.Id
+import Imports
+import Wire.API.Conversation.Code
+import Wire.API.Password (Password)
+import Wire.CodeStore.Scope
+
+insertCode :: PrepQuery W (Key, Value, ConvId, Scope, Maybe Password, Int32) ()
+insertCode = "INSERT INTO conversation_codes (key, value, conversation, scope, password) VALUES (?, ?, ?, ?, ?) USING TTL ?"
+
+lookupCode :: PrepQuery R (Key, Scope) (Value, Int32, ConvId, Maybe Password)
+lookupCode = "SELECT value, ttl(value), conversation, password FROM conversation_codes WHERE key = ? AND scope = ?"
+
+deleteCode :: PrepQuery W (Key, Scope) ()
+deleteCode = "DELETE FROM conversation_codes WHERE key = ? AND scope = ?"
diff --git a/services/galley/src/Galley/Data/Types.hs b/libs/wire-subsystems/src/Wire/CodeStore/Code.hs
similarity index 90%
rename from services/galley/src/Galley/Data/Types.hs
rename to libs/wire-subsystems/src/Wire/CodeStore/Code.hs
index 0f5e372a58..f49588d1b0 100644
--- a/services/galley/src/Galley/Data/Types.hs
+++ b/libs/wire-subsystems/src/Wire/CodeStore/Code.hs
@@ -2,7 +2,7 @@
-- This file is part of the Wire Server implementation.
--
--- Copyright (C) 2022 Wire Swiss GmbH
+-- Copyright (C) 2026 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
@@ -17,10 +17,8 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
-module Galley.Data.Types
- ( StoredConversation (..),
- selfConv,
- Code (..),
+module Wire.CodeStore.Code
+ ( Code (..),
Scope (..),
toCode,
generate,
@@ -34,15 +32,11 @@ import Data.Code
import Data.Id
import Data.Range
import Data.Text.Ascii qualified as Ascii
-import Galley.Data.Scope
import Imports
import OpenSSL.EVP.Digest (digestBS, getDigestByName)
import OpenSSL.Random (randBytes)
import Wire.API.Password (Password)
-import Wire.StoredConversation
-
---------------------------------------------------------------------------------
--- Code
+import Wire.CodeStore.Scope
data Code = Code
{ codeKey :: !Key,
diff --git a/services/galley/src/Galley/Data/Scope.hs b/libs/wire-subsystems/src/Wire/CodeStore/Scope.hs
similarity index 89%
rename from services/galley/src/Galley/Data/Scope.hs
rename to libs/wire-subsystems/src/Wire/CodeStore/Scope.hs
index 8d649e0a69..b48dc6e042 100644
--- a/services/galley/src/Galley/Data/Scope.hs
+++ b/libs/wire-subsystems/src/Wire/CodeStore/Scope.hs
@@ -1,8 +1,6 @@
-{-# LANGUAGE StrictData #-}
-
-- This file is part of the Wire Server implementation.
--
--- Copyright (C) 2022 Wire Swiss GmbH
+-- Copyright (C) 2026 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
@@ -17,7 +15,7 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
-module Galley.Data.Scope where
+module Wire.CodeStore.Scope where
import Cassandra hiding (Value)
import Imports
diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal
index 6a2bc1b7ae..a1991ca4b9 100644
--- a/libs/wire-subsystems/wire-subsystems.cabal
+++ b/libs/wire-subsystems/wire-subsystems.cabal
@@ -222,6 +222,11 @@ library
Wire.BlockListStore.Cassandra
Wire.BrigAPIAccess
Wire.BrigAPIAccess.Rpc
+ Wire.CodeStore
+ Wire.CodeStore.Cassandra
+ Wire.CodeStore.Cassandra.Queries
+ Wire.CodeStore.Code
+ Wire.CodeStore.Scope
Wire.ConversationStore
Wire.ConversationStore.Cassandra
Wire.ConversationStore.Cassandra.Instances
diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal
index 8f8d45f27d..30504a8074 100644
--- a/services/galley/galley.cabal
+++ b/services/galley/galley.cabal
@@ -138,7 +138,6 @@ library
Galley.App
Galley.Cassandra
Galley.Cassandra.Client
- Galley.Cassandra.Code
Galley.Cassandra.CustomBackend
Galley.Cassandra.Queries
Galley.Cassandra.SearchVisibility
@@ -147,12 +146,9 @@ library
Galley.Cassandra.TeamFeatures
Galley.Cassandra.TeamNotifications
Galley.Cassandra.Util
- Galley.Data.Scope
Galley.Data.TeamNotifications
- Galley.Data.Types
Galley.Effects
Galley.Effects.ClientStore
- Galley.Effects.CodeStore
Galley.Effects.CustomBackendStore
Galley.Effects.Queue
Galley.Effects.SearchVisibilityStore
diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs
index a29eb18d89..26be35593a 100644
--- a/services/galley/src/Galley/API/Action.hs
+++ b/services/galley/src/Galley/API/Action.hs
@@ -75,9 +75,7 @@ import Galley.API.MLS.Migration
import Galley.API.MLS.Removal
import Galley.API.Teams.Features.Get
import Galley.API.Util
-import Galley.Data.Scope (Scope (ReusableCode))
import Galley.Effects
-import Galley.Effects.CodeStore qualified as E
import Galley.Env (Env)
import Galley.Options (Opts)
import Galley.Validation
@@ -116,6 +114,9 @@ import Wire.API.Team.Member
import Wire.API.Team.Permission (Perm (AddRemoveConvMember, ModifyConvName))
import Wire.API.User as User
import Wire.BrigAPIAccess qualified as E
+import Wire.CodeStore
+import Wire.CodeStore qualified as E
+import Wire.CodeStore.Scope (Scope (ReusableCode))
import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..))
diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs
index 5a5cd06db6..f5faa04839 100644
--- a/services/galley/src/Galley/API/Federation.hs
+++ b/services/galley/src/Galley/API/Federation.hs
@@ -93,6 +93,7 @@ import Wire.API.Routes.Named
import Wire.API.Routes.Public.Galley.MLS
import Wire.API.ServantProto
import Wire.API.User (BaseProtocolTag (..))
+import Wire.CodeStore
import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
diff --git a/services/galley/src/Galley/API/MLS/Util.hs b/services/galley/src/Galley/API/MLS/Util.hs
index 1127873fc5..77ee2fd670 100644
--- a/services/galley/src/Galley/API/MLS/Util.hs
+++ b/services/galley/src/Galley/API/MLS/Util.hs
@@ -24,7 +24,6 @@ import Data.Id
import Data.Qualified
import Data.Set qualified as Set
import Data.Text qualified as T
-import Galley.Data.Types
import Galley.Effects
import Imports
import Polysemy
@@ -44,6 +43,7 @@ import Wire.API.MLS.Serialisation
import Wire.API.MLS.SubConversation
import Wire.ConversationStore
import Wire.ProposalStore
+import Wire.StoredConversation
getLocalConvForUser ::
( Member (ErrorS 'ConvNotFound) r,
diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs
index 7d3e758030..50cec71d28 100644
--- a/services/galley/src/Galley/API/Query.hs
+++ b/services/galley/src/Galley/API/Query.hs
@@ -75,8 +75,6 @@ import Galley.API.Mapping qualified as Mapping
import Galley.API.One2One
import Galley.API.Teams.Features.Get
import Galley.API.Util
-import Galley.Data.Types (Code (codeConversation))
-import Galley.Data.Types qualified as Data
import Galley.Effects
import Galley.Env
import Galley.Options
@@ -108,6 +106,9 @@ import Wire.API.Routes.MultiTablePaging qualified as Public
import Wire.API.Team.Feature as Public
import Wire.API.Team.Member (HiddenPerm (..), TeamMember)
import Wire.API.User
+import Wire.CodeStore
+import Wire.CodeStore.Code (Code (codeConversation))
+import Wire.CodeStore.Code qualified as Data
import Wire.ConversationStore qualified as E
import Wire.ConversationStore.MLS.Types
import Wire.FederationAPIAccess qualified as E
diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs
index df79cc4504..7bee76d593 100644
--- a/services/galley/src/Galley/API/Teams.hs
+++ b/services/galley/src/Galley/API/Teams.hs
@@ -128,6 +128,7 @@ import Wire.API.Team.SearchVisibility qualified as Public
import Wire.API.User qualified as U
import Wire.BrigAPIAccess qualified as Brig
import Wire.BrigAPIAccess qualified as E
+import Wire.CodeStore
import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs
index 3804928ee5..13f22e658e 100644
--- a/services/galley/src/Galley/API/Teams/Features.hs
+++ b/services/galley/src/Galley/API/Teams/Features.hs
@@ -67,6 +67,7 @@ import Wire.API.Federation.Error
import Wire.API.Team.Feature
import Wire.API.Team.Member
import Wire.BrigAPIAccess (updateSearchVisibilityInbound)
+import Wire.CodeStore
import Wire.ConversationStore (MLSCommitLockStore)
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs
index ee4220a500..c5df5e029e 100644
--- a/services/galley/src/Galley/API/Update.hs
+++ b/services/galley/src/Galley/API/Update.hs
@@ -98,10 +98,8 @@ import Galley.API.Query qualified as Query
import Galley.API.Teams.Features.Get
import Galley.API.Util
import Galley.App
-import Galley.Data.Types
import Galley.Effects
import Galley.Effects.ClientStore qualified as E
-import Galley.Effects.CodeStore qualified as E
import Galley.Env
import Galley.Options
import Imports hiding (forkIO)
@@ -134,6 +132,9 @@ import Wire.API.Team.Feature
import Wire.API.Team.Member
import Wire.API.User.Client
import Wire.API.UserGroup
+import Wire.CodeStore (CodeStore)
+import Wire.CodeStore qualified as E
+import Wire.CodeStore.Code
import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs
index aa6a22995e..a13c62c15d 100644
--- a/services/galley/src/Galley/API/Util.hs
+++ b/services/galley/src/Galley/API/Util.hs
@@ -42,10 +42,8 @@ import Data.Text qualified as T
import Data.Time
import Galley.API.Error
import Galley.API.Mapping
-import Galley.Data.Types qualified as DataTypes
import Galley.Effects
import Galley.Effects.ClientStore
-import Galley.Effects.CodeStore
import Galley.Env
import Galley.Types.Clients (Clients, fromUserClients)
import Galley.Types.Conversations.Roles
@@ -86,6 +84,8 @@ import Wire.API.User.Auth.ReAuth
import Wire.API.VersionInfo
import Wire.BackendNotificationQueueAccess
import Wire.BrigAPIAccess
+import Wire.CodeStore
+import Wire.CodeStore.Code as DataTypes
import Wire.ConversationStore
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..))
import Wire.ExternalAccess
diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs
index 488d692aa7..d7c34ca312 100644
--- a/services/galley/src/Galley/App.hs
+++ b/services/galley/src/Galley/App.hs
@@ -54,7 +54,6 @@ import Data.Range
import Data.Text qualified as Text
import Galley.API.Error
import Galley.Cassandra.Client
-import Galley.Cassandra.Code
import Galley.Cassandra.CustomBackend
import Galley.Cassandra.SearchVisibility
import Galley.Cassandra.Team
@@ -108,6 +107,7 @@ import Wire.API.Team.Feature
import Wire.AWS qualified as Aws
import Wire.BackendNotificationQueueAccess.RabbitMq qualified as BackendNotificationQueueAccess
import Wire.BrigAPIAccess.Rpc
+import Wire.CodeStore.Cassandra
import Wire.ConversationStore.Cassandra
import Wire.ConversationStore.Postgres
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..), interpretConversationSubsystem)
@@ -346,6 +346,7 @@ evalGalley e =
. mapError toResponse -- DynError
. interpretQueue (e ^. deleteQueue)
. nowToIO
+ . runInputConst (e ^. convCodeURI)
. runInputConst (e ^. options)
. runInputConst localUnit
. interpretTeamFeatureSpecialContext e
diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs
index 3f9ca75a5a..c6b8860ad8 100644
--- a/services/galley/src/Galley/Cassandra/Queries.hs
+++ b/services/galley/src/Galley/Cassandra/Queries.hs
@@ -29,9 +29,6 @@ module Galley.Cassandra.Queries
( selectCustomBackend,
upsertCustomBackend,
deleteCustomBackend,
- insertCode,
- lookupCode,
- deleteCode,
upsertMemberAddClient,
upsertMemberRmClient,
selectClients,
@@ -56,27 +53,13 @@ import Data.Id
import Data.LegalHold
import Data.Misc
import Data.Text.Lazy qualified as LT
-import Galley.Data.Scope
import Imports
import Text.RawString.QQ
-import Wire.API.Conversation.Code
-import Wire.API.Password (Password)
import Wire.API.Provider
import Wire.API.Provider.Service
import Wire.API.Team.SearchVisibility
import Wire.API.User.Client.Prekey
--- Conversations accessible by code -----------------------------------------
-
-insertCode :: PrepQuery W (Key, Value, ConvId, Scope, Maybe Password, Int32) ()
-insertCode = "INSERT INTO conversation_codes (key, value, conversation, scope, password) VALUES (?, ?, ?, ?, ?) USING TTL ?"
-
-lookupCode :: PrepQuery R (Key, Scope) (Value, Int32, ConvId, Maybe Password)
-lookupCode = "SELECT value, ttl(value), conversation, password FROM conversation_codes WHERE key = ? AND scope = ?"
-
-deleteCode :: PrepQuery W (Key, Scope) ()
-deleteCode = "DELETE FROM conversation_codes WHERE key = ? AND scope = ?"
-
-- Clients ------------------------------------------------------------------
selectClients :: PrepQuery R (Identity [UserId]) (UserId, C.Set ClientId)
diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs
index 4b20074f9a..e3aef966b7 100644
--- a/services/galley/src/Galley/Effects.hs
+++ b/services/galley/src/Galley/Effects.hs
@@ -32,7 +32,6 @@ module Galley.Effects
-- * Store effects
ClientStore,
- CodeStore,
ConversationStore,
CustomBackendStore,
LegalHoldStore,
@@ -61,9 +60,11 @@ module Galley.Effects
where
import Data.Id
+import Data.Map (Map)
+import Data.Misc (HttpsUrl)
import Data.Qualified
+import Data.Text (Text)
import Galley.Effects.ClientStore
-import Galley.Effects.CodeStore
import Galley.Effects.CustomBackendStore
import Galley.Effects.Queue
import Galley.Effects.SearchVisibilityStore
@@ -73,6 +74,7 @@ import Galley.Effects.TeamNotificationStore
import Galley.Env
import Galley.Options
import Galley.Types.Teams
+import Imports (Either)
import Polysemy
import Polysemy.Error
import Polysemy.Input
@@ -82,6 +84,7 @@ import Wire.API.Federation.Client
import Wire.API.Team.Feature
import Wire.BackendNotificationQueueAccess
import Wire.BrigAPIAccess
+import Wire.CodeStore
import Wire.ConversationStore (ConversationStore, MLSCommitLockStore)
import Wire.ConversationSubsystem
import Wire.ExternalAccess
@@ -150,6 +153,7 @@ type GalleyEffects1 =
Input (FeatureDefaults LegalholdConfig),
Input (Local ()),
Input Opts,
+ Input (Either HttpsUrl (Map Text HttpsUrl)),
Now,
Queue DeleteItem,
Error DynError,
From 179c862f3001b15a09c0e26c120ab13d82a1b535 Mon Sep 17 00:00:00 2001
From: Matthias Fischmann
Date: Fri, 16 Jan 2026 14:54:41 +0100
Subject: [PATCH 10/26] [WPB-22793] GET /teams/:tid/apps (#4960)
---
changelog.d/1-api-changes/WPB-22793-get-apps | 1 +
integration/test/API/Brig.hs | 7 ++++
integration/test/Test/Apps.hs | 13 ++++++++
.../src/Wire/API/Routes/Public/Brig.hs | 9 +++++
libs/wire-subsystems/src/Wire/AppStore.hs | 1 +
.../src/Wire/AppStore/Postgres.hs | 14 ++++++++
libs/wire-subsystems/src/Wire/AppSubsystem.hs | 1 +
.../src/Wire/AppSubsystem/Interpreter.hs | 33 +++++++++++++++++++
.../unit/Wire/MockInterpreters/AppStore.hs | 1 +
services/brig/src/Brig/API/Public.hs | 4 +++
10 files changed, 84 insertions(+)
create mode 100644 changelog.d/1-api-changes/WPB-22793-get-apps
diff --git a/changelog.d/1-api-changes/WPB-22793-get-apps b/changelog.d/1-api-changes/WPB-22793-get-apps
new file mode 100644
index 0000000000..736b9910d1
--- /dev/null
+++ b/changelog.d/1-api-changes/WPB-22793-get-apps
@@ -0,0 +1 @@
+New end-point `GET /teams/:tid/apps` listing all team apps.
\ No newline at end of file
diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs
index 5c7fee6fed..a49b612778 100644
--- a/integration/test/API/Brig.hs
+++ b/integration/test/API/Brig.hs
@@ -1240,11 +1240,18 @@ createApp creator tid new = do
"password" .= defPassword
]
+-- | https://staging-nginz-https.zinfra.io/v14/api/swagger-ui/#/default/get-app
getApp :: (MakesValue self) => self -> String -> String -> App Response
getApp self tid uid = do
req <- baseRequest self Brig Versioned $ joinHttpPath ["teams", tid, "apps", uid]
submit "GET" req
+-- | https://staging-nginz-https.zinfra.io/v14/api/swagger-ui/#/default/get-apps
+getApps :: (MakesValue self) => self -> String -> App Response
+getApps self tid = do
+ req <- baseRequest self Brig Versioned $ joinHttpPath ["teams", tid, "apps"]
+ submit "GET" req
+
refreshAppCookie :: (MakesValue u) => u -> String -> String -> App Response
refreshAppCookie u tid appId = do
req <- baseRequest u Brig Versioned $ joinHttpPath ["teams", tid, "apps", appId, "cookies"]
diff --git a/integration/test/Test/Apps.hs b/integration/test/Test/Apps.hs
index 14991a5375..857db9c7bb 100644
--- a/integration/test/Test/Apps.hs
+++ b/integration/test/Test/Apps.hs
@@ -54,6 +54,19 @@ testCreateApp = do
resp.status `shouldMatchInt` 200
resp.json %. "type" `shouldMatch` "app"
+ -- getApp, getApps
+ bindResponse (getApp owner tid appId) $ \resp -> do
+ resp.status `shouldMatchInt` 200
+ bindResponse (getApps owner tid) $ \resp -> do
+ resp.status `shouldMatchInt` 200
+ void $ resp.json >>= asList >>= assertOne
+ bindResponse (createApp owner tid (new {name = "fmappie"})) $ \resp -> do
+ resp.status `shouldMatchInt` 200
+ bindResponse (getApps owner tid) $ \resp -> do
+ resp.status `shouldMatchInt` 200
+ apps <- resp.json >>= asList
+ (sort <$> ((%. "name") `mapM` apps)) `shouldMatch` ["chappie", "fmappie"]
+
-- Creator should have type "regular"
bindResponse (getUser owner owner) $ \resp -> do
resp.status `shouldMatchInt` 200
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 313a12fe37..80ae80db1b 100644
--- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs
+++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs
@@ -2125,6 +2125,15 @@ type AppsAPI =
:> Capture "uid" UserId
:> Get '[JSON] GetApp
)
+ :<|> Named
+ "get-apps"
+ ( Summary "Get all apps in a team"
+ :> ZLocalUser
+ :> "teams"
+ :> Capture "tid" TeamId
+ :> "apps"
+ :> Get '[JSON] [GetApp]
+ )
:<|> Named
"refresh-app-cookie"
( Summary "Get a new app authentication token"
diff --git a/libs/wire-subsystems/src/Wire/AppStore.hs b/libs/wire-subsystems/src/Wire/AppStore.hs
index 99fc25f1da..5713306827 100644
--- a/libs/wire-subsystems/src/Wire/AppStore.hs
+++ b/libs/wire-subsystems/src/Wire/AppStore.hs
@@ -64,5 +64,6 @@ instance PostgresUnmarshall (UUID, UUID, Value, Text, Text, UUID) StoredApp wher
data AppStore m a where
CreateApp :: StoredApp -> AppStore m ()
GetApp :: UserId -> TeamId -> AppStore m (Maybe StoredApp)
+ GetApps :: TeamId -> AppStore m [StoredApp]
makeSem ''AppStore
diff --git a/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs b/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs
index d457003e3c..4172fce84f 100644
--- a/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs
+++ b/libs/wire-subsystems/src/Wire/AppStore/Postgres.hs
@@ -43,6 +43,7 @@ interpretAppStoreToPostgres =
interpret $ \case
CreateApp app -> createAppImpl app
GetApp userId teamId -> getAppImpl userId teamId
+ GetApps teamId -> getAppsImpl teamId
createAppImpl ::
( Member (Input Pool) r,
@@ -71,3 +72,16 @@ getAppImpl uid tid =
dimapPG
[maybeStatement| select (user_id :: uuid), (team_id :: uuid), (metadata :: json), (category :: text), (description :: text), (creator :: uuid)
from apps where user_id = ($1 :: uuid) and team_id = ($2 :: uuid) |]
+
+getAppsImpl ::
+ ( Member (Input Pool) r,
+ Member (Embed IO) r,
+ Member (Error UsageError) r
+ ) =>
+ TeamId ->
+ Sem r [StoredApp]
+getAppsImpl tid =
+ runStatement tid $
+ dimapPG
+ [vectorStatement| select (user_id :: uuid), (team_id :: uuid), (metadata :: json), (category :: text), (description :: text), (creator :: uuid)
+ from apps where team_id = ($1 :: uuid) |]
diff --git a/libs/wire-subsystems/src/Wire/AppSubsystem.hs b/libs/wire-subsystems/src/Wire/AppSubsystem.hs
index de5ae18a24..9bfd80a894 100644
--- a/libs/wire-subsystems/src/Wire/AppSubsystem.hs
+++ b/libs/wire-subsystems/src/Wire/AppSubsystem.hs
@@ -52,6 +52,7 @@ appSubsystemErrorToHttpError =
data AppSubsystem m a where
CreateApp :: Local UserId -> TeamId -> Apps.NewApp -> AppSubsystem m Apps.CreatedApp
GetApp :: Local UserId -> TeamId -> UserId -> AppSubsystem m Apps.GetApp
+ GetApps :: Local UserId -> TeamId -> AppSubsystem m [Apps.GetApp]
RefreshAppCookie ::
Local UserId ->
TeamId ->
diff --git a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs
index b3f22d1e1e..9daedd390b 100644
--- a/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs
+++ b/libs/wire-subsystems/src/Wire/AppSubsystem/Interpreter.hs
@@ -19,6 +19,7 @@ module Wire.AppSubsystem.Interpreter where
import Data.ByteString.Conversion
import Data.Id
+import Data.Map qualified as Map
import Data.Qualified
import Data.RetryAfter
import Data.Set qualified as Set
@@ -70,6 +71,7 @@ runAppSubsystem ::
runAppSubsystem = interpret \case
CreateApp lusr tid new -> createAppImpl lusr tid new
GetApp lusr tid uid -> getAppImpl lusr tid uid
+ GetApps lusr tid -> getAppsImpl lusr tid
RefreshAppCookie lusr tid appId -> runError $ refreshAppCookieImpl lusr tid appId
createAppImpl ::
@@ -165,6 +167,37 @@ getAppImpl lusr tid uid = do
description = storedApp.description
}
+getAppsImpl ::
+ ( Member AppStore r,
+ Member (Error AppSubsystemError) r,
+ Member GalleyAPIAccess r,
+ Member UserStore r
+ ) =>
+ Local UserId ->
+ TeamId ->
+ Sem r [Apps.GetApp]
+getAppsImpl lusr tid = do
+ void $ ensureTeamMember lusr tid
+ storedApps <- Store.getApps tid
+ us <- Store.getUsers ((.id) <$> storedApps)
+ let mkApp (storedApp, u) =
+ Apps.GetApp
+ { name = u.name,
+ pict = fromMaybe (Pict []) u.pict,
+ assets = fromMaybe [] u.assets,
+ accentId = u.accentId,
+ meta = storedApp.meta,
+ category = storedApp.category,
+ description = storedApp.description
+ }
+ pure $ mkApp <$> matchAndZip storedApps us
+ where
+ matchAndZip :: [StoredApp] -> [StoredUser] -> [(StoredApp, StoredUser)]
+ matchAndZip as us = mapMaybe f as
+ where
+ f a = (a,) <$> Map.lookup a.id umap
+ umap = Map.fromList $ (\u -> (u.id, u)) <$> us
+
refreshAppCookieImpl ::
( Member AuthenticationSubsystem r,
Member AppStore r,
diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs
index 4eec779120..b62e7acd60 100644
--- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs
+++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/AppStore.hs
@@ -31,3 +31,4 @@ inMemoryAppStoreInterpreter ::
inMemoryAppStoreInterpreter = interpret $ \case
CreateApp app -> modify (app :)
GetApp uid tid -> gets $ find $ \app -> app.id == uid && app.teamId == tid
+ GetApps tid -> gets $ filter $ \app -> app.teamId == tid
diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs
index 4ffc108dbf..5a206da833 100644
--- a/services/brig/src/Brig/API/Public.hs
+++ b/services/brig/src/Brig/API/Public.hs
@@ -632,6 +632,7 @@ servantSitemap =
appsAPI =
Named @"create-app" createApp
:<|> Named @"get-app" getApp
+ :<|> Named @"get-apps" getApps
:<|> Named @"refresh-app-cookie" refreshAppCookie
---------------------------------------------------------------------------
@@ -1754,6 +1755,9 @@ createApp lusr tid new = lift . liftSem $ AppSubsystem.createApp lusr tid new
getApp :: (_) => Local UserId -> TeamId -> UserId -> Handler r GetApp
getApp lusr tid uid = lift . liftSem $ AppSubsystem.getApp lusr tid uid
+getApps :: (_) => Local UserId -> TeamId -> Handler r [GetApp]
+getApps lusr tid = lift . liftSem $ AppSubsystem.getApps lusr tid
+
refreshAppCookie :: (_) => Local UserId -> TeamId -> UserId -> Handler r RefreshAppCookieResponse
refreshAppCookie lusr tid appId = do
mc <- lift . liftSem $ AppSubsystem.refreshAppCookie lusr tid appId
From d023e7234a64f5062d010bcc05e94b9c6b911c7d Mon Sep 17 00:00:00 2001
From: Mathias Staab <71255223+mastaab@users.noreply.github.com>
Date: Mon, 19 Jan 2026 09:28:34 +0100
Subject: [PATCH 11/26] Add macOS build support and cross-platform fsnotify for
federator (#4955)
* Add macOS (aarch64-darwin) build support
This commit enables building wire-server components on macOS by fixing
several platform-specific issues:
- **cryptobox**: Fix dylib install_name on Darwin using install_name_tool
to use absolute paths instead of relative paths
- **rusty_jwt_tools_ffi**: Fix dylib install_name on Darwin for Rust FFI
library to ensure proper dynamic linking
- **text-icu-translit**: Add ICU library as pkgconfig dependency
- **http2-manager**: Fix Cabal flag configuration by using disableCabalFlag
instead of enableCabalFlag with negative flag syntax
- Create macOS-compatible stub for federator package that removes Linux-
specific hinotify dependency for file system monitoring
- Implement SSL/TLS context creation without file monitoring (certificates
require manual service restart on macOS instead of auto-reload)
- Disable federator tests on Darwin (depend on filepath which was removed)
- Export mkSSLContextWithoutCert for integration tests
Note: The federator stub on macOS loses automatic certificate file
monitoring/reloading functionality. This is acceptable for local
development; production deployments on Linux retain full functionality.
All modified files:
- nix/manual-overrides.nix
- nix/wire-server.nix
- nix/pkgs/cryptobox/default.nix
- nix/pkgs/rusty_jwt_tools_ffi/default.nix
Co-Authored-By: Claude Sonnet 4.5
* Replace hinotify with fsnotify for cross-platform file monitoring
This replaces the Linux-only hinotify library with the cross-platform
fsnotify library in the federator service, enabling native certificate
file monitoring on both Linux and macOS.
Changes:
- federator.cabal: hinotify -> fsnotify dependency
- Monitor.Internal.hs: Refactored to use fsnotify API
- INotify -> WatchManager
- RawFilePath -> FilePath
- WatchDescriptor -> StopListening
- Adapted event handling for fsnotify events
- Test/Federator/Monitor.hs: Updated for FilePath-based API
- haskell-pins.nix: Added fsnotify 0.4.4.0 hackage pin
- manual-overrides.nix: Removed Darwin stub, added conditional
dontCheck for fsnotify/federator on Darwin (FSEvents doesn't
work in nix sandbox)
Co-Authored-By: Claude Opus 4.5
* Use CloseWrite events instead of Modified for certificate monitoring
fsnotify's CloseWrite event (Linux-only) indicates when a file write is
complete, which is more reliable than Modified events that may fire
multiple times during a write operation. This fixes the unit tests which
were failing due to:
1. appendFile "" not generating Modified events (only CloseWrite)
2. copyFile generating both Modified and CloseWrite, causing duplicate
reloads that confused the test assertions
On macOS (FSEvents), CloseWrite is not available, but the tests are
already disabled there (fsnotify tests don't work in nix sandbox).
Co-Authored-By: Claude Opus 4.5
* Filter redundant WatchedFile entries to prevent duplicate events
With fsnotify, both WatchedFile and WatchedDir entries end up watching
the same directory. When a file event occurs, both handlers receive it,
causing duplicate reloads. This led to test failures where the second
reload would read files in an inconsistent state.
Fix by filtering out WatchedFile entries when they're already covered
by a WatchedDir entry for the same directory and filename.
Co-Authored-By: Claude Opus 4.5
* Fix edge case in filterRedundant for root paths
The property test found that WatchedFile "/" was incorrectly filtered
when WatchedDir "/" {""} existed, because takeFileName "/" returns "".
Fix by not filtering when the filename is empty.
Co-Authored-By: Claude Opus 4.5
* haskell-pins: No need to pin fsnotify, same version is in nixpkgs
---------
Co-authored-by: Claude Sonnet 4.5
Co-authored-by: Akshay Mankar
---
.../federator-fsnotify-cross-platform | 3 +
nix/manual-overrides.nix | 12 +-
nix/pkgs/cryptobox/default.nix | 33 +++-
nix/pkgs/rusty_jwt_tools_ffi/default.nix | 13 ++
nix/wire-server.nix | 2 +-
services/federator/default.nix | 7 +-
services/federator/federator.cabal | 4 +-
.../src/Federator/Monitor/Internal.hs | 146 +++++++++---------
.../test/unit/Test/Federator/Monitor.hs | 41 ++---
9 files changed, 152 insertions(+), 109 deletions(-)
create mode 100644 changelog.d/5-internal/federator-fsnotify-cross-platform
diff --git a/changelog.d/5-internal/federator-fsnotify-cross-platform b/changelog.d/5-internal/federator-fsnotify-cross-platform
new file mode 100644
index 0000000000..5f4d306d85
--- /dev/null
+++ b/changelog.d/5-internal/federator-fsnotify-cross-platform
@@ -0,0 +1,3 @@
+Federator: Replace Linux-only hinotify with cross-platform fsnotify library
+for certificate file monitoring. This enables native file system watching
+on both Linux and macOS, removing the need for platform-specific stubs.
diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix
index ea0449d530..1deaa71cb0 100644
--- a/nix/manual-overrides.nix
+++ b/nix/manual-overrides.nix
@@ -1,4 +1,4 @@
-{ libsodium, protobuf, hlib, mls-test-cli, fetchurl, curl, pkg-config, postgresql, openssl, ... }:
+{ libsodium, protobuf, hlib, mls-test-cli, fetchurl, curl, pkg-config, postgresql, openssl, icu, cryptobox, stdenv, ... }:
# 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: {
@@ -72,6 +72,12 @@ hself: hsuper: {
# ------------------------------------
template = hlib.markUnbroken hsuper.template;
system-linux-proc = hlib.markUnbroken hsuper.system-linux-proc;
+ # FSEvents doesn't work in nix sandbox on macOS; on Linux inotify works fine
+ fsnotify = (if stdenv.isDarwin then hlib.dontCheck else (x: x))
+ (hlib.markUnbroken hsuper.fsnotify);
+
+ # Federator monitor tests use fsnotify which doesn't work in nix sandbox on macOS
+ federator = (if stdenv.isDarwin then hlib.dontCheck else (x: x)) hsuper.federator;
# -----------------
# version overrides
@@ -90,8 +96,10 @@ hself: hsuper: {
hoogle = hlib.justStaticExecutables (hlib.dontCheck (hsuper.hoogle));
# Extra dependencies/flags for local packages
- http2-manager = hlib.enableCabalFlag hsuper.http2-manager "-f-test-trailing-dot";
+ cryptobox-haskell = hlib.addBuildDepends hsuper.cryptobox-haskell [ cryptobox ];
+ http2-manager = hlib.disableCabalFlag hsuper.http2-manager "test-trailing-dot";
sodium-crypto-sign = hlib.addPkgconfigDepend hsuper.sodium-crypto-sign libsodium.dev;
+ text-icu-translit = hlib.addPkgconfigDepend hsuper.text-icu-translit icu;
types-common-journal = hlib.addBuildTool hsuper.types-common-journal protobuf;
wire-api = hlib.addBuildTool hsuper.wire-api mls-test-cli;
wire-message-proto-lens = hlib.addBuildTool hsuper.wire-message-proto-lens protobuf;
diff --git a/nix/pkgs/cryptobox/default.nix b/nix/pkgs/cryptobox/default.nix
index c5c4311c0c..a1784f5a2f 100644
--- a/nix/pkgs/cryptobox/default.nix
+++ b/nix/pkgs/cryptobox/default.nix
@@ -2,6 +2,7 @@
, libsodium
, pkg-config
, runCommand
+, stdenv
}:
let
@@ -21,18 +22,34 @@ let
rootCrate = crates.rootCrate.build;
+ # Use the correct library extension based on platform
+ libExt = if stdenv.isDarwin then "dylib" else "so";
+
in
# HACK: rather than providing the multi-output crate output, expose a single-
# output structure in the format expected by cryptobox-haskell.
- # Note it expects the .so file to be called libcryptobox.so, not
- # libcryptobox_c.so, and the cbox.h to be present.
+ # Note it expects the library file to be called libcryptobox, not
+ # libcryptobox_c, and the cbox.h to be present.
# In the future, we might want to rework this to instead have cryptobox-c crate
# emit a .pc file, and all downstream tooling use pkg-config to discover things,
# but today is not that day.
-runCommand "cryptobox" { } ''
- mkdir -p $out/lib $out/include
- cp ${rootCrate.lib}/lib/libcryptobox_c* $out/lib/
- ln -sfn libcryptobox_c.so $out/lib/libcryptobox.so
- cp ${rootCrate.src}/src/cbox.h $out/include
-''
+runCommand "cryptobox"
+{
+ nativeBuildInputs = if stdenv.isDarwin then [ pkgs.darwin.cctools ] else [ ];
+}
+ ''
+ mkdir -p $out/lib $out/include
+ cp ${rootCrate.lib}/lib/libcryptobox_c* $out/lib/
+
+ # Fix install_name on Darwin to use absolute paths
+ ${if stdenv.isDarwin then ''
+ for lib in $out/lib/libcryptobox_c*.${libExt}; do
+ libname=$(basename "$lib")
+ install_name_tool -id "$out/lib/$libname" "$lib"
+ done
+ '' else ""}
+
+ ln -sfn libcryptobox_c.${libExt} $out/lib/libcryptobox.${libExt}
+ cp ${rootCrate.src}/src/cbox.h $out/include
+ ''
diff --git a/nix/pkgs/rusty_jwt_tools_ffi/default.nix b/nix/pkgs/rusty_jwt_tools_ffi/default.nix
index f411582197..4863378368 100644
--- a/nix/pkgs/rusty_jwt_tools_ffi/default.nix
+++ b/nix/pkgs/rusty_jwt_tools_ffi/default.nix
@@ -4,6 +4,7 @@
, pkg-config
, perl
, gitMinimal
+, stdenv
}:
let
@@ -21,4 +22,16 @@ rustPlatform.buildRustPackage {
# `buildRustPackage` requires `cargoHash`. So we have to update it as well
# when the Git `rev` is changed.
cargoHash = "sha256-gvFEwb+Cxnk7GhWrKs4hEhfyAI/QiE8Zqd2ZXXLbvuE=";
+
+ # Fix install_name on Darwin to use absolute paths
+ postInstall = lib.optionalString stdenv.isDarwin ''
+ for lib in $out/lib/librusty_jwt_tools_ffi*.dylib; do
+ if [ -f "$lib" ]; then
+ libname=$(basename "$lib")
+ install_name_tool -id "$out/lib/$libname" "$lib"
+ fi
+ done
+ '';
+
+ nativeBuildInputs = lib.optionals stdenv.isDarwin [ stdenv.cc.bintools ];
}
diff --git a/nix/wire-server.nix b/nix/wire-server.nix
index dd2b183877..97926ffb53 100644
--- a/nix/wire-server.nix
+++ b/nix/wire-server.nix
@@ -157,7 +157,7 @@ let
];
manualOverrides = import ./manual-overrides.nix (with pkgs; {
- inherit (pkgs) libsodium protobuf fetchpatch fetchurl curl pkg-config postgresql openssl;
+ inherit (pkgs) libsodium protobuf fetchpatch fetchurl curl pkg-config postgresql openssl icu cryptobox stdenv;
inherit hlib mls-test-cli;
});
diff --git a/services/federator/default.nix b/services/federator/default.nix
index e1beff920c..febce147be 100644
--- a/services/federator/default.nix
+++ b/services/federator/default.nix
@@ -16,13 +16,14 @@
, crypton-x509
, crypton-x509-validation
, data-default
+, directory
, dns
, dns-util
, exceptions
, extended
, filepath
+, fsnotify
, gitignoreSource
-, hinotify
, HsOpenSSL
, hspec
, hspec-junit-formatter
@@ -91,12 +92,13 @@ mkDerivation {
crypton-x509
crypton-x509-validation
data-default
+ directory
dns
dns-util
exceptions
extended
filepath
- hinotify
+ fsnotify
HsOpenSSL
http-client
http-media
@@ -174,6 +176,7 @@ mkDerivation {
containers
crypton-x509-validation
data-default
+ directory
dns-util
filepath
HsOpenSSL
diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal
index 597cfc7e11..30da9928c5 100644
--- a/services/federator/federator.cabal
+++ b/services/federator/federator.cabal
@@ -116,12 +116,13 @@ library
, crypton-x509
, crypton-x509-validation
, data-default
+ , directory
, dns
, dns-util
, exceptions
, extended
, filepath
- , hinotify
+ , fsnotify
, HsOpenSSL
, http-client
, http-media
@@ -386,6 +387,7 @@ test-suite federator-tests
, containers
, crypton-x509-validation
, data-default
+ , directory
, dns-util
, federator
, filepath
diff --git a/services/federator/src/Federator/Monitor/Internal.hs b/services/federator/src/Federator/Monitor/Internal.hs
index d696c6e18e..8b3ab0f94a 100644
--- a/services/federator/src/Federator/Monitor/Internal.hs
+++ b/services/federator/src/Federator/Monitor/Internal.hs
@@ -18,16 +18,11 @@
module Federator.Monitor.Internal where
import Control.Exception (try)
-import Data.ByteString (packCStringLen, useAsCStringLen)
import Data.Map qualified as Map
import Data.Set qualified as Set
import Data.Text qualified as Text
-import Data.Text.Encoding qualified as Text
-import Data.Text.Encoding.Error qualified as Text
import Federator.Options (RunSettings (..))
-import GHC.Foreign (peekCStringLen, withCStringLen)
-import GHC.IO.Encoding (getFileSystemEncoding)
-import Imports
+import Imports hiding (makeAbsolute)
import Network.Wai.Utilities.Exception
import OpenSSL.Session (SSLContext)
import OpenSSL.Session qualified as SSL
@@ -38,17 +33,17 @@ import Polysemy.Final (Final)
import Polysemy.Resource qualified as Polysemy
import Polysemy.TinyLog (TinyLog)
import Polysemy.TinyLog qualified as Log
+import System.Directory (makeAbsolute)
+import System.FSNotify
import System.FilePath
-import System.INotify
import System.Logger (Logger)
import System.Logger.Message qualified as Log
-import System.Posix.ByteString (RawFilePath)
import System.Posix.Files
import Wire.Arbitrary
import Wire.Sem.Logger.TinyLog qualified as Log
data Monitor = Monitor
- { monINotify :: INotify,
+ { monWatchManager :: WatchManager,
monOnNewContext :: SSLContext -> IO (),
monWatches :: IORef Watches,
monSettings :: RunSettings,
@@ -56,29 +51,14 @@ data Monitor = Monitor
monLock :: MVar ()
}
--- This is needed because the normal Posix file system API uses strings, while
--- the inotify API uses bytestrings.
--- /Note/: File paths are strings obtained using the "file system encoding",
--- which is the same as the locale encoding, but uses some escaping tricks to
--- be able to represent arbitrary data as strings.
-rawPath :: FilePath -> IO RawFilePath
-rawPath path = do
- encoding <- getFileSystemEncoding
- withCStringLen encoding path packCStringLen
-
-fromRawPath :: RawFilePath -> IO FilePath
-fromRawPath path = do
- encoding <- getFileSystemEncoding
- useAsCStringLen path (peekCStringLen encoding)
-
data WatchedPath
- = WatchedFile RawFilePath
- | WatchedDir RawFilePath (Set RawFilePath)
+ = WatchedFile FilePath
+ | WatchedDir FilePath (Set FilePath)
deriving stock (Eq, Ord, Show, Generic)
deriving (Arbitrary) via (GenericUniform WatchedPath)
mergePaths :: [WatchedPath] -> Set WatchedPath
-mergePaths = Set.fromList . merge . sort
+mergePaths wpaths = Set.fromList $ filterRedundant $ merge $ sort wpaths
where
merge [] = []
merge [w] = [w]
@@ -87,20 +67,30 @@ mergePaths = Set.fromList . merge . sort
(WatchedDir dir1 paths1, WatchedDir dir2 paths2)
| dir1 == dir2 -> merge (WatchedDir dir1 (paths1 <> paths2) : ws)
_ -> w1 : merge (w2 : ws)
-
-watchedPath :: WatchedPath -> RawFilePath
+ -- Filter out WatchedFile entries that are already covered by a WatchedDir
+ -- This prevents duplicate watches on the same directory for the same file,
+ -- which would cause duplicate events with fsnotify
+ filterRedundant ws =
+ let dirs = Map.fromList [(dir, files) | WatchedDir dir files <- ws]
+ isCovered (WatchedFile path) =
+ let dir = takeDirectory path
+ file = takeFileName path
+ in -- Don't filter if filename is empty (edge case for root paths)
+ file /= "" && case Map.lookup dir dirs of
+ Just files -> Set.member file files
+ Nothing -> False
+ isCovered _ = False
+ in filter (not . isCovered) ws
+
+watchedPath :: WatchedPath -> FilePath
watchedPath (WatchedFile path) = path
watchedPath (WatchedDir dir _) = dir
-watchPathEvents :: WatchedPath -> [EventVariety]
-watchPathEvents (WatchedFile _) = [CloseWrite]
-watchPathEvents (WatchedDir _ _) = [MoveIn, Create]
-
-- Since we are watching a filesystem path, and not an inode, we need to replace a
-- file watch when the file gets overwritten.
-- This type is a map of paths to watches used to keep track of both file and
-- directory watches as they get deleted and recreated.
-type Watches = Map RawFilePath (WatchDescriptor, WatchedPath)
+type Watches = Map FilePath (StopListening, WatchedPath)
runSemDefault :: Logger -> Sem '[TinyLog, Embed IO, Final IO] a -> IO a
runSemDefault logger = Polysemy.runFinal . Polysemy.embedToFinal . Log.loggerToTinyLog logger
@@ -120,13 +110,14 @@ delMonitor monitor = Polysemy.resourceToIOFinal
$ do
watches <- readIORef (monWatches monitor)
traverse_ stop watches
+ embed $ stopManager (monWatchManager monitor)
where
- stop (wd, _) = do
+ stop (stopListening, wpath) = do
-- ignore exceptions when removing watches
- embed . void . try @IOException $ removeWatch wd
+ embed . void . try @IOException $ stopListening
Log.trace $
Log.msg ("stopped watching file" :: Text)
- . Log.field "descriptor" (show wd)
+ . Log.field "path" (watchedPath wpath)
mkMonitor ::
( Member TinyLog r,
@@ -140,17 +131,16 @@ mkMonitor ::
RunSettings ->
Sem r Monitor
mkMonitor runSem onNewContext rs = do
- inotify <- embed initINotify
+ mgr <- embed startManager
Log.trace $
- Log.msg ("inotify initialized" :: Text)
- . Log.field "inotify" (show inotify)
+ Log.msg ("fsnotify watch manager initialized" :: Text)
lock <- embed @IO $ newMVar ()
watchesVar <- embed @IO $ newIORef mempty
let monitor =
Monitor
- { monINotify = inotify,
+ { monWatchManager = mgr,
monOnNewContext = onNewContext,
monWatches = watchesVar,
monSettings = rs,
@@ -162,7 +152,7 @@ mkMonitor runSem onNewContext rs = do
traverse_ (addWatchedFile monitor) (toList paths)
pure monitor
-data Action = ReplaceWatch RawFilePath | ReloadSettings
+data MonitorAction = ReplaceWatch FilePath | ReloadSettings
deriving (Eq, Ord, Show)
handleEvent ::
@@ -189,13 +179,25 @@ handleEvent runSem monitor wpath e = do
-- reloaded, otherwise there is a window of time (after reloading settings,
-- but before the new watch is set) where changes to the settings can go
-- undetected
-getActions :: WatchedPath -> Event -> [Action]
-getActions (WatchedFile path) (Closed _ mpath True)
- | maybe True (== path) mpath = [ReloadSettings]
-getActions (WatchedDir dir paths) (MovedIn _ path _)
- | Set.member path paths = [ReplaceWatch (dir <> "/" <> path), ReloadSettings]
-getActions (WatchedDir dir paths) (Created _ path)
- | Set.member path paths = [ReplaceWatch (dir <> "/" <> path), ReloadSettings]
+--
+-- We use CloseWrite events (Linux-only) instead of Modified events for
+-- triggering reloads. CloseWrite is generated when a file is closed after
+-- being opened for writing, which reliably indicates the write is complete.
+-- Modified events may fire multiple times during a write operation (before
+-- the file is fully written) and would cause duplicate reloads.
+--
+-- On macOS (FSEvents), CloseWrite is not available, but the tests are
+-- disabled there anyway (fsnotify tests don't work in nix sandbox on macOS).
+getActions :: WatchedPath -> Event -> [MonitorAction]
+getActions (WatchedFile path) (CloseWrite filePath _ _)
+ | filePath == path = [ReloadSettings]
+getActions (WatchedFile path) (Added filePath _ _)
+ | filePath == path = [ReplaceWatch path, ReloadSettings]
+getActions (WatchedDir _dir paths) (Added filePath _ _)
+ | Set.member (takeFileName filePath) paths =
+ [ReplaceWatch filePath, ReloadSettings]
+getActions (WatchedDir _dir paths) (CloseWrite filePath _ _)
+ | Set.member (takeFileName filePath) paths = [ReloadSettings]
getActions _ _ = []
applyAction ::
@@ -204,7 +206,7 @@ applyAction ::
Member (Polysemy.Error FederationSetupError) r
) =>
Monitor ->
- Action ->
+ MonitorAction ->
Sem r ()
applyAction monitor ReloadSettings = do
sslCtx' <- mkSSLContext (monSettings monitor)
@@ -218,7 +220,7 @@ applyAction monitor (ReplaceWatch path) = do
addWatchedFile monitor wpath
case wpath of
WatchedDir dir paths ->
- traverse_ (applyAction monitor . ReplaceWatch . ((dir <> "/") <>)) paths
+ traverse_ (applyAction monitor . ReplaceWatch . (dir >)) (Set.toList paths)
WatchedFile _ -> pure ()
addWatchedFile ::
@@ -232,17 +234,15 @@ addWatchedFile monitor wpath = do
r <-
embed . try @SomeException $
addWatchAndSave
- (monINotify monitor)
- (watchPathEvents wpath)
+ (monWatchManager monitor)
(monWatches monitor)
wpath
(monHandler monitor wpath)
- let pathText = Text.decodeUtf8With Text.lenientDecode (watchedPath wpath)
+ let pathText = Text.pack (watchedPath wpath)
case r of
- Right w ->
+ Right _ ->
Log.trace $
Log.msg ("watching file" :: Text)
- . Log.field "descriptor" (show w)
. Log.field "path" pathText
Left e -> do
Log.err $
@@ -251,25 +251,31 @@ addWatchedFile monitor wpath = do
. Log.field "error" (displayException e)
addWatchAndSave ::
- INotify ->
- [EventVariety] ->
+ WatchManager ->
IORef Watches ->
WatchedPath ->
(Event -> IO ()) ->
- IO WatchDescriptor
-addWatchAndSave inotify events watchesVar wpath handler = do
+ IO ()
+addWatchAndSave mgr watchesVar wpath handler = do
let path = watchedPath wpath
+ -- For files, watch the parent directory; for directories, watch the directory itself
+ dirToWatch = case wpath of
+ WatchedFile fp -> takeDirectory fp
+ WatchedDir dir _ -> dir
+ -- Create filter predicate based on what we're watching
+ predicate = case wpath of
+ WatchedFile fp -> \event -> eventPath event == fp
+ WatchedDir _ files -> \event -> Set.member (takeFileName (eventPath event)) files
-- create a new watch
- w' <- addWatch inotify events path handler
+ stopListening <- watchDir mgr dirToWatch predicate handler
-- atomically save it in the map, and return the old one
- mw <-
+ mOld <-
atomicModifyIORef watchesVar $
- swap . Map.alterF (,Just (w', wpath)) path
+ swap . Map.alterF (,Just (stopListening, wpath)) path
-- remove the old watch
- case mw of
+ case mOld of
Nothing -> pure ()
- Just (w, _) -> void . try @IOException $ removeWatch w
- pure w'
+ Just (oldStopListening, _) -> void . try @IOException $ oldStopListening
certificatePaths :: RunSettings -> [FilePath]
certificatePaths rs =
@@ -300,9 +306,8 @@ resolveSymlink path' = do
watchedPaths :: (FilePath -> IO (Maybe FilePath)) -> FilePath -> IO [WatchedPath]
watchedPaths resolve path' = do
path <- makeAbsolute path'
- rpath <- rawPath path
dirs <- watchedDirs resolve path
- pure $ WatchedFile rpath : dirs
+ pure $ WatchedFile path : dirs
watchedDirs :: (FilePath -> IO (Maybe FilePath)) -> FilePath -> IO [WatchedPath]
watchedDirs resolve path = do
@@ -313,9 +318,8 @@ watchedDirs resolve path = do
then pure [] -- base case: root directory
else do
wds <- watchedDirs resolve dir
- rdir <- rawPath (dropTrailingPathSeparator dir)
- rbase <- rawPath base
- pure $ WatchedDir rdir (Set.singleton rbase) : wds
+ let normalizedDir = dropTrailingPathSeparator dir
+ pure $ WatchedDir normalizedDir (Set.singleton base) : wds
pure (dirs0 ++ dirs1)
data FederationSetupError
diff --git a/services/federator/test/unit/Test/Federator/Monitor.hs b/services/federator/test/unit/Test/Federator/Monitor.hs
index 6630080f29..c9ca645b18 100644
--- a/services/federator/test/unit/Test/Federator/Monitor.hs
+++ b/services/federator/test/unit/Test/Federator/Monitor.hs
@@ -20,19 +20,18 @@ module Test.Federator.Monitor (tests) where
import Control.Concurrent.Chan
import Control.Exception (bracket)
import Control.Monad.Trans.Cont
-import Data.ByteString qualified as BS
-import Data.ByteString.Char8 qualified as B8
import Data.Set qualified as Set
import Federator.Monitor
import Federator.Monitor.Internal
import Federator.Options
-import Imports
+import Imports hiding (getCurrentDirectory, makeAbsolute)
import OpenSSL.Session (SSLContext)
import Polysemy qualified
import Polysemy.Error qualified as Polysemy
+import System.Directory (getCurrentDirectory, makeAbsolute)
import System.FilePath
import System.IO.Temp
-import System.Posix (createSymbolicLink, getWorkingDirectory)
+import System.Posix (createSymbolicLink)
import System.Timeout
import Test.Federator.Options (defRunSettings)
import Test.Tasty
@@ -218,7 +217,7 @@ testMonitorSymlinkUpdate =
settings <- withSymlinkSettings
_ <- withSilentMonitor reloads settings
liftIO $ do
- wd <- getWorkingDirectory
+ wd <- getCurrentDirectory
removeFile (clientCertificate settings)
createSymbolicLink
@@ -374,32 +373,26 @@ testMergeWatchedPaths =
in mergedCount <= origCount,
testProperty "has the same paths" $ \(wpaths :: [WatchedPath]) ->
let f (WatchedFile path) = [path]
- f (WatchedDir dir files) = map (dir <>) (Set.toList files)
+ f (WatchedDir dir files) = map (dir >) (Set.toList files)
mergedPaths = Set.fromList (Set.toList (mergePaths wpaths) >>= f)
origPaths = Set.fromList (wpaths >>= f)
in mergedPaths == origPaths
]
-newtype Path = Path {getRawPath :: ByteString}
-
-getPath :: Path -> IO FilePath
-getPath = fromRawPath . getRawPath
+-- Wrapper for testing path operations
+newtype Path = Path {getPath :: FilePath}
+ deriving (Show)
getAbsolutePath :: Path -> IO FilePath
-getAbsolutePath p = do
- path <- getPath p
- makeAbsolute ("/" <> path)
-
-instance Show Path where
- show = show . getRawPath
+getAbsolutePath p = makeAbsolute ("/" <> getPath p)
instance Arbitrary Path where
arbitrary =
- Path . B8.intercalate "/"
- <$> listOf (BS.pack <$> listOf1 ch)
+ Path . intercalate "/"
+ <$> listOf (listOf1 ch)
where
- ch :: Gen Word8
- ch = arbitrary `suchThat` (/= fromIntegral (ord '/'))
+ ch :: Gen Char
+ ch = arbitrary `suchThat` (/= '/')
trivialResolve :: FilePath -> IO (Maybe FilePath)
trivialResolve _ = pure Nothing
@@ -415,8 +408,8 @@ testDirectoryTraversal =
pure (length wpaths == length (splitPath path)),
testProperty "relative paths are resolved correctly" $
\(path' :: Path) -> ioProperty $ do
- dir <- getWorkingDirectory
- path <- getPath path'
+ dir <- getCurrentDirectory
+ let path = getPath path'
wpaths <- watchedPaths trivialResolve path
wpaths' <- watchedPaths trivialResolve (dir > path)
pure $ wpaths == wpaths',
@@ -424,10 +417,10 @@ testDirectoryTraversal =
evalContT $ do
settings <- withKubernetesSettings
liftIO $ do
- rroot <- rawPath $ takeDirectory (clientCertificate settings)
+ let root = takeDirectory (clientCertificate settings)
wpaths <- mergePaths <$> watchedPaths resolveSymlink (clientCertificate settings)
assertBool "symlink targets should be watched" $
Set.member
- (WatchedDir rroot (Set.fromList ["cert.pem", "..data", "..foo"]))
+ (WatchedDir root (Set.fromList ["cert.pem", "..data", "..foo"]))
wpaths
]
From 15d3d24c0735a39c30129c08534fb1800e133cb7 Mon Sep 17 00:00:00 2001
From: Matthias Fischmann
Date: Mon, 19 Jan 2026 12:47:07 +0100
Subject: [PATCH 12/26] Fixup charts.
---
.../elasticsearch-index/templates/create-index.yaml | 10 ++++++++++
.../elasticsearch-index/templates/migrate-data.yaml | 11 +++++++++++
2 files changed, 21 insertions(+)
diff --git a/charts/elasticsearch-index/templates/create-index.yaml b/charts/elasticsearch-index/templates/create-index.yaml
index 225ecf82c9..7c02bb93f5 100644
--- a/charts/elasticsearch-index/templates/create-index.yaml
+++ b/charts/elasticsearch-index/templates/create-index.yaml
@@ -35,6 +35,16 @@ spec:
{{- end }}
{{- end }}
initContainers:
+ - name: wait-for-brig
+ image: busybox
+ command:
+ - sh
+ - -c
+ - |
+ until kubectl get configmap brig && kubectl get secret brig; do
+ echo "Waiting for brig...";
+ sleep 5;
+ done
# Creates index in elasticsearch only when it doesn't exist.
# Does nothing if the index exists.
- name: brig-index-create
diff --git a/charts/elasticsearch-index/templates/migrate-data.yaml b/charts/elasticsearch-index/templates/migrate-data.yaml
index 3bf41e02a6..9a4e121b96 100644
--- a/charts/elasticsearch-index/templates/migrate-data.yaml
+++ b/charts/elasticsearch-index/templates/migrate-data.yaml
@@ -21,6 +21,17 @@ spec:
chart: "{{.Chart.Name}}-{{.Chart.Version}}"
spec:
restartPolicy: OnFailure
+ initContainers:
+ - name: wait-for-brig
+ image: busybox
+ command:
+ - sh
+ - -c
+ - |
+ until kubectl get configmap brig && kubectl get secret brig; do
+ echo "Waiting for brig...";
+ sleep 5;
+ done
containers:
# Reindexes all users when a new migration is detected.
- name: brig-index
From c2324fd4e1d54a35314926f1557502bd7275089f Mon Sep 17 00:00:00 2001
From: Matthias Fischmann
Date: Mon, 19 Jan 2026 14:05:48 +0100
Subject: [PATCH 13/26] Revert "Fixup charts."
This reverts commit 15d3d24c0735a39c30129c08534fb1800e133cb7.
(I accidentally committed to the wrong branch, sorry!)
---
.../elasticsearch-index/templates/create-index.yaml | 10 ----------
.../elasticsearch-index/templates/migrate-data.yaml | 11 -----------
2 files changed, 21 deletions(-)
diff --git a/charts/elasticsearch-index/templates/create-index.yaml b/charts/elasticsearch-index/templates/create-index.yaml
index 7c02bb93f5..225ecf82c9 100644
--- a/charts/elasticsearch-index/templates/create-index.yaml
+++ b/charts/elasticsearch-index/templates/create-index.yaml
@@ -35,16 +35,6 @@ spec:
{{- end }}
{{- end }}
initContainers:
- - name: wait-for-brig
- image: busybox
- command:
- - sh
- - -c
- - |
- until kubectl get configmap brig && kubectl get secret brig; do
- echo "Waiting for brig...";
- sleep 5;
- done
# Creates index in elasticsearch only when it doesn't exist.
# Does nothing if the index exists.
- name: brig-index-create
diff --git a/charts/elasticsearch-index/templates/migrate-data.yaml b/charts/elasticsearch-index/templates/migrate-data.yaml
index 9a4e121b96..3bf41e02a6 100644
--- a/charts/elasticsearch-index/templates/migrate-data.yaml
+++ b/charts/elasticsearch-index/templates/migrate-data.yaml
@@ -21,17 +21,6 @@ spec:
chart: "{{.Chart.Name}}-{{.Chart.Version}}"
spec:
restartPolicy: OnFailure
- initContainers:
- - name: wait-for-brig
- image: busybox
- command:
- - sh
- - -c
- - |
- until kubectl get configmap brig && kubectl get secret brig; do
- echo "Waiting for brig...";
- sleep 5;
- done
containers:
# Reindexes all users when a new migration is detected.
- name: brig-index
From e47e82cf28ff49c028176f9f75779c65918ff73a Mon Sep 17 00:00:00 2001
From: Leif Battermann
Date: Mon, 19 Jan 2026 14:23:51 +0100
Subject: [PATCH 14/26] Use one ENV var for all psql/c* data (#4963)
---
hack/helm_vars/common.yaml.gotmpl | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/hack/helm_vars/common.yaml.gotmpl b/hack/helm_vars/common.yaml.gotmpl
index 083e689e10..f9878f508b 100644
--- a/hack/helm_vars/common.yaml.gotmpl
+++ b/hack/helm_vars/common.yaml.gotmpl
@@ -13,10 +13,10 @@ dynBackendDomain1: dynamic-backend-1.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster
dynBackendDomain2: dynamic-backend-2.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster.local
dynBackendDomain3: dynamic-backend-3.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster.local
-{{- if (eq (env "CONVERSATION_STORE") "") }}
+{{- if (eq (env "PREFERRED_STORE") "") }}
conversationStore: cassandra
{{- else }}
-conversationStore: {{ env "CONVERSATION_STORE" }}
+conversationStore: {{ env "PREFERRED_STORE" }}
{{- end }}
{{- if (eq (env "UPLOAD_XML_S3_BASE_URL") "") }}
@@ -26,4 +26,4 @@ uploadXml:
awsAccessKeyId: {{ env "UPLOAD_XML_AWS_ACCESS_KEY_ID" }}
awsSecretAccessKey: {{ env "UPLOAD_XML_AWS_SECRET_ACCESS_KEY" }}
baseUrl: {{ env "UPLOAD_XML_S3_BASE_URL" }}
-{{- end }}
\ No newline at end of file
+{{- end }}
From 6ff57d4a2e4be262eca5b5e5ea0331da668b7c55 Mon Sep 17 00:00:00 2001
From: Leif Battermann
Date: Tue, 20 Jan 2026 07:43:22 +0100
Subject: [PATCH 15/26] WPB-22901 [fix] background-worker default postgres
settings (#4965)
---
changelog.d/0-release-notes/WPB-22901 | 1 +
changelog.d/3-bug-fixes/WPB-22901 | 1 +
charts/background-worker/values.yaml | 2 +-
3 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 changelog.d/0-release-notes/WPB-22901
create mode 100644 changelog.d/3-bug-fixes/WPB-22901
diff --git a/changelog.d/0-release-notes/WPB-22901 b/changelog.d/0-release-notes/WPB-22901
new file mode 100644
index 0000000000..4dadea8fac
--- /dev/null
+++ b/changelog.d/0-release-notes/WPB-22901
@@ -0,0 +1 @@
+The background-worker defaults for the postgres migration now match galley and point to cassandra (previously postgres). This currenlty only affects the background job, which is not expected to run before postgres is in use. However, if you relied on the defaults after migrating to postgres, please update your config to keep using postgres.
diff --git a/changelog.d/3-bug-fixes/WPB-22901 b/changelog.d/3-bug-fixes/WPB-22901
new file mode 100644
index 0000000000..5ec4443ea5
--- /dev/null
+++ b/changelog.d/3-bug-fixes/WPB-22901
@@ -0,0 +1 @@
+`background-worker`'s default settings for `postgresMigration` have been correctly set to `cassandra`.
diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml
index 57f3ce0070..5b729f1472 100644
--- a/charts/background-worker/values.yaml
+++ b/charts/background-worker/values.yaml
@@ -86,7 +86,7 @@ config:
# Controls where conversation data is stored/accessed
postgresMigration:
- conversation: postgresql
+ conversation: cassandra
secrets:
{}
From 78be024d2f24f433bfecd226e383c39b8f759a0d Mon Sep 17 00:00:00 2001
From: Jan Schumacher <155645800+jschumacher-wire@users.noreply.github.com>
Date: Tue, 20 Jan 2026 10:02:06 +0100
Subject: [PATCH 16/26] ingress-nginx chart: add seperate issuer for federator
(#4964)
---
changelog.d/2-features/federator-tls-issuer | 1 +
.../templates/certificate_federator.yaml | 5 +++++
charts/nginx-ingress-services/values.yaml | 3 +++
3 files changed, 9 insertions(+)
create mode 100644 changelog.d/2-features/federator-tls-issuer
diff --git a/changelog.d/2-features/federator-tls-issuer b/changelog.d/2-features/federator-tls-issuer
new file mode 100644
index 0000000000..759d6a0125
--- /dev/null
+++ b/changelog.d/2-features/federator-tls-issuer
@@ -0,0 +1 @@
+nginx-ingress-services: Add `federator.tls.issuer` option to use a separate ClusterIssuer for federation mTLS certificates.
diff --git a/charts/nginx-ingress-services/templates/certificate_federator.yaml b/charts/nginx-ingress-services/templates/certificate_federator.yaml
index 1361ea386b..59d06d5627 100644
--- a/charts/nginx-ingress-services/templates/certificate_federator.yaml
+++ b/charts/nginx-ingress-services/templates/certificate_federator.yaml
@@ -16,8 +16,13 @@ metadata:
heritage: "{{ .Release.Service }}"
spec:
issuerRef:
+ {{- if .Values.federator.tls.issuer.name }}
+ name: {{ .Values.federator.tls.issuer.name | quote }}
+ kind: {{ .Values.federator.tls.issuer.kind | default .Values.tls.issuer.kind }}
+ {{- else }}
name: {{ include "nginx-ingress-services.getIssuerName" . | quote }}
kind: {{ .Values.tls.issuer.kind }}
+ {{- end }}
usages:
- server auth
- client auth
diff --git a/charts/nginx-ingress-services/values.yaml b/charts/nginx-ingress-services/values.yaml
index 8dc8608bb5..8870f71af9 100644
--- a/charts/nginx-ingress-services/values.yaml
+++ b/charts/nginx-ingress-services/values.yaml
@@ -20,6 +20,9 @@ federator:
# rotationPolicy: Always (default) regenerates key on each renewal
# rotationPolicy: Never preserves key across renewals (for key pinning)
rotationPolicy: Always
+ # Issuer for federator certificate (for mTLS with Client Auth EKU).
+ # If not set, uses global tls.issuer configuration.
+ issuer: {}
# If you want to use TLS termination on the ingress,
# then set this variable to true and ensure that there
# is a valid wildcard TLS certificate
From 8b87476a2f70926db69aa9b7b14ee7c3ff66877c Mon Sep 17 00:00:00 2001
From: Paolo Capriotti
Date: Tue, 20 Jan 2026 12:12:51 +0100
Subject: [PATCH 17/26] Application message in commit bundles (#4929)
* Add application message to commit bundles
* Extract app message validation
* Validate and send bundle app messages
* Test application messages in bundles
* Wrap ClientMap in a newtype
* Send extra application message to new clients
* Add CHANGELOG entry
* Fix commit bundle roundtrip tests
* Fix warnings
* Send application message to new conversation members
---
.../2-features/mls-bundle-extra-messages | 1 +
integration/integration.cabal | 1 +
integration/test/MLS/Util.hs | 32 +++++++----
integration/test/Test/MLS/History.hs | 57 +++++++++++++++++++
.../wire-api/src/Wire/API/MLS/CommitBundle.hs | 20 ++++---
libs/wire-api/src/Wire/API/MLS/Message.hs | 33 ++++++++---
.../test/unit/Test/Wire/API/Roundtrip/MLS.hs | 7 ++-
.../src/Wire/ConversationStore/MLS/Types.hs | 52 +++++++++++------
services/galley/src/Galley/API/Internal.hs | 3 +-
.../galley/src/Galley/API/MLS/CheckClients.hs | 6 +-
.../Galley/API/MLS/Commit/InternalCommit.hs | 8 +--
.../src/Galley/API/MLS/IncomingMessage.hs | 2 +
services/galley/src/Galley/API/MLS/Message.hs | 57 +++++++++++++++----
.../galley/src/Galley/API/MLS/OutOfSync.hs | 2 +-
.../galley/src/Galley/API/MLS/Propagate.hs | 9 +--
.../galley/src/Galley/API/MLS/Proposal.hs | 4 +-
services/galley/src/Galley/API/MLS/Removal.hs | 3 +-
.../src/Galley/API/MLS/SubConversation.hs | 3 +-
.../galley/test/integration/API/MLS/Util.hs | 2 +-
19 files changed, 225 insertions(+), 77 deletions(-)
create mode 100644 changelog.d/2-features/mls-bundle-extra-messages
create mode 100644 integration/test/Test/MLS/History.hs
diff --git a/changelog.d/2-features/mls-bundle-extra-messages b/changelog.d/2-features/mls-bundle-extra-messages
new file mode 100644
index 0000000000..ea51d5af86
--- /dev/null
+++ b/changelog.d/2-features/mls-bundle-extra-messages
@@ -0,0 +1 @@
+Allow commit bundles to contain one application message. The message must be for the epoch *after* the commit, and it gets sent after the commit has been accepted.
diff --git a/integration/integration.cabal b/integration/integration.cabal
index 555ca6a7f1..8bdbe597e0 100644
--- a/integration/integration.cabal
+++ b/integration/integration.cabal
@@ -176,6 +176,7 @@ library
Test.MessageTimer
Test.MLS
Test.MLS.Clients
+ Test.MLS.History
Test.MLS.KeyPackage
Test.MLS.Keys
Test.MLS.Message
diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs
index 54971aa8e1..3ecc464c51 100644
--- a/integration/test/MLS/Util.hs
+++ b/integration/test/MLS/Util.hs
@@ -74,7 +74,8 @@ data MessagePackage = MessagePackage
convId :: ConvId,
message :: ByteString,
welcome :: Maybe ByteString,
- groupInfo :: Maybe ByteString
+ groupInfo :: Maybe ByteString,
+ appMessage :: Maybe ByteString
}
toRandomFile :: ByteString -> App FilePath
@@ -445,7 +446,8 @@ createAddCommitWithKeyPackages cid convId clientsAndKeyPackages = do
convId = convId,
message = commit,
welcome = Just welcome,
- groupInfo = Just gi
+ groupInfo = Just gi,
+ appMessage = Nothing
}
createRemoveCommit :: (HasCallStack) => ClientIdentity -> ConvId -> [ClientIdentity] -> App MessagePackage
@@ -502,7 +504,8 @@ createRemoveCommit cid convId targets = do
convId = convId,
message = commit,
welcome = Just welcome,
- groupInfo = Just gi
+ groupInfo = Just gi,
+ appMessage = Nothing
}
createAddProposals :: (HasCallStack) => ConvId -> ClientIdentity -> [Value] -> App [MessagePackage]
@@ -528,7 +531,8 @@ createReInitProposal convId cid = do
convId = convId,
message = prop,
welcome = Nothing,
- groupInfo = Nothing
+ groupInfo = Nothing,
+ appMessage = Nothing
}
createAddProposalWithKeyPackage ::
@@ -551,7 +555,8 @@ createAddProposalWithKeyPackage convId cid (_, kp) = do
convId = convId,
message = prop,
welcome = Nothing,
- groupInfo = Nothing
+ groupInfo = Nothing,
+ appMessage = Nothing
}
createPendingProposalCommit :: (HasCallStack) => ConvId -> ClientIdentity -> App MessagePackage
@@ -585,7 +590,8 @@ createPendingProposalCommit convId cid = do
convId = convId,
message = commit,
welcome = welcome,
- groupInfo = Just pgs
+ groupInfo = Just pgs,
+ appMessage = Nothing
}
createExternalCommit ::
@@ -630,7 +636,8 @@ createExternalCommit convId cid mgi = do
convId = convId,
message = commit,
welcome = Nothing,
- groupInfo = Just newPgs
+ groupInfo = Just newPgs,
+ appMessage = Nothing
}
data MLSNotificationTag = MLSNotificationMessageTag | MLSNotificationWelcomeTag
@@ -721,7 +728,7 @@ consumeMessageNoExternal cs cid mp = consumeMessageWithPredicate isNewMLSMessage
else pure False
mlsCliConsume :: (HasCallStack) => ConvId -> Ciphersuite -> ClientIdentity -> ByteString -> App ByteString
-mlsCliConsume convId cs cid msgData =
+mlsCliConsume convId cs cid msgData = do
mlscli
(Just convId)
cs
@@ -834,7 +841,11 @@ readWelcome fp = runMaybeT $ do
liftIO $ BS.readFile fp
mkBundle :: MessagePackage -> ByteString
-mkBundle mp = mp.message <> foldMap mkGroupInfoMessage mp.groupInfo <> fold mp.welcome
+mkBundle mp =
+ mp.message
+ <> foldMap mkGroupInfoMessage mp.groupInfo
+ <> fold mp.welcome
+ <> fold mp.appMessage
mkGroupInfoMessage :: ByteString -> ByteString
mkGroupInfoMessage gi = BS.pack [0x00, 0x01, 0x00, 0x04] <> gi
@@ -913,7 +924,8 @@ createApplicationMessage convId cid messageContent = do
convId = convId,
message = message,
welcome = Nothing,
- groupInfo = Nothing
+ groupInfo = Nothing,
+ appMessage = Nothing
}
leaveConv ::
diff --git a/integration/test/Test/MLS/History.hs b/integration/test/Test/MLS/History.hs
new file mode 100644
index 0000000000..36dea41908
--- /dev/null
+++ b/integration/test/Test/MLS/History.hs
@@ -0,0 +1,57 @@
+-- This file is part of the Wire Server implementation.
+--
+-- Copyright (C) 2025 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.MLS.History where
+
+import qualified Data.ByteString.Base64 as Base64
+import qualified Data.Text.Encoding as T
+import MLS.Util
+import Notifications
+import SetupHelpers
+import Testlib.Prelude
+
+testExtraAppMessage :: App ()
+testExtraAppMessage = do
+ [alice, bob, charlie] <- createAndConnectUsers (replicate 3 OwnDomain)
+ [alice1, bob1, charlie1] <- traverse (createMLSClient def) [alice, bob, charlie]
+ traverse_ (uploadNewKeyPackage def) [bob1, charlie1]
+ convId <- createNewGroup def alice1
+
+ -- normal commit
+ void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle
+
+ -- make a commit with an extra application message
+ mp <- createAddCommit alice1 convId [charlie]
+ appPackage <- createApplicationMessage convId alice1 "hello"
+ let mp' = mp {appMessage = Just appPackage.message}
+
+ withWebSockets [bob1, charlie1] $ \wss -> do
+ void $ sendAndConsumeCommitBundle mp'
+
+ let isAppMessage :: Value -> App Bool
+ isAppMessage n =
+ isNewMLSMessageNotif n
+ &&~ isNotifConvId mp.convId n
+ &&~ ( do
+ msg <- n %. "payload.0.data" & asByteString >>= showMessage def alice1
+ ty <- msg %. "type" & asString
+ pure $ ty == "private_message"
+ )
+
+ for_ wss $ \ws -> do
+ n <- awaitMatch isAppMessage ws
+ nPayload n %. "data" `shouldMatch` T.decodeUtf8 (Base64.encode appPackage.message)
diff --git a/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs b/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs
index 2307f8ac38..699321022d 100644
--- a/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs
+++ b/libs/wire-api/src/Wire/API/MLS/CommitBundle.hs
@@ -29,14 +29,16 @@ import Wire.API.MLS.Welcome
data CommitBundle = CommitBundle
{ commitMsg :: RawMLS Message,
welcome :: Maybe (RawMLS Welcome),
- groupInfo :: RawMLS GroupInfo
+ groupInfo :: RawMLS GroupInfo,
+ appMessage :: Maybe (RawMLS Message)
}
deriving stock (Eq, Show, Generic)
data CommitBundleF f = CommitBundleF
{ commitMsg :: f (RawMLS Message),
welcome :: f (RawMLS Welcome),
- groupInfo :: f (RawMLS GroupInfo)
+ groupInfo :: f (RawMLS GroupInfo),
+ appMessage :: f (RawMLS Message)
}
deriving instance Show (CommitBundleF [])
@@ -47,9 +49,10 @@ instance (Alternative f) => Semigroup (CommitBundleF f) where
(cb1.commitMsg <|> cb2.commitMsg)
(cb1.welcome <|> cb2.welcome)
(cb1.groupInfo <|> cb2.groupInfo)
+ (cb1.appMessage <|> cb2.appMessage)
instance (Alternative f) => Monoid (CommitBundleF f) where
- mempty = CommitBundleF empty empty empty
+ mempty = CommitBundleF empty empty empty empty
checkCommitBundleF :: CommitBundleF [] -> Either Text CommitBundle
checkCommitBundleF cb =
@@ -57,6 +60,7 @@ checkCommitBundleF cb =
<$> check "commit" cb.commitMsg
<*> checkOpt "welcome" cb.welcome
<*> check "group info" cb.groupInfo
+ <*> checkOpt "application message" cb.appMessage
where
check :: Text -> [a] -> Either Text a
check _ [x] = pure x
@@ -71,10 +75,11 @@ checkCommitBundleF cb =
findMessageInStream :: (Alternative f) => RawMLS Message -> Either Text (CommitBundleF f)
findMessageInStream msg = case msg.value.content of
MessagePublic mp -> case mp.content.value.content of
- FramedContentCommit _ -> pure (CommitBundleF (pure msg) empty empty)
- _ -> Left "unexpected public message"
- MessageWelcome w -> pure (CommitBundleF empty (pure w) empty)
- MessageGroupInfo gi -> pure (CommitBundleF empty empty (pure gi))
+ FramedContentCommit _ -> pure (CommitBundleF (pure msg) empty empty empty)
+ _ -> Left "unexpected proposal"
+ MessageWelcome w -> pure (CommitBundleF empty (pure w) empty empty)
+ MessageGroupInfo gi -> pure (CommitBundleF empty empty (pure gi) empty)
+ MessagePrivate _ -> pure (CommitBundleF empty empty empty (pure msg))
_ -> Left "unexpected message type"
findMessagesInStream :: (Alternative f) => [RawMLS Message] -> Either Text (CommitBundleF f)
@@ -91,6 +96,7 @@ instance SerialiseMLS CommitBundle where
serialiseMLS cb.commitMsg
traverse_ (serialiseMLS . mkMessage . MessageWelcome) cb.welcome
serialiseMLS $ mkMessage (MessageGroupInfo cb.groupInfo)
+ traverse_ serialiseMLS cb.appMessage
instance S.ToSchema CommitBundle where
declareNamedSchema _ = pure (mlsSwagger "CommitBundle")
diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs
index cb1003ab8f..644e2743d4 100644
--- a/libs/wire-api/src/Wire/API/MLS/Message.hs
+++ b/libs/wire-api/src/Wire/API/MLS/Message.hs
@@ -76,7 +76,8 @@ data Message = Message
{ protocolVersion :: ProtocolVersion,
content :: MessageContent
}
- deriving (Eq, Show)
+ deriving (Eq, Show, Generic)
+ deriving (Arbitrary) via GenericUniform Message
mkMessage :: MessageContent -> Message
mkMessage = Message defaultProtocolVersion
@@ -102,7 +103,8 @@ data MessageContent
| MessageWelcome (RawMLS Welcome)
| MessageGroupInfo (RawMLS GroupInfo)
| MessageKeyPackage (RawMLS KeyPackage)
- deriving (Eq, Show)
+ deriving (Eq, Show, Generic)
+ deriving (Arbitrary) via GenericUniform MessageContent
instance HasField "wireFormat" MessageContent WireFormatTag where
getField (MessagePrivate _) = WireFormatPrivateTag
@@ -148,7 +150,8 @@ data PublicMessage = PublicMessage
-- https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6.2-4
membershipTag :: Maybe ByteString
}
- deriving (Eq, Show)
+ deriving (Eq, Show, Generic)
+ deriving (Arbitrary) via GenericUniform PublicMessage
instance ParseMLS PublicMessage where
parseMLS = do
@@ -179,7 +182,8 @@ data PrivateMessage = PrivateMessage
encryptedSenderData :: ByteString,
ciphertext :: ByteString
}
- deriving (Eq, Show)
+ deriving (Eq, Show, Generic)
+ deriving (Arbitrary) via (GenericUniform PrivateMessage)
instance ParseMLS PrivateMessage where
parseMLS =
@@ -191,6 +195,15 @@ instance ParseMLS PrivateMessage where
<*> parseMLSBytes @VarInt
<*> parseMLSBytes @VarInt
+instance SerialiseMLS PrivateMessage where
+ serialiseMLS msg = do
+ serialiseMLS msg.groupId
+ serialiseMLS msg.epoch
+ serialiseMLS msg.tag
+ serialiseMLSBytes @VarInt msg.authenticatedData
+ serialiseMLSBytes @VarInt msg.encryptedSenderData
+ serialiseMLSBytes @VarInt msg.ciphertext
+
-- | https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol-20/draft-ietf-mls-protocol.html#section-6-4
data SenderTag
= SenderMemberTag
@@ -242,7 +255,8 @@ data FramedContent = FramedContent
authenticatedData :: ByteString,
content :: FramedContentData
}
- deriving (Eq, Show)
+ deriving (Eq, Show, Generic)
+ deriving (Arbitrary) via GenericUniform FramedContent
instance ParseMLS FramedContent where
parseMLS =
@@ -265,7 +279,8 @@ data FramedContentDataTag
= FramedContentApplicationDataTag
| FramedContentProposalTag
| FramedContentCommitTag
- deriving (Enum, Bounded, Eq, Ord, Show)
+ deriving (Enum, Bounded, Eq, Ord, Show, Generic)
+ deriving (Arbitrary) via (GenericUniform FramedContentDataTag)
instance ParseMLS FramedContentDataTag where
parseMLS = parseMLSEnum @Word8 "ContentType"
@@ -278,7 +293,8 @@ data FramedContentData
= FramedContentApplicationData ByteString
| FramedContentProposal (RawMLS Proposal)
| FramedContentCommit (RawMLS Commit)
- deriving (Eq, Show)
+ deriving (Eq, Show, Generic)
+ deriving (Arbitrary) via GenericUniform FramedContentData
framedContentDataTag :: FramedContentData -> FramedContentDataTag
framedContentDataTag (FramedContentApplicationData _) = FramedContentApplicationDataTag
@@ -326,7 +342,8 @@ data FramedContentAuthData = FramedContentAuthData
-- Present iff it is part of a commit.
confirmationTag :: Maybe ByteString
}
- deriving (Eq, Show)
+ deriving (Eq, Show, Generic)
+ deriving (Arbitrary) via GenericUniform FramedContentAuthData
parseFramedContentAuthData :: FramedContentDataTag -> Get FramedContentAuthData
parseFramedContentAuthData t = do
diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs
index e68b24d671..6ce3e76f1c 100644
--- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs
+++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/MLS.hs
@@ -179,8 +179,11 @@ instance Arbitrary TestCommitBundle where
commitMsg <-
mkRawMLS . unMessageGenerator @(FramedContentGenerator Sender CommitPayload)
<$> arbitrary
- welcome <- arbitrary
- CommitBundle commitMsg welcome <$> arbitrary
+ appMsg <- arbitrary
+ CommitBundle commitMsg
+ <$> arbitrary
+ <*> arbitrary
+ <*> pure (mkRawMLS . mkMessage . MessagePrivate <$> appMsg)
newtype CommitPayload = CommitPayload {unCommitPayload :: RawMLS Commit}
deriving newtype (Arbitrary)
diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/MLS/Types.hs b/libs/wire-subsystems/src/Wire/ConversationStore/MLS/Types.hs
index b64eb45848..2573cfdd0f 100644
--- a/libs/wire-subsystems/src/Wire/ConversationStore/MLS/Types.hs
+++ b/libs/wire-subsystems/src/Wire/ConversationStore/MLS/Types.hs
@@ -109,7 +109,16 @@ imAssocs = IntMap.assocs . unIndexMap
-- Note that clients that are in the process of being removed from a group
-- (i.e. there is a pending remove proposals for them) are __not__ included in
-- this mapping.
-type ClientMap a = Map (Qualified UserId) (Map ClientId a)
+newtype ClientMap a = ClientMap
+ { unClientMap :: Map (Qualified UserId) (Map ClientId a)
+ }
+ deriving (Show, Eq, Functor)
+
+instance Semigroup (ClientMap a) where
+ ClientMap cm1 <> ClientMap cm2 = ClientMap $ Map.unionWith Map.union cm1 cm2
+
+instance Monoid (ClientMap a) where
+ mempty = ClientMap mempty
mkClientMap :: [(Domain, UserId, ClientId, Int32, Bool)] -> ClientMap LeafIndex
mkClientMap = foldr addEntry mempty
@@ -117,30 +126,40 @@ mkClientMap = foldr addEntry mempty
addEntry :: (Domain, UserId, ClientId, Int32, Bool) -> ClientMap LeafIndex -> ClientMap LeafIndex
addEntry (dom, usr, c, leafidx, pending_removal)
| pending_removal = id -- treat as removed, don't add to ClientMap
- | otherwise = Map.insertWith (<>) (Qualified usr dom) (Map.singleton c (fromIntegral leafidx))
+ | otherwise = ClientMap . Map.insertWith (<>) (Qualified usr dom) (Map.singleton c (fromIntegral leafidx)) . unClientMap
+
+cmNull :: ClientMap a -> Bool
+cmNull (ClientMap cm) = Map.null cm
cmToMap :: (Ord a) => ClientMap a -> Map a ClientIdentity
cmToMap = Map.fromList . map swap . cmAssocs
-cmLookupIndex :: ClientIdentity -> ClientMap LeafIndex -> Maybe LeafIndex
-cmLookupIndex cid cm = do
+cmLookupIndex :: ClientIdentity -> ClientMap a -> Maybe a
+cmLookupIndex cid (ClientMap cm) = do
clients <- Map.lookup (cidQualifiedUser cid) cm
Map.lookup (ciClient cid) clients
-cmRemoveClient :: ClientIdentity -> ClientMap LeafIndex -> ClientMap LeafIndex
-cmRemoveClient cid cm = case Map.lookup (cidQualifiedUser cid) cm of
- Nothing -> cm
- Just clients ->
- let clients' = Map.delete (ciClient cid) clients
- in if Map.null clients'
- then Map.delete (cidQualifiedUser cid) cm
- else Map.insert (cidQualifiedUser cid) clients' cm
+cmLookup :: Qualified UserId -> ClientMap a -> Maybe (Map ClientId a)
+cmLookup quid (ClientMap cm) = Map.lookup quid cm
+
+cmLookupClients :: Qualified UserId -> ClientMap a -> [ClientId]
+cmLookupClients quid (ClientMap cm) = foldMap Map.keys (Map.lookup quid cm)
+
+cmRemoveClient :: ClientIdentity -> ClientMap a -> ClientMap a
+cmRemoveClient cid (ClientMap cm) = ClientMap $
+ case Map.lookup (cidQualifiedUser cid) cm of
+ Nothing -> cm
+ Just clients ->
+ let clients' = Map.delete (ciClient cid) clients
+ in if Map.null clients'
+ then Map.delete (cidQualifiedUser cid) cm
+ else Map.insert (cidQualifiedUser cid) clients' cm
isClientMember :: ClientIdentity -> ClientMap LeafIndex -> Bool
isClientMember ci = isJust . cmLookupIndex ci
cmAssocs :: ClientMap a -> [(ClientIdentity, a)]
-cmAssocs cm = do
+cmAssocs (ClientMap cm) = do
(quid, clients) <- Map.assocs cm
(clientId, idx) <- Map.assocs clients
pure (mkClientIdentity quid clientId, idx)
@@ -150,9 +169,10 @@ cmIdentities = map fst . cmAssocs
cmSingleton :: ClientIdentity -> a -> ClientMap a
cmSingleton cid idx =
- Map.singleton
- (cidQualifiedUser cid)
- (Map.singleton (ciClient cid) idx)
+ ClientMap $
+ Map.singleton
+ (cidQualifiedUser cid)
+ (Map.singleton (ciClient cid) idx)
-- | Inform a handler for 'POST /conversations/list-ids' if the MLS global team
-- conversation and the MLS self-conversation should be included in the
diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs
index e8140aa492..85ef937747 100644
--- a/services/galley/src/Galley/API/Internal.hs
+++ b/services/galley/src/Galley/API/Internal.hs
@@ -91,6 +91,7 @@ import Wire.API.User.Client
import Wire.BackendNotificationQueueAccess
import Wire.ConversationStore
import Wire.ConversationStore qualified as E
+import Wire.ConversationStore.MLS.Types
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
import Wire.LegalHoldStore as LegalHoldStore
@@ -517,4 +518,4 @@ iGetMLSClientListForConv ::
Sem r ClientList
iGetMLSClientListForConv gid = do
cm <- E.lookupMLSClients gid
- pure $ ClientList (concatMap (Map.keys . snd) (Map.assocs cm))
+ pure $ ClientList (concatMap (Map.keys . snd) (Map.assocs (unClientMap cm)))
diff --git a/services/galley/src/Galley/API/MLS/CheckClients.hs b/services/galley/src/Galley/API/MLS/CheckClients.hs
index 31ff97cf2c..9a834fd708 100644
--- a/services/galley/src/Galley/API/MLS/CheckClients.hs
+++ b/services/galley/src/Galley/API/MLS/CheckClients.hs
@@ -58,10 +58,10 @@ checkClients ::
checkClients lConvOrSub ciphersuite newCM = do
let convOrSub = tUnqualified lConvOrSub
cm = convOrSub.members
- fmap catMaybes . forM (Map.assocs newCM) $
+ fmap catMaybes . forM (Map.assocs (unClientMap newCM)) $
\(qtarget, newclients) -> do
mClientData <- getClientData lConvOrSub ciphersuite qtarget
- unreachable <- case (mClientData, Map.lookup qtarget cm) of
+ unreachable <- case (mClientData, cmLookup qtarget cm) of
-- user is already present, skip check in this case
(_, Just existingClients) -> do
-- make sure none of the new clients already exist in the group
@@ -79,7 +79,7 @@ checkClients lConvOrSub ciphersuite newCM = do
let clients =
Map.keysSet
( fmap fst newclients
- <> Map.findWithDefault mempty qtarget cm
+ <> fold (cmLookup qtarget cm)
)
-- We check the following condition:
diff --git a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs
index 4fe175b850..ac15b43399 100644
--- a/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs
+++ b/services/galley/src/Galley/API/MLS/Commit/InternalCommit.hs
@@ -97,7 +97,7 @@ processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdat
let convOrSub = tUnqualified lConvOrSub
qusr = cidQualifiedUser senderIdentity.client
cm = convOrSub.members
- newUserClients = Map.assocs (paAdd action)
+ newUserClients = Map.assocs (unClientMap (paAdd action))
-- check that all pending proposals are referenced in the commit
allPendingProposals <-
@@ -135,9 +135,9 @@ processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdat
-- subconversation case, an empty list is returned.
membersToRemove <- case convOrSub of
SubConv _ _ -> pure []
- Conv _ -> mapMaybe hush <$$> for (Map.assocs (paRemove action)) $
+ Conv _ -> mapMaybe hush <$$> for (Map.assocs (unClientMap (paRemove action))) $
\(qtarget, Map.keysSet -> clients) -> runError @() $ do
- let clientsInConv = Map.keysSet (Map.findWithDefault mempty qtarget cm)
+ let clientsInConv = foldMap Map.keysSet (cmLookup qtarget cm)
let removedClients = Set.intersection clients clientsInConv
-- ignore user if none of their clients are being removed
@@ -242,7 +242,7 @@ processInternalCommit senderIdentity con lConvOrSub ciphersuite ciphersuiteUpdat
-- Remove clients from the conversation state. This includes client removals
-- of all types (see Note [client removal]).
- for_ (Map.assocs (paRemove action)) $ \(qtarget, clients) -> do
+ for_ (Map.assocs (unClientMap (paRemove action))) $ \(qtarget, clients) -> do
removeMLSClients (cnvmlsGroupId convOrSub.mlsMeta) qtarget (Map.keysSet clients)
-- add clients to the conversation state
diff --git a/services/galley/src/Galley/API/MLS/IncomingMessage.hs b/services/galley/src/Galley/API/MLS/IncomingMessage.hs
index 2a0e730964..3a3fba6251 100644
--- a/services/galley/src/Galley/API/MLS/IncomingMessage.hs
+++ b/services/galley/src/Galley/API/MLS/IncomingMessage.hs
@@ -71,6 +71,7 @@ data IncomingBundle = IncomingBundle
sender :: Sender,
commit :: RawMLS Commit,
rawMessage :: RawMLS Message,
+ appMessage :: Maybe IncomingMessage,
welcome :: Maybe (RawMLS Welcome),
groupInfo :: RawMLS GroupInfo,
serialized :: ByteString
@@ -137,6 +138,7 @@ mkIncomingBundle bundle = do
sender = content.sender,
commit = commit,
rawMessage = bundle.value.commitMsg,
+ appMessage = bundle.value.appMessage >>= mkIncomingMessage,
welcome = bundle.value.welcome,
groupInfo = bundle.value.groupInfo,
serialized = bundle.raw
diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs
index 052b8132b0..7a7e87b7b6 100644
--- a/services/galley/src/Galley/API/MLS/Message.hs
+++ b/services/galley/src/Galley/API/MLS/Message.hs
@@ -265,6 +265,9 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do
lConvOrSub <- fetchConvOrSub qusr bundle.groupId ctype lConvOrSubId
let convOrSub = tUnqualified lConvOrSub
+ -- validate application message
+ traverse_ (validateMessage qusr c lConvOrSub (Just (succ bundle.epoch))) bundle.appMessage
+
ciphersuite <-
note (mlsProtocolError "Unsupported ciphersuite") $
cipherSuiteTag bundle.groupInfo.value.groupContext.cipherSuite
@@ -314,7 +317,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do
-- reject message if the conversation is out of sync
lift $ do
- let newUsers = Map.keysSet action.paAdd
+ let newUsers = Map.keysSet (unClientMap action.paAdd)
checkConversationOutOfSync newUsers lConvOrSub ciphersuite
lift $
@@ -333,7 +336,7 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do
bundle.commit.value
-- the sender client is included in the Add action on the first commit,
-- but it doesn't need to get a welcome message, so we filter it out here
- let newClients = filter ((/=) senderIdentity.client) (cmIdentities (paAdd action))
+ let newClients = cmRemoveClient senderIdentity.client (paAdd action)
pure (events, newClients)
Nothing -> do
(newIndexMap, action) <- lift $ getExternalCommitData senderIdentity.client lConvOrSub bundle.epoch bundle.commit.value
@@ -347,15 +350,25 @@ postMLSCommitBundleToLocalConv qusr c conn bundle ctype lConvOrSubId = do
bundle.epoch
action
bundle.commit.value.path
- pure ([], [])
+ pure ([], mempty)
lift $ do
updateOutOfSyncFlag senderIdentity.client lConvOrSub
storeGroupInfo convOrSub.id (GroupInfoData bundle.groupInfo.raw)
propagateMessage qusr (Just c) lConvOrSub conn bundle.rawMessage convOrSub.members
pure (events, newClients)
+ -- send welcome messages
for_ bundle.welcome $ \welcome ->
- sendWelcomes lConvOrSubId qusr conn newClients welcome
+ sendWelcomes lConvOrSubId qusr conn (cmIdentities newClients) welcome
+
+ -- send application message
+ for_ bundle.appMessage $ \msg -> do
+ -- reload conversation from db to make sure we have an up-to-date list of members
+ lConvOrSub' <- fetchConvOrSub qusr bundle.groupId ctype lConvOrSubId
+ let convOrSub' = tUnqualified lConvOrSub'
+ propagateMessage qusr (Just c) lConvOrSub' conn msg.rawMessage $
+ void convOrSub'.members
+
pure events
handleGroupInfoMismatch ::
@@ -510,8 +523,29 @@ postMLSMessageToLocalConv ::
Sem r [LocalConversationUpdate]
postMLSMessageToLocalConv qusr c con msg ctype convOrSubId = do
lConvOrSub <- fetchConvOrSub qusr msg.groupId ctype convOrSubId
- let convOrSub = tUnqualified lConvOrSub
+ validateMessage qusr c lConvOrSub Nothing msg
+ propagateMessage qusr (Just c) lConvOrSub con msg.rawMessage (tUnqualified lConvOrSub).members
+ pure []
+
+validateMessage ::
+ ( HasProposalEffects r,
+ Member (ErrorS ConvNotFound) r,
+ Member (ErrorS MLSClientSenderUserMismatch) r,
+ Member (ErrorS MLSStaleMessage) r,
+ Member (ErrorS MLSUnsupportedMessage) r,
+ Member (Error MLSOutOfSyncError) r,
+ Member (ErrorS MLSInvalidLeafNodeSignature) r,
+ Member (Input EnableOutOfSyncCheck) r
+ ) =>
+ Qualified UserId ->
+ ClientId ->
+ Local ConvOrSubConv ->
+ Maybe Epoch ->
+ IncomingMessage ->
+ Sem r ()
+validateMessage qusr c lConvOrSub mEpoch msg = do
+ let convOrSub = tUnqualified lConvOrSub
for_ msg.sender $ \sender ->
void $ getSenderIdentity qusr c sender lConvOrSub
@@ -537,22 +571,21 @@ postMLSMessageToLocalConv qusr c con msg ctype convOrSubId = do
-- reject application messages for epoch 0
let epochInt :: Epoch -> Integer
epochInt = fromIntegral . epochNumber
+
when (epochInt msg.epoch == 0) . throw $
mlsProtocolError "Application messages at epoch 0 are not supported"
-- reject application messages older than 2 epochs
- case convOrSub.mlsMeta.cnvmlsActiveData of
+ let mEpoch' = mEpoch <|> fmap (.epoch) convOrSub.mlsMeta.cnvmlsActiveData
+ case mEpoch' of
Nothing -> throw $ mlsProtocolError "Application messages at epoch 0 are not supported"
- Just activeData ->
+ Just epoch ->
when
- ( epochInt msg.epoch < epochInt activeData.epoch - 2
- || epochInt msg.epoch > epochInt activeData.epoch
+ ( epochInt msg.epoch < epochInt epoch - 2
+ || epochInt msg.epoch > epochInt epoch
)
$ throwS @'MLSStaleMessage
- propagateMessage qusr (Just c) lConvOrSub con msg.rawMessage (tUnqualified lConvOrSub).members
- pure []
-
postMLSMessageToRemoteConv ::
( Members MLSMessageStaticErrors r,
HasProposalEffects r,
diff --git a/services/galley/src/Galley/API/MLS/OutOfSync.hs b/services/galley/src/Galley/API/MLS/OutOfSync.hs
index 955efdd84f..e12aaddf65 100644
--- a/services/galley/src/Galley/API/MLS/OutOfSync.hs
+++ b/services/galley/src/Galley/API/MLS/OutOfSync.hs
@@ -97,5 +97,5 @@ getOutOfSyncUsers newMembers lconv =
Set.fromList $
map (tUntagged . qualifyAs lconv . (.id_)) conv.mcLocalMembers
<> map (tUntagged . (.id_)) conv.mcRemoteMembers
- groupMembers = Map.keysSet conv.mcMembers <> newMembers
+ groupMembers = Map.keysSet (unClientMap conv.mcMembers) <> newMembers
in Set.difference convMembers groupMembers
diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs
index d7a543349c..a8a656a467 100644
--- a/services/galley/src/Galley/API/MLS/Propagate.hs
+++ b/services/galley/src/Galley/API/MLS/Propagate.hs
@@ -35,7 +35,6 @@ import Wire.API.Federation.API
import Wire.API.Federation.API.Galley
import Wire.API.Federation.Error
import Wire.API.MLS.Credential
-import Wire.API.MLS.LeafNode
import Wire.API.MLS.Message
import Wire.API.MLS.Serialisation
import Wire.API.MLS.SubConversation
@@ -64,7 +63,7 @@ propagateMessage ::
Local ConvOrSubConv ->
Maybe ConnId ->
RawMLS Message ->
- ClientMap LeafIndex ->
+ ClientMap a ->
Sem r ()
propagateMessage qusr mSenderClient lConvOrSub con msg cm = do
now <- Now.get
@@ -120,14 +119,12 @@ propagateMessage qusr mSenderClient lConvOrSub con msg cm = do
localMemberRecipient loc lm = do
let localUserQId = tUntagged (qualifyAs loc localUserId)
localUserId = lm.id_
- clients <- nonEmpty $ Map.keys (Map.findWithDefault mempty localUserQId cmWithoutSender)
+ clients <- nonEmpty $ cmLookupClients localUserQId cmWithoutSender
pure $ Recipient localUserId (RecipientClientsSome clients)
remoteMemberMLSClients :: RemoteMember -> Maybe (UserId, NonEmpty ClientId)
remoteMemberMLSClients rm = do
let remoteUserQId = tUntagged rm.id_
remoteUserId = qUnqualified remoteUserQId
- clients <-
- nonEmpty . map fst $
- Map.assocs (Map.findWithDefault mempty remoteUserQId cmWithoutSender)
+ clients <- nonEmpty $ cmLookupClients remoteUserQId cmWithoutSender
pure (remoteUserId, clients)
diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs
index 72d2db09d3..68dc6a0a7a 100644
--- a/services/galley/src/Galley/API/MLS/Proposal.hs
+++ b/services/galley/src/Galley/API/MLS/Proposal.hs
@@ -82,9 +82,7 @@ data ProposalAction = ProposalAction
instance Semigroup ProposalAction where
ProposalAction add1 rem1 <> ProposalAction add2 rem2 =
- ProposalAction
- (Map.unionWith mappend add1 add2)
- (Map.unionWith mappend rem1 rem2)
+ ProposalAction (add1 <> add2) (rem1 <> rem2)
instance Monoid ProposalAction where
mempty = ProposalAction mempty mempty
diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs
index a65e4a0c80..9ae0b7712f 100644
--- a/services/galley/src/Galley/API/MLS/Removal.hs
+++ b/services/galley/src/Galley/API/MLS/Removal.hs
@@ -249,7 +249,8 @@ removeUser lc includeMain qusr = do
getClients =
map (first (mkClientIdentity qusr))
. Map.assocs
- . Map.findWithDefault mempty qusr
+ . fold
+ . cmLookup qusr
. (.members)
case includeMain of
RemoveUserIncludeMain ->
diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs
index 3616c18e77..8d45dfc410 100644
--- a/services/galley/src/Galley/API/MLS/SubConversation.hs
+++ b/services/galley/src/Galley/API/MLS/SubConversation.hs
@@ -34,7 +34,6 @@ where
import Control.Arrow
import Control.Monad.Codensity hiding (reset)
import Data.Id
-import Data.Map qualified as Map
import Data.Qualified
import Galley.API.MLS
import Galley.API.MLS.Conversation
@@ -336,7 +335,7 @@ leaveLocalSubConversation cid lcnv sub = do
-- plan to remove the leaver from the member list
Eff.planClientRemoval gid (Identity cid)
let cm = cmRemoveClient cid (scMembers subConv)
- if Map.null cm
+ if cmNull cm
then do
resetLocalSubConversation
(cidQualifiedUser cid)
diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs
index ef96d88d20..cd2c7bbaf3 100644
--- a/services/galley/test/integration/API/MLS/Util.hs
+++ b/services/galley/test/integration/API/MLS/Util.hs
@@ -871,7 +871,7 @@ mkBundle mp = do
_ -> Left "expected welcome"
ginfo <- note "group info unavailable" (mpGroupInfo mp)
ginfoB <- first ("GroupInfo: " <>) $ decodeMLS' ginfo
- pure $ CommitBundle commitB welcomeB ginfoB
+ pure $ CommitBundle commitB welcomeB ginfoB Nothing
createBundle :: (HasCallStack, MonadIO m) => MessagePackage -> m ByteString
createBundle mp = do
From 184071f7eff177e6380f0dcc20f018a8f54e697f Mon Sep 17 00:00:00 2001
From: Gautier DI FOLCO
Date: Wed, 21 Jan 2026 10:05:58 +0100
Subject: [PATCH 18/26] WPB-19575: Drop Cryptobox (#4719)
---
changelog.d/5-internal/WPD-19575 | 1 +
flake.lock | 18 -
flake.nix | 4 -
integration/default.nix | 2 -
integration/integration.cabal | 1 -
libs/wire-api/default.nix | 2 +
.../wire-api/src/Wire/API/Conversation/Bot.hs | 4 +-
.../src/Wire/API/Provider/External.hs | 4 +-
.../src/Wire/API/Team/LegalHold/External.hs | 2 +-
libs/wire-api/src/Wire/API/User/Client.hs | 18 +-
.../src/Wire/API/User/Client/Prekey.hs | 178 +++-
.../API/Golden/Generated/ClientPrekey_user.hs | 42 +-
.../Generated/NewBotResponse_provider.hs | 140 +--
.../API/Golden/Generated/NewClient_user.hs | 78 +-
.../Generated/NewLegalHoldClient_team.hs | 148 +--
.../API/Golden/Generated/PrekeyBundle_user.hs | 110 +-
.../Wire/API/Golden/Generated/Prekey_user.hs | 82 +-
.../Golden/Generated/UpdateBotPrekeys_user.hs | 6 +-
.../API/Golden/Generated/UpdateClient_user.hs | 116 +--
.../unit/Test/Wire/API/Roundtrip/Aeson.hs | 2 +-
.../test/unit/Test/Wire/API/Swagger.hs | 2 +-
libs/wire-api/wire-api.cabal | 1 +
.../wire-subsystems/src/Wire/BrigAPIAccess.hs | 4 +-
.../src/Wire/BrigAPIAccess/Rpc.hs | 2 +-
.../src/Wire/LegalHoldStore.hs | 4 +-
.../src/Wire/LegalHoldStore/Cassandra.hs | 8 +-
nix/haskell-pins.nix | 4 +-
nix/overlay.nix | 1 -
nix/pkgs/cryptobox/.gitignore | 1 -
nix/pkgs/cryptobox/Cargo.nix | 958 ------------------
nix/pkgs/cryptobox/README.md | 7 -
nix/pkgs/cryptobox/crate2nix-sources.nix | 115 ---
nix/pkgs/cryptobox/crate2nix.json | 10 -
nix/pkgs/cryptobox/default.nix | 55 -
nix/wire-server.nix | 2 +-
services/brig/brig.cabal | 1 -
services/brig/default.nix | 2 -
services/brig/src/Brig/API/Client.hs | 6 +-
services/brig/src/Brig/Data/Client.hs | 17 +-
.../brig/test/integration/API/Provider.hs | 4 +-
.../brig/test/integration/API/User/Client.hs | 6 +-
.../test/integration/Federation/End2end.hs | 2 +-
.../brig/test/integration/Federation/Util.hs | 2 +-
services/brig/test/integration/Util.hs | 58 +-
services/galley/src/Galley/API/LegalHold.hs | 2 +-
services/galley/test/integration/API/Util.hs | 54 +-
.../test-integration/Test/Spar/APISpec.hs | 6 +-
47 files changed, 629 insertions(+), 1663 deletions(-)
create mode 100644 changelog.d/5-internal/WPD-19575
delete mode 100644 nix/pkgs/cryptobox/.gitignore
delete mode 100644 nix/pkgs/cryptobox/Cargo.nix
delete mode 100644 nix/pkgs/cryptobox/README.md
delete mode 100644 nix/pkgs/cryptobox/crate2nix-sources.nix
delete mode 100644 nix/pkgs/cryptobox/crate2nix.json
delete mode 100644 nix/pkgs/cryptobox/default.nix
diff --git a/changelog.d/5-internal/WPD-19575 b/changelog.d/5-internal/WPD-19575
new file mode 100644
index 0000000000..acab462a0d
--- /dev/null
+++ b/changelog.d/5-internal/WPD-19575
@@ -0,0 +1 @@
+Drop `cryptobox`, handle prekey in pure Haskell.
diff --git a/flake.lock b/flake.lock
index e6994cbce0..aa065d4c19 100644
--- a/flake.lock
+++ b/flake.lock
@@ -68,23 +68,6 @@
"type": "github"
}
},
- "cryptobox-haskell": {
- "flake": false,
- "locked": {
- "lastModified": 1728557781,
- "narHash": "sha256-LROqEzzvKiJ7YoF8SdKUkEgGXKBRW6Wdtd4EBY3LYOk=",
- "owner": "wireapp",
- "repo": "cryptobox-haskell",
- "rev": "05560b2cfae13aac54414952638dadd62204f361",
- "type": "github"
- },
- "original": {
- "owner": "wireapp",
- "ref": "master",
- "repo": "cryptobox-haskell",
- "type": "github"
- }
- },
"flake-utils": {
"inputs": {
"systems": "systems"
@@ -226,7 +209,6 @@
"bloodhound": "bloodhound",
"cql": "cql",
"cql-io": "cql-io",
- "cryptobox-haskell": "cryptobox-haskell",
"flake-utils": "flake-utils",
"hedis": "hedis",
"hsaml2": "hsaml2",
diff --git a/flake.nix b/flake.nix
index 0be37264b9..242b5774b0 100644
--- a/flake.nix
+++ b/flake.nix
@@ -12,10 +12,6 @@
inputs.flake-utils.follows = "flake-utils";
};
- cryptobox-haskell = {
- url = "github:wireapp/cryptobox-haskell?ref=master";
- flake = false;
- };
bloodhound = {
url = "github:wireapp/bloodhound?ref=wire-fork";
flake = false;
diff --git a/integration/default.nix b/integration/default.nix
index 04163fc507..871bf6ae87 100644
--- a/integration/default.nix
+++ b/integration/default.nix
@@ -24,7 +24,6 @@
, cql
, cql-io
, criterion
-, cryptobox-haskell
, crypton
, crypton-x509
, cryptostore
@@ -132,7 +131,6 @@ mkDerivation {
cql
cql-io
criterion
- cryptobox-haskell
crypton
crypton-x509
cryptostore
diff --git a/integration/integration.cabal b/integration/integration.cabal
index 8bdbe597e0..72c0da80ef 100644
--- a/integration/integration.cabal
+++ b/integration/integration.cabal
@@ -256,7 +256,6 @@ library
, cql
, cql-io
, criterion
- , cryptobox-haskell
, crypton
, crypton-x509
, cryptostore
diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix
index 64df657ba5..f0e9150c67 100644
--- a/libs/wire-api/default.nix
+++ b/libs/wire-api/default.nix
@@ -22,6 +22,7 @@
, case-insensitive
, cassandra-util
, cassava
+, cborg
, cereal
, comonad
, conduit
@@ -144,6 +145,7 @@ mkDerivation {
case-insensitive
cassandra-util
cassava
+ cborg
cereal
comonad
conduit
diff --git a/libs/wire-api/src/Wire/API/Conversation/Bot.hs b/libs/wire-api/src/Wire/API/Conversation/Bot.hs
index aea518cfc9..a6878f3835 100644
--- a/libs/wire-api/src/Wire/API/Conversation/Bot.hs
+++ b/libs/wire-api/src/Wire/API/Conversation/Bot.hs
@@ -33,7 +33,7 @@ import Data.Schema
import Imports
import Wire.API.Event.Conversation (Event)
import Wire.API.Locale (Locale)
-import Wire.API.User.Client.Prekey (Prekey)
+import Wire.API.User.Client.Prekey (UncheckedPrekeyBundle)
import Wire.API.User.Profile (Asset, ColourId, Name)
import Wire.Arbitrary (Arbitrary, GenericUniform (..))
@@ -103,7 +103,7 @@ instance ToSchema RemoveBotResponse where
-- UpdateBotPrekeys
newtype UpdateBotPrekeys = UpdateBotPrekeys
- { updateBotPrekeyList :: [Prekey]
+ { updateBotPrekeyList :: [UncheckedPrekeyBundle]
}
deriving stock (Eq, Show)
deriving newtype (Arbitrary)
diff --git a/libs/wire-api/src/Wire/API/Provider/External.hs b/libs/wire-api/src/Wire/API/Provider/External.hs
index aebbd8f38e..f9f7ca0a35 100644
--- a/libs/wire-api/src/Wire/API/Provider/External.hs
+++ b/libs/wire-api/src/Wire/API/Provider/External.hs
@@ -29,7 +29,7 @@ import Data.Json.Util ((#))
import Imports
import Wire.API.Locale (Locale)
import Wire.API.Provider.Bot (BotConvView, BotUserView)
-import Wire.API.User.Client.Prekey (LastPrekey, Prekey)
+import Wire.API.User.Client.Prekey (LastPrekey, UncheckedPrekeyBundle)
import Wire.API.User.Profile (Asset, ColourId, Name)
import Wire.Arbitrary (Arbitrary, GenericUniform (..))
@@ -89,7 +89,7 @@ instance ToJSON NewBotRequest where
-- The returned optional data overrides the defaults taken from
-- the 'Service' definition.
data NewBotResponse = NewBotResponse
- { rsNewBotPrekeys :: [Prekey],
+ { rsNewBotPrekeys :: [UncheckedPrekeyBundle],
rsNewBotLastPrekey :: LastPrekey,
rsNewBotName :: Maybe Name,
rsNewBotColour :: Maybe ColourId,
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 1cb233020f..c49b6b196f 100644
--- a/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs
+++ b/libs/wire-api/src/Wire/API/Team/LegalHold/External.hs
@@ -82,7 +82,7 @@ instance ToSchema RequestNewLegalHoldClient where
-- | Response payload that the LH service returns upon calling @/initiate@
data NewLegalHoldClient = NewLegalHoldClient
- { newLegalHoldClientPrekeys :: [Prekey],
+ { newLegalHoldClientPrekeys :: [UncheckedPrekeyBundle],
newLegalHoldClientLastKey :: LastPrekey
}
deriving stock (Eq, Show, Generic)
diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs
index d020d8c850..50986d499d 100644
--- a/libs/wire-api/src/Wire/API/User/Client.hs
+++ b/libs/wire-api/src/Wire/API/User/Client.hs
@@ -254,12 +254,12 @@ userClientMapSchema sch =
UserClientMap <$> userClientMap .= map_ (map_ sch)
newtype UserClientPrekeyMap = UserClientPrekeyMap
- {getUserClientPrekeyMap :: UserClientMap (Maybe Prekey)}
+ {getUserClientPrekeyMap :: UserClientMap (Maybe UncheckedPrekeyBundle)}
deriving stock (Eq, Show)
deriving newtype (Arbitrary, Semigroup, Monoid)
deriving (FromJSON, ToJSON, Swagger.ToSchema) via Schema UserClientPrekeyMap
-mkUserClientPrekeyMap :: Map UserId (Map ClientId (Maybe Prekey)) -> UserClientPrekeyMap
+mkUserClientPrekeyMap :: Map UserId (Map ClientId (Maybe UncheckedPrekeyBundle)) -> UserClientPrekeyMap
mkUserClientPrekeyMap = coerce
instance ToSchema UserClientPrekeyMap where
@@ -275,7 +275,7 @@ instance ToSchema UserClientPrekeyMap where
(generateExample @UserId)
( Map.singleton
(ClientId 4940483633899001999)
- (Just (Prekey (PrekeyId 1) "pQABAQECoQBYIOjl7hw0D8YRNq..."))
+ (Just (UncheckedPrekeyBundle (PrekeyId 1) "pQABAQECoQBYIOjl7hw0D8YRNq..."))
)
)
@@ -319,7 +319,7 @@ qualifiedUserClientMapSchema sch =
)
data QualifiedUserClientPrekeyMapV4 = QualifiedUserClientPrekeyMapV4
- { qualifiedUserClientPrekeys :: QualifiedUserClientMap (Maybe Prekey),
+ { qualifiedUserClientPrekeys :: QualifiedUserClientMap (Maybe UncheckedPrekeyBundle),
failedToList :: Maybe [Qualified UserId]
}
deriving stock (Eq, Show)
@@ -340,16 +340,16 @@ instance ToSchema QualifiedUserClientPrekeyMapV4 where
where
from' :: QualifiedUserClientPrekeyMapV4 -> Map Domain UserClientPrekeyMap
from' = coerce . qualifiedUserClientPrekeys
- to' :: Map Domain UserClientPrekeyMap -> QualifiedUserClientMap (Maybe Prekey)
+ to' :: Map Domain UserClientPrekeyMap -> QualifiedUserClientMap (Maybe UncheckedPrekeyBundle)
to' = coerce
newtype QualifiedUserClientPrekeyMap = QualifiedUserClientPrekeyMap
- { getQualifiedUserClientPrekeyMap :: QualifiedUserClientMap (Maybe Prekey)
+ { getQualifiedUserClientPrekeyMap :: QualifiedUserClientMap (Maybe UncheckedPrekeyBundle)
}
deriving stock (Eq, Show)
deriving newtype (Arbitrary)
deriving (FromJSON, ToJSON, Swagger.ToSchema) via Schema QualifiedUserClientPrekeyMap
- deriving (Semigroup, Monoid) via (QualifiedUserClientMap (Alt Maybe Prekey))
+ deriving (Semigroup, Monoid) via (QualifiedUserClientMap (Alt Maybe UncheckedPrekeyBundle))
instance ToSchema QualifiedUserClientPrekeyMap where
schema =
@@ -709,7 +709,7 @@ instance C.Cql ClientClass where
-- NewClient
data NewClient = NewClient
- { newClientPrekeys :: [Prekey],
+ { newClientPrekeys :: [UncheckedPrekeyBundle],
newClientLastKey :: LastPrekey,
newClientType :: ClientType,
newClientLabel :: Maybe Text,
@@ -823,7 +823,7 @@ newClient t k =
-- UpdateClient
data UpdateClient = UpdateClient
- { updateClientPrekeys :: [Prekey],
+ { updateClientPrekeys :: [UncheckedPrekeyBundle],
updateClientLastKey :: Maybe LastPrekey,
updateClientLabel :: Maybe Text,
-- | see haddocks for 'ClientCapability'
diff --git a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs
index e2f8eb0408..1261508f7e 100644
--- a/libs/wire-api/src/Wire/API/User/Client/Prekey.hs
+++ b/libs/wire-api/src/Wire/API/User/Client/Prekey.hs
@@ -20,8 +20,11 @@
module Wire.API.User.Client.Prekey
( PrekeyId (..),
- Prekey (..),
+ UncheckedPrekeyBundle (..),
clientIdFromPrekey,
+ parsePrekeyBundlePrekeyId,
+ PrekeyBundlePrekeyPayload (..),
+ PrekeyParseError (..),
LastPrekey,
lastPrekey,
unpackLastPrekey,
@@ -33,18 +36,28 @@ module Wire.API.User.Client.Prekey
where
import Cassandra (ColumnType (IntColumn), Cql (ctype, fromCql, toCql), Tagged (..), Value (CqlInt))
+import Codec.CBOR.Decoding qualified as CBOR
+import Codec.CBOR.Read qualified as CBOR
+import Codec.CBOR.Term qualified as CBOR
import Crypto.Hash (SHA256, hash)
-import Data.Aeson (FromJSON (..), ToJSON (..))
+import Data.Aeson (FromJSON (..), ToJSON (..), withText)
+import Data.Aeson qualified as A
+import Data.Bifunctor (first)
import Data.Bits
import Data.ByteArray (convert)
import Data.ByteString qualified as BS
+import Data.ByteString.Base64 qualified as B64
+import Data.ByteString.Conversion qualified as B
+import Data.ByteString.Lazy qualified as LBS
import Data.Id
+import Data.Json.Util (base64Schema)
import Data.OpenApi qualified as S
-import Data.Schema
+import Data.Schema (Schema (..), ToSchema (..), array, field, named, object, withParser, (.=))
import Data.Text.Encoding (encodeUtf8)
import Imports
import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..))
+-- | We define PrekeyId as Word16, but it dismisses to 32-bits unsigned standard IDs.
newtype PrekeyId = PrekeyId {keyId :: Word16}
deriving stock (Eq, Ord, Show, Generic)
deriving newtype (ToJSON, FromJSON, Arbitrary, S.ToSchema, ToSchema)
@@ -56,20 +69,85 @@ instance Cql PrekeyId where
fromCql _ = Left "PrekeyId: Int expected"
--------------------------------------------------------------------------------
--- Prekey
+-- PrekeyBundle Specific Types
+newtype PrekeyBundlePublicKey = PrekeyBundlePublicKey {unPrekeyBundlePublicKey :: ByteString}
+ deriving stock (Eq, Show, Generic)
+
+instance ToJSON PrekeyBundlePublicKey where
+ toJSON = A.toJSON . B.fromByteString @Text . B64.encode . unPrekeyBundlePublicKey
+
+instance FromJSON PrekeyBundlePublicKey where
+ parseJSON = withText "PrekeyBundlePublicKey" $ \t ->
+ either (const $ fail "Not base 64-encoded") (pure . PrekeyBundlePublicKey) $
+ B64.decode (B.toByteString' t)
+
+instance ToSchema PrekeyBundlePublicKey where
+ schema = named "PrekeyBundlePublicKey" $ PrekeyBundlePublicKey <$> unPrekeyBundlePublicKey .= base64Schema
+
+newtype PrekeyBundleIdentityKey = PrekeyBundleIdentityKey {unPrekeyBundleIdentityKey :: ByteString}
+ deriving stock (Eq, Show, Generic)
+
+instance ToJSON PrekeyBundleIdentityKey where
+ toJSON = A.toJSON . B.fromByteString @Text . B64.encode . unPrekeyBundleIdentityKey
+
+instance FromJSON PrekeyBundleIdentityKey where
+ parseJSON = withText "PrekeyBundleIdentityKey" $ \t ->
+ either (const $ fail "Not base 64-encoded") (pure . PrekeyBundleIdentityKey) $
+ B64.decode (B.toByteString' t)
+
+instance ToSchema PrekeyBundleIdentityKey where
+ schema = named "PrekeyBundleIdentityKey" $ PrekeyBundleIdentityKey <$> unPrekeyBundleIdentityKey .= base64Schema
+
+newtype PrekeyBundleSignature = PrekeyBundleSignature {unPrekeyBundleSignature :: ByteString}
+ deriving stock (Eq, Show, Generic)
+
+instance ToJSON PrekeyBundleSignature where
+ toJSON = A.toJSON . B.fromByteString @Text . B64.encode . unPrekeyBundleSignature
+
+instance FromJSON PrekeyBundleSignature where
+ parseJSON = withText "PrekeyBundleSignature" $ \t ->
+ either (const $ fail "Not base 64-encoded") (pure . PrekeyBundleSignature) $
+ B64.decode (B.toByteString' t)
+
+instance ToSchema PrekeyBundleSignature where
+ schema = named "PrekeyBundleSignature" $ PrekeyBundleSignature <$> unPrekeyBundleSignature .= base64Schema
-data Prekey = Prekey
+-- Decoders for new types
+
+decodePrekeyBundlePublicKey :: CBOR.Decoder s PrekeyBundlePublicKey
+decodePrekeyBundlePublicKey = do
+ decodeUnfoldSingletonMap
+ decodeUnfoldSingletonMap
+ PrekeyBundlePublicKey <$> CBOR.decodeBytes
+
+decodePrekeyBundleIdentityKey :: CBOR.Decoder s PrekeyBundleIdentityKey
+decodePrekeyBundleIdentityKey = do
+ decodeUnfoldSingletonMap
+ PrekeyBundleIdentityKey <$> CBOR.decodeBytes
+
+decodeUnfoldSingletonMap :: CBOR.Decoder s ()
+decodeUnfoldSingletonMap = do
+ n <- CBOR.decodeMapLen
+ unless (n == 1) $ fail $ "Schema Mismatch: Expected Map of 1 element, found " <> show n
+ k <- CBOR.decodeInt
+ unless (k == 0) $ fail $ "Unknown Key: Expected 0, found " <> show k
+
+--------------------------------------------------------------------------------
+-- UncheckedPrekeyBundle
+
+data UncheckedPrekeyBundle = UncheckedPrekeyBundle
{ prekeyId :: PrekeyId,
+ -- | Prekey bundle
prekeyKey :: Text
}
deriving stock (Eq, Show, Generic)
- deriving (Arbitrary) via (GenericUniform Prekey)
- deriving (FromJSON, ToJSON, S.ToSchema) via Schema Prekey
+ deriving (Arbitrary) via (GenericUniform UncheckedPrekeyBundle)
+ deriving (FromJSON, ToJSON, S.ToSchema) via Schema UncheckedPrekeyBundle
-instance ToSchema Prekey where
+instance ToSchema UncheckedPrekeyBundle where
schema =
- object "Prekey" $
- Prekey
+ object "UncheckedPrekeyBundle" $
+ UncheckedPrekeyBundle
<$> prekeyId .= field "id" schema
<*> prekeyKey .= field "key" schema
@@ -78,7 +156,7 @@ instance ToSchema Prekey where
-- This works by taking the SHA256 hash of the prekey, truncating it to its
-- first 8 bytes, and interpreting the resulting bytestring as a big endian
-- Word64.
-clientIdFromPrekey :: Prekey -> ClientId
+clientIdFromPrekey :: UncheckedPrekeyBundle -> ClientId
clientIdFromPrekey =
ClientId
. foldl' (\w d -> (w `shiftL` 8) .|. fromIntegral d) 0
@@ -89,11 +167,81 @@ clientIdFromPrekey =
. encodeUtf8
. prekeyKey
+data PrekeyParseError
+ = PrekeyParseBase64Error String
+ | -- | Byte offset and error message
+ PrekeyParseCborError Int64 String
+ | PrekeyParseTrailingBytes
+ deriving stock (Eq, Show, Generic)
+
+-- | Represents the Prekey Bundle payload.
+--
+-- Structure based on `PrekyBundle` from proteus
+data PrekeyBundlePrekeyPayload = PrekeyBundlePrekeyPayload
+ { -- | Key 0
+ prekeyBundleProtocolVersion :: Word,
+ -- | Key 1
+ prekeyBundlePrekeyId :: PrekeyId,
+ -- | Key 2
+ prekeyBundleIdentityKey :: PrekeyBundleIdentityKey,
+ -- | Key 3
+ prekeyBundleSignedPrekey :: PrekeyBundlePublicKey,
+ -- | Key 4
+ prekeyBundleOneTimePrekey :: Maybe PrekeyBundleSignature
+ }
+ deriving stock (Eq, Show, Generic)
+
+-- | Parses a Base64 CBOR-encoded payload to extract the 'PrekeyId'.
+parsePrekeyBundlePrekeyId :: UncheckedPrekeyBundle -> Either PrekeyParseError PrekeyId
+parsePrekeyBundlePrekeyId pk = do
+ bs <- first (PrekeyParseBase64Error . ("Base64 decoding error: " <>)) $ B64.decode $ B.toByteString' $ prekeyKey pk
+ case CBOR.deserialiseFromBytes decodePrekeyBundlePrekeyPayload (LBS.fromStrict bs) of
+ Left (CBOR.DeserialiseFailure off msg) -> Left $ PrekeyParseCborError off msg
+ Right (rest, payload)
+ | LBS.null rest -> Right (prekeyBundlePrekeyId payload)
+ | otherwise -> Left PrekeyParseTrailingBytes
+
+decodePrekeyBundlePrekeyPayload :: CBOR.Decoder s PrekeyBundlePrekeyPayload
+decodePrekeyBundlePrekeyPayload = do
+ n <- CBOR.decodeMapLen
+ (m0, m1, m2, m3, m4) <- go n (Nothing, Nothing, Nothing, Nothing, Nothing)
+ PrekeyBundlePrekeyPayload
+ <$> maybe (fail "Missing Key 0") pure m0
+ <*> maybe (fail "Missing Key 1") pure m1
+ <*> maybe (fail "Missing Key 2") pure m2
+ <*> maybe (fail "Missing Key 3") pure m3
+ <*> pure m4 -- Key 4 is optional
+ where
+ go 0 acc = pure acc
+ go i (m0, m1, m2, m3, m4) = do
+ k <- CBOR.decodeInt
+ case k of
+ 0 -> do
+ v <- CBOR.decodeWord
+ go (i - 1) (Just v, m1, m2, m3, m4)
+ 1 -> do
+ v <- CBOR.decodeInt
+ when (v < 0) $ fail "Value Error: Prekey ID cannot be negative"
+ go (i - 1) (m0, Just (PrekeyId (fromIntegral v)), m2, m3, m4)
+ 2 -> do
+ v <- decodePrekeyBundleIdentityKey
+ go (i - 1) (m0, m1, Just v, m3, m4)
+ 3 -> do
+ v <- decodePrekeyBundlePublicKey
+ go (i - 1) (m0, m1, m2, Just v, m4)
+ 4 -> do
+ term <- CBOR.decodeTerm
+ case term of
+ CBOR.TNull -> go (i - 1) (m0, m1, m2, m3, Nothing)
+ CBOR.TMap [(CBOR.TInt 0, CBOR.TBytes bs)] -> go (i - 1) (m0, m1, m2, m3, Just $ PrekeyBundleSignature bs)
+ _ -> fail "Invalid onetime prekey component parsing"
+ other -> fail $ "Unknown Key: " <> show other
+
--------------------------------------------------------------------------------
-- LastPrekey
newtype LastPrekey = LastPrekey
- {unpackLastPrekey :: Prekey}
+ {unpackLastPrekey :: UncheckedPrekeyBundle}
deriving stock (Eq, Show, Generic)
deriving (FromJSON, ToJSON, S.ToSchema) via Schema LastPrekey
@@ -112,12 +260,12 @@ lastPrekeyId :: PrekeyId
lastPrekeyId = PrekeyId maxBound
lastPrekey :: Text -> LastPrekey
-lastPrekey = LastPrekey . Prekey lastPrekeyId
+lastPrekey = LastPrekey . UncheckedPrekeyBundle lastPrekeyId
-- for tests only
-- This fake last prekey has the wrong prekeyId
fakeLastPrekey :: LastPrekey
-fakeLastPrekey = LastPrekey $ Prekey (PrekeyId 7) "pQABAQcCoQBYIDXdN8VlKb5lbgPmoDPLPyqNIEyShG4oT/DlW0peRRZUA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY="
+fakeLastPrekey = LastPrekey $ UncheckedPrekeyBundle (PrekeyId 7) "pQABAQcCoQBYIDXdN8VlKb5lbgPmoDPLPyqNIEyShG4oT/DlW0peRRZUA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY="
--------------------------------------------------------------------------------
-- PrekeyBundle
@@ -142,7 +290,7 @@ instance ToSchema PrekeyBundle where
data ClientPrekey = ClientPrekey
{ prekeyClient :: ClientId,
- prekeyData :: Prekey
+ prekeyData :: UncheckedPrekeyBundle
}
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform ClientPrekey)
diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ClientPrekey_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ClientPrekey_user.hs
index f11ccb868c..4e15d78a74 100644
--- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ClientPrekey_user.hs
+++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/ClientPrekey_user.hs
@@ -18,144 +18,144 @@
module Test.Wire.API.Golden.Generated.ClientPrekey_user where
import Data.Id
-import Wire.API.User.Client.Prekey (ClientPrekey (..), Prekey (Prekey, prekeyId, prekeyKey), PrekeyId (PrekeyId, keyId))
+import Wire.API.User.Client.Prekey (ClientPrekey (..), PrekeyId (PrekeyId, keyId), UncheckedPrekeyBundle (UncheckedPrekeyBundle, prekeyId, prekeyKey))
testObject_ClientPrekey_user_1 :: ClientPrekey
testObject_ClientPrekey_user_1 =
ClientPrekey
{ prekeyClient = ClientId 0xf22,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 7}, prekeyKey = ""}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 7}, prekeyKey = ""}
}
testObject_ClientPrekey_user_2 :: ClientPrekey
testObject_ClientPrekey_user_2 =
ClientPrekey
{ prekeyClient = ClientId 0x1f7,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "\b\21129\169584\r;"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "\b\21129\169584\r;"}
}
testObject_ClientPrekey_user_3 :: ClientPrekey
testObject_ClientPrekey_user_3 =
ClientPrekey
{ prekeyClient = ClientId 0xfd8,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 4}, prekeyKey = "KA"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 4}, prekeyKey = "KA"}
}
testObject_ClientPrekey_user_4 :: ClientPrekey
testObject_ClientPrekey_user_4 =
ClientPrekey
{ prekeyClient = ClientId 0x83d,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 8}, prekeyKey = "OeYn"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 8}, prekeyKey = "OeYn"}
}
testObject_ClientPrekey_user_5 :: ClientPrekey
testObject_ClientPrekey_user_5 =
ClientPrekey
{ prekeyClient = ClientId 0xa69,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 3}, prekeyKey = "\131643\&3\ENQN]5~"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 3}, prekeyKey = "\131643\&3\ENQN]5~"}
}
testObject_ClientPrekey_user_6 :: ClientPrekey
testObject_ClientPrekey_user_6 =
ClientPrekey
{ prekeyClient = ClientId 0x05b4,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "\DEL\1053826("}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "\DEL\1053826("}
}
testObject_ClientPrekey_user_7 :: ClientPrekey
testObject_ClientPrekey_user_7 =
ClientPrekey
{ prekeyClient = ClientId 0x7b4,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 5}, prekeyKey = "\1072578P!+"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 5}, prekeyKey = "\1072578P!+"}
}
testObject_ClientPrekey_user_8 :: ClientPrekey
testObject_ClientPrekey_user_8 =
ClientPrekey
{ prekeyClient = ClientId 0x4e8,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 4}, prekeyKey = "AZrl"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 4}, prekeyKey = "AZrl"}
}
testObject_ClientPrekey_user_9 :: ClientPrekey
testObject_ClientPrekey_user_9 =
ClientPrekey
{ prekeyClient = ClientId 0x324,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 5}, prekeyKey = "\v>h"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 5}, prekeyKey = "\v>h"}
}
testObject_ClientPrekey_user_10 :: ClientPrekey
testObject_ClientPrekey_user_10 =
ClientPrekey
{ prekeyClient = ClientId 0x252,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 2}, prekeyKey = "0\EOT\DC2\RS\SI\1082579f"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 2}, prekeyKey = "0\EOT\DC2\RS\SI\1082579f"}
}
testObject_ClientPrekey_user_11 :: ClientPrekey
testObject_ClientPrekey_user_11 =
ClientPrekey
{ prekeyClient = ClientId 0xb99,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 6}, prekeyKey = "2\1025445\DEL"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 6}, prekeyKey = "2\1025445\DEL"}
}
testObject_ClientPrekey_user_12 :: ClientPrekey
testObject_ClientPrekey_user_12 =
ClientPrekey
{ prekeyClient = ClientId 0xbe3,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 4}, prekeyKey = "\US#\1040242M\120180\ETB?"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 4}, prekeyKey = "\US#\1040242M\120180\ETB?"}
}
testObject_ClientPrekey_user_13 :: ClientPrekey
testObject_ClientPrekey_user_13 =
ClientPrekey
{ prekeyClient = ClientId 0x1cf,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 7}, prekeyKey = "O,-%\150104o"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 7}, prekeyKey = "O,-%\150104o"}
}
testObject_ClientPrekey_user_14 :: ClientPrekey
testObject_ClientPrekey_user_14 =
ClientPrekey
{ prekeyClient = ClientId 0x710,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "\DC2\135043\96744\DEL\156322x\1009249"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "\DC2\135043\96744\DEL\156322x\1009249"}
}
testObject_ClientPrekey_user_15 :: ClientPrekey
testObject_ClientPrekey_user_15 =
ClientPrekey
{ prekeyClient = ClientId 0x97e,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\fk\1100893\NUL\ETX"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\fk\1100893\NUL\ETX"}
}
testObject_ClientPrekey_user_16 :: ClientPrekey
testObject_ClientPrekey_user_16 =
ClientPrekey
{ prekeyClient = ClientId 0x2b2,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 2}, prekeyKey = "\39095"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 2}, prekeyKey = "\39095"}
}
testObject_ClientPrekey_user_17 :: ClientPrekey
testObject_ClientPrekey_user_17 =
ClientPrekey
{ prekeyClient = ClientId 0x81c,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 2}, prekeyKey = "\1079390\987156h9\1060117"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 2}, prekeyKey = "\1079390\987156h9\1060117"}
}
testObject_ClientPrekey_user_18 :: ClientPrekey
testObject_ClientPrekey_user_18 =
ClientPrekey
{ prekeyClient = ClientId 0x895,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 8}, prekeyKey = ","}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 8}, prekeyKey = ","}
}
testObject_ClientPrekey_user_19 :: ClientPrekey
testObject_ClientPrekey_user_19 =
ClientPrekey
{ prekeyClient = ClientId 0x792,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 4}, prekeyKey = "g\60021\23060i\ETX"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 4}, prekeyKey = "g\60021\23060i\ETX"}
}
testObject_ClientPrekey_user_20 :: ClientPrekey
testObject_ClientPrekey_user_20 =
ClientPrekey
{ prekeyClient = ClientId 0xb02,
- prekeyData = Prekey {prekeyId = PrekeyId {keyId = 4}, prekeyKey = "D){H"}
+ prekeyData = UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 4}, prekeyKey = "D){H"}
}
diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewBotResponse_provider.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewBotResponse_provider.hs
index db3279d839..d03b6cc00a 100644
--- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewBotResponse_provider.hs
+++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewBotResponse_provider.hs
@@ -24,7 +24,7 @@ import Data.UUID qualified as UUID (fromString)
import Imports (Maybe (Just, Nothing), fromJust)
import Wire.API.Asset
import Wire.API.Provider.External (NewBotResponse (..))
-import Wire.API.User.Client.Prekey (Prekey (Prekey, prekeyId, prekeyKey), PrekeyId (PrekeyId, keyId), lastPrekey)
+import Wire.API.User.Client.Prekey (PrekeyId (PrekeyId, keyId), UncheckedPrekeyBundle (UncheckedPrekeyBundle, prekeyId, prekeyKey), lastPrekey)
import Wire.API.User.Profile
( Asset (ImageAsset),
AssetSize (AssetComplete, AssetPreview),
@@ -36,8 +36,8 @@ testObject_NewBotResponse_provider_1 :: NewBotResponse
testObject_NewBotResponse_provider_1 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\1079194"},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\1079194"},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
],
rsNewBotLastPrekey = lastPrekey "+\1035266\ENQ",
rsNewBotName =
@@ -55,19 +55,19 @@ testObject_NewBotResponse_provider_2 :: NewBotResponse
testObject_NewBotResponse_provider_2 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
],
rsNewBotLastPrekey = lastPrekey "\158260S\1013700\1033003\997116",
rsNewBotName = Just (Name {fromName = "\185552}nqW\t\179361\&7f"}),
@@ -85,11 +85,11 @@ testObject_NewBotResponse_provider_3 :: NewBotResponse
testObject_NewBotResponse_provider_3 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
rsNewBotLastPrekey = lastPrekey "I",
rsNewBotName =
@@ -123,8 +123,8 @@ testObject_NewBotResponse_provider_5 :: NewBotResponse
testObject_NewBotResponse_provider_5 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "U"},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "U"},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
rsNewBotLastPrekey = lastPrekey "\fC\NULL\\\EOT",
rsNewBotName = Nothing,
@@ -136,9 +136,9 @@ testObject_NewBotResponse_provider_6 :: NewBotResponse
testObject_NewBotResponse_provider_6 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\29859"}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\29859"}
],
rsNewBotLastPrekey = lastPrekey "",
rsNewBotName =
@@ -194,9 +194,9 @@ testObject_NewBotResponse_provider_9 :: NewBotResponse
testObject_NewBotResponse_provider_9 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "\NAK"},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "\NAK"},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
],
rsNewBotLastPrekey = lastPrekey "]4\68421\&8\\",
rsNewBotName =
@@ -214,11 +214,11 @@ testObject_NewBotResponse_provider_10 :: NewBotResponse
testObject_NewBotResponse_provider_10 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
],
rsNewBotLastPrekey = lastPrekey "R\165465",
rsNewBotName = Nothing,
@@ -230,8 +230,8 @@ testObject_NewBotResponse_provider_11 :: NewBotResponse
testObject_NewBotResponse_provider_11 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "8"}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "8"}
],
rsNewBotLastPrekey = lastPrekey "U\STX\ETB\1112642x",
rsNewBotName = Nothing,
@@ -252,7 +252,7 @@ testObject_NewBotResponse_provider_12 =
testObject_NewBotResponse_provider_13 :: NewBotResponse
testObject_NewBotResponse_provider_13 =
NewBotResponse
- { rsNewBotPrekeys = [Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "h\131368I"}],
+ { rsNewBotPrekeys = [UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "h\131368I"}],
rsNewBotLastPrekey = lastPrekey "\1000435eP'X",
rsNewBotName =
Just
@@ -276,14 +276,14 @@ testObject_NewBotResponse_provider_14 :: NewBotResponse
testObject_NewBotResponse_provider_14 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
],
rsNewBotLastPrekey = lastPrekey "Q\1012726\39031U",
rsNewBotName =
@@ -301,15 +301,15 @@ testObject_NewBotResponse_provider_15 :: NewBotResponse
testObject_NewBotResponse_provider_15 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
],
rsNewBotLastPrekey = lastPrekey "KuA\172666\1084633",
rsNewBotName =
@@ -327,11 +327,11 @@ testObject_NewBotResponse_provider_16 :: NewBotResponse
testObject_NewBotResponse_provider_16 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
rsNewBotLastPrekey = lastPrekey "e!D*j",
rsNewBotName = Just (Name {fromName = "\174414\&4?rvqg%\DC2\167142\DC1t\CAN\62298\SI_\92287F"}),
@@ -343,8 +343,8 @@ testObject_NewBotResponse_provider_17 :: NewBotResponse
testObject_NewBotResponse_provider_17 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "b"}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "b"}
],
rsNewBotLastPrekey = lastPrekey "\1064414\f\1024452\12105",
rsNewBotName = Just (Name {fromName = "g\49675B{\DC3Cq\CANmbD\DEL5Q\DC4>i\DC4\SI[\1022068|K\44297\57731|\175014"}),
@@ -371,12 +371,12 @@ testObject_NewBotResponse_provider_19 :: NewBotResponse
testObject_NewBotResponse_provider_19 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
rsNewBotLastPrekey = lastPrekey "u=\NAK",
rsNewBotName = Just (Name {fromName = "FvrT0g\\\169897"}),
@@ -395,9 +395,9 @@ testObject_NewBotResponse_provider_20 :: NewBotResponse
testObject_NewBotResponse_provider_20 =
NewBotResponse
{ rsNewBotPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "+"},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\52025"},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "+"},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\52025"},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
rsNewBotLastPrekey = lastPrekey "`|\144284^\US",
rsNewBotName = Nothing,
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 98bb2187a9..83afd30992 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
@@ -34,7 +34,7 @@ import Wire.API.User.Client.Prekey
testObject_NewClient_user_1 :: NewClient
testObject_NewClient_user_1 =
NewClient
- { newClientPrekeys = [Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\r"}],
+ { newClientPrekeys = [UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\r"}],
newClientLastKey = lastPrekey "\EM",
newClientType = TemporaryClientType,
newClientLabel = Just "",
@@ -95,8 +95,8 @@ testObject_NewClient_user_4 :: NewClient
testObject_NewClient_user_4 =
NewClient
{ newClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
newClientLastKey = lastPrekey "i",
newClientType = PermanentClientType,
@@ -117,7 +117,7 @@ testObject_NewClient_user_4 =
testObject_NewClient_user_5 :: NewClient
testObject_NewClient_user_5 =
NewClient
- { newClientPrekeys = [Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\1093219"}],
+ { newClientPrekeys = [UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\1093219"}],
newClientLastKey = lastPrekey "?",
newClientType = TemporaryClientType,
newClientLabel = Just "A\170327)",
@@ -149,7 +149,7 @@ testObject_NewClient_user_6 =
testObject_NewClient_user_7 :: NewClient
testObject_NewClient_user_7 =
NewClient
- { newClientPrekeys = [Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "a"}],
+ { newClientPrekeys = [UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "a"}],
newClientLastKey = lastPrekey "%V[",
newClientType = TemporaryClientType,
newClientLabel = Just "",
@@ -190,10 +190,10 @@ testObject_NewClient_user_9 :: NewClient
testObject_NewClient_user_9 =
NewClient
{ newClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
],
newClientLastKey = lastPrekey "",
newClientType = LegalHoldClientType,
@@ -210,7 +210,7 @@ testObject_NewClient_user_9 =
testObject_NewClient_user_10 :: NewClient
testObject_NewClient_user_10 =
NewClient
- { newClientPrekeys = [Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}],
+ { newClientPrekeys = [UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}],
newClientLastKey = lastPrekey "\STX",
newClientType = TemporaryClientType,
newClientLabel = Just ";*",
@@ -231,9 +231,9 @@ testObject_NewClient_user_11 :: NewClient
testObject_NewClient_user_11 =
NewClient
{ newClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
newClientLastKey = lastPrekey "",
newClientType = LegalHoldClientType,
@@ -255,12 +255,12 @@ testObject_NewClient_user_12 :: NewClient
testObject_NewClient_user_12 =
NewClient
{ newClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
newClientLastKey = lastPrekey "\ENQ",
newClientType = PermanentClientType,
@@ -282,13 +282,13 @@ testObject_NewClient_user_13 :: NewClient
testObject_NewClient_user_13 =
NewClient
{ newClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
newClientLastKey = lastPrekey "\DC1\DC3",
newClientType = LegalHoldClientType,
@@ -326,11 +326,11 @@ testObject_NewClient_user_15 :: NewClient
testObject_NewClient_user_15 =
NewClient
{ newClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
newClientLastKey = lastPrekey "\100417\113707",
newClientType = TemporaryClientType,
@@ -368,10 +368,10 @@ testObject_NewClient_user_17 :: NewClient
testObject_NewClient_user_17 =
NewClient
{ newClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
newClientLastKey = lastPrekey "\138278",
newClientType = TemporaryClientType,
@@ -409,9 +409,9 @@ testObject_NewClient_user_19 :: NewClient
testObject_NewClient_user_19 =
NewClient
{ newClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
newClientLastKey = lastPrekey "",
newClientType = PermanentClientType,
@@ -432,7 +432,7 @@ testObject_NewClient_user_19 =
testObject_NewClient_user_20 :: NewClient
testObject_NewClient_user_20 =
NewClient
- { newClientPrekeys = [Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}],
+ { newClientPrekeys = [UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}],
newClientLastKey = lastPrekey "<",
newClientType = LegalHoldClientType,
newClientLabel = Just "+\FS",
diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewLegalHoldClient_team.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewLegalHoldClient_team.hs
index afd80a7b11..443b98ba3c 100644
--- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewLegalHoldClient_team.hs
+++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/NewLegalHoldClient_team.hs
@@ -20,7 +20,7 @@
module Test.Wire.API.Golden.Generated.NewLegalHoldClient_team where
import Wire.API.Team.LegalHold.External (NewLegalHoldClient (..))
-import Wire.API.User.Client.Prekey (Prekey (Prekey, prekeyId, prekeyKey), PrekeyId (PrekeyId, keyId), lastPrekey)
+import Wire.API.User.Client.Prekey (PrekeyId (PrekeyId, keyId), UncheckedPrekeyBundle (UncheckedPrekeyBundle, prekeyId, prekeyKey), lastPrekey)
testObject_NewLegalHoldClient_team_1 :: NewLegalHoldClient
testObject_NewLegalHoldClient_team_1 =
@@ -30,8 +30,8 @@ testObject_NewLegalHoldClient_team_2 :: NewLegalHoldClient
testObject_NewLegalHoldClient_team_2 =
NewLegalHoldClient
{ newLegalHoldClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 2}, prekeyKey = ",5!"},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "0<\1030053"}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 2}, prekeyKey = ",5!"},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "0<\1030053"}
],
newLegalHoldClientLastKey = lastPrekey "\1104977\DLE\1065349\6667\&9,\1015715tft\FS"
}
@@ -46,7 +46,7 @@ testObject_NewLegalHoldClient_team_3 =
testObject_NewLegalHoldClient_team_4 :: NewLegalHoldClient
testObject_NewLegalHoldClient_team_4 =
NewLegalHoldClient
- { newLegalHoldClientPrekeys = [Prekey {prekeyId = PrekeyId {keyId = 5}, prekeyKey = "tp"}],
+ { newLegalHoldClientPrekeys = [UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 5}, prekeyKey = "tp"}],
newLegalHoldClientLastKey = lastPrekey "u%vZ\DC3\1088709D\173228\ENQ\"\188001"
}
@@ -54,8 +54,8 @@ testObject_NewLegalHoldClient_team_5 :: NewLegalHoldClient
testObject_NewLegalHoldClient_team_5 =
NewLegalHoldClient
{ newLegalHoldClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 2}, prekeyKey = "Y"},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "n"}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 2}, prekeyKey = "Y"},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = "n"}
],
newLegalHoldClientLastKey = lastPrekey "\\\1028142c\128341\&1\182736jO\CAN}T\58009D"
}
@@ -68,17 +68,17 @@ testObject_NewLegalHoldClient_team_7 :: NewLegalHoldClient
testObject_NewLegalHoldClient_team_7 =
NewLegalHoldClient
{ newLegalHoldClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = ""}
],
newLegalHoldClientLastKey = lastPrekey ""
}
@@ -91,12 +91,12 @@ testObject_NewLegalHoldClient_team_9 :: NewLegalHoldClient
testObject_NewLegalHoldClient_team_9 =
NewLegalHoldClient
{ newLegalHoldClientPrekeys =
- [ Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\1027435"},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "}"},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
- Prekey {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
+ [ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "\1027435"},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 0}, prekeyKey = "}"},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""},
+ UncheckedPrekeyBundle {prekeyId = PrekeyId {keyId = 1}, prekeyKey = ""}
],
newLegalHoldClientLastKey = lastPrekey "y=0.5
+ , cborg
, cereal
, comonad
, conduit
diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs
index 34b90fe5b0..1a98323561 100644
--- a/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs
+++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess.hs
@@ -145,7 +145,7 @@ data BrigAPIAccess m a where
AddLegalHoldClientToUserEither ::
UserId ->
ConnId ->
- [Prekey] ->
+ [UncheckedPrekeyBundle] ->
LastPrekey ->
BrigAPIAccess m (Either AuthenticationError ClientId)
RemoveLegalHoldClientFromUser :: UserId -> BrigAPIAccess m ()
@@ -174,7 +174,7 @@ addLegalHoldClientToUser ::
(Member BrigAPIAccess r, Member (Error AuthenticationError) r) =>
UserId ->
ConnId ->
- [Prekey] ->
+ [UncheckedPrekeyBundle] ->
LastPrekey ->
Sem r ClientId
addLegalHoldClientToUser uid con pks lpk =
diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs
index bca65344ee..acb4c5abdd 100644
--- a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs
+++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs
@@ -448,7 +448,7 @@ addLegalHoldClientToUser ::
(Member Rpc r, Member (Input Endpoint) r, Member (Error ParseException) r) =>
UserId ->
ConnId ->
- [Prekey] ->
+ [UncheckedPrekeyBundle] ->
LastPrekey ->
Sem r (Either AuthenticationError ClientId)
addLegalHoldClientToUser uid connId prekeys lastPrekey' = do
diff --git a/libs/wire-subsystems/src/Wire/LegalHoldStore.hs b/libs/wire-subsystems/src/Wire/LegalHoldStore.hs
index 66f675cb49..8bd32acf93 100644
--- a/libs/wire-subsystems/src/Wire/LegalHoldStore.hs
+++ b/libs/wire-subsystems/src/Wire/LegalHoldStore.hs
@@ -17,8 +17,8 @@ data LegalHoldStore m a where
CreateSettings :: LegalHoldService -> LegalHoldStore m ()
GetSettings :: TeamId -> LegalHoldStore m (Maybe LegalHoldService)
RemoveSettings :: TeamId -> LegalHoldStore m ()
- InsertPendingPrekeys :: UserId -> [Prekey] -> LegalHoldStore m ()
- SelectPendingPrekeys :: UserId -> LegalHoldStore m (Maybe ([Prekey], LastPrekey))
+ InsertPendingPrekeys :: UserId -> [UncheckedPrekeyBundle] -> LegalHoldStore m ()
+ SelectPendingPrekeys :: UserId -> LegalHoldStore m (Maybe ([UncheckedPrekeyBundle], LastPrekey))
DropPendingPrekeys :: UserId -> LegalHoldStore m ()
SetUserLegalHoldStatus :: TeamId -> UserId -> UserLegalHoldStatus -> LegalHoldStore m ()
SetTeamLegalholdWhitelisted :: TeamId -> LegalHoldStore m ()
diff --git a/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra.hs
index e0d473b5b7..2213a85c31 100644
--- a/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra.hs
+++ b/libs/wire-subsystems/src/Wire/LegalHoldStore/Cassandra.hs
@@ -91,14 +91,14 @@ getSettings tid = fmap toLegalHoldService <$> retry x1 (query1 Q.selectLegalHold
removeSettings :: (MonadClient m) => TeamId -> m ()
removeSettings tid = retry x5 (write Q.removeLegalHoldSettings (params LocalQuorum (Identity tid)))
-insertPendingPrekeys :: (MonadClient m) => UserId -> [Prekey] -> m ()
+insertPendingPrekeys :: (MonadClient m) => UserId -> [UncheckedPrekeyBundle] -> m ()
insertPendingPrekeys uid keys = retry x5 . batch $ do
- forM_ keys $ \(Prekey keyId key) -> addPrepQuery Q.insertPendingPrekeys (uid, keyId, key)
+ forM_ keys $ \(UncheckedPrekeyBundle keyId key) -> addPrepQuery Q.insertPendingPrekeys (uid, keyId, key)
-selectPendingPrekeys :: (MonadClient m) => UserId -> m (Maybe ([Prekey], LastPrekey))
+selectPendingPrekeys :: (MonadClient m) => UserId -> m (Maybe ([UncheckedPrekeyBundle], LastPrekey))
selectPendingPrekeys uid = pickLastKey . fmap fromTuple <$> retry x1 (query Q.selectPendingPrekeys (params LocalQuorum (Identity uid)))
where
- fromTuple (keyId, key) = Prekey keyId key
+ fromTuple (keyId, key) = UncheckedPrekeyBundle keyId key
pickLastKey allPrekeys = case unsnoc allPrekeys of
Nothing -> Nothing
Just (keys, lst) -> pure (keys, lastPrekey . prekeyKey $ lst)
diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix
index 5481009262..c723af12bc 100644
--- a/nix/haskell-pins.nix
+++ b/nix/haskell-pins.nix
@@ -52,9 +52,7 @@ let
# maintained by us
# ----------------
- cryptobox-haskell = {
- src = inputs.cryptobox-haskell;
- };
+ # None
# --------------------
# END maintained by us
diff --git a/nix/overlay.nix b/nix/overlay.nix
index 2ccc18888e..d418911d2c 100644
--- a/nix/overlay.nix
+++ b/nix/overlay.nix
@@ -1,6 +1,5 @@
self: super: {
- cryptobox = self.callPackage ./pkgs/cryptobox { };
zauth = self.callPackage ./pkgs/zauth { };
mls-test-cli = self.callPackage ./pkgs/mls-test-cli { };
diff --git a/nix/pkgs/cryptobox/.gitignore b/nix/pkgs/cryptobox/.gitignore
deleted file mode 100644
index 3f66fdb7d6..0000000000
--- a/nix/pkgs/cryptobox/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/crate2nix-sources
diff --git a/nix/pkgs/cryptobox/Cargo.nix b/nix/pkgs/cryptobox/Cargo.nix
deleted file mode 100644
index 0f803cf1b3..0000000000
--- a/nix/pkgs/cryptobox/Cargo.nix
+++ /dev/null
@@ -1,958 +0,0 @@
-# This file was @generated by crate2nix 0.10.0 with the command:
-# "generate"
-# See https://github.com/kolloch/crate2nix for more info.
-
-{ nixpkgs ?
-, pkgs ? import nixpkgs { config = { }; }
-, lib ? pkgs.lib
-, stdenv ? pkgs.stdenv
-, buildRustCrateForPkgs ? if buildRustCrate != null
- then lib.warn "crate2nix: Passing `buildRustCrate` as argument to Cargo.nix is deprecated. If you don't customize `buildRustCrate`, replace `callPackage ./Cargo.nix {}` by `import ./Cargo.nix { inherit pkgs; }`, and if you need to customize `buildRustCrate`, use `buildRustCrateForPkgs` instead." (_: buildRustCrate)
- else pkgs: pkgs.buildRustCrate
- # Deprecated
-, buildRustCrate ? null
- # This is used as the `crateOverrides` argument for `buildRustCrate`.
-, defaultCrateOverrides ? pkgs.defaultCrateOverrides
- # The features to enable for the root_crate or the workspace_members.
-, rootFeatures ? [ "default" ]
- # If true, throw errors instead of issueing deprecation warnings.
-, strictDeprecation ? false
- # Used for conditional compilation based on CPU feature detection.
-, targetFeatures ? [ ]
- # Whether to perform release builds: longer compile times, faster binaries.
-, release ? true
- # Additional crate2nix configuration if it exists.
-, crateConfig ? if builtins.pathExists ./crate-config.nix
- then pkgs.callPackage ./crate-config.nix { }
- else { }
-}:
-
-rec {
- #
- # "public" attributes that we attempt to keep stable with new versions of crate2nix.
- #
-
- rootCrate = rec {
- packageId = "cryptobox-c";
-
- # Use this attribute to refer to the derivation building your root crate package.
- # You can override the features with rootCrate.build.override { features = [ "default" "feature1" ... ]; }.
- build = internal.buildRustCrateWithFeatures {
- inherit packageId;
- };
-
- # Debug support which might change between releases.
- # File a bug if you depend on any for non-debug work!
- debug = internal.debugCrate { inherit packageId; };
- };
- # Refer your crate build derivation by name here.
- # You can override the features with
- # workspaceMembers."${crateName}".build.override { features = [ "default" "feature1" ... ]; }.
- workspaceMembers = {
- "cryptobox-c" = rec {
- packageId = "cryptobox-c";
- build = internal.buildRustCrateWithFeatures {
- packageId = "cryptobox-c";
- };
-
- # Debug support which might change between releases.
- # File a bug if you depend on any for non-debug work!
- debug = internal.debugCrate { inherit packageId; };
- };
- };
-
- # A derivation that joins the outputs of all workspace members together.
- allWorkspaceMembers = pkgs.symlinkJoin {
- name = "all-workspace-members";
- paths =
- let members = builtins.attrValues workspaceMembers;
- in builtins.map (m: m.build) members;
- };
-
- #
- # "internal" ("private") attributes that may change in every new version of crate2nix.
- #
-
- internal = rec {
- # Build and dependency information for crates.
- # Many of the fields are passed one-to-one to buildRustCrate.
- #
- # Noteworthy:
- # * `dependencies`/`buildDependencies`: similar to the corresponding fields for buildRustCrate.
- # but with additional information which is used during dependency/feature resolution.
- # * `resolvedDependencies`: the selected default features reported by cargo - only included for debugging.
- # * `devDependencies` as of now not used by `buildRustCrate` but used to
- # inject test dependencies into the build
-
- crates = {
- "byteorder" = rec {
- crateName = "byteorder";
- version = "1.2.1";
- edition = "2015";
- sha256 = "08qdzm6y639swc9crvkav59cp46lmfj84rlsbvcakb9zwyvhaa35";
- authors = [
- "Andrew Gallant "
- ];
- features = {
- "default" = [ "std" ];
- };
- resolvedDefaultFeatures = [ "default" "std" ];
- };
- "cbor-codec" = rec {
- crateName = "cbor-codec";
- version = "0.7.1";
- edition = "2015";
- sha256 = "0ihg2ixp71nkwcgg8jfip774q5dia4d16l786wlcadrbaqis10z0";
- libName = "cbor";
- authors = [
- "Toralf Wittner "
- ];
- dependencies = [
- {
- name = "byteorder";
- packageId = "byteorder";
- }
- {
- name = "libc";
- packageId = "libc";
- }
- ];
- features = {
- "quickcheck" = [ "dep:quickcheck" ];
- "random" = [ "quickcheck" ];
- };
- };
- "cryptobox" = rec {
- crateName = "cryptobox";
- version = "1.0.0";
- edition = "2015";
- workspace_member = null;
- src = pkgs.fetchgit {
- url = "https://github.com/wireapp/cryptobox";
- rev = "ec45a50c3608d00744625125125932beac890965";
- sha256 = "0sada781pwczmxhlnw0qhkh54k22jdhymc5kaczrwk86s4b3w2hk";
- };
- authors = [
- "Wire Swiss GmbH "
- ];
- dependencies = [
- {
- name = "byteorder";
- packageId = "byteorder";
- }
- {
- name = "cbor-codec";
- packageId = "cbor-codec";
- }
- {
- name = "proteus";
- packageId = "proteus";
- }
- ];
-
- };
- "cryptobox-c" = rec {
- crateName = "cryptobox-c";
- version = "1.0.0";
- edition = "2015";
- workspace_member = null;
- src = pkgs.fetchgit {
- url = "https://github.com/wireapp/cryptobox-c";
- rev = "4067ad96b125942545dbdec8c1a89f1e1b65d013";
- sha256 = "1i9dlhw0xk1viglyhail9fb36v1awrypps8jmhrkz8k1bhx98ci3";
- };
- type = [ "cdylib" ];
- authors = [
- "Wire Swiss GmbH "
- ];
- dependencies = [
- {
- name = "cryptobox";
- packageId = "cryptobox";
- }
- {
- name = "libc";
- packageId = "libc";
- }
- {
- name = "proteus";
- packageId = "proteus";
- }
- ];
-
- };
- "hkdf" = rec {
- crateName = "hkdf";
- version = "0.2.0";
- edition = "2015";
- workspace_member = null;
- src = pkgs.fetchgit {
- url = "https://github.com/wireapp/hkdf";
- rev = "215025dc0efec119a7368447ed97bb908eddfb1c";
- sha256 = "1s0q2xq489r4zmbhd501mm9qpwaw9zv3bz0pii493l1qhzvi3n3i";
- };
- authors = [
- "Wire Swiss GmbH "
- ];
- dependencies = [
- {
- name = "sodiumoxide";
- packageId = "sodiumoxide";
- usesDefaultFeatures = false;
- }
- ];
-
- };
- "libc" = rec {
- crateName = "libc";
- version = "0.2.35";
- edition = "2015";
- sha256 = "1sr0la5n6bq1g1yqpfjffmdwiv8szn7siy5vzidx559y56dlw9ln";
- authors = [
- "The Rust Project Developers"
- ];
- features = {
- "default" = [ "use_std" ];
- };
- resolvedDefaultFeatures = [ "default" "use_std" ];
- };
- "libsodium-sys" = rec {
- crateName = "libsodium-sys";
- version = "0.0.16";
- edition = "2015";
- sha256 = "0hjmdxjz32yq4gxwjg608bi7cf9igilsmsv9lslcli4dxpp1pggw";
- libName = "libsodium_sys";
- libPath = "lib.rs";
- authors = [
- "dnaq"
- ];
- dependencies = [
- {
- name = "libc";
- packageId = "libc";
- }
- ];
- buildDependencies = [
- {
- name = "pkg-config";
- packageId = "pkg-config";
- }
- ];
-
- };
- "pkg-config" = rec {
- crateName = "pkg-config";
- version = "0.3.9";
- edition = "2015";
- sha256 = "00x9vc7667m4r8sn8idgpmj9yf1ih6bj1cdrshf1mkb5h5mlr2rs";
- authors = [
- "Alex Crichton "
- ];
-
- };
- "proteus" = rec {
- crateName = "proteus";
- version = "1.0.0";
- edition = "2015";
- workspace_member = null;
- src = pkgs.fetchgit {
- url = "https://github.com/wireapp/proteus";
- rev = "bbecc0c649d020bb208ad83d120c6971913e2eeb";
- sha256 = "0dp4fzd39kzr0z1rq78vmgvyvpg9b13k1fq37gmsvr2qlkkjv4x6";
- };
- authors = [
- "Wire Swiss GmbH "
- ];
- dependencies = [
- {
- name = "byteorder";
- packageId = "byteorder";
- }
- {
- name = "cbor-codec";
- packageId = "cbor-codec";
- }
- {
- name = "hkdf";
- packageId = "hkdf";
- }
- {
- name = "libc";
- packageId = "libc";
- }
- {
- name = "sodiumoxide";
- packageId = "sodiumoxide";
- usesDefaultFeatures = false;
- features = [ "std" ];
- }
- ];
-
- };
- "sodiumoxide" = rec {
- crateName = "sodiumoxide";
- version = "0.0.16";
- edition = "2015";
- sha256 = "02m0mbyq4k4p7baz659ndaixancl19x2anaysqm3alcs9zqv4p7b";
- authors = [
- "dnaq"
- ];
- dependencies = [
- {
- name = "libc";
- packageId = "libc";
- }
- {
- name = "libsodium-sys";
- packageId = "libsodium-sys";
- }
- ];
- features = {
- "default" = [ "serde" "std" ];
- "serde" = [ "dep:serde" ];
- };
- resolvedDefaultFeatures = [ "std" ];
- };
- };
-
- #
- # crate2nix/default.nix (excerpt start)
- #
-
- /* Target (platform) data for conditional dependencies.
- This corresponds roughly to what buildRustCrate is setting.
- */
- defaultTarget = {
- unix = true;
- windows = false;
- fuchsia = true;
- test = false;
-
- # This doesn't appear to be officially documented anywhere yet.
- # See https://github.com/rust-lang-nursery/rust-forge/issues/101.
- os =
- if stdenv.hostPlatform.isDarwin
- then "macos"
- else stdenv.hostPlatform.parsed.kernel.name;
- arch = stdenv.hostPlatform.parsed.cpu.name;
- family = "unix";
- env = "gnu";
- endian =
- if stdenv.hostPlatform.parsed.cpu.significantByte.name == "littleEndian"
- then "little" else "big";
- pointer_width = toString stdenv.hostPlatform.parsed.cpu.bits;
- vendor = stdenv.hostPlatform.parsed.vendor.name;
- debug_assertions = false;
- };
-
- /* Filters common temp files and build files. */
- # TODO(pkolloch): Substitute with gitignore filter
- sourceFilter = name: type:
- let
- baseName = builtins.baseNameOf (builtins.toString name);
- in
- ! (
- # Filter out git
- baseName == ".gitignore"
- || (type == "directory" && baseName == ".git")
-
- # Filter out build results
- || (
- type == "directory" && (
- baseName == "target"
- || baseName == "_site"
- || baseName == ".sass-cache"
- || baseName == ".jekyll-metadata"
- || baseName == "build-artifacts"
- )
- )
-
- # Filter out nix-build result symlinks
- || (
- type == "symlink" && lib.hasPrefix "result" baseName
- )
-
- # Filter out IDE config
- || (
- type == "directory" && (
- baseName == ".idea" || baseName == ".vscode"
- )
- ) || lib.hasSuffix ".iml" baseName
-
- # Filter out nix build files
- || baseName == "Cargo.nix"
-
- # Filter out editor backup / swap files.
- || lib.hasSuffix "~" baseName
- || builtins.match "^\\.sw[a-z]$$" baseName != null
- || builtins.match "^\\..*\\.sw[a-z]$$" baseName != null
- || lib.hasSuffix ".tmp" baseName
- || lib.hasSuffix ".bak" baseName
- || baseName == "tests.nix"
- );
-
- /* Returns a crate which depends on successful test execution
- of crate given as the second argument.
-
- testCrateFlags: list of flags to pass to the test exectuable
- testInputs: list of packages that should be available during test execution
- */
- crateWithTest = { crate, testCrate, testCrateFlags, testInputs, testPreRun, testPostRun }:
- assert builtins.typeOf testCrateFlags == "list";
- assert builtins.typeOf testInputs == "list";
- assert builtins.typeOf testPreRun == "string";
- assert builtins.typeOf testPostRun == "string";
- let
- # override the `crate` so that it will build and execute tests instead of
- # building the actual lib and bin targets We just have to pass `--test`
- # to rustc and it will do the right thing. We execute the tests and copy
- # their log and the test executables to $out for later inspection.
- test =
- let
- drv = testCrate.override
- (
- _: {
- buildTests = true;
- }
- );
- # If the user hasn't set any pre/post commands, we don't want to
- # insert empty lines. This means that any existing users of crate2nix
- # don't get a spurious rebuild unless they set these explicitly.
- testCommand = pkgs.lib.concatStringsSep "\n"
- (pkgs.lib.filter (s: s != "") [
- testPreRun
- "$f $testCrateFlags 2>&1 | tee -a $out"
- testPostRun
- ]);
- in
- pkgs.runCommand "run-tests-${testCrate.name}"
- {
- inherit testCrateFlags;
- buildInputs = testInputs;
- } ''
- set -ex
-
- export RUST_BACKTRACE=1
-
- # recreate a file hierarchy as when running tests with cargo
-
- # the source for test data
- ${pkgs.xorg.lndir}/bin/lndir ${crate.src}
-
- # build outputs
- testRoot=target/debug
- mkdir -p $testRoot
-
- # executables of the crate
- # we copy to prevent std::env::current_exe() to resolve to a store location
- for i in ${crate}/bin/*; do
- cp "$i" "$testRoot"
- done
- chmod +w -R .
-
- # test harness executables are suffixed with a hash, like cargo does
- # this allows to prevent name collision with the main
- # executables of the crate
- hash=$(basename $out)
- for file in ${drv}/tests/*; do
- f=$testRoot/$(basename $file)-$hash
- cp $file $f
- ${testCommand}
- done
- '';
- in
- pkgs.runCommand "${crate.name}-linked"
- {
- inherit (crate) outputs crateName;
- passthru = (crate.passthru or { }) // {
- inherit test;
- };
- } ''
- echo tested by ${test}
- ${lib.concatMapStringsSep "\n" (output: "ln -s ${crate.${output}} ${"$"}${output}") crate.outputs}
- '';
-
- /* A restricted overridable version of builtRustCratesWithFeatures. */
- buildRustCrateWithFeatures =
- { packageId
- , features ? rootFeatures
- , crateOverrides ? defaultCrateOverrides
- , buildRustCrateForPkgsFunc ? null
- , runTests ? false
- , testCrateFlags ? [ ]
- , testInputs ? [ ]
- # Any command to run immediatelly before a test is executed.
- , testPreRun ? ""
- # Any command run immediatelly after a test is executed.
- , testPostRun ? ""
- }:
- lib.makeOverridable
- (
- { features
- , crateOverrides
- , runTests
- , testCrateFlags
- , testInputs
- , testPreRun
- , testPostRun
- }:
- let
- buildRustCrateForPkgsFuncOverriden =
- if buildRustCrateForPkgsFunc != null
- then buildRustCrateForPkgsFunc
- else
- (
- if crateOverrides == pkgs.defaultCrateOverrides
- then buildRustCrateForPkgs
- else
- pkgs: (buildRustCrateForPkgs pkgs).override {
- defaultCrateOverrides = crateOverrides;
- }
- );
- builtRustCrates = builtRustCratesWithFeatures {
- inherit packageId features;
- buildRustCrateForPkgsFunc = buildRustCrateForPkgsFuncOverriden;
- runTests = false;
- };
- builtTestRustCrates = builtRustCratesWithFeatures {
- inherit packageId features;
- buildRustCrateForPkgsFunc = buildRustCrateForPkgsFuncOverriden;
- runTests = true;
- };
- drv = builtRustCrates.crates.${packageId};
- testDrv = builtTestRustCrates.crates.${packageId};
- derivation =
- if runTests then
- crateWithTest
- {
- crate = drv;
- testCrate = testDrv;
- inherit testCrateFlags testInputs testPreRun testPostRun;
- }
- else drv;
- in
- derivation
- )
- { inherit features crateOverrides runTests testCrateFlags testInputs testPreRun testPostRun; };
-
- /* Returns an attr set with packageId mapped to the result of buildRustCrateForPkgsFunc
- for the corresponding crate.
- */
- builtRustCratesWithFeatures =
- { packageId
- , features
- , crateConfigs ? crates
- , buildRustCrateForPkgsFunc
- , runTests
- , target ? defaultTarget
- } @ args:
- assert (builtins.isAttrs crateConfigs);
- assert (builtins.isString packageId);
- assert (builtins.isList features);
- assert (builtins.isAttrs target);
- assert (builtins.isBool runTests);
- let
- rootPackageId = packageId;
- mergedFeatures = mergePackageFeatures
- (
- args // {
- inherit rootPackageId;
- target = target // { test = runTests; };
- }
- );
- # Memoize built packages so that reappearing packages are only built once.
- builtByPackageIdByPkgs = mkBuiltByPackageIdByPkgs pkgs;
- mkBuiltByPackageIdByPkgs = pkgs:
- let
- self = {
- crates = lib.mapAttrs (packageId: value: buildByPackageIdForPkgsImpl self pkgs packageId) crateConfigs;
- build = mkBuiltByPackageIdByPkgs pkgs.buildPackages;
- };
- in
- self;
- buildByPackageIdForPkgsImpl = self: pkgs: packageId:
- let
- features = mergedFeatures."${packageId}" or [ ];
- crateConfig' = crateConfigs."${packageId}";
- crateConfig =
- builtins.removeAttrs crateConfig' [ "resolvedDefaultFeatures" "devDependencies" ];
- devDependencies =
- lib.optionals
- (runTests && packageId == rootPackageId)
- (crateConfig'.devDependencies or [ ]);
- dependencies =
- dependencyDerivations {
- inherit features target;
- buildByPackageId = depPackageId:
- # proc_macro crates must be compiled for the build architecture
- if crateConfigs.${depPackageId}.procMacro or false
- then self.build.crates.${depPackageId}
- else self.crates.${depPackageId};
- dependencies =
- (crateConfig.dependencies or [ ])
- ++ devDependencies;
- };
- buildDependencies =
- dependencyDerivations {
- inherit features target;
- buildByPackageId = depPackageId:
- self.build.crates.${depPackageId};
- dependencies = crateConfig.buildDependencies or [ ];
- };
- filterEnabledDependenciesForThis = dependencies: filterEnabledDependencies {
- inherit dependencies features target;
- };
- dependenciesWithRenames =
- lib.filter (d: d ? "rename")
- (
- filterEnabledDependenciesForThis
- (
- (crateConfig.buildDependencies or [ ])
- ++ (crateConfig.dependencies or [ ])
- ++ devDependencies
- )
- );
- # Crate renames have the form:
- #
- # {
- # crate_name = [
- # { version = "1.2.3"; rename = "crate_name01"; }
- # ];
- # # ...
- # }
- crateRenames =
- let
- grouped =
- lib.groupBy
- (dependency: dependency.name)
- dependenciesWithRenames;
- versionAndRename = dep:
- let
- package = crateConfigs."${dep.packageId}";
- in
- { inherit (dep) rename; version = package.version; };
- in
- lib.mapAttrs (name: choices: builtins.map versionAndRename choices) grouped;
- in
- buildRustCrateForPkgsFunc pkgs
- (
- crateConfig // {
- src = crateConfig.src or (
- pkgs.fetchurl rec {
- name = "${crateConfig.crateName}-${crateConfig.version}.tar.gz";
- # https://www.pietroalbini.org/blog/downloading-crates-io/
- # Not rate-limited, CDN URL.
- url = "https://static.crates.io/crates/${crateConfig.crateName}/${crateConfig.crateName}-${crateConfig.version}.crate";
- sha256 =
- assert (lib.assertMsg (crateConfig ? sha256) "Missing sha256 for ${name}");
- crateConfig.sha256;
- }
- );
- extraRustcOpts = lib.lists.optional (targetFeatures != [ ]) "-C target-feature=${lib.concatMapStringsSep "," (x: "+${x}") targetFeatures}";
- inherit features dependencies buildDependencies crateRenames release;
- }
- );
- in
- builtByPackageIdByPkgs;
-
- /* Returns the actual derivations for the given dependencies. */
- dependencyDerivations =
- { buildByPackageId
- , features
- , dependencies
- , target
- }:
- assert (builtins.isList features);
- assert (builtins.isList dependencies);
- assert (builtins.isAttrs target);
- let
- enabledDependencies = filterEnabledDependencies {
- inherit dependencies features target;
- };
- depDerivation = dependency: buildByPackageId dependency.packageId;
- in
- map depDerivation enabledDependencies;
-
- /* Returns a sanitized version of val with all values substituted that cannot
- be serialized as JSON.
- */
- sanitizeForJson = val:
- if builtins.isAttrs val
- then lib.mapAttrs (n: v: sanitizeForJson v) val
- else if builtins.isList val
- then builtins.map sanitizeForJson val
- else if builtins.isFunction val
- then "function"
- else val;
-
- /* Returns various tools to debug a crate. */
- debugCrate = { packageId, target ? defaultTarget }:
- assert (builtins.isString packageId);
- let
- debug = rec {
- # The built tree as passed to buildRustCrate.
- buildTree = buildRustCrateWithFeatures {
- buildRustCrateForPkgsFunc = _: lib.id;
- inherit packageId;
- };
- sanitizedBuildTree = sanitizeForJson buildTree;
- dependencyTree = sanitizeForJson
- (
- buildRustCrateWithFeatures {
- buildRustCrateForPkgsFunc = _: crate: {
- "01_crateName" = crate.crateName or false;
- "02_features" = crate.features or [ ];
- "03_dependencies" = crate.dependencies or [ ];
- };
- inherit packageId;
- }
- );
- mergedPackageFeatures = mergePackageFeatures {
- features = rootFeatures;
- inherit packageId target;
- };
- diffedDefaultPackageFeatures = diffDefaultPackageFeatures {
- inherit packageId target;
- };
- };
- in
- { internal = debug; };
-
- /* Returns differences between cargo default features and crate2nix default
- features.
-
- This is useful for verifying the feature resolution in crate2nix.
- */
- diffDefaultPackageFeatures =
- { crateConfigs ? crates
- , packageId
- , target
- }:
- assert (builtins.isAttrs crateConfigs);
- let
- prefixValues = prefix: lib.mapAttrs (n: v: { "${prefix}" = v; });
- mergedFeatures =
- prefixValues
- "crate2nix"
- (mergePackageFeatures { inherit crateConfigs packageId target; features = [ "default" ]; });
- configs = prefixValues "cargo" crateConfigs;
- combined = lib.foldAttrs (a: b: a // b) { } [ mergedFeatures configs ];
- onlyInCargo =
- builtins.attrNames
- (lib.filterAttrs (n: v: !(v ? "crate2nix") && (v ? "cargo")) combined);
- onlyInCrate2Nix =
- builtins.attrNames
- (lib.filterAttrs (n: v: (v ? "crate2nix") && !(v ? "cargo")) combined);
- differentFeatures = lib.filterAttrs
- (
- n: v:
- (v ? "crate2nix")
- && (v ? "cargo")
- && (v.crate2nix.features or [ ]) != (v."cargo".resolved_default_features or [ ])
- )
- combined;
- in
- builtins.toJSON {
- inherit onlyInCargo onlyInCrate2Nix differentFeatures;
- };
-
- /* Returns an attrset mapping packageId to the list of enabled features.
-
- If multiple paths to a dependency enable different features, the
- corresponding feature sets are merged. Features in rust are additive.
- */
- mergePackageFeatures =
- { crateConfigs ? crates
- , packageId
- , rootPackageId ? packageId
- , features ? rootFeatures
- , dependencyPath ? [ crates.${packageId}.crateName ]
- , featuresByPackageId ? { }
- , target
- # Adds devDependencies to the crate with rootPackageId.
- , runTests ? false
- , ...
- } @ args:
- assert (builtins.isAttrs crateConfigs);
- assert (builtins.isString packageId);
- assert (builtins.isString rootPackageId);
- assert (builtins.isList features);
- assert (builtins.isList dependencyPath);
- assert (builtins.isAttrs featuresByPackageId);
- assert (builtins.isAttrs target);
- assert (builtins.isBool runTests);
- let
- crateConfig = crateConfigs."${packageId}" or (builtins.throw "Package not found: ${packageId}");
- expandedFeatures = expandFeatures (crateConfig.features or { }) features;
- enabledFeatures = enableFeatures (crateConfig.dependencies or [ ]) expandedFeatures;
- depWithResolvedFeatures = dependency:
- let
- packageId = dependency.packageId;
- features = dependencyFeatures enabledFeatures dependency;
- in
- { inherit packageId features; };
- resolveDependencies = cache: path: dependencies:
- assert (builtins.isAttrs cache);
- assert (builtins.isList dependencies);
- let
- enabledDependencies = filterEnabledDependencies {
- inherit dependencies target;
- features = enabledFeatures;
- };
- directDependencies = map depWithResolvedFeatures enabledDependencies;
- foldOverCache = op: lib.foldl op cache directDependencies;
- in
- foldOverCache
- (
- cache: { packageId, features }:
- let
- cacheFeatures = cache.${packageId} or [ ];
- combinedFeatures = sortedUnique (cacheFeatures ++ features);
- in
- if cache ? ${packageId} && cache.${packageId} == combinedFeatures
- then cache
- else
- mergePackageFeatures {
- features = combinedFeatures;
- featuresByPackageId = cache;
- inherit crateConfigs packageId target runTests rootPackageId;
- }
- );
- cacheWithSelf =
- let
- cacheFeatures = featuresByPackageId.${packageId} or [ ];
- combinedFeatures = sortedUnique (cacheFeatures ++ enabledFeatures);
- in
- featuresByPackageId // {
- "${packageId}" = combinedFeatures;
- };
- cacheWithDependencies =
- resolveDependencies cacheWithSelf "dep"
- (
- crateConfig.dependencies or [ ]
- ++ lib.optionals
- (runTests && packageId == rootPackageId)
- (crateConfig.devDependencies or [ ])
- );
- cacheWithAll =
- resolveDependencies
- cacheWithDependencies "build"
- (crateConfig.buildDependencies or [ ]);
- in
- cacheWithAll;
-
- /* Returns the enabled dependencies given the enabled features. */
- filterEnabledDependencies = { dependencies, features, target }:
- assert (builtins.isList dependencies);
- assert (builtins.isList features);
- assert (builtins.isAttrs target);
-
- lib.filter
- (
- dep:
- let
- targetFunc = dep.target or (features: true);
- in
- targetFunc { inherit features target; }
- && (
- !(dep.optional or false)
- || builtins.any (doesFeatureEnableDependency dep) features
- )
- )
- dependencies;
-
- /* Returns whether the given feature should enable the given dependency. */
- doesFeatureEnableDependency = { name, rename ? null, ... }: feature:
- let
- prefix = "${name}/";
- len = builtins.stringLength prefix;
- startsWithPrefix = builtins.substring 0 len feature == prefix;
- in
- (rename == null && feature == name)
- || (rename != null && rename == feature)
- || startsWithPrefix;
-
- /* Returns the expanded features for the given inputFeatures by applying the
- rules in featureMap.
-
- featureMap is an attribute set which maps feature names to lists of further
- feature names to enable in case this feature is selected.
- */
- expandFeatures = featureMap: inputFeatures:
- assert (builtins.isAttrs featureMap);
- assert (builtins.isList inputFeatures);
- let
- expandFeature = feature:
- assert (builtins.isString feature);
- [ feature ] ++ (expandFeatures featureMap (featureMap."${feature}" or [ ]));
- outFeatures = lib.concatMap expandFeature inputFeatures;
- in
- sortedUnique outFeatures;
-
- /* This function adds optional dependencies as features if they are enabled
- indirectly by dependency features. This function mimics Cargo's behavior
- described in a note at:
- https://doc.rust-lang.org/nightly/cargo/reference/features.html#dependency-features
- */
- enableFeatures = dependencies: features:
- assert (builtins.isList features);
- assert (builtins.isList dependencies);
- let
- additionalFeatures = lib.concatMap
- (
- dependency:
- assert (builtins.isAttrs dependency);
- let
- enabled = builtins.any (doesFeatureEnableDependency dependency) features;
- in
- if (dependency.optional or false) && enabled then [ dependency.name ] else [ ]
- )
- dependencies;
- in
- sortedUnique (features ++ additionalFeatures);
-
- /*
- Returns the actual features for the given dependency.
-
- features: The features of the crate that refers this dependency.
- */
- dependencyFeatures = features: dependency:
- assert (builtins.isList features);
- assert (builtins.isAttrs dependency);
- let
- defaultOrNil =
- if dependency.usesDefaultFeatures or true
- then [ "default" ]
- else [ ];
- explicitFeatures = dependency.features or [ ];
- additionalDependencyFeatures =
- let
- dependencyPrefix = (dependency.rename or dependency.name) + "/";
- dependencyFeatures =
- builtins.filter (f: lib.hasPrefix dependencyPrefix f) features;
- in
- builtins.map (lib.removePrefix dependencyPrefix) dependencyFeatures;
- in
- defaultOrNil ++ explicitFeatures ++ additionalDependencyFeatures;
-
- /* Sorts and removes duplicates from a list of strings. */
- sortedUnique = features:
- assert (builtins.isList features);
- assert (builtins.all builtins.isString features);
- let
- outFeaturesSet = lib.foldl (set: feature: set // { "${feature}" = 1; }) { } features;
- outFeaturesUnique = builtins.attrNames outFeaturesSet;
- in
- builtins.sort (a: b: a < b) outFeaturesUnique;
-
- deprecationWarning = message: value:
- if strictDeprecation
- then builtins.throw "strictDeprecation enabled, aborting: ${message}"
- else builtins.trace message value;
-
- #
- # crate2nix/default.nix (excerpt end)
- #
- };
-}
-
diff --git a/nix/pkgs/cryptobox/README.md b/nix/pkgs/cryptobox/README.md
deleted file mode 100644
index babc4e88b7..0000000000
--- a/nix/pkgs/cryptobox/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# How to update
-
-```
-crate2nix source add git https://github.com/wireapp/cryptobox-c --rev $your-rev
-crate2nix generate
-```
-
diff --git a/nix/pkgs/cryptobox/crate2nix-sources.nix b/nix/pkgs/cryptobox/crate2nix-sources.nix
deleted file mode 100644
index 3e1364ba61..0000000000
--- a/nix/pkgs/cryptobox/crate2nix-sources.nix
+++ /dev/null
@@ -1,115 +0,0 @@
-# Support functions to create a nix generated workspace for out-of-tree sources.
-#
-# You do not need to check this in since it will be regenerated every time it is
-# used by crate2nix.
-#
-# This file was @generated by crate2nix 0.10.0 with the command:
-# "generate"
-#
-# See https://github.com/kolloch/crate2nix for more info.
-
-{ nixpkgs ?
-, pkgs ? import nixpkgs { }
-, lib ? pkgs.lib
- # The path to crate2nix.json.
-, crate2nixJson ? ./crate2nix.json
-}:
-
-let
- config = builtins.fromJSON (builtins.readFile crate2nixJson);
- sources = config.sources or (builtins.throw "no sources in ${crate2nixJson}");
-in
-rec {
- /* An attrset mapping a source name to its source (as a derivation). */
- fetchedSourcesByName = lib.mapAttrs internal.sourceFromConfig sources;
-
- /* A derivation building a directory symlinking all workspace member sources
- by their name.
- */
- fetchedSources =
- let sources = lib.mapAttrsToList (name: path: { inherit name path; }) fetchedSourcesByName;
- in
- pkgs.linkFarm "crate2nix-sources" sources;
-
- internal = rec {
- sourceFromConfig = name: { type, ... } @ source:
- assert builtins.isString name;
- assert builtins.isString type;
-
- if type == "Git"
- then
- pkgs.fetchgit
- {
- url = source.url;
- rev = source.rev;
- sha256 = source.sha256;
- }
- else if type == "CratesIo"
- then downloadFromCratesIo source
- else if type == "Nix"
- then resolveNix source
- else builtins.throw "Unexpected source type '${type}' for source: ${builtins.toJSON source}";
-
- /* Resolves a source configuration of type "Nix".
-
- It can either have
-
- * a `{ package = ...; ... }` path which will be resolved with pkg.callPackage
-
- * or an `{ import = ...; ... }` path which will be imported.
-
- Within that context and additional optional `attr` attribute path is resolved.
-
- E.g.
- ```nix
- {
- type = "Nix";
- import = "./nix/sources.nix";
- attr = "myPackage.release";
- }
- ```
- */
- resolveNix = { type, ... } @ source:
- assert type == "Nix";
-
- let
- attrs =
- if source ? package
- then pkgs.callPackage (./. + "/${source.package}") { }
- else if source ? "import"
- then import (./. + ''/${source."import"}'')
- else builtins.throw "Neither import nor package in nix source.";
- attrPath = lib.splitString "." source.attr;
- sourceDerivation =
- if source ? attr
- then
- lib.attrByPath
- attrPath
- (builtins.throw
- ''
- Did not find attribute '${source.attr or ""}'
- in '${source.package or source.import or "missing file"}'.
- '')
- attrs
- else attrs;
- in
- sourceDerivation;
-
- downloadFromCratesIo = { type, name, version, sha256 }:
- assert type == "CratesIo";
-
- let
- archive = pkgs.fetchurl {
- name = "${name}-${version}.tar.gz";
- url = "https://crates.io/api/v1/crates/${name}/${version}/download";
- inherit sha256;
- };
- in
- pkgs.runCommand (lib.removeSuffix ".tar.gz" name) { }
- ''
- mkdir -p $out
- tar -xzf ${archive} --strip-components=1 -C $out
- '';
- };
-}
-
diff --git a/nix/pkgs/cryptobox/crate2nix.json b/nix/pkgs/cryptobox/crate2nix.json
deleted file mode 100644
index 7cd2843f09..0000000000
--- a/nix/pkgs/cryptobox/crate2nix.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "sources": {
- "cryptobox-c": {
- "type": "Git",
- "url": "https://github.com/wireapp/cryptobox-c",
- "rev": "4067ad96b125942545dbdec8c1a89f1e1b65d013",
- "sha256": "1i9dlhw0xk1viglyhail9fb36v1awrypps8jmhrkz8k1bhx98ci3"
- }
- }
-}
\ No newline at end of file
diff --git a/nix/pkgs/cryptobox/default.nix b/nix/pkgs/cryptobox/default.nix
deleted file mode 100644
index a1784f5a2f..0000000000
--- a/nix/pkgs/cryptobox/default.nix
+++ /dev/null
@@ -1,55 +0,0 @@
-{ pkgs
-, libsodium
-, pkg-config
-, runCommand
-, stdenv
-}:
-
-let
- # load the crate2nix crate tree
- crates = import ./Cargo.nix {
- inherit pkgs;
- nixpkgs = pkgs.path;
-
- # per-crate overrides
- defaultCrateOverrides = pkgs.defaultCrateOverrides // {
- libsodium-sys = prev: {
- nativeBuildInputs = prev.nativeBuildInputs or [ ] ++ [ pkg-config ];
- buildInputs = [ libsodium ];
- };
- };
- };
-
- rootCrate = crates.rootCrate.build;
-
- # Use the correct library extension based on platform
- libExt = if stdenv.isDarwin then "dylib" else "so";
-
-in
-
-# HACK: rather than providing the multi-output crate output, expose a single-
- # output structure in the format expected by cryptobox-haskell.
- # Note it expects the library file to be called libcryptobox, not
- # libcryptobox_c, and the cbox.h to be present.
- # In the future, we might want to rework this to instead have cryptobox-c crate
- # emit a .pc file, and all downstream tooling use pkg-config to discover things,
- # but today is not that day.
-runCommand "cryptobox"
-{
- nativeBuildInputs = if stdenv.isDarwin then [ pkgs.darwin.cctools ] else [ ];
-}
- ''
- mkdir -p $out/lib $out/include
- cp ${rootCrate.lib}/lib/libcryptobox_c* $out/lib/
-
- # Fix install_name on Darwin to use absolute paths
- ${if stdenv.isDarwin then ''
- for lib in $out/lib/libcryptobox_c*.${libExt}; do
- libname=$(basename "$lib")
- install_name_tool -id "$out/lib/$libname" "$lib"
- done
- '' else ""}
-
- ln -sfn libcryptobox_c.${libExt} $out/lib/libcryptobox.${libExt}
- cp ${rootCrate.src}/src/cbox.h $out/include
- ''
diff --git a/nix/wire-server.nix b/nix/wire-server.nix
index 97926ffb53..918e959975 100644
--- a/nix/wire-server.nix
+++ b/nix/wire-server.nix
@@ -196,7 +196,7 @@ let
# so they don't depend on all the haskell dependencies. These exectuables
# are "static" from the perspective of ghc, i.e. they don't dynamically
# depend on other haskell packages but they still dynamically depend on C
- # dependencies like openssl, cryptobox, libxml2, etc. Doing this makes the
+ # dependencies like openssl, libxml2, etc. Doing this makes the
# final images that we generate much smaller as we don't have to carry
# around so files for all haskell packages.
staticExecs = localMods@{ enableOptimization, enableDocs, enableTests }:
diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal
index fd039667b1..be188db9d0 100644
--- a/services/brig/brig.cabal
+++ b/services/brig/brig.cabal
@@ -231,7 +231,6 @@ library
, conduit >=1.2.8
, containers >=0.5
, cookie >=0.4
- , cryptobox-haskell >=0.1.1
, crypton
, currency-codes >=2.0
, data-default
diff --git a/services/brig/default.nix b/services/brig/default.nix
index 550ed4212b..754e7b06b8 100644
--- a/services/brig/default.nix
+++ b/services/brig/default.nix
@@ -29,7 +29,6 @@
, conduit
, containers
, cookie
-, cryptobox-haskell
, crypton
, currency-codes
, data-default
@@ -184,7 +183,6 @@ mkDerivation {
conduit
containers
cookie
- cryptobox-haskell
crypton
currency-codes
data-default
diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs
index c3ebf2899b..b86f5508ac 100644
--- a/services/brig/src/Brig/API/Client.hs
+++ b/services/brig/src/Brig/API/Client.hs
@@ -469,7 +469,7 @@ claimLocalMultiPrekeyBundles protectee userClients = do
. Message.userClients
$ userClients
where
- getChunk :: Map UserId (Set ClientId) -> AppT r (Map UserId (Map ClientId (Maybe Prekey)))
+ getChunk :: Map UserId (Set ClientId) -> AppT r (Map UserId (Map ClientId (Maybe UncheckedPrekeyBundle)))
getChunk m = do
e <- ask
AppT $
@@ -482,13 +482,13 @@ claimLocalMultiPrekeyBundles protectee userClients = do
getUserKeys ::
UserId ->
Set ClientId ->
- (AppT r) (Map ClientId (Maybe Prekey))
+ (AppT r) (Map ClientId (Maybe UncheckedPrekeyBundle))
getUserKeys u =
sequenceA . Map.fromSet (getClientKeys u)
getClientKeys ::
UserId ->
ClientId ->
- (AppT r) (Maybe Prekey)
+ (AppT r) (Maybe UncheckedPrekeyBundle)
getClientKeys u c = do
key <- fmap prekeyData <$> wrapHttpClient (Data.claimPrekey u c)
when (isNothing key) $ noPrekeys u c
diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs
index b94e0dd00d..33248f0962 100644
--- a/services/brig/src/Brig/Data/Client.hs
+++ b/services/brig/src/Brig/Data/Client.hs
@@ -63,7 +63,6 @@ import Control.Lens
import Control.Monad.Catch
import Control.Monad.Random (randomRIO)
import Control.Retry
-import Data.ByteString.Base64 qualified as B64
import Data.ByteString.Conversion (toByteString, toByteString')
import Data.ByteString.Lazy qualified as LBS
import Data.HashMap.Strict qualified as HashMap
@@ -78,8 +77,6 @@ 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
import System.Logger.Class (field, msg, val)
import System.Logger.Class qualified as Log
import UnliftIO (pooledMapConcurrentlyN)
@@ -268,21 +265,15 @@ updateClientLastActive u c t =
updateClientLastActiveQuery
(params LocalQuorum (t, u, c))
-updatePrekeys :: (MonadClient m) => UserId -> ClientId -> [Prekey] -> ExceptT ClientDataError m ()
+updatePrekeys :: (MonadClient m) => UserId -> ClientId -> [UncheckedPrekeyBundle] -> ExceptT ClientDataError m ()
updatePrekeys u c pks = do
- plain <- mapM (hoistEither . fmapL (const MalformedPrekeys) . B64.decode . toByteString' . prekeyKey) pks
- binary <- liftIO $ zipWithM check pks plain
- unless (and binary) $
+ unless (all check pks) $
throwE MalformedPrekeys
for_ pks $ \k -> do
let args = (u, c, prekeyId k, prekeyKey k)
retry x5 $ write insertClientKey (params LocalQuorum args)
where
- check a b = do
- i <- CryptoBox.isPrekey b
- case i of
- Success n -> pure (CryptoBox.prekeyId n == keyId (prekeyId a))
- _ -> pure False
+ check pk = parsePrekeyBundlePrekeyId pk == Right (prekeyId pk)
claimPrekey ::
( Log.MonadLogger m,
@@ -315,7 +306,7 @@ claimPrekey u c =
field "user" (toByteString u)
. field "client" (toByteString c)
. msg (val "last resort prekey used")
- pure $ Just (ClientPrekey c (Prekey i k))
+ pure $ Just (ClientPrekey c (UncheckedPrekeyBundle i k))
removeAndReturnPreKey Nothing = pure Nothing
pickRandomPrekey :: (MonadIO f) => [(PrekeyId, Text)] -> f (Maybe (PrekeyId, Text))
diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs
index 6500e1fa7c..8458e31e23 100644
--- a/services/brig/test/integration/API/Provider.hs
+++ b/services/brig/test/integration/API/Provider.hs
@@ -1411,7 +1411,7 @@ getBotPreKeyIds brig bid =
. header "Z-Type" "bot"
. header "Z-Bot" (toByteString' bid)
-updateBotPrekeys :: Brig -> BotId -> [Prekey] -> Http ResponseLBS
+updateBotPrekeys :: Brig -> BotId -> [UncheckedPrekeyBundle] -> Http ResponseLBS
updateBotPrekeys brig bid prekeys =
post $
brig
@@ -1762,7 +1762,7 @@ data TestBot = TestBot
testBotConv :: !Ext.BotConvView,
testBotToken :: !Text,
testBotLastPrekey :: !LastPrekey,
- testBotPrekeys :: ![Prekey],
+ testBotPrekeys :: ![UncheckedPrekeyBundle],
testBotLocale :: !Locale,
testBotOrigin :: !Ext.BotUserView
}
diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs
index 683225bda0..917c42cef1 100644
--- a/services/brig/test/integration/API/User/Client.hs
+++ b/services/brig/test/integration/API/User/Client.hs
@@ -505,7 +505,7 @@ testClientsWithoutPrekeys brig cannon db opts = do
)
=== responseJsonEither
where
- expectedClientMap :: Domain -> UserId -> [(ClientId, Maybe Prekey)] -> QualifiedUserClientPrekeyMap
+ expectedClientMap :: Domain -> UserId -> [(ClientId, Maybe UncheckedPrekeyBundle)] -> QualifiedUserClientPrekeyMap
expectedClientMap domain u xs =
mkQualifiedUserClientPrekeyMap $
Map.singleton domain $
@@ -595,7 +595,7 @@ testClientsWithoutPrekeysV4 brig cannon db opts = do
)
=== responseJsonEither
-expectedClientMapClientsWithoutPrekeys :: Domain -> UserId -> [(ClientId, Maybe Prekey)] -> Maybe [Qualified UserId] -> QualifiedUserClientPrekeyMapV4
+expectedClientMapClientsWithoutPrekeys :: Domain -> UserId -> [(ClientId, Maybe UncheckedPrekeyBundle)] -> Maybe [Qualified UserId] -> QualifiedUserClientPrekeyMapV4
expectedClientMapClientsWithoutPrekeys domain u xs failed =
QualifiedUserClientPrekeyMapV4
{ qualifiedUserClientPrekeys =
@@ -1228,7 +1228,7 @@ testUpdateClient opts brig = do
=== statusCode
)
- checkClientPrekeys :: (HasCallStack) => Prekey -> Http ()
+ checkClientPrekeys :: (HasCallStack) => UncheckedPrekeyBundle -> Http ()
checkClientPrekeys expectedPrekey = do
flushClientPrekey >>= \case
Nothing -> error "unexpected."
diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs
index a49dace7ef..447dfb016e 100644
--- a/services/brig/test/integration/Federation/End2end.hs
+++ b/services/brig/test/integration/Federation/End2end.hs
@@ -189,7 +189,7 @@ testClaimMultiPrekeyBundleSuccess brig1 brig2 = do
(prekeys1, prekeys') = splitAt 5 prekeys
prekeys2 = take 4 prekeys'
mkClients = Set.fromList . map prekeyClient
- mkClientMap :: [ClientPrekey] -> Map ClientId (Maybe Prekey)
+ mkClientMap :: [ClientPrekey] -> Map ClientId (Maybe UncheckedPrekeyBundle)
mkClientMap = Map.fromList . map (prekeyClient &&& Just . prekeyData)
qmap :: (Ord a) => [(Qualified a, b)] -> Map Domain (Map a b)
qmap = fmap Map.fromList . indexQualified . map (sequenceAOf _1)
diff --git a/services/brig/test/integration/Federation/Util.hs b/services/brig/test/integration/Federation/Util.hs
index d042b258ff..c2cb0fbf55 100644
--- a/services/brig/test/integration/Federation/Util.hs
+++ b/services/brig/test/integration/Federation/Util.hs
@@ -89,7 +89,7 @@ withTempMockFederator opts resp action =
}
withSettingsOverrides opts' action
-generateClientPrekeys :: Brig -> [(Prekey, LastPrekey)] -> Http (Qualified UserId, [ClientPrekey])
+generateClientPrekeys :: Brig -> [(UncheckedPrekeyBundle, LastPrekey)] -> Http (Qualified UserId, [ClientPrekey])
generateClientPrekeys brig prekeys = do
quser <- userQualifiedId <$> randomUser brig
let mkClient (pk, lpk) = defNewClient PermanentClientType [pk] lpk
diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs
index 99f580235f..a4ee56fa4f 100644
--- a/services/brig/test/integration/Util.hs
+++ b/services/brig/test/integration/Util.hs
@@ -684,10 +684,10 @@ addClientReq brig uid new =
. contentJson
. body (RequestBodyLBS $ encode new)
-defNewClient :: ClientType -> [Prekey] -> LastPrekey -> NewClient
+defNewClient :: ClientType -> [UncheckedPrekeyBundle] -> LastPrekey -> NewClient
defNewClient = defNewClientWithVerificationCode Nothing
-defNewClientWithVerificationCode :: Maybe Code.Value -> ClientType -> [Prekey] -> LastPrekey -> NewClient
+defNewClientWithVerificationCode :: Maybe Code.Value -> ClientType -> [UncheckedPrekeyBundle] -> LastPrekey -> NewClient
defNewClientWithVerificationCode mbCode ty pks lpk =
(newClient ty lpk)
{ newClientPassword = Just defPassword,
@@ -903,34 +903,34 @@ defEmailLogin e = emailLogin e defPassword (Just defCookieLabel)
emailLogin :: EmailAddress -> PlainTextPassword6 -> Maybe CookieLabel -> Login
emailLogin e pw cl = MkLogin (LoginByEmail e) pw cl Nothing
-somePrekeys :: [Prekey]
+somePrekeys :: [UncheckedPrekeyBundle]
somePrekeys =
- [ Prekey (PrekeyId 1) "pQABAQECoQBYIOjl7hw0D8YRNqkkBQETCxyr7/ywE/2R5RWcUPM+GJACA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 2) "pQABAQICoQBYIGoXawUQWQ9ZW+MXhvuo9ALOBUjLff8S5VdAokN29C1OA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 3) "pQABAQMCoQBYIEjdt+YWd3lHmG8pamULLMubAMZw556IO8kW7s1MLFytA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 4) "pQABAQQCoQBYIPIaOA3Xqfk4Lh2/pU88Owd2eW5eplHpywr+Mx4QGyiMA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 5) "pQABAQUCoQBYIHnafNR4Gh3ID71lYzToewEVag4EKskDFq+gaeraOlSJA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 6) "pQABAQYCoQBYIFXUkVftE7kK22waAzhOjOmJVex3EBTU8RHZFx2o1Ed8A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 7) "pQABAQcCoQBYIDXdN8VlKb5lbgPmoDPLPyqNIEyShG4oT/DlW0peRRZUA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 8) "pQABAQgCoQBYIJH1ewvIVV3yGqQvdr/QM9HARzMgo5ksOTRyKEuN2aZzA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 9) "pQABAQkCoQBYIFcAnXdx0M1Q1hoDDfgMK9r+Zchn8YlVHHaQwQYhRk1dA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 10) "pQABAQoCoQBYIGs3vyxwmzEZ+qKNy4wpFkxc+Bgkb0D76ZEbxeeh/9DVA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 11) "pQABAQsCoQBYIGUiBeOJALP5dkMduUZ/u6MDhHNrsrBUa3f0YlSSWZbzA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 12) "pQABAQwCoQBYIMp6QNNTPDZgL3DSSD/QWWnBI7LsTZp2RhY/HLqnIwRZA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 13) "pQABAQ0CoQBYIJXSSUrE5RCNyB5pg+m6vGwK7RvJ+rs9dsdHitxnfDhuA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 14) "pQABAQ4CoQBYIHmtOX7jCKBHFDysb4H0z/QWoCSaEyjerZaT/HOP8bgDA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 15) "pQABAQ8CoQBYIIaMCTcPKj2HuYQ7i9ZaxUw9j5Bz8TPjoAaTZ5eB0w1kA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 16) "pQABARACoQBYIHWAOacKuWH81moJVveJ0FSfipWocfspOIBhaU6VLWUsA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 17) "pQABARECoQBYIA8XtUXtnMxQslULnNAeHBIivlLRe/+qdh2j6nTfDAchA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 18) "pQABARICoQBYIGgzg6SzgTTOgnk48pa6y2Rgjy004DkeBo4CMld3Jlr6A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 19) "pQABARMCoQBYIEoEFiIpCHgn74CAD+GhIfIgbQtdCqQqkOXHWxRlG6Y6A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 20) "pQABARQCoQBYINVEwTRxNSe0rxZxon4Rifz2l4rtQZn7mHtKYCiFAK9IA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 21) "pQABARUCoQBYIN3aeX2Ayi2rPFbiaYb+O2rdHUpFhzRs2j28pCmbGpflA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 22) "pQABARYCoQBYIJe5OJ17YKQrNmIH3sE++r++4Z5ld36axqAMjjQ3jtQWA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 23) "pQABARcCoQBYIASE94LjK6Raipk/lN/YewouqO+kcQGpxIqP+iW2hyHiA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 24) "pQABARgYAqEAWCBZ222LpS6/99Btlw+83PihrA655skwsNevt//8oz5axQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2",
- Prekey (PrekeyId 25) "pQABARgZAqEAWCDGEwo61w4O8T8lyw0HdoOjGWBKQUNqo6+jSfrPR9alrAOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2",
- Prekey (PrekeyId 26) "pQABARgaAqEAWCBMSQoQ6B35plB80i1O3AWlJSftCEbCbju97Iykg5+NWQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2"
+ [ UncheckedPrekeyBundle (PrekeyId 1) "pQABAQECoQBYIOjl7hw0D8YRNqkkBQETCxyr7/ywE/2R5RWcUPM+GJACA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 2) "pQABAQICoQBYIGoXawUQWQ9ZW+MXhvuo9ALOBUjLff8S5VdAokN29C1OA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 3) "pQABAQMCoQBYIEjdt+YWd3lHmG8pamULLMubAMZw556IO8kW7s1MLFytA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 4) "pQABAQQCoQBYIPIaOA3Xqfk4Lh2/pU88Owd2eW5eplHpywr+Mx4QGyiMA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 5) "pQABAQUCoQBYIHnafNR4Gh3ID71lYzToewEVag4EKskDFq+gaeraOlSJA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 6) "pQABAQYCoQBYIFXUkVftE7kK22waAzhOjOmJVex3EBTU8RHZFx2o1Ed8A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 7) "pQABAQcCoQBYIDXdN8VlKb5lbgPmoDPLPyqNIEyShG4oT/DlW0peRRZUA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 8) "pQABAQgCoQBYIJH1ewvIVV3yGqQvdr/QM9HARzMgo5ksOTRyKEuN2aZzA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 9) "pQABAQkCoQBYIFcAnXdx0M1Q1hoDDfgMK9r+Zchn8YlVHHaQwQYhRk1dA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 10) "pQABAQoCoQBYIGs3vyxwmzEZ+qKNy4wpFkxc+Bgkb0D76ZEbxeeh/9DVA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 11) "pQABAQsCoQBYIGUiBeOJALP5dkMduUZ/u6MDhHNrsrBUa3f0YlSSWZbzA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 12) "pQABAQwCoQBYIMp6QNNTPDZgL3DSSD/QWWnBI7LsTZp2RhY/HLqnIwRZA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 13) "pQABAQ0CoQBYIJXSSUrE5RCNyB5pg+m6vGwK7RvJ+rs9dsdHitxnfDhuA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 14) "pQABAQ4CoQBYIHmtOX7jCKBHFDysb4H0z/QWoCSaEyjerZaT/HOP8bgDA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 15) "pQABAQ8CoQBYIIaMCTcPKj2HuYQ7i9ZaxUw9j5Bz8TPjoAaTZ5eB0w1kA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 16) "pQABARACoQBYIHWAOacKuWH81moJVveJ0FSfipWocfspOIBhaU6VLWUsA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 17) "pQABARECoQBYIA8XtUXtnMxQslULnNAeHBIivlLRe/+qdh2j6nTfDAchA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 18) "pQABARICoQBYIGgzg6SzgTTOgnk48pa6y2Rgjy004DkeBo4CMld3Jlr6A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 19) "pQABARMCoQBYIEoEFiIpCHgn74CAD+GhIfIgbQtdCqQqkOXHWxRlG6Y6A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 20) "pQABARQCoQBYINVEwTRxNSe0rxZxon4Rifz2l4rtQZn7mHtKYCiFAK9IA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 21) "pQABARUCoQBYIN3aeX2Ayi2rPFbiaYb+O2rdHUpFhzRs2j28pCmbGpflA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 22) "pQABARYCoQBYIJe5OJ17YKQrNmIH3sE++r++4Z5ld36axqAMjjQ3jtQWA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 23) "pQABARcCoQBYIASE94LjK6Raipk/lN/YewouqO+kcQGpxIqP+iW2hyHiA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 24) "pQABARgYAqEAWCBZ222LpS6/99Btlw+83PihrA655skwsNevt//8oz5axQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2",
+ UncheckedPrekeyBundle (PrekeyId 25) "pQABARgZAqEAWCDGEwo61w4O8T8lyw0HdoOjGWBKQUNqo6+jSfrPR9alrAOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2",
+ UncheckedPrekeyBundle (PrekeyId 26) "pQABARgaAqEAWCBMSQoQ6B35plB80i1O3AWlJSftCEbCbju97Iykg5+NWQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2"
]
someLastPrekeys :: [LastPrekey]
diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs
index db530859c1..2aa4a48088 100644
--- a/services/galley/src/Galley/API/LegalHold.hs
+++ b/services/galley/src/Galley/API/LegalHold.hs
@@ -444,7 +444,7 @@ requestDevice lzusr tid uid = do
changeLegalholdStatusAndHandlePolicyConflicts tid luid userLHStatus UserLegalHoldPending
notifyClientsAboutLegalHoldRequest zusr (tUnqualified luid) lastPrekey'
- requestDeviceFromService :: Local UserId -> Sem r (LastPrekey, [Prekey])
+ requestDeviceFromService :: Local UserId -> Sem r (LastPrekey, [UncheckedPrekeyBundle])
requestDeviceFromService luid = do
LegalHoldData.dropPendingPrekeys (tUnqualified luid)
lhDevice <- LHService.requestNewDevice tid luid
diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs
index 11077e53ee..a8f1fb51c8 100644
--- a/services/galley/test/integration/API/Util.hs
+++ b/services/galley/test/integration/API/Util.hs
@@ -2390,34 +2390,34 @@ someClientId = ClientId 0xcc6e640e296e8bba
-- | Changing these will break tests; all prekeys and client Id must match the same
-- fingerprint
-somePrekeys :: [Prekey]
+somePrekeys :: [UncheckedPrekeyBundle]
somePrekeys =
- [ Prekey (PrekeyId 1) "pQABAQECoQBYIOjl7hw0D8YRNqkkBQETCxyr7/ywE/2R5RWcUPM+GJACA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 2) "pQABAQICoQBYIGoXawUQWQ9ZW+MXhvuo9ALOBUjLff8S5VdAokN29C1OA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 3) "pQABAQMCoQBYIEjdt+YWd3lHmG8pamULLMubAMZw556IO8kW7s1MLFytA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 4) "pQABAQQCoQBYIPIaOA3Xqfk4Lh2/pU88Owd2eW5eplHpywr+Mx4QGyiMA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 5) "pQABAQUCoQBYIHnafNR4Gh3ID71lYzToewEVag4EKskDFq+gaeraOlSJA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 6) "pQABAQYCoQBYIFXUkVftE7kK22waAzhOjOmJVex3EBTU8RHZFx2o1Ed8A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 7) "pQABAQcCoQBYIDXdN8VlKb5lbgPmoDPLPyqNIEyShG4oT/DlW0peRRZUA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 8) "pQABAQgCoQBYIJH1ewvIVV3yGqQvdr/QM9HARzMgo5ksOTRyKEuN2aZzA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 9) "pQABAQkCoQBYIFcAnXdx0M1Q1hoDDfgMK9r+Zchn8YlVHHaQwQYhRk1dA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 10) "pQABAQoCoQBYIGs3vyxwmzEZ+qKNy4wpFkxc+Bgkb0D76ZEbxeeh/9DVA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 11) "pQABAQsCoQBYIGUiBeOJALP5dkMduUZ/u6MDhHNrsrBUa3f0YlSSWZbzA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 12) "pQABAQwCoQBYIMp6QNNTPDZgL3DSSD/QWWnBI7LsTZp2RhY/HLqnIwRZA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 13) "pQABAQ0CoQBYIJXSSUrE5RCNyB5pg+m6vGwK7RvJ+rs9dsdHitxnfDhuA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 14) "pQABAQ4CoQBYIHmtOX7jCKBHFDysb4H0z/QWoCSaEyjerZaT/HOP8bgDA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 15) "pQABAQ8CoQBYIIaMCTcPKj2HuYQ7i9ZaxUw9j5Bz8TPjoAaTZ5eB0w1kA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 16) "pQABARACoQBYIHWAOacKuWH81moJVveJ0FSfipWocfspOIBhaU6VLWUsA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 17) "pQABARECoQBYIA8XtUXtnMxQslULnNAeHBIivlLRe/+qdh2j6nTfDAchA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 18) "pQABARICoQBYIGgzg6SzgTTOgnk48pa6y2Rgjy004DkeBo4CMld3Jlr6A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 19) "pQABARMCoQBYIEoEFiIpCHgn74CAD+GhIfIgbQtdCqQqkOXHWxRlG6Y6A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 20) "pQABARQCoQBYINVEwTRxNSe0rxZxon4Rifz2l4rtQZn7mHtKYCiFAK9IA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 21) "pQABARUCoQBYIN3aeX2Ayi2rPFbiaYb+O2rdHUpFhzRs2j28pCmbGpflA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 22) "pQABARYCoQBYIJe5OJ17YKQrNmIH3sE++r++4Z5ld36axqAMjjQ3jtQWA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 23) "pQABARcCoQBYIASE94LjK6Raipk/lN/YewouqO+kcQGpxIqP+iW2hyHiA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
- Prekey (PrekeyId 24) "pQABARgYAqEAWCBZ222LpS6/99Btlw+83PihrA655skwsNevt//8oz5axQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2",
- Prekey (PrekeyId 25) "pQABARgZAqEAWCDGEwo61w4O8T8lyw0HdoOjGWBKQUNqo6+jSfrPR9alrAOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2",
- Prekey (PrekeyId 26) "pQABARgaAqEAWCBMSQoQ6B35plC80i1O3AWlJSftCEbCbju97Iykg5+NWQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2"
+ [ UncheckedPrekeyBundle (PrekeyId 1) "pQABAQECoQBYIOjl7hw0D8YRNqkkBQETCxyr7/ywE/2R5RWcUPM+GJACA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 2) "pQABAQICoQBYIGoXawUQWQ9ZW+MXhvuo9ALOBUjLff8S5VdAokN29C1OA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 3) "pQABAQMCoQBYIEjdt+YWd3lHmG8pamULLMubAMZw556IO8kW7s1MLFytA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 4) "pQABAQQCoQBYIPIaOA3Xqfk4Lh2/pU88Owd2eW5eplHpywr+Mx4QGyiMA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 5) "pQABAQUCoQBYIHnafNR4Gh3ID71lYzToewEVag4EKskDFq+gaeraOlSJA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 6) "pQABAQYCoQBYIFXUkVftE7kK22waAzhOjOmJVex3EBTU8RHZFx2o1Ed8A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 7) "pQABAQcCoQBYIDXdN8VlKb5lbgPmoDPLPyqNIEyShG4oT/DlW0peRRZUA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 8) "pQABAQgCoQBYIJH1ewvIVV3yGqQvdr/QM9HARzMgo5ksOTRyKEuN2aZzA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 9) "pQABAQkCoQBYIFcAnXdx0M1Q1hoDDfgMK9r+Zchn8YlVHHaQwQYhRk1dA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 10) "pQABAQoCoQBYIGs3vyxwmzEZ+qKNy4wpFkxc+Bgkb0D76ZEbxeeh/9DVA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 11) "pQABAQsCoQBYIGUiBeOJALP5dkMduUZ/u6MDhHNrsrBUa3f0YlSSWZbzA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 12) "pQABAQwCoQBYIMp6QNNTPDZgL3DSSD/QWWnBI7LsTZp2RhY/HLqnIwRZA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 13) "pQABAQ0CoQBYIJXSSUrE5RCNyB5pg+m6vGwK7RvJ+rs9dsdHitxnfDhuA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 14) "pQABAQ4CoQBYIHmtOX7jCKBHFDysb4H0z/QWoCSaEyjerZaT/HOP8bgDA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 15) "pQABAQ8CoQBYIIaMCTcPKj2HuYQ7i9ZaxUw9j5Bz8TPjoAaTZ5eB0w1kA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 16) "pQABARACoQBYIHWAOacKuWH81moJVveJ0FSfipWocfspOIBhaU6VLWUsA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 17) "pQABARECoQBYIA8XtUXtnMxQslULnNAeHBIivlLRe/+qdh2j6nTfDAchA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 18) "pQABARICoQBYIGgzg6SzgTTOgnk48pa6y2Rgjy004DkeBo4CMld3Jlr6A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 19) "pQABARMCoQBYIEoEFiIpCHgn74CAD+GhIfIgbQtdCqQqkOXHWxRlG6Y6A6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 20) "pQABARQCoQBYINVEwTRxNSe0rxZxon4Rifz2l4rtQZn7mHtKYCiFAK9IA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 21) "pQABARUCoQBYIN3aeX2Ayi2rPFbiaYb+O2rdHUpFhzRs2j28pCmbGpflA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 22) "pQABARYCoQBYIJe5OJ17YKQrNmIH3sE++r++4Z5ld36axqAMjjQ3jtQWA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 23) "pQABARcCoQBYIASE94LjK6Raipk/lN/YewouqO+kcQGpxIqP+iW2hyHiA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY=",
+ UncheckedPrekeyBundle (PrekeyId 24) "pQABARgYAqEAWCBZ222LpS6/99Btlw+83PihrA655skwsNevt//8oz5axQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2",
+ UncheckedPrekeyBundle (PrekeyId 25) "pQABARgZAqEAWCDGEwo61w4O8T8lyw0HdoOjGWBKQUNqo6+jSfrPR9alrAOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2",
+ UncheckedPrekeyBundle (PrekeyId 26) "pQABARgaAqEAWCBMSQoQ6B35plC80i1O3AWlJSftCEbCbju97Iykg5+NWQOhAKEAWCCy39UyMEgetquvTo7P19bcyfnWBzQMOEG1v+0wub0magT2"
]
-- | Changing these will break tests; all prekeys and client Id must match the same
diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs
index 6c1735c98b..6e76db52e8 100644
--- a/services/spar/test-integration/Test/Spar/APISpec.hs
+++ b/services/spar/test-integration/Test/Spar/APISpec.hs
@@ -1624,8 +1624,8 @@ specReAuthSsoUserWithPassword =
let actual = Vec.length <$> (preview _Array =<< responseJsonMaybe @Value r)
lift $ actual `shouldBe` Just expected
- prekey :: Prekey
- prekey = Prekey (PrekeyId 1) "pQABAQECoQBYIOjl7hw0D8YRNqkkBQETCxyr7/ywE/2R5RWcUPM+GJACA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY="
+ prekey :: UncheckedPrekeyBundle
+ prekey = UncheckedPrekeyBundle (PrekeyId 1) "pQABAQECoQBYIOjl7hw0D8YRNqkkBQETCxyr7/ywE/2R5RWcUPM+GJACA6EAoQBYILLf1TIwSB62q69Ojs/X1tzJ+dYHNAw4QbW/7TC5vSZqBPY="
lPrekey :: LastPrekey
lPrekey = lastPrekey "pQABARn//wKhAFggnCcZIK1pbtlJf4wRQ44h4w7/sfSgj5oWXMQaUGYAJ/sDoQChAFgglacihnqg/YQJHkuHNFU7QD6Pb3KN4FnubaCF2EVOgRkE9g=="
@@ -1644,7 +1644,7 @@ specReAuthSsoUserWithPassword =
)
pure $ c.clientId
- defNewClient :: ClientType -> [Prekey] -> LastPrekey -> NewClient
+ defNewClient :: ClientType -> [UncheckedPrekeyBundle] -> LastPrekey -> NewClient
defNewClient ty pks lpk =
(newClient ty lpk)
{ newClientPassword = Just defPassword,
From b7f2341128b3c41875f6804b187d7f190f525417 Mon Sep 17 00:00:00 2001
From: jschaul
Date: Wed, 21 Jan 2026 10:52:36 +0100
Subject: [PATCH 19/26] helm: drop support for ingress below k8s 1.27 (#4969)
Compatibility k8s 1.18 - >=1.25 was introduced in 58b761737a449e7a8b781d90434f1ac1a3f0b9d8 and some other PRs, but we no longer need support for ancient kubernetes versions now. This deletion of code should improve the readability of our helm charts.
---
changelog.d/0-release-notes/old-kubernetes | 1 +
charts/integration/templates/_helpers.tpl | 26 ---------
charts/integration/templates/ingress.yaml | 19 +------
charts/legalhold/templates/_helpers.tpl | 26 ---------
charts/legalhold/templates/ingress.yaml | 12 +----
.../templates/_helpers.tpl | 28 +---------
.../templates/ingress.yaml | 53 ++-----------------
.../templates/ingress_federator.yaml | 17 +-----
.../templates/ingress_minio.yaml | 17 +-----
charts/outlook-addin/templates/_helpers.tpl | 31 -----------
charts/outlook-addin/templates/ingress.yaml | 10 +---
hack/bin/integration-setup-federation.sh | 7 ---
hack/bin/integration-teardown-federation.sh | 1 -
hack/helm_vars/common.yaml.gotmpl | 2 +-
14 files changed, 14 insertions(+), 236 deletions(-)
create mode 100644 changelog.d/0-release-notes/old-kubernetes
delete mode 100644 charts/legalhold/templates/_helpers.tpl
diff --git a/changelog.d/0-release-notes/old-kubernetes b/changelog.d/0-release-notes/old-kubernetes
new file mode 100644
index 0000000000..b8e99353a0
--- /dev/null
+++ b/changelog.d/0-release-notes/old-kubernetes
@@ -0,0 +1 @@
+Drop support for kubernetes versions below 1.27
diff --git a/charts/integration/templates/_helpers.tpl b/charts/integration/templates/_helpers.tpl
index 68e9c25138..e3a33787bf 100644
--- a/charts/integration/templates/_helpers.tpl
+++ b/charts/integration/templates/_helpers.tpl
@@ -8,32 +8,6 @@
{{- (semverCompare ">= 1.24-0" (include "kubeVersion" .)) -}}
{{- end -}}
-{{/* Get Ingress API Version */}}
-{{- define "ingress.apiVersion" -}}
- {{- if and ($.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" (include "kubeVersion" .)) -}}
- {{- print "networking.k8s.io/v1" -}}
- {{- else if $.Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -}}
- {{- print "networking.k8s.io/v1beta1" -}}
- {{- else -}}
- {{- print "extensions/v1beta1" -}}
- {{- end -}}
-{{- end -}}
-
-{{/* Check Ingress stability */}}
-{{- define "ingress.isStable" -}}
- {{- eq (include "ingress.apiVersion" .) "networking.k8s.io/v1" -}}
-{{- end -}}
-
-{{/* Check Ingress supports pathType */}}
-{{/* pathType was added to networking.k8s.io/v1beta1 in Kubernetes 1.18 */}}
-{{- define "ingress.supportsPathType" -}}
- {{- or (eq (include "ingress.isStable" .) "true") (and (eq (include "ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" (include "kubeVersion" .))) -}}
-{{- end -}}
-
-{{- define "ingress.FieldNotAnnotation" -}}
- {{- (semverCompare ">= 1.27-0" (include "kubeVersion" .)) -}}
-{{- end -}}
-
{{- define "integrationTestHelperNewLabels" -}}
{{- (semverCompare ">= 1.23-0" (include "kubeVersion" .)) -}}
{{- end -}}
diff --git a/charts/integration/templates/ingress.yaml b/charts/integration/templates/ingress.yaml
index 7d2748022f..362b7b0d8f 100644
--- a/charts/integration/templates/ingress.yaml
+++ b/charts/integration/templates/ingress.yaml
@@ -1,18 +1,10 @@
-{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}}
-{{- $ingressFieldNotAnnotation := eq (include "ingress.FieldNotAnnotation" .) "true" -}}
-{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}}
-{{- $ingressAPIVersion := include "ingress.apiVersion" . }}
-
{{- range $name, $dynamicBackend := .Values.config.dynamicBackends }}
---
-apiVersion: {{ $ingressAPIVersion }}
+apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: integration-federator-{{ $name }}
annotations:
- {{- if not $ingressFieldNotAnnotation }}
- kubernetes.io/ingress.class: "{{ $.Values.ingress.class }}"
- {{- end }}
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
@@ -21,9 +13,7 @@ metadata:
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header "X-SSL-Certificate" $ssl_client_escaped_cert;
spec:
- {{- if $ingressFieldNotAnnotation }}
ingressClassName: "{{ $.Values.ingress.class }}"
- {{- end }}
tls:
- hosts:
- {{ $dynamicBackend.federatorExternalHostPrefix }}.{{ $.Release.Namespace }}.svc.cluster.local
@@ -33,17 +23,10 @@ spec:
http:
paths:
- path: /
- {{- if $ingressSupportsPathType }}
pathType: Prefix
- {{- end }}
backend:
- {{- if $apiIsStable }}
service:
name: integration
port:
number: {{ $dynamicBackend.federatorExternalPort }}
- {{- else }}
- serviceName: integration
- servicePort: {{ $dynamicBackend.federatorExternalPort }}
- {{- end }}
{{- end }}
diff --git a/charts/legalhold/templates/_helpers.tpl b/charts/legalhold/templates/_helpers.tpl
deleted file mode 100644
index c0e9c95498..0000000000
--- a/charts/legalhold/templates/_helpers.tpl
+++ /dev/null
@@ -1,26 +0,0 @@
-{{/* Allow KubeVersion to be overridden. */}}
-{{- define "kubeVersion" -}}
- {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}}
-{{- end -}}
-
-{{/* Get Ingress API Version */}}
-{{- define "ingress.apiVersion" -}}
- {{- if and (.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" (include "kubeVersion" .)) -}}
- {{- print "networking.k8s.io/v1" -}}
- {{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -}}
- {{- print "networking.k8s.io/v1beta1" -}}
- {{- else -}}
- {{- print "extensions/v1beta1" -}}
- {{- end -}}
-{{- end -}}
-
-{{/* Check Ingress stability */}}
-{{- define "ingress.isStable" -}}
- {{- eq (include "ingress.apiVersion" .) "networking.k8s.io/v1" -}}
-{{- end -}}
-
-{{/* Check Ingress supports pathType */}}
-{{/* pathType was added to networking.k8s.io/v1beta1 in Kubernetes 1.18 */}}
-{{- define "ingress.supportsPathType" -}}
- {{- or (eq (include "ingress.isStable" .) "true") (and (eq (include "ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" (include "kubeVersion" .))) -}}
-{{- end -}}
diff --git a/charts/legalhold/templates/ingress.yaml b/charts/legalhold/templates/ingress.yaml
index 24cfcd9813..b1a530f0f1 100644
--- a/charts/legalhold/templates/ingress.yaml
+++ b/charts/legalhold/templates/ingress.yaml
@@ -1,12 +1,11 @@
-{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}}
-{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}}
-apiVersion: {{ include "ingress.apiVersion" . }}
+apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hold
annotations:
kubernetes.io/ingress.class: nginx
spec:
+ ingressClassName: nginx
# This assumes you have created the given cert
# https://github.com/kubernetes/ingress-nginx/blob/master/docs/examples/PREREQUISITES.md#tls-certificates
tls:
@@ -18,16 +17,9 @@ spec:
http:
paths:
- path: /
- {{- if $ingressSupportsPathType }}
pathType: Prefix
- {{- end }}
backend:
- {{- if $apiIsStable }}
service:
name: "{{ .Release.Name }}-hold"
port:
number: 8080
- {{- else }}
- serviceName: "{{ .Release.Name }}-hold"
- servicePort: 8080
- {{- end }}
diff --git a/charts/nginx-ingress-services/templates/_helpers.tpl b/charts/nginx-ingress-services/templates/_helpers.tpl
index 71c2eab678..63095902b4 100644
--- a/charts/nginx-ingress-services/templates/_helpers.tpl
+++ b/charts/nginx-ingress-services/templates/_helpers.tpl
@@ -62,39 +62,13 @@ Returns the Letsencrypt API server URL based on whether testMode is enabled or d
{{/* Allow KubeVersion to be overridden. */}}
{{- define "kubeVersion" -}}
- {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}}
-{{- end -}}
-
-{{/* Get Ingress API Version */}}
-{{- define "ingress.apiVersion" -}}
- {{- if and (.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" (include "kubeVersion" .)) -}}
- {{- print "networking.k8s.io/v1" -}}
- {{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -}}
- {{- print "networking.k8s.io/v1beta1" -}}
- {{- else -}}
- {{- print "extensions/v1beta1" -}}
- {{- end -}}
-{{- end -}}
-
-{{/* Check Ingress stability */}}
-{{- define "ingress.isStable" -}}
- {{- eq (include "ingress.apiVersion" .) "networking.k8s.io/v1" -}}
-{{- end -}}
-
-{{/* Check Ingress supports pathType */}}
-{{/* pathType was added to networking.k8s.io/v1beta1 in Kubernetes 1.18 */}}
-{{- define "ingress.supportsPathType" -}}
- {{- or (eq (include "ingress.isStable" .) "true") (and (eq (include "ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" (include "kubeVersion" .))) -}}
+{{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}}
{{- end -}}
{{- define "integrationTestHelperNewLabels" -}}
{{- (semverCompare ">= 1.23-0" (include "kubeVersion" .)) -}}
{{- end -}}
-{{- define "ingress.FieldNotAnnotation" -}}
- {{- (semverCompare ">= 1.27-0" (include "kubeVersion" .)) -}}
-{{- end -}}
-
{{/*
Name of the ingress. Extracted as helper to reduce the complexity in the template
itself. The default name is 'nginx-ingress' for backwards compatibility (it has
diff --git a/charts/nginx-ingress-services/templates/ingress.yaml b/charts/nginx-ingress-services/templates/ingress.yaml
index 428fe88c8b..acca141ae0 100644
--- a/charts/nginx-ingress-services/templates/ingress.yaml
+++ b/charts/nginx-ingress-services/templates/ingress.yaml
@@ -1,18 +1,12 @@
-{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}}
-{{- $ingressFieldNotAnnotation := eq (include "ingress.FieldNotAnnotation" .) "true" -}}
-{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}}
-apiVersion: {{ include "ingress.apiVersion" . }}
+apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "nginx-ingress-services.getIngressName" . | quote }}
+ {{- if .Values.config.renderCSPInIngress }}
annotations:
- {{- if not $ingressFieldNotAnnotation }}
- kubernetes.io/ingress.class: "{{ .Values.config.ingressClass }}"
- {{- end }}
- {{ if .Values.config.renderCSPInIngress }}
- {{ if not (contains .Values.config.ingressClass "nginx") }}
+ {{- if not (contains .Values.config.ingressClass "nginx") }}
{{ fail "In ingress CSP header setting only works with a 'nginx' controller. (Rename it to 'nginx-*' if it is one.)" }}
- {{ end }}
+ {{- end }}
{{/* We need to add CSP headers here for webapp, team-settings and
account-pages requests, because they cannot do it on their own in the
multi-ingress case.
@@ -44,11 +38,9 @@ metadata:
set $CSP "${CSP} upgrade-insecure-requests";
more_set_headers "content-security-policy: $CSP";
}
- {{ end }}
+ {{- end }}
spec:
- {{- if $ingressFieldNotAnnotation }}
ingressClassName: "{{ .Values.config.ingressClass }}"
- {{- end }}
# This assumes you have created the given cert (see secret.yaml)
# https://github.com/kubernetes/ingress-nginx/blob/master/docs/examples/PREREQUISITES.md#tls-certificates
tls:
@@ -72,92 +64,57 @@ spec:
http:
paths:
- path: /
- {{- if $ingressSupportsPathType }}
pathType: Prefix
- {{- end }}
backend:
- {{- if $apiIsStable }}
service:
name: nginz
port:
name: http
- {{- else }}
- serviceName: nginz
- servicePort: http
- {{- end }}
{{- if .Values.websockets.enabled }}
- host: {{ .Values.config.dns.ssl }}
http:
paths:
- path: /
- {{- if $ingressSupportsPathType }}
pathType: Prefix
- {{- end }}
backend:
- {{- if $apiIsStable }}
service:
name: nginz
port:
name: ws
- {{- else }}
- serviceName: nginz
- servicePort: ws
- {{- end }}
{{- end }}
{{- if .Values.webapp.enabled }}
- host: {{ .Values.config.dns.webapp }}
http:
paths:
- path: /
- {{- if $ingressSupportsPathType }}
pathType: Prefix
- {{- end }}
backend:
- {{- if $apiIsStable }}
service:
name: webapp-http
port:
number: {{ .Values.service.webapp.externalPort }}
- {{- else }}
- serviceName: webapp-http
- servicePort: {{ .Values.service.webapp.externalPort }}
- {{- end }}
{{- end }}
{{- if .Values.teamSettings.enabled }}
- host: {{ .Values.config.dns.teamSettings }}
http:
paths:
- path: /
- {{- if $ingressSupportsPathType }}
pathType: Prefix
- {{- end }}
backend:
- {{- if $apiIsStable }}
service:
name: team-settings-http
port:
number: {{ .Values.service.teamSettings.externalPort }}
- {{- else }}
- serviceName: team-settings-http
- servicePort: {{ .Values.service.teamSettings.externalPort }}
- {{- end }}
{{- end }}
{{- if .Values.accountPages.enabled }}
- host: {{ .Values.config.dns.accountPages }}
http:
paths:
- path: /
- {{- if $ingressSupportsPathType }}
pathType: Prefix
- {{- end }}
backend:
- {{- if $apiIsStable }}
service:
name: account-pages-http
port:
number: {{ .Values.service.accountPages.externalPort }}
- {{- else }}
- serviceName: account-pages-http
- servicePort: {{ .Values.service.accountPages.externalPort }}
- {{- end }}
{{- end }}
diff --git a/charts/nginx-ingress-services/templates/ingress_federator.yaml b/charts/nginx-ingress-services/templates/ingress_federator.yaml
index fa76aae8d9..4602fe9811 100644
--- a/charts/nginx-ingress-services/templates/ingress_federator.yaml
+++ b/charts/nginx-ingress-services/templates/ingress_federator.yaml
@@ -1,20 +1,14 @@
-{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}}
-{{- $ingressFieldNotAnnotation := eq (include "ingress.FieldNotAnnotation" .) "true" -}}
-{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}}
{{- if .Values.federator.enabled }}
{{- if .Values.config.isAdditionalIngress -}}
{{ fail "Federation and multi-backend-domain (multi-ingress) cannot be configured together." }}
{{- end -}}
# We use a separate ingress for federator so that we can require client
# certificates only for federation requests
-apiVersion: {{ include "ingress.apiVersion" . }}
+apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: federator-ingress
annotations:
- {{- if not $ingressFieldNotAnnotation }}
- kubernetes.io/ingress.class: "{{ .Values.config.ingressClass }}"
- {{- end }}
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
@@ -23,9 +17,7 @@ metadata:
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header "X-SSL-Certificate" $ssl_client_escaped_cert;
spec:
- {{- if $ingressFieldNotAnnotation }}
ingressClassName: "{{ .Values.config.ingressClass }}"
- {{- end }}
tls:
- hosts:
- {{ .Values.config.dns.federator }}
@@ -35,17 +27,10 @@ spec:
http:
paths:
- path: /
- {{- if $ingressSupportsPathType }}
pathType: Prefix
- {{- end }}
backend:
- {{- if $apiIsStable }}
service:
name: federator
port:
name: federator-ext
- {{- else }}
- serviceName: federator
- servicePort: federator-ext # name must be below 15 chars
- {{- end }}
{{- end }}
diff --git a/charts/nginx-ingress-services/templates/ingress_minio.yaml b/charts/nginx-ingress-services/templates/ingress_minio.yaml
index 6c225925ea..fed523e9e9 100644
--- a/charts/nginx-ingress-services/templates/ingress_minio.yaml
+++ b/charts/nginx-ingress-services/templates/ingress_minio.yaml
@@ -1,25 +1,17 @@
-{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}}
-{{- $ingressFieldNotAnnotation := eq (include "ingress.FieldNotAnnotation" .) "true" -}}
-{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}}
{{- if .Values.fakeS3.enabled }}
# We use a separate ingress for minio because we want to restrict access to /minio/ path
# for security reasons
-apiVersion: {{ include "ingress.apiVersion" . }}
+apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "nginx-ingress-services.getMinioIngressName" . | quote }}
annotations:
- {{- if not $ingressFieldNotAnnotation }}
- kubernetes.io/ingress.class: "{{ .Values.config.ingressClass }}"
- {{- end }}
nginx.ingress.kubernetes.io/server-snippet: |
location /minio/ {
return 403;
}
spec:
- {{- if $ingressFieldNotAnnotation }}
ingressClassName: "{{ .Values.config.ingressClass }}"
- {{- end }}
tls:
- hosts:
- {{ .Values.config.dns.fakeS3 }}
@@ -29,17 +21,10 @@ spec:
http:
paths:
- path: /
- {{- if $ingressSupportsPathType }}
pathType: Prefix
- {{- end }}
backend:
- {{- if $apiIsStable }}
service:
name: {{ .Values.service.s3.serviceName }}
port:
number: {{ .Values.service.s3.externalPort }}
- {{- else }}
- serviceName: {{ .Values.service.s3.serviceName }}
- servicePort: {{ .Values.service.s3.externalPort }}
- {{- end }}
{{- end }}
diff --git a/charts/outlook-addin/templates/_helpers.tpl b/charts/outlook-addin/templates/_helpers.tpl
index c2f40c04c9..3d27c4ba75 100644
--- a/charts/outlook-addin/templates/_helpers.tpl
+++ b/charts/outlook-addin/templates/_helpers.tpl
@@ -49,34 +49,3 @@ Selector labels
app.kubernetes.io/name: {{ include "outlook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
-
-{{/* Allow KubeVersion to be overridden. */}}
-{{- define "kubeVersion" -}}
- {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride -}}
-{{- end -}}
-
-{{/* Get Ingress API Version */}}
-{{- define "ingress.apiVersion" -}}
- {{- if and (.Capabilities.APIVersions.Has "networking.k8s.io/v1") (semverCompare ">= 1.19-0" (include "kubeVersion" .)) -}}
- {{- print "networking.k8s.io/v1" -}}
- {{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -}}
- {{- print "networking.k8s.io/v1beta1" -}}
- {{- else -}}
- {{- print "extensions/v1beta1" -}}
- {{- end -}}
-{{- end -}}
-
-{{/* Check Ingress stability */}}
-{{- define "ingress.isStable" -}}
- {{- eq (include "ingress.apiVersion" .) "networking.k8s.io/v1" -}}
-{{- end -}}
-
-{{/* Check Ingress supports pathType */}}
-{{/* pathType was added to networking.k8s.io/v1beta1 in Kubernetes 1.18 */}}
-{{- define "ingress.supportsPathType" -}}
- {{- or (eq (include "ingress.isStable" .) "true") (and (eq (include "ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" (include "kubeVersion" .))) -}}
-{{- end -}}
-
-{{- define "ingress.FieldNotAnnotation" -}}
- {{- (semverCompare ">= 1.27-0" (include "kubeVersion" .)) -}}
-{{- end -}}
diff --git a/charts/outlook-addin/templates/ingress.yaml b/charts/outlook-addin/templates/ingress.yaml
index f006d3dc0e..aedc8885fe 100644
--- a/charts/outlook-addin/templates/ingress.yaml
+++ b/charts/outlook-addin/templates/ingress.yaml
@@ -1,22 +1,14 @@
-{{- $apiIsStable := eq (include "ingress.isStable" .) "true" -}}
-{{- $ingressFieldNotAnnotation := eq (include "ingress.FieldNotAnnotation" .) "true" -}}
-{{- $ingressSupportsPathType := eq (include "ingress.supportsPathType" .) "true" -}}
-apiVersion: {{ include "ingress.apiVersion" . }}
+apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: "{{ include "outlook.fullname" . }}"
labels:
{{- include "outlook.labels" . | nindent 4 }}
annotations:
- {{- if not $ingressFieldNotAnnotation }}
- kubernetes.io/ingress.class: "{{ .Values.config.ingressClass }}"
- {{- end }}
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "{{ required "Must specify allowOrigin" .Values.allowOrigin }}"
spec:
- {{- if $ingressFieldNotAnnotation }}
ingressClassName: "{{ .Values.config.ingressClass }}"
- {{- end }}
tls:
- hosts:
- "{{ .Values.host }}"
diff --git a/hack/bin/integration-setup-federation.sh b/hack/bin/integration-setup-federation.sh
index 17be6acfc9..355abb417a 100755
--- a/hack/bin/integration-setup-federation.sh
+++ b/hack/bin/integration-setup-federation.sh
@@ -26,13 +26,6 @@ charts=(fake-aws databases-ephemeral rabbitmq wire-server ingress-nginx-controll
mkdir -p ~/.parallel && touch ~/.parallel/will-cite
printf '%s\n' "${charts[@]}" | parallel -P "${HELM_PARALLELISM}" "$DIR/update.sh" "$CHARTS_DIR/{}"
-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="$KUBERNETES_VERSION_MAJOR.$KUBERNETES_VERSION_MINOR"
-export INGRESS_CHART="ingress-nginx-controller"
-
-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/bin/integration-teardown-federation.sh b/hack/bin/integration-teardown-federation.sh
index 9b97eed327..7f5fdacd67 100755
--- a/hack/bin/integration-teardown-federation.sh
+++ b/hack/bin/integration-teardown-federation.sh
@@ -16,7 +16,6 @@ export FEDERATION_DOMAIN_BASE_1="."
export FEDERATION_DOMAIN_BASE_2="."
export FEDERATION_CA_CERTIFICATE="."
export ENTERPRISE_IMAGE_PULL_SECRET="."
-export INGRESS_CHART="ingress-nginx-controller"
# shellcheck disable=SC1091
. "$DIR/helm_overrides.sh"
diff --git a/hack/helm_vars/common.yaml.gotmpl b/hack/helm_vars/common.yaml.gotmpl
index f9878f508b..bce592348c 100644
--- a/hack/helm_vars/common.yaml.gotmpl
+++ b/hack/helm_vars/common.yaml.gotmpl
@@ -5,7 +5,7 @@ namespace2: {{ requiredEnv "NAMESPACE_2" }}
federationDomain2: {{ requiredEnv "FEDERATION_DOMAIN_2" }}
federationDomainBase2: {{ requiredEnv "FEDERATION_DOMAIN_BASE_2" }}
federationCACertificate: {{ requiredEnv "FEDERATION_CA_CERTIFICATE" | quote }}
-ingressChart: {{ requiredEnv "INGRESS_CHART" }}
+ingressChart: ingress-nginx-controller
rabbitmqUsername: guest
rabbitmqPassword: guest
From 294ee04f57c7a969f96669a65d533e0c94dd8240 Mon Sep 17 00:00:00 2001
From: Matthias Fischmann
Date: Wed, 21 Jan 2026 14:30:58 +0100
Subject: [PATCH 20/26] [WPB-21366] Refactor brig-index cli to use brig.yaml
(#4957)
* Changelog.
* Integration test.
* Postgres cli args in brig-index & fix type field in search.
* Work on Makefile.
- new target "es-migrate" (must be separate from "es-reset" because to avoid cyclical dep with `make cr`)
- "cr" target now uses "es-reset" as dependency
- new target "postgres-migrate", make it dependency of "db-migrate"
- fix phonyness annotation of "postgres-schema"
* Haddocks.
---
Makefile | 47 ++++----
.../WPB-21366-refactor-brig-index-cli | 1 +
integration/test/Test/Apps.hs | 18 ++-
libs/types-common/src/Data/Misc.hs | 8 ++
.../src/Wire/IndexedUserStore/Bulk.hs | 1 +
.../IndexedUserStore/Bulk/ElasticSearch.hs | 31 ++++-
.../test/resources/postgres-credentials.yaml | 1 +
services/brig/src/Brig/Index/Eval.hs | 30 +++--
services/brig/src/Brig/Index/Options.hs | 107 ++++++++++++++++--
services/brig/test/integration/API/Search.hs | 16 +--
10 files changed, 204 insertions(+), 56 deletions(-)
create mode 100644 changelog.d/0-release-notes/WPB-21366-refactor-brig-index-cli
create mode 100644 libs/wire-subsystems/test/resources/postgres-credentials.yaml
diff --git a/Makefile b/Makefile
index eda5ea8ba1..556e1056df 100644
--- a/Makefile
+++ b/Makefile
@@ -338,7 +338,7 @@ cassandra-schema: db-migrate cassandra-schema-impl
cassandra-schema-impl:
./hack/bin/cassandra_dump_schema > ./cassandra-schema.cql
-.PHONY: postgres-reset postgres-schema-impl
+.PHONY: postgres-schema
postgres-schema: postgres-reset postgres-schema-impl
.PHONY: postgres-schema-impl
@@ -393,21 +393,43 @@ postgres-reset: c
./dist/brig -c ./services/brig/brig.integration.yaml migrate-postgres --reset --dbname dyn-2
./dist/brig -c ./services/brig/brig.integration.yaml migrate-postgres --reset --dbname dyn-3
+.PHONY: postgres-migrate
+postgres-migrate: c
+ ./dist/brig -c ./services/brig/brig.integration.yaml migrate-postgres --dbname backendA
+ ./dist/brig -c ./services/brig/brig.integration.yaml migrate-postgres --dbname backendB
+ ./dist/brig -c ./services/brig/brig.integration.yaml migrate-postgres --dbname dyn-1
+ ./dist/brig -c ./services/brig/brig.integration.yaml migrate-postgres --dbname dyn-2
+ ./dist/brig -c ./services/brig/brig.integration.yaml migrate-postgres --dbname dyn-3
+
.PHONY: es-reset
es-reset: c
./dist/brig-index reset \
--elasticsearch-index-prefix directory \
--elasticsearch-server https://localhost:9200 \
- --elasticsearch-ca-cert ./libs/wire-subsystems/test/resources/elasticsearch-ca.pem \
+ --elasticsearch-ca-cert ./libs/wire-subsystems/test/resources/elasticsearch-ca.pem \
--elasticsearch-credentials ./libs/wire-subsystems/test/resources/elasticsearch-credentials.yaml > /dev/null
./dist/brig-index reset \
--elasticsearch-index-prefix directory2 \
--elasticsearch-server https://localhost:9200 \
- --elasticsearch-ca-cert ./libs/wire-subsystems/test/resources/elasticsearch-ca.pem \
+ --elasticsearch-ca-cert ./libs/wire-subsystems/test/resources/elasticsearch-ca.pem \
--elasticsearch-credentials ./libs/wire-subsystems/test/resources/elasticsearch-credentials.yaml > /dev/null
./integration/scripts/integration-dynamic-backends-brig-index.sh \
--elasticsearch-server https://localhost:9200 \
- --elasticsearch-ca-cert ./libs/wire-subsystems/test/resources/elasticsearch-ca.pem \
+ --elasticsearch-ca-cert ./libs/wire-subsystems/test/resources/elasticsearch-ca.pem \
+ --elasticsearch-credentials ./libs/wire-subsystems/test/resources/elasticsearch-credentials.yaml > /dev/null
+ @echo -e "\n'brig-index reset' only deletes the index and regenerates the mapping, but doesn't generate or populate a new index, so you need to call 'make es-reindex explicitly now!\n"
+
+.PHONY: es-reindex
+es-reindex: c
+ ./dist/brig-index reindex \
+ --pg-pool-size 10 \
+ --pg-pool-acquisition-timeout 10s \
+ --pg-pool-aging-timeout 1d \
+ --pg-pool-idleness-timeout 1h \
+ --pg-settings '{"host":"127.0.0.1","port":"5432","user":"wire-server","dbname":"backendA"}' \
+ --pg-password-file ./libs/wire-subsystems/test/resources/postgres-credentials.yaml \
+ --elasticsearch-server https://localhost:9200 \
+ --elasticsearch-ca-cert ./libs/wire-subsystems/test/resources/elasticsearch-ca.pem \
--elasticsearch-credentials ./libs/wire-subsystems/test/resources/elasticsearch-credentials.yaml > /dev/null
.PHONY: rabbitmq-reset
@@ -416,7 +438,7 @@ rabbitmq-reset: rabbit-clean
# Migrate all keyspaces and reset the ES index
# Does not migrate postgres as brig does that on startup.
.PHONY: db-migrate
-db-migrate: c
+db-migrate: c postgres-migrate
./dist/brig-schema --keyspace brig_test --replication-factor 1 > /dev/null
./dist/galley-schema --keyspace galley_test --replication-factor 1 > /dev/null
./dist/gundeck-schema --keyspace gundeck_test --replication-factor 1 > /dev/null
@@ -426,20 +448,7 @@ db-migrate: c
./dist/gundeck-schema --keyspace gundeck_test2 --replication-factor 1 > /dev/null
./dist/spar-schema --keyspace spar_test2 --replication-factor 1 > /dev/null
./integration/scripts/integration-dynamic-backends-db-schemas.sh --replication-factor 1 > /dev/null
- ./dist/brig-index reset \
- --elasticsearch-index-prefix directory \
- --elasticsearch-server https://localhost:9200 \
- --elasticsearch-ca-cert ./libs/wire-subsystems/test/resources/elasticsearch-ca.pem \
- --elasticsearch-credentials ./libs/wire-subsystems/test/resources/elasticsearch-credentials.yaml > /dev/null
- ./dist/brig-index reset \
- --elasticsearch-index-prefix directory2 \
- --elasticsearch-server https://localhost:9200 \
- --elasticsearch-ca-cert ./libs/wire-subsystems/test/resources/elasticsearch-ca.pem \
- --elasticsearch-credentials ./libs/wire-subsystems/test/resources/elasticsearch-credentials.yaml > /dev/null
- ./integration/scripts/integration-dynamic-backends-brig-index.sh \
- --elasticsearch-server https://localhost:9200 \
- --elasticsearch-ca-cert ./libs/wire-subsystems/test/resources/elasticsearch-ca.pem \
- --elasticsearch-credentials ./libs/wire-subsystems/test/resources/elasticsearch-credentials.yaml > /dev/null
+ make es-reset
#################################
## dependencies
diff --git a/changelog.d/0-release-notes/WPB-21366-refactor-brig-index-cli b/changelog.d/0-release-notes/WPB-21366-refactor-brig-index-cli
new file mode 100644
index 0000000000..db52bee775
--- /dev/null
+++ b/changelog.d/0-release-notes/WPB-21366-refactor-brig-index-cli
@@ -0,0 +1 @@
+Elasticsearch re-indexing requires postgres access now. If you run `brig-index` directly anywhere, make sure to add the relevant settings. The Elasticsearch index must be refilled from Cassandra in order for the changes to the search results to take effect. See https://docs.wire.com/latest/developer/reference/elastic-search.html?h=index#refill-es-documents-from-cassandra
diff --git a/integration/test/Test/Apps.hs b/integration/test/Test/Apps.hs
index 857db9c7bb..8bb314a771 100644
--- a/integration/test/Test/Apps.hs
+++ b/integration/test/Test/Apps.hs
@@ -101,19 +101,25 @@ testCreateApp = do
void $ bindResponse (createApp owner tid new {category = "notinenum"}) $ \resp -> do
resp.status `shouldMatchInt` 400
- let foundUserType exactMatchTerm aType =
- searchContacts owner exactMatchTerm OwnDomain `bindResponse` \resp -> do
+ let foundUserType :: (HasCallStack) => Value -> String -> [String] -> App ()
+ foundUserType searcher exactMatchTerm aTypes =
+ searchContacts searcher exactMatchTerm OwnDomain `bindResponse` \resp -> do
resp.status `shouldMatchInt` 200
- foundDoc <- resp.json %. "documents" >>= asList >>= assertOne
- foundDoc %. "type" `shouldMatch` aType
+ foundDoc <- resp.json %. "documents" >>= asList
+ (%. "type") `mapM` foundDoc `shouldMatch` aTypes
-- App's user is findable from /search/contacts
BrigI.refreshIndex domain
- foundUserType new.name "app"
+ foundUserType owner new.name ["app"]
+ foundUserType regularMember new.name ["app"]
+
+ -- App's user is *not* findable from other team.
+ BrigI.refreshIndex domain
+ foundUserType owner2 new.name []
-- Regular members still have the type "regular"
memberName <- regularMember %. "name" & asString
- foundUserType memberName "regular"
+ foundUserType owner memberName ["regular"]
testRefreshAppCookie :: (HasCallStack) => App ()
testRefreshAppCookie = do
diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs
index 4fa162f3bf..0bbc6d01ad 100644
--- a/libs/types-common/src/Data/Misc.hs
+++ b/libs/types-common/src/Data/Misc.hs
@@ -43,6 +43,7 @@ module Data.Misc
Duration (..),
diffTimeParser,
parseDuration,
+ unsafeParseDuration,
durationToMicros,
-- * HttpsUrl
@@ -284,6 +285,13 @@ diffTimeParser = do
parseDuration :: Text -> Either String Duration
parseDuration = fmap Duration . Atto.parseOnly (diffTimeParser <* Atto.endOfInput)
+unsafeParseDuration :: Text -> Duration
+unsafeParseDuration txt =
+ either
+ (\err -> error $ "Malformed duration: " <> show txt <> " " <> err)
+ id
+ (parseDuration txt)
+
-- | Useful for threadDelay, timeout, etc.
durationToMicros :: Duration -> Int
durationToMicros =
diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs
index fa2a714db9..47985da588 100644
--- a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs
+++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk.hs
@@ -34,6 +34,7 @@ data IndexedUserStoreBulk m a where
-- | 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 ()
+ -- | Run `ForceSyncAllUsers` iff the index version is out of date.
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
index e5585735bd..bbc3f68bd5 100644
--- a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs
+++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs
@@ -20,6 +20,7 @@ module Wire.IndexedUserStore.Bulk.ElasticSearch where
import Cassandra.Exec (paginateWithStateC)
import Cassandra.Util (Writetime (Writetime))
import Conduit (ConduitT, runConduit, (.|))
+import Data.Aeson (encode)
import Data.Conduit.Combinators qualified as Conduit
import Data.Id
import Data.Json.Util (UTCTimeMillis (fromUTCTimeMillis))
@@ -35,6 +36,7 @@ import Wire.API.Team.Feature
import Wire.API.Team.Member.Info
import Wire.API.Team.Role
import Wire.API.User
+import Wire.AppStore
import Wire.GalleyAPIAccess
import Wire.IndexedUserStore (IndexedUserStore)
import Wire.IndexedUserStore qualified as IndexedUserStore
@@ -50,6 +52,7 @@ import Wire.UserStore.IndexUser
interpretIndexedUserStoreBulk ::
( Member TinyLog r,
Member UserStore r,
+ Member AppStore r,
Member (Concurrency Unsafe) r,
Member GalleyAPIAccess r,
Member IndexedUserStore r,
@@ -65,6 +68,7 @@ interpretIndexedUserStoreBulk = interpret \case
syncAllUsersImpl ::
forall r.
( Member UserStore r,
+ Member AppStore r,
Member TinyLog r,
Member (Concurrency 'Unsafe) r,
Member GalleyAPIAccess r,
@@ -76,6 +80,7 @@ syncAllUsersImpl = syncAllUsersWithVersion ES.ExternalGT
forceSyncAllUsersImpl ::
forall r.
( Member UserStore r,
+ Member AppStore r,
Member TinyLog r,
Member (Concurrency 'Unsafe) r,
Member GalleyAPIAccess r,
@@ -87,6 +92,7 @@ forceSyncAllUsersImpl = syncAllUsersWithVersion ES.ExternalGTE
syncAllUsersWithVersion ::
forall r.
( Member UserStore r,
+ Member AppStore r,
Member TinyLog r,
Member (Concurrency 'Unsafe) r,
Member GalleyAPIAccess r,
@@ -122,6 +128,7 @@ syncAllUsersWithVersion mkVersion =
(t,) <$> teamSearchVisibilityInbound t
userTypes :: Map UserId UserType <- fmap Map.fromList . unsafePooledForConcurrentlyN 16 page $ \iu ->
(iu.userId,) <$> getUserType iu
+ warnIfMissingUserTypes page userTypes
roles :: Map UserId (WithWritetime Role) <- fmap (Map.fromList . concat) . unsafePooledForConcurrentlyN 16 (Map.toList teams) $ \(t, us) -> do
tms <- (.members) <$> selectTeamMemberInfos t (fmap (.userId) us)
pure $ mapMaybe mkRoleWithWriteTime tms
@@ -147,11 +154,29 @@ syncAllUsersWithVersion mkVersion =
)
<$> permissionsToRole tmi.permissions
+ -- `page` and `userTypes` *should* overlap perfectly, but we're
+ -- using `unsafePooledForConcurrentlyN` to make concurrent db
+ -- calls and that swallows any errors that might occur.
+ --
+ -- FUTUREWORK: we need to get rid of `Wire.Sem.Concurrency`, it's
+ -- unidiomatic and dangerous!
+ warnIfMissingUserTypes :: [IndexUser] -> Map UserId ignored -> Sem r ()
+ warnIfMissingUserTypes page userTypes = do
+ let missing = us \\ ts
+ us, ts :: [UserId]
+ us = (.userId) <$> page
+ ts = Map.keys userTypes
+ unless (null missing) do
+ warn $
+ Log.field "missing" (encode missing)
+ . Log.msg (Log.val "Reindex: could not lookup all user types!")
+
migrateDataImpl ::
( Member IndexedUserStore r,
Member (Error MigrationException) r,
Member IndexedUserMigrationStore r,
Member UserStore r,
+ Member AppStore r,
Member (Concurrency Unsafe) r,
Member GalleyAPIAccess r,
Member TinyLog r
@@ -185,17 +210,13 @@ teamSearchVisibilityInbound tid =
-- probably expose it as an action there.
getUserType ::
forall r.
+ (Member AppStore r) =>
IndexUser ->
Sem r UserType
getUserType iu = case iu.serviceId of
Just _ -> pure UserTypeBot
Nothing -> do
- {-
- FUTUREWORK: *correct* type fields from search are coming in a separate PR:
-
mmApp <- mapM (getApp iu.userId) (iu.teamId <&> (.value))
case join mmApp of
Just _ -> pure UserTypeApp
Nothing -> pure UserTypeRegular
- -}
- pure UserTypeRegular
diff --git a/libs/wire-subsystems/test/resources/postgres-credentials.yaml b/libs/wire-subsystems/test/resources/postgres-credentials.yaml
new file mode 100644
index 0000000000..dfcbb7e44a
--- /dev/null
+++ b/libs/wire-subsystems/test/resources/postgres-credentials.yaml
@@ -0,0 +1 @@
+posty-the-gres
diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs
index ad58b6d53f..523582c1c7 100644
--- a/services/brig/src/Brig/Index/Eval.hs
+++ b/services/brig/src/Brig/Index/Eval.hs
@@ -22,11 +22,10 @@ module Brig.Index.Eval
where
import Brig.App (initHttpManagerWithTLSConfig, mkIndexEnv)
-import Brig.Index.Options
-import Brig.Options
+import Brig.Index.Options as IxOpts
+import Brig.Options as Opt
import Brig.User.Search.Index
import Cassandra (Client, runClient)
-import Cassandra.Options
import Cassandra.Util (defInitCassandra)
import Control.Exception (throwIO)
import Control.Lens
@@ -40,16 +39,20 @@ import Data.Id
import Database.Bloodhound qualified as ES
import Database.Bloodhound.Internal.Client (BHEnv (..))
import Hasql.Pool
+import Hasql.Pool.Extended
import Imports
import Polysemy
import Polysemy.Embed (runEmbedded)
import Polysemy.Error
+import Polysemy.Input
import Polysemy.TinyLog hiding (Logger)
import System.Logger qualified as Log
import System.Logger.Class (Logger)
import Util.Options
import Wire.API.Federation.Client (FederatorClient)
import Wire.API.Federation.Error
+import Wire.AppStore
+import Wire.AppStore.Postgres
import Wire.BlockListStore (BlockListStore)
import Wire.BlockListStore.Cassandra
import Wire.FederationAPIAccess
@@ -87,6 +90,7 @@ type BrigIndexEffectStack =
FederationAPIAccess FederatorClient,
Error FederationError,
UserStore,
+ AppStore,
IndexedUserStore,
Error IndexedUserStoreError,
IndexedUserMigrationStore,
@@ -98,16 +102,18 @@ type BrigIndexEffectStack =
Metrics,
TinyLog,
Concurrency 'Unsafe,
+ Input Pool,
Error UsageError,
Embed IO,
Final IO
]
-runSem :: ESConnectionSettings -> CassandraSettings -> Endpoint -> Logger -> Sem BrigIndexEffectStack a -> IO a
-runSem esConn cas galleyEndpoint logger action = do
+runSem :: ESConnectionSettings -> CassandraSettings -> PostgresSettings -> Endpoint -> Logger -> Sem BrigIndexEffectStack a -> IO a
+runSem esConn cas pg galleyEndpoint logger action = do
mgr <- initHttpManagerWithTLSConfig esConn.esInsecureSkipVerifyTls esConn.esCaCert
mEsCreds :: Maybe Credentials <- for esConn.esCredentials initCredentials
casClient <- defInitCassandra (toCassandraOpts cas) logger
+ pgPool <- initPostgresPool pg.pool pg.settings pg.passwordFile
let bhEnv =
BHEnv
{ bhServer = toESServer esConn.esServer,
@@ -128,6 +134,7 @@ runSem esConn cas galleyEndpoint logger action = do
runFinal
. embedToFinal
. throwErrorToIOFinal @UsageError
+ . runInputConst pgPool
. unsafelyPerformConcurrency
. loggerToTinyLogReqId reqId logger
. ignoreMetrics
@@ -141,6 +148,7 @@ runSem esConn cas galleyEndpoint logger action = do
. interpretIndexedUserMigrationStoreES bhEnv migrationIndexName
. throwErrorToIOFinal @IndexedUserStoreError
. interpretIndexedUserStoreES indexedUserStoreConfig
+ . interpretAppStoreToPostgres
. interpretUserStoreCassandra casClient
. throwErrorToIOFinal @FederationError
. noFederationAPIAccess
@@ -164,17 +172,17 @@ runCommand l = \case
Reset es galley -> do
e <- initIndex l (es ^. esConnection) galley
runIndexIO e $ resetIndex (mkCreateIndexSettings es)
- Reindex es cas galley -> do
- runSem (es ^. esConnection) cas galley l $
+ Reindex es cas pg galley -> do
+ runSem (es ^. esConnection) cas pg galley l $
IndexedUserStoreBulk.syncAllUsers
- ReindexSameOrNewer es cas galley -> do
- runSem (es ^. esConnection) cas galley l $
+ ReindexSameOrNewer es cas pg galley -> do
+ runSem (es ^. esConnection) cas pg galley l $
IndexedUserStoreBulk.forceSyncAllUsers
UpdateMapping esConn galley -> do
e <- initIndex l esConn galley
runIndexIO e updateMapping
- Migrate es cas galley -> do
- runSem (es ^. esConnection) cas galley l $
+ Migrate es cas pg galley -> do
+ runSem (es ^. esConnection) cas pg galley l $
IndexedUserStoreBulk.migrateData
ReindexFromAnotherIndex reindexSettings -> do
mgr <-
diff --git a/services/brig/src/Brig/Index/Options.hs b/services/brig/src/Brig/Index/Options.hs
index f72db00304..8b0966d25f 100644
--- a/services/brig/src/Brig/Index/Options.hs
+++ b/services/brig/src/Brig/Index/Options.hs
@@ -34,7 +34,9 @@ module Brig.Index.Options
cPort,
cTlsCa,
cKeyspace,
+ PostgresSettings (..),
localElasticSettings,
+ brigOptsToPostgresSettings,
localCassandraSettings,
commandParser,
mkCreateIndexSettings,
@@ -47,13 +49,23 @@ module Brig.Index.Options
where
import Brig.Index.Types (CreateIndexSettings (..))
+import Brig.Options qualified as Opts
import Cassandra qualified as C
import Control.Lens
+import Data.Aeson as Aeson
+import Data.Aeson.Key qualified as AKey
+import Data.Aeson.KeyMap qualified as AKM
+import Data.Aeson.Text qualified as Aeson
import Data.ByteString.Lens
+import Data.Map qualified as Map
+import Data.Misc
import Data.Text qualified as Text
+import Data.Text.Encoding (encodeUtf8)
+import Data.Text.Lazy qualified as LText
import Data.Text.Strict.Lens
-import Data.Time.Clock (NominalDiffTime)
+import Data.Time (NominalDiffTime)
import Database.Bloodhound qualified as ES
+import Hasql.Pool.Extended
import Imports
import Options.Applicative
import URI.ByteString
@@ -63,11 +75,11 @@ import Util.Options (CassandraOpts (..), Endpoint (..), FilePathSecrets)
data Command
= Create ElasticSettings Endpoint
| Reset ElasticSettings Endpoint
- | Reindex ElasticSettings CassandraSettings Endpoint
- | ReindexSameOrNewer ElasticSettings CassandraSettings Endpoint
+ | Reindex ElasticSettings CassandraSettings PostgresSettings Endpoint
+ | ReindexSameOrNewer ElasticSettings CassandraSettings PostgresSettings Endpoint
| -- | 'ElasticSettings' has shards and other settings that are not needed here.
UpdateMapping ESConnectionSettings Endpoint
- | Migrate ElasticSettings CassandraSettings Endpoint
+ | Migrate ElasticSettings CassandraSettings PostgresSettings Endpoint
| ReindexFromAnotherIndex ReindexFromAnotherIndexSettings
deriving (Show)
@@ -90,6 +102,15 @@ data ElasticSettings = ElasticSettings
}
deriving (Show)
+data PostgresSettings = PostgresSettings
+ { pool :: !PoolConfig,
+ passwordFile :: !(Maybe FilePathSecrets),
+ -- | Postgresql settings, the key values must be in libpq format.
+ -- https://www.postgresql.org/docs/17/libpq-connect.html#LIBPQ-PARAMKEYWORDS
+ settings :: !(Map Text Text)
+ }
+ deriving (Show)
+
data CassandraSettings = CassandraSettings
{ _cHost :: String,
_cPort :: Word16,
@@ -147,6 +168,14 @@ localElasticSettings =
_esDeleteTemplate = Nothing
}
+brigOptsToPostgresSettings :: Opts.Opts -> PostgresSettings
+brigOptsToPostgresSettings opts =
+ PostgresSettings
+ { pool = opts.postgresqlPool,
+ passwordFile = opts.postgresqlPassword,
+ settings = opts.postgresql
+ }
+
localCassandraSettings :: CassandraSettings
localCassandraSettings =
CassandraSettings
@@ -297,6 +326,70 @@ credentialsPathParser =
)
)
+postgresSettingsParser :: Parser PostgresSettings
+postgresSettingsParser =
+ PostgresSettings
+ <$> poolConfigParser
+ <*> optional
+ ( strOption
+ ( long "pg-password-file"
+ <> metavar "FILE"
+ <> help "File containing PostgreSQL password"
+ )
+ )
+ <*> option
+ (eitherReader parseJsonMap)
+ ( long "pg-settings"
+ <> metavar "JSON"
+ <> help "PostgreSQL connection parameters as JSON object"
+ <> value Map.empty
+ )
+
+poolConfigParser :: Parser PoolConfig
+poolConfigParser =
+ PoolConfig
+ <$> option
+ auto
+ ( long "pg-pool-size"
+ <> metavar "INT"
+ <> help "Connection pool size"
+ <> value 10
+ )
+ <*> option
+ (eitherReader (parseDuration . Text.pack))
+ ( long "pg-pool-acquisition-timeout"
+ <> metavar "Duration"
+ <> help "Pool acquisition timeout in seconds"
+ <> value (unsafeParseDuration "10s")
+ )
+ <*> option
+ (eitherReader (parseDuration . Text.pack))
+ ( long "pg-pool-aging-timeout"
+ <> metavar "Duration"
+ <> help "Pool aging timeout in seconds"
+ <> value (unsafeParseDuration "1d")
+ )
+ <*> option
+ (eitherReader (parseDuration . Text.pack))
+ ( long "pg-pool-idleness-timeout"
+ <> metavar "Duration"
+ <> help "Pool idleness timeout in seconds"
+ <> value (unsafeParseDuration "10m")
+ )
+
+parseJsonMap :: String -> Either String (Map Text Text)
+parseJsonMap s = do
+ Aeson.eitherDecodeStrict' (encodeUtf8 (Text.pack s)) >>= \case
+ Object hmap -> pure $ Map.fromList $ bimap AKey.toText valueToText <$> AKM.toList hmap
+ bad -> Left $ "invalid json object: " <> show bad
+ where
+ valueToText :: Value -> Text
+ valueToText (String t) = t
+ valueToText (Bool b) = (if b then "true" else "false")
+ valueToText (Number n) = (Text.pack (show n))
+ valueToText Null = "null"
+ valueToText v = LText.toStrict (Aeson.encodeToLazyText v)
+
cassandraSettingsParser :: Parser CassandraSettings
cassandraSettingsParser =
CassandraSettings
@@ -394,19 +487,19 @@ commandParser =
<> command
"reindex"
( info
- (Reindex <$> elasticSettingsParser <*> cassandraSettingsParser <*> galleyEndpointParser)
+ (Reindex <$> elasticSettingsParser <*> cassandraSettingsParser <*> postgresSettingsParser <*> galleyEndpointParser)
(progDesc "Reindex all users from Cassandra if there is a new version.")
)
<> command
"reindex-if-same-or-newer"
( info
- (ReindexSameOrNewer <$> elasticSettingsParser <*> cassandraSettingsParser <*> galleyEndpointParser)
+ (ReindexSameOrNewer <$> elasticSettingsParser <*> cassandraSettingsParser <*> postgresSettingsParser <*> galleyEndpointParser)
(progDesc "Reindex all users from Cassandra, even if the version has not changed.")
)
<> command
"migrate-data"
( info
- (Migrate <$> elasticSettingsParser <*> cassandraSettingsParser <*> galleyEndpointParser)
+ (Migrate <$> elasticSettingsParser <*> cassandraSettingsParser <*> postgresSettingsParser <*> galleyEndpointParser)
(progDesc "Migrate data in elastic search")
)
<> command
diff --git a/services/brig/test/integration/API/Search.hs b/services/brig/test/integration/API/Search.hs
index 1e87cb4b7a..2fb1fbdf5f 100644
--- a/services/brig/test/integration/API/Search.hs
+++ b/services/brig/test/integration/API/Search.hs
@@ -800,7 +800,7 @@ runReindexFromAnotherIndex logger opts newIndexName migrationIndexName =
in runCommand logger $ ReindexFromAnotherIndex reindexSettings
runReindexFromDatabase ::
- (ElasticSettings -> CassandraSettings -> Endpoint -> Command) ->
+ (ElasticSettings -> CassandraSettings -> PostgresSettings -> Endpoint -> Command) ->
Log.Logger ->
Opt.Opts ->
ES.IndexName ->
@@ -819,14 +819,14 @@ runReindexFromDatabase syncCommand logger opts newIndexName migrationIndexName =
& IndexOpts.esIndexShardCount .~ shards
& IndexOpts.esIndexRefreshInterval .~ refreshInterval
cassandraSettings :: CassandraSettings =
- ( localCassandraSettings
- & IndexOpts.cHost .~ (Text.unpack opts.cassandra.endpoint.host)
- & IndexOpts.cPort .~ (opts.cassandra.endpoint.port)
- & IndexOpts.cKeyspace .~ (C.Keyspace opts.cassandra.keyspace)
- )
-
+ localCassandraSettings
+ & IndexOpts.cHost .~ (Text.unpack opts.cassandra.endpoint.host)
+ & IndexOpts.cPort .~ (opts.cassandra.endpoint.port)
+ & IndexOpts.cKeyspace .~ (C.Keyspace opts.cassandra.keyspace)
+ postgresSettings :: PostgresSettings =
+ brigOptsToPostgresSettings opts
endpoint :: Endpoint = opts.galley
- in runCommand logger $ syncCommand elasticSettings cassandraSettings endpoint
+ in runCommand logger $ syncCommand elasticSettings cassandraSettings postgresSettings endpoint
toESConnectionSettings :: ElasticSearchOpts -> ES.IndexName -> ESConnectionSettings
toESConnectionSettings opts migrationIndexName = ESConnectionSettings {..}
From 3c17b49886ffd57570ed367bf41de265c3fa6317 Mon Sep 17 00:00:00 2001
From: Gautier DI FOLCO
Date: Wed, 21 Jan 2026 18:19:59 +0100
Subject: [PATCH 21/26] WPB-21964: move feature flags logic to wire-subsystems
(#4941)
---
changelog.d/5-internal/WPD-21964 | 1 +
libs/wire-api/src/Wire/API/Team/Feature.hs | 9 +
libs/wire-subsystems/default.nix | 6 +
.../src/Wire/FeaturesConfigSubsystem.hs | 50 +++
.../FeaturesConfigSubsystem/Interpreter.hs | 96 +++++
.../src/Wire/FeaturesConfigSubsystem/Types.hs | 151 ++++++++
.../src/Wire/FeaturesConfigSubsystem/Utils.hs | 19 +
libs/wire-subsystems/src/Wire/LegalHold.hs | 27 ++
.../src/Wire}/TeamFeatureStore.hs | 4 +-
libs/wire-subsystems/wire-subsystems.cabal | 8 +
services/galley/galley.cabal | 1 -
services/galley/src/Galley/API/Action.hs | 91 ++---
services/galley/src/Galley/API/Create.hs | 20 +-
services/galley/src/Galley/API/Federation.hs | 6 +-
services/galley/src/Galley/API/Internal.hs | 8 +-
.../galley/src/Galley/API/LegalHold/Team.hs | 22 +-
.../src/Galley/API/MLS/GroupInfoCheck.hs | 9 +-
services/galley/src/Galley/API/MLS/Message.hs | 13 +-
services/galley/src/Galley/API/Public/Bot.hs | 7 +-
.../galley/src/Galley/API/Public/Feature.hs | 3 +-
services/galley/src/Galley/API/Query.hs | 19 +-
services/galley/src/Galley/API/Teams.hs | 33 +-
.../galley/src/Galley/API/Teams/Features.hs | 33 +-
.../src/Galley/API/Teams/Features/Get.hs | 336 ++----------------
services/galley/src/Galley/API/Update.hs | 22 +-
services/galley/src/Galley/App.hs | 7 +-
.../src/Galley/Cassandra/TeamFeatures.hs | 2 +-
services/galley/src/Galley/Effects.hs | 9 +-
28 files changed, 535 insertions(+), 477 deletions(-)
create mode 100644 changelog.d/5-internal/WPD-21964
create mode 100644 libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem.hs
create mode 100644 libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Interpreter.hs
create mode 100644 libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs
create mode 100644 libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Utils.hs
create mode 100644 libs/wire-subsystems/src/Wire/LegalHold.hs
rename {services/galley/src/Galley/Effects => libs/wire-subsystems/src/Wire}/TeamFeatureStore.hs (95%)
diff --git a/changelog.d/5-internal/WPD-21964 b/changelog.d/5-internal/WPD-21964
new file mode 100644
index 0000000000..ee6e6bd2b5
--- /dev/null
+++ b/changelog.d/5-internal/WPD-21964
@@ -0,0 +1 @@
+Move Feature Flags read to `wire-subsystems`.
diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs
index 5f560380d2..37d226b9b1 100644
--- a/libs/wire-api/src/Wire/API/Team/Feature.hs
+++ b/libs/wire-api/src/Wire/API/Team/Feature.hs
@@ -1,6 +1,7 @@
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StrictData #-}
{-# OPTIONS_GHC -Wno-ambiguous-fields #-}
@@ -37,6 +38,8 @@ module Wire.API.Team.Feature
LockableFeature (..),
defUnlockedFeature,
defLockedFeature,
+ setLockableFeatureLockStatus,
+ setLockableFeatureStatus,
LockableFeaturePatch (..),
Feature (..),
forgetLock,
@@ -368,6 +371,12 @@ data LockableFeature cfg = LockableFeature
instance (Default (LockableFeature cfg)) => Default (Feature cfg) where
def = forgetLock def
+setLockableFeatureLockStatus :: LockableFeature cfg -> LockStatus -> LockableFeature cfg
+setLockableFeatureLockStatus LockableFeature {..} s = LockableFeature {lockStatus = s, ..}
+
+setLockableFeatureStatus :: LockableFeature cfg -> FeatureStatus -> LockableFeature cfg
+setLockableFeatureStatus LockableFeature {..} s = LockableFeature {status = s, ..}
+
-- | A feature that is disabled and locked.
defLockedFeature :: (Default cfg) => LockableFeature cfg
defLockedFeature =
diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix
index 65e5959917..bbe9d0d932 100644
--- a/libs/wire-subsystems/default.nix
+++ b/libs/wire-subsystems/default.nix
@@ -37,6 +37,7 @@
, extra
, file-embed
, galley-types
+, generics-sop
, gitignoreSource
, hashable
, HaskellNet
@@ -93,6 +94,7 @@
, servant-server
, singletons
, sodium-crypto-sign
+, sop-core
, ssl-util
, statistics
, stomp-queue
@@ -162,6 +164,7 @@ mkDerivation {
extra
file-embed
galley-types
+ generics-sop
hashable
HaskellNet
HaskellNet-SSL
@@ -212,6 +215,7 @@ mkDerivation {
servant-server
singletons
sodium-crypto-sign
+ sop-core
ssl-util
statistics
stomp-queue
@@ -275,6 +279,7 @@ mkDerivation {
extra
file-embed
galley-types
+ generics-sop
hashable
HaskellNet
HaskellNet-SSL
@@ -326,6 +331,7 @@ mkDerivation {
servant-server
singletons
sodium-crypto-sign
+ sop-core
ssl-util
statistics
stomp-queue
diff --git a/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem.hs b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem.hs
new file mode 100644
index 0000000000..8d0e802d9f
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+-- This file is part of the Wire Server implementation.
+--
+-- Copyright (C) 2026 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.FeaturesConfigSubsystem where
+
+import Data.Id (TeamId, UserId)
+import Data.Qualified (Local)
+import Imports
+import Polysemy
+import Wire.API.Team.Feature (AllTeamFeatures, LockableFeature)
+import Wire.FeaturesConfigSubsystem.Types
+
+data FeaturesConfigSubsystem m a where
+ GetFeature ::
+ forall cfg m.
+ (GetFeatureConfig cfg) =>
+ UserId -> TeamId -> FeaturesConfigSubsystem m (LockableFeature cfg)
+ GetFeatureForTeam ::
+ forall cfg m.
+ (GetFeatureConfig cfg) =>
+ TeamId -> FeaturesConfigSubsystem m (LockableFeature cfg)
+ GetFeatureForServer ::
+ forall cfg m.
+ (GetFeatureConfig cfg) =>
+ FeaturesConfigSubsystem m (LockableFeature cfg)
+ GetFeatureForTeamUser ::
+ forall cfg m.
+ (GetFeatureConfig cfg) =>
+ UserId -> Maybe TeamId -> FeaturesConfigSubsystem m (LockableFeature cfg)
+ GetAllTeamFeaturesForTeamMember :: Local UserId -> TeamId -> FeaturesConfigSubsystem m AllTeamFeatures
+ GetAllTeamFeaturesForTeam :: TeamId -> FeaturesConfigSubsystem m AllTeamFeatures
+ GetAllTeamFeaturesForServer :: FeaturesConfigSubsystem m AllTeamFeatures
+
+makeSem ''FeaturesConfigSubsystem
diff --git a/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Interpreter.hs
new file mode 100644
index 0000000000..e82d02a1d7
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Interpreter.hs
@@ -0,0 +1,96 @@
+{-# LANGUAGE TypeOperators #-}
+{-# OPTIONS_GHC -Wno-ambiguous-fields #-}
+
+module Wire.FeaturesConfigSubsystem.Interpreter where
+
+import Data.Id
+import Data.Qualified (tUnqualified)
+import Data.SOP
+import Galley.Types.Teams
+import Imports
+import Polysemy
+import Polysemy.Input
+import Wire.API.Error
+import Wire.API.Error.Galley
+import Wire.API.Team.Feature
+import Wire.FeaturesConfigSubsystem
+import Wire.FeaturesConfigSubsystem.Types
+import Wire.FeaturesConfigSubsystem.Utils
+import Wire.TeamFeatureStore
+import Wire.TeamSubsystem (TeamSubsystem)
+import Wire.TeamSubsystem qualified as TeamSubsystem
+
+runFeaturesConfigSubsystem ::
+ forall r a.
+ ( Member TeamFeatureStore r,
+ Member TeamSubsystem r,
+ Member (ErrorS 'NotATeamMember) r,
+ GetFeatureConfigEffects r
+ ) =>
+ Sem (FeaturesConfigSubsystem : r) a ->
+ Sem r a
+runFeaturesConfigSubsystem = interpret $ \case
+ GetFeature uid tid -> do
+ void $ TeamSubsystem.internalGetTeamMember uid tid >>= noteS @'NotATeamMember
+ doGetFeatureForTeam tid
+ GetFeatureForTeam tid ->
+ doGetFeatureForTeam tid
+ GetFeatureForServer ->
+ resolveServerFeature
+ GetFeatureForTeamUser uid mTid ->
+ doGetFeatureForTeamUser uid mTid
+ GetAllTeamFeaturesForTeamMember luid tid -> do
+ void $ TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid >>= noteS @'NotATeamMember
+ doGetAllTeamFeatures tid
+ GetAllTeamFeaturesForTeam tid ->
+ doGetAllTeamFeatures tid
+ GetAllTeamFeaturesForServer ->
+ doGetAllTeamFeaturesForServer
+
+-- Internal helpers
+
+doGetFeatureForTeam ::
+ forall cfg r.
+ ( GetFeatureConfig cfg,
+ Member TeamFeatureStore r,
+ GetFeatureConfigEffects r
+ ) =>
+ TeamId ->
+ Sem r (LockableFeature cfg)
+doGetFeatureForTeam tid = do
+ dbFeature <- getDbFeature tid
+ defFeature <- resolveServerFeature
+ computeFeature tid defFeature dbFeature
+
+doGetFeatureForTeamUser ::
+ forall cfg r.
+ ( GetFeatureConfig cfg,
+ Member TeamFeatureStore r,
+ GetFeatureConfigEffects r
+ ) =>
+ UserId ->
+ Maybe TeamId ->
+ Sem r (LockableFeature cfg)
+doGetFeatureForTeamUser uid Nothing = getFeatureForUser uid
+doGetFeatureForTeamUser _uid (Just tid) = doGetFeatureForTeam tid
+
+doGetAllTeamFeatures ::
+ forall r.
+ ( Member TeamFeatureStore r,
+ GetFeatureConfigEffects r
+ ) =>
+ TeamId ->
+ Sem r AllTeamFeatures
+doGetAllTeamFeatures tid = do
+ features <- getAllDbFeatures tid
+ defFeatures <- doGetAllTeamFeaturesForServer
+ hsequence' $ hcliftA2 (Proxy @(GetAllFeaturesForServerConstraints r)) compute defFeatures features
+ where
+ compute :: forall p. (GetFeatureConfig p) => LockableFeature p -> DbFeature p -> (Sem r :.: LockableFeature) p
+ compute defFeature feat = Comp $ computeFeature tid defFeature feat
+
+doGetAllTeamFeaturesForServer :: forall r. (Member (Input FeatureFlags) r) => Sem r AllTeamFeatures
+doGetAllTeamFeaturesForServer =
+ hsequence' $
+ hcpure (Proxy @GetFeatureConfig) $
+ Comp resolveServerFeature
diff --git a/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs
new file mode 100644
index 0000000000..eaed761e6d
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Types.hs
@@ -0,0 +1,151 @@
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE UndecidableSuperClasses #-}
+
+module Wire.FeaturesConfigSubsystem.Types where
+
+import Data.Default
+import Data.Id (TeamId, UserId)
+import Data.SOP.Sing (SListI)
+import Galley.Types.Teams
+import Imports
+import Polysemy
+import Polysemy.Input
+import Wire.API.Team.Feature
+import Wire.BrigAPIAccess (BrigAPIAccess, getAccountConferenceCallingConfigClient)
+import Wire.FeaturesConfigSubsystem.Utils (resolveServerFeature)
+import Wire.LegalHold
+import Wire.LegalHoldStore
+
+type GetFeatureConfigEffects r =
+ ( Member (Input FeatureFlags) r,
+ Member (Input ExposeInvitationURLsAllowlist) r,
+ Member LegalHoldStore r,
+ Member (Input (FeatureDefaults LegalholdConfig)) r,
+ Member BrigAPIAccess r
+ )
+
+newtype ExposeInvitationURLsAllowlist
+ = ExposeInvitationURLsAllowlist [TeamId]
+
+-- | Don't export methods of this typeclass
+class
+ ( IsFeatureConfig cfg,
+ GetFeatureDefaults (FeatureDefaults cfg),
+ SListI Features
+ ) =>
+ GetFeatureConfig cfg
+ where
+ getFeatureForUser ::
+ (GetFeatureConfigEffects r) =>
+ UserId ->
+ Sem r (LockableFeature cfg)
+ default getFeatureForUser ::
+ (GetFeatureConfigEffects r) =>
+ UserId ->
+ Sem r (LockableFeature cfg)
+ getFeatureForUser _uid = resolveServerFeature
+ computeFeature ::
+ (GetFeatureConfigEffects r) =>
+ TeamId ->
+ LockableFeature cfg ->
+ DbFeature cfg ->
+ Sem r (LockableFeature cfg)
+ default computeFeature ::
+ TeamId ->
+ LockableFeature cfg ->
+ DbFeature cfg ->
+ Sem r (LockableFeature cfg)
+ computeFeature _tid defFeature dbFeature =
+ pure $ resolveDbFeature defFeature dbFeature
+
+class (GetFeatureConfig cfg, GetFeatureConfigEffects r) => GetAllFeaturesForServerConstraints r cfg
+
+instance (GetFeatureConfig cfg, GetFeatureConfigEffects r) => GetAllFeaturesForServerConstraints r cfg
+
+class (GetFeatureConfig cfg, GetFeatureConfigEffects r) => GetAllTeamFeaturesForUserConstraints r cfg
+
+instance (GetFeatureConfig cfg, GetFeatureConfigEffects r) => GetAllTeamFeaturesForUserConstraints r cfg
+
+instance GetFeatureConfig SSOConfig
+
+instance GetFeatureConfig SearchVisibilityAvailableConfig
+
+instance GetFeatureConfig ValidateSAMLEmailsConfig
+
+instance GetFeatureConfig DigitalSignaturesConfig
+
+instance GetFeatureConfig LegalholdConfig where
+ computeFeature tid defFeature dbFeature =
+ setLockableFeatureStatus defFeature <$> computeLegalHoldFeatureStatus tid dbFeature
+
+instance GetFeatureConfig FileSharingConfig
+
+instance GetFeatureConfig AppLockConfig
+
+instance GetFeatureConfig ClassifiedDomainsConfig
+
+instance GetFeatureConfig ConferenceCallingConfig where
+ getFeatureForUser uid = do
+ feat <- getAccountConferenceCallingConfigClient uid
+ pure $ withLockStatus (def @(LockableFeature ConferenceCallingConfig)).lockStatus feat
+
+ computeFeature _tid defFeature dbFeature =
+ pure $
+ let feat = applyDbFeature dbFeature $ setLockableFeatureStatus defFeature FeatureStatusEnabled
+ in case feat.lockStatus of
+ LockStatusLocked -> setLockableFeatureLockStatus defFeature LockStatusLocked
+ LockStatusUnlocked -> feat
+
+instance GetFeatureConfig SelfDeletingMessagesConfig
+
+instance GetFeatureConfig GuestLinksConfig
+
+instance GetFeatureConfig SndFactorPasswordChallengeConfig
+
+instance GetFeatureConfig SearchVisibilityInboundConfig
+
+instance GetFeatureConfig MLSConfig
+
+instance GetFeatureConfig ChannelsConfig
+
+instance GetFeatureConfig ExposeInvitationURLsToTeamAdminConfig where
+ computeFeature tid defFeature dbFeature = do
+ (ExposeInvitationURLsAllowlist allowList) <- inputs id
+ let teamAllowed = tid `elem` allowList
+ lockStatus = if teamAllowed then LockStatusUnlocked else LockStatusLocked
+ pure $ resolveDbFeature defFeature (dbFeatureLockStatus lockStatus <> dbFeature)
+
+instance GetFeatureConfig OutlookCalIntegrationConfig
+
+instance GetFeatureConfig MlsE2EIdConfig
+
+instance GetFeatureConfig MlsMigrationConfig
+
+instance GetFeatureConfig EnforceFileDownloadLocationConfig
+
+instance GetFeatureConfig LimitedEventFanoutConfig
+
+instance GetFeatureConfig DomainRegistrationConfig
+
+instance GetFeatureConfig CellsConfig
+
+instance GetFeatureConfig CellsInternalConfig
+
+instance GetFeatureConfig AllowedGlobalOperationsConfig
+
+instance GetFeatureConfig AssetAuditLogConfig
+
+instance GetFeatureConfig ConsumableNotificationsConfig
+
+instance GetFeatureConfig ChatBubblesConfig
+
+instance GetFeatureConfig AppsConfig
+
+instance GetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig
+
+instance GetFeatureConfig StealthUsersConfig
+
+instance GetFeatureConfig MeetingsConfig
+
+instance GetFeatureConfig MeetingsPremiumConfig
diff --git a/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Utils.hs b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Utils.hs
new file mode 100644
index 0000000000..877352e5b2
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/FeaturesConfigSubsystem/Utils.hs
@@ -0,0 +1,19 @@
+{-# OPTIONS_GHC -Wno-ambiguous-fields #-}
+
+module Wire.FeaturesConfigSubsystem.Utils where
+
+import Galley.Types.Teams
+import Imports
+import Polysemy
+import Polysemy.Input
+import Wire.API.Team.Feature
+
+resolveServerFeature ::
+ forall cfg r.
+ ( GetFeatureDefaults (FeatureDefaults cfg),
+ NpProject cfg Features,
+ Member (Input FeatureFlags) r
+ ) =>
+ Sem r (LockableFeature cfg)
+resolveServerFeature =
+ inputs $ featureDefaults @cfg
diff --git a/libs/wire-subsystems/src/Wire/LegalHold.hs b/libs/wire-subsystems/src/Wire/LegalHold.hs
new file mode 100644
index 0000000000..6261f5ae17
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/LegalHold.hs
@@ -0,0 +1,27 @@
+module Wire.LegalHold where
+
+import Data.Default (def)
+import Data.Id (TeamId)
+import Galley.Types.Teams
+import Imports
+import Polysemy
+import Polysemy.Input (Input, input)
+import Wire.API.Team.Feature
+import Wire.LegalHoldStore qualified as LegalHoldData
+
+computeLegalHoldFeatureStatus ::
+ ( Member LegalHoldData.LegalHoldStore r,
+ Member (Input (FeatureDefaults LegalholdConfig)) r
+ ) =>
+ TeamId ->
+ DbFeature LegalholdConfig ->
+ Sem r FeatureStatus
+computeLegalHoldFeatureStatus tid dbFeature = do
+ featureLegalHold <- input @(FeatureDefaults LegalholdConfig)
+ case featureLegalHold of
+ FeatureLegalHoldDisabledPermanently -> pure FeatureStatusDisabled
+ FeatureLegalHoldDisabledByDefault ->
+ 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/Effects/TeamFeatureStore.hs b/libs/wire-subsystems/src/Wire/TeamFeatureStore.hs
similarity index 95%
rename from services/galley/src/Galley/Effects/TeamFeatureStore.hs
rename to libs/wire-subsystems/src/Wire/TeamFeatureStore.hs
index d8b53db44e..7be546ed65 100644
--- a/services/galley/src/Galley/Effects/TeamFeatureStore.hs
+++ b/libs/wire-subsystems/src/Wire/TeamFeatureStore.hs
@@ -1,6 +1,6 @@
-- This file is part of the Wire Server implementation.
--
--- Copyright (C) 2022 Wire Swiss GmbH
+-- Copyright (C) 2026 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
@@ -15,7 +15,7 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
-module Galley.Effects.TeamFeatureStore where
+module Wire.TeamFeatureStore where
import Data.Id
import Polysemy
diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal
index a1991ca4b9..5b29d71a86 100644
--- a/libs/wire-subsystems/wire-subsystems.cabal
+++ b/libs/wire-subsystems/wire-subsystems.cabal
@@ -116,6 +116,7 @@ common common-all
, extra
, file-embed
, galley-types
+ , generics-sop
, hashable
, HaskellNet
, HaskellNet-SSL
@@ -164,6 +165,7 @@ common common-all
, servant-server
, singletons
, sodium-crypto-sign
+ , sop-core
, ssl-util
, statistics
, stomp-queue
@@ -261,6 +263,10 @@ library
Wire.Events
Wire.ExternalAccess
Wire.ExternalAccess.External
+ Wire.FeaturesConfigSubsystem
+ Wire.FeaturesConfigSubsystem.Interpreter
+ Wire.FeaturesConfigSubsystem.Types
+ Wire.FeaturesConfigSubsystem.Utils
Wire.FederationAPIAccess
Wire.FederationAPIAccess.Interpreter
Wire.FederationConfigStore
@@ -282,6 +288,7 @@ library
Wire.InternalEvent
Wire.InvitationStore
Wire.InvitationStore.Cassandra
+ Wire.LegalHold
Wire.LegalHoldStore
Wire.LegalHoldStore.Cassandra
Wire.LegalHoldStore.Cassandra.Queries
@@ -321,6 +328,7 @@ library
Wire.TeamCollaboratorsStore.Postgres
Wire.TeamCollaboratorsSubsystem
Wire.TeamCollaboratorsSubsystem.Interpreter
+ Wire.TeamFeatureStore
Wire.TeamInvitationSubsystem
Wire.TeamInvitationSubsystem.Error
Wire.TeamInvitationSubsystem.Interpreter
diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal
index 30504a8074..5120488855 100644
--- a/services/galley/galley.cabal
+++ b/services/galley/galley.cabal
@@ -152,7 +152,6 @@ library
Galley.Effects.CustomBackendStore
Galley.Effects.Queue
Galley.Effects.SearchVisibilityStore
- Galley.Effects.TeamFeatureStore
Galley.Effects.TeamMemberStore
Galley.Effects.TeamNotificationStore
Galley.Env
diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs
index 26be35593a..7f40e70076 100644
--- a/services/galley/src/Galley/API/Action.hs
+++ b/services/galley/src/Galley/API/Action.hs
@@ -108,7 +108,6 @@ import Wire.API.MLS.Group.Serialisation qualified as Serialisation
import Wire.API.MLS.SubConversation
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
import Wire.API.Team.Member
import Wire.API.Team.Permission (Perm (AddRemoveConvMember, ModifyConvName))
@@ -120,6 +119,7 @@ import Wire.CodeStore.Scope (Scope (ReusableCode))
import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..))
+import Wire.FeaturesConfigSubsystem
import Wire.FederationAPIAccess qualified as E
import Wire.FireAndForget qualified as E
import Wire.NotificationSubsystem
@@ -259,7 +259,8 @@ type family HasConversationActionEffects (tag :: ConversationActionTag) r :: Con
Member ProposalStore r,
Member Random r,
Member TeamFeatureStore r,
- Member TinyLog r
+ Member TinyLog r,
+ Member FeaturesConfigSubsystem r
)
HasConversationActionEffects 'ConversationUpdateAddPermissionTag r =
( Member (Error NoChanges) r,
@@ -520,7 +521,7 @@ performAction ::
Sem r (PerformActionResult tag)
performAction tag origUser lconv action = do
let lcnv = fmap (.id_) lconv
- conv = tUnqualified lconv
+ storedConv = tUnqualified lconv
case tag of
SConversationJoinTag -> do
(extraTargets, action') <- performConversationJoin origUser lconv action
@@ -541,7 +542,7 @@ performAction tag origUser lconv action = do
traverse_ (removeUser lconv RemoveUserExcludeMain) presentVictims
pure $ mkPerformActionResult action -- FUTUREWORK: should we return the filtered action here?
SConversationMemberUpdateTag -> do
- void $ ensureOtherMember lconv (cmuTarget action) conv
+ void $ ensureOtherMember lconv (cmuTarget action) storedConv
E.setOtherMember lcnv (cmuTarget action) (cmuUpdate action)
pure $ mkPerformActionResult action
SConversationDeleteTag -> do
@@ -549,8 +550,8 @@ performAction tag origUser lconv action = do
E.removeAllMLSClients groupId
E.deleteAllProposals groupId
- let cid = conv.id_
- for_ (conv & mlsMetadata <&> cnvmlsGroupId . fst) $ \gidParent -> do
+ let cid = storedConv.id_
+ for_ (storedConv & mlsMetadata <&> cnvmlsGroupId . fst) $ \gidParent -> do
sconvs <- E.listSubConversations cid
for_ (Map.assocs sconvs) $ \(subid, mlsData) -> do
let gidSub = cnvmlsGroupId mlsData
@@ -560,23 +561,23 @@ performAction tag origUser lconv action = do
key <- E.makeKey (tUnqualified lcnv)
E.deleteCode key ReusableCode
- case convTeam conv of
+ case convTeam storedConv of
Nothing -> E.deleteConversation (tUnqualified lcnv)
Just tid -> E.deleteTeamConversation tid (tUnqualified lcnv)
pure $ mkPerformActionResult action
SConversationRenameTag -> do
- zusrMembership <- join <$> forM conv.metadata.cnvmTeam (TeamSubsystem.internalGetTeamMember (qUnqualified origUser))
+ zusrMembership <- join <$> forM storedConv.metadata.cnvmTeam (TeamSubsystem.internalGetTeamMember (qUnqualified origUser))
for_ zusrMembership $ \tm -> unless (tm `hasPermission` ModifyConvName) $ throwS @'InvalidOperation
cn <- rangeChecked (cupName action)
E.setConversationName (tUnqualified lcnv) cn
pure $ mkPerformActionResult action
SConversationMessageTimerUpdateTag -> do
- when (Data.convMessageTimer conv == cupMessageTimer action) noChanges
+ when (Data.convMessageTimer storedConv == cupMessageTimer action) noChanges
E.setConversationMessageTimer (tUnqualified lcnv) (cupMessageTimer action)
pure $ mkPerformActionResult action
SConversationReceiptModeUpdateTag -> do
- when (Data.convReceiptMode conv == Just (cruReceiptMode action)) noChanges
+ when (Data.convReceiptMode storedConv == Just (cruReceiptMode action)) noChanges
E.setConversationReceiptMode (tUnqualified lcnv) (cruReceiptMode action)
pure $ mkPerformActionResult action
SConversationAccessDataTag -> do
@@ -595,9 +596,9 @@ performAction tag origUser lconv action = do
E.updateToMixedProtocol (tUnqualified lcnv) gid epoch
pure $ mkPerformActionResult action
(ProtocolMixedTag, ProtocolMLSTag, Just tid) -> do
- mig <- getFeatureForTeam @MlsMigrationConfig tid
+ mig <- getFeatureForTeam tid
now <- Now.get
- mlsConv <- mkMLSConversation conv >>= noteS @'ConvInvalidProtocolTransition
+ mlsConv <- mkMLSConversation storedConv >>= noteS @'ConvInvalidProtocolTransition
ok <- checkMigrationCriteria now mlsConv mig
unless ok $ throwS @'MLSMigrationCriteriaNotSatisfied
removeExtraneousClients origUser lconv
@@ -611,7 +612,7 @@ performAction tag origUser lconv action = do
noChanges
(_, _, _) -> throwS @'ConvInvalidProtocolTransition
SConversationUpdateAddPermissionTag -> do
- when (conv.metadata.cnvmChannelAddPermission == Just (addPermission action)) noChanges
+ when (storedConv.metadata.cnvmChannelAddPermission == Just (addPermission action)) noChanges
E.updateChannelAddPermissions (tUnqualified lcnv) (addPermission action)
pure $ mkPerformActionResult action
SConversationResetTag -> do
@@ -1039,39 +1040,39 @@ updateLocalStateOfRemoteConv rcu con = do
-- updated, we do **not** add them to the list of targets, because we have no
-- way to make sure that they are actually supposed to receive that notification.
+ let sca = cu.action
(mActualAction, extraTargets) <- case cu.action of
- sca@(SomeConversationAction singTag action) -> case singTag of
- SConversationJoinTag -> do
- let ConversationJoin toAdd role joinType = action
- let (localUsers, remoteUsers) = partitionQualified loc toAdd
- addedLocalUsers <- Set.toList <$> addLocalUsersToRemoteConv rconvId cu.origUserId localUsers
- let allAddedUsers = map (tUntagged . qualifyAs loc) addedLocalUsers <> map tUntagged remoteUsers
- pure $
- ( fmap
- (\users -> SomeConversationAction SConversationJoinTag (ConversationJoin users role joinType))
- (nonEmpty allAddedUsers),
- addedLocalUsers
- )
- SConversationLeaveTag -> do
- let users = foldQualified loc (pure . tUnqualified) (const []) cu.origUserId
- E.deleteMembersInRemoteConversation rconvId users
- pure (Just sca, [])
- SConversationRemoveMembersTag -> do
- let localUsers = getLocalUsers (tDomain loc) . crmTargets $ action
- E.deleteMembersInRemoteConversation rconvId localUsers
- pure (Just sca, [])
- SConversationMemberUpdateTag ->
- pure (Just sca, [])
- SConversationDeleteTag -> do
- E.deleteMembersInRemoteConversation rconvId presentUsers
- pure (Just sca, [])
- SConversationRenameTag -> pure (Just sca, [])
- SConversationMessageTimerUpdateTag -> pure (Just sca, [])
- SConversationReceiptModeUpdateTag -> pure (Just sca, [])
- SConversationAccessDataTag -> pure (Just sca, [])
- SConversationUpdateProtocolTag -> pure (Just sca, [])
- SConversationUpdateAddPermissionTag -> pure (Just sca, [])
- SConversationResetTag -> pure (Just sca, [])
+ SomeConversationAction SConversationJoinTag action -> do
+ let ConversationJoin toAdd role joinType = action
+ let (localUsers, remoteUsers) = partitionQualified loc toAdd
+ addedLocalUsers <- Set.toList <$> addLocalUsersToRemoteConv rconvId cu.origUserId localUsers
+ let allAddedUsers = map (tUntagged . qualifyAs loc) addedLocalUsers <> map tUntagged remoteUsers
+ pure $
+ ( fmap
+ (\users -> SomeConversationAction SConversationJoinTag (ConversationJoin users role joinType))
+ (nonEmpty allAddedUsers),
+ addedLocalUsers
+ )
+ SomeConversationAction SConversationLeaveTag _ -> do
+ let users = foldQualified loc (pure . tUnqualified) (const []) cu.origUserId
+ E.deleteMembersInRemoteConversation rconvId users
+ pure (Just sca, [])
+ SomeConversationAction SConversationRemoveMembersTag action -> do
+ let localUsers = getLocalUsers (tDomain loc) . crmTargets $ action
+ E.deleteMembersInRemoteConversation rconvId localUsers
+ pure (Just sca, [])
+ SomeConversationAction SConversationMemberUpdateTag _ ->
+ pure (Just sca, [])
+ SomeConversationAction SConversationDeleteTag _ -> do
+ E.deleteMembersInRemoteConversation rconvId presentUsers
+ pure (Just sca, [])
+ SomeConversationAction SConversationRenameTag _ -> pure (Just sca, [])
+ SomeConversationAction SConversationMessageTimerUpdateTag _ -> pure (Just sca, [])
+ SomeConversationAction SConversationReceiptModeUpdateTag _ -> pure (Just sca, [])
+ SomeConversationAction SConversationAccessDataTag _ -> pure (Just sca, [])
+ SomeConversationAction SConversationUpdateProtocolTag _ -> pure (Just sca, [])
+ SomeConversationAction SConversationUpdateAddPermissionTag _ -> pure (Just sca, [])
+ SomeConversationAction SConversationResetTag _ -> pure (Just sca, [])
-- On conversation join, the member(s) joining are not included in the presentUsers,
-- however they are included in the alreadyPresentUsers from the incoming request.
diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs
index 6de6c71e7c..d22b336ea7 100644
--- a/services/galley/src/Galley/API/Create.hs
+++ b/services/galley/src/Galley/API/Create.hs
@@ -47,7 +47,6 @@ import Galley.API.Error
import Galley.API.MLS
import Galley.API.Mapping
import Galley.API.One2One
-import Galley.API.Teams.Features.Get (getFeatureForTeam)
import Galley.API.Util
import Galley.App (Env)
import Galley.Effects
@@ -83,6 +82,7 @@ import Wire.API.User
import Wire.BrigAPIAccess
import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
+import Wire.FeaturesConfigSubsystem
import Wire.FederationAPIAccess qualified as E
import Wire.NotificationSubsystem
import Wire.Sem.Now (Now)
@@ -126,7 +126,7 @@ createGroupConversationUpToV3 ::
Member LegalHoldStore r,
Member TeamStore r,
Member P.TinyLog r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamCollaboratorsSubsystem r,
Member Random r,
Member TeamSubsystem r,
@@ -175,7 +175,7 @@ createGroupOwnConversation ::
Member LegalHoldStore r,
Member TeamStore r,
Member P.TinyLog r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamCollaboratorsSubsystem r,
Member Random r,
Member TeamSubsystem r
@@ -223,7 +223,7 @@ createGroupConversation ::
Member LegalHoldStore r,
Member TeamStore r,
Member P.TinyLog r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamCollaboratorsSubsystem r,
Member Random r,
Member TeamSubsystem r
@@ -271,7 +271,7 @@ createGroupConvAndMkResponse ::
Member NotificationSubsystem r,
Member LegalHoldStore r,
Member TeamStore r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamCollaboratorsSubsystem r,
Member Random r,
Member TeamSubsystem r,
@@ -316,7 +316,7 @@ createGroupConversationGeneric ::
Member LegalHoldStore r,
Member TeamStore r,
Member P.TinyLog r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamCollaboratorsSubsystem r,
Member Random r,
Member TeamSubsystem r
@@ -383,8 +383,7 @@ checkCreateConvPermissions ::
Member (ErrorS ChannelsNotEnabled) r,
Member (ErrorS NotAnMlsConversation) r,
Member TeamStore r,
- Member (Input Opts) r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamCollaboratorsSubsystem r,
Member TeamSubsystem r
) =>
@@ -440,8 +439,7 @@ checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do
ensureCreateChannelPermissions ::
forall r.
( Member (ErrorS OperationDenied) r,
- Member (Input Opts) r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member (ErrorS NotATeamMember) r,
Member (ErrorS ChannelsNotEnabled) r,
Member (ErrorS NotAnMlsConversation) r
@@ -450,7 +448,7 @@ checkCreateConvPermissions lusr newConv (Just tinfo) allUsers = do
Maybe TeamMember ->
Sem r ()
ensureCreateChannelPermissions tid (Just tm) = do
- channelsConf <- getFeatureForTeam @ChannelsConfig tid
+ channelsConf :: LockableFeature ChannelsConfig <- getFeatureForTeam tid
when (channelsConf.status == FeatureStatusDisabled) $ throwS @ChannelsNotEnabled
when (newConv.newConvProtocol /= BaseProtocolMLSTag) $ throwS @NotAnMlsConversation
case channelsConf.config.allowedToCreateChannels of
diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs
index f5faa04839..725117fe7f 100644
--- a/services/galley/src/Galley/API/Federation.hs
+++ b/services/galley/src/Galley/API/Federation.hs
@@ -97,6 +97,7 @@ import Wire.CodeStore
import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
+import Wire.FeaturesConfigSubsystem
import Wire.FireAndForget qualified as E
import Wire.NotificationSubsystem
import Wire.Sem.Now (Now)
@@ -506,7 +507,8 @@ updateConversation ::
Member TeamCollaboratorsSubsystem r,
Member E.MLSCommitLockStore r,
Member TeamStore r,
- Member (Input ConversationSubsystemConfig) r
+ Member (Input ConversationSubsystemConfig) r,
+ Member FeaturesConfigSubsystem r
) =>
Domain ->
ConversationUpdateRequest ->
@@ -636,7 +638,6 @@ sendMLSCommitBundle ::
Member (Input Opts) r,
Member Now r,
Member LegalHoldStore r,
- Member TeamFeatureStore r,
Member Resource r,
Member TeamStore r,
Member TeamSubsystem r,
@@ -645,6 +646,7 @@ sendMLSCommitBundle ::
Member ProposalStore r,
Member TeamCollaboratorsSubsystem r,
Member E.MLSCommitLockStore r,
+ Member FeaturesConfigSubsystem r,
Member (Input ConversationSubsystemConfig) r
) =>
Domain ->
diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs
index 85ef937747..7c50cd9d5e 100644
--- a/services/galley/src/Galley/API/Internal.hs
+++ b/services/galley/src/Galley/API/Internal.hs
@@ -94,6 +94,7 @@ import Wire.ConversationStore qualified as E
import Wire.ConversationStore.MLS.Types
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
+import Wire.FeaturesConfigSubsystem (FeaturesConfigSubsystem)
import Wire.LegalHoldStore as LegalHoldStore
import Wire.NotificationSubsystem
import Wire.Sem.Now (Now)
@@ -339,17 +340,16 @@ rmUser ::
Member NotificationSubsystem r,
Member ConversationSubsystem r,
Member (Input Env) r,
- Member (Input Opts) r,
Member Now r,
Member (ListItems p2 TeamId) r,
Member ProposalStore r,
Member P.TinyLog r,
Member Random r,
- Member TeamFeatureStore r,
Member TeamStore r,
Member (Input FanoutLimit) r,
Member TeamSubsystem r,
- Member (Input ConversationSubsystemConfig) r
+ Member (Input ConversationSubsystemConfig) r,
+ Member FeaturesConfigSubsystem r
) =>
Local UserId ->
Maybe ConnId ->
@@ -377,7 +377,7 @@ rmUser lusr conn = do
leaveTeams page = for_ (pageItems page) $ \tid -> do
toNotify <-
handleImpossibleErrors $
- getFeatureForTeam @LimitedEventFanoutConfig tid
+ getFeatureForTeam @_ @LimitedEventFanoutConfig tid
>>= ( \case
FeatureStatusEnabled -> Left <$> E.getTeamAdmins tid
FeatureStatusDisabled -> Right <$> getTeamMembersForFanout tid
diff --git a/services/galley/src/Galley/API/LegalHold/Team.hs b/services/galley/src/Galley/API/LegalHold/Team.hs
index 557b19fee5..977fcfb291 100644
--- a/services/galley/src/Galley/API/LegalHold/Team.hs
+++ b/services/galley/src/Galley/API/LegalHold/Team.hs
@@ -24,11 +24,9 @@ module Galley.API.LegalHold.Team
)
where
-import Data.Default
import Data.Id
import Data.Range
import Galley.Effects
-import Galley.Effects.TeamFeatureStore
import Galley.Env
import Galley.Types.Teams as Team
import Imports
@@ -39,7 +37,8 @@ import Wire.API.Error.Galley
import Wire.API.Team.Feature
import Wire.API.Team.Size
import Wire.BrigAPIAccess
-import Wire.LegalHoldStore qualified as LegalHoldData
+import Wire.LegalHold
+import Wire.TeamFeatureStore
assertLegalHoldEnabledForTeam ::
forall r.
@@ -54,23 +53,6 @@ assertLegalHoldEnabledForTeam tid =
unlessM (isLegalHoldEnabledForTeam tid) $
throwS @'LegalHoldNotEnabled
-computeLegalHoldFeatureStatus ::
- ( Member LegalHoldStore r,
- Member (Input (FeatureDefaults LegalholdConfig)) r
- ) =>
- TeamId ->
- DbFeature LegalholdConfig ->
- Sem r FeatureStatus
-computeLegalHoldFeatureStatus tid dbFeature = do
- featureLegalHold <- input @(FeatureDefaults LegalholdConfig)
- case featureLegalHold of
- FeatureLegalHoldDisabledPermanently -> pure FeatureStatusDisabled
- FeatureLegalHoldDisabledByDefault ->
- pure (applyDbFeature dbFeature def).status
- FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> do
- wl <- LegalHoldData.isTeamLegalholdWhitelisted tid
- pure $ if wl then FeatureStatusEnabled else FeatureStatusDisabled
-
isLegalHoldEnabledForTeam ::
forall r.
( Member LegalHoldStore r,
diff --git a/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs b/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs
index e4dcfc4e5b..798345adf3 100644
--- a/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs
+++ b/services/galley/src/Galley/API/MLS/GroupInfoCheck.hs
@@ -45,6 +45,7 @@ import Wire.API.MLS.Serialisation
import Wire.API.Team.Feature
import Wire.ConversationStore
import Wire.ConversationStore.MLS.Types
+import Wire.FeaturesConfigSubsystem (FeaturesConfigSubsystem)
data GroupInfoMismatch = GroupInfoMismatch
{clients :: [(Int, ClientIdentity)]}
@@ -55,8 +56,8 @@ checkGroupState ::
( Member (Error GroupInfoMismatch) r,
Member (Input Opts) r,
Member (Error MLSProtocolError) r,
- Member TeamFeatureStore r,
- Member ConversationStore r
+ Member ConversationStore r,
+ Member FeaturesConfigSubsystem r
) =>
ConvOrSubConv ->
IndexMap ->
@@ -102,7 +103,7 @@ existingGroupStateMismatch convOrSub =
Right m -> pure m
isGroupInfoCheckEnabled ::
- ( Member TeamFeatureStore r,
+ ( Member FeaturesConfigSubsystem r,
Member (Input Opts) r
) =>
Maybe TeamId ->
@@ -111,5 +112,5 @@ isGroupInfoCheckEnabled Nothing = pure False
isGroupInfoCheckEnabled (Just tid) = fmap isJust . runNonDetMaybe $ do
global <- inputs (view $ settings . checkGroupInfo)
guard (global == Just True)
- mls <- getFeatureForTeam @MLSConfig tid
+ mls <- getFeatureForTeam @_ @MLSConfig tid
guard (getAny mls.config.mlsGroupInfoDiagnostics)
diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs
index 7a7e87b7b6..8ca29cac36 100644
--- a/services/galley/src/Galley/API/MLS/Message.hs
+++ b/services/galley/src/Galley/API/MLS/Message.hs
@@ -89,6 +89,7 @@ import Wire.ConversationStore
import Wire.ConversationStore.MLS.Types
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
+import Wire.FeaturesConfigSubsystem
import Wire.FederationAPIAccess
import Wire.NotificationSubsystem
import Wire.Sem.Now qualified as Now
@@ -178,7 +179,6 @@ postMLSCommitBundle ::
Member (Error MLSOutOfSyncError) r,
Member (ErrorS GroupIdVersionNotSupported) r,
Member (Input EnableOutOfSyncCheck) r,
- Member TeamFeatureStore r,
Member Random r,
Member Resource r,
Members MLSBundleStaticErrors r,
@@ -186,7 +186,8 @@ postMLSCommitBundle ::
Member ConversationSubsystem r,
Member MLSCommitLockStore r,
Member TeamSubsystem r,
- Member (Input ConversationSubsystemConfig) r
+ Member (Input ConversationSubsystemConfig) r,
+ Member FeaturesConfigSubsystem r
) =>
Local x ->
Qualified UserId ->
@@ -209,7 +210,6 @@ postMLSCommitBundleFromLocalUser ::
Member (Error GroupInfoDiagnostics) r,
Member (Error MLSOutOfSyncError) r,
Member (ErrorS GroupIdVersionNotSupported) r,
- Member TeamFeatureStore r,
Member Random r,
Member Resource r,
Members MLSBundleStaticErrors r,
@@ -217,7 +217,8 @@ postMLSCommitBundleFromLocalUser ::
Member ConversationSubsystem r,
Member MLSCommitLockStore r,
Member TeamSubsystem r,
- Member (Input ConversationSubsystemConfig) r
+ Member (Input ConversationSubsystemConfig) r,
+ Member FeaturesConfigSubsystem r
) =>
Version ->
Local UserId ->
@@ -244,7 +245,6 @@ postMLSCommitBundleToLocalConv ::
Member (Error MLSOutOfSyncError) r,
Member (ErrorS GroupIdVersionNotSupported) r,
Member (Input EnableOutOfSyncCheck) r,
- Member TeamFeatureStore r,
Member Random r,
Member Resource r,
Members MLSBundleStaticErrors r,
@@ -252,7 +252,8 @@ postMLSCommitBundleToLocalConv ::
Member ConversationSubsystem r,
Member MLSCommitLockStore r,
Member TeamSubsystem r,
- Member (Input ConversationSubsystemConfig) r
+ Member (Input ConversationSubsystemConfig) r,
+ Member FeaturesConfigSubsystem r
) =>
Qualified UserId ->
ClientId ->
diff --git a/services/galley/src/Galley/API/Public/Bot.hs b/services/galley/src/Galley/API/Public/Bot.hs
index 4d15132549..1a28c98dfe 100644
--- a/services/galley/src/Galley/API/Public/Bot.hs
+++ b/services/galley/src/Galley/API/Public/Bot.hs
@@ -25,7 +25,6 @@ import Galley.API.Update
import Galley.App
import Galley.Effects
import Galley.Effects qualified as E
-import Galley.Options
import Polysemy
import Polysemy.Input
import Wire.API.Error
@@ -34,6 +33,7 @@ import Wire.API.Event.Team qualified as Public ()
import Wire.API.Provider.Bot
import Wire.API.Routes.API
import Wire.API.Routes.Public.Galley.Bot
+import Wire.FeaturesConfigSubsystem (FeaturesConfigSubsystem)
import Wire.TeamSubsystem (TeamSubsystem)
botAPI :: API BotAPI GalleyEffects
@@ -45,12 +45,11 @@ getBotConversation ::
forall r.
( Member E.ConversationStore r,
Member (Input (Local ())) r,
- Member (Input Opts) r,
- Member TeamFeatureStore r,
Member (ErrorS 'AccessDenied) r,
Member (ErrorS 'ConvNotFound) r,
Member TeamStore r,
- Member TeamSubsystem r
+ Member TeamSubsystem r,
+ Member FeaturesConfigSubsystem r
) =>
BotId ->
ConvId ->
diff --git a/services/galley/src/Galley/API/Public/Feature.hs b/services/galley/src/Galley/API/Public/Feature.hs
index 09e315fc0b..81f8d6247c 100644
--- a/services/galley/src/Galley/API/Public/Feature.hs
+++ b/services/galley/src/Galley/API/Public/Feature.hs
@@ -29,6 +29,7 @@ import Wire.API.Routes.API
import Wire.API.Routes.Public.Galley.Feature
import Wire.API.Routes.Version
import Wire.API.Team.Feature
+import Wire.FeaturesConfigSubsystem (getAllTeamFeaturesForTeamMember)
featureAPIGetPut :: forall cfg r. (_) => API (FeatureAPIGetPut cfg) r
featureAPIGetPut =
@@ -62,7 +63,7 @@ featureAPI =
<@> hoistAPI id featureAPIGetPut
<@> mkNamedAPI @'("get", LimitedEventFanoutConfig) getFeature
<@> mkNamedAPI @"get-all-feature-configs-for-user" getAllTeamFeaturesForUser
- <@> mkNamedAPI @"get-all-feature-configs-for-team" getAllTeamFeaturesForTeam
+ <@> mkNamedAPI @"get-all-feature-configs-for-team" getAllTeamFeaturesForTeamMember
<@> deprecatedFeatureConfigAPI
<@> deprecatedFeatureAPI
<@> mkNamedAPI @'("get", DomainRegistrationConfig) getFeature
diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs
index 50cec71d28..ee61bf748a 100644
--- a/services/galley/src/Galley/API/Query.hs
+++ b/services/galley/src/Galley/API/Query.hs
@@ -77,7 +77,6 @@ import Galley.API.Teams.Features.Get
import Galley.API.Util
import Galley.Effects
import Galley.Env
-import Galley.Options
import Imports
import Polysemy
import Polysemy.Error
@@ -111,6 +110,7 @@ import Wire.CodeStore.Code (Code (codeConversation))
import Wire.CodeStore.Code qualified as Data
import Wire.ConversationStore qualified as E
import Wire.ConversationStore.MLS.Types
+import Wire.FeaturesConfigSubsystem
import Wire.FederationAPIAccess qualified as E
import Wire.HashPassword (HashPassword)
import Wire.RateLimit
@@ -641,8 +641,7 @@ getConversationByReusableCode ::
Member (ErrorS 'ConvAccessDenied) r,
Member (ErrorS 'GuestLinksDisabled) r,
Member (ErrorS 'NotATeamMember) r,
- Member TeamFeatureStore r,
- Member (Input Opts) r,
+ Member FeaturesConfigSubsystem r,
Member HashPassword r,
Member RateLimit r,
Member TeamSubsystem r
@@ -669,8 +668,7 @@ getConversationByReusableCode lusr key value = do
ensureGuestLinksEnabled ::
forall r.
( Member (ErrorS 'GuestLinksDisabled) r,
- Member TeamFeatureStore r,
- Member (Input Opts) r
+ Member FeaturesConfigSubsystem r
) =>
Maybe TeamId ->
Sem r ()
@@ -684,8 +682,7 @@ getConversationGuestLinksStatus ::
( Member ConversationStore r,
Member (ErrorS 'ConvNotFound) r,
Member (ErrorS 'ConvAccessDenied) r,
- Member (Input Opts) r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamSubsystem r
) =>
UserId ->
@@ -699,13 +696,11 @@ getConversationGuestLinksStatus uid convId = do
getConversationGuestLinksFeatureStatus ::
forall r.
- ( Member TeamFeatureStore r,
- Member (Input Opts) r
- ) =>
+ (Member FeaturesConfigSubsystem r) =>
Maybe TeamId ->
Sem r (LockableFeature GuestLinksConfig)
-getConversationGuestLinksFeatureStatus Nothing = getFeatureForServer @GuestLinksConfig
-getConversationGuestLinksFeatureStatus (Just tid) = getFeatureForTeam @GuestLinksConfig tid
+getConversationGuestLinksFeatureStatus Nothing = getFeatureForServer
+getConversationGuestLinksFeatureStatus (Just tid) = getFeatureForTeam 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 7bee76d593..2ac4938634 100644
--- a/services/galley/src/Galley/API/Teams.hs
+++ b/services/galley/src/Galley/API/Teams.hs
@@ -132,6 +132,7 @@ import Wire.CodeStore
import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
+import Wire.FeaturesConfigSubsystem
import Wire.ListItems qualified as E
import Wire.NotificationSubsystem
import Wire.Sem.Now
@@ -708,11 +709,10 @@ deleteTeamMember ::
Member (ErrorS 'TeamNotFound) r,
Member (ErrorS 'NotATeamMember) r,
Member (ErrorS OperationDenied) r,
- Member (Input Opts) r,
Member Now r,
Member NotificationSubsystem r,
Member ConversationSubsystem r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamStore r,
Member P.TinyLog r,
Member (Input FanoutLimit) r,
@@ -737,11 +737,10 @@ deleteNonBindingTeamMember ::
Member (ErrorS 'TeamNotFound) r,
Member (ErrorS 'NotATeamMember) r,
Member (ErrorS OperationDenied) r,
- Member (Input Opts) r,
Member Now r,
Member NotificationSubsystem r,
Member ConversationSubsystem r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamStore r,
Member P.TinyLog r,
Member (Input FanoutLimit) r,
@@ -766,11 +765,10 @@ deleteTeamMember' ::
Member (ErrorS 'TeamNotFound) r,
Member (ErrorS 'NotATeamMember) r,
Member (ErrorS OperationDenied) r,
- Member (Input Opts) r,
Member Now r,
Member NotificationSubsystem r,
Member ConversationSubsystem r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamStore r,
Member P.TinyLog r,
Member (Input FanoutLimit) r,
@@ -812,16 +810,14 @@ deleteTeamMember' lusr zcon tid remove mBody = do
Journal.teamUpdate tid sizeAfterDelete $ filter (/= remove) owners
pure TeamMemberDeleteAccepted
else do
- getFeatureForTeam @LimitedEventFanoutConfig tid
- >>= ( \case
- FeatureStatusEnabled -> do
- admins <- E.getTeamAdmins tid
- uncheckedDeleteTeamMember lusr (Just zcon) tid remove (Left admins)
- FeatureStatusDisabled -> do
- mems <- getTeamMembersForFanout tid
- uncheckedDeleteTeamMember lusr (Just zcon) tid remove (Right mems)
- )
- . (.status)
+ (feat :: LockableFeature LimitedEventFanoutConfig) <- getFeatureForTeam tid
+ case feat.status of
+ FeatureStatusEnabled -> do
+ admins <- E.getTeamAdmins tid
+ uncheckedDeleteTeamMember lusr (Just zcon) tid remove (Left admins)
+ FeatureStatusDisabled -> do
+ mems <- getTeamMembersForFanout tid
+ uncheckedDeleteTeamMember lusr (Just zcon) tid remove (Right mems)
pure TeamMemberDeleteCompleted
-- This function is "unchecked" because it does not validate that the user has the `RemoveTeamMember` permission.
@@ -1311,10 +1307,9 @@ removeTeamCollaborator ::
Member (ErrorS NotATeamMember) r,
Member NotificationSubsystem r,
Member ConversationSubsystem r,
- Member (Input Opts) r,
Member Now r,
Member P.TinyLog r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamStore r,
Member TeamCollaboratorsSubsystem r,
Member (Input FanoutLimit) r,
@@ -1331,7 +1326,7 @@ removeTeamCollaborator lusr tid rusr = do
zusrMember <- TeamSubsystem.internalGetTeamMember (tUnqualified lusr) tid
void $ permissionCheck RemoveTeamCollaborator zusrMember
toNotify <-
- getFeatureForTeam @LimitedEventFanoutConfig tid
+ (getFeatureForTeam @_ @LimitedEventFanoutConfig tid)
>>= ( \case
FeatureStatusEnabled -> Left <$> E.getTeamAdmins tid
FeatureStatusDisabled -> Right <$> getTeamMembersForFanout tid
diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs
index 13f22e658e..4c9bf4b979 100644
--- a/services/galley/src/Galley/API/Teams/Features.hs
+++ b/services/galley/src/Galley/API/Teams/Features.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE UndecidableSuperClasses #-}
{-# OPTIONS_GHC -Wno-ambiguous-fields #-}
-- This file is part of the Wire Server implementation.
@@ -48,7 +49,6 @@ import Galley.API.Util (assertTeamExists, getTeamMembersForFanout, permissionChe
import Galley.App
import Galley.Effects
import Galley.Effects.SearchVisibilityStore qualified as SearchVisibilityData
-import Galley.Effects.TeamFeatureStore
import Galley.Env (FanoutLimit)
import Galley.Options
import Galley.Types.Teams
@@ -71,27 +71,33 @@ import Wire.CodeStore
import Wire.ConversationStore (MLSCommitLockStore)
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
+import Wire.FeaturesConfigSubsystem (FeaturesConfigSubsystem)
+import Wire.FeaturesConfigSubsystem.Types (GetFeatureConfigEffects)
+import Wire.FeaturesConfigSubsystem.Utils (resolveServerFeature)
import Wire.NotificationSubsystem
import Wire.Sem.Now (Now)
import Wire.Sem.Paging
import Wire.Sem.Paging.Cassandra
import Wire.TeamCollaboratorsSubsystem
+import Wire.TeamFeatureStore
import Wire.TeamSubsystem (TeamSubsystem)
import Wire.TeamSubsystem qualified as TeamSubsystem
+type ComputeFeatureConstraints cfg r = (Member FeaturesConfigSubsystem r)
+
patchFeatureInternal ::
forall cfg r.
( SetFeatureConfig cfg,
ComputeFeatureConstraints cfg r,
SetFeatureForTeamConstraints cfg r,
Member (ErrorS 'TeamNotFound) r,
- Member (Input Opts) r,
Member TeamStore r,
Member TeamFeatureStore r,
Member P.TinyLog r,
Member NotificationSubsystem r,
Member (Input FanoutLimit) r,
- Member TeamSubsystem r
+ Member TeamSubsystem r,
+ GetFeatureConfigEffects r
) =>
TeamId ->
LockableFeaturePatch cfg ->
@@ -99,12 +105,12 @@ patchFeatureInternal ::
patchFeatureInternal tid patch = do
assertTeamExists tid
dbFeature <- getDbFeature tid
- defFeature <- getFeatureForServer @cfg
+ (defFeature :: LockableFeature cfg) <- resolveServerFeature
let dbFeatureWithDefaults = dbFeature.applyDbFeature defFeature
let patchedFeature = applyPatch dbFeatureWithDefaults
prepareFeature tid patchedFeature
patchDbFeature tid patch
- returnedFeature <- getFeatureForTeam @cfg tid
+ (returnedFeature :: LockableFeature cfg) <- getFeatureForTeam tid
pushFeatureEvent @cfg tid (mkUpdateEvent tid returnedFeature)
pure returnedFeature
where
@@ -124,7 +130,6 @@ setFeature ::
Member (ErrorS 'NotATeamMember) r,
Member (ErrorS OperationDenied) r,
Member (Error TeamFeatureError) r,
- Member (Input Opts) r,
Member TeamFeatureStore r,
Member P.TinyLog r,
Member NotificationSubsystem r,
@@ -147,7 +152,6 @@ setFeatureInternal ::
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,
@@ -168,7 +172,6 @@ setFeatureUnchecked ::
ComputeFeatureConstraints cfg r,
SetFeatureForTeamConstraints cfg r,
Member (Error TeamFeatureError) r,
- Member (Input Opts) r,
Member TeamFeatureStore r,
Member (P.Logger (Log.Msg -> Log.Msg)) r,
Member NotificationSubsystem r,
@@ -179,7 +182,7 @@ setFeatureUnchecked ::
Feature cfg ->
Sem r (LockableFeature cfg)
setFeatureUnchecked tid feat = do
- feat0 <- getFeatureForTeam @cfg tid
+ (feat0 :: LockableFeature cfg) <- getFeatureForTeam tid
guardLockStatus feat0.lockStatus
setFeatureForTeam @cfg tid (withLockStatus feat0.lockStatus feat)
@@ -202,7 +205,6 @@ persistFeature ::
forall cfg r.
( GetFeatureConfig cfg,
ComputeFeatureConstraints cfg r,
- Member (Input Opts) r,
Member TeamFeatureStore r
) =>
TeamId ->
@@ -210,7 +212,7 @@ persistFeature ::
Sem r (LockableFeature cfg)
persistFeature tid feat = do
setDbFeature tid feat
- getFeatureForTeam @cfg tid
+ getFeatureForTeam tid
pushFeatureEvent ::
forall cfg r.
@@ -251,7 +253,6 @@ setFeatureForTeam ::
( SetFeatureConfig cfg,
SetFeatureForTeamConstraints cfg r,
ComputeFeatureConstraints cfg r,
- Member (Input Opts) r,
Member P.TinyLog r,
Member NotificationSubsystem r,
Member TeamFeatureStore r,
@@ -404,10 +405,11 @@ instance SetFeatureConfig MLSConfig where
SetFeatureForTeamConstraints MLSConfig (r :: EffectRow) =
( Member (Input Opts) r,
Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member (Error TeamFeatureError) r
)
prepareFeature tid feat = do
- mlsMigrationConfig <- getFeatureForTeam @MlsMigrationConfig tid
+ (mlsMigrationConfig :: LockableFeature MlsMigrationConfig) <- getFeatureForTeam tid
unless
( -- default protocol needs to be included in supported protocols
feat.config.mlsDefaultProtocol `elem` feat.config.mlsSupportedProtocols
@@ -441,10 +443,11 @@ instance SetFeatureConfig MlsMigrationConfig where
SetFeatureForTeamConstraints MlsMigrationConfig (r :: EffectRow) =
( Member (Input Opts) r,
Member (Error TeamFeatureError) r,
- Member TeamFeatureStore r
+ Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r
)
prepareFeature tid feat = do
- mlsConfig <- getFeatureForTeam @MLSConfig tid
+ (mlsConfig :: LockableFeature MLSConfig) <- getFeatureForTeam tid
unless
( -- when MLS migration is enabled, MLS needs to be enabled as well
feat.status == FeatureStatusDisabled || mlsConfig.status == FeatureStatusEnabled
diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs
index e99b035ccc..ece1922543 100644
--- a/services/galley/src/Galley/API/Teams/Features/Get.hs
+++ b/services/galley/src/Galley/API/Teams/Features/Get.hs
@@ -1,4 +1,3 @@
-{-# LANGUAGE UndecidableSuperClasses #-}
{-# OPTIONS_GHC -Wno-ambiguous-fields #-}
-- This file is part of the Wire Server implementation.
@@ -27,7 +26,6 @@ module Galley.API.Teams.Features.Get
getSingleFeatureForUser,
GetFeatureConfig (..),
getFeatureForTeam,
- getFeatureForServer,
guardSecondFactorDisabled,
DoAuth (..),
featureEnabledForTeam,
@@ -36,106 +34,34 @@ module Galley.API.Teams.Features.Get
where
import Control.Error (hush)
-import Control.Lens
-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
import Galley.Effects
-import Galley.Effects.TeamFeatureStore
-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
import Wire.API.Error.Galley
import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti qualified as Multi
import Wire.API.Team.Feature
-import Wire.BrigAPIAccess (getAccountConferenceCallingConfigClient)
import Wire.ConversationStore as ConversationStore
+import Wire.FeaturesConfigSubsystem
+import Wire.FeaturesConfigSubsystem.Types
+import Wire.TeamFeatureStore
import Wire.TeamStore qualified as TeamStore
import Wire.TeamSubsystem (TeamSubsystem)
import Wire.TeamSubsystem qualified as TeamSubsystem
data DoAuth = DoAuth UserId | DontDoAuth
-type DefaultGetFeatureForUserConstraints cfg r =
- ( Member (Input Opts) r,
- Member TeamFeatureStore r,
- ComputeFeatureConstraints cfg r
- )
-
--- | Don't export methods of this typeclass
-class
- ( IsFeatureConfig cfg,
- GetFeatureDefaults (FeatureDefaults cfg),
- NpProject cfg Features
- ) =>
- GetFeatureConfig cfg
- where
- type GetFeatureForUserConstraints cfg (r :: EffectRow) :: Constraint
- type
- GetFeatureForUserConstraints cfg (r :: EffectRow) =
- DefaultGetFeatureForUserConstraints cfg r
-
- type ComputeFeatureConstraints cfg (r :: EffectRow) :: Constraint
- type ComputeFeatureConstraints cfg r = ()
-
- getFeatureForUser ::
- (GetFeatureForUserConstraints cfg r) =>
- UserId ->
- Sem r (LockableFeature cfg)
- default getFeatureForUser ::
- (DefaultGetFeatureForUserConstraints cfg r) =>
- UserId ->
- Sem r (LockableFeature cfg)
- getFeatureForUser _ = getFeatureForServer
-
- computeFeature ::
- (ComputeFeatureConstraints cfg r) =>
- TeamId ->
- LockableFeature cfg ->
- DbFeature cfg ->
- Sem r (LockableFeature cfg)
- default computeFeature ::
- TeamId ->
- LockableFeature cfg ->
- DbFeature cfg ->
- Sem r (LockableFeature cfg)
- computeFeature _tid defFeature dbFeature =
- pure $
- resolveDbFeature @cfg defFeature dbFeature
-
-getFeature ::
- forall cfg r.
- ( GetFeatureConfig cfg,
- ComputeFeatureConstraints cfg r,
- Member (Input Opts) r,
- Member TeamFeatureStore r,
- Member (ErrorS 'NotATeamMember) r,
- Member TeamSubsystem r
- ) =>
- UserId ->
- TeamId ->
- Sem r (LockableFeature cfg)
-getFeature uid tid = do
- void $ TeamSubsystem.internalGetTeamMember uid tid >>= 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
+ Member TeamStore r,
+ Member FeaturesConfigSubsystem r
) =>
TeamId ->
Sem r (LockableFeature cfg)
@@ -162,47 +88,11 @@ getTeamAndCheckMembership uid = do
assertTeamExists tid
pure mTid
-getAllTeamFeaturesForTeam ::
- forall r.
- ( Member (Input Opts) r,
- Member (ErrorS 'NotATeamMember) r,
- Member LegalHoldStore r,
- Member TeamFeatureStore r,
- Member TeamStore r,
- Member (Input (FeatureDefaults LegalholdConfig)) r,
- Member TeamSubsystem r
- ) =>
- Local UserId ->
- TeamId ->
- Sem r AllTeamFeatures
-getAllTeamFeaturesForTeam luid tid = do
- void $ TeamSubsystem.internalGetTeamMember (tUnqualified luid) tid >>= noteS @'NotATeamMember
- getAllTeamFeatures tid
-
-class
- (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) =>
- GetAllFeaturesForServerConstraints r cfg
-
-instance
- (GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) =>
- GetAllFeaturesForServerConstraints r cfg
-
-getAllTeamFeaturesForServer ::
- forall r.
- (Member (Input Opts) r) =>
- Sem r AllTeamFeatures
-getAllTeamFeaturesForServer =
- hsequence' $
- hcpure (Proxy @GetFeatureConfig) $
- Comp getFeatureForServer
-
getAllTeamFeatures ::
forall r.
- ( Member (Input Opts) r,
- Member LegalHoldStore r,
- Member TeamFeatureStore r,
- Member TeamStore r,
- Member (Input (FeatureDefaults LegalholdConfig)) r
+ ( Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
+ GetFeatureConfigEffects r
) =>
TeamId ->
Sem r AllTeamFeatures
@@ -212,28 +102,21 @@ getAllTeamFeatures tid = do
hsequence' $ hcliftA2 (Proxy @(GetAllFeaturesForServerConstraints r)) compute defFeatures features
where
compute ::
- (ComputeFeatureConstraints p r, GetFeatureConfig p) =>
+ (GetFeatureConfig p) =>
LockableFeature p ->
DbFeature p ->
(Sem r :.: LockableFeature) p
compute defFeature feat = Comp $ computeFeature tid defFeature feat
-class (GetFeatureForUserConstraints cfg r, GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllTeamFeaturesForUserConstraints r cfg
-
-instance (GetFeatureForUserConstraints cfg r, GetFeatureConfig cfg, ComputeFeatureConstraints cfg r) => GetAllTeamFeaturesForUserConstraints r cfg
-
getAllTeamFeaturesForUser ::
forall r.
- ( Member BrigAPIAccess r,
- Member (ErrorS 'NotATeamMember) r,
+ ( Member (ErrorS 'NotATeamMember) r,
Member (ErrorS 'TeamNotFound) r,
- Member (ErrorS OperationDenied) r,
- Member (Input Opts) r,
- Member LegalHoldStore r,
Member TeamFeatureStore r,
Member TeamStore r,
- Member (Input (FeatureDefaults LegalholdConfig)) r,
- Member TeamSubsystem r
+ Member TeamSubsystem r,
+ Member FeaturesConfigSubsystem r,
+ GetFeatureConfigEffects r
) =>
UserId ->
Sem r AllTeamFeatures
@@ -246,191 +129,17 @@ getAllTeamFeaturesForUser uid = do
getSingleFeatureForUser ::
forall cfg r.
( GetFeatureConfig cfg,
- Member (Input Opts) r,
Member (ErrorS 'NotATeamMember) r,
Member (ErrorS 'TeamNotFound) r,
Member TeamStore r,
- Member TeamFeatureStore r,
- GetFeatureForUserConstraints cfg r,
- ComputeFeatureConstraints cfg r,
- Member TeamSubsystem r
+ Member TeamSubsystem r,
+ Member FeaturesConfigSubsystem r
) =>
UserId ->
Sem r (LockableFeature cfg)
getSingleFeatureForUser uid = do
mTid <- getTeamAndCheckMembership uid
- getFeatureForTeamUser @cfg uid mTid
-
-getFeatureForTeam ::
- forall cfg r.
- ( GetFeatureConfig cfg,
- ComputeFeatureConstraints cfg r,
- Member (Input Opts) r,
- Member TeamFeatureStore r
- ) =>
- TeamId ->
- Sem r (LockableFeature cfg)
-getFeatureForTeam tid = do
- dbFeature <- getDbFeature tid
- defFeature <- getFeatureForServer
- computeFeature @cfg
- tid
- defFeature
- dbFeature
-
-getFeatureForTeamUser ::
- forall cfg r.
- ( GetFeatureConfig cfg,
- GetFeatureForUserConstraints cfg r,
- ComputeFeatureConstraints cfg r,
- Member (Input Opts) r,
- Member TeamFeatureStore r
- ) =>
- UserId ->
- Maybe TeamId ->
- Sem r (LockableFeature cfg)
-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
-
-instance GetFeatureConfig SearchVisibilityAvailableConfig
-
-instance GetFeatureConfig ValidateSAMLEmailsConfig
-
-instance GetFeatureConfig DigitalSignaturesConfig
-
-instance GetFeatureConfig LegalholdConfig where
- type
- GetFeatureForUserConstraints LegalholdConfig (r :: EffectRow) =
- ( Member (Input Opts) r,
- Member TeamFeatureStore r,
- Member LegalHoldStore r,
- Member TeamStore r,
- Member (Input (FeatureDefaults LegalholdConfig)) r,
- Member (ErrorS OperationDenied) r,
- Member (ErrorS 'NotATeamMember) r,
- Member (ErrorS 'TeamNotFound) r
- )
- type
- ComputeFeatureConstraints LegalholdConfig r =
- ( Member TeamStore r,
- Member LegalHoldStore r,
- Member (Input (FeatureDefaults LegalholdConfig)) r
- )
-
- computeFeature tid defFeature dbFeature = do
- status <- computeLegalHoldFeatureStatus tid dbFeature
- pure $ defFeature {status = status}
-
-instance GetFeatureConfig FileSharingConfig
-
-instance GetFeatureConfig AppLockConfig
-
-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
--- "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
- GetFeatureForUserConstraints ConferenceCallingConfig r =
- ( Member (Input Opts) r,
- Member (ErrorS OperationDenied) r,
- Member (ErrorS 'NotATeamMember) r,
- Member (ErrorS 'TeamNotFound) r,
- Member TeamStore r,
- Member TeamFeatureStore r,
- Member BrigAPIAccess r
- )
-
- getFeatureForUser uid = do
- 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
-
-instance GetFeatureConfig GuestLinksConfig
-
-instance GetFeatureConfig SndFactorPasswordChallengeConfig
-
-instance GetFeatureConfig SearchVisibilityInboundConfig
-
-instance GetFeatureConfig MLSConfig
-
-instance GetFeatureConfig ChannelsConfig
-
-instance GetFeatureConfig ExposeInvitationURLsToTeamAdminConfig where
- 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 dbFeature = do
- allowList <- input <&> view (settings . exposeInvitationURLsTeamAllowlist . to (fromMaybe []))
- let teamAllowed = tid `elem` allowList
- lockStatus = if teamAllowed then LockStatusUnlocked else LockStatusLocked
- pure $ resolveDbFeature defFeature (dbFeatureLockStatus lockStatus <> dbFeature)
-
-instance GetFeatureConfig OutlookCalIntegrationConfig
-
-instance GetFeatureConfig MlsE2EIdConfig
-
-instance GetFeatureConfig MlsMigrationConfig
-
-instance GetFeatureConfig EnforceFileDownloadLocationConfig
-
-instance GetFeatureConfig LimitedEventFanoutConfig
-
-instance GetFeatureConfig DomainRegistrationConfig
-
-instance GetFeatureConfig CellsConfig
-
-instance GetFeatureConfig CellsInternalConfig
-
-instance GetFeatureConfig AllowedGlobalOperationsConfig
-
-instance GetFeatureConfig AssetAuditLogConfig
-
-instance GetFeatureConfig ConsumableNotificationsConfig
-
-instance GetFeatureConfig ChatBubblesConfig
-
-instance GetFeatureConfig AppsConfig
-
-instance GetFeatureConfig SimplifiedUserConnectionRequestQRCodeConfig
-
-instance GetFeatureConfig StealthUsersConfig
-
-instance GetFeatureConfig MeetingsConfig
-
-instance GetFeatureConfig MeetingsPremiumConfig
+ getFeatureForTeamUser @_ @cfg uid mTid
-- | 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
@@ -439,11 +148,10 @@ instance GetFeatureConfig MeetingsPremiumConfig
-- This function exists to resolve a cyclic dependency.
guardSecondFactorDisabled ::
forall r.
- ( Member TeamFeatureStore r,
- Member (Input Opts) r,
- Member (ErrorS 'AccessDenied) r,
+ ( Member (ErrorS 'AccessDenied) r,
Member TeamStore r,
- Member ConversationStore r
+ Member ConversationStore r,
+ Member FeaturesConfigSubsystem r
) =>
UserId ->
ConvId ->
@@ -455,7 +163,7 @@ guardSecondFactorDisabled uid cid = do
mapError (unTagged @'TeamNotFound @()) $ assertTeamExists tid
pure tid
- tf <- getFeatureForTeamUser @SndFactorPasswordChallengeConfig uid mTid
+ tf <- getFeatureForTeamUser @_ @SndFactorPasswordChallengeConfig uid mTid
case tf.status of
FeatureStatusDisabled -> pure ()
FeatureStatusEnabled -> throwS @'AccessDenied
@@ -463,11 +171,9 @@ guardSecondFactorDisabled uid cid = do
featureEnabledForTeam ::
forall cfg r.
( GetFeatureConfig cfg,
- Member (Input Opts) r,
Member (ErrorS 'TeamNotFound) r,
Member TeamStore r,
- Member TeamFeatureStore r,
- ComputeFeatureConstraints cfg r
+ Member FeaturesConfigSubsystem r
) =>
TeamId ->
Sem r Bool
diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs
index c5df5e029e..8596392cef 100644
--- a/services/galley/src/Galley/API/Update.hs
+++ b/services/galley/src/Galley/API/Update.hs
@@ -139,6 +139,7 @@ import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig)
import Wire.ExternalAccess qualified as E
+import Wire.FeaturesConfigSubsystem
import Wire.FederationAPIAccess qualified as E
import Wire.HashPassword as HashPassword
import Wire.NotificationSubsystem
@@ -531,7 +532,7 @@ addCodeUnqualifiedWithReqBody ::
Member Now r,
Member HashPassword r,
Member (Input Opts) r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member RateLimit r,
Member TeamSubsystem r
) =>
@@ -557,7 +558,7 @@ addCodeUnqualified ::
Member Now r,
Member (Input Opts) r,
Member HashPassword r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member RateLimit r,
Member TeamSubsystem r
) =>
@@ -585,7 +586,7 @@ addCode ::
Member NotificationSubsystem r,
Member Now r,
Member (Input Opts) r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member RateLimit r,
Member TeamSubsystem r
) =>
@@ -683,8 +684,7 @@ getCode ::
Member (ErrorS 'ConvAccessDenied) r,
Member (ErrorS 'ConvNotFound) r,
Member (ErrorS 'GuestLinksDisabled) r,
- Member (Input Opts) r,
- Member TeamFeatureStore r
+ Member FeaturesConfigSubsystem r
) =>
Maybe ZHostValue ->
Local UserId ->
@@ -705,11 +705,10 @@ checkReusableCode ::
forall r.
( Member CodeStore r,
Member ConversationStore r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member (ErrorS 'CodeNotFound) r,
Member (ErrorS 'ConvNotFound) r,
Member (ErrorS 'InvalidConversationPassword) r,
- Member (Input Opts) r,
Member HashPassword r,
Member RateLimit r
) =>
@@ -749,6 +748,7 @@ updateConversationProtocolWithLocalUser ::
Member Random r,
Member ProposalStore r,
Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member TeamCollaboratorsSubsystem r,
Member E.MLSCommitLockStore r,
Member TeamSubsystem r,
@@ -836,8 +836,7 @@ joinConversationByReusableCode ::
Member (ErrorS 'NotATeamMember) r,
Member (ErrorS 'TooManyMembers) r,
Member ConversationSubsystem r,
- Member (Input Opts) r,
- Member TeamFeatureStore r,
+ Member FeaturesConfigSubsystem r,
Member HashPassword r,
Member RateLimit r,
Member TeamSubsystem r,
@@ -1922,8 +1921,7 @@ updateCellsState ::
( Member ConversationStore r,
Member (ErrorS ConvNotFound) r,
Member (ErrorS InvalidOperation) r,
- Member (Input Opts) r,
- Member TeamFeatureStore r
+ Member FeaturesConfigSubsystem r
) =>
ConvId ->
CellsState ->
@@ -1932,7 +1930,7 @@ updateCellsState cnv state = do
when (state /= CellsDisabled) $ do
conv <- E.getConversation cnv >>= noteS @ConvNotFound
tid <- noteS @InvalidOperation conv.metadata.cnvmTeam
- feat <- getFeatureForTeam @CellsConfig tid
+ (feat :: LockableFeature CellsConfig) <- getFeatureForTeam tid
noteS @InvalidOperation $ guard (feat.status == FeatureStatusEnabled)
E.setConversationCellsState cnv state
diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs
index d7c34ca312..1c790dd3af 100644
--- a/services/galley/src/Galley/App.hs
+++ b/services/galley/src/Galley/App.hs
@@ -113,6 +113,8 @@ import Wire.ConversationStore.Postgres
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..), interpretConversationSubsystem)
import Wire.Error
import Wire.ExternalAccess.External
+import Wire.FeaturesConfigSubsystem.Interpreter (runFeaturesConfigSubsystem)
+import Wire.FeaturesConfigSubsystem.Types (ExposeInvitationURLsAllowlist (..))
import Wire.FederationAPIAccess.Interpreter
import Wire.FireAndForget
import Wire.GundeckAPIAccess (runGundeckAPIAccess)
@@ -350,8 +352,9 @@ evalGalley e =
. runInputConst (e ^. options)
. runInputConst localUnit
. interpretTeamFeatureSpecialContext e
- . runInputSem getAllTeamFeaturesForServer
. runInputConst (currentFanoutLimit (e ^. options))
+ . runInputSem (inputs @Opts $ view (O.settings . O.featureFlags))
+ . runInputSem (inputs @Opts $ ExposeInvitationURLsAllowlist . fromMaybe [] . view (O.settings . O.exposeInvitationURLsTeamAllowlist))
. interpretInternalTeamListToCassandra
. interpretTeamListToCassandra
. interpretTeamMemberStoreToCassandraWithPaging lh
@@ -385,6 +388,8 @@ evalGalley e =
. runNotificationSubsystemGundeck (notificationSubsystemConfig e)
. interpretSparAPIAccessToRpc (e ^. options . spar)
. interpretTeamSubsystem teamSubsystemConfig
+ . runFeaturesConfigSubsystem
+ . runInputSem getAllTeamFeaturesForServer
. interpretConversationSubsystem
. interpretTeamCollaboratorsSubsystem
where
diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs
index c74b96b03f..26e4cf32a7 100644
--- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs
+++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs
@@ -33,7 +33,6 @@ import Galley.API.Error
import Galley.API.Teams.Features.Get
import Galley.Cassandra.Store
import Galley.Cassandra.Util
-import Galley.Effects.TeamFeatureStore qualified as TFS
import Imports
import Polysemy
import Polysemy.Error
@@ -42,6 +41,7 @@ import Polysemy.TinyLog
import Wire.API.Team.Feature
import Wire.API.Team.Feature.TH
import Wire.ConversationStore.Cassandra.Instances ()
+import Wire.TeamFeatureStore qualified as TFS
interpretTeamFeatureStoreToCassandra ::
( Member (Embed IO) r,
diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs
index e3aef966b7..af09983856 100644
--- a/services/galley/src/Galley/Effects.hs
+++ b/services/galley/src/Galley/Effects.hs
@@ -68,7 +68,6 @@ import Galley.Effects.ClientStore
import Galley.Effects.CustomBackendStore
import Galley.Effects.Queue
import Galley.Effects.SearchVisibilityStore
-import Galley.Effects.TeamFeatureStore
import Galley.Effects.TeamMemberStore
import Galley.Effects.TeamNotificationStore
import Galley.Env
@@ -88,6 +87,8 @@ import Wire.CodeStore
import Wire.ConversationStore (ConversationStore, MLSCommitLockStore)
import Wire.ConversationSubsystem
import Wire.ExternalAccess
+import Wire.FeaturesConfigSubsystem (FeaturesConfigSubsystem)
+import Wire.FeaturesConfigSubsystem.Types (ExposeInvitationURLsAllowlist)
import Wire.FederationAPIAccess
import Wire.FireAndForget
import Wire.GundeckAPIAccess
@@ -106,6 +107,7 @@ import Wire.ServiceStore
import Wire.SparAPIAccess
import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore)
import Wire.TeamCollaboratorsSubsystem (TeamCollaboratorsSubsystem)
+import Wire.TeamFeatureStore
import Wire.TeamJournal (TeamJournal)
import Wire.TeamStore
import Wire.TeamSubsystem (TeamSubsystem)
@@ -115,6 +117,8 @@ import Wire.UserGroupStore
type GalleyEffects1 =
'[ TeamCollaboratorsSubsystem,
ConversationSubsystem,
+ Input AllTeamFeatures,
+ FeaturesConfigSubsystem,
TeamSubsystem,
SparAPIAccess,
NotificationSubsystem,
@@ -148,8 +152,9 @@ type GalleyEffects1 =
TeamMemberStore CassandraPaging,
ListItems LegacyPaging TeamId,
ListItems InternalPaging TeamId,
+ Input ExposeInvitationURLsAllowlist,
+ Input FeatureFlags,
Input FanoutLimit,
- Input AllTeamFeatures,
Input (FeatureDefaults LegalholdConfig),
Input (Local ()),
Input Opts,
From 4dc9b3fec8dd3b6396af91e86cfc84b3addc8de7 Mon Sep 17 00:00:00 2001
From: Matthias Fischmann
Date: Thu, 22 Jan 2026 12:42:57 +0100
Subject: [PATCH 22/26] Leave breadcrumbs to internal api access on staging in
swagger TOC page. (#4973)
---
services/brig/src/Brig/API/Public.hs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs
index 5a206da833..1bc41a7d6a 100644
--- a/services/brig/src/Brig/API/Public.hs
+++ b/services/brig/src/Brig/API/Public.hs
@@ -317,7 +317,8 @@ versionedSwaggerDocsAPI Nothing = tocPage
[ "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.
"
+ \method and URL path.
",
+ "BACKDOORS FOR TESTING (staging only): For testing some of the internal end-points can be used on our staging env through basic auth. If you want to know which ones support this, here is one. You can search this file your path and check if that says `basic_auth: true`.
"
]
<> mconcat
[ [ s <> ":
",
From a9c1db50f4bbdd127c3ccd34025b58844ac7a233 Mon Sep 17 00:00:00 2001
From: Leif Battermann
Date: Thu, 22 Jan 2026 12:51:13 +0100
Subject: [PATCH 23/26] WPB-22811 migrate conversation codes table to postgres
(#4961)
---
changelog.d/0-release-notes/WPB-22811 | 5 +
changelog.d/5-internal/WPB-22811 | 2 +-
.../templates/configmap.yaml | 1 +
charts/background-worker/values.yaml | 5 +
charts/galley/values.yaml | 1 +
.../src/developer/reference/config-options.md | 61 +++---
hack/helm_vars/common.yaml.gotmpl | 8 +-
hack/helm_vars/wire-server/values.yaml.gotmpl | 2 +
integration/integration.cabal | 4 +-
.../Conversation.hs} | 32 ++-
.../test/Test/Migration/ConversationCodes.hs | 183 ++++++++++++++++++
integration/test/Test/Migration/Util.hs | 23 +++
libs/wire-api/src/Wire/API/Password.hs | 18 ++
.../wire-api/src/Wire/API/PostgresMarshall.hs | 14 ++
.../Wire/API/Roundtrip/PostgresMarshall.hs | 83 ++++++++
libs/wire-api/test/unit/Test/Wire/API/Run.hs | 4 +-
libs/wire-api/wire-api.cabal | 1 +
.../20260115150600-conversation-codes.sql | 16 ++
libs/wire-subsystems/src/Wire/CodeStore.hs | 6 +-
.../src/Wire/CodeStore/Cassandra.hs | 25 ++-
.../src/Wire/CodeStore/Cassandra/Queries.hs | 16 +-
.../src/Wire/CodeStore/Code.hs | 13 +-
.../src/Wire/CodeStore/DualWrite.hs | 57 ++++++
.../src/Wire/CodeStore/Migration.hs | 135 +++++++++++++
.../src/Wire/CodeStore/Postgres.hs | 111 +++++++++++
.../src/Wire/CodeStore/Scope.hs | 32 ---
.../src/Wire/ConversationStore.hs | 7 +-
.../src/Wire/ConversationStore/Migration.hs | 138 +++++--------
libs/wire-subsystems/src/Wire/Migration.hs | 129 ++++++++++++
libs/wire-subsystems/wire-subsystems.cabal | 5 +-
postgres-schema.sql | 41 +++-
.../background-worker/background-worker.cabal | 2 +-
.../background-worker.integration.yaml | 2 +
.../src/Wire/BackgroundWorker.hs | 15 +-
.../src/Wire/BackgroundWorker/Options.hs | 3 +-
...Conversations.hs => PostgresMigrations.hs} | 24 ++-
.../Wire/BackendNotificationPusherSpec.hs | 12 +-
.../background-worker/test/Test/Wire/Util.hs | 6 +-
services/galley/galley.integration.yaml | 2 +-
services/galley/src/Galley/API/Action.hs | 5 +-
services/galley/src/Galley/API/Update.hs | 8 +-
services/galley/src/Galley/API/Util.hs | 2 +-
services/galley/src/Galley/App.hs | 9 +-
43 files changed, 1038 insertions(+), 230 deletions(-)
create mode 100644 changelog.d/0-release-notes/WPB-22811
rename integration/test/Test/{Conversation/Migration.hs => Migration/Conversation.hs} (93%)
create mode 100644 integration/test/Test/Migration/ConversationCodes.hs
create mode 100644 integration/test/Test/Migration/Util.hs
create mode 100644 libs/wire-api/test/unit/Test/Wire/API/Roundtrip/PostgresMarshall.hs
create mode 100644 libs/wire-subsystems/postgres-migrations/20260115150600-conversation-codes.sql
create mode 100644 libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs
create mode 100644 libs/wire-subsystems/src/Wire/CodeStore/Migration.hs
create mode 100644 libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs
delete mode 100644 libs/wire-subsystems/src/Wire/CodeStore/Scope.hs
create mode 100644 libs/wire-subsystems/src/Wire/Migration.hs
rename services/background-worker/src/Wire/{MigrateConversations.hs => PostgresMigrations.hs} (69%)
diff --git a/changelog.d/0-release-notes/WPB-22811 b/changelog.d/0-release-notes/WPB-22811
new file mode 100644
index 0000000000..e30e9e739b
--- /dev/null
+++ b/changelog.d/0-release-notes/WPB-22811
@@ -0,0 +1,5 @@
+Conversation codes can now be migrated to PostgreSQL. For existing installations:
+- Set `postgresMigration.conversationCodes: migration-to-postgresql` in both `galley` and `background-worker`.
+- Run the backfill with `migrateConversationCodes: true`.
+- Wait for `wire_conv_codes_migration_finished` to reach `1.0`.
+- Switch to `postgresMigration.conversationCodes: postgresql` and disable `migrateConversationCodes`.
diff --git a/changelog.d/5-internal/WPB-22811 b/changelog.d/5-internal/WPB-22811
index 6d1749a04b..2d98a74327 100644
--- a/changelog.d/5-internal/WPB-22811
+++ b/changelog.d/5-internal/WPB-22811
@@ -1 +1 @@
-Moved CodeStore from galley to subsystems
+Migration of conversation codes from cassandra to postgres (#4959, #4961)
diff --git a/charts/background-worker/templates/configmap.yaml b/charts/background-worker/templates/configmap.yaml
index 81e78f5ca7..6c84d80876 100644
--- a/charts/background-worker/templates/configmap.yaml
+++ b/charts/background-worker/templates/configmap.yaml
@@ -90,6 +90,7 @@ data:
{{- end }}
migrateConversations: {{ .migrateConversations }}
+ migrateConversationCodes: {{ .migrateConversationCodes }}
migrateConversationsOptions:
{{toYaml .migrateConversationsOptions | indent 6 }}
diff --git a/charts/background-worker/values.yaml b/charts/background-worker/values.yaml
index 5b729f1472..2896d749e8 100644
--- a/charts/background-worker/values.yaml
+++ b/charts/background-worker/values.yaml
@@ -69,6 +69,10 @@ config:
migrateConversationsOptions:
pageSize: 10000
parallelism: 2
+ # This will start the migration of conversation codes.
+ # It's important to set `settings.postgresMigration.conversationCodes` to `migration-to-postgresql`
+ # before starting the migration.
+ migrateConversationCodes: false
backendNotificationPusher:
pushBackoffMinWait: 10000 # in microseconds, so 10ms
@@ -87,6 +91,7 @@ config:
# Controls where conversation data is stored/accessed
postgresMigration:
conversation: cassandra
+ conversationCodes: cassandra
secrets:
{}
diff --git a/charts/galley/values.yaml b/charts/galley/values.yaml
index a8ac5e2b3b..f4ac3331c5 100644
--- a/charts/galley/values.yaml
+++ b/charts/galley/values.yaml
@@ -71,6 +71,7 @@ config:
postgresMigration:
conversation: cassandra
+ conversationCodes: cassandra
settings:
httpPoolSize: 128
maxTeamSize: 10000
diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md
index d72b8158fa..92f52cbf46 100644
--- a/docs/src/developer/reference/config-options.md
+++ b/docs/src/developer/reference/config-options.md
@@ -1811,79 +1811,94 @@ galley:
config:
postgresMigration:
conversation: postgresql
+ conversationCodes: postgresql
background-worker:
config:
postgresMigration:
conversation: postgresql
+ conversationCodes: postgresql
migrateConversations: false
```
#### Migration for existing installations
-Existing installations should migrate the conversation data to PostgreSQL from
+Existing installations should migrate conversation data to PostgreSQL from
Cassandra. This is necessary for channel search and management of channels from
the team-management UI. It is highly recommended to take a backup of the Galley
Cassandra before triggering the migration.
-The migration needs to happen in 3 steps:
+Migrations are independent and can be run separately, in batches, or all at
+once. This is expected, because migrations will be released over time. The
+pattern below applies per store. Use it for `conversation` and
+`conversationCodes` now, and for future stores as they are added.
-1. Prepare wire-server for migration.
+**Migration pattern per store(s)**
- This step make sure that wire-server keep working as expected during the
- migration. To do this deploy wire-server with this config change:
-
- Configure both `galley` and `background-worker` so that newly created
- conversations are written to PostgreSQL while existing data still reads from
- Cassandra:
+1. Prepare the selected store(s) for migration by setting
+ `postgresMigration.` to `migration-to-postgresql`. This enables the
+ migration interpreter for that store, which ensures data is written to
+ PostgreSQL (store-specific details are handled internally).
+ The configuration must be consistent across `galley` and
+ `background-worker`.
```yaml
galley:
config:
postgresMigration:
conversation: migration-to-postgresql
+ conversationCodes: migration-to-postgresql
background-worker:
config:
postgresMigration:
conversation: migration-to-postgresql
+ conversationCodes: migration-to-postgresql
migrateConversations: false
+ migrateConversationCodes: false
```
- This change should restart all the galley pods, any new conversations will
- now be written to PostgreSQL.
-
-2. Trigger the migration and wait.
+ This change should restart all the galley pods, and new writes will follow
+ the migration interpreter.
- This step will actually carry out the migration. To do this deploy
- wire-server with this config change:
+2. Run the backfill for the selected store(s) via background-worker.
```yaml
background-worker:
config:
migrateConversations: true
+ migrateConversationCodes: true
```
- This change should restart the background-worker pods. It is recommended to
- watch the logs and wait for both of these two metrics to report `1.0`:
- `wire_local_convs_migration_finished` and `wire_user_remote_convs_migration_finished`.
- This can take a long time depending on number of conversations in the DB.
-
-3. Configure wire-server to only use PostgreSQL for conversations.
+ Wait for the store-specific migration metrics to reach `1.0`. For
+ conversations: `wire_local_convs_migration_finished` and
+ `wire_user_remote_convs_migration_finished`. For conversation codes:
+ `wire_conv_codes_migration_finished`.
- This will be the configuration which must be used from now on for every new
- release.
+3. Cut over reads and writes to PostgreSQL for the selected store(s). This
+ configuration must be used from now on for every new release.
```yaml
galley:
config:
postgresMigration:
conversation: postgresql
+ conversationCodes: postgresql
background-worker:
config:
postgresMigration:
conversation: postgresql
+ conversationCodes: postgresql
migrateConversations: false
+ migrateConversationCodes: false
```
+**How to run migrations independently or in batches**
+
+- To migrate a single store, set only that store’s `postgresMigration.`
+ and `migrate` flags; leave others unchanged.
+- To migrate a batch, set multiple stores to `migration-to-postgresql` and
+ enable only the matching `migrate` flags together.
+- To reduce load, run large stores alone and group small stores together.
+
## Configure Cells
If Cells integration is enabled, gundeck must be configured with the name of
diff --git a/hack/helm_vars/common.yaml.gotmpl b/hack/helm_vars/common.yaml.gotmpl
index bce592348c..e1974ced47 100644
--- a/hack/helm_vars/common.yaml.gotmpl
+++ b/hack/helm_vars/common.yaml.gotmpl
@@ -13,11 +13,9 @@ dynBackendDomain1: dynamic-backend-1.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster
dynBackendDomain2: dynamic-backend-2.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster.local
dynBackendDomain3: dynamic-backend-3.{{ requiredEnv "NAMESPACE_1" }}.svc.cluster.local
-{{- if (eq (env "PREFERRED_STORE") "") }}
-conversationStore: cassandra
-{{- else }}
-conversationStore: {{ env "PREFERRED_STORE" }}
-{{- end }}
+{{- $preferredStore := default "cassandra" (env "PREFERRED_STORE") }}
+conversationStore: {{ $preferredStore }}
+conversationCodesStore: {{ $preferredStore }}
{{- if (eq (env "UPLOAD_XML_S3_BASE_URL") "") }}
uploadXml: {}
diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl
index ec4ea1670a..5efdf49dbd 100644
--- a/hack/helm_vars/wire-server/values.yaml.gotmpl
+++ b/hack/helm_vars/wire-server/values.yaml.gotmpl
@@ -306,6 +306,7 @@ galley:
enableFederation: true # keep in sync with brig.config.enableFederation, cargohold.config.enableFederation and tags.federator!
postgresMigration:
conversation: {{ .Values.conversationStore }}
+ conversationCodes: {{ .Values.conversationCodesStore }}
settings:
maxConvAndTeamSize: 16
maxTeamSize: 32
@@ -673,6 +674,7 @@ background-worker:
federationDomain: integration.example.com
postgresMigration:
conversation: {{ .Values.conversationStore }}
+ conversationCodes: {{ .Values.conversationCodesStore }}
rabbitmq:
port: 5671
adminPort: 15671
diff --git a/integration/integration.cabal b/integration/integration.cabal
index 72c0da80ef..fae812e6f5 100644
--- a/integration/integration.cabal
+++ b/integration/integration.cabal
@@ -126,7 +126,6 @@ library
Test.Client
Test.Connection
Test.Conversation
- Test.Conversation.Migration
Test.Demo
Test.DNSMock
Test.DomainVerification
@@ -174,6 +173,9 @@ library
Test.LegalHold
Test.Login
Test.MessageTimer
+ Test.Migration.Conversation
+ Test.Migration.ConversationCodes
+ Test.Migration.Util
Test.MLS
Test.MLS.Clients
Test.MLS.History
diff --git a/integration/test/Test/Conversation/Migration.hs b/integration/test/Test/Migration/Conversation.hs
similarity index 93%
rename from integration/test/Test/Conversation/Migration.hs
rename to integration/test/Test/Migration/Conversation.hs
index 97472960d4..dfb80e91a7 100644
--- a/integration/test/Test/Conversation/Migration.hs
+++ b/integration/test/Test/Migration/Conversation.hs
@@ -10,26 +10,22 @@
-- The tests are from the perspective of mel, a user on the dynamic backend,
-- called backendM (migrating backend). There are also users called mark and mia
-- on this backend.
-module Test.Conversation.Migration where
+module Test.Migration.Conversation where
import API.Galley
import Control.Applicative
-import Control.Concurrent (threadDelay)
import Control.Monad.Codensity
import Control.Monad.Reader
import Data.IntMap (IntMap)
import qualified Data.IntMap as IntMap
import qualified Data.IntSet as IntSet
-import Data.Text (Text)
-import qualified Data.Text as Text
-import qualified Data.Text.Encoding as Text
import GHC.Stack
import MLS.Util
import Notifications
import SetupHelpers hiding (deleteUser)
+import Test.Migration.Util
import Testlib.Prelude
import Testlib.ResourcePool
-import Text.Regex.TDFA ((=~))
import UnliftIO
-- | Our test setup cannot process updates to many MLS convs concurrently, so we
@@ -84,7 +80,9 @@ testMigrationToPostgresMLS = do
actualConvs `shouldMatchSet` ((convIdToQidObject <$> expectedConvs) <> otherMelConvs)
- when (phase == 3) $ waitForMigration domainM
+ when (phase == 3) $ do
+ waitForMigration domainM convMigrationFinishedCounterName
+ waitForMigration domainM userMigrationFinishedCounterName
runPhase 1
runPhase 2
runPhase 3
@@ -191,7 +189,9 @@ testMigrationToPostgresProteus = do
actualConvs `shouldMatchSet` ((convIdToQidObject <$> expectedConvs) <> otherMelConvs)
- when (phase == 3) $ waitForMigration domainM
+ when (phase == 3) $ do
+ waitForMigration domainM convMigrationFinishedCounterName
+ waitForMigration domainM userMigrationFinishedCounterName
runPhase 1
runPhase 2
runPhase 3
@@ -292,17 +292,11 @@ instance Semigroup TestConvList where
addMelConvs = IntMap.unionWith (<>) l1.addMelConvs l2.addMelConvs
}
-waitForMigration :: (HasCallStack) => String -> App ()
-waitForMigration domainM = do
- metrics <-
- getMetrics domainM BackgroundWorker `bindResponse` \resp -> do
- resp.status `shouldMatchInt` 200
- pure $ Text.decodeUtf8 resp.body
- let (_, _, _, convFinishedMatches) :: (Text, Text, Text, [Text]) = (metrics =~ Text.pack "^wire_local_convs_migration_finished\\ ([0-9]+\\.[0-9]+)$")
- let (_, _, _, userFinishedMatches) :: (Text, Text, Text, [Text]) = (metrics =~ Text.pack "^wire_user_remote_convs_migration_finished\\ ([0-9]+\\.[0-9]+)$")
- when (convFinishedMatches /= [Text.pack "1.0"] || userFinishedMatches /= [Text.pack "1.0"]) $ do
- liftIO $ threadDelay 100_000
- waitForMigration domainM
+convMigrationFinishedCounterName :: String
+convMigrationFinishedCounterName = "^wire_local_convs_migration_finished"
+
+userMigrationFinishedCounterName :: String
+userMigrationFinishedCounterName = "^wire_user_remote_convs_migration_finished"
phase1Overrides, phase2Overrides, phase3Overrides, phase4Overrides, phase5Overrides :: ServiceOverrides
phase1Overrides =
diff --git a/integration/test/Test/Migration/ConversationCodes.hs b/integration/test/Test/Migration/ConversationCodes.hs
new file mode 100644
index 0000000000..c56fd73c9a
--- /dev/null
+++ b/integration/test/Test/Migration/ConversationCodes.hs
@@ -0,0 +1,183 @@
+module Test.Migration.ConversationCodes where
+
+import API.Galley
+import Control.Applicative
+import Control.Concurrent.Timeout
+import Control.Monad.Codensity
+import Control.Monad.Reader
+import SetupHelpers
+import Test.Migration.Util (waitForMigration)
+import Testlib.Prelude
+import Testlib.ResourcePool
+
+testConversationCodesMigration :: (HasCallStack) => TaggedBool "has-password" -> App ()
+testConversationCodesMigration (TaggedBool hasPassword) = do
+ resourcePool <- asks (.resourcePool)
+ let pw = if hasPassword then Just "funky password" else Nothing
+
+ runCodensity (acquireResources 1 resourcePool) $ \[backend] -> do
+ let domain = backend.berDomain
+
+ (admin, code1, codeA, convs, members) <- runCodensity (startDynamicBackend backend (conf "cassandra" False)) $ \_ -> do
+ (admin, _, members) <- createTeam domain 6
+ convs1@(conv1 : _) <- replicateM 5 $ postConversation admin (allowGuests defProteus) >>= getJSON 201
+ convs2@(convA : _) <- replicateM 4 $ postConversation admin (allowGuests defProteus) >>= getJSON 201
+ code1 <- genCode admin conv1 pw
+ codeA <- genCode admin convA pw
+ pure (admin, code1, codeA, convs1 <> convs2, members)
+
+ [conv1, conv2, conv3, conv4, conv5, convA, convB, convC, convD] <- pure convs
+ m1 : m2 : m3 : m4 : _ <- pure members
+
+ (code2, codeB) <- runCodensity (startDynamicBackend backend (conf "migration-to-postgresql" False)) $ \_ -> do
+ -- code generation works
+ code2 <- genCode admin conv2 pw
+ codeB <- genCode admin convB pw
+ -- joining works
+ checkJoinAndGet admin m1 conv1 code1 pw
+ checkJoinAndGet admin m1 conv2 code2 pw
+ -- deletion works
+ checkDelete admin m1 convA codeA pw
+ pure (code2, codeB)
+
+ (code3, codeC) <- runCodensity (startDynamicBackend backend (conf "migration-to-postgresql" True)) $ \_ -> do
+ -- code generation works
+ code3 <- genCode admin conv3 pw
+ codeC <- genCode admin convC pw
+ -- joining works
+ checkJoinAndGet admin m2 conv1 code1 pw
+ checkJoinAndGet admin m2 conv2 code2 pw
+ checkJoinAndGet admin m2 conv3 code3 pw
+ -- deletion works
+ checkNoCode admin m1 convA codeA pw
+ checkDelete admin m1 convB codeB pw
+ waitForMigration domain counterName
+ pure (code3, codeC)
+
+ (code4, codeD) <- runCodensity (startDynamicBackend backend (conf "migration-to-postgresql" False)) $ \_ -> do
+ -- code generation works
+ code4 <- genCode admin conv4 pw
+ codeD <- genCode admin convD pw
+ -- joining works
+ checkJoinAndGet admin m3 conv1 code1 pw
+ checkJoinAndGet admin m3 conv2 code2 pw
+ checkJoinAndGet admin m3 conv3 code3 pw
+ checkJoinAndGet admin m3 conv4 code4 pw
+ -- deletion works
+ checkNoCode admin m1 convA codeA pw
+ checkNoCode admin m1 convB codeB pw
+ checkDelete admin m1 convC codeC pw
+ pure (code4, codeD)
+
+ runCodensity (startDynamicBackend backend (conf "postgresql" False)) $ \_ -> do
+ -- code generation works
+ code5 <- genCode admin conv5 pw
+ -- joining works
+ checkJoinAndGet admin m4 conv1 code1 pw
+ checkJoinAndGet admin m4 conv2 code2 pw
+ checkJoinAndGet admin m4 conv3 code3 pw
+ checkJoinAndGet admin m4 conv4 code4 pw
+ checkJoinAndGet admin m4 conv5 code5 pw
+ -- deletion works
+ checkNoCode admin m1 convA codeA pw
+ checkNoCode admin m1 convB codeB pw
+ checkNoCode admin m1 convC codeC pw
+ checkDelete admin m1 convD codeD pw
+ checkDelete admin m1 conv5 code5 pw
+ where
+ checkJoinAndGet admin user conv code pw = do
+ joinWithCode user conv code
+ getCode admin conv pw `shouldMatch` code
+ checkDelete admin user conv (k, v) pw = do
+ assertSuccess =<< deleteConversationCode admin conv
+ checkNoCode admin user conv (k, v) pw
+ checkNoCode admin user conv (k, v) pw = do
+ assertStatus 404 =<< getConversationCode admin conv pw
+ bindResponse (getJoinCodeConv user k v) $ \res -> do
+ res.status `shouldMatchInt` 404
+ res.json %. "label" `shouldMatch` "no-conversation-code"
+
+testConversationCodesMigrationExpiration :: (HasCallStack) => App ()
+testConversationCodesMigrationExpiration = do
+ resourcePool <- asks (.resourcePool)
+ let pw = Nothing
+
+ runCodensity (acquireResources 1 resourcePool) $ \[backend] -> do
+ let domain = backend.berDomain
+
+ (admin, code1, conv, mem) <- runCodensity (startDynamicBackend backend (confWithExpiry "cassandra" False 2)) $ \_ -> do
+ (admin, _, mem : _) <- createTeam domain 2
+ conv <- postConversation admin (allowGuests defProteus) >>= getJSON 201
+ code1 <- genCode admin conv pw
+ pure (admin, code1, conv, mem)
+
+ code2 <- runCodensity (startDynamicBackend backend (confWithExpiry "migration-to-postgresql" False 2)) $ \_ -> do
+ waitForCodeToExpire admin conv pw
+ checkCantJoin mem code1
+ genCode admin conv pw
+
+ code3 <- runCodensity (startDynamicBackend backend (confWithExpiry "migration-to-postgresql" True 2)) $ \_ -> do
+ waitForCodeToExpire admin conv pw
+ checkCantJoin mem code2
+ genCode admin conv pw
+
+ code4 <- runCodensity (startDynamicBackend backend (confWithExpiry "migration-to-postgresql" False 2)) $ \_ -> do
+ waitForCodeToExpire admin conv pw
+ checkCantJoin mem code3
+ genCode admin conv pw
+ runCodensity (startDynamicBackend backend (confWithExpiry "postgresql" False 2)) $ \_ -> do
+ waitForCodeToExpire admin conv pw
+ checkCantJoin mem code4
+ where
+ checkCantJoin user (k, v) = do
+ bindResponse (getJoinCodeConv user k v) $ \res -> do
+ res.status `shouldMatchInt` 404
+ res.json %. "label" `shouldMatch` "no-conversation-code"
+
+-- HELPER
+
+genCode :: (HasCallStack, MakesValue user, MakesValue conv) => user -> conv -> Maybe String -> App (String, String)
+genCode user conv pw =
+ bindResponse (postConversationCode user conv pw Nothing) $ \res -> do
+ payload <- getJSON 201 res
+ k <- payload %. "data.key" & asString
+ v <- payload %. "data.code" & asString
+ pure (k, v)
+
+getCode :: (HasCallStack, MakesValue user, MakesValue conv) => user -> conv -> Maybe String -> App (String, String)
+getCode user conv pw =
+ bindResponse (getConversationCode user conv pw) $ \res -> do
+ payload <- getJSON 200 res
+ k <- payload %. "key" & asString
+ v <- payload %. "code" & asString
+ pure (k, v)
+
+waitForCodeToExpire :: (MakesValue user, MakesValue conv) => user -> conv -> Maybe String -> App ()
+waitForCodeToExpire user conv pw = do
+ res <- getConversationCode user conv pw
+ if res.status == 404
+ then pure ()
+ else do
+ liftIO $ threadDelay 100_000
+ waitForCodeToExpire user conv pw
+
+joinWithCode :: (HasCallStack, MakesValue user) => user -> Value -> (String, String) -> App ()
+joinWithCode user conv (k, v) =
+ bindResponse (getJoinCodeConv user k v) $ \res -> do
+ res.status `shouldMatchInt` 200
+ res.json %. "id" `shouldMatch` (objQidObject conv & objId)
+
+conf :: String -> Bool -> ServiceOverrides
+conf db runMigration = confWithExpiry db runMigration 604800
+
+confWithExpiry :: String -> Bool -> Int -> ServiceOverrides
+confWithExpiry db runMigration expiry =
+ def
+ { galleyCfg =
+ setField "postgresMigration.conversationCodes" db
+ >=> setField "settings.guestLinkTTLSeconds" expiry,
+ backgroundWorkerCfg = setField "migrateConversationCodes" runMigration
+ }
+
+counterName :: String
+counterName = "^wire_conv_codes_migration_finished"
diff --git a/integration/test/Test/Migration/Util.hs b/integration/test/Test/Migration/Util.hs
new file mode 100644
index 0000000000..f55db0c58f
--- /dev/null
+++ b/integration/test/Test/Migration/Util.hs
@@ -0,0 +1,23 @@
+module Test.Migration.Util where
+
+import Control.Applicative
+import Control.Concurrent (threadDelay)
+import Control.Monad.Reader
+import Data.Text (Text)
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import GHC.Stack
+import SetupHelpers hiding (deleteUser)
+import Testlib.Prelude
+import Text.Regex.TDFA ((=~))
+
+waitForMigration :: (HasCallStack) => String -> String -> App ()
+waitForMigration domain name = do
+ metrics <-
+ getMetrics domain BackgroundWorker `bindResponse` \resp -> do
+ resp.status `shouldMatchInt` 200
+ pure $ Text.decodeUtf8 resp.body
+ let (_, _, _, finishedMatches) :: (Text, Text, Text, [Text]) = (metrics =~ Text.pack (name <> "\\ ([0-9]+\\.[0-9]+)$"))
+ when (finishedMatches /= [Text.pack "1.0"]) $ do
+ liftIO $ threadDelay 100_000
+ waitForMigration domain name
diff --git a/libs/wire-api/src/Wire/API/Password.hs b/libs/wire-api/src/Wire/API/Password.hs
index dc5cb9b9df..dfb16d1d25 100644
--- a/libs/wire-api/src/Wire/API/Password.hs
+++ b/libs/wire-api/src/Wire/API/Password.hs
@@ -36,11 +36,13 @@ 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
import OpenSSL.Random (randBytes)
import Wire.API.Password.Argon2id
import Wire.API.Password.Scrypt
+import Wire.API.PostgresMarshall
-- | A derived, stretched password that can be safely stored.
data Password
@@ -50,6 +52,22 @@ data Password
instance Show Password where
show _ = ""
+-------------------------------------------------------------------------------
+-- PSQL
+
+instance PostgresMarshall ByteString Password where
+ postgresMarshall =
+ Text.encodeUtf8 . \case
+ Argon2Password p -> encodeArgon2HashedPassword p
+ ScryptPassword p -> encodeScryptPassword p
+
+instance PostgresUnmarshall ByteString Password where
+ postgresUnmarshall =
+ mapLeft Text.pack . parsePassword . Text.decodeUtf8
+
+-------------------------------------------------------------------------------
+-- CQL
+
instance Cql Password where
ctype = Tagged BlobColumn
diff --git a/libs/wire-api/src/Wire/API/PostgresMarshall.hs b/libs/wire-api/src/Wire/API/PostgresMarshall.hs
index ee78384312..666b5b78c4 100644
--- a/libs/wire-api/src/Wire/API/PostgresMarshall.hs
+++ b/libs/wire-api/src/Wire/API/PostgresMarshall.hs
@@ -27,7 +27,9 @@ where
import Data.Aeson
import Data.Bifunctor (first)
import Data.ByteString qualified as BS
+import Data.ByteString.Conversion (toByteString')
import Data.ByteString.Conversion qualified as BSC
+import Data.Code qualified as Code
import Data.Domain
import Data.Id
import Data.Misc
@@ -530,6 +532,12 @@ instance (PostgresMarshall b a) => PostgresMarshall (Vector b) (Set a) where
instance (PostgresMarshall a b) => PostgresMarshall (Vector a) (Vector b) where
postgresMarshall = V.map postgresMarshall
+instance PostgresMarshall Text Code.Key where
+ postgresMarshall = Text.decodeUtf8 . toByteString'
+
+instance PostgresMarshall Text Code.Value where
+ postgresMarshall = Text.decodeUtf8 . toByteString'
+
---
class PostgresUnmarshall db domain where
@@ -855,6 +863,12 @@ instance (PostgresUnmarshall a b, Ord b) => PostgresUnmarshall (Vector a) (Set b
instance PostgresUnmarshall Int64 Milliseconds where
postgresUnmarshall = Right . int64ToMs
+instance PostgresUnmarshall Text Code.Key where
+ postgresUnmarshall = mapLeft Text.pack . BSC.runParser BSC.parser . Text.encodeUtf8
+
+instance PostgresUnmarshall Text Code.Value where
+ postgresUnmarshall = mapLeft Text.pack . BSC.runParser BSC.parser . Text.encodeUtf8
+
---
lmapPG :: (PostgresMarshall db domain, Profunctor p) => p db x -> p domain x
diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/PostgresMarshall.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/PostgresMarshall.hs
new file mode 100644
index 0000000000..31ce898c43
--- /dev/null
+++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/PostgresMarshall.hs
@@ -0,0 +1,83 @@
+-- This file is part of the Wire Server implementation.
+--
+-- Copyright (C) 2026 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 .
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module Test.Wire.API.Roundtrip.PostgresMarshall (tests) where
+
+import Crypto.Error (CryptoFailable (..))
+import Crypto.KDF.Argon2 qualified as Argon2
+import Data.ByteString.Char8 qualified as BS8
+import Data.Code qualified as Code
+import Data.Misc (PlainTextPassword8, fromPlainTextPassword)
+import Data.Text.Encoding (encodeUtf8)
+import Imports
+import Test.Tasty qualified as T
+import Test.Tasty.QuickCheck
+import Type.Reflection (typeRep)
+import Wire.API.Password as Password
+import Wire.API.Password.Argon2id (Argon2HashedPassword (..), encodeArgon2HashedPassword)
+import Wire.API.Password.Scrypt (encodeScryptPassword)
+import Wire.API.PostgresMarshall
+import Wire.Arbitrary qualified as Arbitrary ()
+
+tests :: T.TestTree
+tests =
+ T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "PostgresMarshall roundtrip tests" $
+ [ testRoundTrip @Text @Code.Key,
+ testRoundTrip @Text @Code.Value,
+ testRoundTrip @ByteString @Password.Password
+ ]
+
+testRoundTrip ::
+ forall db domain.
+ (Arbitrary domain, Typeable domain, PostgresMarshall db domain, PostgresUnmarshall db domain, Eq domain, Show domain) =>
+ T.TestTree
+testRoundTrip = testProperty msg trip
+ where
+ msg = show (typeRep @domain)
+ trip (value :: domain) =
+ counterexample (show value) $
+ Right value === (postgresUnmarshall . postgresMarshall @db) value
+
+instance Arbitrary Password where
+ arbitrary = Argon2Password . hashPlaintext <$> (arbitrary :: Gen PlainTextPassword8)
+ where
+ hashPlaintext plain =
+ let opts =
+ Argon2.Options
+ { variant = Argon2.Argon2id,
+ version = Argon2.Version13,
+ iterations = 1,
+ parallelism = 1,
+ memory = 8
+ }
+ salt = BS8.pack "static-salt-1234"
+ password = encodeUtf8 (fromPlainTextPassword plain)
+ hashedKey = hashWithOptions opts password salt
+ in Argon2HashedPassword {opts, salt, hashedKey}
+ hashWithOptions opts password salt =
+ let tagSize = 16
+ in case Argon2.hash opts password salt tagSize of
+ CryptoFailed err -> error $ "argon2 hash failed: " <> show err
+ CryptoPassed hash -> hash
+
+instance Eq Password where
+ p1 == p2 = passwordText p1 == passwordText p2
+ where
+ passwordText = \case
+ Argon2Password p -> encodeArgon2HashedPassword p
+ ScryptPassword p -> encodeScryptPassword p
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 0a083cd4fe..cf0f89456c 100644
--- a/libs/wire-api/test/unit/Test/Wire/API/Run.hs
+++ b/libs/wire-api/test/unit/Test/Wire/API/Run.hs
@@ -31,6 +31,7 @@ import Test.Wire.API.Roundtrip.ByteString qualified as Roundtrip.ByteString
import Test.Wire.API.Roundtrip.CSV qualified as Roundtrip.CSV
import Test.Wire.API.Roundtrip.HttpApiData qualified as Roundtrip.HttpApiData
import Test.Wire.API.Roundtrip.MLS qualified as Roundtrip.MLS
+import Test.Wire.API.Roundtrip.PostgresMarshall as PostgresMarshall
import Test.Wire.API.Routes qualified as Routes
import Test.Wire.API.Routes.Version qualified as Routes.Version
import Test.Wire.API.Routes.Version.Wai qualified as Routes.Version.Wai
@@ -67,5 +68,6 @@ main =
Routes.Version.tests,
unsafePerformIO Routes.Version.Wai.tests,
RawJson.tests,
- OAuth.tests
+ OAuth.tests,
+ PostgresMarshall.tests
]
diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal
index eb5c79e655..cc4ec01bb5 100644
--- a/libs/wire-api/wire-api.cabal
+++ b/libs/wire-api/wire-api.cabal
@@ -710,6 +710,7 @@ test-suite wire-api-tests
Test.Wire.API.Roundtrip.CSV
Test.Wire.API.Roundtrip.HttpApiData
Test.Wire.API.Roundtrip.MLS
+ Test.Wire.API.Roundtrip.PostgresMarshall
Test.Wire.API.Routes
Test.Wire.API.Routes.Version
Test.Wire.API.Routes.Version.Wai
diff --git a/libs/wire-subsystems/postgres-migrations/20260115150600-conversation-codes.sql b/libs/wire-subsystems/postgres-migrations/20260115150600-conversation-codes.sql
new file mode 100644
index 0000000000..533b2e24a9
--- /dev/null
+++ b/libs/wire-subsystems/postgres-migrations/20260115150600-conversation-codes.sql
@@ -0,0 +1,16 @@
+CREATE TABLE conversation_codes (
+ key text NOT NULL,
+ conversation uuid NOT NULL,
+ password bytea,
+ value text NOT NULL,
+ expires_at timestamptz NOT NULL,
+ PRIMARY KEY (key)
+);
+
+-- index for lookups like `WHERE key = ? AND scope = ? AND expires_at > now()`
+CREATE INDEX conversation_codes_key_expires_at_idx
+ ON conversation_codes (key, expires_at);
+
+-- index for deletes like `DELETE ... WHERE expires_at <= now()`
+CREATE INDEX conversation_codes_expires_at_idx
+ ON conversation_codes (expires_at);
diff --git a/libs/wire-subsystems/src/Wire/CodeStore.hs b/libs/wire-subsystems/src/Wire/CodeStore.hs
index 79946cc04d..528b1b2335 100644
--- a/libs/wire-subsystems/src/Wire/CodeStore.hs
+++ b/libs/wire-subsystems/src/Wire/CodeStore.hs
@@ -29,10 +29,10 @@ import Wire.CodeStore.Code
data CodeStore m a where
CreateCode :: Code -> Maybe Password -> CodeStore m ()
- GetCode :: Key -> Scope -> CodeStore m (Maybe (Code, Maybe Password))
- DeleteCode :: Key -> Scope -> CodeStore m ()
+ GetCode :: Key -> CodeStore m (Maybe (Code, Maybe Password))
+ DeleteCode :: Key -> CodeStore m ()
MakeKey :: ConvId -> CodeStore m Key
- GenerateCode :: ConvId -> Scope -> Timeout -> CodeStore m Code
+ GenerateCode :: ConvId -> Timeout -> CodeStore m Code
GetConversationCodeURI :: Maybe Text -> CodeStore m (Maybe HttpsUrl)
makeSem ''CodeStore
diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs
index a49d7c2a57..d320f13369 100644
--- a/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs
+++ b/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs
@@ -41,16 +41,16 @@ interpretCodeStoreToCassandra ::
Sem (CodeStore ': r) a ->
Sem r a
interpretCodeStoreToCassandra = interpret $ \case
- GetCode k s -> do
- embedClientInput $ lookupCode k s
+ GetCode k -> do
+ embedClientInput $ lookupCode k
CreateCode code mPw -> do
embedClientInput $ insertCode code mPw
- DeleteCode k s -> do
- embedClientInput $ deleteCode k s
+ DeleteCode k -> do
+ embedClientInput $ deleteCode k
MakeKey cid -> do
Code.mkKey cid
- GenerateCode cid s t -> do
- Code.generate cid s t
+ GenerateCode cid t -> do
+ Code.generate cid t
GetConversationCodeURI mbHost -> do
convCodeURI <- input
case convCodeURI of
@@ -67,14 +67,13 @@ insertCode c mPw = do
let v = codeValue c
let cnv = codeConversation c
let t = round (codeTTL c)
- let s = codeScope c
- retry x5 (write Cql.insertCode (params LocalQuorum (k, v, cnv, s, mPw, t)))
+ retry x5 (write Cql.insertCode (params LocalQuorum (k, v, cnv, mPw, t)))
-- | Lookup a conversation by code.
-lookupCode :: Key -> Scope -> Client (Maybe (Code, Maybe Password))
-lookupCode k s =
- fmap (toCode k s) <$> retry x1 (query1 Cql.lookupCode (params LocalQuorum (k, s)))
+lookupCode :: Key -> Client (Maybe (Code, Maybe Password))
+lookupCode k =
+ fmap (toCode k) <$> retry x1 (query1 Cql.lookupCode (params LocalQuorum (Identity k)))
-- | Delete a code associated with the given conversation key
-deleteCode :: Key -> Scope -> Client ()
-deleteCode k s = retry x5 $ write Cql.deleteCode (params LocalQuorum (k, s))
+deleteCode :: Key -> Client ()
+deleteCode k = retry x5 $ write Cql.deleteCode (params LocalQuorum (Identity k))
diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Cassandra/Queries.hs b/libs/wire-subsystems/src/Wire/CodeStore/Cassandra/Queries.hs
index e0eed6d0e8..23b31b8e9a 100644
--- a/libs/wire-subsystems/src/Wire/CodeStore/Cassandra/Queries.hs
+++ b/libs/wire-subsystems/src/Wire/CodeStore/Cassandra/Queries.hs
@@ -22,13 +22,15 @@ import Data.Id
import Imports
import Wire.API.Conversation.Code
import Wire.API.Password (Password)
-import Wire.CodeStore.Scope
-insertCode :: PrepQuery W (Key, Value, ConvId, Scope, Maybe Password, Int32) ()
-insertCode = "INSERT INTO conversation_codes (key, value, conversation, scope, password) VALUES (?, ?, ?, ?, ?) USING TTL ?"
+insertCode :: PrepQuery W (Key, Value, ConvId, Maybe Password, Int32) ()
+insertCode = "INSERT INTO conversation_codes (key, value, conversation, scope, password) VALUES (?, ?, ?, 1, ?) USING TTL ?"
-lookupCode :: PrepQuery R (Key, Scope) (Value, Int32, ConvId, Maybe Password)
-lookupCode = "SELECT value, ttl(value), conversation, password FROM conversation_codes WHERE key = ? AND scope = ?"
+lookupCode :: PrepQuery R (Identity Key) (Value, Int32, ConvId, Maybe Password)
+lookupCode = "SELECT value, ttl(value), conversation, password FROM conversation_codes WHERE key = ? AND scope = 1"
-deleteCode :: PrepQuery W (Key, Scope) ()
-deleteCode = "DELETE FROM conversation_codes WHERE key = ? AND scope = ?"
+deleteCode :: PrepQuery W (Identity Key) ()
+deleteCode = "DELETE FROM conversation_codes WHERE key = ? AND scope = 1"
+
+selectAllCodes :: PrepQuery R () (Key, Value, Int32, ConvId, Maybe Password)
+selectAllCodes = "SELECT key, value, ttl(value), conversation, password FROM conversation_codes"
diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Code.hs b/libs/wire-subsystems/src/Wire/CodeStore/Code.hs
index f49588d1b0..c5c497e44b 100644
--- a/libs/wire-subsystems/src/Wire/CodeStore/Code.hs
+++ b/libs/wire-subsystems/src/Wire/CodeStore/Code.hs
@@ -19,7 +19,6 @@
module Wire.CodeStore.Code
( Code (..),
- Scope (..),
toCode,
generate,
mkKey,
@@ -36,26 +35,23 @@ import Imports
import OpenSSL.EVP.Digest (digestBS, getDigestByName)
import OpenSSL.Random (randBytes)
import Wire.API.Password (Password)
-import Wire.CodeStore.Scope
data Code = Code
{ codeKey :: !Key,
codeValue :: !Value,
codeTTL :: !Timeout,
codeConversation :: !ConvId,
- codeScope :: !Scope,
codeHasPassword :: !Bool
}
deriving (Eq, Show, Generic)
-toCode :: Key -> Scope -> (Value, Int32, ConvId, Maybe Password) -> (Code, Maybe Password)
-toCode k s (val, ttl, cnv, mPw) =
+toCode :: Key -> (Value, Int32, ConvId, Maybe Password) -> (Code, Maybe Password)
+toCode k (val, ttl, cnv, mPw) =
( Code
{ codeKey = k,
codeValue = val,
codeTTL = Timeout (fromIntegral ttl),
codeConversation = cnv,
- codeScope = s,
codeHasPassword = isJust mPw
},
mPw
@@ -68,8 +64,8 @@ toCode k s (val, ttl, cnv, mPw) =
-- The 'key' is a stable, truncated, base64 encoded sha256 hash of the conversation ID
-- The 'value' is a base64 encoded, 120-bit random value (changing on each generation)
-generate :: (MonadIO m) => ConvId -> Scope -> Timeout -> m Code
-generate cnv s t = do
+generate :: (MonadIO m) => ConvId -> Timeout -> m Code
+generate cnv t = do
key <- mkKey cnv
val <- liftIO $ Value . unsafeRange . Ascii.encodeBase64Url <$> randBytes 15
pure
@@ -78,7 +74,6 @@ generate cnv s t = do
codeValue = val,
codeConversation = cnv,
codeTTL = t,
- codeScope = s,
codeHasPassword = False
}
diff --git a/libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs b/libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs
new file mode 100644
index 0000000000..112b61a91e
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs
@@ -0,0 +1,57 @@
+-- This file is part of the Wire Server implementation.
+--
+-- Copyright (C) 2026 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.CodeStore.DualWrite
+ ( interpretCodeStoreToCassandraAndPostgres,
+ )
+where
+
+import Cassandra (ClientState)
+import Data.Misc
+import Imports
+import Polysemy
+import Polysemy.Input
+import Wire.CodeStore (CodeStore (..))
+import Wire.CodeStore qualified as CodeStore
+import Wire.CodeStore.Cassandra qualified as Cassandra
+import Wire.CodeStore.Postgres qualified as Postgres
+import Wire.Postgres (PGConstraints)
+
+interpretCodeStoreToCassandraAndPostgres ::
+ ( Member (Input ClientState) r,
+ Member (Input (Either HttpsUrl (Map Text HttpsUrl))) r,
+ PGConstraints r
+ ) =>
+ Sem (CodeStore ': r) a ->
+ Sem r a
+
+-- | Cassandra is the source of truth during migration; writes are mirrored to Postgres.
+interpretCodeStoreToCassandraAndPostgres = interpret $ \case
+ GetCode k -> do
+ Cassandra.interpretCodeStoreToCassandra $ CodeStore.getCode k
+ CreateCode code mPw -> do
+ Cassandra.interpretCodeStoreToCassandra $ CodeStore.createCode code mPw
+ Postgres.interpretCodeStoreToPostgres $ CodeStore.createCode code mPw
+ DeleteCode k -> do
+ Cassandra.interpretCodeStoreToCassandra $ CodeStore.deleteCode k
+ Postgres.interpretCodeStoreToPostgres $ CodeStore.deleteCode k
+ MakeKey cid -> do
+ Cassandra.interpretCodeStoreToCassandra $ CodeStore.makeKey cid
+ GenerateCode cid t -> do
+ Cassandra.interpretCodeStoreToCassandra $ CodeStore.generateCode cid t
+ GetConversationCodeURI mbHost -> do
+ Cassandra.interpretCodeStoreToCassandra $ CodeStore.getConversationCodeURI mbHost
diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs b/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs
new file mode 100644
index 0000000000..d260e9cb02
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs
@@ -0,0 +1,135 @@
+-- This file is part of the Wire Server implementation.
+--
+-- Copyright (C) 2026 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.CodeStore.Migration
+ ( MigrationOptions (..),
+ migrateCodesLoop,
+ )
+where
+
+import Cassandra hiding (Value)
+import Data.Code (Key, Value)
+import Data.Conduit
+import Data.Conduit.List qualified as C
+import Data.Id (ConvId)
+import Data.Misc (HttpsUrl)
+import Hasql.Pool qualified as Hasql
+import Imports
+import Polysemy
+import Polysemy.Error
+import Polysemy.Input
+import Polysemy.State
+import Polysemy.TinyLog
+import Prometheus qualified
+import System.Logger qualified as Log
+import Wire.API.Password
+import Wire.CodeStore
+import Wire.CodeStore.Cassandra.Queries qualified as Cql
+import Wire.CodeStore.Code
+import Wire.CodeStore.Postgres qualified as Postgres
+import Wire.Migration
+import Wire.Postgres
+import Wire.Sem.Logger (mapLogger)
+import Wire.Sem.Logger.TinyLog (loggerToTinyLog)
+
+type EffectStack =
+ [ State Int,
+ Input ClientState,
+ Input Hasql.Pool,
+ Input (Either HttpsUrl (Map Text HttpsUrl)),
+ TinyLog,
+ Embed IO,
+ Final IO
+ ]
+
+migrateCodesLoop ::
+ MigrationOptions ->
+ ClientState ->
+ Hasql.Pool ->
+ Log.Logger ->
+ Prometheus.Counter ->
+ Prometheus.Counter ->
+ Prometheus.Counter ->
+ IO ()
+migrateCodesLoop migOpts cassClient pgPool logger migCounter migFinished migFailed =
+ migrationLoop
+ logger
+ "conversation codes"
+ migFinished
+ migFailed
+ (interpreter cassClient pgPool logger "conversation codes")
+ (migrateAllCodes migOpts migCounter)
+
+interpreter :: ClientState -> Hasql.Pool -> Log.Logger -> ByteString -> Sem EffectStack a -> IO (Int, a)
+interpreter cassClient pgPool logger name =
+ runFinal
+ . embedToFinal
+ . loggerToTinyLog logger
+ . mapLogger (Log.field "migration" (Log.val name) .)
+ . raiseUnder
+ . runInputConst (Right mempty)
+ . runInputConst pgPool
+ . runInputConst cassClient
+ . runState 0
+
+migrateAllCodes ::
+ ( Member (Input Hasql.Pool) r,
+ Member (Input (Either HttpsUrl (Map Text HttpsUrl))) r,
+ Member (Embed IO) r,
+ Member (Input ClientState) r,
+ Member TinyLog r,
+ Member (State Int) r
+ ) =>
+ MigrationOptions ->
+ Prometheus.Counter ->
+ ConduitM () Void (Sem r) ()
+migrateAllCodes migOpts migCounter = do
+ lift $ info $ Log.msg (Log.val "migrateAllCodes")
+ withCount (paginateSem Cql.selectAllCodes (paramsP LocalQuorum () migOpts.pageSize) x5)
+ .| logRetrievedPage migOpts.pageSize id
+ .| C.mapM_ (traverse_ (handleErrors (migrateCodeRow migCounter)))
+
+handleErrors ::
+ ( Member (State Int) r,
+ Member TinyLog r
+ ) =>
+ ((Key, Value, Int32, ConvId, Maybe Password) -> Sem (Error Hasql.UsageError : r) ()) ->
+ (Key, Value, Int32, ConvId, Maybe Password) ->
+ Sem r ()
+handleErrors action row@(k, _, _, _, _) = do
+ eithErr <- runError (action row)
+ case eithErr of
+ Right _ -> pure ()
+ Left e -> do
+ warn $
+ Log.msg (Log.val "error occurred during migration")
+ . Log.field "key" (show k)
+ . Log.field "error" (show e)
+ modify (+ 1)
+
+migrateCodeRow ::
+ ( Member (Input (Either HttpsUrl (Map Text HttpsUrl))) r,
+ PGConstraints r
+ ) =>
+ Prometheus.Counter ->
+ (Key, Value, Int32, ConvId, Maybe Password) ->
+ Sem r ()
+migrateCodeRow migCounter (k, v, ttl, cnv, mPw) =
+ when (ttl > 0) $ do
+ let (code, _) = toCode k (v, ttl, cnv, mPw)
+ Postgres.interpretCodeStoreToPostgres $ createCode code mPw
+ liftIO $ Prometheus.incCounter migCounter
diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs b/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs
new file mode 100644
index 0000000000..0126216d79
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs
@@ -0,0 +1,111 @@
+-- This file is part of the Wire Server implementation.
+--
+-- Copyright (C) 2026 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.CodeStore.Postgres
+ ( interpretCodeStoreToPostgres,
+ )
+where
+
+import Data.Code
+import Data.Id
+import Data.Map qualified as Map
+import Data.Misc (HttpsUrl)
+import Hasql.Statement qualified as Hasql
+import Hasql.TH
+import Imports
+import Polysemy
+import Polysemy.Input
+import Wire.API.Password
+import Wire.API.PostgresMarshall
+import Wire.CodeStore (CodeStore (..))
+import Wire.CodeStore.Code as Code
+import Wire.Postgres
+
+interpretCodeStoreToPostgres ::
+ ( PGConstraints r,
+ Member (Input (Either HttpsUrl (Map Text HttpsUrl))) r
+ ) =>
+ Sem (CodeStore ': r) a ->
+ Sem r a
+interpretCodeStoreToPostgres = interpret $ \case
+ GetCode k -> do
+ lookupCode k
+ CreateCode code mPw -> do
+ insertCode code mPw
+ DeleteCode k -> do
+ deleteCode k
+ MakeKey cid -> do
+ Code.mkKey cid
+ GenerateCode cid t -> do
+ Code.generate cid t
+ GetConversationCodeURI mbHost -> do
+ convCodeURI <- input
+ pure $ case convCodeURI of
+ Left uri -> Just uri
+ Right map' -> mbHost >>= flip Map.lookup map'
+
+insertCode :: (PGConstraints r) => Code -> Maybe Password -> Sem r ()
+insertCode c password = do
+ runStatement (codeKey c, codeConversation c, password, codeValue c, round (codeTTL c)) insert
+ where
+ insert :: Hasql.Statement (Key, ConvId, Maybe Password, Value, Int32) ()
+ insert =
+ lmapPG
+ [resultlessStatement|INSERT INTO conversation_codes
+ (key, conversation, password, value, expires_at)
+ VALUES
+ ($1 :: text, $2 :: uuid, $3 :: bytea?, $4 :: text, now() + make_interval(secs => $5 :: int))
+ ON CONFLICT (key) DO UPDATE
+ SET conversation = ($2 :: uuid),
+ password = ($3 :: bytea?),
+ value = ($4 :: text),
+ expires_at = now() + make_interval(secs => $5 :: int)
+ |]
+
+lookupCode :: (PGConstraints r) => Key -> Sem r (Maybe (Code, Maybe Password))
+lookupCode k = do
+ mRow <- runStatement k selectCode
+ pure $ fmap (toCode k) mRow
+ where
+ selectCode :: Hasql.Statement Key (Maybe (Value, Int32, ConvId, Maybe Password))
+ selectCode =
+ dimapPG
+ -- on the extraction of the remaining seconds of the TTL
+ -- `expires_at - now()` produces an interval representing how much time is left
+ -- `EXTRACT(EPOCH FROM interval)` converts that interval to seconds (a double precision)
+ -- `FLOOR(...)` truncates fractional seconds
+ -- `GREATEST(0, ...)` clamps negatives to 0 (expired rows)
+ -- `::int4` casts to 32‑bit integer.
+ [maybeStatement|SELECT
+ value :: text,
+ GREATEST(0, FLOOR(EXTRACT(EPOCH FROM (expires_at - now()))))::int4 AS ttl_secs,
+ conversation :: uuid,
+ password :: bytea?
+ FROM conversation_codes
+ WHERE key = ($1 :: text) AND expires_at > now ()
+ |]
+
+deleteCode :: (PGConstraints r) => Key -> Sem r ()
+deleteCode k =
+ runStatement k delete
+ where
+ delete :: Hasql.Statement Key ()
+ delete =
+ lmapPG
+ [resultlessStatement|DELETE FROM conversation_codes
+ WHERE key = ($1 :: text)
+ |]
diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Scope.hs b/libs/wire-subsystems/src/Wire/CodeStore/Scope.hs
deleted file mode 100644
index b48dc6e042..0000000000
--- a/libs/wire-subsystems/src/Wire/CodeStore/Scope.hs
+++ /dev/null
@@ -1,32 +0,0 @@
--- This file is part of the Wire Server implementation.
---
--- Copyright (C) 2026 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.CodeStore.Scope where
-
-import Cassandra hiding (Value)
-import Imports
-
-data Scope = ReusableCode
- deriving (Eq, Show, Generic)
-
-instance Cql Scope where
- ctype = Tagged IntColumn
-
- toCql ReusableCode = CqlInt 1
-
- fromCql (CqlInt 1) = pure ReusableCode
- fromCql _ = Left "unknown Scope"
diff --git a/libs/wire-subsystems/src/Wire/ConversationStore.hs b/libs/wire-subsystems/src/Wire/ConversationStore.hs
index 2a529d40cd..9b7b4b97e2 100644
--- a/libs/wire-subsystems/src/Wire/ConversationStore.hs
+++ b/libs/wire-subsystems/src/Wire/ConversationStore.hs
@@ -224,13 +224,16 @@ instance FromJSON StorageLocation where
x -> fail $ "Invalid storage location: " <> Text.unpack x <> ". Valid options: cassandra, postgresql, migration-to-postgresql"
data PostgresMigrationOpts = PostgresMigrationOpts
- { conversation :: StorageLocation
+ { conversation :: StorageLocation,
+ conversationCodes :: StorageLocation
}
deriving (Show)
instance FromJSON PostgresMigrationOpts where
parseJSON = withObject "PostgresMigrationOpts" $ \o ->
- PostgresMigrationOpts <$> o .: "conversation"
+ PostgresMigrationOpts
+ <$> o .: "conversation"
+ <*> o .: "conversationCodes"
getConvOrSubGroupInfo ::
(Member ConversationStore r) =>
diff --git a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs
index cfc60b0331..a3bf288c23 100644
--- a/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs
+++ b/libs/wire-subsystems/src/Wire/ConversationStore/Migration.hs
@@ -22,9 +22,7 @@ module Wire.ConversationStore.Migration where
import Cassandra
import Cassandra.Settings hiding (pageSize)
import Control.Error (lastMay)
-import Data.Aeson (FromJSON)
import Data.Conduit
-import Data.Conduit.Internal (zipSources)
import Data.Conduit.List qualified as C
import Data.Domain
import Data.Id
@@ -37,7 +35,6 @@ import Data.Time.Calendar.OrdinalDate (fromOrdinalDate)
import Data.Tuple.Extra
import Data.Vector (Vector)
import Data.Vector qualified as Vector
-import GHC.Generics (Generically (..))
import Hasql.Pool qualified as Hasql
import Hasql.Statement qualified as Hasql
import Hasql.TH
@@ -54,7 +51,6 @@ import Polysemy.Time
import Polysemy.TinyLog
import Prometheus qualified
import System.Logger qualified as Log
-import UnliftIO.Exception qualified as UnliftIO
import Wire.API.Conversation hiding (Member)
import Wire.API.Conversation.CellsState
import Wire.API.Conversation.Protocol
@@ -72,6 +68,7 @@ import Wire.ConversationStore.MLS.Types
import Wire.ConversationStore.Migration.Cleanup
import Wire.ConversationStore.Migration.Types
import Wire.ConversationStore.MigrationLock
+import Wire.Migration
import Wire.Postgres
import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (..), unsafePooledMapConcurrentlyN_)
import Wire.Sem.Concurrency.IO (unsafelyPerformConcurrency)
@@ -79,62 +76,56 @@ import Wire.Sem.Logger (mapLogger)
import Wire.Sem.Logger.TinyLog (loggerToTinyLog)
import Wire.Sem.Paging.Cassandra
import Wire.StoredConversation
-import Wire.Util
-- * Top level logic
-type EffectStack = [State Int, Input ClientState, Input Hasql.Pool, Async, Race, TinyLog, Embed IO, Concurrency 'Unsafe, Final IO]
-
-data MigrationOptions = MigrationOptions
- { pageSize :: Int32,
- parallelism :: Int
- }
- deriving (Show, Eq, Generic)
- deriving (FromJSON) via Generically MigrationOptions
-
-migrateConvsLoop :: MigrationOptions -> ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> Prometheus.Counter -> IO ()
+type EffectStack =
+ [ State Int,
+ Input ClientState,
+ Input Hasql.Pool,
+ Async,
+ Race,
+ TinyLog,
+ Embed IO,
+ Concurrency 'Unsafe,
+ Final IO
+ ]
+
+migrateConvsLoop ::
+ MigrationOptions ->
+ ClientState ->
+ Hasql.Pool ->
+ Log.Logger ->
+ Prometheus.Counter ->
+ Prometheus.Counter ->
+ Prometheus.Counter ->
+ IO ()
migrateConvsLoop migOpts cassClient pgPool logger migCounter migFinished migFailed =
- migrationLoop cassClient pgPool logger "conversations" migFinished migFailed $ migrateAllConversations migOpts migCounter
-
-migrateUsersLoop :: MigrationOptions -> ClientState -> Hasql.Pool -> Log.Logger -> Prometheus.Counter -> Prometheus.Counter -> Prometheus.Counter -> IO ()
+ migrationLoop
+ logger
+ "conversations"
+ migFinished
+ migFailed
+ (interpreter cassClient pgPool logger "conversations")
+ (migrateAllConversations migOpts migCounter)
+
+migrateUsersLoop ::
+ MigrationOptions ->
+ ClientState ->
+ Hasql.Pool ->
+ Log.Logger ->
+ Prometheus.Counter ->
+ Prometheus.Counter ->
+ Prometheus.Counter ->
+ IO ()
migrateUsersLoop migOpts cassClient pgPool logger migCounter migFinished migFailed =
- migrationLoop cassClient pgPool logger "users" migFinished migFailed $ migrateAllUsers migOpts migCounter
-
-migrationLoop :: ClientState -> Hasql.Pool -> Log.Logger -> ByteString -> Prometheus.Counter -> Prometheus.Counter -> ConduitT () Void (Sem EffectStack) () -> IO ()
-migrationLoop cassClient pgPool logger name migFinished migFailed migration = do
- go 0 `UnliftIO.catch` handleIOError
- where
- handleIOError :: SomeException -> IO ()
- handleIOError exc = do
- Prometheus.incCounter migFailed
- Log.err logger $
- Log.msg (Log.val "migration failed, it won't restart unless the background-worker is restarted.")
- . Log.field "migration" name
- . Log.field "error" (displayException exc)
- UnliftIO.throwIO exc
-
- go :: Int -> IO ()
- go nIter = do
- runMigration >>= \case
- 0 -> do
- Log.info logger $
- Log.msg (Log.val "finished migration")
- . Log.field "attempt" nIter
- . Log.field "migration" name
- Prometheus.incCounter migFinished
- n -> do
- Log.info logger $
- Log.msg (Log.val "finished migration with errors")
- . Log.field "migration" name
- . Log.field "errors" n
- . Log.field "attempt" nIter
- go (nIter + 1)
-
- runMigration :: IO Int
- runMigration =
- fmap fst
- . interpreter cassClient pgPool logger name
- $ runConduit migration
+ migrationLoop
+ logger
+ "users"
+ migFinished
+ migFailed
+ (interpreter cassClient pgPool logger "users")
+ (migrateAllUsers migOpts migCounter)
interpreter :: ClientState -> Hasql.Pool -> Log.Logger -> ByteString -> Sem EffectStack a -> IO (Int, a)
interpreter cassClient pgPool logger name =
@@ -166,7 +157,7 @@ migrateAllConversations ::
migrateAllConversations migOpts migCounter = do
lift $ info $ Log.msg (Log.val "migrateAllConversations")
withCount (paginateSem select (paramsP LocalQuorum () migOpts.pageSize) x5)
- .| logRetrievedPage migOpts.pageSize
+ .| logRetrievedPage migOpts.pageSize runIdentity
.| C.mapM_ (unsafePooledMapConcurrentlyN_ migOpts.parallelism (handleErrors (migrateConversation migCounter) "conv"))
where
select :: PrepQuery R () (Identity ConvId)
@@ -188,24 +179,12 @@ migrateAllUsers ::
migrateAllUsers migOpts migCounter = do
lift $ info $ Log.msg (Log.val "migrateAllUsers")
withCount (paginateSem select (paramsP LocalQuorum () migOpts.pageSize) x5)
- .| logRetrievedPage migOpts.pageSize
+ .| logRetrievedPage migOpts.pageSize runIdentity
.| C.mapM_ (unsafePooledMapConcurrentlyN_ migOpts.parallelism (handleErrors (migrateUser migCounter) "user"))
where
select :: PrepQuery R () (Identity UserId)
select = "select distinct user from user_remote_conv"
-logRetrievedPage :: (Member TinyLog r) => Int32 -> ConduitM (Int32, [Identity (Id a)]) [Id a] (Sem r) ()
-logRetrievedPage pageSize =
- C.mapM
- ( \(i, rows) -> do
- let estimatedRowsSoFar = (i - 1) * pageSize + fromIntegral (length rows)
- info $ Log.msg (Log.val "retrieved page") . Log.field "estimatedRowsSoFar" estimatedRowsSoFar
- pure $ map runIdentity rows
- )
-
-withCount :: (Monad m) => ConduitM () [a] m () -> ConduitM () (Int32, [a]) m ()
-withCount = zipSources (C.sourceList [1 ..])
-
handleErrors :: (Member (State Int) r, Member TinyLog r) => (Id a -> Sem (Error MigrationLockError : Error Hasql.UsageError : r) b) -> ByteString -> Id a -> Sem r (Maybe b)
handleErrors action lockType id_ =
join <$> handleError (handleError action lockType) lockType id_
@@ -528,24 +507,3 @@ unzip9 [] = ([], [], [], [], [], [], [], [], [])
unzip9 ((y1, y2, y3, y4, y5, y6, y7, y8, y9) : ys) =
let (l1, l2, l3, l4, l5, l6, l7, l8, l9) = unzip9 ys
in (y1 : l1, y2 : l2, y3 : l3, y4 : l4, y5 : l5, y6 : l6, y7 : l7, y8 : l8, y9 : l9)
-
-paginateSem :: forall a b q r. (Tuple a, Tuple b, RunQ q, Member (Input ClientState) r, Member TinyLog r, Member (Embed IO) r) => q R a b -> QueryParams a -> RetrySettings -> ConduitT () [b] (Sem r) ()
-paginateSem q p r = do
- go =<< lift getFirstPage
- where
- go page = do
- lift $ info $ Log.msg (Log.val "Got a page")
- unless (null (result page)) $
- yield (result page)
- when (hasMore page) $
- go =<< lift (getNextPage page)
-
- getFirstPage :: Sem r (Page b)
- getFirstPage = do
- client <- input
- embedClient client $ retry r (paginate q p)
-
- getNextPage :: Page b -> Sem r (Page b)
- getNextPage page = do
- client <- input
- embedClient client $ retry r (nextPage page)
diff --git a/libs/wire-subsystems/src/Wire/Migration.hs b/libs/wire-subsystems/src/Wire/Migration.hs
new file mode 100644
index 0000000000..3a1d6503d3
--- /dev/null
+++ b/libs/wire-subsystems/src/Wire/Migration.hs
@@ -0,0 +1,129 @@
+-- This file is part of the Wire Server implementation.
+--
+-- Copyright (C) 2026 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.Migration where
+
+import Cassandra
+import Cassandra.Settings
+import Data.Aeson
+import Data.Conduit
+import Data.Conduit.Internal (zipSources)
+import Data.Conduit.List qualified as C
+import GHC.Generics (Generically (..))
+import Imports
+import Polysemy
+import Polysemy.Input
+import Polysemy.TinyLog
+import Prometheus qualified
+import System.Logger qualified as Log
+import UnliftIO qualified
+import Wire.Util (embedClient)
+
+data MigrationOptions = MigrationOptions
+ { pageSize :: Int32,
+ parallelism :: Int
+ }
+ deriving (Show, Eq, Generic)
+ deriving (FromJSON) via Generically MigrationOptions
+
+migrationLoop ::
+ Log.Logger ->
+ ByteString ->
+ Prometheus.Counter ->
+ Prometheus.Counter ->
+ (Sem r () -> IO (Int, a)) ->
+ ConduitT () Void (Sem r) () ->
+ IO ()
+migrationLoop logger name migFinished migFailed interpreter migration = do
+ go 0 `UnliftIO.catch` handleIOError
+ where
+ handleIOError :: SomeException -> IO ()
+ handleIOError exc = do
+ Prometheus.incCounter migFailed
+ Log.err logger $
+ Log.msg (Log.val "migration failed, it won't restart unless the background-worker is restarted.")
+ . Log.field "migration" name
+ . Log.field "error" (displayException exc)
+ UnliftIO.throwIO exc
+
+ go :: Int -> IO ()
+ go nIter = do
+ runMigration >>= \case
+ 0 -> do
+ Log.info logger $
+ Log.msg (Log.val "finished migration")
+ . Log.field "attempt" nIter
+ . Log.field "migration" name
+ Prometheus.incCounter migFinished
+ n -> do
+ Log.info logger $
+ Log.msg (Log.val "finished migration with errors")
+ . Log.field "migration" name
+ . Log.field "errors" n
+ . Log.field "attempt" nIter
+ go (nIter + 1)
+
+ runMigration :: IO Int
+ runMigration =
+ fmap fst
+ . interpreter
+ $ runConduit migration
+
+logRetrievedPage :: (Member TinyLog r) => Int32 -> (a -> b) -> ConduitM (Int32, [a]) [b] (Sem r) ()
+logRetrievedPage pageSize toRow =
+ C.mapM
+ ( \(i, rows) -> do
+ let estimatedRowsSoFar = (i - 1) * pageSize + fromIntegral (length rows)
+ info $ Log.msg (Log.val "retrieved page") . Log.field "estimatedRowsSoFar" estimatedRowsSoFar
+ pure $ map toRow rows
+ )
+
+withCount :: (Monad m) => ConduitM () [a] m () -> ConduitM () (Int32, [a]) m ()
+withCount = zipSources (C.sourceList [1 ..])
+
+paginateSem ::
+ forall a b q r.
+ ( Tuple a,
+ Tuple b,
+ RunQ q,
+ Member (Input ClientState) r,
+ Member TinyLog r,
+ Member (Embed IO) r
+ ) =>
+ q R a b ->
+ QueryParams a ->
+ RetrySettings ->
+ ConduitT () [b] (Sem r) ()
+paginateSem q p r = do
+ go =<< lift getFirstPage
+ where
+ go page = do
+ lift $ info $ Log.msg (Log.val "got a page")
+ unless (null (result page)) $
+ yield (result page)
+ when (hasMore page) $
+ go =<< lift (getNextPage page)
+
+ getFirstPage :: Sem r (Page b)
+ getFirstPage = do
+ client <- input
+ embedClient client $ retry r (paginate q p)
+
+ getNextPage :: Page b -> Sem r (Page b)
+ getNextPage page = do
+ client <- input
+ embedClient client $ retry r (nextPage page)
diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal
index 5b29d71a86..090fe8d5bf 100644
--- a/libs/wire-subsystems/wire-subsystems.cabal
+++ b/libs/wire-subsystems/wire-subsystems.cabal
@@ -228,7 +228,9 @@ library
Wire.CodeStore.Cassandra
Wire.CodeStore.Cassandra.Queries
Wire.CodeStore.Code
- Wire.CodeStore.Scope
+ Wire.CodeStore.DualWrite
+ Wire.CodeStore.Migration
+ Wire.CodeStore.Postgres
Wire.ConversationStore
Wire.ConversationStore.Cassandra
Wire.ConversationStore.Cassandra.Instances
@@ -294,6 +296,7 @@ library
Wire.LegalHoldStore.Cassandra.Queries
Wire.LegalHoldStore.Env
Wire.ListItems
+ Wire.Migration
Wire.NotificationSubsystem
Wire.NotificationSubsystem.Interpreter
Wire.PaginationState
diff --git a/postgres-schema.sql b/postgres-schema.sql
index 8cfc62bd5d..a0f4b619b3 100644
--- a/postgres-schema.sql
+++ b/postgres-schema.sql
@@ -9,8 +9,8 @@
\restrict 79bbfb4630959c48307653a5cd3d83f2582b3c2210f75f10d79e3ebf0015620
--- Dumped from database version 17.7
--- Dumped by pg_dump version 17.7
+-- Dumped from database version 17.6
+-- Dumped by pg_dump version 17.6
SET statement_timeout = 0;
SET lock_timeout = 0;
@@ -102,6 +102,21 @@ CREATE TABLE public.conversation (
ALTER TABLE public.conversation OWNER TO "wire-server";
+--
+-- Name: conversation_codes; Type: TABLE; Schema: public; Owner: wire-server
+--
+
+CREATE TABLE public.conversation_codes (
+ key text NOT NULL,
+ conversation uuid NOT NULL,
+ password bytea,
+ value text NOT NULL,
+ expires_at timestamp with time zone NOT NULL
+);
+
+
+ALTER TABLE public.conversation_codes OWNER TO "wire-server";
+
--
-- Name: conversation_member; Type: TABLE; Schema: public; Owner: wire-server
--
@@ -282,6 +297,14 @@ ALTER TABLE ONLY public.collaborators
ADD CONSTRAINT collaborators_pkey PRIMARY KEY (user_id, team_id);
+--
+-- Name: conversation_codes conversation_codes_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server
+--
+
+ALTER TABLE ONLY public.conversation_codes
+ ADD CONSTRAINT conversation_codes_pkey PRIMARY KEY (key);
+
+
--
-- Name: conversation_member conversation_member_pkey; Type: CONSTRAINT; Schema: public; Owner: wire-server
--
@@ -392,6 +415,20 @@ CREATE INDEX collaborators_team_id_idx ON public.collaborators USING btree (team
CREATE INDEX collaborators_user_id_idx ON public.collaborators USING btree (user_id);
+--
+-- Name: conversation_codes_expires_at_idx; Type: INDEX; Schema: public; Owner: wire-server
+--
+
+CREATE INDEX conversation_codes_expires_at_idx ON public.conversation_codes USING btree (expires_at);
+
+
+--
+-- Name: conversation_codes_key_expires_at_idx; Type: INDEX; Schema: public; Owner: wire-server
+--
+
+CREATE INDEX conversation_codes_key_expires_at_idx ON public.conversation_codes USING btree (key, expires_at);
+
+
--
-- Name: conversation_member_user_idx; Type: INDEX; Schema: public; Owner: wire-server
--
diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal
index 891e864d63..595e3d01ea 100644
--- a/services/background-worker/background-worker.cabal
+++ b/services/background-worker/background-worker.cabal
@@ -21,7 +21,7 @@ library
Wire.BackgroundWorker.Options
Wire.BackgroundWorker.Util
Wire.DeadUserNotificationWatcher
- Wire.MigrateConversations
+ Wire.PostgresMigrations
hs-source-dirs: src
default-language: GHC2021
diff --git a/services/background-worker/background-worker.integration.yaml b/services/background-worker/background-worker.integration.yaml
index 4ee7abbe10..db49fb502d 100644
--- a/services/background-worker/background-worker.integration.yaml
+++ b/services/background-worker/background-worker.integration.yaml
@@ -68,6 +68,7 @@ migrateConversations: false
migrateConversationsOptions:
pageSize: 10000
parallelism: 2
+migrateConversationCodes: false
# Background jobs consumer configuration for integration
backgroundJobs:
@@ -77,3 +78,4 @@ backgroundJobs:
postgresMigration:
conversation: postgresql
+ conversationCodes: postgresql
diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs
index c30c1d809a..ab20dd7c8b 100644
--- a/services/background-worker/src/Wire/BackgroundWorker.hs
+++ b/services/background-worker/src/Wire/BackgroundWorker.hs
@@ -34,7 +34,8 @@ import Wire.BackgroundWorker.Health qualified as Health
import Wire.BackgroundWorker.Jobs.Consumer qualified as Jobs
import Wire.BackgroundWorker.Options
import Wire.DeadUserNotificationWatcher qualified as DeadUserNotificationWatcher
-import Wire.MigrateConversations qualified as MigrateConversations
+import Wire.Migration
+import Wire.PostgresMigrations qualified as Migrations
run :: Opts -> IO ()
run opts = do
@@ -53,7 +54,14 @@ run opts = do
then
runAppT env $
withNamedLogger "migrate-conversations" $
- MigrateConversations.startWorker opts.migrateConversationsOptions
+ Migrations.conversations opts.migrateConversationsOptions
+ else pure $ pure ()
+ cleanUpConvCodesMigration <-
+ if opts.migrateConversationCodes
+ then
+ runAppT env $
+ withNamedLogger "migrate-conversation-codes" $
+ Migrations.conversationCodes (MigrationOptions 1000 1)
else pure $ pure ()
cleanupJobs <-
runAppT env $
@@ -61,10 +69,11 @@ run opts = do
Jobs.startWorker amqpEP
let cleanup =
void . runConcurrently $
- (,,,)
+ (,,,,)
<$> Concurrently cleanupDeadUserNotifWatcher
<*> Concurrently cleanupBackendNotifPusher
<*> Concurrently cleanupConvMigration
+ <*> Concurrently cleanUpConvCodesMigration
<*> Concurrently cleanupJobs
let server = defaultServer (T.unpack opts.backgroundWorker.host) opts.backgroundWorker.port env.logger
diff --git a/services/background-worker/src/Wire/BackgroundWorker/Options.hs b/services/background-worker/src/Wire/BackgroundWorker/Options.hs
index 48cc531b58..6dc18f03a2 100644
--- a/services/background-worker/src/Wire/BackgroundWorker/Options.hs
+++ b/services/background-worker/src/Wire/BackgroundWorker/Options.hs
@@ -28,7 +28,7 @@ import Network.AMQP.Extended
import System.Logger.Extended
import Util.Options
import Wire.ConversationStore (PostgresMigrationOpts)
-import Wire.ConversationStore.Migration (MigrationOptions)
+import Wire.Migration (MigrationOptions)
data Opts = Opts
{ logLevel :: !Level,
@@ -52,6 +52,7 @@ data Opts = Opts
postgresMigration :: !PostgresMigrationOpts,
migrateConversations :: !Bool,
migrateConversationsOptions :: !MigrationOptions,
+ migrateConversationCodes :: !Bool,
backgroundJobs :: BackgroundJobsConfig,
federationDomain :: Domain
}
diff --git a/services/background-worker/src/Wire/MigrateConversations.hs b/services/background-worker/src/Wire/PostgresMigrations.hs
similarity index 69%
rename from services/background-worker/src/Wire/MigrateConversations.hs
rename to services/background-worker/src/Wire/PostgresMigrations.hs
index 75587e1ae5..a4c1cbff2d 100644
--- a/services/background-worker/src/Wire/MigrateConversations.hs
+++ b/services/background-worker/src/Wire/PostgresMigrations.hs
@@ -15,7 +15,7 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
-module Wire.MigrateConversations where
+module Wire.PostgresMigrations where
import Imports
import Prometheus
@@ -23,10 +23,11 @@ import System.Logger qualified as Log
import UnliftIO
import Wire.BackgroundWorker.Env
import Wire.BackgroundWorker.Util
+import Wire.CodeStore.Migration
import Wire.ConversationStore.Migration
-startWorker :: MigrationOptions -> AppT IO CleanupAction
-startWorker migOpts = do
+conversations :: MigrationOptions -> AppT IO CleanupAction
+conversations migOpts = do
cassClient <- asks (.cassandraGalley)
pgPool <- asks (.hasqlPool)
logger <- asks (.logger)
@@ -47,3 +48,20 @@ startWorker migOpts = do
Log.info logger $ Log.msg (Log.val "cancelling conversation migration")
cancel convLoop
cancel userLoop
+
+conversationCodes :: MigrationOptions -> AppT IO CleanupAction
+conversationCodes migOpts = do
+ cassClient <- asks (.cassandraGalley)
+ pgPool <- asks (.hasqlPool)
+ logger <- asks (.logger)
+ Log.info logger $ Log.msg (Log.val "starting conversation codes migration")
+ count <- register $ counter $ Prometheus.Info "wire_conv_codes_migrated_to_pg" "Number of conversation codes migrated to Postgresql"
+ finished <- register $ counter $ Prometheus.Info "wire_conv_codes_migration_finished" "Whether the conversation codes migration to Postgresql is finished successfully"
+ failed <- register $ counter $ Prometheus.Info "wire_conv_codes_migration_failed" "Whether the conversation codes migration to Postgresql has failed"
+
+ migrationLoop <- async . lift $ migrateCodesLoop migOpts cassClient pgPool logger count finished failed
+
+ Log.info logger $ Log.msg (Log.val "started conversation codes migration")
+ pure $ do
+ Log.info logger $ Log.msg (Log.val "cancelling conversation codes migration")
+ cancel migrationLoop
diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs
index 04307949b2..e10ab43123 100644
--- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs
+++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs
@@ -361,7 +361,11 @@ spec = do
amqpJobsPublisherChannel = undefined
amqpBackendNotificationsChannel = undefined
federationDomain = Domain "local"
- postgresMigration = PostgresMigrationOpts CassandraStorage
+ postgresMigration =
+ PostgresMigrationOpts
+ { conversation = CassandraStorage,
+ conversationCodes = CassandraStorage
+ }
gundeckEndpoint = undefined
brigEndpoint = undefined
@@ -395,7 +399,11 @@ spec = do
amqpJobsPublisherChannel = undefined
amqpBackendNotificationsChannel = undefined
federationDomain = Domain "local"
- postgresMigration = PostgresMigrationOpts CassandraStorage
+ postgresMigration =
+ PostgresMigrationOpts
+ { conversation = CassandraStorage,
+ conversationCodes = CassandraStorage
+ }
gundeckEndpoint = undefined
brigEndpoint = undefined
backendNotificationMetrics <- mkBackendNotificationMetrics
diff --git a/services/background-worker/test/Test/Wire/Util.hs b/services/background-worker/test/Test/Wire/Util.hs
index f7f24a54f0..cdb020a222 100644
--- a/services/background-worker/test/Test/Wire/Util.hs
+++ b/services/background-worker/test/Test/Wire/Util.hs
@@ -39,7 +39,11 @@ testEnv = do
let cassandra = undefined
cassandraGalley = undefined
cassandraBrig = undefined
- postgresMigration = PostgresMigrationOpts CassandraStorage
+ postgresMigration =
+ PostgresMigrationOpts
+ { conversation = CassandraStorage,
+ conversationCodes = CassandraStorage
+ }
statuses <- newIORef mempty
backendNotificationMetrics <- mkBackendNotificationMetrics
workerRunningGauge <- mkWorkerRunningGauge
diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml
index f4dce07f1b..e2106c63e6 100644
--- a/services/galley/galley.integration.yaml
+++ b/services/galley/galley.integration.yaml
@@ -247,4 +247,4 @@ journal: # if set, journals; if not set, disables journaling
postgresMigration:
conversation: postgresql
- # conversation: cassandra
+ conversationCodes: postgresql
diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs
index 7f40e70076..10474662d9 100644
--- a/services/galley/src/Galley/API/Action.hs
+++ b/services/galley/src/Galley/API/Action.hs
@@ -115,7 +115,6 @@ import Wire.API.User as User
import Wire.BrigAPIAccess qualified as E
import Wire.CodeStore
import Wire.CodeStore qualified as E
-import Wire.CodeStore.Scope (Scope (ReusableCode))
import Wire.ConversationStore qualified as E
import Wire.ConversationSubsystem
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..))
@@ -560,7 +559,7 @@ performAction tag origUser lconv action = do
deleteGroup gidParent
key <- E.makeKey (tUnqualified lcnv)
- E.deleteCode key ReusableCode
+ E.deleteCode key
case convTeam storedConv of
Nothing -> E.deleteConversation (tUnqualified lcnv)
Just tid -> E.deleteTeamConversation tid (tUnqualified lcnv)
@@ -800,7 +799,7 @@ performConversationAccessData qusr lconv action = do
)
$ do
key <- E.makeKey (tUnqualified lcnv)
- E.deleteCode key ReusableCode
+ E.deleteCode key
-- Determine bots and members to be removed
let filterBotsAndMembers =
diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs
index 8596392cef..fd4e4606b8 100644
--- a/services/galley/src/Galley/API/Update.hs
+++ b/services/galley/src/Galley/API/Update.hs
@@ -605,10 +605,10 @@ addCode lusr mbZHost mZcon lcnv mReq = do
ensureGuestsOrNonTeamMembersAllowed conv
convUri <- getConversationCodeURI mbZHost
key <- E.makeKey (tUnqualified lcnv)
- E.getCode key ReusableCode >>= \case
+ E.getCode key >>= \case
Nothing -> do
ttl <- realToFrac . unGuestLinkTTLSeconds . fromMaybe defGuestLinkTTLSeconds . view (settings . guestLinkTTLSeconds) <$> input
- code <- E.generateCode (tUnqualified lcnv) ReusableCode (Timeout ttl)
+ code <- E.generateCode (tUnqualified lcnv) (Timeout ttl)
mPw <- for (mReq >>= (.password)) $ HashPassword.hashPassword8 (RateLimitUser (tUnqualified lusr))
E.createCode code mPw
now <- Now.get
@@ -670,7 +670,7 @@ rmCode lusr zcon lcnv = do
ensureAccess conv CodeAccess
let (bots, users) = localBotsAndUsers $ conv.localMembers
key <- E.makeKey (tUnqualified lcnv)
- E.deleteCode key ReusableCode
+ E.deleteCode key
now <- Now.get
let event = Event (tUntagged lcnv) Nothing (EventFromUser (tUntagged lusr)) now Nothing EdConvCodeDelete
pushConversationEvent (Just zcon) conv event (qualifyAs lusr (map (.id_) users)) bots
@@ -697,7 +697,7 @@ getCode mbZHost lusr cnv = do
ensureAccess conv CodeAccess
ensureConvMember (conv.localMembers) (tUnqualified lusr)
key <- E.makeKey cnv
- (c, mPw) <- E.getCode key ReusableCode >>= noteS @'CodeNotFound
+ (c, mPw) <- E.getCode key >>= noteS @'CodeNotFound
convUri <- getConversationCodeURI mbZHost
pure $ mkConversationCodeInfo (isJust mPw) (codeKey c) (codeValue c) convUri
diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs
index a13c62c15d..d8a1143b9e 100644
--- a/services/galley/src/Galley/API/Util.hs
+++ b/services/galley/src/Galley/API/Util.hs
@@ -750,7 +750,7 @@ verifyReusableCode ::
Sem r DataTypes.Code
verifyReusableCode rateLimitKey checkPw mPtpw convCode = do
(c, mPw) <-
- getCode (conversationKey convCode) DataTypes.ReusableCode
+ getCode (conversationKey convCode)
>>= noteS @'CodeNotFound
unless (DataTypes.codeValue c == conversationCode convCode) $
throwS @'CodeNotFound
diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs
index 1c790dd3af..6d1a34867a 100644
--- a/services/galley/src/Galley/App.hs
+++ b/services/galley/src/Galley/App.hs
@@ -108,6 +108,8 @@ import Wire.AWS qualified as Aws
import Wire.BackendNotificationQueueAccess.RabbitMq qualified as BackendNotificationQueueAccess
import Wire.BrigAPIAccess.Rpc
import Wire.CodeStore.Cassandra
+import Wire.CodeStore.DualWrite
+import Wire.CodeStore.Postgres
import Wire.ConversationStore.Cassandra
import Wire.ConversationStore.Postgres
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..), interpretConversationSubsystem)
@@ -289,6 +291,11 @@ evalGalley e =
CassandraStorage -> interpretConversationStoreToCassandra (e ^. cstate)
MigrationToPostgresql -> interpretConversationStoreToCassandraAndPostgres (e ^. cstate)
PostgresqlStorage -> interpretConversationStoreToPostgres
+ convCodesStoreInterpreter =
+ case (e ^. options . postgresMigration).conversationCodes of
+ CassandraStorage -> interpretCodeStoreToCassandra
+ MigrationToPostgresql -> interpretCodeStoreToCassandraAndPostgres
+ PostgresqlStorage -> interpretCodeStoreToPostgres
localUnit = toLocalUnsafe (e ^. options . settings . federationDomain) ()
teamSubsystemConfig =
TeamSubsystemConfig
@@ -375,7 +382,7 @@ evalGalley e =
. runHashPassword e._options._settings._passwordHashingOptions
. interpretRateLimit e._passwordHashingRateLimitEnv
. interpretProposalStoreToCassandra
- . interpretCodeStoreToCassandra
+ . convCodesStoreInterpreter
. interpretClientStoreToCassandra
. interpretTeamCollaboratorsStoreToPostgres
. interpretFireAndForget
From c8c6ae3632c331487ac1a83ae9c8e086fdf431c6 Mon Sep 17 00:00:00 2001
From: Leif Battermann
Date: Thu, 22 Jan 2026 15:12:55 +0100
Subject: [PATCH 24/26] WPB-21964 follow-up: moved TeamFeatureStore interpreter
to subsystems (#4974)
---
changelog.d/5-internal/WPD-21964 | 2 +-
libs/wire-subsystems/default.nix | 3 ++
.../src/Wire/TeamFeatureStore/Cassandra.hs | 54 ++++++++-----------
libs/wire-subsystems/wire-subsystems.cabal | 2 +
services/galley/default.nix | 2 -
services/galley/galley.cabal | 2 -
services/galley/src/Galley/App.hs | 8 ++-
7 files changed, 34 insertions(+), 39 deletions(-)
rename services/galley/src/Galley/Cassandra/TeamFeatures.hs => libs/wire-subsystems/src/Wire/TeamFeatureStore/Cassandra.hs (79%)
diff --git a/changelog.d/5-internal/WPD-21964 b/changelog.d/5-internal/WPD-21964
index ee6e6bd2b5..05cb3372a2 100644
--- a/changelog.d/5-internal/WPD-21964
+++ b/changelog.d/5-internal/WPD-21964
@@ -1 +1 @@
-Move Feature Flags read to `wire-subsystems`.
+Move Feature Flags read to `wire-subsystems`. (#4918, #4974)
diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix
index bbe9d0d932..cf381ee0da 100644
--- a/libs/wire-subsystems/default.nix
+++ b/libs/wire-subsystems/default.nix
@@ -23,6 +23,7 @@
, case-insensitive
, cassandra-util
, conduit
+, constraints
, containers
, contravariant
, cql
@@ -150,6 +151,7 @@ mkDerivation {
case-insensitive
cassandra-util
conduit
+ constraints
containers
contravariant
cql
@@ -265,6 +267,7 @@ mkDerivation {
case-insensitive
cassandra-util
conduit
+ constraints
containers
contravariant
cql
diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/libs/wire-subsystems/src/Wire/TeamFeatureStore/Cassandra.hs
similarity index 79%
rename from services/galley/src/Galley/Cassandra/TeamFeatures.hs
rename to libs/wire-subsystems/src/Wire/TeamFeatureStore/Cassandra.hs
index 26e4cf32a7..2a897bc6b3 100644
--- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs
+++ b/libs/wire-subsystems/src/Wire/TeamFeatureStore/Cassandra.hs
@@ -17,11 +17,7 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see .
-module Galley.Cassandra.TeamFeatures
- ( interpretTeamFeatureStoreToCassandra,
- getAllTeamFeaturesForServer,
- )
-where
+module Wire.TeamFeatureStore.Cassandra (interpretTeamFeatureStoreToCassandra, TeamFeatureStoreError (..)) where
import Cassandra
import Data.Aeson.Types qualified as A
@@ -29,50 +25,42 @@ import Data.Constraint
import Data.Id
import Data.Map qualified as M
import Data.Text.Lazy qualified as LT
-import Galley.API.Error
-import Galley.API.Teams.Features.Get
-import Galley.Cassandra.Store
-import Galley.Cassandra.Util
import Imports
import Polysemy
import Polysemy.Error
import Polysemy.Input
-import Polysemy.TinyLog
import Wire.API.Team.Feature
import Wire.API.Team.Feature.TH
import Wire.ConversationStore.Cassandra.Instances ()
-import Wire.TeamFeatureStore qualified as TFS
+import Wire.TeamFeatureStore (TeamFeatureStore (..))
+import Wire.Util
+
+data TeamFeatureStoreError = TeamFeatureStoreErrorInternalError LText
interpretTeamFeatureStoreToCassandra ::
( Member (Embed IO) r,
Member (Input ClientState) r,
- Member (Error InternalError) r,
- Member TinyLog r
+ Member (Error TeamFeatureStoreError) r
) =>
- Sem (TFS.TeamFeatureStore ': r) a ->
+ Sem (TeamFeatureStore ': r) a ->
Sem r a
interpretTeamFeatureStoreToCassandra = interpret $ \case
- TFS.GetDbFeature sing tid -> do
- logEffect "TeamFeatureStore.GetFeatureConfig"
+ GetDbFeature sing tid -> do
getDbFeatureDyn sing tid
- TFS.SetDbFeature sing tid feat -> do
- logEffect "TeamFeatureStore.SetFeatureConfig"
+ SetDbFeature sing tid feat -> do
setDbFeatureDyn sing tid feat
- TFS.SetFeatureLockStatus sing tid lock -> do
- logEffect "TeamFeatureStore.SetFeatureLockStatus"
+ SetFeatureLockStatus sing tid lock -> do
setFeatureLockStatusDyn sing tid (Tagged lock)
- TFS.GetAllDbFeatures tid -> do
- logEffect "TeamFeatureStore.GetAllTeamFeatures"
+ GetAllDbFeatures tid -> do
getAllDbFeaturesDyn tid
- TFS.PatchDbFeature sing tid feat -> do
- logEffect "TeamFeatureStore.PatchDbFeature"
+ PatchDbFeature sing tid feat -> do
patchDbFeatureDyn sing tid feat
getDbFeatureDyn ::
forall cfg r.
( Member (Input ClientState) r,
Member (Embed IO) r,
- Member (Error InternalError) r
+ Member (Error TeamFeatureStoreError) r
) =>
FeatureSingleton cfg ->
TeamId ->
@@ -81,7 +69,7 @@ getDbFeatureDyn sing tid = case featureSingIsFeature sing of
Dict -> do
let q :: PrepQuery R (TeamId, Text) (Maybe FeatureStatus, Maybe LockStatus, Maybe DbConfig)
q = "select status, lock_status, config from team_features_dyn where team = ? and feature = ?"
- embedClient (retry x1 $ query1 q (params LocalQuorum (tid, featureName @cfg))) >>= \case
+ (embedClientInput (retry x1 $ query1 q (params LocalQuorum (tid, featureName @cfg)))) >>= \case
Nothing -> pure mempty
Just (status, lockStatus, config) ->
runFeatureParser . parseDbFeature $
@@ -117,7 +105,7 @@ patchDbFeatureDyn ::
LockableFeaturePatch cfg ->
Sem r ()
patchDbFeatureDyn sing tid patch = case featureSingIsFeature sing of
- Dict -> embedClient $ do
+ Dict -> embedClientInput $ do
retry x5 . batch $ do
setType BatchLogged
setConsistency LocalQuorum
@@ -147,21 +135,21 @@ setFeatureLockStatusDyn sing tid (Tagged lockStatus) = case featureSingIsFeature
Dict -> do
let q :: PrepQuery W (LockStatus, TeamId, Text) ()
q = "update team_features_dyn set lock_status = ? where team = ? and feature = ?"
- embedClient $
+ embedClientInput $
retry x5 $
write q (params LocalQuorum (lockStatus, tid, featureName @cfg))
getAllDbFeaturesDyn ::
( Member (Embed IO) r,
- Member (Error InternalError) r,
- Member (Input ClientState) r
+ Member (Input ClientState) r,
+ Member (Error TeamFeatureStoreError) r
) =>
TeamId ->
Sem r (AllFeatures DbFeature)
getAllDbFeaturesDyn tid = do
let q :: PrepQuery R (Identity TeamId) (Text, Maybe FeatureStatus, Maybe LockStatus, Maybe DbConfig)
q = "select feature, status, lock_status, config from team_features_dyn where team = ?"
- rows <- embedClient $ retry x1 $ query q (params LocalQuorum (Identity tid))
+ rows <- embedClientInput $ retry x1 $ query q (params LocalQuorum (Identity tid))
let m = M.fromList $ do
(name, status, lockStatus, config) <- rows
pure (name, LockableFeaturePatch {..})
@@ -169,10 +157,10 @@ getAllDbFeaturesDyn tid = do
runFeatureParser ::
forall r a.
- (Member (Error InternalError) r) =>
+ (Member (Error TeamFeatureStoreError) r) =>
A.Parser a ->
Sem r a
runFeatureParser p =
- mapError (InternalErrorWithDescription . LT.pack)
+ mapError (TeamFeatureStoreErrorInternalError . LT.pack)
. fromEither
$ A.parseEither (const p) ()
diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal
index 090fe8d5bf..5dc5e19c77 100644
--- a/libs/wire-subsystems/wire-subsystems.cabal
+++ b/libs/wire-subsystems/wire-subsystems.cabal
@@ -102,6 +102,7 @@ common common-all
, case-insensitive
, cassandra-util
, conduit
+ , constraints
, containers
, contravariant
, cql
@@ -332,6 +333,7 @@ library
Wire.TeamCollaboratorsSubsystem
Wire.TeamCollaboratorsSubsystem.Interpreter
Wire.TeamFeatureStore
+ Wire.TeamFeatureStore.Cassandra
Wire.TeamInvitationSubsystem
Wire.TeamInvitationSubsystem.Error
Wire.TeamInvitationSubsystem.Interpreter
diff --git a/services/galley/default.nix b/services/galley/default.nix
index a6dbd9161e..988d5378dc 100644
--- a/services/galley/default.nix
+++ b/services/galley/default.nix
@@ -23,7 +23,6 @@
, cereal
, comonad
, conduit
-, constraints
, containers
, cookie
, crypton
@@ -144,7 +143,6 @@ mkDerivation {
cassandra-util
cassava
comonad
- constraints
containers
crypton
crypton-x509
diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal
index 5120488855..f1d6b4f299 100644
--- a/services/galley/galley.cabal
+++ b/services/galley/galley.cabal
@@ -143,7 +143,6 @@ library
Galley.Cassandra.SearchVisibility
Galley.Cassandra.Store
Galley.Cassandra.Team
- Galley.Cassandra.TeamFeatures
Galley.Cassandra.TeamNotifications
Galley.Cassandra.Util
Galley.Data.TeamNotifications
@@ -267,7 +266,6 @@ library
, cassandra-util >=0.16.2
, cassava >=0.5.2
, comonad
- , constraints
, containers >=0.5
, crypton
, crypton-x509
diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs
index 6d1a34867a..2035a64c1c 100644
--- a/services/galley/src/Galley/App.hs
+++ b/services/galley/src/Galley/App.hs
@@ -62,7 +62,6 @@ import Galley.Cassandra.Team
interpretTeamMemberStoreToCassandra,
interpretTeamMemberStoreToCassandraWithPaging,
)
-import Galley.Cassandra.TeamFeatures
import Galley.Cassandra.TeamNotifications
import Galley.Effects
import Galley.Env
@@ -115,6 +114,7 @@ import Wire.ConversationStore.Postgres
import Wire.ConversationSubsystem.Interpreter (ConversationSubsystemConfig (..), interpretConversationSubsystem)
import Wire.Error
import Wire.ExternalAccess.External
+import Wire.FeaturesConfigSubsystem
import Wire.FeaturesConfigSubsystem.Interpreter (runFeaturesConfigSubsystem)
import Wire.FeaturesConfigSubsystem.Types (ExposeInvitationURLsAllowlist (..))
import Wire.FederationAPIAccess.Interpreter
@@ -138,6 +138,7 @@ import Wire.ServiceStore.Cassandra (interpretServiceStoreToCassandra)
import Wire.SparAPIAccess.Rpc
import Wire.TeamCollaboratorsStore.Postgres (interpretTeamCollaboratorsStoreToPostgres)
import Wire.TeamCollaboratorsSubsystem.Interpreter
+import Wire.TeamFeatureStore.Cassandra
import Wire.TeamJournal.Aws
import Wire.TeamStore.Cassandra (interpretTeamStoreToCassandra)
import Wire.TeamSubsystem.Interpreter
@@ -149,6 +150,7 @@ type GalleyEffects0 =
Input Hasql.Pool,
Input Env,
Input ConversationSubsystemConfig,
+ Error TeamFeatureStoreError,
Error MigrationError,
Error InvalidInput,
Error ParseException,
@@ -345,6 +347,7 @@ evalGalley e =
. mapError toResponse
. mapError toResponse
. logAndMapError toResponse (Text.pack . show) "migration error"
+ . mapError mapTeamFeatureStoreError
. runInputConst conversationSubsystemConfig
. runInputConst e
. runInputConst (e ^. hasqlPool)
@@ -410,3 +413,6 @@ interpretTeamFeatureSpecialContext :: Env -> Sem (Input (FeatureDefaults Legalho
interpretTeamFeatureSpecialContext e =
runInputConst
(e ^. options . settings . featureFlags . to npProject)
+
+mapTeamFeatureStoreError :: TeamFeatureStoreError -> InternalError
+mapTeamFeatureStoreError (TeamFeatureStoreErrorInternalError msg) = InternalErrorWithDescription msg
From 1ee2bfb681909a245cf2b0f4e8e39ce178469c70 Mon Sep 17 00:00:00 2001
From: Zebot
Date: Mon, 26 Jan 2026 09:14:31 +0000
Subject: [PATCH 25/26] Add changelog for Release 2026-01-26
---
CHANGELOG.md | 69 +++++++++++++++++++
.../WPB-21366-refactor-brig-index-cli | 1 -
changelog.d/0-release-notes/WPB-22811 | 5 --
changelog.d/0-release-notes/WPB-22901 | 1 -
.../0-release-notes/add-type-field-to-contact | 3 -
changelog.d/0-release-notes/old-kubernetes | 1 -
changelog.d/1-api-changes/WPB-22793-get-apps | 1 -
.../1-api-changes/add-type-field-to-contact | 1 -
changelog.d/2-features/federator-tls-issuer | 1 -
changelog.d/2-features/log-saml-idp-changes | 1 -
.../2-features/mls-bundle-extra-messages | 1 -
changelog.d/3-bug-fixes/WPB-22901 | 1 -
changelog.d/5-internal/WPB-22541 | 1 -
changelog.d/5-internal/WPB-22811 | 1 -
changelog.d/5-internal/WPB-9935 | 3 -
changelog.d/5-internal/WPD-19575 | 1 -
changelog.d/5-internal/WPD-21964 | 1 -
.../federator-fsnotify-cross-platform | 3 -
.../5-internal/simplify_rusty-jwt-tools_setup | 2 -
19 files changed, 69 insertions(+), 29 deletions(-)
delete mode 100644 changelog.d/0-release-notes/WPB-21366-refactor-brig-index-cli
delete mode 100644 changelog.d/0-release-notes/WPB-22811
delete mode 100644 changelog.d/0-release-notes/WPB-22901
delete mode 100644 changelog.d/0-release-notes/add-type-field-to-contact
delete mode 100644 changelog.d/0-release-notes/old-kubernetes
delete mode 100644 changelog.d/1-api-changes/WPB-22793-get-apps
delete mode 100644 changelog.d/1-api-changes/add-type-field-to-contact
delete mode 100644 changelog.d/2-features/federator-tls-issuer
delete mode 100644 changelog.d/2-features/log-saml-idp-changes
delete mode 100644 changelog.d/2-features/mls-bundle-extra-messages
delete mode 100644 changelog.d/3-bug-fixes/WPB-22901
delete mode 100644 changelog.d/5-internal/WPB-22541
delete mode 100644 changelog.d/5-internal/WPB-22811
delete mode 100644 changelog.d/5-internal/WPB-9935
delete mode 100644 changelog.d/5-internal/WPD-19575
delete mode 100644 changelog.d/5-internal/WPD-21964
delete mode 100644 changelog.d/5-internal/federator-fsnotify-cross-platform
delete mode 100644 changelog.d/5-internal/simplify_rusty-jwt-tools_setup
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dbfc9d778c..5e03f36f7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,72 @@
+# [2026-01-26] (Chart Release 5.26.0)
+
+## Release notes
+
+
+* Elasticsearch re-indexing requires postgres access now. If you run `brig-index` directly anywhere, make sure to add the relevant settings. The Elasticsearch index must be refilled from Cassandra in order for the changes to the search results to take effect. See https://docs.wire.com/latest/developer/reference/elastic-search.html?h=index#refill-es-documents-from-cassandra
+
+* Conversation codes can now be migrated to PostgreSQL. For existing installations:
+ - Set `postgresMigration.conversationCodes: migration-to-postgresql` in both `galley` and `background-worker`.
+ - Run the backfill with `migrateConversationCodes: true`.
+ - Wait for `wire_conv_codes_migration_finished` to reach `1.0`.
+ - Switch to `postgresMigration.conversationCodes: postgresql` and disable `migrateConversationCodes`. (#4961)
+
+* The background-worker defaults for the postgres migration now match galley and point to cassandra (previously postgres). This currenlty only affects the background job, which is not expected to run before postgres is in use. However, if you relied on the defaults after migrating to postgres, please update your config to keep using postgres. (#4965)
+
+* Since the index mapping has been updated, the elastic search index
+ needs to be refilled from Cassandra, see
+ https://docs.wire.com/latest/developer/reference/elastic-search.html?h=index#refill-es-documents-from-cassandra
+
+* Drop support for kubernetes versions below 1.27 (#4969)
+
+
+## API changes
+
+
+* New end-point `GET /teams/:tid/apps` listing all team apps. (#4960)
+
+* Add `type` field to search results received from `GET /search/contacts` (#4913)
+
+
+## Features
+
+
+* nginx-ingress-services: Add `federator.tls.issuer` option to use a separate ClusterIssuer for federation mTLS certificates. (#4964)
+
+* Log changes to IdP configurations made via the IdP REST API to syslog. (#4935)
+
+* Allow commit bundles to contain one application message. The message must be for the epoch *after* the commit, and it gets sent after the commit has been accepted. (#4929)
+
+
+## Bug fixes and other updates
+
+
+* `background-worker`'s default settings for `postgresMigration` have been correctly set to `cassandra`. (#4965)
+
+
+## Internal changes
+
+
+* Circumvent potential performance issue with `TVar (Map ...)` (#4948)
+
+* Migration of conversation codes from cassandra to postgres (#4959, #4961)
+
+* - Test for team and user email templates added
+ - Refactoring to make email rendering testable
+ - Removed SMS and call templates (#4699)
+
+* Drop `cryptobox`, handle prekey in pure Haskell. (#4719)
+
+* Move Feature Flags read to `wire-subsystems`. (#4918, #4974)
+
+* Federator: Replace Linux-only hinotify with cross-platform fsnotify library
+ for certificate file monitoring. This enables native file system watching
+ on both Linux and macOS, removing the need for platform-specific stubs. (#4955)
+
+* Simplify and modernize the Nix setup of `rusty-jwt-tools`. This includes
+ updating to version `0.14.0`. (#4952)
+
+
# [2026-01-13] (Chart Release 5.25.0)
## Release notes
diff --git a/changelog.d/0-release-notes/WPB-21366-refactor-brig-index-cli b/changelog.d/0-release-notes/WPB-21366-refactor-brig-index-cli
deleted file mode 100644
index db52bee775..0000000000
--- a/changelog.d/0-release-notes/WPB-21366-refactor-brig-index-cli
+++ /dev/null
@@ -1 +0,0 @@
-Elasticsearch re-indexing requires postgres access now. If you run `brig-index` directly anywhere, make sure to add the relevant settings. The Elasticsearch index must be refilled from Cassandra in order for the changes to the search results to take effect. See https://docs.wire.com/latest/developer/reference/elastic-search.html?h=index#refill-es-documents-from-cassandra
diff --git a/changelog.d/0-release-notes/WPB-22811 b/changelog.d/0-release-notes/WPB-22811
deleted file mode 100644
index e30e9e739b..0000000000
--- a/changelog.d/0-release-notes/WPB-22811
+++ /dev/null
@@ -1,5 +0,0 @@
-Conversation codes can now be migrated to PostgreSQL. For existing installations:
-- Set `postgresMigration.conversationCodes: migration-to-postgresql` in both `galley` and `background-worker`.
-- Run the backfill with `migrateConversationCodes: true`.
-- Wait for `wire_conv_codes_migration_finished` to reach `1.0`.
-- Switch to `postgresMigration.conversationCodes: postgresql` and disable `migrateConversationCodes`.
diff --git a/changelog.d/0-release-notes/WPB-22901 b/changelog.d/0-release-notes/WPB-22901
deleted file mode 100644
index 4dadea8fac..0000000000
--- a/changelog.d/0-release-notes/WPB-22901
+++ /dev/null
@@ -1 +0,0 @@
-The background-worker defaults for the postgres migration now match galley and point to cassandra (previously postgres). This currenlty only affects the background job, which is not expected to run before postgres is in use. However, if you relied on the defaults after migrating to postgres, please update your config to keep using postgres.
diff --git a/changelog.d/0-release-notes/add-type-field-to-contact b/changelog.d/0-release-notes/add-type-field-to-contact
deleted file mode 100644
index 10d7af72c0..0000000000
--- a/changelog.d/0-release-notes/add-type-field-to-contact
+++ /dev/null
@@ -1,3 +0,0 @@
-Since the index mapping has been updated, the elastic search index
-needs to be refilled from Cassandra, see
-https://docs.wire.com/latest/developer/reference/elastic-search.html?h=index#refill-es-documents-from-cassandra
\ No newline at end of file
diff --git a/changelog.d/0-release-notes/old-kubernetes b/changelog.d/0-release-notes/old-kubernetes
deleted file mode 100644
index b8e99353a0..0000000000
--- a/changelog.d/0-release-notes/old-kubernetes
+++ /dev/null
@@ -1 +0,0 @@
-Drop support for kubernetes versions below 1.27
diff --git a/changelog.d/1-api-changes/WPB-22793-get-apps b/changelog.d/1-api-changes/WPB-22793-get-apps
deleted file mode 100644
index 736b9910d1..0000000000
--- a/changelog.d/1-api-changes/WPB-22793-get-apps
+++ /dev/null
@@ -1 +0,0 @@
-New end-point `GET /teams/:tid/apps` listing all team apps.
\ No newline at end of file
diff --git a/changelog.d/1-api-changes/add-type-field-to-contact b/changelog.d/1-api-changes/add-type-field-to-contact
deleted file mode 100644
index cb58adb825..0000000000
--- a/changelog.d/1-api-changes/add-type-field-to-contact
+++ /dev/null
@@ -1 +0,0 @@
-Add `type` field to search results received from `GET /search/contacts`
\ No newline at end of file
diff --git a/changelog.d/2-features/federator-tls-issuer b/changelog.d/2-features/federator-tls-issuer
deleted file mode 100644
index 759d6a0125..0000000000
--- a/changelog.d/2-features/federator-tls-issuer
+++ /dev/null
@@ -1 +0,0 @@
-nginx-ingress-services: Add `federator.tls.issuer` option to use a separate ClusterIssuer for federation mTLS certificates.
diff --git a/changelog.d/2-features/log-saml-idp-changes b/changelog.d/2-features/log-saml-idp-changes
deleted file mode 100644
index 130a4281a1..0000000000
--- a/changelog.d/2-features/log-saml-idp-changes
+++ /dev/null
@@ -1 +0,0 @@
-Log changes to IdP configurations made via the IdP REST API to syslog.
diff --git a/changelog.d/2-features/mls-bundle-extra-messages b/changelog.d/2-features/mls-bundle-extra-messages
deleted file mode 100644
index ea51d5af86..0000000000
--- a/changelog.d/2-features/mls-bundle-extra-messages
+++ /dev/null
@@ -1 +0,0 @@
-Allow commit bundles to contain one application message. The message must be for the epoch *after* the commit, and it gets sent after the commit has been accepted.
diff --git a/changelog.d/3-bug-fixes/WPB-22901 b/changelog.d/3-bug-fixes/WPB-22901
deleted file mode 100644
index 5ec4443ea5..0000000000
--- a/changelog.d/3-bug-fixes/WPB-22901
+++ /dev/null
@@ -1 +0,0 @@
-`background-worker`'s default settings for `postgresMigration` have been correctly set to `cassandra`.
diff --git a/changelog.d/5-internal/WPB-22541 b/changelog.d/5-internal/WPB-22541
deleted file mode 100644
index 4638c9d945..0000000000
--- a/changelog.d/5-internal/WPB-22541
+++ /dev/null
@@ -1 +0,0 @@
-Circumvent potential performance issue with `TVar (Map ...)`
diff --git a/changelog.d/5-internal/WPB-22811 b/changelog.d/5-internal/WPB-22811
deleted file mode 100644
index 2d98a74327..0000000000
--- a/changelog.d/5-internal/WPB-22811
+++ /dev/null
@@ -1 +0,0 @@
-Migration of conversation codes from cassandra to postgres (#4959, #4961)
diff --git a/changelog.d/5-internal/WPB-9935 b/changelog.d/5-internal/WPB-9935
deleted file mode 100644
index 13874f33f7..0000000000
--- a/changelog.d/5-internal/WPB-9935
+++ /dev/null
@@ -1,3 +0,0 @@
-- Test for team and user email templates added
-- Refactoring to make email rendering testable
-- Removed SMS and call templates
diff --git a/changelog.d/5-internal/WPD-19575 b/changelog.d/5-internal/WPD-19575
deleted file mode 100644
index acab462a0d..0000000000
--- a/changelog.d/5-internal/WPD-19575
+++ /dev/null
@@ -1 +0,0 @@
-Drop `cryptobox`, handle prekey in pure Haskell.
diff --git a/changelog.d/5-internal/WPD-21964 b/changelog.d/5-internal/WPD-21964
deleted file mode 100644
index 05cb3372a2..0000000000
--- a/changelog.d/5-internal/WPD-21964
+++ /dev/null
@@ -1 +0,0 @@
-Move Feature Flags read to `wire-subsystems`. (#4918, #4974)
diff --git a/changelog.d/5-internal/federator-fsnotify-cross-platform b/changelog.d/5-internal/federator-fsnotify-cross-platform
deleted file mode 100644
index 5f4d306d85..0000000000
--- a/changelog.d/5-internal/federator-fsnotify-cross-platform
+++ /dev/null
@@ -1,3 +0,0 @@
-Federator: Replace Linux-only hinotify with cross-platform fsnotify library
-for certificate file monitoring. This enables native file system watching
-on both Linux and macOS, removing the need for platform-specific stubs.
diff --git a/changelog.d/5-internal/simplify_rusty-jwt-tools_setup b/changelog.d/5-internal/simplify_rusty-jwt-tools_setup
deleted file mode 100644
index 5853dee370..0000000000
--- a/changelog.d/5-internal/simplify_rusty-jwt-tools_setup
+++ /dev/null
@@ -1,2 +0,0 @@
-Simplify and modernize the Nix setup of `rusty-jwt-tools`. This includes
-updating to version `0.14.0`.
From 2301350a449c1ca7eaf61862771e134666bd5fbb Mon Sep 17 00:00:00 2001
From: Matthias Fischmann
Date: Mon, 26 Jan 2026 15:11:10 +0100
Subject: [PATCH 26/26] Tweak CHANGELOG.md
---
CHANGELOG.md | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e03f36f7e..d4d097147f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,7 @@
## Release notes
-* Elasticsearch re-indexing requires postgres access now. If you run `brig-index` directly anywhere, make sure to add the relevant settings. The Elasticsearch index must be refilled from Cassandra in order for the changes to the search results to take effect. See https://docs.wire.com/latest/developer/reference/elastic-search.html?h=index#refill-es-documents-from-cassandra
+* User search provides information about user type (regular, app, legacy bot) now. Also, Elasticsearch re-indexing requires postgres access now. If you run `brig-index` directly anywhere, make sure to add the relevant settings. The Elasticsearch index must be refilled from Cassandra in order for the changes to the search results to take effect. See https://docs.wire.com/latest/developer/reference/elastic-search.html?h=index#refill-es-documents-from-cassandra (#4913, #4957)
* Conversation codes can now be migrated to PostgreSQL. For existing installations:
- Set `postgresMigration.conversationCodes: migration-to-postgresql` in both `galley` and `background-worker`.
@@ -13,10 +13,6 @@
* The background-worker defaults for the postgres migration now match galley and point to cassandra (previously postgres). This currenlty only affects the background job, which is not expected to run before postgres is in use. However, if you relied on the defaults after migrating to postgres, please update your config to keep using postgres. (#4965)
-* Since the index mapping has been updated, the elastic search index
- needs to be refilled from Cassandra, see
- https://docs.wire.com/latest/developer/reference/elastic-search.html?h=index#refill-es-documents-from-cassandra
-
* Drop support for kubernetes versions below 1.27 (#4969)