Skip to content

Commit b51531c

Browse files
eyeinskyakshaymankarbattermann
authored
WPB-20214: Add member searchability (#4786)
* Remove references to non-existent note Note [ephemeral user sideeffect] was removed in commit: b0934a3 WPB-15801 GET and DELETE Registered Domains (#4438) * Make the `Wire.API.Team.Member.userId` lens more general * Minor refactor - make `!!!` definition easier to understand - extract `getProfile` * Add `searchable` field to data types * Add Elastic Search boolean field type * Add `POST /users/:uid/searchable` * Add Elastic Search indexing * Filter by searchable in Elastic Search * Filter by `searchable` in exact handle search * Test searchable field and contact search * Use common CQL splice for team member queries * Test /team/:tid/members?searchable=false * Add query param to Brig * Update services/brig/src/Brig/Provider/API.hs Co-authored-by: Akshay Mankar <[email protected]> * Update libs/wire-subsystems/src/Wire/UserSubsystem/Interpreter.hs Co-authored-by: Akshay Mankar <[email protected]> * Move test from brig to integration package * Partially revert "Minor refactor": inline getProfile back again This reverts commit 68b6c6e. * Minor refactor: use record syntax, deduplicate golden tests * Update libs/wire-api/src/Wire/API/Routes/Public/Brig.hs Co-authored-by: Leif Battermann <[email protected]> * Create all test users' presence in /teams/:tid/search * Add changelog entry * Wrap searchable POST body to JSON object * fix templates * Add seacrhable to golden tests * Implement searchable flag to MockInterpreters * second attempt at correct /team/:tid/search * Add test to check that legacy users are found --------- Co-authored-by: Akshay Mankar <[email protected]> Co-authored-by: Leif Battermann <[email protected]>
1 parent 778a1a9 commit b51531c

File tree

88 files changed

+851
-820
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+851
-820
lines changed

changelog.d/2-features/WPB-20214

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add `searchable` field to users, users who have it set to `false` won't be found by the public endpoint.
2+
Add `POST /users/:uid/searchable` endpoint where team admin can change it for user.
3+
Add `/teams/:tid/search?searchable=false`, where the query parameter makes it return only non-searchable users.

integration/test/API/Brig.hs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,11 @@ searchTeamWithSearchTerm user q = searchTeam user [("q", q)]
262262
searchTeamAll :: (HasCallStack, MakesValue user) => user -> App Response
263263
searchTeamAll user = searchTeam user [("q", ""), ("size", "100"), ("sortby", "created_at"), ("sortorder", "desc")]
264264

265+
setUserSearchable :: (MakesValue user) => user -> String -> Bool -> App Response
266+
setUserSearchable self uid searchable = do
267+
req <- baseRequest self Brig Versioned $ joinHttpPath ["users", uid, "searchable"]
268+
submit "POST" $ addJSONObject ["set_searchable" .= searchable] req
269+
265270
getAPIVersion :: (HasCallStack, MakesValue domain) => domain -> App Response
266271
getAPIVersion domain = do
267272
req <- baseRequest domain Brig Unversioned $ "/api-version"
@@ -1207,8 +1212,14 @@ refreshAppCookie u tid appId = do
12071212
req <- baseRequest u Brig Versioned $ joinHttpPath ["teams", tid, "apps", appId, "cookies"]
12081213
submit "POST" req
12091214

1210-
removeTeamCollaborator :: (MakesValue owner, MakesValue collaborator, HasCallStack) => owner -> String -> collaborator -> App Response
1211-
removeTeamCollaborator owner tid collaborator = do
1212-
(_, collabId) <- objQid collaborator
1213-
req <- baseRequest owner Galley Versioned $ joinHttpPath ["teams", tid, "collaborators", collabId]
1214-
submit "DELETE" req
1215+
-- | https://staging-nginz-https.zinfra.io/v12/api/swagger-ui/#/default/check-user-handle
1216+
checkHandle :: (MakesValue user) => user -> String -> App Response
1217+
checkHandle self handle = do
1218+
req <- baseRequest self Brig Versioned (joinHttpPath ["handles", handle])
1219+
submit "HEAD" req
1220+
1221+
-- | https://staging-nginz-https.zinfra.io/v12/api/swagger-ui/#/default/check-user-handles
1222+
checkHandles :: (MakesValue user) => user -> [String] -> App Response
1223+
checkHandles self handles = do
1224+
req <- baseRequest self Brig Versioned (joinHttpPath ["handles"]) <&> addJSONObject ["handles" .= handles]
1225+
submit "POST" req

integration/test/API/Galley.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,3 +867,9 @@ resetConversation user groupId epoch = do
867867
req <- baseRequest user Galley Versioned (joinHttpPath ["mls", "reset-conversation"])
868868
let payload = object ["group_id" .= groupId, "epoch" .= epoch]
869869
submit "POST" $ req & addJSON payload
870+
871+
removeTeamCollaborator :: (MakesValue owner, MakesValue collaborator, HasCallStack) => owner -> String -> collaborator -> App Response
872+
removeTeamCollaborator owner tid collaborator = do
873+
(_, collabId) <- objQid collaborator
874+
req <- baseRequest owner Galley Versioned $ joinHttpPath ["teams", tid, "collaborators", collabId]
875+
submit "DELETE" req

integration/test/Test/Search.hs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import qualified API.Common as API
88
import API.Galley
99
import qualified API.Galley as Galley
1010
import qualified API.GalleyInternal as GalleyI
11+
import qualified Data.Set as Set
1112
import GHC.Stack
1213
import SetupHelpers
1314
import Testlib.Assertions
@@ -358,3 +359,124 @@ testTeamSearchUserIncludesUserGroups = do
358359
let expectedUgs = fromMaybe [] (lookup uid expected)
359360
actualUgs <- for ugs asString
360361
actualUgs `shouldMatchSet` expectedUgs
362+
363+
testUserSearchable :: App ()
364+
testUserSearchable = do
365+
-- Create team and all users who are part of this test
366+
(owner, tid, [u1, u2, u3]) <- createTeam OwnDomain 4
367+
admin <- createTeamMember owner def {role = "admin"}
368+
let everyone = [owner, u1, admin, u2, u3]
369+
everyone'sUidSet <- Set.fromList <$> mapM objId everyone
370+
371+
-- All users are searchable by default
372+
assertBool "created users are searchable by default" . and =<< mapM (\u -> u %. "searchable" & asBool) everyone
373+
374+
-- Setting self to non-searchable won't work -- only admin can do it.
375+
u1id <- u1 %. "id" & asString
376+
BrigP.setUserSearchable u1 u1id False `bindResponse` \resp -> do
377+
resp.status `shouldMatchInt` 403
378+
resp.json %. "label" `shouldMatch` "insufficient-permissions"
379+
380+
BrigP.getUser u1 u1 `bindResponse` \resp -> do
381+
resp.status `shouldMatchInt` 200
382+
(resp.json %. "searchable") `shouldMatch` True
383+
384+
-- Team admin can set user to non-searchable.
385+
BrigP.setUserSearchable admin u1id False `bindResponse` \resp -> resp.status `shouldMatchInt` 200
386+
BrigP.getUser u1 u1 `bindResponse` \resp -> do
387+
resp.status `shouldMatchInt` 200
388+
(resp.json %. "searchable") `shouldMatch` False
389+
390+
-- Team owner can, too.
391+
BrigP.setUserSearchable owner u1id True `bindResponse` \resp -> resp.status `shouldMatchInt` 200
392+
BrigP.setUserSearchable owner u1id False `bindResponse` \resp -> resp.status `shouldMatchInt` 200
393+
394+
-- By default created team members are found.
395+
u2id <- u2 %. "id" & asString
396+
BrigI.refreshIndex OwnDomain
397+
withFoundDocs u1 (u2 %. "name") $ \docs -> do
398+
foundUids <- for docs objId
399+
assertBool "u1 must find u2 as they are searchable by default" $ u2id `elem` foundUids
400+
401+
-- User set to non-searchable is not found by other team members.
402+
u3id <- u3 %. "id" & asString
403+
BrigP.setUserSearchable owner u3id False `bindResponse` \resp -> resp.status `shouldMatchInt` 200
404+
BrigI.refreshIndex OwnDomain
405+
withFoundDocs u1 (u3 %. "name") $ \docs -> do
406+
foundUids <- for docs objId
407+
assertBool "u1 must not find u3 as they are set non-searchable" $ notElem u3id foundUids
408+
409+
-- Even admin nor owner won't find non-searchable users via /search/contacts
410+
withFoundDocs admin (u3 %. "name") $ \docs -> do
411+
foundUids <- for docs objId
412+
assertBool "Team admin won't find non-searchable user" $ notElem u3id foundUids
413+
withFoundDocs owner (u3 %. "name") $ \docs -> do
414+
foundUids <- for docs objId
415+
assertBool "Team owner won't find non-searchable user from /search/contacts" $ notElem u3id foundUids
416+
417+
-- Check for handle being available with HTTP HEAD still shows that the handle used by non-searchable users is not available
418+
u3handle <- API.randomHandle
419+
BrigP.putHandle u3 u3handle `bindResponse` assertSuccess
420+
BrigP.checkHandle u2 u3handle `bindResponse` \resp -> resp.status `shouldMatchInt` 200 -- (200 means "handle is taken", 404 would be "not found")
421+
422+
-- Handle for POST /handles still works for non-searchable users
423+
u2handle <- API.randomHandle
424+
BrigP.putHandle u2 u2handle `bindResponse` assertSuccess
425+
BrigP.checkHandles u1 [u3handle, u2handle] `bindResponse` \resp -> do
426+
resp.status `shouldMatchInt` 200
427+
freeHandles <- resp.json & asList
428+
assertBool "POST /handles filters all taken handles, even for regular members" $ null freeHandles
429+
430+
-- Regular user can't find non-searchable team member by exact handle.
431+
withFoundDocs u1 u3handle $ \docs -> do
432+
foundUids <- for docs objId
433+
assertBool "u1 must not find non-searchable u3 by exact handle" $ notElem u3id foundUids
434+
435+
-- /teams/:tid/members gets all members, both searchable and non-searchable
436+
getTeamMembers u1 tid `bindResponse` \resp -> do
437+
resp.status `shouldMatchInt` 200
438+
docs <- resp.json %. "members" >>= asList
439+
foundUids <- mapM (\m -> m %. "user" & asString) docs
440+
assertBool "/teams/:tid/members returns all users in team"
441+
$ Set.fromList foundUids
442+
== everyone'sUidSet
443+
444+
-- /teams/:tid/search also returns all users from team
445+
BrigP.searchTeam admin [] `bindResponse` \resp -> do
446+
resp.status `shouldMatchInt` 200
447+
docs <- resp.json %. "documents" >>= asList
448+
foundUids <- mapM (\m -> m %. "id" & asString) docs
449+
assertBool "/teams/:tid/search returns all users in team" $ Set.fromList foundUids == everyone'sUidSet
450+
451+
-- /teams/:tid/search?searchable=false gets only non-searchable members
452+
BrigP.searchTeam admin [("searchable", "false")] `bindResponse` \resp -> do
453+
resp.status `shouldMatchInt` 200
454+
docs <- resp.json %. "documents" >>= asList
455+
foundUids <- mapM (\m -> m %. "id" & asString) docs
456+
assertBool "/teams/:tid/members?searchable=false returns only non-searchable members"
457+
$ Set.fromList foundUids
458+
== Set.fromList [u1id, u3id]
459+
460+
-- /teams/:tid/search?searchable=true gets only searchable users
461+
BrigP.searchTeam admin [("searchable", "true")] `bindResponse` \resp -> do
462+
resp.status `shouldMatchInt` 200
463+
docs <- resp.json %. "documents" >>= asList
464+
foundUids <- mapM (\m -> m %. "id" & asString) docs
465+
ownerUid <- owner %. "id" & asString
466+
adminUid <- admin %. "id" & asString
467+
assertBool "/teams/:tid/search?searchable=true gets only searchable users"
468+
$ Set.fromList foundUids
469+
== Set.fromList [ownerUid, adminUid, u2id]
470+
where
471+
-- Convenience wrapper around search contacts which applies `f` directly to document list.
472+
withFoundDocs ::
473+
(MakesValue user, MakesValue searchTerm) =>
474+
user ->
475+
searchTerm ->
476+
([Value] -> App a) ->
477+
App a
478+
withFoundDocs self term f = do
479+
BrigP.searchContacts self term OwnDomain `bindResponse` \resp -> do
480+
resp.status `shouldMatchInt` 200
481+
docs <- resp.json %. "documents" >>= asList
482+
f docs

libs/bilge/src/Bilge/Assert.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ io <!! aa = do
106106
m (Response (Maybe Lazy.ByteString)) ->
107107
Assertions () ->
108108
m ()
109-
(!!!) io = void . (<!!) io
109+
io !!! aa = void (io <!! aa)
110110

111111
infix 4 ===
112112

libs/wire-api/src/Wire/API/Routes/Public/Brig.hs

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ instance AsUnion DeleteSelfResponses (Maybe Timeout) where
161161
type ConnectionUpdateResponses = UpdateResponses "Connection unchanged" "Connection updated" UserConnection
162162

163163
type UserAPI =
164-
-- See Note [ephemeral user sideeffect]
165164
Named
166165
"get-user-unqualified"
167166
( Summary "Get a user by UserId"
@@ -171,16 +170,14 @@ type UserAPI =
171170
:> CaptureUserId "uid"
172171
:> GetUserVerb
173172
)
174-
:<|>
175-
-- See Note [ephemeral user sideeffect]
176-
Named
177-
"get-user-qualified"
178-
( Summary "Get a user by Domain and UserId"
179-
:> ZLocalUser
180-
:> "users"
181-
:> QualifiedCaptureUserId "uid"
182-
:> GetUserVerb
183-
)
173+
:<|> Named
174+
"get-user-qualified"
175+
( Summary "Get a user by Domain and UserId"
176+
:> ZLocalUser
177+
:> "users"
178+
:> QualifiedCaptureUserId "uid"
179+
:> GetUserVerb
180+
)
184181
:<|> Named
185182
"update-user-email"
186183
( Summary "Resend email address validation email."
@@ -224,19 +221,17 @@ type UserAPI =
224221
]
225222
(Maybe UserProfile)
226223
)
227-
:<|>
228-
-- See Note [ephemeral user sideeffect]
229-
Named
230-
"list-users-by-unqualified-ids-or-handles"
231-
( Summary "List users (deprecated)"
232-
:> Until 'V2
233-
:> Description "The 'ids' and 'handles' parameters are mutually exclusive."
234-
:> ZUser
235-
:> "users"
236-
:> QueryParam' [Optional, Strict, Description "User IDs of users to fetch"] "ids" (CommaSeparatedList UserId)
237-
:> QueryParam' [Optional, Strict, Description "Handles of users to fetch, min 1 and max 4 (the check for handles is rather expensive)"] "handles" (Range 1 4 (CommaSeparatedList Handle))
238-
:> Get '[JSON] [UserProfile]
239-
)
224+
:<|> Named
225+
"list-users-by-unqualified-ids-or-handles"
226+
( Summary "List users (deprecated)"
227+
:> Until 'V2
228+
:> Description "The 'ids' and 'handles' parameters are mutually exclusive."
229+
:> ZUser
230+
:> "users"
231+
:> QueryParam' [Optional, Strict, Description "User IDs of users to fetch"] "ids" (CommaSeparatedList UserId)
232+
:> QueryParam' [Optional, Strict, Description "Handles of users to fetch, min 1 and max 4 (the check for handles is rather expensive)"] "handles" (Range 1 4 (CommaSeparatedList Handle))
233+
:> Get '[JSON] [UserProfile]
234+
)
240235
:<|> Named
241236
"list-users-by-ids-or-handles"
242237
( Summary "List users"
@@ -247,18 +242,16 @@ type UserAPI =
247242
:> ReqBody '[JSON] ListUsersQuery
248243
:> Post '[JSON] ListUsersById
249244
)
250-
:<|>
251-
-- See Note [ephemeral user sideeffect]
252-
Named
253-
"list-users-by-ids-or-handles@V3"
254-
( Summary "List users"
255-
:> Description "The 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive."
256-
:> ZUser
257-
:> Until 'V4
258-
:> "list-users"
259-
:> ReqBody '[JSON] ListUsersQuery
260-
:> Post '[JSON] [UserProfile]
261-
)
245+
:<|> Named
246+
"list-users-by-ids-or-handles@V3"
247+
( Summary "List users"
248+
:> Description "The 'qualified_ids' and 'qualified_handles' parameters are mutually exclusive."
249+
:> ZUser
250+
:> Until 'V4
251+
:> "list-users"
252+
:> ReqBody '[JSON] ListUsersQuery
253+
:> Post '[JSON] [UserProfile]
254+
)
262255
:<|> Named
263256
"send-verification-code"
264257
( Summary "Send a verification code to a given email address."
@@ -294,6 +287,17 @@ type UserAPI =
294287
'[JSON]
295288
(Respond 200 "Protocols supported by the user" (Set BaseProtocolTag))
296289
)
290+
:<|> Named
291+
"set-user-searchable"
292+
( Summary "Set user's visibility in search"
293+
:> From 'V12
294+
:> ZLocalUser
295+
:> "users"
296+
:> CaptureUserId "uid"
297+
:> "searchable"
298+
:> ReqBody '[JSON] SetSearchable
299+
:> Post '[JSON] ()
300+
)
297301

298302
type LastSeenNameDesc = Description "`name` of the last seen user group, used to get the next page when sorting by name."
299303

@@ -1736,6 +1740,13 @@ type SearchAPI =
17361740
]
17371741
"email"
17381742
EmailVerificationFilter
1743+
:> QueryParam'
1744+
[ Optional,
1745+
Strict,
1746+
Description "Optional, return only non-searchable members when false."
1747+
]
1748+
"searchable"
1749+
Bool
17391750
:> MultiVerb
17401751
'GET
17411752
'[JSON]

