1
0
mirror of https://github.com/bitwarden/server.git synced 2024-12-13 15:36:45 +01:00

Merge branch 'main' into pm-14302-Suspended-icon-remains-after-the-suspended-org-status-updated-to-Active

This commit is contained in:
cyprain-okeke 2024-12-12 13:34:20 +01:00 committed by GitHub
commit f33f93dc0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 10165 additions and 29 deletions

12
.github/CODEOWNERS vendored
View File

@ -15,11 +15,7 @@
## These are shared workflows ##
.github/workflows/_move_finalization_db_scripts.yml
.github/workflows/build.yml
.github/workflows/cleanup-after-pr.yml
.github/workflows/cleanup-rc-branch.yml
.github/workflows/release.yml
.github/workflows/repository-management.yml
# Database Operations for database changes
src/Sql/** @bitwarden/dept-dbops
@ -68,6 +64,14 @@ src/EventsProcessor @bitwarden/team-admin-console-dev
src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev
src/Admin/Views/Tools @bitwarden/team-billing-dev
# Platform team
.github/workflows/build.yml @bitwarden/team-platform-dev
.github/workflows/cleanup-after-pr.yml @bitwarden/team-platform-dev
.github/workflows/cleanup-rc-branch.yml @bitwarden/team-platform-dev
.github/workflows/repository-management.yml @bitwarden/team-platform-dev
.github/workflows/test-database.yml @bitwarden/team-platform-dev
.github/workflows/test.yml @bitwarden/team-platform-dev
# Multiple owners - DO NOT REMOVE (BRE)
**/packages.lock.json
Directory.Build.props

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2024.12.0</Version>
<Version>2024.12.1</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
@ -64,4 +64,4 @@
</ItemGroup>
</Target>
</Project>
</Project>

View File

