From 79697112e7e9717dabd802368c01951335239568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 22 Apr 2022 12:11:27 +0200 Subject: [PATCH 01/41] Refactor effects around password resetting --- services/brig/brig.cabal | 2 + services/brig/src/Brig/API.hs | 4 +- services/brig/src/Brig/API/Internal.hs | 8 +-- services/brig/src/Brig/API/Public.hs | 10 ++-- services/brig/src/Brig/API/User.hs | 12 ++-- services/brig/src/Brig/App.hs | 4 ++ services/brig/src/Brig/Data/Activation.hs | 15 ++++- services/brig/src/Brig/Sem/CodeStore.hs | 3 - .../brig/src/Brig/Sem/CodeStore/Cassandra.hs | 23 -------- .../brig/src/Brig/Sem/PasswordResetStore.hs | 3 + .../Brig/Sem/PasswordResetStore/CodeStore.hs | 11 +++- .../brig/src/Brig/Sem/PasswordResetSupply.hs | 30 ++++++++++ .../src/Brig/Sem/PasswordResetSupply/IO.hs | 57 +++++++++++++++++++ services/brig/test/integration/API/User.hs | 2 +- .../integration/API/User/PasswordReset.hs | 22 ++++--- .../brig/test/integration/API/User/Util.hs | 9 ++- 16 files changed, 150 insertions(+), 65 deletions(-) create mode 100644 services/brig/src/Brig/Sem/PasswordResetSupply.hs create mode 100644 services/brig/src/Brig/Sem/PasswordResetSupply/IO.hs diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index d9d923a334..45b15b3dca 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -89,6 +89,8 @@ library Brig.Sem.CodeStore.Cassandra Brig.Sem.PasswordResetStore Brig.Sem.PasswordResetStore.CodeStore + Brig.Sem.PasswordResetSupply + Brig.Sem.PasswordResetSupply.IO Brig.SMTP Brig.Team.API Brig.Team.DB diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index 4b842e21cd..189186815f 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -23,14 +23,14 @@ where import Brig.API.Handler (Handler) import qualified Brig.API.Internal as Internal import qualified Brig.API.Public as Public -import Brig.Sem.CodeStore import Brig.Sem.PasswordResetStore (PasswordResetStore) +import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import qualified Data.Swagger.Build.Api as Doc import Network.Wai.Routing (Routes) import Polysemy sitemap :: - Members '[CodeStore, PasswordResetStore] r => + Members '[PasswordResetStore, PasswordResetSupply] r => Routes Doc.ApiBuilder (Handler r) () sitemap = do Public.sitemap diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 2b0c659aba..0d2dd80b4b 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -42,8 +42,8 @@ import qualified Brig.Data.User as Data import qualified Brig.IO.Intra as Intra import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Provider.API as Provider -import Brig.Sem.CodeStore (CodeStore) import Brig.Sem.PasswordResetStore (PasswordResetStore) +import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import qualified Brig.Team.API as Team import Brig.Team.DB (lookupInvitationByEmail) import Brig.Types @@ -187,7 +187,7 @@ swaggerDocsAPI = swaggerSchemaUIServer BrigIRoutes.swaggerDoc -- Sitemap (wai-route) sitemap :: - Members '[CodeStore, PasswordResetStore] r => + Members '[PasswordResetStore, PasswordResetSupply] r => Routes a (Handler r) () sitemap = do get "/i/status" (continue $ const $ pure empty) true @@ -489,14 +489,14 @@ instance ToJSON GetActivationCodeResp where toJSON (GetActivationCodeResp (k, c)) = object ["key" .= k, "code" .= c] getPasswordResetCodeH :: - Members '[CodeStore, PasswordResetStore] r => + Members '[PasswordResetStore, PasswordResetSupply] r => JSON ::: Either Email Phone -> (Handler r) Response getPasswordResetCodeH (_ ::: emailOrPhone) = do maybe (throwStd invalidPwResetKey) (pure . json) =<< lift (getPasswordResetCode emailOrPhone) getPasswordResetCode :: - Members '[CodeStore, PasswordResetStore] r => + Members '[PasswordResetStore, PasswordResetSupply] r => Either Email Phone -> (AppT r) (Maybe GetPasswordResetCodeResp) getPasswordResetCode emailOrPhone = diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 05342c39d4..6fbba7ed82 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -46,8 +46,8 @@ import qualified Brig.Data.UserKey as UserKey import qualified Brig.IO.Intra as Intra import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Provider.API as Provider -import Brig.Sem.CodeStore (CodeStore) import Brig.Sem.PasswordResetStore (PasswordResetStore) +import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import qualified Brig.Team.API as Team import qualified Brig.Team.Email as Team import Brig.Types.Activation (ActivationPair) @@ -274,7 +274,7 @@ servantSitemap = userAPI :<|> selfAPI :<|> accountAPI :<|> clientAPI :<|> prekey -- - MemberLeave event to members for all conversations the user was in (via galley) sitemap :: - Members '[CodeStore, PasswordResetStore] r => + Members '[PasswordResetStore, PasswordResetSupply] r => Routes Doc.ApiBuilder (Handler r) () sitemap = do -- User Handle API ---------------------------------------------------- @@ -438,7 +438,7 @@ sitemap = do apiDocs :: forall r. - Members '[CodeStore, PasswordResetStore] r => + Members '[PasswordResetStore, PasswordResetSupply] r => Routes Doc.ApiBuilder (Handler r) () apiDocs = get @@ -835,7 +835,7 @@ beginPasswordReset (Public.NewPasswordReset target) = do Right phone -> wrapClient $ sendPasswordResetSms phone pair loc completePasswordResetH :: - Members '[CodeStore, PasswordResetStore] r => + Members '[PasswordResetStore, PasswordResetSupply] r => JSON ::: JsonRequest Public.CompletePasswordReset -> (Handler r) Response completePasswordResetH (_ ::: req) = do @@ -1078,7 +1078,7 @@ instance ToJSON DeprecatedMatchingResult where ] deprecatedCompletePasswordResetH :: - Members '[CodeStore, PasswordResetStore] r => + Members '[PasswordResetStore, PasswordResetSupply] r => JSON ::: Public.PasswordResetKey ::: JsonRequest Public.PasswordReset -> (Handler r) Response deprecatedCompletePasswordResetH (_ ::: k ::: req) = do diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 4272fb3cb8..8cb305dce7 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -114,10 +114,10 @@ import qualified Brig.InternalEvent.Types as Internal import Brig.Options hiding (Timeout, internalEvents) import Brig.Password import qualified Brig.Queue as Queue -import Brig.Sem.CodeStore (CodeStore) -import qualified Brig.Sem.CodeStore as E import Brig.Sem.PasswordResetStore (PasswordResetStore) import qualified Brig.Sem.PasswordResetStore as E +import Brig.Sem.PasswordResetSupply (PasswordResetSupply) +import qualified Brig.Sem.PasswordResetSupply as E import qualified Brig.Team.DB as Team import Brig.Types import Brig.Types.Code (Timeout (..)) @@ -988,7 +988,7 @@ beginPasswordReset target = do (user,) <$> lift (liftSem $ E.createPasswordResetCode user target) completePasswordReset :: - Members '[CodeStore, PasswordResetStore] r => + Members '[PasswordResetStore, PasswordResetSupply] r => PasswordResetIdentity -> PasswordResetCode -> PlainTextPassword -> @@ -1003,7 +1003,7 @@ completePasswordReset ident code pw = do checkNewIsDifferent uid pw lift $ do wrapClient $ Data.updatePassword uid pw - liftSem $ E.codeDelete key + liftSem $ E.deletePasswordResetCode key wrapClient $ revokeAllCookies uid -- | Pull the current password of a user and compare it against the one about to be installed. @@ -1016,7 +1016,7 @@ checkNewIsDifferent uid pw = do _ -> pure () mkPasswordResetKey :: - Members '[CodeStore] r => + Members '[PasswordResetSupply] r => PasswordResetIdentity -> ExceptT PasswordResetError (AppT r) PasswordResetKey mkPasswordResetKey ident = case ident of @@ -1194,7 +1194,7 @@ lookupActivationCode emailOrPhone = do pure $ (k,) <$> c lookupPasswordResetCode :: - Members '[CodeStore, PasswordResetStore] r => + Members '[PasswordResetStore, PasswordResetSupply] r => Either Email Phone -> (AppT r) (Maybe PasswordResetPair) lookupPasswordResetCode emailOrPhone = do diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 80ba594971..ee09fcd992 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -98,6 +98,8 @@ import Brig.Sem.CodeStore (CodeStore) import Brig.Sem.CodeStore.Cassandra import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetStore.CodeStore +import Brig.Sem.PasswordResetSupply (PasswordResetSupply) +import Brig.Sem.PasswordResetSupply.IO import Brig.Team.Template import Brig.Template (Localised, TemplateBranding, forLocale, genTemplateBranding) import Brig.Types (Locale (..)) @@ -440,6 +442,7 @@ closeEnv e = do type BrigCanonicalEffects = '[ PasswordResetStore, Now, + PasswordResetSupply, CodeStore, Embed Cas.Client, Embed IO, @@ -603,6 +606,7 @@ runAppT e (AppT ma) = . embedToFinal . interpretClientToIO (_casClient e) . codeStoreToCassandra @Cas.Client + . passwordResetSupplyToIO . nowToIOAction (_currentTime e) . passwordResetStoreToCodeStore $ runReaderT ma e diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 7f20e71948..13d690ed9c 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -37,6 +37,8 @@ import Brig.Data.UserKey import Brig.Options import qualified Brig.Sem.CodeStore as E import Brig.Sem.CodeStore.Cassandra +import qualified Brig.Sem.PasswordResetSupply as E +import Brig.Sem.PasswordResetSupply.IO import Brig.Types import Brig.Types.Intra import Cassandra @@ -126,7 +128,7 @@ activateKey k c u = verifyCode k c >>= pickUser >>= activate pure . Just $ foldKey (EmailActivated uid) (PhoneActivated uid) key -- if the key is the same, we only want to update our profile | otherwise = do - lift (runM (codeStoreToCassandra @m @'[Embed m] (E.mkPasswordResetKey uid >>= E.codeDelete))) + lift (deleteCode uid) claim key uid lift $ foldKey (updateEmailAndDeleteEmailUnvalidated uid) (updatePhone uid) key for_ oldKey $ lift . deleteKey @@ -135,6 +137,17 @@ activateKey k c u = verifyCode k c >>= pickUser >>= activate updateEmailAndDeleteEmailUnvalidated :: MonadClient m => UserId -> Email -> m () updateEmailAndDeleteEmailUnvalidated u' email = updateEmail u' email <* deleteEmailUnvalidated u' + deleteCode :: UserId -> m () + deleteCode uId = + runM + -- FUTUREWORK: use the DeletePasswordResetCode action instead of CodeDelete + ( codeStoreToCassandra @m + ( embed @m + ( liftIO @m (runM (passwordResetSupplyToIO @'[Embed IO] (E.mkPasswordResetKey uId))) + ) + >>= E.codeDelete + ) + ) claim key uid = do ok <- lift $ claimKey key uid unless ok $ diff --git a/services/brig/src/Brig/Sem/CodeStore.hs b/services/brig/src/Brig/Sem/CodeStore.hs index e6f5000131..05fa4136db 100644 --- a/services/brig/src/Brig/Sem/CodeStore.hs +++ b/services/brig/src/Brig/Sem/CodeStore.hs @@ -34,9 +34,6 @@ data PRQueryData f = PRQueryData } data CodeStore m a where - MkPasswordResetKey :: UserId -> CodeStore m PasswordResetKey - GenerateEmailCode :: CodeStore m PasswordResetCode - GeneratePhoneCode :: CodeStore m PasswordResetCode CodeSelect :: PasswordResetKey -> CodeStore m (Maybe (PRQueryData Maybe)) diff --git a/services/brig/src/Brig/Sem/CodeStore/Cassandra.hs b/services/brig/src/Brig/Sem/CodeStore/Cassandra.hs index 7ebc58d057..21b21cb477 100644 --- a/services/brig/src/Brig/Sem/CodeStore/Cassandra.hs +++ b/services/brig/src/Brig/Sem/CodeStore/Cassandra.hs @@ -26,17 +26,10 @@ import Brig.Data.Instances () import Brig.Sem.CodeStore import Brig.Types import Cassandra -import Data.ByteString.Conversion (toByteString') import Data.Id -import Data.Text (pack) -import Data.Text.Ascii import Data.Time.Clock import Imports -import OpenSSL.BN (randIntegerZeroToNMinusOne) -import OpenSSL.EVP.Digest (digestBS, getDigestByName) -import OpenSSL.Random (randBytes) import Polysemy -import Text.Printf codeStoreToCassandra :: forall m r a. @@ -47,9 +40,6 @@ codeStoreToCassandra = interpret $ embed @m . \case - MkPasswordResetKey uid -> mkPwdResetKey uid - GenerateEmailCode -> genEmailCode - GeneratePhoneCode -> genPhoneCode CodeSelect prk -> (fmap . fmap) toRecord . retry x1 @@ -75,19 +65,6 @@ codeStoreToCassandra = toRecord (prqdCode, prqdUser, prqdRetries, prqdTimeout) = PRQueryData {..} -genEmailCode :: MonadIO m => m PasswordResetCode -genEmailCode = PasswordResetCode . encodeBase64Url <$> liftIO (randBytes 24) - -genPhoneCode :: MonadIO m => m PasswordResetCode -genPhoneCode = - PasswordResetCode . unsafeFromText . pack . printf "%06d" - <$> liftIO (randIntegerZeroToNMinusOne 1000000) - -mkPwdResetKey :: MonadIO m => UserId -> m PasswordResetKey -mkPwdResetKey u = do - d <- liftIO $ getDigestByName "SHA256" >>= maybe (error "SHA256 not found") pure - pure . PasswordResetKey . encodeBase64Url . digestBS d $ toByteString' u - interpretClientToIO :: Member (Final IO) r => ClientState -> diff --git a/services/brig/src/Brig/Sem/PasswordResetStore.hs b/services/brig/src/Brig/Sem/PasswordResetStore.hs index e9ea6f69d0..00c49c9414 100644 --- a/services/brig/src/Brig/Sem/PasswordResetStore.hs +++ b/services/brig/src/Brig/Sem/PasswordResetStore.hs @@ -34,5 +34,8 @@ data PasswordResetStore m a where VerifyPasswordResetCode :: PasswordResetPair -> PasswordResetStore m (Maybe UserId) + DeletePasswordResetCode :: + PasswordResetKey -> + PasswordResetStore m () makeSem ''PasswordResetStore diff --git a/services/brig/src/Brig/Sem/PasswordResetStore/CodeStore.hs b/services/brig/src/Brig/Sem/PasswordResetStore/CodeStore.hs index c509f3c3a5..19f551da12 100644 --- a/services/brig/src/Brig/Sem/PasswordResetStore/CodeStore.hs +++ b/services/brig/src/Brig/Sem/PasswordResetStore/CodeStore.hs @@ -22,6 +22,7 @@ where import Brig.Sem.CodeStore import Brig.Sem.PasswordResetStore +import Brig.Sem.PasswordResetSupply import Brig.Types import Data.Id import Data.Time @@ -32,13 +33,14 @@ import qualified Wire.Sem.Now as Now passwordResetStoreToCodeStore :: forall r a. - Members '[CodeStore, Now] r => + Members '[CodeStore, Now, PasswordResetSupply] r => Sem (PasswordResetStore ': r) a -> Sem r a passwordResetStoreToCodeStore = interpret $ \case CreatePasswordResetCode uid eEmailPhone -> create uid eEmailPhone LookupPasswordResetCode uid -> lookup uid VerifyPasswordResetCode prp -> verify prp + DeletePasswordResetCode key -> delete key maxAttempts :: Int32 maxAttempts = 3 @@ -47,7 +49,7 @@ ttl :: NominalDiffTime ttl = 3600 -- 60 minutes create :: - Members '[CodeStore, Now] r => + Members '[CodeStore, Now, PasswordResetSupply] r => UserId -> Either Email Phone -> Sem r PasswordResetPair @@ -62,7 +64,7 @@ create u target = do pure (key, code) lookup :: - Members '[CodeStore, Now] r => + Members '[CodeStore, Now, PasswordResetSupply] r => UserId -> Sem r (Maybe PasswordResetCode) lookup u = do @@ -87,3 +89,6 @@ verify (k, c) = do pure Nothing Just PRQueryData {} -> codeDelete k $> Nothing Nothing -> pure Nothing + +delete :: Member CodeStore r => PasswordResetKey -> Sem r () +delete = codeDelete diff --git a/services/brig/src/Brig/Sem/PasswordResetSupply.hs b/services/brig/src/Brig/Sem/PasswordResetSupply.hs new file mode 100644 index 0000000000..6dac550c36 --- /dev/null +++ b/services/brig/src/Brig/Sem/PasswordResetSupply.hs @@ -0,0 +1,30 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . +{-# LANGUAGE TemplateHaskell #-} + +module Brig.Sem.PasswordResetSupply where + +import Brig.Types +import Data.Id +import Polysemy + +data PasswordResetSupply m a where + MkPasswordResetKey :: UserId -> PasswordResetSupply m PasswordResetKey + GenerateEmailCode :: PasswordResetSupply m PasswordResetCode + GeneratePhoneCode :: PasswordResetSupply m PasswordResetCode + +makeSem ''PasswordResetSupply diff --git a/services/brig/src/Brig/Sem/PasswordResetSupply/IO.hs b/services/brig/src/Brig/Sem/PasswordResetSupply/IO.hs new file mode 100644 index 0000000000..761f22a35a --- /dev/null +++ b/services/brig/src/Brig/Sem/PasswordResetSupply/IO.hs @@ -0,0 +1,57 @@ +-- 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.Sem.PasswordResetSupply.IO (passwordResetSupplyToIO) where + +import Brig.Sem.PasswordResetSupply +import Brig.Types +import Data.ByteString.Conversion +import Data.Id +import Data.Text +import Data.Text.Ascii +import Imports +import OpenSSL.BN +import OpenSSL.EVP.Digest +import OpenSSL.Random +import Polysemy +import Text.Printf + +passwordResetSupplyToIO :: + forall r a. + (Member (Embed IO) r) => + Sem (PasswordResetSupply ': r) a -> + Sem r a +passwordResetSupplyToIO = + interpret $ + embed @IO + . \case + MkPasswordResetKey uid -> mkPwdResetKey uid + GenerateEmailCode -> genEmailCode + GeneratePhoneCode -> genPhoneCode + +genEmailCode :: MonadIO m => m PasswordResetCode +genEmailCode = PasswordResetCode . encodeBase64Url <$> liftIO (randBytes 24) + +genPhoneCode :: MonadIO m => m PasswordResetCode +genPhoneCode = + PasswordResetCode . unsafeFromText . pack . printf "%06d" + <$> liftIO (randIntegerZeroToNMinusOne 1000000) + +mkPwdResetKey :: MonadIO m => UserId -> m PasswordResetKey +mkPwdResetKey u = do + d <- liftIO $ getDigestByName "SHA256" >>= maybe (error "SHA256 not found") return + return . PasswordResetKey . encodeBase64Url . digestBS d $ toByteString' u diff --git a/services/brig/test/integration/API/User.hs b/services/brig/test/integration/API/User.hs index f2c41833e2..aedef5941e 100644 --- a/services/brig/test/integration/API/User.hs +++ b/services/brig/test/integration/API/User.hs @@ -67,7 +67,7 @@ tests conf fbc fgc p b c ch g n aws db = do API.User.Auth.tests conf p z db b g n, API.User.Connection.tests cl at conf p b c g fbc fgc db, API.User.Handles.tests cl at conf p b c g, - API.User.PasswordReset.tests db cl at conf p b c g, + API.User.PasswordReset.tests cl at conf p b c g, API.User.Property.tests cl at conf p b c g, API.User.RichInfo.tests cl at conf p b c g ] diff --git a/services/brig/test/integration/API/User/PasswordReset.hs b/services/brig/test/integration/API/User/PasswordReset.hs index e4c167ed42..11231178e8 100644 --- a/services/brig/test/integration/API/User/PasswordReset.hs +++ b/services/brig/test/integration/API/User/PasswordReset.hs @@ -28,14 +28,12 @@ import Bilge.Assert import qualified Brig.Options as Opt import Brig.Types import Brig.Types.User.Auth hiding (user) -import qualified Cassandra as DB import Data.Misc (PlainTextPassword (..)) import Imports import Test.Tasty hiding (Timeout) import Util tests :: - DB.ClientState -> ConnectionLimit -> Opt.Timeout -> Opt.Opts -> @@ -44,15 +42,15 @@ tests :: Cannon -> Galley -> TestTree -tests cs _cl _at _conf p b _c _g = +tests _cl _at _conf p b _c _g = testGroup "password-reset" - [ test p "post /password-reset[/complete] - 201[/200]" $ testPasswordReset b cs, - test p "post /password-reset after put /access/self/email - 400" $ testPasswordResetAfterEmailUpdate b cs + [ test p "post /password-reset[/complete] - 201[/200]" $ testPasswordReset b, + test p "post /password-reset after put /access/self/email - 400" $ testPasswordResetAfterEmailUpdate b ] -testPasswordReset :: Brig -> DB.ClientState -> Http () -testPasswordReset brig cs = do +testPasswordReset :: Brig -> Http () +testPasswordReset brig = do u <- randomUser brig let Just email = userEmail u let uid = userId u @@ -60,7 +58,7 @@ testPasswordReset brig cs = do let newpw = PlainTextPassword "newsecret" do initiatePasswordReset brig email !!! const 201 === statusCode - passwordResetData <- preparePasswordReset brig cs email uid newpw + passwordResetData <- preparePasswordReset brig email uid newpw completePasswordReset brig passwordResetData !!! const 200 === statusCode -- try login login brig (defEmailLogin email) PersistentCookie @@ -70,18 +68,18 @@ testPasswordReset brig cs = do -- reset password again to the same new password, get 400 "must be different" do initiatePasswordReset brig email !!! const 201 === statusCode - passwordResetData <- preparePasswordReset brig cs email uid newpw + passwordResetData <- preparePasswordReset brig email uid newpw completePasswordReset brig passwordResetData !!! const 409 === statusCode -testPasswordResetAfterEmailUpdate :: Brig -> DB.ClientState -> Http () -testPasswordResetAfterEmailUpdate brig cs = do +testPasswordResetAfterEmailUpdate :: Brig -> Http () +testPasswordResetAfterEmailUpdate brig = do u <- randomUser brig let uid = userId u let Just email = userEmail u eml <- randomEmail initiateEmailUpdateLogin brig eml (emailLogin email defPassword Nothing) uid !!! const 202 === statusCode initiatePasswordReset brig email !!! const 201 === statusCode - passwordResetData <- preparePasswordReset brig cs email uid (PlainTextPassword "newsecret") + passwordResetData <- preparePasswordReset brig email uid (PlainTextPassword "newsecret") -- activate new email activateEmail brig eml checkEmail brig uid eml diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 916c527260..4d11907f9e 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -23,8 +23,8 @@ import Bilge hiding (accept, timeout) import Bilge.Assert import qualified Brig.Code as Code import Brig.Options (Opts) -import Brig.Sem.CodeStore -import Brig.Sem.CodeStore.Cassandra +import Brig.Sem.PasswordResetSupply +import Brig.Sem.PasswordResetSupply.IO import Brig.Types import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.Types.User.Auth hiding (user) @@ -193,12 +193,11 @@ initiateEmailUpdateNoSend brig email uid = preparePasswordReset :: (MonadIO m, MonadHttp m) => Brig -> - DB.ClientState -> Email -> UserId -> PlainTextPassword -> m CompletePasswordReset -preparePasswordReset brig cs email uid newpw = do +preparePasswordReset brig email uid newpw = do let qry = queryItem "email" (toByteString' email) r <- get $ brig . path "/i/users/password-reset-code" . qry let lbs = fromMaybe "" $ responseBody r @@ -207,7 +206,7 @@ preparePasswordReset brig cs email uid newpw = do let complete = CompletePasswordReset ident pwcode newpw pure complete where - runSem = liftIO . runFinal @IO . interpretClientToIO cs . codeStoreToCassandra @DB.Client + runSem = liftIO . runM . passwordResetSupplyToIO @'[Embed IO] completePasswordReset :: Brig -> CompletePasswordReset -> (MonadIO m, MonadHttp m) => m ResponseLBS completePasswordReset brig passwordResetData = From b943d47824808172eac97a7666e9dcb20d9e9359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Mon, 25 Apr 2022 14:37:22 +0200 Subject: [PATCH 02/41] Add a changelog --- changelog.d/5-internal/user-effects | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5-internal/user-effects diff --git a/changelog.d/5-internal/user-effects b/changelog.d/5-internal/user-effects new file mode 100644 index 0000000000..3e6804c19d --- /dev/null +++ b/changelog.d/5-internal/user-effects @@ -0,0 +1 @@ +Add the UserQuery and supporting effects From c28c6eba0b56339c1d2547c6d7257e480c60752e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 20 May 2022 10:29:13 +0200 Subject: [PATCH 03/41] Code formatting --- services/brig/src/Brig/Sem/PasswordResetSupply/IO.hs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/services/brig/src/Brig/Sem/PasswordResetSupply/IO.hs b/services/brig/src/Brig/Sem/PasswordResetSupply/IO.hs index 761f22a35a..37ffab8074 100644 --- a/services/brig/src/Brig/Sem/PasswordResetSupply/IO.hs +++ b/services/brig/src/Brig/Sem/PasswordResetSupply/IO.hs @@ -37,11 +37,10 @@ passwordResetSupplyToIO :: Sem r a passwordResetSupplyToIO = interpret $ - embed @IO - . \case - MkPasswordResetKey uid -> mkPwdResetKey uid - GenerateEmailCode -> genEmailCode - GeneratePhoneCode -> genPhoneCode + embed @IO . \case + MkPasswordResetKey uid -> mkPwdResetKey uid + GenerateEmailCode -> genEmailCode + GeneratePhoneCode -> genPhoneCode genEmailCode :: MonadIO m => m PasswordResetCode genEmailCode = PasswordResetCode . encodeBase64Url <$> liftIO (randBytes 24) From 2a3055d95249aab5703c958e63035e303f5be31e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 26 May 2022 09:47:18 +0200 Subject: [PATCH 04/41] Add the logging effect to Brig --- services/brig/src/Brig/API.hs | 3 ++- services/brig/src/Brig/API/Public.hs | 9 +++++---- services/brig/src/Brig/API/User.hs | 6 ++++-- services/brig/src/Brig/App.hs | 4 ++++ 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index 189186815f..4df1a66bfd 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -28,9 +28,10 @@ import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import qualified Data.Swagger.Build.Api as Doc import Network.Wai.Routing (Routes) import Polysemy +import qualified Polysemy.TinyLog as P sitemap :: - Members '[PasswordResetStore, PasswordResetSupply] r => + Members '[P.TinyLog, PasswordResetStore, PasswordResetSupply] r => Routes Doc.ApiBuilder (Handler r) () sitemap = do Public.sitemap diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 6fbba7ed82..6d2ad69d81 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -98,6 +98,7 @@ import Network.Wai.Utilities.Swagger (document, mkSwaggerApi) import qualified Network.Wai.Utilities.Swagger as Doc import Network.Wai.Utilities.ZAuth (zauthUserId) import Polysemy +import qualified Polysemy.TinyLog as P import Servant hiding (Handler, JSON, addHeader, respond) import qualified Servant import Servant.Swagger.Internal.Orphans () @@ -274,7 +275,7 @@ servantSitemap = userAPI :<|> selfAPI :<|> accountAPI :<|> clientAPI :<|> prekey -- - MemberLeave event to members for all conversations the user was in (via galley) sitemap :: - Members '[PasswordResetStore, PasswordResetSupply] r => + Members '[P.TinyLog, PasswordResetStore, PasswordResetSupply] r => Routes Doc.ApiBuilder (Handler r) () sitemap = do -- User Handle API ---------------------------------------------------- @@ -438,7 +439,7 @@ sitemap = do apiDocs :: forall r. - Members '[PasswordResetStore, PasswordResetSupply] r => + Members '[P.TinyLog, PasswordResetStore, PasswordResetSupply] r => Routes Doc.ApiBuilder (Handler r) () apiDocs = get @@ -816,14 +817,14 @@ changeHandle u conn (Public.HandleUpdate h) = lift . exceptTToMaybe $ do API.changeHandle u (Just conn) handle API.ForbidSCIMUpdates beginPasswordResetH :: - Members '[PasswordResetStore] r => + Members '[P.TinyLog, PasswordResetStore] r => JSON ::: JsonRequest Public.NewPasswordReset -> (Handler r) Response beginPasswordResetH (_ ::: req) = setStatus status201 empty <$ (beginPasswordReset =<< parseJsonBody req) beginPasswordReset :: - Members '[PasswordResetStore] r => + Members '[P.TinyLog, PasswordResetStore] r => Public.NewPasswordReset -> (Handler r) () beginPasswordReset (Public.NewPasswordReset target) = do diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 8cb305dce7..2d60373afa 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -155,6 +155,7 @@ import qualified Galley.Types.Teams.Intra as Team import Imports import Network.Wai.Utilities import Polysemy +import qualified Polysemy.TinyLog as P import System.Logger.Class (MonadLogger) import qualified System.Logger.Class as Log import System.Logger.Message @@ -972,13 +973,14 @@ changePassword uid cp = do lift $ wrapClient (Data.updatePassword uid newpw) >> wrapClient (revokeAllCookies uid) beginPasswordReset :: - Members '[PasswordResetStore] r => + Members '[P.TinyLog, PasswordResetStore] r => Either Email Phone -> ExceptT PasswordResetError (AppT r) (UserId, PasswordResetPair) beginPasswordReset target = do let key = either userEmailKey userPhoneKey target user <- lift (wrapClient $ Data.lookupKey key) >>= maybe (throwE InvalidPasswordResetKey) pure - lift . Log.debug $ field "user" (toByteString user) . field "action" (Log.val "User.beginPasswordReset") + lift . liftSem . P.debug $ + field "user" (toByteString user) . field "action" (Log.val "User.beginPasswordReset") status <- lift . wrapClient $ Data.lookupStatus user unless (status == Just Active) $ throwE InvalidPasswordResetKey diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index ee09fcd992..079539b04a 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -144,6 +144,7 @@ import qualified OpenSSL.Session as SSL import qualified OpenSSL.X509.SystemStore as SSL import Polysemy import Polysemy.Final +import qualified Polysemy.TinyLog as P import qualified Ropes.Nexmo as Nexmo import qualified Ropes.Twilio as Twilio import Ssl.Util @@ -154,6 +155,7 @@ import qualified System.Logger.Class as LC import qualified System.Logger.Extended as Log import Util.Options import Wire.API.User.Identity (Email) +import Wire.Sem.Logger.TinyLog import Wire.Sem.Now (Now) import Wire.Sem.Now.IO @@ -444,6 +446,7 @@ type BrigCanonicalEffects = Now, PasswordResetSupply, CodeStore, + P.TinyLog, Embed Cas.Client, Embed IO, Final IO @@ -605,6 +608,7 @@ runAppT e (AppT ma) = runFinal . embedToFinal . interpretClientToIO (_casClient e) + . loggerToTinyLogReqId (view requestId e) (view applog e) . codeStoreToCassandra @Cas.Client . passwordResetSupplyToIO . nowToIOAction (_currentTime e) From 866dcc5a29b1a157f4334f5bb614940c45e43f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 24 May 2022 14:42:03 +0200 Subject: [PATCH 05/41] Introduce the UserQuery effect - so far only two actions implemented: getId and getUsers --- services/brig/brig.cabal | 2 + services/brig/src/Brig/API.hs | 9 +- services/brig/src/Brig/API/Internal.hs | 65 +++++-- services/brig/src/Brig/API/Public.hs | 81 ++++++-- services/brig/src/Brig/API/User.hs | 82 ++++++-- services/brig/src/Brig/App.hs | 6 +- services/brig/src/Brig/Data/User.hs | 42 ++--- services/brig/src/Brig/Provider/API.hs | 78 ++++++-- services/brig/src/Brig/Sem/UserQuery.hs | 124 ++++++++++++ .../brig/src/Brig/Sem/UserQuery/Cassandra.hs | 176 ++++++++++++++++++ services/brig/src/Brig/User/API/Auth.hs | 7 +- services/brig/src/Brig/User/EJPD.hs | 33 ++-- .../brig/test/integration/API/Internal.hs | 4 +- 13 files changed, 617 insertions(+), 92 deletions(-) create mode 100644 services/brig/src/Brig/Sem/UserQuery.hs create mode 100644 services/brig/src/Brig/Sem/UserQuery/Cassandra.hs diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 45b15b3dca..3c93430db1 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -91,6 +91,8 @@ library Brig.Sem.PasswordResetStore.CodeStore Brig.Sem.PasswordResetSupply Brig.Sem.PasswordResetSupply.IO + Brig.Sem.UserQuery + Brig.Sem.UserQuery.Cassandra Brig.SMTP Brig.Team.API Brig.Team.DB diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index 4df1a66bfd..cd25b3d5ca 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -25,13 +25,20 @@ import qualified Brig.API.Internal as Internal import qualified Brig.API.Public as Public import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetSupply (PasswordResetSupply) +import Brig.Sem.UserQuery (UserQuery) import qualified Data.Swagger.Build.Api as Doc import Network.Wai.Routing (Routes) import Polysemy import qualified Polysemy.TinyLog as P sitemap :: - Members '[P.TinyLog, PasswordResetStore, PasswordResetSupply] r => + Members + '[ P.TinyLog, + PasswordResetStore, + PasswordResetSupply, + UserQuery + ] + r => Routes Doc.ApiBuilder (Handler r) () sitemap = do Public.sitemap diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 0d2dd80b4b..1e00b1a763 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -44,6 +44,7 @@ import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Provider.API as Provider import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetSupply (PasswordResetSupply) +import Brig.Sem.UserQuery (UserQuery) import qualified Brig.Team.API as Team import Brig.Team.DB (lookupInvitationByEmail) import Brig.Types @@ -95,10 +96,14 @@ import Wire.API.User.RichInfo --------------------------------------------------------------------------- -- Sitemap (servant) -servantSitemap :: ServerT BrigIRoutes.API (Handler r) +servantSitemap :: + Member UserQuery r => + ServerT BrigIRoutes.API (Handler r) servantSitemap = ejpdAPI :<|> accountAPI :<|> mlsAPI :<|> getVerificationCode :<|> teamsAPI -ejpdAPI :: ServerT BrigIRoutes.EJPD_API (Handler r) +ejpdAPI :: + Member UserQuery r => + ServerT BrigIRoutes.EJPD_API (Handler r) ejpdAPI = Brig.User.EJPD.ejpdRequest :<|> Named @"get-account-feature-config" getAccountFeatureConfig @@ -169,9 +174,15 @@ mapKeyPackageRefsInternal bundle = do for_ (kpbEntries bundle) $ \e -> Data.mapKeyPackageRef (kpbeRef e) (kpbeUser e) (kpbeClient e) -getVerificationCode :: UserId -> VerificationAction -> (Handler r) (Maybe Code.Value) +getVerificationCode :: + Member UserQuery r => + UserId -> + VerificationAction -> + (Handler r) (Maybe Code.Value) getVerificationCode uid action = do - user <- wrapClientE $ Api.lookupUser NoPendingInvitations uid + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings + user <- lift . liftSem $ Api.lookupUser loc locale NoPendingInvitations uid maybe (pure Nothing) (lookupCode action) (userEmail =<< user) where lookupCode :: VerificationAction -> Email -> (Handler r) (Maybe Code.Value) @@ -187,7 +198,12 @@ swaggerDocsAPI = swaggerSchemaUIServer BrigIRoutes.swaggerDoc -- Sitemap (wai-route) sitemap :: - Members '[PasswordResetStore, PasswordResetSupply] r => + Members + '[ PasswordResetStore, + PasswordResetSupply, + UserQuery + ] + r => Routes a (Handler r) () sitemap = do get "/i/status" (continue $ const $ pure empty) true @@ -416,7 +432,10 @@ deleteUserNoVerify uid = do >>= ifNothing (errorToWai @'E.UserNotFound) lift $ API.deleteUserNoVerify uid -changeSelfEmailMaybeSendH :: UserId ::: Bool ::: JsonRequest EmailUpdate -> (Handler r) Response +changeSelfEmailMaybeSendH :: + Member UserQuery r => + UserId ::: Bool ::: JsonRequest EmailUpdate -> + (Handler r) Response changeSelfEmailMaybeSendH (u ::: validate ::: req) = do email <- euEmail <$> parseJsonBody req changeSelfEmailMaybeSend u (if validate then ActuallySendEmail else DoNotSendEmail) email API.AllowSCIMUpdates >>= \case @@ -425,7 +444,13 @@ changeSelfEmailMaybeSendH (u ::: validate ::: req) = do data MaybeSendEmail = ActuallySendEmail | DoNotSendEmail -changeSelfEmailMaybeSend :: UserId -> MaybeSendEmail -> Email -> API.AllowSCIMUpdates -> (Handler r) ChangeEmailResponse +changeSelfEmailMaybeSend :: + Member UserQuery r => + UserId -> + MaybeSendEmail -> + Email -> + API.AllowSCIMUpdates -> + (Handler r) ChangeEmailResponse changeSelfEmailMaybeSend u ActuallySendEmail email allowScim = do API.changeSelfEmail u email allowScim changeSelfEmailMaybeSend u DoNotSendEmail email allowScim = do @@ -642,19 +667,35 @@ getRichInfoMulti :: [UserId] -> (Handler r) [(UserId, RichInfo)] getRichInfoMulti uids = lift (wrapClient $ API.lookupRichInfoMultiUsers uids) -updateHandleH :: UserId ::: JSON ::: JsonRequest HandleUpdate -> (Handler r) Response +updateHandleH :: + Member UserQuery r => + UserId ::: JSON ::: JsonRequest HandleUpdate -> + (Handler r) Response updateHandleH (uid ::: _ ::: body) = empty <$ (updateHandle uid =<< parseJsonBody body) -updateHandle :: UserId -> HandleUpdate -> (Handler r) () +updateHandle :: + Member UserQuery r => + UserId -> + HandleUpdate -> + (Handler r) () updateHandle uid (HandleUpdate handleUpd) = do handle <- validateHandle handleUpd API.changeHandle uid Nothing handle API.AllowSCIMUpdates !>> changeHandleError -updateUserNameH :: UserId ::: JSON ::: JsonRequest NameUpdate -> (Handler r) Response +updateUserNameH :: + Member UserQuery r => + UserId ::: JSON ::: JsonRequest NameUpdate -> + (Handler r) Response updateUserNameH (uid ::: _ ::: body) = empty <$ (updateUserName uid =<< parseJsonBody body) -updateUserName :: UserId -> NameUpdate -> (Handler r) () +updateUserName :: + Member UserQuery r => + UserId -> + NameUpdate -> + (Handler r) () updateUserName uid (NameUpdate nameUpd) = do + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings name <- either (const $ throwStd (errorToWai @'E.InvalidUser)) pure $ mkName nameUpd let uu = UserUpdate @@ -663,7 +704,7 @@ updateUserName uid (NameUpdate nameUpd) = do uupAssets = Nothing, uupAccentId = Nothing } - lift (wrapClient $ Data.lookupUser WithPendingInvitations uid) >>= \case + lift (liftSem $ Data.lookupUser loc locale WithPendingInvitations uid) >>= \case Just _ -> API.updateUser uid Nothing uu API.AllowSCIMUpdates !>> updateProfileError Nothing -> throwStd (errorToWai @'E.InvalidUser) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 6d2ad69d81..4bb481b2d5 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -48,6 +48,7 @@ import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Provider.API as Provider import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetSupply (PasswordResetSupply) +import Brig.Sem.UserQuery (UserQuery) import qualified Brig.Team.API as Team import qualified Brig.Team.Email as Team import Brig.Types.Activation (ActivationPair) @@ -180,8 +181,20 @@ swaggerDocsAPI (Just V1) = $ $(embedLazyByteString =<< makeRelativeToProject "docs/swagger-v1.json") swaggerDocsAPI Nothing = swaggerDocsAPI (Just maxBound) -servantSitemap :: ServerT BrigAPI (Handler r) -servantSitemap = userAPI :<|> selfAPI :<|> accountAPI :<|> clientAPI :<|> prekeyAPI :<|> userClientAPI :<|> connectionAPI :<|> propertiesAPI :<|> mlsAPI +servantSitemap :: + forall r. + Member UserQuery r => + ServerT BrigAPI (Handler r) +servantSitemap = + userAPI + :<|> selfAPI + :<|> accountAPI + :<|> clientAPI + :<|> prekeyAPI + :<|> userClientAPI + :<|> connectionAPI + :<|> propertiesAPI + :<|> mlsAPI where userAPI :: ServerT UserAPI (Handler r) userAPI = @@ -275,7 +288,13 @@ servantSitemap = userAPI :<|> selfAPI :<|> accountAPI :<|> clientAPI :<|> prekey -- - MemberLeave event to members for all conversations the user was in (via galley) sitemap :: - Members '[P.TinyLog, PasswordResetStore, PasswordResetSupply] r => + Members + '[ P.TinyLog, + PasswordResetStore, + PasswordResetSupply, + UserQuery + ] + r => Routes Doc.ApiBuilder (Handler r) () sitemap = do -- User Handle API ---------------------------------------------------- @@ -439,7 +458,13 @@ sitemap = do apiDocs :: forall r. - Members '[P.TinyLog, PasswordResetStore, PasswordResetSupply] r => + Members + '[ P.TinyLog, + PasswordResetStore, + PasswordResetSupply, + UserQuery + ] + r => Routes Doc.ApiBuilder (Handler r) () apiDocs = get @@ -603,20 +628,29 @@ getClientCapabilities uid cid = do mclient <- lift (API.lookupLocalClient uid cid) maybe (throwStd (errorToWai @'E.ClientNotFound)) (pure . Public.clientCapabilities) mclient -getRichInfoH :: UserId ::: UserId ::: JSON -> (Handler r) Response +getRichInfoH :: + Member UserQuery r => + UserId ::: UserId ::: JSON -> + (Handler r) Response getRichInfoH (self ::: user ::: _) = json <$> getRichInfo self user -getRichInfo :: UserId -> UserId -> (Handler r) Public.RichInfoAssocList +getRichInfo :: + Member UserQuery r => + UserId -> + UserId -> + (Handler r) Public.RichInfoAssocList getRichInfo self user = do + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings -- Check that both users exist and the requesting user is allowed to see rich info of the -- other user selfUser <- ifNothing (errorToWai @'E.UserNotFound) - =<< lift (wrapClient $ Data.lookupUser NoPendingInvitations self) + =<< lift (liftSem $ Data.lookupUser loc locale NoPendingInvitations self) otherUser <- ifNothing (errorToWai @'E.UserNotFound) - =<< lift (wrapClient $ Data.lookupUser NoPendingInvitations user) + =<< lift (liftSem $ Data.lookupUser loc locale NoPendingInvitations user) case (Public.userTeam selfUser, Public.userTeam otherUser) of (Just t1, Just t2) | t1 == t2 -> pure () _ -> throwStd insufficientTeamPermissions @@ -756,7 +790,12 @@ newtype GetActivationCodeResp instance ToJSON GetActivationCodeResp where toJSON (GetActivationCodeResp (k, c)) = object ["key" .= k, "code" .= c] -updateUser :: UserId -> ConnId -> Public.UserUpdate -> (Handler r) (Maybe Public.UpdateProfileError) +updateUser :: + Member UserQuery r => + UserId -> + ConnId -> + Public.UserUpdate -> + (Handler r) (Maybe Public.UpdateProfileError) updateUser uid conn uu = do eithErr <- lift $ runExceptT $ API.updateUser uid (Just conn) uu API.ForbidSCIMUpdates pure $ either Just (const Nothing) eithErr @@ -811,7 +850,12 @@ getHandleInfoUnqualifiedH self handle = do Public.UserHandleInfo . Public.profileQualifiedId <$$> Handle.getHandleInfo self (Qualified handle domain) -changeHandle :: UserId -> ConnId -> Public.HandleUpdate -> (Handler r) (Maybe Public.ChangeHandleError) +changeHandle :: + Member UserQuery r => + UserId -> + ConnId -> + Public.HandleUpdate -> + (Handler r) (Maybe Public.ChangeHandleError) changeHandle u conn (Public.HandleUpdate h) = lift . exceptTToMaybe $ do handle <- maybe (throwError Public.ChangeHandleInvalid) pure $ parseHandle h API.changeHandle u (Just conn) handle API.ForbidSCIMUpdates @@ -844,13 +888,19 @@ completePasswordResetH (_ ::: req) = do API.completePasswordReset cpwrIdent cpwrCode cpwrPassword !>> pwResetError pure empty -sendActivationCodeH :: JsonRequest Public.SendActivationCode -> (Handler r) Response +sendActivationCodeH :: + Member UserQuery r => + JsonRequest Public.SendActivationCode -> + (Handler r) Response sendActivationCodeH req = empty <$ (sendActivationCode =<< parseJsonBody req) -- docs/reference/user/activation.md {#RefActivationRequest} -- docs/reference/user/registration.md {#RefRegistration} -sendActivationCode :: Public.SendActivationCode -> (Handler r) () +sendActivationCode :: + Member UserQuery r => + Public.SendActivationCode -> + (Handler r) () sendActivationCode Public.SendActivationCode {..} = do either customerExtensionCheckBlockedDomains (const $ pure ()) saUserKey checkWhitelist saUserKey @@ -965,7 +1015,12 @@ verifyDeleteUserH (r ::: _) = do API.verifyDeleteUser body !>> deleteUserError pure (setStatus status200 empty) -updateUserEmail :: UserId -> UserId -> Public.EmailUpdate -> (Handler r) () +updateUserEmail :: + Member UserQuery r => + UserId -> + UserId -> + Public.EmailUpdate -> + (Handler r) () updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do maybeZuserTeamId <- lift $ wrapClient $ Data.lookupUserTeam zuserId whenM (not <$> assertHasPerm maybeZuserTeamId) $ throwStd insufficientTeamPermissions diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 2d60373afa..281d6fee3c 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -118,6 +118,8 @@ import Brig.Sem.PasswordResetStore (PasswordResetStore) import qualified Brig.Sem.PasswordResetStore as E import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import qualified Brig.Sem.PasswordResetSupply as E +import Brig.Sem.UserQuery (UserQuery) +import Brig.Sem.UserQuery.Cassandra import qualified Brig.Team.DB as Team import Brig.Types import Brig.Types.Code (Timeout (..)) @@ -479,10 +481,18 @@ checkRestrictedUserCreation new = do ------------------------------------------------------------------------------- -- Update Profile -updateUser :: UserId -> Maybe ConnId -> UserUpdate -> AllowSCIMUpdates -> ExceptT UpdateProfileError (AppT r) () +updateUser :: + Member UserQuery r => + UserId -> + Maybe ConnId -> + UserUpdate -> + AllowSCIMUpdates -> + ExceptT UpdateProfileError (AppT r) () updateUser uid mconn uu allowScim = do + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings for_ (uupName uu) $ \newName -> do - mbUser <- lift . wrapClient $ Data.lookupUser WithPendingInvitations uid + mbUser <- lift . liftSem $ Data.lookupUser loc locale WithPendingInvitations uid user <- maybe (throwE ProfileNotFound) pure mbUser unless ( userManagedBy user /= ManagedByScim @@ -513,11 +523,19 @@ changeManagedBy uid conn (ManagedByUpdate mb) = do -------------------------------------------------------------------------------- -- Change Handle -changeHandle :: UserId -> Maybe ConnId -> Handle -> AllowSCIMUpdates -> ExceptT ChangeHandleError (AppT r) () +changeHandle :: + Member UserQuery r => + UserId -> + Maybe ConnId -> + Handle -> + AllowSCIMUpdates -> + ExceptT ChangeHandleError (AppT r) () changeHandle uid mconn hdl allowScim = do when (isBlacklistedHandle hdl) $ throwE ChangeHandleInvalid - usr <- lift $ wrapClient $ Data.lookupUser WithPendingInvitations uid + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings + usr <- lift . liftSem $ Data.lookupUser loc locale WithPendingInvitations uid case usr of Nothing -> throwE ChangeHandleNoIdentity Just u -> do @@ -586,7 +604,12 @@ checkHandles check num = reverse <$> collectFree [] check num -- | Call 'changeEmail' and process result: if email changes to itself, succeed, if not, send -- validation email. -changeSelfEmail :: UserId -> Email -> AllowSCIMUpdates -> ExceptT Error.Error (AppT r) ChangeEmailResponse +changeSelfEmail :: + Member UserQuery r => + UserId -> + Email -> + AllowSCIMUpdates -> + ExceptT Error.Error (AppT r) ChangeEmailResponse changeSelfEmail u email allowScim = do changeEmail u email allowScim !>> Error.changeEmailError >>= \case ChangeEmailIdempotent -> @@ -606,7 +629,12 @@ changeSelfEmail u email allowScim = do (userIdentity usr) -- | Prepare changing the email (checking a number of invariants). -changeEmail :: UserId -> Email -> AllowSCIMUpdates -> ExceptT ChangeEmailError (AppT r) ChangeEmailResult +changeEmail :: + Member UserQuery r => + UserId -> + Email -> + AllowSCIMUpdates -> + ExceptT ChangeEmailError (AppT r) ChangeEmailResult changeEmail u email allowScim = do em <- either @@ -621,7 +649,11 @@ changeEmail u email allowScim = do unless available $ throwE $ EmailExists email - usr <- maybe (throwM $ UserProfileNotFound u) pure =<< lift (wrapClient $ Data.lookupUser WithPendingInvitations u) + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings + usr <- + maybe (throwM $ UserProfileNotFound u) pure + =<< lift (liftSem $ Data.lookupUser loc locale WithPendingInvitations u) case emailIdentity =<< userIdentity usr of -- The user already has an email address and the new one is exactly the same Just current | current == em -> pure ChangeEmailIdempotent @@ -771,7 +803,7 @@ changeSingleAccountStatus :: AccountStatus -> ExceptT AccountStatusError m () changeSingleAccountStatus uid status = do - unlessM (Data.userExists uid) $ throwE AccountNotFound + unlessM (lift $ runM $ userQueryToCassandra @m @'[Embed m] $ Data.userExists uid) $ throwE AccountNotFound ev <- mkUserEvent (List1.singleton uid) status lift $ do Data.updateStatus uid status @@ -853,7 +885,12 @@ onActivated (PhoneActivated uid phone) = do pure (uid, Just (PhoneIdentity phone), False) -- docs/reference/user/activation.md {#RefActivationRequest} -sendActivationCode :: Either Email Phone -> Maybe Locale -> Bool -> ExceptT SendActivationCodeError (AppT r) () +sendActivationCode :: + Member UserQuery r => + Either Email Phone -> + Maybe Locale -> + Bool -> + ExceptT SendActivationCodeError (AppT r) () sendActivationCode emailOrPhone loc call = case emailOrPhone of Left email -> do ek <- @@ -916,7 +953,11 @@ sendActivationCode emailOrPhone loc call = case emailOrPhone of sendActivationEmail ek uc uid = do -- FUTUREWORK(fisx): we allow for 'PendingInvitations' here, but I'm not sure this -- top-level function isn't another piece of a deprecated onboarding flow? - u <- maybe (notFound uid) pure =<< lift (wrapClient $ Data.lookupUser WithPendingInvitations uid) + locu <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings + u <- + maybe (notFound uid) pure + =<< lift (liftSem $ Data.lookupUser locu locale WithPendingInvitations uid) p <- wrapClientE $ mkPair ek (Just uc) (Just uid) let ident = userIdentity u name = userDisplayName u @@ -1334,7 +1375,14 @@ lookupLocalProfiles :: [UserId] -> m [UserProfile] lookupLocalProfiles requestingUser others = do - users <- Data.lookupUsers NoPendingInvitations others >>= mapM userGC + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings + users <- + runM + ( userQueryToCassandra @m @'[Embed m] + (Data.lookupUsers loc locale NoPendingInvitations others) + ) + >>= mapM userGC css <- case requestingUser of Just localReqUser -> toMap <$> Data.lookupConnectionStatus (map userId users) [localReqUser] Nothing -> pure mempty @@ -1342,7 +1390,7 @@ lookupLocalProfiles requestingUser others = do emailVisibility'' <- case emailVisibility' of EmailVisibleIfOnTeam -> pure EmailVisibleIfOnTeam' EmailVisibleIfOnSameTeam -> case requestingUser of - Just localReqUser -> EmailVisibleIfOnSameTeam' <$> getSelfInfo localReqUser + Just localReqUser -> EmailVisibleIfOnSameTeam' <$> getSelfInfo loc locale localReqUser Nothing -> pure EmailVisibleToSelf' EmailVisibleToSelf -> pure EmailVisibleToSelf' usersAndStatus <- for users $ \u -> (u,) <$> getLegalHoldStatus' u @@ -1351,12 +1399,16 @@ lookupLocalProfiles requestingUser others = do toMap :: [ConnectionStatus] -> Map UserId Relation toMap = Map.fromList . map (csFrom &&& csStatus) - getSelfInfo :: UserId -> m (Maybe (TeamId, Team.TeamMember)) - getSelfInfo selfId = do + getSelfInfo :: Local x -> Locale -> UserId -> m (Maybe (TeamId, Team.TeamMember)) + getSelfInfo loc locale selfId = do -- FUTUREWORK: it is an internal error for the two lookups (for 'User' and 'TeamMember') -- to return 'Nothing'. we could throw errors here if that happens, rather than just -- returning an empty profile list from 'lookupProfiles'. - mUser <- Data.lookupUser NoPendingInvitations selfId + mUser <- + runM + ( userQueryToCassandra @m @'[Embed m] + (Data.lookupUser loc locale NoPendingInvitations selfId) + ) case userTeam =<< mUser of Nothing -> pure Nothing Just tid -> (tid,) <$$> Intra.getTeamMember selfId tid diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 079539b04a..29c804a72d 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -100,6 +100,8 @@ import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetStore.CodeStore import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import Brig.Sem.PasswordResetSupply.IO +import Brig.Sem.UserQuery (UserQuery) +import Brig.Sem.UserQuery.Cassandra import Brig.Team.Template import Brig.Template (Localised, TemplateBranding, forLocale, genTemplateBranding) import Brig.Types (Locale (..)) @@ -442,7 +444,8 @@ closeEnv e = do -- App Monad type BrigCanonicalEffects = - '[ PasswordResetStore, + '[ UserQuery, + PasswordResetStore, Now, PasswordResetSupply, CodeStore, @@ -613,6 +616,7 @@ runAppT e (AppT ma) = . passwordResetSupplyToIO . nowToIOAction (_currentTime e) . passwordResetStoreToCodeStore + . userQueryToCassandra @Cas.Client $ runReaderT ma e locationOf :: (MonadIO m, MonadReader Env m) => IP -> m (Maybe Location) diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 10fe01409d..9175f36aa1 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -78,6 +78,7 @@ import Brig.App (Env, currentTime, settings, viewFederationDomain, zauthEnv) import Brig.Data.Instances () import Brig.Options import Brig.Password +import Brig.Sem.UserQuery (UserQuery, getId, getUsers) import Brig.Types import Brig.Types.Intra import qualified Brig.ZAuth as ZAuth @@ -96,6 +97,7 @@ import Data.Time (addUTCTime) import Data.UUID.V4 import Galley.Types.Bot import Imports +import Polysemy import qualified Wire.API.Team.Feature as ApiFt import Wire.API.User.RichInfo @@ -362,8 +364,8 @@ updateStatus :: MonadClient m => UserId -> AccountStatus -> m () updateStatus u s = retry x5 $ write userStatusUpdate (params LocalQuorum (s, u)) -userExists :: MonadClient m => UserId -> m Bool -userExists uid = isJust <$> retry x1 (query1 idSelect (params LocalQuorum (Identity uid))) +userExists :: Member UserQuery r => UserId -> Sem r Bool +userExists uid = isJust <$> getId uid -- | Whether the account has been activated by verifying -- an email address or phone number. @@ -381,8 +383,14 @@ filterActive us = isActiveUser (_, True, Just Active) = True isActiveUser _ = False -lookupUser :: (MonadClient m, MonadReader Env m) => HavePendingInvitations -> UserId -> m (Maybe User) -lookupUser hpi u = listToMaybe <$> lookupUsers hpi [u] +lookupUser :: + Member UserQuery r => + Local x -> + Locale -> + HavePendingInvitations -> + UserId -> + Sem r (Maybe User) +lookupUser loc locale hpi u = listToMaybe <$> lookupUsers loc locale hpi [u] activateUser :: MonadClient m => UserId -> UserIdentity -> m () activateUser u ident = do @@ -441,11 +449,15 @@ lookupAuth u = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Ident -- | Return users with given IDs. -- -- Skips nonexistent users. /Does not/ skip users who have been deleted. -lookupUsers :: (MonadClient m, MonadReader Env m) => HavePendingInvitations -> [UserId] -> m [User] -lookupUsers hpi usrs = do - loc <- setDefaultUserLocale <$> view settings - domain <- viewFederationDomain - toUsers domain loc hpi <$> retry x1 (query usersSelect (params LocalQuorum (Identity usrs))) +lookupUsers :: + Member UserQuery r => + Local x -> + Locale -> + HavePendingInvitations -> + [UserId] -> + Sem r [User] +lookupUsers loc locale hpi usrs = + toUsers (tDomain loc) locale hpi <$> getUsers usrs lookupAccount :: (MonadClient m, MonadReader Env m) => UserId -> m (Maybe UserAccount) lookupAccount u = listToMaybe <$> lookupAccounts [u] @@ -549,8 +561,6 @@ type UserRowInsert = ManagedBy ) -deriving instance Show UserRowInsert - -- Represents a 'UserAccount' type AccountRow = ( UserId, @@ -573,16 +583,6 @@ type AccountRow = Maybe ManagedBy ) -usersSelect :: PrepQuery R (Identity [UserId]) UserRow -usersSelect = - "SELECT id, name, picture, email, phone, sso_id, accent_id, assets, \ - \activated, status, expires, language, country, provider, service, \ - \handle, team, managed_by \ - \FROM user where id IN ?" - -idSelect :: PrepQuery R (Identity UserId) (Identity UserId) -idSelect = "SELECT id FROM user WHERE id = ?" - nameSelect :: PrepQuery R (Identity UserId) (Identity Name) nameSelect = "SELECT name FROM user WHERE id = ?" diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 8a8f96c561..9534cdddc7 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -38,7 +38,7 @@ import qualified Brig.Data.User as User import Brig.Email (mkEmailKey, validateEmail) import qualified Brig.IO.Intra as RPC import qualified Brig.InternalEvent.Types as Internal -import Brig.Options (Settings (..)) +import Brig.Options (Settings (..), setDefaultUserLocale) import qualified Brig.Options as Opt import Brig.Password import Brig.Provider.DB (ServiceConn (..)) @@ -46,6 +46,8 @@ import qualified Brig.Provider.DB as DB import Brig.Provider.Email import qualified Brig.Provider.RPC as RPC import qualified Brig.Queue as Queue +import Brig.Sem.UserQuery (UserQuery) +import Brig.Sem.UserQuery.Cassandra import Brig.Team.Util import Brig.Types.Client (Client (..), ClientType (..), newClient, newClientPrekeys) import Brig.Types.Intra (AccountStatus (..), UserAccount (..)) @@ -97,6 +99,7 @@ import qualified OpenSSL.EVP.PKey as SSL import qualified OpenSSL.PEM as SSL import qualified OpenSSL.RSA as SSL import OpenSSL.Random (randBytes) +import Polysemy import qualified Ssl.Util as SSL import System.Logger.Class (MonadLogger) import UnliftIO.Async (pooledMapConcurrentlyN_) @@ -116,7 +119,9 @@ import qualified Wire.API.User.Client as Public (Client, ClientCapability (Clien import qualified Wire.API.User.Client.Prekey as Public (PrekeyId) import qualified Wire.API.User.Identity as Public (Email) -routesPublic :: Routes Doc.ApiBuilder (Handler r) () +routesPublic :: + Member UserQuery r => + Routes Doc.ApiBuilder (Handler r) () routesPublic = do -- Public API (Unauthenticated) -------------------------------------------- @@ -878,14 +883,25 @@ updateServiceWhitelist uid con tid upd = do wrapClientE $ DB.deleteServiceWhitelist (Just tid) pid sid pure UpdateServiceWhitelistRespChanged -addBotH :: UserId ::: ConnId ::: ConvId ::: JsonRequest Public.AddBot -> (Handler r) Response +addBotH :: + Member UserQuery r => + UserId ::: ConnId ::: ConvId ::: JsonRequest Public.AddBot -> + (Handler r) Response addBotH (zuid ::: zcon ::: cid ::: req) = do mapExceptT wrapHttp $ guardSecondFactorDisabled (Just zuid) setStatus status201 . json <$> (addBot zuid zcon cid =<< parseJsonBody req) -addBot :: UserId -> ConnId -> ConvId -> Public.AddBot -> (Handler r) Public.AddBotResponse +addBot :: + Member UserQuery r => + UserId -> + ConnId -> + ConvId -> + Public.AddBot -> + (Handler r) Public.AddBotResponse addBot zuid zcon cid add = do - zusr <- lift (wrapClient $ User.lookupUser NoPendingInvitations zuid) >>= maybeInvalidUser + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings + zusr <- lift (liftSem $ User.lookupUser loc locale NoPendingInvitations zuid) >>= maybeInvalidUser let pid = addBotProvider add let sid = addBotService add -- Get the conversation and check preconditions @@ -924,7 +940,6 @@ addBot zuid zcon cid add = do let botReq = Ext.NewBotRequest bid bcl busr bcnv btk bloc rs <- RPC.createBot scon botReq !>> StdError . serviceError -- Insert the bot user and client - locale <- Opt.setDefaultUserLocale <$> view settings let name = fromMaybe (serviceProfileName svp) (Ext.rsNewBotName rs) let assets = fromMaybe (serviceProfileAssets svp) (Ext.rsNewBotAssets rs) let colour = fromMaybe defaultAccentId (Ext.rsNewBotColour rs) @@ -980,14 +995,22 @@ removeBot zusr zcon cid bid = do -------------------------------------------------------------------------------- -- Bot API -botGetSelfH :: BotId -> (Handler r) Response +botGetSelfH :: + Member UserQuery r => + BotId -> + (Handler r) Response botGetSelfH bot = do mapExceptT wrapHttp $ guardSecondFactorDisabled (Just (botUserId bot)) json <$> botGetSelf bot -botGetSelf :: BotId -> (Handler r) Public.UserProfile +botGetSelf :: + Member UserQuery r => + BotId -> + (Handler r) Public.UserProfile botGetSelf bot = do - p <- lift $ wrapClient $ User.lookupUser NoPendingInvitations (botUserId bot) + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings + p <- lift . liftSem $ User.lookupUser loc locale NoPendingInvitations (botUserId bot) maybe (throwStd (errorToWai @'UserNotFound)) (pure . (`Public.publicProfile` UserLegalHoldNoConsent)) p botGetClientH :: BotId -> (Handler r) Response @@ -1037,14 +1060,22 @@ botClaimUsersPrekeys body = do throwStd (errorToWai @'TooManyClients) Client.claimLocalMultiPrekeyBundles UnprotectedBot body !>> clientError -botListUserProfilesH :: List UserId -> (Handler r) Response +botListUserProfilesH :: + Member UserQuery r => + List UserId -> + (Handler r) Response botListUserProfilesH uids = do mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing -- should we check all user ids? json <$> botListUserProfiles uids -botListUserProfiles :: List UserId -> (Handler r) [Public.BotUserView] +botListUserProfiles :: + Member UserQuery r => + List UserId -> + (Handler r) [Public.BotUserView] botListUserProfiles uids = do - us <- lift . wrapClient $ User.lookupUsers NoPendingInvitations (fromList uids) + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings + us <- lift . liftSem $ User.lookupUsers loc locale NoPendingInvitations (fromList uids) pure (map mkBotUserView us) botGetUserClientsH :: UserId -> (Handler r) Response @@ -1058,15 +1089,24 @@ botGetUserClients uid = where pubClient c = Public.PubClient (clientId c) (clientClass c) -botDeleteSelfH :: BotId ::: ConvId -> (Handler r) Response +botDeleteSelfH :: + Member UserQuery r => + BotId ::: ConvId -> + (Handler r) Response botDeleteSelfH (bid ::: cid) = do mapExceptT wrapHttp $ guardSecondFactorDisabled (Just (botUserId bid)) empty <$ botDeleteSelf bid cid -botDeleteSelf :: BotId -> ConvId -> (Handler r) () +botDeleteSelf :: + Member UserQuery r => + BotId -> + ConvId -> + (Handler r) () botDeleteSelf bid cid = do + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings mapExceptT wrapHttp $ guardSecondFactorDisabled (Just (botUserId bid)) - bot <- lift . wrapClient $ User.lookupUser NoPendingInvitations (botUserId bid) + bot <- lift . liftSem $ User.lookupUser loc locale NoPendingInvitations (botUserId bid) _ <- maybeInvalidBot (userService =<< bot) _ <- lift $ wrapHttpClient $ deleteBot (botUserId bid) Nothing bid cid pure () @@ -1102,6 +1142,7 @@ activate pid old new = do wrapClientE $ DB.insertKey pid (mkEmailKey <$> old) emailKey deleteBot :: + forall m. ( MonadHttp m, MonadReader Env m, MonadIO m, @@ -1120,7 +1161,12 @@ deleteBot zusr zcon bid cid = do ev <- RPC.removeBotMember zusr zcon cid bid -- Delete the bot user and client let buid = botUserId bid - mbUser <- User.lookupUser NoPendingInvitations buid + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings + mbUser <- + runM $ + userQueryToCassandra @m @'[Embed m] $ + User.lookupUser loc locale NoPendingInvitations buid User.lookupClients buid >>= mapM_ (User.rmClient buid . clientId) for_ (userService =<< mbUser) $ \sref -> do let pid = sref ^. serviceRefProvider diff --git a/services/brig/src/Brig/Sem/UserQuery.hs b/services/brig/src/Brig/Sem/UserQuery.hs new file mode 100644 index 0000000000..1db79e5d35 --- /dev/null +++ b/services/brig/src/Brig/Sem/UserQuery.hs @@ -0,0 +1,124 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Brig.Sem.UserQuery where + +import Brig.Password +import Brig.Types +import Brig.Types.Intra +import Data.Handle +import Data.Id +import Data.Json.Util +import Imports +import Polysemy + +type Activated = Bool + +type UserRow = + ( UserId, + Name, + Maybe Pict, + Maybe Email, + Maybe Phone, + Maybe UserSSOId, + ColourId, + Maybe [Asset], + Activated, + Maybe AccountStatus, + Maybe UTCTimeMillis, + Maybe Language, + Maybe Country, + Maybe ProviderId, + Maybe ServiceId, + Maybe Handle, + Maybe TeamId, + Maybe ManagedBy + ) + +-- Represents a 'UserAccount' +type AccountRow = + ( UserId, + Name, + Maybe Pict, + Maybe Email, + Maybe Phone, + Maybe UserSSOId, + ColourId, + Maybe [Asset], + Activated, + Maybe AccountStatus, + Maybe UTCTimeMillis, + Maybe Language, + Maybe Country, + Maybe ProviderId, + Maybe ServiceId, + Maybe Handle, + Maybe TeamId, + Maybe ManagedBy + ) + +type UserRowInsert = + ( UserId, + Name, + Pict, + [Asset], + Maybe Email, + Maybe Phone, + Maybe UserSSOId, + ColourId, + Maybe Password, + Activated, + AccountStatus, + Maybe UTCTimeMillis, + Language, + Maybe Country, + Maybe ProviderId, + Maybe ServiceId, + Maybe Handle, + Maybe TeamId, + ManagedBy + ) + +deriving instance Show UserRowInsert + +data UserQuery m a where + GetId :: UserId -> UserQuery m (Maybe UserId) -- idSelect + GetUsers :: [UserId] -> UserQuery m [UserRow] -- usersSelect + GetName :: UserId -> UserQuery m (Maybe Name) -- nameSelect + GetLocale :: UserId -> UserQuery m (Maybe (Maybe Language, Maybe Country)) -- localeSelect + GetAuthentication :: UserId -> UserQuery m (Maybe (Maybe Password, Maybe AccountStatus)) -- authSelect + GetPassword :: UserId -> UserQuery m (Maybe Password) -- passwordSelect + GetActivated :: UserId -> UserQuery m Bool -- activatedSelect + GetAccountStatus :: UserId -> UserQuery m (Maybe AccountStatus) -- statusSelect + GetAccountStatuses :: [UserId] -> UserQuery m [(UserId, Bool, Maybe AccountStatus)] -- accountStateSelectAll + GetTeam :: UserId -> UserQuery m (Maybe TeamId) -- teamSelect + GetAccounts :: [UserId] -> UserQuery m [AccountRow] -- accountsSelect + -- FUTUREWORK: The 'InsertAccount' action should perhaps be in an account store effect + InsertAccount :: + UserAccount -> + -- | If a bot: conversation and team + -- (if a team conversation) + Maybe (ConvId, Maybe TeamId) -> + Maybe Password -> + -- | Whether the user is activated + Bool -> + UserQuery m () + UpdateUser :: UserId -> UserUpdate -> UserQuery m () + +makeSem ''UserQuery diff --git a/services/brig/src/Brig/Sem/UserQuery/Cassandra.hs b/services/brig/src/Brig/Sem/UserQuery/Cassandra.hs new file mode 100644 index 0000000000..abace2098a --- /dev/null +++ b/services/brig/src/Brig/Sem/UserQuery/Cassandra.hs @@ -0,0 +1,176 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . +{-# LANGUAGE RecordWildCards #-} + +module Brig.Sem.UserQuery.Cassandra (userQueryToCassandra) where + +import Brig.Data.Instances () +import Brig.Password +import Brig.Sem.UserQuery +import Brig.Types +import Brig.Types.Intra +import Cassandra +import Control.Lens (view, (^.)) +import Data.Id +import Imports +import Polysemy +import Wire.API.Provider.Service + +userQueryToCassandra :: + forall m r a. + (MonadClient m, Member (Embed m) r) => + Sem (UserQuery ': r) a -> + Sem r a +userQueryToCassandra = + interpret $ + embed @m . \case + GetId uid -> runIdentity <$$> retry x1 (query1 idSelect (params LocalQuorum (Identity uid))) + GetUsers uids -> retry x1 (query usersSelect (params LocalQuorum (Identity uids))) + GetName uid -> runIdentity <$$> retry x1 (query1 nameSelect (params LocalQuorum (Identity uid))) + GetLocale uid -> retry x1 (query1 localeSelect (params LocalQuorum (Identity uid))) + GetAuthentication uid -> retry x1 (query1 authSelect (params LocalQuorum (Identity uid))) + GetPassword uid -> (runIdentity =<<) <$> retry x1 (query1 passwordSelect (params LocalQuorum (Identity uid))) + GetActivated uid -> (== Just (Identity True)) <$> retry x1 (query1 activatedSelect (params LocalQuorum (Identity uid))) + GetAccountStatus uid -> (runIdentity =<<) <$> retry x1 (query1 statusSelect (params LocalQuorum (Identity uid))) + GetAccountStatuses uids -> retry x1 (query accountStateSelectAll (params LocalQuorum (Identity uids))) + GetTeam uid -> (runIdentity =<<) <$> retry x1 (query1 teamSelect (params LocalQuorum (Identity uid))) + GetAccounts uids -> retry x1 (query accountsSelect (params LocalQuorum (Identity uids))) + InsertAccount ua mConvTeam mPass act -> accountInsert ua mConvTeam mPass act + UpdateUser uid update -> userUpdate uid update + +-------------------------------------------------------------------------------- +-- Queries + +idSelect :: PrepQuery R (Identity UserId) (Identity UserId) +idSelect = "SELECT id FROM user WHERE id = ?" + +usersSelect :: PrepQuery R (Identity [UserId]) UserRow +usersSelect = + "SELECT id, name, picture, email, phone, sso_id, accent_id, assets, \ + \activated, status, expires, language, country, provider, service, \ + \handle, team, managed_by \ + \FROM user where id IN ?" + +nameSelect :: PrepQuery R (Identity UserId) (Identity Name) +nameSelect = "SELECT name FROM user WHERE id = ?" + +localeSelect :: PrepQuery R (Identity UserId) (Maybe Language, Maybe Country) +localeSelect = "SELECT language, country FROM user WHERE id = ?" + +authSelect :: PrepQuery R (Identity UserId) (Maybe Password, Maybe AccountStatus) +authSelect = "SELECT password, status FROM user WHERE id = ?" + +passwordSelect :: PrepQuery R (Identity UserId) (Identity (Maybe Password)) +passwordSelect = "SELECT password FROM user WHERE id = ?" + +activatedSelect :: PrepQuery R (Identity UserId) (Identity Bool) +activatedSelect = "SELECT activated FROM user WHERE id = ?" + +statusSelect :: PrepQuery R (Identity UserId) (Identity (Maybe AccountStatus)) +statusSelect = "SELECT status FROM user WHERE id = ?" + +accountStateSelectAll :: PrepQuery R (Identity [UserId]) (UserId, Bool, Maybe AccountStatus) +accountStateSelectAll = "SELECT id, activated, status FROM user WHERE id IN ?" + +teamSelect :: PrepQuery R (Identity UserId) (Identity (Maybe TeamId)) +teamSelect = "SELECT team FROM user WHERE id = ?" + +accountsSelect :: PrepQuery R (Identity [UserId]) AccountRow +accountsSelect = + "SELECT id, name, picture, email, phone, sso_id, accent_id, assets, \ + \activated, status, expires, language, country, provider, \ + \service, handle, team, managed_by \ + \FROM user WHERE id IN ?" + +accountInsert :: + MonadClient m => + UserAccount -> + -- | If a bot: conversation and team + -- (if a team conversation) + Maybe (ConvId, Maybe TeamId) -> + Maybe Password -> + -- | Whether the user is activated + Bool -> + m () +accountInsert (UserAccount u status) mbConv password activated = retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + let Locale l c = userLocale u + addPrepQuery + userInsert + ( userId u, + userDisplayName u, + userPict u, + userAssets u, + userEmail u, + userPhone u, + userSSOId u, + userAccentId u, + password, + activated, + status, + userExpire u, + l, + c, + view serviceRefProvider <$> userService u, + view serviceRefId <$> userService u, + userHandle u, + userTeam u, + userManagedBy u + ) + for_ ((,) <$> userService u <*> mbConv) $ \(sref, (cid, mbTid)) -> do + let pid = sref ^. serviceRefProvider + sid = sref ^. serviceRefId + addPrepQuery cqlServiceUser (pid, sid, BotId (userId u), cid, mbTid) + for_ mbTid $ \tid -> + addPrepQuery cqlServiceTeam (pid, sid, BotId (userId u), cid, tid) + where + cqlServiceUser :: PrepQuery W (ProviderId, ServiceId, BotId, ConvId, Maybe TeamId) () + cqlServiceUser = + "INSERT INTO service_user (provider, service, user, conv, team) \ + \VALUES (?, ?, ?, ?, ?)" + cqlServiceTeam :: PrepQuery W (ProviderId, ServiceId, BotId, ConvId, TeamId) () + cqlServiceTeam = + "INSERT INTO service_team (provider, service, user, conv, team) \ + \VALUES (?, ?, ?, ?, ?)" + userInsert :: PrepQuery W UserRowInsert () + userInsert = + "INSERT INTO user (id, name, picture, assets, email, phone, sso_id, \ + \accent_id, password, activated, status, expires, language, \ + \country, provider, service, handle, team, managed_by) \ + \VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + +userUpdate :: MonadClient m => UserId -> UserUpdate -> m () +userUpdate u UserUpdate {..} = retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + for_ uupName $ \n -> addPrepQuery userDisplayNameUpdate (n, u) + for_ uupPict $ \p -> addPrepQuery userPictUpdate (p, u) + for_ uupAssets $ \a -> addPrepQuery userAssetsUpdate (a, u) + for_ uupAccentId $ \c -> addPrepQuery userAccentIdUpdate (c, u) + where + userDisplayNameUpdate :: PrepQuery W (Name, UserId) () + userDisplayNameUpdate = "UPDATE user SET name = ? WHERE id = ?" + + userPictUpdate :: PrepQuery W (Pict, UserId) () + userPictUpdate = "UPDATE user SET picture = ? WHERE id = ?" + + userAssetsUpdate :: PrepQuery W ([Asset], UserId) () + userAssetsUpdate = "UPDATE user SET assets = ? WHERE id = ?" + + userAccentIdUpdate :: PrepQuery W (ColourId, UserId) () + userAccentIdUpdate = "UPDATE user SET accent_id = ? WHERE id = ?" diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index daa60d91f2..591b8719b6 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -27,6 +27,7 @@ import Brig.API.Types import qualified Brig.API.User as User import Brig.App import Brig.Phone +import Brig.Sem.UserQuery (UserQuery) import Brig.Types.Intra (ReAuthUser, reAuthCode, reAuthCodeAction, reAuthPassword) import Brig.Types.User.Auth import qualified Brig.User.Auth as Auth @@ -57,13 +58,16 @@ import Network.Wai.Utilities.Response (empty, json) import qualified Network.Wai.Utilities.Response as WaiResp import Network.Wai.Utilities.Swagger (document) import qualified Network.Wai.Utilities.Swagger as Doc +import Polysemy import Wire.API.Error import qualified Wire.API.Error.Brig as E import qualified Wire.API.User as Public import Wire.API.User.Auth as Public import Wire.Swagger as Doc (pendingLoginError) -routesPublic :: Routes Doc.ApiBuilder (Handler r) () +routesPublic :: + Member UserQuery r => + Routes Doc.ApiBuilder (Handler r) () routesPublic = do -- Note: this endpoint should always remain available at its unversioned -- path, since the login cookie hardcodes @/access@ as a path. @@ -290,6 +294,7 @@ logout (Just (Left ut)) (Just (Left at)) = wrapHttpClientE (Auth.logout ut at) ! logout (Just (Right ut)) (Just (Right at)) = wrapHttpClientE (Auth.logout ut at) !>> zauthError changeSelfEmailH :: + Member UserQuery r => JSON ::: JsonRequest Public.EmailUpdate ::: Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index bd9ae04f2f..9273161152 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -22,17 +22,21 @@ module Brig.User.EJPD (ejpdRequest) where import Brig.API.Handler import Brig.API.User (lookupHandle) -import Brig.App (AppT, wrapClient, wrapHttp) +import Brig.App import qualified Brig.Data.Connection as Conn import Brig.Data.User (lookupUser) import qualified Brig.IO.Intra as Intra -import Brig.Types.User (HavePendingInvitations (NoPendingInvitations)) +import Brig.Options (setDefaultUserLocale) +import Brig.Sem.UserQuery (UserQuery) +import Brig.Types.User (HavePendingInvitations (NoPendingInvitations), Locale) import Control.Error hiding (bool) import Control.Lens (view, (^.)) import Data.Handle (Handle) import Data.Id (UserId) +import Data.Qualified import qualified Data.Set as Set import Imports hiding (head) +import Polysemy import Servant.Swagger.Internal.Orphans () import Wire.API.Connection (Relation, RelationWithHistory (..), relationDropHistory) import qualified Wire.API.Push.Token as PushTok @@ -40,20 +44,27 @@ import Wire.API.Routes.Internal.Brig.EJPD (EJPDRequestBody (EJPDRequestBody), EJ import qualified Wire.API.Team.Member as Team import Wire.API.User (User, userDisplayName, userEmail, userHandle, userId, userPhone, userTeam) -ejpdRequest :: Maybe Bool -> EJPDRequestBody -> (Handler r) EJPDResponseBody +ejpdRequest :: + forall r. + Member UserQuery r => + Maybe Bool -> + EJPDRequestBody -> + (Handler r) EJPDResponseBody ejpdRequest includeContacts (EJPDRequestBody handles) = do ExceptT $ Right . EJPDResponseBody . catMaybes <$> forM handles (go1 (fromMaybe False includeContacts)) where -- find uid given handle go1 :: Bool -> Handle -> (AppT r) (Maybe EJPDResponseItem) go1 includeContacts' handle = do + loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locale <- setDefaultUserLocale <$> view settings mbUid <- wrapClient $ lookupHandle handle - mbUsr <- maybe (pure Nothing) (wrapClient . lookupUser NoPendingInvitations) mbUid - maybe (pure Nothing) (fmap Just . go2 includeContacts') mbUsr + mbUsr <- maybe (pure Nothing) (liftSem . lookupUser loc locale NoPendingInvitations) mbUid + maybe (pure Nothing) (fmap Just . go2 loc locale includeContacts') mbUsr -- construct response item given uid - go2 :: Bool -> User -> (AppT r) EJPDResponseItem - go2 includeContacts' target = do + go2 :: Local x -> Locale -> Bool -> User -> (AppT r) EJPDResponseItem + go2 loc locale includeContacts' target = do let uid = userId target ptoks <- @@ -67,8 +78,8 @@ ejpdRequest includeContacts (EJPDRequestBody handles) = do contactsFull :: [Maybe (Relation, EJPDResponseItem)] <- forM contacts $ \(uid', relationDropHistory -> rel) -> do - mbUsr <- wrapClient $ lookupUser NoPendingInvitations uid' - maybe (pure Nothing) (fmap (Just . (rel,)) . go2 False) mbUsr + mbUsr <- liftSem $ lookupUser loc locale NoPendingInvitations uid' + maybe (pure Nothing) (fmap (Just . (rel,)) . go2 loc locale False) mbUsr pure . Just . Set.fromList . catMaybes $ contactsFull else do @@ -82,8 +93,8 @@ ejpdRequest includeContacts (EJPDRequestBody handles) = do contactsFull :: [Maybe EJPDResponseItem] <- forM members $ \uid' -> do - mbUsr <- wrapClient $ lookupUser NoPendingInvitations uid' - maybe (pure Nothing) (fmap Just . go2 False) mbUsr + mbUsr <- liftSem $ lookupUser loc locale NoPendingInvitations uid' + maybe (pure Nothing) (fmap Just . go2 loc locale False) mbUsr pure . Just . (,Team.toNewListType (memberList ^. Team.teamMemberListType)) . Set.fromList . catMaybes $ contactsFull _ -> do diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index 6d4ab3f1a7..98465566c0 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -27,6 +27,7 @@ import Bilge import Bilge.Assert import Brig.Data.User (lookupFeatureConferenceCalling, lookupStatus, userExists) import qualified Brig.Options as Opt +import Brig.Sem.UserQuery.Cassandra import Brig.Types.Intra import Brig.Types.User (User (userQualifiedId), userId) import qualified Cassandra as Cass @@ -41,6 +42,7 @@ import Data.Id import Data.Qualified (Qualified (qDomain, qUnqualified)) import qualified Data.Set as Set import Imports +import Polysemy import Servant.API (ToHttpApiData (toUrlPiece)) import Test.QuickCheck (Arbitrary (arbitrary), generate) import Test.Tasty @@ -88,7 +90,7 @@ testSuspendNonExistingUser :: forall m. TestConstraints m => Cass.ClientState -> testSuspendNonExistingUser db brig = do nonExistingUserId <- randomId setAccountStatus brig nonExistingUserId Suspended !!! const 404 === statusCode - isUserCreated <- Cass.runClient db (userExists nonExistingUserId) + isUserCreated <- Cass.runClient db (runM $ userQueryToCassandra @Cass.Client @'[Embed Cass.Client] $ userExists nonExistingUserId) liftIO $ isUserCreated @?= False setAccountStatus :: (MonadIO m, MonadHttp m, HasCallStack, MonadCatch m) => Brig -> UserId -> AccountStatus -> m ResponseLBS From b9d94af5f6d7a1db8bdaee20d445f1f458e46b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 24 May 2022 15:01:43 +0200 Subject: [PATCH 06/41] Implement the getName action (replace lookupName) --- services/brig/src/Brig/API/Connection.hs | 23 ++++++++++++------ services/brig/src/Brig/API/Internal.hs | 5 +++- services/brig/src/Brig/API/Public.hs | 30 ++++++++++++++++++++---- services/brig/src/Brig/API/User.hs | 2 +- services/brig/src/Brig/Data/User.hs | 12 ++-------- 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index da4a3dd5fc..70ae638cd2 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -40,6 +40,7 @@ import qualified Brig.Data.Connection as Data import Brig.Data.Types (resultHasMore, resultList) import qualified Brig.Data.User as Data import qualified Brig.IO.Intra as Intra +import Brig.Sem.UserQuery (UserQuery) import Brig.Types import Brig.Types.User.Event import Control.Error @@ -52,6 +53,7 @@ import Data.Range import qualified Data.UUID.V4 as UUID import Galley.Types (ConvType (..), cnvType) import Imports +import Polysemy import qualified System.Logger.Class as Log import System.Logger.Message import Wire.API.Connection (RelationWithHistory (..)) @@ -72,6 +74,7 @@ ensureNotSameTeam self target = do throwE ConnectSameBindingTeamUsers createConnection :: + Member UserQuery r => Local UserId -> ConnId -> Qualified UserId -> @@ -91,6 +94,8 @@ createConnection self con target = do target createConnectionToLocalUser :: + forall r. + Member UserQuery r => Local UserId -> ConnId -> Local UserId -> @@ -119,7 +124,7 @@ createConnectionToLocalUser self conn target = do o2s' <- wrapClient $ Data.insertConnection target (qUntagged self) PendingWithHistory qcnv e2o <- ConnectionUpdated o2s' (ucStatus <$> o2s) - <$> wrapClient (Data.lookupName (tUnqualified self)) + <$> liftSem (Data.getName (tUnqualified self)) let e2s = ConnectionUpdated s2o' (ucStatus <$> s2o) Nothing mapM_ (Intra.onConnectionEvent (tUnqualified self) (Just conn)) [e2o, e2s] pure s2o' @@ -154,9 +159,9 @@ createConnectionToLocalUser self conn target = do then Data.updateConnection o2s BlockedWithHistory else Data.updateConnection o2s AcceptedWithHistory e2o <- - lift . wrapClient $ + lift . liftSem $ ConnectionUpdated o2s' (Just $ ucStatus o2s) - <$> Data.lookupName (tUnqualified self) + <$> Data.getName (tUnqualified self) let e2s = ConnectionUpdated s2o' (Just $ ucStatus s2o) Nothing lift $ mapM_ (Intra.onConnectionEvent (tUnqualified self) (Just conn)) [e2o, e2s] pure $ Existed s2o' @@ -201,6 +206,7 @@ checkLegalholdPolicyConflict uid1 uid2 = do oneway status2 status1 updateConnection :: + Member UserQuery r => Local UserId -> Qualified UserId -> Relation -> @@ -220,6 +226,8 @@ updateConnection self other newStatus conn = -- because a connection between two team members can not exist in the first place. -- {#RefConnectionTeam} updateConnectionToLocalUser :: + forall r. + Member UserQuery r => -- | From Local UserId -> -- | To @@ -298,7 +306,7 @@ updateConnectionToLocalUser self other newStatus conn = do else Data.updateConnection o2s BlockedWithHistory e2o <- ConnectionUpdated o2s' (Just $ ucStatus o2s) - <$> wrapClient (Data.lookupName (tUnqualified self)) + <$> liftSem (Data.getName (tUnqualified self)) Intra.onConnectionEvent (tUnqualified self) conn e2o lift . wrapClient $ Just <$> Data.updateConnection s2o AcceptedWithHistory @@ -326,9 +334,9 @@ updateConnectionToLocalUser self other newStatus conn = do then Data.updateConnection o2s AcceptedWithHistory else Data.updateConnection o2s BlockedWithHistory e2o :: ConnectionEvent <- - wrapClient $ + liftSem $ ConnectionUpdated o2s' (Just $ ucStatus o2s) - <$> Data.lookupName (tUnqualified self) + <$> Data.getName (tUnqualified self) -- TODO: is this correct? shouldnt o2s be sent to other? Intra.onConnectionEvent (tUnqualified self) conn e2o lift . wrapClient $ Just <$> Data.updateConnection s2o (mkRelationWithHistory (error "impossible") new) @@ -378,6 +386,7 @@ mkRelationWithHistory oldRel = \case updateConnectionInternal :: forall r. + Member UserQuery r => UpdateConnectionsInternal -> ExceptT ConnectionError (AppT r) () updateConnectionInternal = \case @@ -446,7 +455,7 @@ updateConnectionInternal = \case void . lift . for (ucConvId uconn) $ wrapHttp . Intra.unblockConv lfrom Nothing uconnRevRel :: RelationWithHistory <- relationWithHistory lfrom (ucTo uconnRev) uconnRev' <- lift . wrapClient $ Data.updateConnection uconnRev (undoRelationHistory uconnRevRel) - connName <- lift . wrapClient $ Data.lookupName (tUnqualified lfrom) + connName <- lift . liftSem $ Data.getName (tUnqualified lfrom) let connEvent = ConnectionUpdated { ucConn = uconnRev', diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 1e00b1a763..5333e1ef6d 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -574,7 +574,10 @@ revokeIdentityH emailOrPhone = do lift $ API.revokeIdentity emailOrPhone pure $ setStatus status200 empty -updateConnectionInternalH :: JSON ::: JsonRequest UpdateConnectionsInternal -> (Handler r) Response +updateConnectionInternalH :: + Member UserQuery r => + JSON ::: JsonRequest UpdateConnectionsInternal -> + (Handler r) Response updateConnectionInternalH (_ ::: req) = do updateConn <- parseJsonBody req API.updateConnectionInternal updateConn !>> connError diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 4bb481b2d5..8de2b632a1 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -921,23 +921,45 @@ customerExtensionCheckBlockedDomains email = do when (domain `elem` blockedDomains) $ throwM $ customerExtensionBlockedDomain domain -createConnectionUnqualified :: UserId -> ConnId -> Public.ConnectionRequest -> (Handler r) (Public.ResponseForExistedCreated Public.UserConnection) +createConnectionUnqualified :: + Member UserQuery r => + UserId -> + ConnId -> + Public.ConnectionRequest -> + (Handler r) (Public.ResponseForExistedCreated Public.UserConnection) createConnectionUnqualified self conn cr = do lself <- qualifyLocal self target <- qualifyLocal (Public.crUser cr) API.createConnection lself conn (qUntagged target) !>> connError -createConnection :: UserId -> ConnId -> Qualified UserId -> (Handler r) (Public.ResponseForExistedCreated Public.UserConnection) +createConnection :: + Member UserQuery r => + UserId -> + ConnId -> + Qualified UserId -> + (Handler r) (Public.ResponseForExistedCreated Public.UserConnection) createConnection self conn target = do lself <- qualifyLocal self API.createConnection lself conn target !>> connError -updateLocalConnection :: UserId -> ConnId -> UserId -> Public.ConnectionUpdate -> (Handler r) (Public.UpdateResult Public.UserConnection) +updateLocalConnection :: + Member UserQuery r => + UserId -> + ConnId -> + UserId -> + Public.ConnectionUpdate -> + (Handler r) (Public.UpdateResult Public.UserConnection) updateLocalConnection self conn other update = do lother <- qualifyLocal other updateConnection self conn (qUntagged lother) update -updateConnection :: UserId -> ConnId -> Qualified UserId -> Public.ConnectionUpdate -> (Handler r) (Public.UpdateResult Public.UserConnection) +updateConnection :: + Member UserQuery r => + UserId -> + ConnId -> + Qualified UserId -> + Public.ConnectionUpdate -> + (Handler r) (Public.UpdateResult Public.UserConnection) updateConnection self conn other update = do let newStatus = Public.cuStatus update lself <- qualifyLocal self diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 281d6fee3c..30b2325664 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -41,7 +41,7 @@ module Brig.API.User lookupProfiles, lookupLocalProfiles, getLegalHoldStatus, - Data.lookupName, + Data.getName, Data.lookupLocale, Data.lookupUser, Data.lookupRichInfo, diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 9175f36aa1..794c47de70 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -38,7 +38,7 @@ module Brig.Data.User lookupAccounts, lookupUser, lookupUsers, - lookupName, + getName, lookupLocale, lookupPassword, lookupStatus, @@ -78,7 +78,7 @@ import Brig.App (Env, currentTime, settings, viewFederationDomain, zauthEnv) import Brig.Data.Instances () import Brig.Options import Brig.Password -import Brig.Sem.UserQuery (UserQuery, getId, getUsers) +import Brig.Sem.UserQuery (UserQuery, getId, getName, getUsers) import Brig.Types import Brig.Types.Intra import qualified Brig.ZAuth as ZAuth @@ -407,11 +407,6 @@ lookupLocale u = do defLoc <- setDefaultUserLocale <$> view settings fmap (toLocale defLoc) <$> retry x1 (query1 localeSelect (params LocalQuorum (Identity u))) -lookupName :: MonadClient m => UserId -> m (Maybe Name) -lookupName u = - fmap runIdentity - <$> retry x1 (query1 nameSelect (params LocalQuorum (Identity u))) - lookupPassword :: MonadClient m => UserId -> m (Maybe Password) lookupPassword u = (runIdentity =<<) @@ -583,9 +578,6 @@ type AccountRow = Maybe ManagedBy ) -nameSelect :: PrepQuery R (Identity UserId) (Identity Name) -nameSelect = "SELECT name FROM user WHERE id = ?" - localeSelect :: PrepQuery R (Identity UserId) (Maybe Language, Maybe Country) localeSelect = "SELECT language, country FROM user WHERE id = ?" From e2271c19b193e98f7adcb12e96bf2af3cd31b4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 24 May 2022 16:15:25 +0200 Subject: [PATCH 07/41] Implement the getLocale action (was lookupLocale) --- services/brig/src/Brig/API/Public.hs | 27 ++++++++++++++++++++++----- services/brig/src/Brig/Data/User.hs | 15 +++++++-------- services/brig/src/Brig/User/Auth.hs | 6 +++++- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 8de2b632a1..977e06a36e 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -800,10 +800,16 @@ updateUser uid conn uu = do eithErr <- lift $ runExceptT $ API.updateUser uid (Just conn) uu API.ForbidSCIMUpdates pure $ either Just (const Nothing) eithErr -changePhone :: UserId -> ConnId -> Public.PhoneUpdate -> (Handler r) (Maybe Public.ChangePhoneError) +changePhone :: + Member UserQuery r => + UserId -> + ConnId -> + Public.PhoneUpdate -> + (Handler r) (Maybe Public.ChangePhoneError) changePhone u _ (Public.puPhone -> phone) = lift . exceptTToMaybe $ do (adata, pn) <- API.changePhone u phone - loc <- lift $ wrapClient $ API.lookupLocale u + defLocale <- setDefaultUserLocale <$> view settings + loc <- lift . liftSem $ API.lookupLocale defLocale u let apair = (activationKey adata, activationCode adata) lift . wrapClient $ sendActivationSms pn apair loc @@ -861,20 +867,31 @@ changeHandle u conn (Public.HandleUpdate h) = lift . exceptTToMaybe $ do API.changeHandle u (Just conn) handle API.ForbidSCIMUpdates beginPasswordResetH :: - Members '[P.TinyLog, PasswordResetStore] r => + Members + '[ P.TinyLog, + PasswordResetStore, + UserQuery + ] + r => JSON ::: JsonRequest Public.NewPasswordReset -> (Handler r) Response beginPasswordResetH (_ ::: req) = setStatus status201 empty <$ (beginPasswordReset =<< parseJsonBody req) beginPasswordReset :: - Members '[P.TinyLog, PasswordResetStore] r => + Members + '[ P.TinyLog, + PasswordResetStore, + UserQuery + ] + r => Public.NewPasswordReset -> (Handler r) () beginPasswordReset (Public.NewPasswordReset target) = do checkWhitelist target (u, pair) <- API.beginPasswordReset target !>> pwResetError - loc <- lift $ wrapClient $ API.lookupLocale u + defLocale <- setDefaultUserLocale <$> view settings + loc <- lift . liftSem $ API.lookupLocale defLocale u lift $ case target of Left email -> sendPasswordResetMail email pair loc Right phone -> wrapClient $ sendPasswordResetSms phone pair loc diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 794c47de70..034d46fe34 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -78,7 +78,7 @@ import Brig.App (Env, currentTime, settings, viewFederationDomain, zauthEnv) import Brig.Data.Instances () import Brig.Options import Brig.Password -import Brig.Sem.UserQuery (UserQuery, getId, getName, getUsers) +import Brig.Sem.UserQuery (UserQuery, getId, getLocale, getName, getUsers) import Brig.Types import Brig.Types.Intra import qualified Brig.ZAuth as ZAuth @@ -402,10 +402,12 @@ deactivateUser :: MonadClient m => UserId -> m () deactivateUser u = retry x5 $ write userDeactivatedUpdate (params LocalQuorum (Identity u)) -lookupLocale :: (MonadClient m, MonadReader Env m) => UserId -> m (Maybe Locale) -lookupLocale u = do - defLoc <- setDefaultUserLocale <$> view settings - fmap (toLocale defLoc) <$> retry x1 (query1 localeSelect (params LocalQuorum (Identity u))) +lookupLocale :: + Member UserQuery r => + Locale -> + UserId -> + Sem r (Maybe Locale) +lookupLocale defLoc u = fmap (toLocale defLoc) <$> getLocale u lookupPassword :: MonadClient m => UserId -> m (Maybe Password) lookupPassword u = @@ -578,9 +580,6 @@ type AccountRow = Maybe ManagedBy ) -localeSelect :: PrepQuery R (Identity UserId) (Maybe Language, Maybe Country) -localeSelect = "SELECT language, country FROM user WHERE id = ?" - authSelect :: PrepQuery R (Identity UserId) (Maybe Password, Maybe AccountStatus) authSelect = "SELECT password, status FROM user WHERE id = ?" diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 3088a6eb61..4f9bef20c6 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -52,6 +52,7 @@ import Brig.Email import qualified Brig.IO.Intra as Intra import qualified Brig.Options as Opt import Brig.Phone +import Brig.Sem.UserQuery.Cassandra import Brig.Types.Common import Brig.Types.Intra import Brig.Types.User @@ -75,6 +76,7 @@ import Data.Misc (PlainTextPassword (..)) import qualified Data.ZAuth.Token as ZAuth import Imports import Network.Wai.Utilities.Error ((!>>)) +import Polysemy import System.Logger (field, msg, val, (~~)) import qualified System.Logger.Class as Log import Wire.API.Team.Feature (TeamFeatureStatusNoConfig (..), TeamFeatureStatusValue (..)) @@ -87,6 +89,7 @@ data Access u = Access } sendLoginCode :: + forall m. ( MonadClient m, MonadReader Env m, MonadCatch m, @@ -111,7 +114,8 @@ sendLoginCode phone call force = do unless (isNothing pw || force) $ throwE SendLoginPasswordExists lift $ do - l <- Data.lookupLocale u + defLoc <- Opt.setDefaultUserLocale <$> view settings + l <- runM $ userQueryToCassandra @m @'[Embed m] $ Data.lookupLocale defLoc u c <- Data.createLoginCode u void . forPhoneKey pk $ \ph -> if call From 8775800c00f5a8784a593bb0bcbaa185fd8f97b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 25 May 2022 10:15:30 +0200 Subject: [PATCH 08/41] Define the BudgetStore effect (to replace Brig.Budget) --- services/brig/brig.cabal | 2 + services/brig/src/Brig/Sem/BudgetStore.hs | 89 +++++++++++++++++++ .../src/Brig/Sem/BudgetStore/Cassandra.hs | 41 +++++++++ 3 files changed, 132 insertions(+) create mode 100644 services/brig/src/Brig/Sem/BudgetStore.hs create mode 100644 services/brig/src/Brig/Sem/BudgetStore/Cassandra.hs diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 3c93430db1..16d41945ad 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -85,6 +85,8 @@ library Brig.Queue.Types Brig.RPC Brig.Run + Brig.Sem.BudgetStore + Brig.Sem.BudgetStore.Cassandra Brig.Sem.CodeStore Brig.Sem.CodeStore.Cassandra Brig.Sem.PasswordResetStore diff --git a/services/brig/src/Brig/Sem/BudgetStore.hs b/services/brig/src/Brig/Sem/BudgetStore.hs new file mode 100644 index 0000000000..995ad527bb --- /dev/null +++ b/services/brig/src/Brig/Sem/BudgetStore.hs @@ -0,0 +1,89 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.BudgetStore where + +import Cassandra (Cql) +import Data.Time.Clock +import Imports +import Polysemy + +data Budget = Budget + { budgetTimeout :: !NominalDiffTime, + budgetValue :: !Int32 + } + deriving (Eq, Show, Generic) + +data Budgeted a + = BudgetExhausted NominalDiffTime + | BudgetedValue a Int32 + deriving (Eq, Show, Generic) + +newtype BudgetKey = BudgetKey Text + deriving (Eq, Show, Cql) + +data BudgetStore m a where + GetBudget :: BudgetKey -> BudgetStore m (Maybe (Int32, Int32)) + InsertBudget :: BudgetKey -> Budget -> BudgetStore m () + +makeSem ''BudgetStore + +-- | @withBudget (BudgetKey "k") (Budget 30 5) action@ runs @action@ at most 5 times every 30 +-- seconds. @"k"@ is used for keeping different calls to 'withBudget' apart; use something +-- there that's unique to your context, like @"login#" <> uid@. +-- +-- See the docs in "Gundeck.ThreadBudget" for related work. +-- +-- FUTUREWORK: encourage caller to define their own type for budget keys (rather than using an +-- untyped text), and represent the types in a way that guarantees that if i'm using a local +-- type that i don't export, then nobody will be able to use my namespace. +-- +-- FUTUREWORK: exceptions are not handled very nicely, but it's not clear what it would mean +-- to improve this. +withBudget :: + Member BudgetStore r => + BudgetKey -> + Budget -> + Sem r a -> + Sem r (Budgeted a) +withBudget k b ma = do + Budget ttl val <- fromMaybe b <$> lookupBudget k + let remaining = val - 1 + if remaining < 0 + then return (BudgetExhausted ttl) + else do + a <- ma + insertBudget k (Budget ttl remaining) + return (BudgetedValue a remaining) + +-- | Like 'withBudget', but does not decrease budget, only takes a look. +checkBudget :: Member BudgetStore r => BudgetKey -> Budget -> Sem r (Budgeted ()) +checkBudget k b = do + Budget ttl val <- fromMaybe b <$> lookupBudget k + let remaining = val - 1 + return $ + if remaining < 0 + then BudgetExhausted ttl + else BudgetedValue () remaining + +lookupBudget :: Member BudgetStore r => BudgetKey -> Sem r (Maybe Budget) +lookupBudget k = fmap mk <$> getBudget k + where + mk (val, ttl) = Budget (fromIntegral ttl) val diff --git a/services/brig/src/Brig/Sem/BudgetStore/Cassandra.hs b/services/brig/src/Brig/Sem/BudgetStore/Cassandra.hs new file mode 100644 index 0000000000..ba4e7a5c2f --- /dev/null +++ b/services/brig/src/Brig/Sem/BudgetStore/Cassandra.hs @@ -0,0 +1,41 @@ +-- 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.Sem.BudgetStore.Cassandra where + +import Brig.Sem.BudgetStore +import Cassandra +import Imports +import Polysemy + +budgetStoreToCassandra :: + forall m r a. + (MonadClient m, Member (Embed m) r) => + Sem (BudgetStore ': r) a -> + Sem r a +budgetStoreToCassandra = + interpret $ + embed @m . \case + GetBudget k -> query1 budgetSelect (params One (Identity k)) + InsertBudget k (Budget ttl b) -> + retry x5 $ write budgetInsert (params One (k, b, round ttl)) + +budgetInsert :: PrepQuery W (BudgetKey, Int32, Int32) () +budgetInsert = "INSERT INTO budget (key, budget) VALUES (?, ?) USING TTL ?" + +budgetSelect :: PrepQuery R (Identity BudgetKey) (Int32, Int32) +budgetSelect = "SELECT budget, ttl(budget) FROM budget where key = ?" From f5cbebf49d4b9f09dc0c5b51a17f7d141c5694d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 25 May 2022 16:28:00 +0200 Subject: [PATCH 09/41] Implement the getAuthentication action --- services/brig/brig.cabal | 1 - services/brig/src/Brig/API.hs | 8 +- services/brig/src/Brig/API/Client.hs | 22 +- services/brig/src/Brig/API/Internal.hs | 69 +++++-- services/brig/src/Brig/API/Public.hs | 69 +++++-- services/brig/src/Brig/API/User.hs | 59 ++++-- services/brig/src/Brig/API/Util.hs | 25 ++- services/brig/src/Brig/App.hs | 28 ++- services/brig/src/Brig/Budget.hs | 98 --------- services/brig/src/Brig/Data/Activation.hs | 16 +- services/brig/src/Brig/Data/Client.hs | 22 +- services/brig/src/Brig/Data/User.hs | 193 ++++++------------ .../brig/src/Brig/InternalEvent/Process.hs | 43 ++-- services/brig/src/Brig/Phone.hs | 10 +- services/brig/src/Brig/Sem/UserQuery.hs | 184 ++++++++++++++++- services/brig/src/Brig/Team/API.hs | 20 +- services/brig/src/Brig/User/API/Auth.hs | 37 +++- services/brig/src/Brig/User/Auth.hs | 151 +++++++++++--- 18 files changed, 708 insertions(+), 347 deletions(-) delete mode 100644 services/brig/src/Brig/Budget.hs diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 16d41945ad..f835153e3b 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -41,7 +41,6 @@ library Brig.AWS Brig.AWS.SesNotification Brig.AWS.Types - Brig.Budget Brig.Calling Brig.Calling.API Brig.Calling.Internal diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index cd25b3d5ca..447552a17d 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -23,17 +23,23 @@ where import Brig.API.Handler (Handler) import qualified Brig.API.Internal as Internal import qualified Brig.API.Public as Public +import Brig.API.Types import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import Brig.Sem.UserQuery (UserQuery) +import Data.Qualified import qualified Data.Swagger.Build.Api as Doc import Network.Wai.Routing (Routes) import Polysemy +import Polysemy.Error +import Polysemy.Input import qualified Polysemy.TinyLog as P sitemap :: Members - '[ P.TinyLog, + '[ Error ReAuthError, + Input (Local ()), + P.TinyLog, PasswordResetStore, PasswordResetSupply, UserQuery diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 5744bb9b95..ef96957088 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -57,6 +57,7 @@ import qualified Brig.Federation.Client as Federation import Brig.IO.Intra (guardLegalhold) import qualified Brig.IO.Intra as Intra import qualified Brig.Options as Opt +import Brig.Sem.UserQuery (UserQuery) import Brig.Types import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) @@ -80,7 +81,10 @@ import Data.Misc (PlainTextPassword (..)) import Data.Qualified import qualified Data.Set as Set import Imports -import Network.Wai.Utilities +import Network.Wai.Utilities hiding (Error) +import Polysemy +import Polysemy.Error +import Polysemy.Input import System.Logger.Class (field, msg, val, (~~)) import qualified System.Logger.Class as Log import UnliftIO.Async (Concurrently (Concurrently, runConcurrently)) @@ -128,6 +132,7 @@ lookupLocalPubClientsBulk :: [UserId] -> ExceptT ClientError (AppT r) (UserMap ( lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk addClient :: + Members '[Input (Local ()), UserQuery] r => UserId -> Maybe ConnId -> Maybe IP -> @@ -138,6 +143,7 @@ addClient = addClientWithReAuthPolicy Data.reAuthForNewClients -- nb. We must ensure that the set of clients known to brig is always -- a superset of the clients known to galley. addClientWithReAuthPolicy :: + Members '[Input (Local ()), UserQuery] r => Data.ReAuthPolicy -> UserId -> Maybe ConnId -> @@ -145,7 +151,8 @@ addClientWithReAuthPolicy :: NewClient -> ExceptT ClientError (AppT r) Client addClientWithReAuthPolicy policy u con ip new = do - acc <- lift (wrapClient $ Data.lookupAccount u) >>= maybe (throwE (ClientUserNotFound u)) pure + locale <- Opt.setDefaultUserLocale <$> view settings + acc <- lift (liftSem $ Data.lookupAccount locale u) >>= maybe (throwE (ClientUserNotFound u)) pure wrapHttpClientE $ verifyCode (newClientVerificationCode new) (userId . accountUser $ acc) loc <- maybe (pure Nothing) locationOf ip maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings @@ -211,18 +218,25 @@ updateClient u c r = do -- nb. We must ensure that the set of clients known to brig is always -- a superset of the clients known to galley. -rmClient :: UserId -> ConnId -> ClientId -> Maybe PlainTextPassword -> ExceptT ClientError (AppT r) () +rmClient :: + Members '[Error ReAuthError, Input (Local ()), UserQuery] r => + UserId -> + ConnId -> + ClientId -> + Maybe PlainTextPassword -> + ExceptT ClientError (AppT r) () rmClient u con clt pw = maybe (throwE ClientNotFound) fn =<< lift (wrapClient $ Data.lookupClient u clt) where fn client = do + locale <- Opt.setDefaultUserLocale <$> view settings case clientType client of -- Legal hold clients can't be removed LegalHoldClientType -> throwE ClientLegalHoldCannotBeRemoved -- Temporary clients don't need to re-auth TemporaryClientType -> pure () -- All other clients must authenticate - _ -> wrapClientE (Data.reauthenticate u pw) !>> ClientDataError . ClientReAuthError + _ -> lift (liftSem (Data.reauthenticate locale u pw)) !>> ClientDataError . ClientReAuthError lift $ execDelete u (Just con) client claimPrekey :: diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 5333e1ef6d..0dac54cdbf 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -75,6 +75,8 @@ import Network.Wai.Routing hiding (toList) import Network.Wai.Utilities as Utilities import Network.Wai.Utilities.ZAuth (zauthConnId, zauthUserId) import Polysemy +import qualified Polysemy.Error as P +import Polysemy.Input import Servant hiding (Handler, JSON, addHeader, respond) import Servant.Swagger.Internal.Orphans () import Servant.Swagger.UI @@ -97,7 +99,7 @@ import Wire.API.User.RichInfo -- Sitemap (servant) servantSitemap :: - Member UserQuery r => + Members '[Input (Local ()), UserQuery] r => ServerT BrigIRoutes.API (Handler r) servantSitemap = ejpdAPI :<|> accountAPI :<|> mlsAPI :<|> getVerificationCode :<|> teamsAPI @@ -122,7 +124,9 @@ mlsAPI = :<|> getMLSClients :<|> mapKeyPackageRefsInternal -accountAPI :: ServerT BrigIRoutes.AccountAPI (Handler r) +accountAPI :: + Members '[Input (Local ()), UserQuery] r => + ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = Named @"createUserNoVerify" createUserNoVerify teamsAPI :: ServerT BrigIRoutes.TeamsAPI (Handler r) @@ -199,7 +203,9 @@ swaggerDocsAPI = swaggerSchemaUIServer BrigIRoutes.swaggerDoc sitemap :: Members - '[ PasswordResetStore, + '[ Input (Local ()), + P.Error ReAuthError, + PasswordResetStore, PasswordResetSupply, UserQuery ] @@ -367,12 +373,21 @@ sitemap = do -- Handlers -- | Add a client without authentication checks -addClientInternalH :: UserId ::: Maybe Bool ::: JsonRequest NewClient ::: Maybe ConnId ::: JSON -> (Handler r) Response +addClientInternalH :: + Members '[Input (Local ()), UserQuery] r => + UserId ::: Maybe Bool ::: JsonRequest NewClient ::: Maybe ConnId ::: JSON -> + Handler r Response addClientInternalH (usr ::: mSkipReAuth ::: req ::: connId ::: _) = do new <- parseJsonBody req setStatus status201 . json <$> addClientInternal usr mSkipReAuth new connId -addClientInternal :: UserId -> Maybe Bool -> NewClient -> Maybe ConnId -> (Handler r) Client +addClientInternal :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + Maybe Bool -> + NewClient -> + Maybe ConnId -> + Handler r Client addClientInternal usr mSkipReAuth new connId = do let policy | mSkipReAuth == Just True = \_ _ -> False @@ -407,7 +422,10 @@ internalListFullClients :: UserSet -> (AppT r) UserClientsFull internalListFullClients (UserSet usrs) = UserClientsFull <$> wrapClient (Data.lookupClientsBulk (Set.toList usrs)) -createUserNoVerify :: NewUser -> (Handler r) (Either RegisterError SelfProfile) +createUserNoVerify :: + Members '[Input (Local ()), UserQuery] r => + NewUser -> + Handler r (Either RegisterError SelfProfile) createUserNoVerify uData = lift . runExceptT $ do result <- API.createUser uData let acc = createdAccount result @@ -421,14 +439,21 @@ createUserNoVerify uData = lift . runExceptT $ do in API.activate key code (Just uid) !>> activationErrorToRegisterError pure (SelfProfile usr) -deleteUserNoVerifyH :: UserId -> (Handler r) Response +deleteUserNoVerifyH :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + Handler r Response deleteUserNoVerifyH uid = do setStatus status202 empty <$ deleteUserNoVerify uid -deleteUserNoVerify :: UserId -> (Handler r) () +deleteUserNoVerify :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + Handler r () deleteUserNoVerify uid = do + locale <- setDefaultUserLocale <$> view settings void $ - lift (wrapClient $ API.lookupAccount uid) + lift (liftSem $ API.lookupAccount locale uid) >>= ifNothing (errorToWai @'E.UserNotFound) lift $ API.deleteUserNoVerify uid @@ -458,11 +483,19 @@ changeSelfEmailMaybeSend u DoNotSendEmail email allowScim = do ChangeEmailIdempotent -> pure ChangeEmailResponseIdempotent ChangeEmailNeedsActivation _ -> pure ChangeEmailResponseNeedsActivation -listActivatedAccountsH :: JSON ::: Either (List UserId) (List Handle) ::: Bool -> (Handler r) Response +listActivatedAccountsH :: + Members '[Input (Local ()), UserQuery] r => + JSON ::: Either (List UserId) (List Handle) ::: Bool -> + Handler r Response listActivatedAccountsH (_ ::: qry ::: includePendingInvitations) = do json <$> lift (listActivatedAccounts qry includePendingInvitations) -listActivatedAccounts :: Either (List UserId) (List Handle) -> Bool -> (AppT r) [UserAccount] +listActivatedAccounts :: + forall r. + Members '[Input (Local ()), UserQuery] r => + Either (List UserId) (List Handle) -> + Bool -> + AppT r [UserAccount] listActivatedAccounts elh includePendingInvitations = do Log.debug (Log.msg $ "listActivatedAccounts: " <> show (elh, includePendingInvitations)) case elh of @@ -472,7 +505,9 @@ listActivatedAccounts elh includePendingInvitations = do byIds (catMaybes us) where byIds :: [UserId] -> (AppT r) [UserAccount] - byIds uids = wrapClient (API.lookupAccounts uids) >>= filterM accountValid + byIds uids = do + locale <- setDefaultUserLocale <$> view settings + liftSem (API.lookupAccounts locale uids) >>= filterM accountValid accountValid :: UserAccount -> (AppT r) Bool accountValid account = case userIdentity . accountUser $ account of @@ -493,7 +528,10 @@ listActivatedAccounts elh includePendingInvitations = do (Deleted, _, _) -> pure True (Ephemeral, _, _) -> pure True -listAccountsByIdentityH :: JSON ::: Either Email Phone ::: Bool -> (Handler r) Response +listAccountsByIdentityH :: + Members '[Input (Local ()), UserQuery] r => + JSON ::: Either Email Phone ::: Bool -> + Handler r Response listAccountsByIdentityH (_ ::: emailOrPhone ::: includePendingInvitations) = lift $ json @@ -569,7 +607,10 @@ getConnectionsStatus (ConnectionsStatusRequestV2 froms mtos mrel) = do where filterByRelation l rel = filter ((== rel) . csv2Status) l -revokeIdentityH :: Either Email Phone -> (Handler r) Response +revokeIdentityH :: + Members '[Input (Local ()), UserQuery] r => + Either Email Phone -> + Handler r Response revokeIdentityH emailOrPhone = do lift $ API.revokeIdentity emailOrPhone pure $ setStatus status200 empty diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 977e06a36e..bf6503291a 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -29,7 +29,7 @@ where import qualified Brig.API.Client as API import qualified Brig.API.Connection as API -import Brig.API.Error +import Brig.API.Error hiding (Error) import Brig.API.Handler import Brig.API.MLS.KeyPackages import qualified Brig.API.Properties as API @@ -92,13 +92,15 @@ import Galley.Types.Teams (HiddenPerm (..), hasPermission) import Imports hiding (head) import Network.HTTP.Types.Status import Network.Wai -import Network.Wai.Predicate hiding (result, setStatus) +import Network.Wai.Predicate hiding (Error, result, setStatus) import Network.Wai.Routing import Network.Wai.Utilities as Utilities import Network.Wai.Utilities.Swagger (document, mkSwaggerApi) import qualified Network.Wai.Utilities.Swagger as Doc import Network.Wai.Utilities.ZAuth (zauthUserId) import Polysemy +import qualified Polysemy.Error as P +import Polysemy.Input import qualified Polysemy.TinyLog as P import Servant hiding (Handler, JSON, addHeader, respond) import qualified Servant @@ -183,7 +185,7 @@ swaggerDocsAPI Nothing = swaggerDocsAPI (Just maxBound) servantSitemap :: forall r. - Member UserQuery r => + Members '[P.Error ReAuthError, Input (Local ()), UserQuery] r => ServerT BrigAPI (Handler r) servantSitemap = userAPI @@ -289,7 +291,8 @@ servantSitemap = sitemap :: Members - '[ P.TinyLog, + '[ Input (Local ()), + P.TinyLog, PasswordResetStore, PasswordResetSupply, UserQuery @@ -459,7 +462,8 @@ sitemap = do apiDocs :: forall r. Members - '[ P.TinyLog, + '[ Input (Local ()), + P.TinyLog, PasswordResetStore, PasswordResetSupply, UserQuery @@ -572,7 +576,13 @@ getMultiUserPrekeyBundleH zusr qualUserClients = do throwStd (errorToWai @'E.TooManyClients) API.claimMultiPrekeyBundles (ProtectedUser zusr) qualUserClients !>> clientError -addClient :: UserId -> ConnId -> Maybe IpAddr -> Public.NewClient -> (Handler r) NewClientResponse +addClient :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + ConnId -> + Maybe IpAddr -> + Public.NewClient -> + Handler r NewClientResponse addClient usr con ip new = do -- Users can't add legal hold clients when (Public.newClientType new == Public.LegalHoldClientType) $ @@ -583,7 +593,13 @@ addClient usr con ip new = do clientResponse :: Public.Client -> NewClientResponse clientResponse client = Servant.addHeader (Public.clientId client) client -deleteClient :: UserId -> ConnId -> ClientId -> Public.RmClient -> (Handler r) () +deleteClient :: + Members '[Input (Local ()), P.Error ReAuthError, UserQuery] r => + UserId -> + ConnId -> + ClientId -> + Public.RmClient -> + (Handler r) () deleteClient usr con clt body = API.rmClient usr con clt (Public.rmPassword body) !>> clientError @@ -661,7 +677,10 @@ getClientPrekeys :: UserId -> ClientId -> (Handler r) [Public.PrekeyId] getClientPrekeys usr clt = lift (wrapClient $ API.lookupPrekeyIds usr clt) -- | docs/reference/user/registration.md {#RefRegistration} -createUser :: Public.NewUserPublic -> (Handler r) (Either Public.RegisterError Public.RegisterSuccess) +createUser :: + Members '[Input (Local ()), UserQuery] r => + Public.NewUserPublic -> + Handler r (Either Public.RegisterError Public.RegisterSuccess) createUser (Public.NewUserPublic new) = lift . runExceptT $ do API.checkRestrictedUserCreation new for_ (Public.newUserEmail new) $ mapExceptT wrapHttp . checkWhitelistWithError RegisterErrorWhitelistError . Left @@ -730,7 +749,10 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do Public.NewTeamMemberSSO _ -> Team.sendMemberWelcomeMail e t n l -getSelf :: UserId -> (Handler r) Public.SelfProfile +getSelf :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + Handler r Public.SelfProfile getSelf self = lift (API.lookupSelfProfile self) >>= ifNothing (errorToWai @'E.UserNotFound) @@ -813,11 +835,19 @@ changePhone u _ (Public.puPhone -> phone) = lift . exceptTToMaybe $ do let apair = (activationKey adata, activationCode adata) lift . wrapClient $ sendActivationSms pn apair loc -removePhone :: UserId -> ConnId -> (Handler r) (Maybe Public.RemoveIdentityError) +removePhone :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + ConnId -> + Handler r (Maybe Public.RemoveIdentityError) removePhone self conn = lift . exceptTToMaybe $ API.removePhone self conn -removeEmail :: UserId -> ConnId -> (Handler r) (Maybe Public.RemoveIdentityError) +removeEmail :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + ConnId -> + Handler r (Maybe Public.RemoveIdentityError) removeEmail self conn = lift . exceptTToMaybe $ API.removeEmail self conn @@ -1042,13 +1072,17 @@ getConnection self other = do lift . wrapClient $ Data.lookupConnection lself other deleteSelfUser :: + Members '[Input (Local ()), UserQuery] r => UserId -> Public.DeleteUser -> - (Handler r) (Maybe Code.Timeout) + Handler r (Maybe Code.Timeout) deleteSelfUser u body = API.deleteSelfUser u (Public.deleteUserPassword body) !>> deleteUserError -verifyDeleteUserH :: JsonRequest Public.VerifyDeleteUser ::: JSON -> (Handler r) Response +verifyDeleteUserH :: + Members '[Input (Local ()), UserQuery] r => + JsonRequest Public.VerifyDeleteUser ::: JSON -> + Handler r Response verifyDeleteUserH (r ::: _) = do body <- parseJsonBody r API.verifyDeleteUser body !>> deleteUserError @@ -1120,7 +1154,11 @@ activate (Public.Activate tgt code dryrun) respond (Just ident) x = ActivationResp $ Public.ActivationResponse ident x respond Nothing _ = ActivationRespSuccessNoIdent -sendVerificationCode :: Public.SendVerificationCode -> (Handler r) () +sendVerificationCode :: + forall r. + Members '[Input (Local ()), UserQuery] r => + Public.SendVerificationCode -> + Handler r () sendVerificationCode req = do let email = Public.svcEmail req let action = Public.svcAction req @@ -1143,8 +1181,9 @@ sendVerificationCode req = do where getAccount :: Public.Email -> (Handler r) (Maybe UserAccount) getAccount email = lift $ do + locale <- setDefaultUserLocale <$> view settings mbUserId <- wrapClient . UserKey.lookupKey $ UserKey.userEmailKey email - join <$> wrapClient (Data.lookupAccount `traverse` mbUserId) + join <$> liftSem (Data.lookupAccount locale `traverse` mbUserId) sendMail :: Public.Email -> Code.Value -> Maybe Public.Locale -> Public.VerificationAction -> (Handler r) () sendMail email value mbLocale = diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 30b2325664..c4a42b51af 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -157,6 +157,7 @@ import qualified Galley.Types.Teams.Intra as Team import Imports import Network.Wai.Utilities import Polysemy +import Polysemy.Input import qualified Polysemy.TinyLog as P import System.Logger.Class (MonadLogger) import qualified System.Logger.Class as Log @@ -207,7 +208,10 @@ verifyUniquenessAndCheckBlacklist uk = do throwE IdentityErrorUserKeyExists -- docs/reference/user/registration.md {#RefRegistration} -createUser :: NewUser -> ExceptT RegisterError (AppT r) CreateUserResult +createUser :: + Members '[Input (Local ()), UserQuery] r => + NewUser -> + ExceptT RegisterError (AppT r) CreateUserResult createUser new = do (email, phone) <- validateEmailAndPhone new @@ -228,7 +232,8 @@ createUser new = do Nothing -> pure (Nothing, Nothing, Nothing) let mbInv = Team.inInvitation . fst <$> teamInvitation - mbExistingAccount <- lift $ join <$> for mbInv (\(Id uuid) -> wrapClient $ Data.lookupAccount (Id uuid)) + locale <- setDefaultUserLocale <$> view settings + mbExistingAccount <- lift $ join <$> for mbInv (\(Id uuid) -> liftSem $ Data.lookupAccount locale (Id uuid)) let (new', mbHandle) = case mbExistingAccount of Nothing -> @@ -695,7 +700,11 @@ changePhone u phone = do ------------------------------------------------------------------------------- -- Remove Email -removeEmail :: UserId -> ConnId -> ExceptT RemoveIdentityError (AppT r) () +removeEmail :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + ConnId -> + ExceptT RemoveIdentityError (AppT r) () removeEmail uid conn = do ident <- lift $ fetchUserIdentity uid case ident of @@ -709,7 +718,11 @@ removeEmail uid conn = do ------------------------------------------------------------------------------- -- Remove Phone -removePhone :: UserId -> ConnId -> ExceptT RemoveIdentityError (AppT r) () +removePhone :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + ConnId -> + ExceptT RemoveIdentityError (AppT r) () removePhone uid conn = do ident <- lift $ fetchUserIdentity uid case ident of @@ -727,7 +740,10 @@ removePhone uid conn = do ------------------------------------------------------------------------------- -- Forcefully revoke a verified identity -revokeIdentity :: Either Email Phone -> AppT r () +revokeIdentity :: + Members '[Input (Local ()), UserQuery] r => + Either Email Phone -> + AppT r () revokeIdentity key = do let uk = either userEmailKey userPhoneKey key mu <- wrapClient $ Data.lookupKey uk @@ -1084,9 +1100,14 @@ mkPasswordResetKey ident = case ident of -- delete them in the team settings. This protects teams against orphanhood. -- -- TODO: communicate deletions of SSO users to SSO service. -deleteSelfUser :: UserId -> Maybe PlainTextPassword -> ExceptT DeleteUserError (AppT r) (Maybe Timeout) +deleteSelfUser :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + Maybe PlainTextPassword -> + ExceptT DeleteUserError (AppT r) (Maybe Timeout) deleteSelfUser uid pwd = do - account <- lift . wrapClient $ Data.lookupAccount uid + locale <- setDefaultUserLocale <$> view settings + account <- lift . liftSem $ Data.lookupAccount locale uid case account of Nothing -> throwE DeleteUserInvalid Just a -> case accountStatus a of @@ -1157,13 +1178,17 @@ deleteSelfUser uid pwd = do -- | Conclude validation and scheduling of user's deletion request that was initiated in -- 'deleteUser'. Called via @post /delete@. -verifyDeleteUser :: VerifyDeleteUser -> ExceptT DeleteUserError (AppT r) () +verifyDeleteUser :: + Members '[Input (Local ()), UserQuery] r => + VerifyDeleteUser -> + ExceptT DeleteUserError (AppT r) () verifyDeleteUser d = do let key = verifyDeleteUserKey d let code = verifyDeleteUserCode d c <- lift . wrapClient $ Code.verify key Code.AccountDeletion code a <- maybe (throwE DeleteUserInvalidCode) pure (Code.codeAccount =<< c) - account <- lift . wrapClient $ Data.lookupAccount (Id a) + locale <- setDefaultUserLocale <$> view settings + account <- lift . liftSem $ Data.lookupAccount locale (Id a) for_ account $ lift . wrapHttpClient . deleteAccount lift . wrapClient $ Code.delete key Code.AccountDeletion @@ -1424,6 +1449,7 @@ lookupLocalProfiles requestingUser others = do in baseProfile {profileEmail = profileEmail'} getLegalHoldStatus :: + forall m. ( MonadLogger m, MonadReader Env m, MonadMask m, @@ -1433,7 +1459,11 @@ getLegalHoldStatus :: ) => UserId -> m (Maybe UserLegalHoldStatus) -getLegalHoldStatus uid = traverse (getLegalHoldStatus' . accountUser) =<< lookupAccount uid +getLegalHoldStatus uid = do + locale <- setDefaultUserLocale <$> view settings + locDomain <- qualifyLocal () + traverse (getLegalHoldStatus' . accountUser) + =<< (runM . userQueryToCassandra @m @'[Embed m] . runInputConst locDomain $ lookupAccount locale uid) getLegalHoldStatus' :: ( MonadLogger m, @@ -1476,12 +1506,17 @@ getEmailForProfile _ EmailVisibleToSelf' = Nothing -- | Find user accounts for a given identity, both activated and those -- currently pending activation. -lookupAccountsByIdentity :: Either Email Phone -> Bool -> (AppT r) [UserAccount] +lookupAccountsByIdentity :: + Members '[Input (Local ()), UserQuery] r => + Either Email Phone -> + Bool -> + AppT r [UserAccount] lookupAccountsByIdentity emailOrPhone includePendingInvitations = do let uk = either userEmailKey userPhoneKey emailOrPhone activeUid <- wrapClient $ Data.lookupKey uk uidFromKey <- (>>= fst) <$> wrapClient (Data.lookupActivationCode uk) - result <- wrapClient $ Data.lookupAccounts (nub $ catMaybes [activeUid, uidFromKey]) + locale <- setDefaultUserLocale <$> view settings + result <- liftSem $ Data.lookupAccounts locale (nub $ catMaybes [activeUid, uidFromKey]) if includePendingInvitations then pure result else pure $ filter ((/= PendingInvitation) . accountStatus) result diff --git a/services/brig/src/Brig/API/Util.hs b/services/brig/src/Brig/API/Util.hs index f7e068a29b..62afee0091 100644 --- a/services/brig/src/Brig/API/Util.hs +++ b/services/brig/src/Brig/API/Util.hs @@ -36,6 +36,7 @@ import Brig.App import qualified Brig.Data.User as Data import Brig.Options (FederationDomainConfig, federationDomainConfigs) import qualified Brig.Options as Opts +import Brig.Sem.UserQuery (UserQuery) import Brig.Types import Brig.Types.Intra (accountUser) import Control.Lens (view) @@ -49,6 +50,8 @@ import Data.Qualified import Data.String.Conversions (cs) import Data.Text.Ascii (AsciiText (toText)) import Imports +import Polysemy +import Polysemy.Input import System.Logger (Msg) import qualified System.Logger as Log import UnliftIO.Async @@ -66,7 +69,14 @@ lookupProfilesMaybeFilterSameTeamOnly self us = do Just team -> filter (\x -> profileTeam x == Just team) us Nothing -> us -fetchUserIdentity :: UserId -> (AppT r) (Maybe UserIdentity) +fetchUserIdentity :: + Members + '[ Input (Local ()), + UserQuery + ] + r => + UserId -> + AppT r (Maybe UserIdentity) fetchUserIdentity uid = lookupSelfProfile uid >>= maybe @@ -74,8 +84,17 @@ fetchUserIdentity uid = (pure . userIdentity . selfUser) -- | Obtain a profile for a user as he can see himself. -lookupSelfProfile :: UserId -> (AppT r) (Maybe SelfProfile) -lookupSelfProfile = fmap (fmap mk) . wrapClient . Data.lookupAccount +lookupSelfProfile :: + Members + '[ Input (Local ()), + UserQuery + ] + r => + UserId -> + AppT r (Maybe SelfProfile) +lookupSelfProfile u = do + locale <- Opts.setDefaultUserLocale <$> view settings + fmap (fmap mk) $ liftSem $ Data.lookupAccount locale u where mk a = SelfProfile (accountUser a) diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 29c804a72d..f3889579d1 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -79,6 +79,7 @@ module Brig.App wrapHttp, HttpClientIO (..), liftSem, + liftSemE, ) where @@ -100,7 +101,7 @@ import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetStore.CodeStore import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import Brig.Sem.PasswordResetSupply.IO -import Brig.Sem.UserQuery (UserQuery) +import Brig.Sem.UserQuery (ReAuthError, UserQuery) import Brig.Sem.UserQuery.Cassandra import Brig.Team.Template import Brig.Template (Localised, TemplateBranding, forLocale, genTemplateBranding) @@ -145,7 +146,9 @@ import OpenSSL.Session (SSLOption (..)) import qualified OpenSSL.Session as SSL import qualified OpenSSL.X509.SystemStore as SSL import Polysemy +import qualified Polysemy.Error as P import Polysemy.Final +import Polysemy.Input import qualified Polysemy.TinyLog as P import qualified Ropes.Nexmo as Nexmo import qualified Ropes.Twilio as Twilio @@ -155,7 +158,9 @@ import qualified System.FilePath as Path import System.Logger.Class hiding (Settings, settings) import qualified System.Logger.Class as LC import qualified System.Logger.Extended as Log +import qualified UnliftIO.Exception as UnliftIO import Util.Options +import Wire.API.Error (APIError, toWai) import Wire.API.User.Identity (Email) import Wire.Sem.Logger.TinyLog import Wire.Sem.Now (Now) @@ -450,7 +455,9 @@ type BrigCanonicalEffects = PasswordResetSupply, CodeStore, P.TinyLog, + Input (Local ()), Embed Cas.Client, + P.Error ReAuthError, Embed IO, Final IO ] @@ -502,6 +509,9 @@ instance MonadReader Env (AppT r) where liftSem :: Sem r a -> AppT r a liftSem sem = AppT $ lift sem +liftSemE :: ExceptT e (Sem r) a -> ExceptT e (AppT r) a +liftSemE = mapExceptT liftSem + instance MonadIO m => MonadLogger (ReaderT Env m) where log l m = do g <- view applog @@ -606,11 +616,27 @@ instance MonadIndexIO (AppT r) => MonadIndexIO (ExceptT err (AppT r)) where instance Monad m => HasRequestId (AppT r) where getRequestId = view requestId +-- TODO(md): Copied from Galley.App. Move this utility function to a library. +interpretErrorToException :: + (Exception exc, Member (Embed IO) r) => + (err -> exc) -> + Sem (P.Error err ': r) a -> + Sem r a +interpretErrorToException f = either (embed @IO . UnliftIO.throwIO . f) pure <=< P.runError + +interpretWaiErrorToException :: + (APIError e, Member (Embed IO) r) => + Sem (P.Error e ': r) a -> + Sem r a +interpretWaiErrorToException = interpretErrorToException toWai + runAppT :: Env -> AppT BrigCanonicalEffects a -> IO a runAppT e (AppT ma) = runFinal . embedToFinal + . interpretWaiErrorToException . interpretClientToIO (_casClient e) + . runInputConst (toLocalUnsafe (Opt.setFederationDomain . _settings $ e) ()) . loggerToTinyLogReqId (view requestId e) (view applog e) . codeStoreToCassandra @Cas.Client . passwordResetSupplyToIO diff --git a/services/brig/src/Brig/Budget.hs b/services/brig/src/Brig/Budget.hs deleted file mode 100644 index cf952a3ed7..0000000000 --- a/services/brig/src/Brig/Budget.hs +++ /dev/null @@ -1,98 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Brig.Budget - ( Budget (..), - BudgetKey (..), - Budgeted (..), - withBudget, - checkBudget, - lookupBudget, - insertBudget, - ) -where - -import Cassandra -import Data.Time.Clock -import Imports - -data Budget = Budget - { budgetTimeout :: !NominalDiffTime, - budgetValue :: !Int32 - } - deriving (Eq, Show, Generic) - -data Budgeted a - = BudgetExhausted NominalDiffTime - | BudgetedValue a Int32 - deriving (Eq, Show, Generic) - -newtype BudgetKey = BudgetKey Text - deriving (Eq, Show, Cql) - --- | @withBudget (BudgetKey "k") (Budget 30 5) action@ runs @action@ at most 5 times every 30 --- seconds. @"k"@ is used for keeping different calls to 'withBudget' apart; use something --- there that's unique to your context, like @"login#" <> uid@. --- --- See the docs in "Gundeck.ThreadBudget" for related work. --- --- FUTUREWORK: encourage caller to define their own type for budget keys (rather than using an --- untyped text), and represent the types in a way that guarantees that if i'm using a local --- type that i don't export, then nobody will be able to use my namespace. --- --- FUTUREWORK: exceptions are not handled very nicely, but it's not clear what it would mean --- to improve this. -withBudget :: MonadClient m => BudgetKey -> Budget -> m a -> m (Budgeted a) -withBudget k b ma = do - Budget ttl val <- fromMaybe b <$> lookupBudget k - let remaining = val - 1 - if remaining < 0 - then pure (BudgetExhausted ttl) - else do - a <- ma - insertBudget k (Budget ttl remaining) - pure (BudgetedValue a remaining) - --- | Like 'withBudget', but does not decrease budget, only takes a look. -checkBudget :: MonadClient m => BudgetKey -> Budget -> m (Budgeted ()) -checkBudget k b = do - Budget ttl val <- fromMaybe b <$> lookupBudget k - let remaining = val - 1 - pure $ - if remaining < 0 - then BudgetExhausted ttl - else BudgetedValue () remaining - -lookupBudget :: MonadClient m => BudgetKey -> m (Maybe Budget) -lookupBudget k = fmap mk <$> query1 budgetSelect (params One (Identity k)) - where - mk (val, ttl) = Budget (fromIntegral ttl) val - -insertBudget :: MonadClient m => BudgetKey -> Budget -> m () -insertBudget k (Budget ttl val) = - retry x5 $ write budgetInsert (params One (k, val, round ttl)) - -------------------------------------------------------------------------------- --- Queries - -budgetInsert :: PrepQuery W (BudgetKey, Int32, Int32) () -budgetInsert = "INSERT INTO budget (key, budget) VALUES (?, ?) USING TTL ?" - -budgetSelect :: PrepQuery R (Identity BudgetKey) (Int32, Int32) -budgetSelect = "SELECT budget, ttl(budget) FROM budget where key = ?" diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 13d690ed9c..01b44ae55d 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -31,7 +31,7 @@ module Brig.Data.Activation ) where -import Brig.App (Env) +import Brig.App (Env, qualifyLocal, settings) import Brig.Data.User import Brig.Data.UserKey import Brig.Options @@ -39,10 +39,12 @@ import qualified Brig.Sem.CodeStore as E import Brig.Sem.CodeStore.Cassandra import qualified Brig.Sem.PasswordResetSupply as E import Brig.Sem.PasswordResetSupply.IO +import Brig.Sem.UserQuery.Cassandra import Brig.Types import Brig.Types.Intra import Cassandra import Control.Error +import Control.Lens (view) import Data.Id import Data.Text (pack) import qualified Data.Text.Ascii as Ascii @@ -52,6 +54,7 @@ import Imports import OpenSSL.BN (randIntegerZeroToNMinusOne) import OpenSSL.EVP.Digest (digestBS, getDigestByName) import Polysemy +import Polysemy.Input import Text.Printf (printf) import Wire.API.User @@ -100,7 +103,16 @@ activateKey k c u = verifyCode k c >>= pickUser >>= activate where pickUser (uk, u') = maybe (throwE invalidUser) (pure . (uk,)) (u <|> u') activate (key, uid) = do - a <- lift (lookupAccount uid) >>= maybe (throwE invalidUser) pure + locale <- setDefaultUserLocale <$> view settings + locDomain <- qualifyLocal () + a <- + lift + ( runM + . userQueryToCassandra @m @'[Embed m] + . runInputConst locDomain + $ lookupAccount locale uid + ) + >>= maybe (throwE invalidUser) pure unless (accountStatus a == Active) $ -- this is never 'PendingActivation' in the flow this function is used in. throwE invalidCode case userIdentity (accountUser a) of diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 51b42529a3..edcb605f89 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -57,6 +57,8 @@ import Brig.App import Brig.Data.Instances () import Brig.Data.User (AuthError (..), ReAuthError (..)) import qualified Brig.Data.User as User +import Brig.Options (setDefaultUserLocale) +import Brig.Sem.UserQuery.Cassandra import Brig.Types.Instances () import Brig.Types.User.Auth (CookieLabel) import Brig.User.Auth.DB.Instances () @@ -71,6 +73,7 @@ import Control.Retry import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Conversion (toByteString, toByteString') import qualified Data.ByteString.Lazy as LBS +import Data.Either.Combinators import qualified Data.HashMap.Strict as HashMap import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) @@ -81,6 +84,9 @@ import qualified Data.Set as Set import qualified Data.Text as Text import qualified Data.UUID as UUID import Imports +import Polysemy hiding (run) +import Polysemy.Error +import Polysemy.Input import System.CryptoBox (Result (Success)) import qualified System.CryptoBox as CryptoBox import System.Logger.Class (field, msg, val) @@ -124,6 +130,7 @@ addClient :: addClient = addClientWithReAuthPolicy reAuthForNewClients addClientWithReAuthPolicy :: + forall m. (MonadClient m, MonadReader Brig.App.Env m) => ReAuthPolicy -> UserId -> @@ -135,12 +142,21 @@ addClientWithReAuthPolicy :: ExceptT ClientDataError m (Client, [Client], Word) addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients loc cps = do clients <- lookupClients u + locale <- setDefaultUserLocale <$> view settings + locDomain <- qualifyLocal () let typed = filter ((== newClientType c) . clientType) clients let count = length typed let upsert = any exists typed - when (reAuthPolicy count upsert) $ - fmapLT ClientReAuthError $ - User.reauthenticate u (newClientPassword c) + when (reAuthPolicy count upsert) $ do + o <- + lift + . runM + . userQueryToCassandra @m @'[Embed m] + $ runError + . mapError ClientReAuthError + . runInputConst locDomain + $ User.reauthenticate locale u (newClientPassword c) + whenLeft o throwE let capacity = fmap (+ (- count)) limit unless (maybe True (> 0) capacity || upsert) $ throwE TooManyClients diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 034d46fe34..a01125d4d1 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -78,7 +78,18 @@ import Brig.App (Env, currentTime, settings, viewFederationDomain, zauthEnv) import Brig.Data.Instances () import Brig.Options import Brig.Password -import Brig.Sem.UserQuery (UserQuery, getId, getLocale, getName, getUsers) +import Brig.Sem.UserQuery + ( AuthError (..), + ReAuthError (..), + UserQuery, + getAuthentication, + getId, + getLocale, + getName, + getUsers, + lookupAccount, + lookupAccounts, + ) import Brig.Types import Brig.Types.Intra import qualified Brig.ZAuth as ZAuth @@ -98,25 +109,11 @@ import Data.UUID.V4 import Galley.Types.Bot import Imports import Polysemy +import Polysemy.Error +import Polysemy.Input import qualified Wire.API.Team.Feature as ApiFt import Wire.API.User.RichInfo --- | Authentication errors. -data AuthError - = AuthInvalidUser - | AuthInvalidCredentials - | AuthSuspended - | AuthEphemeral - | AuthPendingInvitation - --- | Re-authentication errors. -data ReAuthError - = ReAuthError !AuthError - | ReAuthMissingPassword - | ReAuthCodeVerificationRequired - | ReAuthCodeVerificationNoPendingCode - | ReAuthCodeVerificationNoEmail - -- | Preconditions: -- -- 1. @newUserUUID u == Just inv || isNothing (newUserUUID u)@. @@ -186,42 +183,64 @@ newAccountInviteViaScim uid tid locale name email = do ManagedByScim -- | Mandatory password authentication. -authenticate :: MonadClient m => UserId -> PlainTextPassword -> ExceptT AuthError m () +authenticate :: + Members + '[ Error AuthError, + UserQuery + ] + r => + UserId -> + PlainTextPassword -> + Sem r () authenticate u pw = - lift (lookupAuth u) >>= \case - Nothing -> throwE AuthInvalidUser - Just (_, Deleted) -> throwE AuthInvalidUser - Just (_, Suspended) -> throwE AuthSuspended - Just (_, Ephemeral) -> throwE AuthEphemeral - Just (_, PendingInvitation) -> throwE AuthPendingInvitation - Just (Nothing, _) -> throwE AuthInvalidCredentials + lookupAuth u >>= \case + Nothing -> throw AuthInvalidUser + Just (_, Deleted) -> throw AuthInvalidUser + Just (_, Suspended) -> throw AuthSuspended + Just (_, Ephemeral) -> throw AuthEphemeral + Just (_, PendingInvitation) -> throw AuthPendingInvitation + Just (Nothing, _) -> throw AuthInvalidCredentials Just (Just pw', Active) -> unless (verifyPassword pw pw') $ - throwE AuthInvalidCredentials + throw AuthInvalidCredentials -- | Password reauthentication. If the account has a password, reauthentication -- is mandatory. If the account has no password, or is an SSO user, and no password is given, -- reauthentication is a no-op. -reauthenticate :: (MonadClient m, MonadReader Env m) => UserId -> Maybe PlainTextPassword -> ExceptT ReAuthError m () -reauthenticate u pw = - lift (lookupAuth u) >>= \case - Nothing -> throwE (ReAuthError AuthInvalidUser) - Just (_, Deleted) -> throwE (ReAuthError AuthInvalidUser) - Just (_, Suspended) -> throwE (ReAuthError AuthSuspended) - Just (_, PendingInvitation) -> throwE (ReAuthError AuthPendingInvitation) - Just (Nothing, _) -> for_ pw $ const (throwE $ ReAuthError AuthInvalidCredentials) +reauthenticate :: + Members + '[ Error ReAuthError, + Input (Local ()), + UserQuery + ] + r => + Locale -> + UserId -> + Maybe PlainTextPassword -> + Sem r () +reauthenticate locale u pw = + lookupAuth u >>= \case + Nothing -> throw (ReAuthError AuthInvalidUser) + Just (_, Deleted) -> throw (ReAuthError AuthInvalidUser) + Just (_, Suspended) -> throw (ReAuthError AuthSuspended) + Just (_, PendingInvitation) -> throw (ReAuthError AuthPendingInvitation) + Just (Nothing, _) -> for_ pw $ const (throw $ ReAuthError AuthInvalidCredentials) Just (Just pw', Active) -> maybeReAuth pw' Just (Just pw', Ephemeral) -> maybeReAuth pw' where maybeReAuth pw' = case pw of - Nothing -> unlessM (isSamlUser u) $ throwE ReAuthMissingPassword + Nothing -> unlessM (isSamlUser locale u) $ throw ReAuthMissingPassword Just p -> unless (verifyPassword p pw') $ - throwE (ReAuthError AuthInvalidCredentials) + throw (ReAuthError AuthInvalidCredentials) -isSamlUser :: (MonadClient m, MonadReader Env m) => UserId -> m Bool -isSamlUser uid = do - account <- lookupAccount uid +isSamlUser :: + Members '[Input (Local ()), UserQuery] r => + Locale -> + UserId -> + Sem r Bool +isSamlUser locale uid = do + account <- lookupAccount locale uid case userIdentity . accountUser =<< account of Just (SSOIdentity (UserSSOId _) _ _) -> pure True _ -> pure False @@ -438,8 +457,11 @@ lookupUserTeam u = (runIdentity =<<) <$> retry x1 (query1 teamSelect (params LocalQuorum (Identity u))) -lookupAuth :: MonadClient m => (MonadClient m) => UserId -> m (Maybe (Maybe Password, AccountStatus)) -lookupAuth u = fmap f <$> retry x1 (query1 authSelect (params LocalQuorum (Identity u))) +lookupAuth :: + Member UserQuery r => + UserId -> + Sem r (Maybe (Maybe Password, AccountStatus)) +lookupAuth u = fmap f <$> getAuthentication u where f (pw, st) = (pw, fromMaybe Active st) @@ -456,15 +478,6 @@ lookupUsers :: lookupUsers loc locale hpi usrs = toUsers (tDomain loc) locale hpi <$> getUsers usrs -lookupAccount :: (MonadClient m, MonadReader Env m) => UserId -> m (Maybe UserAccount) -lookupAccount u = listToMaybe <$> lookupAccounts [u] - -lookupAccounts :: (MonadClient m, MonadReader Env m) => [UserId] -> m [UserAccount] -lookupAccounts usrs = do - loc <- setDefaultUserLocale <$> view settings - domain <- viewFederationDomain - fmap (toUserAccount domain loc) <$> retry x1 (query accountsSelect (params LocalQuorum (Identity usrs))) - lookupServiceUser :: MonadClient m => ProviderId -> ServiceId -> BotId -> m (Maybe (ConvId, Maybe TeamId)) lookupServiceUser pid sid bid = retry x1 (query1 cql (params LocalQuorum (pid, sid, bid))) where @@ -558,31 +571,6 @@ type UserRowInsert = ManagedBy ) --- Represents a 'UserAccount' -type AccountRow = - ( UserId, - Name, - Maybe Pict, - Maybe Email, - Maybe Phone, - Maybe UserSSOId, - ColourId, - Maybe [Asset], - Activated, - Maybe AccountStatus, - Maybe UTCTimeMillis, - Maybe Language, - Maybe Country, - Maybe ProviderId, - Maybe ServiceId, - Maybe Handle, - Maybe TeamId, - Maybe ManagedBy - ) - -authSelect :: PrepQuery R (Identity UserId) (Maybe Password, Maybe AccountStatus) -authSelect = "SELECT password, status FROM user WHERE id = ?" - passwordSelect :: PrepQuery R (Identity UserId) (Identity (Maybe Password)) passwordSelect = "SELECT password FROM user WHERE id = ?" @@ -604,13 +592,6 @@ richInfoSelectMulti = "SELECT user, json FROM rich_info WHERE user in ?" teamSelect :: PrepQuery R (Identity UserId) (Identity (Maybe TeamId)) teamSelect = "SELECT team FROM user WHERE id = ?" -accountsSelect :: PrepQuery R (Identity [UserId]) AccountRow -accountsSelect = - "SELECT id, name, picture, email, phone, sso_id, accent_id, assets, \ - \activated, status, expires, language, country, provider, \ - \service, handle, team, managed_by \ - \FROM user WHERE id IN ?" - userInsert :: PrepQuery W UserRowInsert () userInsert = "INSERT INTO user (id, name, picture, assets, email, phone, sso_id, \ @@ -678,54 +659,6 @@ userRichInfoUpdate = "UPDATE rich_info SET json = ? WHERE user = ?" ------------------------------------------------------------------------------- -- Conversions --- | Construct a 'UserAccount' from a raw user record in the database. -toUserAccount :: Domain -> Locale -> AccountRow -> UserAccount -toUserAccount - domain - defaultLocale - ( uid, - name, - pict, - email, - phone, - ssoid, - accent, - assets, - activated, - status, - expires, - lan, - con, - pid, - sid, - handle, - tid, - managed_by - ) = - let ident = toIdentity activated email phone ssoid - deleted = Just Deleted == status - expiration = if status == Just Ephemeral then expires else Nothing - loc = toLocale defaultLocale (lan, con) - svc = newServiceRef <$> sid <*> pid - in UserAccount - ( User - uid - (Qualified uid domain) - ident - name - (fromMaybe noPict pict) - (fromMaybe [] assets) - accent - deleted - loc - svc - handle - expiration - tid - (fromMaybe ManagedByWire managed_by) - ) - (fromMaybe Active status) - toUsers :: Domain -> Locale -> HavePendingInvitations -> [UserRow] -> [User] toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp where diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 3e1baa90e5..51a0c37dca 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -25,14 +25,17 @@ import Bilge.RPC (HasRequestId) import qualified Brig.API.User as API import Brig.App import Brig.InternalEvent.Types -import Brig.Options (defDeleteThrottleMillis, setDeleteThrottleMillis) +import Brig.Options (defDeleteThrottleMillis, setDefaultUserLocale, setDeleteThrottleMillis) import qualified Brig.Provider.API as API +import Brig.Sem.UserQuery.Cassandra import Brig.User.Search.Index (MonadIndexIO) import Cassandra (MonadClient) import Control.Lens (view) import Control.Monad.Catch import Data.ByteString.Conversion import Imports +import Polysemy +import Polysemy.Input import System.Logger.Class (field, msg, val, (~~)) import qualified System.Logger.Class as Log import UnliftIO (timeout) @@ -41,6 +44,7 @@ import UnliftIO (timeout) -- -- Has a one-minute timeout that should be enough for anything that it does. onEvent :: + forall m. ( Log.MonadLogger m, MonadCatch m, MonadThrow m, @@ -55,23 +59,26 @@ onEvent :: ) => InternalNotification -> m () -onEvent n = handleTimeout $ case n of - DeleteUser uid -> do - Log.info $ - msg (val "Processing user delete event") - ~~ field "user" (toByteString uid) - API.lookupAccount uid >>= mapM_ API.deleteAccount - -- As user deletions are expensive resource-wise in the context of - -- bulk user deletions (e.g. during team deletions), - -- wait 'delay' ms before processing the next event - delay <- fromMaybe defDeleteThrottleMillis . setDeleteThrottleMillis <$> view settings - liftIO $ threadDelay (1000 * delay) - DeleteService pid sid -> do - Log.info $ - msg (val "Processing service delete event") - ~~ field "provider" (toByteString pid) - ~~ field "service" (toByteString sid) - API.finishDeleteService pid sid +onEvent n = do + locale <- setDefaultUserLocale <$> view settings + locDomain <- qualifyLocal () + handleTimeout $ case n of + DeleteUser uid -> do + Log.info $ + msg (val "Processing user delete event") + ~~ field "user" (toByteString uid) + (runM . userQueryToCassandra @m @'[Embed m] . runInputConst locDomain $ API.lookupAccount locale uid) >>= mapM_ API.deleteAccount + -- As user deletions are expensive resource-wise in the context of + -- bulk user deletions (e.g. during team deletions), + -- wait 'delay' ms before processing the next event + delay <- fromMaybe defDeleteThrottleMillis . setDeleteThrottleMillis <$> view settings + liftIO $ threadDelay (1000 * delay) + DeleteService pid sid -> do + Log.info $ + msg (val "Processing service delete event") + ~~ field "provider" (toByteString pid) + ~~ field "service" (toByteString sid) + API.finishDeleteService pid sid where handleTimeout act = timeout 60000000 act >>= \case diff --git a/services/brig/src/Brig/Phone.hs b/services/brig/src/Brig/Phone.hs index 44eb72f796..532381f7ac 100644 --- a/services/brig/src/Brig/Phone.hs +++ b/services/brig/src/Brig/Phone.hs @@ -39,7 +39,8 @@ where import Bilge.Retry (httpHandlers) import Brig.App -import Brig.Budget +import Brig.Sem.BudgetStore +import Brig.Sem.BudgetStore.Cassandra import Brig.Types import Cassandra (MonadClient) import Control.Lens (view) @@ -51,6 +52,7 @@ import qualified Data.Text as Text import Data.Time.Clock import Imports import Network.HTTP.Client (HttpException, Manager) +import Polysemy import qualified Ropes.Nexmo as Nexmo import Ropes.Twilio (LookupDetail (..)) import qualified Ropes.Twilio as Twilio @@ -232,6 +234,7 @@ smsBudget = } withSmsBudget :: + forall m a. ( MonadClient m, Log.MonadLogger m, MonadReader Env m @@ -241,7 +244,7 @@ withSmsBudget :: m a withSmsBudget phone go = do let k = BudgetKey ("sms#" <> phone) - r <- withBudget k smsBudget go + r <- runM $ budgetStoreToCassandra @m @'[Embed m] $ withBudget k smsBudget (embed @m go) case r of BudgetExhausted t -> do Log.info $ @@ -267,6 +270,7 @@ callBudget = } withCallBudget :: + forall m a. ( MonadClient m, Log.MonadLogger m, MonadReader Env m @@ -276,7 +280,7 @@ withCallBudget :: m a withCallBudget phone go = do let k = BudgetKey ("call#" <> phone) - r <- withBudget k callBudget go + r <- runM $ budgetStoreToCassandra @m @'[Embed m] $ withBudget k callBudget (embed @m go) case r of BudgetExhausted t -> do Log.info $ diff --git a/services/brig/src/Brig/Sem/UserQuery.hs b/services/brig/src/Brig/Sem/UserQuery.hs index 1db79e5d35..c6fc1df851 100644 --- a/services/brig/src/Brig/Sem/UserQuery.hs +++ b/services/brig/src/Brig/Sem/UserQuery.hs @@ -17,16 +17,53 @@ {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -module Brig.Sem.UserQuery where +module Brig.Sem.UserQuery + ( UserQuery (..), + getId, + getUsers, + getName, + getLocale, + getAuthentication, + getPassword, + getActivated, + getAccountStatus, + getAccountStatuses, + getTeam, + getAccounts, + insertAccount, + updateUser, + + -- * effect-derived functions + lookupAccount, + lookupAccounts, + + -- * error types + AuthError (..), + ReAuthError (..), + + -- * misc types + AccountRow, + UserRow, + UserRowInsert, + ) +where import Brig.Password import Brig.Types import Brig.Types.Intra +import Data.Domain import Data.Handle import Data.Id import Data.Json.Util +import Data.Qualified import Imports +import Network.HTTP.Types.Status +import qualified Network.Wai.Utilities.Error as Wai import Polysemy +import Polysemy.Input +import Wire.API.Error +import qualified Wire.API.Error.Brig as E +import Wire.API.Provider.Service type Activated = Bool @@ -97,6 +134,135 @@ type UserRowInsert = deriving instance Show UserRowInsert +-- | Authentication errors. +data AuthError + = AuthInvalidUser + | AuthInvalidCredentials + | AuthSuspended + | AuthEphemeral + | AuthPendingInvitation + +instance APIError AuthError where + toWai AuthInvalidUser = errorToWai @'E.BadCredentials + toWai AuthInvalidCredentials = errorToWai @'E.BadCredentials + toWai AuthSuspended = accountSuspended + toWai AuthEphemeral = accountEphemeral + toWai AuthPendingInvitation = accountPending + +-- TODO(md): all the Wai.Error values in this module have been copied from +-- Brig.API.Error to avoid a cycle in module imports. Fix that. +accountSuspended :: Wai.Error +accountSuspended = Wai.mkError status403 "suspended" "Account suspended." + +accountEphemeral :: Wai.Error +accountEphemeral = Wai.mkError status403 "ephemeral" "Account is ephemeral." + +accountPending :: Wai.Error +accountPending = Wai.mkError status403 "pending-activation" "Account pending activation." + +-- | Re-authentication errors. +data ReAuthError + = ReAuthError !AuthError + | ReAuthMissingPassword + | ReAuthCodeVerificationRequired + | ReAuthCodeVerificationNoPendingCode + | ReAuthCodeVerificationNoEmail + +instance APIError ReAuthError where + toWai (ReAuthError e) = toWai e + toWai ReAuthMissingPassword = errorToWai @'E.MissingAuth + toWai ReAuthCodeVerificationRequired = verificationCodeRequired + toWai ReAuthCodeVerificationNoPendingCode = verificationCodeNoPendingCode + toWai ReAuthCodeVerificationNoEmail = verificationCodeNoEmail + +verificationCodeRequired :: Wai.Error +verificationCodeRequired = Wai.mkError status403 "code-authentication-required" "Verification code required." + +verificationCodeNoPendingCode :: Wai.Error +verificationCodeNoPendingCode = Wai.mkError status403 "code-authentication-failed" "Code authentication failed (no such code)." + +verificationCodeNoEmail :: Wai.Error +verificationCodeNoEmail = Wai.mkError status403 "code-authentication-failed" "Code authentication failed (no such email)." + +------------------------------------------------------------------------------- +-- Conversions + +-- | Construct a 'UserAccount' from a raw user record in the database. +toUserAccount :: Domain -> Locale -> AccountRow -> UserAccount +toUserAccount + domain + defaultLocale + ( uid, + name, + pict, + email, + phone, + ssoid, + accent, + assets, + activated, + status, + expires, + lan, + con, + pid, + sid, + handle, + tid, + managed_by + ) = + let ident = toIdentity activated email phone ssoid + deleted = Just Deleted == status + expiration = if status == Just Ephemeral then expires else Nothing + loc = toLocale defaultLocale (lan, con) + svc = newServiceRef <$> sid <*> pid + in UserAccount + ( User + uid + (Qualified uid domain) + ident + name + (fromMaybe noPict pict) + (fromMaybe [] assets) + accent + deleted + loc + svc + handle + expiration + tid + (fromMaybe ManagedByWire managed_by) + ) + (fromMaybe Active status) + +toLocale :: Locale -> (Maybe Language, Maybe Country) -> Locale +toLocale _ (Just l, c) = Locale l c +toLocale l _ = l + +-- | Construct a 'UserIdentity'. +-- +-- If the user is not activated, 'toIdentity' will return 'Nothing' as a precaution, because +-- elsewhere we rely on the fact that a non-empty 'UserIdentity' means that the user is +-- activated. +-- +-- The reason it's just a "precaution" is that we /also/ have an invariant that having an +-- email or phone in the database means the user has to be activated. +toIdentity :: + -- | Whether the user is activated + Bool -> + Maybe Email -> + Maybe Phone -> + Maybe UserSSOId -> + Maybe UserIdentity +toIdentity True (Just e) (Just p) Nothing = Just $! FullIdentity e p +toIdentity True (Just e) Nothing Nothing = Just $! EmailIdentity e +toIdentity True Nothing (Just p) Nothing = Just $! PhoneIdentity p +toIdentity True email phone (Just ssoid) = Just $! SSOIdentity ssoid email phone +toIdentity True Nothing Nothing Nothing = Nothing +toIdentity False _ _ _ = Nothing + +------------------------------------------------------------------------------- + data UserQuery m a where GetId :: UserId -> UserQuery m (Maybe UserId) -- idSelect GetUsers :: [UserId] -> UserQuery m [UserRow] -- usersSelect @@ -122,3 +288,19 @@ data UserQuery m a where UpdateUser :: UserId -> UserUpdate -> UserQuery m () makeSem ''UserQuery + +lookupAccount :: + Members '[Input (Local ()), UserQuery] r => + Locale -> + UserId -> + Sem r (Maybe UserAccount) +lookupAccount locale u = listToMaybe <$> lookupAccounts locale [u] + +lookupAccounts :: + Members '[Input (Local ()), UserQuery] r => + Locale -> + [UserId] -> + Sem r [UserAccount] +lookupAccounts locale users = do + domain <- tDomain <$> input @(Local ()) + fmap (toUserAccount domain locale) <$> getAccounts users diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 189c2e1245..84ecb19000 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -34,6 +34,7 @@ import qualified Brig.Email as Email import qualified Brig.IO.Intra as Intra import Brig.Options (setMaxTeamSize, setTeamInvitationTimeout) import qualified Brig.Phone as Phone +import Brig.Sem.UserQuery (UserQuery) import qualified Brig.Team.DB as DB import Brig.Team.Email import Brig.Team.Util (ensurePermissionToAddUser, ensurePermissions) @@ -48,6 +49,7 @@ import Data.Aeson hiding (json) import Data.ByteString.Conversion import Data.Id import qualified Data.List1 as List1 +import Data.Qualified import Data.Range import Data.String.Conversions (cs) import qualified Data.Swagger.Build.Api as Doc @@ -61,6 +63,8 @@ import Network.Wai.Routing import Network.Wai.Utilities hiding (code, message) import Network.Wai.Utilities.Swagger (document) import qualified Network.Wai.Utilities.Swagger as Doc +import Polysemy +import Polysemy.Input import System.Logger (Msg) import qualified System.Logger.Class as Log import Util.Logging (logFunction, logTeam) @@ -71,7 +75,9 @@ import qualified Wire.API.Team.Role as Public import qualified Wire.API.Team.Size as Public import qualified Wire.API.User as Public -routesPublic :: Routes Doc.ApiBuilder (Handler r) () +routesPublic :: + Members '[Input (Local ()), UserQuery] r => + Routes Doc.ApiBuilder (Handler r) () routesPublic = do post "/teams/:tid/invitations" (continue createInvitationPublicH) $ accept "application" "json" @@ -237,7 +243,10 @@ newtype FoundInvitationCode = FoundInvitationCode InvitationCode instance ToJSON FoundInvitationCode where toJSON (FoundInvitationCode c) = object ["code" .= c] -createInvitationPublicH :: JSON ::: UserId ::: TeamId ::: JsonRequest Public.InvitationRequest -> (Handler r) Response +createInvitationPublicH :: + Members '[Input (Local ()), UserQuery] r => + JSON ::: UserId ::: TeamId ::: JsonRequest Public.InvitationRequest -> + Handler r Response createInvitationPublicH (_ ::: uid ::: tid ::: req) = do body <- parseJsonBody req newInv <- createInvitationPublic uid tid body @@ -253,7 +262,12 @@ data CreateInvitationInviter = CreateInvitationInviter } deriving (Eq, Show) -createInvitationPublic :: UserId -> TeamId -> Public.InvitationRequest -> (Handler r) Public.Invitation +createInvitationPublic :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + TeamId -> + Public.InvitationRequest -> + Handler r Public.Invitation createInvitationPublic uid tid body = do let inviteeRole = fromMaybe Team.defaultRole . irRole $ body inviter <- do diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index 591b8719b6..c6610cd7d0 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -21,11 +21,12 @@ module Brig.User.API.Auth ) where -import Brig.API.Error +import Brig.API.Error hiding (Error) import Brig.API.Handler import Brig.API.Types import qualified Brig.API.User as User import Brig.App +import Brig.Options (setDefaultUserLocale) import Brig.Phone import Brig.Sem.UserQuery (UserQuery) import Brig.Types.Intra (ReAuthUser, reAuthCode, reAuthCodeAction, reAuthPassword) @@ -34,6 +35,7 @@ import qualified Brig.User.Auth as Auth import qualified Brig.User.Auth.Cookie as Auth import qualified Brig.ZAuth as ZAuth import Control.Error (catchE) +import Control.Lens (view) import Control.Monad.Except import Control.Monad.Trans.Except (throwE) import qualified Data.ByteString as BS @@ -43,12 +45,13 @@ import Data.Id import Data.List1 (List1) import qualified Data.List1 as List1 import Data.Predicate +import Data.Qualified import qualified Data.Swagger.Build.Api as Doc import qualified Data.ZAuth.Token as ZAuth import Imports import Network.HTTP.Types.Status import Network.Wai (Response) -import Network.Wai.Predicate +import Network.Wai.Predicate hiding (Error) import qualified Network.Wai.Predicate as P import qualified Network.Wai.Predicate.Request as R import Network.Wai.Routing @@ -59,6 +62,8 @@ import qualified Network.Wai.Utilities.Response as WaiResp import Network.Wai.Utilities.Swagger (document) import qualified Network.Wai.Utilities.Swagger as Doc import Polysemy +import Polysemy.Error +import Polysemy.Input import Wire.API.Error import qualified Wire.API.Error.Brig as E import qualified Wire.API.User as Public @@ -193,7 +198,9 @@ routesPublic = do Doc.body (Doc.ref Public.modelRemoveCookies) Doc.end Doc.errorResponse (errorToWai @'E.BadCredentials) -routesInternal :: Routes a (Handler r) () +routesInternal :: + Members '[Error ReAuthError, Input (Local ()), UserQuery] r => + Routes a (Handler r) () routesInternal = do -- galley can query this endpoint at the right moment in the LegalHold flow post "/i/legalhold-login" (continue legalHoldLoginH) $ @@ -233,14 +240,32 @@ getLoginCode phone = do code <- lift $ wrapClient $ Auth.lookupLoginCode phone maybe (throwStd loginCodeNotFound) pure code -reAuthUserH :: UserId ::: JsonRequest ReAuthUser -> (Handler r) Response +reAuthUserH :: + Members + '[ Error ReAuthError, + Input (Local ()), + UserQuery + ] + r => + UserId ::: JsonRequest ReAuthUser -> + (Handler r) Response reAuthUserH (uid ::: req) = do reAuthUser uid =<< parseJsonBody req pure empty -reAuthUser :: UserId -> ReAuthUser -> (Handler r) () +reAuthUser :: + Members + '[ Error ReAuthError, + Input (Local ()), + UserQuery + ] + r => + UserId -> + ReAuthUser -> + (Handler r) () reAuthUser uid body = do - wrapClientE (User.reauthenticate uid (reAuthPassword body)) !>> reauthError + locale <- setDefaultUserLocale <$> view settings + lift (liftSem (User.reauthenticate locale uid (reAuthPassword body))) !>> reauthError case reAuthCodeAction body of Just action -> wrapHttpClientE (Auth.verifyCode (reAuthCode body) action uid) diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 4f9bef20c6..d262b8f832 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -41,7 +41,6 @@ import Bilge.RPC import Brig.API.Types import Brig.API.User (changeSingleAccountStatus) import Brig.App -import Brig.Budget import qualified Brig.Code as Code import qualified Brig.Data.Activation as Data import qualified Brig.Data.LoginCode as Data @@ -52,6 +51,9 @@ import Brig.Email import qualified Brig.IO.Intra as Intra import qualified Brig.Options as Opt import Brig.Phone +import Brig.Sem.BudgetStore +import Brig.Sem.BudgetStore.Cassandra +import Brig.Sem.UserQuery (UserQuery) import Brig.Sem.UserQuery.Cassandra import Brig.Types.Common import Brig.Types.Intra @@ -66,7 +68,9 @@ import Cassandra import Control.Error hiding (bool) import Control.Lens (to, view) import Control.Monad.Catch +import Control.Monad.Trans.Except import Data.ByteString.Conversion (toByteString) +import Data.Either.Combinators import Data.Handle (Handle) import Data.Id import qualified Data.List.NonEmpty as NE @@ -77,6 +81,8 @@ import qualified Data.ZAuth.Token as ZAuth import Imports import Network.Wai.Utilities.Error ((!>>)) import Polysemy +import Polysemy.Error +import Polysemy.Input import System.Logger (field, msg, val, (~~)) import qualified System.Logger.Class as Log import Wire.API.Team.Feature (TeamFeatureStatusNoConfig (..), TeamFeatureStatusValue (..)) @@ -156,30 +162,33 @@ login :: login (PasswordLogin li pw label code) typ = do uid <- resolveLoginId li lift . Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.login") - checkRetryLimit uid - Data.authenticate uid pw `catchE` \case - AuthInvalidUser -> loginFailed uid - AuthInvalidCredentials -> loginFailed uid + mLimitFailedLogins <- view (settings . to Opt.setLimitFailedLogins) + runBudgetStoreAction $ checkRetryLimit uid mLimitFailedLogins + o <- runUserQueryAction $ Data.authenticate uid pw + whenLeft o $ \case + AuthInvalidUser -> runBudgetStoreAction $ loginFailed uid mLimitFailedLogins + AuthInvalidCredentials -> runBudgetStoreAction $ loginFailed uid mLimitFailedLogins AuthSuspended -> throwE LoginSuspended AuthEphemeral -> throwE LoginEphemeral AuthPendingInvitation -> throwE LoginPendingActivation - verifyLoginCode code uid + verifyLoginCode code uid mLimitFailedLogins newAccess @ZAuth.User @ZAuth.Access uid typ label where - verifyLoginCode :: Maybe Code.Value -> UserId -> ExceptT LoginError m () - verifyLoginCode mbCode uid = + verifyLoginCode :: Maybe Code.Value -> UserId -> Maybe Opt.LimitFailedLogins -> ExceptT LoginError m () + verifyLoginCode mbCode uid mLimitFailedLogins = verifyCode mbCode Login uid `catchE` \case - VerificationCodeNoPendingCode -> loginFailedWith LoginCodeInvalid uid - VerificationCodeRequired -> loginFailedWith LoginCodeRequired uid - VerificationCodeNoEmail -> loginFailed uid + VerificationCodeNoPendingCode -> runBudgetStoreAction $ loginFailedWith LoginCodeInvalid uid mLimitFailedLogins + VerificationCodeRequired -> runBudgetStoreAction $ loginFailedWith LoginCodeRequired uid mLimitFailedLogins + VerificationCodeNoEmail -> runBudgetStoreAction $ loginFailed uid mLimitFailedLogins login (SmsLogin phone code label) typ = do uid <- resolveLoginId (LoginByPhone phone) lift . Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.login") - checkRetryLimit uid + mLimitFailedLogins <- view (settings . to Opt.setLimitFailedLogins) + runBudgetStoreAction $ checkRetryLimit uid mLimitFailedLogins ok <- lift $ Data.verifyLoginCode uid code unless ok $ - loginFailed uid + runBudgetStoreAction $ loginFailed uid mLimitFailedLogins newAccess @ZAuth.User @ZAuth.Access uid typ label verifyCode :: @@ -214,37 +223,61 @@ verifyCode mbCode action uid = do UserId -> ExceptT e m (Maybe Email, Maybe TeamId) getEmailAndTeamId u = do - mbAccount <- Data.lookupAccount u + locDomain <- qualifyLocal () + locale <- Opt.setDefaultUserLocale <$> view settings + mbAccount <- + lift + . runM + . userQueryToCassandra @m @'[Embed m] + . runInputConst locDomain + $ Data.lookupAccount locale u pure (userEmail <$> accountUser =<< mbAccount, userTeam <$> accountUser =<< mbAccount) -loginFailedWith :: (MonadClient m, MonadReader Env m) => LoginError -> UserId -> ExceptT LoginError m () -loginFailedWith e uid = decrRetryLimit uid >> throwE e +loginFailedWith :: + Member BudgetStore r => + LoginError -> + UserId -> + Maybe Opt.LimitFailedLogins -> + Sem (Error LoginError ': r) () +loginFailedWith e uid mLimitFailedLogins = + decrRetryLimit uid mLimitFailedLogins >> throw e -loginFailed :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () +loginFailed :: + Member BudgetStore r => + UserId -> + Maybe Opt.LimitFailedLogins -> + Sem (Error LoginError ': r) () loginFailed = loginFailedWith LoginFailed -decrRetryLimit :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () +decrRetryLimit :: + Member BudgetStore r => + UserId -> + Maybe Opt.LimitFailedLogins -> + Sem (Error LoginError ': r) () decrRetryLimit = withRetryLimit (\k b -> withBudget k b $ pure ()) -checkRetryLimit :: (MonadClient m, MonadReader Env m) => UserId -> ExceptT LoginError m () +checkRetryLimit :: + Member BudgetStore r => + UserId -> + Maybe Opt.LimitFailedLogins -> + Sem (Error LoginError ': r) () checkRetryLimit = withRetryLimit checkBudget withRetryLimit :: - (MonadClient m, MonadReader Env m) => - (BudgetKey -> Budget -> ExceptT LoginError m (Budgeted ())) -> + Member BudgetStore r => + (BudgetKey -> Budget -> Sem r (Budgeted ())) -> UserId -> - ExceptT LoginError m () -withRetryLimit action uid = do - mLimitFailedLogins <- view (settings . to Opt.setLimitFailedLogins) + Maybe Opt.LimitFailedLogins -> + Sem (Error LoginError ': r) () +withRetryLimit action uid mLimitFailedLogins = do forM_ mLimitFailedLogins $ \opts -> do let bkey = BudgetKey ("login#" <> idToText uid) budget = Budget (Opt.timeoutDiff $ Opt.timeout opts) (fromIntegral $ Opt.retryLimit opts) - bresult <- action bkey budget - case bresult of - BudgetExhausted ttl -> throwE . LoginBlocked . RetryAfter . floor $ ttl + raise @(Error LoginError) (action bkey budget) >>= \case + BudgetExhausted ttl -> throw . LoginBlocked . RetryAfter . floor $ ttl BudgetedValue () _ -> pure () logout :: @@ -284,6 +317,7 @@ renewAccess uts at = do pure $ Access at' ck' revokeAccess :: + forall m. (MonadClient m, Log.MonadLogger m, MonadReader Env m) => UserId -> PlainTextPassword -> @@ -292,7 +326,10 @@ revokeAccess :: ExceptT AuthError m () revokeAccess u pw cc ll = do lift $ Log.debug $ field "user" (toByteString u) . field "action" (Log.val "User.revokeAccess") - unlessM (Data.isSamlUser u) $ Data.authenticate u pw + locDomain <- qualifyLocal () + locale <- Opt.setDefaultUserLocale <$> view settings + unlessM (lift . runM . userQueryToCassandra @m @'[Embed m] $ runInputConst locDomain $ Data.isSamlUser locale u) $ + runUserQueryAction (Data.authenticate u pw) >>= except lift $ revokeCookies u cc ll -------------------------------------------------------------------------------- @@ -380,7 +417,7 @@ validateLoginId (LoginByPhone phone) = validateLoginId (LoginByHandle h) = pure (Right h) -isPendingActivation :: (MonadClient m, MonadReader Env m) => LoginId -> m Bool +isPendingActivation :: forall m. (MonadClient m, MonadReader Env m) => LoginId -> m Bool isPendingActivation ident = case ident of (LoginByHandle _) -> pure False (LoginByEmail e) -> checkKey (userEmailKey e) @@ -388,9 +425,16 @@ isPendingActivation ident = case ident of where checkKey k = do usr <- (>>= fst) <$> Data.lookupActivationCode k + locale <- Opt.setDefaultUserLocale <$> view settings + locDomain <- qualifyLocal () case usr of Nothing -> pure False - Just u -> maybe False (checkAccount k) <$> Data.lookupAccount u + Just u -> + maybe False (checkAccount k) + <$> ( runM . userQueryToCassandra @m @'[Embed m] + . runInputConst locDomain + $ Data.lookupAccount locale u + ) checkAccount k a = let i = userIdentity (accountUser a) statusAdmitsPending = case accountStatus a of @@ -451,6 +495,7 @@ validateToken ut at = do -- | Allow to login as any user without having the credentials. ssoLogin :: + forall m. ( MonadClient m, MonadReader Env m, ZAuth.MonadZAuth m, @@ -466,7 +511,10 @@ ssoLogin :: CookieType -> ExceptT LoginError m (Access ZAuth.User) ssoLogin (SsoLogin uid label) typ = do - Data.reauthenticate uid Nothing `catchE` \case + locale <- Opt.setDefaultUserLocale <$> view settings + locDomain <- qualifyLocal () + o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid Nothing + whenLeft o $ \case ReAuthMissingPassword -> pure () ReAuthCodeVerificationRequired -> pure () ReAuthCodeVerificationNoPendingCode -> pure () @@ -495,7 +543,10 @@ legalHoldLogin :: CookieType -> ExceptT LegalHoldLoginError m (Access ZAuth.LegalHoldUser) legalHoldLogin (LegalHoldLogin uid plainTextPassword label) typ = do - Data.reauthenticate uid plainTextPassword !>> LegalHoldReAuthError + locale <- Opt.setDefaultUserLocale <$> view settings + locDomain <- qualifyLocal () + o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid plainTextPassword + except o !>> LegalHoldReAuthError -- legalhold login is only possible if -- the user is a team user -- and the team has legalhold enabled @@ -522,3 +573,39 @@ assertLegalHoldEnabled tid = do case tfwoStatus stat of TeamFeatureDisabled -> throwE LegalHoldLoginLegalHoldNotEnabled TeamFeatureEnabled -> pure () + +-------------------------------------------------------------------------------- +-- Polysemy crutches +-- +-- These can be removed once functions in this module run in 'Sem r' instead of +-- 'ExceptT e m' or 'm' for some constrained 'm'. + +runBudgetStoreAction :: + forall m e a. + MonadClient m => + Sem '[Error e, BudgetStore, Embed m] a -> + ExceptT e m a +runBudgetStoreAction = runStoreActionExceptT (budgetStoreToCassandra @m) + +runUserQueryAction :: + forall m e a t. + (MonadClient m, MonadTrans t) => + Sem '[Error e, UserQuery, Embed m] a -> + t m (Either e a) +runUserQueryAction = runStoreAction (userQueryToCassandra @m) + +runStoreAction :: + forall m e a t store. + (MonadClient m, MonadTrans t) => + (forall n b. (MonadClient n, n ~ m) => Sem '[store, Embed n] b -> Sem '[Embed n] b) -> + Sem '[Error e, store, Embed m] a -> + t m (Either e a) +runStoreAction interpreter = lift . runM . interpreter @m . runError @e + +runStoreActionExceptT :: + forall m e a store. + MonadClient m => + (forall n b. (MonadClient n, n ~ m) => Sem '[store, Embed n] b -> Sem '[Embed n] b) -> + Sem '[Error e, store, Embed m] a -> + ExceptT e m a +runStoreActionExceptT interpreter = runStoreAction interpreter >=> except From e0547e2c8bc5f601bbc9b96d32219965e872c728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 3 Jun 2022 13:09:43 +0200 Subject: [PATCH 10/41] Removing MonadReader in the UserKey module --- services/brig/src/Brig/API/User.hs | 20 ++++++++----- services/brig/src/Brig/Data/Activation.hs | 13 +++++---- services/brig/src/Brig/Data/UserKey.hs | 34 +++++++++++------------ 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index c4a42b51af..79a963ccd6 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -368,7 +368,8 @@ createUser new = do ExceptT RegisterError (AppT r) () acceptTeamInvitation account inv ii uk ident = do let uid = userId (accountUser account) - ok <- lift . wrapClient $ Data.claimKey uk uid + d <- view digestSHA256 + ok <- lift . wrapClient $ Data.claimKey d uk uid unless ok $ throwE RegisterErrorUserKeyExists let minvmeta :: (Maybe (UserId, UTCTimeMillis), Team.Role) @@ -707,9 +708,10 @@ removeEmail :: ExceptT RemoveIdentityError (AppT r) () removeEmail uid conn = do ident <- lift $ fetchUserIdentity uid + d <- view digestSHA256 case ident of Just (FullIdentity e _) -> lift $ do - wrapClient . deleteKey $ userEmailKey e + wrapClient . deleteKey d $ userEmailKey e wrapClient $ Data.deleteEmail uid wrapHttpClient $ Intra.onUserEvent uid (Just conn) (emailRemoved uid e) Just _ -> throwE LastIdentity @@ -725,13 +727,14 @@ removePhone :: ExceptT RemoveIdentityError (AppT r) () removePhone uid conn = do ident <- lift $ fetchUserIdentity uid + d <- view digestSHA256 case ident of Just (FullIdentity _ p) -> do pw <- lift . wrapClient $ Data.lookupPassword uid unless (isJust pw) $ throwE NoPassword lift $ do - wrapClient . deleteKey $ userPhoneKey p + wrapClient . deleteKey d $ userPhoneKey p wrapClient $ Data.deletePhone uid wrapHttpClient $ Intra.onUserEvent uid (Just conn) (phoneRemoved uid p) Just _ -> throwE LastIdentity @@ -762,7 +765,8 @@ revokeIdentity key = do where revokeKey :: UserId -> UserKey -> AppT r () revokeKey u uk = do - wrapClient $ deleteKey uk + d <- view digestSHA256 + wrapClient $ deleteKey d uk wrapClient $ foldKey (\(_ :: Email) -> Data.deleteEmail u) @@ -860,7 +864,8 @@ activateWithCurrency tgt code usr cur = do field "activation.key" (toByteString key) . field "activation.code" (toByteString code) . msg (val "Activating") - event <- wrapClientE $ Data.activateKey key code usr + d <- view digestSHA256 + event <- wrapClientE $ Data.activateKey d key code usr case event of Nothing -> pure ActivationPass Just e -> do @@ -1215,8 +1220,9 @@ deleteAccount account@(accountUser -> user) = do let uid = userId user Log.info $ field "user" (toByteString uid) . msg (val "Deleting account") -- Free unique keys - for_ (userEmail user) $ deleteKey . userEmailKey - for_ (userPhone user) $ deleteKey . userPhoneKey + d <- view digestSHA256 + for_ (userEmail user) $ deleteKey d . userEmailKey + for_ (userPhone user) $ deleteKey d . userPhoneKey for_ (userHandle user) $ freeHandle (userId user) -- Wipe data Data.clearProperties uid diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 01b44ae55d..fbc096a4c2 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -52,7 +52,7 @@ import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as LT import Imports import OpenSSL.BN (randIntegerZeroToNMinusOne) -import OpenSSL.EVP.Digest (digestBS, getDigestByName) +import OpenSSL.EVP.Digest (Digest, digestBS, getDigestByName) import Polysemy import Polysemy.Input import Text.Printf (printf) @@ -94,12 +94,15 @@ maxAttempts = 3 -- docs/reference/user/activation.md {#RefActivationSubmit} activateKey :: forall m. - (MonadClient m, MonadReader Env m) => + ( MonadClient m, + MonadReader Env m + ) => + Digest -> ActivationKey -> ActivationCode -> Maybe UserId -> ExceptT ActivationError m (Maybe ActivationEvent) -activateKey k c u = verifyCode k c >>= pickUser >>= activate +activateKey d k c u = verifyCode k c >>= pickUser >>= activate where pickUser (uk, u') = maybe (throwE invalidUser) (pure . (uk,)) (u <|> u') activate (key, uid) = do @@ -143,7 +146,7 @@ activateKey k c u = verifyCode k c >>= pickUser >>= activate lift (deleteCode uid) claim key uid lift $ foldKey (updateEmailAndDeleteEmailUnvalidated uid) (updatePhone uid) key - for_ oldKey $ lift . deleteKey + for_ oldKey $ lift . deleteKey d pure . Just $ foldKey (EmailActivated uid) (PhoneActivated uid) key where updateEmailAndDeleteEmailUnvalidated :: MonadClient m => UserId -> Email -> m () @@ -161,7 +164,7 @@ activateKey k c u = verifyCode k c >>= pickUser >>= activate ) ) claim key uid = do - ok <- lift $ claimKey key uid + ok <- lift $ claimKey d key uid unless ok $ throwE . UserKeyExists . LT.fromStrict $ foldKey fromEmail fromPhone key diff --git a/services/brig/src/Brig/Data/UserKey.hs b/services/brig/src/Brig/Data/UserKey.hs index 027fbac7d1..c3119277d0 100644 --- a/services/brig/src/Brig/Data/UserKey.hs +++ b/services/brig/src/Brig/Data/UserKey.hs @@ -33,21 +33,19 @@ module Brig.Data.UserKey ) where -import Brig.App (Env, digestSHA256) import Brig.Data.Instances () import qualified Brig.Data.User as User import Brig.Email import Brig.Phone import Brig.Types import Cassandra -import Control.Lens (view) import qualified Data.ByteString as B import Data.ByteString.Lazy (toStrict) import Data.Id import qualified Data.Multihash.Digest as MH import qualified Data.Text.Encoding as T import Imports -import OpenSSL.EVP.Digest (digestBS) +import OpenSSL.EVP.Digest (Digest, digestBS) -- | A natural identifier (i.e. unique key) of a user. data UserKey @@ -118,15 +116,17 @@ keyTextOriginal (UserPhoneKey k) = fromPhone (phoneKeyOrig k) -- | Claim a 'UserKey' for a user. claimKey :: - (MonadClient m, MonadReader Env m) => + MonadClient m => + -- | The SHA256 digest + Digest -> -- | The key to claim. UserKey -> -- | The user claiming the key. UserId -> m Bool -claimKey k u = do +claimKey d k u = do free <- keyAvailable k (Just u) - when free (insertKey u k) + when free (insertKey d u k) pure free -- | Check whether a 'UserKey' is available. @@ -151,25 +151,23 @@ lookupKey k = fmap runIdentity <$> retry x1 (query1 keySelect (params LocalQuorum (Identity $ keyText k))) -insertKey :: (MonadClient m, MonadReader Env m) => UserId -> UserKey -> m () -insertKey u k = do - hk <- hashKey k - let kt = foldKey (\(_ :: Email) -> UKHashEmail) (\(_ :: Phone) -> UKHashPhone) k +insertKey :: MonadClient m => Digest -> UserId -> UserKey -> m () +insertKey d u k = do + let hk = hashKey d k + kt = foldKey (\(_ :: Email) -> UKHashEmail) (\(_ :: Phone) -> UKHashPhone) k retry x5 $ write insertHashed (params LocalQuorum (hk, kt, u)) retry x5 $ write keyInsert (params LocalQuorum (keyText k, u)) -deleteKey :: (MonadClient m, MonadReader Env m) => UserKey -> m () -deleteKey k = do - hk <- hashKey k +deleteKey :: MonadClient m => Digest -> UserKey -> m () +deleteKey d k = do + let hk = hashKey d k retry x5 $ write deleteHashed (params LocalQuorum (Identity hk)) retry x5 $ write keyDelete (params LocalQuorum (Identity $ keyText k)) -hashKey :: MonadReader Env m => UserKey -> m UserKeyHash -hashKey uk = do - d <- view digestSHA256 +hashKey :: Digest -> UserKey -> UserKeyHash +hashKey d uk = let d' = digestBS d $ T.encodeUtf8 (keyText uk) - pure . UserKeyHash $ - MH.MultihashDigest MH.SHA256 (B.length d') d' + in UserKeyHash $ MH.MultihashDigest MH.SHA256 (B.length d') d' lookupPhoneHashes :: MonadClient m => [ByteString] -> m [(ByteString, UserId)] lookupPhoneHashes hp = From c7c7f57ae8e96f786dc510501bdecf2345cc6314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 7 Jun 2022 11:18:43 +0200 Subject: [PATCH 11/41] WIP: Almost compiles with getUserAuthentication or something like it --- services/brig/brig.cabal | 11 + services/brig/package.yaml | 1 + services/brig/src/Brig/API/User.hs | 287 +++++++++++------- services/brig/src/Brig/Data/Activation.hs | 106 +++---- services/brig/src/Brig/Data/User.hs | 42 +-- services/brig/src/Brig/Data/UserKey.hs | 101 +----- .../brig/src/Brig/Sem/ActivationKeyStore.hs | 41 +++ .../Brig/Sem/ActivationKeyStore/Cassandra.hs | 53 ++++ .../brig/src/Brig/Sem/ActivationSupply.hs | 30 ++ .../brig/src/Brig/Sem/ActivationSupply/IO.hs | 53 ++++ services/brig/src/Brig/Sem/GalleyAccess.hs | 29 ++ .../brig/src/Brig/Sem/GalleyAccess/Http.hs | 40 +++ services/brig/src/Brig/Sem/Twilio.hs | 37 +++ services/brig/src/Brig/Sem/Twilio/IO.hs | 40 +++ services/brig/src/Brig/Sem/UserKeyStore.hs | 81 +++++ .../src/Brig/Sem/UserKeyStore/Cassandra.hs | 80 +++++ services/brig/src/Brig/Sem/UserQuery.hs | 13 + .../brig/src/Brig/Sem/UserQuery/Cassandra.hs | 38 +++ 18 files changed, 782 insertions(+), 301 deletions(-) create mode 100644 services/brig/src/Brig/Sem/ActivationKeyStore.hs create mode 100644 services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs create mode 100644 services/brig/src/Brig/Sem/ActivationSupply.hs create mode 100644 services/brig/src/Brig/Sem/ActivationSupply/IO.hs create mode 100644 services/brig/src/Brig/Sem/GalleyAccess.hs create mode 100644 services/brig/src/Brig/Sem/GalleyAccess/Http.hs create mode 100644 services/brig/src/Brig/Sem/Twilio.hs create mode 100644 services/brig/src/Brig/Sem/Twilio/IO.hs create mode 100644 services/brig/src/Brig/Sem/UserKeyStore.hs create mode 100644 services/brig/src/Brig/Sem/UserKeyStore/Cassandra.hs diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index f835153e3b..e2ff8800db 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -84,14 +84,24 @@ library Brig.Queue.Types Brig.RPC Brig.Run + Brig.Sem.ActivationKeyStore + Brig.Sem.ActivationKeyStore.Cassandra + Brig.Sem.ActivationSupply + Brig.Sem.ActivationSupply.IO Brig.Sem.BudgetStore Brig.Sem.BudgetStore.Cassandra Brig.Sem.CodeStore Brig.Sem.CodeStore.Cassandra + Brig.Sem.GalleyAccess + Brig.Sem.GalleyAccess.Http Brig.Sem.PasswordResetStore Brig.Sem.PasswordResetStore.CodeStore Brig.Sem.PasswordResetSupply Brig.Sem.PasswordResetSupply.IO + Brig.Sem.Twilio + Brig.Sem.Twilio.IO + Brig.Sem.UserKeyStore + Brig.Sem.UserKeyStore.Cassandra Brig.Sem.UserQuery Brig.Sem.UserQuery.Cassandra Brig.SMTP @@ -224,6 +234,7 @@ library , imports , insert-ordered-containers , iproute >=1.5 + , iso3166-country-codes , iso639 >=0.1 , lens >=3.8 , lens-aeson >=1.0 diff --git a/services/brig/package.yaml b/services/brig/package.yaml index 7e7e48d29f..861d201aab 100644 --- a/services/brig/package.yaml +++ b/services/brig/package.yaml @@ -68,6 +68,7 @@ library: - imports - insert-ordered-containers - iproute >=1.5 + - iso3166-country-codes - iso639 >=0.1 - lens >=3.8 - lens-aeson >=1.0 diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 79a963ccd6..98a3e29e5a 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -114,10 +114,13 @@ import qualified Brig.InternalEvent.Types as Internal import Brig.Options hiding (Timeout, internalEvents) import Brig.Password import qualified Brig.Queue as Queue +import Brig.Sem.ActivationKeyStore (ActivationKeyStore) +import Brig.Sem.ActivationSupply (ActivationSupply) import Brig.Sem.PasswordResetStore (PasswordResetStore) import qualified Brig.Sem.PasswordResetStore as E import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import qualified Brig.Sem.PasswordResetSupply as E +import Brig.Sem.UserKeyStore (UserKeyStore) import Brig.Sem.UserQuery (UserQuery) import Brig.Sem.UserQuery.Cassandra import qualified Brig.Team.DB as Team @@ -195,9 +198,12 @@ identityErrorToBrigError = \case IdentityErrorBlacklistedPhone -> Error.StdError $ errorToWai @'E.BlacklistedPhone IdentityErrorUserKeyExists -> Error.StdError $ errorToWai @'E.UserKeyExists -verifyUniquenessAndCheckBlacklist :: UserKey -> ExceptT IdentityError (AppT r) () +verifyUniquenessAndCheckBlacklist :: + Members '[UserKeyStore, UserQuery] r => + UserKey -> + ExceptT IdentityError (AppT r) () verifyUniquenessAndCheckBlacklist uk = do - wrapClientE $ checkKey Nothing uk + liftSemE $ checkKey Nothing uk blacklisted <- lift $ wrapClient $ Blacklist.exists uk when blacklisted $ throwE (foldKey (const IdentityErrorBlacklistedEmail) (const IdentityErrorBlacklistedPhone) uk) @@ -209,7 +215,17 @@ verifyUniquenessAndCheckBlacklist uk = do -- docs/reference/user/registration.md {#RefRegistration} createUser :: - Members '[Input (Local ()), UserQuery] r => + forall r. + Members + '[ ActivationKeyStore, + ActivationSupply, + Input (Local ()), + PasswordResetSupply, + PasswordResetStore, + UserKeyStore, + UserQuery + ] + r => NewUser -> ExceptT RegisterError (AppT r) CreateUserResult createUser new = do @@ -369,7 +385,7 @@ createUser new = do acceptTeamInvitation account inv ii uk ident = do let uid = userId (accountUser account) d <- view digestSHA256 - ok <- lift . wrapClient $ Data.claimKey d uk uid + ok <- lift . liftSem $ Data.claimKey d uk uid unless ok $ throwE RegisterErrorUserKeyExists let minvmeta :: (Maybe (UserId, UTCTimeMillis), Team.Role) @@ -378,7 +394,7 @@ createUser new = do unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do - wrapClient $ activateUser uid ident -- ('insertAccount' sets column activated to False; here it is set to True.) + liftSem $ activateUser uid ident -- ('insertAccount' sets column activated to False; here it is set to True.) void $ onActivated (AccountActivated account) Log.info $ field "user" (toByteString uid) @@ -395,7 +411,7 @@ createUser new = do unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do - wrapClient $ activateUser uid ident + liftSem $ activateUser uid ident void $ onActivated (AccountActivated account) Log.info $ field "user" (toByteString uid) @@ -410,7 +426,7 @@ createUser new = do fmap join . for (userEmailKey <$> email) $ \ek -> case newUserEmailCode new of Nothing -> do timeout <- setActivationTimeout <$> view settings - edata <- lift . wrapClient $ Data.newActivation ek timeout (Just uid) + edata <- lift . liftSem $ Data.newActivation ek timeout (Just uid) lift . Log.info $ field "user" (toByteString uid) . field "activation.key" (toByteString $ activationKey edata) @@ -429,7 +445,7 @@ createUser new = do fmap join . for (userPhoneKey <$> phone) $ \pk -> case newUserPhoneCode new of Nothing -> do timeout <- setActivationTimeout <$> view settings - pdata <- lift . wrapClient $ Data.newActivation pk timeout (Just uid) + pdata <- lift . liftSem $ Data.newActivation pk timeout (Just uid) lift . Log.info $ field "user" (toByteString uid) . field "activation.key" (toByteString $ activationKey pdata) @@ -448,7 +464,11 @@ initAccountFeatureConfig uid = do -- | 'createUser' is becoming hard to maintian, and instead of adding more case distinctions -- all over the place there, we add a new function that handles just the one new flow where -- users are invited to the team via scim. -createUserInviteViaScim :: UserId -> NewUserScimInvitation -> ExceptT Error.Error (AppT r) UserAccount +createUserInviteViaScim :: + Members '[UserKeyStore, UserQuery] r => + UserId -> + NewUserScimInvitation -> + ExceptT Error.Error (AppT r) UserAccount createUserInviteViaScim uid (NewUserScimInvitation tid loc name rawEmail) = do email <- either (const . throwE . Error.StdError $ errorToWai @'E.InvalidEmail) pure (validateEmail rawEmail) let emKey = userEmailKey email @@ -611,7 +631,13 @@ checkHandles check num = reverse <$> collectFree [] check num -- | Call 'changeEmail' and process result: if email changes to itself, succeed, if not, send -- validation email. changeSelfEmail :: - Member UserQuery r => + Members + '[ ActivationKeyStore, + ActivationSupply, + UserKeyStore, + UserQuery + ] + r => UserId -> Email -> AllowSCIMUpdates -> @@ -636,7 +662,13 @@ changeSelfEmail u email allowScim = do -- | Prepare changing the email (checking a number of invariants). changeEmail :: - Member UserQuery r => + Members + '[ ActivationKeyStore, + ActivationSupply, + UserKeyStore, + UserQuery + ] + r => UserId -> Email -> AllowSCIMUpdates -> @@ -651,7 +683,7 @@ changeEmail u email allowScim = do blacklisted <- lift . wrapClient $ Blacklist.exists ek when blacklisted $ throwE (ChangeBlacklistedEmail email) - available <- lift . wrapClient $ Data.keyAvailable ek (Just u) + available <- lift . liftSem $ Data.keyAvailable ek (Just u) unless available $ throwE $ EmailExists email @@ -670,13 +702,23 @@ changeEmail u email allowScim = do ) $ throwE EmailManagedByScim timeout <- setActivationTimeout <$> view settings - act <- lift . wrapClient $ Data.newActivation ek timeout (Just u) + act <- lift . liftSem $ Data.newActivation ek timeout (Just u) pure $ ChangeEmailNeedsActivation (usr, act, em) ------------------------------------------------------------------------------- -- Change Phone -changePhone :: UserId -> Phone -> ExceptT ChangePhoneError (AppT r) (Activation, Phone) +changePhone :: + Members + '[ ActivationKeyStore, + ActivationSupply, + UserKeyStore, + UserQuery + ] + r => + UserId -> + Phone -> + ExceptT ChangePhoneError (AppT r) (Activation, Phone) changePhone u phone = do canonical <- maybe @@ -684,7 +726,7 @@ changePhone u phone = do pure =<< lift (wrapClient $ validatePhone phone) let pk = userPhoneKey canonical - available <- lift . wrapClient $ Data.keyAvailable pk (Just u) + available <- lift . liftSem $ Data.keyAvailable pk (Just u) unless available $ throwE PhoneExists timeout <- setActivationTimeout <$> view settings @@ -695,14 +737,14 @@ changePhone u phone = do prefixExcluded <- lift . wrapClient $ Blacklist.existsAnyPrefix canonical when prefixExcluded $ throwE BlacklistedNewPhone - act <- lift . wrapClient $ Data.newActivation pk timeout (Just u) + act <- lift . liftSem $ Data.newActivation pk timeout (Just u) pure (act, canonical) ------------------------------------------------------------------------------- -- Remove Email removeEmail :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserKeyStore, UserQuery] r => UserId -> ConnId -> ExceptT RemoveIdentityError (AppT r) () @@ -711,7 +753,7 @@ removeEmail uid conn = do d <- view digestSHA256 case ident of Just (FullIdentity e _) -> lift $ do - wrapClient . deleteKey d $ userEmailKey e + liftSem . deleteKey d $ userEmailKey e wrapClient $ Data.deleteEmail uid wrapHttpClient $ Intra.onUserEvent uid (Just conn) (emailRemoved uid e) Just _ -> throwE LastIdentity @@ -721,7 +763,7 @@ removeEmail uid conn = do -- Remove Phone removePhone :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserKeyStore, UserQuery] r => UserId -> ConnId -> ExceptT RemoveIdentityError (AppT r) () @@ -734,7 +776,7 @@ removePhone uid conn = do unless (isJust pw) $ throwE NoPassword lift $ do - wrapClient . deleteKey d $ userPhoneKey p + liftSem . deleteKey d $ userPhoneKey p wrapClient $ Data.deletePhone uid wrapHttpClient $ Intra.onUserEvent uid (Just conn) (phoneRemoved uid p) Just _ -> throwE LastIdentity @@ -744,12 +786,13 @@ removePhone uid conn = do -- Forcefully revoke a verified identity revokeIdentity :: - Members '[Input (Local ()), UserQuery] r => + forall r. + Members '[Input (Local ()), UserKeyStore, UserQuery] r => Either Email Phone -> AppT r () revokeIdentity key = do let uk = either userEmailKey userPhoneKey key - mu <- wrapClient $ Data.lookupKey uk + mu <- liftSem $ Data.getKey uk case mu of Nothing -> pure () Just u -> @@ -766,7 +809,7 @@ revokeIdentity key = do revokeKey :: UserId -> UserKey -> AppT r () revokeKey u uk = do d <- view digestSHA256 - wrapClient $ deleteKey d uk + liftSem $ deleteKey d uk wrapClient $ foldKey (\(_ :: Email) -> Data.deleteEmail u) @@ -842,6 +885,16 @@ mkUserEvent usrs status = -- Activation activate :: + Members + '[ ActivationKeyStore, + ActivationSupply, + Input (Local ()), + PasswordResetSupply, + PasswordResetStore, + UserKeyStore, + UserQuery + ] + r => ActivationTarget -> ActivationCode -> -- | The user for whom to activate the key. @@ -850,6 +903,16 @@ activate :: activate tgt code usr = activateWithCurrency tgt code usr Nothing activateWithCurrency :: + Members + '[ ActivationKeyStore, + ActivationSupply, + Input (Local ()), + PasswordResetSupply, + PasswordResetStore, + UserKeyStore, + UserQuery + ] + r => ActivationTarget -> ActivationCode -> -- | The user for whom to activate the key. @@ -859,13 +922,14 @@ activateWithCurrency :: Maybe Currency.Alpha -> ExceptT ActivationError (AppT r) ActivationResult activateWithCurrency tgt code usr cur = do - key <- wrapClientE $ mkActivationKey tgt + key <- liftSemE $ mkActivationKey tgt lift . Log.info $ field "activation.key" (toByteString key) . field "activation.code" (toByteString code) . msg (val "Activating") d <- view digestSHA256 - event <- wrapClientE $ Data.activateKey d key code usr + locale <- setDefaultUserLocale <$> view settings + event <- liftSemE $ Data.activateKey locale d key code usr case event of Nothing -> pure ActivationPass Just e -> do @@ -880,17 +944,18 @@ activateWithCurrency tgt code usr cur = do for_ tid $ \t -> wrapHttp $ Intra.changeTeamStatus t Team.Active cur preverify :: - ( MonadClient m, - MonadReader Env m - ) => + Members '[ActivationKeyStore, ActivationSupply] r => ActivationTarget -> ActivationCode -> - ExceptT ActivationError m () + ExceptT ActivationError (Sem r) () preverify tgt code = do key <- mkActivationKey tgt void $ Data.verifyCode key code -onActivated :: ActivationEvent -> (AppT r) (UserId, Maybe UserIdentity, Bool) +onActivated :: + Members '[UserQuery] r => + ActivationEvent -> + AppT r (UserId, Maybe UserIdentity, Bool) onActivated (AccountActivated account) = do let uid = userId (accountUser account) Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.onActivated") @@ -899,7 +964,7 @@ onActivated (AccountActivated account) = do pure (uid, userIdentity (accountUser account), True) onActivated (EmailActivated uid email) = do wrapHttpClient $ Intra.onUserEvent uid Nothing (emailUpdated uid email) - wrapHttpClient $ Data.deleteEmailUnvalidated uid + liftSem $ Data.deleteEmailUnvalidated uid pure (uid, Just (EmailIdentity email), False) onActivated (PhoneActivated uid phone) = do wrapHttpClient $ Intra.onUserEvent uid Nothing (phoneUpdated uid phone) @@ -907,79 +972,80 @@ onActivated (PhoneActivated uid phone) = do -- docs/reference/user/activation.md {#RefActivationRequest} sendActivationCode :: - Member UserQuery r => + Members '[ActivationKeyStore, ActivationSupply, UserKeyStore, UserQuery] r => Either Email Phone -> Maybe Locale -> Bool -> ExceptT SendActivationCodeError (AppT r) () -sendActivationCode emailOrPhone loc call = case emailOrPhone of - Left email -> do - ek <- - either - (const . throwE . InvalidRecipient $ userEmailKey email) - (pure . userEmailKey) - (validateEmail email) - exists <- lift $ isJust <$> wrapClient (Data.lookupKey ek) - when exists $ - throwE $ - UserKeyInUse ek - blacklisted <- lift . wrapClient $ Blacklist.exists ek - when blacklisted $ - throwE (ActivationBlacklistedUserKey ek) - uc <- lift . wrapClient $ Data.lookupActivationCode ek - case uc of - Nothing -> sendVerificationEmail ek Nothing -- Fresh code request, no user - Just (Nothing, c) -> sendVerificationEmail ek (Just c) -- Re-requesting existing code - Just (Just uid, c) -> sendActivationEmail ek c uid -- User re-requesting activation - Right phone -> do - -- validatePhone returns the canonical E.164 phone number format - canonical <- - maybe - (throwE $ InvalidRecipient (userPhoneKey phone)) - pure - =<< lift (wrapClient $ validatePhone phone) - let pk = userPhoneKey canonical - exists <- lift $ isJust <$> wrapClient (Data.lookupKey pk) - when exists $ - throwE $ - UserKeyInUse pk - blacklisted <- lift . wrapClient $ Blacklist.exists pk - when blacklisted $ - throwE (ActivationBlacklistedUserKey pk) - -- check if any prefixes of this phone number are blocked - prefixExcluded <- lift . wrapClient $ Blacklist.existsAnyPrefix canonical - when prefixExcluded $ - throwE (ActivationBlacklistedUserKey pk) - c <- lift . wrapClient $ fmap snd <$> Data.lookupActivationCode pk - p <- wrapClientE $ mkPair pk c Nothing - void . forPhoneKey pk $ \ph -> - lift $ - if call - then wrapClient $ sendActivationCall ph p loc - else wrapClient $ sendActivationSms ph p loc +sendActivationCode emailOrPhone loc call = do + timeout <- setActivationTimeout <$> view settings + case emailOrPhone of + Left email -> do + ek <- + either + (const . throwE . InvalidRecipient $ userEmailKey email) + (pure . userEmailKey) + (validateEmail email) + exists <- lift $ isJust <$> liftSem (Data.getKey ek) + when exists $ + throwE $ + UserKeyInUse ek + blacklisted <- lift . wrapClient $ Blacklist.exists ek + when blacklisted $ + throwE (ActivationBlacklistedUserKey ek) + uc <- lift . wrapClient $ Data.lookupActivationCode ek + case uc of + Nothing -> sendVerificationEmail timeout ek Nothing -- Fresh code request, no user + Just (Nothing, c) -> sendVerificationEmail timeout ek (Just c) -- Re-requesting existing code + Just (Just uid, c) -> sendActivationEmail timeout ek c uid -- User re-requesting activation + Right phone -> do + -- validatePhone returns the canonical E.164 phone number format + canonical <- + maybe + (throwE $ InvalidRecipient (userPhoneKey phone)) + pure + =<< lift (wrapClient $ validatePhone phone) + let pk = userPhoneKey canonical + exists <- lift $ isJust <$> liftSem (Data.getKey pk) + when exists $ + throwE $ + UserKeyInUse pk + blacklisted <- lift . wrapClient $ Blacklist.exists pk + when blacklisted $ + throwE (ActivationBlacklistedUserKey pk) + -- check if any prefixes of this phone number are blocked + prefixExcluded <- lift . wrapClient $ Blacklist.existsAnyPrefix canonical + when prefixExcluded $ + throwE (ActivationBlacklistedUserKey pk) + c <- lift . wrapClient $ fmap snd <$> Data.lookupActivationCode pk + p <- lift . liftSem $ mkPair timeout pk c Nothing + void . forPhoneKey pk $ \ph -> + lift $ + if call + then wrapClient $ sendActivationCall ph p loc + else wrapClient $ sendActivationSms ph p loc where notFound = throwM . UserDisplayNameNotFound - mkPair k c u = do - timeout <- setActivationTimeout <$> view settings + mkPair timeout k c u = do case c of - Just c' -> liftIO $ (,c') <$> Data.mkActivationKey k - Nothing -> lift $ do + Just c' -> (,c') <$> Data.makeActivationKey k + Nothing -> do dat <- Data.newActivation k timeout u pure (activationKey dat, activationCode dat) - sendVerificationEmail ek uc = do - p <- wrapClientE $ mkPair ek uc Nothing + sendVerificationEmail timeout ek uc = do + p <- lift . liftSem $ mkPair timeout ek uc Nothing void . forEmailKey ek $ \em -> lift $ sendVerificationMail em p loc - sendActivationEmail ek uc uid = do + sendActivationEmail timeout ek uc uid = do -- FUTUREWORK(fisx): we allow for 'PendingInvitations' here, but I'm not sure this -- top-level function isn't another piece of a deprecated onboarding flow? - locu <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain + locu <- qualifyLocal () locale <- setDefaultUserLocale <$> view settings u <- maybe (notFound uid) pure =<< lift (liftSem $ Data.lookupUser locu locale WithPendingInvitations uid) - p <- wrapClientE $ mkPair ek (Just uc) (Just uid) + p <- lift . liftSem $ mkPair timeout ek (Just uc) (Just uid) let ident = userIdentity u name = userDisplayName u loc' = loc <|> Just (userLocale u) @@ -997,7 +1063,10 @@ sendActivationCode emailOrPhone loc call = case emailOrPhone of _otherwise -> sendActivationMail em name p loc' ident -mkActivationKey :: (MonadClient m, MonadReader Env m) => ActivationTarget -> ExceptT ActivationError m ActivationKey +mkActivationKey :: + Member ActivationSupply r => + ActivationTarget -> + ExceptT ActivationError (Sem r) ActivationKey mkActivationKey (ActivateKey k) = pure k mkActivationKey (ActivateEmail e) = do ek <- @@ -1005,21 +1074,26 @@ mkActivationKey (ActivateEmail e) = do (throwE . InvalidActivationEmail e) (pure . userEmailKey) (validateEmail e) - liftIO $ Data.mkActivationKey ek + lift $ Data.makeActivationKey ek mkActivationKey (ActivatePhone p) = do pk <- maybe (throwE $ InvalidActivationPhone p) (pure . userPhoneKey) - =<< lift (validatePhone p) - liftIO $ Data.mkActivationKey pk + =<< undefined + -- =<< lift (validatePhone p) + lift $ Data.makeActivationKey pk ------------------------------------------------------------------------------- -- Password Management -changePassword :: UserId -> PasswordChange -> ExceptT ChangePasswordError (AppT r) () +changePassword :: + Members '[UserQuery] r => + UserId -> + PasswordChange -> + ExceptT ChangePasswordError (AppT r) () changePassword uid cp = do - activated <- lift . wrapClient $ Data.isActivated uid + activated <- lift . liftSem $ Data.isActivated uid unless activated $ throwE ChangePasswordNoIdentity currpw <- lift . wrapClient $ Data.lookupPassword uid @@ -1035,12 +1109,12 @@ changePassword uid cp = do lift $ wrapClient (Data.updatePassword uid newpw) >> wrapClient (revokeAllCookies uid) beginPasswordReset :: - Members '[P.TinyLog, PasswordResetStore] r => + Members '[P.TinyLog, PasswordResetStore, UserKeyStore] r => Either Email Phone -> ExceptT PasswordResetError (AppT r) (UserId, PasswordResetPair) beginPasswordReset target = do let key = either userEmailKey userPhoneKey target - user <- lift (wrapClient $ Data.lookupKey key) >>= maybe (throwE InvalidPasswordResetKey) pure + user <- lift (liftSem $ Data.getKey key) >>= maybe (throwE InvalidPasswordResetKey) pure lift . liftSem . P.debug $ field "user" (toByteString user) . field "action" (Log.val "User.beginPasswordReset") status <- lift . wrapClient $ Data.lookupStatus user @@ -1052,7 +1126,7 @@ beginPasswordReset target = do (user,) <$> lift (liftSem $ E.createPasswordResetCode user target) completePasswordReset :: - Members '[PasswordResetStore, PasswordResetSupply] r => + Members '[PasswordResetStore, PasswordResetSupply, UserKeyStore] r => PasswordResetIdentity -> PasswordResetCode -> PlainTextPassword -> @@ -1080,19 +1154,19 @@ checkNewIsDifferent uid pw = do _ -> pure () mkPasswordResetKey :: - Members '[PasswordResetSupply] r => + Members '[PasswordResetSupply, UserKeyStore] r => PasswordResetIdentity -> ExceptT PasswordResetError (AppT r) PasswordResetKey mkPasswordResetKey ident = case ident of PasswordResetIdentityKey k -> pure k PasswordResetEmailIdentity e -> - wrapClientE (user (userEmailKey e)) + user (userEmailKey e) >>= lift . liftSem . E.mkPasswordResetKey PasswordResetPhoneIdentity p -> - wrapClientE (user (userPhoneKey p)) + user (userPhoneKey p) >>= lift . liftSem . E.mkPasswordResetKey where - user uk = lift (Data.lookupKey uk) >>= maybe (throwE InvalidPasswordResetKey) pure + user uk = lift (liftSem $ Data.getKey uk) >>= maybe (throwE InvalidPasswordResetKey) pure ------------------------------------------------------------------------------- -- User Deletion @@ -1268,12 +1342,17 @@ lookupActivationCode emailOrPhone = do pure $ (k,) <$> c lookupPasswordResetCode :: - Members '[PasswordResetStore, PasswordResetSupply] r => + Members + '[ PasswordResetStore, + PasswordResetSupply, + UserKeyStore + ] + r => Either Email Phone -> - (AppT r) (Maybe PasswordResetPair) + AppT r (Maybe PasswordResetPair) lookupPasswordResetCode emailOrPhone = do let uk = either userEmailKey userPhoneKey emailOrPhone - usr <- wrapClient $ Data.lookupKey uk + usr <- liftSem $ Data.getKey uk liftSem $ case usr of Nothing -> pure Nothing Just u -> do @@ -1513,13 +1592,13 @@ getEmailForProfile _ EmailVisibleToSelf' = Nothing -- | Find user accounts for a given identity, both activated and those -- currently pending activation. lookupAccountsByIdentity :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserKeyStore, UserQuery] r => Either Email Phone -> Bool -> AppT r [UserAccount] lookupAccountsByIdentity emailOrPhone includePendingInvitations = do let uk = either userEmailKey userPhoneKey emailOrPhone - activeUid <- wrapClient $ Data.lookupKey uk + activeUid <- liftSem $ Data.getKey uk uidFromKey <- (>>= fst) <$> wrapClient (Data.lookupActivationCode uk) locale <- setDefaultUserLocale <$> view settings result <- liftSem $ Data.lookupAccounts locale (nub $ catMaybes [activeUid, uidFromKey]) diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index fbc096a4c2..d26e8ba5f1 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -31,31 +31,29 @@ module Brig.Data.Activation ) where -import Brig.App (Env, qualifyLocal, settings) import Brig.Data.User import Brig.Data.UserKey import Brig.Options -import qualified Brig.Sem.CodeStore as E -import Brig.Sem.CodeStore.Cassandra +import Brig.Sem.ActivationKeyStore +import Brig.Sem.ActivationSupply +import Brig.Sem.PasswordResetStore +import qualified Brig.Sem.PasswordResetStore as E import qualified Brig.Sem.PasswordResetSupply as E -import Brig.Sem.PasswordResetSupply.IO -import Brig.Sem.UserQuery.Cassandra +import Brig.Sem.UserKeyStore (UserKeyStore) +import Brig.Sem.UserQuery (UserQuery) import Brig.Types import Brig.Types.Intra import Cassandra import Control.Error -import Control.Lens (view) import Data.Id -import Data.Text (pack) +import Data.Qualified import qualified Data.Text.Ascii as Ascii import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as LT import Imports -import OpenSSL.BN (randIntegerZeroToNMinusOne) import OpenSSL.EVP.Digest (Digest, digestBS, getDigestByName) import Polysemy import Polysemy.Input -import Text.Printf (printf) import Wire.API.User -- | The information associated with the pending activation of a 'UserKey'. @@ -87,35 +85,30 @@ data ActivationEvent | EmailActivated !UserId !Email | PhoneActivated !UserId !Phone --- | Max. number of activation attempts per 'ActivationKey'. -maxAttempts :: Int32 -maxAttempts = 3 - -- docs/reference/user/activation.md {#RefActivationSubmit} activateKey :: - forall m. - ( MonadClient m, - MonadReader Env m - ) => + forall r. + Members + '[ ActivationKeyStore, + Input (Local ()), + E.PasswordResetSupply, + PasswordResetStore, + UserKeyStore, + UserQuery + ] + r => + Locale -> Digest -> ActivationKey -> ActivationCode -> Maybe UserId -> - ExceptT ActivationError m (Maybe ActivationEvent) -activateKey d k c u = verifyCode k c >>= pickUser >>= activate + ExceptT ActivationError (Sem r) (Maybe ActivationEvent) +activateKey locale d k c u = verifyCode k c >>= pickUser >>= activate where pickUser (uk, u') = maybe (throwE invalidUser) (pure . (uk,)) (u <|> u') activate (key, uid) = do - locale <- setDefaultUserLocale <$> view settings - locDomain <- qualifyLocal () a <- - lift - ( runM - . userQueryToCassandra @m @'[Embed m] - . runInputConst locDomain - $ lookupAccount locale uid - ) - >>= maybe (throwE invalidUser) pure + lift (lookupAccount locale uid) >>= maybe (throwE invalidUser) pure unless (accountStatus a == Active) $ -- this is never 'PendingActivation' in the flow this function is used in. throwE invalidCode case userIdentity (accountUser a) of @@ -149,20 +142,14 @@ activateKey d k c u = verifyCode k c >>= pickUser >>= activate for_ oldKey $ lift . deleteKey d pure . Just $ foldKey (EmailActivated uid) (PhoneActivated uid) key where - updateEmailAndDeleteEmailUnvalidated :: MonadClient m => UserId -> Email -> m () + updateEmailAndDeleteEmailUnvalidated :: UserId -> Email -> Sem r () updateEmailAndDeleteEmailUnvalidated u' email = updateEmail u' email <* deleteEmailUnvalidated u' - deleteCode :: UserId -> m () + deleteCode :: UserId -> Sem r () deleteCode uId = - runM - -- FUTUREWORK: use the DeletePasswordResetCode action instead of CodeDelete - ( codeStoreToCassandra @m - ( embed @m - ( liftIO @m (runM (passwordResetSupplyToIO @'[Embed IO] (E.mkPasswordResetKey uId))) - ) - >>= E.codeDelete - ) - ) + -- FUTUREWORK: use the DeletePasswordResetCode action instead of CodeDelete + E.mkPasswordResetKey uId + >>= E.deletePasswordResetCode claim key uid = do ok <- lift $ claimKey d key uid unless ok $ @@ -171,29 +158,25 @@ activateKey d k c u = verifyCode k c >>= pickUser >>= activate -- | Create a new pending activation for a given 'UserKey'. newActivation :: - (MonadIO m, MonadClient m) => + Members '[ActivationKeyStore, ActivationSupply] r => UserKey -> -- | The timeout for the activation code. Timeout -> -- | The user with whom to associate the activation code. Maybe UserId -> - m Activation + Sem r Activation newActivation uk timeout u = do (typ, key, code) <- - liftIO $ - foldKey - (\e -> ("email",fromEmail e,) <$> genCode) - (\p -> ("phone",fromPhone p,) <$> genCode) - uk + foldKey + (\e -> ("email",fromEmail e,) <$> makeActivationCode) + (\p -> ("phone",fromPhone p,) <$> makeActivationCode) + uk insert typ key code where insert t k c = do - key <- liftIO $ mkActivationKey uk - retry x5 . write keyInsert $ params LocalQuorum (key, t, k, c, u, maxAttempts, round timeout) + key <- makeActivationKey uk + insertActivationKey (key, t, k, c, u, maxAttempts, round timeout) pure $ Activation key c - genCode = - ActivationCode . Ascii.unsafeFromText . pack . printf "%06d" - <$> randIntegerZeroToNMinusOne 1000000 -- | Lookup an activation code and it's associated owner (if any) for a 'UserKey'. lookupActivationCode :: MonadClient m => UserKey -> m (Maybe (Maybe UserId, ActivationCode)) @@ -203,12 +186,12 @@ lookupActivationCode k = -- | Verify an activation code. verifyCode :: - MonadClient m => + Members '[ActivationKeyStore] r => ActivationKey -> ActivationCode -> - ExceptT ActivationError m (UserKey, Maybe UserId) + ExceptT ActivationError (Sem r) (UserKey, Maybe UserId) verifyCode key code = do - s <- lift . retry x1 . query1 keySelect $ params LocalQuorum (Identity key) + s <- lift . getActivationKey $ key case s of Just (ttl, Ascii t, k, c, u, r) -> if @@ -234,26 +217,11 @@ mkActivationKey k = do let bs = digestBS d' (T.encodeUtf8 $ keyText k) pure . ActivationKey $ Ascii.encodeBase64Url bs -deleteActivationPair :: MonadClient m => ActivationKey -> m () -deleteActivationPair = write keyDelete . params LocalQuorum . Identity - invalidUser :: ActivationError invalidUser = InvalidActivationCodeWrongUser -- "User does not exist." invalidCode :: ActivationError invalidCode = InvalidActivationCodeWrongCode -- "Invalid activation code" -keyInsert :: PrepQuery W (ActivationKey, Text, Text, ActivationCode, Maybe UserId, Int32, Int32) () -keyInsert = - "INSERT INTO activation_keys \ - \(key, key_type, key_text, code, user, retries) VALUES \ - \(? , ? , ? , ? , ? , ? ) USING TTL ?" - -keySelect :: PrepQuery R (Identity ActivationKey) (Int32, Ascii, Text, ActivationCode, Maybe UserId, Int32) -keySelect = "SELECT ttl(code) as ttl, key_type, key_text, code, user, retries FROM activation_keys WHERE key = ?" - codeSelect :: PrepQuery R (Identity ActivationKey) (Maybe UserId, ActivationCode) codeSelect = "SELECT user, code FROM activation_keys WHERE key = ?" - -keyDelete :: PrepQuery W (Identity ActivationKey) () -keyDelete = "DELETE FROM activation_keys WHERE key = ?" diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index a01125d4d1..b6bda55a3b 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -82,13 +82,18 @@ import Brig.Sem.UserQuery ( AuthError (..), ReAuthError (..), UserQuery, + activateUser, + deleteEmailUnvalidated, getAuthentication, getId, getLocale, getName, getUsers, + isActivated, lookupAccount, lookupAccounts, + updateEmail, + updatePhone, ) import Brig.Types import Brig.Types.Intra @@ -309,15 +314,9 @@ updateUser u UserUpdate {..} = retry x5 . batch $ do for_ uupAssets $ \a -> addPrepQuery userAssetsUpdate (a, u) for_ uupAccentId $ \c -> addPrepQuery userAccentIdUpdate (c, u) -updateEmail :: MonadClient m => UserId -> Email -> m () -updateEmail u e = retry x5 $ write userEmailUpdate (params LocalQuorum (e, u)) - updateEmailUnvalidated :: MonadClient m => UserId -> Email -> m () updateEmailUnvalidated u e = retry x5 $ write userEmailUnvalidatedUpdate (params LocalQuorum (e, u)) -updatePhone :: MonadClient m => UserId -> Phone -> m () -updatePhone u p = retry x5 $ write userPhoneUpdate (params LocalQuorum (p, u)) - updateSSOId :: MonadClient m => UserId -> Maybe UserSSOId -> m Bool updateSSOId u ssoid = do mteamid <- lookupUserTeam u @@ -353,9 +352,6 @@ updateFeatureConferenceCalling uid mbStatus = do deleteEmail :: MonadClient m => UserId -> m () deleteEmail u = retry x5 $ write userEmailDelete (params LocalQuorum (Identity u)) -deleteEmailUnvalidated :: MonadClient m => UserId -> m () -deleteEmailUnvalidated u = retry x5 $ write userEmailUnvalidatedDelete (params LocalQuorum (Identity u)) - deletePhone :: MonadClient m => UserId -> m () deletePhone u = retry x5 $ write userPhoneDelete (params LocalQuorum (Identity u)) @@ -386,13 +382,6 @@ updateStatus u s = userExists :: Member UserQuery r => UserId -> Sem r Bool userExists uid = isJust <$> getId uid --- | Whether the account has been activated by verifying --- an email address or phone number. -isActivated :: MonadClient m => UserId -> m Bool -isActivated u = - (== Just (Identity True)) - <$> retry x1 (query1 activatedSelect (params LocalQuorum (Identity u))) - filterActive :: MonadClient m => [UserId] -> m [UserId] filterActive us = map (view _1) . filter isActiveUser @@ -411,12 +400,6 @@ lookupUser :: Sem r (Maybe User) lookupUser loc locale hpi u = listToMaybe <$> lookupUsers loc locale hpi [u] -activateUser :: MonadClient m => UserId -> UserIdentity -> m () -activateUser u ident = do - let email = emailIdentity ident - let phone = phoneIdentity ident - retry x5 $ write userActivatedUpdate (params LocalQuorum (email, phone, u)) - deactivateUser :: MonadClient m => UserId -> m () deactivateUser u = retry x5 $ write userDeactivatedUpdate (params LocalQuorum (Identity u)) @@ -574,9 +557,6 @@ type UserRowInsert = passwordSelect :: PrepQuery R (Identity UserId) (Identity (Maybe Password)) passwordSelect = "SELECT password FROM user WHERE id = ?" -activatedSelect :: PrepQuery R (Identity UserId) (Identity Bool) -activatedSelect = "SELECT activated FROM user WHERE id = ?" - accountStateSelectAll :: PrepQuery R (Identity [UserId]) (UserId, Bool, Maybe AccountStatus) accountStateSelectAll = "SELECT id, activated, status FROM user WHERE id IN ?" @@ -611,18 +591,9 @@ userAssetsUpdate = "UPDATE user SET assets = ? WHERE id = ?" userAccentIdUpdate :: PrepQuery W (ColourId, UserId) () userAccentIdUpdate = "UPDATE user SET accent_id = ? WHERE id = ?" -userEmailUpdate :: PrepQuery W (Email, UserId) () -userEmailUpdate = "UPDATE user SET email = ? WHERE id = ?" - userEmailUnvalidatedUpdate :: PrepQuery W (Email, UserId) () userEmailUnvalidatedUpdate = "UPDATE user SET email_unvalidated = ? WHERE id = ?" -userEmailUnvalidatedDelete :: PrepQuery W (Identity UserId) () -userEmailUnvalidatedDelete = "UPDATE user SET email_unvalidated = null WHERE id = ?" - -userPhoneUpdate :: PrepQuery W (Phone, UserId) () -userPhoneUpdate = "UPDATE user SET phone = ? WHERE id = ?" - userSSOIdUpdate :: PrepQuery W (Maybe UserSSOId, UserId) () userSSOIdUpdate = "UPDATE user SET sso_id = ? WHERE id = ?" @@ -641,9 +612,6 @@ userStatusUpdate = "UPDATE user SET status = ? WHERE id = ?" userDeactivatedUpdate :: PrepQuery W (Identity UserId) () userDeactivatedUpdate = "UPDATE user SET activated = false WHERE id = ?" -userActivatedUpdate :: PrepQuery W (Maybe Email, Maybe Phone, UserId) () -userActivatedUpdate = "UPDATE user SET activated = true, email = ?, phone = ? WHERE id = ?" - userLocaleUpdate :: PrepQuery W (Language, Maybe Country, UserId) () userLocaleUpdate = "UPDATE user SET language = ?, country = ? WHERE id = ?" diff --git a/services/brig/src/Brig/Data/UserKey.hs b/services/brig/src/Brig/Data/UserKey.hs index c3119277d0..ab81a8f748 100644 --- a/services/brig/src/Brig/Data/UserKey.hs +++ b/services/brig/src/Brig/Data/UserKey.hs @@ -27,7 +27,7 @@ module Brig.Data.UserKey keyTextOriginal, claimKey, keyAvailable, - lookupKey, + getKey, deleteKey, lookupPhoneHashes, ) @@ -37,54 +37,16 @@ import Brig.Data.Instances () import qualified Brig.Data.User as User import Brig.Email import Brig.Phone +import Brig.Sem.UserKeyStore +import Brig.Sem.UserQuery (UserQuery) import Brig.Types import Cassandra import qualified Data.ByteString as B -import Data.ByteString.Lazy (toStrict) import Data.Id import qualified Data.Multihash.Digest as MH -import qualified Data.Text.Encoding as T import Imports -import OpenSSL.EVP.Digest (Digest, digestBS) - --- | A natural identifier (i.e. unique key) of a user. -data UserKey - = UserEmailKey !EmailKey - | UserPhoneKey !PhoneKey - -instance Eq UserKey where - (UserEmailKey k) == (UserEmailKey k') = k == k' - (UserPhoneKey k) == (UserPhoneKey k') = k == k' - _ == _ = False - -data UKHashType - = UKHashPhone - | UKHashEmail - deriving (Eq) - -instance Cql UKHashType where - ctype = Tagged IntColumn - - fromCql (CqlInt i) = case i of - 0 -> pure UKHashPhone - 1 -> pure UKHashEmail - n -> Left $ "unexpected hashtype: " ++ show n - fromCql _ = Left "userkeyhashtype: int expected" - - toCql UKHashPhone = CqlInt 0 - toCql UKHashEmail = CqlInt 1 - -newtype UserKeyHash = UserKeyHash MH.MultihashDigest - -instance Cql UserKeyHash where - ctype = Tagged BlobColumn - - fromCql (CqlBlob lbs) = case MH.decode (toStrict lbs) of - Left e -> Left ("userkeyhash: " ++ e) - Right h -> pure $ UserKeyHash h - fromCql _ = Left "userkeyhash: expected blob" - - toCql (UserKeyHash d) = CqlBlob $ MH.encode (MH.algorithm d) (MH.digest d) +import OpenSSL.EVP.Digest (Digest) +import Polysemy userEmailKey :: Email -> UserKey userEmailKey = UserEmailKey . mkEmailKey @@ -103,11 +65,6 @@ forEmailKey k f = foldKey (fmap Just . f) (const (pure Nothing)) k forPhoneKey :: Applicative f => UserKey -> (Phone -> f a) -> f (Maybe a) forPhoneKey k f = foldKey (const (pure Nothing)) (fmap Just . f) k --- | Get the normalised text of a 'UserKey'. -keyText :: UserKey -> Text -keyText (UserEmailKey k) = emailKeyUniq k -keyText (UserPhoneKey k) = phoneKeyUniq k - -- | Get the original text of a 'UserKey', i.e. the original phone number -- or email address. keyTextOriginal :: UserKey -> Text @@ -116,14 +73,14 @@ keyTextOriginal (UserPhoneKey k) = fromPhone (phoneKeyOrig k) -- | Claim a 'UserKey' for a user. claimKey :: - MonadClient m => + Members '[UserKeyStore, UserQuery] r => -- | The SHA256 digest Digest -> -- | The key to claim. UserKey -> -- | The user claiming the key. UserId -> - m Bool + Sem r Bool claimKey d k u = do free <- keyAvailable k (Just u) when free (insertKey d u k) @@ -133,42 +90,19 @@ claimKey d k u = do -- A key is available if it is not already actived for another user or -- if the other user and the user looking to claim the key are the same. keyAvailable :: - MonadClient m => + Members '[UserKeyStore, UserQuery] r => -- | The key to check. UserKey -> -- | The user looking to claim the key, if any. Maybe UserId -> - m Bool + Sem r Bool keyAvailable k u = do - o <- lookupKey k + o <- getKey k case (o, u) of (Nothing, _) -> pure True (Just x, Just y) | x == y -> pure True (Just x, _) -> not <$> User.isActivated x -lookupKey :: MonadClient m => UserKey -> m (Maybe UserId) -lookupKey k = - fmap runIdentity - <$> retry x1 (query1 keySelect (params LocalQuorum (Identity $ keyText k))) - -insertKey :: MonadClient m => Digest -> UserId -> UserKey -> m () -insertKey d u k = do - let hk = hashKey d k - kt = foldKey (\(_ :: Email) -> UKHashEmail) (\(_ :: Phone) -> UKHashPhone) k - retry x5 $ write insertHashed (params LocalQuorum (hk, kt, u)) - retry x5 $ write keyInsert (params LocalQuorum (keyText k, u)) - -deleteKey :: MonadClient m => Digest -> UserKey -> m () -deleteKey d k = do - let hk = hashKey d k - retry x5 $ write deleteHashed (params LocalQuorum (Identity hk)) - retry x5 $ write keyDelete (params LocalQuorum (Identity $ keyText k)) - -hashKey :: Digest -> UserKey -> UserKeyHash -hashKey d uk = - let d' = digestBS d $ T.encodeUtf8 (keyText uk) - in UserKeyHash $ MH.MultihashDigest MH.SHA256 (B.length d') d' - lookupPhoneHashes :: MonadClient m => [ByteString] -> m [(ByteString, UserId)] lookupPhoneHashes hp = mapMaybe mk <$> retry x1 (query selectHashed (params One (Identity hashed))) @@ -180,20 +114,5 @@ lookupPhoneHashes hp = -------------------------------------------------------------------------------- -- Queries -keyInsert :: PrepQuery W (Text, UserId) () -keyInsert = "INSERT INTO user_keys (key, user) VALUES (?, ?)" - -keySelect :: PrepQuery R (Identity Text) (Identity UserId) -keySelect = "SELECT user FROM user_keys WHERE key = ?" - -keyDelete :: PrepQuery W (Identity Text) () -keyDelete = "DELETE FROM user_keys WHERE key = ?" - -insertHashed :: PrepQuery W (UserKeyHash, UKHashType, UserId) () -insertHashed = "INSERT INTO user_keys_hash(key, key_type, user) VALUES (?, ?, ?)" - -deleteHashed :: PrepQuery W (Identity UserKeyHash) () -deleteHashed = "DELETE FROM user_keys_hash WHERE key = ?" - selectHashed :: PrepQuery R (Identity [UserKeyHash]) (UserKeyHash, UKHashType, UserId) selectHashed = "SELECT key, key_type, user FROM user_keys_hash WHERE key IN ?" diff --git a/services/brig/src/Brig/Sem/ActivationKeyStore.hs b/services/brig/src/Brig/Sem/ActivationKeyStore.hs new file mode 100644 index 0000000000..1f9fe3c800 --- /dev/null +++ b/services/brig/src/Brig/Sem/ActivationKeyStore.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.ActivationKeyStore where + +import Cassandra (Ascii) +import Data.Id +import Imports +import Polysemy +import Wire.API.User.Activation + +-- | Max. number of activation attempts per 'ActivationKey'. +maxAttempts :: Int32 +maxAttempts = 3 + +type GetKeyTuple = (Int32, Ascii, Text, ActivationCode, Maybe UserId, Int32) + +type InsertKeyTuple = (ActivationKey, Text, Text, ActivationCode, Maybe UserId, Int32, Int32) + +data ActivationKeyStore m a where + GetActivationKey :: ActivationKey -> ActivationKeyStore m (Maybe GetKeyTuple) + InsertActivationKey :: InsertKeyTuple -> ActivationKeyStore m () + DeleteActivationPair :: ActivationKey -> ActivationKeyStore m () + +makeSem ''ActivationKeyStore diff --git a/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs b/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs new file mode 100644 index 0000000000..51ebfcf377 --- /dev/null +++ b/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs @@ -0,0 +1,53 @@ +-- 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.Sem.ActivationKeyStore.Cassandra (activationKeyStoreToCassandra) where + +import Brig.Sem.ActivationKeyStore +import Cassandra +import Data.Id +import Imports +import Polysemy +import Wire.API.User.Activation + +activationKeyStoreToCassandra :: + forall m r a. + (MonadClient m, Member (Embed m) r, Cql ActivationKey, Cql ActivationCode) => + Sem (ActivationKeyStore ': r) a -> + Sem r a +activationKeyStoreToCassandra = + interpret $ + embed @m . \case + GetActivationKey k -> getKey k + InsertActivationKey tuple -> keyInsertQuery tuple + DeleteActivationPair k -> undefined k + +getKey :: (MonadClient m, Cql ActivationKey, Cql ActivationCode) => ActivationKey -> m (Maybe GetKeyTuple) +getKey key = retry x1 . query1 keySelect $ params LocalQuorum (Identity key) + where + keySelect :: PrepQuery R (Identity ActivationKey) (Int32, Ascii, Text, ActivationCode, Maybe UserId, Int32) + keySelect = "SELECT ttl(code) as ttl, key_type, key_text, code, user, retries FROM activation_keys WHERE key = ?" + +keyInsertQuery :: (MonadClient m, Cql ActivationKey, Cql ActivationCode) => InsertKeyTuple -> m () +keyInsertQuery (key, t, k, c, u, attempts, timeout) = + retry x5 . write keyInsert $ params LocalQuorum (key, t, k, c, u, attempts, timeout) + where + keyInsert :: PrepQuery W (ActivationKey, Text, Text, ActivationCode, Maybe UserId, Int32, Int32) () + keyInsert = + "INSERT INTO activation_keys \ + \(key, key_type, key_text, code, user, retries) VALUES \ + \(? , ? , ? , ? , ? , ? ) USING TTL ?" diff --git a/services/brig/src/Brig/Sem/ActivationSupply.hs b/services/brig/src/Brig/Sem/ActivationSupply.hs new file mode 100644 index 0000000000..1ba2cfd90c --- /dev/null +++ b/services/brig/src/Brig/Sem/ActivationSupply.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.ActivationSupply where + +import Brig.Sem.UserKeyStore (UserKey) -- TODO: The UserKey type should be moved to wire-api +import Polysemy +import Wire.API.User.Activation + +data ActivationSupply m a where + MakeActivationKey :: UserKey -> ActivationSupply m ActivationKey + MakeActivationCode :: ActivationSupply m ActivationCode + +makeSem ''ActivationSupply diff --git a/services/brig/src/Brig/Sem/ActivationSupply/IO.hs b/services/brig/src/Brig/Sem/ActivationSupply/IO.hs new file mode 100644 index 0000000000..57d05ee635 --- /dev/null +++ b/services/brig/src/Brig/Sem/ActivationSupply/IO.hs @@ -0,0 +1,53 @@ +-- 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.Sem.ActivationSupply.IO (activationSupplyToIO) where + +import Brig.Sem.ActivationSupply +import Brig.Sem.UserKeyStore (UserKey, keyText) +import Data.Text +import qualified Data.Text.Ascii as Ascii +import qualified Data.Text.Encoding as T +import Imports +import OpenSSL.BN +import OpenSSL.EVP.Digest +import Polysemy +import Text.Printf +import Wire.API.User.Activation + +activationSupplyToIO :: + forall r a. + Member (Embed IO) r => + Sem (ActivationSupply ': r) a -> + Sem r a +activationSupplyToIO = + interpret $ + embed @IO . \case + MakeActivationKey key -> mkActivationKey key + MakeActivationCode -> mkActivationCode + +mkActivationKey :: UserKey -> IO ActivationKey +mkActivationKey k = do + d <- liftIO $ getDigestByName "SHA256" + d' <- maybe (fail "SHA256 not found") pure d + let bs = digestBS d' (T.encodeUtf8 $ keyText k) + pure . ActivationKey $ Ascii.encodeBase64Url bs + +mkActivationCode :: IO ActivationCode +mkActivationCode = + ActivationCode . Ascii.unsafeFromText . pack . printf "%06d" + <$> randIntegerZeroToNMinusOne 1000000 diff --git a/services/brig/src/Brig/Sem/GalleyAccess.hs b/services/brig/src/Brig/Sem/GalleyAccess.hs new file mode 100644 index 0000000000..212e031902 --- /dev/null +++ b/services/brig/src/Brig/Sem/GalleyAccess.hs @@ -0,0 +1,29 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.GalleyAccess where + +import Data.Id +import Imports +import Polysemy + +data GalleyAccess m a where + GetTeamSndFactorPasswordChallenge :: TeamId -> GalleyAccess m TeamFeatureEnabled + +makeSem ''GalleyAccess diff --git a/services/brig/src/Brig/Sem/GalleyAccess/Http.hs b/services/brig/src/Brig/Sem/GalleyAccess/Http.hs new file mode 100644 index 0000000000..30e99e595f --- /dev/null +++ b/services/brig/src/Brig/Sem/GalleyAccess/Http.hs @@ -0,0 +1,40 @@ +-- 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.Sem.GalleyAccess.Http (galleyAccessToHttp) where + +import Brig.RPC +import Brig.Sem.GalleyAccess +import Imports +import Polysemy + +galleyAccessToHttp :: + forall m r a. + ( MonadReader Env m, + MonadIO m, + MonadMask m, + MonadHttp m, + HasRequestId m + ) => + Sem (GalleyAccess ': r) a -> + Sem r a +galleyAccessToHttp = + interpret $ + embed @m . \case + GetTeamSndFactorPasswordChallenge tid -> do + response <- galleyRequest GET req + tfwoStatus <$> decodeBody "galley" response diff --git a/services/brig/src/Brig/Sem/Twilio.hs b/services/brig/src/Brig/Sem/Twilio.hs new file mode 100644 index 0000000000..cd57693c39 --- /dev/null +++ b/services/brig/src/Brig/Sem/Twilio.hs @@ -0,0 +1,37 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.Twilio where + +import Data.ISO3166_CountryCodes +import Imports +import Network.HTTP.Client +import Polysemy +import Ropes.Twilio + +data Twilio m a where + LookupPhone :: + Credentials -> + Manager -> + Text -> + LookupDetail -> + Maybe CountryCode -> + Twilio m (Either ErrorResponse LookupResult) + +makeSem ''Twilio diff --git a/services/brig/src/Brig/Sem/Twilio/IO.hs b/services/brig/src/Brig/Sem/Twilio/IO.hs new file mode 100644 index 0000000000..a6e67b19b4 --- /dev/null +++ b/services/brig/src/Brig/Sem/Twilio/IO.hs @@ -0,0 +1,40 @@ +-- 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.Sem.Twilio.IO (twilioToIO) where + +import Bilge.Retry +import Brig.RPC +import Brig.Sem.Twilio +import Control.Monad.Catch +import Control.Retry +import Imports +import Polysemy +import qualified Ropes.Twilio as Ropes + +twilioToIO :: + forall r a. + Member (Embed IO) r => + Sem (Twilio ': r) a -> + Sem r a +twilioToIO = + interpret $ + embed @IO . \case + LookupPhone cred m txt detail code -> + liftIO . try @_ @Ropes.ErrorResponse $ + recovering x3 httpHandlers $ + const $ Ropes.lookupPhone cred m txt detail code diff --git a/services/brig/src/Brig/Sem/UserKeyStore.hs b/services/brig/src/Brig/Sem/UserKeyStore.hs new file mode 100644 index 0000000000..b560b098fd --- /dev/null +++ b/services/brig/src/Brig/Sem/UserKeyStore.hs @@ -0,0 +1,81 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.UserKeyStore where + +import Brig.Email +import Brig.Phone +import Cassandra +import Data.ByteString.Lazy +import Data.Id +import qualified Data.Multihash.Digest as MH +import Imports +import OpenSSL.EVP.Digest +import Polysemy + +-- | A natural identifier (i.e. unique key) of a user. +data UserKey + = UserEmailKey !EmailKey + | UserPhoneKey !PhoneKey + +instance Eq UserKey where + (UserEmailKey k) == (UserEmailKey k') = k == k' + (UserPhoneKey k) == (UserPhoneKey k') = k == k' + _ == _ = False + +newtype UserKeyHash = UserKeyHash MH.MultihashDigest + +instance Cql UserKeyHash where + ctype = Tagged BlobColumn + + fromCql (CqlBlob lbs) = case MH.decode (toStrict lbs) of + Left e -> Left ("userkeyhash: " ++ e) + Right h -> pure $ UserKeyHash h + fromCql _ = Left "userkeyhash: expected blob" + + toCql (UserKeyHash d) = CqlBlob $ MH.encode (MH.algorithm d) (MH.digest d) + +data UKHashType + = UKHashPhone + | UKHashEmail + deriving (Eq) + +instance Cql UKHashType where + ctype = Tagged IntColumn + + fromCql (CqlInt i) = case i of + 0 -> pure UKHashPhone + 1 -> pure UKHashEmail + n -> Left $ "unexpected hashtype: " ++ show n + fromCql _ = Left "userkeyhashtype: int expected" + + toCql UKHashPhone = CqlInt 0 + toCql UKHashEmail = CqlInt 1 + +data UserKeyStore m a where + GetKey :: UserKey -> UserKeyStore m (Maybe UserId) + InsertKey :: Digest -> UserId -> UserKey -> UserKeyStore m () + DeleteKey :: Digest -> UserKey -> UserKeyStore m () + +makeSem ''UserKeyStore + +-- | Get the normalised text of a 'UserKey'. +keyText :: UserKey -> Text +keyText (UserEmailKey k) = emailKeyUniq k +keyText (UserPhoneKey k) = phoneKeyUniq k diff --git a/services/brig/src/Brig/Sem/UserKeyStore/Cassandra.hs b/services/brig/src/Brig/Sem/UserKeyStore/Cassandra.hs new file mode 100644 index 0000000000..67973a83aa --- /dev/null +++ b/services/brig/src/Brig/Sem/UserKeyStore/Cassandra.hs @@ -0,0 +1,80 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.UserKeyStore.Cassandra (keyStoreToCassandra) where + +import Brig.Data.UserKey +import Brig.Email +import Brig.Phone +import Brig.Sem.UserKeyStore +import Cassandra +import qualified Data.ByteString as B +import Data.Id +import qualified Data.Multihash.Digest as MH +import qualified Data.Text.Encoding as T +import Imports +import OpenSSL.EVP.Digest +import Polysemy + +keyStoreToCassandra :: + forall m r a. + (MonadClient m, Member (Embed m) r) => + Sem (UserKeyStore ': r) a -> + Sem r a +keyStoreToCassandra = + interpret $ + embed @m . \case + GetKey uid -> lookupKeyQuery uid + InsertKey d uid k -> insertKeyQuery d uid k + DeleteKey d k -> deleteKeyQuery d k + +hashKey :: Digest -> UserKey -> UserKeyHash +hashKey d uk = + let d' = digestBS d $ T.encodeUtf8 (keyText uk) + in UserKeyHash $ MH.MultihashDigest MH.SHA256 (B.length d') d' + +lookupKeyQuery :: MonadClient m => UserKey -> m (Maybe UserId) +lookupKeyQuery k = + fmap runIdentity + <$> retry x1 (query1 keySelect (params LocalQuorum (Identity $ keyText k))) + +keySelect :: PrepQuery R (Identity Text) (Identity UserId) +keySelect = "SELECT user FROM user_keys WHERE key = ?" + +insertKeyQuery :: MonadClient m => Digest -> UserId -> UserKey -> m () +insertKeyQuery d u k = do + let hk = hashKey d k + kt = foldKey (\(_ :: Email) -> UKHashEmail) (\(_ :: Phone) -> UKHashPhone) k + retry x5 $ write insertHashed (params LocalQuorum (hk, kt, u)) + retry x5 $ write keyInsert (params LocalQuorum (keyText k, u)) + +insertHashed :: PrepQuery W (UserKeyHash, UKHashType, UserId) () +insertHashed = "INSERT INTO user_keys_hash(key, key_type, user) VALUES (?, ?, ?)" + +keyInsert :: PrepQuery W (Text, UserId) () +keyInsert = "INSERT INTO user_keys (key, user) VALUES (?, ?)" + +deleteKeyQuery :: MonadClient m => Digest -> UserKey -> m () +deleteKeyQuery d k = do + let hk = hashKey d k + retry x5 $ write deleteHashed (params LocalQuorum (Identity hk)) + retry x5 $ write keyDelete (params LocalQuorum (Identity $ keyText k)) + where + deleteHashed :: PrepQuery W (Identity UserKeyHash) () + deleteHashed = "DELETE FROM user_keys_hash WHERE key = ?" + keyDelete :: PrepQuery W (Identity Text) () + keyDelete = "DELETE FROM user_keys WHERE key = ?" diff --git a/services/brig/src/Brig/Sem/UserQuery.hs b/services/brig/src/Brig/Sem/UserQuery.hs index c6fc1df851..61b3378174 100644 --- a/services/brig/src/Brig/Sem/UserQuery.hs +++ b/services/brig/src/Brig/Sem/UserQuery.hs @@ -30,8 +30,13 @@ module Brig.Sem.UserQuery getAccountStatuses, getTeam, getAccounts, + isActivated, insertAccount, updateUser, + updateEmail, + updatePhone, + activateUser, + deleteEmailUnvalidated, -- * effect-derived functions lookupAccount, @@ -275,6 +280,10 @@ data UserQuery m a where GetAccountStatuses :: [UserId] -> UserQuery m [(UserId, Bool, Maybe AccountStatus)] -- accountStateSelectAll GetTeam :: UserId -> UserQuery m (Maybe TeamId) -- teamSelect GetAccounts :: [UserId] -> UserQuery m [AccountRow] -- accountsSelect + + -- | Whether the account has been activated by verifying an email address or + -- phone number. + IsActivated :: UserId -> UserQuery m Bool -- FUTUREWORK: The 'InsertAccount' action should perhaps be in an account store effect InsertAccount :: UserAccount -> @@ -286,6 +295,10 @@ data UserQuery m a where Bool -> UserQuery m () UpdateUser :: UserId -> UserUpdate -> UserQuery m () + UpdateEmail :: UserId -> Email -> UserQuery m () + UpdatePhone :: UserId -> Phone -> UserQuery m () + ActivateUser :: UserId -> UserIdentity -> UserQuery m () + DeleteEmailUnvalidated :: UserId -> UserQuery m () makeSem ''UserQuery diff --git a/services/brig/src/Brig/Sem/UserQuery/Cassandra.hs b/services/brig/src/Brig/Sem/UserQuery/Cassandra.hs index abace2098a..494c23dda7 100644 --- a/services/brig/src/Brig/Sem/UserQuery/Cassandra.hs +++ b/services/brig/src/Brig/Sem/UserQuery/Cassandra.hs @@ -49,8 +49,13 @@ userQueryToCassandra = GetAccountStatuses uids -> retry x1 (query accountStateSelectAll (params LocalQuorum (Identity uids))) GetTeam uid -> (runIdentity =<<) <$> retry x1 (query1 teamSelect (params LocalQuorum (Identity uid))) GetAccounts uids -> retry x1 (query accountsSelect (params LocalQuorum (Identity uids))) + IsActivated uid -> isActivatedQuery uid InsertAccount ua mConvTeam mPass act -> accountInsert ua mConvTeam mPass act UpdateUser uid update -> userUpdate uid update + UpdateEmail uid email -> updateEmailQuery uid email + UpdatePhone uid phone -> updatePhoneQuery uid phone + ActivateUser uid ui -> activateUserQuery uid ui + DeleteEmailUnvalidated uid -> deleteEmailUnvalidatedQuery uid -------------------------------------------------------------------------------- -- Queries @@ -174,3 +179,36 @@ userUpdate u UserUpdate {..} = retry x5 . batch $ do userAccentIdUpdate :: PrepQuery W (ColourId, UserId) () userAccentIdUpdate = "UPDATE user SET accent_id = ? WHERE id = ?" + +isActivatedQuery :: MonadClient m => UserId -> m Bool +isActivatedQuery u = + (== Just (Identity True)) + <$> retry x1 (query1 activatedSelect (params LocalQuorum (Identity u))) + +updateEmailQuery :: MonadClient m => UserId -> Email -> m () +updateEmailQuery u e = retry x5 $ write userEmailUpdate (params LocalQuorum (e, u)) + where + userEmailUpdate :: PrepQuery W (Email, UserId) () + userEmailUpdate = "UPDATE user SET email = ? WHERE id = ?" + +deleteEmailUnvalidatedQuery :: MonadClient m => UserId -> m () +deleteEmailUnvalidatedQuery u = + retry x5 $ write userEmailUnvalidatedDelete (params LocalQuorum (Identity u)) + where + userEmailUnvalidatedDelete :: PrepQuery W (Identity UserId) () + userEmailUnvalidatedDelete = "UPDATE user SET email_unvalidated = null WHERE id = ?" + +updatePhoneQuery :: MonadClient m => UserId -> Phone -> m () +updatePhoneQuery u p = retry x5 $ write userPhoneUpdate (params LocalQuorum (p, u)) + where + userPhoneUpdate :: PrepQuery W (Phone, UserId) () + userPhoneUpdate = "UPDATE user SET phone = ? WHERE id = ?" + +activateUserQuery :: MonadClient m => UserId -> UserIdentity -> m () +activateUserQuery u ident = do + let email = emailIdentity ident + let phone = phoneIdentity ident + retry x5 $ write userActivatedUpdate (params LocalQuorum (email, phone, u)) + where + userActivatedUpdate :: PrepQuery W (Maybe Email, Maybe Phone, UserId) () + userActivatedUpdate = "UPDATE user SET activated = true, email = ?, phone = ? WHERE id = ?" From 41835f0aeab2701f73213a284ca529d2e66748b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 15 Jun 2022 07:22:48 +0200 Subject: [PATCH 12/41] Lots of new effects - It finally compiles!! (though yet to fix the brig-integration package) --- libs/brig-types/src/Brig/Types/Common.hs | 84 ++++++ services/brig/brig.cabal | 9 + services/brig/package.yaml | 2 + services/brig/src/Brig/API.hs | 32 +- services/brig/src/Brig/API/Client.hs | 23 +- services/brig/src/Brig/API/Connection.hs | 7 +- services/brig/src/Brig/API/Federation.hs | 57 ++-- services/brig/src/Brig/API/Internal.hs | 181 ++++++++++-- services/brig/src/Brig/API/Public.hs | 275 +++++++++++++++--- services/brig/src/Brig/API/Types.hs | 1 - services/brig/src/Brig/API/User.hs | 162 ++++++++--- services/brig/src/Brig/App.hs | 86 +++++- services/brig/src/Brig/Code.hs | 110 +------ services/brig/src/Brig/Data/Activation.hs | 5 +- services/brig/src/Brig/Data/User.hs | 79 +---- services/brig/src/Brig/Data/UserKey.hs | 5 - services/brig/src/Brig/Email.hs | 39 +-- .../brig/src/Brig/InternalEvent/Process.hs | 60 ++-- services/brig/src/Brig/Phone.hs | 50 +--- services/brig/src/Brig/Provider/API.hs | 98 +++++-- services/brig/src/Brig/Provider/DB.hs | 4 +- services/brig/src/Brig/RPC.hs | 27 +- services/brig/src/Brig/RPC/Decode.hs | 47 +++ services/brig/src/Brig/Run.hs | 9 +- .../brig/src/Brig/Sem/ActivationSupply.hs | 3 +- .../brig/src/Brig/Sem/ActivationSupply/IO.hs | 3 +- services/brig/src/Brig/Sem/GalleyAccess.hs | 4 +- .../brig/src/Brig/Sem/GalleyAccess/Http.hs | 50 +++- services/brig/src/Brig/Sem/Twilio/IO.hs | 4 +- .../brig/src/Brig/Sem/UniqueClaimsStore.hs | 33 +++ .../Brig/Sem/UniqueClaimsStore/Cassandra.hs | 81 ++++++ services/brig/src/Brig/Sem/UserHandleStore.hs | 44 +++ .../src/Brig/Sem/UserHandleStore/Cassandra.hs | 73 +++++ services/brig/src/Brig/Sem/UserKeyStore.hs | 18 +- .../src/Brig/Sem/UserKeyStore/Cassandra.hs | 10 +- services/brig/src/Brig/Sem/UserQuery.hs | 22 +- .../brig/src/Brig/Sem/UserQuery/Cassandra.hs | 8 + .../src/Brig/Sem/VerificationCodeStore.hs | 91 ++++++ .../Sem/VerificationCodeStore/Cassandra.hs | 110 +++++++ services/brig/src/Brig/Team/API.hs | 86 +++++- services/brig/src/Brig/Unique.hs | 79 ++--- services/brig/src/Brig/User/API/Auth.hs | 134 +++++++-- services/brig/src/Brig/User/API/Handle.hs | 16 +- services/brig/src/Brig/User/API/Search.hs | 20 +- services/brig/src/Brig/User/Auth.hs | 241 ++++++++------- services/brig/src/Brig/User/EJPD.hs | 6 +- services/brig/src/Brig/User/Handle.hs | 97 +++--- 47 files changed, 1897 insertions(+), 788 deletions(-) create mode 100644 services/brig/src/Brig/RPC/Decode.hs create mode 100644 services/brig/src/Brig/Sem/UniqueClaimsStore.hs create mode 100644 services/brig/src/Brig/Sem/UniqueClaimsStore/Cassandra.hs create mode 100644 services/brig/src/Brig/Sem/UserHandleStore.hs create mode 100644 services/brig/src/Brig/Sem/UserHandleStore/Cassandra.hs create mode 100644 services/brig/src/Brig/Sem/VerificationCodeStore.hs create mode 100644 services/brig/src/Brig/Sem/VerificationCodeStore/Cassandra.hs diff --git a/libs/brig-types/src/Brig/Types/Common.hs b/libs/brig-types/src/Brig/Types/Common.hs index b92ec41867..fc52b2dfa0 100644 --- a/libs/brig-types/src/Brig/Types/Common.hs +++ b/libs/brig-types/src/Brig/Types/Common.hs @@ -30,6 +30,19 @@ module Brig.Types.Common allPrefixes, ExcludedPrefix (..), + -- * Unique Keys + EmailKey, + mkEmailKey, + emailKeyUniq, + emailKeyOrig, + PhoneKey, + mkPhoneKey, + phoneKeyUniq, + phoneKeyOrig, + UserKey (..), + keyText, + foldKey, + -- * re-exports Name (..), ColourId (..), @@ -144,3 +157,74 @@ instance FromJSON ExcludedPrefix where instance ToJSON ExcludedPrefix where toJSON (ExcludedPrefix p c) = object ["phone_prefix" .= p, "comment" .= c] + +------------------------------------------------------------------------------- +-- Unique Keys + +-- | An 'EmailKey' is an 'Email' in a form that serves as a unique lookup key. +data EmailKey = EmailKey + { emailKeyUniq :: !Text, + emailKeyOrig :: !Email + } + +instance Show EmailKey where + showsPrec _ = shows . emailKeyUniq + +instance Eq EmailKey where + (EmailKey k _) == (EmailKey k' _) = k == k' + +-- | Turn an 'Email' into an 'EmailKey'. +-- +-- The following transformations are performed: +-- +-- * Both local and domain parts are forced to lowercase to make +-- e-mail addresses fully case-insensitive. +-- * "+" suffixes on the local part are stripped unless the domain +-- part is contained in a trusted whitelist. +mkEmailKey :: Email -> EmailKey +mkEmailKey orig@(Email localPart domain) = + let uniq = Text.toLower localPart' <> "@" <> Text.toLower domain + in EmailKey uniq orig + where + localPart' + | domain `notElem` trusted = Text.takeWhile (/= '+') localPart + | otherwise = localPart + trusted = ["wearezeta.com", "wire.com", "simulator.amazonses.com"] + +data PhoneKey = PhoneKey + { -- | canonical form of 'phoneKeyOrig', without whitespace. + phoneKeyUniq :: !Text, + -- | phone number with whitespace. + phoneKeyOrig :: !Phone + } + +instance Show PhoneKey where + showsPrec _ = shows . phoneKeyUniq + +instance Eq PhoneKey where + (PhoneKey k _) == (PhoneKey k' _) = k == k' + +mkPhoneKey :: Phone -> PhoneKey +mkPhoneKey orig = + let uniq = Text.filter (not . isSpace) (fromPhone orig) + in PhoneKey uniq orig + +-- | A natural identifier (i.e. unique key) of a user. +data UserKey + = UserEmailKey !EmailKey + | UserPhoneKey !PhoneKey + +instance Eq UserKey where + (UserEmailKey k) == (UserEmailKey k') = k == k' + (UserPhoneKey k) == (UserPhoneKey k') = k == k' + _ == _ = False + +-- | Get the normalised text of a 'UserKey'. +keyText :: UserKey -> Text +keyText (UserEmailKey k) = emailKeyUniq k +keyText (UserPhoneKey k) = phoneKeyUniq k + +foldKey :: (Email -> a) -> (Phone -> a) -> UserKey -> a +foldKey f g k = case k of + UserEmailKey ek -> f (emailKeyOrig ek) + UserPhoneKey pk -> g (phoneKeyOrig pk) diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index e2ff8800db..5e55edf1ac 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -83,6 +83,7 @@ library Brig.Queue.Stomp Brig.Queue.Types Brig.RPC + Brig.RPC.Decode Brig.Run Brig.Sem.ActivationKeyStore Brig.Sem.ActivationKeyStore.Cassandra @@ -100,10 +101,16 @@ library Brig.Sem.PasswordResetSupply.IO Brig.Sem.Twilio Brig.Sem.Twilio.IO + Brig.Sem.UniqueClaimsStore + Brig.Sem.UniqueClaimsStore.Cassandra + Brig.Sem.UserHandleStore + Brig.Sem.UserHandleStore.Cassandra Brig.Sem.UserKeyStore Brig.Sem.UserKeyStore.Cassandra Brig.Sem.UserQuery Brig.Sem.UserQuery.Cassandra + Brig.Sem.VerificationCodeStore + Brig.Sem.VerificationCodeStore.Cassandra Brig.SMTP Brig.Team.API Brig.Team.DB @@ -251,6 +258,8 @@ library , optparse-applicative >=0.11 , pem >=0.2 , polysemy + , polysemy-conc + , polysemy-time , polysemy-wire-zoo , proto-lens >=0.1 , random-shuffle >=0.0.3 diff --git a/services/brig/package.yaml b/services/brig/package.yaml index 861d201aab..913c63a5f1 100644 --- a/services/brig/package.yaml +++ b/services/brig/package.yaml @@ -86,6 +86,8 @@ library: - optparse-applicative >=0.11 - pem >=0.2 - polysemy + - polysemy-conc + - polysemy-time - polysemy-wire-zoo - proto-lens >=0.1 - random-shuffle >=0.0.3 diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index 447552a17d..8994671a19 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -24,25 +24,51 @@ import Brig.API.Handler (Handler) import qualified Brig.API.Internal as Internal import qualified Brig.API.Public as Public import Brig.API.Types +import Brig.Sem.ActivationKeyStore +import Brig.Sem.ActivationSupply +import Brig.Sem.BudgetStore +import Brig.Sem.GalleyAccess import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetSupply (PasswordResetSupply) -import Brig.Sem.UserQuery (UserQuery) +import Brig.Sem.Twilio +import Brig.Sem.UniqueClaimsStore +import Brig.Sem.UserHandleStore +import Brig.Sem.UserKeyStore +import Brig.Sem.UserQuery +import Brig.Sem.VerificationCodeStore import Data.Qualified import qualified Data.Swagger.Build.Api as Doc import Network.Wai.Routing (Routes) import Polysemy +import Polysemy.Async +import Polysemy.Conc.Effect.Race import Polysemy.Error import Polysemy.Input +import Polysemy.Resource import qualified Polysemy.TinyLog as P +import qualified Ropes.Twilio as Twilio sitemap :: Members - '[ Error ReAuthError, + '[ ActivationKeyStore, + ActivationSupply, + Async, + BudgetStore, + Error ReAuthError, + Error Twilio.ErrorResponse, + GalleyAccess, Input (Local ()), P.TinyLog, PasswordResetStore, PasswordResetSupply, - UserQuery + Race, + Resource, + Twilio, + UniqueClaimsStore, + UserHandleStore, + UserKeyStore, + UserQuery, + VerificationCodeStore ] r => Routes Doc.ApiBuilder (Handler r) () diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index ef96957088..7206c640c6 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -57,7 +57,9 @@ import qualified Brig.Federation.Client as Federation import Brig.IO.Intra (guardLegalhold) import qualified Brig.IO.Intra as Intra import qualified Brig.Options as Opt +import Brig.Sem.GalleyAccess import Brig.Sem.UserQuery (UserQuery) +import Brig.Sem.VerificationCodeStore import Brig.Types import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) @@ -132,7 +134,7 @@ lookupLocalPubClientsBulk :: [UserId] -> ExceptT ClientError (AppT r) (UserMap ( lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk addClient :: - Members '[Input (Local ()), UserQuery] r => + Members '[GalleyAccess, Input (Local ()), UserQuery, VerificationCodeStore] r => UserId -> Maybe ConnId -> Maybe IP -> @@ -143,7 +145,8 @@ addClient = addClientWithReAuthPolicy Data.reAuthForNewClients -- nb. We must ensure that the set of clients known to brig is always -- a superset of the clients known to galley. addClientWithReAuthPolicy :: - Members '[Input (Local ()), UserQuery] r => + forall r. + Members '[GalleyAccess, Input (Local ()), UserQuery, VerificationCodeStore] r => Data.ReAuthPolicy -> UserId -> Maybe ConnId -> @@ -153,7 +156,7 @@ addClientWithReAuthPolicy :: addClientWithReAuthPolicy policy u con ip new = do locale <- Opt.setDefaultUserLocale <$> view settings acc <- lift (liftSem $ Data.lookupAccount locale u) >>= maybe (throwE (ClientUserNotFound u)) pure - wrapHttpClientE $ verifyCode (newClientVerificationCode new) (userId . accountUser $ acc) + verifyCodeThrow (newClientVerificationCode new) (userId . accountUser $ acc) loc <- maybe (pure Nothing) locationOf ip maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings let caps :: Maybe (Set ClientCapability) @@ -183,19 +186,11 @@ addClientWithReAuthPolicy policy u con ip new = do where clientId' = clientIdFromPrekey (unpackLastPrekey $ newClientLastKey new) - verifyCode :: - ( MonadReader Env m, - MonadMask m, - MonadHttp m, - MonadIO m, - HasRequestId m, - Log.MonadLogger m, - MonadClient m - ) => + verifyCodeThrow :: Maybe Code.Value -> UserId -> - ExceptT ClientError m () - verifyCode mbCode userId = + ExceptT ClientError (AppT r) () + verifyCodeThrow mbCode userId = -- this only happens inside the login flow (in particular, when logging in from a new device) -- the code obtained for logging in is used a second time for adding the device UserAuth.verifyCode mbCode Code.Login userId `catchE` \case diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index 70ae638cd2..14d8c37796 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -61,9 +61,12 @@ import Wire.API.Error import qualified Wire.API.Error.Brig as E import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) -ensureIsActivated :: Local UserId -> MaybeT (AppT r) () +ensureIsActivated :: + Member UserQuery r => + Local UserId -> + MaybeT (AppT r) () ensureIsActivated lusr = do - active <- lift . wrapClient $ Data.isActivated (tUnqualified lusr) + active <- lift . liftSem $ Data.isActivated (tUnqualified lusr) guard active ensureNotSameTeam :: Local UserId -> Local UserId -> (ConnectionM r) () diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index dccceb7a17..2c18e461c4 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -32,10 +32,11 @@ import Brig.App import qualified Brig.Data.Connection as Data import qualified Brig.Data.User as Data import Brig.IO.Intra (notify) +import Brig.Sem.UserHandleStore +import Brig.Sem.UserQuery import Brig.Types (PrekeyBundle, Relation (Accepted)) import Brig.Types.User.Event import Brig.User.API.Handle -import Brig.User.Search.Index import qualified Brig.User.Search.SearchIndex as Q import Cassandra (MonadClient) import Control.Error.Util @@ -51,6 +52,7 @@ import Data.Range import qualified Gundeck.Types.Push as Push import Imports import Network.Wai.Utilities.Error ((!>>)) +import Polysemy import Servant (ServerT) import Servant.API import qualified System.Logger.Class as Log @@ -71,23 +73,29 @@ import Wire.API.UserMap (UserMap) type FederationAPI = "federation" :> BrigApi -federationSitemap :: ServerT FederationAPI (Handler r) +federationSitemap :: + Members '[UserHandleStore, UserQuery] r => + ServerT FederationAPI (Handler r) federationSitemap = Named @"api-version" (\_ _ -> pure versionInfo) - :<|> Named @"get-user-by-handle" (\d h -> wrapHttpClientE $ getUserByHandle d h) + :<|> Named @"get-user-by-handle" getUserByHandle :<|> Named @"get-users-by-ids" (\d us -> wrapHttpClientE $ getUsersByIds d us) :<|> Named @"claim-prekey" claimPrekey :<|> Named @"claim-prekey-bundle" claimPrekeyBundle :<|> Named @"claim-multi-prekey-bundle" claimMultiPrekeyBundle - :<|> Named @"search-users" (\d sr -> wrapHttpClientE $ searchUsers d sr) + :<|> Named @"search-users" searchUsers :<|> Named @"get-user-clients" getUserClients :<|> Named @"send-connection-action" sendConnectionAction :<|> Named @"on-user-deleted-connections" onUserDeleted :<|> Named @"claim-key-packages" fedClaimKeyPackages -sendConnectionAction :: Domain -> NewConnectionRequest -> Handler r NewConnectionResponse +sendConnectionAction :: + Members '[UserQuery] r => + Domain -> + NewConnectionRequest -> + Handler r NewConnectionResponse sendConnectionAction originDomain NewConnectionRequest {..} = do - active <- lift $ wrapClient $ Data.isActivated ncrTo + active <- lift . liftSem $ Data.isActivated ncrTo if active then do self <- qualifyLocal ncrTo @@ -98,16 +106,10 @@ sendConnectionAction originDomain NewConnectionRequest {..} = do else pure NewConnectionResponseUserNotActivated getUserByHandle :: - ( HasRequestId m, - Log.MonadLogger m, - MonadClient m, - MonadHttp m, - MonadMask m, - MonadReader Env m - ) => + Members '[UserHandleStore] r => Domain -> Handle -> - ExceptT Error m (Maybe UserProfile) + ExceptT Error (AppT r) (Maybe UserProfile) getUserByHandle domain handle = do searchPolicy <- lookupSearchPolicy domain @@ -119,12 +121,12 @@ getUserByHandle domain handle = do if not performHandleLookup then pure Nothing else lift $ do - maybeOwnerId <- API.lookupHandle handle + maybeOwnerId <- liftSem $ API.lookupHandle handle case maybeOwnerId of Nothing -> pure Nothing Just ownerId -> - listToMaybe <$> API.lookupLocalProfiles Nothing [ownerId] + listToMaybe <$> wrapHttpClient (API.lookupLocalProfiles Nothing [ownerId]) getUsersByIds :: ( MonadClient m, @@ -162,18 +164,11 @@ fedClaimKeyPackages domain ckpr = do -- only search by exact handle search, not in elasticsearch. -- (This decision may change in the future) searchUsers :: - forall m. - ( HasRequestId m, - Log.MonadLogger m, - MonadClient m, - MonadHttp m, - MonadIndexIO m, - MonadMask m, - MonadReader Env m - ) => + forall r. + Members '[UserHandleStore] r => Domain -> SearchRequest -> - ExceptT Error m SearchResponse + ExceptT Error (AppT r) SearchResponse searchUsers domain (SearchRequest searchTerm) = do searchPolicy <- lift $ lookupSearchPolicy domain @@ -187,25 +182,25 @@ searchUsers domain (SearchRequest searchTerm) = do contacts <- go [] maxResults searches pure $ SearchResponse contacts searchPolicy where - go :: [Contact] -> Int -> [Int -> ExceptT Error m [Contact]] -> ExceptT Error m [Contact] + go :: [Contact] -> Int -> [Int -> ExceptT Error (AppT r) [Contact]] -> ExceptT Error (AppT r) [Contact] go contacts _ [] = pure contacts go contacts maxResult (search : searches) = do contactsNew <- search maxResult go (contacts <> contactsNew) (maxResult - length contactsNew) searches - fullSearch :: Int -> ExceptT Error m [Contact] + fullSearch :: Int -> ExceptT Error (AppT r) [Contact] fullSearch n | n > 0 = lift $ searchResults <$> Q.searchIndex Q.FederatedSearch searchTerm n | otherwise = pure [] - exactHandleSearch :: Int -> ExceptT Error m [Contact] + exactHandleSearch :: Int -> ExceptT Error (AppT r) [Contact] exactHandleSearch n | n > 0 = do let maybeHandle = parseHandle searchTerm - maybeOwnerId <- maybe (pure Nothing) (lift . API.lookupHandle) maybeHandle + maybeOwnerId <- maybe (pure Nothing) (lift . liftSem . API.lookupHandle) maybeHandle case maybeOwnerId of Nothing -> pure [] - Just foundUser -> lift $ contactFromProfile <$$> API.lookupLocalProfiles Nothing [foundUser] + Just foundUser -> lift $ contactFromProfile <$$> wrapHttpClient (API.lookupLocalProfiles Nothing [foundUser]) | otherwise = pure [] getUserClients :: Domain -> GetUserClients -> (Handler r) (UserMap (Set PubClient)) diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 0dac54cdbf..cc4b759ed4 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -42,9 +42,17 @@ import qualified Brig.Data.User as Data import qualified Brig.IO.Intra as Intra import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Provider.API as Provider +import Brig.Sem.ActivationKeyStore +import Brig.Sem.ActivationSupply +import Brig.Sem.GalleyAccess import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetSupply (PasswordResetSupply) +import Brig.Sem.Twilio +import Brig.Sem.UniqueClaimsStore +import Brig.Sem.UserHandleStore +import Brig.Sem.UserKeyStore import Brig.Sem.UserQuery (UserQuery) +import Brig.Sem.VerificationCodeStore import qualified Brig.Team.API as Team import Brig.Team.DB (lookupInvitationByEmail) import Brig.Types @@ -75,13 +83,18 @@ import Network.Wai.Routing hiding (toList) import Network.Wai.Utilities as Utilities import Network.Wai.Utilities.ZAuth (zauthConnId, zauthUserId) import Polysemy +import Polysemy.Async +import Polysemy.Conc.Effect.Race import qualified Polysemy.Error as P import Polysemy.Input +import Polysemy.Resource +import qualified Polysemy.TinyLog as P +import qualified Ropes.Twilio as Twilio import Servant hiding (Handler, JSON, addHeader, respond) import Servant.Swagger.Internal.Orphans () import Servant.Swagger.UI import qualified System.Logger.Class as Log -import UnliftIO.Async +import UnliftIO.Async hiding (Async) import Wire.API.Error import Wire.API.Error.Brig import qualified Wire.API.Error.Brig as E @@ -99,12 +112,25 @@ import Wire.API.User.RichInfo -- Sitemap (servant) servantSitemap :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ ActivationKeyStore, + ActivationSupply, + Input (Local ()), + P.Error Twilio.ErrorResponse, + PasswordResetStore, + PasswordResetSupply, + Twilio, + UserHandleStore, + UserKeyStore, + UserQuery, + VerificationCodeStore + ] + r => ServerT BrigIRoutes.API (Handler r) servantSitemap = ejpdAPI :<|> accountAPI :<|> mlsAPI :<|> getVerificationCode :<|> teamsAPI ejpdAPI :: - Member UserQuery r => + Members '[UserHandleStore, UserQuery] r => ServerT BrigIRoutes.EJPD_API (Handler r) ejpdAPI = Brig.User.EJPD.ejpdRequest @@ -125,7 +151,18 @@ mlsAPI = :<|> mapKeyPackageRefsInternal accountAPI :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ ActivationKeyStore, + ActivationSupply, + Input (Local ()), + P.Error Twilio.ErrorResponse, + PasswordResetSupply, + PasswordResetStore, + Twilio, + UserKeyStore, + UserQuery + ] + r => ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = Named @"createUserNoVerify" createUserNoVerify @@ -161,7 +198,7 @@ getMLSClients :: Qualified UserId -> SignatureSchemeTag -> Handler r (Set Client getMLSClients qusr ss = do usr <- lift $ tUnqualified <$> ensureLocal qusr results <- lift (wrapClient (API.lookupUsersClientIds (pure usr))) >>= getResult usr - keys <- lift . wrapClient $ pooledMapConcurrentlyN 16 getKey (toList results) + keys <- lift . wrapClient $ pooledMapConcurrentlyN 16 getMLSKey (toList results) pure . Set.fromList . map fst . filter (isJust . snd) $ keys where getResult _ [] = throwStd (errorToWai @'UserNotFound) @@ -169,8 +206,8 @@ getMLSClients qusr ss = do | u == usr = pure cs | otherwise = getResult usr rs - getKey :: MonadClient m => ClientId -> m (ClientId, Maybe LByteString) - getKey cid = (cid,) <$> Data.lookupMLSPublicKey (qUnqualified qusr) cid ss + getMLSKey :: MonadClient m => ClientId -> m (ClientId, Maybe LByteString) + getMLSKey cid = (cid,) <$> Data.lookupMLSPublicKey (qUnqualified qusr) cid ss mapKeyPackageRefsInternal :: KeyPackageBundle -> Handler r () mapKeyPackageRefsInternal bundle = do @@ -179,7 +216,8 @@ mapKeyPackageRefsInternal bundle = do Data.mapKeyPackageRef (kpbeRef e) (kpbeUser e) (kpbeClient e) getVerificationCode :: - Member UserQuery r => + forall r. + Members '[VerificationCodeStore, UserQuery] r => UserId -> VerificationAction -> (Handler r) (Maybe Code.Value) @@ -189,10 +227,10 @@ getVerificationCode uid action = do user <- lift . liftSem $ Api.lookupUser loc locale NoPendingInvitations uid maybe (pure Nothing) (lookupCode action) (userEmail =<< user) where - lookupCode :: VerificationAction -> Email -> (Handler r) (Maybe Code.Value) + lookupCode :: VerificationAction -> Email -> Handler r (Maybe Code.Value) lookupCode a e = do key <- Code.mkKey (Code.ForEmail e) - code <- wrapClientE $ Code.lookup key (Code.scopeFromAction a) + code <- lift . liftSem $ Code.getPendingCode key (Code.scopeFromAction a) pure $ Code.codeValue <$> code swaggerDocsAPI :: Servant.Server BrigIRoutes.SwaggerDocsAPI @@ -203,11 +241,24 @@ swaggerDocsAPI = swaggerSchemaUIServer BrigIRoutes.swaggerDoc sitemap :: Members - '[ Input (Local ()), + '[ ActivationKeyStore, + ActivationSupply, + Async, + GalleyAccess, + Input (Local ()), P.Error ReAuthError, + P.Error Twilio.ErrorResponse, + P.TinyLog, PasswordResetStore, PasswordResetSupply, - UserQuery + Race, + Resource, + Twilio, + UniqueClaimsStore, + UserHandleStore, + UserKeyStore, + UserQuery, + VerificationCodeStore ] r => Routes a (Handler r) () @@ -374,7 +425,13 @@ sitemap = do -- | Add a client without authentication checks addClientInternalH :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ GalleyAccess, + Input (Local ()), + UserQuery, + VerificationCodeStore + ] + r => UserId ::: Maybe Bool ::: JsonRequest NewClient ::: Maybe ConnId ::: JSON -> Handler r Response addClientInternalH (usr ::: mSkipReAuth ::: req ::: connId ::: _) = do @@ -382,7 +439,13 @@ addClientInternalH (usr ::: mSkipReAuth ::: req ::: connId ::: _) = do setStatus status201 . json <$> addClientInternal usr mSkipReAuth new connId addClientInternal :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ GalleyAccess, + Input (Local ()), + UserQuery, + VerificationCodeStore + ] + r => UserId -> Maybe Bool -> NewClient -> @@ -423,7 +486,18 @@ internalListFullClients (UserSet usrs) = UserClientsFull <$> wrapClient (Data.lookupClientsBulk (Set.toList usrs)) createUserNoVerify :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ ActivationKeyStore, + ActivationSupply, + P.Error Twilio.ErrorResponse, + Input (Local ()), + PasswordResetSupply, + PasswordResetStore, + Twilio, + UserKeyStore, + UserQuery + ] + r => NewUser -> Handler r (Either RegisterError SelfProfile) createUserNoVerify uData = lift . runExceptT $ do @@ -458,7 +532,13 @@ deleteUserNoVerify uid = do lift $ API.deleteUserNoVerify uid changeSelfEmailMaybeSendH :: - Member UserQuery r => + Members + '[ ActivationKeyStore, + ActivationSupply, + UserKeyStore, + UserQuery + ] + r => UserId ::: Bool ::: JsonRequest EmailUpdate -> (Handler r) Response changeSelfEmailMaybeSendH (u ::: validate ::: req) = do @@ -470,7 +550,13 @@ changeSelfEmailMaybeSendH (u ::: validate ::: req) = do data MaybeSendEmail = ActuallySendEmail | DoNotSendEmail changeSelfEmailMaybeSend :: - Member UserQuery r => + Members + '[ ActivationKeyStore, + ActivationSupply, + UserKeyStore, + UserQuery + ] + r => UserId -> MaybeSendEmail -> Email -> @@ -484,7 +570,12 @@ changeSelfEmailMaybeSend u DoNotSendEmail email allowScim = do ChangeEmailNeedsActivation _ -> pure ChangeEmailResponseNeedsActivation listActivatedAccountsH :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ Input (Local ()), + UserHandleStore, + UserQuery + ] + r => JSON ::: Either (List UserId) (List Handle) ::: Bool -> Handler r Response listActivatedAccountsH (_ ::: qry ::: includePendingInvitations) = do @@ -492,7 +583,12 @@ listActivatedAccountsH (_ ::: qry ::: includePendingInvitations) = do listActivatedAccounts :: forall r. - Members '[Input (Local ()), UserQuery] r => + Members + '[ Input (Local ()), + UserHandleStore, + UserQuery + ] + r => Either (List UserId) (List Handle) -> Bool -> AppT r [UserAccount] @@ -501,7 +597,7 @@ listActivatedAccounts elh includePendingInvitations = do case elh of Left us -> byIds (fromList us) Right hs -> do - us <- mapM (wrapClient . API.lookupHandle) (fromList hs) + us <- mapM (liftSem . API.lookupHandle) (fromList hs) byIds (catMaybes us) where byIds :: [UserId] -> (AppT r) [UserAccount] @@ -529,7 +625,7 @@ listActivatedAccounts elh includePendingInvitations = do (Ephemeral, _, _) -> pure True listAccountsByIdentityH :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserKeyStore, UserQuery] r => JSON ::: Either Email Phone ::: Bool -> Handler r Response listAccountsByIdentityH (_ ::: emailOrPhone ::: includePendingInvitations) = @@ -552,14 +648,24 @@ instance ToJSON GetActivationCodeResp where toJSON (GetActivationCodeResp (k, c)) = object ["key" .= k, "code" .= c] getPasswordResetCodeH :: - Members '[PasswordResetStore, PasswordResetSupply] r => + Members + '[ PasswordResetStore, + PasswordResetSupply, + UserKeyStore + ] + r => JSON ::: Either Email Phone -> (Handler r) Response getPasswordResetCodeH (_ ::: emailOrPhone) = do maybe (throwStd invalidPwResetKey) (pure . json) =<< lift (getPasswordResetCode emailOrPhone) getPasswordResetCode :: - Members '[PasswordResetStore, PasswordResetSupply] r => + Members + '[ PasswordResetStore, + PasswordResetSupply, + UserKeyStore + ] + r => Either Email Phone -> (AppT r) (Maybe GetPasswordResetCodeResp) getPasswordResetCode emailOrPhone = @@ -608,7 +714,7 @@ getConnectionsStatus (ConnectionsStatusRequestV2 froms mtos mrel) = do filterByRelation l rel = filter ((== rel) . csv2Status) l revokeIdentityH :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserKeyStore, UserQuery] r => Either Email Phone -> Handler r Response revokeIdentityH emailOrPhone = do @@ -712,16 +818,32 @@ getRichInfoMulti uids = lift (wrapClient $ API.lookupRichInfoMultiUsers uids) updateHandleH :: - Member UserQuery r => + Members + '[ Async, + Race, + Resource, + UniqueClaimsStore, + UserHandleStore, + UserQuery + ] + r => UserId ::: JSON ::: JsonRequest HandleUpdate -> (Handler r) Response updateHandleH (uid ::: _ ::: body) = empty <$ (updateHandle uid =<< parseJsonBody body) updateHandle :: - Member UserQuery r => + Members + '[ Async, + Race, + Resource, + UniqueClaimsStore, + UserHandleStore, + UserQuery + ] + r => UserId -> HandleUpdate -> - (Handler r) () + Handler r () updateHandle uid (HandleUpdate handleUpd) = do handle <- validateHandle handleUpd API.changeHandle uid Nothing handle API.AllowSCIMUpdates !>> changeHandleError @@ -752,7 +874,10 @@ updateUserName uid (NameUpdate nameUpd) = do Just _ -> API.updateUser uid Nothing uu API.AllowSCIMUpdates !>> updateProfileError Nothing -> throwStd (errorToWai @'E.InvalidUser) -checkHandleInternalH :: Text -> (Handler r) Response +checkHandleInternalH :: + Members '[UserHandleStore] r => + Text -> + Handler r Response checkHandleInternalH = API.checkHandle >=> \case API.CheckHandleInvalid -> throwE (StdError (errorToWai @'E.InvalidHandle)) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index bf6503291a..7c52232643 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -46,9 +46,18 @@ import qualified Brig.Data.UserKey as UserKey import qualified Brig.IO.Intra as Intra import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Provider.API as Provider +import Brig.Sem.ActivationKeyStore +import Brig.Sem.ActivationSupply +import Brig.Sem.BudgetStore +import Brig.Sem.GalleyAccess import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetSupply (PasswordResetSupply) +import Brig.Sem.Twilio +import Brig.Sem.UniqueClaimsStore +import Brig.Sem.UserHandleStore +import Brig.Sem.UserKeyStore import Brig.Sem.UserQuery (UserQuery) +import Brig.Sem.VerificationCodeStore import qualified Brig.Team.API as Team import qualified Brig.Team.Email as Team import Brig.Types.Activation (ActivationPair) @@ -99,9 +108,13 @@ import Network.Wai.Utilities.Swagger (document, mkSwaggerApi) import qualified Network.Wai.Utilities.Swagger as Doc import Network.Wai.Utilities.ZAuth (zauthUserId) import Polysemy +import Polysemy.Async +import Polysemy.Conc.Effect.Race import qualified Polysemy.Error as P import Polysemy.Input +import Polysemy.Resource import qualified Polysemy.TinyLog as P +import qualified Ropes.Twilio as Twilio import Servant hiding (Handler, JSON, addHeader, respond) import qualified Servant import Servant.Swagger.Internal.Orphans () @@ -185,7 +198,26 @@ swaggerDocsAPI Nothing = swaggerDocsAPI (Just maxBound) servantSitemap :: forall r. - Members '[P.Error ReAuthError, Input (Local ()), UserQuery] r => + Members + '[ ActivationKeyStore, + ActivationSupply, + Async, + GalleyAccess, + Input (Local ()), + P.Error ReAuthError, + P.Error Twilio.ErrorResponse, + PasswordResetStore, + PasswordResetSupply, + Race, + Resource, + Twilio, + UniqueClaimsStore, + UserHandleStore, + UserKeyStore, + UserQuery, + VerificationCodeStore + ] + r => ServerT BrigAPI (Handler r) servantSitemap = userAPI @@ -291,11 +323,21 @@ servantSitemap = sitemap :: Members - '[ Input (Local ()), + '[ ActivationKeyStore, + ActivationSupply, + BudgetStore, + GalleyAccess, + Input (Local ()), + P.Error Twilio.ErrorResponse, P.TinyLog, PasswordResetStore, PasswordResetSupply, - UserQuery + Twilio, + UniqueClaimsStore, + UserHandleStore, + UserKeyStore, + UserQuery, + VerificationCodeStore ] r => Routes Doc.ApiBuilder (Handler r) () @@ -462,11 +504,21 @@ sitemap = do apiDocs :: forall r. Members - '[ Input (Local ()), + '[ ActivationKeyStore, + ActivationSupply, + BudgetStore, + GalleyAccess, + Input (Local ()), + P.Error Twilio.ErrorResponse, P.TinyLog, PasswordResetStore, PasswordResetSupply, - UserQuery + Twilio, + UniqueClaimsStore, + UserHandleStore, + UserKeyStore, + UserQuery, + VerificationCodeStore ] r => Routes Doc.ApiBuilder (Handler r) () @@ -492,8 +544,8 @@ setProperty u c key raw = do checkPropertyKey :: Public.PropertyKey -> Handler r () checkPropertyKey k = do maxKeyLen <- fromMaybe defMaxKeyLen <$> view (settings . propertyMaxKeyLen) - let keyText = Ascii.toText (Public.propertyKeyName k) - when (Text.compareLength keyText (fromIntegral maxKeyLen) == GT) $ + let keyTxt = Ascii.toText (Public.propertyKeyName k) + when (Text.compareLength keyTxt (fromIntegral maxKeyLen) == GT) $ throwStd propertyKeyTooLarge -- | Parse a 'PropertyValue' from a bytestring. This is different from 'FromJSON' in that @@ -577,7 +629,13 @@ getMultiUserPrekeyBundleH zusr qualUserClients = do API.claimMultiPrekeyBundles (ProtectedUser zusr) qualUserClients !>> clientError addClient :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ GalleyAccess, + Input (Local ()), + UserQuery, + VerificationCodeStore + ] + r => UserId -> ConnId -> Maybe IpAddr -> @@ -678,7 +736,18 @@ getClientPrekeys usr clt = lift (wrapClient $ API.lookupPrekeyIds usr clt) -- | docs/reference/user/registration.md {#RefRegistration} createUser :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ ActivationKeyStore, + ActivationSupply, + Input (Local ()), + P.Error Twilio.ErrorResponse, + PasswordResetStore, + PasswordResetSupply, + Twilio, + UserKeyStore, + UserQuery + ] + r => Public.NewUserPublic -> Handler r (Either Public.RegisterError Public.RegisterSuccess) createUser (Public.NewUserPublic new) = lift . runExceptT $ do @@ -768,7 +837,12 @@ getUser self qualifiedUserId = do wrapHttpClientE $ API.lookupProfile lself qualifiedUserId !>> fedError -- FUTUREWORK: Make servant understand that at least one of these is required -listUsersByUnqualifiedIdsOrHandles :: UserId -> Maybe (CommaSeparatedList UserId) -> Maybe (Range 1 4 (CommaSeparatedList Handle)) -> (Handler r) [Public.UserProfile] +listUsersByUnqualifiedIdsOrHandles :: + Members '[UserHandleStore] r => + UserId -> + Maybe (CommaSeparatedList UserId) -> + Maybe (Range 1 4 (CommaSeparatedList Handle)) -> + Handler r [Public.UserProfile] listUsersByUnqualifiedIdsOrHandles self mUids mHandles = do domain <- viewFederationDomain case (mUids, mHandles) of @@ -784,7 +858,12 @@ listUsersByUnqualifiedIdsOrHandles self mUids mHandles = do in listUsersByIdsOrHandles self (Public.ListUsersByHandles qualifiedRangedList) (Nothing, Nothing) -> throwStd $ badRequest "at least one ids or handles must be provided" -listUsersByIdsOrHandles :: UserId -> Public.ListUsersQuery -> (Handler r) [Public.UserProfile] +listUsersByIdsOrHandles :: + forall r. + Members '[UserHandleStore] r => + UserId -> + Public.ListUsersQuery -> + Handler r [Public.UserProfile] listUsersByIdsOrHandles self q = do lself <- qualifyLocal self foundUsers <- case q of @@ -800,7 +879,7 @@ listUsersByIdsOrHandles self q = do where getIds :: [Handle] -> (Handler r) [Qualified UserId] getIds localHandles = do - localUsers <- catMaybes <$> traverse (lift . wrapClient . API.lookupHandle) localHandles + localUsers <- catMaybes <$> traverse (lift . liftSem . API.lookupHandle) localHandles domain <- viewFederationDomain pure $ map (`Qualified` domain) localUsers byIds :: Local UserId -> [Qualified UserId] -> (Handler r) [Public.UserProfile] @@ -823,7 +902,15 @@ updateUser uid conn uu = do pure $ either Just (const Nothing) eithErr changePhone :: - Member UserQuery r => + Members + '[ ActivationKeyStore, + ActivationSupply, + P.Error Twilio.ErrorResponse, + Twilio, + UserKeyStore, + UserQuery + ] + r => UserId -> ConnId -> Public.PhoneUpdate -> @@ -836,7 +923,7 @@ changePhone u _ (Public.puPhone -> phone) = lift . exceptTToMaybe $ do lift . wrapClient $ sendActivationSms pn apair loc removePhone :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserKeyStore, UserQuery] r => UserId -> ConnId -> Handler r (Maybe Public.RemoveIdentityError) @@ -844,7 +931,7 @@ removePhone self conn = lift . exceptTToMaybe $ API.removePhone self conn removeEmail :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserKeyStore, UserQuery] r => UserId -> ConnId -> Handler r (Maybe Public.RemoveIdentityError) @@ -854,7 +941,11 @@ removeEmail self conn = checkPasswordExists :: UserId -> (Handler r) Bool checkPasswordExists = fmap isJust . lift . wrapClient . API.lookupPassword -changePassword :: UserId -> Public.PasswordChange -> (Handler r) (Maybe Public.ChangePasswordError) +changePassword :: + Members '[UserQuery] r => + UserId -> + Public.PasswordChange -> + Handler r (Maybe Public.ChangePasswordError) changePassword u cp = lift . exceptTToMaybe $ API.changePassword u cp changeLocale :: UserId -> ConnId -> Public.LocaleUpdate -> (Handler r) () @@ -862,32 +953,50 @@ changeLocale u conn l = lift $ API.changeLocale u conn l -- | (zusr is ignored by this handler, ie. checking handles is allowed as long as you have -- *any* account.) -checkHandleH :: UserId ::: Text -> (Handler r) Response +checkHandleH :: + Members '[UserHandleStore] r => + UserId ::: Text -> + Handler r Response checkHandleH (_uid ::: hndl) = API.checkHandle hndl >>= \case API.CheckHandleInvalid -> throwE (StdError (errorToWai @'E.InvalidHandle)) API.CheckHandleFound -> pure $ setStatus status200 empty API.CheckHandleNotFound -> pure $ setStatus status404 empty -checkHandlesH :: JSON ::: UserId ::: JsonRequest Public.CheckHandles -> (Handler r) Response +checkHandlesH :: + Members '[UserHandleStore] r => + JSON ::: UserId ::: JsonRequest Public.CheckHandles -> + Handler r Response checkHandlesH (_ ::: _ ::: req) = do Public.CheckHandles hs num <- parseJsonBody req let handles = mapMaybe parseHandle (fromRange hs) - free <- lift . wrapClient $ API.checkHandles handles (fromRange num) + free <- lift . liftSem $ API.checkHandles handles (fromRange num) pure $ json (free :: [Handle]) -- | This endpoint returns UserHandleInfo instead of UserProfile for backwards -- compatibility, whereas the corresponding qualified endpoint (implemented by -- 'Handle.getHandleInfo') returns UserProfile to reduce traffic between backends -- in a federated scenario. -getHandleInfoUnqualifiedH :: UserId -> Handle -> (Handler r) (Maybe Public.UserHandleInfo) +getHandleInfoUnqualifiedH :: + Members '[UserHandleStore] r => + UserId -> + Handle -> + Handler r (Maybe Public.UserHandleInfo) getHandleInfoUnqualifiedH self handle = do domain <- viewFederationDomain Public.UserHandleInfo . Public.profileQualifiedId <$$> Handle.getHandleInfo self (Qualified handle domain) changeHandle :: - Member UserQuery r => + Members + '[ Async, + Race, + Resource, + UniqueClaimsStore, + UserHandleStore, + UserQuery + ] + r => UserId -> ConnId -> Public.HandleUpdate -> @@ -900,6 +1009,7 @@ beginPasswordResetH :: Members '[ P.TinyLog, PasswordResetStore, + UserKeyStore, UserQuery ] r => @@ -912,6 +1022,7 @@ beginPasswordReset :: Members '[ P.TinyLog, PasswordResetStore, + UserKeyStore, UserQuery ] r => @@ -927,7 +1038,12 @@ beginPasswordReset (Public.NewPasswordReset target) = do Right phone -> wrapClient $ sendPasswordResetSms phone pair loc completePasswordResetH :: - Members '[PasswordResetStore, PasswordResetSupply] r => + Members + '[ PasswordResetStore, + PasswordResetSupply, + UserKeyStore + ] + r => JSON ::: JsonRequest Public.CompletePasswordReset -> (Handler r) Response completePasswordResetH (_ ::: req) = do @@ -936,7 +1052,15 @@ completePasswordResetH (_ ::: req) = do pure empty sendActivationCodeH :: - Member UserQuery r => + Members + '[ ActivationKeyStore, + ActivationSupply, + P.Error Twilio.ErrorResponse, + Twilio, + UserKeyStore, + UserQuery + ] + r => JsonRequest Public.SendActivationCode -> (Handler r) Response sendActivationCodeH req = @@ -945,7 +1069,15 @@ sendActivationCodeH req = -- docs/reference/user/activation.md {#RefActivationRequest} -- docs/reference/user/registration.md {#RefRegistration} sendActivationCode :: - Member UserQuery r => + Members + '[ ActivationKeyStore, + ActivationSupply, + P.Error Twilio.ErrorResponse, + Twilio, + UserKeyStore, + UserQuery + ] + r => Public.SendActivationCode -> (Handler r) () sendActivationCode Public.SendActivationCode {..} = do @@ -1072,7 +1204,14 @@ getConnection self other = do lift . wrapClient $ Data.lookupConnection lself other deleteSelfUser :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ Input (Local ()), + UniqueClaimsStore, + UserHandleStore, + UserQuery, + VerificationCodeStore + ] + r => UserId -> Public.DeleteUser -> Handler r (Maybe Code.Timeout) @@ -1080,7 +1219,14 @@ deleteSelfUser u body = API.deleteSelfUser u (Public.deleteUserPassword body) !>> deleteUserError verifyDeleteUserH :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ Input (Local ()), + UniqueClaimsStore, + UserHandleStore, + UserQuery, + VerificationCodeStore + ] + r => JsonRequest Public.VerifyDeleteUser ::: JSON -> Handler r Response verifyDeleteUserH (r ::: _) = do @@ -1089,7 +1235,13 @@ verifyDeleteUserH (r ::: _) = do pure (setStatus status200 empty) updateUserEmail :: - Member UserQuery r => + Members + '[ ActivationKeyStore, + ActivationSupply, + UserKeyStore, + UserQuery + ] + r => UserId -> UserId -> Public.EmailUpdate -> @@ -1130,20 +1282,56 @@ respFromActivationRespWithStatus = \case ActivationRespSuccessNoIdent -> empty -- docs/reference/user/activation.md {#RefActivationSubmit} -activateKeyH :: JSON ::: JsonRequest Public.Activate -> (Handler r) Response +activateKeyH :: + Members + '[ ActivationKeyStore, + ActivationSupply, + Input (Local ()), + PasswordResetSupply, + PasswordResetStore, + UserKeyStore, + UserQuery + ] + r => + JSON ::: JsonRequest Public.Activate -> + Handler r Response activateKeyH (_ ::: req) = do activationRequest <- parseJsonBody req respFromActivationRespWithStatus <$> activate activationRequest -activateH :: Public.ActivationKey ::: Public.ActivationCode -> (Handler r) Response +activateH :: + Members + '[ ActivationKeyStore, + ActivationSupply, + Input (Local ()), + PasswordResetSupply, + PasswordResetStore, + UserKeyStore, + UserQuery + ] + r => + Public.ActivationKey ::: Public.ActivationCode -> + Handler r Response activateH (k ::: c) = do let activationRequest = Public.Activate (Public.ActivateKey k) c False respFromActivationRespWithStatus <$> activate activationRequest -activate :: Public.Activate -> (Handler r) ActivationRespWithStatus +activate :: + Members + '[ ActivationKeyStore, + ActivationSupply, + Input (Local ()), + PasswordResetSupply, + PasswordResetStore, + UserKeyStore, + UserQuery + ] + r => + Public.Activate -> + Handler r ActivationRespWithStatus activate (Public.Activate tgt code dryrun) | dryrun = do - wrapClientE (API.preverify tgt code) !>> actError + liftSemE (API.preverify tgt code) !>> actError pure ActivationRespDryRun | otherwise = do result <- API.activate tgt code Nothing !>> actError @@ -1156,7 +1344,13 @@ activate (Public.Activate tgt code dryrun) sendVerificationCode :: forall r. - Members '[Input (Local ()), UserQuery] r => + Members + '[ Input (Local ()), + UserKeyStore, + UserQuery, + VerificationCodeStore + ] + r => Public.SendVerificationCode -> Handler r () sendVerificationCode req = do @@ -1167,22 +1361,22 @@ sendVerificationCode req = do case (mbAccount, featureEnabled) of (Just account, True) -> do gen <- Code.mk6DigitGen $ Code.ForEmail email - timeout <- setVerificationTimeout <$> view settings + tout <- setVerificationTimeout <$> view settings code <- Code.generate gen (Code.scopeFromAction action) (Code.Retries 3) - timeout + tout (Just $ toUUID $ Public.userId $ accountUser account) - wrapClientE $ Code.insert code + lift . liftSem $ Code.insertCode code sendMail email (Code.codeValue code) (Just $ Public.userLocale $ accountUser account) action _ -> pure () where - getAccount :: Public.Email -> (Handler r) (Maybe UserAccount) + getAccount :: Public.Email -> Handler r (Maybe UserAccount) getAccount email = lift $ do locale <- setDefaultUserLocale <$> view settings - mbUserId <- wrapClient . UserKey.lookupKey $ UserKey.userEmailKey email + mbUserId <- liftSem . UserKey.getKey $ UserKey.userEmailKey email join <$> liftSem (Data.lookupAccount locale `traverse` mbUserId) sendMail :: Public.Email -> Code.Value -> Maybe Public.Locale -> Public.VerificationAction -> (Handler r) () @@ -1212,7 +1406,12 @@ instance ToJSON DeprecatedMatchingResult where ] deprecatedCompletePasswordResetH :: - Members '[PasswordResetStore, PasswordResetSupply] r => + Members + '[ PasswordResetStore, + PasswordResetSupply, + UserKeyStore + ] + r => JSON ::: Public.PasswordResetKey ::: JsonRequest Public.PasswordReset -> (Handler r) Response deprecatedCompletePasswordResetH (_ ::: k ::: req) = do diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 676f1eeb87..9c5c97b79b 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -35,7 +35,6 @@ import Brig.Data.Activation (Activation (..), ActivationError (..)) import Brig.Data.Client (ClientDataError (..)) import Brig.Data.Properties (PropertiesDataError (..)) import Brig.Data.User (AuthError (..), ReAuthError (..)) -import Brig.Data.UserKey (UserKey, foldKey) import Brig.Types import Brig.Types.Code (Timeout) import Brig.Types.Intra diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 98a3e29e5a..5f24b9e54d 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -120,9 +120,13 @@ import Brig.Sem.PasswordResetStore (PasswordResetStore) import qualified Brig.Sem.PasswordResetStore as E import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import qualified Brig.Sem.PasswordResetSupply as E +import Brig.Sem.Twilio (Twilio) +import Brig.Sem.UniqueClaimsStore +import Brig.Sem.UserHandleStore import Brig.Sem.UserKeyStore (UserKeyStore) import Brig.Sem.UserQuery (UserQuery) import Brig.Sem.UserQuery.Cassandra +import Brig.Sem.VerificationCodeStore import qualified Brig.Team.DB as Team import Brig.Types import Brig.Types.Code (Timeout (..)) @@ -160,12 +164,17 @@ import qualified Galley.Types.Teams.Intra as Team import Imports import Network.Wai.Utilities import Polysemy +import Polysemy.Async +import Polysemy.Conc.Effect.Race (Race) +import qualified Polysemy.Error as P import Polysemy.Input +import Polysemy.Resource hiding (onException) import qualified Polysemy.TinyLog as P +import qualified Ropes.Twilio as Twilio import System.Logger.Class (MonadLogger) import qualified System.Logger.Class as Log import System.Logger.Message -import UnliftIO.Async +import UnliftIO.Async hiding (Async) import Wire.API.Error import qualified Wire.API.Error.Brig as E import Wire.API.Federation.Error @@ -219,9 +228,11 @@ createUser :: Members '[ ActivationKeyStore, ActivationSupply, + P.Error Twilio.ErrorResponse, Input (Local ()), PasswordResetSupply, PasswordResetStore, + Twilio, UserKeyStore, UserQuery ] @@ -280,7 +291,7 @@ createUser new = do Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.createUser") Log.info $ field "user" (toByteString uid) . msg (val "Creating user") - wrapClient $ Data.insertAccount account Nothing pw False + liftSem $ Data.insertAccount account Nothing pw False wrapHttp $ Intra.createSelfConv uid wrapHttpClient $ Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) @@ -327,7 +338,9 @@ createUser new = do where -- NOTE: all functions in the where block don't use any arguments of createUser - validateEmailAndPhone :: NewUser -> ExceptT RegisterError (AppT r) (Maybe Email, Maybe Phone) + validateEmailAndPhone :: + NewUser -> + ExceptT RegisterError (AppT r) (Maybe Email, Maybe Phone) validateEmailAndPhone newUser = do -- Validate e-mail email <- for (newUserEmail newUser) $ \e -> @@ -336,12 +349,14 @@ createUser new = do pure (validateEmail e) + c <- view twilioCreds + m <- view httpManager -- Validate phone phone <- for (newUserPhone newUser) $ \p -> maybe (throwE RegisterErrorInvalidPhone) pure - =<< lift (wrapClient $ validatePhone p) + =<< lift (liftSem $ validatePhone c m p) for_ (catMaybes [userEmailKey <$> email, userPhoneKey <$> phone]) $ \k -> verifyUniquenessAndCheckBlacklist k !>> identityErrorToRegisterError @@ -489,7 +504,7 @@ createUserInviteViaScim uid (NewUserScimInvitation tid loc name rawEmail) = do -- would not produce an identity, and so we won't have the email address to construct -- the SCIM user. True - lift . wrapClient $ Data.insertAccount account Nothing Nothing activated + lift . liftSem $ Data.insertAccount account Nothing Nothing activated pure account @@ -550,7 +565,15 @@ changeManagedBy uid conn (ManagedByUpdate mb) = do -- Change Handle changeHandle :: - Member UserQuery r => + Members + '[ Async, + Race, + Resource, + UniqueClaimsStore, + UserHandleStore, + UserQuery + ] + r => UserId -> Maybe ConnId -> Handle -> @@ -576,7 +599,7 @@ changeHandle uid mconn hdl allowScim = do claim u = do unless (isJust (userIdentity u)) $ throwE ChangeHandleNoIdentity - claimed <- lift . wrapClient $ claimHandle (userId u) (userHandle u) hdl + claimed <- lift . liftSem $ claimHandle (userId u) (userHandle u) hdl unless claimed $ throwE ChangeHandleExists lift $ wrapHttpClient $ Intra.onUserEvent uid mconn (handleUpdated uid hdl) @@ -589,10 +612,13 @@ data CheckHandleResp | CheckHandleFound | CheckHandleNotFound -checkHandle :: Text -> API.Handler r CheckHandleResp +checkHandle :: + Members '[UserHandleStore] r => + Text -> + API.Handler r CheckHandleResp checkHandle uhandle = do xhandle <- validateHandle uhandle - owner <- lift . wrapClient $ lookupHandle xhandle + owner <- lift . liftSem $ lookupHandle xhandle if | isJust owner -> -- Handle is taken (=> getHandleInfo will return 200) @@ -611,7 +637,11 @@ checkHandle uhandle = do -------------------------------------------------------------------------------- -- Check Handles -checkHandles :: MonadClient m => [Handle] -> Word -> m [Handle] +checkHandles :: + Member UserHandleStore r => + [Handle] -> + Word -> + Sem r [Handle] checkHandles check num = reverse <$> collectFree [] check num where collectFree free _ 0 = pure free @@ -712,6 +742,8 @@ changePhone :: Members '[ ActivationKeyStore, ActivationSupply, + P.Error Twilio.ErrorResponse, + Twilio, UserKeyStore, UserQuery ] @@ -720,11 +752,13 @@ changePhone :: Phone -> ExceptT ChangePhoneError (AppT r) (Activation, Phone) changePhone u phone = do + c <- view twilioCreds + m <- view httpManager canonical <- maybe (throwE InvalidNewPhone) pure - =<< lift (wrapClient $ validatePhone phone) + =<< lift (liftSem $ validatePhone c m phone) let pk = userPhoneKey canonical available <- lift . liftSem $ Data.keyAvailable pk (Just u) unless available $ @@ -872,7 +906,11 @@ changeSingleAccountStatus uid status = do Data.updateStatus uid status Intra.onUserEvent uid Nothing (ev uid) -mkUserEvent :: (MonadUnliftIO m, Traversable t, MonadClient m) => t UserId -> AccountStatus -> ExceptT AccountStatusError m (UserId -> UserEvent) +mkUserEvent :: + (MonadUnliftIO m, Traversable t, MonadClient m) => + t UserId -> + AccountStatus -> + ExceptT AccountStatusError m (UserId -> UserEvent) mkUserEvent usrs status = case status of Active -> pure UserResumed @@ -972,7 +1010,15 @@ onActivated (PhoneActivated uid phone) = do -- docs/reference/user/activation.md {#RefActivationRequest} sendActivationCode :: - Members '[ActivationKeyStore, ActivationSupply, UserKeyStore, UserQuery] r => + Members + '[ ActivationKeyStore, + ActivationSupply, + P.Error Twilio.ErrorResponse, + Twilio, + UserKeyStore, + UserQuery + ] + r => Either Email Phone -> Maybe Locale -> Bool -> @@ -1000,11 +1046,13 @@ sendActivationCode emailOrPhone loc call = do Just (Just uid, c) -> sendActivationEmail timeout ek c uid -- User re-requesting activation Right phone -> do -- validatePhone returns the canonical E.164 phone number format + creds <- view twilioCreds + m <- view httpManager canonical <- maybe (throwE $ InvalidRecipient (userPhoneKey phone)) pure - =<< lift (wrapClient $ validatePhone phone) + =<< lift (liftSem $ validatePhone creds m phone) let pk = userPhoneKey canonical exists <- lift $ isJust <$> liftSem (Data.getKey pk) when exists $ @@ -1180,7 +1228,14 @@ mkPasswordResetKey ident = case ident of -- -- TODO: communicate deletions of SSO users to SSO service. deleteSelfUser :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ Input (Local ()), + UniqueClaimsStore, + UserHandleStore, + UserQuery, + VerificationCodeStore + ] + r => UserId -> Maybe PlainTextPassword -> ExceptT DeleteUserError (AppT r) (Maybe Timeout) @@ -1215,7 +1270,7 @@ deleteSelfUser uid pwd = do Just emailOrPhone -> sendCode a emailOrPhone Nothing -> case pwd of Just _ -> throwE DeleteUserMissingPassword - Nothing -> lift $ wrapHttpClient $ deleteAccount a >> pure Nothing + Nothing -> lift $ deleteAccount a >> pure Nothing byPassword a pw = do lift . Log.info $ field "user" (toByteString uid) @@ -1226,10 +1281,10 @@ deleteSelfUser uid pwd = do Just p -> do unless (verifyPassword pw p) $ throwE DeleteUserInvalidPassword - lift $ wrapHttpClient $ deleteAccount a >> pure Nothing + lift $ deleteAccount a >> pure Nothing sendCode a target = do gen <- Code.mkGen (either Code.ForEmail Code.ForPhone target) - pending <- lift . wrapClient $ Code.lookup (Code.genKey gen) Code.AccountDeletion + pending <- lift . liftSem $ Code.getPendingCode (Code.genKey gen) Code.AccountDeletion case pending of Just c -> throwE $! DeleteUserPendingCode (Code.codeTTL c) Nothing -> do @@ -1243,7 +1298,7 @@ deleteSelfUser uid pwd = do (Code.Retries 3) (Code.Timeout 600) (Just (toUUID uid)) - wrapClientE $ Code.insert c + lift . liftSem $ Code.insertCode c let k = Code.codeKey c let v = Code.codeValue c let l = userLocale (accountUser a) @@ -1258,17 +1313,24 @@ deleteSelfUser uid pwd = do -- | Conclude validation and scheduling of user's deletion request that was initiated in -- 'deleteUser'. Called via @post /delete@. verifyDeleteUser :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ Input (Local ()), + UniqueClaimsStore, + UserHandleStore, + UserQuery, + VerificationCodeStore + ] + r => VerifyDeleteUser -> ExceptT DeleteUserError (AppT r) () verifyDeleteUser d = do let key = verifyDeleteUserKey d let code = verifyDeleteUserCode d - c <- lift . wrapClient $ Code.verify key Code.AccountDeletion code + c <- lift . liftSem $ Code.verifyCode key Code.AccountDeletion code a <- maybe (throwE DeleteUserInvalidCode) pure (Code.codeAccount =<< c) locale <- setDefaultUserLocale <$> view settings account <- lift . liftSem $ Data.lookupAccount locale (Id a) - for_ account $ lift . wrapHttpClient . deleteAccount + for_ account $ lift . deleteAccount lift . wrapClient $ Code.delete key Code.AccountDeletion -- | Internal deletion without validation. Called via @delete /i/user/:uid@, or indirectly @@ -1276,40 +1338,48 @@ verifyDeleteUser d = do -- Team owners can be deleted if the team is not orphaned, i.e. there is at least one -- other owner left. deleteAccount :: - ( MonadLogger m, - MonadCatch m, - MonadThrow m, - MonadIndexIO m, - MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m, - MonadClient m - ) => + forall r. + Members + '[ UniqueClaimsStore, + UserHandleStore, + UserQuery + ] + r => + -- ( MonadLogger m, + -- MonadCatch m, + -- -- MonadThrow m, + -- MonadIndexIO m, + -- MonadReader Env m, + -- MonadIO m, + -- MonadMask m, + -- MonadHttp m, + -- HasRequestId m, + -- -- MonadUnliftIO m, + -- MonadClient m + -- ) => UserAccount -> - m () + AppT r () deleteAccount account@(accountUser -> user) = do let uid = userId user Log.info $ field "user" (toByteString uid) . msg (val "Deleting account") -- Free unique keys - d <- view digestSHA256 - for_ (userEmail user) $ deleteKey d . userEmailKey - for_ (userPhone user) $ deleteKey d . userPhoneKey - for_ (userHandle user) $ freeHandle (userId user) + -- d <- view digestSHA256 + -- TODO: Bring these two for loops back + -- for_ (userEmail user) $ deleteKey d . userEmailKey + -- for_ (userPhone user) $ deleteKey d . userPhoneKey + liftSem $ for_ (userHandle user) $ freeHandle (userId user) -- Wipe data - Data.clearProperties uid + wrapClient $ Data.clearProperties uid tombstone <- mkTombstone - Data.insertAccount tombstone Nothing Nothing False - Intra.rmUser uid (userAssets user) - Data.lookupClients uid >>= mapM_ (Data.rmClient uid . clientId) + liftSem $ Data.insertAccount tombstone Nothing Nothing False + wrapHttp $ Intra.rmUser uid (userAssets user) + wrapClient (Data.lookupClients uid >>= mapM_ (Data.rmClient uid . clientId)) luid <- qualifyLocal uid - Intra.onUserEvent uid Nothing (UserDeleted (qUntagged luid)) + wrapHttpClient $ Intra.onUserEvent uid Nothing (UserDeleted (qUntagged luid)) -- Note: Connections can only be deleted afterwards, since -- they need to be notified. - Data.deleteConnections uid - revokeAllCookies uid + wrapClient $ Data.deleteConnections uid + wrapClient $ revokeAllCookies uid where mkTombstone = do defLoc <- setDefaultUserLocale <$> view settings diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index f3889579d1..1ee7653649 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -95,14 +95,32 @@ import Brig.Provider.Template import qualified Brig.Queue.Stomp as Stomp import Brig.Queue.Types (Queue (..)) import qualified Brig.SMTP as SMTP +import Brig.Sem.ActivationKeyStore (ActivationKeyStore) +import Brig.Sem.ActivationKeyStore.Cassandra +import Brig.Sem.ActivationSupply (ActivationSupply) +import Brig.Sem.ActivationSupply.IO +import Brig.Sem.BudgetStore (BudgetStore) +import Brig.Sem.BudgetStore.Cassandra import Brig.Sem.CodeStore (CodeStore) import Brig.Sem.CodeStore.Cassandra +import Brig.Sem.GalleyAccess (GalleyAccess) +import Brig.Sem.GalleyAccess.Http import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetStore.CodeStore import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import Brig.Sem.PasswordResetSupply.IO +import Brig.Sem.Twilio +import Brig.Sem.Twilio.IO +import Brig.Sem.UniqueClaimsStore (UniqueClaimsStore) +import Brig.Sem.UniqueClaimsStore.Cassandra +import Brig.Sem.UserHandleStore (UserHandleStore) +import Brig.Sem.UserHandleStore.Cassandra +import Brig.Sem.UserKeyStore (UserKeyStore) +import Brig.Sem.UserKeyStore.Cassandra import Brig.Sem.UserQuery (ReAuthError, UserQuery) import Brig.Sem.UserQuery.Cassandra +import Brig.Sem.VerificationCodeStore (VerificationCodeStore) +import Brig.Sem.VerificationCodeStore.Cassandra import Brig.Team.Template import Brig.Template (Localised, TemplateBranding, forLocale, genTemplateBranding) import Brig.Types (Locale (..)) @@ -130,6 +148,7 @@ import Data.Metrics (Metrics) import qualified Data.Metrics.Middleware as Metrics import Data.Misc import Data.Qualified +import Data.String.Conversions import Data.Text (unpack) import qualified Data.Text as Text import Data.Text.Encoding (encodeUtf8) @@ -141,14 +160,20 @@ import qualified Database.Bloodhound as ES import Imports import Network.HTTP.Client (responseTimeoutMicro) import Network.HTTP.Client.OpenSSL +import Network.HTTP.Types.Status +import qualified Network.Wai.Utilities.Error as Wai import OpenSSL.EVP.Digest (Digest, getDigestByName) import OpenSSL.Session (SSLOption (..)) import qualified OpenSSL.Session as SSL import qualified OpenSSL.X509.SystemStore as SSL import Polysemy +import Polysemy.Async +import Polysemy.Conc.Effect.Race +import Polysemy.Conc.Interpreter.Race import qualified Polysemy.Error as P import Polysemy.Final import Polysemy.Input +import Polysemy.Resource import qualified Polysemy.TinyLog as P import qualified Ropes.Nexmo as Nexmo import qualified Ropes.Twilio as Twilio @@ -449,14 +474,28 @@ closeEnv e = do -- App Monad type BrigCanonicalEffects = - '[ UserQuery, + '[ VerificationCodeStore, + UserKeyStore, + UserHandleStore, + Twilio, + ActivationKeyStore, + ActivationSupply, + UniqueClaimsStore, + GalleyAccess, + Embed HttpClientIO, + UserQuery, PasswordResetStore, Now, PasswordResetSupply, CodeStore, + BudgetStore, P.TinyLog, Input (Local ()), + Async, + Race, + Resource, Embed Cas.Client, + P.Error Twilio.ErrorResponse, P.Error ReAuthError, Embed IO, Final IO @@ -630,21 +669,66 @@ interpretWaiErrorToException :: Sem r a interpretWaiErrorToException = interpretErrorToException toWai +twilioToWai :: Twilio.ErrorResponse -> Wai.Error +twilioToWai e = + Wai.Error + { Wai.code = Status (Twilio.errStatus e) "", + Wai.label = "twilio-error", + Wai.message = cs . Twilio.errMessage $ e, + Wai.errorData = Nothing + } + runAppT :: Env -> AppT BrigCanonicalEffects a -> IO a runAppT e (AppT ma) = runFinal . embedToFinal . interpretWaiErrorToException + . interpretErrorToException twilioToWai . interpretClientToIO (_casClient e) + . resourceToIO + . interpretRace + . asyncToIO . runInputConst (toLocalUnsafe (Opt.setFederationDomain . _settings $ e) ()) . loggerToTinyLogReqId (view requestId e) (view applog e) + . budgetStoreToCassandra @Cas.Client . codeStoreToCassandra @Cas.Client . passwordResetSupplyToIO . nowToIOAction (_currentTime e) . passwordResetStoreToCodeStore . userQueryToCassandra @Cas.Client + . interpretHttpToIO e + . galleyAccessToHttp @HttpClientIO (_galley e) + . uniqueClaimsStoreToCassandra @Cas.Client + . activationSupplyToIO + . activationKeyStoreToCassandra @Cas.Client + . twilioToIO + . userHandleStoreToCassandra @Cas.Client + . userKeyStoreToCassandra @Cas.Client + . verificationCodeStoreToCassandra @Cas.Client $ runReaderT ma e +interpretHttpToIO :: + Member (Final IO) r => + Env -> + Sem (Embed HttpClientIO ': r) a -> + Sem r a +interpretHttpToIO e = interpret $ \case + Embed action -> embedFinal @IO $ do + let ctx = _casClient e + manager = _httpManager e + runClient ctx + . runHttpT manager + . flip runReaderT e + . runHttpClientIO + $ action + +-- interpretClientToIO :: +-- Member (Final IO) r => +-- ClientState -> +-- Sem (Embed Cassandra.Client ': r) a -> +-- Sem r a +-- interpretClientToIO ctx = interpret $ \case +-- Embed action -> embedFinal @IO $ runClient ctx action locationOf :: (MonadIO m, MonadReader Env m) => IP -> m (Maybe Location) locationOf ip = view geoDb >>= \case diff --git a/services/brig/src/Brig/Code.hs b/services/brig/src/Brig/Code.hs index ec7383151e..f0ec8ec6d4 100644 --- a/services/brig/src/Brig/Code.hs +++ b/services/brig/src/Brig/Code.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -52,9 +50,9 @@ module Brig.Code mkKey, -- * Storage - insert, - lookup, - verify, + insertCode, + getPendingCode, + verifyCode, delete, ) where @@ -62,7 +60,19 @@ where import Brig.Data.Instances () import Brig.Email (emailKeyUniq, mkEmailKey) import Brig.Phone (mkPhoneKey, phoneKeyUniq) -import Brig.Types (Email, Phone) +import Brig.Sem.VerificationCodeStore + ( Code (..), + CodeFor (..), + Retries (..), + Scope (..), + codeForEmail, + codeForPhone, + getPendingCode, + insertCode, + verifyCode, + ) +import Brig.Sem.VerificationCodeStore.Cassandra () +import Brig.Types (Email) import Brig.Types.Code (Key (..), KeyValuePair (..), Timeout (..), Value (..)) import Cassandra hiding (Value) import qualified Data.ByteString as BS @@ -81,79 +91,12 @@ import qualified Wire.API.User as User -------------------------------------------------------------------------------- -- Code -data Code = Code - { codeKey :: !Key, - codeScope :: !Scope, - codeValue :: !Value, - codeRetries :: !Retries, - codeTTL :: !Timeout, - codeFor :: !CodeFor, - codeAccount :: !(Maybe UUID) - } - deriving (Eq, Show) - -data CodeFor - = ForEmail !Email - | ForPhone !Phone - deriving (Eq, Show) - -codeForEmail :: Code -> Maybe Email -codeForEmail c - | ForEmail e <- codeFor c = Just e - | otherwise = Nothing - -codeForPhone :: Code -> Maybe Phone -codeForPhone c - | ForPhone p <- codeFor c = Just p - | otherwise = Nothing - scopeFromAction :: User.VerificationAction -> Scope scopeFromAction = \case User.CreateScimToken -> CreateScimToken User.Login -> AccountLogin User.DeleteTeam -> DeleteTeam --- | The same 'Key' can exist with different 'Value's in different --- 'Scope's at the same time. -data Scope - = AccountDeletion - | IdentityVerification - | PasswordReset - | AccountLogin - | AccountApproval - | CreateScimToken - | DeleteTeam - deriving (Eq, Show) - -instance Cql Scope where - ctype = Tagged IntColumn - - toCql AccountDeletion = CqlInt 1 - toCql IdentityVerification = CqlInt 2 - toCql PasswordReset = CqlInt 3 - toCql AccountLogin = CqlInt 4 - toCql AccountApproval = CqlInt 5 - toCql CreateScimToken = CqlInt 6 - toCql DeleteTeam = CqlInt 7 - - fromCql (CqlInt 1) = pure AccountDeletion - fromCql (CqlInt 2) = pure IdentityVerification - fromCql (CqlInt 3) = pure PasswordReset - fromCql (CqlInt 4) = pure AccountLogin - fromCql (CqlInt 5) = pure AccountApproval - fromCql (CqlInt 6) = pure CreateScimToken - fromCql (CqlInt 7) = pure DeleteTeam - fromCql _ = Left "fromCql: Scope: int expected" - -newtype Retries = Retries {numRetries :: Word8} - deriving (Eq, Show, Ord, Num, Integral, Enum, Real) - -instance Cql Retries where - ctype = Tagged IntColumn - toCql = CqlInt . fromIntegral . numRetries - fromCql (CqlInt n) = pure (Retries (fromIntegral n)) - fromCql _ = Left "fromCql: Retries: int expected" - -------------------------------------------------------------------------------- -- Generation @@ -312,24 +255,3 @@ delete k s = retry x5 $ write cql (params LocalQuorum (k, s)) where cql :: PrepQuery W (Key, Scope) () cql = "DELETE FROM vcodes WHERE key = ? AND scope = ?" - --------------------------------------------------------------------------------- --- Internal - -toCode :: Key -> Scope -> (Value, Int32, Retries, Maybe Email, Maybe Phone, Maybe UUID) -> Code -toCode k s (val, ttl, retries, email, phone, account) = - let ek = ForEmail <$> email - pk = ForPhone <$> phone - to = Timeout (fromIntegral ttl) - in case ek <|> pk of - Nothing -> error "toCode: email or phone must be present" - Just cf -> - Code - { codeKey = k, - codeScope = s, - codeValue = val, - codeTTL = to, - codeRetries = retries, - codeFor = cf, - codeAccount = account - } diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index d26e8ba5f1..77fae0e557 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -28,6 +28,9 @@ module Brig.Data.Activation lookupActivationCode, activateKey, verifyCode, + + -- * polysemized version of 'mkActivationKey' + makeActivationKey, ) where @@ -207,7 +210,7 @@ verifyCode key code = do Just p -> pure (userPhoneKey p, u) Nothing -> throwE invalidCode mkScope _ _ _ = throwE invalidCode - countdown = lift . retry x5 . write keyInsert . params LocalQuorum + countdown = lift . insertActivationKey revoke = lift $ deleteActivationPair key mkActivationKey :: UserKey -> IO ActivationKey diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index b6bda55a3b..f03d4135e3 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -89,10 +89,12 @@ import Brig.Sem.UserQuery getLocale, getName, getUsers, + insertAccount, isActivated, lookupAccount, lookupAccounts, updateEmail, + updateHandle, updatePhone, ) import Brig.Types @@ -250,58 +252,6 @@ isSamlUser locale uid = do Just (SSOIdentity (UserSSOId _) _ _) -> pure True _ -> pure False -insertAccount :: - MonadClient m => - UserAccount -> - -- | If a bot: conversation and team - -- (if a team conversation) - Maybe (ConvId, Maybe TeamId) -> - Maybe Password -> - -- | Whether the user is activated - Bool -> - m () -insertAccount (UserAccount u status) mbConv password activated = retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - let Locale l c = userLocale u - addPrepQuery - userInsert - ( userId u, - userDisplayName u, - userPict u, - userAssets u, - userEmail u, - userPhone u, - userSSOId u, - userAccentId u, - password, - activated, - status, - userExpire u, - l, - c, - view serviceRefProvider <$> userService u, - view serviceRefId <$> userService u, - userHandle u, - userTeam u, - userManagedBy u - ) - for_ ((,) <$> userService u <*> mbConv) $ \(sref, (cid, mbTid)) -> do - let pid = sref ^. serviceRefProvider - sid = sref ^. serviceRefId - addPrepQuery cqlServiceUser (pid, sid, BotId (userId u), cid, mbTid) - for_ mbTid $ \tid -> - addPrepQuery cqlServiceTeam (pid, sid, BotId (userId u), cid, tid) - where - cqlServiceUser :: PrepQuery W (ProviderId, ServiceId, BotId, ConvId, Maybe TeamId) () - cqlServiceUser = - "INSERT INTO service_user (provider, service, user, conv, team) \ - \VALUES (?, ?, ?, ?, ?)" - cqlServiceTeam :: PrepQuery W (ProviderId, ServiceId, BotId, ConvId, TeamId) () - cqlServiceTeam = - "INSERT INTO service_team (provider, service, user, conv, team) \ - \VALUES (?, ?, ?, ?, ?)" - updateLocale :: MonadClient m => UserId -> Locale -> m () updateLocale u (Locale l c) = write userLocaleUpdate (params LocalQuorum (l, c, u)) @@ -532,28 +482,6 @@ type UserRow = Maybe ManagedBy ) -type UserRowInsert = - ( UserId, - Name, - Pict, - [Asset], - Maybe Email, - Maybe Phone, - Maybe UserSSOId, - ColourId, - Maybe Password, - Activated, - AccountStatus, - Maybe UTCTimeMillis, - Language, - Maybe Country, - Maybe ProviderId, - Maybe ServiceId, - Maybe Handle, - Maybe TeamId, - ManagedBy - ) - passwordSelect :: PrepQuery R (Identity UserId) (Identity (Maybe Password)) passwordSelect = "SELECT password FROM user WHERE id = ?" @@ -600,9 +528,6 @@ userSSOIdUpdate = "UPDATE user SET sso_id = ? WHERE id = ?" userManagedByUpdate :: PrepQuery W (ManagedBy, UserId) () userManagedByUpdate = "UPDATE user SET managed_by = ? WHERE id = ?" -userHandleUpdate :: PrepQuery W (Handle, UserId) () -userHandleUpdate = "UPDATE user SET handle = ? WHERE id = ?" - userPasswordUpdate :: PrepQuery W (Password, UserId) () userPasswordUpdate = "UPDATE user SET password = ? WHERE id = ?" diff --git a/services/brig/src/Brig/Data/UserKey.hs b/services/brig/src/Brig/Data/UserKey.hs index ab81a8f748..fe14d3c789 100644 --- a/services/brig/src/Brig/Data/UserKey.hs +++ b/services/brig/src/Brig/Data/UserKey.hs @@ -54,11 +54,6 @@ userEmailKey = UserEmailKey . mkEmailKey userPhoneKey :: Phone -> UserKey userPhoneKey = UserPhoneKey . mkPhoneKey -foldKey :: (Email -> a) -> (Phone -> a) -> UserKey -> a -foldKey f g k = case k of - UserEmailKey ek -> f (emailKeyOrig ek) - UserPhoneKey pk -> g (phoneKeyOrig pk) - forEmailKey :: Applicative f => UserKey -> (Email -> f a) -> f (Maybe a) forEmailKey k f = foldKey (fmap Just . f) (const (pure Nothing)) k diff --git a/services/brig/src/Brig/Email.hs b/services/brig/src/Brig/Email.hs index ff868636c2..8e3f015f3b 100644 --- a/services/brig/src/Brig/Email.hs +++ b/services/brig/src/Brig/Email.hs @@ -19,15 +19,13 @@ module Brig.Email ( -- * Validation validateEmail, - -- * Unique Keys + -- * Re-exports + Email (..), EmailKey, mkEmailKey, emailKeyUniq, emailKeyOrig, - -- * Re-exports - Email (..), - -- * MIME Re-exports Mail (..), emptyMail, @@ -55,39 +53,6 @@ sendMail m = Just smtp -> SMTP.sendMail smtp m Nothing -> view awsEnv >>= \e -> AWS.execute e $ AWS.sendMail m -------------------------------------------------------------------------------- --- Unique Keys - --- | An 'EmailKey' is an 'Email' in a form that serves as a unique lookup key. -data EmailKey = EmailKey - { emailKeyUniq :: !Text, - emailKeyOrig :: !Email - } - -instance Show EmailKey where - showsPrec _ = shows . emailKeyUniq - -instance Eq EmailKey where - (EmailKey k _) == (EmailKey k' _) = k == k' - --- | Turn an 'Email' into an 'EmailKey'. --- --- The following transformations are performed: --- --- * Both local and domain parts are forced to lowercase to make --- e-mail addresses fully case-insensitive. --- * "+" suffixes on the local part are stripped unless the domain --- part is contained in a trusted whitelist. -mkEmailKey :: Email -> EmailKey -mkEmailKey orig@(Email localPart domain) = - let uniq = Text.toLower localPart' <> "@" <> Text.toLower domain - in EmailKey uniq orig - where - localPart' - | domain `notElem` trusted = Text.takeWhile (/= '+') localPart - | otherwise = localPart - trusted = ["wearezeta.com", "wire.com", "simulator.amazonses.com"] - ------------------------------------------------------------------------------- -- MIME Conversions diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 51a0c37dca..3faf928944 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -20,70 +20,66 @@ module Brig.InternalEvent.Process ) where -import Bilge.IO (MonadHttp) -import Bilge.RPC (HasRequestId) import qualified Brig.API.User as API import Brig.App import Brig.InternalEvent.Types import Brig.Options (defDeleteThrottleMillis, setDefaultUserLocale, setDeleteThrottleMillis) import qualified Brig.Provider.API as API -import Brig.Sem.UserQuery.Cassandra -import Brig.User.Search.Index (MonadIndexIO) -import Cassandra (MonadClient) +import Brig.Sem.UniqueClaimsStore +import Brig.Sem.UserHandleStore +import Brig.Sem.UserQuery import Control.Lens (view) import Control.Monad.Catch import Data.ByteString.Conversion +import Data.Qualified import Imports import Polysemy +import Polysemy.Conc.Effect.Race +import Polysemy.Conc.Race import Polysemy.Input +import Polysemy.Time.Data.TimeUnit +import qualified Polysemy.TinyLog as P import System.Logger.Class (field, msg, val, (~~)) -import qualified System.Logger.Class as Log -import UnliftIO (timeout) -- | Handle an internal event. -- -- Has a one-minute timeout that should be enough for anything that it does. onEvent :: - forall m. - ( Log.MonadLogger m, - MonadCatch m, - MonadThrow m, - MonadIndexIO m, - MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m, - MonadClient m - ) => + forall r. + Members + '[ Input (Local ()), + P.TinyLog, + Race, + UniqueClaimsStore, + UserHandleStore, + UserQuery + ] + r => InternalNotification -> - m () + AppT r () onEvent n = do locale <- setDefaultUserLocale <$> view settings - locDomain <- qualifyLocal () + delay <- fromMaybe defDeleteThrottleMillis . setDeleteThrottleMillis <$> view settings handleTimeout $ case n of DeleteUser uid -> do - Log.info $ + liftSem . P.info $ msg (val "Processing user delete event") ~~ field "user" (toByteString uid) - (runM . userQueryToCassandra @m @'[Embed m] . runInputConst locDomain $ API.lookupAccount locale uid) >>= mapM_ API.deleteAccount + liftSem (API.lookupAccount locale uid) >>= mapM_ API.deleteAccount -- As user deletions are expensive resource-wise in the context of -- bulk user deletions (e.g. during team deletions), -- wait 'delay' ms before processing the next event - delay <- fromMaybe defDeleteThrottleMillis . setDeleteThrottleMillis <$> view settings - liftIO $ threadDelay (1000 * delay) + liftSem $ timeoutU (MicroSeconds $ 1000 * fromIntegral delay) $ pure () DeleteService pid sid -> do - Log.info $ + liftSem . P.info $ msg (val "Processing service delete event") ~~ field "provider" (toByteString pid) ~~ field "service" (toByteString sid) - API.finishDeleteService pid sid + wrapHttpClient $ API.finishDeleteService pid sid where - handleTimeout act = - timeout 60000000 act >>= \case - Just x -> pure x - Nothing -> throwM (InternalEventTimeout n) + handleTimeout :: AppT r a -> AppT r a + handleTimeout _act = undefined + newtype InternalEventException = -- | 'onEvent' has timed out diff --git a/services/brig/src/Brig/Phone.hs b/services/brig/src/Brig/Phone.hs index 532381f7ac..21e33bb1b2 100644 --- a/services/brig/src/Brig/Phone.hs +++ b/services/brig/src/Brig/Phone.hs @@ -26,14 +26,12 @@ module Brig.Phone -- * Validation validatePhone, - -- * Unique Keys + -- * Re-exports + Phone (..), PhoneKey, mkPhoneKey, phoneKeyUniq, phoneKeyOrig, - - -- * Re-exports - Phone (..), ) where @@ -41,6 +39,7 @@ import Bilge.Retry (httpHandlers) import Brig.App import Brig.Sem.BudgetStore import Brig.Sem.BudgetStore.Cassandra +import Brig.Sem.Twilio import Brig.Types import Cassandra (MonadClient) import Control.Lens (view) @@ -48,11 +47,13 @@ import Control.Monad.Catch import Control.Retry import Data.LanguageCodes import qualified Data.Metrics as Metrics +import Data.String.Conversions import qualified Data.Text as Text import Data.Time.Clock import Imports import Network.HTTP.Client (HttpException, Manager) import Polysemy +import qualified Polysemy.Error as P import qualified Ropes.Nexmo as Nexmo import Ropes.Twilio (LookupDetail (..)) import qualified Ropes.Twilio as Twilio @@ -204,21 +205,19 @@ sendSms loc SMSMessage {..} = unless (isTestPhone smsTo) $ do -- | Validate a phone number. Returns the canonical -- E.164 format of the given phone number on success. -validatePhone :: (MonadClient m, MonadReader Env m) => Phone -> m (Maybe Phone) -validatePhone (Phone p) +validatePhone :: + Members '[P.Error Twilio.ErrorResponse, Twilio] r => + Twilio.Credentials -> + Manager -> + Phone -> + Sem r (Maybe Phone) +validatePhone c m (Phone p) | isTestPhone p = pure (Just (Phone p)) | otherwise = do - c <- view twilioCreds - m <- view httpManager - r <- - liftIO . try @_ @Twilio.ErrorResponse $ - recovering x3 httpHandlers $ - const $ - Twilio.lookupPhone c m p LookupNoDetail Nothing - case r of + lookupPhone c m p LookupNoDetail Nothing >>= \case Right x -> pure (Just (Phone (Twilio.lookupE164 x))) Left e | Twilio.errStatus e == 404 -> pure Nothing - Left e -> throwM e + Left e -> P.throw e isTestPhone :: Text -> Bool isTestPhone = Text.isPrefixOf "+0" @@ -295,27 +294,6 @@ withCallBudget phone go = do ~~ field "phone" phone pure a --------------------------------------------------------------------------------- --- Unique Keys - -data PhoneKey = PhoneKey - { -- | canonical form of 'phoneKeyOrig', without whitespace. - phoneKeyUniq :: !Text, - -- | phone number with whitespace. - phoneKeyOrig :: !Phone - } - -instance Show PhoneKey where - showsPrec _ = shows . phoneKeyUniq - -instance Eq PhoneKey where - (PhoneKey k _) == (PhoneKey k' _) = k == k' - -mkPhoneKey :: Phone -> PhoneKey -mkPhoneKey orig = - let uniq = Text.filter (not . isSpace) (fromPhone orig) - in PhoneKey uniq orig - ------------------------------------------------------------------------------- -- Retry Settings diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 9534cdddc7..0c6ae7a233 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -48,6 +48,7 @@ import qualified Brig.Provider.RPC as RPC import qualified Brig.Queue as Queue import Brig.Sem.UserQuery (UserQuery) import Brig.Sem.UserQuery.Cassandra +import Brig.Sem.VerificationCodeStore import Brig.Team.Util import Brig.Types.Client (Client (..), ClientType (..), newClient, newClientPrekeys) import Brig.Types.Intra (AccountStatus (..), UserAccount (..)) @@ -120,7 +121,7 @@ import qualified Wire.API.User.Client.Prekey as Public (PrekeyId) import qualified Wire.API.User.Identity as Public (Email) routesPublic :: - Member UserQuery r => + Members '[UserQuery, VerificationCodeStore] r => Routes Doc.ApiBuilder (Handler r) () routesPublic = do -- Public API (Unauthenticated) -------------------------------------------- @@ -320,7 +321,9 @@ routesPublic = do .&> zauth ZAuthBot .&> capture "uid" -routesInternal :: Routes a (Handler r) () +routesInternal :: + Members '[VerificationCodeStore] r => + Routes a (Handler r) () routesInternal = do get "/i/provider/activation-code" (continue getActivationCodeH) $ accept "application" "json" @@ -329,12 +332,18 @@ routesInternal = do -------------------------------------------------------------------------------- -- Public API (Unauthenticated) -newAccountH :: JsonRequest Public.NewProvider -> (Handler r) Response +newAccountH :: + Members '[VerificationCodeStore] r => + JsonRequest Public.NewProvider -> + Handler r Response newAccountH req = do mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing setStatus status201 . json <$> (newAccount =<< parseJsonBody req) -newAccount :: Public.NewProvider -> (Handler r) Public.NewProviderResponse +newAccount :: + Members '[VerificationCodeStore] r => + Public.NewProvider -> + Handler r Public.NewProviderResponse newAccount new = do email <- case validateEmail (Public.newProviderEmail new) of Right em -> pure em @@ -360,20 +369,27 @@ newAccount new = do (Code.Retries 3) (Code.Timeout (3600 * 24)) -- 24h (Just (toUUID pid)) - wrapClientE $ Code.insert code + lift . liftSem $ Code.insertCode code let key = Code.codeKey code let val = Code.codeValue code lift $ sendActivationMail name email key val False pure $ Public.NewProviderResponse pid newPass -activateAccountKeyH :: Code.Key ::: Code.Value -> (Handler r) Response +activateAccountKeyH :: + Members '[VerificationCodeStore] r => + Code.Key ::: Code.Value -> + Handler r Response activateAccountKeyH (key ::: val) = do mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing maybe (setStatus status204 empty) json <$> activateAccountKey key val -activateAccountKey :: Code.Key -> Code.Value -> (Handler r) (Maybe Public.ProviderActivationResponse) +activateAccountKey :: + Members '[VerificationCodeStore] r => + Code.Key -> + Code.Value -> + Handler r (Maybe Public.ProviderActivationResponse) activateAccountKey key val = do - c <- wrapClientE (Code.verify key Code.IdentityVerification val) >>= maybeInvalidCode + c <- (lift . liftSem) (Code.verifyCode key Code.IdentityVerification val) >>= maybeInvalidCode (pid, email) <- case (Code.codeAccount c, Code.codeForEmail c) of (Just p, Just e) -> pure (Id p, e) _ -> throwStd (errorToWai @'InvalidCode) @@ -393,18 +409,24 @@ activateAccountKey key val = do lift $ sendApprovalConfirmMail name email pure . Just $ Public.ProviderActivationResponse email -getActivationCodeH :: Public.Email -> (Handler r) Response +getActivationCodeH :: + Members '[VerificationCodeStore] r => + Public.Email -> + Handler r Response getActivationCodeH e = do mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing json <$> getActivationCode e -getActivationCode :: Public.Email -> (Handler r) FoundActivationCode +getActivationCode :: + Members '[VerificationCodeStore] r => + Public.Email -> + Handler r FoundActivationCode getActivationCode e = do email <- case validateEmail e of Right em -> pure em Left _ -> throwStd (errorToWai @'InvalidEmail) gen <- Code.mkGen (Code.ForEmail email) - code <- wrapClientE $ Code.lookup (Code.genKey gen) Code.IdentityVerification + code <- lift . liftSem $ Code.getPendingCode (Code.genKey gen) Code.IdentityVerification maybe (throwStd activationKeyNotFound) (pure . FoundActivationCode) code newtype FoundActivationCode = FoundActivationCode Code.Code @@ -414,14 +436,21 @@ instance ToJSON FoundActivationCode where toJSON $ Code.KeyValuePair (Code.codeKey vcode) (Code.codeValue vcode) -approveAccountKeyH :: Code.Key ::: Code.Value -> (Handler r) Response +approveAccountKeyH :: + Members '[VerificationCodeStore] r => + Code.Key ::: Code.Value -> + Handler r Response approveAccountKeyH (key ::: val) = do mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing empty <$ approveAccountKey key val -approveAccountKey :: Code.Key -> Code.Value -> (Handler r) () +approveAccountKey :: + Members '[VerificationCodeStore] r => + Code.Key -> + Code.Value -> + Handler r () approveAccountKey key val = do - c <- wrapClientE (Code.verify key Code.AccountApproval val) >>= maybeInvalidCode + c <- (lift . liftSem) (Code.verifyCode key Code.AccountApproval val) >>= maybeInvalidCode case (Code.codeAccount c, Code.codeForEmail c) of (Just pid, Just email) -> do (name, _, _, _) <- wrapClientE (DB.lookupAccountData (Id pid)) >>= maybeInvalidCode @@ -443,16 +472,22 @@ login l = do throwStd (errorToWai @'BadCredentials) ZAuth.newProviderToken pid -beginPasswordResetH :: JsonRequest Public.PasswordReset -> (Handler r) Response +beginPasswordResetH :: + Members '[VerificationCodeStore] r => + JsonRequest Public.PasswordReset -> + Handler r Response beginPasswordResetH req = do mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing setStatus status201 empty <$ (beginPasswordReset =<< parseJsonBody req) -beginPasswordReset :: Public.PasswordReset -> (Handler r) () +beginPasswordReset :: + Members '[VerificationCodeStore] r => + Public.PasswordReset -> + Handler r () beginPasswordReset (Public.PasswordReset target) = do pid <- wrapClientE (DB.lookupKey (mkEmailKey target)) >>= maybeBadCredentials gen <- Code.mkGen (Code.ForEmail target) - pending <- lift . wrapClient $ Code.lookup (Code.genKey gen) Code.PasswordReset + pending <- lift . liftSem $ Code.getPendingCode (Code.genKey gen) Code.PasswordReset code <- case pending of Just p -> throwE $ pwResetError (PasswordResetInProgress . Just $ Code.codeTTL p) Nothing -> @@ -462,17 +497,23 @@ beginPasswordReset (Public.PasswordReset target) = do (Code.Retries 3) (Code.Timeout 3600) -- 1h (Just (toUUID pid)) - wrapClientE $ Code.insert code + lift . liftSem $ Code.insertCode code lift $ sendPasswordResetMail target (Code.codeKey code) (Code.codeValue code) -completePasswordResetH :: JsonRequest Public.CompletePasswordReset -> (Handler r) Response +completePasswordResetH :: + Members '[VerificationCodeStore] r => + JsonRequest Public.CompletePasswordReset -> + Handler r Response completePasswordResetH req = do mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing empty <$ (completePasswordReset =<< parseJsonBody req) -completePasswordReset :: Public.CompletePasswordReset -> (Handler r) () +completePasswordReset :: + Members '[VerificationCodeStore] r => + Public.CompletePasswordReset -> + Handler r () completePasswordReset (Public.CompletePasswordReset key val newpwd) = do - code <- wrapClientE (Code.verify key Code.PasswordReset val) >>= maybeInvalidCode + code <- (lift . liftSem) (Code.verifyCode key Code.PasswordReset val) >>= maybeInvalidCode case Id <$> Code.codeAccount code of Nothing -> throwE $ pwResetError InvalidPasswordResetCode Just pid -> do @@ -511,12 +552,19 @@ updateAccountProfile pid upd = do (updateProviderUrl upd) (updateProviderDescr upd) -updateAccountEmailH :: ProviderId ::: JsonRequest Public.EmailUpdate -> (Handler r) Response +updateAccountEmailH :: + Members '[VerificationCodeStore] r => + ProviderId ::: JsonRequest Public.EmailUpdate -> + Handler r Response updateAccountEmailH (pid ::: req) = do mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing setStatus status202 empty <$ (updateAccountEmail pid =<< parseJsonBody req) -updateAccountEmail :: ProviderId -> Public.EmailUpdate -> (Handler r) () +updateAccountEmail :: + Members '[VerificationCodeStore] r => + ProviderId -> + Public.EmailUpdate -> + Handler r () updateAccountEmail pid (Public.EmailUpdate new) = do email <- case validateEmail new of Right em -> pure em @@ -531,7 +579,7 @@ updateAccountEmail pid (Public.EmailUpdate new) = do (Code.Retries 3) (Code.Timeout (3600 * 24)) -- 24h (Just (toUUID pid)) - wrapClientE $ Code.insert code + lift . liftSem $ Code.insertCode code lift $ sendActivationMail (Name "name") email (Code.codeKey code) (Code.codeValue code) True updateAccountPasswordH :: ProviderId ::: JsonRequest Public.PasswordChange -> (Handler r) Response @@ -950,7 +998,7 @@ addBot zuid zcon cid add = do (newClient PermanentClientType (Ext.rsNewBotLastPrekey rs)) { newClientPrekeys = Ext.rsNewBotPrekeys rs } - lift $ wrapClient $ User.insertAccount (UserAccount usr Active) (Just (cid, cnvTeam cnv)) Nothing True + lift . liftSem $ User.insertAccount (UserAccount usr Active) (Just (cid, cnvTeam cnv)) Nothing True maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings (clt, _, _) <- do _ <- do diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index bf14143ddf..2c343bf343 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -18,10 +18,8 @@ module Brig.Provider.DB where import Brig.Data.Instances () -import Brig.Email (EmailKey, emailKeyOrig, emailKeyUniq) -import Brig.Password -- import Brig.Provider.DB.Instances () - +import Brig.Password import Brig.Types.Common import Brig.Types.Instances () import Brig.Types.Provider hiding (updateServiceTags) diff --git a/services/brig/src/Brig/RPC.hs b/services/brig/src/Brig/RPC.hs index 5320a81f19..796b9b55a8 100644 --- a/services/brig/src/Brig/RPC.hs +++ b/services/brig/src/Brig/RPC.hs @@ -16,20 +16,23 @@ -- with this program. If not, see . -- | General RPC utilities. -module Brig.RPC where +module Brig.RPC + ( module Brig.RPC, + module Brig.RPC.Decode, + ) +where import Bilge import Bilge.RPC import Bilge.Retry import Brig.App +import Brig.RPC.Decode import Control.Lens import Control.Monad.Catch import Control.Retry -import Data.Aeson import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as BL import Data.Id -import qualified Data.Text as Text import qualified Data.Text.Lazy as LT import Imports import Network.HTTP.Client (HttpExceptionContent (..), checkResponse) @@ -46,9 +49,6 @@ zUser = header "Z-User" . toByteString' remote :: ByteString -> Msg -> Msg remote = field "remote" -decodeBody :: (Typeable a, FromJSON a, MonadThrow m) => Text -> Response (Maybe BL.ByteString) -> m a -decodeBody ctx = responseJsonThrow (ParseException ctx) - expect :: [Status] -> Request -> Request expect ss rq = rq {checkResponse = check} where @@ -92,18 +92,3 @@ serviceRequest nm svc m r = do recovering x3 rpcHandlers $ const $ rpc' nm service (method m . r) - --- | Failed to parse a response from another service. -data ParseException = ParseException - { _parseExceptionRemote :: !Text, - _parseExceptionMsg :: String - } - -instance Show ParseException where - show (ParseException r m) = - "Failed to parse response from remote " - ++ Text.unpack r - ++ " with message: " - ++ m - -instance Exception ParseException diff --git a/services/brig/src/Brig/RPC/Decode.hs b/services/brig/src/Brig/RPC/Decode.hs new file mode 100644 index 0000000000..ec6bdb8e87 --- /dev/null +++ b/services/brig/src/Brig/RPC/Decode.hs @@ -0,0 +1,47 @@ +-- 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.RPC.Decode where + +import Bilge.Response +import Control.Monad.Catch +import Data.Aeson +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Text as Text +import Imports + +decodeBody :: + (Typeable a, FromJSON a, MonadThrow m) => + Text -> + Response (Maybe LBS.ByteString) -> + m a +decodeBody ctx = responseJsonThrow (ParseException ctx) + +-- | Failed to parse a response from another service. +data ParseException = ParseException + { _parseExceptionRemote :: !Text, + _parseExceptionMsg :: String + } + +instance Show ParseException where + show (ParseException r m) = + "Failed to parse response from remote " + ++ Text.unpack r + ++ " with message: " + ++ m + +instance Exception ParseException diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index a2b87e71a1..71501d185d 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -35,9 +35,7 @@ import qualified Brig.AWS.SesNotification as SesNotification import Brig.App import qualified Brig.Calling as Calling import Brig.Data.UserPendingActivation (UserPendingActivation (..), usersPendingActivationList, usersPendingActivationRemoveMultiple) -import qualified Brig.InternalEvent.Process as Internal import Brig.Options hiding (internalEvents, sesQueue) -import qualified Brig.Queue as Queue import Brig.Types.Intra (AccountStatus (PendingInvitation)) import Brig.Version import Cassandra (Page (Page)) @@ -82,11 +80,8 @@ run :: Opts -> IO () run o = do (app, e) <- mkApp o s <- Server.newSettings (server e) - internalEventListener <- - Async.async $ - runAppT e $ - wrapHttpClient $ - Queue.listen (e ^. internalEvents) Internal.onEvent + -- TODO(md): Find or implement a Polysemy equivalent + internalEventListener :: Async.Async () <- undefined let throttleMillis = fromMaybe defSqsThrottleMillis $ setSqsThrottleMillis (optSettings o) emailListener <- for (e ^. awsEnv . sesQueue) $ \q -> Async.async $ diff --git a/services/brig/src/Brig/Sem/ActivationSupply.hs b/services/brig/src/Brig/Sem/ActivationSupply.hs index 1ba2cfd90c..9126fd58e2 100644 --- a/services/brig/src/Brig/Sem/ActivationSupply.hs +++ b/services/brig/src/Brig/Sem/ActivationSupply.hs @@ -19,9 +19,8 @@ module Brig.Sem.ActivationSupply where -import Brig.Sem.UserKeyStore (UserKey) -- TODO: The UserKey type should be moved to wire-api +import Brig.Types import Polysemy -import Wire.API.User.Activation data ActivationSupply m a where MakeActivationKey :: UserKey -> ActivationSupply m ActivationKey diff --git a/services/brig/src/Brig/Sem/ActivationSupply/IO.hs b/services/brig/src/Brig/Sem/ActivationSupply/IO.hs index 57d05ee635..15d42cb6db 100644 --- a/services/brig/src/Brig/Sem/ActivationSupply/IO.hs +++ b/services/brig/src/Brig/Sem/ActivationSupply/IO.hs @@ -18,7 +18,7 @@ module Brig.Sem.ActivationSupply.IO (activationSupplyToIO) where import Brig.Sem.ActivationSupply -import Brig.Sem.UserKeyStore (UserKey, keyText) +import Brig.Types import Data.Text import qualified Data.Text.Ascii as Ascii import qualified Data.Text.Encoding as T @@ -27,7 +27,6 @@ import OpenSSL.BN import OpenSSL.EVP.Digest import Polysemy import Text.Printf -import Wire.API.User.Activation activationSupplyToIO :: forall r a. diff --git a/services/brig/src/Brig/Sem/GalleyAccess.hs b/services/brig/src/Brig/Sem/GalleyAccess.hs index 212e031902..d017f123e1 100644 --- a/services/brig/src/Brig/Sem/GalleyAccess.hs +++ b/services/brig/src/Brig/Sem/GalleyAccess.hs @@ -20,10 +20,10 @@ module Brig.Sem.GalleyAccess where import Data.Id -import Imports import Polysemy +import Wire.API.Team.Feature data GalleyAccess m a where - GetTeamSndFactorPasswordChallenge :: TeamId -> GalleyAccess m TeamFeatureEnabled + GetTeamSndFactorPasswordChallenge :: TeamId -> GalleyAccess m TeamFeatureStatusValue makeSem ''GalleyAccess diff --git a/services/brig/src/Brig/Sem/GalleyAccess/Http.hs b/services/brig/src/Brig/Sem/GalleyAccess/Http.hs index 30e99e595f..1a7fea9585 100644 --- a/services/brig/src/Brig/Sem/GalleyAccess/Http.hs +++ b/services/brig/src/Brig/Sem/GalleyAccess/Http.hs @@ -17,24 +17,60 @@ module Brig.Sem.GalleyAccess.Http (galleyAccessToHttp) where -import Brig.RPC +import qualified Bilge as RPC +import Bilge.IO +import Bilge.RPC +import Bilge.Request +import Bilge.Retry +import qualified Brig.RPC.Decode as RPC import Brig.Sem.GalleyAccess +import Control.Monad.Catch +import Control.Retry +import Data.ByteString.Conversion.To +import qualified Data.ByteString.Lazy as LBS import Imports +import Network.HTTP.Client (Response) +import Network.HTTP.Types.Method import Polysemy +import Wire.API.Team.Feature galleyAccessToHttp :: forall m r a. - ( MonadReader Env m, - MonadIO m, + ( MonadIO m, MonadMask m, MonadHttp m, - HasRequestId m + HasRequestId m, + Member (Embed m) r ) => + RPC.Request -> Sem (GalleyAccess ': r) a -> Sem r a -galleyAccessToHttp = +galleyAccessToHttp g = interpret $ embed @m . \case GetTeamSndFactorPasswordChallenge tid -> do - response <- galleyRequest GET req - tfwoStatus <$> decodeBody "galley" response + let req = + paths + [ "i", + "teams", + toByteString' tid, + "features", + toByteString' TeamFeatureSndFactorPasswordChallenge + ] + . expect2xx + response <- makeReq g GET req + tfwoStatus <$> RPC.decodeBody "galley" response + +makeReq :: + (MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + RPC.Request -> + StdMethod -> + (Request -> Request) -> + m (Response (Maybe LBS.ByteString)) +makeReq galley m r = + recovering x3 rpcHandlers $ + const $ + rpc' "galley" galley (method m . r) + +x3 :: RetryPolicy +x3 = limitRetries 3 <> exponentialBackoff 100000 diff --git a/services/brig/src/Brig/Sem/Twilio/IO.hs b/services/brig/src/Brig/Sem/Twilio/IO.hs index a6e67b19b4..fec684023c 100644 --- a/services/brig/src/Brig/Sem/Twilio/IO.hs +++ b/services/brig/src/Brig/Sem/Twilio/IO.hs @@ -18,7 +18,6 @@ module Brig.Sem.Twilio.IO (twilioToIO) where import Bilge.Retry -import Brig.RPC import Brig.Sem.Twilio import Control.Monad.Catch import Control.Retry @@ -38,3 +37,6 @@ twilioToIO = liftIO . try @_ @Ropes.ErrorResponse $ recovering x3 httpHandlers $ const $ Ropes.lookupPhone cred m txt detail code + +x3 :: RetryPolicy +x3 = limitRetries 3 <> exponentialBackoff 100000 diff --git a/services/brig/src/Brig/Sem/UniqueClaimsStore.hs b/services/brig/src/Brig/Sem/UniqueClaimsStore.hs new file mode 100644 index 0000000000..4d4e340fdd --- /dev/null +++ b/services/brig/src/Brig/Sem/UniqueClaimsStore.hs @@ -0,0 +1,33 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.UniqueClaimsStore where + +import Data.Id +import Data.Timeout +import Imports +import Polysemy + +data UniqueClaimsStore m a where + AddClaims :: Id a -> Timeout -> Text -> UniqueClaimsStore m () + -- | Lookup the current claims on a value. + GetClaims :: Text -> UniqueClaimsStore m [Id a] + DeleteClaims :: Id i -> Timeout -> Text -> UniqueClaimsStore m () + +makeSem ''UniqueClaimsStore diff --git a/services/brig/src/Brig/Sem/UniqueClaimsStore/Cassandra.hs b/services/brig/src/Brig/Sem/UniqueClaimsStore/Cassandra.hs new file mode 100644 index 0000000000..8c4e53d126 --- /dev/null +++ b/services/brig/src/Brig/Sem/UniqueClaimsStore/Cassandra.hs @@ -0,0 +1,81 @@ +-- 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.Sem.UniqueClaimsStore.Cassandra (uniqueClaimsStoreToCassandra) where + +import Brig.Sem.UniqueClaimsStore +import Cassandra +import qualified Cassandra as C +import Data.Id +import Data.Timeout +import Imports +import Polysemy + +minTtl :: Timeout +minTtl = 60 # Second + +uniqueClaimsStoreToCassandra :: + forall m r a. + (MonadClient m, Member (Embed m) r) => + Sem (UniqueClaimsStore ': r) a -> + Sem r a +uniqueClaimsStoreToCassandra = + interpret $ + embed @m . \case + AddClaims u t txt -> addClaimsQuery u t txt + GetClaims txt -> lookupClaims txt + DeleteClaims u t txt -> deleteClaimsQuery u t txt + +deleteClaimsQuery :: + MonadClient m => + -- | The 'Id' associated with the claim. + Id a -> + -- | The minimum timeout (i.e. duration) of the rest of the claim. (Each + -- claim can have more than one claimer (even though this is a feature we + -- never use), so removing a claim is an update operation on the database. + -- Therefore, we reset the TTL the same way we reset it in 'withClaim'.) + Timeout -> + -- | The value on which to acquire the claim. + Text -> + m () +deleteClaimsQuery u t v = do + let ttl = max minTtl t + retry x5 $ write cql $ params LocalQuorum (fromIntegral $ (ttl * 2) #> Second, C.Set [u], v) + where + cql :: PrepQuery W (Int32, C.Set (Id a), Text) () + cql = "UPDATE unique_claims USING TTL ? SET claims = claims - ? WHERE value = ?" + +addClaimsQuery :: MonadClient m => Id a -> Timeout -> Text -> m () +addClaimsQuery u t v = do + let ttl = max minTtl t + retry x5 + . write cql + $ params LocalQuorum (fromIntegral $ (ttl * 2) #> Second, C.Set [u], v) + where + cql :: PrepQuery W (Int32, C.Set (Id a), Text) () + cql = "UPDATE unique_claims USING TTL ? SET claims = claims + ? WHERE value = ?" + +-- | Lookup the current claims on a value. +lookupClaims :: MonadClient m => Text -> m [Id a] +lookupClaims v = + fmap (maybe [] (fromSet . runIdentity)) $ + retry x1 $ + query1 cql $ + params LocalQuorum (Identity v) + where + cql :: PrepQuery R (Identity Text) (Identity (C.Set (Id a))) + cql = "SELECT claims FROM unique_claims WHERE value = ?" diff --git a/services/brig/src/Brig/Sem/UserHandleStore.hs b/services/brig/src/Brig/Sem/UserHandleStore.hs new file mode 100644 index 0000000000..3c324cff62 --- /dev/null +++ b/services/brig/src/Brig/Sem/UserHandleStore.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.UserHandleStore where + +import Data.Handle +import Data.Id +import Imports +import Polysemy + +-- | An enum with a one-to-one mapping to 'Cassandra.Consistency'. +data Consistency + = One + | LocalQuorum + | All + deriving (Eq) + +data UserHandleStore m a where + InsertHandle :: Handle -> UserId -> UserHandleStore m () + -- | Lookup the current owner of a 'Handle'. │ + GetHandleWithConsistency :: Consistency -> Handle -> UserHandleStore m (Maybe UserId) + DeleteHandle :: Handle -> UserHandleStore m () + +makeSem ''UserHandleStore + +-- | Lookup the current owner of a 'Handle'. +lookupHandle :: Member UserHandleStore r => Handle -> Sem r (Maybe UserId) +lookupHandle = getHandleWithConsistency LocalQuorum diff --git a/services/brig/src/Brig/Sem/UserHandleStore/Cassandra.hs b/services/brig/src/Brig/Sem/UserHandleStore/Cassandra.hs new file mode 100644 index 0000000000..ae3e71ac40 --- /dev/null +++ b/services/brig/src/Brig/Sem/UserHandleStore/Cassandra.hs @@ -0,0 +1,73 @@ +-- 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.Sem.UserHandleStore.Cassandra (userHandleStoreToCassandra) where + +import Brig.Data.Instances () +import Brig.Sem.UserHandleStore +import Cassandra hiding (Consistency (..)) +import qualified Cassandra as C +import Data.Handle +import Data.Id +import Imports hiding (All) +import Polysemy + +userHandleStoreToCassandra :: + forall m r a. + (MonadClient m, Member (Embed m) r) => + Sem (UserHandleStore ': r) a -> + Sem r a +userHandleStoreToCassandra = + interpret $ + embed @m . \case + InsertHandle h uid -> insertHandleQuery h uid + GetHandleWithConsistency c h -> lookupHandleWithPolicy (mapConsistency c) h + DeleteHandle h -> deleteHandleQuery h + +deleteHandleQuery :: MonadClient m => Handle -> m () +deleteHandleQuery h = retry x5 $ write handleDelete (params C.LocalQuorum (Identity h)) + +mapConsistency :: Consistency -> C.Consistency +mapConsistency = \case + One -> C.One + LocalQuorum -> C.LocalQuorum + All -> C.All + +{-# INLINE lookupHandleWithPolicy #-} + +insertHandleQuery :: MonadClient m => Handle -> UserId -> m () +insertHandleQuery newHandle uid = + retry x5 $ write handleInsert (params C.LocalQuorum (newHandle, uid)) + +-- | Sending an empty 'Handle' here causes C* to throw "Key may not be empty" +-- error. +-- +-- FUTUREWORK: This should ideally be tackled by hiding constructor for 'Handle' +-- and only allowing it to be parsed. +lookupHandleWithPolicy :: MonadClient m => C.Consistency -> Handle -> m (Maybe UserId) +lookupHandleWithPolicy policy h = do + (runIdentity =<<) + <$> retry x1 (query1 handleSelect (params policy (Identity h))) + +handleSelect :: PrepQuery R (Identity Handle) (Identity (Maybe UserId)) +handleSelect = "SELECT user FROM user_handle WHERE handle = ?" + +handleInsert :: PrepQuery W (Handle, UserId) () +handleInsert = "INSERT INTO user_handle (handle, user) VALUES (?, ?)" + +handleDelete :: PrepQuery W (Identity Handle) () +handleDelete = "DELETE FROM user_handle WHERE handle = ?" diff --git a/services/brig/src/Brig/Sem/UserKeyStore.hs b/services/brig/src/Brig/Sem/UserKeyStore.hs index b560b098fd..08ebac1e01 100644 --- a/services/brig/src/Brig/Sem/UserKeyStore.hs +++ b/services/brig/src/Brig/Sem/UserKeyStore.hs @@ -19,8 +19,7 @@ module Brig.Sem.UserKeyStore where -import Brig.Email -import Brig.Phone +import Brig.Types import Cassandra import Data.ByteString.Lazy import Data.Id @@ -29,16 +28,6 @@ import Imports import OpenSSL.EVP.Digest import Polysemy --- | A natural identifier (i.e. unique key) of a user. -data UserKey - = UserEmailKey !EmailKey - | UserPhoneKey !PhoneKey - -instance Eq UserKey where - (UserEmailKey k) == (UserEmailKey k') = k == k' - (UserPhoneKey k) == (UserPhoneKey k') = k == k' - _ == _ = False - newtype UserKeyHash = UserKeyHash MH.MultihashDigest instance Cql UserKeyHash where @@ -74,8 +63,3 @@ data UserKeyStore m a where DeleteKey :: Digest -> UserKey -> UserKeyStore m () makeSem ''UserKeyStore - --- | Get the normalised text of a 'UserKey'. -keyText :: UserKey -> Text -keyText (UserEmailKey k) = emailKeyUniq k -keyText (UserPhoneKey k) = phoneKeyUniq k diff --git a/services/brig/src/Brig/Sem/UserKeyStore/Cassandra.hs b/services/brig/src/Brig/Sem/UserKeyStore/Cassandra.hs index 67973a83aa..8aaf2c0f8b 100644 --- a/services/brig/src/Brig/Sem/UserKeyStore/Cassandra.hs +++ b/services/brig/src/Brig/Sem/UserKeyStore/Cassandra.hs @@ -15,12 +15,10 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.Sem.UserKeyStore.Cassandra (keyStoreToCassandra) where +module Brig.Sem.UserKeyStore.Cassandra (userKeyStoreToCassandra) where -import Brig.Data.UserKey -import Brig.Email -import Brig.Phone import Brig.Sem.UserKeyStore +import Brig.Types.Common import Cassandra import qualified Data.ByteString as B import Data.Id @@ -30,12 +28,12 @@ import Imports import OpenSSL.EVP.Digest import Polysemy -keyStoreToCassandra :: +userKeyStoreToCassandra :: forall m r a. (MonadClient m, Member (Embed m) r) => Sem (UserKeyStore ': r) a -> Sem r a -keyStoreToCassandra = +userKeyStoreToCassandra = interpret $ embed @m . \case GetKey uid -> lookupKeyQuery uid diff --git a/services/brig/src/Brig/Sem/UserQuery.hs b/services/brig/src/Brig/Sem/UserQuery.hs index 61b3378174..7a9ad422be 100644 --- a/services/brig/src/Brig/Sem/UserQuery.hs +++ b/services/brig/src/Brig/Sem/UserQuery.hs @@ -34,6 +34,7 @@ module Brig.Sem.UserQuery insertAccount, updateUser, updateEmail, + updateHandle, updatePhone, activateUser, deleteEmailUnvalidated, @@ -269,6 +270,16 @@ toIdentity False _ _ _ = Nothing ------------------------------------------------------------------------------- data UserQuery m a where + -- FUTUREWORK: The 'InsertAccount' action should perhaps be in an account store effect + InsertAccount :: + UserAccount -> + -- | If a bot: conversation and team + -- (if a team conversation) + Maybe (ConvId, Maybe TeamId) -> + Maybe Password -> + -- | Whether the user is activated + Bool -> + UserQuery m () GetId :: UserId -> UserQuery m (Maybe UserId) -- idSelect GetUsers :: [UserId] -> UserQuery m [UserRow] -- usersSelect GetName :: UserId -> UserQuery m (Maybe Name) -- nameSelect @@ -284,18 +295,9 @@ data UserQuery m a where -- | Whether the account has been activated by verifying an email address or -- phone number. IsActivated :: UserId -> UserQuery m Bool - -- FUTUREWORK: The 'InsertAccount' action should perhaps be in an account store effect - InsertAccount :: - UserAccount -> - -- | If a bot: conversation and team - -- (if a team conversation) - Maybe (ConvId, Maybe TeamId) -> - Maybe Password -> - -- | Whether the user is activated - Bool -> - UserQuery m () UpdateUser :: UserId -> UserUpdate -> UserQuery m () UpdateEmail :: UserId -> Email -> UserQuery m () + UpdateHandle :: UserId -> Handle -> UserQuery m () UpdatePhone :: UserId -> Phone -> UserQuery m () ActivateUser :: UserId -> UserIdentity -> UserQuery m () DeleteEmailUnvalidated :: UserId -> UserQuery m () diff --git a/services/brig/src/Brig/Sem/UserQuery/Cassandra.hs b/services/brig/src/Brig/Sem/UserQuery/Cassandra.hs index 494c23dda7..205012934b 100644 --- a/services/brig/src/Brig/Sem/UserQuery/Cassandra.hs +++ b/services/brig/src/Brig/Sem/UserQuery/Cassandra.hs @@ -25,6 +25,7 @@ import Brig.Types import Brig.Types.Intra import Cassandra import Control.Lens (view, (^.)) +import Data.Handle import Data.Id import Imports import Polysemy @@ -53,6 +54,7 @@ userQueryToCassandra = InsertAccount ua mConvTeam mPass act -> accountInsert ua mConvTeam mPass act UpdateUser uid update -> userUpdate uid update UpdateEmail uid email -> updateEmailQuery uid email + UpdateHandle uid handle -> updateHandleQuery uid handle UpdatePhone uid phone -> updatePhoneQuery uid phone ActivateUser uid ui -> activateUserQuery uid ui DeleteEmailUnvalidated uid -> deleteEmailUnvalidatedQuery uid @@ -212,3 +214,9 @@ activateUserQuery u ident = do where userActivatedUpdate :: PrepQuery W (Maybe Email, Maybe Phone, UserId) () userActivatedUpdate = "UPDATE user SET activated = true, email = ?, phone = ? WHERE id = ?" + +updateHandleQuery :: MonadClient m => UserId -> Handle -> m () +updateHandleQuery u h = retry x5 $ write userHandleUpdate (params LocalQuorum (h, u)) + where + userHandleUpdate :: PrepQuery W (Handle, UserId) () + userHandleUpdate = "UPDATE user SET handle = ? WHERE id = ?" diff --git a/services/brig/src/Brig/Sem/VerificationCodeStore.hs b/services/brig/src/Brig/Sem/VerificationCodeStore.hs new file mode 100644 index 0000000000..8f08eba2a4 --- /dev/null +++ b/services/brig/src/Brig/Sem/VerificationCodeStore.hs @@ -0,0 +1,91 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.VerificationCodeStore where + +import Brig.Types.Code +import Data.UUID +import Imports +import Polysemy +import Wire.API.User.Identity + +data Code = Code + { codeKey :: !Key, + codeScope :: !Scope, + codeValue :: !Value, + codeRetries :: !Retries, + codeTTL :: !Timeout, + codeFor :: !CodeFor, + codeAccount :: !(Maybe UUID) + } + deriving (Eq, Show) + +data CodeFor + = ForEmail !Email + | ForPhone !Phone + deriving (Eq, Show) + +-- | The same 'Key' can exist with different 'Value's in different +-- 'Scope's at the same time. +data Scope + = AccountDeletion + | IdentityVerification + | PasswordReset + | AccountLogin + | AccountApproval + | CreateScimToken + | DeleteTeam + deriving (Eq, Show) + +newtype Retries = Retries {numRetries :: Word8} + deriving (Eq, Show, Ord, Num, Integral, Enum, Real) + +data VerificationCodeStore m a where + GetPendingCode :: Key -> Scope -> VerificationCodeStore m (Maybe Code) -- 'lookup' in 'Brig.Code' + InsertCode :: Code -> VerificationCodeStore m () -- 'insert' in 'Brig.Code' + +makeSem ''VerificationCodeStore + +-- | Lookup and verify the code for the given key and scope +-- against the given value. +verifyCode :: + Member VerificationCodeStore r => + Key -> + Scope -> + Value -> + Sem r (Maybe Code) +verifyCode k s v = getPendingCode k s >>= maybe (pure Nothing) continue + where + continue c + | codeValue c == v = pure (Just c) + | codeRetries c > 0 = do + insertCode (c {codeRetries = codeRetries c - 1}) + pure Nothing + | otherwise = pure Nothing + +codeForEmail :: Code -> Maybe Email +codeForEmail c + | ForEmail e <- codeFor c = Just e + | otherwise = Nothing + +codeForPhone :: Code -> Maybe Phone +codeForPhone c + | ForPhone p <- codeFor c = Just p + | otherwise = Nothing diff --git a/services/brig/src/Brig/Sem/VerificationCodeStore/Cassandra.hs b/services/brig/src/Brig/Sem/VerificationCodeStore/Cassandra.hs new file mode 100644 index 0000000000..9e769e2874 --- /dev/null +++ b/services/brig/src/Brig/Sem/VerificationCodeStore/Cassandra.hs @@ -0,0 +1,110 @@ +-- 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 . +{-# OPTIONS_GHC -Wno-orphans #-} + +module Brig.Sem.VerificationCodeStore.Cassandra (verificationCodeStoreToCassandra) where + +import Brig.Data.Instances () +import Brig.Sem.VerificationCodeStore +import Cassandra hiding (Value) +import Data.Code +import Data.UUID +import Imports +import Polysemy +import Wire.API.User.Identity + +instance Cql Scope where + ctype = Tagged IntColumn + + toCql AccountDeletion = CqlInt 1 + toCql IdentityVerification = CqlInt 2 + toCql PasswordReset = CqlInt 3 + toCql AccountLogin = CqlInt 4 + toCql AccountApproval = CqlInt 5 + toCql CreateScimToken = CqlInt 6 + toCql DeleteTeam = CqlInt 7 + + fromCql (CqlInt 1) = pure AccountDeletion + fromCql (CqlInt 2) = pure IdentityVerification + fromCql (CqlInt 3) = pure PasswordReset + fromCql (CqlInt 4) = pure AccountLogin + fromCql (CqlInt 5) = pure AccountApproval + fromCql (CqlInt 6) = pure CreateScimToken + fromCql (CqlInt 7) = pure DeleteTeam + fromCql _ = Left "fromCql: Scope: int expected" + +instance Cql Retries where + ctype = Tagged IntColumn + toCql = CqlInt . fromIntegral . numRetries + fromCql (CqlInt n) = pure (Retries (fromIntegral n)) + fromCql _ = Left "fromCql: Retries: int expected" + +verificationCodeStoreToCassandra :: + forall m r a. + (MonadClient m, Member (Embed m) r) => + Sem (VerificationCodeStore ': r) a -> + Sem r a +verificationCodeStoreToCassandra = + interpret $ + embed @m . \case + x -> case x of + GetPendingCode key scope -> lookupCode key scope + InsertCode code -> insert code + +-- | Lookup a pending code. +lookupCode :: MonadClient m => Key -> Scope -> m (Maybe Code) +lookupCode k s = fmap (toCode k s) <$> retry x1 (query1 cql (params LocalQuorum (k, s))) + where + cql :: PrepQuery R (Key, Scope) (Value, Int32, Retries, Maybe Email, Maybe Phone, Maybe UUID) + cql = + "SELECT value, ttl(value), retries, email, phone, account \ + \FROM vcodes WHERE key = ? AND scope = ?" + +insert :: MonadClient m => Code -> m () +insert c = do + let k = codeKey c + let s = codeScope c + let v = codeValue c + let r = fromIntegral (codeRetries c) + let a = codeAccount c + let e = codeForEmail c + let p = codeForPhone c + let t = round (codeTTL c) + retry x5 (write cql (params LocalQuorum (k, s, v, r, e, p, a, t))) + where + cql :: PrepQuery W (Key, Scope, Value, Retries, Maybe Email, Maybe Phone, Maybe UUID, Int32) () + cql = + "INSERT INTO vcodes (key, scope, value, retries, email, phone, account) \ + \VALUES (?, ?, ?, ?, ?, ?, ?) USING TTL ?" + +toCode :: Key -> Scope -> (Value, Int32, Retries, Maybe Email, Maybe Phone, Maybe UUID) -> Code +toCode k s (val, ttl, retries, email, phone, account) = + let ek = ForEmail <$> email + pk = ForPhone <$> phone + to = Timeout (fromIntegral ttl) + in case ek <|> pk of + Nothing -> error "toCode: email or phone must be present" + Just cf -> + Code + { codeKey = k, + codeScope = s, + codeValue = val, + codeTTL = to, + codeRetries = retries, + codeFor = cf, + codeAccount = account + } diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 84ecb19000..fc9bbe752b 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -34,6 +34,8 @@ import qualified Brig.Email as Email import qualified Brig.IO.Intra as Intra import Brig.Options (setMaxTeamSize, setTeamInvitationTimeout) import qualified Brig.Phone as Phone +import Brig.Sem.Twilio (Twilio) +import Brig.Sem.UserKeyStore (UserKeyStore) import Brig.Sem.UserQuery (UserQuery) import qualified Brig.Team.DB as DB import Brig.Team.Email @@ -64,7 +66,9 @@ import Network.Wai.Utilities hiding (code, message) import Network.Wai.Utilities.Swagger (document) import qualified Network.Wai.Utilities.Swagger as Doc import Polysemy +import qualified Polysemy.Error as P import Polysemy.Input +import qualified Ropes.Twilio as Twilio import System.Logger (Msg) import qualified System.Logger.Class as Log import Util.Logging (logFunction, logTeam) @@ -76,7 +80,14 @@ import qualified Wire.API.Team.Size as Public import qualified Wire.API.User as Public routesPublic :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ Input (Local ()), + P.Error Twilio.ErrorResponse, + Twilio, + UserKeyStore, + UserQuery + ] + r => Routes Doc.ApiBuilder (Handler r) () routesPublic = do post "/teams/:tid/invitations" (continue createInvitationPublicH) $ @@ -187,7 +198,15 @@ routesPublic = do Doc.response 200 "Invitation successful." Doc.end Doc.response 403 "No permission (not admin or owner of this team)." Doc.end -routesInternal :: Routes a (Handler r) () +routesInternal :: + Members + '[ P.Error Twilio.ErrorResponse, + Twilio, + UserKeyStore, + UserQuery + ] + r => + Routes a (Handler r) () routesInternal = do get "/i/teams/invitations/by-email" (continue getInvitationByEmailH) $ accept "application" "json" @@ -244,7 +263,14 @@ instance ToJSON FoundInvitationCode where toJSON (FoundInvitationCode c) = object ["code" .= c] createInvitationPublicH :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ Input (Local ()), + P.Error Twilio.ErrorResponse, + Twilio, + UserKeyStore, + UserQuery + ] + r => JSON ::: UserId ::: TeamId ::: JsonRequest Public.InvitationRequest -> Handler r Response createInvitationPublicH (_ ::: uid ::: tid ::: req) = do @@ -263,7 +289,14 @@ data CreateInvitationInviter = CreateInvitationInviter deriving (Eq, Show) createInvitationPublic :: - Members '[Input (Local ()), UserQuery] r => + Members + '[ Input (Local ()), + P.Error Twilio.ErrorResponse, + Twilio, + UserKeyStore, + UserQuery + ] + r => UserId -> TeamId -> Public.InvitationRequest -> @@ -287,12 +320,30 @@ createInvitationPublic uid tid body = do context (createInvitation' tid inviteeRole (Just (inviterUid inviter)) (inviterEmail inviter) body) -createInvitationViaScimH :: JSON ::: JsonRequest NewUserScimInvitation -> (Handler r) Response +createInvitationViaScimH :: + Members + '[ P.Error Twilio.ErrorResponse, + Twilio, + UserKeyStore, + UserQuery + ] + r => + JSON ::: JsonRequest NewUserScimInvitation -> + Handler r Response createInvitationViaScimH (_ ::: req) = do body <- parseJsonBody req setStatus status201 . json <$> createInvitationViaScim body -createInvitationViaScim :: NewUserScimInvitation -> (Handler r) UserAccount +createInvitationViaScim :: + Members + '[ P.Error Twilio.ErrorResponse, + Twilio, + UserKeyStore, + UserQuery + ] + r => + NewUserScimInvitation -> + Handler r UserAccount createInvitationViaScim newUser@(NewUserScimInvitation tid loc name email) = do env <- ask let inviteeRole = Team.defaultRole @@ -330,7 +381,19 @@ logInvitationRequest context action = Log.info $ (context . logInvitationCode code) . Log.msg @Text "Successfully created invitation" pure (Right result) -createInvitation' :: TeamId -> Public.Role -> Maybe UserId -> Email -> Public.InvitationRequest -> (Handler r) (Public.Invitation, Public.InvitationCode) +createInvitation' :: + Members + '[ P.Error Twilio.ErrorResponse, + Twilio, + UserKeyStore + ] + r => + TeamId -> + Public.Role -> + Maybe UserId -> + Email -> + Public.InvitationRequest -> + Handler r (Public.Invitation, Public.InvitationCode) createInvitation' tid inviteeRole mbInviterUid fromEmail body = do -- FUTUREWORK: These validations are nearly copy+paste from accountCreation and -- sendActivationCode. Refactor this to a single place @@ -341,18 +404,21 @@ createInvitation' tid inviteeRole mbInviterUid fromEmail body = do blacklistedEm <- lift $ wrapClient $ Blacklist.exists uke when blacklistedEm $ throwStd blacklistedEmail - emailTaken <- lift $ isJust <$> wrapClient (Data.lookupKey uke) + emailTaken <- lift $ isJust <$> liftSem (Data.getKey uke) when emailTaken $ throwStd emailExists -- Validate phone inviteePhone <- for (irInviteePhone body) $ \p -> do - validatedPhone <- maybe (throwStd (errorToWai @'E.InvalidPhone)) pure =<< lift (wrapClient $ Phone.validatePhone p) + c <- view twilioCreds + m <- view httpManager + validatedPhone <- + maybe (throwStd (errorToWai @'E.InvalidPhone)) pure =<< lift (liftSem $ Phone.validatePhone c m p) let ukp = userPhoneKey validatedPhone blacklistedPh <- lift $ wrapClient $ Blacklist.exists ukp when blacklistedPh $ throwStd (errorToWai @'E.BlacklistedPhone) - phoneTaken <- lift $ isJust <$> wrapClient (Data.lookupKey ukp) + phoneTaken <- lift $ isJust <$> liftSem (Data.getKey ukp) when phoneTaken $ throwStd phoneExists pure validatedPhone diff --git a/services/brig/src/Brig/Unique.hs b/services/brig/src/Brig/Unique.hs index 88e325e8c4..ef0b29ec49 100644 --- a/services/brig/src/Brig/Unique.hs +++ b/services/brig/src/Brig/Unique.hs @@ -19,8 +19,7 @@ -- to contention, i.e. where strong guarantees on uniqueness are desired. module Brig.Unique ( withClaim, - deleteClaim, - lookupClaims, + getClaims, -- * Re-exports Timeout, @@ -30,11 +29,16 @@ module Brig.Unique where import Brig.Data.Instances () -import Cassandra as C -import Control.Concurrent.Timeout +import Brig.Sem.UniqueClaimsStore import Data.Id import Data.Timeout import Imports +import Polysemy +import Polysemy.Async +import Polysemy.Conc.Async +import Polysemy.Conc.Effect.Race +import Polysemy.Resource +import Polysemy.Time.Data.TimeUnit -- | Obtain a (temporary) exclusive claim on a 'Text' value for some -- 'Id'entifier. The claim expires after the provided timeout, whether @@ -44,7 +48,7 @@ import Imports -- and is responsible for turning the temporary claim into permanent -- ownership, if desired. withClaim :: - MonadClient m => + Members '[Async, Race, Resource, UniqueClaimsStore] r => -- | The 'Id' associated with the claim. Id a -> -- | The value on which to acquire the claim. @@ -52,60 +56,29 @@ withClaim :: -- | The minimum timeout (i.e. duration) of the claim. Timeout -> -- | The computation to run with a successful claim. - IO b -> - -- | 'Just b' if the claim was successful and the 'IO' - -- computation completed within the given timeout. - m (Maybe b) -withClaim u v t io = do - claims <- lookupClaims v + Sem r b -> + -- | 'Just b' if the claim was successful and the computation completed within + -- the given timeout. + Sem r (Maybe b) +withClaim u v t action = do + claims <- getClaims v + -- [Note: Guarantees] + let claim = do + let ttl = max minTtl t + addClaims u t v + if claims == [u] + then withAsyncWait (toPolyTime ttl) action await + else pure Nothing case claims of [] -> claim -- Free [u'] | u == u' -> claim -- Claimed by 'u' (retries are allowed). _ -> pure Nothing -- Conflicting claims, TTL must expire. - where - -- [Note: Guarantees] - claim = do - let ttl = max minTtl (fromIntegral (t #> Second)) - retry x5 $ write cql $ params LocalQuorum (ttl * 2, C.Set [u], v) - claimed <- (== [u]) <$> lookupClaims v - if claimed - then liftIO $ timeout (fromIntegral ttl # Second) io - else pure Nothing - cql :: PrepQuery W (Int32, C.Set (Id a), Text) () - cql = "UPDATE unique_claims USING TTL ? SET claims = claims + ? WHERE value = ?" -deleteClaim :: - MonadClient m => - -- | The 'Id' associated with the claim. - Id a -> - -- | The value on which to acquire the claim. - Text -> - -- | The minimum timeout (i.e. duration) of the rest of the claim. (Each - -- claim can have more than one claimer (even though this is a feature we - -- never use), so removing a claim is an update operation on the database. - -- Therefore, we reset the TTL the same way we reset it in 'withClaim'.) - Timeout -> - m () -deleteClaim u v t = do - let ttl = max minTtl (fromIntegral (t #> Second)) - retry x5 $ write cql $ params LocalQuorum (ttl * 2, C.Set [u], v) - where - cql :: PrepQuery W (Int32, C.Set (Id a), Text) () - cql = "UPDATE unique_claims USING TTL ? SET claims = claims - ? WHERE value = ?" - --- | Lookup the current claims on a value. -lookupClaims :: MonadClient m => Text -> m [Id a] -lookupClaims v = - fmap (maybe [] (fromSet . runIdentity)) $ - retry x1 $ - query1 cql $ - params LocalQuorum (Identity v) - where - cql :: PrepQuery R (Identity Text) (Identity (C.Set (Id a))) - cql = "SELECT claims FROM unique_claims WHERE value = ?" +toPolyTime :: Timeout -> NanoSeconds +toPolyTime t = NanoSeconds . fromIntegral $ t #> NanoSecond -minTtl :: Int32 -minTtl = 60 -- Seconds +minTtl :: Timeout +minTtl = 60 # Second -- [Note: Guarantees] -- ~~~~~~~~~~~~~~~~~~ diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index c6610cd7d0..e61967b967 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -28,7 +28,15 @@ import qualified Brig.API.User as User import Brig.App import Brig.Options (setDefaultUserLocale) import Brig.Phone +import Brig.Sem.ActivationKeyStore +import Brig.Sem.ActivationSupply +import Brig.Sem.BudgetStore +import Brig.Sem.GalleyAccess +import Brig.Sem.Twilio +import Brig.Sem.UserHandleStore +import Brig.Sem.UserKeyStore import Brig.Sem.UserQuery (UserQuery) +import Brig.Sem.VerificationCodeStore import Brig.Types.Intra (ReAuthUser, reAuthCode, reAuthCodeAction, reAuthPassword) import Brig.Types.User.Auth import qualified Brig.User.Auth as Auth @@ -64,6 +72,8 @@ import qualified Network.Wai.Utilities.Swagger as Doc import Polysemy import Polysemy.Error import Polysemy.Input +import qualified Polysemy.TinyLog as P +import qualified Ropes.Twilio as Twilio import Wire.API.Error import qualified Wire.API.Error.Brig as E import qualified Wire.API.User as Public @@ -71,7 +81,21 @@ import Wire.API.User.Auth as Public import Wire.Swagger as Doc (pendingLoginError) routesPublic :: - Member UserQuery r => + Members + '[ ActivationKeyStore, + ActivationSupply, + BudgetStore, + Error Twilio.ErrorResponse, + GalleyAccess, + Input (Local ()), + P.TinyLog, + Twilio, + UserHandleStore, + UserKeyStore, + UserQuery, + VerificationCodeStore + ] + r => Routes Doc.ApiBuilder (Handler r) () routesPublic = do -- Note: this endpoint should always remain available at its unversioned @@ -199,7 +223,16 @@ routesPublic = do Doc.errorResponse (errorToWai @'E.BadCredentials) routesInternal :: - Members '[Error ReAuthError, Input (Local ()), UserQuery] r => + Members + '[ Error ReAuthError, + GalleyAccess, + Input (Local ()), + P.TinyLog, + UserKeyStore, + UserQuery, + VerificationCodeStore + ] + r => Routes a (Handler r) () routesInternal = do -- galley can query this endpoint at the right moment in the LegalHold flow @@ -222,29 +255,57 @@ routesInternal = do -- Handlers -sendLoginCodeH :: JsonRequest Public.SendLoginCode -> (Handler r) Response +sendLoginCodeH :: + Members + '[ Error Twilio.ErrorResponse, + P.TinyLog, + UserKeyStore, + UserQuery, + Twilio + ] + r => + JsonRequest Public.SendLoginCode -> + Handler r Response sendLoginCodeH req = do json <$> (sendLoginCode =<< parseJsonBody req) -sendLoginCode :: Public.SendLoginCode -> (Handler r) Public.LoginCodeTimeout +sendLoginCode :: + Members + '[ Error Twilio.ErrorResponse, + P.TinyLog, + UserKeyStore, + UserQuery, + Twilio + ] + r => + Public.SendLoginCode -> + Handler r Public.LoginCodeTimeout sendLoginCode (Public.SendLoginCode phone call force) = do checkWhitelist (Right phone) - c <- wrapClientE (Auth.sendLoginCode phone call force) !>> sendLoginCodeError + c <- Auth.sendLoginCode phone call force !>> sendLoginCodeError pure $ Public.LoginCodeTimeout (pendingLoginTimeout c) -getLoginCodeH :: JSON ::: Phone -> (Handler r) Response +getLoginCodeH :: + Members '[P.TinyLog, UserKeyStore] r => + JSON ::: Phone -> + Handler r Response getLoginCodeH (_ ::: phone) = json <$> getLoginCode phone -getLoginCode :: Phone -> (Handler r) Public.PendingLoginCode +getLoginCode :: + Members '[P.TinyLog, UserKeyStore] r => + Phone -> + Handler r Public.PendingLoginCode getLoginCode phone = do - code <- lift $ wrapClient $ Auth.lookupLoginCode phone + code <- lift $ Auth.lookupLoginCode phone maybe (throwStd loginCodeNotFound) pure code reAuthUserH :: Members '[ Error ReAuthError, + GalleyAccess, Input (Local ()), - UserQuery + UserQuery, + VerificationCodeStore ] r => UserId ::: JsonRequest ReAuthUser -> @@ -256,8 +317,10 @@ reAuthUserH (uid ::: req) = do reAuthUser :: Members '[ Error ReAuthError, + GalleyAccess, Input (Local ()), - UserQuery + UserQuery, + VerificationCodeStore ] r => UserId -> @@ -268,21 +331,52 @@ reAuthUser uid body = do lift (liftSem (User.reauthenticate locale uid (reAuthPassword body))) !>> reauthError case reAuthCodeAction body of Just action -> - wrapHttpClientE (Auth.verifyCode (reAuthCode body) action uid) + Auth.verifyCode (reAuthCode body) action uid `catchE` \case VerificationCodeRequired -> throwE $ reauthError ReAuthCodeVerificationRequired VerificationCodeNoPendingCode -> throwE $ reauthError ReAuthCodeVerificationNoPendingCode VerificationCodeNoEmail -> throwE $ reauthError ReAuthCodeVerificationNoEmail Nothing -> pure () -loginH :: JsonRequest Public.Login ::: Bool ::: JSON -> (Handler r) Response +loginH :: + Members + '[ BudgetStore, + Error Twilio.ErrorResponse, + GalleyAccess, + Input (Local ()), + P.TinyLog, + Twilio, + UserHandleStore, + UserKeyStore, + UserQuery, + VerificationCodeStore + ] + r => + JsonRequest Public.Login ::: Bool ::: JSON -> + Handler r Response loginH (req ::: persist ::: _) = do lift . tokenResponse =<< flip login persist =<< parseJsonBody req -login :: Public.Login -> Bool -> (Handler r) (Auth.Access ZAuth.User) +login :: + Members + '[ BudgetStore, + Error Twilio.ErrorResponse, + GalleyAccess, + Input (Local ()), + P.TinyLog, + Twilio, + UserHandleStore, + UserKeyStore, + UserQuery, + VerificationCodeStore + ] + r => + Public.Login -> + Bool -> + (Handler r) (Auth.Access ZAuth.User) login l persist = do let typ = if persist then PersistentCookie else SessionCookie - wrapHttpClientE (Auth.login l typ) !>> loginError + Auth.login l typ !>> loginError ssoLoginH :: JsonRequest SsoLogin ::: Bool ::: JSON -> (Handler r) Response ssoLoginH (req ::: persist ::: _) = do @@ -319,12 +413,18 @@ logout (Just (Left ut)) (Just (Left at)) = wrapHttpClientE (Auth.logout ut at) ! logout (Just (Right ut)) (Just (Right at)) = wrapHttpClientE (Auth.logout ut at) !>> zauthError changeSelfEmailH :: - Member UserQuery r => + Members + '[ ActivationKeyStore, + ActivationSupply, + UserKeyStore, + UserQuery + ] + r => JSON ::: JsonRequest Public.EmailUpdate ::: Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) ::: Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> - (Handler r) Response + Handler r Response changeSelfEmailH (_ ::: req ::: ckies ::: toks) = do usr <- validateCredentials ckies toks email <- Public.euEmail <$> parseJsonBody req @@ -335,7 +435,7 @@ changeSelfEmailH (_ ::: req ::: ckies ::: toks) = do validateCredentials :: Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) -> Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> - (Handler r) UserId + Handler r UserId validateCredentials = \case Nothing -> const $ throwStd authMissingCookie diff --git a/services/brig/src/Brig/User/API/Handle.hs b/services/brig/src/Brig/User/API/Handle.hs index db71c04406..47ccbaa8d9 100644 --- a/services/brig/src/Brig/User/API/Handle.hs +++ b/services/brig/src/Brig/User/API/Handle.hs @@ -30,19 +30,25 @@ import Brig.App import qualified Brig.Data.User as Data import qualified Brig.Federation.Client as Federation import Brig.Options (searchSameTeamOnly) +import Brig.Sem.UserHandleStore import Control.Lens (view) import Data.Handle (Handle, fromHandle) import Data.Id (UserId) import Data.Qualified import Imports import Network.Wai.Utilities ((!>>)) +import Polysemy import qualified System.Logger.Class as Log import Wire.API.User import qualified Wire.API.User as Public import Wire.API.User.Search import qualified Wire.API.User.Search as Public -getHandleInfo :: UserId -> Qualified Handle -> (Handler r) (Maybe Public.UserProfile) +getHandleInfo :: + Member UserHandleStore r => + UserId -> + Qualified Handle -> + Handler r (Maybe Public.UserProfile) getHandleInfo self handle = do lself <- qualifyLocal self foldQualified @@ -58,10 +64,14 @@ getRemoteHandleInfo handle = do . Log.field "domain" (show (tDomain handle)) Federation.getUserHandleInfo handle !>> fedError -getLocalHandleInfo :: Local UserId -> Handle -> (Handler r) (Maybe Public.UserProfile) +getLocalHandleInfo :: + Member UserHandleStore r => + Local UserId -> + Handle -> + Handler r (Maybe Public.UserProfile) getLocalHandleInfo self handle = do lift . Log.info $ Log.msg $ Log.val "getHandleInfo - local lookup" - maybeOwnerId <- lift . wrapClient $ API.lookupHandle handle + maybeOwnerId <- lift . liftSem $ API.lookupHandle handle case maybeOwnerId of Nothing -> pure Nothing Just ownerId -> do diff --git a/services/brig/src/Brig/User/API/Search.hs b/services/brig/src/Brig/User/API/Search.hs index 50ab3490aa..4ad3cc28a6 100644 --- a/services/brig/src/Brig/User/API/Search.hs +++ b/services/brig/src/Brig/User/API/Search.hs @@ -29,6 +29,7 @@ import qualified Brig.Data.User as DB import qualified Brig.Federation.Client as Federation import qualified Brig.IO.Intra as Intra import qualified Brig.Options as Opts +import Brig.Sem.UserHandleStore import Brig.Team.Util (ensurePermissions) import Brig.Types.Search as Search import qualified Brig.User.API.Handle as HandleAPI @@ -51,6 +52,7 @@ import Network.Wai.Routing import Network.Wai.Utilities ((!>>)) import Network.Wai.Utilities.Response (empty, json) import Network.Wai.Utilities.Swagger (document) +import Polysemy import System.Logger (field, msg) import System.Logger.Class (val, (~~)) import qualified System.Logger.Class as Log @@ -122,7 +124,13 @@ routesInternal = do -- FUTUREWORK: Consider augmenting 'SearchResult' with full user profiles -- for all results. This is tracked in https://wearezeta.atlassian.net/browse/SQCORE-599 -search :: UserId -> Text -> Maybe Domain -> Maybe (Range 1 500 Int32) -> (Handler r) (Public.SearchResult Public.Contact) +search :: + Members '[UserHandleStore] r => + UserId -> + Text -> + Maybe Domain -> + Maybe (Range 1 500 Int32) -> + Handler r (Public.SearchResult Public.Contact) search searcherId searchTerm maybeDomain maybeMaxResults = do federationDomain <- viewFederationDomain let queryDomain = fromMaybe federationDomain maybeDomain @@ -148,7 +156,13 @@ searchRemotely domain searchTerm = do searchPolicy = S.searchPolicy searchResponse } -searchLocally :: UserId -> Text -> Maybe (Range 1 500 Int32) -> (Handler r) (Public.SearchResult Public.Contact) +searchLocally :: + forall r. + Members '[UserHandleStore] r => + UserId -> + Text -> + Maybe (Range 1 500 Int32) -> + Handler r (Public.SearchResult Public.Contact) searchLocally searcherId searchTerm maybeMaxResults = do let maxResults = maybe 15 (fromIntegral . fromRange) maybeMaxResults searcherTeamId <- lift $ wrapClient $ DB.lookupUserTeam searcherId @@ -189,7 +203,7 @@ searchLocally searcherId searchTerm maybeMaxResults = do -- For team users, we need to check the visibility flag handleTeamVisibility t <$> wrapHttp (Intra.getTeamSearchVisibility t) - exactHandleSearch :: (Handler r) (Maybe Contact) + exactHandleSearch :: Handler r (Maybe Contact) exactHandleSearch = do lsearcherId <- qualifyLocal searcherId case parseHandle searchTerm of diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index d262b8f832..7093edfac1 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -52,15 +52,18 @@ import qualified Brig.IO.Intra as Intra import qualified Brig.Options as Opt import Brig.Phone import Brig.Sem.BudgetStore -import Brig.Sem.BudgetStore.Cassandra +import Brig.Sem.GalleyAccess +import Brig.Sem.Twilio (Twilio) +import Brig.Sem.UserHandleStore +import Brig.Sem.UserKeyStore (UserKeyStore) import Brig.Sem.UserQuery (UserQuery) import Brig.Sem.UserQuery.Cassandra +import Brig.Sem.VerificationCodeStore (VerificationCodeStore) import Brig.Types.Common import Brig.Types.Intra import Brig.Types.User import Brig.Types.User.Auth hiding (user) import Brig.User.Auth.Cookie -import Brig.User.Handle import Brig.User.Phone import Brig.User.Search.Index import qualified Brig.ZAuth as ZAuth @@ -77,12 +80,16 @@ import qualified Data.List.NonEmpty as NE import Data.List1 (List1) import qualified Data.List1 as List1 import Data.Misc (PlainTextPassword (..)) +import Data.Qualified import qualified Data.ZAuth.Token as ZAuth import Imports import Network.Wai.Utilities.Error ((!>>)) import Polysemy import Polysemy.Error +import qualified Polysemy.Error as P import Polysemy.Input +import qualified Polysemy.TinyLog as P +import qualified Ropes.Twilio as Twilio import System.Logger (field, msg, val, (~~)) import qualified System.Logger.Class as Log import Wire.API.Team.Feature (TeamFeatureStatusNoConfig (..), TeamFeatureStatusValue (..)) @@ -95,142 +102,160 @@ data Access u = Access } sendLoginCode :: - forall m. - ( MonadClient m, - MonadReader Env m, - MonadCatch m, - Log.MonadLogger m - ) => + forall r. + Members + '[ Error Twilio.ErrorResponse, + P.TinyLog, + Twilio, + UserKeyStore, + UserQuery + ] + r => Phone -> Bool -> Bool -> - ExceptT SendLoginCodeError m PendingLoginCode + ExceptT SendLoginCodeError (AppT r) PendingLoginCode sendLoginCode phone call force = do + creds <- view twilioCreds + m <- view httpManager pk <- maybe (throwE $ SendLoginInvalidPhone phone) (pure . userPhoneKey) - =<< lift (validatePhone phone) - user <- lift $ Data.lookupKey pk + =<< lift (liftSem $ validatePhone creds m phone) + user <- lift . liftSem $ Data.getKey pk case user of Nothing -> throwE $ SendLoginInvalidPhone phone Just u -> do - lift . Log.debug $ field "user" (toByteString u) . field "action" (Log.val "User.sendLoginCode") - pw <- lift $ Data.lookupPassword u + lift . liftSem + . P.debug + $ field "user" (toByteString u) . field "action" (Log.val "User.sendLoginCode") + pw <- lift . wrapClient $ Data.lookupPassword u unless (isNothing pw || force) $ throwE SendLoginPasswordExists lift $ do defLoc <- Opt.setDefaultUserLocale <$> view settings - l <- runM $ userQueryToCassandra @m @'[Embed m] $ Data.lookupLocale defLoc u - c <- Data.createLoginCode u + l <- liftSem $ Data.lookupLocale defLoc u + c <- wrapClient $ Data.createLoginCode u void . forPhoneKey pk $ \ph -> - if call - then sendLoginCall ph (pendingLoginCode c) l - else sendLoginSms ph (pendingLoginCode c) l + wrapClient $ + if call + then sendLoginCall ph (pendingLoginCode c) l + else sendLoginSms ph (pendingLoginCode c) l pure c lookupLoginCode :: - ( MonadClient m, - Log.MonadLogger m, - MonadReader Env m - ) => + Members + '[ P.TinyLog, + UserKeyStore + ] + r => Phone -> - m (Maybe PendingLoginCode) + AppT r (Maybe PendingLoginCode) lookupLoginCode phone = - Data.lookupKey (userPhoneKey phone) >>= \case + liftSem (Data.getKey (userPhoneKey phone)) >>= \case Nothing -> pure Nothing Just u -> do - Log.debug $ field "user" (toByteString u) . field "action" (Log.val "User.lookupLoginCode") - Data.lookupLoginCode u + liftSem $ P.debug $ field "user" (toByteString u) . field "action" (Log.val "User.lookupLoginCode") + wrapClient $ Data.lookupLoginCode u login :: - forall m. - ( MonadReader Env m, - MonadMask m, - MonadHttp m, - MonadIO m, - HasRequestId m, - Log.MonadLogger m, - MonadClient m, - ZAuth.MonadZAuth m, - MonadIndexIO m, - MonadUnliftIO m - ) => + forall r. + Members + '[ BudgetStore, + Error Twilio.ErrorResponse, + GalleyAccess, + Input (Local ()), + P.TinyLog, + Twilio, + UserHandleStore, + UserKeyStore, + UserQuery, + VerificationCodeStore + ] + r => Login -> CookieType -> - ExceptT LoginError m (Access ZAuth.User) + ExceptT LoginError (AppT r) (Access ZAuth.User) login (PasswordLogin li pw label code) typ = do - uid <- resolveLoginId li - lift . Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.login") + c <- view twilioCreds + man <- view httpManager + uid <- resolveLoginId c man li + lift . liftSem . P.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.login") mLimitFailedLogins <- view (settings . to Opt.setLimitFailedLogins) - runBudgetStoreAction $ checkRetryLimit uid mLimitFailedLogins - o <- runUserQueryAction $ Data.authenticate uid pw + liftSemE . semErrToExceptT $ checkRetryLimit uid mLimitFailedLogins + o <- lift . liftSem . runError @AuthError $ Data.authenticate uid pw whenLeft o $ \case - AuthInvalidUser -> runBudgetStoreAction $ loginFailed uid mLimitFailedLogins - AuthInvalidCredentials -> runBudgetStoreAction $ loginFailed uid mLimitFailedLogins + AuthInvalidUser -> liftSemE . semErrToExceptT $ loginFailed uid mLimitFailedLogins + AuthInvalidCredentials -> liftSemE . semErrToExceptT $ loginFailed uid mLimitFailedLogins AuthSuspended -> throwE LoginSuspended AuthEphemeral -> throwE LoginEphemeral AuthPendingInvitation -> throwE LoginPendingActivation verifyLoginCode code uid mLimitFailedLogins - newAccess @ZAuth.User @ZAuth.Access uid typ label + wrapHttpClientE $ newAccess @ZAuth.User @ZAuth.Access uid typ label where - verifyLoginCode :: Maybe Code.Value -> UserId -> Maybe Opt.LimitFailedLogins -> ExceptT LoginError m () + verifyLoginCode :: + Maybe Code.Value -> + UserId -> + Maybe Opt.LimitFailedLogins -> + ExceptT LoginError (AppT r) () verifyLoginCode mbCode uid mLimitFailedLogins = verifyCode mbCode Login uid `catchE` \case - VerificationCodeNoPendingCode -> runBudgetStoreAction $ loginFailedWith LoginCodeInvalid uid mLimitFailedLogins - VerificationCodeRequired -> runBudgetStoreAction $ loginFailedWith LoginCodeRequired uid mLimitFailedLogins - VerificationCodeNoEmail -> runBudgetStoreAction $ loginFailed uid mLimitFailedLogins + VerificationCodeNoPendingCode -> liftSemE . semErrToExceptT $ loginFailedWith LoginCodeInvalid uid mLimitFailedLogins + VerificationCodeRequired -> liftSemE . semErrToExceptT $ loginFailedWith LoginCodeRequired uid mLimitFailedLogins + VerificationCodeNoEmail -> liftSemE . semErrToExceptT $ loginFailed uid mLimitFailedLogins login (SmsLogin phone code label) typ = do - uid <- resolveLoginId (LoginByPhone phone) + c <- view twilioCreds + man <- view httpManager + uid <- resolveLoginId c man (LoginByPhone phone) lift . Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.login") mLimitFailedLogins <- view (settings . to Opt.setLimitFailedLogins) - runBudgetStoreAction $ checkRetryLimit uid mLimitFailedLogins - ok <- lift $ Data.verifyLoginCode uid code - unless ok $ - runBudgetStoreAction $ loginFailed uid mLimitFailedLogins - newAccess @ZAuth.User @ZAuth.Access uid typ label + e <- lift . liftSem . runError $ checkRetryLimit uid mLimitFailedLogins + whenLeft e throwE + ok <- lift . wrapClient $ Data.verifyLoginCode uid code + unless ok $ do + r <- lift . liftSem . runError $ loginFailed uid mLimitFailedLogins + whenLeft r throwE + wrapHttpClientE $ newAccess @ZAuth.User @ZAuth.Access uid typ label verifyCode :: - forall m. - ( MonadReader Env m, - MonadMask m, - MonadHttp m, - MonadIO m, - HasRequestId m, - Log.MonadLogger m, - MonadClient m - ) => + forall r. + Members + '[ GalleyAccess, + Input (Local ()), + UserQuery, + VerificationCodeStore + ] + r => Maybe Code.Value -> VerificationAction -> UserId -> - ExceptT VerificationCodeError m () + ExceptT VerificationCodeError (AppT r) () verifyCode mbCode action uid = do (mbEmail, mbTeamId) <- getEmailAndTeamId uid - featureEnabled <- lift $ do - mbFeatureEnabled <- Intra.getVerificationCodeEnabled `traverse` mbTeamId - pure $ fromMaybe (Public.tfwoapsStatus (Public.defTeamFeatureStatus @'Public.TeamFeatureSndFactorPasswordChallenge) == Public.TeamFeatureEnabled) mbFeatureEnabled + featureEnabled <- lift . liftSem $ do + mbFeatureEnabled <- getTeamSndFactorPasswordChallenge `traverse` mbTeamId + pure $ + maybe + (Public.tfwoapsStatus (Public.defTeamFeatureStatus @'Public.TeamFeatureSndFactorPasswordChallenge) == Public.TeamFeatureEnabled) + (== TeamFeatureEnabled) + mbFeatureEnabled when featureEnabled $ do case (mbCode, mbEmail) of (Just code, Just email) -> do key <- Code.mkKey $ Code.ForEmail email - codeValid <- isJust <$> Code.verify key (Code.scopeFromAction action) code + codeValid <- lift . liftSem $ isJust <$> Code.verifyCode key (Code.scopeFromAction action) code unless codeValid $ throwE VerificationCodeNoPendingCode (Nothing, _) -> throwE VerificationCodeRequired (_, Nothing) -> throwE VerificationCodeNoEmail where getEmailAndTeamId :: UserId -> - ExceptT e m (Maybe Email, Maybe TeamId) + ExceptT e (AppT r) (Maybe Email, Maybe TeamId) getEmailAndTeamId u = do - locDomain <- qualifyLocal () locale <- Opt.setDefaultUserLocale <$> view settings - mbAccount <- - lift - . runM - . userQueryToCassandra @m @'[Embed m] - . runInputConst locDomain - $ Data.lookupAccount locale u + mbAccount <- lift . liftSem $ Data.lookupAccount locale u pure (userEmail <$> accountUser =<< mbAccount, userTeam <$> accountUser =<< mbAccount) loginFailedWith :: @@ -391,30 +416,51 @@ newAccess uid ct cl = do t <- lift $ newAccessToken @u @a ck Nothing pure $ Access t (Just ck) -resolveLoginId :: (MonadClient m, MonadReader Env m) => LoginId -> ExceptT LoginError m UserId -resolveLoginId li = do - usr <- validateLoginId li >>= lift . either lookupKey lookupHandle +resolveLoginId :: + Members + '[ P.Error Twilio.ErrorResponse, + UserHandleStore, + UserKeyStore, + Twilio + ] + r => + Twilio.Credentials -> + Manager -> + LoginId -> + ExceptT LoginError (AppT r) UserId +resolveLoginId c m li = do + usr <- + liftSemE (validateLoginId c m li) + >>= lift + . either + (liftSem . getKey) + (liftSem . lookupHandle) case usr of Nothing -> do - pending <- lift $ isPendingActivation li + pending <- lift . wrapClient $ isPendingActivation li throwE $ if pending then LoginPendingActivation else LoginFailed Just uid -> pure uid -validateLoginId :: (MonadClient m, MonadReader Env m) => LoginId -> ExceptT LoginError m (Either UserKey Handle) -validateLoginId (LoginByEmail email) = +validateLoginId :: + Members '[P.Error Twilio.ErrorResponse, Twilio] r => + Twilio.Credentials -> + Manager -> + LoginId -> + ExceptT LoginError (Sem r) (Either UserKey Handle) +validateLoginId _ _ (LoginByEmail email) = either (const $ throwE LoginFailed) (pure . Left . userEmailKey) (validateEmail email) -validateLoginId (LoginByPhone phone) = +validateLoginId c m (LoginByPhone phone) = maybe (throwE LoginFailed) (pure . Left . userPhoneKey) - =<< lift (validatePhone phone) -validateLoginId (LoginByHandle h) = + =<< lift (validatePhone c m phone) +validateLoginId _ _ (LoginByHandle h) = pure (Right h) isPendingActivation :: forall m. (MonadClient m, MonadReader Env m) => LoginId -> m Bool @@ -580,13 +626,6 @@ assertLegalHoldEnabled tid = do -- These can be removed once functions in this module run in 'Sem r' instead of -- 'ExceptT e m' or 'm' for some constrained 'm'. -runBudgetStoreAction :: - forall m e a. - MonadClient m => - Sem '[Error e, BudgetStore, Embed m] a -> - ExceptT e m a -runBudgetStoreAction = runStoreActionExceptT (budgetStoreToCassandra @m) - runUserQueryAction :: forall m e a t. (MonadClient m, MonadTrans t) => @@ -602,10 +641,10 @@ runStoreAction :: t m (Either e a) runStoreAction interpreter = lift . runM . interpreter @m . runError @e -runStoreActionExceptT :: - forall m e a store. - MonadClient m => - (forall n b. (MonadClient n, n ~ m) => Sem '[store, Embed n] b -> Sem '[Embed n] b) -> - Sem '[Error e, store, Embed m] a -> - ExceptT e m a -runStoreActionExceptT interpreter = runStoreAction interpreter >=> except +semErrToExceptT :: + Sem (Error e ': r) a -> + ExceptT e (Sem r) a +semErrToExceptT act = + lift (runError act) >>= \case + Left p -> throwE p + Right v -> pure v diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index 9273161152..ee158b4b81 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -21,12 +21,12 @@ module Brig.User.EJPD (ejpdRequest) where import Brig.API.Handler -import Brig.API.User (lookupHandle) import Brig.App import qualified Brig.Data.Connection as Conn import Brig.Data.User (lookupUser) import qualified Brig.IO.Intra as Intra import Brig.Options (setDefaultUserLocale) +import Brig.Sem.UserHandleStore import Brig.Sem.UserQuery (UserQuery) import Brig.Types.User (HavePendingInvitations (NoPendingInvitations), Locale) import Control.Error hiding (bool) @@ -46,7 +46,7 @@ import Wire.API.User (User, userDisplayName, userEmail, userHandle, userId, user ejpdRequest :: forall r. - Member UserQuery r => + Members '[UserHandleStore, UserQuery] r => Maybe Bool -> EJPDRequestBody -> (Handler r) EJPDResponseBody @@ -58,7 +58,7 @@ ejpdRequest includeContacts (EJPDRequestBody handles) = do go1 includeContacts' handle = do loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain locale <- setDefaultUserLocale <$> view settings - mbUid <- wrapClient $ lookupHandle handle + mbUid <- liftSem $ lookupHandle handle mbUsr <- maybe (pure Nothing) (liftSem . lookupUser loc locale NoPendingInvitations) mbUid maybe (pure Nothing) (fmap Just . go2 loc locale includeContacts') mbUsr diff --git a/services/brig/src/Brig/User/Handle.hs b/services/brig/src/Brig/User/Handle.hs index 245679ae51..cd73777142 100644 --- a/services/brig/src/Brig/User/Handle.hs +++ b/services/brig/src/Brig/User/Handle.hs @@ -24,73 +24,72 @@ module Brig.User.Handle ) where -import Brig.App import Brig.Data.Instances () import qualified Brig.Data.User as User +import Brig.Sem.UniqueClaimsStore +import Brig.Sem.UserHandleStore + ( Consistency (..), + UserHandleStore, + deleteHandle, + getHandleWithConsistency, + insertHandle, + lookupHandle, + ) +import Brig.Sem.UserQuery import Brig.Unique -import Cassandra import Data.Handle (Handle, fromHandle) import Data.Id -import Imports +import Imports hiding (All) +import Polysemy +import Polysemy.Async +import Polysemy.Conc.Effect.Race +import Polysemy.Resource -- | Claim a new handle for an existing 'User'. -claimHandle :: (MonadClient m, MonadReader Env m) => UserId -> Maybe Handle -> Handle -> m Bool +claimHandle :: + forall r. + Members + '[ Async, + Race, + Resource, + UniqueClaimsStore, + UserHandleStore, + UserQuery + ] + r => + UserId -> + Maybe Handle -> + Handle -> + Sem r Bool claimHandle uid oldHandle newHandle = isJust <$> do owner <- lookupHandle newHandle case owner of Just uid' | uid /= uid' -> pure Nothing _ -> do - env <- ask let key = "@" <> fromHandle newHandle - withClaim uid key (30 # Minute) $ - runAppT env $ - do - -- Record ownership - wrapClient $ retry x5 $ write handleInsert (params LocalQuorum (newHandle, uid)) - -- Update profile - result <- wrapClient $ User.updateHandle uid newHandle - -- Free old handle (if it changed) - for_ (mfilter (/= newHandle) oldHandle) $ - wrapClient . freeHandle uid - pure result + withClaim uid key (30 # Minute) $ do + -- Record ownership + insertHandle @r newHandle uid + -- Update profile + result <- User.updateHandle uid newHandle + -- Free old handle (if it changed) + for_ (mfilter (/= newHandle) oldHandle) $ + freeHandle uid + pure result -- | Free a 'Handle', making it available to be claimed again. -freeHandle :: MonadClient m => UserId -> Handle -> m () +freeHandle :: + Members '[UniqueClaimsStore, UserHandleStore] r => + UserId -> + Handle -> + Sem r () freeHandle uid h = do - retry x5 $ write handleDelete (params LocalQuorum (Identity h)) + deleteHandle h let key = "@" <> fromHandle h - deleteClaim uid key (30 # Minute) - --- | Lookup the current owner of a 'Handle'. -lookupHandle :: MonadClient m => Handle -> m (Maybe UserId) -lookupHandle = lookupHandleWithPolicy LocalQuorum + deleteClaims uid (30 # Minute) key -- | A weaker version of 'lookupHandle' that trades availability -- (and potentially speed) for the possibility of returning stale data. -glimpseHandle :: MonadClient m => Handle -> m (Maybe UserId) -glimpseHandle = lookupHandleWithPolicy One - -{-# INLINE lookupHandleWithPolicy #-} - --- | Sending an empty 'Handle' here causes C* to throw "Key may not be empty" --- error. --- --- FUTUREWORK: This should ideally be tackled by hiding constructor for 'Handle' --- and only allowing it to be parsed. -lookupHandleWithPolicy :: MonadClient m => Consistency -> Handle -> m (Maybe UserId) -lookupHandleWithPolicy policy h = do - (runIdentity =<<) - <$> retry x1 (query1 handleSelect (params policy (Identity h))) - --------------------------------------------------------------------------------- --- Queries - -handleInsert :: PrepQuery W (Handle, UserId) () -handleInsert = "INSERT INTO user_handle (handle, user) VALUES (?, ?)" - -handleSelect :: PrepQuery R (Identity Handle) (Identity (Maybe UserId)) -handleSelect = "SELECT user FROM user_handle WHERE handle = ?" - -handleDelete :: PrepQuery W (Identity Handle) () -handleDelete = "DELETE FROM user_handle WHERE handle = ?" +glimpseHandle :: Member UserHandleStore r => Handle -> Sem r (Maybe UserId) +glimpseHandle = getHandleWithConsistency One From 24dc4ef2b91de931b9091a66b435d6bcda87224d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 15 Jun 2022 10:14:21 +0200 Subject: [PATCH 13/41] Interpretation for an ActivationKeyStore action --- .../brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs b/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs index 51ebfcf377..6843c0fead 100644 --- a/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs +++ b/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs @@ -34,7 +34,7 @@ activationKeyStoreToCassandra = embed @m . \case GetActivationKey k -> getKey k InsertActivationKey tuple -> keyInsertQuery tuple - DeleteActivationPair k -> undefined k + DeleteActivationPair k -> keyDelete k getKey :: (MonadClient m, Cql ActivationKey, Cql ActivationCode) => ActivationKey -> m (Maybe GetKeyTuple) getKey key = retry x1 . query1 keySelect $ params LocalQuorum (Identity key) @@ -51,3 +51,9 @@ keyInsertQuery (key, t, k, c, u, attempts, timeout) = "INSERT INTO activation_keys \ \(key, key_type, key_text, code, user, retries) VALUES \ \(? , ? , ? , ? , ? , ? ) USING TTL ?" + +keyDelete :: (MonadClient m, Cql ActivationKey) => ActivationKey -> m () +keyDelete = write q . params LocalQuorum . Identity + where + q :: PrepQuery W (Identity ActivationKey) () + q = "DELETE FROM activation_keys WHERE key = ?" From 21b2822797c67c0785af6ed6148bd460baa14b66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 15 Jun 2022 13:29:19 +0200 Subject: [PATCH 14/41] It compiles (though with undefined's and commented out code) --- services/brig/brig.cabal | 1 + services/brig/src/Brig/API/Client.hs | 24 +++---- services/brig/src/Brig/API/User.hs | 2 + services/brig/src/Brig/App.hs | 1 + services/brig/src/Brig/Code.hs | 66 +++++++++---------- services/brig/src/Brig/Data/Activation.hs | 3 + services/brig/src/Brig/Data/Client.hs | 52 ++++++++------- services/brig/src/Brig/Data/User.hs | 16 ++--- .../brig/src/Brig/InternalEvent/Process.hs | 18 +++++ services/brig/src/Brig/Provider/API.hs | 22 +++++-- services/brig/src/Brig/Run.hs | 5 ++ .../Brig/Sem/ActivationKeyStore/Cassandra.hs | 3 + .../brig/src/Brig/Sem/CodeStore/Cassandra.hs | 9 --- services/brig/src/Brig/Sem/Common.hs | 30 +++++++++ services/brig/src/Brig/User/API/Auth.hs | 11 ++-- services/brig/src/Brig/User/Auth.hs | 17 ++--- .../brig/test/integration/API/Provider.hs | 10 ++- .../brig/test/integration/API/User/Util.hs | 16 +++-- 18 files changed, 197 insertions(+), 109 deletions(-) create mode 100644 services/brig/src/Brig/Sem/Common.hs diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 5e55edf1ac..67d0988082 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -93,6 +93,7 @@ library Brig.Sem.BudgetStore.Cassandra Brig.Sem.CodeStore Brig.Sem.CodeStore.Cassandra + Brig.Sem.Common Brig.Sem.GalleyAccess Brig.Sem.GalleyAccess.Http Brig.Sem.PasswordResetStore diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 7206c640c6..8632580684 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -153,28 +153,28 @@ addClientWithReAuthPolicy :: Maybe IP -> NewClient -> ExceptT ClientError (AppT r) Client -addClientWithReAuthPolicy policy u con ip new = do +addClientWithReAuthPolicy _policy u con ip new = do locale <- Opt.setDefaultUserLocale <$> view settings acc <- lift (liftSem $ Data.lookupAccount locale u) >>= maybe (throwE (ClientUserNotFound u)) pure verifyCodeThrow (newClientVerificationCode new) (userId . accountUser $ acc) - loc <- maybe (pure Nothing) locationOf ip - maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings - let caps :: Maybe (Set ClientCapability) - caps = updlhdev $ newClientCapabilities new + _loc <- maybe (pure Nothing) locationOf ip + _maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings + let _caps :: Maybe (Set ClientCapability) + _caps = updlhdev $ newClientCapabilities new where updlhdev = if newClientType new == LegalHoldClientType then Just . maybe (Set.singleton lhcaps) (Set.insert lhcaps) else id lhcaps = ClientSupportsLegalholdImplicitConsent - (clt0, old, count) <- - wrapClientE - (Data.addClientWithReAuthPolicy policy u clientId' new maxPermClients loc caps) - !>> ClientDataError + (clt0, old, count :: Word) <- undefined + -- wrapClientE + -- (Data.addClientWithReAuthPolicy policy u clientId' new maxPermClients loc caps) + -- !>> ClientDataError let clt = clt0 {clientMLSPublicKeys = newClientMLSPublicKeys new} let usr = accountUser acc lift $ do - for_ old $ execDelete u con + for_ (old :: Maybe Client) $ execDelete u con wrapHttp $ Intra.newClient u (clientId clt) Intra.onClientEvent u con (ClientAdded u clt) when (clientType clt == LegalHoldClientType) $ wrapHttpClient $ Intra.onUserEvent u con (UserLegalHoldEnabled u) @@ -184,7 +184,7 @@ addClientWithReAuthPolicy policy u con ip new = do sendNewClientEmail (userDisplayName usr) email clt (userLocale usr) pure clt where - clientId' = clientIdFromPrekey (unpackLastPrekey $ newClientLastKey new) + -- clientId' = clientIdFromPrekey (unpackLastPrekey $ newClientLastKey new) verifyCodeThrow :: Maybe Code.Value -> @@ -388,7 +388,7 @@ claimLocalMultiPrekeyBundles protectee userClients = do -- Utilities -- | Perform an orderly deletion of an existing client. -execDelete :: UserId -> Maybe ConnId -> Client -> (AppT r) () +execDelete :: UserId -> Maybe ConnId -> Client -> AppT r () execDelete u con c = do wrapHttp $ Intra.rmClient u (clientId c) for_ (clientCookie c) $ \l -> wrapClient $ Auth.revokeCookies u [] [l] diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 5f24b9e54d..0830a03215 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -1111,6 +1111,8 @@ sendActivationCode emailOrPhone loc call = do _otherwise -> sendActivationMail em name p loc' ident +-- TODO(md): polysemize this, it is straightforward given that everything +-- underneath should be in Polysemy mkActivationKey :: Member ActivationSupply r => ActivationTarget -> diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 1ee7653649..5f5280bf9c 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -103,6 +103,7 @@ import Brig.Sem.BudgetStore (BudgetStore) import Brig.Sem.BudgetStore.Cassandra import Brig.Sem.CodeStore (CodeStore) import Brig.Sem.CodeStore.Cassandra +import Brig.Sem.Common import Brig.Sem.GalleyAccess (GalleyAccess) import Brig.Sem.GalleyAccess.Http import Brig.Sem.PasswordResetStore (PasswordResetStore) diff --git a/services/brig/src/Brig/Code.hs b/services/brig/src/Brig/Code.hs index f0ec8ec6d4..b2a025afc4 100644 --- a/services/brig/src/Brig/Code.hs +++ b/services/brig/src/Brig/Code.hs @@ -211,43 +211,43 @@ generate gen scope retries ttl account = do -------------------------------------------------------------------------------- -- Storage -insert :: MonadClient m => Code -> m () -insert c = do - let k = codeKey c - let s = codeScope c - let v = codeValue c - let r = fromIntegral (codeRetries c) - let a = codeAccount c - let e = codeForEmail c - let p = codeForPhone c - let t = round (codeTTL c) - retry x5 (write cql (params LocalQuorum (k, s, v, r, e, p, a, t))) - where - cql :: PrepQuery W (Key, Scope, Value, Retries, Maybe Email, Maybe Phone, Maybe UUID, Int32) () - cql = - "INSERT INTO vcodes (key, scope, value, retries, email, phone, account) \ - \VALUES (?, ?, ?, ?, ?, ?, ?) USING TTL ?" +-- insert :: MonadClient m => Code -> m () +-- insert c = do +-- let k = codeKey c +-- let s = codeScope c +-- let v = codeValue c +-- let r = fromIntegral (codeRetries c) +-- let a = codeAccount c +-- let e = codeForEmail c +-- let p = codeForPhone c +-- let t = round (codeTTL c) +-- retry x5 (write cql (params LocalQuorum (k, s, v, r, e, p, a, t))) +-- where +-- cql :: PrepQuery W (Key, Scope, Value, Retries, Maybe Email, Maybe Phone, Maybe UUID, Int32) () +-- cql = +-- "INSERT INTO vcodes (key, scope, value, retries, email, phone, account) \ +-- \VALUES (?, ?, ?, ?, ?, ?, ?) USING TTL ?" --- | Lookup a pending code. -lookup :: MonadClient m => Key -> Scope -> m (Maybe Code) -lookup k s = fmap (toCode k s) <$> retry x1 (query1 cql (params LocalQuorum (k, s))) - where - cql :: PrepQuery R (Key, Scope) (Value, Int32, Retries, Maybe Email, Maybe Phone, Maybe UUID) - cql = - "SELECT value, ttl(value), retries, email, phone, account \ - \FROM vcodes WHERE key = ? AND scope = ?" +-- -- | Lookup a pending code. +-- lookup :: MonadClient m => Key -> Scope -> m (Maybe Code) +-- lookup k s = fmap (toCode k s) <$> retry x1 (query1 cql (params LocalQuorum (k, s))) +-- where +-- cql :: PrepQuery R (Key, Scope) (Value, Int32, Retries, Maybe Email, Maybe Phone, Maybe UUID) +-- cql = +-- "SELECT value, ttl(value), retries, email, phone, account \ +-- \FROM vcodes WHERE key = ? AND scope = ?" -- | Lookup and verify the code for the given key and scope -- against the given value. -verify :: MonadClient m => Key -> Scope -> Value -> m (Maybe Code) -verify k s v = lookup k s >>= maybe (pure Nothing) continue - where - continue c - | codeValue c == v = pure (Just c) - | codeRetries c > 0 = do - insert (c {codeRetries = codeRetries c - 1}) - pure Nothing - | otherwise = pure Nothing +-- verify :: MonadClient m => Key -> Scope -> Value -> m (Maybe Code) +-- verify k s v = lookup k s >>= maybe (pure Nothing) continue +-- where +-- continue c +-- | codeValue c == v = pure (Just c) +-- | codeRetries c > 0 = do +-- insert (c {codeRetries = codeRetries c - 1}) +-- pure Nothing +-- | otherwise = pure Nothing -- | Delete a code associated with the given key and scope. delete :: MonadClient m => Key -> Scope -> m () diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 77fae0e557..941d4199d2 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -211,8 +211,11 @@ verifyCode key code = do Nothing -> throwE invalidCode mkScope _ _ _ = throwE invalidCode countdown = lift . insertActivationKey + -- countdown = lift . retry x5 . write keyInsert . params LocalQuorum revoke = lift $ deleteActivationPair key +-- TODO(md): This should be deleted and an effect action 'makeActivationKey' +-- should be used instead. mkActivationKey :: UserKey -> IO ActivationKey mkActivationKey k = do d <- liftIO $ getDigestByName "SHA256" diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index edcb605f89..91f0e586ce 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -55,20 +55,22 @@ import Bilge.Retry (httpHandlers) import Brig.AWS import Brig.App import Brig.Data.Instances () -import Brig.Data.User (AuthError (..), ReAuthError (..)) -import qualified Brig.Data.User as User -import Brig.Options (setDefaultUserLocale) -import Brig.Sem.UserQuery.Cassandra +-- import Brig.Data.User (AuthError (..), ReAuthError (..)) +-- import qualified Brig.Data.User as User +-- import Brig.Options (setDefaultUserLocale) +import Brig.Sem.UserQuery import Brig.Types.Instances () import Brig.Types.User.Auth (CookieLabel) import Brig.User.Auth.DB.Instances () import Cassandra as C hiding (Client) import Cassandra.Settings as C hiding (Client) +-- import Control.Arrow import Control.Error import qualified Control.Exception.Lens as EL import Control.Lens import Control.Monad.Catch import Control.Monad.Random (randomRIO) +-- import Control.Monad.Trans.Except import Control.Retry import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Conversion (toByteString, toByteString') @@ -80,12 +82,14 @@ import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) import qualified Data.Map as Map import qualified Data.Metrics as Metrics import Data.Misc +import Data.Qualified import qualified Data.Set as Set import qualified Data.Text as Text import qualified Data.UUID as UUID import Imports import Polysemy hiding (run) import Polysemy.Error +import qualified Polysemy.Error as P import Polysemy.Input import System.CryptoBox (Result (Success)) import qualified System.CryptoBox as CryptoBox @@ -119,19 +123,19 @@ reAuthForNewClients :: ReAuthPolicy reAuthForNewClients count upsert = count > 0 && not upsert addClient :: - (MonadClient m, MonadReader Brig.App.Env m) => + Members '[Input (Local ()), UserQuery] r => UserId -> ClientId -> NewClient -> Int -> Maybe Location -> Maybe (Imports.Set ClientCapability) -> - ExceptT ClientDataError m (Client, [Client], Word) + ExceptT ClientDataError (AppT r) (Client, [Client], Word) addClient = addClientWithReAuthPolicy reAuthForNewClients addClientWithReAuthPolicy :: - forall m. - (MonadClient m, MonadReader Brig.App.Env m) => + forall r. + Members '[Input (Local ()), UserQuery] r => ReAuthPolicy -> UserId -> ClientId -> @@ -139,24 +143,19 @@ addClientWithReAuthPolicy :: Int -> Maybe Location -> Maybe (Imports.Set ClientCapability) -> - ExceptT ClientDataError m (Client, [Client], Word) -addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients loc cps = do - clients <- lookupClients u - locale <- setDefaultUserLocale <$> view settings - locDomain <- qualifyLocal () + ExceptT ClientDataError (AppT r) (Client, [Client], Word) +addClientWithReAuthPolicy _reAuthPolicy u newId c maxPermClients loc cps = do + clients <- lift . wrapClient $ lookupClients u + -- locale <- setDefaultUserLocale <$> view settings let typed = filter ((== newClientType c) . clientType) clients let count = length typed let upsert = any exists typed - when (reAuthPolicy count upsert) $ do - o <- - lift - . runM - . userQueryToCassandra @m @'[Embed m] - $ runError - . mapError ClientReAuthError - . runInputConst locDomain - $ User.reauthenticate locale u (newClientPassword c) - whenLeft o throwE + -- when (reAuthPolicy count upsert) $ do + -- liftSemE + -- . fmapLT ClientReAuthError + -- . semToExceptT + -- . raise @(P.Error ReAuthError) + -- $ User.reauthenticate locale u (newClientPassword c) let capacity = fmap (+ (- count)) limit unless (maybe True (> 0) capacity || upsert) $ throwE TooManyClients @@ -174,8 +173,8 @@ addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients loc cps = do exists :: Client -> Bool exists = (==) newId . clientId - insert :: (MonadClient m, MonadReader Brig.App.Env m) => ExceptT ClientDataError m Client - insert = do + insert :: ExceptT ClientDataError (AppT r) Client + insert = wrapClientE $ do -- Is it possible to do this somewhere else? Otherwise we could use `MonadClient` instead now <- toUTCTimeMillis <$> (liftIO =<< view currentTime) let keys = unpackLastPrekey (newClientLastKey c) : newClientPrekeys c @@ -200,6 +199,9 @@ addClientWithReAuthPolicy reAuthPolicy u newId c maxPermClients loc cps = do clientMLSPublicKeys = mempty } +_semToExceptT :: Sem (P.Error e ': r) () -> ExceptT e (Sem r) () +_semToExceptT = lift . runError >=> flip whenLeft throwE + lookupClient :: MonadClient m => UserId -> ClientId -> m (Maybe Client) lookupClient u c = do keys <- retry x1 (query selectMLSPublicKeys (params LocalQuorum (u, c))) diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index f03d4135e3..ae76b59301 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -279,8 +279,8 @@ updateSSOId u ssoid = do updateManagedBy :: MonadClient m => UserId -> ManagedBy -> m () updateManagedBy u h = retry x5 $ write userManagedByUpdate (params LocalQuorum (h, u)) -updateHandle :: MonadClient m => UserId -> Handle -> m () -updateHandle u h = retry x5 $ write userHandleUpdate (params LocalQuorum (h, u)) +-- updateHandle :: MonadClient m => UserId -> Handle -> m () +-- updateHandle u h = retry x5 $ write userHandleUpdate (params LocalQuorum (h, u)) updatePassword :: MonadClient m => UserId -> PlainTextPassword -> m () updatePassword u t = do @@ -500,12 +500,12 @@ richInfoSelectMulti = "SELECT user, json FROM rich_info WHERE user in ?" teamSelect :: PrepQuery R (Identity UserId) (Identity (Maybe TeamId)) teamSelect = "SELECT team FROM user WHERE id = ?" -userInsert :: PrepQuery W UserRowInsert () -userInsert = - "INSERT INTO user (id, name, picture, assets, email, phone, sso_id, \ - \accent_id, password, activated, status, expires, language, \ - \country, provider, service, handle, team, managed_by) \ - \VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" +-- userInsert :: PrepQuery W UserRowInsert () +-- userInsert = +-- "INSERT INTO user (id, name, picture, assets, email, phone, sso_id, \ +-- \accent_id, password, activated, status, expires, language, \ +-- \country, provider, service, handle, team, managed_by) \ +-- \VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" userDisplayNameUpdate :: PrepQuery W (Name, UserId) () userDisplayNameUpdate = "UPDATE user SET name = ? WHERE id = ?" diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 3faf928944..f33fb5f2d4 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -55,6 +55,18 @@ onEvent :: UserQuery ] r => + -- ( Log.MonadLogger m, + -- MonadCatch m, + -- MonadThrow m, + -- MonadIndexIO m, + -- MonadReader Env m, + -- MonadIO m, + -- MonadMask m, + -- MonadHttp m, + -- HasRequestId m, + -- MonadUnliftIO m, + -- MonadClient m + -- ) => InternalNotification -> AppT r () onEvent n = do @@ -80,6 +92,12 @@ onEvent n = do handleTimeout :: AppT r a -> AppT r a handleTimeout _act = undefined +-- act +-- >>= ( \case +-- Just x -> pure x +-- Nothing -> throwM (InternalEventTimeout n) +-- ) +-- . flip (withAsyncWait (Minutes 1)) await newtype InternalEventException = -- | 'onEvent' has timed out diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 0c6ae7a233..d64e3310dd 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -101,6 +101,7 @@ import qualified OpenSSL.PEM as SSL import qualified OpenSSL.RSA as SSL import OpenSSL.Random (randBytes) import Polysemy +import Polysemy.Input import qualified Ssl.Util as SSL import System.Logger.Class (MonadLogger) import UnliftIO.Async (pooledMapConcurrentlyN_) @@ -121,7 +122,12 @@ import qualified Wire.API.User.Client.Prekey as Public (PrekeyId) import qualified Wire.API.User.Identity as Public (Email) routesPublic :: - Members '[UserQuery, VerificationCodeStore] r => + Members + '[ Input (Local ()), + UserQuery, + VerificationCodeStore + ] + r => Routes Doc.ApiBuilder (Handler r) () routesPublic = do -- Public API (Unauthenticated) -------------------------------------------- @@ -932,7 +938,11 @@ updateServiceWhitelist uid con tid upd = do pure UpdateServiceWhitelistRespChanged addBotH :: - Member UserQuery r => + Members + '[ Input (Local ()), + UserQuery + ] + r => UserId ::: ConnId ::: ConvId ::: JsonRequest Public.AddBot -> (Handler r) Response addBotH (zuid ::: zcon ::: cid ::: req) = do @@ -940,7 +950,11 @@ addBotH (zuid ::: zcon ::: cid ::: req) = do setStatus status201 . json <$> (addBot zuid zcon cid =<< parseJsonBody req) addBot :: - Member UserQuery r => + Members + '[ Input (Local ()), + UserQuery + ] + r => UserId -> ConnId -> ConvId -> @@ -1005,7 +1019,7 @@ addBot zuid zcon cid add = do -- if we want to protect bots against lh, 'addClient' cannot just send lh capability -- implicitly in the next line. pure $ FutureWork @'UnprotectedBot undefined - wrapClientE (User.addClient (botUserId bid) bcl newClt maxPermClients Nothing (Just $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent)) + User.addClient (botUserId bid) bcl newClt maxPermClients Nothing (Just $ Set.singleton Public.ClientSupportsLegalholdImplicitConsent) !>> const (StdError badGateway) -- MalformedPrekeys -- Add the bot to the conversation diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 71501d185d..2b2ac97a48 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -35,7 +35,9 @@ import qualified Brig.AWS.SesNotification as SesNotification import Brig.App import qualified Brig.Calling as Calling import Brig.Data.UserPendingActivation (UserPendingActivation (..), usersPendingActivationList, usersPendingActivationRemoveMultiple) +-- import qualified Brig.InternalEvent.Process as Internal import Brig.Options hiding (internalEvents, sesQueue) +-- import qualified Brig.Queue as Queue import Brig.Types.Intra (AccountStatus (PendingInvitation)) import Brig.Version import Cassandra (Page (Page)) @@ -82,6 +84,9 @@ run o = do s <- Server.newSettings (server e) -- TODO(md): Find or implement a Polysemy equivalent internalEventListener :: Async.Async () <- undefined + -- Async.async + -- $ runAppT e $ + -- Queue.listen (e ^. internalEvents) Internal.onEvent let throttleMillis = fromMaybe defSqsThrottleMillis $ setSqsThrottleMillis (optSettings o) emailListener <- for (e ^. awsEnv . sesQueue) $ \q -> Async.async $ diff --git a/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs b/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs index 6843c0fead..eb2bc901be 100644 --- a/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs +++ b/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs @@ -24,6 +24,9 @@ import Imports import Polysemy import Wire.API.User.Activation +-- TODO(md): See why there's no instance for 'Cql ActivationKey', yet the +-- Brig.Data.User module sees one. Then remove all explicitly spelled out Cql +-- constraints in this module. activationKeyStoreToCassandra :: forall m r a. (MonadClient m, Member (Embed m) r, Cql ActivationKey, Cql ActivationCode) => diff --git a/services/brig/src/Brig/Sem/CodeStore/Cassandra.hs b/services/brig/src/Brig/Sem/CodeStore/Cassandra.hs index 21b21cb477..e9f99ad30a 100644 --- a/services/brig/src/Brig/Sem/CodeStore/Cassandra.hs +++ b/services/brig/src/Brig/Sem/CodeStore/Cassandra.hs @@ -18,7 +18,6 @@ module Brig.Sem.CodeStore.Cassandra ( codeStoreToCassandra, - interpretClientToIO, ) where @@ -65,14 +64,6 @@ codeStoreToCassandra = toRecord (prqdCode, prqdUser, prqdRetries, prqdTimeout) = PRQueryData {..} -interpretClientToIO :: - Member (Final IO) r => - ClientState -> - Sem (Embed Cassandra.Client ': r) a -> - Sem r a -interpretClientToIO ctx = interpret $ \case - Embed action -> embedFinal @IO $ runClient ctx action - --------------------------------------------------------------------------------- -- Queries diff --git a/services/brig/src/Brig/Sem/Common.hs b/services/brig/src/Brig/Sem/Common.hs new file mode 100644 index 0000000000..d72a283ab2 --- /dev/null +++ b/services/brig/src/Brig/Sem/Common.hs @@ -0,0 +1,30 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.Common where + +import Cassandra +import Imports +import Polysemy + +interpretClientToIO :: + Member (Final IO) r => + ClientState -> + Sem (Embed Client ': r) a -> + Sem r a +interpretClientToIO ctx = interpret $ \case + Embed action -> embedFinal @IO $ runClient ctx action diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index e61967b967..edb6d8743e 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -26,7 +26,7 @@ import Brig.API.Handler import Brig.API.Types import qualified Brig.API.User as User import Brig.App -import Brig.Options (setDefaultUserLocale) +-- import Brig.Options (setDefaultUserLocale) import Brig.Phone import Brig.Sem.ActivationKeyStore import Brig.Sem.ActivationSupply @@ -37,13 +37,14 @@ import Brig.Sem.UserHandleStore import Brig.Sem.UserKeyStore import Brig.Sem.UserQuery (UserQuery) import Brig.Sem.VerificationCodeStore -import Brig.Types.Intra (ReAuthUser, reAuthCode, reAuthCodeAction, reAuthPassword) +import Brig.Types.Intra (ReAuthUser, reAuthCode, reAuthCodeAction) +-- import Brig.Types.Intra (ReAuthUser, reAuthCode, reAuthCodeAction, reAuthPassword) import Brig.Types.User.Auth import qualified Brig.User.Auth as Auth import qualified Brig.User.Auth.Cookie as Auth import qualified Brig.ZAuth as ZAuth import Control.Error (catchE) -import Control.Lens (view) +-- import Control.Lens (view) import Control.Monad.Except import Control.Monad.Trans.Except (throwE) import qualified Data.ByteString as BS @@ -327,8 +328,8 @@ reAuthUser :: ReAuthUser -> (Handler r) () reAuthUser uid body = do - locale <- setDefaultUserLocale <$> view settings - lift (liftSem (User.reauthenticate locale uid (reAuthPassword body))) !>> reauthError + -- locale <- setDefaultUserLocale <$> view settings + -- lift (liftSem (User.reauthenticate locale uid (reAuthPassword body))) !>> reauthError case reAuthCodeAction body of Just action -> Auth.verifyCode (reAuthCode body) action uid diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 7093edfac1..bd1063d8b8 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -557,9 +557,10 @@ ssoLogin :: CookieType -> ExceptT LoginError m (Access ZAuth.User) ssoLogin (SsoLogin uid label) typ = do - locale <- Opt.setDefaultUserLocale <$> view settings - locDomain <- qualifyLocal () - o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid Nothing + -- locale <- Opt.setDefaultUserLocale <$> view settings + -- locDomain <- qualifyLocal () + o <- undefined + -- o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid Nothing whenLeft o $ \case ReAuthMissingPassword -> pure () ReAuthCodeVerificationRequired -> pure () @@ -588,11 +589,11 @@ legalHoldLogin :: LegalHoldLogin -> CookieType -> ExceptT LegalHoldLoginError m (Access ZAuth.LegalHoldUser) -legalHoldLogin (LegalHoldLogin uid plainTextPassword label) typ = do - locale <- Opt.setDefaultUserLocale <$> view settings - locDomain <- qualifyLocal () - o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid plainTextPassword - except o !>> LegalHoldReAuthError +legalHoldLogin (LegalHoldLogin uid _plainTextPassword label) typ = do + -- locale <- Opt.setDefaultUserLocale <$> view settings + -- locDomain <- qualifyLocal () + -- o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid plainTextPassword + -- except o !>> LegalHoldReAuthError -- legalhold login is only possible if -- the user is a team user -- and the team has legalhold enabled diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 7cd730553b..6ead1b43a4 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -28,6 +28,8 @@ import qualified API.Team.Util as Team import Bilge hiding (accept, head, timeout) import Bilge.Assert import qualified Brig.Code as Code +import Brig.Sem.Common +import Brig.Sem.VerificationCodeStore.Cassandra import Brig.Types hiding (CompletePasswordReset (..), EmailUpdate (..), NewPasswordReset (..), PasswordChange (..), PasswordReset (..)) import qualified Brig.Types.Intra as Intra import Brig.Types.Provider @@ -79,6 +81,7 @@ import qualified Network.Wai.Route as Wai import qualified Network.Wai.Utilities.Error as Error import OpenSSL.PEM (writePublicKey) import OpenSSL.RSA (generateRSAKey') +import Polysemy import System.IO.Temp (withSystemTempFile) import Test.Tasty hiding (Timeout) import qualified Test.Tasty.Cannon as WS @@ -1410,7 +1413,12 @@ enabled2ndFaForTeamInternal galley tid = do -- DB Operations lookupCode :: MonadIO m => DB.ClientState -> Code.Gen -> Code.Scope -> m (Maybe Code.Code) -lookupCode db gen = liftIO . DB.runClient db . Code.lookup (Code.genKey gen) +lookupCode db gen = + liftIO + . runFinal + . interpretClientToIO db + . verificationCodeStoreToCassandra @DB.Client + . Code.getPendingCode (Code.genKey gen) -------------------------------------------------------------------------------- -- Utilities diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 4d11907f9e..01d905b8b5 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -21,11 +21,14 @@ module API.User.Util where import Bilge hiding (accept, timeout) import Bilge.Assert -import qualified Brig.Code as Code import Brig.Options (Opts) +import Brig.Sem.Common import Brig.Sem.PasswordResetSupply import Brig.Sem.PasswordResetSupply.IO +import qualified Brig.Sem.VerificationCodeStore as Code +import Brig.Sem.VerificationCodeStore.Cassandra import Brig.Types +import Brig.Types.Code hiding (Value) import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.Types.User.Auth hiding (user) import qualified Brig.ZAuth @@ -33,7 +36,7 @@ import qualified Cassandra as DB import qualified Codec.MIME.Type as MIME import Control.Lens (preview, (^?)) import Control.Monad.Catch (MonadCatch) -import Data.Aeson +import Data.Aeson hiding (Key) import Data.Aeson.Lens import Data.ByteString.Builder (toLazyByteString) import Data.ByteString.Char8 (pack) @@ -518,5 +521,10 @@ setTeamFeatureLockStatus :: setTeamFeatureLockStatus galley tid status = put (galley . paths ["i", "teams", toByteString' tid, "features", toByteString' (Public.knownTeamFeatureName @a), toByteString' status]) !!! const 200 === statusCode -lookupCode :: MonadIO m => DB.ClientState -> Code.Key -> Code.Scope -> m (Maybe Code.Code) -lookupCode db k = liftIO . DB.runClient db . Code.lookup k +lookupCode :: MonadIO m => DB.ClientState -> Key -> Code.Scope -> m (Maybe Code.Code) +lookupCode db k = + liftIO + . runFinal + . interpretClientToIO db + . verificationCodeStoreToCassandra @DB.Client + . Code.getPendingCode k From 9b79b8d3148d724d8162f3ea0c98727d7fd89725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 12 Aug 2022 14:52:51 +0200 Subject: [PATCH 15/41] Get rid of one undefined --- services/brig/src/Brig/API/Client.hs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 586f0cb120..61c21c5317 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -154,28 +154,27 @@ addClientWithReAuthPolicy :: Maybe IP -> NewClient -> ExceptT ClientError (AppT r) Client -addClientWithReAuthPolicy _policy u con ip new = do +addClientWithReAuthPolicy policy u con ip new = do locale <- Opt.setDefaultUserLocale <$> view settings acc <- lift (liftSem $ Data.lookupAccount locale u) >>= maybe (throwE (ClientUserNotFound u)) pure verifyCodeThrow (newClientVerificationCode new) (userId . accountUser $ acc) - _loc <- maybe (pure Nothing) locationOf ip - _maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings - let _caps :: Maybe (Set ClientCapability) - _caps = updlhdev $ newClientCapabilities new + loc <- maybe (pure Nothing) locationOf ip + maxPermClients <- fromMaybe Opt.defUserMaxPermClients . Opt.setUserMaxPermClients <$> view settings + let caps :: Maybe (Set ClientCapability) + caps = updlhdev $ newClientCapabilities new where updlhdev = if newClientType new == LegalHoldClientType then Just . maybe (Set.singleton lhcaps) (Set.insert lhcaps) else id lhcaps = ClientSupportsLegalholdImplicitConsent - (clt0, old, count :: Word) <- undefined - -- wrapClientE - -- (Data.addClientWithReAuthPolicy policy u clientId' new maxPermClients loc caps) - -- !>> ClientDataError + (clt0, old, count) <- + (Data.addClientWithReAuthPolicy policy u clientId' new maxPermClients loc caps) + !>> ClientDataError let clt = clt0 {clientMLSPublicKeys = newClientMLSPublicKeys new} let usr = accountUser acc lift $ do - for_ (old :: Maybe Client) $ execDelete u con + for_ old $ execDelete u con wrapHttp $ Intra.newClient u (clientId clt) Intra.onClientEvent u con (ClientAdded u clt) when (clientType clt == LegalHoldClientType) $ wrapHttpClient $ Intra.onUserEvent u con (UserLegalHoldEnabled u) @@ -185,7 +184,7 @@ addClientWithReAuthPolicy _policy u con ip new = do sendNewClientEmail (userDisplayName usr) email clt (userLocale usr) pure clt where - -- clientId' = clientIdFromPrekey (unpackLastPrekey $ newClientLastKey new) + clientId' = clientIdFromPrekey (unpackLastPrekey $ newClientLastKey new) verifyCodeThrow :: Maybe Code.Value -> From 142e740b2b62d5b4b0a7b1bc09ca2822c7c73f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 12 Aug 2022 15:09:29 +0200 Subject: [PATCH 16/41] Drop undefined in ssoLogin --- services/brig/src/Brig/User/Auth.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 0f9b0cb97f..eb6e29fac6 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -558,10 +558,9 @@ ssoLogin :: CookieType -> ExceptT LoginError m (Access ZAuth.User) ssoLogin (SsoLogin uid label) typ = do - -- locale <- Opt.setDefaultUserLocale <$> view settings - -- locDomain <- qualifyLocal () - o <- undefined - -- o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid Nothing + locale <- Opt.setDefaultUserLocale <$> view settings + locDomain <- qualifyLocal () + o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid Nothing whenLeft o $ \case ReAuthMissingPassword -> pure () ReAuthCodeVerificationRequired -> pure () From 980342d779aeb2d0b5d62aba16609985a944c8f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 12 Aug 2022 15:34:11 +0200 Subject: [PATCH 17/41] Drop undefined from Brig.API.User.mkActivationKey --- services/brig/src/Brig/API/Internal.hs | 2 ++ services/brig/src/Brig/API/Public.hs | 10 +++++- services/brig/src/Brig/API/User.hs | 44 ++++++++++++++++++-------- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 80c086ec21..70fbdd63e6 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -547,10 +547,12 @@ createUserNoVerifySpar :: ActivationSupply, Async, Input (Local ()), + P.Error Twilio.ErrorResponse, PasswordResetStore, PasswordResetSupply, Race, Resource, + Twilio, UniqueClaimsStore, UserHandleStore, UserKeyStore, diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index a0114ba990..94c297096b 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1274,8 +1274,10 @@ activateKeyH :: '[ ActivationKeyStore, ActivationSupply, Input (Local ()), + P.Error Twilio.ErrorResponse, PasswordResetSupply, PasswordResetStore, + Twilio, UserKeyStore, UserQuery ] @@ -1291,8 +1293,10 @@ activateH :: '[ ActivationKeyStore, ActivationSupply, Input (Local ()), + P.Error Twilio.ErrorResponse, PasswordResetSupply, PasswordResetStore, + Twilio, UserKeyStore, UserQuery ] @@ -1308,8 +1312,10 @@ activate :: '[ ActivationKeyStore, ActivationSupply, Input (Local ()), + P.Error Twilio.ErrorResponse, PasswordResetSupply, PasswordResetStore, + Twilio, UserKeyStore, UserQuery ] @@ -1318,7 +1324,9 @@ activate :: Handler r ActivationRespWithStatus activate (Public.Activate tgt code dryrun) | dryrun = do - liftSemE (API.preverify tgt code) !>> actError + tc <- view twilioCreds + m <- view httpManager + liftSemE (API.preverify tgt code tc m) !>> actError pure ActivationRespDryRun | otherwise = do result <- API.activate tgt code Nothing !>> actError diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 0253c474ca..b6d6a1cc5a 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -89,7 +89,7 @@ module Brig.API.User ) where -import Bilge.IO (MonadHttp) +import Bilge.IO (Manager, MonadHttp) import Bilge.RPC (HasRequestId) import qualified Brig.API.Error as Error import qualified Brig.API.Handler as API (Handler, UserNotAllowedToJoinTeam (..)) @@ -1027,8 +1027,10 @@ activate :: '[ ActivationKeyStore, ActivationSupply, Input (Local ()), + P.Error Twilio.ErrorResponse, PasswordResetSupply, PasswordResetStore, + Twilio, UserKeyStore, UserQuery ] @@ -1045,8 +1047,10 @@ activateWithCurrency :: '[ ActivationKeyStore, ActivationSupply, Input (Local ()), + P.Error Twilio.ErrorResponse, PasswordResetSupply, PasswordResetStore, + Twilio, UserKeyStore, UserQuery ] @@ -1060,7 +1064,9 @@ activateWithCurrency :: Maybe Currency.Alpha -> ExceptT ActivationError (AppT r) ActivationResult activateWithCurrency tgt code usr cur = do - key <- liftSemE $ mkActivationKey tgt + tc <- view twilioCreds + m <- view httpManager + key <- liftSemE $ mkActivationKey tgt tc m lift . Log.info $ field "activation.key" (toByteString key) . field "activation.code" (toByteString code) @@ -1082,12 +1088,20 @@ activateWithCurrency tgt code usr cur = do for_ tid $ \t -> wrapHttp $ Intra.changeTeamStatus t Team.Active cur preverify :: - Members '[ActivationKeyStore, ActivationSupply] r => + Members + '[ ActivationKeyStore, + ActivationSupply, + P.Error Twilio.ErrorResponse, + Twilio + ] + r => ActivationTarget -> ActivationCode -> + Twilio.Credentials -> + Manager -> ExceptT ActivationError (Sem r) () -preverify tgt code = do - key <- mkActivationKey tgt +preverify tgt code creds m = do + key <- mkActivationKey tgt creds m void $ Data.verifyCode key code onActivated :: @@ -1213,27 +1227,31 @@ sendActivationCode emailOrPhone loc call = do _otherwise -> sendActivationMail em name p loc' ident --- TODO(md): polysemize this, it is straightforward given that everything --- underneath should be in Polysemy mkActivationKey :: - Member ActivationSupply r => + Members + '[ ActivationSupply, + P.Error Twilio.ErrorResponse, + Twilio + ] + r => ActivationTarget -> + Twilio.Credentials -> + Manager -> ExceptT ActivationError (Sem r) ActivationKey -mkActivationKey (ActivateKey k) = pure k -mkActivationKey (ActivateEmail e) = do +mkActivationKey (ActivateKey k) _c _m = pure k +mkActivationKey (ActivateEmail e) _c _m = do ek <- either (throwE . InvalidActivationEmail e) (pure . userEmailKey) (validateEmail e) lift $ Data.makeActivationKey ek -mkActivationKey (ActivatePhone p) = do +mkActivationKey (ActivatePhone p) c m = do pk <- maybe (throwE $ InvalidActivationPhone p) (pure . userPhoneKey) - =<< undefined - -- =<< lift (validatePhone p) + =<< lift (validatePhone c m p) lift $ Data.makeActivationKey pk ------------------------------------------------------------------------------- From 011b2c3f505b0df5153011715a7c96b4e7cf37b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 12 Aug 2022 15:43:03 +0200 Subject: [PATCH 18/41] Add back code for Birg.User.Auth.legalHoldLogin --- services/brig/src/Brig/User/Auth.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index eb6e29fac6..0de32c6d2b 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -589,11 +589,11 @@ legalHoldLogin :: LegalHoldLogin -> CookieType -> ExceptT LegalHoldLoginError m (Access ZAuth.LegalHoldUser) -legalHoldLogin (LegalHoldLogin uid _plainTextPassword label) typ = do - -- locale <- Opt.setDefaultUserLocale <$> view settings - -- locDomain <- qualifyLocal () - -- o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid plainTextPassword - -- except o !>> LegalHoldReAuthError +legalHoldLogin (LegalHoldLogin uid plainTextPassword label) typ = do + locale <- Opt.setDefaultUserLocale <$> view settings + locDomain <- qualifyLocal () + o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid plainTextPassword + except o !>> LegalHoldReAuthError -- legalhold login is only possible if -- the user is a team user -- and the team has legalhold enabled From 419a63e72fce154f6bc69319cb9705b7157b5bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 12 Aug 2022 16:23:18 +0200 Subject: [PATCH 19/41] Move a utility function --- services/brig/src/Brig/Sem/Common.hs | 24 +++++++++++++++++++ .../brig/src/Brig/Sem/GalleyAccess/Http.hs | 19 +-------------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/services/brig/src/Brig/Sem/Common.hs b/services/brig/src/Brig/Sem/Common.hs index d72a283ab2..8a662ddf3e 100644 --- a/services/brig/src/Brig/Sem/Common.hs +++ b/services/brig/src/Brig/Sem/Common.hs @@ -17,8 +17,18 @@ module Brig.Sem.Common where +import qualified Bilge as RPC +import Bilge.IO +import Bilge.RPC +import Bilge.Request +import Bilge.Retry import Cassandra +import Control.Monad.Catch +import Control.Retry +import qualified Data.ByteString.Lazy as LBS import Imports +import Network.HTTP.Client (Response) +import Network.HTTP.Types.Method import Polysemy interpretClientToIO :: @@ -28,3 +38,17 @@ interpretClientToIO :: Sem r a interpretClientToIO ctx = interpret $ \case Embed action -> embedFinal @IO $ runClient ctx action + +makeReq :: + (MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + RPC.Request -> + StdMethod -> + (Request -> Request) -> + m (Response (Maybe LBS.ByteString)) +makeReq galley m r = + recovering x3 rpcHandlers $ + const $ + rpc' "galley" galley (method m . r) + +x3 :: RetryPolicy +x3 = limitRetries 3 <> exponentialBackoff 100000 diff --git a/services/brig/src/Brig/Sem/GalleyAccess/Http.hs b/services/brig/src/Brig/Sem/GalleyAccess/Http.hs index 743a784fc1..e0fc589e89 100644 --- a/services/brig/src/Brig/Sem/GalleyAccess/Http.hs +++ b/services/brig/src/Brig/Sem/GalleyAccess/Http.hs @@ -21,15 +21,12 @@ import qualified Bilge as RPC import Bilge.IO import Bilge.RPC import Bilge.Request -import Bilge.Retry import qualified Brig.RPC.Decode as RPC +import Brig.Sem.Common import Brig.Sem.GalleyAccess import Control.Monad.Catch -import Control.Retry import Data.ByteString.Conversion.To -import qualified Data.ByteString.Lazy as LBS import Imports -import Network.HTTP.Client (Response) import Network.HTTP.Types.Method import Polysemy import Wire.API.Team.Feature @@ -61,17 +58,3 @@ galleyAccessToHttp g = response <- makeReq g GET req wsStatus @SndFactorPasswordChallengeConfig <$> RPC.decodeBody "galley" response - -makeReq :: - (MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => - RPC.Request -> - StdMethod -> - (Request -> Request) -> - m (Response (Maybe LBS.ByteString)) -makeReq galley m r = - recovering x3 rpcHandlers $ - const $ - rpc' "galley" galley (method m . r) - -x3 :: RetryPolicy -x3 = limitRetries 3 <> exponentialBackoff 100000 From 3b9514865dd7d158eec36d3993d0a7401e27b6b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 12 Aug 2022 17:12:52 +0200 Subject: [PATCH 20/41] Define the GundeckAccess effect --- services/brig/brig.cabal | 2 + services/brig/src/Brig/Sem/Common.hs | 11 +- .../brig/src/Brig/Sem/GalleyAccess/Http.hs | 2 +- services/brig/src/Brig/Sem/GundeckAccess.hs | 43 +++ .../brig/src/Brig/Sem/GundeckAccess/Http.hs | 305 ++++++++++++++++++ 5 files changed, 359 insertions(+), 4 deletions(-) create mode 100644 services/brig/src/Brig/Sem/GundeckAccess.hs create mode 100644 services/brig/src/Brig/Sem/GundeckAccess/Http.hs diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 425dce2fa4..f735e9456a 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -100,6 +100,8 @@ library Brig.Sem.Common Brig.Sem.GalleyAccess Brig.Sem.GalleyAccess.Http + Brig.Sem.GundeckAccess + Brig.Sem.GundeckAccess.Http Brig.Sem.PasswordResetStore Brig.Sem.PasswordResetStore.CodeStore Brig.Sem.PasswordResetSupply diff --git a/services/brig/src/Brig/Sem/Common.hs b/services/brig/src/Brig/Sem/Common.hs index 8a662ddf3e..12bfd250e7 100644 --- a/services/brig/src/Brig/Sem/Common.hs +++ b/services/brig/src/Brig/Sem/Common.hs @@ -40,15 +40,20 @@ interpretClientToIO ctx = interpret $ \case Embed action -> embedFinal @IO $ runClient ctx action makeReq :: - (MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + ( MonadIO m, + MonadMask m, + MonadHttp m, + HasRequestId m + ) => + LText -> RPC.Request -> StdMethod -> (Request -> Request) -> m (Response (Maybe LBS.ByteString)) -makeReq galley m r = +makeReq component cReq m r = recovering x3 rpcHandlers $ const $ - rpc' "galley" galley (method m . r) + rpc' component cReq (method m . r) x3 :: RetryPolicy x3 = limitRetries 3 <> exponentialBackoff 100000 diff --git a/services/brig/src/Brig/Sem/GalleyAccess/Http.hs b/services/brig/src/Brig/Sem/GalleyAccess/Http.hs index e0fc589e89..9258b156de 100644 --- a/services/brig/src/Brig/Sem/GalleyAccess/Http.hs +++ b/services/brig/src/Brig/Sem/GalleyAccess/Http.hs @@ -55,6 +55,6 @@ galleyAccessToHttp g = featureNameBS @SndFactorPasswordChallengeConfig ] . expect2xx - response <- makeReq g GET req + response <- makeReq "galley" g GET req wsStatus @SndFactorPasswordChallengeConfig <$> RPC.decodeBody "galley" response diff --git a/services/brig/src/Brig/Sem/GundeckAccess.hs b/services/brig/src/Brig/Sem/GundeckAccess.hs new file mode 100644 index 0000000000..0f8c6c12a4 --- /dev/null +++ b/services/brig/src/Brig/Sem/GundeckAccess.hs @@ -0,0 +1,43 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.GundeckAccess where + +import Brig.Types.User.Event +import Data.Id +import Data.List.NonEmpty +import Gundeck.Types.Push.V2 +import Imports +import Polysemy + +data GundeckAccess m a where + PushEvents :: + -- | The events to push. + NonEmpty Event -> + -- | The users to push to. + NonEmpty UserId -> + -- | The originator of the events. + UserId -> + -- | The push routing strategy. + Route -> + -- | The originating device connection. + Maybe ConnId -> + GundeckAccess m () + +makeSem ''GundeckAccess diff --git a/services/brig/src/Brig/Sem/GundeckAccess/Http.hs b/services/brig/src/Brig/Sem/GundeckAccess/Http.hs new file mode 100644 index 0000000000..d857a2f93d --- /dev/null +++ b/services/brig/src/Brig/Sem/GundeckAccess/Http.hs @@ -0,0 +1,305 @@ +{-# LANGUAGE RecordWildCards #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Sem.GundeckAccess.Http (gundeckAccessToHttp) where + +import qualified Bilge as RPC +import Bilge.IO +import Bilge.RPC +import Bilge.Request +import Brig.RPC +import Brig.Sem.Common +import Brig.Sem.GundeckAccess +import Brig.Types.User.Event +import Control.Lens ((.~), (?~)) +import Control.Monad.Catch +import Data.Aeson (object, (.=)) +import qualified Data.Aeson as A +import qualified Data.Aeson.KeyMap as A +import Data.Id +import Data.Json.Util ((#)) +import Data.List.NonEmpty +import Data.List.Split (chunksOf) +import Data.Qualified +import Data.Range +import qualified Data.Set as Set +import Gundeck.Types.Push.V2 +import Imports hiding (toList) +import Network.HTTP.Types.Method +import Polysemy +import System.Logger.Class as Log hiding (name, (.=)) +import Wire.API.Connection +import Wire.API.Properties +import Wire.API.User +import Wire.API.User.Client + +gundeckAccessToHttp :: + forall m r a. + ( MonadIO m, + MonadLogger m, + MonadMask m, + MonadHttp m, + HasRequestId m, + Member (Embed m) r + ) => + RPC.Request -> + Sem (GundeckAccess ': r) a -> + Sem r a +gundeckAccessToHttp g = + interpret $ + embed @m . \case + PushEvents events users orig route mConn -> do + push g events users orig route mConn + +-- | Push events to other users. +push :: + ( MonadIO m, + Log.MonadLogger m, + MonadMask m, + MonadCatch m, + MonadHttp m, + HasRequestId m + ) => + -- | The request to the Gundeck component + RPC.Request -> + -- | The events to push. + NonEmpty Event -> + -- | The users to push to. + NonEmpty UserId -> + -- | The originator of the events. + UserId -> + -- | The push routing strategy. + Route -> + -- | The originating device connection. + Maybe ConnId -> + m () +push g (toList -> events) usrs orig route conn = + case mapMaybe toPushData events of + [] -> pure () + x : xs -> rawPush g (x :| xs) usrs orig route conn + where + toPushData :: Event -> Maybe (Builder, (A.Object, Maybe ApsData)) + toPushData e = case toPushFormat e of + Just o -> Just (Log.bytes e, (o, toApsData e)) + Nothing -> Nothing + +-- | Push encoded events to other users. Useful if you want to push +-- something that's not defined in Brig. +rawPush :: + ( MonadIO m, + Log.MonadLogger m, + MonadMask m, + MonadCatch m, + MonadHttp m, + HasRequestId m + ) => + -- | The request to the Gundeck component + RPC.Request -> + -- | The events to push. + NonEmpty (Builder, (A.Object, Maybe ApsData)) -> + -- | The users to push to. + NonEmpty UserId -> + -- | The originator of the events. + UserId -> + -- | The push routing strategy. + Route -> + -- | The originating device connection. + Maybe ConnId -> + m () +-- TODO: if we decide to have service whitelist events in Brig instead of +-- Galley, let's merge 'push' and 'rawPush' back. See Note [whitelist events]. +rawPush g (toList -> events) usrs orig route conn = do + for_ events $ \e -> debug $ remote "gundeck" . msg (fst e) + let gReq rs = + method POST + . path "/i/push/v2" + . zUser orig -- FUTUREWORK: Remove, because gundeck handler ignores this. + . json (fmap (mkPush rs . snd) events) + . expect2xx + forM_ recipients $ \rcps -> makeReq "gundeck" g POST (gReq rcps) + where + recipients :: [Range 1 1024 (Set.Set Recipient)] + recipients = + fmap (unsafeRange . Set.fromList) $ + chunksOf 512 $ + fmap (`recipient` route) $ + toList usrs + mkPush :: Range 1 1024 (Set.Set Recipient) -> (A.Object, Maybe ApsData) -> Push + mkPush rcps (o, aps) = + newPush + (Just orig) + rcps + (singletonPayload o) + & pushOriginConnection .~ conn + & pushNativeAps .~ aps + +toPushFormat :: Event -> Maybe A.Object +toPushFormat (UserEvent (UserCreated u)) = + Just $ + A.fromList + [ "type" .= ("user.new" :: Text), + "user" .= SelfProfile (u {userIdentity = Nothing}) + ] +toPushFormat (UserEvent (UserActivated u)) = + Just $ + A.fromList + [ "type" .= ("user.activate" :: Text), + "user" .= SelfProfile u + ] +toPushFormat (UserEvent (UserUpdated (UserUpdatedData i n pic acc ass hdl loc mb ssoId ssoIdDel))) = + Just $ + A.fromList + [ "type" .= ("user.update" :: Text), + "user" + .= object + ( "id" .= i + # "name" .= n + # "picture" .= pic -- DEPRECATED + # "accent_id" .= acc + # "assets" .= ass + # "handle" .= hdl + # "locale" .= loc + # "managed_by" .= mb + # "sso_id" .= ssoId + # "sso_id_deleted" .= ssoIdDel + # [] + ) + ] +toPushFormat (UserEvent (UserIdentityUpdated UserIdentityUpdatedData {..})) = + Just $ + A.fromList + [ "type" .= ("user.update" :: Text), + "user" + .= object + ( "id" .= eiuId + # "email" .= eiuEmail + # "phone" .= eiuPhone + # [] + ) + ] +toPushFormat (UserEvent (UserIdentityRemoved (UserIdentityRemovedData i e p))) = + Just $ + A.fromList + [ "type" .= ("user.identity-remove" :: Text), + "user" + .= object + ( "id" .= i + # "email" .= e + # "phone" .= p + # [] + ) + ] +toPushFormat (ConnectionEvent (ConnectionUpdated uc _ name)) = + Just $ + A.fromList $ + "type" .= ("user.connection" :: Text) + # "connection" .= uc + # "user" .= case name of + Just n -> Just $ object ["name" .= n] + Nothing -> Nothing + # [] +toPushFormat (UserEvent (UserSuspended i)) = + Just $ + A.fromList + [ "type" .= ("user.suspend" :: Text), + "id" .= i + ] +toPushFormat (UserEvent (UserResumed i)) = + Just $ + A.fromList + [ "type" .= ("user.resume" :: Text), + "id" .= i + ] +toPushFormat (UserEvent (UserDeleted qid)) = + Just $ + A.fromList + [ "type" .= ("user.delete" :: Text), + "id" .= qUnqualified qid, + "qualified_id" .= qid + ] +toPushFormat (UserEvent (UserLegalHoldDisabled i)) = + Just $ + A.fromList + [ "type" .= ("user.legalhold-disable" :: Text), + "id" .= i + ] +toPushFormat (UserEvent (UserLegalHoldEnabled i)) = + Just $ + A.fromList + [ "type" .= ("user.legalhold-enable" :: Text), + "id" .= i + ] +toPushFormat (PropertyEvent (PropertySet _ k v)) = + Just $ + A.fromList + [ "type" .= ("user.properties-set" :: Text), + "key" .= k, + "value" .= propertyValue v + ] +toPushFormat (PropertyEvent (PropertyDeleted _ k)) = + Just $ + A.fromList + [ "type" .= ("user.properties-delete" :: Text), + "key" .= k + ] +toPushFormat (PropertyEvent (PropertiesCleared _)) = + Just $ + A.fromList + [ "type" .= ("user.properties-clear" :: Text) + ] +toPushFormat (ClientEvent (ClientAdded _ c)) = + Just $ + A.fromList + [ "type" .= ("user.client-add" :: Text), + "client" .= c + ] +toPushFormat (ClientEvent (ClientRemoved _ c)) = + Just $ + A.fromList + [ "type" .= ("user.client-remove" :: Text), + "client" .= IdObject (clientId c) + ] +toPushFormat (UserEvent (LegalHoldClientRequested payload)) = + let LegalHoldClientRequestedData targetUser lastPrekey' clientId = payload + in Just $ + A.fromList + [ "type" .= ("user.legalhold-request" :: Text), + "id" .= targetUser, + "last_prekey" .= lastPrekey', + "client" .= IdObject clientId + ] + +toApsData :: Event -> Maybe ApsData +toApsData (ConnectionEvent (ConnectionUpdated uc _ name)) = + case (ucStatus uc, name) of + (MissingLegalholdConsent, _) -> Nothing + (Pending, n) -> apsConnRequest <$> n + (Accepted, n) -> apsConnAccept <$> n + (Blocked, _) -> Nothing + (Ignored, _) -> Nothing + (Sent, _) -> Nothing + (Cancelled, _) -> Nothing + where + apsConnRequest n = + apsData (ApsLocKey "push.notification.connection.request") [fromName n] + & apsSound ?~ ApsSound "new_message_apns.caf" + apsConnAccept n = + apsData (ApsLocKey "push.notification.connection.accepted") [fromName n] + & apsSound ?~ ApsSound "new_message_apns.caf" +toApsData _ = Nothing From 69da294f108fe3aed642c7a560a96eb9466a5a48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 18 Aug 2022 14:59:58 +0200 Subject: [PATCH 21/41] Use the GundeckAccess effect --- services/brig/src/Brig/API.hs | 2 + services/brig/src/Brig/API/Client.hs | 55 +- services/brig/src/Brig/API/Connection.hs | 17 +- .../brig/src/Brig/API/Connection/Remote.hs | 17 +- services/brig/src/Brig/API/Federation.hs | 28 +- services/brig/src/Brig/API/Internal.hs | 70 ++- services/brig/src/Brig/API/Properties.hs | 24 +- services/brig/src/Brig/API/Public.hs | 106 +++- services/brig/src/Brig/API/User.hs | 224 +++++-- .../brig/src/Brig/CanonicalInterpreter.hs | 4 + services/brig/src/Brig/IO/Intra.hs | 581 ++++++------------ .../brig/src/Brig/InternalEvent/Process.hs | 8 +- services/brig/src/Brig/Sem/Common.hs | 10 + services/brig/src/Brig/Sem/GalleyAccess.hs | 8 + .../brig/src/Brig/Sem/GalleyAccess/Http.hs | 51 +- services/brig/src/Brig/Team/API.hs | 36 +- services/brig/src/Brig/User/API/Auth.hs | 99 ++- services/brig/src/Brig/User/Auth.hs | 191 +++--- 18 files changed, 886 insertions(+), 645 deletions(-) diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index 510d92df61..f04b302e83 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -31,6 +31,7 @@ import Brig.Sem.ActivationSupply import Brig.Sem.BudgetStore import Brig.Sem.CodeStore import Brig.Sem.GalleyAccess +import Brig.Sem.GundeckAccess (GundeckAccess) import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import Brig.Sem.Twilio @@ -63,6 +64,7 @@ sitemap :: Error ReAuthError, Error Twilio.ErrorResponse, GalleyAccess, + GundeckAccess, Input (Local ()), P.TinyLog, PasswordResetStore, diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 61c21c5317..d404976848 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -58,6 +58,7 @@ import Brig.IO.Intra (guardLegalhold) import qualified Brig.IO.Intra as Intra import qualified Brig.Options as Opt import Brig.Sem.GalleyAccess +import Brig.Sem.GundeckAccess (GundeckAccess) import Brig.Sem.UserQuery (UserQuery) import Brig.Sem.VerificationCodeStore import Brig.Types.Intra @@ -84,6 +85,7 @@ import qualified Data.Set as Set import Imports import Network.Wai.Utilities hiding (Error) import Polysemy +import Polysemy.Async import Polysemy.Error import Polysemy.Input import System.Logger.Class (field, msg, val, (~~)) @@ -135,7 +137,15 @@ lookupLocalPubClientsBulk :: [UserId] -> ExceptT ClientError (AppT r) (UserMap ( lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk addClient :: - Members '[GalleyAccess, Input (Local ()), UserQuery, VerificationCodeStore] r => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserQuery, + VerificationCodeStore + ] + r => UserId -> Maybe ConnId -> Maybe IP -> @@ -147,7 +157,15 @@ addClient = addClientWithReAuthPolicy Data.reAuthForNewClients -- a superset of the clients known to galley. addClientWithReAuthPolicy :: forall r. - Members '[GalleyAccess, Input (Local ()), UserQuery, VerificationCodeStore] r => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserQuery, + VerificationCodeStore + ] + r => Data.ReAuthPolicy -> UserId -> Maybe ConnId -> @@ -177,7 +195,8 @@ addClientWithReAuthPolicy policy u con ip new = do for_ old $ execDelete u con wrapHttp $ Intra.newClient u (clientId clt) Intra.onClientEvent u con (ClientAdded u clt) - when (clientType clt == LegalHoldClientType) $ wrapHttpClient $ Intra.onUserEvent u con (UserLegalHoldEnabled u) + when (clientType clt == LegalHoldClientType) $ + Intra.onUserEvent u con (UserLegalHoldEnabled u) when (count > 1) $ for_ (userEmail usr) $ \email -> @@ -214,7 +233,13 @@ updateClient u c r = do -- nb. We must ensure that the set of clients known to brig is always -- a superset of the clients known to galley. rmClient :: - Members '[Error ReAuthError, Input (Local ()), UserQuery] r => + Members + '[ Error ReAuthError, + GundeckAccess, + Input (Local ()), + UserQuery + ] + r => UserId -> ConnId -> ClientId -> @@ -388,7 +413,12 @@ claimLocalMultiPrekeyBundles protectee userClients = do -- Utilities -- | Perform an orderly deletion of an existing client. -execDelete :: UserId -> Maybe ConnId -> Client -> AppT r () +execDelete :: + Members '[GundeckAccess] r => + UserId -> + Maybe ConnId -> + Client -> + AppT r () execDelete u con c = do wrapHttp $ Intra.rmClient u (clientId c) for_ (clientCookie c) $ \l -> wrapClient $ Auth.revokeCookies u [] [l] @@ -432,9 +462,13 @@ pubClient c = pubClientClass = clientClass c } -legalHoldClientRequested :: UserId -> LegalHoldClientRequest -> (AppT r) () +legalHoldClientRequested :: + Members '[Async, GalleyAccess, GundeckAccess] r => + UserId -> + LegalHoldClientRequest -> + AppT r () legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPrekey') = - wrapHttpClient $ Intra.onUserEvent targetUser Nothing lhClientEvent + Intra.onUserEvent targetUser Nothing lhClientEvent where clientId :: ClientId clientId = clientIdFromPrekey $ unpackLastPrekey lastPrekey' @@ -443,11 +477,14 @@ legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPreke lhClientEvent :: UserEvent lhClientEvent = LegalHoldClientRequested eventData -removeLegalHoldClient :: UserId -> (AppT r) () +removeLegalHoldClient :: + Members '[Async, GalleyAccess, GundeckAccess] r => + UserId -> + AppT r () removeLegalHoldClient uid = do clients <- wrapClient $ Data.lookupClients uid -- Should only be one; but just in case we'll treat it as a list let legalHoldClients = filter ((== LegalHoldClientType) . clientType) clients -- maybe log if this isn't the case forM_ legalHoldClients (execDelete uid Nothing) - wrapHttpClient $ Intra.onUserEvent uid Nothing (UserLegalHoldDisabled uid) + Intra.onUserEvent uid Nothing (UserLegalHoldDisabled uid) diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index e9af2498f0..862805f10c 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -40,6 +40,7 @@ import qualified Brig.Data.Connection as Data import Brig.Data.Types (resultHasMore, resultList) import qualified Brig.Data.User as Data import qualified Brig.IO.Intra as Intra +import Brig.Sem.GundeckAccess (GundeckAccess) import Brig.Sem.UserQuery (UserQuery) import Brig.Types.Connection import Brig.Types.User.Event @@ -53,6 +54,7 @@ import Data.Range import qualified Data.UUID.V4 as UUID import Imports import Polysemy +import Polysemy.Async hiding (cancel) import qualified System.Logger.Class as Log import System.Logger.Message import Wire.API.Connection hiding (relationWithHistory) @@ -77,7 +79,7 @@ ensureNotSameTeam self target = do throwE ConnectSameBindingTeamUsers createConnection :: - Member UserQuery r => + Members '[Async, GundeckAccess, UserQuery] r => Local UserId -> ConnId -> Qualified UserId -> @@ -98,7 +100,7 @@ createConnection self con target = do createConnectionToLocalUser :: forall r. - Member UserQuery r => + Members '[Async, GundeckAccess, UserQuery] r => Local UserId -> ConnId -> Local UserId -> @@ -209,7 +211,7 @@ checkLegalholdPolicyConflict uid1 uid2 = do oneway status2 status1 updateConnection :: - Member UserQuery r => + Members '[Async, GundeckAccess, UserQuery] r => Local UserId -> Qualified UserId -> Relation -> @@ -230,7 +232,7 @@ updateConnection self other newStatus conn = -- {#RefConnectionTeam} updateConnectionToLocalUser :: forall r. - Member UserQuery r => + Members '[Async, GundeckAccess, UserQuery] r => -- | From Local UserId -> -- | To @@ -344,7 +346,10 @@ updateConnectionToLocalUser self other newStatus conn = do Intra.onConnectionEvent (tUnqualified self) conn e2o lift . wrapClient $ Just <$> Data.updateConnection s2o (mkRelationWithHistory (error "impossible") new) - cancel :: UserConnection -> UserConnection -> ExceptT ConnectionError (AppT r) (Maybe UserConnection) + cancel :: + UserConnection -> + UserConnection -> + ExceptT ConnectionError (AppT r) (Maybe UserConnection) cancel s2o o2s = do lift . Log.info $ logLocalConnection (tUnqualified self) (qUnqualified (ucTo s2o)) @@ -389,7 +394,7 @@ mkRelationWithHistory oldRel = \case updateConnectionInternal :: forall r. - Member UserQuery r => + Members '[Async, GundeckAccess, UserQuery] r => UpdateConnectionsInternal -> ExceptT ConnectionError (AppT r) () updateConnectionInternal = \case diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index 572b51eb3c..d84d9f2581 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -29,6 +29,7 @@ import Brig.App import qualified Brig.Data.Connection as Data import Brig.Federation.Client (sendConnectionAction) import qualified Brig.IO.Intra as Intra +import Brig.Sem.GundeckAccess import Brig.Types.User.Event import Control.Comonad import Control.Error.Util ((??)) @@ -38,6 +39,8 @@ import Data.Qualified import Galley.Types.Conversations.Intra (Actor (..), DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (uuorConvId)) import Imports import Network.Wai.Utilities.Error +import Polysemy +import Polysemy.Async import Wire.API.Connection import Wire.API.Federation.API.Brig ( NewConnectionResponse (..), @@ -141,13 +144,14 @@ updateOne2OneConv lUsr _mbConn remoteUser mbConvId rel actor = do -- -- Returns the connection, and whether it was updated or not. transitionTo :: + Members '[Async, GundeckAccess] r => Local UserId -> Maybe ConnId -> Remote UserId -> Maybe UserConnection -> Maybe Relation -> Actor -> - (ConnectionM r) (ResponseForExistedCreated UserConnection, Bool) + ConnectionM r (ResponseForExistedCreated UserConnection, Bool) transitionTo self _ _ Nothing Nothing _ = -- This can only happen if someone tries to ignore as a first action on a -- connection. This shouldn't be possible. @@ -181,12 +185,18 @@ transitionTo self mzcon other (Just connection) (Just rel) actor = lift $ do pure (Existed connection', True) -- | Send an event to the local user when the state of a connection changes. -pushEvent :: Local UserId -> Maybe ConnId -> UserConnection -> (AppT r) () +pushEvent :: + Members '[Async, GundeckAccess] r => + Local UserId -> + Maybe ConnId -> + UserConnection -> + AppT r () pushEvent self mzcon connection = do let event = ConnectionUpdated connection Nothing Nothing Intra.onConnectionEvent (tUnqualified self) mzcon event performLocalAction :: + Members '[Async, GundeckAccess] r => Local UserId -> Maybe ConnId -> Remote UserId -> @@ -234,6 +244,7 @@ performLocalAction self mzcon other mconnection action = do -- B connects & A reacts: Accepted Accepted -- @ performRemoteAction :: + Members '[Async, GundeckAccess] r => Local UserId -> Remote UserId -> Maybe UserConnection -> @@ -251,6 +262,7 @@ performRemoteAction self other mconnection action = do reaction _ = Nothing createConnectionToRemoteUser :: + Members '[Async, GundeckAccess] r => Local UserId -> ConnId -> Remote UserId -> @@ -260,6 +272,7 @@ createConnectionToRemoteUser self zcon other = do fst <$> performLocalAction self (Just zcon) other mconnection LocalConnect updateConnectionToRemoteUser :: + Members '[Async, GundeckAccess] r => Local UserId -> Remote UserId -> Relation -> diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 16493f0c56..4a18707a1f 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -33,6 +33,7 @@ import Brig.App import qualified Brig.Data.Connection as Data import qualified Brig.Data.User as Data import Brig.IO.Intra (notify) +import Brig.Sem.GundeckAccess (GundeckAccess) import Brig.Sem.UserHandleStore import Brig.Sem.UserQuery import Brig.Types.User.Event @@ -46,17 +47,16 @@ import Data.Domain import Data.Handle (Handle (..), parseHandle) import Data.Id (ClientId, UserId) import Data.List.NonEmpty (nonEmpty) -import Data.List1 import Data.Qualified import Data.Range import qualified Gundeck.Types.Push as Push import Imports import Network.Wai.Utilities.Error ((!>>)) import Polysemy +import Polysemy.Async import Servant (ServerT) import Servant.API import qualified System.Logger.Class as Log -import UnliftIO.Async (pooledForConcurrentlyN_) import Wire.API.Connection import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Common @@ -75,7 +75,13 @@ import Wire.API.UserMap (UserMap) type FederationAPI = "federation" :> BrigApi federationSitemap :: - Members '[UserHandleStore, UserQuery] r => + Members + '[ Async, + GundeckAccess, + UserHandleStore, + UserQuery + ] + r => ServerT FederationAPI (Handler r) federationSitemap = Named @"api-version" (\_ _ -> pure versionInfo) @@ -92,7 +98,7 @@ federationSitemap = :<|> Named @"claim-key-packages" fedClaimKeyPackages sendConnectionAction :: - Members '[UserQuery] r => + Members '[Async, GundeckAccess, UserQuery] r => Domain -> NewConnectionRequest -> Handler r NewConnectionResponse @@ -212,7 +218,11 @@ getMLSClients :: Domain -> MLSClientsRequest -> Handler r (Set ClientId) getMLSClients _domain mcr = do Internal.getMLSClients (mcrUserId mcr) (mcrSignatureScheme mcr) -onUserDeleted :: Domain -> UserDeletedConnectionsNotification -> (Handler r) EmptyResponse +onUserDeleted :: + Members '[Async, GundeckAccess] r => + Domain -> + UserDeletedConnectionsNotification -> + Handler r EmptyResponse onUserDeleted origDomain udcn = lift $ do let deletedUser = toRemoteUnsafe origDomain (udcnUser udcn) connections = udcnConnections udcn @@ -221,8 +231,10 @@ onUserDeleted origDomain udcn = lift $ do map csv2From . filter (\x -> csv2Status x == Accepted) <$> wrapClient (Data.lookupRemoteConnectionStatuses (fromRange connections) (fmap pure deletedUser)) - wrapHttp $ - pooledForConcurrentlyN_ 16 (nonEmpty acceptedLocals) $ \(List1 -> recipients) -> - notify event (tUnqualified deletedUser) Push.RouteDirect Nothing (pure recipients) + -- wrapHttp $ + -- pooledForConcurrentlyN_ 16 (nonEmpty acceptedLocals) $ \(List1 -> recipients) -> + -- TODO(md): run this in an effect interpreter because this is purely an optimisation + for_ (nonEmpty acceptedLocals) $ \recipients -> + notify event (tUnqualified deletedUser) Push.RouteDirect Nothing (pure recipients) wrapClient $ Data.deleteRemoteConnections deletedUser connections pure EmptyResponse diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 4be1d8b01b..5120606bde 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -48,6 +48,7 @@ import Brig.Sem.ActivationKeyStore import Brig.Sem.ActivationSupply import Brig.Sem.CodeStore (CodeStore) import Brig.Sem.GalleyAccess +import Brig.Sem.GundeckAccess (GundeckAccess) import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import Brig.Sem.Twilio @@ -123,6 +124,8 @@ servantSitemap :: ActivationSupply, Async, BlacklistStore, + GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, PasswordResetStore, @@ -170,6 +173,8 @@ accountAPI :: ActivationSupply, Async, BlacklistStore, + GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, PasswordResetSupply, @@ -283,6 +288,7 @@ sitemap :: BlacklistStore, CodeStore, GalleyAccess, + GundeckAccess, Input (Local ()), P.Error ReAuthError, P.Error Twilio.ErrorResponse, @@ -464,7 +470,9 @@ sitemap = do -- | Add a client without authentication checks addClientInternalH :: Members - '[ GalleyAccess, + '[ Async, + GalleyAccess, + GundeckAccess, Input (Local ()), UserQuery, VerificationCodeStore @@ -478,7 +486,9 @@ addClientInternalH (usr ::: mSkipReAuth ::: req ::: connId ::: _) = do addClientInternal :: Members - '[ GalleyAccess, + '[ Async, + GalleyAccess, + GundeckAccess, Input (Local ()), UserQuery, VerificationCodeStore @@ -495,13 +505,19 @@ addClientInternal usr mSkipReAuth new connId = do | otherwise = Data.reAuthForNewClients API.addClientWithReAuthPolicy policy usr connId Nothing new !>> clientError -legalHoldClientRequestedH :: UserId ::: JsonRequest LegalHoldClientRequest ::: JSON -> (Handler r) Response +legalHoldClientRequestedH :: + Members '[Async, GalleyAccess, GundeckAccess] r => + UserId ::: JsonRequest LegalHoldClientRequest ::: JSON -> + Handler r Response legalHoldClientRequestedH (targetUser ::: req ::: _) = do clientRequest <- parseJsonBody req lift $ API.legalHoldClientRequested targetUser clientRequest pure $ setStatus status200 empty -removeLegalHoldClientH :: UserId ::: JSON -> (Handler r) Response +removeLegalHoldClientH :: + Members '[Async, GalleyAccess, GundeckAccess] r => + UserId ::: JSON -> + Handler r Response removeLegalHoldClientH (uid ::: _) = do lift $ API.removeLegalHoldClient uid pure $ setStatus status200 empty @@ -527,7 +543,10 @@ createUserNoVerify :: Members '[ ActivationKeyStore, ActivationSupply, + Async, BlacklistStore, + GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, PasswordResetSupply, @@ -557,6 +576,8 @@ createUserNoVerifySpar :: '[ ActivationKeyStore, ActivationSupply, Async, + GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, PasswordResetStore, @@ -751,10 +772,13 @@ newtype GetPasswordResetCodeResp = GetPasswordResetCodeResp (PasswordResetKey, P instance ToJSON GetPasswordResetCodeResp where toJSON (GetPasswordResetCodeResp (k, c)) = object ["key" .= k, "code" .= c] -changeAccountStatusH :: UserId ::: JsonRequest AccountStatusUpdate -> (Handler r) Response +changeAccountStatusH :: + Members '[Async, GalleyAccess, GundeckAccess, UserQuery] r => + UserId ::: JsonRequest AccountStatusUpdate -> + Handler r Response changeAccountStatusH (usr ::: req) = do status <- suStatus <$> parseJsonBody req - wrapHttpClientE (API.changeSingleAccountStatus usr status) !>> accountStatusError + API.changeSingleAccountStatus usr status !>> accountStatusError pure empty getAccountStatusH :: JSON ::: UserId -> (Handler r) Response @@ -789,7 +813,15 @@ getConnectionsStatus (ConnectionsStatusRequestV2 froms mtos mrel) = do filterByRelation l rel = filter ((== rel) . csv2Status) l revokeIdentityH :: - Members '[Input (Local ()), UserKeyStore, UserQuery] r => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserKeyStore, + UserQuery + ] + r => Either Email Phone -> Handler r Response revokeIdentityH emailOrPhone = do @@ -797,7 +829,7 @@ revokeIdentityH emailOrPhone = do pure $ setStatus status200 empty updateConnectionInternalH :: - Member UserQuery r => + Members '[Async, GundeckAccess, UserQuery] r => JSON ::: JsonRequest UpdateConnectionsInternal -> (Handler r) Response updateConnectionInternalH (_ ::: req) = do @@ -841,22 +873,28 @@ addPhonePrefixH (_ ::: req) = do void . lift $ API.phonePrefixInsert prefix pure empty -updateSSOIdH :: UserId ::: JSON ::: JsonRequest UserSSOId -> (Handler r) Response +updateSSOIdH :: + Members '[Async, GalleyAccess, GundeckAccess] r => + UserId ::: JSON ::: JsonRequest UserSSOId -> + Handler r Response updateSSOIdH (uid ::: _ ::: req) = do ssoid :: UserSSOId <- parseJsonBody req success <- lift $ wrapClient $ Data.updateSSOId uid (Just ssoid) if success then do - lift $ wrapHttpClient $ Intra.onUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOId = Just ssoid})) + lift $ Intra.onUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOId = Just ssoid})) pure empty else pure . setStatus status404 $ plain "User does not exist or has no team." -deleteSSOIdH :: UserId ::: JSON -> (Handler r) Response +deleteSSOIdH :: + Members '[Async, GalleyAccess, GundeckAccess] r => + UserId ::: JSON -> + Handler r Response deleteSSOIdH (uid ::: _) = do success <- lift $ wrapClient $ Data.updateSSOId uid Nothing if success then do - lift $ wrapHttpClient $ Intra.onUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOIdRemoved = True})) + lift $ Intra.onUserEvent uid Nothing (UserUpdated ((emptyUserUpdatedData uid) {eupSSOIdRemoved = True})) pure empty else pure . setStatus status404 $ plain "User does not exist or has no team." @@ -911,6 +949,8 @@ updateHandleH :: Members '[ Async, Race, + GalleyAccess, + GundeckAccess, Resource, UniqueClaimsStore, UserHandleStore, @@ -924,6 +964,8 @@ updateHandleH (uid ::: _ ::: body) = empty <$ (updateHandle uid =<< parseJsonBod updateHandle :: Members '[ Async, + GalleyAccess, + GundeckAccess, Race, Resource, UniqueClaimsStore, @@ -939,13 +981,13 @@ updateHandle uid (HandleUpdate handleUpd) = do API.changeHandle uid Nothing handle API.AllowSCIMUpdates !>> changeHandleError updateUserNameH :: - Member UserQuery r => + Members '[Async, GalleyAccess, GundeckAccess, UserQuery] r => UserId ::: JSON ::: JsonRequest NameUpdate -> (Handler r) Response updateUserNameH (uid ::: _ ::: body) = empty <$ (updateUserName uid =<< parseJsonBody body) updateUserName :: - Member UserQuery r => + Members '[Async, GalleyAccess, GundeckAccess, UserQuery] r => UserId -> NameUpdate -> (Handler r) () diff --git a/services/brig/src/Brig/API/Properties.hs b/services/brig/src/Brig/API/Properties.hs index 00004eeb0d..8c51e983b1 100644 --- a/services/brig/src/Brig/API/Properties.hs +++ b/services/brig/src/Brig/API/Properties.hs @@ -30,23 +30,41 @@ import Brig.App import Brig.Data.Properties (PropertiesDataError) import qualified Brig.Data.Properties as Data import qualified Brig.IO.Intra as Intra +import Brig.Sem.GundeckAccess (GundeckAccess) import Brig.Types.User.Event import Control.Error import Data.Id import Imports +import Polysemy +import Polysemy.Async import Wire.API.Properties -setProperty :: UserId -> ConnId -> PropertyKey -> PropertyValue -> ExceptT PropertiesDataError (AppT r) () +setProperty :: + Members '[Async, GundeckAccess] r => + UserId -> + ConnId -> + PropertyKey -> + PropertyValue -> + ExceptT PropertiesDataError (AppT r) () setProperty u c k v = do wrapClientE $ Data.insertProperty u k (propertyRaw v) lift $ Intra.onPropertyEvent u c (PropertySet u k v) -deleteProperty :: UserId -> ConnId -> PropertyKey -> AppT r () +deleteProperty :: + Members '[Async, GundeckAccess] r => + UserId -> + ConnId -> + PropertyKey -> + AppT r () deleteProperty u c k = do wrapClient $ Data.deleteProperty u k Intra.onPropertyEvent u c (PropertyDeleted u k) -clearProperties :: UserId -> ConnId -> AppT r () +clearProperties :: + Members '[Async, GundeckAccess] r => + UserId -> + ConnId -> + AppT r () clearProperties u c = do wrapClient $ Data.clearProperties u Intra.onPropertyEvent u c (PropertiesCleared u) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 94c297096b..7e8679657d 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -53,6 +53,7 @@ import Brig.Sem.ActivationSupply import Brig.Sem.BudgetStore import Brig.Sem.CodeStore (CodeStore) import Brig.Sem.GalleyAccess +import Brig.Sem.GundeckAccess (GundeckAccess) import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetSupply (PasswordResetSupply) import Brig.Sem.Twilio @@ -209,6 +210,7 @@ servantSitemap :: BlacklistStore, BlacklistPhonePrefixStore, GalleyAccess, + GundeckAccess, Input (Local ()), P.Error ReAuthError, P.Error Twilio.ErrorResponse, @@ -343,11 +345,13 @@ sitemap :: Members '[ ActivationKeyStore, ActivationSupply, + Async, BlacklistStore, BlacklistPhonePrefixStore, BudgetStore, CodeStore, GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, P.TinyLog, @@ -487,11 +491,13 @@ apiDocs :: Members '[ ActivationKeyStore, ActivationSupply, + Async, BlacklistStore, BlacklistPhonePrefixStore, BudgetStore, CodeStore, GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, P.TinyLog, @@ -519,7 +525,13 @@ apiDocs = --------------------------------------------------------------------------- -- Handlers -setProperty :: UserId -> ConnId -> Public.PropertyKey -> Public.RawPropertyValue -> Handler r () +setProperty :: + Members '[Async, GundeckAccess] r => + UserId -> + ConnId -> + Public.PropertyKey -> + Public.RawPropertyValue -> + Handler r () setProperty u c key raw = do checkPropertyKey key val <- safeParsePropertyValue raw @@ -558,10 +570,19 @@ parseStoredPropertyValue raw = case propertyValueFromRaw raw of . Log.field "parse_error" e throwStd internalServerError -deleteProperty :: UserId -> ConnId -> Public.PropertyKey -> Handler r () +deleteProperty :: + Members '[Async, GundeckAccess] r => + UserId -> + ConnId -> + Public.PropertyKey -> + Handler r () deleteProperty u c k = lift (API.deleteProperty u c k) -clearProperties :: UserId -> ConnId -> Handler r () +clearProperties :: + Members '[Async, GundeckAccess] r => + UserId -> + ConnId -> + Handler r () clearProperties u c = lift (API.clearProperties u c) getProperty :: UserId -> Public.PropertyKey -> Handler r (Maybe Public.RawPropertyValue) @@ -614,7 +635,9 @@ getMultiUserPrekeyBundleH zusr qualUserClients = do addClient :: Members - '[ GalleyAccess, + '[ Async, + GalleyAccess, + GundeckAccess, Input (Local ()), UserQuery, VerificationCodeStore @@ -636,7 +659,13 @@ addClient usr con ip new = do clientResponse client = Servant.addHeader (Public.clientId client) client deleteClient :: - Members '[Input (Local ()), P.Error ReAuthError, UserQuery] r => + Members + '[ GundeckAccess, + Input (Local ()), + P.Error ReAuthError, + UserQuery + ] + r => UserId -> ConnId -> ClientId -> @@ -716,7 +745,10 @@ createUser :: Members '[ ActivationKeyStore, ActivationSupply, + Async, BlacklistStore, + GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, PasswordResetStore, @@ -870,7 +902,13 @@ instance ToJSON GetActivationCodeResp where toJSON (GetActivationCodeResp (k, c)) = object ["key" .= k, "code" .= c] updateUser :: - Member UserQuery r => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + UserQuery + ] + r => UserId -> ConnId -> Public.UserUpdate -> @@ -903,7 +941,15 @@ changePhone u _ (Public.puPhone -> phone) = lift . exceptTToMaybe $ do lift . wrapClient $ sendActivationSms pn apair loc removePhone :: - Members '[Input (Local ()), UserKeyStore, UserQuery] r => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserKeyStore, + UserQuery + ] + r => UserId -> ConnId -> Handler r (Maybe Public.RemoveIdentityError) @@ -911,7 +957,15 @@ removePhone self conn = lift . exceptTToMaybe $ API.removePhone self conn removeEmail :: - Members '[Input (Local ()), UserKeyStore, UserQuery] r => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserKeyStore, + UserQuery + ] + r => UserId -> ConnId -> Handler r (Maybe Public.RemoveIdentityError) @@ -928,7 +982,12 @@ changePassword :: Handler r (Maybe Public.ChangePasswordError) changePassword u cp = lift . exceptTToMaybe $ API.changePassword u cp -changeLocale :: UserId -> ConnId -> Public.LocaleUpdate -> (Handler r) () +changeLocale :: + Members '[Async, GalleyAccess, GundeckAccess] r => + UserId -> + ConnId -> + Public.LocaleUpdate -> + Handler r () changeLocale u conn l = lift $ API.changeLocale u conn l -- | (zusr is ignored by this handler, ie. checking handles is allowed as long as you have @@ -972,6 +1031,8 @@ getHandleInfoUnqualifiedH self handle = do changeHandle :: Members '[ Async, + GalleyAccess, + GundeckAccess, Race, Resource, UniqueClaimsStore, @@ -1087,7 +1148,7 @@ customerExtensionCheckBlockedDomains email = do throwM $ customerExtensionBlockedDomain domain createConnectionUnqualified :: - Member UserQuery r => + Members '[Async, GundeckAccess, UserQuery] r => UserId -> ConnId -> Public.ConnectionRequest -> @@ -1098,7 +1159,7 @@ createConnectionUnqualified self conn cr = do API.createConnection lself conn (qUntagged target) !>> connError createConnection :: - Member UserQuery r => + Members '[Async, GundeckAccess, UserQuery] r => UserId -> ConnId -> Qualified UserId -> @@ -1108,7 +1169,7 @@ createConnection self conn target = do API.createConnection lself conn target !>> connError updateLocalConnection :: - Member UserQuery r => + Members '[Async, GundeckAccess, UserQuery] r => UserId -> ConnId -> UserId -> @@ -1119,7 +1180,7 @@ updateLocalConnection self conn other update = do updateConnection self conn (qUntagged lother) update updateConnection :: - Member UserQuery r => + Members '[Async, GundeckAccess, UserQuery] r => UserId -> ConnId -> Qualified UserId -> @@ -1191,7 +1252,10 @@ getConnection self other = do deleteSelfUser :: Members - '[ Input (Local ()), + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), UniqueClaimsStore, UserHandleStore, UserQuery, @@ -1206,7 +1270,10 @@ deleteSelfUser u body = verifyDeleteUserH :: Members - '[ Input (Local ()), + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), UniqueClaimsStore, UserHandleStore, UserQuery, @@ -1273,6 +1340,9 @@ activateKeyH :: Members '[ ActivationKeyStore, ActivationSupply, + Async, + GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, PasswordResetSupply, @@ -1292,6 +1362,9 @@ activateH :: Members '[ ActivationKeyStore, ActivationSupply, + Async, + GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, PasswordResetSupply, @@ -1311,6 +1384,9 @@ activate :: Members '[ ActivationKeyStore, ActivationSupply, + Async, + GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, PasswordResetSupply, diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index b6d6a1cc5a..07a0b01a1f 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -120,6 +120,8 @@ import Brig.Password import qualified Brig.Queue as Queue import Brig.Sem.ActivationKeyStore (ActivationKeyStore) import Brig.Sem.ActivationSupply (ActivationSupply) +import Brig.Sem.GalleyAccess +import Brig.Sem.GundeckAccess import Brig.Sem.PasswordResetStore (PasswordResetStore) import qualified Brig.Sem.PasswordResetStore as E import Brig.Sem.PasswordResetSupply (PasswordResetSupply) @@ -142,7 +144,7 @@ import Brig.User.Email import Brig.User.Handle import Brig.User.Handle.Blacklist import Brig.User.Phone -import Brig.User.Search.Index (MonadIndexIO, reindex) +import Brig.User.Search.Index (reindex) import qualified Brig.User.Search.TeamSize as TeamSize import Cassandra import Control.Arrow ((&&&)) @@ -178,7 +180,6 @@ import qualified Ropes.Twilio as Twilio import System.Logger.Class (MonadLogger) import qualified System.Logger.Class as Log import System.Logger.Message -import UnliftIO.Async hiding (Async) import Wire.API.Connection import Wire.API.Error import qualified Wire.API.Error.Brig as E @@ -246,6 +247,8 @@ createUserSpar :: forall r. Members '[ Async, + GalleyAccess, + GundeckAccess, Race, Resource, UniqueClaimsStore, @@ -273,7 +276,7 @@ createUserSpar new = do Just richInfo -> wrapClient $ Data.updateRichInfo uid richInfo Nothing -> pure () -- Nothing to do wrapHttp $ Intra.createSelfConv uid - wrapHttpClient $ Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) + Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) pure account @@ -318,9 +321,12 @@ createUser :: Members '[ ActivationKeyStore, ActivationSupply, + Async, BlacklistStore, - P.Error Twilio.ErrorResponse, + GalleyAccess, + GundeckAccess, Input (Local ()), + P.Error Twilio.ErrorResponse, PasswordResetSupply, PasswordResetStore, Twilio, @@ -384,7 +390,7 @@ createUser new = do liftSem $ Data.insertAccount account Nothing pw False wrapHttp $ Intra.createSelfConv uid - wrapHttpClient $ Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) + Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) pure account @@ -619,7 +625,13 @@ checkRestrictedUserCreation new = do -- Update Profile updateUser :: - Member UserQuery r => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + UserQuery + ] + r => UserId -> Maybe ConnId -> UserUpdate -> @@ -639,23 +651,43 @@ updateUser uid mconn uu allowScim = do $ throwE DisplayNameManagedByScim lift $ do wrapClient $ Data.updateUser uid uu - wrapHttpClient $ Intra.onUserEvent uid mconn (profileUpdated uid uu) + Intra.onUserEvent uid mconn (profileUpdated uid uu) ------------------------------------------------------------------------------- -- Update Locale -changeLocale :: UserId -> ConnId -> LocaleUpdate -> (AppT r) () +changeLocale :: + Members + '[ Async, + GalleyAccess, + GundeckAccess + ] + r => + UserId -> + ConnId -> + LocaleUpdate -> + AppT r () changeLocale uid conn (LocaleUpdate loc) = do wrapClient $ Data.updateLocale uid loc - wrapHttpClient $ Intra.onUserEvent uid (Just conn) (localeUpdate uid loc) + Intra.onUserEvent uid (Just conn) (localeUpdate uid loc) ------------------------------------------------------------------------------- -- Update ManagedBy -changeManagedBy :: UserId -> ConnId -> ManagedByUpdate -> (AppT r) () +changeManagedBy :: + Members + '[ Async, + GalleyAccess, + GundeckAccess + ] + r => + UserId -> + ConnId -> + ManagedByUpdate -> + AppT r () changeManagedBy uid conn (ManagedByUpdate mb) = do wrapClient $ Data.updateManagedBy uid mb - wrapHttpClient $ Intra.onUserEvent uid (Just conn) (managedByUpdate uid mb) + Intra.onUserEvent uid (Just conn) (managedByUpdate uid mb) -------------------------------------------------------------------------------- -- Change Handle @@ -663,6 +695,8 @@ changeManagedBy uid conn (ManagedByUpdate mb) = do changeHandle :: Members '[ Async, + GalleyAccess, + GundeckAccess, Race, Resource, UniqueClaimsStore, @@ -698,7 +732,7 @@ changeHandle uid mconn hdl allowScim = do claimed <- lift . liftSem $ claimHandle (userId u) (userHandle u) hdl unless claimed $ throwE ChangeHandleExists - lift $ wrapHttpClient $ Intra.onUserEvent uid mconn (handleUpdated uid hdl) + lift $ Intra.onUserEvent uid mconn (handleUpdated uid hdl) -------------------------------------------------------------------------------- -- Check Handle @@ -878,7 +912,15 @@ changePhone u phone = do -- Remove Email removeEmail :: - Members '[Input (Local ()), UserKeyStore, UserQuery] r => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserKeyStore, + UserQuery + ] + r => UserId -> ConnId -> ExceptT RemoveIdentityError (AppT r) () @@ -889,7 +931,7 @@ removeEmail uid conn = do Just (FullIdentity e _) -> lift $ do liftSem . deleteKey d $ userEmailKey e wrapClient $ Data.deleteEmail uid - wrapHttpClient $ Intra.onUserEvent uid (Just conn) (emailRemoved uid e) + Intra.onUserEvent uid (Just conn) (emailRemoved uid e) Just _ -> throwE LastIdentity Nothing -> throwE NoIdentity @@ -897,7 +939,15 @@ removeEmail uid conn = do -- Remove Phone removePhone :: - Members '[Input (Local ()), UserKeyStore, UserQuery] r => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserKeyStore, + UserQuery + ] + r => UserId -> ConnId -> ExceptT RemoveIdentityError (AppT r) () @@ -912,7 +962,7 @@ removePhone uid conn = do lift $ do liftSem . deleteKey d $ userPhoneKey p wrapClient $ Data.deletePhone uid - wrapHttpClient $ Intra.onUserEvent uid (Just conn) (phoneRemoved uid p) + Intra.onUserEvent uid (Just conn) (phoneRemoved uid p) Just _ -> throwE LastIdentity Nothing -> throwE NoIdentity @@ -921,7 +971,15 @@ removePhone uid conn = do revokeIdentity :: forall r. - Members '[Input (Local ()), UserKeyStore, UserQuery] r => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserKeyStore, + UserQuery + ] + r => Either Email Phone -> AppT r () revokeIdentity key = do @@ -949,72 +1007,89 @@ revokeIdentity key = do (\(_ :: Email) -> Data.deleteEmail u) (\(_ :: Phone) -> Data.deletePhone u) uk - wrapHttpClient $ - Intra.onUserEvent u Nothing $ - foldKey - (emailRemoved u) - (phoneRemoved u) - uk + Intra.onUserEvent u Nothing $ + foldKey + (emailRemoved u) + (phoneRemoved u) + uk ------------------------------------------------------------------------------- -- Change Account Status changeAccountStatus :: - forall m. - ( MonadClient m, - MonadLogger m, - MonadIndexIO m, - MonadReader Env m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m - ) => + forall r. + Members + '[ Async, + GalleyAccess, + GundeckAccess + ] + r => + -- ( MonadClient m, + -- MonadLogger m, + -- MonadIndexIO m, + -- MonadReader Env m, + -- MonadMask m, + -- MonadHttp m, + -- HasRequestId m, + -- MonadUnliftIO m + -- ) => List1 UserId -> AccountStatus -> - ExceptT AccountStatusError m () + ExceptT AccountStatusError (AppT r) () changeAccountStatus usrs status = do - ev <- mkUserEvent usrs status - lift $ mapConcurrently_ (update ev) usrs + ev <- wrapClientE $ mkUserEvent usrs status + lift $ do + -- mapConcurrently_ (update ev) usrs + -- TODO(md): do this updating concurrently, perhaps in an effect + traverse_ (update ev) usrs where update :: (UserId -> UserEvent) -> UserId -> - m () + AppT r () update ev u = do - Data.updateStatus u status + wrapClient $ Data.updateStatus u status Intra.onUserEvent u Nothing (ev u) changeSingleAccountStatus :: - forall m. - ( MonadClient m, - MonadLogger m, - MonadIndexIO m, - MonadReader Env m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m - ) => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + UserQuery + ] + r => + -- ( MonadClient m, + -- MonadLogger m, + -- MonadIndexIO m, + -- MonadReader Env m, + -- MonadMask m, + -- MonadHttp m, + -- HasRequestId m, + -- MonadUnliftIO m + -- ) => UserId -> AccountStatus -> - ExceptT AccountStatusError m () + ExceptT AccountStatusError (AppT r) () changeSingleAccountStatus uid status = do - unlessM (lift $ runM $ userQueryToCassandra @m @'[Embed m] $ Data.userExists uid) $ throwE AccountNotFound - ev <- mkUserEvent (List1.singleton uid) status - lift $ do - Data.updateStatus uid status - Intra.onUserEvent uid Nothing (ev uid) + unlessM (lift . liftSem $ Data.userExists uid) $ throwE AccountNotFound + ev <- wrapClientE $ mkUserEvent (singleton uid) status + wrapClientE $ Data.updateStatus uid status + lift $ Intra.onUserEvent uid Nothing (ev uid) mkUserEvent :: - (MonadUnliftIO m, Traversable t, MonadClient m) => + (Traversable t, MonadClient m) => t UserId -> AccountStatus -> ExceptT AccountStatusError m (UserId -> UserEvent) mkUserEvent usrs status = case status of Active -> pure UserResumed - Suspended -> lift $ mapConcurrently revokeAllCookies usrs >> pure UserSuspended + Suspended -> + lift $ + -- mapConcurrently revokeAllCookies usrs >> pure UserSuspended + -- TODO(md): implement concurrently traversing this as an effect + traverse revokeAllCookies usrs >> pure UserSuspended Deleted -> throwE InvalidAccountStatus Ephemeral -> throwE InvalidAccountStatus PendingInvitation -> throwE InvalidAccountStatus @@ -1026,6 +1101,9 @@ activate :: Members '[ ActivationKeyStore, ActivationSupply, + Async, + GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, PasswordResetSupply, @@ -1046,6 +1124,9 @@ activateWithCurrency :: Members '[ ActivationKeyStore, ActivationSupply, + Async, + GalleyAccess, + GundeckAccess, Input (Local ()), P.Error Twilio.ErrorResponse, PasswordResetSupply, @@ -1105,21 +1186,27 @@ preverify tgt code creds m = do void $ Data.verifyCode key code onActivated :: - Members '[UserQuery] r => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + UserQuery + ] + r => ActivationEvent -> AppT r (UserId, Maybe UserIdentity, Bool) onActivated (AccountActivated account) = do let uid = userId (accountUser account) Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.onActivated") Log.info $ field "user" (toByteString uid) . msg (val "User activated") - wrapHttpClient $ Intra.onUserEvent uid Nothing $ UserActivated (accountUser account) + Intra.onUserEvent uid Nothing $ UserActivated (accountUser account) pure (uid, userIdentity (accountUser account), True) onActivated (EmailActivated uid email) = do - wrapHttpClient $ Intra.onUserEvent uid Nothing (emailUpdated uid email) + Intra.onUserEvent uid Nothing (emailUpdated uid email) liftSem $ Data.deleteEmailUnvalidated uid pure (uid, Just (EmailIdentity email), False) onActivated (PhoneActivated uid phone) = do - wrapHttpClient $ Intra.onUserEvent uid Nothing (phoneUpdated uid phone) + Intra.onUserEvent uid Nothing (phoneUpdated uid phone) pure (uid, Just (PhoneIdentity phone), False) -- docs/reference/user/activation.md {#RefActivationRequest} @@ -1351,7 +1438,10 @@ mkPasswordResetKey ident = case ident of -- TODO: communicate deletions of SSO users to SSO service. deleteSelfUser :: Members - '[ Input (Local ()), + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), UniqueClaimsStore, UserHandleStore, UserQuery, @@ -1437,7 +1527,10 @@ deleteSelfUser uid pwd = do -- 'deleteUser'. Called via @post /delete@. verifyDeleteUser :: Members - '[ Input (Local ()), + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), UniqueClaimsStore, UserHandleStore, UserQuery, @@ -1463,7 +1556,10 @@ verifyDeleteUser d = do deleteAccount :: forall r. Members - '[ UniqueClaimsStore, + '[ Async, + GalleyAccess, + GundeckAccess, + UniqueClaimsStore, UserHandleStore, UserQuery ] @@ -1498,7 +1594,7 @@ deleteAccount account@(accountUser -> user) = do wrapHttp $ Intra.rmUser uid (userAssets user) wrapClient (Data.lookupClients uid >>= mapM_ (Data.rmClient uid . clientId)) luid <- qualifyLocal uid - wrapHttpClient $ Intra.onUserEvent uid Nothing (UserDeleted (qUntagged luid)) + Intra.onUserEvent uid Nothing (UserDeleted (qUntagged luid)) -- Note: Connections can only be deleted afterwards, since -- they need to be notified. wrapClient $ Data.deleteConnections uid diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index d1ca714966..a4285da1a5 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -40,6 +40,8 @@ import Brig.Sem.CodeStore.Cassandra (codeStoreToCassandra) import Brig.Sem.Common import Brig.Sem.GalleyAccess (GalleyAccess) import Brig.Sem.GalleyAccess.Http +import Brig.Sem.GundeckAccess (GundeckAccess) +import Brig.Sem.GundeckAccess.Http (gundeckAccessToHttp) import Brig.Sem.PasswordResetStore (PasswordResetStore) import Brig.Sem.PasswordResetStore.CodeStore (passwordResetStoreToCodeStore) import Brig.Sem.PasswordResetSupply (PasswordResetSupply) @@ -90,6 +92,7 @@ type BrigCanonicalEffects = ActivationSupply, UniqueClaimsStore, GalleyAccess, + GundeckAccess, Embed HttpClientIO, UserQuery, PasswordResetStore, @@ -128,6 +131,7 @@ runBrigToIO e (AppT ma) = . passwordResetStoreToCodeStore . userQueryToCassandra @Cas.Client . interpretHttpToIO e + . gundeckAccessToHttp @HttpClientIO (e ^. gundeck) . galleyAccessToHttp @HttpClientIO (e ^. galley) . uniqueClaimsStoreToCassandra @Cas.Client . activationSupplyToIO diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 51d1b6c57a..2caf74f45a 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -80,19 +80,19 @@ import qualified Brig.Data.Connection as Data import Brig.Federation.Client (notifyUserDeleted) import qualified Brig.IO.Journal as Journal import Brig.RPC +import Brig.Sem.GalleyAccess hiding (getTeamId, getTeamLegalHoldStatus) +import Brig.Sem.GundeckAccess import Brig.Types.User.Event import Brig.User.Search.Index (MonadIndexIO) import qualified Brig.User.Search.Index as Search import Cassandra (MonadClient) import Conduit (runConduit, (.|)) import Control.Error (ExceptT) -import Control.Error.Util -import Control.Lens (view, (.~), (?~), (^.)) +import Control.Lens (view, (^.)) import Control.Monad.Catch import Control.Monad.Trans.Except (runExceptT, throwE) import Control.Retry import Data.Aeson hiding (json) -import qualified Data.Aeson.KeyMap as KeyMap import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as BL import Data.Coerce (coerce) @@ -101,13 +101,11 @@ import qualified Data.Currency as Currency import Data.Domain import Data.Either.Combinators (whenLeft) import Data.Id -import Data.Json.Util (UTCTimeMillis, (#)) -import Data.List.Split (chunksOf) -import Data.List1 (List1, list1, singleton) +import Data.Json.Util (UTCTimeMillis) +import Data.List.NonEmpty import Data.Proxy import Data.Qualified import Data.Range -import qualified Data.Set as Set import GHC.TypeLits import Galley.Types.Conversations.Intra (UpsertOne2OneConversationRequest, UpsertOne2OneConversationResponse) import qualified Galley.Types.Teams as Team @@ -119,14 +117,14 @@ import Imports import Network.HTTP.Types.Method import Network.HTTP.Types.Status import qualified Network.Wai.Utilities.Error as Wai +import Polysemy +import Polysemy.Async import System.Logger.Class as Log hiding (name, (.=)) -import qualified System.Logger.Extended as ExLog import Wire.API.Connection -import Wire.API.Conversation +import Wire.API.Conversation hiding (Member) import Wire.API.Event.Conversation (Connect (Connect)) import Wire.API.Federation.API.Brig import Wire.API.Federation.Error -import Wire.API.Properties import Wire.API.Team import qualified Wire.API.Team.Conversation as Conv import Wire.API.Team.Feature @@ -142,76 +140,73 @@ import Wire.API.User.Client -- Event Handlers onUserEvent :: - ( MonadLogger m, - MonadCatch m, - MonadThrow m, - MonadIndexIO m, - MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m, - MonadClient m - ) => + Members + '[ Async, + GalleyAccess, + GundeckAccess + ] + r => UserId -> Maybe ConnId -> UserEvent -> - m () + AppT r () onUserEvent orig conn e = - updateSearchIndex orig e + wrapClient (updateSearchIndex orig e) *> dispatchNotifications orig conn e - *> journalEvent orig e + *> wrapClient (journalEvent orig e) -- TODO(md): this just needs MonadIO m and MonadReader + -- Env m. It seems to access AWS, so + -- perhaps that's an effect on its own. onConnectionEvent :: + Members '[Async, GundeckAccess] r => -- | Originator of the event. UserId -> -- | Client connection ID, if any. Maybe ConnId -> -- | The event. ConnectionEvent -> - (AppT r) () + AppT r () onConnectionEvent orig conn evt = do let from = ucFrom (ucConn evt) - wrapHttp $ - notify - (singleton $ ConnectionEvent evt) - orig - Push.RouteAny - conn - (pure $ list1 from []) + notify + (pure $ ConnectionEvent evt) + orig + Push.RouteAny + conn + (pure $ pure from) onPropertyEvent :: + Members '[Async, GundeckAccess] r => -- | Originator of the event. UserId -> -- | Client connection ID. ConnId -> PropertyEvent -> - (AppT r) () + AppT r () onPropertyEvent orig conn e = - wrapHttp $ - notify - (singleton $ PropertyEvent e) - orig - Push.RouteDirect - (Just conn) - (pure $ list1 orig []) + notify + (pure $ PropertyEvent e) + orig + Push.RouteDirect + (Just conn) + (pure $ pure orig) onClientEvent :: + Members '[GundeckAccess] r => -- | Originator of the event. UserId -> -- | Client connection ID. Maybe ConnId -> -- | The event. ClientEvent -> - (AppT r) () + AppT r () onClientEvent orig conn e = do - let events = singleton (ClientEvent e) - let rcps = list1 orig [] + let events = pure (ClientEvent e) + let rcps = pure orig -- Synchronous push for better delivery guarantees of these -- events and to make sure new clients have a first notification -- in the stream. - wrapHttp $ push events rcps orig Push.RouteAny conn + liftSem $ pushEvents events rcps orig Push.RouteAny conn updateSearchIndex :: ( MonadClient m, @@ -273,20 +268,16 @@ journalEvent orig e = case e of -- as well as his other clients about a change to his user account -- or profile. dispatchNotifications :: - ( MonadIO m, - Log.MonadLogger m, - MonadReader Env m, - MonadMask m, - MonadCatch m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m, - MonadClient m - ) => + Members + '[ Async, + GalleyAccess, + GundeckAccess + ] + r => UserId -> Maybe ConnId -> UserEvent -> - m () + AppT r () dispatchNotifications orig conn e = case e of UserCreated {} -> pure () UserSuspended {} -> pure () @@ -305,27 +296,18 @@ dispatchNotifications orig conn e = case e of -- n.b. Synchronously fetch the contact list on the current thread. -- If done asynchronously, the connections may already have been deleted. notifyUserDeletionLocals orig conn event - notifyUserDeletionRemotes orig + wrapClient $ notifyUserDeletionRemotes orig where - event = singleton $ UserEvent e + event = pure $ UserEvent e notifyUserDeletionLocals :: - ( MonadIO m, - Log.MonadLogger m, - MonadReader Env m, - MonadMask m, - MonadCatch m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m, - MonadClient m - ) => + Members '[Async, GundeckAccess] r => UserId -> Maybe ConnId -> - List1 Event -> - m () + NonEmpty Event -> + AppT r () notifyUserDeletionLocals deleted conn event = do - recipients <- list1 deleted <$> lookupContactList deleted + recipients <- wrapClient $ (deleted :|) <$> lookupContactList deleted notify event deleted Push.RouteDirect conn (pure recipients) notifyUserDeletionRemotes :: @@ -367,103 +349,95 @@ notifyUserDeletionRemotes deleted = do . Log.field "error" (show fErr) -- | Push events to other users. -push :: - ( MonadIO m, - Log.MonadLogger m, - MonadReader Env m, - MonadMask m, - MonadCatch m, - MonadHttp m, - HasRequestId m - ) => - -- | The events to push. - List1 Event -> - -- | The users to push to. - List1 UserId -> - -- | The originator of the events. - UserId -> - -- | The push routing strategy. - Push.Route -> - -- | The originating device connection. - Maybe ConnId -> - m () -push (toList -> events) usrs orig route conn = - case mapMaybe toPushData events of - [] -> pure () - x : xs -> rawPush (list1 x xs) usrs orig route conn - where - toPushData :: Event -> Maybe (Builder, (Object, Maybe ApsData)) - toPushData e = case toPushFormat e of - Just o -> Just (Log.bytes e, (o, toApsData e)) - Nothing -> Nothing +-- push :: +-- ( MonadIO m, +-- Log.MonadLogger m, +-- MonadReader Env m, +-- MonadMask m, +-- MonadCatch m, +-- MonadHttp m, +-- HasRequestId m +-- ) => +-- -- | The events to push. +-- List1 Event -> +-- -- | The users to push to. +-- List1 UserId -> +-- -- | The originator of the events. +-- UserId -> +-- -- | The push routing strategy. +-- Push.Route -> +-- -- | The originating device connection. +-- Maybe ConnId -> +-- m () +-- push (toList -> events) usrs orig route conn = +-- case mapMaybe toPushData events of +-- [] -> pure () +-- x : xs -> rawPush (list1 x xs) usrs orig route conn +-- where +-- toPushData :: Event -> Maybe (Builder, (Object, Maybe ApsData)) +-- toPushData e = case toPushFormat e of +-- Just o -> Just (Log.bytes e, (o, toApsData e)) +-- Nothing -> Nothing -- | Push encoded events to other users. Useful if you want to push -- something that's not defined in Brig. -rawPush :: - ( MonadIO m, - Log.MonadLogger m, - MonadReader Env m, - MonadMask m, - MonadCatch m, - MonadHttp m, - HasRequestId m - ) => - -- | The events to push. - List1 (Builder, (Object, Maybe ApsData)) -> - -- | The users to push to. - List1 UserId -> - -- | The originator of the events. - UserId -> - -- | The push routing strategy. - Push.Route -> - -- | The originating device connection. - Maybe ConnId -> - m () --- TODO: if we decide to have service whitelist events in Brig instead of --- Galley, let's merge 'push' and 'rawPush' back. See Note [whitelist events]. -rawPush (toList -> events) usrs orig route conn = do - for_ events $ \e -> debug $ remote "gundeck" . msg (fst e) - g <- view gundeck - forM_ recipients $ \rcps -> - void . recovering x3 rpcHandlers $ - const $ - rpc' - "gundeck" - g - ( method POST - . path "/i/push/v2" - . zUser orig -- FUTUREWORK: Remove, because gundeck handler ignores this. - . json (map (mkPush rcps . snd) events) - . expect2xx - ) - where - recipients :: [Range 1 1024 (Set.Set Recipient)] - recipients = - map (unsafeRange . Set.fromList) $ - chunksOf 512 $ - map (`recipient` route) $ - toList usrs - mkPush :: Range 1 1024 (Set.Set Recipient) -> (Object, Maybe ApsData) -> Push - mkPush rcps (o, aps) = - newPush - (Just orig) - rcps - (singletonPayload o) - & pushOriginConnection .~ conn - & pushNativeAps .~ aps +-- rawPush :: +-- ( MonadIO m, +-- Log.MonadLogger m, +-- MonadReader Env m, +-- MonadMask m, +-- MonadCatch m, +-- MonadHttp m, +-- HasRequestId m +-- ) => +-- -- | The events to push. +-- List1 (Builder, (Object, Maybe ApsData)) -> +-- -- | The users to push to. +-- List1 UserId -> +-- -- | The originator of the events. +-- UserId -> +-- -- | The push routing strategy. +-- Push.Route -> +-- -- | The originating device connection. +-- Maybe ConnId -> +-- m () +-- -- TODO: if we decide to have service whitelist events in Brig instead of +-- -- Galley, let's merge 'push' and 'rawPush' back. See Note [whitelist events]. +-- rawPush (toList -> events) usrs orig route conn = do +-- for_ events $ \e -> debug $ remote "gundeck" . msg (fst e) +-- g <- view gundeck +-- forM_ recipients $ \rcps -> +-- void . recovering x3 rpcHandlers $ +-- const $ +-- rpc' +-- "gundeck" +-- g +-- ( method POST +-- . path "/i/push/v2" +-- . zUser orig -- FUTUREWORK: Remove, because gundeck handler ignores this. +-- . json (map (mkPush rcps . snd) events) +-- . expect2xx +-- ) +-- where +-- recipients :: [Range 1 1024 (Set.Set Recipient)] +-- recipients = +-- map (unsafeRange . Set.fromList) $ +-- chunksOf 512 $ +-- map (`recipient` route) $ +-- toList usrs +-- mkPush :: Range 1 1024 (Set.Set Recipient) -> (Object, Maybe ApsData) -> Push +-- mkPush rcps (o, aps) = +-- newPush +-- (Just orig) +-- rcps +-- (singletonPayload o) +-- & pushOriginConnection .~ conn +-- & pushNativeAps .~ aps -- | (Asynchronously) notifies other users of events. notify :: - ( MonadIO m, - Log.MonadLogger m, - MonadReader Env m, - MonadMask m, - MonadCatch m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m - ) => - List1 Event -> + Members '[Async, GundeckAccess] r => + NonEmpty Event -> -- | Origin user, TODO: Delete UserId -> -- | Push routing strategy. @@ -471,243 +445,82 @@ notify :: -- | Origin device connection, if any. Maybe ConnId -> -- | Users to notify. - m (List1 UserId) -> - m () -notify events orig route conn recipients = fork (Just orig) $ do + AppT r (NonEmpty UserId) -> + AppT r () +notify events orig route conn recipients = do rs <- recipients - push events rs orig route conn + fork (Just orig) $ do + pushEvents events rs orig route conn fork :: - (MonadIO m, MonadUnliftIO m, MonadReader Env m) => + Members '[Async] r => Maybe UserId -> - m a -> - m () -fork u ma = do - g <- view applog - r <- view requestId - let logErr e = ExLog.err g $ request r ~~ user u ~~ msg (show e) - withRunInIO $ \lower -> - void . liftIO . forkIO $ - either logErr (const $ pure ()) - =<< runExceptT (syncIO $ lower ma) + Sem r a -> + AppT r () +fork _u act = do + -- g <- view applog + -- r <- view requestId + -- let logErr e = P.err $ request r ~~ user u ~~ msg (show e) + -- TODO(md): see what exceptions (if any) I can catch here. This is used + -- exclusively in making a call to Gundeck, which is an effect + void $ liftSem (async act) -- >>= \case + -- Nothing -> liftSem $ logErr "sending events via Gundeck failed" + -- Just _ -> pure () where - request = field "request" . unRequestId - user = maybe id (field "user" . toByteString) + +-- withRunInIO $ \lower -> +-- void . liftIO . forkIO $ +-- either logErr (const $ pure ()) +-- =<< runExceptT (syncIO $ lower ma) + +-- request = field "request" . unRequestId +-- user = maybe id (field "user" . toByteString) notifySelf :: - ( MonadIO m, - Log.MonadLogger m, - MonadReader Env m, - MonadMask m, - MonadCatch m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m - ) => - List1 Event -> + Members '[Async, GundeckAccess] r => + NonEmpty Event -> -- | Origin user. UserId -> -- | Push routing strategy. Push.Route -> -- | Origin device connection, if any. Maybe ConnId -> - m () + AppT r () notifySelf events orig route conn = - notify events orig route conn (pure (singleton orig)) + notify events orig route conn (pure (pure orig)) notifyContacts :: - forall m. - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m, - MonadClient m, - MonadUnliftIO m - ) => - List1 Event -> + Members + '[ Async, + GalleyAccess, + GundeckAccess + ] + r => + NonEmpty Event -> -- | Origin user. UserId -> -- | Push routing strategy. Push.Route -> -- | Origin device connection, if any. Maybe ConnId -> - m () -notifyContacts events orig route conn = do + AppT r () +notifyContacts events orig route conn = notify events orig route conn $ - list1 orig <$> liftA2 (++) contacts teamContacts + (orig :|) <$> liftA2 (++) (wrapClient contacts) (liftSem teamContacts) where contacts :: MonadClient m => m [UserId] contacts = lookupContactList orig - teamContacts :: m [UserId] + teamContacts :: Member GalleyAccess r => Sem r [UserId] teamContacts = screenMemberList =<< getTeamContacts orig -- If we have a truncated team, we just ignore it all together to avoid very large fanouts -- - screenMemberList :: Maybe Team.TeamMemberList -> m [UserId] + screenMemberList :: Monad m => Maybe Team.TeamMemberList -> m [UserId] screenMemberList (Just mems) | mems ^. Team.teamMemberListType == Team.ListComplete = pure $ fmap (view Team.userId) (mems ^. Team.teamMembers) screenMemberList _ = pure [] --- Event Serialisation: - -toPushFormat :: Event -> Maybe Object -toPushFormat (UserEvent (UserCreated u)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.new" :: Text), - "user" .= SelfProfile (u {userIdentity = Nothing}) - ] -toPushFormat (UserEvent (UserActivated u)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.activate" :: Text), - "user" .= SelfProfile u - ] -toPushFormat (UserEvent (UserUpdated (UserUpdatedData i n pic acc ass hdl loc mb ssoId ssoIdDel))) = - Just $ - KeyMap.fromList - [ "type" .= ("user.update" :: Text), - "user" - .= object - ( "id" .= i - # "name" .= n - # "picture" .= pic -- DEPRECATED - # "accent_id" .= acc - # "assets" .= ass - # "handle" .= hdl - # "locale" .= loc - # "managed_by" .= mb - # "sso_id" .= ssoId - # "sso_id_deleted" .= ssoIdDel - # [] - ) - ] -toPushFormat (UserEvent (UserIdentityUpdated UserIdentityUpdatedData {..})) = - Just $ - KeyMap.fromList - [ "type" .= ("user.update" :: Text), - "user" - .= object - ( "id" .= eiuId - # "email" .= eiuEmail - # "phone" .= eiuPhone - # [] - ) - ] -toPushFormat (UserEvent (UserIdentityRemoved (UserIdentityRemovedData i e p))) = - Just $ - KeyMap.fromList - [ "type" .= ("user.identity-remove" :: Text), - "user" - .= object - ( "id" .= i - # "email" .= e - # "phone" .= p - # [] - ) - ] -toPushFormat (ConnectionEvent (ConnectionUpdated uc _ name)) = - Just $ - KeyMap.fromList $ - "type" .= ("user.connection" :: Text) - # "connection" .= uc - # "user" .= case name of - Just n -> Just $ object ["name" .= n] - Nothing -> Nothing - # [] -toPushFormat (UserEvent (UserSuspended i)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.suspend" :: Text), - "id" .= i - ] -toPushFormat (UserEvent (UserResumed i)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.resume" :: Text), - "id" .= i - ] -toPushFormat (UserEvent (UserDeleted qid)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.delete" :: Text), - "id" .= qUnqualified qid, - "qualified_id" .= qid - ] -toPushFormat (UserEvent (UserLegalHoldDisabled i)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.legalhold-disable" :: Text), - "id" .= i - ] -toPushFormat (UserEvent (UserLegalHoldEnabled i)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.legalhold-enable" :: Text), - "id" .= i - ] -toPushFormat (PropertyEvent (PropertySet _ k v)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.properties-set" :: Text), - "key" .= k, - "value" .= propertyValue v - ] -toPushFormat (PropertyEvent (PropertyDeleted _ k)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.properties-delete" :: Text), - "key" .= k - ] -toPushFormat (PropertyEvent (PropertiesCleared _)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.properties-clear" :: Text) - ] -toPushFormat (ClientEvent (ClientAdded _ c)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.client-add" :: Text), - "client" .= c - ] -toPushFormat (ClientEvent (ClientRemoved _ c)) = - Just $ - KeyMap.fromList - [ "type" .= ("user.client-remove" :: Text), - "client" .= IdObject (clientId c) - ] -toPushFormat (UserEvent (LegalHoldClientRequested payload)) = - let LegalHoldClientRequestedData targetUser lastPrekey' clientId = payload - in Just $ - KeyMap.fromList - [ "type" .= ("user.legalhold-request" :: Text), - "id" .= targetUser, - "last_prekey" .= lastPrekey', - "client" .= IdObject clientId - ] - -toApsData :: Event -> Maybe ApsData -toApsData (ConnectionEvent (ConnectionUpdated uc _ name)) = - case (ucStatus uc, name) of - (MissingLegalholdConsent, _) -> Nothing - (Pending, n) -> apsConnRequest <$> n - (Accepted, n) -> apsConnAccept <$> n - (Blocked, _) -> Nothing - (Ignored, _) -> Nothing - (Sent, _) -> Nothing - (Cancelled, _) -> Nothing - where - apsConnRequest n = - apsData (ApsLocKey "push.notification.connection.request") [fromName n] - & apsSound ?~ ApsSound "new_message_apns.caf" - apsConnAccept n = - apsData (ApsLocKey "push.notification.connection.accepted") [fromName n] - & apsSound ?~ ApsSound "new_message_apns.caf" -toApsData _ = Nothing - ------------------------------------------------------------------------------- -- Conversation Management @@ -1256,26 +1069,26 @@ memberIsTeamOwner tid uid = do -- | Only works on 'BindingTeam's! The list of members returned is potentially truncated. -- -- Calls 'Galley.API.getBindingTeamMembersH'. -getTeamContacts :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - UserId -> - m (Maybe Team.TeamMemberList) -getTeamContacts u = do - debug $ remote "galley" . msg (val "Get team contacts") - rs <- galleyRequest GET req - case Bilge.statusCode rs of - 200 -> Just <$> decodeBody "galley" rs - _ -> pure Nothing - where - req = - paths ["i", "users", toByteString' u, "team", "members"] - . expect [status200, status404] +-- getTeamContacts :: +-- ( MonadReader Env m, +-- MonadIO m, +-- MonadMask m, +-- MonadHttp m, +-- HasRequestId m, +-- MonadLogger m +-- ) => +-- UserId -> +-- m (Maybe Team.TeamMemberList) +-- getTeamContacts u = do +-- debug $ remote "galley" . msg (val "Get team contacts") +-- rs <- galleyRequest GET req +-- case Bilge.statusCode rs of +-- 200 -> Just <$> decodeBody "galley" rs +-- _ -> pure Nothing +-- where +-- req = +-- paths ["i", "users", toByteString' u, "team", "members"] +-- . expect [status200, status404] -- | Calls 'Galley.API.getBindingTeamIdH'. getTeamId :: diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index f33fb5f2d4..e6d5e4c4a5 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -25,6 +25,8 @@ import Brig.App import Brig.InternalEvent.Types import Brig.Options (defDeleteThrottleMillis, setDefaultUserLocale, setDeleteThrottleMillis) import qualified Brig.Provider.API as API +import Brig.Sem.GalleyAccess (GalleyAccess) +import Brig.Sem.GundeckAccess (GundeckAccess) import Brig.Sem.UniqueClaimsStore import Brig.Sem.UserHandleStore import Brig.Sem.UserQuery @@ -34,6 +36,7 @@ import Data.ByteString.Conversion import Data.Qualified import Imports import Polysemy +import Polysemy.Async import Polysemy.Conc.Effect.Race import Polysemy.Conc.Race import Polysemy.Input @@ -47,7 +50,10 @@ import System.Logger.Class (field, msg, val, (~~)) onEvent :: forall r. Members - '[ Input (Local ()), + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), P.TinyLog, Race, UniqueClaimsStore, diff --git a/services/brig/src/Brig/Sem/Common.hs b/services/brig/src/Brig/Sem/Common.hs index 12bfd250e7..2f97b808ec 100644 --- a/services/brig/src/Brig/Sem/Common.hs +++ b/services/brig/src/Brig/Sem/Common.hs @@ -24,12 +24,14 @@ import Bilge.Request import Bilge.Retry import Cassandra import Control.Monad.Catch +import Control.Monad.Trans.Except import Control.Retry import qualified Data.ByteString.Lazy as LBS import Imports import Network.HTTP.Client (Response) import Network.HTTP.Types.Method import Polysemy +import Polysemy.Error interpretClientToIO :: Member (Final IO) r => @@ -57,3 +59,11 @@ makeReq component cReq m r = x3 :: RetryPolicy x3 = limitRetries 3 <> exponentialBackoff 100000 + +semErrToExceptT :: + Sem (Error e ': r) a -> + ExceptT e (Sem r) a +semErrToExceptT act = + lift (runError act) >>= \case + Left p -> throwE p + Right v -> pure v diff --git a/services/brig/src/Brig/Sem/GalleyAccess.hs b/services/brig/src/Brig/Sem/GalleyAccess.hs index 024c8a1e47..6463267634 100644 --- a/services/brig/src/Brig/Sem/GalleyAccess.hs +++ b/services/brig/src/Brig/Sem/GalleyAccess.hs @@ -20,10 +20,18 @@ module Brig.Sem.GalleyAccess where import Data.Id +import Imports import Polysemy import Wire.API.Team.Feature +import Wire.API.Team.Member data GalleyAccess m a where GetTeamSndFactorPasswordChallenge :: TeamId -> GalleyAccess m FeatureStatus + -- | Only works on 'BindingTeam's! The lisetBindingTeamMembersH'. + GetTeamContacts :: UserId -> GalleyAccess m (Maybe TeamMemberList) + -- | Calls 'Galley.API.getBindingTeamIdH'. + GetTeamId :: UserId -> GalleyAccess m (Maybe TeamId) + -- | Calls 'Galley.API.getTeamFeatureStatusH'. + GetTeamLegalHoldStatus :: TeamId -> GalleyAccess m (WithStatus LegalholdConfig) makeSem ''GalleyAccess diff --git a/services/brig/src/Brig/Sem/GalleyAccess/Http.hs b/services/brig/src/Brig/Sem/GalleyAccess/Http.hs index 9258b156de..244388dab2 100644 --- a/services/brig/src/Brig/Sem/GalleyAccess/Http.hs +++ b/services/brig/src/Brig/Sem/GalleyAccess/Http.hs @@ -21,6 +21,7 @@ import qualified Bilge as RPC import Bilge.IO import Bilge.RPC import Bilge.Request +import Brig.RPC import qualified Brig.RPC.Decode as RPC import Brig.Sem.Common import Brig.Sem.GalleyAccess @@ -28,11 +29,15 @@ import Control.Monad.Catch import Data.ByteString.Conversion.To import Imports import Network.HTTP.Types.Method +import Network.HTTP.Types.Status import Polysemy +import Polysemy.TinyLog +import System.Logger.Class hiding (debug) import Wire.API.Team.Feature galleyAccessToHttp :: forall m r a. + Member TinyLog r => ( MonadIO m, MonadMask m, MonadHttp m, @@ -43,18 +48,50 @@ galleyAccessToHttp :: Sem (GalleyAccess ': r) a -> Sem r a galleyAccessToHttp g = - interpret $ - embed @m . \case - GetTeamSndFactorPasswordChallenge tid -> do + interpret $ \case + GetTeamSndFactorPasswordChallenge tid -> embed @m $ do + let req = + paths + [ "i", + "teams", + toByteString' tid, + "features", + featureNameBS @SndFactorPasswordChallengeConfig + ] + . expect2xx + response <- makeReq "galley" g GET req + wsStatus @SndFactorPasswordChallengeConfig + <$> RPC.decodeBody "galley" response + GetTeamContacts uid -> do + debug $ remote "galley" . msg (val "Get team contacts") + embed @m $ do + let req = + paths ["i", "users", toByteString' uid, "team", "members"] + . expect [status200, status404] + response <- makeReq "galley" g GET req + case RPC.statusCode response of + 200 -> Just <$> decodeBody "galley" response + _ -> pure Nothing + GetTeamId uid -> do + debug $ remote "galley" . msg (val "Get team from user") + embed @m $ do + let req = + paths ["i", "users", toByteString' uid, "team"] + . expect [status200, status404] + response <- makeReq "galley" g GET req + case RPC.statusCode response of + 200 -> Just <$> decodeBody "galley" response + _ -> pure Nothing + GetTeamLegalHoldStatus tid -> do + debug $ remote "galley" . msg (val "Get legalhold settings") + embed @m $ do let req = paths [ "i", "teams", toByteString' tid, "features", - featureNameBS @SndFactorPasswordChallengeConfig + featureNameBS @LegalholdConfig ] . expect2xx - response <- makeReq "galley" g GET req - wsStatus @SndFactorPasswordChallengeConfig - <$> RPC.decodeBody "galley" response + makeReq "galley" g GET req >>= decodeBody "galley" diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index d03738df51..e9e20e3559 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -35,6 +35,8 @@ import qualified Brig.Email as Email import qualified Brig.IO.Intra as Intra import Brig.Options (setMaxTeamSize, setTeamInvitationTimeout) import qualified Brig.Phone as Phone +import Brig.Sem.GalleyAccess +import Brig.Sem.GundeckAccess import Brig.Sem.Twilio (Twilio) import Brig.Sem.UserKeyStore (UserKeyStore) import Brig.Sem.UserQuery (UserQuery) @@ -65,6 +67,7 @@ import Network.Wai.Utilities hiding (code, message) import Network.Wai.Utilities.Swagger (document) import qualified Network.Wai.Utilities.Swagger as Doc import Polysemy +import Polysemy.Async import qualified Polysemy.Error as P import Polysemy.Input import qualified Ropes.Twilio as Twilio @@ -207,7 +210,10 @@ routesPublic = do routesInternal :: Members - '[ BlacklistStore, + '[ Async, + BlacklistStore, + GalleyAccess, + GundeckAccess, P.Error Twilio.ErrorResponse, Twilio, UserKeyStore, @@ -521,21 +527,33 @@ getInvitationByEmail email = do inv <- lift $ wrapClient $ DB.lookupInvitationByEmail email maybe (throwStd (notFound "Invitation not found")) pure inv -suspendTeamH :: JSON ::: TeamId -> (Handler r) Response +suspendTeamH :: + Members '[Async, GalleyAccess, GundeckAccess] r => + JSON ::: TeamId -> + Handler r Response suspendTeamH (_ ::: tid) = do empty <$ suspendTeam tid -suspendTeam :: TeamId -> (Handler r) () +suspendTeam :: + Members '[Async, GalleyAccess, GundeckAccess] r => + TeamId -> + Handler r () suspendTeam tid = do changeTeamAccountStatuses tid Suspended lift $ wrapClient $ DB.deleteInvitations tid lift $ wrapHttp $ Intra.changeTeamStatus tid Team.Suspended Nothing -unsuspendTeamH :: JSON ::: TeamId -> (Handler r) Response +unsuspendTeamH :: + Members '[Async, GalleyAccess, GundeckAccess] r => + JSON ::: TeamId -> + Handler r Response unsuspendTeamH (_ ::: tid) = do empty <$ unsuspendTeam tid -unsuspendTeam :: TeamId -> (Handler r) () +unsuspendTeam :: + Members '[Async, GalleyAccess, GundeckAccess] r => + TeamId -> + Handler r () unsuspendTeam tid = do changeTeamAccountStatuses tid Active lift $ wrapHttp $ Intra.changeTeamStatus tid Team.Active Nothing @@ -543,13 +561,17 @@ unsuspendTeam tid = do ------------------------------------------------------------------------------- -- Internal -changeTeamAccountStatuses :: TeamId -> AccountStatus -> (Handler r) () +changeTeamAccountStatuses :: + Members '[Async, GalleyAccess, GundeckAccess] r => + TeamId -> + AccountStatus -> + Handler r () changeTeamAccountStatuses tid s = do team <- Team.tdTeam <$> lift (wrapHttp $ Intra.getTeam tid) unless (team ^. teamBinding == Binding) $ throwStd noBindingTeam uids <- toList1 =<< lift (fmap (view Teams.userId) . view teamMembers <$> wrapHttp (Intra.getTeamMembers tid)) - wrapHttpClientE (API.changeAccountStatus uids s) !>> accountStatusError + API.changeAccountStatus uids s !>> accountStatusError where toList1 (x : xs) = pure $ List1.list1 x xs toList1 [] = throwStd (notFound "Team not found or no members") diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index cc476bb7c6..8978e92137 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -32,20 +32,21 @@ import Brig.Phone import Brig.Sem.ActivationKeyStore import Brig.Sem.ActivationSupply import Brig.Sem.BudgetStore +-- import Brig.Types.Intra (ReAuthUser, reAuthCode, reAuthCodeAction, reAuthPassword) +-- import Control.Lens (view) import Brig.Sem.GalleyAccess +import Brig.Sem.GundeckAccess (GundeckAccess) import Brig.Sem.Twilio import Brig.Sem.UserHandleStore import Brig.Sem.UserKeyStore import Brig.Sem.UserQuery (UserQuery) import Brig.Sem.VerificationCodeStore import Brig.Types.Intra (ReAuthUser, reAuthCode, reAuthCodeAction) --- import Brig.Types.Intra (ReAuthUser, reAuthCode, reAuthCodeAction, reAuthPassword) import Brig.Types.User.Auth import qualified Brig.User.Auth as Auth import qualified Brig.User.Auth.Cookie as Auth import qualified Brig.ZAuth as ZAuth import Control.Error (catchE) --- import Control.Lens (view) import Control.Monad.Except import Control.Monad.Trans.Except (throwE) import qualified Data.ByteString as BS @@ -72,6 +73,7 @@ import qualified Network.Wai.Utilities.Response as WaiResp import Network.Wai.Utilities.Swagger (document) import qualified Network.Wai.Utilities.Swagger as Doc import Polysemy +import Polysemy.Async import Polysemy.Error import Polysemy.Input import qualified Polysemy.TinyLog as P @@ -86,10 +88,12 @@ routesPublic :: Members '[ ActivationKeyStore, ActivationSupply, + Async, BlacklistStore, BudgetStore, Error Twilio.ErrorResponse, GalleyAccess, + GundeckAccess, Input (Local ()), P.TinyLog, Twilio, @@ -227,8 +231,10 @@ routesPublic = do routesInternal :: Members - '[ Error ReAuthError, + '[ Async, + Error ReAuthError, GalleyAccess, + GundeckAccess, Input (Local ()), P.TinyLog, UserKeyStore, @@ -343,9 +349,11 @@ reAuthUser uid body = do loginH :: Members - '[ BudgetStore, + '[ Async, + BudgetStore, Error Twilio.ErrorResponse, GalleyAccess, + GundeckAccess, Input (Local ()), P.TinyLog, Twilio, @@ -362,9 +370,11 @@ loginH (req ::: persist ::: _) = do login :: Members - '[ BudgetStore, + '[ Async, + BudgetStore, Error Twilio.ErrorResponse, GalleyAccess, + GundeckAccess, Input (Local ()), P.TinyLog, Twilio, @@ -376,28 +386,69 @@ login :: r => Public.Login -> Bool -> - (Handler r) (Auth.Access ZAuth.User) + Handler r (Auth.Access ZAuth.User) login l persist = do let typ = if persist then PersistentCookie else SessionCookie Auth.login l typ !>> loginError -ssoLoginH :: JsonRequest SsoLogin ::: Bool ::: JSON -> (Handler r) Response +ssoLoginH :: + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserQuery + ] + r => + JsonRequest SsoLogin ::: Bool ::: JSON -> + Handler r Response ssoLoginH (req ::: persist ::: _) = do lift . tokenResponse =<< flip ssoLogin persist =<< parseJsonBody req -ssoLogin :: SsoLogin -> Bool -> (Handler r) (Auth.Access ZAuth.User) +ssoLogin :: + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserQuery + ] + r => + SsoLogin -> + Bool -> + Handler r (Auth.Access ZAuth.User) ssoLogin l persist = do let typ = if persist then PersistentCookie else SessionCookie - wrapHttpClientE (Auth.ssoLogin l typ) !>> loginError + Auth.ssoLogin l typ !>> loginError -legalHoldLoginH :: JsonRequest LegalHoldLogin ::: JSON -> (Handler r) Response +legalHoldLoginH :: + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserQuery + ] + r => + JsonRequest LegalHoldLogin ::: JSON -> + Handler r Response legalHoldLoginH (req ::: _) = do lift . tokenResponse =<< legalHoldLogin =<< parseJsonBody req -legalHoldLogin :: LegalHoldLogin -> (Handler r) (Auth.Access ZAuth.LegalHoldUser) +legalHoldLogin :: + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserQuery + ] + r => + LegalHoldLogin -> + Handler r (Auth.Access ZAuth.LegalHoldUser) legalHoldLogin l = do let typ = PersistentCookie -- Session cookie isn't a supported use case here - wrapHttpClientE (Auth.legalHoldLogin l typ) !>> legalHoldLoginError + Auth.legalHoldLogin l typ !>> legalHoldLoginError logoutH :: JSON ::: Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) ::: Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> (Handler r) Response logoutH (_ ::: ut ::: at) = empty <$ logout ut at @@ -469,7 +520,16 @@ rmCookies :: UserId -> Public.RemoveCookies -> (Handler r) () rmCookies uid (Public.RemoveCookies pw lls ids) = wrapClientE (Auth.revokeAccess uid pw ids lls) !>> authError -renewH :: JSON ::: Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) ::: Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> (Handler r) Response +renewH :: + Members + '[ Async, + GalleyAccess, + GundeckAccess, + UserQuery + ] + r => + JSON ::: Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) ::: Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> + Handler r Response renewH (_ ::: ut ::: at) = lift . either tokenResponse tokenResponse =<< renew ut at -- | renew access for either: @@ -478,18 +538,25 @@ renewH (_ ::: ut ::: at) = lift . either tokenResponse tokenResponse =<< renew u -- -- Other combinations of provided inputs will cause an error to be raised. renew :: + Members + '[ Async, + GalleyAccess, + GundeckAccess, + UserQuery + ] + r => Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) -> Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> - (Handler r) (Either (Auth.Access ZAuth.User) (Auth.Access ZAuth.LegalHoldUser)) + Handler r (Either (Auth.Access ZAuth.User) (Auth.Access ZAuth.LegalHoldUser)) renew = \case Nothing -> const $ throwStd authMissingCookie (Just (Left userTokens)) -> -- normal UserToken, so we want a normal AccessToken - fmap Left . wrapHttpClientE . renewAccess userTokens <=< matchingOrNone leftToMaybe + fmap Left . renewAccess userTokens <=< matchingOrNone leftToMaybe (Just (Right legalholdUserTokens)) -> -- LegalholdUserToken, so we want a LegalholdAccessToken - fmap Right . wrapHttpClientE . renewAccess legalholdUserTokens <=< matchingOrNone rightToMaybe + fmap Right . renewAccess legalholdUserTokens <=< matchingOrNone rightToMaybe where renewAccess uts mat = Auth.renewAccess uts mat !>> zauthError diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 0de32c6d2b..df448fa182 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -37,7 +37,6 @@ module Brig.User.Auth where import Bilge.IO -import Bilge.RPC import Brig.API.Types import Brig.API.User (changeSingleAccountStatus) import Brig.App @@ -52,7 +51,9 @@ import qualified Brig.IO.Intra as Intra import qualified Brig.Options as Opt import Brig.Phone import Brig.Sem.BudgetStore +import Brig.Sem.Common import Brig.Sem.GalleyAccess +import Brig.Sem.GundeckAccess import Brig.Sem.Twilio (Twilio) import Brig.Sem.UserHandleStore import Brig.Sem.UserKeyStore (UserKeyStore) @@ -64,12 +65,10 @@ import Brig.Types.Intra import Brig.Types.User.Auth import Brig.User.Auth.Cookie import Brig.User.Phone -import Brig.User.Search.Index import qualified Brig.ZAuth as ZAuth import Cassandra import Control.Error hiding (bool) import Control.Lens (to, view) -import Control.Monad.Catch import Control.Monad.Trans.Except import Data.ByteString.Conversion (toByteString) import Data.Either.Combinators @@ -84,6 +83,7 @@ import qualified Data.ZAuth.Token as ZAuth import Imports import Network.Wai.Utilities.Error ((!>>)) import Polysemy +import Polysemy.Async import Polysemy.Error import qualified Polysemy.Error as P import Polysemy.Input @@ -162,9 +162,11 @@ lookupLoginCode phone = login :: forall r. Members - '[ BudgetStore, + '[ Async, + BudgetStore, Error Twilio.ErrorResponse, GalleyAccess, + GundeckAccess, Input (Local ()), P.TinyLog, Twilio, @@ -192,7 +194,7 @@ login (PasswordLogin li pw label code) typ = do AuthEphemeral -> throwE LoginEphemeral AuthPendingInvitation -> throwE LoginPendingActivation verifyLoginCode code uid mLimitFailedLogins - wrapHttpClientE $ newAccess @ZAuth.User @ZAuth.Access uid typ label + newAccess @ZAuth.User @ZAuth.Access uid typ label where verifyLoginCode :: Maybe Code.Value -> @@ -217,7 +219,7 @@ login (SmsLogin phone code label) typ = do unless ok $ do r <- lift . liftSem . runError $ loginFailed uid mLimitFailedLogins whenLeft r throwE - wrapHttpClientE $ newAccess @ZAuth.User @ZAuth.Access uid typ label + newAccess @ZAuth.User @ZAuth.Access uid typ label verifyCode :: forall r. @@ -319,26 +321,23 @@ logout uts at = do lift $ revokeCookies u [cookieId ck] [] renewAccess :: - forall m u a. - ( ZAuth.TokenPair u a, - MonadClient m, - ZAuth.MonadZAuth m, - Log.MonadLogger m, - MonadReader Env m, - MonadIndexIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m - ) => + forall u a r. + ZAuth.TokenPair u a => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + UserQuery + ] + r => List1 (ZAuth.Token u) -> Maybe (ZAuth.Token a) -> - ExceptT ZAuth.Failure m (Access u) + ExceptT ZAuth.Failure (AppT r) (Access u) renewAccess uts at = do - (uid, ck) <- validateTokens uts at + (uid, ck) <- wrapHttpClientE $ validateTokens uts at lift . Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.renewAccess") catchSuspendInactiveUser uid ZAuth.Expired - ck' <- lift $ nextCookie ck + ck' <- lift . wrapHttpClient $ nextCookie ck at' <- lift $ newAccessToken (fromMaybe ck ck') at pure $ Access at' ck' @@ -362,21 +361,18 @@ revokeAccess u pw cc ll = do -- Internal catchSuspendInactiveUser :: - ( MonadClient m, - Log.MonadLogger m, - MonadIndexIO m, - MonadReader Env m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m, - Log.MonadLogger m - ) => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + UserQuery + ] + r => UserId -> e -> - ExceptT e m () + ExceptT e (AppT r) () catchSuspendInactiveUser uid errval = do - mustsuspend <- lift $ mustSuspendInactiveUser uid + mustsuspend <- wrapClientE $ mustSuspendInactiveUser uid when mustsuspend $ do lift . Log.warn $ msg (val "Suspending user due to inactivity") @@ -392,25 +388,22 @@ catchSuspendInactiveUser uid errval = do Right () -> pure () newAccess :: - forall u a m. - ( ZAuth.TokenPair u a, - MonadReader Env m, - MonadClient m, - ZAuth.MonadZAuth m, - Log.MonadLogger m, - MonadIndexIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m - ) => + forall u a r. + ZAuth.TokenPair u a => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + UserQuery + ] + r => UserId -> CookieType -> Maybe CookieLabel -> - ExceptT LoginError m (Access u) + ExceptT LoginError (AppT r) (Access u) newAccess uid ct cl = do catchSuspendInactiveUser uid LoginSuspended - r <- lift $ newCookieLimited uid ct cl + r <- lift . wrapHttpClient $ newCookieLimited uid ct cl case r of Left delay -> throwE $ LoginThrottled delay Right ck -> do @@ -542,83 +535,71 @@ validateToken ut at = do -- | Allow to login as any user without having the credentials. ssoLogin :: - forall m. - ( MonadClient m, - MonadReader Env m, - ZAuth.MonadZAuth m, - ZAuth.MonadZAuth m, - Log.MonadLogger m, - MonadIndexIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadUnliftIO m - ) => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserQuery + ] + r => SsoLogin -> CookieType -> - ExceptT LoginError m (Access ZAuth.User) + ExceptT LoginError (AppT r) (Access ZAuth.User) ssoLogin (SsoLogin uid label) typ = do locale <- Opt.setDefaultUserLocale <$> view settings - locDomain <- qualifyLocal () - o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid Nothing - whenLeft o $ \case - ReAuthMissingPassword -> pure () - ReAuthCodeVerificationRequired -> pure () - ReAuthCodeVerificationNoPendingCode -> pure () - ReAuthCodeVerificationNoEmail -> pure () - ReAuthError e -> case e of - AuthInvalidUser -> throwE LoginFailed - AuthInvalidCredentials -> pure () - AuthSuspended -> throwE LoginSuspended - AuthEphemeral -> throwE LoginEphemeral - AuthPendingInvitation -> throwE LoginPendingActivation + lift + ( liftSem (runError @ReAuthError (Data.reauthenticate locale uid Nothing)) + ) + >>= \case + Right _ -> pure () + Left ReAuthMissingPassword -> pure () + Left ReAuthCodeVerificationRequired -> pure () + Left ReAuthCodeVerificationNoPendingCode -> pure () + Left ReAuthCodeVerificationNoEmail -> pure () + Left (ReAuthError e) -> case e of + AuthInvalidUser -> throwE LoginFailed + AuthInvalidCredentials -> pure () + AuthSuspended -> throwE LoginSuspended + AuthEphemeral -> throwE LoginEphemeral + AuthPendingInvitation -> throwE LoginPendingActivation newAccess @ZAuth.User @ZAuth.Access uid typ label -- | Log in as a LegalHold service, getting LegalHoldUser/Access Tokens. legalHoldLogin :: - ( MonadClient m, - MonadReader Env m, - MonadMask m, - MonadHttp m, - HasRequestId m, - Log.MonadLogger m, - ZAuth.MonadZAuth m, - MonadIndexIO m, - MonadUnliftIO m - ) => + Members + '[ Async, + GalleyAccess, + GundeckAccess, + Input (Local ()), + UserQuery + ] + r => LegalHoldLogin -> CookieType -> - ExceptT LegalHoldLoginError m (Access ZAuth.LegalHoldUser) + ExceptT LegalHoldLoginError (AppT r) (Access ZAuth.LegalHoldUser) legalHoldLogin (LegalHoldLogin uid plainTextPassword label) typ = do locale <- Opt.setDefaultUserLocale <$> view settings - locDomain <- qualifyLocal () - o <- runUserQueryAction $ runInputConst locDomain $ Data.reauthenticate locale uid plainTextPassword - except o !>> LegalHoldReAuthError + mapExceptT liftSem . semErrToExceptT . mapError LegalHoldReAuthError $ + Data.reauthenticate locale uid plainTextPassword -- legalhold login is only possible if -- the user is a team user -- and the team has legalhold enabled - mteam <- lift $ Intra.getTeamId uid + mteam <- lift . liftSem $ getTeamId uid case mteam of Nothing -> throwE LegalHoldLoginNoBindingTeam - Just tid -> assertLegalHoldEnabled tid + Just tid -> mapExceptT liftSem . semErrToExceptT $ assertLegalHoldEnabled tid -- create access token and cookie newAccess @ZAuth.LegalHoldUser @ZAuth.LegalHoldAccess uid typ label !>> LegalHoldLoginError assertLegalHoldEnabled :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - Log.MonadLogger m - ) => + Members '[Error LegalHoldLoginError, GalleyAccess] r => TeamId -> - ExceptT LegalHoldLoginError m () -assertLegalHoldEnabled tid = do - stat <- lift $ Intra.getTeamLegalHoldStatus tid - case wsStatus stat of - FeatureStatusDisabled -> throwE LegalHoldLoginLegalHoldNotEnabled + Sem r () +assertLegalHoldEnabled tid = + wsStatus <$> getTeamLegalHoldStatus tid >>= \case + FeatureStatusDisabled -> throw LegalHoldLoginLegalHoldNotEnabled FeatureStatusEnabled -> pure () -------------------------------------------------------------------------------- @@ -641,11 +622,3 @@ runStoreAction :: Sem '[Error e, store, Embed m] a -> t m (Either e a) runStoreAction interpreter = lift . runM . interpreter @m . runError @e - -semErrToExceptT :: - Sem (Error e ': r) a -> - ExceptT e (Sem r) a -semErrToExceptT act = - lift (runError act) >>= \case - Left p -> throwE p - Right v -> pure v From 08adb085fa3ad0f53bda14786cf11a8e503de5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 25 Aug 2022 17:03:20 +0200 Subject: [PATCH 22/41] Remove temporary Polysemy crutches --- services/brig/src/Brig/User/API/Auth.hs | 14 +++++++-- services/brig/src/Brig/User/Auth.hs | 39 ++++++------------------- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index c2a50b1a03..21ed633cd6 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -76,6 +76,7 @@ import Polysemy import Polysemy.Async import Polysemy.Error import Polysemy.Input +import Polysemy.TinyLog (TinyLog) import qualified Polysemy.TinyLog as P import qualified Ropes.Twilio as Twilio import Wire.API.Error @@ -512,13 +513,20 @@ listCookies :: UserId -> Maybe (List Public.CookieLabel) -> (AppT r) Public.Cook listCookies u ll = do Public.CookieList <$> wrapClient (Auth.listCookies u (maybe [] fromList ll)) -rmCookiesH :: UserId ::: JsonRequest Public.RemoveCookies -> (Handler r) Response +rmCookiesH :: + Members '[Input (Local ()), TinyLog, UserQuery] r => + UserId ::: JsonRequest Public.RemoveCookies -> + Handler r Response rmCookiesH (uid ::: req) = do empty <$ (rmCookies uid =<< parseJsonBody req) -rmCookies :: UserId -> Public.RemoveCookies -> (Handler r) () +rmCookies :: + Members '[Input (Local ()), TinyLog, UserQuery] r => + UserId -> + Public.RemoveCookies -> + Handler r () rmCookies uid (Public.RemoveCookies pw lls ids) = - wrapClientE (Auth.revokeAccess uid pw ids lls) !>> authError + Auth.revokeAccess uid pw ids lls !>> authError renewH :: Members diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index df448fa182..5f2fc95f65 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -69,7 +69,6 @@ import qualified Brig.ZAuth as ZAuth import Cassandra import Control.Error hiding (bool) import Control.Lens (to, view) -import Control.Monad.Trans.Except import Data.ByteString.Conversion (toByteString) import Data.Either.Combinators import Data.Handle (Handle) @@ -87,6 +86,7 @@ import Polysemy.Async import Polysemy.Error import qualified Polysemy.Error as P import Polysemy.Input +import Polysemy.TinyLog import qualified Polysemy.TinyLog as P import qualified Ropes.Twilio as Twilio import System.Logger (field, msg, val, (~~)) @@ -342,20 +342,20 @@ renewAccess uts at = do pure $ Access at' ck' revokeAccess :: - forall m. - (MonadClient m, Log.MonadLogger m, MonadReader Env m) => + Members '[Input (Local ()), TinyLog, UserQuery] r => UserId -> PlainTextPassword -> [CookieId] -> [CookieLabel] -> - ExceptT AuthError m () + ExceptT AuthError (AppT r) () revokeAccess u pw cc ll = do - lift $ Log.debug $ field "user" (toByteString u) . field "action" (Log.val "User.revokeAccess") - locDomain <- qualifyLocal () + lift . liftSem $ + debug $ field "user" (toByteString u) . field "action" (Log.val "User.revokeAccess") locale <- Opt.setDefaultUserLocale <$> view settings - unlessM (lift . runM . userQueryToCassandra @m @'[Embed m] $ runInputConst locDomain $ Data.isSamlUser locale u) $ - runUserQueryAction (Data.authenticate u pw) >>= except - lift $ revokeCookies u cc ll + unlessM (lift . liftSem $ Data.isSamlUser locale u) + . mapExceptT liftSem + . semErrToExceptT $Data.authenticate u pw + lift . wrapClient $ revokeCookies u cc ll -------------------------------------------------------------------------------- -- Internal @@ -601,24 +601,3 @@ assertLegalHoldEnabled tid = wsStatus <$> getTeamLegalHoldStatus tid >>= \case FeatureStatusDisabled -> throw LegalHoldLoginLegalHoldNotEnabled FeatureStatusEnabled -> pure () - --------------------------------------------------------------------------------- --- Polysemy crutches --- --- These can be removed once functions in this module run in 'Sem r' instead of --- 'ExceptT e m' or 'm' for some constrained 'm'. - -runUserQueryAction :: - forall m e a t. - (MonadClient m, MonadTrans t) => - Sem '[Error e, UserQuery, Embed m] a -> - t m (Either e a) -runUserQueryAction = runStoreAction (userQueryToCassandra @m) - -runStoreAction :: - forall m e a t store. - (MonadClient m, MonadTrans t) => - (forall n b. (MonadClient n, n ~ m) => Sem '[store, Embed n] b -> Sem '[Embed n] b) -> - Sem '[Error e, store, Embed m] a -> - t m (Either e a) -runStoreAction interpreter = lift . runM . interpreter @m . runError @e From 3f0b89493870d1608ff46d41a57747c752fa5c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 26 Aug 2022 08:27:07 +0200 Subject: [PATCH 23/41] Handle a TODO in Brig.API.User.deleteAccount --- services/brig/src/Brig/API/Public.hs | 2 ++ services/brig/src/Brig/API/User.hs | 10 ++++++---- services/brig/src/Brig/InternalEvent/Process.hs | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index dc281ee2d0..3b5ac9aa2f 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1269,6 +1269,7 @@ deleteSelfUser :: Input (Local ()), UniqueClaimsStore, UserHandleStore, + UserKeyStore, UserQuery, VerificationCodeStore ] @@ -1287,6 +1288,7 @@ verifyDeleteUserH :: Input (Local ()), UniqueClaimsStore, UserHandleStore, + UserKeyStore, UserQuery, VerificationCodeStore ] diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 07a0b01a1f..7db03008b9 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -1444,6 +1444,7 @@ deleteSelfUser :: Input (Local ()), UniqueClaimsStore, UserHandleStore, + UserKeyStore, UserQuery, VerificationCodeStore ] @@ -1533,6 +1534,7 @@ verifyDeleteUser :: Input (Local ()), UniqueClaimsStore, UserHandleStore, + UserKeyStore, UserQuery, VerificationCodeStore ] @@ -1561,6 +1563,7 @@ deleteAccount :: GundeckAccess, UniqueClaimsStore, UserHandleStore, + UserKeyStore, UserQuery ] r => @@ -1582,10 +1585,9 @@ deleteAccount account@(accountUser -> user) = do let uid = userId user Log.info $ field "user" (toByteString uid) . msg (val "Deleting account") -- Free unique keys - -- d <- view digestSHA256 - -- TODO: Bring these two for loops back - -- for_ (userEmail user) $ deleteKey d . userEmailKey - -- for_ (userPhone user) $ deleteKey d . userPhoneKey + d <- view digestSHA256 + for_ (userEmail user) $ liftSem . deleteKey d . userEmailKey + for_ (userPhone user) $ liftSem . deleteKey d . userPhoneKey liftSem $ for_ (userHandle user) $ freeHandle (userId user) -- Wipe data wrapClient $ Data.clearProperties uid diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index ed4c2f3b29..dd34b41ac7 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -29,6 +29,7 @@ import Brig.Sem.GalleyAccess (GalleyAccess) import Brig.Sem.GundeckAccess (GundeckAccess) import Brig.Sem.UniqueClaimsStore import Brig.Sem.UserHandleStore +import Brig.Sem.UserKeyStore (UserKeyStore) import Brig.Sem.UserQuery import Control.Lens (view) import Control.Monad.Catch @@ -58,6 +59,7 @@ onEvent :: Race, UniqueClaimsStore, UserHandleStore, + UserKeyStore, UserQuery ] r => From 975d3efc8a758f1f2e816066e8c73bf04e47797a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 26 Aug 2022 11:19:05 +0200 Subject: [PATCH 24/41] Move an error interpreter into a separate module --- .../polysemy-wire-zoo/polysemy-wire-zoo.cabal | 2 + libs/polysemy-wire-zoo/src/Wire/Sem/Error.hs | 37 +++++++++++++++++++ .../brig/src/Brig/CanonicalInterpreter.hs | 17 +-------- 3 files changed, 40 insertions(+), 16 deletions(-) create mode 100644 libs/polysemy-wire-zoo/src/Wire/Sem/Error.hs diff --git a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal index f239bef190..b34e185855 100644 --- a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal +++ b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal @@ -13,6 +13,7 @@ build-type: Simple library exposed-modules: Polysemy.TinyLog + Wire.Sem.Error Wire.Sem.FromUTC Wire.Sem.Logger Wire.Sem.Logger.Level @@ -86,6 +87,7 @@ library , time , tinylog , types-common + , unliftio , uuid , wire-api diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Error.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Error.hs new file mode 100644 index 0000000000..5c2d8b27df --- /dev/null +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Error.hs @@ -0,0 +1,37 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.Sem.Error where + +import Imports +import Polysemy +import Polysemy.Error +import qualified UnliftIO.Exception as UnliftIO +import Wire.API.Error + +interpretErrorToException :: + (Exception exc, Member (Embed IO) r) => + (err -> exc) -> + Sem (Error err ': r) a -> + Sem r a +interpretErrorToException f = either (embed @IO . UnliftIO.throwIO . f) pure <=< runError + +interpretWaiErrorToException :: + (APIError e, Member (Embed IO) r) => + Sem (Error e ': r) a -> + Sem r a +interpretWaiErrorToException = interpretErrorToException toWai diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index a4285da1a5..2f67121ad4 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -75,8 +75,7 @@ import Polysemy.Input import Polysemy.Resource (Resource, resourceToIO) import qualified Polysemy.TinyLog as P import qualified Ropes.Twilio as Twilio -import qualified UnliftIO.Exception as UnliftIO -import Wire.API.Error +import Wire.Sem.Error import Wire.Sem.Logger.TinyLog import Wire.Sem.Now (Now) import Wire.Sem.Now.IO @@ -159,20 +158,6 @@ interpretHttpToIO e = interpret $ \case . runHttpClientIO $ action --- TODO(md): Copied from Galley.App. Move this utility function to a library. -interpretErrorToException :: - (Exception exc, Member (Embed IO) r) => - (err -> exc) -> - Sem (P.Error err ': r) a -> - Sem r a -interpretErrorToException f = either (embed @IO . UnliftIO.throwIO . f) pure <=< P.runError - -interpretWaiErrorToException :: - (APIError e, Member (Embed IO) r) => - Sem (P.Error e ': r) a -> - Sem r a -interpretWaiErrorToException = interpretErrorToException toWai - twilioToWai :: Twilio.ErrorResponse -> Wai.Error twilioToWai e = Wai.Error From 7b3184080325d2de3745853579ca66d12787eb1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 26 Aug 2022 11:20:40 +0200 Subject: [PATCH 25/41] Change TODO to FUTUREWORK notes --- services/brig/src/Brig/Data/Activation.hs | 2 +- services/brig/src/Brig/IO/Intra.hs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 3652bd7d7d..893e644471 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -215,7 +215,7 @@ verifyCode key code = do -- countdown = lift . retry x5 . write keyInsert . params LocalQuorum revoke = lift $ deleteActivationPair key --- TODO(md): This should be deleted and an effect action 'makeActivationKey' +-- FUTUREWORK: This should be deleted and an effect action 'makeActivationKey' -- should be used instead. mkActivationKey :: UserKey -> IO ActivationKey mkActivationKey k = do diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index fdf7693f27..d7dbd5c5a3 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -153,7 +153,7 @@ onUserEvent :: onUserEvent orig conn e = wrapClient (updateSearchIndex orig e) *> dispatchNotifications orig conn e - *> wrapClient (journalEvent orig e) -- TODO(md): this just needs MonadIO m and MonadReader + *> wrapClient (journalEvent orig e) -- FUTUREWORK: this just needs MonadIO m and MonadReader -- Env m. It seems to access AWS, so -- perhaps that's an effect on its own. From 556b850cfc34147b6bc028f1018ebd3613d2bcff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 26 Aug 2022 11:27:20 +0200 Subject: [PATCH 26/41] Remove commented out code in Brig.IO.Intra --- services/brig/src/Brig/IO/Intra.hs | 86 ------------------------------ 1 file changed, 86 deletions(-) diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index d7dbd5c5a3..a6746973a2 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -344,92 +344,6 @@ notifyUserDeletionRemotes deleted = do . Log.field "domain" (domainText domain) . Log.field "error" (show fErr) --- | Push events to other users. --- push :: --- ( MonadIO m, --- Log.MonadLogger m, --- MonadReader Env m, --- MonadMask m, --- MonadCatch m, --- MonadHttp m, --- HasRequestId m --- ) => --- -- | The events to push. --- List1 Event -> --- -- | The users to push to. --- List1 UserId -> --- -- | The originator of the events. --- UserId -> --- -- | The push routing strategy. --- Push.Route -> --- -- | The originating device connection. --- Maybe ConnId -> --- m () --- push (toList -> events) usrs orig route conn = --- case mapMaybe toPushData events of --- [] -> pure () --- x : xs -> rawPush (list1 x xs) usrs orig route conn --- where --- toPushData :: Event -> Maybe (Builder, (Object, Maybe ApsData)) --- toPushData e = case toPushFormat e of --- Just o -> Just (Log.bytes e, (o, toApsData e)) --- Nothing -> Nothing - --- | Push encoded events to other users. Useful if you want to push --- something that's not defined in Brig. --- rawPush :: --- ( MonadIO m, --- Log.MonadLogger m, --- MonadReader Env m, --- MonadMask m, --- MonadCatch m, --- MonadHttp m, --- HasRequestId m --- ) => --- -- | The events to push. --- List1 (Builder, (Object, Maybe ApsData)) -> --- -- | The users to push to. --- List1 UserId -> --- -- | The originator of the events. --- UserId -> --- -- | The push routing strategy. --- Push.Route -> --- -- | The originating device connection. --- Maybe ConnId -> --- m () --- -- TODO: if we decide to have service whitelist events in Brig instead of --- -- Galley, let's merge 'push' and 'rawPush' back. See Note [whitelist events]. --- rawPush (toList -> events) usrs orig route conn = do --- for_ events $ \e -> debug $ remote "gundeck" . msg (fst e) --- g <- view gundeck --- forM_ recipients $ \rcps -> --- void . recovering x3 rpcHandlers $ --- const $ --- rpc' --- "gundeck" --- g --- ( method POST --- . path "/i/push/v2" --- . zUser orig -- FUTUREWORK: Remove, because gundeck handler ignores this. --- . json (map (mkPush rcps . snd) events) --- . expect2xx --- ) --- where --- recipients :: [Range 1 1024 (Set.Set Recipient)] --- recipients = --- map (unsafeRange . Set.fromList) $ --- chunksOf 512 $ --- map (`recipient` route) $ --- toList usrs --- mkPush :: Range 1 1024 (Set.Set Recipient) -> (Object, Maybe ApsData) -> Push --- mkPush rcps (o, aps) = --- newPush --- (Just orig) --- rcps --- (singletonPayload o) --- & pushOriginConnection .~ conn --- & pushNativeAps .~ aps - -- | (Asynchronously) notifies other users of events. notify :: Members '[Async, GundeckAccess] r => From 7212fa9d825b9f2bd824c8a96f39a4d6f9660311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Fri, 26 Aug 2022 11:32:06 +0200 Subject: [PATCH 27/41] Fix missing Cql instances for activation types --- .../src/Brig/Sem/ActivationKeyStore/Cassandra.hs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs b/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs index eb2bc901be..aeede7c54e 100644 --- a/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs +++ b/services/brig/src/Brig/Sem/ActivationKeyStore/Cassandra.hs @@ -17,6 +17,7 @@ module Brig.Sem.ActivationKeyStore.Cassandra (activationKeyStoreToCassandra) where +import Brig.Data.Instances () import Brig.Sem.ActivationKeyStore import Cassandra import Data.Id @@ -24,12 +25,9 @@ import Imports import Polysemy import Wire.API.User.Activation --- TODO(md): See why there's no instance for 'Cql ActivationKey', yet the --- Brig.Data.User module sees one. Then remove all explicitly spelled out Cql --- constraints in this module. activationKeyStoreToCassandra :: forall m r a. - (MonadClient m, Member (Embed m) r, Cql ActivationKey, Cql ActivationCode) => + (MonadClient m, Member (Embed m) r) => Sem (ActivationKeyStore ': r) a -> Sem r a activationKeyStoreToCassandra = @@ -39,13 +37,13 @@ activationKeyStoreToCassandra = InsertActivationKey tuple -> keyInsertQuery tuple DeleteActivationPair k -> keyDelete k -getKey :: (MonadClient m, Cql ActivationKey, Cql ActivationCode) => ActivationKey -> m (Maybe GetKeyTuple) +getKey :: MonadClient m => ActivationKey -> m (Maybe GetKeyTuple) getKey key = retry x1 . query1 keySelect $ params LocalQuorum (Identity key) where keySelect :: PrepQuery R (Identity ActivationKey) (Int32, Ascii, Text, ActivationCode, Maybe UserId, Int32) keySelect = "SELECT ttl(code) as ttl, key_type, key_text, code, user, retries FROM activation_keys WHERE key = ?" -keyInsertQuery :: (MonadClient m, Cql ActivationKey, Cql ActivationCode) => InsertKeyTuple -> m () +keyInsertQuery :: MonadClient m => InsertKeyTuple -> m () keyInsertQuery (key, t, k, c, u, attempts, timeout) = retry x5 . write keyInsert $ params LocalQuorum (key, t, k, c, u, attempts, timeout) where @@ -55,7 +53,7 @@ keyInsertQuery (key, t, k, c, u, attempts, timeout) = \(key, key_type, key_text, code, user, retries) VALUES \ \(? , ? , ? , ? , ? , ? ) USING TTL ?" -keyDelete :: (MonadClient m, Cql ActivationKey) => ActivationKey -> m () +keyDelete :: MonadClient m => ActivationKey -> m () keyDelete = write q . params LocalQuorum . Identity where q :: PrepQuery W (Identity ActivationKey) () From 19df7e167dd9b3c1f8bcce5a0cfcb97852a2ae40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 30 Aug 2022 09:45:11 +0200 Subject: [PATCH 28/41] Move the FireAndForget effect from Galley into zoo --- .../polysemy-wire-zoo/polysemy-wire-zoo.cabal | 2 ++ .../src/Wire/Sem/FireAndForget.hs | 31 +++++++++++++++++++ .../src/Wire/Sem/FireAndForget/IO.hs | 25 ++++----------- services/galley/galley.cabal | 1 - services/galley/src/Galley/API/Action.hs | 2 +- services/galley/src/Galley/API/Federation.hs | 2 +- services/galley/src/Galley/API/LegalHold.hs | 2 +- services/galley/src/Galley/App.hs | 2 +- services/galley/src/Galley/Effects.hs | 2 +- 9 files changed, 44 insertions(+), 25 deletions(-) create mode 100644 libs/polysemy-wire-zoo/src/Wire/Sem/FireAndForget.hs rename services/galley/src/Galley/Effects/FireAndForget.hs => libs/polysemy-wire-zoo/src/Wire/Sem/FireAndForget/IO.hs (74%) diff --git a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal index b34e185855..b8b46e33e1 100644 --- a/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal +++ b/libs/polysemy-wire-zoo/polysemy-wire-zoo.cabal @@ -14,6 +14,8 @@ library exposed-modules: Polysemy.TinyLog Wire.Sem.Error + Wire.Sem.FireAndForget + Wire.Sem.FireAndForget.IO Wire.Sem.FromUTC Wire.Sem.Logger Wire.Sem.Logger.Level diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/FireAndForget.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/FireAndForget.hs new file mode 100644 index 0000000000..d2d8a16247 --- /dev/null +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/FireAndForget.hs @@ -0,0 +1,31 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.Sem.FireAndForget where + +import Polysemy + +data FireAndForget m a where + FireAndForgetOne :: m () -> FireAndForget m () + SpawnMany :: [m ()] -> FireAndForget m () + +makeSem ''FireAndForget + +fireAndForget :: Member FireAndForget r => Sem r () -> Sem r () +fireAndForget = fireAndForgetOne diff --git a/services/galley/src/Galley/Effects/FireAndForget.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/FireAndForget/IO.hs similarity index 74% rename from services/galley/src/Galley/Effects/FireAndForget.hs rename to libs/polysemy-wire-zoo/src/Wire/Sem/FireAndForget/IO.hs index 0a99f3c555..82729b5576 100644 --- a/services/galley/src/Galley/Effects/FireAndForget.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/FireAndForget/IO.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE TemplateHaskell #-} - -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2022 Wire Swiss GmbH @@ -17,33 +15,22 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Galley.Effects.FireAndForget - ( FireAndForget, - fireAndForget, - spawnMany, - interpretFireAndForget, - ) -where +module Wire.Sem.FireAndForget.IO (interpretFireAndForget) where import Imports import Polysemy import Polysemy.Final import UnliftIO.Async (pooledMapConcurrentlyN_) - -data FireAndForget m a where - FireAndForgetOne :: m () -> FireAndForget m () - SpawnMany :: [m ()] -> FireAndForget m () - -makeSem ''FireAndForget - -fireAndForget :: Member FireAndForget r => Sem r () -> Sem r () -fireAndForget = fireAndForgetOne +import Wire.Sem.FireAndForget -- | Run actions in separate threads and ignore results. -- -- /Note/: this will also ignore any state and error effects contained in the -- 'FireAndForget' action. Use with care. -interpretFireAndForget :: Member (Final IO) r => Sem (FireAndForget ': r) a -> Sem r a +interpretFireAndForget :: + Member (Final IO) r => + Sem (FireAndForget ': r) a -> + Sem r a interpretFireAndForget = interpretFinal @IO $ \case FireAndForgetOne action -> do action' <- runS action diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index f10581474e..88b0f4152f 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -81,7 +81,6 @@ library Galley.Effects.CustomBackendStore Galley.Effects.ExternalAccess Galley.Effects.FederatorAccess - Galley.Effects.FireAndForget Galley.Effects.GundeckAccess Galley.Effects.LegalHoldStore Galley.Effects.ListItems diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index d3201e1c5e..52e2a37574 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -62,7 +62,6 @@ import qualified Galley.Effects.BrigAccess as E import qualified Galley.Effects.CodeStore as E import qualified Galley.Effects.ConversationStore as E import qualified Galley.Effects.FederatorAccess as E -import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E import qualified Galley.Effects.TeamStore as E import Galley.Options @@ -88,6 +87,7 @@ import Wire.API.Federation.Error import Wire.API.Team.LegalHold import Wire.API.Team.Member import qualified Wire.API.User as User +import qualified Wire.Sem.FireAndForget as E data NoChanges = NoChanges diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 4a93480e8c..bd381e0661 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -50,7 +50,6 @@ import qualified Galley.Data.Conversation as Data import Galley.Effects import qualified Galley.Effects.BrigAccess as E import qualified Galley.Effects.ConversationStore as E -import qualified Galley.Effects.FireAndForget as E import qualified Galley.Effects.MemberStore as E import Galley.Effects.ProposalStore (ProposalStore) import Galley.Options @@ -88,6 +87,7 @@ import Wire.API.Message import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Named import Wire.API.ServantProto +import qualified Wire.Sem.FireAndForget as E type FederationAPI = "federation" :> FedApi 'Galley diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 302ef62ad5..829e2f6d58 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -51,7 +51,6 @@ import Galley.API.Util import qualified Galley.Data.Conversation as Data import Galley.Effects import Galley.Effects.BrigAccess -import Galley.Effects.FireAndForget import qualified Galley.Effects.LegalHoldStore as LegalHoldData import qualified Galley.Effects.TeamFeatureStore as TeamFeatures import Galley.Effects.TeamMemberStore @@ -79,6 +78,7 @@ import qualified Wire.API.Team.LegalHold as Public import Wire.API.Team.LegalHold.External hiding (userId) import Wire.API.Team.Member import Wire.API.User.Client.Prekey +import Wire.Sem.FireAndForget import Wire.Sem.Paging import Wire.Sem.Paging.Cassandra diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index a145b9b446..794a13a837 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -74,7 +74,6 @@ import Galley.Cassandra.Team import Galley.Cassandra.TeamFeatures import Galley.Cassandra.TeamNotifications import Galley.Effects -import Galley.Effects.FireAndForget (interpretFireAndForget) import Galley.Effects.WaiRoutes.IO import Galley.Env import Galley.External @@ -106,6 +105,7 @@ import qualified UnliftIO.Exception as UnliftIO import Util.Options import Wire.API.Error import Wire.API.Federation.Error +import Wire.Sem.FireAndForget.IO (interpretFireAndForget) import qualified Wire.Sem.Logger -- Effects needed by the interpretation of other effects diff --git a/services/galley/src/Galley/Effects.hs b/services/galley/src/Galley/Effects.hs index 3cdcd04dd4..1ec846a1c4 100644 --- a/services/galley/src/Galley/Effects.hs +++ b/services/galley/src/Galley/Effects.hs @@ -71,7 +71,6 @@ import Galley.Effects.ConversationStore import Galley.Effects.CustomBackendStore import Galley.Effects.ExternalAccess import Galley.Effects.FederatorAccess -import Galley.Effects.FireAndForget import Galley.Effects.GundeckAccess import Galley.Effects.LegalHoldStore import Galley.Effects.ListItems @@ -93,6 +92,7 @@ import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog import Wire.API.Error +import Wire.Sem.FireAndForget import Wire.Sem.Paging.Cassandra -- All the possible high-level effects. From 48c289be18e9b7584d3d128c0579d53dfa871fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 30 Aug 2022 11:03:42 +0200 Subject: [PATCH 29/41] Simplify the interface of Brig.IO.Intra.notify --- services/brig/src/Brig/API/Client.hs | 6 +-- services/brig/src/Brig/API/Connection.hs | 16 +++---- .../brig/src/Brig/API/Connection/Remote.hs | 6 +-- services/brig/src/Brig/API/Federation.hs | 2 +- services/brig/src/Brig/API/Properties.hs | 6 +-- services/brig/src/Brig/IO/Intra.hs | 43 +++++++++---------- 6 files changed, 39 insertions(+), 40 deletions(-) diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 3a182f289f..7a787fe012 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -187,14 +187,14 @@ addClientWithReAuthPolicy policy u con ip new = do else id lhcaps = ClientSupportsLegalholdImplicitConsent (clt0, old, count) <- - (Data.addClientWithReAuthPolicy policy u clientId' new maxPermClients loc caps) + Data.addClientWithReAuthPolicy policy u clientId' new maxPermClients loc caps !>> ClientDataError let clt = clt0 {clientMLSPublicKeys = newClientMLSPublicKeys new} let usr = accountUser acc lift $ do for_ old $ execDelete u con wrapHttp $ Intra.newClient u (clientId clt) - Intra.onClientEvent u con (ClientAdded u clt) + liftSem $ Intra.onClientEvent u con (ClientAdded u clt) when (clientType clt == LegalHoldClientType) $ Intra.onUserEvent u con (UserLegalHoldEnabled u) when (count > 1) $ @@ -417,7 +417,7 @@ execDelete :: execDelete u con c = do wrapHttp $ Intra.rmClient u (clientId c) for_ (clientCookie c) $ \l -> wrapClient $ Auth.revokeCookies u [] [l] - Intra.onClientEvent u con (ClientRemoved u c) + liftSem $ Intra.onClientEvent u con (ClientRemoved u c) wrapClient $ Data.rmClient u (clientId c) -- | Defensive measure when no prekey is found for a diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index 862805f10c..3f3495e203 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -131,7 +131,7 @@ createConnectionToLocalUser self conn target = do ConnectionUpdated o2s' (ucStatus <$> o2s) <$> liftSem (Data.getName (tUnqualified self)) let e2s = ConnectionUpdated s2o' (ucStatus <$> s2o) Nothing - mapM_ (Intra.onConnectionEvent (tUnqualified self) (Just conn)) [e2o, e2s] + liftSem $ mapM_ (Intra.onConnectionEvent (tUnqualified self) (Just conn)) [e2o, e2s] pure s2o' update :: UserConnection -> UserConnection -> ExceptT ConnectionError (AppT r) (ResponseForExistedCreated UserConnection) @@ -168,7 +168,7 @@ createConnectionToLocalUser self conn target = do ConnectionUpdated o2s' (Just $ ucStatus o2s) <$> Data.getName (tUnqualified self) let e2s = ConnectionUpdated s2o' (Just $ ucStatus s2o) Nothing - lift $ mapM_ (Intra.onConnectionEvent (tUnqualified self) (Just conn)) [e2o, e2s] + lift . liftSem $ mapM_ (Intra.onConnectionEvent (tUnqualified self) (Just conn)) [e2o, e2s] pure $ Existed s2o' resend :: UserConnection -> UserConnection -> ExceptT ConnectionError (AppT r) (ResponseForExistedCreated UserConnection) @@ -287,7 +287,7 @@ updateConnectionToLocalUser self other newStatus conn = do -- invalid _ -> throwE $ InvalidTransition (tUnqualified self) let s2oUserConn = s2o' - lift . for_ s2oUserConn $ \c -> + lift . liftSem . for_ s2oUserConn $ \c -> let e2s = ConnectionUpdated c (Just $ ucStatus s2o) Nothing in Intra.onConnectionEvent (tUnqualified self) conn e2s pure s2oUserConn @@ -312,7 +312,7 @@ updateConnectionToLocalUser self other newStatus conn = do e2o <- ConnectionUpdated o2s' (Just $ ucStatus o2s) <$> liftSem (Data.getName (tUnqualified self)) - Intra.onConnectionEvent (tUnqualified self) conn e2o + liftSem $ Intra.onConnectionEvent (tUnqualified self) conn e2o lift . wrapClient $ Just <$> Data.updateConnection s2o AcceptedWithHistory block :: UserConnection -> ExceptT ConnectionError (AppT r) (Maybe UserConnection) @@ -343,7 +343,7 @@ updateConnectionToLocalUser self other newStatus conn = do ConnectionUpdated o2s' (Just $ ucStatus o2s) <$> Data.getName (tUnqualified self) -- TODO: is this correct? shouldnt o2s be sent to other? - Intra.onConnectionEvent (tUnqualified self) conn e2o + liftSem $ Intra.onConnectionEvent (tUnqualified self) conn e2o lift . wrapClient $ Just <$> Data.updateConnection s2o (mkRelationWithHistory (error "impossible") new) cancel :: @@ -358,7 +358,7 @@ updateConnectionToLocalUser self other newStatus conn = do lift $ traverse_ (wrapHttp . Intra.blockConv lfrom conn) (ucConvId s2o) o2s' <- lift . wrapClient $ Data.updateConnection o2s CancelledWithHistory let e2o = ConnectionUpdated o2s' (Just $ ucStatus o2s) Nothing - lift $ Intra.onConnectionEvent (tUnqualified self) conn e2o + lift . liftSem $ Intra.onConnectionEvent (tUnqualified self) conn e2o change s2o Cancelled change :: UserConnection -> Relation -> ExceptT ConnectionError (AppT r) (Maybe UserConnection) @@ -426,7 +426,7 @@ updateConnectionInternal = \case traverse_ (wrapHttp . Intra.blockConv lfrom Nothing) (ucConvId uconn) uconn' <- wrapClient $ Data.updateConnection uconn (mkRelationWithHistory (ucStatus uconn) MissingLegalholdConsent) let ev = ConnectionUpdated uconn' (Just $ ucStatus uconn) Nothing - Intra.onConnectionEvent (tUnqualified self) Nothing ev + liftSem $ Intra.onConnectionEvent (tUnqualified self) Nothing ev removeLHBlocksInvolving :: Local UserId -> ExceptT ConnectionError (AppT r) () removeLHBlocksInvolving self = @@ -470,7 +470,7 @@ updateConnectionInternal = \case ucPrev = Just $ ucStatus uconnRev, ucName = connName } - lift $ Intra.onConnectionEvent (ucFrom uconn) Nothing connEvent + lift . liftSem $ Intra.onConnectionEvent (ucFrom uconn) Nothing connEvent relationWithHistory :: Local UserId -> diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index d84d9f2581..78fa9eb532 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -170,7 +170,7 @@ transitionTo self mzcon other Nothing (Just rel) actor = lift $ do qcnv -- send event - pushEvent self mzcon connection + liftSem $ pushEvent self mzcon connection pure (Created connection, True) transitionTo _self _zcon _other (Just connection) Nothing _actor = pure (Existed connection, False) transitionTo self mzcon other (Just connection) (Just rel) actor = lift $ do @@ -181,7 +181,7 @@ transitionTo self mzcon other (Just connection) (Just rel) actor = lift $ do connection' <- wrapClient $ Data.updateConnection connection (relationWithHistory rel) -- send event - pushEvent self mzcon connection' + liftSem $ pushEvent self mzcon connection' pure (Existed connection', True) -- | Send an event to the local user when the state of a connection changes. @@ -190,7 +190,7 @@ pushEvent :: Local UserId -> Maybe ConnId -> UserConnection -> - AppT r () + Sem r () pushEvent self mzcon connection = do let event = ConnectionUpdated connection Nothing Nothing Intra.onConnectionEvent (tUnqualified self) mzcon event diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 29f567e44f..d6d4effefe 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -234,6 +234,6 @@ onUserDeleted origDomain udcn = lift $ do -- pooledForConcurrentlyN_ 16 (nonEmpty acceptedLocals) $ \(List1 -> recipients) -> -- TODO(md): run this in an effect interpreter because this is purely an optimisation for_ (nonEmpty acceptedLocals) $ \recipients -> - notify event (tUnqualified deletedUser) Push.RouteDirect Nothing (pure recipients) + liftSem $ notify event (tUnqualified deletedUser) Push.RouteDirect Nothing recipients wrapClient $ Data.deleteRemoteConnections deletedUser connections pure EmptyResponse diff --git a/services/brig/src/Brig/API/Properties.hs b/services/brig/src/Brig/API/Properties.hs index 8c51e983b1..ec093aebb7 100644 --- a/services/brig/src/Brig/API/Properties.hs +++ b/services/brig/src/Brig/API/Properties.hs @@ -48,7 +48,7 @@ setProperty :: ExceptT PropertiesDataError (AppT r) () setProperty u c k v = do wrapClientE $ Data.insertProperty u k (propertyRaw v) - lift $ Intra.onPropertyEvent u c (PropertySet u k v) + lift . liftSem $ Intra.onPropertyEvent u c (PropertySet u k v) deleteProperty :: Members '[Async, GundeckAccess] r => @@ -58,7 +58,7 @@ deleteProperty :: AppT r () deleteProperty u c k = do wrapClient $ Data.deleteProperty u k - Intra.onPropertyEvent u c (PropertyDeleted u k) + liftSem $ Intra.onPropertyEvent u c (PropertyDeleted u k) clearProperties :: Members '[Async, GundeckAccess] r => @@ -67,4 +67,4 @@ clearProperties :: AppT r () clearProperties u c = do wrapClient $ Data.clearProperties u - Intra.onPropertyEvent u c (PropertiesCleared u) + liftSem $ Intra.onPropertyEvent u c (PropertiesCleared u) diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index a6746973a2..a000ae594d 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -165,7 +165,7 @@ onConnectionEvent :: Maybe ConnId -> -- | The event. ConnectionEvent -> - AppT r () + Sem r () onConnectionEvent orig conn evt = do let from = ucFrom (ucConn evt) notify @@ -173,7 +173,7 @@ onConnectionEvent orig conn evt = do orig Push.RouteAny conn - (pure $ pure from) + (pure from) onPropertyEvent :: Members '[Async, GundeckAccess] r => @@ -182,14 +182,14 @@ onPropertyEvent :: -- | Client connection ID. ConnId -> PropertyEvent -> - AppT r () + Sem r () onPropertyEvent orig conn e = notify (pure $ PropertyEvent e) orig Push.RouteDirect (Just conn) - (pure $ pure orig) + (pure orig) onClientEvent :: Members '[GundeckAccess] r => @@ -199,14 +199,14 @@ onClientEvent :: Maybe ConnId -> -- | The event. ClientEvent -> - AppT r () + Sem r () onClientEvent orig conn e = do let events = pure (ClientEvent e) let rcps = pure orig -- Synchronous push for better delivery guarantees of these -- events and to make sure new clients have a first notification -- in the stream. - liftSem $ pushEvents events rcps orig Push.RouteAny conn + pushEvents events rcps orig Push.RouteAny conn updateSearchIndex :: ( MonadClient m, @@ -284,11 +284,11 @@ dispatchNotifications orig conn e = case e of UserLegalHoldEnabled {} -> notifyContacts event orig Push.RouteAny conn UserUpdated UserUpdatedData {..} -- This relies on the fact that we never change the locale AND something else. - | isJust eupLocale -> notifySelf event orig Push.RouteDirect conn + | isJust eupLocale -> liftSem $ notifySelf event orig Push.RouteDirect conn | otherwise -> notifyContacts event orig Push.RouteDirect conn - UserActivated {} -> notifySelf event orig Push.RouteAny conn - UserIdentityUpdated {} -> notifySelf event orig Push.RouteDirect conn - UserIdentityRemoved {} -> notifySelf event orig Push.RouteDirect conn + UserActivated {} -> liftSem $ notifySelf event orig Push.RouteAny conn + UserIdentityUpdated {} -> liftSem $ notifySelf event orig Push.RouteDirect conn + UserIdentityRemoved {} -> liftSem $ notifySelf event orig Push.RouteDirect conn UserDeleted {} -> do -- n.b. Synchronously fetch the contact list on the current thread. -- If done asynchronously, the connections may already have been deleted. @@ -305,7 +305,7 @@ notifyUserDeletionLocals :: AppT r () notifyUserDeletionLocals deleted conn event = do recipients <- wrapClient $ (deleted :|) <$> lookupContactList deleted - notify event deleted Push.RouteDirect conn (pure recipients) + liftSem $ notify event deleted Push.RouteDirect conn recipients notifyUserDeletionRemotes :: forall m. @@ -355,25 +355,24 @@ notify :: -- | Origin device connection, if any. Maybe ConnId -> -- | Users to notify. - AppT r (NonEmpty UserId) -> - AppT r () + NonEmpty UserId -> + Sem r () notify events orig route conn recipients = do - rs <- recipients fork (Just orig) $ do - pushEvents events rs orig route conn + pushEvents events recipients orig route conn fork :: Members '[Async] r => Maybe UserId -> Sem r a -> - AppT r () + Sem r () fork _u act = do -- g <- view applog -- r <- view requestId -- let logErr e = P.err $ request r ~~ user u ~~ msg (show e) -- TODO(md): see what exceptions (if any) I can catch here. This is used -- exclusively in making a call to Gundeck, which is an effect - void $ liftSem (async act) -- >>= \case + void $ async act -- >>= \case -- Nothing -> liftSem $ logErr "sending events via Gundeck failed" -- Just _ -> pure () where @@ -395,9 +394,9 @@ notifySelf :: Push.Route -> -- | Origin device connection, if any. Maybe ConnId -> - AppT r () + Sem r () notifySelf events orig route conn = - notify events orig route conn (pure (pure orig)) + notify events orig route conn (pure orig) notifyContacts :: Members @@ -414,9 +413,9 @@ notifyContacts :: -- | Origin device connection, if any. Maybe ConnId -> AppT r () -notifyContacts events orig route conn = - notify events orig route conn $ - (orig :|) <$> liftA2 (++) (wrapClient contacts) (liftSem teamContacts) +notifyContacts events orig route conn = do + rs <- (orig :|) <$> liftA2 (++) (wrapClient contacts) (liftSem teamContacts) + liftSem $ notify events orig route conn rs where contacts :: MonadClient m => m [UserId] contacts = lookupContactList orig From dc55d2edbdccbfbd0f9300b02b35d9e782cb00b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Mon, 19 Sep 2022 16:17:19 +0200 Subject: [PATCH 30/41] Resolve a non-TODO --- services/brig/src/Brig/API/Federation.hs | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 81e4f5823e..0a6bf63901 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -230,9 +230,6 @@ onUserDeleted origDomain udcn = lift $ do map csv2From . filter (\x -> csv2Status x == Accepted) <$> wrapClient (Data.lookupRemoteConnectionStatuses (fromRange connections) (fmap pure deletedUser)) - -- wrapHttp $ - -- pooledForConcurrentlyN_ 16 (nonEmpty acceptedLocals) $ \(List1 -> recipients) -> - -- TODO(md): run this in an effect interpreter because this is purely an optimisation for_ (nonEmpty acceptedLocals) $ \recipients -> liftSem $ notify event (tUnqualified deletedUser) Push.RouteDirect Nothing recipients wrapClient $ Data.deleteRemoteConnections deletedUser connections From f105822f4ed57ad5bb9d691151aeceab849fddf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Mon, 19 Sep 2022 22:46:04 +0200 Subject: [PATCH 31/41] Remove the Polysemy.Async effect - Most of the time the effect wasn't actually used so I've removed threading. --- services/brig/src/Brig/API/Client.hs | 11 ++- services/brig/src/Brig/API/Connection.hs | 11 ++- .../brig/src/Brig/API/Connection/Remote.hs | 13 ++-- services/brig/src/Brig/API/Federation.hs | 8 +-- services/brig/src/Brig/API/Internal.hs | 35 ++++------ services/brig/src/Brig/API/Properties.hs | 7 +- services/brig/src/Brig/API/Public.hs | 46 +++++------- services/brig/src/Brig/API/User.hs | 51 ++++---------- .../brig/src/Brig/Effects/GundeckAccess.hs | 41 +++++++++++ .../src/Brig/Effects/GundeckAccess/Http.hs | 31 +++++++- services/brig/src/Brig/IO/Intra.hs | 70 ++----------------- .../brig/src/Brig/InternalEvent/Process.hs | 4 +- services/brig/src/Brig/Team/API.hs | 14 ++-- services/brig/src/Brig/User/API/Auth.hs | 29 +++----- services/brig/src/Brig/User/Auth.hs | 19 ++--- 15 files changed, 164 insertions(+), 226 deletions(-) diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 626e66c926..c32afa54c5 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -87,7 +87,6 @@ import qualified Data.Set as Set import Imports import Network.Wai.Utilities hiding (Error) import Polysemy -import Polysemy.Async import Polysemy.Error import Polysemy.Input import System.Logger.Class (field, msg, val, (~~)) @@ -140,8 +139,7 @@ lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk addClient :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserQuery, @@ -160,8 +158,7 @@ addClient = addClientWithReAuthPolicy Data.reAuthForNewClients addClientWithReAuthPolicy :: forall r. Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserQuery, @@ -457,7 +454,7 @@ pubClient c = } legalHoldClientRequested :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => UserId -> LegalHoldClientRequest -> AppT r () @@ -472,7 +469,7 @@ legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPreke lhClientEvent = LegalHoldClientRequested eventData removeLegalHoldClient :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => UserId -> AppT r () removeLegalHoldClient uid = do diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index b801ffd8e2..2b32d61d87 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -54,7 +54,6 @@ import Data.Range import qualified Data.UUID.V4 as UUID import Imports import Polysemy -import Polysemy.Async hiding (cancel) import qualified System.Logger.Class as Log import System.Logger.Message import Wire.API.Connection hiding (relationWithHistory) @@ -79,7 +78,7 @@ ensureNotSameTeam self target = do throwE ConnectSameBindingTeamUsers createConnection :: - Members '[Async, GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery] r => Local UserId -> ConnId -> Qualified UserId -> @@ -100,7 +99,7 @@ createConnection self con target = do createConnectionToLocalUser :: forall r. - Members '[Async, GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery] r => Local UserId -> ConnId -> Local UserId -> @@ -211,7 +210,7 @@ checkLegalholdPolicyConflict uid1 uid2 = do oneway status2 status1 updateConnection :: - Members '[Async, GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery] r => Local UserId -> Qualified UserId -> Relation -> @@ -232,7 +231,7 @@ updateConnection self other newStatus conn = -- {#RefConnectionTeam} updateConnectionToLocalUser :: forall r. - Members '[Async, GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery] r => -- | From Local UserId -> -- | To @@ -394,7 +393,7 @@ mkRelationWithHistory oldRel = \case updateConnectionInternal :: forall r. - Members '[Async, GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery] r => UpdateConnectionsInternal -> ExceptT ConnectionError (AppT r) () updateConnectionInternal = \case diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index 84c2cdac90..c44ae6c039 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -40,7 +40,6 @@ import Galley.Types.Conversations.Intra (Actor (..), DesiredMembership (..), Ups import Imports import Network.Wai.Utilities.Error import Polysemy -import Polysemy.Async import Wire.API.Connection import Wire.API.Federation.API.Brig ( NewConnectionResponse (..), @@ -144,7 +143,7 @@ updateOne2OneConv lUsr _mbConn remoteUser mbConvId rel actor = do -- -- Returns the connection, and whether it was updated or not. transitionTo :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => Local UserId -> Maybe ConnId -> Remote UserId -> @@ -186,7 +185,7 @@ transitionTo self mzcon other (Just connection) (Just rel) actor = lift $ do -- | Send an event to the local user when the state of a connection changes. pushEvent :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => Local UserId -> Maybe ConnId -> UserConnection -> @@ -196,7 +195,7 @@ pushEvent self mzcon connection = do Intra.onConnectionEvent (tUnqualified self) mzcon event performLocalAction :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => Local UserId -> Maybe ConnId -> Remote UserId -> @@ -244,7 +243,7 @@ performLocalAction self mzcon other mconnection action = do -- B connects & A reacts: Accepted Accepted -- @ performRemoteAction :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => Local UserId -> Remote UserId -> Maybe UserConnection -> @@ -262,7 +261,7 @@ performRemoteAction self other mconnection action = do reaction _ = Nothing createConnectionToRemoteUser :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => Local UserId -> ConnId -> Remote UserId -> @@ -272,7 +271,7 @@ createConnectionToRemoteUser self zcon other = do fst <$> performLocalAction self (Just zcon) other mconnection LocalConnect updateConnectionToRemoteUser :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => Local UserId -> Remote UserId -> Relation -> diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 0a6bf63901..cdcabde640 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -53,7 +53,6 @@ import qualified Gundeck.Types.Push as Push import Imports import Network.Wai.Utilities.Error ((!>>)) import Polysemy -import Polysemy.Async import Servant (ServerT) import Servant.API import qualified System.Logger.Class as Log @@ -75,8 +74,7 @@ type FederationAPI = "federation" :> BrigApi federationSitemap :: Members - '[ Async, - GundeckAccess, + '[ GundeckAccess, UserHandleStore, UserQuery ] @@ -97,7 +95,7 @@ federationSitemap = :<|> Named @"claim-key-packages" fedClaimKeyPackages sendConnectionAction :: - Members '[Async, GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery] r => Domain -> NewConnectionRequest -> Handler r NewConnectionResponse @@ -218,7 +216,7 @@ getMLSClients _domain mcr = do Internal.getMLSClients (mcrUserId mcr) (mcrSignatureScheme mcr) onUserDeleted :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => Domain -> UserDeletedConnectionsNotification -> Handler r EmptyResponse diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 31e3d209e1..a38ea57acb 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -332,11 +332,11 @@ sitemap :: GalleyAccess, GundeckAccess, Input (Local ()), + PasswordResetStore, + PasswordResetSupply, P.Error ReAuthError, P.Error Twilio.ErrorResponse, P.TinyLog, - PasswordResetStore, - PasswordResetSupply, Race, Resource, Twilio, @@ -513,8 +513,7 @@ sitemap = do -- | Add a client without authentication checks addClientInternalH :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserQuery, @@ -529,8 +528,7 @@ addClientInternalH (usr ::: mSkipReAuth ::: req ::: connId ::: _) = do addClientInternal :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserQuery, @@ -549,7 +547,7 @@ addClientInternal usr mSkipReAuth new connId = do API.addClientWithReAuthPolicy policy usr connId Nothing new !>> clientError legalHoldClientRequestedH :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => UserId ::: JsonRequest LegalHoldClientRequest ::: JSON -> Handler r Response legalHoldClientRequestedH (targetUser ::: req ::: _) = do @@ -558,7 +556,7 @@ legalHoldClientRequestedH (targetUser ::: req ::: _) = do pure $ setStatus status200 empty removeLegalHoldClientH :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => UserId ::: JSON -> Handler r Response removeLegalHoldClientH (uid ::: _) = do @@ -586,7 +584,6 @@ createUserNoVerify :: Members '[ ActivationKeyStore, ActivationSupply, - Async, BlacklistStore, GalleyAccess, GundeckAccess, @@ -653,8 +650,7 @@ createUserNoVerifySpar uData = deleteUserNoAuthH :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, @@ -820,7 +816,7 @@ instance ToJSON GetPasswordResetCodeResp where toJSON (GetPasswordResetCodeResp (k, c)) = object ["key" .= k, "code" .= c] changeAccountStatusH :: - Members '[Async, GalleyAccess, GundeckAccess, UserQuery] r => + Members '[GalleyAccess, GundeckAccess, UserQuery] r => UserId ::: JsonRequest AccountStatusUpdate -> Handler r Response changeAccountStatusH (usr ::: req) = do @@ -861,8 +857,7 @@ getConnectionsStatus (ConnectionsStatusRequestV2 froms mtos mrel) = do revokeIdentityH :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserKeyStore, @@ -876,7 +871,7 @@ revokeIdentityH emailOrPhone = do pure $ setStatus status200 empty updateConnectionInternalH :: - Members '[Async, GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery] r => JSON ::: JsonRequest UpdateConnectionsInternal -> (Handler r) Response updateConnectionInternalH (_ ::: req) = do @@ -921,7 +916,7 @@ addPhonePrefixH (_ ::: req) = do pure empty updateSSOIdH :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => UserId ::: JSON ::: JsonRequest UserSSOId -> Handler r Response updateSSOIdH (uid ::: _ ::: req) = do @@ -934,7 +929,7 @@ updateSSOIdH (uid ::: _ ::: req) = do else pure . setStatus status404 $ plain "User does not exist or has no team." deleteSSOIdH :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => UserId ::: JSON -> Handler r Response deleteSSOIdH (uid ::: _) = do @@ -995,9 +990,9 @@ getRichInfoMulti uids = updateHandleH :: Members '[ Async, - Race, GalleyAccess, GundeckAccess, + Race, Resource, UniqueClaimsStore, UserHandleStore, @@ -1028,13 +1023,13 @@ updateHandle uid (HandleUpdate handleUpd) = do API.changeHandle uid Nothing handle API.AllowSCIMUpdates !>> changeHandleError updateUserNameH :: - Members '[Async, GalleyAccess, GundeckAccess, UserQuery] r => + Members '[GalleyAccess, GundeckAccess, UserQuery] r => UserId ::: JSON ::: JsonRequest NameUpdate -> (Handler r) Response updateUserNameH (uid ::: _ ::: body) = empty <$ (updateUserName uid =<< parseJsonBody body) updateUserName :: - Members '[Async, GalleyAccess, GundeckAccess, UserQuery] r => + Members '[GalleyAccess, GundeckAccess, UserQuery] r => UserId -> NameUpdate -> (Handler r) () diff --git a/services/brig/src/Brig/API/Properties.hs b/services/brig/src/Brig/API/Properties.hs index 6eec1d74e7..8327ecd8b4 100644 --- a/services/brig/src/Brig/API/Properties.hs +++ b/services/brig/src/Brig/API/Properties.hs @@ -36,11 +36,10 @@ import Control.Error import Data.Id import Imports import Polysemy -import Polysemy.Async import Wire.API.Properties setProperty :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => UserId -> ConnId -> PropertyKey -> @@ -51,7 +50,7 @@ setProperty u c k v = do lift . liftSem $ Intra.onPropertyEvent u c (PropertySet u k v) deleteProperty :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => UserId -> ConnId -> PropertyKey -> @@ -61,7 +60,7 @@ deleteProperty u c k = do liftSem $ Intra.onPropertyEvent u c (PropertyDeleted u k) clearProperties :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => UserId -> ConnId -> AppT r () diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index ec26dcd2ea..70f9b6e7bc 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -210,15 +210,15 @@ servantSitemap :: '[ ActivationKeyStore, ActivationSupply, Async, - BlacklistStore, BlacklistPhonePrefixStore, + BlacklistStore, GalleyAccess, GundeckAccess, Input (Local ()), - P.Error ReAuthError, - P.Error Twilio.ErrorResponse, PasswordResetStore, PasswordResetSupply, + P.Error ReAuthError, + P.Error Twilio.ErrorResponse, Race, Resource, Twilio, @@ -351,7 +351,6 @@ sitemap :: Members '[ ActivationKeyStore, ActivationSupply, - Async, BlacklistStore, BlacklistPhonePrefixStore, BudgetStore, @@ -497,7 +496,6 @@ apiDocs :: Members '[ ActivationKeyStore, ActivationSupply, - Async, BlacklistStore, BlacklistPhonePrefixStore, BudgetStore, @@ -532,7 +530,7 @@ apiDocs = -- Handlers setProperty :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => UserId -> ConnId -> Public.PropertyKey -> @@ -577,7 +575,7 @@ parseStoredPropertyValue raw = case propertyValueFromRaw raw of throwStd internalServerError deleteProperty :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => UserId -> ConnId -> Public.PropertyKey -> @@ -585,7 +583,7 @@ deleteProperty :: deleteProperty u c k = lift (API.deleteProperty u c k) clearProperties :: - Members '[Async, GundeckAccess] r => + Members '[GundeckAccess] r => UserId -> ConnId -> Handler r () @@ -641,8 +639,7 @@ getMultiUserPrekeyBundleH zusr qualUserClients = do addClient :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserQuery, @@ -758,7 +755,6 @@ createUser :: Members '[ ActivationKeyStore, ActivationSupply, - Async, BlacklistStore, GalleyAccess, GundeckAccess, @@ -917,8 +913,7 @@ instance ToJSON GetActivationCodeResp where updateUser :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, UserQuery ] @@ -956,8 +951,7 @@ changePhone u _ (Public.puPhone -> phone) = lift . exceptTToMaybe $ do removePhone :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserKeyStore, @@ -972,8 +966,7 @@ removePhone self conn = removeEmail :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserKeyStore, @@ -997,7 +990,7 @@ changePassword :: changePassword u cp = lift . exceptTToMaybe $ API.changePassword u cp changeLocale :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => UserId -> ConnId -> Public.LocaleUpdate -> @@ -1162,7 +1155,7 @@ customerExtensionCheckBlockedDomains email = do throwM $ customerExtensionBlockedDomain domain createConnectionUnqualified :: - Members '[Async, GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery] r => UserId -> ConnId -> Public.ConnectionRequest -> @@ -1173,7 +1166,7 @@ createConnectionUnqualified self conn cr = do API.createConnection lself conn (qUntagged target) !>> connError createConnection :: - Members '[Async, GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery] r => UserId -> ConnId -> Qualified UserId -> @@ -1183,7 +1176,7 @@ createConnection self conn target = do API.createConnection lself conn target !>> connError updateLocalConnection :: - Members '[Async, GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery] r => UserId -> ConnId -> UserId -> @@ -1194,7 +1187,7 @@ updateLocalConnection self conn other update = do updateConnection self conn (qUntagged lother) update updateConnection :: - Members '[Async, GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery] r => UserId -> ConnId -> Qualified UserId -> @@ -1266,8 +1259,7 @@ getConnection self other = do deleteSelfUser :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, @@ -1285,8 +1277,7 @@ deleteSelfUser u body = verifyDeleteUserH :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, @@ -1356,7 +1347,6 @@ activateKeyH :: Members '[ ActivationKeyStore, ActivationSupply, - Async, GalleyAccess, GundeckAccess, Input (Local ()), @@ -1378,7 +1368,6 @@ activateH :: Members '[ ActivationKeyStore, ActivationSupply, - Async, GalleyAccess, GundeckAccess, Input (Local ()), @@ -1400,7 +1389,6 @@ activate :: Members '[ ActivationKeyStore, ActivationSupply, - Async, GalleyAccess, GundeckAccess, Input (Local ()), diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 8336ce00bd..99c119b4e7 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -326,7 +326,6 @@ createUser :: Members '[ ActivationKeyStore, ActivationSupply, - Async, BlacklistStore, GalleyAccess, GundeckAccess, @@ -633,8 +632,7 @@ checkRestrictedUserCreation new = do updateUser :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, UserQuery ] @@ -665,8 +663,7 @@ updateUser uid mconn uu allowScim = do changeLocale :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess ] r => @@ -683,8 +680,7 @@ changeLocale uid conn (LocaleUpdate loc) = do changeManagedBy :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess ] r => @@ -920,8 +916,7 @@ changePhone u phone = do removeEmail :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserKeyStore, @@ -947,8 +942,7 @@ removeEmail uid conn = do removePhone :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserKeyStore, @@ -979,8 +973,7 @@ removePhone uid conn = do revokeIdentity :: forall r. Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserKeyStore, @@ -1026,8 +1019,7 @@ revokeIdentity key = do changeAccountStatus :: forall r. Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess ] r => @@ -1060,21 +1052,11 @@ changeAccountStatus usrs status = do changeSingleAccountStatus :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, UserQuery ] r => - -- ( MonadClient m, - -- MonadLogger m, - -- MonadIndexIO m, - -- MonadReader Env m, - -- MonadMask m, - -- MonadHttp m, - -- HasRequestId m, - -- MonadUnliftIO m - -- ) => UserId -> AccountStatus -> ExceptT AccountStatusError (AppT r) () @@ -1108,7 +1090,6 @@ activate :: Members '[ ActivationKeyStore, ActivationSupply, - Async, GalleyAccess, GundeckAccess, Input (Local ()), @@ -1131,7 +1112,6 @@ activateWithCurrency :: Members '[ ActivationKeyStore, ActivationSupply, - Async, GalleyAccess, GundeckAccess, Input (Local ()), @@ -1194,8 +1174,7 @@ preverify tgt code creds m = do onActivated :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, UserQuery ] @@ -1445,8 +1424,7 @@ mkPasswordResetKey ident = case ident of -- TODO: communicate deletions of SSO users to SSO service. deleteSelfUser :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, @@ -1535,8 +1513,7 @@ deleteSelfUser uid pwd = do -- 'deleteUser'. Called via @post /delete@. verifyDeleteUser :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, @@ -1562,8 +1539,7 @@ verifyDeleteUser d = do -- Called via @delete /i/user/:uid@. ensureAccountDeleted :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, @@ -1613,8 +1589,7 @@ ensureAccountDeleted uid = do deleteAccount :: forall r. Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, UniqueClaimsStore, UserHandleStore, diff --git a/services/brig/src/Brig/Effects/GundeckAccess.hs b/services/brig/src/Brig/Effects/GundeckAccess.hs index 2c8dfc3e48..8bae78dfbd 100644 --- a/services/brig/src/Brig/Effects/GundeckAccess.hs +++ b/services/brig/src/Brig/Effects/GundeckAccess.hs @@ -39,5 +39,46 @@ data GundeckAccess m a where -- | The originating device connection. Maybe ConnId -> GundeckAccess m () + PushEventsAsync :: + -- | The events to push. + NonEmpty Event -> + -- | The users to push to. + NonEmpty UserId -> + -- | The originator of the events. + UserId -> + -- | The push routing strategy. + Route -> + -- | The originating device connection. + Maybe ConnId -> + GundeckAccess m () makeSem ''GundeckAccess + +-- | (Asynchronously) notifies other users of events. +notify :: + Member GundeckAccess r => + NonEmpty Event -> + -- | Origin user, TODO: Delete + UserId -> + -- | Push routing strategy. + Route -> + -- | Origin device connection, if any. + Maybe ConnId -> + -- | Users to notify. + NonEmpty UserId -> + Sem r () +notify events orig route conn recipients = + pushEventsAsync events recipients orig route conn + +notifySelf :: + Member GundeckAccess r => + NonEmpty Event -> + -- | Origin user. + UserId -> + -- | Push routing strategy. + Route -> + -- | Origin device connection, if any. + Maybe ConnId -> + Sem r () +notifySelf events orig route conn = + notify events orig route conn (pure orig) diff --git a/services/brig/src/Brig/Effects/GundeckAccess/Http.hs b/services/brig/src/Brig/Effects/GundeckAccess/Http.hs index 732f726d8d..80dae14a84 100644 --- a/services/brig/src/Brig/Effects/GundeckAccess/Http.hs +++ b/services/brig/src/Brig/Effects/GundeckAccess/Http.hs @@ -22,16 +22,20 @@ module Brig.Effects.GundeckAccess.Http (gundeckAccessToHttp) where import qualified Bilge as RPC import Bilge.IO import Bilge.RPC -import Bilge.Request +import Bilge.Request hiding (requestId) +import Brig.App import Brig.Effects.Common import Brig.Effects.GundeckAccess import Brig.RPC import Brig.Types.User.Event -import Control.Lens ((.~), (?~)) +import Control.Error.Util +import Control.Lens (view, (.~), (?~)) import Control.Monad.Catch +import Control.Monad.Trans.Except import Data.Aeson (object, (.=)) import qualified Data.Aeson as A import qualified Data.Aeson.KeyMap as A +import Data.ByteString.Conversion.To import Data.Id import Data.Json.Util ((#)) import Data.List.NonEmpty @@ -44,6 +48,7 @@ import Imports hiding (toList) import Network.HTTP.Types.Method import Polysemy import System.Logger.Class as Log hiding (name, (.=)) +import qualified System.Logger.Extended as ExLog import Wire.API.Connection import Wire.API.Properties import Wire.API.User @@ -55,6 +60,8 @@ gundeckAccessToHttp :: MonadLogger m, MonadMask m, MonadHttp m, + MonadUnliftIO m, + MonadReader Env m, HasRequestId m, Member (Embed m) r ) => @@ -66,6 +73,26 @@ gundeckAccessToHttp g = embed @m . \case PushEvents events users orig route mConn -> do push g events users orig route mConn + PushEventsAsync events users orig route mConn -> do + fork (Just orig) $ + push g events users orig route mConn + +fork :: + (MonadIO m, MonadUnliftIO m, MonadReader Env m) => + Maybe UserId -> + m a -> + m () +fork u ma = do + g <- view applog + r <- view requestId + let logErr e = ExLog.err g $ request r ~~ user u ~~ msg (show e) + withRunInIO $ \lower -> + void . liftIO . forkIO $ + either logErr (const $ pure ()) + =<< runExceptT (syncIO $ lower ma) + where + request = field "request" . unRequestId + user = maybe id (field "user" . toByteString) -- | Push events to other users. push :: diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index a61a219b52..5e6fd7cbe9 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -120,7 +120,6 @@ import Network.HTTP.Types.Method import Network.HTTP.Types.Status import qualified Network.Wai.Utilities.Error as Wai import Polysemy -import Polysemy.Async import System.Logger.Class as Log hiding (name, (.=)) import Wire.API.Connection import Wire.API.Conversation hiding (Member) @@ -143,8 +142,7 @@ import Wire.API.User.Client onUserEvent :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess ] r => @@ -160,7 +158,7 @@ onUserEvent orig conn e = -- perhaps that's an effect on its own. onConnectionEvent :: - Members '[Async, GundeckAccess] r => + Member GundeckAccess r => -- | Originator of the event. UserId -> -- | Client connection ID, if any. @@ -178,7 +176,7 @@ onConnectionEvent orig conn evt = do (pure from) onPropertyEvent :: - Members '[Async, GundeckAccess] r => + Member GundeckAccess r => -- | Originator of the event. UserId -> -- | Client connection ID. @@ -268,8 +266,7 @@ journalEvent orig e = case e of -- or profile. dispatchNotifications :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess ] r => @@ -300,7 +297,7 @@ dispatchNotifications orig conn e = case e of event = pure $ UserEvent e notifyUserDeletionLocals :: - Members '[Async, GundeckAccess] r => + Member GundeckAccess r => UserId -> Maybe ConnId -> NonEmpty Event -> @@ -346,64 +343,9 @@ notifyUserDeletionRemotes deleted = do . Log.field "domain" (domainText domain) . Log.field "error" (show fErr) --- | (Asynchronously) notifies other users of events. -notify :: - Members '[Async, GundeckAccess] r => - NonEmpty Event -> - -- | Origin user, TODO: Delete - UserId -> - -- | Push routing strategy. - Push.Route -> - -- | Origin device connection, if any. - Maybe ConnId -> - -- | Users to notify. - NonEmpty UserId -> - Sem r () -notify events orig route conn recipients = do - fork (Just orig) $ do - pushEvents events recipients orig route conn - -fork :: - Members '[Async] r => - Maybe UserId -> - Sem r a -> - Sem r () -fork _u act = do - -- g <- view applog - -- r <- view requestId - -- let logErr e = P.err $ request r ~~ user u ~~ msg (show e) - -- TODO(md): see what exceptions (if any) I can catch here. This is used - -- exclusively in making a call to Gundeck, which is an effect - void $ async act -- >>= \case - -- Nothing -> liftSem $ logErr "sending events via Gundeck failed" - -- Just _ -> pure () - where - --- withRunInIO $ \lower -> --- void . liftIO . forkIO $ --- either logErr (const $ pure ()) --- =<< runExceptT (syncIO $ lower ma) - --- request = field "request" . unRequestId --- user = maybe id (field "user" . toByteString) - -notifySelf :: - Members '[Async, GundeckAccess] r => - NonEmpty Event -> - -- | Origin user. - UserId -> - -- | Push routing strategy. - Push.Route -> - -- | Origin device connection, if any. - Maybe ConnId -> - Sem r () -notifySelf events orig route conn = - notify events orig route conn (pure orig) - notifyContacts :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess ] r => diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index cb7271d877..031d3dec30 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -41,7 +41,6 @@ import Data.ByteString.Conversion import Data.Qualified import Imports import Polysemy -import Polysemy.Async import Polysemy.Conc.Effect.Race import Polysemy.Conc.Race import Polysemy.Input @@ -55,8 +54,7 @@ import System.Logger.Class (field, msg, val, (~~)) onEvent :: forall r. Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), P.TinyLog, diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index ea80d61b65..5164f3c2c6 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -69,7 +69,6 @@ import Network.Wai.Utilities hiding (code, message) import Network.Wai.Utilities.Swagger (document) import qualified Network.Wai.Utilities.Swagger as Doc import Polysemy -import Polysemy.Async import qualified Polysemy.Error as P import Polysemy.Input import qualified Ropes.Twilio as Twilio @@ -212,8 +211,7 @@ routesPublic = do routesInternal :: Members - '[ Async, - BlacklistStore, + '[ BlacklistStore, GalleyAccess, GundeckAccess, P.Error Twilio.ErrorResponse, @@ -537,14 +535,14 @@ getInvitationByEmail email = do maybe (throwStd (notFound "Invitation not found")) pure inv suspendTeamH :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => JSON ::: TeamId -> Handler r Response suspendTeamH (_ ::: tid) = do empty <$ suspendTeam tid suspendTeam :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => TeamId -> Handler r () suspendTeam tid = do @@ -553,14 +551,14 @@ suspendTeam tid = do lift $ wrapHttp $ Intra.changeTeamStatus tid Team.Suspended Nothing unsuspendTeamH :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => JSON ::: TeamId -> Handler r Response unsuspendTeamH (_ ::: tid) = do empty <$ unsuspendTeam tid unsuspendTeam :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => TeamId -> Handler r () unsuspendTeam tid = do @@ -571,7 +569,7 @@ unsuspendTeam tid = do -- Internal changeTeamAccountStatuses :: - Members '[Async, GalleyAccess, GundeckAccess] r => + Members '[GalleyAccess, GundeckAccess] r => TeamId -> AccountStatus -> Handler r () diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index 69351bd407..5363919f06 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -74,7 +74,6 @@ import qualified Network.Wai.Utilities.Response as WaiResp import Network.Wai.Utilities.Swagger (document) import qualified Network.Wai.Utilities.Swagger as Doc import Polysemy -import Polysemy.Async import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog (TinyLog) @@ -90,7 +89,6 @@ routesPublic :: Members '[ ActivationKeyStore, ActivationSupply, - Async, BlacklistStore, BudgetStore, Error Twilio.ErrorResponse, @@ -233,8 +231,7 @@ routesPublic = do routesInternal :: Members - '[ Async, - Error ReAuthError, + '[ Error ReAuthError, GalleyAccess, GundeckAccess, Input (Local ()), @@ -351,8 +348,7 @@ reAuthUser uid body = do loginH :: Members - '[ Async, - BudgetStore, + '[ BudgetStore, Error Twilio.ErrorResponse, GalleyAccess, GundeckAccess, @@ -372,8 +368,7 @@ loginH (req ::: persist ::: _) = do login :: Members - '[ Async, - BudgetStore, + '[ BudgetStore, Error Twilio.ErrorResponse, GalleyAccess, GundeckAccess, @@ -395,8 +390,7 @@ login l persist = do ssoLoginH :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserQuery @@ -409,8 +403,7 @@ ssoLoginH (req ::: persist ::: _) = do ssoLogin :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserQuery @@ -425,8 +418,7 @@ ssoLogin l persist = do legalHoldLoginH :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserQuery @@ -439,8 +431,7 @@ legalHoldLoginH (req ::: _) = do legalHoldLogin :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserQuery @@ -531,8 +522,7 @@ rmCookies uid (Public.RemoveCookies pw lls ids) = renewH :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, UserQuery ] @@ -548,8 +538,7 @@ renewH (_ ::: ut ::: at) = lift . either tokenResponse tokenResponse =<< renew u -- Other combinations of provided inputs will cause an error to be raised. renew :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, UserQuery ] diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 072adc7947..96a7578f57 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -82,7 +82,6 @@ import qualified Data.ZAuth.Token as ZAuth import Imports import Network.Wai.Utilities.Error ((!>>)) import Polysemy -import Polysemy.Async import Polysemy.Error import qualified Polysemy.Error as P import Polysemy.Input @@ -162,8 +161,7 @@ lookupLoginCode phone = login :: forall r. Members - '[ Async, - BudgetStore, + '[ BudgetStore, Error Twilio.ErrorResponse, GalleyAccess, GundeckAccess, @@ -326,8 +324,7 @@ renewAccess :: forall u a r. ZAuth.TokenPair u a => Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, UserQuery ] @@ -365,8 +362,7 @@ revokeAccess u pw cc ll = do catchSuspendInactiveUser :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, UserQuery ] @@ -394,8 +390,7 @@ newAccess :: forall u a r. ZAuth.TokenPair u a => Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, UserQuery ] @@ -539,8 +534,7 @@ validateToken ut at = do -- | Allow to login as any user without having the credentials. ssoLogin :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserQuery @@ -571,8 +565,7 @@ ssoLogin (SsoLogin uid label) typ = do -- | Log in as a LegalHold service, getting LegalHoldUser/Access Tokens. legalHoldLogin :: Members - '[ Async, - GalleyAccess, + '[ GalleyAccess, GundeckAccess, Input (Local ()), UserQuery From eef87c28c26a70df91368002f6ece2e4c2dfca40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 20 Sep 2022 11:56:00 +0200 Subject: [PATCH 32/41] Replace List1 with NonEmpty --- services/brig/src/Brig/API/User.hs | 15 +++------------ services/brig/src/Brig/Team/API.hs | 15 ++++++++++----- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 99c119b4e7..896643a423 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -163,7 +163,7 @@ import Data.Id as Id import Data.Json.Util import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) import Data.List.Extra -import Data.List1 as List1 (List1, singleton) +import Data.List.NonEmpty (NonEmpty) import qualified Data.Map.Strict as Map import qualified Data.Metrics as Metrics import Data.Misc (PlainTextPassword (..)) @@ -1023,16 +1023,7 @@ changeAccountStatus :: GundeckAccess ] r => - -- ( MonadClient m, - -- MonadLogger m, - -- MonadIndexIO m, - -- MonadReader Env m, - -- MonadMask m, - -- MonadHttp m, - -- HasRequestId m, - -- MonadUnliftIO m - -- ) => - List1 UserId -> + NonEmpty UserId -> AccountStatus -> ExceptT AccountStatusError (AppT r) () changeAccountStatus usrs status = do @@ -1062,7 +1053,7 @@ changeSingleAccountStatus :: ExceptT AccountStatusError (AppT r) () changeSingleAccountStatus uid status = do unlessM (lift . liftSem $ Data.userExists uid) $ throwE AccountNotFound - ev <- wrapClientE $ mkUserEvent (singleton uid) status + ev <- wrapClientE $ mkUserEvent [uid] status wrapClientE $ Data.updateStatus uid status lift $ Intra.onUserEvent uid Nothing (ev uid) diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 5164f3c2c6..a57243e77b 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -53,7 +53,7 @@ import Control.Monad.Trans.Except (mapExceptT) import Data.Aeson hiding (json) import Data.ByteString.Conversion import Data.Id -import qualified Data.List1 as List1 +import Data.List.NonEmpty (nonEmpty) import Data.Qualified import Data.Range import Data.String.Conversions (cs) @@ -577,8 +577,13 @@ changeTeamAccountStatuses tid s = do team <- Team.tdTeam <$> lift (wrapHttp $ Intra.getTeam tid) unless (team ^. teamBinding == Binding) $ throwStd noBindingTeam - uids <- toList1 =<< lift (fmap (view Teams.userId) . view teamMembers <$> wrapHttp (Intra.getTeamMembers tid)) + uids <- do + r <- + lift + ( fmap (view Teams.userId) . view teamMembers + <$> wrapHttp (Intra.getTeamMembers tid) + ) + case nonEmpty r of + Nothing -> throwStd (notFound "Team not found or no members") + Just v -> pure v API.changeAccountStatus uids s !>> accountStatusError - where - toList1 (x : xs) = pure $ List1.list1 x xs - toList1 [] = throwStd (notFound "Team not found or no members") From 14284184d164fa6d6402d001296b96c8371791f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 20 Sep 2022 13:06:43 +0200 Subject: [PATCH 33/41] Remove some in-line effect interpretations --- services/brig/src/Brig/API/Connection.hs | 25 ++++- services/brig/src/Brig/API/Federation.hs | 45 ++++---- services/brig/src/Brig/API/Public.hs | 51 +++++++-- services/brig/src/Brig/API/User.hs | 126 +++++++--------------- services/brig/src/Brig/User/API/Handle.hs | 18 +++- services/brig/src/Brig/User/API/Search.hs | 17 ++- 6 files changed, 157 insertions(+), 125 deletions(-) diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index 2b32d61d87..555280b31a 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -54,6 +54,7 @@ import Data.Range import qualified Data.UUID.V4 as UUID import Imports import Polysemy +import Polysemy.Input import qualified System.Logger.Class as Log import System.Logger.Message import Wire.API.Connection hiding (relationWithHistory) @@ -78,7 +79,12 @@ ensureNotSameTeam self target = do throwE ConnectSameBindingTeamUsers createConnection :: - Members '[GundeckAccess, UserQuery] r => + Members + '[ Input (Local ()), + GundeckAccess, + UserQuery + ] + r => Local UserId -> ConnId -> Qualified UserId -> @@ -99,7 +105,12 @@ createConnection self con target = do createConnectionToLocalUser :: forall r. - Members '[GundeckAccess, UserQuery] r => + Members + '[ Input (Local ()), + GundeckAccess, + UserQuery + ] + r => Local UserId -> ConnId -> Local UserId -> @@ -187,15 +198,19 @@ createConnectionToLocalUser self conn target = do -- -- FUTUREWORK: we may want to move this to the LH application logic, so we can recycle it for -- group conv creation and possibly other situations. -checkLegalholdPolicyConflict :: UserId -> UserId -> ExceptT ConnectionError (AppT r) () +checkLegalholdPolicyConflict :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + UserId -> + ExceptT ConnectionError (AppT r) () checkLegalholdPolicyConflict uid1 uid2 = do let catchProfileNotFound = -- Does not fit into 'ExceptT', so throw in '(AppT r)'. Anyway at the time of writing -- this, users are guaranteed to exist when called from 'createConnectionToLocalUser'. maybe (throwM (errorToWai @'E.UserNotFound)) pure - status1 <- lift (wrapHttpClient $ getLegalHoldStatus uid1) >>= catchProfileNotFound - status2 <- lift (wrapHttpClient $ getLegalHoldStatus uid2) >>= catchProfileNotFound + status1 <- lift (getLegalHoldStatus uid1) >>= catchProfileNotFound + status2 <- lift (getLegalHoldStatus uid2) >>= catchProfileNotFound let oneway s1 s2 = case (s1, s2) of (LH.UserLegalHoldNoConsent, LH.UserLegalHoldNoConsent) -> pure () diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index cdcabde640..c856e9e9f0 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -19,8 +19,6 @@ module Brig.API.Federation (federationSitemap, FederationAPI) where -import Bilge.IO -import Bilge.RPC import qualified Brig.API.Client as API import Brig.API.Connection.Remote (performRemoteAction) import Brig.API.Error @@ -39,9 +37,7 @@ import Brig.IO.Intra (notify) import Brig.Types.User.Event import Brig.User.API.Handle import qualified Brig.User.Search.SearchIndex as Q -import Cassandra (MonadClient) import Control.Error.Util -import Control.Monad.Catch (MonadMask) import Control.Monad.Trans.Except import Data.Domain import Data.Handle (Handle (..), parseHandle) @@ -53,9 +49,9 @@ import qualified Gundeck.Types.Push as Push import Imports import Network.Wai.Utilities.Error ((!>>)) import Polysemy +import Polysemy.Input import Servant (ServerT) import Servant.API -import qualified System.Logger.Class as Log import Wire.API.Connection import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Common @@ -75,6 +71,7 @@ type FederationAPI = "federation" :> BrigApi federationSitemap :: Members '[ GundeckAccess, + Input (Local ()), UserHandleStore, UserQuery ] @@ -83,7 +80,7 @@ federationSitemap :: federationSitemap = Named @"api-version" (\_ _ -> pure versionInfo) :<|> Named @"get-user-by-handle" getUserByHandle - :<|> Named @"get-users-by-ids" (\d us -> wrapHttpClientE $ getUsersByIds d us) + :<|> Named @"get-users-by-ids" getUsersByIds :<|> Named @"claim-prekey" claimPrekey :<|> Named @"claim-prekey-bundle" claimPrekeyBundle :<|> Named @"claim-multi-prekey-bundle" claimMultiPrekeyBundle @@ -111,7 +108,12 @@ sendConnectionAction originDomain NewConnectionRequest {..} = do else pure NewConnectionResponseUserNotActivated getUserByHandle :: - Members '[UserHandleStore] r => + Members + '[ Input (Local ()), + UserHandleStore, + UserQuery + ] + r => Domain -> Handle -> ExceptT Error (AppT r) (Maybe UserProfile) @@ -131,21 +133,15 @@ getUserByHandle domain handle = do Nothing -> pure Nothing Just ownerId -> - listToMaybe <$> wrapHttpClient (API.lookupLocalProfiles Nothing [ownerId]) + listToMaybe <$> API.lookupLocalProfiles Nothing [ownerId] getUsersByIds :: - ( MonadClient m, - MonadReader Env m, - Log.MonadLogger m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => + Members '[Input (Local ()), UserQuery] r => Domain -> [UserId] -> - ExceptT Error m [UserProfile] + ExceptT Error (AppT r) [UserProfile] getUsersByIds _ uids = - lift (API.lookupLocalProfiles Nothing uids) + lift $ API.lookupLocalProfiles Nothing uids claimPrekey :: Domain -> (UserId, ClientId) -> (Handler r) (Maybe ClientPrekey) claimPrekey _ (user, client) = do @@ -170,7 +166,12 @@ fedClaimKeyPackages domain ckpr = do -- (This decision may change in the future) searchUsers :: forall r. - Members '[UserHandleStore] r => + Members + '[ Input (Local ()), + UserHandleStore, + UserQuery + ] + r => Domain -> SearchRequest -> ExceptT Error (AppT r) SearchResponse @@ -187,7 +188,11 @@ searchUsers domain (SearchRequest searchTerm) = do contacts <- go [] maxResults searches pure $ SearchResponse contacts searchPolicy where - go :: [Contact] -> Int -> [Int -> ExceptT Error (AppT r) [Contact]] -> ExceptT Error (AppT r) [Contact] + go :: + [Contact] -> + Int -> + [Int -> ExceptT Error (AppT r) [Contact]] -> + ExceptT Error (AppT r) [Contact] go contacts _ [] = pure contacts go contacts maxResult (search : searches) = do contactsNew <- search maxResult @@ -205,7 +210,7 @@ searchUsers domain (SearchRequest searchTerm) = do maybeOwnerId <- maybe (pure Nothing) (lift . liftSem . API.lookupHandle) maybeHandle case maybeOwnerId of Nothing -> pure [] - Just foundUser -> lift $ contactFromProfile <$$> wrapHttpClient (API.lookupLocalProfiles Nothing [foundUser]) + Just foundUser -> lift $ contactFromProfile <$$> API.lookupLocalProfiles Nothing [foundUser] | otherwise = pure [] getUserClients :: Domain -> GetUserClients -> (Handler r) (UserMap (Set PubClient)) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 70f9b6e7bc..019ef712c5 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -846,19 +846,32 @@ getSelf self = lift (API.lookupSelfProfile self) >>= ifNothing (errorToWai @'E.UserNotFound) -getUserUnqualifiedH :: UserId -> UserId -> (Handler r) (Maybe Public.UserProfile) +getUserUnqualifiedH :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + UserId -> + Handler r (Maybe Public.UserProfile) getUserUnqualifiedH self uid = do domain <- viewFederationDomain getUser self (Qualified uid domain) -getUser :: UserId -> Qualified UserId -> (Handler r) (Maybe Public.UserProfile) +getUser :: + Members '[Input (Local ()), UserQuery] r => + UserId -> + Qualified UserId -> + Handler r (Maybe Public.UserProfile) getUser self qualifiedUserId = do lself <- qualifyLocal self - wrapHttpClientE $ API.lookupProfile lself qualifiedUserId !>> fedError + API.lookupProfile lself qualifiedUserId !>> fedError -- FUTUREWORK: Make servant understand that at least one of these is required listUsersByUnqualifiedIdsOrHandles :: - Members '[UserHandleStore] r => + Members + '[ Input (Local ()), + UserHandleStore, + UserQuery + ] + r => UserId -> Maybe (CommaSeparatedList UserId) -> Maybe (Range 1 4 (CommaSeparatedList Handle)) -> @@ -880,7 +893,12 @@ listUsersByUnqualifiedIdsOrHandles self mUids mHandles = do listUsersByIdsOrHandles :: forall r. - Members '[UserHandleStore] r => + Members + '[ Input (Local ()), + UserHandleStore, + UserQuery + ] + r => UserId -> Public.ListUsersQuery -> Handler r [Public.UserProfile] @@ -903,7 +921,7 @@ listUsersByIdsOrHandles self q = do domain <- viewFederationDomain pure $ map (`Qualified` domain) localUsers byIds :: Local UserId -> [Qualified UserId] -> (Handler r) [Public.UserProfile] - byIds lself uids = wrapHttpClientE (API.lookupProfiles lself uids) !>> fedError + byIds lself uids = API.lookupProfiles lself uids !>> fedError newtype GetActivationCodeResp = GetActivationCodeResp (Public.ActivationKey, Public.ActivationCode) @@ -1026,7 +1044,12 @@ checkHandles _ (Public.CheckHandles hs num) = do -- 'Handle.getHandleInfo') returns UserProfile to reduce traffic between backends -- in a federated scenario. getHandleInfoUnqualifiedH :: - Members '[UserHandleStore] r => + Members + '[ Input (Local ()), + UserHandleStore, + UserQuery + ] + r => UserId -> Handle -> Handler r (Maybe Public.UserHandleInfo) @@ -1155,7 +1178,12 @@ customerExtensionCheckBlockedDomains email = do throwM $ customerExtensionBlockedDomain domain createConnectionUnqualified :: - Members '[GundeckAccess, UserQuery] r => + Members + '[ GundeckAccess, + Input (Local ()), + UserQuery + ] + r => UserId -> ConnId -> Public.ConnectionRequest -> @@ -1166,7 +1194,12 @@ createConnectionUnqualified self conn cr = do API.createConnection lself conn (qUntagged target) !>> connError createConnection :: - Members '[GundeckAccess, UserQuery] r => + Members + '[ GundeckAccess, + Input (Local ()), + UserQuery + ] + r => UserId -> ConnId -> Qualified UserId -> diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 896643a423..6d68d08f50 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -90,8 +90,7 @@ module Brig.API.User ) where -import Bilge.IO (Manager, MonadHttp) -import Bilge.RPC (HasRequestId) +import Bilge.IO (Manager) import qualified Brig.API.Error as Error import qualified Brig.API.Handler as API (Handler, UserNotAllowedToJoinTeam (..)) import Brig.API.Types @@ -127,7 +126,6 @@ import Brig.Effects.UserKeyStore (UserKeyStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) import qualified Brig.Effects.UserPendingActivationStore as UserPendingActivationStore import Brig.Effects.UserQuery (UserQuery) -import Brig.Effects.UserQuery.Cassandra import Brig.Effects.VerificationCodeStore import qualified Brig.Federation.Client as Federation import qualified Brig.IO.Intra as Intra @@ -1588,18 +1586,6 @@ deleteAccount :: UserQuery ] r => - -- ( MonadLogger m, - -- MonadCatch m, - -- -- MonadThrow m, - -- MonadIndexIO m, - -- MonadReader Env m, - -- MonadIO m, - -- MonadMask m, - -- MonadHttp m, - -- HasRequestId m, - -- -- MonadUnliftIO m, - -- MonadClient m - -- ) => UserAccount -> AppT r () deleteAccount account@(accountUser -> user) = do @@ -1711,16 +1697,10 @@ userGC u = case userExpire u of pure u lookupProfile :: - ( MonadClient m, - MonadReader Env m, - MonadLogger m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => + Members '[Input (Local ()), UserQuery] r => Local UserId -> Qualified UserId -> - ExceptT FederationError m (Maybe UserProfile) + ExceptT FederationError (AppT r) (Maybe UserProfile) lookupProfile self other = listToMaybe <$> lookupProfilesFromDomain @@ -1733,36 +1713,37 @@ lookupProfile self other = -- Otherwise only the 'PublicProfile' is accessible for user 'self'. -- If 'self' is an unknown 'UserId', return '[]'. lookupProfiles :: - ( MonadUnliftIO m, - MonadClient m, - MonadReader Env m, - MonadLogger m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => + Members '[Input (Local ()), UserQuery] r => + -- ( MonadUnliftIO m, + -- MonadClient m, + -- MonadReader Env m, + -- MonadLogger m, + -- MonadMask m, + -- MonadHttp m, + -- HasRequestId m + -- ) => + -- | User 'self' on whose behalf the profiles are requested. Local UserId -> -- | The users ('others') for which to obtain the profiles. [Qualified UserId] -> - ExceptT FederationError m [UserProfile] + ExceptT FederationError (AppT r) [UserProfile] lookupProfiles self others = concat - <$> traverseConcurrentlyWithErrors + <$> + -- <$> traverseConcurrentlyWithErrors + -- (lookupProfilesFromDomain self) + -- (bucketQualified others) + -- TODO(md): Use an effect with error handling for this + traverse (lookupProfilesFromDomain self) (bucketQualified others) lookupProfilesFromDomain :: - ( MonadClient m, - MonadReader Env m, - MonadLogger m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => + Members '[Input (Local ()), UserQuery] r => Local UserId -> Qualified [UserId] -> - ExceptT FederationError m [UserProfile] + ExceptT FederationError (AppT r) [UserProfile] lookupProfilesFromDomain self = foldQualified self @@ -1770,12 +1751,8 @@ lookupProfilesFromDomain self = lookupRemoteProfiles lookupRemoteProfiles :: - ( MonadIO m, - MonadReader Env m, - MonadLogger m - ) => Remote [UserId] -> - ExceptT FederationError m [UserProfile] + ExceptT FederationError (AppT r) [UserProfile] lookupRemoteProfiles (qUntagged -> Qualified uids domain) = Federation.getUsersByIds domain uids @@ -1783,30 +1760,23 @@ lookupRemoteProfiles (qUntagged -> Qualified uids domain) = -- ids, but it is also very complex. Maybe this can be made easy by extracting a -- pure function and writing tests for that. lookupLocalProfiles :: - forall m. - ( MonadClient m, - MonadReader Env m, - MonadLogger m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => + forall r. + Members '[Input (Local ()), UserQuery] r => -- | This is present only when an authenticated user is requesting access. Maybe UserId -> -- | The users ('others') for which to obtain the profiles. [UserId] -> - m [UserProfile] + AppT r [UserProfile] lookupLocalProfiles requestingUser others = do loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain locale <- setDefaultUserLocale <$> view settings users <- - runM - ( userQueryToCassandra @m @'[Embed m] - (Data.lookupUsers loc locale NoPendingInvitations others) - ) + liftSem (Data.lookupUsers loc locale NoPendingInvitations others) >>= mapM userGC css <- case requestingUser of - Just localReqUser -> toMap <$> Data.lookupConnectionStatus (map userId users) [localReqUser] + Just localReqUser -> + toMap + <$> wrapClient (Data.lookupConnectionStatus (map userId users) [localReqUser]) Nothing -> pure mempty emailVisibility' <- view (settings . emailVisibility) emailVisibility'' <- case emailVisibility' of @@ -1821,19 +1791,16 @@ lookupLocalProfiles requestingUser others = do toMap :: [ConnectionStatus] -> Map UserId Relation toMap = Map.fromList . map (csFrom &&& csStatus) - getSelfInfo :: Local x -> Locale -> UserId -> m (Maybe (TeamId, TeamMember)) + getSelfInfo :: Local x -> Locale -> UserId -> AppT r (Maybe (TeamId, TeamMember)) getSelfInfo loc locale selfId = do -- FUTUREWORK: it is an internal error for the two lookups (for 'User' and 'TeamMember') -- to return 'Nothing'. we could throw errors here if that happens, rather than just -- returning an empty profile list from 'lookupProfiles'. mUser <- - runM - ( userQueryToCassandra @m @'[Embed m] - (Data.lookupUser loc locale NoPendingInvitations selfId) - ) + liftSem (Data.lookupUser loc locale NoPendingInvitations selfId) case userTeam =<< mUser of Nothing -> pure Nothing - Just tid -> (tid,) <$$> Intra.getTeamMember selfId tid + Just tid -> (tid,) <$$> wrapHttp (Intra.getTeamMember selfId tid) toProfile :: EmailVisibility' -> Map UserId Relation -> (User, UserLegalHoldStatus) -> UserProfile toProfile emailVisibility'' css (u, userLegalHold) = @@ -1846,37 +1813,24 @@ lookupLocalProfiles requestingUser others = do in baseProfile {profileEmail = profileEmail'} getLegalHoldStatus :: - forall m. - ( MonadLogger m, - MonadReader Env m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadClient m - ) => + Members '[Input (Local ()), UserQuery] r => UserId -> - m (Maybe UserLegalHoldStatus) + AppT r (Maybe UserLegalHoldStatus) getLegalHoldStatus uid = do locale <- setDefaultUserLocale <$> view settings - locDomain <- qualifyLocal () traverse (getLegalHoldStatus' . accountUser) - =<< (runM . userQueryToCassandra @m @'[Embed m] . runInputConst locDomain $ lookupAccount locale uid) + =<< liftSem (lookupAccount locale uid) getLegalHoldStatus' :: - ( MonadLogger m, - MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => + forall r. + Members '[Input (Local ()), UserQuery] r => User -> - m UserLegalHoldStatus + AppT r UserLegalHoldStatus getLegalHoldStatus' user = case userTeam user of Nothing -> pure defUserLegalHoldStatus Just tid -> do - teamMember <- Intra.getTeamMember (userId user) tid + teamMember <- wrapHttp $ Intra.getTeamMember (userId user) tid pure $ maybe defUserLegalHoldStatus (^. legalHoldStatus) teamMember data EmailVisibility' diff --git a/services/brig/src/Brig/User/API/Handle.hs b/services/brig/src/Brig/User/API/Handle.hs index 3c170928af..0790141b71 100644 --- a/services/brig/src/Brig/User/API/Handle.hs +++ b/services/brig/src/Brig/User/API/Handle.hs @@ -29,6 +29,7 @@ import qualified Brig.API.User as API import Brig.App import qualified Brig.Data.User as Data import Brig.Effects.UserHandleStore +import Brig.Effects.UserQuery import qualified Brig.Federation.Client as Federation import Brig.Options (searchSameTeamOnly) import Control.Lens (view) @@ -38,6 +39,7 @@ import Data.Qualified import Imports import Network.Wai.Utilities ((!>>)) import Polysemy +import Polysemy.Input import qualified System.Logger.Class as Log import Wire.API.User import qualified Wire.API.User as Public @@ -45,7 +47,12 @@ import Wire.API.User.Search import qualified Wire.API.User.Search as Public getHandleInfo :: - Member UserHandleStore r => + Members + '[ Input (Local ()), + UserHandleStore, + UserQuery + ] + r => UserId -> Qualified Handle -> Handler r (Maybe Public.UserProfile) @@ -65,7 +72,12 @@ getRemoteHandleInfo handle = do Federation.getUserHandleInfo handle !>> fedError getLocalHandleInfo :: - Member UserHandleStore r => + Members + '[ Input (Local ()), + UserHandleStore, + UserQuery + ] + r => Local UserId -> Handle -> Handler r (Maybe Public.UserProfile) @@ -76,7 +88,7 @@ getLocalHandleInfo self handle = do Nothing -> pure Nothing Just ownerId -> do domain <- viewFederationDomain - ownerProfile <- wrapHttpClientE (API.lookupProfile self (Qualified ownerId domain)) !>> fedError + ownerProfile <- API.lookupProfile self (Qualified ownerId domain) !>> fedError owner <- filterHandleResults self (maybeToList ownerProfile) pure $ listToMaybe owner diff --git a/services/brig/src/Brig/User/API/Search.hs b/services/brig/src/Brig/User/API/Search.hs index ddc096bab3..c083bc6a5e 100644 --- a/services/brig/src/Brig/User/API/Search.hs +++ b/services/brig/src/Brig/User/API/Search.hs @@ -27,6 +27,7 @@ import Brig.API.Handler import Brig.App import qualified Brig.Data.User as DB import Brig.Effects.UserHandleStore +import Brig.Effects.UserQuery import qualified Brig.Federation.Client as Federation import qualified Brig.IO.Intra as Intra import qualified Brig.Options as Opts @@ -41,12 +42,14 @@ import Data.Domain (Domain) import Data.Handle (parseHandle) import Data.Id import Data.Predicate +import Data.Qualified import Data.Range import Imports import Network.Wai.Routing import Network.Wai.Utilities ((!>>)) import Network.Wai.Utilities.Response (empty) import Polysemy +import Polysemy.Input import System.Logger (field, msg) import System.Logger.Class (val, (~~)) import qualified System.Logger.Class as Log @@ -85,7 +88,12 @@ routesInternal = do -- FUTUREWORK: Consider augmenting 'SearchResult' with full user profiles -- for all results. This is tracked in https://wearezeta.atlassian.net/browse/SQCORE-599 search :: - Members '[UserHandleStore] r => + Members + '[ Input (Local ()), + UserHandleStore, + UserQuery + ] + r => UserId -> Text -> Maybe Domain -> @@ -118,7 +126,12 @@ searchRemotely domain searchTerm = do searchLocally :: forall r. - Members '[UserHandleStore] r => + Members + '[ Input (Local ()), + UserHandleStore, + UserQuery + ] + r => UserId -> Text -> Maybe (Range 1 500 Int32) -> From b46217593ae927e227f10811b0478f3c78728e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 20 Sep 2022 15:31:11 +0200 Subject: [PATCH 34/41] Improve the interface for a Twilio effect action --- services/brig/src/Brig/API/Public.hs | 4 +-- services/brig/src/Brig/API/User.hs | 33 +++++++-------------- services/brig/src/Brig/Effects/Twilio.hs | 3 -- services/brig/src/Brig/Effects/Twilio/IO.hs | 12 +++++--- services/brig/src/Brig/Phone.hs | 6 ++-- services/brig/src/Brig/Team/API.hs | 4 +-- services/brig/src/Brig/User/Auth.hs | 29 ++++++------------ 7 files changed, 31 insertions(+), 60 deletions(-) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 019ef712c5..7df675e565 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1437,9 +1437,7 @@ activate :: Handler r ActivationRespWithStatus activate (Public.Activate tgt code dryrun) | dryrun = do - tc <- view twilioCreds - m <- view httpManager - liftSemE (API.preverify tgt code tc m) !>> actError + liftSemE (API.preverify tgt code) !>> actError pure ActivationRespDryRun | otherwise = do result <- API.activate tgt code Nothing !>> actError diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 6d68d08f50..2b129dcfa3 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -90,7 +90,6 @@ module Brig.API.User ) where -import Bilge.IO (Manager) import qualified Brig.API.Error as Error import qualified Brig.API.Handler as API (Handler, UserNotAllowedToJoinTeam (..)) import Brig.API.Types @@ -449,14 +448,12 @@ createUser new = do pure (validateEmail e) - c <- view twilioCreds - m <- view httpManager -- Validate phone phone <- for (newUserPhone newUser) $ \p -> maybe (throwE RegisterErrorInvalidPhone) pure - =<< lift (liftSem $ validatePhone c m p) + =<< lift (liftSem $ validatePhone p) for_ (catMaybes [userEmailKey <$> email, userPhoneKey <$> phone]) $ \k -> verifyUniquenessAndCheckBlacklist k !>> identityErrorToRegisterError @@ -887,13 +884,11 @@ changePhone :: Phone -> ExceptT ChangePhoneError (AppT r) (Activation, Phone) changePhone u phone = do - c <- view twilioCreds - m <- view httpManager canonical <- maybe (throwE InvalidNewPhone) pure - =<< lift (liftSem $ validatePhone c m phone) + =<< lift (liftSem $ validatePhone phone) let pk = userPhoneKey canonical available <- lift . liftSem $ Data.keyAvailable pk (Just u) unless available $ @@ -1121,9 +1116,7 @@ activateWithCurrency :: Maybe Currency.Alpha -> ExceptT ActivationError (AppT r) ActivationResult activateWithCurrency tgt code usr cur = do - tc <- view twilioCreds - m <- view httpManager - key <- liftSemE $ mkActivationKey tgt tc m + key <- liftSemE $ mkActivationKey tgt lift . Log.info $ field "activation.key" (toByteString key) . field "activation.code" (toByteString code) @@ -1154,11 +1147,9 @@ preverify :: r => ActivationTarget -> ActivationCode -> - Twilio.Credentials -> - Manager -> ExceptT ActivationError (Sem r) () -preverify tgt code creds m = do - key <- mkActivationKey tgt creds m +preverify tgt code = do + key <- mkActivationKey tgt void $ Data.verifyCode key code onActivated :: @@ -1224,13 +1215,11 @@ sendActivationCode emailOrPhone loc call = do Just (Just uid, c) -> sendActivationEmail timeout ek c uid -- User re-requesting activation Right phone -> do -- validatePhone returns the canonical E.164 phone number format - creds <- view twilioCreds - m <- view httpManager canonical <- maybe (throwE $ InvalidRecipient (userPhoneKey phone)) pure - =<< lift (liftSem $ validatePhone creds m phone) + =<< lift (liftSem $ validatePhone phone) let pk = userPhoneKey canonical exists <- lift $ isJust <$> liftSem (Data.getKey pk) when exists $ @@ -1297,23 +1286,21 @@ mkActivationKey :: ] r => ActivationTarget -> - Twilio.Credentials -> - Manager -> ExceptT ActivationError (Sem r) ActivationKey -mkActivationKey (ActivateKey k) _c _m = pure k -mkActivationKey (ActivateEmail e) _c _m = do +mkActivationKey (ActivateKey k) = pure k +mkActivationKey (ActivateEmail e) = do ek <- either (throwE . InvalidActivationEmail e) (pure . userEmailKey) (validateEmail e) lift $ Data.makeActivationKey ek -mkActivationKey (ActivatePhone p) c m = do +mkActivationKey (ActivatePhone p) = do pk <- maybe (throwE $ InvalidActivationPhone p) (pure . userPhoneKey) - =<< lift (validatePhone c m p) + =<< lift (validatePhone p) lift $ Data.makeActivationKey pk ------------------------------------------------------------------------------- diff --git a/services/brig/src/Brig/Effects/Twilio.hs b/services/brig/src/Brig/Effects/Twilio.hs index f570db201e..96244c5eb2 100644 --- a/services/brig/src/Brig/Effects/Twilio.hs +++ b/services/brig/src/Brig/Effects/Twilio.hs @@ -21,14 +21,11 @@ module Brig.Effects.Twilio where import Data.ISO3166_CountryCodes import Imports -import Network.HTTP.Client import Polysemy import Ropes.Twilio data Twilio m a where LookupPhone :: - Credentials -> - Manager -> Text -> LookupDetail -> Maybe CountryCode -> diff --git a/services/brig/src/Brig/Effects/Twilio/IO.hs b/services/brig/src/Brig/Effects/Twilio/IO.hs index 58506f60eb..af79d53e48 100644 --- a/services/brig/src/Brig/Effects/Twilio/IO.hs +++ b/services/brig/src/Brig/Effects/Twilio/IO.hs @@ -18,7 +18,9 @@ module Brig.Effects.Twilio.IO (twilioToIO) where import Bilge.Retry +import Brig.App import Brig.Effects.Twilio +import Control.Lens (view) import Control.Monad.Catch import Control.Retry import Imports @@ -26,14 +28,16 @@ import Polysemy import qualified Ropes.Twilio as Ropes twilioToIO :: - forall r a. - Member (Embed IO) r => + forall m r a. + (Member (Embed m) r, MonadReader Env m, MonadIO m) => Sem (Twilio ': r) a -> Sem r a twilioToIO = interpret $ - embed @IO . \case - LookupPhone cred m txt detail code -> + embed @m . \case + LookupPhone txt detail code -> do + cred <- view twilioCreds + m <- view httpManager liftIO . try @_ @Ropes.ErrorResponse $ recovering x3 httpHandlers $ const $ Ropes.lookupPhone cred m txt detail code diff --git a/services/brig/src/Brig/Phone.hs b/services/brig/src/Brig/Phone.hs index 60afb09d35..3a1cf18e8e 100644 --- a/services/brig/src/Brig/Phone.hs +++ b/services/brig/src/Brig/Phone.hs @@ -208,14 +208,12 @@ sendSms loc SMSMessage {..} = unless (isTestPhone smsTo) $ do -- E.164 format of the given phone number on success. validatePhone :: Members '[P.Error Twilio.ErrorResponse, Twilio] r => - Twilio.Credentials -> - Manager -> Phone -> Sem r (Maybe Phone) -validatePhone c m (Phone p) +validatePhone (Phone p) | isTestPhone p = pure (Just (Phone p)) | otherwise = do - lookupPhone c m p LookupNoDetail Nothing >>= \case + lookupPhone p LookupNoDetail Nothing >>= \case Right x -> pure (Just (Phone (Twilio.lookupE164 x))) Left e | Twilio.errStatus e == 404 -> pure Nothing Left e -> P.throw e diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index a57243e77b..110e287ac8 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -432,10 +432,8 @@ createInvitation' tid inviteeRole mbInviterUid fromEmail body = do -- Validate phone inviteePhone <- for (irInviteePhone body) $ \p -> do - c <- view twilioCreds - m <- view httpManager validatedPhone <- - maybe (throwStd (errorToWai @'E.InvalidPhone)) pure =<< lift (liftSem $ Phone.validatePhone c m p) + maybe (throwStd (errorToWai @'E.InvalidPhone)) pure =<< lift (liftSem $ Phone.validatePhone p) let ukp = userPhoneKey validatedPhone blacklistedPh <- lift $ liftSem $ BlacklistStore.exists ukp when blacklistedPh $ diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 96a7578f57..13fca448d2 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -36,7 +36,6 @@ module Brig.User.Auth ) where -import Bilge.IO import Brig.API.Types import Brig.API.User (changeSingleAccountStatus) import Brig.App @@ -115,13 +114,11 @@ sendLoginCode :: Bool -> ExceptT SendLoginCodeError (AppT r) PendingLoginCode sendLoginCode phone call force = do - creds <- view twilioCreds - m <- view httpManager pk <- maybe (throwE $ SendLoginInvalidPhone phone) (pure . userPhoneKey) - =<< lift (liftSem $ validatePhone creds m phone) + =<< lift (liftSem $ validatePhone phone) user <- lift . liftSem $ Data.getKey pk case user of Nothing -> throwE $ SendLoginInvalidPhone phone @@ -178,9 +175,7 @@ login :: CookieType -> ExceptT LoginError (AppT r) (Access ZAuth.User) login (PasswordLogin li pw label code) typ = do - c <- view twilioCreds - man <- view httpManager - uid <- resolveLoginId c man li + uid <- resolveLoginId li lift . liftSem . P.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.login") mLimitFailedLogins <- view (settings . to Opt.setLimitFailedLogins) liftSemE . semErrToExceptT $ checkRetryLimit uid mLimitFailedLogins @@ -206,9 +201,7 @@ login (PasswordLogin li pw label code) typ = do VerificationCodeRequired -> liftSemE . semErrToExceptT $ loginFailedWith LoginCodeRequired uid mLimitFailedLogins VerificationCodeNoEmail -> liftSemE . semErrToExceptT $ loginFailed uid mLimitFailedLogins login (SmsLogin phone code label) typ = do - c <- view twilioCreds - man <- view httpManager - uid <- resolveLoginId c man (LoginByPhone phone) + uid <- resolveLoginId (LoginByPhone phone) lift . Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.login") mLimitFailedLogins <- view (settings . to Opt.setLimitFailedLogins) e <- lift . liftSem . runError $ checkRetryLimit uid mLimitFailedLogins @@ -416,13 +409,11 @@ resolveLoginId :: Twilio ] r => - Twilio.Credentials -> - Manager -> LoginId -> ExceptT LoginError (AppT r) UserId -resolveLoginId c m li = do +resolveLoginId li = do usr <- - liftSemE (validateLoginId c m li) + liftSemE (validateLoginId li) >>= lift . either (liftSem . getKey) @@ -438,21 +429,19 @@ resolveLoginId c m li = do validateLoginId :: Members '[P.Error Twilio.ErrorResponse, Twilio] r => - Twilio.Credentials -> - Manager -> LoginId -> ExceptT LoginError (Sem r) (Either UserKey Handle) -validateLoginId _ _ (LoginByEmail email) = +validateLoginId (LoginByEmail email) = either (const $ throwE LoginFailed) (pure . Left . userEmailKey) (validateEmail email) -validateLoginId c m (LoginByPhone phone) = +validateLoginId (LoginByPhone phone) = maybe (throwE LoginFailed) (pure . Left . userPhoneKey) - =<< lift (validatePhone c m phone) -validateLoginId _ _ (LoginByHandle h) = + =<< lift (validatePhone phone) +validateLoginId (LoginByHandle h) = pure (Right h) isPendingActivation :: forall m. (MonadClient m, MonadReader Env m) => LoginId -> m Bool From d29eefdf6520a4cb4abf69f20eac28eb0817912f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 20 Sep 2022 15:36:50 +0200 Subject: [PATCH 35/41] Linting of BudgetStore --- services/brig/src/Brig/Effects/BudgetStore.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/brig/src/Brig/Effects/BudgetStore.hs b/services/brig/src/Brig/Effects/BudgetStore.hs index e56cd2d84b..a15600e5cf 100644 --- a/services/brig/src/Brig/Effects/BudgetStore.hs +++ b/services/brig/src/Brig/Effects/BudgetStore.hs @@ -67,18 +67,18 @@ withBudget k b ma = do Budget ttl val <- fromMaybe b <$> lookupBudget k let remaining = val - 1 if remaining < 0 - then return (BudgetExhausted ttl) + then pure (BudgetExhausted ttl) else do a <- ma insertBudget k (Budget ttl remaining) - return (BudgetedValue a remaining) + pure (BudgetedValue a remaining) -- | Like 'withBudget', but does not decrease budget, only takes a look. checkBudget :: Member BudgetStore r => BudgetKey -> Budget -> Sem r (Budgeted ()) checkBudget k b = do Budget ttl val <- fromMaybe b <$> lookupBudget k let remaining = val - 1 - return $ + pure $ if remaining < 0 then BudgetExhausted ttl else BudgetedValue () remaining From 0f694444f50a26741e31d1af9f6044e30326d1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Tue, 4 Oct 2022 11:07:33 +0200 Subject: [PATCH 36/41] Polysemise Brig.Provider.API.deleteBot - This commit doesn't compile, but only due to the -Werror flag. Use sites of deleteBot are commented out as they're wrapped in conduits. The next step is to handle that. --- services/brig/brig.cabal | 2 + services/brig/src/Brig/API/User.hs | 7 +- services/brig/src/Brig/Data/Client.hs | 91 +--------- services/brig/src/Brig/Data/User.hs | 37 +--- services/brig/src/Brig/Effects/ClientStore.hs | 30 ++++ .../src/Brig/Effects/ClientStore/Cassandra.hs | 161 ++++++++++++++++++ .../brig/src/Brig/Effects/GalleyAccess.hs | 8 + .../src/Brig/Effects/GalleyAccess/Http.hs | 21 +++ services/brig/src/Brig/Effects/UserQuery.hs | 8 + .../src/Brig/Effects/UserQuery/Cassandra.hs | 41 +++++ services/brig/src/Brig/Provider/API.hs | 129 +++++++++----- 11 files changed, 362 insertions(+), 173 deletions(-) create mode 100644 services/brig/src/Brig/Effects/ClientStore.hs create mode 100644 services/brig/src/Brig/Effects/ClientStore/Cassandra.hs diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 288dae5f3a..8db8d8066e 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -64,6 +64,8 @@ library Brig.Effects.BlacklistStore.Cassandra Brig.Effects.BudgetStore Brig.Effects.BudgetStore.Cassandra + Brig.Effects.ClientStore + Brig.Effects.ClientStore.Cassandra Brig.Effects.CodeStore Brig.Effects.CodeStore.Cassandra Brig.Effects.Common diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 2b129dcfa3..1eca0d1617 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -1013,7 +1013,8 @@ changeAccountStatus :: forall r. Members '[ GalleyAccess, - GundeckAccess + GundeckAccess, + UserQuery ] r => NonEmpty UserId -> @@ -1031,7 +1032,7 @@ changeAccountStatus usrs status = do UserId -> AppT r () update ev u = do - wrapClient $ Data.updateStatus u status + liftSem $ Data.updateStatus u status Intra.onUserEvent u Nothing (ev u) changeSingleAccountStatus :: @@ -1047,7 +1048,7 @@ changeSingleAccountStatus :: changeSingleAccountStatus uid status = do unlessM (lift . liftSem $ Data.userExists uid) $ throwE AccountNotFound ev <- wrapClientE $ mkUserEvent [uid] status - wrapClientE $ Data.updateStatus uid status + lift . liftSem $ Data.updateStatus uid status lift $ Intra.onUserEvent uid Nothing (ev uid) mkUserEvent :: diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 8e7f230166..f127cb2ed8 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -55,6 +55,7 @@ import Bilge.Retry (httpHandlers) import Brig.AWS import Brig.App import Brig.Data.Instances () +import Brig.Effects.ClientStore.Cassandra (key, lookupClients, rmClient, toClient) -- import Brig.Data.User (AuthError (..), ReAuthError (..)) -- import qualified Brig.Data.User as User -- import Brig.Options (setDefaultUserLocale) @@ -84,7 +85,6 @@ import Data.Misc import Data.Qualified import qualified Data.Set as Set import qualified Data.Text as Text -import qualified Data.UUID as UUID import Imports import Polysemy hiding (run) import Polysemy.Error @@ -229,20 +229,6 @@ lookupPubClientsBulk uids = liftClient $ do executeQuery :: MonadClient m => UserId -> m [(ClientId, Maybe ClientClass)] executeQuery u = retry x1 (query selectPubClients (params LocalQuorum (Identity u))) -lookupClients :: MonadClient m => UserId -> m [Client] -lookupClients u = do - keys <- - (\(cid, ss, Blob b) -> (cid, [(ss, LBS.toStrict b)])) - <$$> retry x1 (query selectMLSPublicKeysByUser (params LocalQuorum (Identity u))) - let keyMap = Map.fromListWith (<>) keys - updateKeys c = - c - { clientMLSPublicKeys = - Map.fromList $ Map.findWithDefault [] (clientId c) keyMap - } - updateKeys . toClient [] - <$$> retry x1 (query selectClients (params LocalQuorum (Identity u))) - lookupClientIds :: MonadClient m => UserId -> m [ClientId] lookupClientIds u = map runIdentity @@ -262,19 +248,6 @@ lookupPrekeyIds u c = hasClient :: MonadClient m => UserId -> ClientId -> m Bool hasClient u d = isJust <$> retry x1 (query1 checkClient (params LocalQuorum (u, d))) -rmClient :: - ( MonadClient m, - MonadReader Brig.App.Env m, - MonadCatch m - ) => - UserId -> - ClientId -> - m () -rmClient u c = do - retry x5 $ write removeClient (params LocalQuorum (u, c)) - retry x5 $ write removeClientKeys (params LocalQuorum (u, c)) - unlessM (isJust <$> view randomPrekeyLocalLock) $ deleteOptLock u c - updateClientLabel :: MonadClient m => UserId -> ClientId -> Maybe Text -> m () updateClientLabel u c l = retry x5 $ write updateClientLabelQuery (params LocalQuorum (l, u, c)) @@ -395,9 +368,6 @@ updateClientCapabilitiesQuery = "UPDATE clients SET capabilities = ? WHERE user selectClientIds :: PrepQuery R (Identity UserId) (Identity ClientId) selectClientIds = "SELECT client from clients where user = ?" -selectClients :: PrepQuery R (Identity UserId) (ClientId, ClientType, UTCTimeMillis, Maybe Text, Maybe ClientClass, Maybe CookieLabel, Maybe Latitude, Maybe Longitude, Maybe Text, Maybe (C.Set ClientCapability)) -selectClients = "SELECT client, type, tstamp, label, class, cookie, lat, lon, model, capabilities from clients where user = ?" - selectPubClients :: PrepQuery R (Identity UserId) (ClientId, Maybe ClientClass) selectPubClients = "SELECT client, class from clients where user = ?" @@ -407,12 +377,6 @@ selectClient = "SELECT client, type, tstamp, label, class, cookie, lat, lon, mod insertClientKey :: PrepQuery W (UserId, ClientId, PrekeyId, Text) () insertClientKey = "INSERT INTO prekeys (user, client, key, data) VALUES (?, ?, ?, ?)" -removeClient :: PrepQuery W (UserId, ClientId) () -removeClient = "DELETE FROM clients where user = ? and client = ?" - -removeClientKeys :: PrepQuery W (UserId, ClientId) () -removeClientKeys = "DELETE FROM prekeys where user = ? and client = ?" - userPrekey :: PrepQuery R (UserId, ClientId) (PrekeyId, Text) userPrekey = "SELECT key, data FROM prekeys where user = ? and client = ? LIMIT 1" @@ -434,9 +398,6 @@ selectMLSPublicKey = "SELECT key from mls_public_keys where user = ? and client selectMLSPublicKeys :: PrepQuery R (UserId, ClientId) (SignatureSchemeTag, Blob) selectMLSPublicKeys = "SELECT sig_scheme, key from mls_public_keys where user = ? and client = ?" -selectMLSPublicKeysByUser :: PrepQuery R (Identity UserId) (ClientId, SignatureSchemeTag, Blob) -selectMLSPublicKeysByUser = "SELECT client, sig_scheme, key from mls_public_keys where user = ?" - insertMLSPublicKeys :: PrepQuery W (UserId, ClientId, SignatureSchemeTag, Blob) Row insertMLSPublicKeys = "INSERT INTO mls_public_keys (user, client, sig_scheme, key) \ @@ -445,65 +406,15 @@ insertMLSPublicKeys = ------------------------------------------------------------------------------- -- Conversions -toClient :: - [(SignatureSchemeTag, Blob)] -> - ( ClientId, - ClientType, - UTCTimeMillis, - Maybe Text, - Maybe ClientClass, - Maybe CookieLabel, - Maybe Latitude, - Maybe Longitude, - Maybe Text, - Maybe (C.Set ClientCapability) - ) -> - Client -toClient keys (cid, cty, tme, lbl, cls, cok, lat, lon, mdl, cps) = - Client - { clientId = cid, - clientType = cty, - clientTime = tme, - clientClass = cls, - clientLabel = lbl, - clientCookie = cok, - clientLocation = location <$> lat <*> lon, - clientModel = mdl, - clientCapabilities = ClientCapabilityList $ maybe Set.empty (Set.fromList . C.fromSet) cps, - clientMLSPublicKeys = fmap (LBS.toStrict . fromBlob) (Map.fromList keys) - } - toPubClient :: (ClientId, Maybe ClientClass) -> PubClient toPubClient = uncurry PubClient ------------------------------------------------------------------------------- -- Best-effort optimistic locking for prekeys via DynamoDB -ddbClient :: Text -ddbClient = "client" - ddbVersion :: Text ddbVersion = "version" -ddbKey :: UserId -> ClientId -> AWS.AttributeValue -ddbKey u c = AWS.S (UUID.toText (toUUID u) <> "." <> client c) - -key :: UserId -> ClientId -> HashMap Text AWS.AttributeValue -key u c = HashMap.singleton ddbClient (ddbKey u c) - -deleteOptLock :: - ( MonadReader Brig.App.Env m, - MonadCatch m, - MonadIO m - ) => - UserId -> - ClientId -> - m () -deleteOptLock u c = do - t <- view (awsEnv . prekeyTable) - e <- view (awsEnv . amazonkaEnv) - void $ exec e (AWS.newDeleteItem t & AWS.deleteItem_key .~ key u c) - withOptLock :: forall a m. ( MonadIO m, diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 2cb9152357..b59b33d794 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -82,6 +82,7 @@ import Brig.Effects.UserQuery UserQuery, activateUser, deleteEmailUnvalidated, + deleteServiceUser, getAuthentication, getId, getLocale, @@ -94,6 +95,7 @@ import Brig.Effects.UserQuery updateEmail, updateHandle, updatePhone, + updateStatus, ) import Brig.Options import Brig.Password @@ -306,30 +308,6 @@ deleteEmail u = retry x5 $ write userEmailDelete (params LocalQuorum (Identity u deletePhone :: MonadClient m => UserId -> m () deletePhone u = retry x5 $ write userPhoneDelete (params LocalQuorum (Identity u)) -deleteServiceUser :: MonadClient m => ProviderId -> ServiceId -> BotId -> m () -deleteServiceUser pid sid bid = do - lookupServiceUser pid sid bid >>= \case - Nothing -> pure () - Just (_, mbTid) -> retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery cql (pid, sid, bid) - for_ mbTid $ \tid -> - addPrepQuery cqlTeam (pid, sid, tid, bid) - where - cql :: PrepQuery W (ProviderId, ServiceId, BotId) () - cql = - "DELETE FROM service_user \ - \WHERE provider = ? AND service = ? AND user = ?" - cqlTeam :: PrepQuery W (ProviderId, ServiceId, TeamId, BotId) () - cqlTeam = - "DELETE FROM service_team \ - \WHERE provider = ? AND service = ? AND team = ? AND user = ?" - -updateStatus :: MonadClient m => UserId -> AccountStatus -> m () -updateStatus u s = - retry x5 $ write userStatusUpdate (params LocalQuorum (s, u)) - userExists :: Member UserQuery r => UserId -> Sem r Bool userExists uid = isJust <$> getId uid @@ -412,14 +390,6 @@ lookupUsers :: lookupUsers loc locale hpi usrs = toUsers (tDomain loc) locale hpi <$> getUsers usrs -lookupServiceUser :: MonadClient m => ProviderId -> ServiceId -> BotId -> m (Maybe (ConvId, Maybe TeamId)) -lookupServiceUser pid sid bid = retry x1 (query1 cql (params LocalQuorum (pid, sid, bid))) - where - cql :: PrepQuery R (ProviderId, ServiceId, BotId) (ConvId, Maybe TeamId) - cql = - "SELECT conv, team FROM service_user \ - \WHERE provider = ? AND service = ? AND user = ?" - -- | NB: might return a lot of users, and therefore we do streaming here (page-by-page). lookupServiceUsers :: MonadClient m => @@ -534,9 +504,6 @@ userManagedByUpdate = "UPDATE user SET managed_by = ? WHERE id = ?" userPasswordUpdate :: PrepQuery W (Password, UserId) () userPasswordUpdate = "UPDATE user SET password = ? WHERE id = ?" -userStatusUpdate :: PrepQuery W (AccountStatus, UserId) () -userStatusUpdate = "UPDATE user SET status = ? WHERE id = ?" - userDeactivatedUpdate :: PrepQuery W (Identity UserId) () userDeactivatedUpdate = "UPDATE user SET activated = false WHERE id = ?" diff --git a/services/brig/src/Brig/Effects/ClientStore.hs b/services/brig/src/Brig/Effects/ClientStore.hs new file mode 100644 index 0000000000..d2e330db17 --- /dev/null +++ b/services/brig/src/Brig/Effects/ClientStore.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Effects.ClientStore where + +import Data.Id +import Polysemy +import Wire.API.User.Client + +data ClientStore m a where + LookupClients :: UserId -> ClientStore m [Client] + DeleteClient :: UserId -> ClientId -> ClientStore m () + +makeSem ''ClientStore diff --git a/services/brig/src/Brig/Effects/ClientStore/Cassandra.hs b/services/brig/src/Brig/Effects/ClientStore/Cassandra.hs new file mode 100644 index 0000000000..03ee42919b --- /dev/null +++ b/services/brig/src/Brig/Effects/ClientStore/Cassandra.hs @@ -0,0 +1,161 @@ +-- 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.ClientStore.Cassandra + ( clientStoreToCassandra, + -- FUTUREWORK: Do not export the names below once the module Brig.Data.Client has been + -- polysemised. + lookupClients, + rmClient, + toClient, + key, + ) +where + +import qualified Amazonka.DynamoDB as AWS +import qualified Amazonka.DynamoDB.DeleteItem as AWS +import Brig.AWS hiding (Env) +import Brig.App +import Brig.Data.Instances () +import qualified Brig.Effects.ClientStore as E +import Brig.User.Auth.DB.Instances () +import Cassandra hiding (Client) +import qualified Cassandra as C +import Control.Lens (view, (.~)) +import Control.Monad.Catch +import qualified Data.ByteString.Lazy as LBS +import qualified Data.HashMap.Strict as HashMap +import Data.Id +import Data.Json.Util +import qualified Data.Map as Map +import qualified Data.Set as Set +import qualified Data.UUID as UUID +import Imports +import Polysemy +import Wire.API.MLS.Credential +import Wire.API.User.Auth +import Wire.API.User.Client + +clientStoreToCassandra :: + forall m r a. + ( MonadCatch m, + MonadClient m, + MonadReader Env m, + Member (Embed m) r + ) => + Sem (E.ClientStore ': r) a -> + Sem r a +clientStoreToCassandra = + interpret $ + embed @m . \case + E.LookupClients uid -> lookupClients uid + E.DeleteClient uid cid -> rmClient uid cid + +lookupClients :: MonadClient m => UserId -> m [Client] +lookupClients u = do + keys <- + (\(cid, ss, Blob b) -> (cid, [(ss, LBS.toStrict b)])) + <$$> retry x1 (query selectMLSPublicKeysByUser (params LocalQuorum (Identity u))) + let keyMap = Map.fromListWith (<>) keys + updateKeys c = + c + { clientMLSPublicKeys = + Map.fromList $ Map.findWithDefault [] (clientId c) keyMap + } + updateKeys . toClient [] + <$$> retry x1 (query selectClients (params LocalQuorum (Identity u))) + +selectMLSPublicKeysByUser :: PrepQuery R (Identity UserId) (ClientId, SignatureSchemeTag, Blob) +selectMLSPublicKeysByUser = "SELECT client, sig_scheme, key from mls_public_keys where user = ?" + +selectClients :: PrepQuery R (Identity UserId) (ClientId, ClientType, UTCTimeMillis, Maybe Text, Maybe ClientClass, Maybe CookieLabel, Maybe Latitude, Maybe Longitude, Maybe Text, Maybe (C.Set ClientCapability)) +selectClients = "SELECT client, type, tstamp, label, class, cookie, lat, lon, model, capabilities from clients where user = ?" + +rmClient :: + ( MonadClient m, + MonadReader Env m, + MonadCatch m + ) => + UserId -> + ClientId -> + m () +rmClient u c = do + retry x5 $ write removeClient (params LocalQuorum (u, c)) + retry x5 $ write removeClientKeys (params LocalQuorum (u, c)) + unlessM (isJust <$> view randomPrekeyLocalLock) $ deleteOptLock u c + +removeClient :: PrepQuery W (UserId, ClientId) () +removeClient = "DELETE FROM clients where user = ? and client = ?" + +removeClientKeys :: PrepQuery W (UserId, ClientId) () +removeClientKeys = "DELETE FROM prekeys where user = ? and client = ?" + +deleteOptLock :: + ( MonadReader Env m, + MonadCatch m, + MonadIO m + ) => + UserId -> + ClientId -> + m () +deleteOptLock u c = do + t <- view (awsEnv . prekeyTable) + e <- view (awsEnv . amazonkaEnv) + void $ exec e (AWS.newDeleteItem t & AWS.deleteItem_key .~ key u c) + +-------------------------------------------------------------------------------- +-- Conversions + +toClient :: + [(SignatureSchemeTag, Blob)] -> + ( ClientId, + ClientType, + UTCTimeMillis, + Maybe Text, + Maybe ClientClass, + Maybe CookieLabel, + Maybe Latitude, + Maybe Longitude, + Maybe Text, + Maybe (C.Set ClientCapability) + ) -> + Client +toClient keys (cid, cty, tme, lbl, cls, cok, lat, lon, mdl, cps) = + Client + { clientId = cid, + clientType = cty, + clientTime = tme, + clientClass = cls, + clientLabel = lbl, + clientCookie = cok, + clientLocation = location <$> lat <*> lon, + clientModel = mdl, + clientCapabilities = ClientCapabilityList $ maybe Set.empty (Set.fromList . C.fromSet) cps, + clientMLSPublicKeys = fmap (LBS.toStrict . fromBlob) (Map.fromList keys) + } + +key :: UserId -> ClientId -> HashMap Text AWS.AttributeValue +key uid cid = HashMap.singleton ddbClient (ddbKey uid cid) + +------------------------------------------------------------------------------- +-- Best-effort optimistic locking for prekeys via DynamoDB + +ddbClient :: Text +ddbClient = "client" + +ddbKey :: UserId -> ClientId -> AWS.AttributeValue +ddbKey u c = AWS.S (UUID.toText (toUUID u) <> "." <> client c) diff --git a/services/brig/src/Brig/Effects/GalleyAccess.hs b/services/brig/src/Brig/Effects/GalleyAccess.hs index ea673ac40a..897a208e92 100644 --- a/services/brig/src/Brig/Effects/GalleyAccess.hs +++ b/services/brig/src/Brig/Effects/GalleyAccess.hs @@ -22,6 +22,7 @@ module Brig.Effects.GalleyAccess where import Data.Id import Imports import Polysemy +import Wire.API.Event.Conversation import Wire.API.Team.Feature import Wire.API.Team.Member @@ -33,5 +34,12 @@ data GalleyAccess m a where GetTeamId :: UserId -> GalleyAccess m (Maybe TeamId) -- | Calls 'Galley.API.getTeamFeatureStatusH'. GetTeamLegalHoldStatus :: TeamId -> GalleyAccess m (WithStatus LegalholdConfig) + -- | Tell Galley to remove a service bot from a conversation. + RemoveBotMember :: + UserId -> + Maybe ConnId -> + ConvId -> + BotId -> + GalleyAccess m (Maybe Event) makeSem ''GalleyAccess diff --git a/services/brig/src/Brig/Effects/GalleyAccess/Http.hs b/services/brig/src/Brig/Effects/GalleyAccess/Http.hs index c7e67baf8e..e14331ca13 100644 --- a/services/brig/src/Brig/Effects/GalleyAccess/Http.hs +++ b/services/brig/src/Brig/Effects/GalleyAccess/Http.hs @@ -26,7 +26,9 @@ import Brig.Effects.GalleyAccess import Brig.RPC import qualified Brig.RPC.Decode as RPC import Control.Monad.Catch +import Data.Aeson (encode) import Data.ByteString.Conversion.To +import Galley.Types.Bot import Imports import Network.HTTP.Types.Method import Network.HTTP.Types.Status @@ -95,3 +97,22 @@ galleyAccessToHttp g = ] . expect2xx makeReq "galley" g GET req >>= decodeBody "galley" + RemoveBotMember zusr conn conv bot -> do + debug $ + remote "galley" + . field "user" (toByteString zusr) + . field "conv" (toByteString conv) + . field "bot" (toByteString bot) + . msg (val "Removing bot member") + embed @m $ do + let req = + path "/i/bots" + . header "Z-User" (toByteString' zusr) + . maybe id (header "Z-Connection" . toByteString') conn + . contentJson + . lbytes (encode (removeBot conv bot)) + . expect [status200, status404] -- 404 is allowed: a given conversation may no longer exist + response <- makeReq "galley" g DELETE req + if isJust (RPC.responseBody response) && RPC.statusCode response == 200 + then Just <$> decodeBody "galley" response + else pure Nothing diff --git a/services/brig/src/Brig/Effects/UserQuery.hs b/services/brig/src/Brig/Effects/UserQuery.hs index 2ce287510f..0a1d3c78d8 100644 --- a/services/brig/src/Brig/Effects/UserQuery.hs +++ b/services/brig/src/Brig/Effects/UserQuery.hs @@ -36,8 +36,10 @@ module Brig.Effects.UserQuery updateEmail, updateHandle, updatePhone, + updateStatus, activateUser, deleteEmailUnvalidated, + deleteServiceUser, -- * effect-derived functions lookupAccount, @@ -299,8 +301,14 @@ data UserQuery m a where UpdateEmail :: UserId -> Email -> UserQuery m () UpdateHandle :: UserId -> Handle -> UserQuery m () UpdatePhone :: UserId -> Phone -> UserQuery m () + UpdateStatus :: UserId -> AccountStatus -> UserQuery m () ActivateUser :: UserId -> UserIdentity -> UserQuery m () DeleteEmailUnvalidated :: UserId -> UserQuery m () + DeleteServiceUser :: + ProviderId -> + ServiceId -> + BotId -> + UserQuery m () makeSem ''UserQuery diff --git a/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs b/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs index f072efd844..d5d9c8c322 100644 --- a/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs +++ b/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs @@ -56,8 +56,10 @@ userQueryToCassandra = UpdateEmail uid email -> updateEmailQuery uid email UpdateHandle uid handle -> updateHandleQuery uid handle UpdatePhone uid phone -> updatePhoneQuery uid phone + UpdateStatus uid s -> updateStatusQuery uid s ActivateUser uid ui -> activateUserQuery uid ui DeleteEmailUnvalidated uid -> deleteEmailUnvalidatedQuery uid + DeleteServiceUser pid sid bid -> deleteServiceUserQuery pid sid bid -------------------------------------------------------------------------------- -- Queries @@ -220,3 +222,42 @@ updateHandleQuery u h = retry x5 $ write userHandleUpdate (params LocalQuorum (h where userHandleUpdate :: PrepQuery W (Handle, UserId) () userHandleUpdate = "UPDATE user SET handle = ? WHERE id = ?" + +deleteServiceUserQuery :: + MonadClient m => + ProviderId -> + ServiceId -> + BotId -> + m () +deleteServiceUserQuery pid sid bid = + lookupServiceUser >>= \case + Nothing -> pure () + Just (_, mbTid) -> retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery cql (pid, sid, bid) + for_ mbTid $ \tid -> + addPrepQuery cqlTeam (pid, sid, tid, bid) + where + lookupServiceUser :: MonadClient m => m (Maybe (ConvId, Maybe TeamId)) + lookupServiceUser = retry x1 (query1 q (params LocalQuorum (pid, sid, bid))) + where + q :: PrepQuery R (ProviderId, ServiceId, BotId) (ConvId, Maybe TeamId) + q = + "SELECT conv, team FROM service_user \ + \WHERE provider = ? AND service = ? AND user = ?" + cql :: PrepQuery W (ProviderId, ServiceId, BotId) () + cql = + "DELETE FROM service_user \ + \WHERE provider = ? AND service = ? AND user = ?" + cqlTeam :: PrepQuery W (ProviderId, ServiceId, TeamId, BotId) () + cqlTeam = + "DELETE FROM service_team \ + \WHERE provider = ? AND service = ? AND team = ? AND user = ?" + +updateStatusQuery :: MonadClient m => UserId -> AccountStatus -> m () +updateStatusQuery u s = + retry x5 $ write userStatusUpdate (params LocalQuorum (s, u)) + where + userStatusUpdate :: PrepQuery W (AccountStatus, UserId) () + userStatusUpdate = "UPDATE user SET status = ? WHERE id = ?" diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index e38f771e2a..5e4d58ccfc 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -36,8 +36,9 @@ import Brig.App import qualified Brig.Code as Code import qualified Brig.Data.Client as User import qualified Brig.Data.User as User +import Brig.Effects.ClientStore +import Brig.Effects.GalleyAccess import Brig.Effects.UserQuery (UserQuery) -import Brig.Effects.UserQuery.Cassandra import Brig.Effects.VerificationCodeStore import Brig.Email (mkEmailKey) import qualified Brig.IO.Intra as RPC @@ -128,7 +129,9 @@ import qualified Wire.API.User.Identity as Public (Email) routesPublic :: Members - '[ Input (Local ()), + '[ ClientStore, + GalleyAccess, + Input (Local ()), UserQuery, VerificationCodeStore ] @@ -760,19 +763,20 @@ finishDeleteService :: ) => ProviderId -> ServiceId -> - m () + AppT r () finishDeleteService pid sid = do - mbSvc <- DB.lookupService pid sid + mbSvc <- wrapClient $ DB.lookupService pid sid + locale <- setDefaultUserLocale <$> view settings for_ mbSvc $ \svc -> do let tags = unsafeRange (serviceTags svc) name = serviceName svc - runConduit $ - User.lookupServiceUsers pid sid - .| C.mapM_ (pooledMapConcurrentlyN_ 16 kick) - RPC.removeServiceConn pid sid - DB.deleteService pid sid name tags + -- runConduit $ + -- User.lookupServiceUsers pid sid + -- .| C.mapM_ (pooledMapConcurrentlyN_ 16 . kick locale) + wrapHttp $ RPC.removeServiceConn pid sid + wrapClient $ DB.deleteService pid sid name tags where - kick (bid, cid, _) = deleteBot (botUserId bid) Nothing bid cid + kick l (bid, cid, _) = deleteBot l (botUserId bid) Nothing bid cid deleteAccountH :: ( MonadReader Env m, @@ -919,6 +923,7 @@ updateServiceWhitelist uid con tid upd = do _ <- wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound -- Add to various tables whitelisted <- wrapClientE $ DB.getServiceWhitelistStatus tid pid sid + locale <- setDefaultUserLocale <$> view settings case (whitelisted, newWhitelisted) of (False, False) -> pure UpdateServiceWhitelistRespUnchanged (True, True) -> pure UpdateServiceWhitelistRespUnchanged @@ -928,17 +933,17 @@ updateServiceWhitelist uid con tid upd = do (True, False) -> do -- When the service is de-whitelisted, remove its bots from team -- conversations - lift $ - fmap - wrapHttpClient - runConduit - $ User.lookupServiceUsersForTeam pid sid tid - .| C.mapM_ - ( pooledMapConcurrentlyN_ - 16 - ( uncurry (deleteBot uid (Just con)) - ) - ) + -- lift $ + -- fmap + -- wrapHttpClient + -- runConduit $ + -- User.lookupServiceUsersForTeam pid sid tid + -- .| C.mapM_ + -- ( pooledMapConcurrentlyN_ + -- 16 + -- ( uncurry (deleteBot locale uid (Just con)) + -- ) + -- ) wrapClientE $ DB.deleteServiceWhitelist (Just tid) pid sid pure UpdateServiceWhitelistRespChanged @@ -1039,12 +1044,33 @@ addBot zuid zcon cid add = do Public.rsAddBotEvent = ev } -removeBotH :: UserId ::: ConnId ::: ConvId ::: BotId -> (Handler r) Response +removeBotH :: + Members + '[ ClientStore, + GalleyAccess, + Input (Local ()), + UserQuery + ] + r => + UserId ::: ConnId ::: ConvId ::: BotId -> + Handler r Response removeBotH (zusr ::: zcon ::: cid ::: bid) = do mapExceptT wrapHttp $ guardSecondFactorDisabled (Just zusr) maybe (setStatus status204 empty) json <$> removeBot zusr zcon cid bid -removeBot :: UserId -> ConnId -> ConvId -> BotId -> (Handler r) (Maybe Public.RemoveBotResponse) +removeBot :: + Members + '[ ClientStore, + GalleyAccess, + Input (Local ()), + UserQuery + ] + r => + UserId -> + ConnId -> + ConvId -> + BotId -> + Handler r (Maybe Public.RemoveBotResponse) removeBot zusr zcon cid bid = do -- Get the conversation and check preconditions cnv <- lift (wrapHttp $ RPC.getConv zusr cid) >>= maybeConvNotFound @@ -1054,10 +1080,13 @@ removeBot zusr zcon cid bid = do -- Find the bot in the member list and delete it let busr = botUserId bid let bot = List.find ((== busr) . qUnqualified . omQualifiedId) (cmOthers mems) + locale <- setDefaultUserLocale <$> view settings case bot >>= omService of Nothing -> pure Nothing Just _ -> do - lift $ Public.RemoveBotResponse <$$> wrapHttpClient (deleteBot zusr (Just zcon) bid cid) + lift . liftSem $ + Public.RemoveBotResponse + <$$> deleteBot locale zusr (Just zcon) bid cid -------------------------------------------------------------------------------- -- Bot API @@ -1157,7 +1186,13 @@ botGetUserClients uid = pubClient c = Public.PubClient (clientId c) (clientClass c) botDeleteSelfH :: - Member UserQuery r => + Members + '[ ClientStore, + GalleyAccess, + Input (Local ()), + UserQuery + ] + r => BotId ::: ConvId -> (Handler r) Response botDeleteSelfH (bid ::: cid) = do @@ -1165,7 +1200,13 @@ botDeleteSelfH (bid ::: cid) = do empty <$ botDeleteSelf bid cid botDeleteSelf :: - Member UserQuery r => + Members + '[ ClientStore, + GalleyAccess, + Input (Local ()), + UserQuery + ] + r => BotId -> ConvId -> (Handler r) () @@ -1175,7 +1216,7 @@ botDeleteSelf bid cid = do mapExceptT wrapHttp $ guardSecondFactorDisabled (Just (botUserId bid)) bot <- lift . liftSem $ User.lookupUser loc locale NoPendingInvitations (botUserId bid) _ <- maybeInvalidBot (userService =<< bot) - _ <- lift $ wrapHttpClient $ deleteBot (botUserId bid) Nothing bid cid + _ <- lift . liftSem $ deleteBot locale (botUserId bid) Nothing bid cid pure () -------------------------------------------------------------------------------- @@ -1209,38 +1250,36 @@ activate pid old new = do wrapClientE $ DB.insertKey pid (mkEmailKey <$> old) emailKey deleteBot :: - forall m. - ( MonadHttp m, - MonadReader Env m, - MonadMask m, - HasRequestId m, - MonadLogger m, - MonadClient m - ) => + forall r. + Members + '[ ClientStore, + GalleyAccess, + Input (Local ()), + UserQuery + ] + r => + Locale -> UserId -> Maybe ConnId -> BotId -> ConvId -> - m (Maybe Public.Event) -deleteBot zusr zcon bid cid = do + Sem r (Maybe Public.Event) +deleteBot locale zusr zcon bid cid = do -- Remove the bot from the conversation - ev <- RPC.removeBotMember zusr zcon cid bid + ev <- removeBotMember zusr zcon cid bid -- Delete the bot user and client let buid = botUserId bid - loc <- fmap (qTagUnsafe @'QLocal) $ Qualified () <$> viewFederationDomain - locale <- setDefaultUserLocale <$> view settings + loc <- input mbUser <- - runM $ - userQueryToCassandra @m @'[Embed m] $ - User.lookupUser loc locale NoPendingInvitations buid - User.lookupClients buid >>= mapM_ (User.rmClient buid . clientId) + User.lookupUser loc locale NoPendingInvitations buid + lookupClients buid >>= mapM_ (deleteClient buid . clientId) for_ (userService =<< mbUser) $ \sref -> do let pid = sref ^. serviceRefProvider sid = sref ^. serviceRefId User.deleteServiceUser pid sid bid -- TODO: Consider if we can actually delete the bot user entirely, -- i.e. not just marking the account as deleted. - void $ runExceptT $ User.updateStatus buid Deleted + User.updateStatus buid Deleted pure ev validateServiceKey :: MonadIO m => Public.ServiceKeyPEM -> m (Maybe (Public.ServiceKey, Fingerprint Rsa)) From a41b0fb8e4771d316ff1b22a073702df0c6fa5e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 5 Oct 2022 16:27:00 +0200 Subject: [PATCH 37/41] Add a TODO to replace the Async effect --- services/brig/src/Brig/Unique.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/services/brig/src/Brig/Unique.hs b/services/brig/src/Brig/Unique.hs index ea733fb216..7482e69505 100644 --- a/services/brig/src/Brig/Unique.hs +++ b/services/brig/src/Brig/Unique.hs @@ -48,6 +48,7 @@ import Polysemy.Time.Data.TimeUnit -- and is responsible for turning the temporary claim into permanent -- ownership, if desired. withClaim :: + -- TODO(md): Replace the dependency on the Async effect as it is totally broken. Members '[Async, Race, Resource, UniqueClaimsStore] r => -- | The 'Id' associated with the claim. Id a -> From fd083038c0d7ba7dfbc7ad964add9f0eaa220544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 5 Oct 2022 17:06:05 +0200 Subject: [PATCH 38/41] Fix one use site of deleteBot --- services/brig/src/Brig/API/Client.hs | 8 +-- services/brig/src/Brig/API/User.hs | 72 +++++++++---------- services/brig/src/Brig/API/Util.hs | 4 +- .../brig/src/Brig/CanonicalInterpreter.hs | 5 +- services/brig/src/Brig/Data/Activation.hs | 4 +- services/brig/src/Brig/Data/Client.hs | 6 +- services/brig/src/Brig/Data/User.hs | 16 ++--- services/brig/src/Brig/Data/UserKey.hs | 4 +- services/brig/src/Brig/Effects/UserQuery.hs | 57 ++++++++------- .../src/Brig/Effects/UserQuery/Cassandra.hs | 25 +++++-- services/brig/src/Brig/Provider/API.hs | 55 +++++++------- services/brig/src/Brig/Team/API.hs | 47 +++++++++--- services/brig/src/Brig/User/Auth.hs | 49 ++++++------- services/brig/src/Brig/User/Handle.hs | 4 +- 14 files changed, 207 insertions(+), 149 deletions(-) diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 152f68fce6..3419aba0c2 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -156,7 +156,7 @@ addClient :: '[ GalleyAccess, GundeckAccess, Input (Local ()), - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -170,12 +170,12 @@ addClient = addClientWithReAuthPolicy Data.reAuthForNewClients -- nb. We must ensure that the set of clients known to brig is always -- a superset of the clients known to galley. addClientWithReAuthPolicy :: - forall r. + forall r p. Members '[ GalleyAccess, GundeckAccess, Input (Local ()), - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -250,7 +250,7 @@ rmClient :: '[ Error ReAuthError, GundeckAccess, Input (Local ()), - UserQuery + UserQuery p ] r => UserId -> diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 1eca0d1617..3bf90d537b 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -229,7 +229,7 @@ verifyUniquenessAndCheckBlacklist :: Members '[ BlacklistStore, UserKeyStore, - UserQuery + UserQuery p ] r => UserKey -> @@ -246,7 +246,7 @@ verifyUniquenessAndCheckBlacklist uk = do throwE IdentityErrorUserKeyExists createUserSpar :: - forall r. + forall r p. Members '[ Async, GalleyAccess, @@ -255,7 +255,7 @@ createUserSpar :: Resource, UniqueClaimsStore, UserHandleStore, - UserQuery + UserQuery p ] r => NewUserSpar -> @@ -333,7 +333,7 @@ createUser :: Twilio, UserKeyStore, UserPendingActivationStore p, - UserQuery + UserQuery p ] r => NewUser -> @@ -581,7 +581,7 @@ createUserInviteViaScim :: '[ BlacklistStore, UserKeyStore, UserPendingActivationStore p, - UserQuery + UserQuery p ] r => UserId -> @@ -629,7 +629,7 @@ updateUser :: Members '[ GalleyAccess, GundeckAccess, - UserQuery + UserQuery p ] r => UserId -> @@ -699,7 +699,7 @@ changeHandle :: Resource, UniqueClaimsStore, UserHandleStore, - UserQuery + UserQuery p ] r => UserId -> @@ -794,7 +794,7 @@ changeSelfEmail :: ActivationSupply, BlacklistStore, UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -826,7 +826,7 @@ changeEmail :: ActivationSupply, BlacklistStore, UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -877,7 +877,7 @@ changePhone :: P.Error Twilio.ErrorResponse, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -913,7 +913,7 @@ removeEmail :: GundeckAccess, Input (Local ()), UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -939,7 +939,7 @@ removePhone :: GundeckAccess, Input (Local ()), UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -964,13 +964,13 @@ removePhone uid conn = do -- Forcefully revoke a verified identity revokeIdentity :: - forall r. + forall r p. Members '[ GalleyAccess, GundeckAccess, Input (Local ()), UserKeyStore, - UserQuery + UserQuery p ] r => Either Email Phone -> @@ -1010,11 +1010,11 @@ revokeIdentity key = do -- Change Account Status changeAccountStatus :: - forall r. + forall r p. Members '[ GalleyAccess, GundeckAccess, - UserQuery + UserQuery p ] r => NonEmpty UserId -> @@ -1039,7 +1039,7 @@ changeSingleAccountStatus :: Members '[ GalleyAccess, GundeckAccess, - UserQuery + UserQuery p ] r => UserId -> @@ -1083,7 +1083,7 @@ activate :: PasswordResetStore, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => ActivationTarget -> @@ -1105,7 +1105,7 @@ activateWithCurrency :: PasswordResetStore, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => ActivationTarget -> @@ -1157,7 +1157,7 @@ onActivated :: Members '[ GalleyAccess, GundeckAccess, - UserQuery + UserQuery p ] r => ActivationEvent -> @@ -1186,7 +1186,7 @@ sendActivationCode :: P.Error Twilio.ErrorResponse, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => Either Email Phone -> @@ -1308,7 +1308,7 @@ mkActivationKey (ActivatePhone p) = do -- Password Management changePassword :: - Members '[UserQuery] r => + Members '[UserQuery p] r => UserId -> PasswordChange -> ExceptT ChangePasswordError (AppT r) () @@ -1407,7 +1407,7 @@ deleteSelfUser :: UniqueClaimsStore, UserHandleStore, UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -1496,7 +1496,7 @@ verifyDeleteUser :: UniqueClaimsStore, UserHandleStore, UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -1522,7 +1522,7 @@ ensureAccountDeleted :: UniqueClaimsStore, UserHandleStore, UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -1564,14 +1564,14 @@ ensureAccountDeleted uid = do -- statements matters! Other functions reason upon some states to imply other -- states. Please change this order only with care! deleteAccount :: - forall r. + forall r p. Members '[ GalleyAccess, GundeckAccess, UniqueClaimsStore, UserHandleStore, UserKeyStore, - UserQuery + UserQuery p ] r => UserAccount -> @@ -1685,7 +1685,7 @@ userGC u = case userExpire u of pure u lookupProfile :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => Local UserId -> Qualified UserId -> ExceptT FederationError (AppT r) (Maybe UserProfile) @@ -1701,7 +1701,7 @@ lookupProfile self other = -- Otherwise only the 'PublicProfile' is accessible for user 'self'. -- If 'self' is an unknown 'UserId', return '[]'. lookupProfiles :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => -- ( MonadUnliftIO m, -- MonadClient m, -- MonadReader Env m, @@ -1728,7 +1728,7 @@ lookupProfiles self others = (bucketQualified others) lookupProfilesFromDomain :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => Local UserId -> Qualified [UserId] -> ExceptT FederationError (AppT r) [UserProfile] @@ -1748,8 +1748,8 @@ lookupRemoteProfiles (qUntagged -> Qualified uids domain) = -- ids, but it is also very complex. Maybe this can be made easy by extracting a -- pure function and writing tests for that. lookupLocalProfiles :: - forall r. - Members '[Input (Local ()), UserQuery] r => + forall r p. + Members '[Input (Local ()), UserQuery p] r => -- | This is present only when an authenticated user is requesting access. Maybe UserId -> -- | The users ('others') for which to obtain the profiles. @@ -1801,7 +1801,7 @@ lookupLocalProfiles requestingUser others = do in baseProfile {profileEmail = profileEmail'} getLegalHoldStatus :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => UserId -> AppT r (Maybe UserLegalHoldStatus) getLegalHoldStatus uid = do @@ -1810,8 +1810,8 @@ getLegalHoldStatus uid = do =<< liftSem (lookupAccount locale uid) getLegalHoldStatus' :: - forall r. - Members '[Input (Local ()), UserQuery] r => + forall r p. + Members '[Input (Local ()), UserQuery p] r => User -> AppT r UserLegalHoldStatus getLegalHoldStatus' user = @@ -1846,7 +1846,7 @@ getEmailForProfile _ EmailVisibleToSelf' = Nothing -- | Find user accounts for a given identity, both activated and those -- currently pending activation. lookupAccountsByIdentity :: - Members '[Input (Local ()), UserKeyStore, UserQuery] r => + Members '[Input (Local ()), UserKeyStore, UserQuery p] r => Either Email Phone -> Bool -> AppT r [UserAccount] diff --git a/services/brig/src/Brig/API/Util.hs b/services/brig/src/Brig/API/Util.hs index a4024dec52..120c3d5517 100644 --- a/services/brig/src/Brig/API/Util.hs +++ b/services/brig/src/Brig/API/Util.hs @@ -75,7 +75,7 @@ lookupProfilesMaybeFilterSameTeamOnly self us = do fetchUserIdentity :: Members '[ Input (Local ()), - UserQuery + UserQuery p ] r => UserId -> @@ -90,7 +90,7 @@ fetchUserIdentity uid = lookupSelfProfile :: Members '[ Input (Local ()), - UserQuery + UserQuery p ] r => UserId -> diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index ad17a36e90..bdcacdc642 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -84,6 +84,7 @@ import Wire.Sem.Logger.TinyLog import Wire.Sem.Now (Now) import Wire.Sem.Now.IO import Wire.Sem.Paging.Cassandra (InternalPaging) +import qualified Wire.Sem.Paging.Cassandra as PC type BrigCanonicalEffects = '[ PublicKeyBundle, @@ -100,7 +101,7 @@ type BrigCanonicalEffects = GalleyAccess, GundeckAccess, Embed HttpClientIO, - UserQuery, + UserQuery PC.InternalPaging, PasswordResetStore, UserPendingActivationStore InternalPaging, Now, @@ -137,7 +138,7 @@ runBrigToIO e (AppT ma) = . nowToIOAction (e ^. currentTime) . userPendingActivationStoreToCassandra . passwordResetStoreToCodeStore - . userQueryToCassandra @Cas.Client + . userQueryToCassandra . interpretHttpToIO e . gundeckAccessToHttp @HttpClientIO (e ^. gundeck) . galleyAccessToHttp @HttpClientIO (e ^. galley) diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index ae072ef653..790c4d0c26 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -91,14 +91,14 @@ data ActivationEvent -- docs/reference/user/activation.md {#RefActivationSubmit} activateKey :: - forall r. + forall r p. Members '[ ActivationKeyStore, Input (Local ()), E.PasswordResetSupply, PasswordResetStore, UserKeyStore, - UserQuery + UserQuery p ] r => Locale -> diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index f127cb2ed8..3e52dc7489 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -125,7 +125,7 @@ reAuthForNewClients :: ReAuthPolicy reAuthForNewClients count upsert = count > 0 && not upsert addClient :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => UserId -> ClientId -> NewClient -> @@ -136,8 +136,8 @@ addClient :: addClient = addClientWithReAuthPolicy reAuthForNewClients addClientWithReAuthPolicy :: - forall r. - Members '[Input (Local ()), UserQuery] r => + forall r p. + Members '[Input (Local ()), UserQuery p] r => ReAuthPolicy -> UserId -> ClientId -> diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index b59b33d794..042c235fa2 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -196,7 +196,7 @@ newAccountInviteViaScim uid tid locale name email = do authenticate :: Members '[ Error AuthError, - UserQuery + UserQuery p ] r => UserId -> @@ -221,7 +221,7 @@ reauthenticate :: Members '[ Error ReAuthError, Input (Local ()), - UserQuery + UserQuery p ] r => Locale -> @@ -245,7 +245,7 @@ reauthenticate locale u pw = throw (ReAuthError AuthInvalidCredentials) isSamlUser :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => Locale -> UserId -> Sem r Bool @@ -308,7 +308,7 @@ deleteEmail u = retry x5 $ write userEmailDelete (params LocalQuorum (Identity u deletePhone :: MonadClient m => UserId -> m () deletePhone u = retry x5 $ write userPhoneDelete (params LocalQuorum (Identity u)) -userExists :: Member UserQuery r => UserId -> Sem r Bool +userExists :: Member (UserQuery p) r => UserId -> Sem r Bool userExists uid = isJust <$> getId uid filterActive :: MonadClient m => [UserId] -> m [UserId] @@ -321,7 +321,7 @@ filterActive us = isActiveUser _ = False lookupUser :: - Member UserQuery r => + Member (UserQuery p) r => Local x -> Locale -> HavePendingInvitations -> @@ -334,7 +334,7 @@ deactivateUser u = retry x5 $ write userDeactivatedUpdate (params LocalQuorum (Identity u)) lookupLocale :: - Member UserQuery r => + Member (UserQuery p) r => Locale -> UserId -> Sem r (Maybe Locale) @@ -370,7 +370,7 @@ lookupUserTeam u = <$> retry x1 (query1 teamSelect (params LocalQuorum (Identity u))) lookupAuth :: - Member UserQuery r => + Member (UserQuery p) r => UserId -> Sem r (Maybe (Maybe Password, AccountStatus)) lookupAuth u = fmap f <$> getAuthentication u @@ -381,7 +381,7 @@ lookupAuth u = fmap f <$> getAuthentication u -- -- Skips nonexistent users. /Does not/ skip users who have been deleted. lookupUsers :: - Member UserQuery r => + Member (UserQuery p) r => Local x -> Locale -> HavePendingInvitations -> diff --git a/services/brig/src/Brig/Data/UserKey.hs b/services/brig/src/Brig/Data/UserKey.hs index 1f8db3a2a8..903a2a7793 100644 --- a/services/brig/src/Brig/Data/UserKey.hs +++ b/services/brig/src/Brig/Data/UserKey.hs @@ -69,7 +69,7 @@ keyTextOriginal (UserPhoneKey k) = fromPhone (phoneKeyOrig k) -- | Claim a 'UserKey' for a user. claimKey :: - Members '[UserKeyStore, UserQuery] r => + Members '[UserKeyStore, UserQuery p] r => -- | The SHA256 digest Digest -> -- | The key to claim. @@ -86,7 +86,7 @@ claimKey d k u = do -- A key is available if it is not already actived for another user or -- if the other user and the user looking to claim the key are the same. keyAvailable :: - Members '[UserKeyStore, UserQuery] r => + Members '[UserKeyStore, UserQuery p] r => -- | The key to check. UserKey -> -- | The user looking to claim the key, if any. diff --git a/services/brig/src/Brig/Effects/UserQuery.hs b/services/brig/src/Brig/Effects/UserQuery.hs index 0a1d3c78d8..36229bd611 100644 --- a/services/brig/src/Brig/Effects/UserQuery.hs +++ b/services/brig/src/Brig/Effects/UserQuery.hs @@ -21,6 +21,7 @@ module Brig.Effects.UserQuery ( UserQuery (..), getId, getUsers, + getServiceUsers, getName, getLocale, getAuthentication, @@ -72,6 +73,7 @@ import Wire.API.Error import qualified Wire.API.Error.Brig as E import Wire.API.Provider.Service import Wire.API.User +import Wire.Sem.Paging type Activated = Bool @@ -271,7 +273,9 @@ toIdentity False _ _ _ = Nothing ------------------------------------------------------------------------------- -data UserQuery m a where +type BotInConv = (BotId, ConvId, Maybe TeamId) + +data UserQuery p m a where -- FUTUREWORK: The 'InsertAccount' action should perhaps be in an account store effect InsertAccount :: UserAccount -> @@ -281,46 +285,51 @@ data UserQuery m a where Maybe Password -> -- | Whether the user is activated Bool -> - UserQuery m () - GetId :: UserId -> UserQuery m (Maybe UserId) -- idSelect - GetUsers :: [UserId] -> UserQuery m [UserRow] -- usersSelect - GetName :: UserId -> UserQuery m (Maybe Name) -- nameSelect - GetLocale :: UserId -> UserQuery m (Maybe (Maybe Language, Maybe Country)) -- localeSelect - GetAuthentication :: UserId -> UserQuery m (Maybe (Maybe Password, Maybe AccountStatus)) -- authSelect - GetPassword :: UserId -> UserQuery m (Maybe Password) -- passwordSelect - GetActivated :: UserId -> UserQuery m Bool -- activatedSelect - GetAccountStatus :: UserId -> UserQuery m (Maybe AccountStatus) -- statusSelect - GetAccountStatuses :: [UserId] -> UserQuery m [(UserId, Bool, Maybe AccountStatus)] -- accountStateSelectAll - GetTeam :: UserId -> UserQuery m (Maybe TeamId) -- teamSelect - GetAccounts :: [UserId] -> UserQuery m [AccountRow] -- accountsSelect + UserQuery p m () + GetId :: UserId -> UserQuery p m (Maybe UserId) -- idSelect + GetUsers :: [UserId] -> UserQuery p m [UserRow] -- usersSelect + GetServiceUsers :: -- lookupServiceUsers + ProviderId -> + ServiceId -> + Maybe (PagingState p BotInConv) -> + UserQuery p m (Page p BotInConv) + GetName :: UserId -> UserQuery p m (Maybe Name) -- nameSelect + GetLocale :: UserId -> UserQuery p m (Maybe (Maybe Language, Maybe Country)) -- localeSelect + GetAuthentication :: UserId -> UserQuery p m (Maybe (Maybe Password, Maybe AccountStatus)) -- authSelect + GetPassword :: UserId -> UserQuery p m (Maybe Password) -- passwordSelect + GetActivated :: UserId -> UserQuery p m Bool -- activatedSelect + GetAccountStatus :: UserId -> UserQuery p m (Maybe AccountStatus) -- statusSelect + GetAccountStatuses :: [UserId] -> UserQuery p m [(UserId, Bool, Maybe AccountStatus)] -- accountStateSelectAll + GetTeam :: UserId -> UserQuery p m (Maybe TeamId) -- teamSelect + GetAccounts :: [UserId] -> UserQuery p m [AccountRow] -- accountsSelect -- | Whether the account has been activated by verifying an email address or -- phone number. - IsActivated :: UserId -> UserQuery m Bool - UpdateUser :: UserId -> UserUpdate -> UserQuery m () - UpdateEmail :: UserId -> Email -> UserQuery m () - UpdateHandle :: UserId -> Handle -> UserQuery m () - UpdatePhone :: UserId -> Phone -> UserQuery m () - UpdateStatus :: UserId -> AccountStatus -> UserQuery m () - ActivateUser :: UserId -> UserIdentity -> UserQuery m () - DeleteEmailUnvalidated :: UserId -> UserQuery m () + IsActivated :: UserId -> UserQuery p m Bool + UpdateUser :: UserId -> UserUpdate -> UserQuery p m () + UpdateEmail :: UserId -> Email -> UserQuery p m () + UpdateHandle :: UserId -> Handle -> UserQuery p m () + UpdatePhone :: UserId -> Phone -> UserQuery p m () + UpdateStatus :: UserId -> AccountStatus -> UserQuery p m () + ActivateUser :: UserId -> UserIdentity -> UserQuery p m () + DeleteEmailUnvalidated :: UserId -> UserQuery p m () DeleteServiceUser :: ProviderId -> ServiceId -> BotId -> - UserQuery m () + UserQuery p m () makeSem ''UserQuery lookupAccount :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => Locale -> UserId -> Sem r (Maybe UserAccount) lookupAccount locale u = listToMaybe <$> lookupAccounts locale [u] lookupAccounts :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => Locale -> [UserId] -> Sem r [UserAccount] diff --git a/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs b/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs index d5d9c8c322..efe26a7292 100644 --- a/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs +++ b/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs @@ -30,17 +30,20 @@ import Imports import Polysemy import Wire.API.Provider.Service import Wire.API.User +import qualified Wire.Sem.Paging.Cassandra as PC userQueryToCassandra :: - forall m r a. - (MonadClient m, Member (Embed m) r) => - Sem (UserQuery ': r) a -> + forall r a. + (Member (Embed Client) r) => + Sem (UserQuery PC.InternalPaging ': r) a -> Sem r a userQueryToCassandra = interpret $ - embed @m . \case + embed @Client . \case GetId uid -> runIdentity <$$> retry x1 (query1 idSelect (params LocalQuorum (Identity uid))) GetUsers uids -> retry x1 (query usersSelect (params LocalQuorum (Identity uids))) + GetServiceUsers pid sid Nothing -> flip PC.mkInternalPage pure =<< lookupServiceUsers pid sid + GetServiceUsers _pid _sid (Just ps) -> PC.ipNext ps GetName uid -> runIdentity <$$> retry x1 (query1 nameSelect (params LocalQuorum (Identity uid))) GetLocale uid -> retry x1 (query1 localeSelect (params LocalQuorum (Identity uid))) GetAuthentication uid -> retry x1 (query1 authSelect (params LocalQuorum (Identity uid))) @@ -261,3 +264,17 @@ updateStatusQuery u s = where userStatusUpdate :: PrepQuery W (AccountStatus, UserId) () userStatusUpdate = "UPDATE user SET status = ? WHERE id = ?" + +-- | NB: might return a lot of users, and therefore we do paging here. +lookupServiceUsers :: + MonadClient m => + ProviderId -> + ServiceId -> + m (Page (BotId, ConvId, Maybe TeamId)) +lookupServiceUsers pid sid = + retry x1 (paginate cql (params LocalQuorum (pid, sid))) + where + cql :: PrepQuery R (ProviderId, ServiceId) (BotId, ConvId, Maybe TeamId) + cql = + "SELECT user, conv, team FROM service_user \ + \WHERE provider = ? AND service = ?" diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 5e4d58ccfc..593290a587 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -38,7 +38,7 @@ import qualified Brig.Data.Client as User import qualified Brig.Data.User as User import Brig.Effects.ClientStore import Brig.Effects.GalleyAccess -import Brig.Effects.UserQuery (UserQuery) +import Brig.Effects.UserQuery (UserQuery, getServiceUsers) import Brig.Effects.VerificationCodeStore import Brig.Email (mkEmailKey) import qualified Brig.IO.Intra as RPC @@ -126,13 +126,15 @@ import Wire.API.User.Client import qualified Wire.API.User.Client as Public (Client, ClientCapability (ClientSupportsLegalholdImplicitConsent), PubClient (..), UserClientPrekeyMap, UserClients, userClients) import qualified Wire.API.User.Client.Prekey as Public (PrekeyId) import qualified Wire.API.User.Identity as Public (Email) +import Wire.Sem.Concurrency +import Wire.Sem.Paging routesPublic :: Members '[ ClientStore, GalleyAccess, Input (Local ()), - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -753,14 +755,16 @@ deleteService pid sid del = do lift $ Queue.enqueue queue (Internal.DeleteService pid sid) finishDeleteService :: - ( MonadReader Env m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m, - MonadClient m, - MonadUnliftIO m - ) => + forall p r. + Paging p => + Members + '[ ClientStore, + Concurrency 'Unsafe, + GalleyAccess, + Input (Local ()), + UserQuery p + ] + r => ProviderId -> ServiceId -> AppT r () @@ -770,9 +774,10 @@ finishDeleteService pid sid = do for_ mbSvc $ \svc -> do let tags = unsafeRange (serviceTags svc) name = serviceName svc - -- runConduit $ - -- User.lookupServiceUsers pid sid - -- .| C.mapM_ (pooledMapConcurrentlyN_ 16 . kick locale) + liftSem $ + withChunks @p + (getServiceUsers pid sid) + (unsafePooledMapConcurrentlyN_ 16 (kick locale)) wrapHttp $ RPC.removeServiceConn pid sid wrapClient $ DB.deleteService pid sid name tags where @@ -950,7 +955,7 @@ updateServiceWhitelist uid con tid upd = do addBotH :: Members '[ Input (Local ()), - UserQuery + UserQuery p ] r => UserId ::: ConnId ::: ConvId ::: JsonRequest Public.AddBot -> @@ -962,7 +967,7 @@ addBotH (zuid ::: zcon ::: cid ::: req) = do addBot :: Members '[ Input (Local ()), - UserQuery + UserQuery p ] r => UserId -> @@ -1049,7 +1054,7 @@ removeBotH :: '[ ClientStore, GalleyAccess, Input (Local ()), - UserQuery + UserQuery p ] r => UserId ::: ConnId ::: ConvId ::: BotId -> @@ -1063,7 +1068,7 @@ removeBot :: '[ ClientStore, GalleyAccess, Input (Local ()), - UserQuery + UserQuery p ] r => UserId -> @@ -1092,7 +1097,7 @@ removeBot zusr zcon cid bid = do -- Bot API botGetSelfH :: - Member UserQuery r => + Member (UserQuery p) r => BotId -> (Handler r) Response botGetSelfH bot = do @@ -1100,7 +1105,7 @@ botGetSelfH bot = do json <$> botGetSelf bot botGetSelf :: - Member UserQuery r => + Member (UserQuery p) r => BotId -> (Handler r) Public.UserProfile botGetSelf bot = do @@ -1157,7 +1162,7 @@ botClaimUsersPrekeys body = do Client.claimLocalMultiPrekeyBundles UnprotectedBot body !>> clientError botListUserProfilesH :: - Member UserQuery r => + Member (UserQuery p) r => List UserId -> (Handler r) Response botListUserProfilesH uids = do @@ -1165,7 +1170,7 @@ botListUserProfilesH uids = do json <$> botListUserProfiles uids botListUserProfiles :: - Member UserQuery r => + Member (UserQuery p) r => List UserId -> (Handler r) [Public.BotUserView] botListUserProfiles uids = do @@ -1190,7 +1195,7 @@ botDeleteSelfH :: '[ ClientStore, GalleyAccess, Input (Local ()), - UserQuery + UserQuery p ] r => BotId ::: ConvId -> @@ -1204,7 +1209,7 @@ botDeleteSelf :: '[ ClientStore, GalleyAccess, Input (Local ()), - UserQuery + UserQuery p ] r => BotId -> @@ -1250,12 +1255,12 @@ activate pid old new = do wrapClientE $ DB.insertKey pid (mkEmailKey <$> old) emailKey deleteBot :: - forall r. + forall r p. Members '[ ClientStore, GalleyAccess, Input (Local ()), - UserQuery + UserQuery p ] r => Locale -> diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 110e287ac8..2f5057e797 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -96,7 +96,7 @@ routesPublic :: P.Error Twilio.ErrorResponse, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => Routes Doc.ApiBuilder (Handler r) () @@ -218,7 +218,7 @@ routesInternal :: Twilio, UserKeyStore, UserPendingActivationStore p, - UserQuery + UserQuery p ] r => Routes a (Handler r) () @@ -284,7 +284,7 @@ createInvitationPublicH :: P.Error Twilio.ErrorResponse, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => JSON ::: UserId ::: TeamId ::: JsonRequest Public.InvitationRequest -> @@ -311,7 +311,7 @@ createInvitationPublic :: P.Error Twilio.ErrorResponse, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -344,7 +344,7 @@ createInvitationViaScimH :: Twilio, UserKeyStore, UserPendingActivationStore p, - UserQuery + UserQuery p ] r => JSON ::: JsonRequest NewUserScimInvitation -> @@ -360,7 +360,7 @@ createInvitationViaScim :: Twilio, UserKeyStore, UserPendingActivationStore p, - UserQuery + UserQuery p ] r => NewUserScimInvitation -> @@ -533,14 +533,24 @@ getInvitationByEmail email = do maybe (throwStd (notFound "Invitation not found")) pure inv suspendTeamH :: - Members '[GalleyAccess, GundeckAccess] r => + Members + '[ GalleyAccess, + GundeckAccess, + UserQuery p + ] + r => JSON ::: TeamId -> Handler r Response suspendTeamH (_ ::: tid) = do empty <$ suspendTeam tid suspendTeam :: - Members '[GalleyAccess, GundeckAccess] r => + Members + '[ GalleyAccess, + GundeckAccess, + UserQuery p + ] + r => TeamId -> Handler r () suspendTeam tid = do @@ -549,14 +559,24 @@ suspendTeam tid = do lift $ wrapHttp $ Intra.changeTeamStatus tid Team.Suspended Nothing unsuspendTeamH :: - Members '[GalleyAccess, GundeckAccess] r => + Members + '[ GalleyAccess, + GundeckAccess, + UserQuery p + ] + r => JSON ::: TeamId -> Handler r Response unsuspendTeamH (_ ::: tid) = do empty <$ unsuspendTeam tid unsuspendTeam :: - Members '[GalleyAccess, GundeckAccess] r => + Members + '[ GalleyAccess, + GundeckAccess, + UserQuery p + ] + r => TeamId -> Handler r () unsuspendTeam tid = do @@ -567,7 +587,12 @@ unsuspendTeam tid = do -- Internal changeTeamAccountStatuses :: - Members '[GalleyAccess, GundeckAccess] r => + Members + '[ GalleyAccess, + GundeckAccess, + UserQuery p + ] + r => TeamId -> AccountStatus -> Handler r () diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 13fca448d2..cb51260e9b 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -53,7 +53,6 @@ import Brig.Effects.Twilio (Twilio) import Brig.Effects.UserHandleStore import Brig.Effects.UserKeyStore (UserKeyStore) import Brig.Effects.UserQuery (UserQuery) -import Brig.Effects.UserQuery.Cassandra import Brig.Effects.VerificationCodeStore (VerificationCodeStore) import Brig.Email import qualified Brig.IO.Intra as Intra @@ -100,13 +99,13 @@ data Access u = Access } sendLoginCode :: - forall r. + forall r p. Members '[ Error Twilio.ErrorResponse, P.TinyLog, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => Phone -> @@ -156,7 +155,7 @@ lookupLoginCode phone = wrapClient $ Data.lookupLoginCode u login :: - forall r. + forall r p. Members '[ BudgetStore, Error Twilio.ErrorResponse, @@ -167,7 +166,7 @@ login :: Twilio, UserHandleStore, UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -213,11 +212,11 @@ login (SmsLogin phone code label) typ = do newAccess @ZAuth.User @ZAuth.Access uid typ label verifyCode :: - forall r. + forall r p. Members '[ GalleyAccess, Input (Local ()), - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -314,12 +313,12 @@ logout uts at = do lift $ revokeCookies u [cookieId ck] [] renewAccess :: - forall u a r. + forall u a r p. ZAuth.TokenPair u a => Members '[ GalleyAccess, GundeckAccess, - UserQuery + UserQuery p ] r => List1 (ZAuth.Token u) -> @@ -334,7 +333,7 @@ renewAccess uts at = do pure $ Access at' ck' revokeAccess :: - Members '[Input (Local ()), TinyLog, UserQuery] r => + Members '[Input (Local ()), TinyLog, UserQuery p] r => UserId -> PlainTextPassword -> [CookieId] -> @@ -357,7 +356,7 @@ catchSuspendInactiveUser :: Members '[ GalleyAccess, GundeckAccess, - UserQuery + UserQuery p ] r => UserId -> @@ -380,12 +379,12 @@ catchSuspendInactiveUser uid errval = do Right () -> pure () newAccess :: - forall u a r. + forall u a r p. ZAuth.TokenPair u a => Members '[ GalleyAccess, GundeckAccess, - UserQuery + UserQuery p ] r => UserId -> @@ -403,9 +402,11 @@ newAccess uid ct cl = do resolveLoginId :: Members - '[ P.Error Twilio.ErrorResponse, + '[ Input (Local ()), + P.Error Twilio.ErrorResponse, UserHandleStore, UserKeyStore, + UserQuery p, Twilio ] r => @@ -420,7 +421,7 @@ resolveLoginId li = do (liftSem . lookupHandle) case usr of Nothing -> do - pending <- lift . wrapClient $ isPendingActivation li + pending <- lift $ isPendingActivation li throwE $ if pending then LoginPendingActivation @@ -444,24 +445,24 @@ validateLoginId (LoginByPhone phone) = validateLoginId (LoginByHandle h) = pure (Right h) -isPendingActivation :: forall m. (MonadClient m, MonadReader Env m) => LoginId -> m Bool +isPendingActivation :: + forall r p. + Members '[Input (Local ()), UserQuery p] r => + LoginId -> + AppT r Bool isPendingActivation ident = case ident of (LoginByHandle _) -> pure False (LoginByEmail e) -> checkKey (userEmailKey e) (LoginByPhone p) -> checkKey (userPhoneKey p) where checkKey k = do - usr <- (>>= fst) <$> Data.lookupActivationCode k + usr <- (>>= fst) <$> wrapClient (Data.lookupActivationCode k) locale <- Opt.setDefaultUserLocale <$> view settings - locDomain <- qualifyLocal () case usr of Nothing -> pure False Just u -> maybe False (checkAccount k) - <$> ( runM . userQueryToCassandra @m @'[Embed m] - . runInputConst locDomain - $ Data.lookupAccount locale u - ) + <$> liftSem (Data.lookupAccount locale u) checkAccount k a = let i = userIdentity (accountUser a) statusAdmitsPending = case accountStatus a of @@ -526,7 +527,7 @@ ssoLogin :: '[ GalleyAccess, GundeckAccess, Input (Local ()), - UserQuery + UserQuery p ] r => SsoLogin -> @@ -557,7 +558,7 @@ legalHoldLogin :: '[ GalleyAccess, GundeckAccess, Input (Local ()), - UserQuery + UserQuery p ] r => LegalHoldLogin -> diff --git a/services/brig/src/Brig/User/Handle.hs b/services/brig/src/Brig/User/Handle.hs index 1c4e4388a7..b419524b87 100644 --- a/services/brig/src/Brig/User/Handle.hs +++ b/services/brig/src/Brig/User/Handle.hs @@ -46,14 +46,14 @@ import Polysemy.Resource -- | Claim a new handle for an existing 'User'. claimHandle :: - forall r. + forall r p. Members '[ Async, Race, Resource, UniqueClaimsStore, UserHandleStore, - UserQuery + UserQuery p ] r => UserId -> From a3b9a936d0e87e59420c829b449d655864204289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 5 Oct 2022 22:51:46 +0200 Subject: [PATCH 39/41] Propagate effect constraints (compiles again) --- services/brig/src/Brig/API.hs | 8 +- services/brig/src/Brig/API/Connection.hs | 20 ++--- services/brig/src/Brig/API/Federation.hs | 12 +-- services/brig/src/Brig/API/Internal.hs | 48 ++++++------ services/brig/src/Brig/API/Public.hs | 78 +++++++++++-------- .../brig/src/Brig/CanonicalInterpreter.hs | 10 ++- services/brig/src/Brig/Data/User.hs | 35 +-------- services/brig/src/Brig/Effects/UserQuery.hs | 7 ++ .../src/Brig/Effects/UserQuery/Cassandra.hs | 18 +++++ .../brig/src/Brig/InternalEvent/Process.hs | 21 +++-- services/brig/src/Brig/Provider/API.hs | 56 +++++++++---- services/brig/src/Brig/User/API/Auth.hs | 34 ++++---- services/brig/src/Brig/User/API/Handle.hs | 4 +- services/brig/src/Brig/User/API/Search.hs | 6 +- services/brig/src/Brig/User/EJPD.hs | 4 +- 15 files changed, 205 insertions(+), 156 deletions(-) diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index 77f1925d36..ec31d48571 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -29,6 +29,7 @@ import Brig.Effects.ActivationSupply import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BudgetStore +import Brig.Effects.ClientStore import Brig.Effects.CodeStore import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess (GundeckAccess) @@ -52,9 +53,12 @@ import Polysemy.Input import Polysemy.Resource import qualified Polysemy.TinyLog as P import qualified Ropes.Twilio as Twilio +import Wire.Sem.Concurrency +import Wire.Sem.Paging sitemap :: forall r p. + Paging p => Members '[ ActivationKeyStore, ActivationSupply, @@ -62,7 +66,9 @@ sitemap :: BlacklistStore, BlacklistPhonePrefixStore, BudgetStore, + ClientStore, CodeStore, + Concurrency 'Unsafe, Error ReAuthError, Error Twilio.ErrorResponse, GalleyAccess, @@ -78,7 +84,7 @@ sitemap :: UserHandleStore, UserKeyStore, UserPendingActivationStore p, - UserQuery, + UserQuery p, VerificationCodeStore ] r => diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index 555280b31a..24b519b939 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -64,7 +64,7 @@ import qualified Wire.API.Error.Brig as E import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) ensureIsActivated :: - Member UserQuery r => + Member (UserQuery p) r => Local UserId -> MaybeT (AppT r) () ensureIsActivated lusr = do @@ -82,7 +82,7 @@ createConnection :: Members '[ Input (Local ()), GundeckAccess, - UserQuery + UserQuery p ] r => Local UserId -> @@ -104,11 +104,11 @@ createConnection self con target = do target createConnectionToLocalUser :: - forall r. + forall r p. Members '[ Input (Local ()), GundeckAccess, - UserQuery + UserQuery p ] r => Local UserId -> @@ -199,7 +199,7 @@ createConnectionToLocalUser self conn target = do -- FUTUREWORK: we may want to move this to the LH application logic, so we can recycle it for -- group conv creation and possibly other situations. checkLegalholdPolicyConflict :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => UserId -> UserId -> ExceptT ConnectionError (AppT r) () @@ -225,7 +225,7 @@ checkLegalholdPolicyConflict uid1 uid2 = do oneway status2 status1 updateConnection :: - Members '[GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery p] r => Local UserId -> Qualified UserId -> Relation -> @@ -245,8 +245,8 @@ updateConnection self other newStatus conn = -- because a connection between two team members can not exist in the first place. -- {#RefConnectionTeam} updateConnectionToLocalUser :: - forall r. - Members '[GundeckAccess, UserQuery] r => + forall r p. + Members '[GundeckAccess, UserQuery p] r => -- | From Local UserId -> -- | To @@ -407,8 +407,8 @@ mkRelationWithHistory oldRel = \case MissingLegalholdConsent -> error "impossible old relation" updateConnectionInternal :: - forall r. - Members '[GundeckAccess, UserQuery] r => + forall r p. + Members '[GundeckAccess, UserQuery p] r => UpdateConnectionsInternal -> ExceptT ConnectionError (AppT r) () updateConnectionInternal = \case diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index c856e9e9f0..2690536ba0 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -73,7 +73,7 @@ federationSitemap :: '[ GundeckAccess, Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => ServerT FederationAPI (Handler r) @@ -92,7 +92,7 @@ federationSitemap = :<|> Named @"claim-key-packages" fedClaimKeyPackages sendConnectionAction :: - Members '[GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery p] r => Domain -> NewConnectionRequest -> Handler r NewConnectionResponse @@ -111,7 +111,7 @@ getUserByHandle :: Members '[ Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => Domain -> @@ -136,7 +136,7 @@ getUserByHandle domain handle = do listToMaybe <$> API.lookupLocalProfiles Nothing [ownerId] getUsersByIds :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => Domain -> [UserId] -> ExceptT Error (AppT r) [UserProfile] @@ -165,11 +165,11 @@ fedClaimKeyPackages domain ckpr = do -- only search by exact handle search, not in elasticsearch. -- (This decision may change in the future) searchUsers :: - forall r. + forall r p. Members '[ Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => Domain -> diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index c50c57d721..b8b97add86 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -140,7 +140,7 @@ servantSitemap :: UserHandleStore, UserKeyStore, UserPendingActivationStore p, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -148,7 +148,7 @@ servantSitemap :: servantSitemap = ejpdAPI :<|> accountAPI :<|> mlsAPI :<|> getVerificationCode :<|> teamsAPI :<|> userAPI ejpdAPI :: - Members '[UserHandleStore, UserQuery] r => + Members '[UserHandleStore, UserQuery p] r => ServerT BrigIRoutes.EJPD_API (Handler r) ejpdAPI = Brig.User.EJPD.ejpdRequest @@ -191,7 +191,7 @@ accountAPI :: UserHandleStore, UserKeyStore, UserPendingActivationStore p, - UserQuery + UserQuery p ] r => ServerT BrigIRoutes.AccountAPI (Handler r) @@ -298,8 +298,8 @@ mapKeyPackageRefsInternal bundle = do Data.mapKeyPackageRef (kpbeRef e) (kpbeUser e) (kpbeClient e) getVerificationCode :: - forall r. - Members '[VerificationCodeStore, UserQuery] r => + forall r p. + Members '[VerificationCodeStore, UserQuery p] r => UserId -> VerificationAction -> Handler r (Maybe Code.Value) @@ -344,7 +344,7 @@ sitemap :: UserHandleStore, UserKeyStore, UserPendingActivationStore p, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -516,7 +516,7 @@ addClientInternalH :: '[ GalleyAccess, GundeckAccess, Input (Local ()), - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -531,7 +531,7 @@ addClientInternal :: '[ GalleyAccess, GundeckAccess, Input (Local ()), - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -594,7 +594,7 @@ createUserNoVerify :: Twilio, UserKeyStore, UserPendingActivationStore p, - UserQuery + UserQuery p ] r => NewUser -> @@ -629,7 +629,7 @@ createUserNoVerifySpar :: UniqueClaimsStore, UserHandleStore, UserKeyStore, - UserQuery + UserQuery p ] r => NewUserSpar -> @@ -656,7 +656,7 @@ deleteUserNoAuthH :: UniqueClaimsStore, UserHandleStore, UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -674,7 +674,7 @@ changeSelfEmailMaybeSendH :: ActivationSupply, BlacklistStore, UserKeyStore, - UserQuery + UserQuery p ] r => UserId ::: Bool ::: JsonRequest EmailUpdate -> @@ -693,7 +693,7 @@ changeSelfEmailMaybeSend :: ActivationSupply, BlacklistStore, UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -712,7 +712,7 @@ listActivatedAccountsH :: Members '[ Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => JSON ::: Either (List UserId) (List Handle) ::: Bool -> @@ -721,11 +721,11 @@ listActivatedAccountsH (_ ::: qry ::: includePendingInvitations) = do json <$> lift (listActivatedAccounts qry includePendingInvitations) listActivatedAccounts :: - forall r. + forall r p. Members '[ Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => Either (List UserId) (List Handle) -> @@ -764,7 +764,7 @@ listActivatedAccounts elh includePendingInvitations = do (Ephemeral, _, _) -> pure True listAccountsByIdentityH :: - Members '[Input (Local ()), UserKeyStore, UserQuery] r => + Members '[Input (Local ()), UserKeyStore, UserQuery p] r => JSON ::: Either Email Phone ::: Bool -> Handler r Response listAccountsByIdentityH (_ ::: emailOrPhone ::: includePendingInvitations) = @@ -816,7 +816,7 @@ instance ToJSON GetPasswordResetCodeResp where toJSON (GetPasswordResetCodeResp (k, c)) = object ["key" .= k, "code" .= c] changeAccountStatusH :: - Members '[GalleyAccess, GundeckAccess, UserQuery] r => + Members '[GalleyAccess, GundeckAccess, UserQuery p] r => UserId ::: JsonRequest AccountStatusUpdate -> Handler r Response changeAccountStatusH (usr ::: req) = do @@ -861,7 +861,7 @@ revokeIdentityH :: GundeckAccess, Input (Local ()), UserKeyStore, - UserQuery + UserQuery p ] r => Either Email Phone -> @@ -871,7 +871,7 @@ revokeIdentityH emailOrPhone = do pure $ setStatus status200 empty updateConnectionInternalH :: - Members '[GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery p] r => JSON ::: JsonRequest UpdateConnectionsInternal -> (Handler r) Response updateConnectionInternalH (_ ::: req) = do @@ -996,7 +996,7 @@ updateHandleH :: Resource, UniqueClaimsStore, UserHandleStore, - UserQuery + UserQuery p ] r => UserId ::: JSON ::: JsonRequest HandleUpdate -> @@ -1012,7 +1012,7 @@ updateHandle :: Resource, UniqueClaimsStore, UserHandleStore, - UserQuery + UserQuery p ] r => UserId -> @@ -1023,13 +1023,13 @@ updateHandle uid (HandleUpdate handleUpd) = do API.changeHandle uid Nothing handle API.AllowSCIMUpdates !>> changeHandleError updateUserNameH :: - Members '[GalleyAccess, GundeckAccess, UserQuery] r => + Members '[GalleyAccess, GundeckAccess, UserQuery p] r => UserId ::: JSON ::: JsonRequest NameUpdate -> (Handler r) Response updateUserNameH (uid ::: _ ::: body) = empty <$ (updateUserName uid =<< parseJsonBody body) updateUserName :: - Members '[GalleyAccess, GundeckAccess, UserQuery] r => + Members '[GalleyAccess, GundeckAccess, UserQuery p] r => UserId -> NameUpdate -> (Handler r) () diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index aa7e7abde7..fae6f75672 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -50,6 +50,7 @@ import Brig.Effects.ActivationSupply import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BudgetStore +import Brig.Effects.ClientStore (ClientStore) import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess (GundeckAccess) @@ -155,7 +156,9 @@ import qualified Wire.API.User.Password as Public import qualified Wire.API.User.RichInfo as Public import qualified Wire.API.UserMap as Public import qualified Wire.API.Wrapped as Public +import Wire.Sem.Concurrency import Wire.Sem.Now (Now) +import Wire.Sem.Paging -- User API ----------------------------------------------------------- @@ -205,7 +208,7 @@ servantSitemap :: UserHandleStore, UserKeyStore, UserPendingActivationStore p, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -337,13 +340,17 @@ servantSitemap = -- - MemberLeave event to members for all conversations the user was in (via galley) sitemap :: + forall r p. + Paging p => Members '[ ActivationKeyStore, ActivationSupply, BlacklistStore, BlacklistPhonePrefixStore, BudgetStore, + ClientStore, CodeStore, + Concurrency 'Unsafe, GalleyAccess, GundeckAccess, Input (Local ()), @@ -355,7 +362,7 @@ sitemap :: UniqueClaimsStore, UserHandleStore, UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -367,14 +374,17 @@ sitemap = do Calling.routesPublic apiDocs :: - forall r. + forall r p. + Paging p => Members '[ ActivationKeyStore, ActivationSupply, BlacklistStore, BlacklistPhonePrefixStore, BudgetStore, + ClientStore, CodeStore, + Concurrency 'Unsafe, GalleyAccess, GundeckAccess, Input (Local ()), @@ -386,7 +396,7 @@ apiDocs :: UniqueClaimsStore, UserHandleStore, UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -517,7 +527,7 @@ addClient :: '[ GalleyAccess, GundeckAccess, Input (Local ()), - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -541,7 +551,7 @@ deleteClient :: '[ GundeckAccess, Input (Local ()), P.Error ReAuthError, - UserQuery + UserQuery p ] r => UserId -> @@ -594,7 +604,7 @@ getClientCapabilities uid cid = do maybe (throwStd (errorToWai @'E.ClientNotFound)) (pure . Public.clientCapabilities) mclient getRichInfo :: - Member UserQuery r => + Member (UserQuery p) r => UserId -> UserId -> Handler r Public.RichInfoAssocList @@ -658,7 +668,7 @@ createUser :: Twilio, UserKeyStore, UserPendingActivationStore p, - UserQuery + UserQuery p ] r => Public.NewUserPublic -> @@ -732,7 +742,7 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do Team.sendMemberWelcomeMail e t n l getSelf :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => UserId -> Handler r Public.SelfProfile getSelf self = @@ -740,7 +750,7 @@ getSelf self = >>= ifNothing (errorToWai @'E.UserNotFound) getUserUnqualifiedH :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => UserId -> UserId -> Handler r (Maybe Public.UserProfile) @@ -749,7 +759,7 @@ getUserUnqualifiedH self uid = do getUser self (Qualified uid domain) getUser :: - Members '[Input (Local ()), UserQuery] r => + Members '[Input (Local ()), UserQuery p] r => UserId -> Qualified UserId -> Handler r (Maybe Public.UserProfile) @@ -762,7 +772,7 @@ listUsersByUnqualifiedIdsOrHandles :: Members '[ Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => UserId -> @@ -785,11 +795,11 @@ listUsersByUnqualifiedIdsOrHandles self mUids mHandles = do (Nothing, Nothing) -> throwStd $ badRequest "at least one ids or handles must be provided" listUsersByIdsOrHandles :: - forall r. + forall r p. Members '[ Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => UserId -> @@ -826,7 +836,7 @@ updateUser :: Members '[ GalleyAccess, GundeckAccess, - UserQuery + UserQuery p ] r => UserId -> @@ -846,7 +856,7 @@ changePhone :: P.Error Twilio.ErrorResponse, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -866,7 +876,7 @@ removePhone :: GundeckAccess, Input (Local ()), UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -881,7 +891,7 @@ removeEmail :: GundeckAccess, Input (Local ()), UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -894,7 +904,7 @@ checkPasswordExists :: UserId -> (Handler r) Bool checkPasswordExists = fmap isJust . lift . wrapClient . API.lookupPassword changePassword :: - Members '[UserQuery] r => + Members '[UserQuery p] r => UserId -> Public.PasswordChange -> Handler r (Maybe Public.ChangePasswordError) @@ -940,7 +950,7 @@ getHandleInfoUnqualifiedH :: Members '[ Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => UserId -> @@ -960,7 +970,7 @@ changeHandle :: Resource, UniqueClaimsStore, UserHandleStore, - UserQuery + UserQuery p ] r => UserId -> @@ -976,7 +986,7 @@ beginPasswordReset :: '[ P.TinyLog, PasswordResetStore, UserKeyStore, - UserQuery + UserQuery p ] r => Public.NewPasswordReset -> @@ -1014,7 +1024,7 @@ sendActivationCode :: P.Error Twilio.ErrorResponse, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => Public.SendActivationCode -> @@ -1043,7 +1053,7 @@ createConnectionUnqualified :: Members '[ GundeckAccess, Input (Local ()), - UserQuery + UserQuery p ] r => UserId -> @@ -1059,7 +1069,7 @@ createConnection :: Members '[ GundeckAccess, Input (Local ()), - UserQuery + UserQuery p ] r => UserId -> @@ -1071,7 +1081,7 @@ createConnection self conn target = do API.createConnection lself conn target !>> connError updateLocalConnection :: - Members '[GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery p] r => UserId -> ConnId -> UserId -> @@ -1082,7 +1092,7 @@ updateLocalConnection self conn other update = do updateConnection self conn (qUntagged lother) update updateConnection :: - Members '[GundeckAccess, UserQuery] r => + Members '[GundeckAccess, UserQuery p] r => UserId -> ConnId -> Qualified UserId -> @@ -1160,7 +1170,7 @@ deleteSelfUser :: UniqueClaimsStore, UserHandleStore, UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -1178,7 +1188,7 @@ verifyDeleteUser :: UniqueClaimsStore, UserHandleStore, UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -1192,7 +1202,7 @@ updateUserEmail :: ActivationSupply, BlacklistStore, UserKeyStore, - UserQuery + UserQuery p ] r => UserId -> @@ -1233,7 +1243,7 @@ activate :: PasswordResetStore, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => Public.ActivationKey -> @@ -1256,7 +1266,7 @@ activateKey :: PasswordResetStore, Twilio, UserKeyStore, - UserQuery + UserQuery p ] r => Public.Activate -> @@ -1275,11 +1285,11 @@ activateKey (Public.Activate tgt code dryrun) respond Nothing _ = ActivationRespSuccessNoIdent sendVerificationCode :: - forall r. + forall r p. Members '[ Input (Local ()), UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index bdcacdc642..c2a4b32242 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -34,6 +34,8 @@ import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BlacklistStore.Cassandra (interpretBlacklistStoreToCassandra) import Brig.Effects.BudgetStore (BudgetStore) import Brig.Effects.BudgetStore.Cassandra +import Brig.Effects.ClientStore (ClientStore) +import Brig.Effects.ClientStore.Cassandra import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.CodeStore.Cassandra (codeStoreToCassandra) import Brig.Effects.Common @@ -79,6 +81,8 @@ import Polysemy.Input import Polysemy.Resource (Resource, resourceToIO) import qualified Polysemy.TinyLog as P import qualified Ropes.Twilio as Twilio +import Wire.Sem.Concurrency +import Wire.Sem.Concurrency.IO import Wire.Sem.Error import Wire.Sem.Logger.TinyLog import Wire.Sem.Now (Now) @@ -87,7 +91,8 @@ import Wire.Sem.Paging.Cassandra (InternalPaging) import qualified Wire.Sem.Paging.Cassandra as PC type BrigCanonicalEffects = - '[ PublicKeyBundle, + '[ ClientStore, + PublicKeyBundle, JwtTools, BlacklistPhonePrefixStore, BlacklistStore, @@ -116,6 +121,7 @@ type BrigCanonicalEffects = Embed Cas.Client, P.Error Twilio.ErrorResponse, P.Error ReAuthError, + Concurrency 'Unsafe, Embed IO, Final IO ] @@ -124,6 +130,7 @@ runBrigToIO :: Env -> AppT BrigCanonicalEffects a -> IO a runBrigToIO e (AppT ma) = runFinal . embedToFinal + . unsafelyPerformConcurrency . interpretWaiErrorToException . interpretErrorToException twilioToWai . interpretClientToIO (e ^. casClient) @@ -153,6 +160,7 @@ runBrigToIO e (AppT ma) = . interpretBlacklistPhonePrefixStoreToCassandra @Cas.Client . interpretJwtTools . interpretPublicKeyBundle + . clientStoreToCassandra @HttpClientIO $ runReaderT ma e interpretHttpToIO :: diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 042c235fa2..be2ec43304 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -45,8 +45,8 @@ module Brig.Data.User lookupRichInfo, lookupRichInfoMultiUsers, lookupUserTeam, - lookupServiceUsers, - lookupServiceUsersForTeam, + getServiceUsers, + getServiceUsersForTeam, lookupFeatureConferenceCalling, userExists, @@ -87,6 +87,8 @@ import Brig.Effects.UserQuery getId, getLocale, getName, + getServiceUsers, + getServiceUsersForTeam, getUsers, insertAccount, isActivated, @@ -105,7 +107,6 @@ import qualified Brig.ZAuth as ZAuth import Cassandra import Control.Error import Control.Lens hiding (from) -import Data.Conduit (ConduitM) import Data.Domain import Data.Handle (Handle) import Data.Id @@ -390,34 +391,6 @@ lookupUsers :: lookupUsers loc locale hpi usrs = toUsers (tDomain loc) locale hpi <$> getUsers usrs --- | NB: might return a lot of users, and therefore we do streaming here (page-by-page). -lookupServiceUsers :: - MonadClient m => - ProviderId -> - ServiceId -> - ConduitM () [(BotId, ConvId, Maybe TeamId)] m () -lookupServiceUsers pid sid = - paginateC cql (paramsP LocalQuorum (pid, sid) 100) x1 - where - cql :: PrepQuery R (ProviderId, ServiceId) (BotId, ConvId, Maybe TeamId) - cql = - "SELECT user, conv, team FROM service_user \ - \WHERE provider = ? AND service = ?" - -lookupServiceUsersForTeam :: - MonadClient m => - ProviderId -> - ServiceId -> - TeamId -> - ConduitM () [(BotId, ConvId)] m () -lookupServiceUsersForTeam pid sid tid = - paginateC cql (paramsP LocalQuorum (pid, sid, tid) 100) x1 - where - cql :: PrepQuery R (ProviderId, ServiceId, TeamId) (BotId, ConvId) - cql = - "SELECT user, conv FROM service_team \ - \WHERE provider = ? AND service = ? AND team = ?" - lookupFeatureConferenceCalling :: MonadClient m => UserId -> m (Maybe (ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig)) lookupFeatureConferenceCalling uid = do let q = query1 select (params LocalQuorum (Identity uid)) diff --git a/services/brig/src/Brig/Effects/UserQuery.hs b/services/brig/src/Brig/Effects/UserQuery.hs index 36229bd611..6a4653b14c 100644 --- a/services/brig/src/Brig/Effects/UserQuery.hs +++ b/services/brig/src/Brig/Effects/UserQuery.hs @@ -22,6 +22,7 @@ module Brig.Effects.UserQuery getId, getUsers, getServiceUsers, + getServiceUsersForTeam, getName, getLocale, getAuthentication, @@ -293,6 +294,12 @@ data UserQuery p m a where ServiceId -> Maybe (PagingState p BotInConv) -> UserQuery p m (Page p BotInConv) + GetServiceUsersForTeam :: -- lookupServiceUsersForTeam + ProviderId -> + ServiceId -> + TeamId -> + Maybe (PagingState p (BotId, ConvId)) -> + UserQuery p m (Page p (BotId, ConvId)) GetName :: UserId -> UserQuery p m (Maybe Name) -- nameSelect GetLocale :: UserId -> UserQuery p m (Maybe (Maybe Language, Maybe Country)) -- localeSelect GetAuthentication :: UserId -> UserQuery p m (Maybe (Maybe Password, Maybe AccountStatus)) -- authSelect diff --git a/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs b/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs index efe26a7292..327b138437 100644 --- a/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs +++ b/services/brig/src/Brig/Effects/UserQuery/Cassandra.hs @@ -44,6 +44,10 @@ userQueryToCassandra = GetUsers uids -> retry x1 (query usersSelect (params LocalQuorum (Identity uids))) GetServiceUsers pid sid Nothing -> flip PC.mkInternalPage pure =<< lookupServiceUsers pid sid GetServiceUsers _pid _sid (Just ps) -> PC.ipNext ps + GetServiceUsersForTeam pid sid tid Nothing -> + flip PC.mkInternalPage pure + =<< lookupServiceUsersForTeam pid sid tid + GetServiceUsersForTeam _pid _sid _tid (Just ps) -> PC.ipNext ps GetName uid -> runIdentity <$$> retry x1 (query1 nameSelect (params LocalQuorum (Identity uid))) GetLocale uid -> retry x1 (query1 localeSelect (params LocalQuorum (Identity uid))) GetAuthentication uid -> retry x1 (query1 authSelect (params LocalQuorum (Identity uid))) @@ -278,3 +282,17 @@ lookupServiceUsers pid sid = cql = "SELECT user, conv, team FROM service_user \ \WHERE provider = ? AND service = ?" + +lookupServiceUsersForTeam :: + MonadClient m => + ProviderId -> + ServiceId -> + TeamId -> + m (Page (BotId, ConvId)) +lookupServiceUsersForTeam pid sid tid = + retry x1 (paginate cql (params LocalQuorum (pid, sid, tid))) + where + cql :: PrepQuery R (ProviderId, ServiceId, TeamId) (BotId, ConvId) + cql = + "SELECT user, conv FROM service_team \ + \WHERE provider = ? AND service = ? AND team = ?" diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index ff2cb0b6f0..df52303741 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -23,6 +23,7 @@ where import qualified Brig.API.User as API import Brig.App import qualified Brig.Data.Client as Data +import Brig.Effects.ClientStore (ClientStore) import Brig.Effects.GalleyAccess (GalleyAccess) import Brig.Effects.GundeckAccess (GundeckAccess) import Brig.Effects.UniqueClaimsStore @@ -47,17 +48,20 @@ import Polysemy.Input import Polysemy.Time.Data.TimeUnit import qualified Polysemy.TinyLog as P import System.Logger.Class (field, msg, val, (~~)) -import qualified System.Logger.Class as Log -import UnliftIO (timeout) -import Wire.API.User.Client (clientId) +import Wire.API.User.Client +import Wire.Sem.Concurrency +import Wire.Sem.Paging -- | Handle an internal event. -- -- Has a one-minute timeout that should be enough for anything that it does. onEvent :: - forall r. + forall r p. + Paging p => Members - '[ GalleyAccess, + '[ ClientStore, + Concurrency 'Unsafe, + GalleyAccess, GundeckAccess, Input (Local ()), P.TinyLog, @@ -65,7 +69,7 @@ onEvent :: UniqueClaimsStore, UserHandleStore, UserKeyStore, - UserQuery + UserQuery p ] r => InternalNotification -> @@ -74,7 +78,8 @@ onEvent n = do locale <- setDefaultUserLocale <$> view settings delay <- fromMaybe defDeleteThrottleMillis . setDeleteThrottleMillis <$> view settings handleTimeout $ case n of - DeleteClient cid uid mcon -> do + DeleteClient client uid mcon -> do + let cid = clientId client mc <- wrapClient $ Data.lookupClient uid cid for_ mc $ \c -> do wrapHttp $ rmClient uid cid @@ -94,7 +99,7 @@ onEvent n = do msg (val "Processing service delete event") ~~ field "provider" (toByteString pid) ~~ field "service" (toByteString sid) - wrapHttpClient $ API.finishDeleteService pid sid + API.finishDeleteService pid sid where handleTimeout :: AppT r a -> AppT r a handleTimeout _act = undefined diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 593290a587..298ff6a09c 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -38,7 +38,7 @@ import qualified Brig.Data.Client as User import qualified Brig.Data.User as User import Brig.Effects.ClientStore import Brig.Effects.GalleyAccess -import Brig.Effects.UserQuery (UserQuery, getServiceUsers) +import Brig.Effects.UserQuery (UserQuery, getServiceUsers, getServiceUsersForTeam) import Brig.Effects.VerificationCodeStore import Brig.Email (mkEmailKey) import qualified Brig.IO.Intra as RPC @@ -64,8 +64,6 @@ import Control.Monad.Except import Data.Aeson hiding (json) import Data.ByteString.Conversion import qualified Data.ByteString.Lazy.Char8 as LC8 -import Data.Conduit (runConduit, (.|)) -import qualified Data.Conduit.List as C import Data.Hashable (hash) import Data.Id import Data.LegalHold @@ -99,7 +97,6 @@ import Polysemy import Polysemy.Input import qualified Ssl.Util as SSL import System.Logger.Class (MonadLogger) -import UnliftIO.Async (pooledMapConcurrentlyN_) import qualified Web.Cookie as Cookie import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Bot @@ -130,8 +127,10 @@ import Wire.Sem.Concurrency import Wire.Sem.Paging routesPublic :: + Paging p => Members '[ ClientStore, + Concurrency 'Unsafe, GalleyAccess, Input (Local ()), UserQuery p, @@ -905,7 +904,18 @@ getServiceTagList () = pure (Public.ServiceTagList allTags) where allTags = [(minBound :: Public.ServiceTag) ..] -updateServiceWhitelistH :: UserId ::: ConnId ::: TeamId ::: JsonRequest Public.UpdateServiceWhitelist -> (Handler r) Response +updateServiceWhitelistH :: + Paging p => + Members + '[ ClientStore, + Concurrency 'Unsafe, + GalleyAccess, + Input (Local ()), + UserQuery p + ] + r => + UserId ::: ConnId ::: TeamId ::: JsonRequest Public.UpdateServiceWhitelist -> + Handler r Response updateServiceWhitelistH (uid ::: con ::: tid ::: req) = do mapExceptT wrapHttp $ guardSecondFactorDisabled (Just uid) resp <- updateServiceWhitelist uid con tid =<< parseJsonBody req @@ -918,7 +928,22 @@ data UpdateServiceWhitelistResp = UpdateServiceWhitelistRespChanged | UpdateServiceWhitelistRespUnchanged -updateServiceWhitelist :: UserId -> ConnId -> TeamId -> Public.UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp +updateServiceWhitelist :: + forall r p. + Paging p => + Members + '[ ClientStore, + Concurrency 'Unsafe, + GalleyAccess, + Input (Local ()), + UserQuery p + ] + r => + UserId -> + ConnId -> + TeamId -> + Public.UpdateServiceWhitelist -> + (Handler r) UpdateServiceWhitelistResp updateServiceWhitelist uid con tid upd = do let pid = updateServiceWhitelistProvider upd sid = updateServiceWhitelistService upd @@ -938,17 +963,14 @@ updateServiceWhitelist uid con tid upd = do (True, False) -> do -- When the service is de-whitelisted, remove its bots from team -- conversations - -- lift $ - -- fmap - -- wrapHttpClient - -- runConduit $ - -- User.lookupServiceUsersForTeam pid sid tid - -- .| C.mapM_ - -- ( pooledMapConcurrentlyN_ - -- 16 - -- ( uncurry (deleteBot locale uid (Just con)) - -- ) - -- ) + lift . liftSem $ + withChunks @p + (getServiceUsersForTeam pid sid tid) + ( unsafePooledMapConcurrentlyN_ + 16 + ( uncurry (deleteBot locale uid (Just con)) + ) + ) wrapClientE $ DB.deleteServiceWhitelist (Just tid) pid sid pure UpdateServiceWhitelistRespChanged diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index 043bb15310..8ad3daf5b0 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -99,7 +99,7 @@ routesPublic :: Twilio, UserHandleStore, UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -241,7 +241,7 @@ routesInternal :: Input (Local ()), P.TinyLog, UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -272,7 +272,7 @@ sendLoginCodeH :: '[ Error Twilio.ErrorResponse, P.TinyLog, UserKeyStore, - UserQuery, + UserQuery p, Twilio ] r => @@ -286,7 +286,7 @@ sendLoginCode :: '[ Error Twilio.ErrorResponse, P.TinyLog, UserKeyStore, - UserQuery, + UserQuery p, Twilio ] r => @@ -316,7 +316,7 @@ reAuthUserH :: '[ Error ReAuthError, GalleyAccess, Input (Local ()), - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -331,7 +331,7 @@ reAuthUser :: '[ Error ReAuthError, GalleyAccess, Input (Local ()), - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -361,7 +361,7 @@ loginH :: Twilio, UserHandleStore, UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -381,7 +381,7 @@ login :: Twilio, UserHandleStore, UserKeyStore, - UserQuery, + UserQuery p, VerificationCodeStore ] r => @@ -397,7 +397,7 @@ ssoLoginH :: '[ GalleyAccess, GundeckAccess, Input (Local ()), - UserQuery + UserQuery p ] r => JsonRequest SsoLogin ::: Bool ::: JSON -> @@ -410,7 +410,7 @@ ssoLogin :: '[ GalleyAccess, GundeckAccess, Input (Local ()), - UserQuery + UserQuery p ] r => SsoLogin -> @@ -425,7 +425,7 @@ legalHoldLoginH :: '[ GalleyAccess, GundeckAccess, Input (Local ()), - UserQuery + UserQuery p ] r => JsonRequest LegalHoldLogin ::: JSON -> @@ -438,7 +438,7 @@ legalHoldLogin :: '[ GalleyAccess, GundeckAccess, Input (Local ()), - UserQuery + UserQuery p ] r => LegalHoldLogin -> @@ -469,7 +469,7 @@ changeSelfEmailH :: ActivationSupply, BlacklistStore, UserKeyStore, - UserQuery + UserQuery p ] r => JSON @@ -510,14 +510,14 @@ listCookies u ll = do Public.CookieList <$> wrapClient (Auth.listCookies u (maybe [] fromList ll)) rmCookiesH :: - Members '[Input (Local ()), TinyLog, UserQuery] r => + Members '[Input (Local ()), TinyLog, UserQuery p] r => UserId ::: JsonRequest Public.RemoveCookies -> Handler r Response rmCookiesH (uid ::: req) = do empty <$ (rmCookies uid =<< parseJsonBody req) rmCookies :: - Members '[Input (Local ()), TinyLog, UserQuery] r => + Members '[Input (Local ()), TinyLog, UserQuery p] r => UserId -> Public.RemoveCookies -> Handler r () @@ -528,7 +528,7 @@ renewH :: Members '[ GalleyAccess, GundeckAccess, - UserQuery + UserQuery p ] r => JSON ::: Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) ::: Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> @@ -544,7 +544,7 @@ renew :: Members '[ GalleyAccess, GundeckAccess, - UserQuery + UserQuery p ] r => Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) -> diff --git a/services/brig/src/Brig/User/API/Handle.hs b/services/brig/src/Brig/User/API/Handle.hs index 0790141b71..8bfa1cfe6f 100644 --- a/services/brig/src/Brig/User/API/Handle.hs +++ b/services/brig/src/Brig/User/API/Handle.hs @@ -50,7 +50,7 @@ getHandleInfo :: Members '[ Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => UserId -> @@ -75,7 +75,7 @@ getLocalHandleInfo :: Members '[ Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => Local UserId -> diff --git a/services/brig/src/Brig/User/API/Search.hs b/services/brig/src/Brig/User/API/Search.hs index c083bc6a5e..964da9b285 100644 --- a/services/brig/src/Brig/User/API/Search.hs +++ b/services/brig/src/Brig/User/API/Search.hs @@ -91,7 +91,7 @@ search :: Members '[ Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => UserId -> @@ -125,11 +125,11 @@ searchRemotely domain searchTerm = do } searchLocally :: - forall r. + forall r p. Members '[ Input (Local ()), UserHandleStore, - UserQuery + UserQuery p ] r => UserId -> diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index d6460c137c..c4ef255469 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -45,8 +45,8 @@ import qualified Wire.API.Team.Member as Team import Wire.API.User (Locale, User, userDisplayName, userEmail, userHandle, userId, userPhone, userTeam) ejpdRequest :: - forall r. - Members '[UserHandleStore, UserQuery] r => + forall r p. + Members '[UserHandleStore, UserQuery p] r => Maybe Bool -> EJPDRequestBody -> (Handler r) EJPDResponseBody From 995ac55adda630e7532457ca9d1aa69d777aa8bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 6 Oct 2022 08:07:11 +0200 Subject: [PATCH 40/41] Use a ClientStore action instead of its interpretation --- services/brig/src/Brig/API/Client.hs | 15 ++++++++++----- services/brig/src/Brig/API/Internal.hs | 13 +++++++++---- services/brig/src/Brig/API/Public.hs | 14 +++++++++----- services/brig/src/Brig/API/User.hs | 15 ++++++++++----- services/brig/src/Brig/Data/Client.hs | 3 +-- services/brig/src/Brig/InternalEvent/Process.hs | 3 ++- 6 files changed, 41 insertions(+), 22 deletions(-) diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 3419aba0c2..2f9d3406d4 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -54,6 +54,7 @@ import Brig.App import qualified Brig.Data.Client as Data import Brig.Data.Nonce as Nonce import qualified Brig.Data.User as Data +import Brig.Effects.ClientStore import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess (GundeckAccess) import Brig.Effects.JwtTools (JwtTools) @@ -153,7 +154,8 @@ lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk addClient :: Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, Input (Local ()), UserQuery p, @@ -172,7 +174,8 @@ addClient = addClientWithReAuthPolicy Data.reAuthForNewClients addClientWithReAuthPolicy :: forall r p. Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, Input (Local ()), UserQuery p, @@ -247,7 +250,8 @@ updateClient u c r = do -- a superset of the clients known to galley. rmClient :: Members - '[ Error ReAuthError, + '[ ClientStore, + Error ReAuthError, GundeckAccess, Input (Local ()), UserQuery p @@ -422,12 +426,13 @@ claimLocalMultiPrekeyBundles protectee userClients = do -- | Enqueue an orderly deletion of an existing client. execDelete :: + Members '[ClientStore] r => UserId -> Maybe ConnId -> Client -> AppT r () execDelete u con c = do - wrapClient $ Data.rmClient u (clientId c) + liftSem $ deleteClient u (clientId c) for_ (clientCookie c) $ \l -> wrapClient $ Auth.revokeCookies u [] [l] queue <- view internalEvents Queue.enqueue queue (Internal.DeleteClient c u con) @@ -484,7 +489,7 @@ legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPreke lhClientEvent = LegalHoldClientRequested eventData removeLegalHoldClient :: - Members '[GalleyAccess, GundeckAccess] r => + Members '[ClientStore, GalleyAccess, GundeckAccess] r => UserId -> AppT r () removeLegalHoldClient uid = do diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index b8b97add86..c9965485cf 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -44,6 +44,7 @@ import Brig.Effects.ActivationKeyStore import Brig.Effects.ActivationSupply import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) +import Brig.Effects.ClientStore (ClientStore) import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess (GundeckAccess) @@ -328,6 +329,7 @@ sitemap :: Async, BlacklistPhonePrefixStore, BlacklistStore, + ClientStore, CodeStore, GalleyAccess, GundeckAccess, @@ -513,7 +515,8 @@ sitemap = do -- | Add a client without authentication checks addClientInternalH :: Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, Input (Local ()), UserQuery p, @@ -528,7 +531,8 @@ addClientInternalH (usr ::: mSkipReAuth ::: req ::: connId ::: _) = do addClientInternal :: Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, Input (Local ()), UserQuery p, @@ -556,7 +560,7 @@ legalHoldClientRequestedH (targetUser ::: req ::: _) = do pure $ setStatus status200 empty removeLegalHoldClientH :: - Members '[GalleyAccess, GundeckAccess] r => + Members '[ClientStore, GalleyAccess, GundeckAccess] r => UserId ::: JSON -> Handler r Response removeLegalHoldClientH (uid ::: _) = do @@ -650,7 +654,8 @@ createUserNoVerifySpar uData = deleteUserNoAuthH :: Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index fae6f75672..4eab23eef7 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -188,7 +188,7 @@ servantSitemap :: Async, BlacklistPhonePrefixStore, BlacklistStore, - CodeStore, + ClientStore, CodeStore, GalleyAccess, GundeckAccess, @@ -524,7 +524,8 @@ getMultiUserPrekeyBundleH zusr qualUserClients = do addClient :: Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, Input (Local ()), UserQuery p, @@ -548,7 +549,8 @@ addClient usr con ip new = do deleteClient :: Members - '[ GundeckAccess, + '[ ClientStore, + GundeckAccess, Input (Local ()), P.Error ReAuthError, UserQuery p @@ -1164,7 +1166,8 @@ getConnection self other = do deleteSelfUser :: Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, @@ -1182,7 +1185,8 @@ deleteSelfUser u body = verifyDeleteUser :: Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 3bf90d537b..4474b2f04c 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -112,6 +112,7 @@ import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import qualified Brig.Effects.BlacklistPhonePrefixStore as BlacklistPhonePrefixStore import Brig.Effects.BlacklistStore (BlacklistStore) import qualified Brig.Effects.BlacklistStore as BlacklistStore +import Brig.Effects.ClientStore import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess import Brig.Effects.PasswordResetStore (PasswordResetStore) @@ -1401,7 +1402,8 @@ mkPasswordResetKey ident = case ident of -- TODO: communicate deletions of SSO users to SSO service. deleteSelfUser :: Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, @@ -1490,7 +1492,8 @@ deleteSelfUser uid pwd = do -- 'deleteUser'. Called via @post /delete@. verifyDeleteUser :: Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, @@ -1516,7 +1519,8 @@ verifyDeleteUser d = do -- Called via @delete /i/user/:uid@. ensureAccountDeleted :: Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, Input (Local ()), UniqueClaimsStore, @@ -1566,7 +1570,8 @@ ensureAccountDeleted uid = do deleteAccount :: forall r p. Members - '[ GalleyAccess, + '[ ClientStore, + GalleyAccess, GundeckAccess, UniqueClaimsStore, UserHandleStore, @@ -1589,7 +1594,7 @@ deleteAccount account@(accountUser -> user) = do tombstone <- mkTombstone liftSem $ Data.insertAccount tombstone Nothing Nothing False wrapHttp $ Intra.rmUser uid (userAssets user) - wrapClient (Data.lookupClients uid >>= mapM_ (Data.rmClient uid . clientId)) + liftSem (lookupClients uid >>= mapM_ (deleteClient uid . clientId)) luid <- qualifyLocal uid Intra.onUserEvent uid Nothing (UserDeleted (qUntagged luid)) -- Note: Connections can only be deleted afterwards, since diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 3e52dc7489..55fa90e70f 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -26,7 +26,6 @@ module Brig.Data.Client reAuthForNewClients, addClientWithReAuthPolicy, addClient, - rmClient, hasClient, lookupClient, lookupClients, @@ -55,7 +54,7 @@ import Bilge.Retry (httpHandlers) import Brig.AWS import Brig.App import Brig.Data.Instances () -import Brig.Effects.ClientStore.Cassandra (key, lookupClients, rmClient, toClient) +import Brig.Effects.ClientStore.Cassandra (key, lookupClients, toClient) -- import Brig.Data.User (AuthError (..), ReAuthError (..)) -- import qualified Brig.Data.User as User -- import Brig.Options (setDefaultUserLocale) diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index df52303741..e4ba2fd9ca 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -24,6 +24,7 @@ import qualified Brig.API.User as API import Brig.App import qualified Brig.Data.Client as Data import Brig.Effects.ClientStore (ClientStore) +import qualified Brig.Effects.ClientStore as E import Brig.Effects.GalleyAccess (GalleyAccess) import Brig.Effects.GundeckAccess (GundeckAccess) import Brig.Effects.UniqueClaimsStore @@ -83,7 +84,7 @@ onEvent n = do mc <- wrapClient $ Data.lookupClient uid cid for_ mc $ \c -> do wrapHttp $ rmClient uid cid - wrapClient $ Data.rmClient uid cid + liftSem $ E.deleteClient uid cid liftSem $ Intra.onClientEvent uid mcon (ClientRemoved uid c) DeleteUser uid -> do liftSem . P.info $ From fa52ece0b787fd9f65112f17b04f7d79ab07002b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 6 Oct 2022 10:40:20 +0200 Subject: [PATCH 41/41] Introduce the CookieStore effect --- services/brig/brig.cabal | 2 + services/brig/src/Brig/API.hs | 2 + services/brig/src/Brig/API/Client.hs | 16 +++- services/brig/src/Brig/API/Internal.hs | 24 +++++- services/brig/src/Brig/API/Public.hs | 12 ++- services/brig/src/Brig/API/User.hs | 47 +++++++---- .../brig/src/Brig/CanonicalInterpreter.hs | 6 +- services/brig/src/Brig/Effects/CookieStore.hs | 31 ++++++++ .../src/Brig/Effects/CookieStore/Cassandra.hs | 76 ++++++++++++++++++ .../brig/src/Brig/InternalEvent/Process.hs | 2 + services/brig/src/Brig/Team/API.hs | 24 ++++-- services/brig/src/Brig/User/API/Auth.hs | 78 +++++++++++++++---- services/brig/src/Brig/User/Auth.hs | 49 ++++++++---- services/brig/src/Brig/User/Auth/Cookie.hs | 55 ++++++------- services/brig/src/Brig/User/Auth/DB/Cookie.hs | 38 +-------- 15 files changed, 341 insertions(+), 121 deletions(-) create mode 100644 services/brig/src/Brig/Effects/CookieStore.hs create mode 100644 services/brig/src/Brig/Effects/CookieStore/Cassandra.hs diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 8db8d8066e..9516e797a8 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -69,6 +69,8 @@ library Brig.Effects.CodeStore Brig.Effects.CodeStore.Cassandra Brig.Effects.Common + Brig.Effects.CookieStore + Brig.Effects.CookieStore.Cassandra Brig.Effects.Delay Brig.Effects.GalleyAccess Brig.Effects.GalleyAccess.Http diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index ec31d48571..d15b227b8a 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -31,6 +31,7 @@ import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BudgetStore import Brig.Effects.ClientStore import Brig.Effects.CodeStore +import Brig.Effects.CookieStore import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess (GundeckAccess) import Brig.Effects.PasswordResetStore (PasswordResetStore) @@ -69,6 +70,7 @@ sitemap :: ClientStore, CodeStore, Concurrency 'Unsafe, + CookieStore, Error ReAuthError, Error Twilio.ErrorResponse, GalleyAccess, diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 2f9d3406d4..e25e02ab69 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -55,6 +55,7 @@ import qualified Brig.Data.Client as Data import Brig.Data.Nonce as Nonce import qualified Brig.Data.User as Data import Brig.Effects.ClientStore +import Brig.Effects.CookieStore import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess (GundeckAccess) import Brig.Effects.JwtTools (JwtTools) @@ -155,6 +156,7 @@ lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk addClient :: Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -175,6 +177,7 @@ addClientWithReAuthPolicy :: forall r p. Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -251,6 +254,7 @@ updateClient u c r = do rmClient :: Members '[ ClientStore, + CookieStore, Error ReAuthError, GundeckAccess, Input (Local ()), @@ -426,14 +430,14 @@ claimLocalMultiPrekeyBundles protectee userClients = do -- | Enqueue an orderly deletion of an existing client. execDelete :: - Members '[ClientStore] r => + Members '[ClientStore, CookieStore] r => UserId -> Maybe ConnId -> Client -> AppT r () execDelete u con c = do liftSem $ deleteClient u (clientId c) - for_ (clientCookie c) $ \l -> wrapClient $ Auth.revokeCookies u [] [l] + for_ (clientCookie c) $ \l -> liftSem $ Auth.revokeCookies u [] [l] queue <- view internalEvents Queue.enqueue queue (Internal.DeleteClient c u con) @@ -489,7 +493,13 @@ legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPreke lhClientEvent = LegalHoldClientRequested eventData removeLegalHoldClient :: - Members '[ClientStore, GalleyAccess, GundeckAccess] r => + Members + '[ ClientStore, + CookieStore, + GalleyAccess, + GundeckAccess + ] + r => UserId -> AppT r () removeLegalHoldClient uid = do diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index c9965485cf..d37acfc744 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -46,6 +46,7 @@ import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.ClientStore (ClientStore) import Brig.Effects.CodeStore (CodeStore) +import Brig.Effects.CookieStore (CookieStore) import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess (GundeckAccess) import Brig.Effects.PasswordResetStore (PasswordResetStore) @@ -118,6 +119,7 @@ import Wire.API.User.Activation import Wire.API.User.Client import Wire.API.User.Password import Wire.API.User.RichInfo +import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe)) --------------------------------------------------------------------------- -- Sitemap (servant) @@ -331,6 +333,8 @@ sitemap :: BlacklistStore, ClientStore, CodeStore, + Concurrency 'Unsafe, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -516,6 +520,7 @@ sitemap = do addClientInternalH :: Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -532,6 +537,7 @@ addClientInternalH (usr ::: mSkipReAuth ::: req ::: connId ::: _) = do addClientInternal :: Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -560,7 +566,13 @@ legalHoldClientRequestedH (targetUser ::: req ::: _) = do pure $ setStatus status200 empty removeLegalHoldClientH :: - Members '[ClientStore, GalleyAccess, GundeckAccess] r => + Members + '[ ClientStore, + CookieStore, + GalleyAccess, + GundeckAccess + ] + r => UserId ::: JSON -> Handler r Response removeLegalHoldClientH (uid ::: _) = do @@ -655,6 +667,7 @@ createUserNoVerifySpar uData = deleteUserNoAuthH :: Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -821,7 +834,14 @@ instance ToJSON GetPasswordResetCodeResp where toJSON (GetPasswordResetCodeResp (k, c)) = object ["key" .= k, "code" .= c] changeAccountStatusH :: - Members '[GalleyAccess, GundeckAccess, UserQuery p] r => + Members + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, + GundeckAccess, + UserQuery p + ] + r => UserId ::: JsonRequest AccountStatusUpdate -> Handler r Response changeAccountStatusH (usr ::: req) = do diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 4eab23eef7..6bf675830f 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -52,6 +52,7 @@ import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BudgetStore import Brig.Effects.ClientStore (ClientStore) import Brig.Effects.CodeStore (CodeStore) +import Brig.Effects.CookieStore (CookieStore) import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess (GundeckAccess) import Brig.Effects.JwtTools (JwtTools) @@ -190,6 +191,7 @@ servantSitemap :: BlacklistStore, ClientStore, CodeStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -351,6 +353,7 @@ sitemap :: ClientStore, CodeStore, Concurrency 'Unsafe, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -385,6 +388,7 @@ apiDocs :: ClientStore, CodeStore, Concurrency 'Unsafe, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -525,6 +529,7 @@ getMultiUserPrekeyBundleH zusr qualUserClients = do addClient :: Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -550,6 +555,7 @@ addClient usr con ip new = do deleteClient :: Members '[ ClientStore, + CookieStore, GundeckAccess, Input (Local ()), P.Error ReAuthError, @@ -906,7 +912,7 @@ checkPasswordExists :: UserId -> (Handler r) Bool checkPasswordExists = fmap isJust . lift . wrapClient . API.lookupPassword changePassword :: - Members '[UserQuery p] r => + Members '[CookieStore, UserQuery p] r => UserId -> Public.PasswordChange -> Handler r (Maybe Public.ChangePasswordError) @@ -1005,6 +1011,7 @@ beginPasswordReset (Public.NewPasswordReset target) = do completePasswordReset :: Members '[ CodeStore, + CookieStore, PasswordResetStore, PasswordResetSupply, UserKeyStore @@ -1167,6 +1174,7 @@ getConnection self other = do deleteSelfUser :: Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -1186,6 +1194,7 @@ deleteSelfUser u body = verifyDeleteUser :: Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -1346,6 +1355,7 @@ deprecatedOnboarding _ _ = pure DeprecatedMatchingResult deprecatedCompletePasswordReset :: Members '[ CodeStore, + CookieStore, PasswordResetStore, PasswordResetStore, PasswordResetSupply, diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 4474b2f04c..9d7897ebba 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -113,6 +113,7 @@ import qualified Brig.Effects.BlacklistPhonePrefixStore as BlacklistPhonePrefixS import Brig.Effects.BlacklistStore (BlacklistStore) import qualified Brig.Effects.BlacklistStore as BlacklistStore import Brig.Effects.ClientStore +import Brig.Effects.CookieStore (CookieStore) import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess import Brig.Effects.PasswordResetStore (PasswordResetStore) @@ -200,6 +201,7 @@ import Wire.API.User.Activation import Wire.API.User.Client import Wire.API.User.Password import Wire.API.User.RichInfo +import Wire.Sem.Concurrency data AllowSCIMUpdates = AllowSCIMUpdates @@ -1013,7 +1015,9 @@ revokeIdentity key = do changeAccountStatus :: forall r p. Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] @@ -1022,7 +1026,7 @@ changeAccountStatus :: AccountStatus -> ExceptT AccountStatusError (AppT r) () changeAccountStatus usrs status = do - ev <- wrapClientE $ mkUserEvent usrs status + ev <- liftSemE $ mkUserEvent usrs status lift $ do -- mapConcurrently_ (update ev) usrs -- TODO(md): do this updating concurrently, perhaps in an effect @@ -1038,7 +1042,9 @@ changeAccountStatus usrs status = do changeSingleAccountStatus :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] @@ -1048,23 +1054,24 @@ changeSingleAccountStatus :: ExceptT AccountStatusError (AppT r) () changeSingleAccountStatus uid status = do unlessM (lift . liftSem $ Data.userExists uid) $ throwE AccountNotFound - ev <- wrapClientE $ mkUserEvent [uid] status + ev <- liftSemE $ mkUserEvent [uid] status lift . liftSem $ Data.updateStatus uid status lift $ Intra.onUserEvent uid Nothing (ev uid) mkUserEvent :: - (Traversable t, MonadClient m) => + ( Traversable t, + Members '[Concurrency 'Unsafe, CookieStore] r + ) => t UserId -> AccountStatus -> - ExceptT AccountStatusError m (UserId -> UserEvent) + ExceptT AccountStatusError (Sem r) (UserId -> UserEvent) mkUserEvent usrs status = case status of Active -> pure UserResumed Suspended -> lift $ - -- mapConcurrently revokeAllCookies usrs >> pure UserSuspended - -- TODO(md): implement concurrently traversing this as an effect - traverse revokeAllCookies usrs >> pure UserSuspended + unsafePooledMapConcurrentlyN_ 16 revokeAllCookies usrs + >> pure UserSuspended Deleted -> throwE InvalidAccountStatus Ephemeral -> throwE InvalidAccountStatus PendingInvitation -> throwE InvalidAccountStatus @@ -1309,7 +1316,7 @@ mkActivationKey (ActivatePhone p) = do -- Password Management changePassword :: - Members '[UserQuery p] r => + Members '[CookieStore, UserQuery p] r => UserId -> PasswordChange -> ExceptT ChangePasswordError (AppT r) () @@ -1327,7 +1334,7 @@ changePassword uid cp = do throwE InvalidCurrentPassword when (verifyPassword newpw pw) $ throwE ChangePasswordMustDiffer - lift $ wrapClient (Data.updatePassword uid newpw) >> wrapClient (revokeAllCookies uid) + lift $ wrapClient (Data.updatePassword uid newpw) >> liftSem (revokeAllCookies uid) beginPasswordReset :: Members '[P.TinyLog, PasswordResetStore, UserKeyStore] r => @@ -1347,7 +1354,13 @@ beginPasswordReset target = do (user,) <$> lift (liftSem $ E.createPasswordResetCode user target) completePasswordReset :: - Members '[PasswordResetStore, PasswordResetSupply, UserKeyStore] r => + Members + '[ CookieStore, + PasswordResetStore, + PasswordResetSupply, + UserKeyStore + ] + r => PasswordResetIdentity -> PasswordResetCode -> PlainTextPassword -> @@ -1363,7 +1376,7 @@ completePasswordReset ident code pw = do lift $ do wrapClient $ Data.updatePassword uid pw liftSem $ E.deletePasswordResetCode key - wrapClient $ revokeAllCookies uid + liftSem $ revokeAllCookies uid -- | Pull the current password of a user and compare it against the one about to be installed. -- If the two are the same, throw an error. If no current password can be found, do nothing. @@ -1403,6 +1416,7 @@ mkPasswordResetKey ident = case ident of deleteSelfUser :: Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -1493,6 +1507,7 @@ deleteSelfUser uid pwd = do verifyDeleteUser :: Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -1520,6 +1535,7 @@ verifyDeleteUser d = do ensureAccountDeleted :: Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), @@ -1546,7 +1562,7 @@ ensureAccountDeleted uid = do conCount <- wrapClient $ countConnections localUid [(minBound @Relation) .. maxBound] - cookies <- wrapClient $ listCookies uid [] + cookies <- liftSem $ listCookies uid [] if notNull probs || not accIsDeleted @@ -1571,6 +1587,7 @@ deleteAccount :: forall r p. Members '[ ClientStore, + CookieStore, GalleyAccess, GundeckAccess, UniqueClaimsStore, @@ -1600,7 +1617,7 @@ deleteAccount account@(accountUser -> user) = do -- Note: Connections can only be deleted afterwards, since -- they need to be notified. wrapClient $ Data.deleteConnections uid - wrapClient $ revokeAllCookies uid + liftSem $ revokeAllCookies uid where mkTombstone = do defLoc <- setDefaultUserLocale <$> view settings diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index c2a4b32242..821cb26c0e 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -39,6 +39,8 @@ import Brig.Effects.ClientStore.Cassandra import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.CodeStore.Cassandra (codeStoreToCassandra) import Brig.Effects.Common +import Brig.Effects.CookieStore (CookieStore) +import Brig.Effects.CookieStore.Cassandra import Brig.Effects.GalleyAccess (GalleyAccess) import Brig.Effects.GalleyAccess.Http import Brig.Effects.GundeckAccess (GundeckAccess) @@ -91,7 +93,8 @@ import Wire.Sem.Paging.Cassandra (InternalPaging) import qualified Wire.Sem.Paging.Cassandra as PC type BrigCanonicalEffects = - '[ ClientStore, + '[ CookieStore, + ClientStore, PublicKeyBundle, JwtTools, BlacklistPhonePrefixStore, @@ -161,6 +164,7 @@ runBrigToIO e (AppT ma) = . interpretJwtTools . interpretPublicKeyBundle . clientStoreToCassandra @HttpClientIO + . cookieStoreToCassandra @Cas.Client $ runReaderT ma e interpretHttpToIO :: diff --git a/services/brig/src/Brig/Effects/CookieStore.hs b/services/brig/src/Brig/Effects/CookieStore.hs new file mode 100644 index 0000000000..a8326e16eb --- /dev/null +++ b/services/brig/src/Brig/Effects/CookieStore.hs @@ -0,0 +1,31 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Effects.CookieStore where + +import Data.Id +import Polysemy +import Wire.API.User.Auth + +data CookieStore m a where + GetCookies :: UserId -> CookieStore m [Cookie ()] + DeleteCookies :: UserId -> [Cookie c] -> CookieStore m () + DeleteAllCookies :: UserId -> CookieStore m () + +makeSem ''CookieStore diff --git a/services/brig/src/Brig/Effects/CookieStore/Cassandra.hs b/services/brig/src/Brig/Effects/CookieStore/Cassandra.hs new file mode 100644 index 0000000000..a4028f9890 --- /dev/null +++ b/services/brig/src/Brig/Effects/CookieStore/Cassandra.hs @@ -0,0 +1,76 @@ +-- 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.CookieStore.Cassandra (cookieStoreToCassandra) where + +import Brig.Effects.CookieStore +import Brig.User.Auth.DB.Instances () +import Cassandra +import Data.Id +import Data.Time.Clock +import Imports +import Polysemy +import Wire.API.User.Auth + +cookieStoreToCassandra :: + forall m r a. + (MonadClient m, Member (Embed m) r) => + Sem (CookieStore ': r) a -> + Sem r a +cookieStoreToCassandra = + interpret $ + embed @m . \case + GetCookies u -> listCookies u + DeleteCookies u cs -> deleteCookiesQuery u cs + DeleteAllCookies u -> deleteAllCookiesQuery u + +deleteCookiesQuery :: MonadClient m => UserId -> [Cookie a] -> m () +deleteCookiesQuery u cs = retry x5 . batch $ do + setType BatchUnLogged + setConsistency LocalQuorum + for_ cs $ \c -> addPrepQuery cql (u, cookieExpires c, cookieId c) + where + cql :: PrepQuery W (UserId, UTCTime, CookieId) () + cql = "DELETE FROM user_cookies WHERE user = ? AND expires = ? AND id = ?" + +deleteAllCookiesQuery :: MonadClient m => UserId -> m () +deleteAllCookiesQuery u = retry x5 (write cql (params LocalQuorum (Identity u))) + where + cql :: PrepQuery W (Identity UserId) () + cql = "DELETE FROM user_cookies WHERE user = ?" + +listCookies :: MonadClient m => UserId -> m [Cookie ()] +listCookies u = + map toCookie <$> retry x1 (query cql (params LocalQuorum (Identity u))) + where + cql :: PrepQuery R (Identity UserId) (CookieId, UTCTime, UTCTime, CookieType, Maybe CookieLabel, Maybe CookieId) + cql = + "SELECT id, created, expires, type, label, succ_id \ + \FROM user_cookies \ + \WHERE user = ? \ + \ORDER BY expires ASC" + toCookie :: (CookieId, UTCTime, UTCTime, CookieType, Maybe CookieLabel, Maybe CookieId) -> Cookie () + toCookie (i, ct, et, t, l, sc) = + Cookie + { cookieId = i, + cookieType = t, + cookieCreated = ct, + cookieExpires = et, + cookieLabel = l, + cookieSucc = sc, + cookieValue = () + } diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index e4ba2fd9ca..d3a61a150c 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -25,6 +25,7 @@ import Brig.App import qualified Brig.Data.Client as Data import Brig.Effects.ClientStore (ClientStore) import qualified Brig.Effects.ClientStore as E +import Brig.Effects.CookieStore import Brig.Effects.GalleyAccess (GalleyAccess) import Brig.Effects.GundeckAccess (GundeckAccess) import Brig.Effects.UniqueClaimsStore @@ -62,6 +63,7 @@ onEvent :: Members '[ ClientStore, Concurrency 'Unsafe, + CookieStore, GalleyAccess, GundeckAccess, Input (Local ()), diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index 2f5057e797..d07f5e7b6c 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -31,6 +31,7 @@ import Brig.Data.UserKey import qualified Brig.Data.UserKey as Data import Brig.Effects.BlacklistStore (BlacklistStore) import qualified Brig.Effects.BlacklistStore as BlacklistStore +import Brig.Effects.CookieStore import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess import Brig.Effects.Twilio (Twilio) @@ -88,6 +89,7 @@ import qualified Wire.API.Team.Role as Public import qualified Wire.API.Team.Size as Public import Wire.API.User hiding (fromEmail) import qualified Wire.API.User as Public +import Wire.Sem.Concurrency routesPublic :: Members @@ -212,6 +214,8 @@ routesPublic = do routesInternal :: Members '[ BlacklistStore, + Concurrency 'Unsafe, + CookieStore, GalleyAccess, GundeckAccess, P.Error Twilio.ErrorResponse, @@ -534,7 +538,9 @@ getInvitationByEmail email = do suspendTeamH :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] @@ -546,7 +552,9 @@ suspendTeamH (_ ::: tid) = do suspendTeam :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] @@ -560,7 +568,9 @@ suspendTeam tid = do unsuspendTeamH :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] @@ -572,7 +582,9 @@ unsuspendTeamH (_ ::: tid) = do unsuspendTeam :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] @@ -588,7 +600,9 @@ unsuspendTeam tid = do changeTeamAccountStatuses :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index 8ad3daf5b0..604e1cb52a 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -34,6 +34,8 @@ import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BudgetStore -- import Brig.Types.Intra (ReAuthUser, reAuthCode, reAuthCodeAction, reAuthPassword) -- import Control.Lens (view) + +import Brig.Effects.CookieStore import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess (GundeckAccess) import Brig.Effects.Twilio @@ -83,6 +85,7 @@ import Wire.API.Error import qualified Wire.API.Error.Brig as E import qualified Wire.API.User as Public import Wire.API.User.Auth as Public +import Wire.Sem.Concurrency import Wire.Swagger as Doc (pendingLoginError) routesPublic :: @@ -91,6 +94,8 @@ routesPublic :: ActivationSupply, BlacklistStore, BudgetStore, + Concurrency 'Unsafe, + CookieStore, Error Twilio.ErrorResponse, GalleyAccess, GundeckAccess, @@ -235,7 +240,9 @@ routesPublic = do routesInternal :: Members - '[ Error ReAuthError, + '[ Concurrency 'Unsafe, + CookieStore, + Error ReAuthError, GalleyAccess, GundeckAccess, Input (Local ()), @@ -353,6 +360,8 @@ reAuthUser uid body = do loginH :: Members '[ BudgetStore, + Concurrency 'Unsafe, + CookieStore, Error Twilio.ErrorResponse, GalleyAccess, GundeckAccess, @@ -373,6 +382,8 @@ loginH (req ::: persist ::: _) = do login :: Members '[ BudgetStore, + Concurrency 'Unsafe, + CookieStore, Error Twilio.ErrorResponse, GalleyAccess, GundeckAccess, @@ -394,7 +405,9 @@ login l persist = do ssoLoginH :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, Input (Local ()), UserQuery p @@ -407,7 +420,9 @@ ssoLoginH (req ::: persist ::: _) = do ssoLogin :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, Input (Local ()), UserQuery p @@ -422,7 +437,9 @@ ssoLogin l persist = do legalHoldLoginH :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, Input (Local ()), UserQuery p @@ -435,7 +452,9 @@ legalHoldLoginH (req ::: _) = do legalHoldLogin :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, Input (Local ()), UserQuery p @@ -447,11 +466,15 @@ legalHoldLogin l = do let typ = PersistentCookie -- Session cookie isn't a supported use case here Auth.legalHoldLogin l typ !>> legalHoldLoginError -logoutH :: JSON ::: Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) ::: Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> (Handler r) Response +logoutH :: + Member CookieStore r => + JSON ::: Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) ::: Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> + Handler r Response logoutH (_ ::: ut ::: at) = empty <$ logout ut at -- TODO: add legalhold test checking cookies are revoked (/access/logout is called) when legalhold device is deleted. logout :: + Member CookieStore r => Maybe (Either (List1 ZAuth.UserToken) (List1 ZAuth.LegalHoldUserToken)) -> Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> (Handler r) () @@ -460,8 +483,8 @@ logout Nothing (Just _) = throwStd authMissingCookie logout (Just _) Nothing = throwStd authMissingToken logout (Just (Left _)) (Just (Right _)) = throwStd authTokenMismatch logout (Just (Right _)) (Just (Left _)) = throwStd authTokenMismatch -logout (Just (Left ut)) (Just (Left at)) = wrapHttpClientE (Auth.logout ut at) !>> zauthError -logout (Just (Right ut)) (Just (Right at)) = wrapHttpClientE (Auth.logout ut at) !>> zauthError +logout (Just (Left ut)) (Just (Left at)) = Auth.logout ut at !>> zauthError +logout (Just (Right ut)) (Just (Right at)) = Auth.logout ut at !>> zauthError changeSelfEmailH :: Members @@ -502,22 +525,41 @@ changeSelfEmailH (_ ::: req ::: ckies ::: toks) = do Just (Left userTokens) -> fst <$> wrapHttpClientE (Auth.validateTokens userCookies (Just userTokens)) !>> zauthError -listCookiesH :: UserId ::: Maybe (List Public.CookieLabel) ::: JSON -> (Handler r) Response -listCookiesH (u ::: ll ::: _) = json <$> lift (listCookies u ll) +listCookiesH :: + Member CookieStore r => + UserId ::: Maybe (List Public.CookieLabel) ::: JSON -> + Handler r Response +listCookiesH (u ::: ll ::: _) = json <$> (lift . liftSem $ listCookies u ll) -listCookies :: UserId -> Maybe (List Public.CookieLabel) -> (AppT r) Public.CookieList +listCookies :: + Member CookieStore r => + UserId -> + Maybe (List Public.CookieLabel) -> + Sem r Public.CookieList listCookies u ll = do - Public.CookieList <$> wrapClient (Auth.listCookies u (maybe [] fromList ll)) + Public.CookieList <$> Auth.listCookies u (maybe [] fromList ll) rmCookiesH :: - Members '[Input (Local ()), TinyLog, UserQuery p] r => + Members + '[ CookieStore, + Input (Local ()), + TinyLog, + UserQuery p + ] + r => UserId ::: JsonRequest Public.RemoveCookies -> Handler r Response rmCookiesH (uid ::: req) = do empty <$ (rmCookies uid =<< parseJsonBody req) rmCookies :: - Members '[Input (Local ()), TinyLog, UserQuery p] r => + Members + '[ CookieStore, + Input (Local ()), + TinyLog, + UserQuery p + ] + r => UserId -> Public.RemoveCookies -> Handler r () @@ -526,7 +568,9 @@ rmCookies uid (Public.RemoveCookies pw lls ids) = renewH :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] @@ -542,7 +586,9 @@ renewH (_ ::: ut ::: at) = lift . either tokenResponse tokenResponse =<< renew u -- Other combinations of provided inputs will cause an error to be raised. renew :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index cb51260e9b..efcfc19c95 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -47,6 +47,7 @@ import Brig.Data.UserKey import qualified Brig.Data.UserKey as Data import Brig.Effects.BudgetStore import Brig.Effects.Common +import Brig.Effects.CookieStore import Brig.Effects.GalleyAccess import Brig.Effects.GundeckAccess import Brig.Effects.Twilio (Twilio) @@ -92,6 +93,7 @@ import Wire.API.Team.Feature import qualified Wire.API.Team.Feature as Public import Wire.API.User import Wire.API.User.Auth +import Wire.Sem.Concurrency data Access u = Access { accessToken :: !AccessToken, @@ -158,6 +160,8 @@ login :: forall r p. Members '[ BudgetStore, + Concurrency 'Unsafe, + CookieStore, Error Twilio.ErrorResponse, GalleyAccess, GundeckAccess, @@ -302,21 +306,22 @@ withRetryLimit action uid mLimitFailedLogins = do logout :: ( ZAuth.TokenPair u a, - ZAuth.MonadZAuth m, - MonadClient m + Member CookieStore r ) => List1 (ZAuth.Token u) -> ZAuth.Token a -> - ExceptT ZAuth.Failure m () + ExceptT ZAuth.Failure (AppT r) () logout uts at = do - (u, ck) <- validateTokens uts (Just at) - lift $ revokeCookies u [cookieId ck] [] + (u, ck) <- mapExceptT wrapHttpClient $ validateTokens uts (Just at) + lift . liftSem $ revokeCookies u [cookieId ck] [] renewAccess :: forall u a r p. ZAuth.TokenPair u a => Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] @@ -328,12 +333,18 @@ renewAccess uts at = do (uid, ck) <- wrapHttpClientE $ validateTokens uts at lift . Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.renewAccess") catchSuspendInactiveUser uid ZAuth.Expired - ck' <- lift . wrapHttpClient $ nextCookie ck + ck' <- lift $ nextCookie ck at' <- lift $ newAccessToken (fromMaybe ck ck') at pure $ Access at' ck' revokeAccess :: - Members '[Input (Local ()), TinyLog, UserQuery p] r => + Members + '[ CookieStore, + Input (Local ()), + TinyLog, + UserQuery p + ] + r => UserId -> PlainTextPassword -> [CookieId] -> @@ -347,14 +358,16 @@ revokeAccess u pw cc ll = do . mapExceptT liftSem . semErrToExceptT $ Data.authenticate u pw - lift . wrapClient $ revokeCookies u cc ll + lift . liftSem $ revokeCookies u cc ll -------------------------------------------------------------------------------- -- Internal catchSuspendInactiveUser :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] @@ -363,7 +376,7 @@ catchSuspendInactiveUser :: e -> ExceptT e (AppT r) () catchSuspendInactiveUser uid errval = do - mustsuspend <- wrapClientE $ mustSuspendInactiveUser uid + mustsuspend <- lift $ mustSuspendInactiveUser uid when mustsuspend $ do lift . Log.warn $ msg (val "Suspending user due to inactivity") @@ -382,7 +395,9 @@ newAccess :: forall u a r p. ZAuth.TokenPair u a => Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, UserQuery p ] @@ -393,7 +408,7 @@ newAccess :: ExceptT LoginError (AppT r) (Access u) newAccess uid ct cl = do catchSuspendInactiveUser uid LoginSuspended - r <- lift . wrapHttpClient $ newCookieLimited uid ct cl + r <- lift $ newCookieLimited uid ct cl case r of Left delay -> throwE $ LoginThrottled delay Right ck -> do @@ -524,7 +539,9 @@ validateToken ut at = do -- | Allow to login as any user without having the credentials. ssoLogin :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, Input (Local ()), UserQuery p @@ -555,7 +572,9 @@ ssoLogin (SsoLogin uid label) typ = do -- | Log in as a LegalHold service, getting LegalHoldUser/Access Tokens. legalHoldLogin :: Members - '[ GalleyAccess, + '[ Concurrency 'Unsafe, + CookieStore, + GalleyAccess, GundeckAccess, Input (Local ()), UserQuery p diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index 05f24ff8c7..564d3d4d97 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -42,6 +42,8 @@ module Brig.User.Auth.Cookie where import Brig.App +import Brig.Effects.CookieStore (CookieStore) +import qualified Brig.Effects.CookieStore as E import Brig.Options hiding (user) import Brig.User.Auth.Cookie.Limit import qualified Brig.User.Auth.DB.Cookie as DB @@ -58,6 +60,7 @@ import Data.Time.Clock import Imports import Network.Wai (Response) import Network.Wai.Utilities.Response (addHeader) +import Polysemy import System.Logger.Class (field, msg, val, (~~)) import qualified System.Logger.Class as Log import qualified Web.Cookie as WebCookie @@ -99,13 +102,10 @@ newCookie uid typ label = do -- exceeds the configured minimum threshold. nextCookie :: ( ZAuth.UserTokenLike u, - MonadReader Env m, - Log.MonadLogger m, - ZAuth.MonadZAuth m, - MonadClient m + Member CookieStore r ) => Cookie (ZAuth.Token u) -> - m (Maybe (Cookie (ZAuth.Token u))) + AppT r (Maybe (Cookie (ZAuth.Token u))) nextCookie c = do s <- view settings now <- liftIO =<< view currentTime @@ -119,13 +119,13 @@ nextCookie c = do where persist = (PersistentCookie ==) . cookieType getNext = case cookieSucc c of - Nothing -> renewCookie c + Nothing -> wrapHttpClient $ renewCookie c Just ck -> do let uid = ZAuth.userTokenOf (cookieValue c) trackSuperseded uid (cookieId c) - cs <- DB.listCookies uid + cs <- liftSem $ E.getCookies uid case List.find (\x -> cookieId x == ck && persist x) cs of - Nothing -> renewCookie c + Nothing -> wrapHttpClient $ renewCookie c Just c' -> do t <- ZAuth.mkUserToken uid (cookieIdNum ck) (cookieExpires c') pure c' {cookieValue = t} @@ -156,7 +156,7 @@ renewCookie old = do -- 'suspendCookiesOlderThanSecs'. Call this always before 'newCookie', 'nextCookie', -- 'newCookieLimited' if there is a chance that the user should be suspended (we don't do it -- implicitly because of cyclical dependencies). -mustSuspendInactiveUser :: (MonadReader Env m, MonadClient m) => UserId -> m Bool +mustSuspendInactiveUser :: Member CookieStore r => UserId -> AppT r Bool mustSuspendInactiveUser uid = view (settings . to setSuspendInactiveUsers) >>= \case Nothing -> pure False @@ -166,7 +166,7 @@ mustSuspendInactiveUser uid = suspendHere = addUTCTime (-suspendAge) now youngEnough :: Cookie () -> Bool youngEnough = (>= suspendHere) . cookieCreated - ckies <- listCookies uid [] + ckies <- liftSem $ listCookies uid [] let mustSuspend | null ckies = False | any youngEnough ckies = False @@ -202,20 +202,25 @@ lookupCookie t = do where setToken c = c {cookieValue = t} -listCookies :: MonadClient m => UserId -> [CookieLabel] -> m [Cookie ()] -listCookies u [] = DB.listCookies u -listCookies u ll = filter byLabel <$> DB.listCookies u +listCookies :: Member CookieStore r => UserId -> [CookieLabel] -> Sem r [Cookie ()] +listCookies u [] = E.getCookies u +listCookies u ll = filter byLabel <$> E.getCookies u where byLabel c = maybe False (`elem` ll) (cookieLabel c) -revokeAllCookies :: MonadClient m => UserId -> m () +revokeAllCookies :: Member CookieStore r => UserId -> Sem r () revokeAllCookies u = revokeCookies u [] [] -revokeCookies :: MonadClient m => UserId -> [CookieId] -> [CookieLabel] -> m () -revokeCookies u [] [] = DB.deleteAllCookies u +revokeCookies :: + Member CookieStore r => + UserId -> + [CookieId] -> + [CookieLabel] -> + Sem r () +revokeCookies u [] [] = E.deleteAllCookies u revokeCookies u ids labels = do - cc <- filter matching <$> DB.listCookies u - DB.deleteCookies u cc + cc <- filter matching <$> E.getCookies u + E.deleteCookies u cc where matching c = cookieId c `elem` ids @@ -226,27 +231,25 @@ revokeCookies u ids labels = do newCookieLimited :: ( ZAuth.UserTokenLike t, - MonadReader Env m, - MonadClient m, - ZAuth.MonadZAuth m + Member CookieStore r ) => UserId -> CookieType -> Maybe CookieLabel -> - m (Either RetryAfter (Cookie (ZAuth.Token t))) + AppT r (Either RetryAfter (Cookie (ZAuth.Token t))) newCookieLimited u typ label = do - cs <- filter ((typ ==) . cookieType) <$> DB.listCookies u + cs <- liftSem $ filter ((typ ==) . cookieType) <$> E.getCookies u now <- liftIO =<< view currentTime lim <- CookieLimit . setUserCookieLimit <$> view settings thr <- setUserCookieThrottle <$> view settings let evict = map cookieId (limitCookies lim now cs) if null evict - then Right <$> newCookie u typ label + then Right <$> wrapHttpClient (newCookie u typ label) else case throttleCookies now thr cs of Just wait -> pure (Left wait) Nothing -> do - revokeCookies u evict [] - Right <$> newCookie u typ label + liftSem $ revokeCookies u evict [] + Right <$> wrapHttpClient (newCookie u typ label) -------------------------------------------------------------------------------- -- HTTP diff --git a/services/brig/src/Brig/User/Auth/DB/Cookie.hs b/services/brig/src/Brig/User/Auth/DB/Cookie.hs index c0d43ef234..24bc5800a3 100644 --- a/services/brig/src/Brig/User/Auth/DB/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/DB/Cookie.hs @@ -17,6 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . +-- FUTUREWORK: Migrate the remaining actions to 'Brig.Effects.CookieStore' module Brig.User.Auth.DB.Cookie where import Brig.User.Auth.DB.Instances () @@ -64,40 +65,3 @@ lookupCookie u t c = "SELECT type, created, label, succ_id \ \FROM user_cookies \ \WHERE user = ? AND expires = ? AND id = ?" - -listCookies :: MonadClient m => UserId -> m [Cookie ()] -listCookies u = - map toCookie <$> retry x1 (query cql (params LocalQuorum (Identity u))) - where - cql :: PrepQuery R (Identity UserId) (CookieId, UTCTime, UTCTime, CookieType, Maybe CookieLabel, Maybe CookieId) - cql = - "SELECT id, created, expires, type, label, succ_id \ - \FROM user_cookies \ - \WHERE user = ? \ - \ORDER BY expires ASC" - toCookie :: (CookieId, UTCTime, UTCTime, CookieType, Maybe CookieLabel, Maybe CookieId) -> Cookie () - toCookie (i, ct, et, t, l, sc) = - Cookie - { cookieId = i, - cookieType = t, - cookieCreated = ct, - cookieExpires = et, - cookieLabel = l, - cookieSucc = sc, - cookieValue = () - } - -deleteCookies :: MonadClient m => UserId -> [Cookie a] -> m () -deleteCookies u cs = retry x5 . batch $ do - setType BatchUnLogged - setConsistency LocalQuorum - for_ cs $ \c -> addPrepQuery cql (u, cookieExpires c, cookieId c) - where - cql :: PrepQuery W (UserId, UTCTime, CookieId) () - cql = "DELETE FROM user_cookies WHERE user = ? AND expires = ? AND id = ?" - -deleteAllCookies :: MonadClient m => UserId -> m () -deleteAllCookies u = retry x5 (write cql (params LocalQuorum (Identity u))) - where - cql :: PrepQuery W (Identity UserId) () - cql = "DELETE FROM user_cookies WHERE user = ?"