libs/wire-api/src/Wire/API/Team/Member.hs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ data HiddenPerm
483483
| CreateApp
484484
| ManageApps
485485
| RemoveTeamCollaborator
486+
| SetMemberSearchable
486487
deriving (Eq, Ord, Show)
487488

488489
-- | See Note [hidden team roles]
@@ -570,7 +571,8 @@ roleHiddenPermissions role = HiddenPermissions p p
570571
NewTeamCollaborator,
571572
CreateApp,
572573
ManageApps,
573-
RemoveTeamCollaborator
574+
RemoveTeamCollaborator,
575+
SetMemberSearchable
574576
]
575577
roleHiddenPerms RoleMember =
576578
(roleHiddenPerms RoleExternalPartner <>) $
@@ -654,7 +656,7 @@ makeLenses ''TeamMemberList'
654656
makeLenses ''NewTeamMember'
655657
makeLenses ''TeamMemberDeleteData
656658

657-
userId :: Lens' TeamMember UserId
659+
userId :: Lens' (TeamMember' tag) UserId
658660
userId = newTeamMember . nUserId
659661

660662
permissions :: Lens (TeamMember' tag1) (TeamMember' tag2) (PermissionType tag1) (PermissionType tag2)

libs/wire-api/src/Wire/API/Team/Permission.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ serviceWhitelistPermissions =
128128
-- Perm
129129

130130
-- | Team-level permission. Analog to conversation-level 'Action'.
131+
--
132+
-- If you ever think about adding a new permission flag, read Note
133+
-- [team roles] first.
131134
data Perm
132135
= CreateConversation
133136
| -- NOTE: This may get overruled by conv level checks in case those are more restrictive
@@ -153,8 +156,6 @@ data Perm
153156
| DeleteTeam
154157
-- FUTUREWORK: make the verbs in the roles more consistent
155158
-- (CRUD vs. Add,Remove vs; Get,Set vs. Create,Delete etc).
156-
-- If you ever think about adding a new permission flag,
157-
-- read Note [team roles] first.
158159
deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
159160
deriving (Arbitrary) via (GenericUniform Perm)
160161
deriving (FromJSON, ToJSON) via (CustomEncoded Perm)

0 commit comments

Comments
 (0)