Files
server/src/Core/Services/Implementations/OrganizationService.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

2196 lines
95 KiB
C#
Raw Normal View History

using System;
2017-03-09 23:58:43 -05:00
using System.Collections.Generic;
2017-08-14 21:25:06 -04:00
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
2017-05-11 14:52:35 -04:00
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging;
2017-04-04 10:13:16 -04:00
using Stripe;
namespace Bit.Core.Services
{
public class OrganizationService : IOrganizationService
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
2017-04-27 09:19:30 -04:00
private readonly ICollectionRepository _collectionRepository;
2017-03-04 21:28:41 -05:00
private readonly IUserRepository _userRepository;
private readonly IGroupRepository _groupRepository;
private readonly IDataProtector _dataProtector;
private readonly IMailService _mailService;
2017-05-26 22:52:50 -04:00
private readonly IPushNotificationService _pushNotificationService;
private readonly IPushRegistrationService _pushRegistrationService;
2017-08-11 08:57:31 -04:00
private readonly IDeviceRepository _deviceRepository;
2017-08-14 20:57:45 -04:00
private readonly ILicensingService _licensingService;
private readonly IEventService _eventService;
private readonly IInstallationRepository _installationRepository;
private readonly IApplicationCacheService _applicationCacheService;
2019-02-08 23:53:09 -05:00
private readonly IPaymentService _paymentService;
2020-01-15 15:00:54 -05:00
private readonly IPolicyRepository _policyRepository;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly ISsoUserRepository _ssoUserRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly IGlobalSettings _globalSettings;
private readonly ITaxRateRepository _taxRateRepository;
private readonly ICurrentContext _currentContext;
private readonly ILogger<OrganizationService> _logger;
public OrganizationService(
IOrganizationRepository organizationRepository,
2017-03-04 21:28:41 -05:00
IOrganizationUserRepository organizationUserRepository,
2017-04-27 09:19:30 -04:00
ICollectionRepository collectionRepository,
IUserRepository userRepository,
IGroupRepository groupRepository,
IDataProtectionProvider dataProtectionProvider,
IMailService mailService,
2017-05-26 22:52:50 -04:00
IPushNotificationService pushNotificationService,
2017-08-11 08:57:31 -04:00
IPushRegistrationService pushRegistrationService,
2017-08-14 20:57:45 -04:00
IDeviceRepository deviceRepository,
ILicensingService licensingService,
IEventService eventService,
IInstallationRepository installationRepository,
IApplicationCacheService applicationCacheService,
2019-02-08 23:53:09 -05:00
IPaymentService paymentService,
2020-01-15 15:00:54 -05:00
IPolicyRepository policyRepository,
ISsoConfigRepository ssoConfigRepository,
ISsoUserRepository ssoUserRepository,
IReferenceEventService referenceEventService,
IGlobalSettings globalSettings,
ITaxRateRepository taxRateRepository,
ICurrentContext currentContext,
ILogger<OrganizationService> logger)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
2017-04-27 09:19:30 -04:00
_collectionRepository = collectionRepository;
2017-03-04 21:28:41 -05:00
_userRepository = userRepository;
_groupRepository = groupRepository;
_dataProtector = dataProtectionProvider.CreateProtector("OrganizationServiceDataProtector");
_mailService = mailService;
2017-05-26 22:52:50 -04:00
_pushNotificationService = pushNotificationService;
_pushRegistrationService = pushRegistrationService;
2017-08-11 08:57:31 -04:00
_deviceRepository = deviceRepository;
2017-08-14 20:57:45 -04:00
_licensingService = licensingService;
_eventService = eventService;
_installationRepository = installationRepository;
_applicationCacheService = applicationCacheService;
2019-02-08 23:53:09 -05:00
_paymentService = paymentService;
2020-01-15 15:00:54 -05:00
_policyRepository = policyRepository;
_ssoConfigRepository = ssoConfigRepository;
_ssoUserRepository = ssoUserRepository;
_referenceEventService = referenceEventService;
2017-08-14 20:57:45 -04:00
_globalSettings = globalSettings;
_taxRateRepository = taxRateRepository;
_currentContext = currentContext;
_logger = logger;
}
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
PaymentMethodType paymentMethodType, TaxInfo taxInfo)
2017-04-08 16:41:40 -04:00
{
var organization = await GetOrgById(organizationId);
if (organization == null)
2017-04-08 16:41:40 -04:00
{
throw new NotFoundException();
}
await _paymentService.SaveTaxInfoAsync(organization, taxInfo);
2019-02-08 23:53:09 -05:00
var updated = await _paymentService.UpdatePaymentMethodAsync(organization,
2019-02-26 12:45:34 -05:00
paymentMethodType, paymentToken);
if (updated)
2017-04-08 16:41:40 -04:00
{
await ReplaceAndUpdateCache(organization);
2017-04-08 16:41:40 -04:00
}
}
2018-12-31 13:34:02 -05:00
public async Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null)
2017-04-08 18:15:20 -04:00
{
var organization = await GetOrgById(organizationId);
if (organization == null)
2017-04-08 18:15:20 -04:00
{
throw new NotFoundException();
}
2018-12-31 13:34:02 -05:00
var eop = endOfPeriod.GetValueOrDefault(true);
if (!endOfPeriod.HasValue && organization.ExpirationDate.HasValue &&
2018-12-31 13:34:02 -05:00
organization.ExpirationDate.Value < DateTime.UtcNow)
{
eop = false;
}
2019-02-08 23:53:09 -05:00
await _paymentService.CancelSubscriptionAsync(organization, eop);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.CancelSubscription, organization)
{
EndOfPeriod = endOfPeriod,
});
2017-04-08 18:15:20 -04:00
}
public async Task ReinstateSubscriptionAsync(Guid organizationId)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
2019-02-08 23:53:09 -05:00
await _paymentService.ReinstateSubscriptionAsync(organization);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.ReinstateSubscription, organization));
}
public async Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{
2019-03-21 21:36:03 -04:00
throw new BadRequestException("Your account has no payment method available.");
}
var existingPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
if (existingPlan == null)
{
throw new BadRequestException("Existing plan not found.");
}
2019-03-21 21:36:03 -04:00
var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
if (newPlan == null)
{
throw new BadRequestException("Plan not found.");
}
if (existingPlan.Type == newPlan.Type)
{
throw new BadRequestException("Organization is already on this plan.");
}
if (existingPlan.UpgradeSortOrder >= newPlan.UpgradeSortOrder)
{
throw new BadRequestException("You cannot upgrade to this plan.");
}
if (existingPlan.Type != PlanType.Free)
{
2019-03-21 21:36:03 -04:00
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
}
2019-03-21 21:36:03 -04:00
ValidateOrganizationUpgradeParameters(newPlan, upgrade);
2019-03-21 21:36:03 -04:00
var newPlanSeats = (short)(newPlan.BaseSeats +
(newPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
if (!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
if (userCount > newPlanSeats)
{
2019-03-21 21:36:03 -04:00
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
$"Your new plan only has ({newPlanSeats}) seats. Remove some users.");
}
}
if (newPlan.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
2019-03-21 21:36:03 -04:00
organization.MaxCollections.Value > newPlan.MaxCollections.Value))
{
2017-04-27 09:19:30 -04:00
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
if (collectionCount > newPlan.MaxCollections.Value)
{
2017-04-27 09:19:30 -04:00
throw new BadRequestException($"Your organization currently has {collectionCount} collections. " +
$"Your new plan allows for a maximum of ({newPlan.MaxCollections.Value}) collections. " +
"Remove some collections.");
}
}
if (!newPlan.HasGroups && organization.UseGroups)
{
2019-03-21 21:36:03 -04:00
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
if (groups.Any())
2017-05-08 14:40:04 -04:00
{
2019-03-21 21:36:03 -04:00
throw new BadRequestException($"Your new plan does not allow the groups feature. " +
$"Remove your groups.");
2017-05-08 14:40:04 -04:00
}
2019-03-21 21:36:03 -04:00
}
2017-05-08 14:40:04 -04:00
if (!newPlan.HasPolicies && organization.UsePolicies)
2020-01-15 15:00:54 -05:00
{
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id);
if (policies.Any(p => p.Enabled))
2020-01-15 15:00:54 -05:00
{
throw new BadRequestException($"Your new plan does not allow the policies feature. " +
$"Disable your policies.");
}
}
if (!newPlan.HasSso && organization.UseSso)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig != null && ssoConfig.Enabled)
{
throw new BadRequestException($"Your new plan does not allow the SSO feature. " +
$"Disable your SSO configuration.");
}
}
2021-11-17 11:46:35 +01:00
if (!newPlan.HasKeyConnector && organization.UseKeyConnector)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig != null && ssoConfig.GetData().KeyConnectorEnabled)
{
throw new BadRequestException("Your new plan does not allow the Key Connector feature. " +
"Disable your Key Connector.");
}
}
if (!newPlan.HasResetPassword && organization.UseResetPassword)
{
var resetPasswordPolicy =
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
if (resetPasswordPolicy != null && resetPasswordPolicy.Enabled)
{
throw new BadRequestException("Your new plan does not allow the Password Reset feature. " +
"Disable your Password Reset policy.");
}
}
2020-01-15 15:00:54 -05:00
2019-03-21 21:36:03 -04:00
// TODO: Check storage?
string paymentIntentClientSecret = null;
var success = true;
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
2019-03-21 21:36:03 -04:00
{
paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization, newPlan,
upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon, upgrade.TaxInfo);
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
}
else
{
2019-03-21 21:36:03 -04:00
// TODO: Update existing sub
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
}
organization.BusinessName = upgrade.BusinessName;
organization.PlanType = newPlan.Type;
organization.Seats = (short)(newPlan.BaseSeats + upgrade.AdditionalSeats);
organization.MaxCollections = newPlan.MaxCollections;
organization.UseGroups = newPlan.HasGroups;
organization.UseDirectory = newPlan.HasDirectory;
organization.UseEvents = newPlan.HasEvents;
organization.UseTotp = newPlan.HasTotp;
organization.Use2fa = newPlan.Has2fa;
organization.UseApi = newPlan.HasApi;
organization.SelfHost = newPlan.HasSelfHost;
organization.UsePolicies = newPlan.HasPolicies;
organization.MaxStorageGb = !newPlan.BaseStorageGb.HasValue ?
(short?)null : (short)(newPlan.BaseStorageGb.Value + upgrade.AdditionalStorageGb);
organization.UseGroups = newPlan.HasGroups;
organization.UseDirectory = newPlan.HasDirectory;
organization.UseEvents = newPlan.HasEvents;
organization.UseTotp = newPlan.HasTotp;
organization.Use2fa = newPlan.Has2fa;
organization.UseApi = newPlan.HasApi;
organization.UseSso = newPlan.HasSso;
2021-11-17 11:46:35 +01:00
organization.UseKeyConnector = newPlan.HasKeyConnector;
organization.UseResetPassword = newPlan.HasResetPassword;
organization.SelfHost = newPlan.HasSelfHost;
2019-03-21 21:36:03 -04:00
organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon;
organization.Plan = newPlan.Name;
organization.Enabled = success;
organization.PublicKey = upgrade.PublicKey;
organization.PrivateKey = upgrade.PrivateKey;
2019-03-21 21:36:03 -04:00
await ReplaceAndUpdateCache(organization);
if (success)
{
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.UpgradePlan, organization)
{
PlanName = newPlan.Name,
PlanType = newPlan.Type,
OldPlanName = existingPlan.Name,
OldPlanType = existingPlan.Type,
Seats = organization.Seats,
Storage = organization.MaxStorageGb,
});
}
return new Tuple<bool, string>(success, paymentIntentClientSecret);
}
public async Task<string> AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb)
2017-07-11 10:59:59 -04:00
{
var organization = await GetOrgById(organizationId);
if (organization == null)
2017-07-11 10:59:59 -04:00
{
throw new NotFoundException();
}
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
if (plan == null)
2017-07-11 10:59:59 -04:00
{
throw new BadRequestException("Existing plan not found.");
}
if (!plan.HasAdditionalStorageOption)
2017-07-11 10:59:59 -04:00
{
throw new BadRequestException("Plan does not allow additional storage.");
}
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
plan.StripeStoragePlanId);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.AdjustStorage, organization)
{
PlanName = plan.Name,
PlanType = plan.Type,
Storage = storageAdjustmentGb,
});
await ReplaceAndUpdateCache(organization);
return secret;
2017-07-11 10:59:59 -04:00
}
public async Task UpdateSubscription(Guid organizationId, int seatAdjustment, int? maxAutoscaleSeats)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
var newSeatCount = organization.Seats + seatAdjustment;
if (maxAutoscaleSeats.HasValue && newSeatCount > maxAutoscaleSeats.Value)
{
throw new BadRequestException("Cannot set max seat autoscaling below seat count.");
}
if (seatAdjustment != 0)
{
await AdjustSeatsAsync(organization, seatAdjustment);
}
if (maxAutoscaleSeats != organization.MaxAutoscaleSeats)
{
await UpdateAutoscalingAsync(organization, maxAutoscaleSeats);
}
}
private async Task UpdateAutoscalingAsync(Organization organization, int? maxAutoscaleSeats)
{
if (maxAutoscaleSeats.HasValue &&
organization.Seats.HasValue &&
maxAutoscaleSeats.Value < organization.Seats.Value)
{
throw new BadRequestException($"Cannot set max seat autoscaling below current seat count.");
}
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
if (plan == null)
{
throw new BadRequestException("Existing plan not found.");
}
if (!plan.AllowSeatAutoscale)
{
throw new BadRequestException("Your plan does not allow seat autoscaling.");
}
if (plan.MaxUsers.HasValue && maxAutoscaleSeats.HasValue &&
maxAutoscaleSeats > plan.MaxUsers)
{
throw new BadRequestException(string.Concat($"Your plan has a seat limit of {plan.MaxUsers}, ",
$"but you have specified a max autoscale count of {maxAutoscaleSeats}.",
"Reduce your max autoscale seat count."));
}
organization.MaxAutoscaleSeats = maxAutoscaleSeats;
await ReplaceAndUpdateCache(organization);
}
public async Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment, DateTime? prorationDate = null)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
return await AdjustSeatsAsync(organization, seatAdjustment, prorationDate);
}
private async Task<string> AdjustSeatsAsync(Organization organization, int seatAdjustment, DateTime? prorationDate = null, IEnumerable<string> ownerEmails = null)
{
if (organization.Seats == null)
{
throw new BadRequestException("Organization has no seat limit, no need to adjust seats");
}
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{
throw new BadRequestException("No payment method found.");
}
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
throw new BadRequestException("No subscription found.");
}
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
if (plan == null)
{
throw new BadRequestException("Existing plan not found.");
}
if (!plan.HasAdditionalSeatsOption)
{
throw new BadRequestException("Plan does not allow additional seats.");
}
var newSeatTotal = organization.Seats.Value + seatAdjustment;
if (plan.BaseSeats > newSeatTotal)
{
throw new BadRequestException($"Plan has a minimum of {plan.BaseSeats} seats.");
}
if (newSeatTotal <= 0)
2017-05-20 15:33:17 -04:00
{
throw new BadRequestException("You must have at least 1 seat.");
}
var additionalSeats = newSeatTotal - plan.BaseSeats;
if (plan.MaxAdditionalSeats.HasValue && additionalSeats > plan.MaxAdditionalSeats.Value)
{
throw new BadRequestException($"Organization plan allows a maximum of " +
$"{plan.MaxAdditionalSeats.Value} additional seats.");
}
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
if (userCount > newSeatTotal)
{
2019-05-14 11:16:30 -04:00
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
$"Your new plan only has ({newSeatTotal}) seats. Remove some users.");
}
}
var paymentIntentClientSecret = await _paymentService.AdjustSeatsAsync(organization, plan, additionalSeats, prorationDate);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.AdjustSeats, organization)
{
PlanName = plan.Name,
PlanType = plan.Type,
Seats = newSeatTotal,
PreviousSeats = organization.Seats
});
organization.Seats = (short?)newSeatTotal;
await ReplaceAndUpdateCache(organization);
if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue && organization.Seats == organization.MaxAutoscaleSeats)
{
try
{
if (ownerEmails == null)
{
ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,
OrganizationUserType.Owner)).Select(u => u.Email).Distinct();
}
await _mailService.SendOrganizationMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSeats.Value, ownerEmails);
}
catch (Exception e)
{
_logger.LogError(e, "Error encountered notifying organization owners of seat limit reached.");
}
}
return paymentIntentClientSecret;
}
2017-08-14 09:23:54 -04:00
public async Task VerifyBankAsync(Guid organizationId, int amount1, int amount2)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
2017-08-14 09:23:54 -04:00
{
throw new NotFoundException();
}
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
2017-08-14 09:23:54 -04:00
{
throw new GatewayException("Not a gateway customer.");
}
var bankService = new BankAccountService();
var customerService = new CustomerService();
2017-08-14 09:23:54 -04:00
var customer = await customerService.GetAsync(organization.GatewayCustomerId);
if (customer == null)
2017-08-14 09:23:54 -04:00
{
throw new GatewayException("Cannot find customer.");
}
var bankAccount = customer.Sources
.FirstOrDefault(s => s is BankAccount && ((BankAccount)s).Status != "verified") as BankAccount;
if (bankAccount == null)
2017-08-14 09:23:54 -04:00
{
throw new GatewayException("Cannot find an unverified bank account.");
}
try
{
var result = await bankService.VerifyAsync(organization.GatewayCustomerId, bankAccount.Id,
new BankAccountVerifyOptions { Amounts = new List<long> { amount1, amount2 } });
if (result.Status != "verified")
2017-08-14 09:23:54 -04:00
{
throw new GatewayException("Unable to verify account.");
}
}
catch (StripeException e)
2017-08-14 09:23:54 -04:00
{
throw new GatewayException(e.Message);
}
}
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup,
bool provider = false)
{
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan);
if (!(plan is { LegacyYear: null }))
{
throw new BadRequestException("Invalid plan selected.");
}
if (plan.Disabled)
{
throw new BadRequestException("Plan not found.");
}
if (!provider)
{
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
}
2019-03-21 21:36:03 -04:00
ValidateOrganizationUpgradeParameters(plan, signup);
2017-04-08 10:52:10 -04:00
var organization = new Organization
{
2019-01-31 14:25:46 -05:00
// Pre-generate the org id so that we can save it with the Stripe subscription..
Id = CoreHelpers.GenerateComb(),
2017-03-04 21:28:41 -05:00
Name = signup.Name,
2017-04-04 12:57:50 -04:00
BillingEmail = signup.BillingEmail,
BusinessName = signup.BusinessName,
PlanType = plan.Type,
Seats = (short)(plan.BaseSeats + signup.AdditionalSeats),
2017-04-27 09:19:30 -04:00
MaxCollections = plan.MaxCollections,
MaxStorageGb = !plan.BaseStorageGb.HasValue ?
(short?)null : (short)(plan.BaseStorageGb.Value + signup.AdditionalStorageGb),
UsePolicies = plan.HasPolicies,
UseSso = plan.HasSso,
UseGroups = plan.HasGroups,
UseEvents = plan.HasEvents,
UseDirectory = plan.HasDirectory,
UseTotp = plan.HasTotp,
Use2fa = plan.Has2fa,
UseApi = plan.HasApi,
UseResetPassword = plan.HasResetPassword,
SelfHost = plan.HasSelfHost,
UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon,
2017-04-08 16:41:40 -04:00
Plan = plan.Name,
2019-01-31 14:25:46 -05:00
Gateway = null,
ReferenceData = signup.Owner.ReferenceData,
2017-05-20 15:31:16 -04:00
Enabled = true,
LicenseKey = CoreHelpers.SecureRandomString(20),
2019-03-02 15:09:33 -05:00
ApiKey = CoreHelpers.SecureRandomString(30),
PublicKey = signup.PublicKey,
PrivateKey = signup.PrivateKey,
CreationDate = DateTime.UtcNow,
2020-06-25 12:28:22 -04:00
RevisionDate = DateTime.UtcNow,
};
if (plan.Type == PlanType.Free && !provider)
2019-01-31 14:25:46 -05:00
{
var adminCount =
await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id);
if (adminCount > 0)
2019-01-31 14:25:46 -05:00
{
throw new BadRequestException("You can only be an admin of one free organization.");
}
}
else if (plan.Type != PlanType.Free)
2019-01-31 14:25:46 -05:00
{
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
2019-01-31 14:25:46 -05:00
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
2020-06-08 17:40:18 -04:00
signup.PremiumAccessAddon, signup.TaxInfo);
2019-01-31 14:25:46 -05:00
}
var ownerId = provider ? default : signup.Owner.Id;
var returnValue = await SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.Signup, organization)
{
PlanName = plan.Name,
PlanType = plan.Type,
Seats = returnValue.Item1.Seats,
Storage = returnValue.Item1.MaxStorageGb,
});
return returnValue;
2017-08-14 20:57:45 -04:00
}
private async Task ValidateSignUpPoliciesAsync(Guid ownerId)
{
var singleOrgPolicyCount = await _policyRepository.GetCountByTypeApplicableToUserIdAsync(ownerId, PolicyType.SingleOrg);
if (singleOrgPolicyCount > 0)
{
throw new BadRequestException("You may not create an organization. You belong to an organization " +
"which has a policy that prohibits you from being a member of any other organization.");
}
}
2017-08-14 20:57:45 -04:00
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(
OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey,
string privateKey)
2017-08-14 20:57:45 -04:00
{
if (license?.LicenseType != null && license.LicenseType != LicenseType.Organization)
{
throw new BadRequestException("Premium licenses cannot be applied to an organization. "
+ "Upload this license from your personal account settings page.");
}
if (license == null || !_licensingService.VerifyLicense(license))
2017-08-14 20:57:45 -04:00
{
throw new BadRequestException("Invalid license.");
}
if (!license.CanUse(_globalSettings))
{
throw new BadRequestException("Invalid license. Make sure your license allows for on-premise " +
2017-08-16 15:45:38 -04:00
"hosting of organizations and that the installation id matches your current installation.");
}
if (license.PlanType != PlanType.Custom &&
StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType && !p.Disabled) == null)
2017-08-14 20:57:45 -04:00
{
throw new BadRequestException("Plan not found.");
}
var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();
if (enabledOrgs.Any(o => o.LicenseKey.Equals(license.LicenseKey)))
{
throw new BadRequestException("License is already in use by another organization.");
}
await ValidateSignUpPoliciesAsync(owner.Id);
2017-08-14 20:57:45 -04:00
var organization = new Organization
{
Name = license.Name,
2017-08-16 13:58:52 -04:00
BillingEmail = license.BillingEmail,
BusinessName = license.BusinessName,
2017-08-14 20:57:45 -04:00
PlanType = license.PlanType,
Seats = license.Seats,
MaxCollections = license.MaxCollections,
2017-08-16 13:58:52 -04:00
MaxStorageGb = _globalSettings.SelfHosted ? 10240 : license.MaxStorageGb, // 10 TB
2020-01-15 15:00:54 -05:00
UsePolicies = license.UsePolicies,
UseSso = license.UseSso,
2021-11-17 11:46:35 +01:00
UseKeyConnector = license.UseKeyConnector,
2017-08-14 20:57:45 -04:00
UseGroups = license.UseGroups,
UseDirectory = license.UseDirectory,
2017-12-14 15:48:44 -05:00
UseEvents = license.UseEvents,
2017-08-14 20:57:45 -04:00
UseTotp = license.UseTotp,
Use2fa = license.Use2fa,
2019-03-02 15:09:33 -05:00
UseApi = license.UseApi,
UseResetPassword = license.UseResetPassword,
2017-08-14 20:57:45 -04:00
Plan = license.Plan,
SelfHost = license.SelfHost,
UsersGetPremium = license.UsersGetPremium,
2017-08-14 20:57:45 -04:00
Gateway = null,
GatewayCustomerId = null,
GatewaySubscriptionId = null,
ReferenceData = owner.ReferenceData,
2017-08-16 13:58:52 -04:00
Enabled = license.Enabled,
2017-08-14 20:57:45 -04:00
ExpirationDate = license.Expires,
LicenseKey = license.LicenseKey,
2019-03-02 15:09:33 -05:00
ApiKey = CoreHelpers.SecureRandomString(30),
PublicKey = publicKey,
PrivateKey = privateKey,
2017-08-14 20:57:45 -04:00
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow
};
2017-08-30 21:25:46 -04:00
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
var dir = $"{_globalSettings.LicenseDirectory}/organization";
Directory.CreateDirectory(dir);
using var fs = System.IO.File.OpenWrite(Path.Combine(dir, $"{organization.Id}.json"));
await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);
return result;
2017-08-14 20:57:45 -04:00
}
private async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(Organization organization,
2017-08-30 21:25:46 -04:00
Guid ownerId, string ownerKey, string collectionName, bool withPayment)
2017-08-14 20:57:45 -04:00
{
try
{
2017-04-04 12:57:50 -04:00
await _organizationRepository.CreateAsync(organization);
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
2017-04-04 12:57:50 -04:00
if (!string.IsNullOrWhiteSpace(collectionName))
{
2017-08-30 21:25:46 -04:00
var defaultCollection = new Collection
{
Name = collectionName,
OrganizationId = organization.Id,
CreationDate = organization.CreationDate,
RevisionDate = organization.CreationDate
};
await _collectionRepository.CreateAsync(defaultCollection);
}
OrganizationUser orgUser = null;
if (ownerId != default)
{
orgUser = new OrganizationUser
{
OrganizationId = organization.Id,
UserId = ownerId,
Key = ownerKey,
Type = OrganizationUserType.Owner,
Status = OrganizationUserStatusType.Confirmed,
AccessAll = true,
CreationDate = organization.CreationDate,
RevisionDate = organization.CreationDate
};
await _organizationUserRepository.CreateAsync(orgUser);
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds,
organization.Id.ToString());
await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
}
2017-04-21 22:39:46 -04:00
return new Tuple<Organization, OrganizationUser>(organization, orgUser);
}
catch
{
if (withPayment)
2017-08-14 20:57:45 -04:00
{
2019-02-08 23:53:09 -05:00
await _paymentService.CancelAndRecoverChargesAsync(organization);
2017-08-14 20:57:45 -04:00
}
if (organization.Id != default(Guid))
2017-04-04 12:57:50 -04:00
{
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
2017-04-04 12:57:50 -04:00
}
throw;
}
}
2017-03-04 21:28:41 -05:00
2017-08-14 21:25:06 -04:00
public async Task UpdateLicenseAsync(Guid organizationId, OrganizationLicense license)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
2017-08-14 21:25:06 -04:00
{
throw new NotFoundException();
}
if (!_globalSettings.SelfHosted)
2017-08-14 21:25:06 -04:00
{
throw new InvalidOperationException("Licenses require self hosting.");
}
if (license?.LicenseType != null && license.LicenseType != LicenseType.Organization)
{
throw new BadRequestException("Premium licenses cannot be applied to an organization. "
+ "Upload this license from your personal account settings page.");
}
if (license == null || !_licensingService.VerifyLicense(license))
2017-08-14 21:25:06 -04:00
{
throw new BadRequestException("Invalid license.");
}
if (!license.CanUse(_globalSettings))
2017-08-14 21:25:06 -04:00
{
throw new BadRequestException("Invalid license. Make sure your license allows for on-premise " +
2017-08-16 15:45:38 -04:00
"hosting of organizations and that the installation id matches your current installation.");
2017-08-14 21:25:06 -04:00
}
var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();
if (enabledOrgs.Any(o => o.LicenseKey.Equals(license.LicenseKey) && o.Id != organizationId))
{
throw new BadRequestException("License is already in use by another organization.");
}
if (license.Seats.HasValue &&
2019-05-14 11:16:30 -04:00
(!organization.Seats.HasValue || organization.Seats.Value > license.Seats.Value))
2017-08-14 21:25:06 -04:00
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
if (userCount > license.Seats.Value)
2017-08-14 21:25:06 -04:00
{
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
$"Your new license only has ({ license.Seats.Value}) seats. Remove some users.");
}
}
if (license.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
2019-05-14 11:16:30 -04:00
organization.MaxCollections.Value > license.MaxCollections.Value))
2017-08-14 21:25:06 -04:00
{
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
if (collectionCount > license.MaxCollections.Value)
2017-08-14 21:25:06 -04:00
{
throw new BadRequestException($"Your organization currently has {collectionCount} collections. " +
$"Your new license allows for a maximum of ({license.MaxCollections.Value}) collections. " +
"Remove some collections.");
}
}
if (!license.UseGroups && organization.UseGroups)
{
var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id);
if (groups.Count > 0)
{
throw new BadRequestException($"Your organization currently has {groups.Count} groups. " +
$"Your new license does not allow for the use of groups. Remove all groups.");
}
}
2017-08-14 21:25:06 -04:00
if (!license.UsePolicies && organization.UsePolicies)
2020-01-15 15:00:54 -05:00
{
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id);
if (policies.Any(p => p.Enabled))
2020-01-15 15:00:54 -05:00
{
throw new BadRequestException($"Your organization currently has {policies.Count} enabled " +
$"policies. Your new license does not allow for the use of policies. Disable all policies.");
}
}
if (!license.UseSso && organization.UseSso)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig != null && ssoConfig.Enabled)
{
throw new BadRequestException($"Your organization currently has a SSO configuration. " +
$"Your new license does not allow for the use of SSO. Disable your SSO configuration.");
}
}
2021-11-17 11:46:35 +01:00
if (!license.UseKeyConnector && organization.UseKeyConnector)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig != null && ssoConfig.GetData().KeyConnectorEnabled)
{
throw new BadRequestException($"Your organization currently has Key Connector enabled. " +
$"Your new license does not allow for the use of Key Connector. Disable your Key Connector.");
}
}
if (!license.UseResetPassword && organization.UseResetPassword)
{
var resetPasswordPolicy =
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
if (resetPasswordPolicy != null && resetPasswordPolicy.Enabled)
{
throw new BadRequestException("Your new license does not allow the Password Reset feature. "
+ "Disable your Password Reset policy.");
}
}
2020-01-15 15:00:54 -05:00
2017-08-14 21:25:06 -04:00
var dir = $"{_globalSettings.LicenseDirectory}/organization";
Directory.CreateDirectory(dir);
using var fs = System.IO.File.OpenWrite(Path.Combine(dir, $"{organization.Id}.json"));
await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);
2017-08-14 21:25:06 -04:00
organization.Name = license.Name;
2017-08-16 13:58:52 -04:00
organization.BusinessName = license.BusinessName;
organization.BillingEmail = license.BillingEmail;
2017-08-14 21:25:06 -04:00
organization.PlanType = license.PlanType;
organization.Seats = license.Seats;
organization.MaxCollections = license.MaxCollections;
organization.UseGroups = license.UseGroups;
organization.UseDirectory = license.UseDirectory;
2017-12-14 15:48:44 -05:00
organization.UseEvents = license.UseEvents;
2017-08-14 21:25:06 -04:00
organization.UseTotp = license.UseTotp;
organization.Use2fa = license.Use2fa;
2019-03-02 15:09:33 -05:00
organization.UseApi = license.UseApi;
2020-03-03 22:32:59 -05:00
organization.UsePolicies = license.UsePolicies;
organization.UseSso = license.UseSso;
2021-11-17 11:46:35 +01:00
organization.UseKeyConnector = license.UseKeyConnector;
organization.UseResetPassword = license.UseResetPassword;
organization.SelfHost = license.SelfHost;
organization.UsersGetPremium = license.UsersGetPremium;
2017-08-14 21:25:06 -04:00
organization.Plan = license.Plan;
2017-08-16 13:58:52 -04:00
organization.Enabled = license.Enabled;
2017-08-14 21:25:06 -04:00
organization.ExpirationDate = license.Expires;
organization.LicenseKey = license.LicenseKey;
organization.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(organization);
2017-08-14 21:25:06 -04:00
}
2017-04-11 10:52:28 -04:00
public async Task DeleteAsync(Organization organization)
{
await ValidateDeleteOrganizationAsync(organization);
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
2017-04-11 10:52:28 -04:00
{
try
{
2018-12-31 14:07:19 -05:00
var eop = !organization.ExpirationDate.HasValue ||
organization.ExpirationDate.Value >= DateTime.UtcNow;
2019-02-08 23:53:09 -05:00
await _paymentService.CancelSubscriptionAsync(organization, eop);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.DeleteAccount, organization));
}
catch (GatewayException) { }
2017-04-11 10:52:28 -04:00
}
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
2017-04-11 10:52:28 -04:00
}
public async Task EnableAsync(Guid organizationId, DateTime? expirationDate)
{
var org = await GetOrgById(organizationId);
if (org != null && !org.Enabled && org.Gateway.HasValue)
{
org.Enabled = true;
org.ExpirationDate = expirationDate;
org.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(org);
}
}
2017-08-12 22:16:42 -04:00
public async Task DisableAsync(Guid organizationId, DateTime? expirationDate)
{
var org = await GetOrgById(organizationId);
if (org != null && org.Enabled)
{
org.Enabled = false;
2017-08-12 22:16:42 -04:00
org.ExpirationDate = expirationDate;
org.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(org);
// TODO: send email to owners?
}
}
2017-08-12 22:16:42 -04:00
public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate)
{
var org = await GetOrgById(organizationId);
if (org != null)
2017-08-12 22:16:42 -04:00
{
org.ExpirationDate = expirationDate;
org.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(org);
2017-08-12 22:16:42 -04:00
}
}
public async Task EnableAsync(Guid organizationId)
{
var org = await GetOrgById(organizationId);
if (org != null && !org.Enabled)
{
org.Enabled = true;
await ReplaceAndUpdateCache(org);
}
}
2017-04-10 19:07:38 -04:00
public async Task UpdateAsync(Organization organization, bool updateBilling = false)
{
if (organization.Id == default(Guid))
2017-04-10 19:07:38 -04:00
{
throw new ApplicationException("Cannot create org this way. Call SignUpAsync.");
}
if (!string.IsNullOrWhiteSpace(organization.Identifier))
{
var orgById = await _organizationRepository.GetByIdentifierAsync(organization.Identifier);
if (orgById != null && orgById.Id != organization.Id)
{
throw new BadRequestException("Identifier already in use by another organization.");
}
}
await ReplaceAndUpdateCache(organization, EventType.Organization_Updated);
2017-04-10 19:07:38 -04:00
if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
2017-04-10 19:07:38 -04:00
{
var customerService = new CustomerService();
await customerService.UpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
2017-04-10 19:07:38 -04:00
{
Email = organization.BillingEmail,
Description = organization.BusinessName
});
}
}
2018-04-02 23:18:26 -04:00
public async Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type)
{
if (!type.ToString().Contains("Organization"))
2018-04-02 23:18:26 -04:00
{
throw new ArgumentException("Not an organization provider type.");
}
if (!organization.Use2fa)
2018-04-02 23:18:26 -04:00
{
throw new BadRequestException("Organization cannot use 2FA.");
}
var providers = organization.GetTwoFactorProviders();
if (!providers?.ContainsKey(type) ?? true)
2018-04-02 23:18:26 -04:00
{
return;
}
providers[type].Enabled = true;
organization.SetTwoFactorProviders(providers);
await UpdateAsync(organization);
}
public async Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type)
{
if (!type.ToString().Contains("Organization"))
2018-04-02 23:18:26 -04:00
{
throw new ArgumentException("Not an organization provider type.");
}
var providers = organization.GetTwoFactorProviders();
if (!providers?.ContainsKey(type) ?? true)
2018-04-02 23:18:26 -04:00
{
return;
}
providers.Remove(type);
organization.SetTwoFactorProviders(providers);
await UpdateAsync(organization);
}
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,
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>
2021-05-17 09:43:02 -05:00
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
{
var organization = await GetOrgById(organizationId);
var initialSeatCount = organization.Seats;
if (organization == null || invites.Any(i => i.invite.Emails == null))
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>
2021-05-17 09:43:02 -05:00
{
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 ValidateOrganizationUserUpdatePermissions(organizationId, type, null);
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>
2021-05-17 09:43:02 -05:00
}
}
var newSeatsRequired = 0;
var existingEmails = new HashSet<string>(await _organizationUserRepository.SelectKnownEmailsAsync(
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
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>
2021-05-17 09:43:02 -05:00
if (organization.Seats.HasValue)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
var availableSeats = organization.Seats.Value - userCount;
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
}
if (newSeatsRequired > 0)
{
var (canScale, failureReason) = CanScale(organization, newSeatsRequired);
if (!canScale)
{
throw new BadRequestException(failureReason);
}
}
var invitedAreAllOwners = invites.All(i => i.invite.Type == OrganizationUserType.Owner);
if (!invitedAreAllOwners && !await HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
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>
2021-05-17 09:43:02 -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>
2021-05-17 09:43:02 -05:00
var orgUsers = new List<OrganizationUser>();
var limitedCollectionOrgUsers = new List<(OrganizationUser, IEnumerable<SelectionReadOnly>)>();
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>
2021-05-17 09:43:02 -05:00
var orgUserInvitedCount = 0;
var exceptions = new List<Exception>();
var events = new List<(OrganizationUser, EventType, DateTime?)>();
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 = JsonSerializer.Serialize(invite.Permissions, JsonHelpers.CamelCase);
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>
2021-05-17 09:43:02 -05:00
}
if (!orgUser.AccessAll && invite.Collections.Any())
{
limitedCollectionOrgUsers.Add((orgUser, invite.Collections));
}
else
{
orgUsers.Add(orgUser);
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>
2021-05-17 09:43:02 -05:00
}
events.Add((orgUser, EventType.OrganizationUser_Invited, DateTime.UtcNow));
orgUserInvitedCount++;
}
catch (Exception e)
{
exceptions.Add(e);
}
}
}
if (exceptions.Any())
{
throw new AggregateException("One or more errors occurred while inviting users.", exceptions);
}
var prorationDate = DateTime.UtcNow;
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>
2021-05-17 09:43:02 -05:00
try
{
await _organizationUserRepository.CreateManyAsync(orgUsers);
foreach (var (orgUser, collections) in limitedCollectionOrgUsers)
{
await _organizationUserRepository.CreateAsync(orgUser, collections);
}
if (!await _currentContext.ManageUsers(organization.Id))
{
throw new BadRequestException("Cannot add seats. Cannot manage organization users.");
}
await AutoAddSeatsAsync(organization, newSeatsRequired, prorationDate);
await SendInvitesAsync(orgUsers.Concat(limitedCollectionOrgUsers.Select(u => u.Item1)), organization);
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>
2021-05-17 09:43:02 -05:00
await _eventService.LogOrganizationUserEventsAsync(events);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization)
{
Users = orgUserInvitedCount
});
}
catch (Exception e)
{
// Revert any added users.
var invitedOrgUserIds = orgUsers.Select(u => u.Id).Concat(limitedCollectionOrgUsers.Select(u => u.Item1.Id));
await _organizationUserRepository.DeleteManyAsync(invitedOrgUserIds);
var currentSeatCount = (await _organizationRepository.GetByIdAsync(organization.Id)).Seats;
if (initialSeatCount.HasValue && currentSeatCount.HasValue && currentSeatCount.Value != initialSeatCount.Value)
{
await AdjustSeatsAsync(organization, initialSeatCount.Value - currentSeatCount.Value, prorationDate);
}
exceptions.Add(e);
}
if (exceptions.Any())
2017-03-28 21:16:19 -04:00
{
throw new AggregateException("One or more errors occurred while inviting users.", exceptions);
}
2017-03-04 21:28:41 -05:00
2017-05-18 12:04:27 -04:00
return orgUsers;
2017-03-04 21:28:41 -05:00
}
public async Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId,
IEnumerable<Guid> organizationUsersId)
{
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
var org = await GetOrgById(organizationId);
var result = new List<Tuple<OrganizationUser, string>>();
foreach (var orgUser in orgUsers)
{
if (orgUser.Status != OrganizationUserStatusType.Invited || orgUser.OrganizationId != organizationId)
{
result.Add(Tuple.Create(orgUser, "User invalid."));
continue;
}
await SendInviteAsync(orgUser, org);
result.Add(Tuple.Create(orgUser, ""));
}
return result;
}
2019-10-07 16:23:38 -04:00
public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId)
{
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null || orgUser.OrganizationId != organizationId ||
orgUser.Status != OrganizationUserStatusType.Invited)
{
throw new BadRequestException("User invalid.");
}
var org = await GetOrgById(orgUser.OrganizationId);
await SendInviteAsync(orgUser, org);
}
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>
2021-05-17 09:43:02 -05:00
private async Task SendInvitesAsync(IEnumerable<OrganizationUser> orgUsers, Organization organization)
{
string MakeToken(OrganizationUser orgUser) =>
_dataProtector.Protect($"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
2021-12-16 15:35:09 +01:00
Families for Enterprise (#1714) * Create common test infrastructure project * Add helpers to further type PlanTypes * Enable testing of ASP.net MVC controllers Controller properties have all kinds of validations in the background. In general, we don't user properties on our Controllers, so the easiest way to allow for Autofixture-based testing of our Controllers is to just omit setting all properties on them. * Workaround for broken MemberAutoDataAttribute https://github.com/AutoFixture/AutoFixture/pull/1164 shows that only the first test case is pulled for this attribute. This is a workaround that populates the provided parameters, left to right, using AutoFixture to populate any remaining. * WIP: Organization sponsorship flow * Add Attribute to use the Bit Autodata dependency chain BitAutoDataAttribute is used to mark a Theory as autopopulating parameters. Extract common attribute methods to to a helper class. Cannot inherit a common base, since both require inheriting from different Xunit base classes to work. * WIP: scaffolding for families for enterprise sponsorship flow * Fix broken tests * Create sponsorship offer (#1688) * Initial db work (#1687) * Add organization sponsorship databases to all providers * Generalize create and update for database, specialize in code * Add PlanSponsorshipType to db model * Write valid json for test entries * Initial scaffolding of emails (#1686) * Initial scaffolding of emails * Work on adding models for FamilyForEnterprise emails * Switch verbage * Put preliminary copy in emails * Skip test * Families for enterprise/stripe integrations (#1699) * Add PlanSponsorshipType to static store * Add sponsorship type to token and creates sponsorship * PascalCase properties * Require sponsorship for remove * Create subscription sponsorship helper class * Handle Sponsored subscription changes * Add sponsorship id to subscription metadata * Make sponsoring references nullable This state indicates that a sponsorship has lapsed, but was not able to be reverted for billing reasons * WIP: Validate and remove subscriptions * Update sponsorships on organization and org user delete * Add friendly name to organization sponsorship * Add sponsorship available boolean to orgDetails * Add sponsorship service to DI * Use userId to find org users * Send f4e offer email * Simplify names of f4e mail messages * Fix Stripe org default tax rates * Universal sponsorship redeem api * Populate user in current context * Add product type to organization details * Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate * Use organization and auth to find organization sponsorship * Add resend sponsorship offer api endpoint * Fix double email send * Fix sponsorship upgrade options * Add is sponsored item to subscription response * Add sponsorship validation to upcoming invoice webhook * Add sponsorship validation to upcoming invoice webhook * Fix organization delete sponsorship hooks * Test org sponsorship service * Fix sproc * Create common test infrastructure project * Add helpers to further type PlanTypes * Enable testing of ASP.net MVC controllers Controller properties have all kinds of validations in the background. In general, we don't user properties on our Controllers, so the easiest way to allow for Autofixture-based testing of our Controllers is to just omit setting all properties on them. * Workaround for broken MemberAutoDataAttribute https://github.com/AutoFixture/AutoFixture/pull/1164 shows that only the first test case is pulled for this attribute. This is a workaround that populates the provided parameters, left to right, using AutoFixture to populate any remaining. * WIP: Organization sponsorship flow * Add Attribute to use the Bit Autodata dependency chain BitAutoDataAttribute is used to mark a Theory as autopopulating parameters. Extract common attribute methods to to a helper class. Cannot inherit a common base, since both require inheriting from different Xunit base classes to work. * WIP: scaffolding for families for enterprise sponsorship flow * Fix broken tests * Create sponsorship offer (#1688) * Initial db work (#1687) * Add organization sponsorship databases to all providers * Generalize create and update for database, specialize in code * Add PlanSponsorshipType to db model * Write valid json for test entries * Initial scaffolding of emails (#1686) * Initial scaffolding of emails * Work on adding models for FamilyForEnterprise emails * Switch verbage * Put preliminary copy in emails * Skip test * Families for enterprise/stripe integrations (#1699) * Add PlanSponsorshipType to static store * Add sponsorship type to token and creates sponsorship * PascalCase properties * Require sponsorship for remove * Create subscription sponsorship helper class * Handle Sponsored subscription changes * Add sponsorship id to subscription metadata * Make sponsoring references nullable This state indicates that a sponsorship has lapsed, but was not able to be reverted for billing reasons * WIP: Validate and remove subscriptions * Update sponsorships on organization and org user delete * Add friendly name to organization sponsorship * Add sponsorship available boolean to orgDetails * Add sponsorship service to DI * Use userId to find org users * Send f4e offer email * Simplify names of f4e mail messages * Fix Stripe org default tax rates * Universal sponsorship redeem api * Populate user in current context * Add product type to organization details * Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate * Use organization and auth to find organization sponsorship * Add resend sponsorship offer api endpoint * Fix double email send * Fix sponsorship upgrade options * Add is sponsored item to subscription response * Add sponsorship validation to upcoming invoice webhook * Add sponsorship validation to upcoming invoice webhook * Fix organization delete sponsorship hooks * Test org sponsorship service * Fix sproc * Fix build error * Update emails * Fix tests * Skip local test * Add newline * Fix stripe subscription update * Finish emails * Skip test * Fix unit tests * Remove unused variable * Fix unit tests * Switch to handlebars ifs * Remove ending email * Remove reconfirmation template * Switch naming convention * Switch naming convention * Fix migration * Update copy and links * Switch to using Guid in the method * Remove unneeded css styles * Add sql files to Sql.sqlproj * Removed old comments * Made name more verbose * Fix SQL error * Move unit tests to service * Fix sp * Revert "Move unit tests to service" This reverts commit 1185bf3ec8ca36ccd75717ed2463adf8885159a6. * Do repository validation in service layer * Fix tests * Fix merge conflicts and remove TODO * Remove unneeded models * Fix spacing and formatting * Switch Org -> Organization * Remove single use variables * Switch method name * Fix Controller * Switch to obfuscating email * Fix unit tests Co-authored-by: Justin Baur <admin@justinbaur.com>
2021-11-19 16:25:06 -06:00
await _mailService.BulkSendOrganizationInviteEmailAsync(organization.Name, CheckOrganizationCanSponsor(organization),
orgUsers.Select(o => (o, new ExpiringToken(MakeToken(o), DateTime.UtcNow.AddDays(5)))));
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>
2021-05-17 09:43:02 -05:00
}
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization)
{
var now = DateTime.UtcNow;
var nowMillis = CoreHelpers.ToEpocMilliseconds(now);
var token = _dataProtector.Protect(
2017-03-23 11:51:37 -04:00
$"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {nowMillis}");
2021-12-16 15:35:09 +01:00
Families for Enterprise (#1714) * Create common test infrastructure project * Add helpers to further type PlanTypes * Enable testing of ASP.net MVC controllers Controller properties have all kinds of validations in the background. In general, we don't user properties on our Controllers, so the easiest way to allow for Autofixture-based testing of our Controllers is to just omit setting all properties on them. * Workaround for broken MemberAutoDataAttribute https://github.com/AutoFixture/AutoFixture/pull/1164 shows that only the first test case is pulled for this attribute. This is a workaround that populates the provided parameters, left to right, using AutoFixture to populate any remaining. * WIP: Organization sponsorship flow * Add Attribute to use the Bit Autodata dependency chain BitAutoDataAttribute is used to mark a Theory as autopopulating parameters. Extract common attribute methods to to a helper class. Cannot inherit a common base, since both require inheriting from different Xunit base classes to work. * WIP: scaffolding for families for enterprise sponsorship flow * Fix broken tests * Create sponsorship offer (#1688) * Initial db work (#1687) * Add organization sponsorship databases to all providers * Generalize create and update for database, specialize in code * Add PlanSponsorshipType to db model * Write valid json for test entries * Initial scaffolding of emails (#1686) * Initial scaffolding of emails * Work on adding models for FamilyForEnterprise emails * Switch verbage * Put preliminary copy in emails * Skip test * Families for enterprise/stripe integrations (#1699) * Add PlanSponsorshipType to static store * Add sponsorship type to token and creates sponsorship * PascalCase properties * Require sponsorship for remove * Create subscription sponsorship helper class * Handle Sponsored subscription changes * Add sponsorship id to subscription metadata * Make sponsoring references nullable This state indicates that a sponsorship has lapsed, but was not able to be reverted for billing reasons * WIP: Validate and remove subscriptions * Update sponsorships on organization and org user delete * Add friendly name to organization sponsorship * Add sponsorship available boolean to orgDetails * Add sponsorship service to DI * Use userId to find org users * Send f4e offer email * Simplify names of f4e mail messages * Fix Stripe org default tax rates * Universal sponsorship redeem api * Populate user in current context * Add product type to organization details * Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate * Use organization and auth to find organization sponsorship * Add resend sponsorship offer api endpoint * Fix double email send * Fix sponsorship upgrade options * Add is sponsored item to subscription response * Add sponsorship validation to upcoming invoice webhook * Add sponsorship validation to upcoming invoice webhook * Fix organization delete sponsorship hooks * Test org sponsorship service * Fix sproc * Create common test infrastructure project * Add helpers to further type PlanTypes * Enable testing of ASP.net MVC controllers Controller properties have all kinds of validations in the background. In general, we don't user properties on our Controllers, so the easiest way to allow for Autofixture-based testing of our Controllers is to just omit setting all properties on them. * Workaround for broken MemberAutoDataAttribute https://github.com/AutoFixture/AutoFixture/pull/1164 shows that only the first test case is pulled for this attribute. This is a workaround that populates the provided parameters, left to right, using AutoFixture to populate any remaining. * WIP: Organization sponsorship flow * Add Attribute to use the Bit Autodata dependency chain BitAutoDataAttribute is used to mark a Theory as autopopulating parameters. Extract common attribute methods to to a helper class. Cannot inherit a common base, since both require inheriting from different Xunit base classes to work. * WIP: scaffolding for families for enterprise sponsorship flow * Fix broken tests * Create sponsorship offer (#1688) * Initial db work (#1687) * Add organization sponsorship databases to all providers * Generalize create and update for database, specialize in code * Add PlanSponsorshipType to db model * Write valid json for test entries * Initial scaffolding of emails (#1686) * Initial scaffolding of emails * Work on adding models for FamilyForEnterprise emails * Switch verbage * Put preliminary copy in emails * Skip test * Families for enterprise/stripe integrations (#1699) * Add PlanSponsorshipType to static store * Add sponsorship type to token and creates sponsorship * PascalCase properties * Require sponsorship for remove * Create subscription sponsorship helper class * Handle Sponsored subscription changes * Add sponsorship id to subscription metadata * Make sponsoring references nullable This state indicates that a sponsorship has lapsed, but was not able to be reverted for billing reasons * WIP: Validate and remove subscriptions * Update sponsorships on organization and org user delete * Add friendly name to organization sponsorship * Add sponsorship available boolean to orgDetails * Add sponsorship service to DI * Use userId to find org users * Send f4e offer email * Simplify names of f4e mail messages * Fix Stripe org default tax rates * Universal sponsorship redeem api * Populate user in current context * Add product type to organization details * Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate * Use organization and auth to find organization sponsorship * Add resend sponsorship offer api endpoint * Fix double email send * Fix sponsorship upgrade options * Add is sponsored item to subscription response * Add sponsorship validation to upcoming invoice webhook * Add sponsorship validation to upcoming invoice webhook * Fix organization delete sponsorship hooks * Test org sponsorship service * Fix sproc * Fix build error * Update emails * Fix tests * Skip local test * Add newline * Fix stripe subscription update * Finish emails * Skip test * Fix unit tests * Remove unused variable * Fix unit tests * Switch to handlebars ifs * Remove ending email * Remove reconfirmation template * Switch naming convention * Switch naming convention * Fix migration * Update copy and links * Switch to using Guid in the method * Remove unneeded css styles * Add sql files to Sql.sqlproj * Removed old comments * Made name more verbose * Fix SQL error * Move unit tests to service * Fix sp * Revert "Move unit tests to service" This reverts commit 1185bf3ec8ca36ccd75717ed2463adf8885159a6. * Do repository validation in service layer * Fix tests * Fix merge conflicts and remove TODO * Remove unneeded models * Fix spacing and formatting * Switch Org -> Organization * Remove single use variables * Switch method name * Fix Controller * Switch to obfuscating email * Fix unit tests Co-authored-by: Justin Baur <admin@justinbaur.com>
2021-11-19 16:25:06 -06:00
await _mailService.SendOrganizationInviteEmailAsync(organization.Name, CheckOrganizationCanSponsor(organization), orgUser, new ExpiringToken(token, now.AddDays(5)));
}
private bool CheckOrganizationCanSponsor(Organization organization)
Families for Enterprise (#1714) * Create common test infrastructure project * Add helpers to further type PlanTypes * Enable testing of ASP.net MVC controllers Controller properties have all kinds of validations in the background. In general, we don't user properties on our Controllers, so the easiest way to allow for Autofixture-based testing of our Controllers is to just omit setting all properties on them. * Workaround for broken MemberAutoDataAttribute https://github.com/AutoFixture/AutoFixture/pull/1164 shows that only the first test case is pulled for this attribute. This is a workaround that populates the provided parameters, left to right, using AutoFixture to populate any remaining. * WIP: Organization sponsorship flow * Add Attribute to use the Bit Autodata dependency chain BitAutoDataAttribute is used to mark a Theory as autopopulating parameters. Extract common attribute methods to to a helper class. Cannot inherit a common base, since both require inheriting from different Xunit base classes to work. * WIP: scaffolding for families for enterprise sponsorship flow * Fix broken tests * Create sponsorship offer (#1688) * Initial db work (#1687) * Add organization sponsorship databases to all providers * Generalize create and update for database, specialize in code * Add PlanSponsorshipType to db model * Write valid json for test entries * Initial scaffolding of emails (#1686) * Initial scaffolding of emails * Work on adding models for FamilyForEnterprise emails * Switch verbage * Put preliminary copy in emails * Skip test * Families for enterprise/stripe integrations (#1699) * Add PlanSponsorshipType to static store * Add sponsorship type to token and creates sponsorship * PascalCase properties * Require sponsorship for remove * Create subscription sponsorship helper class * Handle Sponsored subscription changes * Add sponsorship id to subscription metadata * Make sponsoring references nullable This state indicates that a sponsorship has lapsed, but was not able to be reverted for billing reasons * WIP: Validate and remove subscriptions * Update sponsorships on organization and org user delete * Add friendly name to organization sponsorship * Add sponsorship available boolean to orgDetails * Add sponsorship service to DI * Use userId to find org users * Send f4e offer email * Simplify names of f4e mail messages * Fix Stripe org default tax rates * Universal sponsorship redeem api * Populate user in current context * Add product type to organization details * Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate * Use organization and auth to find organization sponsorship * Add resend sponsorship offer api endpoint * Fix double email send * Fix sponsorship upgrade options * Add is sponsored item to subscription response * Add sponsorship validation to upcoming invoice webhook * Add sponsorship validation to upcoming invoice webhook * Fix organization delete sponsorship hooks * Test org sponsorship service * Fix sproc * Create common test infrastructure project * Add helpers to further type PlanTypes * Enable testing of ASP.net MVC controllers Controller properties have all kinds of validations in the background. In general, we don't user properties on our Controllers, so the easiest way to allow for Autofixture-based testing of our Controllers is to just omit setting all properties on them. * Workaround for broken MemberAutoDataAttribute https://github.com/AutoFixture/AutoFixture/pull/1164 shows that only the first test case is pulled for this attribute. This is a workaround that populates the provided parameters, left to right, using AutoFixture to populate any remaining. * WIP: Organization sponsorship flow * Add Attribute to use the Bit Autodata dependency chain BitAutoDataAttribute is used to mark a Theory as autopopulating parameters. Extract common attribute methods to to a helper class. Cannot inherit a common base, since both require inheriting from different Xunit base classes to work. * WIP: scaffolding for families for enterprise sponsorship flow * Fix broken tests * Create sponsorship offer (#1688) * Initial db work (#1687) * Add organization sponsorship databases to all providers * Generalize create and update for database, specialize in code * Add PlanSponsorshipType to db model * Write valid json for test entries * Initial scaffolding of emails (#1686) * Initial scaffolding of emails * Work on adding models for FamilyForEnterprise emails * Switch verbage * Put preliminary copy in emails * Skip test * Families for enterprise/stripe integrations (#1699) * Add PlanSponsorshipType to static store * Add sponsorship type to token and creates sponsorship * PascalCase properties * Require sponsorship for remove * Create subscription sponsorship helper class * Handle Sponsored subscription changes * Add sponsorship id to subscription metadata * Make sponsoring references nullable This state indicates that a sponsorship has lapsed, but was not able to be reverted for billing reasons * WIP: Validate and remove subscriptions * Update sponsorships on organization and org user delete * Add friendly name to organization sponsorship * Add sponsorship available boolean to orgDetails * Add sponsorship service to DI * Use userId to find org users * Send f4e offer email * Simplify names of f4e mail messages * Fix Stripe org default tax rates * Universal sponsorship redeem api * Populate user in current context * Add product type to organization details * Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate * Use organization and auth to find organization sponsorship * Add resend sponsorship offer api endpoint * Fix double email send * Fix sponsorship upgrade options * Add is sponsored item to subscription response * Add sponsorship validation to upcoming invoice webhook * Add sponsorship validation to upcoming invoice webhook * Fix organization delete sponsorship hooks * Test org sponsorship service * Fix sproc * Fix build error * Update emails * Fix tests * Skip local test * Add newline * Fix stripe subscription update * Finish emails * Skip test * Fix unit tests * Remove unused variable * Fix unit tests * Switch to handlebars ifs * Remove ending email * Remove reconfirmation template * Switch naming convention * Switch naming convention * Fix migration * Update copy and links * Switch to using Guid in the method * Remove unneeded css styles * Add sql files to Sql.sqlproj * Removed old comments * Made name more verbose * Fix SQL error * Move unit tests to service * Fix sp * Revert "Move unit tests to service" This reverts commit 1185bf3ec8ca36ccd75717ed2463adf8885159a6. * Do repository validation in service layer * Fix tests * Fix merge conflicts and remove TODO * Remove unneeded models * Fix spacing and formatting * Switch Org -> Organization * Remove single use variables * Switch method name * Fix Controller * Switch to obfuscating email * Fix unit tests Co-authored-by: Justin Baur <admin@justinbaur.com>
2021-11-19 16:25:06 -06:00
{
return StaticStore.GetPlan(organization.PlanType).Product == ProductType.Enterprise
&& !_globalSettings.SelfHosted;
}
2020-02-19 14:56:16 -05:00
public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
IUserService userService)
2017-03-04 21:28:41 -05:00
{
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null)
2017-03-04 21:28:41 -05:00
{
throw new BadRequestException("User invalid.");
}
if (!CoreHelpers.UserInviteTokenIsValid(_dataProtector, token, user.Email, orgUser.Id, _globalSettings))
{
throw new BadRequestException("Invalid token.");
}
var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
orgUser.OrganizationId, user.Email, true);
if (existingOrgUserCount > 0)
{
if (orgUser.Status == OrganizationUserStatusType.Accepted)
{
throw new BadRequestException("Invitation already accepted. You will receive an email when your organization membership is confirmed.");
}
throw new BadRequestException("You are already part of this organization.");
}
if (string.IsNullOrWhiteSpace(orgUser.Email) ||
!orgUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
{
throw new BadRequestException("User email does not match invite.");
}
return await AcceptUserAsync(orgUser, user, userService);
}
public async Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService)
{
var org = await _organizationRepository.GetByIdentifierAsync(orgIdentifier);
if (org == null)
{
throw new BadRequestException("Organization invalid.");
}
var usersOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
var orgUser = usersOrgs.FirstOrDefault(u => u.OrganizationId == org.Id);
if (orgUser == null)
{
throw new BadRequestException("User not found within organization.");
}
return await AcceptUserAsync(orgUser, user, userService);
}
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>
2021-05-17 09:43:02 -05:00
private async Task<OrganizationUser> AcceptUserAsync(OrganizationUser orgUser, User user,
IUserService userService)
{
if (orgUser.Status != OrganizationUserStatusType.Invited)
{
throw new BadRequestException("Already accepted.");
}
if (orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin)
{
var org = await GetOrgById(orgUser.OrganizationId);
if (org.PlanType == PlanType.Free)
2017-09-08 17:14:15 -04:00
{
2019-05-14 11:16:30 -04:00
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(
user.Id);
if (adminCount > 0)
2017-09-08 17:14:15 -04:00
{
throw new BadRequestException("You can only be an admin of one free organization.");
}
}
}
// Enforce Single Organization Policy of organization user is trying to join
var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(user.Id);
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
var invitedSingleOrgPolicies = await _policyRepository.GetManyByTypeApplicableToUserIdAsync(user.Id,
PolicyType.SingleOrg, OrganizationUserStatusType.Invited);
if (hasOtherOrgs && invitedSingleOrgPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
2020-02-19 14:56:16 -05:00
{
throw new BadRequestException("You may not join this organization until you leave or remove " +
"all other organizations.");
}
// Enforce Single Organization Policy of other organizations user is a member of
var singleOrgPolicyCount = await _policyRepository.GetCountByTypeApplicableToUserIdAsync(user.Id,
PolicyType.SingleOrg);
if (singleOrgPolicyCount > 0)
{
throw new BadRequestException("You cannot join this organization because you are a member of " +
"another organization which forbids it");
}
// Enforce Two Factor Authentication Policy of organization user is trying to join
if (!await userService.TwoFactorIsEnabledAsync(user))
{
var invitedTwoFactorPolicies = await _policyRepository.GetManyByTypeApplicableToUserIdAsync(user.Id,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
{
throw new BadRequestException("You cannot join this organization until you enable " +
"two-step login on your user account.");
}
2020-02-19 14:56:16 -05:00
}
orgUser.Status = OrganizationUserStatusType.Accepted;
2017-03-23 16:56:25 -04:00
orgUser.UserId = user.Id;
2017-03-04 21:28:41 -05:00
orgUser.Email = null;
2017-03-04 21:28:41 -05:00
await _organizationUserRepository.ReplaceAsync(orgUser);
var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin);
var adminEmails = admins.Select(a => a.Email).Distinct().ToList();
if (adminEmails.Count > 0)
{
var organization = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId);
await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails);
}
2017-03-04 21:28:41 -05:00
return orgUser;
}
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, IUserService userService)
2017-03-04 21:28:41 -05:00
{
var result = await ConfirmUsersAsync(organizationId, new Dictionary<Guid, string>() { { organizationUserId, key } },
confirmingUserId, userService);
if (!result.Any())
2017-03-04 21:28:41 -05:00
{
throw new BadRequestException("User not valid.");
2017-03-04 21:28:41 -05:00
}
var (orgUser, error) = result[0];
if (error != "")
2017-09-08 17:14:15 -04:00
{
throw new BadRequestException(error);
}
return orgUser;
}
public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
Guid confirmingUserId, IUserService userService)
{
var organizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);
var validOrganizationUsers = organizationUsers
.Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null)
.ToList();
if (!validOrganizationUsers.Any())
{
return new List<Tuple<OrganizationUser, string>>();
}
var validOrganizationUserIds = validOrganizationUsers.Select(u => u.UserId.Value).ToList();
var organization = await GetOrgById(organizationId);
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId);
var usersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validOrganizationUserIds);
var users = await _userRepository.GetManyAsync(validOrganizationUserIds);
var keyedFilteredUsers = validOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);
var keyedOrganizationUsers = usersOrgs.GroupBy(u => u.UserId.Value)
.ToDictionary(u => u.Key, u => u.ToList());
var succeededUsers = new List<OrganizationUser>();
var result = new List<Tuple<OrganizationUser, string>>();
foreach (var user in users)
{
if (!keyedFilteredUsers.ContainsKey(user.Id))
2017-09-08 17:14:15 -04:00
{
continue;
}
var orgUser = keyedFilteredUsers[user.Id];
var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List<OrganizationUser>());
try
{
if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin
|| orgUser.Type == OrganizationUserType.Owner))
{
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);
if (adminCount > 0)
{
throw new BadRequestException("User can only be an admin of one free organization.");
}
}
await CheckPolicies(policies, organizationId, user, orgUsers, userService);
orgUser.Status = OrganizationUserStatusType.Confirmed;
orgUser.Key = keys[orgUser.Id];
orgUser.Email = null;
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
await _mailService.SendOrganizationConfirmedEmailAsync(organization.Name, user.Email);
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
succeededUsers.Add(orgUser);
result.Add(Tuple.Create(orgUser, ""));
}
catch (BadRequestException e)
{
result.Add(Tuple.Create(orgUser, e.Message));
2017-09-08 17:14:15 -04:00
}
}
await _organizationUserRepository.ReplaceManyAsync(succeededUsers);
return result;
}
internal (bool canScale, string failureReason) CanScale(Organization organization,
int seatsToAdd)
{
var failureReason = "";
if (_globalSettings.SelfHosted)
{
failureReason = "Cannot autoscale on self-hosted instance.";
return (false, failureReason);
}
if (seatsToAdd < 1)
{
return (true, failureReason);
}
if (organization.Seats.HasValue &&
organization.MaxAutoscaleSeats.HasValue &&
organization.MaxAutoscaleSeats.Value < organization.Seats.Value + seatsToAdd)
{
return (false, $"Cannot invite new users. Seat limit has been reached.");
}
return (true, failureReason);
}
public async Task AutoAddSeatsAsync(Organization organization, int seatsToAdd, DateTime? prorationDate = null)
{
if (seatsToAdd < 1 || !organization.Seats.HasValue)
{
return;
}
var (canScale, failureMessage) = CanScale(organization, seatsToAdd);
if (!canScale)
{
throw new BadRequestException(failureMessage);
}
var ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,
OrganizationUserType.Owner)).Select(u => u.Email).Distinct();
2021-10-07 08:05:02 -05:00
var initialSeatCount = organization.Seats.Value;
await AdjustSeatsAsync(organization, seatsToAdd, prorationDate, ownerEmails);
if (!organization.OwnersNotifiedOfAutoscaling.HasValue)
{
2021-10-07 08:05:02 -05:00
await _mailService.SendOrganizationAutoscaledEmailAsync(organization, initialSeatCount,
ownerEmails);
organization.OwnersNotifiedOfAutoscaling = DateTime.UtcNow;
await _organizationRepository.UpsertAsync(organization);
}
}
private async Task CheckPolicies(ICollection<Policy> policies, Guid organizationId, User user,
ICollection<OrganizationUser> userOrgs, IUserService userService)
{
var usingTwoFactorPolicy = policies.Any(p => p.Type == PolicyType.TwoFactorAuthentication && p.Enabled);
if (usingTwoFactorPolicy && !await userService.TwoFactorIsEnabledAsync(user))
{
throw new BadRequestException("User does not have two-step login enabled.");
}
var usingSingleOrgPolicy = policies.Any(p => p.Type == PolicyType.SingleOrg && p.Enabled);
if (usingSingleOrgPolicy)
{
if (userOrgs.Any(ou => ou.OrganizationId != organizationId && ou.Status != OrganizationUserStatusType.Invited))
{
throw new BadRequestException("User is a member of another organization.");
}
}
2017-03-04 21:28:41 -05:00
}
2017-03-09 23:58:43 -05:00
2019-03-05 23:24:14 -05:00
public async Task SaveUserAsync(OrganizationUser user, Guid? savingUserId,
IEnumerable<SelectionReadOnly> collections)
2017-03-09 23:58:43 -05:00
{
if (user.Id.Equals(default(Guid)))
2017-03-09 23:58:43 -05:00
{
throw new BadRequestException("Invite the user first.");
}
var originalUser = await _organizationUserRepository.GetByIdAsync(user.Id);
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>
2021-05-17 09:43:02 -05:00
if (user.Equals(originalUser))
{
throw new BadRequestException("Please make changes before saving.");
}
if (savingUserId.HasValue)
2017-09-27 22:37:13 -04:00
{
await ValidateOrganizationUserUpdatePermissions(user.OrganizationId, user.Type, originalUser.Type);
2017-09-27 22:37:13 -04:00
}
if (user.Type != OrganizationUserType.Owner &&
!await HasConfirmedOwnersExceptAsync(user.OrganizationId, new[] { user.Id }))
2017-03-29 21:26:19 -04:00
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
if (user.AccessAll)
{
2017-04-27 09:19:30 -04:00
// We don't need any collections if we're flagged to have all access.
2017-05-11 14:52:35 -04:00
collections = new List<SelectionReadOnly>();
}
2017-05-11 14:52:35 -04:00
await _organizationUserRepository.ReplaceAsync(user, collections);
await _eventService.LogOrganizationUserEventAsync(user, EventType.OrganizationUser_Updated);
2017-03-11 22:42:27 -05:00
}
2017-03-09 23:58:43 -05:00
2017-12-12 13:21:15 -05:00
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
{
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null || orgUser.OrganizationId != organizationId)
{
throw new BadRequestException("User not valid.");
}
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value)
2017-04-18 15:27:54 -04:00
{
throw new BadRequestException("You cannot remove yourself.");
}
if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue &&
!await _currentContext.OrganizationOwner(organizationId))
2017-09-27 22:37:13 -04:00
{
throw new BadRequestException("Only owners can delete other owners.");
2017-09-27 22:37:13 -04:00
}
if (!await HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }))
2017-03-29 21:26:19 -04:00
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
2017-04-12 10:07:27 -04:00
await _organizationUserRepository.DeleteAsync(orgUser);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
2017-05-26 22:52:50 -04:00
if (orgUser.UserId.HasValue)
2017-07-10 16:38:18 -04:00
{
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
2017-07-10 16:38:18 -04:00
}
2017-04-12 10:07:27 -04:00
}
public async Task DeleteUserAsync(Guid organizationId, Guid userId)
{
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
if (orgUser == null)
2017-04-12 10:07:27 -04:00
{
throw new NotFoundException();
}
if (!await HasConfirmedOwnersExceptAsync(organizationId, new[] { orgUser.Id }))
2017-04-12 10:07:27 -04:00
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
await _organizationUserRepository.DeleteAsync(orgUser);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
2017-05-26 22:52:50 -04:00
if (orgUser.UserId.HasValue)
2017-07-10 16:38:18 -04:00
{
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
2017-07-10 16:38:18 -04:00
}
}
public async Task<List<Tuple<OrganizationUser, string>>> DeleteUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUsersId,
Guid? deletingUserId)
{
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
.ToList();
if (!filteredUsers.Any())
{
throw new BadRequestException("Users invalid.");
}
if (!await HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
var deletingUserIsOwner = false;
if (deletingUserId.HasValue)
{
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
}
var result = new List<Tuple<OrganizationUser, string>>();
var deletedUserIds = new List<Guid>();
foreach (var orgUser in filteredUsers)
{
try
{
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId)
{
throw new BadRequestException("You cannot remove yourself.");
}
if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner)
{
throw new BadRequestException("Only owners can delete other owners.");
}
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
if (orgUser.UserId.HasValue)
{
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
}
result.Add(Tuple.Create(orgUser, ""));
deletedUserIds.Add(orgUser.Id);
}
catch (BadRequestException e)
{
result.Add(Tuple.Create(orgUser, e.Message));
}
await _organizationUserRepository.DeleteManyAsync(deletedUserIds);
}
return result;
}
public async Task<bool> HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable<Guid> organizationUsersId, bool includeProvider = true)
{
var confirmedOwners = await GetConfirmedOwnersAsync(organizationId);
var confirmedOwnersIds = confirmedOwners.Select(u => u.Id);
bool hasOtherOwner = confirmedOwnersIds.Except(organizationUsersId).Any();
if (!hasOtherOwner && includeProvider)
{
return (await _currentContext.ProviderIdForOrg(organizationId)).HasValue;
}
return hasOtherOwner;
}
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId)
{
if (loggedInUserId.HasValue)
{
await ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, null);
}
await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupIds);
2019-05-14 11:16:30 -04:00
await _eventService.LogOrganizationUserEventAsync(organizationUser,
EventType.OrganizationUser_UpdatedGroups);
}
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>
2021-05-17 09:43:02 -05:00
public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid organizationUserId, string resetPasswordKey, Guid? callingUserId)
{
// Org User must be the same as the calling user and the organization ID associated with the user must match passed org ID
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, organizationUserId);
if (!callingUserId.HasValue || orgUser == null || orgUser.UserId != callingUserId.Value ||
orgUser.OrganizationId != organizationId)
{
throw new BadRequestException("User not valid.");
}
// Make sure the organization has the ability to use password reset
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null || !org.UseResetPassword)
{
throw new BadRequestException("Organization does not allow password reset enrollment.");
}
// Make sure the organization has the policy enabled
var resetPasswordPolicy =
await _policyRepository.GetByOrganizationIdTypeAsync(organizationId, PolicyType.ResetPassword);
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
{
throw new BadRequestException("Organization does not have the password reset policy enabled.");
}
// Block the user from withdrawal if auto enrollment is enabled
if (resetPasswordKey == null && resetPasswordPolicy.Data != null)
{
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);
if (data?.AutoEnrollEnabled ?? false)
{
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from Password Reset.");
}
}
orgUser.ResetPasswordKey = resetPasswordKey;
await _organizationUserRepository.ReplaceAsync(orgUser);
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>
2021-05-17 09:43:02 -05:00
await _eventService.LogOrganizationUserEventAsync(orgUser, resetPasswordKey != null ?
EventType.OrganizationUser_ResetPassword_Enroll : EventType.OrganizationUser_ResetPassword_Withdraw);
}
public async Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId)
{
var organization = await GetOrgById(organizationId);
return await GenerateLicenseAsync(organization, installationId);
}
public async Task<OrganizationLicense> GenerateLicenseAsync(Organization organization, Guid installationId,
int? version = null)
{
if (organization == null)
{
throw new NotFoundException();
}
var installation = await _installationRepository.GetByIdAsync(installationId);
if (installation == null || !installation.Enabled)
{
throw new BadRequestException("Invalid installation id");
}
2019-02-18 15:40:47 -05:00
var subInfo = await _paymentService.GetSubscriptionAsync(organization);
return new OrganizationLicense(organization, subInfo, installationId, _licensingService, version);
}
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections)
{
var invite = new OrganizationUserInvite()
{
Emails = new List<string> { email },
Type = type,
AccessAll = accessAll,
Collections = collections,
};
var results = await InviteUsersAsync(organizationId, invitingUserId,
new (OrganizationUserInvite, string)[] { (invite, externalId) });
var result = results.FirstOrDefault();
if (result == null)
{
throw new BadRequestException("This user has already been invited.");
}
return result;
}
2017-05-15 14:41:20 -04:00
public async Task ImportAsync(Guid organizationId,
Guid? importingUserId,
2017-05-16 00:11:21 -04:00
IEnumerable<ImportedGroup> groups,
IEnumerable<ImportedOrganizationUser> newUsers,
2019-05-06 21:31:20 -04:00
IEnumerable<string> removeUserExternalIds,
bool overwriteExisting)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
if (!organization.UseDirectory)
{
2017-05-20 15:31:16 -04:00
throw new BadRequestException("Organization cannot use directory syncing.");
}
2017-05-20 15:31:16 -04:00
var newUsersSet = new HashSet<string>(newUsers?.Select(u => u.ExternalId) ?? new List<string>());
2017-05-18 10:41:47 -04:00
var existingUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var existingExternalUsers = existingUsers.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();
var existingExternalUsersIdDict = existingExternalUsers.ToDictionary(u => u.ExternalId, u => u.Id);
2017-05-15 14:41:20 -04:00
// Users
2017-11-13 12:09:39 -05:00
2017-05-15 14:41:20 -04:00
// Remove Users
if (removeUserExternalIds?.Any() ?? false)
{
2017-05-16 00:11:21 -04:00
var removeUsersSet = new HashSet<string>(removeUserExternalIds);
2017-05-18 10:41:47 -04:00
var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId);
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>
2021-05-17 09:43:02 -05:00
await _organizationUserRepository.DeleteManyAsync(removeUsersSet
2017-05-15 14:41:20 -04:00
.Except(newUsersSet)
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>
2021-05-17 09:43:02 -05:00
.Where(u => existingUsersDict.ContainsKey(u) && existingUsersDict[u].Type != OrganizationUserType.Owner)
.Select(u => existingUsersDict[u].Id));
}
if (overwriteExisting)
2019-05-06 21:31:20 -04:00
{
// Remove existing external users that are not in new user set
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>
2021-05-17 09:43:02 -05:00
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)
2019-05-06 21:31:20 -04: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>
2021-05-17 09:43:02 -05:00
existingExternalUsersIdDict.Remove(deletedUser.ExternalId);
2019-05-06 21:31:20 -04:00
}
}
if (newUsers?.Any() ?? false)
{
2017-11-10 15:22:19 -05:00
// Marry existing users
var existingUsersEmailsDict = existingUsers
.Where(u => string.IsNullOrWhiteSpace(u.ExternalId))
.ToDictionary(u => u.Email);
var newUsersEmailsDict = newUsers.ToDictionary(u => u.Email);
var usersToAttach = existingUsersEmailsDict.Keys.Intersect(newUsersEmailsDict.Keys).ToList();
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>
2021-05-17 09:43:02 -05:00
var usersToUpsert = new List<OrganizationUser>();
foreach (var user in usersToAttach)
2017-11-10 15:22:19 -05:00
{
var orgUserDetails = existingUsersEmailsDict[user];
var orgUser = await _organizationUserRepository.GetByIdAsync(orgUserDetails.Id);
if (orgUser != null)
2017-11-10 15:22:19 -05:00
{
orgUser.ExternalId = newUsersEmailsDict[user].ExternalId;
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>
2021-05-17 09:43:02 -05:00
usersToUpsert.Add(orgUser);
2017-11-10 15:22:19 -05:00
existingExternalUsersIdDict.Add(orgUser.ExternalId, orgUser.Id);
}
}
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>
2021-05-17 09:43:02 -05:00
await _organizationUserRepository.UpsertManyAsync(usersToUpsert);
2017-11-10 15:22:19 -05:00
// Add new users
var existingUsersSet = new HashSet<string>(existingExternalUsersIdDict.Keys);
2017-05-15 14:41:20 -04:00
var usersToAdd = newUsersSet.Except(existingUsersSet).ToList();
2017-05-15 14:41:20 -04:00
var seatsAvailable = int.MaxValue;
2017-05-18 08:58:08 -04:00
var enoughSeatsAvailable = true;
if (organization.Seats.HasValue)
{
2017-05-15 14:41:20 -04:00
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
seatsAvailable = organization.Seats.Value - userCount;
2017-05-18 08:58:08 -04:00
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
}
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>
2021-05-17 09:43:02 -05:00
var userInvites = new List<(OrganizationUserInvite, string)>();
foreach (var user in newUsers)
{
if (!usersToAdd.Contains(user.ExternalId) || string.IsNullOrWhiteSpace(user.Email))
2017-05-16 00:11:21 -04:00
{
continue;
}
2017-05-16 00:11:21 -04:00
try
{
var invite = new OrganizationUserInvite
2017-05-18 08:58:08 -04:00
{
Emails = new List<string> { user.Email },
Type = OrganizationUserType.User,
AccessAll = false,
Collections = new List<SelectionReadOnly>(),
};
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>
2021-05-17 09:43:02 -05:00
userInvites.Add((invite, user.ExternalId));
}
catch (BadRequestException)
{
// Thrown when the user is already invited to the organization
continue;
}
}
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>
2021-05-17 09:43:02 -05:00
var invitedUsers = await InviteUsersAsync(organizationId, importingUserId, userInvites);
foreach (var invitedUser in invitedUsers)
{
existingExternalUsersIdDict.Add(invitedUser.ExternalId, invitedUser.Id);
}
}
2017-11-13 12:09:39 -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>
2021-05-17 09:43:02 -05:00
// Groups
if (groups?.Any() ?? false)
{
if (!organization.UseGroups)
2017-05-20 15:31:16 -04:00
{
throw new BadRequestException("Organization cannot use groups.");
}
2017-05-16 00:11:21 -04:00
var groupsDict = groups.ToDictionary(g => g.Group.ExternalId);
2017-05-18 10:41:47 -04:00
var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(organizationId);
2019-05-14 11:16:30 -04:00
var existingExternalGroups = existingGroups
.Where(u => !string.IsNullOrWhiteSpace(u.ExternalId)).ToList();
2017-05-18 10:41:47 -04:00
var existingExternalGroupsDict = existingExternalGroups.ToDictionary(g => g.ExternalId);
2017-05-15 14:41:20 -04:00
var newGroups = groups
2017-05-18 10:41:47 -04:00
.Where(g => !existingExternalGroupsDict.ContainsKey(g.Group.ExternalId))
2017-05-16 00:11:21 -04:00
.Select(g => g.Group);
2017-05-15 14:41:20 -04:00
foreach (var group in newGroups)
{
2017-05-15 14:41:20 -04:00
group.CreationDate = group.RevisionDate = DateTime.UtcNow;
2017-05-15 14:41:20 -04:00
await _groupRepository.CreateAsync(group);
2019-05-14 11:16:30 -04:00
await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds,
existingExternalUsersIdDict);
2017-05-15 14:41:20 -04:00
}
2017-05-18 10:41:47 -04:00
var updateGroups = existingExternalGroups
2017-05-15 16:37:56 -04:00
.Where(g => groupsDict.ContainsKey(g.ExternalId))
.ToList();
if (updateGroups.Any())
{
2017-05-15 16:37:56 -04:00
var groupUsers = await _groupRepository.GetManyGroupUsersByOrganizationIdAsync(organizationId);
var existingGroupUsers = groupUsers
.GroupBy(gu => gu.GroupId)
2017-05-15 16:37:56 -04:00
.ToDictionary(g => g.Key, g => new HashSet<Guid>(g.Select(gr => gr.OrganizationUserId)));
2017-05-15 14:41:20 -04:00
foreach (var group in updateGroups)
{
2017-05-16 00:11:21 -04:00
var updatedGroup = groupsDict[group.ExternalId].Group;
if (group.Name != updatedGroup.Name)
2017-05-15 16:37:56 -04:00
{
group.RevisionDate = DateTime.UtcNow;
group.Name = updatedGroup.Name;
await _groupRepository.ReplaceAsync(group);
}
2019-05-14 11:16:30 -04:00
await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds,
existingExternalUsersIdDict,
2017-05-15 16:37:56 -04:00
existingGroupUsers.ContainsKey(group.Id) ? existingGroupUsers[group.Id] : null);
}
}
}
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.DirectorySynced, organization));
}
2019-03-04 09:52:43 -05:00
public async Task RotateApiKeyAsync(Organization organization)
{
organization.ApiKey = CoreHelpers.SecureRandomString(30);
organization.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(organization);
}
public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId)
{
await _ssoUserRepository.DeleteAsync(userId, organizationId);
if (organizationId.HasValue)
{
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId.Value, userId);
if (organizationUser != null)
{
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UnlinkedSso);
}
}
}
public async Task<Organization> UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey)
{
if (!await _currentContext.ManageResetPassword(orgId))
{
throw new UnauthorizedAccessException();
}
// If the keys already exist, error out
var org = await _organizationRepository.GetByIdAsync(orgId);
if (org.PublicKey != null && org.PrivateKey != null)
{
throw new BadRequestException("Organization Keys already exist");
}
// Update org with generated public/private key
org.PublicKey = publicKey;
org.PrivateKey = privateKey;
await UpdateAsync(org);
return org;
}
2017-05-15 14:41:20 -04:00
private async Task UpdateUsersAsync(Group group, HashSet<string> groupUsers,
Dictionary<string, Guid> existingUsersIdDict, HashSet<Guid> existingUsers = null)
2017-05-15 14:41:20 -04:00
{
2017-05-15 16:37:56 -04:00
var availableUsers = groupUsers.Intersect(existingUsersIdDict.Keys);
var users = new HashSet<Guid>(availableUsers.Select(u => existingUsersIdDict[u]));
if (existingUsers != null && existingUsers.Count == users.Count && users.SetEquals(existingUsers))
{
return;
}
2017-05-15 14:41:20 -04:00
await _groupRepository.UpdateUsersAsync(group.Id, users);
}
2017-03-29 21:26:19 -04:00
private async Task<IEnumerable<OrganizationUser>> GetConfirmedOwnersAsync(Guid organizationId)
{
var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId,
OrganizationUserType.Owner);
return owners.Where(o => o.Status == OrganizationUserStatusType.Confirmed);
2017-03-29 21:26:19 -04:00
}
2017-08-11 08:57:31 -04:00
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
{
var deviceIds = await GetUserDeviceIdsAsync(userId);
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds,
organizationId.ToString());
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
}
2017-08-11 08:57:31 -04:00
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
{
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
2017-11-14 08:39:16 -05:00
return devices.Where(d => !string.IsNullOrWhiteSpace(d.PushToken)).Select(d => d.Id.ToString());
2017-08-11 08:57:31 -04:00
}
private async Task ReplaceAndUpdateCache(Organization org, EventType? orgEvent = null)
{
await _organizationRepository.ReplaceAsync(org);
await _applicationCacheService.UpsertOrganizationAbilityAsync(org);
if (orgEvent.HasValue)
{
await _eventService.LogOrganizationEventAsync(org, orgEvent.Value);
}
}
private async Task<Organization> GetOrgById(Guid id)
{
return await _organizationRepository.GetByIdAsync(id);
}
2019-03-21 21:36:03 -04:00
private void ValidateOrganizationUpgradeParameters(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
{
if (!plan.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0)
2019-03-21 21:36:03 -04:00
{
throw new BadRequestException("Plan does not allow additional storage.");
}
if (upgrade.AdditionalStorageGb < 0)
2019-03-21 21:36:03 -04:00
{
throw new BadRequestException("You can't subtract storage!");
}
if (!plan.HasPremiumAccessOption && upgrade.PremiumAccessAddon)
2019-03-21 21:36:03 -04:00
{
throw new BadRequestException("This plan does not allow you to buy the premium access addon.");
}
if (plan.BaseSeats + upgrade.AdditionalSeats <= 0)
2019-03-21 21:36:03 -04:00
{
throw new BadRequestException("You do not have any seats!");
}
if (upgrade.AdditionalSeats < 0)
2019-03-21 21:36:03 -04:00
{
throw new BadRequestException("You can't subtract seats!");
}
if (!plan.HasAdditionalSeatsOption && upgrade.AdditionalSeats > 0)
2019-03-21 21:36:03 -04:00
{
throw new BadRequestException("Plan does not allow additional users.");
}
if (plan.HasAdditionalSeatsOption && plan.MaxAdditionalSeats.HasValue &&
2019-03-21 21:36:03 -04:00
upgrade.AdditionalSeats > plan.MaxAdditionalSeats.Value)
{
throw new BadRequestException($"Selected plan allows a maximum of " +
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
}
}
private async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType,
OrganizationUserType? oldType)
{
if (await _currentContext.OrganizationOwner(organizationId))
{
return;
}
if (oldType == OrganizationUserType.Owner || newType == OrganizationUserType.Owner)
{
throw new BadRequestException("Only an Owner can configure another Owner's account.");
}
if (await _currentContext.OrganizationAdmin(organizationId))
{
return;
}
if (oldType == OrganizationUserType.Custom || newType == OrganizationUserType.Custom)
{
throw new BadRequestException("Only Owners and Admins can configure Custom accounts.");
}
if (!await _currentContext.ManageUsers(organizationId))
{
throw new BadRequestException("Your account does not have permission to manage users.");
}
if (oldType == OrganizationUserType.Admin || newType == OrganizationUserType.Admin)
{
throw new BadRequestException("Custom users can not manage Admins or Owners.");
}
}
private async Task ValidateDeleteOrganizationAsync(Organization organization)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
2021-11-17 11:46:35 +01:00
if (ssoConfig?.GetData()?.KeyConnectorEnabled == true)
{
throw new BadRequestException("You cannot delete an Organization that is using Key Connector.");
}
}
}
}