1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

[EC-635] Extract organizationService.UpdateLicenseAsync to a command (#2408)

* move UpdateLicenseAsync from service to command
* create new SelfHostedOrganizationDetails view model and move license validation logic there
* move occupied seat count logic to database level
This commit is contained in:
Thomas Rittson
2023-02-24 07:54:19 +10:00
committed by GitHub
parent 7d0bba3a29
commit 4643f5960e
30 changed files with 967 additions and 239 deletions

View File

@ -2,6 +2,7 @@
using System.Text.Json;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Models.Business;
using Bit.Core.Utilities;
namespace Bit.Core.Entities;
@ -197,4 +198,33 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
return providers[provider];
}
public void UpdateFromLicense(OrganizationLicense license)
{
Name = license.Name;
BusinessName = license.BusinessName;
BillingEmail = license.BillingEmail;
PlanType = license.PlanType;
Seats = license.Seats;
MaxCollections = license.MaxCollections;
UseGroups = license.UseGroups;
UseDirectory = license.UseDirectory;
UseEvents = license.UseEvents;
UseTotp = license.UseTotp;
Use2fa = license.Use2fa;
UseApi = license.UseApi;
UsePolicies = license.UsePolicies;
UseSso = license.UseSso;
UseKeyConnector = license.UseKeyConnector;
UseScim = license.UseScim;
UseResetPassword = license.UseResetPassword;
SelfHost = license.SelfHost;
UsersGetPremium = license.UsersGetPremium;
UseCustomPermissions = license.UseCustomPermissions;
Plan = license.Plan;
Enabled = license.Enabled;
ExpirationDate = license.Expires;
LicenseKey = license.LicenseKey;
RevisionDate = DateTime.UtcNow;
}
}

View File

@ -199,21 +199,42 @@ public class OrganizationLicense : ILicense
}
}
public bool CanUse(IGlobalSettings globalSettings)
public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception)
{
if (!Enabled || Issued > DateTime.UtcNow || Expires < DateTime.UtcNow)
{
exception = "Invalid license. Your organization is disabled or the license has expired.";
return false;
}
if (ValidLicenseVersion)
if (!ValidLicenseVersion)
{
return InstallationId == globalSettings.Installation.Id && SelfHost;
exception = $"Version {Version} is not supported.";
return false;
}
else
if (InstallationId != globalSettings.Installation.Id || !SelfHost)
{
throw new NotSupportedException($"Version {Version} is not supported.");
exception = "Invalid license. Make sure your license allows for on-premise " +
"hosting of organizations and that the installation id matches your current installation.";
return false;
}
if (LicenseType != null && LicenseType != Enums.LicenseType.Organization)
{
exception = "Premium licenses cannot be applied to an organization. "
+ "Upload this license from your personal account settings page.";
return false;
}
if (!licensingService.VerifyLicense(this))
{
exception = "Invalid license.";
return false;
}
exception = "";
return true;
}
public bool VerifyData(Organization organization, IGlobalSettings globalSettings)

View File