@ -3,7 +3,9 @@ using System.Text.Json;
using Bit.Admin.Enums;
using Bit.Admin.Models;
using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Models.BitStripe;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
@ -28,6 +30,7 @@ public class ToolsController : Controller
private readonly ITransactionRepository _transactionRepository;
private readonly IInstallationRepository _installationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IProviderUserRepository _providerUserRepository;
private readonly IPaymentService _paymentService;
private readonly ITaxRateRepository _taxRateRepository;
private readonly IStripeAdapter _stripeAdapter;
@ -41,6 +44,7 @@ public class ToolsController : Controller
ITransactionRepository transactionRepository,
IInstallationRepository installationRepository,
IOrganizationUserRepository organizationUserRepository,
IProviderUserRepository providerUserRepository,
ITaxRateRepository taxRateRepository,
IPaymentService paymentService,
IStripeAdapter stripeAdapter,
@ -53,6 +57,7 @@ public class ToolsController : Controller
_transactionRepository = transactionRepository;
_installationRepository = installationRepository;
_organizationUserRepository = organizationUserRepository;
_providerUserRepository = providerUserRepository;
_taxRateRepository = taxRateRepository;
_paymentService = paymentService;
_stripeAdapter = stripeAdapter;
@ -220,6 +225,46 @@ public class ToolsController : Controller
return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId.Value });
}
[RequireFeature(FeatureFlagKeys.PromoteProviderServiceUserTool)]
[RequirePermission(Permission.Tools_PromoteProviderServiceUser)]
public IActionResult PromoteProviderServiceUser()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
[RequireFeature(FeatureFlagKeys.PromoteProviderServiceUserTool)]
[RequirePermission(Permission.Tools_PromoteProviderServiceUser)]
public async Task<IActionResult> PromoteProviderServiceUser(PromoteProviderServiceUserModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var providerUsers = await _providerUserRepository.GetManyByProviderAsync(
model.ProviderId.Value, null);
var serviceUser = providerUsers.FirstOrDefault(u => u.UserId == model.UserId.Value);
if (serviceUser == null)
{
ModelState.AddModelError(nameof(model.UserId), "Service User Id not found in this provider.");
}
else if (serviceUser.Type != Core.AdminConsole.Enums.Provider.ProviderUserType.ServiceUser)
{
ModelState.AddModelError(nameof(model.UserId), "User is not a service user of this provider.");
}
if (!ModelState.IsValid)
{
return View(model);
}
serviceUser.Type = Core.AdminConsole.Enums.Provider.ProviderUserType.ProviderAdmin;
await _providerUserRepository.ReplaceAsync(serviceUser);
return RedirectToAction("Edit", "Providers", new { id = model.ProviderId.Value });
}
[RequirePermission(Permission.Tools_GenerateLicenseFile)]
public IActionResult GenerateLicense()
{

View File

@ -44,6 +44,7 @@ public enum Permission
Tools_ChargeBrainTreeCustomer,
Tools_PromoteAdmin,
Tools_PromoteProviderServiceUser,
Tools_GenerateLicenseFile,
Tools_ManageTaxRates,
Tools_ManageStripeSubscriptions,

View File

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Admin.Models;
public class PromoteProviderServiceUserModel
{
[Required]
[Display(Name = "Provider Service User Id")]
public Guid? UserId { get; set; }
[Required]
[Display(Name = "Provider Id")]
public Guid? ProviderId { get; set; }
}

View File

@ -45,6 +45,7 @@ public static class RolePermissionMapping
Permission.Provider_ResendEmailInvite,
Permission.Tools_ChargeBrainTreeCustomer,
Permission.Tools_PromoteAdmin,
Permission.Tools_PromoteProviderServiceUser,
Permission.Tools_GenerateLicenseFile,
Permission.Tools_ManageTaxRates,
Permission.Tools_ManageStripeSubscriptions
@ -91,6 +92,7 @@ public static class RolePermissionMapping
Permission.Provider_ResendEmailInvite,
Permission.Tools_ChargeBrainTreeCustomer,
Permission.Tools_PromoteAdmin,
Permission.Tools_PromoteProviderServiceUser,
Permission.Tools_GenerateLicenseFile,
Permission.Tools_ManageTaxRates,
Permission.Tools_ManageStripeSubscriptions,

View File

@ -1,8 +1,10 @@
@using Bit.Admin.Enums;
@using Bit.Core
@inject SignInManager<IdentityUser> SignInManager
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
@inject Bit.Admin.Services.IAccessControlService AccessControlService
@inject Bit.Core.Services.IFeatureService FeatureService
@{
var canViewUsers = AccessControlService.UserHasPermission(Permission.User_List_View);
@ -11,13 +13,15 @@
var canChargeBraintree = AccessControlService.UserHasPermission(Permission.Tools_ChargeBrainTreeCustomer);
var canCreateTransaction = AccessControlService.UserHasPermission(Permission.Tools_CreateEditTransaction);
var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin);
var canPromoteProviderServiceUser = FeatureService.IsEnabled(FeatureFlagKeys.PromoteProviderServiceUserTool) &&
AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser);
var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile);
var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates);
var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions);
var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents);
var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders);
var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin ||
var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canPromoteProviderServiceUser ||
canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions;
}
@ -91,6 +95,12 @@
Promote Admin
</a>
}
@if (canPromoteProviderServiceUser)
{
<a class="dropdown-item" asp-controller="Tools" asp-action="PromoteProviderServiceUser">
Promote Provider Service User
</a>
}
@if (canGenerateLicense)
{
<a class="dropdown-item" asp-controller="Tools" asp-action="GenerateLicense">

View File

@ -0,0 +1,25 @@
@model PromoteProviderServiceUserModel
@{
ViewData["Title"] = "Promote Provider Service User";
}
<h1>Promote Provider Service User</h1>
<form method="post">
<div asp-validation-summary="All" class="alert alert-danger"></div>
<div class="row">
<div class="col-md">
<div class="mb-3">
<label asp-for="UserId" class="form-label"></label>
<input type="text" class="form-control" asp-for="UserId">
</div>
</div>
<div class="col-md">
<div class="mb-3">
<label asp-for="ProviderId" class="form-label"></label>
<input type="text" class="form-control" asp-for="ProviderId">
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Promote Service User</button>
</form>

View File

@ -259,7 +259,7 @@ public class OrganizationsController : Controller
throw new BadRequestException("Managed user account cannot leave managing organization. Contact your organization administrator for additional details.");
}
await _removeOrganizationUserCommand.RemoveUserAsync(id, user.Id);
await _removeOrganizationUserCommand.UserLeaveAsync(id, user.Id);
}
[HttpDelete("{id}")]
@ -540,4 +540,17 @@ public class OrganizationsController : Controller
await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated);
return new OrganizationResponseModel(organization);
}
[HttpGet("{id}/plan-type")]
public async Task<PlanType> GetPlanType(string id)
{
var orgIdGuid = new Guid(id);
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
if (organization == null)
{
throw new NotFoundException();
}
return organization.PlanType;
}
}

View File

@ -6,7 +6,9 @@ using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Enums;
@ -42,7 +44,8 @@ public class OrganizationsController(
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
IReferenceEventService referenceEventService,
ISubscriberService subscriberService)
ISubscriberService subscriberService,
IOrganizationInstallationRepository organizationInstallationRepository)
: Controller
{
[HttpGet("{id:guid}/subscription")]
@ -97,6 +100,8 @@ public class OrganizationsController(
throw new NotFoundException();
}
await SaveOrganizationInstallationAsync(id, installationId);
return license;
}
@ -366,4 +371,24 @@ public class OrganizationsController(
return await organizationRepository.GetByIdAsync(id);
}
private async Task SaveOrganizationInstallationAsync(Guid organizationId, Guid installationId)
{
var organizationInstallation =
await organizationInstallationRepository.GetByInstallationIdAsync(installationId);
if (organizationInstallation == null)
{
await organizationInstallationRepository.CreateAsync(new OrganizationInstallation
{
OrganizationId = organizationId,
InstallationId = installationId
});
}
else if (organizationInstallation.OrganizationId == organizationId)
{
organizationInstallation.RevisionDate = DateTime.UtcNow;
await organizationInstallationRepository.ReplaceAsync(organizationInstallation);
}
}
}

View File

