mirror of
https://github.com/bitwarden/server.git
synced 2025-07-04 09:32:48 -05:00
Support large organization sync (#1311)
* Increase organization max seat size from 30k to 2b (#1274) * Increase organization max seat size from 30k to 2b * PR review. Do not modify unless state matches expected * Organization sync simultaneous event reporting (#1275) * Split up azure messages according to max size * Allow simultaneous login of organization user events * Early resolve small event lists * Clarify logic Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> * Improve readability This comes at the cost of multiple serializations, but the improvement in wire-time should more than make up for this on message where serialization time matters Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> * Queue emails (#1286) * Extract common Azure queue methods * Do not use internal entity framework namespace * Prefer IEnumerable to IList unless needed All of these implementations were just using `Count == 1`, which is easily replicated. This will be used when abstracting Azure queues * Add model for azure queue message * Abstract Azure queue for reuse * Creat service to enqueue mail messages for later processing Azure queue mail service uses Azure queues. Blocking just blocks until all the work is done -- This is how emailing works today * Provide mail queue service to DI * Queue organization invite emails for later processing All emails can later be added to this queue * Create Admin hosted service to process enqueued mail messages * Prefer constructors to static generators * Mass delete organization users (#1287) * Add delete many to Organization Users * Correct formatting * Remove erroneous migration * Clarify parameter name * Formatting fixes * Simplify bump account revision sproc * Formatting fixes * Match file names to objects * Indicate if large import is expected * Early pull all existing users we were planning on inviting (#1290) * Early pull all existing users we were planning on inviting * Improve sproc name * Batch upsert org users (#1289) * Add UpsertMany sprocs to OrganizationUser * Add method to create TVPs from any object. Uses DbOrder attribute to generate. Sproc will fail unless TVP column order matches that of the db type * Combine migrations * Correct formatting * Include sql objects in sql project * Keep consisten parameter names * Batch deletes for performance * Correct formatting * consolidate migrations * Use batch methods in OrganizationImport * Declare @BatchSize * Transaction names limited to 32 chars Drop sproc before creating it if it exists * Update import tests * Allow for more users in org upgrades * Fix formatting * Improve class hierarchy structure * Use name tuple types * Fix formatting * Front load all reflection * Format constructor * Simplify ToTvp as class-specific extension Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
This commit is contained in:
@ -4,34 +4,16 @@ using Azure.Storage.Queues;
|
||||
using Newtonsoft.Json;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Settings;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class AzureQueueEventWriteService : IEventWriteService
|
||||
public class AzureQueueEventWriteService : AzureQueueService<IEvent>, IEventWriteService
|
||||
{
|
||||
private readonly QueueClient _queueClient;
|
||||
|
||||
private JsonSerializerSettings _jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
public AzureQueueEventWriteService(
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_queueClient = new QueueClient(globalSettings.Events.ConnectionString, "event");
|
||||
}
|
||||
|
||||
public async Task CreateAsync(IEvent e)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(e, _jsonSettings);
|
||||
await _queueClient.SendMessageAsync(json);
|
||||
}
|
||||
|
||||
public async Task CreateManyAsync(IList<IEvent> e)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(e, _jsonSettings);
|
||||
await _queueClient.SendMessageAsync(json);
|
||||
}
|
||||
public AzureQueueEventWriteService(GlobalSettings globalSettings) : base(
|
||||
new QueueClient(globalSettings.Events.ConnectionString, "event"),
|
||||
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })
|
||||
{ }
|
||||
}
|
||||
}
|
||||
|
25
src/Core/Services/Implementations/AzureQueueMailService.cs
Normal file
25
src/Core/Services/Implementations/AzureQueueMailService.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.Storage.Queues;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Settings;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class AzureQueueMailService : AzureQueueService<IMailQueueMessage>, IMailEnqueuingService
|
||||
{
|
||||
public AzureQueueMailService(GlobalSettings globalSettings) : base(
|
||||
new QueueClient(globalSettings.Mail.ConnectionString, "mail"),
|
||||
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })
|
||||
{ }
|
||||
|
||||
public Task EnqueueAsync(IMailQueueMessage message, Func<IMailQueueMessage, Task> fallback) =>
|
||||
CreateAsync(message);
|
||||
|
||||
public Task EnqueueManyAsync(IEnumerable<IMailQueueMessage> messages, Func<IMailQueueMessage, Task> fallback) =>
|
||||
CreateManyAsync(messages);
|
||||
}
|
||||
}
|
72
src/Core/Services/Implementations/AzureQueueService.cs
Normal file
72
src/Core/Services/Implementations/AzureQueueService.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Azure.Storage.Queues;
|
||||
using IdentityServer4.Extensions;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public abstract class AzureQueueService<T>
|
||||
{
|
||||
protected QueueClient _queueClient;
|
||||
protected JsonSerializerSettings _jsonSettings;
|
||||
|
||||
protected AzureQueueService(QueueClient queueClient, JsonSerializerSettings jsonSettings)
|
||||
{
|
||||
_queueClient = queueClient;
|
||||
_jsonSettings = jsonSettings;
|
||||
}
|
||||
|
||||
public async Task CreateAsync(T message)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(message, _jsonSettings);
|
||||
await _queueClient.SendMessageAsync(json);
|
||||
}
|
||||
|
||||
public async Task CreateManyAsync(IEnumerable<T> messages)
|
||||
{
|
||||
if (messages?.Any() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!messages.Skip(1).Any())
|
||||
{
|
||||
await CreateAsync(messages.First());
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var json in SerializeMany(messages, _jsonSettings))
|
||||
{
|
||||
await _queueClient.SendMessageAsync(json);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected IEnumerable<string> SerializeMany(IEnumerable<T> messages, JsonSerializerSettings jsonSettings)
|
||||
{
|
||||
var messagesLists = new List<List<T>> { new List<T>() };
|
||||
var strings = new List<string>();
|
||||
var ListMessageLength = 2; // to account for json array brackets "[]"
|
||||
foreach (var (message, jsonEvent) in messages.Select(e => (e, JsonConvert.SerializeObject(e, jsonSettings))))
|
||||
{
|
||||
|
||||
var messageLength = jsonEvent.Length + 1; // To account for json array comma
|
||||
if (ListMessageLength + messageLength > _queueClient.MessageMaxBytes)
|
||||
{
|
||||
messagesLists.Add(new List<T> { message });
|
||||
ListMessageLength = 2 + messageLength;
|
||||
}
|
||||
else
|
||||
{
|
||||
messagesLists.Last().Add(message);
|
||||
ListMessageLength += messageLength;
|
||||
}
|
||||
}
|
||||
return messagesLists.Select(l => JsonConvert.SerializeObject(l, jsonSettings));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Mail;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class BlockingMailEnqueuingService : IMailEnqueuingService
|
||||
{
|
||||
public async Task EnqueueAsync(IMailQueueMessage message, Func<IMailQueueMessage, Task> fallback)
|
||||
{
|
||||
await fallback(message);
|
||||
}
|
||||
|
||||
public async Task EnqueueManyAsync(IEnumerable<IMailQueueMessage> messages, Func<IMailQueueMessage, Task> fallback)
|
||||
{
|
||||
foreach(var message in messages)
|
||||
{
|
||||
await fallback(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -178,24 +178,31 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
public async Task LogOrganizationUserEventAsync(OrganizationUser organizationUser, EventType type,
|
||||
DateTime? date = null)
|
||||
DateTime? date = null) =>
|
||||
await LogOrganizationUserEventsAsync(new[] { (organizationUser, type, date) });
|
||||
|
||||
public async Task LogOrganizationUserEventsAsync(IEnumerable<(OrganizationUser, EventType, DateTime?)> events)
|
||||
{
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
if (!CanUseEvents(orgAbilities, organizationUser.OrganizationId))
|
||||
var eventMessages = new List<IEvent>();
|
||||
foreach (var (organizationUser, type, date) in events)
|
||||
{
|
||||
return;
|
||||
if (!CanUseEvents(orgAbilities, organizationUser.OrganizationId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
eventMessages.Add(new EventMessage
|
||||
{
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
UserId = organizationUser.UserId,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Type = type,
|
||||
ActingUserId = _currentContext?.UserId,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||
});
|
||||
}
|
||||
|
||||
var e = new EventMessage(_currentContext)
|
||||
{
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
UserId = organizationUser.UserId,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Type = type,
|
||||
ActingUserId = _currentContext?.UserId,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||
};
|
||||
await _eventWriteService.CreateAsync(e);
|
||||
await _eventWriteService.CreateManyAsync(eventMessages);
|
||||
}
|
||||
|
||||
public async Task LogOrganizationEventAsync(Organization organization, EventType type, DateTime? date = null)
|
||||
|
@ -19,6 +19,7 @@ namespace Bit.Core.Services
|
||||
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IMailDeliveryService _mailDeliveryService;
|
||||
private readonly IMailEnqueuingService _mailEnqueuingService;
|
||||
private readonly Dictionary<string, Func<object, string>> _templateCache =
|
||||
new Dictionary<string, Func<object, string>>();
|
||||
|
||||
@ -26,10 +27,12 @@ namespace Bit.Core.Services
|
||||
|
||||
public HandlebarsMailService(
|
||||
GlobalSettings globalSettings,
|
||||
IMailDeliveryService mailDeliveryService)
|
||||
IMailDeliveryService mailDeliveryService,
|
||||
IMailEnqueuingService mailEnqueuingService)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
_mailDeliveryService = mailDeliveryService;
|
||||
_mailEnqueuingService = mailEnqueuingService;
|
||||
}
|
||||
|
||||
public async Task SendVerifyEmailEmailAsync(string email, Guid userId, string token)
|
||||
@ -168,23 +171,32 @@ namespace Bit.Core.Services
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, string token)
|
||||
public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, string token) =>
|
||||
BulkSendOrganizationInviteEmailAsync(organizationName, new[] { (orgUser, token) });
|
||||
|
||||
public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, string token)> invites)
|
||||
{
|
||||
var message = CreateDefaultMessage($"Join {organizationName}", orgUser.Email);
|
||||
var model = new OrganizationUserInvitedViewModel
|
||||
MailQueueMessage CreateMessage(string email, object model)
|
||||
{
|
||||
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName),
|
||||
Email = WebUtility.UrlEncode(orgUser.Email),
|
||||
OrganizationId = orgUser.OrganizationId.ToString(),
|
||||
OrganizationUserId = orgUser.Id.ToString(),
|
||||
Token = WebUtility.UrlEncode(token),
|
||||
OrganizationNameUrlEncoded = WebUtility.UrlEncode(organizationName),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName
|
||||
};
|
||||
await AddMessageContentAsync(message, "OrganizationUserInvited", model);
|
||||
message.Category = "OrganizationUserInvited";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
var message = CreateDefaultMessage($"Join {organizationName}", email);
|
||||
return new MailQueueMessage(message, "OrganizationUserInvited", model);
|
||||
}
|
||||
|
||||
var messageModels = invites.Select(invite => CreateMessage(invite.orgUser.Email,
|
||||
new OrganizationUserInvitedViewModel
|
||||
{
|
||||
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName),
|
||||
Email = WebUtility.UrlEncode(invite.orgUser.Email),
|
||||
OrganizationId = invite.orgUser.OrganizationId.ToString(),
|
||||
OrganizationUserId = invite.orgUser.Id.ToString(),
|
||||
Token = WebUtility.UrlEncode(invite.token),
|
||||
OrganizationNameUrlEncoded = WebUtility.UrlEncode(organizationName),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName,
|
||||
}
|
||||
));
|
||||
|
||||
await EnqueueMailAsync(messageModels);
|
||||
}
|
||||
|
||||
public async Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email)
|
||||
@ -341,6 +353,21 @@ namespace Bit.Core.Services
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendEnqueuedMailMessageAsync(IMailQueueMessage queueMessage)
|
||||
{
|
||||
var message = CreateDefaultMessage(queueMessage.Subject, queueMessage.ToEmails);
|
||||
message.BccEmails = queueMessage.BccEmails;
|
||||
message.Category = queueMessage.Category;
|
||||
await AddMessageContentAsync(message, queueMessage.TemplateName, queueMessage.Model);
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
private Task EnqueueMailAsync(IMailQueueMessage queueMessage) =>
|
||||
_mailEnqueuingService.EnqueueAsync(queueMessage, SendEnqueuedMailMessageAsync);
|
||||
|
||||
private Task EnqueueMailAsync(IEnumerable<IMailQueueMessage> queueMessages) =>
|
||||
_mailEnqueuingService.EnqueueManyAsync(queueMessages, SendEnqueuedMailMessageAsync);
|
||||
|
||||
private MailMessage CreateDefaultMessage(string subject, string toEmail)
|
||||
{
|
||||
return CreateDefaultMessage(subject, new List<string> { toEmail });
|
||||
|
@ -443,9 +443,9 @@ namespace Bit.Core.Services
|
||||
var taxRate = taxRates.FirstOrDefault();
|
||||
if (taxRate != null && !sub.DefaultTaxRates.Any(x => x.Equals(taxRate.Id)))
|
||||
{
|
||||
subUpdateOptions.DefaultTaxRates = new List<string>(1)
|
||||
{
|
||||
taxRate.Id
|
||||
subUpdateOptions.DefaultTaxRates = new List<string>(1)
|
||||
{
|
||||
taxRate.Id
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1011,6 +1011,117 @@ namespace Bit.Core.Services
|
||||
await UpdateAsync(organization);
|
||||
}
|
||||
|
||||
private async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,
|
||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
|
||||
{
|
||||
var organization = await GetOrgById(organizationId);
|
||||
if (organization == null || invites.Any(i => i.invite.Emails == null || i.externalId == null))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var inviteTypes = new HashSet<OrganizationUserType>(invites.Where(i => i.invite.Type.HasValue)
|
||||
.Select(i => i.invite.Type.Value));
|
||||
if (invitingUserId.HasValue && inviteTypes.Count > 0)
|
||||
{
|
||||
foreach (var type in inviteTypes)
|
||||
{
|
||||
await ValidateOrganizationUserUpdatePermissionsAsync(invitingUserId.Value, organizationId, type, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (organization.Seats.HasValue)
|
||||
{
|
||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
|
||||
var availableSeats = organization.Seats.Value - userCount;
|
||||
if (availableSeats < invites.Select(i => i.invite.Emails.Count()).Sum())
|
||||
{
|
||||
throw new BadRequestException("You have reached the maximum number of users " +
|
||||
$"({organization.Seats.Value}) for this organization.");
|
||||
}
|
||||
}
|
||||
|
||||
var orgUsers = new List<OrganizationUser>();
|
||||
var orgUserInvitedCount = 0;
|
||||
var exceptions = new List<Exception>();
|
||||
var events = new List<(OrganizationUser, EventType, DateTime?)>();
|
||||
var existingEmails = new HashSet<string>(await _organizationUserRepository.SelectKnownEmailsAsync(
|
||||
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
|
||||
foreach (var (invite, externalId) in invites)
|
||||
{
|
||||
foreach (var email in invite.Emails)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Make sure user is not already invited
|
||||
if (existingEmails.Contains(email))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
UserId = null,
|
||||
Email = email.ToLowerInvariant(),
|
||||
Key = null,
|
||||
Type = invite.Type.Value,
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
AccessAll = invite.AccessAll,
|
||||
ExternalId = externalId,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
if (invite.Permissions != null)
|
||||
{
|
||||
orgUser.Permissions = System.Text.Json.JsonSerializer.Serialize(invite.Permissions, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
});
|
||||
}
|
||||
|
||||
if (!orgUser.AccessAll && invite.Collections.Any())
|
||||
{
|
||||
throw new Exception("Bulk invite does not support limited collection invites");
|
||||
}
|
||||
|
||||
events.Add((orgUser, EventType.OrganizationUser_Invited, DateTime.UtcNow));
|
||||
orgUsers.Add(orgUser);
|
||||
orgUserInvitedCount++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
exceptions.Add(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _organizationUserRepository.CreateManyAsync(orgUsers);
|
||||
await SendInvitesAsync(orgUsers, organization);
|
||||
await _eventService.LogOrganizationUserEventsAsync(events);
|
||||
|
||||
await _referenceEventService.RaiseEventAsync(
|
||||
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization)
|
||||
{
|
||||
Users = orgUserInvitedCount
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
exceptions.Add(e);
|
||||
}
|
||||
|
||||
if (exceptions.Any())
|
||||
{
|
||||
throw new AggregateException("One or more errors occurred while inviting users.", exceptions);
|
||||
}
|
||||
|
||||
return orgUsers;
|
||||
}
|
||||
|
||||
public async Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId,
|
||||
string externalId, OrganizationUserInvite invite)
|
||||
{
|
||||
@ -1022,7 +1133,7 @@ namespace Bit.Core.Services
|
||||
|
||||
if (invitingUserId.HasValue && invite.Type.HasValue)
|
||||
{
|
||||
await ValidateOrganizationUserUpdatePermissions(invitingUserId.Value, organizationId, invite.Type.Value, null);
|
||||
await ValidateOrganizationUserUpdatePermissionsAsync(invitingUserId.Value, organizationId, invite.Type.Value, null);
|
||||
}
|
||||
|
||||
if (organization.Seats.HasValue)
|
||||
@ -1125,6 +1236,14 @@ namespace Bit.Core.Services
|
||||
await SendInviteAsync(orgUser, org);
|
||||
}
|
||||
|
||||
private async Task SendInvitesAsync(IEnumerable<OrganizationUser> orgUsers, Organization organization)
|
||||
{
|
||||
string MakeToken(OrganizationUser orgUser) =>
|
||||
_dataProtector.Protect($"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
|
||||
await _mailService.BulkSendOrganizationInviteEmailAsync(organization.Name,
|
||||
orgUsers.Select(o => (o, MakeToken(o))));
|
||||
}
|
||||
|
||||
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization)
|
||||
{
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
@ -1185,7 +1304,7 @@ namespace Bit.Core.Services
|
||||
return await AcceptUserAsync(orgUser, user, userService);
|
||||
}
|
||||
|
||||
private async Task<OrganizationUser> AcceptUserAsync(OrganizationUser orgUser, User user,
|
||||
private async Task<OrganizationUser> AcceptUserAsync(OrganizationUser orgUser, User user,
|
||||
IUserService userService)
|
||||
{
|
||||
if (orgUser.Status != OrganizationUserStatusType.Invited)
|
||||
@ -1322,13 +1441,14 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
var originalUser = await _organizationUserRepository.GetByIdAsync(user.Id);
|
||||
if (user.Equals(originalUser)) {
|
||||
if (user.Equals(originalUser))
|
||||
{
|
||||
throw new BadRequestException("Please make changes before saving.");
|
||||
}
|
||||
|
||||
if (savingUserId.HasValue)
|
||||
{
|
||||
await ValidateOrganizationUserUpdatePermissions(savingUserId.Value, user.OrganizationId, user.Type, originalUser.Type);
|
||||
await ValidateOrganizationUserUpdatePermissionsAsync(savingUserId.Value, user.OrganizationId, user.Type, originalUser.Type);
|
||||
}
|
||||
|
||||
if (user.Type != OrganizationUserType.Owner &&
|
||||
@ -1459,13 +1579,13 @@ namespace Bit.Core.Services
|
||||
{
|
||||
if (loggedInUserId.HasValue)
|
||||
{
|
||||
await ValidateOrganizationUserUpdatePermissions(loggedInUserId.Value, organizationUser.OrganizationId, organizationUser.Type, null);
|
||||
await ValidateOrganizationUserUpdatePermissionsAsync(loggedInUserId.Value, organizationUser.OrganizationId, organizationUser.Type, null);
|
||||
}
|
||||
await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupIds);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser,
|
||||
EventType.OrganizationUser_UpdatedGroups);
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid organizationUserId, string resetPasswordKey, Guid? callingUserId)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, organizationUserId);
|
||||
@ -1480,7 +1600,7 @@ namespace Bit.Core.Services
|
||||
|
||||
orgUser.ResetPasswordKey = resetPasswordKey;
|
||||
await _organizationUserRepository.ReplaceAsync(orgUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, resetPasswordKey != null ?
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, resetPasswordKey != null ?
|
||||
EventType.OrganizationUser_ResetPassword_Enroll : EventType.OrganizationUser_ResetPassword_Withdraw);
|
||||
}
|
||||
|
||||
@ -1558,32 +1678,23 @@ namespace Bit.Core.Services
|
||||
var removeUsersSet = new HashSet<string>(removeUserExternalIds);
|
||||
var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId);
|
||||
|
||||
var usersToRemove = removeUsersSet
|
||||
await _organizationUserRepository.DeleteManyAsync(removeUsersSet
|
||||
.Except(newUsersSet)
|
||||
.Where(ru => existingUsersDict.ContainsKey(ru))
|
||||
.Select(ru => existingUsersDict[ru]);
|
||||
|
||||
foreach (var user in usersToRemove)
|
||||
{
|
||||
if (user.Type != OrganizationUserType.Owner)
|
||||
{
|
||||
await _organizationUserRepository.DeleteAsync(new OrganizationUser { Id = user.Id });
|
||||
existingExternalUsersIdDict.Remove(user.ExternalId);
|
||||
}
|
||||
}
|
||||
.Where(u => existingUsersDict.ContainsKey(u) && existingUsersDict[u].Type != OrganizationUserType.Owner)
|
||||
.Select(u => existingUsersDict[u].Id));
|
||||
}
|
||||
|
||||
if (overwriteExisting)
|
||||
{
|
||||
// Remove existing external users that are not in new user set
|
||||
foreach (var user in existingExternalUsers)
|
||||
var usersToDelete = existingExternalUsers.Where(u =>
|
||||
u.Type != OrganizationUserType.Owner &&
|
||||
!newUsersSet.Contains(u.ExternalId) &&
|
||||
existingExternalUsersIdDict.ContainsKey(u.ExternalId));
|
||||
await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id));
|
||||
foreach (var deletedUser in usersToDelete)
|
||||
{
|
||||
if (user.Type != OrganizationUserType.Owner && !newUsersSet.Contains(user.ExternalId) &&
|
||||
existingExternalUsersIdDict.ContainsKey(user.ExternalId))
|
||||
{
|
||||
await _organizationUserRepository.DeleteAsync(new OrganizationUser { Id = user.Id });
|
||||
existingExternalUsersIdDict.Remove(user.ExternalId);
|
||||
}
|
||||
existingExternalUsersIdDict.Remove(deletedUser.ExternalId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1595,6 +1706,7 @@ namespace Bit.Core.Services
|
||||
.ToDictionary(u => u.Email);
|
||||
var newUsersEmailsDict = newUsers.ToDictionary(u => u.Email);
|
||||
var usersToAttach = existingUsersEmailsDict.Keys.Intersect(newUsersEmailsDict.Keys).ToList();
|
||||
var usersToUpsert = new List<OrganizationUser>();
|
||||
foreach (var user in usersToAttach)
|
||||
{
|
||||
var orgUserDetails = existingUsersEmailsDict[user];
|
||||
@ -1602,10 +1714,11 @@ namespace Bit.Core.Services
|
||||
if (orgUser != null)
|
||||
{
|
||||
orgUser.ExternalId = newUsersEmailsDict[user].ExternalId;
|
||||
await _organizationUserRepository.UpsertAsync(orgUser);
|
||||
usersToUpsert.Add(orgUser);
|
||||
existingExternalUsersIdDict.Add(orgUser.ExternalId, orgUser.Id);
|
||||
}
|
||||
}
|
||||
await _organizationUserRepository.UpsertManyAsync(usersToUpsert);
|
||||
|
||||
// Add new users
|
||||
var existingUsersSet = new HashSet<string>(existingExternalUsersIdDict.Keys);
|
||||
@ -1620,11 +1733,12 @@ namespace Bit.Core.Services
|
||||
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
||||
}
|
||||
|
||||
if (!enoughSeatsAvailable)
|
||||
if (!enoughSeatsAvailable)
|
||||
{
|
||||
throw new BadRequestException($"Organization does not have enough seats available. Need {usersToAdd.Count} but {seatsAvailable} available.");
|
||||
}
|
||||
|
||||
var userInvites = new List<(OrganizationUserInvite, string)>();
|
||||
foreach (var user in newUsers)
|
||||
{
|
||||
if (!usersToAdd.Contains(user.ExternalId) || string.IsNullOrWhiteSpace(user.Email))
|
||||
@ -1641,9 +1755,7 @@ namespace Bit.Core.Services
|
||||
AccessAll = false,
|
||||
Collections = new List<SelectionReadOnly>(),
|
||||
};
|
||||
var newUser = await InviteUserAsync(organizationId, importingUserId, user.Email,
|
||||
OrganizationUserType.User, false, user.ExternalId, new List<SelectionReadOnly>());
|
||||
existingExternalUsersIdDict.Add(newUser.ExternalId, newUser.Id);
|
||||
userInvites.Add((invite, user.ExternalId));
|
||||
}
|
||||
catch (BadRequestException)
|
||||
{
|
||||
@ -1651,10 +1763,16 @@ namespace Bit.Core.Services
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var invitedUsers = await InviteUsersAsync(organizationId, importingUserId, userInvites);
|
||||
foreach (var invitedUser in invitedUsers)
|
||||
{
|
||||
existingExternalUsersIdDict.Add(invitedUser.ExternalId, invitedUser.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// Groups
|
||||
|
||||
// Groups
|
||||
if (groups?.Any() ?? false)
|
||||
{
|
||||
if (!organization.UseGroups)
|
||||
@ -1822,7 +1940,8 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateOrganizationUserUpdatePermissions(Guid loggedInUserId, Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType)
|
||||
private async Task ValidateOrganizationUserUpdatePermissionsAsync(Guid loggedInUserId, Guid organizationId,
|
||||
OrganizationUserType newType, OrganizationUserType? oldType)
|
||||
{
|
||||
var loggedInUserOrgs = await _organizationUserRepository.GetManyByUserAsync(loggedInUserId);
|
||||
var loggedInAsOrgOwner = loggedInUserOrgs
|
||||
|
@ -20,7 +20,7 @@ namespace Bit.Core.Services
|
||||
await _eventRepository.CreateAsync(e);
|
||||
}
|
||||
|
||||
public async Task CreateManyAsync(IList<IEvent> e)
|
||||
public async Task CreateManyAsync(IEnumerable<IEvent> e)
|
||||
{
|
||||
await _eventRepository.CreateManyAsync(e);
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ namespace Bit.Core.Services
|
||||
|
||||
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
||||
string paymentToken, Models.StaticStore.Plan plan, short additionalStorageGb,
|
||||
short additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
|
||||
int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
|
||||
{
|
||||
var customerService = new CustomerService();
|
||||
|
||||
@ -202,7 +202,7 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan,
|
||||
short additionalStorageGb, short additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
|
||||
short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
|
||||
{
|
||||
|
Reference in New Issue
Block a user