From 88de892c287a89452d52727f78216611f96699e9 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Fri, 19 Jun 2026 17:46:33 +0200 Subject: [PATCH 1/5] WPB-26474: extends code to meetings --- .../5-internal/WPB-26474-code-store-meetings | 1 + ...260619120000-conversation-codes-target.sql | 4 ++ libs/wire-subsystems/src/Wire/CodeStore.hs | 5 +-- .../src/Wire/CodeStore/Cassandra.hs | 23 +++++++--- .../src/Wire/CodeStore/Code.hs | 43 ++++++++++++++----- .../src/Wire/CodeStore/DualWrite.hs | 11 +++-- .../src/Wire/CodeStore/Migration.hs | 2 +- .../src/Wire/CodeStore/Postgres.hs | 35 +++++++++------ .../src/Wire/ConversationSubsystem/Action.hs | 5 ++- .../src/Wire/ConversationSubsystem/Query.hs | 5 ++- .../src/Wire/ConversationSubsystem/Update.hs | 14 +++--- 11 files changed, 101 insertions(+), 47 deletions(-) create mode 100644 changelog.d/5-internal/WPB-26474-code-store-meetings create mode 100644 libs/wire-subsystems/postgres-migrations/20260619120000-conversation-codes-target.sql diff --git a/changelog.d/5-internal/WPB-26474-code-store-meetings b/changelog.d/5-internal/WPB-26474-code-store-meetings new file mode 100644 index 00000000000..c184a9480da --- /dev/null +++ b/changelog.d/5-internal/WPB-26474-code-store-meetings @@ -0,0 +1 @@ +Extend the conversation Code store to support meeting access codes in addition to conversation codes. Adds a backwards-compatible `target` column to the `conversation_codes` Postgres table (defaults to `'conv'`); the Cassandra store continues to serve conversation codes only. Foundational work for meeting access codes. diff --git a/libs/wire-subsystems/postgres-migrations/20260619120000-conversation-codes-target.sql b/libs/wire-subsystems/postgres-migrations/20260619120000-conversation-codes-target.sql new file mode 100644 index 00000000000..ba077eb17ae --- /dev/null +++ b/libs/wire-subsystems/postgres-migrations/20260619120000-conversation-codes-target.sql @@ -0,0 +1,4 @@ +-- Distinguish conversation codes from meeting codes. Existing rows refer to +-- conversations, so the default is 'conv'. Meeting codes store 'meeting'. +ALTER TABLE conversation_codes + ADD COLUMN target text DEFAULT 'conv' NOT NULL; diff --git a/libs/wire-subsystems/src/Wire/CodeStore.hs b/libs/wire-subsystems/src/Wire/CodeStore.hs index 528b1b23358..bae3e0da4cd 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore.hs @@ -20,7 +20,6 @@ module Wire.CodeStore where import Data.Code -import Data.Id import Data.Misc import Imports import Polysemy @@ -31,8 +30,8 @@ data CodeStore m a where CreateCode :: Code -> Maybe Password -> CodeStore m () GetCode :: Key -> CodeStore m (Maybe (Code, Maybe Password)) DeleteCode :: Key -> CodeStore m () - MakeKey :: ConvId -> CodeStore m Key - GenerateCode :: ConvId -> Timeout -> CodeStore m Code + MakeKey :: CodeReferent -> CodeStore m Key + GenerateCode :: CodeReferent -> 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 d320f13369b..ddd1f1ab600 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs @@ -47,10 +47,14 @@ interpretCodeStoreToCassandra = interpret $ \case embedClientInput $ insertCode code mPw DeleteCode k -> do embedClientInput $ deleteCode k - MakeKey cid -> do - Code.mkKey cid - GenerateCode cid t -> do - Code.generate cid t + MakeKey ref -> case ref of + CodeReferentConv _ -> Code.mkKey ref + CodeReferentMeeting _ -> + error "CodeStore.Cassandra.MakeKey: meetings are not supported on cassandra" + GenerateCode ref t -> case ref of + CodeReferentConv _ -> Code.generate ref t + CodeReferentMeeting _ -> + error "CodeStore.Cassandra.GenerateCode: meetings are not supported on cassandra" GetConversationCodeURI mbHost -> do convCodeURI <- input case convCodeURI of @@ -65,14 +69,21 @@ insertCode :: Code -> Maybe Password -> Client () insertCode c mPw = do let k = codeKey c let v = codeValue c - let cnv = codeConversation c + let cnv = convReferent (codeReferent c) let t = round (codeTTL c) retry x5 (write Cql.insertCode (params LocalQuorum (k, v, cnv, mPw, t))) + where + -- Cassandra only stores conversation codes; meeting codes never reach here + -- because 'GenerateCode' errors out for meetings above. + convReferent (CodeReferentConv cid) = cid + convReferent CodeReferentMeeting {} = + error "CodeStore.Cassandra.insertCode: meetings are not supported on cassandra" -- | Lookup a conversation by code. lookupCode :: Key -> Client (Maybe (Code, Maybe Password)) lookupCode k = - fmap (toCode k) <$> retry x1 (query1 Cql.lookupCode (params LocalQuorum (Identity k))) + fmap (toCode k . (\(val, ttl, cnv, mPw) -> (val, ttl, CodeReferentConv cnv, mPw))) + <$> retry x1 (query1 Cql.lookupCode (params LocalQuorum (Identity k))) -- | Delete a code associated with the given conversation key deleteCode :: Key -> Client () diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Code.hs b/libs/wire-subsystems/src/Wire/CodeStore/Code.hs index c5c497e44be..e12de4273c3 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/Code.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/Code.hs @@ -19,6 +19,8 @@ module Wire.CodeStore.Code ( Code (..), + CodeReferent (..), + codeConvId, toCode, generate, mkKey, @@ -36,27 +38,42 @@ import OpenSSL.EVP.Digest (digestBS, getDigestByName) import OpenSSL.Random (randBytes) import Wire.API.Password (Password) +-- | The conversation or meeting a 'Code' refers to. Since both 'ConvId' and +-- 'MeetingId' are 'Id' values over a 'UUID', they are stored in a single uuid +-- column on the database, disambiguated by this constructor. +data CodeReferent + = CodeReferentConv ConvId + | CodeReferentMeeting MeetingId + deriving (Eq, Show, Generic) + data Code = Code { codeKey :: !Key, codeValue :: !Value, codeTTL :: !Timeout, - codeConversation :: !ConvId, + codeReferent :: !CodeReferent, codeHasPassword :: !Bool } deriving (Eq, Show, Generic) -toCode :: Key -> (Value, Int32, ConvId, Maybe Password) -> (Code, Maybe Password) -toCode k (val, ttl, cnv, mPw) = +toCode :: Key -> (Value, Int32, CodeReferent, Maybe Password) -> (Code, Maybe Password) +toCode k (val, ttl, ref, mPw) = ( Code { codeKey = k, codeValue = val, codeTTL = Timeout (fromIntegral ttl), - codeConversation = cnv, + codeReferent = ref, codeHasPassword = isJust mPw }, mPw ) +-- | Extract the 'ConvId' from a 'Code' that refers to a conversation. +-- Returns 'Nothing' for codes that refer to a meeting. +codeConvId :: Code -> Maybe ConvId +codeConvId c = case codeReferent c of + CodeReferentConv cid -> Just cid + CodeReferentMeeting _ -> Nothing + -- Note on key/value used for a conversation Code -- -- For similar reasons to those given for Codes used for verification, Password reset, etc @@ -64,20 +81,24 @@ toCode k (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 -> Timeout -> m Code -generate cnv t = do - key <- mkKey cnv +generate :: (MonadIO m) => CodeReferent -> Timeout -> m Code +generate ref t = do + key <- mkKey ref val <- liftIO $ Value . unsafeRange . Ascii.encodeBase64Url <$> randBytes 15 pure Code { codeKey = key, codeValue = val, - codeConversation = cnv, + codeReferent = ref, codeTTL = t, codeHasPassword = False } -mkKey :: (MonadIO m) => ConvId -> m Key -mkKey cnv = do +mkKey :: (MonadIO m) => CodeReferent -> m Key +mkKey (CodeReferentConv cid) = mkKeyId cid +mkKey (CodeReferentMeeting mid) = mkKeyId mid + +mkKeyId :: (MonadIO m) => Id a -> m Key +mkKeyId ident = do sha256 <- liftIO $ fromJust <$> getDigestByName "SHA256" - pure $ Key . unsafeRange . Ascii.encodeBase64Url . BS.take 15 $ digestBS sha256 (toByteString' cnv) + pure $ Key . unsafeRange . Ascii.encodeBase64Url . BS.take 15 $ digestBS sha256 (toByteString' ident) diff --git a/libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs b/libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs index 1f7d263b8ae..dba079bd87e 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs @@ -28,6 +28,7 @@ import Polysemy.Input import Wire.CodeStore (CodeStore (..)) import Wire.CodeStore qualified as CodeStore import Wire.CodeStore.Cassandra qualified as Cassandra +import Wire.CodeStore.Code (CodeReferent (..)) import Wire.CodeStore.Postgres qualified as Postgres import Wire.Postgres (PGConstraints) @@ -48,9 +49,11 @@ interpretCodeStoreToCassandraAndPostgres = interpret $ \case 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 + MakeKey ref -> case ref of + CodeReferentConv _ -> Cassandra.interpretCodeStoreToCassandra $ CodeStore.makeKey ref + CodeReferentMeeting _ -> Postgres.interpretCodeStoreToPostgres $ CodeStore.makeKey ref + GenerateCode ref t -> case ref of + CodeReferentConv _ -> Cassandra.interpretCodeStoreToCassandra $ CodeStore.generateCode ref t + CodeReferentMeeting _ -> Postgres.interpretCodeStoreToPostgres $ CodeStore.generateCode ref 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 index 84e1dd771bb..14decb92f63 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/Migration.hs @@ -132,7 +132,7 @@ migrateCodeRow :: Sem r () migrateCodeRow migOpts migCounter migDuration (k, v, ttl, cnv, mPw) = when (ttl > 0) $ do - let (code, _) = toCode k (v, ttl, cnv, mPw) + let (code, _) = toCode k (v, ttl, CodeReferentConv cnv, mPw) keyText = T.pack (show k) outcomeRef <- liftIO $ IORef.newIORef @Text "error" bracket diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs b/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs index 0126216d79f..4e8aeda6d52 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs @@ -21,6 +21,7 @@ module Wire.CodeStore.Postgres where import Data.Code +import Data.Coerce (coerce) import Data.Id import Data.Map qualified as Map import Data.Misc (HttpsUrl) @@ -48,10 +49,10 @@ interpretCodeStoreToPostgres = interpret $ \case insertCode code mPw DeleteCode k -> do deleteCode k - MakeKey cid -> do - Code.mkKey cid - GenerateCode cid t -> do - Code.generate cid t + MakeKey ref -> do + Code.mkKey ref + GenerateCode ref t -> do + Code.generate ref t GetConversationCodeURI mbHost -> do convCodeURI <- input pure $ case convCodeURI of @@ -60,28 +61,37 @@ interpretCodeStoreToPostgres = interpret $ \case insertCode :: (PGConstraints r) => Code -> Maybe Password -> Sem r () insertCode c password = do - runStatement (codeKey c, codeConversation c, password, codeValue c, round (codeTTL c)) insert + runStatement (codeKey c, cnv, password, codeValue c, round (codeTTL c), targetTxt) insert where - insert :: Hasql.Statement (Key, ConvId, Maybe Password, Value, Int32) () + (cnv, targetTxt) = case codeReferent c of + CodeReferentConv cid -> (cid, "conv") + CodeReferentMeeting mid -> (coerce mid, "meeting") + insert :: Hasql.Statement (Key, ConvId, Maybe Password, Value, Int32, Text) () insert = lmapPG [resultlessStatement|INSERT INTO conversation_codes - (key, conversation, password, value, expires_at) + (key, conversation, password, value, expires_at, target) VALUES - ($1 :: text, $2 :: uuid, $3 :: bytea?, $4 :: text, now() + make_interval(secs => $5 :: int)) + ($1 :: text, $2 :: uuid, $3 :: bytea?, $4 :: text, now() + make_interval(secs => $5 :: int), $6 :: text) ON CONFLICT (key) DO UPDATE SET conversation = ($2 :: uuid), password = ($3 :: bytea?), value = ($4 :: text), - expires_at = now() + make_interval(secs => $5 :: int) + expires_at = now() + make_interval(secs => $5 :: int), + target = ($6 :: text) |] lookupCode :: (PGConstraints r) => Key -> Sem r (Maybe (Code, Maybe Password)) lookupCode k = do mRow <- runStatement k selectCode - pure $ fmap (toCode k) mRow + pure $ fmap (toCode k . mkReferent) mRow where - selectCode :: Hasql.Statement Key (Maybe (Value, Int32, ConvId, Maybe Password)) + mkReferent (val, ttl, cnv, mPw, targetTxt) = + let ref = case targetTxt :: Text of + "meeting" -> CodeReferentMeeting (coerce cnv) + _ -> CodeReferentConv cnv + in (val, ttl, ref, mPw) + selectCode :: Hasql.Statement Key (Maybe (Value, Int32, ConvId, Maybe Password, Text)) selectCode = dimapPG -- on the extraction of the remaining seconds of the TTL @@ -94,7 +104,8 @@ lookupCode k = do value :: text, GREATEST(0, FLOOR(EXTRACT(EPOCH FROM (expires_at - now()))))::int4 AS ttl_secs, conversation :: uuid, - password :: bytea? + password :: bytea?, + target :: text FROM conversation_codes WHERE key = ($1 :: text) AND expires_at > now () |] diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Action.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Action.hs index 957a9f445e6..a6e6921db12 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Action.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Action.hs @@ -114,6 +114,7 @@ import Wire.BackendNotificationQueueAccess import Wire.BrigAPIAccess qualified as E import Wire.CodeStore import Wire.CodeStore qualified as E +import Wire.CodeStore.Code (CodeReferent (..)) import Wire.ConversationStore qualified as E import Wire.ConversationSubsystem.Action.Kick import Wire.ConversationSubsystem.Action.Leave @@ -390,7 +391,7 @@ instance IsConversationAction 'ConversationDeleteTag where deleteGroup gidSub deleteGroup gidParent - key <- E.makeKey (tUnqualified lcnv) + key <- E.makeKey (CodeReferentConv (tUnqualified lcnv)) E.deleteCode key case convTeam storedConv of Nothing -> E.deleteConversation (tUnqualified lcnv) @@ -912,7 +913,7 @@ performConversationAccessData qusr lconv action = do && CodeAccess `notElem` cupAccess action ) $ do - key <- E.makeKey (tUnqualified lcnv) + key <- E.makeKey (CodeReferentConv (tUnqualified lcnv)) E.deleteCode key -- Determine bots and members to be removed diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs index ce8009d45bd..87ad25e2b77 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Query.hs @@ -98,7 +98,7 @@ import Wire.API.Team.Member (HiddenPerm (..), TeamMember) import Wire.API.User import Wire.BrigAPIAccess (BrigAPIAccess) import Wire.CodeStore -import Wire.CodeStore.Code (Code (codeConversation)) +import Wire.CodeStore.Code (codeConvId) import Wire.CodeStore.Code qualified as Data import Wire.ConversationStore qualified as ConversationStore import Wire.ConversationStore.MLS.Types @@ -656,7 +656,8 @@ getConversationByReusableCode :: Sem r ConversationCoverView getConversationByReusableCode lusr key value = do c <- verifyReusableCode (RateLimitUser (tUnqualified lusr)) False Nothing (ConversationCode key value) - conv <- ConversationStore.getConversation (codeConversation c) >>= noteS @'ConvNotFound + codeCid <- noteS @'ConvNotFound (codeConvId c) + conv <- ConversationStore.getConversation codeCid >>= noteS @'ConvNotFound ensureConversationAccess (tUnqualified lusr) conv CodeAccess ensureGuestLinksEnabled (Data.convTeam conv) pure $ coverView c conv diff --git a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs index 42f907bd939..c55071c44fc 100644 --- a/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs +++ b/libs/wire-subsystems/src/Wire/ConversationSubsystem/Update.hs @@ -529,11 +529,11 @@ addCode lusr mbZHost mZcon lcnv mReq = do ensureAccess conv CodeAccess ensureGuestsOrNonTeamMembersAllowed conv convUri <- getConversationCodeURI mbZHost - key <- E.makeKey (tUnqualified lcnv) + key <- E.makeKey (CodeReferentConv (tUnqualified lcnv)) E.getCode key >>= \case Nothing -> do ttl <- inputs (realToFrac . unGuestLinkTTLSeconds . fromMaybe defGuestLinkTTLSeconds) - code <- E.generateCode (tUnqualified lcnv) (Timeout ttl) + code <- E.generateCode (CodeReferentConv (tUnqualified lcnv)) (Timeout ttl) mPw <- for (mReq >>= (.password)) $ HashPassword.hashPassword8 (RateLimitUser (tUnqualified lusr)) E.createCode code mPw now <- Now.get @@ -594,7 +594,7 @@ rmCode lusr zcon lcnv = do Query.ensureConvAdmin conv (tUnqualified lusr) mTeamMember ensureAccess conv CodeAccess let (bots, users) = localBotsAndUsers $ conv.localMembers - key <- E.makeKey (tUnqualified lcnv) + key <- E.makeKey (CodeReferentConv (tUnqualified lcnv)) E.deleteCode key now <- Now.get let event = Event (tUntagged lcnv) Nothing (EventFromUser (tUntagged lusr)) now Nothing EdConvCodeDelete @@ -621,7 +621,7 @@ getCode mbZHost lusr cnv = do Query.ensureGuestLinksEnabled (convTeam conv) ensureAccess conv CodeAccess ensureConvMember (conv.localMembers) (tUnqualified lusr) - key <- E.makeKey cnv + key <- E.makeKey (CodeReferentConv cnv) (c, mPw) <- E.getCode key >>= noteS @'CodeNotFound convUri <- getConversationCodeURI mbZHost pure $ mkConversationCodeInfo (isJust mPw) (codeKey c) (codeValue c) convUri @@ -642,7 +642,8 @@ checkReusableCode :: Sem r () checkReusableCode origIp convCode = do code <- verifyReusableCode (RateLimitIp origIp) False Nothing convCode - conv <- E.getConversation (codeConversation code) >>= noteS @'ConvNotFound + codeCid <- noteS @'ConvNotFound (codeConvId code) + conv <- E.getConversation codeCid >>= noteS @'ConvNotFound mapErrorS @'GuestLinksDisabled @'CodeNotFound $ Query.ensureGuestLinksEnabled (convTeam conv) @@ -761,7 +762,8 @@ joinConversationByReusableCode :: Sem r (UpdateResult Event) joinConversationByReusableCode lusr zcon req = do c <- verifyReusableCode (RateLimitUser (tUnqualified lusr)) True req.password req.code - conv <- E.getConversation (codeConversation c) >>= noteS @'ConvNotFound + codeCid <- noteS @'ConvNotFound (codeConvId c) + conv <- E.getConversation codeCid >>= noteS @'ConvNotFound Query.ensureGuestLinksEnabled (convTeam conv) joinConversation lusr zcon conv CodeAccess From fa035d98b1dfc94982df12080e190d5313fc2380 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 22 Jun 2026 10:09:43 +0200 Subject: [PATCH 2/5] fix: missing line in PostGreSQL schema --- postgres-schema.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/postgres-schema.sql b/postgres-schema.sql index 2f9f0effcea..607aefc83bf 100644 --- a/postgres-schema.sql +++ b/postgres-schema.sql @@ -126,7 +126,8 @@ CREATE TABLE public.conversation_codes ( conversation uuid NOT NULL, password bytea, value text NOT NULL, - expires_at timestamp with time zone NOT NULL + expires_at timestamp with time zone NOT NULL, + target text DEFAULT 'conv'::text NOT NULL ); From c0d0cb0f23815892bac42482b2a810be4b4923c9 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 22 Jun 2026 11:29:16 +0200 Subject: [PATCH 3/5] Update libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs Co-authored-by: Akshay Mankar --- libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs b/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs index 4e8aeda6d52..282397bd61e 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs @@ -63,10 +63,10 @@ insertCode :: (PGConstraints r) => Code -> Maybe Password -> Sem r () insertCode c password = do runStatement (codeKey c, cnv, password, codeValue c, round (codeTTL c), targetTxt) insert where - (cnv, targetTxt) = case codeReferent c of - CodeReferentConv cid -> (cid, "conv") - CodeReferentMeeting mid -> (coerce mid, "meeting") - insert :: Hasql.Statement (Key, ConvId, Maybe Password, Value, Int32, Text) () + (targetId, targetTxt) = case codeReferent c of + CodeReferentConv cid -> (toUUID cid, "conv") + CodeReferentMeeting mid -> (toUUID mid, "meeting") + insert :: Hasql.Statement (Key, UUID, Maybe Password, Value, Int32, Text) () insert = lmapPG [resultlessStatement|INSERT INTO conversation_codes From 19c39bf6c7df5313eff408fee696390929e09cd7 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 22 Jun 2026 11:29:33 +0200 Subject: [PATCH 4/5] Update libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs Co-authored-by: Akshay Mankar --- libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs b/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs index 282397bd61e..161c98cec5f 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs @@ -91,6 +91,11 @@ lookupCode k = do "meeting" -> CodeReferentMeeting (coerce cnv) _ -> CodeReferentConv cnv in (val, ttl, ref, mPw) + mkReferent (val, ttl, targetId, mPw, targetTxt) = + let ref = case targetTxt :: Text of + "meeting" -> CodeReferentMeeting (Id targetId) + _ -> CodeReferentConv cnv + in (val, ttl, ref, mPw) selectCode :: Hasql.Statement Key (Maybe (Value, Int32, ConvId, Maybe Password, Text)) selectCode = dimapPG From 01165ddbf52a413154c1ad62c08ce654a899f934 Mon Sep 17 00:00:00 2001 From: Gautier DI FOLCO Date: Mon, 22 Jun 2026 12:02:13 +0200 Subject: [PATCH 5/5] fix(akshay): merge persistence layer --- .../src/Wire/CodeStore/Cassandra.hs | 14 +++------- .../src/Wire/CodeStore/Code.hs | 28 +++++++++++++++++++ .../src/Wire/CodeStore/DualWrite.hs | 6 ++-- .../src/Wire/CodeStore/Postgres.hs | 26 ++++++----------- 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs b/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs index ddd1f1ab600..61ec6fd8b9d 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/Cassandra.hs @@ -47,14 +47,8 @@ interpretCodeStoreToCassandra = interpret $ \case embedClientInput $ insertCode code mPw DeleteCode k -> do embedClientInput $ deleteCode k - MakeKey ref -> case ref of - CodeReferentConv _ -> Code.mkKey ref - CodeReferentMeeting _ -> - error "CodeStore.Cassandra.MakeKey: meetings are not supported on cassandra" - GenerateCode ref t -> case ref of - CodeReferentConv _ -> Code.generate ref t - CodeReferentMeeting _ -> - error "CodeStore.Cassandra.GenerateCode: meetings are not supported on cassandra" + MakeKey ref -> Code.mkKey ref + GenerateCode ref t -> Code.generate ref t GetConversationCodeURI mbHost -> do convCodeURI <- input case convCodeURI of @@ -73,8 +67,8 @@ insertCode c mPw = do let t = round (codeTTL c) retry x5 (write Cql.insertCode (params LocalQuorum (k, v, cnv, mPw, t))) where - -- Cassandra only stores conversation codes; meeting codes never reach here - -- because 'GenerateCode' errors out for meetings above. + -- Cassandra only stores conversation codes. Meeting codes never reach here: + -- the DualWrite interpreter routes 'CreateCode' for meetings to Postgres only. convReferent (CodeReferentConv cid) = cid convReferent CodeReferentMeeting {} = error "CodeStore.Cassandra.insertCode: meetings are not supported on cassandra" diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Code.hs b/libs/wire-subsystems/src/Wire/CodeStore/Code.hs index e12de4273c3..8e8d0b55881 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/Code.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/Code.hs @@ -20,7 +20,10 @@ module Wire.CodeStore.Code ( Code (..), CodeReferent (..), + CodeTarget (..), codeConvId, + codeTarget, + codeReferentFromTarget, toCode, generate, mkKey, @@ -33,10 +36,12 @@ import Data.Code import Data.Id import Data.Range import Data.Text.Ascii qualified as Ascii +import Data.UUID (UUID) import Imports import OpenSSL.EVP.Digest (digestBS, getDigestByName) import OpenSSL.Random (randBytes) import Wire.API.Password (Password) +import Wire.API.PostgresMarshall -- | The conversation or meeting a 'Code' refers to. Since both 'ConvId' and -- 'MeetingId' are 'Id' values over a 'UUID', they are stored in a single uuid @@ -46,6 +51,29 @@ data CodeReferent | CodeReferentMeeting MeetingId deriving (Eq, Show, Generic) +-- | Database discriminator for 'CodeReferent'. Stored in the @target@ column +-- of @conversation_codes@ to distinguish conversation codes from meeting codes. +data CodeTarget = CodeTargetConv | CodeTargetMeeting + deriving (Eq, Show, Generic) + +codeTarget :: CodeReferent -> CodeTarget +codeTarget CodeReferentConv {} = CodeTargetConv +codeTarget CodeReferentMeeting {} = CodeTargetMeeting + +codeReferentFromTarget :: CodeTarget -> UUID -> CodeReferent +codeReferentFromTarget CodeTargetConv uid = CodeReferentConv (Id uid) +codeReferentFromTarget CodeTargetMeeting uid = CodeReferentMeeting (Id uid) + +instance PostgresMarshall Text CodeTarget where + postgresMarshall CodeTargetConv = "conv" + postgresMarshall CodeTargetMeeting = "meeting" + +instance PostgresUnmarshall Text CodeTarget where + postgresUnmarshall = \case + "conv" -> Right CodeTargetConv + "meeting" -> Right CodeTargetMeeting + other -> Left $ "unexpected code target: " <> other + data Code = Code { codeKey :: !Key, codeValue :: !Value, diff --git a/libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs b/libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs index dba079bd87e..f4721ea0ea6 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/DualWrite.hs @@ -28,7 +28,7 @@ import Polysemy.Input import Wire.CodeStore (CodeStore (..)) import Wire.CodeStore qualified as CodeStore import Wire.CodeStore.Cassandra qualified as Cassandra -import Wire.CodeStore.Code (CodeReferent (..)) +import Wire.CodeStore.Code (CodeReferent (..), codeReferent) import Wire.CodeStore.Postgres qualified as Postgres import Wire.Postgres (PGConstraints) @@ -44,7 +44,9 @@ interpretCodeStoreToCassandraAndPostgres = interpret $ \case GetCode k -> do Cassandra.interpretCodeStoreToCassandra $ CodeStore.getCode k CreateCode code mPw -> do - Cassandra.interpretCodeStoreToCassandra $ CodeStore.createCode code mPw + case codeReferent code of + CodeReferentConv _ -> Cassandra.interpretCodeStoreToCassandra $ CodeStore.createCode code mPw + CodeReferentMeeting _ -> pure () Postgres.interpretCodeStoreToPostgres $ CodeStore.createCode code mPw DeleteCode k -> do Cassandra.interpretCodeStoreToCassandra $ CodeStore.deleteCode k diff --git a/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs b/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs index 161c98cec5f..9f60db88959 100644 --- a/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/CodeStore/Postgres.hs @@ -21,10 +21,10 @@ module Wire.CodeStore.Postgres where import Data.Code -import Data.Coerce (coerce) import Data.Id import Data.Map qualified as Map import Data.Misc (HttpsUrl) +import Data.UUID (UUID) import Hasql.Statement qualified as Hasql import Hasql.TH import Imports @@ -61,12 +61,12 @@ interpretCodeStoreToPostgres = interpret $ \case insertCode :: (PGConstraints r) => Code -> Maybe Password -> Sem r () insertCode c password = do - runStatement (codeKey c, cnv, password, codeValue c, round (codeTTL c), targetTxt) insert + runStatement (codeKey c, targetId, password, codeValue c, round (codeTTL c), codeTarget (codeReferent c)) insert where - (targetId, targetTxt) = case codeReferent c of - CodeReferentConv cid -> (toUUID cid, "conv") - CodeReferentMeeting mid -> (toUUID mid, "meeting") - insert :: Hasql.Statement (Key, UUID, Maybe Password, Value, Int32, Text) () + targetId = case codeReferent c of + CodeReferentConv cid -> toUUID cid + CodeReferentMeeting mid -> toUUID mid + insert :: Hasql.Statement (Key, UUID, Maybe Password, Value, Int32, CodeTarget) () insert = lmapPG [resultlessStatement|INSERT INTO conversation_codes @@ -86,17 +86,9 @@ lookupCode k = do mRow <- runStatement k selectCode pure $ fmap (toCode k . mkReferent) mRow where - mkReferent (val, ttl, cnv, mPw, targetTxt) = - let ref = case targetTxt :: Text of - "meeting" -> CodeReferentMeeting (coerce cnv) - _ -> CodeReferentConv cnv - in (val, ttl, ref, mPw) - mkReferent (val, ttl, targetId, mPw, targetTxt) = - let ref = case targetTxt :: Text of - "meeting" -> CodeReferentMeeting (Id targetId) - _ -> CodeReferentConv cnv - in (val, ttl, ref, mPw) - selectCode :: Hasql.Statement Key (Maybe (Value, Int32, ConvId, Maybe Password, Text)) + mkReferent (val, ttl, targetId, mPw, target) = + (val, ttl, codeReferentFromTarget target targetId, mPw) + selectCode :: Hasql.Statement Key (Maybe (Value, Int32, UUID, Maybe Password, CodeTarget)) selectCode = dimapPG -- on the extraction of the remaining seconds of the TTL