@ -50,4 +50,11 @@ public interface IRemoveOrganizationUserCommand
/// </returns>
Task<IEnumerable<(Guid OrganizationUserId, string ErrorMessage)>> RemoveUsersAsync(
Guid organizationId, IEnumerable<Guid> organizationUserIds, EventSystemUser eventSystemUser);
/// <summary>
/// Removes a user from an organization when they have left voluntarily. This should only be called by the same user who is being removed.
/// </summary>
/// <param name="organizationId">Organization to leave.</param>
/// <param name="userId">User to leave.</param>
Task UserLeaveAsync(Guid organizationId, Guid userId);
}

View File

@ -114,6 +114,16 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
return result.Select(r => (r.OrganizationUser.Id, r.ErrorMessage));
}
public async Task UserLeaveAsync(Guid organizationId, Guid userId)
{
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
ValidateRemoveUser(organizationId, organizationUser);
await RepositoryRemoveUserAsync(organizationUser, deletingUserId: null, eventSystemUser: null);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Left);
}
private void ValidateRemoveUser(Guid organizationId, OrganizationUser orgUser)
{
if (orgUser == null || orgUser.OrganizationId != organizationId)
@ -234,7 +244,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
await _organizationUserRepository.DeleteManyAsync(organizationUsersToRemove.Select(ou => ou.Id));
foreach (var orgUser in organizationUsersToRemove.Where(ou => ou.UserId.HasValue))
{
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId!.Value);
}
}

View File

@ -17,6 +17,7 @@ public class OrganizationDomainService : IOrganizationDomainService
private readonly TimeProvider _timeProvider;
private readonly ILogger<OrganizationDomainService> _logger;
private readonly IGlobalSettings _globalSettings;
private readonly IFeatureService _featureService;
public OrganizationDomainService(
IOrganizationDomainRepository domainRepository,
@ -26,7 +27,8 @@ public class OrganizationDomainService : IOrganizationDomainService
IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand,
TimeProvider timeProvider,
ILogger<OrganizationDomainService> logger,
IGlobalSettings globalSettings)
IGlobalSettings globalSettings,
IFeatureService featureService)
{
_domainRepository = domainRepository;
_organizationUserRepository = organizationUserRepository;
@ -36,6 +38,7 @@ public class OrganizationDomainService : IOrganizationDomainService
_timeProvider = timeProvider;
_logger = logger;
_globalSettings = globalSettings;
_featureService = featureService;
}
public async Task ValidateOrganizationsDomainAsync()
@ -90,8 +93,16 @@ public class OrganizationDomainService : IOrganizationDomainService
//Send email to administrators
if (adminEmails.Count > 0)
{
await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails,
domain.OrganizationId.ToString(), domain.DomainName);
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails,
domain.OrganizationId.ToString(), domain.DomainName);
}
else
{
await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails,
domain.OrganizationId.ToString(), domain.DomainName);
}
}
_logger.LogInformation(Constants.BypassFiltersEventId, "Expired domain: {domainName}", domain.DomainName);

View File

@ -14,6 +14,7 @@ using Bit.Core.Auth.Models.Business;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Services;
@ -1324,6 +1325,12 @@ public class OrganizationService : IOrganizationService
return (false, $"Seat limit has been reached.");
}
var subscription = await _paymentService.GetSubscriptionAsync(organization);
if (subscription?.Subscription?.Status == StripeConstants.SubscriptionStatus.Canceled)
{
return (false, "Cannot autoscale with a canceled subscription.");
}
return (true, failureReason);
}

View File

@ -0,0 +1,24 @@
using Bit.Core.Entities;
using Bit.Core.Utilities;
namespace Bit.Core.Billing.Entities;
#nullable enable
public class OrganizationInstallation : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public Guid InstallationId { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime? RevisionDate { get; set; }
public void SetNewId()
{
if (Id == default)
{
Id = CoreHelpers.GenerateComb();
}
}
}

View File

@ -0,0 +1,10 @@
using Bit.Core.Billing.Entities;
using Bit.Core.Repositories;
namespace Bit.Core.Billing.Repositories;
public interface IOrganizationInstallationRepository : IRepository<OrganizationInstallation, Guid>
{
Task<OrganizationInstallation> GetByInstallationIdAsync(Guid installationId);
Task<ICollection<OrganizationInstallation>> GetByOrganizationIdAsync(Guid organizationId);
}

View File

@ -144,7 +144,6 @@ public static class FeatureFlagKeys
public const string AccessIntelligence = "pm-13227-access-intelligence";
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises";
public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
public const string GeneratorToolsModernization = "generator-tools-modernization";
public const string NewDeviceVerification = "new-device-verification";
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
@ -158,7 +157,9 @@ public static class FeatureFlagKeys
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
public const string InlineMenuTotp = "inline-menu-totp";
public const string PM12443RemovePagingLogic = "pm-12443-remove-paging-logic";
public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor";
public const string PromoteProviderServiceUserTool = "pm-15128-promote-provider-service-user-tool";
public static List<string> GetAllKeys()
{
@ -174,7 +175,6 @@ public static class FeatureFlagKeys
return new Dictionary<string, string>()
{
{ DuoRedirect, "true" },
{ CipherKeyEncryption, "true" },
};
}
}

