diff --git a/.gitignore b/.gitignore index 45911297b0e..28cbc6beab1 100644 --- a/.gitignore +++ b/.gitignore @@ -63,5 +63,14 @@ swagger-ui deploy/services-demo/resources/templates/* deploy/services-demo/conf/nginz/zwagger-ui/* +deploy/docker-ephemeral/build/airdock_base-all/ +deploy/docker-ephemeral/build/airdock_base/ +deploy/docker-ephemeral/build/airdock_fakesqs-all/ +deploy/docker-ephemeral/build/airdock_fakesqs/ +deploy/docker-ephemeral/build/airdock_rvm-all/ +deploy/docker-ephemeral/build/airdock_rvm/ +deploy/docker-ephemeral/build/dynamodb_local/ +deploy/docker-ephemeral/build/smtp/ + # Ignore cabal files; use package.yaml instead *.cabal diff --git a/CHANGELOG.md b/CHANGELOG.md index b30badcd88c..dbd92c353f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,28 @@ -# 2019-09-03 +# 2019-09-16 #858 + +## Relevant for self-hosters + +- Documentation changes for Twilio configurations and TURN setup. (#775) + +## Relevant for client developers + +- Better events for deletion of team conversations (also send `conversation.delete` to team members) (#849) +- Add a new type of authorization tokens for legalhold (for details on legalhold, see https://github.com/wireapp/wire-server/blob/develop/docs/reference/team/legalhold.md) (#761) + +## Bug fixes + +- Fix swagger docs. (#852) +- Fix intra call in stern (aka customer support, aka backoffice) (#844) + +## Internal Changes + +- Change feature flags from boolean to custom enum types. (#850) +- Fix flaky integration test. (#848) +- Cleanup: incoherent functions for response body parsing. (#847) +- add route for consistency (#851) + + +# 2019-09-03 #843 ## Relevant for self-hosters diff --git a/deploy/services-demo/conf/brig.demo.yaml b/deploy/services-demo/conf/brig.demo.yaml index daac6c80659..4bb1ff5bf55 100644 --- a/deploy/services-demo/conf/brig.demo.yaml +++ b/deploy/services-demo/conf/brig.demo.yaml @@ -84,6 +84,8 @@ zauth: sessionTokenTimeout: 604800 # 7 days accessTokenTimeout: 900 # 15 minutes providerTokenTimeout: 604800 # 7 days + legalHoldUserTokenTimeout: 4838400 # 56 days + legalHoldSessionTokenTimeout: 604800 # 7 days turn: serversV2: resources/turn/servers-v2.txt diff --git a/deploy/services-demo/conf/nginz/nginx-docker.conf b/deploy/services-demo/conf/nginz/nginx-docker.conf index 4c52e9a1e1c..a2a89eb7066 100644 --- a/deploy/services-demo/conf/nginz/nginx-docker.conf +++ b/deploy/services-demo/conf/nginz/nginx-docker.conf @@ -114,6 +114,11 @@ http { return 200; } + location /i/status { + zauth off; + return 200; + } + location /vts { zauth off; vhost_traffic_status_display; diff --git a/deploy/services-demo/conf/nginz/nginx.conf b/deploy/services-demo/conf/nginz/nginx.conf index a3eadce8380..94d6f96eec8 100644 --- a/deploy/services-demo/conf/nginz/nginx.conf +++ b/deploy/services-demo/conf/nginz/nginx.conf @@ -113,6 +113,11 @@ http { return 200; } + location /i/status { + zauth off; + return 200; + } + location /vts { zauth off; vhost_traffic_status_display; diff --git a/deploy/services-demo/conf/nginz/zauth_acl.txt b/deploy/services-demo/conf/nginz/zauth_acl.txt index 9cefc22eeea..9498b8cc43f 100644 --- a/deploy/services-demo/conf/nginz/zauth_acl.txt +++ b/deploy/services-demo/conf/nginz/zauth_acl.txt @@ -4,10 +4,14 @@ a (blacklist (path "/provider") (path "/bot/**") (path "/i/**")) -u (whitelist (path "/access")) - b (whitelist (path "/bot") (path "/bot/**")) p (whitelist (path "/provider") (path "/provider/**")) + +# LegalHold Access Tokens +la (whitelist (path "/notifications") + (path "/assets/v3/**") + (path "/users") + (path "/users/**")) diff --git a/docs/reference/config-options.md b/docs/reference/config-options.md new file mode 100644 index 00000000000..d3bef084d24 --- /dev/null +++ b/docs/reference/config-options.md @@ -0,0 +1,50 @@ +# Config Options {#RefConfigOptions} + +Fragment. + +This page is about the yaml files that determine the configuration of +the Wire backend services. + + +## Feature flags + +Feature flags can be used to turn features on or off, or determine the +behavior of the features. Example: + +``` +# [galley.yaml] +settings: + featureFlags: + sso: disabled-by-default + legalhold: disabled-by-default +``` + +The `featureFlags` field in the galley settings is mandatory, and all +features must be listed. Each feature defines its own set of allowed +flag values. (The reason for that is that as we will see, the +semantics is slightly different (or more specific) than boolean.) + +### SSO + +This sets the default setting for all teams, and can be overridden by +customer support / backoffice. [Allowed +values](https://github.com/wireapp/wire-server/blob/46713382a1a6544de3936eb03e987b9f76df3faa/libs/galley-types/src/Galley/Types/Teams.hs#L327-L329): +`disabled-by-default`, `enabled-by-default`. + +IMPORTANT: if you change this from 'enabled-by-default' to +'disabled-by-default' in production, you need to run [this migration +script](https://github.com/wireapp/wire-server/tree/master/tools/db/migrate-sso-feature-flag) +to fix all teams that have registered an idp. (if you don't, the idp +will keep working, but the admin won't be able to register new idps.) + +### LegalHold + +Optionally block customer support / backoffice from enabling legal +hold for individual teams. [Allowed +values](https://github.com/wireapp/wire-server/blob/46713382a1a6544de3936eb03e987b9f76df3faa/libs/galley-types/src/Galley/Types/Teams.hs#L332-L334): +'disabled-permanently', 'disabled-by-default'. + +IMPORTANT: If you switch this back to `disabled-permanently` from +`disabled-by-default`, LegalHold devices may still be active in teams +that have created them while it was allowed. This may change in the +future. diff --git a/libs/api-client/src/Network/Wire/Client/API/Auth.hs b/libs/api-client/src/Network/Wire/Client/API/Auth.hs index ab258072174..a813d5cc5fc 100644 --- a/libs/api-client/src/Network/Wire/Client/API/Auth.hs +++ b/libs/api-client/src/Network/Wire/Client/API/Auth.hs @@ -15,6 +15,7 @@ import Bilge import Brig.Types.User.Auth as Auth hiding (Cookie, user) import Control.Monad.Catch (MonadMask) import Data.List.NonEmpty +import Data.Text (pack) import Data.Time (getCurrentTime) import Network.HTTP.Client (generateCookie) import Network.HTTP.Types.Method @@ -95,7 +96,7 @@ tokenResponse rq rs ck where mkAuth = do cok <- mkCookie $ parseSetCookie <$> getHeader "Set-Cookie" rs - tok <- fromBody rs + tok <- responseJsonThrow (ParseError . pack) rs return . Just $ Auth cok tok mkCookie Nothing = maybe (unexpected rs "missing set-cookie") return ck diff --git a/libs/api-client/src/Network/Wire/Client/API/Conversation.hs b/libs/api-client/src/Network/Wire/Client/API/Conversation.hs index 76df77ff0fd..ab6ad42471a 100644 --- a/libs/api-client/src/Network/Wire/Client/API/Conversation.hs +++ b/libs/api-client/src/Network/Wire/Client/API/Conversation.hs @@ -13,16 +13,19 @@ module Network.Wire.Client.API.Conversation import Imports import Bilge +import Control.Monad.Catch (MonadThrow) import Data.ByteString.Conversion import Data.Id import Data.List.NonEmpty hiding (cons, toList) import Data.List1 +import Data.Text (pack) import Galley.Types as M hiding (Event, EventType) import Network.HTTP.Types.Method import Network.HTTP.Types.Status hiding (statusCode) import Network.Wire.Client.HTTP import Network.Wire.Client.Session import Network.Wire.Client.API.Push (ConvEvent) +import Network.Wire.Client.Monad (ClientException(ParseError)) postOtrMessage :: MonadSession m => ConvId -> NewOtrMessage -> m ClientMismatch postOtrMessage cnv msg = sessionRequest req rsc readBody @@ -40,11 +43,11 @@ postOtrMessage cnv msg = sessionRequest req rsc readBody -- If some users can not be added to the conversation, 'UnexpectedResponse' -- will be thrown. It's not possible that some users will be added and -- others will not. -addMembers :: MonadSession m => ConvId -> List1 UserId -> m (Maybe (ConvEvent Members)) +addMembers :: (MonadSession m, MonadThrow m) => ConvId -> List1 UserId -> m (Maybe (ConvEvent Members)) addMembers cnv mems = do rs <- sessionRequest req rsc consumeBody case statusCode rs of - 200 -> Just <$> fromBody rs + 200 -> Just <$> responseJsonThrow (ParseError . pack) rs 204 -> return Nothing _ -> unexpected rs "addMembers: status code" where @@ -57,11 +60,11 @@ addMembers cnv mems = do -- | Remove a user and (in case of success) return the event corresponding -- to the user removal. -removeMember :: MonadSession m => ConvId -> UserId -> m (Maybe (ConvEvent Members)) +removeMember :: (MonadSession m, MonadThrow m) => ConvId -> UserId -> m (Maybe (ConvEvent Members)) removeMember cnv mem = do rs <- sessionRequest req rsc consumeBody case statusCode rs of - 200 -> Just <$> fromBody rs + 200 -> Just <$> responseJsonThrow (ParseError . pack) rs 204 -> return Nothing _ -> unexpected rs "removeMember: status code" where @@ -81,11 +84,11 @@ memberUpdate cnv updt = sessionRequest req rsc (const $ return ()) $ empty rsc = status200 :| [] -getConv :: MonadSession m => ConvId -> m (Maybe Conversation) +getConv :: (MonadSession m, MonadThrow m) => ConvId -> m (Maybe Conversation) getConv cnv = do rs <- sessionRequest req rsc consumeBody case statusCode rs of - 200 -> fromBody rs + 200 -> responseJsonThrow (ParseError . pack) rs 404 -> return Nothing _ -> unexpected rs "getConv: status code" where diff --git a/libs/api-client/src/Network/Wire/Client/API/Push.hs b/libs/api-client/src/Network/Wire/Client/API/Push.hs index 947832b316c..3425b3df6a0 100644 --- a/libs/api-client/src/Network/Wire/Client/API/Push.hs +++ b/libs/api-client/src/Network/Wire/Client/API/Push.hs @@ -29,11 +29,13 @@ import Brig.Types import Control.Concurrent (myThreadId) import Control.Concurrent.Async import Control.Exception (bracket, finally, onException) +import Control.Monad.Catch (MonadThrow) import Data.Aeson hiding (Error) import Data.Aeson.Types (Parser) import Data.Default.Class import Data.Id import Data.List.NonEmpty +import Data.Text (pack) import Data.Time.Clock import Data.UUID (UUID, fromString) import Galley.Types hiding (Event, EventType) @@ -103,14 +105,14 @@ awaitNotifications f = do readChunk c = (\x -> if C.null x then Nothing else Just x) <$> connectionGetChunk c writeChunk c = maybe (return ()) (connectionPut c . L.toStrict) -fetchNotifications :: MonadSession m +fetchNotifications :: (MonadSession m, MonadThrow m) => Maybe ByteString -> m (Bool, [Notification]) fetchNotifications snc = do rs <- sessionRequest req rsc consumeBody case statusCode rs of - 200 -> (True,) <$> fromBody rs - 404 -> (False,) <$> fromBody rs + 200 -> (True,) <$> responseJsonThrow (ParseError . pack) rs + 404 -> (False,) <$> responseJsonThrow (ParseError . pack) rs _ -> unexpected rs "fetch: status code" where req = method GET @@ -120,11 +122,11 @@ fetchNotifications snc = do $ empty rsc = status200 :| [status404] -lastNotification :: MonadSession m => m (Maybe Notification) +lastNotification :: (MonadSession m, MonadThrow m) => m (Maybe Notification) lastNotification = do rs <- sessionRequest req rsc consumeBody case statusCode rs of - 200 -> Just <$> fromBody rs + 200 -> Just <$> responseJsonThrow (ParseError . pack) rs 404 -> return Nothing _ -> unexpected rs "last: status code" where diff --git a/libs/api-client/src/Network/Wire/Client/API/User.hs b/libs/api-client/src/Network/Wire/Client/API/User.hs index 11ce492b676..618c052fce4 100644 --- a/libs/api-client/src/Network/Wire/Client/API/User.hs +++ b/libs/api-client/src/Network/Wire/Client/API/User.hs @@ -18,6 +18,7 @@ import Control.Monad.Catch (MonadMask) import Data.ByteString.Conversion import Data.Id import Data.List.NonEmpty +import Data.Text (pack) import Network.HTTP.Types.Method import Network.HTTP.Types.Status hiding (statusCode) import Network.Wire.Client.HTTP @@ -95,7 +96,7 @@ getConnection :: (MonadSession m, MonadUnliftIO m, MonadMask m) => UserId -> m ( getConnection u = do rs <- sessionRequest req rsc consumeBody case statusCode rs of - 200 -> fromBody rs + 200 -> responseJsonThrow (ParseError . pack) rs 404 -> return Nothing _ -> unexpected rs "getConnection: status code" where diff --git a/libs/api-client/src/Network/Wire/Client/HTTP.hs b/libs/api-client/src/Network/Wire/Client/HTTP.hs index 5fbdec3bf47..5cc069bd8af 100644 --- a/libs/api-client/src/Network/Wire/Client/HTTP.hs +++ b/libs/api-client/src/Network/Wire/Client/HTTP.hs @@ -4,7 +4,6 @@ module Network.Wire.Client.HTTP ( clientRequest , readBody - , fromBody , unexpected , mkErrorResponse ) where @@ -77,16 +76,8 @@ clientRequest rq expected f = do ------------------------------------------------------------------------------- -- Utilities -readBody :: FromJSON a => Response BodyReader -> IO a -readBody = consumeBody >=> fromBody - -fromBody :: (MonadIO m, FromJSON a) => Response (Maybe Lazy.ByteString) -> m a -fromBody = either (liftIO . throwIO . ParseError . ("fromBody: "<>)) return . parse - where - parse = maybe (Left "missing response body") - (fmapL pack . eitherDecode) - . - responseBody +readBody :: (Typeable a, FromJSON a) => Response BodyReader -> IO a +readBody = consumeBody >=> responseJsonThrow (ParseError . pack) unexpected :: MonadIO m => Response a -> Text -> m b unexpected r = liftIO . throwIO . UnexpectedResponse (responseStatus r) (responseHeaders r) diff --git a/libs/bilge/src/Bilge/Response.hs b/libs/bilge/src/Bilge/Response.hs index 67f4371b6a6..5fd84bb8447 100644 --- a/libs/bilge/src/Bilge/Response.hs +++ b/libs/bilge/src/Bilge/Response.hs @@ -12,21 +12,32 @@ module Bilge.Response -- * Re-exports , Response + , ResponseLBS , responseStatus , responseHeaders , responseVersion , responseBody - , responseJson + , responseJsonEither + , responseJsonMaybe + , responseJsonThrow + , responseJsonError + , responseJsonUnsafe + , responseJsonUnsafeWithMsg ) where import Imports +import Control.Exception (ErrorCall(ErrorCall)) import Control.Lens +import Control.Monad.Catch import Data.Aeson (FromJSON, eitherDecode) import Data.CaseInsensitive (original) +import Data.EitherR (fmapL) +import Data.Typeable (typeRep) import Network.HTTP.Client import Network.HTTP.Types (HeaderName, httpMajor, httpMinor) import Web.Cookie +import qualified Data.Proxy import qualified Data.ByteString.Char8 as C import qualified Network.HTTP.Types as HTTP @@ -60,6 +71,55 @@ getCookieValue cookieName resp = . to setCookieValue -- extract the cookie value +type ResponseLBS = Response (Maybe LByteString) + +{-# INLINE responseJsonEither #-} +responseJsonEither + :: forall a. (HasCallStack, Typeable a, FromJSON a) + => ResponseLBS -> Either String a +responseJsonEither = fmapL addTypeInfo . eitherDecode <=< maybe err pure . responseBody + where + err :: Either String void + err = Left "Missing response body." + + addTypeInfo :: String -> String + addTypeInfo = ((show (typeRep (Data.Proxy.Proxy @a)) <> " ") <>) + +{-# INLINE responseJsonMaybe #-} +responseJsonMaybe + :: (HasCallStack, Typeable a, FromJSON a) + => ResponseLBS -> Maybe a +responseJsonMaybe = either (const Nothing) Just . responseJsonEither + +{-# INLINE responseJsonThrow #-} +responseJsonThrow + :: (HasCallStack, MonadThrow m, Typeable a, FromJSON a, Exception e) + => (String -> e) -> ResponseLBS -> m a +responseJsonThrow mkErr = either (throwM . mkErr) pure . responseJsonEither + +{-# INLINE responseJsonError #-} +responseJsonError + :: (HasCallStack, MonadThrow m, Typeable a, FromJSON a) + => ResponseLBS -> m a +responseJsonError = responseJsonThrow ErrorCall + +{-# INLINE responseJsonUnsafe #-} +responseJsonUnsafe + :: (HasCallStack, Typeable a, FromJSON a) + => ResponseLBS -> a +responseJsonUnsafe = responseJsonUnsafeWithMsg "" + +{-# INLINE responseJsonUnsafeWithMsg #-} +responseJsonUnsafeWithMsg + :: (HasCallStack, Typeable a, FromJSON a) + => String -> ResponseLBS -> a +responseJsonUnsafeWithMsg userErr = either err id . responseJsonEither + where + err parserErr = error . intercalate " " $ + [ "responseJsonUnsafeWithMsg:" ] <> + [ userErr | not $ null userErr ] <> + [ parserErr ] + showResponse :: Show a => Response a -> String showResponse r = showString "HTTP/" . shows (httpMajor . responseVersion $ r) @@ -77,9 +137,3 @@ showResponse r = showString "HTTP/" where showHeaders = foldl' (.) (showString "") (map showHdr (responseHeaders r)) showHdr (k, v) = showString . C.unpack $ original k <> ": " <> v <> "\n" - - -responseJson :: FromJSON a => Response (Maybe LByteString) -> Either String a -responseJson resp = case responseBody resp of - Nothing -> Left "no body" - Just raw -> eitherDecode raw diff --git a/libs/brig-types/src/Brig/Types/User/Auth.hs b/libs/brig-types/src/Brig/Types/User/Auth.hs index 04d19febfd8..69e2aee5643 100644 --- a/libs/brig-types/src/Brig/Types/User/Auth.hs +++ b/libs/brig-types/src/Brig/Types/User/Auth.hs @@ -51,6 +51,12 @@ data Login data SsoLogin = SsoLogin !UserId !(Maybe CookieLabel) +-- | A special kind of login that is only used for an internal endpoint. +-- This kind of login returns restricted 'LegalHoldUserToken's instead of regular +-- tokens. +data LegalHoldLogin + = LegalHoldLogin !UserId !(Maybe PlainTextPassword) !(Maybe CookieLabel) + loginLabel :: Login -> Maybe CookieLabel loginLabel (PasswordLogin _ _ l) = l loginLabel (SmsLogin _ _ l) = l @@ -107,6 +113,18 @@ instance ToJSON SsoLogin where toJSON (SsoLogin uid label) = object [ "user" .= uid, "label" .= label ] +instance FromJSON LegalHoldLogin where + parseJSON = withObject "LegalHoldLogin" $ \o -> + LegalHoldLogin <$> o .: "user" + <*> o .:? "password" + <*> o .:? "label" + +instance ToJSON LegalHoldLogin where + toJSON (LegalHoldLogin uid password label) = + object [ "user" .= uid + , "password" .= password + , "label" .= label ] + instance FromJSON PendingLoginCode where parseJSON = withObject "PendingLoginCode" $ \o -> PendingLoginCode <$> o .: "code" diff --git a/libs/brig-types/test/unit/Test/Brig/Types/Common.hs b/libs/brig-types/test/unit/Test/Brig/Types/Common.hs index f03cf38f5d0..82ea0801b70 100644 --- a/libs/brig-types/test/unit/Test/Brig/Types/Common.hs +++ b/libs/brig-types/test/unit/Test/Brig/Types/Common.hs @@ -85,8 +85,6 @@ instance Arbitrary SSOTeamConfig where arbitrary = SSOTeamConfig <$> arbitrary instance Arbitrary FeatureFlags where - arbitrary = FeatureFlags <$> arbitrary - shrink (FeatureFlags ls) = FeatureFlags <$> shrink ls - -instance Arbitrary FeatureFlag where - arbitrary = Test.Tasty.QuickCheck.elements [minBound..] + arbitrary = FeatureFlags + <$> Test.Tasty.QuickCheck.elements [minBound..] + <*> Test.Tasty.QuickCheck.elements [minBound..] diff --git a/libs/galley-types/package.yaml b/libs/galley-types/package.yaml index 4510aadf5c3..2c1ff0174aa 100644 --- a/libs/galley-types/package.yaml +++ b/libs/galley-types/package.yaml @@ -27,6 +27,7 @@ library: - exceptions >=0.10.0 - lens >=4.12 - protobuf >=0.2 + - string-conversions - swagger >=0.1 - text >=0.11 - time >=1.4 diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 29e93e6431d..471f128592c 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -21,8 +21,9 @@ module Galley.Types.Teams , TeamCreationTime (..) , tcTime - , FeatureFlags(..) - , FeatureFlag(..) + , FeatureFlags(..), flagSSO, flagLegalHold + , FeatureSSO(..) + , FeatureLegalHold(..) , TeamList , newTeamList @@ -128,6 +129,7 @@ import Data.Id (TeamId, ConvId, UserId) import Data.Json.Util import Data.Misc (PlainTextPassword (..)) import Data.Range +import Data.String.Conversions (cs) import Data.Time (UTCTime) import Data.LegalHold (UserLegalHoldStatus(..)) import Galley.Types.Teams.Internal @@ -316,24 +318,50 @@ newtype TeamCreationTime = TeamCreationTime { _tcTime :: Int64 } -newtype FeatureFlags = FeatureFlags (Set FeatureFlag) +data FeatureFlags = FeatureFlags + { _flagSSO :: !FeatureSSO + , _flagLegalHold :: !FeatureLegalHold + } deriving (Eq, Show, Generic) -data FeatureFlag = FeatureSSO | FeatureLegalHold +data FeatureSSO + = FeatureSSOEnabledByDefault + | FeatureSSODisabledByDefault + deriving (Eq, Ord, Show, Enum, Bounded, Generic) + +data FeatureLegalHold + = FeatureLegalHoldDisabledPermanently + | FeatureLegalHoldDisabledByDefault deriving (Eq, Ord, Show, Enum, Bounded, Generic) instance FromJSON FeatureFlags where - parseJSON = withObject "FeatureFlags" $ \obj -> do - sso <- fromMaybe False <$> obj .:? "sso" - legalhold <- fromMaybe False <$> obj .:? "legalhold" - pure . FeatureFlags . Set.fromList $ - [ FeatureSSO | sso ] <> - [ FeatureLegalHold | legalhold ] + parseJSON = withObject "FeatureFlags" $ \obj -> FeatureFlags + <$> (obj .: "sso") + <*> (obj .: "legalhold") instance ToJSON FeatureFlags where - toJSON (FeatureFlags flags) = object $ - [ "sso" .= (FeatureSSO `elem` flags) ] <> - [ "legalhold" .= (FeatureLegalHold `elem` flags) ] + toJSON (FeatureFlags sso legalhold) = object $ + [ "sso" .= sso + , "legalhold" .= legalhold + ] + +instance FromJSON FeatureSSO where + parseJSON (String "enabled-by-default") = pure FeatureSSOEnabledByDefault + parseJSON (String "disabled-by-default") = pure FeatureSSODisabledByDefault + parseJSON bad = fail $ "FeatureSSO: " <> cs (encode bad) + +instance ToJSON FeatureSSO where + toJSON FeatureSSOEnabledByDefault = String "enabled-by-default" + toJSON FeatureSSODisabledByDefault = String "disabled-by-default" + +instance FromJSON FeatureLegalHold where + parseJSON (String "disabled-permanently") = pure $ FeatureLegalHoldDisabledPermanently + parseJSON (String "disabled-by-default") = pure $ FeatureLegalHoldDisabledByDefault + parseJSON bad = fail $ "FeatureLegalHold: " <> cs (encode bad) + +instance ToJSON FeatureLegalHold where + toJSON FeatureLegalHoldDisabledPermanently = String "disabled-permanently" + toJSON FeatureLegalHoldDisabledByDefault = String "disabled-by-default" newTeam :: TeamId -> UserId -> Text -> Text -> TeamBinding -> Team newTeam tid uid nme ico bnd = Team tid uid nme ico Nothing bnd @@ -404,6 +432,7 @@ makeLenses ''TeamUpdateData makeLenses ''TeamMemberDeleteData makeLenses ''TeamDeleteData makeLenses ''TeamCreationTime +makeLenses ''FeatureFlags -- Note [hidden team roles] diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index 253109d5364..c213abd4392 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -143,6 +143,7 @@ import Control.Monad.IO.Unlift import Control.DeepSeq (NFData(..), deepseq) + ---------------------------------------------------------------------------- -- Type aliases diff --git a/libs/libzauth/libzauth-c/Cargo.lock b/libs/libzauth/libzauth-c/Cargo.lock index f46bcc98c48..c5d68fd722b 100644 --- a/libs/libzauth/libzauth-c/Cargo.lock +++ b/libs/libzauth/libzauth-c/Cargo.lock @@ -1,3 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. [[package]] name = "asexp" version = "0.3.1" diff --git a/libs/libzauth/libzauth-c/src/lib.rs b/libs/libzauth/libzauth-c/src/lib.rs index 7a688af6d84..1b87759fefb 100644 --- a/libs/libzauth/libzauth-c/src/lib.rs +++ b/libs/libzauth/libzauth-c/src/lib.rs @@ -163,21 +163,25 @@ pub extern fn zauth_token_delete(t: *mut ZauthToken) { #[no_mangle] #[derive(Clone, Copy, Debug)] pub enum ZauthTokenType { - User = 0, - Bot = 1, - Access = 2, - Provider = 4, - Unknown = 3 + User = 0, + Bot = 1, + Access = 2, + Provider = 4, + LegalHoldUser = 5, + LegalHoldAccess = 6, + Unknown = 3 } impl From for ZauthTokenType { fn from(t: TokenType) -> ZauthTokenType { match t { - TokenType::User => ZauthTokenType::User, - TokenType::Access => ZauthTokenType::Access, - TokenType::Bot => ZauthTokenType::Bot, - TokenType::Provider => ZauthTokenType::Provider, - TokenType::Unknown => ZauthTokenType::Unknown + TokenType::User => ZauthTokenType::User, + TokenType::Access => ZauthTokenType::Access, + TokenType::Bot => ZauthTokenType::Bot, + TokenType::Provider => ZauthTokenType::Provider, + TokenType::LegalHoldUser => ZauthTokenType::LegalHoldUser, + TokenType::LegalHoldAccess => ZauthTokenType::LegalHoldAccess, + TokenType::Unknown => ZauthTokenType::Unknown } } } diff --git a/libs/libzauth/libzauth-c/src/zauth.h b/libs/libzauth/libzauth-c/src/zauth.h index be6e7245701..e8878e36092 100644 --- a/libs/libzauth/libzauth-c/src/zauth.h +++ b/libs/libzauth/libzauth-c/src/zauth.h @@ -30,11 +30,13 @@ typedef enum { } ZauthResult; typedef enum { - ZAUTH_TOKEN_TYPE_USER = 0, - ZAUTH_TOKEN_TYPE_BOT = 1, - ZAUTH_TOKEN_TYPE_ACCESS = 2, - ZAUTH_TOKEN_TYPE_UNKNOWN = 3, - ZAUTH_TOKEN_TYPE_PROVIDER = 4, + ZAUTH_TOKEN_TYPE_USER = 0, + ZAUTH_TOKEN_TYPE_BOT = 1, + ZAUTH_TOKEN_TYPE_ACCESS = 2, + ZAUTH_TOKEN_TYPE_UNKNOWN = 3, + ZAUTH_TOKEN_TYPE_PROVIDER = 4, + ZAUTH_TOKEN_TYPE_LEGAL_HOLD_USER = 5, + ZAUTH_TOKEN_TYPE_LEGAL_HOLD_ACCESS = 6, } ZauthTokenType; typedef struct ZauthAcl ZauthAcl; diff --git a/libs/libzauth/libzauth/src/acl.rs b/libs/libzauth/libzauth/src/acl.rs index fa0f3adecfc..d83d6558069 100644 --- a/libs/libzauth/libzauth/src/acl.rs +++ b/libs/libzauth/libzauth/src/acl.rs @@ -129,6 +129,9 @@ mod tests { b (whitelist (path "/conversation/message") (path "/foo/bar/*")) + # this is a comment that should not lead to a parse failure. + la (whitelist (path "/legalhold/**")) + x (blacklist ()) y (whitelist ()) @@ -151,5 +154,7 @@ mod tests { assert!(acl.allowed("x", "/everywhere")); assert!(acl.allowed("x", "/")); assert!(!acl.allowed("y", "/nowhere")); + assert!(acl.allowed("la", "/legalhold/something")); + assert!(!acl.allowed("la", "/mistyped/something")); } } diff --git a/libs/libzauth/libzauth/src/zauth.rs b/libs/libzauth/libzauth/src/zauth.rs index 8d1d3a1cf83..42eec64f147 100644 --- a/libs/libzauth/libzauth/src/zauth.rs +++ b/libs/libzauth/libzauth/src/zauth.rs @@ -48,6 +48,8 @@ pub enum TokenType { Access, Bot, Provider, + LegalHoldUser, + LegalHoldAccess, Unknown } @@ -55,11 +57,13 @@ impl FromStr for TokenType { type Err = (); fn from_str(s: &str) -> Result { match s { - "u" => Ok(TokenType::User), - "a" => Ok(TokenType::Access), - "b" => Ok(TokenType::Bot), - "p" => Ok(TokenType::Provider), - _ => Ok(TokenType::Unknown) + "u" => Ok(TokenType::User), + "a" => Ok(TokenType::Access), + "b" => Ok(TokenType::Bot), + "p" => Ok(TokenType::Provider), + "lu" => Ok(TokenType::LegalHoldUser), + "la" => Ok(TokenType::LegalHoldAccess), + _ => Ok(TokenType::Unknown) } } } @@ -137,7 +141,7 @@ impl<'r> Token<'r> { pub fn verify(&self, store: &Keystore) -> Result<(), Error> { match self.token_type { - TokenType::Access | TokenType::User | TokenType::Provider => + TokenType::Access | TokenType::User | TokenType::Provider | TokenType::LegalHoldUser | TokenType::LegalHoldAccess => if self.is_expired() { Err(Error::Expired) } else { @@ -155,11 +159,13 @@ impl<'r> Token<'r> { pub fn has_access(&self, acl: &Acl, path: &str) -> bool { match self.token_type { - TokenType::User => acl.allowed("u", path), - TokenType::Access => acl.allowed("a", path), - TokenType::Bot => acl.allowed("b", path), - TokenType::Provider => acl.allowed("p", path), - TokenType::Unknown => false + TokenType::User => acl.allowed("u", path), + TokenType::Access => acl.allowed("a", path), + TokenType::Bot => acl.allowed("b", path), + TokenType::Provider => acl.allowed("p", path), + TokenType::LegalHoldUser => acl.allowed("lu", path), + TokenType::LegalHoldAccess => acl.allowed("la", path), + TokenType::Unknown => false } } @@ -203,6 +209,12 @@ mod tests { const PROVIDER_TOKEN: &'static str = "qcJ9zxFHMaiqj-tauhywI435BBs8t6wFyXAShkSQqaHK9r36k012rJYJIE7TTCHlFaGOzsk6E7h5G8JkLVjFDg==.v=1.k=1.d=1467640768.t=p.l=.p=84fa6cbf-0845-42cf-93b5-1e2195c68e11"; + const LEGAL_HOLD_ACCESS_TOKEN: &'static str = + "6wca6kIO7_SFAev_Pl2uS6cBdkKuGk6MIh8WBK_ivZnwtRVrXF2pEHiocUWQZDy8YTrEweTJrqxUDptA7M1SBA==.v=1.k=1.d=1558361639.t=la.l=.u=4763099d-ab9b-4720-a1a3-558877f8b3e2.c=17967041325642812284"; + + const LEGAL_HOLD_USER_TOKEN: &'static str = + "GsydW1LQvwGYBGFErvqcqJvcipumtcdfVL4Li83KwR1ucnm-IrPM40SKl9Rhsdv0sqF_MF_eyTqMe_XpXR81Cg==.v=1.k=1.d=1558361914.t=lu.l=.u=ca754009-bc1e-4ef7-8384-dfec056bcc97.r=64"; + #[test] fn parse_access() { let t = Token::parse(ACCESS_TOKEN).unwrap(); @@ -267,4 +279,31 @@ mod tests { assert_eq!(t.token_type, TokenType::Provider); assert_eq!(t.lookup('p'), Some("84fa6cbf-0845-42cf-93b5-1e2195c68e11")) } + + #[test] + fn parse_legal_hold_access() { + let t = Token::parse(LEGAL_HOLD_ACCESS_TOKEN).unwrap(); + assert_eq!(t.signature.0[..], "6wca6kIO7_SFAev_Pl2uS6cBdkKuGk6MIh8WBK_ivZnwtRVrXF2pEHiocUWQZDy8YTrEweTJrqxUDptA7M1SBA==".from_base64().unwrap()[..]); + assert_eq!(t.version, 1); + assert_eq!(t.key_idx, 1); + assert_eq!(t.timestamp, 1558361639); + assert_eq!(t.token_tag, None); + assert_eq!(t.token_type, TokenType::LegalHoldAccess); + assert_eq!(t.lookup('u'), Some("4763099d-ab9b-4720-a1a3-558877f8b3e2")); + assert_eq!(t.lookup('c'), Some("17967041325642812284")) + } + + #[test] + fn parse_legal_hold_user() { + let t = Token::parse(LEGAL_HOLD_USER_TOKEN).unwrap(); + assert_eq!(t.signature.0[..], "GsydW1LQvwGYBGFErvqcqJvcipumtcdfVL4Li83KwR1ucnm-IrPM40SKl9Rhsdv0sqF_MF_eyTqMe_XpXR81Cg==".from_base64().unwrap()[..]); + assert_eq!(t.version, 1); + assert_eq!(t.key_idx, 1); + assert_eq!(t.timestamp, 1558361914); + assert_eq!(t.token_tag, None); + assert_eq!(t.token_type, TokenType::LegalHoldUser); + assert_eq!(t.lookup('u'), Some("ca754009-bc1e-4ef7-8384-dfec056bcc97")); + assert_eq!(t.lookup('r'), Some("64")) + } + } diff --git a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs index fd143223ecc..def34a070fb 100644 --- a/libs/tasty-cannon/src/Test/Tasty/Cannon.hs +++ b/libs/tasty-cannon/src/Test/Tasty/Cannon.hs @@ -53,6 +53,7 @@ import Data.Aeson (decodeStrict', FromJSON, fromJSON, Value (..)) import Data.ByteString.Conversion import Data.Id import Data.List1 +import Data.Misc ((<$$>)) import Data.Timeout (Timeout, TimeoutUnit (..), (#)) import Gundeck.Types import Network.HTTP.Client @@ -250,7 +251,14 @@ awaitMatchN :: (HasCallStack, MonadIO m) -> [WebSocket] -> (Notification -> Assertion) -> m [Either MatchTimeout Notification] -awaitMatchN t wss f = liftIO $ mapConcurrently (\ws -> awaitMatch t ws f) wss +awaitMatchN t wss f = snd <$$> awaitMatchN' t (((),) <$> wss) f + +awaitMatchN' :: (HasCallStack, MonadIO m) + => Timeout + -> [(extra, WebSocket)] + -> (Notification -> Assertion) + -> m [(extra, Either MatchTimeout Notification)] +awaitMatchN' t wss f = liftIO $ mapConcurrently (\(extra, ws) -> (extra,) <$> awaitMatch t ws f) wss assertMatchN :: (HasCallStack, MonadIO m, MonadThrow m) => Timeout @@ -264,11 +272,11 @@ assertSuccess = either throwM return assertNoEvent :: (HasCallStack, MonadIO m, MonadCatch m) => Timeout -> [WebSocket] -> m () assertNoEvent t ww = do - results <- awaitMatchN t ww (const $ pure ()) - for_ results $ - either (const $ pure ()) (liftIO . f) + results <- awaitMatchN' t (zip [(0 :: Int)..] ww) (const $ pure ()) + for_ results $ \(ix, result) -> + either (const $ pure ()) (liftIO . f ix) result where - f n = assertFailure $ "unexpected notification received: " ++ show n + f ix n = assertFailure $ "unexpected notification received: " ++ show (ix, n) ----------------------------------------------------------------------------- -- Unpacking Notifications diff --git a/libs/zauth/src/Data/ZAuth/Creation.hs b/libs/zauth/src/Data/ZAuth/Creation.hs index b57a2c9f8d8..b4ad1a24524 100644 --- a/libs/zauth/src/Data/ZAuth/Creation.hs +++ b/libs/zauth/src/Data/ZAuth/Creation.hs @@ -17,6 +17,9 @@ module Data.ZAuth.Creation , sessionToken , botToken , providerToken + , legalHoldAccessToken + , legalHoldAccessToken1 + , legalHoldUserToken -- * Generic , withIndex @@ -85,17 +88,37 @@ sessionToken dur usr rnd = do d <- expiry dur newToken d U (Just S) (mkUser usr rnd) +-- | Create an access token taking a duration, userId and a (random) number that can be used as connection identifier accessToken :: Integer -> UUID -> Word64 -> Create (Token Access) accessToken dur usr con = do d <- expiry dur newToken d A Nothing (mkAccess usr con) +-- | Create an access token taking a duration and userId. Similar to 'accessToken', except that the connection identifier is randomly generated. accessToken1 :: Integer -> UUID -> Create (Token Access) accessToken1 dur usr = do g <- Create $ asks randGen d <- liftIO $ asGenIO (uniform :: GenIO -> IO Word64) g accessToken dur usr d +legalHoldUserToken :: Integer -> UUID -> Word32 -> Create (Token LegalHoldUser) +legalHoldUserToken dur usr rnd = do + d <- expiry dur + newToken d LU Nothing (mkLegalHoldUser usr rnd) + +-- | Create a legal hold access token taking a duration, userId and a (random) number that can be used as connection identifier +legalHoldAccessToken :: Integer -> UUID -> Word64 -> Create (Token LegalHoldAccess) +legalHoldAccessToken dur usr con = do + d <- expiry dur + newToken d LA Nothing (mkLegalHoldAccess usr con) + +-- | Create a legal hold access token taking a duration, userId. Similar to 'legalHoldAccessToken', except that the connection identifier is randomly generated. +legalHoldAccessToken1 :: Integer -> UUID -> Create (Token LegalHoldAccess) +legalHoldAccessToken1 dur usr = do + g <- Create $ asks randGen + d <- liftIO $ asGenIO (uniform :: GenIO -> IO Word64) g + legalHoldAccessToken dur usr d + botToken :: UUID -> UUID -> UUID -> Create (Token Bot) botToken pid bid cnv = newToken (-1) B Nothing (mkBot pid bid cnv) diff --git a/libs/zauth/src/Data/ZAuth/Token.hs b/libs/zauth/src/Data/ZAuth/Token.hs index 0d7a4283c50..a362e601c72 100644 --- a/libs/zauth/src/Data/ZAuth/Token.hs +++ b/libs/zauth/src/Data/ZAuth/Token.hs @@ -46,6 +46,16 @@ module Data.ZAuth.Token , provider , mkProvider + + -- * LegalHold body + , LegalHoldUser + , legalHoldUser + , mkLegalHoldUser + + , LegalHoldAccess + , legalHoldAccess + , mkLegalHoldAccess + , writeData ) where @@ -61,7 +71,17 @@ import Data.ByteString.Lazy.Char8 (split, break, drop) import Data.UUID import Sodium.Crypto.Sign (Signature (..)) -data Type = A | U | B | P deriving (Eq, Show) +data Type = A -- ^ Access (Used as short-lived token for Users) + | U -- ^ User (Used as a cookie for Users to refresh access tokens) + | B -- ^ Bot + | P -- ^ Provider + | LA -- ^ LegalHold Access (Used as short-lived token for LegalHold Service) + | LU -- ^ LegalHold User (Used as a cookie for LegalHold Service to refresh access tokens) + deriving (Eq, Show) + +-- | Tag: Tokens for Users with no tag are refreshable themselves and called "UserToken" +-- Tokens for Users with the tag 'S' are non-refreshable themselves and called "SessionToken" +-- FUTUREWORK: rename 'S' to 'SessionTag' for clarity data Tag = S deriving (Eq, Show) data Token a = Token @@ -70,6 +90,9 @@ data Token a = Token , _body :: !a } deriving (Eq, Show) +-- FUTUREWORK: maybe refactor to +-- data Header (t :: Type) = +-- Header { ... everything except _typ ...} ? data Header = Header { _version :: !Int , _key :: !Int @@ -98,6 +121,14 @@ newtype Provider = Provider { _provider :: UUID } deriving (Eq, Show) +newtype LegalHoldUser = LegalHoldUser + { _legalHoldUser :: User + } deriving (Eq, Show) + +newtype LegalHoldAccess = LegalHoldAccess + { _legalHoldAccess :: Access + } deriving (Eq, Show) + type Properties = [(LByteString, LByteString)] signature :: Getter (Token a) Signature @@ -114,31 +145,45 @@ makeLenses ''Access makeLenses ''User makeLenses ''Bot makeLenses ''Provider +makeLenses ''LegalHoldUser +makeLenses ''LegalHoldAccess instance FromByteString (Token Access) where parser = takeLazyByteString >>= \b -> - case readToken readAccessBody b of + 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 readUserBody b of + 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 readBotBody b of + 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 readProviderBody b of + 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 + +instance FromByteString (Token LegalHoldUser) where + 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 @@ -163,34 +208,42 @@ mkBot = Bot mkProvider :: UUID -> Provider mkProvider = Provider +mkLegalHoldAccess :: UUID -> Word64 -> LegalHoldAccess +mkLegalHoldAccess uid cid = LegalHoldAccess $ Access uid cid + +mkLegalHoldUser :: UUID -> Word32 -> LegalHoldUser +mkLegalHoldUser uid r = LegalHoldUser $ User uid r + ----------------------------------------------------------------------------- -- Reading -readToken :: (Properties -> Maybe a) -> LByteString -> Maybe (Token a) -readToken f b = case split '.' b of +readToken :: Type -> (Properties -> Maybe a) -> LByteString -> Maybe (Token a) +readToken t f b = case split '.' b of (s:rest) -> let p = map pairwise rest in Token <$> hush (Signature <$> decode (toStrict s)) - <*> readHeader p + <*> readHeader t p <*> f p _ -> Nothing where pairwise :: LByteString -> (LByteString, LByteString) pairwise x = let (k, v) = break (== '=') x in (k, drop 1 v) -readHeader :: Properties -> Maybe Header -readHeader p = Header +readHeader :: Type -> Properties -> Maybe Header +readHeader t p = Header <$> (lookup "v" p >>= fromByteString') <*> (lookup "k" p >>= fromByteString') <*> (lookup "d" p >>= fromByteString') - <*> (lookup "t" p >>= readType) + <*> (lookup "t" p >>= readType t) <*> (readTag <$> lookup "l" p) where - readType "a" = Just A - readType "u" = Just U - readType "b" = Just B - readType "p" = Just P - readType _ = Nothing + readType A "a" = Just A + readType U "u" = Just U + readType B "b" = Just B + readType P "p" = Just P + readType LA "la" = Just LA + readType LU "lu" = Just LU + readType _ _ = Nothing readTag "s" = Just S readTag _ = Nothing @@ -214,6 +267,12 @@ readBotBody t = Bot readProviderBody :: Properties -> Maybe Provider readProviderBody t = Provider <$> (lookup "p" t >>= fromLazyASCIIBytes) +readLegalHoldAccessBody :: Properties -> Maybe LegalHoldAccess +readLegalHoldAccessBody t = LegalHoldAccess <$> readAccessBody t + +readLegalHoldUserBody :: Properties -> Maybe LegalHoldUser +readLegalHoldUserBody t = LegalHoldUser <$> readUserBody t + ----------------------------------------------------------------------------- -- Writing @@ -249,11 +308,21 @@ instance ToByteString Bot where instance ToByteString Provider where builder t = field "p" (toLazyASCIIBytes $ t^.provider) +instance ToByteString LegalHoldAccess where + builder t = field "u" (toLazyASCIIBytes $ t^.legalHoldAccess.userId) <> dot <> + field "c" (t^.legalHoldAccess.connection) + +instance ToByteString LegalHoldUser where + builder t = field "u" (toLazyASCIIBytes $ t^.legalHoldUser.user) <> dot <> + field "r" (Hex (t^.legalHoldUser.rand)) + instance ToByteString Type where builder A = char8 'a' builder U = char8 'u' builder B = char8 'b' builder P = char8 'p' + builder LA = char8 'l' <> char8 'a' + builder LU = char8 'l' <> char8 'u' instance ToByteString Tag where builder S = char8 's' diff --git a/libs/zauth/src/Data/ZAuth/Validation.hs b/libs/zauth/src/Data/ZAuth/Validation.hs index 01eb2aa4a49..eb060a263ea 100644 --- a/libs/zauth/src/Data/ZAuth/Validation.hs +++ b/libs/zauth/src/Data/ZAuth/Validation.hs @@ -30,8 +30,11 @@ data Failure = Falsified -- ^ The token signature is incorrect. | Expired -- ^ The token is expired. | Invalid -- ^ Invalid token. + | Unsupported -- ^ This operation is unsupported on this token type deriving (Eq, Show) +instance Exception Failure + newtype Env = Env { verifyFns :: Vector (Signature -> Strict.ByteString -> IO Bool) } diff --git a/libs/zauth/test/Arbitraries.hs b/libs/zauth/test/Arbitraries.hs index 5f0684f4369..9ff61a61636 100644 --- a/libs/zauth/test/Arbitraries.hs +++ b/libs/zauth/test/Arbitraries.hs @@ -5,16 +5,17 @@ module Arbitraries where import Imports +import Control.Lens ((.~)) import Data.UUID hiding (fromString) import Data.ZAuth.Token import Sodium.Crypto.Sign import Test.Tasty.QuickCheck instance Arbitrary (Token Access) where - arbitrary = mkToken <$> arbitrary <*> arbitrary <*> arbitrary + arbitrary = mkToken <$> arbitrary <*> ((typ .~ A) <$> arbitrary) <*> arbitrary instance Arbitrary (Token User) where - arbitrary = mkToken <$> arbitrary <*> arbitrary <*> arbitrary + arbitrary = mkToken <$> arbitrary <*> ((typ .~ U) <$> arbitrary) <*> arbitrary instance Arbitrary (Token Bot) where arbitrary = mkToken <$> arbitrary <*> arbitrary <*> arbitrary @@ -22,6 +23,12 @@ instance Arbitrary (Token Bot) where instance Arbitrary (Token Provider) where arbitrary = mkToken <$> arbitrary <*> arbitrary <*> arbitrary +instance Arbitrary (Token LegalHoldAccess) where + arbitrary = mkToken <$> arbitrary <*> ((typ .~ LA) <$> arbitrary) <*> arbitrary + +instance Arbitrary (Token LegalHoldUser) where + arbitrary = mkToken <$> arbitrary <*> ((typ .~ LU) <$> arbitrary) <*> arbitrary + instance Arbitrary Header where arbitrary = mkHeader <$> arbitrary @@ -42,6 +49,12 @@ instance Arbitrary Bot where instance Arbitrary Provider where arbitrary = mkProvider <$> arbitrary +instance Arbitrary LegalHoldAccess where + arbitrary = mkLegalHoldAccess <$> arbitrary <*> arbitrary + +instance Arbitrary LegalHoldUser where + arbitrary = mkLegalHoldUser <$> arbitrary <*> arbitrary + instance Arbitrary ByteString where arbitrary = fromString <$> arbitrary `suchThat` (not . any (== '.')) @@ -49,7 +62,7 @@ instance Arbitrary Signature where arbitrary = Signature <$> arbitrary instance Arbitrary Type where - arbitrary = elements [A, U] + arbitrary = elements [A, U, LA, LU] instance Arbitrary Tag where arbitrary = return S diff --git a/libs/zauth/test/ZAuth.hs b/libs/zauth/test/ZAuth.hs index 15c66615375..b6aa22489d9 100644 --- a/libs/zauth/test/ZAuth.hs +++ b/libs/zauth/test/ZAuth.hs @@ -26,6 +26,10 @@ tests = do [ testGroup "Parsing" [ testProperty "decode . encode == id [access]" testDecEncAccessToken , testProperty "decode . encode == id [user]" testDecEncUserToken + , testProperty "decode . encode == id [legalhold access]" testDecEncLegalHoldAccessToken + , testProperty "decode . encode == id [legalhold user]" testDecEncLegalHoldUserToken + , testProperty "decode as User . encode as LegalHoldUser == Nothing" testUserIsNotLegalHoldUser + , testProperty "decode as LegalHoldUser . encode as User == Nothing" testUserIsNotLegalHoldUser' ] , testGroup "Signing and Verifying" [ testCase "expired" (runCreate z 1 $ testExpired v) @@ -39,12 +43,24 @@ tests = do defDuration :: Integer defDuration = 1 +testUserIsNotLegalHoldUser :: Token LegalHoldUser -> Bool +testUserIsNotLegalHoldUser t = fromByteString @(Token User) (toByteString' t) == Nothing + +testUserIsNotLegalHoldUser' :: Token User -> Bool +testUserIsNotLegalHoldUser' t = fromByteString @(Token LegalHoldUser) (toByteString' t) == Nothing + testDecEncAccessToken :: Token Access -> Bool testDecEncAccessToken t = fromByteString (toByteString' t) == Just t testDecEncUserToken :: Token User -> Bool testDecEncUserToken t = fromByteString (toByteString' t) == Just t +testDecEncLegalHoldUserToken :: Token LegalHoldUser -> Bool +testDecEncLegalHoldUserToken t = fromByteString (toByteString' t) == Just t + +testDecEncLegalHoldAccessToken :: Token LegalHoldAccess -> Bool +testDecEncLegalHoldAccessToken t = fromByteString (toByteString' t) == Just t + testNotExpired :: V.Env -> Create () testNotExpired p = do u <- liftIO nextRandom diff --git a/mailboxes.json b/mailboxes.json new file mode 100644 index 00000000000..b7b2803b19e --- /dev/null +++ b/mailboxes.json @@ -0,0 +1,7 @@ +[ + { "host": "imap.bar.com" + , "user": "foo@bar.com" + , "pass": "secret" + , "conn": 1 + } +] diff --git a/services/brig/Makefile b/services/brig/Makefile index 6083f014c0e..bb8b7d2818a 100644 --- a/services/brig/Makefile +++ b/services/brig/Makefile @@ -86,18 +86,18 @@ $(DEB_INDEX): install .PHONY: i i: - ../integration.sh $(EXE_IT) -s $(NAME).integration.yaml -i ../integration.yaml $(WIRE_INTEGRATION_TEST_OPTIONS) + INTEGRATION_USE_NGINZ=1 ../integration.sh $(EXE_IT) -s $(NAME).integration.yaml -i ../integration.yaml $(WIRE_INTEGRATION_TEST_OPTIONS) .PHONY: i-aws i-aws: - INTEGRATION_USE_REAL_AWS=1 ../integration.sh $(EXE_IT) -s $(NAME).integration-aws.yaml -i ../integration.yaml $(WIRE_INTEGRATION_TEST_OPTIONS) + INTEGRATION_USE_REAL_AWS=1 INTEGRATION_USE_NGINZ=1 ../integration.sh $(EXE_IT) -s $(NAME).integration-aws.yaml -i ../integration.yaml $(WIRE_INTEGRATION_TEST_OPTIONS) .PHONY: i-list i-list: $(EXE_IT) -s $(NAME).integration.yaml -i ../integration.yaml -l i-%: - ../integration.sh $(EXE_IT) -s $(NAME).integration.yaml -i ../integration.yaml -p "$*" $(WIRE_INTEGRATION_TEST_OPTIONS) + INTEGRATION_USE_NGINZ=1 ../integration.sh $(EXE_IT) -s $(NAME).integration.yaml -i ../integration.yaml -p "$*" $(WIRE_INTEGRATION_TEST_OPTIONS) .PHONY: integration integration: fast i diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index cf7316323e9..d6ac7dd4ffb 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -113,6 +113,8 @@ zauth: sessionTokenTimeout: 20 accessTokenTimeout: 30 providerTokenTimeout: 60 + legalHoldUserTokenTimeout: 120 + legalHoldAccessTokenTimeout: 30 turn: servers: test/resources/turn/servers.txt diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs index 98bd0741dd7..d834240398a 100644 --- a/services/brig/src/Brig/API.hs +++ b/services/brig/src/Brig/API.hs @@ -59,6 +59,7 @@ import qualified Brig.Provider.API as Provider import qualified Brig.Team.API as Team import qualified Brig.Team.Email as Team import qualified Brig.TURN.API as TURN +import qualified Data.ZAuth.Token as ZAuth --------------------------------------------------------------------------- -- Sitemap @@ -213,7 +214,7 @@ sitemap o = do document "HEAD" "userExists" $ do Doc.summary "Check if a user ID exists" - Doc.parameter Doc.Path "id" Doc.bytes' $ + Doc.parameter Doc.Path "uid" Doc.bytes' $ Doc.description "User ID" Doc.response 200 "User exists" Doc.end Doc.errorResponse userNotFound @@ -227,7 +228,7 @@ sitemap o = do document "GET" "user" $ do Doc.summary "Get a user by ID" - Doc.parameter Doc.Path "id" Doc.bytes' $ + Doc.parameter Doc.Path "uid" Doc.bytes' $ Doc.description "User ID" Doc.returns (Doc.ref Doc.user) Doc.response 200 "User" Doc.end @@ -316,7 +317,7 @@ sitemap o = do document "GET" "getPrekeyBundle" $ do Doc.summary "Get a prekey for each client of a user." - Doc.parameter Doc.Path "user" Doc.bytes' $ + Doc.parameter Doc.Path "uid" Doc.bytes' $ Doc.description "User ID" Doc.returns (Doc.ref Doc.prekeyBundle) Doc.response 200 "Prekey Bundle" Doc.end @@ -330,7 +331,7 @@ sitemap o = do document "GET" "getPrekey" $ do Doc.summary "Get a prekey for a specific client of a user." - Doc.parameter Doc.Path "user" Doc.bytes' $ + Doc.parameter Doc.Path "uid" Doc.bytes' $ Doc.description "User ID" Doc.parameter Doc.Path "client" Doc.bytes' $ Doc.description "Client ID" @@ -345,7 +346,7 @@ sitemap o = do document "GET" "getUserClients" $ do Doc.summary "Get all of a user's clients." - Doc.parameter Doc.Path "user" Doc.bytes' $ + Doc.parameter Doc.Path "uid" Doc.bytes' $ Doc.description "User ID" Doc.returns (Doc.array (Doc.ref Doc.pubClient)) Doc.response 200 "List of clients" Doc.end @@ -359,7 +360,7 @@ sitemap o = do document "GET" "getUserClient" $ do Doc.summary "Get a specific client of a user." - Doc.parameter Doc.Path "user" Doc.bytes' $ + Doc.parameter Doc.Path "uid" Doc.bytes' $ Doc.description "User ID" Doc.parameter Doc.Path "client" Doc.bytes' $ Doc.description "Client ID" @@ -375,7 +376,7 @@ sitemap o = do document "GET" "getRichInfo" $ do Doc.summary "Get user's rich info" - Doc.parameter Doc.Path "user" Doc.bytes' $ + Doc.parameter Doc.Path "uid" Doc.bytes' $ Doc.description "User ID" Doc.returns (Doc.ref Doc.richInfo) Doc.response 200 "RichInfo" Doc.end @@ -1118,8 +1119,8 @@ createUser (_ ::: req) = do for_ (liftM3 (,,) (userEmail usr) (createdUserTeam result) (newUserTeam new)) $ \(e, ct, ut) -> sendWelcomeEmail e ct ut (Just lang) cok <- case acc of - UserAccount _ Ephemeral -> lift $ Auth.newCookie (userId usr) SessionCookie (newUserLabel new) - UserAccount _ _ -> lift $ Auth.newCookie (userId usr) PersistentCookie (newUserLabel new) + UserAccount _ Ephemeral -> lift $ Auth.newCookie @ZAuth.User (userId usr) SessionCookie (newUserLabel new) + UserAccount _ _ -> lift $ Auth.newCookie @ZAuth.User (userId usr) PersistentCookie (newUserLabel new) lift $ Auth.setResponseCookie cok $ setStatus status201 . addHeader "Location" (toByteString' (userId usr)) diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index 7b29cfd1f67..ff3ed3bdba7 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -102,6 +102,12 @@ changeHandleError ChangeHandleNoIdentity = StdError noIdentity changeHandleError ChangeHandleExists = StdError handleExists changeHandleError ChangeHandleInvalid = StdError invalidHandle +legalHoldLoginError :: LegalHoldLoginError -> Error +legalHoldLoginError LegalHoldLoginNoBindingTeam = StdError noBindingTeam +legalHoldLoginError LegalHoldLoginLegalHoldNotEnabled = StdError legalHoldNotEnabled +legalHoldLoginError (LegalHoldLoginError e) = loginError e +legalHoldLoginError (LegalHoldReAuthError e) = reauthError e + loginError :: LoginError -> Error loginError LoginFailed = StdError badCredentials loginError LoginSuspended = StdError accountSuspended @@ -123,9 +129,10 @@ reauthError ReAuthMissingPassword = StdError missingAuthError reauthError (ReAuthError e) = authError e zauthError :: ZAuth.Failure -> Error -zauthError ZAuth.Expired = StdError authTokenExpired -zauthError ZAuth.Falsified = StdError authTokenInvalid -zauthError ZAuth.Invalid = StdError authTokenInvalid +zauthError ZAuth.Expired = StdError authTokenExpired +zauthError ZAuth.Falsified = StdError authTokenInvalid +zauthError ZAuth.Invalid = StdError authTokenInvalid +zauthError ZAuth.Unsupported = StdError authTokenUnsupported clientError :: ClientError -> Error clientError ClientNotFound = StdError clientNotFound @@ -349,6 +356,9 @@ authTokenExpired = Wai.Error status403 "invalid-credentials" "Token expired" authTokenInvalid :: Wai.Error authTokenInvalid = Wai.Error status403 "invalid-credentials" "Invalid token" +authTokenUnsupported :: Wai.Error +authTokenUnsupported = Wai.Error status403 "invalid-credentials" "Unsupported token operation for this token type" + incorrectPermissions :: Wai.Error incorrectPermissions = Wai.Error status403 "invalid-permissions" "Copy permissions must be a subset of self permissions" @@ -407,3 +417,7 @@ can'tAddLegalHoldClient = Wai.Error status400 "client-error" "LegalHold clients cannot be added manually. LegalHold must be enabled on this user by an admin" + +legalHoldNotEnabled :: Wai.Error +legalHoldNotEnabled = Wai.Error status403 "legalhold-not-enabled" "LegalHold must be enabled and configured on the team first" + diff --git a/services/brig/src/Brig/API/Handler.hs b/services/brig/src/Brig/API/Handler.hs index 47f3b5e2b20..5bd7c0d1234 100644 --- a/services/brig/src/Brig/API/Handler.hs +++ b/services/brig/src/Brig/API/Handler.hs @@ -34,6 +34,7 @@ import qualified Brig.Whitelist as Whitelist import qualified Control.Monad.Catch as Catch import qualified Network.Wai.Utilities.Error as WaiError import qualified Network.Wai.Utilities.Server as Server +import qualified Data.ZAuth.Validation as ZV ------------------------------------------------------------------------------- -- HTTP Handler Monad @@ -49,6 +50,8 @@ runHandler e r h k = do errors = [ Catch.Handler $ \(ex :: PhoneException) -> pure (Left (phoneError ex)) + , Catch.Handler $ \(ex :: ZV.Failure) -> + pure (Left (zauthError ex)) , Catch.Handler $ \(ex :: AWS.Error) -> case ex of AWS.SESInvalidDomain -> pure (Left (StdError invalidEmail)) diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index 3d1017c5c2d..2a68e025351 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -2,13 +2,14 @@ module Brig.API.Types ( module Brig.API.Types - , Activation (..) - , ActivationError (..) - , ClientDataError (..) - , PropertiesDataError (..) - , AuthError (..) - , ReAuthError (..) - , RetryAfter (..) + , Activation (..) + , ActivationError (..) + , ClientDataError (..) + , PropertiesDataError (..) + , AuthError (..) + , ReAuthError (..) + , LegalHoldLoginError (..) + , RetryAfter (..) , foldKey ) where @@ -106,6 +107,12 @@ data PasswordResetError | InvalidPasswordResetCode | ResetPasswordMustDiffer +data LegalHoldLoginError + = LegalHoldLoginNoBindingTeam + | LegalHoldLoginLegalHoldNotEnabled + | LegalHoldLoginError LoginError + | LegalHoldReAuthError ReAuthError + data LoginError = LoginFailed | LoginSuspended diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 8679554b897..703dd4bb940 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -35,6 +35,7 @@ module Brig.IO.Intra , getTeamContacts , getTeamOwners , getTeamOwnersWithEmail + , getTeamLegalHoldStatus , changeTeamStatus ) where @@ -49,6 +50,7 @@ import Brig.API.Types import Brig.RPC import Brig.Types import Brig.Types.Intra +import Brig.Types.Team.LegalHold (LegalHoldTeamConfig) import Brig.User.Event import Control.Lens (view, (.~), (?~), (^.)) import Control.Lens.Prism (_Just) @@ -664,6 +666,14 @@ getTeamName tid = do req = paths ["i", "teams", toByteString' tid, "name"] . expect2xx +getTeamLegalHoldStatus :: TeamId -> AppIO LegalHoldTeamConfig +getTeamLegalHoldStatus tid = do + debug $ remote "galley" . msg (val "Get legalhold settings") + galleyRequest GET req >>= decodeBody "galley" + where + req = paths ["i", "teams", toByteString' tid, "features", "legalhold"] + . expect2xx + changeTeamStatus :: TeamId -> Team.TeamStatus -> Maybe Currency.Alpha -> AppIO () changeTeamStatus tid s cur = do debug $ remote "galley" . msg (val "Change Team status") diff --git a/services/brig/src/Brig/Provider/RPC.hs b/services/brig/src/Brig/Provider/RPC.hs index 6734b072701..cba627a5f71 100644 --- a/services/brig/src/Brig/Provider/RPC.hs +++ b/services/brig/src/Brig/Provider/RPC.hs @@ -62,6 +62,12 @@ createBot scon new = do 409 -> throwE ServiceBotConflict _ -> extLogError scon rs >> throwE ServiceUnavailable where + -- we can't use 'responseJsonEither' instead, because we have a @Response ByteString@ + -- here, not a @Response (Maybe ByteString)@. + decodeBytes ctx bs = case eitherDecode' bs of + Left e -> throwM $ ParseException ctx e + Right a -> return a + reqBuilder = extReq scon ["bots"] . method POST diff --git a/services/brig/src/Brig/RPC.hs b/services/brig/src/Brig/RPC.hs index d63d88c5316..6af4b3db10a 100644 --- a/services/brig/src/Brig/RPC.hs +++ b/services/brig/src/Brig/RPC.hs @@ -30,15 +30,8 @@ zUser = header "Z-User" . toByteString' remote :: ByteString -> Msg -> Msg remote = field "remote" -decodeBody :: (FromJSON a, MonadThrow m) => Text -> Response (Maybe BL.ByteString) -> m a -decodeBody rm rs = case responseBody rs of - Nothing -> throwM $ ParseException rm "Missing response body." - Just bs -> decodeBytes rm bs - -decodeBytes :: (FromJSON a, MonadThrow m) => Text -> BL.ByteString -> m a -decodeBytes ctx bs = case eitherDecode' bs of - Left e -> throwM $ ParseException ctx e - Right a -> return a +decodeBody :: (Typeable a, FromJSON a, MonadThrow m) => Text -> Response (Maybe BL.ByteString) -> m a +decodeBody ctx = responseJsonThrow (ParseException ctx) expect :: [Status] -> Request -> Request expect ss rq = rq { checkResponse = check } diff --git a/services/brig/src/Brig/User/API/Auth.hs b/services/brig/src/Brig/User/API/Auth.hs index e58349e8cef..a9134051354 100644 --- a/services/brig/src/Brig/User/API/Auth.hs +++ b/services/brig/src/Brig/User/API/Auth.hs @@ -27,6 +27,7 @@ import qualified Brig.Types.Swagger as Doc import qualified Data.Swagger.Build.Api as Doc import qualified Network.Wai.Utilities.Swagger as Doc import qualified Brig.ZAuth as ZAuth +import qualified Data.ZAuth.Token as ZAuth import qualified Network.Wai.Predicate as P routes :: Routes Doc.ApiBuilder Handler () @@ -138,6 +139,11 @@ routes = do -- Internal + -- galley can query this endpoint at the right moment in the LegalHold flow + post "/i/legalhold-login" (continue legalHoldLogin) $ + jsonRequest @LegalHoldLogin + .&. accept "application" "json" + post "/i/sso-login" (continue ssoLogin) $ jsonRequest @SsoLogin .&. def False (query "persist") @@ -185,11 +191,24 @@ ssoLogin (req ::: persist ::: _) = do a <- Auth.ssoLogin l typ !>> loginError tokenResponse a -logout :: JSON ::: Maybe ZAuth.UserToken ::: Maybe ZAuth.AccessToken -> Handler Response +legalHoldLogin :: JsonRequest LegalHoldLogin ::: JSON -> Handler Response +legalHoldLogin (req ::: _) = do + l <- parseJsonBody req + let typ = PersistentCookie -- Session cookie isn't a supported use case here + a <- Auth.legalHoldLogin l typ !>> legalHoldLoginError + tokenResponse a + +-- TODO: add legalhold test checking cookies are revoked (/access/logout is called) when legalhold device is deleted. +logout :: JSON ::: Maybe (Either ZAuth.UserToken ZAuth.LegalHoldUserToken) ::: Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> Handler Response logout (_ ::: Nothing ::: Nothing) = throwStd authMissingCookieAndToken logout (_ ::: Nothing ::: Just _ ) = throwStd authMissingCookie logout (_ ::: Just _ ::: Nothing) = throwStd authMissingToken -logout (_ ::: Just ut ::: Just at) = do +logout (_ ::: Just (Left _) ::: Just (Right _)) = throwStd authTokenMismatch +logout (_ ::: Just (Right _) ::: Just (Left _)) = throwStd authTokenMismatch +logout (_ ::: Just (Left ut) ::: Just (Left at)) = do + Auth.logout ut at !>> zauthError + return empty +logout (_ ::: Just (Right ut) ::: Just (Right at)) = do Auth.logout ut at !>> zauthError return empty @@ -204,25 +223,33 @@ rmCookies (uid ::: req) = do Auth.revokeAccess uid pw ids lls !>> authError return empty -renew :: JSON ::: Maybe ZAuth.UserToken ::: Maybe ZAuth.AccessToken -> Handler Response +renew :: JSON ::: Maybe (Either ZAuth.UserToken ZAuth.LegalHoldUserToken) ::: Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) -> Handler Response renew (_ ::: Nothing ::: _) = throwStd authMissingCookie -renew (_ ::: Just ut ::: at) = do - a <- Auth.renewAccess ut at !>> zauthError - tokenResponse a +renew (_ ::: Just userToken ::: accessToken) = do + case (userToken, accessToken) of + (Left ut, Just (Left at)) -> (Auth.renewAccess ut (Just at) !>> zauthError) >>= tokenResponse + (Left ut, Nothing) -> Auth.renewAccess @ZAuth.User @ZAuth.Access ut Nothing !>> zauthError >>= tokenResponse + (Right lut, Just (Right lat)) -> Auth.renewAccess @ZAuth.LegalHoldUser @ZAuth.LegalHoldAccess lut (Just lat) !>> zauthError >>= tokenResponse + (Right lut, Nothing) -> Auth.renewAccess @ZAuth.LegalHoldUser @ZAuth.LegalHoldAccess lut Nothing !>> zauthError >>= tokenResponse + (_, _) -> throwStd authTokenMismatch -- Utilities +-- -- | A predicate that captures user and access tokens for a request handler. -tokenRequest :: (HasCookies r, HasHeaders r, HasQuery r) - => Predicate r P.Error (Maybe ZAuth.UserToken ::: Maybe ZAuth.AccessToken) -tokenRequest = opt userToken .&. opt accessToken +tokenRequest :: forall r . (HasCookies r, HasHeaders r, HasQuery r) + => Predicate r P.Error (Maybe (Either ZAuth.UserToken ZAuth.LegalHoldUserToken) + ::: Maybe (Either ZAuth.AccessToken ZAuth.LegalHoldAccessToken) ) +tokenRequest = opt (userToken ||| legalHoldUserToken) .&. opt (accessToken ||| legalHoldAccessToken) where - userToken = cookieErr <$> cookie "zuid" - accessToken = parse <$> (tokenHeader .|. tokenQuery) - tokenHeader = bearer <$> header "authorization" - tokenQuery = query "access_token" - - cookieErr :: Result P.Error ZAuth.UserToken -> Result P.Error ZAuth.UserToken + userToken = cookieErr @ZAuth.User <$> cookie "zuid" + legalHoldUserToken = cookieErr @ZAuth.LegalHoldUser <$> cookie "zuid" + accessToken = parse @ZAuth.Access <$> (tokenHeader .|. tokenQuery) + legalHoldAccessToken = parse @ZAuth.LegalHoldAccess <$> (tokenHeader .|. tokenQuery) + tokenHeader = bearer <$> header "authorization" + tokenQuery = query "access_token" + + cookieErr :: ZAuth.UserTokenLike u => Result P.Error (ZAuth.Token u) -> Result P.Error (ZAuth.Token u) cookieErr x@Okay{} = x cookieErr (Fail x) = Fail (setMessage "Invalid user token" (P.setStatus status403 x)) @@ -236,13 +263,13 @@ tokenRequest = opt userToken .&. opt accessToken (setMessage "Invalid authorization scheme" (err status403))) -- Parse the access token - parse :: Result P.Error ByteString -> Result P.Error ZAuth.AccessToken + parse :: ZAuth.AccessTokenLike a => Result P.Error ByteString -> Result P.Error (ZAuth.Token a) parse (Fail x) = Fail x parse (Okay _ b) = case fromByteString b of Nothing -> Fail (setReason TypeError (setMessage "Invalid access token" (err status403))) Just t -> return t -tokenResponse :: Auth.Access -> Handler Response +tokenResponse :: ZAuth.UserTokenLike u => Auth.Access u -> Handler Response tokenResponse (Auth.Access t Nothing) = return (json t) tokenResponse (Auth.Access t (Just c)) = lift $ Auth.setResponseCookie c (json t) diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index fb522821b4b..035f00e84f1 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -10,6 +10,7 @@ module Brig.User.Auth -- * Internal , lookupLoginCode , ssoLogin + , legalHoldLogin -- * Re-exports , listCookies @@ -31,12 +32,14 @@ import Brig.User.Phone import Brig.Types.Common import Brig.Types.Intra import Brig.Types.User +import Brig.Types.Team.LegalHold (LegalHoldTeamConfig (..), LegalHoldStatus (..)) import Brig.Types.User.Auth hiding (user) import Control.Error hiding (bool) import Data.Id import Data.ByteString.Conversion (toByteString) import Data.List1 (singleton) import Data.Misc (PlainTextPassword (..)) +import Network.Wai.Utilities.Error ((!>>)) import System.Logger (msg, field, (~~), val) import qualified Brig.Data.Activation as Data @@ -45,12 +48,14 @@ import qualified Brig.Data.User as Data import qualified Brig.Data.UserKey as Data import qualified Brig.Options as Opt import qualified Brig.ZAuth as ZAuth +import qualified Data.ZAuth.Token as ZAuth +import qualified Brig.IO.Intra as Intra import qualified System.Logger.Class as Log -data Access = Access +data Access u = Access { accessToken :: !AccessToken - , accessCookie :: !(Maybe (Cookie ZAuth.UserToken)) + , accessCookie :: !(Maybe (Cookie (ZAuth.Token u))) } sendLoginCode :: Phone -> Bool -> Bool -> ExceptT SendLoginCodeError AppIO PendingLoginCode @@ -78,7 +83,7 @@ lookupLoginCode phone = Data.lookupKey (userPhoneKey phone) >>= \case Nothing -> return Nothing Just u -> Data.lookupLoginCode u -login :: Login -> CookieType -> ExceptT LoginError AppIO Access +login :: Login -> CookieType -> ExceptT LoginError AppIO (Access ZAuth.User) login (PasswordLogin li pw label) typ = do uid <- resolveLoginId li checkRetryLimit uid @@ -87,14 +92,14 @@ login (PasswordLogin li pw label) typ = do AuthEphemeral -> throwE LoginEphemeral AuthInvalidCredentials -> loginFailed uid AuthInvalidUser -> loginFailed uid - newAccess uid typ label + newAccess @ZAuth.User @ZAuth.Access uid typ label login (SmsLogin phone code label) typ = do uid <- resolveLoginId (LoginByPhone phone) checkRetryLimit uid ok <- lift $ Data.verifyLoginCode uid code unless ok $ loginFailed uid - newAccess uid typ label + newAccess @ZAuth.User @ZAuth.Access uid typ label loginFailed :: UserId -> ExceptT LoginError AppIO () loginFailed uid = decrRetryLimit uid >> throwE LoginFailed @@ -121,15 +126,16 @@ withRetryLimit action uid = do BudgetExhausted ttl -> throwE . LoginBlocked . RetryAfter . floor $ ttl BudgetedValue () _ -> pure () -logout :: ZAuth.UserToken -> ZAuth.AccessToken -> ExceptT ZAuth.Failure AppIO () +logout :: ZAuth.TokenPair u a => ZAuth.Token u -> ZAuth.Token a -> ExceptT ZAuth.Failure AppIO () logout ut at = do (u, ck) <- validateTokens ut (Just at) lift $ revokeCookies u [cookieId ck] [] renewAccess - :: ZAuth.UserToken - -> Maybe ZAuth.AccessToken - -> ExceptT ZAuth.Failure AppIO Access + :: ZAuth.TokenPair u a + => ZAuth.Token u + -> Maybe (ZAuth.Token a) + -> ExceptT ZAuth.Failure AppIO (Access u) renewAccess ut at = do (uid, ck) <- validateTokens ut at catchSuspendInactiveUser uid ZAuth.Expired @@ -160,14 +166,14 @@ catchSuspendInactiveUser uid errval = do lift $ suspendAccount (singleton uid) throwE errval -newAccess :: UserId -> CookieType -> Maybe CookieLabel -> ExceptT LoginError AppIO Access -newAccess u ct cl = do - catchSuspendInactiveUser u LoginSuspended - r <- lift $ newCookieLimited u ct cl +newAccess :: forall u a. ZAuth.TokenPair u a => UserId -> CookieType -> Maybe CookieLabel -> ExceptT LoginError AppIO (Access u) +newAccess uid ct cl = do + catchSuspendInactiveUser uid LoginSuspended + r <- lift $ newCookieLimited uid ct cl case r of Left delay -> throwE $ LoginThrottled delay Right ck -> do - t <- lift $ newAccessToken ck Nothing + t <- lift $ newAccessToken @u @a ck Nothing return $ Access t (Just ck) resolveLoginId :: LoginId -> ExceptT LoginError AppIO UserId @@ -216,22 +222,23 @@ isPendingActivation ident = case ident of Nothing -> True validateTokens - :: ZAuth.UserToken - -> Maybe ZAuth.AccessToken - -> ExceptT ZAuth.Failure AppIO (UserId, Cookie ZAuth.UserToken) + :: ZAuth.TokenPair u a + => ZAuth.Token u + -> Maybe (ZAuth.Token a) + -> ExceptT ZAuth.Failure AppIO (UserId, Cookie (ZAuth.Token u)) validateTokens ut at = do unless (maybe True ((ZAuth.userTokenOf ut ==) . ZAuth.accessTokenOf) at) $ throwE ZAuth.Invalid ExceptT (ZAuth.validateToken ut) - forM_ at $ \a -> - ExceptT (ZAuth.validateToken a) + forM_ at $ \token -> + ExceptT (ZAuth.validateToken token) `catchE` \e -> unless (e == ZAuth.Expired) (throwE e) ck <- lift (lookupCookie ut) >>= maybe (throwE ZAuth.Invalid) return return (ZAuth.userTokenOf ut, ck) -- | Allow to login as any user without having the credentials. -ssoLogin :: SsoLogin -> CookieType -> ExceptT LoginError AppIO Access +ssoLogin :: SsoLogin -> CookieType -> ExceptT LoginError AppIO (Access ZAuth.User) ssoLogin (SsoLogin uid label) typ = do Data.reauthenticate uid Nothing `catchE` \case ReAuthMissingPassword -> pure () @@ -240,4 +247,26 @@ ssoLogin (SsoLogin uid label) typ = do AuthSuspended -> throwE LoginSuspended AuthEphemeral -> throwE LoginEphemeral AuthInvalidUser -> throwE LoginFailed - newAccess uid typ label + newAccess @ZAuth.User @ZAuth.Access uid typ label + +-- | Log in as a LegalHold service, getting LegalHoldUser/Access Tokens. +legalHoldLogin :: LegalHoldLogin -> CookieType -> ExceptT LegalHoldLoginError AppIO (Access ZAuth.LegalHoldUser) +legalHoldLogin (LegalHoldLogin uid plainTextPassword label) typ = do + Data.reauthenticate uid plainTextPassword !>> LegalHoldReAuthError + -- legalhold login is only possible if + -- * the user is a team user + -- * and the team has legalhold enabled + mteam <- lift $ Intra.getTeamId uid + case mteam of + Nothing -> throwE LegalHoldLoginNoBindingTeam + Just tid -> assertLegalHoldEnabled tid + -- create access token and cookie + newAccess @ZAuth.LegalHoldUser @ZAuth.LegalHoldAccess uid typ label + !>> LegalHoldLoginError + +assertLegalHoldEnabled :: TeamId -> ExceptT LegalHoldLoginError AppIO () +assertLegalHoldEnabled tid = do + LegalHoldTeamConfig stat <- lift $ Intra.getTeamLegalHoldStatus tid + case stat of + LegalHoldDisabled -> throwE LegalHoldLoginLegalHoldNotEnabled + LegalHoldEnabled -> pure () diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index 89c763fd2d5..f5485f2a51e 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -30,6 +30,7 @@ import Brig.User.Auth.Cookie.Limit import Control.Lens (view, to) import Data.ByteString.Conversion import Data.Id +import Data.Proxy import Data.Text.Encoding (encodeUtf8) import Data.Time.Clock import Network.Wai (Response) @@ -46,16 +47,16 @@ import qualified Brig.ZAuth as ZAuth -------------------------------------------------------------------------------- -- Basic Cookie Management -newCookie - :: UserId +newCookie :: ZAuth.UserTokenLike u + => UserId -> CookieType -> Maybe CookieLabel - -> AppIO (Cookie ZAuth.UserToken) -newCookie u typ label = do + -> AppIO (Cookie (ZAuth.Token u)) +newCookie uid typ label = do now <- liftIO =<< view currentTime tok <- if typ == PersistentCookie - then ZAuth.newUserToken u - else ZAuth.newSessionToken u + then ZAuth.newUserToken uid + else ZAuth.newSessionToken uid let c = Cookie { cookieId = CookieId (ZAuth.userTokenRand tok) , cookieCreated = now @@ -65,12 +66,12 @@ newCookie u typ label = do , cookieSucc = Nothing , cookieValue = tok } - DB.insertCookie u c Nothing + DB.insertCookie uid c Nothing return c -- | Renew the given cookie with a fresh token, if its age -- exceeds the configured minimum threshold. -nextCookie :: Cookie ZAuth.UserToken -> AppIO (Maybe (Cookie ZAuth.UserToken)) +nextCookie :: ZAuth.UserTokenLike u => Cookie (ZAuth.Token u) -> AppIO (Maybe (Cookie (ZAuth.Token u))) nextCookie c = do s <- view settings now <- liftIO =<< view currentTime @@ -86,28 +87,28 @@ nextCookie c = do getNext = case cookieSucc c of Nothing -> renewCookie c Just ck -> do - let u = ZAuth.userTokenOf (cookieValue c) - trackSuperseded u (cookieId c) - cs <- DB.listCookies u + let uid = ZAuth.userTokenOf (cookieValue c) + trackSuperseded uid (cookieId c) + cs <- DB.listCookies uid case List.find (\x -> cookieId x == ck && persist x) cs of Nothing -> renewCookie c Just c' -> do - t <- ZAuth.mkUserToken u (cookieIdNum ck) (cookieExpires c') + t <- ZAuth.mkUserToken uid (cookieIdNum ck) (cookieExpires c') return c' { cookieValue = t } -- | Renew the given cookie with a fresh token. -renewCookie :: Cookie ZAuth.UserToken -> AppIO (Cookie ZAuth.UserToken) +renewCookie :: ZAuth.UserTokenLike u => Cookie (ZAuth.Token u) -> AppIO (Cookie (ZAuth.Token u)) renewCookie old = do let t = cookieValue old - let u = ZAuth.userTokenOf t + let uid = ZAuth.userTokenOf t -- Insert new cookie - new <- newCookie u (cookieType old) (cookieLabel old) + new <- newCookie uid (cookieType old) (cookieLabel old) -- Link the old cookie to the new (successor), keeping it -- around only for another renewal period so as not to build -- an ever growing chain of superseded cookies. let old' = old { cookieSucc = Just (cookieId new) } ttl <- setUserCookieRenewAge <$> view settings - DB.insertCookie u old' (Just (DB.TTL (fromIntegral ttl))) + DB.insertCookie uid old' (Just (DB.TTL (fromIntegral ttl))) return new -- | Whether a user has not renewed any of her cookies for longer than @@ -134,19 +135,20 @@ mustSuspendInactiveUser uid = view (settings . to setSuspendInactiveUsers) >>= \ pure mustSuspend -newAccessToken :: Cookie ZAuth.UserToken -> Maybe ZAuth.AccessToken -> AppIO AccessToken +newAccessToken :: forall u a . ZAuth.TokenPair u a => Cookie (ZAuth.Token u) -> Maybe (ZAuth.Token a) -> AppIO AccessToken newAccessToken c mt = do t' <- case mt of Nothing -> ZAuth.newAccessToken (cookieValue c) Just t -> ZAuth.renewAccessToken t - ttl <- view (zauthEnv.ZAuth.settings.ZAuth.accessTokenTimeout) + zSettings <- view (zauthEnv.ZAuth.settings) + let ttl = view (ZAuth.settingsTTL (Proxy @a)) zSettings return $ bearerToken (ZAuth.accessTokenOf t') (toByteString t') - (ZAuth.accessTokenTimeoutSeconds ttl) + ttl -- | Lookup the stored cookie associated with a user token, -- if one exists. -lookupCookie :: ZAuth.UserToken -> AppIO (Maybe (Cookie ZAuth.UserToken)) +lookupCookie :: ZAuth.UserTokenLike u => ZAuth.Token u -> AppIO (Maybe (Cookie (ZAuth.Token u))) lookupCookie t = do let user = ZAuth.userTokenOf t let rand = ZAuth.userTokenRand t @@ -177,10 +179,11 @@ revokeCookies u ids labels = do -- Limited Cookies newCookieLimited - :: UserId + :: ZAuth.UserTokenLike t + => UserId -> CookieType -> Maybe CookieLabel - -> AppIO (Either RetryAfter (Cookie ZAuth.UserToken)) + -> AppIO (Either RetryAfter (Cookie (ZAuth.Token t))) newCookieLimited u typ label = do cs <- filter ((typ ==) . cookieType) <$> DB.listCookies u now <- liftIO =<< view currentTime @@ -199,8 +202,8 @@ newCookieLimited u typ label = do -- HTTP setResponseCookie - :: Monad m - => Cookie ZAuth.UserToken + :: (Monad m, ZAuth.UserTokenLike u) + => Cookie (ZAuth.Token u) -> Response -> AppT m Response setResponseCookie c r = do diff --git a/services/brig/src/Brig/ZAuth.hs b/services/brig/src/Brig/ZAuth.hs index da4d364c54d..d03835bffdf 100644 --- a/services/brig/src/Brig/ZAuth.hs +++ b/services/brig/src/Brig/ZAuth.hs @@ -20,20 +20,23 @@ module Brig.ZAuth , defSettings , localSettings , keyIndex - , UserTokenTimeout (..) - , userTokenTimeout , SessionTokenTimeout (..) , sessionTokenTimeout - , AccessTokenTimeout (..) - , accessTokenTimeout , ProviderTokenTimeout (..) , providerTokenTimeout + -- * timeout settings for access and legalholdaccess + , settingsTTL + , userTTL + -- * Token Creation + , Token , UserToken , AccessToken , ProviderToken , BotToken + , LegalHoldUserToken + , LegalHoldAccessToken , mkUserToken , newUserToken , newSessionToken @@ -42,6 +45,11 @@ module Brig.ZAuth , newBotToken , renewAccessToken + -- * TODO find a better names? + , UserTokenLike + , AccessTokenLike + , TokenPair + -- * Token Validation , validateToken , ZV.Failure (..) @@ -49,10 +57,13 @@ module Brig.ZAuth -- * Token Inspection , accessTokenOf , userTokenOf + , legalHoldAccessTokenOf + , legalHoldUserTokenOf , userTokenRand , tokenExpires , tokenExpiresUTC , tokenKeyIndex + , zauthType -- * Re-exports , SecretKey @@ -60,10 +71,11 @@ module Brig.ZAuth ) where import Imports -import Control.Lens ((^.), makeLenses, over) +import Control.Lens ((^.), makeLenses, over, Lens') import Data.Aeson import Data.Bits -import Data.ByteString.Conversion.To +import Data.Proxy +import Data.ByteString.Conversion import Data.Id import Data.List.NonEmpty (NonEmpty, nonEmpty) import Data.Time.Clock @@ -71,6 +83,7 @@ import Data.Time.Clock.POSIX import Data.ZAuth.Token import OpenSSL.Random import Sodium.Crypto.Sign +import Control.Monad.Catch import qualified Data.ByteString as BS import qualified Data.List.NonEmpty as NonEmpty @@ -90,20 +103,24 @@ runZAuth :: MonadIO m => Env -> ZAuth a -> m a runZAuth e za = liftIO $ runReaderT (unZAuth za) e data Settings = Settings - { _keyIndex :: !Int -- ^ Secret key index to use - -- for token creation - , _userTokenTimeout :: !UserTokenTimeout -- ^ User token validity timeout - , _sessionTokenTimeout :: !SessionTokenTimeout -- ^ Session token validity timeout - , _accessTokenTimeout :: !AccessTokenTimeout -- ^ Access token validity timeout - , _providerTokenTimeout :: !ProviderTokenTimeout -- ^ Access token validity timeout + { _keyIndex :: !Int -- ^ Secret key index to use + -- for token creation + , _userTokenTimeout :: !UserTokenTimeout -- ^ User token validity timeout + , _sessionTokenTimeout :: !SessionTokenTimeout -- ^ Session token validity timeout + , _accessTokenTimeout :: !AccessTokenTimeout -- ^ Access token validity timeout + , _providerTokenTimeout :: !ProviderTokenTimeout -- ^ Proider token validity timeout + , _legalHoldUserTokenTimeout :: !LegalHoldUserTokenTimeout -- ^ Legal Hold User token validity timeout + , _legalHoldAccessTokenTimeout :: !LegalHoldAccessTokenTimeout -- ^ Legal Hold Access token validity timeout } deriving (Show, Generic) defSettings :: Settings defSettings = Settings 1 - (UserTokenTimeout (60 * 60 * 24 * 28)) -- 28 days - (SessionTokenTimeout (60 * 60 * 24)) -- 1 day - (AccessTokenTimeout 900) -- 15 minutes - (ProviderTokenTimeout (60 * 60 * 24 * 7)) -- 7 days + (UserTokenTimeout (60 * 60 * 24 * 28)) -- 28 days + (SessionTokenTimeout (60 * 60 * 24)) -- 1 day + (AccessTokenTimeout 900) -- 15 minutes + (ProviderTokenTimeout (60 * 60 * 24 * 7)) -- 7 days + (LegalHoldUserTokenTimeout (60 * 60 * 24 * 56)) -- 56 days + (LegalHoldAccessTokenTimeout (60 * 15)) -- 15 minutes data Env = Env { _private :: !ZC.Env @@ -111,13 +128,15 @@ data Env = Env , _settings :: !Settings } -type AccessToken = Token Access -type UserToken = Token User -type ProviderToken = Token Provider -type BotToken = Token Bot +type AccessToken = Token Access +type UserToken = Token User +type ProviderToken = Token Provider +type BotToken = Token Bot +type LegalHoldUserToken = Token LegalHoldUser +type LegalHoldAccessToken = Token LegalHoldAccess newtype UserTokenTimeout = UserTokenTimeout - { userTokenTimeoutSeconds :: Integer } + { _userTokenTimeoutSeconds :: Integer } deriving (Show, Generic) newtype SessionTokenTimeout = SessionTokenTimeout @@ -125,17 +144,27 @@ newtype SessionTokenTimeout = SessionTokenTimeout deriving (Show, Generic) newtype AccessTokenTimeout = AccessTokenTimeout - { accessTokenTimeoutSeconds :: Integer } + { _accessTokenTimeoutSeconds :: Integer } deriving (Show, Generic) newtype ProviderTokenTimeout = ProviderTokenTimeout { providerTokenTimeoutSeconds :: Integer } deriving (Show, Generic) +newtype LegalHoldUserTokenTimeout = LegalHoldUserTokenTimeout + { _legalHoldUserTokenTimeoutSeconds :: Integer } + deriving (Show, Generic) + +newtype LegalHoldAccessTokenTimeout = LegalHoldAccessTokenTimeout + { _legalHoldAccessTokenTimeoutSeconds :: Integer } + deriving (Show, Generic) + instance FromJSON UserTokenTimeout instance FromJSON SessionTokenTimeout instance FromJSON AccessTokenTimeout instance FromJSON ProviderTokenTimeout +instance FromJSON LegalHoldAccessTokenTimeout +instance FromJSON LegalHoldUserTokenTimeout instance FromJSON Settings where parseJSON = withObject "ZAuth.Settings" $ \o -> @@ -144,8 +173,14 @@ instance FromJSON Settings where (UserTokenTimeout <$> o .: "userTokenTimeout") <*> (SessionTokenTimeout <$> o .: "sessionTokenTimeout") <*> (AccessTokenTimeout <$> o .: "accessTokenTimeout") <*> - (ProviderTokenTimeout <$> o .: "providerTokenTimeout") - + (ProviderTokenTimeout <$> o .: "providerTokenTimeout") <*> + (LegalHoldUserTokenTimeout <$> o .: "legalHoldUserTokenTimeout") <*> + (LegalHoldAccessTokenTimeout <$> o .: "legalHoldAccessTokenTimeout") + +makeLenses ''LegalHoldAccessTokenTimeout +makeLenses ''AccessTokenTimeout +makeLenses ''UserTokenTimeout +makeLenses ''LegalHoldUserTokenTimeout makeLenses ''Settings makeLenses ''Env @@ -161,37 +196,88 @@ mkEnv sk pk sets = do let zv = ZV.mkEnv (NonEmpty.head pk) (NonEmpty.tail pk) return $! Env zc zv sets -mkUserToken :: MonadZAuth m => UserId -> Word32 -> UTCTime -> m UserToken -mkUserToken u r t = liftZAuth $ do +class (UserTokenLike u, AccessTokenLike a) => TokenPair u a where + newAccessToken :: MonadZAuth m => Token u -> m (Token a) + +instance TokenPair User Access where + newAccessToken = newAccessToken' + +instance TokenPair LegalHoldUser LegalHoldAccess where + newAccessToken = newLegalHoldAccessToken + +class (FromByteString (Token a), ToByteString a) => AccessTokenLike a where + accessTokenOf :: Token a -> UserId + renewAccessToken :: MonadZAuth m => Token a -> m (Token a) + settingsTTL :: Proxy a -> Lens' Settings Integer + +instance AccessTokenLike Access where + accessTokenOf = accessTokenOf' + renewAccessToken = renewAccessToken' + settingsTTL _ = accessTokenTimeout . accessTokenTimeoutSeconds + +instance AccessTokenLike LegalHoldAccess where + accessTokenOf = legalHoldAccessTokenOf + renewAccessToken = renewLegalHoldAccessToken + settingsTTL _ = legalHoldAccessTokenTimeout . legalHoldAccessTokenTimeoutSeconds + +class (FromByteString (Token u), ToByteString u) => UserTokenLike u where + userTokenOf :: Token u -> UserId + mkUserToken :: MonadZAuth m => UserId -> Word32 -> UTCTime -> m (Token u) + userTokenRand :: Token u -> Word32 + newUserToken :: MonadZAuth m => UserId -> m (Token u) + newSessionToken :: (MonadThrow m, MonadZAuth m) => UserId -> m (Token u) + userTTL :: Proxy u -> Lens' Settings Integer + zauthType :: Type -- see libs/zauth/src/Token.hs + +instance UserTokenLike User where + mkUserToken = mkUserToken' + userTokenOf = userTokenOf' + userTokenRand = userTokenRand' + newUserToken = newUserToken' + newSessionToken uid = newSessionToken' uid + userTTL _ = userTokenTimeout . userTokenTimeoutSeconds + zauthType = U + +instance UserTokenLike LegalHoldUser where + mkUserToken = mkLegalHoldUserToken + userTokenOf = legalHoldUserTokenOf + userTokenRand = legalHoldUserTokenRand + newUserToken = newLegalHoldUserToken + newSessionToken _ = throwM ZV.Unsupported + userTTL _ = legalHoldUserTokenTimeout . legalHoldUserTokenTimeoutSeconds + zauthType = LU + +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) -newUserToken :: MonadZAuth m => UserId -> m UserToken -newUserToken u = liftZAuth $ do +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 -newSessionToken :: MonadZAuth m => UserId -> m UserToken -newSessionToken u = liftZAuth $ do +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 -newAccessToken :: MonadZAuth m => UserToken -> m AccessToken -newAccessToken xt = liftZAuth $ do +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) -renewAccessToken :: MonadZAuth m => AccessToken -> m AccessToken -renewAccessToken old = liftZAuth $ do +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 @@ -210,6 +296,40 @@ newProviderToken pid = liftZAuth $ do let ProviderTokenTimeout ttl = z^.settings.providerTokenTimeout in ZC.providerToken ttl (toUUID pid) +-- FUTUREWORK: this function is very similar to mkUserToken', +-- the differences are +-- 1) LU / U +-- 2) (mkLegalHoldUser uid r) / (mkUser uid r) +-- Possibly some duplication could be removed. +-- See https://github.com/wireapp/wire-server/pull/761/files#r318612423 +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) + +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 + +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) + +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 + validateToken :: (MonadZAuth m, ToByteString a) => Token a -> m (Either ZV.Failure ()) @@ -217,14 +337,23 @@ validateToken t = liftZAuth $ do z <- ask void <$> ZV.runValidate (z^.public) (ZV.check t) -accessTokenOf :: AccessToken -> UserId -accessTokenOf t = Id (t^.body.userId) +accessTokenOf' :: AccessToken -> UserId +accessTokenOf' t = Id (t^.body.userId) + +userTokenOf' :: UserToken -> UserId +userTokenOf' t = Id (t^.body.user) + +legalHoldAccessTokenOf :: LegalHoldAccessToken -> UserId +legalHoldAccessTokenOf t = Id (t^.body.legalHoldAccess.userId) + +legalHoldUserTokenOf :: LegalHoldUserToken -> UserId +legalHoldUserTokenOf t = Id (t^.body.legalHoldUser.user) -userTokenOf :: UserToken -> UserId -userTokenOf t = Id (t^.body.user) +userTokenRand' :: UserToken -> Word32 +userTokenRand' t = t^.body.rand -userTokenRand :: UserToken -> Word32 -userTokenRand t = t^.body.rand +legalHoldUserTokenRand :: LegalHoldUserToken -> Word32 +legalHoldUserTokenRand t = t^.body.legalHoldUser.rand tokenKeyIndex :: Token a -> Int tokenKeyIndex t = t^.header.key diff --git a/services/brig/test/integration/API/Metrics.hs b/services/brig/test/integration/API/Metrics.hs index e0b74daea55..e88dbe40920 100644 --- a/services/brig/test/integration/API/Metrics.hs +++ b/services/brig/test/integration/API/Metrics.hs @@ -47,16 +47,12 @@ testMonitoringEndpoint brig = do _ <- get (brig . path (p2 $ toByteString' uid) . zAuthAccess uid "conn" . expect2xx) _ <- get (brig . path (p2 $ toByteString' uid') . zAuthAccess uid "conn" . expect2xx) - resp :: Value <- jsonBody <$> get (brig . path "i/monitoring") + resp :: Value <- responseJsonUnsafe <$> get (brig . path "i/monitoring") let have :: Set Text = Set.fromList $ fst <$> (resp ^@.. key "net" . key "resources" . members) want :: Set Text = Set.fromList $ cs <$> [p1, p2 ":uid"] errmsg = "some of " <> show want <> " missing in metrics: " <> show have liftIO $ assertBool errmsg (want `Set.isSubsetOf` have) --- | Only for testing! I've seen this in our code-base, but I can't find it any more. -jsonBody :: ResponseLBS -> Value -jsonBody = either (error . show) id . eitherDecode . fromJust . responseBody - {- FUTUREWORK: diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 9dbe9abab05..3a5b132ac48 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -183,7 +183,7 @@ testUpdateProvider db brig = do } updateProvider brig pid upd !!! const 200 === statusCode _rs <- getProvider brig pid rsNewServiceToken srs) (Just (serviceTokens svc)) assertEqual "name" defServiceName (serviceName svc) @@ -316,7 +316,7 @@ testAddGetService config db brig = do -- Get public service profile uid <- randomId _rs <- getServiceProfile brig uid pid sid DB.ClientState -> Brig -> Http () @@ -386,7 +386,7 @@ testUpdateServiceConn config db brig = do updateServiceConn brig pid sid upd !!! const 200 === statusCode _rs <- getService brig pid sid decodeBody _rs + let Just uc = clientId <$> responseJsonMaybe _rs -- Create conversation _rs <- createConv galley uid [] decodeBody _rs + let Just cid = cnvId <$> responseJsonMaybe _rs testMessageBotUtil uid uc cid pid sid sref buf brig galley cannon @@ -566,7 +566,7 @@ testBadFingerprint config db brig galley _cannon = do _rs <- addClient brig uid new decodeBody _rs + let Just cid = cnvId <$> responseJsonMaybe _rs -- Try to add a bot and observe failure addBot brig uid pid sid cid !!! const 502 === statusCode @@ -579,7 +579,7 @@ testAddRemoveBotTeam config db brig galley cannon = withTestService config db br cidFail <- Team.createManagedConv galley tid uid1 [uid2] Nothing addBot brig uid1 pid sid cidFail !!! do const 403 === statusCode - const (Just "invalid-conversation") === fmap Error.label . decodeBody + const (Just "invalid-conversation") === fmap Error.label . responseJsonMaybe testAddRemoveBotUtil pid sid cid u1 u2 h sref buf brig galley cannon testBotTeamOnlyConv :: Config -> DB.ClientState -> Brig -> Galley -> Cannon -> Http () @@ -591,7 +591,7 @@ testBotTeamOnlyConv config db brig galley cannon = withTestService config db bri setAccessRole uid1 cid TeamAccessRole addBot brig uid1 pid sid cid !!! do const 403 === statusCode - const (Just "invalid-conversation") === fmap Error.label . decodeBody + const (Just "invalid-conversation") === fmap Error.label . responseJsonMaybe -- Make the conversation allowed for guests and add the bot successfully setAccessRole uid1 cid NonActivatedAccessRole bid <- addBotConv brig cannon uid1 uid2 cid pid sid buf @@ -620,7 +620,7 @@ testMessageBotTeam config db brig galley cannon = withTestService config db brig (uid, tid) <- Team.createUserWithTeam brig galley let new = defNewClient PermanentClientType [somePrekeys !! 0] (someLastPrekeys !! 0) _rs <- addClient brig uid new decodeBody _rs + let Just uc = clientId <$> responseJsonMaybe _rs -- Whitelist the bot whitelistService brig uid tid pid sid @@ -687,7 +687,7 @@ testWhitelistSearchPermissions _config _db brig galley = do nonMember <- userId <$> randomUser brig listTeamServiceProfilesByPrefix brig nonMember tid Nothing True 20 !!! do const 403 === statusCode - const (Just "insufficient-permissions") === fmap Error.label . decodeBody + const (Just "insufficient-permissions") === fmap Error.label . responseJsonMaybe -- Check that team members with no permissions can search member <- userId <$> Team.createTeamMember brig galley owner tid Team.noPermissions listTeamServiceProfilesByPrefix brig member tid Nothing True 20 !!! @@ -709,12 +709,12 @@ testWhitelistUpdatePermissions config db brig galley = do _uid <- userId <$> randomUser brig updateServiceWhitelist brig _uid tid (UpdateServiceWhitelist pid sid True) !!! do const 403 === statusCode - const (Just "insufficient-permissions") === fmap Error.label . decodeBody + const (Just "insufficient-permissions") === fmap Error.label . responseJsonMaybe -- Check that a member who's not a team admin also can't add it to the whitelist _uid <- userId <$> Team.createTeamMember brig galley owner tid Team.noPermissions updateServiceWhitelist brig _uid tid (UpdateServiceWhitelist pid sid True) !!! do const 403 === statusCode - const (Just "insufficient-permissions") === fmap Error.label . decodeBody + const (Just "insufficient-permissions") === fmap Error.label . responseJsonMaybe -- Check that a team admin can add and remove from the whitelist whitelistService brig admin tid pid sid dewhitelistService brig admin tid pid sid @@ -843,10 +843,10 @@ testWhitelistBasic config db brig galley = cid <- Team.createTeamConv galley tid owner [] Nothing addBot brig owner pid sid cid !!! do const 403 === statusCode - const (Just "service-not-whitelisted") === fmap Error.label . decodeBody + const (Just "service-not-whitelisted") === fmap Error.label . responseJsonMaybe -- Check that after whitelisting the service, it can be added to the conversation whitelistService brig owner tid pid sid - bid <- fmap rsAddBotId . decodeBody =<< + bid <- fmap rsAddBotId . responseJsonError =<< (addBot brig owner pid sid cid decodeBody _rs + let Just pid = rsNewProviderId <$> responseJsonMaybe _rs -- Activate (auto-approval) Just vcode <- lookupCode db gen Code.IdentityVerification activateProvider brig (Code.codeKey vcode) (Code.codeValue vcode) !!! const 200 === statusCode -- Fetch _rs <- getProvider brig pid Brig -> ProviderId -> NewService -> Http Service addGetService brig pid new = do _rs <- addService brig pid new Brig -> ProviderId -> ServiceId -> Http () @@ -1774,7 +1774,7 @@ testAddRemoveBotUtil pid sid cid u1 u2 h sref buf brig galley cannon = do -- including the bot itself. (rs, bot) <- WS.bracketR2 cannon uid1 uid2 $ \(ws1, ws2) -> do _rs <- addBot brig uid1 pid sid cid do -- 200 response with event on success _rs <- removeBot brig uid2 cid bid decodeBody _rs + let Just ev = rsRemoveBotEvent <$> responseJsonMaybe _rs liftIO $ assertEqual "bot event" MemberLeave (evtType ev) -- Events for both users forM_ [ws1, ws2] $ \ws -> wsAssertMemberLeave ws cid uid2 [buid] @@ -1851,7 +1851,7 @@ testMessageBotUtil :: UserId testMessageBotUtil uid uc cid pid sid sref buf brig galley cannon = do -- Add bot to conversation _rs <- addBot brig uid pid sid cid other) @@ -1933,7 +1933,7 @@ addBotConv brig cannon uid1 uid2 cid pid sid buf = -- including the bot itself. WS.bracketR2 cannon uid1 uid2 $ \(ws1, ws2) -> do _rs <- addBot brig uid1 pid sid cid Http () searchAndAssertNameChange brig pid sid uid uniq search = do -- First let's figure out how the service is called now - origName <- fmap serviceProfileName . decodeBody =<< + origName <- fmap serviceProfileName . responseJsonError =<< (getServiceProfile brig uid pid sid error "searchServices: query not supported" (Just start, Nothing) -> - decodeBody =<< + responseJsonError =<< (listServiceProfilesByPrefix brig uid start size - decodeBody =<< + responseJsonError =<< (listServiceProfilesByTag brig uid tags mbStart size Brig -> Int -> UserId -> TeamId -> Maybe Text -> Http ServiceProfilePage searchServiceWhitelist brig size uid tid mbStart = - decodeBody =<< + responseJsonError =<< (listTeamServiceProfilesByPrefix brig uid tid mbStart True size Brig -> Int -> UserId -> TeamId -> Maybe Text -> Http ServiceProfilePage searchServiceWhitelistAll brig size uid tid mbStart = - decodeBody =<< + responseJsonError =<< (listTeamServiceProfilesByPrefix brig uid tid mbStart False size Right <$> decodeBody r + if | statusCode r == 200 -> Right <$> responseJsonError r | statusCode r `elem` [403, 404] -> pure . Left . statusCode $ r | otherwise -> error $ "expected status code 200, 403, or 404, got: " <> show (statusCode r) diff --git a/services/brig/test/integration/API/Search/Util.hs b/services/brig/test/integration/API/Search/Util.hs index 055cfe4dfa2..81d6bbfb5a8 100644 --- a/services/brig/test/integration/API/Search/Util.hs +++ b/services/brig/test/integration/API/Search/Util.hs @@ -77,4 +77,4 @@ assertSearchable :: HasCallStack => String -> (Request -> Request) -> UserId -> assertSearchable label brig uid status = do response <- get (brig . path "/self/searchable" . zUser uid) liftIO $ assertEqual (label ++ ", statuscode") 200 (statusCode response) - liftIO $ assertEqual label (Just status) (isSearchable <$> decodeBody response) + liftIO $ assertEqual label (Just status) (isSearchable <$> responseJsonMaybe response) diff --git a/services/brig/test/integration/API/Settings.hs b/services/brig/test/integration/API/Settings.hs index 145d4c5df06..badd75558ef 100644 --- a/services/brig/test/integration/API/Settings.hs +++ b/services/brig/test/integration/API/Settings.hs @@ -104,7 +104,7 @@ testUsersEmailVisibleIffExpected opts brig galley visibilitySetting = do const 200 === statusCode const (Just expected) === result where - result r = Set.fromList . map (jsonField "id" &&& jsonField "email") <$> decodeBody r + result r = Set.fromList . map (jsonField "id" &&& jsonField "email") <$> responseJsonMaybe r testGetUserEmailShowsEmailsIffExpected :: Opts -> Brig -> Galley -> Opt.EmailVisibility -> Http () testGetUserEmailShowsEmailsIffExpected opts brig galley visibilitySetting = do @@ -131,4 +131,4 @@ testGetUserEmailShowsEmailsIffExpected opts brig galley visibilitySetting = do const expectedEmail === emailResult where emailResult :: Response (Maybe LByteString) -> Maybe Email - emailResult r = decodeBody r >>= jsonField "email" + emailResult r = responseJsonMaybe r >>= jsonField "email" diff --git a/services/brig/test/integration/API/TURN.hs b/services/brig/test/integration/API/TURN.hs index ee229334235..549fa9eaf95 100644 --- a/services/brig/test/integration/API/TURN.hs +++ b/services/brig/test/integration/API/TURN.hs @@ -140,7 +140,7 @@ getTurnConfiguration suffix u b = get ( b getAndValidateTurnConfiguration :: HasCallStack => ByteString -> UserId -> Brig -> Http RTCConfiguration getAndValidateTurnConfiguration suffix u b = - decodeBody =<< (getTurnConfiguration suffix u b UserId -> Brig -> Http (Response (Maybe LB.ByteString)) getTurnConfigurationV2Limit limit u b = get ( b @@ -152,7 +152,7 @@ getTurnConfigurationV2Limit limit u b = get ( b getAndValidateTurnConfigurationLimit :: HasCallStack => Int -> UserId -> Brig -> Http RTCConfiguration getAndValidateTurnConfigurationLimit limit u b = - decodeBody =<< (getTurnConfigurationV2Limit limit u b Port -> TurnURI toTurnURILegacy h p = toTurnURI SchemeTurn h p Nothing diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index d6617f6916a..4bd556e009c 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -10,8 +10,7 @@ import Brig.Types.Team.Invitation import Brig.Types.User.Auth import Brig.Types.Intra import Control.Arrow ((&&&)) -import UnliftIO.Async - (mapConcurrently_, replicateConcurrently, pooledForConcurrentlyN_) +import UnliftIO.Async (mapConcurrently_, replicateConcurrently, pooledForConcurrentlyN_) import Control.Lens ((^.), view) import Data.Aeson import Data.ByteString.Conversion @@ -85,13 +84,13 @@ testUpdateEvents brig galley cannon = do -- invite and register Bob let invite = InvitationRequest inviteeEmail (Name "Bob") Nothing Nothing - inv <- decodeBody =<< postInvitation brig tid alice invite + inv <- responseJsonError =<< postInvitation brig tid alice invite Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) rsp2 <- post (brig . path "/register" . contentJson . body (accept inviteeEmail inviteeCode)) decodeBody rsp2 + let Just bob = userId <$> responseJsonMaybe rsp2 -- ensure Alice and Bob are not connected void $ getConnection brig bob alice Brig -> Galley -> Http () @@ -144,26 +143,26 @@ testInvitationRoles brig galley = do . contentJson . body (accept invemail inviteeCode)) decodeBody rsp + let Just invitee = userId <$> responseJsonMaybe rsp pure invitee -- owner creates a member alice. alice :: UserId <- do aliceEmail <- randomEmail let invite = InvitationRequest aliceEmail (Name "Alice") Nothing (Just Team.RoleAdmin) - inv :: Invitation <- decodeBody =<< postInvitation brig tid owner invite + inv :: Invitation <- responseJsonError =<< postInvitation brig tid owner invite registerInvite inv aliceEmail -- alice creates a external partner bob. success! bob only has externalPartner perms. do bobEmail <- randomEmail let invite = InvitationRequest bobEmail (Name "Bob") Nothing (Just Team.RoleExternalPartner) - inv :: Invitation <- decodeBody =<< (postInvitation brig tid alice invite Galley -> Http () testInvitationEmailAccepted brig galley = do (inviter, tid) <- createUserWithTeam brig galley inviteeEmail <- randomEmail let invite = InvitationRequest inviteeEmail (Name "Bob") Nothing Nothing - inv <- decodeBody =<< postInvitation brig tid inviter invite + inv <- responseJsonError =<< postInvitation brig tid inviter invite let invmeta = Just (inviter, inCreatedAt inv) Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) rsp2 <- post (brig . path "/register" . contentJson . body (accept inviteeEmail inviteeCode)) decodeBody rsp2 + let Just (invitee, Just email2) = (userId &&& userEmail) <$> responseJsonMaybe rsp2 let zuid = parseSetCookie <$> getHeader "Set-Cookie" rsp2 liftIO $ assertEqual "Wrong cookie" (Just "zuid") (setCookieName <$> zuid) -- Verify that the invited user is active @@ -201,7 +200,7 @@ testInvitationEmailAccepted brig galley = do testCreateTeam :: Brig -> Galley -> AWS.Env -> Http () testCreateTeam brig galley aws = do email <- randomEmail - usr <- decodeBody' =<< register email newTeam brig + usr <- responseJsonError =<< register email newTeam brig let uid = userId usr -- Verify that the user is part of exactly one (binding) team teams <- view Team.teamListTeams <$> getTeams uid galley @@ -235,7 +234,7 @@ testCreateTeamPreverified brig galley aws = do case act of Nothing -> liftIO $ assertFailure "activation key/code not found" Just (_, c) -> do - usr <- decodeBody' =<< register' email newTeam c brig getTeams uid galley @@ -259,7 +258,7 @@ testInvitationNoPermission brig galley = do let invite = InvitationRequest email (Name "Bob") Nothing Nothing postInvitation brig tid alice invite !!! do const 403 === statusCode - const (Just "insufficient-permissions") === fmap Error.label . decodeBody + const (Just "insufficient-permissions") === fmap Error.label . responseJsonMaybe testTeamNoPassword :: Brig -> Http () testTeamNoPassword brig = do @@ -289,7 +288,7 @@ testInvitationCodeExists brig galley = do let invite email_ = InvitationRequest email_ (Name "Bob") Nothing Nothing rsp <- postInvitation brig tid uid (invite email) decodeBody rsp + let Just invId = inInvitation <$> responseJsonMaybe rsp Just invCode <- getInvitationCode brig tid invId post (brig . path "/register" . contentJson . body (accept email invCode)) !!! @@ -297,12 +296,12 @@ testInvitationCodeExists brig galley = do post (brig . path "/register" . contentJson . body (accept email invCode)) !!! do const 409 === statusCode - const (Just "key-exists") === fmap Error.label . decodeBody + const (Just "key-exists") === fmap Error.label . responseJsonMaybe email2 <- randomEmail post (brig . path "/register" . contentJson . body (accept email2 invCode)) !!! do const 400 === statusCode - const (Just "invalid-invitation-code") === fmap Error.label . decodeBody + const (Just "invalid-invitation-code") === fmap Error.label . responseJsonMaybe testInvitationInvalidCode :: Brig -> Http () testInvitationInvalidCode brig = do @@ -311,19 +310,19 @@ testInvitationInvalidCode brig = do let code1 = InvitationCode (Ascii.unsafeFromText "8z6JVcO1o4o¿9kFeb4Y3N-BmhIjH6b33") post (brig . path "/register" . contentJson . body (accept email code1)) !!! do const 400 === statusCode - const (Just "bad-request") === fmap Error.label . decodeBody + const (Just "bad-request") === fmap Error.label . responseJsonMaybe -- Syntactically valid but semantically invalid code2 <- liftIO $ InvitationCode . Ascii.encodeBase64Url <$> randomBytes 24 post (brig . path "/register" . contentJson . body (accept email code2)) !!! do const 400 === statusCode - const (Just "invalid-invitation-code") === fmap Error.label . decodeBody + const (Just "invalid-invitation-code") === fmap Error.label . responseJsonMaybe testInvitationCodeNoIdentity :: Brig -> Http () testInvitationCodeNoIdentity brig = do uid <- liftIO $ Id <$> UUID.nextRandom post (brig . path "/register" . contentJson . body (payload uid)) !!! do const 403 === statusCode - const (Just "missing-identity") === fmap Error.label . decodeBody + const (Just "missing-identity") === fmap Error.label . responseJsonMaybe where payload u = RequestBodyLBS . encode $ object [ "name" .= ("Bob" :: Text) @@ -361,13 +360,13 @@ testInvitationTooManyMembers brig galley (TeamSizeLimit limit) = do em <- randomEmail let invite = InvitationRequest em (Name "Bob") Nothing Nothing - inv <- decodeBody =<< postInvitation brig tid creator invite + inv <- responseJsonError =<< postInvitation brig tid creator invite Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) post (brig . path "/register" . contentJson . body (accept em inviteeCode)) !!! do const 403 === statusCode - const (Just "too-many-team-members") === fmap Error.label . decodeBody + const (Just "too-many-team-members") === fmap Error.label . responseJsonMaybe testInvitationPaging :: HasCallStack => Brig -> Galley -> Http () testInvitationPaging brig galley = do @@ -389,7 +388,7 @@ testInvitationPaging brig galley = do let range = queryRange (toByteString' <$> start) (Just step) r <- get (brig . paths ["teams", toByteString' tid, "invitations"] . zUser uid . range) decodeBody r + let (Just (invs, more)) = (ilInvitations &&& ilHasMore) <$> responseJsonMaybe r liftIO $ assertEqual "page size" actualPageLen (length invs) liftIO $ assertEqual "has more" (count' < total) more liftIO $ validateInv `mapM_` invs @@ -414,7 +413,7 @@ testInvitationInfo brig galley = do email <- randomEmail (uid, tid) <- createUserWithTeam brig galley let invite = InvitationRequest email (Name "Bob") Nothing Nothing - inv <- decodeBody =<< postInvitation brig tid uid invite + inv <- responseJsonError =<< postInvitation brig tid uid invite Just invCode <- getInvitationCode brig tid (inInvitation inv) Just invitation <- getInvitation brig invCode @@ -433,7 +432,7 @@ testInvitationInfoExpired brig galley timeout = do email <- randomEmail (uid, tid) <- createUserWithTeam brig galley let invite = InvitationRequest email (Name "Bob") Nothing Nothing - inv <- decodeBody =<< postInvitation brig tid uid invite + 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) getCode tid (inInvitation inv) !!! const 400 === statusCode @@ -460,17 +459,17 @@ testSuspendTeam brig galley = do -- invite and register invitee let invite = InvitationRequest inviteeEmail (Name "Bob") Nothing Nothing - inv <- decodeBody =<< postInvitation brig tid inviter invite + inv <- responseJsonError =<< postInvitation brig tid inviter invite Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) rsp2 <- post (brig . path "/register" . contentJson . body (accept inviteeEmail inviteeCode)) decodeBody rsp2 + let Just (invitee, Just email) = (userId &&& userEmail) <$> responseJsonMaybe rsp2 -- invite invitee2 (don't register) let invite2 = InvitationRequest inviteeEmail2 (Name "Bob") Nothing Nothing - inv2 <- decodeBody =<< postInvitation brig tid inviter invite2 + inv2 <- responseJsonError =<< postInvitation brig tid inviter invite2 Just _ <- getInvitationCode brig tid (inInvitation inv2) -- suspend team @@ -478,7 +477,7 @@ testSuspendTeam brig galley = do -- login fails login brig (defEmailLogin email) PersistentCookie !!! do const 403 === statusCode - const (Just "suspended") === fmap Error.label . decodeBody + const (Just "suspended") === fmap Error.label . responseJsonMaybe -- check status chkStatus brig inviter Suspended chkStatus brig invitee Suspended @@ -497,13 +496,13 @@ testDeleteTeamUser brig galley = do -- Cannot delete the user since it will make the team orphan deleteUser creator (Just defPassword) brig !!! do const 403 === statusCode - const (Just "no-other-owner") === fmap Error.label . decodeBody + const (Just "no-other-owner") === fmap Error.label . responseJsonMaybe -- We need to invite another user to a full permission member invitee <- userId <$> inviteAndRegisterUser creator tid brig -- Still cannot delete, need to make this a full permission member deleteUser creator (Just defPassword) brig !!! do const 403 === statusCode - const (Just "no-other-owner") === fmap Error.label . decodeBody + const (Just "no-other-owner") === fmap Error.label . responseJsonMaybe -- Let's promote the other user updatePermissions creator tid (invitee, Team.fullPermissions) galley -- Now the creator can delete the account @@ -520,7 +519,7 @@ testDeleteTeamUser brig galley = do deleteUser invitee (Just defPassword) brig !!! do const 403 === statusCode - const (Just "no-other-owner") === fmap Error.label . decodeBody + const (Just "no-other-owner") === fmap Error.label . responseJsonMaybe -- Ensure internal endpoints are also exercised deleteUserInternal invitee brig !!! const 202 === statusCode -- Eventually the user will be deleted, leaving the team orphan @@ -549,7 +548,7 @@ testConnectionSameTeam brig galley = do postConnection brig creatorA inviteeA !!! do const 403 === statusCode - const (Just "same-binding-team-users") === fmap Error.label . decodeBody + const (Just "same-binding-team-users") === fmap Error.label . responseJsonMaybe creatorB <- userId <$> randomUser brig @@ -575,7 +574,7 @@ testNonSearchableDefault brig galley = do Nothing -> liftIO $ assertFailure "activation key/code not found" Just kc -> activate brig kc !!! const 200 === statusCode - let Just creator = decodeBody rsp + let Just creator = responseJsonMaybe rsp [team] <- view Team.teamListTeams <$> getTeams (userId creator) galley let tid = view Team.teamId team invitee <- inviteAndRegisterUser (userId creator) tid brig @@ -619,10 +618,10 @@ testCreateUserInternalSSO brig galley = do -- creating user with sso_id, team_id is ok resp <- postUser "dummy" True False (Just ssoid) (Just teamid) brig decodeBody resp + let Just uid = userId <$> responseJsonMaybe resp profile <- getSelfProfile brig uid liftIO $ assertEqual "self profile user identity mismatch" (Just ssoid) @@ -643,7 +642,7 @@ testDeleteUserSSO brig galley = do (creator, tid) <- createUserWithTeam brig galley let ssoid = UserSSOId "nil" "nil" mkuser :: Bool -> Http (Maybe User) - mkuser withemail = decodeBody <$> + mkuser withemail = responseJsonMaybe <$> (postUser "dummy" withemail False (Just ssoid) (Just tid) brig getTeams (userId user) galley liftIO $ assertBool "Team ID in registration and team table do not match" (tid == view Team.teamId team) @@ -73,13 +74,13 @@ inviteAndRegisterUser :: UserId -> TeamId -> Brig -> Http User inviteAndRegisterUser u tid brig = do inviteeEmail <- randomEmail let invite = InvitationRequest inviteeEmail (Name "Bob") Nothing Nothing - inv <- decodeBody =<< postInvitation brig tid u invite + inv <- responseJsonError =<< postInvitation brig tid u invite Just inviteeCode <- getInvitationCode brig tid (inInvitation inv) rspInvitee <- post (brig . path "/register" . contentJson . body (accept inviteeEmail inviteeCode)) getSelfProfile brig (userId invitee) liftIO $ assertEqual "Team ID in self profile and team table do not match" selfTeam (Just tid) @@ -146,7 +147,7 @@ deleteTeam g tid u = do getTeams :: UserId -> Galley -> Http Team.TeamList getTeams u galley = - decodeBody =<< + responseJsonError =<< get ( galley . paths ["teams"] . zAuthAccess u "conn" @@ -156,6 +157,14 @@ getTeams u galley = newTeam :: Team.BindingNewTeam newTeam = Team.BindingNewTeam $ Team.newNewTeam (unsafeRange "teamName") (unsafeRange "defaultIcon") +putLegalHoldEnabled :: HasCallStack => TeamId -> LegalHoldStatus -> Galley -> Http () +putLegalHoldEnabled tid enabled g = do + void . put $ g + . paths ["i", "teams", toByteString' tid, "features", "legalhold"] + . contentJson + . lbytes (encode (LegalHoldTeamConfig enabled)) + . expect2xx + accept :: Email -> InvitationCode -> RequestBody accept email code = RequestBodyLBS . encode $ object [ "name" .= ("Bob" :: Text) @@ -187,7 +196,7 @@ register' e t c brig = post (brig . path "/register" . contentJson . body ( listConnections :: HasCallStack => UserId -> Brig -> Http UserConnectionList listConnections u brig = do - decodeBody =<< + responseJsonError =<< get ( brig . path "connections" . zUser u @@ -219,7 +228,7 @@ unsuspendTeam brig t = post $ brig getTeam :: HasCallStack => Galley -> TeamId -> Http Team.TeamData getTeam galley t = - decodeBody =<< get (galley . paths ["i", "teams", toByteString' t]) + responseJsonError =<< get (galley . paths ["i", "teams", toByteString' t]) getInvitationCode :: HasCallStack => Brig -> TeamId -> InvitationId -> Http (Maybe InvitationCode) getInvitationCode brig t ref = do @@ -239,14 +248,11 @@ assertNoInvitationCode brig t i = . queryItem "invitation_id" (toByteString' i) ) !!! do const 400 === statusCode - const (Just "invalid-invitation-code") === fmap Error.label . decodeBody - -decodeBody' :: (Typeable a, FromJSON a) => Response (Maybe LByteString) -> Http a -decodeBody' x = maybe (error $ "Failed to decodeBody: " ++ show x) return $ decodeBody x + const (Just "invalid-invitation-code") === fmap Error.label . responseJsonMaybe isActivatedUser :: UserId -> Brig -> Http Bool isActivatedUser uid brig = do resp <- get (brig . path "/i/users" . queryItem "ids" (toByteString' uid) . expect2xx) - pure $ case decodeBody @[User] resp of + pure $ case responseJsonMaybe @[User] resp of Just (_:_) -> True _ -> False diff --git a/services/brig/test/integration/API/User.hs b/services/brig/test/integration/API/User.hs index 4f2c19adcb8..9cb6dbcef93 100644 --- a/services/brig/test/integration/API/User.hs +++ b/services/brig/test/integration/API/User.hs @@ -22,15 +22,15 @@ import qualified Brig.AWS as AWS import qualified Brig.Options as Opt import qualified Brig.ZAuth as ZAuth -tests :: Maybe Opt.Opts -> Manager -> Brig -> Cannon -> CargoHold -> Galley -> AWS.Env -> IO TestTree -tests conf p b c ch g aws = do +tests :: Maybe Opt.Opts -> Manager -> Brig -> Cannon -> CargoHold -> Galley -> Nginz -> AWS.Env -> IO TestTree +tests conf p b c ch g n aws = do cl <- optOrEnv (ConnectionLimit . Opt.setUserMaxConnections . Opt.optSettings) conf (ConnectionLimit . read) "USER_CONNECTION_LIMIT" at <- optOrEnv (Opt.setActivationTimeout . Opt.optSettings) conf read "USER_ACTIVATION_TIMEOUT" z <- mkZAuthEnv conf return $ testGroup "user" [ API.User.Client.tests cl at conf p b c g , API.User.Account.tests cl at conf p b c ch g aws - , API.User.Auth.tests conf p z b + , API.User.Auth.tests conf p z b g n , API.User.Connection.tests cl at conf p b c g , API.User.Handles.tests cl at conf p b c g , API.User.Onboarding.tests cl at conf p b c g diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 4a8bac83d3c..c2999e8bf5a 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -106,7 +106,7 @@ testCreateUserWithPreverified brig aws = do let uid = userId usr get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode - const (Just p) === (userPhone <=< decodeBody) + const (Just p) === (userPhone <=< responseJsonMaybe) liftIO $ Util.assertUserJournalQueue "user activate" aws (userActivateJournaled usr) @@ -126,7 +126,7 @@ testCreateUserWithPreverified brig aws = do let uid = userId usr get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode - const (Just e) === (userEmail <=< decodeBody) + const (Just e) === (userEmail <=< responseJsonMaybe) liftIO $ Util.assertUserJournalQueue "user activate" aws (userActivateJournaled usr) @@ -161,7 +161,7 @@ testCreateUserAnon brig galley = do liftIO $ assertBool "Missing zuid cookie" (isJust zuid) -- Every registered user gets a self conversation. - let Just uid = userId <$> decodeBody rs + let Just uid = userId <$> responseJsonMaybe rs get (galley . path "conversations" . zAuthAccess uid "conn") !!! do const 200 === statusCode @@ -199,14 +199,14 @@ testCreateUserPending brig = do -- Cannot login via email (pending activation) login brig (defEmailLogin e) PersistentCookie !!! do const 403 === statusCode - const (Just "pending-activation") === fmap Error.label . decodeBody + const (Just "pending-activation") === fmap Error.label . responseJsonMaybe -- The user has no verified / activated identity yet - let Just uid = userId <$> decodeBody rs + let Just uid = userId <$> responseJsonMaybe rs get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode const (Just True) === \rs' -> do - self <- decodeBody rs' + self <- responseJsonMaybe rs' return $! isNothing (userIdentity (selfUser self)) -- should not appear in search @@ -224,7 +224,7 @@ testCreateUserNoEmailNoPassword brig = do rs <- post (brig . path "/i/users" . contentJson . body newUser) decodeBody rs + let Just uid = userId <$> responseJsonMaybe rs e <- randomEmail let setEmail = RequestBodyLBS . encode $ EmailUpdate e @@ -243,7 +243,7 @@ testCreateUserConflict brig = do ] post (brig . path "/register" . contentJson . body p) !!! do const 409 === statusCode - const (Just "key-exists") === fmap Error.label . decodeBody + const (Just "key-exists") === fmap Error.label . responseJsonMaybe -- untrusted email domains u2 <- createUserUntrustedEmail "conflict" brig @@ -255,7 +255,7 @@ testCreateUserConflict brig = do ] post (brig . path "/register" . contentJson . body p2) !!! do const 409 === statusCode - const (Just "key-exists") === fmap Error.label . decodeBody + const (Just "key-exists") === fmap Error.label . responseJsonMaybe testCreateUserInvalidPhone :: Brig -> Http () testCreateUserInvalidPhone brig = do @@ -284,7 +284,7 @@ testCreateUserBlacklist brig aws = awaitBlacklist 30 e post (brig . path "/register" . contentJson . body (p e)) !!! do const 403 === statusCode - const (Just "blacklisted-email") === fmap Error.label . decodeBody + const (Just "blacklisted-email") === fmap Error.label . responseJsonMaybe p email = RequestBodyLBS . encode $ object [ "name" .= ("Alice" :: Text) , "email" .= email @@ -329,7 +329,7 @@ testCreateUserExternalSSO brig = do testActivateWithExpiry :: Brig -> Opt.Timeout -> Http () testActivateWithExpiry brig timeout = do - u <- decodeBody =<< registerUser "dilbert" brig + u <- responseJsonError =<< registerUser "dilbert" brig let email = fromMaybe (error "missing email") (userEmail u) act <- getActivationCode brig (Left email) case act of @@ -343,7 +343,7 @@ testActivateWithExpiry brig timeout = do activate brig kc !!! const 404 === statusCode where actualBody rs = do - a <- decodeBody rs + a <- responseJsonMaybe rs Just (Just (activatedIdentity a), activatedFirst a) awaitExpiry :: Int -> ActivationPair -> Http () @@ -387,7 +387,7 @@ testMultipleUsers brig = do where result r = Set.fromList . map (field "name" &&& field "email") - <$> decodeBody r + <$> responseJsonMaybe r field :: FromJSON a => Text -> Value -> Maybe a field f u = u ^? key f >>= maybeFromJSON @@ -434,10 +434,10 @@ testCreateUserAnonExpiry b = do liftIO $ assertBool "expiry must be less than 10 days" (diff < fromIntegral maxExp) expire :: ResponseLBS -> Maybe UTCTime - expire r = join $ field "expires_at" <$> decodeBody r + expire r = join $ field "expires_at" <$> responseJsonMaybe r deleted :: ResponseLBS -> Maybe Bool - deleted r = join $ field "deleted" <$> decodeBody r + deleted r = join $ field "deleted" <$> responseJsonMaybe r field :: FromJSON a => Text -> Value -> Maybe a field f u = u ^? key f >>= maybeFromJSON @@ -475,7 +475,7 @@ testUserUpdate brig cannon aws = do ( fmap userName u , fmap userAccentId u , fmap userAssets u - )) . decodeBody + )) . responseJsonMaybe -- get only the new name get (brig . path "/self/name" . zUser alice) !!! do @@ -516,7 +516,7 @@ testEmailUpdate brig aws = do where ensureNoOtherUserWithEmail eml = do tk :: Maybe AccessToken <- - decodeBody <$> login brig (defEmailLogin eml) SessionCookie + responseJsonMaybe <$> login brig (defEmailLogin eml) SessionCookie for_ tk $ \t -> do deleteUser (Auth.user t) (Just defPassword) brig !!! const 200 === statusCode liftIO $ Util.assertUserJournalQueue "user deletion" aws (userDeleteJournaled $ Auth.user t) @@ -538,7 +538,7 @@ testPhoneUpdate brig = do -- check new phone get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode - const (Just phn) === (userPhone <=< decodeBody) + const (Just phn) === (userPhone <=< responseJsonMaybe) testCreateAccountPendingActivationKey :: Brig -> Http () testCreateAccountPendingActivationKey brig = do @@ -581,7 +581,7 @@ testUserLocaleUpdate brig aws = do -- get the updated locale get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode - const (parseLocale "pt-PT") === (Just . userLocale . selfUser <=< decodeBody) + const (parseLocale "pt-PT") === (Just . userLocale . selfUser <=< responseJsonMaybe) where locale l = body . RequestBodyLBS . encode $ object ["locale" .= l] @@ -594,7 +594,7 @@ testSuspendUser brig = do -- login fails login brig (defEmailLogin email) PersistentCookie !!! do const 403 === statusCode - const (Just "suspended") === fmap Error.label . decodeBody + const (Just "suspended") === fmap Error.label . responseJsonMaybe -- check status chkStatus brig uid Suspended -- should not appear in search @@ -624,7 +624,7 @@ testGetByIdentity brig = do rs <- post (brig . path "/i/users" . contentJson . body newUser) decodeBody rs + let Just uid = userId <$> responseJsonMaybe rs get (brig . zUser uid . path "i/users" . queryItem "email" emailBs) !!! do const 200 === statusCode @@ -633,7 +633,7 @@ testGetByIdentity brig = do const 200 === statusCode const (Just [uid]) === getUids where - getUids r = return . fmap (userId . accountUser) =<< decodeBody r + getUids r = return . fmap (userId . accountUser) =<< responseJsonMaybe r testPasswordSet :: Brig -> Http () testPasswordSet brig = do @@ -645,7 +645,7 @@ testPasswordSet brig = do rs <- post (brig . path "/i/users" . contentJson . body newUser) decodeBody rs + let Just uid = userId <$> responseJsonMaybe rs -- No password set yet Bilge.head (brig . path "/self/password" . zUser uid) !!! const 404 === statusCode @@ -703,7 +703,7 @@ testSendActivationCode brig = do requestActivationCode brig 200 . Left =<< randomEmail -- Standard email registration flow r <- registerUser "Alice" brig Http [ExcludedPrefix] - getPrefixes prefix = decodeBody =<< getPrefix (phonePrefix prefix) + getPrefixes prefix = responseJsonError =<< getPrefix (phonePrefix prefix) getPrefix :: PhonePrefix -> Http ResponseLBS getPrefix prefix = get ( brig . paths ["/i/users/phone-prefixes", toByteString' prefix]) @@ -794,7 +794,7 @@ testEmailPhoneDelete brig cannon = do get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode - const Nothing === (userEmail <=< decodeBody) + const Nothing === (userEmail <=< responseJsonMaybe) -- Cannot remove the only remaining identity delete (brig . path "/self/phone" . zUser uid . zConn "c") !!! @@ -824,7 +824,7 @@ testEmailPhoneDelete brig cannon = do ephone @?= Just (fromPhone phone) get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode - const Nothing === (userPhone <=< decodeBody) + const Nothing === (userPhone <=< responseJsonMaybe) testDeleteUserByPassword :: Brig -> Cannon -> AWS.Env -> Http () testDeleteUserByPassword brig cannon aws = do @@ -874,21 +874,21 @@ testDeleteUserByPassword brig cannon aws = do Nothing -> liftIO $ assertFailure "missing activation key/code" Just kc -> activate brig kc !!! do const 404 === statusCode - const (Just "invalid-code") === fmap Error.label . decodeBody + const (Just "invalid-code") === fmap Error.label . responseJsonMaybe -- Connections involving uid1 are gone (uid2 <-> uid3 remains) let u1Conns = UserConnectionList [] False - let u2Conns = UserConnectionList (maybeToList (decodeBody con23)) False - let u3Conns = UserConnectionList (maybeToList (decodeBody con32)) False + let u2Conns = UserConnectionList (maybeToList (responseJsonMaybe con23)) False + let u3Conns = UserConnectionList (maybeToList (responseJsonMaybe con32)) False listConnections brig uid1 !!! do const 200 === statusCode - const (Just u1Conns) === decodeBody + const (Just u1Conns) === responseJsonMaybe listConnections brig uid2 !!! do const 200 === statusCode - const (Just u2Conns) === decodeBody + const (Just u2Conns) === responseJsonMaybe listConnections brig uid3 !!! do const 200 === statusCode - const (Just u3Conns) === decodeBody + const (Just u3Conns) === responseJsonMaybe testDeleteUserWithLegalHold :: Brig -> Cannon -> AWS.Env -> Http () testDeleteUserWithLegalHold brig cannon aws = do @@ -914,14 +914,14 @@ testDeleteUserByCode brig = do let _code = "123" :: Text send _key _code !!! do const 400 === statusCode - const (Just "bad-request") === fmap Error.label . decodeBody + const (Just "bad-request") === fmap Error.label . responseJsonMaybe -- (Semantically) invalid key / code let _key = T.replicate 20 "x" let _code = "idontknow" :: Text send _key _code !!! do const 403 === statusCode - const (Just "invalid-code") === fmap Error.label . decodeBody + const (Just "invalid-code") === fmap Error.label . responseJsonMaybe where send k c = post (brig . path "/delete" . contentJson . body (payload k c)) payload k c = RequestBodyLBS . encode $ object @@ -979,7 +979,7 @@ testUpdateSSOId brig galley = do . Bilge.json ssoid ) !!! const 200 === statusCode - profile :: SelfProfile <- decodeBody =<< get (brig . path "/self" . zUser uid) + profile :: SelfProfile <- responseJsonError =<< get (brig . path "/self" . zUser uid) let Just (SSOIdentity ssoid' mEmail mPhone) = userIdentity . selfUser $ profile liftIO $ do assertEqual "updateSSOId/ssoid" ssoid ssoid' @@ -995,7 +995,7 @@ testUpdateSSOId brig galley = do updatePhone brig (userId member) =<< randomPhone when (not hasEmail) $ do error "not implemented" - selfUser <$> (decodeBody =<< get (brig . path "/self" . zUser (userId member))) + selfUser <$> (responseJsonError =<< get (brig . path "/self" . zUser (userId member))) let ssoids1 = [ UserSSOId "1" "1", UserSSOId "1" "2" ] ssoids2 = [ UserSSOId "2" "1", UserSSOId "2" "2" ] @@ -1043,12 +1043,12 @@ setHandleAndDeleteUser brig cannon u others aws execDelete = do -- Clients are gone get (brig . path "clients" . zUser (userId u)) !!! do const 200 === statusCode - const (Just [] :: Maybe [Client]) === decodeBody + const (Just [] :: Maybe [Client]) === responseJsonMaybe -- Can no longer log in login brig (defEmailLogin email) PersistentCookie !!! do const 403 === statusCode - const (Just "invalid-credentials") === fmap Error.label . decodeBody + const (Just "invalid-credentials") === fmap Error.label . responseJsonMaybe -- Deleted flag appears in self profile; email, handle and picture are gone get (brig . path "/self" . zUser uid) !!! assertDeletedProfileSelf @@ -1082,7 +1082,7 @@ setHandleAndDeleteUser brig cannon u others aws execDelete = do , fmap userDeleted u' , fmap userAssets u' , userHandle =<< u' - )) . decodeBody + )) . responseJsonMaybe assertDeletedProfilePublic = do const 200 === statusCode @@ -1090,4 +1090,4 @@ setHandleAndDeleteUser brig cannon u others aws execDelete = do ( fmap profilePict u' , fmap profileDeleted u' , profileHandle =<< u' - )) . decodeBody + )) . responseJsonMaybe diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index f423968f93a..da7ee846982 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} {-# OPTIONS_GHC -Wno-incomplete-patterns #-} module API.User.Auth (tests) where @@ -10,9 +11,10 @@ import Brig.Types.User import Brig.Types.User.Auth import Brig.ZAuth (ZAuth, runZAuth) import UnliftIO.Async hiding (wait) -import Control.Lens ((^?), set) +import Control.Lens ((^?), (^.), set) import Control.Retry import Data.Aeson +import Data.Proxy import Data.Aeson.Lens import Data.ByteString.Conversion import Data.Id @@ -23,6 +25,8 @@ import Test.Tasty import Test.Tasty.HUnit import Util import Util.Options.Common +import API.Team.Util +import Brig.Types.Team.LegalHold (LegalHoldStatus (..)) import qualified Brig.Options as Opts import qualified Brig.Types.User.Auth as Auth @@ -32,14 +36,14 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as Lazy import qualified Data.Text.Lazy as Lazy import qualified Brig.ZAuth as ZAuth +import qualified Data.ZAuth.Token as ZAuth import qualified Data.Text as Text import qualified Data.UUID.V4 as UUID import qualified Test.Tasty.HUnit as HUnit import qualified Network.Wai.Utilities.Error as Error - -tests :: Maybe Opts.Opts -> Manager -> ZAuth.Env -> Brig -> TestTree -tests conf m z b = testGroup "auth" +tests :: Maybe Opts.Opts -> Manager -> ZAuth.Env -> Brig -> Galley -> Nginz -> TestTree +tests conf m z b g n = testGroup "auth" [ testGroup "login" [ test m "email" (testEmailLogin b) , test m "phone" (testPhoneLogin b) @@ -54,12 +58,29 @@ tests conf m z b = testGroup "auth" , test m "failure-suspended" (testSuspendedSsoLogin b) , test m "failure-no-user" (testNoUserSsoLogin b) ] + , testGroup "legalhold-login" + [ test m "failure-no-team" (testRegularUserLegalHoldLogin b) + , test m "team-user-with-legalhold-enabled" (testTeamUserLegalHoldLogin b g) + , test m "failure-suspended" (testSuspendedLegalHoldLogin b g) + , test m "failure-no-user" (testNoUserLegalHoldLogin b) + , test m "failure-wrong-password" (testWrongPasswordLegalHoldLogin b g) + , test m "always-persistent-cookie" (testLegalHoldSessionCookie b g) + , test m "legalhold-logout" (testLegalHoldLogout b g) + ] + , testGroup "nginz" + [ test m "nginz-login" (testNginz b n) + , test m "nginz-legalhold-login" (testNginzLegalHold b g n) + ] ] - , testGroup "refresh" - [ test m "invalid-cookie" (testInvalidCookie z b) + , testGroup "refresh /access" + [ test m "invalid-cookie" (testInvalidCookie @ZAuth.User z b) + , test m "invalid-cookie legalhold" (testInvalidCookie @ZAuth.LegalHoldUser z b) , test m "invalid-token" (testInvalidToken b) - , test m "missing-cookie" (testMissingCookie z b) - , test m "unknown-cookie" (testUnknownCookie z b) + , test m "missing-cookie" (testMissingCookie @ZAuth.User @ZAuth.Access z b) + , test m "missing-cookie legalhold" (testMissingCookie @ZAuth.LegalHoldUser @ZAuth.LegalHoldAccess z b) + , test m "unknown-cookie" (testUnknownCookie @ZAuth.User z b) + , test m "unknown-cookie legalhold" (testUnknownCookie @ZAuth.LegalHoldUser z b) + , test m "token mismatch" (testTokenMismatch z b g) , test m "new-persistent-cookie" (testNewPersistentCookie conf b) , test m "new-session-cookie" (testNewSessionCookie conf b) , test m "suspend-inactive" (testSuspendInactiveUsers conf b) @@ -79,12 +100,63 @@ tests conf m z b = testGroup "auth" -------------------------------------------------------------------------------- -- ZAuth test environment for generating arbitrary tokens. -randomAccessToken :: ZAuth ZAuth.AccessToken -randomAccessToken = randomUserToken >>= ZAuth.newAccessToken -randomUserToken :: ZAuth ZAuth.UserToken +randomAccessToken :: forall u a . ZAuth.TokenPair u a => ZAuth (ZAuth.Token a) +randomAccessToken = randomUserToken @u >>= ZAuth.newAccessToken + +randomUserToken :: ZAuth.UserTokenLike u => ZAuth (ZAuth.Token u) randomUserToken = (Id <$> liftIO UUID.nextRandom) >>= ZAuth.newUserToken +------------------------------------------------------------------------------- +-- Nginz authentication tests (end-to-end sanity checks) +-- + +testNginz :: Brig -> Nginz -> Http () +testNginz b n = do + u <- randomUser b + let Just email = userEmail u + -- Login with email + rs <- login b (defEmailLogin email) PersistentCookie + (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))) !!! const 200 === statusCode + +testNginzLegalHold :: Brig -> Galley -> Nginz -> Http () +testNginzLegalHold b g n = do + -- create team user Alice + (alice, tid) <- createUserWithTeam b g + putLegalHoldEnabled tid LegalHoldEnabled g -- enable it for this team + rs <- legalHoldLogin b (LegalHoldLogin alice (Just defPassword) Nothing) PersistentCookie + (toByteString' t))) !!! do + const 200 === statusCode + + -- ensure legalhold tokens CANNOT fetch /clients + get (n . path "/clients" . header "Authorization" ("Bearer " <> (toByteString' t))) !!! const 403 === statusCode + get (n . path "/self" . header "Authorization" ("Bearer " <> (toByteString' t))) !!! const 403 === statusCode + + -- ensure legal hold tokens can fetch notifications + get (n . path "/notifications" . header "Authorization" ("Bearer " <> (toByteString' t))) !!! const 200 === statusCode + + ------------------------------------------------------------------------------- -- Login @@ -98,7 +170,7 @@ testEmailLogin brig = do rs <- login brig (defEmailLogin email) PersistentCookie decodeBody rsp1 + let _timeout = fromLoginCodeTimeout <$> responseJsonMaybe rsp1 liftIO $ assertEqual "timeout" (Just (Code.Timeout 600)) _timeout -- Retry with a voice call @@ -170,7 +242,7 @@ testSendLoginCode brig = do decodeBody rsp2 + let _timeout = fromLoginCodeTimeout <$> responseJsonMaybe rsp2 liftIO $ assertEqual "timeout" (Just (Code.Timeout 600)) _timeout testLoginFailure :: Brig -> Http () @@ -195,7 +267,7 @@ testLoginFailure brig = do ] res <- post (brig . path "/i/users" . contentJson . Http.body newUser) decodeBody res + uid <- userId <$> responseJsonError res eml <- randomEmail -- Add email @@ -282,6 +354,92 @@ testLimitRetries (Just conf) brig = do login brig (defEmailLogin email) SessionCookie !!! const 200 === statusCode +------------------------------------------------------------------------------- +-- LegalHold Login + +testRegularUserLegalHoldLogin :: Brig -> Http () +testRegularUserLegalHoldLogin brig = do + -- Create a regular user + uid <- userId <$> randomUser brig + -- fail if user is not a team user + legalHoldLogin brig (LegalHoldLogin uid (Just defPassword) Nothing) PersistentCookie !!! do + const 403 === statusCode + +testTeamUserLegalHoldLogin :: Brig -> Galley -> Http () +testTeamUserLegalHoldLogin brig galley = do + -- create team user Alice + (alice, tid) <- createUserWithTeam brig galley + now <- liftIO getCurrentTime + -- fail if legalhold isn't activated yet for this user + legalHoldLogin brig (LegalHoldLogin alice (Just defPassword) Nothing) PersistentCookie !!! do + const 403 === statusCode + + putLegalHoldEnabled tid LegalHoldEnabled galley -- enable it for this team + _rs <- legalHoldLogin brig (LegalHoldLogin alice (Just defPassword) Nothing) PersistentCookie + Galley -> Http () +testLegalHoldSessionCookie brig galley = do + alice <- prepareLegalHoldUser brig galley + -- attempt a legalhold login with a session cookie (?persist=false) + rs <- legalHoldLogin brig (LegalHoldLogin alice (Just defPassword) Nothing) SessionCookie + Galley -> Http () +testSuspendedLegalHoldLogin brig galley = do + -- Create a user and immediately suspend them + (uid, _tid) <- createUserWithTeam brig galley + setStatus brig uid Suspended + -- Try to login and see if we fail + legalHoldLogin brig (LegalHoldLogin uid (Just defPassword) Nothing) PersistentCookie !!! do + const 403 === statusCode + const (Just "suspended") === errorLabel + +-- | Check that @/i/legalhold-login@ fails if the user doesn't exist. +testNoUserLegalHoldLogin :: Brig -> Http () +testNoUserLegalHoldLogin brig = do + -- Try to login with random UID and see if we fail + uid <- randomId + legalHoldLogin brig (LegalHoldLogin uid (Just defPassword) Nothing) PersistentCookie !!! do + const 403 === statusCode + const (Just "invalid-credentials") === errorLabel + +testWrongPasswordLegalHoldLogin :: Brig -> Galley -> Http () +testWrongPasswordLegalHoldLogin brig galley = do + alice <- prepareLegalHoldUser brig galley + -- attempt a legalhold login with a wrong password + legalHoldLogin brig (LegalHoldLogin alice (Just (PlainTextPassword "wrong-password")) Nothing) PersistentCookie !!! do + const 403 === statusCode + const (Just "invalid-credentials") === errorLabel + -- attempt a legalhold login with a no password + legalHoldLogin brig (LegalHoldLogin alice Nothing Nothing) PersistentCookie !!! do + const 403 === statusCode + const (Just "missing-auth") === errorLabel + +testLegalHoldLogout :: Brig -> Galley -> Http () +testLegalHoldLogout brig galley = do + uid <- prepareLegalHoldUser brig galley + _rs <- legalHoldLogin brig (LegalHoldLogin uid (Just defPassword) Nothing) PersistentCookie Brig -> Http () +testInvalidCookie :: forall u . ZAuth.UserTokenLike u => ZAuth.Env -> Brig -> Http () testInvalidCookie z b = do -- Syntactically invalid post (b . path "/access" . cookieRaw "zuid" "xxx") !!! do @@ -331,35 +489,35 @@ testInvalidCookie z b = do const (Just "Invalid user token") =~= responseBody -- Expired - u <- userId <$> randomUser b - let f = set ZAuth.userTokenTimeout (ZAuth.UserTokenTimeout 0) - t <- toByteString' <$> runZAuth z (ZAuth.localSettings f (ZAuth.newUserToken u)) + user <- userId <$> randomUser b + let f = set (ZAuth.userTTL (Proxy @u)) 0 + t <- toByteString' <$> runZAuth z (ZAuth.localSettings f (ZAuth.newUserToken @u user)) liftIO $ threadDelay 1000000 post (b . path "/access" . cookieRaw "zuid" t) !!! do const 403 === statusCode const (Just "expired") =~= responseBody testInvalidToken :: Brig -> Http () -testInvalidToken r = do +testInvalidToken b = do -- Syntactically invalid - post (r . path "/access" . queryItem "access_token" "xxx") + post (b . path "/access" . queryItem "access_token" "xxx") !!! errResponse - post (r . path "/access" . header "Authorization" "Bearer xxx") + post (b . path "/access" . header "Authorization" "Bearer xxx") !!! errResponse where errResponse = do const 403 === statusCode const (Just "Invalid access token") =~= responseBody -testMissingCookie :: ZAuth.Env -> Brig -> Http () -testMissingCookie z r = do +testMissingCookie :: forall u a . ZAuth.TokenPair u a => ZAuth.Env -> Brig -> Http () +testMissingCookie z b = do -- Missing cookie, i.e. token refresh mandates a cookie. - post (r . path "/access") + post (b . path "/access") !!! errResponse - t <- toByteString' <$> runZAuth z randomAccessToken - post (r . path "/access" . header "Authorization" ("Bearer " <> t)) + t <- toByteString' <$> runZAuth z (randomAccessToken @u @a) + post (b . path "/access" . header "Authorization" ("Bearer " <> t)) !!! errResponse - post (r . path "/access" . queryItem "access_token" t) + post (b . path "/access" . queryItem "access_token" t) !!! errResponse where errResponse = do @@ -367,14 +525,38 @@ testMissingCookie z r = do const (Just "Missing cookie") =~= responseBody const (Just "invalid-credentials") =~= responseBody -testUnknownCookie :: ZAuth.Env -> Brig -> Http () -testUnknownCookie z r = do +testUnknownCookie :: forall u . ZAuth.UserTokenLike u => ZAuth.Env -> Brig -> Http () +testUnknownCookie z b = do -- Valid cookie but unknown to the server. - t <- toByteString' <$> runZAuth z randomUserToken - post (r . path "/access" . cookieRaw "zuid" t) !!! do + t <- toByteString' <$> runZAuth z (randomUserToken @u) + post (b . path "/access" . cookieRaw "zuid" t) !!! do const 403 === statusCode const (Just "invalid-credentials") =~= responseBody +testTokenMismatch :: ZAuth.Env -> Brig -> Galley -> Http () +testTokenMismatch z brig galley = do + u <- randomUser brig + let Just email = userEmail u + _rs <- login brig (emailLogin email defPassword (Just "nexus1")) PersistentCookie + runZAuth z (randomAccessToken @ZAuth.LegalHoldUser @ZAuth.LegalHoldAccess) + post (brig . path "/access" . cookie c . header "Authorization" ("Bearer " <> t)) !!! do + const 403 === statusCode + const (Just "Token mismatch") =~= responseBody + + -- try refresh with a regular AccessToken but a LegalHoldUserCookie + (alice, tid) <- createUserWithTeam brig galley + putLegalHoldEnabled tid LegalHoldEnabled galley -- enable it for this team + _rs <- legalHoldLogin brig (LegalHoldLogin alice (Just defPassword) Nothing) PersistentCookie + let c' = decodeCookie _rs + t' <- toByteString' <$> runZAuth z (randomAccessToken @ZAuth.User @ZAuth.Access) + post (brig . path "/access" . cookie c' . header "Authorization" ("Bearer " <> t')) !!! do + const 403 === statusCode + const (Just "Token mismatch") =~= responseBody + testNewPersistentCookie :: Maybe Opts.Opts -> Brig -> Http () testNewPersistentCookie config b = do u <- randomUser b @@ -573,12 +755,12 @@ testTooManyCookies config b = do (do tcs <- replicateM (l + carry) $ loginWhenAllowed pwlP PersistentCookie cs <- listCookiesWithLabel b (userId u) ["persistent"] - liftIO $ map cookieId cs @=? map getCookieId (drop carry tcs)) + liftIO $ map cookieId cs @=? map (getCookieId @ZAuth.User) (drop carry tcs)) -- Session logins (do tcs' <- replicateM (l + carry) $ loginWhenAllowed pwlS SessionCookie cs' <- listCookiesWithLabel b (userId u) ["session"] - liftIO $ map cookieId cs' @=? map getCookieId (drop carry tcs')) + liftIO $ map cookieId cs' @=? map (getCookieId @ZAuth.User) (drop carry tcs')) where -- We expect that after `setUserCookieLimit` login attempts, we get rate -- limited; in those cases, we need to wait `Retry-After` seconds. @@ -640,18 +822,28 @@ testReauthentication b = do ----------------------------------------------------------------------------- -- Helpers +prepareLegalHoldUser :: Brig -> Galley -> Http (UserId) +prepareLegalHoldUser brig galley = do + (uid, tid) <- createUserWithTeam brig galley + -- enable it for this team - without that, legalhold login will fail. + putLegalHoldEnabled tid LegalHoldEnabled galley + return uid + decodeCookie :: HasCallStack => Response a -> Http.Cookie decodeCookie = fromMaybe (error "missing zuid cookie") . getCookie "zuid" decodeToken :: HasCallStack => Response (Maybe Lazy.ByteString) -> ZAuth.AccessToken -decodeToken r = fromMaybe (error "invalid access_token") $ do +decodeToken = decodeToken' @ZAuth.Access + +decodeToken' :: (HasCallStack, ZAuth.AccessTokenLike a) => Response (Maybe Lazy.ByteString) -> ZAuth.Token a +decodeToken' r = fromMaybe (error "invalid access_token") $ do x <- responseBody r t <- x ^? key "access_token" . _String fromByteString (encodeUtf8 t) -getCookieId :: HasCallStack => Http.Cookie -> CookieId +getCookieId :: forall u . (HasCallStack, ZAuth.UserTokenLike u) => Http.Cookie -> CookieId getCookieId c = maybe (error "no cookie value") - (CookieId . ZAuth.userTokenRand) + (CookieId . ZAuth.userTokenRand @u) (fromByteString (cookie_value c)) listCookies :: HasCallStack => Brig -> UserId -> Http [Auth.Cookie ()] @@ -663,7 +855,7 @@ listCookiesWithLabel b u l = do . queryItem "labels" labels . header "Z-User" (toByteString' u)) decodeBody rs + let Just cs = cookieList <$> responseJsonMaybe rs return cs where labels = BS.intercalate "," $ map toByteString' l @@ -671,19 +863,23 @@ listCookiesWithLabel b u l = do -- | Check that the cookie returned after login is sane. -- -- Doesn't check everything, just some basic properties. -assertSanePersistentCookie :: Http.Cookie -> Assertion +assertSanePersistentCookie :: forall u . ZAuth.UserTokenLike u => Http.Cookie -> Assertion assertSanePersistentCookie ck = do assertBool "type" (cookie_persistent ck) assertBool "http-only" (cookie_http_only ck) assertBool "expiry" (cookie_expiry_time ck > cookie_creation_time ck) assertBool "domain" (cookie_domain ck /= "") assertBool "path" (cookie_path ck /= "") + let Just (token :: ZAuth.Token u) = fromByteString (cookie_value ck) + tokentype = ZAuth.zauthType @u + assertBool "type field (t=)" $ token ^. ZAuth.header . ZAuth.typ == tokentype -- | Check that the access token returned after login is sane. assertSaneAccessToken - :: UTCTime -- ^ Some moment in time before the user was created + :: ZAuth.AccessTokenLike a + => UTCTime -- ^ Some moment in time before the user was created -> UserId - -> ZAuth.AccessToken + -> ZAuth.Token a -> Assertion assertSaneAccessToken now uid tk = do assertEqual "user" uid (ZAuth.accessTokenOf tk) @@ -691,7 +887,7 @@ assertSaneAccessToken now uid tk = do -- | Get error label from the response (for use in assertions). errorLabel :: Response (Maybe Lazy.ByteString) -> Maybe Lazy.Text -errorLabel = fmap Error.label . decodeBody +errorLabel = fmap Error.label . responseJsonMaybe remJson :: PlainTextPassword -> Maybe [CookieLabel] -> Maybe [CookieId] -> Value remJson p l ids = object diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index 4f8ecf667f3..bc3007323da 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -51,7 +51,7 @@ testAddGetClient hasPwd brig cannon = do let rq = addClientReq brig uid (defNewClient TemporaryClientType [somePrekeys !! 0] (someLastPrekeys !! 0)) . header "X-Forwarded-For" "127.0.0.1" -- Fake IP to test IpAddr parsing. c <- WS.bracketR cannon uid $ \ws -> do - c <- decodeBody =<< (post rq do @@ -63,7 +63,7 @@ testAddGetClient hasPwd brig cannon = do return c getClient brig uid (clientId c) !!! do const 200 === statusCode - const (Just c) === decodeBody + const (Just c) === responseJsonMaybe testClientReauthentication :: Brig -> Http () testClientReauthentication brig = do @@ -81,25 +81,25 @@ testClientReauthentication brig = do -- User with password uid <- userId <$> randomUser brig -- The first client never requires authentication - c <- decodeBody =<< (addClient brig uid payload1 createAnonUser "Mr. X" brig - c2 <- decodeBody =<< (addClient brig uid2 payload1 Http () @@ -108,54 +108,54 @@ testListClients brig = do let (pk1, lk1) = (somePrekeys !! 0, (someLastPrekeys !! 0)) let (pk2, lk2) = (somePrekeys !! 1, (someLastPrekeys !! 1)) let (pk3, lk3) = (somePrekeys !! 2, (someLastPrekeys !! 2)) - c1 <- decodeBody <$> addClient brig uid (defNewClient PermanentClientType [pk1] lk1) - c2 <- decodeBody <$> addClient brig uid (defNewClient PermanentClientType [pk2] lk2) - c3 <- decodeBody <$> addClient brig uid (defNewClient TemporaryClientType [pk3] lk3) + c1 <- responseJsonMaybe <$> addClient brig uid (defNewClient PermanentClientType [pk1] lk1) + c2 <- responseJsonMaybe <$> addClient brig uid (defNewClient PermanentClientType [pk2] lk2) + c3 <- responseJsonMaybe <$> addClient brig uid (defNewClient TemporaryClientType [pk3] lk3) let cs = sortBy (compare `on` clientId) $ catMaybes [c1, c2, c3] get ( brig . path "clients" . zUser uid ) !!! do const 200 === statusCode - const (Just cs) === decodeBody + const (Just cs) === responseJsonMaybe testListPrekeyIds :: Brig -> Http () testListPrekeyIds brig = do uid <- userId <$> randomUser brig let new = defNewClient PermanentClientType [somePrekeys !! 0] (someLastPrekeys !! 0) - c <- decodeBody =<< addClient brig uid new + c <- responseJsonError =<< addClient brig uid new let pks = [PrekeyId 1, lastPrekeyId] get ( brig . paths ["clients", toByteString' (clientId c), "prekeys"] . zUser uid ) !!! do const 200 === statusCode - const (Just pks) === fmap sort . decodeBody + const (Just pks) === fmap sort . responseJsonMaybe testGetUserPrekeys :: Brig -> Http () testGetUserPrekeys brig = do uid <- userId <$> randomUser brig let new = defNewClient TemporaryClientType [somePrekeys !! 0] (someLastPrekeys !! 0) - c <- decodeBody =<< addClient brig uid new + c <- responseJsonError =<< addClient brig uid new let cpk = ClientPrekey (clientId c) (somePrekeys !! 0) get (brig . paths ["users", toByteString' uid, "prekeys"]) !!! do const 200 === statusCode - const (Just $ PrekeyBundle uid [cpk]) === decodeBody + const (Just $ PrekeyBundle uid [cpk]) === responseJsonMaybe -- prekeys are deleted when retrieved, except the last one let lpk = ClientPrekey (clientId c) (unpackLastPrekey (someLastPrekeys !! 0)) replicateM_ 2 $ get (brig . paths ["users", toByteString' uid, "prekeys"]) !!! do const 200 === statusCode - const (Just $ PrekeyBundle uid [lpk]) === decodeBody + const (Just $ PrekeyBundle uid [lpk]) === responseJsonMaybe testGetClientPrekey :: Brig -> Http () testGetClientPrekey brig = do uid <- userId <$> randomUser brig let new = defNewClient TemporaryClientType [somePrekeys !! 0] (someLastPrekeys !! 0) - c <- decodeBody =<< addClient brig uid new + c <- responseJsonError =<< addClient brig uid new get (brig . paths ["users", toByteString' uid, "prekeys", toByteString' (clientId c)]) !!! do const 200 === statusCode - const (Just $ ClientPrekey (clientId c) (somePrekeys !! 0)) === decodeBody + const (Just $ ClientPrekey (clientId c) (somePrekeys !! 0)) === responseJsonMaybe testTooManyClients :: Brig -> Http () testTooManyClients brig = do @@ -176,7 +176,7 @@ testTooManyClients brig = do addClient brig uid (defNewClient PermanentClientType [somePrekeys !! 17] (someLastPrekeys !! 17)) !!! do const 403 === statusCode - const (Just "too-many-clients") === fmap Error.label . decodeBody + const (Just "too-many-clients") === fmap Error.label . responseJsonMaybe testRemoveClient :: Bool -> Brig -> Cannon -> Http () testRemoveClient hasPwd brig cannon = do @@ -191,7 +191,7 @@ testRemoveClient hasPwd brig cannon = do numCookies <- countCookies brig uid defCookieLabel liftIO $ Just 1 @=? numCookies - c <- decodeBody =<< addClient brig uid (client PermanentClientType (someLastPrekeys !! 10)) + c <- responseJsonError =<< addClient brig uid (client PermanentClientType (someLastPrekeys !! 10)) when hasPwd $ do -- Missing password @@ -230,16 +230,16 @@ testUpdateClient brig = do { newClientClass = Just PhoneClient , newClientModel = Just "featurephone" } - c <- decodeBody =<< addClient brig uid clt + c <- responseJsonError =<< addClient brig uid clt get (brig . paths ["users", toByteString' uid, "prekeys", toByteString' (clientId c)]) !!! do const 200 === statusCode - const (Just $ ClientPrekey (clientId c) (somePrekeys !! 0)) === decodeBody + const (Just $ ClientPrekey (clientId c) (somePrekeys !! 0)) === responseJsonMaybe getClient brig uid (clientId c) !!! do const 200 === statusCode - const (Just "Test Device") === (clientLabel <=< decodeBody) - const (Just PhoneClient) === (clientClass <=< decodeBody) - const (Just "featurephone") === (clientModel <=< decodeBody) + const (Just "Test Device") === (clientLabel <=< responseJsonMaybe) + const (Just PhoneClient) === (clientClass <=< responseJsonMaybe) + const (Just "featurephone") === (clientModel <=< responseJsonMaybe) let newPrekey = somePrekeys !! 2 let update = UpdateClient [newPrekey] Nothing (Just "label") @@ -253,19 +253,19 @@ testUpdateClient brig = do get (brig . paths ["users", toByteString' uid, "prekeys", toByteString' (clientId c)]) !!! do const 200 === statusCode - const (Just $ ClientPrekey (clientId c) newPrekey) === decodeBody + const (Just $ ClientPrekey (clientId c) newPrekey) === responseJsonMaybe -- check if label has been updated getClient brig uid (clientId c) !!! do const 200 === statusCode - const (Just "label") === (clientLabel <=< decodeBody) + const (Just "label") === (clientLabel <=< responseJsonMaybe) -- via `/users/:uid/clients/:client`, only `id` and `class` are visible: get (brig . paths ["users", toByteString' uid, "clients", toByteString' (clientId c)]) !!! do const 200 === statusCode - const (Just $ clientId c) === (fmap pubClientId . decodeBody) - const (Just PhoneClient) === (pubClientClass <=< decodeBody) - const Nothing === (preview (key "label") <=< asValue) + const (Just $ clientId c) === (fmap pubClientId . responseJsonMaybe) + const (Just PhoneClient) === (pubClientClass <=< responseJsonMaybe) + const Nothing === (preview (key "label") <=< responseJsonMaybe @Value) let update' = UpdateClient [] Nothing Nothing @@ -280,7 +280,7 @@ testUpdateClient brig = do -- check if label is still present getClient brig uid (clientId c) !!! do const 200 === statusCode - const (Just "label") === (clientLabel <=< decodeBody) + const (Just "label") === (clientLabel <=< responseJsonMaybe) -- Legacy (galley) testAddMultipleTemporary :: Brig -> Galley -> Http () @@ -312,22 +312,22 @@ testAddMultipleTemporary brig galley = do r <- get $ brig . path "clients" . zUser u - return $ Vec.length <$> (preview _Array =<< asValue r) + return $ Vec.length <$> (preview _Array =<< responseJsonMaybe @Value r) numOfGalleyClients u = do r <- get $ galley . path "i/test/clients" . zUser u - return $ Vec.length <$> (preview _Array =<< asValue r) + return $ Vec.length <$> (preview _Array =<< responseJsonMaybe @Value r) testPreKeyRace :: Brig -> Http () testPreKeyRace brig = do uid <- userId <$> randomUser brig let pks = map (\i -> somePrekeys !! i) [1..10] - c <- decodeBody =<< addClient brig uid (defNewClient PermanentClientType pks (someLastPrekeys !! 0)) + c <- responseJsonError =<< addClient brig uid (defNewClient PermanentClientType pks (someLastPrekeys !! 0)) pks' <- flip mapConcurrently pks $ \_ -> do rs <- getPreKey brig uid (clientId c) decodeBody rs + return $ prekeyId . prekeyData <$> responseJsonMaybe rs -- We should not hand out regular prekeys more than once (i.e. at most once). let actual = catMaybes pks' liftIO $ assertEqual "insufficient prekeys" (length pks) (length actual) @@ -346,7 +346,7 @@ testCan'tDeleteLegalHoldClient brig = do resp <- addClientInternal brig uid (defNewClient LegalHoldClientType [pk] lk) decodeBody resp + lhClientId <- clientId <$> responseJsonError resp deleteClient brig uid lhClientId Nothing !!! const 400 === statusCode diff --git a/services/brig/test/integration/API/User/Connection.hs b/services/brig/test/integration/API/User/Connection.hs index 7156a0ff981..52874ba009b 100644 --- a/services/brig/test/integration/API/User/Connection.hs +++ b/services/brig/test/integration/API/User/Connection.hs @@ -51,11 +51,11 @@ testCreateConnectionInvalidUser brig = do uid2 <- Id <$> liftIO UUID.nextRandom postConnection brig uid1 uid2 !!! do const 400 === statusCode - const (Just "invalid-user") === fmap Error.label . decodeBody + const (Just "invalid-user") === fmap Error.label . responseJsonMaybe -- cannot create a connection with yourself postConnection brig uid1 uid1 !!! do const 400 === statusCode - const (Just "invalid-user") === fmap Error.label . decodeBody + const (Just "invalid-user") === fmap Error.label . responseJsonMaybe testCreateManualConnections :: Brig -> Http () testCreateManualConnections brig = do @@ -80,15 +80,15 @@ testCreateMutualConnections brig galley = do rsp <- postConnection brig uid2 uid1 >= ucConvId of + case responseJsonMaybe rsp >>= ucConvId of Nothing -> liftIO $ assertFailure "incomplete connection" Just cnv -> do getConversation galley uid1 cnv !!! do const 200 === statusCode - const (Just One2OneConv) === fmap cnvType . decodeBody + const (Just One2OneConv) === fmap cnvType . responseJsonMaybe getConversation galley uid2 cnv !!! do const 200 === statusCode - const (Just One2OneConv) === fmap cnvType . decodeBody + const (Just One2OneConv) === fmap cnvType . responseJsonMaybe testAcceptConnection :: Brig -> Http () testAcceptConnection brig = do @@ -159,7 +159,7 @@ testCancelConnection2 brig galley = do assertConnections brig uid1 [ConnectionStatus uid1 uid2 Cancelled] assertConnections brig uid2 [ConnectionStatus uid2 uid1 Cancelled] - let Just cnv = ucConvId =<< decodeBody rsp + let Just cnv = ucConvId =<< responseJsonMaybe rsp -- A cannot see the conversation (due to cancelling) getConversation galley uid1 cnv !!! do @@ -177,7 +177,7 @@ testCancelConnection2 brig galley = do getConversation galley uid2 cnv !!! do const 200 === statusCode const (Just ConnectConv) === \rs -> do - conv <- decodeBody rs + conv <- responseJsonMaybe rs Just (cnvType conv) -- A is a past member, cannot see the conversation @@ -215,7 +215,7 @@ testBlockConnection brig = do -- A does not notice that he got blocked postConnection brig uid1 uid2 !!! do const 200 === statusCode - const (Just Sent) === fmap ucStatus . decodeBody + const (Just Sent) === fmap ucStatus . responseJsonMaybe assertConnections brig uid2 [ConnectionStatus uid2 uid1 Blocked] -- B accepts after all @@ -269,7 +269,7 @@ testBlockAndResendConnection brig galley = do assertConnections brig uid2 [ConnectionStatus uid2 uid1 Blocked] -- B never accepted and thus does not see the conversation - let Just cnv = ucConvId =<< decodeBody rsp + let Just cnv = ucConvId =<< responseJsonMaybe rsp getConversation galley uid2 cnv !!! const 403 === statusCode -- A can see the conversation and is a current member @@ -325,7 +325,7 @@ testBadUpdateConnection brig = do where assertBadUpdate u1 u2 s = putConnection brig u1 u2 s !!! do const 403 === statusCode - const (Just "bad-conn-update") === fmap Error.label . decodeBody + const (Just "bad-conn-update") === fmap Error.label . responseJsonMaybe testConnectionPaging :: Brig -> Http () testConnectionPaging b = do @@ -343,7 +343,7 @@ testConnectionPaging b = do let range = queryRange (toByteString' <$> start) (Just step) r <- get (b . path "/connections" . zUser u . range) conns) liftIO $ assertEqual "has more" (Just (count' < total)) more return . (count',) $ (conns >>= fmap ucTo . listToMaybe . reverse) @@ -373,7 +373,7 @@ testConnectionLimit brig (ConnectionLimit l) = do assertLimited = do const 403 === statusCode - const (Just "connection-limit") === fmap Error.label . decodeBody + const (Just "connection-limit") === fmap Error.label . responseJsonMaybe testAutoConnectionOK :: Brig -> Galley -> Http () testAutoConnectionOK brig galley = do @@ -386,15 +386,15 @@ testAutoConnectionOK brig galley = do Vec.length <$> (decode b :: Maybe (Vector UserConnection)) assertConnections brig uid1 [ConnectionStatus uid1 uid2 Accepted] assertConnections brig uid2 [ConnectionStatus uid2 uid1 Accepted] - case decodeBody bdy >>= headMay >>= ucConvId of + case responseJsonMaybe bdy >>= headMay >>= ucConvId of Nothing -> liftIO $ assertFailure "incomplete connection" Just cnv -> do getConversation galley uid1 cnv !!! do const 200 === statusCode - const (Just One2OneConv) === fmap cnvType . decodeBody + const (Just One2OneConv) === fmap cnvType . responseJsonMaybe getConversation galley uid2 cnv !!! do const 200 === statusCode - const (Just One2OneConv) === fmap cnvType . decodeBody + const (Just One2OneConv) === fmap cnvType . responseJsonMaybe testAutoConnectionNoChanges :: Brig -> Http () testAutoConnectionNoChanges brig = do @@ -420,7 +420,7 @@ testAutoConnectionBadRequest brig = do uid2 <- userId <$> createAnonUser "foo2" brig postAutoConnection brig uid2 (take 1 uids) !!! do const 403 === statusCode - const (Just "no-identity") === fmap Error.label . decodeBody + const (Just "no-identity") === fmap Error.label . responseJsonMaybe -- unactivated / unverified target users simply get filtered out postAutoConnection brig uid1 [uid2] !!! do const 200 === statusCode diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index c2074c8dcdd..0e660383877 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -41,7 +41,7 @@ testHandleUpdate brig cannon = do let upd = RequestBodyLBS . encode $ HandleUpdate h put (brig . path "/self/handle" . contentJson . zUser uid . zConn "c" . body upd) !!! do const 400 === statusCode - const (Just "invalid-handle") === fmap Error.label . decodeBody + const (Just "invalid-handle") === fmap Error.label . responseJsonMaybe -- Claim a valid handle & receive notification hdl <- randomHandle @@ -66,7 +66,7 @@ testHandleUpdate brig cannon = do uid2 <- userId <$> randomUser brig put (brig . path "/self/handle" . contentJson . zUser uid2 . zConn "c" . body update) !!! do const 409 === statusCode - const (Just "handle-exists") === fmap Error.label . decodeBody + const (Just "handle-exists") === fmap Error.label . responseJsonMaybe -- The owner appears by that handle in search Search.refreshIndex brig @@ -112,7 +112,7 @@ testHandleRace brig = do let update = RequestBodyLBS . encode $ HandleUpdate hdl void $ flip mapConcurrently us $ \u -> put (brig . path "/self/handle" . contentJson . zUser u . zConn "c" . body update) - ps <- forM us $ \u -> decodeBody <$> get (brig . path "/self" . zUser u) + ps <- forM us $ \u -> responseJsonMaybe <$> get (brig . path "/self" . zUser u) let owners = catMaybes $ filter (maybe False ((== Just (Handle hdl)) . userHandle)) ps liftIO $ assertBool "More than one owner of a handle" (length owners <= 1) @@ -133,7 +133,7 @@ testHandleQuery brig = do -- Query the updated profile get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode - const (Just (Handle hdl)) === (>>= userHandle) . decodeBody + const (Just (Handle hdl)) === (>>= userHandle) . responseJsonMaybe -- Query for the handle availability (must be taken) Bilge.head (brig . paths ["users", "handles", toByteString' hdl] . zUser uid) !!! @@ -142,14 +142,14 @@ testHandleQuery brig = do -- Query user profiles by handles get (brig . path "/users" . queryItem "handles" (toByteString' hdl) . zUser uid) !!! do const 200 === statusCode - const (Just (Handle hdl)) === (>>= (listToMaybe >=> userHandle)) . decodeBody + const (Just (Handle hdl)) === (>>= (listToMaybe >=> userHandle)) . responseJsonMaybe -- Bulk availability check hdl2 <- randomHandle hdl3 <- randomHandle checkHandles brig uid [hdl, hdl2, "InVa£iD", hdl3] 1 !!! do const 200 === statusCode - const (Just [hdl2]) === decodeBody + const (Just [hdl2]) === responseJsonMaybe checkHandles brig uid [hdl2, hdl, hdl3] 3 !!! do const 200 === statusCode - const (Just [hdl2, hdl3]) === decodeBody + const (Just [hdl2, hdl3]) === responseJsonMaybe diff --git a/services/brig/test/integration/API/User/Property.hs b/services/brig/test/integration/API/User/Property.hs index 7124e953b63..e7d0db4a1c2 100644 --- a/services/brig/test/integration/API/User/Property.hs +++ b/services/brig/test/integration/API/User/Property.hs @@ -31,7 +31,7 @@ testSetGetProperty brig = do const 200 === statusCode getProperty brig (userId u) "foo" !!! do const 200 === statusCode - const (Just objectProp) === decodeBody + const (Just objectProp) === responseJsonMaybe -- String Literals setProperty brig (userId u) "foo" (String "foo") !!! const 200 === statusCode @@ -83,7 +83,7 @@ testListProperties' endpoint rval brig = do const 200 === statusCode get (brig . path endpoint . zUser (userId u)) !!! do const 200 === statusCode - const (Just rval) === decodeBody + const (Just rval) === responseJsonMaybe testClearProperties :: Brig -> Http () testClearProperties brig = do @@ -105,12 +105,12 @@ testPropertyLimits brig = do -- Maximum key length setProperty brig (userId u) (C.replicate 257 'x') (String "y") !!! do const 403 === statusCode - const (Just "property-key-too-large") === fmap Error.label . decodeBody + const (Just "property-key-too-large") === fmap Error.label . responseJsonMaybe -- Maximum value length setProperty brig (userId u) "foo" (String (T.replicate 513 "x")) !!! do const 403 === statusCode - const (Just "property-value-too-large") === fmap Error.label . decodeBody + const (Just "property-value-too-large") === fmap Error.label . responseJsonMaybe -- Maximum count forM_ [1..16 :: Int] $ \i -> @@ -118,4 +118,4 @@ testPropertyLimits brig = do const 200 === statusCode setProperty brig (userId u) "bar" (String "hello") !!! do const 403 === statusCode - const (Just "too-many-properties") === fmap Error.label . decodeBody + const (Just "too-many-properties") === fmap Error.label . responseJsonMaybe diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index af8c88e167b..12f59062c10 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -67,7 +67,7 @@ createRandomPhoneUser brig = do -- check new phone get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode - const (Just phn) === (userPhone <=< decodeBody) + const (Just phn) === (userPhone <=< responseJsonMaybe) return (uid, phn) @@ -86,13 +86,13 @@ activateEmail brig email = do Nothing -> liftIO $ assertFailure "missing activation key/code" Just kc -> activate brig kc !!! do const 200 === statusCode - const(Just False) === fmap activatedFirst . decodeBody + const(Just False) === fmap activatedFirst . responseJsonMaybe checkEmail :: HasCallStack => Brig -> UserId -> Email -> HttpT IO () checkEmail brig uid expectedEmail = get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode - const (Just expectedEmail) === (userEmail <=< decodeBody) + const (Just expectedEmail) === (userEmail <=< responseJsonMaybe) initiateEmailUpdate :: Brig -> Email -> UserId -> Http ResponseLBS initiateEmailUpdate brig email uid = @@ -183,12 +183,12 @@ countCookies brig u label = do . queryItem "labels" (toByteString' label) . header "Z-User" (toByteString' u) ) (preview (key "cookies" . _Array) =<< asValue r) + 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) . decodeBody + 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) @@ -198,8 +198,8 @@ assertEmailVisibility brig a b visible = get (brig . paths ["users", pack . show $ userId b] . zUser (userId a)) !!! do const 200 === statusCode if visible - then const (Just (userEmail b)) === fmap userEmail . decodeBody - else const Nothing === (userEmail <=< decodeBody) + then const (Just (userEmail b)) === fmap userEmail . responseJsonMaybe + else const Nothing === (userEmail <=< responseJsonMaybe) uploadAsset :: HasCallStack => CargoHold -> UserId -> ByteString -> Http CHV3.Asset uploadAsset c usr dat = do @@ -213,7 +213,7 @@ uploadAsset c usr dat = do . content "multipart/mixed" . lbytes (toLazyByteString mpb) ) UserId -> ByteString -> Http (Response (Maybe LB.ByteString)) downloadAsset c usr ast = @@ -232,7 +232,7 @@ uploadAddressBook b u a m = . body (RequestBodyLBS $ encode a) ) !!! do const 200 === statusCode - const (Just (f m)) === (fmap f . decodeBody) + const (Just (f m)) === (fmap f . responseJsonMaybe) where f :: MatchingResult -> MatchingResult f (MatchingResult x y) = MatchingResult (sort x) (sort y) diff --git a/services/brig/test/integration/Main.hs b/services/brig/test/integration/Main.hs index afd0bf1bfa1..d46f4cee3c8 100644 --- a/services/brig/test/integration/Main.hs +++ b/services/brig/test/integration/Main.hs @@ -42,6 +42,7 @@ data Config = Config , cannon :: Endpoint , cargohold :: Endpoint , galley :: Endpoint + , nginz :: Endpoint -- external provider , provider :: Provider.Config } deriving (Show, Generic) @@ -59,6 +60,7 @@ runTests iConf bConf otherArgs = do c <- mkRequest <$> optOrEnv cannon iConf (local . read) "CANNON_WEB_PORT" ch <- mkRequest <$> optOrEnv cargohold iConf (local . read) "CARGOHOLD_WEB_PORT" g <- mkRequest <$> optOrEnv galley iConf (local . read) "GALLEY_WEB_PORT" + n <- mkRequest <$> optOrEnv nginz iConf (local . read) "NGINZ_WEB_PORT" turnFile <- optOrEnv (Opts.servers . Opts.turn) bConf id "TURN_SERVERS" turnFileV2 <- optOrEnv (Opts.serversV2 . Opts.turn) bConf id "TURN_SERVERS_V2" casHost <- optOrEnv (\v -> (Opts.cassandra v)^.casEndpoint.epHost) bConf pack "BRIG_CASSANDRA_HOST" @@ -72,7 +74,7 @@ runTests iConf bConf otherArgs = do emailAWSOpts <- parseEmailAWSOpts awsEnv <- AWS.mkEnv lg awsOpts emailAWSOpts mg - userApi <- User.tests bConf mg b c ch g awsEnv + userApi <- User.tests bConf mg b c ch g n awsEnv providerApi <- Provider.tests (provider <$> iConf) mg db b c g searchApis <- Search.tests mg b teamApis <- Team.tests bConf mg b c g awsEnv diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 13f6cfe6e0f..d1c7502f3f5 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -13,7 +13,6 @@ import Brig.Types.User import Brig.Types.User.Auth import Brig.Types.Intra import Control.Lens ((^?), (^?!)) -import Control.Monad.Catch (MonadThrow) import Control.Retry import Data.Aeson import Data.Aeson.Lens (key, _String, _Integral, _JSON) @@ -22,8 +21,6 @@ import Data.ByteString.Conversion import Data.Id import Data.List1 (List1) import Data.Misc (PlainTextPassword(..)) -import Data.Proxy (Proxy(..)) -import Data.Typeable (typeRep) import Galley.Types (Member (..)) import Gundeck.Types.Notification import System.Random (randomRIO, randomIO) @@ -36,12 +33,10 @@ import Util.AWS import qualified Data.Aeson.Types as Aeson import qualified Galley.Types.Teams as Team import qualified Brig.AWS as AWS -import qualified Brig.RPC as RPC import qualified Brig.Options as Opts import qualified Brig.Run as Run import qualified Data.Text.Ascii as Ascii import qualified Data.ByteString as BS -import qualified Data.ByteString.Lazy as Lazy import qualified Data.ByteString.Char8 as C8 import qualified Data.List1 as List1 import qualified Data.Text as Text @@ -53,8 +48,7 @@ type Brig = Request -> Request type Cannon = Request -> Request type CargoHold = Request -> Request type Galley = Request -> Request - -type ResponseLBS = Response (Maybe Lazy.ByteString) +type Nginz = Request -> Request instance ToJSON SESBounceType where toJSON BounceUndetermined = String "Undetermined" @@ -96,13 +90,13 @@ createUser' :: HasCallStack => Bool -> Text -> Brig -> Http User createUser' hasPwd name brig = do r <- postUser' hasPwd True name True False Nothing Nothing brig Text -> Email -> Brig -> Http User createUserWithEmail name email brig = do r <- postUserWithEmail True True name (Just email) False Nothing Nothing brig Text -> Brig -> Http User createUserUntrustedEmail name brig = do @@ -116,7 +110,7 @@ createAnonUserExpiry :: HasCallStack => Maybe Integer -> Text -> Brig -> Http Us createAnonUserExpiry expires name brig = do let p = RequestBodyLBS . encode $ object [ "name" .= name, "expires_in" .= expires ] r <- post (brig . path "/register" . contentJson . body p) Brig -> Int -> Either Email Phone -> Http () requestActivationCode brig expectedStatus ep = @@ -197,12 +191,12 @@ postUserWithEmail hasPassword validateBody name email havePhone ssoid teamid bri postUserInternal :: Object -> Brig -> Http User postUserInternal payload brig = do rs <- post (brig . path "/i/users" . contentJson . body (RequestBodyLBS $ encode payload)) Brig -> Http User postUserRegister payload brig = do rs <- post (brig . path "/register" . contentJson . body (RequestBodyLBS $ encode payload)) Maybe PlainTextPassword -> Brig -> Http ResponseLBS deleteUser u p brig = delete $ brig @@ -223,7 +217,7 @@ activate brig (k, c) = get $ brig getSelfProfile :: Brig -> UserId -> Http SelfProfile getSelfProfile brig usr = do - decodeBody =<< get (brig . path "/self" . zUser usr) + responseJsonError =<< get (brig . path "/self" . zUser usr) getUser :: Brig -> UserId -> UserId -> Http ResponseLBS getUser brig zusr usr = get $ brig @@ -244,6 +238,13 @@ ssoLogin b l t = let js = RequestBodyLBS (encode l) in post $ b . (if t == PersistentCookie then queryItem "persist" "true" else id) . body js +legalHoldLogin :: Brig -> LegalHoldLogin -> CookieType -> Http ResponseLBS +legalHoldLogin b l t = let js = RequestBodyLBS (encode l) in post $ b + . path "/i/legalhold-login" + . contentJson + . (if t == PersistentCookie then queryItem "persist" "true" else id) + . body js + data LoginCodeType = LoginCodeSMS | LoginCodeVoice deriving Eq @@ -329,7 +330,7 @@ getPreKey brig u c = get $ brig getTeamMember :: HasCallStack => UserId -> TeamId -> Galley -> Http Team.TeamMember getTeamMember u tid galley = - decodeBody =<< + responseJsonError =<< get ( galley . paths ["i", "teams", toByteString' tid, "members", toByteString' u] . zUser u @@ -346,13 +347,13 @@ isMember g usr cnv = do res <- get $ g . paths ["i", "conversations", toByteString' cnv, "members", toByteString' usr] . expect2xx - case decodeBody res of + case responseJsonMaybe res of Nothing -> return False Just m -> return (usr == memId m) getStatus :: HasCallStack => Brig -> UserId -> HttpT IO AccountStatus getStatus brig u = - either (error . show) (^?! key "status" . (_JSON @Value @AccountStatus)) . (responseJson @Value) <$> + (^?! key "status" . (_JSON @Value @AccountStatus)) . (responseJsonUnsafe @Value) <$> get ( brig . paths ["i", "users", toByteString' u, "status"] . expect2xx ) @@ -392,16 +393,6 @@ zUser = header "Z-User" . C8.pack . show zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" --- TODO: we have a bunch of 'decodeBody's lying around, they should be --- unified and moved into some utils module -decodeBody :: forall a m. - (HasCallStack, Typeable a, FromJSON a, MonadThrow m) - => Response (Maybe Lazy.ByteString) -> m a -decodeBody = RPC.decodeBody (Text.pack (show (typeRep (Proxy @a)))) - -asValue :: (HasCallStack, MonadThrow m) => Response (Maybe Lazy.ByteString) -> m Value -asValue = decodeBody - mkEmailRandomLocalSuffix :: MonadIO m => Text -> m Email mkEmailRandomLocalSuffix e = do uid <- liftIO UUID.nextRandom @@ -443,7 +434,7 @@ updatePhone brig uid phn = do Nothing -> liftIO $ assertFailure "missing activation key/code" Just kc -> activate brig kc !!! do const 200 === statusCode - const (Just False) === fmap activatedFirst . decodeBody + const (Just False) === fmap activatedFirst . responseJsonMaybe defEmailLogin :: Email -> Login defEmailLogin e = emailLogin e defPassword (Just defCookieLabel) diff --git a/services/cargohold/test/integration/API/V3.hs b/services/cargohold/test/integration/API/V3.hs index 86adf630d17..527b27ef000 100644 --- a/services/cargohold/test/integration/API/V3.hs +++ b/services/cargohold/test/integration/API/V3.hs @@ -67,7 +67,7 @@ testSimpleRoundtrip c = do const 201 === statusCode let loc = decodeHeader "Location" r1 :: ByteString - let Just ast = decodeBody r1 :: Maybe V3.Asset + let Just ast = responseJsonMaybe @V3.Asset r1 let Just tok = view V3.assetToken ast -- Check mandatory Date header @@ -111,7 +111,7 @@ testSimpleTokens c = do const 201 === statusCode let loc = decodeHeader "Location" r1 :: ByteString - let Just ast = decodeBody r1 :: Maybe V3.Asset + let Just ast = responseJsonMaybe @V3.Asset r1 let key = view V3.assetKey ast let Just tok = view V3.assetToken ast @@ -126,12 +126,12 @@ testSimpleTokens c = do -- Token renewal fails if not done by owner post (c . paths ["assets", "v3", toByteString' key, "token"] . zUser uid2) !!! do const 403 === statusCode - const (Just "unauthorised") === fmap label . decodeBody + const (Just "unauthorised") === fmap label . responseJsonMaybe -- Token renewal succeeds if done by owner r2 <- post (c . paths ["assets", "v3", toByteString' key, "token"] . zUser uid) decodeBody r2 + let Just tok' = V3.newAssetToken <$> responseJsonMaybe r2 liftIO $ assertBool "token unchanged" (tok /= tok') -- Download by owner with new token. @@ -157,7 +157,7 @@ testSimpleTokens c = do -- Delete Token fails if not done by owner delete (c . paths ["assets", "v3", toByteString' key, "token"] . zUser uid2) !!! do const 403 === statusCode - const (Just "unauthorised") === fmap label . decodeBody + const (Just "unauthorised") === fmap label . responseJsonMaybe -- Delete Token succeeds by owner delete (c . paths ["assets", "v3", toByteString' key, "token"] . zUser uid) !!! do @@ -277,7 +277,7 @@ createResumable c u sets size = do . header "Upload-Length" (toByteString' size) . lbytes (encode sets) ) toByteString' (ast^.V3.resumableAsset.V3.assetKey) liftIO $ assertEqual "Location" loc' loc @@ -362,6 +362,3 @@ zUser = header "Z-User" . UUID.toASCIIBytes . toUUID zConn :: ByteString -> Request -> Request zConn = header "Z-Connection" - -decodeBody :: FromJSON a => Response (Maybe Lazy.ByteString) -> Maybe a -decodeBody = responseBody >=> decode diff --git a/services/galley/galley.integration.yaml b/services/galley/galley.integration.yaml index c6dd2d9d00a..5e4b4924c14 100644 --- a/services/galley/galley.integration.yaml +++ b/services/galley/galley.integration.yaml @@ -26,24 +26,9 @@ settings: maxConvSize: 16 intraListing: false conversationCodeURI: https://app.wire.com/join/ - featureFlags: - # SSO: this sets the default setting for each time, which can - # always be overridden by customer support / backoffice. - # IMPORTANT: if you change sso from 'enabled' to 'disabled' after - # running 'enabled' in production, you need to run this migration - # script to fix all teams that have registered an idp: - # https://github.com/wireapp/wire-server/tree/master/tools/db/migrate-sso-feature-flag - # if you don't, the idp will keep working, but the admin won't be - # able to register new idps. - # disabled for integration tests (the ones who need it on will - # turn it on themselves). - sso: false - - # Legal Hold: this decides whether customer support / backoffice - # is allowed to turn the feature on for individual teams. the - # default for new teams is always "false", no matter what the - # feature flag is set to. - legalhold: true + featureFlags: # see #RefConfigOptions in `/docs/reference` + sso: disabled-by-default + legalhold: disabled-by-default logLevel: Info logNetStrings: false diff --git a/services/galley/src/Galley/API.hs b/services/galley/src/Galley/API.hs index 6c2d729a935..6717eb2d434 100644 --- a/services/galley/src/Galley/API.hs +++ b/services/galley/src/Galley/API.hs @@ -67,7 +67,7 @@ sitemap = do document "PUT" "updateTeam" $ do summary "Update team properties" - parameter Path "id" bytes' $ + parameter Path "tid" bytes' $ description "Team ID" body (ref TeamsModel.update) $ description "JSON body" @@ -96,7 +96,7 @@ sitemap = do document "GET" "getTeam" $ do summary "Get a team by ID" - parameter Path "id" bytes' $ + parameter Path "tid" bytes' $ description "Team ID" returns (ref TeamsModel.team) response 200 "Team data" end @@ -114,7 +114,7 @@ sitemap = do document "DELETE" "deleteTeam" $ do summary "Delete a team" - parameter Path "id" bytes' $ + parameter Path "tid" bytes' $ description "Team ID" body (ref TeamsModel.teamDelete) $ do optional @@ -135,7 +135,7 @@ sitemap = do document "GET" "getTeamMembers" $ do summary "Get team members" - parameter Path "id" bytes' $ + parameter Path "tid" bytes' $ description "Team ID" returns (ref TeamsModel.teamMemberList) response 200 "Team members" end @@ -171,7 +171,7 @@ sitemap = do document "POST" "addTeamMember" $ do summary "Add a new team member" - parameter Path "id" bytes' $ + parameter Path "tid" bytes' $ description "Team ID" body (ref TeamsModel.newTeamMember) $ description "JSON body" @@ -216,7 +216,7 @@ sitemap = do document "PUT" "updateTeamMember" $ do summary "Update an existing team member" - parameter Path "id" bytes' $ + parameter Path "tid" bytes' $ description "Team ID" body (ref TeamsModel.newTeamMember) $ description "JSON body" @@ -233,7 +233,7 @@ sitemap = do document "GET" "getTeamConversations" $ do summary "Get team conversations" - parameter Path "id" bytes' $ + parameter Path "tid" bytes' $ description "Team ID" returns (ref TeamsModel.teamConversationList) response 200 "Team conversations" end diff --git a/services/galley/src/Galley/API/LegalHold.hs b/services/galley/src/Galley/API/LegalHold.hs index 9abb37d61b4..c097e8ca622 100644 --- a/services/galley/src/Galley/API/LegalHold.hs +++ b/services/galley/src/Galley/API/LegalHold.hs @@ -195,7 +195,12 @@ approveDevice (zusr ::: tid ::: uid ::: connId ::: req ::: _) = do clientId <- Client.addLegalHoldClientToUser uid connId prekeys lastPrekey' - legalHoldAuthToken <- Client.getLegalHoldAuthToken uid + -- Note: teamId could be passed in the getLegalHoldAuthToken request instead of lookup up again + -- Note: both 'Client.getLegalHoldToken' and 'ensureReAuthorized' check the password + -- Note: both 'Client.getLegalHoldToken' and this function in 'assertOnTeam' above + -- checks that the user is part of a binding team + -- FUTUREWORK: reduce double checks + legalHoldAuthToken <- Client.getLegalHoldAuthToken uid mPassword LHService.confirmLegalHold clientId tid uid legalHoldAuthToken LegalHoldData.setUserLegalHoldStatus tid uid UserLegalHoldEnabled -- TODO: send event at this point (see also: diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index ac31ac1cdf4..189893bd4d3 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -193,7 +193,7 @@ uncheckedDeleteTeam zusr zcon tid = do membs <- Data.teamMembers tid now <- liftIO getCurrentTime convs <- filter (not . view managedConversation) <$> Data.teamConversations tid - (ue, be) <- foldrM (pushEvents now membs) ([],[]) convs + (ue, be) <- foldrM (pushConvDeleteEvents now membs) ([],[]) convs let e = newEvent TeamDelete tid now let r = list1 (userRecipient zusr) (membersToRecipients (Just zusr) membs) pushSome ((newPush1 zusr (TeamEvent e) r & pushConn .~ zcon) : ue) @@ -206,12 +206,17 @@ uncheckedDeleteTeam zusr zcon tid = do Journal.teamDelete tid Data.deleteTeam tid where - pushEvents :: UTCTime -> [TeamMember] -> TeamConversation -> ([Push],[(BotMember, Conv.Event)]) -> Galley ([Push],[(BotMember, Conv.Event)]) - pushEvents now membs c (pp, ee) = do - (bots, users) <- botsAndUsers <$> Data.members (c^.conversationId) - let mm = nonTeamMembers users membs - let e = Conv.Event Conv.ConvDelete (c^.conversationId) zusr now Nothing - let p = newPush zusr (ConvEvent e) (map recipient mm) + pushConvDeleteEvents + :: UTCTime + -> [TeamMember] + -> TeamConversation + -> ([Push],[(BotMember, Conv.Event)]) + -> Galley ([Push],[(BotMember, Conv.Event)]) + pushConvDeleteEvents now teamMembs c (pp, ee) = do + (bots, convMembs) <- botsAndUsers <$> Data.members (c ^. conversationId) + let mm = convMembsAndTeamMembs convMembs teamMembs + let e = Conv.Event Conv.ConvDelete (c ^. conversationId) zusr now Nothing + let p = newPush zusr (ConvEvent e) mm let ee' = bots `zip` repeat e let pp' = maybe pp (\x -> (x & pushConn .~ zcon) : pp) p pure (pp', ee' ++ ee) @@ -392,10 +397,11 @@ deleteTeamConversation (zusr::: zcon ::: tid ::: cid ::: _) = do let te = newEvent Teams.ConvDelete tid now & eventData .~ Just (Teams.EdConvDelete cid) let ce = Conv.Event Conv.ConvDelete cid zusr now Nothing let tr = list1 (userRecipient zusr) (membersToRecipients (Just zusr) tmems) - let p = newPush1 zusr (TeamEvent te) tr & pushConn .~ Just zcon - case map recipient (nonTeamMembers cmems tmems) of - [] -> push1 p - (m:mm) -> pushSome [p, newPush1 zusr (ConvEvent ce) (list1 m mm) & pushConn .~ Just zcon] + let teamPush = [newPush1 zusr (TeamEvent te) tr & pushConn .~ Just zcon] + convPush = case convMembsAndTeamMembs cmems tmems of + [] -> [] + (m:mm) -> [newPush1 zusr (ConvEvent ce) (list1 m mm) & pushConn .~ Just zcon] + pushSome $ teamPush <> convPush void . forkIO $ void $ External.deliver (bots `zip` repeat ce) -- TODO: we don't delete bots here, but we should do that, since every -- bot user can only be in a single conversation @@ -527,10 +533,10 @@ getLegalholdStatus (uid ::: tid ::: ct) = do getSSOStatusInternal :: TeamId ::: JSON -> Galley Response getSSOStatusInternal (tid ::: _) = do defConfig <- do - featureSSO <- view (options . optSettings . featureEnabled FeatureSSO) - pure $ if featureSSO - then SSOTeamConfig SSOEnabled - else SSOTeamConfig SSODisabled + featureSSO <- view (options . optSettings . setFeatureFlags . flagSSO) + pure . SSOTeamConfig $ case featureSSO of + FeatureSSOEnabledByDefault -> SSOEnabled + FeatureSSODisabledByDefault -> SSODisabled ssoTeamConfig <- SSOData.getSSOTeamConfig tid pure . json . fromMaybe defConfig $ ssoTeamConfig @@ -547,16 +553,25 @@ setSSOStatusInternal (tid ::: req ::: _) = do -- | Get legal hold status for a team. getLegalholdStatusInternal :: TeamId ::: JSON -> Galley Response getLegalholdStatusInternal (tid ::: _) = do - legalHoldTeamConfig <- LegalHoldData.getLegalHoldTeamConfig tid - pure . json . fromMaybe defConfig $ legalHoldTeamConfig + featureLegalHold <- view (options . optSettings . setFeatureFlags . flagLegalHold) + case featureLegalHold of + FeatureLegalHoldDisabledByDefault -> do + legalHoldTeamConfig <- LegalHoldData.getLegalHoldTeamConfig tid + pure . json . fromMaybe disabledConfig $ legalHoldTeamConfig + FeatureLegalHoldDisabledPermanently -> do + pure . json $ disabledConfig where - defConfig = LegalHoldTeamConfig LegalHoldDisabled + disabledConfig = LegalHoldTeamConfig LegalHoldDisabled -- | Enable or disable legal hold for a team. setLegalholdStatusInternal :: TeamId ::: JsonRequest LegalHoldTeamConfig ::: JSON -> Galley Response setLegalholdStatusInternal (tid ::: req ::: _) = do - do featureLegalHold <- view (options . optSettings . featureEnabled FeatureLegalHold) - unless featureLegalHold $ throwM legalHoldFeatureFlagNotEnabled + do featureLegalHold <- view (options . optSettings . setFeatureFlags . flagLegalHold) + case featureLegalHold of + FeatureLegalHoldDisabledByDefault -> do + pure () + FeatureLegalHoldDisabledPermanently -> do + throwM legalHoldFeatureFlagNotEnabled legalHoldTeamConfig <- fromJsonBody req case legalHoldTeamConfigStatus legalHoldTeamConfig of diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index 050ea36bf61..28c485e423b 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -22,6 +22,7 @@ import Network.Wai.Predicate import Network.Wai.Utilities import UnliftIO (concurrently) +import qualified Data.Set as Set import qualified Data.Text.Lazy as LT import qualified Galley.Data as Data @@ -152,6 +153,12 @@ location = addHeader hLocation . toByteString' nonTeamMembers :: [Member] -> [TeamMember] -> [Member] nonTeamMembers cm tm = filter (not . flip isTeamMember tm . memId) cm +convMembsAndTeamMembs :: [Member] -> [TeamMember] -> [Recipient] +convMembsAndTeamMembs convMembs teamMembs + = fmap userRecipient . setnub $ map memId convMembs <> map (view userId) teamMembs + where + setnub = Set.toList . Set.fromList + membersToRecipients :: Maybe UserId -> [TeamMember] -> [Recipient] membersToRecipients Nothing = map (userRecipient . view userId) membersToRecipients (Just u) = map userRecipient . filter (/= u) . map (view userId) diff --git a/services/galley/src/Galley/Intra/Client.hs b/services/galley/src/Galley/Intra/Client.hs index c0d2c99f949..cc22da76e67 100644 --- a/services/galley/src/Galley/Intra/Client.hs +++ b/services/galley/src/Galley/Intra/Client.hs @@ -10,7 +10,7 @@ import Imports import Bilge hiding (options, getHeader, statusCode) import Bilge.RPC import Brig.Types.Intra -import Brig.Types.User.Auth (SsoLogin(..)) +import Brig.Types.User.Auth (LegalHoldLogin(..)) import Brig.Types.Client.Prekey (LastPrekey, Prekey) import Brig.Types.Client import Brig.Types.Team.LegalHold (LegalHoldClientRequest(..)) @@ -23,6 +23,7 @@ import Galley.Intra.Util import Galley.Types (UserClients, filterClients) import Data.Id import Data.Text.Encoding +import Data.Misc import Network.HTTP.Types.Method import Network.HTTP.Types.Status import Network.Wai.Utilities.Error @@ -52,16 +53,16 @@ notifyClientsAboutLegalHoldRequest requesterUid targetUid lastPrekey' = do . json (LegalHoldClientRequest requesterUid lastPrekey') . expect2xx -getLegalHoldAuthToken :: UserId -> Galley OpaqueAuthToken -getLegalHoldAuthToken uid = do +getLegalHoldAuthToken :: UserId -> Maybe PlainTextPassword -> Galley OpaqueAuthToken +getLegalHoldAuthToken uid pw = do (brigHost, brigPort) <- brigReq r <- call "brig" $ method POST . host brigHost . port brigPort - . path "/i/sso-login" -- ^ TODO: switch to '/i/legalhold-login' + . path "/i/legalhold-login" . queryItem "persist" "true" - . json (SsoLogin uid Nothing) + . json (LegalHoldLogin uid pw Nothing) . expect2xx case getCookieValue "zuid" r of Nothing -> do diff --git a/services/galley/src/Galley/Options.hs b/services/galley/src/Galley/Options.hs index 41977fdf406..33989be8b0d 100644 --- a/services/galley/src/Galley/Options.hs +++ b/services/galley/src/Galley/Options.hs @@ -7,7 +7,7 @@ import Util.Options import Util.Options.Common import System.Logger.Extended (Level, LogFormat) import Data.Misc -import Galley.Types.Teams (FeatureFlags(..), FeatureFlag) +import Galley.Types.Teams (FeatureFlags(..)) data Settings = Settings { @@ -21,19 +21,12 @@ data Settings = Settings , _setIntraListing :: !Bool -- | URI prefix for conversations with access mode @code@ , _setConversationCodeURI :: !HttpsUrl - , _setFeatureFlags :: !(Maybe FeatureFlags) + , _setFeatureFlags :: !FeatureFlags } deriving (Show, Generic) deriveFromJSON toOptionFieldName ''Settings makeLenses ''Settings -featureEnabled :: FeatureFlag -> Getter Settings Bool -featureEnabled flag - = setFeatureFlags - . to (\case - Nothing -> False - Just (FeatureFlags flags) -> flag `elem` flags) - data JournalOpts = JournalOpts { _awsQueueName :: !Text -- ^ SQS queue name to send team events , _awsEndpoint :: !AWSEndpoint -- ^ AWS endpoint diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 7cbcef3b99d..76886e05f29 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -141,7 +141,7 @@ postConvOk = do cvs <- mapM (convView cid) [alice, bob, jane] liftIO $ mapM_ WS.assertSuccess =<< Async.mapConcurrently (checkWs alice) (zip cvs [wsA, wsB, wsJ]) where - convView cnv usr = decodeBodyMsg "conversation" <$> getConv usr cnv + convView cnv usr = responseJsonUnsafeWithMsg "conversation" <$> getConv usr cnv checkWs alice (cnv, ws) = WS.awaitMatch (5 # Second) ws $ \n -> do ntfTransient n @?= False let e = List1.head (WS.unpackPayload n) @@ -168,14 +168,14 @@ postCryptoMessage1 = do let m1 = [(bob, bc, "ciphertext1")] postOtrMessage id alice ac conv m1 !!! do const 412 === statusCode - assertTrue_ (eqMismatch [(eve, Set.singleton ec)] [] [] . decodeBody) + assertTrue_ (eqMismatch [(eve, Set.singleton ec)] [] [] . responseJsonUnsafe) -- Complete WS.bracketR2 c bob eve $ \(wsB, wsE) -> do let m2 = [(bob, bc, "ciphertext2"), (eve, ec, "ciphertext2")] postOtrMessage id alice ac conv m2 !!! do const 201 === statusCode - assertTrue_ (eqMismatch [] [] [] . decodeBody) + assertTrue_ (eqMismatch [] [] [] . responseJsonUnsafe) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr conv alice ac bc "ciphertext2") void . liftIO $ WS.assertMatch t wsE (wsAssertOtr conv alice ac ec "ciphertext2") @@ -184,7 +184,7 @@ postCryptoMessage1 = do let m3 = [(alice, ac, "ciphertext3"), (bob, bc, "ciphertext3"), (eve, ec, "ciphertext3")] postOtrMessage id alice ac conv m3 !!! do const 201 === statusCode - assertTrue_ (eqMismatch [] [(alice, Set.singleton ac)] [] . decodeBody) + assertTrue_ (eqMismatch [] [(alice, Set.singleton ac)] [] . responseJsonUnsafe) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr conv alice ac bc "ciphertext3") void . liftIO $ WS.assertMatch t wsE (wsAssertOtr conv alice ac ec "ciphertext3") -- Alice should not get it @@ -196,7 +196,7 @@ postCryptoMessage1 = do let m4 = [(bob, bc, "ciphertext4"), (eve, ec, "ciphertext4")] postOtrMessage id alice ac conv m4 !!! do const 201 === statusCode - assertTrue_ (eqMismatch [] [] [(eve, Set.singleton ec)] . decodeBody) + assertTrue_ (eqMismatch [] [] [(eve, Set.singleton ec)] . responseJsonUnsafe) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr conv alice ac bc "ciphertext4") -- Eve should not get it assertNoMsg wsE (wsAssertOtr conv alice ac ec "ciphertext4") @@ -206,7 +206,7 @@ postCryptoMessage1 = do let m5 = [(bob, bc, "ciphertext5"), (eve, ec, "ciphertext5"), (alice, ac, "ciphertext5")] postOtrMessage id alice ac conv m5 !!! do const 201 === statusCode - assertTrue_ (eqMismatch [] [(alice, Set.singleton ac)] [(eve, Set.singleton ec)] . decodeBody) + assertTrue_ (eqMismatch [] [(alice, Set.singleton ac)] [(eve, Set.singleton ec)] . responseJsonUnsafe) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr conv alice ac bc "ciphertext5") -- Neither Alice nor Eve should get it assertNoMsg wsA (wsAssertOtr conv alice ac ac "ciphertext5") @@ -218,7 +218,7 @@ postCryptoMessage1 = do const 412 === statusCode assertTrue_ (eqMismatch [(bob, Set.singleton bc)] [(alice, Set.singleton ac)] - [(eve, Set.singleton ec)] . decodeBody) + [(eve, Set.singleton ec)] . responseJsonUnsafe) -- A second client for Bob bc2 <- randomClient bob (someLastPrekeys !! 3) @@ -230,7 +230,7 @@ postCryptoMessage1 = do let m7 = [(bob, bc, cipher), (bob, bc2, cipher)] postOtrMessage id alice ac conv m7 !!! do const 201 === statusCode - assertTrue_ (eqMismatch [] [] [] . decodeBody) + assertTrue_ (eqMismatch [] [] [] . responseJsonUnsafe) -- Bob's first client gets both messages void . liftIO $ WS.assertMatch t wsB (wsAssertOtr conv alice ac bc cipher) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr conv alice ac bc2 cipher) @@ -251,12 +251,12 @@ postCryptoMessage2 = do let m = [(bob, bc, "hello bob")] r1 <- postOtrMessage id alice ac conv m Map.lookup eve (userClientMap p) @=? Just [ec] @@ -274,12 +274,12 @@ postCryptoMessage3 = do let m = otrRecipients [(bob, [(bc, ciphertext)])] r1 <- postProtoOtrMessage alice ac conv m Map.lookup eve (userClientMap p) @=? Just [ec] @@ -327,7 +327,7 @@ postCryptoMessage5 = do const 201 === statusCode _rs <- postOtrMessage (queryItem "report_missing" (toByteString' eve)) alice ac conv [] postO2OConv alice bob (Just "gossip1") + cnv1 <- responseJsonUnsafeWithMsg "conversation" <$> postO2OConv alice bob (Just "gossip1") getConvs alice (Just $ Left [cnvId cnv1]) Nothing !!! do const 200 === statusCode - const (Just [cnvId cnv1]) === fmap (map cnvId . convList) . decodeBody + const (Just [cnvId cnv1]) === fmap (map cnvId . convList) . responseJsonUnsafe -- create & get group conv carl <- randomUser connectUsers alice (singleton carl) - cnv2 <- decodeBodyMsg "conversation" <$> postConv alice [bob, carl] (Just "gossip2") [] Nothing Nothing + cnv2 <- responseJsonUnsafeWithMsg "conversation" <$> postConv alice [bob, carl] (Just "gossip2") [] Nothing Nothing getConvs alice (Just $ Left [cnvId cnv2]) Nothing !!! do const 200 === statusCode - const (Just [cnvId cnv2]) === fmap (map cnvId . convList) . decodeBody + const (Just [cnvId cnv2]) === fmap (map cnvId . convList) . responseJsonUnsafe -- get both rs <- getConvs alice Nothing Nothing decodeBody rs + 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 @@ -546,7 +546,7 @@ paginateConvIds = do where getChunk size alice start n = do resp <- getConvIds alice start (Just size) 0 @@ -570,11 +570,11 @@ getConvsPagingOk = do walk u = foldM_ (next u 3) Nothing next u step start n = do r1 <- getConvIds u (Right <$> start) (Just step) decodeBody r1 + let ids1 = convList <$> responseJsonUnsafe r1 liftIO $ assertEqual "unexpected length (getConvIds)" (Just n) (length <$> ids1) r2 <- getConvs u (Right <$> start) (Just step) decodeBody r2 + let ids3 = map cnvId . convList <$> responseJsonUnsafe r2 liftIO $ assertEqual "unexpected length (getConvs)" (Just n) (length <$> ids3) liftIO $ assertBool "getConvIds /= getConvs" (ids1 == ids3) @@ -588,7 +588,7 @@ postConvFailNotConnected = do jane <- randomUser postConv alice [bob, jane] Nothing [] Nothing Nothing !!! do const 403 === statusCode - const (Just "not-connected") === fmap label . decodeBody + const (Just "not-connected") === fmap label . responseJsonUnsafe postConvFailNumMembers :: TestM () postConvFailNumMembers = do @@ -598,7 +598,7 @@ postConvFailNumMembers = do connectUsers alice (list1 bob others) postConv alice (bob:others) Nothing [] Nothing Nothing !!! do const 400 === statusCode - const (Just "client-error") === fmap label . decodeBody + const (Just "client-error") === fmap label . responseJsonUnsafe -- | If somebody has blocked a user, that user shouldn't be able to create a -- group conversation which includes them. @@ -612,7 +612,7 @@ postConvFailBlocked = do !!! const 200 === statusCode postConv alice [bob, jane] Nothing [] Nothing Nothing !!! do const 403 === statusCode - const (Just "not-connected") === fmap label . decodeBody + const (Just "not-connected") === fmap label . responseJsonUnsafe postSelfConvOk :: TestM () postSelfConvOk = do @@ -641,7 +641,7 @@ postConvO2OFailWithSelf = do let inv = NewConvUnmanaged (NewConv [alice] Nothing mempty Nothing Nothing Nothing Nothing) post (g . path "/conversations/one2one" . zUser alice . zConn "conn" . zType "access" . json inv) !!! do const 403 === statusCode - const (Just "invalid-op") === fmap label . decodeBody + const (Just "invalid-op") === fmap label . responseJsonUnsafe postConnectConvOk :: TestM () postConnectConvOk = do @@ -674,10 +674,10 @@ putConvAcceptOk = do putConvAccept bob cnv !!! const 200 === statusCode getConv alice cnv !!! do const 200 === statusCode - const (Just One2OneConv) === fmap cnvType . decodeBody + const (Just One2OneConv) === fmap cnvType . responseJsonUnsafe getConv bob cnv !!! do const 200 === statusCode - const (Just One2OneConv) === fmap cnvType . decodeBody + const (Just One2OneConv) === fmap cnvType . responseJsonUnsafe putConvAcceptRetry :: TestM () putConvAcceptRetry = do @@ -709,7 +709,7 @@ postRepeatConnectConvCancel = do -- Alice wants to connect rsp1 <- postConnectConv alice bob "A" "a" Nothing getConv bob (cnvId cnv) + cnvX <- responseJsonUnsafeWithMsg "conversation" <$> getConv bob (cnvId cnv) liftIO $ do ConnectConv @=? cnvType cnvX (Just "B") @=? cnvName cnvX @@ -749,7 +749,7 @@ postRepeatConnectConvCancel = do -- Alice accepts, finally turning it into a 1-1 putConvAccept alice (cnvId cnv) !!! const 200 === statusCode - cnv4 <- decodeBodyMsg "conversation" <$> getConv alice (cnvId cnv) + cnv4 <- responseJsonUnsafeWithMsg "conversation" <$> getConv alice (cnvId cnv) liftIO $ do One2OneConv @=? cnvType cnv4 (Just "B") @=? cnvName cnv4 @@ -766,7 +766,7 @@ putBlockConvOk = do g <- view tsGalley alice <- randomUser bob <- randomUser - conv <- decodeBodyMsg "conversation" <$> postConnectConv alice bob "Alice" "connect with me!" (Just "me@me.com") + conv <- responseJsonUnsafeWithMsg "conversation" <$> postConnectConv alice bob "Alice" "connect with me!" (Just "me@me.com") getConv alice (cnvId conv) !!! const 200 === statusCode getConv bob (cnvId conv) !!! const 403 === statusCode @@ -777,7 +777,7 @@ putBlockConvOk = do -- A is still the only member of the 1-1 getConv alice (cnvId conv) !!! do const 200 === statusCode - const (cnvMembers conv) === cnvMembers . decodeBodyMsg "conversation" + const (cnvMembers conv) === cnvMembers . responseJsonUnsafeWithMsg "conversation" -- B accepts the conversation by unblocking put (g . paths ["/i/conversations", toByteString' (cnvId conv), "unblock"] . zUser bob) !!! @@ -828,7 +828,7 @@ leaveConnectConversation = do alice <- randomUser bob <- randomUser bdy <- postConnectConv alice bob "alice" "ni" Nothing decodeBody bdy) + let c = fromMaybe (error "invalid connect conversation") (cnvId <$> responseJsonUnsafe bdy) deleteMember alice alice c !!! const 403 === statusCode postMembersOk :: TestM () @@ -855,7 +855,7 @@ postMembersOk2 = do connectUsers bob (singleton chuck) conv <- decodeConvId <$> postConv alice [bob, chuck] Nothing [] Nothing Nothing postMembers bob (singleton chuck) conv !!! const 204 === statusCode - chuck' <- decodeBody <$> (getSelfMember chuck conv (getSelfMember chuck conv chuck') (Just chuck) @@ -895,7 +895,7 @@ postMembersFail = do postMembers chuck (singleton eve) conv !!! const 204 === statusCode postMembers chuck (singleton dave) conv !!! do const 403 === statusCode - const (Just "not-connected") === fmap label . decodeBody + const (Just "not-connected") === fmap label . responseJsonUnsafe void $ connectUsers chuck (singleton dave) postMembers chuck (singleton dave) conv !!! const 200 === statusCode postMembers chuck (singleton dave) conv !!! const 204 === statusCode @@ -911,7 +911,7 @@ postTooManyMembersFail = do x:xs <- randomUsers (n - 2) postMembers chuck (list1 x xs) conv !!! do const 403 === statusCode - const (Just "too-many-members") === fmap label . decodeBody + const (Just "too-many-members") === fmap label . responseJsonUnsafe deleteMembersOk :: TestM () deleteMembersOk = do @@ -1040,7 +1040,7 @@ putMemberOk update = do -- Verify new member state rs <- getConv bob conv decodeBody rs + let bob' = cmSelf . cnvMembers <$> responseJsonUnsafe rs liftIO $ do assertBool "user" (isJust bob') let newBob = fromJust bob' @@ -1065,7 +1065,7 @@ putReceiptModeOk = do -- By default, nothing is set getConv alice cnv !!! do const 200 === statusCode - const (Just Nothing) === fmap cnvReceiptMode . decodeBody + const (Just Nothing) === fmap cnvReceiptMode . responseJsonUnsafe -- Set receipt mode put ( g @@ -1079,7 +1079,7 @@ putReceiptModeOk = do -- Ensure the field is properly set getConv alice cnv !!! do const 200 === statusCode - const (Just $ Just (ReceiptMode 0)) === fmap cnvReceiptMode . decodeBody + const (Just $ Just (ReceiptMode 0)) === fmap cnvReceiptMode . responseJsonUnsafe void . liftIO $ checkWs alice (cnv, wsB) @@ -1097,12 +1097,12 @@ putReceiptModeOk = do -- Ensure that the new field remains unchanged getConv alice cnv !!! do const 200 === statusCode - const (Just $ Just (ReceiptMode 0)) === fmap cnvReceiptMode . decodeBody + const (Just $ Just (ReceiptMode 0)) === fmap cnvReceiptMode . responseJsonUnsafe cnv' <- decodeConvId <$> postConvWithReceipt alice [bob, jane] (Just "gossip") [] Nothing Nothing (ReceiptMode 0) getConv alice cnv' !!! do const 200 === statusCode - const (Just (Just (ReceiptMode 0))) === fmap cnvReceiptMode . decodeBody + const (Just (Just (ReceiptMode 0))) === fmap cnvReceiptMode . responseJsonUnsafe where checkWs alice (cnv, ws) = WS.awaitMatch (5 # Second) ws $ \n -> do ntfTransient n @?= False @@ -1156,9 +1156,9 @@ removeUser = do void . liftIO $ WS.assertMatchN (5 # Second) [wsA, wsB, wsC] $ matchMemberLeave conv2 bob -- Check memberships - mems1 <- fmap cnvMembers . decodeBody <$> getConv alice conv1 - mems2 <- fmap cnvMembers . decodeBody <$> getConv alice conv2 - mems3 <- fmap cnvMembers . decodeBody <$> getConv alice conv3 + mems1 <- fmap cnvMembers . responseJsonUnsafe <$> getConv alice conv1 + mems2 <- fmap cnvMembers . responseJsonUnsafe <$> getConv alice conv2 + mems3 <- fmap cnvMembers . responseJsonUnsafe <$> getConv alice conv3 let other u = find ((== u) . omId) . cmOthers liftIO $ do (mems1 >>= other bob) @?= Nothing diff --git a/services/galley/test/integration/API/MessageTimer.hs b/services/galley/test/integration/API/MessageTimer.hs index a09c2787f18..74bb45132d1 100644 --- a/services/galley/test/integration/API/MessageTimer.hs +++ b/services/galley/test/integration/API/MessageTimer.hs @@ -40,7 +40,7 @@ messageTimerInit mtimer = do cid <- assertConv rsp RegularConv alice alice [bob, jane] Nothing mtimer -- Check that the timer is indeed what it should be getConv jane cid !!! - const mtimer === (cnvMessageTimer <=< decodeBody) + const mtimer === (cnvMessageTimer <=< responseJsonUnsafe) messageTimerChange :: TestM () messageTimerChange = do @@ -57,17 +57,17 @@ messageTimerChange = do putMessageTimerUpdate alice cid (ConversationMessageTimerUpdate timer1sec) !!! const 200 === statusCode getConv jane cid !!! - const timer1sec === (cnvMessageTimer <=< decodeBody) + const timer1sec === (cnvMessageTimer <=< responseJsonUnsafe) -- Set timer to null putMessageTimerUpdate bob cid (ConversationMessageTimerUpdate Nothing) !!! const 200 === statusCode getConv jane cid !!! - const Nothing === (cnvMessageTimer <=< decodeBody) + const Nothing === (cnvMessageTimer <=< responseJsonUnsafe) -- Set timer to 1 year putMessageTimerUpdate bob cid (ConversationMessageTimerUpdate timer1year) !!! const 200 === statusCode getConv jane cid !!! - const timer1year === (cnvMessageTimer <=< decodeBody) + const timer1year === (cnvMessageTimer <=< responseJsonUnsafe) messageTimerChangeGuest :: TestM () messageTimerChangeGuest = do @@ -80,14 +80,14 @@ messageTimerChangeGuest = do -- Try to change the timer (as the guest user) and observe failure putMessageTimerUpdate guest cid (ConversationMessageTimerUpdate timer1sec) !!! do const 403 === statusCode - const "access-denied" === (label . decodeBodyMsg "error label") + const "access-denied" === (label . responseJsonUnsafeWithMsg "error label") getConv guest cid !!! - const Nothing === (cnvMessageTimer <=< decodeBody) + const Nothing === (cnvMessageTimer <=< responseJsonUnsafe) -- Try to change the timer (as a team member) and observe success putMessageTimerUpdate member cid (ConversationMessageTimerUpdate timer1sec) !!! const 200 === statusCode getConv guest cid !!! - const timer1sec === (cnvMessageTimer <=< decodeBody) + const timer1sec === (cnvMessageTimer <=< responseJsonUnsafe) messageTimerChangeO2O :: TestM () messageTimerChangeO2O = do @@ -100,9 +100,9 @@ messageTimerChangeO2O = do -- Try to change the timer and observe failure putMessageTimerUpdate alice cid (ConversationMessageTimerUpdate timer1sec) !!! do const 403 === statusCode - const "invalid-op" === (label . decodeBodyMsg "error label") + const "invalid-op" === (label . responseJsonUnsafeWithMsg "error label") getConv alice cid !!! - const Nothing === (cnvMessageTimer <=< decodeBodyM) + const Nothing === (cnvMessageTimer <=< responseJsonMaybe) messageTimerEvent :: TestM () messageTimerEvent = do diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index b5b74e640db..341b7ff5023 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -15,7 +15,7 @@ import Data.Id import Data.List1 import Data.Misc (PlainTextPassword (..)) import Data.Range -import Galley.Options (optSettings, featureEnabled) +import Galley.Options (optSettings, setFeatureFlags) import Galley.Types hiding (EventType (..), EventData (..), MemberUpdate (..)) import Galley.Types.Teams import Galley.Types.Teams.Intra @@ -163,25 +163,23 @@ testEnableSSOPerTeam = do let check :: HasCallStack => String -> SSOStatus -> TestM () check msg enabledness = do - SSOTeamConfig status <- jsonBody <$> (getSSOEnabledInternal tid (getSSOEnabledInternal tid TestM () putSSOEnabledInternalCheckNotImplemented = do g <- view tsGalley - Wai.Error status label _ <- jsonBody <$> put (g + Wai.Error status label _ <- responseJsonUnsafe <$> put (g . paths ["i", "teams", toByteString' tid, "features", "sso"] . json (SSOTeamConfig SSODisabled)) liftIO $ do assertEqual "bad status" status403 status assertEqual "bad label" "not-implemented" label - featureSSO <- view (tsGConf . optSettings . featureEnabled FeatureSSO) - if not featureSSO - then do - check "Teams should start with SSO disabled" SSODisabled - else do - check "Teams should start with SSO enabled" SSOEnabled + featureSSO <- view (tsGConf . optSettings . setFeatureFlags . flagSSO) + case featureSSO of + FeatureSSOEnabledByDefault -> check "Teams should start with SSO enabled" SSOEnabled + FeatureSSODisabledByDefault -> check "Teams should start with SSO disabled" SSODisabled putSSOEnabledInternal tid SSOEnabled check "Calling 'putEnabled True' should enable SSO" SSOEnabled @@ -201,7 +199,7 @@ testCreateOne2OneFailNonBindingTeamMembers = do -- Cannot create a 1-1 conversation, not connected and in the same team but not binding Util.createOne2OneTeamConv (mem1^.userId) (mem2^.userId) Nothing tid !!! do const 404 === statusCode - const "non-binding-team" === (Error.label . Util.decodeBodyMsg "error label") + const "non-binding-team" === (Error.label . responseJsonUnsafeWithMsg "error label") -- Both have a binding team but not the same team owner1 <- Util.randomUser tid1 <- Util.createTeamInternal "foo" owner1 @@ -211,7 +209,7 @@ testCreateOne2OneFailNonBindingTeamMembers = do assertQueue "create another team" tActivate Util.createOne2OneTeamConv owner1 owner2 Nothing tid1 !!! do const 403 === statusCode - const "non-binding-team-members" === (Error.label . Util.decodeBodyMsg "error label") + const "non-binding-team-members" === (Error.label . responseJsonUnsafeWithMsg "error label") testCreateOne2OneWithMembers :: HasCallStack @@ -390,7 +388,7 @@ testRemoveBindingTeamMember ownerHasPassword = do . json (newTeamMemberDeleteData (Just $ PlainTextPassword "wrong passwd")) ) !!! do const 403 === statusCode - const "access-denied" === (Error.label . Util.decodeBodyMsg "error label") + const "access-denied" === (Error.label . responseJsonUnsafeWithMsg "error label") -- Mem1 is still part of Wire Util.ensureDeletedState False owner (mem1^.userId) @@ -497,7 +495,7 @@ testAddTeamConvAsExternalPartner = do (Just "blaa") acc (Just TeamAccessRole) Nothing !!! do const 403 === statusCode - const "operation-denied" === (Error.label . Util.decodeBodyMsg "error label") + const "operation-denied" === (Error.label . responseJsonUnsafeWithMsg "error label") testAddManagedConv :: TestM () testAddManagedConv = do @@ -556,7 +554,7 @@ testAddTeamMemberToConv = do Util.assertNotConvMember (mem3^.userId) cid Util.postMembers (mem3^.userId) (list1 (mem1^.userId) []) cid !!! do const 403 === statusCode - const "operation-denied" === (Error.label . Util.decodeBodyMsg "error label") + const "operation-denied" === (Error.label . responseJsonUnsafeWithMsg "error label") testUpdateTeamConv :: Role -- ^ Role of the user who creates the conversation @@ -600,6 +598,8 @@ testDeleteTeam = do const 202 === statusCode checkTeamDeleteEvent tid wsOwner checkTeamDeleteEvent tid wsMember + checkConvDeleteEvent cid1 wsOwner + checkConvDeleteEvent cid1 wsMember checkConvDeleteEvent cid1 wsExtern WS.assertNoEvent timeout [wsOwner, wsExtern, wsMember] @@ -619,7 +619,7 @@ testDeleteTeam = do Util.getConv u x !!! const 404 === statusCode Util.getSelfMember u x !!! do const 200 === statusCode - const (Just Null) === Util.decodeBodyM + const (Just Null) === responseJsonMaybe assertQueueEmpty testDeleteBindingTeam :: Bool -> TestM () @@ -650,7 +650,7 @@ testDeleteBindingTeam ownerHasPassword = do . json (newTeamDeleteData (Just $ PlainTextPassword "wrong passwd")) ) !!! do const 403 === statusCode - const "access-denied" === (Error.label . Util.decodeBodyMsg "error label") + const "access-denied" === (Error.label . responseJsonUnsafeWithMsg "error label") delete ( g . paths ["teams", toByteString' tid, "members", toByteString' (mem3^.userId)] @@ -724,6 +724,9 @@ testDeleteTeamConv = do checkTeamConvDeleteEvent tid cid2 wsOwner checkTeamConvDeleteEvent tid cid2 wsMember + checkConvDeleteEvent cid2 wsOwner + checkConvDeleteEvent cid2 wsMember + WS.assertNoEvent timeout [wsOwner, wsMember] delete ( g . paths ["teams", toByteString' tid, "conversations", toByteString' cid1] @@ -733,9 +736,10 @@ testDeleteTeamConv = do checkTeamConvDeleteEvent tid cid1 wsOwner checkTeamConvDeleteEvent tid cid1 wsMember - checkConvDeletevent cid1 wsExtern - - WS.assertNoEvent timeout [wsOwner, wsExtern, wsMember] + checkConvDeleteEvent cid1 wsOwner + checkConvDeleteEvent cid1 wsMember + checkConvDeleteEvent cid1 wsExtern + WS.assertNoEvent timeout [wsOwner, wsMember, wsExtern] for_ [cid1, cid2] $ \x -> for_ [owner, member^.userId, extern] $ \u -> do @@ -743,20 +747,6 @@ testDeleteTeamConv = do Util.assertNotConvMember u x postConvCodeCheck code !!! const 404 === statusCode - where - checkTeamConvDeleteEvent tid cid w = WS.assertMatch_ timeout w $ \notif -> do - ntfTransient notif @?= False - let e = List1.head (WS.unpackPayload notif) - e^.eventType @?= ConvDelete - e^.eventTeam @?= tid - e^.eventData @?= Just (EdConvDelete cid) - - checkConvDeletevent cid w = WS.assertMatch_ timeout w $ \notif -> do - ntfTransient notif @?= False - let e = List1.head (WS.unpackPayload notif) - evtType e @?= Conv.ConvDelete - evtConv e @?= cid - evtData e @?= Nothing testUpdateTeam :: TestM () testUpdateTeam = do @@ -815,7 +805,7 @@ testUpdateTeamMember = do . json changeOwner ) !!! do const 403 === statusCode - const "no-other-owner" === (Error.label . Util.decodeBodyMsg "error label") + const "no-other-owner" === (Error.label . responseJsonUnsafeWithMsg "error label") let changeMember = newNewTeamMember (member & permissions .~ fullPermissions) WS.bracketR2 c owner (member^.userId) $ \(wsOwner, wsMember) -> do put ( g @@ -875,7 +865,7 @@ testUpdateTeamStatus = do . json (TeamStatusUpdate Deleted Nothing) ) !!! do const 403 === statusCode - const "invalid-team-status-update" === (Error.label . Util.decodeBodyMsg "error label") + const "invalid-team-status-update" === (Error.label . responseJsonUnsafeWithMsg "error label") checkUserDeleteEvent :: HasCallStack => UserId -> WS.WebSocket -> TestM () checkUserDeleteEvent uid w = WS.assertMatch_ timeout w $ \notif -> do @@ -926,6 +916,14 @@ checkTeamDeleteEvent tid w = WS.assertMatch_ timeout w $ \notif -> do e^.eventTeam @?= tid e^.eventData @?= Nothing +checkTeamConvDeleteEvent :: HasCallStack => TeamId -> ConvId -> WS.WebSocket -> TestM () +checkTeamConvDeleteEvent tid cid w = WS.assertMatch_ timeout w $ \notif -> do + ntfTransient notif @?= False + let e = List1.head (WS.unpackPayload notif) + e^.eventType @?= ConvDelete + e^.eventTeam @?= tid + e^.eventData @?= Just (EdConvDelete cid) + checkConvDeleteEvent :: HasCallStack => ConvId -> WS.WebSocket -> TestM () checkConvDeleteEvent cid w = WS.assertMatch_ timeout w $ \notif -> do ntfTransient notif @?= False @@ -970,7 +968,7 @@ postCryptoBroadcastMessageJson = do WS.bracketR (c . queryItem "client" (toByteString' ac)) alice $ \wsA1 -> do Util.postOtrBroadcastMessage id alice ac msg !!! do const 201 === statusCode - assertTrue_ (eqMismatch [] [] [] . decodeBody) + assertTrue_ (eqMismatch [] [] [] . responseJsonUnsafe) -- Bob should get the broadcast (team member of alice) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr (selfConv bob) alice ac bc "ciphertext1") -- Charlie should get the broadcast (contact of alice and user of teams feature) @@ -1000,14 +998,14 @@ postCryptoBroadcastMessageJson2 = do let m1 = [(bob, bc, "ciphertext1")] Util.postOtrBroadcastMessage id alice ac m1 !!! do const 412 === statusCode - assertTrue "1: Only Charlie and his device" (eqMismatch [(charlie, Set.singleton cc)] [] [] . decodeBody) + assertTrue "1: Only Charlie and his device" (eqMismatch [(charlie, Set.singleton cc)] [] [] . responseJsonUnsafe) -- Complete WS.bracketR2 c bob charlie $ \(wsB, wsE) -> do let m2 = [(bob, bc, "ciphertext2"), (charlie, cc, "ciphertext2")] Util.postOtrBroadcastMessage id alice ac m2 !!! do const 201 === statusCode - assertTrue "No devices expected" (eqMismatch [] [] [] . decodeBody) + assertTrue "No devices expected" (eqMismatch [] [] [] . responseJsonUnsafe) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr (selfConv bob) alice ac bc "ciphertext2") void . liftIO $ WS.assertMatch t wsE (wsAssertOtr (selfConv charlie) alice ac cc "ciphertext2") @@ -1016,7 +1014,7 @@ postCryptoBroadcastMessageJson2 = do let m3 = [(alice, ac, "ciphertext3"), (bob, bc, "ciphertext3"), (charlie, cc, "ciphertext3")] Util.postOtrBroadcastMessage id alice ac m3 !!! do const 201 === statusCode - assertTrue "2: Only Alice and her device" (eqMismatch [] [(alice, Set.singleton ac)] [] . decodeBody) + assertTrue "2: Only Alice and her device" (eqMismatch [] [(alice, Set.singleton ac)] [] . responseJsonUnsafe) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr (selfConv bob) alice ac bc "ciphertext3") void . liftIO $ WS.assertMatch t wsE (wsAssertOtr (selfConv charlie) alice ac cc "ciphertext3") -- Alice should not get it @@ -1028,7 +1026,7 @@ postCryptoBroadcastMessageJson2 = do let m4 = [(bob, bc, "ciphertext4"), (charlie, cc, "ciphertext4")] Util.postOtrBroadcastMessage id alice ac m4 !!! do const 201 === statusCode - assertTrue "3: Only Charlie and his device" (eqMismatch [] [] [(charlie, Set.singleton cc)] . decodeBody) + assertTrue "3: Only Charlie and his device" (eqMismatch [] [] [(charlie, Set.singleton cc)] . responseJsonUnsafe) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr (selfConv bob) alice ac bc "ciphertext4") -- charlie should not get it assertNoMsg wsE (wsAssertOtr (selfConv charlie) alice ac cc "ciphertext4") @@ -1057,7 +1055,7 @@ postCryptoBroadcastMessageProto = do let msg = otrRecipients [(bob, [(bc, ciphertext)]), (charlie, [(cc, ciphertext)]), (dan, [(dc, ciphertext)])] Util.postProtoOtrBroadcast alice ac msg !!! do const 201 === statusCode - assertTrue_ (eqMismatch [] [] [] . decodeBody) + assertTrue_ (eqMismatch [] [] [] . responseJsonUnsafe) -- Bob should get the broadcast (team member of alice) void . liftIO $ WS.assertMatch t wsB (wsAssertOtr' (encodeCiphertext "data") (selfConv bob) alice ac bc ciphertext) -- Charlie should get the broadcast (contact of alice and user of teams feature) @@ -1089,7 +1087,7 @@ postCryptoBroadcastMessage100OrMaxConns = do let msg = (bob, bc, "ciphertext") : (f <$> others) Util.postOtrBroadcastMessage id alice ac msg !!! do const 201 === statusCode - assertTrue_ (eqMismatch [] [] [] . decodeBody) + assertTrue_ (eqMismatch [] [] [] . responseJsonUnsafe) void . liftIO $ WS.assertMatch t (Imports.head ws) (wsAssertOtr (selfConv bob) alice ac bc "ciphertext") for_ (zip (tail ws) others) $ \(wsU, (u, clt)) -> liftIO $ WS.assertMatch t wsU (wsAssertOtr (selfConv u) alice ac clt "ciphertext") @@ -1165,27 +1163,26 @@ testFeatureFlags = do let getSSO :: HasCallStack => SSOStatus -> TestM () getSSO expected = getSSOEnabled owner tid !!! do statusCode === const 200 - responseJson === const (Right (SSOTeamConfig expected)) + responseJsonEither === const (Right (SSOTeamConfig expected)) getSSOInternal :: HasCallStack => SSOStatus -> TestM () getSSOInternal expected = getSSOEnabledInternal tid !!! do statusCode === const 200 - responseJson === const (Right (SSOTeamConfig expected)) + responseJsonEither === const (Right (SSOTeamConfig expected)) setSSOInternal :: HasCallStack => SSOStatus -> TestM () setSSOInternal = putSSOEnabledInternal tid - featureSSO <- view (tsGConf . optSettings . featureEnabled FeatureSSO) - if not featureSSO - then do -- disabled + featureSSO <- view (tsGConf . optSettings . setFeatureFlags . flagSSO) + case featureSSO of + FeatureSSODisabledByDefault -> do getSSO SSODisabled getSSOInternal SSODisabled setSSOInternal SSOEnabled getSSO SSOEnabled getSSOInternal SSOEnabled - - else do -- enabled + FeatureSSOEnabledByDefault -> do -- since we don't allow to disable (see 'disableSsoNotImplemented'), we can't test -- much here. (disable failure is covered in "enable/disable SSO" above.) getSSO SSOEnabled @@ -1196,12 +1193,12 @@ testFeatureFlags = do let getLegalHold :: HasCallStack => LegalHoldStatus -> TestM () getLegalHold expected = getLegalHoldEnabled owner tid !!! do statusCode === const 200 - responseJson === const (Right (LegalHoldTeamConfig expected)) + responseJsonEither === const (Right (LegalHoldTeamConfig expected)) getLegalHoldInternal :: HasCallStack => LegalHoldStatus -> TestM () getLegalHoldInternal expected = getLegalHoldEnabledInternal tid !!! do statusCode === const 200 - responseJson === const (Right (LegalHoldTeamConfig expected)) + responseJsonEither === const (Right (LegalHoldTeamConfig expected)) setLegalHoldInternal :: HasCallStack => LegalHoldStatus -> TestM () setLegalHoldInternal = putLegalHoldEnabledInternal tid @@ -1209,11 +1206,11 @@ testFeatureFlags = do getLegalHold LegalHoldDisabled getLegalHoldInternal LegalHoldDisabled - featureLegalHold <- view (tsGConf . optSettings . featureEnabled FeatureLegalHold) - if featureLegalHold - then do + featureLegalHold <- view (tsGConf . optSettings . setFeatureFlags . flagLegalHold) + case featureLegalHold of + FeatureLegalHoldDisabledByDefault -> do setLegalHoldInternal LegalHoldEnabled getLegalHold LegalHoldEnabled getLegalHoldInternal LegalHoldEnabled - else do + FeatureLegalHoldDisabledPermanently -> do putLegalHoldEnabledInternal' expect4xx tid LegalHoldEnabled diff --git a/services/galley/test/integration/API/Teams/LegalHold.hs b/services/galley/test/integration/API/Teams/LegalHold.hs index 55f352369ef..c9d0c1ea513 100644 --- a/services/galley/test/integration/API/Teams/LegalHold.hs +++ b/services/galley/test/integration/API/Teams/LegalHold.hs @@ -31,7 +31,7 @@ import Data.Text.Encoding (encodeUtf8) import Galley.API.Swagger (GalleyRoutes) import Galley.External.LegalHoldService (validateServiceKey) import Galley.Types.Teams -import Galley.Options (optSettings, featureEnabled) +import Galley.Options (optSettings, setFeatureFlags) import GHC.Generics hiding (to) import GHC.TypeLits import Gundeck.Types.Notification (ntfPayload) @@ -69,10 +69,12 @@ import qualified Test.Tasty.Cannon as WS onlyIfLhEnabled :: TestM () -> TestM () onlyIfLhEnabled action = do - featureLegalHold <- view (tsGConf . optSettings . featureEnabled FeatureLegalHold) - if featureLegalHold - then action - else liftIO $ hPutStrLn stderr "*** legalhold feature flag disabled, not running integration tests" + featureLegalHold <- view (tsGConf . optSettings . setFeatureFlags . flagLegalHold) + case featureLegalHold of + FeatureLegalHoldDisabledPermanently + -> liftIO $ hPutStrLn stderr "*** legalhold feature flag disabled, not running integration tests" + FeatureLegalHoldDisabledByDefault + -> action tests :: IO TestSetup -> TestTree tests s = testGroup "Teams LegalHold API" @@ -100,11 +102,6 @@ tests s = testGroup "Teams LegalHold API" -- tested {- TODO: - zauth/libzauth level: Allow access to legal hold service tokens - conversations/{cnv}/otr/messages - /notifications - /access - (maybe others?) conversations/{cnv}/otr/messages - possibly show the legal hold device (if missing) as a different device type (or show that on device level, depending on how client teams prefer) GET /team/{tid}/members - show legal hold status of all members @@ -420,7 +417,7 @@ testGetLegalHoldTeamSettings = do do let respOk :: ResponseLBS -> TestM () respOk resp = liftIO $ do assertEqual "bad status code" 200 (statusCode resp) - assertEqual "bad body" ViewLegalHoldServiceDisabled (jsonBody resp) + assertEqual "bad body" ViewLegalHoldServiceDisabled (responseJsonUnsafe resp) getSettings owner tid >>= respOk getSettings member tid >>= respOk @@ -430,7 +427,7 @@ testGetLegalHoldTeamSettings = do do let respOk :: ResponseLBS -> TestM () respOk resp = liftIO $ do assertEqual "bad status code" 200 (statusCode resp) - assertEqual "bad body" ViewLegalHoldServiceNotConfigured (jsonBody resp) + assertEqual "bad body" ViewLegalHoldServiceNotConfigured (responseJsonUnsafe resp) getSettings owner tid >>= respOk getSettings member tid >>= respOk @@ -487,7 +484,7 @@ testRemoveLegalHoldFromTeam = do assertEqual "path" ["legalhold", "remove"] (pathInfo req) assertEqual "method" "POST" (requestMethod req) resp <- getSettings owner tid - liftIO $ assertEqual "bad body" ViewLegalHoldServiceNotConfigured (jsonBody resp) + liftIO $ assertEqual "bad body" ViewLegalHoldServiceNotConfigured (responseJsonUnsafe resp) -- returns 204 if legal hold is successfully removed from team -- is idempotent (deleting twice in a row works) from BE's PoV @@ -509,11 +506,11 @@ testEnablePerTeam = do addTeamMemberInternal tid $ newTeamMember member (rolePermissions RoleMember) Nothing ensureQueueEmpty - do LegalHoldTeamConfig status <- jsonBody <$> (getEnabled tid (getEnabled tid (getEnabled tid (getEnabled tid do @@ -524,7 +521,7 @@ testEnablePerTeam = do do putEnabled tid LegalHoldDisabled -- disable again - LegalHoldTeamConfig status <- jsonBody <$> (getEnabled tid (getEnabled tid UserId -> TeamId -> TestM ViewLegalHoldService -getSettingsTyped uid tid = jsonBody <$> (getSettings uid tid (getSettings uid tid UserId -> TeamId -> TestM ResponseLBS getSettings uid tid = do @@ -668,7 +665,7 @@ deleteSettings mPassword uid tid = do getUserStatusTyped :: HasCallStack => UserId -> TeamId -> TestM UserLegalHoldStatusResponse getUserStatusTyped uid tid = do resp <- getUserStatus uid tid UserId -> TeamId -> TestM ResponseLBS getUserStatus uid tid = do @@ -705,15 +702,15 @@ disableLegalHoldForUser mPassword tid zusr uid = do assertExactlyOneLegalHoldDevice :: HasCallStack => UserId -> TestM () assertExactlyOneLegalHoldDevice uid = do clients :: [Client] - <- getClients uid >>= maybe (error $ "decodeBody: [Client]") pure . decodeBody + <- getClients uid >>= responseJsonError liftIO $ do let numdevs = length $ clientType <$> clients assertEqual ("expected exactly one legal hold device for user: " <> show uid) numdevs 1 assertZeroLegalHoldDevices :: HasCallStack => UserId -> TestM () assertZeroLegalHoldDevices uid = do - clients :: [Client] <- getClients uid - >>= maybe (error $ "decodeBody: [Client]") pure . decodeBody + clients :: [Client] + <- getClients uid >>= responseJsonError liftIO $ do let numdevs = length $ clientType <$> clients assertBool ("a legal hold device was found when none was expected for user" diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 1485041b042..b224af20e35 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -9,7 +9,6 @@ import Control.Retry (retrying, constantDelay, limitRetries) import Data.Aeson hiding (json) import Data.Aeson.Lens (key) import Data.ByteString.Conversion -import Data.EitherR (fmapL) import Data.Id import Data.List1 as List1 import Data.Misc @@ -39,7 +38,7 @@ import qualified Galley.Types.Proto as Proto import qualified Test.QuickCheck as Q import qualified Test.Tasty.Cannon as WS -type ResponseLBS = Response (Maybe LByteString) + ------------------------------------------------------------------------------- -- API Operations @@ -93,25 +92,25 @@ getTeam :: HasCallStack => UserId -> TeamId -> TestM Team getTeam usr tid = do g <- view tsGalley r <- get (g . paths ["teams", toByteString' tid] . zUser usr) UserId -> TeamId -> TestM TeamMemberList getTeamMembers usr tid = do g <- view tsGalley r <- get (g . paths ["teams", toByteString' tid, "members"] . zUser usr) UserId -> TeamId -> UserId -> TestM TeamMember getTeamMember usr tid mid = do g <- view tsGalley r <- get (g . paths ["teams", toByteString' tid, "members", toByteString' mid] . zUser usr) TeamId -> UserId -> TestM TeamMember getTeamMemberInternal tid mid = do g <- view tsGalley r <- get (g . paths ["i", "teams", toByteString' tid, "members", toByteString' mid]) UserId -> TeamId -> TeamMember -> TestM () addTeamMember usr tid mem = do @@ -449,13 +448,13 @@ assertConvMember :: HasCallStack => UserId -> ConvId -> TestM () assertConvMember u c = getSelfMember u c !!! do const 200 === statusCode - const (Right u) === (fmap memId <$> decodeBodyE) + const (Right u) === (fmap memId <$> responseJsonEither) assertNotConvMember :: HasCallStack => UserId -> ConvId -> TestM () assertNotConvMember u c = getSelfMember u c !!! do const 200 === statusCode - const (Right Null) === decodeBodyE + const (Right Null) === responseJsonEither ------------------------------------------------------------------------------- -- Common Assertions @@ -485,7 +484,7 @@ assertConv :: HasCallStack -> TestM ConvId assertConv r t c s us n mt = do cId <- fromBS $ getHeader' "Location" r - let cnv = decodeBody r :: Maybe Conversation + let cnv = responseJsonMaybe @Conversation r let _self = cmSelf . cnvMembers <$> cnv let others = cmOthers . cnvMembers <$> cnv liftIO $ do @@ -569,21 +568,12 @@ assertNoMsg ws f = do ------------------------------------------------------------------------------- -- Helpers -jsonBody :: (HasCallStack, FromJSON v) => ResponseLBS -> v -jsonBody resp = either (error . show . (, bdy)) id . eitherDecode $ bdy - where - bdy = fromJust $ responseBody resp - --- FUTUREWORK: move this to /lib/bilge? (there is another copy of this in spar.) -responseJSON :: (HasCallStack, FromJSON a) => ResponseLBS -> Either String a -responseJSON = fmapL show . eitherDecode <=< maybe (Left "no body") pure . responseBody - testResponse :: HasCallStack => Int -> Maybe TestErrorLabel -> Assertions () testResponse status mlabel = do const status === statusCode case mlabel of - Just label -> responseJSON === const (Right label) - Nothing -> (isLeft <$> responseJSON @TestErrorLabel) === const True + Just label -> responseJsonEither === const (Right label) + Nothing -> (isLeft <$> responseJsonEither @TestErrorLabel) === const True newtype TestErrorLabel = TestErrorLabel { fromTestErrorLabel :: ST } deriving (Eq, Show) @@ -595,23 +585,21 @@ instance FromJSON TestErrorLabel where parseJSON = fmap TestErrorLabel . withObject "TestErrorLabel" (.: "label") decodeConvCode :: Response (Maybe Lazy.ByteString) -> ConversationCode -decodeConvCode r = fromMaybe (error "Failed to parse ConversationCode response") $ - decodeBody r +decodeConvCode = responseJsonUnsafe decodeConvCodeEvent :: Response (Maybe Lazy.ByteString) -> ConversationCode -decodeConvCodeEvent r = case fromMaybe (error "Failed to parse Event") $ decodeBody r of +decodeConvCodeEvent r = case responseJsonUnsafe r of (Event ConvCodeUpdate _ _ _ (Just (EdConvCodeUpdate c))) -> c _ -> error "Failed to parse ConversationCode from Event" decodeConvId :: Response (Maybe Lazy.ByteString) -> ConvId -decodeConvId r = fromMaybe (error "Failed to parse conversation") $ - cnvId <$> decodeBody r +decodeConvId = cnvId . responseJsonUnsafe decodeConvList :: Response (Maybe Lazy.ByteString) -> [Conversation] -decodeConvList = convList . decodeBodyMsg "conversations" +decodeConvList = convList . responseJsonUnsafeWithMsg "conversations" decodeConvIdList :: Response (Maybe Lazy.ByteString) -> [ConvId] -decodeConvIdList = convList . decodeBodyMsg "conversation-ids" +decodeConvIdList = convList . responseJsonUnsafeWithMsg "conversation-ids" zUser :: UserId -> Request -> Request zUser = header "Z-User" . toByteString' @@ -699,7 +687,7 @@ ephemeralUser = do name <- UUID.toText <$> liftIO nextRandom let p = object [ "name" .= name ] r <- post (b . path "/register" . json p) UserId -> LastPrekey -> TestM ClientId @@ -707,8 +695,7 @@ randomClient uid lk = do b <- view tsBrig resp <- post (b . paths ["i", "clients", toByteString' uid] . json newClientBody) TestM ResponseLBS getClients u = do @@ -774,7 +761,7 @@ isMember usr cnv = do res <- get $ g . paths ["i", "conversations", toByteString' cnv, "members", toByteString' usr] . expect2xx - return $ isJust (decodeBody res :: Maybe Member) + return $ isJust (responseJsonMaybe @Member res) randomUserWithClient :: LastPrekey -> TestM (UserId, ClientId) randomUserWithClient lk = do @@ -785,20 +772,6 @@ randomUserWithClient lk = do newNonce :: TestM (Id ()) newNonce = randomId -decodeBodyE :: (HasCallStack, FromJSON a) => Response (Maybe Lazy.ByteString) -> Either String a -decodeBodyE rsp = do - bdy <- maybe (Left "no body") Right $ responseBody rsp - eitherDecode bdy - -decodeBodyM :: (HasCallStack, FromJSON a) => Response (Maybe Lazy.ByteString) -> Maybe a -decodeBodyM = either (const Nothing) Just . decodeBodyE - -decodeBody :: (HasCallStack, FromJSON a) => Response (Maybe Lazy.ByteString) -> a -decodeBody = decodeBodyMsg mempty - -decodeBodyMsg :: (HasCallStack, FromJSON a) => String -> Response (Maybe Lazy.ByteString) -> a -decodeBodyMsg usrerr = either (\prserr -> error $ "decodeBody: " ++ show (prserr, usrerr)) id . decodeBodyE - fromBS :: (HasCallStack, FromByteString a, Monad m) => ByteString -> m a fromBS = maybe (fail "fromBS: no parse") return . fromByteString diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index dd381093350..70def7a3115 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -7,7 +7,7 @@ import Bilge.Assert import Control.Arrow ((&&&)) import Control.Concurrent.Async (Async, async, wait, concurrently_, forConcurrently_) import Control.Lens ((.~), (^.), (^?), view, (<&>), _2, (%~)) -import Control.Retry (retrying, constantDelay, limitRetries) +import Control.Retry (retrying, recoverAll, constantDelay, limitRetries) import Data.Aeson hiding (json) import Data.Aeson.Lens import Data.ByteString.Conversion @@ -160,7 +160,8 @@ removeStalePresence = do wsAssertPresences uid 1 liftIO $ void $ putMVar m () >> wait w sendPush (push uid [uid]) - ensurePresent uid 0 + recoverAll (constantDelay 1000000 <> limitRetries 10) $ \_ -> do + ensurePresent uid 0 where pload = List1.singleton $ HashMap.fromList [ "foo" .= (42 :: Int) ] push u us = newPush u (toRecipients us) pload & pushOriginConnection .~ Just (ConnId "dev") diff --git a/services/integration.sh b/services/integration.sh index 6f8c5ec721f..e40c67d2643 100755 --- a/services/integration.sh +++ b/services/integration.sh @@ -26,7 +26,7 @@ function list_descendants () { } function kill_gracefully() { - pkill "gundeck|brig|galley|cargohold|cannon|spar" + pkill "gundeck|brig|galley|cargohold|cannon|spar|nginz" sleep 1 kill $(list_descendants "$PARENT_PID") &> /dev/null } @@ -92,6 +92,7 @@ function run() { | sed ${UNBUFFERED} -e "s/^/$(tput setaf ${colour})[${service}] /" -e "s/$/$(tput sgr0)/" & } + check_prerequisites run brig "" ${green} @@ -102,10 +103,27 @@ run cannon "2" ${orange} run cargohold "" ${purpleish} run spar "" ${orange} +function run_nginz() { + colour=$1 + prefix=$([ -w /usr/local ] && echo /usr/local || echo "${HOME}/.wire-dev") + (cd ${NGINZ_WORK_DIR} && LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${prefix}/lib/ ${TOP_LEVEL}/dist/nginx -p ${NGINZ_WORK_DIR} -c ${NGINZ_WORK_DIR}/conf/nginz/nginx.conf -g 'daemon off;' || kill_all) \ + | sed -e "s/^/$(tput setaf ${colour})[nginz] /" -e "s/$/$(tput sgr0)/" & +} + +NGINZ_PORT="" +if [[ $INTEGRATION_USE_NGINZ -eq 1 ]]; then + NGINZ_PORT=8080 + # Note: for integration tests involving nginz, + # nginz and brig must share the same zauth public/private keys + export NGINZ_WORK_DIR="$TOP_LEVEL/services/nginz/integration-test" + + run_nginz ${purpleish} +fi + # the ports are copied from ./integration.yaml while [ "$all_services_are_up" == "" ]; do export all_services_are_up="1" - for port in $(seq 8082 8086) 8088; do + for port in $(seq 8082 8086) 8088 $NGINZ_PORT; do ( curl --write-out '%{http_code}' --silent --output /dev/null http://localhost:"$port"/i/status \ | grep -q '^20[04]' ) \ || export all_services_are_up="" diff --git a/services/integration.yaml b/services/integration.yaml index eed7c9b0e86..7a1400edd95 100644 --- a/services/integration.yaml +++ b/services/integration.yaml @@ -31,6 +31,10 @@ spar: host: 127.0.0.1 port: 8088 +nginz: + host: 127.0.0.1 + port: 8080 + # Used by brig-integration (bot providers), galley-integration (legal hold) provider: privateKey: test/resources/key.pem diff --git a/services/nginz/integration-test/conf/nginz b/services/nginz/integration-test/conf/nginz new file mode 120000 index 00000000000..c4846f61eac --- /dev/null +++ b/services/nginz/integration-test/conf/nginz @@ -0,0 +1 @@ +../../../../deploy/services-demo/conf/nginz \ No newline at end of file diff --git a/services/nginz/integration-test/resources/zauth b/services/nginz/integration-test/resources/zauth new file mode 120000 index 00000000000..9e38324808c --- /dev/null +++ b/services/nginz/integration-test/resources/zauth @@ -0,0 +1 @@ +../../../brig/test/resources/zauth \ No newline at end of file diff --git a/services/nginz/third_party/nginx-zauth-module/zauth_module.c b/services/nginz/third_party/nginx-zauth-module/zauth_module.c index f3ec98a5bb0..ca7e878ad39 100644 --- a/services/nginz/third_party/nginx-zauth-module/zauth_module.c +++ b/services/nginz/third_party/nginx-zauth-module/zauth_module.c @@ -429,6 +429,14 @@ static ngx_int_t zauth_token_typeinfo (ngx_http_request_t * r, ngx_http_variable Range range = { (u_char*) "user", 4 }; return zauth_set_var(r->pool, v, range); } + case ZAUTH_TOKEN_TYPE_LEGAL_HOLD_ACCESS: { + Range range = {(u_char*) "legal_hold_access", 9 }; + return zauth_set_var(r->pool, v, range); + } + case ZAUTH_TOKEN_TYPE_LEGAL_HOLD_USER: { + Range range = { (u_char*) "legal_hold_user", 10 }; + return zauth_set_var(r->pool, v, range); + } case ZAUTH_TOKEN_TYPE_PROVIDER: { Range range = { (u_char*) "provider", 8 }; return zauth_set_var(r->pool, v, range); @@ -453,7 +461,7 @@ static ngx_int_t zauth_token_var_conn (ngx_http_request_t * r, ngx_http_variable if (t == NULL) { return NGX_ERROR; } - if (zauth_token_type(t) == ZAUTH_TOKEN_TYPE_ACCESS) { + if (zauth_token_type(t) == ZAUTH_TOKEN_TYPE_ACCESS || zauth_token_type(t) == ZAUTH_TOKEN_TYPE_LEGAL_HOLD_ACCESS) { return zauth_set_var(r->pool, v, zauth_token_lookup(t, 'c')); } else { zauth_empty_val(v); diff --git a/services/restund/README.md b/services/restund/README.md index 4e7dccc0981..260ecd131cf 100644 --- a/services/restund/README.md +++ b/services/restund/README.md @@ -127,3 +127,17 @@ Example rkt command: --user=restund \ --group=restund ``` + +In case You have set up restund without docker, you just need to make some of these changes: + +Put your private IP of the server in place of: `{{ ansible_default_ipv4.address }}`. And replace restund listen ports with `3478`, for both UDP and TCP. + +You may comment these out in case you don't want to use: +``` +module zrest.so +module auth.so +zrest_secret {{ restund_zrest_secret }} +``` +It'll help in running the TURN server without interuption or further configuration for testing purpose. List out TURN IP and port in `deploy/services-demo/resources/turn/servers.txt`, and `deploy/services-demo/resources/turn/servers-v2.txt`, as given below: +`turn::3478` +Then run the command restund command and You'll get the live stun log in your terminal. \ No newline at end of file diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 03f8187b284..372d15fefc1 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -261,7 +261,7 @@ specFinalizeLogin = do sparresp <- submitAuthnResponse authnresp liftIO $ do statusCode sparresp `shouldBe` 404 - responseJSON sparresp `shouldBe` Right (TestErrorLabel "not-found") + responseJsonEither sparresp `shouldBe` Right (TestErrorLabel "not-found") context "AuthnResponse does not match any request" $ do it "rejects" $ do @@ -327,7 +327,7 @@ specBindingUsers = describe "binding existing users to sso identities" $ do statusCode resp `shouldBe` 403 resp `shouldSatisfy` (not . checkRespBody) hasSetBindCookieHeader resp `shouldBe` Left "no set-cookie header" - responseJSON resp `shouldBe` Right (TestErrorLabel "bind-without-auth") + responseJsonEither resp `shouldBe` Right (TestErrorLabel "bind-without-auth") describe "GET /sso-initiate-bind/:idp" $ do context "known IdP, running session without authentication" $ do @@ -490,7 +490,7 @@ specBindingUsers = describe "binding existing users to sso identities" $ do specCRUDIdentityProvider :: SpecWith TestEnv specCRUDIdentityProvider = do let checkErr :: HasCallStack => (Int -> Bool) -> TestErrorLabel -> ResponseLBS -> Bool - checkErr statusIs label resp = statusIs (statusCode resp) && responseJSON resp == Right label + checkErr statusIs label resp = statusIs (statusCode resp) && responseJsonEither resp == Right label testGetPutDelete :: HasCallStack => (SparReq -> Maybe UserId -> IdPId -> Http ResponseLBS) -> SpecWith TestEnv testGetPutDelete whichone = do @@ -700,10 +700,10 @@ specCRUDIdentityProvider = do statusCode resp1 `shouldBe` 201 statusCode resp2 `shouldBe` 400 - responseJSON resp2 `shouldBe` Right (TestErrorLabel "idp-already-in-use") + responseJsonEither resp2 `shouldBe` Right (TestErrorLabel "idp-already-in-use") statusCode resp3 `shouldBe` 400 - responseJSON resp3 `shouldBe` Right (TestErrorLabel "idp-already-in-use") + responseJsonEither resp3 `shouldBe` Right (TestErrorLabel "idp-already-in-use") context "client is owner with email" $ do it "responds with 2xx; makes IdP available for GET /identity-providers/" $ do diff --git a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs index 91befbc4e7b..55c6e58ce14 100644 --- a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs @@ -225,4 +225,4 @@ checkErr status mlabel = do const status === statusCode case mlabel of Nothing -> pure () - Just label -> const (Right label) === responseJSON + Just label -> const (Right label) === responseJsonEither diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index a8bf17c8d90..ea933e282f4 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -75,7 +75,7 @@ testCreateUser = do let userid = scimUserId scimStoredUser -- Check that this user is present in Brig and that Brig's view of the user -- matches SCIM's view of the user - brigUser :: User <- fmap decodeBody' . call . get $ + brigUser :: User <- fmap responseJsonUnsafe . call . get $ ( (env ^. teBrig) . header "Z-User" (toByteString' userid) . path "/self" @@ -182,7 +182,7 @@ testLocation = do req <- parseRequest (show (Scim.unURI location)) <&> scimAuth (Just tok) . acceptScim r <- call (get (const req)) post (brg . path "/i/users" . contentJson . body p) + bdy <- responseJsonUnsafe <$> post (brg . path "/i/users" . contentJson . body p) let (uid, Just tid) = (Brig.userId bdy, Brig.userTeam bdy) (team:_) <- (^. Galley.teamListTeams) <$> getTeams uid gly () <- Control.Exception.assert {- "Team ID in registration and team table do not match" -} (tid == team ^. Galley.teamId) @@ -279,7 +275,7 @@ createTeamMember brigreq galleyreq teamid perms = do resp :: ResponseLBS <- postUser name False (Just ssoid) (Just teamid) brigreq ResponseLBS -> Either String a -decodeBody = maybe (Left "no body") (\s -> (<> (": " <> cs (show s))) `fmapL` eitherDecode' s) . responseBody - -decodeBody' :: (HasCallStack, FromJSON a) => ResponseLBS -> a -decodeBody' = either (error . show) id . decodeBody - getTeams :: (HasCallStack, MonadHttp m, MonadIO m) => UserId -> GalleyReq -> m Galley.TeamList getTeams u gly = do r <- get ( gly @@ -371,7 +361,7 @@ getTeams u gly = do . zAuthAccess u "conn" . expect2xx ) - return $ decodeBody' r + return $ responseJsonUnsafe r getTeamMembers :: HasCallStack => UserId -> TeamId -> TestSpar [UserId] getTeamMembers usr tid = do @@ -379,7 +369,7 @@ getTeamMembers usr tid = do resp <- call $ get (gly . paths ["teams", toByteString' tid, "members"] . zUser usr) (mems ^. Galley.teamMembers) promoteTeamMember :: HasCallStack => UserId -> TeamId -> UserId -> TestSpar () @@ -393,7 +383,7 @@ promoteTeamMember usr tid memid = do getSelfProfile :: (HasCallStack, MonadHttp m, MonadIO m) => BrigReq -> UserId -> m Brig.SelfProfile getSelfProfile brg usr = do rsp <- get $ brg . path "/self" . zUser usr - return $ decodeBody' rsp + return $ responseJsonUnsafe rsp zAuthAccess :: UserId -> SBS -> Request -> Request zAuthAccess u c = header "Z-Type" "access" . zUser u . zConn c @@ -421,7 +411,7 @@ createUser :: (HasCallStack, MonadCatch m, MonadIO m, MonadHttp m) => ST -> BrigReq -> m Brig.User createUser name brig_ = do r <- postUser name True Nothing Nothing brig_ ResponseLBS -> Either String a -responseJSON = fmapL show . Aeson.eitherDecode <=< maybe (Left "no body") pure . responseBody - callAuthnReq :: forall m. (HasCallStack, MonadIO m, MonadHttp m) => SparReq -> SAML.IdPId -> m (URI, SAML.AuthnRequest) callAuthnReq sparreq_ idpid = assert test_parseAuthnReqResp $ do @@ -722,7 +708,7 @@ callIdpGet :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdPId callIdpGet sparreq_ muid idpid = do resp <- callIdpGet' (sparreq_ . expect2xx) muid idpid either (liftIO . throwIO . ErrorCall . show) pure - $ responseJSON @IdP resp + $ responseJsonEither @IdP resp callIdpGet' :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdPId -> m ResponseLBS callIdpGet' sparreq_ muid idpid = do @@ -732,7 +718,7 @@ callIdpGetAll :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> m IdPLis callIdpGetAll sparreq_ muid = do resp <- callIdpGetAll' (sparreq_ . expect2xx) muid either (liftIO . throwIO . ErrorCall . show) pure - $ responseJSON resp + $ responseJsonEither resp callIdpGetAll' :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> m ResponseLBS callIdpGetAll' sparreq_ muid = do @@ -742,7 +728,7 @@ callIdpCreate :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdP callIdpCreate sparreq_ muid metadata = do resp <- callIdpCreate' (sparreq_ . expect2xx) muid metadata either (liftIO . throwIO . ErrorCall . show) pure - $ responseJSON @IdP resp + $ responseJsonEither @IdP resp callIdpCreate' :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SAML.IdPMetadata -> m ResponseLBS callIdpCreate' sparreq_ muid metadata = do @@ -756,7 +742,7 @@ callIdpCreateRaw :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SBS - callIdpCreateRaw sparreq_ muid ctyp metadata = do resp <- callIdpCreateRaw' (sparreq_ . expect2xx) muid ctyp metadata either (liftIO . throwIO . ErrorCall . show) pure - $ responseJSON @IdP resp + $ responseJsonEither @IdP resp callIdpCreateRaw' :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> SBS -> LBS -> m ResponseLBS callIdpCreateRaw' sparreq_ muid ctyp metadata = do diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index b9ce1242438..dfda329ed11 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -149,7 +149,7 @@ createUser tok user = do user (env ^. teSpar) (Request -> Request) -type ResponseLBS = Bilge.Response (Maybe LBS) - data IntegrationConfig = IntegrationConfig { cfgBrig :: Endpoint , cfgGalley :: Endpoint diff --git a/tools/stern/src/Stern/API.hs b/tools/stern/src/Stern/API.hs index 485f09a2532..e08397b9366 100644 --- a/tools/stern/src/Stern/API.hs +++ b/tools/stern/src/Stern/API.hs @@ -526,7 +526,7 @@ getSSOStatus = liftM json . Intra.getSSOStatus setSSOStatus :: JSON ::: TeamId ::: JsonRequest Bool -> Handler Response setSSOStatus (_ ::: tid ::: req) = do status <- parseBody req !>> Error status400 "client-error" - liftM json $ Intra.setLegalholdStatus tid status + liftM json $ Intra.setSSOStatus tid status getTeamBillingInfo :: TeamId -> Handler Response getTeamBillingInfo tid = do diff --git a/tools/stern/src/Stern/Intra.hs b/tools/stern/src/Stern/Intra.hs index 92145fbdd12..7c3cbc6bb33 100644 --- a/tools/stern/src/Stern/Intra.hs +++ b/tools/stern/src/Stern/Intra.hs @@ -362,7 +362,7 @@ getLegalholdStatus tid = do ) where fromResponseBody :: Response (Maybe LByteString) -> Handler Bool - fromResponseBody resp = case responseJson resp of + fromResponseBody resp = case responseJsonEither resp of Right (LegalHoldTeamConfig LegalHoldDisabled) -> pure False Right (LegalHoldTeamConfig LegalHoldEnabled) -> pure True Left errmsg -> throwE (Error status502 "bad-upstream" ("bad response; error message: " <> pack errmsg)) @@ -393,7 +393,7 @@ getSSOStatus tid = do ) where fromResponseBody :: Response (Maybe LByteString) -> Handler Bool - fromResponseBody resp = case responseJson resp of + fromResponseBody resp = case responseJsonEither resp of Right (SSOTeamConfig SSODisabled) -> pure False Right (SSOTeamConfig SSOEnabled) -> pure True Left errmsg -> throwE (Error status502 "bad-upstream" ("bad response; error message: " <> pack errmsg))