1
0
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:
Matt Gibson
2021-05-17 09:43:02 -05:00
committed by GitHub
parent 738a4c2bac
commit 785e788cb6
64 changed files with 1704 additions and 234 deletions

View File

@ -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 })
{ }
}
}

View 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);
}
}

View 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));
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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)

View File

@ -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 });

View File

@ -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

View File

@ -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);
}

View File

@ -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))
{