From 7bce7cb756ae7801f164ec192f89d56a58d79ebc Mon Sep 17 00:00:00 2001 From: fisx Date: Thu, 30 Jul 2020 10:13:28 +0200 Subject: [PATCH 01/23] Cleanup (mostly spar) (#1179) * Make all imports qualified or explicit (in some modules). * Don't use (#) for json object construction. * Make a failed attempt at consolidating redundant test helpers. (at least now they are in the same place, and one has a slightly more convenient type.) * "Always" test for error labels, not just status codes. * Use smaller types in brig events. UserAccount contains User, and nothing else was used anywhere in the event code. --- services/brig/src/Brig/API/User.hs | 4 +- services/brig/src/Brig/IO/Intra.hs | 7 +- services/brig/src/Brig/User/Event.hs | 9 +- services/spar/src/Spar/Scim/Auth.hs | 29 +-- services/spar/src/Spar/Scim/Swagger.hs | 12 +- services/spar/src/Spar/Scim/Types.hs | 104 +++++------ services/spar/src/Spar/Scim/User.hs | 167 +++++++++--------- .../test-integration/Test/Spar/APISpec.hs | 43 +++-- .../Test/Spar/Scim/AuthSpec.hs | 7 - services/spar/test-integration/Util/Core.hs | 14 +- 10 files changed, 203 insertions(+), 193 deletions(-) diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index e30bd700644..5302c8b29ca 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -177,7 +177,7 @@ createUser new@NewUser {..} = do activatedTeam <- lift $ do Data.insertAccount account Nothing pw False Intra.createSelfConv uid - Intra.onUserEvent uid Nothing (UserCreated account) + Intra.onUserEvent uid Nothing (UserCreated (accountUser account)) -- If newUserEmailCode is set, team gets activated _now_ else createUser fails case (tid, newTeam) of (Just t, Just nt) -> createTeam uid (isJust newUserEmailCode) (bnuTeam nt) t @@ -599,7 +599,7 @@ onActivated (AccountActivated account) = do let uid = userId (accountUser account) Log.debug $ field "user" (toByteString uid) . field "action" (Log.val "User.onActivated") Log.info $ field "user" (toByteString uid) . msg (val "User activated") - Intra.onUserEvent uid Nothing $ UserActivated account + Intra.onUserEvent uid Nothing $ UserActivated (accountUser account) return (uid, userIdentity (accountUser account), True) onActivated (EmailActivated uid email) = do Intra.onUserEvent uid Nothing (emailUpdated uid email) diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index c1eaf44e849..95b3245da25 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -67,7 +67,6 @@ import Brig.Data.Connection (lookupContactList) import qualified Brig.IO.Journal as Journal import Brig.RPC import Brig.Types -import Brig.Types.Intra import Brig.User.Event import qualified Brig.User.Event.Log as Log import qualified Brig.User.Search.Index as Search @@ -181,7 +180,7 @@ updateSearchIndex orig e = case e of journalEvent :: UserId -> UserEvent -> AppIO () journalEvent orig e = case e of UserActivated acc -> - Journal.userActivate (accountUser acc) + Journal.userActivate acc UserUpdated UserUpdatedData {eupName = Just name} -> Journal.userUpdate orig Nothing Nothing (Just name) UserUpdated UserUpdatedData {eupLocale = Just loc} -> @@ -350,13 +349,13 @@ notifyContacts events orig route conn = do -- Event Serialisation: toPushFormat :: Event -> Maybe Object -toPushFormat (UserEvent (UserCreated (UserAccount u _))) = +toPushFormat (UserEvent (UserCreated u)) = Just $ M.fromList [ "type" .= ("user.new" :: Text), "user" .= SelfProfile (u {userIdentity = Nothing}) ] -toPushFormat (UserEvent (UserActivated (UserAccount u _))) = +toPushFormat (UserEvent (UserActivated u)) = Just $ M.fromList [ "type" .= ("user.activate" :: Text), diff --git a/services/brig/src/Brig/User/Event.hs b/services/brig/src/Brig/User/Event.hs index 50569d494ea..6144b9d2555 100644 --- a/services/brig/src/Brig/User/Event.hs +++ b/services/brig/src/Brig/User/Event.hs @@ -20,7 +20,6 @@ module Brig.User.Event where import Brig.Types -import Brig.Types.Intra import Data.Handle (Handle) import Data.Id import Imports @@ -32,10 +31,10 @@ data Event | ClientEvent !ClientEvent data UserEvent - = UserCreated !UserAccount + = UserCreated !User | -- | A user is activated when the first user identity (email address or phone number) -- is verified. {#RefActivationEvent} - UserActivated !UserAccount + UserActivated !User | -- | Account & API access of a user has been suspended. UserSuspended !UserId | -- | Account & API access of a previously suspended user @@ -157,8 +156,8 @@ connEventUserId :: ConnectionEvent -> UserId connEventUserId ConnectionUpdated {..} = ucFrom ucConn userEventUserId :: UserEvent -> UserId -userEventUserId (UserCreated u) = userId (accountUser u) -userEventUserId (UserActivated u) = userId (accountUser u) +userEventUserId (UserCreated u) = userId u +userEventUserId (UserActivated u) = userId u userEventUserId (UserSuspended u) = u userEventUserId (UserResumed u) = u userEventUserId (UserDeleted u) = u diff --git a/services/spar/src/Spar/Scim/Auth.hs b/services/spar/src/Spar/Scim/Auth.hs index 0c5312d6469..01c2e5d4a81 100644 --- a/services/spar/src/Spar/Scim/Auth.hs +++ b/services/spar/src/Spar/Scim/Auth.hs @@ -36,18 +36,27 @@ where import Control.Lens hiding ((.=), Strict) import qualified Data.ByteString.Base64 as ES -import Data.Id -import Data.String.Conversions -import Data.Time +import Data.Id (ScimTokenId, UserId, randomId) +import Data.String.Conversions (cs) +import Data.Time (getCurrentTime) import Imports import OpenSSL.Random (randBytes) import qualified SAML2.WebSSO as SAML -import Servant +import Servant ((:<|>) ((:<|>)), NoContent (NoContent), ServerT) import Spar.App (Spar, sparCtxOpts, wrapMonadClient, wrapMonadClient) import qualified Spar.Data as Data -import Spar.Error +import qualified Spar.Error as E import qualified Spar.Intra.Brig as Intra.Brig import Spar.Scim.Types + ( APIScimToken, + CreateScimToken (CreateScimToken), + CreateScimTokenResponse (..), + ScimTokenList, + ScimTokenList (..), + SparTag, + createScimTokenDescr, + createScimTokenPassword, + ) import Spar.Types -- FUTUREWORK: these imports are not very handy. split up Spar.Scim into -- Spar.Scim.{Core,User,Group} to avoid at least some of the hscim name clashes? @@ -94,7 +103,7 @@ createScimToken zusr CreateScimToken {..} = do tokenNumber <- fmap length $ wrapMonadClient $ Data.getScimTokens teamid maxTokens <- asks (maxScimTokens . sparCtxOpts) unless (tokenNumber < maxTokens) $ - throwSpar SparProvisioningTokenLimitReached + E.throwSpar E.SparProvisioningTokenLimitReached idps <- wrapMonadClient $ Data.getIdPConfigsByTeam teamid case idps of [idp] -> do @@ -117,13 +126,13 @@ createScimToken zusr CreateScimToken {..} = do -- NB: if the two following cases do not result in errors, 'validateScimUser' needs to -- be changed. currently, it relies on the fact that there is always an IdP. [] -> - throwSpar $ - SparProvisioningNoSingleIdP + E.throwSpar $ + E.SparProvisioningNoSingleIdP "SCIM tokens can only be created for a team with an IdP, \ \but none are found" _ -> - throwSpar $ - SparProvisioningNoSingleIdP + E.throwSpar $ + E.SparProvisioningNoSingleIdP "SCIM tokens can only be created for a team with exactly one IdP, \ \but more are found" diff --git a/services/spar/src/Spar/Scim/Swagger.hs b/services/spar/src/Spar/Scim/Swagger.hs index 44651d4a240..c0ae1fa7a1f 100644 --- a/services/spar/src/Spar/Scim/Swagger.hs +++ b/services/spar/src/Spar/Scim/Swagger.hs @@ -30,16 +30,16 @@ module Spar.Scim.Swagger ) where -import Control.Lens -import Data.Id -import Data.Proxy +import Control.Lens ((&), (.~), (?~), mapped) +import Data.Id (ScimTokenId, TeamId) +import Data.Proxy (Proxy (Proxy)) import Data.Swagger hiding (Header (..)) -import Data.Time +import Data.Time (UTCTime) import Imports import qualified SAML2.WebSSO as SAML import Spar.Orphans () -import Spar.Scim -import Spar.Types +import Spar.Scim (CreateScimToken, CreateScimTokenResponse (..), ScimTokenList (..)) +import Spar.Types (ScimToken, ScimTokenInfo) instance ToParamSchema ScimToken where toParamSchema _ = toParamSchema (Proxy @Text) diff --git a/services/spar/src/Spar/Scim/Types.hs b/services/spar/src/Spar/Scim/Types.hs index 55fd8faa470..fc7ddd60bc6 100644 --- a/services/spar/src/Spar/Scim/Types.hs +++ b/services/spar/src/Spar/Scim/Types.hs @@ -41,21 +41,21 @@ module Spar.Scim.Types where import Brig.Types.Intra (AccountStatus (Active, Deleted, Ephemeral, Suspended)) -import Brig.Types.User as Brig -import Control.Lens hiding ((#), (.=), Strict) -import Data.Aeson as Aeson +import qualified Brig.Types.User as BT +import Control.Lens (makeLenses) +import Control.Monad.Except (throwError) +import qualified Data.Aeson as Aeson import qualified Data.CaseInsensitive as CI import Data.Handle (Handle) -import Data.Id -import Data.Json.Util ((#)) +import Data.Id (ScimTokenId, UserId) import qualified Data.Map as Map import Data.Misc (PlainTextPassword) import Imports import qualified SAML2.WebSSO as SAML -import Servant +import Servant ((:<|>), (:>), DeleteNoContent, Get, Header, JSON, NoContent, Post, QueryParam', ReqBody, Required, Strict) import Servant.API.Generic ((:-), ToServantApi) -import Spar.API.Util -import Spar.Types +import Spar.API.Util (OmitDocs) +import Spar.Types (ScimToken, ScimTokenInfo) import Web.Scim.AttrName (AttrName (..)) import qualified Web.Scim.Capabilities.MetaSchema as Scim.Meta import qualified Web.Scim.Class.Auth as Scim.Auth @@ -68,7 +68,7 @@ import qualified Web.Scim.Schema.PatchOp as Scim import Web.Scim.Schema.Schema (Schema (CustomSchema)) import qualified Web.Scim.Schema.Schema as Scim import qualified Web.Scim.Schema.User as Scim.User -import Wire.API.User.RichInfo +import qualified Wire.API.User.RichInfo as RI ---------------------------------------------------------------------------- -- Schemas @@ -76,8 +76,8 @@ import Wire.API.User.RichInfo userSchemas :: [Scim.Schema] userSchemas = [ Scim.User20, - Scim.CustomSchema richInfoAssocListURN, - Scim.CustomSchema richInfoMapURN + Scim.CustomSchema RI.richInfoAssocListURN, + Scim.CustomSchema RI.richInfoMapURN ] ---------------------------------------------------------------------------- @@ -137,42 +137,42 @@ newtype WrappedScimUser tag = WrappedScimUser -- | Extra Wire-specific data contained in a SCIM user profile. data ScimUserExtra = ScimUserExtra - { _sueRichInfo :: RichInfo + { _sueRichInfo :: RI.RichInfo } deriving (Eq, Show) makeLenses ''ScimUserExtra -instance FromJSON ScimUserExtra where - parseJSON v = ScimUserExtra <$> parseJSON v +instance Aeson.FromJSON ScimUserExtra where + parseJSON v = ScimUserExtra <$> Aeson.parseJSON v -instance ToJSON ScimUserExtra where - toJSON (ScimUserExtra rif) = toJSON rif +instance Aeson.ToJSON ScimUserExtra where + toJSON (ScimUserExtra rif) = Aeson.toJSON rif instance Scim.Patchable ScimUserExtra where - applyOperation (ScimUserExtra (RichInfo rinfRaw)) (Operation o (Just (NormalPath (AttrPath (Just (CustomSchema schema)) (AttrName (CI.mk -> ciAttrName)) Nothing))) val) - | schema == richInfoMapURN = - let rinf = richInfoMap $ fromRichInfoAssocList rinfRaw - unrinf = ScimUserExtra . RichInfo . toRichInfoAssocList . (`RichInfoMapAndList` mempty) + applyOperation (ScimUserExtra (RI.RichInfo rinfRaw)) (Operation o (Just (NormalPath (AttrPath (Just (CustomSchema schema)) (AttrName (CI.mk -> ciAttrName)) Nothing))) val) + | schema == RI.richInfoMapURN = + let rinf = RI.richInfoMap $ RI.fromRichInfoAssocList rinfRaw + unrinf = ScimUserExtra . RI.RichInfo . RI.toRichInfoAssocList . (`RI.RichInfoMapAndList` mempty) in unrinf <$> case o of Scim.Remove -> pure $ Map.delete ciAttrName rinf _AddOrReplace -> case val of - (Just (String textVal)) -> + (Just (Aeson.String textVal)) -> pure $ Map.insert ciAttrName textVal rinf _ -> throwError $ Scim.badRequest Scim.InvalidValue $ Just "rich info values can only be text" - | schema == richInfoAssocListURN = - let rinf = richInfoAssocList $ fromRichInfoAssocList rinfRaw - unrinf = ScimUserExtra . RichInfo . toRichInfoAssocList . (mempty `RichInfoMapAndList`) - matchesAttrName (RichField k _) = k == ciAttrName + | schema == RI.richInfoAssocListURN = + let rinf = RI.richInfoAssocList $ RI.fromRichInfoAssocList rinfRaw + unrinf = ScimUserExtra . RI.RichInfo . RI.toRichInfoAssocList . (mempty `RI.RichInfoMapAndList`) + matchesAttrName (RI.RichField k _) = k == ciAttrName in unrinf <$> case o of Scim.Remove -> pure $ filter (not . matchesAttrName) rinf _AddOrReplace -> case val of - (Just (String textVal)) -> - let newField = RichField ciAttrName textVal + (Just (Aeson.String textVal)) -> + let newField = RI.RichField ciAttrName textVal replaceIfMatchesAttrName f = if matchesAttrName f then newField else f newRichInfo = if not $ any matchesAttrName rinf @@ -202,8 +202,8 @@ instance Scim.Patchable ScimUserExtra where data ValidScimUser = ValidScimUser { _vsuUserRef :: SAML.UserRef, _vsuHandle :: Handle, - _vsuName :: Maybe Name, -- TODO: remove the 'Maybe' here, and construct the name not in "Spar.Intra.Brig", but in 'validateScimUser'. - _vsuRichInfo :: RichInfo, + _vsuName :: Maybe BT.Name, -- TODO: remove the 'Maybe' here, and construct the name not in "Spar.Intra.Brig", but in 'validateScimUser'. + _vsuRichInfo :: RI.RichInfo, _vsuActive :: Bool } deriving (Eq, Show) @@ -242,19 +242,19 @@ data CreateScimToken = CreateScimToken } deriving (Eq, Show) -instance FromJSON CreateScimToken where - parseJSON = withObject "CreateScimToken" $ \o -> do - createScimTokenDescr <- o .: "description" - createScimTokenPassword <- o .:? "password" +instance Aeson.FromJSON CreateScimToken where + parseJSON = Aeson.withObject "CreateScimToken" $ \o -> do + createScimTokenDescr <- o Aeson..: "description" + createScimTokenPassword <- o Aeson..:? "password" pure CreateScimToken {..} -- Used for integration tests -instance ToJSON CreateScimToken where +instance Aeson.ToJSON CreateScimToken where toJSON CreateScimToken {..} = - object $ - "description" .= createScimTokenDescr - # "password" .= createScimTokenPassword - # [] + Aeson.object + [ "description" Aeson..= createScimTokenDescr, + "password" Aeson..= createScimTokenPassword + ] -- | Type used for the response of 'APIScimTokenCreate'. data CreateScimTokenResponse = CreateScimTokenResponse @@ -264,17 +264,17 @@ data CreateScimTokenResponse = CreateScimTokenResponse deriving (Eq, Show) -- Used for integration tests -instance FromJSON CreateScimTokenResponse where - parseJSON = withObject "CreateScimTokenResponse" $ \o -> do - createScimTokenResponseToken <- o .: "token" - createScimTokenResponseInfo <- o .: "info" +instance Aeson.FromJSON CreateScimTokenResponse where + parseJSON = Aeson.withObject "CreateScimTokenResponse" $ \o -> do + createScimTokenResponseToken <- o Aeson..: "token" + createScimTokenResponseInfo <- o Aeson..: "info" pure CreateScimTokenResponse {..} -instance ToJSON CreateScimTokenResponse where +instance Aeson.ToJSON CreateScimTokenResponse where toJSON CreateScimTokenResponse {..} = - object - [ "token" .= createScimTokenResponseToken, - "info" .= createScimTokenResponseInfo + Aeson.object + [ "token" Aeson..= createScimTokenResponseToken, + "info" Aeson..= createScimTokenResponseInfo ] -- | Type used for responses of endpoints that return a list of SCIM tokens. @@ -286,15 +286,15 @@ data ScimTokenList = ScimTokenList } deriving (Eq, Show) -instance FromJSON ScimTokenList where - parseJSON = withObject "ScimTokenList" $ \o -> do - scimTokenListTokens <- o .: "tokens" +instance Aeson.FromJSON ScimTokenList where + parseJSON = Aeson.withObject "ScimTokenList" $ \o -> do + scimTokenListTokens <- o Aeson..: "tokens" pure ScimTokenList {..} -instance ToJSON ScimTokenList where +instance Aeson.ToJSON ScimTokenList where toJSON ScimTokenList {..} = - object - [ "tokens" .= scimTokenListTokens + Aeson.object + [ "tokens" Aeson..= scimTokenListTokens ] ---------------------------------------------------------------------- diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index 66708ed7925..e6900d73096 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -42,30 +42,31 @@ module Spar.Scim.User where import Brig.Types.Intra (AccountStatus) -import Brig.Types.User as BrigTypes +import Brig.Types.User (ManagedBy (..), Name (..), User (..), ssoIdentity) +import qualified Brig.Types.User as BT import Control.Error ((!?), (??)) import Control.Exception (assert) import Control.Lens ((^.)) -import Control.Monad.Except -import Control.Monad.Trans.Maybe -import Crypto.Hash -import Data.Aeson as Aeson +import Control.Monad.Except (MonadError, throwError) +import Control.Monad.Trans.Maybe (MaybeT (MaybeT), runMaybeT) +import Crypto.Hash (Digest, SHA256, hashlazy) +import qualified Data.Aeson as Aeson import Data.Handle (Handle (Handle), parseHandle) -import Data.Id +import Data.Id (Id (Id), UserId, idToText) import Data.Json.Util (UTCTimeMillis, fromUTCTimeMillis, toUTCTimeMillis) -import Data.Range -import Data.String.Conversions +import Data.Range (checkedEitherMsg, fromRange) +import Data.String.Conversions (cs) import qualified Data.Text as Text import qualified Data.UUID.V4 as UUID import Imports -import Network.URI +import Network.URI (URI, parseURI) import qualified SAML2.WebSSO as SAML import Spar.App (Spar, getUser, sparCtxOpts, validateEmailIfExists, wrapMonadClient) import qualified Spar.Data as Data import qualified Spar.Intra.Brig as Brig import Spar.Scim.Auth () -import Spar.Scim.Types -import Spar.Types +import qualified Spar.Scim.Types as ST +import Spar.Types (IdP, ScimTokenInfo (..), derivedOpts, derivedOptsScimBaseURI, richInfoLimit, wiTeam) import qualified System.Logger.Class as Log import qualified URI.ByteString as URIBS import qualified Web.Scim.Class.User as Scim @@ -78,16 +79,16 @@ import qualified Web.Scim.Schema.Meta as Scim import qualified Web.Scim.Schema.ResourceType as Scim import qualified Web.Scim.Schema.User as Scim import qualified Web.Scim.Schema.User as Scim.User (schemas) -import Wire.API.User.RichInfo +import qualified Wire.API.User.RichInfo as RI ---------------------------------------------------------------------------- -- UserDB instance -instance Scim.UserDB SparTag Spar where +instance Scim.UserDB ST.SparTag Spar where getUsers :: ScimTokenInfo -> Maybe Scim.Filter -> - Scim.ScimHandler Spar (Scim.ListResponse (Scim.StoredUser SparTag)) + Scim.ScimHandler Spar (Scim.ListResponse (Scim.StoredUser ST.SparTag)) getUsers _ Nothing = do throwError $ Scim.badRequest Scim.TooMany (Just "Please specify a filter when getting users.") getUsers ScimTokenInfo {stiTeam, stiIdP} (Just filter') = do @@ -116,7 +117,7 @@ instance Scim.UserDB SparTag Spar where getUser :: ScimTokenInfo -> UserId -> - Scim.ScimHandler Spar (Scim.StoredUser SparTag) + Scim.ScimHandler Spar (Scim.StoredUser ST.SparTag) getUser ScimTokenInfo {stiTeam} uid = do let notfound = Scim.notFound "User" (idToText uid) brigUser <- lift (Brig.getBrigUser uid) >>= maybe (throwError notfound) pure @@ -125,15 +126,15 @@ instance Scim.UserDB SparTag Spar where postUser :: ScimTokenInfo -> - Scim.User SparTag -> - Scim.ScimHandler Spar (Scim.StoredUser SparTag) + Scim.User ST.SparTag -> + Scim.ScimHandler Spar (Scim.StoredUser ST.SparTag) postUser tokinfo user = createValidScimUser tokinfo =<< validateScimUser tokinfo user putUser :: ScimTokenInfo -> UserId -> - Scim.User SparTag -> - Scim.ScimHandler Spar (Scim.StoredUser SparTag) + Scim.User ST.SparTag -> + Scim.ScimHandler Spar (Scim.StoredUser ST.SparTag) putUser tokinfo uid newScimUser = updateValidScimUser tokinfo uid =<< validateScimUser tokinfo newScimUser @@ -150,8 +151,8 @@ validateScimUser :: (m ~ Scim.ScimHandler Spar) => -- | Used to decide what IdP to assign the user to ScimTokenInfo -> - Scim.User SparTag -> - m ValidScimUser + Scim.User ST.SparTag -> + m ST.ValidScimUser validateScimUser tokinfo user = do idpConfig <- tokenInfoToIdP tokinfo richInfoLimit <- lift $ asks (richInfoLimit . sparCtxOpts) @@ -204,17 +205,17 @@ validateScimUser' :: IdP -> -- | Rich info limit Int -> - Scim.User SparTag -> - m ValidScimUser + Scim.User ST.SparTag -> + m ST.ValidScimUser validateScimUser' idp richInfoLimit user = do uref :: SAML.UserRef <- mkUserRef idp (Scim.externalId user) handl <- validateHandle . Text.toLower . Scim.userName $ user -- FUTUREWORK: 'Scim.userName' should be case insensitive; then the toLower here would -- be a little less brittle. mbName <- mapM validateName (Scim.displayName user) - richInfo <- validateRichInfo (Scim.extra user ^. sueRichInfo) + richInfo <- validateRichInfo (Scim.extra user ^. ST.sueRichInfo) let active = Scim.active user - pure $ ValidScimUser uref handl mbName richInfo (fromMaybe True active) + pure $ ST.ValidScimUser uref handl mbName richInfo (fromMaybe True active) where -- Validate a name (@displayName@). It has to conform to standard Wire rules. validateName :: Text -> m Name @@ -226,14 +227,14 @@ validateScimUser' idp richInfoLimit user = do Scim.InvalidValue (Just ("displayName must be a valid Wire name, but: " <> Text.pack err)) -- Validate rich info (@richInfo@). It must not exceed the rich info limit. - validateRichInfo :: RichInfo -> m RichInfo + validateRichInfo :: RI.RichInfo -> m RI.RichInfo validateRichInfo richInfo = do - let sze = richInfoSize richInfo + let sze = RI.richInfoSize richInfo when (sze > richInfoLimit) $ throwError $ ( Scim.badRequest Scim.InvalidValue ( Just . cs $ - show [richInfoMapURN, richInfoAssocListURN] + show [RI.richInfoMapURN, RI.richInfoAssocListURN] <> " together exceed the size limit: max " <> show richInfoLimit <> " characters, but got " @@ -294,9 +295,9 @@ createValidScimUser :: forall m. (m ~ Scim.ScimHandler Spar) => ScimTokenInfo -> - ValidScimUser -> - m (Scim.StoredUser SparTag) -createValidScimUser tokinfo vsu@(ValidScimUser uref handl mbName richInfo active) = do + ST.ValidScimUser -> + m (Scim.StoredUser ST.SparTag) +createValidScimUser tokinfo vsu@(ST.ValidScimUser uref handl mbName richInfo active) = do idpConfig <- tokenInfoToIdP tokinfo -- sanity check: do tenant of the URef and the Issuer of the IdP match? (this is mostly -- here to make sure a refactoring we did in the past is sound: we removed a lookup by @@ -345,7 +346,7 @@ createValidScimUser tokinfo vsu@(ValidScimUser uref handl mbName richInfo active -- checked.) lift $ Brig.getStatus buid >>= \old -> do - let new = scimActiveFlagToAccountStatus old (Just active) + let new = ST.scimActiveFlagToAccountStatus old (Just active) when (new /= old) $ Brig.setStatus buid new pure storedUser @@ -354,8 +355,8 @@ updateValidScimUser :: (m ~ Scim.ScimHandler Spar) => ScimTokenInfo -> UserId -> - ValidScimUser -> - m (Scim.StoredUser SparTag) + ST.ValidScimUser -> + m (Scim.StoredUser ST.SparTag) updateValidScimUser tokinfo uid newScimUser = do -- TODO: currently the types in @hscim@ are constructed in such a way that -- 'Scim.User.User' doesn't contain an ID, only 'Scim.StoredUser' @@ -369,20 +370,20 @@ updateValidScimUser tokinfo uid newScimUser = do -- TODO: how do we get this safe w.r.t. race conditions / crashes? -- construct old and new user values with metadata. - oldScimStoredUser :: Scim.StoredUser SparTag <- + oldScimStoredUser :: Scim.StoredUser ST.SparTag <- Scim.getUser tokinfo uid - oldValidScimUser :: ValidScimUser <- + oldValidScimUser :: ST.ValidScimUser <- validateScimUser tokinfo . Scim.value . Scim.thing $ oldScimStoredUser - assertUserRefNotUsedElsewhere (newScimUser ^. vsuUserRef) uid - assertHandleNotUsedElsewhere (newScimUser ^. vsuHandle) uid + assertUserRefNotUsedElsewhere (newScimUser ^. ST.vsuUserRef) uid + assertHandleNotUsedElsewhere (newScimUser ^. ST.vsuHandle) uid if oldValidScimUser == newScimUser then pure oldScimStoredUser else do - newScimStoredUser :: Scim.StoredUser SparTag <- + newScimStoredUser :: Scim.StoredUser ST.SparTag <- lift $ updScimStoredUser (synthesizeScimUser newScimUser) oldScimStoredUser -- update 'SAML.UserRef' on spar (also delete the old 'SAML.UserRef' if it exists and -- is different from the new one) - let newuref = newScimUser ^. vsuUserRef + let newuref = newScimUser ^. ST.vsuUserRef olduref <- do let extid :: Maybe Text extid = Scim.externalId . Scim.value . Scim.thing $ oldScimStoredUser @@ -401,25 +402,25 @@ updateValidScimUser tokinfo uid newScimUser = do -- TODO: if the user has been suspended or unsuspended in brig since the last scim -- write, we'll find the wrong information here. -- [see also](https://github.com/zinfra/backend-issues/issues/1006) - oldScimUser :: ValidScimUser <- + oldScimUser :: ST.ValidScimUser <- validateScimUser tokinfo . Scim.value . Scim.thing $ oldScimStoredUser -- the old scim user from our db is already validated, but this also recovers -- the extra details not stored in the DB that we need here. lift $ do - case newScimUser ^. vsuName of - Just nm | oldScimUser ^. vsuName /= Just nm -> Brig.setBrigUserName uid nm + case newScimUser ^. ST.vsuName of + Just nm | oldScimUser ^. ST.vsuName /= Just nm -> Brig.setBrigUserName uid nm _ -> pure () - when (oldScimUser ^. vsuHandle /= newScimUser ^. vsuHandle) + when (oldScimUser ^. ST.vsuHandle /= newScimUser ^. ST.vsuHandle) $ Brig.setBrigUserHandle uid - $ newScimUser ^. vsuHandle - when (oldScimUser ^. vsuRichInfo /= newScimUser ^. vsuRichInfo) + $ newScimUser ^. ST.vsuHandle + when (oldScimUser ^. ST.vsuRichInfo /= newScimUser ^. ST.vsuRichInfo) $ Brig.setBrigUserRichInfo uid - $ newScimUser ^. vsuRichInfo + $ newScimUser ^. ST.vsuRichInfo lift $ Brig.getStatus uid >>= \old -> do - let new = scimActiveFlagToAccountStatus old (Just $ newScimUser ^. vsuActive) + let new = ST.scimActiveFlagToAccountStatus old (Just $ newScimUser ^. ST.vsuActive) when (new /= old) $ Brig.setStatus uid new -- store new user value to scim_user table (spar). (this must happen last, so in case @@ -429,8 +430,8 @@ updateValidScimUser tokinfo uid newScimUser = do toScimStoredUser :: UserId -> - Scim.User SparTag -> - Spar (Scim.StoredUser SparTag) + Scim.User ST.SparTag -> + Spar (Scim.StoredUser ST.SparTag) toScimStoredUser uid usr = do SAML.Time (toUTCTimeMillis -> now) <- SAML.getNow (createdAt, lastUpdatedAt) <- fromMaybe (now, now) <$> wrapMonadClient (Data.readScimUserTimes uid) @@ -443,12 +444,12 @@ toScimStoredUser' :: UTCTimeMillis -> URIBS.URI -> UserId -> - Scim.User SparTag -> - Scim.StoredUser SparTag + Scim.User ST.SparTag -> + Scim.StoredUser ST.SparTag toScimStoredUser' createdAt lastChangedAt baseuri uid usr = Scim.WithMeta meta $ Scim.WithId uid - $ usr {Scim.User.schemas = userSchemas} + $ usr {Scim.User.schemas = ST.userSchemas} where mkLocation :: String -> URI mkLocation pathSuffix = convURI $ baseuri SAML.=/ cs pathSuffix @@ -470,18 +471,18 @@ toScimStoredUser' createdAt lastChangedAt baseuri uid usr = updScimStoredUser :: forall m. (SAML.HasNow m) => - Scim.User SparTag -> - Scim.StoredUser SparTag -> - m (Scim.StoredUser SparTag) + Scim.User ST.SparTag -> + Scim.StoredUser ST.SparTag -> + m (Scim.StoredUser ST.SparTag) updScimStoredUser usr storedusr = do SAML.Time (toUTCTimeMillis -> now) <- SAML.getNow pure $ updScimStoredUser' now usr storedusr updScimStoredUser' :: UTCTimeMillis -> - Scim.User SparTag -> - Scim.StoredUser SparTag -> - Scim.StoredUser SparTag + Scim.User ST.SparTag -> + Scim.StoredUser ST.SparTag -> + Scim.StoredUser ST.SparTag updScimStoredUser' now usr (Scim.WithMeta meta (Scim.WithId scimuid _)) = Scim.WithMeta meta' (Scim.WithId scimuid usr) where @@ -512,7 +513,7 @@ deleteScimUser ScimTokenInfo {stiTeam} uid = do maybe (logThenServerError $ "no userSSOId for user " <> cs (idToText uid)) pure - $ BrigTypes.userSSOId brigUser + $ BT.userSSOId brigUser uref <- either logThenServerError pure $ Brig.fromUserSSOId ssoId lift . wrapMonadClient $ Data.deleteSAMLUser uref lift . wrapMonadClient $ Data.deleteScimUserTimes uid @@ -539,7 +540,7 @@ deleteScimUser ScimTokenInfo {stiTeam} uid = do -- requirements of strong ETags ("same resources have the same version"). calculateVersion :: UserId -> - Scim.User SparTag -> + Scim.User ST.SparTag -> Scim.ETag calculateVersion uid usr = Scim.Weak (Text.pack (show h)) where @@ -586,17 +587,17 @@ assertHandleNotUsedElsewhere hndl uid = do -- | Helper function that translates a given brig user into a 'Scim.StoredUser', with some -- effects like updating the 'ManagedBy' field in brig and storing creation and update time -- stamps. -synthesizeStoredUser :: BrigTypes.User -> Scim.ScimHandler Spar (Scim.StoredUser SparTag) +synthesizeStoredUser :: BT.User -> Scim.ScimHandler Spar (Scim.StoredUser ST.SparTag) synthesizeStoredUser usr = do - let readState :: Spar (RichInfo, AccountStatus, Maybe (UTCTimeMillis, UTCTimeMillis), URIBS.URI) + let readState :: Spar (RI.RichInfo, AccountStatus, Maybe (UTCTimeMillis, UTCTimeMillis), URIBS.URI) readState = do - richInfo <- Brig.getBrigUserRichInfo (BrigTypes.userId usr) - accStatus <- Brig.getStatus (BrigTypes.userId usr) - accessTimes <- wrapMonadClient (Data.readScimUserTimes (BrigTypes.userId usr)) + richInfo <- Brig.getBrigUserRichInfo (BT.userId usr) + accStatus <- Brig.getStatus (BT.userId usr) + accessTimes <- wrapMonadClient (Data.readScimUserTimes (BT.userId usr)) baseuri <- asks $ derivedOptsScimBaseURI . derivedOpts . sparCtxOpts pure (richInfo, accStatus, accessTimes, baseuri) - let writeState :: UserId -> Maybe (UTCTimeMillis, UTCTimeMillis) -> ManagedBy -> Scim.StoredUser SparTag -> Spar () + let writeState :: UserId -> Maybe (UTCTimeMillis, UTCTimeMillis) -> ManagedBy -> Scim.StoredUser ST.SparTag -> Spar () writeState uid accessTimes managedBy storedUser = do when (isNothing accessTimes) $ do wrapMonadClient $ Data.writeScimUserTimes storedUser @@ -619,7 +620,7 @@ synthesizeStoredUser usr = do createdAt lastUpdatedAt baseuri - lift $ writeState (BrigTypes.userId usr) accessTimes (BrigTypes.userManagedBy usr) storedUser + lift $ writeState (BT.userId usr) accessTimes (BT.userManagedBy usr) storedUser pure storedUser synthesizeStoredUser' :: @@ -627,40 +628,40 @@ synthesizeStoredUser' :: Maybe SAML.UserRef -> Name -> Handle -> - RichInfo -> + RI.RichInfo -> AccountStatus -> UTCTimeMillis -> UTCTimeMillis -> URIBS.URI -> - MonadError Scim.ScimError m => m (Scim.StoredUser SparTag) + MonadError Scim.ScimError m => m (Scim.StoredUser ST.SparTag) synthesizeStoredUser' uid ssoid dname handle richInfo accStatus createdAt lastUpdatedAt baseuri = do sso <- do let err = throwError $ Scim.notFound "User" (cs $ show uid) -- See https://github.com/zinfra/backend-issues/issues/1365 maybe err pure ssoid - let scimUser :: Scim.User SparTag + let scimUser :: Scim.User ST.SparTag scimUser = synthesizeScimUser - ValidScimUser - { _vsuUserRef = sso, - _vsuHandle = handle, -- 'Maybe' there is one in @usr@, but we want to type checker to make sure this exists. - _vsuName = Just dname, - _vsuRichInfo = richInfo, - _vsuActive = scimActiveFlagFromAccountStatus accStatus + ST.ValidScimUser + { ST._vsuUserRef = sso, + ST._vsuHandle = handle, -- 'Maybe' there is one in @usr@, but we want to type checker to make sure this exists. + ST._vsuName = Just dname, + ST._vsuRichInfo = richInfo, + ST._vsuActive = ST.scimActiveFlagFromAccountStatus accStatus } pure $ toScimStoredUser' createdAt lastUpdatedAt baseuri uid scimUser -synthesizeScimUser :: ValidScimUser -> Scim.User SparTag +synthesizeScimUser :: ST.ValidScimUser -> Scim.User ST.SparTag synthesizeScimUser info = - let Handle userName = info ^. vsuHandle - mDisplayName = fromName <$> (info ^. vsuName) + let Handle userName = info ^. ST.vsuHandle + mDisplayName = fromName <$> (info ^. ST.vsuName) toExternalId' :: SAML.UserRef -> Maybe Text toExternalId' = either (const Nothing) Just . Brig.toExternalId . Brig.toUserSSOId - in (Scim.empty userSchemas userName (ScimUserExtra (info ^. vsuRichInfo))) - { Scim.externalId = toExternalId' $ info ^. vsuUserRef, + in (Scim.empty ST.userSchemas userName (ST.ScimUserExtra (info ^. ST.vsuRichInfo))) + { Scim.externalId = toExternalId' $ info ^. ST.vsuUserRef, Scim.displayName = mDisplayName, - Scim.active = Just $ info ^. vsuActive + Scim.active = Just $ info ^. ST.vsuActive } {- TODO: might be useful later. diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 10ad6ab4513..defbcf575cf 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -501,9 +501,6 @@ specBindingUsers = describe "binding existing users to sso identities" $ do context "with bind cookie and two other cookies in the request" $ do check (\bindcky -> Just . addAtEnd cky1 . addAtEnd cky2 . addAtBeginning cky3 $ bindcky) True -checkErr :: HasCallStack => (Int -> Bool) -> TestErrorLabel -> ResponseLBS -> Bool -checkErr statusIs label resp = statusIs (statusCode resp) && responseJsonEither resp == Right label - testGetPutDelete :: HasCallStack => (SparReq -> Maybe UserId -> IdPId -> IdPMetadataInfo -> Http ResponseLBS) -> SpecWith TestEnv testGetPutDelete whichone = do context "unknown IdP" $ do @@ -511,20 +508,20 @@ testGetPutDelete whichone = do env <- ask (_, _, _, (idpmeta, _)) <- registerTestIdPWithMeta whichone (env ^. teSpar) Nothing (IdPId UUID.nil) idpmeta - `shouldRespondWith` checkErr (== 404) "not-found" + `shouldRespondWith` checkErrHspec 404 "not-found" context "no zuser" $ do it "responds with 'client error'" $ do env <- ask (_, _, (^. idpId) -> idpid, (idpmeta, _)) <- registerTestIdPWithMeta whichone (env ^. teSpar) Nothing idpid idpmeta - `shouldRespondWith` checkErr (== 400) "client-error" + `shouldRespondWith` checkErrHspec 400 "client-error" context "zuser has no team" $ do it "responds with 'no team member'" $ do env <- ask (_, _, (^. idpId) -> idpid, (idpmeta, _)) <- registerTestIdPWithMeta (uid, _) <- call $ createRandomPhoneUser (env ^. teBrig) whichone (env ^. teSpar) (Just uid) idpid idpmeta - `shouldRespondWith` checkErr (== 403) "no-team-member" + `shouldRespondWith` checkErrHspec 403 "no-team-member" context "zuser is a team member, but not a team owner" $ do it "responds with 'insufficient-permissions' and a helpful message" $ do env <- ask @@ -533,7 +530,7 @@ testGetPutDelete whichone = do let Just perms = Galley.newPermissions mempty mempty in call $ createTeamMember (env ^. teBrig) (env ^. teGalley) teamid perms whichone (env ^. teSpar) (Just newmember) idpid idpmeta - `shouldRespondWith` checkErr (== 403) "insufficient-permissions" + `shouldRespondWith` checkErrHspec 403 "insufficient-permissions" -- Authenticate via sso, and assign owner status to the thus created user. (This doesn't work -- via the cookie, since we don't talk to nginz here, so we assume there is only one user in @@ -559,7 +556,7 @@ specCRUDIdentityProvider = do (_, _, (^. idpId) -> idpid) <- registerTestIdP (uid, _) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) callIdpGet' (env ^. teSpar) (Just uid) idpid - `shouldRespondWith` checkErr (== 403) "no-team-member" + `shouldRespondWith` checkErrHspec 403 "no-team-member" context "known IdP, client is team owner" $ do it "responds with 2xx and IdP" $ do env <- ask @@ -581,7 +578,7 @@ specCRUDIdentityProvider = do let Just perms = Galley.newPermissions mempty mempty in call $ createTeamMember (env ^. teBrig) (env ^. teGalley) teamid perms callIdpGetAll' (env ^. teSpar) (Just member) - `shouldRespondWith` checkErr (== 403) "insufficient-permissions" + `shouldRespondWith` checkErrHspec 403 "insufficient-permissions" context "no idps registered" $ do context "client is team owner" $ do it "returns an empty list" $ do @@ -614,7 +611,7 @@ specCRUDIdentityProvider = do (_, _, (^. idpId) -> idpid) <- registerTestIdP (uid, _) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) callIdpDelete' (env ^. teSpar) (Just uid) idpid - `shouldRespondWith` checkErr (== 403) "no-team-member" + `shouldRespondWith` checkErrHspec 403 "no-team-member" context "known IdP, IdP empty, client is team owner, without email" $ do it "responds with 2xx and removes IdP" $ do env <- ask @@ -622,16 +619,16 @@ specCRUDIdentityProvider = do callIdpDelete' (env ^. teSpar) (Just userid) idpid `shouldRespondWith` \resp -> statusCode resp < 300 callIdpGet' (env ^. teSpar) (Just userid) idpid - `shouldRespondWith` checkErr (== 404) "not-found" + `shouldRespondWith` checkErrHspec 404 "not-found" callIdpGetRaw' (env ^. teSpar) (Just userid) idpid - `shouldRespondWith` checkErr (== 404) "not-found" + `shouldRespondWith` checkErrHspec 404 "not-found" context "with email, idp non-empty, purge=false" $ do it "responds with 412 and does not remove IdP" $ do env <- ask (firstOwner, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta ssoOwner <- mkSsoOwner firstOwner tid idp privcreds callIdpDelete' (env ^. teSpar) (Just ssoOwner) (idp ^. idpId) - `shouldRespondWith` checkErr (== 412) "idp-has-bound-users" + `shouldRespondWith` checkErrHspec 412 "idp-has-bound-users" callIdpGet' (env ^. teSpar) (Just ssoOwner) (idp ^. idpId) `shouldRespondWith` \resp -> statusCode resp < 300 context "with email, idp non-empty, purge=true" $ do @@ -648,7 +645,7 @@ specCRUDIdentityProvider = do ssoOwner' `shouldBe` Nothing firstOwner' `shouldBe` Just firstOwner callIdpGet' (env ^. teSpar) (Just firstOwner) (idp ^. idpId) - `shouldRespondWith` checkErr (== 404) "not-found" + `shouldRespondWith` checkErrHspec 404 "not-found" describe "PUT /identity-providers/:idp" $ do testGetPutDelete callIdpUpdate' context "known IdP, client is team owner" $ do @@ -667,14 +664,14 @@ specCRUDIdentityProvider = do env <- ask (owner, _, (^. idpId) -> idpid) <- registerTestIdP callIdpUpdate' (env ^. teSpar) (Just owner) idpid (IdPMetadataValue "bloo" undefined) - `shouldRespondWith` ((== 400) . statusCode) + `shouldRespondWith` checkErrHspec 400 "invalid-metadata" describe "issuer changed to one that already exists in *another* team" $ do it "rejects" $ do env <- ask (owner1, _, (^. idpId) -> idpid1) <- registerTestIdP (_, _, _, (IdPMetadataValue _ idpmeta2, _)) <- registerTestIdPWithMeta callIdpUpdate' (env ^. teSpar) (Just owner1) idpid1 (IdPMetadataValue (cs $ SAML.encode idpmeta2) undefined) - `shouldRespondWith` checkErr (== 400) "idp-issuer-in-use" + `shouldRespondWith` checkErrHspec 400 "idp-issuer-in-use" describe "issuer changed to one that already exists in *the same* team" $ do it "rejects" $ do env <- ask @@ -683,7 +680,7 @@ specCRUDIdentityProvider = do _ <- call $ callIdpCreate (env ^. teSpar) (Just owner1) idpmeta2 let idpmeta3 = idpmeta1 & edIssuer .~ (idpmeta2 ^. edIssuer) callIdpUpdate' (env ^. teSpar) (Just owner1) idpid1 (IdPMetadataValue (cs $ SAML.encode idpmeta3) undefined) - `shouldRespondWith` checkErr (== 400) "idp-issuer-in-use" + `shouldRespondWith` checkErrHspec 400 "idp-issuer-in-use" describe "issuer changed to one that is new" $ do it "updates old idp, updating both issuer and old_issuers" $ do env <- ask @@ -797,25 +794,25 @@ specCRUDIdentityProvider = do (uid, _tid) <- call $ createUserWithTeamDisableSSO (env ^. teBrig) (env ^. teGalley) (SampleIdP metadata _ _ _) <- makeSampleIdPMetadata callIdpCreate' (env ^. teSpar) (Just uid) metadata - `shouldRespondWith` checkErr (== 403) "sso-disabled" + `shouldRespondWith` checkErrHspec 403 "sso-disabled" context "bad xml" $ do it "responds with a 'client error'" $ do env <- ask callIdpCreateRaw' (env ^. teSpar) Nothing "application/xml" "@@ bad xml ###" - `shouldRespondWith` checkErr (== 400) "invalid-metadata" + `shouldRespondWith` checkErrHspec 400 "invalid-metadata" context "no zuser" $ do it "responds with 'client error'" $ do env <- ask (SampleIdP idpmeta _ _ _) <- makeSampleIdPMetadata callIdpCreate' (env ^. teSpar) Nothing idpmeta - `shouldRespondWith` checkErr (== 400) "client-error" + `shouldRespondWith` checkErrHspec 400 "client-error" context "zuser has no team" $ do it "responds with 'no team member'" $ do env <- ask (uid, _) <- call $ createRandomPhoneUser (env ^. teBrig) (SampleIdP idpmeta _ _ _) <- makeSampleIdPMetadata callIdpCreate' (env ^. teSpar) (Just uid) idpmeta - `shouldRespondWith` checkErr (== 403) "no-team-member" + `shouldRespondWith` checkErrHspec 403 "no-team-member" context "zuser is a team member, but not a team owner" $ do it "responds with 'insufficient-permissions' and a helpful message" $ do env <- ask @@ -824,7 +821,7 @@ specCRUDIdentityProvider = do let Just perms = Galley.newPermissions mempty mempty in call $ createTeamMember (env ^. teBrig) (env ^. teGalley) tid perms callIdpCreate' (env ^. teSpar) (Just newmember) (idp ^. idpMetadata) - `shouldRespondWith` checkErr (== 403) "insufficient-permissions" + `shouldRespondWith` checkErrHspec 403 "insufficient-permissions" context "idp (identified by issuer) is in use by other team" $ do it "rejects" $ do env <- ask @@ -860,7 +857,7 @@ specCRUDIdentityProvider = do it "responds with a 'client error'" $ do env <- ask callIdpCreateRaw' (env ^. teSpar) Nothing "application/json" "@@ bad json ###" - `shouldRespondWith` checkErr (== 400) "invalid-metadata" + `shouldRespondWith` checkErrHspec 400 "invalid-metadata" context "good json" $ do it "responds with 2xx; makes IdP available for GET /identity-providers/" $ do env <- ask diff --git a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs index 909a13f4518..f4326721243 100644 --- a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs @@ -276,10 +276,3 @@ testAuthIsNeeded = do -- Try to do @GET /Users@ without a token and check that it fails listUsers_ Nothing Nothing (env ^. teSpar) !!! checkErr 401 Nothing - -checkErr :: HasCallStack => Int -> Maybe TestErrorLabel -> Assertions () -checkErr status mlabel = do - const status === statusCode - case mlabel of - Nothing -> pure () - Just label -> const (Right label) === responseJsonEither diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 1b98b0fc4c4..64996367c7a 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -117,11 +117,13 @@ module Util.Core callGetDefaultSsoCode, callSetDefaultSsoCode, callDeleteDefaultSsoCode, + checkErr, + checkErrHspec, ) where import Bilge hiding (getCookie) -- we use Web.Cookie instead of the http-client type -import Bilge.Assert ((!!!), ( UserRef -> TestSpar (Maybe UserId) getUserIdViaRef' uref = do aFewTimes (runSparCass $ Data.getSAMLUser uref) isJust + +checkErr :: HasCallStack => Int -> Maybe TestErrorLabel -> Assertions () +checkErr status mlabel = do + const status === statusCode + case mlabel of + Nothing -> pure () + Just label -> const (Right label) === responseJsonEither + +checkErrHspec :: HasCallStack => Int -> TestErrorLabel -> ResponseLBS -> Bool +checkErrHspec status label resp = status == statusCode resp && responseJsonEither resp == Right label From 786a4661c44b9ea76895fedcca5d03e81b8077d2 Mon Sep 17 00:00:00 2001 From: jschaul Date: Fri, 31 Jul 2020 08:54:11 +0200 Subject: [PATCH 02/23] Discover and expose SFT servers in /calls/config/v2 (#1177) When configured with SFT information, brig will start a thread to discover SFT servers using DNS SRV records. The discovered servers are then made available on the `/calls/config/v2` endpoint along with the TURN servers. Major Changes: * Move code for SRV lookup from federation-util to dedicated package, delete federation-util * Refactor all `List1` to `NonEmpty` in RTCConfiguration * Integration tests now depend on globally created DNS entries Log whenever sft * service discovery fails or returns empty Add a safe way to create `HttpsUrl` * Add deprecation notice to /calls/config swagger docs Co-authored-by: Akshay Mankar Co-authored-by: fisx --- libs/brig-types/brig-types.cabal | 6 +- libs/brig-types/src/Brig/Types.hs | 2 +- .../src/Brig/Types/{TURN.hs => Calling.hs} | 4 +- libs/{federation-util => dns-util}/LICENSE | 0 .../dns-util.cabal} | 56 ++--- .../package.yaml | 29 +-- .../dns-util/src/Wire/Network/DNS/Effect.hs | 31 ++- .../src/Wire/Network/DNS/SRV.hs} | 54 ++--- .../test/Spec.hs | 0 .../test/Test/Wire/Network/DNS/SRVSpec.hs} | 56 +++-- libs/federation-util/.ghcid | 1 - .../src/Network/Federation/Util.hs | 23 -- .../src/Network/Federation/Util/DNS.hs | 45 ---- libs/types-common/src/Data/Misc.hs | 8 +- .../src/Wire/API/Call/{TURN.hs => Config.hs} | 70 ++++-- libs/wire-api/src/Wire/API/Swagger.hs | 6 +- libs/wire-api/test/unit/Main.hs | 4 +- .../Test/Wire/API/Call/{TURN.hs => Config.hs} | 4 +- .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 17 +- .../Test/Wire/API/Roundtrip/ByteString.hs | 12 +- libs/wire-api/wire-api.cabal | 4 +- services/brig/brig.cabal | 23 +- services/brig/package.yaml | 11 + services/brig/src/Brig/API/Public.hs | 4 +- services/brig/src/Brig/App.hs | 18 +- services/brig/src/Brig/Calling.hs | 113 ++++++++++ .../brig/src/Brig/{TURN => Calling}/API.hs | 55 +++-- services/brig/src/Brig/Calling/Internal.hs | 42 ++++ services/brig/src/Brig/Options.hs | 44 +++- services/brig/src/Brig/PolyLog.hs | 19 ++ services/brig/src/Brig/Run.hs | 7 + .../integration/API/{TURN.hs => Calling.hs} | 64 ++++-- services/brig/test/integration/Main.hs | 4 +- services/brig/test/integration/Util.hs | 16 +- services/brig/test/unit/Main.hs | 6 +- services/brig/test/unit/Test/Brig/Calling.hs | 206 ++++++++++++++++++ .../test/unit/Test/Brig/Calling/Internal.hs | 48 ++++ stack.yaml | 5 +- stack.yaml.lock | 73 +------ 39 files changed, 806 insertions(+), 384 deletions(-) rename libs/brig-types/src/Brig/Types/{TURN.hs => Calling.hs} (96%) rename libs/{federation-util => dns-util}/LICENSE (100%) rename libs/{federation-util/federation-util.cabal => dns-util/dns-util.cabal} (69%) rename libs/{federation-util => dns-util}/package.yaml (53%) rename services/brig/src/Brig/TURN.hs => libs/dns-util/src/Wire/Network/DNS/Effect.hs (59%) rename libs/{federation-util/src/Network/Federation/Util/Internal.hs => dns-util/src/Wire/Network/DNS/SRV.hs} (76%) rename libs/{federation-util => dns-util}/test/Spec.hs (100%) rename libs/{federation-util/test/Test/DNSSpec.hs => dns-util/test/Test/Wire/Network/DNS/SRVSpec.hs} (60%) delete mode 100644 libs/federation-util/.ghcid delete mode 100644 libs/federation-util/src/Network/Federation/Util.hs delete mode 100644 libs/federation-util/src/Network/Federation/Util/DNS.hs rename libs/wire-api/src/Wire/API/Call/{TURN.hs => Config.hs} (85%) rename libs/wire-api/test/unit/Test/Wire/API/Call/{TURN.hs => Config.hs} (95%) create mode 100644 services/brig/src/Brig/Calling.hs rename services/brig/src/Brig/{TURN => Calling}/API.hs (73%) create mode 100644 services/brig/src/Brig/Calling/Internal.hs create mode 100644 services/brig/src/Brig/PolyLog.hs rename services/brig/test/integration/API/{TURN.hs => Calling.hs} (75%) create mode 100644 services/brig/test/unit/Test/Brig/Calling.hs create mode 100644 services/brig/test/unit/Test/Brig/Calling/Internal.hs diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index 53bcdd0e1fd..d467905e717 100644 --- a/libs/brig-types/brig-types.cabal +++ b/libs/brig-types/brig-types.cabal @@ -1,10 +1,10 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.2. +-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack -- --- hash: 7497d04521f12339e2a8f5537dacf242839dea9034f69e67f0a254b1548cadd9 +-- hash: fe16e52e870cb548573366fe9192319004e52c3e4d2ece172df69e408cbe391b name: brig-types version: 1.35.0 @@ -21,6 +21,7 @@ library exposed-modules: Brig.Types Brig.Types.Activation + Brig.Types.Calling Brig.Types.Client Brig.Types.Client.Prekey Brig.Types.Code @@ -37,7 +38,6 @@ library Brig.Types.Team.Invitation Brig.Types.Team.LegalHold Brig.Types.Test.Arbitrary - Brig.Types.TURN Brig.Types.User Brig.Types.User.Auth other-modules: diff --git a/libs/brig-types/src/Brig/Types.hs b/libs/brig-types/src/Brig/Types.hs index b4f3307ba91..fb1cff375a9 100644 --- a/libs/brig-types/src/Brig/Types.hs +++ b/libs/brig-types/src/Brig/Types.hs @@ -21,10 +21,10 @@ module Brig.Types where import Brig.Types.Activation as M +import Brig.Types.Calling as M import Brig.Types.Client as M import Brig.Types.Connection as M import Brig.Types.Properties as M import Brig.Types.Search as M -import Brig.Types.TURN as M import Brig.Types.Team as M import Brig.Types.User as M diff --git a/libs/brig-types/src/Brig/Types/TURN.hs b/libs/brig-types/src/Brig/Types/Calling.hs similarity index 96% rename from libs/brig-types/src/Brig/Types/TURN.hs rename to libs/brig-types/src/Brig/Types/Calling.hs index 0244e753405..2d7ee7ecfc1 100644 --- a/libs/brig-types/src/Brig/Types/TURN.hs +++ b/libs/brig-types/src/Brig/Types/Calling.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.Types.TURN +module Brig.Types.Calling ( -- * re-exports RTCConfiguration, rtcConfiguration, @@ -50,4 +50,4 @@ module Brig.Types.TURN ) where -import Wire.API.Call.TURN +import Wire.API.Call.Config diff --git a/libs/federation-util/LICENSE b/libs/dns-util/LICENSE similarity index 100% rename from libs/federation-util/LICENSE rename to libs/dns-util/LICENSE diff --git a/libs/federation-util/federation-util.cabal b/libs/dns-util/dns-util.cabal similarity index 69% rename from libs/federation-util/federation-util.cabal rename to libs/dns-util/dns-util.cabal index 8f9d28983e7..5f48111b480 100644 --- a/libs/federation-util/federation-util.cabal +++ b/libs/dns-util/dns-util.cabal @@ -1,16 +1,16 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.2. +-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack -- --- hash: d327ef72460d5f79332d80fa4a70516b7351472f50c17a8c491d28c65ec0f024 +-- hash: 82f20a0525faea5f899c0a68fdc8a82623913d8b063f34a6cd4c6e32aa6acf54 -name: federation-util +name: dns-util version: 0.1.0 -synopsis: Various helpers for federation -description: Small helper functions useful when federating. -category: Web +synopsis: Library to deal with DNS SRV records +description: Library to deal with DNS SRV records +category: Network author: Wire Swiss GmbH maintainer: Wire Swiss GmbH copyright: (c) 2020 Wire Swiss GmbH @@ -20,66 +20,44 @@ build-type: Simple library exposed-modules: - Network.Federation.Util - Network.Federation.Util.DNS - Network.Federation.Util.Internal + Wire.Network.DNS.Effect + Wire.Network.DNS.SRV other-modules: - Paths_federation_util + Paths_dns_util hs-source-dirs: src default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DerivingStrategies DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings PackageImports PatternSynonyms PolyKinds QuasiQuotes RankNTypes ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeFamilyDependencies TypeOperators UndecidableInstances ViewPatterns ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path build-depends: - async >=2.0 - , base >=4.6 && <5.0 - , bytestring >=0.10 - , bytestring-conversion >=0.3 - , containers >=0.5 + base >=4.6 && <5.0 , dns - , errors >=2.0 - , exceptions >=0.6 - , http-types >=0.8 , imports + , polysemy , random - , stm - , streaming-commons >=0.1 - , string-conversions , text >=0.11 - , tinylog >=0.8 - , transformers >=0.3 default-language: Haskell2010 test-suite spec type: exitcode-stdio-1.0 main-is: Spec.hs other-modules: - Test.DNSSpec - Paths_federation_util + Test.Wire.Network.DNS.SRVSpec + Paths_dns_util hs-source-dirs: test default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DerivingStrategies DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings PackageImports PatternSynonyms PolyKinds QuasiQuotes RankNTypes ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeFamilyDependencies TypeOperators UndecidableInstances ViewPatterns ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N + build-tool-depends: + hspec-discover:hspec-discover build-depends: QuickCheck - , async >=2.0 , base >=4.6 && <5.0 - , bytestring >=0.10 - , bytestring-conversion >=0.3 - , containers >=0.5 , dns - , errors >=2.0 - , exceptions >=0.6 - , federation-util + , dns-util , hspec , hspec-discover - , http-types >=0.8 , imports + , polysemy , random - , stm - , streaming-commons >=0.1 - , string-conversions , text >=0.11 - , tinylog >=0.8 - , transformers >=0.3 - , uri-bytestring default-language: Haskell2010 diff --git a/libs/federation-util/package.yaml b/libs/dns-util/package.yaml similarity index 53% rename from libs/federation-util/package.yaml rename to libs/dns-util/package.yaml index ea89acc6bc6..210a4299ab6 100644 --- a/libs/federation-util/package.yaml +++ b/libs/dns-util/package.yaml @@ -1,32 +1,21 @@ defaults: local: ../../package-defaults.yaml -name: federation-util +name: dns-util version: '0.1.0' -synopsis: Various helpers for federation -description: Small helper functions useful when federating. -category: Web +synopsis: Library to deal with DNS SRV records +description: Library to deal with DNS SRV records +category: Network author: Wire Swiss GmbH maintainer: Wire Swiss GmbH copyright: (c) 2020 Wire Swiss GmbH license: AGPL-3 dependencies: -- async >=2.0 - base >=4.6 && <5.0 -- bytestring >=0.10 -- bytestring-conversion >=0.3 -- containers >=0.5 -- errors >=2.0 -- exceptions >=0.6 -- http-types >=0.8 -- imports - dns - random -- streaming-commons >=0.1 -- string-conversions -- stm - text >=0.11 -- transformers >=0.3 -- tinylog >=0.8 +- imports +- polysemy library: source-dirs: src @@ -36,10 +25,10 @@ tests: source-dirs: - test ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-tools: + - hspec-discover:hspec-discover dependencies: - hspec - hspec-discover - QuickCheck - - federation-util - - uri-bytestring - + - dns-util diff --git a/services/brig/src/Brig/TURN.hs b/libs/dns-util/src/Wire/Network/DNS/Effect.hs similarity index 59% rename from services/brig/src/Brig/TURN.hs rename to libs/dns-util/src/Wire/Network/DNS/Effect.hs index 0c95068938e..c70cdafa2a5 100644 --- a/services/brig/src/Brig/TURN.hs +++ b/libs/dns-util/src/Wire/Network/DNS/Effect.hs @@ -15,25 +15,22 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.TURN where +module Wire.Network.DNS.Effect where -import Brig.Types (TurnURI) -import Control.Lens -import Data.List1 import Imports -import OpenSSL.EVP.Digest (Digest) -import System.Random.MWC (GenIO, createSystemRandom) +import Network.DNS (Domain) +import qualified Network.DNS as DNS +import Polysemy +import Wire.Network.DNS.SRV -data Env = Env - { _turnServers :: List1 TurnURI, - _turnTokenTTL :: Word32, - _turnConfigTTL :: Word32, - _turnSecret :: ByteString, - _turnSHA512 :: Digest, - _turnPrng :: GenIO - } +data DNSLookup m a where + LookupSRV :: Domain -> DNSLookup m SrvResponse -makeLenses ''Env +makeSem ''DNSLookup -newEnv :: Digest -> List1 TurnURI -> Word32 -> Word32 -> ByteString -> IO Env -newEnv sha512 srvs tTTL cTTL secret = Env srvs tTTL cTTL secret sha512 <$> createSystemRandom +runDNSLookupDefault :: Member (Embed IO) r => Sem (DNSLookup ': r) a -> Sem r a +runDNSLookupDefault = + interpret $ \(LookupSRV domain) -> embed $ do + rs <- DNS.makeResolvSeed DNS.defaultResolvConf + DNS.withResolver rs $ \resolver -> + interpretResponse <$> DNS.lookupSRV resolver domain diff --git a/libs/federation-util/src/Network/Federation/Util/Internal.hs b/libs/dns-util/src/Wire/Network/DNS/SRV.hs similarity index 76% rename from libs/federation-util/src/Network/Federation/Util/Internal.hs rename to libs/dns-util/src/Wire/Network/DNS/SRV.hs index 26d2160175b..27105010929 100644 --- a/libs/federation-util/src/Network/Federation/Util/Internal.hs +++ b/libs/dns-util/src/Wire/Network/DNS/SRV.hs @@ -15,7 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . --- Parts of this code, namely functions srvLookup'' and orderSrvResult, +-- Parts of this code, namely functions interpretResponse and orderSrvResult, -- which were taken from http://hackage.haskell.org/package/pontarius-xmpp -- are also licensed under the three-clause BSD license: -- @@ -55,12 +55,12 @@ -- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -- OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -module Network.Federation.Util.Internal where +module Wire.Network.DNS.SRV where import Control.Category ((>>>)) -import Data.Text.Encoding (encodeUtf8) +import Data.List.NonEmpty (NonEmpty (..)) import Imports -import Network.DNS (DNSError, Domain, ResolvSeed, Resolver, lookupSRV, withResolver) +import Network.DNS (DNSError, Domain) import System.Random (randomRIO) data SrvEntry = SrvEntry @@ -78,37 +78,21 @@ data SrvTarget = SrvTarget } deriving (Eq, Show) -toSrvEntry :: (Word16, Word16, Word16, Domain) -> SrvEntry -toSrvEntry (prio, weight, port, domain) = SrvEntry prio weight (SrvTarget domain port) +data SrvResponse + = SrvNotAvailable + | SrvAvailable (NonEmpty SrvEntry) + | SrvResponseError DNSError + deriving (Eq, Show) --- Given a prefix (e.g. _wire-server) and a domain (e.g. wire.com), --- provides a list of A(AAA) names and port numbers upon a successful --- DNS-SRV request, or `Nothing' if the DNS-SRV request failed. --- Modified version inspired from http://hackage.haskell.org/package/pontarius-xmpp -srvLookup' :: Text -> Text -> ResolvSeed -> IO (Maybe [SrvTarget]) -srvLookup' = srvLookup'' lookupSRV +interpretResponse :: Either DNSError [(Word16, Word16, Word16, Domain)] -> SrvResponse +interpretResponse = \case + Left err -> SrvResponseError err + Right [] -> SrvNotAvailable + Right [(_, _, _, ".")] -> SrvNotAvailable -- According to RFC2782 + Right (r : rs) -> SrvAvailable $ fmap toSrvEntry (r :| rs) --- internal version for testing --- --- FUTUREWORK: return more precise errors than 'Nothing'? -srvLookup'' :: - (Resolver -> Domain -> IO (Either DNSError [(Word16, Word16, Word16, Domain)])) -> - Text -> - Text -> - ResolvSeed -> - IO (Maybe [SrvTarget]) -srvLookup'' lookupF prefix realm resolvSeed = withResolver resolvSeed $ \resolver -> do - srvResult <- lookupF resolver $ encodeUtf8 $ prefix <> "._tcp." <> realm <> "." - case srvResult of - -- The service is not available at this domain. - Left _ -> return Nothing - Right [] -> return Nothing - Right [(_, _, _, ".")] -> return Nothing -- "not available" as in RFC2782 - Right srvResult' -> do - let srvEntries = toSrvEntry <$> srvResult' - -- Get [(Domain, PortNumber)] of SRV request, if any. - -- Sorts the records based on the priority value. - Just . fmap srvTarget <$> orderSrvResult srvEntries +toSrvEntry :: (Word16, Word16, Word16, Domain) -> SrvEntry +toSrvEntry (prio, weight, port, domain) = SrvEntry prio weight (SrvTarget domain port) -- FUTUREWORK: maybe improve sorting algorithm here? (with respect to performance and code style) -- @@ -121,7 +105,7 @@ srvLookup'' lookupF prefix realm resolvSeed = withResolver resolvSeed $ \resolve orderSrvResult :: [SrvEntry] -> IO [SrvEntry] orderSrvResult = -- Order the result set by priority. - sortBy (comparing srvPriority) + sortOn srvPriority -- Group elements in sublists based on their priority. -- The result type is `[[(Word16, Word16, Word16, Domain)]]' (nested list). >>> groupBy ((==) `on` srvPriority) @@ -148,7 +132,7 @@ orderSrvResult = (b, (c : e)) -> (b, c, e) _ -> error "orderSrvResult: no record with running sum greater than random number" -- Remove the running total number from the remaining elements. - let remainingSrvs = map (\(srv, _) -> srv) (concat [beginning, end]) + let remainingSrvs = map fst (concat [beginning, end]) -- Repeat the ordering procedure on the remaining elements. rest <- orderSublist remainingSrvs return $ firstSrv : rest diff --git a/libs/federation-util/test/Spec.hs b/libs/dns-util/test/Spec.hs similarity index 100% rename from libs/federation-util/test/Spec.hs rename to libs/dns-util/test/Spec.hs diff --git a/libs/federation-util/test/Test/DNSSpec.hs b/libs/dns-util/test/Test/Wire/Network/DNS/SRVSpec.hs similarity index 60% rename from libs/federation-util/test/Test/DNSSpec.hs rename to libs/dns-util/test/Test/Wire/Network/DNS/SRVSpec.hs index f559f4a0dda..39c67a1ee2b 100644 --- a/libs/federation-util/test/Test/DNSSpec.hs +++ b/libs/dns-util/test/Test/Wire/Network/DNS/SRVSpec.hs @@ -15,16 +15,38 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Test.DNSSpec where +module Test.Wire.Network.DNS.SRVSpec where +import Data.List.NonEmpty (NonEmpty (..)) import Imports -import Network.DNS -import Network.Federation.Util.Internal +import qualified Network.DNS as DNS import Test.Hspec +import Wire.Network.DNS.SRV spec :: Spec spec = do - describe "order" $ do + describe "interpretResponse" $ do + it "should interpret error correctly" $ + interpretResponse (Left DNS.UnknownDNSError) `shouldBe` SrvResponseError DNS.UnknownDNSError + + it "should interpret empty response as SrvNotAvailable" $ + interpretResponse (Right []) `shouldBe` SrvNotAvailable + + it "should interpret explicitly not available response as SrvNotAvailable" $ + interpretResponse (Right [(0, 0, 0, ".")]) `shouldBe` SrvNotAvailable + + it "should interpret an available service correctly" $ do + let input = + [ (0, 1, 443, "service01.example.com."), + (10, 20, 8443, "service02.example.com.") + ] + let expectedOutput = + SrvAvailable + ( SrvEntry 0 1 (SrvTarget "service01.example.com." 443) + :| [SrvEntry 10 20 (SrvTarget "service02.example.com." 8443)] + ) + interpretResponse (Right input) `shouldBe` expectedOutput + describe "orderSrvResult" $ do it "orders records according to ascending priority" $ do actual <- orderSrvResult . map toSrvEntry $ @@ -59,29 +81,3 @@ spec = do length x `shouldSatisfy` (< 49) length y `shouldSatisfy` (> 0) length y `shouldSatisfy` (< 49) - describe "srvLookup" $ do - it "returns the expected result for wire.com" $ do - rs <- makeResolvSeed defaultResolvConf - wire <- srvLookup'' mockLookupSRV "_wire-server" "wire.com" rs - wire `shouldBe` Just [SrvTarget "wire.com" 443] - it "filters out single '.' results" $ do - rs <- makeResolvSeed defaultResolvConf - exampleDotCom <- srvLookup'' mockLookupSRV "_wire-server" "example.com" rs - exampleDotCom `shouldBe` Nothing - it "can return multiple results" $ do - rs <- makeResolvSeed defaultResolvConf - zinfra <- srvLookup'' mockLookupSRV "_wire-server" "zinfra.io" rs - (length <$> zinfra) `shouldBe` Just 2 - it "returns Nothing if there is no DNS record" $ do - rs <- makeResolvSeed defaultResolvConf - noRecord <- srvLookup'' mockLookupSRV "_wire-server" "no-record-here" rs - noRecord `shouldBe` Nothing - --- mock function matching Network.DNS's 'lookupSRV' types -mockLookupSRV :: Resolver -> Domain -> IO (Either DNSError [(Word16, Word16, Word16, Domain)]) -mockLookupSRV _ domain = do - case domain of - "_wire-server._tcp.wire.com." -> return $ Right [(0, 0, 443, "wire.com")] - "_wire-server._tcp.zinfra.io." -> return $ Right [(0, 0, 443, "server1.zinfra.io"), (0, 0, 443, "server2.zinfra.io")] - "_wire-server._tcp.example.com." -> return $ Right [(0, 0, 443, ".")] - _ -> return $ Right [] diff --git a/libs/federation-util/.ghcid b/libs/federation-util/.ghcid deleted file mode 100644 index fdd66810c9b..00000000000 --- a/libs/federation-util/.ghcid +++ /dev/null @@ -1 +0,0 @@ ---command "stack ghci federation-util --test" diff --git a/libs/federation-util/src/Network/Federation/Util.hs b/libs/federation-util/src/Network/Federation/Util.hs deleted file mode 100644 index bd9a191b618..00000000000 --- a/libs/federation-util/src/Network/Federation/Util.hs +++ /dev/null @@ -1,23 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2020 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Network.Federation.Util - ( module Network.Federation.Util.DNS, - ) -where - -import Network.Federation.Util.DNS diff --git a/libs/federation-util/src/Network/Federation/Util/DNS.hs b/libs/federation-util/src/Network/Federation/Util/DNS.hs deleted file mode 100644 index 14ac4db02b4..00000000000 --- a/libs/federation-util/src/Network/Federation/Util/DNS.hs +++ /dev/null @@ -1,45 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2020 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Network.Federation.Util.DNS - ( srvLookup, - SrvTarget (..), - ) -where - -import Imports -import Network.DNS -import Network.Federation.Util.Internal - --- | Looks up a SRV record given a domain, returning A(AAA) records with their --- ports (ordered by priority and weight according to RFC 2782). Connection --- attempts should be made to the returned result list in order. --- --- Example: --- --- > import Network.DNS.Resolver --- > import Network.Federation.Util --- > --- > main :: IO () --- > main = do --- > rs <- makeResolvSeed defaultResolvConf --- > x <- srvLookup "staging.zinfra.io" rs -srvLookup :: Text -> ResolvSeed -> IO (Maybe [SrvTarget]) -srvLookup = srvLookup' srvDefaultPrefix - -srvDefaultPrefix :: Text -srvDefaultPrefix = "_wire-server" diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index b2fe9456421..2e3301fff8e 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -42,6 +42,7 @@ module Data.Misc -- * HttpsUrl HttpsUrl (..), mkHttpsUrl, + ensureHttpsUrl, -- * Fingerprint Fingerprint (..), @@ -60,7 +61,7 @@ module Data.Misc where import Cassandra -import Control.Lens ((^.), makeLenses) +import Control.Lens ((.~), (^.), makeLenses) import Data.Aeson import qualified Data.Aeson.Types as Json import qualified Data.Attoparsec.ByteString.Char8 as Chars @@ -236,7 +237,7 @@ instance Cql Milliseconds where newtype HttpsUrl = HttpsUrl { httpsUrl :: URIRef Absolute } - deriving stock (Eq, Generic) + deriving stock (Eq, Ord, Generic) mkHttpsUrl :: URIRef Absolute -> Either String HttpsUrl mkHttpsUrl uri = @@ -244,6 +245,9 @@ mkHttpsUrl uri = then Right $ HttpsUrl uri else Left $ "Non-HTTPS URL: " ++ show uri +ensureHttpsUrl :: URIRef Absolute -> HttpsUrl +ensureHttpsUrl = HttpsUrl . (uriSchemeL . schemeBSL .~ "https") + instance Show HttpsUrl where showsPrec i = showsPrec i . httpsUrl diff --git a/libs/wire-api/src/Wire/API/Call/TURN.hs b/libs/wire-api/src/Wire/API/Call/Config.hs similarity index 85% rename from libs/wire-api/src/Wire/API/Call/TURN.hs rename to libs/wire-api/src/Wire/API/Call/Config.hs index f49be9689c9..6839bcd9110 100644 --- a/libs/wire-api/src/Wire/API/Call/TURN.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -20,11 +20,12 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.Call.TURN +module Wire.API.Call.Config ( -- * RTCConfiguration RTCConfiguration, rtcConfiguration, rtcConfIceServers, + rtcConfSftServers, rtcConfTTL, -- * RTCIceServer @@ -55,6 +56,11 @@ module Wire.API.Call.TURN tuT, tuRandom, + -- * SFTServer + SFTServer, + sftServer, + sftURL, + -- * convenience isUdp, isTcp, @@ -74,8 +80,8 @@ import Data.Attoparsec.Text hiding (parse) import Data.ByteString.Builder import qualified Data.ByteString.Conversion as BC import qualified Data.IP as IP -import Data.List1 -import Data.Misc (IpAddr (IpAddr), Port (..)) +import Data.List.NonEmpty (NonEmpty) +import Data.Misc (HttpsUrl (..), IpAddr (IpAddr), Port (..)) import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text as Text import Data.Text.Ascii @@ -93,16 +99,18 @@ import Wire.API.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -- | A configuration object resembling \"RTCConfiguration\" -- -- The \"ttl\" field is a proprietary extension +-- The \"sft_servers\" field is a proprietary extension -- -- cf. https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#RTCConfiguration_dictionary data RTCConfiguration = RTCConfiguration - { _rtcConfIceServers :: List1 RTCIceServer, + { _rtcConfIceServers :: NonEmpty RTCIceServer, + _rtcConfSftServers :: Maybe (NonEmpty SFTServer), _rtcConfTTL :: Word32 } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform RTCConfiguration) -rtcConfiguration :: List1 RTCIceServer -> Word32 -> RTCConfiguration +rtcConfiguration :: NonEmpty RTCIceServer -> Maybe (NonEmpty SFTServer) -> Word32 -> RTCConfiguration rtcConfiguration = RTCConfiguration modelRtcConfiguration :: Doc.Model @@ -110,19 +118,52 @@ modelRtcConfiguration = Doc.defineModel "RTCConfiguration" $ do Doc.description "A subset of the WebRTC 'RTCConfiguration' dictionary" Doc.property "ice_servers" (Doc.array (Doc.ref modelRtcIceServer)) $ Doc.description "Array of 'RTCIceServer' objects" + Doc.property "sft_servers" (Doc.array (Doc.ref modelRtcSftServer)) $ + Doc.description "Array of 'SFTServer' objects (optional)" Doc.property "ttl" Doc.int32' $ Doc.description "Number of seconds after which the configuration should be refreshed (advisory)" instance ToJSON RTCConfiguration where - toJSON (RTCConfiguration srvs ttl) = + toJSON (RTCConfiguration srvs sfts ttl) = object [ "ice_servers" .= srvs, + "sft_servers" .= sfts, "ttl" .= ttl ] instance FromJSON RTCConfiguration where parseJSON = withObject "RTCConfiguration" $ \o -> - RTCConfiguration <$> o .: "ice_servers" <*> o .: "ttl" + RTCConfiguration <$> o .: "ice_servers" <*> o .: "sft_servers" <*> o .: "ttl" + +-------------------------------------------------------------------------------- +-- SFTServer + +newtype SFTServer = SFTServer + { _sftURL :: HttpsUrl + } + deriving stock (Eq, Show, Ord, Generic) + deriving (Arbitrary) via (GenericUniform SFTServer) + +instance ToJSON SFTServer where + toJSON (SFTServer url) = + object + [ "urls" .= [url] + ] + +instance FromJSON SFTServer where + parseJSON = withObject "SFTServer" $ \o -> + o .: "urls" >>= \case + [url] -> pure $ SFTServer url + xs -> fail $ "SFTServer can only have exactly one URL, found " <> show (length xs) + +sftServer :: HttpsUrl -> SFTServer +sftServer = SFTServer + +modelRtcSftServer :: Doc.Model +modelRtcSftServer = Doc.defineModel "RTC SFT Server" $ do + Doc.description "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers" + Doc.property "urls" (Doc.array Doc.string') $ + Doc.description "Array containing exactly one SFT server address of the form 'https://:'" -------------------------------------------------------------------------------- -- RTCIceServer @@ -131,14 +172,14 @@ instance FromJSON RTCConfiguration where -- -- cf. https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer data RTCIceServer = RTCIceServer - { _iceURLs :: List1 TurnURI, + { _iceURLs :: NonEmpty TurnURI, _iceUsername :: TurnUsername, _iceCredential :: AsciiBase64 } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform RTCIceServer) -rtcIceServer :: List1 TurnURI -> TurnUsername -> AsciiBase64 -> RTCIceServer +rtcIceServer :: NonEmpty TurnURI -> TurnUsername -> AsciiBase64 -> RTCIceServer rtcIceServer = RTCIceServer modelRtcIceServer :: Doc.Model @@ -207,7 +248,7 @@ parseTurnURI = parseOnly (parser <* endOfInput) <$> ((takeWhile1 (/= ':') <* char ':' >>= parseScheme) "parsingScheme") <*> ((takeWhile1 (/= ':') <* char ':' >>= parseHost) "parsingHost") <*> (decimal "parsingPort") - <*> ((optional ((string "?transport=" *> takeText) >>= parseTransport)) "parsingTransport") + <*> (optional ((string "?transport=" *> takeText) >>= parseTransport) "parsingTransport") parseScheme = parse "parseScheme" parseHost = parse "parseHost" parseTransport = parse "parseTransport" @@ -421,21 +462,22 @@ limitServers uris limit = limitServers' [] limit uris isUdp :: TurnURI -> Bool isUdp uri = _turiScheme uri == SchemeTurn - && ( _turiTransport uri == Just (TransportUDP) - || _turiTransport uri == Nothing + && ( _turiTransport uri == Just TransportUDP + || isNothing (_turiTransport uri) ) isTcp :: TurnURI -> Bool isTcp uri = _turiScheme uri == SchemeTurn - && _turiTransport uri == Just (TransportTCP) + && _turiTransport uri == Just TransportTCP isTls :: TurnURI -> Bool isTls uri = _turiScheme uri == SchemeTurns - && _turiTransport uri == Just (TransportTCP) + && _turiTransport uri == Just TransportTCP makeLenses ''RTCConfiguration makeLenses ''RTCIceServer makeLenses ''TurnURI makeLenses ''TurnUsername +makeLenses ''SFTServer diff --git a/libs/wire-api/src/Wire/API/Swagger.hs b/libs/wire-api/src/Wire/API/Swagger.hs index 3feada1b9db..32e0f205732 100644 --- a/libs/wire-api/src/Wire/API/Swagger.hs +++ b/libs/wire-api/src/Wire/API/Swagger.hs @@ -18,7 +18,7 @@ module Wire.API.Swagger where import Data.Swagger.Build.Api (Model) -import qualified Wire.API.Call.TURN as Call.TURN +import qualified Wire.API.Call.Config as Call.Config import qualified Wire.API.Connection as Connection import qualified Wire.API.Conversation as Conversation import qualified Wire.API.Conversation.Code as Conversation.Code @@ -53,8 +53,8 @@ import qualified Wire.API.User.Search as User.Search models :: [Model] models = - [ Call.TURN.modelRtcConfiguration, - Call.TURN.modelRtcIceServer, + [ Call.Config.modelRtcConfiguration, + Call.Config.modelRtcIceServer, Connection.modelConnectionList, Connection.modelConnection, Connection.modelConnectionRequest, diff --git a/libs/wire-api/test/unit/Main.hs b/libs/wire-api/test/unit/Main.hs index 53893a18af8..15bfb7b4178 100644 --- a/libs/wire-api/test/unit/Main.hs +++ b/libs/wire-api/test/unit/Main.hs @@ -22,7 +22,7 @@ where import Imports import Test.Tasty -import qualified Test.Wire.API.Call.TURN as Call.TURN +import qualified Test.Wire.API.Call.Config as Call.Config import qualified Test.Wire.API.Roundtrip.Aeson as Roundtrip.Aeson import qualified Test.Wire.API.Roundtrip.ByteString as Roundtrip.ByteString import qualified Test.Wire.API.Team.Member as Team.Member @@ -34,7 +34,7 @@ main = defaultMain $ testGroup "Tests" - [ Call.TURN.tests, + [ Call.Config.tests, Team.Member.tests, User.tests, User.RichInfo.tests, diff --git a/libs/wire-api/test/unit/Test/Wire/API/Call/TURN.hs b/libs/wire-api/test/unit/Test/Wire/API/Call/Config.hs similarity index 95% rename from libs/wire-api/test/unit/Test/Wire/API/Call/TURN.hs rename to libs/wire-api/test/unit/Test/Wire/API/Call/Config.hs index 84c13902572..b9885328a63 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Call/TURN.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Call/Config.hs @@ -15,14 +15,14 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Test.Wire.API.Call.TURN where +module Test.Wire.API.Call.Config where import Data.Aeson import Imports import Test.Tasty import Test.Tasty.QuickCheck hiding (total) import Wire.API.Arbitrary () -import Wire.API.Call.TURN (TurnURI, isTcp, isTls, isUdp, limitServers) +import Wire.API.Call.Config (TurnURI, isTcp, isTls, isUdp, limitServers) tests :: TestTree tests = diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index b4174ca0478..e5ccb7e8b76 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs @@ -27,7 +27,7 @@ import Test.Tasty.QuickCheck ((===), Arbitrary, counterexample, testProperty) import Type.Reflection (typeRep) import qualified Wire.API.Asset as Asset import qualified Wire.API.Asset.V3.Resumable as Asset.Resumable -import qualified Wire.API.Call.TURN as Call.TURN +import qualified Wire.API.Call.Config as Call.Config import qualified Wire.API.Connection as Connection import qualified Wire.API.Conversation as Conversation import qualified Wire.API.Conversation.Bot as Conversation.Bot @@ -85,13 +85,14 @@ tests = testRoundTrip @Asset.Resumable.ChunkSize, testRoundTrip @Asset.Resumable.Offset, currentlyFailing (testRoundTrip @Asset.Resumable.ResumableAsset), -- because ToJSON is rounding UTCTime - testRoundTrip @Call.TURN.TurnHost, - testRoundTrip @Call.TURN.Scheme, - testRoundTrip @Call.TURN.Transport, - testRoundTrip @Call.TURN.TurnURI, - testRoundTrip @Call.TURN.TurnUsername, - testRoundTrip @Call.TURN.RTCIceServer, - testRoundTrip @Call.TURN.RTCConfiguration, + testRoundTrip @Call.Config.TurnHost, + testRoundTrip @Call.Config.Scheme, + testRoundTrip @Call.Config.Transport, + testRoundTrip @Call.Config.TurnURI, + testRoundTrip @Call.Config.TurnUsername, + testRoundTrip @Call.Config.RTCIceServer, + testRoundTrip @Call.Config.RTCConfiguration, + testRoundTrip @Call.Config.SFTServer, testRoundTrip @Connection.ConnectionRequest, testRoundTrip @Connection.Relation, testRoundTrip @Connection.Message, diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs index 579ecdeb5d1..00dfe9263c3 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs @@ -25,7 +25,7 @@ import Type.Reflection (typeRep) import qualified Wire.API.Arbitrary as Arbitrary () import qualified Wire.API.Asset.V3 as Asset.V3 import qualified Wire.API.Asset.V3.Resumable as Asset.V3.Resumable -import qualified Wire.API.Call.TURN as Call.TURN +import qualified Wire.API.Call.Config as Call.Config import qualified Wire.API.Conversation.Code as Conversation.Code import qualified Wire.API.Conversation.Role as Conversation.Role import qualified Wire.API.Properties as Properties @@ -50,10 +50,10 @@ tests = testRoundTrip @Asset.V3.Resumable.ChunkSize, testRoundTrip @Asset.V3.Resumable.Offset, testRoundTrip @Asset.V3.Resumable.TotalSize, - testRoundTrip @Call.TURN.Scheme, - testRoundTrip @Call.TURN.Transport, - testRoundTrip @Call.TURN.TurnHost, - testRoundTrip @Call.TURN.TurnURI, + testRoundTrip @Call.Config.Scheme, + testRoundTrip @Call.Config.Transport, + testRoundTrip @Call.Config.TurnHost, + testRoundTrip @Call.Config.TurnURI, testRoundTrip @Conversation.Code.Key, testRoundTrip @Conversation.Code.Value, testRoundTrip @Conversation.Role.RoleName, @@ -77,7 +77,7 @@ tests = testRoundTrip @(Provider.Service.Tag.QueryAllTags 3 5), testRoundTrip @(Provider.Service.Tag.QueryAnyTags 3 5) -- FUTUREWORK: - -- testCase "Call.TURN.TurnUsername (doesn't have FromByteString)" ... + -- testCase "Call.Config.TurnUsername (doesn't have FromByteString)" ... -- testCase "User.Activation.ActivationTarget (doesn't have FromByteString)" ... ] diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index f084cc5bf6e..5509101fba5 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -23,7 +23,7 @@ library Wire.API.Asset Wire.API.Asset.V3 Wire.API.Asset.V3.Resumable - Wire.API.Call.TURN + Wire.API.Call.Config Wire.API.Connection Wire.API.Conversation Wire.API.Conversation.Bot @@ -121,7 +121,7 @@ test-suite wire-api-tests type: exitcode-stdio-1.0 main-is: Main.hs other-modules: - Test.Wire.API.Call.TURN + Test.Wire.API.Call.Config Test.Wire.API.Roundtrip.Aeson Test.Wire.API.Roundtrip.ByteString Test.Wire.API.Team.Member diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 9fe0cc99d48..11f5b3a6223 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -4,7 +4,7 @@ cabal-version: 2.0 -- -- see: https://github.com/sol/hpack -- --- hash: f150b19d31eda4f6f32ad2782dccc7deae05b01ffd46558fe1f403ba49500307 +-- hash: 49345fd7c41cab0441ba95bd4fdd01b1f64b6053e348ec991695254be96ed5d4 name: brig version: 1.35.0 @@ -36,6 +36,9 @@ library Brig.AWS.SesNotification Brig.AWS.Types Brig.Budget + Brig.Calling + Brig.Calling.API + Brig.Calling.Internal Brig.Code Brig.Data.Activation Brig.Data.Blacklist @@ -59,6 +62,7 @@ library Brig.Options Brig.Password Brig.Phone + Brig.PolyLog Brig.Provider.API Brig.Provider.DB Brig.Provider.Email @@ -76,8 +80,6 @@ library Brig.Team.Template Brig.Team.Util Brig.Template - Brig.TURN - Brig.TURN.API Brig.Unique Brig.User.API.Auth Brig.User.API.Search @@ -137,6 +139,8 @@ library , data-default >=0.5 , data-timeout >=0.3 , directory >=1.2 + , dns + , dns-util , either >=4.3 , enclosed-exceptions >=1.0 , errors >=1.4 @@ -172,6 +176,7 @@ library , network-uri >=2.6 , optparse-applicative >=0.11 , pem >=0.2 + , polysemy , prometheus-client , proto-lens >=0.1 , random-shuffle >=0.0.3 @@ -289,6 +294,7 @@ executable brig-index executable brig-integration main-is: Main.hs other-modules: + API.Calling API.IdMapping API.Metrics API.Provider @@ -298,7 +304,6 @@ executable brig-integration API.Settings API.Team API.Team.Util - API.TURN API.User API.User.Account API.User.Auth @@ -462,6 +467,8 @@ test-suite brig-tests type: exitcode-stdio-1.0 main-is: Main.hs other-modules: + Test.Brig.Calling + Test.Brig.Calling.Internal Test.Brig.User.Search.Index.Types Paths_brig hs-source-dirs: @@ -474,9 +481,17 @@ test-suite brig-tests , bloodhound , brig , brig-types + , dns + , dns-util , imports + , polysemy + , retry , tasty , tasty-hunit + , tinylog , types-common + , unliftio + , uri-bytestring , uuid + , wire-api default-language: Haskell2010 diff --git a/services/brig/package.yaml b/services/brig/package.yaml index 53aee10de4d..6394ec964d7 100644 --- a/services/brig/package.yaml +++ b/services/brig/package.yaml @@ -40,6 +40,8 @@ library: - data-default >=0.5 - data-timeout >=0.3 - directory >=1.2 + - dns + - dns-util - either >=4.3 - enclosed-exceptions >=1.0 - errors >=1.4 @@ -80,6 +82,7 @@ library: - network-uri >=2.6 - optparse-applicative >=0.11 - pem >=0.2 + - polysemy - proto-lens >=0.1 - prometheus-client - resourcet >=1.1 @@ -163,11 +166,19 @@ tests: - bloodhound - brig - brig-types + - dns + - dns-util + - polysemy - imports + - retry - tasty - tasty-hunit + - tinylog - types-common + - unliftio + - uri-bytestring - uuid + - wire-api executables: brig-schema: main: Main.hs diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 071c441b831..809b5b661dd 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -32,10 +32,10 @@ import qualified Brig.API.Properties as API import Brig.API.Types import qualified Brig.API.User as API import Brig.App +import qualified Brig.Calling.API as Calling import qualified Brig.Data.User as Data import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Provider.API as Provider -import qualified Brig.TURN.API as TURN import qualified Brig.Team.API as Team import qualified Brig.Team.Email as Team import Brig.Types.Intra (AccountStatus (Ephemeral), UserAccount (UserAccount, accountUser)) @@ -781,7 +781,7 @@ sitemap o = do Auth.routesPublic Search.routesPublic Team.routesPublic - TURN.routesPublic + Calling.routesPublic apiDocs :: Opts -> Routes Doc.ApiBuilder Handler () apiDocs o = do diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index a711ac4f39a..4980a07850f 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -51,6 +51,7 @@ module Brig.App applog, turnEnv, turnEnvV2, + sftEnv, internalEvents, -- * App Monad @@ -67,13 +68,13 @@ import Bilge (Manager, MonadHttp, RequestId (..), newManager, withResponse) import qualified Bilge as RPC import Bilge.RPC (HasRequestId (..)) import qualified Brig.AWS as AWS +import qualified Brig.Calling as Calling import Brig.Options (Opts, Settings) import qualified Brig.Options as Opt import Brig.Provider.Template import qualified Brig.Queue.Stomp as Stomp import Brig.Queue.Types (Queue (..)) import qualified Brig.SMTP as SMTP -import qualified Brig.TURN as TURN import Brig.Team.Template import Brig.Template (Localised, TemplateBranding, forLocale, genTemplateBranding) import Brig.Types (Locale (..), TurnURI) @@ -155,8 +156,9 @@ data Env = Env _twilioCreds :: Twilio.Credentials, _geoDb :: Maybe (IORef GeoIp.GeoDB), _fsWatcher :: FS.WatchManager, - _turnEnv :: IORef TURN.Env, - _turnEnvV2 :: IORef TURN.Env, + _turnEnv :: IORef Calling.Env, + _turnEnvV2 :: IORef Calling.Env, + _sftEnv :: Maybe Calling.SFTEnv, _currentTime :: IO UTCTime, _zauthEnv :: ZAuth.Env, _digestSHA256 :: Digest, @@ -202,6 +204,7 @@ newEnv o = do eventsQueue <- case Opt.internalEventsQueue (Opt.internalEvents o) of StompQueue q -> pure (StompQueue q) SqsQueue q -> SqsQueue <$> AWS.getQueueUrl (aws ^. AWS.amazonkaEnv) q + mSFTEnv <- mapM Calling.mkSFTEnv $ Opt.sft o return $! Env { _cargohold = mkEndpoint $ Opt.cargohold o, @@ -227,6 +230,7 @@ newEnv o = do _geoDb = g, _turnEnv = turn, _turnEnvV2 = turnV2, + _sftEnv = mSFTEnv, _fsWatcher = w, _currentTime = clock, _zauthEnv = zau, @@ -264,7 +268,7 @@ geoSetup lgr w (Just db) = do startWatching w path (replaceGeoDb lgr geodb) return $ Just geodb -turnSetup :: Logger -> FS.WatchManager -> Digest -> Opt.TurnOpts -> IO (IORef TURN.Env, IORef TURN.Env) +turnSetup :: Logger -> FS.WatchManager -> Digest -> Opt.TurnOpts -> IO (IORef Calling.Env, IORef Calling.Env) turnSetup lgr w dig o = do secret <- Text.encodeUtf8 . Text.strip <$> Text.readFile (Opt.secret o) cfg <- setupTurn secret (Opt.servers o) @@ -274,7 +278,7 @@ turnSetup lgr w dig o = do setupTurn secret cfg = do path <- canonicalizePath cfg servers <- fromMaybe (error "Empty TURN list, check turn file!") <$> readTurnList path - te <- newIORef =<< TURN.newEnv dig servers (Opt.tokenTTL o) (Opt.configTTL o) secret + te <- newIORef =<< Calling.newEnv dig servers (Opt.tokenTTL o) (Opt.configTTL o) secret startWatching w path (replaceTurnServers lgr te) return te @@ -293,13 +297,13 @@ replaceGeoDb g ref e = do GeoIp.openGeoDB (FS.eventPath e) >>= atomicWriteIORef ref Log.info g (msg $ val "New GeoIP database loaded.") -replaceTurnServers :: Logger -> IORef TURN.Env -> FS.Event -> IO () +replaceTurnServers :: Logger -> IORef Calling.Env -> FS.Event -> IO () replaceTurnServers g ref e = do let logErr x = Log.err g (msg $ val "Error loading turn servers: " +++ show x) handleAny logErr $ readTurnList (FS.eventPath e) >>= \case Just servers -> readIORef ref >>= \old -> do - atomicWriteIORef ref (old & TURN.turnServers .~ servers) + atomicWriteIORef ref (old & Calling.turnServers .~ servers) Log.info g (msg $ val "New turn servers loaded.") Nothing -> Log.warn g (msg $ val "Empty or malformed turn servers list, ignoring!") diff --git a/services/brig/src/Brig/Calling.hs b/services/brig/src/Brig/Calling.hs new file mode 100644 index 00000000000..61fdf80e98f --- /dev/null +++ b/services/brig/src/Brig/Calling.hs @@ -0,0 +1,113 @@ +{-# LANGUAGE RecordWildCards #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Calling where + +import Brig.Options (SFTOptions (..), defSftDiscoveryIntervalSeconds, defSftServiceName) +import qualified Brig.Options as Opts +import Brig.PolyLog +import Brig.Types (TurnURI) +import Control.Lens +import Data.List.NonEmpty +import Data.List1 +import Data.Time.Clock (DiffTime, diffTimeToPicoseconds) +import Imports +import qualified Network.DNS as DNS +import OpenSSL.EVP.Digest (Digest) +import Polysemy +import qualified System.Logger as Log +import System.Random.MWC (GenIO, createSystemRandom) +import Wire.Network.DNS.Effect +import Wire.Network.DNS.SRV + +data SFTEnv = SFTEnv + { -- | Starts off as `NotDiscoveredYet`, once it has servers, it should never + -- go back to `NotDiscoveredYet` and continue having stale values if + -- subsequent discovries fail + sftServers :: IORef (Discovery (NonEmpty SrvEntry)), + sftDomain :: DNS.Domain, + -- | Microseconds, as expected by 'threadDelay' + sftDiscoveryInterval :: Int + } + +data Discovery a + = NotDiscoveredYet + | Discovered a + deriving (Show, Eq) + +discoveryToMaybe :: Discovery a -> Maybe a +discoveryToMaybe = \case + NotDiscoveredYet -> Nothing + Discovered x -> Just x + +discoverSFTServers :: Members [DNSLookup, PolyLog] r => DNS.Domain -> Sem r (Maybe (NonEmpty SrvEntry)) +discoverSFTServers domain = + lookupSRV domain >>= \case + SrvAvailable es -> pure $ Just es + SrvNotAvailable -> do + polyLog Log.Warn (Log.msg ("No SFT servers available" :: ByteString)) + pure Nothing + SrvResponseError e -> do + polyLog Log.Error (Log.msg ("DNS Lookup failed for SFT Discovery" :: ByteString) . Log.field "Error" (show e)) + pure Nothing + +mkSFTDomain :: SFTOptions -> DNS.Domain +mkSFTDomain SFTOptions {..} = DNS.normalize $ maybe defSftServiceName ("_" <>) sftSRVServiceName <> "._tcp." <> sftBaseDomain + +-- FUTUREWORK: Remove Embed IO from here and put threadDelay into another +-- effect. This will also make tests for this faster and deterministic +sftDiscoveryLoop :: Members [DNSLookup, PolyLog, Embed IO] r => SFTEnv -> Sem r () +sftDiscoveryLoop SFTEnv {..} = forever $ do + servers <- discoverSFTServers sftDomain + case servers of + Nothing -> pure () + Just es -> atomicWriteIORef sftServers (Discovered es) + threadDelay sftDiscoveryInterval + +mkSFTEnv :: SFTOptions -> IO SFTEnv +mkSFTEnv opts = + SFTEnv + <$> newIORef NotDiscoveredYet + <*> pure (mkSFTDomain opts) + <*> pure (diffTimeToMicroseconds (fromMaybe defSftDiscoveryIntervalSeconds (Opts.sftDiscoveryIntervalSeconds opts))) + +startSFTServiceDiscovery :: Log.Logger -> SFTEnv -> IO () +startSFTServiceDiscovery logger = + runM . runPolyLog logger . runDNSLookupDefault . sftDiscoveryLoop + +-- | >>> diffTimeToMicroseconds 1 +-- 1000000 +diffTimeToMicroseconds :: DiffTime -> Int +diffTimeToMicroseconds = fromIntegral . (`quot` 1000000) . diffTimeToPicoseconds + +-- TURN specific + +data Env = Env + { _turnServers :: List1 TurnURI, + _turnTokenTTL :: Word32, + _turnConfigTTL :: Word32, + _turnSecret :: ByteString, + _turnSHA512 :: Digest, + _turnPrng :: GenIO + } + +makeLenses ''Env + +newEnv :: Digest -> List1 TurnURI -> Word32 -> Word32 -> ByteString -> IO Env +newEnv sha512 srvs tTTL cTTL secret = Env srvs tTTL cTTL secret sha512 <$> createSystemRandom diff --git a/services/brig/src/Brig/TURN/API.hs b/services/brig/src/Brig/Calling/API.hs similarity index 73% rename from services/brig/src/Brig/TURN/API.hs rename to services/brig/src/Brig/Calling/API.hs index ec0785cc0bd..8ca7e279b93 100644 --- a/services/brig/src/Brig/TURN/API.hs +++ b/services/brig/src/Brig/Calling/API.hs @@ -15,23 +15,26 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.TURN.API +module Brig.Calling.API ( routesPublic, ) where import Brig.API.Handler import Brig.App -import Brig.TURN hiding (Env) -import qualified Brig.TURN as TURN +import Brig.Calling +import qualified Brig.Calling as Calling +import Brig.Calling.Internal import Control.Lens import Control.Monad.Fail (MonadFail) import Control.Monad.Random.Class import Data.ByteString.Conversion (toByteString') import Data.ByteString.Lens import Data.Id -import Data.List1 (List1) +import Data.List.NonEmpty (NonEmpty (..)) +import qualified Data.List.NonEmpty as NonEmpty import qualified Data.List1 as List1 +import Data.Misc ((<$$>)) import Data.Range import qualified Data.Swagger.Build.Api as Doc import Data.Text.Ascii (AsciiBase64, encodeBase64) @@ -46,15 +49,19 @@ import Network.Wai.Utilities.Swagger (document) import OpenSSL.EVP.Digest (Digest, hmacBS) import qualified System.Random.MWC as MWC import System.Random.Shuffle -import qualified Wire.API.Call.TURN as Public +import qualified Wire.API.Call.Config as Public +import Wire.Network.DNS.SRV (srvTarget) routesPublic :: Routes Doc.ApiBuilder Handler () routesPublic = do + -- Deprecated endpoint, but still used by old clients. + -- See https://github.com/zinfra/backend-issues/issues/1616 for context get "/calls/config" (continue getCallsConfigH) $ accept "application" "json" .&. header "Z-User" .&. header "Z-Connection" document "GET" "getCallsConfig" $ do + Doc.deprecated Doc.summary "Retrieve TURN server addresses and credentials for \ \ IP addresses, scheme `turn` and transport `udp` only " @@ -78,23 +85,24 @@ routesPublic = do Doc.response 200 "RTCConfiguration" Doc.end getCallsConfigV2H :: JSON ::: UserId ::: ConnId ::: Maybe (Range 1 10 Int) -> Handler Response -getCallsConfigV2H (_ ::: uid ::: connid ::: limit) = do +getCallsConfigV2H (_ ::: uid ::: connid ::: limit) = json <$> getCallsConfigV2 uid connid limit -- | ('UserId', 'ConnId' are required as args here to make sure this is an authenticated end-point.) getCallsConfigV2 :: UserId -> ConnId -> Maybe (Range 1 10 Int) -> Handler Public.RTCConfiguration getCallsConfigV2 _ _ limit = do env <- liftIO =<< readIORef <$> view turnEnvV2 - newConfig env limit + sftEnv' <- view sftEnv + newConfig env sftEnv' limit getCallsConfigH :: JSON ::: UserId ::: ConnId -> Handler Response -getCallsConfigH (_ ::: uid ::: connid) = do +getCallsConfigH (_ ::: uid ::: connid) = json <$> getCallsConfig uid connid getCallsConfig :: UserId -> ConnId -> Handler Public.RTCConfiguration getCallsConfig _ _ = do env <- liftIO =<< readIORef <$> view turnEnv - dropTransport <$> newConfig env Nothing + dropTransport <$> newConfig env Nothing Nothing where -- In order to avoid being backwards incompatible, remove the `transport` query param from the URIs dropTransport :: Public.RTCConfiguration -> Public.RTCConfiguration @@ -103,11 +111,11 @@ getCallsConfig _ _ = do (Public.rtcConfIceServers . traverse . Public.iceURLs . traverse . Public.turiTransport) Nothing -newConfig :: MonadIO m => TURN.Env -> Maybe (Range 1 10 Int) -> m Public.RTCConfiguration -newConfig env limit = do +newConfig :: MonadIO m => Calling.Env -> Maybe SFTEnv -> Maybe (Range 1 10 Int) -> m Public.RTCConfiguration +newConfig env mSftEnv limit = do let (sha, secret, tTTL, cTTL, prng) = (env ^. turnSHA512, env ^. turnSecret, env ^. turnTokenTTL, env ^. turnConfigTTL, env ^. turnPrng) -- randomize list of servers (before limiting the list, to ensure not always the same servers are chosen if limit is set) - randomizedUris <- liftIO $ randomize (env ^. turnServers) + randomizedUris <- liftIO $ randomize (List1.toNonEmpty $ env ^. turnServers) let limitedUris = case limit of Nothing -> randomizedUris Just lim -> limitedList randomizedUris lim @@ -115,22 +123,27 @@ newConfig env limit = do finalUris <- liftIO $ randomize limitedUris srvs <- for finalUris $ \uri -> do u <- liftIO $ genUsername tTTL prng - pure $ Public.rtcIceServer (List1.singleton uri) u (computeCred sha secret u) - pure $ Public.rtcConfiguration srvs cTTL + pure $ Public.rtcIceServer (uri :| []) u (computeCred sha secret u) + sftSrvEntries <- maybe (pure Nothing) ((fmap discoveryToMaybe) . readIORef . sftServers) mSftEnv + -- According to RFC2782, the SRV Entries are supposed to be tried in order of + -- priority and weight, but we internally agreed to randomize the list of + -- available servers for poor man's "load balancing" purposes. + -- FUTUREWORK: be smarter about list orderding depending on how much capacity SFT servers have. + randomizedSftEntries <- liftIO $ mapM randomize sftSrvEntries + pure $ Public.rtcConfiguration srvs (sftServerFromSrvTarget . srvTarget <$$> randomizedSftEntries) cTTL where -- NOTE: even though `shuffleM` works only for [a], input is List1 so it's -- safe to pattern match; ideally, we'd have `shuffleM` for `NonEmpty` - randomize :: (MonadRandom m, MonadFail m) => List1 Public.TurnURI -> m (List1 Public.TurnURI) - randomize xs = do - (f : fs) <- shuffleM (toList xs) - return $ List1.list1 f fs - limitedList :: List1 Public.TurnURI -> Range 1 10 Int -> List1 Public.TurnURI - limitedList uris lim = do + randomize :: (MonadRandom m, MonadFail m) => NonEmpty a -> m (NonEmpty a) + randomize xs = NonEmpty.fromList <$> shuffleM (NonEmpty.toList xs) + -- + limitedList :: NonEmpty Public.TurnURI -> Range 1 10 Int -> NonEmpty Public.TurnURI + limitedList uris lim = -- assuming limitServers is safe with respect to the length of its return value -- (see property tests in brig-types) -- since the input is List1 and limit is in Range 1 10 -- it should also be safe to assume the returning list has length >= 1 - List1.maybeList1 (Public.limitServers (toList uris) (fromRange lim)) + NonEmpty.nonEmpty (Public.limitServers (NonEmpty.toList uris) (fromRange lim)) & fromMaybe (error "newConfig:limitedList: empty list of servers") genUsername :: Word32 -> MWC.GenIO -> IO Public.TurnUsername genUsername ttl prng = do diff --git a/services/brig/src/Brig/Calling/Internal.hs b/services/brig/src/Brig/Calling/Internal.hs new file mode 100644 index 00000000000..0ce1947b094 --- /dev/null +++ b/services/brig/src/Brig/Calling/Internal.hs @@ -0,0 +1,42 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Calling.Internal where + +import Control.Lens ((?~)) +import qualified Data.ByteString.Char8 as BS +import Data.Misc (ensureHttpsUrl) +import Imports +import qualified URI.ByteString as URI +import qualified URI.ByteString.QQ as URI +import qualified Wire.API.Call.Config as Public +import Wire.Network.DNS.SRV (SrvTarget (..)) + +-- FUTUREWORK: Extract function to translate SrvTarget to HttpsUrl and use it +-- wherever we use DNS for service discovery +sftServerFromSrvTarget :: SrvTarget -> Public.SFTServer +sftServerFromSrvTarget (SrvTarget host port) = + let uriPort = URI.Port (fromIntegral port) + uriHost = URI.Host (dropTrailingDot host) + uri = [URI.uri|https://|] & URI.authorityL ?~ URI.Authority Nothing uriHost (Just uriPort) + in Public.sftServer (ensureHttpsUrl uri) + where + dropTrailingDot :: ByteString -> ByteString + dropTrailingDot bs = + if BS.last bs == '.' + then BS.init bs + else bs diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 76c301ec2de..2e8ac3370d2 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -30,13 +30,18 @@ import qualified Control.Lens as Lens import Data.Aeson (withText) import qualified Data.Aeson as Aeson import Data.Aeson.Types (typeMismatch) +import qualified Data.Char as Char import Data.Domain (Domain) import Data.Id +import Data.Misc ((<$$>)) import Data.Scientific (toBoundedInteger) -import Data.Time.Clock (NominalDiffTime) -import Data.Yaml (FromJSON (..), ToJSON (..)) +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text +import Data.Time.Clock (DiffTime, NominalDiffTime, secondsToDiffTime) +import Data.Yaml ((.:), (.:?), FromJSON (..), ToJSON (..)) import qualified Data.Yaml as Y import Imports +import qualified Network.DNS as DNS import System.Logger.Extended (Level, LogFormat) import Util.Options @@ -379,8 +384,8 @@ data Opts = Opts logFormat :: !(Maybe (Last LogFormat)), -- | TURN server settings turn :: !TurnOpts, - -- Runtime settings - + -- | SFT Settings + sft :: !(Maybe SFTOptions), -- | Runtime settings optSettings :: !Settings } @@ -519,6 +524,26 @@ data CustomerExtensions = CustomerExtensions newtype DomainsBlockedForRegistration = DomainsBlockedForRegistration [Domain] deriving newtype (Show, FromJSON, Generic) +data SFTOptions = SFTOptions + { sftBaseDomain :: !DNS.Domain, + sftSRVServiceName :: !(Maybe ByteString), -- defaults to defSftServiceName if unset + sftDiscoveryIntervalSeconds :: !(Maybe DiffTime) -- defaults to defSftDiscoveryIntervalSeconds + } + deriving (Show, Generic) + +instance FromJSON SFTOptions where + parseJSON = Y.withObject "SFTOptions" $ \o -> + SFTOptions + <$> (asciiOnly =<< o .: "sftBaseDomain") + <*> (mapM asciiOnly =<< o .:? "sftSRVServiceName") + <*> (secondsToDiffTime <$$> o .:? "sftDiscoveryIntervalSeconds") + where + asciiOnly :: Text -> Y.Parser ByteString + asciiOnly t = + if Text.all Char.isAscii t + then pure $ Text.encodeUtf8 t + else fail $ "Expected ascii string only, found: " <> Text.unpack t + defMaxKeyLen :: Int64 defMaxKeyLen = 1024 @@ -534,6 +559,12 @@ defSqsThrottleMillis = 500 defUserMaxPermClients :: Int defUserMaxPermClients = 7 +defSftServiceName :: ByteString +defSftServiceName = "_sft" + +defSftDiscoveryIntervalSeconds :: DiffTime +defSftDiscoveryIntervalSeconds = secondsToDiffTime 10 + instance FromJSON Timeout where parseJSON (Y.Number n) = let defaultV = 3600 @@ -551,7 +582,8 @@ instance FromJSON Opts -- TODO: Does it make sense to generate lens'es for all? Lens.makeLensesFor [ ("optSettings", "optionSettings"), - ("elasticsearch", "elasticsearchL") + ("elasticsearch", "elasticsearchL"), + ("sft", "sftL") ] ''Opts @@ -572,3 +604,5 @@ Lens.makeLensesFor ("additionalWriteIndex", "additionalWriteIndexL") ] ''ElasticSearchOpts + +Lens.makeLensesFor [("sftBaseDomain", "sftBaseDomainL")] ''SFTOptions diff --git a/services/brig/src/Brig/PolyLog.hs b/services/brig/src/Brig/PolyLog.hs new file mode 100644 index 00000000000..758e5283548 --- /dev/null +++ b/services/brig/src/Brig/PolyLog.hs @@ -0,0 +1,19 @@ +module Brig.PolyLog where + +import Imports +import Polysemy +import qualified System.Logger as Log + +-- | This effect will help us write tests for log messages +-- +-- FUTUREWORK: Move this to a separate module if it is required +-- +-- FUTUREWORK: Either write an orphan instance for MonadLogger or provide +-- equivalent functions in System.Logger.Class +data PolyLog m a where + PolyLog :: Log.Level -> (Log.Msg -> Log.Msg) -> PolyLog m () + +makeSem 'PolyLog + +runPolyLog :: Member (Embed IO) r => Log.Logger -> Sem (PolyLog ': r) a -> Sem r a +runPolyLog logger = interpret $ \(PolyLog lvl msg) -> Log.log logger lvl msg diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 4215bd3c223..f4f25cc9da1 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -27,6 +27,7 @@ import Brig.AWS (sesQueue) import qualified Brig.AWS as AWS import qualified Brig.AWS.SesNotification as SesNotification import Brig.App +import qualified Brig.Calling as Calling import qualified Brig.InternalEvent.Process as Internal import Brig.Options hiding (internalEvents, sesQueue) import qualified Brig.Queue as Queue @@ -43,6 +44,10 @@ import Network.Wai.Utilities.Server import qualified Network.Wai.Utilities.Server as Server import Util.Options +-- FUTUREWORK: If any of these async threads die, we will have no clue about it +-- and brig could start misbehaving. We should ensure that brig dies whenever a +-- thread terminates for any reason. +-- https://github.com/zinfra/backend-issues/issues/1647 run :: Opts -> IO () run o = do (app, e) <- mkApp o @@ -56,9 +61,11 @@ run o = do Async.async $ AWS.execute (e ^. awsEnv) $ AWS.listen throttleMillis q (runAppT e . SesNotification.onEvent) + sftDiscovery <- forM (e ^. sftEnv) $ Async.async . Calling.startSFTServiceDiscovery (e ^. applog) runSettingsWithShutdown s app 5 `finally` do mapM_ Async.cancel emailListener Async.cancel internalEventListener + mapM_ Async.cancel sftDiscovery closeEnv e where endpoint = brig o diff --git a/services/brig/test/integration/API/TURN.hs b/services/brig/test/integration/API/Calling.hs similarity index 75% rename from services/brig/test/integration/API/TURN.hs rename to services/brig/test/integration/API/Calling.hs index a7804c6e65d..d7ce6376feb 100644 --- a/services/brig/test/integration/API/TURN.hs +++ b/services/brig/test/integration/API/Calling.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2020 Wire Swiss GmbH @@ -15,38 +17,46 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module API.TURN where +module API.Calling where import Bilge import Bilge.Assert +import qualified Brig.Options as Opts import Brig.Types -import Control.Lens ((^.)) +import Control.Lens ((?~), (^.), view) +import Control.Monad.Catch (MonadCatch, MonadThrow) +import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as LB import Data.Id import Data.List ((\\)) +import qualified Data.List.NonEmpty as NonEmpty import Data.List1 (List1) import qualified Data.List1 as List1 -import Data.Misc (Port) +import Data.Misc (Port, mkHttpsUrl) +import qualified Data.Set as Set import Imports import Network.HTTP.Client (Manager) import System.FilePath (()) import Test.Tasty import Test.Tasty.HUnit +import URI.ByteString (laxURIParserOptions, parseURI) import UnliftIO.Exception (finally) import qualified UnliftIO.Temporary as Temp import Util - -tests :: Manager -> Brig -> FilePath -> FilePath -> IO TestTree -tests m b turn turnV2 = do - return $ - testGroup - "turn" - [ test m "basic /calls/config - 200" $ testCallsConfig b, - -- FIXME: requires tests to run on same host as brig - test m "multiple servers /calls/config - 200" . withTurnFile turn $ testCallsConfigMultiple b, - test m "multiple servers /calls/config/v2 - 200" . withTurnFile turnV2 $ testCallsConfigMultipleV2 b - ] +import Wire.API.Call.Config + +tests :: Manager -> Brig -> Opts.Opts -> FilePath -> FilePath -> IO TestTree +tests m b opts turn turnV2 = do + return $ testGroup "calling" $ + [ testGroup "turn" $ + [ test m "basic /calls/config - 200" $ testCallsConfig b, + -- FIXME: requires tests to run on same host as brig + test m "multiple servers /calls/config - 200" . withTurnFile turn $ testCallsConfigMultiple b, + test m "multiple servers /calls/config/v2 - 200" . withTurnFile turnV2 $ testCallsConfigMultipleV2 b + ], + testGroup "sft" $ [test m "SFT servers /calls/config/v2 - 200" $ testSFT b opts] + ] testCallsConfig :: Brig -> Http () testCallsConfig b = do @@ -75,6 +85,26 @@ testCallsConfigMultiple b turnUpdater = do let _expected = List1.singleton (toTurnURILegacy "127.0.0.1" 3478) modifyAndAssert b uid getTurnConfigurationV1 turnUpdater "turn:127.0.0.1:3478" _expected +testSFT :: Brig -> Opts.Opts -> Http () +testSFT b opts = do + uid <- userId <$> randomUser b + cfg <- getTurnConfigurationV2 uid b + liftIO $ + assertEqual + "when SFT discovery is not enabled, sft_servers shouldn't be returned" + Nothing + (cfg ^. rtcConfSftServers) + withSettingsOverrides (opts & Opts.sftL ?~ Opts.SFTOptions "integration-tests.zinfra.io" Nothing (Just 0.001)) $ do + cfg1 <- retryWhileN 10 (isNothing . view rtcConfSftServers) (getTurnConfigurationV2 uid b) + -- These values are controlled by https://github.com/zinfra/cailleach/tree/77ca2d23cf2959aa183dd945d0a0b13537a8950d/environments/dns-integration-tests + let Right server1 = mkHttpsUrl =<< first show (parseURI laxURIParserOptions "https://sft01.integration-tests.zinfra.io:443") + let Right server2 = mkHttpsUrl =<< first show (parseURI laxURIParserOptions "https://sft02.integration-tests.zinfra.io:8443") + liftIO $ + assertEqual + "when SFT discovery is enabled, sft_servers should be returned" + (Set.fromList [sftServer server1, sftServer server2]) + (Set.fromList $ maybe [] NonEmpty.toList $ cfg1 ^. rtcConfSftServers) + modifyAndAssert :: Brig -> UserId -> @@ -150,10 +180,10 @@ assertConfiguration cfg turns = getTurnConfigurationV1 :: UserId -> Brig -> Http RTCConfiguration getTurnConfigurationV1 = getAndValidateTurnConfiguration "" -getTurnConfigurationV2 :: UserId -> Brig -> Http RTCConfiguration +getTurnConfigurationV2 :: HasCallStack => UserId -> Brig -> ((Monad m, MonadHttp m, MonadIO m, MonadCatch m) => m RTCConfiguration) getTurnConfigurationV2 = getAndValidateTurnConfiguration "v2" -getTurnConfiguration :: ByteString -> UserId -> Brig -> Http (Response (Maybe LB.ByteString)) +getTurnConfiguration :: ByteString -> UserId -> Brig -> ((MonadHttp m, MonadIO m) => m (Response (Maybe LB.ByteString))) getTurnConfiguration suffix u b = get ( b @@ -162,7 +192,7 @@ getTurnConfiguration suffix u b = . zConn "conn" ) -getAndValidateTurnConfiguration :: HasCallStack => ByteString -> UserId -> Brig -> Http RTCConfiguration +getAndValidateTurnConfiguration :: HasCallStack => ByteString -> UserId -> Brig -> ((Monad m, MonadIO m, MonadHttp m, MonadThrow m, MonadCatch m) => m RTCConfiguration) getAndValidateTurnConfiguration suffix u b = responseJsonError =<< (getTurnConfiguration suffix u b iConf) mg db b c g searchApis <- Search.tests brigOpts mg g b teamApis <- Team.tests brigOpts mg n b c g awsEnv - turnApi <- TURN.tests mg b turnFile turnFileV2 + turnApi <- Calling.tests mg b brigOpts turnFile turnFileV2 idMappingApi <- pure $ IdMapping.tests brigOpts mg b metricsApi <- Metrics.tests mg b settingsApi <- Settings.tests brigOpts mg b g diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 6e21af2e25e..2161b748019 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -25,6 +25,8 @@ import Bilge import Bilge.Assert import qualified Brig.AWS as AWS import Brig.AWS.Types +import Brig.App (applog, sftEnv) +import Brig.Calling as Calling import qualified Brig.Options as Opts import qualified Brig.Run as Run import Brig.Types.Activation @@ -33,7 +35,7 @@ import Brig.Types.Connection import Brig.Types.Intra import Brig.Types.User import Brig.Types.User.Auth -import Control.Lens ((^?), (^?!)) +import Control.Lens ((^.), (^?), (^?!)) import Control.Monad.Catch (MonadCatch) import Control.Monad.Fail (MonadFail) import Control.Retry @@ -62,6 +64,7 @@ import Test.Tasty (TestName, TestTree) import Test.Tasty.Cannon import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit +import qualified UnliftIO.Async as Async import Util.AWS import Wire.API.Conversation.Member (Member (..)) @@ -704,10 +707,17 @@ retryWhileN n f m = -- | This allows you to run requests against a brig instantiated using the given options. -- Note that ONLY 'brig' calls should occur within the provided action, calls to other -- services will fail. +-- +-- Beware: Not all async parts of brig are running in this. withSettingsOverrides :: MonadIO m => Opts.Opts -> WaiTest.Session a -> m a withSettingsOverrides opts action = liftIO $ do - (brigApp, _) <- Run.mkApp opts - WaiTest.runSession action brigApp + (brigApp, env) <- Run.mkApp opts + sftDiscovery <- + forM (env ^. sftEnv) $ \sftEnv' -> + Async.async $ Calling.startSFTServiceDiscovery (env ^. applog) sftEnv' + res <- WaiTest.runSession action brigApp + mapM_ Async.cancel sftDiscovery + pure res -- | When we remove the customer-specific extension of domain blocking, this test will fail to -- compile. diff --git a/services/brig/test/unit/Main.hs b/services/brig/test/unit/Main.hs index 6f12acae661..75aa2dfddf8 100644 --- a/services/brig/test/unit/Main.hs +++ b/services/brig/test/unit/Main.hs @@ -21,6 +21,8 @@ module Main where import Imports +import qualified Test.Brig.Calling +import qualified Test.Brig.Calling.Internal import qualified Test.Brig.User.Search.Index.Types import Test.Tasty @@ -29,5 +31,7 @@ main = defaultMain $ testGroup "Tests" - [ Test.Brig.User.Search.Index.Types.tests + [ Test.Brig.User.Search.Index.Types.tests, + Test.Brig.Calling.tests, + Test.Brig.Calling.Internal.tests ] diff --git a/services/brig/test/unit/Test/Brig/Calling.hs b/services/brig/test/unit/Test/Brig/Calling.hs new file mode 100644 index 00000000000..02248bbef0b --- /dev/null +++ b/services/brig/test/unit/Test/Brig/Calling.hs @@ -0,0 +1,206 @@ +{-# LANGUAGE RecordWildCards #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Brig.Calling where + +import Brig.Calling +import Brig.Options +import Brig.PolyLog +import Control.Retry +import Data.List.NonEmpty (NonEmpty (..)) +import Imports +import Network.DNS +import Polysemy +import qualified System.Logger as Log +import Test.Tasty +import Test.Tasty.HUnit +import qualified UnliftIO.Async as Async +import Wire.Network.DNS.Effect +import Wire.Network.DNS.SRV + +data FakeDNSEnv = FakeDNSEnv + { fakeLookupFn :: Domain -> SrvResponse, + fakeLookupCalls :: IORef [Domain] + } + +newFakeDNSEnv :: (Domain -> SrvResponse) -> IO FakeDNSEnv +newFakeDNSEnv lookupFn = do + FakeDNSEnv lookupFn <$> newIORef [] + +runFakeDNSLookup :: Member (Embed IO) r => FakeDNSEnv -> Sem (DNSLookup ': r) a -> Sem r a +runFakeDNSLookup FakeDNSEnv {..} = interpret $ \case + LookupSRV domain -> do + modifyIORef' fakeLookupCalls (++ [domain]) + pure $ fakeLookupFn domain + +newtype LogRecorder = LogRecorder {recordedLogs :: IORef [(Log.Level, LByteString)]} + +newLogRecorder :: IO LogRecorder +newLogRecorder = LogRecorder <$> newIORef [] + +recordLogs :: Member (Embed IO) r => LogRecorder -> Sem (PolyLog ': r) a -> Sem r a +recordLogs LogRecorder {..} = interpret $ \(PolyLog lvl msg) -> + modifyIORef' recordedLogs (++ [(lvl, Log.render (Log.renderDefault ", ") msg)]) + +ignoreLogs :: Sem (PolyLog ': r) a -> Sem r a +ignoreLogs = interpret $ \(PolyLog _ _) -> pure () + +tests :: TestTree +tests = + testGroup "Calling" $ + [ testGroup "mkSFTDomain" $ + [ testCase "when service name is provided" $ + assertEqual + "should use the service name to form domain" + "_foo._tcp.example.com." + (mkSFTDomain (SFTOptions "example.com" (Just "foo") Nothing)), + testCase "when service name is not provided" $ + assertEqual + "should assume service name to be 'sft'" + "_sft._tcp.example.com." + (mkSFTDomain (SFTOptions "example.com" Nothing Nothing)) + ], + testGroup "sftDiscoveryLoop" $ + [ testCase "when service can be discovered" $ void testDiscoveryLoopWhenSuccessful, + testCase "when service can be discovered and the URLs change" testDiscoveryLoopWhenURLsChange, + testCase "when service cannot be discovered" testDiscoveryLoopWhenUnsuccessful, + testCase "when service cannot be discovered after a successful discovery" testDiscoveryLoopWhenUnsuccessfulAfterSuccess + ], + testGroup "discoverSFTServers" $ + [ testCase "when service is available" testSFTDiscoverWhenAvailable, + testCase "when service is not available" testSFTDiscoverWhenNotAvailable, + testCase "when dns lookup fails" testSFTDiscoverWhenDNSFails + ] + ] + +testDiscoveryLoopWhenSuccessful :: IO SFTEnv +testDiscoveryLoopWhenSuccessful = do + let entry1 = SrvEntry 0 0 (SrvTarget "sft1.foo.example.com." 443) + entry2 = SrvEntry 0 0 (SrvTarget "sft2.foo.example.com." 443) + entry3 = SrvEntry 0 0 (SrvTarget "sft3.foo.example.com." 443) + returnedEntries = (entry1 :| [entry2, entry3]) + fakeDNSEnv <- newFakeDNSEnv (\_ -> SrvAvailable returnedEntries) + sftEnv <- mkSFTEnv (SFTOptions "foo.example.com" Nothing (Just 0.001)) + + discoveryLoop <- Async.async $ runM . ignoreLogs . runFakeDNSLookup fakeDNSEnv $ sftDiscoveryLoop sftEnv + void $ retryEvery10MicrosWhileN 2000 (== 0) (length <$> readIORef (fakeLookupCalls fakeDNSEnv)) + -- We don't want to stop the loop before it has written to the sftServers IORef + void $ retryEvery10MicrosWhileN 2000 (== NotDiscoveredYet) (readIORef (sftServers sftEnv)) + Async.cancel discoveryLoop + + actualServers <- readIORef (sftServers sftEnv) + assertEqual "servers should be the ones read from DNS" (Discovered returnedEntries) actualServers + pure sftEnv + +testDiscoveryLoopWhenUnsuccessful :: IO () +testDiscoveryLoopWhenUnsuccessful = do + fakeDNSEnv <- newFakeDNSEnv (\_ -> SrvNotAvailable) + sftEnv <- mkSFTEnv (SFTOptions "foo.example.com" Nothing (Just 0.001)) + + discoveryLoop <- Async.async $ runM . ignoreLogs . runFakeDNSLookup fakeDNSEnv $ sftDiscoveryLoop sftEnv + -- We wait for at least two lookups to be sure that the lookup loop looped at + -- least once + void $ retryEvery10MicrosWhileN 2000 (<= 1) (length <$> readIORef (fakeLookupCalls fakeDNSEnv)) + Async.cancel discoveryLoop + + actualServers <- readIORef (sftServers sftEnv) + assertEqual "servers should be the ones read from DNS" NotDiscoveredYet actualServers + +testDiscoveryLoopWhenUnsuccessfulAfterSuccess :: IO () +testDiscoveryLoopWhenUnsuccessfulAfterSuccess = do + sftEnv <- testDiscoveryLoopWhenSuccessful + previousEntries <- readIORef (sftServers sftEnv) + + -- In the following lines we re-use the 'sftEnv' from a successful lookup to + -- replicate what will happen when a dns lookup fails after success + failingFakeDNSEnv <- newFakeDNSEnv (\_ -> SrvNotAvailable) + discoveryLoop <- Async.async $ runM . ignoreLogs . runFakeDNSLookup failingFakeDNSEnv $ sftDiscoveryLoop sftEnv + -- We wait for at least two lookups to be sure that the lookup loop looped at + -- least once + void $ retryEvery10MicrosWhileN 2000 (<= 1) (length <$> readIORef (fakeLookupCalls failingFakeDNSEnv)) + Async.cancel discoveryLoop + + actualServers <- readIORef (sftServers sftEnv) + assertEqual "servers shouldn't get overwriten" previousEntries actualServers + +testDiscoveryLoopWhenURLsChange :: IO () +testDiscoveryLoopWhenURLsChange = do + sftEnv <- testDiscoveryLoopWhenSuccessful + + -- In the following lines we re-use the 'sftEnv' from a successful lookup to + -- replicate what will happen when a dns lookup returns new URLs + let entry1 = SrvEntry 0 0 (SrvTarget "sft4.foo.example.com." 443) + entry2 = SrvEntry 0 0 (SrvTarget "sft5.foo.example.com." 443) + newEntries = (entry1 :| [entry2]) + + fakeDNSEnv <- newFakeDNSEnv (\_ -> SrvAvailable newEntries) + discoveryLoop <- Async.async $ runM . ignoreLogs . runFakeDNSLookup fakeDNSEnv $ sftDiscoveryLoop sftEnv + void $ retryEvery10MicrosWhileN 2000 (== 0) (length <$> readIORef (fakeLookupCalls fakeDNSEnv)) + -- We don't want to stop the loop before it has written to the sftServers IORef + void $ retryEvery10MicrosWhileN 2000 (== Discovered newEntries) (readIORef (sftServers sftEnv)) + Async.cancel discoveryLoop + + actualServers <- readIORef (sftServers sftEnv) + assertEqual "servers should get overwritten" (Discovered newEntries) actualServers + +testSFTDiscoverWhenAvailable :: IO () +testSFTDiscoverWhenAvailable = do + logRecorder <- newLogRecorder + let entry1 = SrvEntry 0 0 (SrvTarget "sft7.foo.example.com." 443) + entry2 = SrvEntry 0 0 (SrvTarget "sft8.foo.example.com." 8843) + returnedEntries = (entry1 :| [entry2]) + fakeDNSEnv <- newFakeDNSEnv (\_ -> SrvAvailable returnedEntries) + + assertEqual "discovered servers should be returned" (Just returnedEntries) + =<< ( runM . recordLogs logRecorder . runFakeDNSLookup fakeDNSEnv $ + discoverSFTServers "_sft._tcp.foo.example.com" + ) + assertEqual "nothing should be logged" [] + =<< readIORef (recordedLogs logRecorder) + +testSFTDiscoverWhenNotAvailable :: IO () +testSFTDiscoverWhenNotAvailable = do + logRecorder <- newLogRecorder + fakeDNSEnv <- newFakeDNSEnv (\_ -> SrvNotAvailable) + + assertEqual "discovered servers should be returned" Nothing + =<< ( runM . recordLogs logRecorder . runFakeDNSLookup fakeDNSEnv $ + discoverSFTServers "_sft._tcp.foo.example.com" + ) + assertEqual "should warn about it in the logs" [(Log.Warn, "No SFT servers available\n")] + =<< readIORef (recordedLogs logRecorder) + +testSFTDiscoverWhenDNSFails :: IO () +testSFTDiscoverWhenDNSFails = do + logRecorder <- newLogRecorder + fakeDNSEnv <- newFakeDNSEnv (\_ -> SrvResponseError IllegalDomain) + + assertEqual "discovered servers should be returned" Nothing + =<< ( runM . recordLogs logRecorder . runFakeDNSLookup fakeDNSEnv $ + discoverSFTServers "_sft._tcp.foo.example.com" + ) + assertEqual "should warn about it in the logs" [(Log.Error, "DNS Lookup failed for SFT Discovery, Error=IllegalDomain\n")] + =<< readIORef (recordedLogs logRecorder) + +retryEvery10MicrosWhileN :: (MonadIO m) => Int -> (a -> Bool) -> m a -> m a +retryEvery10MicrosWhileN n f m = + retrying + (constantDelay 10 <> limitRetries n) + (const (return . f)) + (const m) diff --git a/services/brig/test/unit/Test/Brig/Calling/Internal.hs b/services/brig/test/unit/Test/Brig/Calling/Internal.hs new file mode 100644 index 00000000000..5289db03eb7 --- /dev/null +++ b/services/brig/test/unit/Test/Brig/Calling/Internal.hs @@ -0,0 +1,48 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Brig.Calling.Internal where + +import Brig.Calling.Internal +import Data.Misc (mkHttpsUrl) +import Imports +import Test.Tasty +import Test.Tasty.HUnit +import URI.ByteString.QQ as URI +import Wire.API.Call.Config (sftServer) +import Wire.Network.DNS.SRV (SrvTarget (SrvTarget)) + +tests :: TestTree +tests = + testGroup "Calling.API" $ + [ testGroup "sftServerFromSrvTarget" $ + [ testCase "when srvTarget ends with a dot" $ do + let Right expectedServer = sftServer <$> mkHttpsUrl [URI.uri|https://sft1.env.example.com:9364|] + assertEqual + "the dot should be stripped from sft server" + expectedServer + (sftServerFromSrvTarget $ SrvTarget "sft1.env.example.com." 9364), + testCase "when srvTarget doesn't end with a dot" $ do + let Right expectedServer = sftServer <$> mkHttpsUrl [URI.uri|https://sft2.env.example.com:443|] + assertEqual + "the dot should be stripped from sft server" + expectedServer + (sftServerFromSrvTarget $ SrvTarget "sft2.env.example.com" 443) + ] + ] diff --git a/stack.yaml b/stack.yaml index e4b80a6b2f4..6c6808e7dbf 100644 --- a/stack.yaml +++ b/stack.yaml @@ -8,7 +8,7 @@ packages: - libs/cargohold-types - libs/cassandra-util - libs/extended -- libs/federation-util +- libs/dns-util - libs/galley-types - libs/gundeck-types - libs/hscim @@ -170,6 +170,9 @@ extra-deps: - QuickCheck-2.14 - splitmix-0.0.4 # needed for QuickCheck +# Newer than the one one stackage +- polysemy-1.3.0.0 + ############################################################ # Development tools ############################################################ diff --git a/stack.yaml.lock b/stack.yaml.lock index 0fea55f66bc..17bbcfc454a 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -13,9 +13,6 @@ packages: hackage: swagger2-2.4 - completed: subdir: wai-middleware-prometheus - cabal-file: - size: 1314 - sha256: 1625792914fb2139f005685be8ce519111451cfb854816e430fbf54af46238b4 name: wai-middleware-prometheus version: 1.0.0 git: https://github.com/fimad/prometheus-haskell @@ -28,9 +25,6 @@ packages: git: https://github.com/fimad/prometheus-haskell commit: 2e3282e5fb27ba8d989c271a0a989823fad7ec43 - completed: - cabal-file: - size: 8392 - sha256: 2c5d7f46633fb414eeb43facfe1018378f001b8b67dd77a45d110db84e46034c name: saml2-web-sso version: '0.18' git: https://github.com/wireapp/saml2-web-sso @@ -42,9 +36,6 @@ packages: git: https://github.com/wireapp/saml2-web-sso commit: 687d9ac8ac2994aff8436189c6ecce29faad8500 - completed: - cabal-file: - size: 912 - sha256: 71d9fd9fd55cfb7b549eb2cecfb5258171be14669f990e4d05c6b3508dd5878b name: collectd version: 0.0.0.2 git: https://github.com/kim/hs-collectd @@ -56,9 +47,6 @@ packages: git: https://github.com/kim/hs-collectd commit: 885da222be2375f78c7be36127620ed772b677c9 - completed: - cabal-file: - size: 1196 - sha256: 1321f0148c87e75202829edac7f375bffa6159dad050b9737403306a493579ba name: snappy-framing version: 0.1.1 git: https://github.com/kim/snappy-framing @@ -70,9 +58,6 @@ packages: git: https://github.com/kim/snappy-framing commit: d99f702c0086729efd6848dea8a01e5266c3a61c - completed: - cabal-file: - size: 3722 - sha256: 1509c11cbcc23595f4b9503bac28df4b10cc870cd7869f1859d43a373476d5a8 name: wai-routing version: 0.13.0 git: https://gitlab.com/twittner/wai-routing @@ -84,9 +69,6 @@ packages: git: https://gitlab.com/twittner/wai-routing commit: 7e996a93fec5901767f845a50316b3c18e51a61d - completed: - cabal-file: - size: 2490 - sha256: 7cca808c05cb584f1d4c6f60893bd28b0de41f450135bf48b70414a1f547d31f name: multihash version: 0.1.6 git: https://github.com/wireapp/haskell-multihash.git @@ -98,9 +80,6 @@ packages: git: https://github.com/wireapp/haskell-multihash.git commit: 300a6f46384bfca33e545c8bab52ef3717452d12 - completed: - cabal-file: - size: 2289 - sha256: 07c1e684acf4ba1c097fe5dd2525cb887269f7d3335c42783c1f4d2bfdc01283 name: hspec-wai version: 0.9.2 git: https://github.com/wireapp/hspec-wai @@ -112,9 +91,6 @@ packages: git: https://github.com/wireapp/hspec-wai commit: 0a5142cd3ba48116ff059c041348b817fb7bdb25 - completed: - cabal-file: - size: 3403 - sha256: 2fd9aef25802bf62848b7087a9476264b50bc7d9f195c7f0012e52d19ed2ebe3 name: bloodhound version: 0.17.0.0 git: https://github.com/wireapp/bloodhound @@ -143,9 +119,6 @@ packages: size: 11138812 subdir: amazonka url: https://github.com/wireapp/amazonka/archive/9de5e0e4b2511ec555fb0975581b3087a94c1b4a.tar.gz - cabal-file: - size: 3457 - sha256: 7ac360751e371ba853f56d357e861c5fe103b1da17f045ac47fd285c164a37f7 name: amazonka version: 1.6.1 sha256: b9277a51b60d639fbd91b630f353bf9db305f5759f8e1ee48f0ab026e6b43d00 @@ -161,9 +134,6 @@ packages: size: 11138812 subdir: amazonka-cloudfront url: https://github.com/wireapp/amazonka/archive/9de5e0e4b2511ec555fb0975581b3087a94c1b4a.tar.gz - cabal-file: - size: 5668 - sha256: 79c95e0ec544437a613cab891a2057bc35f1b0fed2361b36e7f05437839bdce2 name: amazonka-cloudfront version: 1.6.1 sha256: b9277a51b60d639fbd91b630f353bf9db305f5759f8e1ee48f0ab026e6b43d00 @@ -179,9 +149,6 @@ packages: size: 11138812 subdir: amazonka-dynamodb url: https://github.com/wireapp/amazonka/archive/9de5e0e4b2511ec555fb0975581b3087a94c1b4a.tar.gz - cabal-file: - size: 4459 - sha256: 6b8852049c65207a7b3741aafa3e4e6c77cfa115e05de3c74868218ae642b6b0 name: amazonka-dynamodb version: 1.6.1 sha256: b9277a51b60d639fbd91b630f353bf9db305f5759f8e1ee48f0ab026e6b43d00 @@ -197,9 +164,6 @@ packages: size: 11138812 subdir: amazonka-s3 url: https://github.com/wireapp/amazonka/archive/9de5e0e4b2511ec555fb0975581b3087a94c1b4a.tar.gz - cabal-file: - size: 6317 - sha256: 9d07240fca59ad5197fb614ce3051e701e4951e6d4625a2dab4a9c17a1900194 name: amazonka-s3 version: 1.6.1 sha256: b9277a51b60d639fbd91b630f353bf9db305f5759f8e1ee48f0ab026e6b43d00 @@ -215,9 +179,6 @@ packages: size: 11138812 subdir: amazonka-ses url: https://github.com/wireapp/amazonka/archive/9de5e0e4b2511ec555fb0975581b3087a94c1b4a.tar.gz - cabal-file: - size: 6425 - sha256: 335796c855121ca34affd35097676587d5ebe0b2e576da42faaedd9d163881b0 name: amazonka-ses version: 1.6.1 sha256: b9277a51b60d639fbd91b630f353bf9db305f5759f8e1ee48f0ab026e6b43d00 @@ -233,9 +194,6 @@ packages: size: 11138812 subdir: amazonka-sns url: https://github.com/wireapp/amazonka/archive/9de5e0e4b2511ec555fb0975581b3087a94c1b4a.tar.gz - cabal-file: - size: 4271 - sha256: b07fbf8a2806fe775b25ea74d0d78f14f286811e4aa59f9c50e97ed99f2a14a6 name: amazonka-sns version: 1.6.1 sha256: b9277a51b60d639fbd91b630f353bf9db305f5759f8e1ee48f0ab026e6b43d00 @@ -251,9 +209,6 @@ packages: size: 11138812 subdir: amazonka-sqs url: https://github.com/wireapp/amazonka/archive/9de5e0e4b2511ec555fb0975581b3087a94c1b4a.tar.gz - cabal-file: - size: 3708 - sha256: 1578844a31a2e53f9f21fd217e14406a3f02aefa637678ef88b201b01fbed492 name: amazonka-sqs version: 1.6.1 sha256: b9277a51b60d639fbd91b630f353bf9db305f5759f8e1ee48f0ab026e6b43d00 @@ -269,9 +224,6 @@ packages: size: 11138812 subdir: core url: https://github.com/wireapp/amazonka/archive/9de5e0e4b2511ec555fb0975581b3087a94c1b4a.tar.gz - cabal-file: - size: 4957 - sha256: 8ff9614130407588370e12e905f3539a733b76f6d9397ed3522ce54fc154d918 name: amazonka-core version: 1.6.1 sha256: b9277a51b60d639fbd91b630f353bf9db305f5759f8e1ee48f0ab026e6b43d00 @@ -284,9 +236,6 @@ packages: url: https://github.com/wireapp/amazonka/archive/9de5e0e4b2511ec555fb0975581b3087a94c1b4a.tar.gz sha256: b9277a51b60d639fbd91b630f353bf9db305f5759f8e1ee48f0ab026e6b43d00 - completed: - cabal-file: - size: 1150 - sha256: bbb1a78c1c8a2fe2a7b46a734f3b60754a86e07f07a1a27d781f121831918289 name: cryptobox-haskell version: 0.1.1 git: https://github.com/wireapp/cryptobox-haskell @@ -298,9 +247,6 @@ packages: git: https://github.com/wireapp/cryptobox-haskell commit: 7546a1a25635ef65183e3d44c1052285e8401608 - completed: - cabal-file: - size: 3593 - sha256: 1f822adc38dcba267caa05c4f1405f92c60a340ea17c4fbbf92934e71ccf4809 name: hsaml2 version: '0.1' git: https://github.com/wireapp/hsaml2 @@ -313,9 +259,6 @@ packages: commit: fe08618e81dee9b7a25f10f5b9d26d1ff1837c79 - completed: subdir: http-client - cabal-file: - size: 5350 - sha256: 868faa3479fa330ac6eb897e6888296a32f10a249d2d91ece5ab2add9f0c24d4 name: http-client version: 0.7.0 git: https://github.com/wireapp/http-client @@ -329,9 +272,6 @@ packages: commit: 9100baeddbd15d93dc58a826ae812dafff29d5fd - completed: subdir: http-client-openssl - cabal-file: - size: 1494 - sha256: 423d74b93d5b2a79991340da8d2cd8fccd496fb470483bad8c73857200509e4e name: http-client-openssl version: 0.3.1.0 git: https://github.com/wireapp/http-client @@ -345,9 +285,6 @@ packages: commit: 9100baeddbd15d93dc58a826ae812dafff29d5fd - completed: subdir: http-client-tls - cabal-file: - size: 2041 - sha256: 1043cb22bc772acdc5176b3db88ea74ae299a658d03aa7d4027f970328487f4c name: http-client-tls version: 0.3.5.3 git: https://github.com/wireapp/http-client @@ -361,9 +298,6 @@ packages: commit: 9100baeddbd15d93dc58a826ae812dafff29d5fd - completed: subdir: http-conduit - cabal-file: - size: 2910 - sha256: 4e0024c25cb1a6c5a20b687201c78a7a2c781a582f669d0f88125d113e65c326 name: http-conduit version: 2.3.7.3 git: https://github.com/wireapp/http-client @@ -564,6 +498,13 @@ packages: sha256: e58892088b95190bfb59a7c0803f7ef65338e57fc9b938d7c166563605003902 original: hackage: splitmix-0.0.4 +- completed: + hackage: polysemy-1.3.0.0@sha256:fa76e96a883fd1c4bdbad792a0a9d88f59f84817651aea5c71d9b4f74e42c5b6,6141 + pantry-tree: + size: 4309 + sha256: 3d2fb15ddda9053f6bfd4b0810a79a9542505acb5e7e528856ec3cd86d6df066 + original: + hackage: polysemy-1.3.0.0 - completed: hackage: ormolu-0.0.5.0@sha256:e5f49c51c6ebd8b3cd16113e585312de7315c1e1561fbb599988cebc61c14f4e,7956 pantry-tree: From f57843120add9f91c47023d568cc0bc3c644e068 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Fri, 31 Jul 2020 13:37:19 +0200 Subject: [PATCH 03/23] Merge brig:lib and brig-index-lib (#1183) This unblocks zoogle and avoids other weird issues with stack not handling internal libraries properly. --- services/brig/brig.cabal | 45 +++---------------- services/brig/package.yaml | 25 +---------- .../brig/{index => }/src/Brig/Index/Eval.hs | 0 .../{index => }/src/Brig/Index/Migrations.hs | 0 .../src/Brig/Index/Migrations/Types.hs | 0 .../{index => }/src/Brig/Index/Options.hs | 0 6 files changed, 8 insertions(+), 62 deletions(-) rename services/brig/{index => }/src/Brig/Index/Eval.hs (100%) rename services/brig/{index => }/src/Brig/Index/Migrations.hs (100%) rename services/brig/{index => }/src/Brig/Index/Migrations/Types.hs (100%) rename services/brig/{index => }/src/Brig/Index/Options.hs (100%) diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 11f5b3a6223..5ec599d4eca 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -1,10 +1,10 @@ -cabal-version: 2.0 +cabal-version: 1.12 -- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack -- --- hash: 49345fd7c41cab0441ba95bd4fdd01b1f64b6053e348ec991695254be96ed5d4 +-- hash: 0efba75abb2e931761d20c36d7c690eb2cf9711711c4b002dfd2cd5ff7bd60ee name: brig version: 1.35.0 @@ -53,6 +53,10 @@ library Brig.Data.User Brig.Data.UserKey Brig.Email + Brig.Index.Eval + Brig.Index.Migrations + Brig.Index.Migrations.Types + Brig.Index.Options Brig.InternalEvent.Process Brig.InternalEvent.Types Brig.IO.Intra @@ -227,40 +231,6 @@ library , zauth >=0.10.3 default-language: Haskell2010 -library brig-index-lib - exposed-modules: - Brig.Index.Eval - Brig.Index.Migrations - Brig.Index.Migrations.Types - Brig.Index.Options - Main - other-modules: - Paths_brig - hs-source-dirs: - index/src - default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DerivingStrategies DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings PackageImports PatternSynonyms PolyKinds QuasiQuotes RankNTypes ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeFamilyDependencies TypeOperators UndecidableInstances ViewPatterns - ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields - build-depends: - aeson - , base - , bloodhound - , brig - , cassandra-util >=0.12 - , exceptions - , http-client - , imports - , lens - , metrics-core - , mtl - , optparse-applicative >=0.13 - , retry - , text - , time - , tinylog - , types-common - , uri-bytestring - default-language: Haskell2010 - executable brig main-is: src/Main.hs other-modules: @@ -285,7 +255,7 @@ executable brig-index ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -funbox-strict-fields -threaded -with-rtsopts=-N build-depends: base - , brig-index-lib + , brig , imports , optparse-applicative , tinylog @@ -331,7 +301,6 @@ executable brig-integration , bilge , bloodhound , brig - , brig-index-lib , brig-types , bytestring >=0.9 , bytestring-conversion diff --git a/services/brig/package.yaml b/services/brig/package.yaml index 6394ec964d7..8e6e3f711be 100644 --- a/services/brig/package.yaml +++ b/services/brig/package.yaml @@ -131,28 +131,6 @@ library: - wire-api - yaml >=0.8.22 - zauth >=0.10.3 -internal-libraries: - brig-index-lib: - source-dirs: index/src - dependencies: - - aeson - - base - - bloodhound - - brig - - cassandra-util >=0.12 - - exceptions - - http-client - - imports - - lens - - metrics-core - - mtl - - optparse-applicative >=0.13 - - retry - - text - - time - - tinylog - - types-common - - uri-bytestring tests: brig-tests: main: Main.hs @@ -205,7 +183,6 @@ executables: - bloodhound - base - brig - - brig-index-lib - brig-types - bytestring >=0.9 - bytestring-conversion @@ -271,7 +248,7 @@ executables: - -with-rtsopts=-N dependencies: - base - - brig-index-lib + - brig - imports - optparse-applicative - tinylog diff --git a/services/brig/index/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs similarity index 100% rename from services/brig/index/src/Brig/Index/Eval.hs rename to services/brig/src/Brig/Index/Eval.hs diff --git a/services/brig/index/src/Brig/Index/Migrations.hs b/services/brig/src/Brig/Index/Migrations.hs similarity index 100% rename from services/brig/index/src/Brig/Index/Migrations.hs rename to services/brig/src/Brig/Index/Migrations.hs diff --git a/services/brig/index/src/Brig/Index/Migrations/Types.hs b/services/brig/src/Brig/Index/Migrations/Types.hs similarity index 100% rename from services/brig/index/src/Brig/Index/Migrations/Types.hs rename to services/brig/src/Brig/Index/Migrations/Types.hs diff --git a/services/brig/index/src/Brig/Index/Options.hs b/services/brig/src/Brig/Index/Options.hs similarity index 100% rename from services/brig/index/src/Brig/Index/Options.hs rename to services/brig/src/Brig/Index/Options.hs From af79b2109d111641a4455a39d9ee326074dd92f2 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Fri, 31 Jul 2020 15:57:20 +0200 Subject: [PATCH 04/23] Ensure `sft_servers` is never set to `null` in /calls/config (#1184) --- libs/wire-api/package.yaml | 1 + libs/wire-api/src/Wire/API/Call/Config.hs | 11 ++++++----- libs/wire-api/test/unit/Test/Wire/API/Call/Config.hs | 11 +++++++++-- libs/wire-api/wire-api.cabal | 5 +++-- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/libs/wire-api/package.yaml b/libs/wire-api/package.yaml index c7f8a436f5e..7cb775cf62a 100644 --- a/libs/wire-api/package.yaml +++ b/libs/wire-api/package.yaml @@ -70,3 +70,4 @@ tests: - tasty-expected-failure - tasty-hunit - tasty-quickcheck + - unordered-containers diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index 6839bcd9110..4bf3013c188 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -126,14 +126,15 @@ modelRtcConfiguration = Doc.defineModel "RTCConfiguration" $ do instance ToJSON RTCConfiguration where toJSON (RTCConfiguration srvs sfts ttl) = object - [ "ice_servers" .= srvs, - "sft_servers" .= sfts, - "ttl" .= ttl - ] + ( [ "ice_servers" .= srvs, + "ttl" .= ttl + ] + <> ["sft_servers" .= sfts | isJust sfts] + ) instance FromJSON RTCConfiguration where parseJSON = withObject "RTCConfiguration" $ \o -> - RTCConfiguration <$> o .: "ice_servers" <*> o .: "sft_servers" <*> o .: "ttl" + RTCConfiguration <$> o .: "ice_servers" <*> o .:? "sft_servers" <*> o .: "ttl" -------------------------------------------------------------------------------- -- SFTServer diff --git a/libs/wire-api/test/unit/Test/Wire/API/Call/Config.hs b/libs/wire-api/test/unit/Test/Wire/API/Call/Config.hs index b9885328a63..9394f8bd32d 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Call/Config.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Call/Config.hs @@ -18,11 +18,12 @@ module Test.Wire.API.Call.Config where import Data.Aeson +import qualified Data.HashMap.Strict as HM import Imports import Test.Tasty import Test.Tasty.QuickCheck hiding (total) import Wire.API.Arbitrary () -import Wire.API.Call.Config (TurnURI, isTcp, isTls, isUdp, limitServers) +import Wire.API.Call.Config (RTCConfiguration, TurnURI, isTcp, isTls, isUdp, limitServers) tests :: TestTree tests = @@ -33,7 +34,8 @@ tests = testProperty "limitServers/fairness udp" (fairnessProp isUdp), testProperty "limitServers/fairness tls" (fairnessProp isTls), testProperty "limitServers/fairness tcp" (fairnessProp isTcp), - testProperty "limitServers/udpPriority" udpPriority + testProperty "limitServers/udpPriority" udpPriority, + testProperty "RTCConfiguration/toJson: sftServersAreNeverNull" sftServersAreNeverNull ] turnURIid :: TurnURI -> Property @@ -64,6 +66,11 @@ udpPriority uris = do then returnedUdp >= 2 else True +sftServersAreNeverNull :: RTCConfiguration -> Bool +sftServersAreNeverNull cfg = case toJSON cfg of + Object o -> HM.lookup "sft_servers" o /= Just Null + v -> error . show $ "type mismatch, expected RTCConfiguration to be Object, but got: " <> encode v + newtype ZeroToTen = ZeroToTen Int deriving (Eq, Show) diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 5509101fba5..fb28ea708cc 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -1,10 +1,10 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.2. +-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack -- --- hash: b5bd8bb8589df54572262f534ebd7e422f17cc5bb76b88b1b64ef0390ffbd6ad +-- hash: 806e7ef5feb03ce2ead26b199aff52dd62cdd2126aa4930b74ae089c40b15d84 name: wire-api version: 0.1.0 @@ -145,5 +145,6 @@ test-suite wire-api-tests , tasty-hunit , tasty-quickcheck , types-common >=0.16 + , unordered-containers , wire-api default-language: Haskell2010 From ada6fdbfab626864842f462d8501be44c70266bc Mon Sep 17 00:00:00 2001 From: Matthias Heinzel Date: Fri, 31 Jul 2020 18:51:11 +0200 Subject: [PATCH 05/23] Add script to rebase branch onto formatting changes (#1180) Upgrading Ormolu can be painful, because the style will often change in a lot of places (see for example #1145). Any active branches/PRs will then have to rebase onto these changes and resolve the resulting conflicts. It becomes a bit less cumbersome after first squashing all commits of the branch into a single one, but that's often not desirable. This script will automate the rebasing process, keeping the (linear) history of intact and making the commits appear as if the changes had been applied onto the newly-formatted version all along. --- tools/rebase-onto-formatter.sh | 118 +++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100755 tools/rebase-onto-formatter.sh diff --git a/tools/rebase-onto-formatter.sh b/tools/rebase-onto-formatter.sh new file mode 100755 index 00000000000..fe41734d712 --- /dev/null +++ b/tools/rebase-onto-formatter.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +set -euo pipefail + +command -v sed >/dev/null 2>&1 || { echo >&2 "sed is not installed, aborting."; exit 1; } + +BASE_COMMIT=${1:-} +TARGET_COMMIT=${2:-} +FORMATTING_COMMAND='make formatf' +USAGE=" +USAGE: $0 TARGET_COMMIT BASE_COMMIT + + TARGET_COMMIT: + The commit introducing the formatting that you want to rebase onto. + BASE_COMMIT: + A commit very similar to TARGET_COMMIT, just that the automated formatting changes are not applied yet. + It has to include changes to formatting version and config already. + +Rebase a branch onto changes created by an automated formatter. The script +will keep the (linear) history of the branch intact and make the commits appear +as if the changes had been applied onto the newly-formatted version all along. + +INSTRUCTIONS: +1. Make a copy of your branch (or be prepared to salvage it from reflog). + $ git branch mybranch-backup +2. Find out what the base commit is. +3. Rebase onto the base commit yourself. + $ git rebase \$BASE_COMMIT +4. Make sure the formatting tool is installed with the correct version and settings. + $ stack install ormolu +5. Run this script. + $ $0 \$TARGET_COMMIT \$BASE_COMMIT + +" + +if [ -z "$BASE_COMMIT" ] || [ -z "$TARGET_COMMIT" ] || [ -z "$FORMATTING_COMMAND" ] +then + echo "$USAGE" 1>&2 + exit 1 +fi + +echo "Running the script now. This might take a while..." + +# The general idea is the following: +# +# We have a branch consisting of commits C1, C2, ... on top of our BASE_COMMIT C0. +# Also, from C0 an automated formatting change f was made on some branch (e.g. develop). +# +# C0 ----> C1 ----> C2 ----> ... ----> Cn +# | +# f +# | +# v +# C0' +# +# Now, how do we obtain versions of our commits operating on the formatted code (let's call them Ci')? +# +# C0 ----> C1 ----> C2 ----> ... ----> Cn +# | +# f +# | +# v +# C0' ---> C1' ---> C2' ---> ... ----> Cn' +# +# One useful thing is that since f is defined by an automated tool, +# we know f applied at every commit Ci, resulting in a hypothetical Ci'. +# +# C0 ----> C1 ----> C2 ----> ... ----> Cn +# | | | | +# f f f f +# | | | | +# v v v v +# C0' C1' C2' Cn' +# +# And we can also get its inverse g (applied at Ci') by reverting the commit. +# +# C0 ----> C1 ----> C2 ----> ... ----> Cn +# |^ |^ |^ |^ +# f| f| f| f| +# |g |g |g |g +# v| v| v| v| +# C0' C1' C2' Cn' +# +# Finally, we can get from C(i-1)' to Ci' by composing three arrows: +# - g at C(i-1) +# - Ci +# - f at C1 +# +# C0 ----> C1 ----> C2 ----> ... ----> Cn +# |^ |^ |^ |^ +# f| f| f| f| +# |g |g |g |g +# v| v| v| v| +# C0' ---> C1' ---> C2' ---> ... ----> Cn' + +set -x + +# edit every commit Ci, adding new commits representing f at Ci and it's inverse g +git rebase $BASE_COMMIT~1 --exec "$FORMATTING_COMMAND && git commit -am "format" && git revert HEAD --no-edit" + +# drop last commit +git reset HEAD~1 --hard + +# now for every Ci, squash with the previous and next commit (i.e. g at C(i-1) and f at Ci) +# - in sequence editor, squash lines 3, 6, 9, ... and fixup lines 4, 7, 10, ... +# - in commit message editor, drop first 9 lines (removing the commit message of the revert commit) +GIT_SEQUENCE_EDITOR='sed -i -e "3~3s/pick/squash/" -e "4~3s/pick/fixup/"' \ + GIT_EDITOR='sed -i "1,9d"' \ + git rebase --interactive $BASE_COMMIT + +# rebase onto TARGET_COMMIT. +# Annoyingly, we still have this first "format" commit that should already be +# part of the TARGET_COMMIT. So we drop it. +GIT_SEQUENCE_EDITOR='sed -i "1s/pick/drop/"' \ + git rebase --interactive $BASE_COMMIT --onto $TARGET_COMMIT + +echo "Done." +echo "Please check that the history looks as it should and all expected commits are there." From 086ebb3200dcabe8237afc26eacd7bb3af969598 Mon Sep 17 00:00:00 2001 From: Matthias Heinzel Date: Fri, 31 Jul 2020 23:52:04 +0200 Subject: [PATCH 06/23] Upgrade to ormolu 0.1.2.0 (#1145) To rebase your branch onto the changes introduced here, you can use `tools/rebase-onto-formatter.sh`. The BASE_COMMIT for that is 029ebd5458985bec3374247484d7d8703e2d4f0b. $ git rebase 029ebd5458985bec3374247484d7d8703e2d4f0b $ stack install ormolu $ tools/rebase-onto-formatter.sh develop 029ebd5458985bec3374247484d7d8703e2d4f0b * prepare to minimize formatting diffs * bump ormolu * change ormolu option (breaking) * run ormolu --- libs/api-bot/src/Network/Wire/Bot/Cache.hs | 4 +- libs/api-bot/src/Network/Wire/Bot/Crypto.hs | 6 +- .../src/Network/Wire/Bot/Crypto/Glue.hs | 2 +- libs/api-bot/src/Network/Wire/Bot/Monad.hs | 98 ++++++++-------- libs/bilge/src/Bilge/Assert.hs | 8 +- libs/bilge/src/Bilge/IO.hs | 2 +- libs/bilge/src/Bilge/RPC.hs | 6 +- libs/bilge/src/Bilge/Response.hs | 12 +- .../test/unit/Test/Brig/Roundtrip.hs | 2 +- libs/cassandra-util/src/Cassandra/Exec.hs | 3 +- libs/cassandra-util/src/Cassandra/Schema.hs | 18 +-- libs/cassandra-util/src/Cassandra/Settings.hs | 15 +-- libs/cassandra-util/src/Cassandra/Util.hs | 6 +- libs/extended/src/Servant/API/Extended.hs | 6 +- libs/galley-types/src/Galley/Types/Teams.hs | 4 +- .../test/unit/Test/Galley/Roundtrip.hs | 2 +- .../test/unit/Test/Galley/Types.hs | 11 +- .../src/Gundeck/Types/Push/V2.hs | 14 ++- libs/hscim/src/Web/Scim/ContentType.hs | 8 +- libs/hscim/src/Web/Scim/Filter.hs | 2 +- libs/hscim/src/Web/Scim/Schema/PatchOp.hs | 4 +- libs/hscim/src/Web/Scim/Schema/User.hs | 7 +- libs/hscim/src/Web/Scim/Test/Util.hs | 5 +- libs/hscim/test/Test/Schema/PatchOpSpec.hs | 2 +- libs/imports/src/Imports.hs | 12 +- .../src/Data/Metrics/Middleware/Prometheus.hs | 3 +- libs/ropes/src/Ropes/Nexmo.hs | 6 +- libs/tasty-cannon/src/Test/Tasty/Cannon.hs | 21 ++-- libs/types-common-aws/src/Util/Test/SQS.hs | 17 +-- libs/types-common/src/Data/Handle.hs | 7 +- libs/types-common/src/Data/IdMapping.hs | 2 +- libs/types-common/src/Data/Json/Util.hs | 2 +- libs/types-common/src/Data/Misc.hs | 2 +- libs/types-common/src/Data/Text/Ascii.hs | 13 +- libs/types-common/src/Util/Test.hs | 11 +- .../src/Network/Wai/Utilities/Server.hs | 15 +-- libs/wire-api/src/Wire/API/Arbitrary.hs | 2 +- libs/wire-api/src/Wire/API/Asset/V3.hs | 17 +-- .../src/Wire/API/Asset/V3/Resumable.hs | 2 +- libs/wire-api/src/Wire/API/Call/Config.hs | 18 +-- libs/wire-api/src/Wire/API/Connection.hs | 17 +-- .../src/Wire/API/Conversation/Code.hs | 2 +- .../src/Wire/API/Event/Conversation.hs | 13 +- libs/wire-api/src/Wire/API/Event/Team.hs | 12 +- libs/wire-api/src/Wire/API/Notification.hs | 2 +- .../src/Wire/API/Provider/Service/Tag.hs | 69 +++++------ libs/wire-api/src/Wire/API/Push/V2/Token.hs | 15 +-- libs/wire-api/src/Wire/API/Team/Feature.hs | 32 ++--- libs/wire-api/src/Wire/API/Team/Permission.hs | 13 +- libs/wire-api/src/Wire/API/User.hs | 2 +- libs/wire-api/src/Wire/API/User/Client.hs | 9 +- libs/wire-api/src/Wire/API/User/Identity.hs | 11 +- libs/wire-api/src/Wire/API/User/Profile.hs | 7 +- .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 2 +- .../Test/Wire/API/Roundtrip/ByteString.hs | 2 +- libs/zauth/main/Main.hs | 25 ++-- libs/zauth/src/Data/ZAuth/Creation.hs | 2 +- libs/zauth/src/Data/ZAuth/Token.hs | 54 +++++---- libs/zauth/src/Data/ZAuth/Validation.hs | 2 +- services/brig/schema/src/V42.hs | 8 +- services/brig/src/Brig/API/Client.hs | 10 +- services/brig/src/Brig/API/Connection.hs | 30 ++--- services/brig/src/Brig/API/Handler.hs | 6 +- services/brig/src/Brig/API/IdMapping.hs | 16 +-- services/brig/src/Brig/API/Internal.hs | 12 +- services/brig/src/Brig/API/Public.hs | 7 +- services/brig/src/Brig/API/User.hs | 102 ++++++++-------- services/brig/src/Brig/AWS.hs | 12 +- services/brig/src/Brig/App.hs | 32 ++--- services/brig/src/Brig/Calling/API.hs | 2 +- services/brig/src/Brig/Data/Activation.hs | 9 +- services/brig/src/Brig/Data/Client.hs | 6 +- services/brig/src/Brig/Data/Connection.hs | 13 +- services/brig/src/Brig/Data/IdMapping.hs | 5 +- services/brig/src/Brig/Data/User.hs | 51 ++++---- services/brig/src/Brig/Email.hs | 7 +- services/brig/src/Brig/IO/Intra.hs | 95 +++++++-------- services/brig/src/Brig/IO/Journal.hs | 29 ++--- services/brig/src/Brig/Index/Eval.hs | 13 +- services/brig/src/Brig/Index/Migrations.hs | 16 +-- .../brig/src/Brig/Index/Migrations/Types.hs | 2 +- .../brig/src/Brig/InternalEvent/Process.hs | 7 +- services/brig/src/Brig/Options.hs | 10 +- services/brig/src/Brig/Phone.hs | 23 ++-- services/brig/src/Brig/Provider/API.hs | 62 +++++----- services/brig/src/Brig/Provider/DB.hs | 90 +++++++------- services/brig/src/Brig/Provider/RPC.hs | 13 +- services/brig/src/Brig/Queue.hs | 2 +- services/brig/src/Brig/Queue/Stomp.hs | 41 ++++--- services/brig/src/Brig/RPC.hs | 13 +- services/brig/src/Brig/Run.hs | 12 +- services/brig/src/Brig/Team/API.hs | 2 +- services/brig/src/Brig/Team/DB.hs | 28 +++-- services/brig/src/Brig/Template.hs | 3 +- services/brig/src/Brig/Unique.hs | 8 +- services/brig/src/Brig/User/Auth.hs | 11 +- services/brig/src/Brig/User/Auth/Cookie.hs | 29 ++--- services/brig/src/Brig/User/Auth/DB/Cookie.hs | 2 +- services/brig/src/Brig/User/Handle.hs | 20 ++-- services/brig/src/Brig/User/Search/Index.hs | 72 ++++++------ services/brig/src/Brig/ZAuth.hs | 73 +++++++----- services/brig/test/integration/API/Calling.hs | 21 ++-- .../brig/test/integration/API/IdMapping.hs | 2 +- .../brig/test/integration/API/Provider.hs | 111 ++++++++---------- .../test/integration/API/RichInfo/Util.hs | 3 +- services/brig/test/integration/API/Search.hs | 10 +- services/brig/test/integration/API/Team.hs | 16 +-- .../brig/test/integration/API/Team/Util.hs | 18 +-- .../brig/test/integration/API/User/Account.hs | 19 +-- .../brig/test/integration/API/User/Auth.hs | 25 ++-- .../brig/test/integration/API/User/Client.hs | 4 +- .../test/integration/API/User/Connection.hs | 18 +-- .../brig/test/integration/API/User/Handles.hs | 8 +- .../test/integration/API/User/RichInfo.hs | 7 +- .../brig/test/integration/API/User/Util.hs | 16 +-- services/brig/test/integration/Util.hs | 9 +- services/cannon/src/Cannon/App.hs | 5 +- services/cannon/src/Cannon/Dict.hs | 8 +- services/cannon/src/Cannon/Run.hs | 12 +- services/cannon/src/Cannon/Types.hs | 7 +- services/cannon/src/Cannon/WS.hs | 17 +-- services/cannon/test/Test/Cannon/Dict.hs | 9 +- services/cargohold/src/CargoHold/API/Error.hs | 34 +++--- .../cargohold/src/CargoHold/API/Public.hs | 2 +- services/cargohold/src/CargoHold/API/V3.hs | 8 +- .../src/CargoHold/API/V3/Resumable.hs | 2 +- services/cargohold/src/CargoHold/AWS.hs | 4 +- services/cargohold/src/CargoHold/App.hs | 2 +- services/cargohold/src/CargoHold/Run.hs | 2 +- services/cargohold/src/CargoHold/S3.hs | 46 ++++---- services/cargohold/src/CargoHold/Util.hs | 7 +- services/cargohold/test/integration/API/V3.hs | 21 ++-- services/cargohold/test/integration/Main.hs | 23 ++-- .../cargohold/test/integration/TestSetup.hs | 2 +- services/federator/src/Federator/Run.hs | 4 +- .../migrate-data/src/Galley/DataMigration.hs | 6 +- services/galley/src/Galley/API/Create.hs | 47 ++++---- services/galley/src/Galley/API/IdMapping.hs | 14 +-- services/galley/src/Galley/API/Internal.hs | 11 +- services/galley/src/Galley/API/LegalHold.hs | 2 +- services/galley/src/Galley/API/Mapping.hs | 2 +- services/galley/src/Galley/API/Public.hs | 9 +- services/galley/src/Galley/API/Swagger.hs | 72 ++++++------ services/galley/src/Galley/API/Teams.hs | 26 ++-- services/galley/src/Galley/API/Update.hs | 27 +++-- services/galley/src/Galley/API/Util.hs | 13 +- services/galley/src/Galley/App.hs | 2 +- services/galley/src/Galley/Aws.hs | 8 +- services/galley/src/Galley/Data.hs | 18 +-- .../galley/src/Galley/Data/CustomBackend.hs | 5 +- services/galley/src/Galley/Data/IdMapping.hs | 5 +- services/galley/src/Galley/Data/LegalHold.hs | 13 +- .../src/Galley/Data/SearchVisibility.hs | 5 +- services/galley/src/Galley/Data/Services.hs | 2 +- .../src/Galley/Data/TeamNotifications.hs | 8 +- services/galley/src/Galley/External.hs | 25 ++-- .../src/Galley/External/LegalHoldService.hs | 42 ++++--- services/galley/src/Galley/Intra/Journal.hs | 21 ++-- services/galley/src/Galley/Intra/Push.hs | 2 +- services/galley/src/Galley/Intra/User.hs | 29 ++--- services/galley/src/Galley/Options.hs | 2 +- services/galley/src/Galley/Run.hs | 4 +- services/galley/test/integration/API.hs | 48 ++++---- .../galley/test/integration/API/IdMapping.hs | 4 +- .../test/integration/API/MessageTimer.hs | 7 +- services/galley/test/integration/API/Roles.hs | 21 ++-- services/galley/test/integration/API/SQS.hs | 38 +++--- services/galley/test/integration/API/Teams.hs | 19 +-- .../test/integration/API/Teams/Feature.hs | 21 ++-- .../test/integration/API/Teams/LegalHold.hs | 28 +++-- services/galley/test/integration/API/Util.hs | 29 +++-- .../test/integration/API/Util/TeamFeature.hs | 4 +- services/galley/test/integration/Main.hs | 23 ++-- services/gundeck/src/Gundeck/Aws.hs | 15 ++- services/gundeck/src/Gundeck/Env.hs | 14 +-- services/gundeck/src/Gundeck/Instances.hs | 9 +- services/gundeck/src/Gundeck/Monad.hs | 2 +- .../gundeck/src/Gundeck/Notification/Data.hs | 9 +- services/gundeck/src/Gundeck/Presence/Data.hs | 4 +- services/gundeck/src/Gundeck/Push.hs | 59 +++++----- services/gundeck/src/Gundeck/Push/Native.hs | 2 +- .../src/Gundeck/Push/Native/Serialise.hs | 8 +- .../gundeck/src/Gundeck/Push/Native/Types.hs | 2 +- .../gundeck/src/Gundeck/Push/Websocket.hs | 17 +-- services/gundeck/src/Gundeck/React.hs | 7 +- .../src/Gundeck/ThreadBudget/Internal.hs | 26 ++-- services/gundeck/test/integration/API.hs | 69 ++++++----- services/gundeck/test/integration/Main.hs | 23 ++-- .../gundeck/test/integration/TestSetup.hs | 2 +- services/gundeck/test/unit/MockGundeck.hs | 49 ++++---- services/gundeck/test/unit/ThreadBudget.hs | 4 +- services/proxy/src/Proxy/API/Public.hs | 30 ++--- services/proxy/src/Proxy/Env.hs | 2 +- services/spar/src/Spar/API.hs | 7 +- services/spar/src/Spar/API/Swagger.hs | 13 +- services/spar/src/Spar/App.hs | 11 +- services/spar/src/Spar/Data.hs | 16 +-- services/spar/src/Spar/Intra/Brig.hs | 51 ++++---- services/spar/src/Spar/Scim.hs | 10 +- services/spar/src/Spar/Scim/Auth.hs | 7 +- services/spar/src/Spar/Scim/Swagger.hs | 74 ++++++------ services/spar/src/Spar/Scim/Types.hs | 4 +- services/spar/src/Spar/Scim/User.hs | 59 +++++----- .../test-integration/Test/Spar/APISpec.hs | 12 +- .../Test/Spar/Scim/UserSpec.hs | 39 +++--- services/spar/test-integration/Util/Core.hs | 26 ++-- services/spar/test-integration/Util/Scim.hs | 32 ++--- services/spar/test-integration/Util/Types.hs | 6 +- services/spar/test/Test/Spar/APISpec.hs | 18 +-- stack.yaml | 4 +- stack.yaml.lock | 8 +- tools/api-simulations/loadtest/src/Main.hs | 18 +-- .../src/Network/Wire/Simulations/LoadTest.hs | 2 +- .../src/Network/Wire/Simulations/SmokeTest.hs | 33 +++--- tools/bonanza/main/Kibanana.hs | 52 ++++---- tools/bonanza/src/Bonanza/Anon.hs | 2 +- tools/bonanza/src/Bonanza/App.hs | 87 +++++++------- tools/bonanza/src/Bonanza/Parser/CommonLog.hs | 12 +- tools/bonanza/src/Bonanza/Parser/Tinylog.hs | 4 +- .../test/unit/Test/Bonanza/Arbitrary.hs | 10 +- .../test/unit/Test/Bonanza/Streaming.hs | 14 +-- tools/db/auto-whitelist/src/Work.hs | 2 +- .../billing-team-member-backfill/src/Work.hs | 2 +- tools/db/find-undead/src/Work.hs | 24 ++-- tools/db/service-backfill/src/Work.hs | 2 +- tools/ormolu.sh | 2 +- tools/stern/src/Stern/API.hs | 8 +- tools/stern/src/Stern/App.hs | 2 +- tools/stern/src/Stern/Intra.hs | 59 +++++----- tools/stern/src/Stern/Types.hs | 8 +- 230 files changed, 2079 insertions(+), 1870 deletions(-) diff --git a/libs/api-bot/src/Network/Wire/Bot/Cache.hs b/libs/api-bot/src/Network/Wire/Bot/Cache.hs index ff532c5befc..94fe9c7b9ab 100644 --- a/libs/api-bot/src/Network/Wire/Bot/Cache.hs +++ b/libs/api-bot/src/Network/Wire/Bot/Cache.hs @@ -62,7 +62,7 @@ empty :: IO Cache empty = Cache <$> newIORef [] get :: (MonadIO m, HasCallStack) => Cache -> m CachedUser -get c = liftIO $ atomicModifyIORef (cache c) $ \u -> +get c = liftIO . atomicModifyIORef (cache c) $ \u -> case u of [] -> error @@ -71,7 +71,7 @@ get c = liftIO $ atomicModifyIORef (cache c) $ \u -> (x : xs) -> (xs, x) put :: MonadIO m => Cache -> CachedUser -> m () -put c a = liftIO $ atomicModifyIORef (cache c) $ \u -> (a : u, ()) +put c a = liftIO . atomicModifyIORef (cache c) $ \u -> (a : u, ()) toUser :: HasCallStack => Logger -> [CachedUser] -> [LText] -> IO [CachedUser] toUser _ acc [i, e, p] = do diff --git a/libs/api-bot/src/Network/Wire/Bot/Crypto.hs b/libs/api-bot/src/Network/Wire/Bot/Crypto.hs index 672ec009fbe..3167ef5fb21 100644 --- a/libs/api-bot/src/Network/Wire/Bot/Crypto.hs +++ b/libs/api-bot/src/Network/Wire/Bot/Crypto.hs @@ -181,9 +181,9 @@ decryptSymmetric _ (SymmetricKeys ekey mkey) msg = liftIO $ do let (dgst, ciphertext) = BS.splitAt 32 msg sha256 <- requireMaybe (digestFromByteString dgst) "Bad MAC" let mac = hmac (toByteString' mkey) ciphertext :: HMAC SHA256 - unless (HMAC sha256 == mac) - $ throwM - $ RequirementFailed "Bad MAC" + unless (HMAC sha256 == mac) $ + throwM $ + RequirementFailed "Bad MAC" let (iv, dat) = BS.splitAt 16 ciphertext return $ unpadPKCS7 $ cbcDecrypt aes (aesIV iv) dat diff --git a/libs/api-bot/src/Network/Wire/Bot/Crypto/Glue.hs b/libs/api-bot/src/Network/Wire/Bot/Crypto/Glue.hs index 6da93041c3d..61e306f7f27 100644 --- a/libs/api-bot/src/Network/Wire/Bot/Crypto/Glue.hs +++ b/libs/api-bot/src/Network/Wire/Bot/Crypto/Glue.hs @@ -50,7 +50,7 @@ deleteBox :: UserId -> Maybe Text -> IO () deleteBox uid label = do dir <- getBoxDir uid label removePathForcibly dir -- using "forcibly" so that it wouldn't fail - -- if the directory doesn't exist + -- if the directory doesn't exist genPrekeys :: Box -> Word16 -> IO [C.Prekey] genPrekeys box n = mapM (genPrekey box) [1 .. n - 1] diff --git a/libs/api-bot/src/Network/Wire/Bot/Monad.hs b/libs/api-bot/src/Network/Wire/Bot/Monad.hs index 81dc76665d9..da01c668e79 100644 --- a/libs/api-bot/src/Network/Wire/Bot/Monad.hs +++ b/libs/api-bot/src/Network/Wire/Bot/Monad.hs @@ -187,16 +187,16 @@ initMetrics = do return m where counters = - Metrics.assertionsTotal - : Metrics.assertionsFailed - : Metrics.exceptionsTotal - : Metrics.botsCreatedNew - : Metrics.botsCreatedCached - : Metrics.eventsTotalRcvd - : Metrics.eventsTotalAckd - : Metrics.eventsTotalIgnd - : Metrics.eventsTotalMssd - : concatMap etc [(minBound :: EventType) ..] + Metrics.assertionsTotal : + Metrics.assertionsFailed : + Metrics.exceptionsTotal : + Metrics.botsCreatedNew : + Metrics.botsCreatedCached : + Metrics.eventsTotalRcvd : + Metrics.eventsTotalAckd : + Metrics.eventsTotalIgnd : + Metrics.eventsTotalMssd : + concatMap etc [(minBound :: EventType) ..] etc t = [ Metrics.eventTypeRcvd t, Metrics.eventTypeAckd t, @@ -562,9 +562,9 @@ assertEqual a b m = assertTrue :: (HasCallStack, MonadBotNet m) => Bool -> Text -> m () assertTrue b m = - whenAsserts - $ unless b - $ assertFailure m -- the 'unless' is hidden under 'whenAsserts' + whenAsserts $ + unless b $ + assertFailure m -- the 'unless' is hidden under 'whenAsserts' -- because we don't want 'b' to be evaluated -- when asserts are disabled @@ -615,7 +615,7 @@ scheduleAssert bot typ f out = whenAsserts $ do writeTQueue (botAsserts bot) (EventAssertion typ t f out callStack) writeTVar (botAssertCount bot) (n + 1) return True - unless r $ liftBotNet $ do + unless r . liftBotNet $ do incrAssertFailed runBotSession bot . log Error . msg $ "Too many event assertions. Dropped: " <> eventTypeText typ @@ -698,7 +698,7 @@ mkBot tag user pw = do return bot connectPush :: Bot -> BotNetEnv -> IO (Async ()) -connectPush bot e = runBotNet e $ runBotSession bot $ do +connectPush bot e = runBotNet e . runBotSession bot $ do log Info $ msg (val "Establishing push channel") awaitNotifications (consume bot e) @@ -725,14 +725,14 @@ heartbeat bot e = forever $ do let l = botNetLogger e -- Refresh the auth token, if necessary (auth, expiry) <- readIORef $ botAuth bot - when (now > expiry) - $ void . forkIO . runBotNet e . runBotSession bot - $ do - log Debug $ msg (val "Refreshing auth token") - refreshAuth auth - >>= maybe - (log Error $ msg (val "Failed to refresh auth token")) - setAuth + when (now > expiry) $ + void . forkIO . runBotNet e . runBotSession bot $ + do + log Debug $ msg (val "Refreshing auth token") + refreshAuth auth + >>= maybe + (log Error $ msg (val "Failed to refresh auth token")) + setAuth -- Event & assertion maintenance when (botNetAssert e) $ do -- Remove old events from the inbox @@ -742,17 +742,18 @@ heartbeat bot e = forever $ do msg ("Event Timeout: " <> showEventType evt) -- Check if the event inbox is full and if so, log a warning size <- fst <$> readTVarIO (botEvents bot) - when (size == botMaxEvents (botSettings bot)) - $ botLog l bot Warn - $ msg (val "Event inbox full!") + when (size == botMaxEvents (botSettings bot)) $ + botLog l bot Warn $ + msg (val "Event inbox full!") -- Remove old assertions from the backlog asserts <- atomically $ gcBacklog bot now forM_ asserts $ \(EventAssertion typ _ _ out stack) -> do for_ out $ liftIO . atomically . flip tryPutTMVar Nothing - botLog l bot Warn $ msg $ - "Assertion Timeout: " <> eventTypeText typ - <> "\nAssertion was created at: " - <> pack (prettyCallStack stack) + botLog l bot Warn $ + msg $ + "Assertion Timeout: " <> eventTypeText typ + <> "\nAssertion was created at: " + <> pack (prettyCallStack stack) -- Re-establish the push connection, if it died push <- maybe (return Nothing) poll =<< readIORef (botPushThread bot) case push of @@ -922,24 +923,25 @@ incrEventsMssd b e = HashMap.insertWith (+) (Metrics.eventTypeMssd e) 1 transferBotMetrics :: MonadBotNet m => Bot -> m () -transferBotMetrics b = getMetrics >>= \m -> liftIO $ do - -- Obtain current values - l@[rcvd, ackd, ignd, mssd] <- atomically $ do - rcvd <- readTVar $ botEventsRcvd (botMetrics b) - ackd <- readTVar $ botEventsAckd (botMetrics b) - ignd <- readTVar $ botEventsIgnd (botMetrics b) - mssd <- readTVar $ botEventsMssd (botMetrics b) - return [rcvd, ackd, ignd, mssd] - -- Update per event type counters - let add (p, n) = Metrics.counterAdd n p m - mapM_ add (concatMap HashMap.toList l) - -- Update Totals - add (Metrics.eventsTotalRcvd, sum rcvd) - add (Metrics.eventsTotalAckd, sum ackd) - add (Metrics.eventsTotalIgnd, sum ignd) - let s = sum mssd - add (Metrics.eventsTotalMssd, s) - add (Metrics.assertionsFailed, s) +transferBotMetrics b = + getMetrics >>= \m -> liftIO $ do + -- Obtain current values + l@[rcvd, ackd, ignd, mssd] <- atomically $ do + rcvd <- readTVar $ botEventsRcvd (botMetrics b) + ackd <- readTVar $ botEventsAckd (botMetrics b) + ignd <- readTVar $ botEventsIgnd (botMetrics b) + mssd <- readTVar $ botEventsMssd (botMetrics b) + return [rcvd, ackd, ignd, mssd] + -- Update per event type counters + let add (p, n) = Metrics.counterAdd n p m + mapM_ add (concatMap HashMap.toList l) + -- Update Totals + add (Metrics.eventsTotalRcvd, sum rcvd) + add (Metrics.eventsTotalAckd, sum ackd) + add (Metrics.eventsTotalIgnd, sum ignd) + let s = sum mssd + add (Metrics.eventsTotalMssd, s) + add (Metrics.assertionsFailed, s) ------------------------------------------------------------------------------- -- Logging diff --git a/libs/bilge/src/Bilge/Assert.hs b/libs/bilge/src/Bilge/Assert.hs index f2bfe648ddf..472117d11ff 100644 --- a/libs/bilge/src/Bilge/Assert.hs +++ b/libs/bilge/src/Bilge/Assert.hs @@ -85,9 +85,11 @@ io String diff --git a/libs/bilge/src/Bilge/IO.hs b/libs/bilge/src/Bilge/IO.hs index 221a3552d84..0a794cb2691 100644 --- a/libs/bilge/src/Bilge/IO.hs +++ b/libs/bilge/src/Bilge/IO.hs @@ -205,7 +205,7 @@ instance MonadBaseControl IO (HttpT IO) where instance MonadUnliftIO m => MonadUnliftIO (HttpT m) where withRunInIO inner = - HttpT $ ReaderT $ \r -> + HttpT . ReaderT $ \r -> withRunInIO $ \run -> inner (run . runHttpT r) diff --git a/libs/bilge/src/Bilge/RPC.hs b/libs/bilge/src/Bilge/RPC.hs index b671eabf961..50d8a6507b4 100644 --- a/libs/bilge/src/Bilge/RPC.hs +++ b/libs/bilge/src/Bilge/RPC.hs @@ -109,9 +109,9 @@ statusCheck :: Response (Maybe LByteString) -> m () statusCheck c f r = - unless (statusCode r == c) - $ throwError - $ f ("unexpected status code: " <> pack (show $ statusCode r)) + unless (statusCode r == c) $ + throwError $ + f ("unexpected status code: " <> pack (show $ statusCode r)) parseResponse :: (Exception e, MonadThrow m, Monad m, FromJSON a) => diff --git a/libs/bilge/src/Bilge/Response.hs b/libs/bilge/src/Bilge/Response.hs index 9aba3b8446d..5b6e373cba4 100644 --- a/libs/bilge/src/Bilge/Response.hs +++ b/libs/bilge/src/Bilge/Response.hs @@ -80,12 +80,12 @@ getCookieValue :: ByteString -> Response a -> Maybe ByteString getCookieValue cookieName resp = resp ^? to responseHeaders - . traversed -- Over each header - . filtered ((== "Set-Cookie") . fst) -- Select the cookie headers by name - . _2 -- Select Set-Cookie values - . to parseSetCookie - . filtered ((== cookieName) . setCookieName) -- Select only the cookie we want - . to setCookieValue -- extract the cookie value + . traversed -- Over each header + . filtered ((== "Set-Cookie") . fst) -- Select the cookie headers by name + . _2 -- Select Set-Cookie values + . to parseSetCookie + . filtered ((== cookieName) . setCookieName) -- Select only the cookie we want + . to setCookieValue -- extract the cookie value type ResponseLBS = Response (Maybe LByteString) diff --git a/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs b/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs index 3bb3ac81d17..937baf53744 100644 --- a/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs +++ b/libs/brig-types/test/unit/Test/Brig/Roundtrip.hs @@ -21,7 +21,7 @@ import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) import Data.Aeson.Types (parseEither) import Imports import Test.Tasty (TestTree) -import Test.Tasty.QuickCheck ((===), Arbitrary, counterexample, testProperty) +import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===)) import Type.Reflection (typeRep) testRoundTrip :: diff --git a/libs/cassandra-util/src/Cassandra/Exec.hs b/libs/cassandra-util/src/Cassandra/Exec.hs index 711b2beda91..b8f594e0842 100644 --- a/libs/cassandra-util/src/Cassandra/Exec.hs +++ b/libs/cassandra-util/src/Cassandra/Exec.hs @@ -34,9 +34,10 @@ import Cassandra.CQL (Consistency, R) import Control.Monad.Catch import Data.Conduit -- Things we just import and re-export. -import Database.CQL.IO as C (BatchM, Client, ClientState, MonadClient, Page (..), PrepQuery, Row, addPrepQuery, addQuery, adjustConsistency, adjustResponseTimeout, adjustSendTimeout, batch, emptyPage, init, liftClient, localState, paginate, prepared, query, query1, queryString, retry, runClient, schema, setConsistency, setSerialConsistency, setType, shutdown, trans, write) + -- We only use these locally. import Database.CQL.IO (RetrySettings, RunQ, defRetrySettings, eagerRetrySettings) +import Database.CQL.IO as C (BatchM, Client, ClientState, MonadClient, Page (..), PrepQuery, Row, addPrepQuery, addQuery, adjustConsistency, adjustResponseTimeout, adjustSendTimeout, batch, emptyPage, init, liftClient, localState, paginate, prepared, query, query1, queryString, retry, runClient, schema, setConsistency, setSerialConsistency, setType, shutdown, trans, write) import Database.CQL.Protocol (Error, QueryParams (QueryParams), Tuple) import Imports hiding (init) diff --git a/libs/cassandra-util/src/Cassandra/Schema.hs b/libs/cassandra-util/src/Cassandra/Schema.hs index 64243832540..9abf9b4ba27 100644 --- a/libs/cassandra-util/src/Cassandra/Schema.hs +++ b/libs/cassandra-util/src/Cassandra/Schema.hs @@ -125,12 +125,12 @@ schemaVersion = catch (fmap runIdentity <$> qry) h versionCheck :: Int32 -> Client () versionCheck v = do v' <- schemaVersion - unless (Just v <= v') - $ error - $ "Schema Version too old! Expecting at least: " - <> show v - <> ", but got: " - <> fromMaybe "" (show <$> v') + unless (Just v <= v') $ + error $ + "Schema Version too old! Expecting at least: " + <> show v + <> ", but got: " + <> fromMaybe "" (show <$> v') createKeyspace :: Keyspace -> ReplicationStrategy -> Client () createKeyspace (Keyspace k) rs = void $ schema (cql rs) (params All ()) @@ -165,8 +165,8 @@ migrateSchema :: Log.Logger -> MigrationOpts -> [Migration] -> IO () migrateSchema l o ms = do hosts <- initialContactsPlain $ pack (migHost o) p <- - CQL.init - $ setLogger (CT.mkLogger l) + CQL.init $ + setLogger (CT.mkLogger l) . setContacts (NonEmpty.head hosts) (NonEmpty.tail hosts) . setPortNumber (fromIntegral $ migPort o) . setMaxConnections 1 @@ -183,7 +183,7 @@ migrateSchema l o ms = do . setSendTimeout 20 . setResponseTimeout 50 . setProtocolVersion V4 - $ defSettings + $ defSettings runClient p $ do let keyspace = Keyspace . migKeyspace $ o when (migReset o) $ do diff --git a/libs/cassandra-util/src/Cassandra/Settings.hs b/libs/cassandra-util/src/Cassandra/Settings.hs index eab36353dc1..f11c0b7a6c2 100644 --- a/libs/cassandra-util/src/Cassandra/Settings.hs +++ b/libs/cassandra-util/src/Cassandra/Settings.hs @@ -46,13 +46,14 @@ initialContactsDisco (pack -> srv) url = liftIO $ do Nothing -> [srv, srv <> "_seed"] Just _ -> [srv] -- requesting only seeds is a valid use-case let ip = - rs ^.. responseBody - . key "roles" - . members - . indices (`elem` srvs) - . values - . key "privateIpAddress" - . _String + rs + ^.. responseBody + . key "roles" + . members + . indices (`elem` srvs) + . values + . key "privateIpAddress" + . _String & map unpack case ip of i : ii -> return (i :| ii) diff --git a/libs/cassandra-util/src/Cassandra/Util.hs b/libs/cassandra-util/src/Cassandra/Util.hs index 828c02e5706..348c1fa0f2a 100644 --- a/libs/cassandra-util/src/Cassandra/Util.hs +++ b/libs/cassandra-util/src/Cassandra/Util.hs @@ -38,9 +38,9 @@ writeTimeToUTC = posixSecondsToUTCTime . fromIntegral . (`div` 1000000) defInitCassandra :: Text -> Text -> Word16 -> Log.Logger -> IO ClientState defInitCassandra ks h p lg = - init - $ setLogger (CT.mkLogger lg) + init $ + setLogger (CT.mkLogger lg) . setPortNumber (fromIntegral p) . setContacts (unpack h) [] . setKeyspace (Keyspace ks) - $ defSettings + $ defSettings diff --git a/libs/extended/src/Servant/API/Extended.hs b/libs/extended/src/Servant/API/Extended.hs index 9b8a7c1c70c..5122f4f737f 100644 --- a/libs/extended/src/Servant/API/Extended.hs +++ b/libs/extended/src/Servant/API/Extended.hs @@ -92,9 +92,9 @@ instance -- See also "W3C Internet Media Type registration, consistency of use" -- http://www.w3.org/2001/tag/2002/0129-mime let contentTypeH = - fromMaybe "application/octet-stream" - $ lookup hContentType - $ requestHeaders request + fromMaybe "application/octet-stream" $ + lookup hContentType $ + requestHeaders request case canHandleCTypeH (Proxy :: Proxy list) (cs contentTypeH) :: Maybe (BL.ByteString -> Either String a) of Nothing -> delayedFail err415 Just f -> return f diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index bfcf6c6aa05..7aba8945405 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -126,7 +126,7 @@ module Galley.Types.Teams where import Control.Exception (ErrorCall (ErrorCall)) -import Control.Lens ((^.), makeLenses, view) +import Control.Lens (makeLenses, view, (^.)) import Control.Monad.Catch import Data.Aeson import Data.Id (UserId) @@ -137,8 +137,8 @@ import qualified Data.Set as Set import Data.String.Conversions (cs) import Imports import Wire.API.Event.Team -import Wire.API.Team (NewTeam (..), Team (..), TeamBinding (..)) import Wire.API.Team +import Wire.API.Team (NewTeam (..), Team (..), TeamBinding (..)) import Wire.API.Team.Conversation import Wire.API.Team.Feature import Wire.API.Team.Member diff --git a/libs/galley-types/test/unit/Test/Galley/Roundtrip.hs b/libs/galley-types/test/unit/Test/Galley/Roundtrip.hs index 71b733c6d69..095ca336c14 100644 --- a/libs/galley-types/test/unit/Test/Galley/Roundtrip.hs +++ b/libs/galley-types/test/unit/Test/Galley/Roundtrip.hs @@ -21,7 +21,7 @@ import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON) import Data.Aeson.Types (parseEither) import Imports import Test.Tasty (TestTree) -import Test.Tasty.QuickCheck ((===), Arbitrary, counterexample, testProperty) +import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===)) import Type.Reflection (typeRep) testRoundTrip :: diff --git a/libs/galley-types/test/unit/Test/Galley/Types.hs b/libs/galley-types/test/unit/Test/Galley/Types.hs index 5c8fd8336e4..bee8c4c8a20 100644 --- a/libs/galley-types/test/unit/Test/Galley/Types.hs +++ b/libs/galley-types/test/unit/Test/Galley/Types.hs @@ -38,14 +38,13 @@ tests = "Tests" [ testCase "owner has all permissions" $ rolePermissions RoleOwner @=? fullPermissions, - testCase "smaller roles (further to the left/top in the type def) are strictly more powerful" - $ + testCase "smaller roles (further to the left/top in the type def) are strictly more powerful" $ -- we may not want to maintain this property in the future when adding more roles, but for -- now it's true, and it's nice to have that written down somewhere. - forM_ [(r1, r2) | r1 <- [minBound ..], r2 <- drop 1 [r1 ..]] - $ \(r1, r2) -> do - assertBool "owner.self" ((rolePermissions r2 ^. self) `isSubsetOf` (rolePermissions r1 ^. self)) - assertBool "owner.copy" ((rolePermissions r2 ^. copy) `isSubsetOf` (rolePermissions r1 ^. copy)), + forM_ [(r1, r2) | r1 <- [minBound ..], r2 <- drop 1 [r1 ..]] $ + \(r1, r2) -> do + assertBool "owner.self" ((rolePermissions r2 ^. self) `isSubsetOf` (rolePermissions r1 ^. self)) + assertBool "owner.copy" ((rolePermissions r2 ^. copy) `isSubsetOf` (rolePermissions r1 ^. copy)), testCase "permissions for viewing feature flags" $ -- We currently (at the time of writing this test) grant view permissions for all -- 'TeamFeatureName's to all roles. If we add more features in the future and forget to diff --git a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs index a4e6d4902a7..c59537aff06 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs +++ b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs @@ -155,14 +155,16 @@ instance ToJSON Recipient where -- "All clients" is encoded in the API as an empty list. instance FromJSON RecipientClients where - parseJSON x = parseJSON @[ClientId] x >>= \case - [] -> pure RecipientClientsAll - c : cs -> pure (RecipientClientsSome (list1 c cs)) + parseJSON x = + parseJSON @[ClientId] x >>= \case + [] -> pure RecipientClientsAll + c : cs -> pure (RecipientClientsSome (list1 c cs)) instance ToJSON RecipientClients where - toJSON = toJSON . \case - RecipientClientsAll -> [] - RecipientClientsSome cs -> toList cs + toJSON = + toJSON . \case + RecipientClientsAll -> [] + RecipientClientsSome cs -> toList cs ----------------------------------------------------------------------------- -- ApsData diff --git a/libs/hscim/src/Web/Scim/ContentType.hs b/libs/hscim/src/Web/Scim/ContentType.hs index 7527123e854..60cc857037d 100644 --- a/libs/hscim/src/Web/Scim/ContentType.hs +++ b/libs/hscim/src/Web/Scim/ContentType.hs @@ -38,10 +38,10 @@ data SCIM instance Accept SCIM where contentTypes _ = "application" // "scim+json" /: ("charset", "utf-8") - :| "application" // "scim+json" - : "application" // "json" /: ("charset", "utf-8") - : "application" // "json" - : [] + :| "application" // "scim+json" : + "application" // "json" /: ("charset", "utf-8") : + "application" // "json" : + [] instance ToJSON a => MimeRender SCIM a where mimeRender _ = mimeRender (Proxy @JSON) diff --git a/libs/hscim/src/Web/Scim/Filter.hs b/libs/hscim/src/Web/Scim/Filter.hs index 32589b0f74b..6a910399b5b 100644 --- a/libs/hscim/src/Web/Scim/Filter.hs +++ b/libs/hscim/src/Web/Scim/Filter.hs @@ -56,7 +56,7 @@ module Web.Scim.Filter ) where -import Control.Applicative ((<|>), optional) +import Control.Applicative (optional, (<|>)) import Data.Aeson as Aeson import Data.Aeson.Parser as Aeson import Data.Aeson.Text as Aeson diff --git a/libs/hscim/src/Web/Scim/Schema/PatchOp.hs b/libs/hscim/src/Web/Scim/Schema/PatchOp.hs index 893aba422f2..b44fe9f7a30 100644 --- a/libs/hscim/src/Web/Scim/Schema/PatchOp.hs +++ b/libs/hscim/src/Web/Scim/Schema/PatchOp.hs @@ -20,12 +20,12 @@ module Web.Scim.Schema.PatchOp where import Control.Applicative import Control.Monad (guard) import Control.Monad.Except -import Data.Aeson.Types ((.:), (.:?), (.=), FromJSON (parseJSON), ToJSON (toJSON), Value (String), object, withObject, withText) +import Data.Aeson.Types (FromJSON (parseJSON), ToJSON (toJSON), Value (String), object, withObject, withText, (.:), (.:?), (.=)) import qualified Data.Aeson.Types as Aeson import Data.Attoparsec.ByteString (Parser, endOfInput, parseOnly) import Data.Bifunctor (first) -import qualified Data.HashMap.Strict as HashMap import qualified Data.HashMap.Strict as HM +import qualified Data.HashMap.Strict as HashMap import Data.Text (Text, toCaseFold, toLower) import Data.Text.Encoding (encodeUtf8) import Web.Scim.AttrName (AttrName (..)) diff --git a/libs/hscim/src/Web/Scim/Schema/User.hs b/libs/hscim/src/Web/Scim/Schema/User.hs index 9786a6660b4..c16aa8709d5 100644 --- a/libs/hscim/src/Web/Scim/Schema/User.hs +++ b/libs/hscim/src/Web/Scim/Schema/User.hs @@ -182,9 +182,10 @@ instance FromJSON (UserExtra tag) => FromJSON (User tag) where parseJSON = withObject "User" $ \obj -> do -- Lowercase all fields let o = HM.fromList . map (over _1 toLower) . HM.toList $ obj - schemas <- o .:? "schemas" <&> \case - Nothing -> [User20] - Just xs -> if User20 `elem` xs then xs else User20 : xs + schemas <- + o .:? "schemas" <&> \case + Nothing -> [User20] + Just xs -> if User20 `elem` xs then xs else User20 : xs userName <- o .: "username" externalId <- o .:? "externalid" name <- o .:? "name" diff --git a/libs/hscim/src/Web/Scim/Test/Util.hs b/libs/hscim/src/Web/Scim/Test/Util.hs index fef6c1f97d3..38feba2a0ef 100644 --- a/libs/hscim/src/Web/Scim/Test/Util.hs +++ b/libs/hscim/src/Web/Scim/Test/Util.hs @@ -52,7 +52,7 @@ where import qualified Control.Retry as Retry import Data.Aeson -import Data.Aeson.Internal ((), JSONPathElement (Key)) +import Data.Aeson.Internal (JSONPathElement (Key), ()) import Data.Aeson.QQ import Data.ByteString (ByteString) import qualified Data.ByteString as BS @@ -71,8 +71,7 @@ import Network.Wai (Application) import Network.Wai.Test (SResponse) import Test.Hspec.Expectations (expectationFailure) import Test.Hspec.Wai hiding (patch, post, put, shouldRespondWith) -import Test.Hspec.Wai.Matcher (bodyEquals) -import Test.Hspec.Wai.Matcher (match) +import Test.Hspec.Wai.Matcher (bodyEquals, match) import Web.Scim.Class.Auth (AuthTypes (..)) import Web.Scim.Class.Group (GroupTypes (..)) import Web.Scim.Schema.Schema (Schema (CustomSchema, User20)) diff --git a/libs/hscim/test/Test/Schema/PatchOpSpec.hs b/libs/hscim/test/Test/Schema/PatchOpSpec.hs index 1fc7240bc1c..49d46331134 100644 --- a/libs/hscim/test/Test/Schema/PatchOpSpec.hs +++ b/libs/hscim/test/Test/Schema/PatchOpSpec.hs @@ -21,8 +21,8 @@ module Test.Schema.PatchOpSpec where import qualified Data.Aeson as Aeson -import qualified Data.Aeson.Types as Aeson import Data.Aeson.Types (Result (Error, Success), Value (String), fromJSON, toJSON) +import qualified Data.Aeson.Types as Aeson import Data.Attoparsec.ByteString (parseOnly) import Data.Either (isLeft) import Data.Foldable (for_) diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index 85d3c80bffb..223089de63a 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -121,7 +121,7 @@ where -- with e.g. UnliftIO modules import Control.Applicative hiding (empty, many, optional, some) -- common in - -- some libs +-- some libs -- conflicts with Options.Applicative.Option (should we care?) -- First and Last are going to be deprecated. Use Semigroup instead @@ -191,10 +191,8 @@ import UnliftIO.IO hiding (Handle, getMonotonicTime) import UnliftIO.IORef import UnliftIO.MVar import UnliftIO.STM -import qualified Prelude as P import Prelude - ( ($!), - Bounded (..), + ( Bounded (..), Double, Enum (..), Eq (..), @@ -215,8 +213,6 @@ import Prelude RealFrac (..), Show (..), ShowS, - (^), - (^^), error, even, fromIntegral, @@ -235,7 +231,11 @@ import Prelude shows, subtract, undefined, + ($!), + (^), + (^^), ) +import qualified Prelude as P ---------------------------------------------------------------------------- -- Type aliases diff --git a/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs b/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs index e6e19b80465..b35e502da28 100644 --- a/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs +++ b/libs/metrics-wai/src/Data/Metrics/Middleware/Prometheus.hs @@ -21,8 +21,7 @@ module Data.Metrics.Middleware.Prometheus where import Data.Maybe (fromMaybe) -import Data.Metrics.Types (Paths) -import Data.Metrics.Types (treeLookup) +import Data.Metrics.Types (Paths, treeLookup) import Data.Metrics.WaiRoute (treeToPaths) import Data.Text (Text) import qualified Data.Text.Encoding as T diff --git a/libs/ropes/src/Ropes/Nexmo.hs b/libs/ropes/src/Ropes/Nexmo.hs index 11cd3a98990..7ec1e976432 100644 --- a/libs/ropes/src/Ropes/Nexmo.hs +++ b/libs/ropes/src/Ropes/Nexmo.hs @@ -319,9 +319,9 @@ sendFeedback cr mgr fb = httpLbs req mgr >>= parseResponse -- You must _always_ specify a timestamp nexmoTimeFormat = formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S" parseResponse res = - unless (responseStatus res == status200) - $ throwIO - $ FeedbackErrorResponse (decodeUtf8 . toStrict . responseBody $ res) + unless (responseStatus res == status200) $ + throwIO $ + FeedbackErrorResponse (decodeUtf8 . toStrict . responseBody $ res) sendMessage :: Credentials -> Manager -> Message -> IO MessageResponse sendMessage cr mgr msg = N.head <$> sendMessages cr mgr (msg :| []) diff --git a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs index 9423da4fcaa..c40cef37ef2 100644 --- a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs +++ b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs @@ -75,7 +75,7 @@ import Data.ByteString.Conversion import Data.Id import Data.List1 import Data.Misc ((<$$>)) -import Data.Timeout ((#), Timeout, TimeoutUnit (..)) +import Data.Timeout (Timeout, TimeoutUnit (..), (#)) import Gundeck.Types import Imports import Network.HTTP.Client @@ -240,15 +240,16 @@ awaitMatch t ws match = go [] [] go buf errs = do mn <- await t ws case mn of - Just n -> do - liftIO (match n) - refill buf - return (Right n) - `catchAll` \e -> case asyncExceptionFromException e of - Just x -> throwM (x :: SomeAsyncException) - Nothing -> - let e' = MatchFailure e - in go (n : buf) (e' : errs) + Just n -> + do + liftIO (match n) + refill buf + return (Right n) + `catchAll` \e -> case asyncExceptionFromException e of + Just x -> throwM (x :: SomeAsyncException) + Nothing -> + let e' = MatchFailure e + in go (n : buf) (e' : errs) Nothing -> do refill buf return (Left (MatchTimeout errs)) diff --git a/libs/types-common-aws/src/Util/Test/SQS.hs b/libs/types-common-aws/src/Util/Test/SQS.hs index 4ffa22f3a81..dddaa98d67d 100644 --- a/libs/types-common-aws/src/Util/Test/SQS.hs +++ b/libs/types-common-aws/src/Util/Test/SQS.hs @@ -91,8 +91,8 @@ receive :: Int -> Text -> SQS.ReceiveMessage receive n url = SQS.receiveMessage url & set SQS.rmWaitTimeSeconds (Just 1) - . set SQS.rmMaxNumberOfMessages (Just n) - . set SQS.rmVisibilityTimeout (Just 1) + . set SQS.rmMaxNumberOfMessages (Just n) + . set SQS.rmVisibilityTimeout (Just 1) fetchMessage :: (MonadIO m, AWS.MonadAWS m, Message a) => Text -> String -> (String -> Maybe a -> IO ()) -> m () fetchMessage url label callback = do @@ -140,9 +140,10 @@ tryMatch label tries url callback = go tries when (null ok) $ do liftIO $ threadDelay (10 ^ (6 :: Int)) go (n - 1) - check e = do - liftIO $ callback label e - return (Right $ show e) - `catchAll` \ex -> case asyncExceptionFromException ex of - Just x -> throwM (x :: SomeAsyncException) - Nothing -> return . Left $ MatchFailure (e, ex) + check e = + do + liftIO $ callback label e + return (Right $ show e) + `catchAll` \ex -> case asyncExceptionFromException ex of + Just x -> throwM (x :: SomeAsyncException) + Nothing -> return . Left $ MatchFailure (e, ex) diff --git a/libs/types-common/src/Data/Handle.hs b/libs/types-common/src/Data/Handle.hs index 6ee2059a031..81699f9f3b7 100644 --- a/libs/types-common/src/Data/Handle.hs +++ b/libs/types-common/src/Data/Handle.hs @@ -83,6 +83,7 @@ handleParser = do isHandleChar = Atto.inClass "a-z0-9_.-" instance Arbitrary Handle where - arbitrary = Handle . Text.pack <$> do - len <- oneof [choose (2, 10), choose (2, 256)] -- prefer short handles - replicateM len (elements $ ['a' .. 'z'] <> ['0' .. '9'] <> "_-.") + arbitrary = + Handle . Text.pack <$> do + len <- oneof [choose (2, 10), choose (2, 256)] -- prefer short handles + replicateM len (elements $ ['a' .. 'z'] <> ['0' .. '9'] <> "_-.") diff --git a/libs/types-common/src/Data/IdMapping.hs b/libs/types-common/src/Data/IdMapping.hs index 0eac38efec3..41db2cced9f 100644 --- a/libs/types-common/src/Data/IdMapping.hs +++ b/libs/types-common/src/Data/IdMapping.hs @@ -19,7 +19,7 @@ module Data.IdMapping where -import Data.Aeson ((.=), ToJSON (toJSON)) +import Data.Aeson (ToJSON (toJSON), (.=)) import qualified Data.Aeson as Aeson import qualified Data.ByteString as BS import Data.Domain (domainText) diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index 03d8a495d4c..b2479a4e48b 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -36,7 +36,7 @@ module Data.Json.Util where import qualified Cassandra as CQL -import Control.Lens ((%~), coerced) +import Control.Lens (coerced, (%~)) import Data.Aeson import Data.Aeson.Types import qualified Data.ByteString.Base64.Lazy as EL diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index 2e3301fff8e..b58c715e2ac 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -61,7 +61,7 @@ module Data.Misc where import Cassandra -import Control.Lens ((.~), (^.), makeLenses) +import Control.Lens (makeLenses, (.~), (^.)) import Data.Aeson import qualified Data.Aeson.Types as Json import qualified Data.Attoparsec.ByteString.Char8 as Chars diff --git a/libs/types-common/src/Data/Text/Ascii.hs b/libs/types-common/src/Data/Text/Ascii.hs index 7ed284749a1..f759d9d4bcc 100644 --- a/libs/types-common/src/Data/Text/Ascii.hs +++ b/libs/types-common/src/Data/Text/Ascii.hs @@ -371,12 +371,13 @@ check m f t | otherwise = Left m parseBytes :: (Text -> Either String a) -> Parser a -parseBytes f = parser >>= \bs -> - case decodeUtf8' bs of - Left _ -> fail $ "Invalid ASCII characters in: " ++ C8.unpack bs - Right t -> case f t of - Left e -> fail $ e ++ ": " ++ Text.unpack t - Right a -> pure a +parseBytes f = + parser >>= \bs -> + case decodeUtf8' bs of + Left _ -> fail $ "Invalid ASCII characters in: " ++ C8.unpack bs + Right t -> case f t of + Left e -> fail $ e ++ ": " ++ Text.unpack t + Right a -> pure a unsafeString :: (Text -> Either String a) -> String -> a unsafeString f s = case f (Text.pack s) of diff --git a/libs/types-common/src/Util/Test.hs b/libs/types-common/src/Util/Test.hs index 3123ce652b5..69b5d8deb6a 100644 --- a/libs/types-common/src/Util/Test.hs +++ b/libs/types-common/src/Util/Test.hs @@ -35,11 +35,12 @@ instance IsOption IntegrationConfigFile where optionName = return "integration-config" optionHelp = return "Integration config file to read from" optionCLParser = - fmap IntegrationConfigFile $ strOption $ - ( short (untag (return 'i' :: Tagged IntegrationConfigFile Char)) - <> long (untag (optionName :: Tagged IntegrationConfigFile String)) - <> help (untag (optionHelp :: Tagged IntegrationConfigFile String)) - ) + fmap IntegrationConfigFile $ + strOption $ + ( short (untag (return 'i' :: Tagged IntegrationConfigFile Char)) + <> long (untag (optionName :: Tagged IntegrationConfigFile String)) + <> help (untag (optionHelp :: Tagged IntegrationConfigFile String)) + ) handleParseError :: (Show a) => Either a b -> IO (Maybe b) handleParseError (Left err) = do diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs index 901da290a5a..913d09341f6 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs @@ -99,14 +99,14 @@ newSettings (Server h p l m t) = do -- (Atomically) initialise the standard metrics, to avoid races. void $ gaugeGet (path "net.connections") m void $ counterGet (path "net.errors") m - return - $ setHost (fromString h) + return $ + setHost (fromString h) . setPort (fromIntegral p) . setBeforeMainLoop logStart . setOnOpen (const $ connStart >> return True) . setOnClose (const connEnd) . setTimeout (fromMaybe 300 t) - $ defaultSettings + $ defaultSettings where connStart = gaugeIncr (path "net.connections") m connEnd = gaugeDecr (path "net.connections") m @@ -357,10 +357,11 @@ runHandlers e [] = throw e runHandlers e (Handler h : hs) = maybe (runHandlers e hs) h (fromException e) restrict :: Int -> Int -> Predicate r P.Error Int -> Predicate r P.Error Int -restrict l u = fmap $ \x -> x >>= \v -> - if v >= l && v <= u - then x - else Fail (setMessage (emsg v) . setReason TypeError $ e400) +restrict l u = fmap $ \x -> + x >>= \v -> + if v >= l && v <= u + then x + else Fail (setMessage (emsg v) . setReason TypeError $ e400) where emsg v = LBS.toStrict . toLazyByteString $ diff --git a/libs/wire-api/src/Wire/API/Arbitrary.hs b/libs/wire-api/src/Wire/API/Arbitrary.hs index 5558eec1889..d1621f02a48 100644 --- a/libs/wire-api/src/Wire/API/Arbitrary.hs +++ b/libs/wire-api/src/Wire/API/Arbitrary.hs @@ -43,8 +43,8 @@ import Data.List1 (List1, list1) import qualified Data.Map.Strict as Map import qualified Data.Set as Set import GHC.Generics (Rep) +import Generic.Random (listOf', (:+) ((:+))) import qualified Generic.Random as Generic -import Generic.Random ((:+) ((:+)), listOf') import Imports import Test.QuickCheck.Arbitrary (Arbitrary (arbitrary)) import qualified Test.QuickCheck.Arbitrary as QC diff --git a/libs/wire-api/src/Wire/API/Asset/V3.hs b/libs/wire-api/src/Wire/API/Asset/V3.hs index 748a13ff6bc..fea687bc4a0 100644 --- a/libs/wire-api/src/Wire/API/Asset/V3.hs +++ b/libs/wire-api/src/Wire/API/Asset/V3.hs @@ -68,7 +68,7 @@ import Data.ByteString.Builder import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as LBS import Data.Id -import Data.Json.Util ((#), toUTCTimeMillis) +import Data.Json.Util (toUTCTimeMillis, (#)) import Data.Text.Ascii (AsciiBase64Url) import qualified Data.Text.Encoding as T import Data.Time.Clock @@ -303,13 +303,14 @@ instance ToByteString AssetRetention where -- | ByteString representation is used in AssetKey instance FromByteString AssetRetention where - parser = decimal >>= \d -> case (d :: Word) of - 1 -> return AssetEternal - 2 -> return AssetPersistent - 3 -> return AssetVolatile - 4 -> return AssetEternalInfrequentAccess - 5 -> return AssetExpiring - _ -> fail $ "Invalid asset retention: " ++ show d + parser = + decimal >>= \d -> case (d :: Word) of + 1 -> return AssetEternal + 2 -> return AssetPersistent + 3 -> return AssetVolatile + 4 -> return AssetEternalInfrequentAccess + 5 -> return AssetExpiring + _ -> fail $ "Invalid asset retention: " ++ show d instance ToJSON AssetRetention where toJSON = String . retentionToTextRep diff --git a/libs/wire-api/src/Wire/API/Asset/V3/Resumable.hs b/libs/wire-api/src/Wire/API/Asset/V3/Resumable.hs index 71aebb10aa7..15778704c4e 100644 --- a/libs/wire-api/src/Wire/API/Asset/V3/Resumable.hs +++ b/libs/wire-api/src/Wire/API/Asset/V3/Resumable.hs @@ -46,7 +46,7 @@ import Control.Lens (makeLenses) import Data.Aeson import Data.Aeson.Types import Data.ByteString.Conversion -import Data.Json.Util ((#), toUTCTimeMillis) +import Data.Json.Util (toUTCTimeMillis, (#)) import Data.Time.Clock import Imports import Wire.API.Arbitrary (Arbitrary, GenericUniform (..)) diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index 4bf3013c188..e7ccbcd6f5e 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -282,10 +282,11 @@ instance BC.ToByteString Scheme where builder SchemeTurns = "turns" instance BC.FromByteString Scheme where - parser = BC.parser >>= \t -> case (t :: ByteString) of - "turn" -> pure SchemeTurn - "turns" -> pure SchemeTurns - _ -> fail $ "Invalid turn scheme: " ++ show t + parser = + BC.parser >>= \t -> case (t :: ByteString) of + "turn" -> pure SchemeTurn + "turns" -> pure SchemeTurns + _ -> fail $ "Invalid turn scheme: " ++ show t instance ToJSON Scheme where toJSON = String . TE.decodeUtf8 . BC.toByteString' @@ -348,10 +349,11 @@ instance BC.ToByteString Transport where builder TransportTCP = "tcp" instance BC.FromByteString Transport where - parser = BC.parser >>= \t -> case (t :: ByteString) of - "udp" -> pure TransportUDP - "tcp" -> pure TransportTCP - _ -> fail $ "Invalid turn transport: " ++ show t + parser = + BC.parser >>= \t -> case (t :: ByteString) of + "udp" -> pure TransportUDP + "tcp" -> pure TransportTCP + _ -> fail $ "Invalid turn transport: " ++ show t instance ToJSON Transport where toJSON = String . TE.decodeUtf8 . BC.toByteString' diff --git a/libs/wire-api/src/Wire/API/Connection.hs b/libs/wire-api/src/Wire/API/Connection.hs index b40c579a813..4003290b77d 100644 --- a/libs/wire-api/src/Wire/API/Connection.hs +++ b/libs/wire-api/src/Wire/API/Connection.hs @@ -186,14 +186,15 @@ instance FromJSON Relation where parseJSON _ = mzero instance FromByteString Relation where - parser = takeByteString >>= \b -> case b of - "accepted" -> return Accepted - "blocked" -> return Blocked - "pending" -> return Pending - "ignored" -> return Ignored - "sent" -> return Sent - "cancelled" -> return Cancelled - x -> fail $ "Invalid relation-type " <> show x + parser = + takeByteString >>= \b -> case b of + "accepted" -> return Accepted + "blocked" -> return Blocked + "pending" -> return Pending + "ignored" -> return Ignored + "sent" -> return Sent + "cancelled" -> return Cancelled + x -> fail $ "Invalid relation-type " <> show x -------------------------------------------------------------------------------- -- Message diff --git a/libs/wire-api/src/Wire/API/Conversation/Code.hs b/libs/wire-api/src/Wire/API/Conversation/Code.hs index 6b8ff56635c..49c2b19194c 100644 --- a/libs/wire-api/src/Wire/API/Conversation/Code.hs +++ b/libs/wire-api/src/Wire/API/Conversation/Code.hs @@ -34,7 +34,7 @@ module Wire.API.Conversation.Code where import Control.Lens ((.~)) -import Data.Aeson ((.:), (.:?), (.=), FromJSON (parseJSON), ToJSON (toJSON)) +import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON), (.:), (.:?), (.=)) import qualified Data.Aeson as JSON import Data.ByteString.Conversion (toByteString') -- FUTUREWORK: move content of Data.Code here? diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index 1d0efa3d4b7..eb75defd444 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -67,19 +67,18 @@ import Data.Aeson import Data.Aeson.Types (Parser) import qualified Data.HashMap.Strict as HashMap import Data.Id -import Data.Json.Util ((#), ToJSONObject (toJSONObject), toUTCTimeMillis) +import Data.Json.Util (ToJSONObject (toJSONObject), toUTCTimeMillis, (#)) import qualified Data.Swagger.Build.Api as Doc import Data.Time import Imports import qualified Test.QuickCheck as QC import URI.ByteString () import Wire.API.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) -import Wire.API.Conversation (modelConversationAccessUpdate, modelConversationMessageTimerUpdate, modelConversationReceiptModeUpdate, modelConversationUpdateName) import Wire.API.Conversation +import Wire.API.Conversation (modelConversationAccessUpdate, modelConversationMessageTimerUpdate, modelConversationReceiptModeUpdate, modelConversationUpdateName) import Wire.API.Conversation.Code (ConversationCode (..), modelConversationCode) import Wire.API.Conversation.Role -import Wire.API.Conversation.Typing (modelTyping) -import Wire.API.Conversation.Typing (TypingData (..)) +import Wire.API.Conversation.Typing (TypingData (..), modelTyping) import Wire.API.User (UserIdList (..)) -------------------------------------------------------------------------------- @@ -359,9 +358,9 @@ newtype SimpleMembers = SimpleMembers -- | Used both for 'SimpleMembers' and 'UserIdList'. modelMembers :: Doc.Model modelMembers = - Doc.defineModel "Members" - $ Doc.property "users" (Doc.unique $ Doc.array Doc.bytes') - $ Doc.description "List of user IDs" + Doc.defineModel "Members" $ + Doc.property "users" (Doc.unique $ Doc.array Doc.bytes') $ + Doc.description "List of user IDs" instance ToJSON SimpleMembers where toJSON e = diff --git a/libs/wire-api/src/Wire/API/Event/Team.hs b/libs/wire-api/src/Wire/API/Event/Team.hs index 604de971b65..a51b1fd2cfb 100644 --- a/libs/wire-api/src/Wire/API/Event/Team.hs +++ b/libs/wire-api/src/Wire/API/Event/Team.hs @@ -98,9 +98,9 @@ modelMemberEvent = Doc.defineModel "TeamMemberEvent" $ do modelMemberData :: Doc.Model modelMemberData = - Doc.defineModel "MemberData" - $ Doc.property "user" Doc.bytes' - $ Doc.description "user ID" + Doc.defineModel "MemberData" $ + Doc.property "user" Doc.bytes' $ + Doc.description "user ID" modelConvEvent :: Doc.Model modelConvEvent = Doc.defineModel "TeamConversationEvent" $ do @@ -109,9 +109,9 @@ modelConvEvent = Doc.defineModel "TeamConversationEvent" $ do modelConversationData :: Doc.Model modelConversationData = - Doc.defineModel "ConversationData" - $ Doc.property "conv" Doc.bytes' - $ Doc.description "conversation ID" + Doc.defineModel "ConversationData" $ + Doc.property "conv" Doc.bytes' $ + Doc.description "conversation ID" modelUpdateEvent :: Doc.Model modelUpdateEvent = Doc.defineModel "TeamUpdateEvent" $ do diff --git a/libs/wire-api/src/Wire/API/Notification.hs b/libs/wire-api/src/Wire/API/Notification.hs index 1bc4b6018ce..fc2a343b9cd 100644 --- a/libs/wire-api/src/Wire/API/Notification.hs +++ b/libs/wire-api/src/Wire/API/Notification.hs @@ -42,8 +42,8 @@ module Wire.API.Notification where import Control.Lens (makeLenses) +import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON), (.!=), (.:), (.:?), (.=)) import qualified Data.Aeson as JSON -import Data.Aeson ((.!=), (.:), (.:?), (.=), FromJSON (parseJSON), ToJSON (toJSON)) import Data.Id import Data.Json.Util ((#)) import Data.List1 diff --git a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs index 5363d8678d0..61f94d9d916 100644 --- a/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs +++ b/libs/wire-api/src/Wire/API/Provider/Service/Tag.hs @@ -45,8 +45,8 @@ import qualified Data.Aeson as JSON import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Char8 as C8 import Data.ByteString.Conversion -import qualified Data.Range as Range import Data.Range (LTE, Range, fromRange) +import qualified Data.Range as Range import qualified Data.Set as Set import qualified Data.Text.Encoding as Text import GHC.TypeLits (KnownNat, Nat) @@ -97,39 +97,40 @@ data ServiceTag deriving (Arbitrary) via (GenericUniform ServiceTag) instance FromByteString ServiceTag where - parser = parser >>= \t -> case (t :: ByteString) of - "audio" -> pure AudioTag - "books" -> pure BooksTag - "business" -> pure BusinessTag - "design" -> pure DesignTag - "education" -> pure EducationTag - "entertainment" -> pure EntertainmentTag - "finance" -> pure FinanceTag - "fitness" -> pure FitnessTag - "food-drink" -> pure FoodDrinkTag - "games" -> pure GamesTag - "graphics" -> pure GraphicsTag - "health" -> pure HealthTag - "integration" -> pure IntegrationTag - "lifestyle" -> pure LifestyleTag - "media" -> pure MediaTag - "medical" -> pure MedicalTag - "movies" -> pure MoviesTag - "music" -> pure MusicTag - "news" -> pure NewsTag - "photography" -> pure PhotographyTag - "poll" -> pure PollTag - "productivity" -> pure ProductivityTag - "quiz" -> pure QuizTag - "rating" -> pure RatingTag - "shopping" -> pure ShoppingTag - "social" -> pure SocialTag - "sports" -> pure SportsTag - "travel" -> pure TravelTag - "tutorial" -> pure TutorialTag - "video" -> pure VideoTag - "weather" -> pure WeatherTag - _ -> fail $ "Invalid tag: " ++ show t + parser = + parser >>= \t -> case (t :: ByteString) of + "audio" -> pure AudioTag + "books" -> pure BooksTag + "business" -> pure BusinessTag + "design" -> pure DesignTag + "education" -> pure EducationTag + "entertainment" -> pure EntertainmentTag + "finance" -> pure FinanceTag + "fitness" -> pure FitnessTag + "food-drink" -> pure FoodDrinkTag + "games" -> pure GamesTag + "graphics" -> pure GraphicsTag + "health" -> pure HealthTag + "integration" -> pure IntegrationTag + "lifestyle" -> pure LifestyleTag + "media" -> pure MediaTag + "medical" -> pure MedicalTag + "movies" -> pure MoviesTag + "music" -> pure MusicTag + "news" -> pure NewsTag + "photography" -> pure PhotographyTag + "poll" -> pure PollTag + "productivity" -> pure ProductivityTag + "quiz" -> pure QuizTag + "rating" -> pure RatingTag + "shopping" -> pure ShoppingTag + "social" -> pure SocialTag + "sports" -> pure SportsTag + "travel" -> pure TravelTag + "tutorial" -> pure TutorialTag + "video" -> pure VideoTag + "weather" -> pure WeatherTag + _ -> fail $ "Invalid tag: " ++ show t instance ToByteString ServiceTag where builder AudioTag = "audio" diff --git a/libs/wire-api/src/Wire/API/Push/V2/Token.hs b/libs/wire-api/src/Wire/API/Push/V2/Token.hs index 9d8fc8de8c9..ad2a8d9de99 100644 --- a/libs/wire-api/src/Wire/API/Push/V2/Token.hs +++ b/libs/wire-api/src/Wire/API/Push/V2/Token.hs @@ -156,13 +156,14 @@ instance FromJSON Transport where x -> fail $ "Invalid push transport: " ++ show x instance FromByteString Transport where - parser = takeByteString >>= \case - "GCM" -> return GCM - "APNS" -> return APNS - "APNS_SANDBOX" -> return APNSSandbox - "APNS_VOIP" -> return APNSVoIP - "APNS_VOIP_SANDBOX" -> return APNSVoIPSandbox - x -> fail $ "Invalid push transport: " <> show x + parser = + takeByteString >>= \case + "GCM" -> return GCM + "APNS" -> return APNS + "APNS_SANDBOX" -> return APNSSandbox + "APNS_VOIP" -> return APNSVoIP + "APNS_VOIP_SANDBOX" -> return APNSVoIPSandbox + x -> fail $ "Invalid push transport: " <> show x newtype Token = Token { tokenText :: Text diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 5ed9fd781f0..23ea0ae9874 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -51,15 +51,16 @@ data TeamFeatureName deriving (Arbitrary) via (GenericUniform TeamFeatureName) instance FromByteString TeamFeatureName where - parser = Parser.takeByteString >>= \b -> - case T.decodeUtf8' b of - Left e -> fail $ "Invalid TeamFeatureName: " <> show e - Right "legalhold" -> pure TeamFeatureLegalHold - Right "sso" -> pure TeamFeatureSSO - Right "search-visibility" -> pure TeamFeatureSearchVisibility - Right "validate-saml-emails" -> pure TeamFeatureValidateSAMLEmails - Right "digital-signatures" -> pure TeamFeatureDigitalSignatures - Right t -> fail $ "Invalid TeamFeatureName: " <> T.unpack t + parser = + Parser.takeByteString >>= \b -> + case T.decodeUtf8' b of + Left e -> fail $ "Invalid TeamFeatureName: " <> show e + Right "legalhold" -> pure TeamFeatureLegalHold + Right "sso" -> pure TeamFeatureSSO + Right "search-visibility" -> pure TeamFeatureSearchVisibility + Right "validate-saml-emails" -> pure TeamFeatureValidateSAMLEmails + Right "digital-signatures" -> pure TeamFeatureDigitalSignatures + Right t -> fail $ "Invalid TeamFeatureName: " <> T.unpack t instance ToByteString TeamFeatureName where builder TeamFeatureLegalHold = "legalhold" @@ -121,9 +122,10 @@ instance ToByteString TeamFeatureStatusValue where builder TeamFeatureDisabled = "disabled" instance FromByteString TeamFeatureStatusValue where - parser = Parser.takeByteString >>= \b -> - case T.decodeUtf8' b of - Right "enabled" -> pure TeamFeatureEnabled - Right "disabled" -> pure TeamFeatureDisabled - Right t -> fail $ "Invalid TeamFeatureStatusValue: " <> T.unpack t - Left e -> fail $ "Invalid TeamFeatureStatusValue: " <> show e + parser = + Parser.takeByteString >>= \b -> + case T.decodeUtf8' b of + Right "enabled" -> pure TeamFeatureEnabled + Right "disabled" -> pure TeamFeatureDisabled + Right t -> fail $ "Invalid TeamFeatureStatusValue: " <> T.unpack t + Left e -> fail $ "Invalid TeamFeatureStatusValue: " <> show e diff --git a/libs/wire-api/src/Wire/API/Team/Permission.hs b/libs/wire-api/src/Wire/API/Team/Permission.hs index c6eae0c2fc9..4c3e023a7d6 100644 --- a/libs/wire-api/src/Wire/API/Team/Permission.hs +++ b/libs/wire-api/src/Wire/API/Team/Permission.hs @@ -44,9 +44,9 @@ where import qualified Cassandra as Cql import qualified Control.Error.Util as Err -import Control.Lens ((^.), makeLenses) +import Control.Lens (makeLenses, (^.)) import Data.Aeson -import Data.Bits ((.|.), testBit) +import Data.Bits (testBit, (.|.)) import Data.Json.Util import qualified Data.Set as Set import qualified Data.Swagger.Build.Api as Doc @@ -89,10 +89,11 @@ instance FromJSON Permissions where Just ps -> pure ps instance Arbitrary Permissions where - arbitrary = maybe (error "instance Arbitrary Permissions") pure =<< do - selfperms <- arbitrary - copyperms <- Set.intersection selfperms <$> arbitrary - pure $ newPermissions selfperms copyperms + arbitrary = + maybe (error "instance Arbitrary Permissions") pure =<< do + selfperms <- arbitrary + copyperms <- Set.intersection selfperms <$> arbitrary + pure $ newPermissions selfperms copyperms newPermissions :: -- | User's permissions diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index e0f6a886ddb..09542bb085a 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -96,7 +96,7 @@ import qualified Data.Currency as Currency import Data.Handle (Handle) import qualified Data.HashMap.Strict as HashMap import Data.Id -import Data.Json.Util ((#), UTCTimeMillis) +import Data.Json.Util (UTCTimeMillis, (#)) import Data.Misc (PlainTextPassword (..)) import Data.Range import qualified Data.Swagger.Build.Api as Doc diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index d9e4ae8bb06..31b52316050 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -124,9 +124,9 @@ newtype UserClients = UserClients modelUserClients :: Doc.Model modelUserClients = - Doc.defineModel "UserClients" - $ Doc.property "" (Doc.unique $ Doc.array Doc.bytes') - $ Doc.description "Map of user IDs to sets of client IDs ({ UserId: [ClientId] })." + Doc.defineModel "UserClients" $ + Doc.property "" (Doc.unique $ Doc.array Doc.bytes') $ + Doc.description "Map of user IDs to sets of client IDs ({ UserId: [ClientId] })." instance ToJSON UserClients where toJSON = @@ -256,12 +256,15 @@ instance FromJSON PubClient where -- team on a per-user basis -- * A LegalHoldClient is a client outside that user's control (but under the + -- control of that team's business) -- * Users need to click "accept" before a LegalHoldClient is added to their + -- account. -- * Any user interacting with a user which has a LegalHoldClient will upon + -- first interaction receive a warning, have the option of cancelling the -- interaction, and on an ongoing basis see a visual indication in all -- conversations where such a device is active. diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index edf244b6811..92acc22a824 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -219,11 +219,12 @@ instance FromByteString Phone where parser = parser >>= maybe (fail "Invalid phone") return . parsePhone instance Arbitrary Phone where - arbitrary = Phone . Text.pack <$> do - let mkdigits n = replicateM n (QC.elements ['0' .. '9']) - mini <- mkdigits 8 - maxi <- mkdigits =<< QC.chooseInt (0, 7) - pure $ '+' : mini <> maxi + arbitrary = + Phone . Text.pack <$> do + let mkdigits n = replicateM n (QC.elements ['0' .. '9']) + mini <- mkdigits 8 + maxi <- mkdigits =<< QC.chooseInt (0, 7) + pure $ '+' : mini <> maxi -- | Parses a phone number in E.164 format with a mandatory leading '+'. parsePhone :: Text -> Maybe Phone diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index 9005dda98b7..e6380d7cb14 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -271,9 +271,10 @@ typeManagedBy = ] instance ToJSON ManagedBy where - toJSON = String . \case - ManagedByWire -> "wire" - ManagedByScim -> "scim" + toJSON = + String . \case + ManagedByWire -> "wire" + ManagedByScim -> "scim" instance FromJSON ManagedBy where parseJSON = withText "ManagedBy" $ \case diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index e5ccb7e8b76..7dcdd07d696 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs @@ -23,7 +23,7 @@ import Data.Id (ConvId) import Imports import qualified Test.Tasty as T import Test.Tasty.ExpectedFailure (ignoreTest) -import Test.Tasty.QuickCheck ((===), Arbitrary, counterexample, testProperty) +import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===)) import Type.Reflection (typeRep) import qualified Wire.API.Asset as Asset import qualified Wire.API.Asset.V3.Resumable as Asset.Resumable diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs index 00dfe9263c3..9b7da669c0c 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs @@ -20,7 +20,7 @@ module Test.Wire.API.Roundtrip.ByteString (tests) where import Data.ByteString.Conversion import Imports import qualified Test.Tasty as T -import Test.Tasty.QuickCheck ((===), Arbitrary, counterexample, testProperty) +import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty, (===)) import Type.Reflection (typeRep) import qualified Wire.API.Arbitrary as Arbitrary () import qualified Wire.API.Asset.V3 as Asset.V3 diff --git a/libs/zauth/main/Main.hs b/libs/zauth/main/Main.hs index 5c58885a800..924804b2d84 100644 --- a/libs/zauth/main/Main.hs +++ b/libs/zauth/main/Main.hs @@ -179,15 +179,16 @@ options = long "data" <> metavar "STRING" <> help "token data" - toMode = readerAsk >>= \s -> case s of - "create-user" -> return CreateUser - "create-session" -> return CreateSession - "create-access" -> return CreateAccess - "create-bot" -> return CreateBot - "create-provider" -> return CreateProvider - "verify-user" -> return VerifyUser - "verify-access" -> return VerifyAccess - "verify-bot" -> return VerifyBot - "verify-provider" -> return VerifyProvider - "gen-keypair" -> return GenKeyPair - other -> readerError $ "invalid mode: " <> other + toMode = + readerAsk >>= \s -> case s of + "create-user" -> return CreateUser + "create-session" -> return CreateSession + "create-access" -> return CreateAccess + "create-bot" -> return CreateBot + "create-provider" -> return CreateProvider + "verify-user" -> return VerifyUser + "verify-access" -> return VerifyAccess + "verify-bot" -> return VerifyBot + "verify-provider" -> return VerifyProvider + "gen-keypair" -> return GenKeyPair + other -> readerError $ "invalid mode: " <> other diff --git a/libs/zauth/src/Data/ZAuth/Creation.hs b/libs/zauth/src/Data/ZAuth/Creation.hs index 389e6b38a8b..b3dc42126b0 100644 --- a/libs/zauth/src/Data/ZAuth/Creation.hs +++ b/libs/zauth/src/Data/ZAuth/Creation.hs @@ -53,7 +53,7 @@ import Data.ByteString.Conversion import Data.ByteString.Lazy (toStrict) import Data.Time.Clock.POSIX import Data.UUID -import Data.Vector ((!), Vector) +import Data.Vector (Vector, (!)) import qualified Data.Vector as Vec import Data.ZAuth.Token hiding (signature) import Imports diff --git a/libs/zauth/src/Data/ZAuth/Token.hs b/libs/zauth/src/Data/ZAuth/Token.hs index 45b1772917d..33ab055b67e 100644 --- a/libs/zauth/src/Data/ZAuth/Token.hs +++ b/libs/zauth/src/Data/ZAuth/Token.hs @@ -185,40 +185,46 @@ makeLenses ''LegalHoldUser makeLenses ''LegalHoldAccess instance FromByteString (Token Access) where - parser = takeLazyByteString >>= \b -> - case readToken A readAccessBody b of - Nothing -> fail "Invalid access token" - Just t -> return t + parser = + takeLazyByteString >>= \b -> + case readToken A readAccessBody b of + Nothing -> fail "Invalid access token" + Just t -> return t instance FromByteString (Token User) where - parser = takeLazyByteString >>= \b -> - case readToken U readUserBody b of - Nothing -> fail "Invalid user token" - Just t -> return t + parser = + takeLazyByteString >>= \b -> + case readToken U readUserBody b of + Nothing -> fail "Invalid user token" + Just t -> return t instance FromByteString (Token Bot) where - parser = takeLazyByteString >>= \b -> - case readToken B readBotBody b of - Nothing -> fail "Invalid bot token" - Just t -> return t + parser = + takeLazyByteString >>= \b -> + case readToken B readBotBody b of + Nothing -> fail "Invalid bot token" + Just t -> return t instance FromByteString (Token Provider) where - parser = takeLazyByteString >>= \b -> - case readToken P readProviderBody b of - Nothing -> fail "Invalid provider token" - Just t -> return t + parser = + takeLazyByteString >>= \b -> + case readToken P readProviderBody b of + Nothing -> fail "Invalid provider token" + Just t -> return t instance FromByteString (Token LegalHoldAccess) where - parser = takeLazyByteString >>= \b -> - case readToken LA readLegalHoldAccessBody b of - Nothing -> fail "Invalid access token" - Just t -> return t + parser = + takeLazyByteString >>= \b -> + case readToken LA readLegalHoldAccessBody b of + Nothing -> fail "Invalid access token" + Just t -> return t instance FromByteString (Token LegalHoldUser) where - parser = takeLazyByteString >>= \b -> - case readToken LU readLegalHoldUserBody b of - Nothing -> fail "Invalid user token" - Just t -> return t + parser = + takeLazyByteString >>= \b -> + case readToken LU readLegalHoldUserBody b of + Nothing -> fail "Invalid user token" + Just t -> return t instance ToByteString a => ToByteString (Token a) where builder = writeToken diff --git a/libs/zauth/src/Data/ZAuth/Validation.hs b/libs/zauth/src/Data/ZAuth/Validation.hs index ae2812ba5e4..6f514d10eca 100644 --- a/libs/zauth/src/Data/ZAuth/Validation.hs +++ b/libs/zauth/src/Data/ZAuth/Validation.hs @@ -37,7 +37,7 @@ import Control.Monad.Except import qualified Data.ByteString as Strict import Data.ByteString.Conversion import Data.Time.Clock.POSIX -import Data.Vector ((!), Vector) +import Data.Vector (Vector, (!)) import qualified Data.Vector as Vec import Data.ZAuth.Token import Imports diff --git a/services/brig/schema/src/V42.hs b/services/brig/schema/src/V42.hs index 8c6eaa9d1c3..38fcd0c3b9d 100644 --- a/services/brig/schema/src/V42.hs +++ b/services/brig/schema/src/V42.hs @@ -26,9 +26,9 @@ import Text.RawString.QQ migration :: Migration migration = - Migration 42 "Remove user.tracking_id" - $ void - $ schema' - [r| + Migration 42 "Remove user.tracking_id" $ + void $ + schema' + [r| alter columnfamily user drop tracking_id; |] diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 21910465d04..795a3741b6a 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -57,8 +57,8 @@ import Control.Lens (view) import Data.Bitraversable (bitraverse) import Data.ByteString.Conversion import Data.IP (IP) -import qualified Data.Id as Id import Data.Id (ClientId, ConnId, UserId, makeIdOpaque, makeMappedIdOpaque) +import qualified Data.Id as Id import Data.IdMapping import Data.List.NonEmpty (nonEmpty) import Data.List.Split (chunksOf) @@ -108,10 +108,10 @@ addClient u con ip new = do Intra.newClient u (clientId clt) Intra.onClientEvent u con (ClientAdded u clt) when (clientType clt == LegalHoldClientType) $ Intra.onUserEvent u con (UserLegalHoldEnabled u) - when (count > 1) - $ for_ (userEmail usr) - $ \email -> - sendNewClientEmail (userDisplayName usr) email clt (userLocale usr) + when (count > 1) $ + for_ (userEmail usr) $ + \email -> + sendNewClientEmail (userDisplayName usr) email clt (userLocale usr) return clt where clientId' = clientIdFromPrekey (unpackLastPrekey $ newClientLastKey new) diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index eff56ce4243..f6032e6374b 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -77,16 +77,16 @@ createConnectionToLocalUser :: ConnId -> ExceptT ConnectionError AppIO ConnectionResult createConnectionToLocalUser self crUser ConnectionRequest {crName, crMessage} conn = do - when (self == crUser) - $ throwE - $ InvalidUser (makeIdOpaque crUser) + when (self == crUser) $ + throwE $ + InvalidUser (makeIdOpaque crUser) selfActive <- lift $ Data.isActivated self unless selfActive $ throwE ConnectNoIdentity otherActive <- lift $ Data.isActivated crUser - unless otherActive - $ throwE - $ InvalidUser (makeIdOpaque crUser) + unless otherActive $ + throwE $ + InvalidUser (makeIdOpaque crUser) -- Users belonging to the same team are always treated as connected, so creating a -- connection between them is useless. {#RefConnectionTeam} sameTeam <- lift $ belongSameTeam @@ -209,7 +209,7 @@ updateConnection self other newStatus conn = do (old, _, new) | old == new -> return Nothing _ -> throwE $ InvalidTransition self newStatus - lift $ for_ s2o' $ \c -> + lift . for_ s2o' $ \c -> let e2s = ConnectionUpdated c (Just $ ucStatus s2o) Nothing in Intra.onConnectionEvent self conn e2s return s2o' @@ -219,12 +219,12 @@ updateConnection self other newStatus conn = do Log.info $ Log.connection self (ucTo s2o) . msg (val "Accepting connection") - cnv <- lift $ for (ucConvId s2o) $ Intra.acceptConnectConv self conn + cnv <- lift . for (ucConvId s2o) $ Intra.acceptConnectConv self conn -- Note: The check for @Pending@ accounts for situations in which both -- sides are pending, which can occur due to rare race conditions -- when sending mutual connection requests, combined with untimely -- crashes. - when (ucStatus o2s `elem` [Sent, Pending]) $ lift $ do + when (ucStatus o2s `elem` [Sent, Pending]) . lift $ do o2s' <- if (cnvType <$> cnv) /= Just ConnectConv then Data.updateConnection o2s Accepted @@ -244,8 +244,8 @@ updateConnection self other newStatus conn = do Log.info $ Log.connection self (ucTo s2o) . msg (val "Unblocking connection") - cnv <- lift $ for (ucConvId s2o) $ Intra.unblockConv (ucFrom s2o) conn - when (ucStatus o2s == Sent && new == Accepted) $ lift $ do + cnv <- lift . for (ucConvId s2o) $ Intra.unblockConv (ucFrom s2o) conn + when (ucStatus o2s == Sent && new == Accepted) . lift $ do o2s' <- if (cnvType <$> cnv) /= Just ConnectConv then Data.updateConnection o2s Accepted @@ -257,7 +257,7 @@ updateConnection self other newStatus conn = do Log.info $ Log.connection self (ucTo s2o) . msg (val "Cancelling connection") - lift $ for_ (ucConvId s2o) $ Intra.blockConv (ucFrom s2o) conn + lift . for_ (ucConvId s2o) $ Intra.blockConv (ucFrom s2o) conn o2s' <- lift $ Data.updateConnection o2s Cancelled let e2o = ConnectionUpdated o2s' (Just $ ucStatus o2s) Nothing lift $ Intra.onConnectionEvent self conn e2o @@ -321,6 +321,6 @@ checkLimit :: UserId -> ExceptT ConnectionError AppIO () checkLimit u = do n <- lift $ Data.countConnections u [Accepted, Sent] l <- setUserMaxConnections <$> view settings - unless (n < l) - $ throwE - $ TooManyConnections u + unless (n < l) $ + throwE $ + TooManyConnections u diff --git a/services/brig/src/Brig/API/Handler.hs b/services/brig/src/Brig/API/Handler.hs index f0a4baf0f95..4929008cce4 100644 --- a/services/brig/src/Brig/API/Handler.hs +++ b/services/brig/src/Brig/API/Handler.hs @@ -79,10 +79,10 @@ onError :: Logger -> Request -> Continue IO -> Error -> IO ResponseReceived onError g r k e = do Server.logError g (Just r) we Server.flushRequestBody r - k - $ setStatus (WaiError.code we) + k $ + setStatus (WaiError.code we) . appEndo (foldMap (Endo . uncurry addHeader) hs) - $ json e + $ json e where (we, hs) = case e of StdError x -> (x, []) diff --git a/services/brig/src/Brig/API/IdMapping.hs b/services/brig/src/Brig/API/IdMapping.hs index 0599799d772..915a9e1bb98 100644 --- a/services/brig/src/Brig/API/IdMapping.hs +++ b/services/brig/src/Brig/API/IdMapping.hs @@ -32,15 +32,15 @@ import Brig.App (AppIO) import qualified Brig.Data.IdMapping as Data (getIdMapping, insertIdMapping) import qualified Brig.IO.Intra.IdMapping as Intra import Control.Monad.Catch (throwM) -import qualified Data.Id as Id import Data.Id (Id (Id, toUUID), OpaqueUserId, idToText) +import qualified Data.Id as Id import Data.IdMapping (IdMapping (IdMapping, _imQualifiedId), MappedOrLocalId (Local, Mapped), hashQualifiedId) import Data.Qualified (Qualified, renderQualifiedId) import Galley.Types.IdMapping (PostIdMappingRequest (PostIdMappingRequest), PostIdMappingResponse (PostIdMappingResponse), mkPostIdMappingRequest) import Imports import Network.HTTP.Types (forbidden403, notFound404) import Network.Wai (Response) -import Network.Wai.Predicate ((.&.), (:::) ((:::)), accept) +import Network.Wai.Predicate (accept, (.&.), (:::) ((:::))) import Network.Wai.Routing (Routes, capture, continue, get, post) import Network.Wai.Utilities (JsonRequest, empty, json, jsonRequest, setStatus) import qualified System.Logger.Class as Log @@ -147,12 +147,12 @@ createIdMapping qualifiedId = do let idMapping = IdMapping mappedId qualifiedId Data.getIdMapping mappedId >>= \case Just existingMapping -> - when (_imQualifiedId existingMapping /= qualifiedId) - $ Log.err - $ Log.msg @Text "Conflict when creating IdMapping" - . Log.field "mapped_id" (idToText mappedId) - . Log.field "existing_qualified_id" (renderQualifiedId qualifiedId) - . Log.field "new_qualified_id" (renderQualifiedId (_imQualifiedId existingMapping)) + when (_imQualifiedId existingMapping /= qualifiedId) $ + Log.err $ + Log.msg @Text "Conflict when creating IdMapping" + . Log.field "mapped_id" (idToText mappedId) + . Log.field "existing_qualified_id" (renderQualifiedId qualifiedId) + . Log.field "new_qualified_id" (renderQualifiedId (_imQualifiedId existingMapping)) Nothing -> do Data.insertIdMapping idMapping Intra.createIdMappingInGalley (mkPostIdMappingRequest qualifiedId) diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index f4fc89853a3..50b15bb3b01 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -257,12 +257,12 @@ autoConnectH (_ ::: uid ::: conn ::: req) = do autoConnect :: UserId -> Maybe ConnId -> UserSet -> Handler [UserConnection] autoConnect uid conn (UserSet to) = do let num = Set.size to - when (num < 1) - $ throwStd - $ badRequest "No users given for auto-connect." - when (num > 25) - $ throwStd - $ badRequest "Too many users given for auto-connect (> 25)." + when (num < 1) $ + throwStd $ + badRequest "No users given for auto-connect." + when (num > 25) $ + throwStd $ + badRequest "Too many users given for auto-connect (> 25)." API.autoConnect uid to conn !>> connError createUserNoVerifyH :: JSON ::: JsonRequest NewUser -> Handler Response diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 809b5b661dd..28a939ab53e 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -45,7 +45,7 @@ import qualified Brig.User.Auth.Cookie as Auth import Brig.User.Email import Brig.User.Phone import Control.Error hiding (bool) -import Control.Lens ((^.), view) +import Control.Lens (view, (^.)) import Control.Monad.Catch (throwM) import Data.Aeson hiding (json) import Data.ByteString.Conversion @@ -55,7 +55,7 @@ import Data.Handle (Handle, parseHandle) import Data.Id as Id import Data.IdMapping (MappedOrLocalId (Local)) import qualified Data.Map.Strict as Map -import Data.Misc ((<$$>), IpAddr (..)) +import Data.Misc (IpAddr (..), (<$$>)) import Data.Qualified (OptionallyQualified, eitherQualifiedOrNot) import Data.Range import qualified Data.Swagger.Build.Api as Doc @@ -1152,7 +1152,8 @@ checkHandle :: UserId -> Text -> Handler CheckHandleResp checkHandle _ uhandle = do handle <- validateHandle uhandle owner <- lift $ API.lookupHandle handle - if | isJust owner -> + if + | isJust owner -> -- Handle is taken (=> getHandleInfo will return 200) return CheckHandleFound | API.isBlacklistedHandle handle -> diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 5302c8b29ca..37ebb939743 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -119,7 +119,7 @@ import qualified Brig.User.Search.Index as Index import Control.Arrow ((&&&)) import Control.Concurrent.Async (mapConcurrently, mapConcurrently_) import Control.Error -import Control.Lens ((^.), view) +import Control.Lens (view, (^.)) import Control.Monad.Catch import Data.ByteString.Conversion import qualified Data.Currency as Currency @@ -129,8 +129,7 @@ import Data.IdMapping (MappedOrLocalId, partitionMappedOrLocalIds) import Data.Json.Util import Data.List1 (List1) import qualified Data.Map.Strict as Map -import Data.Misc ((<$$>)) -import Data.Misc (PlainTextPassword (..)) +import Data.Misc (PlainTextPassword (..), (<$$>)) import Data.Time.Clock (diffUTCTime) import Data.UUID.V4 (nextRandom) import qualified Galley.Types.Teams as Team @@ -229,9 +228,9 @@ createUser new@NewUser {..} = do where checkKey u k = do av <- lift $ Data.keyAvailable k u - unless av - $ throwE - $ DuplicateUserKey k + unless av $ + throwE $ + DuplicateUserKey k createTeam uid activating t tid = do created <- Intra.createTeam uid t tid return $ @@ -248,24 +247,27 @@ createUser new@NewUser {..} = do Maybe (Team.Invitation, Team.InvitationInfo), Maybe TeamId ) - handleTeam (Just (NewTeamMember i)) e = findTeamInvitation e i >>= return . \case - Just (inv, info, tid) -> (Nothing, Just (inv, info), Just tid) - Nothing -> (Nothing, Nothing, Nothing) + handleTeam (Just (NewTeamMember i)) e = + findTeamInvitation e i + >>= return . \case + Just (inv, info, tid) -> (Nothing, Just (inv, info), Just tid) + Nothing -> (Nothing, Nothing, Nothing) handleTeam (Just (NewTeamCreator t)) _ = (Just t,Nothing,) <$> (Just . Id <$> liftIO nextRandom) handleTeam (Just (NewTeamMemberSSO tid)) _ = pure (Nothing, Nothing, Just tid) handleTeam Nothing _ = return (Nothing, Nothing, Nothing) findTeamInvitation :: Maybe UserKey -> InvitationCode -> ExceptT CreateUserError AppIO (Maybe (Team.Invitation, Team.InvitationInfo, TeamId)) findTeamInvitation Nothing _ = throwE MissingIdentity - findTeamInvitation (Just e) c = lift (Team.lookupInvitationInfo c) >>= \case - Just ii -> do - inv <- lift $ Team.lookupInvitation (Team.iiTeam ii) (Team.iiInvId ii) - case (inv, Team.inIdentity <$> inv) of - (Just invite, Just em) - | e == userEmailKey em -> do - _ <- ensureMemberCanJoin (Team.iiTeam ii) - return $ Just (invite, ii, Team.iiTeam ii) - _ -> throwE InvalidInvitationCode - Nothing -> throwE InvalidInvitationCode + findTeamInvitation (Just e) c = + lift (Team.lookupInvitationInfo c) >>= \case + Just ii -> do + inv <- lift $ Team.lookupInvitation (Team.iiTeam ii) (Team.iiInvId ii) + case (inv, Team.inIdentity <$> inv) of + (Just invite, Just em) + | e == userEmailKey em -> do + _ <- ensureMemberCanJoin (Team.iiTeam ii) + return $ Just (invite, ii, Team.iiTeam ii) + _ -> throwE InvalidInvitationCode + Nothing -> throwE InvalidInvitationCode ensureMemberCanJoin :: TeamId -> ExceptT CreateUserError AppIO () ensureMemberCanJoin tid = do maxSize <- fromIntegral . setMaxTeamSize <$> view settings @@ -281,9 +283,9 @@ createUser new@NewUser {..} = do acceptTeamInvitation account inv ii uk ident = do let uid = userId (accountUser account) ok <- lift $ Data.claimKey uk uid - unless ok - $ throwE - $ DuplicateUserKey uk + unless ok $ + throwE $ + DuplicateUserKey uk let minvmeta :: (Maybe (UserId, UTCTimeMillis), Team.Role) minvmeta = ((,inCreatedAt inv) <$> inCreatedBy inv, Team.inRole inv) added <- lift $ Intra.addTeamMember uid (Team.iiTeam ii) minvmeta @@ -432,9 +434,9 @@ changeEmail u email = do when blacklisted $ throwE (ChangeBlacklistedEmail email) available <- lift $ Data.keyAvailable ek (Just u) - unless available - $ throwE - $ EmailExists email + unless available $ + throwE $ + EmailExists email usr <- maybe (throwM $ UserProfileNotFound u) return =<< lift (Data.lookupUser u) case join (emailIdentity <$> userIdentity usr) of -- The user already has an email address and the new one is exactly the same @@ -456,9 +458,9 @@ changePhone u phone = do =<< lift (validatePhone phone) let pk = userPhoneKey ph available <- lift $ Data.keyAvailable pk (Just u) - unless available - $ throwE - $ PhoneExists phone + unless available $ + throwE $ + PhoneExists phone timeout <- setActivationTimeout <$> view settings act <- lift $ Data.newActivation pk timeout (Just u) return (act, ph) @@ -504,15 +506,16 @@ revokeIdentity key = do mu <- Data.lookupKey uk case mu of Nothing -> return () - Just u -> fetchUserIdentity u >>= \case - Just (FullIdentity _ _) -> revokeKey u uk - Just (EmailIdentity e) | Left e == key -> do - revokeKey u uk - Data.deactivateUser u - Just (PhoneIdentity p) | Right p == key -> do - revokeKey u uk - Data.deactivateUser u - _ -> return () + Just u -> + fetchUserIdentity u >>= \case + Just (FullIdentity _ _) -> revokeKey u uk + Just (EmailIdentity e) | Left e == key -> do + revokeKey u uk + Data.deactivateUser u + Just (PhoneIdentity p) | Right p == key -> do + revokeKey u uk + Data.deactivateUser u + _ -> return () where revokeKey u uk = do deleteKey uk @@ -545,9 +548,10 @@ changeAccountStatus usrs status = do Intra.onUserEvent u Nothing (ev u) suspendAccount :: HasCallStack => List1 UserId -> AppIO () -suspendAccount usrs = runExceptT (changeAccountStatus usrs Suspended) >>= \case - Right _ -> pure () - Left InvalidAccountStatus -> error "impossible." +suspendAccount usrs = + runExceptT (changeAccountStatus usrs Suspended) >>= \case + Right _ -> pure () + Left InvalidAccountStatus -> error "impossible." ------------------------------------------------------------------------------- -- Activation @@ -580,9 +584,9 @@ activateWithCurrency tgt code usr cur = do Nothing -> return ActivationPass Just e -> do (uid, ident, first) <- lift $ onActivated e - when first - $ lift - $ activateTeam uid + when first $ + lift $ + activateTeam uid return $ ActivationSuccess ident first where activateTeam uid = do @@ -618,9 +622,9 @@ sendActivationCode emailOrPhone loc call = case emailOrPhone of (return . userEmailKey) (validateEmail email) exists <- lift $ isJust <$> Data.lookupKey ek - when exists - $ throwE - $ UserKeyInUse ek + when exists $ + throwE $ + UserKeyInUse ek blacklisted <- lift $ Blacklist.exists ek when blacklisted $ throwE (ActivationBlacklistedUserKey ek) @@ -638,9 +642,9 @@ sendActivationCode emailOrPhone loc call = case emailOrPhone of =<< lift (validatePhone phone) let pk = userPhoneKey canonical exists <- lift $ isJust <$> Data.lookupKey pk - when exists - $ throwE - $ UserKeyInUse pk + when exists $ + throwE $ + UserKeyInUse pk blacklisted <- lift $ Blacklist.exists pk when blacklisted $ throwE (ActivationBlacklistedUserKey pk) diff --git a/services/brig/src/Brig/AWS.hs b/services/brig/src/Brig/AWS.hs index 09af84e1a5d..745ed6408d7 100644 --- a/services/brig/src/Brig/AWS.hs +++ b/services/brig/src/Brig/AWS.hs @@ -102,7 +102,7 @@ newtype Amazon a = Amazon ) instance MonadUnliftIO Amazon where - askUnliftIO = Amazon $ ReaderT $ \r -> + askUnliftIO = Amazon . ReaderT $ \r -> withUnliftIO $ \u -> return (UnliftIO (unliftIO u . flip runReaderT r . unAmazon)) @@ -173,7 +173,7 @@ instance Exception Error -- SQS listen :: (FromJSON a, Show a) => Int -> Text -> (a -> IO ()) -> Amazon () -listen throttleMillis url callback = forever $ handleAny unexpectedError $ do +listen throttleMillis url callback = forever . handleAny unexpectedError $ do msgs <- view rmrsMessages <$> send receive void $ mapConcurrently onMessage msgs when (null msgs) $ @@ -182,7 +182,7 @@ listen throttleMillis url callback = forever $ handleAny unexpectedError $ do receive = SQS.receiveMessage url & set SQS.rmWaitTimeSeconds (Just 20) - . set SQS.rmMaxNumberOfMessages (Just 10) + . set SQS.rmMaxNumberOfMessages (Just 10) onMessage m = case decodeStrict =<< Text.encodeUtf8 <$> m ^. mBody of Nothing -> err $ msg ("Failed to parse SQS event: " ++ show m) @@ -252,9 +252,9 @@ execCatch :: a -> m (Either AWS.Error (Rs a)) execCatch e cmd = - runResourceT . AWST.runAWST e - $ AWST.trying AWS._Error - $ AWST.send cmd + runResourceT . AWST.runAWST e $ + AWST.trying AWS._Error $ + AWST.send cmd exec :: (AWSRequest a, AWS.HasEnv r, MonadUnliftIO m, MonadCatch m, MonadThrow m, MonadIO m) => diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 4980a07850f..1daf11a86c6 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -89,7 +89,7 @@ import qualified Cassandra.Settings as Cas import Control.AutoUpdate import Control.Error import Control.Exception.Enclosed (handleAny) -import Control.Lens hiding ((.=), index) +import Control.Lens hiding (index, (.=)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) import Control.Monad.Trans.Resource import Data.ByteString.Conversion @@ -302,9 +302,10 @@ replaceTurnServers g ref e = do let logErr x = Log.err g (msg $ val "Error loading turn servers: " +++ show x) handleAny logErr $ readTurnList (FS.eventPath e) >>= \case - Just servers -> readIORef ref >>= \old -> do - atomicWriteIORef ref (old & Calling.turnServers .~ servers) - Log.info g (msg $ val "New turn servers loaded.") + Just servers -> + readIORef ref >>= \old -> do + atomicWriteIORef ref (old & Calling.turnServers .~ servers) + Log.info g (msg $ val "New turn servers loaded.") Nothing -> Log.warn g (msg $ val "Empty or malformed turn servers list, ignoring!") initZAuth :: Opts -> IO ZAuth.Env @@ -381,8 +382,8 @@ initCassandra o g = do (Cas.initialContactsDisco "cassandra_brig") (unpack <$> Opt.discoUrl o) p <- - Cas.init - $ Cas.setLogger (Cas.mkLogger (Log.clone (Just "cassandra.brig") g)) + Cas.init $ + Cas.setLogger (Cas.mkLogger (Log.clone (Just "cassandra.brig") g)) . Cas.setContacts (NE.head c) (NE.tail c) . Cas.setPortNumber (fromIntegral ((Opt.cassandra o) ^. casEndpoint . epPort)) . Cas.setKeyspace (Keyspace ((Opt.cassandra o) ^. casKeyspace)) @@ -391,7 +392,7 @@ initCassandra o g = do . Cas.setSendTimeout 3 . Cas.setResponseTimeout 10 . Cas.setProtocolVersion Cas.V4 - $ Cas.defSettings + $ Cas.defSettings runClient p $ versionCheck schemaVersion return p @@ -474,7 +475,7 @@ instance Monad m => HasRequestId (AppT m) where instance MonadUnliftIO m => MonadUnliftIO (AppT m) where withRunInIO inner = - AppT $ ReaderT $ \r -> + AppT . ReaderT $ \r -> withRunInIO $ \run -> inner (run . flip runReaderT r . unAppT) @@ -500,13 +501,14 @@ forkAppIO u ma = do user = maybe id (field "user" . toByteString) locationOf :: (MonadIO m, MonadReader Env m) => IP -> m (Maybe Location) -locationOf ip = view geoDb >>= \case - Just g -> do - database <- liftIO $ readIORef g - return $! do - loc <- GeoIp.geoLocation =<< hush (GeoIp.findGeoData database "en" ip) - return $ location (Latitude $ GeoIp.locationLatitude loc) (Longitude $ GeoIp.locationLongitude loc) - Nothing -> return Nothing +locationOf ip = + view geoDb >>= \case + Just g -> do + database <- liftIO $ readIORef g + return $! do + loc <- GeoIp.geoLocation =<< hush (GeoIp.findGeoData database "en" ip) + return $ location (Latitude $ GeoIp.locationLatitude loc) (Longitude $ GeoIp.locationLongitude loc) + Nothing -> return Nothing readTurnList :: FilePath -> IO (Maybe (List1 TurnURI)) readTurnList = Text.readFile >=> return . fn . mapMaybe fromByteString . fmap Text.encodeUtf8 . Text.lines diff --git a/services/brig/src/Brig/Calling/API.hs b/services/brig/src/Brig/Calling/API.hs index 8ca7e279b93..e1900114acf 100644 --- a/services/brig/src/Brig/Calling/API.hs +++ b/services/brig/src/Brig/Calling/API.hs @@ -42,7 +42,7 @@ import Data.Text.Strict.Lens import Data.Time.Clock.POSIX (getPOSIXTime) import Imports hiding (head) import Network.Wai (Response) -import Network.Wai.Predicate hiding ((#), and, result, setStatus) +import Network.Wai.Predicate hiding (and, result, setStatus, (#)) import Network.Wai.Routing hiding (toList) import Network.Wai.Utilities hiding (code, message) import Network.Wai.Utilities.Swagger (document) diff --git a/services/brig/src/Brig/Data/Activation.hs b/services/brig/src/Brig/Data/Activation.hs index 027a8462d78..11ef153147e 100644 --- a/services/brig/src/Brig/Data/Activation.hs +++ b/services/brig/src/Brig/Data/Activation.hs @@ -118,9 +118,9 @@ activateKey k c u = verifyCode k c >>= pickUser >>= activate return . Just $ foldKey (EmailActivated uid) (PhoneActivated uid) key claim key uid = do ok <- lift $ claimKey key uid - unless ok - $ throwE . UserKeyExists . LT.fromStrict - $ foldKey fromEmail fromPhone key + unless ok $ + throwE . UserKeyExists . LT.fromStrict $ + foldKey fromEmail fromPhone key -- | Create a new pending activation for a given 'UserKey'. newActivation :: @@ -162,7 +162,8 @@ verifyCode key code = do s <- lift . retry x1 . query1 keySelect $ params Quorum (Identity key) case s of Just (ttl, Ascii t, k, c, u, r) -> - if | c == code -> mkScope t k u + if + | c == code -> mkScope t k u | r >= 1 -> countdown (key, t, k, c, u, r -1, ttl) >> throwE invalidCode | otherwise -> revoke >> throwE invalidCode Nothing -> throwE invalidCode diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 139d83de840..d21931764c0 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -91,9 +91,9 @@ addClient u newId c maxPermClients loc = do let typed = filter ((== newClientType c) . clientType) clients let count = length typed let upsert = any exists typed - unless (count == 0 || upsert) - $ fmapLT ClientReAuthError - $ User.reauthenticate u (newClientPassword c) + unless (count == 0 || upsert) $ + fmapLT ClientReAuthError $ + User.reauthenticate u (newClientPassword c) let capacity = fmap (+ (- count)) limit unless (maybe True (> 0) capacity || upsert) $ throwE TooManyClients diff --git a/services/brig/src/Brig/Data/Connection.hs b/services/brig/src/Brig/Data/Connection.hs index d380f0ab589..6e4f49ad9fc 100644 --- a/services/brig/src/Brig/Data/Connection.hs +++ b/services/brig/src/Brig/Data/Connection.hs @@ -37,7 +37,7 @@ import Brig.Data.Types as T import Brig.Types import Brig.Types.Intra import Cassandra -import Data.Conduit ((.|), runConduit) +import Data.Conduit (runConduit, (.|)) import qualified Data.Conduit.List as C import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) @@ -49,13 +49,13 @@ import UnliftIO.Async (pooledMapConcurrentlyN_) connectUsers :: UserId -> [(UserId, ConvId)] -> AppIO [UserConnection] connectUsers from to = do now <- toUTCTimeMillis <$> liftIO getCurrentTime - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum forM_ to $ \(u, c) -> do addPrepQuery connectionInsert (from, u, Accepted, now, Nothing, c) addPrepQuery connectionInsert (u, from, Accepted, now, Nothing, c) - return $ concat $ (`map` to) $ \(u, c) -> + return . concat . (`map` to) $ \(u, c) -> [ UserConnection from u Accepted now Nothing (Just c), UserConnection u from Accepted now Nothing (Just c) ] @@ -97,9 +97,10 @@ lookupConnection from to = -- | For a given user 'A', lookup his outgoing connections (A -> X) to other users. lookupConnections :: UserId -> Maybe UserId -> Range 1 500 Int32 -> AppIO (ResultPage UserConnection) -lookupConnections from start (fromRange -> size) = toResult <$> case start of - Just u -> retry x1 $ paginate connectionsSelectFrom (paramsP Quorum (from, u) (size + 1)) - Nothing -> retry x1 $ paginate connectionsSelect (paramsP Quorum (Identity from) (size + 1)) +lookupConnections from start (fromRange -> size) = + toResult <$> case start of + Just u -> retry x1 $ paginate connectionsSelectFrom (paramsP Quorum (from, u) (size + 1)) + Nothing -> retry x1 $ paginate connectionsSelect (paramsP Quorum (Identity from) (size + 1)) where toResult = cassandraResultPage . fmap toUserConnection . trim trim p = p {result = take (fromIntegral size) (result p)} diff --git a/services/brig/src/Brig/Data/IdMapping.hs b/services/brig/src/Brig/Data/IdMapping.hs index 525103725e5..96ad9db05d7 100644 --- a/services/brig/src/Brig/Data/IdMapping.hs +++ b/services/brig/src/Brig/Data/IdMapping.hs @@ -33,8 +33,9 @@ import Imports -- | Only a single namespace/table is used for for potentially multiple different types of -- mapped IDs. getIdMapping :: Id (Mapped a) -> AppIO (Maybe (IdMapping a)) -getIdMapping mappedId = fmap toIdMapping <$> do - retry x1 $ query1 idMappingSelect (params Quorum (Identity mappedId)) +getIdMapping mappedId = + fmap toIdMapping <$> do + retry x1 $ query1 idMappingSelect (params Quorum (Identity mappedId)) where toIdMapping (remoteId, domain) = IdMapping mappedId (Qualified remoteId domain) diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index a82e9da0a84..02eb9c484da 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -105,11 +105,12 @@ data ReAuthError newAccount :: NewUser -> Maybe InvitationId -> Maybe TeamId -> AppIO (UserAccount, Maybe Password) newAccount u inv tid = do defLoc <- setDefaultLocale <$> view settings - uid <- Id <$> do - case (inv, newUserUUID u) of - (Just (toUUID -> uuid), _) -> pure uuid - (_, Just uuid) -> pure uuid - (Nothing, Nothing) -> liftIO nextRandom + uid <- + Id <$> do + case (inv, newUserUUID u) of + (Just (toUUID -> uuid), _) -> pure uuid + (_, Just uuid) -> pure uuid + (Nothing, Nothing) -> liftIO nextRandom passwd <- maybe (return Nothing) (fmap Just . liftIO . mkSafePassword) pass expiry <- case status of Ephemeral -> do @@ -140,27 +141,29 @@ newAccount u inv tid = do -- | Mandatory password authentication. authenticate :: UserId -> PlainTextPassword -> ExceptT AuthError AppIO () -authenticate u pw = lift (lookupAuth u) >>= \case - Nothing -> throwE AuthInvalidUser - Just (_, Deleted) -> throwE AuthInvalidUser - Just (_, Suspended) -> throwE AuthSuspended - Just (_, Ephemeral) -> throwE AuthEphemeral - Just (Nothing, _) -> throwE AuthInvalidCredentials - Just (Just pw', Active) -> - unless (verifyPassword pw pw') $ - throwE AuthInvalidCredentials +authenticate u pw = + lift (lookupAuth u) >>= \case + Nothing -> throwE AuthInvalidUser + Just (_, Deleted) -> throwE AuthInvalidUser + Just (_, Suspended) -> throwE AuthSuspended + Just (_, Ephemeral) -> throwE AuthEphemeral + Just (Nothing, _) -> throwE AuthInvalidCredentials + Just (Just pw', Active) -> + unless (verifyPassword pw pw') $ + throwE AuthInvalidCredentials -- | Password reauthentication. If the account has a password, reauthentication -- is mandatory. If the account has no password and no password is given, -- reauthentication is a no-op. reauthenticate :: (MonadClient m) => UserId -> Maybe PlainTextPassword -> ExceptT ReAuthError m () -reauthenticate u pw = lift (lookupAuth u) >>= \case - Nothing -> throwE (ReAuthError AuthInvalidUser) - Just (_, Deleted) -> throwE (ReAuthError AuthInvalidUser) - Just (_, Suspended) -> throwE (ReAuthError AuthSuspended) - Just (Nothing, _) -> for_ pw $ const (throwE $ ReAuthError AuthInvalidCredentials) - Just (Just pw', Active) -> maybeReAuth pw' - Just (Just pw', Ephemeral) -> maybeReAuth pw' +reauthenticate u pw = + lift (lookupAuth u) >>= \case + Nothing -> throwE (ReAuthError AuthInvalidUser) + Just (_, Deleted) -> throwE (ReAuthError AuthInvalidUser) + Just (_, Suspended) -> throwE (ReAuthError AuthSuspended) + Just (Nothing, _) -> for_ pw $ const (throwE $ ReAuthError AuthInvalidCredentials) + Just (Just pw', Active) -> maybeReAuth pw' + Just (Just pw', Ephemeral) -> maybeReAuth pw' where maybeReAuth pw' = case pw of Nothing -> throwE ReAuthMissingPassword @@ -177,7 +180,7 @@ insertAccount :: -- | Whether the user is activated Bool -> AppIO () -insertAccount (UserAccount u status) mbConv password activated = retry x5 $ batch $ do +insertAccount (UserAccount u status) mbConv password activated = retry x5 . batch $ do setType BatchLogged setConsistency Quorum let Locale l c = userLocale u @@ -223,7 +226,7 @@ updateLocale :: UserId -> Locale -> AppIO () updateLocale u (Locale l c) = write userLocaleUpdate (params Quorum (l, c, u)) updateUser :: UserId -> UserUpdate -> AppIO () -updateUser u UserUpdate {..} = retry x5 $ batch $ do +updateUser u UserUpdate {..} = retry x5 . batch $ do setType BatchLogged setConsistency Quorum for_ uupName $ \n -> addPrepQuery userDisplayNameUpdate (n, u) @@ -270,7 +273,7 @@ deleteServiceUser :: ProviderId -> ServiceId -> BotId -> AppIO () deleteServiceUser pid sid bid = do lookupServiceUser pid sid bid >>= \case Nothing -> pure () - Just (_, mbTid) -> retry x5 $ batch $ do + Just (_, mbTid) -> retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery cql (pid, sid, bid) diff --git a/services/brig/src/Brig/Email.hs b/services/brig/src/Brig/Email.hs index fedd0e07a42..1335ef784a2 100644 --- a/services/brig/src/Brig/Email.hs +++ b/services/brig/src/Brig/Email.hs @@ -50,9 +50,10 @@ import Network.Mail.Mime ------------------------------------------------------------------------------- sendMail :: Mail -> AppIO () -sendMail m = view smtpEnv >>= \case - Just smtp -> SMTP.sendMail smtp m - Nothing -> view awsEnv >>= \e -> AWS.execute e $ AWS.sendMail m +sendMail m = + view smtpEnv >>= \case + Just smtp -> SMTP.sendMail smtp m + Nothing -> view awsEnv >>= \e -> AWS.execute e $ AWS.sendMail m ------------------------------------------------------------------------------- -- Unique Keys diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 95b3245da25..3f41449e971 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -70,7 +70,7 @@ import Brig.Types import Brig.User.Event import qualified Brig.User.Event.Log as Log import qualified Brig.User.Search.Index as Search -import Control.Lens ((.~), (?~), (^.), view) +import Control.Lens (view, (.~), (?~), (^.)) import Control.Retry import Data.Aeson hiding (json) import Data.ByteString.Conversion @@ -79,8 +79,7 @@ import Data.Coerce (coerce) import qualified Data.Currency as Currency import qualified Data.HashMap.Strict as M import Data.Id -import Data.Json.Util ((#), UTCTimeMillis) -import Data.Json.Util () +import Data.Json.Util (UTCTimeMillis, (#)) import Data.List.Split (chunksOf) import Data.List1 (List1, list1, singleton) import Data.Range @@ -95,7 +94,7 @@ 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 System.Logger.Class as Log hiding (name, (.=)) import Wire.API.Team.Feature (TeamFeatureName (..), TeamFeatureStatus) ----------------------------------------------------------------------------- @@ -266,23 +265,24 @@ rawPush (toList -> events) usrs orig route conn = do for_ events $ \e -> debug $ remote "gundeck" . msg (fst e) g <- view gundeck forM_ recipients $ \rcps -> - void . recovering x3 rpcHandlers $ const $ - rpc' - "gundeck" - g - ( method POST - . path "/i/push/v2" - . zUser orig - . json (map (mkPush rcps . snd) events) - . expect2xx - ) + void . recovering x3 rpcHandlers $ + const $ + rpc' + "gundeck" + g + ( method POST + . path "/i/push/v2" + . zUser orig + . json (map (mkPush rcps . snd) events) + . expect2xx + ) where recipients :: [Range 1 1024 (Set.Set Recipient)] recipients = - map (unsafeRange . Set.fromList) - $ chunksOf 512 - $ map (`recipient` route) - $ toList usrs + map (unsafeRange . Set.fromList) $ + chunksOf 512 $ + map (`recipient` route) $ + toList usrs mkPush :: Range 1 1024 (Set.Set Recipient) -> (Object, Maybe ApsData) -> Push mkPush rcps (o, aps) = newPush @@ -331,9 +331,9 @@ notifyContacts :: AppIO () notifyContacts events orig route conn = do env <- ask - notify events orig route conn - $ runAppT env - $ list1 orig <$> liftA2 (++) contacts teamContacts + notify events orig route conn $ + runAppT env $ + list1 orig <$> liftA2 (++) contacts teamContacts where contacts :: AppIO [UserId] contacts = lookupContactList orig @@ -403,13 +403,14 @@ toPushFormat (UserEvent (UserIdentityRemoved (UserIdentityRemovedData i e p))) = ) ] toPushFormat (ConnectionEvent (ConnectionUpdated uc _ name)) = - Just $ M.fromList $ - "type" .= ("user.connection" :: Text) - # "connection" .= uc - # "user" .= case name of - Just n -> Just $ object ["name" .= n] - Nothing -> Nothing - # [] + Just $ + M.fromList $ + "type" .= ("user.connection" :: Text) + # "connection" .= uc + # "user" .= case name of + Just n -> Just $ object ["name" .= n] + Nothing -> Nothing + # [] toPushFormat (UserEvent (UserSuspended i)) = Just $ M.fromList @@ -519,9 +520,9 @@ createConnectConv from to cname mess conn = do . remote "galley" . msg (val "Creating connect conversation") r <- galleyRequest POST req - maybe (error "invalid conv id") return - $ fromByteString - $ getHeader' "Location" r + maybe (error "invalid conv id") return $ + fromByteString $ + getHeader' "Location" r where req = path "/i/conversations/connect" @@ -668,15 +669,16 @@ rmClient u c = do . field "client" (BL.fromStrict cid) . msg (val "unregister push client") g <- view gundeck - void . recovering x3 rpcHandlers $ const $ - rpc' - "gundeck" - g - ( method DELETE - . paths ["i", "clients", cid] - . zUser u - . expect expected - ) + void . recovering x3 rpcHandlers $ + const $ + rpc' + "gundeck" + g + ( method DELETE + . paths ["i", "clients", cid] + . zUser u + . expect expected + ) where expected = [status200, status204, status404] @@ -728,9 +730,9 @@ createTeam u t@(Team.BindingNewTeam bt) teamid = do . msg (val "Creating Team") r <- galleyRequest PUT $ req teamid tid <- - maybe (error "invalid team id") return - $ fromByteString - $ getHeader' "Location" r + maybe (error "invalid team id") return $ + fromByteString $ + getHeader' "Location" r return (CreateUserTeam tid $ fromRange (bt ^. Team.newTeamName)) where req tid = @@ -837,9 +839,10 @@ getTeamLegalHoldStatus tid = do -- | Calls 'Galley.API.getSearchVisibilityInternalH'. getTeamSearchVisibility :: TeamId -> AppIO Team.TeamSearchVisibility -getTeamSearchVisibility tid = coerce @Team.TeamSearchVisibilityView @Team.TeamSearchVisibility <$> do - debug $ remote "galley" . msg (val "Get search visibility settings") - galleyRequest GET req >>= decodeBody "galley" +getTeamSearchVisibility tid = + coerce @Team.TeamSearchVisibilityView @Team.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"] diff --git a/services/brig/src/Brig/IO/Journal.hs b/services/brig/src/Brig/IO/Journal.hs index 799edc4f08f..fbb51f54167 100644 --- a/services/brig/src/Brig/IO/Journal.hs +++ b/services/brig/src/Brig/IO/Journal.hs @@ -60,17 +60,18 @@ userDelete :: UserId -> AppIO () userDelete uid = journalEvent UserEvent'USER_DELETE uid Nothing Nothing Nothing Nothing journalEvent :: UserEvent'EventType -> UserId -> Maybe Email -> Maybe Locale -> Maybe TeamId -> Maybe Name -> AppIO () -journalEvent typ uid em loc tid nm = view awsEnv >>= \env -> for_ (view AWS.userJournalQueue env) $ \queue -> do - ts <- now - rnd <- liftIO nextRandom - let userEvent :: UserEvent = - defMessage - & U.eventType .~ typ - & U.userId .~ (toBytes uid) - & U.utcTime .~ ts - & U.maybe'email .~ (toByteString' <$> em) - & U.maybe'locale .~ (pack . show <$> loc) - & U.maybe'teamId .~ (toBytes <$> tid) - & U.maybe'name .~ (toByteString' <$> nm) -- [] - encoded = fromStrict $ B64.encode $ encodeMessage userEvent - AWS.execute env (AWS.enqueueFIFO queue "user.events" rnd encoded) +journalEvent typ uid em loc tid nm = + view awsEnv >>= \env -> for_ (view AWS.userJournalQueue env) $ \queue -> do + ts <- now + rnd <- liftIO nextRandom + let userEvent :: UserEvent = + defMessage + & U.eventType .~ typ + & U.userId .~ (toBytes uid) + & U.utcTime .~ ts + & U.maybe'email .~ (toByteString' <$> em) + & U.maybe'locale .~ (pack . show <$> loc) + & U.maybe'teamId .~ (toBytes <$> tid) + & U.maybe'name .~ (toByteString' <$> nm) -- [] + encoded = fromStrict $ B64.encode $ encodeMessage userEvent + AWS.execute env (AWS.enqueueFIFO queue "user.events" rnd encoded) diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index 147eae33fcc..ba60819a926 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -99,13 +99,13 @@ runCommand l = \case ES.mkBHEnv (toESServer esURI) <$> newManager defaultManagerSettings initDb cas = - C.init - $ C.setLogger (C.mkLogger l) + C.init $ + C.setLogger (C.mkLogger l) . C.setContacts (view cHost cas) [] . C.setPortNumber (fromIntegral (view cPort cas)) . C.setKeyspace (view cKeyspace cas) . C.setProtocolVersion C.V4 - $ C.defSettings + $ C.defSettings waitForTaskToComplete :: forall a m. (ES.MonadBH m, MonadIO m, MonadThrow m, FromJSON a) => Int -> ES.TaskNodeId -> m () waitForTaskToComplete timeoutSeconds taskNodeId = do @@ -117,9 +117,10 @@ waitForTaskToComplete timeoutSeconds taskNodeId = do unless (ES.taskResponseCompleted task) $ do throwM $ ReindexFromAnotherIndexError $ "Timed out waiting for task: " <> show taskNodeId when (isJust $ ES.taskResponseError task) $ do - throwM $ ReindexFromAnotherIndexError $ - "Task failed with error: " - <> LensBS.unpackLazy8 (Aeson.encode $ ES.taskResponseError task) + throwM $ + ReindexFromAnotherIndexError $ + "Task failed with error: " + <> LensBS.unpackLazy8 (Aeson.encode $ ES.taskResponseError task) where isTaskComplete :: Either ES.EsError (ES.TaskResponse a) -> m Bool isTaskComplete (Left e) = throwM $ ReindexFromAnotherIndexError $ "Error response while getting task: " <> show e diff --git a/services/brig/src/Brig/Index/Migrations.hs b/services/brig/src/Brig/Index/Migrations.hs index 10987ddfaca..14ea80ebf4e 100644 --- a/services/brig/src/Brig/Index/Migrations.hs +++ b/services/brig/src/Brig/Index/Migrations.hs @@ -25,9 +25,9 @@ import qualified Brig.Index.Options as Opts import qualified Brig.User.Search.Index as Search import qualified Cassandra as C import qualified Cassandra.Settings as C -import Control.Lens ((^.), view) +import Control.Lens (view, (^.)) import Control.Monad.Catch (Exception, MonadThrow, finally, throwM) -import Data.Aeson ((.=), Value, object) +import Data.Aeson (Value, object, (.=)) import qualified Data.Metrics as Metrics import qualified Data.Text as Text import qualified Database.Bloodhound as ES @@ -79,13 +79,13 @@ mkEnv l es cas = (Opts.toESServer (es ^. Opts.esServer)) <$> HTTP.newManager HTTP.defaultManagerSettings initCassandra = - C.init - $ C.setLogger (C.mkLogger l) + C.init $ + C.setLogger (C.mkLogger l) . C.setContacts (view Opts.cHost cas) [] . C.setPortNumber (fromIntegral (view Opts.cPort cas)) . C.setKeyspace (view Opts.cKeyspace cas) . C.setProtocolVersion C.V4 - $ C.defSettings + $ C.defSettings initLogger = pure l createMigrationsIndexIfNotPresent :: (MonadThrow m, MonadIO m, ES.MonadBH m) => m () @@ -98,9 +98,9 @@ createMigrationsIndexIfNotPresent = >>= throwIfNotCreated PutMappingFailed where throwIfNotCreated err response = - unless (ES.isSuccess response) - $ throwM - $ err (show response) + unless (ES.isSuccess response) $ + throwM $ + err (show response) failIfIndexAbsent :: (MonadThrow m, MonadIO m, ES.MonadBH m) => ES.IndexName -> m () failIfIndexAbsent targetIndex = diff --git a/services/brig/src/Brig/Index/Migrations/Types.hs b/services/brig/src/Brig/Index/Migrations/Types.hs index 33e404ff0be..920ed007e1d 100644 --- a/services/brig/src/Brig/Index/Migrations/Types.hs +++ b/services/brig/src/Brig/Index/Migrations/Types.hs @@ -24,7 +24,7 @@ import qualified Brig.User.Search.Index as Search import qualified Cassandra as C import Control.Monad.Catch (MonadThrow) import Control.Monad.Reader (MonadReader (..), ReaderT, lift, runReaderT) -import Data.Aeson ((.:), (.=), FromJSON (..), ToJSON (..), object, withObject) +import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.=)) import Data.Metrics (Metrics) import qualified Database.Bloodhound as ES import Imports diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index ca9ca7a12aa..e4a02e8f3f3 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -55,9 +55,10 @@ onEvent n = handleTimeout $ case n of ~~ field "service" (toByteString sid) API.finishDeleteService pid sid where - handleTimeout act = timeout 60000000 act >>= \case - Just x -> pure x - Nothing -> throwM (InternalEventTimeout n) + handleTimeout act = + timeout 60000000 act >>= \case + Just x -> pure x + Nothing -> throwM (InternalEventTimeout n) data InternalEventException = -- | 'onEvent' has timed out diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 2e8ac3370d2..dd3c3a6a747 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -38,7 +38,7 @@ import Data.Scientific (toBoundedInteger) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import Data.Time.Clock (DiffTime, NominalDiffTime, secondsToDiffTime) -import Data.Yaml ((.:), (.:?), FromJSON (..), ToJSON (..)) +import Data.Yaml (FromJSON (..), ToJSON (..), (.:), (.:?)) import qualified Data.Yaml as Y import Imports import qualified Network.DNS as DNS @@ -569,10 +569,10 @@ instance FromJSON Timeout where parseJSON (Y.Number n) = let defaultV = 3600 bounded = toBoundedInteger n :: Maybe Int64 - in pure - $ Timeout - $ fromIntegral @Int - $ maybe defaultV fromIntegral bounded + in pure $ + Timeout $ + fromIntegral @Int $ + maybe defaultV fromIntegral bounded parseJSON v = typeMismatch "activationTimeout" v instance FromJSON Settings diff --git a/services/brig/src/Brig/Phone.hs b/services/brig/src/Brig/Phone.hs index d1c32501459..41520c0dc41 100644 --- a/services/brig/src/Brig/Phone.hs +++ b/services/brig/src/Brig/Phone.hs @@ -80,8 +80,9 @@ sendCall call = unless (isTestPhone $ Nexmo.callTo call) $ do cred <- view nexmoCreds withCallBudget (Nexmo.callTo call) $ do r <- - liftIO . try @_ @Nexmo.CallErrorResponse . recovering x3 nexmoHandlers $ const $ - Nexmo.sendCall cred m call + liftIO . try @_ @Nexmo.CallErrorResponse . recovering x3 nexmoHandlers $ + const $ + Nexmo.sendCall cred m call case r of Left ex -> case Nexmo.caStatus ex of Nexmo.CallDestinationNotPermitted -> unreachable ex @@ -135,9 +136,10 @@ sendSms loc SMSMessage {..} = unless (isTestPhone smsTo) $ do sendNexmoSms :: Manager -> AppIO () sendNexmoSms mgr = do crd <- view nexmoCreds - void . liftIO . recovering x3 nexmoHandlers $ const - $ Nexmo.sendMessage crd mgr - $ Nexmo.Message "Wire" smsTo smsText (toNexmoCharset loc) + void . liftIO . recovering x3 nexmoHandlers $ + const $ + Nexmo.sendMessage crd mgr $ + Nexmo.Message "Wire" smsTo smsText (toNexmoCharset loc) toNexmoCharset :: Locale -> Nexmo.Charset toNexmoCharset l = case fromLanguage (lLanguage l) of RU -> Nexmo.UCS2 @@ -151,8 +153,9 @@ sendSms loc SMSMessage {..} = unless (isTestPhone smsTo) $ do sendTwilioSms :: Manager -> AppIO () sendTwilioSms mgr = do crd <- view twilioCreds - void . liftIO . recovering x3 twilioHandlers $ const $ - Twilio.sendMessage crd mgr (Twilio.Message smsFrom smsTo smsText) + void . liftIO . recovering x3 twilioHandlers $ + const $ + Twilio.sendMessage crd mgr (Twilio.Message smsFrom smsTo smsText) nexmoFailed = [ Handler $ \(ex :: HttpException) -> return (Just (SomeException ex)), @@ -199,8 +202,10 @@ validatePhone (Phone p) c <- view twilioCreds m <- view httpManager r <- - liftIO . try @_ @Twilio.ErrorResponse $ recovering x3 httpHandlers $ const $ - Twilio.lookupPhone c m p LookupNoDetail Nothing + liftIO . try @_ @Twilio.ErrorResponse $ + recovering x3 httpHandlers $ + const $ + Twilio.lookupPhone c m p LookupNoDetail Nothing case r of Right x -> return (Just (Phone (Twilio.lookupE164 x))) Left e | Twilio.errStatus e == 404 -> return Nothing diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index d72d16131da..b2b27087678 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -47,35 +47,31 @@ import qualified Brig.Queue as Queue import Brig.Team.Util import Brig.Types.Client (Client (..), ClientType (..), newClient, newClientPrekeys) import Brig.Types.Intra (AccountStatus (..), UserAccount (..)) -import Brig.Types.Provider (DeleteProvider (..), PasswordChange (..), Provider (..), ProviderLogin (..), UpdateProvider (..)) -import Brig.Types.Provider (Service (..), ServiceProfile (..), ServiceToken (..)) -import Brig.Types.Provider (DeleteService (..), NewService (..), UpdateService (..), UpdateServiceConn (..), UpdateServiceWhitelist (..)) -import Brig.Types.Provider (AddBot (..), UpdateBotPrekeys (..)) +import Brig.Types.Provider (AddBot (..), DeleteProvider (..), DeleteService (..), NewService (..), PasswordChange (..), Provider (..), ProviderLogin (..), Service (..), ServiceProfile (..), ServiceToken (..), UpdateBotPrekeys (..), UpdateProvider (..), UpdateService (..), UpdateServiceConn (..), UpdateServiceWhitelist (..)) import qualified Brig.Types.Provider.External as Ext import Brig.Types.User (ManagedBy (..), Name (..), Pict (..), User (..), defaultAccentId) import qualified Brig.ZAuth as ZAuth import Control.Error (throwE) import Control.Exception.Enclosed (handleAny) -import Control.Lens ((^.), view) +import Control.Lens (view, (^.)) import Data.Aeson hiding (json) import Data.ByteString.Conversion import qualified Data.ByteString.Lazy.Char8 as LC8 -import Data.Conduit ((.|), runConduit) +import Data.Conduit (runConduit, (.|)) import qualified Data.Conduit.List as C import Data.Hashable (hash) import Data.Id import qualified Data.List as List import Data.List1 (maybeList1) import qualified Data.Map.Strict as Map -import Data.Misc ((<$$>), Fingerprint (..), Rsa) +import Data.Misc (Fingerprint (..), Rsa, (<$$>)) import Data.Predicate import Data.Range import qualified Data.Set as Set import qualified Data.Swagger.Build.Api as Doc import qualified Data.Text.Ascii as Ascii import qualified Data.Text.Encoding as Text -import Galley.Types (AccessRole (..), ConvMembers (..), ConvType (..), Conversation (..)) -import Galley.Types (OtherMember (..)) +import Galley.Types (AccessRole (..), ConvMembers (..), ConvType (..), Conversation (..), OtherMember (..)) import Galley.Types.Bot (newServiceRef, serviceRefId, serviceRefProvider) import Galley.Types.Conversations.Roles (roleNameWireAdmin) import qualified Galley.Types.Teams as Teams @@ -784,15 +780,16 @@ updateServiceWhitelist uid con tid upd = do (True, False) -> do -- When the service is de-whitelisted, remove its bots from team -- conversations - lift $ runConduit $ - User.lookupServiceUsersForTeam pid sid tid - .| C.mapM_ - ( pooledMapConcurrentlyN_ - 16 - ( \(bid, cid) -> - deleteBot uid (Just con) bid cid - ) - ) + lift $ + runConduit $ + User.lookupServiceUsersForTeam pid sid tid + .| C.mapM_ + ( pooledMapConcurrentlyN_ + 16 + ( \(bid, cid) -> + deleteBot uid (Just con) bid cid + ) + ) DB.deleteServiceWhitelist (Just tid) pid sid return UpdateServiceWhitelistRespChanged @@ -1007,20 +1004,21 @@ deleteBot zusr zcon bid cid = do return ev validateServiceKey :: MonadIO m => Public.ServiceKeyPEM -> m (Maybe (Public.ServiceKey, Fingerprint Rsa)) -validateServiceKey pem = liftIO $ - readPublicKey >>= \pk -> - case join (SSL.toPublicKey <$> pk) of - Nothing -> return Nothing - Just pk' -> do - Just sha <- SSL.getDigestByName "SHA256" - let size = SSL.rsaSize (pk' :: SSL.RSAPubKey) - if size < minRsaKeySize - then return Nothing - else do - fpr <- Fingerprint <$> SSL.rsaFingerprint sha pk' - let bits = fromIntegral size * 8 - let key = Public.ServiceKey Public.RsaServiceKey bits pem - return $ Just (key, fpr) +validateServiceKey pem = + liftIO $ + readPublicKey >>= \pk -> + case join (SSL.toPublicKey <$> pk) of + Nothing -> return Nothing + Just pk' -> do + Just sha <- SSL.getDigestByName "SHA256" + let size = SSL.rsaSize (pk' :: SSL.RSAPubKey) + if size < minRsaKeySize + then return Nothing + else do + fpr <- Fingerprint <$> SSL.rsaFingerprint sha pk' + let bits = fromIntegral size * 8 + let key = Public.ServiceKey Public.RsaServiceKey bits pem + return $ Just (key, fpr) where readPublicKey = handleAny diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index 44e30b5b427..c9adc1089a8 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -65,7 +65,7 @@ updateAccountProfile :: Maybe HttpsUrl -> Maybe Text -> m () -updateAccountProfile p name url descr = retry x5 $ batch $ do +updateAccountProfile p name url descr = retry x5 . batch $ do setType BatchUnLogged setConsistency Quorum for_ name $ \x -> addPrepQuery cqlName (x, p) @@ -110,10 +110,10 @@ lookupPassword :: ProviderId -> m (Maybe Password) lookupPassword p = - fmap (fmap runIdentity) - $ retry x1 - $ query1 cql - $ params Quorum (Identity p) + fmap (fmap runIdentity) $ + retry x1 $ + query1 cql $ + params Quorum (Identity p) where cql :: PrepQuery R (Identity ProviderId) (Identity Password) cql = "SELECT password FROM provider WHERE id = ?" @@ -148,7 +148,7 @@ insertKey :: Maybe EmailKey -> EmailKey -> m () -insertKey p old new = retry x5 $ batch $ do +insertKey p old new = retry x5 . batch $ do setConsistency Quorum setType BatchLogged for_ old $ \old' -> addPrepQuery cqlKeyDelete (Identity (emailKeyUniq old')) @@ -167,10 +167,10 @@ lookupKey :: EmailKey -> m (Maybe ProviderId) lookupKey k = - fmap (fmap runIdentity) - $ retry x1 - $ query1 cql - $ params Quorum (Identity (emailKeyUniq k)) + fmap (fmap runIdentity) $ + retry x1 $ + query1 cql $ + params Quorum (Identity (emailKeyUniq k)) where cql :: PrepQuery R (Identity Text) (Identity ProviderId) cql = "SELECT provider FROM provider_keys WHERE key = ?" @@ -200,10 +200,11 @@ insertService :: insertService pid name summary descr url token key fprint assets tags = do sid <- randomId let tagSet = C.Set (Set.toList tags) - retry x5 $ write cql $ - params - Quorum - (pid, sid, name, summary, descr, url, [token], [key], [fprint], assets, tagSet, False) + retry x5 $ + write cql $ + params + Quorum + (pid, sid, name, summary, descr, url, [token], [key], [fprint], assets, tagSet, False) return sid where cql :: @@ -234,10 +235,10 @@ lookupService :: ServiceId -> m (Maybe Service) lookupService pid sid = - fmap (fmap mk) - $ retry x1 - $ query1 cql - $ params Quorum (pid, sid) + fmap (fmap mk) $ + retry x1 $ + query1 cql $ + params Quorum (pid, sid) where cql :: PrepQuery @@ -255,10 +256,10 @@ listServices :: ProviderId -> m [Service] listServices p = - fmap (map mk) - $ retry x1 - $ query cql - $ params Quorum (Identity p) + fmap (map mk) $ + retry x1 $ + query cql $ + params Quorum (Identity p) where cql :: PrepQuery @@ -285,7 +286,7 @@ updateService :: Maybe (RangedServiceTags, RangedServiceTags) -> Bool -> m () -updateService pid sid svcName svcTags nameChange summary descr assets tagsChange enabled = retry x5 $ batch $ do +updateService pid sid svcName svcTags nameChange summary descr assets tagsChange enabled = retry x5 . batch $ do setConsistency Quorum setType BatchUnLogged -- If there is a name change, update the service name; if enabled, update indexes @@ -330,7 +331,7 @@ deleteService pid sid name tags = do -- (or as a part of the last batch, in this case) because otherwise API -- consumers won't be able to retry a half-done 'deleteService' call. deleteServiceWhitelist Nothing pid sid - retry x5 $ batch $ do + retry x5 . batch $ do setConsistency Quorum setType BatchUnLogged addPrepQuery cql (pid, sid) @@ -350,10 +351,10 @@ lookupServiceProfile :: ServiceId -> m (Maybe ServiceProfile) lookupServiceProfile p s = - fmap (fmap mk) - $ retry x1 - $ query1 cql - $ params One (p, s) + fmap (fmap mk) $ + retry x1 $ + query1 cql $ + params One (p, s) where cql :: PrepQuery R (ProviderId, ServiceId) (Name, Maybe Text, Text, [Asset], C.Set ServiceTag, Bool) cql = @@ -369,10 +370,10 @@ listServiceProfiles :: ProviderId -> m [ServiceProfile] listServiceProfiles p = - fmap (map mk) - $ retry x1 - $ query cql - $ params One (Identity p) + fmap (map mk) $ + retry x1 $ + query cql $ + params One (Identity p) where cql :: PrepQuery @@ -405,10 +406,10 @@ lookupServiceConn :: ServiceId -> m (Maybe ServiceConn) lookupServiceConn pid sid = - fmap (fmap mk) - $ retry x1 - $ query1 cql - $ params Quorum (pid, sid) + fmap (fmap mk) $ + retry x1 $ + query1 cql $ + params Quorum (pid, sid) where cql :: PrepQuery R (ProviderId, ServiceId) (HttpsUrl, List1 ServiceToken, List1 (Fingerprint Rsa), Bool) cql = @@ -426,7 +427,7 @@ updateServiceConn :: Maybe (List1 (ServiceKey, Fingerprint Rsa)) -> Maybe Bool -> m () -updateServiceConn pid sid url tokens keys enabled = retry x5 $ batch $ do +updateServiceConn pid sid url tokens keys enabled = retry x5 . batch $ do setConsistency Quorum setType BatchLogged for_ url $ \x -> addPrepQuery cqlBaseUrl (x, pid, sid) @@ -458,7 +459,7 @@ insertServiceIndexes :: RangedServiceTags -> m () insertServiceIndexes pid sid name tags = - retry x5 $ batch $ do + retry x5 . batch $ do setConsistency Quorum setType BatchLogged insertServicePrefix pid sid name @@ -472,7 +473,7 @@ deleteServiceIndexes :: RangedServiceTags -> m () deleteServiceIndexes pid sid name tags = - retry x5 $ batch $ do + retry x5 . batch $ do setConsistency Quorum setType BatchLogged deleteServicePrefix sid name @@ -686,8 +687,9 @@ paginateServiceNames mbPrefix size providerFilter = liftClient $ do \FROM service_prefix \ \WHERE prefix = ? AND name >= ?" p <- - retry x1 $ paginate cql $ - paramsP One (mkPrefixIndex (Name prefix), prefix) len + retry x1 $ + paginate cql $ + paramsP One (mkPrefixIndex (Name prefix), prefix) len return $! p {result = trim size (result p)} -- Pagination utilities @@ -718,7 +720,7 @@ resolveRow (_, pid, sid) = lookupServiceProfile pid sid insertServiceWhitelist :: MonadClient m => TeamId -> ProviderId -> ServiceId -> m () insertServiceWhitelist tid pid sid = - retry x5 $ batch $ do + retry x5 . batch $ do addPrepQuery insert1 (tid, pid, sid) addPrepQuery insert1Rev (tid, pid, sid) where @@ -740,13 +742,13 @@ deleteServiceWhitelist :: MonadClient m => Maybe TeamId -> ProviderId -> Service deleteServiceWhitelist mbTid pid sid = case mbTid of Nothing -> do teams <- retry x5 $ query lookupRev $ params Quorum (pid, sid) - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery deleteAllRev (pid, sid) for_ teams $ \(Identity tid) -> addPrepQuery delete1 (tid, pid, sid) Just tid -> - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery delete1 (tid, pid, sid) diff --git a/services/brig/src/Brig/Provider/RPC.hs b/services/brig/src/Brig/Provider/RPC.hs index 5bbbd436c38..faef52a3e19 100644 --- a/services/brig/src/Brig/Provider/RPC.hs +++ b/services/brig/src/Brig/Provider/RPC.hs @@ -36,7 +36,7 @@ import Brig.RPC import Brig.Types.Provider (HttpsUrl (..)) import Brig.Types.Provider.External import Control.Error -import Control.Lens ((^.), set, view) +import Control.Lens (set, view, (^.)) import Control.Monad.Catch import Control.Retry (recovering) import Data.Aeson @@ -70,10 +70,13 @@ createBot scon new = do let fprs = toList (sconFingerprints scon) (man, verifyFingerprints) <- view extGetManager extHandleAll onExc $ do - rs <- lift $ recovering x3 httpHandlers $ const $ liftIO - $ withVerifiedSslConnection (verifyFingerprints fprs) man reqBuilder - $ \req -> - Http.httpLbs req man + rs <- lift $ + recovering x3 httpHandlers $ + const $ + liftIO $ + withVerifiedSslConnection (verifyFingerprints fprs) man reqBuilder $ + \req -> + Http.httpLbs req man case Bilge.statusCode rs of 201 -> decodeBytes "External" (responseBody rs) 409 -> throwE ServiceBotConflict diff --git a/services/brig/src/Brig/Queue.hs b/services/brig/src/Brig/Queue.hs index bf0f292ae0e..31bf303774d 100644 --- a/services/brig/src/Brig/Queue.hs +++ b/services/brig/src/Brig/Queue.hs @@ -29,7 +29,7 @@ import Brig.Options import qualified Brig.Queue.Stomp as Stomp import Brig.Queue.Types import Control.Exception (ErrorCall (..)) -import Control.Lens ((^.), view) +import Control.Lens (view, (^.)) import Control.Monad.Catch import Data.Aeson import qualified Data.ByteString.Base16 as B16 diff --git a/services/brig/src/Brig/Queue/Stomp.hs b/services/brig/src/Brig/Queue/Stomp.hs index cbb1d3b698b..75d4462876d 100644 --- a/services/brig/src/Brig/Queue/Stomp.hs +++ b/services/brig/src/Brig/Queue/Stomp.hs @@ -94,19 +94,20 @@ enqueue b q m = retryPredicate _ res = pure (isLeft res) retryPolicy = limitRetries 5 <> exponentialBackoff 50000 enqueueAction = - liftIO $ try @StomplException - $ stompTimeout "enqueue" 500000 - $ withConnection' b - $ \conn -> - withWriter - conn - (unpack q) - (unpack q) - [OWithReceipt, OWaitReceipt] - [] - oconv - $ \w -> - writeQ w jsonType [("persistent", "true")] m + liftIO $ + try @StomplException $ + stompTimeout "enqueue" 500000 $ + withConnection' b $ + \conn -> + withWriter + conn + (unpack q) + (unpack q) + [OWithReceipt, OWaitReceipt] + [] + oconv + $ \w -> + writeQ w jsonType [("persistent", "true")] m -- Note [receipts] -- ~~~ @@ -159,7 +160,7 @@ listen b q callback = runInIO $ callback (msgContent m) stompTimeout "listen/ack" 1000000 $ ack conn m handlers = skipAsyncExceptions ++ [logError] - logError = const $ Handler $ \(e :: SomeException) -> do + logError = const . Handler $ \(e :: SomeException) -> do Log.err $ msg (val "Exception when listening to a STOMP queue") ~~ field "queue" (show q) @@ -204,8 +205,10 @@ withConnection' b = -- | Like 'timeout', but throws an 'AppException' instead of returning a -- 'Maybe'. Not very composable, but kinda convenient here. stompTimeout :: String -> Int -> IO a -> IO a -stompTimeout location t act = timeout t act >>= \case - Just x -> pure x - Nothing -> - throwIO $ AppException $ - location <> ": STOMP request took more than " <> show t <> "mcs and has timed out" +stompTimeout location t act = + timeout t act >>= \case + Just x -> pure x + Nothing -> + throwIO $ + AppException $ + location <> ": STOMP request took more than " <> show t <> "mcs and has timed out" diff --git a/services/brig/src/Brig/RPC.hs b/services/brig/src/Brig/RPC.hs index 9dca1c62ebd..1bb3a0f9419 100644 --- a/services/brig/src/Brig/RPC.hs +++ b/services/brig/src/Brig/RPC.hs @@ -35,7 +35,7 @@ import Imports import Network.HTTP.Client (HttpException (..), HttpExceptionContent (..), checkResponse) import Network.HTTP.Types.Method import Network.HTTP.Types.Status -import System.Logger.Class hiding ((.=), name) +import System.Logger.Class hiding (name, (.=)) x3 :: RetryPolicy x3 = limitRetries 3 <> exponentialBackoff 100000 @@ -55,9 +55,9 @@ expect ss rq = rq {checkResponse = check} check rq' rs = do let s = responseStatus rs rs' = rs {responseBody = ()} - when (statusIsServerError s || s `notElem` ss) - $ throwM - $ HttpExceptionRequest rq' (StatusCodeException rs' mempty) + when (statusIsServerError s || s `notElem` ss) $ + throwM $ + HttpExceptionRequest rq' (StatusCodeException rs' mempty) cargoholdRequest :: StdMethod -> @@ -85,8 +85,9 @@ serviceRequest :: AppIO (Response (Maybe BL.ByteString)) serviceRequest nm svc m r = do service <- view svc - recovering x3 rpcHandlers $ const $ - rpc' nm service (method m . r) + recovering x3 rpcHandlers $ + const $ + rpc' nm service (method m . r) -- | Failed to parse a response from another service. data ParseException = ParseException diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index f4f25cc9da1..1c573a72a03 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -53,14 +53,14 @@ run o = do (app, e) <- mkApp o s <- Server.newSettings (server e) internalEventListener <- - Async.async - $ runAppT e - $ Queue.listen (e ^. internalEvents) Internal.onEvent + Async.async $ + runAppT e $ + Queue.listen (e ^. internalEvents) Internal.onEvent let throttleMillis = fromMaybe defSqsThrottleMillis $ setSqsThrottleMillis (optSettings o) emailListener <- for (e ^. awsEnv . sesQueue) $ \q -> - Async.async - $ AWS.execute (e ^. awsEnv) - $ AWS.listen throttleMillis q (runAppT e . SesNotification.onEvent) + Async.async $ + AWS.execute (e ^. awsEnv) $ + AWS.listen throttleMillis q (runAppT e . SesNotification.onEvent) sftDiscovery <- forM (e ^. sftEnv) $ Async.async . Calling.startSFTServiceDiscovery (e ^. applog) runSettingsWithShutdown s app 5 `finally` do mapM_ Async.cancel emailListener diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index f35071ea2b7..b5684696593 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -41,7 +41,7 @@ import Brig.Types.Team (TeamSize) import Brig.Types.Team.Invitation import Brig.Types.User (Email, InvitationCode, emailIdentity) import qualified Brig.User.Search.Index as ESIndex -import Control.Lens ((^.), view) +import Control.Lens (view, (^.)) import Data.Aeson hiding (json) import Data.ByteString.Conversion import Data.Id diff --git a/services/brig/src/Brig/Team/DB.hs b/services/brig/src/Brig/Team/DB.hs index 2b8763ff63b..1dcb15bef6c 100644 --- a/services/brig/src/Brig/Team/DB.hs +++ b/services/brig/src/Brig/Team/DB.hs @@ -44,7 +44,7 @@ import Brig.Types.Common import Brig.Types.Team.Invitation import Brig.Types.User import Cassandra as C -import Data.Conduit ((.|), runConduit) +import Data.Conduit (runConduit, (.|)) import qualified Data.Conduit.List as C import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) @@ -93,7 +93,7 @@ insertInvitation t role email (toUTCTimeMillis -> now) minviter inviteeName phon iid <- liftIO mkInvitationId code <- liftIO mkInvitationCode let inv = Invitation t role iid email now minviter inviteeName phone - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery cqlInvitation (t, role, iid, code, email, now, minviter, inviteeName, phone, round timeout) @@ -118,9 +118,10 @@ lookupInvitation t r = cqlInvitation = "SELECT team, role, id, email, created_at, created_by, name, phone FROM team_invitation WHERE team = ? AND id = ?" lookupInvitationByCode :: MonadClient m => InvitationCode -> m (Maybe Invitation) -lookupInvitationByCode i = lookupInvitationInfo i >>= \case - Just InvitationInfo {..} -> lookupInvitation iiTeam iiInvId - _ -> return Nothing +lookupInvitationByCode i = + lookupInvitationInfo i >>= \case + Just InvitationInfo {..} -> lookupInvitation iiTeam iiInvId + _ -> return Nothing lookupInvitationCode :: MonadClient m => TeamId -> InvitationId -> m (Maybe InvitationCode) lookupInvitationCode t r = @@ -159,7 +160,7 @@ deleteInvitation :: MonadClient m => TeamId -> InvitationId -> m () deleteInvitation t i = do codeEmail <- lookupInvitationCodeEmail t i case codeEmail of - Just (invCode, invEmail) -> retry x5 $ batch $ do + Just (invCode, invEmail) -> retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery cqlInvitation (t, i) @@ -177,10 +178,10 @@ deleteInvitation t i = do deleteInvitations :: (MonadClient m, MonadUnliftIO m) => TeamId -> m () deleteInvitations t = - liftClient - $ runConduit - $ paginateC cqlSelect (paramsP Quorum (Identity t) 100) x1 - .| C.mapM_ (pooledMapConcurrentlyN_ 16 (deleteInvitation t . runIdentity)) + liftClient $ + runConduit $ + paginateC cqlSelect (paramsP Quorum (Identity t) 100) x1 + .| C.mapM_ (pooledMapConcurrentlyN_ 16 (deleteInvitation t . runIdentity)) where cqlSelect :: PrepQuery R (Identity TeamId) (Identity InvitationId) cqlSelect = "SELECT id FROM team_invitation WHERE team = ? ORDER BY id ASC" @@ -197,9 +198,10 @@ lookupInvitationInfo ic@(InvitationCode c) cqlInvitationInfo = "SELECT team, id FROM team_invitation_info WHERE code = ?" lookupInvitationByEmail :: (Log.MonadLogger m, MonadClient m) => Email -> m (Maybe Invitation) -lookupInvitationByEmail e = lookupInvitationInfoByEmail e >>= \case - InvitationByEmail InvitationInfo {..} -> lookupInvitation iiTeam iiInvId - _ -> return Nothing +lookupInvitationByEmail e = + lookupInvitationInfoByEmail e >>= \case + InvitationByEmail InvitationInfo {..} -> lookupInvitation iiTeam iiInvId + _ -> return Nothing lookupInvitationInfoByEmail :: (Log.MonadLogger m, MonadClient m) => Email -> m InvitationByEmail lookupInvitationInfoByEmail email = do diff --git a/services/brig/src/Brig/Template.hs b/services/brig/src/Brig/Template.hs index 1751143e23c..91e6e732a7b 100644 --- a/services/brig/src/Brig/Template.hs +++ b/services/brig/src/Brig/Template.hs @@ -79,7 +79,8 @@ readLocalesDir defLocale base typ load = do -- Ignore locales if no such directory exist for the locale ls <- filterM (doesDirectoryExist . basePath) - . filter (/= defLocaleDir) =<< listDirectory base + . filter (/= defLocaleDir) + =<< listDirectory base Map.fromList . zip (map readLocale ls) <$> mapM (load . basePath) ls where basePath :: FilePath -> FilePath diff --git a/services/brig/src/Brig/Unique.hs b/services/brig/src/Brig/Unique.hs index dfa8051cde1..2fc7849d22f 100644 --- a/services/brig/src/Brig/Unique.hs +++ b/services/brig/src/Brig/Unique.hs @@ -96,10 +96,10 @@ deleteClaim u v t = do -- | Lookup the current claims on a value. lookupClaims :: MonadClient m => Text -> m [Id a] lookupClaims v = - fmap (maybe [] (fromSet . runIdentity)) - $ retry x1 - $ query1 cql - $ params Quorum (Identity v) + fmap (maybe [] (fromSet . runIdentity)) $ + retry x1 $ + query1 cql $ + params Quorum (Identity v) where cql :: PrepQuery R (Identity Text) (Identity (C.Set (Id a))) cql = "SELECT claims FROM unique_claims WHERE value = ?" diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index f4a0ec7be2b..251e094d446 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -99,11 +99,12 @@ sendLoginCode phone call force = do return c lookupLoginCode :: Phone -> AppIO (Maybe PendingLoginCode) -lookupLoginCode phone = Data.lookupKey (userPhoneKey phone) >>= \case - Nothing -> return Nothing - Just u -> do - Log.debug $ field "user" (toByteString u) . field "action" (Log.val "User.lookupLoginCode") - Data.lookupLoginCode u +lookupLoginCode phone = + Data.lookupKey (userPhoneKey phone) >>= \case + Nothing -> return Nothing + Just u -> do + Log.debug $ field "user" (toByteString u) . field "action" (Log.val "User.lookupLoginCode") + Data.lookupLoginCode u login :: Login -> CookieType -> ExceptT LoginError AppIO (Access ZAuth.User) login (PasswordLogin li pw label) typ = do diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index daf97b573ca..037c2b8016b 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -136,20 +136,21 @@ renewCookie old = do -- 'newCookieLimited' if there is a chance that the user should be suspended (we don't do it -- implicitly because of cyclical dependencies). mustSuspendInactiveUser :: UserId -> AppIO Bool -mustSuspendInactiveUser uid = view (settings . to setSuspendInactiveUsers) >>= \case - Nothing -> pure False - Just (SuspendInactiveUsers (Timeout suspendAge)) -> do - now <- liftIO =<< view currentTime - let suspendHere :: UTCTime - suspendHere = addUTCTime (- suspendAge) now - youngEnough :: Cookie () -> Bool - youngEnough = (>= suspendHere) . cookieCreated - ckies <- listCookies uid [] - let mustSuspend - | null ckies = False - | any youngEnough ckies = False - | otherwise = True - pure mustSuspend +mustSuspendInactiveUser uid = + view (settings . to setSuspendInactiveUsers) >>= \case + Nothing -> pure False + Just (SuspendInactiveUsers (Timeout suspendAge)) -> do + now <- liftIO =<< view currentTime + let suspendHere :: UTCTime + suspendHere = addUTCTime (- suspendAge) now + youngEnough :: Cookie () -> Bool + youngEnough = (>= suspendHere) . cookieCreated + ckies <- listCookies uid [] + let mustSuspend + | null ckies = False + | any youngEnough ckies = False + | otherwise = True + pure mustSuspend newAccessToken :: forall u a. ZAuth.TokenPair u a => Cookie (ZAuth.Token u) -> Maybe (ZAuth.Token a) -> AppIO AccessToken newAccessToken c mt = do diff --git a/services/brig/src/Brig/User/Auth/DB/Cookie.hs b/services/brig/src/Brig/User/Auth/DB/Cookie.hs index 9d8dae2a5ca..9654883f479 100644 --- a/services/brig/src/Brig/User/Auth/DB/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/DB/Cookie.hs @@ -88,7 +88,7 @@ listCookies u = } deleteCookies :: MonadClient m => UserId -> [Cookie a] -> m () -deleteCookies u cs = retry x5 $ batch $ do +deleteCookies u cs = retry x5 . batch $ do setType BatchUnLogged setConsistency Quorum for_ cs $ \c -> addPrepQuery cql (u, cookieExpires c, cookieId c) diff --git a/services/brig/src/Brig/User/Handle.hs b/services/brig/src/Brig/User/Handle.hs index 40f9b0b3f1c..9ee4d21a2cc 100644 --- a/services/brig/src/Brig/User/Handle.hs +++ b/services/brig/src/Brig/User/Handle.hs @@ -42,16 +42,16 @@ claimHandle u h = do _ -> do env <- ask let key = "@" <> fromHandle h - claimed <- withClaim (userId u) key (30 # Minute) - $ runAppT env - $ do - -- Record ownership - retry x5 $ write handleInsert (params Quorum (h, userId u)) - -- Update profile - User.updateHandle (userId u) h - -- Free old handle (if it changed) - for_ (mfilter (/= h) (userHandle u)) $ - freeHandle u + claimed <- withClaim (userId u) key (30 # Minute) $ + runAppT env $ + do + -- Record ownership + retry x5 $ write handleInsert (params Quorum (h, userId u)) + -- Update profile + User.updateHandle (userId u) h + -- Free old handle (if it changed) + for_ (mfilter (/= h) (userHandle u)) $ + freeHandle u return (isJust claimed) -- | Free a 'Handle', making it available to be claimed again. diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index b3ba41883c0..95ceb2ec267 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -82,13 +82,13 @@ import Network.HTTP.Client hiding (path) import Network.HTTP.Types (hContentType, statusCode) import qualified System.Logger as Log import System.Logger.Class - ( (+++), - Logger, + ( Logger, MonadLogger (..), field, info, msg, val, + (+++), (~~), ) @@ -246,30 +246,30 @@ defaultUserQuery u teamSearchInfo (normalized -> term') = mkUserQuery :: UserId -> TeamSearchInfo -> ES.Query -> IndexQuery Contact mkUserQuery (review _TextId -> self) teamSearchInfo q = - IndexQuery q - $ ES.Filter . ES.QueryBoolQuery - $ boolQuery - { ES.boolQueryMustNotMatch = [termQ "_id" self], - ES.boolQueryMustMatch = - [ optionallySearchWithinTeam teamSearchInfo, - ES.QueryBoolQuery - boolQuery - { ES.boolQueryShouldMatch = - [ termQ "account_status" "active", - -- Also match entries where the account_status field is not present. - -- These must have been inserted before we added the account_status - -- and at that time we only inserted active users in the first place. - -- This should be unnecessary after re-indexing, but let's be lenient - -- here for a while. - ES.QueryBoolQuery - boolQuery - { ES.boolQueryMustNotMatch = - [ES.QueryExistsQuery (ES.FieldName "account_status")] - } - ] - } - ] - } + IndexQuery q $ + ES.Filter . ES.QueryBoolQuery $ + boolQuery + { ES.boolQueryMustNotMatch = [termQ "_id" self], + ES.boolQueryMustMatch = + [ optionallySearchWithinTeam teamSearchInfo, + ES.QueryBoolQuery + boolQuery + { ES.boolQueryShouldMatch = + [ termQ "account_status" "active", + -- Also match entries where the account_status field is not present. + -- These must have been inserted before we added the account_status + -- and at that time we only inserted active users in the first place. + -- This should be unnecessary after re-indexing, but let's be lenient + -- here for a while. + ES.QueryBoolQuery + boolQuery + { ES.boolQueryMustNotMatch = + [ES.QueryExistsQuery (ES.FieldName "account_status")] + } + ] + } + ] + } where termQ f v = ES.TermQuery @@ -477,8 +477,9 @@ updateMapping = liftIndexIO $ do -- FUTUREWORK: check return code (ES.isSuccess) and fail if appropriate. -- But to do that we have to consider the consequences of this failing in our helm chart: -- https://github.com/wireapp/wire-server-deploy/blob/92311d189818ffc5e26ff589f81b95c95de8722c/charts/elasticsearch-index/templates/create-index.yaml - void $ traceES "Put mapping" $ - ES.putMapping idx (ES.MappingName "user") indexMapping + void $ + traceES "Put mapping" $ + ES.putMapping idx (ES.MappingName "user") indexMapping resetIndex :: MonadIndexIO m => @@ -488,9 +489,10 @@ resetIndex :: m () resetIndex settings shardCount = liftIndexIO $ do idx <- asks idxName - gone <- ES.indexExists idx >>= \case - True -> ES.isSuccess <$> traceES "Delete Index" (ES.deleteIndex idx) - False -> return True + gone <- + ES.indexExists idx >>= \case + True -> ES.isSuccess <$> traceES "Delete Index" (ES.deleteIndex idx) + False -> return True if gone then createIndex settings shardCount else throwM (IndexError "Index deletion failed.") @@ -784,10 +786,10 @@ reindexRowToIndexUser (u, mteam, name, t0, status, t1, handle, t2, colour, t4, a then iu & set iuTeam mteam - . set iuName (Just name) - . set iuHandle handle - . set iuColourId (Just colour) - . set iuAccountStatus status + . set iuName (Just name) + . set iuHandle handle + . set iuColourId (Just colour) + . set iuAccountStatus status else iu -- We insert a tombstone-style user here, as it's easier than deleting the old one. diff --git a/services/brig/src/Brig/ZAuth.hs b/services/brig/src/Brig/ZAuth.hs index 1ce133b3814..f357959b8ba 100644 --- a/services/brig/src/Brig/ZAuth.hs +++ b/services/brig/src/Brig/ZAuth.hs @@ -88,7 +88,7 @@ module Brig.ZAuth ) where -import Control.Lens (Lens', (^.), makeLenses, over) +import Control.Lens (Lens', makeLenses, over, (^.)) import Control.Monad.Catch import Data.Aeson import Data.Bits @@ -292,51 +292,58 @@ instance UserTokenLike LegalHoldUser where mkUserToken' :: MonadZAuth m => UserId -> Word32 -> UTCTime -> m UserToken mkUserToken' u r t = liftZAuth $ do z <- ask - liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - ZC.newToken (utcTimeToPOSIXSeconds t) U Nothing (mkUser (toUUID u) r) + liftIO $ + ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ + ZC.newToken (utcTimeToPOSIXSeconds t) U Nothing (mkUser (toUUID u) r) newUserToken' :: MonadZAuth m => UserId -> m UserToken newUserToken' u = liftZAuth $ do z <- ask r <- liftIO randomValue - liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - let UserTokenTimeout ttl = z ^. settings . userTokenTimeout - in ZC.userToken ttl (toUUID u) r + liftIO $ + ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ + let UserTokenTimeout ttl = z ^. settings . userTokenTimeout + in ZC.userToken ttl (toUUID u) r newSessionToken' :: MonadZAuth m => UserId -> m UserToken newSessionToken' u = liftZAuth $ do z <- ask r <- liftIO randomValue - liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - let SessionTokenTimeout ttl = z ^. settings . sessionTokenTimeout - in ZC.sessionToken ttl (toUUID u) r + liftIO $ + ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ + let SessionTokenTimeout ttl = z ^. settings . sessionTokenTimeout + in ZC.sessionToken ttl (toUUID u) r newAccessToken' :: MonadZAuth m => UserToken -> m AccessToken newAccessToken' xt = liftZAuth $ do z <- ask - liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - let AccessTokenTimeout ttl = z ^. settings . accessTokenTimeout - in ZC.accessToken1 ttl (xt ^. body . user) + liftIO $ + ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ + let AccessTokenTimeout ttl = z ^. settings . accessTokenTimeout + in ZC.accessToken1 ttl (xt ^. body . user) renewAccessToken' :: MonadZAuth m => AccessToken -> m AccessToken renewAccessToken' old = liftZAuth $ do z <- ask - liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - let AccessTokenTimeout ttl = z ^. settings . accessTokenTimeout - in ZC.renewToken ttl old + liftIO $ + ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ + let AccessTokenTimeout ttl = z ^. settings . accessTokenTimeout + in ZC.renewToken ttl old newBotToken :: MonadZAuth m => ProviderId -> BotId -> ConvId -> m BotToken newBotToken pid bid cid = liftZAuth $ do z <- ask - liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - ZC.botToken (toUUID pid) (toUUID (botUserId bid)) (toUUID cid) + liftIO $ + ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ + ZC.botToken (toUUID pid) (toUUID (botUserId bid)) (toUUID cid) newProviderToken :: MonadZAuth m => ProviderId -> m ProviderToken newProviderToken pid = liftZAuth $ do z <- ask - liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - let ProviderTokenTimeout ttl = z ^. settings . providerTokenTimeout - in ZC.providerToken ttl (toUUID pid) + liftIO $ + ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ + let ProviderTokenTimeout ttl = z ^. settings . providerTokenTimeout + in ZC.providerToken ttl (toUUID pid) -- FUTUREWORK: this function is very similar to mkUserToken', -- the differences are @@ -347,30 +354,34 @@ newProviderToken pid = liftZAuth $ do mkLegalHoldUserToken :: MonadZAuth m => UserId -> Word32 -> UTCTime -> m LegalHoldUserToken mkLegalHoldUserToken u r t = liftZAuth $ do z <- ask - liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - ZC.newToken (utcTimeToPOSIXSeconds t) LU Nothing (mkLegalHoldUser (toUUID u) r) + liftIO $ + ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ + ZC.newToken (utcTimeToPOSIXSeconds t) LU Nothing (mkLegalHoldUser (toUUID u) r) newLegalHoldUserToken :: MonadZAuth m => UserId -> m LegalHoldUserToken newLegalHoldUserToken u = liftZAuth $ do z <- ask r <- liftIO randomValue - liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - let LegalHoldUserTokenTimeout ttl = z ^. settings . legalHoldUserTokenTimeout - in ZC.legalHoldUserToken ttl (toUUID u) r + liftIO $ + ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ + let LegalHoldUserTokenTimeout ttl = z ^. settings . legalHoldUserTokenTimeout + in ZC.legalHoldUserToken ttl (toUUID u) r newLegalHoldAccessToken :: MonadZAuth m => LegalHoldUserToken -> m LegalHoldAccessToken newLegalHoldAccessToken xt = liftZAuth $ do z <- ask - liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - let LegalHoldAccessTokenTimeout ttl = z ^. settings . legalHoldAccessTokenTimeout - in ZC.legalHoldAccessToken1 ttl (xt ^. body . legalHoldUser . user) + liftIO $ + ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ + let LegalHoldAccessTokenTimeout ttl = z ^. settings . legalHoldAccessTokenTimeout + in ZC.legalHoldAccessToken1 ttl (xt ^. body . legalHoldUser . user) renewLegalHoldAccessToken :: MonadZAuth m => LegalHoldAccessToken -> m LegalHoldAccessToken renewLegalHoldAccessToken old = liftZAuth $ do z <- ask - liftIO $ ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ - let LegalHoldAccessTokenTimeout ttl = z ^. settings . legalHoldAccessTokenTimeout - in ZC.renewToken ttl old + liftIO $ + ZC.runCreate (z ^. private) (z ^. settings . keyIndex) $ + let LegalHoldAccessTokenTimeout ttl = z ^. settings . legalHoldAccessTokenTimeout + in ZC.renewToken ttl old validateToken :: (MonadZAuth m, ToByteString a) => diff --git a/services/brig/test/integration/API/Calling.hs b/services/brig/test/integration/API/Calling.hs index d7ce6376feb..b36a2182270 100644 --- a/services/brig/test/integration/API/Calling.hs +++ b/services/brig/test/integration/API/Calling.hs @@ -23,7 +23,7 @@ import Bilge import Bilge.Assert import qualified Brig.Options as Opts import Brig.Types -import Control.Lens ((?~), (^.), view) +import Control.Lens (view, (?~), (^.)) import Control.Monad.Catch (MonadCatch, MonadThrow) import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Conversion @@ -48,15 +48,16 @@ import Wire.API.Call.Config tests :: Manager -> Brig -> Opts.Opts -> FilePath -> FilePath -> IO TestTree tests m b opts turn turnV2 = do - return $ testGroup "calling" $ - [ testGroup "turn" $ - [ test m "basic /calls/config - 200" $ testCallsConfig b, - -- FIXME: requires tests to run on same host as brig - test m "multiple servers /calls/config - 200" . withTurnFile turn $ testCallsConfigMultiple b, - test m "multiple servers /calls/config/v2 - 200" . withTurnFile turnV2 $ testCallsConfigMultipleV2 b - ], - testGroup "sft" $ [test m "SFT servers /calls/config/v2 - 200" $ testSFT b opts] - ] + return $ + testGroup "calling" $ + [ testGroup "turn" $ + [ test m "basic /calls/config - 200" $ testCallsConfig b, + -- FIXME: requires tests to run on same host as brig + test m "multiple servers /calls/config - 200" . withTurnFile turn $ testCallsConfigMultiple b, + test m "multiple servers /calls/config/v2 - 200" . withTurnFile turnV2 $ testCallsConfigMultipleV2 b + ], + testGroup "sft" $ [test m "SFT servers /calls/config/v2 - 200" $ testSFT b opts] + ] testCallsConfig :: Brig -> Http () testCallsConfig b = do diff --git a/services/brig/test/integration/API/IdMapping.hs b/services/brig/test/integration/API/IdMapping.hs index 4ed0bab7df1..8f14c605eab 100644 --- a/services/brig/test/integration/API/IdMapping.hs +++ b/services/brig/test/integration/API/IdMapping.hs @@ -27,8 +27,8 @@ import Control.Lens ((?~)) import Data.ByteString.Conversion (toByteString') import Data.Coerce (coerce) import Data.Domain (Domain, mkDomain) -import qualified Data.Id as Id import Data.Id (Id) +import qualified Data.Id as Id import Data.Qualified (Qualified (Qualified)) import Galley.Types.IdMapping (PostIdMappingRequest (PostIdMappingRequest), PostIdMappingResponse (PostIdMappingResponse)) import Imports diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 5ece933fc94..76168b6bf9c 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -60,23 +60,10 @@ import qualified Data.Text.Ascii as Ascii import Data.Text.Encoding (encodeUtf8) import qualified Data.Text.Encoding as Text import Data.Time.Clock -import Data.Timeout ((#), TimedOut (..), Timeout, TimeoutUnit (..)) +import Data.Timeout (TimedOut (..), Timeout, TimeoutUnit (..), (#)) import qualified Data.UUID as UUID import qualified Data.ZAuth.Token as ZAuth -import Galley.Types - ( Access (..), - AccessRole (..), - Conversation (..), - ConversationAccessUpdate (..), - NewConv (..), - NewConvUnmanaged (..), - SimpleMember (..), - SimpleMembers (..), - SimpleMembers (..), - UserIdList (..), - ) -import Galley.Types (ConvMembers (..), OtherMember (..)) -import Galley.Types (Event (..), EventData (..), EventType (..), OtrMessage (..)) +import Galley.Types (Access (..), AccessRole (..), ConvMembers (..), Conversation (..), ConversationAccessUpdate (..), Event (..), EventData (..), EventType (..), NewConv (..), NewConvUnmanaged (..), OtherMember (..), OtrMessage (..), SimpleMember (..), SimpleMembers (..), UserIdList (..)) import Galley.Types.Bot (ServiceRef, newServiceRef, serviceRefId, serviceRefProvider) import Galley.Types.Conversations.Roles (roleNameWireAdmin) import qualified Galley.Types.Teams as Team @@ -196,9 +183,10 @@ testLoginProvider db brig = do prv <- randomProvider db brig let pid = providerId prv let email = providerEmail prv - _rs <- loginProvider brig email defProviderPassword getHeader "Set-Cookie" _rs now <- liftIO getCurrentTime let ttl = (`diffUTCTime` now) <$> setCookieExpires cok @@ -372,7 +360,7 @@ testUpdateService config db brig = do let newSummary = "short" let newDescr = "looooooooooooong" let newAssets = [] -- TODO - -- Exercise all updateable attributes + -- Exercise all updateable attributes let upd = UpdateService { updateServiceName = Just newName, @@ -1558,7 +1546,8 @@ randServiceKey = liftIO $ do waitFor :: MonadIO m => Timeout -> (a -> Bool) -> m a -> m a waitFor t f ma = do a <- ma - if | f a -> return a + if + | f a -> return a | t <= 0 -> liftIO $ throwM TimedOut | otherwise -> do liftIO $ threadDelay (1 # Second) @@ -1600,9 +1589,9 @@ runService config mkApp go = do let defs = Warp.defaultSettings {Warp.settingsPort = botPort config} buf <- liftIO newChan srv <- - liftIO . Async.async - $ Warp.runTLS tlss defs - $ mkApp buf + liftIO . Async.async $ + Warp.runTLS tlss defs $ + mkApp buf go buf `finally` liftIO (Async.cancel srv) data TestBot = TestBot @@ -1667,48 +1656,52 @@ defServiceApp buf = k $ responseLBS status200 [] "success" wsAssertMemberJoin :: MonadIO m => WS.WebSocket -> ConvId -> UserId -> [UserId] -> m () -wsAssertMemberJoin ws conv usr new = void $ liftIO - $ WS.assertMatch (5 # Second) ws - $ \n -> do - let e = List1.head (unpackEvents n) - ntfTransient n @?= False - evtConv e @?= conv - evtType e @?= MemberJoin - evtFrom e @?= usr - evtData e @?= Just (EdMembersJoin (SimpleMembers (fmap (\u -> SimpleMember u roleNameWireAdmin) new))) +wsAssertMemberJoin ws conv usr new = void $ + liftIO $ + WS.assertMatch (5 # Second) ws $ + \n -> do + let e = List1.head (unpackEvents n) + ntfTransient n @?= False + evtConv e @?= conv + evtType e @?= MemberJoin + evtFrom e @?= usr + evtData e @?= Just (EdMembersJoin (SimpleMembers (fmap (\u -> SimpleMember u roleNameWireAdmin) new))) wsAssertMemberLeave :: MonadIO m => WS.WebSocket -> ConvId -> UserId -> [UserId] -> m () -wsAssertMemberLeave ws conv usr old = void $ liftIO - $ WS.assertMatch (5 # Second) ws - $ \n -> do - let e = List1.head (unpackEvents n) - ntfTransient n @?= False - evtConv e @?= conv - evtType e @?= MemberLeave - evtFrom e @?= usr - evtData e @?= Just (EdMembersLeave (UserIdList old)) +wsAssertMemberLeave ws conv usr old = void $ + liftIO $ + WS.assertMatch (5 # Second) ws $ + \n -> do + let e = List1.head (unpackEvents n) + ntfTransient n @?= False + evtConv e @?= conv + evtType e @?= MemberLeave + evtFrom e @?= usr + evtData e @?= Just (EdMembersLeave (UserIdList old)) wsAssertConvDelete :: MonadIO m => WS.WebSocket -> ConvId -> UserId -> m () -wsAssertConvDelete ws conv from = void $ liftIO - $ WS.assertMatch (5 # Second) ws - $ \n -> do - let e = List1.head (WS.unpackPayload n) - ntfTransient n @?= False - evtConv e @?= conv - evtType e @?= ConvDelete - evtFrom e @?= from - evtData e @?= Nothing +wsAssertConvDelete ws conv from = void $ + liftIO $ + WS.assertMatch (5 # Second) ws $ + \n -> do + let e = List1.head (WS.unpackPayload n) + ntfTransient n @?= False + evtConv e @?= conv + evtType e @?= ConvDelete + evtFrom e @?= from + evtData e @?= Nothing wsAssertMessage :: MonadIO m => WS.WebSocket -> ConvId -> UserId -> ClientId -> ClientId -> Text -> m () -wsAssertMessage ws conv fromu fromc to txt = void $ liftIO - $ WS.assertMatch (5 # Second) ws - $ \n -> do - let e = List1.head (unpackEvents n) - ntfTransient n @?= False - evtConv e @?= conv - evtType e @?= OtrMessageAdd - evtFrom e @?= fromu - evtData e @?= Just (EdOtrMessage (OtrMessage fromc to txt (Just "data"))) +wsAssertMessage ws conv fromu fromc to txt = void $ + liftIO $ + WS.assertMatch (5 # Second) ws $ + \n -> do + let e = List1.head (unpackEvents n) + ntfTransient n @?= False + evtConv e @?= conv + evtType e @?= OtrMessageAdd + evtFrom e @?= fromu + evtData e @?= Just (EdOtrMessage (OtrMessage fromc to txt (Just "data"))) svcAssertMemberJoin :: MonadIO m => Chan TestBotEvent -> UserId -> [UserId] -> ConvId -> m () svcAssertMemberJoin buf usr new cnv = liftIO $ do diff --git a/services/brig/test/integration/API/RichInfo/Util.hs b/services/brig/test/integration/API/RichInfo/Util.hs index 782ab54a2f6..63a5643a9ac 100644 --- a/services/brig/test/integration/API/RichInfo/Util.hs +++ b/services/brig/test/integration/API/RichInfo/Util.hs @@ -40,7 +40,8 @@ getRichInfo brig self uid = do . paths ["users", toByteString' uid, "rich-info"] . zUser self ) - if | statusCode r == 200 -> Right <$> responseJsonError r + if + | statusCode r == 200 -> Right <$> responseJsonError r | statusCode r `elem` [403, 404] -> pure . Left . statusCode $ r | otherwise -> error $ diff --git a/services/brig/test/integration/API/Search.hs b/services/brig/test/integration/API/Search.hs index 31873c5ffc8..7574a5be620 100644 --- a/services/brig/test/integration/API/Search.hs +++ b/services/brig/test/integration/API/Search.hs @@ -32,7 +32,7 @@ import Control.Lens ((.~), (?~), (^.)) import Control.Monad.Catch (MonadCatch, MonadThrow) import Control.Monad.Fail (MonadFail) import Control.Retry -import Data.Aeson ((.=), FromJSON, Value) +import Data.Aeson (FromJSON, Value, (.=)) import qualified Data.Aeson as Aeson import Data.Handle (fromHandle) import Data.Id @@ -41,8 +41,8 @@ import qualified Data.Text as Text import qualified Database.Bloodhound as ES import qualified Galley.Types.Teams.SearchVisibility as Team import Imports -import qualified Network.HTTP.Client as HTTP import Network.HTTP.Client (Manager) +import qualified Network.HTTP.Client as HTTP import qualified Network.Wai.Test as WaiTest import Test.Tasty import Test.Tasty.HUnit @@ -53,9 +53,9 @@ import Wire.API.Team.Feature (TeamFeatureStatusValue (..)) tests :: Opt.Opts -> Manager -> Galley -> Brig -> IO TestTree tests opts mgr galley brig = do testSetupOutboundOnly <- runHttpT mgr prepareUsersForSearchVisibilityNoNameOutsideTeamTests - return - $ testGroup "search" - $ [ testWithBothIndices opts mgr "by-name" $ testSearchByName brig, + return $ + testGroup "search" $ + [ testWithBothIndices opts mgr "by-name" $ testSearchByName brig, testWithBothIndices opts mgr "by-handle" $ testSearchByHandle brig, test mgr "reindex" $ testReindex brig, testWithBothIndices opts mgr "no match" $ testSearchNoMatch brig, diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 0073cedfb70..8b7f1b16a92 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -118,10 +118,11 @@ testTeamSize brig = do assertSize tid expectedSize where assertSize :: HasCallStack => TeamId -> Natural -> Http () - assertSize tid expectedSize = void $ - get (brig . paths ["i", "teams", toByteString' tid, "size"]) responseJsonMaybe resp profile <- getSelfProfile brig uid diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 65439f284d6..0ea886afe16 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -113,9 +113,9 @@ createTeam u galley = do . expect2xx . lbytes (encode newTeam) ) - maybe (error "invalid team id") return - $ fromByteString - $ getHeader' "Location" r + maybe (error "invalid team id") return $ + fromByteString $ + getHeader' "Location" r -- | Create user and binding team. -- @@ -218,9 +218,9 @@ createTeamConv g tid u us mtimer = do ) Galley -> TeamId -> UserId -> [UserId] -> Maybe Milliseconds -> Http ConvId @@ -240,9 +240,9 @@ createManagedConv g tid u us mtimer = do ) Galley -> TeamId -> ConvId -> UserId -> Http () deleteTeamConv g tid cid u = do diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index e07fb265629..4273ca88ece 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -530,7 +530,7 @@ testUserUpdate brig cannon aws = do fmap userAssets u ) ) - . responseJsonMaybe + . responseJsonMaybe -- get only the new name get (brig . path "/self/name" . zUser alice) !!! do const 200 === statusCode @@ -842,7 +842,7 @@ testEmailPhoneDelete brig cannon = do WS.bracketR cannon uid $ \ws -> do delete (brig . path "/self/email" . zUser uid . zConn "c") !!! (const 200 === statusCode) - void . liftIO $ WS.assertMatch (5 # Second) ws $ \n -> do + void . liftIO . WS.assertMatch (5 # Second) ws $ \n -> do let j = Object $ List1.head (ntfPayload n) let etype = j ^? key "type" . _String let euser = j ^? key "user" . key "id" . _String @@ -869,7 +869,7 @@ testEmailPhoneDelete brig cannon = do WS.bracketR cannon uid $ \ws -> do delete (brig . path "/self/phone" . zUser uid . zConn "c") !!! const 200 === statusCode - void . liftIO $ WS.assertMatch (5 # Second) ws $ \n -> do + void . liftIO . WS.assertMatch (5 # Second) ws $ \n -> do let j = Object $ List1.head (ntfPayload n) let etype = j ^? key "type" . _String let euser = j ^? key "user" . key "id" . _String @@ -921,9 +921,10 @@ testDeleteUserByPassword brig cannon aws = do act <- getActivationCode brig (Left eml) case act of Nothing -> liftIO $ assertFailure "missing activation key/code" - Just kc -> activate brig kc !!! do - const 404 === statusCode - const (Just "invalid-code") === fmap Error.label . responseJsonMaybe + Just kc -> + activate brig kc !!! do + const 404 === statusCode + const (Just "invalid-code") === fmap Error.label . responseJsonMaybe -- Connections involving uid1 are gone (uid2 <-> uid3 remains) let u1Conns = UserConnectionList [] False let u2Conns = UserConnectionList (maybeToList (responseJsonMaybe con23)) False @@ -1156,7 +1157,7 @@ setHandleAndDeleteUser brig cannon u others aws execDelete = do -- Delete the user WS.bracketRN cannon (uid : others) $ \wss -> do execDelete uid - void . liftIO $ WS.assertMatchN (5 # Second) wss $ \n -> do + void . liftIO . WS.assertMatchN (5 # Second) wss $ \n -> do let j = Object $ List1.head (ntfPayload n) let etype = j ^? key "type" . _String let euser = j ^? key "id" . _String @@ -1207,7 +1208,7 @@ setHandleAndDeleteUser brig cannon u others aws execDelete = do userHandle =<< u' ) ) - . responseJsonMaybe + . responseJsonMaybe assertDeletedProfilePublic = do const 200 === statusCode const (Just noPict, Just True, Nothing) @@ -1217,4 +1218,4 @@ setHandleAndDeleteUser brig cannon u others aws execDelete = do profileHandle =<< u' ) ) - . responseJsonMaybe + . responseJsonMaybe diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 2f96bc96cac..2059a71d956 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -36,7 +36,7 @@ import Brig.Types.User.Auth import qualified Brig.Types.User.Auth as Auth import Brig.ZAuth (ZAuth, runZAuth) import qualified Brig.ZAuth as ZAuth -import Control.Lens ((^.), (^?), set) +import Control.Lens (set, (^.), (^?)) import Control.Retry import Data.Aeson import Data.Aeson.Lens @@ -158,8 +158,9 @@ testNginz b n = do _rs <- get (n . path "/clients" . header "Authorization" ("Bearer " <> (toByteString' t))) liftIO $ assertEqual "Ensure nginz is started. Ensure nginz and brig share the same private/public zauth keys. Ensure ACL file is correct." 200 (statusCode _rs) -- ensure nginz allows refresh at /access - _rs <- post (n . path "/access" . cookie c . header "Authorization" ("Bearer " <> (toByteString' t))) (toByteString' t))) (toByteString' t))) !!! const 200 === statusCode @@ -591,19 +592,21 @@ testNewPersistentCookie config b = do -- Wait for the cookie to be eligible for renewal liftIO $ threadDelay minAge -- Refresh tokens - _rs <- post (b . path "/access" . cookie c) cookie_expiry_time c) -- Refresh with the old cookie should still work for the -- duration of another BRIG_COOKIE_RENEW_AGE seconds, -- but the response should keep advertising the new cookie. - _rs <- post (b . path "/access" . cookie c) do + void . liftIO . WS.assertMatch (5 # Second) ws $ \n -> do let j = Object $ List1.head (ntfPayload n) let etype = j ^? key "type" . _String let eclient = j ^? key "client" @@ -228,7 +228,7 @@ testRemoveClient hasPwd brig cannon = do WS.bracketR cannon uid $ \ws -> do deleteClient brig uid (clientId c) (if hasPwd then Just defPassword else Nothing) !!! const 200 === statusCode - void . liftIO $ WS.assertMatch (5 # Second) ws $ \n -> do + void . liftIO . WS.assertMatch (5 # Second) ws $ \n -> do let j = Object $ List1.head (ntfPayload n) let etype = j ^? key "type" . _String let eclient = j ^? key "client" . key "id" . _String diff --git a/services/brig/test/integration/API/User/Connection.hs b/services/brig/test/integration/API/User/Connection.hs index 1799f762b45..92f60fae1d8 100644 --- a/services/brig/test/integration/API/User/Connection.hs +++ b/services/brig/test/integration/API/User/Connection.hs @@ -305,9 +305,10 @@ testBadUpdateConnection brig = do assertBadUpdate uid1 uid2 Accepted assertBadUpdate uid2 uid1 Sent where - assertBadUpdate u1 u2 s = putConnection brig u1 u2 s !!! do - const 403 === statusCode - const (Just "bad-conn-update") === fmap Error.label . responseJsonMaybe + assertBadUpdate u1 u2 s = + putConnection brig u1 u2 s !!! do + const 403 === statusCode + const (Just "bad-conn-update") === fmap Error.label . responseJsonMaybe testConnectionPaging :: Brig -> Http () testConnectionPaging b = do @@ -357,11 +358,12 @@ testAutoConnectionOK :: Brig -> Galley -> Http () testAutoConnectionOK brig galley = do uid1 <- userId <$> randomUser brig uid2 <- userId <$> randomUser brig - bdy <- postAutoConnection brig uid1 [uid2] do - b <- responseBody r - Vec.length <$> (decode b :: Maybe (Vector UserConnection)) + bdy <- + postAutoConnection brig uid1 [uid2] do + b <- responseBody r + Vec.length <$> (decode b :: Maybe (Vector UserConnection)) assertConnections brig uid1 [ConnectionStatus uid1 uid2 Accepted] assertConnections brig uid2 [ConnectionStatus uid2 uid1 Accepted] case responseJsonMaybe bdy >>= headMay >>= ucConvId of diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index e5d566690aa..8b38683f68f 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -27,7 +27,7 @@ import Bilge hiding (accept, timeout) import Bilge.Assert import qualified Brig.Options as Opt import Brig.Types -import Control.Lens hiding ((#), from) +import Control.Lens hiding (from, (#)) import Control.Monad.Catch (MonadCatch) import Data.Aeson import Data.Aeson.Lens @@ -75,7 +75,7 @@ testHandleUpdate brig cannon = do WS.bracketR cannon uid $ \ws -> do put (brig . path "/self/handle" . contentJson . zUser uid . zConn "c" . body update) !!! const 200 === statusCode - void . liftIO $ WS.assertMatch (5 # Second) ws $ \n -> do + void . liftIO . WS.assertMatch (5 # Second) ws $ \n -> do let j = Object $ List1.head (ntfPayload n) j ^? key "type" . _String @?= Just "user.update" let u = j ^?! key "user" @@ -126,10 +126,10 @@ testHandleRace brig = do -- 10 races. In each race, 10 users try to claim the same handle. -- At most one of them should get the handle in each race -- (usually no-one due to the contention). - void $ replicateM 10 $ do + void . replicateM 10 $ do hdl <- randomHandle let update = RequestBodyLBS . encode $ HandleUpdate hdl - void $ flip mapConcurrently us $ \u -> + void . flip mapConcurrently us $ \u -> put (brig . path "/self/handle" . contentJson . zUser u . zConn "c" . body update) ps <- forM us $ \u -> responseJsonMaybe <$> get (brig . path "/self" . zUser u) let owners = catMaybes $ filter (maybe False ((== Just (Handle hdl)) . userHandle)) ps diff --git a/services/brig/test/integration/API/User/RichInfo.hs b/services/brig/test/integration/API/User/RichInfo.hs index 9b0fcef4003..d2facf6849e 100644 --- a/services/brig/test/integration/API/User/RichInfo.hs +++ b/services/brig/test/integration/API/User/RichInfo.hs @@ -116,9 +116,10 @@ testRichInfoSizeLimit brig conf = do RichInfoAssocList [ RichField "department" (Text.replicate (fromIntegral maxSize) "#") ] - bad2 = RichInfoAssocList $ - [0 .. ((maxSize `div` 2))] - <&> \i -> RichField (CI.mk $ Text.pack $ show i) "#" + bad2 = + RichInfoAssocList $ + [0 .. ((maxSize `div` 2))] + <&> \i -> RichField (CI.mk $ Text.pack $ show i) "#" putRichInfo brig owner bad1 !!! const 413 === statusCode putRichInfo brig owner bad2 !!! const 413 === statusCode diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 433842a5c06..01256e41418 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -28,7 +28,7 @@ import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.Types.User.Auth hiding (user) import qualified CargoHold.Types.V3 as CHV3 import qualified Codec.MIME.Type as MIME -import Control.Lens ((^?), preview) +import Control.Lens (preview, (^?)) import Control.Monad.Catch (MonadCatch) import Data.Aeson import Data.Aeson.Lens @@ -131,9 +131,10 @@ activateEmail brig email = do act <- getActivationCode brig (Left email) case act of Nothing -> liftIO $ assertFailure "missing activation key/code" - Just kc -> activate brig kc !!! do - const 200 === statusCode - const (Just False) === fmap activatedFirst . responseJsonMaybe + Just kc -> + activate brig kc !!! do + const 200 === statusCode + const (Just False) === fmap activatedFirst . responseJsonMaybe checkEmail :: HasCallStack => Brig -> UserId -> Email -> HttpT IO () checkEmail brig uid expectedEmail = @@ -254,9 +255,10 @@ countCookies brig u label = do return $ Vec.length <$> (preview (key "cookies" . _Array) =<< responseJsonMaybe @Value r) assertConnections :: HasCallStack => Brig -> UserId -> [ConnectionStatus] -> Http () -assertConnections brig u cs = listConnections brig u !!! do - const 200 === statusCode - const (Just True) === fmap (check . map status . clConnections) . responseJsonMaybe +assertConnections brig u cs = + listConnections brig u !!! do + const 200 === statusCode + const (Just True) === fmap (check . map status . clConnections) . responseJsonMaybe where check xs = all (`elem` xs) cs status c = ConnectionStatus (ucFrom c) (ucTo c) (ucStatus c) diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 2161b748019..405ee2b69df 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -40,7 +40,7 @@ import Control.Monad.Catch (MonadCatch) import Control.Monad.Fail (MonadFail) import Control.Retry import Data.Aeson -import Data.Aeson.Lens (_Integral, _JSON, _String, key) +import Data.Aeson.Lens (key, _Integral, _JSON, _String) import qualified Data.Aeson.Types as Aeson import qualified Data.ByteString as BS import Data.ByteString.Char8 (pack) @@ -574,9 +574,10 @@ updatePhone brig uid phn = do act <- getActivationCode brig (Right phn) case act of Nothing -> liftIO $ assertFailure "missing activation key/code" - Just kc -> activate brig kc !!! do - const 200 === statusCode - const (Just False) === fmap activatedFirst . responseJsonMaybe + Just kc -> + activate brig kc !!! do + const 200 === statusCode + const (Just False) === fmap activatedFirst . responseJsonMaybe defEmailLogin :: Email -> Login defEmailLogin e = emailLogin e defPassword (Just defCookieLabel) diff --git a/services/cannon/src/Cannon/App.hs b/services/cannon/src/Cannon/App.hs index 921597f12ba..74de9451438 100644 --- a/services/cannon/src/Cannon/App.hs +++ b/services/cannon/src/Cannon/App.hs @@ -26,7 +26,7 @@ import Cannon.WS import Control.Concurrent.Async import Control.Concurrent.Timeout import Control.Monad.Catch -import Data.Aeson hiding ((.=), Error) +import Data.Aeson hiding (Error, (.=)) import Data.ByteString.Conversion import Data.ByteString.Lazy (toStrict) import Data.Id (ClientId) @@ -106,7 +106,8 @@ writeLoop ws clock (TTL ttl) st = loop where loop = do s <- readIORef st - if | s ^. counter == 0 -> do + if + | s ^. counter == 0 -> do set counter st succ threadDelay $ s ^. pingFreq keepAlive diff --git a/services/cannon/src/Cannon/Dict.hs b/services/cannon/src/Cannon/Dict.hs index 3e214743ed0..3ba43b99ec3 100644 --- a/services/cannon/src/Cannon/Dict.hs +++ b/services/cannon/src/Cannon/Dict.hs @@ -30,7 +30,7 @@ where import Data.Hashable (Hashable, hash) import Data.SizedHashMap (SizedHashMap) import qualified Data.SizedHashMap as SHM -import Data.Vector ((!), Vector) +import Data.Vector (Vector, (!)) import qualified Data.Vector as V import Imports hiding (lookup) @@ -51,7 +51,7 @@ insert :: (Eq a, Hashable a, MonadIO m) => a -> b -> Dict a b -> m () insert k v = mutDict (SHM.insert k v) . getSlice k add :: (Eq a, Hashable a, MonadIO m) => a -> b -> Dict a b -> m Bool -add k v d = liftIO $ atomicModifyIORef' (getSlice k d) $ \m -> +add k v d = liftIO . atomicModifyIORef' (getSlice k d) $ \m -> if k `elem` SHM.keys m then (m, False) else (SHM.insert k v m, True) @@ -60,7 +60,7 @@ remove :: (Eq a, Hashable a, MonadIO m) => a -> Dict a b -> m Bool remove = removeIf (const True) removeIf :: (Eq a, Hashable a, MonadIO m) => (Maybe b -> Bool) -> a -> Dict a b -> m Bool -removeIf f k d = liftIO $ atomicModifyIORef' (getSlice k d) $ \m -> +removeIf f k d = liftIO . atomicModifyIORef' (getSlice k d) $ \m -> if f (SHM.lookup k m) then (SHM.delete k m, True) else (m, False) @@ -76,7 +76,7 @@ mutDict :: (SizedHashMap a b -> SizedHashMap a b) -> IORef (SizedHashMap a b) -> m () -mutDict f d = liftIO $ atomicModifyIORef' d $ \m -> (f m, ()) +mutDict f d = liftIO . atomicModifyIORef' d $ \m -> (f m, ()) getSlice :: (Hashable a) => a -> Dict a b -> IORef (SizedHashMap a b) getSlice k (Dict m) = m ! (hash k `mod` V.length m) diff --git a/services/cannon/src/Cannon/Run.hs b/services/cannon/src/Cannon/Run.hs index 7cf605b4b08..9f12e9d59c8 100644 --- a/services/cannon/src/Cannon/Run.hs +++ b/services/cannon/src/Cannon/Run.hs @@ -30,8 +30,7 @@ import Cannon.WS hiding (env) import qualified Control.Concurrent.Async as Async import Control.Exception.Safe (catchAny) import Control.Lens ((^.)) -import Control.Monad.Catch (MonadCatch) -import Control.Monad.Catch (finally) +import Control.Monad.Catch (MonadCatch, finally) import Data.Metrics.Middleware (gaugeSet, path) import qualified Data.Metrics.Middleware as Middleware import Data.Metrics.Middleware.Prometheus (waiPrometheusMiddleware) @@ -95,7 +94,8 @@ refreshMetrics = do threadDelay 1000000 where safeForever :: (MonadIO m, LC.MonadLogger m, MonadCatch m) => m () -> m () - safeForever action = forever $ - action `catchAny` \exc -> do - LC.err $ "error" LC..= show exc LC.~~ LC.msg (LC.val "refreshMetrics failed") - threadDelay 60000000 -- pause to keep worst-case noise in logs manageable + safeForever action = + forever $ + action `catchAny` \exc -> do + LC.err $ "error" LC..= show exc LC.~~ LC.msg (LC.val "refreshMetrics failed") + threadDelay 60000000 -- pause to keep worst-case noise in logs manageable diff --git a/services/cannon/src/Cannon/Types.hs b/services/cannon/src/Cannon/Types.hs index 0d4a598d83f..b70f40a358c 100644 --- a/services/cannon/src/Cannon/Types.hs +++ b/services/cannon/src/Cannon/Types.hs @@ -81,9 +81,10 @@ newtype Cannon a = Cannon ) mapConcurrentlyCannon :: Traversable t => (a -> Cannon b) -> t a -> Cannon (t b) -mapConcurrentlyCannon action inputs = Cannon $ - ask >>= \e -> - liftIO $ mapConcurrently ((`runReaderT` e) . unCannon . action) inputs +mapConcurrentlyCannon action inputs = + Cannon $ + ask >>= \e -> + liftIO $ mapConcurrently ((`runReaderT` e) . unCannon . action) inputs instance MonadLogger Cannon where log l m = Cannon $ do diff --git a/services/cannon/src/Cannon/WS.hs b/services/cannon/src/Cannon/WS.hs index db53f1293cb..aeff344f222 100644 --- a/services/cannon/src/Cannon/WS.hs +++ b/services/cannon/src/Cannon/WS.hs @@ -61,7 +61,7 @@ import Data.Default (def) import Data.Hashable import Data.Id (ClientId, ConnId (..), UserId) import Data.Text.Encoding (decodeUtf8) -import Data.Timeout ((#), TimeoutUnit (..)) +import Data.Timeout (TimeoutUnit (..), (#)) import Gundeck.Types import Imports hiding (threadDelay) import Network.HTTP.Types.Method @@ -69,7 +69,7 @@ import Network.HTTP.Types.Status import Network.Wai.Utilities.Error import Network.WebSockets hiding (Request) import qualified System.Logger as Logger -import System.Logger.Class hiding ((.=), Error, Settings, close) +import System.Logger.Class hiding (Error, Settings, close, (.=)) import System.Random.MWC (GenIO, uniform) ----------------------------------------------------------------------------- @@ -115,7 +115,7 @@ newtype Clock = Clock (IORef Word64) mkClock :: IO Clock mkClock = do r <- newIORef 0 - void . forkIO $ forever $ do + void . forkIO . forever $ do threadDelay (1 # Second) modifyIORef' r (+ 1) return $ Clock r @@ -208,16 +208,19 @@ registerRemote k c = do debug $ client kb . msg (val "register-remote") e <- WS ask i <- regInfo k c - void $ recovering retry3x rpcHandlers $ const $ - rpc' "gundeck" (upstream e) (method POST . path "/i/presences" . i . expect2xx) + void $ + recovering retry3x rpcHandlers $ + const $ + rpc' "gundeck" (upstream e) (method POST . path "/i/presences" . i . expect2xx) debug $ client kb . msg (val "registered") isRemoteRegistered :: UserId -> ConnId -> WS Bool isRemoteRegistered u c = do e <- WS ask rs <- - recovering retry3x rpcHandlers $ const $ - rpc' "gundeck" (upstream e) (method GET . paths ["/i/presences", toByteString' u] . expect2xx) + recovering retry3x rpcHandlers $ + const $ + rpc' "gundeck" (upstream e) (method GET . paths ["/i/presences", toByteString' u] . expect2xx) cs <- map connId <$> parseResponse (Error status502 "server-error") rs return $ c `elem` cs diff --git a/services/cannon/test/Test/Cannon/Dict.hs b/services/cannon/test/Test/Cannon/Dict.hs index b08fa5d5e18..9303d8ba1ee 100644 --- a/services/cannon/test/Test/Cannon/Dict.hs +++ b/services/cannon/test/Test/Cannon/Dict.hs @@ -98,10 +98,11 @@ insertLookup = do action d k = do v <- toByteString <$> nextRandom added <- D.add k v d - when added $ replicateM_ 361 $ do - threadDelay 3571 - x <- D.lookup k d - Just v @=? x + when added $ + replicateM_ 361 $ do + threadDelay 3571 + x <- D.lookup k d + Just v @=? x assertEq :: (Show a, Eq a, Monad m) => String -> a -> a -> PropertyM m () assertEq m a b diff --git a/services/cargohold/src/CargoHold/API/Error.hs b/services/cargohold/src/CargoHold/API/Error.hs index f7ad71d4db3..be4acb92538 100644 --- a/services/cargohold/src/CargoHold/API/Error.hs +++ b/services/cargohold/src/CargoHold/API/Error.hs @@ -50,14 +50,15 @@ requestTimeout = invalidOffset :: Offset -> Offset -> Error invalidOffset expected given = - Error status409 "invalid-offset" $ toLazyText $ - "Invalid offset: " - <> "expected: " - <> decimal expected - <> ", " - <> "given: " - <> decimal given - <> "." + Error status409 "invalid-offset" $ + toLazyText $ + "Invalid offset: " + <> "expected: " + <> decimal expected + <> ", " + <> "given: " + <> decimal given + <> "." uploadTooSmall :: Error uploadTooSmall = @@ -77,14 +78,15 @@ uploadTooLarge = uploadIncomplete :: TotalSize -> TotalSize -> Error uploadIncomplete expected actual = - Error status403 "client-error" $ toLazyText $ - "The upload is incomplete: " - <> "expected size: " - <> decimal expected - <> ", " - <> "current size: " - <> decimal actual - <> "." + Error status403 "client-error" $ + toLazyText $ + "The upload is incomplete: " + <> "expected size: " + <> decimal expected + <> ", " + <> "current size: " + <> decimal actual + <> "." clientError :: LText -> Error clientError = Error status400 "client-error" diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 4c0dab658f9..73ba7eb5dd4 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -30,7 +30,7 @@ import CargoHold.Options import qualified CargoHold.TUS as TUS import qualified CargoHold.Types.V3 as V3 (Principal (..)) import Control.Error -import Control.Lens ((^.), view) +import Control.Lens (view, (^.)) import Data.ByteString.Conversion import Data.Id import Data.Predicate diff --git a/services/cargohold/src/CargoHold/API/V3.hs b/services/cargohold/src/CargoHold/API/V3.hs index d579f764983..bbee56c0cd7 100644 --- a/services/cargohold/src/CargoHold/API/V3.hs +++ b/services/cargohold/src/CargoHold/API/V3.hs @@ -38,7 +38,7 @@ import qualified Codec.MIME.Type as MIME import qualified Conduit as Conduit import Control.Applicative (optional) import Control.Error -import Control.Lens ((^.), set, view) +import Control.Lens (set, view, (^.)) import Control.Monad.Trans.Resource import Crypto.Hash import Crypto.Random (getRandomBytes) @@ -214,9 +214,9 @@ headers names = count (length names) (header names) header :: [HeaderName] -> Parser (HeaderName, ByteString) header names = do name <- CI.mk <$> takeTill (== ':') "header name" - unless (name `elem` names) - $ fail - $ "Unexpected header: " ++ show (CI.original name) + unless (name `elem` names) $ + fail $ + "Unexpected header: " ++ show (CI.original name) _ <- char ':' skipSpace value <- takeTill isEOL "header value" diff --git a/services/cargohold/src/CargoHold/API/V3/Resumable.hs b/services/cargohold/src/CargoHold/API/V3/Resumable.hs index 0862a01fbfe..878842375b4 100644 --- a/services/cargohold/src/CargoHold/API/V3/Resumable.hs +++ b/services/cargohold/src/CargoHold/API/V3/Resumable.hs @@ -119,7 +119,7 @@ upload own key off len src = do let totalBytes = V3.totalSizeBytes (S3.resumableTotalSize r) let numBytes = min (chunkSize r) remaining if numBytes < chunkSize r && coerce offset + remaining < totalBytes - then-- Remaining input that is not a full chunk size and does + then -- Remaining input that is not a full chunk size and does -- not constitute the last chunk is ignored, i.e. all chunks -- except the last must have the same size (the chunk size). return (r, offset) diff --git a/services/cargohold/src/CargoHold/AWS.hs b/services/cargohold/src/CargoHold/AWS.hs index fe967ace206..096daf00d36 100644 --- a/services/cargohold/src/CargoHold/AWS.hs +++ b/services/cargohold/src/CargoHold/AWS.hs @@ -53,8 +53,8 @@ import qualified Network.AWS.Env as AWS import qualified Network.AWS.S3 as S3 import Network.HTTP.Client (HttpException (..), HttpExceptionContent (..), Manager) import qualified System.Logger as Logger -import qualified System.Logger.Class as Log import System.Logger.Class (Logger, MonadLogger (log), (~~)) +import qualified System.Logger.Class as Log import Util.Options (AWSEndpoint (..)) data Env = Env @@ -97,7 +97,7 @@ instance MonadLogger Amazon where log l m = view logger >>= \g -> Logger.log g l m instance MonadUnliftIO Amazon where - askUnliftIO = Amazon $ ReaderT $ \r -> + askUnliftIO = Amazon . ReaderT $ \r -> withUnliftIO $ \u -> return (UnliftIO (unliftIO u . flip runReaderT r . unAmazon)) diff --git a/services/cargohold/src/CargoHold/App.hs b/services/cargohold/src/CargoHold/App.hs index 74657f73e39..54433d36354 100644 --- a/services/cargohold/src/CargoHold/App.hs +++ b/services/cargohold/src/CargoHold/App.hs @@ -49,7 +49,7 @@ import Bilge.RPC (HasRequestId (..)) import qualified CargoHold.AWS as AWS import CargoHold.Options as Opt import Control.Error (ExceptT, exceptT) -import Control.Lens ((^.), makeLenses, set, view) +import Control.Lens (makeLenses, set, view, (^.)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) import Control.Monad.Trans.Resource (ResourceT, runResourceT, transResourceT) import Data.Default (def) diff --git a/services/cargohold/src/CargoHold/Run.hs b/services/cargohold/src/CargoHold/Run.hs index e63a7d5a7b8..378d1d648ea 100644 --- a/services/cargohold/src/CargoHold/Run.hs +++ b/services/cargohold/src/CargoHold/Run.hs @@ -30,8 +30,8 @@ import Data.Text (unpack) import Imports import qualified Network.Wai as Wai import qualified Network.Wai.Middleware.Gzip as GZip -import qualified Network.Wai.Utilities.Server as Server import Network.Wai.Utilities.Server +import qualified Network.Wai.Utilities.Server as Server import Util.Options run :: Opts -> IO () diff --git a/services/cargohold/src/CargoHold/S3.hs b/services/cargohold/src/CargoHold/S3.hs index 7d99498cd5c..dbe4e84a721 100644 --- a/services/cargohold/src/CargoHold/S3.hs +++ b/services/cargohold/src/CargoHold/S3.hs @@ -59,7 +59,7 @@ import qualified Codec.MIME.Parse as MIME import qualified Codec.MIME.Type as MIME import Conduit import Control.Error (ExceptT, throwE) -import Control.Lens hiding ((.=), (:<), (:>), parts) +import Control.Lens hiding (parts, (.=), (:<), (:>)) import Data.ByteString.Builder (toLazyByteString) import qualified Data.ByteString.Char8 as C8 import Data.ByteString.Conversion @@ -85,7 +85,7 @@ import Network.AWS.S3 import Network.Wai.Utilities.Error (Error (..)) import Safe (readMay) import qualified System.Logger.Class as Log -import System.Logger.Message ((.=), msg, val, (~~)) +import System.Logger.Message (msg, val, (.=), (~~)) import URI.ByteString newtype S3AssetKey = S3AssetKey {s3Key :: Text} @@ -175,9 +175,9 @@ updateMetadataV3 (s3Key . mkKey -> key) (S3AssetMeta prc tok ct) = do void $ exec req where copySrc b = - decodeLatin1 . LBS.toStrict . toLazyByteString - $ urlEncode [] - $ Text.encodeUtf8 (b <> "/" <> key) + decodeLatin1 . LBS.toStrict . toLazyByteString $ + urlEncode [] $ + Text.encodeUtf8 (b <> "/" <> key) req b = copyObject (BucketName b) (copySrc b) (ObjectKey key) & coContentType ?~ MIME.showType ct @@ -308,7 +308,8 @@ calculateChunkSize (fromIntegral -> total) = smallSize = total `quot` smallChunks bigSize = total `quot` bigChunks in V3.ChunkSize $ - if | smallChunks < maxSmallChunks -> minSmallSize + if + | smallChunks < maxSmallChunks -> minSmallSize | smallSize <= maxSmallSize -> smallSize | bigChunks < maxTotalChunks -> minBigSize | otherwise -> bigSize @@ -406,14 +407,14 @@ createResumable k p typ size tok = do chunkBytes = V3.chunkSizeBytes (resumableChunkSize r) totalBytes = V3.totalSizeBytes (resumableTotalSize r) resumableMeta csize expires upl = - setAmzMetaPrincipal p - : setAmzMetaTotalSize size - : setAmzMetaChunkSize csize - : setAmzMetaUploadExpires expires - : catMaybes - [ setAmzMetaToken <$> tok, - setAmzMetaUploadId <$> upl - ] + setAmzMetaPrincipal p : + setAmzMetaTotalSize size : + setAmzMetaChunkSize csize : + setAmzMetaUploadExpires expires : + catMaybes + [ setAmzMetaToken <$> tok, + setAmzMetaUploadId <$> upl + ] uploadChunk :: S3Resumable -> @@ -499,8 +500,8 @@ completeResumable r = do -- the same here. let rk = resumableKey r let keys = - s3ResumableKey rk - : map (s3ChunkKey . mkChunkKey rk . chunkNr) (toList chunks) + s3ResumableKey rk : + map (s3ChunkKey . mkChunkKey rk . chunkNr) (toList chunks) let del = delete' & dObjects .~ map (objectIdentifier . ObjectKey) keys & dQuiet ?~ True @@ -530,9 +531,9 @@ completeResumable r = do -- upload is complete. verifyChunks cs = do let !total = V3.TotalSize $ foldl' (\t v -> t + chunkSize v) 0 cs - unless (total == resumableTotalSize r) - $ throwE - $ uploadIncomplete (resumableTotalSize r) total + unless (total == resumableTotalSize r) $ + throwE $ + uploadIncomplete (resumableTotalSize r) total -- Construct a 'Source' by downloading the chunks. -- chunkSource :: AWS.Env -- -> Seq S3Chunk @@ -544,9 +545,10 @@ completeResumable r = do let b = view AWS.s3Bucket env let req = getObject (BucketName b) (ObjectKey ck) v <- - lift $ AWS.execute env $ - AWS.send req - >>= flip sinkBody Conduit.sinkLbs . view gorsBody + lift $ + AWS.execute env $ + AWS.send req + >>= flip sinkBody Conduit.sinkLbs . view gorsBody Conduit.yield (LBS.toStrict v) >> chunkSource env cc listChunks :: S3Resumable -> ExceptT Error App (Maybe (Seq S3Chunk)) diff --git a/services/cargohold/src/CargoHold/Util.hs b/services/cargohold/src/CargoHold/Util.hs index bfd9c9a29ef..22c2284fe01 100644 --- a/services/cargohold/src/CargoHold/Util.hs +++ b/services/cargohold/src/CargoHold/Util.hs @@ -28,7 +28,8 @@ import URI.ByteString hiding (urlEncode) genSignedURL :: (ToByteString p) => p -> Handler URI genSignedURL path = do - uri <- view (aws . cloudFront) >>= \case - Nothing -> S3.signedURL path - Just cf -> CloudFront.signedURL cf path + uri <- + view (aws . cloudFront) >>= \case + Nothing -> S3.signedURL path + Just cf -> CloudFront.signedURL cf path return $! uri diff --git a/services/cargohold/test/integration/API/V3.hs b/services/cargohold/test/integration/API/V3.hs index cc8161de81a..be66969654a 100644 --- a/services/cargohold/test/integration/API/V3.hs +++ b/services/cargohold/test/integration/API/V3.hs @@ -98,9 +98,10 @@ testSimpleRoundtrip c = do when (isJust $ join (V3.assetRetentionSeconds <$> (sets ^. V3.setAssetRetention))) $ do liftIO $ assertBool "invalid expiration" (Just utc < view V3.assetExpires ast) -- Lookup with token and download via redirect. - r2 <- get (c . path loc . zUser uid . header "Asset-Token" (toByteString' tok) . noRedirect) responseJsonMaybe r2 liftIO $ assertBool "token unchanged" (tok /= tok') -- Download by owner with new token. - r3 <- get (c . path loc . zUser uid . header "Asset-Token" (toByteString' tok') . noRedirect) CargoHold -> UserId -> V3.AssetKey -> Maybe V3.AssetToken -> Http (Response (Maybe Lazy.ByteString)) downloadAsset c u k t = do - r <- getAsset c u k t long (untag (optionName :: Tagged ServiceConfigFile String)) - <> help (untag (optionHelp :: Tagged ServiceConfigFile String)) - ) + fmap ServiceConfigFile $ + strOption $ + ( short (untag (return 's' :: Tagged ServiceConfigFile Char)) + <> long (untag (optionName :: Tagged ServiceConfigFile String)) + <> help (untag (optionHelp :: Tagged ServiceConfigFile String)) + ) runTests :: (String -> String -> TestTree) -> IO () -runTests run = defaultMainWithIngredients ings - $ askOption - $ \(ServiceConfigFile c) -> - askOption $ \(IntegrationConfigFile i) -> run c i +runTests run = defaultMainWithIngredients ings $ + askOption $ + \(ServiceConfigFile c) -> + askOption $ \(IntegrationConfigFile i) -> run c i where ings = includingOptions [ Option (Proxy :: Proxy ServiceConfigFile), Option (Proxy :: Proxy IntegrationConfigFile) - ] - : defaultIngredients + ] : + defaultIngredients main :: IO () main = runTests go diff --git a/services/cargohold/test/integration/TestSetup.hs b/services/cargohold/test/integration/TestSetup.hs index 0eb3e69dd5e..872332dbc24 100644 --- a/services/cargohold/test/integration/TestSetup.hs +++ b/services/cargohold/test/integration/TestSetup.hs @@ -27,7 +27,7 @@ where import Bilge (Request) import Bilge.IO (Http, Manager, runHttpT) -import Control.Lens ((^.), makeLenses) +import Control.Lens (makeLenses, (^.)) import Imports import Test.Tasty import Test.Tasty.HUnit diff --git a/services/federator/src/Federator/Run.hs b/services/federator/src/Federator/Run.hs index e2f05c10252..1744745e1f6 100644 --- a/services/federator/src/Federator/Run.hs +++ b/services/federator/src/Federator/Run.hs @@ -37,7 +37,7 @@ where import Bilge (RequestId (unRequestId)) import Bilge.RPC (HasRequestId (..)) import Control.Error -import Control.Lens ((^.), view) +import Control.Lens (view, (^.)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) import Control.Monad.Trans.Resource import Data.Default (def) @@ -118,7 +118,7 @@ instance Monad m => HasRequestId (AppT m) where instance MonadUnliftIO m => MonadUnliftIO (AppT m) where withRunInIO inner = - AppT $ ReaderT $ \r -> + AppT . ReaderT $ \r -> withRunInIO $ \runner -> inner (runner . flip runReaderT r . unAppT) diff --git a/services/galley/migrate-data/src/Galley/DataMigration.hs b/services/galley/migrate-data/src/Galley/DataMigration.hs index 1b648ded6bf..8bf027be615 100644 --- a/services/galley/migrate-data/src/Galley/DataMigration.hs +++ b/services/galley/migrate-data/src/Galley/DataMigration.hs @@ -70,13 +70,13 @@ mkEnv l cas = <*> initLogger where initCassandra = - C.init - $ C.setLogger (C.mkLogger l) + C.init $ + C.setLogger (C.mkLogger l) . C.setContacts (cHost cas) [] . C.setPortNumber (fromIntegral (cPort cas)) . C.setKeyspace (cKeyspace cas) . C.setProtocolVersion C.V4 - $ C.defSettings + $ C.defSettings initLogger = pure l -- | Runs only the migrations which need to run diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index 9c5b18657e0..0e23b73e02d 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -181,9 +181,9 @@ createOne2OneConversation :: UserId -> ConnId -> NewConvUnmanaged -> Galley Conv createOne2OneConversation zusr zcon (NewConvUnmanaged j) = do other <- head . fromRange <$> (rangeChecked (newConvUsers j) :: Galley (Range 1 1 [OpaqueUserId])) (x, y) <- toUUIDs (makeIdOpaque zusr) other - when (x == y) - $ throwM - $ invalidOp "Cannot create a 1-1 with yourself" + when (x == y) $ + throwM $ + invalidOp "Cannot create a 1-1 with yourself" otherUserId <- IdMapping.resolveOpaqueUserId other case newConvTeam j of Just ti @@ -239,26 +239,27 @@ createConnectConversation usr conn j = do update n conv = let mems = Data.convMembers conv in conversationExisted usr - =<< if | Local usr `isMember` mems -> - -- we already were in the conversation, maybe also other - connect n conv - | otherwise -> do - now <- liftIO getCurrentTime - mm <- snd <$> Data.addMember now (Data.convId conv) usr - let conv' = - conv - { Data.convMembers = Data.convMembers conv <> toList mm - } - if null mems - then do - -- the conversation was empty - connect n conv' - else do - -- we were not in the conversation, but someone else - conv'' <- acceptOne2One usr conv' conn - if Data.convType conv'' == ConnectConv - then connect n conv'' - else return conv'' + =<< if + | Local usr `isMember` mems -> + -- we already were in the conversation, maybe also other + connect n conv + | otherwise -> do + now <- liftIO getCurrentTime + mm <- snd <$> Data.addMember now (Data.convId conv) usr + let conv' = + conv + { Data.convMembers = Data.convMembers conv <> toList mm + } + if null mems + then do + -- the conversation was empty + connect n conv' + else do + -- we were not in the conversation, but someone else + conv'' <- acceptOne2One usr conv' conn + if Data.convType conv'' == ConnectConv + then connect n conv'' + else return conv'' connect n conv | Data.convType conv == ConnectConv = do n' <- case n of diff --git a/services/galley/src/Galley/API/IdMapping.hs b/services/galley/src/Galley/API/IdMapping.hs index 37ce5e3e1c6..e2329bd83d9 100644 --- a/services/galley/src/Galley/API/IdMapping.hs +++ b/services/galley/src/Galley/API/IdMapping.hs @@ -29,8 +29,8 @@ module Galley.API.IdMapping where import Control.Monad.Catch (throwM) -import qualified Data.Id as Id import Data.Id (Id (Id, toUUID), OpaqueConvId, OpaqueUserId, idToText) +import qualified Data.Id as Id import Data.IdMapping (IdMapping (IdMapping, _imQualifiedId), MappedOrLocalId (Local, Mapped), hashQualifiedId) import Data.Qualified (Qualified, renderQualifiedId) import Galley.API.Error (federationNotEnabled) @@ -146,12 +146,12 @@ createIdMapping qualifiedId = do let idMapping = IdMapping mappedId qualifiedId Data.getIdMapping mappedId >>= \case Just existingMapping -> - when (_imQualifiedId existingMapping /= qualifiedId) - $ Log.err - $ Log.msg @Text "Conflict when creating IdMapping" - . Log.field "mapped_id" (idToText mappedId) - . Log.field "existing_qualified_id" (renderQualifiedId qualifiedId) - . Log.field "new_qualified_id" (renderQualifiedId (_imQualifiedId existingMapping)) + when (_imQualifiedId existingMapping /= qualifiedId) $ + Log.err $ + Log.msg @Text "Conflict when creating IdMapping" + . Log.field "mapped_id" (idToText mappedId) + . Log.field "existing_qualified_id" (renderQualifiedId qualifiedId) + . Log.field "new_qualified_id" (renderQualifiedId (_imQualifiedId existingMapping)) Nothing -> do Data.insertIdMapping idMapping Intra.createIdMappingInBrig (mkPostIdMappingRequest qualifiedId) diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index a7097605d90..4aa1a244d76 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -292,7 +292,7 @@ rmUser user conn = do return $ (Intra.newPush ListComplete (evtFrom e) (Intra.ConvEvent e) (Intra.recipient <$> Data.convMembers c)) <&> set Intra.pushConn conn - . set Intra.pushRoute Intra.RouteDirect + . set Intra.pushRoute Intra.RouteDirect | otherwise -> return Nothing for_ (maybeList1 (catMaybes pp)) @@ -315,7 +315,8 @@ deleteLoop = do liftIO $ threadDelay 1000000 safeForever :: (MonadIO m, MonadLogger m, MonadCatch m) => String -> m () -> m () -safeForever funName action = forever $ - action `catchAny` \exc -> do - err $ "error" .= show exc ~~ msg (val $ cs funName <> " failed") - threadDelay 60000000 -- pause to keep worst-case noise in logs manageable +safeForever funName action = + forever $ + action `catchAny` \exc -> do + err $ "error" .= show exc ~~ msg (val $ cs funName <> " failed") + threadDelay 60000000 -- pause to keep worst-case noise in logs manageable diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 1c32218de16..76d1a081834 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -31,7 +31,7 @@ where import Brig.Types.Client.Prekey import Brig.Types.Provider import Brig.Types.Team.LegalHold hiding (userId) -import Control.Lens ((^.), view) +import Control.Lens (view, (^.)) import Control.Monad.Catch import Data.ByteString.Conversion (toByteString, toByteString') import Data.Id diff --git a/services/galley/src/Galley/API/Mapping.hs b/services/galley/src/Galley/API/Mapping.hs index 2472200feda..56873878564 100644 --- a/services/galley/src/Galley/API/Mapping.hs +++ b/services/galley/src/Galley/API/Mapping.hs @@ -32,7 +32,7 @@ import Imports import Network.HTTP.Types.Status import Network.Wai.Utilities.Error import qualified System.Logger.Class as Log -import System.Logger.Message ((+++), msg, val) +import System.Logger.Message (msg, val, (+++)) import qualified Wire.API.Conversation as Public conversationView :: MappedOrLocalId Id.U -> Data.Conversation -> Galley Public.Conversation diff --git a/services/galley/src/Galley/API/Public.hs b/services/galley/src/Galley/API/Public.hs index 791a5866f04..f21292b0e20 100644 --- a/services/galley/src/Galley/API/Public.hs +++ b/services/galley/src/Galley/API/Public.hs @@ -1071,10 +1071,11 @@ filterMissing = (>>= go) <$> (query "ignore_missing" ||| query "report_missing") users :: ByteString -> ByteString -> P.Result P.Error (Set OpaqueUserId) users src bs = case fromByteString bs of Nothing -> - P.Fail $ P.setMessage "Boolean or list of user IDs expected." - $ P.setReason P.TypeError - $ P.setSource src - $ P.err status400 + P.Fail $ + P.setMessage "Boolean or list of user IDs expected." $ + P.setReason P.TypeError $ + P.setSource src $ + P.err status400 -- NB. 'fromByteString' parses a comma-separated list ('List') of -- user IDs, and then 'fromList' unwraps it; took me a while to -- understand this diff --git a/services/galley/src/Galley/API/Swagger.hs b/services/galley/src/Galley/API/Swagger.hs index 973a21999fc..2d0da3ef867 100644 --- a/services/galley/src/Galley/API/Swagger.hs +++ b/services/galley/src/Galley/API/Swagger.hs @@ -33,8 +33,7 @@ import Brig.Types.Client.Prekey (LastPrekey, Prekey, PrekeyId) import Brig.Types.Provider import Brig.Types.Team.LegalHold import Control.Lens -import Data.Aeson (toJSON) -import Data.Aeson (Value (..)) +import Data.Aeson (Value (..), toJSON) import Data.HashMap.Strict.InsOrd import Data.Id import Data.LegalHold @@ -157,14 +156,15 @@ instance ToSchema NewLegalHoldService where instance ToSchema ViewLegalHoldService where declareNamedSchema _ = - pure $ NamedSchema (Just "ViewLegalHoldService") $ - mempty - & properties .~ properties_ - & example .~ Just (toJSON example_) - & required .~ ["status"] - & minProperties .~ Just 1 - & maxProperties .~ Just 2 - & type_ .~ Just SwaggerObject + pure $ + NamedSchema (Just "ViewLegalHoldService") $ + mempty + & properties .~ properties_ + & example .~ Just (toJSON example_) + & required .~ ["status"] + & minProperties .~ Just 1 + & maxProperties .~ Just 2 + & type_ .~ Just SwaggerObject where properties_ :: InsOrdHashMap Text (Referenced Schema) properties_ = @@ -204,12 +204,13 @@ instance ToSchema ViewLegalHoldServiceInfo where } -} declareNamedSchema _ = - pure $ NamedSchema (Just "ViewLegalHoldServiceInfo") $ - mempty - & properties .~ properties_ - & example .~ Just (toJSON example_) - & required .~ ["team_id", "base_url", "fingerprint", "auth_token", "public_key"] - & type_ .~ Just SwaggerObject + pure $ + NamedSchema (Just "ViewLegalHoldServiceInfo") $ + mempty + & properties .~ properties_ + & example .~ Just (toJSON example_) + & required .~ ["team_id", "base_url", "fingerprint", "auth_token", "public_key"] + & type_ .~ Just SwaggerObject where properties_ :: InsOrdHashMap Text (Referenced Schema) properties_ = @@ -226,12 +227,13 @@ instance ToSchema ViewLegalHoldServiceInfo where instance ToSchema TeamFeatureStatus where declareNamedSchema _ = - pure $ NamedSchema (Just "TeamFeatureStatus") $ - mempty - & properties .~ (fromList [("status", Inline statusValue)]) - & required .~ ["status"] - & type_ ?~ SwaggerObject - & description ?~ "whether a given team feature is enabled" + pure $ + NamedSchema (Just "TeamFeatureStatus") $ + mempty + & properties .~ (fromList [("status", Inline statusValue)]) + & required .~ ["status"] + & type_ ?~ SwaggerObject + & description ?~ "whether a given team feature is enabled" where statusValue = mempty @@ -259,13 +261,14 @@ instance ToSchema NewLegalHoldClient where instance ToSchema UserLegalHoldStatusResponse where declareNamedSchema _ = - pure $ NamedSchema (Just "UserLegalHoldStatusResponse") $ - mempty - & properties .~ properties_ - & required .~ ["status"] - & minProperties .~ Just 1 - & maxProperties .~ Just 3 - & type_ .~ Just SwaggerObject + pure $ + NamedSchema (Just "UserLegalHoldStatusResponse") $ + mempty + & properties .~ properties_ + & required .~ ["status"] + & minProperties .~ Just 1 + & maxProperties .~ Just 3 + & type_ .~ Just SwaggerObject where properties_ :: InsOrdHashMap Text (Referenced Schema) properties_ = @@ -277,11 +280,12 @@ instance ToSchema UserLegalHoldStatusResponse where instance ToSchema a => ToSchema (IdObject a) where declareNamedSchema _ = - pure $ NamedSchema (Just "IdObject a") $ - mempty - & properties .~ properties_ - & required .~ ["id"] - & type_ .~ Just SwaggerObject + pure $ + NamedSchema (Just "IdObject a") $ + mempty + & properties .~ properties_ + & required .~ ["id"] + & type_ .~ Just SwaggerObject where properties_ :: InsOrdHashMap Text (Referenced Schema) properties_ = diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index e5afb96ec76..dfe04392cd2 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -62,8 +62,8 @@ import Brig.Types.Team (TeamSize (..)) import Control.Lens import Control.Monad.Catch import Data.ByteString.Conversion hiding (fromList) -import qualified Data.Id as Id import Data.Id +import qualified Data.Id as Id import Data.IdMapping (MappedOrLocalId (Local)) import qualified Data.List.Extra as List import Data.List1 (list1) @@ -493,11 +493,12 @@ updateTeamMember zusr zcon tid targetMember = do -- user may not elevate permissions targetPermissions `ensureNotElevated` user - previousMember <- Data.teamMember tid targetId >>= \case - Nothing -> - -- target user must be in same team - throwM teamMemberNotFound - Just previousMember -> pure previousMember + previousMember <- + Data.teamMember tid targetId >>= \case + Nothing -> + -- target user must be in same team + throwM teamMemberNotFound + Just previousMember -> pure previousMember when ( downgradesOwner previousMember targetPermissions && not (canDowngradeOwner user previousMember) @@ -609,12 +610,13 @@ uncheckedDeleteTeamMember zusr zcon tid remove mems = do let tmids = Set.fromList $ map (Local . view userId) (mems ^. teamMembers) let edata = Conv.EdMembersLeave (Conv.UserIdList [remove]) cc <- Data.teamConversations tid - for_ cc $ \c -> Data.conversation (c ^. conversationId) >>= \conv -> - for_ conv $ \dc -> when (Local remove `isMember` Data.convMembers dc) $ do - Data.removeMember (Local remove) (c ^. conversationId) - -- If the list was truncated, then the tmids list is incomplete so we simply drop these events - unless (c ^. managedConversation || mems ^. teamMemberListType == ListTruncated) $ - pushEvent tmids edata now dc + for_ cc $ \c -> + Data.conversation (c ^. conversationId) >>= \conv -> + for_ conv $ \dc -> when (Local remove `isMember` Data.convMembers dc) $ do + Data.removeMember (Local remove) (c ^. conversationId) + -- If the list was truncated, then the tmids list is incomplete so we simply drop these events + unless (c ^. managedConversation || mems ^. teamMemberListType == ListTruncated) $ + pushEvent tmids edata now dc pushEvent :: Set (MappedOrLocalId Id.U) -> Conv.EventData -> UTCTime -> Data.Conversation -> Galley () pushEvent exceptTo edata now dc = do (bots, users) <- botsAndUsers (Data.convMembers dc) diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index e35bf40a6cc..127b44f0c7d 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -94,7 +94,7 @@ import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Network.HTTP.Types import Network.Wai -import Network.Wai.Predicate hiding (_1, _2, failure, setStatus) +import Network.Wai.Predicate hiding (failure, setStatus, _1, _2) import Network.Wai.Utilities import qualified Wire.API.Conversation as Public import qualified Wire.API.Conversation.Code as Public @@ -119,9 +119,9 @@ blockConvH (zusr ::: cnv) = do blockConv :: UserId -> ConvId -> Galley () blockConv zusr cnv = do conv <- Data.conversation cnv >>= ifNothing convNotFound - unless (Data.convType conv `elem` [ConnectConv, One2OneConv]) - $ throwM - $ invalidOp "block: invalid conversation type" + unless (Data.convType conv `elem` [ConnectConv, One2OneConv]) $ + throwM $ + invalidOp "block: invalid conversation type" let mems = Data.convMembers conv when (Local zusr `isMember` mems) $ Data.removeMember (Local zusr) cnv @@ -132,9 +132,9 @@ unblockConvH (usr ::: conn ::: cnv) = do unblockConv :: UserId -> Maybe ConnId -> ConvId -> Galley Conversation unblockConv usr conn cnv = do conv <- Data.conversation cnv >>= ifNothing convNotFound - unless (Data.convType conv `elem` [ConnectConv, One2OneConv]) - $ throwM - $ invalidOp "unblock: invalid conversation type" + unless (Data.convType conv `elem` [ConnectConv, One2OneConv]) $ + throwM $ + invalidOp "unblock: invalid conversation type" conv' <- acceptOne2One usr conv conn conversationView (Local usr) conv' @@ -690,9 +690,9 @@ newMessage usr con cnv msg now (m, c, t) ~(toBots, toUsers) = let p = newPush ListComplete (evtFrom e) (ConvEvent e) [r] <&> set pushConn con - . set pushNativePriority (newOtrNativePriority msg) - . set pushRoute (bool RouteDirect RouteAny (newOtrNativePush msg)) - . set pushTransient (newOtrTransient msg) + . set pushNativePriority (newOtrNativePriority msg) + . set pushRoute (bool RouteDirect RouteAny (newOtrNativePush msg)) + . set pushTransient (newOtrTransient msg) in (toBots, p : toUsers) updateConversationDeprecatedH :: UserId ::: ConnId ::: ConvId ::: JsonRequest Public.ConversationRename -> Galley Response @@ -902,9 +902,10 @@ withValidOtrBroadcastRecipients usr clt rcps val now go = Teams.withBindingTeam -- In large teams, we may still use the broadcast endpoint but only if `report_missing` -- is used and length `report_missing` < limit since we cannot fetch larger teams than -- that. - tMembers <- fmap (view userId) <$> case val of - OtrReportMissing us -> maybeFetchLimitedTeamMemberList limit tid us - _ -> maybeFetchAllMembersInTeam tid + tMembers <- + fmap (view userId) <$> case val of + OtrReportMissing us -> maybeFetchLimitedTeamMemberList limit tid us + _ -> maybeFetchAllMembersInTeam tid contacts <- getContactList usr let users = Set.toList $ Set.union (Set.fromList tMembers) (Set.fromList contacts) isInternal <- view $ options . optSettings . setIntraListing diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 3d94051c836..59e0fca8083 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -19,7 +19,7 @@ module Galley.API.Util where import Brig.Types (Relation (..)) import Brig.Types.Intra (ReAuthUser (..)) -import Control.Lens ((.~), (^.), view) +import Control.Lens (view, (.~), (^.)) import Control.Monad.Catch import Data.ByteString.Conversion import Data.Domain (Domain) @@ -154,11 +154,12 @@ assertOnTeam uid tid = do -- | If the conversation is in a team, throw iff zusr is a team member and does not have named -- permission. If the conversation is not in a team, do nothing (no error). permissionCheckTeamConv :: UserId -> ConvId -> Perm -> Galley () -permissionCheckTeamConv zusr cnv perm = Data.conversation cnv >>= \case - Just cnv' -> case Data.convTeam cnv' of - Just tid -> void $ permissionCheck perm =<< Data.teamMember tid zusr - Nothing -> pure () - Nothing -> throwM convNotFound +permissionCheckTeamConv zusr cnv perm = + Data.conversation cnv >>= \case + Just cnv' -> case Data.convTeam cnv' of + Just tid -> void $ permissionCheck perm =<< Data.teamMember tid zusr + Nothing -> pure () + Nothing -> throwM convNotFound -- | Try to accept a 1-1 conversation, promoting connect conversations as appropriate. acceptOne2One :: UserId -> Data.Conversation -> Maybe ConnId -> Galley Data.Conversation diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index 038b5f1712a..43bd9784230 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -164,7 +164,7 @@ validateOptions l o = do instance MonadUnliftIO Galley where askUnliftIO = - Galley $ ReaderT $ \r -> + Galley . ReaderT $ \r -> withUnliftIO $ \u -> return (UnliftIO (unliftIO u . flip runReaderT r . unGalley)) diff --git a/services/galley/src/Galley/Aws.hs b/services/galley/src/Galley/Aws.hs index 0284eff6a67..5f5f055a7c6 100644 --- a/services/galley/src/Galley/Aws.hs +++ b/services/galley/src/Galley/Aws.hs @@ -95,7 +95,7 @@ newtype Amazon a = Amazon ) instance MonadUnliftIO Amazon where - askUnliftIO = Amazon $ ReaderT $ \r -> + askUnliftIO = Amazon . ReaderT $ \r -> withUnliftIO $ \u -> return (UnliftIO (unliftIO u . flip runReaderT r . unAmazon)) @@ -150,9 +150,9 @@ mkEnv lgr mgr opts = do getQueueUrl :: AWS.Env -> Text -> IO QueueUrl getQueueUrl e q = do x <- - runResourceT . AWST.runAWST e - $ AWST.trying AWS._Error - $ AWST.send (SQS.getQueueURL q) + runResourceT . AWST.runAWST e $ + AWST.trying AWS._Error $ + AWST.send (SQS.getQueueURL q) either (throwM . GeneralError) (return . QueueUrl . view SQS.gqursQueueURL) diff --git a/services/galley/src/Galley/Data.hs b/services/galley/src/Galley/Data.hs index 7d2bbbbe940..355a6b09adb 100644 --- a/services/galley/src/Galley/Data.hs +++ b/services/galley/src/Galley/Data.hs @@ -369,7 +369,7 @@ deleteTeam tid = do addTeamMember :: MonadClient m => TeamId -> TeamMember -> m () addTeamMember t m = - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery @@ -394,7 +394,7 @@ updateTeamMember :: Permissions -> m () updateTeamMember oldPerms tid uid newPerms = do - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery Cql.updatePermissions (newPerms, tid, uid) @@ -411,7 +411,7 @@ updateTeamMember oldPerms tid uid newPerms = do removeTeamMember :: MonadClient m => TeamId -> UserId -> m () removeTeamMember t m = - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery Cql.deleteTeamMember (t, m) @@ -425,7 +425,7 @@ listBillingTeamMembers tid = removeTeamConv :: (MonadClient m, Log.MonadLogger m, MonadThrow m) => TeamId -> ConvId -> m () removeTeamConv tid cid = do - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery Cql.markConvDeleted (Identity cid) @@ -436,7 +436,7 @@ updateTeamStatus :: MonadClient m => TeamId -> TeamStatus -> m () updateTeamStatus t s = retry x5 $ write Cql.updateTeamStatus (params Quorum (s, t)) updateTeam :: MonadClient m => TeamId -> TeamUpdateData -> m () -updateTeam tid u = retry x5 $ batch $ do +updateTeam tid u = retry x5 . batch $ do setType BatchLogged setConsistency Quorum for_ (u ^. nameUpdate) $ \n -> @@ -805,7 +805,7 @@ addMembersUncheckedWithRole t conv (orig, _origRole) usrs = do -- With chunk size of 64: -- [galley] Server warning: Batch for [galley_test.member, galley_test.user] is of size 7040, exceeding specified threshold of 5120 by 1920. for_ (List.chunksOf 32 (toList usrs)) $ \chunk -> do - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum for_ chunk $ \(u, r) -> do @@ -826,7 +826,7 @@ addMembersUncheckedWithRole t conv (orig, _origRole) usrs = do updateMember :: MonadClient m => ConvId -> UserId -> MemberUpdate -> m MemberUpdateData updateMember cid uid mup = do - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchUnLogged setConsistency Quorum let opaqueUserId = makeIdOpaque uid @@ -856,7 +856,7 @@ updateMember cid uid mup = do removeMembers :: MonadClient m => Conversation -> UserId -> List1 (MappedOrLocalId Id.U) -> m Event removeMembers conv orig victims = do t <- liftIO getCurrentTime - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum for_ (toList victims) $ \u -> do @@ -876,7 +876,7 @@ removeMembers conv orig victims = do Mapped _ -> Nothing removeMember :: MonadClient m => MappedOrLocalId Id.U -> ConvId -> m () -removeMember usr cnv = retry x5 $ batch $ do +removeMember usr cnv = retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery Cql.removeMember (cnv, opaqueIdFromMappedOrLocal usr) diff --git a/services/galley/src/Galley/Data/CustomBackend.hs b/services/galley/src/Galley/Data/CustomBackend.hs index 9d7a668956e..10cd979b8af 100644 --- a/services/galley/src/Galley/Data/CustomBackend.hs +++ b/services/galley/src/Galley/Data/CustomBackend.hs @@ -32,8 +32,9 @@ import Galley.Types import Imports getCustomBackend :: MonadClient m => Domain -> m (Maybe CustomBackend) -getCustomBackend domain = fmap toCustomBackend <$> do - retry x1 $ query1 Cql.selectCustomBackend (params Quorum (Identity domain)) +getCustomBackend domain = + fmap toCustomBackend <$> do + retry x1 $ query1 Cql.selectCustomBackend (params Quorum (Identity domain)) where toCustomBackend (backendConfigJsonUrl, backendWebappWelcomeUrl) = CustomBackend {..} diff --git a/services/galley/src/Galley/Data/IdMapping.hs b/services/galley/src/Galley/Data/IdMapping.hs index 4a03b3dcbd6..42df6870dc0 100644 --- a/services/galley/src/Galley/Data/IdMapping.hs +++ b/services/galley/src/Galley/Data/IdMapping.hs @@ -32,8 +32,9 @@ import Imports -- | Only a single namespace/table is used for for potentially multiple different types of -- mapped IDs. getIdMapping :: MonadClient m => Id (Mapped a) -> m (Maybe (IdMapping a)) -getIdMapping mappedId = fmap toIdMapping <$> do - retry x1 $ query1 Cql.selectIdMapping (params Quorum (Identity mappedId)) +getIdMapping mappedId = + fmap toIdMapping <$> do + retry x1 $ query1 Cql.selectIdMapping (params Quorum (Identity mappedId)) where toIdMapping (remoteId, domain) = IdMapping mappedId (Qualified remoteId domain) diff --git a/services/galley/src/Galley/Data/LegalHold.hs b/services/galley/src/Galley/Data/LegalHold.hs index 857fa9aa499..7a01ffb5824 100644 --- a/services/galley/src/Galley/Data/LegalHold.hs +++ b/services/galley/src/Galley/Data/LegalHold.hs @@ -48,8 +48,9 @@ createSettings (LegalHoldService tid url fpr tok key) = do -- | Returns 'Nothing' if no settings are saved -- The Caller is responsible for checking whether legal hold is enabled for this team getSettings :: MonadClient m => TeamId -> m (Maybe LegalHoldService) -getSettings tid = fmap toLegalHoldService <$> do - retry x1 $ query1 selectLegalHoldSettings (params Quorum (Identity tid)) +getSettings tid = + fmap toLegalHoldService <$> do + retry x1 $ query1 selectLegalHoldSettings (params Quorum (Identity tid)) where toLegalHoldService (httpsUrl, fingerprint, tok, key) = LegalHoldService tid httpsUrl fingerprint tok key @@ -57,10 +58,10 @@ removeSettings :: MonadClient m => TeamId -> m () removeSettings tid = retry x5 (write removeLegalHoldSettings (params Quorum (Identity tid))) insertPendingPrekeys :: MonadClient m => UserId -> [Prekey] -> m () -insertPendingPrekeys uid keys = retry x5 . batch - $ forM_ keys - $ \key -> - addPrepQuery Q.insertPendingPrekeys (toTuple key) +insertPendingPrekeys uid keys = retry x5 . batch $ + forM_ keys $ + \key -> + addPrepQuery Q.insertPendingPrekeys (toTuple key) where toTuple (Prekey keyId key) = (uid, keyId, key) diff --git a/services/galley/src/Galley/Data/SearchVisibility.hs b/services/galley/src/Galley/Data/SearchVisibility.hs index 72b3085ec34..680a74702b0 100644 --- a/services/galley/src/Galley/Data/SearchVisibility.hs +++ b/services/galley/src/Galley/Data/SearchVisibility.hs @@ -33,8 +33,9 @@ import Imports -- | Return whether a given team is allowed to enable/disable sso getSearchVisibility :: MonadClient m => TeamId -> m TeamSearchVisibility -getSearchVisibility tid = toSearchVisibility <$> do - retry x1 $ query1 selectSearchVisibility (params Quorum (Identity tid)) +getSearchVisibility tid = + toSearchVisibility <$> do + retry x1 $ query1 selectSearchVisibility (params Quorum (Identity tid)) where -- The value is either set or we return the default toSearchVisibility :: (Maybe (Identity (Maybe TeamSearchVisibility))) -> TeamSearchVisibility diff --git a/services/galley/src/Galley/Data/Services.hs b/services/galley/src/Galley/Data/Services.hs index 4692c52c758..b35fbcb5e2a 100644 --- a/services/galley/src/Galley/Data/Services.hs +++ b/services/galley/src/Galley/Data/Services.hs @@ -64,7 +64,7 @@ addBotMember :: UserId -> ServiceRef -> BotId -> ConvId -> UTCTime -> Galley (Ev addBotMember orig s bot cnv now = do let pid = s ^. serviceRefProvider let sid = s ^. serviceRefId - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery insertUserConv (botUserId bot, makeIdOpaque cnv, Nothing, Nothing) diff --git a/services/galley/src/Galley/Data/TeamNotifications.hs b/services/galley/src/Galley/Data/TeamNotifications.hs index 9feb8d22f12..1922c1580ab 100644 --- a/services/galley/src/Galley/Data/TeamNotifications.hs +++ b/services/galley/src/Galley/Data/TeamNotifications.hs @@ -35,7 +35,7 @@ import qualified Data.Aeson as JSON import Data.Id import Data.List1 (List1) import Data.Range (Range, fromRange) -import Data.Sequence ((<|), (><), Seq, ViewL (..), ViewR (..)) +import Data.Sequence (Seq, ViewL (..), ViewR (..), (<|), (><)) import qualified Data.Sequence as Seq import Gundeck.Types.Notification import Imports @@ -128,9 +128,9 @@ toNotif (i, b) ns = ns (\p1 -> queuedNotification notifId p1 : ns) ( JSON.decode' (fromBlob b) - -- FUTUREWORK: this is from the database, so it's slightly more ok to ignore parse - -- errors than if it's data provided by a client. it would still be better to have an - -- error entry in the log file and crash, rather than ignore the error and continue. + -- FUTUREWORK: this is from the database, so it's slightly more ok to ignore parse + -- errors than if it's data provided by a client. it would still be better to have an + -- error entry in the log file and crash, rather than ignore the error and continue. ) where notifId = Id (fromTimeUuid i) diff --git a/services/galley/src/Galley/External.hs b/services/galley/src/Galley/External.hs index a43dac6a93c..22b76e9a0af 100644 --- a/services/galley/src/Galley/External.hs +++ b/services/galley/src/Galley/External.hs @@ -103,17 +103,18 @@ deliver1 s bm e let u = s ^. serviceUrl let b = botMemId bm let HttpsUrl url = u - recovering x3 httpHandlers $ const - $ sendMessage (s ^. serviceFingerprints) - $ method POST - . maybe id host (urlHost u) - . maybe (port 443) port (urlPort u) - . paths [url ^. pathL, "bots", toByteString' b, "messages"] - . header "Authorization" ("Bearer " <> t) - . json e - . timeout 5000 - . secure - . expect2xx + recovering x3 httpHandlers $ + const $ + sendMessage (s ^. serviceFingerprints) $ + method POST + . maybe id host (urlHost u) + . maybe (port 443) port (urlPort u) + . paths [url ^. pathL, "bots", toByteString' b, "messages"] + . header "Authorization" ("Bearer " <> t) + . json e + . timeout 5000 + . secure + . expect2xx | otherwise = return () urlHost :: HttpsUrl -> Maybe ByteString @@ -128,7 +129,7 @@ urlPort (HttpsUrl u) = do sendMessage :: [Fingerprint Rsa] -> (Request -> Request) -> Galley () sendMessage fprs reqBuilder = do (man, verifyFingerprints) <- view (extEnv . extGetManager) - liftIO $ withVerifiedSslConnection (verifyFingerprints fprs) man reqBuilder $ \req -> + liftIO . withVerifiedSslConnection (verifyFingerprints fprs) man reqBuilder $ \req -> Http.withResponse req man (const $ return ()) x3 :: RetryPolicy diff --git a/services/galley/src/Galley/External/LegalHoldService.hs b/services/galley/src/Galley/External/LegalHoldService.hs index 39d0bb270c7..67d2f6bd9ac 100644 --- a/services/galley/src/Galley/External/LegalHoldService.hs +++ b/services/galley/src/Galley/External/LegalHoldService.hs @@ -70,7 +70,8 @@ import URI.ByteString (uriPath) checkLegalHoldServiceStatus :: Fingerprint Rsa -> HttpsUrl -> Galley () checkLegalHoldServiceStatus fpr url = do resp <- makeVerifiedRequestFreshManager fpr url reqBuilder - if | Bilge.statusCode resp < 400 -> pure () + if + | Bilge.statusCode resp < 400 -> pure () | otherwise -> do Log.info . Log.msg $ showResponse resp throwM legalHoldServiceBadResponse @@ -176,10 +177,12 @@ makeVerifiedRequestWithManager :: Http.Manager -> ([Fingerprint Rsa] -> SSL.SSL makeVerifiedRequestWithManager mgr verifyFingerprints fpr (HttpsUrl url) reqBuilder = do let verified = verifyFingerprints [fpr] extHandleAll errHandler $ do - recovering x3 httpHandlers $ const $ liftIO - $ withVerifiedSslConnection verified mgr (reqBuilderMods . reqBuilder) - $ \req -> - Http.httpLbs req mgr + recovering x3 httpHandlers $ + const $ + liftIO $ + withVerifiedSslConnection verified mgr (reqBuilderMods . reqBuilder) $ + \req -> + Http.httpLbs req mgr where reqBuilderMods = maybe id Bilge.host (Bilge.extHost url) @@ -211,20 +214,21 @@ makeVerifiedRequestWithManager mgr verifyFingerprints fpr (HttpsUrl url) reqBuil -- FUTUREWORK: It would be nice to move (part of) this to ssl-util, but it has types from -- brig-types and types-common. validateServiceKey :: MonadIO m => ServiceKeyPEM -> m (Maybe (ServiceKey, Fingerprint Rsa)) -validateServiceKey pem = liftIO $ - readPublicKey >>= \pk -> - case join (SSL.toPublicKey <$> pk) of - Nothing -> return Nothing - Just pk' -> do - Just sha <- SSL.getDigestByName "SHA256" - let size = SSL.rsaSize (pk' :: SSL.RSAPubKey) - if size < minRsaKeySize - then return Nothing - else do - fpr <- Fingerprint <$> SSL.rsaFingerprint sha pk' - let bits = fromIntegral size * 8 - let key = ServiceKey RsaServiceKey bits pem - return $ Just (key, fpr) +validateServiceKey pem = + liftIO $ + readPublicKey >>= \pk -> + case join (SSL.toPublicKey <$> pk) of + Nothing -> return Nothing + Just pk' -> do + Just sha <- SSL.getDigestByName "SHA256" + let size = SSL.rsaSize (pk' :: SSL.RSAPubKey) + if size < minRsaKeySize + then return Nothing + else do + fpr <- Fingerprint <$> SSL.rsaFingerprint sha pk' + let bits = fromIntegral size * 8 + let key = ServiceKey RsaServiceKey bits pem + return $ Just (key, fpr) where readPublicKey = handleAny diff --git a/services/galley/src/Galley/Intra/Journal.hs b/services/galley/src/Galley/Intra/Journal.hs index 84b118fd34a..768d29faa5d 100644 --- a/services/galley/src/Galley/Intra/Journal.hs +++ b/services/galley/src/Galley/Intra/Journal.hs @@ -65,16 +65,17 @@ teamSuspend :: TeamId -> Galley () teamSuspend tid = journalEvent TeamEvent'TEAM_SUSPEND tid Nothing Nothing journalEvent :: TeamEvent'EventType -> TeamId -> Maybe TeamEvent'EventData -> Maybe TeamCreationTime -> Galley () -journalEvent typ tid dat tim = view aEnv >>= \mEnv -> for_ mEnv $ \e -> do - -- writetime is in microseconds in cassandra 3.11 - ts <- maybe now (return . (`div` 1000000) . view tcTime) tim - let ev = - defMessage - & T.eventType .~ typ - & T.teamId .~ toBytes tid - & T.utcTime .~ ts - & T.maybe'eventData .~ dat - Aws.execute e (Aws.enqueue ev) +journalEvent typ tid dat tim = + view aEnv >>= \mEnv -> for_ mEnv $ \e -> do + -- writetime is in microseconds in cassandra 3.11 + ts <- maybe now (return . (`div` 1000000) . view tcTime) tim + let ev = + defMessage + & T.eventType .~ typ + & T.teamId .~ toBytes tid + & T.utcTime .~ ts + & T.maybe'eventData .~ dat + Aws.execute e (Aws.enqueue ev) ---------------------------------------------------------------------------- -- utils diff --git a/services/galley/src/Galley/Intra/Push.hs b/services/galley/src/Galley/Intra/Push.hs index e1f29fdbf30..6dd52e483fe 100644 --- a/services/galley/src/Galley/Intra/Push.hs +++ b/services/galley/src/Galley/Intra/Push.hs @@ -50,7 +50,7 @@ where import Bilge hiding (options) import Bilge.RPC import Bilge.Retry -import Control.Lens ((&), (.~), (^.), makeLenses, set, view) +import Control.Lens (makeLenses, set, view, (&), (.~), (^.)) import Control.Monad.Catch import Control.Retry import Data.Aeson (Object) diff --git a/services/galley/src/Galley/Intra/User.hs b/services/galley/src/Galley/Intra/User.hs index 495c1de5be5..9e2dd2cd3b4 100644 --- a/services/galley/src/Galley/Intra/User.hs +++ b/services/galley/src/Galley/Intra/User.hs @@ -28,10 +28,9 @@ where import Bilge hiding (getHeader, options, statusCode) import Bilge.RPC -import Brig.Types.Connection (UserIds (..)) -import Brig.Types.Connection (ConnectionsStatusRequest (..), Relation (..)) -import Brig.Types.Intra (ConnectionStatus (..), ReAuthUser (..)) +import Brig.Types.Connection (ConnectionsStatusRequest (..), Relation (..), UserIds (..)) import Brig.Types.Intra +import Brig.Types.Intra (ConnectionStatus (..), ReAuthUser (..)) import Brig.Types.User (User) import Control.Monad.Catch (throwM) import Data.ByteString.Char8 (pack) @@ -71,13 +70,14 @@ getConnections uFrom uTo rlt = do deleteBot :: ConvId -> BotId -> Galley () deleteBot cid bot = do (h, p) <- brigReq - void $ call "brig" $ - method DELETE . host h . port p - . path "/bot/self" - . header "Z-Type" "bot" - . header "Z-Bot" (toByteString' bot) - . header "Z-Conversation" (toByteString' cid) - . expect2xx + void $ + call "brig" $ + method DELETE . host h . port p + . path "/bot/self" + . header "Z-Type" "bot" + . header "Z-Bot" (toByteString' bot) + . header "Z-Conversation" (toByteString' cid) + . expect2xx -- | Calls 'Brig.User.API.Auth.reAuthUserH'. reAuthUser :: UserId -> ReAuthUser -> Galley Bool @@ -129,10 +129,11 @@ getUser uid = do deleteUser :: UserId -> Galley () deleteUser uid = do (h, p) <- brigReq - void $ call "brig" $ - method DELETE . host h . port p - . paths ["/i/users", toByteString' uid] - . expect2xx + void $ + call "brig" $ + method DELETE . host h . port p + . paths ["/i/users", toByteString' uid] + . expect2xx -- | Calls 'Brig.API.getContactListH'. getContactList :: UserId -> Galley [UserId] diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 079bae85332..9fa67bc5c5c 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -17,7 +17,7 @@ module Galley.Options where -import Control.Lens hiding ((.=), Level) +import Control.Lens hiding (Level, (.=)) import Data.Aeson.TH (deriveFromJSON) import Data.Domain (Domain) import Data.Misc diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index 0749663760f..4d2786666f9 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -25,15 +25,15 @@ import Cassandra (runClient, shutdown) import Cassandra.Schema (versionCheck) import qualified Control.Concurrent.Async as Async import Control.Exception (finally) -import Control.Lens ((^.), view) +import Control.Lens (view, (^.)) import qualified Data.Metrics.Middleware as M import Data.Metrics.Middleware.Prometheus (waiPrometheusMiddleware) import Data.Misc (portNumber) import Data.Text (unpack) import Galley.API (sitemap) import qualified Galley.API.Internal as Internal -import qualified Galley.App as App import Galley.App +import qualified Galley.App as App import qualified Galley.Data as Data import Galley.Options (Opts, optGalley) import qualified Galley.Queue as Q diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index a261297f41d..34db3b8cfa2 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -54,7 +54,7 @@ import Gundeck.Types.Notification import Imports import Network.Wai.Utilities.Error import Test.Tasty -import Test.Tasty.Cannon ((#), TimeoutUnit (..)) +import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import TestHelpers @@ -384,8 +384,9 @@ postJoinConvOk = do WS.bracketR2 c alice bob $ \(wsA, wsB) -> do postJoinConv bob conv !!! const 200 === statusCode postJoinConv bob conv !!! const 204 === statusCode - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB] $ - wsAssertMemberJoinWithRole conv bob [bob] roleNameWireMember + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA, wsB] $ + wsAssertMemberJoinWithRole conv bob [bob] roleNameWireMember postJoinCodeConvOk :: TestM () postJoinCodeConvOk = do @@ -412,8 +413,9 @@ postJoinCodeConvOk = do postJoinCodeConv bob payload !!! const 204 === statusCode -- eve cannot join postJoinCodeConv eve payload !!! const 403 === statusCode - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB] $ - wsAssertMemberJoinWithRole conv bob [bob] roleNameWireMember + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA, wsB] $ + wsAssertMemberJoinWithRole conv bob [bob] roleNameWireMember -- changing access to non-activated should give eve access let nonActivatedAccess = ConversationAccessUpdate [CodeAccess] NonActivatedAccessRole putAccessUpdate alice conv nonActivatedAccess !!! const 200 === statusCode @@ -441,8 +443,9 @@ postConvertCodeConv = do putAccessUpdate alice conv nonActivatedAccess !!! const 200 === statusCode -- test no-op putAccessUpdate alice conv nonActivatedAccess !!! const 204 === statusCode - void . liftIO $ WS.assertMatchN (5 # Second) [wsA] $ - wsAssertConvAccessUpdate conv alice nonActivatedAccess + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA] $ + wsAssertConvAccessUpdate conv alice nonActivatedAccess -- Create/get/update/delete codes getConvCode alice conv !!! const 404 === statusCode c1 <- decodeConvCodeEvent <$> (postConvCode alice conv postConvCode alice conv WS.bracketR3 c alice bob eve $ \(wsA, wsB, wsE) -> do postJoinCodeConv mallory j !!! const 200 === statusCode - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB, wsE] $ - wsAssertMemberJoinWithRole conv mallory [mallory] roleNameWireMember + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA, wsB, wsE] $ + wsAssertMemberJoinWithRole conv mallory [mallory] roleNameWireMember WS.bracketRN c [alice, bob, eve, mallory] $ \[wsA, wsB, wsE, wsM] -> do let teamAccess = ConversationAccessUpdate [InviteAccess, CodeAccess] TeamAccessRole putAccessUpdate alice conv teamAccess !!! const 200 === statusCode - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB, wsE, wsM] $ - wsAssertConvAccessUpdate conv alice teamAccess + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA, wsB, wsE, wsM] $ + wsAssertConvAccessUpdate conv alice teamAccess -- non-team members get kicked out - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB, wsE, wsM] $ - wsAssertMemberLeave conv alice [eve, mallory] + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA, wsB, wsE, wsM] $ + wsAssertMemberLeave conv alice [eve, mallory] -- joining (for mallory) is no longer possible postJoinCodeConv mallory j !!! const 403 === statusCode -- team members (dave) can still join @@ -535,7 +541,7 @@ getConvsOk2 = do let cs = convList <$> responseJsonUnsafe rs let c1 = cs >>= find ((== cnvId cnv1) . cnvId) let c2 = cs >>= find ((== cnvId cnv2) . cnvId) - liftIO $ forM_ [(cnv1, c1), (cnv2, c2)] $ \(expected, actual) -> do + liftIO . forM_ [(cnv1, c1), (cnv2, c2)] $ \(expected, actual) -> do assertEqual "name mismatch" (Just $ cnvName expected) @@ -968,7 +974,7 @@ putConvRenameOk = do -- This endpoint should be deprecated but clients still use it WS.bracketR2 c alice bob $ \(wsA, wsB) -> do void $ putConversationName bob conv "gossip++" !!! const 200 === statusCode - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB] $ \n -> do + void . liftIO . WS.assertMatchN (5 # Second) [wsA, wsB] $ \n -> do let e = List1.head (WS.unpackPayload n) ntfTransient n @?= False evtConv e @?= conv @@ -1030,7 +1036,7 @@ putMemberOk update = do -- Update member state & verify push notification WS.bracketR c bob $ \ws -> do putMember bob update conv !!! const 200 === statusCode - void . liftIO $ WS.assertMatch (5 # Second) ws $ \n -> do + void . liftIO . WS.assertMatch (5 # Second) ws $ \n -> do let e = List1.head (WS.unpackPayload n) ntfTransient n @?= False evtConv e @?= conv @@ -1141,10 +1147,12 @@ removeUser = do conv3 <- decodeConvId <$> postConv alice [carl] (Just "gossip3") [] Nothing Nothing WS.bracketR3 c alice bob carl $ \(wsA, wsB, wsC) -> do deleteUser bob - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB] $ - matchMemberLeave conv1 bob - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ - matchMemberLeave conv2 bob + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA, wsB] $ + matchMemberLeave conv1 bob + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ + matchMemberLeave conv2 bob -- Check memberships mems1 <- fmap cnvMembers . responseJsonUnsafe <$> getConv alice conv1 mems2 <- fmap cnvMembers . responseJsonUnsafe <$> getConv alice conv2 diff --git a/services/galley/test/integration/API/IdMapping.hs b/services/galley/test/integration/API/IdMapping.hs index 3717117b8d6..d35d550bf44 100644 --- a/services/galley/test/integration/API/IdMapping.hs +++ b/services/galley/test/integration/API/IdMapping.hs @@ -22,12 +22,12 @@ module API.IdMapping where import API.Util (withSettingsOverrides) import Bilge hiding (timeout) import Bilge.Assert -import Control.Lens ((?~), view) +import Control.Lens (view, (?~)) import Data.ByteString.Conversion (toByteString') import Data.Coerce (coerce) import Data.Domain (Domain, mkDomain) -import qualified Data.Id as Id import Data.Id (Id) +import qualified Data.Id as Id import Data.Qualified (Qualified (Qualified)) import Galley.Options (optSettings, setEnableFederationWithDomain) import Galley.Types.IdMapping (PostIdMappingRequest (PostIdMappingRequest), PostIdMappingResponse (PostIdMappingResponse)) diff --git a/services/galley/test/integration/API/MessageTimer.hs b/services/galley/test/integration/API/MessageTimer.hs index f6aa0584501..e87eb88864a 100644 --- a/services/galley/test/integration/API/MessageTimer.hs +++ b/services/galley/test/integration/API/MessageTimer.hs @@ -32,7 +32,7 @@ import qualified Galley.Types.Teams as Teams import Imports hiding (head) import Network.Wai.Utilities.Error import Test.Tasty -import Test.Tasty.Cannon ((#), TimeoutUnit (..)) +import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import qualified Test.Tasty.Cannon as WS import TestHelpers import TestSetup @@ -151,8 +151,9 @@ messageTimerEvent = do let update = ConversationMessageTimerUpdate timer1sec putMessageTimerUpdate alice cid update !!! const 200 === statusCode - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB] $ - wsAssertConvMessageTimerUpdate cid alice update + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA, wsB] $ + wsAssertConvMessageTimerUpdate cid alice update ---------------------------------------------------------------------------- -- Utilities diff --git a/services/galley/test/integration/API/Roles.hs b/services/galley/test/integration/API/Roles.hs index 8e8f72ff691..d9fb7837771 100644 --- a/services/galley/test/integration/API/Roles.hs +++ b/services/galley/test/integration/API/Roles.hs @@ -28,7 +28,7 @@ import Galley.Types.Conversations.Roles import Imports import Network.Wai.Utilities.Error import Test.Tasty -import Test.Tasty.Cannon ((#), TimeoutUnit (..)) +import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import qualified Test.Tasty.Cannon as WS import TestHelpers import TestSetup @@ -59,12 +59,14 @@ handleConversationRoleAdmin = do let cid = decodeConvId rsp -- Make sure everyone gets the correct event postMembersWithRole alice (singleton eve) cid role !!! const 200 === statusCode - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ - wsAssertMemberJoinWithRole cid alice [eve] role + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ + wsAssertMemberJoinWithRole cid alice [eve] role -- Add a member to help out with testing postMembersWithRole alice (singleton jack) cid roleNameWireMember !!! const 200 === statusCode - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ - wsAssertMemberJoinWithRole cid alice [jack] roleNameWireMember + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ + wsAssertMemberJoinWithRole cid alice [jack] roleNameWireMember return cid -- Added bob as a wire_admin and do the checks wireAdminChecks cid alice bob jack @@ -72,7 +74,7 @@ handleConversationRoleAdmin = do WS.bracketR3 c alice bob chuck $ \(wsA, wsB, wsC) -> do let updateDown = OtherMemberUpdate (Just roleNameWireMember) putOtherMember alice bob updateDown cid !!! assertActionSucceeded - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ do + void . liftIO . WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ do wsAssertMemberUpdateWithRole cid alice bob roleNameWireMember wireMemberChecks cid bob alice jack @@ -94,8 +96,9 @@ handleConversationRoleMember = do let cid = decodeConvId rsp -- Make sure everyone gets the correct event postMembersWithRole alice (singleton eve) cid role !!! const 200 === statusCode - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ - wsAssertMemberJoinWithRole cid alice [eve] role + void . liftIO $ + WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ + wsAssertMemberJoinWithRole cid alice [eve] role return cid -- Added bob as a wire_member and do the checks wireMemberChecks cid bob alice chuck @@ -105,7 +108,7 @@ handleConversationRoleMember = do -- Chuck cannot update, member only putOtherMember chuck bob updateUp cid !!! assertActionDenied putOtherMember alice bob updateUp cid !!! assertActionSucceeded - void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ do + void . liftIO . WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ do wsAssertMemberUpdateWithRole cid alice bob roleNameWireAdmin wireAdminChecks cid bob alice chuck diff --git a/services/galley/test/integration/API/SQS.hs b/services/galley/test/integration/API/SQS.hs index 489192a145d..afd7f1f7663 100644 --- a/services/galley/test/integration/API/SQS.hs +++ b/services/galley/test/integration/API/SQS.hs @@ -59,20 +59,23 @@ ensureQueueEmptyIO (Just env) = liftIO $ Aws.execute env purgeQueue ensureQueueEmptyIO Nothing = return () assertQueue :: String -> (String -> Maybe E.TeamEvent -> IO ()) -> TestM () -assertQueue label check = view tsAwsEnv >>= \case - Just env -> liftIO $ Aws.execute env $ fetchMessage label check - Nothing -> return () +assertQueue label check = + view tsAwsEnv >>= \case + Just env -> liftIO $ Aws.execute env $ fetchMessage label check + Nothing -> return () -- Try to assert an event in the queue for a `timeout` amount of seconds tryAssertQueue :: Int -> String -> (String -> Maybe E.TeamEvent -> IO ()) -> TestM () -tryAssertQueue timeout label check = view tsAwsEnv >>= \case - Just env -> liftIO $ Aws.execute env $ awaitMessage label timeout check - Nothing -> return () +tryAssertQueue timeout label check = + view tsAwsEnv >>= \case + Just env -> liftIO $ Aws.execute env $ awaitMessage label timeout check + Nothing -> return () assertQueueEmpty :: (HasCallStack) => TestM () -assertQueueEmpty = view tsAwsEnv >>= \case - Just env -> liftIO $ Aws.execute env ensureNoMessages - Nothing -> return () +assertQueueEmpty = + view tsAwsEnv >>= \case + Just env -> liftIO $ Aws.execute env ensureNoMessages + Nothing -> return () tActivateWithCurrency :: HasCallStack => Maybe Currency.Alpha -> String -> Maybe E.TeamEvent -> IO () tActivateWithCurrency c l (Just e) = do @@ -161,12 +164,13 @@ tryMatch label tries url callback = go tries liftIO $ threadDelay (10 ^ (6 :: Int)) go (n - 1) check :: Maybe E.TeamEvent -> Amazon (Either MatchFailure String) - check e = do - liftIO $ callback label e - return (Right $ show e) - `catchAll` \ex -> case asyncExceptionFromException ex of - Just x -> throwM (x :: SomeAsyncException) - Nothing -> return . Left $ MatchFailure (e, ex) + check e = + do + liftIO $ callback label e + return (Right $ show e) + `catchAll` \ex -> case asyncExceptionFromException ex of + Just x -> throwM (x :: SomeAsyncException) + Nothing -> return . Left $ MatchFailure (e, ex) -- Note that Amazon's purge queue is a bit incovenient for testing purposes because -- it may be delayed in ~60 seconds which causes messages that are published later @@ -178,8 +182,8 @@ receive :: Int -> Text -> SQS.ReceiveMessage receive n url = SQS.receiveMessage url & set SQS.rmWaitTimeSeconds (Just 1) - . set SQS.rmMaxNumberOfMessages (Just n) - . set SQS.rmVisibilityTimeout (Just 1) + . set SQS.rmMaxNumberOfMessages (Just n) + . set SQS.rmVisibilityTimeout (Just 1) queueEvent :: E.TeamEvent -> Amazon () queueEvent e = do diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index ac1821742dd..dcd0b09600a 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -63,7 +63,7 @@ import qualified Network.Wai.Utilities.Error as Wai import qualified Proto.TeamEvents as E import qualified Proto.TeamEvents_Fields as E import Test.Tasty -import Test.Tasty.Cannon ((#), TimeoutUnit (..)) +import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import TestHelpers (test) @@ -330,9 +330,10 @@ testEnableTeamSearchVisibilityPerTeam = do assertEqual "bad status" status403 status assertEqual "bad label" "team-search-visibility-not-enabled" label let getSearchVisibilityCheck :: (HasCallStack, MonadCatch m, MonadIO m, MonadHttp m) => TeamSearchVisibility -> m () - getSearchVisibilityCheck vis = getSearchVisibility g owner tid !!! do - const 200 === statusCode - const (Just (TeamSearchVisibilityView vis)) === responseJsonUnsafe + getSearchVisibilityCheck vis = + getSearchVisibility g owner tid !!! do + const 200 === statusCode + const (Just (TeamSearchVisibilityView vis)) === responseJsonUnsafe Util.withCustomSearchFeature FeatureTeamSearchVisibilityEnabledByDefault $ do check "Teams should start with Custom Search Visibility enabled" Public.TeamFeatureEnabled @@ -941,7 +942,7 @@ testDeleteTeam = do Util.assertConvMember owner cid1 Util.assertConvMember extern cid1 Util.assertNotConvMember (member ^. userId) cid1 - void $ WS.bracketR3 c owner extern (member ^. userId) $ \(wsOwner, wsExtern, wsMember) -> do + void . WS.bracketR3 c owner extern (member ^. userId) $ \(wsOwner, wsExtern, wsMember) -> do delete (g . paths ["teams", toByteString' tid] . zUser owner . zConn "conn") !!! const 202 === statusCode checkTeamDeleteEvent tid wsOwner @@ -1004,7 +1005,7 @@ testDeleteBindingTeamSingleMember = do (/= Just True) (getDeletedState extern (other ^. userId)) - void $ WS.bracketRN c [owner, extern] $ \[wsOwner, wsExtern] -> do + void . WS.bracketRN c [owner, extern] $ \[wsOwner, wsExtern] -> do delete ( g . paths ["/i/teams", toByteString' tid] @@ -1075,7 +1076,7 @@ testDeleteBindingTeam ownerHasPassword = do !!! const 202 === statusCode assertQueue "team member leave 1" $ tUpdate 4 [ownerWithPassword, owner] - void $ WS.bracketRN c [owner, (mem1 ^. userId), (mem2 ^. userId), extern] $ \[wsOwner, wsMember1, wsMember2, wsExtern] -> do + void . WS.bracketRN c [owner, (mem1 ^. userId), (mem2 ^. userId), extern] $ \[wsOwner, wsMember1, wsMember2, wsExtern] -> do delete ( g . paths ["teams", toByteString' tid] @@ -1327,7 +1328,7 @@ testTeamAddRemoveMemberAboveThresholdNoEvents = do deleteTeam tid owner otherRealUsersInTeam teamCidsThatExternBelongsTo extern = do c <- view tsCannon g <- view tsGalley - void $ WS.bracketRN c (owner : extern : otherRealUsersInTeam) $ \(_wsOwner : wsExtern : _wsotherRealUsersInTeam) -> do + void . WS.bracketRN c (owner : extern : otherRealUsersInTeam) $ \(_wsOwner : wsExtern : _wsotherRealUsersInTeam) -> do delete ( g . paths ["teams", toByteString' tid] @@ -1769,7 +1770,7 @@ postCryptoBroadcastMessageJson2 = do cc <- Util.randomClient charlie (someLastPrekeys !! 2) connectUsers alice (list1 charlie []) let t = 3 # Second -- WS receive timeout - -- Missing charlie + -- Missing charlie let m1 = [(bob, bc, "ciphertext1")] Util.postOtrBroadcastMessage id alice ac m1 !!! do const 412 === statusCode diff --git a/services/galley/test/integration/API/Teams/Feature.hs b/services/galley/test/integration/API/Teams/Feature.hs index c6107e06b4a..79b2e1f3f8b 100644 --- a/services/galley/test/integration/API/Teams/Feature.hs +++ b/services/galley/test/integration/API/Teams/Feature.hs @@ -121,18 +121,20 @@ testSearchVisibility = do TeamId -> Public.TeamFeatureStatusValue -> m () - getTeamSearchVisibility teamid expected = Util.getTeamSearchVisibilityAvailable g owner teamid !!! do - statusCode === const 200 - responseJsonEither === const (Right (Public.TeamFeatureStatus expected)) + getTeamSearchVisibility teamid expected = + Util.getTeamSearchVisibilityAvailable g owner teamid !!! do + statusCode === const 200 + responseJsonEither === const (Right (Public.TeamFeatureStatus expected)) let getTeamSearchVisibilityInternal :: (Monad m, MonadHttp m, MonadIO m, MonadCatch m, HasCallStack) => TeamId -> Public.TeamFeatureStatusValue -> m () - getTeamSearchVisibilityInternal teamid expected = Util.getTeamSearchVisibilityAvailableInternal g teamid !!! do - statusCode === const 200 - responseJsonEither === const (Right (Public.TeamFeatureStatus expected)) + getTeamSearchVisibilityInternal teamid expected = + Util.getTeamSearchVisibilityAvailableInternal g teamid !!! do + statusCode === const 200 + responseJsonEither === const (Right (Public.TeamFeatureStatus expected)) let setTeamSearchVisibilityInternal :: (Monad m, MonadHttp m, MonadIO m, HasCallStack) => @@ -187,6 +189,7 @@ testSimpleFlag feature = do getFlagInternal feature Public.TeamFeatureEnabled assertFlag :: HasCallStack => TestM ResponseLBS -> Public.TeamFeatureStatusValue -> TestM () -assertFlag res expected = res !!! do - statusCode === const 200 - responseJsonEither === const (Right (Public.TeamFeatureStatus expected)) +assertFlag res expected = + res !!! do + statusCode === const 200 + responseJsonEither === const (Right (Public.TeamFeatureStatus expected)) diff --git a/services/galley/test/integration/API/Teams/LegalHold.hs b/services/galley/test/integration/API/Teams/LegalHold.hs index dd859e26e67..b66c6887d1a 100644 --- a/services/galley/test/integration/API/Teams/LegalHold.hs +++ b/services/galley/test/integration/API/Teams/LegalHold.hs @@ -41,7 +41,7 @@ import Control.Lens import Control.Monad.Catch import Control.Retry (RetryPolicy, RetryStatus, exponentialBackoff, limitRetries, retrying) import qualified Data.Aeson as Aeson -import Data.Aeson.Types ((.:), FromJSON) +import Data.Aeson.Types (FromJSON, (.:)) import qualified Data.Aeson.Types as Aeson import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BS @@ -321,7 +321,7 @@ testDisableLegalHoldForUser = do disableLegalHoldForUser Nothing tid owner member !!! const 403 === statusCode assertExactlyOneLegalHoldDevice member disableLegalHoldForUser (Just defPassword) tid owner member !!! testResponse 200 Nothing - liftIO $ assertMatchChan chan $ \(req, _) -> do + liftIO . assertMatchChan chan $ \(req, _) -> do assertEqual "method" "POST" (requestMethod req) assertEqual "path" (pathInfo req) ["legalhold", "remove"] assertNotification mws $ \case @@ -359,7 +359,8 @@ testCreateLegalHoldTeamSettings = do let lhapp :: HasCallStack => IsWorking -> Chan Void -> Application lhapp NotWorking _ _ cont = cont respondBad lhapp Working _ req cont = do - if | pathInfo req /= ["legalhold", "status"] -> cont respondBad + if + | pathInfo req /= ["legalhold", "status"] -> cont respondBad | requestMethod req /= "GET" -> cont respondBad | otherwise -> cont respondOk respondOk :: Wai.Response @@ -468,7 +469,7 @@ testRemoveLegalHoldFromTeam = do deleteSettings Nothing owner tid !!! testResponse 403 (Just "access-denied") let delete'' expectRemoteLHCall = do deleteSettings (Just defPassword) owner tid !!! testResponse 204 Nothing - when expectRemoteLHCall $ liftIO $ assertMatchChan chan $ \(req, _) -> do + when expectRemoteLHCall . liftIO . assertMatchChan chan $ \(req, _) -> do putStrLn (show (pathInfo req, pathInfo req == ["legalhold", "remove"])) putStrLn (show (requestMethod req, requestMethod req == "POST")) assertEqual "path" ["legalhold", "remove"] (pathInfo req) @@ -843,9 +844,9 @@ withTestService mkApp go = do let defs = Warp.defaultSettings {Warp.settingsPort = botPort config} buf <- liftIO newChan srv <- - liftIO . Async.async - $ Warp.runTLS tlss defs - $ mkApp buf + liftIO . Async.async $ + Warp.runTLS tlss defs $ + mkApp buf go buf `finally` liftIO (Async.cancel srv) publicKeyNotMatchingService :: PEM @@ -946,12 +947,13 @@ assertMatchChan c match = go [] go buf = do m <- liftIO . timeout (5 WS.# WS.Second) . readChan $ c case m of - Just n -> do - match n - refill buf - `catchAll` \e -> case asyncExceptionFromException e of - Just x -> throwM (x :: SomeAsyncException) - Nothing -> go (n : buf) + Just n -> + do + match n + refill buf + `catchAll` \e -> case asyncExceptionFromException e of + Just x -> throwM (x :: SomeAsyncException) + Nothing -> go (n : buf) Nothing -> do refill buf liftIO $ assertBool "Timeout" False diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 243dcff24df..257151c0f87 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -25,12 +25,12 @@ import Bilge.Assert import Brig.Types import Brig.Types.Team.Invitation import Brig.Types.User.Auth (CookieLabel (..)) -import Control.Lens hiding ((#), (.=), from, to) +import Control.Lens hiding (from, to, (#), (.=)) import Control.Monad.Catch (MonadCatch) import Control.Monad.Fail (MonadFail) import Control.Retry (constantDelay, limitRetries, retrying) import Data.Aeson hiding (json) -import Data.Aeson.Lens (_String, key) +import Data.Aeson.Lens (key, _String) import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as C import Data.ByteString.Conversion @@ -46,16 +46,16 @@ import Data.Range import Data.Serialize (runPut) import qualified Data.Set as Set import Data.String.Conversions (ST, cs) -import qualified Data.Text.Encoding as Text import Data.Text.Encoding (decodeUtf8) +import qualified Data.Text.Encoding as Text import qualified Data.UUID as UUID import Data.UUID.V4 import qualified Galley.Options as Opts import qualified Galley.Run as Run import Galley.Types hiding (InternalMember (..), Member) import Galley.Types.Conversations.Roles hiding (DeleteConversation) -import qualified Galley.Types.Teams as Team import Galley.Types.Teams hiding (Event, EventType (..)) +import qualified Galley.Types.Teams as Team import Galley.Types.Teams.Intra import Gundeck.Types.Notification ( Notification (..), @@ -71,7 +71,7 @@ import Gundeck.Types.Notification import Imports import qualified Network.Wai.Test as WaiTest import qualified Test.QuickCheck as Q -import Test.Tasty.Cannon ((#), TimeoutUnit (..)) +import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import qualified Test.Tasty.Cannon as WS import Test.Tasty.HUnit import TestSetup @@ -138,9 +138,10 @@ createNonBindingTeam name owner mems = do g <- view tsGalley let mm = if null mems then Nothing else Just $ unsafeRange (take 127 mems) let nt = NonBindingNewTeam $ newNewTeam (unsafeRange name) (unsafeRange "icon") & newTeamMembers .~ mm - resp <- post (g . path "/teams" . zUser owner . zConn "conn" . zType "access" . json nt) TeamId -> TeamStatus -> TestM () @@ -164,9 +165,10 @@ createBindingTeamInternalNoActivate name owner = do g <- view tsGalley tid <- randomId let nt = BindingNewTeam $ newNewTeam (unsafeRange name) (unsafeRange "icon") - _ <- put (g . paths ["/i/teams", toByteString' tid] . zUser owner . zConn "conn" . zType "access" . json nt) Text -> UserId -> Currency.Alpha -> TestM TeamId @@ -1414,8 +1416,9 @@ withSettingsOverrides opts action = liftIO $ do waitForMemberDeletion :: UserId -> TeamId -> UserId -> TestM () waitForMemberDeletion zusr tid uid = do maybeTimedOut <- timeout 2000000 loop - liftIO $ when (isNothing maybeTimedOut) $ - assertFailure "Timed out waiting for member deletion" + liftIO $ + when (isNothing maybeTimedOut) $ + assertFailure "Timed out waiting for member deletion" where loop = do galley <- view tsGalley diff --git a/services/galley/test/integration/API/Util/TeamFeature.hs b/services/galley/test/integration/API/Util/TeamFeature.hs index 183495679f4..99299b3ad64 100644 --- a/services/galley/test/integration/API/Util/TeamFeature.hs +++ b/services/galley/test/integration/API/Util/TeamFeature.hs @@ -17,10 +17,10 @@ module API.Util.TeamFeature where -import qualified API.Util as Util import API.Util (zUser) +import qualified API.Util as Util import Bilge -import Control.Lens ((.~), view) +import Control.Lens (view, (.~)) import Data.ByteString.Conversion (toByteString') import Data.Id (TeamId, UserId) import Galley.Options (optSettings, setFeatureFlags) diff --git a/services/galley/test/integration/Main.hs b/services/galley/test/integration/Main.hs index 244eb55b7d0..33794c1410e 100644 --- a/services/galley/test/integration/Main.hs +++ b/services/galley/test/integration/Main.hs @@ -60,24 +60,25 @@ instance IsOption ServiceConfigFile where optionName = return "service-config" optionHelp = return "Service config file to read from" optionCLParser = - fmap ServiceConfigFile $ strOption $ - ( short (untag (return 's' :: Tagged ServiceConfigFile Char)) - <> long (untag (optionName :: Tagged ServiceConfigFile String)) - <> help (untag (optionHelp :: Tagged ServiceConfigFile String)) - ) + fmap ServiceConfigFile $ + strOption $ + ( short (untag (return 's' :: Tagged ServiceConfigFile Char)) + <> long (untag (optionName :: Tagged ServiceConfigFile String)) + <> help (untag (optionHelp :: Tagged ServiceConfigFile String)) + ) runTests :: (String -> String -> TestTree) -> IO () -runTests run = defaultMainWithIngredients ings - $ askOption - $ \(ServiceConfigFile c) -> - askOption $ \(IntegrationConfigFile i) -> run c i +runTests run = defaultMainWithIngredients ings $ + askOption $ + \(ServiceConfigFile c) -> + askOption $ \(IntegrationConfigFile i) -> run c i where ings = includingOptions [ Option (Proxy :: Proxy ServiceConfigFile), Option (Proxy :: Proxy IntegrationConfigFile) - ] - : defaultIngredients + ] : + defaultIngredients main :: IO () main = withOpenSSL $ runTests go diff --git a/services/gundeck/src/Gundeck/Aws.hs b/services/gundeck/src/Gundeck/Aws.hs index aac05c7ad49..b4a05082752 100644 --- a/services/gundeck/src/Gundeck/Aws.hs +++ b/services/gundeck/src/Gundeck/Aws.hs @@ -72,8 +72,7 @@ import Gundeck.Options import Gundeck.Types.Push (AppName (..), Token, Transport (..)) import qualified Gundeck.Types.Push as Push import Imports -import Network.AWS (AWSRequest, Rs) -import Network.AWS (serviceAbbrev, serviceCode, serviceMessage, serviceStatus) +import Network.AWS (AWSRequest, Rs, serviceAbbrev, serviceCode, serviceMessage, serviceStatus) import qualified Network.AWS as AWS import qualified Network.AWS.Data as AWS import qualified Network.AWS.Env as AWS @@ -141,7 +140,7 @@ newtype Amazon a = Amazon ) instance MonadUnliftIO Amazon where - askUnliftIO = Amazon $ ReaderT $ \r -> + askUnliftIO = Amazon . ReaderT $ \r -> withUnliftIO $ \u -> return (UnliftIO (unliftIO u . flip runReaderT r . unAmazon)) @@ -202,9 +201,9 @@ mkEnv lgr opts mgr = do getQueueUrl :: AWS.Env -> Text -> IO QueueUrl getQueueUrl e q = do x <- - runResourceT . AWST.runAWST e - $ AWST.trying AWS._Error - $ AWST.send (SQS.getQueueURL q) + runResourceT . AWST.runAWST e $ + AWST.trying AWS._Error $ + AWST.send (SQS.getQueueURL q) either (throwM . GeneralError) (return . QueueUrl . view SQS.gqursQueueURL) @@ -438,7 +437,7 @@ publish arn txt attrs = do listen :: Int -> (Event -> IO ()) -> Amazon () listen throttleMillis callback = do QueueUrl url <- view eventQueue - forever $ handleAny unexpectedError $ do + forever . handleAny unexpectedError $ do msgs <- view rmrsMessages <$> send (receive url) void $ mapConcurrently (onMessage url) msgs when (null msgs) $ @@ -447,7 +446,7 @@ listen throttleMillis callback = do receive url = SQS.receiveMessage url & set SQS.rmWaitTimeSeconds (Just 20) - . set SQS.rmMaxNumberOfMessages (Just 10) + . set SQS.rmMaxNumberOfMessages (Just 10) onMessage url m = case decodeStrict =<< Text.encodeUtf8 <$> m ^. mBody of Nothing -> diff --git a/services/gundeck/src/Gundeck/Env.hs b/services/gundeck/src/Gundeck/Env.hs index 781d610f813..3c9888281d4 100644 --- a/services/gundeck/src/Gundeck/Env.hs +++ b/services/gundeck/src/Gundeck/Env.hs @@ -22,7 +22,7 @@ import Cassandra (ClientState, Keyspace (..)) import qualified Cassandra as C import qualified Cassandra.Settings as C import Control.AutoUpdate -import Control.Lens ((^.), makeLenses) +import Control.Lens (makeLenses, (^.)) import Data.Default (def) import qualified Data.List.NonEmpty as NE import Data.Metrics.Middleware (Metrics) @@ -73,17 +73,17 @@ createEnv m o = do managerResponseTimeout = responseTimeoutMicro 5000000 } r <- - Redis.mkPool (Logger.clone (Just "redis.gundeck") l) - $ Redis.setHost (unpack $ o ^. optRedis . epHost) + Redis.mkPool (Logger.clone (Just "redis.gundeck") l) $ + Redis.setHost (unpack $ o ^. optRedis . epHost) . Redis.setPort (o ^. optRedis . epPort) . Redis.setMaxConnections 100 . Redis.setPoolStripes 4 . Redis.setConnectTimeout 3 . Redis.setSendRecvTimeout 5 - $ Redis.defSettings + $ Redis.defSettings p <- - C.init - $ C.setLogger (C.mkLogger (Logger.clone (Just "cassandra.gundeck") l)) + C.init $ + C.setLogger (C.mkLogger (Logger.clone (Just "cassandra.gundeck") l)) . C.setContacts (NE.head c) (NE.tail c) . C.setPortNumber (fromIntegral $ o ^. optCassandra . casEndpoint . epPort) . C.setKeyspace (Keyspace (o ^. optCassandra . casKeyspace)) @@ -93,7 +93,7 @@ createEnv m o = do . C.setSendTimeout 3 . C.setResponseTimeout 10 . C.setProtocolVersion C.V4 - $ C.defSettings + $ C.defSettings a <- Aws.mkEnv l o n io <- mkAutoUpdate diff --git a/services/gundeck/src/Gundeck/Instances.hs b/services/gundeck/src/Gundeck/Instances.hs index e4211673aaa..e162984af34 100644 --- a/services/gundeck/src/Gundeck/Instances.hs +++ b/services/gundeck/src/Gundeck/Instances.hs @@ -83,7 +83,8 @@ instance ToText (Id a) where toText = Text.decodeUtf8 . Uuid.toASCIIBytes . toUUID instance FromText (Id a) where - parser = Parser.take 36 >>= \txt -> - txt & Text.encodeUtf8 - & Uuid.fromASCIIBytes - & maybe (fail "Invalid UUID") (return . Id) + parser = + Parser.take 36 >>= \txt -> + txt & Text.encodeUtf8 + & Uuid.fromASCIIBytes + & maybe (fail "Invalid UUID") (return . Id) diff --git a/services/gundeck/src/Gundeck/Monad.hs b/services/gundeck/src/Gundeck/Monad.hs index dd07d342028..510d7e3e414 100644 --- a/services/gundeck/src/Gundeck/Monad.hs +++ b/services/gundeck/src/Gundeck/Monad.hs @@ -75,7 +75,7 @@ newtype Gundeck a = Gundeck instance MonadUnliftIO Gundeck where askUnliftIO = - Gundeck $ ReaderT $ \r -> + Gundeck . ReaderT $ \r -> withUnliftIO $ \u -> return (UnliftIO (unliftIO u . flip runReaderT r . unGundeck)) diff --git a/services/gundeck/src/Gundeck/Notification/Data.hs b/services/gundeck/src/Gundeck/Notification/Data.hs index cea89377fb0..65bd3d6f793 100644 --- a/services/gundeck/src/Gundeck/Notification/Data.hs +++ b/services/gundeck/src/Gundeck/Notification/Data.hs @@ -31,7 +31,7 @@ import qualified Data.Aeson as JSON import Data.Id import Data.List1 (List1) import Data.Range (Range, fromRange) -import Data.Sequence ((<|), (><), Seq, ViewL (..), ViewR (..)) +import Data.Sequence (Seq, ViewL (..), ViewR (..), (<|), (><)) import qualified Data.Sequence as Seq import Gundeck.Options (NotificationTTL (..)) import Gundeck.Types.Notification @@ -87,9 +87,10 @@ fetchLast u c = do ls <- query cqlLast (params Quorum (Identity u)) & retry x1 case ls of [] -> return Nothing - ns@(n : _) -> ns `getFirstOrElse` do - p <- paginate cqlSeek (paramsP Quorum (u, n ^. _1) 100) & retry x1 - seek p + ns@(n : _) -> + ns `getFirstOrElse` do + p <- paginate cqlSeek (paramsP Quorum (u, n ^. _1) 100) & retry x1 + seek p where seek p = result p diff --git a/services/gundeck/src/Gundeck/Presence/Data.hs b/services/gundeck/src/Gundeck/Presence/Data.hs index 66583e60a0c..ab0e8956b7e 100644 --- a/services/gundeck/src/Gundeck/Presence/Data.hs +++ b/services/gundeck/src/Gundeck/Presence/Data.hs @@ -60,7 +60,7 @@ add p = do let k = toKey (userId p) let v = toField (connId p) let d = encode $ PresenceData (resource p) (clientId p) now - retry x3 $ commands $ do + retry x3 . commands $ do multi void $ hset k v d -- nb. All presences of a user are expired 'maxIdleTime' after the @@ -77,7 +77,7 @@ deleteAll [] = return () deleteAll pp = for_ pp $ \p -> do let k = toKey (userId p) let f = __field p - retry x3 $ commands $ do + retry x3 . commands $ do watch (pure k) value <- hget k f multi diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index ba8da833896..380e37b0279 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -34,7 +34,7 @@ where import Control.Arrow ((&&&)) import Control.Error import Control.Exception (ErrorCall (ErrorCall)) -import Control.Lens ((%~), (.~), (^.), _2, view) +import Control.Lens (view, (%~), (.~), (^.), _2) import Control.Monad.Catch import Data.Aeson as Aeson (Object) import Data.Id @@ -65,7 +65,7 @@ import Gundeck.Util import Imports import Network.HTTP.Types import Network.Wai.Utilities -import System.Logger.Class ((+++), (.=), msg, val, (~~)) +import System.Logger.Class (msg, val, (+++), (.=), (~~)) import qualified System.Logger.Class as Log import UnliftIO.Concurrent (forkIO) import qualified Wire.API.Push.Token as Public @@ -221,9 +221,9 @@ pushAll pushes = do -- to be sent out. -- If perPushConcurrency is defined, we take the min with 'perNativePushConcurrency', as native push requests -- to cassandra and SNS are limited to 'perNativePushConcurrency' in parallel. - unless (psh ^. pushTransient) - $ mpaRunWithBudget cost () - $ mpaPushNative notif psh =<< nativeTargets psh rcps' alreadySent + unless (psh ^. pushTransient) $ + mpaRunWithBudget cost () $ + mpaPushNative notif psh =<< nativeTargets psh rcps' alreadySent -- REFACTOR: @[Presence]@ here should be @newtype WebSockedDelivered = WebSockedDelivered [Presence]@ compilePushReq :: (Push, (Notification, List1 (Recipient, [Presence]))) -> (Notification, [Presence]) @@ -319,10 +319,10 @@ nativeTargets psh rcps' alreadySent = addresses :: Recipient -> m [Address] addresses u = do addrs <- mntgtLookupAddresses (u ^. recipientId) - return - $ preference + return $ + preference . filter (eligible u) - $ addrs + $ addrs eligible :: Recipient -> Address -> Bool eligible u a -- Never include the origin client. @@ -459,27 +459,28 @@ addToken uid cid newtok = mpaRunWithBudget 1 AddTokenNoBudget $ do ept <- Aws.execute aws (Aws.lookupEndpoint arn) case ept of Nothing -> create (n + 1) t - Just ep -> do - updateEndpoint uid t arn ep - Data.insert - uid - (t ^. tokenTransport) - (t ^. tokenApp) - (t ^. token) - arn - cid - (t ^. tokenClient) - return (Right (mkAddr t arn)) - `catch` \case - -- Note: If the endpoint was recently deleted (not necessarily - -- concurrently), we may get an EndpointNotFound error despite - -- the previous lookup, i.e. endpoint lookups may exhibit eventually - -- consistent semantics with regards to endpoint deletion (or - -- possibly updates in general). We make another attempt to (re-)create - -- the endpoint in these cases instead of failing immediately. - Aws.EndpointNotFound {} -> create (n + 1) t - Aws.InvalidCustomData {} -> return (Left AddTokenMetadataTooLong) - ex -> throwM ex + Just ep -> + do + updateEndpoint uid t arn ep + Data.insert + uid + (t ^. tokenTransport) + (t ^. tokenApp) + (t ^. token) + arn + cid + (t ^. tokenClient) + return (Right (mkAddr t arn)) + `catch` \case + -- Note: If the endpoint was recently deleted (not necessarily + -- concurrently), we may get an EndpointNotFound error despite + -- the previous lookup, i.e. endpoint lookups may exhibit eventually + -- consistent semantics with regards to endpoint deletion (or + -- possibly updates in general). We make another attempt to (re-)create + -- the endpoint in these cases instead of failing immediately. + Aws.EndpointNotFound {} -> create (n + 1) t + Aws.InvalidCustomData {} -> return (Left AddTokenMetadataTooLong) + ex -> throwM ex -- mkAddr :: PushToken -> diff --git a/services/gundeck/src/Gundeck/Push/Native.hs b/services/gundeck/src/Gundeck/Push/Native.hs index 5309b612e7c..5788de8cf85 100644 --- a/services/gundeck/src/Gundeck/Push/Native.hs +++ b/services/gundeck/src/Gundeck/Push/Native.hs @@ -22,7 +22,7 @@ module Gundeck.Push.Native ) where -import Control.Lens ((.~), (^.), view) +import Control.Lens (view, (.~), (^.)) import Control.Monad.Catch import Data.ByteString.Conversion.To import Data.Id diff --git a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs index ff193eaeb7d..ac084dc1898 100644 --- a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs +++ b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs @@ -22,7 +22,7 @@ module Gundeck.Push.Native.Serialise where import Control.Lens ((^.), (^?), _Just) -import Data.Aeson ((.=), Value, object) +import Data.Aeson (Value, object, (.=)) import Data.Aeson.Text (encodeToTextBuilder) import qualified Data.ByteString as BS import Data.Json.Util @@ -85,8 +85,10 @@ renderText t aps prio x = case t of # "loc-args" .= (aps ^? _Just . apsLocArgs) # [] ) - # "sound" .= (aps ^? _Just . apsSound) - # "content-available" .= '1' + # "sound" + .= (aps ^? _Just . apsSound) + # "content-available" + .= '1' # [] apsDict LowPriority = object $ diff --git a/services/gundeck/src/Gundeck/Push/Native/Types.hs b/services/gundeck/src/Gundeck/Push/Native/Types.hs index 3787c238dd1..70f391a6c7f 100644 --- a/services/gundeck/src/Gundeck/Push/Native/Types.hs +++ b/services/gundeck/src/Gundeck/Push/Native/Types.hs @@ -40,7 +40,7 @@ module Gundeck.Push.Native.Types ) where -import Control.Lens (Lens', (^.), makeLenses, view) +import Control.Lens (Lens', makeLenses, view, (^.)) import Data.Id (ClientId, ConnId, UserId) import Gundeck.Aws.Arn import Gundeck.Types diff --git a/services/gundeck/src/Gundeck/Push/Websocket.hs b/services/gundeck/src/Gundeck/Push/Websocket.hs index 1c9f26e5e1f..6cfdab100ef 100644 --- a/services/gundeck/src/Gundeck/Push/Websocket.hs +++ b/services/gundeck/src/Gundeck/Push/Websocket.hs @@ -27,7 +27,7 @@ import Bilge.RPC import Bilge.Retry (rpcHandlers) import Control.Arrow ((&&&)) import Control.Exception (ErrorCall (ErrorCall)) -import Control.Lens ((%~), (^.), _2, view) +import Control.Lens (view, (%~), (^.), _2) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, catch, throwM, try) import Control.Retry import Data.Aeson (eitherDecode, encode) @@ -51,7 +51,7 @@ import Network.HTTP.Client (HttpException (..), HttpExceptionContent (..)) import qualified Network.HTTP.Client.Internal as Http import Network.HTTP.Types (StdMethod (POST), status200, status410) import qualified Network.URI as URI -import System.Logger.Class ((+++), val, (~~)) +import System.Logger.Class (val, (+++), (~~)) import qualified System.Logger.Class as Log import UnliftIO (handleAny, mapConcurrently) @@ -368,12 +368,13 @@ send n pp = where fn js p = do req <- Http.setUri empty (fromURI (resource p)) - recovering x1 rpcHandlers $ const - $ rpc' "cannon" (check req) - $ method POST - . contentJson - . lbytes js - . timeout 3000 -- ms + recovering x1 rpcHandlers $ + const $ + rpc' "cannon" (check req) $ + method POST + . contentJson + . lbytes js + . timeout 3000 -- ms check r = r { Http.checkResponse = \rq rs -> diff --git a/services/gundeck/src/Gundeck/React.hs b/services/gundeck/src/Gundeck/React.hs index 358ce9ff7cc..d2d3a183cd3 100644 --- a/services/gundeck/src/Gundeck/React.hs +++ b/services/gundeck/src/Gundeck/React.hs @@ -22,7 +22,7 @@ module Gundeck.React ) where -import Control.Lens ((.~), (^.), view) +import Control.Lens (view, (.~), (^.)) import Data.ByteString.Conversion import Data.Id (ClientId, UserId) import qualified Data.List as List @@ -44,7 +44,7 @@ import qualified Gundeck.Push.Websocket as Web import Gundeck.Types import Gundeck.Util import Imports -import System.Logger.Class ((+++), (.=), Msg, msg, val, (~~)) +import System.Logger.Class (Msg, msg, val, (+++), (.=), (~~)) import qualified System.Logger.Class as Log onEvent :: Event -> Gundeck () @@ -76,7 +76,8 @@ onUpdated ev = withEndpoint ev $ \e as -> forM_ sup $ \a -> do logUserEvent (a ^. addrUser) ev $ msg (val "Removing superseded token") deleteToken (a ^. addrUser) ev (a ^. addrToken) (a ^. addrClient) - if | null sup -> return () + if + | null sup -> return () | null cur -> deleteEndpoint ev | otherwise -> updateEndpoint ev e (map (view addrUser) cur) diff --git a/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs b/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs index 8d66e207cc5..4ce346495a8 100644 --- a/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs +++ b/services/gundeck/src/Gundeck/ThreadBudget/Internal.hs @@ -226,14 +226,15 @@ removeStaleHandles ref = do "watchThreadBudgetState: total overall thread budget diverged from async weights (repaired)." where getStaleHandles :: m (Set UUID) - getStaleHandles = Set.fromList . mconcat <$> do - handles <- HM.toList . bmap <$> readIORef ref - forM handles $ \case - (_, (_, Nothing)) -> do - pure [] - (key, (_, Just handle)) -> do - status <- poll handle - pure [key | isJust status] + getStaleHandles = + Set.fromList . mconcat <$> do + handles <- HM.toList . bmap <$> readIORef ref + forM handles $ \case + (_, (_, Nothing)) -> do + pure [] + (key, (_, Just handle)) -> do + status <- poll handle + pure [key | isJust status] warnStaleHandles :: Int -> BudgetMap -> m () warnStaleHandles num (BudgetMap spent _) = LC.warn $ @@ -245,7 +246,8 @@ safeForever :: (MonadIO m, LC.MonadLogger m, MonadCatch m) => m () -> m () -safeForever action = forever $ - action `catchAny` \exc -> do - LC.err $ "error" LC..= show exc LC.~~ LC.msg (LC.val "watchThreadBudgetState: crashed; retrying") - threadDelay 60000000 -- pause to keep worst-case noise in logs manageable +safeForever action = + forever $ + action `catchAny` \exc -> do + LC.err $ "error" LC..= show exc LC.~~ LC.msg (LC.val "watchThreadBudgetState: crashed; retrying") + threadDelay 60000000 -- pause to keep worst-case noise in logs manageable diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index 6ca9d0a22f1..7cc5984ceaa 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -27,7 +27,7 @@ import Bilge.Assert import qualified Cassandra as Cql import Control.Arrow ((&&&)) import Control.Concurrent.Async (Async, async, concurrently_, forConcurrently_, wait) -import Control.Lens ((%~), (.~), (<&>), (^.), (^?), _2, view) +import Control.Lens (view, (%~), (.~), (<&>), (^.), (^?), _2) import Control.Retry (constantDelay, limitRetries, recoverAll, retrying) import Data.Aeson hiding (json) import Data.Aeson.Lens @@ -260,9 +260,10 @@ bulkPush isE2E numUsers numConnsPerUser = do ploadE2E :: ConnId -> List1 Aeson.Object ploadE2E connid = List1.singleton $ HashMap.fromList ["connid" .= connid] pushE2E :: UserId -> [(UserId, [(ConnId, Bool)])] -> [Push] - pushE2E u ucs = targets <&> \(uid, connid) -> - newPush u (toRecipients [uid]) (ploadE2E connid) - & pushConnections .~ Set.singleton connid + pushE2E u ucs = + targets <&> \(uid, connid) -> + newPush u (toRecipients [uid]) (ploadE2E connid) + & pushConnections .~ Set.singleton connid where targets :: [(UserId, ConnId)] targets = @@ -322,7 +323,7 @@ sendMultipleUsers = do -- 'uid1' and 'uid2' should each have 1 notification ntfs1 <- listNotifications uid1 Nothing ntfs2 <- listNotifications uid2 Nothing - liftIO $ forM_ [ntfs1, ntfs2] $ \ntfs -> do + liftIO . forM_ [ntfs1, ntfs2] $ \ntfs -> do assertEqual "Not exactly 1 notification" 1 (length ntfs) let p = view queuedNotificationPayload (Prelude.head ntfs) assertEqual "Wrong events in notification" pload p @@ -390,7 +391,7 @@ targetClientPush = do -- Check the notification stream ns1 <- listNotifications uid (Just cid1) ns2 <- listNotifications uid (Just cid2) - liftIO $ forM_ [(ns1, cid1), (ns2, cid2)] $ \(ns, c) -> do + liftIO . forM_ [(ns1, cid1), (ns2, cid2)] $ \(ns, c) -> do assertEqual "Not exactly 1 notification" 1 (length ns) let p = view queuedNotificationPayload (Prelude.head ns) assertEqual "Wrong events in notification" (pload c) p @@ -797,10 +798,10 @@ testSharePushToken = do let t2 = tk c2 t1' <- registerPushToken u1 t1 t2' <- registerPushToken u2 t2 -- share the token with u1 - -- Unfortunately this fails locally :( - -- "Duplicate endpoint token: 61d22005-af6e-4199-add9-899aae79c70a" - -- Instead of getting something in the lines of - -- "Invalid parameter: Token Reason: Endpoint " already exists with the same Token, but different attributes." + -- Unfortunately this fails locally :( + -- "Duplicate endpoint token: 61d22005-af6e-4199-add9-899aae79c70a" + -- Instead of getting something in the lines of + -- "Invalid parameter: Token Reason: Endpoint " already exists with the same Token, but different attributes." liftIO $ assertEqual "token mismatch" (t1 ^. token) t1' liftIO $ assertEqual "token mismatch" (t2 ^. token) t2' liftIO $ assertEqual "token mismatch" t1' t2' @@ -889,12 +890,13 @@ connectUsersAndDevicesWithSendingClients :: [(UserId, [ConnId])] -> TestM [(UserId, [(TChan ByteString, TChan ByteString)])] connectUsersAndDevicesWithSendingClients ca uidsAndConnIds = do - chs <- forM uidsAndConnIds $ \(uid, conns) -> (uid,) <$> do - forM conns $ \conn -> do - chread <- liftIO $ atomically newTChan - chwrite <- liftIO $ atomically newTChan - _ <- wsRun ca uid conn (wsReaderWriter chread chwrite) - pure (chread, chwrite) + chs <- forM uidsAndConnIds $ \(uid, conns) -> + (uid,) <$> do + forM conns $ \conn -> do + chread <- liftIO $ atomically newTChan + chwrite <- liftIO $ atomically newTChan + _ <- wsRun ca uid conn (wsReaderWriter chread chwrite) + pure (chread, chwrite) (\(uid, conns) -> wsAssertPresences uid (length conns)) `mapM_` uidsAndConnIds pure chs @@ -1009,20 +1011,22 @@ listNotifications u c = do (view queuedTime ns) getNotifications :: UserId -> Maybe ClientId -> TestM (Response (Maybe BL.ByteString)) -getNotifications u c = view tsGundeck >>= \gu -> - get $ - runGundeckR gu - . zUser u - . path "notifications" - . maybe id (queryItem "client" . toByteString') c +getNotifications u c = + view tsGundeck >>= \gu -> + get $ + runGundeckR gu + . zUser u + . path "notifications" + . maybe id (queryItem "client" . toByteString') c getLastNotification :: UserId -> Maybe ClientId -> TestM (Response (Maybe BL.ByteString)) -getLastNotification u c = view tsGundeck >>= \gu -> - get $ - runGundeckR gu - . zUser u - . paths ["notifications", "last"] - . maybe id (queryItem "client" . toByteString') c +getLastNotification u c = + view tsGundeck >>= \gu -> + get $ + runGundeckR gu + . zUser u + . paths ["notifications", "last"] + . maybe id (queryItem "client" . toByteString') c sendPush :: HasCallStack => Push -> TestM () sendPush push = sendPushes [push] @@ -1101,10 +1105,11 @@ toRecipients :: [UserId] -> Range 1 1024 (Set Recipient) toRecipients = unsafeRange . Set.fromList . map (`recipient` RouteAny) randomConnId :: MonadIO m => m ConnId -randomConnId = liftIO $ - ConnId <$> do - r <- randomIO :: IO Word32 - return $ C.pack $ show r +randomConnId = + liftIO $ + ConnId <$> do + r <- randomIO :: IO Word32 + return $ C.pack $ show r randomClientId :: MonadIO m => m ClientId randomClientId = liftIO $ newClientId <$> (randomIO :: IO Word64) diff --git a/services/gundeck/test/integration/Main.hs b/services/gundeck/test/integration/Main.hs index 21d4ea80a1b..a24fde8dc84 100644 --- a/services/gundeck/test/integration/Main.hs +++ b/services/gundeck/test/integration/Main.hs @@ -65,24 +65,25 @@ instance IsOption ServiceConfigFile where optionName = return "service-config" optionHelp = return "Service config file to read from" optionCLParser = - fmap ServiceConfigFile $ strOption $ - ( short (untag (return 's' :: Tagged ServiceConfigFile Char)) - <> long (untag (optionName :: Tagged ServiceConfigFile String)) - <> help (untag (optionHelp :: Tagged ServiceConfigFile String)) - ) + fmap ServiceConfigFile $ + strOption $ + ( short (untag (return 's' :: Tagged ServiceConfigFile Char)) + <> long (untag (optionName :: Tagged ServiceConfigFile String)) + <> help (untag (optionHelp :: Tagged ServiceConfigFile String)) + ) runTests :: (String -> String -> TestTree) -> IO () -runTests run = defaultMainWithIngredients ings - $ askOption - $ \(ServiceConfigFile c) -> - askOption $ \(IntegrationConfigFile i) -> run c i +runTests run = defaultMainWithIngredients ings $ + askOption $ + \(ServiceConfigFile c) -> + askOption $ \(IntegrationConfigFile i) -> run c i where ings = includingOptions [ Option (Proxy :: Proxy ServiceConfigFile), Option (Proxy :: Proxy IntegrationConfigFile) - ] - : defaultIngredients + ] : + defaultIngredients main :: IO () main = withOpenSSL $ runTests go diff --git a/services/gundeck/test/integration/TestSetup.hs b/services/gundeck/test/integration/TestSetup.hs index 3c7947850ed..47e41b27ac2 100644 --- a/services/gundeck/test/integration/TestSetup.hs +++ b/services/gundeck/test/integration/TestSetup.hs @@ -37,7 +37,7 @@ where import Bilge (HttpT (..), Manager, MonadHttp, Request, runHttpT) import qualified Cassandra as Cql -import Control.Lens ((^.), makeLenses) +import Control.Lens (makeLenses, (^.)) import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) import Control.Monad.Fail (MonadFail) import Imports diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index 2f65c2178d9..9ea022a2303 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -53,7 +53,7 @@ import qualified Data.IntMultiSet as MSet import qualified Data.List.NonEmpty as NE import Data.List1 import qualified Data.Map as Map -import Data.Misc ((<$$>), Milliseconds (Ms)) +import Data.Misc (Milliseconds (Ms), (<$$>)) import Data.Range import qualified Data.Scientific as Scientific import qualified Data.Set as Set @@ -237,9 +237,11 @@ genMockEnv = do in nubrec <$> forM uids gencids -- Build an 'MockEnv' containing a map with all those 'ClientInfo's, and -- check that it validates - env <- MockEnv . Map.fromList . fmap (_2 %~ Map.fromList) <$> do - forM (zip uids cidss) $ \(uid, cids) -> (uid,) <$> do - forM cids $ \cid -> (cid,) <$> genClientInfo uid cid + env <- + MockEnv . Map.fromList . fmap (_2 %~ Map.fromList) <$> do + forM (zip uids cidss) $ \(uid, cids) -> + (uid,) <$> do + forM cids $ \cid -> (cid,) <$> genClientInfo uid cid validateMockEnv env & either error (const $ pure env) -- Try to shrink a 'MockEnv' by removing some users from '_meClientInfos'. @@ -354,16 +356,17 @@ genPush env = do -- | Shuffle devices. With probability 0.5, drop at least one device, but not all. If number of -- devices is @<2@ or if devices are set to 'RecipientClientsAll', the input is returned. dropSomeDevices :: Recipient -> Gen Recipient -dropSomeDevices = recipientClients %%~ \case - RecipientClientsAll -> pure RecipientClientsAll - RecipientClientsSome cids -> do - numdevs :: Int <- - oneof - [ pure $ length cids, - choose (1, max 1 (length cids - 1)) - ] - RecipientClientsSome . unsafeList1 . take numdevs - <$> QC.shuffle (toList cids) +dropSomeDevices = + recipientClients %%~ \case + RecipientClientsAll -> pure RecipientClientsAll + RecipientClientsSome cids -> do + numdevs :: Int <- + oneof + [ pure $ length cids, + choose (1, max 1 (length cids - 1)) + ] + RecipientClientsSome . unsafeList1 . take numdevs + <$> QC.shuffle (toList cids) shrinkPushes :: HasCallStack => [Push] -> [[Push]] shrinkPushes = shrinkList shrinkPush @@ -408,7 +411,7 @@ runMockGundeck env (MockGundeck m) = instance MonadThrow MockGundeck where throwM = error . show -- (we are not expecting any interesting errors in these tests, so we might - -- as well crash badly here, as long as it doesn't go unnoticed...) + -- as well crash badly here, as long as it doesn't go unnoticed...) instance MonadPushAll MockGundeck where mpaNotificationTTL = pure $ NotificationTTL 300 -- (longer than we want any test to take.) @@ -418,7 +421,7 @@ instance MonadPushAll MockGundeck where mpaStreamAdd = mockStreamAdd mpaPushNative = mockPushNative mpaForkIO = id -- just don't fork. (this *may* cause deadlocks in principle, but as long as it - -- doesn't, this is good enough for testing). + -- doesn't, this is good enough for testing). mpaRunWithBudget = \_ _ -> id -- no throttling needed as long as we don't overdo it in the tests... @@ -670,9 +673,10 @@ mockOldSimpleWebPush notif tgts _senderid mconnid connWhitelist = do then id else targetClients %~ filter ((`elem` connWhitelist) . fakeConnId) emptyMeansFullHack :: NotificationTarget -> NotificationTarget - emptyMeansFullHack tgt = tgt & targetClients %~ \case - [] -> clientIdsOfUser env (tgt ^. targetUser) - same@(_ : _) -> same + emptyMeansFullHack tgt = + tgt & targetClients %~ \case + [] -> clientIdsOfUser env (tgt ^. targetUser) + same@(_ : _) -> same forM_ clients $ \(userid, clientid) -> do msWSQueue %= deliver (userid, clientid) (ntfPayload notif) pure $ uncurry fakePresence <$> clients @@ -693,9 +697,10 @@ shrinkPretty shrnk (Pretty xs) = Pretty <$> shrnk xs sublist1Of :: HasCallStack => [a] -> Gen (List1 a) sublist1Of [] = error "sublist1Of: empty list" -sublist1Of xs = sublistOf xs >>= \case - [] -> sublist1Of xs - c : cc -> pure (list1 c cc) +sublist1Of xs = + sublistOf xs >>= \case + [] -> sublist1Of xs + c : cc -> pure (list1 c cc) unsafeList1 :: HasCallStack => [a] -> List1 a unsafeList1 [] = error "unsafeList1: empty list" diff --git a/services/gundeck/test/unit/ThreadBudget.hs b/services/gundeck/test/unit/ThreadBudget.hs index 7bd07c4c3b8..b84154e88f4 100644 --- a/services/gundeck/test/unit/ThreadBudget.hs +++ b/services/gundeck/test/unit/ThreadBudget.hs @@ -154,8 +154,8 @@ testThreadBudgets :: Assertion testThreadBudgets = do let timeUnits n = MilliSeconds $ lengthOfTimeUnit * n lengthOfTimeUnit = 5 -- if you make this larger, the test will run more slowly, and be - -- less likely to have timing issues. if you make it too small, some of the calls to - -- 'delayms' may return too fast and some things may not be ready yet. + -- less likely to have timing issues. if you make it too small, some of the calls to + -- 'delayms' may return too fast and some things may not be ready yet. tbs <- mkThreadBudgetState (MaxConcurrentNativePushes (Just 5) (Just 5)) logHistory :: LogHistory <- newMVar [] watcher <- mkWatcher tbs logHistory diff --git a/services/proxy/src/Proxy/API/Public.hs b/services/proxy/src/Proxy/API/Public.hs index 8015a0a6854..3b7c0a5b058 100644 --- a/services/proxy/src/Proxy/API/Public.hs +++ b/services/proxy/src/Proxy/API/Public.hs @@ -117,16 +117,16 @@ spotifyToken rq = do req = baseReq {Client.requestHeaders = hdr} mgr <- view manager res <- liftIO $ recovering x2 [handler] $ const (Client.httpLbs (Req.lbytes b req) mgr) - when (isError (Client.responseStatus res)) - $ debug - $ msg (val "unexpected upstream response") - ~~ "upstream" .= val "spotify::token" - ~~ "status" .= S (Client.responseStatus res) - ~~ "body" .= B.take 256 (Client.responseBody res) + when (isError (Client.responseStatus res)) $ + debug $ + msg (val "unexpected upstream response") + ~~ "upstream" .= val "spotify::token" + ~~ "status" .= S (Client.responseStatus res) + ~~ "body" .= B.take 256 (Client.responseBody res) return $ plain (Client.responseBody res) & setStatus (Client.responseStatus res) - . maybeHeader hContentType res + . maybeHeader hContentType res where baseReq = Req.method POST @@ -142,16 +142,16 @@ soundcloudResolve url = do let req = Req.queryItem "client_id" s . Req.queryItem "url" url $ baseReq mgr <- view manager res <- liftIO $ recovering x2 [handler] $ const (Client.httpLbs req mgr) - when (isError (Client.responseStatus res)) - $ debug - $ msg (val "unexpected upstream response") - ~~ "upstream" .= val "soundcloud::resolve" - ~~ "status" .= S (Client.responseStatus res) - ~~ "body" .= B.take 256 (Client.responseBody res) + when (isError (Client.responseStatus res)) $ + debug $ + msg (val "unexpected upstream response") + ~~ "upstream" .= val "soundcloud::resolve" + ~~ "status" .= S (Client.responseStatus res) + ~~ "body" .= B.take 256 (Client.responseBody res) return $ plain (Client.responseBody res) & setStatus (Client.responseStatus res) - . maybeHeader hContentType res + . maybeHeader hContentType res where baseReq = Req.method GET @@ -184,7 +184,7 @@ x2 :: RetryPolicy x2 = exponentialBackoff 5000 <> limitRetries 2 handler :: (MonadIO m, MonadMask m) => RetryStatus -> Handler m Bool -handler = const $ Handler $ \case +handler = const . Handler $ \case Client.HttpExceptionRequest _ Client.NoResponseDataReceived -> return True Client.HttpExceptionRequest _ Client.IncompleteHeaders -> return True Client.HttpExceptionRequest _ (Client.ConnectionTimeout) -> return True diff --git a/services/proxy/src/Proxy/Env.hs b/services/proxy/src/Proxy/Env.hs index 8fb7f38ef4f..1befea351c6 100644 --- a/services/proxy/src/Proxy/Env.hs +++ b/services/proxy/src/Proxy/Env.hs @@ -28,7 +28,7 @@ module Proxy.Env ) where -import Control.Lens ((^.), makeLenses) +import Control.Lens (makeLenses, (^.)) import Data.Configurator import Data.Configurator.Types import Data.Default (def) diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 3468c839e9e..8ead38ef6b0 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -367,9 +367,10 @@ validateIdPUpdate :: SAML.IdPId -> m (TeamId, IdP) validateIdPUpdate zusr _idpMetadata _idpId = do - previousIdP <- wrapMonadClient (Data.getIdPConfig _idpId) >>= \case - Nothing -> throwError errUnknownIdPId - Just idp -> pure idp + previousIdP <- + wrapMonadClient (Data.getIdPConfig _idpId) >>= \case + Nothing -> throwError errUnknownIdPId + Just idp -> pure idp teamId <- authorizeIdP zusr previousIdP unless (previousIdP ^. SAML.idpExtraInfo . wiTeam == teamId) $ do throwError errUnknownIdP diff --git a/services/spar/src/Spar/API/Swagger.hs b/services/spar/src/Spar/API/Swagger.hs index d0fd8f272ca..a642aa80a44 100644 --- a/services/spar/src/Spar/API/Swagger.hs +++ b/services/spar/src/Spar/API/Swagger.hs @@ -131,12 +131,13 @@ instance ToSchema SAML.IdPMetadata where instance ToSchema IdPMetadataInfo where declareNamedSchema _ = - pure $ NamedSchema (Just "IdPMetadataInfo") $ - mempty - & properties .~ properties_ - & minProperties ?~ 1 - & maxProperties ?~ 1 - & type_ .~ Just SwaggerObject + pure $ + NamedSchema (Just "IdPMetadataInfo") $ + mempty + & properties .~ properties_ + & minProperties ?~ 1 + & maxProperties ?~ 1 + & type_ .~ Just SwaggerObject where properties_ :: InsOrdHashMap Text (Referenced Schema) properties_ = diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index e02789a47af..ebc6baeafa0 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -43,7 +43,7 @@ import Control.Exception (assert) import Control.Lens hiding ((.=)) import qualified Control.Monad.Catch as Catch import Control.Monad.Except -import Data.Aeson as Aeson ((.=), encode, object) +import Data.Aeson as Aeson (encode, object, (.=)) import qualified Data.ByteString.Builder as Builder import qualified Data.ByteString.Lazy.Char8 as LBS import Data.Id @@ -401,10 +401,11 @@ verdictHandlerResultCore bindCky = \case -- not be the title of any page sent by the IdP while it negotiates with the user. -- - The page broadcasts a message to '*', to be picked up by the app. verdictHandlerWeb :: HasCallStack => VerdictHandlerResult -> Spar SAML.ResponseVerdict -verdictHandlerWeb = pure . \case - VerifyHandlerGranted cky _uid -> successPage cky - VerifyHandlerDenied reasons -> forbiddenPage "forbidden" (explainDeniedReason <$> reasons) - VerifyHandlerError lbl msg -> forbiddenPage lbl [msg] +verdictHandlerWeb = + pure . \case + VerifyHandlerGranted cky _uid -> successPage cky + VerifyHandlerDenied reasons -> forbiddenPage "forbidden" (explainDeniedReason <$> reasons) + VerifyHandlerError lbl msg -> forbiddenPage lbl [msg] where forbiddenPage :: ST -> [ST] -> SAML.ResponseVerdict forbiddenPage errlbl reasons = diff --git a/services/spar/src/Spar/Data.hs b/services/spar/src/Spar/Data.hs index abefd39cc1b..8eb46193db0 100644 --- a/services/spar/src/Spar/Data.hs +++ b/services/spar/src/Spar/Data.hs @@ -142,7 +142,8 @@ mkTTL now maxttl endOfLife = mkTTLNDT maxttl $ endOfLife `diffUTCTime` now mkTTLNDT :: (MonadError TTLError m, KnownSymbol a) => TTL a -> NominalDiffTime -> m (TTL a) mkTTLNDT maxttl ttlNDT = - if | actualttl > maxttl -> throwError $ TTLTooLong (showTTL actualttl) (showTTL maxttl) + if + | actualttl > maxttl -> throwError $ TTLTooLong (showTTL actualttl) (showTTL maxttl) | actualttl <= 0 -> throwError $ TTLNegative (showTTL actualttl) | otherwise -> pure actualttl where @@ -320,8 +321,9 @@ insertBindCookie cky uid ttlNDT = do -- | The counter-part of 'insertBindCookie'. lookupBindCookie :: (HasCallStack, MonadClient m) => BindCookie -> m (Maybe UserId) -lookupBindCookie (cs . fromBindCookie -> ckyval :: ST) = runIdentity <$$> do - (retry x1 . query1 sel $ params Quorum (Identity ckyval)) +lookupBindCookie (cs . fromBindCookie -> ckyval :: ST) = + runIdentity <$$> do + (retry x1 . query1 sel $ params Quorum (Identity ckyval)) where sel :: PrepQuery R (Identity ST) (Identity UserId) sel = "SELECT session_owner FROM bind_cookie WHERE cookie = ?" @@ -465,7 +467,7 @@ deleteIdPConfig :: SAML.Issuer -> TeamId -> m () -deleteIdPConfig idp issuer team = retry x5 $ batch $ do +deleteIdPConfig idp issuer team = retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery delDefaultIdp (Identity idp) @@ -587,7 +589,7 @@ insertScimToken :: ScimToken -> ScimTokenInfo -> m () -insertScimToken token ScimTokenInfo {..} = retry x5 $ batch $ do +insertScimToken token ScimTokenInfo {..} = retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery insByToken (token, stiTeam, stiId, stiCreatedAt, stiIdP, stiDescr) @@ -650,7 +652,7 @@ deleteScimToken :: m () deleteScimToken team tokenid = do mbToken <- retry x1 . query1 selById $ params Quorum (team, tokenid) - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery delById (team, tokenid) @@ -683,7 +685,7 @@ deleteTeamScimTokens :: m () deleteTeamScimTokens team = do tokens <- retry x5 $ query sel $ params Quorum (Identity team) - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum addPrepQuery delByTeam (Identity team) diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index c00757b9d04..1f31882c5d0 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -169,7 +169,8 @@ createBrigUser suid (Id buid) teamid mbName managedBy = do . path "/i/users" . json newUser let sCode = statusCode resp - if | sCode < 300 -> + if + | sCode < 300 -> userId . selfUser <$> parseResponse @SelfProfile resp | inRange (400, 499) sCode -> throwSpar . SparBrigErrorWith (responseStatus resp) $ "create user failed" @@ -256,7 +257,8 @@ setBrigUserName buid name = do uupAccentId = Nothing } let sCode = statusCode resp - if | sCode < 300 -> + if + | sCode < 300 -> pure () | inRange (400, 499) sCode -> throwSpar . SparBrigErrorWith (responseStatus resp) $ "set name failed" @@ -275,7 +277,8 @@ setBrigUserHandle buid handle = do . header "Z-Connection" "" . json (HandleUpdate (fromHandle handle)) let sCode = statusCode resp - if | sCode < 300 -> + if + | sCode < 300 -> pure () | inRange (400, 499) sCode -> throwSpar . SparBrigErrorWith (responseStatus resp) $ "set handle failed" @@ -292,7 +295,8 @@ setBrigUserManagedBy buid managedBy = do . paths ["i", "users", toByteString' buid, "managed-by"] . json (ManagedByUpdate managedBy) let sCode = statusCode resp - if | sCode < 300 -> + if + | sCode < 300 -> pure () | inRange (400, 499) sCode -> throwSpar . SparBrigErrorWith (responseStatus resp) $ "set managedBy failed" @@ -308,7 +312,8 @@ setBrigUserUserRef buid uref = do . paths ["i", "users", toByteString' buid, "sso-id"] . json (toUserSSOId uref) let sCode = statusCode resp - if | sCode < 300 -> + if + | sCode < 300 -> pure () | inRange (400, 499) sCode -> throwSpar . SparBrigErrorWith (responseStatus resp) $ "set UserSSOId failed" @@ -325,7 +330,8 @@ setBrigUserRichInfo buid richInfo = do . paths ["i", "users", toByteString' buid, "rich-info"] . json (RichInfoUpdate $ unRichInfo richInfo) let sCode = statusCode resp - if | sCode < 300 -> + if + | sCode < 300 -> pure () | inRange (400, 499) sCode -> throwSpar . SparBrigErrorWith (responseStatus resp) $ "set richInfo failed" @@ -334,16 +340,17 @@ setBrigUserRichInfo buid richInfo = do -- TODO: We should add an internal endpoint for this instead getBrigUserRichInfo :: (HasCallStack, MonadSparToBrig m) => UserId -> m RichInfo -getBrigUserRichInfo buid = RichInfo.RichInfo <$> do - resp <- - call $ - method GET - . paths ["users", toByteString' buid, "rich-info"] - . header "Z-User" (toByteString' buid) - . header "Z-Connection" "" - case statusCode resp of - 200 -> parseResponse resp - _ -> throwSpar (SparBrigErrorWith (responseStatus resp) "Could not retrieve rich info") +getBrigUserRichInfo buid = + RichInfo.RichInfo <$> do + resp <- + call $ + method GET + . paths ["users", toByteString' buid, "rich-info"] + . header "Z-User" (toByteString' buid) + . header "Z-Connection" "" + case statusCode resp of + 200 -> parseResponse resp + _ -> throwSpar (SparBrigErrorWith (responseStatus resp) "Could not retrieve rich info") -- | At the time of writing this, @HEAD /users/handles/:uid@ does not use the 'UserId' for -- anything but authorization. @@ -356,7 +363,8 @@ checkHandleAvailable hnd buid = do . header "Z-User" (toByteString' buid) . header "Z-Connection" "" let sCode = statusCode resp - if | sCode == 200 -> -- handle exists + if + | sCode == 200 -> -- handle exists pure False | sCode == 404 -> -- handle not found pure True @@ -386,7 +394,8 @@ deleteBrigUser buid = do method DELETE . paths ["/i/users", toByteString' buid] let sCode = statusCode resp - if | sCode < 300 -> pure () + if + | sCode < 300 -> pure () | inRange (400, 499) sCode -> throwSpar $ SparBrigErrorWith (responseStatus resp) "failed to delete user" | otherwise -> @@ -430,7 +439,8 @@ ensureReAuthorised (Just uid) secret = do . paths ["/i/users", toByteString' uid, "reauthenticate"] . json (ReAuthUser secret) let sCode = statusCode resp - if | sCode == 200 -> + if + | sCode == 200 -> pure () | sCode == 403 -> throwSpar SparReAuthRequired @@ -454,7 +464,8 @@ ssoLogin buid = do . json (SsoLogin buid Nothing) . queryItem "persist" "true" let sCode = statusCode resp - if | sCode < 300 -> + if + | sCode < 300 -> Just <$> respToCookie resp | inRange (400, 499) sCode -> pure Nothing diff --git a/services/spar/src/Spar/Scim.hs b/services/spar/src/Spar/Scim.hs index e9603c5ae17..2feb83e0e70 100644 --- a/services/spar/src/Spar/Scim.hs +++ b/services/spar/src/Spar/Scim.hs @@ -119,8 +119,9 @@ apiScim = -- We caught an exception that's not a Spar exception at all. It is wrapped into -- Scim.serverError. Left someException -> - pure $ Left . SAML.CustomError . SparScimError $ - Scim.serverError (cs (displayException someException)) + pure $ + Left . SAML.CustomError . SparScimError $ + Scim.serverError (cs (displayException someException)) -- We caught a 'SparScimError' exception. It is left as-is. Right err@(Left (SAML.CustomError (SparScimError _))) -> pure err @@ -129,8 +130,9 @@ apiScim = -- TODO: does it have to be logged? Right (Left sparError) -> do err <- sparToServerErrorWithLogging (sparCtxLogger env) sparError - pure $ Left . SAML.CustomError . SparScimError $ - Scim.serverError (cs (errBody err)) + pure $ + Left . SAML.CustomError . SparScimError $ + Scim.serverError (cs (errBody err)) -- No exceptions! Good. Right (Right x) -> pure $ Right x diff --git a/services/spar/src/Spar/Scim/Auth.hs b/services/spar/src/Spar/Scim/Auth.hs index 01c2e5d4a81..642196698d2 100644 --- a/services/spar/src/Spar/Scim/Auth.hs +++ b/services/spar/src/Spar/Scim/Auth.hs @@ -34,7 +34,7 @@ module Spar.Scim.Auth ) where -import Control.Lens hiding ((.=), Strict) +import Control.Lens hiding (Strict, (.=)) import qualified Data.ByteString.Base64 as ES import Data.Id (ScimTokenId, UserId, randomId) import Data.String.Conversions (cs) @@ -42,8 +42,8 @@ import Data.Time (getCurrentTime) import Imports import OpenSSL.Random (randBytes) import qualified SAML2.WebSSO as SAML -import Servant ((:<|>) ((:<|>)), NoContent (NoContent), ServerT) -import Spar.App (Spar, sparCtxOpts, wrapMonadClient, wrapMonadClient) +import Servant (NoContent (NoContent), ServerT, (:<|>) ((:<|>))) +import Spar.App (Spar, sparCtxOpts, wrapMonadClient) import qualified Spar.Data as Data import qualified Spar.Error as E import qualified Spar.Intra.Brig as Intra.Brig @@ -51,7 +51,6 @@ import Spar.Scim.Types ( APIScimToken, CreateScimToken (CreateScimToken), CreateScimTokenResponse (..), - ScimTokenList, ScimTokenList (..), SparTag, createScimTokenDescr, diff --git a/services/spar/src/Spar/Scim/Swagger.hs b/services/spar/src/Spar/Scim/Swagger.hs index c0ae1fa7a1f..60a20e52df4 100644 --- a/services/spar/src/Spar/Scim/Swagger.hs +++ b/services/spar/src/Spar/Scim/Swagger.hs @@ -30,7 +30,7 @@ module Spar.Scim.Swagger ) where -import Control.Lens ((&), (.~), (?~), mapped) +import Control.Lens (mapped, (&), (.~), (?~)) import Data.Id (ScimTokenId, TeamId) import Data.Proxy (Proxy (Proxy)) import Data.Swagger hiding (Header (..)) @@ -56,50 +56,54 @@ instance ToSchema ScimTokenInfo where createdAtSchema <- declareSchemaRef (Proxy @UTCTime) idpSchema <- declareSchemaRef (Proxy @SAML.IdPId) descrSchema <- declareSchemaRef (Proxy @Text) - return $ NamedSchema (Just "ScimTokenInfo") $ - mempty - & type_ .~ Just SwaggerObject - & properties - .~ [ ("team", teamSchema), - ("id", idSchema), - ("created_at", createdAtSchema), - ("idp", idpSchema), - ("description", descrSchema) - ] - & required .~ ["team", "id", "created_at", "description"] + return $ + NamedSchema (Just "ScimTokenInfo") $ + mempty + & type_ .~ Just SwaggerObject + & properties + .~ [ ("team", teamSchema), + ("id", idSchema), + ("created_at", createdAtSchema), + ("idp", idpSchema), + ("description", descrSchema) + ] + & required .~ ["team", "id", "created_at", "description"] instance ToSchema CreateScimToken where declareNamedSchema _ = do textSchema <- declareSchemaRef (Proxy @Text) - return $ NamedSchema (Just "CreateScimToken") $ - mempty - & type_ .~ Just SwaggerObject - & properties - .~ [ ("description", textSchema), - ("password", textSchema) - ] - & required .~ ["description"] + return $ + NamedSchema (Just "CreateScimToken") $ + mempty + & type_ .~ Just SwaggerObject + & properties + .~ [ ("description", textSchema), + ("password", textSchema) + ] + & required .~ ["description"] instance ToSchema CreateScimTokenResponse where declareNamedSchema _ = do tokenSchema <- declareSchemaRef (Proxy @ScimToken) infoSchema <- declareSchemaRef (Proxy @ScimTokenInfo) - return $ NamedSchema (Just "CreateScimTokenResponse") $ - mempty - & type_ .~ Just SwaggerObject - & properties - .~ [ ("token", tokenSchema), - ("info", infoSchema) - ] - & required .~ ["token", "info"] + return $ + NamedSchema (Just "CreateScimTokenResponse") $ + mempty + & type_ .~ Just SwaggerObject + & properties + .~ [ ("token", tokenSchema), + ("info", infoSchema) + ] + & required .~ ["token", "info"] instance ToSchema ScimTokenList where declareNamedSchema _ = do infoListSchema <- declareSchemaRef (Proxy @[ScimTokenInfo]) - return $ NamedSchema (Just "ScimTokenList") $ - mempty - & type_ .~ Just SwaggerObject - & properties - .~ [ ("tokens", infoListSchema) - ] - & required .~ ["tokens"] + return $ + NamedSchema (Just "ScimTokenList") $ + mempty + & type_ .~ Just SwaggerObject + & properties + .~ [ ("tokens", infoListSchema) + ] + & required .~ ["tokens"] diff --git a/services/spar/src/Spar/Scim/Types.hs b/services/spar/src/Spar/Scim/Types.hs index fc7ddd60bc6..a02fbace1c5 100644 --- a/services/spar/src/Spar/Scim/Types.hs +++ b/services/spar/src/Spar/Scim/Types.hs @@ -52,8 +52,8 @@ import qualified Data.Map as Map import Data.Misc (PlainTextPassword) import Imports import qualified SAML2.WebSSO as SAML -import Servant ((:<|>), (:>), DeleteNoContent, Get, Header, JSON, NoContent, Post, QueryParam', ReqBody, Required, Strict) -import Servant.API.Generic ((:-), ToServantApi) +import Servant (DeleteNoContent, Get, Header, JSON, NoContent, Post, QueryParam', ReqBody, Required, Strict, (:<|>), (:>)) +import Servant.API.Generic (ToServantApi, (:-)) import Spar.API.Util (OmitDocs) import Spar.Types (ScimToken, ScimTokenInfo) import Web.Scim.AttrName (AttrName (..)) diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index e6900d73096..2954147817f 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -230,19 +230,20 @@ validateScimUser' idp richInfoLimit user = do validateRichInfo :: RI.RichInfo -> m RI.RichInfo validateRichInfo richInfo = do let sze = RI.richInfoSize richInfo - when (sze > richInfoLimit) $ throwError $ - ( Scim.badRequest - Scim.InvalidValue - ( Just . cs $ - show [RI.richInfoMapURN, RI.richInfoAssocListURN] - <> " together exceed the size limit: max " - <> show richInfoLimit - <> " characters, but got " - <> show sze - ) - ) - { Scim.status = Scim.Status 413 - } + when (sze > richInfoLimit) $ + throwError $ + ( Scim.badRequest + Scim.InvalidValue + ( Just . cs $ + show [RI.richInfoMapURN, RI.richInfoAssocListURN] + <> " together exceed the size limit: max " + <> show richInfoLimit + <> " characters, but got " + <> show sze + ) + ) + { Scim.status = Scim.Status 413 + } pure richInfo -- | Given an 'externalId' and an 'IdP', construct a 'SAML.UserRef'. @@ -411,12 +412,12 @@ updateValidScimUser tokinfo uid newScimUser = do case newScimUser ^. ST.vsuName of Just nm | oldScimUser ^. ST.vsuName /= Just nm -> Brig.setBrigUserName uid nm _ -> pure () - when (oldScimUser ^. ST.vsuHandle /= newScimUser ^. ST.vsuHandle) - $ Brig.setBrigUserHandle uid - $ newScimUser ^. ST.vsuHandle - when (oldScimUser ^. ST.vsuRichInfo /= newScimUser ^. ST.vsuRichInfo) - $ Brig.setBrigUserRichInfo uid - $ newScimUser ^. ST.vsuRichInfo + when (oldScimUser ^. ST.vsuHandle /= newScimUser ^. ST.vsuHandle) $ + Brig.setBrigUserHandle uid $ + newScimUser ^. ST.vsuHandle + when (oldScimUser ^. ST.vsuRichInfo /= newScimUser ^. ST.vsuRichInfo) $ + Brig.setBrigUserRichInfo uid $ + newScimUser ^. ST.vsuRichInfo lift $ Brig.getStatus uid >>= \old -> do @@ -447,9 +448,9 @@ toScimStoredUser' :: Scim.User ST.SparTag -> Scim.StoredUser ST.SparTag toScimStoredUser' createdAt lastChangedAt baseuri uid usr = - Scim.WithMeta meta - $ Scim.WithId uid - $ usr {Scim.User.schemas = ST.userSchemas} + Scim.WithMeta meta $ + Scim.WithId uid $ + usr {Scim.User.schemas = ST.userSchemas} where mkLocation :: String -> URI mkLocation pathSuffix = convURI $ baseuri SAML.=/ cs pathSuffix @@ -504,11 +505,10 @@ deleteScimUser ScimTokenInfo {stiTeam} uid = do -- FUTUREWORK: currently it's impossible to delete the last available team owner via SCIM -- (because that owner won't be managed by SCIM in the first place), but if it ever becomes -- possible, we should do a check here and prohibit it. - unless (userTeam brigUser == Just stiTeam) - $ + unless (userTeam brigUser == Just stiTeam) $ -- users from other teams get you a 404. - throwError - $ Scim.notFound "user" (idToText uid) + throwError $ + Scim.notFound "user" (idToText uid) ssoId <- maybe (logThenServerError $ "no userSSOId for user " <> cs (idToText uid)) @@ -574,9 +574,10 @@ assertHandleUnused :: Handle -> UserId -> Scim.ScimHandler Spar () assertHandleUnused = assertHandleUnused' "userName is already taken" assertHandleUnused' :: Text -> Handle -> UserId -> Scim.ScimHandler Spar () -assertHandleUnused' msg hndl uid = lift (Brig.checkHandleAvailable hndl uid) >>= \case - True -> pure () - False -> throwError Scim.conflict {Scim.detail = Just msg} +assertHandleUnused' msg hndl uid = + lift (Brig.checkHandleAvailable hndl uid) >>= \case + True -> pure () + False -> throwError Scim.conflict {Scim.detail = Just msg} assertHandleNotUsedElsewhere :: Handle -> UserId -> Scim.ScimHandler Spar () assertHandleNotUsedElsewhere hndl uid = do diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index defbcf575cf..50fe0812db0 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -237,8 +237,8 @@ specFinalizeLogin = do . expect2xx ) liftIO $ threadDelay 100000 -- make sure deletion is done. if we don't want to take - -- the time, we should find another way to robustly - -- confirm that deletion has compelted in the background. + -- the time, we should find another way to robustly + -- confirm that deletion has compelted in the background. -- second login do @@ -1012,10 +1012,10 @@ specDeleteCornerCases = describe "delete corner cases" $ do samlUserShouldSatisfy uref isJust deleteViaBrig uid samlUserShouldSatisfy uref isJust -- brig doesn't talk to spar right now when users - -- are deleted there. we need to work around this - -- fact for now. (if the test fails here, this may - -- mean that you fixed the behavior and can - -- change this to 'isNothing'.) + -- are deleted there. we need to work around this + -- fact for now. (if the test fails here, this may + -- mean that you fixed the behavior and can + -- change this to 'isNothing'.) (Just _) <- createViaSaml idp privcreds uref samlUserShouldSatisfy uref isJust where diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 3370899fcd0..027b9dc795e 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -34,12 +34,11 @@ import Control.Lens import Control.Monad.Catch (MonadCatch) import Control.Retry (exponentialBackoff, limitRetries, recovering) import qualified Data.Aeson as Aeson -import Data.Aeson.Lens (_String, key) +import Data.Aeson.Lens (key, _String) import Data.Aeson.QQ (aesonQQ) import Data.Aeson.Types (fromJSON, toJSON) import Data.ByteString.Conversion -import Data.Handle (Handle (Handle)) -import Data.Handle (fromHandle) +import Data.Handle (Handle (Handle), fromHandle) import Data.Id (TeamId, UserId, randomId) import Data.Ix (inRange) import Data.String.Conversions (cs) @@ -50,8 +49,8 @@ import qualified SAML2.WebSSO.Types as SAML import qualified Spar.Data as Data import qualified Spar.Intra.Brig as Intra import Spar.Scim -import qualified Spar.Types import Spar.Types (IdP) +import qualified Spar.Types import qualified Text.XML.DSig as SAML import Util import qualified Web.Scim.Class.User as Scim.UserC @@ -129,7 +128,7 @@ specSuspend = do void $ aFewTimes (runSpar $ Intra.getStatus uid) (== Active) it "PUT will change state from active to inactive and back" $ do - void $ activeInactiveAndBack $ \tok uid user active -> + void . activeInactiveAndBack $ \tok uid user active -> updateUser tok uid user {Scim.User.active = Just active} it "PATCH will change state from active to inactive and back" $ do @@ -138,7 +137,7 @@ specSuspend = do PatchOp.Replace (Just (PatchOp.NormalPath (Filter.topLevelAttrPath name))) (Just (toJSON value)) - void $ activeInactiveAndBack $ \tok uid _user active -> + void . activeInactiveAndBack $ \tok uid _user active -> patchUser tok uid $ PatchOp.PatchOp [replaceAttrib "active" active] -- Consider the following series of events: @@ -416,10 +415,10 @@ testScimCreateVsUserRef = do samlUserShouldSatisfy uref isJust deleteViaBrig uid samlUserShouldSatisfy uref isJust -- brig doesn't talk to spar right now when users - -- are deleted there. we need to work around this - -- fact for now. (if the test fails here, this may - -- mean that you fixed the behavior and can - -- change this to 'isNothing'.) + -- are deleted there. we need to work around this + -- fact for now. (if the test fails here, this may + -- mean that you fixed the behavior and can + -- change this to 'isNothing'.) tok <- registerScimToken teamid (Just (idp ^. SAML.idpId)) storedusr :: Scim.UserC.StoredUser SparTag <- do @@ -844,11 +843,12 @@ testUpdateSameHandle = do let userid = scimUserId storedUser -- Overwrite the user with another randomly-generated user who has the same name and -- handle - user' <- randomScimUser <&> \u -> - u - { Scim.User.userName = Scim.User.userName user, - Scim.User.displayName = Scim.User.displayName user - } + user' <- + randomScimUser <&> \u -> + u + { Scim.User.userName = Scim.User.userName user, + Scim.User.displayName = Scim.User.displayName user + } updatedUser <- updateUser tok userid user' -- Get the updated user and check that it matches the user returned by 'updateUser' storedUser' <- getUser tok userid @@ -1223,10 +1223,11 @@ specEmailValidation = do act <- getActivationCode brig (Left email) case act of Nothing -> pure () -- missing activation key/code; this happens if the feature is - -- disabled (second test case below) - Just kc -> activate brig kc !!! do - const 200 === statusCode - const (Just False) === fmap Activation.activatedFirst . responseJsonMaybe + -- disabled (second test case below) + Just kc -> + activate brig kc !!! do + const 200 === statusCode + const (Just False) === fmap Activation.activatedFirst . responseJsonMaybe -- -- copied from brig integration tests. getActivationCode :: diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 64996367c7a..462f1d935b7 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -123,7 +123,7 @@ module Util.Core where import Bilge hiding (getCookie) -- we use Web.Cookie instead of the http-client type -import Bilge.Assert ((!!!), ( randomRIO (0 :: Int, 13371137) -- | Generate a 'SAML.UserRef' subject. nextSubject :: (HasCallStack, MonadIO m) => m NameID nextSubject = liftIO $ do - unameId <- randomRIO (0, 1 :: Int) >>= \case - 0 -> either (error . show) id . SAML.mkUNameIDEmail . Brig.fromEmail <$> randomEmail - 1 -> SAML.mkUNameIDUnspecified . UUID.toText <$> UUID.nextRandom - _ -> error "nextSubject: impossible" + unameId <- + randomRIO (0, 1 :: Int) >>= \case + 0 -> either (error . show) id . SAML.mkUNameIDEmail . Brig.fromEmail <$> randomEmail + 1 -> SAML.mkUNameIDUnspecified . UUID.toText <$> UUID.nextRandom + _ -> error "nextSubject: impossible" either (error . show) pure $ SAML.mkNameID unameId Nothing Nothing Nothing nextUserRef :: MonadIO m => m SAML.UserRef @@ -776,9 +777,9 @@ getCookie proxy rsp = do hasPersistentCookieHeader :: ResponseLBS -> Either String () hasPersistentCookieHeader rsp = do cky <- getCookie (Proxy @"zuid") rsp - when (isNothing . Web.setCookieExpires $ fromSimpleSetCookie cky) - $ Left - $ "expiration date should NOT empty: " <> show cky + when (isNothing . Web.setCookieExpires $ fromSimpleSetCookie cky) $ + Left $ + "expiration date should NOT empty: " <> show cky -- | A bind cookie is always sent, but if we do not want to send one, it looks like this: -- "wire.com=; Path=/sso/finalize-login; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=-1; Secure" @@ -835,10 +836,11 @@ negotiateAuthnRequest :: (HasCallStack, MonadIO m, MonadReader TestEnv m) => IdP -> m SAML.AuthnRequest -negotiateAuthnRequest idp = negotiateAuthnRequest' DoInitiateLogin idp id >>= \case - (req, cky) -> case maybe (Left "missing") isDeleteBindCookie cky of - Right () -> pure req - Left msg -> error $ "unexpected bind cookie: " <> show (cky, msg) +negotiateAuthnRequest idp = + negotiateAuthnRequest' DoInitiateLogin idp id >>= \case + (req, cky) -> case maybe (Left "missing") isDeleteBindCookie cky of + Right () -> pure req + Left msg -> error $ "unexpected bind cookie: " <> show (cky, msg) doInitiatePath :: DoInitiate -> [ST] doInitiatePath DoInitiateLogin = ["sso", "initiate-login"] diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index 7891181b40b..274d9d6202c 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -78,9 +78,10 @@ registerIdPAndScimTokenWithMeta = do registerScimToken :: HasCallStack => TeamId -> Maybe IdPId -> TestSpar ScimToken registerScimToken teamid midpid = do env <- ask - tok <- ScimToken <$> do - code <- liftIO UUID.nextRandom - pure $ "scim-test-token/" <> "team=" <> idToText teamid <> "/code=" <> UUID.toText code + tok <- + ScimToken <$> do + code <- liftIO UUID.nextRandom + pure $ "scim-test-token/" <> "team=" <> idToText teamid <> "/code=" <> UUID.toText code scimTokenId <- randomId now <- liftIO getCurrentTime runClient (env ^. teCql) $ @@ -121,17 +122,18 @@ randomScimUserWithSubjectAndRichInfo richInfo = do emails <- getRandomR (0, 3) >>= \n -> replicateM n randomScimEmail phones <- getRandomR (0, 3) >>= \n -> replicateM n randomScimPhone -- Related, but non-trivial to re-use here: 'nextSubject' - (externalId, subj) <- getRandomR (0, 1 :: Int) <&> \case - 0 -> - ( "scimuser_extid_" <> suffix <> "@example.com", - either (error . show) id $ - SAML.mkUNameIDEmail ("scimuser_extid_" <> suffix <> "@example.com") - ) - 1 -> - ( "scimuser_extid_" <> suffix, - SAML.mkUNameIDUnspecified ("scimuser_extid_" <> suffix) - ) - _ -> error "randomScimUserWithSubject: impossible" + (externalId, subj) <- + getRandomR (0, 1 :: Int) <&> \case + 0 -> + ( "scimuser_extid_" <> suffix <> "@example.com", + either (error . show) id $ + SAML.mkUNameIDEmail ("scimuser_extid_" <> suffix <> "@example.com") + ) + 1 -> + ( "scimuser_extid_" <> suffix, + SAML.mkUNameIDUnspecified ("scimuser_extid_" <> suffix) + ) + _ -> error "randomScimUserWithSubject: impossible" pure ( (Scim.User.empty userSchemas ("scimuser_" <> suffix) (ScimUserExtra richInfo)) { Scim.User.displayName = Just ("Scim User #" <> suffix), @@ -159,7 +161,7 @@ randomScimEmail :: MonadRandom m => m Email.Email randomScimEmail = do let typ :: Maybe Text = Nothing primary :: Maybe Bool = Nothing -- TODO: where should we catch users with more than one - -- primary email? + -- primary email? value :: Email.EmailAddress2 <- do localpart <- cs <$> replicateM 15 (getRandomR ('a', 'z')) domainpart <- (<> ".com") . cs <$> replicateM 15 (getRandomR ('a', 'z')) diff --git a/services/spar/test-integration/Util/Types.hs b/services/spar/test-integration/Util/Types.hs index 66ba4e80da7..de0b9c50e55 100644 --- a/services/spar/test-integration/Util/Types.hs +++ b/services/spar/test-integration/Util/Types.hs @@ -104,6 +104,6 @@ _unitTestTestErrorLabel :: IO () _unitTestTestErrorLabel = do let val :: Either String TestErrorLabel val = Aeson.eitherDecode "{\"code\":404,\"message\":\"Not found.\",\"label\":\"not-found\"}" - unless (val == Right "not-found") - $ throwIO . ErrorCall . show - $ val + unless (val == Right "not-found") $ + throwIO . ErrorCall . show $ + val diff --git a/services/spar/test/Test/Spar/APISpec.hs b/services/spar/test/Test/Spar/APISpec.hs index ea483b4aa81..321bb4dd760 100644 --- a/services/spar/test/Test/Spar/APISpec.hs +++ b/services/spar/test/Test/Spar/APISpec.hs @@ -45,12 +45,12 @@ spec = do let withoutRaw (IdPMetadataValue _ x) = x (withoutRaw <$> (Aeson.eitherDecode . Aeson.encode) val) `shouldBe` Right (withoutRaw val) describe "SsoSettings JSON instance" $ do - it "always has and requires the field default_sso_code" - $ property - $ \(ssoSettings :: SsoSettings) -> do - let object = Aeson.toJSON ssoSettings - let objectWithoutKey = Lens.over Aeson._Object (HM.delete "default_sso_code") $ object - (HM.lookup "default_sso_code" =<< Lens.preview Aeson._Object object) - `shouldSatisfy` isJust - Aeson.parseMaybe (Aeson.parseJSON @SsoSettings) objectWithoutKey - `shouldSatisfy` isNothing + it "always has and requires the field default_sso_code" $ + property $ + \(ssoSettings :: SsoSettings) -> do + let object = Aeson.toJSON ssoSettings + let objectWithoutKey = Lens.over Aeson._Object (HM.delete "default_sso_code") $ object + (HM.lookup "default_sso_code" =<< Lens.preview Aeson._Object object) + `shouldSatisfy` isJust + Aeson.parseMaybe (Aeson.parseJSON @SsoSettings) objectWithoutKey + `shouldSatisfy` isNothing diff --git a/stack.yaml b/stack.yaml index 6c6808e7dbf..56e2c61f5e7 100644 --- a/stack.yaml +++ b/stack.yaml @@ -177,7 +177,7 @@ extra-deps: # Development tools ############################################################ -- ormolu-0.0.5.0 -- ghc-lib-parser-8.10.1.20200412@sha256:b0517bb150a02957d7180f131f5b94abd2a7f58a7d1532a012e71618282339c2,8751 # for ormolu-0.0.5.0 +- ormolu-0.1.2.0 +- ghc-lib-parser-8.10.1.20200412@sha256:b0517bb150a02957d7180f131f5b94abd2a7f58a7d1532a012e71618282339c2,8751 # for ormolu-0.1.2.0 - headroom-0.2.1.0 diff --git a/stack.yaml.lock b/stack.yaml.lock index 17bbcfc454a..1d2d2035253 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -506,12 +506,12 @@ packages: original: hackage: polysemy-1.3.0.0 - completed: - hackage: ormolu-0.0.5.0@sha256:e5f49c51c6ebd8b3cd16113e585312de7315c1e1561fbb599988cebc61c14f4e,7956 + hackage: ormolu-0.1.2.0@sha256:24e6512750576978b6f045c1e53a7aad28ab61960f738a3c74fb0bc2beaf4030,6237 pantry-tree: - size: 66187 - sha256: fd591a96bb129610f89d23d2986b1b11dad8c1c41e23ea1c6f03340b7265b617 + size: 71915 + sha256: 5a857d9bf0e9579ee4daacfb63b4665cdf9e0a0de31d8e0715a27836007e9c42 original: - hackage: ormolu-0.0.5.0 + hackage: ormolu-0.1.2.0 - completed: hackage: ghc-lib-parser-8.10.1.20200412@sha256:b0517bb150a02957d7180f131f5b94abd2a7f58a7d1532a012e71618282339c2,8751 pantry-tree: diff --git a/tools/api-simulations/loadtest/src/Main.hs b/tools/api-simulations/loadtest/src/Main.hs index 9352e225e0f..bca0c7eb2ae 100644 --- a/tools/api-simulations/loadtest/src/Main.hs +++ b/tools/api-simulations/loadtest/src/Main.hs @@ -92,14 +92,16 @@ ltsSettingsParser = do conversationRamp <- optional $ asum - [ fmap RampStep $ option auto $ - long "ramp-step" - <> metavar "INT" - <> help "delay in microseconds between conversations start", - fmap RampTotal $ option auto $ - long "ramp-total" - <> metavar "INT" - <> help "time in microseconds until full load" + [ fmap RampStep $ + option auto $ + long "ramp-step" + <> metavar "INT" + <> help "delay in microseconds between conversations start", + fmap RampTotal $ + option auto $ + long "ramp-total" + <> metavar "INT" + <> help "time in microseconds until full load" ] conversationsTotal <- option auto $ diff --git a/tools/api-simulations/loadtest/src/Network/Wire/Simulations/LoadTest.hs b/tools/api-simulations/loadtest/src/Network/Wire/Simulations/LoadTest.hs index cbc62c74fb6..f8bbad1cac3 100644 --- a/tools/api-simulations/loadtest/src/Network/Wire/Simulations/LoadTest.hs +++ b/tools/api-simulations/loadtest/src/Network/Wire/Simulations/LoadTest.hs @@ -113,7 +113,7 @@ runConv s g = do Clients.addMembers (botClientSessions client) conv (map botId bots) let removeClients (b, st) = mapM_ (removeBotClient b) (botClient st : botOtherClients st) - void $ flip mapConcurrently (zip bots states) $ \(b, st) -> + void . flip mapConcurrently (zip bots states) $ \(b, st) -> runBotSession b $ do log Info $ msg $ val "Starting bot" runBot s st `Ex.onException` removeClients (b, st) diff --git a/tools/api-simulations/smoketest/src/Network/Wire/Simulations/SmokeTest.hs b/tools/api-simulations/smoketest/src/Network/Wire/Simulations/SmokeTest.hs index fe0fa2730c1..720f65b75cd 100644 --- a/tools/api-simulations/smoketest/src/Network/Wire/Simulations/SmokeTest.hs +++ b/tools/api-simulations/smoketest/src/Network/Wire/Simulations/SmokeTest.hs @@ -128,15 +128,17 @@ mainBotNet n = do let carlWithTablet = (carl, carlTablet) let people :: [(Bot, ConvId, BotClient)] -- everyone except for Ally people = - (bill, a2b, billPC) : (carl, a2c, carlTablet) - : zip3 goons a2goons goonClients + (bill, a2b, billPC) : + (carl, a2c, carlTablet) : + zip3 goons a2goons goonClients info $ msg (val "OTR 1-1 greetings") -- Ally greets everyone in 1-1 - runBotSession ally $ for_ people $ \(user, conv, _client) -> do - botInitSession (botId user) - Clients.addMembers (botClientSessions allyPhone) conv [botId user] - let message = "Hey " <> unTag (botTag user) <> ", Everything secure?" - postOtrTextMsg allyPhone conv message >>= assertNoClientMismatch + runBotSession ally $ + for_ people $ \(user, conv, _client) -> do + botInitSession (botId user) + Clients.addMembers (botClientSessions allyPhone) conv [botId user] + let message = "Hey " <> unTag (botTag user) <> ", Everything secure?" + postOtrTextMsg allyPhone conv message >>= assertNoClientMismatch -- Everyone answers for_ people $ \(user, conv, client) -> runBotSession user $ do pkm <- awaitOtrMsg conv allyWithPhone (user, client) @@ -148,14 +150,15 @@ mainBotNet n = do Clients.addMembers (botClientSessions client) conv [botId ally] postOtrTextMsg client conv "Thanks Ally, All good." >>= assertNoClientMismatch -- Ally confirms the answers - runBotSession ally $ for_ people $ \(user, conv, client) -> do - message <- awaitOtrMsg conv (user, client) allyWithPhone - plain <- decryptTextMsg allyPhone message - assertEqual - plain - "Thanks Ally, All good." - ("Ally (from " <> unTag (botTag user) <> "): Plaintext /= CipherText") - postOtrTextMsg allyPhone conv "Glad to hear that." >>= assertNoClientMismatch + runBotSession ally $ + for_ people $ \(user, conv, client) -> do + message <- awaitOtrMsg conv (user, client) allyWithPhone + plain <- decryptTextMsg allyPhone message + assertEqual + plain + "Thanks Ally, All good." + ("Ally (from " <> unTag (botTag user) <> "): Plaintext /= CipherText") + postOtrTextMsg allyPhone conv "Glad to hear that." >>= assertNoClientMismatch -- Everyone checks Ally's response for_ people $ \(user, conv, client) -> runBotSession user $ do message <- awaitOtrMsg conv allyWithPhone (user, client) diff --git a/tools/bonanza/main/Kibanana.hs b/tools/bonanza/main/Kibanana.hs index de5abe0700c..d9954f5c35f 100644 --- a/tools/bonanza/main/Kibanana.hs +++ b/tools/bonanza/main/Kibanana.hs @@ -100,23 +100,24 @@ optInfo = data Signal = Stop | Go main :: IO () -main = execParser optInfo >>= \Opts {..} -> do - mgr <- newManager tlsManagerSettings - req <- baseReq url - buffer <- newTVarIO Seq.empty - signal <- newTVarIO Go - -- Start consumers - cs <- - replicateM concurrency - $ async - $ consume buffer req signal mgr maxBulkSize - -- Setup producer pipeline - runConduit $ - CB.sourceHandle stdin - .| breakByte 0 - .| CL.mapM_ (produce buffer maxBufferSize) - -- Graceful stop - drain buffer >> atomically (writeTVar signal Stop) >> mapM_ wait cs +main = + execParser optInfo >>= \Opts {..} -> do + mgr <- newManager tlsManagerSettings + req <- baseReq url + buffer <- newTVarIO Seq.empty + signal <- newTVarIO Go + -- Start consumers + cs <- + replicateM concurrency $ + async $ + consume buffer req signal mgr maxBulkSize + -- Setup producer pipeline + runConduit $ + CB.sourceHandle stdin + .| breakByte 0 + .| CL.mapM_ (produce buffer maxBufferSize) + -- Graceful stop + drain buffer >> atomically (writeTVar signal Stop) >> mapM_ wait cs where baseReq url = (\req -> req {path = "/_bulk", method = "POST"}) @@ -127,14 +128,15 @@ main = execParser optInfo >>= \Opts {..} -> do then retry else writeTVar b (xs |> x) consume b r s m i = do - chunk <- atomically $ - readTVar s >>= \case - Stop -> return Seq.empty - Go -> do - (now, later) <- Seq.splitAt i <$> readTVar b - if Seq.null now - then retry - else writeTVar b later >> return now + chunk <- + atomically $ + readTVar s >>= \case + Stop -> return Seq.empty + Go -> do + (now, later) <- Seq.splitAt i <$> readTVar b + if Seq.null now + then retry + else writeTVar b later >> return now unless (Seq.null chunk) $ do let body = requestBodySourceChunked (mapM_ yield chunk) let req = r {requestBody = body} diff --git a/tools/bonanza/src/Bonanza/Anon.hs b/tools/bonanza/src/Bonanza/Anon.hs index 4d11a720775..b82c3419619 100644 --- a/tools/bonanza/src/Bonanza/Anon.hs +++ b/tools/bonanza/src/Bonanza/Anon.hs @@ -21,7 +21,7 @@ module Bonanza.Anon where import Bonanza.Types -import Control.Lens ((%~), _Wrapped', over) +import Control.Lens (over, (%~), _Wrapped') import Data.HashMap.Strict (filterWithKey) import Imports diff --git a/tools/bonanza/src/Bonanza/App.hs b/tools/bonanza/src/Bonanza/App.hs index b2b2d8832a7..19368d9c011 100644 --- a/tools/bonanza/src/Bonanza/App.hs +++ b/tools/bonanza/src/Bonanza/App.hs @@ -33,7 +33,7 @@ import qualified Bonanza.Streaming.Snappy as Snappy import qualified Data.Aeson as Aeson import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BL -import Data.Conduit ((.|), ConduitM, runConduit) +import Data.Conduit (ConduitM, runConduit, (.|)) import Data.Conduit.Binary (sinkHandle, sourceHandle) import qualified Data.Conduit.List as Conduit import qualified Data.Conduit.Zlib as Conduit @@ -184,48 +184,49 @@ optInfo = ) runBonanza :: IO () -runBonanza = execParser optInfo >>= \(Opts CommonOpts {..} cmd) -> do - started <- getCurrentTime - (bytes_in, bytes_out, events_in) <- - (,,) - <$> newIORef 0 - <*> newIORef 0 - <*> newIORef 0 - geoDB <- mkGeo geodat - runConduit $ - sourceHandle stdin - .| runDecompress decomp - .| Conduit.mapM - ( \bs -> - modifyIORef' bytes_in (+ fromIntegral (BS.length bs)) - *> pure bs - ) - .| readWith parser - .| Conduit.mapM - ( \evt -> - modifyIORef' events_in (+ 1) - *> pure evt - ) - .| runGeo geo geoDB - .| runAnonymise anon - .| runCmd cmd - .| runCompress comp - .| Conduit.mapM - ( \bs -> - modifyIORef' bytes_out (+ fromIntegral (BS.length bs)) - *> pure bs - ) - .| sinkHandle stdout - completed <- getCurrentTime - stats <- - Stats - <$> readIORef bytes_in - <*> readIORef bytes_out - <*> (picosecondsToDiffTime <$> getCPUTime) - <*> pure (completed `diffUTCTime` started) - <*> readIORef events_in - unless quiet $ do - dumpStderr stats +runBonanza = + execParser optInfo >>= \(Opts CommonOpts {..} cmd) -> do + started <- getCurrentTime + (bytes_in, bytes_out, events_in) <- + (,,) + <$> newIORef 0 + <*> newIORef 0 + <*> newIORef 0 + geoDB <- mkGeo geodat + runConduit $ + sourceHandle stdin + .| runDecompress decomp + .| Conduit.mapM + ( \bs -> + modifyIORef' bytes_in (+ fromIntegral (BS.length bs)) + *> pure bs + ) + .| readWith parser + .| Conduit.mapM + ( \evt -> + modifyIORef' events_in (+ 1) + *> pure evt + ) + .| runGeo geo geoDB + .| runAnonymise anon + .| runCmd cmd + .| runCompress comp + .| Conduit.mapM + ( \bs -> + modifyIORef' bytes_out (+ fromIntegral (BS.length bs)) + *> pure bs + ) + .| sinkHandle stdout + completed <- getCurrentTime + stats <- + Stats + <$> readIORef bytes_in + <*> readIORef bytes_out + <*> (picosecondsToDiffTime <$> getCPUTime) + <*> pure (completed `diffUTCTime` started) + <*> readIORef events_in + unless quiet $ do + dumpStderr stats where runGeo [] _ = Conduit.map id runGeo tags db = diff --git a/tools/bonanza/src/Bonanza/Parser/CommonLog.hs b/tools/bonanza/src/Bonanza/Parser/CommonLog.hs index 288f50a876e..4a4f1a8c8af 100644 --- a/tools/bonanza/src/Bonanza/Parser/CommonLog.hs +++ b/tools/bonanza/src/Bonanza/Parser/CommonLog.hs @@ -85,9 +85,9 @@ instance ToLogEvent CommonLogRecord where commonLogFields :: [Text] commonLogFields = - "remote_addr" - : "remote_user" - : map fst fieldParsers + "remote_addr" : + "remote_user" : + map fst fieldParsers fieldParsers :: [(Text, Parser CommonLogField)] fieldParsers = @@ -112,9 +112,9 @@ commonLogRecord moreFieldParsers = do (_, CEmpty) -> Nothing (k, CField v) -> Just (k, v) ) - $ ("remote_addr", raddr) - : ("remote_user", ruser) - : flds, + $ ("remote_addr", raddr) : + ("remote_user", ruser) : + flds, cRequest = req } where diff --git a/tools/bonanza/src/Bonanza/Parser/Tinylog.hs b/tools/bonanza/src/Bonanza/Parser/Tinylog.hs index 907e904123c..fce8c348f57 100644 --- a/tools/bonanza/src/Bonanza/Parser/Tinylog.hs +++ b/tools/bonanza/src/Bonanza/Parser/Tinylog.hs @@ -53,8 +53,8 @@ instance ToLogEvent TinyLogRecord where where tgs = Tags . fromList . map (second String) $ - ("level", T.singleton tLevel) - : tFields + ("level", T.singleton tLevel) : + tFields ++ maybeToList ((,) "time" <$> tDate) tinyLogRecord :: Parser TinyLogRecord diff --git a/tools/bonanza/test/unit/Test/Bonanza/Arbitrary.hs b/tools/bonanza/test/unit/Test/Bonanza/Arbitrary.hs index 9b7dc38f89c..dccf2a3c018 100644 --- a/tools/bonanza/test/unit/Test/Bonanza/Arbitrary.hs +++ b/tools/bonanza/test/unit/Test/Bonanza/Arbitrary.hs @@ -281,8 +281,8 @@ instance Arbitrary (ParseInput TinyLogRecord) where encodeUtf8 . mconcat $ [ maybe "" (\d -> decodeUtf8 $ df d <> ", ") date, T.intercalate ", " $ - T.singleton level - : map (\(k, v) -> alnum k <> "=" <> fieldValue v) fields + T.singleton level : + map (\(k, v) -> alnum k <> "=" <> fieldValue v) fields ++ [message] ] return $ ParseInput (rec, inp) @@ -351,9 +351,9 @@ instance Arbitrary (ParseInput (NginzLogRecord)) where { cTime = date, cFields = mapMaybe (\(k, v) -> (,) k <$> fromField v) $ - ("remote_addr", raddr) - : ("remote_user", ruser) - : fields, + ("remote_addr", raddr) : + ("remote_user", ruser) : + fields, cRequest = req } inp = diff --git a/tools/bonanza/test/unit/Test/Bonanza/Streaming.hs b/tools/bonanza/test/unit/Test/Bonanza/Streaming.hs index 76005abc601..4c17d7f8ca1 100644 --- a/tools/bonanza/test/unit/Test/Bonanza/Streaming.hs +++ b/tools/bonanza/test/unit/Test/Bonanza/Streaming.hs @@ -31,7 +31,7 @@ import Bonanza.Types import Data.Attoparsec.ByteString.Char8 import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as BL -import Data.Conduit ((.|), runConduit) +import Data.Conduit (runConduit, (.|)) import qualified Data.Conduit.Binary as Conduit import qualified Data.Conduit.List as Conduit import Imports @@ -110,12 +110,12 @@ run_prop :: [ParseInput a] -> Property run_prop p i = - ioProperty - $ runConduit - $ Conduit.sourceLbs inp - .| P.stream (P.MkParser p) - .| Conduit.consume - >>= pure . (=== out) . map secs + ioProperty $ + runConduit $ + Conduit.sourceLbs inp + .| P.stream (P.MkParser p) + .| Conduit.consume + >>= pure . (=== out) . map secs where inp = BL.fromStrict . B.intercalate "\n" $ map (snd . parseInput) i out = map (secs . toLogEvent . fst . parseInput) i diff --git a/tools/db/auto-whitelist/src/Work.hs b/tools/db/auto-whitelist/src/Work.hs index 103b3dc9499..b8ff4c84c50 100644 --- a/tools/db/auto-whitelist/src/Work.hs +++ b/tools/db/auto-whitelist/src/Work.hs @@ -72,7 +72,7 @@ whitelistService l (pid, sid, tid) = do . Log.field "provider" (show pid) . Log.field "service" (show sid) . Log.field "team" (show tid) - retry x5 $ batch $ do + retry x5 . batch $ do setConsistency Quorum setType BatchLogged addPrepQuery insert1 (tid, pid, sid) diff --git a/tools/db/billing-team-member-backfill/src/Work.hs b/tools/db/billing-team-member-backfill/src/Work.hs index e19dff15935..318610f8bef 100644 --- a/tools/db/billing-team-member-backfill/src/Work.hs +++ b/tools/db/billing-team-member-backfill/src/Work.hs @@ -68,7 +68,7 @@ getTeamMembers = paginateC cql (paramsP Quorum () pageSize) x5 createBillingTeamMembers :: [(TeamId, UserId)] -> Client () createBillingTeamMembers pairs = - retry x5 $ batch $ do + retry x5 . batch $ do setType BatchLogged setConsistency Quorum mapM_ (addPrepQuery cql) pairs diff --git a/tools/db/find-undead/src/Work.hs b/tools/db/find-undead/src/Work.hs index 32757e22c12..f60e30f9719 100644 --- a/tools/db/find-undead/src/Work.hs +++ b/tools/db/find-undead/src/Work.hs @@ -25,8 +25,8 @@ import Brig.Types.Intra (AccountStatus (..)) import Cassandra import Cassandra.Util (Writetime, writeTimeToUTC) import Conduit -import Control.Lens (_1, _2, view) -import Data.Aeson ((.:), FromJSON) +import Control.Lens (view, _1, _2) +import Data.Aeson (FromJSON, (.:)) import qualified Data.Aeson as Aeson import qualified Data.Conduit.List as C import qualified Data.Set as Set @@ -41,16 +41,16 @@ runCommand :: Logger -> ClientState -> ES.BHEnv -> String -> String -> IO () runCommand l cas es indexStr mappingStr = do let index = ES.IndexName $ Text.pack indexStr mapping = ES.MappingName $ Text.pack mappingStr - runConduit - $ transPipe (ES.runBH es) - $ getScrolled index mapping - .| C.iterM (logProgress l) - .| C.mapM - ( \uuids -> do - fromCas <- runClient cas $ usersInCassandra uuids - pure (uuids, fromCas) - ) - .| C.mapM_ (logDifference l) + runConduit $ + transPipe (ES.runBH es) $ + getScrolled index mapping + .| C.iterM (logProgress l) + .| C.mapM + ( \uuids -> do + fromCas <- runClient cas $ usersInCassandra uuids + pure (uuids, fromCas) + ) + .| C.mapM_ (logDifference l) ---------------------------------------------------------------------------- -- Queries diff --git a/tools/db/service-backfill/src/Work.hs b/tools/db/service-backfill/src/Work.hs index 5b439dd1856..5b73e8d68c6 100644 --- a/tools/db/service-backfill/src/Work.hs +++ b/tools/db/service-backfill/src/Work.hs @@ -84,7 +84,7 @@ writeBots :: [(ProviderId, ServiceId, BotId, ConvId, Maybe TeamId)] -> Client () writeBots [] = pure () -writeBots xs = retry x5 $ batch $ do +writeBots xs = retry x5 . batch $ do setConsistency Quorum setType BatchLogged forM_ xs $ \(pid, sid, bid, cid, mbTid) -> do diff --git a/tools/ormolu.sh b/tools/ormolu.sh index f6c6e4a037f..b7bc03d99d5 100755 --- a/tools/ormolu.sh +++ b/tools/ormolu.sh @@ -74,7 +74,7 @@ FAILURES=0 for hsfile in $(git ls-files | grep '\.hsc\?$'); do FAILED=0 - ormolu --mode $ARG_ORMOLU_MODE --check-idempotency $LANGUAGE_EXTS "$hsfile" || FAILED=1 + ormolu --mode $ARG_ORMOLU_MODE --check-idempotence $LANGUAGE_EXTS "$hsfile" || FAILED=1 if [ "$FAILED" == "1" ]; then ((++FAILURES)) echo "$hsfile... *** FAILED" diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 5fb4d16c01f..4368489f85b 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -65,7 +65,7 @@ import qualified Stern.Intra as Intra import Stern.Options import qualified Stern.Swagger as Doc import Stern.Types -import System.Logger.Class hiding ((.=), Error, name, trace) +import System.Logger.Class hiding (Error, name, trace, (.=)) import Util.Options import qualified Wire.API.Team.Feature as Public import qualified Wire.API.Team.SearchVisibility as Public @@ -649,9 +649,9 @@ getTeamInvoice (tid ::: iid ::: _) = do getConsentLog :: Email -> Handler Response getConsentLog e = do acc <- (listToMaybe <$> Intra.getUserProfilesByIdentity (Left e)) - when (isJust acc) - $ throwE - $ Error status403 "user-exists" "Trying to access consent log of existing user!" + when (isJust acc) $ + throwE $ + Error status403 "user-exists" "Trying to access consent log of existing user!" consentLog <- Intra.getEmailConsentLog e marketo <- Intra.getMarketoResult e return . json $ diff --git a/tools/stern/src/Stern/App.hs b/tools/stern/src/Stern/App.hs index 0127a4c9863..fc5132def11 100644 --- a/tools/stern/src/Stern/App.hs +++ b/tools/stern/src/Stern/App.hs @@ -27,7 +27,7 @@ import qualified Bilge import qualified Bilge.IO as Bilge (withResponse) import Bilge.RPC (HasRequestId (..)) import Control.Error -import Control.Lens ((^.), makeLenses, set, view) +import Control.Lens (makeLenses, set, view, (^.)) import Control.Monad.Catch (MonadCatch, MonadThrow) import Control.Monad.IO.Class import Control.Monad.Reader.Class diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index f52cc7bd202..bb8a891512f 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -63,7 +63,7 @@ import Brig.Types import Brig.Types.Intra import Brig.Types.User.Auth import Control.Error -import Control.Lens ((^.), view) +import Control.Lens (view, (^.)) import Control.Monad.Reader import Data.Aeson hiding (Error) import Data.Aeson.Types (emptyArray) @@ -89,7 +89,7 @@ import Network.HTTP.Types.Status hiding (statusCode) import Network.Wai.Utilities (Error (..)) import Stern.App import Stern.Types -import System.Logger.Class hiding ((.=), Error, name) +import System.Logger.Class hiding (Error, name, (.=)) import qualified System.Logger.Class as Log import UnliftIO.Exception hiding (Handler) import qualified Wire.API.Team.Feature as Public @@ -100,33 +100,35 @@ putUser :: UserId -> UserUpdate -> Handler () putUser uid upd = do info $ userMsg uid . msg "Changing user state" b <- view brig - void $ catchRpcErrors $ - rpc' - "brig" - b - ( method PUT - . path "/self" - . header "Z-User" (toByteString' uid) - . header "Z-Connection" (toByteString' "") - . lbytes (encode upd) - . contentJson - . expect2xx - ) + void $ + catchRpcErrors $ + rpc' + "brig" + b + ( method PUT + . path "/self" + . header "Z-User" (toByteString' uid) + . header "Z-Connection" (toByteString' "") + . lbytes (encode upd) + . contentJson + . expect2xx + ) putUserStatus :: AccountStatus -> UserId -> Handler () putUserStatus status uid = do info $ userMsg uid . msg "Changing user status" b <- view brig - void $ catchRpcErrors $ - rpc' - "brig" - b - ( method PUT - . paths ["/i/users", toByteString' uid, "status"] - . lbytes (encode payload) - . contentJson - . expect2xx - ) + void $ + catchRpcErrors $ + rpc' + "brig" + b + ( method PUT + . paths ["/i/users", toByteString' uid, "status"] + . lbytes (encode payload) + . contentJson + . expect2xx + ) where payload = AccountStatusUpdate status @@ -335,10 +337,11 @@ getUserBindingTeam u = do . expect2xx ) teams <- parseResponse (Error status502 "bad-upstream") r - return $ listToMaybe - $ fmap (view teamId) - $ filter ((== Binding) . view teamBinding) - $ teams ^. teamListTeams + return $ + listToMaybe $ + fmap (view teamId) $ + filter ((== Binding) . view teamBinding) $ + teams ^. teamListTeams getInvoiceUrl :: TeamId -> InvoiceId -> Handler ByteString getInvoiceUrl tid iid = do diff --git a/tools/stern/src/Stern/Types.hs b/tools/stern/src/Stern/Types.hs index 3ea26464e57..6f2e43a2729 100644 --- a/tools/stern/src/Stern/Types.hs +++ b/tools/stern/src/Stern/Types.hs @@ -42,10 +42,10 @@ instance ToJSON TeamMemberInfo where toJSON (TeamMemberInfo m) = case teamMemberJson (const True) m of Object o -> - Object - $ M.insert "can_update_billing" (Bool (hasPermission m SetBilling)) - $ M.insert "can_view_billing" (Bool (hasPermission m GetBilling)) - $ o + Object $ + M.insert "can_update_billing" (Bool (hasPermission m SetBilling)) $ + M.insert "can_view_billing" (Bool (hasPermission m GetBilling)) $ + o other -> error $ "toJSON TeamMemberInfo: not an object: " <> show (encode other) From af38216d0ced94653dea4269668337af30f1954e Mon Sep 17 00:00:00 2001 From: fisx Date: Tue, 4 Aug 2020 11:07:55 +0200 Subject: [PATCH 07/23] work on rebase-formatter script (#1185) Co-authored-by: Matthias Heinzel --- tools/rebase-onto-formatter.sh | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tools/rebase-onto-formatter.sh b/tools/rebase-onto-formatter.sh index fe41734d712..6212658c39d 100755 --- a/tools/rebase-onto-formatter.sh +++ b/tools/rebase-onto-formatter.sh @@ -8,13 +8,16 @@ BASE_COMMIT=${1:-} TARGET_COMMIT=${2:-} FORMATTING_COMMAND='make formatf' USAGE=" -USAGE: $0 TARGET_COMMIT BASE_COMMIT +USAGE: $0 BASE_COMMIT TARGET_COMMIT + BASE_COMMIT: + A commit that contains the changes to formatting version and + config already from TARGET_COMMIT, but not the automatically + applied formatting changes. Must be the first commit on the + branch you are about to rebase (not the one returned by + git-merge-base). It will be removed from the resulting branch. TARGET_COMMIT: The commit introducing the formatting that you want to rebase onto. - BASE_COMMIT: - A commit very similar to TARGET_COMMIT, just that the automated formatting changes are not applied yet. - It has to include changes to formatting version and config already. Rebase a branch onto changes created by an automated formatter. The script will keep the (linear) history of the branch intact and make the commits appear @@ -29,7 +32,7 @@ INSTRUCTIONS: 4. Make sure the formatting tool is installed with the correct version and settings. $ stack install ormolu 5. Run this script. - $ $0 \$TARGET_COMMIT \$BASE_COMMIT + $ $0 \$BASE_COMMIT \$TARGET_COMMIT " @@ -96,9 +99,9 @@ echo "Running the script now. This might take a while..." set -x # edit every commit Ci, adding new commits representing f at Ci and it's inverse g -git rebase $BASE_COMMIT~1 --exec "$FORMATTING_COMMAND && git commit -am "format" && git revert HEAD --no-edit" +git rebase $BASE_COMMIT~1 --exec "$FORMATTING_COMMAND && git commit -am format && git revert HEAD --no-edit" -# drop last commit +# drop last commit (do not revert formatting at the end of the branch) git reset HEAD~1 --hard # now for every Ci, squash with the previous and next commit (i.e. g at C(i-1) and f at Ci) From b30a95c38022b76300d6ff9b4e8d667f21a8fbd0 Mon Sep 17 00:00:00 2001 From: Matthias Heinzel Date: Wed, 5 Aug 2020 10:05:18 +0200 Subject: [PATCH 08/23] Some formatting cleanup (#1186) With Ormolu 0.1.2, we can now stop separating definitions in a where-clause by empty comments. This commit fixes all the places I found using grep. It also fixes some comments that got broken when Ormolu was introduced. * remove now-unneeded comments * fix glitched comments --- libs/cassandra-util/src/Cassandra/Exec.hs | 3 +- libs/imports/src/Imports.hs | 29 +++++++------------ services/brig/src/Brig/API/User.hs | 4 +-- services/brig/src/Brig/Index/Eval.hs | 2 +- services/galley/src/Galley/API/Teams.hs | 11 ++++--- services/galley/src/Galley/Intra/Journal.hs | 4 +-- services/galley/src/Galley/Intra/Push.hs | 4 +-- services/galley/test/integration/API/Util.hs | 4 +-- services/gundeck/src/Gundeck/Push.hs | 8 ++--- .../Test/Spar/Scim/UserSpec.hs | 12 ++++---- 10 files changed, 35 insertions(+), 46 deletions(-) diff --git a/libs/cassandra-util/src/Cassandra/Exec.hs b/libs/cassandra-util/src/Cassandra/Exec.hs index b8f594e0842..bfe15af27f1 100644 --- a/libs/cassandra-util/src/Cassandra/Exec.hs +++ b/libs/cassandra-util/src/Cassandra/Exec.hs @@ -33,10 +33,9 @@ where import Cassandra.CQL (Consistency, R) import Control.Monad.Catch import Data.Conduit --- Things we just import and re-export. - -- We only use these locally. import Database.CQL.IO (RetrySettings, RunQ, defRetrySettings, eagerRetrySettings) +-- Things we just import and re-export. import Database.CQL.IO as C (BatchM, Client, ClientState, MonadClient, Page (..), PrepQuery, Row, addPrepQuery, addQuery, adjustConsistency, adjustResponseTimeout, adjustSendTimeout, batch, emptyPage, init, liftClient, localState, paginate, prepared, query, query1, queryString, retry, runClient, schema, setConsistency, setSerialConsistency, setType, shutdown, trans, write) import Database.CQL.Protocol (Error, QueryParams (QueryParams), Tuple) import Imports hiding (init) diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index 223089de63a..50be52f3589 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -117,25 +117,8 @@ module Imports ) where --- Explicitly saying what to import because some things from Prelude clash --- with e.g. UnliftIO modules - -import Control.Applicative hiding (empty, many, optional, some) -- common in --- some libs - --- conflicts with Options.Applicative.Option (should we care?) --- First and Last are going to be deprecated. Use Semigroup instead - --- 'insert' and 'delete' are --- common in database modules - --- Handle is hidden --- because it's common --- in Brig --- Permissions is common in Galley - --- Lazy and strict versions are the same - +-- common in some libs +import Control.Applicative hiding (empty, many, optional, some) import Control.DeepSeq (NFData (..), deepseq) import Control.Monad hiding (forM, forM_, mapM, mapM_, msum, sequence, sequence_) import Control.Monad.Extra (unlessM, whenM) @@ -165,11 +148,15 @@ import Data.Functor.Identity import Data.HashMap.Strict (HashMap) import Data.HashSet (HashSet) import Data.Int +-- 'insert' and 'delete' are common in database modules import Data.List hiding (delete, insert) +-- Lazy and strict versions are the same import Data.Map (Map) import Data.Maybe +-- First and Last are going to be deprecated. Use Semigroup instead import Data.Monoid hiding (First (..), Last (..)) import Data.Ord +-- conflicts with Options.Applicative.Option (should we care?) import Data.Semigroup hiding (Option, diff, option) import Data.Set (Set) import Data.String @@ -184,13 +171,17 @@ import GHC.Generics (Generic) import GHC.Stack (HasCallStack) import Text.Read (readEither, readMaybe) import UnliftIO.Concurrent +-- Permissions is common in Galley import UnliftIO.Directory hiding (Permissions) import UnliftIO.Environment import UnliftIO.Exception +-- Handle is hidden because it's common in Brig import UnliftIO.IO hiding (Handle, getMonotonicTime) import UnliftIO.IORef import UnliftIO.MVar import UnliftIO.STM +-- Explicitly saying what to import because some things from Prelude clash +-- with e.g. UnliftIO modules import Prelude ( Bounded (..), Double, diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 37ebb939743..3861d3dc63e 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -985,7 +985,7 @@ lookupProfilesOfLocalUsers self others = do where toMap :: [ConnectionStatus] -> Map UserId Relation toMap = Map.fromList . map (csFrom &&& csStatus) - -- + getSelfInfo :: AppIO (Maybe (TeamId, Team.TeamMember)) getSelfInfo = do -- FUTUREWORK: it is an internal error for the two lookups (for 'User' and 'TeamMember') @@ -995,7 +995,7 @@ lookupProfilesOfLocalUsers self others = do case userTeam =<< mUser of Nothing -> pure Nothing Just tid -> (tid,) <$$> Intra.getTeamMember self tid - -- + toProfile :: EmailVisibility' -> Map UserId Relation -> User -> UserProfile toProfile emailVisibility'' css u = let cs = Map.lookup (userId u) css diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index ba60819a926..a5249152a29 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -125,7 +125,7 @@ waitForTaskToComplete timeoutSeconds taskNodeId = do isTaskComplete :: Either ES.EsError (ES.TaskResponse a) -> m Bool isTaskComplete (Left e) = throwM $ ReindexFromAnotherIndexError $ "Error response while getting task: " <> show e isTaskComplete (Right taskRes) = pure $ ES.taskResponseCompleted taskRes - -- + errTaskGet :: MonadThrow m => ES.EsError -> m x errTaskGet e = throwM $ ReindexFromAnotherIndexError $ "Error response while getting task: " <> show e diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index dfe04392cd2..0f73416b321 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -512,21 +512,20 @@ updateTeamMember zusr zcon tid targetMember = do updateJournal team updatedMembers updatePeers targetId targetPermissions updatedMembers where - -- canDowngradeOwner = canDeleteMember - -- + downgradesOwner :: TeamMember -> Permissions -> Bool downgradesOwner previousMember targetPermissions = permissionsRole (previousMember ^. permissions) == Just RoleOwner && permissionsRole targetPermissions /= Just RoleOwner - -- + updateJournal :: Team -> TeamMemberList -> Galley () updateJournal team mems = do when (team ^. teamBinding == Binding) $ do (TeamSize size) <- BrigTeam.getSize tid billingUserIds <- Journal.getBillingUserIds tid $ Just mems Journal.teamUpdate tid size billingUserIds - -- + updatePeers :: UserId -> Permissions -> TeamMemberList -> Galley () updatePeers targetId targetPermissions updatedMembers = do -- inform members of the team about the change @@ -798,14 +797,14 @@ getTeamNotificationsH (zusr ::: sinceRaw ::: size ::: _) = do where parseSince :: Galley (Maybe Public.NotificationId) parseSince = maybe (pure Nothing) (fmap Just . parseUUID) sinceRaw - -- + parseUUID :: ByteString -> Galley Public.NotificationId parseUUID raw = maybe (throwM invalidTeamNotificationId) (pure . Id) ((UUID.fromASCIIBytes >=> isV1UUID) raw) - -- + isV1UUID :: UUID.UUID -> Maybe UUID.UUID isV1UUID u = if UUID.version u == 1 then Just u else Nothing diff --git a/services/galley/src/Galley/Intra/Journal.hs b/services/galley/src/Galley/Intra/Journal.hs index 768d29faa5d..234db9ebbac 100644 --- a/services/galley/src/Galley/Intra/Journal.hs +++ b/services/galley/src/Galley/Intra/Journal.hs @@ -102,11 +102,11 @@ getBillingUserIds tid maybeMemberList = do where fetchFromDB :: Galley [UserId] fetchFromDB = Data.listBillingTeamMembers tid - -- + filterFromMembers :: TeamMemberList -> Galley [UserId] filterFromMembers list = pure $ map (view userId) $ filter (`hasPermission` SetBilling) (list ^. teamMembers) - -- + handleList :: Bool -> TeamMemberList -> Galley [UserId] handleList enableIndexedBillingTeamMembers list = case list ^. teamMemberListType of diff --git a/services/galley/src/Galley/Intra/Push.hs b/services/galley/src/Galley/Intra/Push.hs index 6dd52e483fe..27a4944fcc1 100644 --- a/services/galley/src/Galley/Intra/Push.hs +++ b/services/galley/src/Galley/Intra/Push.hs @@ -159,12 +159,12 @@ push ps = do where (localRecipients, remoteRecipients) = partitionEithers . fmap localOrRemoteRecipient . toList $ pushRecipients p - -- + localOrRemoteRecipient :: RecipientBy (MappedOrLocalId Id.U) -> Either (RecipientBy UserId) (RecipientBy (IdMapping Id.U)) localOrRemoteRecipient rcp = case _recipientUserId rcp of Local localId -> Left $ rcp {_recipientUserId = localId} Mapped idMapping -> Right $ rcp {_recipientUserId = idMapping} - -- + mkPushTo :: [RecipientBy a] -> PushTo b -> Maybe (PushTo a) mkPushTo recipients p = nonEmpty recipients <&> \nonEmptyRecipients -> diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 257151c0f87..9bfce86cd14 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -846,10 +846,10 @@ getTeamQueue zusr msince msize onlyLast = do error $ "expected time: Nothing; but found: " <> show (qnl ^. queuedTime) | otherwise = fmap (_2 %~ parseEvt) . mconcat . fmap parseEvts . view queuedNotifications $ qnl - -- + parseEvts :: QueuedNotification -> [(NotificationId, Object)] parseEvts qn = (qn ^. queuedNotificationId,) <$> (toList . toNonEmpty $ qn ^. queuedNotificationPayload) - -- + parseEvt :: Object -> UserId parseEvt o = case fromJSON (Object o) of (Error msg) -> error msg diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 380e37b0279..c1f07844c19 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -408,14 +408,14 @@ addToken uid cid newtok = mpaRunWithBudget 1 AddTokenNoBudget $ do then (Just a, old) else (x, a : old) | otherwise = (x, old) - -- + continue :: PushToken -> Maybe Address -> Gundeck (Either AddTokenResponse Address) continue t Nothing = create (0 :: Int) t continue t (Just a) = update (0 :: Int) t (a ^. addrEndpoint) - -- + create :: Int -> PushToken -> @@ -445,7 +445,7 @@ addToken uid cid newtok = mpaRunWithBudget 1 AddTokenNoBudget $ do Right arn -> do Data.insert uid trp app tok arn cid (t ^. tokenClient) return (Right (mkAddr t arn)) - -- + update :: Int -> PushToken -> @@ -481,7 +481,7 @@ addToken uid cid newtok = mpaRunWithBudget 1 AddTokenNoBudget $ do Aws.EndpointNotFound {} -> create (n + 1) t Aws.InvalidCustomData {} -> return (Left AddTokenMetadataTooLong) ex -> throwM ex - -- + mkAddr :: PushToken -> EndpointArn -> diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 027b9dc795e..3ac97e9a351 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -1189,7 +1189,7 @@ specEmailValidation = do let req = put $ galley . paths p . json (Feature.TeamFeatureStatus Feature.TeamFeatureEnabled) p = ["/i/teams", toByteString' tid, "features", "validate-saml-emails"] call req !!! const 204 === statusCode - -- + assertEmail :: HasCallStack => UserId -> Maybe Email -> TestSpar () assertEmail uid expectedEmail = do brig <- asks (^. teBrig) @@ -1197,10 +1197,10 @@ specEmailValidation = do call req !!! do const 200 === statusCode const expectedEmail === (userEmail <=< responseJsonMaybe) - -- + eventually :: HasCallStack => TestSpar a -> TestSpar a eventually = recovering (limitRetries 3 <> exponentialBackoff 100000) [] . const - -- + setup :: HasCallStack => Bool -> TestSpar (UserId, Email) setup enabled = do (tok, (_ownerid, teamid, idp)) <- registerIdPAndScimToken @@ -1212,7 +1212,7 @@ specEmailValidation = do brig <- asks (^. teBrig) call $ activateEmail brig email pure (uid, email) - -- + -- copied from brig integration tests. activateEmail :: HasCallStack => @@ -1228,7 +1228,7 @@ specEmailValidation = do activate brig kc !!! do const 200 === statusCode const (Just False) === fmap Activation.activatedFirst . responseJsonMaybe - -- + -- copied from brig integration tests. getActivationCode :: HasCallStack => @@ -1242,7 +1242,7 @@ specEmailValidation = do let akey = Activation.ActivationKey . Ascii.unsafeFromText <$> (lbs ^? key "key" . _String) let acode = Activation.ActivationCode . Ascii.unsafeFromText <$> (lbs ^? key "code" . _String) return $ (,) <$> akey <*> acode - -- + -- copied from brig integration tests. activate :: HasCallStack => From 88954d1d17be998e236955f51d1f51c11e53da09 Mon Sep 17 00:00:00 2001 From: Matthias Heinzel Date: Wed, 5 Aug 2020 18:08:58 +0200 Subject: [PATCH 09/23] Create wire-api-federation with definition of MemberJoin event (#1187) * create wire-api-federation with definition of MemberJoin event --- libs/wire-api-federation/LICENSE | 661 ++++++++++++++++++ libs/wire-api-federation/package.yaml | 40 ++ .../src/Wire/API/Federation/Event.hs | 101 +++ .../src/Wire/API/Federation/Util/Aeson.hs | 45 ++ libs/wire-api-federation/test/Spec.hs | 18 + .../test/Test/SerializationSpec.hs | 25 + .../wire-api-federation.cabal | 76 ++ .../src/Wire/API/Event/Conversation.hs | 3 - stack.yaml | 1 + 9 files changed, 967 insertions(+), 3 deletions(-) create mode 100644 libs/wire-api-federation/LICENSE create mode 100644 libs/wire-api-federation/package.yaml create mode 100644 libs/wire-api-federation/src/Wire/API/Federation/Event.hs create mode 100644 libs/wire-api-federation/src/Wire/API/Federation/Util/Aeson.hs create mode 100644 libs/wire-api-federation/test/Spec.hs create mode 100644 libs/wire-api-federation/test/Test/SerializationSpec.hs create mode 100644 libs/wire-api-federation/wire-api-federation.cabal diff --git a/libs/wire-api-federation/LICENSE b/libs/wire-api-federation/LICENSE new file mode 100644 index 00000000000..dba13ed2ddf --- /dev/null +++ b/libs/wire-api-federation/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/libs/wire-api-federation/package.yaml b/libs/wire-api-federation/package.yaml new file mode 100644 index 00000000000..ded3759a671 --- /dev/null +++ b/libs/wire-api-federation/package.yaml @@ -0,0 +1,40 @@ +defaults: + local: ../../package-defaults.yaml +name: wire-api-federation +version: '0.1.0' +synopsis: The Wire server-to-server API for federation +description: Servant endpoints and types for implementing a Wire backend that can federate with others +category: Web +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2020 Wire Swiss GmbH +license: AGPL-3 +dependencies: +- aeson >=1.4 +- base >=4.6 && <5.0 +- bytestring >=0.10 +- bytestring-conversion >=0.3 +- containers >=0.5 +- errors >=2.0 +- exceptions >=0.6 +- imports +- servant >=0.16 +- text >=0.11 +- time >=1.8 +- transformers >=0.3 +- types-common +- QuickCheck >=2.13 + +library: + source-dirs: src + +tests: + spec: + main: Spec.hs + source-dirs: + - test + ghc-options: -threaded -rtsopts -with-rtsopts=-N + dependencies: + - wire-api-federation + - hspec + - hspec-discover diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Event.hs b/libs/wire-api-federation/src/Wire/API/Federation/Event.hs new file mode 100644 index 00000000000..45c456bdb7b --- /dev/null +++ b/libs/wire-api-federation/src/Wire/API/Federation/Event.hs @@ -0,0 +1,101 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE StrictData #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.Federation.Event + ( AnyEvent (..), + ConversationEvent (..), + + -- * MemberJoin + MemberJoin (..), + SimpleMember (..), + ConversationRole (..), + ) +where + +import Data.Aeson (FromJSON, ToJSON) +import Data.Id +import Data.Qualified (Qualified) +import Data.Time +import Imports +import Test.QuickCheck (Arbitrary (arbitrary)) +import qualified Test.QuickCheck as QC +import Wire.API.Federation.Util.Aeson (CustomEncoded (CustomEncoded)) + +data AnyEvent + = EventMemberJoin (ConversationEvent MemberJoin) + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded AnyEvent) + +-- | Similar to 'Wire.API.Event.ConversationEvent', but all IDs are qualified to allow +-- this representation to be sent across backends. +-- +-- Also, instead of having a sum type in 'eventData', it allows specifying which type +-- of event it is, e.g. @ConversationEvent MemberJoin@. +-- To represent possiblity of multiple different event types, use a sum type around it. +data ConversationEvent a = ConversationEvent + { eventConversation :: Qualified ConvId, + eventFrom :: Qualified UserId, + eventTime :: UTCTime, + eventData :: a + } + deriving stock (Eq, Show, Generic, Foldable, Functor, Traversable) + deriving (ToJSON, FromJSON) via (CustomEncoded (ConversationEvent a)) + +newtype MemberJoin = MemberJoin + { smUsers :: [SimpleMember] + } + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded MemberJoin) + +data SimpleMember = SimpleMember + { smId :: Qualified UserId, + smConversationRole :: ConversationRole + } + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded SimpleMember) + +data ConversationRole + = ConversationRoleAdmin + | ConversationRoleMember + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded ConversationRole) + +-- Arbitrary + +instance Arbitrary AnyEvent where + arbitrary = + QC.oneof + [ EventMemberJoin <$> arbitrary + ] + +instance Arbitrary a => Arbitrary (ConversationEvent a) where + arbitrary = ConversationEvent <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary + +instance Arbitrary MemberJoin where + arbitrary = MemberJoin <$> arbitrary + +instance Arbitrary SimpleMember where + arbitrary = SimpleMember <$> arbitrary <*> arbitrary + +instance Arbitrary ConversationRole where + arbitrary = QC.elements [ConversationRoleAdmin, ConversationRoleMember] diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Util/Aeson.hs b/libs/wire-api-federation/src/Wire/API/Federation/Util/Aeson.hs new file mode 100644 index 00000000000..c288f0100be --- /dev/null +++ b/libs/wire-api-federation/src/Wire/API/Federation/Util/Aeson.hs @@ -0,0 +1,45 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.Federation.Util.Aeson + ( customEncodingOptions, + CustomEncoded (..), + ) +where + +import Data.Aeson +import qualified Data.Char as Char +import GHC.Generics (Rep) +import Imports + +-- | Drops record field name prefixes (anything until the first upper-case char) +-- and turns the rest into snake_case. +-- +-- For example, it converts @_recordFieldLabel@ into @field_label@. +customEncodingOptions :: Options +customEncodingOptions = + defaultOptions + { fieldLabelModifier = camelTo2 '_' . dropWhile (not . Char.isUpper) + } + +newtype CustomEncoded a = CustomEncoded {unCustomEncoded :: a} + +instance (Generic a, GToJSON Zero (Rep a)) => ToJSON (CustomEncoded a) where + toJSON = genericToJSON @a customEncodingOptions . unCustomEncoded + +instance (Generic a, GFromJSON Zero (Rep a)) => FromJSON (CustomEncoded a) where + parseJSON = fmap CustomEncoded . genericParseJSON @a customEncodingOptions diff --git a/libs/wire-api-federation/test/Spec.hs b/libs/wire-api-federation/test/Spec.hs new file mode 100644 index 00000000000..7b57431c0d0 --- /dev/null +++ b/libs/wire-api-federation/test/Spec.hs @@ -0,0 +1,18 @@ +{-# OPTIONS_GHC -F -pgmF hspec-discover #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . diff --git a/libs/wire-api-federation/test/Test/SerializationSpec.hs b/libs/wire-api-federation/test/Test/SerializationSpec.hs new file mode 100644 index 00000000000..3deccdf7b35 --- /dev/null +++ b/libs/wire-api-federation/test/Test/SerializationSpec.hs @@ -0,0 +1,25 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.SerializationSpec where + +import Imports +import Test.Hspec + +spec :: Spec +spec = do + pure () diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal new file mode 100644 index 00000000000..29c9aff6de3 --- /dev/null +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -0,0 +1,76 @@ +cabal-version: 1.12 + +-- This file has been generated from package.yaml by hpack version 0.33.0. +-- +-- see: https://github.com/sol/hpack +-- +-- hash: 365781b146aeb5cff30955591ab1c8141b368aaa3b9a25b566331dfa677431b2 + +name: wire-api-federation +version: 0.1.0 +synopsis: The Wire server-to-server API for federation +description: Servant endpoints and types for implementing a Wire backend that can federate with others +category: Web +author: Wire Swiss GmbH +maintainer: Wire Swiss GmbH +copyright: (c) 2020 Wire Swiss GmbH +license: AGPL-3 +license-file: LICENSE +build-type: Simple + +library + exposed-modules: + Wire.API.Federation.Event + Wire.API.Federation.Util.Aeson + other-modules: + Paths_wire_api_federation + hs-source-dirs: + src + default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DerivingStrategies DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings PackageImports PatternSynonyms PolyKinds QuasiQuotes RankNTypes ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeFamilyDependencies TypeOperators UndecidableInstances ViewPatterns + ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path + build-depends: + QuickCheck >=2.13 + , aeson >=1.4 + , base >=4.6 && <5.0 + , bytestring >=0.10 + , bytestring-conversion >=0.3 + , containers >=0.5 + , errors >=2.0 + , exceptions >=0.6 + , imports + , servant >=0.16 + , text >=0.11 + , time >=1.8 + , transformers >=0.3 + , types-common + default-language: Haskell2010 + +test-suite spec + type: exitcode-stdio-1.0 + main-is: Spec.hs + other-modules: + Test.SerializationSpec + Paths_wire_api_federation + hs-source-dirs: + test + default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DerivingStrategies DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings PackageImports PatternSynonyms PolyKinds QuasiQuotes RankNTypes ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeApplications TypeFamilies TypeFamilyDependencies TypeOperators UndecidableInstances ViewPatterns + ghc-options: -O2 -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wpartial-fields -fwarn-tabs -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N + build-depends: + QuickCheck >=2.13 + , aeson >=1.4 + , base >=4.6 && <5.0 + , bytestring >=0.10 + , bytestring-conversion >=0.3 + , containers >=0.5 + , errors >=2.0 + , exceptions >=0.6 + , hspec + , hspec-discover + , imports + , servant >=0.16 + , text >=0.11 + , time >=1.8 + , transformers >=0.3 + , types-common + , wire-api-federation + default-language: Haskell2010 diff --git a/libs/wire-api/src/Wire/API/Event/Conversation.hs b/libs/wire-api/src/Wire/API/Event/Conversation.hs index eb75defd444..491c640b001 100644 --- a/libs/wire-api/src/Wire/API/Event/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Event/Conversation.hs @@ -224,9 +224,6 @@ instance FromJSON EventType where parseJSON (String "conversation.otr-message-add") = return OtrMessageAdd parseJSON x = fail $ "No event-type: " <> show (encode x) --- FUTUREWORK(federation, #1213): --- A lot of information in the events can contain remote IDs, but the --- receiver might be on another backend, so mapped IDs don't work for them. data EventData = EdMembersJoin SimpleMembers | EdMembersLeave UserIdList diff --git a/stack.yaml b/stack.yaml index 56e2c61f5e7..d417313b894 100644 --- a/stack.yaml +++ b/stack.yaml @@ -24,6 +24,7 @@ packages: - libs/types-common-journal - libs/wai-utilities - libs/wire-api +- libs/wire-api-federation - libs/zauth - services/brig - services/cannon From 8eb4e0091ed0495dc3cc3faa09d9acd311ca3015 Mon Sep 17 00:00:00 2001 From: Matthias Heinzel Date: Fri, 7 Aug 2020 16:47:35 +0200 Subject: [PATCH 10/23] create first endpoint in Federation API (#1188) --- libs/wire-api-federation/package.yaml | 1 + .../src/Wire/API/Federation/API.hs | 30 +++++++++ .../Wire/API/Federation/API/Conversation.hs | 63 +++++++++++++++++++ .../API/Federation/APISpec.hs} | 10 ++- .../wire-api-federation.cabal | 7 ++- 5 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 libs/wire-api-federation/src/Wire/API/Federation/API.hs create mode 100644 libs/wire-api-federation/src/Wire/API/Federation/API/Conversation.hs rename libs/wire-api-federation/test/Test/{SerializationSpec.hs => Wire/API/Federation/APISpec.hs} (72%) diff --git a/libs/wire-api-federation/package.yaml b/libs/wire-api-federation/package.yaml index ded3759a671..38e1c253a9d 100644 --- a/libs/wire-api-federation/package.yaml +++ b/libs/wire-api-federation/package.yaml @@ -38,3 +38,4 @@ tests: - wire-api-federation - hspec - hspec-discover + - metrics-wai diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API.hs b/libs/wire-api-federation/src/Wire/API/Federation/API.hs new file mode 100644 index 00000000000..7bb48ee3da2 --- /dev/null +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -0,0 +1,30 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.Federation.API where + +import GHC.Generics (Generic) +import Servant.API.Generic (AsApi, ToServant, (:-)) +import qualified Wire.API.Federation.API.Conversation as Conversation (Api) + +type PlainApi = ToServant Api AsApi + +-- FUTUREWORK: Add Swagger docs +data Api routes = Api + { conversation :: routes :- ToServant Conversation.Api AsApi + } + deriving stock (Generic) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Conversation.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Conversation.hs new file mode 100644 index 00000000000..6640cff5917 --- /dev/null +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Conversation.hs @@ -0,0 +1,63 @@ +{-# LANGUAGE DerivingVia #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2020 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.Federation.API.Conversation where + +import Data.Aeson (FromJSON, ToJSON) +import Data.Id (ConvId, UserId) +import Data.Qualified (Qualified) +import Imports +import Servant.API (Capture, JSON, Post, ReqBody, (:>)) +import Servant.API.Generic ((:-)) +import Test.QuickCheck (Arbitrary (arbitrary)) +import qualified Test.QuickCheck as QC +import Wire.API.Federation.Event (ConversationEvent, MemberJoin) +import Wire.API.Federation.Util.Aeson (CustomEncoded (CustomEncoded)) + +data Api routes = Api + { joinConversationById :: + routes + :- "f" + :> "conversation" + :> Capture "cnv" (Qualified ConvId) + :> "join" + :> ReqBody '[JSON] JoinConversationByIdRequest + :> Post '[JSON] (ConversationUpdateResult MemberJoin) + } + deriving stock (Generic) + +data JoinConversationByIdRequest = JoinConversationByIdRequest + { joinUserId :: Qualified UserId + } + deriving stock (Eq, Show, Generic) + deriving (ToJSON, FromJSON) via (CustomEncoded JoinConversationByIdRequest) + +data ConversationUpdateResult a + = ConversationUpdated (ConversationEvent a) + | ConversationUnchanged + deriving stock (Eq, Show, Generic, Foldable, Functor, Traversable) + deriving (ToJSON, FromJSON) via (CustomEncoded (ConversationUpdateResult a)) + +-- Arbitrary + +instance Arbitrary JoinConversationByIdRequest where + arbitrary = JoinConversationByIdRequest <$> arbitrary + +instance Arbitrary a => Arbitrary (ConversationUpdateResult a) where + arbitrary = QC.oneof [pure ConversationUnchanged, ConversationUpdated <$> arbitrary] diff --git a/libs/wire-api-federation/test/Test/SerializationSpec.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/APISpec.hs similarity index 72% rename from libs/wire-api-federation/test/Test/SerializationSpec.hs rename to libs/wire-api-federation/test/Test/Wire/API/Federation/APISpec.hs index 3deccdf7b35..770447c8f11 100644 --- a/libs/wire-api-federation/test/Test/SerializationSpec.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/APISpec.hs @@ -15,11 +15,15 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Test.SerializationSpec where +module Test.Wire.API.Federation.APISpec where +import Data.Metrics.Servant (routesToPaths) +import Data.Metrics.Test (pathsConsistencyCheck) import Imports -import Test.Hspec +import Test.Hspec (Spec, it, shouldBe) +import Wire.API.Federation.API as API spec :: Spec spec = do - pure () + it "API consistency" $ do + pathsConsistencyCheck (routesToPaths @API.PlainApi) `shouldBe` mempty diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index 29c9aff6de3..7ad62767acf 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: 365781b146aeb5cff30955591ab1c8141b368aaa3b9a25b566331dfa677431b2 +-- hash: 942d9870717b6c174641f5f84d1344154f017118bd6870e939fba4eedc747d34 name: wire-api-federation version: 0.1.0 @@ -20,6 +20,8 @@ build-type: Simple library exposed-modules: + Wire.API.Federation.API + Wire.API.Federation.API.Conversation Wire.API.Federation.Event Wire.API.Federation.Util.Aeson other-modules: @@ -49,7 +51,7 @@ test-suite spec type: exitcode-stdio-1.0 main-is: Spec.hs other-modules: - Test.SerializationSpec + Test.Wire.API.Federation.APISpec Paths_wire_api_federation hs-source-dirs: test @@ -67,6 +69,7 @@ test-suite spec , hspec , hspec-discover , imports + , metrics-wai , servant >=0.16 , text >=0.11 , time >=1.8 From dcabede61b9c3dada40931972ba1a315ea8d1464 Mon Sep 17 00:00:00 2001 From: Matthias Heinzel Date: Thu, 13 Aug 2020 14:26:23 +0200 Subject: [PATCH 11/23] fix rebase script (#1191) The previous approach kept the commit message, but overwrote the author of the commits being rebased. This is fixed by using git commit --reuse-message. --- tools/rebase-onto-formatter.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/rebase-onto-formatter.sh b/tools/rebase-onto-formatter.sh index 6212658c39d..1196fad8a35 100755 --- a/tools/rebase-onto-formatter.sh +++ b/tools/rebase-onto-formatter.sh @@ -104,11 +104,13 @@ git rebase $BASE_COMMIT~1 --exec "$FORMATTING_COMMAND && git commit -am format & # drop last commit (do not revert formatting at the end of the branch) git reset HEAD~1 --hard -# now for every Ci, squash with the previous and next commit (i.e. g at C(i-1) and f at Ci) -# - in sequence editor, squash lines 3, 6, 9, ... and fixup lines 4, 7, 10, ... -# - in commit message editor, drop first 9 lines (removing the commit message of the revert commit) -GIT_SEQUENCE_EDITOR='sed -i -e "3~3s/pick/squash/" -e "4~3s/pick/fixup/"' \ - GIT_EDITOR='sed -i "1,9d"' \ +# now for every Ci, squash with the previous and next commit (i.e. g at C(i-1) and f at Ci). +# However, we want to use Ci's commit message and author. +# To do this, we run the following command after each group of these 3 commits: +# Ci=$(git rev-parse HEAD~1); git reset --soft HEAD~3; git commit --reuse-message $Ci +# We do an interactive rebase, but instead of editing the commit sequence manually, +# we use sed for that, inserting an `exec` command after every 3 commits. +GIT_SEQUENCE_EDITOR='sed -i -e "4~3s/^\(pick \S* format\)$/\1\nexec Ci=\$(git rev-parse HEAD~1); git reset --soft HEAD~3; git commit --reuse-message \$Ci/"' \ git rebase --interactive $BASE_COMMIT # rebase onto TARGET_COMMIT. From 811416340326c3656eaca6147d63cc94501582b0 Mon Sep 17 00:00:00 2001 From: Matthias Heinzel Date: Tue, 18 Aug 2020 12:53:05 +0200 Subject: [PATCH 12/23] Add endpoint to Federator API, cleanup (#1189) * Add endpoint to Federator API, cleanup * rename API to Api * remove search endpoint It being here doesn't give us a lot, still needs some answers and tweaks. We will need a while to get there anyways. --- services/federator/federator.cabal | 14 ++-- services/federator/package.yaml | 4 +- services/federator/src/Federator/API.hs | 65 +++++++--------- services/federator/src/Federator/App.hs | 75 +++++++++++++++---- .../src/Federator/{Util.hs => Impl.hs} | 34 ++++----- services/federator/src/Federator/Run.hs | 65 +--------------- 6 files changed, 116 insertions(+), 141 deletions(-) rename services/federator/src/Federator/{Util.hs => Impl.hs} (57%) diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index 0b26b11d71a..308f6966089 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -1,10 +1,10 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.2. +-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack -- --- hash: 05e45399becaef2bbecfa958ec4d5633900e2e1d8e1c1a2e7ecd2afbdbc2404f +-- hash: 72597caf8878551b0609bfdc790852575f38db429ea5c83935a399c676ee3c2c name: federator version: 1.0.0 @@ -20,10 +20,10 @@ library exposed-modules: Federator.API Federator.App + Federator.Impl Federator.Options Federator.Run Federator.Types - Federator.Util other-modules: Paths_federator hs-source-dirs: @@ -35,14 +35,12 @@ library , aeson , base , bilge - , brig-types , bytestring-conversion , data-default , email-validate , errors , exceptions , extended - , galley-types , imports , lens , metrics-core @@ -60,6 +58,8 @@ library , wai , wai-utilities , warp + , wire-api + , wire-api-federation default-language: Haskell2010 executable federator @@ -75,7 +75,6 @@ executable federator , aeson , base , bilge - , brig-types , bytestring-conversion , data-default , email-validate @@ -83,7 +82,6 @@ executable federator , exceptions , extended , federator - , galley-types , imports , lens , metrics-core @@ -101,4 +99,6 @@ executable federator , wai , wai-utilities , warp + , wire-api + , wire-api-federation default-language: Haskell2010 diff --git a/services/federator/package.yaml b/services/federator/package.yaml index 892fe7194c8..6035999778c 100644 --- a/services/federator/package.yaml +++ b/services/federator/package.yaml @@ -12,14 +12,12 @@ dependencies: - aeson - base - bilge -- brig-types - bytestring-conversion - data-default - email-validate - errors - exceptions - extended -- galley-types - imports - lens - metrics-core @@ -38,6 +36,8 @@ dependencies: - wai - wai-utilities - warp +- wire-api +- wire-api-federation library: source-dirs: src executables: diff --git a/services/federator/src/Federator/API.hs b/services/federator/src/Federator/API.hs index 1ea46ddd105..ba5026aee23 100644 --- a/services/federator/src/Federator/API.hs +++ b/services/federator/src/Federator/API.hs @@ -17,53 +17,42 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Federator.API where - -import Brig.Types.Client.Prekey -import Brig.Types.Test.Arbitrary () -import Data.Aeson.TH (deriveJSON) -import Data.Handle (Handle (..)) -import Data.Id (UserId) -import Data.Qualified -import Federator.Util +module Federator.API + ( Api (..), + module Fed, + ) +where + +import Data.Id (ConvId, UserId) +import Data.Qualified (Qualified) import Imports import Servant.API import Servant.API.Generic -import Test.QuickCheck +import Wire.API.Federation.API.Conversation as Fed hiding (Api) +import Wire.API.Federation.Event as Fed +import Wire.API.User.Client.Prekey (PrekeyBundle) -data API route = API - { _gapiSearch :: - route - :- "i" - :> "search" - -- QUESTION: what exactly should the query be? text + domain? - :> QueryParam' [Required, Strict] "q" (Qualified Handle) - :> Get '[JSON] FUser, - _gapiPrekeys :: +data Api route = Api + { _gapiPrekeys :: route :- "i" :> "users" - :> Capture "fqu" (Qualified UserId) + :> Capture "id" (Qualified UserId) :> "prekeys" - :> Get '[JSON] PrekeyBundle + -- FUTUREWORK(federation): + -- this should return a version of PrekeyBundle with qualified UserId, + -- defined in wire-api-federation + :> Get '[JSON] PrekeyBundle, + _gapiJoinConversationById :: + route + :- "i" + :> "conversations" + :> Capture "cnv" (Qualified ConvId) + :> "join" + :> ReqBody '[JSON] Fed.JoinConversationByIdRequest + :> Post '[JSON] (Fed.ConversationUpdateResult Fed.MemberJoin) } deriving (Generic) --- curl http://localhost:8097/i/search?q=wef@a.com; curl http://localhost:8097/i/users/`uuid`@example.com/prekeys - ---------------------------------------------------------------------- --- TODO: add roundtrip tests for *HttpApiData, *JSON, ... --- --- TODO: the client ids in the 'PrekeyBundle' aren't really needed here. do we want to make a --- new type for that, then? - -data FUser = FUser - { _fuGlobalHandle :: !(Qualified Handle), - _fuFQU :: !(Qualified UserId) - } - deriving (Eq, Show, Generic) - -deriveJSON (wireJsonOptions "_fu") ''FUser - -instance Arbitrary FUser where - arbitrary = FUser <$> arbitrary <*> arbitrary +-- FUTUREWORK: add roundtrip tests for *HttpApiData, *JSON, ... diff --git a/services/federator/src/Federator/App.hs b/services/federator/src/Federator/App.hs index 2c375802d91..f6dcd763ab2 100644 --- a/services/federator/src/Federator/App.hs +++ b/services/federator/src/Federator/App.hs @@ -1,5 +1,5 @@ +{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE RecordWildCards #-} -- This file is part of the Wire Server implementation. -- @@ -19,19 +19,66 @@ -- with this program. If not, see . module Federator.App - ( app, + ( AppT, + AppIO, + runAppT, + runAppResourceT, ) where -import Data.Proxy -import qualified Federator.API as API -import Federator.Types -import Network.Wai -import Servant.API.Generic -import Servant.Mock -import Servant.Server - -app :: Env -> Application -app _ = serve api (mock api Proxy) - where - api = Proxy @(ToServantApi API.API) +import Bilge (RequestId (unRequestId)) +import Bilge.RPC (HasRequestId (..)) +import Control.Error (ExceptT) +import Control.Lens (view) +import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) +import Control.Monad.Trans.Resource (MonadUnliftIO, ResourceT, runResourceT, transResourceT) +import Federator.Types (Env, applog, requestId) +import Imports +import Servant.API.Generic () +import Servant.Server () +import System.Logger.Class as LC +import qualified System.Logger.Extended as Log + +-- FUTUREWORK: this code re-occurs in every service. introduce 'MkAppT' in types-common that +-- takes 'Env' as one more argument. +newtype AppT m a = AppT + { unAppT :: ReaderT Env m a + } + deriving newtype + ( Functor, + Applicative, + Monad, + MonadIO, + MonadThrow, + MonadCatch, + MonadMask, + MonadReader Env + ) + +type AppIO = AppT IO + +instance MonadIO m => LC.MonadLogger (AppT m) where + log l m = do + g <- view applog + r <- view requestId + Log.log g l $ field "request" (unRequestId r) ~~ m + +instance MonadIO m => LC.MonadLogger (ExceptT err (AppT m)) where + log l m = lift (LC.log l m) + +instance Monad m => HasRequestId (AppT m) where + getRequestId = view requestId + +instance MonadUnliftIO m => MonadUnliftIO (AppT m) where + withRunInIO inner = + AppT . ReaderT $ \r -> + withRunInIO $ \runner -> + inner (runner . flip runReaderT r . unAppT) + +runAppT :: Env -> AppT m a -> m a +runAppT e (AppT ma) = runReaderT ma e + +runAppResourceT :: ResourceT AppIO a -> AppIO a +runAppResourceT ma = do + e <- ask + liftIO . runResourceT $ transResourceT (runAppT e) ma diff --git a/services/federator/src/Federator/Util.hs b/services/federator/src/Federator/Impl.hs similarity index 57% rename from services/federator/src/Federator/Util.hs rename to services/federator/src/Federator/Impl.hs index c947f0e0e46..75a01c5c800 100644 --- a/services/federator/src/Federator/Util.hs +++ b/services/federator/src/Federator/Impl.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE RecordWildCards #-} + -- This file is part of the Wire Server implementation. -- -- Copyright (C) 2020 Wire Swiss GmbH @@ -15,25 +18,20 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Federator.Util - ( wireJsonOptions, +module Federator.Impl + ( app, ) where -import Data.Aeson as Aeson -import Imports - -dropPrefix :: String -> String -> Maybe String -dropPrefix pfx str = - if length pfx > length str - then Nothing - else case splitAt (length pfx) str of - (pfx', sfx) -> - if pfx' /= pfx - then Nothing - else Just sfx +import Data.Proxy +import qualified Federator.API as API +import Federator.Types +import Network.Wai +import Servant.API.Generic +import Servant.Mock +import Servant.Server --- | This is a partial function; totality of all calls must be verified by roundtrip tests on --- the aeson instances involved. -wireJsonOptions :: String -> Options -wireJsonOptions pfx = defaultOptions {fieldLabelModifier = fromJust . dropPrefix pfx . fmap toLower} +app :: Env -> Application +app _ = serve api (mock api Proxy) + where + api = Proxy @(ToServantApi API.Api) diff --git a/services/federator/src/Federator/Run.hs b/services/federator/src/Federator/Run.hs index 1744745e1f6..023bbd0956a 100644 --- a/services/federator/src/Federator/Run.hs +++ b/services/federator/src/Federator/Run.hs @@ -25,32 +25,20 @@ module Federator.Run -- * App Environment newEnv, closeEnv, - - -- * App Monad - AppT, - AppIO, - runAppT, - runAppResourceT, ) where -import Bilge (RequestId (unRequestId)) -import Bilge.RPC (HasRequestId (..)) -import Control.Error -import Control.Lens (view, (^.)) -import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow) -import Control.Monad.Trans.Resource +import Control.Lens ((^.)) import Data.Default (def) import qualified Data.Metrics.Middleware as Metrics import Data.Text (unpack) -import qualified Federator.App as App +import qualified Federator.Impl as Impl import Federator.Options as Opt import Federator.Types import Imports import Network.Wai (Application) import qualified Network.Wai.Handler.Warp as Warp import Network.Wai.Utilities.Server as Server -import System.Logger.Class as LC import qualified System.Logger.Extended as Log import Util.Options @@ -66,7 +54,7 @@ run opts = do mkApp :: Opts -> IO (Application, Env) mkApp opts = do env <- newEnv opts - pure (App.app env, env) + pure (Impl.app env, env) ------------------------------------------------------------------------------- -- Environment @@ -82,50 +70,3 @@ closeEnv :: Env -> IO () closeEnv e = do Log.flush $ e ^. applog Log.close $ e ^. applog - -------------------------------------------------------------------------------- --- App Monad - --- FUTUREWORK: this code re-occurs in every service. introduce 'MkAppT' in types-common that --- takes 'Env' as one more argument. -newtype AppT m a = AppT - { unAppT :: ReaderT Env m a - } - deriving - ( Functor, - Applicative, - Monad, - MonadIO, - MonadThrow, - MonadCatch, - MonadMask, - MonadReader Env - ) - -type AppIO = AppT IO - -instance MonadIO m => LC.MonadLogger (AppT m) where - log l m = do - g <- view applog - r <- view requestId - Log.log g l $ field "request" (unRequestId r) ~~ m - -instance MonadIO m => LC.MonadLogger (ExceptT err (AppT m)) where - log l m = lift (LC.log l m) - -instance Monad m => HasRequestId (AppT m) where - getRequestId = view requestId - -instance MonadUnliftIO m => MonadUnliftIO (AppT m) where - withRunInIO inner = - AppT . ReaderT $ \r -> - withRunInIO $ \runner -> - inner (runner . flip runReaderT r . unAppT) - -runAppT :: Env -> AppT m a -> m a -runAppT e (AppT ma) = runReaderT ma e - -runAppResourceT :: ResourceT AppIO a -> AppIO a -runAppResourceT ma = do - e <- ask - liftIO . runResourceT $ transResourceT (runAppT e) ma From 090f320796e2c19e23f39970f74bae694430c64d Mon Sep 17 00:00:00 2001 From: Franziskus Kiefer Date: Thu, 20 Aug 2020 14:42:18 +0200 Subject: [PATCH 13/23] Create SECURITY.md --- SECURITY.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..0c552947319 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1 @@ +Please check Wire's [global SECURITY.md](https://github.com/wireapp/wire/blob/master/SECURITY.md). From 2cd04b6f05dfd0e469f3b5fd9af7c223fd1d0386 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 25 Aug 2020 15:32:43 +0200 Subject: [PATCH 14/23] A Bounded instance for Range. --- libs/types-common/src/Data/Range.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index a9e444768a1..f372dbe0c0e 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -89,6 +89,10 @@ newtype Range (n :: Nat) (m :: Nat) a = Range } deriving (Eq, Ord, Show) +instance (Show a, Num a, Within a n m, KnownNat n, KnownNat m) => Bounded (Range n m a) where + minBound = unsafeRange $ (fromKnownNat (Proxy @n) :: a) + maxBound = unsafeRange $ (fromKnownNat (Proxy @m) :: a) + instance NFData (Range n m a) where rnf (Range a) = seq a () instance ToJSON a => ToJSON (Range n m a) where From 8040c966f69c4eca6d803eea8310e26ecf92f3a9 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 25 Aug 2020 15:37:03 +0200 Subject: [PATCH 15/23] Convenience constant: emptyNewUser. --- libs/wire-api/src/Wire/API/User.hs | 20 ++++++++++++++++++++ services/spar/src/Spar/Intra/Brig.hs | 15 +++------------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 09542bb085a..927251d8c89 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -36,6 +36,7 @@ module Wire.API.User -- * NewUser NewUserPublic (..), NewUser (..), + emptyNewUser, ExpiresIn, newUserInvitationCode, newUserTeam, @@ -514,6 +515,25 @@ data NewUser = NewUser } deriving stock (Eq, Show, Generic) +emptyNewUser :: Name -> NewUser +emptyNewUser name = + NewUser + { newUserDisplayName = name, + newUserUUID = Nothing, + newUserIdentity = Nothing, + newUserPict = Nothing, + newUserAssets = [], + newUserAccentId = Nothing, + newUserEmailCode = Nothing, + newUserPhoneCode = Nothing, + newUserOrigin = Nothing, + newUserLabel = Nothing, + newUserLocale = Nothing, + newUserPassword = Nothing, + newUserExpiresIn = Nothing, + newUserManagedBy = Nothing + } + -- | 1 second - 1 week type ExpiresIn = Range 1 604800 Integer diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index 1f31882c5d0..cb965e6cdba 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -74,6 +74,7 @@ import qualified Servant.Server as Servant import Spar.Error import Spar.Intra.Galley as Galley (MonadSparToGalley, assertIsTeamOwner, isEmailValidationEnabledTeam) import Web.Cookie +import Wire.API.User import Wire.API.User.RichInfo as RichInfo ---------------------------------------------------------------------- @@ -147,20 +148,10 @@ createBrigUser suid (Id buid) teamid mbName managedBy = do Nothing -> throwSpar err let newUser :: NewUser newUser = - NewUser - { newUserDisplayName = uname, - newUserUUID = Just buid, + (emptyNewUser uname) + { newUserUUID = Just buid, newUserIdentity = Just $ SSOIdentity (toUserSSOId suid) Nothing Nothing, - newUserPict = Nothing, - newUserAssets = [], - newUserAccentId = Nothing, - newUserEmailCode = Nothing, - newUserPhoneCode = Nothing, newUserOrigin = Just . NewUserOriginTeamUser . NewTeamMemberSSO $ teamid, - newUserLabel = Nothing, - newUserLocale = Nothing, - newUserPassword = Nothing, - newUserExpiresIn = Nothing, newUserManagedBy = Just managedBy } resp :: Response (Maybe LBS) <- From e059e69430774e08b875ba604dcb42decc0051fe Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 25 Aug 2020 16:12:45 +0200 Subject: [PATCH 16/23] Construct brig user name during scim user parsing. --- libs/wire-api/src/Wire/API/User/Profile.hs | 4 +++ services/spar/src/Spar/App.hs | 23 ++++++++-------- services/spar/src/Spar/Intra/Brig.hs | 29 +++++++++------------ services/spar/src/Spar/Scim/Types.hs | 2 +- services/spar/src/Spar/Scim/User.hs | 26 ++++++------------ services/spar/test-integration/Util/Scim.hs | 2 +- 6 files changed, 38 insertions(+), 48 deletions(-) diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index e6380d7cb14..944c0737f8c 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -21,6 +21,7 @@ module Wire.API.User.Profile ( Name (..), + mkName, ColourId (..), defaultAccentId, @@ -81,6 +82,9 @@ newtype Name = Name deriving newtype (ToJSON, FromByteString, ToByteString) deriving (Arbitrary) via (Ranged 1 128 Text) +mkName :: Text -> Either String Name +mkName txt = Name . fromRange <$> checkedEitherMsg @_ @1 @128 "Name" txt + modelUserDisplayName :: Doc.Model modelUserDisplayName = Doc.defineModel "UserDisplayName" $ do Doc.description "User name" diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index ebc6baeafa0..5dcfc3c01dd 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -36,7 +36,7 @@ module Spar.App where import Bilge -import Brig.Types (ManagedBy (..), Name) +import Brig.Types (ManagedBy (..)) import Cassandra import qualified Cassandra as Cas import Control.Exception (assert) @@ -188,24 +188,25 @@ getUser uref = do -- FUTUREWORK: once we support , brig will refuse to delete -- users that have an sso id, unless the request comes from spar. then we can make users -- undeletable in the team admin page, and ask admins to go talk to their IdP system. -createSamlUserWithId :: UserId -> SAML.UserRef -> Maybe Name -> ManagedBy -> Spar () -createSamlUserWithId buid suid mbName managedBy = do +createSamlUserWithId :: UserId -> SAML.UserRef -> ManagedBy -> Spar () +createSamlUserWithId buid suid managedBy = do teamid <- (^. idpExtraInfo . wiTeam) <$> getIdPConfigByIssuer (suid ^. uidTenant) - buid' <- Intra.createBrigUser suid buid teamid mbName managedBy + uname <- either (throwSpar . SparBadUserName . cs) pure $ Intra.mkUserName Nothing suid + buid' <- Intra.createBrigUser suid buid teamid uname managedBy assert (buid == buid') $ pure () insertUser suid buid -- | If the team has no scim token, call 'createSamlUser'. Otherwise, raise "invalid -- credentials". -autoprovisionSamlUser :: SAML.UserRef -> Maybe Name -> ManagedBy -> Spar UserId -autoprovisionSamlUser suid mbName managedBy = do +autoprovisionSamlUser :: SAML.UserRef -> ManagedBy -> Spar UserId +autoprovisionSamlUser suid managedBy = do buid <- Id <$> liftIO UUID.nextRandom - autoprovisionSamlUserWithId buid suid mbName managedBy + autoprovisionSamlUserWithId buid suid managedBy pure buid -- | Like 'autoprovisionSamlUser', but for an already existing 'UserId'. -autoprovisionSamlUserWithId :: UserId -> SAML.UserRef -> Maybe Name -> ManagedBy -> Spar () -autoprovisionSamlUserWithId buid suid mbName managedBy = do +autoprovisionSamlUserWithId :: UserId -> SAML.UserRef -> ManagedBy -> Spar () +autoprovisionSamlUserWithId buid suid managedBy = do idp <- getIdPConfigByIssuer (suid ^. uidTenant) unless (isNothing $ idp ^. idpExtraInfo . wiReplacedBy) $ do throwSpar $ SparCannotCreateUsersOnReplacedIdP (cs . SAML.idPIdToST $ idp ^. idpId) @@ -213,7 +214,7 @@ autoprovisionSamlUserWithId buid suid mbName managedBy = do scimtoks <- wrapMonadClient $ Data.getScimTokens teamid if null scimtoks then do - createSamlUserWithId buid suid mbName managedBy + createSamlUserWithId buid suid managedBy validateEmailIfExists buid suid else throwError . SAML.Forbidden $ @@ -367,7 +368,7 @@ verdictHandlerResultCore bindCky = \case -- This is the first SSO authentication, so we auto-create a user. We know the user -- has not been created via SCIM because then we would've ended up in the -- "reauthentication" branch, so we pass 'ManagedByWire'. - (Nothing, Nothing, Nothing) -> autoprovisionSamlUser userref Nothing ManagedByWire + (Nothing, Nothing, Nothing) -> autoprovisionSamlUser userref ManagedByWire -- If the user is only found under an old (previous) issuer, move it here. (Nothing, Nothing, Just (oldUserRef, uid)) -> moveUserToNewIssuer oldUserRef userref uid >> pure uid -- SSO re-authentication (the most common case). diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index cb965e6cdba..ed87edcd94b 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -22,6 +22,7 @@ module Spar.Intra.Brig ( toUserSSOId, fromUserSSOId, toExternalId, + mkUserName, getBrigUser, getBrigUserTeam, getBrigUsers, @@ -65,7 +66,6 @@ import Data.Handle (Handle (Handle, fromHandle)) import Data.Id (Id (Id), TeamId, UserId) import Data.Ix import Data.Misc (PlainTextPassword) -import Data.Range import Data.String.Conversions import Imports import Network.HTTP.Types.Method @@ -97,6 +97,13 @@ toExternalId ssoid = do let subj = uref ^. SAML.uidSubject pure $ SAML.nameIDToST subj +-- | Take a maybe text, construct a 'Name' from what we have in a scim user. If the text +-- isn't present, use the saml subject (usually an email address). If both are 'Nothing', +-- fail. +mkUserName :: Maybe Text -> SAML.UserRef -> Either String Name +mkUserName (Just n) _ = mkName n +mkUserName Nothing uref = mkName (SAML.unsafeShowNameID $ uref ^. SAML.uidSubject) + parseResponse :: (FromJSON a, MonadError SparError m) => Response (Maybe LBS) -> m a parseResponse resp = do bdy <- maybe (throwSpar SparNoBodyInBrigResponse) pure $ responseBody resp @@ -121,31 +128,19 @@ class MonadError SparError m => MonadSparToBrig m where instance MonadSparToBrig m => MonadSparToBrig (ReaderT r m) where call = lift . call --- | Create a user on brig. User name is derived from 'SAML.UserRef'. +-- | Create a user on brig. createBrigUser :: (HasCallStack, MonadSparToBrig m) => -- | SSO identity SAML.UserRef -> UserId -> TeamId -> - -- | User name (if 'Nothing', the subject ID will be used) - Maybe Name -> + -- | User name + Name -> -- | Who should have control over the user ManagedBy -> m UserId -createBrigUser suid (Id buid) teamid mbName managedBy = do - uname :: Name <- case mbName of - Just n -> pure n - Nothing -> do - -- 1. use 'SAML.unsafeShowNameID' to get a 'Name'. rationale: it does not need to be - -- unique. - let subj = suid ^. SAML.uidSubject - subjtxt = SAML.unsafeShowNameID subj - muname = checked @ST @1 @128 subjtxt - err = SparBadUserName $ "must have >= 1, <= 128 chars: " <> cs subjtxt - case muname of - Just uname -> pure . Name . fromRange $ uname - Nothing -> throwSpar err +createBrigUser suid (Id buid) teamid uname managedBy = do let newUser :: NewUser newUser = (emptyNewUser uname) diff --git a/services/spar/src/Spar/Scim/Types.hs b/services/spar/src/Spar/Scim/Types.hs index a02fbace1c5..eb450b3c57f 100644 --- a/services/spar/src/Spar/Scim/Types.hs +++ b/services/spar/src/Spar/Scim/Types.hs @@ -202,7 +202,7 @@ instance Scim.Patchable ScimUserExtra where data ValidScimUser = ValidScimUser { _vsuUserRef :: SAML.UserRef, _vsuHandle :: Handle, - _vsuName :: Maybe BT.Name, -- TODO: remove the 'Maybe' here, and construct the name not in "Spar.Intra.Brig", but in 'validateScimUser'. + _vsuName :: BT.Name, _vsuRichInfo :: RI.RichInfo, _vsuActive :: Bool } diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index 2954147817f..ef603b556dc 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -54,7 +54,6 @@ import qualified Data.Aeson as Aeson import Data.Handle (Handle (Handle), parseHandle) import Data.Id (Id (Id), UserId, idToText) import Data.Json.Util (UTCTimeMillis, fromUTCTimeMillis, toUTCTimeMillis) -import Data.Range (checkedEitherMsg, fromRange) import Data.String.Conversions (cs) import qualified Data.Text as Text import qualified Data.UUID.V4 as UUID @@ -212,20 +211,13 @@ validateScimUser' idp richInfoLimit user = do handl <- validateHandle . Text.toLower . Scim.userName $ user -- FUTUREWORK: 'Scim.userName' should be case insensitive; then the toLower here would -- be a little less brittle. - mbName <- mapM validateName (Scim.displayName user) + uname <- do + let err = throwError . Scim.badRequest Scim.InvalidValue . Just . cs + either err pure $ Brig.mkUserName (Scim.displayName user) uref richInfo <- validateRichInfo (Scim.extra user ^. ST.sueRichInfo) let active = Scim.active user - pure $ ST.ValidScimUser uref handl mbName richInfo (fromMaybe True active) + pure $ ST.ValidScimUser uref handl uname richInfo (fromMaybe True active) where - -- Validate a name (@displayName@). It has to conform to standard Wire rules. - validateName :: Text -> m Name - validateName txt = case checkedEitherMsg @_ @1 @128 "displayName" txt of - Right rtxt -> pure $ Name (fromRange rtxt) - Left err -> - throwError $ - Scim.badRequest - Scim.InvalidValue - (Just ("displayName must be a valid Wire name, but: " <> Text.pack err)) -- Validate rich info (@richInfo@). It must not exceed the rich info limit. validateRichInfo :: RI.RichInfo -> m RI.RichInfo validateRichInfo richInfo = do @@ -409,9 +401,8 @@ updateValidScimUser tokinfo uid newScimUser = do -- the extra details not stored in the DB that we need here. lift $ do - case newScimUser ^. ST.vsuName of - Just nm | oldScimUser ^. ST.vsuName /= Just nm -> Brig.setBrigUserName uid nm - _ -> pure () + when (newScimUser ^. ST.vsuName /= oldScimUser ^. ST.vsuName) $ + Brig.setBrigUserName uid (newScimUser ^. ST.vsuName) when (oldScimUser ^. ST.vsuHandle /= newScimUser ^. ST.vsuHandle) $ Brig.setBrigUserHandle uid $ newScimUser ^. ST.vsuHandle @@ -646,7 +637,7 @@ synthesizeStoredUser' uid ssoid dname handle richInfo accStatus createdAt lastUp ST.ValidScimUser { ST._vsuUserRef = sso, ST._vsuHandle = handle, -- 'Maybe' there is one in @usr@, but we want to type checker to make sure this exists. - ST._vsuName = Just dname, + ST._vsuName = dname, ST._vsuRichInfo = richInfo, ST._vsuActive = ST.scimActiveFlagFromAccountStatus accStatus } @@ -656,12 +647,11 @@ synthesizeStoredUser' uid ssoid dname handle richInfo accStatus createdAt lastUp synthesizeScimUser :: ST.ValidScimUser -> Scim.User ST.SparTag synthesizeScimUser info = let Handle userName = info ^. ST.vsuHandle - mDisplayName = fromName <$> (info ^. ST.vsuName) toExternalId' :: SAML.UserRef -> Maybe Text toExternalId' = either (const Nothing) Just . Brig.toExternalId . Brig.toUserSSOId in (Scim.empty ST.userSchemas userName (ST.ScimUserExtra (info ^. ST.vsuRichInfo))) { Scim.externalId = toExternalId' $ info ^. ST.vsuUserRef, - Scim.displayName = mDisplayName, + Scim.displayName = Just $ fromName (info ^. ST.vsuName), Scim.active = Just $ info ^. ST.vsuActive } diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index 274d9d6202c..0d04142176d 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -548,7 +548,7 @@ class IsUser u where instance IsUser ValidScimUser where maybeUserId = Nothing maybeHandle = Just (Just . view vsuHandle) - maybeName = Just (view vsuName) + maybeName = Just (Just . view vsuName) maybeTenant = Just (Just . view (vsuUserRef . SAML.uidTenant)) maybeSubject = Just (Just . view (vsuUserRef . SAML.uidSubject)) maybeSubjectRaw = Just (SAML.shortShowNameID . view (vsuUserRef . SAML.uidSubject)) From cbecc84b38863d49f9dce9fbd9aa1c1b76aebb96 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 25 Aug 2020 20:37:22 +0200 Subject: [PATCH 17/23] FUTUREWORK. --- services/spar/test-integration/Util/Scim.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index 0d04142176d..da87c4243c2 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -530,6 +530,10 @@ scimUserId = Scim.id . Scim.thing -- -- Note: we don't compare rich info here, because 'User' doesn't contain it. However, we have -- separate tests for rich info that cover that. +-- +-- FUTUREWORK: tenant, subject, subjectraw are not scim concepts, we should use the +-- corresponding scim terminology for that. subjectraw is externalId; the other two don't +-- have exact correspondences. perhaps they can be removed? or changed to fit scim better? class IsUser u where maybeUserId :: Maybe (u -> UserId) maybeHandle :: Maybe (u -> Maybe Handle) From 546174d2f80f30cb22057b3149079ec44a4be1fa Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 25 Aug 2020 20:41:49 +0200 Subject: [PATCH 18/23] [off topic] Remove useless helper. --- services/galley/test/integration/API/Util.hs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 9bfce86cd14..5ecf9cb02d6 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -270,10 +270,6 @@ addTeamMemberInternal' tid mem = do let payload = json (newNewTeamMember mem) post (g . paths ["i", "teams", toByteString' tid, "members"] . payload) -stdInvitationRequest :: Email -> Name -> Maybe Locale -> Maybe Team.Role -> InvitationRequest -stdInvitationRequest e inviterName loc role = - InvitationRequest e inviterName loc role Nothing Nothing - addUserToTeam :: HasCallStack => UserId -> TeamId -> TestM TeamMember addUserToTeam = addUserToTeamWithRole Nothing @@ -297,7 +293,7 @@ addUserToTeamWithRole' role inviter tid = do brig <- view tsBrig inviteeEmail <- randomEmail let name = Name $ fromEmail inviteeEmail - let invite = stdInvitationRequest inviteeEmail name Nothing role + let invite = InvitationRequest inviteeEmail name Nothing role Nothing Nothing invResponse <- postInvitation tid inviter invite inv <- responseJsonError invResponse Just inviteeCode <- getInvitationCode tid (inInvitation inv) From 9c0b34ed0a9ccaf27cc6fc3ab0558455b9eb4f5f Mon Sep 17 00:00:00 2001 From: Tiago Manuel Ventura Loureiro Date: Fri, 28 Aug 2020 13:57:07 +0200 Subject: [PATCH 19/23] Fixed logic related to ephemeral users (#1197) --- docs/reference/user/registration.md | 7 +++---- libs/brig-types/src/Brig/Types/User.hs | 2 ++ libs/wire-api/src/Wire/API/User.hs | 14 ++++++++++++++ services/brig/src/Brig/API/User.hs | 13 ++----------- services/brig/src/Brig/Data/User.hs | 10 ++++------ .../brig/test/integration/API/User/Account.hs | 18 +++++++++--------- 6 files changed, 34 insertions(+), 30 deletions(-) diff --git a/docs/reference/user/registration.md b/docs/reference/user/registration.md index 16eeec5e005..331932e774e 100644 --- a/docs/reference/user/registration.md +++ b/docs/reference/user/registration.md @@ -180,14 +180,13 @@ We need an option to block 1, 2, 5 on-prem; 3, 4 should remain available (no blo * Allow team members to register (via email/phone or SSO) * Allow ephemeral users -During registration, we can take advantage of [NewUserOrigin](https://github.com/wireapp/wire-server/blob/a89b9cd818997e7837e5d0938ecfd90cf8dd9e52/libs/wire-api/src/Wire/API/User.hs#L625); we're particularly interested in `NewUserOriginTeamUser` --> only `NewTeamMember` or `NewTeamMemberSSO` should be accepted. In case this is a `Nothing`, we need to check if the user expires, i.e., `newUserExpiresIn` must be a `Just`. +During registration, we can take advantage of [NewUserOrigin](https://github.com/wireapp/wire-server/blob/a89b9cd818997e7837e5d0938ecfd90cf8dd9e52/libs/wire-api/src/Wire/API/User.hs#L625); we're particularly interested in `NewUserOriginTeamUser` --> only `NewTeamMember` or `NewTeamMemberSSO` should be accepted. In case this is a `Nothing`, we need to check if the user expires, i.e., if the user has no identity (and thus `Ephemeral`). So `/register` should only succeed iff at least one of these conditions is true: ``` -newUserTeam == (Just (NewTeamMember _)) OR -newUserTeam == (Just (NewTeamMemberSSO _)) OR -newUserExpiresIn == (Just _) +import Brig.Types.User +isNewUserTeamMember || isNewUserEphemeral ``` The rest of the unauthorized end-points is safe: diff --git a/libs/brig-types/src/Brig/Types/User.hs b/libs/brig-types/src/Brig/Types/User.hs index 11aebab832c..036681fcc55 100644 --- a/libs/brig-types/src/Brig/Types/User.hs +++ b/libs/brig-types/src/Brig/Types/User.hs @@ -43,6 +43,8 @@ module Brig.Types.User newUserEmail, newUserPhone, newUserSSOId, + isNewUserEphemeral, + isNewUserTeamMember, InvitationCode (..), BindingNewTeamUser (..), NewTeamUser (..), diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 927251d8c89..1688697f1d8 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -43,6 +43,8 @@ module Wire.API.User newUserEmail, newUserPhone, newUserSSOId, + isNewUserEphemeral, + isNewUserTeamMember, -- * NewUserOrigin NewUserOrigin (..), @@ -492,6 +494,18 @@ validateNewUserPublic nu | otherwise = Right (NewUserPublic nu) +-- | Any user registering without either an email or a phone is Ephemeral, +-- i.e. can be deleted after expires_in or sessionTokenTimeout +isNewUserEphemeral :: NewUser -> Bool +isNewUserEphemeral = isNothing . newUserIdentity + +isNewUserTeamMember :: NewUser -> Bool +isNewUserTeamMember u = case newUserTeam u of + Just (NewTeamMember _) -> True + Just (NewTeamMemberSSO _) -> True + Just (NewTeamCreator _) -> False + Nothing -> False + instance Arbitrary NewUserPublic where arbitrary = arbitrary `QC.suchThatMap` (rightMay . validateNewUserPublic) diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 3861d3dc63e..6c886a52daf 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -318,22 +318,13 @@ createUser new@NewUser {..} = do -- | docs/reference/user/registration.md {#RefRestrictRegistration}. checkRestrictedUserCreation :: NewUser -> ExceptT CreateUserError AppIO () checkRestrictedUserCreation new = do - let nTeam = newUserTeam new - nExpires = newUserExpiresIn new - restrictPlease <- lift . asks $ fromMaybe False . setRestrictUserCreation . view settings when ( restrictPlease - && not (isTeamMember nTeam) - && not (isEphemeral nExpires) + && not (isNewUserTeamMember new) + && not (isNewUserEphemeral new) ) $ throwE UserCreationRestricted - where - isTeamMember (Just (NewTeamMember _)) = True - isTeamMember (Just (NewTeamMemberSSO _)) = True - isTeamMember _ = False - isEphemeral (Just _) = True - isEphemeral _ = False ------------------------------------------------------------------------------- -- Update Profile diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 02eb9c484da..6387da78ba8 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -128,12 +128,10 @@ newAccount u inv tid = do name = newUserDisplayName u pict = fromMaybe noPict (newUserPict u) assets = newUserAssets u - status = case ident of - Nothing -> - -- any user registering without either an email or a phone is Ephemeral, - -- i.e. can be deleted after expires_in or sessionTokenTimeout - Ephemeral - Just _ -> Active + status = + if isNewUserEphemeral u + then Ephemeral + else Active colour = fromMaybe defaultAccentId (newUserAccentId u) locale defLoc = fromMaybe defLoc (newUserLocale u) managedBy = fromMaybe defaultManagedBy (newUserManagedBy u) diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 4273ca88ece..aba28d05f29 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -1079,14 +1079,6 @@ testRestrictedUserCreation opts brig = do let opts' = opts {Opt.optSettings = (Opt.optSettings opts) {Opt.setRestrictUserCreation = Just True}} withSettingsOverrides opts' $ do e <- randomEmail - -- Ephemeral users MUST have an expires_in - let Object ephemeralUserWithoutExpires = - object - [ "name" .= Name "Alice" - ] - postUserRegister' ephemeralUserWithoutExpires brig !!! do - const 403 === statusCode - const (Just "user-creation-restricted") === (^? AesonL.key "label" . AesonL._String) . (responseJsonUnsafe @Value) let Object regularUser = object @@ -1124,7 +1116,7 @@ testRestrictedUserCreation opts brig = do -- Ensure you can invite team users void $ inviteAndRegisterUser teamOwner createdTeam brig - -- Ephemeral users can always be created + -- Ephemeral users can always be created (expires_in is OPTIONAL) let Object ephemeralUser = object [ "name" .= Name "Alice", @@ -1132,6 +1124,14 @@ testRestrictedUserCreation opts brig = do ] postUserRegister' ephemeralUser brig !!! const 201 === statusCode + -- Ephemeral users can always be created (expires_in is OPTIONAL and + -- used for instance when creating guestrooms + let Object ephemeralUserWithoutExpires = + object + [ "name" .= Name "Alice" + ] + postUserRegister' ephemeralUserWithoutExpires brig !!! const 201 === statusCode + -- NOTE: SSO users are anyway not allowed on the `/register` endpoint teamid <- Id <$> liftIO UUID.nextRandom let ssoid = UserSSOId "nil" "nil" From 12e9492b2cc1ce50c89b4ccbd0fa2d036dd3c3b4 Mon Sep 17 00:00:00 2001 From: fisx Date: Fri, 28 Aug 2020 15:28:34 +0200 Subject: [PATCH 20/23] Refactor Invitations (#1196) * Cleanup data Invitation, InvitationRequest. - drop irName, which carried the inviter name, but was never used. - fix test suite where we used irName wrong, and then ignored it. - give irEmail, irPhone, inIdentity, inPhone less ambiguous names. - shuffle record fields. - swagger. - move around validateHandle and some other helpers. * Fix harmless bug. * Remove confused TODO. * Separate Id creation and DB effects better. * ormolu --- libs/wire-api/src/Wire/API/Team/Invitation.hs | 54 +++++++------- services/brig/src/Brig/API/Public.hs | 38 +++------- services/brig/src/Brig/API/User.hs | 53 ++++++++------ services/brig/src/Brig/API/Util.hs | 37 ++++++++-- services/brig/src/Brig/Team/API.hs | 53 ++++++++++---- services/brig/src/Brig/Team/DB.hs | 35 +++++---- services/brig/test/integration/API/Team.hs | 72 +++++++++---------- .../brig/test/integration/API/Team/Util.hs | 13 ++-- services/galley/test/integration/API/Util.hs | 19 +++-- services/spar/test-integration/Util/Core.hs | 11 ++- 10 files changed, 225 insertions(+), 160 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Team/Invitation.hs b/libs/wire-api/src/Wire/API/Team/Invitation.hs index 20b8e71b9a1..1473300149e 100644 --- a/libs/wire-api/src/Wire/API/Team/Invitation.hs +++ b/libs/wire-api/src/Wire/API/Team/Invitation.hs @@ -44,12 +44,11 @@ import Wire.API.User.Profile (Locale, Name) -- InvitationRequest data InvitationRequest = InvitationRequest - { irEmail :: Email, - irName :: Name, - irLocale :: Maybe Locale, + { irLocale :: Maybe Locale, irRole :: Maybe Role, irInviteeName :: Maybe Name, - irPhone :: Maybe Phone + irInviteeEmail :: Email, + irInviteePhone :: Maybe Phone } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform InvitationRequest) @@ -57,42 +56,40 @@ data InvitationRequest = InvitationRequest modelTeamInvitationRequest :: Doc.Model modelTeamInvitationRequest = Doc.defineModel "TeamInvitationRequest" $ do Doc.description "A request to join a team on Wire." - Doc.property "inviter_name" Doc.string' $ - Doc.description "Name of the inviter (1 - 128 characters)" - Doc.property "email" Doc.string' $ - Doc.description "Email of the invitee" Doc.property "locale" Doc.string' $ do Doc.description "Locale to use for the invitation." Doc.optional Doc.property "role" typeRole $ do - Doc.description "Role of the invited user" + Doc.description "Role of the invitee (invited user)." Doc.optional Doc.property "name" Doc.string' $ do - Doc.description "Name of the invitee (1 - 128 characters)" + Doc.description "Name of the invitee (1 - 128 characters)." Doc.optional + Doc.property "email" Doc.string' $ + Doc.description "Email of the invitee." Doc.property "phone" Doc.string' $ do - Doc.description "Phone number of the invitee, in the E.164 format" + Doc.description "Phone number of the invitee, in the E.164 format." Doc.optional + Doc.property "inviter_name" Doc.string' $ + Doc.description "DEPRECATED - WILL BE IGNORED IN FAVOR OF REQ AUTH DATA - Name of the inviter (1 - 128 characters)." instance ToJSON InvitationRequest where toJSON i = object $ - [ "email" .= irEmail i, - "inviter_name" .= irName i, - "locale" .= irLocale i, + [ "locale" .= irLocale i, "role" .= irRole i, "name" .= irInviteeName i, - "phone" .= irPhone i + "email" .= irInviteeEmail i, + "phone" .= irInviteePhone i ] instance FromJSON InvitationRequest where parseJSON = withObject "invitation-request" $ \o -> InvitationRequest - <$> o .: "email" - <*> o .: "inviter_name" - <*> o .:? "locale" + <$> o .:? "locale" <*> o .:? "role" <*> o .:? "name" + <*> o .: "email" <*> o .:? "phone" -------------------------------------------------------------------------------- @@ -102,22 +99,19 @@ data Invitation = Invitation { inTeam :: TeamId, inRole :: Role, inInvitation :: InvitationId, - inIdentity :: Email, inCreatedAt :: UTCTimeMillis, -- | this is always 'Just' for new invitations, but for -- migration it is allowed to be 'Nothing'. inCreatedBy :: Maybe UserId, + inInviteeEmail :: Email, inInviteeName :: Maybe Name, - inPhone :: Maybe Phone + inInviteePhone :: Maybe Phone } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform Invitation) --- | This is *not* the swagger model for the 'TeamInvitation' type (which does not exist), but --- for the use of 'Invitation' under @/teams/{tid}/invitations@. --- --- TODO: swagger should be replaced by something more type-safe at some point so this will be --- forcibly resolved and won't happen again. +-- | (This is *not* the swagger model for the 'TeamInvitation' type (which does not exist), +-- but for the use of 'Invitation' under @/teams/{tid}/invitations@.) modelTeamInvitation :: Doc.Model modelTeamInvitation = Doc.defineModel "TeamInvitation" $ do Doc.description "An invitation to join a team on Wire" @@ -128,13 +122,13 @@ modelTeamInvitation = Doc.defineModel "TeamInvitation" $ do Doc.optional Doc.property "id" Doc.bytes' $ Doc.description "UUID used to refer the invitation" - Doc.property "email" Doc.string' $ - Doc.description "Email of the invitee" Doc.property "created_at" Doc.dateTime' $ Doc.description "Timestamp of invitation creation" Doc.property "created_by" Doc.bytes' $ do Doc.description "ID of the inviting user" Doc.optional + Doc.property "email" Doc.string' $ + Doc.description "Email of the invitee" Doc.property "name" Doc.string' $ do Doc.description "Name of the invitee (1 - 128 characters)" Doc.optional @@ -148,11 +142,11 @@ instance ToJSON Invitation where [ "team" .= inTeam i, "role" .= inRole i, "id" .= inInvitation i, - "email" .= inIdentity i, "created_at" .= inCreatedAt i, "created_by" .= inCreatedBy i, + "email" .= inInviteeEmail i, "name" .= inInviteeName i, - "phone" .= inPhone i + "phone" .= inInviteePhone i ] instance FromJSON Invitation where @@ -162,9 +156,9 @@ instance FromJSON Invitation where -- clients, when leaving "role" empty, can leave the default role choice to us <*> o .:? "role" .!= defaultRole <*> o .: "id" - <*> o .: "email" <*> o .: "created_at" <*> o .:? "created_by" + <*> o .: "email" <*> o .:? "name" <*> o .:? "phone" diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 28a939ab53e..ef9158eff73 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -31,6 +31,7 @@ import Brig.API.IdMapping (resolveOpaqueUserId) import qualified Brig.API.Properties as API import Brig.API.Types import qualified Brig.API.User as API +import qualified Brig.API.Util as API import Brig.App import qualified Brig.Calling.API as Calling import qualified Brig.Data.User as Data @@ -1136,32 +1137,14 @@ changeLocaleH (u ::: conn ::: req) = do lift $ API.changeLocale u conn l return empty -data CheckHandleResp - = CheckHandleInvalid - | CheckHandleFound - | CheckHandleNotFound - +-- | (zusr are is ignored by this handler, ie. checking handles is allowed as long as you have +-- *any* account.) checkHandleH :: UserId ::: Text -> Handler Response -checkHandleH (uid ::: hndl) = do - checkHandle uid hndl >>= \case - CheckHandleInvalid -> throwE (StdError invalidHandle) - CheckHandleFound -> pure $ setStatus status200 empty - CheckHandleNotFound -> pure $ setStatus status404 empty - -checkHandle :: UserId -> Text -> Handler CheckHandleResp -checkHandle _ uhandle = do - handle <- validateHandle uhandle - owner <- lift $ API.lookupHandle handle - if - | isJust owner -> - -- Handle is taken (=> getHandleInfo will return 200) - return CheckHandleFound - | API.isBlacklistedHandle handle -> - -- Handle is free but cannot be taken - return CheckHandleInvalid - | otherwise -> - -- Handle is free and can be taken - return CheckHandleNotFound +checkHandleH (_uid ::: hndl) = do + API.checkHandle hndl >>= \case + API.CheckHandleInvalid -> throwE (StdError invalidHandle) + API.CheckHandleFound -> pure $ setStatus status200 empty + API.CheckHandleNotFound -> pure $ setStatus status404 empty checkHandlesH :: JSON ::: UserId ::: JsonRequest Public.CheckHandles -> Handler Response checkHandlesH (_ ::: _ ::: req) = do @@ -1193,7 +1176,7 @@ changeHandleH (u ::: conn ::: req) = do changeHandle :: UserId -> ConnId -> Public.HandleUpdate -> Handler () changeHandle u conn (Public.HandleUpdate h) = do - handle <- validateHandle h + handle <- API.validateHandle h API.changeHandle u conn handle !>> changeHandleError beginPasswordResetH :: JSON ::: JsonRequest Public.NewPasswordReset -> Handler Response @@ -1357,9 +1340,6 @@ deprecatedCompletePasswordResetH (_ ::: k ::: req) = do -- Utilities -validateHandle :: Text -> Handler Handle -validateHandle = maybe (throwE (StdError invalidHandle)) return . parseHandle - ifNothing :: Utilities.Error -> Maybe a -> Handler a ifNothing e = maybe (throwStd e) return diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 6c886a52daf..11bb08d49d8 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -28,6 +28,8 @@ module Brig.API.User changeEmail, changePhone, changeHandle, + CheckHandleResp (..), + checkHandle, lookupHandle, changeManagedBy, changeAccountStatus, @@ -36,7 +38,6 @@ module Brig.API.User Data.lookupAccount, Data.lookupStatus, lookupAccountsByIdentity, - lookupSelfProfile, lookupProfile, lookupProfiles, Data.lookupName, @@ -84,7 +85,9 @@ module Brig.API.User where import qualified Brig.API.Error as Error +import qualified Brig.API.Handler as API (Handler) import Brig.API.Types +import Brig.API.Util (fetchUserIdentity, validateHandle) import Brig.App import qualified Brig.Code as Code import Brig.Data.Activation (ActivationEvent (..)) @@ -183,7 +186,7 @@ createUser new@NewUser {..} = do _ -> return Nothing (teamEmailInvited, joinedTeamInvite) <- case teamInvitation of Just (inv, invInfo) -> do - let em = Team.inIdentity inv + let em = Team.inInviteeEmail inv acceptTeamInvitation account inv invInfo (userEmailKey em) (EmailIdentity em) Team.TeamName nm <- lift $ Intra.getTeamName (Team.inTeam inv) return (True, Just $ CreateUserTeam (Team.inTeam inv) nm) @@ -261,7 +264,7 @@ createUser new@NewUser {..} = do lift (Team.lookupInvitationInfo c) >>= \case Just ii -> do inv <- lift $ Team.lookupInvitation (Team.iiTeam ii) (Team.iiInvId ii) - case (inv, Team.inIdentity <$> inv) of + case (inv, Team.inInviteeEmail <$> inv) of (Just invite, Just em) | e == userEmailKey em -> do _ <- ensureMemberCanJoin (Team.iiTeam ii) @@ -373,6 +376,33 @@ changeHandle uid conn hdl = do throwE ChangeHandleExists lift $ Intra.onUserEvent uid (Just conn) (handleUpdated uid hdl) +-------------------------------------------------------------------------------- +-- Check Handle + +data CheckHandleResp + = CheckHandleInvalid + | CheckHandleFound + | CheckHandleNotFound + +checkHandle :: Text -> API.Handler CheckHandleResp +checkHandle uhandle = do + xhandle <- validateHandle uhandle + owner <- lift $ lookupHandle xhandle + if + | isJust owner -> + -- Handle is taken (=> getHandleInfo will return 200) + return CheckHandleFound + | isBlacklistedHandle xhandle -> + -- Handle is free but cannot be taken + -- + -- FUTUREWORK: i wonder if this is correct? isn't this the error for malformed + -- handles? shouldn't we throw not-found here? or should there be a fourth case + -- 'CheckHandleBlacklisted'? + return CheckHandleInvalid + | otherwise -> + -- Handle is free and can be taken + return CheckHandleNotFound + -------------------------------------------------------------------------------- -- Check Handles @@ -1020,12 +1050,6 @@ getEmailForProfile profileOwner (EmailVisibleIfOnSameTeam' (Just (viewerTeamId, getEmailForProfile _ (EmailVisibleIfOnSameTeam' Nothing) = Nothing getEmailForProfile _ EmailVisibleToSelf' = Nothing --- | Obtain a profile for a user as he can see himself. -lookupSelfProfile :: UserId -> AppIO (Maybe SelfProfile) -lookupSelfProfile = fmap (fmap mk) . Data.lookupAccount - where - mk a = SelfProfile (accountUser a) - -- | Find user accounts for a given identity, both activated and those -- currently pending activation. lookupAccountsByIdentity :: Either Email Phone -> AppIO [UserAccount] @@ -1058,14 +1082,3 @@ phonePrefixDelete = Blacklist.deletePrefix phonePrefixInsert :: ExcludedPrefix -> AppIO () phonePrefixInsert = Blacklist.insertPrefix - -------------------------------------------------------------------------------- --- Utilities - --- TODO: Move to a util module or similar -fetchUserIdentity :: UserId -> AppIO (Maybe UserIdentity) -fetchUserIdentity uid = - lookupSelfProfile uid - >>= maybe - (throwM $ UserProfileNotFound uid) - (return . userIdentity . selfUser) diff --git a/services/brig/src/Brig/API/Util.hs b/services/brig/src/Brig/API/Util.hs index a82fd2130bf..2831f49df5e 100644 --- a/services/brig/src/Brig/API/Util.hs +++ b/services/brig/src/Brig/API/Util.hs @@ -15,17 +15,30 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Brig.API.Util where +module Brig.API.Util + ( fetchUserIdentity, + isFederationEnabled, + lookupProfilesMaybeFilterSameTeamOnly, + lookupSelfProfile, + validateHandle, + viewFederationDomain, + ) +where +import qualified Brig.API.Error as Error import Brig.API.Handler -import Brig.App (Env, settings) +import Brig.API.Types +import Brig.App (AppIO, Env, settings) import qualified Brig.Data.User as Data import Brig.Options (enableFederationWithDomain) import Brig.Types +import Brig.Types.Intra (accountUser) import Control.Lens (view) -import Control.Monad +import Control.Monad.Catch (throwM) +import Control.Monad.Trans.Except (throwE) import Data.Domain (Domain) -import Data.Id as Id +import Data.Handle (Handle, parseHandle) +import Data.Id import Data.Maybe import Imports @@ -36,6 +49,22 @@ lookupProfilesMaybeFilterSameTeamOnly self us = do Just team -> filter (\x -> profileTeam x == Just team) us Nothing -> us +fetchUserIdentity :: UserId -> AppIO (Maybe UserIdentity) +fetchUserIdentity uid = + lookupSelfProfile uid + >>= maybe + (throwM $ UserProfileNotFound uid) + (return . userIdentity . selfUser) + +-- | Obtain a profile for a user as he can see himself. +lookupSelfProfile :: UserId -> AppIO (Maybe SelfProfile) +lookupSelfProfile = fmap (fmap mk) . Data.lookupAccount + where + mk a = SelfProfile (accountUser a) + +validateHandle :: Text -> Handler Handle +validateHandle = maybe (throwE (Error.StdError Error.invalidHandle)) return . parseHandle + -------------------------------------------------------------------------------- -- Federation diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index b5684696593..ee5d7366761 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -25,7 +25,7 @@ import Brig.API.Error import Brig.API.Handler import Brig.API.User (fetchUserIdentity) import qualified Brig.API.User as API -import Brig.App (currentTime, settings) +import Brig.App (AppIO, currentTime, settings) import qualified Brig.Data.Blacklist as Blacklist import Brig.Data.UserKey import qualified Brig.Data.UserKey as Data @@ -59,7 +59,7 @@ import Network.Wai.Utilities hiding (code, message) import Network.Wai.Utilities.Swagger (document) import qualified Network.Wai.Utilities.Swagger as Doc import qualified Wire.API.Team.Invitation as Public -import qualified Wire.API.User as Public (InvitationCode) +import qualified Wire.API.User as Public routesPublic :: Routes Doc.ApiBuilder Handler () routesPublic = do @@ -204,18 +204,27 @@ createInvitationH (_ ::: uid ::: tid ::: req) = do addHeader "Location" $ "/teams/" <> toByteString' tid <> "/invitations/" <> toByteString' iid +data CreateInvitationInviter = CreateInvitationInviter + { inviterUid :: UserId, + inviterEmail :: Email + } + deriving (Eq, Show) + createInvitation :: UserId -> TeamId -> Public.InvitationRequest -> Handler Public.Invitation createInvitation uid tid body = do - idt <- maybe (throwStd noIdentity) return =<< lift (fetchUserIdentity uid) - from <- maybe (throwStd noEmail) return (emailIdentity idt) let inviteePerms = Team.rolePermissions inviteeRole inviteeRole = fromMaybe Team.defaultRole . irRole $ body - ensurePermissionToAddUser uid tid inviteePerms + inviter <- do + idt <- maybe (throwStd noIdentity) return =<< lift (fetchUserIdentity uid) + from <- maybe (throwStd noEmail) return (emailIdentity idt) + ensurePermissionToAddUser uid tid inviteePerms + pure $ CreateInvitationInviter uid from + -- FUTUREWORK: These validations are nearly copy+paste from accountCreation and -- sendActivationCode. Refactor this to a single place -- Validate e-mail - email <- either (const $ throwStd invalidEmail) return (Email.validateEmail (irEmail body)) + email <- either (const $ throwStd invalidEmail) return (Email.validateEmail (irInviteeEmail body)) let uke = userEmailKey email blacklistedEm <- lift $ Blacklist.exists uke when blacklistedEm $ @@ -224,7 +233,7 @@ createInvitation uid tid body = do when emailTaken $ throwStd emailExists -- Validate phone - phone <- for (irPhone body) $ \p -> do + phone <- for (irInviteePhone body) $ \p -> do validatedPhone <- maybe (throwStd invalidPhone) return =<< lift (Phone.validatePhone p) let ukp = userPhoneKey validatedPhone blacklistedPh <- lift $ Blacklist.exists ukp @@ -238,14 +247,34 @@ createInvitation uid tid body = do pending <- lift $ DB.countInvitations tid when (fromIntegral pending >= maxSize) $ throwStd tooManyTeamInvitations - doInvite inviteeRole email from (irLocale body) (irInviteeName body) phone + + iid <- liftIO DB.mkInvitationId + lift $ doInvite iid inviteeRole inviter (irLocale body) email (irInviteeName body) phone where - doInvite role toEmail from lc toName toPhone = lift $ do + doInvite :: + InvitationId -> + Team.Role -> + CreateInvitationInviter -> + Maybe Public.Locale -> + Email -> + Maybe Public.Name -> + Maybe Public.Phone -> + AppIO Invitation + doInvite iid role inviter lc toEmail toName toPhone = do now <- liftIO =<< view currentTime timeout <- setTeamInvitationTimeout <$> view settings - (newInv, code) <- DB.insertInvitation tid role toEmail now (Just uid) toName toPhone timeout - void $ sendInvitationMail toEmail tid from code lc - return newInv + (newInv, code) <- + DB.insertInvitation + iid + tid + role + now + (Just $ inviterUid inviter) + toEmail + toName + toPhone + timeout + newInv <$ sendInvitationMail toEmail tid (inviterEmail inviter) code lc deleteInvitationH :: JSON ::: UserId ::: TeamId ::: InvitationId -> Handler Response deleteInvitationH (_ ::: uid ::: tid ::: iid) = do diff --git a/services/brig/src/Brig/Team/DB.hs b/services/brig/src/Brig/Team/DB.hs index 1dcb15bef6c..76a472b768b 100644 --- a/services/brig/src/Brig/Team/DB.hs +++ b/services/brig/src/Brig/Team/DB.hs @@ -79,20 +79,20 @@ data InvitationByEmail insertInvitation :: MonadClient m => + InvitationId -> TeamId -> Role -> - Email -> UTCTime -> Maybe UserId -> + Email -> Maybe Name -> Maybe Phone -> -- | The timeout for the invitation code. Timeout -> m (Invitation, InvitationCode) -insertInvitation t role email (toUTCTimeMillis -> now) minviter inviteeName phone timeout = do - iid <- liftIO mkInvitationId +insertInvitation iid t role (toUTCTimeMillis -> now) minviter email inviteeName phone timeout = do code <- liftIO mkInvitationCode - let inv = Invitation t role iid email now minviter inviteeName phone + let inv = Invitation t role iid now minviter email inviteeName phone retry x5 . batch $ do setType BatchLogged setConsistency Quorum @@ -114,8 +114,8 @@ lookupInvitation t r = fmap toInvitation <$> retry x1 (query1 cqlInvitation (params Quorum (t, r))) where - cqlInvitation :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, Email, UTCTimeMillis, Maybe UserId, Maybe Name, Maybe Phone) - cqlInvitation = "SELECT team, role, id, email, created_at, created_by, name, phone FROM team_invitation WHERE team = ? AND id = ?" + cqlInvitation :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone) + cqlInvitation = "SELECT team, role, id, created_at, created_by, email, name, phone FROM team_invitation WHERE team = ? AND id = ?" lookupInvitationByCode :: MonadClient m => InvitationCode -> m (Maybe Invitation) lookupInvitationByCode i = @@ -151,10 +151,10 @@ lookupInvitations team start (fromRange -> size) = do { result = invs, hasMore = more } - cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, Email, UTCTimeMillis, Maybe UserId, Maybe Name, Maybe Phone) - cqlSelect = "SELECT team, role, id, email, created_at, created_by, name, phone FROM team_invitation WHERE team = ? ORDER BY id ASC" - cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, Email, UTCTimeMillis, Maybe UserId, Maybe Name, Maybe Phone) - cqlSelectFrom = "SELECT team, role, id, email, created_at, created_by, name, phone FROM team_invitation WHERE team = ? AND id > ? ORDER BY id ASC" + cqlSelect :: PrepQuery R (Identity TeamId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone) + cqlSelect = "SELECT team, role, id, created_at, created_by, email, name, phone FROM team_invitation WHERE team = ? ORDER BY id ASC" + cqlSelectFrom :: PrepQuery R (TeamId, InvitationId) (TeamId, Maybe Role, InvitationId, UTCTimeMillis, Maybe UserId, Email, Maybe Name, Maybe Phone) + cqlSelectFrom = "SELECT team, role, id, created_at, created_by, email, name, phone FROM team_invitation WHERE team = ? AND id > ? ORDER BY id ASC" deleteInvitation :: MonadClient m => TeamId -> InvitationId -> m () deleteInvitation t i = do @@ -231,5 +231,16 @@ countInvitations t = -- | brig used to not store the role, so for migration we allow this to be empty and fill in the -- default here. -toInvitation :: (TeamId, Maybe Role, InvitationId, Email, UTCTimeMillis, Maybe UserId, Maybe Name, Maybe Phone) -> Invitation -toInvitation (t, r, i, e, tm, minviter, inviteeName, p) = Invitation t (fromMaybe Team.defaultRole r) i e tm minviter inviteeName p +toInvitation :: + ( TeamId, + Maybe Role, + InvitationId, + UTCTimeMillis, + Maybe UserId, + Email, + Maybe Name, + Maybe Phone + ) -> + Invitation +toInvitation (t, r, i, tm, minviter, e, inviteeName, p) = + Invitation t (fromMaybe Team.defaultRole r) i tm minviter e inviteeName p diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 8b7f1b16a92..1a83cdad7af 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -132,7 +132,7 @@ testUpdateEvents brig cannon = do (alice, tid) <- createUserWithTeam brig inviteeEmail <- randomEmail -- invite and register Bob - let invite = stdInvitationRequest inviteeEmail (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest inviteeEmail inv <- responseJsonError =<< postInvitation brig tid alice invite Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) rsp2 <- @@ -162,8 +162,7 @@ testUpdateEvents brig cannon = do testInvitationEmail :: Brig -> Http () testInvitationEmail brig = do (inviter, tid) <- createUserWithTeam brig - invitee <- randomEmail - let invite = stdInvitationRequest invitee (Name "Bob") Nothing Nothing + invite <- stdInvitationRequest <$> randomEmail void $ postInvitation brig tid inviter invite testInvitationEmailLookup :: Brig -> Http () @@ -172,12 +171,12 @@ testInvitationEmailLookup brig = do -- expect no invitation to be found for an email before that person is invited headInvitationByEmail brig email 404 (uid, tid) <- createUserWithTeam brig - let invite = stdInvitationRequest email (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest email void $ postInvitation brig tid uid invite -- expect an invitation to be found querying with email after invite headInvitationByEmail brig email 200 (uid2, tid2) <- createUserWithTeam brig - let invite2 = stdInvitationRequest email (Name "Bob2") Nothing Nothing + let invite2 = stdInvitationRequest email void $ postInvitation brig tid2 uid2 invite2 -- expect a 409 conflict result for a second team inviting the same user headInvitationByEmail brig email 409 @@ -186,7 +185,7 @@ testInvitationEmailLookupRegister :: Brig -> Http () testInvitationEmailLookupRegister brig = do email <- randomEmail (owner, tid) <- createUserWithTeam brig - let invite = stdInvitationRequest email (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest email void $ postInvitation brig tid owner invite inv :: Invitation <- responseJsonError =<< postInvitation brig tid owner invite -- expect an invitation to be found querying with email after invite @@ -201,7 +200,7 @@ testInvitationEmailLookupNginz brig nginz = do -- expect no invitation to be found for an email before that person is invited headInvitationByEmail nginz email 404 (uid, tid) <- createUserWithTeam brig - let invite = stdInvitationRequest email (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest email void $ postInvitation brig tid uid invite -- expect an invitation to be found querying with email after invite headInvitationByEmail nginz email 200 @@ -215,13 +214,11 @@ testInvitationTooManyPending :: Brig -> TeamSizeLimit -> Http () testInvitationTooManyPending brig (TeamSizeLimit limit) = do (inviter, tid) <- createUserWithTeam brig emails <- replicateConcurrently (fromIntegral limit) randomEmail - let invite e = stdInvitationRequest e (Name "Bob") Nothing Nothing - pooledForConcurrentlyN_ 16 emails $ \email -> - postInvitation brig tid inviter (invite email) - e <- randomEmail + pooledForConcurrentlyN_ 16 emails $ postInvitation brig tid inviter . stdInvitationRequest + email <- randomEmail -- TODO: If this test takes longer to run than `team-invitation-timeout`, then some of the -- invitations have likely expired already and this test will actually _fail_ - postInvitation brig tid inviter (invite e) !!! do + postInvitation brig tid inviter (stdInvitationRequest email) !!! do const 403 === statusCode const (Just "too-many-team-invitations") === fmap Error.label . responseJsonMaybe @@ -245,13 +242,13 @@ testInvitationRoles brig galley = do -- owner creates a member alice. alice :: UserId <- do aliceEmail <- randomEmail - let invite = stdInvitationRequest aliceEmail (Name "Alice") Nothing (Just Team.RoleAdmin) + let invite = stdInvitationRequest' Nothing (Just Team.RoleAdmin) aliceEmail inv :: Invitation <- responseJsonError =<< postInvitation brig tid owner invite registerInvite brig tid inv aliceEmail -- alice creates a external partner bob. success! bob only has externalPartner perms. do bobEmail <- randomEmail - let invite = stdInvitationRequest bobEmail (Name "Bob") Nothing (Just Team.RoleExternalPartner) + let invite = stdInvitationRequest' Nothing (Just Team.RoleExternalPartner) bobEmail inv :: Invitation <- responseJsonError =<< ( postInvitation brig tid alice invite Galley -> Http () testInvitationEmailAccepted brig galley = do inviteeEmail <- randomEmail - let invite = stdInvitationRequest inviteeEmail (Name "Bob") Nothing Nothing - void $ createAndVerifyInvitation (accept (irEmail invite)) invite brig galley + let invite = stdInvitationRequest inviteeEmail + void $ createAndVerifyInvitation (accept (irInviteeEmail invite)) invite brig galley -- | Related: 'testDomainsBlockedForRegistration'. When we remove the customer-specific -- extension of domain blocking, this test will fail to compile (so you will know it's time to @@ -283,18 +280,18 @@ testInvitationEmailAccepted brig galley = do testInvitationEmailAcceptedInBlockedDomain :: Opt.Opts -> Brig -> Galley -> Http () testInvitationEmailAcceptedInBlockedDomain opts brig galley = do inviteeEmail :: Email <- randomEmail - let invite = stdInvitationRequest inviteeEmail (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest inviteeEmail replacementBrigApp = withDomainsBlockedForRegistration opts [emailDomain inviteeEmail] - void $ createAndVerifyInvitation' (Just replacementBrigApp) (accept (irEmail invite)) invite brig galley + void $ createAndVerifyInvitation' (Just replacementBrigApp) (accept (irInviteeEmail invite)) invite brig galley testInvitationEmailAndPhoneAccepted :: Brig -> Galley -> Http () testInvitationEmailAndPhoneAccepted brig galley = do inviteeEmail <- randomEmail inviteePhone <- randomPhone -- Prepare the extended invitation - let stdInvite = stdInvitationRequest inviteeEmail (Name "Bob") Nothing Nothing + let stdInvite = stdInvitationRequest inviteeEmail inviteeName = Name "Invited Member" - extInvite = stdInvite {irPhone = Just inviteePhone, irInviteeName = Just inviteeName} + extInvite = stdInvite {irInviteePhone = Just inviteePhone, irInviteeName = Just inviteeName} -- Register the same (pre verified) phone number let phoneReq = RequestBodyLBS . encode $ object ["phone" .= fromPhone inviteePhone] post (brig . path "/activate/send" . contentJson . body phoneReq) !!! (const 200 === statusCode) @@ -304,7 +301,7 @@ testInvitationEmailAndPhoneAccepted brig galley = do liftIO $ assertEqual "Wrong name in profile" (Just inviteeName) (userDisplayName . selfUser <$> profile) liftIO $ assertEqual "Wrong name in invitation" (Just inviteeName) (inInviteeName invitation) liftIO $ assertEqual "Wrong phone number in profile" (Just inviteePhone) (join (userPhone . selfUser <$> profile)) - liftIO $ assertEqual "Wrong phone number in invitation" (Just inviteePhone) (inPhone invitation) + liftIO $ assertEqual "Wrong phone number in invitation" (Just inviteePhone) (inInviteePhone invitation) -- | FUTUREWORK: this is an alternative helper to 'createPopulatedBindingTeam'. it has been -- added concurrently, and the two should probably be consolidated. @@ -391,7 +388,7 @@ testCreateTeam brig galley aws = do liftIO $ assertBool "Member not part of the team" (uid == mem ^. Team.userId) -- Verify that the user cannot send invitations before activating their account inviteeEmail <- randomEmail - let invite = stdInvitationRequest inviteeEmail (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest inviteeEmail postInvitation brig (team ^. Team.teamId) uid invite !!! const 403 === statusCode -- Verify that the team is still in status "pending" team2 <- getTeam galley (team ^. Team.teamId) @@ -427,7 +424,7 @@ testCreateTeamPreverified brig galley aws = do liftIO $ assertEqual "Team should already be active" Team.Active (Team.tdStatus team2) -- Verify that the user can already send invitations before activating their account inviteeEmail <- randomEmail - let invite = stdInvitationRequest inviteeEmail (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest inviteeEmail postInvitation brig (team ^. Team.teamId) uid invite !!! const 201 === statusCode testInvitationNoPermission :: Brig -> Http () @@ -435,7 +432,7 @@ testInvitationNoPermission brig = do (_, tid) <- createUserWithTeam brig alice <- userId <$> randomUser brig email <- randomEmail - let invite = stdInvitationRequest email (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest email postInvitation brig tid alice invite !!! do const 403 === statusCode const (Just "insufficient-permissions") === fmap Error.label . responseJsonMaybe @@ -475,9 +472,9 @@ testTeamNoPassword brig = do testInvitationCodeExists :: Brig -> Http () testInvitationCodeExists brig = do - email <- randomEmail (uid, tid) <- createUserWithTeam brig - let invite email_ = stdInvitationRequest email_ (Name "Bob") Nothing Nothing + let invite email = stdInvitationRequest email + email <- randomEmail rsp <- postInvitation brig tid uid (invite email) responseJsonMaybe rsp Just invCode <- getInvitationCode brig tid invId @@ -557,14 +554,14 @@ testInvitationTooManyMembers brig galley (TeamSizeLimit limit) = do pooledForConcurrentlyN_ 16 [1 .. limit -1] $ \_ -> do void $ createTeamMember brig galley creator tid Team.fullPermissions SearchUtil.refreshIndex brig - em <- randomEmail - let invite = stdInvitationRequest em (Name "Bob") Nothing Nothing - inv <- responseJsonError =<< postInvitation brig tid creator invite + let invite email = stdInvitationRequest email + email <- randomEmail + inv <- responseJsonError =<< postInvitation brig tid creator (invite email) Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) post ( brig . path "/register" . contentJson - . body (accept em inviteeCode) + . body (accept email inviteeCode) ) !!! do const 403 === statusCode @@ -575,7 +572,7 @@ testInvitationPaging brig = do before <- liftIO $ toUTCTimeMillis . addUTCTime (-1) <$> getCurrentTime (uid, tid) <- createUserWithTeam brig let total = 5 - invite email = stdInvitationRequest email (Name "Bob") Nothing Nothing + invite email = stdInvitationRequest email emails <- replicateM total $ do email <- randomEmail postInvitation brig tid uid (invite email) !!! const 201 === statusCode @@ -596,7 +593,7 @@ testInvitationPaging brig = do validateInv :: Invitation -> Assertion validateInv inv = do assertEqual "tid" tid (inTeam inv) - assertBool "email" (inIdentity inv `elem` emails) + assertBool "email" (inInviteeEmail inv `elem` emails) -- (the output list is not ordered chronologically and emails are unique, so we just -- check whether the email is one of the valid ones.) assertBool "timestamp" (inCreatedAt inv > before && inCreatedAt inv < after1ms) @@ -611,7 +608,7 @@ testInvitationInfo :: Brig -> Http () testInvitationInfo brig = do email <- randomEmail (uid, tid) <- createUserWithTeam brig - let invite = stdInvitationRequest email (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest email inv <- responseJsonError =<< postInvitation brig tid uid invite Just invCode <- getInvitationCode brig tid (inInvitation inv) Just invitation <- getInvitation brig invCode @@ -628,7 +625,7 @@ testInvitationInfoExpired :: Brig -> Opt.Timeout -> Http () testInvitationInfoExpired brig timeout = do email <- randomEmail (uid, tid) <- createUserWithTeam brig - let invite = stdInvitationRequest email (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest email inv <- responseJsonError =<< postInvitation brig tid uid invite -- Note: This value must be larger than the option passed as `team-invitation-timeout` awaitExpiry (round timeout + 5) tid (inInvitation inv) @@ -655,7 +652,7 @@ testSuspendTeam brig = do inviteeEmail2 <- randomEmail (inviter, tid) <- createUserWithTeam brig -- invite and register invitee - let invite = stdInvitationRequest inviteeEmail (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest inviteeEmail inv <- responseJsonError =<< postInvitation brig tid inviter invite Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) rsp2 <- @@ -667,7 +664,8 @@ testSuspendTeam brig = do responseJsonMaybe rsp2 -- invite invitee2 (don't register) - let invite2 = stdInvitationRequest inviteeEmail2 (Name "Bob") Nothing Nothing + let invite2 = stdInvitationRequest inviteeEmail2 + inv2 <- responseJsonError =<< postInvitation brig tid inviter invite2 Just _ <- getInvitationCode brig tid (inInvitation inv2) -- suspend team diff --git a/services/brig/test/integration/API/Team/Util.hs b/services/brig/test/integration/API/Team/Util.hs index 0ea886afe16..107ac430dba 100644 --- a/services/brig/test/integration/API/Team/Util.hs +++ b/services/brig/test/integration/API/Team/Util.hs @@ -84,7 +84,7 @@ createPopulatedBindingTeamWithNames brig names = do (inviter, tid) <- createUserWithTeam' brig invitees <- forM names $ \name -> do inviteeEmail <- randomEmail - let invite = stdInvitationRequest inviteeEmail name Nothing Nothing + let invite = stdInvitationRequest inviteeEmail inv <- responseJsonError =<< postInvitation brig tid (userId inviter) invite Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) rsp2 <- @@ -170,7 +170,7 @@ inviteAndRegisterUser :: m User inviteAndRegisterUser u tid brig = do inviteeEmail <- randomEmail - let invite = stdInvitationRequest inviteeEmail (Name "Bob") Nothing Nothing + let invite = stdInvitationRequest inviteeEmail inv <- responseJsonError =<< postInvitation brig tid u invite Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) rspInvitee <- @@ -437,9 +437,12 @@ isActivatedUser uid brig = do Just (_ : _) -> True _ -> False -stdInvitationRequest :: Email -> Name -> Maybe Locale -> Maybe Team.Role -> InvitationRequest -stdInvitationRequest e inviterName loc role = - InvitationRequest e inviterName loc role Nothing Nothing +stdInvitationRequest :: Email -> InvitationRequest +stdInvitationRequest = stdInvitationRequest' Nothing Nothing + +stdInvitationRequest' :: Maybe Locale -> Maybe Team.Role -> Email -> InvitationRequest +stdInvitationRequest' loc role email = + InvitationRequest loc role Nothing email Nothing setTeamTeamSearchVisibilityAvailable :: HasCallStack => Galley -> TeamId -> TeamFeatureStatusValue -> Http () setTeamTeamSearchVisibilityAvailable galley tid status = diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 5ecf9cb02d6..8892fa136ee 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -243,9 +243,9 @@ bulkGetTeamMembersTruncated usr tid uids trnc = do ) getTeamMember :: HasCallStack => UserId -> TeamId -> UserId -> TestM TeamMember -getTeamMember usr tid mid = do +getTeamMember getter tid gettee = do g <- view tsGalley - r <- get (g . paths ["teams", toByteString' tid, "members", toByteString' mid] . zUser usr) TeamId -> UserId -> TestM TeamMember @@ -278,11 +278,11 @@ addUserToTeam' u t = snd <$> addUserToTeamWithRole' Nothing u t addUserToTeamWithRole :: HasCallStack => Maybe Role -> UserId -> TeamId -> TestM TeamMember addUserToTeamWithRole role inviter tid = do - (inv, rsp2) <- addUserToTeamWithRole' role inviter tid -- TODO: getHeader "Set-Cookie" rsp2 liftIO $ assertEqual "Wrong cookie" (Just "zuid") (setCookieName <$> zuid) @@ -292,8 +292,7 @@ addUserToTeamWithRole' :: HasCallStack => Maybe Role -> UserId -> TeamId -> Test addUserToTeamWithRole' role inviter tid = do brig <- view tsBrig inviteeEmail <- randomEmail - let name = Name $ fromEmail inviteeEmail - let invite = InvitationRequest inviteeEmail name Nothing role Nothing Nothing + let invite = InvitationRequest Nothing role Nothing inviteeEmail Nothing invResponse <- postInvitation tid inviter invite inv <- responseJsonError invResponse Just inviteeCode <- getInvitationCode tid (inInvitation inv) @@ -301,7 +300,7 @@ addUserToTeamWithRole' role inviter tid = do post ( brig . path "/register" . contentJson - . body (acceptInviteBody name inviteeEmail inviteeCode) + . body (acceptInviteBody inviteeEmail inviteeCode) ) return (inv, r) @@ -326,11 +325,11 @@ makeOwner owner mem tid = do !!! const 200 === statusCode -acceptInviteBody :: Name -> Email -> InvitationCode -> RequestBody -acceptInviteBody name email code = +acceptInviteBody :: Email -> InvitationCode -> RequestBody +acceptInviteBody email code = RequestBodyLBS . encode $ object - [ "name" .= fromName name, + [ "name" .= Name "bob", "email" .= fromEmail email, "password" .= defPassword, "team_code" .= code diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 462f1d935b7..e3669d33953 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -358,7 +358,7 @@ inviteAndRegisterUser :: m User inviteAndRegisterUser brig u tid = do inviteeEmail <- randomEmail - let invite = TeamInvitation.InvitationRequest inviteeEmail (User.Name "Bob") Nothing Nothing Nothing Nothing + let invite = stdInvitationRequest inviteeEmail inv <- responseJsonError =<< postInvitation tid u invite Just inviteeCode <- getInvitationCode tid (TeamInvitation.inInvitation inv) rspInvitee <- @@ -1172,3 +1172,12 @@ checkErr status mlabel = do checkErrHspec :: HasCallStack => Int -> TestErrorLabel -> ResponseLBS -> Bool checkErrHspec status label resp = status == statusCode resp && responseJsonEither resp == Right label + +-- | copied from brig integration tests +stdInvitationRequest :: User.Email -> TeamInvitation.InvitationRequest +stdInvitationRequest = stdInvitationRequest' Nothing Nothing + +-- | copied from brig integration tests +stdInvitationRequest' :: Maybe User.Locale -> Maybe Galley.Role -> User.Email -> TeamInvitation.InvitationRequest +stdInvitationRequest' loc role email = + TeamInvitation.InvitationRequest loc role Nothing email Nothing From 0b3beff921fece9ee5adcb63c3c3916c2c8bc0e7 Mon Sep 17 00:00:00 2001 From: fisx Date: Mon, 31 Aug 2020 15:59:30 +0200 Subject: [PATCH 21/23] Scim-created users are never ephemeral. (#1198) --- libs/wire-api/src/Wire/API/User.hs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 1688697f1d8..af31709fb6a 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -494,10 +494,17 @@ validateNewUserPublic nu | otherwise = Right (NewUserPublic nu) --- | Any user registering without either an email or a phone is Ephemeral, --- i.e. can be deleted after expires_in or sessionTokenTimeout +-- | A user is Ephemeral if she has neither email, phone, nor sso credentials and is not +-- created via scim. Ephemeral users can be deleted after expires_in or sessionTokenTimeout +-- (whichever comes earlier). isNewUserEphemeral :: NewUser -> Bool -isNewUserEphemeral = isNothing . newUserIdentity +isNewUserEphemeral u = noId && noScim + where + noId = isNothing $ newUserIdentity u + noScim = case newUserManagedBy u of + Nothing -> True + Just ManagedByWire -> True + Just ManagedByScim -> False isNewUserTeamMember :: NewUser -> Bool isNewUserTeamMember u = case newUserTeam u of From 7cab08d43d549267f14261d1fe063125bfa855b1 Mon Sep 17 00:00:00 2001 From: fisx Date: Mon, 31 Aug 2020 20:11:07 +0200 Subject: [PATCH 22/23] Refactor misc things. (#1199) * Remove redundant code. * Fix typo. * Better error constructor name. * Remove unhelpful helper. * {To,From}ByteString instances for ManagedBy. needed in upcoming new internal end-points. * NameUpdate type with json instances. needed in upcoming new internal end-points. * Reference issue in FUTUREWORK comment. * Eliminate code duplication. * Refactor claiming handles. - more powerful variant of `claimHandle` (not exported, but separates concerns better). - take user ids instead of users (fewer interconnected parts). --- libs/wire-api/src/Wire/API/User.hs | 12 ++++++ libs/wire-api/src/Wire/API/User/Profile.hs | 12 ++++++ .../unit/Test/Wire/API/Roundtrip/Aeson.hs | 1 + .../Test/Wire/API/Roundtrip/ByteString.hs | 7 +-- services/brig/src/Brig/API/Internal.hs | 2 +- services/brig/src/Brig/API/User.hs | 6 +-- services/brig/src/Brig/User/Handle.hs | 43 +++++++++++-------- services/spar/src/Spar/API.hs | 6 +-- services/spar/src/Spar/App.hs | 12 ++---- services/spar/src/Spar/Error.hs | 6 +-- services/spar/src/Spar/Intra/Brig.hs | 19 -------- services/spar/src/Spar/Scim/User.hs | 6 +-- .../test-integration/Test/Spar/APISpec.hs | 2 +- 13 files changed, 69 insertions(+), 65 deletions(-) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index af31709fb6a..5afbd6d6bf7 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -59,6 +59,7 @@ module Wire.API.User EmailUpdate (..), PhoneUpdate (..), HandleUpdate (..), + NameUpdate (..), -- * Account Deletion DeleteUser (..), @@ -896,6 +897,17 @@ instance FromJSON HandleUpdate where parseJSON = withObject "handle-update" $ \o -> HandleUpdate <$> o .: "handle" +newtype NameUpdate = NameUpdate {nuHandle :: Text} + deriving stock (Eq, Show, Generic) + deriving newtype (Arbitrary) + +instance ToJSON NameUpdate where + toJSON h = object ["name" .= nuHandle h] + +instance FromJSON NameUpdate where + parseJSON = withObject "name-update" $ \o -> + NameUpdate <$> o .: "name" + ----------------------------------------------------------------------------- -- Account Deletion diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index 944c0737f8c..07a10a4050d 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -59,6 +59,7 @@ import Control.Applicative (optional) import Control.Error (hush) import Data.Aeson hiding (()) import qualified Data.Aeson.Types as Json +import Data.Attoparsec.ByteString.Char8 (takeByteString) import Data.Attoparsec.Text import Data.ByteString.Conversion import Data.ISO3166_CountryCodes @@ -286,6 +287,17 @@ instance FromJSON ManagedBy where "scim" -> pure ManagedByScim other -> fail $ "Invalid ManagedBy: " ++ show other +instance ToByteString ManagedBy where + builder ManagedByWire = "wire" + builder ManagedByScim = "scim" + +instance FromByteString ManagedBy where + parser = + takeByteString >>= \case + "wire" -> pure ManagedByWire + "scim" -> pure ManagedByScim + x -> fail $ "Invalid ManagedBy value: " <> show x + defaultManagedBy :: ManagedBy defaultManagedBy = ManagedByWire diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs index 7dcdd07d696..d6268514913 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/Aeson.hs @@ -220,6 +220,7 @@ tests = testRoundTrip @Team.Role.Role, testRoundTrip @Team.SearchVisibility.TeamSearchVisibility, testRoundTrip @Team.SearchVisibility.TeamSearchVisibilityView, + testRoundTrip @User.NameUpdate, testRoundTrip @User.NewUser, testRoundTrip @User.NewUserPublic, testRoundTrip @User.UserIdList, diff --git a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs index 9b7da669c0c..c98a5472085 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Roundtrip/ByteString.hs @@ -61,6 +61,8 @@ tests = testRoundTrip @Provider.HttpsUrl, testRoundTrip @Provider.Service.ServiceKeyPEM, testRoundTrip @Provider.Service.ServiceToken, + testRoundTrip @(Provider.Service.Tag.QueryAllTags 3 5), + testRoundTrip @(Provider.Service.Tag.QueryAnyTags 3 5), testRoundTrip @Provider.Service.Tag.ServiceTag, testRoundTrip @Push.V2.Token.Token, testRoundTrip @Team.Feature.TeamFeatureName, @@ -73,9 +75,8 @@ tests = testRoundTrip @User.InvitationCode, testRoundTrip @User.Password.PasswordResetCode, testRoundTrip @User.Password.PasswordResetKey, - testRoundTrip @User.Profile.Name, - testRoundTrip @(Provider.Service.Tag.QueryAllTags 3 5), - testRoundTrip @(Provider.Service.Tag.QueryAnyTags 3 5) + testRoundTrip @User.Profile.ManagedBy, + testRoundTrip @User.Profile.Name -- FUTUREWORK: -- testCase "Call.Config.TurnUsername (doesn't have FromByteString)" ... -- testCase "User.Activation.ActivationTarget (doesn't have FromByteString)" ... diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 50b15bb3b01..9fe36ce232a 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -89,7 +89,7 @@ sitemap = do accept "application" "json" .&. jsonRequest @NewUser - -- internal email activation (used in tests and in spar for validating emails obtains as + -- internal email activation (used in tests and in spar for validating emails obtained as -- SAML user identifiers). if the validate query parameter is false or missing, only set -- the activation timeout, but do not send an email, and do not do anything about activating -- the email. diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 11bb08d49d8..5c0fa7b9964 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -333,7 +333,7 @@ checkRestrictedUserCreation new = do -- Update Profile -- FUTUREWORK: this and other functions should refuse to modify a ManagedByScim user. See --- {#SparBrainDump} +-- {#SparBrainDump} https://github.com/zinfra/backend-issues/issues/1632 updateUser :: UserId -> ConnId -> UserUpdate -> AppIO () updateUser uid conn uu = do @@ -371,7 +371,7 @@ changeHandle uid conn hdl = do claim u = do unless (isJust (userIdentity u)) $ throwE ChangeHandleNoIdentity - claimed <- lift $ claimHandle u hdl + claimed <- lift $ claimHandle (userId u) (userHandle u) hdl unless claimed $ throwE ChangeHandleExists lift $ Intra.onUserEvent uid (Just conn) (handleUpdated uid hdl) @@ -899,7 +899,7 @@ deleteAccount account@(accountUser -> user) = do -- Free unique keys for_ (userEmail user) $ deleteKey . userEmailKey for_ (userPhone user) $ deleteKey . userPhoneKey - for_ (userHandle user) $ freeHandle user + for_ (userHandle user) $ freeHandle (userId user) -- Wipe data Data.clearProperties uid tombstone <- mkTombstone diff --git a/services/brig/src/Brig/User/Handle.hs b/services/brig/src/Brig/User/Handle.hs index 9ee4d21a2cc..b04e959bc15 100644 --- a/services/brig/src/Brig/User/Handle.hs +++ b/services/brig/src/Brig/User/Handle.hs @@ -27,52 +27,59 @@ where import Brig.App import Brig.Data.Instances () import qualified Brig.Data.User as User -import Brig.Types.User import Brig.Unique import Cassandra import Data.Handle (Handle, fromHandle) import Data.Id import Imports -claimHandle :: User -> Handle -> AppIO Bool -claimHandle u h = do +-- | Claim a new handle for an existing 'User'. +claimHandle :: UserId -> Maybe Handle -> Handle -> AppIO Bool +claimHandle uid oldHandle newHandle = isJust <$> claimHandleWith (User.updateHandle) uid oldHandle newHandle + +-- | Claim a handle for an invitation or a user. Invitations can be referenced by the coerced +-- 'UserId'. +claimHandleWith :: (UserId -> Handle -> AppIO a) -> UserId -> Maybe Handle -> Handle -> AppIO (Maybe a) +claimHandleWith updOperation uid oldHandle h = do owner <- lookupHandle h case owner of - Just u' | userId u /= u' -> return False + Just uid' | uid /= uid' -> return Nothing _ -> do env <- ask let key = "@" <> fromHandle h - claimed <- withClaim (userId u) key (30 # Minute) $ + withClaim uid key (30 # Minute) $ runAppT env $ do -- Record ownership - retry x5 $ write handleInsert (params Quorum (h, userId u)) + retry x5 $ write handleInsert (params Quorum (h, uid)) -- Update profile - User.updateHandle (userId u) h + result <- updOperation uid h -- Free old handle (if it changed) - for_ (mfilter (/= h) (userHandle u)) $ - freeHandle u - return (isJust claimed) + for_ (mfilter (/= h) oldHandle) $ + freeHandle uid + return result -- | Free a 'Handle', making it available to be claimed again. -freeHandle :: User -> Handle -> AppIO () -freeHandle u h = do +freeHandle :: UserId -> Handle -> AppIO () +freeHandle uid h = do retry x5 $ write handleDelete (params Quorum (Identity h)) let key = "@" <> fromHandle h - deleteClaim (userId u) key (30 # Minute) + deleteClaim uid key (30 # Minute) -- | Lookup the current owner of a 'Handle'. lookupHandle :: Handle -> AppIO (Maybe UserId) -lookupHandle h = - join . fmap runIdentity - <$> retry x1 (query1 handleSelect (params Quorum (Identity h))) +lookupHandle = lookupHandleWithPolicy Quorum -- | A weaker version of 'lookupHandle' that trades availability -- (and potentially speed) for the possibility of returning stale data. glimpseHandle :: Handle -> AppIO (Maybe UserId) -glimpseHandle h = +glimpseHandle = lookupHandleWithPolicy One + +{-# INLINE lookupHandleWithPolicy #-} +lookupHandleWithPolicy :: Consistency -> Handle -> AppIO (Maybe UserId) +lookupHandleWithPolicy policy h = do join . fmap runIdentity - <$> retry x1 (query1 handleSelect (params One (Identity h))) + <$> retry x1 (query1 handleSelect (params policy (Identity h))) -------------------------------------------------------------------------------- -- Queries diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 8ead38ef6b0..3549a3a1b41 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -220,7 +220,7 @@ idpGetRaw zusr idpid = do _ <- authorizeIdP zusr idp wrapMonadClient (Data.getIdPRawMetadata idpid) >>= \case Just txt -> pure $ RawIdPMetadata txt - Nothing -> throwSpar SparNotFound + Nothing -> throwSpar SparIdPNotFound idpGetAll :: Maybe UserId -> Spar IdPList idpGetAll zusr = withDebugLog "idpGetAll" (const Nothing) $ do @@ -329,7 +329,7 @@ validateNewIdP _idpMetadata teamId mReplaces = do oldIssuers :: [SAML.Issuer] <- case mReplaces of Nothing -> pure [] Just replaces -> do - idp <- wrapMonadClient (Data.getIdPConfig replaces) >>= maybe (throwSpar SparNotFound) pure + idp <- wrapMonadClient (Data.getIdPConfig replaces) >>= maybe (throwSpar SparIdPNotFound) pure pure $ (idp ^. SAML.idpMetadata . SAML.edIssuer) : (idp ^. SAML.idpExtraInfo . wiOldIssuers) let requri = _idpMetadata ^. SAML.edRequestURI _idpExtraInfo = WireIdP teamId oldIssuers Nothing @@ -440,7 +440,7 @@ internalPutSsoSettings SsoSettings {defaultSsoCode = Just code} = do -- this will return a 404, which is not quite right, -- but it's an internal endpoint and the message clearly says -- "Could not find IdP". - throwSpar SparNotFound + throwSpar SparIdPNotFound Just _ -> do wrapMonadClient $ Data.storeDefaultSsoCode code pure NoContent diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index 5dcfc3c01dd..d7f041c6794 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -130,10 +130,10 @@ instance SPStoreIdP SparError Spar where storeIdPConfig idp = wrapMonadClient $ Data.storeIdPConfig idp getIdPConfig :: IdPId -> Spar IdP - getIdPConfig = (>>= maybe (throwSpar SparNotFound) pure) . wrapMonadClientWithEnv . Data.getIdPConfig + getIdPConfig = (>>= maybe (throwSpar SparIdPNotFound) pure) . wrapMonadClientWithEnv . Data.getIdPConfig getIdPConfigByIssuer :: Issuer -> Spar IdP - getIdPConfigByIssuer = (>>= maybe (throwSpar SparNotFound) pure) . wrapMonadClientWithEnv . Data.getIdPConfigByIssuer + getIdPConfigByIssuer = (>>= maybe (throwSpar SparIdPNotFound) pure) . wrapMonadClientWithEnv . Data.getIdPConfigByIssuer -- | 'wrapMonadClient' with an 'Env' in a 'ReaderT', and exceptions. If you -- don't need either of those, 'wrapMonadClient' will suffice. @@ -169,7 +169,7 @@ getUser uref = do case muid of Nothing -> pure Nothing Just uid -> do - itis <- Intra.isTeamUser uid + itis <- isJust <$> Intra.getBrigUserTeam uid pure $ if itis then Just uid else Nothing -- | Create a fresh 'Data.Id.UserId', store it on C* locally together with 'SAML.UserRef', then @@ -245,11 +245,7 @@ bindUser buid userref = do (uteamid == Just teamid) (throwSpar . SparBindFromWrongOrNoTeam . cs . show $ uteamid) insertUser userref buid - Intra.bindBrigUser buid userref >>= \case - True -> pure buid - False -> do - SAML.logger SAML.Warn $ "SparBindUserDisappearedFromBrig: " <> show buid - throwSpar SparBindUserDisappearedFromBrig + buid <$ Intra.setBrigUserUserRef buid userref instance SPHandler SparError Spar where type NTCTX Spar = Env diff --git a/services/spar/src/Spar/Error.hs b/services/spar/src/Spar/Error.hs index f5c8ea6eb79..cc9f7273c79 100644 --- a/services/spar/src/Spar/Error.hs +++ b/services/spar/src/Spar/Error.hs @@ -61,14 +61,13 @@ throwSpar :: MonadError SparError m => SparCustomError -> m a throwSpar = throwError . SAML.CustomError data SparCustomError - = SparNotFound + = SparIdPNotFound | SparMissingZUsr | SparNotInTeam | SparNotTeamOwner | SparSSODisabled | SparInitLoginWithAuth | SparInitBindWithoutAuth - | SparBindUserDisappearedFromBrig | SparNoSuchRequest | SparNoRequestRefInResponse LT | SparCouldNotSubstituteSuccessURI LT @@ -162,14 +161,13 @@ renderSparError SAML.BadSamlResponseIssuerMissing = Right $ Wai.Error status400 renderSparError SAML.BadSamlResponseNoAssertions = Right $ Wai.Error status400 "bad-response-saml" ("Bad response: no assertions in AuthnResponse") renderSparError SAML.BadSamlResponseAssertionWithoutID = Right $ Wai.Error status400 "bad-response-saml" ("Bad response: assertion without ID") renderSparError (SAML.BadSamlResponseInvalidSignature msg) = Right $ Wai.Error status400 "bad-response-signature" (cs msg) -renderSparError (SAML.CustomError SparNotFound) = Right $ Wai.Error status404 "not-found" "Could not find IdP." +renderSparError (SAML.CustomError SparIdPNotFound) = Right $ Wai.Error status404 "not-found" "Could not find IdP." renderSparError (SAML.CustomError SparMissingZUsr) = Right $ Wai.Error status400 "client-error" "[header] 'Z-User' required" renderSparError (SAML.CustomError SparNotInTeam) = Right $ Wai.Error status403 "no-team-member" "Requesting user is not a team member or not a member of this team." renderSparError (SAML.CustomError SparNotTeamOwner) = Right $ Wai.Error status403 "insufficient-permissions" "You need to be a team owner." renderSparError (SAML.CustomError SparSSODisabled) = Right $ Wai.Error status403 "sso-disabled" "Please ask customer support to enable this feature for your team." renderSparError (SAML.CustomError SparInitLoginWithAuth) = Right $ Wai.Error status403 "login-with-auth" "This end-point is only for login, not binding." renderSparError (SAML.CustomError SparInitBindWithoutAuth) = Right $ Wai.Error status403 "bind-without-auth" "This end-point is only for binding, not login." -renderSparError (SAML.CustomError SparBindUserDisappearedFromBrig) = Right $ Wai.Error status404 "bind-user-disappeared" "Your user appears to have been deleted?" renderSparError SAML.UnknownError = Right $ Wai.Error status500 "server-error" "Unknown server error." renderSparError (SAML.BadServerConfig msg) = Right $ Wai.Error status500 "server-error" ("Error in server config: " <> msg) renderSparError (SAML.InvalidCert msg) = Right $ Wai.Error status500 "invalid-certificate" ("Error in idp certificate: " <> msg) diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index ed87edcd94b..1f2e579e707 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -34,11 +34,9 @@ module Spar.Intra.Brig setBrigUserUserRef, setBrigUserRichInfo, checkHandleAvailable, - bindBrigUser, deleteBrigUser, createBrigUser, updateEmail, - isTeamUser, getZUsrOwnedTeam, ensureReAuthorised, ssoLogin, @@ -359,19 +357,6 @@ checkHandleAvailable hnd buid = do | otherwise -> throwSpar . SparBrigError . cs $ "check handle failed with status " <> show sCode --- | This works under the assumption that the user must exist on brig. If it does not, brig --- responds with 404 and this function returns 'False'. --- --- See also: 'setBrigUserUserRef'. -bindBrigUser :: (HasCallStack, MonadSparToBrig m) => UserId -> SAML.UserRef -> m Bool -bindBrigUser uid (toUserSSOId -> ussoid) = do - resp <- - call $ - method PUT - . paths ["/i/users", toByteString' uid, "sso-id"] - . json ussoid - pure $ Bilge.statusCode resp < 300 - -- | Call brig to delete a user deleteBrigUser :: (HasCallStack, MonadSparToBrig m, MonadIO m) => UserId -> m () deleteBrigUser buid = do @@ -387,10 +372,6 @@ deleteBrigUser buid = do | otherwise -> throwSpar $ SparBrigError ("delete user failed with status " <> cs (show sCode)) --- | Check that a user id exists on brig and has a team id. -isTeamUser :: (HasCallStack, MonadSparToBrig m) => UserId -> m Bool -isTeamUser buid = isJust <$> getBrigUserTeam buid - -- | Check that a user id exists on brig and has a team id. getBrigUserTeam :: (HasCallStack, MonadSparToBrig m) => UserId -> m (Maybe TeamId) getBrigUserTeam buid = do diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index ef603b556dc..dfa849eaefd 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -386,11 +386,7 @@ updateValidScimUser tokinfo uid newScimUser = do lift . wrapMonadClient $ Data.deleteSAMLUser olduref lift . wrapMonadClient $ Data.insertSAMLUser newuref uid -- update 'SAML.UserRef' on brig - bindok <- lift $ Brig.bindBrigUser uid newuref - unless bindok . throwError $ - Scim.serverError "Failed to update SAML UserRef on brig." - -- this can only happen if user is found in spar.scim_user, but missing on brig. - -- (internal error? race condition?) + lift $ Brig.setBrigUserUserRef uid newuref -- TODO: if the user has been suspended or unsuspended in brig since the last scim -- write, we'll find the wrong information here. diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 50fe0812db0..30a95b1e4e1 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -288,7 +288,7 @@ specFinalizeLogin = do statusCode sparresp `shouldBe` 404 -- body should contain the error label in the title, the verbatim haskell error, and the request: (cs . fromJust . responseBody $ sparresp) `shouldContain` "wire:sso:error:not-found" - (cs . fromJust . responseBody $ sparresp) `shouldContainInBase64` "CustomError SparNotFound" + (cs . fromJust . responseBody $ sparresp) `shouldContainInBase64` "CustomError SparIdPNotFound" (cs . fromJust . responseBody $ sparresp) `shouldContainInBase64` "Input {iName = \"SAMLResponse\"" -- TODO(arianvp): Ask Matthias what this even means context "AuthnResponse does not match any request" $ do From c2a07f78b8f6659a5dfbe6b369caabc483cd5bb6 Mon Sep 17 00:00:00 2001 From: Tiago Loureiro Date: Fri, 4 Sep 2020 13:39:22 +0200 Subject: [PATCH 23/23] Update CHANGELOG --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56689513437..05411c73826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# 2020-09-04 + +## Release Notes + +## Bug Fixes + +* Fixed logic related to ephemeral users (#1197) + +## New Features + +* SFT servers now exposed over /calls/config/v2 (#1177) +* First federation endpoint (#1188) + +## Internal changes + +* ormolu upgrade to 0.1.2.0 and formatting (#1145, #1185, #1186) +* handy cqlsh make target to manually poke at the database (#1170) +* spar cleanup +* brig user name during scim user parsing (#1195) +* invitation refactor (#1196) +* SCIM users are never ephemeral (#1198) + + # 2020-07-29 ## Release Notes