View File

@ -1,4 +1,5 @@
using Quartz;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Quartz.Spi;
namespace Bit.Core.Jobs;
@ -14,7 +15,8 @@ public class JobFactory : IJobFactory
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
return _container.GetService(bundle.JobDetail.JobType) as IJob;
var scope = _container.CreateScope();
return scope.ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob;
}
public void ReturnJob(IJob job)

View File

@ -0,0 +1,27 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;" valign="top">
The domain {{DomainName}} in your Bitwarden organization could not be claimed.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Check the corresponding record in your domain host. Then reclaim this domain in Bitwarden to use it for your organization.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
The domain will be removed from your organization in 7 days if it is not claimed.
</td>
</tr>
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
Manage Domains
</a>
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,10 @@
{{#>BasicTextLayout}}
The domain {{DomainName}} in your Bitwarden organization could not be claimed.
Check the corresponding record in your domain host. Then reclaim this domain in Bitwarden to use it for your organization.
The domain will be removed from your organization in 7 days if it is not claimed.
{{Url}}
{{/BasicTextLayout}}

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
@ -12,15 +14,18 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
private readonly IInstallationRepository _installationRepository;
private readonly IPaymentService _paymentService;
private readonly ILicensingService _licensingService;
private readonly IProviderRepository _providerRepository;
public CloudGetOrganizationLicenseQuery(
IInstallationRepository installationRepository,
IPaymentService paymentService,
ILicensingService licensingService)
ILicensingService licensingService,
IProviderRepository providerRepository)
{
_installationRepository = installationRepository;
_paymentService = paymentService;
_licensingService = licensingService;
_providerRepository = providerRepository;
}
public async Task<OrganizationLicense> GetLicenseAsync(Organization organization, Guid installationId,
@ -32,11 +37,22 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
throw new BadRequestException("Invalid installation id");
}
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization);
var subscriptionInfo = await GetSubscriptionAsync(organization);
return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version)
{
Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo)
};
}
private async Task<SubscriptionInfo> GetSubscriptionAsync(Organization organization)
{
if (organization is not { Status: OrganizationStatusType.Managed })
{
return await _paymentService.GetSubscriptionAsync(organization);
}
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
return await _paymentService.GetSubscriptionAsync(provider);
}
}

View File

@ -85,6 +85,7 @@ public interface IMailService
Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip);
Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip);
Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier);

View File