@ -62,14 +62,6 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
return Premium.GetValueOrDefault(false);
}
public bool OccupiesOrganizationSeat
{
get
{
return Status != OrganizationUserStatusType.Revoked;
}
}
public Permissions GetPermissions()
{
return string.IsNullOrWhiteSpace(Permissions) ? null

View File

@ -0,0 +1,145 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.OrganizationConnectionConfigs;
namespace Bit.Core.Models.Data.Organizations;
public class SelfHostedOrganizationDetails : Organization
{
public int OccupiedSeatCount { get; set; }
public int CollectionCount { get; set; }
public int GroupCount { get; set; }
public IEnumerable<OrganizationUser> OrganizationUsers { get; set; }
public IEnumerable<Policy> Policies { get; set; }
public SsoConfig SsoConfig { get; set; }
public IEnumerable<OrganizationConnection> ScimConnections { get; set; }
public bool CanUseLicense(OrganizationLicense license, out string exception)
{
if (license.Seats.HasValue && OccupiedSeatCount > license.Seats.Value)
{
exception = $"Your organization currently has {OccupiedSeatCount} seats filled. " +
$"Your new license only has ({license.Seats.Value}) seats. Remove some users.";
return false;
}
if (license.MaxCollections.HasValue && CollectionCount > license.MaxCollections.Value)
{
exception = $"Your organization currently has {CollectionCount} collections. " +
$"Your new license allows for a maximum of ({license.MaxCollections.Value}) collections. " +
"Remove some collections.";
return false;
}
if (!license.UseGroups && UseGroups && GroupCount > 1)
{
exception = $"Your organization currently has {GroupCount} groups. " +
$"Your new license does not allow for the use of groups. Remove all groups.";
return false;
}
var enabledPolicyCount = Policies.Count(p => p.Enabled);
if (!license.UsePolicies && UsePolicies && enabledPolicyCount > 0)
{
exception = $"Your organization currently has {enabledPolicyCount} enabled " +
$"policies. Your new license does not allow for the use of policies. Disable all policies.";
return false;
}
if (!license.UseSso && UseSso && SsoConfig is { Enabled: true })
{
exception = $"Your organization currently has a SSO configuration. " +
$"Your new license does not allow for the use of SSO. Disable your SSO configuration.";
return false;
}
if (!license.UseKeyConnector && UseKeyConnector && SsoConfig?.Data != null &&
SsoConfig.GetData().KeyConnectorEnabled)
{
exception = $"Your organization currently has Key Connector enabled. " +
$"Your new license does not allow for the use of Key Connector. Disable your Key Connector.";
return false;
}
if (!license.UseScim && UseScim && ScimConnections != null &&
ScimConnections.Any(c => c.GetConfig<ScimConfig>() is { Enabled: true }))
{
exception = "Your new plan does not allow the SCIM feature. " +
"Disable your SCIM configuration.";
return false;
}
if (!license.UseCustomPermissions && UseCustomPermissions &&
OrganizationUsers.Any(ou => ou.Type == OrganizationUserType.Custom))
{
exception = "Your new plan does not allow the Custom Permissions feature. " +
"Disable your Custom Permissions configuration.";
return false;
}
if (!license.UseResetPassword && UseResetPassword &&
Policies.Any(p => p.Type == PolicyType.ResetPassword && p.Enabled))
{
exception = "Your new license does not allow the Password Reset feature. "
+ "Disable your Password Reset policy.";
return false;
}
exception = "";
return true;
}
public Organization ToOrganization()
{
// Any new Organization properties must be added here for them to flow through to self-hosted organizations
return new Organization
{
Id = Id,
Identifier = Identifier,
Name = Name,
BusinessName = BusinessName,
BusinessAddress1 = BusinessAddress1,
BusinessAddress2 = BusinessAddress2,
BusinessAddress3 = BusinessAddress3,
BusinessCountry = BusinessCountry,
BusinessTaxNumber = BusinessTaxNumber,
BillingEmail = BillingEmail,
Plan = Plan,
PlanType = PlanType,
Seats = Seats,
MaxCollections = MaxCollections,
UsePolicies = UsePolicies,
UseSso = UseSso,
UseKeyConnector = UseKeyConnector,
UseScim = UseScim,
UseGroups = UseGroups,
UseDirectory = UseDirectory,
UseEvents = UseEvents,
UseTotp = UseTotp,
Use2fa = Use2fa,
UseApi = UseApi,
UseResetPassword = UseResetPassword,
UseSecretsManager = UseSecretsManager,
SelfHost = SelfHost,
UsersGetPremium = UsersGetPremium,
UseCustomPermissions = UseCustomPermissions,
Storage = Storage,
MaxStorageGb = MaxStorageGb,
Gateway = Gateway,
GatewayCustomerId = GatewayCustomerId,
GatewaySubscriptionId = GatewaySubscriptionId,
ReferenceData = ReferenceData,
Enabled = Enabled,
LicenseKey = LicenseKey,
PublicKey = PublicKey,
PrivateKey = PrivateKey,
TwoFactorProviders = TwoFactorProviders,
ExpirationDate = ExpirationDate,
CreationDate = CreationDate,
RevisionDate = RevisionDate,
MaxAutoscaleSeats = MaxAutoscaleSeats,
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
};
}
}

View File

@ -32,7 +32,7 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
throw new BadRequestException("Invalid installation id");
}
var subInfo = await _paymentService.GetSubscriptionAsync(organization);
return new OrganizationLicense(organization, subInfo, installationId, _licensingService, version);
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization);
return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version);
}
}

