Skip to content

Conversation

eyeinsky
Copy link
Collaborator

@eyeinsky eyeinsky commented Sep 23, 2025

Task checklist:

  • add field searchable
  • default value is True
  • must only be changed from the new /users/:uid/searchable API endpoint and by team admin, and nowhere else
  • add permission SetMemberSearchable
  • must be exposed from user profile
  • must be indexed by ES
  • /search/contacts must filter based on searchable = True
  • /teams/:tid/search must expose a filter to find non-searchable users; otherwise ignore it
  • exact handle search via public endpoint must return 404
  • calling HEAD should ignore the field: used to check whether handle is taken
  • add documentation of searchable field behavior
  • adds a test to check the above
  • exact handle search via internal endpoint should ignore this property
    • if no such endpoint exists, it should be created

Open questions

  • is the changed admin toggle API type ok? The task designates this as POST /users/:uid/searchable, but as TeamId is required, then it currently is POST /users/:uid/:tid/searchable -- could this be improved?
  • POST /handles: should this endpoint also filter based on searchability?
  • are there no other locations than /search/contacts to take searchability into account?
  • should the test be moved into the integration package? (@battermann mentioned this -- are we moving tests there and hence, should this test also go there as well?)

Checklist

  • Add a new entry in an appropriate subdirectory of changelog.d
  • Read and follow the PR guidelines

@zebot zebot added the ok-to-test Approved for running tests in CI, overrides not-ok-to-test if both labels exist label Sep 23, 2025
@eyeinsky eyeinsky force-pushed the ml/WPB-20214-user-searchable branch 5 times, most recently from bc31287 to 78ad65e Compare September 25, 2025 08:44
@eyeinsky
Copy link
Collaborator Author

eyeinsky commented Sep 25, 2025

The below is now resolved.


The current error when running the test is this:

[[email protected]] E, IO Exception occurred, message=cql-io: protocol error: parse error: response body reading: Failed reading: column count: 24 =/= 26 Empty call stack , request=37d3c1fa-f1ee-4050-b623-e4c9c79e5584
[[email protected]] E, request=37d3c1fa-f1ee-4050-b623-e4c9c79e5584, code=500, label=server-error, "Server Error"
brig-integration: Assertions failed:
 1: 201 =/= 500

Response was:

Response {responseStatus = Status {statusCode = 500, statusMessage = "Internal Server Error"}, responseVersion = HTTP/1.1, responseHeaders = [("Transfer-Encoding","chunked"),("Date","Thu, 25 Sep 2025 09:00:01 GMT"),("Server","Warp/3.4.2"),("traceparent","00-96836cd3b1eedf0553338dde6b2d4b0c-1ced0252881c377c-01"),("tracestate",""),("Content-Encoding","gzip"),("Content-Type","application/json"),("Vary","Accept-Encoding")], responseBody = Just "{\"code\":500,\"label\":\"server-error\",\"message\":\"Internal Server Error\"}", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose, responseOriginalRequest = Request {
  host                 = "127.0.0.1"
  port                 = 8082
  secure               = False
  requestHeaders       = [("Content-Type","application/json")]
  path                 = "/i/users"
  queryString          = ""
  method               = "POST"
  proxy                = Nothing
  rawBody              = False
  redirectCount        = 10
  responseTimeout      = ResponseTimeoutDefault
  requestVersion       = HTTP/1.1
  proxySecureMode      = ProxySecureWithConnect
}
, responseEarlyHints = []}
CallStack (from HasCallStack):
  error, called at src/Bilge/Assert.hs:91:5 in bilge-0.22.0-inplace:Bilge.Assert
  <!!, called at test/integration/API/Team/Util.hs:140:9 in brig-2.0-inplace-brig-integration:API.Team.Util
  createUserWithTeam', called at test/integration/API/Team/Util.hs:89:21 in brig-2.0-inplace-brig-integration:API.Team.Util
  createPopulatedBindingTeamWithNames, called at test/integration/API/Team/Util.hs:68:25 in brig-2.0-inplace-brig-integration:API.Team.Util
  createPopulatedBindingTeamWithNamesAndHandles, called at test/integration/API/Search.hs:165:38 in brig-2.0-inplace-brig-integration:API.Search

From running this command: make c package=all && ./hack/bin/cabal-run-integration.sh brig -p '/testUserSearchable/'

@eyeinsky eyeinsky force-pushed the ml/WPB-20214-user-searchable branch 3 times, most recently from 9891c56 to 6a1a1b8 Compare October 3, 2025 07:56
@eyeinsky eyeinsky marked this pull request as ready for review October 3, 2025 08:17
@eyeinsky eyeinsky requested a review from a team as a code owner October 3, 2025 08:17
@eyeinsky eyeinsky force-pushed the ml/WPB-20214-user-searchable branch from 6a1a1b8 to 4790b05 Compare October 6, 2025 09:59
@eyeinsky
Copy link
Collaborator Author

eyeinsky commented Oct 6, 2025

@akshaymankar Noting here what you asked about in the standup: the /search/contacts endpoint fetches the results from ES which is populated from Cassandra's user table. This has the searchable field, so filtering there is easy. The /team/:tid/members?searchable=false on the other hand gets its results from Cassandra's team_member table, which doesn't have a searchable field.. But since I need to return all non-searchable users from this table, then I think the current (pre-postgres) way to do it would be to get all rows from team_member and then filter that down to only non-searchable users by the help of users table. With large table this might costly to do. 🤔

When we had all data in postgres though, the query could entirely be done on the database side: join team_member with user, then filter by searchable.

@akshaymankar
Copy link
Member