@ -1068,6 +1068,19 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
{
var message = CreateDefaultMessage("Domain not claimed", adminEmails);
var model = new OrganizationDomainUnverifiedViewModel
{
Url = $"{_globalSettings.BaseServiceUri.VaultWithHash}/organizations/{organizationId}/settings/domain-verification",
DomainName = domainName
};
await AddMessageContentAsync(message, "OrganizationDomainUnclaimed", model);
message.Category = "UnclaimedOrganizationDomain";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount,
IEnumerable<string> ownerEmails)
{

View File

@ -273,6 +273,11 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
public Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
{
return Task.FromResult(0);
}
public Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount,
IEnumerable<string> ownerEmails)
{

View File

@ -0,0 +1,39 @@
using System.Data;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
using Dapper;
using Microsoft.Data.SqlClient;
namespace Bit.Infrastructure.Dapper.Billing.Repositories;
public class OrganizationInstallationRepository(
GlobalSettings globalSettings) : Repository<OrganizationInstallation, Guid>(
globalSettings.SqlServer.ConnectionString,
globalSettings.SqlServer.ReadOnlyConnectionString), IOrganizationInstallationRepository
{
public async Task<OrganizationInstallation> GetByInstallationIdAsync(Guid installationId)
{
var sqlConnection = new SqlConnection(ConnectionString);
var results = await sqlConnection.QueryAsync<OrganizationInstallation>(
"[dbo].[OrganizationInstallation_ReadByInstallationId]",
new { InstallationId = installationId },
commandType: CommandType.StoredProcedure);
return results.FirstOrDefault();
}
public async Task<ICollection<OrganizationInstallation>> GetByOrganizationIdAsync(Guid organizationId)
{
var sqlConnection = new SqlConnection(ConnectionString);
var results = await sqlConnection.QueryAsync<OrganizationInstallation>(
"[dbo].[OrganizationInstallation_ReadByOrganizationId]",
new { OrganizationId = organizationId },
commandType: CommandType.StoredProcedure);
return results.ToArray();
}
}

View File

@ -63,6 +63,7 @@ public static class DapperServiceCollectionExtensions
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
if (selfHosted)
{

View File

@ -0,0 +1,29 @@
using Bit.Infrastructure.EntityFramework.Billing.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Bit.Infrastructure.EntityFramework.Billing.Configurations;
public class OrganizationInstallationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationInstallation>
{
public void Configure(EntityTypeBuilder<OrganizationInstallation> builder)
{
builder
.Property(oi => oi.Id)
.ValueGeneratedNever();
builder
.HasKey(oi => oi.Id)
.IsClustered();
builder
.HasIndex(oi => oi.OrganizationId)
.IsClustered(false);
builder
.HasIndex(oi => oi.InstallationId)
.IsClustered(false);
builder.ToTable(nameof(OrganizationInstallation));
}
}

View File

@ -0,0 +1,19 @@
using AutoMapper;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.Models;
namespace Bit.Infrastructure.EntityFramework.Billing.Models;
public class OrganizationInstallation : Core.Billing.Entities.OrganizationInstallation
{
public virtual Installation Installation { get; set; }
public virtual Organization Organization { get; set; }
}
public class OrganizationInstallationMapperProfile : Profile
{
public OrganizationInstallationMapperProfile()
{
CreateMap<Core.Billing.Entities.OrganizationInstallation, OrganizationInstallation>().ReverseMap();
}
}

View File

@ -0,0 +1,45 @@
using AutoMapper;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using EFOrganizationInstallation = Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation;
namespace Bit.Infrastructure.EntityFramework.Billing.Repositories;
public class OrganizationInstallationRepository(
IMapper mapper,
IServiceScopeFactory serviceScopeFactory) : Repository<OrganizationInstallation, EFOrganizationInstallation, Guid>(
serviceScopeFactory,
mapper,
context => context.OrganizationInstallations), IOrganizationInstallationRepository
{
public async Task<OrganizationInstallation> GetByInstallationIdAsync(Guid installationId)
{
using var serviceScope = ServiceScopeFactory.CreateScope();
var databaseContext = GetDatabaseContext(serviceScope);
var query =
from organizationInstallation in databaseContext.OrganizationInstallations
where organizationInstallation.Id == installationId
select organizationInstallation;
return await query.FirstOrDefaultAsync();
}
public async Task<ICollection<OrganizationInstallation>> GetByOrganizationIdAsync(Guid organizationId)
{
using var serviceScope = ServiceScopeFactory.CreateScope();
var databaseContext = GetDatabaseContext(serviceScope);
var query =
from organizationInstallation in databaseContext.OrganizationInstallations
where organizationInstallation.OrganizationId == organizationId
select organizationInstallation;
return await query.ToArrayAsync();
}
}

View File

@ -78,6 +78,7 @@ public class DatabaseContext : DbContext
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
public DbSet<SecurityTask> SecurityTasks { get; set; }
public DbSet<OrganizationInstallation> OrganizationInstallations { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{

View File

@ -0,0 +1,27 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@OrganizationId UNIQUEIDENTIFIER,
@InstallationId UNIQUEIDENTIFIER,
@CreationDate DATETIME2 (7),
@RevisionDate DATETIME2 (7) = NULL
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[OrganizationInstallation]
(
[Id],
[OrganizationId],
[InstallationId],
[CreationDate],
[RevisionDate]
)
VALUES
(
@Id,
@OrganizationId,
@InstallationId,
@CreationDate,
@RevisionDate
)
END

View File

@ -0,0 +1,12 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_DeleteById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
DELETE
FROM
[dbo].[OrganizationInstallation]
WHERE
[Id] = @Id
END

View File

@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationInstallationView]
WHERE
[Id] = @Id
END

View File

@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByInstallationId]
@InstallationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationInstallationView]
WHERE
[InstallationId] = @InstallationId
END

View File

@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationInstallationView]
WHERE
[OrganizationId] = @OrganizationId
END

View File

@ -0,0 +1,17 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_Update]
@Id UNIQUEIDENTIFIER OUTPUT,
@OrganizationId UNIQUEIDENTIFIER,
@InstallationId UNIQUEIDENTIFIER,
@CreationDate DATETIME2 (7),
@RevisionDate DATETIME2 (7)
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[OrganizationInstallation]
SET
[RevisionDate] = @RevisionDate
WHERE
[Id] = @Id
END

View File

@ -0,0 +1,18 @@
CREATE TABLE [dbo].[OrganizationInstallation] (
[Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
[InstallationId] UNIQUEIDENTIFIER NOT NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NULL,
CONSTRAINT [PK_OrganizationInstallation] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OrganizationInstallation_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_OrganizationInstallation_Installation] FOREIGN KEY ([InstallationId]) REFERENCES [dbo].[Installation] ([Id]) ON DELETE CASCADE
);
GO
CREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_OrganizationId]
ON [dbo].[OrganizationInstallation]([OrganizationId] ASC);
GO
CREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_InstallationId]
ON [dbo].[OrganizationInstallation]([InstallationId] ASC);

View File

@ -0,0 +1,6 @@
CREATE VIEW [dbo].[OrganizationInstallationView]
AS
SELECT
*
FROM
[dbo].[OrganizationInstallation];

View File

@ -130,7 +130,7 @@ public class OrganizationsControllerTests : IDisposable
Assert.Contains("Your organization's Single Sign-On settings prevent you from leaving.",
exception.Message);
await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default);
await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().UserLeaveAsync(default, default);
}
[Theory, AutoData]
@ -193,7 +193,7 @@ public class OrganizationsControllerTests : IDisposable
await _sut.Leave(orgId);
await _removeOrganizationUserCommand.Received(1).RemoveUserAsync(orgId, user.Id);
await _removeOrganizationUserCommand.Received(1).UserLeaveAsync(orgId, user.Id);
}
[Theory, AutoData]

View File

@ -10,6 +10,7 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
@ -47,6 +48,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IReferenceEventService _referenceEventService;
private readonly ISubscriberService _subscriberService;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IOrganizationInstallationRepository _organizationInstallationRepository;
private readonly OrganizationsController _sut;
@ -70,6 +72,7 @@ public class OrganizationsControllerTests : IDisposable
_referenceEventService = Substitute.For<IReferenceEventService>();
_subscriberService = Substitute.For<ISubscriberService>();
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
_organizationInstallationRepository = Substitute.For<IOrganizationInstallationRepository>();
_sut = new OrganizationsController(
_organizationRepository,
@ -85,7 +88,8 @@ public class OrganizationsControllerTests : IDisposable
_upgradeOrganizationPlanCommand,
_addSecretsManagerSubscriptionCommand,
_referenceEventService,
_subscriberService);
_subscriberService,
_organizationInstallationRepository);
}
public void Dispose()

View File

@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="Divergic.Logging.Xunit" Version="4.3.0" />
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="FluentAssertions" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageReference Include="xunit" Version="$(XUnitVersion)" />

View File

@ -202,6 +202,14 @@ public class RemoveOrganizationUserCommandTests
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)), true);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.DidNotReceiveWithAnyArgs()
.DeleteAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventAsync((OrganizationUser)default, default);
}
[Theory, BitAutoData]
@ -346,9 +354,7 @@ public class RemoveOrganizationUserCommandTests
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUserRepository
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)
.Returns(organizationUser);
@ -361,7 +367,13 @@ public class RemoveOrganizationUserCommandTests
await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.DeleteAsync(organizationUser);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
}
[Theory, BitAutoData]
@ -370,6 +382,14 @@ public class RemoveOrganizationUserCommandTests
{
// Act & Assert
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, userId));
await sutProvider.GetDependency<IOrganizationUserRepository>()
.DidNotReceiveWithAnyArgs()
.DeleteAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventAsync((OrganizationUser)default, default);
}
[Theory, BitAutoData]
@ -413,6 +433,14 @@ public class RemoveOrganizationUserCommandTests
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),
Arg.Any<bool>());
await sutProvider.GetDependency<IOrganizationUserRepository>()
.DidNotReceiveWithAnyArgs()
.DeleteAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventAsync((OrganizationUser)default, default);
}
[Theory, BitAutoData]
@ -424,6 +452,7 @@ public class RemoveOrganizationUserCommandTests
var sutProvider = SutProviderFactory();
var eventDate = sutProvider.GetDependency<FakeTimeProvider>().GetUtcNow().UtcDateTime;
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
var organizationUsers = new[] { orgUser1, orgUser2 };
var organizationUserIds = organizationUsers.Select(u => u.Id);
@ -774,6 +803,105 @@ public class RemoveOrganizationUserCommandTests
Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);
}
[Theory, BitAutoData]
public async Task UserLeave_Success(
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)
.Returns(organizationUser);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),
Arg.Any<bool>())
.Returns(true);
await sutProvider.Sut.UserLeaveAsync(organizationUser.OrganizationId, organizationUser.UserId.Value);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.DeleteAsync(organizationUser);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Left);
}
[Theory, BitAutoData]
public async Task UserLeave_NotFound_ThrowsException(SutProvider<RemoveOrganizationUserCommand> sutProvider,
Guid organizationId, Guid userId)
{
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UserLeaveAsync(organizationId, userId));
await sutProvider.GetDependency<IOrganizationUserRepository>()
.DidNotReceiveWithAnyArgs()
.DeleteAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventAsync((OrganizationUser)default, default);
}
[Theory, BitAutoData]
public async Task UserLeave_InvalidUser_ThrowsException(OrganizationUser organizationUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)
.Returns(organizationUser);
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UserLeaveAsync(Guid.NewGuid(), organizationUser.UserId.Value));
Assert.Contains(RemoveOrganizationUserCommand.UserNotFoundErrorMessage, exception.Message);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.DidNotReceiveWithAnyArgs()
.DeleteAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventAsync((OrganizationUser)default, default);
}
[Theory, BitAutoData]
public async Task UserLeave_RemovingLastOwner_ThrowsException(
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)
.Returns(organizationUser);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),
Arg.Any<bool>())
.Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UserLeaveAsync(organizationUser.OrganizationId, organizationUser.UserId.Value));
Assert.Contains(RemoveOrganizationUserCommand.RemoveLastConfirmedOwnerErrorMessage, exception.Message);
_ = sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.Received(1)
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),
Arg.Any<bool>());
await sutProvider.GetDependency<IOrganizationUserRepository>()
.DidNotReceiveWithAnyArgs()
.DeleteAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventAsync((OrganizationUser)default, default);
}
/// <summary>
/// Returns a new SutProvider with a FakeTimeProvider registered in the Sut.
/// </summary>

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -11,6 +13,7 @@ using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationLicenses;
@ -62,4 +65,34 @@ public class CloudGetOrganizationLicenseQueryTests
Assert.Equal(installationId, result.InstallationId);
Assert.Equal(licenseSignature, result.SignatureBytes);
}
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_MSPManagedOrganization_UsesProviderSubscription(SutProvider<CloudGetOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo,
byte[] licenseSignature, Provider provider)
{
organization.Status = OrganizationStatusType.Managed;
organization.ExpirationDate = null;
subInfo.Subscription = new SubscriptionInfo.BillingSubscription(new Subscription
{
CurrentPeriodStart = DateTime.UtcNow,
CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1)
});
installation.Enabled = true;
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
sutProvider.GetDependency<IProviderRepository>().GetByOrganizationIdAsync(organization.Id).Returns(provider);
sutProvider.GetDependency<IPaymentService>().GetSubscriptionAsync(provider).Returns(subInfo);
sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);
var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId);
Assert.Equal(LicenseType.Organization, result.LicenseType);
Assert.Equal(organization.Id, result.Id);
Assert.Equal(installationId, result.InstallationId);
Assert.Equal(licenseSignature, result.SignatureBytes);
Assert.Equal(DateTime.UtcNow.AddYears(1).Date, result.Expires!.Value.Date);
}
}