View File

@ -0,0 +1,13 @@
#nullable enable
using Bit.Core.Entities;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
namespace Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
public interface IUpdateOrganizationLicenseCommand
{
Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,
OrganizationLicense license, Organization? currentOrganizationUsingLicenseKey);
}

View File

@ -0,0 +1,66 @@
#nullable enable
using System.Text.Json;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
namespace Bit.Core.OrganizationFeatures.OrganizationLicenses;
public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseCommand
{
private readonly ILicensingService _licensingService;
private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationService _organizationService;
public UpdateOrganizationLicenseCommand(
ILicensingService licensingService,
IGlobalSettings globalSettings,
IOrganizationService organizationService)
{
_licensingService = licensingService;
_globalSettings = globalSettings;
_organizationService = organizationService;
}
public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,
OrganizationLicense license, Organization? currentOrganizationUsingLicenseKey)
{
if (currentOrganizationUsingLicenseKey != null && currentOrganizationUsingLicenseKey.Id != selfHostedOrganization.Id)
{
throw new BadRequestException("License is already in use by another organization.");
}
var canUse = license.CanUse(_globalSettings, _licensingService, out var exception) &&
selfHostedOrganization.CanUseLicense(license, out exception);
if (!canUse)
{
throw new BadRequestException(exception);
}
await WriteLicenseFileAsync(selfHostedOrganization, license);
await UpdateOrganizationAsync(selfHostedOrganization, license);
}
private async Task WriteLicenseFileAsync(Organization organization, OrganizationLicense license)
{
var dir = $"{_globalSettings.LicenseDirectory}/organization";
Directory.CreateDirectory(dir);
await using var fs = new FileStream(Path.Combine(dir, $"{organization.Id}.json"), FileMode.Create);
await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);
}
private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)
{
var organization = selfHostedOrganizationDetails.ToOrganization();
organization.UpdateFromLicense(license);
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
}
}

View File

@ -36,7 +36,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationApiKeyCommandsQueries();
services.AddOrganizationCollectionCommands();
services.AddOrganizationGroupCommands();
services.AddOrganizationLicenseCommandQueries();
services.AddOrganizationLicenseCommandsQueries();
services.AddOrganizationDomainCommandsQueries();
}
@ -91,10 +91,11 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IUpdateGroupCommand, UpdateGroupCommand>();
}
private static void AddOrganizationLicenseCommandQueries(this IServiceCollection services)
private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services)
{
services.AddScoped<ICloudGetOrganizationLicenseQuery, CloudGetOrganizationLicenseQuery>();
services.AddScoped<ISelfHostedGetOrganizationLicenseQuery, SelfHostedGetOrganizationLicenseQuery>();
services.AddScoped<IUpdateOrganizationLicenseCommand, UpdateOrganizationLicenseCommand>();
}
private static void AddOrganizationDomainCommandsQueries(this IServiceCollection services)

View File

@ -11,4 +11,6 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
Task<ICollection<Organization>> SearchAsync(string name, string userEmail, bool? paid, int skip, int take);
Task UpdateStorageAsync(Guid id);
Task<ICollection<OrganizationAbility>> GetManyAbilitiesAsync();
Task<Organization> GetByLicenseKeyAsync(string licenseKey);
Task<SelfHostedOrganizationDetails> GetSelfHostedOrganizationDetailsById(Guid id);
}