@eyeinsky I think we shouldn't support this query param on /teams/:tid/members endpoint, but rather on /teams/:tid/search endpoint. I just saw the ticket and it seems like I made a mistake in the name of this endpoint. Sorry about that 😞

@eyeinsky eyeinsky force-pushed the ml/WPB-20214-user-searchable branch from 4790b05 to b0f782c Compare October 7, 2025 09:47
@eyeinsky
Copy link
Collaborator Author

eyeinsky commented Oct 7, 2025

@akshaymankar I'm ready for another round of review!

@akshaymankar
Copy link
Member

is the changed admin toggle API type ok? The task designates this as POST /users/:uid/searchable, but as TeamId is required, then it currently is POST /users/:uid/:tid/searchable -- could this be improved?

I think we can just lookup the team id of the targetted user and check whether the user who made the request is an admin of that team. We shouldn't need team id in the path.

POST /handles: should this endpoint also filter based on searchability?

No, we can expose the fact that his handle is taken as far as we don't tell who has this handle.

are there no other locations than /search/contacts to take searchability into account?

Yeah, this is the only location.

should the test be moved into the integration package? (@battermann mentioned this -- are we moving tests there and hence, should this test also go there as well?)

Ideally, yes. Sometimes we've been lazy, but the agreed upon rule was to move a test if you need to work on it and not to add more tests in the legacy test suites.

Comment on lines 293 to 300
:<|> Named
"set-user-searchable"
( Summary "Set user's visibility in search"
:> From 'V12
:> ZLocalUser
:> "users"
:> CaptureUserId "uid"
:> Capture "tid" TeamId
:> ReqBody '[JSON] Bool
:> "searchable"
:> Post '[JSON] ()
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in earlier comment, we shouldn't need the TeamId in the path.

| SetMemberPermissions
| GetTeamConversations
| DeleteTeam
| SetMemberSearchable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this go to the HiddenPerm type?

Comment on lines 50 to 51
teamContactEmailUnvalidated = Nothing
teamContactEmailUnvalidated = Nothing,
teamContactSearchable = Nothing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should reduce these tests and perhaps add a test where this value is not Nothing.

teamContactSso :: Maybe Sso,
teamContactEmailUnvalidated :: Maybe EmailAddress
teamContactEmailUnvalidated :: Maybe EmailAddress,
teamContactSearchable :: Maybe Bool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a Maybe?

let domain = Domain "example.com"
let colour = ColourId 0
let userProfile = UserProfile (Qualified uid domain) (Name "name") Nothing (Pict []) [] colour False Nothing Nothing Nothing Nothing Nothing UserLegalHoldNoConsent defSupportedProtocols UserTypeRegular
let userProfile = UserProfile (Qualified uid domain) (Name "name") Nothing (Pict []) [] colour False Nothing Nothing Nothing Nothing Nothing UserLegalHoldNoConsent defSupportedProtocols UserTypeRegular True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm refactoring these to record syntax whenever I come across them. Its a bit painful, but IMO its necessary to do this as we go.

test p "get /users/<localdomain>/:uid - 404" $ testNonExistingUser b,
test p "get /users/:domain/:uid - 422" $ testUserInvalidDomain b,
test p "get /users/:uid - 200" $ testExistingUserUnqualified b,
test p "testUserSearchable" $ testUserSearchable b g,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should write this test in the new test suite.

],
ES.boolQueryShouldMatch = [ES.QueryExistsQuery (ES.FieldName "handle")]
ES.boolQueryShouldMatch = [ES.QueryExistsQuery (ES.FieldName "handle")],
ES.boolQueryMustNotMatch = [ES.TermQuery (ES.Term "searchable" "false") Nothing]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it should work with old users who don't have anything set in searchable, just to be sure did you test it?
If it does work we should write a comment explaining this double negation (mustNot and false)

@eyeinsky eyeinsky force-pushed the ml/WPB-20214-user-searchable branch from 7901c95 to d791f07 Compare October 8, 2025 09:02
@eyeinsky eyeinsky force-pushed the ml/WPB-20214-user-searchable branch from 99bfb21 to 671e93a Compare October 9, 2025 20:20
@eyeinsky eyeinsky force-pushed the ml/WPB-20214-user-searchable branch from c0e5357 to cfc7888 Compare October 10, 2025 09:27
Copy link
Contributor

@battermann battermann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a release note explaining how the search index mapping has to be updated and how to avoid downtime, etc.

And there are some minor comments, looks good overall.

Comment on lines +356 to +359
let -- Helper to change user searchability.
setSearchable self uid searchable = do
req <- baseRequest self Brig Versioned $ joinHttpPath ["users", uid, "searchable"]
submit "POST" $ addJSON searchable req
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should go into API.Brig

Comment on lines +371 to +372
u1' <- BrigP.getUser u1 u1 >>= getJSON 200
assertBool "Searchable is still True" =<< (u1' %. "searchable" & asBool)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If u1' is not used later I would prefer to do the assertion in a bindResponse body, to reduce number of ticked variable names. Also you could assert like this: u1 %. "searchable" shouldMatch True (I think) which I find more readable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above applies to the following lines, too.

:> QueryParam'
[ Optional,
Strict,
Description "Optional, return only non-seacrhable members when false."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will it return only searchable members when true?

Comment on lines +291 to +299
randomUserPrefix ::
(MonadCatch m, MonadIO m, MonadHttp m, HasCallStack) =>
Text ->
Brig ->
m User
randomUserPrefix prefix brig = do
n <- fromName <$> randomName
createUser' True (prefix <> n) brig

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems unused, no?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ok-to-test Approved for running tests in CI, overrides not-ok-to-test if both labels exist
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants