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

feat: implement deleteUsers mutation #10498

Closed
wants to merge 9 commits into from
Closed

Conversation

tianrunhe
Copy link
Contributor

@tianrunhe tianrunhe commented Nov 19, 2024

Description

Part 1 of #10186

Testing scenarios

mutation {
  deleteUsers(emails: [
    "[email protected]",
    "[email protected]"
  ]) {
    ...on ErrorPayload {
      error {
        message
      }
    }
    ...on DeleteUsersSuccess {
      success
    }
  }
}

Final checklist

  • I checked the code review guidelines
  • I have added Metrics Representative as reviewer(s) if my PR invovles metrics/data/analytics related changes
  • I have performed a self-review of my code, the same way I'd do it for any other team member
  • I have tested all cases I listed in the testing scenarios and I haven't found any issues or regressions
  • Whenever I took a non-obvious choice I added a comment explaining why I did it this way
  • I added the label Skip Maintainer Review Indicating the PR only requires reviewer review and can be merged right after it's approved if the PR introduces only minor changes, does not contain any architectural changes or does not introduce any new patterns and I think one review is sufficient'
  • PR title is human readable and could be used in changelog

@tianrunhe tianrunhe linked an issue Nov 19, 2024 that may be closed by this pull request
1 task
@tianrunhe tianrunhe changed the title feat/massUserDeletion feat: implement deleteUsers mutations Nov 19, 2024
@tianrunhe tianrunhe changed the title feat: implement deleteUsers mutations feat: implement deleteUsers mutation Nov 19, 2024
Copy link
Contributor

@Dschoordsch Dschoordsch left a comment

Choose a reason for hiding this comment

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

I think this would work, but I'm a bit skeptical if it's the correct approach. If I'm a contractor at Contractors United and join Megacorp for a job, I don't want them to be able to delete my whole account just because they have admin rights there.

packages/server/postgres/constants.ts Show resolved Hide resolved
packages/server/graphql/public/mutations/deleteUsers.ts Outdated Show resolved Hide resolved
packages/server/graphql/public/mutations/deleteUsers.ts Outdated Show resolved Hide resolved
packages/server/graphql/public/mutations/deleteUsers.ts Outdated Show resolved Hide resolved
@Dschoordsch
Copy link
Contributor

I think there are 2 possible scenarios we could implement:

  1. the org owns the user, i.e. it owns the email domain the user is registered with, then they can delete it
  2. the org does not own the user, then we should only remove it from the org

@tianrunhe
Copy link
Contributor Author

tianrunhe commented Dec 10, 2024

Hi @Dschoordsch , I'm coming back to this PR after vacation. Sorry for dragging it this long!

If I'm a contractor at Contractors United and join Megacorp for a job, I don't want them to be able to delete my whole account just because they have admin rights there.

I would argue that if the contractor at Contractors United is a billing leader or an org admin, then they should have the rights to delete users within the org. How would he/she has become a billing leader or an org admin in the first place? Well, for billing leader someone has to promote them, and for org admin, we need to run the mutation for them.

the org owns the user, i.e. it owns the email domain the user is registered with, then they can delete it

Can you elaborate what you are thinking on "owning the email domain"? Are you talking about verified org domains? How would we test that in the code?

Thanks for your time!

@Dschoordsch
Copy link
Contributor

Dschoordsch commented Dec 11, 2024

I, Bill, contractor at Contractors United, am in 2 organizations: Contractors United and Magacorp for a gig. I don't want Evil Bob, COO of Megacorp, be able to delete my account, which I also use for my Contractors United internal holiday planning.

packages/server/graphql/public/mutations/deleteUsers.ts Outdated Show resolved Hide resolved
Comment on lines 86 to 88
const hasTeamPermission = teamMembers.some((userTeamMember) =>
viewerTeamLeadPermissions.has(userTeamMember.teamId)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

-1 If I'm a grumpy employee I can create a team "Status Reports" and get the CEO to join, then I can delete his user?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point! I removed the team level permission check and only rely on org level permissions

Comment on lines 43 to 50
// Pre-compute org-level permissions
const viewerOrgPermissions = await Promise.all(
viewerOrgUsers.map(async (orgUser) => ({
orgId: orgUser.orgId,
isOrgAdmin: await isUserOrgAdmin(viewerId, orgUser.orgId, dataLoader),
isBillingLeader: await isUserBillingLeader(viewerId, orgUser.orgId, dataLoader)
}))
)
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 I find it not very easy to read the whole delete operation. I think the mutation would benefit from not trying to do these kind of optimizations, but instead do a simple

const deletedUsers = await Promise.all(usersToDelete.map(...
  // load objects and check permission
  // delete
).filter(isNotNull)

The dataloader can still partially still do it's thing and it's much easier to ensure we don't have any index errors.

If you want to go even further, we put the permission in a separate function

canDeleteUser(actor: User, user: User)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would like to implement the "all-or-nothing" strategy: if the caller does not have permissions to delete all of the users he/she requested, we won't delete any one and as a friendly robot, we return with error message to indicate which set of the users he/she requested cannot be deleted.
With that in mind, it's not possible to merge in the deletion operation into the permission check. WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. I went back to the issue to see how this would look in practice. I'm not sure this would be the best user experience. What I expect a user to do in this case is searching the selected users to unselect the ones they can't delete and try again.
I'm also not sure deleting users is what is actually required for this issue.

Copy link
Contributor

@Dschoordsch Dschoordsch left a comment

Choose a reason for hiding this comment

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

I think a mutation like this deleteUser one makes sense for enterprise contexts where the company basically owns the user accounts. In all other cases removing the org user should be sufficient, shouldn't it?

Comment on lines +66 to +67
const organization = await dataLoader.get('organizations').loadNonNull(orgId)
return organization.activeDomain === getDomainFromEmail(userToDelete.email)
Copy link
Contributor

Choose a reason for hiding this comment

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

-1 This is not enough for owning a domain. We can only allow to delete users fully for organizations with verified domains. This usually means just enterprise orgs with SAML, but we can manually verify domains for them without them using SAML.

Comment on lines 43 to 50
// Pre-compute org-level permissions
const viewerOrgPermissions = await Promise.all(
viewerOrgUsers.map(async (orgUser) => ({
orgId: orgUser.orgId,
isOrgAdmin: await isUserOrgAdmin(viewerId, orgUser.orgId, dataLoader),
isBillingLeader: await isUserBillingLeader(viewerId, orgUser.orgId, dataLoader)
}))
)
Copy link
Contributor

Choose a reason for hiding this comment

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

I see. I went back to the issue to see how this would look in practice. I'm not sure this would be the best user experience. What I expect a user to do in this case is searching the selected users to unselect the ones they can't delete and try again.
I'm also not sure deleting users is what is actually required for this issue.

@tianrunhe
Copy link
Contributor Author

With Charles' comment here, I think the Safe-To-Try™️ version of next step is to be able to remove multiple users from the org, instead of deleting the accounts altogether. I'm therefore closing this PR.

Thanks a ton @Dschoordsch and apologize for taking too much of your time!

@tianrunhe tianrunhe closed this Dec 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

User can mass select and delete members from the members page
2 participants