Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display a warning when an unverified user's identity changes #28211

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from

Conversation

uhoreg
Copy link
Member

@uhoreg uhoreg commented Oct 16, 2024

Fixes element-hq/element-meta#2513

image

Checklist

  • Tests written for new code (and old code if feasible).
  • New or updated public/exported symbols have accurate TSDoc documentation.
  • Linter and other CI checks pass.
  • I have licensed the changes to Element by completing the Contributor License Agreement (CLA)

@uhoreg uhoreg requested review from a team and richvdh and removed request for a team October 16, 2024 21:54
@CLAassistant
Copy link

CLAassistant commented Oct 16, 2024

CLA assistant check
All committers have signed the CLA.

Copy link
Member

@florianduros florianduros left a comment

Choose a reason for hiding this comment

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

In a first time, we should move UserIdentifyWarning into a functional component and break down its behaviour into multiple hooks. Let me know if you need help to do it

There are differences with the figma design too.

res/css/views/rooms/_UserIdentityWarning.pcss Outdated Show resolved Hide resolved
res/css/views/rooms/_UserIdentityWarning.pcss Outdated Show resolved Hide resolved
res/css/views/rooms/_UserIdentityWarning.pcss Outdated Show resolved Hide resolved
res/css/views/rooms/_UserIdentityWarning.pcss Show resolved Hide resolved
res/css/views/rooms/_UserIdentityWarning.pcss Outdated Show resolved Hide resolved
res/css/views/rooms/_UserIdentityWarning.pcss Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
@uhoreg
Copy link
Member Author

uhoreg commented Oct 23, 2024

I think that I've addressed all of @florianduros 's concerns. The test failure looks like possible flakiness, since it's testing components that I haven't touched.

Copy link
Member

@florianduros florianduros left a comment

Choose a reason for hiding this comment

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

You are in the right direction! Now, we can refactor this hook into multiple custom hooks seperate by logic concern et will be ready to go.

About the test failure, yes it's a flackiness which has been adresse in another PR

src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
Copy link
Member

@florianduros florianduros left a comment

Choose a reason for hiding this comment

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

It's looking better! Now we can move theses hooks and their logic into separate custom hooks in order to encapsulate their behaviour and make the component easier to maintain.

In test:

  • use jest.spyOn in test to handle the mocking of the client or the crypto api.
  • use https://testing-library.com/docs/queries/about/ to get the dom element. We want to favour the role query in order to make it sure that we are also accessible
  • In case of async behaviour when we need to wait for a rerender, waitFor is doing for us the retry/waiting.

@uhoreg
Copy link
Member Author

uhoreg commented Oct 31, 2024

@florianduros ping

Copy link
Member

@florianduros florianduros left a comment

Choose a reason for hiding this comment

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

Sorry for the delay.
I think it's in a good state to be merged.
One comment about room.hasEncryptionStateEvent.

src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
@uhoreg
Copy link
Member Author

uhoreg commented Nov 7, 2024

I've addressed all of Florian's concerns. @richvdh can you give this a crypto team review?

@richvdh richvdh self-requested a review November 7, 2024 22:09
Copy link
Member

@richvdh richvdh left a comment

Choose a reason for hiding this comment

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

Looks mostly ok to me; a couple of bugs though.

It would be good to get at least one playwright test of this behaviour. Could be a followup PR though.

src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved

if (
event.getContent().membership === KnownMembership.Join ||
(event.getContent().membership === KnownMembership.Join && room.shouldEncryptForInvitedMembers())
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
(event.getContent().membership === KnownMembership.Join && room.shouldEncryptForInvitedMembers())
(event.getContent().membership === KnownMembership.Invite && room.shouldEncryptForInvitedMembers())

I'd feel happier if we had a unit test that picked this up...

src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
@uhoreg
Copy link
Member Author

uhoreg commented Nov 9, 2024

I managed to find another race condition while writing a test, so I need to investigate and fix it.

src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
) {
// Someone's membership changed and we will now encrypt to them. If
// their identity needs approval, show a warning.
await addMemberIfNeedsApproval(userId);
Copy link
Member

Choose a reason for hiding this comment

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

Why do we catch on the loadMembers() call at line 241, but not here? Any errors thrown here will get lost.

Comment on lines 256 to 267
if (
event.getContent().membership === KnownMembership.Join ||
(event.getContent().membership === KnownMembership.Invite && room.shouldEncryptForInvitedMembers())
) {
// Someone's membership changed and we will now encrypt to them. If
// their identity needs approval, show a warning.
await addMemberIfNeedsApproval(userId);
} else {
// Someone's membership changed and we no longer encrypt to them.
// If we're showing a warning about them, we don't need to any more.
removeMemberNeedingApproval(userId);
}
Copy link
Member

Choose a reason for hiding this comment

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

Suppose we see a member join and leave in quick succession (potentially, in the same /sync response)

The join will trigger a call to addMemberIfNeedsApproval, which will start a call to userNeedsApproval.
The leave will run removeMemberNeedingApproval, which won't do much.
userNeedsApproval will complete, and the user may be added to the list of approvals needed. We now have a non-member in the list of approvals.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right. Similar thing if someone leaves while we're still processing the initial room membership. Will have to think about how to solve this. I think that setting the flag in gotVerificationStatusUpdate might fix the join-then-leave issue, but not the leave-while-initialising problem.

src/components/views/rooms/UserIdentityWarning.tsx Outdated Show resolved Hide resolved
}
gotVerificationStatusUpdate.set(userId, false);
if (await userNeedsApproval(crypto!, userId)) {
if (!membersNeedingApproval.has(userId) && gotVerificationStatusUpdate.get(userId) === false) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (!membersNeedingApproval.has(userId) && gotVerificationStatusUpdate.get(userId) === false) {
// Only actually update the list if we haven't received a `UserTrustStatusChanged` for this user in
// the meantime.
if (!membersNeedingApproval.has(userId) && gotVerificationStatusUpdate.get(userId) === false) {

Copy link
Member

Choose a reason for hiding this comment

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

Also, consider removing !membersNeedingApproval.has(userId) from this condition. AFAICT, addMemberNeedingApproval is a no-op if the user is already in the list, and removing the check would simplify this function.

Comment on lines 85 to 95
const selectCurrentPrompt = useCallback((): RoomMember | undefined => {
const membersNeedingApproval = membersNeedingApprovalRef.current;
if (membersNeedingApproval.size === 0) {
return undefined;
}

// We pick the user with the smallest user ID.
const keys = Array.from(membersNeedingApproval.keys()).sort((a, b) => a.localeCompare(b));
const selection = membersNeedingApproval.get(keys[0]!);
return selection;
}, []);
Copy link
Member

Choose a reason for hiding this comment

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

aside: I wondered if it would be good to have a single updateCurrentPrompt function which updates currentPrompt based on the current state of the list. Hence we'd have something like:

const updateCurrentPrompt = useCallback(() => {
    // Some sort of comment about callbacks and await
    setCurrentPrompt((currentPrompt) => {
        // If we have an existing prompt, leave it in place unless the user has been removed from the list
        if (currentPrompt && membersNeedingApproval.has(currentPrompt.userId)) {
            return currentPrompt;
        }

        return selectCurrentPrompt();  // probably inline this
    });
}, [setCurrentPrompt]);

... and then use that in the three places we currently call setCurrentPrompt.

It feels cognitively simpler to me, and has the advantage that we don't need to worry about someone slipping in an async before a call to setCurrentPrompt and breaking everything.

[cli, room, updateCurrentPrompt],
);

// Check if the user's identity needs approval, and if so, add them to the
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Check if the user's identity needs approval, and if so, add them to the
// For each user in the list, check if their identity needs approval, and if so, add them to the

} else if (eventType !== EventType.RoomMember) {
return;
}

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// We're processing an m.room.member event

Comment on lines 275 to 282
const gotVerificationStatusUpdate = gotVerificationStatusUpdateRef.current;
if (gotVerificationStatusUpdate.has(userId)) {
// There is an ongoing call to `addMembersWhoNeedApproval`.
// Indicate that we've received a verification status update
// to prevent it from trying to add the user as needing
// verification.
gotVerificationStatusUpdate.set(userId, true);
}
Copy link
Member

Choose a reason for hiding this comment

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

okayyy, but now don't we have a problem for a join-leave-join scenario?

I can't help but feel we need a proper queue of things to process :/

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I figured out a way to solve it while minimising the changes.

}
gotVerificationStatusUpdate.set(userId, false);
promises.push(
Copy link
Member

Choose a reason for hiding this comment

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

out of interest, was the switch to doing this in parallel meant to fix something, or just an optimisation?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just an optimisation. I changed the function to take a list of users so that it would update gotVerificationStatusUpdate for all the users at once, so that if we get an update for, say, Alice, but we haven't started fetching her update status, we won't try to overwrite the update with a fetched value. Then I figured it was silly that we were waiting for one to complete before querying the next.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Invisible Crypto: Web: display a warning when an *unverified* user changes identity
5 participants