View File

@ -0,0 +1,158 @@
-- OrganizationInstallation
-- Table
IF OBJECT_ID('[dbo].[OrganizationInstallation]') IS NULL
BEGIN
CREATE TABLE [dbo].[OrganizationInstallation] (
[Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
[InstallationId] UNIQUEIDENTIFIER NOT NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NULL,
CONSTRAINT [PK_OrganizationInstallation] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OrganizationInstallation_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_OrganizationInstallation_Installation] FOREIGN KEY ([InstallationId]) REFERENCES [dbo].[Installation] ([Id]) ON DELETE CASCADE
);
END
GO
-- Indexes
IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationInstallation_OrganizationId')
BEGIN
CREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_OrganizationId]
ON [dbo].[OrganizationInstallation]([OrganizationId] ASC);
END
IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationInstallation_InstallationId')
BEGIN
CREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_InstallationId]
ON [dbo].[OrganizationInstallation]([InstallationId] ASC);
END
-- View
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationInstallationView')
BEGIN
DROP VIEW [dbo].[OrganizationInstallationView];
END
GO
CREATE VIEW [dbo].[OrganizationInstallationView]
AS
SELECT
*
FROM
[dbo].[OrganizationInstallation]
GO
-- Stored Procedures: Create
CREATE OR ALTER PROCEDURE [dbo].[OrganizationInstallation_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@OrganizationId UNIQUEIDENTIFIER,
@InstallationId UNIQUEIDENTIFIER,
@CreationDate DATETIME2 (7),
@RevisionDate DATETIME2 (7) = NULL
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[OrganizationInstallation]
(
[Id],
[OrganizationId],
[InstallationId],
[CreationDate],
[RevisionDate]
)
VALUES
(
@Id,
@OrganizationId,
@InstallationId,
@CreationDate,
@RevisionDate
)
END
GO
-- Stored Procedures: DeleteById
CREATE OR ALTER PROCEDURE [dbo].[OrganizationInstallation_DeleteById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
DELETE
FROM
[dbo].[OrganizationInstallation]
WHERE
[Id] = @Id
END
GO
-- Stored Procedures: ReadById
CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationInstallationView]
WHERE
[Id] = @Id
END
GO
-- Stored Procedures: ReadByInstallationId
CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByInstallationId]
@InstallationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationInstallationView]
WHERE
[InstallationId] = @InstallationId
END
GO
-- Stored Procedures: ReadByOrganizationId
CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationInstallationView]
WHERE
[OrganizationId] = @OrganizationId
END
GO
-- Stored Procedures: Update
CREATE PROCEDURE [dbo].[OrganizationInstallation_Update]
@Id UNIQUEIDENTIFIER OUTPUT,
@OrganizationId UNIQUEIDENTIFIER,
@InstallationId UNIQUEIDENTIFIER,
@CreationDate DATETIME2 (7),
@RevisionDate DATETIME2 (7)
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[OrganizationInstallation]
SET
[RevisionDate] = @RevisionDate
WHERE
[Id] = @Id
END
GO

View File

