diff --git a/changelog.d/0-release-notes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search b/changelog.d/0-release-notes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search new file mode 100644 index 00000000000..3caefeb19af --- /dev/null +++ b/changelog.d/0-release-notes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search @@ -0,0 +1,29 @@ +`GET /contacts/search` returns apps (and regular users) that collaborate with the searcher's team. + +This means that `brig-index-migrate-data` now requires you to +configure the `elasticsearch-index` chart's postgres setup, and have a +postgres instance reachable with that setup, eg., like this: + +``` +# in (charts/elasticsearch-index/)values.yaml +postgresql: + host: postgresql # DNS name without protocol + port: "5432" + user: wire-server + dbname: wire-server +postgresqlPool: + size: 100 + acquisitionTimeout: 10s + idlenessTimeout: 10m + +postgresMigration: + user: cassandra # (or postgresql, migration-to-postgresql, ...) +``` + +Notes: +- If you have experienced any elasticsearch index update issues since 2026-03-24 (Chart Release 5.29.0), this might be related. If you have not resolved them, consider updating your `values.yaml` now and running a full re-index. +- Note: `brig-index` understands `--user-storage-location`, but that is not relevant here, because collaborators are not technically user accounts, but pointers to user accounts, and they are stored in a different table. + +More info: +- [configuring postgres](https://docs.wire.com/latest/developer/reference/config-options.html?h=config#configure-postgresql) +- [maintaining elastic search](https://docs.wire.com/latest/developer/reference/elastic-search.html?h=elasticsearch) diff --git a/changelog.d/1-api-changes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search b/changelog.d/1-api-changes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search new file mode 100644 index 00000000000..d66924aaeea --- /dev/null +++ b/changelog.d/1-api-changes/WPB-28089-treat-team-collaborators-like-team-members-in-contact-search @@ -0,0 +1 @@ +Treat team collaborators like team members in contact search. diff --git a/integration/test/Test/TeamCollaborators.hs b/integration/test/Test/TeamCollaborators.hs index cf55c3a558e..642dad537e8 100644 --- a/integration/test/Test/TeamCollaborators.hs +++ b/integration/test/Test/TeamCollaborators.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2025 Wire Swiss GmbH @@ -17,6 +19,9 @@ module Test.TeamCollaborators where +import qualified API.Brig as BrigP +import qualified API.BrigInternal as BrigI +import API.Common (randomName) import API.Galley import qualified API.GalleyInternal as Internal import Data.Tuple.Extra @@ -317,3 +322,104 @@ testUpdateCollaborator = do [] >>= assertSuccess postOne2OneConversation bob alice team "chit-chat" >>= assertLabel 403 "operation-denied" + +-- | Collaborators are part of the search space of the team they +-- collaborate with: `GET /search/contacts` returns them to members of +-- that team, just like it returns the team's own members. We test +-- collaborators from other teams, personal user accounts that +-- collaborate, and app. +testSearchFindsCollaborator :: (HasCallStack) => App () +testSearchFindsCollaborator = do + (owner, team, [alice]) <- createTeam OwnDomain 2 + (otherOwner, otherTeam, [bob, collab1]) <- createTeam OwnDomain 3 + collab2 :: Value <- randomUser OwnDomain def + collab3 :: Value <- + BrigP.createApp otherOwner otherTeam def + `bindResponse` \resp -> resp.json %. "user" + + collab1Name <- collab1 %. "name" & asString + collab2Name <- collab2 %. "name" & asString + collab3Name <- collab3 %. "name" & asString + + collab1Name' <- randomName + collab2Name' <- randomName + collab3Name' <- randomName + + -- Find before any collaborations have been established. + let assertFinds :: + (HasCallStack, MakesValue expectFound, MakesValue searcher) => + String -> + expectFound -> + searcher -> + App () + assertFinds searchTerm expectFound searcher = do + BrigI.refreshIndex OwnDomain + BrigP.searchContacts searcher searchTerm OwnDomain `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + foundIds :: [String] <- resp.json %. "documents" >>= asList >>= mapM objId + expectedIds :: [String] <- (make >=> asList >=> mapM objId) expectFound + assertBool + ("found: " <> show foundIds <> "; expected: " <> show expectedIds) + (sort foundIds == sort expectedIds) + + for_ [owner, alice] $ assertFinds collab1Name ([] @Value) + for_ [otherOwner, bob] $ assertFinds collab1Name [collab1] + + for_ [owner, alice] $ assertFinds collab2Name [collab2] + for_ [otherOwner, bob] $ assertFinds collab2Name [collab2] + + for_ [owner, alice] $ assertFinds collab3Name ([] @Value) + for_ [otherOwner, bob] $ assertFinds collab3Name [collab3] + + -- Add collaborators to team + for_ [collab1, collab2, collab3] + $ \collab -> + addTeamCollaborator owner team collab ["implicit_connection"] >>= assertSuccess + + for_ [owner, alice] $ assertFinds collab1Name [collab1] + for_ [otherOwner, bob] $ assertFinds collab1Name [collab1] + + for_ [owner, alice] $ assertFinds collab2Name [collab2] + for_ [otherOwner, bob] $ assertFinds collab2Name [collab2] + + for_ [owner, alice] $ assertFinds collab3Name [collab3] + for_ [otherOwner, bob] $ assertFinds collab3Name [collab3] + + -- Check that updating name does not erase collaborating teams in index. + for_ [(collab1, collab1Name'), (collab2, collab2Name'), (collab3, collab3Name')] + $ \(collab, newName) -> do + let updateBody = (def :: BrigP.PutSelf) {BrigP.name = Just newName} + in BrigP.putSelf collab updateBody >>= assertSuccess + + for_ [owner, alice] $ assertFinds collab1Name' [collab1] + for_ [otherOwner, bob] $ assertFinds collab1Name' [collab1] + + for_ [owner, alice] $ assertFinds collab2Name' [collab2] + for_ [otherOwner, bob] $ assertFinds collab2Name' [collab2] + + for_ [owner, alice] $ assertFinds collab3Name' [collab3] + for_ [otherOwner, bob] $ assertFinds collab3Name' [collab3] + + -- Check that updating collaborating teams does not erase name in index. + for_ [collab1, collab2, collab3] + $ \collab -> do + removeTeamCollaborator owner team collab >>= assertSuccess + + for_ [owner, alice] $ assertFinds collab1Name' ([] @Value) + for_ [otherOwner, bob] $ assertFinds collab1Name' [collab1] + + for_ [owner, alice] $ assertFinds collab2Name' [collab2] + for_ [otherOwner, bob] $ assertFinds collab2Name' [collab2] + + for_ [owner, alice] $ assertFinds collab3Name' ([] @Value) + for_ [otherOwner, bob] $ assertFinds collab3Name' [collab3] + + -- Can one user collaborate in multiple teams without breaking search? + (_thirdOwner, _thirdTeam, [multiCollab]) <- createTeam OwnDomain 2 + multiCollabName <- multiCollab %. "name" & asString + + addTeamCollaborator owner team multiCollab ["implicit_connection"] >>= assertSuccess + addTeamCollaborator otherOwner otherTeam multiCollab ["implicit_connection"] >>= assertSuccess + + for_ [owner, alice] $ assertFinds multiCollabName [multiCollab] + for_ [otherOwner, bob] $ assertFinds multiCollabName [multiCollab] diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Local.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Local.hs new file mode 100644 index 00000000000..d89904b0a8e --- /dev/null +++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Local.hs @@ -0,0 +1,65 @@ +-- 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 . + +-- | Interprets 'BrigAPIAccess' from within brig itself, by calling into the local +-- subsystems directly instead of round-tripping over HTTP to itself (as +-- 'Wire.BrigAPIAccess.Rpc.interpretBrigAccess' does for every other service). +-- +-- Only the operations needed by code shared with other services (e.g. +-- 'Wire.TeamCollaboratorsSubsystem') are implemented locally. Everything else +-- falls back to the RPC handler, pointed at brig itself: correct, but a wasteful +-- round-trip through our own listen socket, so it logs a warning and should be +-- given a local implementation once something actually relies on it. +module Wire.BrigAPIAccess.Local where + +import Imports +import Polysemy +import Polysemy.Error (Error) +import Polysemy.Input (runInputConst) +import Polysemy.TinyLog (TinyLog) +import Polysemy.TinyLog qualified as Log +import System.Logger.Message qualified as Log +import Util.Options (Endpoint) +import Wire.BrigAPIAccess +import Wire.BrigAPIAccess.Rpc (brigAccessRpcHandler) +import Wire.ParseException (ParseException) +import Wire.Rpc (Rpc) +import Wire.RpcException (RpcException) +import Wire.UserSubsystem (UserSubsystem) +import Wire.UserSubsystem qualified as UserSubsystem + +-- | The 'Endpoint' is brig's own; it is only used for the operations that have +-- no local implementation yet. +interpretBrigAPIAccessLocally :: + forall r. + ( Member TinyLog r, + Member Rpc r, + Member (Error ParseException) r, + Member (Error RpcException) r + ) => + Endpoint -> + InterpreterFor UserSubsystem r -> + InterpreterFor BrigAPIAccess r +interpretBrigAPIAccessLocally selfEndpoint runUser = interpret $ \case + UpdateSearchIndex uid -> runUser (UserSubsystem.internalUpdateSearchIndex uid) + other -> selfRpc other + where + selfRpc :: forall m x. BrigAPIAccess m x -> Sem r x + selfRpc action = do + Log.warn $ + Log.msg (Log.val "BrigAPIAccess.Local: no local implementation, calling brig over HTTP") + runInputConst selfEndpoint (brigAccessRpcHandler action) diff --git a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs index e42a4791392..b96d0abeea0 100644 --- a/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs +++ b/libs/wire-subsystems/src/Wire/BrigAPIAccess/Rpc.hs @@ -82,115 +82,131 @@ interpretBrigAccess :: Sem (BrigAPIAccess ': r) a -> Sem r a interpretBrigAccess brigEndpoint = - interpret $ - runInputConst brigEndpoint . \case - GetConnectionsUnqualified uids muids mrel -> do - getConnectionsUnqualified uids muids mrel - GetConnections uids mquids mrel -> do - getConnections uids mquids mrel - PutConnectionInternal uc -> do - putConnectionInternal uc - ReauthUser uid reauth -> do - reAuthUser uid reauth - LookupActivatedUsers uids -> do - lookupActivatedUsers uids - GetUsers uids -> do - getUsers uids - DeleteUser uid -> do - deleteUser uid - GetContactList uid -> do - getContactList uid - GetUserExportData uid -> do - getUserExportData uid - GetSize tid -> do - getSize tid - LookupClients uids -> do - lookupClients uids - LookupClientsFull uids -> do - lookupClientsFull uids - NotifyClientsAboutLegalHoldRequest self other pk -> do - notifyClientsAboutLegalHoldRequest self other pk - GetLegalHoldAuthToken uid mpwd -> do - getLegalHoldAuthToken uid mpwd - AddLegalHoldClientToUserEither uid conn pks lpk -> do - addLegalHoldClientToUser uid conn pks lpk - RemoveLegalHoldClientFromUser uid -> do - removeLegalHoldClientFromUser uid - GetAccountConferenceCallingConfigClient uid -> do - getAccountConferenceCallingConfigClient uid - GetLocalMLSClients qusr ss -> do - getLocalMLSClients qusr ss - GetLocalMLSClient qusr cid ss -> do - getLocalMLSClient qusr cid ss - UpdateSearchVisibilityInbound status -> do - updateSearchVisibilityInbound status - DeleteBot convId botId -> - deleteBot convId botId - UpdateSearchIndex uid -> updateSearchIndex uid - GetAccountsBy localGetBy -> - getAccountsBy localGetBy - GetUsersByVariousKeys uids handles emails includePendingInvitations -> - getUsersByVariousKeys uids handles emails includePendingInvitations - CreateGroupInternal managedBy teamId creatorUserId newGroup -> - createGroupInternal managedBy teamId creatorUserId newGroup - GetGroupsInternal tid mbFilter mbManagedBy startIndex mbCount -> - getGroupsInternal tid mbFilter mbManagedBy startIndex mbCount - GetGroupInternal tid gid includeChannels -> - getGroupInternal tid gid includeChannels - UpdateGroup req -> - updateGroup req - DeleteGroupInternal managedBy teamId groupId -> - deleteGroupInternal managedBy teamId groupId - GetAppIdsForTeam teamId -> - getAppIdsForTeam teamId - SetAccountStatus uid status -> - setAccountStatus uid status - DeleteApp teamId uid -> - deleteApp teamId uid - CreateSAML uref buid teamid name managedBy handle richInfo mLocale role -> - createSAML uref buid teamid name managedBy handle richInfo mLocale role - CreateNoSAML extId email uid teamid uname locale role -> - createNoSAML extId email uid teamid uname locale role - UpdateEmail uid email activation -> - updateEmail uid email activation - GetAccount havePending uid -> - getAccount havePending uid - GetAccountByHandle handle -> - getByHandle handle - GetByEmail email -> - getByEmail email - SetName uid name -> - setName uid name - SetHandle uid handle -> - setHandle uid handle - SetManagedBy uid managedBy -> - setManagedBy uid managedBy - DeletePendingEmailUpdate uid -> - deletePendingEmailUpdate uid - SetSSOId uid ssoId -> - setSSOId uid ssoId - SetRichInfo uid richInfo -> - setRichInfo uid richInfo - SetLocale uid mLocale -> - setLocale uid mLocale - GetRichInfo uid -> - getRichInfo uid - CheckHandleAvailable handle -> - checkHandleAvailable handle - SsoLogin uid mLabel -> - ssoLogin uid mLabel - GetStatus uid -> - getStatus uid - GetStatusMaybe uid -> - getStatusMaybe uid - SetStatus uid status -> - setStatus uid status - GetDefaultUserLocale -> - getDefaultUserLocale - CheckAdminGetTeamId uid -> - checkAdminGetTeamId uid - SendSAMLIdPChangedEmail notif -> - sendSAMLIdPChangedEmail notif + interpret $ runInputConst brigEndpoint . brigAccessRpcHandler + +-- | Handles a single 'BrigAPIAccess' action by calling brig over HTTP. +-- +-- Exposed separately from 'interpretBrigAccess' so that +-- 'Wire.BrigAPIAccess.Local.interpretBrigAPIAccessLocally' can delegate the +-- actions it does not implement itself. 'BrigAPIAccess' is a first-order +-- effect, so @m@ is unconstrained and any handler's action can be passed here. +brigAccessRpcHandler :: + ( Member TinyLog r, + Member Rpc r, + Member (Error ParseException) r, + Member (Error RpcException) r, + Member (Input Endpoint) r + ) => + BrigAPIAccess m a -> + Sem r a +brigAccessRpcHandler = \case + GetConnectionsUnqualified uids muids mrel -> do + getConnectionsUnqualified uids muids mrel + GetConnections uids mquids mrel -> do + getConnections uids mquids mrel + PutConnectionInternal uc -> do + putConnectionInternal uc + ReauthUser uid reauth -> do + reAuthUser uid reauth + LookupActivatedUsers uids -> do + lookupActivatedUsers uids + GetUsers uids -> do + getUsers uids + DeleteUser uid -> do + deleteUser uid + GetContactList uid -> do + getContactList uid + GetUserExportData uid -> do + getUserExportData uid + GetSize tid -> do + getSize tid + LookupClients uids -> do + lookupClients uids + LookupClientsFull uids -> do + lookupClientsFull uids + NotifyClientsAboutLegalHoldRequest self other pk -> do + notifyClientsAboutLegalHoldRequest self other pk + GetLegalHoldAuthToken uid mpwd -> do + getLegalHoldAuthToken uid mpwd + AddLegalHoldClientToUserEither uid conn pks lpk -> do + addLegalHoldClientToUser uid conn pks lpk + RemoveLegalHoldClientFromUser uid -> do + removeLegalHoldClientFromUser uid + GetAccountConferenceCallingConfigClient uid -> do + getAccountConferenceCallingConfigClient uid + GetLocalMLSClients qusr ss -> do + getLocalMLSClients qusr ss + GetLocalMLSClient qusr cid ss -> do + getLocalMLSClient qusr cid ss + UpdateSearchVisibilityInbound status -> do + updateSearchVisibilityInbound status + DeleteBot convId botId -> + deleteBot convId botId + UpdateSearchIndex uid -> updateSearchIndex uid + GetAccountsBy localGetBy -> + getAccountsBy localGetBy + GetUsersByVariousKeys uids handles emails includePendingInvitations -> + getUsersByVariousKeys uids handles emails includePendingInvitations + CreateGroupInternal managedBy teamId creatorUserId newGroup -> + createGroupInternal managedBy teamId creatorUserId newGroup + GetGroupsInternal tid mbFilter mbManagedBy startIndex mbCount -> + getGroupsInternal tid mbFilter mbManagedBy startIndex mbCount + GetGroupInternal tid gid includeChannels -> + getGroupInternal tid gid includeChannels + UpdateGroup req -> + updateGroup req + DeleteGroupInternal managedBy teamId groupId -> + deleteGroupInternal managedBy teamId groupId + GetAppIdsForTeam teamId -> + getAppIdsForTeam teamId + SetAccountStatus uid status -> + setAccountStatus uid status + DeleteApp teamId uid -> + deleteApp teamId uid + CreateSAML uref buid teamid name managedBy handle richInfo mLocale role -> + createSAML uref buid teamid name managedBy handle richInfo mLocale role + CreateNoSAML extId email uid teamid uname locale role -> + createNoSAML extId email uid teamid uname locale role + UpdateEmail uid email activation -> + updateEmail uid email activation + GetAccount havePending uid -> + getAccount havePending uid + GetAccountByHandle handle -> + getByHandle handle + GetByEmail email -> + getByEmail email + SetName uid name -> + setName uid name + SetHandle uid handle -> + setHandle uid handle + SetManagedBy uid managedBy -> + setManagedBy uid managedBy + DeletePendingEmailUpdate uid -> + deletePendingEmailUpdate uid + SetSSOId uid ssoId -> + setSSOId uid ssoId + SetRichInfo uid richInfo -> + setRichInfo uid richInfo + SetLocale uid mLocale -> + setLocale uid mLocale + GetRichInfo uid -> + getRichInfo uid + CheckHandleAvailable handle -> + checkHandleAvailable handle + SsoLogin uid mLabel -> + ssoLogin uid mLabel + GetStatus uid -> + getStatus uid + GetStatusMaybe uid -> + getStatusMaybe uid + SetStatus uid status -> + setStatus uid status + GetDefaultUserLocale -> + getDefaultUserLocale + CheckAdminGetTeamId uid -> + checkAdminGetTeamId uid + SendSAMLIdPChangedEmail notif -> + sendSAMLIdPChangedEmail notif brigRequest :: (Member Rpc r, Member (Input Endpoint) r) => (Request -> Request) -> Sem r (Response (Maybe LByteString)) brigRequest req = do diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs index 6317ed7ba2d..31d77779011 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/Bulk/ElasticSearch.hs @@ -29,6 +29,7 @@ import Data.Conduit.List qualified as CL import Data.Id import Data.Json.Util (UTCTimeMillis (fromUTCTimeMillis)) import Data.Map qualified as Map +import Data.Set qualified as Set import Database.Bloodhound qualified as ES import Imports import Polysemy @@ -37,6 +38,7 @@ import Polysemy.TinyLog import Polysemy.TinyLog qualified as Log import System.Logger.Message qualified as Log import UnliftIO (pooledForConcurrentlyN) +import Wire.API.Team.Collaborator (gTeam, gUser) import Wire.API.Team.Feature import Wire.API.Team.Member.Info import Wire.API.Team.Role @@ -45,6 +47,7 @@ import Wire.IndexedUserStore (IndexedUserStore) import Wire.IndexedUserStore qualified as IndexedUserStore import Wire.IndexedUserStore.MigrationStore import Wire.IndexedUserStore.MigrationStore qualified as MigrationStore +import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore, getTeamCollaborationsForUsers) import Wire.UserSearch.Migration import Wire.UserSearch.Types import Wire.UserStore @@ -54,22 +57,28 @@ type IOInterpreter r = forall a. Sem r a -> IO a -- | Increase this number any time you want to force reindexing. expectedMigrationVersion :: MigrationVersion -expectedMigrationVersion = MigrationVersion 6 +expectedMigrationVersion = MigrationVersion 7 -syncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r) => IOInterpreter r -> Int32 -> IO () +syncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => IOInterpreter r -> Int32 -> IO Int syncAllUsers interpreter pageSize = syncAllUsersWithVersion interpreter pageSize ES.ExternalGT -forceSyncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r) => IOInterpreter r -> Int32 -> IO () +forceSyncAllUsers :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => IOInterpreter r -> Int32 -> IO Int forceSyncAllUsers interpreter pageSize = syncAllUsersWithVersion interpreter pageSize ES.ExternalGTE -syncAllUsersWithVersion :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r) => IOInterpreter r -> Int32 -> (ES.ExternalDocVersion -> ES.VersionControl) -> IO () +-- | Returns the number of users that could not be indexed because some of the +-- data needed to build their document was unavailable. Those users have been +-- logged individually by 'logAndHush'. +syncAllUsersWithVersion :: (Member UserStore r, Member IndexedUserStore r, Member TinyLog r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => IOInterpreter r -> Int32 -> (ES.ExternalDocVersion -> ES.VersionControl) -> IO Int syncAllUsersWithVersion interpreter pageSize mkVersion = - runConduit $ + fmap getSum . runConduit $ zipSources (CL.sourceList [1 ..]) (paginateWithStateC (interpreter . getIndexUsersPaginated pageSize)) .| logPage .| mkUserDocs - .| Conduit.mapM_ (interpreter . IndexedUserStore.bulkUpsert) + .| Conduit.foldMapM upsertPage where + upsertPage :: (Int, [(ES.DocId, UserDoc, ES.VersionControl)]) -> IO (Sum Int) + upsertPage (skipped, docs) = Sum skipped <$ interpreter (IndexedUserStore.bulkUpsert docs) + logPage :: ConduitT (Int32, [IndexUser]) [IndexUser] IO () logPage = Conduit.mapM $ \(pageNumber, page) -> do interpreter $ @@ -79,7 +88,9 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = . Log.field "firstUser" (maybe "N/A" (idToText . (.userId)) (headMay page)) pure page - mkUserDocs :: ConduitT [IndexUser] [(ES.DocId, UserDoc, ES.VersionControl)] IO () + -- Emits the documents to be indexed together with the number of users of + -- this page that had to be skipped. + mkUserDocs :: ConduitT [IndexUser] (Int, [(ES.DocId, UserDoc, ES.VersionControl)]) IO () mkUserDocs = Conduit.mapM $ \page -> do -- FUTUREWORK: extract team visibilities, roles and user type -- more efficiently sending one query per page @@ -88,10 +99,10 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = -- 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]) <$> u.teamId) page - teamIds = Map.keys teams + let teams :: Map TeamId [IndexUser] + teams = Map.fromListWith (<>) $ mapMaybe (\u -> (,[u]) <$> u.teamId) page - visMap <- fmap Map.fromList . pooledForConcurrentlyN 16 teamIds $ \t -> do + visMap <- fmap Map.fromList . pooledForConcurrentlyN 16 (Map.keys teams) $ \t -> do x <- try $ interpreter $ teamSearchVisibilityInbound t pure (t, x) @@ -114,6 +125,12 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = fmap Map.unions . pooledForConcurrentlyN 16 (Map.toList teams) $ \(t, us) -> getRoles t (fmap (.userId) us) + -- One query for the whole page. A failure here fails every document of the + -- page, which 'logAndHush' then logs and skips. + eithCollabTeams :: Either SomeException (Map UserId [TeamId]) <- + try . fmap (Map.fromListWith (<>) . map (\tc -> (gUser tc, [gTeam tc]))) . interpreter $ + getTeamCollaborationsForUsers (Set.fromList (map (.userId) page)) + let vis :: IndexUser -> Either SomeException SearchVisibilityInbound vis indexUser = fromMaybe (Right defaultSearchVisibilityInbound) $ flip Map.lookup visMap =<< indexUser.teamId @@ -122,15 +139,20 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = mkUserDoc indexUser = do currentVis <- vis indexUser currentRole <- sequence $ Map.lookup indexUser.userId roles - pure $ indexUserToDoc currentVis ((.value) <$> currentRole) indexUser + currentCollabTeams <- Map.findWithDefault [] indexUser.userId <$> eithCollabTeams + pure $ indexUserToDoc currentVis ((.value) <$> currentRole) currentCollabTeams indexUser mkDocVersion :: IndexUser -> Either SomeException ES.VersionControl mkDocVersion u = do roleWithTime <- sequence (Map.lookup u.userId roles) pure . mkVersion . ES.ExternalDocVersion . docVersion $ indexUserToVersion roleWithTime u - let docsWithErrors = map (\u -> (userIdToDocId u.userId, mkUserDoc u, mkDocVersion u)) page - interpreter . flip mapMaybeM docsWithErrors $ logAndHush + docsWithErrors :: (e ~ Either SomeException) => [(ES.DocId, e UserDoc, e ES.VersionControl)] + docsWithErrors = map (\u -> (userIdToDocId u.userId, mkUserDoc u, mkDocVersion u)) page + + docs <- interpreter . flip mapMaybeM docsWithErrors $ logAndHush + let skipped = length docsWithErrors - length docs + pure (skipped, docs) rightSecond :: (a, b) -> (a, Either c b) rightSecond (a, b) = (a, Right b) @@ -159,7 +181,7 @@ syncAllUsersWithVersion interpreter pageSize mkVersion = <$> permissionsToRole tmi.permissions migrateData :: - (Member (Embed IO) r, Member IndexedUserStore r, Member (Error MigrationException) r, Member IndexedUserMigrationStore r, Member TinyLog r, Member UserStore r, Member GalleyAPIAccess r) => + (Member (Embed IO) r, Member IndexedUserStore r, Member (Error MigrationException) r, Member IndexedUserMigrationStore r, Member TinyLog r, Member UserStore r, Member GalleyAPIAccess r, Member TeamCollaboratorsStore r) => IOInterpreter r -> Int32 -> IO () @@ -174,8 +196,15 @@ migrateData interpreter pageSize = interpreter $ do Log.msg (Log.val "Migration necessary.") . Log.field "expectedVersion" expectedMigrationVersion . Log.field "foundVersion" foundVersion - embed $ forceSyncAllUsers interpreter pageSize - MigrationStore.persistMigrationVersion expectedMigrationVersion + skipped <- embed $ forceSyncAllUsers interpreter pageSize + if skipped == 0 + then MigrationStore.persistMigrationVersion expectedMigrationVersion + else do + Log.err $ + Log.msg (Log.val "Migration incomplete, not persisting migration version.") + . Log.field "expectedVersion" expectedMigrationVersion + . Log.field "skippedUsers" skipped + throw $ SyncIncomplete else do Log.info $ Log.msg (Log.val "No migration necessary.") diff --git a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs index 156f8f6e479..07572a29851 100644 --- a/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs +++ b/libs/wire-subsystems/src/Wire/IndexedUserStore/ElasticSearch.hs @@ -526,13 +526,17 @@ matchSelf :: UserId -> Maybe ES.Query matchSelf searcher = Just (termQ "_id" (idToText searcher)) -- | Exclude apps from other teams. --- Apps should only be searchable within their own team. +-- Apps should only be searchable within their own team, or within a team they +-- collaborate with. matchAppsFromOtherTeams :: Maybe TeamId -> Maybe ES.Query matchAppsFromOtherTeams mSearcherTeamId = Just $ ES.QueryBoolQuery boolQuery - { ES.boolQueryMustMatch = + { -- Apps collaborating with the searcher's team are not excluded. + ES.boolQueryMustNotMatch = + maybeToList (termQ "collaborating_teams" . idToText <$> mSearcherTeamId), + ES.boolQueryMustMatch = [ -- Match apps (type = "app") termQ "type" "app", -- That are from a different team than the searcher @@ -640,7 +644,16 @@ restrictSearchSpaceByUserType = \case else ES.TermsQuery "type" (userTypeFilterToText <$> (utsH :| utsT)) matchTeamMembersOf :: TeamId -> ES.Query -matchTeamMembersOf team = ES.TermQuery (ES.Term "team" $ idToText team) Nothing +matchTeamMembersOf team = + ES.QueryBoolQuery + boolQuery + { ES.boolQueryShouldMatch = + [ -- Match users who are members of the team + ES.TermQuery (ES.Term "team" $ idToText team) Nothing, + -- Match users who are collaborators in the team + ES.TermQuery (ES.Term "collaborating_teams" $ idToText team) Nothing + ] + } matchTeamMembersSearchableByAllTeams :: ES.Query matchTeamMembersSearchableByAllTeams = diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs index fcdf0731b08..ebf79c96721 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore.hs @@ -29,6 +29,8 @@ data TeamCollaboratorsStore m a where GetAllTeamCollaborators :: TeamId -> TeamCollaboratorsStore m [TeamCollaborator] GetTeamCollaborator :: TeamId -> UserId -> TeamCollaboratorsStore m (Maybe TeamCollaborator) GetTeamCollaborations :: UserId -> TeamCollaboratorsStore m ([TeamCollaborator]) + -- | Batched 'GetTeamCollaborations', for callers that process users in pages. + GetTeamCollaborationsForUsers :: Set UserId -> TeamCollaboratorsStore m [TeamCollaborator] GetTeamCollaboratorsWithIds :: Set TeamId -> Set UserId -> TeamCollaboratorsStore m [TeamCollaborator] UpdateTeamCollaborator :: UserId -> TeamId -> Set CollaboratorPermission -> TeamCollaboratorsStore m () RemoveTeamCollaborator :: UserId -> TeamId -> TeamCollaboratorsStore m () diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs index a6a1e968a72..b898ae69b51 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsStore/Postgres.hs @@ -49,6 +49,7 @@ interpretTeamCollaboratorsStoreToPostgres = GetAllTeamCollaborators teamId -> getAllTeamCollaboratorsImpl teamId GetTeamCollaborator teamId userId -> getTeamCollaboratorImpl teamId userId GetTeamCollaborations userId -> getTeamCollaborationsImpl userId + GetTeamCollaborationsForUsers userIds -> getTeamCollaborationsForUsersImpl userIds GetTeamCollaboratorsWithIds teamIds userIds -> getTeamCollaboratorsWithIdsImpl teamIds userIds UpdateTeamCollaborator userId teamId permissions -> updateTeamCollaboratorImpl userId teamId permissions RemoveTeamCollaborator userId teamId -> removeTeamCollaboratorImpl userId teamId @@ -181,6 +182,22 @@ getTeamCollaborationsImpl teamId = do select user_id :: uuid, team_id :: uuid, permissions :: int2[] from collaborators where user_id = ($1 :: uuid) |] +getTeamCollaborationsForUsersImpl :: + (PGConstraints r) => + Set UserId -> + Sem r [TeamCollaborator] +getTeamCollaborationsForUsersImpl userIds = do + runStatement (Data.Set.toList userIds) getAllCollaborationsByUsersStatement + where + getAllCollaborationsByUsersStatement :: Statement [UserId] [TeamCollaborator] + getAllCollaborationsByUsersStatement = + dimap + (Data.Vector.fromList . Imports.map toUUID) + (Data.Vector.toList . (toTeamCollaborator <$>)) + $ [vectorStatement| + select user_id :: uuid, team_id :: uuid, permissions :: int2[] from collaborators where user_id = ANY($1 :: uuid[]) + |] + getTeamCollaboratorsWithIdsImpl :: (PGConstraints r) => Set TeamId -> diff --git a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs index 30a970706e3..bb0541636b0 100644 --- a/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/TeamCollaboratorsSubsystem/Interpreter.hs @@ -29,6 +29,8 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Event.Team import Wire.API.Team.Collaborator import Wire.API.Team.Member qualified as TeamMember +import Wire.BrigAPIAccess (BrigAPIAccess) +import Wire.BrigAPIAccess qualified as BrigAPIAccess import Wire.Error import Wire.NotificationSubsystem import Wire.Sem.Now @@ -44,15 +46,18 @@ interpretTeamCollaboratorsSubsystem :: Member Now r, Member NotificationSubsystem r ) => + InterpreterFor BrigAPIAccess r -> InterpreterFor TeamCollaboratorsSubsystem r -interpretTeamCollaboratorsSubsystem = interpret $ \case - CreateTeamCollaborator zUser user team perms -> createTeamCollaboratorImpl zUser user team perms - GetAllTeamCollaborators zUser team -> getAllTeamCollaboratorsImpl zUser team - InternalGetTeamCollaborator team user -> internalGetTeamCollaboratorImpl team user - InternalGetTeamCollaborations userId -> internalGetTeamCollaborationsImpl userId - InternalGetTeamCollaboratorsWithIds teams userIds -> internalGetTeamCollaboratorsWithIdsImpl teams userIds - InternalUpdateTeamCollaborator user team perms -> internalUpdateTeamCollaboratorImpl user team perms - InternalRemoveTeamCollaborator user team -> internalRemoveTeamCollaboratorImpl user team +interpretTeamCollaboratorsSubsystem brigAPIAccess = + interpret $ + brigAPIAccess . \case + CreateTeamCollaborator zUser user team perms -> createTeamCollaboratorImpl zUser user team perms + GetAllTeamCollaborators zUser team -> getAllTeamCollaboratorsImpl zUser team + InternalGetTeamCollaborator team user -> internalGetTeamCollaboratorImpl team user + InternalGetTeamCollaborations userId -> internalGetTeamCollaborationsImpl userId + InternalGetTeamCollaboratorsWithIds teams userIds -> internalGetTeamCollaboratorsWithIdsImpl teams userIds + InternalUpdateTeamCollaborator user team perms -> internalUpdateTeamCollaboratorImpl user team perms + InternalRemoveTeamCollaborator user team -> internalRemoveTeamCollaboratorImpl user team internalGetTeamCollaboratorImpl :: (Member Store.TeamCollaboratorsStore r) => @@ -74,7 +79,8 @@ createTeamCollaboratorImpl :: Member (Error TeamCollaboratorsError) r, Member Store.TeamCollaboratorsStore r, Member Now r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member BrigAPIAccess r ) => Local UserId -> UserId -> @@ -85,9 +91,11 @@ createTeamCollaboratorImpl zUser user team perms = do guardPermission (tUnqualified zUser) team TeamMember.GetTeamCollaborators InsufficientRights Store.createTeamCollaborator user team perms - -- TODO: Review the event's values generateTeamEvents (tUnqualified zUser) team [EdCollaboratorAdd user (Set.toList perms)] + -- Reindex the collaborator with their new collaboration team + BrigAPIAccess.updateSearchIndex user + getAllTeamCollaboratorsImpl :: ( Member TeamSubsystem r, Member (Error TeamCollaboratorsError) r, @@ -109,21 +117,25 @@ internalGetTeamCollaboratorsWithIdsImpl = do Store.getTeamCollaboratorsWithIds internalUpdateTeamCollaboratorImpl :: - (Member Store.TeamCollaboratorsStore r) => + (Member Store.TeamCollaboratorsStore r, Member BrigAPIAccess r) => UserId -> TeamId -> Set CollaboratorPermission -> Sem r () internalUpdateTeamCollaboratorImpl user team perms = do Store.updateTeamCollaborator user team perms + -- Reindex collaborator when permissions change + BrigAPIAccess.updateSearchIndex user internalRemoveTeamCollaboratorImpl :: - (Member Store.TeamCollaboratorsStore r) => + (Member Store.TeamCollaboratorsStore r, Member BrigAPIAccess r) => UserId -> TeamId -> Sem r () internalRemoveTeamCollaboratorImpl user team = do Store.removeTeamCollaborator user team + -- Reindex collaborator when removed + BrigAPIAccess.updateSearchIndex user -- This is of general usefulness. However, we cannot move this to wire-api as -- this would lead to a cyclic dependency. diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs b/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs index 817d10370e9..7cbbbcd734e 100644 --- a/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs +++ b/libs/wire-subsystems/src/Wire/UserSearch/Migration.hs @@ -42,6 +42,7 @@ data MigrationException | PutMappingFailed String | TargetIndexAbsent | VersionSourceMissing (ES.SearchResult MigrationVersion) + | SyncIncomplete deriving (Show) instance Exception MigrationException diff --git a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs index 5e8dcac765e..5464dae2a8f 100644 --- a/libs/wire-subsystems/src/Wire/UserSearch/Types.hs +++ b/libs/wire-subsystems/src/Wire/UserSearch/Types.hs @@ -79,7 +79,10 @@ data UserDoc = UserDoc udScimExternalId :: Maybe Text, udSso :: Maybe Sso, udEmailUnvalidated :: Maybe EmailAddress, - udSearchable :: Maybe Bool + udSearchable :: Maybe Bool, + -- | Teams that have added this user as a collaborator. + -- Updated separately via 'syncUserIndexCollaborations' when collaborator relationships change. + udCollaboratingTeams :: [TeamId] } deriving (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserDoc) @@ -104,7 +107,8 @@ instance ToJSON UserDoc where "scim_external_id" .= udScimExternalId ud, "sso" .= udSso ud, "email_unvalidated" .= udEmailUnvalidated ud, - "searchable" .= udSearchable ud + "searchable" .= udSearchable ud, + "collaborating_teams" .= udCollaboratingTeams ud ] instance FromJSON UserDoc where @@ -128,6 +132,7 @@ instance FromJSON UserDoc where <*> o .:? "sso" <*> o .:? "email_unvalidated" <*> o .:? "searchable" + <*> o .:? "collaborating_teams" .!= [] searchVisibilityInboundFieldName :: Key searchVisibilityInboundFieldName = "search_visibility_inbound" diff --git a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs index 09ac630d191..b051132f1ed 100644 --- a/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs +++ b/libs/wire-subsystems/src/Wire/UserStore/IndexUser.hs @@ -126,8 +126,8 @@ indexUserToVersion :: Maybe (WithWritetime Role) -> IndexUser -> IndexVersion indexUserToVersion role iu = mkIndexVersion [Just $ Writetime iu.updatedAt, const () <$$> fmap writetime role] -indexUserToDoc :: SearchVisibilityInbound -> Maybe Role -> IndexUser -> UserDoc -indexUserToDoc searchVisInbound mRole IndexUser {..} = +indexUserToDoc :: SearchVisibilityInbound -> Maybe Role -> [TeamId] -> IndexUser -> UserDoc +indexUserToDoc searchVisInbound mRole collaboratingTeams IndexUser {..} = if shouldIndex then UserDoc @@ -148,7 +148,8 @@ indexUserToDoc searchVisInbound mRole IndexUser {..} = udHandle = handle, udNormalized = Just $ normalized name.fromName, udName = Just name, - udTeam = teamId + udTeam = teamId, + udCollaboratingTeams = collaboratingTeams } 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 @@ -209,5 +210,6 @@ emptyUserDoc uid = udNormalized = Nothing, udName = Nothing, udTeam = Nothing, + udCollaboratingTeams = [], udId = uid } diff --git a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs index d5cb2dfee62..f78029cebde 100644 --- a/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs +++ b/libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs @@ -60,6 +60,7 @@ import Wire.API.Federation.Error import Wire.API.MLS.CipherSuite (CipherSuiteTag, csSignatureScheme) import Wire.API.Routes.FederationDomainConfig import Wire.API.Routes.Internal.Galley.TeamFeatureNoConfigMulti (TeamStatus (..)) +import Wire.API.Team.Collaborator (gTeam) import Wire.API.Team.Export import Wire.API.Team.Feature import Wire.API.Team.Member @@ -102,6 +103,8 @@ import Wire.Sem.Metrics qualified as Metrics import Wire.Sem.Now (Now) import Wire.Sem.Now qualified as Now import Wire.StoredUser +import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore) +import Wire.TeamCollaboratorsStore qualified as TeamCollaboratorsStore import Wire.TeamSubsystem import Wire.UserGroupStore (UserGroupStore, getUserGroupIdsForUsers) import Wire.UserKeyStore @@ -141,6 +144,7 @@ runUserSubsystem :: Member TinyLog r, Member (Input UserSubsystemConfig) r, Member TeamSubsystem r, + Member TeamCollaboratorsStore r, Member UserGroupStore r, Member (Input (Local any)) r ) => @@ -711,6 +715,7 @@ updateUserProfileImpl :: Member Events r, Member GalleyAPIAccess r, Member IndexedUserStore r, + Member TeamCollaboratorsStore r, Member Metrics r ) => Local UserId -> @@ -772,6 +777,7 @@ updateHandleImpl :: Member Events r, Member UserStore r, Member IndexedUserStore r, + Member TeamCollaboratorsStore r, Member Metrics r ) => Local UserId -> @@ -839,7 +845,8 @@ syncUserIndex :: ( Member UserStore r, Member GalleyAPIAccess r, Member IndexedUserStore r, - Member Metrics r + Member Metrics r, + Member TeamCollaboratorsStore r ) => UserId -> Sem r () @@ -860,9 +867,13 @@ syncUserIndex uid = teamSearchVisibilityInbound indexUser.teamId tm <- maybe (pure Nothing) selectTeamMember indexUser.teamId + collabTeams <- map gTeam <$> TeamCollaboratorsStore.getTeamCollaborations uid let mRole = tm >>= mkRoleWithWriteTime - userDoc = indexUserToDoc vis (value <$> mRole) indexUser - version = ES.ExternalGT . ES.ExternalDocVersion . docVersion $ indexUserToVersion mRole indexUser + userDoc = indexUserToDoc vis (value <$> mRole) collabTeams indexUser + -- GTE, not GT: the version comes from the user row alone, but the document also + -- holds data that changes without touching that row (collaborations), and under + -- GT those updates would be dropped as version conflicts. Older writes still lose. + version = ES.ExternalGTE . ES.ExternalDocVersion . docVersion $ indexUserToVersion mRole indexUser Metrics.incCounter indexUpdateCounter IndexedUserStore.upsert (userIdToDocId uid) userDoc version @@ -1180,6 +1191,7 @@ acceptTeamInvitationImpl :: Member (Error UserSubsystemError) r, Member InvitationStore r, Member IndexedUserStore r, + Member TeamCollaboratorsStore r, Member Metrics r, Member Events r, Member AuthenticationSubsystem r, @@ -1244,6 +1256,7 @@ removeEmailEitherImpl :: Member UserStore r, Member Events r, Member IndexedUserStore r, + Member TeamCollaboratorsStore r, Member (Input UserSubsystemConfig) r, Member GalleyAPIAccess r, Member Metrics r @@ -1280,6 +1293,7 @@ setUserSearchableImpl :: Member TeamSubsystem r, Member GalleyAPIAccess r, Member IndexedUserStore r, + Member TeamCollaboratorsStore r, Member Metrics r ) => Local UserId -> diff --git a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs index 1324d919db3..ff27a27fa7f 100644 --- a/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs +++ b/libs/wire-subsystems/test/unit/Wire/MiniBackend.hs @@ -365,22 +365,21 @@ miniBackendLowerEffectsInterpreters mb@(MiniBackendParams {..}) = . runInputConst conversationCfg . runClientSubsystem undefined undefined where - -- Mock BrigAPIAccess interpreter for tests - mockBrigAPIAccess :: forall r'. InterpreterFor BrigAPIAccess r' - mockBrigAPIAccess = interpret $ \case - _ -> error "Unimplemented BrigAPIAccess operation in mock" -- Mock UserClientIndexStore interpreter for tests mockUserClientIndexStore :: forall r'. InterpreterFor UserClientIndexStore r' mockUserClientIndexStore = interpret $ \case _ -> error "Unimplemented UserClientIndexStore operation in mock" + -- Mock BackendNotificationQueueAccess interpreter for tests mockBackendNotificationQueueAccess :: forall r'. InterpreterFor BackendNotificationQueueAccess r' mockBackendNotificationQueueAccess = interpret $ \case _ -> error "Unimplemented BackendNotificationQueueAccess operation in mock" + -- Mock ConversationSubsystem interpreter for tests mockConversationSubsystem :: forall r'. InterpreterFor ConversationSubsystem r' mockConversationSubsystem = interpretH $ \case _ -> error "Unimplemented ConversationSubsystem operation in mock" + mockMlsKeyPackageSubsystem :: forall r'. InterpreterFor MlsKeyPackageSubsystem r' mockMlsKeyPackageSubsystem = interpret $ \case HasMlsKeyPackages {} -> pure False @@ -786,7 +785,7 @@ interpretMaybeFederationStackState :: Sem (MiniBackendEffects `Append` r) a -> Sem r (MiniBackend, a) interpretMaybeFederationStackState mb = - miniBackendLowerEffectsInterpreters mb . interpretTeamCollaboratorsSubsystem . runRecursiveAuthUserApp + miniBackendLowerEffectsInterpreters mb . interpretTeamCollaboratorsSubsystem subsume . runRecursiveAuthUserApp -- FUTUREWORK(fisx): it would be nice to have a definition of an -- interpreter of all the subsystems combined, but since the diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs index 4630c0c7f77..ea57140aad5 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters.hs @@ -25,6 +25,7 @@ import Wire.MockInterpreters.AppStore as MockInterpreters import Wire.MockInterpreters.AuthenticationSubsystem as MockInterpreters import Wire.MockInterpreters.BackgroundJobPublisher as MockInterpreters import Wire.MockInterpreters.BlockListStore as MockInterpreters +import Wire.MockInterpreters.BrigAPIAccess as MockInterpreters import Wire.MockInterpreters.ClientStore as MockInterpreters import Wire.MockInterpreters.ConversationStore as MockInterpreters import Wire.MockInterpreters.ConversationSubsystem as MockInterpreters diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BrigAPIAccess.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BrigAPIAccess.hs new file mode 100644 index 00000000000..aeb76299093 --- /dev/null +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/BrigAPIAccess.hs @@ -0,0 +1,81 @@ +-- 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.MockInterpreters.BrigAPIAccess where + +import Imports +import Polysemy +import Wire.BrigAPIAccess + +-- | Errors out on everything except 'UpdateSearchIndex', which is a no-op. +mockBrigAPIAccess :: InterpreterFor BrigAPIAccess r +mockBrigAPIAccess = interpret $ \case + UpdateSearchIndex _ -> pure () + -- everything else is not implemented + GetConnectionsUnqualified {} -> error "GetConnectionsUnqualified: implement on demand (mockBrigAPIAccess)" + GetConnections {} -> error "GetConnections: implement on demand (mockBrigAPIAccess)" + PutConnectionInternal {} -> error "PutConnectionInternal: implement on demand (mockBrigAPIAccess)" + ReauthUser {} -> error "ReauthUser: implement on demand (mockBrigAPIAccess)" + LookupActivatedUsers {} -> error "LookupActivatedUsers: implement on demand (mockBrigAPIAccess)" + GetUsers {} -> error "GetUsers: implement on demand (mockBrigAPIAccess)" + DeleteUser {} -> error "DeleteUser: implement on demand (mockBrigAPIAccess)" + GetContactList {} -> error "GetContactList: implement on demand (mockBrigAPIAccess)" + GetSize {} -> error "GetSize: implement on demand (mockBrigAPIAccess)" + LookupClients {} -> error "LookupClients: implement on demand (mockBrigAPIAccess)" + LookupClientsFull {} -> error "LookupClientsFull: implement on demand (mockBrigAPIAccess)" + NotifyClientsAboutLegalHoldRequest {} -> error "NotifyClientsAboutLegalHoldRequest: implement on demand (mockBrigAPIAccess)" + GetLegalHoldAuthToken {} -> error "GetLegalHoldAuthToken: implement on demand (mockBrigAPIAccess)" + AddLegalHoldClientToUserEither {} -> error "AddLegalHoldClientToUserEither: implement on demand (mockBrigAPIAccess)" + RemoveLegalHoldClientFromUser {} -> error "RemoveLegalHoldClientFromUser: implement on demand (mockBrigAPIAccess)" + GetAccountConferenceCallingConfigClient {} -> error "GetAccountConferenceCallingConfigClient: implement on demand (mockBrigAPIAccess)" + GetLocalMLSClients {} -> error "GetLocalMLSClients: implement on demand (mockBrigAPIAccess)" + GetLocalMLSClient {} -> error "GetLocalMLSClient: implement on demand (mockBrigAPIAccess)" + UpdateSearchVisibilityInbound {} -> error "UpdateSearchVisibilityInbound: implement on demand (mockBrigAPIAccess)" + GetUserExportData {} -> error "GetUserExportData: implement on demand (mockBrigAPIAccess)" + DeleteBot {} -> error "DeleteBot: implement on demand (mockBrigAPIAccess)" + GetAccountsBy {} -> error "GetAccountsBy: implement on demand (mockBrigAPIAccess)" + GetUsersByVariousKeys {} -> error "GetUsersByVariousKeys: implement on demand (mockBrigAPIAccess)" + CreateGroupInternal {} -> error "CreateGroupInternal: implement on demand (mockBrigAPIAccess)" + GetGroupInternal {} -> error "GetGroupInternal: implement on demand (mockBrigAPIAccess)" + GetGroupsInternal {} -> error "GetGroupsInternal: implement on demand (mockBrigAPIAccess)" + UpdateGroup {} -> error "UpdateGroup: implement on demand (mockBrigAPIAccess)" + DeleteGroupInternal {} -> error "DeleteGroupInternal: implement on demand (mockBrigAPIAccess)" + DeleteApp {} -> error "DeleteApp: implement on demand (mockBrigAPIAccess)" + GetAppIdsForTeam {} -> error "GetAppIdsForTeam: implement on demand (mockBrigAPIAccess)" + SetAccountStatus {} -> error "SetAccountStatus: implement on demand (mockBrigAPIAccess)" + CreateSAML {} -> error "CreateSAML: implement on demand (mockBrigAPIAccess)" + CreateNoSAML {} -> error "CreateNoSAML: implement on demand (mockBrigAPIAccess)" + UpdateEmail {} -> error "UpdateEmail: implement on demand (mockBrigAPIAccess)" + GetAccount {} -> error "GetAccount: implement on demand (mockBrigAPIAccess)" + GetAccountByHandle {} -> error "GetAccountByHandle: implement on demand (mockBrigAPIAccess)" + GetByEmail {} -> error "GetByEmail: implement on demand (mockBrigAPIAccess)" + SetName {} -> error "SetName: implement on demand (mockBrigAPIAccess)" + SetHandle {} -> error "SetHandle: implement on demand (mockBrigAPIAccess)" + SetManagedBy {} -> error "SetManagedBy: implement on demand (mockBrigAPIAccess)" + DeletePendingEmailUpdate {} -> error "DeletePendingEmailUpdate: implement on demand (mockBrigAPIAccess)" + SetSSOId {} -> error "SetSSOId: implement on demand (mockBrigAPIAccess)" + SetRichInfo {} -> error "SetRichInfo: implement on demand (mockBrigAPIAccess)" + SetLocale {} -> error "SetLocale: implement on demand (mockBrigAPIAccess)" + GetRichInfo {} -> error "GetRichInfo: implement on demand (mockBrigAPIAccess)" + CheckHandleAvailable {} -> error "CheckHandleAvailable: implement on demand (mockBrigAPIAccess)" + SsoLogin {} -> error "SsoLogin: implement on demand (mockBrigAPIAccess)" + GetStatus {} -> error "GetStatus: implement on demand (mockBrigAPIAccess)" + GetStatusMaybe {} -> error "GetStatusMaybe: implement on demand (mockBrigAPIAccess)" + SetStatus {} -> error "SetStatus: implement on demand (mockBrigAPIAccess)" + GetDefaultUserLocale {} -> error "GetDefaultUserLocale: implement on demand (mockBrigAPIAccess)" + CheckAdminGetTeamId {} -> error "CheckAdminGetTeamId: implement on demand (mockBrigAPIAccess)" + SendSAMLIdPChangedEmail {} -> error "SendSAMLIdPChangedEmail: implement on demand (mockBrigAPIAccess)" diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs index 4def51eeef7..63a334527ab 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/TeamCollaboratorsStore.hs @@ -41,6 +41,8 @@ inMemoryTeamCollaboratorsStoreInterpreter = gets $ \(s :: Map TeamId [TeamCollaborator]) -> find (\tc -> tc.gUser == userId) =<< Map.lookup teamId s GetTeamCollaborations userId -> gets $ \(s :: Map TeamId [TeamCollaborator]) -> concatMap (filter (\tc -> tc.gUser == userId)) (Map.elems s) + GetTeamCollaborationsForUsers userIds -> + gets $ \(s :: Map TeamId [TeamCollaborator]) -> concatMap (filter (\tc -> tc.gUser `elem` userIds)) (Map.elems s) GetTeamCollaboratorsWithIds teamIds userIds -> gets $ \(s :: Map TeamId [TeamCollaborator]) -> concatMap (concatMap (filter (\tc -> tc.gUser `elem` userIds)) . (\(tid :: TeamId) -> Map.lookup tid s)) teamIds diff --git a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs index 786f733240f..043f3a834db 100644 --- a/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs +++ b/libs/wire-subsystems/test/unit/Wire/MockInterpreters/UserSubsystem.hs @@ -87,7 +87,7 @@ inMemoryUserSubsystemInterpreter = BlockListInsert _ -> error "BlockListInsert: implement on demand (userSubsystemInterpreter)" UpdateTeamSearchVisibilityInbound _ -> error "UpdateTeamSearchVisibilityInbound: implement on demand (userSubsystemInterpreter)" AcceptTeamInvitation {} -> error "AcceptTeamInvitation: implement on demand (userSubsystemInterpreter)" - InternalUpdateSearchIndex _ -> error "InternalUpdateSearchIndex: implement on demand (userSubsystemInterpreter)" + InternalUpdateSearchIndex {} -> error "InternalUpdateSearchIndex: implement on demand (userSubsystemInterpreter)" InternalFindTeamInvitation {} -> error "InternalFindTeamInvitation: implement on demand (userSubsystemInterpreter)" GetUserExportData _ -> error "GetUserExportData: implement on demand (userSubsystemInterpreter)" RemoveEmailEither _ -> error "RemoveEmailEither: implement on demand (userSubsystemInterpreter)" diff --git a/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs index 6861f097797..f623e0012dd 100644 --- a/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/ScimSubsystem/InterpreterSpec.hs @@ -42,7 +42,7 @@ import Wire.API.User as User import Wire.API.User.Scim import Wire.API.UserGroup import Wire.BrigAPIAccess (BrigAPIAccess (..)) -import Wire.MockInterpreters +import Wire.MockInterpreters hiding (mockBrigAPIAccess) import Wire.ScimSubsystem import Wire.ScimSubsystem.Interpreter import Wire.StoredUser diff --git a/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs index a09d56bd8ff..ea721887393 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSearch/TypesSpec.hs @@ -50,6 +50,7 @@ userDoc1 = UserDoc { udId = fromJust . hush . parseIdFromText $ "0a96b396-57d6-11ea-a04b-7b93d1a5c19c", udTeam = hush . parseIdFromText $ "17c59b18-57d6-11ea-9220-8bbf5eee961a", + udCollaboratingTeams = either (error . show) (: []) . parseIdFromText $ "17c59b18-57d6-11ea-9220-8bbf5eee961a", udName = Just . Name $ "Carl Phoomp", udNormalized = Just $ "carl phoomp", udHandle = Just . fromJust . parseHandle $ "phoompy", @@ -68,6 +69,5 @@ userDoc1 = udType = Nothing } --- Dont touch this. This represents serialized legacy data. userDoc1ByteString :: LByteString -userDoc1ByteString = "{\"email\":\"phoompy@example.com\",\"account_status\":\"active\",\"handle\":\"phoompy\",\"managed_by\":\"scim\",\"role\":\"admin\",\"accent_id\":32,\"name\":\"Carl Phoomp\",\"created_at\":\"2020-08-29T21:50:00.000Z\",\"team\":\"17c59b18-57d6-11ea-9220-8bbf5eee961a\",\"id\":\"0a96b396-57d6-11ea-a04b-7b93d1a5c19c\",\"normalized\":\"carl phoomp\",\"saml_idp\":\"https://issuer.net/214234\"}" +userDoc1ByteString = "{\"collaborating_teams\":[\"17c59b18-57d6-11ea-9220-8bbf5eee961a\"],\"email\":\"phoompy@example.com\",\"account_status\":\"active\",\"handle\":\"phoompy\",\"managed_by\":\"scim\",\"role\":\"admin\",\"accent_id\":32,\"name\":\"Carl Phoomp\",\"created_at\":\"2020-08-29T21:50:00.000Z\",\"team\":\"17c59b18-57d6-11ea-9220-8bbf5eee961a\",\"id\":\"0a96b396-57d6-11ea-a04b-7b93d1a5c19c\",\"normalized\":\"carl phoomp\",\"saml_idp\":\"https://issuer.net/214234\"}" diff --git a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs index 68942a77cb3..9e309d50dda 100644 --- a/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/UserSubsystem/InterpreterSpec.hs @@ -1110,7 +1110,7 @@ spec = describe "UserSubsystem.Interpreter" do searchee = searcheeNoHandle {handle = Just searcheeHandle} :: StoredUser storedUserToDoc :: StoredUser -> UserDoc - storedUserToDoc user = indexUserToDoc defaultSearchVisibilityInbound Nothing (storedUserToIndexUser user) + storedUserToDoc user = indexUserToDoc defaultSearchVisibilityInbound Nothing [] (storedUserToIndexUser user) indexFromStoredUsers :: [StoredUser] -> UserIndex indexFromStoredUsers storedUsers = do diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index f7627036f1c..42196d6e18c 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -244,6 +244,7 @@ library Wire.BoundedQueue Wire.BoundedQueue.STM Wire.BrigAPIAccess + Wire.BrigAPIAccess.Local Wire.BrigAPIAccess.Rpc Wire.BudgetStore Wire.BudgetStore.Cassandra @@ -650,6 +651,7 @@ test-suite wire-subsystems-tests Wire.MockInterpreters.AuthenticationSubsystem Wire.MockInterpreters.BackgroundJobPublisher Wire.MockInterpreters.BlockListStore + Wire.MockInterpreters.BrigAPIAccess Wire.MockInterpreters.ClientStore Wire.MockInterpreters.ConversationStore Wire.MockInterpreters.ConversationSubsystem diff --git a/services/background-worker/src/Wire/Effects.hs b/services/background-worker/src/Wire/Effects.hs index 0d367f19eed..66dbee09c56 100644 --- a/services/background-worker/src/Wire/Effects.hs +++ b/services/background-worker/src/Wire/Effects.hs @@ -367,7 +367,7 @@ runBackgroundWorkerEffects env extEnv requestId mJobId = ) . runFeaturesConfigSubsystem . runInputSem getAllTeamFeaturesForServer - . interpretTeamCollaboratorsSubsystem + . interpretTeamCollaboratorsSubsystem (interpretBrigAccess env.brigEndpoint) . discardMeetingNotifier . interpretConversationSubsystem where diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 6c2145ea8cd..27812d06b47 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -36,6 +36,7 @@ module Brig.App cargoholdLens, galleyLens, galleyEndpointLens, + brigEndpointLens, sparEndpointLens, gundeckEndpointLens, cargoholdEndpointLens, @@ -189,6 +190,10 @@ data Env = Env { cargohold :: RPC.Request, galley :: RPC.Request, galleyEndpoint :: Endpoint, + -- | Brig's own listen address. Used only to call ourselves over HTTP for + -- 'BrigAPIAccess' operations that have no local implementation yet; see + -- 'Wire.BrigAPIAccess.Local'. + brigEndpoint :: Endpoint, sparEndpoint :: Endpoint, gundeckEndpoint :: Endpoint, cargoholdEndpoint :: Endpoint, @@ -307,6 +312,7 @@ newEnv opts = do { cargohold = mkEndpoint $ opts.cargohold, galley = mkEndpoint $ opts.galley, galleyEndpoint = opts.galley, + brigEndpoint = opts.brig, sparEndpoint = opts.spar, gundeckEndpoint = opts.gundeck, cargoholdEndpoint = opts.cargohold, diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index f866fc5a9ca..4414567c910 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -47,6 +47,7 @@ import Polysemy.Input (Input, runInputConst) import Polysemy.Internal.Kind import Polysemy.Resource import Polysemy.TinyLog (TinyLog) +import Util.Options (Endpoint) import Wire.API.Error (ErrorS, errorToWai) import Wire.API.Error.Galley import Wire.API.Federation.Client qualified @@ -68,6 +69,8 @@ import Wire.BackgroundJobsPublisher (BackgroundJobPublisher) import Wire.BackgroundJobsPublisher.RabbitMQ (interpretBackgroundJobPublisherRabbitMQ) import Wire.BlockListStore import Wire.BlockListStore.Cassandra +import Wire.BrigAPIAccess (BrigAPIAccess) +import Wire.BrigAPIAccess.Local (interpretBrigAPIAccessLocally) import Wire.BudgetStore import Wire.BudgetStore.Cassandra import Wire.ClientStore (ClientStore) @@ -130,6 +133,7 @@ import Wire.PropertySubsystem.Interpreter import Wire.RateLimit import Wire.RateLimit.Interpreter import Wire.Rpc +import Wire.RpcException (RpcException) import Wire.SAMLEmailSubsystem import Wire.SAMLEmailSubsystem.Interpreter import Wire.SFT (SFT, interpretSFT) @@ -189,13 +193,12 @@ type RecursiveEffects = '[ AuthenticationSubsystem, UserSubsystem, AppSubsystem, - ClientSubsystem + ClientSubsystem, + BrigAPIAccess, + TeamCollaboratorsSubsystem ] -type NonRecursiveEffects2 = - '[ TeamCollaboratorsSubsystem - ] - `Append` BrigLowerLevelEffects +type NonRecursiveEffects2 = BrigLowerLevelEffects -- | These effects have interpreters which don't depend on each other type BrigLowerLevelEffects = @@ -276,6 +279,7 @@ type BrigLowerLevelEffects = Embed Cas.Client, Error ClientError, Error ParseException, + Error RpcException, Error ErrorCall, Error SomeException, Error HttpError, @@ -297,9 +301,11 @@ type BrigLowerLevelEffects = -- Cloned from "Wire.MiniBackend". runRecursiveEffects :: (Members NonRecursiveEffects2 r) => + -- | Brig's own endpoint; see 'interpretBrigAPIAccessLocally'. + Endpoint -> Sem (RecursiveEffects `Append` r) a -> Sem r a -runRecursiveEffects = runClient . runApp . runUser . runAuth +runRecursiveEffects selfEndpoint = runTeamCollaborators . runBrigAPIAccess . runClient . runApp . runUser . runAuth where runAuth :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor AuthenticationSubsystem r runAuth = interpretAuthenticationSubsystem runUser @@ -313,6 +319,12 @@ runRecursiveEffects = runClient . runApp . runUser . runAuth runClient :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor ClientSubsystem r runClient = runClientSubsystem runAuth runUser + runBrigAPIAccess :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor BrigAPIAccess r + runBrigAPIAccess = interpretBrigAPIAccessLocally selfEndpoint runUser + + runTeamCollaborators :: forall r. (Members NonRecursiveEffects2 r) => InterpreterFor TeamCollaboratorsSubsystem r + runTeamCollaborators = interpretTeamCollaboratorsSubsystem runBrigAPIAccess + runBrigToIO :: App.Env -> AppT BrigCanonicalEffects a -> IO a runBrigToIO e (AppT ma) = do let blockedDomains = @@ -433,6 +445,7 @@ runBrigToIO e (AppT ma) = do . rethrowHttpErrorIO . runError @SomeException . mapError @ErrorCall SomeException + . mapError @RpcException SomeException . mapError @ParseException SomeException . mapError clientErrorToHttpError . interpretClientToIO e.casClient @@ -510,8 +523,7 @@ runBrigToIO e (AppT ma) = do . interpretTeamCollaboratorsStoreToPostgres . interpretTeamSubsystemToGalleyAPI . samlEmailSubsystemInterpreter - . interpretTeamCollaboratorsSubsystem - . runRecursiveEffects + . runRecursiveEffects e.brigEndpoint . interpretUserGroupSubsystem . maybe runEnterpriseLoginSubsystemNoConfig diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index ea72f9aeef5..695007e195e 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -51,6 +51,7 @@ import Polysemy.TinyLog (TinyLog) import System.Logger qualified as Log import System.Logger.Class (Logger) import Util.Options +import Wire.API.Team.Collaborator (TeamCollaboratorsError) import Wire.ClientSubsystem.Error (ClientError) import Wire.GalleyAPIAccess (GalleyAPIAccess) import Wire.GalleyAPIAccess.Rpc @@ -65,6 +66,8 @@ import Wire.Rpc import Wire.Sem.Logger.TinyLog import Wire.Sem.Metrics (Metrics) import Wire.Sem.Metrics.IO +import Wire.TeamCollaboratorsStore (TeamCollaboratorsStore) +import Wire.TeamCollaboratorsStore.Postgres (interpretTeamCollaboratorsStoreToPostgres) import Wire.UserKeyStore (UserKeyStore) import Wire.UserKeyStore.Cassandra import Wire.UserSearch.Migration (MigrationException) @@ -75,6 +78,7 @@ import Wire.UserStore.Postgres (interpretUserStorePostgres) type BrigIndexEffectStack = [ UserKeyStore, UserStore, + TeamCollaboratorsStore, IndexedUserStore, Error IndexedUserStoreError, IndexedUserMigrationStore, @@ -86,6 +90,7 @@ type BrigIndexEffectStack = TinyLog, Input Hasql.Pool, Error UsageError, + Error TeamCollaboratorsError, Error ClientError, Embed IO, Final IO @@ -132,6 +137,7 @@ runSem (mgr, casClient, pgPool, bhEnv, indexedUserStoreConfig, reqId, migrationI runFinal . embedToFinal . throwErrorToIOFinal @ClientError + . throwErrorToIOFinal @TeamCollaboratorsError . throwPostgresUsageErrorToIOFinal . runInputConst pgPool . loggerToTinyLogReqId reqId logger @@ -143,6 +149,7 @@ runSem (mgr, casClient, pgPool, bhEnv, indexedUserStoreConfig, reqId, migrationI . interpretIndexedUserMigrationStoreES bhEnv migrationIndexName . throwErrorToIOFinal @IndexedUserStoreError . interpretIndexedUserStoreES indexedUserStoreConfig + . interpretTeamCollaboratorsStoreToPostgres . userStoreInterpreter . interpretUserKeyStoreCassandra casClient $ action @@ -169,10 +176,14 @@ runCommand l = \case runIndexIO e $ resetIndex (mkCreateIndexSettings es) Reindex es cas pg userStorageLocation galley pageSize -> do semDeps <- mkSemDeps (es ^. esConnection) cas pg l - IndexedUserStoreBulk.syncAllUsers (runSem semDeps userStorageLocation galley l) pageSize + skipped <- IndexedUserStoreBulk.syncAllUsers (runSem semDeps userStorageLocation galley l) pageSize + when (skipped /= 0) do + throwM . IndexMigrationError $ "Reindex: failed to sync " <> show skipped <> " documents." ReindexSameOrNewer es cas pg userStorageLocation galley pageSize -> do semDeps <- mkSemDeps (es ^. esConnection) cas pg l - IndexedUserStoreBulk.forceSyncAllUsers (runSem semDeps userStorageLocation galley l) pageSize + skipped <- IndexedUserStoreBulk.forceSyncAllUsers (runSem semDeps userStorageLocation galley l) pageSize + when (skipped /= 0) do + throwM . IndexMigrationError $ "ReindexSameOrNewer: failed to sync " <> show skipped <> " documents." UpdateMapping esConn galley -> do e <- initIndex l esConn galley runIndexIO e updateMapping @@ -253,6 +264,11 @@ waitForTaskToComplete timeoutSeconds taskNodeId = do errTaskGet :: ES.EsError -> m x errTaskGet e = throwM $ ReindexFromAnotherIndexError $ "Error response while getting task: " <> show e +newtype IndexMigrationError = IndexMigrationError String + deriving (Show) + +instance Exception IndexMigrationError + newtype ReindexFromAnotherIndexError = ReindexFromAnotherIndexError String deriving (Show) diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 4c4919729d9..68a17f07b1f 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -364,6 +364,15 @@ indexMapping = mpAnalyzer = Nothing, mpFields = mempty }, + -- teams this user collaborates with (without being a member of them) + "collaborating_teams" + .= MappingProperty + { mpType = MPKeyword, + mpStore = False, + mpIndex = True, + mpAnalyzer = Nothing, + mpFields = mempty + }, "accent_id" .= MappingProperty { mpType = MPByte, diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 4d755d8c612..f47dc7f3798 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -572,7 +572,7 @@ evalGalley e = . interpretTeamSubsystem teamSubsystemConfig . runFeaturesConfigSubsystem . runInputSem getAllTeamFeaturesForServer - . interpretTeamCollaboratorsSubsystem + . interpretTeamCollaboratorsSubsystem (interpretBrigAccess (e ^. brig)) . runFederationSubsystem conversationSubsystemConfig.federationProtocols . runInputConst (e ^. reqId) . interpretJobSubsystem