diff --git a/changelog.d/5-internal/galley-rpc b/changelog.d/5-internal/galley-rpc new file mode 100644 index 0000000000..1726de5a6b --- /dev/null +++ b/changelog.d/5-internal/galley-rpc @@ -0,0 +1 @@ +Add RPC, ServiceRPC and GalleyProvider effects to brig diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index f74e685d14..eca21d9c27 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -62,10 +62,16 @@ library Brig.Effects.CodeStore Brig.Effects.CodeStore.Cassandra Brig.Effects.Delay + Brig.Effects.GalleyProvider + Brig.Effects.GalleyProvider.RPC Brig.Effects.JwtTools Brig.Effects.PasswordResetStore Brig.Effects.PasswordResetStore.CodeStore Brig.Effects.PublicKeyBundle + Brig.Effects.RPC + Brig.Effects.RPC.IO + Brig.Effects.ServiceRPC + Brig.Effects.ServiceRPC.IO Brig.Effects.SFT Brig.Effects.UserPendingActivationStore Brig.Effects.UserPendingActivationStore.Cassandra diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index 9b7ce571d2..6ce411f724 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -26,6 +26,7 @@ import qualified Brig.API.Public as Public import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.CodeStore +import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import qualified Data.Swagger.Build.Api as Doc @@ -39,6 +40,7 @@ sitemap :: PasswordResetStore, BlacklistStore, BlacklistPhonePrefixStore, + GalleyProvider, UserPendingActivationStore p ] r => diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 0d96f8351e..3da9219944 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -54,6 +54,8 @@ 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.GalleyProvider (GalleyProvider) +import qualified Brig.Effects.GalleyProvider as GalleyProvider import Brig.Effects.JwtTools (JwtTools) import qualified Brig.Effects.JwtTools as JwtTools import Brig.Effects.PublicKeyBundle (PublicKeyBundle) @@ -90,7 +92,7 @@ import Data.String.Conversions (cs) import Imports import Network.HTTP.Types.Method (StdMethod) import Network.Wai.Utilities -import Polysemy (Member) +import Polysemy (Member, Members) import Servant (Link, ToHttpApiData (toUrlPiece)) import System.Logger.Class (field, msg, val, (~~)) import qualified System.Logger.Class as Log @@ -146,6 +148,7 @@ lookupLocalPubClientsBulk :: [UserId] -> ExceptT ClientError (AppT r) (UserMap ( lookupLocalPubClientsBulk = lift . wrapClient . Data.lookupPubClientsBulk addClient :: + Members '[GalleyProvider] r => UserId -> Maybe ConnId -> Maybe IP -> @@ -156,6 +159,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 :: + forall r. + Members '[GalleyProvider] r => Data.ReAuthPolicy -> UserId -> Maybe ConnId -> @@ -164,7 +169,7 @@ addClientWithReAuthPolicy :: ExceptT ClientError (AppT r) Client addClientWithReAuthPolicy policy u con ip new = do acc <- lift (wrapClient $ Data.lookupAccount u) >>= maybe (throwE (ClientUserNotFound u)) pure - wrapHttpClientE $ verifyCode (newClientVerificationCode new) (userId . accountUser $ acc) + verifyCode (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,9 +188,8 @@ addClientWithReAuthPolicy policy u con ip new = do let usr = accountUser acc lift $ do for_ old $ execDelete u con - wrapHttp $ do - Intra.newClient u (clientId clt) - Intra.onClientEvent u con (ClientAdded u clt) + liftSem $ GalleyProvider.newClient u (clientId clt) + wrapHttp $ Intra.onClientEvent u con (ClientAdded u clt) when (clientType clt == LegalHoldClientType) $ wrapHttpClient $ Intra.onUserEvent u con (UserLegalHoldEnabled u) when (count > 1) $ for_ (userEmail usr) $ @@ -196,16 +200,9 @@ addClientWithReAuthPolicy policy u con ip new = do clientId' = clientIdFromPrekey (unpackLastPrekey $ newClientLastKey new) verifyCode :: - ( MonadReader Env m, - MonadMask m, - MonadHttp m, - HasRequestId m, - Log.MonadLogger m, - MonadClient m - ) => Maybe Code.Value -> UserId -> - ExceptT ClientError m () + ExceptT ClientError (AppT r) () verifyCode 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 diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index 4a222ec790..d00b758680 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -39,6 +39,8 @@ import Brig.App import qualified Brig.Data.Connection as Data import Brig.Data.Types (resultHasMore, resultList) import qualified Brig.Data.User as Data +import Brig.Effects.GalleyProvider (GalleyProvider) +import qualified Brig.Effects.GalleyProvider as GalleyProvider import qualified Brig.IO.Intra as Intra import Brig.Types.Connection import Brig.Types.User.Event @@ -51,6 +53,7 @@ import Data.Qualified import Data.Range import qualified Data.UUID.V4 as UUID import Imports +import Polysemy (Members) import qualified System.Logger.Class as Log import System.Logger.Message import Wire.API.Connection hiding (relationWithHistory) @@ -64,14 +67,15 @@ ensureIsActivated lusr = do active <- lift . wrapClient $ Data.isActivated (tUnqualified lusr) guard active -ensureNotSameTeam :: Local UserId -> Local UserId -> (ConnectionM r) () +ensureNotSameTeam :: Members '[GalleyProvider] r => Local UserId -> Local UserId -> (ConnectionM r) () ensureNotSameTeam self target = do - selfTeam <- lift $ wrapHttp $ Intra.getTeamId (tUnqualified self) - targetTeam <- lift $ wrapHttp $ Intra.getTeamId (tUnqualified target) + selfTeam <- lift $ liftSem $ GalleyProvider.getTeamId (tUnqualified self) + targetTeam <- lift $ liftSem $ GalleyProvider.getTeamId (tUnqualified target) when (isJust selfTeam && selfTeam == targetTeam) $ throwE ConnectSameBindingTeamUsers createConnection :: + Members '[GalleyProvider] r => Local UserId -> ConnId -> Qualified UserId -> @@ -91,6 +95,7 @@ createConnection self con target = do target createConnectionToLocalUser :: + Members '[GalleyProvider] r => Local UserId -> ConnId -> Local UserId -> @@ -178,15 +183,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 '[GalleyProvider] 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 f6cbc7ea95..5f72178583 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 @@ -32,14 +30,12 @@ import Brig.API.Util (lookupSearchPolicy) import Brig.App import qualified Brig.Data.Connection as Data import qualified Brig.Data.User as Data +import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.IO.Intra (notify) 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 -import Control.Monad.Catch (MonadMask) import Control.Monad.Trans.Except import Data.Domain import Data.Handle (Handle (..), parseHandle) @@ -51,9 +47,9 @@ import Data.Range import qualified Gundeck.Types.Push as Push import Imports import Network.Wai.Utilities.Error ((!>>)) +import Polysemy (Members) 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 @@ -71,15 +67,20 @@ import Wire.API.UserMap (UserMap) type FederationAPI = "federation" :> BrigApi -federationSitemap :: ServerT FederationAPI (Handler r) +federationSitemap :: + Members + '[ GalleyProvider + ] + r => + ServerT FederationAPI (Handler r) federationSitemap = Named @"api-version" (\_ _ -> pure versionInfo) - :<|> Named @"get-user-by-handle" (\d h -> wrapHttpClientE $ getUserByHandle d h) - :<|> Named @"get-users-by-ids" (\d us -> wrapHttpClientE $ getUsersByIds d us) + :<|> Named @"get-user-by-handle" (\d h -> getUserByHandle d h) + :<|> Named @"get-users-by-ids" (\d us -> 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" (\d sr -> searchUsers d sr) :<|> Named @"get-user-clients" getUserClients :<|> Named @"get-mls-clients" getMLSClients :<|> Named @"send-connection-action" sendConnectionAction @@ -99,16 +100,13 @@ sendConnectionAction originDomain NewConnectionRequest {..} = do else pure NewConnectionResponseUserNotActivated getUserByHandle :: - ( HasRequestId m, - Log.MonadLogger m, - MonadClient m, - MonadHttp m, - MonadMask m, - MonadReader Env m - ) => + Members + '[ GalleyProvider + ] + r => Domain -> Handle -> - ExceptT Error m (Maybe UserProfile) + ExceptT Error (AppT r) (Maybe UserProfile) getUserByHandle domain handle = do searchPolicy <- lookupSearchPolicy domain @@ -120,7 +118,7 @@ getUserByHandle domain handle = do if not performHandleLookup then pure Nothing else lift $ do - maybeOwnerId <- API.lookupHandle handle + maybeOwnerId <- wrapClient $ API.lookupHandle handle case maybeOwnerId of Nothing -> pure Nothing @@ -128,16 +126,13 @@ getUserByHandle domain handle = do listToMaybe <$> API.lookupLocalProfiles Nothing [ownerId] getUsersByIds :: - ( MonadClient m, - MonadReader Env m, - Log.MonadLogger m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => + Members + '[ GalleyProvider + ] + r => Domain -> [UserId] -> - ExceptT Error m [UserProfile] + ExceptT Error (AppT r) [UserProfile] getUsersByIds _ uids = lift (API.lookupLocalProfiles Nothing uids) @@ -163,18 +158,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 '[GalleyProvider] r => Domain -> SearchRequest -> - ExceptT Error m SearchResponse + ExceptT Error (AppT r) SearchResponse searchUsers domain (SearchRequest searchTerm) = do searchPolicy <- lift $ lookupSearchPolicy domain @@ -188,22 +176,22 @@ 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) (wrapHttpClientE . API.lookupHandle) maybeHandle case maybeOwnerId of Nothing -> pure [] Just foundUser -> lift $ contactFromProfile <$$> API.lookupLocalProfiles Nothing [foundUser] diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index a69ebeac66..7fe68751cd 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -43,6 +43,7 @@ import qualified Brig.Data.User as Data import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.CodeStore (CodeStore) +import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import qualified Brig.IO.Intra as Intra @@ -106,13 +107,19 @@ import Wire.API.User.RichInfo servantSitemap :: Members '[ BlacklistStore, + GalleyProvider, UserPendingActivationStore p ] r => ServerT BrigIRoutes.API (Handler r) servantSitemap = ejpdAPI :<|> accountAPI :<|> mlsAPI :<|> getVerificationCode :<|> teamsAPI :<|> userAPI -ejpdAPI :: ServerT BrigIRoutes.EJPD_API (Handler r) +ejpdAPI :: + Members + '[ GalleyProvider + ] + r => + ServerT BrigIRoutes.EJPD_API (Handler r) ejpdAPI = Brig.User.EJPD.ejpdRequest :<|> Named @"get-account-conference-calling-config" getAccountConferenceCallingConfig @@ -138,6 +145,7 @@ mlsAPI = accountAPI :: Members '[ BlacklistStore, + GalleyProvider, UserPendingActivationStore p ] r => @@ -267,6 +275,7 @@ sitemap :: PasswordResetStore, BlacklistStore, BlacklistPhonePrefixStore, + GalleyProvider, UserPendingActivationStore p ] r => @@ -433,12 +442,27 @@ sitemap = do -- Handlers -- | Add a client without authentication checks -addClientInternalH :: UserId ::: Maybe Bool ::: JsonRequest NewClient ::: Maybe ConnId ::: JSON -> (Handler r) Response +addClientInternalH :: + Members + '[ GalleyProvider + ] + 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 + '[ GalleyProvider + ] + r => + UserId -> + Maybe Bool -> + NewClient -> + Maybe ConnId -> + (Handler r) Client addClientInternal usr mSkipReAuth new connId = do let policy | mSkipReAuth == Just True = \_ _ -> False @@ -476,6 +500,7 @@ internalListFullClients (UserSet usrs) = createUserNoVerify :: Members '[ BlacklistStore, + GalleyProvider, UserPendingActivationStore p ] r => @@ -494,7 +519,13 @@ createUserNoVerify uData = lift . runExceptT $ do in API.activate key code (Just uid) !>> activationErrorToRegisterError pure . SelfProfile $ usr -createUserNoVerifySpar :: NewUserSpar -> (Handler r) (Either CreateUserSparError SelfProfile) +createUserNoVerifySpar :: + Members + '[ GalleyProvider + ] + r => + NewUserSpar -> + (Handler r) (Either CreateUserSparError SelfProfile) createUserNoVerifySpar uData = lift . runExceptT $ do result <- API.createUserSpar uData diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index d94c6747b7..6620bf1f9f 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -48,11 +48,12 @@ import qualified Brig.Data.UserKey as UserKey import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.CodeStore (CodeStore) +import Brig.Effects.GalleyProvider (GalleyProvider) +import qualified Brig.Effects.GalleyProvider as GalleyProvider import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) -import qualified Brig.IO.Intra as Intra import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Provider.API as Provider import qualified Brig.Team.API as Team @@ -136,6 +137,7 @@ 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) -- User API ----------------------------------------------------------- @@ -163,11 +165,13 @@ servantSitemap :: Members '[ BlacklistStore, BlacklistPhonePrefixStore, + GalleyProvider, UserPendingActivationStore p, PasswordResetStore, CodeStore, JwtTools, PublicKeyBundle, + Concurrency 'Unsafe, Now ] r => @@ -292,7 +296,8 @@ sitemap :: '[ CodeStore, PasswordResetStore, BlacklistStore, - BlacklistPhonePrefixStore + BlacklistPhonePrefixStore, + GalleyProvider ] r => Routes Doc.ApiBuilder (Handler r) () @@ -308,7 +313,8 @@ apiDocs :: '[ CodeStore, PasswordResetStore, BlacklistStore, - BlacklistPhonePrefixStore + BlacklistPhonePrefixStore, + GalleyProvider ] r => Routes Doc.ApiBuilder (Handler r) () @@ -418,7 +424,16 @@ 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 + '[ GalleyProvider + ] + 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) $ @@ -522,6 +537,7 @@ createAccessToken method uid cid proof = do createUser :: Members '[ BlacklistStore, + GalleyProvider, UserPendingActivationStore p ] r => @@ -600,18 +616,41 @@ getSelf self = lift (API.lookupSelfProfile self) >>= ifNothing (errorToWai @'E.UserNotFound) -getUserUnqualifiedH :: UserId -> UserId -> (Handler r) (Maybe Public.UserProfile) +getUserUnqualifiedH :: + Members + '[ GalleyProvider + ] + 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 + '[ GalleyProvider + ] + 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 :: UserId -> Maybe (CommaSeparatedList UserId) -> Maybe (Range 1 4 (CommaSeparatedList Handle)) -> (Handler r) [Public.UserProfile] +listUsersByUnqualifiedIdsOrHandles :: + Members + '[ GalleyProvider, + Concurrency 'Unsafe + ] + 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 @@ -627,7 +666,16 @@ 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 + '[ GalleyProvider, + Concurrency 'Unsafe + ] + r => + UserId -> + Public.ListUsersQuery -> + (Handler r) [Public.UserProfile] listUsersByIdsOrHandles self q = do lself <- qualifyLocal self foundUsers <- case q of @@ -647,7 +695,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) @@ -713,7 +761,14 @@ checkHandles _ (Public.CheckHandles hs num) = do -- 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 + '[ GalleyProvider + ] + r => + UserId -> + Handle -> + (Handler r) (Maybe Public.UserHandleInfo) getHandleInfoUnqualifiedH self handle = do domain <- viewFederationDomain Public.UserHandleInfo . Public.profileQualifiedId @@ -748,7 +803,8 @@ completePasswordReset req = do sendActivationCode :: Members '[ BlacklistStore, - BlacklistPhonePrefixStore + BlacklistPhonePrefixStore, + GalleyProvider ] r => Public.SendActivationCode -> @@ -773,13 +829,29 @@ customerExtensionCheckBlockedDomains email = do when (domain `elem` blockedDomains) $ throwM $ customerExtensionBlockedDomain domain -createConnectionUnqualified :: UserId -> ConnId -> Public.ConnectionRequest -> (Handler r) (Public.ResponseForExistedCreated Public.UserConnection) +createConnectionUnqualified :: + Members + '[ GalleyProvider + ] + 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 :: + Members + '[ GalleyProvider + ] + 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 @@ -855,6 +927,10 @@ getConnection self other = do lift . wrapClient $ Data.lookupConnection lself other deleteSelfUser :: + Members + '[ GalleyProvider + ] + r => UserId -> Public.DeleteUser -> (Handler r) (Maybe Code.Timeout) @@ -864,7 +940,17 @@ deleteSelfUser u body = verifyDeleteUser :: Public.VerifyDeleteUser -> Handler r () verifyDeleteUser body = API.verifyDeleteUser body !>> deleteUserError -updateUserEmail :: Member BlacklistStore r => UserId -> UserId -> Public.EmailUpdate -> (Handler r) () +updateUserEmail :: + forall r. + Members + '[ BlacklistStore, + GalleyProvider + ] + 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 @@ -882,18 +968,31 @@ updateUserEmail zuserId emailOwnerId (Public.EmailUpdate email) = do where check = runMaybeT $ do teamId <- hoistMaybe maybeTeamId - teamMember <- MaybeT $ lift $ wrapHttp $ Intra.getTeamMember zuserId teamId + teamMember <- MaybeT $ lift $ liftSem $ GalleyProvider.getTeamMember zuserId teamId pure $ teamMember `hasPermission` ChangeTeamMemberProfiles -- activation -activate :: Public.ActivationKey -> Public.ActivationCode -> (Handler r) ActivationRespWithStatus +activate :: + Members + '[ GalleyProvider + ] + r => + Public.ActivationKey -> + Public.ActivationCode -> + (Handler r) ActivationRespWithStatus activate k c = do let activationRequest = Public.Activate (Public.ActivateKey k) c False activateKey activationRequest -- docs/reference/user/activation.md {#RefActivationSubmit} -activateKey :: Public.Activate -> (Handler r) ActivationRespWithStatus +activateKey :: + Members + '[ GalleyProvider + ] + r => + Public.Activate -> + (Handler r) ActivationRespWithStatus activateKey (Public.Activate tgt code dryrun) | dryrun = do wrapClientE (API.preverify tgt code) !>> actError @@ -907,7 +1006,14 @@ activateKey (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 + '[ GalleyProvider + ] + r => + Public.SendVerificationCode -> + (Handler r) () sendVerificationCode req = do let email = Public.svcEmail req let action = Public.svcAction req @@ -942,7 +1048,7 @@ sendVerificationCode req = do getFeatureStatus :: Maybe UserAccount -> (Handler r) Bool getFeatureStatus mbAccount = do - mbStatusEnabled <- lift $ wrapHttp $ Intra.getVerificationCodeEnabled `traverse` (Public.userTeam <$> accountUser =<< mbAccount) + mbStatusEnabled <- lift $ liftSem $ GalleyProvider.getVerificationCodeEnabled `traverse` (Public.userTeam <$> accountUser =<< mbAccount) pure $ fromMaybe False mbStatusEnabled -- Deprecated diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index ea3932461f..07c3d600e2 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -114,6 +114,8 @@ import Brig.Effects.BlacklistStore (BlacklistStore) import qualified Brig.Effects.BlacklistStore as BlacklistStore import Brig.Effects.CodeStore (CodeStore) import qualified Brig.Effects.CodeStore as E +import Brig.Effects.GalleyProvider (GalleyProvider) +import qualified Brig.Effects.GalleyProvider as GalleyProvider import Brig.Effects.PasswordResetStore (PasswordResetStore) import qualified Brig.Effects.PasswordResetStore as E import Brig.Effects.UserPendingActivationStore (UserPendingActivation (..), UserPendingActivationStore) @@ -184,6 +186,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 @@ -222,7 +225,14 @@ verifyUniquenessAndCheckBlacklist uk = do unless av $ throwE IdentityErrorUserKeyExists -createUserSpar :: NewUserSpar -> ExceptT CreateUserSparError (AppT r) CreateUserResult +createUserSpar :: + forall r. + Members + '[ GalleyProvider + ] + r => + NewUserSpar -> + ExceptT CreateUserSparError (AppT r) CreateUserResult createUserSpar new = do let handle' = newUserSparHandle new new' = newUserFromSpar new @@ -240,7 +250,7 @@ createUserSpar new = do case unRichInfo <$> newUserSparRichInfo new of Just richInfo -> wrapClient $ Data.updateRichInfo uid richInfo Nothing -> pure () -- Nothing to do - wrapHttp $ Intra.createSelfConv uid + liftSem $ GalleyProvider.createSelfConv uid wrapHttpClient $ Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) pure account @@ -267,7 +277,7 @@ createUserSpar new = do addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> ExceptT RegisterError (AppT r) CreateUserTeam addUserToTeamSSO account tid ident = do let uid = userId (accountUser account) - added <- lift $ wrapHttp $ Intra.addTeamMember uid tid (Nothing, defaultRole) + added <- lift $ liftSem $ GalleyProvider.addTeamMember uid tid (Nothing, defaultRole) unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do @@ -277,7 +287,7 @@ createUserSpar new = do field "user" (toByteString uid) . field "team" (toByteString tid) . msg (val "Added via SSO") - Team.TeamName nm <- lift $ wrapHttp $ Intra.getTeamName tid + Team.TeamName nm <- lift $ liftSem $ GalleyProvider.getTeamName tid pure $ CreateUserTeam tid nm -- docs/reference/user/registration.md {#RefRegistration} @@ -285,6 +295,7 @@ createUser :: forall r p. Members '[ BlacklistStore, + GalleyProvider, UserPendingActivationStore p ] r => @@ -342,7 +353,7 @@ createUser new = do Log.info $ field "user" (toByteString uid) . msg (val "Creating user") wrapClient $ Data.insertAccount account Nothing pw False - wrapHttp $ Intra.createSelfConv uid + liftSem $ GalleyProvider.createSelfConv uid wrapHttpClient $ Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) pure account @@ -353,7 +364,7 @@ createUser new = do activatedTeam <- lift $ do case (tid, newTeam) of (Just tid', Just nt) -> do - created <- wrapHttp $ Intra.createTeam uid (bnuTeam nt) tid' + created <- liftSem $ GalleyProvider.createTeam uid (bnuTeam nt) tid' let activating = isJust (newUserEmailCode new) pure $ if activating @@ -365,7 +376,7 @@ createUser new = do Just (inv, invInfo) -> do let em = Team.inInviteeEmail inv acceptTeamInvitation account inv invInfo (userEmailKey em) (EmailIdentity em) - Team.TeamName nm <- lift $ wrapHttp $ Intra.getTeamName (Team.inTeam inv) + Team.TeamName nm <- lift $ liftSem $ GalleyProvider.getTeamName (Team.inTeam inv) pure (Just $ CreateUserTeam (Team.inTeam inv) nm) Nothing -> pure Nothing @@ -431,7 +442,7 @@ createUser new = do throwE RegisterErrorTooManyTeamMembers -- FUTUREWORK: The above can easily be done/tested in the intra call. -- Remove after the next release. - canAdd <- lift $ wrapHttp $ Intra.checkUserCanJoinTeam tid + canAdd <- lift $ liftSem $ GalleyProvider.checkUserCanJoinTeam tid case canAdd of Just e -> throwM $ API.UserNotAllowedToJoinTeam e Nothing -> pure () @@ -450,7 +461,7 @@ createUser new = do throwE RegisterErrorUserKeyExists let minvmeta :: (Maybe (UserId, UTCTimeMillis), Role) minvmeta = ((,inCreatedAt inv) <$> inCreatedBy inv, Team.inRole inv) - added <- lift $ wrapHttp $ Intra.addTeamMember uid (Team.iiTeam ii) minvmeta + added <- lift $ liftSem $ GalleyProvider.addTeamMember uid (Team.iiTeam ii) minvmeta unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do @@ -467,7 +478,7 @@ createUser new = do addUserToTeamSSO :: UserAccount -> TeamId -> UserIdentity -> ExceptT RegisterError (AppT r) CreateUserTeam addUserToTeamSSO account tid ident = do let uid = userId (accountUser account) - added <- lift $ wrapHttp $ Intra.addTeamMember uid tid (Nothing, defaultRole) + added <- lift $ liftSem $ GalleyProvider.addTeamMember uid tid (Nothing, defaultRole) unless added $ throwE RegisterErrorTooManyTeamMembers lift $ do @@ -477,7 +488,7 @@ createUser new = do field "user" (toByteString uid) . field "team" (toByteString tid) . msg (val "Added via SSO") - Team.TeamName nm <- lift $ wrapHttp $ Intra.getTeamName tid + Team.TeamName nm <- lift $ liftSem $ GalleyProvider.getTeamName tid pure $ CreateUserTeam tid nm -- Handle e-mail activation (deprecated, see #RefRegistrationNoPreverification in /docs/reference/user/registration.md) @@ -890,6 +901,7 @@ mkUserEvent usrs status = -- Activation activate :: + Members '[GalleyProvider] r => ActivationTarget -> ActivationCode -> -- | The user for whom to activate the key. @@ -898,6 +910,7 @@ activate :: activate tgt code usr = activateWithCurrency tgt code usr Nothing activateWithCurrency :: + Members '[GalleyProvider] r => ActivationTarget -> ActivationCode -> -- | The user for whom to activate the key. @@ -923,8 +936,8 @@ activateWithCurrency tgt code usr cur = do pure $ ActivationSuccess ident first where activateTeam uid = do - tid <- wrapHttp $ Intra.getTeamId uid - for_ tid $ \t -> wrapHttp $ Intra.changeTeamStatus t Team.Active cur + tid <- liftSem $ GalleyProvider.getTeamId uid + for_ tid $ \t -> liftSem $ GalleyProvider.changeTeamStatus t Team.Active cur preverify :: ( MonadClient m, @@ -956,7 +969,8 @@ onActivated (PhoneActivated uid phone) = do sendActivationCode :: Members '[ BlacklistStore, - BlacklistPhonePrefixStore + BlacklistPhonePrefixStore, + GalleyProvider ] r => Either Email Phone -> @@ -1032,7 +1046,7 @@ sendActivationCode emailOrPhone loc call = case emailOrPhone of loc' = loc <|> Just (userLocale u) void . forEmailKey ek $ \em -> lift $ do -- Get user's team, if any. - mbTeam <- mapM (fmap Team.tdTeam . wrapHttp . Intra.getTeam) (userTeam u) + mbTeam <- mapM (fmap Team.tdTeam . liftSem . GalleyProvider.getTeam) (userTeam u) -- Depending on whether the user is a team creator, send either -- a team activation email or a regular email. Note that we -- don't have to check if the team is binding because if the @@ -1151,7 +1165,15 @@ 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 :: + forall r. + Members + '[ GalleyProvider + ] + r => + UserId -> + Maybe PlainTextPassword -> + ExceptT DeleteUserError (AppT r) (Maybe Timeout) deleteSelfUser uid pwd = do account <- lift . wrapClient $ Data.lookupAccount uid case account of @@ -1168,7 +1190,7 @@ deleteSelfUser uid pwd = do case userTeam $ accountUser acc of Nothing -> pure () Just tid -> do - isOwner <- lift $ wrapHttp $ Intra.memberIsTeamOwner tid uid + isOwner <- lift $ liftSem $ GalleyProvider.memberIsTeamOwner tid uid when isOwner $ throwE DeleteUserOwnerDeletingSelf go a = maybe (byIdentity a) (byPassword a) pwd getEmailOrPhone :: UserIdentity -> Maybe (Either Email Phone) @@ -1400,16 +1422,10 @@ userGC u = case userExpire u of pure u lookupProfile :: - ( MonadClient m, - MonadReader Env m, - MonadLogger m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => + Members '[GalleyProvider] r => Local UserId -> Qualified UserId -> - ExceptT FederationError m (Maybe UserProfile) + ExceptT FederationError (AppT r) (Maybe UserProfile) lookupProfile self other = listToMaybe <$> lookupProfilesFromDomain @@ -1422,41 +1438,32 @@ 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 + '[ GalleyProvider, + Concurrency 'Unsafe + ] + r => -- | 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 + <$> traverseConcurrentlyWithErrorsAppT (lookupProfilesFromDomain self) (bucketQualified others) lookupProfilesFromDomain :: - ( MonadClient m, - MonadReader Env m, - MonadLogger m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => + Members '[GalleyProvider] r => Local UserId -> Qualified [UserId] -> - ExceptT FederationError m [UserProfile] + ExceptT FederationError (AppT r) [UserProfile] lookupProfilesFromDomain self = foldQualified self (lift . lookupLocalProfiles (Just (tUnqualified self)) . tUnqualified) - lookupRemoteProfiles + (mapExceptT wrapHttp . lookupRemoteProfiles) lookupRemoteProfiles :: ( MonadIO m, @@ -1472,23 +1479,17 @@ 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 '[GalleyProvider] 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 - users <- Data.lookupUsers NoPendingInvitations others >>= mapM userGC + users <- wrapHttpClient $ Data.lookupUsers NoPendingInvitations others >>= mapM userGC css <- case requestingUser of - Just localReqUser -> toMap <$> Data.lookupConnectionStatus (map userId users) [localReqUser] + Just localReqUser -> toMap <$> wrapHttpClient (Data.lookupConnectionStatus (map userId users) [localReqUser]) Nothing -> pure mempty emailVisibility' <- view (settings . emailVisibility) emailVisibility'' <- case emailVisibility' of @@ -1497,21 +1498,21 @@ lookupLocalProfiles requestingUser others = do Just localReqUser -> EmailVisibleIfOnSameTeam' <$> getSelfInfo localReqUser Nothing -> pure EmailVisibleToSelf' EmailVisibleToSelf -> pure EmailVisibleToSelf' - usersAndStatus <- for users $ \u -> (u,) <$> getLegalHoldStatus' u + usersAndStatus <- liftSem $ for users $ \u -> (u,) <$> getLegalHoldStatus' u pure $ map (toProfile emailVisibility'' css) usersAndStatus where toMap :: [ConnectionStatus] -> Map UserId Relation toMap = Map.fromList . map (csFrom &&& csStatus) - getSelfInfo :: UserId -> m (Maybe (TeamId, TeamMember)) + getSelfInfo :: UserId -> AppT r (Maybe (TeamId, TeamMember)) getSelfInfo 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 <- wrapHttp $ Data.lookupUser NoPendingInvitations selfId case userTeam =<< mUser of Nothing -> pure Nothing - Just tid -> (tid,) <$$> Intra.getTeamMember selfId tid + Just tid -> (tid,) <$$> liftSem (GalleyProvider.getTeamMember selfId tid) toProfile :: EmailVisibility' -> Map UserId Relation -> (User, UserLegalHoldStatus) -> UserProfile toProfile emailVisibility'' css (u, userLegalHold) = @@ -1524,32 +1525,20 @@ lookupLocalProfiles requestingUser others = do in baseProfile {profileEmail = profileEmail'} getLegalHoldStatus :: - ( MonadLogger m, - MonadReader Env m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadClient m - ) => + Members '[GalleyProvider] r => UserId -> - m (Maybe UserLegalHoldStatus) -getLegalHoldStatus uid = traverse (getLegalHoldStatus' . accountUser) =<< lookupAccount uid + AppT r (Maybe UserLegalHoldStatus) +getLegalHoldStatus uid = traverse (liftSem . getLegalHoldStatus' . accountUser) =<< wrapHttpClient (lookupAccount uid) getLegalHoldStatus' :: - ( MonadLogger m, - MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => + Members '[GalleyProvider] r => User -> - m UserLegalHoldStatus + Sem r UserLegalHoldStatus getLegalHoldStatus' user = case userTeam user of Nothing -> pure defUserLegalHoldStatus Just tid -> do - teamMember <- Intra.getTeamMember (userId user) tid + teamMember <- GalleyProvider.getTeamMember (userId user) tid pure $ maybe defUserLegalHoldStatus (^. legalHoldStatus) teamMember data EmailVisibility' diff --git a/services/brig/src/Brig/API/Util.hs b/services/brig/src/Brig/API/Util.hs index 01607c93bd..322fbf391b 100644 --- a/services/brig/src/Brig/API/Util.hs +++ b/services/brig/src/Brig/API/Util.hs @@ -23,6 +23,8 @@ module Brig.API.Util validateHandle, logEmail, traverseConcurrentlyWithErrors, + traverseConcurrentlyWithErrorsSem, + traverseConcurrentlyWithErrorsAppT, exceptTToMaybe, lookupSearchPolicy, ensureLocal, @@ -50,6 +52,8 @@ import Data.Qualified import Data.String.Conversions (cs) import Data.Text.Ascii (AsciiText (toText)) import Imports +import Polysemy +import qualified Polysemy.Error as E import System.Logger (Msg) import qualified System.Logger as Log import UnliftIO.Async @@ -60,6 +64,7 @@ import Wire.API.Error.Brig import Wire.API.Federation.Error import Wire.API.User import Wire.API.User.Search (FederatedUserSearchPolicy (NoSearch)) +import qualified Wire.Sem.Concurrency as C lookupProfilesMaybeFilterSameTeamOnly :: UserId -> [UserProfile] -> (Handler r) [UserProfile] lookupProfilesMaybeFilterSameTeamOnly self us = do @@ -103,6 +108,35 @@ traverseConcurrentlyWithErrors f = <=< pooledMapConcurrentlyN 8 (runExceptT . f) ) +-- | Traverse concurrently and fail on first error. +traverseConcurrentlyWithErrorsSem :: + forall t e a r b. + (Traversable t, Member (C.Concurrency 'C.Unsafe) r) => + (a -> ExceptT e (Sem r) b) -> + t a -> + ExceptT e (Sem r) [b] +traverseConcurrentlyWithErrorsSem f = + ExceptT . E.runError + . ( traverse (either E.throw pure) + <=< C.unsafePooledMapConcurrentlyN 8 (raise . runExceptT . f) + ) + +traverseConcurrentlyWithErrorsAppT :: + forall t e a r b. + (Traversable t, Member (C.Concurrency 'C.Unsafe) r) => + (a -> ExceptT e (AppT r) b) -> + t a -> + ExceptT e (AppT r) [b] +traverseConcurrentlyWithErrorsAppT f t = do + env <- lift temporaryGetEnv + ExceptT $ + AppT $ + lift $ + runExceptT $ + traverseConcurrentlyWithErrorsSem + (mapExceptT (lowerAppT env) . f) + t + exceptTToMaybe :: Monad m => ExceptT e m () -> m (Maybe e) exceptTToMaybe = (pure . either Just (const Nothing)) <=< runExceptT diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 4a18a47ec8..1b8a7d388d 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -78,6 +78,8 @@ module Brig.App wrapHttp, HttpClientIO (..), liftSem, + lowerAppT, + temporaryGetEnv, ) where @@ -438,6 +440,12 @@ newtype AppT r a = AppT ) via (Ap (AppT r) a) +lowerAppT :: Member (Final IO) r => Env -> AppT r a -> Sem r a +lowerAppT env = flip runReaderT env . unAppT + +temporaryGetEnv :: AppT r Env +temporaryGetEnv = AppT ask + instance Functor (AppT r) where fmap fab (AppT x0) = AppT $ fmap fab x0 diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index c05ec6710f..3214185917 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -7,16 +7,29 @@ import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BlacklistStore.Cassandra (interpretBlacklistStoreToCassandra) import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.CodeStore.Cassandra (codeStoreToCassandra, interpretClientToIO) +import Brig.Effects.GalleyProvider (GalleyProvider) +import Brig.Effects.GalleyProvider.RPC (interpretGalleyProviderToRPC) import Brig.Effects.JwtTools import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.PasswordResetStore.CodeStore (passwordResetStoreToCodeStore) import Brig.Effects.PublicKeyBundle +import Brig.Effects.RPC (RPC) +import Brig.Effects.RPC.IO (interpretRpcToIO) +import Brig.Effects.ServiceRPC (Service (Galley), ServiceRPC) +import Brig.Effects.ServiceRPC.IO (interpretServiceRpcToRpc) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore.Cassandra (userPendingActivationStoreToCassandra) +import Brig.RPC (ParseException) import qualified Cassandra as Cas import Control.Lens ((^.)) +import Control.Monad.Catch (throwM) import Imports import Polysemy (Embed, Final, embedToFinal, runFinal) +import Polysemy.Error (Error, mapError, runError) +import Polysemy.TinyLog (TinyLog) +import Wire.Sem.Concurrency (Concurrency, ConcurrencySafety (Unsafe)) +import Wire.Sem.Concurrency.IO (unsafelyPerformConcurrency) +import Wire.Sem.Logger.TinyLog (loggerToTinyLog) import Wire.Sem.Now (Now) import Wire.Sem.Now.IO (nowToIOAction) import Wire.Sem.Paging.Cassandra (InternalPaging) @@ -30,16 +43,31 @@ type BrigCanonicalEffects = UserPendingActivationStore InternalPaging, Now, CodeStore, + GalleyProvider, + ServiceRPC 'Galley, + RPC, Embed Cas.Client, + Error ParseException, + Error SomeException, + TinyLog, Embed IO, + Concurrency 'Unsafe, Final IO ] runBrigToIO :: Env -> AppT BrigCanonicalEffects a -> IO a -runBrigToIO e (AppT ma) = - runFinal +runBrigToIO e (AppT ma) = do + (either throwM pure =<<) + . runFinal + . unsafelyPerformConcurrency . embedToFinal + . loggerToTinyLog (e ^. applog) + . runError @SomeException + . mapError @ParseException SomeException . interpretClientToIO (e ^. casClient) + . interpretRpcToIO (e ^. httpManager) (e ^. requestId) + . interpretServiceRpcToRpc @'Galley "galley" (e ^. galley) + . interpretGalleyProviderToRPC . codeStoreToCassandra @Cas.Client . nowToIOAction (e ^. currentTime) . userPendingActivationStoreToCassandra diff --git a/services/brig/src/Brig/Effects/GalleyProvider.hs b/services/brig/src/Brig/Effects/GalleyProvider.hs new file mode 100644 index 0000000000..e92c14e720 --- /dev/null +++ b/services/brig/src/Brig/Effects/GalleyProvider.hs @@ -0,0 +1,93 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Brig.Effects.GalleyProvider where + +import Brig.API.Types +import Brig.Team.Types (ShowOrHideInvitationUrl (..)) +import qualified Data.Currency as Currency +import Data.Id +import Data.Json.Util (UTCTimeMillis) +import qualified Galley.Types.Teams.Intra as Team +import Imports +import qualified Network.Wai.Utilities.Error as Wai +import Polysemy +import Wire.API.Conversation +import Wire.API.Team +import qualified Wire.API.Team.Conversation as Conv +import Wire.API.Team.Feature +import qualified Wire.API.Team.Member as Team +import Wire.API.Team.Role +import Wire.API.Team.SearchVisibility + +data GalleyProvider m a where + CreateSelfConv :: + UserId -> + GalleyProvider m () + GetConv :: + UserId -> + ConvId -> + GalleyProvider m (Maybe Conversation) + GetTeamConv :: + UserId -> + TeamId -> + ConvId -> + GalleyProvider m (Maybe Conv.TeamConversation) + NewClient :: + UserId -> + ClientId -> + GalleyProvider m () + CheckUserCanJoinTeam :: + TeamId -> + GalleyProvider m (Maybe Wai.Error) + AddTeamMember :: + UserId -> + TeamId -> + (Maybe (UserId, UTCTimeMillis), Role) -> + GalleyProvider m Bool + CreateTeam :: + UserId -> + BindingNewTeam -> + TeamId -> + GalleyProvider m CreateUserTeam + GetTeamMember :: + UserId -> + TeamId -> + GalleyProvider m (Maybe Team.TeamMember) + GetTeamMembers :: + TeamId -> + GalleyProvider m Team.TeamMemberList + GetTeamId :: + UserId -> + GalleyProvider m (Maybe TeamId) + GetTeam :: + TeamId -> + GalleyProvider m Team.TeamData + GetTeamName :: + TeamId -> + GalleyProvider m Team.TeamName + GetTeamLegalHoldStatus :: + TeamId -> + GalleyProvider m (WithStatus LegalholdConfig) + GetTeamSearchVisibility :: + TeamId -> + GalleyProvider m TeamSearchVisibility + ChangeTeamStatus :: + TeamId -> + Team.TeamStatus -> + Maybe Currency.Alpha -> + GalleyProvider m () + MemberIsTeamOwner :: + TeamId -> + UserId -> + GalleyProvider m Bool + GetAllFeatureConfigsForUser :: + Maybe UserId -> + GalleyProvider m AllFeatureConfigs + GetVerificationCodeEnabled :: + TeamId -> + GalleyProvider m Bool + GetExposeInvitationURLsToTeamAdmin :: + TeamId -> + GalleyProvider m ShowOrHideInvitationUrl + +makeSem ''GalleyProvider diff --git a/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs new file mode 100644 index 0000000000..e84ef002e0 --- /dev/null +++ b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs @@ -0,0 +1,490 @@ +{-# OPTIONS_GHC -Wno-unused-matches #-} + +module Brig.Effects.GalleyProvider.RPC where + +import Bilge hiding (head, options, requestId) +import Brig.API.Types +import Brig.Effects.GalleyProvider (GalleyProvider (..)) +import Brig.Effects.ServiceRPC (Service (Galley), ServiceRPC) +import qualified Brig.Effects.ServiceRPC as ServiceRPC +import Brig.RPC +import Brig.Team.Types (ShowOrHideInvitationUrl (..)) +import Control.Error (hush) +import Control.Lens ((^.)) +import Data.Aeson hiding (json) +import Data.ByteString.Conversion +import qualified Data.ByteString.Lazy as BL +import Data.Coerce (coerce) +import qualified Data.Currency as Currency +import Data.Id +import Data.Json.Util (UTCTimeMillis) +import Data.Range +import qualified Galley.Types.Teams as Team +import qualified Galley.Types.Teams.Intra as Team +import Imports +import Network.HTTP.Types.Method +import Network.HTTP.Types.Status +import qualified Network.Wai.Utilities.Error as Wai +import Polysemy +import Polysemy.Error +import System.Logger (Msg, field, msg, val) +import Wire.API.Conversation hiding (Member) +import Wire.API.Team +import qualified Wire.API.Team.Conversation as Conv +import Wire.API.Team.Feature +import qualified Wire.API.Team.Member as Member +import qualified Wire.API.Team.Member as Team +import Wire.API.Team.Role +import Wire.API.Team.SearchVisibility +import Wire.Sem.Logger + +interpretGalleyProviderToRPC :: + Members + '[ Error ParseException, + ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + Sem (GalleyProvider ': r) a -> + Sem r a +interpretGalleyProviderToRPC = interpret $ \case + CreateSelfConv id' -> createSelfConv id' + GetConv id' id'' -> getConv id' id'' + GetTeamConv id' id'' id'2 -> getTeamConv id' id'' id'2 + NewClient id' ci -> newClient id' ci + CheckUserCanJoinTeam id' -> checkUserCanJoinTeam id' + AddTeamMember id' id'' x0 -> addTeamMember id' id'' x0 + CreateTeam id' bnt id'' -> createTeam id' bnt id'' + GetTeamMember id' id'' -> getTeamMember id' id'' + GetTeamMembers id' -> getTeamMembers id' + GetTeamId id' -> getTeamId id' + GetTeam id' -> getTeam id' + GetTeamName id' -> getTeamName id' + GetTeamLegalHoldStatus id' -> getTeamLegalHoldStatus id' + GetTeamSearchVisibility id' -> getTeamSearchVisibility id' + ChangeTeamStatus id' ts m_al -> changeTeamStatus id' ts m_al + MemberIsTeamOwner id' id'' -> memberIsTeamOwner id' id'' + GetAllFeatureConfigsForUser m_id' -> getAllFeatureConfigsForUser m_id' + GetVerificationCodeEnabled id' -> getVerificationCodeEnabled id' + GetExposeInvitationURLsToTeamAdmin id' -> getTeamExposeInvitationURLsToTeamAdmin id' + +-- | Calls 'Galley.API.createSelfConversationH'. +createSelfConv :: + Members + '[ ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + UserId -> + Sem r () +createSelfConv u = do + debug $ + remote "galley" + . msg (val "Creating self conversation") + void $ ServiceRPC.request @'Galley POST req + where + req = + path "/conversations/self" + . zUser u + . expect2xx + +-- | Calls 'Galley.API.getConversationH'. +getConv :: + Members + '[ Error ParseException, + ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + UserId -> + ConvId -> + Sem r (Maybe Conversation) +getConv usr cnv = do + debug $ + remote "galley" + . field "conv" (toByteString cnv) + . msg (val "Getting conversation") + rs <- ServiceRPC.request @'Galley GET req + case Bilge.statusCode rs of + 200 -> Just <$> decodeBodyOrThrow "galley" rs + _ -> pure Nothing + where + req = + paths ["conversations", toByteString' cnv] + . zUser usr + . expect [status200, status404] + +-- | Calls 'Galley.API.getTeamConversationH'. +getTeamConv :: + Members + '[ Error ParseException, + ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + UserId -> + TeamId -> + ConvId -> + Sem r (Maybe Conv.TeamConversation) +getTeamConv usr tid cnv = do + debug $ + remote "galley" + . field "conv" (toByteString cnv) + . msg (val "Getting team conversation") + rs <- ServiceRPC.request @'Galley GET req + case Bilge.statusCode rs of + 200 -> Just <$> decodeBodyOrThrow "galley" rs + _ -> pure Nothing + where + req = + paths ["teams", toByteString' tid, "conversations", toByteString' cnv] + . zUser usr + . expect [status200, status404] + +-- | Calls 'Galley.API.addClientH'. +newClient :: + Members + '[ ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + UserId -> + ClientId -> + Sem r () +newClient u c = do + debug $ + remote "galley" + . field "user" (toByteString u) + . field "client" (toByteString c) + . msg (val "new client") + let p = paths ["i", "clients", toByteString' c] + void $ ServiceRPC.request @'Galley POST (p . zUser u . expect2xx) + +-- | Calls 'Galley.API.canUserJoinTeamH'. +checkUserCanJoinTeam :: + Members + '[ ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + TeamId -> + Sem r (Maybe Wai.Error) +checkUserCanJoinTeam tid = do + debug $ + remote "galley" + . msg (val "Check if can add member to team") + rs <- ServiceRPC.request @'Galley GET req + pure $ case Bilge.statusCode rs of + 200 -> Nothing + _ -> case decodeBodyMaybe "galley" rs of + Just (e :: Wai.Error) -> pure e + Nothing -> error ("Invalid response from galley: " <> show rs) + where + req = + paths ["i", "teams", toByteString' tid, "members", "check"] + . header "Content-Type" "application/json" + +-- | Calls 'Galley.API.uncheckedAddTeamMemberH'. +addTeamMember :: + Members + '[ ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + UserId -> + TeamId -> + (Maybe (UserId, UTCTimeMillis), Role) -> + Sem r Bool +addTeamMember u tid (minvmeta, role) = do + debug $ + remote "galley" + . msg (val "Adding member to team") + rs <- ServiceRPC.request @'Galley POST req + pure $ case Bilge.statusCode rs of + 200 -> True + _ -> False + where + prm = Team.rolePermissions role + bdy = Member.mkNewTeamMember u prm minvmeta + req = + paths ["i", "teams", toByteString' tid, "members"] + . header "Content-Type" "application/json" + . zUser u + . expect [status200, status403] + . lbytes (encode bdy) + +-- | Calls 'Galley.API.createBindingTeamH'. +createTeam :: + Members + '[ ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + UserId -> + BindingNewTeam -> + TeamId -> + Sem r CreateUserTeam +createTeam u t@(BindingNewTeam bt) teamid = do + debug $ + remote "galley" + . msg (val "Creating Team") + r <- ServiceRPC.request @'Galley PUT $ req teamid + tid <- + maybe (error "invalid team id") pure $ + fromByteString $ + getHeader' "Location" r + pure (CreateUserTeam tid $ fromRange (bt ^. newTeamName)) + where + req tid = + paths ["i", "teams", toByteString' tid] + . header "Content-Type" "application/json" + . zUser u + . expect2xx + . lbytes (encode t) + +-- | Calls 'Galley.API.uncheckedGetTeamMemberH'. +getTeamMember :: + Members + '[ Error ParseException, + ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + UserId -> + TeamId -> + Sem r (Maybe Team.TeamMember) +getTeamMember u tid = do + debug $ + remote "galley" + . msg (val "Get team member") + rs <- ServiceRPC.request @'Galley GET req + case Bilge.statusCode rs of + 200 -> Just <$> decodeBodyOrThrow "galley" rs + _ -> pure Nothing + where + req = + paths ["i", "teams", toByteString' tid, "members", toByteString' u] + . zUser u + . expect [status200, status404] + +-- | Calls 'Galley.API.uncheckedGetTeamMembersH'. +-- +-- | TODO: is now truncated. this is (only) used for team suspension / unsuspension, which +-- means that only the first 2000 members of a team (according to some arbitrary order) will +-- be suspended, and the rest will remain active. +getTeamMembers :: + Members + '[ Error ParseException, + ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + TeamId -> + Sem r Team.TeamMemberList +getTeamMembers tid = do + debug $ remote "galley" . msg (val "Get team members") + ServiceRPC.request @'Galley GET req >>= decodeBodyOrThrow "galley" + where + req = + paths ["i", "teams", toByteString' tid, "members"] + . expect2xx + +memberIsTeamOwner :: + Members + '[ ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + TeamId -> + UserId -> + Sem r Bool +memberIsTeamOwner tid uid = do + r <- + ServiceRPC.request @'Galley GET $ + paths ["i", "teams", toByteString' tid, "is-team-owner", toByteString' uid] + pure $ responseStatus r /= status403 + +-- | Calls 'Galley.API.getBindingTeamIdH'. +getTeamId :: + Members + '[ Error ParseException, + ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + UserId -> + Sem r (Maybe TeamId) +getTeamId u = do + debug $ remote "galley" . msg (val "Get team from user") + rs <- ServiceRPC.request @'Galley GET req + case Bilge.statusCode rs of + 200 -> Just <$> decodeBodyOrThrow "galley" rs + _ -> pure Nothing + where + req = + paths ["i", "users", toByteString' u, "team"] + . expect [status200, status404] + +-- | Calls 'Galley.API.getTeamInternalH'. +getTeam :: + Members + '[ Error ParseException, + ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + TeamId -> + Sem r Team.TeamData +getTeam tid = do + debug $ remote "galley" . msg (val "Get team info") + ServiceRPC.request @'Galley GET req >>= decodeBodyOrThrow "galley" + where + req = + paths ["i", "teams", toByteString' tid] + . expect2xx + +-- | Calls 'Galley.API.getTeamInternalH'. +getTeamName :: + Members + '[ Error ParseException, + ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + TeamId -> + Sem r Team.TeamName +getTeamName tid = do + debug $ remote "galley" . msg (val "Get team info") + ServiceRPC.request @'Galley GET req >>= decodeBodyOrThrow "galley" + where + req = + paths ["i", "teams", toByteString' tid, "name"] + . expect2xx + +-- | Calls 'Galley.API.getTeamFeatureStatusH'. +getTeamLegalHoldStatus :: + Members + '[ Error ParseException, + ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + TeamId -> + Sem r (WithStatus LegalholdConfig) +getTeamLegalHoldStatus tid = do + debug $ remote "galley" . msg (val "Get legalhold settings") + ServiceRPC.request @'Galley GET req >>= decodeBodyOrThrow "galley" + where + req = + paths ["i", "teams", toByteString' tid, "features", featureNameBS @LegalholdConfig] + . expect2xx + +-- | Calls 'Galley.API.getSearchVisibilityInternalH'. +getTeamSearchVisibility :: + Members + '[ Error ParseException, + ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + TeamId -> + Sem r TeamSearchVisibility +getTeamSearchVisibility tid = + coerce @TeamSearchVisibilityView @TeamSearchVisibility <$> do + debug $ remote "galley" . msg (val "Get search visibility settings") + ServiceRPC.request @'Galley GET req >>= decodeBodyOrThrow "galley" + where + req = + paths ["i", "teams", toByteString' tid, "search-visibility"] + . expect2xx + +getVerificationCodeEnabled :: + Members + '[ Error ParseException, + ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + TeamId -> + Sem r Bool +getVerificationCodeEnabled tid = do + debug $ remote "galley" . msg (val "Get snd factor password challenge settings") + response <- ServiceRPC.request @'Galley GET req + status <- wsStatus <$> decodeBodyOrThrow @(WithStatus SndFactorPasswordChallengeConfig) "galley" response + case status of + FeatureStatusEnabled -> pure True + FeatureStatusDisabled -> pure False + where + req = + paths ["i", "teams", toByteString' tid, "features", featureNameBS @SndFactorPasswordChallengeConfig] + . expect2xx + +decodeBodyOrThrow :: forall a r. (Typeable a, FromJSON a, Member (Error ParseException) r) => Text -> Response (Maybe BL.ByteString) -> Sem r a +decodeBodyOrThrow t r = + case decodeBody @a t r of + Left a -> + case Imports.fromException a of + Just pe -> throw @ParseException pe + Nothing -> error "impossible: something other than ParseExceptionNothing was thrown by decodeBody" + Right b -> pure b + +decodeBodyMaybe :: (Typeable a, FromJSON a) => Text -> Response (Maybe BL.ByteString) -> Maybe a +decodeBodyMaybe t r = hush $ decodeBody t r + +getAllFeatureConfigsForUser :: + Members + '[ ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + Maybe UserId -> + Sem r AllFeatureConfigs +getAllFeatureConfigsForUser mbUserId = + responseJsonUnsafe + <$> ServiceRPC.request @'Galley + GET + ( paths ["i", "feature-configs"] + . maybe id (queryItem "user_id" . toByteString') mbUserId + ) + +-- | Calls 'Galley.API.updateTeamStatusH'. +changeTeamStatus :: + Members + '[ ServiceRPC 'Galley, + Logger (Msg -> Msg) + ] + r => + TeamId -> + Team.TeamStatus -> + Maybe Currency.Alpha -> + Sem r () +changeTeamStatus tid s cur = do + debug $ remote "galley" . msg (val "Change Team status") + void $ ServiceRPC.request @'Galley PUT req + where + req = + paths ["i", "teams", toByteString' tid, "status"] + . header "Content-Type" "application/json" + . expect2xx + . lbytes (encode $ Team.TeamStatusUpdate s cur) + +getTeamExposeInvitationURLsToTeamAdmin :: + Members + '[ ServiceRPC 'Galley, + Error ParseException, + Logger (Msg -> Msg) + ] + r => + TeamId -> + Sem r ShowOrHideInvitationUrl +getTeamExposeInvitationURLsToTeamAdmin tid = do + debug $ remote "galley" . msg (val "Get expose invitation URLs to team admin settings") + response <- ServiceRPC.request @'Galley GET req + status <- wsStatus <$> decodeBodyOrThrow @(WithStatus ExposeInvitationURLsToTeamAdminConfig) "galley" response + case status of + FeatureStatusEnabled -> pure ShowInvitationUrl + FeatureStatusDisabled -> pure HideInvitationUrl + where + req = + paths ["i", "teams", toByteString' tid, "features", featureNameBS @ExposeInvitationURLsToTeamAdminConfig] + . expect2xx diff --git a/services/brig/src/Brig/Effects/RPC.hs b/services/brig/src/Brig/Effects/RPC.hs new file mode 100644 index 0000000000..9b6e928ca0 --- /dev/null +++ b/services/brig/src/Brig/Effects/RPC.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Brig.Effects.RPC where + +import Bilge +import qualified Data.ByteString.Lazy as BL +import qualified Data.Text.Lazy as LT +import Imports +import Network.HTTP.Types.Method +import Polysemy + +data RPC m a where + ServiceRequest :: + LT.Text -> + Request -> + StdMethod -> + (Request -> Request) -> + RPC m (Response (Maybe BL.ByteString)) + +makeSem ''RPC diff --git a/services/brig/src/Brig/Effects/RPC/IO.hs b/services/brig/src/Brig/Effects/RPC/IO.hs new file mode 100644 index 0000000000..4f1b13d062 --- /dev/null +++ b/services/brig/src/Brig/Effects/RPC/IO.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} + +module Brig.Effects.RPC.IO where + +import Bilge (HttpT, MonadHttp, RequestId) +import Bilge.IO (Manager, runHttpT) +import Bilge.RPC +import Brig.Effects.RPC +import qualified Brig.RPC as RPC +import Control.Monad.Catch +import Imports +import Polysemy + +interpretRpcToIO :: Members '[Final IO] r => Manager -> RequestId -> Sem (RPC ': r) a -> Sem r a +interpretRpcToIO mgr rid = interpret $ \case + ServiceRequest txt f sm g -> + embedFinal @IO $ viaHttpIO mgr rid $ RPC.serviceRequestImpl txt f sm g + +viaHttpIO :: Manager -> RequestId -> HttpIO a -> IO a +viaHttpIO mgr rid = runHttpT mgr . flip runReaderT rid . runHttpIO + +newtype HttpIO a = HttpIO + { runHttpIO :: ReaderT RequestId (HttpT IO) a + } + deriving newtype + ( Functor, + Applicative, + Monad, + MonadHttp, + MonadIO, + MonadThrow, + MonadCatch, + MonadMask, + MonadUnliftIO + ) + +instance HasRequestId HttpIO where + getRequestId = HttpIO ask diff --git a/services/brig/src/Brig/Effects/ServiceRPC.hs b/services/brig/src/Brig/Effects/ServiceRPC.hs new file mode 100644 index 0000000000..8753a613f3 --- /dev/null +++ b/services/brig/src/Brig/Effects/ServiceRPC.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE TemplateHaskell #-} + +module Brig.Effects.ServiceRPC where + +import Bilge +import qualified Data.ByteString.Lazy as BL +import Imports +import Network.HTTP.Types.Method +import Polysemy + +data Service + = Galley + +data ServiceRPC (service :: Service) m a where + Request :: + StdMethod -> + (Request -> Request) -> + ServiceRPC service m (Response (Maybe BL.ByteString)) + +makeSem ''ServiceRPC diff --git a/services/brig/src/Brig/Effects/ServiceRPC/IO.hs b/services/brig/src/Brig/Effects/ServiceRPC/IO.hs new file mode 100644 index 0000000000..51dcc4191e --- /dev/null +++ b/services/brig/src/Brig/Effects/ServiceRPC/IO.hs @@ -0,0 +1,18 @@ +module Brig.Effects.ServiceRPC.IO where + +import Bilge (Request) +import Brig.Effects.RPC +import Brig.Effects.ServiceRPC +import qualified Data.Text.Lazy as LT +import Imports +import Polysemy + +interpretServiceRpcToRpc :: + forall service r a. + Member RPC r => + LT.Text -> + Request -> + Sem (ServiceRPC service ': r) a -> + Sem r a +interpretServiceRpcToRpc lt r = interpret $ \case + Request sm f -> serviceRequest lt r sm f diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index c5c4b65042..88c1894a97 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -26,41 +26,19 @@ module Brig.IO.Intra onClientEvent, -- * Conversations - createSelfConv, createConnectConv, acceptConnectConv, blockConv, unblockConv, - getConv, upsertOne2OneConversation, -- * Clients - Brig.IO.Intra.newClient, rmClient, lookupPushToken, -- * Account Deletion rmUser, - -- * Teams - addTeamMember, - checkUserCanJoinTeam, - createTeam, - getTeamMember, - getTeamMembers, - memberIsTeamOwner, - getTeam, - getTeamConv, - getTeamName, - getTeamId, - getTeamContacts, - getTeamLegalHoldStatus, - changeTeamStatus, - getTeamSearchVisibility, - getAllFeatureConfigsForUser, - getVerificationCodeEnabled, - getTeamExposeInvitationURLsToTeamAdmin, - -- * Legalhold guardLegalhold, @@ -81,7 +59,6 @@ import qualified Brig.Data.Connection as Data import Brig.Federation.Client (notifyUserDeleted) import qualified Brig.IO.Journal as Journal import Brig.RPC -import Brig.Team.Types (ShowOrHideInvitationUrl (..)) import Brig.Types.User.Event import Brig.User.Search.Index (MonadIndexIO) import qualified Brig.User.Search.Index as Search @@ -97,13 +74,11 @@ 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) import qualified Data.Conduit.List as C -import qualified Data.Currency as Currency import Data.Domain import Data.Either.Combinators (whenLeft) import Data.Id -import Data.Json.Util (UTCTimeMillis, (#)) +import Data.Json.Util ((#)) import Data.List.Split (chunksOf) import Data.List1 (List1, list1, singleton) import Data.Proxy @@ -112,15 +87,12 @@ 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 import Galley.Types.Teams.Intra (GuardLegalholdPolicyConflicts (GuardLegalholdPolicyConflicts)) -import qualified Galley.Types.Teams.Intra as Team import Gundeck.Types.Push.V2 import qualified Gundeck.Types.Push.V2 as Push import Imports import Network.HTTP.Types.Method import Network.HTTP.Types.Status -import qualified Network.Wai.Utilities.Error as Wai import System.Logger.Class as Log hiding (name, (.=)) import qualified System.Logger.Extended as ExLog import Wire.API.Connection @@ -129,14 +101,8 @@ 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 import Wire.API.Team.LegalHold (LegalholdProtectee) -import qualified Wire.API.Team.Member as Member import qualified Wire.API.Team.Member as Team -import Wire.API.Team.Role -import Wire.API.Team.SearchVisibility import Wire.API.User import Wire.API.User.Client @@ -706,28 +672,6 @@ toApsData _ = Nothing ------------------------------------------------------------------------------- -- Conversation Management --- | Calls 'Galley.API.createSelfConversationH'. -createSelfConv :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - UserId -> - m () -createSelfConv u = do - debug $ - remote "galley" - . msg (val "Creating self conversation") - void $ galleyRequest POST req - where - req = - path "/conversations/self" - . zUser u - . expect2xx - -- | Calls 'Galley.API.Create.createConnectConversation'. createLocalConnectConv :: ( MonadReader Env m, @@ -892,33 +836,6 @@ unblockConv luid conn = (unblockLocalConv luid conn . tUnqualified) (const (throwM federationNotImplemented)) --- | Calls 'Galley.API.getConversationH'. -getConv :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - UserId -> - ConvId -> - m (Maybe Conversation) -getConv usr cnv = do - debug $ - remote "galley" - . field "conv" (toByteString cnv) - . msg (val "Getting conversation") - rs <- galleyRequest GET req - case Bilge.statusCode rs of - 200 -> Just <$> decodeBody "galley" rs - _ -> pure Nothing - where - req = - paths ["conversations", toByteString' cnv] - . zUser usr - . expect [status200, status404] - upsertOne2OneConversation :: ( MonadReader Env m, MonadIO m, @@ -939,34 +856,6 @@ upsertOne2OneConversation urequest = do . header "Content-Type" "application/json" . lbytes (encode urequest) --- | Calls 'Galley.API.getTeamConversationH'. -getTeamConv :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - UserId -> - TeamId -> - ConvId -> - m (Maybe Conv.TeamConversation) -getTeamConv usr tid cnv = do - debug $ - remote "galley" - . field "conv" (toByteString cnv) - . msg (val "Getting team conversation") - rs <- galleyRequest GET req - case Bilge.statusCode rs of - 200 -> Just <$> decodeBody "galley" rs - _ -> pure Nothing - where - req = - paths ["teams", toByteString' tid, "conversations", toByteString' cnv] - . zUser usr - . expect [status200, status404] - ------------------------------------------------------------------------------- -- User management @@ -1006,27 +895,6 @@ rmUser usr asts = do ------------------------------------------------------------------------------- -- Client management --- | Calls 'Galley.API.addClientH'. -newClient :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - UserId -> - ClientId -> - m () -newClient u c = do - debug $ - remote "galley" - . field "user" (toByteString u) - . field "client" (toByteString c) - . msg (val "new client") - let p = paths ["i", "clients", toByteString' c] - void $ galleyRequest POST (p . zUser u . expect2xx) - -- | Calls 'Galley.API.rmClientH', as well as gundeck. rmClient :: ( MonadReader Env m, @@ -1093,159 +961,6 @@ lookupPushToken uid = do ------------------------------------------------------------------------------- -- Team Management --- | Calls 'Galley.API.canUserJoinTeamH'. -checkUserCanJoinTeam :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - TeamId -> - m (Maybe Wai.Error) -checkUserCanJoinTeam tid = do - debug $ - remote "galley" - . msg (val "Check if can add member to team") - rs <- galleyRequest GET req - pure $ case Bilge.statusCode rs of - 200 -> Nothing - _ -> case decodeBody "galley" rs of - Just (e :: Wai.Error) -> pure e - Nothing -> error ("Invalid response from galley: " <> show rs) - where - req = - paths ["i", "teams", toByteString' tid, "members", "check"] - . header "Content-Type" "application/json" - --- | Calls 'Galley.API.uncheckedAddTeamMemberH'. -addTeamMember :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - UserId -> - TeamId -> - (Maybe (UserId, UTCTimeMillis), Role) -> - m Bool -addTeamMember u tid (minvmeta, role) = do - debug $ - remote "galley" - . msg (val "Adding member to team") - rs <- galleyRequest POST req - pure $ case Bilge.statusCode rs of - 200 -> True - _ -> False - where - prm = Team.rolePermissions role - bdy = Member.mkNewTeamMember u prm minvmeta - req = - paths ["i", "teams", toByteString' tid, "members"] - . header "Content-Type" "application/json" - . zUser u - . expect [status200, status403] - . lbytes (encode bdy) - --- | Calls 'Galley.API.createBindingTeamH'. -createTeam :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - UserId -> - BindingNewTeam -> - TeamId -> - m CreateUserTeam -createTeam u t@(BindingNewTeam bt) teamid = do - debug $ - remote "galley" - . msg (val "Creating Team") - r <- galleyRequest PUT $ req teamid - tid <- - maybe (error "invalid team id") pure $ - fromByteString $ - getHeader' "Location" r - pure (CreateUserTeam tid $ fromRange (bt ^. newTeamName)) - where - req tid = - paths ["i", "teams", toByteString' tid] - . header "Content-Type" "application/json" - . zUser u - . expect2xx - . lbytes (encode t) - --- | Calls 'Galley.API.uncheckedGetTeamMemberH'. -getTeamMember :: - ( MonadLogger m, - MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => - UserId -> - TeamId -> - m (Maybe Team.TeamMember) -getTeamMember u tid = do - debug $ - remote "galley" - . msg (val "Get team member") - rs <- galleyRequest GET req - case Bilge.statusCode rs of - 200 -> Just <$> decodeBody "galley" rs - _ -> pure Nothing - where - req = - paths ["i", "teams", toByteString' tid, "members", toByteString' u] - . zUser u - . expect [status200, status404] - --- | Calls 'Galley.API.uncheckedGetTeamMembersH'. --- --- | TODO: is now truncated. this is (only) used for team suspension / unsuspension, which --- means that only the first 2000 members of a team (according to some arbitrary order) will --- be suspended, and the rest will remain active. -getTeamMembers :: - ( MonadLogger m, - MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => - TeamId -> - m Team.TeamMemberList -getTeamMembers tid = do - debug $ remote "galley" . msg (val "Get team members") - galleyRequest GET req >>= decodeBody "galley" - where - req = - paths ["i", "teams", toByteString' tid, "members"] - . expect2xx - -memberIsTeamOwner :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => - TeamId -> - UserId -> - m Bool -memberIsTeamOwner tid uid = do - r <- - galleyRequest GET $ - paths ["i", "teams", toByteString' tid, "is-team-owner", toByteString' uid] - pure $ responseStatus r /= status403 - -- | Only works on 'BindingTeam's! The list of members returned is potentially truncated. -- -- Calls 'Galley.API.getBindingTeamMembersH'. @@ -1270,189 +985,6 @@ getTeamContacts u = do paths ["i", "users", toByteString' u, "team", "members"] . expect [status200, status404] --- | Calls 'Galley.API.getBindingTeamIdH'. -getTeamId :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - UserId -> - m (Maybe TeamId) -getTeamId u = do - debug $ remote "galley" . msg (val "Get team from user") - rs <- galleyRequest GET req - case Bilge.statusCode rs of - 200 -> Just <$> decodeBody "galley" rs - _ -> pure Nothing - where - req = - paths ["i", "users", toByteString' u, "team"] - . expect [status200, status404] - --- | Calls 'Galley.API.getTeamInternalH'. -getTeam :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - TeamId -> - m Team.TeamData -getTeam tid = do - debug $ remote "galley" . msg (val "Get team info") - galleyRequest GET req >>= decodeBody "galley" - where - req = - paths ["i", "teams", toByteString' tid] - . expect2xx - --- | Calls 'Galley.API.getTeamInternalH'. -getTeamName :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - TeamId -> - m Team.TeamName -getTeamName tid = do - debug $ remote "galley" . msg (val "Get team info") - galleyRequest GET req >>= decodeBody "galley" - where - req = - paths ["i", "teams", toByteString' tid, "name"] - . expect2xx - --- | Calls 'Galley.API.getTeamFeatureStatusH'. -getTeamLegalHoldStatus :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - TeamId -> - m (WithStatus LegalholdConfig) -getTeamLegalHoldStatus tid = do - debug $ remote "galley" . msg (val "Get legalhold settings") - galleyRequest GET req >>= decodeBody "galley" - where - req = - paths ["i", "teams", toByteString' tid, "features", featureNameBS @LegalholdConfig] - . expect2xx - --- | Calls 'Galley.API.getSearchVisibilityInternalH'. -getTeamSearchVisibility :: - ( MonadLogger m, - MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => - TeamId -> - m TeamSearchVisibility -getTeamSearchVisibility tid = - coerce @TeamSearchVisibilityView @TeamSearchVisibility <$> do - debug $ remote "galley" . msg (val "Get search visibility settings") - galleyRequest GET req >>= decodeBody "galley" - where - req = - paths ["i", "teams", toByteString' tid, "search-visibility"] - . expect2xx - -getTeamExposeInvitationURLsToTeamAdmin :: - ( MonadLogger m, - MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => - TeamId -> - m ShowOrHideInvitationUrl -getTeamExposeInvitationURLsToTeamAdmin tid = do - debug $ remote "galley" . msg (val "Get expose invitation URLs to team admin settings") - response <- galleyRequest GET req - status <- wsStatus <$> decodeBody @(WithStatus ExposeInvitationURLsToTeamAdminConfig) "galley" response - case status of - FeatureStatusEnabled -> pure ShowInvitationUrl - FeatureStatusDisabled -> pure HideInvitationUrl - where - req = - paths ["i", "teams", toByteString' tid, "features", featureNameBS @ExposeInvitationURLsToTeamAdminConfig] - . expect2xx - -getVerificationCodeEnabled :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - TeamId -> - m Bool -getVerificationCodeEnabled tid = do - debug $ remote "galley" . msg (val "Get snd factor password challenge settings") - response <- galleyRequest GET req - status <- wsStatus <$> decodeBody @(WithStatus SndFactorPasswordChallengeConfig) "galley" response - case status of - FeatureStatusEnabled -> pure True - FeatureStatusDisabled -> pure False - where - req = - paths ["i", "teams", toByteString' tid, "features", featureNameBS @SndFactorPasswordChallengeConfig] - . expect2xx - -getAllFeatureConfigsForUser :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => - Maybe UserId -> - m AllFeatureConfigs -getAllFeatureConfigsForUser mbUserId = - responseJsonUnsafe - <$> galleyRequest - GET - ( paths ["i", "feature-configs"] - . maybe id (queryItem "user_id" . toByteString') mbUserId - ) - --- | Calls 'Galley.API.updateTeamStatusH'. -changeTeamStatus :: - ( MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m, - MonadLogger m - ) => - TeamId -> - Team.TeamStatus -> - Maybe Currency.Alpha -> - m () -changeTeamStatus tid s cur = do - debug $ remote "galley" . msg (val "Change Team status") - void $ galleyRequest PUT req - where - req = - paths ["i", "teams", toByteString' tid, "status"] - . header "Content-Type" "application/json" - . expect2xx - . lbytes (encode $ Team.TeamStatusUpdate s cur) - guardLegalhold :: ( MonadReader Env m, MonadIO m, diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index f02a1d5bb2..3b216f787c 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.GalleyProvider (GalleyProvider) +import qualified Brig.Effects.GalleyProvider as GalleyProvider import Brig.Email (mkEmailKey) -import qualified Brig.IO.Intra as RPC import qualified Brig.InternalEvent.Types as Internal import Brig.Options (Settings (..)) import qualified Brig.Options as Opt @@ -91,6 +92,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 (Members) import qualified Ssl.Util as SSL import System.Logger.Class (MonadLogger) import UnliftIO.Async (pooledMapConcurrentlyN_) @@ -121,7 +123,7 @@ 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 :: Members '[GalleyProvider] r => Routes Doc.ApiBuilder (Handler r) () routesPublic = do -- Public API (Unauthenticated) -------------------------------------------- @@ -152,7 +154,7 @@ routesPublic = do -- Provider API ------------------------------------------------------------ - delete "/provider" (continue $ wrapHttpClientE <$> deleteAccountH) $ + delete "/provider" (continue deleteAccountH) $ zauth ZAuthProvider .&> zauthProviderId .&. jsonRequest @Public.DeleteProvider @@ -320,7 +322,7 @@ routesPublic = do .&> zauth ZAuthBot .&> capture "uid" -routesInternal :: Routes a (Handler r) () +routesInternal :: Members '[GalleyProvider] r => Routes a (Handler r) () routesInternal = do get "/i/provider/activation-code" (continue getActivationCodeH) $ accept "application" "json" @@ -329,9 +331,9 @@ routesInternal = do -------------------------------------------------------------------------------- -- Public API (Unauthenticated) -newAccountH :: JsonRequest Public.NewProvider -> (Handler r) Response +newAccountH :: Members '[GalleyProvider] r => JsonRequest Public.NewProvider -> (Handler r) Response newAccountH req = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing setStatus status201 . json <$> (newAccount =<< parseJsonBody req) newAccount :: Public.NewProvider -> (Handler r) Public.NewProviderResponse @@ -366,9 +368,9 @@ newAccount new = do lift $ sendActivationMail name email key val False pure $ Public.NewProviderResponse pid newPass -activateAccountKeyH :: Code.Key ::: Code.Value -> (Handler r) Response +activateAccountKeyH :: Members '[GalleyProvider] r => Code.Key ::: Code.Value -> (Handler r) Response activateAccountKeyH (key ::: val) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing maybe (setStatus status204 empty) json <$> activateAccountKey key val activateAccountKey :: Code.Key -> Code.Value -> (Handler r) (Maybe Public.ProviderActivationResponse) @@ -393,9 +395,9 @@ activateAccountKey key val = do lift $ sendApprovalConfirmMail name email pure . Just $ Public.ProviderActivationResponse email -getActivationCodeH :: Public.Email -> (Handler r) Response +getActivationCodeH :: Members '[GalleyProvider] r => Public.Email -> (Handler r) Response getActivationCodeH e = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing json <$> getActivationCode e getActivationCode :: Public.Email -> (Handler r) FoundActivationCode @@ -414,9 +416,9 @@ instance ToJSON FoundActivationCode where toJSON $ Code.KeyValuePair (Code.codeKey vcode) (Code.codeValue vcode) -approveAccountKeyH :: Code.Key ::: Code.Value -> (Handler r) Response +approveAccountKeyH :: Members '[GalleyProvider] r => Code.Key ::: Code.Value -> (Handler r) Response approveAccountKeyH (key ::: val) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing empty <$ approveAccountKey key val approveAccountKey :: Code.Key -> Code.Value -> (Handler r) () @@ -429,9 +431,9 @@ approveAccountKey key val = do lift $ sendApprovalConfirmMail name email _ -> throwStd (errorToWai @'E.InvalidCode) -loginH :: JsonRequest Public.ProviderLogin -> (Handler r) Response +loginH :: Members '[GalleyProvider] r => JsonRequest Public.ProviderLogin -> (Handler r) Response loginH req = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing tok <- login =<< parseJsonBody req setProviderCookie tok empty @@ -443,9 +445,9 @@ login l = do throwStd (errorToWai @'E.BadCredentials) ZAuth.newProviderToken pid -beginPasswordResetH :: JsonRequest Public.PasswordReset -> (Handler r) Response +beginPasswordResetH :: Members '[GalleyProvider] r => JsonRequest Public.PasswordReset -> (Handler r) Response beginPasswordResetH req = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing setStatus status201 empty <$ (beginPasswordReset =<< parseJsonBody req) beginPasswordReset :: Public.PasswordReset -> (Handler r) () @@ -465,9 +467,9 @@ beginPasswordReset (Public.PasswordReset target) = do tryInsertVerificationCode code $ verificationCodeThrottledError . VerificationCodeThrottled lift $ sendPasswordResetMail target (Code.codeKey code) (Code.codeValue code) -completePasswordResetH :: JsonRequest Public.CompletePasswordReset -> (Handler r) Response +completePasswordResetH :: Members '[GalleyProvider] r => JsonRequest Public.CompletePasswordReset -> (Handler r) Response completePasswordResetH req = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing empty <$ (completePasswordReset =<< parseJsonBody req) completePasswordReset :: Public.CompletePasswordReset -> (Handler r) () @@ -486,9 +488,9 @@ completePasswordReset (Public.CompletePasswordReset key val newpwd) = do -------------------------------------------------------------------------------- -- Provider API -getAccountH :: ProviderId -> (Handler r) Response +getAccountH :: Members '[GalleyProvider] r => ProviderId -> (Handler r) Response getAccountH pid = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing getAccount pid <&> \case Just p -> json p Nothing -> setStatus status404 empty @@ -496,9 +498,9 @@ getAccountH pid = do getAccount :: ProviderId -> (Handler r) (Maybe Public.Provider) getAccount = wrapClientE . DB.lookupAccount -updateAccountProfileH :: ProviderId ::: JsonRequest Public.UpdateProvider -> (Handler r) Response +updateAccountProfileH :: Members '[GalleyProvider] r => ProviderId ::: JsonRequest Public.UpdateProvider -> (Handler r) Response updateAccountProfileH (pid ::: req) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing empty <$ (updateAccountProfile pid =<< parseJsonBody req) updateAccountProfile :: ProviderId -> Public.UpdateProvider -> (Handler r) () @@ -511,9 +513,9 @@ updateAccountProfile pid upd = do (updateProviderUrl upd) (updateProviderDescr upd) -updateAccountEmailH :: ProviderId ::: JsonRequest Public.EmailUpdate -> (Handler r) Response +updateAccountEmailH :: Members '[GalleyProvider] r => ProviderId ::: JsonRequest Public.EmailUpdate -> (Handler r) Response updateAccountEmailH (pid ::: req) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing setStatus status202 empty <$ (updateAccountEmail pid =<< parseJsonBody req) updateAccountEmail :: ProviderId -> Public.EmailUpdate -> (Handler r) () @@ -534,9 +536,9 @@ updateAccountEmail pid (Public.EmailUpdate new) = do tryInsertVerificationCode code $ verificationCodeThrottledError . VerificationCodeThrottled lift $ sendActivationMail (Name "name") email (Code.codeKey code) (Code.codeValue code) True -updateAccountPasswordH :: ProviderId ::: JsonRequest Public.PasswordChange -> (Handler r) Response +updateAccountPasswordH :: Members '[GalleyProvider] r => ProviderId ::: JsonRequest Public.PasswordChange -> (Handler r) Response updateAccountPasswordH (pid ::: req) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing empty <$ (updateAccountPassword pid =<< parseJsonBody req) updateAccountPassword :: ProviderId -> Public.PasswordChange -> (Handler r) () @@ -548,9 +550,9 @@ updateAccountPassword pid upd = do throwStd newPasswordMustDiffer wrapClientE $ DB.updateAccountPassword pid (cpNewPassword upd) -addServiceH :: ProviderId ::: JsonRequest Public.NewService -> (Handler r) Response +addServiceH :: Members '[GalleyProvider] r => ProviderId ::: JsonRequest Public.NewService -> (Handler r) Response addServiceH (pid ::: req) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing setStatus status201 . json <$> (addService pid =<< parseJsonBody req) addService :: ProviderId -> Public.NewService -> (Handler r) Public.NewServiceResponse @@ -569,26 +571,26 @@ addService pid new = do let rstoken = maybe (Just token) (const Nothing) (newServiceToken new) pure $ Public.NewServiceResponse sid rstoken -listServicesH :: ProviderId -> (Handler r) Response +listServicesH :: Members '[GalleyProvider] r => ProviderId -> (Handler r) Response listServicesH pid = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing json <$> listServices pid listServices :: ProviderId -> (Handler r) [Public.Service] listServices = wrapClientE . DB.listServices -getServiceH :: ProviderId ::: ServiceId -> (Handler r) Response +getServiceH :: Members '[GalleyProvider] r => ProviderId ::: ServiceId -> (Handler r) Response getServiceH (pid ::: sid) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing json <$> getService pid sid getService :: ProviderId -> ServiceId -> (Handler r) Public.Service getService pid sid = wrapClientE (DB.lookupService pid sid) >>= maybeServiceNotFound -updateServiceH :: ProviderId ::: ServiceId ::: JsonRequest Public.UpdateService -> (Handler r) Response +updateServiceH :: Members '[GalleyProvider] r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateService -> (Handler r) Response updateServiceH (pid ::: sid ::: req) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing empty <$ (updateService pid sid =<< parseJsonBody req) updateService :: ProviderId -> ServiceId -> Public.UpdateService -> (Handler r) () @@ -619,9 +621,9 @@ updateService pid sid upd = do tagsChange (serviceEnabled svc) -updateServiceConnH :: ProviderId ::: ServiceId ::: JsonRequest Public.UpdateServiceConn -> (Handler r) Response +updateServiceConnH :: Members '[GalleyProvider] r => ProviderId ::: ServiceId ::: JsonRequest Public.UpdateServiceConn -> (Handler r) Response updateServiceConnH (pid ::: sid ::: req) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing empty <$ (updateServiceConn pid sid =<< parseJsonBody req) updateServiceConn :: ProviderId -> ServiceId -> Public.UpdateServiceConn -> (Handler r) () @@ -660,14 +662,14 @@ updateServiceConn pid sid upd = do -- TODO: Send informational email to provider. --- | The endpoint that is called to delete a service. +-- | Members '[GalleyProvider] r => The endpoint that is called to delete a service. -- -- Since deleting a service can be costly, it just marks the service as -- disabled and then creates an event that will, when processed, actually -- delete the service. See 'finishDeleteService'. -deleteServiceH :: ProviderId ::: ServiceId ::: JsonRequest Public.DeleteService -> (Handler r) Response +deleteServiceH :: Members '[GalleyProvider] r => ProviderId ::: ServiceId ::: JsonRequest Public.DeleteService -> (Handler r) Response deleteServiceH (pid ::: sid ::: req) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing setStatus status202 empty <$ (deleteService pid sid =<< parseJsonBody req) -- | The endpoint that is called to delete a service. @@ -713,18 +715,12 @@ finishDeleteService pid sid = do kick (bid, cid, _) = deleteBot (botUserId bid) Nothing bid cid deleteAccountH :: - ( MonadReader Env m, - MonadMask m, - MonadHttp m, - MonadClient m, - HasRequestId m, - MonadLogger m - ) => + Members '[GalleyProvider] r => ProviderId ::: JsonRequest Public.DeleteProvider -> - ExceptT Error m Response + ExceptT Error (AppT r) Response deleteAccountH (pid ::: req) = do guardSecondFactorDisabled Nothing - empty <$ (deleteAccount pid =<< parseJsonBody req) + empty <$ (mapExceptT wrapHttpClient $ deleteAccount pid =<< parseJsonBody req) deleteAccount :: ( MonadReader Env m, @@ -755,35 +751,35 @@ deleteAccount pid del = do -------------------------------------------------------------------------------- -- User API -getProviderProfileH :: ProviderId -> (Handler r) Response +getProviderProfileH :: Members '[GalleyProvider] r => ProviderId -> (Handler r) Response getProviderProfileH pid = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing json <$> getProviderProfile pid getProviderProfile :: ProviderId -> (Handler r) Public.ProviderProfile getProviderProfile pid = wrapClientE (DB.lookupAccountProfile pid) >>= maybeProviderNotFound -listServiceProfilesH :: ProviderId -> (Handler r) Response +listServiceProfilesH :: Members '[GalleyProvider] r => ProviderId -> (Handler r) Response listServiceProfilesH pid = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing json <$> listServiceProfiles pid listServiceProfiles :: ProviderId -> (Handler r) [Public.ServiceProfile] listServiceProfiles = wrapClientE . DB.listServiceProfiles -getServiceProfileH :: ProviderId ::: ServiceId -> (Handler r) Response +getServiceProfileH :: Members '[GalleyProvider] r => ProviderId ::: ServiceId -> (Handler r) Response getServiceProfileH (pid ::: sid) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing json <$> getServiceProfile pid sid getServiceProfile :: ProviderId -> ServiceId -> (Handler r) Public.ServiceProfile getServiceProfile pid sid = wrapClientE (DB.lookupServiceProfile pid sid) >>= maybeServiceNotFound -searchServiceProfilesH :: Maybe (Public.QueryAnyTags 1 3) ::: Maybe Text ::: Range 10 100 Int32 -> (Handler r) Response +searchServiceProfilesH :: Members '[GalleyProvider] r => Maybe (Public.QueryAnyTags 1 3) ::: Maybe Text ::: Range 10 100 Int32 -> (Handler r) Response searchServiceProfilesH (qt ::: start ::: size) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing json <$> searchServiceProfiles qt start size -- TODO: in order to actually make it possible for clients to implement @@ -800,10 +796,11 @@ searchServiceProfiles Nothing Nothing _ = do throwStd $ badRequest "At least `tags` or `start` must be provided." searchTeamServiceProfilesH :: + Members '[GalleyProvider] r => UserId ::: TeamId ::: Maybe (Range 1 128 Text) ::: Bool ::: Range 10 100 Int32 -> (Handler r) Response searchTeamServiceProfilesH (uid ::: tid ::: prefix ::: filterDisabled ::: size) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled (Just uid) + guardSecondFactorDisabled (Just uid) json <$> searchTeamServiceProfiles uid tid prefix filterDisabled size -- NB: unlike 'searchServiceProfiles', we don't filter by service provider here @@ -824,9 +821,9 @@ searchTeamServiceProfiles uid tid prefix filterDisabled size = do -- Get search results wrapClientE $ DB.paginateServiceWhitelist tid prefix filterDisabled (fromRange size) -getServiceTagListH :: () -> (Handler r) Response +getServiceTagListH :: Members '[GalleyProvider] r => () -> (Handler r) Response getServiceTagListH () = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing json <$> getServiceTagList () getServiceTagList :: () -> Monad m => m Public.ServiceTagList @@ -834,9 +831,9 @@ getServiceTagList () = pure (Public.ServiceTagList allTags) where allTags = [(minBound :: Public.ServiceTag) ..] -updateServiceWhitelistH :: UserId ::: ConnId ::: TeamId ::: JsonRequest Public.UpdateServiceWhitelist -> (Handler r) Response +updateServiceWhitelistH :: Members '[GalleyProvider] r => UserId ::: ConnId ::: TeamId ::: JsonRequest Public.UpdateServiceWhitelist -> (Handler r) Response updateServiceWhitelistH (uid ::: con ::: tid ::: req) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled (Just uid) + guardSecondFactorDisabled (Just uid) resp <- updateServiceWhitelist uid con tid =<< parseJsonBody req let status = case resp of UpdateServiceWhitelistRespChanged -> status200 @@ -847,7 +844,7 @@ data UpdateServiceWhitelistResp = UpdateServiceWhitelistRespChanged | UpdateServiceWhitelistRespUnchanged -updateServiceWhitelist :: UserId -> ConnId -> TeamId -> Public.UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp +updateServiceWhitelist :: Members '[GalleyProvider] r => UserId -> ConnId -> TeamId -> Public.UpdateServiceWhitelist -> (Handler r) UpdateServiceWhitelistResp updateServiceWhitelist uid con tid upd = do let pid = updateServiceWhitelistProvider upd sid = updateServiceWhitelistService upd @@ -880,18 +877,18 @@ 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 :: Members '[GalleyProvider] r => UserId ::: ConnId ::: ConvId ::: JsonRequest Public.AddBot -> (Handler r) Response addBotH (zuid ::: zcon ::: cid ::: req) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled (Just zuid) + guardSecondFactorDisabled (Just zuid) setStatus status201 . json <$> (addBot zuid zcon cid =<< parseJsonBody req) -addBot :: UserId -> ConnId -> ConvId -> Public.AddBot -> (Handler r) Public.AddBotResponse +addBot :: Members '[GalleyProvider] r => UserId -> ConnId -> ConvId -> Public.AddBot -> (Handler r) Public.AddBotResponse addBot zuid zcon cid add = do zusr <- lift (wrapClient $ User.lookupUser NoPendingInvitations zuid) >>= maybeInvalidUser let pid = addBotProvider add let sid = addBotService add -- Get the conversation and check preconditions - cnv <- lift (wrapHttp $ RPC.getConv zuid cid) >>= maybeConvNotFound + cnv <- lift (liftSem $ GalleyProvider.getConv zuid cid) >>= maybeConvNotFound let mems = cnvMembers cnv unless (cnvType cnv == RegularConv) $ throwStd invalidConv @@ -959,15 +956,15 @@ addBot zuid zcon cid add = do Public.rsAddBotEvent = ev } -removeBotH :: UserId ::: ConnId ::: ConvId ::: BotId -> (Handler r) Response +removeBotH :: Members '[GalleyProvider] r => UserId ::: ConnId ::: ConvId ::: BotId -> (Handler r) Response removeBotH (zusr ::: zcon ::: cid ::: bid) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled (Just zusr) + 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 '[GalleyProvider] 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 + cnv <- lift (liftSem $ GalleyProvider.getConv zusr cid) >>= maybeConvNotFound let mems = cnvMembers cnv unless (cnvType cnv == RegularConv) $ throwStd invalidConv @@ -982,9 +979,9 @@ removeBot zusr zcon cid bid = do -------------------------------------------------------------------------------- -- Bot API -botGetSelfH :: BotId -> (Handler r) Response +botGetSelfH :: Members '[GalleyProvider] r => BotId -> (Handler r) Response botGetSelfH bot = do - mapExceptT wrapHttp $ guardSecondFactorDisabled (Just (botUserId bot)) + guardSecondFactorDisabled (Just (botUserId bot)) json <$> botGetSelf bot botGetSelf :: BotId -> (Handler r) Public.UserProfile @@ -992,18 +989,18 @@ botGetSelf bot = do p <- lift $ wrapClient $ User.lookupUser NoPendingInvitations (botUserId bot) maybe (throwStd (errorToWai @'E.UserNotFound)) (pure . (`Public.publicProfile` UserLegalHoldNoConsent)) p -botGetClientH :: BotId -> (Handler r) Response +botGetClientH :: Members '[GalleyProvider] r => BotId -> (Handler r) Response botGetClientH bot = do - mapExceptT wrapHttp $ guardSecondFactorDisabled (Just (botUserId bot)) + guardSecondFactorDisabled (Just (botUserId bot)) maybe (throwStd (errorToWai @'E.ClientNotFound)) (pure . json) =<< lift (botGetClient bot) botGetClient :: BotId -> (AppT r) (Maybe Public.Client) botGetClient bot = listToMaybe <$> wrapClient (User.lookupClients (botUserId bot)) -botListPrekeysH :: BotId -> (Handler r) Response +botListPrekeysH :: Members '[GalleyProvider] r => BotId -> (Handler r) Response botListPrekeysH bot = do - mapExceptT wrapHttp $ guardSecondFactorDisabled (Just (botUserId bot)) + guardSecondFactorDisabled (Just (botUserId bot)) json <$> botListPrekeys bot botListPrekeys :: BotId -> (Handler r) [Public.PrekeyId] @@ -1013,9 +1010,9 @@ botListPrekeys bot = do Nothing -> pure [] Just ci -> lift (wrapClient $ User.lookupPrekeyIds (botUserId bot) ci) -botUpdatePrekeysH :: BotId ::: JsonRequest Public.UpdateBotPrekeys -> (Handler r) Response +botUpdatePrekeysH :: Members '[GalleyProvider] r => BotId ::: JsonRequest Public.UpdateBotPrekeys -> (Handler r) Response botUpdatePrekeysH (bot ::: req) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled (Just (botUserId bot)) + guardSecondFactorDisabled (Just (botUserId bot)) empty <$ (botUpdatePrekeys bot =<< parseJsonBody req) botUpdatePrekeys :: BotId -> Public.UpdateBotPrekeys -> (Handler r) () @@ -1027,9 +1024,9 @@ botUpdatePrekeys bot upd = do let pks = updateBotPrekeyList upd wrapClientE (User.updatePrekeys (botUserId bot) (clientId c) pks) !>> clientDataError -botClaimUsersPrekeysH :: JsonRequest Public.UserClients -> (Handler r) Response +botClaimUsersPrekeysH :: Members '[GalleyProvider] r => JsonRequest Public.UserClients -> (Handler r) Response botClaimUsersPrekeysH req = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing + guardSecondFactorDisabled Nothing json <$> (botClaimUsersPrekeys =<< parseJsonBody req) botClaimUsersPrekeys :: Public.UserClients -> (Handler r) Public.UserClientPrekeyMap @@ -1039,9 +1036,9 @@ botClaimUsersPrekeys body = do throwStd (errorToWai @'E.TooManyClients) Client.claimLocalMultiPrekeyBundles UnprotectedBot body !>> clientError -botListUserProfilesH :: List UserId -> (Handler r) Response +botListUserProfilesH :: Members '[GalleyProvider] r => List UserId -> (Handler r) Response botListUserProfilesH uids = do - mapExceptT wrapHttp $ guardSecondFactorDisabled Nothing -- should we check all user ids? + guardSecondFactorDisabled Nothing -- should we check all user ids? json <$> botListUserProfiles uids botListUserProfiles :: List UserId -> (Handler r) [Public.BotUserView] @@ -1049,9 +1046,9 @@ botListUserProfiles uids = do us <- lift . wrapClient $ User.lookupUsers NoPendingInvitations (fromList uids) pure (map mkBotUserView us) -botGetUserClientsH :: UserId -> (Handler r) Response +botGetUserClientsH :: Members '[GalleyProvider] r => UserId -> (Handler r) Response botGetUserClientsH uid = do - mapExceptT wrapHttp $ guardSecondFactorDisabled (Just uid) + guardSecondFactorDisabled (Just uid) json <$> lift (botGetUserClients uid) botGetUserClients :: UserId -> (AppT r) [Public.PubClient] @@ -1060,14 +1057,14 @@ botGetUserClients uid = where pubClient c = Public.PubClient (clientId c) (clientClass c) -botDeleteSelfH :: BotId ::: ConvId -> (Handler r) Response +botDeleteSelfH :: Members '[GalleyProvider] r => BotId ::: ConvId -> (Handler r) Response botDeleteSelfH (bid ::: cid) = do - mapExceptT wrapHttp $ guardSecondFactorDisabled (Just (botUserId bid)) + guardSecondFactorDisabled (Just (botUserId bid)) empty <$ botDeleteSelf bid cid -botDeleteSelf :: BotId -> ConvId -> (Handler r) () +botDeleteSelf :: Members '[GalleyProvider] r => BotId -> ConvId -> (Handler r) () botDeleteSelf bid cid = do - mapExceptT wrapHttp $ guardSecondFactorDisabled (Just (botUserId bid)) + guardSecondFactorDisabled (Just (botUserId bid)) bot <- lift . wrapClient $ User.lookupUser NoPendingInvitations (botUserId bid) _ <- maybeInvalidBot (userService =<< bot) _ <- lift $ wrapHttpClient $ deleteBot (botUserId bid) Nothing bid cid @@ -1079,17 +1076,11 @@ botDeleteSelf bid cid = do -- | If second factor auth is enabled, make sure that end-points that don't support it, but should, are blocked completely. -- (This is a workaround until we have 2FA for those end-points as well.) guardSecondFactorDisabled :: - ( MonadLogger m, - MonadReader Env m, - MonadIO m, - MonadMask m, - MonadHttp m, - HasRequestId m - ) => + Members '[GalleyProvider] r => Maybe UserId -> - ExceptT Error m () + ExceptT Error (AppT r) () guardSecondFactorDisabled mbUserId = do - enabled <- lift $ (==) Feature.FeatureStatusEnabled . Feature.wsStatus . Feature.afcSndFactorPasswordChallenge <$> RPC.getAllFeatureConfigsForUser mbUserId + enabled <- lift $ liftSem $ (==) Feature.FeatureStatusEnabled . Feature.wsStatus . Feature.afcSndFactorPasswordChallenge <$> GalleyProvider.getAllFeatureConfigsForUser mbUserId when enabled $ throwStd accessDenied minRsaKeySize :: Int diff --git a/services/brig/src/Brig/RPC.hs b/services/brig/src/Brig/RPC.hs index 5320a81f19..986ea5725f 100644 --- a/services/brig/src/Brig/RPC.hs +++ b/services/brig/src/Brig/RPC.hs @@ -89,6 +89,16 @@ serviceRequest :: m (Response (Maybe BL.ByteString)) serviceRequest nm svc m r = do service <- view svc + serviceRequestImpl nm service m r + +serviceRequestImpl :: + (MonadIO m, MonadMask m, MonadHttp m, HasRequestId m) => + LT.Text -> + Request -> + StdMethod -> + (Request -> Request) -> + m (Response (Maybe BL.ByteString)) +serviceRequestImpl nm service m r = do recovering x3 rpcHandlers $ const $ rpc' nm service (method m . r) diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index f00cd9ae1b..7d64a9f9c9 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -31,9 +31,10 @@ 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.GalleyProvider (GalleyProvider) +import qualified Brig.Effects.GalleyProvider as GalleyProvider import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) 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 qualified Brig.Team.DB as DB @@ -62,7 +63,7 @@ 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 (Member, Members) +import Polysemy (Members) import System.Logger (Msg) import qualified System.Logger.Class as Log import Util.Logging (logFunction, logTeam) @@ -80,7 +81,13 @@ import qualified Wire.API.Team.Size as Public import Wire.API.User hiding (fromEmail) import qualified Wire.API.User as Public -routesPublic :: Member BlacklistStore r => Routes Doc.ApiBuilder (Handler r) () +routesPublic :: + Members + '[ BlacklistStore, + GalleyProvider + ] + r => + Routes Doc.ApiBuilder (Handler r) () routesPublic = do post "/teams/:tid/invitations" (continue createInvitationPublicH) $ accept "application" "json" @@ -193,6 +200,7 @@ routesPublic = do routesInternal :: Members '[ BlacklistStore, + GalleyProvider, UserPendingActivationStore p ] r => @@ -223,10 +231,10 @@ routesInternal = do accept "application" "json" .&. jsonRequest @NewUserScimInvitation -teamSizePublicH :: JSON ::: UserId ::: TeamId -> (Handler r) Response +teamSizePublicH :: Members '[GalleyProvider] r => JSON ::: UserId ::: TeamId -> (Handler r) Response teamSizePublicH (_ ::: uid ::: tid) = json <$> teamSizePublic uid tid -teamSizePublic :: UserId -> TeamId -> (Handler r) TeamSize +teamSizePublic :: Members '[GalleyProvider] r => UserId -> TeamId -> (Handler r) TeamSize teamSizePublic uid tid = do ensurePermissions uid tid [AddTeamMember] -- limit this to team admins to reduce risk of involuntary DOS attacks teamSize tid @@ -252,7 +260,14 @@ newtype FoundInvitationCode = FoundInvitationCode InvitationCode instance ToJSON FoundInvitationCode where toJSON (FoundInvitationCode c) = object ["code" .= c] -createInvitationPublicH :: Member BlacklistStore r => JSON ::: UserId ::: TeamId ::: JsonRequest Public.InvitationRequest -> (Handler r) Response +createInvitationPublicH :: + Members + '[ BlacklistStore, + GalleyProvider + ] + r => + JSON ::: UserId ::: TeamId ::: JsonRequest Public.InvitationRequest -> + (Handler r) Response createInvitationPublicH (_ ::: uid ::: tid ::: req) = do body <- parseJsonBody req newInv <- createInvitationPublic uid tid body @@ -268,7 +283,16 @@ data CreateInvitationInviter = CreateInvitationInviter } deriving (Eq, Show) -createInvitationPublic :: Member BlacklistStore r => UserId -> TeamId -> Public.InvitationRequest -> Handler r Public.Invitation +createInvitationPublic :: + Members + '[ BlacklistStore, + GalleyProvider + ] + r => + UserId -> + TeamId -> + Public.InvitationRequest -> + Handler r Public.Invitation createInvitationPublic uid tid body = do let inviteeRole = fromMaybe defaultRole . irRole $ body inviter <- do @@ -291,6 +315,7 @@ createInvitationPublic uid tid body = do createInvitationViaScimH :: Members '[ BlacklistStore, + GalleyProvider, UserPendingActivationStore p ] r => @@ -303,6 +328,7 @@ createInvitationViaScimH (_ ::: req) = do createInvitationViaScim :: Members '[ BlacklistStore, + GalleyProvider, UserPendingActivationStore p ] r => @@ -345,7 +371,18 @@ logInvitationRequest context action = Log.info $ (context . logInvitationCode code) . Log.msg @Text "Successfully created invitation" pure (Right result) -createInvitation' :: Member BlacklistStore r => TeamId -> Public.Role -> Maybe UserId -> Email -> Public.InvitationRequest -> Handler r (Public.Invitation, Public.InvitationCode) +createInvitation' :: + Members + '[ BlacklistStore, + GalleyProvider + ] + 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 @@ -378,7 +415,7 @@ createInvitation' tid inviteeRole mbInviterUid fromEmail body = do let locale = irLocale body let inviteeName = irInviteeName body - showInvitationUrl <- lift $ wrapHttp $ Intra.getTeamExposeInvitationURLsToTeamAdmin tid + showInvitationUrl <- lift $ liftSem $ GalleyProvider.getExposeInvitationURLsToTeamAdmin tid lift $ do iid <- liftIO DB.mkInvitationId @@ -399,37 +436,37 @@ createInvitation' tid inviteeRole mbInviterUid fromEmail body = do timeout (newInv, code) <$ sendInvitationMail inviteeEmail tid fromEmail code locale -deleteInvitationH :: JSON ::: UserId ::: TeamId ::: InvitationId -> (Handler r) Response +deleteInvitationH :: Members '[GalleyProvider] r => JSON ::: UserId ::: TeamId ::: InvitationId -> (Handler r) Response deleteInvitationH (_ ::: uid ::: tid ::: iid) = do empty <$ deleteInvitation uid tid iid -deleteInvitation :: UserId -> TeamId -> InvitationId -> (Handler r) () +deleteInvitation :: Members '[GalleyProvider] r => UserId -> TeamId -> InvitationId -> (Handler r) () deleteInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] lift $ wrapClient $ DB.deleteInvitation tid iid -listInvitationsH :: JSON ::: UserId ::: TeamId ::: Maybe InvitationId ::: Range 1 500 Int32 -> (Handler r) Response +listInvitationsH :: Members '[GalleyProvider] r => JSON ::: UserId ::: TeamId ::: Maybe InvitationId ::: Range 1 500 Int32 -> (Handler r) Response listInvitationsH (_ ::: uid ::: tid ::: start ::: size) = do json <$> listInvitations uid tid start size -listInvitations :: UserId -> TeamId -> Maybe InvitationId -> Range 1 500 Int32 -> (Handler r) Public.InvitationList +listInvitations :: Members '[GalleyProvider] r => UserId -> TeamId -> Maybe InvitationId -> Range 1 500 Int32 -> (Handler r) Public.InvitationList listInvitations uid tid start size = do ensurePermissions uid tid [AddTeamMember] - showInvitationUrl <- lift $ wrapHttp $ Intra.getTeamExposeInvitationURLsToTeamAdmin tid + showInvitationUrl <- lift $ liftSem $ GalleyProvider.getExposeInvitationURLsToTeamAdmin tid rs <- lift $ wrapClient $ DB.lookupInvitations showInvitationUrl tid start size pure $! Public.InvitationList (DB.resultList rs) (DB.resultHasMore rs) -getInvitationH :: JSON ::: UserId ::: TeamId ::: InvitationId -> (Handler r) Response +getInvitationH :: Members '[GalleyProvider] r => JSON ::: UserId ::: TeamId ::: InvitationId -> (Handler r) Response getInvitationH (_ ::: uid ::: tid ::: iid) = do inv <- getInvitation uid tid iid pure $ case inv of Just i -> json i Nothing -> setStatus status404 empty -getInvitation :: UserId -> TeamId -> InvitationId -> (Handler r) (Maybe Public.Invitation) +getInvitation :: Members '[GalleyProvider] r => UserId -> TeamId -> InvitationId -> (Handler r) (Maybe Public.Invitation) getInvitation uid tid iid = do ensurePermissions uid tid [AddTeamMember] - showInvitationUrl <- lift $ wrapHttp $ Intra.getTeamExposeInvitationURLsToTeamAdmin tid + showInvitationUrl <- lift $ liftSem $ GalleyProvider.getExposeInvitationURLsToTeamAdmin tid lift $ wrapClient $ DB.lookupInvitation showInvitationUrl tid iid getInvitationByCodeH :: JSON ::: Public.InvitationCode -> (Handler r) Response @@ -461,34 +498,44 @@ getInvitationByEmail email = do inv <- lift $ wrapClient $ DB.lookupInvitationByEmail HideInvitationUrl email maybe (throwStd (notFound "Invitation not found")) pure inv -suspendTeamH :: JSON ::: TeamId -> (Handler r) Response +suspendTeamH :: Members '[GalleyProvider] r => JSON ::: TeamId -> (Handler r) Response suspendTeamH (_ ::: tid) = do empty <$ suspendTeam tid -suspendTeam :: TeamId -> (Handler r) () +suspendTeam :: Members '[GalleyProvider] r => TeamId -> (Handler r) () suspendTeam tid = do changeTeamAccountStatuses tid Suspended lift $ wrapClient $ DB.deleteInvitations tid - lift $ wrapHttp $ Intra.changeTeamStatus tid Team.Suspended Nothing + lift $ liftSem $ GalleyProvider.changeTeamStatus tid Team.Suspended Nothing -unsuspendTeamH :: JSON ::: TeamId -> (Handler r) Response +unsuspendTeamH :: + Members '[GalleyProvider] r => + JSON ::: TeamId -> + (Handler r) Response unsuspendTeamH (_ ::: tid) = do empty <$ unsuspendTeam tid -unsuspendTeam :: TeamId -> (Handler r) () +unsuspendTeam :: + Members '[GalleyProvider] r => + TeamId -> + (Handler r) () unsuspendTeam tid = do changeTeamAccountStatuses tid Active - lift $ wrapHttp $ Intra.changeTeamStatus tid Team.Active Nothing + lift $ liftSem $ GalleyProvider.changeTeamStatus tid Team.Active Nothing ------------------------------------------------------------------------------- -- Internal -changeTeamAccountStatuses :: TeamId -> AccountStatus -> (Handler r) () +changeTeamAccountStatuses :: + Members '[GalleyProvider] r => + TeamId -> + AccountStatus -> + (Handler r) () changeTeamAccountStatuses tid s = do - team <- Team.tdTeam <$> lift (wrapHttp $ Intra.getTeam tid) + team <- Team.tdTeam <$> lift (liftSem $ GalleyProvider.getTeam tid) unless (team ^. teamBinding == Binding) $ throwStd noBindingTeam - uids <- toList1 =<< lift (fmap (view Teams.userId) . view teamMembers <$> wrapHttp (Intra.getTeamMembers tid)) + uids <- toList1 =<< lift (fmap (view Teams.userId) . view teamMembers <$> liftSem (GalleyProvider.getTeamMembers tid)) wrapHttpClientE (API.changeAccountStatus uids s) !>> accountStatusError where toList1 (x : xs) = pure $ List1.list1 x xs diff --git a/services/brig/src/Brig/Team/Util.hs b/services/brig/src/Brig/Team/Util.hs index c5a442df12..987e849af7 100644 --- a/services/brig/src/Brig/Team/Util.hs +++ b/services/brig/src/Brig/Team/Util.hs @@ -19,19 +19,21 @@ module Brig.Team.Util where -- TODO: remove this module and move contents to Bri import Brig.API.Error import Brig.App -import qualified Brig.IO.Intra as Intra +import Brig.Effects.GalleyProvider (GalleyProvider) +import qualified Brig.Effects.GalleyProvider as GalleyProvider import Control.Error import Control.Lens import Data.Id import qualified Data.Set as Set import Galley.Types.Teams import Imports +import Polysemy (Member) import Wire.API.Team.Member import Wire.API.Team.Permission -ensurePermissions :: UserId -> TeamId -> [Perm] -> ExceptT Error (AppT r) () +ensurePermissions :: Member GalleyProvider r => UserId -> TeamId -> [Perm] -> ExceptT Error (AppT r) () ensurePermissions u t perms = do - m <- lift $ wrapHttp $ Intra.getTeamMember u t + m <- lift $ liftSem $ GalleyProvider.getTeamMember u t unless (check m) $ throwStd insufficientTeamPermissions where @@ -42,9 +44,9 @@ ensurePermissions u t perms = do -- | Privilege escalation detection (make sure no `RoleMember` user creates a `RoleOwner`). -- -- There is some code duplication with 'Galley.API.Teams.ensureNotElevated'. -ensurePermissionToAddUser :: UserId -> TeamId -> Permissions -> ExceptT Error (AppT r) () +ensurePermissionToAddUser :: Member GalleyProvider r => UserId -> TeamId -> Permissions -> ExceptT Error (AppT r) () ensurePermissionToAddUser u t inviteePerms = do - minviter <- lift $ wrapHttp $ Intra.getTeamMember u t + minviter <- lift $ liftSem $ GalleyProvider.getTeamMember u t unless (check minviter) $ throwStd insufficientTeamPermissions where diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index 2ea7a8d86f..2d66ea2736 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.Effects.BlacklistStore (BlacklistStore) +import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Phone import Brig.Types.Intra (ReAuthUser, reAuthCode, reAuthCodeAction, reAuthPassword) import Brig.Types.User.Auth @@ -58,7 +59,7 @@ 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 (Member) +import Polysemy (Member, Members) import Wire.API.Error import qualified Wire.API.Error.Brig as E import qualified Wire.API.User as Public @@ -66,7 +67,11 @@ import Wire.API.User.Auth as Public import Wire.Swagger as Doc (pendingLoginError) routesPublic :: - Member BlacklistStore r => + Members + '[ BlacklistStore, + GalleyProvider + ] + r => Routes Doc.ApiBuilder (Handler r) () routesPublic = do -- Note: this endpoint should always remain available at its unversioned @@ -197,7 +202,9 @@ routesPublic = do Doc.body (Doc.ref Public.modelRemoveCookies) Doc.end Doc.errorResponse (errorToWai @'E.BadCredentials) -routesInternal :: Routes a (Handler r) () +routesInternal :: + Members '[GalleyProvider] 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) $ @@ -237,31 +244,45 @@ getLoginCode phone = do code <- lift $ wrapClient $ Auth.lookupLoginCode phone maybe (throwStd loginCodeNotFound) pure code -reAuthUserH :: UserId ::: JsonRequest ReAuthUser -> (Handler r) Response +reAuthUserH :: + Members '[GalleyProvider] r => + UserId ::: JsonRequest ReAuthUser -> + (Handler r) Response reAuthUserH (uid ::: req) = do reAuthUser uid =<< parseJsonBody req pure empty -reAuthUser :: UserId -> ReAuthUser -> (Handler r) () +reAuthUser :: + Members '[GalleyProvider] r => + UserId -> + ReAuthUser -> + (Handler r) () reAuthUser uid body = do wrapClientE (User.reauthenticate 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 '[GalleyProvider] 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 '[GalleyProvider] 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 @@ -272,14 +293,20 @@ ssoLogin l persist = do let typ = if persist then PersistentCookie else SessionCookie wrapHttpClientE (Auth.ssoLogin l typ) !>> loginError -legalHoldLoginH :: JsonRequest LegalHoldLogin ::: JSON -> (Handler r) Response +legalHoldLoginH :: + Members '[GalleyProvider] 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 '[GalleyProvider] 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 diff --git a/services/brig/src/Brig/User/API/Handle.hs b/services/brig/src/Brig/User/API/Handle.hs index db71c04406..c32303e49c 100644 --- a/services/brig/src/Brig/User/API/Handle.hs +++ b/services/brig/src/Brig/User/API/Handle.hs @@ -28,6 +28,7 @@ import Brig.API.Handler (Handler) import qualified Brig.API.User as API import Brig.App import qualified Brig.Data.User as Data +import Brig.Effects.GalleyProvider (GalleyProvider) import qualified Brig.Federation.Client as Federation import Brig.Options (searchSameTeamOnly) import Control.Lens (view) @@ -36,13 +37,18 @@ 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 :: + Members '[GalleyProvider] r => + UserId -> + Qualified Handle -> + (Handler r) (Maybe Public.UserProfile) getHandleInfo self handle = do lself <- qualifyLocal self foldQualified @@ -58,7 +64,11 @@ getRemoteHandleInfo handle = do . Log.field "domain" (show (tDomain handle)) Federation.getUserHandleInfo handle !>> fedError -getLocalHandleInfo :: Local UserId -> Handle -> (Handler r) (Maybe Public.UserProfile) +getLocalHandleInfo :: + Members '[GalleyProvider] 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 @@ -66,7 +76,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 c303d36ac4..33cc30c3b0 100644 --- a/services/brig/src/Brig/User/API/Search.hs +++ b/services/brig/src/Brig/User/API/Search.hs @@ -26,8 +26,9 @@ import Brig.API.Error (fedError) import Brig.API.Handler import Brig.App import qualified Brig.Data.User as DB +import Brig.Effects.GalleyProvider (GalleyProvider) +import qualified Brig.Effects.GalleyProvider as GalleyProvider import qualified Brig.Federation.Client as Federation -import qualified Brig.IO.Intra as Intra import qualified Brig.Options as Opts import Brig.Team.Util (ensurePermissions) import Brig.Types.Search as Search @@ -45,6 +46,7 @@ import Imports import Network.Wai.Routing import Network.Wai.Utilities ((!>>)) import Network.Wai.Utilities.Response (empty) +import Polysemy import System.Logger (field, msg) import System.Logger.Class (val, (~~)) import qualified System.Logger.Class as Log @@ -82,7 +84,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 '[GalleyProvider] 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 @@ -108,7 +116,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 '[GalleyProvider] 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 @@ -147,7 +161,7 @@ searchLocally searcherId searchTerm maybeMaxResults = do then pure (Search.TeamOnly t) else do -- For team users, we need to check the visibility flag - handleTeamVisibility t <$> wrapHttp (Intra.getTeamSearchVisibility t) + handleTeamVisibility t <$> liftSem (GalleyProvider.getTeamSearchVisibility t) exactHandleSearch :: (Handler r) (Maybe Contact) exactHandleSearch = do @@ -159,6 +173,7 @@ searchLocally searcherId searchTerm maybeMaxResults = do <$$> HandleAPI.getLocalHandleInfo lsearcherId handle teamUserSearch :: + Members '[GalleyProvider] r => UserId -> TeamId -> Maybe Text -> diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index 712da1d6b0..95ce5c13eb 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -48,8 +48,9 @@ import qualified Brig.Data.LoginCode as Data import qualified Brig.Data.User as Data import Brig.Data.UserKey import qualified Brig.Data.UserKey as Data +import Brig.Effects.GalleyProvider (GalleyProvider) +import qualified Brig.Effects.GalleyProvider as GalleyProvider import Brig.Email -import qualified Brig.IO.Intra as Intra import qualified Brig.Options as Opt import Brig.Phone import Brig.Types.Intra @@ -73,6 +74,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 @@ -133,82 +135,68 @@ lookupLoginCode phone = Data.lookupLoginCode u login :: - forall m. - ( MonadReader Env m, - MonadMask m, - MonadHttp m, - HasRequestId m, - Log.MonadLogger m, - MonadClient m, - ZAuth.MonadZAuth m, - MonadIndexIO m, - MonadUnliftIO m - ) => + forall r. + Members '[GalleyProvider] 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 + uid <- wrapHttpClientE $ 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 - AuthSuspended -> throwE LoginSuspended - AuthEphemeral -> throwE LoginEphemeral - AuthPendingInvitation -> throwE LoginPendingActivation + wrapHttpClientE $ checkRetryLimit uid + wrapHttpClientE $ + Data.authenticate uid pw `catchE` \case + AuthInvalidUser -> loginFailed uid + AuthInvalidCredentials -> loginFailed uid + AuthSuspended -> throwE LoginSuspended + AuthEphemeral -> throwE LoginEphemeral + AuthPendingInvitation -> throwE LoginPendingActivation verifyLoginCode code uid - newAccess @ZAuth.User @ZAuth.Access uid typ label + wrapHttpClientE $ newAccess @ZAuth.User @ZAuth.Access uid typ label where - verifyLoginCode :: Maybe Code.Value -> UserId -> ExceptT LoginError m () + verifyLoginCode :: Maybe Code.Value -> UserId -> ExceptT LoginError (AppT r) () verifyLoginCode mbCode uid = verifyCode mbCode Login uid `catchE` \case - VerificationCodeNoPendingCode -> loginFailedWith LoginCodeInvalid uid - VerificationCodeRequired -> loginFailedWith LoginCodeRequired uid - VerificationCodeNoEmail -> loginFailed uid + VerificationCodeNoPendingCode -> wrapHttpClientE $ loginFailedWith LoginCodeInvalid uid + VerificationCodeRequired -> wrapHttpClientE $ loginFailedWith LoginCodeRequired uid + VerificationCodeNoEmail -> wrapHttpClientE $ loginFailed uid login (SmsLogin phone code label) typ = do - uid <- resolveLoginId (LoginByPhone phone) + uid <- wrapHttpClientE $ resolveLoginId (LoginByPhone phone) lift . Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.login") - checkRetryLimit uid - ok <- lift $ Data.verifyLoginCode uid code + wrapHttpClientE $ checkRetryLimit uid + ok <- wrapHttpClientE $ Data.verifyLoginCode uid code unless ok $ - loginFailed uid - newAccess @ZAuth.User @ZAuth.Access uid typ label + wrapHttpClientE $ loginFailed uid + wrapHttpClientE $ newAccess @ZAuth.User @ZAuth.Access uid typ label verifyCode :: - forall m. - ( MonadReader Env m, - MonadMask m, - MonadHttp m, - HasRequestId m, - Log.MonadLogger m, - MonadClient m - ) => + forall r. + Members '[GalleyProvider] 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 + mbFeatureEnabled <- liftSem $ GalleyProvider.getVerificationCodeEnabled `traverse` mbTeamId pure $ fromMaybe (Public.wsStatus (Public.defFeatureStatus @Public.SndFactorPasswordChallengeConfig) == Public.FeatureStatusEnabled) mbFeatureEnabled - isSsoUser <- Data.isSamlUser uid + isSsoUser <- wrapHttpClientE $ Data.isSamlUser uid when (featureEnabled && not isSsoUser) $ 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 <- isJust <$> wrapHttpClientE (Code.verify 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 - mbAccount <- Data.lookupAccount u + mbAccount <- wrapHttpClientE $ Data.lookupAccount u pure (userEmail <$> accountUser =<< mbAccount, userTeam <$> accountUser =<< mbAccount) loginFailedWith :: (MonadClient m, MonadReader Env m) => LoginError -> UserId -> ExceptT LoginError m () @@ -473,44 +461,29 @@ ssoLogin (SsoLogin uid label) typ = do -- | 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 '[GalleyProvider] r => LegalHoldLogin -> CookieType -> - ExceptT LegalHoldLoginError m (Access ZAuth.LegalHoldUser) + ExceptT LegalHoldLoginError (AppT r) (Access ZAuth.LegalHoldUser) legalHoldLogin (LegalHoldLogin uid plainTextPassword label) typ = do - Data.reauthenticate uid plainTextPassword !>> LegalHoldReAuthError + wrapHttpClientE (Data.reauthenticate uid plainTextPassword) !>> LegalHoldReAuthError -- 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 $ GalleyProvider.getTeamId uid case mteam of Nothing -> throwE LegalHoldLoginNoBindingTeam Just tid -> assertLegalHoldEnabled tid -- create access token and cookie - newAccess @ZAuth.LegalHoldUser @ZAuth.LegalHoldAccess uid typ label + wrapHttpClientE (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 '[GalleyProvider] r => TeamId -> - ExceptT LegalHoldLoginError m () + ExceptT LegalHoldLoginError (AppT r) () assertLegalHoldEnabled tid = do - stat <- lift $ Intra.getTeamLegalHoldStatus tid + stat <- lift $ liftSem $ GalleyProvider.getTeamLegalHoldStatus tid case wsStatus stat of FeatureStatusDisabled -> throwE LegalHoldLoginLegalHoldNotEnabled FeatureStatusEnabled -> pure () diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index bd9ae04f2f..70760c9ac6 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -22,9 +22,11 @@ module Brig.User.EJPD (ejpdRequest) where import Brig.API.Handler import Brig.API.User (lookupHandle) -import Brig.App (AppT, wrapClient, wrapHttp) +import Brig.App (AppT, liftSem, wrapClient, wrapHttp) import qualified Brig.Data.Connection as Conn import Brig.Data.User (lookupUser) +import Brig.Effects.GalleyProvider (GalleyProvider) +import qualified Brig.Effects.GalleyProvider as GalleyProvider import qualified Brig.IO.Intra as Intra import Brig.Types.User (HavePendingInvitations (NoPendingInvitations)) import Control.Error hiding (bool) @@ -33,6 +35,7 @@ import Data.Handle (Handle) import Data.Id (UserId) import qualified Data.Set as Set import Imports hiding (head) +import Polysemy (Member) import Servant.Swagger.Internal.Orphans () import Wire.API.Connection (Relation, RelationWithHistory (..), relationDropHistory) import qualified Wire.API.Push.Token as PushTok @@ -40,7 +43,7 @@ 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 GalleyProvider r => Maybe Bool -> EJPDRequestBody -> (Handler r) EJPDResponseBody ejpdRequest includeContacts (EJPDRequestBody handles) = do ExceptT $ Right . EJPDResponseBody . catMaybes <$> forM handles (go1 (fromMaybe False includeContacts)) where @@ -77,7 +80,7 @@ ejpdRequest includeContacts (EJPDRequestBody handles) = do mbTeamContacts <- case (includeContacts', userTeam target) of (True, Just tid) -> do - memberList <- wrapHttp $ Intra.getTeamMembers tid + memberList <- liftSem $ GalleyProvider.getTeamMembers tid let members = (view Team.userId <$> (memberList ^. Team.teamMembers)) \\ [uid] contactsFull :: [Maybe EJPDResponseItem] <-