1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 09:32:48 -05:00

Auth/PM-1658 - Dynamic Org Invite Link to accelerate users through org invite accept process (#3378)

* PM-1658 - Create User_ReadByEmails stored proc

* PM-1658 - Update UserRepository.cs with dapper and EF implementations of GetManyByEmailsAsync using new stored proc

* PM-1658 - OrganizationService.cs - Proved out that the new GetManyByEmailsAsync along with a hash set will allow me to generate a a dict mapping org user ids to a bool representing if they have an org user account or not.

* PM-1658 - OrganizationService.cs - re-implement all send invites logic as part of rebase

* PM-1658 - Add new User_ReadByEmails stored proc to SQL project

* PM-1658 - HandlebarsMailService.cs - (1) Remove unnecessary SendOrganizationInviteEmailAsync method as we can simply use the bulk method for one or more emails (2) Refactor BulkSendOrganizationInviteEmailAsync parameters into new OrganizationInvitesInfo class

* PM-1658 - OrganizationService.cs - rebase commit 2

* PM-1658 - rebase commit 3 - org service + IMailService conflicts resolved

* PM-1658 - Update HandlebarsMailService.cs and OrganizationUserInvitedViewModel.cs to include new query params required client side for accelerating the user through the org invite accept process.

* dotnet format

* PM-1658 - rebase commit 4 -  Fix broken OrganizationServiceTests.cs

* PM-1658 TODO cleanup

* PM-1658 - Remove noop for deleted method.

* rebase commit 5 - fix NoopMailService merge conflicts

* PM-1658 - Fix SQL formatting with proper indentations

* PM-1658 - Rename BulkSendOrganizationInviteEmailAsync to SendOrganizationInviteEmailsAsync per PR feedback

* PM-1658 - Per PR Feedback, refactor OrganizationUserInvitedViewModel creation to use new static factory function for better encapsulation of the creation process.

* PM-1658 - Rename OrganizationInvitesInfo.Invites to OrgUserTokenPairs b/c that just makes sense.

* PM-1658 - Per PR feedback, simplify query params sent down to client. Always include whether the user exists but only include the org sso identifier if it is meant to be used (b/c sso is enabled and sso required policy is on)

* dotnet format

* PM-1658 - OrganizationServiceTests.cs - Fix mysteriously failing tests - several tests were falling into logic which created n org users using the organizationUserRepository.CreateAsync instead of the organizationUserRepository.CreateManyAsync method.  This meant that I had to add a new mock helper to ensure that those created org users had valid and distinct guids to avoid aggregate exceptions due to my added dict in the latter parts of the invite process.

* PM-1658 - Resolve errors from mistakes made during rebase merge conflict resolutions

* PM-1658 - OrganizationServiceTests.cs - fix new test with mock to make guids unique.

* dotnet format

---------

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
This commit is contained in:
Jared Snider
2023-12-18 11:16:17 -05:00
committed by GitHub
parent d206c03ad1
commit d2808b2615
12 changed files with 349 additions and 75 deletions

View File

@ -18,6 +18,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.Mail;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Repositories;
using Bit.Core.Settings;
@ -1078,6 +1079,54 @@ public class OrganizationService : IOrganizationService
private async Task SendInvitesAsync(IEnumerable<OrganizationUser> orgUsers, Organization organization)
{
var orgInvitesInfo = await BuildOrganizationInvitesInfoAsync(orgUsers, organization);
await _mailService.SendOrganizationInviteEmailsAsync(orgInvitesInfo);
}
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization, bool initOrganization)
{
// convert single org user into array of 1 org user
var orgUsers = new[] { orgUser };
var orgInvitesInfo = await BuildOrganizationInvitesInfoAsync(orgUsers, organization, initOrganization);
await _mailService.SendOrganizationInviteEmailsAsync(orgInvitesInfo);
}
private async Task<OrganizationInvitesInfo> BuildOrganizationInvitesInfoAsync(
IEnumerable<OrganizationUser> orgUsers,
Organization organization,
bool initOrganization = false)
{
// Materialize the sequence into a list to avoid multiple enumeration warnings
var orgUsersList = orgUsers.ToList();
// Email links must include information about the org and user for us to make routing decisions client side
// Given an org user, determine if existing BW user exists
var orgUserEmails = orgUsersList.Select(ou => ou.Email).ToList();
var existingUsers = await _userRepository.GetManyByEmailsAsync(orgUserEmails);
// hash existing users emails list for O(1) lookups
var existingUserEmailsHashSet = new HashSet<string>(existingUsers.Select(u => u.Email));
// Create a dictionary of org user guids and bools for whether or not they have an existing BW user
var orgUserHasExistingUserDict = orgUsersList.ToDictionary(
ou => ou.Id,
ou => existingUserEmailsHashSet.Contains(ou.Email)
);
// Determine if org has SSO enabled and if user is required to login with SSO
// Note: we only want to call the DB after checking if the org can use SSO per plan and if they have any policies enabled.
var orgSsoEnabled = organization.UseSso && (await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id)).Enabled;
// Even though the require SSO policy can be turned on regardless of SSO being enabled, for this logic, we only
// need to check the policy if the org has SSO enabled.
var orgSsoLoginRequiredPolicyEnabled = orgSsoEnabled &&
organization.UsePolicies &&
(await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.RequireSso)).Enabled;
// Generate the list of org users and expiring tokens
// create helper function to create expiring tokens
(OrganizationUser, ExpiringToken) MakeOrgUserExpiringTokenPair(OrganizationUser orgUser)
{
var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser);
@ -1087,22 +1136,12 @@ public class OrganizationService : IOrganizationService
var orgUsersWithExpTokens = orgUsers.Select(MakeOrgUserExpiringTokenPair);
await _mailService.BulkSendOrganizationInviteEmailAsync(
organization.Name,
return new OrganizationInvitesInfo(
organization,
orgSsoEnabled,
orgSsoLoginRequiredPolicyEnabled,
orgUsersWithExpTokens,
organization.PlanType == PlanType.Free
);
}
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization, bool initOrganization)
{
var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser);
var protectedToken = _orgUserInviteTokenDataFactory.Protect(orgUserInviteTokenable);
await _mailService.SendOrganizationInviteEmailAsync(
organization.Name,
orgUser,
new ExpiringToken(protectedToken, orgUserInviteTokenable.ExpirationDate),
organization.PlanType == PlanType.Free,
orgUserHasExistingUserDict,
initOrganization
);
}