mirror of
https://github.com/bitwarden/server.git
synced 2025-05-23 04:21:05 -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:
parent
7d0bba3a29
commit
4643f5960e
@ -483,7 +483,7 @@ public class AccountController : Controller
|
|||||||
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
|
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
|
||||||
if (orgUser == null && organization.Seats.HasValue)
|
if (orgUser == null && organization.Seats.HasValue)
|
||||||
{
|
{
|
||||||
var occupiedSeats = await _organizationService.GetOccupiedSeatCount(organization);
|
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
var initialSeatCount = organization.Seats.Value;
|
var initialSeatCount = organization.Seats.Value;
|
||||||
var availableSeats = initialSeatCount - occupiedSeats;
|
var availableSeats = initialSeatCount - occupiedSeats;
|
||||||
var prorationDate = DateTime.UtcNow;
|
var prorationDate = DateTime.UtcNow;
|
||||||
|
@ -18,7 +18,7 @@ public class OrganizationViewModel
|
|||||||
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
|
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
|
||||||
UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted);
|
UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted);
|
||||||
UserConfirmedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Confirmed);
|
UserConfirmedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Confirmed);
|
||||||
OccupiedSeatCount = orgUsers.Count(u => u.OccupiesOrganizationSeat);
|
OccupiedSeatCount = UserInvitedCount + UserAcceptedCount + UserConfirmedCount;
|
||||||
CipherCount = ciphers.Count();
|
CipherCount = ciphers.Count();
|
||||||
CollectionCount = collections.Count();
|
CollectionCount = collections.Count();
|
||||||
GroupCount = groups?.Count() ?? 0;
|
GroupCount = groups?.Count() ?? 0;
|
||||||
|
@ -39,6 +39,7 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
||||||
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
||||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
|
private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
|
||||||
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
|
||||||
@ -56,6 +57,7 @@ public class OrganizationsController : Controller
|
|||||||
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
|
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
|
||||||
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
|
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
|
||||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
|
IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand,
|
||||||
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
@ -72,6 +74,7 @@ public class OrganizationsController : Controller
|
|||||||
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
|
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
|
||||||
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
|
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
|
||||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||||
|
_updateOrganizationLicenseCommand = updateOrganizationLicenseCommand;
|
||||||
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
|
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
}
|
}
|
||||||
@ -461,7 +464,15 @@ public class OrganizationsController : Controller
|
|||||||
throw new BadRequestException("Invalid license");
|
throw new BadRequestException("Invalid license");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _organizationService.UpdateLicenseAsync(new Guid(id), license);
|
var selfHostedOrganizationDetails = await _organizationRepository.GetSelfHostedOrganizationDetailsById(orgIdGuid);
|
||||||
|
if (selfHostedOrganizationDetails == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingOrganization = await _organizationRepository.GetByLicenseKeyAsync(license.LicenseKey);
|
||||||
|
|
||||||
|
await _updateOrganizationLicenseCommand.UpdateLicenseAsync(selfHostedOrganizationDetails, license, existingOrganization);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/import")]
|
[HttpPost("{id}/import")]
|
||||||
|
@ -27,6 +27,7 @@ public class SelfHostedOrganizationLicensesController : Controller
|
|||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
|
private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
|
||||||
|
|
||||||
public SelfHostedOrganizationLicensesController(
|
public SelfHostedOrganizationLicensesController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@ -34,7 +35,8 @@ public class SelfHostedOrganizationLicensesController : Controller
|
|||||||
IOrganizationConnectionRepository organizationConnectionRepository,
|
IOrganizationConnectionRepository organizationConnectionRepository,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IUserService userService)
|
IUserService userService,
|
||||||
|
IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand)
|
||||||
{
|
{
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_selfHostedGetOrganizationLicenseQuery = selfHostedGetOrganizationLicenseQuery;
|
_selfHostedGetOrganizationLicenseQuery = selfHostedGetOrganizationLicenseQuery;
|
||||||
@ -42,6 +44,7 @@ public class SelfHostedOrganizationLicensesController : Controller
|
|||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_updateOrganizationLicenseCommand = updateOrganizationLicenseCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("")]
|
[HttpPost("")]
|
||||||
@ -79,25 +82,33 @@ public class SelfHostedOrganizationLicensesController : Controller
|
|||||||
throw new BadRequestException("Invalid license");
|
throw new BadRequestException("Invalid license");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _organizationService.UpdateLicenseAsync(new Guid(id), license);
|
var selfHostedOrganizationDetails = await _organizationRepository.GetSelfHostedOrganizationDetailsById(orgIdGuid);
|
||||||
|
if (selfHostedOrganizationDetails == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentOrganization = await _organizationRepository.GetByLicenseKeyAsync(license.LicenseKey);
|
||||||
|
|
||||||
|
await _updateOrganizationLicenseCommand.UpdateLicenseAsync(selfHostedOrganizationDetails, license, currentOrganization);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/sync")]
|
[HttpPost("{id}/sync")]
|
||||||
public async Task SyncLicenseAsync(string id)
|
public async Task SyncLicenseAsync(string id)
|
||||||
{
|
{
|
||||||
var organization = await _organizationRepository.GetByIdAsync(new Guid(id));
|
var selfHostedOrganizationDetails = await _organizationRepository.GetSelfHostedOrganizationDetailsById(new Guid(id));
|
||||||
if (organization == null)
|
if (selfHostedOrganizationDetails == null)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await _currentContext.OrganizationOwner(organization.Id))
|
if (!await _currentContext.OrganizationOwner(selfHostedOrganizationDetails.Id))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var billingSyncConnection =
|
var billingSyncConnection =
|
||||||
(await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,
|
(await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(selfHostedOrganizationDetails.Id,
|
||||||
OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();
|
OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();
|
||||||
if (billingSyncConnection == null)
|
if (billingSyncConnection == null)
|
||||||
{
|
{
|
||||||
@ -105,9 +116,10 @@ public class SelfHostedOrganizationLicensesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var license =
|
var license =
|
||||||
await _selfHostedGetOrganizationLicenseQuery.GetLicenseAsync(organization, billingSyncConnection);
|
await _selfHostedGetOrganizationLicenseQuery.GetLicenseAsync(selfHostedOrganizationDetails, billingSyncConnection);
|
||||||
|
var currentOrganization = await _organizationRepository.GetByLicenseKeyAsync(license.LicenseKey);
|
||||||
|
|
||||||
await _organizationService.UpdateLicenseAsync(organization.Id, license);
|
await _updateOrganizationLicenseCommand.UpdateLicenseAsync(selfHostedOrganizationDetails, license, currentOrganization);
|
||||||
|
|
||||||
var config = billingSyncConnection.GetConfig<BillingSyncConfig>();
|
var config = billingSyncConnection.GetConfig<BillingSyncConfig>();
|
||||||
config.LastLicenseSync = DateTime.Now;
|
config.LastLicenseSync = DateTime.Now;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Entities;
|
namespace Bit.Core.Entities;
|
||||||
@ -197,4 +198,33 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
|
|||||||
|
|
||||||
return providers[provider];
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
if (!Enabled || Issued > DateTime.UtcNow || Expires < DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
|
exception = "Invalid license. Your organization is disabled or the license has expired.";
|
||||||
return false;
|
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)
|
public bool VerifyData(Organization organization, IGlobalSettings globalSettings)
|
||||||
|
@ -62,14 +62,6 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
|
|||||||
return Premium.GetValueOrDefault(false);
|
return Premium.GetValueOrDefault(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OccupiesOrganizationSeat
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return Status != OrganizationUserStatusType.Revoked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Permissions GetPermissions()
|
public Permissions GetPermissions()
|
||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(Permissions) ? null
|
return string.IsNullOrWhiteSpace(Permissions) ? null
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -32,7 +32,7 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
|
|||||||
throw new BadRequestException("Invalid installation id");
|
throw new BadRequestException("Invalid installation id");
|
||||||
}
|
}
|
||||||
|
|
||||||
var subInfo = await _paymentService.GetSubscriptionAsync(organization);
|
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization);
|
||||||
return new OrganizationLicense(organization, subInfo, installationId, _licensingService, version);
|
return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -36,7 +36,7 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddOrganizationApiKeyCommandsQueries();
|
services.AddOrganizationApiKeyCommandsQueries();
|
||||||
services.AddOrganizationCollectionCommands();
|
services.AddOrganizationCollectionCommands();
|
||||||
services.AddOrganizationGroupCommands();
|
services.AddOrganizationGroupCommands();
|
||||||
services.AddOrganizationLicenseCommandQueries();
|
services.AddOrganizationLicenseCommandsQueries();
|
||||||
services.AddOrganizationDomainCommandsQueries();
|
services.AddOrganizationDomainCommandsQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,10 +91,11 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddScoped<IUpdateGroupCommand, UpdateGroupCommand>();
|
services.AddScoped<IUpdateGroupCommand, UpdateGroupCommand>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddOrganizationLicenseCommandQueries(this IServiceCollection services)
|
private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<ICloudGetOrganizationLicenseQuery, CloudGetOrganizationLicenseQuery>();
|
services.AddScoped<ICloudGetOrganizationLicenseQuery, CloudGetOrganizationLicenseQuery>();
|
||||||
services.AddScoped<ISelfHostedGetOrganizationLicenseQuery, SelfHostedGetOrganizationLicenseQuery>();
|
services.AddScoped<ISelfHostedGetOrganizationLicenseQuery, SelfHostedGetOrganizationLicenseQuery>();
|
||||||
|
services.AddScoped<IUpdateOrganizationLicenseCommand, UpdateOrganizationLicenseCommand>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddOrganizationDomainCommandsQueries(this IServiceCollection services)
|
private static void AddOrganizationDomainCommandsQueries(this IServiceCollection services)
|
||||||
|
@ -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<ICollection<Organization>> SearchAsync(string name, string userEmail, bool? paid, int skip, int take);
|
||||||
Task UpdateStorageAsync(Guid id);
|
Task UpdateStorageAsync(Guid id);
|
||||||
Task<ICollection<OrganizationAbility>> GetManyAbilitiesAsync();
|
Task<ICollection<OrganizationAbility>> GetManyAbilitiesAsync();
|
||||||
|
Task<Organization> GetByLicenseKeyAsync(string licenseKey);
|
||||||
|
Task<SelfHostedOrganizationDetails> GetSelfHostedOrganizationDetailsById(Guid id);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
|||||||
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
|
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
|
||||||
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
|
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
|
||||||
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
|
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<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
|
||||||
Task<OrganizationUser> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
Task<OrganizationUser> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
||||||
Task<Tuple<OrganizationUser, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
|
Task<Tuple<OrganizationUser, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
|
||||||
|
@ -20,7 +20,6 @@ public interface IOrganizationService
|
|||||||
Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup organizationSignup, bool provider = false);
|
Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup organizationSignup, bool provider = false);
|
||||||
Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationLicense license, User owner,
|
Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationLicense license, User owner,
|
||||||
string ownerKey, string collectionName, string publicKey, string privateKey);
|
string ownerKey, string collectionName, string publicKey, string privateKey);
|
||||||
Task UpdateLicenseAsync(Guid organizationId, OrganizationLicense license);
|
|
||||||
Task DeleteAsync(Organization organization);
|
Task DeleteAsync(Organization organization);
|
||||||
Task EnableAsync(Guid organizationId, DateTime? expirationDate);
|
Task EnableAsync(Guid organizationId, DateTime? expirationDate);
|
||||||
Task DisableAsync(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 RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser, IUserService userService);
|
||||||
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
||||||
Task<int> GetOccupiedSeatCount(Organization organization);
|
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly ISsoUserRepository _ssoUserRepository;
|
private readonly ISsoUserRepository _ssoUserRepository;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
private readonly ITaxRateRepository _taxRateRepository;
|
|
||||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
@ -68,7 +67,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
ISsoUserRepository ssoUserRepository,
|
ISsoUserRepository ssoUserRepository,
|
||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
ITaxRateRepository taxRateRepository,
|
|
||||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||||
IOrganizationConnectionRepository organizationConnectionRepository,
|
IOrganizationConnectionRepository organizationConnectionRepository,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@ -96,7 +94,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
_ssoUserRepository = ssoUserRepository;
|
_ssoUserRepository = ssoUserRepository;
|
||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_taxRateRepository = taxRateRepository;
|
|
||||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||||
_organizationConnectionRepository = organizationConnectionRepository;
|
_organizationConnectionRepository = organizationConnectionRepository;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
@ -119,7 +116,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
paymentMethodType, paymentToken);
|
paymentMethodType, paymentToken);
|
||||||
if (updated)
|
if (updated)
|
||||||
{
|
{
|
||||||
await ReplaceAndUpdateCache(organization);
|
await ReplaceAndUpdateCacheAsync(organization);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +202,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
(newPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
(newPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
||||||
if (!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
|
if (!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
|
||||||
{
|
{
|
||||||
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
if (occupiedSeats > newPlanSeats)
|
if (occupiedSeats > newPlanSeats)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
||||||
@ -346,7 +343,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
organization.Enabled = success;
|
organization.Enabled = success;
|
||||||
organization.PublicKey = upgrade.PublicKey;
|
organization.PublicKey = upgrade.PublicKey;
|
||||||
organization.PrivateKey = upgrade.PrivateKey;
|
organization.PrivateKey = upgrade.PrivateKey;
|
||||||
await ReplaceAndUpdateCache(organization);
|
await ReplaceAndUpdateCacheAsync(organization);
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
await _referenceEventService.RaiseEventAsync(
|
await _referenceEventService.RaiseEventAsync(
|
||||||
@ -392,7 +389,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
PlanType = plan.Type,
|
PlanType = plan.Type,
|
||||||
Storage = storageAdjustmentGb,
|
Storage = storageAdjustmentGb,
|
||||||
});
|
});
|
||||||
await ReplaceAndUpdateCache(organization);
|
await ReplaceAndUpdateCacheAsync(organization);
|
||||||
return secret;
|
return secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,7 +448,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
organization.MaxAutoscaleSeats = maxAutoscaleSeats;
|
organization.MaxAutoscaleSeats = maxAutoscaleSeats;
|
||||||
|
|
||||||
await ReplaceAndUpdateCache(organization);
|
await ReplaceAndUpdateCacheAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> AdjustSeatsAsync(Guid organizationId, int seatAdjustment, DateTime? prorationDate = null)
|
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)
|
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
|
||||||
{
|
{
|
||||||
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
if (occupiedSeats > newSeatTotal)
|
if (occupiedSeats > newSeatTotal)
|
||||||
{
|
{
|
||||||
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
||||||
@ -531,7 +528,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
PreviousSeats = organization.Seats
|
PreviousSeats = organization.Seats
|
||||||
});
|
});
|
||||||
organization.Seats = (short?)newSeatTotal;
|
organization.Seats = (short?)newSeatTotal;
|
||||||
await ReplaceAndUpdateCache(organization);
|
await ReplaceAndUpdateCacheAsync(organization);
|
||||||
|
|
||||||
if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue && organization.Seats == organization.MaxAutoscaleSeats)
|
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,
|
OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey,
|
||||||
string privateKey)
|
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. "
|
throw new BadRequestException(exception);
|
||||||
+ "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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (license.PlanType != PlanType.Custom &&
|
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)
|
public async Task DeleteAsync(Organization organization)
|
||||||
{
|
{
|
||||||
await ValidateDeleteOrganizationAsync(organization);
|
await ValidateDeleteOrganizationAsync(organization);
|
||||||
@ -1038,7 +858,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
org.Enabled = true;
|
org.Enabled = true;
|
||||||
org.ExpirationDate = expirationDate;
|
org.ExpirationDate = expirationDate;
|
||||||
org.RevisionDate = DateTime.UtcNow;
|
org.RevisionDate = DateTime.UtcNow;
|
||||||
await ReplaceAndUpdateCache(org);
|
await ReplaceAndUpdateCacheAsync(org);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1050,7 +870,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
org.Enabled = false;
|
org.Enabled = false;
|
||||||
org.ExpirationDate = expirationDate;
|
org.ExpirationDate = expirationDate;
|
||||||
org.RevisionDate = DateTime.UtcNow;
|
org.RevisionDate = DateTime.UtcNow;
|
||||||
await ReplaceAndUpdateCache(org);
|
await ReplaceAndUpdateCacheAsync(org);
|
||||||
|
|
||||||
// TODO: send email to owners?
|
// TODO: send email to owners?
|
||||||
}
|
}
|
||||||
@ -1063,7 +883,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
{
|
{
|
||||||
org.ExpirationDate = expirationDate;
|
org.ExpirationDate = expirationDate;
|
||||||
org.RevisionDate = DateTime.UtcNow;
|
org.RevisionDate = DateTime.UtcNow;
|
||||||
await ReplaceAndUpdateCache(org);
|
await ReplaceAndUpdateCacheAsync(org);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1073,7 +893,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
if (org != null && !org.Enabled)
|
if (org != null && !org.Enabled)
|
||||||
{
|
{
|
||||||
org.Enabled = true;
|
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))
|
if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
@ -1193,7 +1013,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
|
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
|
||||||
if (organization.Seats.HasValue)
|
if (organization.Seats.HasValue)
|
||||||
{
|
{
|
||||||
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
var availableSeats = organization.Seats.Value - occupiedSeats;
|
var availableSeats = organization.Seats.Value - occupiedSeats;
|
||||||
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
|
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
|
||||||
}
|
}
|
||||||
@ -2044,7 +1864,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
var enoughSeatsAvailable = true;
|
var enoughSeatsAvailable = true;
|
||||||
if (organization.Seats.HasValue)
|
if (organization.Seats.HasValue)
|
||||||
{
|
{
|
||||||
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
seatsAvailable = organization.Seats.Value - occupiedSeats;
|
seatsAvailable = organization.Seats.Value - occupiedSeats;
|
||||||
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
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());
|
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 _organizationRepository.ReplaceAsync(org);
|
||||||
await _applicationCacheService.UpsertOrganizationAbilityAsync(org);
|
await _applicationCacheService.UpsertOrganizationAbilityAsync(org);
|
||||||
@ -2463,7 +2283,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
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;
|
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||||
if (availableSeats < 1)
|
if (availableSeats < 1)
|
||||||
{
|
{
|
||||||
@ -2491,7 +2311,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
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 availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||||
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
||||||
await AutoAddSeatsAsync(organization, newSeatsRequired, DateTime.UtcNow);
|
await AutoAddSeatsAsync(organization, newSeatsRequired, DateTime.UtcNow);
|
||||||
@ -2606,10 +2426,4 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> GetOccupiedSeatCount(Organization organization)
|
|
||||||
{
|
|
||||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organization.Id);
|
|
||||||
return orgUsers.Count(ou => ou.OccupiesOrganizationSeat);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -94,4 +94,44 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
|
|||||||
return results.ToList();
|
return results.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Organization> GetByLicenseKeyAsync(string licenseKey)
|
||||||
|
{
|
||||||
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var result = await connection.QueryAsync<Organization>(
|
||||||
|
"[dbo].[Organization_ReadByLicenseKey]",
|
||||||
|
new { LicenseKey = licenseKey },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return result.SingleOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SelfHostedOrganizationDetails> GetSelfHostedOrganizationDetailsById(Guid id)
|
||||||
|
{
|
||||||
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var result = await connection.QueryMultipleAsync(
|
||||||
|
"[dbo].[Organization_ReadSelfHostedDetailsById]",
|
||||||
|
new { Id = id },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
var selfHostOrganization = await result.ReadSingleOrDefaultAsync<SelfHostedOrganizationDetails>();
|
||||||
|
if (selfHostOrganization == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
selfHostOrganization.OccupiedSeatCount = await result.ReadSingleAsync<int>();
|
||||||
|
selfHostOrganization.CollectionCount = await result.ReadSingleAsync<int>();
|
||||||
|
selfHostOrganization.GroupCount = await result.ReadSingleAsync<int>();
|
||||||
|
selfHostOrganization.OrganizationUsers = await result.ReadAsync<OrganizationUser>();
|
||||||
|
selfHostOrganization.Policies = await result.ReadAsync<Policy>();
|
||||||
|
selfHostOrganization.SsoConfig = await result.ReadFirstOrDefaultAsync<SsoConfig>();
|
||||||
|
selfHostOrganization.ScimConnections = await result.ReadAsync<OrganizationConnection>();
|
||||||
|
|
||||||
|
return selfHostOrganization;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,19 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
{
|
||||||
|
var result = await connection.ExecuteScalarAsync<int>(
|
||||||
|
"[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]",
|
||||||
|
new { OrganizationId = organizationId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails,
|
public async Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails,
|
||||||
bool onlyRegisteredUsers)
|
bool onlyRegisteredUsers)
|
||||||
{
|
{
|
||||||
|
@ -8,6 +8,7 @@ public class Organization : Core.Entities.Organization
|
|||||||
public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; }
|
public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; }
|
||||||
public virtual ICollection<Group> Groups { get; set; }
|
public virtual ICollection<Group> Groups { get; set; }
|
||||||
public virtual ICollection<Policy> Policies { get; set; }
|
public virtual ICollection<Policy> Policies { get; set; }
|
||||||
|
public virtual ICollection<Collection> Collections { get; set; }
|
||||||
public virtual ICollection<SsoConfig> SsoConfigs { get; set; }
|
public virtual ICollection<SsoConfig> SsoConfigs { get; set; }
|
||||||
public virtual ICollection<SsoUser> SsoUsers { get; set; }
|
public virtual ICollection<SsoUser> SsoUsers { get; set; }
|
||||||
public virtual ICollection<Transaction> Transactions { get; set; }
|
public virtual ICollection<Transaction> Transactions { get; set; }
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Organization = Bit.Infrastructure.EntityFramework.Models.Organization;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Repositories;
|
namespace Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
|
||||||
@ -139,4 +140,40 @@ public class OrganizationRepository : Repository<Core.Entities.Organization, Org
|
|||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Core.Entities.Organization> GetByLicenseKeyAsync(string licenseKey)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var organization = await GetDbSet(dbContext)
|
||||||
|
.FirstOrDefaultAsync(o => o.LicenseKey == licenseKey);
|
||||||
|
|
||||||
|
return organization;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SelfHostedOrganizationDetails> GetSelfHostedOrganizationDetailsById(Guid id)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var organization = await GetDbSet(dbContext).FindAsync(id);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selfHostOrganization = Mapper.Map<SelfHostedOrganizationDetails>(organization);
|
||||||
|
|
||||||
|
selfHostOrganization.OccupiedSeatCount =
|
||||||
|
organization.OrganizationUsers.Count(ou => ou.Status >= OrganizationUserStatusType.Invited);
|
||||||
|
selfHostOrganization.CollectionCount = organization.Collections?.Count ?? 0;
|
||||||
|
selfHostOrganization.GroupCount = organization?.Groups.Count ?? 0;
|
||||||
|
selfHostOrganization.SsoConfig = organization.SsoConfigs.SingleOrDefault();
|
||||||
|
selfHostOrganization.ScimConnections = organization.Connections.Where(c => c.Type == OrganizationConnectionType.Scim);
|
||||||
|
|
||||||
|
return selfHostOrganization;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,6 +197,12 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
|||||||
return await GetCountFromQuery(query);
|
return await GetCountFromQuery(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
var query = new OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery(organizationId);
|
||||||
|
return await GetCountFromQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<int> GetCountByOrganizationIdAsync(Guid organizationId)
|
public async Task<int> GetCountByOrganizationIdAsync(Guid organizationId)
|
||||||
{
|
{
|
||||||
var query = new OrganizationUserReadCountByOrganizationIdQuery(organizationId);
|
var query = new OrganizationUserReadCountByOrganizationIdQuery(organizationId);
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Models;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||||
|
|
||||||
|
public class OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery : IQuery<OrganizationUser>
|
||||||
|
{
|
||||||
|
private readonly Guid _organizationId;
|
||||||
|
|
||||||
|
public OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery(Guid organizationId)
|
||||||
|
{
|
||||||
|
_organizationId = organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)
|
||||||
|
{
|
||||||
|
var query = from ou in dbContext.OrganizationUsers
|
||||||
|
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited
|
||||||
|
select ou;
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
}
|
@ -454,6 +454,9 @@
|
|||||||
<Build Include="SecretsManager\dbo\Tables\ServiceAccount.sql" />
|
<Build Include="SecretsManager\dbo\Tables\ServiceAccount.sql" />
|
||||||
<Build Include="SecretsManager\dbo\Views\ApiKeyDetailsView.sql" />
|
<Build Include="SecretsManager\dbo\Views\ApiKeyDetailsView.sql" />
|
||||||
<Build Include="SecretsManager\dbo\Views\ApiKeyView.sql" />
|
<Build Include="SecretsManager\dbo\Views\ApiKeyView.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\Organization_ReadByLicenseKey.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\Organization_ReadSelfHostedDetailsById.sql" />
|
||||||
|
<Build Include="dbo\Stored Procedures\OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="dbo\Stored Procedures\OrganizationDomain_Create.sql" />
|
<Content Include="dbo\Stored Procedures\OrganizationDomain_Create.sql" />
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Group_ReadCountByOrganizationId]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(1)
|
||||||
|
FROM
|
||||||
|
[dbo].[Group]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @OrganizationId
|
||||||
|
END
|
@ -0,0 +1,14 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(1)
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUserView]
|
||||||
|
WHERE
|
||||||
|
OrganizationId = @OrganizationId
|
||||||
|
AND Status >= 0 --Invited
|
||||||
|
END
|
@ -0,0 +1,13 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Organization_ReadByLicenseKey]
|
||||||
|
@LicenseKey VARCHAR (100)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationView]
|
||||||
|
WHERE
|
||||||
|
[LicenseKey] = @LicenseKey
|
||||||
|
END
|
@ -0,0 +1,15 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Organization_ReadSelfHostedDetailsById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
EXEC [dbo].[Organization_ReadById] @Id
|
||||||
|
EXEC [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId] @Id
|
||||||
|
EXEC [dbo].[Collection_ReadCountByOrganizationId] @Id
|
||||||
|
EXEC [dbo].[Group_ReadCountByOrganizationId] @Id
|
||||||
|
EXEC [dbo].[OrganizationUser_ReadByOrganizationId] @Id, NULL
|
||||||
|
EXEC [dbo].[Policy_ReadByOrganizationId] @Id
|
||||||
|
EXEC [dbo].[SsoConfig_ReadByOrganizationId] @Id
|
||||||
|
EXEC [dbo].[OrganizationConnection_ReadByOrganizationIdType] @Id, 2 --Scim connection type
|
||||||
|
END
|
@ -32,6 +32,7 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||||
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
||||||
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
||||||
|
private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand;
|
||||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||||
|
|
||||||
private readonly OrganizationsController _sut;
|
private readonly OrganizationsController _sut;
|
||||||
@ -53,11 +54,13 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_userService = Substitute.For<IUserService>();
|
_userService = Substitute.For<IUserService>();
|
||||||
_cloudGetOrganizationLicenseQuery = Substitute.For<ICloudGetOrganizationLicenseQuery>();
|
_cloudGetOrganizationLicenseQuery = Substitute.For<ICloudGetOrganizationLicenseQuery>();
|
||||||
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
|
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
|
||||||
|
_updateOrganizationLicenseCommand = Substitute.For<IUpdateOrganizationLicenseCommand>();
|
||||||
|
|
||||||
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository,
|
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository,
|
||||||
_policyRepository, _organizationService, _userService, _paymentService, _currentContext,
|
_policyRepository, _organizationService, _userService, _paymentService, _currentContext,
|
||||||
_ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand,
|
_ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand,
|
||||||
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _cloudGetOrganizationLicenseQuery, _globalSettings);
|
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand,
|
||||||
|
_cloudGetOrganizationLicenseQuery, _globalSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
379
test/Core.Test/Models/Data/SelfHostedOrganizationDetailsTests.cs
Normal file
379
test/Core.Test/Models/Data/SelfHostedOrganizationDetailsTests.cs
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||||
|
using Bit.Core.Test.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Models.Data;
|
||||||
|
|
||||||
|
public class SelfHostedOrganizationDetailsTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_Success(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(string.IsNullOrEmpty(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_OccupiedSeatCount_ExceedsLicense_Fail(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.Seats = 1;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.Contains("Remove some users", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_MaxCollections_ExceedsLicense_Fail(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.MaxCollections = 1;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.Contains("Remove some collections", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_Groups_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseGroups = false;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.Contains("Your new license does not allow for the use of groups", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_Policies_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UsePolicies = false;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.Contains("Your new license does not allow for the use of policies", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_DisabledPolicies_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UsePolicies = false;
|
||||||
|
((List<Policy>)orgDetails.Policies).ForEach(p => p.Enabled = false);
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(string.IsNullOrEmpty(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_Sso_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseSso = false;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.Contains("Your new license does not allow for the use of SSO", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_DisabledSso_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseSso = false;
|
||||||
|
orgDetails.SsoConfig.Enabled = false;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(string.IsNullOrEmpty(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_NoSso_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseSso = false;
|
||||||
|
orgDetails.SsoConfig = null;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(string.IsNullOrEmpty(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_KeyConnector_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseKeyConnector = false;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.Contains("Your new license does not allow for the use of Key Connector", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_DisabledKeyConnector_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseKeyConnector = false;
|
||||||
|
orgDetails.SsoConfig.SetData(new SsoConfigurationData() { KeyConnectorEnabled = false });
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(string.IsNullOrEmpty(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_NoSsoKeyConnector_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseKeyConnector = false;
|
||||||
|
orgDetails.SsoConfig = null;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(string.IsNullOrEmpty(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_Scim_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseScim = false;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.Contains("Your new plan does not allow the SCIM feature", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_DisabledScim_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseScim = false;
|
||||||
|
((List<OrganizationConnection<ScimConfig>>)orgDetails.ScimConnections)
|
||||||
|
.ForEach(c => c.SetConfig(new ScimConfig() { Enabled = false }));
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(string.IsNullOrEmpty(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_NoScimConfig_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseScim = false;
|
||||||
|
orgDetails.ScimConnections = null;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(string.IsNullOrEmpty(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_CustomPermissions_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseCustomPermissions = false;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.Contains("Your new plan does not allow the Custom Permissions feature", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_NoCustomPermissions_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseCustomPermissions = false;
|
||||||
|
((List<OrganizationUser>)orgDetails.OrganizationUsers).ForEach(ou => ou.Type = OrganizationUserType.User);
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(string.IsNullOrEmpty(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_ResetPassword_NotAllowedByLicense_Fail(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseResetPassword = false;
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.Contains("Your new license does not allow the Password Reset feature", exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[OrganizationLicenseCustomize]
|
||||||
|
public async Task ValidateForOrganization_DisabledResetPassword_NotAllowedByLicense_Success(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license);
|
||||||
|
orgLicense.UseResetPassword = false;
|
||||||
|
((List<Policy>)orgDetails.Policies).ForEach(p => p.Enabled = false);
|
||||||
|
|
||||||
|
var result = orgDetails.CanUseLicense(license, out var exception);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(string.IsNullOrEmpty(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
private (SelfHostedOrganizationDetails organization, OrganizationLicense license) GetOrganizationAndLicense(List<OrganizationUser> orgUsers,
|
||||||
|
List<Policy> policies, SsoConfig ssoConfig, List<OrganizationConnection<ScimConfig>> scimConnections, OrganizationLicense license)
|
||||||
|
{
|
||||||
|
// The default state is that all features are used by Org and allowed by License
|
||||||
|
// Each test then toggles on/off as necessary
|
||||||
|
policies.ForEach(p => p.Enabled = true);
|
||||||
|
policies.First().Type = PolicyType.ResetPassword;
|
||||||
|
|
||||||
|
ssoConfig.Enabled = true;
|
||||||
|
ssoConfig.SetData(new SsoConfigurationData()
|
||||||
|
{
|
||||||
|
KeyConnectorEnabled = true
|
||||||
|
});
|
||||||
|
|
||||||
|
var enabledScimConfig = new ScimConfig() { Enabled = true };
|
||||||
|
scimConnections.ForEach(c => c.Config = enabledScimConfig);
|
||||||
|
|
||||||
|
orgUsers.First().Type = OrganizationUserType.Custom;
|
||||||
|
|
||||||
|
var organization = new SelfHostedOrganizationDetails()
|
||||||
|
{
|
||||||
|
OccupiedSeatCount = 10,
|
||||||
|
CollectionCount = 5,
|
||||||
|
GroupCount = 5,
|
||||||
|
OrganizationUsers = orgUsers,
|
||||||
|
Policies = policies,
|
||||||
|
SsoConfig = ssoConfig,
|
||||||
|
ScimConnections = scimConnections,
|
||||||
|
|
||||||
|
UsePolicies = true,
|
||||||
|
UseSso = true,
|
||||||
|
UseKeyConnector = true,
|
||||||
|
UseScim = true,
|
||||||
|
UseGroups = true,
|
||||||
|
UseDirectory = true,
|
||||||
|
UseEvents = true,
|
||||||
|
UseTotp = true,
|
||||||
|
Use2fa = true,
|
||||||
|
UseApi = true,
|
||||||
|
UseResetPassword = true,
|
||||||
|
SelfHost = true,
|
||||||
|
UsersGetPremium = true,
|
||||||
|
UseCustomPermissions = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
license.Enabled = true;
|
||||||
|
license.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
license.Seats = 10;
|
||||||
|
license.MaxCollections = 5;
|
||||||
|
license.UsePolicies = true;
|
||||||
|
license.UseSso = true;
|
||||||
|
license.UseKeyConnector = true;
|
||||||
|
license.UseScim = true;
|
||||||
|
license.UseGroups = true;
|
||||||
|
license.UseEvents = true;
|
||||||
|
license.UseDirectory = true;
|
||||||
|
license.UseTotp = true;
|
||||||
|
license.Use2fa = true;
|
||||||
|
license.UseApi = true;
|
||||||
|
license.UseResetPassword = true;
|
||||||
|
license.MaxStorageGb = 1;
|
||||||
|
license.SelfHost = true;
|
||||||
|
license.UsersGetPremium = true;
|
||||||
|
license.UseCustomPermissions = true;
|
||||||
|
license.Version = 11;
|
||||||
|
license.Issued = DateTime.Now;
|
||||||
|
license.Expires = DateTime.Now.AddYears(1);
|
||||||
|
|
||||||
|
return (organization, license);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Group_ReadCountByOrganizationId]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(1)
|
||||||
|
FROM
|
||||||
|
[dbo].[Group]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @OrganizationId
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(1)
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUserView]
|
||||||
|
WHERE
|
||||||
|
OrganizationId = @OrganizationId
|
||||||
|
AND Status >= 0 --Invited
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadByLicenseKey]
|
||||||
|
@LicenseKey VARCHAR (100)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationView]
|
||||||
|
WHERE
|
||||||
|
[LicenseKey] = @LicenseKey
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadSelfHostedDetailsById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
EXEC [dbo].[Organization_ReadById] @Id
|
||||||
|
EXEC [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId] @Id
|
||||||
|
EXEC [dbo].[Collection_ReadCountByOrganizationId] @Id
|
||||||
|
EXEC [dbo].[Group_ReadCountByOrganizationId] @Id
|
||||||
|
EXEC [dbo].[OrganizationUser_ReadByOrganizationId] @Id, NULL
|
||||||
|
EXEC [dbo].[Policy_ReadByOrganizationId] @Id
|
||||||
|
EXEC [dbo].[SsoConfig_ReadByOrganizationId] @Id
|
||||||
|
EXEC [dbo].[OrganizationConnection_ReadByOrganizationIdType] @Id, 2 --Scim connection type
|
||||||
|
END
|
||||||
|
GO
|
Loading…
x
Reference in New Issue
Block a user