@ -9,7 +9,7 @@ internal class Program
}
[DefaultCommand]
public void Execute(
public int Execute(
[Operand(Description = "Database connection string")]
string databaseConnectionString,
[Option('r', "repeatable", Description = "Mark scripts as repeatable")]
@ -20,7 +20,11 @@ internal class Program
bool dryRun = false,
[Option("no-transaction", Description = "Run without adding transaction per script or all scripts")]
bool noTransactionMigration = false
) => MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun, noTransactionMigration);
)
{
return MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun, noTransactionMigration) ? 0 : -1;
}
private static bool MigrateDatabase(string databaseConnectionString,
bool repeatable = false, string folderName = "", bool dryRun = false, bool noTransactionMigration = false)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.MySqlMigrations.Migrations;
/// <inheritdoc />
public partial class AddTable_OrganizationInstallation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OrganizationInstallation",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
OrganizationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
InstallationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
RevisionDate = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OrganizationInstallation", x => x.Id);
table.ForeignKey(
name: "FK_OrganizationInstallation_Installation_InstallationId",
column: x => x.InstallationId,
principalTable: "Installation",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_OrganizationInstallation_Organization_OrganizationId",
column: x => x.OrganizationId,
principalTable: "Organization",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_OrganizationInstallation_InstallationId",
table: "OrganizationInstallation",
column: "InstallationId");
migrationBuilder.CreateIndex(
name: "IX_OrganizationInstallation_OrganizationId",
table: "OrganizationInstallation",
column: "OrganizationId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OrganizationInstallation");
}
}

View File

@ -737,6 +737,35 @@ namespace Bit.MySqlMigrations.Migrations
b.ToTable("ClientOrganizationMigrationRecord", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b =>
{
b.Property<Guid>("Id")
.HasColumnType("char(36)");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime(6)");
b.Property<Guid>("InstallationId")
.HasColumnType("char(36)");
b.Property<Guid>("OrganizationId")
.HasColumnType("char(36)");
b.Property<DateTime?>("RevisionDate")
.HasColumnType("datetime(6)");
b.HasKey("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("InstallationId")
.HasAnnotation("SqlServer:Clustered", false);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("OrganizationInstallation", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b =>
{
b.Property<Guid>("Id")
@ -2336,6 +2365,25 @@ namespace Bit.MySqlMigrations.Migrations
b.Navigation("User");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.Models.Installation", "Installation")
.WithMany()
.HasForeignKey("InstallationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Installation");
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.PostgresMigrations.Migrations;
/// <inheritdoc />
public partial class AddTable_OrganizationInstallation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OrganizationInstallation",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false),
InstallationId = table.Column<Guid>(type: "uuid", nullable: false),
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OrganizationInstallation", x => x.Id);
table.ForeignKey(
name: "FK_OrganizationInstallation_Installation_InstallationId",
column: x => x.InstallationId,
principalTable: "Installation",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_OrganizationInstallation_Organization_OrganizationId",
column: x => x.OrganizationId,
principalTable: "Organization",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_OrganizationInstallation_InstallationId",
table: "OrganizationInstallation",
column: "InstallationId");
migrationBuilder.CreateIndex(
name: "IX_OrganizationInstallation_OrganizationId",
table: "OrganizationInstallation",
column: "OrganizationId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OrganizationInstallation");
}
}

View File

@ -742,6 +742,35 @@ namespace Bit.PostgresMigrations.Migrations
b.ToTable("ClientOrganizationMigrationRecord", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<DateTime>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("InstallationId")
.HasColumnType("uuid");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<DateTime?>("RevisionDate")
.HasColumnType("timestamp with time zone");
b.HasKey("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("InstallationId")
.HasAnnotation("SqlServer:Clustered", false);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("OrganizationInstallation", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b =>
{
b.Property<Guid>("Id")
@ -2342,6 +2371,25 @@ namespace Bit.PostgresMigrations.Migrations
b.Navigation("User");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.Models.Installation", "Installation")
.WithMany()
.HasForeignKey("InstallationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Installation");
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.SqliteMigrations.Migrations;
/// <inheritdoc />
public partial class AddTable_OrganizationInstallation : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OrganizationInstallation",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
OrganizationId = table.Column<Guid>(type: "TEXT", nullable: false),
InstallationId = table.Column<Guid>(type: "TEXT", nullable: false),
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
RevisionDate = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OrganizationInstallation", x => x.Id);
table.ForeignKey(
name: "FK_OrganizationInstallation_Installation_InstallationId",
column: x => x.InstallationId,
principalTable: "Installation",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_OrganizationInstallation_Organization_OrganizationId",
column: x => x.OrganizationId,
principalTable: "Organization",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_OrganizationInstallation_InstallationId",
table: "OrganizationInstallation",
column: "InstallationId");
migrationBuilder.CreateIndex(
name: "IX_OrganizationInstallation_OrganizationId",
table: "OrganizationInstallation",
column: "OrganizationId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OrganizationInstallation");
}
}

View File

@ -726,6 +726,35 @@ namespace Bit.SqliteMigrations.Migrations
b.ToTable("ClientOrganizationMigrationRecord", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<Guid>("InstallationId")
.HasColumnType("TEXT");
b.Property<Guid>("OrganizationId")
.HasColumnType("TEXT");
b.Property<DateTime?>("RevisionDate")
.HasColumnType("TEXT");
b.HasKey("Id")
.HasAnnotation("SqlServer:Clustered", true);
b.HasIndex("InstallationId")
.HasAnnotation("SqlServer:Clustered", false);
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("OrganizationInstallation", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b =>
{
b.Property<Guid>("Id")
@ -2325,6 +2354,25 @@ namespace Bit.SqliteMigrations.Migrations
b.Navigation("User");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.Models.Installation", "Installation")
.WithMany()
.HasForeignKey("InstallationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Installation");
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider")