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

1722 lines
74 KiB
C#
Raw Normal View History

using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Repositories;
using Bit.Core.Models.Business;
2017-03-08 21:45:08 -05:00
using Bit.Core.Models.Table;
using Bit.Core.Utilities;
using Bit.Core.Exceptions;
2017-03-09 23:58:43 -05:00
using System.Collections.Generic;
using Microsoft.AspNetCore.DataProtection;
2017-04-04 10:13:16 -04:00
using Stripe;
using Bit.Core.Enums;
2017-05-11 14:52:35 -04:00
using Bit.Core.Models.Data;
2017-08-14 21:25:06 -04:00
using System.IO;
using Newtonsoft.Json;
using System.Text.Json;
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;
2017-08-14 20:57:45 -04:00
private readonly GlobalSettings _globalSettings;
private readonly ITaxRateRepository _taxRateRepository;
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,
GlobalSettings globalSettings,
ITaxRateRepository taxRateRepository)
{
_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;
}
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.");
}
}
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;
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;
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,
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<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
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 + 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 subscriptionItemService = new SubscriptionItemService();
var subscriptionService = new SubscriptionService();
var sub = await subscriptionService.GetAsync(organization.GatewaySubscriptionId);
if (sub == null)
{
2017-04-10 12:28:41 -04:00
throw new BadRequestException("Subscription not found.");
}
var prorationDate = DateTime.UtcNow;
2017-04-10 12:28:41 -04:00
var seatItem = sub.Items?.Data?.FirstOrDefault(i => i.Plan.Id == plan.StripeSeatPlanId);
// Retain original collection method
var collectionMethod = sub.CollectionMethod;
var subUpdateOptions = new SubscriptionUpdateOptions
{
2020-05-12 17:22:04 -04:00
Items = new List<SubscriptionItemOptions>
{
2020-05-12 17:22:04 -04:00
new SubscriptionItemOptions
{
Id = seatItem?.Id,
Plan = plan.StripeSeatPlanId,
Quantity = additionalSeats,
Deleted = (seatItem?.Id != null && additionalSeats == 0) ? true : (bool?)null
}
},
ProrationBehavior = "always_invoice",
DaysUntilDue = 1,
CollectionMethod = "send_invoice",
ProrationDate = prorationDate,
};
var customer = await new CustomerService().GetAsync(sub.CustomerId);
var taxRates = await _taxRateRepository.GetByLocationAsync(
new Bit.Core.Models.Table.TaxRate()
{
Country = customer.Address.Country,
PostalCode = customer.Address.PostalCode
}
);
var taxRate = taxRates.FirstOrDefault();
if (taxRate != null && !sub.DefaultTaxRates.Any(x => x.Equals(taxRate.Id)))
{
subUpdateOptions.DefaultTaxRates = new List<string>(1)
{
taxRate.Id
};
}
var subResponse = await subscriptionService.UpdateAsync(sub.Id, subUpdateOptions);
string paymentIntentClientSecret = null;
if (additionalSeats > 0)
{
try
{
paymentIntentClientSecret = await (_paymentService as StripePaymentService)
2020-05-12 17:22:04 -04:00
.PayInvoiceAfterSubscriptionChangeAsync(organization, subResponse.LatestInvoiceId);
}
catch
{
// Need to revert the subscription
2020-05-12 17:22:04 -04:00
await subscriptionService.UpdateAsync(sub.Id, new SubscriptionUpdateOptions
{
2020-05-12 17:22:04 -04:00
Items = new List<SubscriptionItemOptions>
{
2020-05-12 17:22:04 -04:00
new SubscriptionItemOptions
{
Id = seatItem?.Id,
Plan = plan.StripeSeatPlanId,
Quantity = organization.Seats,
2020-05-12 17:22:04 -04:00
Deleted = seatItem?.Id == null ? true : (bool?)null
}
},
// This proration behavior prevents a false "credit" from
// being applied forward to the next month's invoice
ProrationBehavior = "none",
CollectionMethod = collectionMethod,
});
throw;
}
}
2017-07-06 14:55:58 -04:00
// Change back the subscription collection method
if (collectionMethod != "send_invoice")
{
await subscriptionService.UpdateAsync(sub.Id, new SubscriptionUpdateOptions
{
CollectionMethod = collectionMethod,
});
}
2017-07-06 14:55:58 -04:00
organization.Seats = (short?)newSeatTotal;
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.AdjustSeats, organization)
{
PlanName = plan.Name,
PlanType = plan.Type,
Seats = organization.Seats,
});
await ReplaceAndUpdateCache(organization);
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);
}
}
2017-03-04 21:28:41 -05:00
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup)
{
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan && !p.Disabled);
if (plan == null)
{
throw new BadRequestException("Plan not found.");
}
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,
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),
CreationDate = DateTime.UtcNow,
2020-06-25 12:28:22 -04:00
RevisionDate = DateTime.UtcNow,
};
if (plan.Type == PlanType.Free)
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
{
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 returnValue = await SignUpAsync(organization, signup.Owner.Id, 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
}
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(
2017-08-30 21:25:46 -04:00
OrganizationLicense license, User owner, string ownerKey, string collectionName)
2017-08-14 20:57:45 -04:00
{
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.");
}
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,
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,
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),
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);
System.IO.File.WriteAllText($"{dir}/{organization.Id}.json",
JsonConvert.SerializeObject(license, Formatting.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
var orgUser = new OrganizationUser
{
OrganizationId = organization.Id,
2017-08-14 20:57:45 -04:00
UserId = ownerId,
Key = ownerKey,
2017-04-11 13:04:37 -04:00
Type = OrganizationUserType.Owner,
Status = OrganizationUserStatusType.Confirmed,
AccessAll = true,
CreationDate = organization.CreationDate,
RevisionDate = organization.CreationDate
};
await _organizationUserRepository.CreateAsync(orgUser);
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);
}
2017-04-21 22:39:46 -04:00
// push
2017-08-11 08:57:31 -04:00
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
2019-03-21 21:36:03 -04:00
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds,
organization.Id.ToString());
2017-08-14 20:57:45 -04:00
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 == 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.");
}
}
2020-01-15 15:00:54 -05:00
2017-08-14 21:25:06 -04:00
var dir = $"{_globalSettings.LicenseDirectory}/organization";
Directory.CreateDirectory(dir);
System.IO.File.WriteAllText($"{dir}/{organization.Id}.json",
JsonConvert.SerializeObject(license, Formatting.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;
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)
{
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);
}
2017-12-12 13:21:15 -05:00
public async Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId,
string externalId, OrganizationUserInvite invite)
2017-03-04 21:28:41 -05:00
{
var organization = await GetOrgById(organizationId);
if (organization == null || invite?.Emails == null)
{
throw new NotFoundException();
}
if (invitingUserId.HasValue && invite.Type.HasValue)
2017-09-27 22:37:13 -04:00
{
await ValidateOrganizationUserUpdatePermissions(invitingUserId.Value, organizationId, invite.Type.Value, null);
2017-09-27 22:37:13 -04:00
}
if (organization.Seats.HasValue)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
2017-05-18 12:04:27 -04:00
var availableSeats = organization.Seats.Value - userCount;
if (availableSeats < invite.Emails.Count())
{
throw new BadRequestException("You have reached the maximum number of users " +
$"({organization.Seats.Value}) for this organization.");
}
}
2017-05-18 12:04:27 -04:00
var orgUsers = new List<OrganizationUser>();
var orgUserInvitedCount = 0;
foreach (var email in invite.Emails)
2017-03-28 21:16:19 -04:00
{
2017-05-18 12:04:27 -04:00
// Make sure user is not already invited
2017-11-13 10:06:54 -05:00
var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
organizationId, email, false);
if (existingOrgUserCount > 0)
2017-05-18 12:04:27 -04:00
{
continue;
2017-05-18 12:04:27 -04:00
}
2017-05-18 12:04:27 -04:00
var orgUser = new OrganizationUser
{
OrganizationId = organizationId,
UserId = null,
Email = email.ToLowerInvariant(),
Key = null,
Type = invite.Type.Value,
2017-05-18 12:04:27 -04:00
Status = OrganizationUserStatusType.Invited,
AccessAll = invite.AccessAll,
2017-05-18 12:04:27 -04:00
ExternalId = externalId,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Permissions = System.Text.Json.JsonSerializer.Serialize(invite.Permissions, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}),
2017-05-18 12:04:27 -04:00
};
2017-03-04 21:28:41 -05:00
if (!orgUser.AccessAll && invite.Collections.Any())
2017-05-18 12:04:27 -04:00
{
await _organizationUserRepository.CreateAsync(orgUser, invite.Collections);
2017-05-18 12:04:27 -04:00
}
else
{
await _organizationUserRepository.CreateAsync(orgUser);
}
await SendInviteAsync(orgUser, organization);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Invited);
2017-05-18 12:04:27 -04:00
orgUsers.Add(orgUser);
orgUserInvitedCount++;
}
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization)
{
Users = orgUserInvitedCount
});
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
}
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);
}
private async Task SendInviteAsync(OrganizationUser orgUser, Organization organization)
{
2017-03-23 11:51:37 -04:00
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var token = _dataProtector.Protect(
2017-03-23 11:51:37 -04:00
$"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {nowMillis}");
await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, token);
}
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.");
}
if (string.IsNullOrWhiteSpace(orgUser.Email) ||
2018-01-01 22:05:43 -05:00
!orgUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
{
throw new BadRequestException("User email does not match invite.");
}
var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
orgUser.OrganizationId, user.Email, true);
if (existingOrgUserCount > 0)
{
throw new BadRequestException("You are already part of this organization.");
}
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);
}
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.");
}
}
}
ICollection<Policy> orgPolicies = null;
ICollection<Policy> userPolicies = null;
async Task<bool> hasPolicyAsync(PolicyType policyType, bool useUserPolicies = false)
{
var policies = useUserPolicies ?
userPolicies = userPolicies ?? await _policyRepository.GetManyByUserIdAsync(user.Id) :
orgPolicies = orgPolicies ?? await _policyRepository.GetManyByOrganizationIdAsync(orgUser.OrganizationId);
return policies.Any(p => p.Type == policyType && p.Enabled);
}
var userOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
if (userOrgs.Any(ou => ou.OrganizationId != orgUser.OrganizationId && ou.Status != OrganizationUserStatusType.Invited))
{
if (await hasPolicyAsync(PolicyType.SingleOrg))
{
throw new BadRequestException("You may not join this organization until you leave or remove " +
"all other organizations.");
}
if (await hasPolicyAsync(PolicyType.SingleOrg, true))
{
throw new BadRequestException("You cannot join this organization because you are a member of " +
"an organization which forbids it");
}
}
if (!await userService.TwoFactorIsEnabledAsync(user))
2020-02-19 14:56:16 -05:00
{
if (await hasPolicyAsync(PolicyType.TwoFactorAuthentication))
2020-02-19 14:56:16 -05:00
{
throw new BadRequestException("You cannot join this organization until you enable " +
"two-step login on your user account.");
}
}
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);
// TODO: send notification emails to org admins and accepting user?
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 orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Accepted ||
orgUser.OrganizationId != organizationId)
2017-03-04 21:28:41 -05:00
{
throw new BadRequestException("User not valid.");
2017-03-04 21:28:41 -05:00
}
var org = await GetOrgById(organizationId);
if (org.PlanType == PlanType.Free &&
2017-09-08 17:14:15 -04:00
(orgUser.Type == OrganizationUserType.Admin || orgUser.Type == OrganizationUserType.Owner))
{
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(
orgUser.UserId.Value);
if (adminCount > 0)
2017-09-08 17:14:15 -04:00
{
throw new BadRequestException("User can only be an admin of one free organization.");
}
}
var user = await _userRepository.GetByIdAsync(orgUser.UserId.Value);
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId);
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)
{
var userOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
if (userOrgs.Any(ou => ou.OrganizationId != organizationId && ou.Status != OrganizationUserStatusType.Invited))
{
throw new BadRequestException("User is a member of another organization.");
}
}
orgUser.Status = OrganizationUserStatusType.Confirmed;
2017-03-04 21:28:41 -05:00
orgUser.Key = key;
orgUser.Email = null;
await _organizationUserRepository.ReplaceAsync(orgUser);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
2017-09-08 17:14:15 -04:00
await _mailService.SendOrganizationConfirmedEmailAsync(org.Name, user.Email);
2017-03-04 21:28:41 -05:00
// push
2017-08-11 08:57:31 -04:00
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds, organizationId.ToString());
2017-05-26 22:52:50 -04:00
await _pushNotificationService.PushSyncOrgKeysAsync(orgUser.UserId.Value);
2017-03-04 21:28:41 -05:00
return orgUser;
}
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);
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(savingUserId.Value, user.OrganizationId, user.Type, originalUser.Type);
2017-09-27 22:37:13 -04:00
}
2017-03-29 21:26:19 -04:00
var confirmedOwners = (await GetConfirmedOwnersAsync(user.OrganizationId)).ToList();
if (user.Type != OrganizationUserType.Owner &&
2019-05-14 11:16:30 -04:00
confirmedOwners.Count == 1 && confirmedOwners[0].Id == 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)
2017-09-27 22:37:13 -04:00
{
2017-12-12 13:21:15 -05:00
var deletingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(deletingUserId.Value);
2019-05-14 11:16:30 -04:00
var anyOwners = deletingUserOrgs.Any(
u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner);
if (!anyOwners)
2017-09-27 22:37:13 -04:00
{
throw new BadRequestException("Only owners can delete other owners.");
}
}
2017-03-29 21:26:19 -04:00
var confirmedOwners = (await GetConfirmedOwnersAsync(organizationId)).ToList();
if (confirmedOwners.Count == 1 && confirmedOwners[0].Id == 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
{
// push
2017-08-11 08:57:31 -04:00
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
2019-05-14 11:16:30 -04:00
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds,
organizationId.ToString());
await _pushNotificationService.PushSyncOrgKeysAsync(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();
}
var confirmedOwners = (await GetConfirmedOwnersAsync(organizationId)).ToList();
if (confirmedOwners.Count == 1 && confirmedOwners[0].Id == 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
{
// push
2017-08-11 08:57:31 -04:00
var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
2019-05-14 11:16:30 -04:00
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds,
organizationId.ToString());
await _pushNotificationService.PushSyncOrgKeysAsync(orgUser.UserId.Value);
2017-07-10 16:38:18 -04:00
}
}
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId)
{
if (loggedInUserId.HasValue)
{
await ValidateOrganizationUserUpdatePermissions(loggedInUserId.Value, 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);
}
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);
}
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);
2017-05-15 14:41:20 -04:00
var usersToRemove = removeUsersSet
.Except(newUsersSet)
.Where(ru => existingUsersDict.ContainsKey(ru))
.Select(ru => existingUsersDict[ru]);
foreach (var user in usersToRemove)
{
if (user.Type != OrganizationUserType.Owner)
{
await _organizationUserRepository.DeleteAsync(new OrganizationUser { Id = user.Id });
existingExternalUsersIdDict.Remove(user.ExternalId);
}
}
}
if (overwriteExisting)
2019-05-06 21:31:20 -04:00
{
// Remove existing external users that are not in new user set
foreach (var user in existingExternalUsers)
2019-05-06 21:31:20 -04:00
{
if (user.Type != OrganizationUserType.Owner && !newUsersSet.Contains(user.ExternalId) &&
2019-05-06 21:31:20 -04:00
existingExternalUsersIdDict.ContainsKey(user.ExternalId))
{
await _organizationUserRepository.DeleteAsync(new OrganizationUser { Id = user.Id });
2019-05-06 21:31:20 -04:00
existingExternalUsersIdDict.Remove(user.ExternalId);
}
}
}
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();
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;
await _organizationUserRepository.UpsertAsync(orgUser);
existingExternalUsersIdDict.Add(orgUser.ExternalId, orgUser.Id);
}
}
// 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;
}
if (enoughSeatsAvailable)
{
foreach (var user in newUsers)
2017-05-16 00:11:21 -04:00
{
if (!usersToAdd.Contains(user.ExternalId) || string.IsNullOrWhiteSpace(user.Email))
2017-05-18 08:58:08 -04:00
{
continue;
}
2017-05-16 00:11:21 -04:00
2017-05-18 08:58:08 -04:00
try
{
var invite = new OrganizationUserInvite
{
Emails = new List<string> { user.Email },
Type = OrganizationUserType.User,
AccessAll = false,
Collections = new List<SelectionReadOnly>(),
};
var newUserPromise = await InviteUserAsync(organizationId, importingUserId, user.ExternalId, invite);
var newUser = newUserPromise.FirstOrDefault();
2017-05-18 10:41:47 -04:00
existingExternalUsersIdDict.Add(newUser.ExternalId, newUser.Id);
2017-05-18 08:58:08 -04:00
}
catch (BadRequestException)
2017-05-18 08:58:08 -04:00
{
continue;
}
}
}
}
2017-05-15 14:41:20 -04:00
// Groups
2017-11-13 12:09:39 -05:00
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);
}
}
}
}
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);
}
}
}
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<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 loggedInUserId, Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType)
{
var loggedInUserOrgs = await _organizationUserRepository.GetManyByUserAsync(loggedInUserId);
var loggedInAsOrgOwner = loggedInUserOrgs
.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner);
if (loggedInAsOrgOwner)
{
return;
}
var isOwner = oldType == OrganizationUserType.Owner;
var nowOwner = newType == OrganizationUserType.Owner;
var ownerUserConfigurationAttempt = (isOwner && nowOwner) || !(isOwner.Equals(nowOwner));
if (ownerUserConfigurationAttempt)
{
throw new BadRequestException("Only an Owner can configure another Owner's account.");
}
var loggedInAsOrgAdmin = loggedInUserOrgs.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Admin);
if (loggedInAsOrgAdmin)
{
return;
}
var isCustom = oldType == OrganizationUserType.Custom;
var nowCustom = newType == OrganizationUserType.Custom;
var customUserConfigurationAttempt = (isCustom && nowCustom) || !(isCustom.Equals(nowCustom));
if (customUserConfigurationAttempt)
{
throw new BadRequestException("Only Owners and Admins can configure Custom accounts.");
}
var loggedInAsOrgCustom = loggedInUserOrgs.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Custom);
if (!loggedInAsOrgCustom)
{
return;
}
var loggedInCustomOrgUser = loggedInUserOrgs.First(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Custom);
var loggedInUserPermissions = CoreHelpers.LoadClassFromJsonData<Permissions>(loggedInCustomOrgUser.Permissions);
if (!loggedInUserPermissions.ManageUsers)
{
throw new BadRequestException("Your account does not have permission to manage users.");
}
var isAdmin = oldType == OrganizationUserType.Admin;
var nowAdmin = newType == OrganizationUserType.Admin;
var adminUserConfigurationAttempt = (isAdmin && nowAdmin) || !(isAdmin.Equals(nowAdmin));
if (adminUserConfigurationAttempt)
{
throw new BadRequestException("Custom users can not manage Admins or Owners.");
}
}
}
}