From a7020acdffec3fac59e78466a75207e82482ec2c Mon Sep 17 00:00:00 2001 From: jschaul Date: Wed, 9 Dec 2020 11:37:16 +0100 Subject: [PATCH 01/12] drive-by fix: allow federator to locally start up by specifying config --- services/federator/federator.integration.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/federator/federator.integration.yaml b/services/federator/federator.integration.yaml index d29da8e07e5..a8c89ae814c 100644 --- a/services/federator/federator.integration.yaml +++ b/services/federator/federator.integration.yaml @@ -8,8 +8,8 @@ logNetStrings: false optSettings: # Would you like to federate with every wire-server installation ? # - # setFederationStrategy: - # allowAll: + setFederationStrategy: + allowAll: # # or only with a select set of other wire-server installations? # From 0c45ae6d04387130677d718810dea8c65d884b90 Mon Sep 17 00:00:00 2001 From: jschaul Date: Wed, 9 Dec 2020 21:18:57 +0100 Subject: [PATCH 02/12] move /self to servant --- services/brig/src/Brig/API/Public.hs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 3a0ceda2054..d9fb480c625 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -218,11 +218,18 @@ type GetUserQualified = :> CaptureUserId "uid" :> Get '[Servant.JSON] Public.UserProfile +type GetSelf = + Summary "Get your own profile" + :> ZAuthServant + :> "self" + :> Get '[Servant.JSON] Public.SelfProfile + type OutsideWorldAPI = CheckUserExistsUnqualified :<|> CheckUserExistsQualified :<|> GetUserUnqualified :<|> GetUserQualified + :<|> GetSelf type SwaggerDocsAPI = "api" :> SwaggerSchemaUI "swagger-ui" "swagger.json" @@ -245,6 +252,7 @@ servantSitemap = :<|> checkUserExistsH :<|> getUserUnqualifiedH :<|> getUserH + :<|> getSelf -- Note [ephemeral user sideeffect] -- If the user is ephemeral and expired, it will be removed upon calling @@ -395,14 +403,6 @@ sitemap o = do -- User Self API ------------------------------------------------------ - get "/self" (continue getSelfH) $ - accept "application" "json" - .&. zauthUserId - document "GET" "self" $ do - Doc.summary "Get your profile" - Doc.returns (Doc.ref Public.modelSelf) - Doc.response 200 "Self profile" Doc.end - -- This endpoint can lead to the following events being sent: -- - UserUpdated event to contacts of self put "/self" (continue updateUserH) $ @@ -1176,10 +1176,6 @@ checkUserExists :: UserId -> Qualified UserId -> Handler Bool checkUserExists self qualifiedUserId = isJust <$> getUser self qualifiedUserId -getSelfH :: JSON ::: UserId -> Handler Response -getSelfH (_ ::: self) = do - json <$> getSelf self - getSelf :: UserId -> Handler Public.SelfProfile getSelf self = do lift (API.lookupSelfProfile self) >>= ifNothing userNotFound From 8e3600f21b57a0ad6ff0aad464f952be35c84d3e Mon Sep 17 00:00:00 2001 From: jschaul Date: Wed, 9 Dec 2020 21:33:38 +0100 Subject: [PATCH 03/12] ToSchema & round-trip tests --- libs/wire-api/package.yaml | 1 + libs/wire-api/src/Wire/API/User.hs | 37 +++++++++++++++++-- libs/wire-api/src/Wire/API/User/Identity.hs | 36 +++++++++++++++++- libs/wire-api/src/Wire/API/User/Profile.hs | 1 + .../test/unit/Test/Wire/API/Swagger.hs | 5 ++- libs/wire-api/wire-api.cabal | 3 +- 6 files changed, 76 insertions(+), 7 deletions(-) diff --git a/libs/wire-api/package.yaml b/libs/wire-api/package.yaml index 54fcb65e8fe..31b34c0b480 100644 --- a/libs/wire-api/package.yaml +++ b/libs/wire-api/package.yaml @@ -35,6 +35,7 @@ library: - generic-random >=1.2 - hashable - hostname-validate + - insert-ordered-containers - iproute >=1.5 - iso3166-country-codes >=0.2 - iso639 >=0.1 diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 9b3f90eab56..c459ad531b9 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -92,7 +92,7 @@ module Wire.API.User where import Control.Error.Safe (rightMay) -import Control.Lens (over) +import Control.Lens (over, view) import Data.Aeson ( FromJSON (parseJSON), KeyValue ((.=)), @@ -111,13 +111,14 @@ import qualified Data.Code as Code import qualified Data.Currency as Currency import Data.Handle (Handle) import qualified Data.HashMap.Strict as HashMap +import qualified Data.HashMap.Strict.InsOrd as InsHashMap import Data.Id import Data.Json.Util (UTCTimeMillis, (#)) import qualified Data.List as List import Data.Misc (PlainTextPassword (..)) import Data.Proxy (Proxy (..)) import Data.Range -import Data.Swagger (ToSchema (..), genericDeclareNamedSchema, required, schema) +import Data.Swagger (ToSchema (..), genericDeclareNamedSchema, properties, required, schema) import qualified Data.Swagger.Build.Api as Doc import Data.Text.Ascii import Data.UUID (UUID, nil) @@ -271,11 +272,12 @@ instance FromJSON UserProfile where -- SelfProfile -- | A self profile. -data SelfProfile = SelfProfile +newtype SelfProfile = SelfProfile { selfUser :: User } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform SelfProfile) + deriving newtype (ToSchema) modelSelf :: Doc.Model modelSelf = Doc.defineModel "Self" $ do @@ -352,6 +354,35 @@ data User = User deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform User) +-- Cannot use deriving (ToSchema) via (CustomSwagger ...) because we need to +-- mark 'deleted' as optional, but it is not a 'Maybe' +-- and we need to manually add the identity schema fields at the top level +-- instead of nesting them under the 'identity' field. +instance ToSchema User where + declareNamedSchema _ = do + identityProperties <- view (schema . properties) <$> declareNamedSchema (Proxy @UserIdentity) + genericSchema <- + genericDeclareNamedSchema + ( swaggerOptions + @'[ FieldLabelModifier + ( StripPrefix "user", + CamelToSnake, + LabelMappings + '[ "pict" ':-> "picture", + "expire" ':-> "expires_at", + "display_name" ':-> "name" + ] + ) + ] + ) + (Proxy @User) + pure $ + genericSchema + & over (schema . required) (List.delete "deleted") + -- The UserIdentity fields need to be flat-included, not be in a sub-object + & over (schema . properties) (InsHashMap.delete "identity") + & over (schema . properties) (InsHashMap.union identityProperties) + -- FUTUREWORK: -- disentangle json serializations for 'User', 'NewUser', 'UserIdentity', 'NewUserOrigin'. instance ToJSON User where diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index 121f41de588..a8f9b3c72f2 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DerivingVia #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE StrictData #-} -- This file is part of the Wire Server implementation. @@ -46,12 +47,13 @@ module Wire.API.User.Identity where import Control.Applicative (optional) +import Control.Lens ((.~), (?~)) import Data.Aeson hiding (()) import Data.Attoparsec.Text import Data.Bifunctor (first) import Data.ByteString.Conversion import Data.Proxy (Proxy (..)) -import Data.Swagger (ToSchema (..)) +import Data.Swagger (NamedSchema (..), SwaggerType (..), ToSchema (..), declareSchemaRef, properties, type_) import qualified Data.Text as Text import Data.Text.Encoding (decodeUtf8', encodeUtf8) import Data.Time.Clock @@ -73,6 +75,21 @@ data UserIdentity deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserIdentity) +instance ToSchema UserIdentity where + declareNamedSchema _ = do + emailSchema <- declareSchemaRef (Proxy @Email) + phoneSchema <- declareSchemaRef (Proxy @Phone) + ssoSchema <- declareSchemaRef (Proxy @UserSSOId) + return $ + NamedSchema (Just "userIdentity") $ + mempty + & type_ ?~ SwaggerObject + & properties + .~ [ ("email", emailSchema), + ("phone", phoneSchema), + ("sso_id", ssoSchema) + ] + instance ToJSON UserIdentity where toJSON = \case FullIdentity em ph -> go (Just em) (Just ph) Nothing @@ -209,7 +226,7 @@ validateEmail = newtype Phone = Phone {fromPhone :: Text} deriving stock (Eq, Show, Generic) - deriving newtype (ToJSON) + deriving newtype (ToJSON, ToSchema) instance FromJSON Phone where parseJSON (String s) = case parsePhone s of @@ -266,6 +283,21 @@ data UserSSOId deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserSSOId) +instance ToSchema UserSSOId where + declareNamedSchema _ = do + tenantSchema <- declareSchemaRef (Proxy @Text) + subjectSchema <- declareSchemaRef (Proxy @Text) + scimSchema <- declareSchemaRef (Proxy @Text) + return $ + NamedSchema (Just "UserSSOId") $ + mempty + & type_ ?~ SwaggerObject + & properties + .~ [ ("tenant", tenantSchema), + ("subject", subjectSchema), + ("scim_external_id", scimSchema) + ] + instance ToJSON UserSSOId where toJSON = \case UserSSOId tenant subject -> object ["tenant" .= tenant, "subject" .= subject] diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index 5d3de8da15f..9fc77aab5ea 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -289,6 +289,7 @@ data ManagedBy ManagedByScim deriving stock (Eq, Bounded, Enum, Show, Generic) deriving (Arbitrary) via (GenericUniform ManagedBy) + deriving (ToSchema) via (CustomSwagger '[ConstructorTagModifier (StripPrefix "ManagedBy", CamelToSnake)] ManagedBy) typeManagedBy :: Doc.DataType typeManagedBy = diff --git a/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs b/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs index f72b23210cf..5d889fb2574 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Swagger.hs @@ -28,7 +28,10 @@ import qualified Wire.API.User as User tests :: T.TestTree tests = T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "JSON roundtrip tests" $ - [testToJSON @User.UserProfile] + [ testToJSON @User.UserProfile, + testToJSON @User.User, + testToJSON @User.SelfProfile + ] testToJSON :: forall a. (Arbitrary a, Typeable a, ToJSON a, ToSchema a, Show a) => T.TestTree testToJSON = testProperty msg trip diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 425fd1f576b..70313fa8b03 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: cb5d5f266e2fdd724d4f01f6136992c85aff49fa7ad01f06f8373a1f3c99abbe +-- hash: 4e9f1f53fb43a39da1366fe060fe0ef8e2b0185847abdc61552ca3262bf25322 name: wire-api version: 0.1.0 @@ -97,6 +97,7 @@ library , hashable , hostname-validate , imports + , insert-ordered-containers , iproute >=1.5 , iso3166-country-codes >=0.2 , iso639 >=0.1 From ceb4aaccf7b36d28045725f0d28281b7c5f1a0cc Mon Sep 17 00:00:00 2001 From: jschaul Date: Thu, 10 Dec 2020 21:05:23 +0100 Subject: [PATCH 04/12] add qualified_user field to /self (WIP) --- libs/types-common/src/Data/Qualified.hs | 26 +++++++++++++++++---- libs/wire-api/src/Wire/API/User.hs | 4 ++++ services/brig/src/Brig/API/Public.hs | 10 ++++----- services/brig/src/Brig/API/User.hs | 2 +- services/brig/src/Brig/API/Util.hs | 12 +--------- services/brig/src/Brig/App.hs | 9 ++++++++ services/brig/src/Brig/Data/User.hs | 30 ++++++++++++++++--------- services/brig/src/Brig/Provider/API.hs | 6 +++-- 8 files changed, 66 insertions(+), 33 deletions(-) diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index d21a0110bc5..f765770f966 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE StrictData #-} -- This file is part of the Wire Server implementation. @@ -35,15 +36,18 @@ module Data.Qualified where import Control.Applicative (optional) -import Data.Aeson (FromJSON, ToJSON, withText) +import Control.Lens ((.~), (?~)) +import Data.Aeson (FromJSON, ToJSON, withObject, withText, (.:), (.=)) import qualified Data.Aeson as Aeson import qualified Data.Attoparsec.ByteString.Char8 as Atto import Data.Bifunctor (first) import Data.ByteString.Conversion (FromByteString (parser)) import Data.Domain (Domain, domainText) import Data.Handle (Handle (..)) -import Data.Id (Id (toUUID)) +import Data.Id (Id (toUUID), UserId) +import Data.Proxy (Proxy (..)) import Data.String.Conversions (cs) +import Data.Swagger import qualified Data.Text.Encoding as Text.E import qualified Data.UUID as UUID import Imports hiding (local) @@ -129,11 +133,25 @@ renderQualifiedId = renderQualified (cs . UUID.toString . toUUID) mkQualifiedId :: Text -> Either String (Qualified (Id a)) mkQualifiedId = Atto.parseOnly (parser <* Atto.endOfInput) . Text.E.encodeUtf8 +instance ToSchema (Qualified UserId) where + declareNamedSchema _ = do + idSchema <- declareSchemaRef (Proxy @UserId) + domainSchema <- declareSchemaRef (Proxy @Domain) + return $ + NamedSchema (Just "QualifiedUserId") $ + mempty + & type_ ?~ SwaggerObject + & properties + .~ [ ("id", idSchema), + ("domain", domainSchema) + ] + instance ToJSON (Qualified (Id a)) where - toJSON = Aeson.String . renderQualifiedId + toJSON qu = Aeson.object ["id" .= _qLocalPart qu, "domain" .= _qDomain qu] instance FromJSON (Qualified (Id a)) where - parseJSON = withText "QualifiedUserId" $ either fail pure . mkQualifiedId + parseJSON = withObject "QualifiedUserId" $ \o -> + Qualified <$> o .: "id" <*> o .: "domain" instance FromHttpApiData (Qualified (Id a)) where parseUrlPiece = first cs . mkQualifiedId diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index c459ad531b9..5ec5c978e04 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -132,6 +132,7 @@ import Wire.API.User.Activation (ActivationCode) import Wire.API.User.Auth (CookieLabel) import Wire.API.User.Identity import Wire.API.User.Profile +import Data.Qualified -------------------------------------------------------------------------------- -- UserIdList @@ -326,6 +327,7 @@ instance FromJSON SelfProfile where -- | The data of an existing user. data User = User { userId :: UserId, + userQualifiedUser :: Qualified UserId, -- | User identity. For endpoints like @/self@, it will be present in the response iff -- the user is activated, and the email/phone contained in it will be guaranteedly -- verified. {#RefActivation} @@ -389,6 +391,7 @@ instance ToJSON User where toJSON u = object $ "id" .= userId u + # "qualified_user" .= userQualifiedUser u # "name" .= userDisplayName u # "picture" .= userPict u # "assets" .= userAssets u @@ -410,6 +413,7 @@ instance FromJSON User where ssoid <- o .:? "sso_id" User <$> o .: "id" + <*> o .: "qualified_user" <*> parseIdentity ssoid o <*> o .: "name" <*> o .:? "picture" .!= noPict diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index d9fb480c625..0f3e7685f0e 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1162,7 +1162,7 @@ createUser (Public.NewUserPublic new) = do checkUserExistsUnqualifiedH :: UserId -> UserId -> Handler (Union CheckUserExistsResponse) checkUserExistsUnqualifiedH self uid = do - domain <- API.viewFederationDomain + domain <- viewFederationDomain checkUserExistsH self domain uid checkUserExistsH :: UserId -> Domain -> UserId -> Handler (Union CheckUserExistsResponse) @@ -1182,7 +1182,7 @@ getSelf self = do getUserUnqualifiedH :: UserId -> UserId -> Handler Public.UserProfile getUserUnqualifiedH self uid = do - domain <- API.viewFederationDomain + domain <- viewFederationDomain getUserH self domain uid getUserH :: UserId -> Domain -> UserId -> Handler Public.UserProfile @@ -1215,7 +1215,7 @@ listUsersH (_ ::: self ::: qry) = listUsers :: UserId -> Either (List UserId) (Range 1 4 (List Handle)) -> Handler [Public.UserProfile] listUsers self = \case Left us -> do - domain <- API.viewFederationDomain + domain <- viewFederationDomain byIds $ map (`Qualified` domain) (fromList us) Right hs -> do us <- getIds (fromList $ fromRange hs) @@ -1225,7 +1225,7 @@ listUsers self = \case getIds localHandles = do localUsers <- catMaybes <$> traverse (lift . API.lookupHandle) localHandles -- FUTUREWORK(federation, #1268): resolve qualified handles, too - domain <- API.viewFederationDomain + domain <- viewFederationDomain pure $ map (`Qualified` domain) localUsers byIds :: [Qualified UserId] -> Handler [Public.UserProfile] byIds uids = @@ -1307,7 +1307,7 @@ getHandleInfo :: UserId -> Handle -> Handler (Maybe Public.UserHandleInfo) getHandleInfo self handle = do ownerProfile <- do -- FUTUREWORK(federation, #1268): resolve qualified handles, too - domain <- API.viewFederationDomain + domain <- viewFederationDomain maybeOwnerId <- fmap (flip Qualified domain) <$> (lift $ API.lookupHandle handle) case maybeOwnerId of Just ownerId -> lift $ API.lookupProfile self ownerId diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 8d8b98f395e..842fc14bc54 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -88,7 +88,7 @@ where import qualified Brig.API.Error as Error import qualified Brig.API.Handler as API (Handler) import Brig.API.Types -import Brig.API.Util (fetchUserIdentity, validateHandle, viewFederationDomain) +import Brig.API.Util (fetchUserIdentity, validateHandle) import Brig.App import qualified Brig.Code as Code import Brig.Data.Activation (ActivationEvent (..)) diff --git a/services/brig/src/Brig/API/Util.hs b/services/brig/src/Brig/API/Util.hs index 95544f4a75d..7ed69980210 100644 --- a/services/brig/src/Brig/API/Util.hs +++ b/services/brig/src/Brig/API/Util.hs @@ -20,22 +20,18 @@ module Brig.API.Util lookupProfilesMaybeFilterSameTeamOnly, lookupSelfProfile, validateHandle, - viewFederationDomain, ) where import qualified Brig.API.Error as Error import Brig.API.Handler import Brig.API.Types -import Brig.App (AppIO, Env, settings) +import Brig.App (AppIO) import qualified Brig.Data.User as Data -import Brig.Options (federationDomain) import Brig.Types import Brig.Types.Intra (accountUser) -import Control.Lens (view) import Control.Monad.Catch (throwM) import Control.Monad.Trans.Except (throwE) -import Data.Domain (Domain) import Data.Handle (Handle, parseHandle) import Data.Id import Data.Maybe @@ -63,9 +59,3 @@ lookupSelfProfile = fmap (fmap mk) . Data.lookupAccount validateHandle :: Text -> Handler Handle validateHandle = maybe (throwE (Error.StdError Error.invalidHandle)) return . parseHandle - --------------------------------------------------------------------------------- --- Federation - -viewFederationDomain :: MonadReader Env m => m (Domain) -viewFederationDomain = view (settings . federationDomain) diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index c808312cba3..ef763a3f60d 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -62,6 +62,8 @@ module Brig.App runAppResourceT, forkAppIO, locationOf, + + viewFederationDomain ) where @@ -128,6 +130,7 @@ import qualified System.Logger.Class as LC import qualified System.Logger.Extended as Log import Util.Options import Wire.API.User.Identity (Email) +import Data.Domain schemaVersion :: Int32 schemaVersion = 61 @@ -519,3 +522,9 @@ readTurnList = Text.readFile >=> return . fn . mapMaybe fromByteString . fmap Te where fn [] = Nothing fn (x : xs) = Just (list1 x xs) + +-------------------------------------------------------------------------------- +-- Federation + +viewFederationDomain :: MonadReader Env m => m (Domain) +viewFederationDomain = view (settings . Opt.federationDomain) diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 95d20b7a2e1..26613a0348e 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -67,7 +67,7 @@ module Brig.Data.User ) where -import Brig.App (AppIO, currentTime, settings, zauthEnv) +import Brig.App (AppIO, currentTime, settings, viewFederationDomain, zauthEnv) import Brig.Data.Instances () import Brig.Options import Brig.Password @@ -78,10 +78,12 @@ import Cassandra import Control.Error import Control.Lens hiding (from) import Data.Conduit (ConduitM) +import Data.Domain import Data.Handle (Handle) import Data.Id import Data.Json.Util (UTCTimeMillis, toUTCTimeMillis) import Data.Misc (PlainTextPassword (..)) +import Data.Qualified import Data.Range (fromRange) import Data.Time (addUTCTime) import Data.UUID.V4 @@ -105,6 +107,7 @@ data ReAuthError newAccount :: NewUser -> Maybe InvitationId -> Maybe TeamId -> AppIO (UserAccount, Maybe Password) newAccount u inv tid = do defLoc <- setDefaultLocale <$> view settings + domain <- viewFederationDomain uid <- Id <$> do case (inv, newUserUUID u) of @@ -121,7 +124,7 @@ newAccount u inv tid = do now <- liftIO =<< view currentTime return . Just . toUTCTimeMillis $ addUTCTime (fromIntegral ttl) now _ -> return Nothing - return (UserAccount (user uid (locale defLoc) expiry) status, passwd) + return (UserAccount (user uid domain (locale defLoc) expiry) status, passwd) where ident = newUserIdentity u pass = newUserPassword u @@ -135,16 +138,18 @@ newAccount u inv tid = do colour = fromMaybe defaultAccentId (newUserAccentId u) locale defLoc = fromMaybe defLoc (newUserLocale u) managedBy = fromMaybe defaultManagedBy (newUserManagedBy u) - user uid l e = User uid ident name pict assets colour False l Nothing Nothing e tid managedBy + user uid domain l e = User uid (Qualified uid domain) ident name pict assets colour False l Nothing Nothing e tid managedBy newAccountInviteViaScim :: UserId -> TeamId -> Maybe Locale -> Name -> Email -> AppIO UserAccount newAccountInviteViaScim uid tid locale name email = do defLoc <- setDefaultLocale <$> view settings - return (UserAccount (user (fromMaybe defLoc locale)) PendingInvitation) + domain <- viewFederationDomain + return (UserAccount (user domain (fromMaybe defLoc locale)) PendingInvitation) where - user loc = + user domain loc = User uid + (Qualified uid domain) (Just $ EmailIdentity email) name (Pict []) @@ -386,7 +391,8 @@ lookupAuth u = fmap f <$> retry x1 (query1 authSelect (params Quorum (Identity u lookupUsers :: HavePendingInvitations -> [UserId] -> AppIO [User] lookupUsers hpi usrs = do loc <- setDefaultLocale <$> view settings - toUsers loc hpi <$> retry x1 (query usersSelect (params Quorum (Identity usrs))) + domain <- viewFederationDomain + toUsers domain loc hpi <$> retry x1 (query usersSelect (params Quorum (Identity usrs))) lookupAccount :: UserId -> AppIO (Maybe UserAccount) lookupAccount u = listToMaybe <$> lookupAccounts [u] @@ -394,7 +400,8 @@ lookupAccount u = listToMaybe <$> lookupAccounts [u] lookupAccounts :: [UserId] -> AppIO [UserAccount] lookupAccounts usrs = do loc <- setDefaultLocale <$> view settings - fmap (toUserAccount loc) <$> retry x1 (query accountsSelect (params Quorum (Identity usrs))) + domain <- viewFederationDomain + fmap (toUserAccount domain loc) <$> retry x1 (query accountsSelect (params Quorum (Identity usrs))) lookupServiceUser :: ProviderId -> ServiceId -> BotId -> AppIO (Maybe (ConvId, Maybe TeamId)) lookupServiceUser pid sid bid = retry x1 (query1 cql (params Quorum (pid, sid, bid))) @@ -605,8 +612,9 @@ userRichInfoUpdate = "UPDATE rich_info SET json = ? WHERE user = ?" -- Conversions -- | Construct a 'UserAccount' from a raw user record in the database. -toUserAccount :: Locale -> AccountRow -> UserAccount +toUserAccount :: Domain -> Locale -> AccountRow -> UserAccount toUserAccount + domain defaultLocale ( uid, name, @@ -635,6 +643,7 @@ toUserAccount in UserAccount ( User uid + (Qualified uid domain) ident name (fromMaybe noPict pict) @@ -650,8 +659,8 @@ toUserAccount ) (fromMaybe Active status) -toUsers :: Locale -> HavePendingInvitations -> [UserRow] -> [User] -toUsers defaultLocale havePendingInvitations = fmap mk . filter fp +toUsers :: Domain -> Locale -> HavePendingInvitations -> [UserRow] -> [User] +toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp where fp :: UserRow -> Bool fp = @@ -707,6 +716,7 @@ toUsers defaultLocale havePendingInvitations = fmap mk . filter fp svc = newServiceRef <$> sid <*> pid in User uid + (Qualified uid domain) ident name (fromMaybe noPict pict) diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index f11d71080c8..3f70ffeae01 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -29,7 +29,7 @@ import qualified Brig.API.Client as Client import Brig.API.Error import Brig.API.Handler import Brig.API.Types (PasswordResetError (..)) -import Brig.App (AppIO, internalEvents, settings) +import Brig.App (AppIO, internalEvents, settings, viewFederationDomain) import qualified Brig.Code as Code import qualified Brig.Data.Client as User import qualified Brig.Data.User as User @@ -66,6 +66,7 @@ import Data.List1 (maybeList1) import qualified Data.Map.Strict as Map import Data.Misc (Fingerprint (..), Rsa) import Data.Predicate +import Data.Qualified import Data.Range import qualified Data.Set as Set import qualified Data.Swagger.Build.Api as Doc @@ -829,6 +830,7 @@ addBot zuid zcon cid add = do throwStd serviceNotWhitelisted -- Prepare a user ID, client ID and token for the bot. bid <- BotId <$> randomId + domain <- viewFederationDomain btk <- Text.decodeLatin1 . toByteString' <$> ZAuth.newBotToken pid bid cid let bcl = newClientId (fromIntegral (hash bid)) -- Ask the external service to create a bot @@ -846,7 +848,7 @@ addBot zuid zcon cid add = do let colour = fromMaybe defaultAccentId (Ext.rsNewBotColour rs) let pict = Pict [] -- Legacy let sref = newServiceRef sid pid - let usr = User (botUserId bid) Nothing name pict assets colour False locale (Just sref) Nothing Nothing Nothing ManagedByWire + let usr = User (botUserId bid) (Qualified (botUserId bid) domain) Nothing name pict assets colour False locale (Just sref) Nothing Nothing Nothing ManagedByWire let newClt = (newClient PermanentClientType (Ext.rsNewBotLastPrekey rs)) { newClientPrekeys = Ext.rsNewBotPrekeys rs From ed21f4daa7620736d17975c33763642e2a8ead54 Mon Sep 17 00:00:00 2001 From: jschaul Date: Mon, 14 Dec 2020 15:04:28 +0100 Subject: [PATCH 05/12] Fix api-bot compilation and introduce new cli argument --- libs/api-bot/src/Network/Wire/Bot/Cache.hs | 15 +++++++++------ libs/api-bot/src/Network/Wire/Bot/Monad.hs | 3 ++- libs/api-bot/src/Network/Wire/Bot/Settings.hs | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/libs/api-bot/src/Network/Wire/Bot/Cache.hs b/libs/api-bot/src/Network/Wire/Bot/Cache.hs index 94fe9c7b9ab..cafb3dac3f3 100644 --- a/libs/api-bot/src/Network/Wire/Bot/Cache.hs +++ b/libs/api-bot/src/Network/Wire/Bot/Cache.hs @@ -28,8 +28,10 @@ module Network.Wire.Bot.Cache where import Data.ByteString.Conversion +import Data.Domain import Data.LanguageCodes import Data.Misc +import Data.Qualified import Data.Text.Encoding import qualified Data.Text.Lazy as Text import qualified Data.Text.Lazy.IO as Text @@ -51,11 +53,11 @@ data CachedUser = CachedUser !PlainTextPassword !User -- user2's UUID,email2,password2 -- ... -- @ -fromFile :: Logger -> GenIO -> FilePath -> IO Cache -fromFile logger gen path = do +fromFile :: Logger -> GenIO -> Domain -> FilePath -> IO Cache +fromFile logger gen domain path = do triples <- map (Text.splitOn ",") . Text.lines <$> Text.readFile path shuffled <- V.toList <$> uniformShuffle (V.fromList triples) gen - c <- newIORef =<< foldM (toUser logger) [] shuffled + c <- newIORef =<< foldM (toUser logger domain) [] shuffled return (Cache c) empty :: IO Cache @@ -73,8 +75,8 @@ get c = liftIO . atomicModifyIORef (cache c) $ \u -> put :: MonadIO m => Cache -> CachedUser -> m () put c a = liftIO . atomicModifyIORef (cache c) $ \u -> (a : u, ()) -toUser :: HasCallStack => Logger -> [CachedUser] -> [LText] -> IO [CachedUser] -toUser _ acc [i, e, p] = do +toUser :: HasCallStack => Logger -> Domain -> [CachedUser] -> [LText] -> IO [CachedUser] +toUser _ domain acc [i, e, p] = do let pw = PlainTextPassword . Text.toStrict $ Text.strip p let iu = error "Cache.toUser: invalid user" let ie = error "Cache.toUser: invalid email" @@ -85,6 +87,7 @@ toUser _ acc [i, e, p] = do pw User { userId = ui, + userQualifiedUser = Qualified ui domain, userDisplayName = Name $ "Fakebot-" <> Text.toStrict (Text.strip i), userPict = Pict [], userAssets = [], @@ -98,6 +101,6 @@ toUser _ acc [i, e, p] = do userTeam = Nothing, userManagedBy = ManagedByWire } -toUser g acc entry = do +toUser g _ acc entry = do warn g $ msg (val "invalid entry: " +++ show entry) return acc diff --git a/libs/api-bot/src/Network/Wire/Bot/Monad.hs b/libs/api-bot/src/Network/Wire/Bot/Monad.hs index d7fd11ea459..db2ff93f34e 100644 --- a/libs/api-bot/src/Network/Wire/Bot/Monad.hs +++ b/libs/api-bot/src/Network/Wire/Bot/Monad.hs @@ -149,7 +149,8 @@ data BotNetEnv = BotNetEnv newBotNetEnv :: Manager -> Logger -> BotNetSettings -> IO BotNetEnv newBotNetEnv manager logger o = do gen <- MWC.createSystemRandom - usr <- maybe Cache.empty (Cache.fromFile logger gen) (setBotNetUsersFile o) + let domain = setBotNetFederationDomain o + usr <- maybe Cache.empty (Cache.fromFile logger gen domain) (setBotNetUsersFile o) mbx <- maybe (return []) loadMailboxConfig (setBotNetMailboxConfig o) met <- initMetrics let srv = diff --git a/libs/api-bot/src/Network/Wire/Bot/Settings.hs b/libs/api-bot/src/Network/Wire/Bot/Settings.hs index ccdb7b4ead6..9bdaa85cf2d 100644 --- a/libs/api-bot/src/Network/Wire/Bot/Settings.hs +++ b/libs/api-bot/src/Network/Wire/Bot/Settings.hs @@ -34,8 +34,11 @@ module Network.Wire.Bot.Settings ) where +import qualified Data.Attoparsec.ByteString as A import Data.ByteString.Char8 (pack) +import Data.Domain import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text import Data.Time.Clock (NominalDiffTime) import Imports import Network.Wire.Client.API.User (Email (..), parseEmail) @@ -54,6 +57,7 @@ data BotNetSettings = BotNetSettings setBotNetMailboxConfig :: !(Maybe FilePath), setBotNetSender :: !Email, setBotNetUsersFile :: !(Maybe FilePath), + setBotNetFederationDomain :: !Domain, setBotNetReportDir :: !(Maybe FilePath), setBotNetBotSettings :: !BotSettings, setBotNetMailboxFolders :: ![String] @@ -72,6 +76,7 @@ botNetSettingsParser = <*> mailboxConfigOption <*> mailSenderOption <*> usersFileOption + <*> usersFederationDomain <*> reportDirOption <*> botSettingsParser <*> mailboxFoldersOption @@ -142,6 +147,14 @@ usersFileOption = \ containing a list of ALREADY EXISTING users with the columns: \ \ User-Id,Email,Password" +usersFederationDomain :: Parser (Domain) +usersFederationDomain = + domainOption $ + long "users-federation-domain" + <> metavar "DOMAIN" + <> help + "federationDomain of all users from the usersFile CSV" + reportDirOption :: Parser (Maybe FilePath) reportDirOption = optional . strOption $ @@ -276,3 +289,9 @@ emailOption = maybe (Left "Invalid email") Right . parseEmail . Text.pack + +attoReadM :: A.Parser a -> ReadM a +attoReadM p = eitherReader (A.parseOnly p . Text.encodeUtf8 . Text.pack) + +domainOption :: Mod OptionFields Domain -> Parser Domain +domainOption = option $ attoReadM domainParser From 68a442460334083a8b4511c826ae9286ceaa6b5d Mon Sep 17 00:00:00 2001 From: jschaul Date: Mon, 14 Dec 2020 15:15:41 +0100 Subject: [PATCH 06/12] qualified_user -> qualified_id --- libs/wire-api/src/Wire/API/User.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 5ec5c978e04..8289a4b63b1 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -117,6 +117,7 @@ import Data.Json.Util (UTCTimeMillis, (#)) import qualified Data.List as List import Data.Misc (PlainTextPassword (..)) import Data.Proxy (Proxy (..)) +import Data.Qualified import Data.Range import Data.Swagger (ToSchema (..), genericDeclareNamedSchema, properties, required, schema) import qualified Data.Swagger.Build.Api as Doc @@ -132,7 +133,6 @@ import Wire.API.User.Activation (ActivationCode) import Wire.API.User.Auth (CookieLabel) import Wire.API.User.Identity import Wire.API.User.Profile -import Data.Qualified -------------------------------------------------------------------------------- -- UserIdList @@ -327,7 +327,7 @@ instance FromJSON SelfProfile where -- | The data of an existing user. data User = User { userId :: UserId, - userQualifiedUser :: Qualified UserId, + userQualifiedId :: Qualified UserId, -- | User identity. For endpoints like @/self@, it will be present in the response iff -- the user is activated, and the email/phone contained in it will be guaranteedly -- verified. {#RefActivation} @@ -391,7 +391,7 @@ instance ToJSON User where toJSON u = object $ "id" .= userId u - # "qualified_user" .= userQualifiedUser u + # "qualified_id" .= userQualifiedId u # "name" .= userDisplayName u # "picture" .= userPict u # "assets" .= userAssets u @@ -413,7 +413,7 @@ instance FromJSON User where ssoid <- o .:? "sso_id" User <$> o .: "id" - <*> o .: "qualified_user" + <*> o .: "qualified_id" <*> parseIdentity ssoid o <*> o .: "name" <*> o .:? "picture" .!= noPict From d042ab53425129acf60cf466e1f9a3a1ba66519e Mon Sep 17 00:00:00 2001 From: jschaul Date: Mon, 14 Dec 2020 15:16:55 +0100 Subject: [PATCH 07/12] ormolu --- services/brig/src/Brig/App.hs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index ef763a3f60d..025bd189a97 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -62,8 +62,7 @@ module Brig.App runAppResourceT, forkAppIO, locationOf, - - viewFederationDomain + viewFederationDomain, ) where @@ -97,6 +96,7 @@ import Control.Monad.Catch (MonadCatch, MonadMask) import Control.Monad.Trans.Resource import Data.ByteString.Conversion import Data.Default (def) +import Data.Domain import qualified Data.GeoIP2 as GeoIp import Data.IP import Data.Id (UserId) @@ -130,7 +130,6 @@ import qualified System.Logger.Class as LC import qualified System.Logger.Extended as Log import Util.Options import Wire.API.User.Identity (Email) -import Data.Domain schemaVersion :: Int32 schemaVersion = 61 From 28cf1df886d9557a1b98043acf2925df67d77107 Mon Sep 17 00:00:00 2001 From: jschaul Date: Mon, 14 Dec 2020 15:39:52 +0100 Subject: [PATCH 08/12] check for qualified_id in integration testing --- services/brig/test/integration/API/User/Account.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 80b5599f510..0e318ee1dfb 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -49,6 +49,7 @@ import Data.Json.Util (fromUTCTimeMillis) import Data.List1 (singleton) import qualified Data.List1 as List1 import Data.Misc (PlainTextPassword (..)) +import Data.Qualified import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.Encoding as T @@ -141,9 +142,12 @@ testCreateUserWithPreverified opts brig aws = do else do usr <- postUserRegister reg brig let uid = userId usr + let domain = Opt.setFederationDomain $ Opt.optSettings opts get (brig . path "/self" . zUser uid) !!! do const 200 === statusCode const (Just p) === (userPhone <=< responseJsonMaybe) + -- check /self returns the qualified_id field in the response + const (Just (Qualified uid domain)) === (fmap userQualifiedId . responseJsonMaybe) liftIO $ Util.assertUserJournalQueue "user activate" aws (userActivateJournaled usr) -- Register (pre verified) user with email e <- randomEmail From 417e1a029d9379c61aae990c87d285f74b8fb166 Mon Sep 17 00:00:00 2001 From: jschaul Date: Mon, 14 Dec 2020 15:56:18 +0100 Subject: [PATCH 09/12] cleanup --- libs/api-bot/src/Network/Wire/Bot/Cache.hs | 2 +- libs/wire-api/src/Wire/API/Swagger.hs | 1 - libs/wire-api/src/Wire/API/User.hs | 47 ++++------------------ 3 files changed, 8 insertions(+), 42 deletions(-) diff --git a/libs/api-bot/src/Network/Wire/Bot/Cache.hs b/libs/api-bot/src/Network/Wire/Bot/Cache.hs index cafb3dac3f3..214735df44a 100644 --- a/libs/api-bot/src/Network/Wire/Bot/Cache.hs +++ b/libs/api-bot/src/Network/Wire/Bot/Cache.hs @@ -87,7 +87,7 @@ toUser _ domain acc [i, e, p] = do pw User { userId = ui, - userQualifiedUser = Qualified ui domain, + userQualifiedId = Qualified ui domain, userDisplayName = Name $ "Fakebot-" <> Text.toStrict (Text.strip i), userPict = Pict [], userAssets = [], diff --git a/libs/wire-api/src/Wire/API/Swagger.hs b/libs/wire-api/src/Wire/API/Swagger.hs index 0484c0eb614..c2c3326e429 100644 --- a/libs/wire-api/src/Wire/API/Swagger.hs +++ b/libs/wire-api/src/Wire/API/Swagger.hs @@ -137,7 +137,6 @@ models = Team.Permission.modelPermissions, Team.SearchVisibility.modelTeamSearchVisibility, User.modelUserIdList, - User.modelSelf, User.modelUser, User.modelNewUser, User.modelUserUpdate, diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 8289a4b63b1..94c199eedad 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -76,17 +76,16 @@ module Wire.API.User module Wire.API.User.Profile, -- * Swagger - modelUserIdList, - modelSelf, - modelUser, - modelNewUser, - modelUserUpdate, - modelChangePassword, + modelChangeHandle, modelChangeLocale, + modelChangePassword, + modelDelete, modelEmailUpdate, + modelNewUser, modelPhoneUpdate, - modelChangeHandle, - modelDelete, + modelUser, + modelUserIdList, + modelUserUpdate, modelVerifyDelete, ) where @@ -280,38 +279,6 @@ newtype SelfProfile = SelfProfile deriving (Arbitrary) via (GenericUniform SelfProfile) deriving newtype (ToSchema) -modelSelf :: Doc.Model -modelSelf = Doc.defineModel "Self" $ do - Doc.description "Self Profile" - Doc.property "id" Doc.bytes' $ - Doc.description "User ID" - Doc.property "name" Doc.string' $ - Doc.description "Name" - Doc.property "assets" (Doc.array (Doc.ref modelAsset)) $ - Doc.description "Profile assets" - Doc.property "email" Doc.string' $ do - Doc.description "Email address" - Doc.optional - Doc.property "phone" Doc.string' $ do - Doc.description "E.164 Phone number" - Doc.optional - Doc.property "accent_id" Doc.int32' $ do - Doc.description "Accent colour ID" - Doc.optional - Doc.property "locale" Doc.string' $ - Doc.description "Locale in format." - Doc.property "handle" Doc.string' $ do - Doc.description "Unique handle." - Doc.optional - Doc.property "deleted" Doc.bool' $ do - Doc.description "Whether the account has been deleted." - Doc.optional - Doc.property "managed_by" typeManagedBy $ do - Doc.description - "What is the source of truth for this user; if it's SCIM \ - \then the profile can't be edited via normal means" - Doc.optional - instance ToJSON SelfProfile where toJSON (SelfProfile u) = toJSON u From 8dd4be632769e52361c963a6a92b591b3026bcee Mon Sep 17 00:00:00 2001 From: jschaul Date: Mon, 14 Dec 2020 16:31:52 +0100 Subject: [PATCH 10/12] minor improvement to ToSchema of Qualified --- libs/types-common/src/Data/Qualified.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/types-common/src/Data/Qualified.hs b/libs/types-common/src/Data/Qualified.hs index f765770f966..f015365170e 100644 --- a/libs/types-common/src/Data/Qualified.hs +++ b/libs/types-common/src/Data/Qualified.hs @@ -44,7 +44,7 @@ import Data.Bifunctor (first) import Data.ByteString.Conversion (FromByteString (parser)) import Data.Domain (Domain, domainText) import Data.Handle (Handle (..)) -import Data.Id (Id (toUUID), UserId) +import Data.Id (Id (toUUID)) import Data.Proxy (Proxy (..)) import Data.String.Conversions (cs) import Data.Swagger @@ -133,9 +133,9 @@ renderQualifiedId = renderQualified (cs . UUID.toString . toUUID) mkQualifiedId :: Text -> Either String (Qualified (Id a)) mkQualifiedId = Atto.parseOnly (parser <* Atto.endOfInput) . Text.E.encodeUtf8 -instance ToSchema (Qualified UserId) where +instance ToSchema (Qualified (Id a)) where declareNamedSchema _ = do - idSchema <- declareSchemaRef (Proxy @UserId) + idSchema <- declareSchemaRef (Proxy @(Id a)) domainSchema <- declareSchemaRef (Proxy @Domain) return $ NamedSchema (Just "QualifiedUserId") $ From 1a151f0ea4fe468f132822c10d30b44933f92c3f Mon Sep 17 00:00:00 2001 From: jschaul Date: Mon, 14 Dec 2020 16:57:16 +0100 Subject: [PATCH 11/12] comments --- libs/wire-api/src/Wire/API/User/Identity.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index a8f9b3c72f2..7967fa9e478 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -283,6 +283,9 @@ data UserSSOId deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserSSOId) +-- FUTUREWORK: This schema should ideally be a choice of either tenant+subject, or scim_external_id +-- but this is currently not possible to derive in swagger2 +-- Maybe this becomes possible with swagger 3? instance ToSchema UserSSOId where declareNamedSchema _ = do tenantSchema <- declareSchemaRef (Proxy @Text) From 670a59815f16ed3cc5090790bccba8476ec15173 Mon Sep 17 00:00:00 2001 From: jschaul Date: Mon, 14 Dec 2020 20:32:07 +0100 Subject: [PATCH 12/12] fix handle test: incorrect /users?handles= parsing" The json from /users?handles=... is a 'UserProfile' object, yet it was incorrectly parsed implictly as a 'User' object with the use of 'userHandle' function. This test faiure occurred due to the addition of the now mandatory qualified_id field in the mostly-internal 'User' object (which is shown only to /self) --- services/brig/test/integration/API/User/Handles.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/brig/test/integration/API/User/Handles.hs b/services/brig/test/integration/API/User/Handles.hs index dc4f9d68f56..df5ea0ff2ee 100644 --- a/services/brig/test/integration/API/User/Handles.hs +++ b/services/brig/test/integration/API/User/Handles.hs @@ -156,7 +156,7 @@ testHandleQuery opts 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)) === (userHandle <=< listToMaybe <=< responseJsonMaybe) + const (Just (Handle hdl)) === (profileHandle <=< listToMaybe <=< responseJsonMaybe) -- Bulk availability check hdl2 <- randomHandle hdl3 <- randomHandle