View File

@ -13,6 +13,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
Task<OrganizationUser> GetByOrganizationAsync(Guid organizationId, Guid userId);
Task<Tuple<OrganizationUser, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);

View File

@ -20,7 +20,6 @@ public interface IOrganizationService
Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup organizationSignup, bool provider = false);
Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationLicense license, User owner,
string ownerKey, string collectionName, string publicKey, string privateKey);
Task UpdateLicenseAsync(Guid organizationId, OrganizationLicense license);
Task DeleteAsync(Organization organization);
Task EnableAsync(Guid organizationId, DateTime? expirationDate);
Task DisableAsync(Guid organizationId, DateTime? expirationDate);
@ -70,5 +69,5 @@ public interface IOrganizationService
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser, IUserService userService);
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
Task<int> GetOccupiedSeatCount(Organization organization);
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
}

View File

@ -39,7 +39,6 @@ public class OrganizationService : IOrganizationService
private readonly ISsoUserRepository _ssoUserRepository;
private readonly IReferenceEventService _referenceEventService;
private readonly IGlobalSettings _globalSettings;
private readonly ITaxRateRepository _taxRateRepository;
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
private readonly ICurrentContext _currentContext;
@ -68,7 +67,6 @@ public class OrganizationService : IOrganizationService
ISsoUserRepository ssoUserRepository,
IReferenceEventService referenceEventService,
IGlobalSettings globalSettings,
ITaxRateRepository taxRateRepository,
IOrganizationApiKeyRepository organizationApiKeyRepository,
IOrganizationConnectionRepository organizationConnectionRepository,
ICurrentContext currentContext,
@ -96,7 +94,6 @@ public class OrganizationService : IOrganizationService
_ssoUserRepository = ssoUserRepository;
_referenceEventService = referenceEventService;
_globalSettings = globalSettings;
_taxRateRepository = taxRateRepository;
_organizationApiKeyRepository = organizationApiKeyRepository;
_organizationConnectionRepository = organizationConnectionRepository;
_currentContext = currentContext;
@ -119,7 +116,7 @@ public class OrganizationService : IOrganizationService
paymentMethodType, paymentToken);
if (updated)
{
await ReplaceAndUpdateCache(organization);
await ReplaceAndUpdateCacheAsync(organization);
}
}
@ -205,7 +202,7 @@ public class OrganizationService : IOrganizationService
(newPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
if (!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
{
var occupiedSeats = await GetOccupiedSeatCount(organization);
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
if (occupiedSeats > newPlanSeats)
{
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
@ -346,7 +343,7 @@ public class OrganizationService : IOrganizationService
organization.Enabled = success;
organization.PublicKey = upgrade.PublicKey;
organization.PrivateKey = upgrade.PrivateKey;
await ReplaceAndUpdateCache(organization);
await ReplaceAndUpdateCacheAsync(organization);
if (success)
{
await _referenceEventService.RaiseEventAsync(
@ -392,7 +389,7 @@ public class OrganizationService : IOrganizationService
PlanType = plan.Type,
Storage = storageAdjustmentGb,
});
await ReplaceAndUpdateCache(organization);
await ReplaceAndUpdateCacheAsync(organization);
return secret;
}
@ -451,7 +448,7 @@ public class OrganizationService : IOrganizationService
organization.MaxAutoscaleSeats = maxAutoscaleSeats;
await ReplaceAndUpdateCache(organization);
await ReplaceAndUpdateCacheAsync(organization);
}
public async Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment, DateTime? prorationDate = null)
@ -513,7 +510,7 @@ public class OrganizationService : IOrganizationService
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
{
var occupiedSeats = await GetOccupiedSeatCount(organization);
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
if (occupiedSeats > newSeatTotal)
{
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
@ -531,7 +528,7 @@ public class OrganizationService : IOrganizationService
PreviousSeats = organization.Seats
});
organization.Seats = (short?)newSeatTotal;
await ReplaceAndUpdateCache(organization);
await ReplaceAndUpdateCacheAsync(organization);
if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue && organization.Seats == organization.MaxAutoscaleSeats)
{
@ -697,21 +694,10 @@ public class OrganizationService : IOrganizationService
OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey,
string privateKey)
{
if (license?.LicenseType != null && license.LicenseType != LicenseType.Organization)
var canUse = license.CanUse(_globalSettings, _licensingService, out var exception);
if (!canUse)
{
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))
{
throw new BadRequestException("Invalid license.");
}
if (!license.CanUse(_globalSettings))
{
throw new BadRequestException("Invalid license. Make sure your license allows for on-premise " +
"hosting of organizations and that the installation id matches your current installation.");
throw new BadRequestException(exception);
}
if (license.PlanType != PlanType.Custom &&
@ -843,172 +829,6 @@ public class OrganizationService : IOrganizationService
}
}
public async Task UpdateLicenseAsync(Guid organizationId, OrganizationLicense license)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
{
throw new NotFoundException();
}
if (!_globalSettings.SelfHosted)
{
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))
{
throw new BadRequestException("Invalid license.");
}
if (!license.CanUse(_globalSettings))
{
throw new BadRequestException("Invalid license. Make sure your license allows for on-premise " +
"hosting of organizations and that the installation id matches your current installation.");
}
var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();
if (enabledOrgs.Any(o => string.Equals(o.LicenseKey, license.LicenseKey) && o.Id != organizationId))
{
throw new BadRequestException("License is already in use by another organization.");
}
if (license.Seats.HasValue &&
(!organization.Seats.HasValue || organization.Seats.Value > license.Seats.Value))
{
var occupiedSeats = await GetOccupiedSeatCount(organization);
if (occupiedSeats > license.Seats.Value)
{
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
$"Your new license only has ({license.Seats.Value}) seats. Remove some users.");
}
}
if (license.MaxCollections.HasValue && (!organization.MaxCollections.HasValue ||
organization.MaxCollections.Value > license.MaxCollections.Value))
{
var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id);
if (collectionCount > license.MaxCollections.Value)
{
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.");
}
}
if (!license.UsePolicies && organization.UsePolicies)
{
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id);
if (policies.Any(p => p.Enabled))
{
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.");
}
}
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.UseScim && organization.UseScim)
{
var scimConnections = await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,
OrganizationConnectionType.Scim);
if (scimConnections != null && scimConnections.Any(c => c.GetConfig<ScimConfig>()?.Enabled == true))
{
throw new BadRequestException("Your new plan does not allow the SCIM feature. " +
"Disable your SCIM configuration.");
}
}
if (!license.UseCustomPermissions && organization.UseCustomPermissions)
{
var organizationCustomUsers =
await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id,
OrganizationUserType.Custom);
if (organizationCustomUsers.Any())
{
throw new BadRequestException("Your new plan does not allow the Custom Permissions feature. " +
"Disable your Custom Permissions configuration.");
}
}
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.");
}
}
var dir = $"{_globalSettings.LicenseDirectory}/organization";
Directory.CreateDirectory(dir);
await using var fs = new FileStream(Path.Combine(dir, $"{organization.Id}.json"), FileMode.Create);
await JsonSerializer.SerializeAsync(fs, license, JsonHelpers.Indented);
organization.Name = license.Name;
organization.BusinessName = license.BusinessName;
organization.BillingEmail = license.BillingEmail;
organization.PlanType = license.PlanType;
organization.Seats = license.Seats;
organization.MaxCollections = license.MaxCollections;
organization.UseGroups = license.UseGroups;
organization.UseDirectory = license.UseDirectory;
organization.UseEvents = license.UseEvents;
organization.UseTotp = license.UseTotp;
organization.Use2fa = license.Use2fa;
organization.UseApi = license.UseApi;
organization.UsePolicies = license.UsePolicies;
organization.UseSso = license.UseSso;
organization.UseKeyConnector = license.UseKeyConnector;
organization.UseScim = license.UseScim;
organization.UseResetPassword = license.UseResetPassword;
organization.SelfHost = license.SelfHost;
organization.UsersGetPremium = license.UsersGetPremium;
organization.UseCustomPermissions = license.UseCustomPermissions;
organization.Plan = license.Plan;
organization.Enabled = license.Enabled;
organization.ExpirationDate = license.Expires;
organization.LicenseKey = license.LicenseKey;
organization.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(organization);
}
public async Task DeleteAsync(Organization organization)
{
await ValidateDeleteOrganizationAsync(organization);
@ -1038,7 +858,7 @@ public class OrganizationService : IOrganizationService
org.Enabled = true;
org.ExpirationDate = expirationDate;
org.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(org);
await ReplaceAndUpdateCacheAsync(org);
}
}
@ -1050,7 +870,7 @@ public class OrganizationService : IOrganizationService
org.Enabled = false;
org.ExpirationDate = expirationDate;
org.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(org);
await ReplaceAndUpdateCacheAsync(org);
// TODO: send email to owners?
}
@ -1063,7 +883,7 @@ public class OrganizationService : IOrganizationService
{
org.ExpirationDate = expirationDate;
org.RevisionDate = DateTime.UtcNow;
await ReplaceAndUpdateCache(org);
await ReplaceAndUpdateCacheAsync(org);
}
}
@ -1073,7 +893,7 @@ public class OrganizationService : IOrganizationService
if (org != null && !org.Enabled)
{
org.Enabled = true;
await ReplaceAndUpdateCache(org);
await ReplaceAndUpdateCacheAsync(org);
}
}
@ -1093,7 +913,7 @@ public class OrganizationService : IOrganizationService
}
}
await ReplaceAndUpdateCache(organization, EventType.Organization_Updated);
await ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated);
if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{
@ -1193,7 +1013,7 @@ public class OrganizationService : IOrganizationService
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
if (organization.Seats.HasValue)
{
var occupiedSeats = await GetOccupiedSeatCount(organization);
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var availableSeats = organization.Seats.Value - occupiedSeats;
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
}
@ -2044,7 +1864,7 @@ public class OrganizationService : IOrganizationService
var enoughSeatsAvailable = true;
if (organization.Seats.HasValue)
{
var occupiedSeats = await GetOccupiedSeatCount(organization);
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
seatsAvailable = organization.Seats.Value - occupiedSeats;
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
}
@ -2213,7 +2033,7 @@ public class OrganizationService : IOrganizationService
return devices.Where(d => !string.IsNullOrWhiteSpace(d.PushToken)).Select(d => d.Id.ToString());
}
private async Task ReplaceAndUpdateCache(Organization org, EventType? orgEvent = null)
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
{
await _organizationRepository.ReplaceAsync(org);
await _applicationCacheService.UpsertOrganizationAbilityAsync(org);
@ -2463,7 +2283,7 @@ public class OrganizationService : IOrganizationService
}
var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
var occupiedSeats = await GetOccupiedSeatCount(organization);
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
if (availableSeats < 1)
{
@ -2491,7 +2311,7 @@ public class OrganizationService : IOrganizationService
}
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var occupiedSeats = await GetOccupiedSeatCount(organization);
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
await AutoAddSeatsAsync(organization, newSeatsRequired, DateTime.UtcNow);
@ -2606,10 +2426,4 @@ public class OrganizationService : IOrganizationService
return status;
}
public async Task<int> GetOccupiedSeatCount(Organization organization)
{
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organization.Id);
return orgUsers.Count(ou => ou.OccupiesOrganizationSeat);
}
}