Skip to content

Commit fe64794

Browse files
Add endpoint to get User Id by qualified handle (#1281)
* Federation is not implemented yet, so remote handles will always return 404. * Include qualified user id in 'UserProfile' and 'UserHandleInfo' Also changes 'FromJSON' instances of these types as they are never required to be sent from clients. We use the FromJSON instances only in integration tests. * Rename schema for 'Qualified (Id a)' from QualifiedUserId to QualifiedId Co-authored-by: jschaul <[email protected]>
1 parent b79aacc commit fe64794

File tree

7 files changed

+129
-58
lines changed

7 files changed

+129
-58
lines changed

libs/types-common/src/Data/Handle.hs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ where
2828

2929
import Data.Aeson hiding ((<?>))
3030
import qualified Data.Attoparsec.ByteString.Char8 as Atto
31+
import Data.Bifunctor (Bifunctor (first))
3132
import qualified Data.ByteString as BS
3233
import Data.ByteString.Conversion (FromByteString (parser), ToByteString)
3334
import Data.Hashable (Hashable)
34-
import Data.Swagger (ToSchema (..))
35+
import Data.Swagger (ToParamSchema, ToSchema (..))
3536
import qualified Data.Text as Text
3637
import qualified Data.Text.Encoding as Text.E
3738
import Imports
39+
import Servant (FromHttpApiData (..))
3840
import Test.QuickCheck (Arbitrary (arbitrary), choose, elements, oneof)
3941
import Util.Attoparsec (takeUpToWhile)
4042

@@ -45,7 +47,11 @@ import Util.Attoparsec (takeUpToWhile)
4547
newtype Handle = Handle
4648
{fromHandle :: Text}
4749
deriving stock (Eq, Show, Generic)
48-
deriving newtype (ToJSON, ToByteString, Hashable, ToSchema)
50+
deriving newtype (ToJSON, ToByteString, Hashable, ToSchema, ToParamSchema)
51+
52+
instance FromHttpApiData Handle where
53+
parseUrlPiece =
54+
first Text.pack . parseHandleEither
4955

5056
instance FromByteString Handle where
5157
parser = handleParser

libs/types-common/src/Data/Qualified.hs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ module Data.Qualified
3232
renderQualifiedHandle,
3333
mkQualifiedHandle,
3434
partitionRemoteOrLocalIds,
35+
deprecatedUnqualifiedSchemaRef,
3536
)
3637
where
3738

3839
import Control.Applicative (optional)
39-
import Control.Lens ((.~), (?~))
40+
import Control.Lens (view, (.~), (?~))
4041
import Data.Aeson (FromJSON, ToJSON, withObject, withText, (.:), (.=))
4142
import qualified Data.Aeson as Aeson
4243
import qualified Data.Attoparsec.ByteString.Char8 as Atto
@@ -48,6 +49,7 @@ import Data.Id (Id (toUUID))
4849
import Data.Proxy (Proxy (..))
4950
import Data.String.Conversions (cs)
5051
import Data.Swagger
52+
import Data.Swagger.Declare (Declare)
5153
import qualified Data.Text.Encoding as Text.E
5254
import qualified Data.UUID as UUID
5355
import Imports hiding (local)
@@ -133,12 +135,19 @@ renderQualifiedId = renderQualified (cs . UUID.toString . toUUID)
133135
mkQualifiedId :: Text -> Either String (Qualified (Id a))
134136
mkQualifiedId = Atto.parseOnly (parser <* Atto.endOfInput) . Text.E.encodeUtf8
135137

138+
deprecatedUnqualifiedSchemaRef :: ToSchema a => Proxy a -> Text -> Declare (Definitions Schema) (Referenced Schema)
139+
deprecatedUnqualifiedSchemaRef p newField =
140+
Inline
141+
. (description ?~ ("Deprecated, use " <> newField))
142+
. view schema
143+
<$> declareNamedSchema p
144+
136145
instance ToSchema (Qualified (Id a)) where
137146
declareNamedSchema _ = do
138147
idSchema <- declareSchemaRef (Proxy @(Id a))
139148
domainSchema <- declareSchemaRef (Proxy @Domain)
140149
return $
141-
NamedSchema (Just "QualifiedUserId") $
150+
NamedSchema (Just "QualifiedId") $
142151
mempty
143152
& type_ ?~ SwaggerObject
144153
& properties

libs/wire-api/src/Wire/API/User.hs

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ import qualified Data.Code as Code
110110
import qualified Data.Currency as Currency
111111
import Data.Handle (Handle)
112112
import qualified Data.HashMap.Strict as HashMap
113-
import qualified Data.HashMap.Strict.InsOrd as InsHashMap
113+
import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap
114114
import Data.Id
115115
import Data.Json.Util (UTCTimeMillis, (#))
116116
import qualified Data.List as List
@@ -166,7 +166,7 @@ instance ToJSON UserIdList where
166166
-- other users. Each user also has access to their own profile in a richer format --
167167
-- 'SelfProfile'.
168168
data UserProfile = UserProfile
169-
{ profileId :: UserId,
169+
{ profileQualifiedId :: Qualified UserId,
170170
profileName :: Name,
171171
-- | DEPRECATED
172172
profilePict :: Pict,
@@ -189,6 +189,7 @@ data UserProfile = UserProfile
189189
-- mark 'deleted' as optional, but it is not a 'Maybe'
190190
instance ToSchema UserProfile where
191191
declareNamedSchema _ = do
192+
idSchema <- deprecatedUnqualifiedSchemaRef (Proxy @UserId) "qualified_id"
192193
genericSchema <-
193194
genericDeclareNamedSchema
194195
( swaggerOptions
@@ -206,6 +207,7 @@ instance ToSchema UserProfile where
206207
pure $
207208
genericSchema
208209
& over (schema . required) (List.delete "deleted")
210+
& over (schema . properties) (InsOrdHashMap.insert "id" idSchema)
209211

210212
modelUser :: Doc.Model
211213
modelUser = Doc.defineModel "User" $ do
@@ -237,25 +239,26 @@ modelUser = Doc.defineModel "User" $ do
237239

238240
instance ToJSON UserProfile where
239241
toJSON u =
240-
object $
241-
"id" .= profileId u
242-
# "name" .= profileName u
243-
# "picture" .= profilePict u
244-
# "assets" .= profileAssets u
245-
# "accent_id" .= profileAccentId u
246-
# "deleted" .= (if profileDeleted u then Just True else Nothing)
247-
# "service" .= profileService u
248-
# "handle" .= profileHandle u
249-
# "locale" .= profileLocale u
250-
# "expires_at" .= profileExpire u
251-
# "team" .= profileTeam u
252-
# "email" .= profileEmail u
253-
# []
242+
object
243+
[ "id" .= _qLocalPart (profileQualifiedId u),
244+
"qualified_id" .= profileQualifiedId u,
245+
"name" .= profileName u,
246+
"picture" .= profilePict u,
247+
"assets" .= profileAssets u,
248+
"accent_id" .= profileAccentId u,
249+
"deleted" .= (if profileDeleted u then Just True else Nothing),
250+
"service" .= profileService u,
251+
"handle" .= profileHandle u,
252+
"locale" .= profileLocale u,
253+
"expires_at" .= profileExpire u,
254+
"team" .= profileTeam u,
255+
"email" .= profileEmail u
256+
]
254257

255258
instance FromJSON UserProfile where
256259
parseJSON = withObject "UserProfile" $ \o ->
257260
UserProfile
258-
<$> o .: "id"
261+
<$> o .: "qualified_id"
259262
<*> o .: "name"
260263
<*> o .:? "picture" .!= noPict
261264
<*> o .:? "assets" .!= []
@@ -349,8 +352,8 @@ instance ToSchema User where
349352
genericSchema
350353
& over (schema . required) (List.delete "deleted")
351354
-- The UserIdentity fields need to be flat-included, not be in a sub-object
352-
& over (schema . properties) (InsHashMap.delete "identity")
353-
& over (schema . properties) (InsHashMap.union identityProperties)
355+
& over (schema . properties) (InsOrdHashMap.delete "identity")
356+
& over (schema . properties) (InsOrdHashMap.union identityProperties)
354357

355358
-- FUTUREWORK:
356359
-- disentangle json serializations for 'User', 'NewUser', 'UserIdentity', 'NewUserOrigin'.
@@ -406,7 +409,7 @@ userSSOId = ssoIdentity <=< userIdentity
406409
connectedProfile :: User -> UserProfile
407410
connectedProfile u =
408411
UserProfile
409-
{ profileId = userId u,
412+
{ profileQualifiedId = userQualifiedId u,
410413
profileHandle = userHandle u,
411414
profileName = userDisplayName u,
412415
profilePict = userPict u,
@@ -429,7 +432,7 @@ publicProfile u =
429432
-- RecordWildCards or something similar because we want changes to the public profile
430433
-- to be EXPLICIT and INTENTIONAL so we don't accidentally leak sensitive data.
431434
let UserProfile
432-
{ profileId,
435+
{ profileQualifiedId,
433436
profileHandle,
434437
profileName,
435438
profilePict,
@@ -443,7 +446,7 @@ publicProfile u =
443446
in UserProfile
444447
{ profileLocale = Nothing,
445448
profileEmail = Nothing,
446-
profileId,
449+
profileQualifiedId,
447450
profileHandle,
448451
profileName,
449452
profilePict,

libs/wire-api/src/Wire/API/User/Handle.hs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{-# LANGUAGE DerivingVia #-}
22
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3-
{-# LANGUAGE RecordWildCards #-}
3+
{-# LANGUAGE OverloadedLists #-}
44

55
-- This file is part of the Wire Server implementation.
66
--
@@ -29,17 +29,21 @@ module Wire.API.User.Handle
2929
)
3030
where
3131

32+
import Control.Lens ((.~), (?~))
3233
import Data.Aeson
3334
import Data.Id (UserId)
35+
import Data.Proxy (Proxy (..))
36+
import Data.Qualified (Qualified (..), deprecatedUnqualifiedSchemaRef)
3437
import Data.Range
38+
import Data.Swagger (NamedSchema (..), SwaggerType (..), ToSchema (..), declareSchemaRef, properties, type_)
3539
import qualified Data.Swagger.Build.Api as Doc
3640
import Imports
3741
import Wire.API.Arbitrary (Arbitrary, GenericUniform (..))
3842

3943
--------------------------------------------------------------------------------
4044
-- UserHandleInfo
4145

42-
newtype UserHandleInfo = UserHandleInfo {userHandleId :: UserId}
46+
newtype UserHandleInfo = UserHandleInfo {userHandleId :: Qualified UserId}
4347
deriving stock (Eq, Show, Generic)
4448
deriving newtype (Arbitrary)
4549

@@ -49,14 +53,30 @@ modelUserHandleInfo = Doc.defineModel "UserHandleInfo" $ do
4953
Doc.property "user" Doc.string' $
5054
Doc.description "ID of the user owning the handle"
5155

56+
instance ToSchema UserHandleInfo where
57+
declareNamedSchema _ = do
58+
qualifiedIdSchema <- declareSchemaRef (Proxy @(Qualified UserId))
59+
unqualifiedIdSchema <- deprecatedUnqualifiedSchemaRef (Proxy @UserId) "qualified_user"
60+
pure $
61+
NamedSchema
62+
(Just "UserHandleInfo")
63+
$ mempty
64+
& type_ ?~ SwaggerObject
65+
& properties
66+
.~ [ ("user", unqualifiedIdSchema),
67+
("qualified_user", qualifiedIdSchema)
68+
]
69+
5270
instance ToJSON UserHandleInfo where
5371
toJSON (UserHandleInfo u) =
5472
object
55-
["user" .= u]
73+
[ "user" .= _qLocalPart u, -- For backwards compatibility
74+
"qualified_user" .= u
75+
]
5676

5777
instance FromJSON UserHandleInfo where
5878
parseJSON = withObject "UserHandleInfo" $ \o ->
59-
UserHandleInfo <$> o .: "user"
79+
UserHandleInfo <$> o .: "qualified_user"
6080

6181
--------------------------------------------------------------------------------
6282
-- CheckHandles

libs/wire-api/test/unit/Test/Wire/API/Swagger.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ import qualified Test.Tasty as T
2424
import Test.Tasty.QuickCheck (Arbitrary, counterexample, testProperty)
2525
import Type.Reflection (typeRep)
2626
import qualified Wire.API.User as User
27+
import qualified Wire.API.User.Handle as Handle
2728

2829
tests :: T.TestTree
2930
tests =
3031
T.localOption (T.Timeout (60 * 1000000) "60s") . T.testGroup "JSON roundtrip tests" $
3132
[ testToJSON @User.UserProfile,
3233
testToJSON @User.User,
33-
testToJSON @User.SelfProfile
34+
testToJSON @User.SelfProfile,
35+
testToJSON @Handle.UserHandleInfo
3436
]
3537

3638
testToJSON :: forall a. (Arbitrary a, Typeable a, ToJSON a, ToSchema a, Show a) => T.TestTree

services/brig/src/Brig/API/Public.hs

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,41 @@ type GetSelf =
224224
:> "self"
225225
:> Get '[Servant.JSON] Public.SelfProfile
226226

227+
-- See Note [document responses]
228+
-- The responses looked like this:
229+
-- Doc.returns (Doc.ref Public.modelUserHandleInfo)
230+
-- Doc.response 200 "Handle info" Doc.end
231+
-- Doc.errorResponse handleNotFound
232+
type GetHandleInfoUnqualified =
233+
Summary "Get information on a user handle"
234+
:> ZAuthServant
235+
:> "users"
236+
:> "handles"
237+
:> Capture' '[Description "The user handle"] "handle" Handle
238+
:> Get '[Servant.JSON] Public.UserHandleInfo
239+
240+
-- See Note [document responses]
241+
-- The responses looked like this:
242+
-- Doc.returns (Doc.ref Public.modelUserHandleInfo)
243+
-- Doc.response 200 "Handle info" Doc.end
244+
-- Doc.errorResponse handleNotFound
245+
type GetHandleInfoQualified =
246+
Summary "Get information on a user handle"
247+
:> ZAuthServant
248+
:> "users"
249+
:> "handles"
250+
:> Capture "domain" Domain
251+
:> Capture' '[Description "The user handle"] "handle" Handle
252+
:> Get '[Servant.JSON] Public.UserHandleInfo
253+
227254
type OutsideWorldAPI =
228255
CheckUserExistsUnqualified
229256
:<|> CheckUserExistsQualified
230257
:<|> GetUserUnqualified
231258
:<|> GetUserQualified
232259
:<|> GetSelf
260+
:<|> GetHandleInfoUnqualified
261+
:<|> GetHandleInfoQualified
233262

234263
type SwaggerDocsAPI = "api" :> SwaggerSchemaUI "swagger-ui" "swagger.json"
235264

@@ -253,6 +282,8 @@ servantSitemap =
253282
:<|> getUserUnqualifiedH
254283
:<|> getUserH
255284
:<|> getSelf
285+
:<|> getHandleInfoUnqualifiedH
286+
:<|> getHandleInfoH
256287

257288
-- Note [ephemeral user sideeffect]
258289
-- If the user is ephemeral and expired, it will be removed upon calling
@@ -287,18 +318,7 @@ sitemap o = do
287318
Doc.errorResponse invalidHandle
288319
Doc.errorResponse handleNotFound
289320

290-
get "/users/handles/:handle" (continue getHandleInfoH) $
291-
accept "application" "json"
292-
.&. zauthUserId
293-
.&. capture "handle"
294-
document "GET" "getUserHandleInfo" $ do
295-
Doc.summary "Get information on a user handle"
296-
Doc.parameter Doc.Path "handle" Doc.bytes' $
297-
Doc.description "The user handle"
298-
Doc.returns (Doc.ref Public.modelUserHandleInfo)
299-
Doc.response 200 "Handle info" Doc.end
300-
Doc.errorResponse handleNotFound
301-
321+
-- some APIs moved to servant
302322
-- end User Handle API
303323

304324
-- If the user is ephemeral and expired, it will be removed, see 'Brig.API.User.userGC'.
@@ -1297,23 +1317,34 @@ checkHandlesH (_ ::: _ ::: req) = do
12971317
free <- lift $ API.checkHandles handles (fromRange num)
12981318
return $ json (free :: [Handle])
12991319

1300-
getHandleInfoH :: JSON ::: UserId ::: Handle -> Handler Response
1301-
getHandleInfoH (_ ::: self ::: handle) =
1302-
maybe (setStatus status404 empty) json
1303-
<$> getHandleInfo self handle
1320+
getHandleInfoUnqualifiedH :: UserId -> Handle -> Handler Public.UserHandleInfo
1321+
getHandleInfoUnqualifiedH self handle = do
1322+
domain <- viewFederationDomain
1323+
getHandleInfoH self domain handle
1324+
1325+
getHandleInfoH :: UserId -> Domain -> Handle -> Handler Public.UserHandleInfo
1326+
getHandleInfoH self domain handle =
1327+
ifNothing (notFound "handle not found")
1328+
=<< getHandleInfo self (Qualified handle domain)
13041329

13051330
-- FUTUREWORK: use 'runMaybeT' to simplify this.
1306-
getHandleInfo :: UserId -> Handle -> Handler (Maybe Public.UserHandleInfo)
1331+
getHandleInfo :: UserId -> Qualified Handle -> Handler (Maybe Public.UserHandleInfo)
13071332
getHandleInfo self handle = do
1308-
ownerProfile <- do
1309-
-- FUTUREWORK(federation, #1268): resolve qualified handles, too
1310-
domain <- viewFederationDomain
1311-
maybeOwnerId <- fmap (flip Qualified domain) <$> (lift $ API.lookupHandle handle)
1312-
case maybeOwnerId of
1313-
Just ownerId -> lift $ API.lookupProfile self ownerId
1314-
Nothing -> return Nothing
1315-
owner <- filterHandleResults self (maybeToList ownerProfile)
1316-
return $ Public.UserHandleInfo . Public.profileId <$> listToMaybe owner
1333+
domain <- viewFederationDomain
1334+
if _qDomain handle == domain
1335+
then getLocalHandleInfo domain
1336+
else getRemoteHandleInfo
1337+
where
1338+
getLocalHandleInfo domain = do
1339+
maybeOwnerId <- lift $ API.lookupHandle (_qLocalPart handle)
1340+
case maybeOwnerId of
1341+
Nothing -> return Nothing
1342+
Just ownerId -> do
1343+
ownerProfile <- lift $ API.lookupProfile self (Qualified ownerId domain)
1344+
owner <- filterHandleResults self (maybeToList ownerProfile)
1345+
return $ Public.UserHandleInfo . Public.profileQualifiedId <$> listToMaybe owner
1346+
-- FUTUREWORK: Federate with remote backends
1347+
getRemoteHandleInfo = return Nothing
13171348

13181349
changeHandleH :: UserId ::: ConnId ::: JsonRequest Public.HandleUpdate -> Handler Response
13191350
changeHandleH (u ::: conn ::: req) = do

services/brig/test/integration/API/User/Handles.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ assertCanFind brig from target = do
221221
const (userHandle target) === (>>= (listToMaybe >=> profileHandle)) . responseJsonMaybe
222222
get (brig . paths ["users", "handles", toByteString' targetHandle] . zUser (userId from)) !!! do
223223
const 200 === statusCode
224-
const (Just (UserHandleInfo $ userId target)) === responseJsonMaybe
224+
const (Just (UserHandleInfo $ userQualifiedId target)) === responseJsonMaybe
225225

226226
assertCannotFind :: (Monad m, MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) => Brig -> User -> User -> m ()
227227
assertCannotFind brig from target = do

0 commit comments

Comments
 (0)