diff --git a/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs b/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs index e6ed4ecc4c2..95ed33c5558 100644 --- a/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs +++ b/libs/wire-api/src/Wire/API/Routes/FederationDomainConfig.hs @@ -54,7 +54,7 @@ instance ToSchema FederationRestriction where -- information for search policy. data FederationDomainConfig = FederationDomainConfig { domain :: Domain, - cfgSearchPolicy :: FederatedUserSearchPolicy, + searchPolicy :: FederatedUserSearchPolicy, restriction :: FederationRestriction } deriving (Eq, Ord, Show, Generic) @@ -66,7 +66,7 @@ instance ToSchema FederationDomainConfig where object "FederationDomainConfig" $ FederationDomainConfig <$> domain .= field "domain" schema - <*> cfgSearchPolicy .= field "search_policy" schema + <*> searchPolicy .= field "search_policy" schema <*> restriction .= field "restriction" schema data FederationDomainConfigs = FederationDomainConfigs diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 9d217d4aefd..b081cb0c5f9 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -110,7 +110,6 @@ library Brig.Data.Activation Brig.Data.Client Brig.Data.Connection - Brig.Data.Federation Brig.Data.Instances Brig.Data.LoginCode Brig.Data.MLS.KeyPackage @@ -126,6 +125,8 @@ library Brig.Effects.CodeStore Brig.Effects.CodeStore.Cassandra Brig.Effects.Delay + Brig.Effects.FederationConfigStore + Brig.Effects.FederationConfigStore.Cassandra Brig.Effects.GalleyProvider Brig.Effects.GalleyProvider.RPC Brig.Effects.JwtTools @@ -318,7 +319,6 @@ library , polysemy-plugin , polysemy-wire-zoo , proto-lens >=0.1 - , random , random-shuffle >=0.0.3 , raw-strings-qq , resource-pool >=0.2 diff --git a/services/brig/default.nix b/services/brig/default.nix index 6887c802f38..14f1634b1d5 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -242,7 +242,6 @@ mkDerivation { polysemy-plugin polysemy-wire-zoo proto-lens - random random-shuffle raw-strings-qq resource-pool diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 90ddd22a281..781984d2bf5 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -32,6 +32,7 @@ import Brig.API.User qualified as API import Brig.App import Brig.Data.Connection qualified as Data import Brig.Data.User qualified as Data +import Brig.Effects.FederationConfigStore (FederationConfigStore) import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.IO.Intra (notify) import Brig.Options @@ -76,7 +77,8 @@ type FederationAPI = "federation" :> BrigApi federationSitemap :: ( Member GalleyProvider r, - Member (Concurrency 'Unsafe) r + Member (Concurrency 'Unsafe) r, + Member FederationConfigStore r ) => ServerT FederationAPI (Handler r) federationSitemap = @@ -96,7 +98,7 @@ federationSitemap = -- Allow remote domains to send their known remote federation instances, and respond -- with the subset of those we aren't connected to. -getFederationStatus :: Domain -> DomainSet -> Handler r NonConnectedBackends +getFederationStatus :: (Member FederationConfigStore r) => Domain -> DomainSet -> Handler r NonConnectedBackends getFederationStatus _ request = do cfg <- ask case setFederationStrategy (cfg ^. settings) of @@ -118,7 +120,9 @@ sendConnectionAction originDomain NewConnectionRequest {..} = do else pure NewConnectionResponseUserNotActivated getUserByHandle :: - Member GalleyProvider r => + ( Member GalleyProvider r, + Member FederationConfigStore r + ) => Domain -> Handle -> ExceptT Error (AppT r) (Maybe UserProfile) @@ -179,7 +183,9 @@ fedClaimKeyPackages domain ckpr = -- (This decision may change in the future) searchUsers :: forall r. - Member GalleyProvider r => + ( Member GalleyProvider r, + Member FederationConfigStore r + ) => Domain -> SearchRequest -> ExceptT Error (AppT r) SearchResponse @@ -240,8 +246,8 @@ onUserDeleted origDomain udcn = lift $ do pure EmptyResponse -- | If domain is not configured fall back to `NoSearch` -lookupSearchPolicy :: Domain -> (Handler r) FederatedUserSearchPolicy +lookupSearchPolicy :: (Member FederationConfigStore r) => Domain -> (Handler r) FederatedUserSearchPolicy lookupSearchPolicy domain = do domainConfigs <- getFederationRemotes let mConfig = find ((== domain) . FD.domain) (domainConfigs.remotes) - pure $ maybe NoSearch FD.cfgSearchPolicy mConfig + pure $ maybe NoSearch FD.searchPolicy mConfig diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 61368a7012d..84fea996015 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -38,12 +38,13 @@ import Brig.Code qualified as Code import Brig.Data.Activation import Brig.Data.Client qualified as Data import Brig.Data.Connection qualified as Data -import Brig.Data.Federation qualified as Data import Brig.Data.MLS.KeyPackage qualified as Data import Brig.Data.User qualified as Data import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.CodeStore (CodeStore) +import Brig.Effects.FederationConfigStore (AddFederationRemoteResult (..), FederationConfigStore) +import Brig.Effects.FederationConfigStore qualified as FederationConfigStore import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) @@ -78,7 +79,6 @@ import Polysemy import Servant hiding (Handler, JSON, addHeader, respond) import Servant.OpenApi.Internal.Orphans () import System.Logger.Class qualified as Log -import System.Random (randomRIO) import UnliftIO.Async import Wire.API.Connection import Wire.API.Error @@ -106,7 +106,8 @@ servantSitemap :: Member BlacklistPhonePrefixStore r, Member PasswordResetStore r, Member GalleyProvider r, - Member (UserPendingActivationStore p) r + Member (UserPendingActivationStore p) r, + Member FederationConfigStore r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -214,7 +215,7 @@ authAPI = :<|> Named @"login-code" getLoginCode :<|> Named @"reauthenticate" reauthenticate -federationRemotesAPI :: ServerT BrigIRoutes.FederationRemotesAPI (Handler r) +federationRemotesAPI :: (Member FederationConfigStore r) => ServerT BrigIRoutes.FederationRemotesAPI (Handler r) federationRemotesAPI = Named @"add-federation-remotes" addFederationRemote :<|> Named @"get-federation-remotes" getFederationRemotes @@ -223,25 +224,25 @@ federationRemotesAPI = :<|> Named @"get-federation-remote-teams" getFederationRemoteTeams :<|> Named @"delete-federation-remote-team" deleteFederationRemoteTeam -deleteFederationRemoteTeam :: Domain -> TeamId -> (Handler r) () +deleteFederationRemoteTeam :: (Member FederationConfigStore r) => Domain -> TeamId -> (Handler r) () deleteFederationRemoteTeam domain teamId = - lift . wrapClient $ Data.deleteFederationRemoteTeam domain teamId + lift $ liftSem $ FederationConfigStore.removeFederationRemoteTeam domain teamId -getFederationRemoteTeams :: Domain -> (Handler r) [FederationRemoteTeam] +getFederationRemoteTeams :: (Member FederationConfigStore r) => Domain -> (Handler r) [FederationRemoteTeam] getFederationRemoteTeams domain = - lift . wrapClient $ Data.getFederationRemoteTeams domain + lift $ liftSem $ FederationConfigStore.getFederationRemoteTeams domain -addFederationRemoteTeam :: Domain -> FederationRemoteTeam -> (Handler r) () +addFederationRemoteTeam :: (Member FederationConfigStore r) => Domain -> FederationRemoteTeam -> (Handler r) () addFederationRemoteTeam domain rt = - lift . wrapClient $ Data.addFederationRemoteTeam domain rt + lift $ liftSem $ FederationConfigStore.addFederationRemoteTeam domain rt.teamId -addFederationRemote :: FederationDomainConfig -> ExceptT Brig.API.Error.Error (AppT r) () +addFederationRemote :: (Member FederationConfigStore r) => FederationDomainConfig -> (Handler r) () addFederationRemote fedDomConf = do assertNoDivergingDomainInConfigFiles fedDomConf - result <- lift . wrapClient $ Data.addFederationRemote fedDomConf + result <- lift $ liftSem $ FederationConfigStore.addFederationConfig fedDomConf case result of - Data.AddFederationRemoteSuccess -> pure () - Data.AddFederationRemoteMaxRemotesReached -> + AddFederationRemoteSuccess -> pure () + AddFederationRemoteMaxRemotesReached -> throwError . fedError . FederationUnexpectedError $ "Maximum number of remote backends reached. If you need to create more connections, \ \please contact wire.com." @@ -258,14 +259,9 @@ remotesMapFromCfgFile = do else error $ "error in config file: conflicting parameters on domain: " <> show (c, c') pure $ Map.fromListWith merge dict --- | Return the config file list. Use this to make sure the config file is consistent (ie., --- no two entries for the same domain). Based on `remotesMapFromCfgFile`. -remotesListFromCfgFile :: AppT r [FederationDomainConfig] -remotesListFromCfgFile = Map.elems <$> remotesMapFromCfgFile - -- | If remote domain is registered in config file, the version that can be added to the -- database must be the same. -assertNoDivergingDomainInConfigFiles :: FederationDomainConfig -> ExceptT Brig.API.Error.Error (AppT r) () +assertNoDivergingDomainInConfigFiles :: FederationDomainConfig -> (Handler r) () assertNoDivergingDomainInConfigFiles fedComConf = do cfg <- lift remotesMapFromCfgFile let diverges = case Map.lookup (domain fedComConf) cfg of @@ -283,55 +279,41 @@ assertNoDivergingDomainInConfigFiles fedComConf = do <> cs (show (Map.lookup (domain fedComConf) cfg)) ) -getFederationRemotes :: ExceptT Brig.API.Error.Error (AppT r) FederationDomainConfigs +getFederationRemotes :: (Member FederationConfigStore r) => (Handler r) FederationDomainConfigs getFederationRemotes = lift $ do -- FUTUREWORK: we should solely rely on `db` in the future for remote domains; merging -- remote domains from `cfg` is just for providing an easier, more robust migration path. -- See -- https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections, -- http://docs.wire.com/developer/developer/federation-design-aspects.html#configuring-remote-connections-dev-perspective - db <- wrapClient Data.getFederationRemotes - (ms :: Maybe FederationStrategy, mf :: [FederationDomainConfig], mu :: Maybe Int) <- do + db <- liftSem $ FederationConfigStore.getFederationConfigs + ms :: Maybe FederationStrategy <- do cfg <- ask - domcfgs <- remotesListFromCfgFile -- (it's not very elegant to prove the env twice here, but this code is transitory.) - pure - ( setFederationStrategy (cfg ^. settings), - domcfgs, - setFederationDomainConfigsUpdateFreq (cfg ^. settings) - ) - - -- update frequency settings of `<1` are interpreted as `1 second`. only warn about this every now and - -- then, that'll be noise enough for the logs given the traffic on this end-point. - unless (maybe True (> 0) mu) $ - randomRIO (0 :: Int, 1000) - >>= \case - 0 -> Log.warn (Log.msg (Log.val "Invalid brig configuration: setFederationDomainConfigsUpdateFreq must be > 0. setting to 1 second.")) - _ -> pure () + pure (setFederationStrategy (cfg ^. settings)) defFederationDomainConfigs & maybe id (\v cfg -> cfg {strategy = v}) ms - & (\cfg -> cfg {remotes = nub $ db <> mf}) - & maybe id (\v cfg -> cfg {updateInterval = min 1 v}) mu + & (\cfg -> cfg {remotes = fmap FederationConfigStore.fromFederationDomainConfig db}) & pure -updateFederationRemote :: Domain -> FederationDomainConfig -> ExceptT Brig.API.Error.Error (AppT r) () +updateFederationRemote :: (Member FederationConfigStore r) => Domain -> FederationDomainConfig -> (Handler r) () updateFederationRemote dom fedcfg = do assertDomainIsNotUpdated dom fedcfg assertNoDomainsFromConfigFiles dom - (lift . wrapClient . Data.updateFederationRemote $ fedcfg) >>= \case + (lift . liftSem . FederationConfigStore.updateFederationConfig $ fedcfg) >>= \case True -> pure () False -> throwError . fedError . FederationUnexpectedError . cs $ "federation domain does not exist and cannot be updated: " <> show (dom, fedcfg) -assertDomainIsNotUpdated :: Domain -> FederationDomainConfig -> ExceptT Brig.API.Error.Error (AppT r) () +assertDomainIsNotUpdated :: Domain -> FederationDomainConfig -> (Handler r) () assertDomainIsNotUpdated dom fedcfg = do when (dom /= domain fedcfg) $ throwError . fedError . FederationUnexpectedError . cs $ "federation domain of a given peer cannot be changed from " <> show (domain fedcfg) <> " to " <> show dom <> "." -- | FUTUREWORK: should go away in the future; see 'getFederationRemotes'. -assertNoDomainsFromConfigFiles :: Domain -> ExceptT Brig.API.Error.Error (AppT r) () +assertNoDomainsFromConfigFiles :: Domain -> (Handler r) () assertNoDomainsFromConfigFiles dom = do cfg <- fmap (.federationDomainConfig) <$> asks (fromMaybe [] . setFederationDomainConfigs . view settings) when (dom `elem` (domain <$> cfg)) $ do diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 5322de63927..cd259218a4e 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -7,6 +7,8 @@ import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BlacklistStore.Cassandra (interpretBlacklistStoreToCassandra) import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.CodeStore.Cassandra (codeStoreToCassandra, interpretClientToIO) +import Brig.Effects.FederationConfigStore (FederationConfigStore) +import Brig.Effects.FederationConfigStore.Cassandra (interpretFederationDomainConfig) import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.GalleyProvider.RPC (interpretGalleyProviderToRPC) import Brig.Effects.JwtTools @@ -19,6 +21,7 @@ import Brig.Effects.ServiceRPC (Service (Galley), ServiceRPC) import Brig.Effects.ServiceRPC.IO (interpretServiceRpcToRpc) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore.Cassandra (userPendingActivationStoreToCassandra) +import Brig.Options (ImplicitNoFederationRestriction (federationDomainConfig), federationDomainConfigs) import Brig.RPC (ParseException) import Cassandra qualified as Cas import Control.Lens ((^.)) @@ -36,7 +39,8 @@ import Wire.Sem.Now.IO (nowToIOAction) import Wire.Sem.Paging.Cassandra (InternalPaging) type BrigCanonicalEffects = - '[ Jwk, + '[ FederationConfigStore, + Jwk, PublicKeyBundle, JwtTools, BlacklistPhonePrefixStore, @@ -79,6 +83,7 @@ runBrigToIO e (AppT ma) = do . interpretJwtTools . interpretPublicKeyBundle . interpretJwk + . interpretFederationDomainConfig (maybe [] (fmap (.federationDomainConfig)) (e ^. settings . federationDomainConfigs)) ) ) $ runReaderT ma e diff --git a/services/brig/src/Brig/Data/Federation.hs b/services/brig/src/Brig/Data/Federation.hs deleted file mode 100644 index 3ae38325d40..00000000000 --- a/services/brig/src/Brig/Data/Federation.hs +++ /dev/null @@ -1,101 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Brig.Data.Federation - ( getFederationRemotes, - addFederationRemote, - updateFederationRemote, - deleteFederationRemote, - addFederationRemoteTeam, - getFederationRemoteTeams, - deleteFederationRemoteTeam, - AddFederationRemoteResult (..), - ) -where - -import Brig.Data.Instances () -import Cassandra -import Control.Exception (ErrorCall (ErrorCall)) -import Control.Monad.Catch (throwM) -import Data.Domain -import Data.Id -import Database.CQL.Protocol (SerialConsistency (LocalSerialConsistency), serialConsistency) -import Imports -import Wire.API.Routes.FederationDomainConfig -import Wire.API.User.Search - -maxKnownNodes :: Int -maxKnownNodes = 10000 - -getFederationRemotes :: forall m. MonadClient m => m [FederationDomainConfig] -getFederationRemotes = (\(d, p, r) -> FederationDomainConfig d p r) <$$> qry - where - qry :: m [(Domain, FederatedUserSearchPolicy, FederationRestriction)] - qry = retry x1 . query get $ params LocalQuorum () - - get :: PrepQuery R () (Domain, FederatedUserSearchPolicy, FederationRestriction) - get = fromString $ "SELECT domain, search_policy, restriction FROM federation_remotes LIMIT " <> show maxKnownNodes - -data AddFederationRemoteResult = AddFederationRemoteSuccess | AddFederationRemoteMaxRemotesReached - -addFederationRemote :: MonadClient m => FederationDomainConfig -> m AddFederationRemoteResult -addFederationRemote (FederationDomainConfig rDomain searchPolicy restriction) = do - l <- length <$> getFederationRemotes - if l >= maxKnownNodes - then pure AddFederationRemoteMaxRemotesReached - else AddFederationRemoteSuccess <$ retry x5 (write add (params LocalQuorum (rDomain, searchPolicy, restriction))) - where - add :: PrepQuery W (Domain, FederatedUserSearchPolicy, FederationRestriction) () - add = "INSERT INTO federation_remotes (domain, search_policy, restriction) VALUES (?, ?, ?)" - -updateFederationRemote :: MonadClient m => FederationDomainConfig -> m Bool -updateFederationRemote (FederationDomainConfig rDomain searchPolicy restriction) = do - retry x1 (trans upd (params LocalQuorum (searchPolicy, restriction, rDomain)) {serialConsistency = Just LocalSerialConsistency}) >>= \case - [] -> pure False - [_] -> pure True - _ -> throwM $ ErrorCall "Primary key violation detected federation_remotes" - where - upd :: PrepQuery W (FederatedUserSearchPolicy, FederationRestriction, Domain) x - upd = "UPDATE federation_remotes SET search_policy = ?, restriction = ? WHERE domain = ? IF EXISTS" - -deleteFederationRemote :: MonadClient m => Domain -> m () -deleteFederationRemote rDomain = - retry x1 $ write delete (params LocalQuorum (Identity rDomain)) - where - delete :: PrepQuery W (Identity Domain) () - delete = "DELETE FROM federation_remotes WHERE domain = ?" - -addFederationRemoteTeam :: MonadClient m => Domain -> FederationRemoteTeam -> m () -addFederationRemoteTeam rDomain rteam = - retry x1 $ write add (params LocalQuorum (rDomain, rteam.teamId)) - where - add :: PrepQuery W (Domain, TeamId) () - add = "INSERT INTO federation_remote_teams (domain, team) VALUES (?, ?)" - -getFederationRemoteTeams :: MonadClient m => Domain -> m [FederationRemoteTeam] -getFederationRemoteTeams rDomain = do - fmap (FederationRemoteTeam . runIdentity) <$> retry x1 (query get (params LocalQuorum (Identity rDomain))) - where - get :: PrepQuery R (Identity Domain) (Identity TeamId) - get = "SELECT team FROM federation_remote_teams WHERE domain = ?" - -deleteFederationRemoteTeam :: MonadClient m => Domain -> TeamId -> m () -deleteFederationRemoteTeam rDomain rteam = - retry x1 $ write delete (params LocalQuorum (rDomain, rteam)) - where - delete :: PrepQuery W (Domain, TeamId) () - delete = "DELETE FROM federation_remote_teams WHERE domain = ? AND team = ?" diff --git a/services/brig/src/Brig/Effects/FederationConfigStore.hs b/services/brig/src/Brig/Effects/FederationConfigStore.hs new file mode 100644 index 00000000000..b84df100889 --- /dev/null +++ b/services/brig/src/Brig/Effects/FederationConfigStore.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Brig.Effects.FederationConfigStore where + +import Data.Domain +import Data.Id +import Imports +import Polysemy +import Wire.API.Routes.FederationDomainConfig qualified as API +import Wire.API.User.Search (FederatedUserSearchPolicy) + +data FederationRestriction = FederationRestrictionAllowAll | FederationRestrictionByTeam [TeamId] + deriving stock (Eq, Show, Ord) + +data AddFederationRemoteResult = AddFederationRemoteSuccess | AddFederationRemoteMaxRemotesReached + +data FederationDomainConfig = FederationDomainConfig + { domain :: Domain, + searchPolicy :: FederatedUserSearchPolicy, + restriction :: FederationRestriction + } + deriving stock (Show, Eq) + +fromFederationDomainConfig :: FederationDomainConfig -> API.FederationDomainConfig +fromFederationDomainConfig (FederationDomainConfig d p FederationRestrictionAllowAll) = API.FederationDomainConfig d p API.FederationRestrictionAllowAll +fromFederationDomainConfig (FederationDomainConfig d p (FederationRestrictionByTeam _)) = API.FederationDomainConfig d p API.FederationRestrictionByTeam + +data FederationConfigStore m a where + GetFederationConfig :: Domain -> FederationConfigStore m (Maybe FederationDomainConfig) + GetFederationConfigs :: FederationConfigStore m [FederationDomainConfig] + AddFederationConfig :: API.FederationDomainConfig -> FederationConfigStore m AddFederationRemoteResult + UpdateFederationConfig :: API.FederationDomainConfig -> FederationConfigStore m Bool + AddFederationRemoteTeam :: Domain -> TeamId -> FederationConfigStore m () + RemoveFederationRemoteTeam :: Domain -> TeamId -> FederationConfigStore m () + GetFederationRemoteTeams :: Domain -> FederationConfigStore m [API.FederationRemoteTeam] + +makeSem ''FederationConfigStore diff --git a/services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs b/services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs new file mode 100644 index 00000000000..5b62e0e2f03 --- /dev/null +++ b/services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs @@ -0,0 +1,151 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Effects.FederationConfigStore.Cassandra + ( interpretFederationDomainConfig, + AddFederationRemoteResult (..), + ) +where + +import Brig.Data.Instances () +import Brig.Effects.FederationConfigStore +import Cassandra +import Control.Exception (ErrorCall (ErrorCall)) +import Control.Lens +import Control.Monad.Catch (throwM) +import Data.Domain +import Data.Id +import Data.Map qualified as Map +import Database.CQL.Protocol (SerialConsistency (LocalSerialConsistency), serialConsistency) +import Imports +import Polysemy +import Wire.API.Routes.FederationDomainConfig qualified as API +import Wire.API.User.Search + +interpretFederationDomainConfig :: + forall m r a. + ( MonadClient m, + Member (Embed m) r + ) => + [API.FederationDomainConfig] -> + Sem (FederationConfigStore ': r) a -> + Sem r a +interpretFederationDomainConfig cfgs = + interpret $ + embed @m . \case + GetFederationConfig d -> getFederationConfig' cfgs d + GetFederationConfigs -> getFederationConfigs' cfgs + AddFederationConfig cnf -> addFederationConfig' cnf + UpdateFederationConfig cnf -> updateFederationConfig' cnf + AddFederationRemoteTeam d t -> addFederationRemoteTeam' d t + RemoveFederationRemoteTeam d t -> removeFederationRemoteTeam' d t + GetFederationRemoteTeams d -> getFederationRemoteTeams' d + +-- | Compile config file list into a map indexed by domains. Use this to make sure the config +-- file is consistent (ie., no two entries for the same domain). +remotesMapFromCfgFile :: (Monad m) => [API.FederationDomainConfig] -> m (Map Domain API.FederationDomainConfig) +remotesMapFromCfgFile cfg = do + let dict = [(cnf.domain, cnf) | cnf <- cfg] + merge c c' = + if c == c' + then c + else error $ "error in config file: conflicting parameters on domain: " <> show (c, c') + pure $ Map.fromListWith merge dict + +-- | Return the config file list. Use this to make sure the config file is consistent (ie., +-- no two entries for the same domain). Based on `remotesMapFromCfgFile`. +remotesListFromCfgFile :: Monad m => [API.FederationDomainConfig] -> m [API.FederationDomainConfig] +remotesListFromCfgFile cfgs = Map.elems <$> remotesMapFromCfgFile cfgs + +getFederationConfigs' :: forall m. (MonadClient m) => [API.FederationDomainConfig] -> m [FederationDomainConfig] +getFederationConfigs' cfgs = do + xs <- getFederationRemotes + ys <- remotesListFromCfgFile cfgs + configs <- + forM (xs <> ys) $ + \case + API.FederationDomainConfig d p API.FederationRestrictionAllowAll -> + pure $ FederationDomainConfig d p FederationRestrictionAllowAll + API.FederationDomainConfig d p API.FederationRestrictionByTeam -> + FederationDomainConfig d p . FederationRestrictionByTeam . fmap API.teamId <$> getFederationRemoteTeams' d + pure $ nub configs + +maxKnownNodes :: Int +maxKnownNodes = 10000 + +getFederationConfig' :: MonadClient m => [API.FederationDomainConfig] -> Domain -> m (Maybe FederationDomainConfig) +getFederationConfig' cfgs rDomain = do + let mFromCfgFile = (\c -> (c.searchPolicy, c.restriction)) <$> find ((== rDomain) . API.domain) cfgs + mCnf <- retry x1 (query1 q (params LocalQuorum (Identity rDomain))) + teams <- fmap API.teamId <$> getFederationRemoteTeams' rDomain + pure $ + (mFromCfgFile <|> mCnf) <&> \case + (sp, API.FederationRestrictionAllowAll) -> FederationDomainConfig rDomain sp FederationRestrictionAllowAll + (sp, API.FederationRestrictionByTeam) -> FederationDomainConfig rDomain sp (FederationRestrictionByTeam teams) + where + q :: PrepQuery R (Identity Domain) (FederatedUserSearchPolicy, API.FederationRestriction) + q = "SELECT search_policy, restriction FROM federation_remotes WHERE domain = ?" + +getFederationRemotes :: forall m. MonadClient m => m [API.FederationDomainConfig] +getFederationRemotes = (\(d, p, r) -> API.FederationDomainConfig d p r) <$$> qry + where + qry :: m [(Domain, FederatedUserSearchPolicy, API.FederationRestriction)] + qry = retry x1 . query get $ params LocalQuorum () + + get :: PrepQuery R () (Domain, FederatedUserSearchPolicy, API.FederationRestriction) + get = fromString $ "SELECT domain, search_policy, restriction FROM federation_remotes LIMIT " <> show maxKnownNodes + +addFederationConfig' :: MonadClient m => API.FederationDomainConfig -> m AddFederationRemoteResult +addFederationConfig' (API.FederationDomainConfig rDomain searchPolicy restriction) = do + l <- length <$> getFederationRemotes + if l >= maxKnownNodes + then pure AddFederationRemoteMaxRemotesReached + else AddFederationRemoteSuccess <$ retry x5 (write add (params LocalQuorum (rDomain, searchPolicy, restriction))) + where + add :: PrepQuery W (Domain, FederatedUserSearchPolicy, API.FederationRestriction) () + add = "INSERT INTO federation_remotes (domain, search_policy, restriction) VALUES (?, ?, ?)" + +updateFederationConfig' :: MonadClient m => API.FederationDomainConfig -> m Bool +updateFederationConfig' (API.FederationDomainConfig rDomain searchPolicy restriction) = do + retry x1 (trans upd (params LocalQuorum (searchPolicy, restriction, rDomain)) {serialConsistency = Just LocalSerialConsistency}) >>= \case + [] -> pure False + [_] -> pure True + _ -> throwM $ ErrorCall "Primary key violation detected federation_remotes" + where + upd :: PrepQuery W (FederatedUserSearchPolicy, API.FederationRestriction, Domain) x + upd = "UPDATE federation_remotes SET search_policy = ?, restriction = ? WHERE domain = ? IF EXISTS" + +addFederationRemoteTeam' :: MonadClient m => Domain -> TeamId -> m () +addFederationRemoteTeam' rDomain tid = + retry x1 $ write add (params LocalQuorum (rDomain, tid)) + where + add :: PrepQuery W (Domain, TeamId) () + add = "INSERT INTO federation_remote_teams (domain, team) VALUES (?, ?)" + +getFederationRemoteTeams' :: MonadClient m => Domain -> m [API.FederationRemoteTeam] +getFederationRemoteTeams' rDomain = do + fmap (API.FederationRemoteTeam . runIdentity) <$> retry x1 (query get (params LocalQuorum (Identity rDomain))) + where + get :: PrepQuery R (Identity Domain) (Identity TeamId) + get = "SELECT team FROM federation_remote_teams WHERE domain = ?" + +removeFederationRemoteTeam' :: MonadClient m => Domain -> TeamId -> m () +removeFederationRemoteTeam' rDomain rteam = + retry x1 $ write delete (params LocalQuorum (rDomain, rteam)) + where + delete :: PrepQuery W (Domain, TeamId) () + delete = "DELETE FROM federation_remote_teams WHERE domain = ? AND team = ?"